Ich habe ein Problem, bei dem der Dienst beim Initiieren einer REST-Ressource von einem Drittanbieter (Twilio) so schnell reagiert, dass wir keine Zeit haben, unsere SIDs in die Datenbank zu schreiben. Wir können dem Dienst nicht sagen, dass er warten soll, da er die SID nur zurückgibt, wenn der Dienst initiiert wurde. Die Anwendung selbst kann den Status nicht halten, da nicht garantiert werden kann, dass der RESTful-Rückruf dieselbe Instanz unserer Anwendung erreicht.

Wir haben das Problem gemindert, indem wir die SIDs in eine Puffertabelle in der Datenbank geschrieben haben, und wir haben einige Strategien ausprobiert, um die Webantwort zum Warten zu zwingen, aber die Verwendung von Thread.Sleep scheint andere nicht verwandte Webantworten zu blockieren und allgemein zu verlangsamen der Server während der Spitzenlast.

Wie kann ich eine Webantwort ordnungsgemäß bitten, eine Minute zu warten, während wir die Datenbank überprüfen? Am besten ohne den gesamten Server mit blockierten Threads zu verkleiden.

Dies ist der Code, der den Dienst initiiert:

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

    }

Dies ist der Code, der antwortet:

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

Dies ist der Code, der verzögert:

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;
    }
2
Scuba Steve 17 Jän. 2019 im 22:24

3 Antworten

Beste Antwort

Mit Good APIs ™ kann der Verbraucher eine ID angeben, mit der seine Nachricht verknüpft werden soll. Ich habe Twilio selbst noch nie verwendet, aber ich habe die API-Referenz für Erstellen einer Nachrichtenressource jetzt, und leider scheinen sie keinen Parameter dafür bereitzustellen. Aber es gibt noch Hoffnung!

Mögliche Lösung (bevorzugt)

Auch wenn es keinen expliziten Parameter dafür gibt, können Sie möglicherweise für jede von Ihnen erstellte Nachricht leicht unterschiedliche Rückruf-URLs angeben. Angenommen, Ihre CallQueue Entitäten haben eine eindeutige Id -Eigenschaft, könnten Sie die Rückruf-URL für jede Nachricht einen Abfragezeichenfolgenparameter enthalten lassen, der diese ID angibt. Dann können Sie die Rückrufe bearbeiten, ohne die Nachricht Sid zu kennen.

Damit dies funktioniert, ordnen Sie die Dinge in der Methode SendSMS neu an, sodass Sie die Entität CallQueue speichern, bevor Sie die Twilio-API aufrufen:

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

Sie würden auch den Callback-Handler TwilioSMSCallback so ändern, dass er die Entität CallQueue anhand ihrer ID sucht, die er aus dem Querystring-Parameter cq_id abruft.

Lösung, die fast garantiert funktioniert (aber mehr Arbeit erfordert)

Einige Cloud-Dienste erlauben nur Rückruf-URLs, die genau mit einem der Einträge in einer vorkonfigurierten Liste übereinstimmen. Bei solchen Diensten funktioniert der Ansatz mit unterschiedlichen Rückruf-URLs nicht. Wenn dies bei Twilio der Fall ist, sollten Sie in der Lage sein, Ihr Problem mithilfe der folgenden Idee zu lösen.

Im Vergleich zum anderen Ansatz erfordert dieser größere Änderungen an Ihrem Code, daher werde ich nur eine kurze Beschreibung geben und Sie die Details herausarbeiten lassen.

Die Idee ist, die TwilioSMSCallback -Methode auch dann funktionsfähig zu machen, wenn die CallQueue -Entität noch nicht in der Datenbank vorhanden ist:

  • Wenn die Datenbank keine übereinstimmende CallQueue Entität enthält, sollte TwilioSMSCallback die Aktualisierung des Status der empfangenen Nachricht nur in einem neuen Entitätstyp MessageStatusUpdate speichern, damit sie später behandelt werden kann.

  • "Später" steht ganz am Ende von SendSMS: Hier fügen Sie Code hinzu, um nicht behandelte MessageStatusUpdate Entitäten mit übereinstimmenden twilio_sid abzurufen und zu verarbeiten.

  • Der Code, der die Aktualisierung des Nachrichtenstatus tatsächlich verarbeitet (Aktualisieren des zugehörigen Shift_Offer usw.), sollte von TwilioSMSCallback entfernt und in eine separate Methode gestellt werden, die auch über den neuen Code am aufgerufen werden kann Ende von SendSMS.

Bei diesem Ansatz müssten Sie auch eine Art Sperrmechanismus einführen, um Race-Bedingungen zwischen mehreren Threads / Prozessen zu vermeiden, die versuchen, Aktualisierungen für dasselbe twilio_sid zu verarbeiten.

3
mbj 17 Jän. 2019 im 23:20

Sie sollten einen RESTful-Anruf wirklich nicht verzögern. Machen Sie es zu einer zweistufigen Operation, eine zum Starten und eine zum Abrufen des Status. Letzteres können Sie mehrmals anrufen, bis der Vorgang sicher abgeschlossen ist, ist leicht und ermöglicht auf Wunsch auch eine Fortschrittsanzeige oder eine Statusrückmeldung an den Anrufer.

1
Johannes Schidlowski 17 Jän. 2019 im 19:33

Async / await kann Ihnen helfen, Ihre Threads nicht zu blockieren.

Sie können await Task.Delay(...).ConfigureAwait(false) anstelle von Thread.Sleep() ausprobieren

UPDATE

Ich sehe, dass Sie in TwilioSMSCallback eine lang laufende Logik haben, und ich glaube, dieser Rückruf sollte so schnell wie möglich ausgeführt werden, da er von Twilio-Diensten kommt (es kann Strafen geben).

Ich schlage vor, dass Sie Ihre SMS-Statusverarbeitungslogik an das Ende der SendSMS -Methode verschieben und dort die Datenbank mit async/await abfragen, bis Sie den SMS-Status erhalten. Dadurch bleibt jedoch die SendSMS -Anforderung auf der Anruferseite aktiv. Es ist daher besser, einen separaten Dienst zu haben, der die Datenbank abfragt und Ihre API aufruft, wenn sich etwas ändert.

-1
opewix 17 Jän. 2019 im 20:08