如何在 ASP.net 中安全地延迟 Web 响应?
How do I safely delay a web response in ASP.net?
我有一个问题,当我们从第三方 (Twilio) 启动 REST 资源时,服务响应太快,我们没有时间将我们的 SID 写入数据库。我们不能告诉服务等待,因为它 returns 只有在服务启动时才发送 SID。应用程序本身无法保持状态,因为无法保证 RESTful 回调会到达我们应用程序的同一实例。
我们已经通过将 SID 写入数据库中的缓冲区 table 来缓解这个问题,并且我们已经尝试了一些强制 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;
}
你真的不应该延迟 RESTful 电话。使其成为两步操作,一步用于启动它,一步用于获取状态。后者你可以多次调用,直到操作安全完成,它是轻量级的,如果你愿意的话,还允许向调用者提供进度指示器或状态反馈。
Async/await 可以帮助您不阻塞线程。
您可以尝试 await Task.Delay(...).ConfigureAwait(false)
而不是 Thread.Sleep()
更新
我看到你在 TwilioSMSCallback
中有一些很长的 运行 逻辑,我相信这个回调应该尽快执行,因为它来自 Twilio 服务(可能会有惩罚)。
我建议您将短信状态处理逻辑移动到 SendSMS
方法的末尾,并在那里使用 async/await
轮询数据库,直到您获得短信状态。然而,这将使 SendSMS
请求在调用方保持活动状态,因此最好有单独的服务,该服务将轮询数据库并在发生变化时调用您的 api。
好 APIs™ 让消费者指定一个 ID,他们希望他们的消息与之相关联。我自己从未使用过 Twilio,但我现在已经阅读了他们的 API Reference for Creating a Message Resource,遗憾的是他们似乎没有为此提供参数。但还是有希望的!
潜在解决方案(首选)
即使没有明确的参数,也许您可以为您创建的每条消息指定稍微不同的回调 URL?假设您的 CallQueue
实体具有唯一的 Id
属性,您可以让每条消息的回调 URL 包含指定此 ID 的查询字符串参数。然后你可以在不知道消息 Sid 的情况下处理回调。
要完成这项工作,您需要在 SendSMS
方法中重新排序,以便在调用 Twilio API 之前保存 CallQueue
实体:
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
,以便它通过 ID 查找 CallQueue
实体,它从 cq_id
querystring 参数中检索。
几乎可以保证有效的解决方案(但需要更多工作)
一些云服务只允许回调 URL 与预配置列表中的条目之一完全匹配。对于此类服务,具有不同回调 URLs 的方法将不起作用。如果 Twilio 是这种情况,那么您应该能够使用以下想法解决您的问题。
与其他方法相比,这种方法需要对您的代码进行较大的更改,因此我将只做一个简短的描述,让您了解细节。
想法是让 TwilioSMSCallback
方法工作,即使 CallQueue
实体在数据库中尚不存在:
如果数据库中没有匹配的CallQueue
实体,TwilioSMSCallback
应该只是将接收到的消息状态更新存储在新的实体类型MessageStatusUpdate
中,所以以后再处理。
"Later" 在 SendSMS
的最后:在这里,您将添加代码以获取和处理任何未处理的 MessageStatusUpdate
匹配 [=25] 的实体=].
实际处理消息状态更新(更新关联的 Shift_Offer
等)的代码应该从 TwilioSMSCallback
移开并放在单独的方法中也可以从 SendSMS
.
末尾的新代码中调用
使用这种方法,您还必须引入某种锁定机制来避免多个 threads/processes 尝试处理相同 twilio_sid
.
的更新之间的竞争条件
我有一个问题,当我们从第三方 (Twilio) 启动 REST 资源时,服务响应太快,我们没有时间将我们的 SID 写入数据库。我们不能告诉服务等待,因为它 returns 只有在服务启动时才发送 SID。应用程序本身无法保持状态,因为无法保证 RESTful 回调会到达我们应用程序的同一实例。
我们已经通过将 SID 写入数据库中的缓冲区 table 来缓解这个问题,并且我们已经尝试了一些强制 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;
}
你真的不应该延迟 RESTful 电话。使其成为两步操作,一步用于启动它,一步用于获取状态。后者你可以多次调用,直到操作安全完成,它是轻量级的,如果你愿意的话,还允许向调用者提供进度指示器或状态反馈。
Async/await 可以帮助您不阻塞线程。
您可以尝试 await Task.Delay(...).ConfigureAwait(false)
而不是 Thread.Sleep()
更新
我看到你在 TwilioSMSCallback
中有一些很长的 运行 逻辑,我相信这个回调应该尽快执行,因为它来自 Twilio 服务(可能会有惩罚)。
我建议您将短信状态处理逻辑移动到 SendSMS
方法的末尾,并在那里使用 async/await
轮询数据库,直到您获得短信状态。然而,这将使 SendSMS
请求在调用方保持活动状态,因此最好有单独的服务,该服务将轮询数据库并在发生变化时调用您的 api。
好 APIs™ 让消费者指定一个 ID,他们希望他们的消息与之相关联。我自己从未使用过 Twilio,但我现在已经阅读了他们的 API Reference for Creating a Message Resource,遗憾的是他们似乎没有为此提供参数。但还是有希望的!
潜在解决方案(首选)
即使没有明确的参数,也许您可以为您创建的每条消息指定稍微不同的回调 URL?假设您的 CallQueue
实体具有唯一的 Id
属性,您可以让每条消息的回调 URL 包含指定此 ID 的查询字符串参数。然后你可以在不知道消息 Sid 的情况下处理回调。
要完成这项工作,您需要在 SendSMS
方法中重新排序,以便在调用 Twilio API 之前保存 CallQueue
实体:
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
,以便它通过 ID 查找 CallQueue
实体,它从 cq_id
querystring 参数中检索。
几乎可以保证有效的解决方案(但需要更多工作)
一些云服务只允许回调 URL 与预配置列表中的条目之一完全匹配。对于此类服务,具有不同回调 URLs 的方法将不起作用。如果 Twilio 是这种情况,那么您应该能够使用以下想法解决您的问题。
与其他方法相比,这种方法需要对您的代码进行较大的更改,因此我将只做一个简短的描述,让您了解细节。
想法是让 TwilioSMSCallback
方法工作,即使 CallQueue
实体在数据库中尚不存在:
如果数据库中没有匹配的
CallQueue
实体,TwilioSMSCallback
应该只是将接收到的消息状态更新存储在新的实体类型MessageStatusUpdate
中,所以以后再处理。"Later" 在
SendSMS
的最后:在这里,您将添加代码以获取和处理任何未处理的MessageStatusUpdate
匹配 [=25] 的实体=].实际处理消息状态更新(更新关联的
Shift_Offer
等)的代码应该从TwilioSMSCallback
移开并放在单独的方法中也可以从SendSMS
. 末尾的新代码中调用
使用这种方法,您还必须引入某种锁定机制来避免多个 threads/processes 尝试处理相同 twilio_sid
.