繁体   English   中英

如何安全地延迟ASP.net中的Web响应?

[英]How do I safely delay a web response in ASP.net?

我有一个问题,当我们从第三方(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;
    }

良好的API™可以让使用者指定一个他们希望将其消息与之关联的ID。 我自己从未使用过Twilio,但我现在已经阅读了他们的API Reference,用于创建消息资源 ,可悲的是,似乎他们没有为此提供参数。 但是仍然有希望!

潜在的解决方案(首选)

即使没有明确的参数,也许您可​​以为创建的每条消息指定略有不同的回调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(从cq_id查询字符串参数中检索到)来查找CallQueue实体。

几乎可以保证工作的解决方案(但需要更多工作)

某些云服务仅允许与预配置列表中的条目之一完全匹配的回调URL。 对于此类服务,使用不同的回调URL的方法将不起作用。 如果Twilio是这种情况,那么您应该可以使用以下思路解决您的问题。

与另一种方法相比,此方法需要对代码进行较大的更改,因此,我仅作简要说明,让您确定详细信息。

这个想法是即使在数据库中还不存在CallQueue实体的情况下,也可以使TwilioSMSCallback方法起作用:

  • 如果数据库中没有匹配的CallQueue实体,则TwilioSMSCallback应该仅将接收到的消息状态更新存储在新的实体类型MessageStatusUpdate ,以便以后进行处理。

  • “最新”位于SendSMS :在这里,您将添加代码以获取和处理任何具有匹配twilio_sid未处理MessageStatusUpdate实体。

  • 实际处理消息状态更新(更新关联的Shift_Offer等)的代码应从TwilioSMSCallback并放在一个单独的方法中,该方法也可以在SendSMS结尾时从新代码中SendSMS

使用这种方法,您还必须引入某种锁定机制,以避免试图处理同一twilio_sid更新的多个线程/进程之间的竞争状况。

您确实不应该延迟RESTful调用。 将其分为两步,一个用于启动它,一个用于获取状态。 您可以多次调用后者,直到操作安全完成为止,该控件是轻量级的,并且如果需要,还可以向呼叫者提供进度指示器或状态反馈。

异步/等待可以帮助您避免阻塞线程。

您可以尝试await Task.Delay(...).ConfigureAwait(false)而不是Thread.Sleep()

UPDATE

我看到您在TwilioSMSCallback有一些长期运行的逻辑,并且我相信此回调应尽可能快地执行,因为它来自Twilio服务(可能会受到处罚)。

我建议您将短信状态处理逻辑移至SendSMS方法的末尾,并使用async/await轮询数据库,直到获得短信状态为止。 但是,这将使SendSMS请求在调用方保持活动状态,因此最好有单独的服务,该服务将轮询数据库并在发生更改时调用您的api。

暂无
暂无

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM