c# - 如何在 ASP.net 中安全地延迟 Web 响应?
问题描述
我有一个问题,当我们从第三方(Twilio)启动 REST 资源时,服务响应如此之快,我们没有时间将 SID 写入数据库。我们不能告诉服务等待,因为它仅在服务启动时才返回 SID。应用程序本身无法保持状态,因为无法保证 RESTful 回调将到达我们应用程序的同一实例。
我们通过将 SID 写入数据库中的缓冲区表来缓解该问题,并且我们尝试了一些策略来强制 Web 响应等待,但使用 Thread.Sleep 似乎会阻止其他不相关的 Web 响应并且通常会减慢峰值负载期间的服务器。
在我们检查数据库时,如何优雅地要求 Web 响应暂停一分钟?最好不要用阻塞的线程弄乱整个服务器。
这是启动服务的代码:
private static void SendSMS(Shift_Offer so, Callout co,testdb2Entities5 db)
{
co.status = CalloutStatus.inprogress;
db.SaveChanges();
try
{
CallQueue cq = new CallQueue();
cq.offer_id = so.shift_offer_id;
cq.offer_finished = false;
string ShMessage = getNewShiftMessage(so, co, db);
so.offer_timestamp = DateTime.Now;
string ServiceSID = "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx";
var message = MessageResource.Create
(
body: ShMessage,
messagingServiceSid: ServiceSID,
to: new Twilio.Types.PhoneNumber(RCHStringHelpers.formatPhoneNumber(so.employee_phone_number)),
statusCallback: new Uri(TwilioCallBotController.SMSCallBackURL)
);
cq.twilio_sid = message.Sid;
db.CallQueues.Add(cq);
db.SaveChanges();
so.offer_status = ShiftOfferStatus.OfferInProgress;
so.status = message.Status.ToString();
so.twillio_sid = message.Sid;
db.SaveChanges();
}
catch (SqlException e) //if we run into any problems here, release the lock to prevent stalling;
//note to self - this should all be wrapped in a transaction and rolled back on error
{
Debug.WriteLine("Failure in CalloutManager.cs at method SendSMS: /n" +
"Callout Id: " + co.callout_id_pk + "/n"
+ "Shift Offer Id: " + so.shift_offer_id + "/n"
+ e.StackTrace);
ResetCalloutStatus(co, db);
ReleaseLock(co, db);
}
catch (Twilio.Exceptions.ApiException e)
{
ReleaseLock(co, db);
ResetCalloutStatus(co, db);
Debug.WriteLine(e.Message + "/n" + e.StackTrace);
}
}
这是响应的代码:
public ActionResult TwilioSMSCallback()
{
//invalid operation exception occurring here
string sid = Request.Form["SmsSid"];
string status = Request.Form["SmsStatus"];
Shift_Offer shoffer;
CallQueue cq = null;
List<Shift_Offer> sho = db.Shift_Offers.Where(s => s.twillio_sid == sid).ToList();
List<CallQueue> cqi = getCallQueueItems(sid, db);
if (sho.Count > 0)
{
shoffer = sho.First();
if (cqi.Count > 0)
{
cq = cqi.First();
}
}
else
{
if (cqi.Count > 0)
{
cq = cqi.First();
shoffer = db.Shift_Offers.Where(x => x.shift_offer_id == cq.offer_id).ToList().First();
}
else
{
return new Twilio.AspNet.Mvc.HttpStatusCodeResult(HttpStatusCode.NoContent);
}
}
Callout co = db.Callouts.Where(s => s.callout_id_pk == shoffer.callout_id_fk).ToList().First();
shoffer.status = status;
if (status.Contains("accepted"))
{
shoffer.offer_timestamp = DateTime.Now;
shoffer.offer_status = ShiftOfferStatus.SMSAccepted + " " + DateTime.Now;
}
else if (status.Contains("queued") || status.Contains("sending"))
{
shoffer.offer_timestamp = DateTime.Now;
shoffer.offer_status = ShiftOfferStatus.SMSSent + " " + DateTime.Now;
}
else if (status.Contains("delivered") || status.Contains("sent"))
{
shoffer.offer_timestamp = DateTime.Now;
shoffer.offer_status = ShiftOfferStatus.SMSDelivered + " " + DateTime.Now;
setStatus(co);
if (cq != null){
cq.offer_finished = true;
}
CalloutManager.ReleaseLock(co, db);
}
else if (status.Contains("undelivered"))
{
shoffer.offer_status = ShiftOfferStatus.Failed + " " + DateTime.Now;
setStatus(co);
if (cq != null){
cq.offer_finished = true;
}
CalloutManager.ReleaseLock(co, db);
}
else if (status.Contains("failed"))
{
shoffer.offer_status = ShiftOfferStatus.Failed + " " + DateTime.Now;
setStatus(co);
if (cq != null){
cq.offer_finished = true;
}
cq.offer_finished = true;
CalloutManager.ReleaseLock(co, db);
}
db.SaveChanges();
return new Twilio.AspNet.Mvc.HttpStatusCodeResult(HttpStatusCode.OK);
}
这是延迟的代码:
public static List<CallQueue> getCallQueueItems(string twilioSID, testdb2Entities5 db)
{
List<CallQueue> cqItems = new List<CallQueue>();
int retryCount = 0;
while (retryCount < 100)
{
cqItems = db.CallQueues.Where(x => x.twilio_sid == twilioSID).ToList();
if (cqItems.Count > 0)
{
return cqItems;
}
Thread.Sleep(100);
retryCount++;
}
return cqItems;
}
解决方案
Good APIs™ 让消费者指定一个他们希望与他们的消息相关联的 ID。我自己从未使用过 Twilio,但我现在已经阅读了他们的 API Reference for Creating a Message Resource,遗憾的是他们似乎没有为此提供参数。但是还是有希望的!
潜在解决方案(首选)
即使没有明确的参数,也许您可以为您创建的每条消息指定稍微不同的回调 URL?假设您的CallQueue
实体具有唯一Id
属性,您可以让每条消息的回调 URL 包含指定此 ID 的查询字符串参数。然后您可以在不知道消息 Sid 的情况下处理回调。
为了使这项工作正常进行,您将在SendSMS
方法中重新排序,以便CallQueue
在调用 Twilio API 之前保存实体:
db.CallQueues.Add(cq);
db.SaveChanges();
string queryStringParameter = "?cq_id=" + cq.id;
string callbackUrl = TwilioCallBotController.SMSCallBackURL + queryStringParameter;
var message = MessageResource.Create
(
[...]
statusCallback: new Uri(callbackUrl)
);
您还可以修改回调处理程序TwilioSMSCallback
,以便它CallQueue
通过它从cq_id
查询字符串参数中检索的 ID 来查找实体。
几乎可以保证工作的解决方案(但需要更多工作)
某些云服务仅允许与预配置列表中的条目之一完全匹配的回调 URL。对于此类服务,具有不同回调 URL 的方法将不起作用。如果 Twilio 是这种情况,那么您应该能够使用以下想法解决您的问题。
与另一种方法相比,这种方法需要对您的代码进行较大的更改,因此我将仅进行简要说明,并让您了解细节。
这个想法是让TwilioSMSCallback
方法工作,即使CallQueue
实体在数据库中尚不存在:
如果数据库中没有匹配
CallQueue
的实体,则TwilioSMSCallback
应该将接收到的消息状态更新存储在一个新的实体类型MessageStatusUpdate
中,以便以后处理。“稍后”位于 : 的最后
SendSMS
,您将添加代码以获取和处理任何未处理MessageStatusUpdate
的实体,并匹配twilio_sid
。实际处理消息状态更新(更新关联
Shift_Offer
的 等)的代码应移出TwilioSMSCallback
并放置在单独的方法中,该方法也可以从SendSMS
.
使用这种方法,您还必须引入某种锁定机制,以避免多个线程/进程之间试图处理相同的更新twilio_sid
。
推荐阅读
- angularjs - Angular JS:错误:意外请求:GET 模板/login.html
- postgresql - Golang GORM 搜索条件
- laravel - 如何使用 Eloquent 保存 OneToMany 关系
- python - 如何在不创建数据副本的情况下 pd.merge?
- python - Pandas - 根据日期差异返回 x 列
- c++ - syscall.MustLoadDll.MustFindProc 抛出“找不到指定的过程”
- elasticsearch - 用于弹性搜索的边缘 N-gram 中的连字符
- bash - 打开 tmux 终端的快捷方式
- ios - 线程 1:致命错误:NSArray 元素无法匹配 Swift Array 元素类型
- xamarin - 绑定文本未以 xamarin 形式显示