Как я могу безопасно отложить веб-ответ в ASP.net? - PullRequest
0 голосов
/ 17 января 2019

У меня проблема с тем, что когда мы инициируем ресурс REST от третьей стороны (Twilio), служба реагирует так быстро, что у нас нет времени записывать наши SID в базу данных. Мы не можем сказать службе ждать, так как она возвращает SID, только когда служба инициирована. Само приложение не может содержать состояние, поскольку нет гарантии, что обратный вызов RESTful достигнет того же экземпляра нашего приложения.

Мы уменьшили проблему, записав идентификаторы безопасности в буферную таблицу в базе данных, и мы попробовали несколько стратегий, чтобы заставить веб-ответ ждать, но использование Thread.Sleep, похоже, блокирует другие не связанные с ним веб-ответы и как правило, замедляет работу сервера во время пиковой нагрузки.

Как я могу изящно попросить веб-ответ подождать минуту, пока мы проверяем базу данных? Желательно, не засоряя весь сервер заблокированными потоками.

Это код, который инициирует услугу:

 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;
    }

Ответы [ 3 ]

0 голосов
/ 17 января 2019

Async / await может помочь вам не блокировать ваши темы.

Вы можете попробовать await Task.Delay(...).ConfigureAwait(false) вместо Thread.Sleep()

ОБНОВЛЕНИЕ

Я вижу, что у вас есть какая-то длительная логика в TwilioSMSCallback, и ясчитаю, что этот обратный вызов должен выполняться как можно быстрее, поскольку он поступает от сервисов Twilio (могут быть штрафы).

Я предлагаю вам перенести логику обработки статуса смс в конец SendSMS метода и базы данных опросатам с async/await, пока вы не получите статус смс.Это, однако, сохранит активный запрос SendSMS на стороне вызывающего абонента, поэтому лучше иметь отдельную службу, которая будет опрашивать базу данных и вызывать ваш API, когда что-то изменится.

0 голосов
/ 18 января 2019

Хорошие API ™ позволяют потребителю указывать идентификатор, с которым он хочет связать свое сообщение.Я никогда не использовал Twilio сам, но я прочитал их Справочник API для Создание ресурса сообщений сейчас, и, к сожалению, кажется, что они не предоставляют параметр для этого.Но все еще есть надежда!

Потенциальное решение (предпочтительно)

Даже если для него нет явного параметра, может быть, вы можете указать несколько разные URL-адреса обратного вызова для каждого создаваемого сообщения?Предполагая, что ваши CallQueue сущности имеют уникальное свойство Id, вы можете позволить URL-адресу обратного вызова для каждого сообщения содержать параметр строки запроса, указывающий этот идентификатор.Затем вы можете обрабатывать обратные вызовы, не зная Sid сообщения.

Чтобы сделать это, вам нужно изменить порядок вещей в методе SendSMS, чтобы сохранить сущность CallQueue перед вызовом API Twilio:

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.

Решение, которое почти гарантированно работает (но требует дополнительной работы)

Некоторые облачные службы допускают только URL-адреса обратного вызова, которые точно соответствуют одной из записей в предварительно настроенном списке.Для таких сервисов подход с различными URL-адресами обратного вызова не будет работать.Если это относится к Twilio, то вы сможете решить свою проблему, используя следующую идею.

По сравнению с другим подходом, этот требует больших изменений в вашем коде, поэтому я дам только краткое описание и позволю вам проработать детали.

Идея состоит в том, чтобы заставить метод TwilioSMSCallback работать, даже если сущность CallQueue еще не существует в базе данных:

  • Если не найдено совпадений CallQueue объект в базе данных, TwilioSMSCallback должен просто сохранить полученное обновление статуса сообщения в новом типе объекта MessageStatusUpdate, чтобы с ним можно было справиться позже.

  • "Позже"находится в самом конце SendSMS: здесь вы бы добавили код для извлечения и обработки любых необработанных MessageStatusUpdate сущностей с совпадающими twilio_sid.

  • Код, который фактически обрабатывает обновление состояния сообщения (обновление связанного Shift_Offer и т. Д.), Следует удалить из TwilioSMSCallback и поместить в отдельный метод, который также может бытьвызываться из нового кода в конце SendSMS.

При таком подходе вам также придется ввести какой-то механизм блокировки, чтобы избежать условий гонки между несколькими потоками / процессамипытается обработать обновления для того же twilio_sid.

0 голосов
/ 17 января 2019

Вы действительно не должны откладывать вызов RESTful. Сделайте это двухэтапной операцией, одна для запуска и одна для получения состояния. Последний, который вы можете вызывать более одного раза, до тех пор, пока операция не будет благополучно завершена, является легким и позволяет также при необходимости вызывать индикатор прогресса или обратную связь с вызывающим абонентом.

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...