首页 > 解决方案 > 如何在 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;
    }

标签: c#restasp.net-mvc-5

解决方案


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


推荐阅读