Asynchrone Request-Reply patroon
Ontkoppel de back-endverwerking van een front-endhost waar de back-endverwerking asynchroon moet zijn, maar de front-end nog steeds een duidelijke respons nodig heeft.
Context en probleem
Bij de ontwikkeling van moderne toepassingen is het normaal dat clienttoepassingen die worden uitgevoerd in een webclient (browser) vaak afhankelijk zijn van externe API's om bedrijfslogica en — — compose-functionaliteit te bieden. Deze API's kunnen rechtstreeks zijn gerelateerd aan de toepassing of kunnen gedeelde services zijn die worden geleverd door een derde partij. Deze API-aanroepen vinden doorgaans plaats via het HTTP(S)-protocol en volgen rest-semantiek.
In de meeste gevallen zijn API's voor een clienttoepassing ontworpen om snel te reageren, in de orde van 100 ms of minder. Veel factoren kunnen van invloed zijn op de reactielatentie, waaronder:
- De hostingstack van een toepassing.
- Beveiligingsonderdelen.
- De relatieve geografische locatie van de aanroeper en de back-end.
- Netwerkinfrastructuur.
- Huidige belasting.
- De grootte van de nettolading van de aanvraag.
- Wachtrijlengte verwerken.
- De tijd voor de back-end om de aanvraag te verwerken.
Elk van deze factoren kan latentie toevoegen aan het antwoord. Sommige kunnen worden beperkt door de back-out te schalen. Andere, zoals de netwerkinfrastructuur, zijn grotendeels buiten het beheer van de toepassingsontwikkelaar. De meeste API's kunnen snel genoeg reageren om reacties terug te laten komen via dezelfde verbinding. Toepassingscode kan een synchrone API-aanroep maken op een niet-blokkerende manier, waardoor het lijkt op asynchrone verwerking, wat wordt aanbevolen voor I/O-gebonden bewerkingen.
In sommige scenario's kan het werk dat door de back-end wordt uitgevoerd echter lang worden uitgevoerd, in de volgorde van seconden, of een achtergrondproces zijn dat in minuten of zelfs uren wordt uitgevoerd. In dat geval is het niet haalbaar om te wachten tot het werk is voltooid voordat op de aanvraag wordt gereageerd. Deze situatie is een mogelijk probleem voor een synchrone aanvraag-antwoordpatroon.
Sommige architecturen lossen dit probleem op door een berichtenbroker te gebruiken om de aanvraag- en antwoordfasen van elkaar te scheiden. Deze scheiding wordt vaak bereikt door gebruik te maken van het patroon Load Levelingop basis van wachtrijen. Door deze scheiding kunnen het clientproces en de back-end-API onafhankelijk worden geschaald. Maar deze scheiding brengt ook extra complexiteit met zich mee wanneer de client een melding over slagen nodig heeft, omdat deze stap asynchroon moet worden.
Veel van dezelfde overwegingen die worden besproken voor clienttoepassingen zijn ook van toepassing op server-naar-server-REST API in gedistribueerde systemen, bijvoorbeeld — in een microservicesarchitectuur.
Oplossing
Een oplossing voor dit probleem is het gebruik van HTTP-polling. Polling is handig voor code aan de clientzijde, omdat het lastig kan zijn om call-back-eindpunten te bieden of langlopende verbindingen te gebruiken. Zelfs wanneer callbacks mogelijk zijn, kunnen de extra bibliotheken en services die nodig zijn soms te veel extra complexiteit toevoegen.
De clienttoepassing maakt een synchrone aanroep naar de API, waardoor een langlopende bewerking op de back-up wordt uitgevoerd.
De API reageert zo snel mogelijk synchroon. Er wordt een HTTP 202-statuscode (Geaccepteerd) retourneert, waarin wordt bevestigt dat de aanvraag is ontvangen voor verwerking.
Notitie
De API moet zowel de aanvraag als de actie valideren die moet worden uitgevoerd voordat het langdurige proces wordt uitgevoerd. Als de aanvraag ongeldig is, antwoordt u onmiddellijk met een foutcode zoals HTTP 400 (Ongeldige aanvraag).
Het antwoord bevat een locatieverwijzing die verwijst naar een eindpunt dat de client kan peilen om het resultaat van de langdurige bewerking te controleren.
De API offloadt de verwerking naar een ander onderdeel, zoals een berichtenwachtrij.
Terwijl het werk nog in behandeling is, retourneert het status-eindpunt HTTP 202. Zodra het werk is voltooid, kan het status-eindpunt een resource retourneren die voltooiing aangeeft of omleiden naar een andere resource-URL. Als met de asynchrone bewerking bijvoorbeeld een nieuwe resource wordt gemaakt, wordt het status-eindpunt omgeleid naar de URL voor die resource.
In het volgende diagram ziet u een typische stroom:

- De client verzendt een aanvraag en ontvangt een HTTP 202-antwoord (geaccepteerd).
- De client verzendt een HTTP GET-aanvraag naar het status-eindpunt. Het werk is nog in behandeling, dus deze aanroep retourneert ook HTTP 202.
- Op een bepaald moment is het werk voltooid en retourneert het status-eindpunt 302 (Gevonden) omleiden naar de resource.
- De client haalt de resource op bij de opgegeven URL.
Problemen en overwegingen
Er zijn een aantal mogelijke manieren om dit patroon via HTTP te implementeren en niet alle upstream-services hebben dezelfde semantiek. De meeste services retourneren bijvoorbeeld geen HTTP 202-antwoord van een GET-methode wanneer een extern proces niet is voltooid. Na pure REST-semantiek moeten ze HTTP 404 (Niet gevonden) retourneren. Dit antwoord is logisch wanneer u overweegt dat het resultaat van de aanroep nog niet aanwezig is.
Een HTTP 202-antwoord moet de locatie en frequentie aangeven die de client moet peilen voor het antwoord. Deze moet de volgende extra headers hebben:
Header Description Notities Locatie Een URL die de client moet peilen naar een antwoordstatus. Deze URL kan een SAS-token zijn met het valetsleutelpatroon dat geschikt is als deze locatie toegangsbeheer nodig heeft. Het valetsleutelpatroon is ook geldig wanneer polling van antwoorden offloading naar een andere back-end nodig heeft Retry-After Een schatting van wanneer de verwerking wordt voltooid Deze header is ontworpen om te voorkomen dat polling-clients de back-end overstelpen met nieuwe proberen. Mogelijk moet u een verwerkingsproxy of -façade gebruiken om de antwoordheaders of nettolading te bewerken, afhankelijk van de onderliggende services die worden gebruikt.
Als het status-eindpunt na voltooiing wordt omgeleid, zijn HTTP 302 of HTTP 303 de juiste retourcodes, afhankelijk van de exacte semantiek die u ondersteunt.
Als de verwerking is geslaagd, moet de resource die is opgegeven door de locatieheader een juiste HTTP-antwoordcode retourneren, zoals 200 (OK), 201 (gemaakt) of 204 (geen inhoud).
Als er een fout optreedt tijdens de verwerking, moet u de fout persistent maken op de resource-URL die wordt beschreven in de Location-header en idealiter een juiste antwoordcode van die resource (4xx-code) retourneren naar de client.
Niet alle oplossingen implementeren dit patroon op dezelfde manier en sommige services bevatten aanvullende of alternatieve headers. Een voorbeeld: Azure Resource Manager een gewijzigde variant van dit patroon gebruikt. Zie Async Operations Azure Resource Manager meer informatie.
Verouderde clients bieden mogelijk geen ondersteuning voor dit patroon. In dat geval moet u mogelijk een façade over de asynchrone API plaatsen om de asynchrone verwerking van de oorspronkelijke client te verbergen. Zo kan Azure Logic Apps dit patroon systeemeigen worden gebruikt als een integratielaag tussen een asynchrone API en een client die synchrone aanroepen doet. Zie Langlopende taken uitvoeren met het actiepatroon van de webhook.
In sommige scenario's wilt u mogelijk een manier bieden voor clients om een langlopende aanvraag te annuleren. In dat geval moet de back-endservice een vorm van annuleringsinstructie ondersteunen.
Wanneer dit patroon gebruiken
Gebruik dit patroon voor:
Code aan de clientzijde, zoals browsertoepassingen, waar het lastig is om call-back-eindpunten te bieden, of het gebruik van langlopende verbindingen voegt te veel extra complexiteit toe.
Service-aanroepen waarbij alleen het HTTP-protocol beschikbaar is en de retourservice geen callbacks kan afroepen vanwege firewallbeperkingen aan de clientzijde.
Service-aanroepen die moeten worden geïntegreerd met verouderde architecturen die geen ondersteuning bieden voor moderne callback-technologieën zoals WebSockets of webhooks.
Dit patroon is mogelijk niet geschikt wanneer:
- U kunt in plaats daarvan een service gebruiken die is gebouwd voor asynchrone meldingen, zoals Azure Event Grid.
- Antwoorden moeten in realtime naar de client worden gestreamd.
- De client moet veel resultaten verzamelen en de latentie van deze resultaten is belangrijk. Overweeg in plaats daarvan een Service Bus-patroon.
- U kunt permanente netwerkverbindingen aan de serverzijde gebruiken, zoals WebSockets of SignalR. Deze services kunnen worden gebruikt om de aanroeper op de hoogte te stellen van het resultaat.
- Met het netwerkontwerp kunt u poorten openen voor het ontvangen van asynchrone callbacks of webhooks.
Voorbeeld
De volgende code bevat fragmenten van een toepassing die gebruikmaakt van Azure Functions dit patroon te implementeren. Er zijn drie functies in de oplossing:
- Het asynchrone API-eindpunt.
- Het status-eindpunt.
- Een back-endfunctie die werkitems in de wachtrij neemt en uitvoert.

Dit voorbeeld is beschikbaar op GitHub.
<a name="asyncprocessingworkacceptor-function">De functie AsyncProcessingWorkAcceptor
De functie implementeert een eindpunt dat werk van een clienttoepassing accepteert en in een wachtrij plaatst AsyncProcessingWorkAcceptor voor verwerking.
- De functie genereert een aanvraag-id en voegt deze als metagegevens toe aan het wachtrijbericht.
- Het HTTP-antwoord bevat een locatieheader die verwijst naar een status-eindpunt. De aanvraag-id maakt deel uit van het URL-pad.
public static class AsyncProcessingWorkAcceptor
{
[FunctionName("AsyncProcessingWorkAcceptor")]
public static async Task<IActionResult> Run(
[HttpTrigger(AuthorizationLevel.Anonymous, "post", Route = null)] CustomerPOCO customer,
[ServiceBus("outqueue", Connection = "ServiceBusConnectionAppSetting")] IAsyncCollector<Message> OutMessage,
ILogger log)
{
if (String.IsNullOrEmpty(customer.id) || String.IsNullOrEmpty(customer.customername))
{
return new BadRequestResult();
}
string reqid = Guid.NewGuid().ToString();
string rqs = $"http://{Environment.GetEnvironmentVariable("WEBSITE_HOSTNAME")}/api/RequestStatus/{reqid}";
var messagePayload = JsonConvert.SerializeObject(customer);
Message m = new Message(Encoding.UTF8.GetBytes(messagePayload));
m.UserProperties["RequestGUID"] = reqid;
m.UserProperties["RequestSubmittedAt"] = DateTime.Now;
m.UserProperties["RequestStatusURL"] = rqs;
await OutMessage.AddAsync(m);
return (ActionResult) new AcceptedResult(rqs, $"Request Accepted for Processing{Environment.NewLine}ProxyStatus: {rqs}");
}
}
De functie AsyncProcessingBackgroundWorker
De functie haalt de bewerking op uit de wachtrij, doet wat werk op basis van de nettolading van het bericht en schrijft het resultaat AsyncProcessingBackgroundWorker naar de sas-handtekeninglocatie.
public static class AsyncProcessingBackgroundWorker
{
[FunctionName("AsyncProcessingBackgroundWorker")]
public static void Run(
[ServiceBusTrigger("outqueue", Connection = "ServiceBusConnectionAppSetting")]Message myQueueItem,
[Blob("data", FileAccess.ReadWrite, Connection = "StorageConnectionAppSetting")] CloudBlobContainer inputBlob,
ILogger log)
{
// Perform an actual action against the blob data source for the async readers to be able to check against.
// This is where your actual service worker processing will be performed.
var id = myQueueItem.UserProperties["RequestGUID"] as string;
CloudBlockBlob cbb = inputBlob.GetBlockBlobReference($"{id}.blobdata");
// Now write the results to blob storage.
cbb.UploadFromByteArrayAsync(myQueueItem.Body, 0, myQueueItem.Body.Length);
}
}
De functie AsyncOperationStatusChecker
De AsyncOperationStatusChecker functie implementeert het status-eindpunt. Met deze functie wordt eerst gecontroleerd of de aanvraag is voltooid
- Als de aanvraag is voltooid, retourneert de functie een valetsleutel naar het antwoord of wordt de aanroep onmiddellijk omgeleid naar de valetsleutel-URL.
- Als de aanvraag nog steeds in behandeling is, moeten we een 202 geaccepteerd retourneren met een locatieheader die naar zichzelf verwijst, en een ETA voor een voltooid antwoord in de http-Retry-After header.
public static class AsyncOperationStatusChecker
{
[FunctionName("AsyncOperationStatusChecker")]
public static async Task<IActionResult> Run(
[HttpTrigger(AuthorizationLevel.Anonymous, "get", Route = "RequestStatus/{thisGUID}")] HttpRequest req,
[Blob("data/{thisGuid}.blobdata", FileAccess.Read, Connection = "StorageConnectionAppSetting")] CloudBlockBlob inputBlob, string thisGUID,
ILogger log)
{
OnCompleteEnum OnComplete = Enum.Parse<OnCompleteEnum>(req.Query["OnComplete"].FirstOrDefault() ?? "Redirect");
OnPendingEnum OnPending = Enum.Parse<OnPendingEnum>(req.Query["OnPending"].FirstOrDefault() ?? "Accepted");
log.LogInformation($"C# HTTP trigger function processed a request for status on {thisGUID} - OnComplete {OnComplete} - OnPending {OnPending}");
// Check to see if the blob is present.
if (await inputBlob.ExistsAsync())
{
// If it's present, depending on the value of the optional "OnComplete" parameter choose what to do.
return await OnCompleted(OnComplete, inputBlob, thisGUID);
}
else
{
// If it's NOT present, check the optional "OnPending" parameter.
string rqs = $"http://{Environment.GetEnvironmentVariable("WEBSITE_HOSTNAME")}/api/RequestStatus/{thisGUID}";
switch (OnPending)
{
case OnPendingEnum.Accepted:
{
// Return an HTTP 202 status code.
return (ActionResult)new AcceptedResult() { Location = rqs };
}
case OnPendingEnum.Synchronous:
{
// Back off and retry. Time out if the backoff period hits one minute
int backoff = 250;
while (!await inputBlob.ExistsAsync() && backoff < 64000)
{
log.LogInformation($"Synchronous mode {thisGUID}.blob - retrying in {backoff} ms");
backoff = backoff * 2;
await Task.Delay(backoff);
}
if (await inputBlob.ExistsAsync())
{
log.LogInformation($"Synchronous Redirect mode {thisGUID}.blob - completed after {backoff} ms");
return await OnCompleted(OnComplete, inputBlob, thisGUID);
}
else
{
log.LogInformation($"Synchronous mode {thisGUID}.blob - NOT FOUND after timeout {backoff} ms");
return (ActionResult)new NotFoundResult();
}
}
default:
{
throw new InvalidOperationException($"Unexpected value: {OnPending}");
}
}
}
}
private static async Task<IActionResult> OnCompleted(OnCompleteEnum OnComplete, CloudBlockBlob inputBlob, string thisGUID)
{
switch (OnComplete)
{
case OnCompleteEnum.Redirect:
{
// Redirect to the SAS URI to blob storage
return (ActionResult)new RedirectResult(inputBlob.GenerateSASURI());
}
case OnCompleteEnum.Stream:
{
// Download the file and return it directly to the caller.
// For larger files, use a stream to minimize RAM usage.
return (ActionResult)new OkObjectResult(await inputBlob.DownloadTextAsync());
}
default:
{
throw new InvalidOperationException($"Unexpected value: {OnComplete}");
}
}
}
}
public enum OnCompleteEnum {
Redirect,
Stream
}
public enum OnPendingEnum {
Accepted,
Synchronous
}
Volgende stappen
De volgende informatie kan relevant zijn bij het implementeren van dit patroon:
- Azure Logic Apps: langlopende taken uitvoeren met het polling-actiepatroon.
- Zie Web-API-ontwerp voor algemene best practices bij het ontwerpen van een web-API.