2
votes

Comment retarder en toute sécurité une réponse Web dans ASP.net?

J'ai un problème où lorsque nous lançons une ressource REST à partir d'un tiers (Twilio), le service répond si rapidement que nous n'avons pas le temps d'écrire nos SID dans la base de données. Nous ne pouvons pas dire au service d'attendre, car il ne renvoie le SID que lorsque le service a démarré. L'application elle-même ne peut pas conserver l'état, car il n'y a aucune garantie que le rappel RESTful atteindra la même instance de notre application.

Nous avons atténué le problème en écrivant les SID dans une table tampon de la base de données, et nous J'ai essayé quelques stratégies pour forcer la réponse Web à attendre, mais l'utilisation de Thread.Sleep semble bloquer d'autres réponses Web indépendantes et ralentir généralement le serveur pendant la charge maximale.

Comment puis-je demander gracieusement à un site Web réponse à accrocher une minute pendant que nous vérifions la base de données? De préférence sans gommer tout le serveur avec des threads bloqués.

Voici le code qui lance le service:

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

Voici le code qui répond:

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

Voici le code qui retarde:

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

    }


9 commentaires

quoi? pourquoi revenez-vous avant que la base de données n'écrit le SID? Cette question est assez large sans poster de code,


Ce n'est pas nous qui retournons avant l'écriture de la base de données, c'est un service tiers.


Veuillez expliquer le problème en entier, ce que vous demandez autrement n'est pas clair. nous ne connaissons pas l'architecture de ce qui se passe. s'il vous plaît code postal et une explication complète et nous vous répondrons correctement


@ScubaSteve - cette modification n'a pas été utile. Veuillez publier un exemple minimal reproductible . Il est très difficile de répondre à une question qui ne contient pas de code.


Je vais éditer avec du code, attendez.


Je pense que je comprends. Le code commence dans SendSMS , cela crée une demande avec un identifiant généré à Twilio, appelle d'abord twilo puis enregistre l'identifiant généré. La réponse à l'appel arrive sur un point de terminaison d'API Web probablement hébergé sur un processus différent ou au moins un thread différent. Ça sonne pas?


^ ------ Si c'est correct: pourquoi ne pas sauvegarder l'id avant d'appeler le service Twilio (donc inverser l'ordre des appels)?


@Igor - L'ID est généré par le service lorsque le SMS est lancé et nous est retourné immédiatement. Les rappels qui suivent reviennent avec ce même SID généré lorsque le SMS est lancé. TL; DR - l'ID est généré en même temps que le service Twilio démarre


@ScubaSteve, il semble que vous soyez dépendant du cq.twilio_sid = message.Sid; mais il semble que ce soit le SID que vous lui passez. Si c'est quelque chose retourné par le service, alors je suppose que twilio l'inclurait dans la demande de rappel qui est faite à votre serveur êtes-vous sûr qu'il n'y a pas de PostParam / Header avec la valeur équivalente?


3 Réponses :


1
votes

Vous ne devriez vraiment pas retarder un appel RESTful. Faites-en une opération en 2 étapes, une pour le démarrer et une pour obtenir l'état. Ce dernier que vous pouvez appeler plus d'une fois, jusqu'à ce que l'opération soit terminée en toute sécurité, est léger et permet également un indicateur de progression ou un retour d'état à l'appelant, si vous le souhaitez.


10 commentaires

Oui, malheureusement, c'est un service tiers qui ne renvoie un SID qu'une fois que vous avez lancé le processus, sinon je suis d'accord. Démarrez le service, obtenez le SID, puis marquez le moment où nous sommes prêts à recevoir des rappels.


@ScubaSteve si vous souhaitez résoudre votre question, publiez toutes les informations pertinentes dans les questions, par exemple, il existe un service tiers, qui ne renvoie que le SID. Il est difficile de faire une réponse concise sinon


Un service Windows, non? Pourriez-vous en augmenter la priorité de processus afin de le forcer à répondre plus rapidement? Veuillez noter que cela peut avoir un effet négatif sur l'ensemble du serveur.


Non, un service RESTful tiers. Clarifié ci-dessus.


Ok, alors ... qu'en est-il d'écrire un script batch qui lit la requête à partir d'une base de données ou d'un fichier et effectue l'appel au service hors ligne? Si vous avez vos commentaires et votre échange sur la base de données, vous pouvez déléguer l'opération au système, tandis que l'application appelant utilise votre API RESTful pour obtenir des informations sur l'état. Je fais exactement la même chose pour les SMS sur les systèmes Linux.


@JohannesSchidlowski - Je ne suis pas sûr de suivre. Lorsque nous lançons le message SMS, le service tiers renvoie un SID. Lorsque le service nous fait des rappels d'état, il le fait avec le SID. Pour autant que je sache, nous n'avons pas la possibilité de faire tout cela hors ligne.


Je ne suis peut-être pas ce que vous suggérez, alors soyez indulgents avec moi.


1. Votre application appelle votre API et initialise le processus, renvoie un handle "aléatoire" à l'appelant et écrit une commande dans une table, ainsi que le handle. Par exemple, numéro de téléphone, titre, texte, état = 0 et le handle. 2. Votre serveur interroge ou reçoit une notification et exécute le code et met à jour l'état en conséquence 3. L'application appelant s'informe périodiquement de l'état.


@JohannesSchidlowski - Je suis désolé, je ne suis toujours pas ce que vous voulez dire. Êtes-vous en train de dire que c'est ce que fait mon application ou ce qu'elle devrait faire?


continuons cette discussion dans le chat .



-1
votes

Async / await peut vous aider à ne pas bloquer vos threads.

Vous pouvez essayer wait Task.Delay (...). ConfigureAwait (false) au lieu de Thread.Sleep()

<₹UPDATE

Je vois que vous avez une logique de longue date dans TwilioSMSCallback et je pense que ce rappel doit être exécuté aussi vite que possible car il provient des services Twilio (il peut y avoir des pénalités).

Je vous suggère de déplacer votre logique de traitement de l'état des SMS à la fin de la méthode SendSMS et d'y interroger la base de données avec async / await jusqu'à ce que vous obteniez l'état des SMS. Cependant, cela gardera la requête SendSMS active du côté de l'appelant, il est donc préférable d'avoir un service séparé qui interrogera la base de données et appellera votre API lorsque quelque chose change.


0 commentaires

3
votes

Les bonnes API ™ permettent au consommateur de spécifier un identifiant auquel il souhaite que son message soit associé. Je n'ai jamais utilisé Twilio moi-même, mais j'ai lu leur référence API pour Créer une ressource de message maintenant, et malheureusement, il semble qu'ils ne fournissent pas de paramètre pour cela. Mais il y a encore de l'espoir!

Solution potentielle (préférée)

Même s'il n'y a pas de paramètre explicite pour cela, peut-être pouvez-vous spécifier des URL de rappel légèrement différentes pour chaque message que vous créez? En supposant que vos entités CallQueue ont une propriété Id unique, vous pouvez laisser l'URL de rappel de chaque message contenir un paramètre de chaîne de requête spécifiant cet ID. Ensuite, vous pouvez gérer les rappels sans connaître le message Sid.

Pour que cela fonctionne, vous devez réorganiser les choses dans la méthode SendSMS afin de sauvegarder le CallQueue code> entité avant d'appeler l'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)
);

Vous devez également modifier le gestionnaire de rappel TwilioSMSCallback afin qu'il recherche le CallQueue par son ID, qu'il extrait du paramètre de chaîne de requête cq_id .

Solution dont le fonctionnement est presque garanti (mais nécessite plus de travail)

Certains services cloud n'autorisent que les URL de rappel qui correspondent exactement à l'une des entrées d'une liste préconfigurée. Pour de tels services, l'approche avec différentes URL de rappel ne fonctionnera pas. Si tel est le cas pour Twilio, vous devriez être en mesure de résoudre votre problème en utilisant l'idée suivante.

Par rapport à l'autre approche, celle-ci nécessite des modifications plus importantes de votre code, donc je ne donnerai qu'une brève description et vous laisserai travailler les détails.

L'idée est de faire fonctionner la méthode TwilioSMSCallback même si l'entité CallQueue n'existe pas encore dans la base de données:

  • S'il n'y a pas d'entité CallQueue correspondante dans la base de données, TwilioSMSCallback devrait simplement stocker la mise à jour de l'état du message reçu dans un nouveau type d'entité MessageStatusUpdate , afin qu'il puisse être traité plus tard.

  • "Plus tard" est à la toute fin de SendSMS : ici, vous ajouteriez du code pour récupérer et traiter toutes les entités MessageStatusUpdate non gérées avec correspondant twilio_sid .

  • Le code qui traite réellement la mise à jour de l'état du message (mise à jour du Shift_Offer associé, etc.) doit être éloigné de TwilioSMSCallback et placé dans un autre méthode qui peut également être appelée à partir du nouveau code à la fin de SendSMS.

Avec cette approche, vous devriez également introduire une sorte de mécanisme de verrouillage pour éviter les conditions de concurrence entre plusieurs threads / processus essayant de traiter les mises à jour pour le même twilio_sid . p >


4 commentaires

Ahhh, c'est intéressant. J'examinerai ceci.


Plus je regarde cette solution, plus je l'aime - nous allons certainement le faire.


@ScubaSteve: Merci! J'espère que Twilio autorise l'utilisation de paramètres de chaîne de requête. Sinon, vous devrez apporter d'autres modifications à votre code pour le faire fonctionner quel que soit le moment entre les mises à jour de la base de données dans SendSMS et le gestionnaire de rappel.


Je ne vois pas pourquoi ils ne le permettraient pas (de cette manière). Les URL sont entièrement spécifiées par nous. Nous devrions pouvoir lire cet appel d'URL avec le paramètre fourni lors de la création de la ressource. Il est peut-être possible de définir le paramètre dans la ressource elle-même, mais je vais devoir fouiller dans la documentation de référence de l'API pour le comprendre. Mais je ne vois pas pourquoi il y aurait un problème avec une URL numérotée.