Asynkront Request-Reply mönster
Frikoppla serverdelsbearbetning från en klientdelsvärd, där serverdelsbearbetning måste vara asynkron, men klientdelen fortfarande behöver ett klart svar.
Kontext och problem
I modern programutveckling är det normalt att klientprogram – ofta kod som körs i en webbklient (webbläsare) – är beroende av fjärranslutna API:er för att tillhandahålla affärslogik och skrivfunktioner. Dessa API:er kan vara direkt relaterade till programmet eller kan vara delade tjänster som tillhandahålls av en tredje part. Dessa API-anrop sker vanligtvis via HTTP(S)-protokollet och följer REST-semantik.
I de flesta fall är API:er för ett klientprogram utformade för att svara snabbt, i ordningen 100 ms eller mindre. Många faktorer kan påverka svarstiden för svar, inklusive:
- Ett programs värdstack.
- Säkerhetskomponenter.
- Den relativa geografiska platsen för anroparen och backend.
- Nätverksinfrastruktur.
- Aktuell inläsning.
- Storleken på nyttolasten för begäran.
- Bearbetning av kölängd.
- Tiden för backend att bearbeta begäran.
Alla dessa faktorer kan ge längre svarstider i svaret. Vissa kan minimeras genom att skala ut backend- Andra, till exempel nätverksinfrastruktur, är i stort sett utanför programutvecklarens kontroll. De flesta API:er kan svara tillräckligt snabbt för att svar ska komma tillbaka via samma anslutning. Programkod kan göra ett synkront API-anrop på ett icke-blockerande sätt, vilket ger upphov till asynkron bearbetning, vilket rekommenderas för I/O-bundna åtgärder.
I vissa fall kan dock arbetet som utförs av backend vara långvarigt, i sekunder eller vara en bakgrundsprocess som körs på några minuter eller till och med timmar. I så fall är det inte möjligt att vänta tills arbetet har slutförts innan det svarar på begäran. Den här situationen är ett potentiellt problem för alla synkrona mönster för begäran/svar.
Vissa arkitekturer löser det här problemet genom att använda en meddelandekoordinator för att separera faserna för begäran och svar. Den här separationen uppnås ofta med hjälp av det köbaserade belastningsutjämningsmönstret. Den här separationen kan tillåta att klientprocessen och backend-API:et skalas oberoende av varandra. Men den här separationen medför också ytterligare komplexitet när klienten kräver ett meddelande eftersom det här steget måste bli asynkront.
Många av de överväganden som diskuteras för klientprogram gäller även för server-till-server-REST API-anrop i distribuerade system – till exempel i en mikrotjänstarkitektur.
Lösning
En lösning på det här problemet är att använda HTTP-avsökning. Avsökning är användbart för kod på klientsidan eftersom det kan vara svårt att tillhandahålla återringningsslutpunkter eller använda långvariga anslutningar. Även om återanrop är möjliga kan de extra bibliotek och tjänster som krävs ibland lägga till för mycket extra komplexitet.
Klientprogrammet gör ett synkront anrop till API:et, vilket utlöser en långvarig åtgärd på backend-datorn.
API:et svarar synkront så snabbt som möjligt. Den returnerar en HTTP 202-statuskod (accepterad) som bekräftar att begäran har tagits emot för bearbetning.
Anteckning
API:et bör verifiera både begäran och den åtgärd som ska utföras innan den långvariga processen startas. Om begäran är ogiltig svarar du omedelbart med en felkod, till exempel HTTP 400 (felaktig begäran).
Svaret innehåller en platsreferens som pekar på en slutpunkt som klienten kan avse för att söka efter resultatet av den långvariga åtgärden.
API:et avlastar bearbetningen till en annan komponent, till exempel en meddelandekö.
Medan arbetet fortfarande väntar returnerar statusslutpunkten HTTP 202. När arbetet är klart kan statusslutpunkten antingen returnera en resurs som anger att det är klart eller omdirigera till en annan resurs-URL. Om den asynkrona åtgärden till exempel skapar en ny resurs omdirigeras statusslutpunkten till url:en för den resursen.
Följande diagram visar ett typiskt flöde:

- Klienten skickar en begäran och får ett HTTP 202-svar (godkänt).
- Klienten skickar en HTTP GET-begäran till statusslutpunkten. Arbetet är fortfarande väntande, så det här anropet returnerar även HTTP 202.
- Vid något tillfälle är arbetet klart och statusslutpunkten returnerar 302 (hittades) som omdirigerar till resursen.
- Klienten hämtar resursen på den angivna URL:en.
Problem och överväganden
Det finns ett antal möjliga sätt att implementera det här mönstret via HTTP och alla överordnade tjänster har inte samma semantik. De flesta tjänster returnerar till exempel inte ett HTTP 202-svar från en GET-metod när en fjärrprocess inte har slutförts. Efter ren REST-semantik bör de returnera HTTP 404 (hittades inte). Det här svaret är logiskt när du överväger att resultatet av anropet inte finns ännu.
Ett HTTP 202-svar ska ange den plats och frekvens som klienten ska avse för svaret. Den bör ha följande ytterligare huvuden:
Huvud Beskrivning Kommentarer Location En URL som klienten ska avssöka efter svarsstatus. Den här URL:en kan vara en SAS-token där Valet-nyckelmönstret är lämpligt om den här platsen behöver åtkomstkontroll. Valet-nyckelmönstret är också giltigt när svarssökning behöver avlastas till en annan backend Retry-After En uppskattning av när bearbetningen slutförs Det här huvudet är utformat för att förhindra att avsökningsklienter överväldigar backend med återförsök. Du kan behöva använda en bearbetningsproxy eller fasad för att ändra svarshuvudena eller nyttolasten beroende på de underliggande tjänster som används.
Om statusslutpunkten omdirigeras när den är klar är HTTP 302 eller HTTP 303 lämpliga returkoder, beroende på den exakta semantik som du stöder.
Vid lyckad bearbetning ska resursen som anges av Location-huvudet returnera en lämplig HTTP-svarskod, till exempel 200 (OK), 201 (skapad) eller 204 (inget innehåll).
Om ett fel inträffar under bearbetningen beständiga du felet i resurs-URL:en som beskrivs i platshuvudet och returnerar helst en lämplig svarskod till klienten från resursen (4xx-kod).
Det är inte alla lösningar som implementerar det här mönstret på samma sätt och vissa tjänster innehåller ytterligare eller alternativa huvuden. Till exempel Azure Resource Manager en modifierad variant av det här mönstret. Mer information finns i Azure Resource Manager Async Operations.
Äldre klienter kanske inte stöder det här mönstret. I så fall kan du behöva placera en fasad över det asynkrona API:et för att dölja den asynkrona bearbetningen från den ursprungliga klienten. Till exempel Azure Logic Apps det här mönstret inbyggt kan användas som ett integreringslager mellan ett asynkront API och en klient som gör synkrona anrop. Se Utföra långvariga uppgifter med webhook-åtgärdsmönstret.
I vissa fall kanske du vill ge klienterna ett sätt att avbryta en långvarig begäran. I så fall måste backend-tjänsten ha stöd för någon form av annulleringsinstruktioner.
När du ska använda det här mönstret
Använd det här mönstret för:
Kod på klientsidan, till exempel webbläsarprogram, där det är svårt att tillhandahålla återringningsslutpunkter eller användningen av långvariga anslutningar ger för mycket ytterligare komplexitet.
Tjänstanrop där endast HTTP-protokollet är tillgängligt och returtjänsten inte kan skicka återanrop på grund av brandväggsbegränsningar på klientsidan.
Tjänstanrop som måste integreras med äldre arkitekturer som inte stöder modern återanropstekniker som WebSockets eller webhooks.
Det här mönstret kanske inte lämpar sig när:
- Du kan använda en tjänst som skapats för asynkrona meddelanden i stället, till exempel Azure Event Grid.
- Svar måste strömmas i realtid till klienten.
- Klienten måste samla in många resultat och den mottagna svarstiden för dessa resultat är viktig. Överväg ett Service Bus-mönster i stället.
- Du kan använda beständiga nätverksanslutningar på serversidan, till exempel WebSockets eller SignalR. Dessa tjänster kan användas för att meddela anroparen om resultatet.
- Med nätverksdesignen kan du öppna portar för att ta emot asynkrona återanrop eller webhooks.
Exempel
Följande kod visar utdrag från ett program som använder Azure Functions för att implementera det här mönstret. Det finns tre funktioner i lösningen:
- Den asynkrona API-slutpunkten.
- Statusslutpunkten.
- En backend-funktion som tar köade arbetsobjekt och kör dem.

exempellogotypen finns på GitHub.
Funktionen AsyncProcessingWorkAcceptor
Funktionen AsyncProcessingWorkAcceptor implementerar en slutpunkt som accepterar arbete från ett klientprogram och placerar det i en kö för bearbetning.
- Funktionen genererar ett begärande-ID och lägger till det som metadata i kömeddelandet.
- HTTP-svaret innehåller ett platshuvud som pekar på en statusslutpunkt. Begärande-ID:t är en del av URL-sökvägen.
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}");
}
}
Funktionen AsyncProcessingBackgroundWorker
Funktionen hämtar åtgärden från kön, gör en del arbete baserat på meddelandenyttolasten och skriver resultatet till AsyncProcessingBackgroundWorker SAS-signaturplatsen.
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);
}
}
Funktionen AsyncOperationStatusChecker
Funktionen AsyncOperationStatusChecker implementerar statusslutpunkten. Den här funktionen kontrollerar först om begäran har slutförts
- Om begäran slutfördes returnerar funktionen antingen en valet-key till svaret eller omdirigerar anropet omedelbart till valet-key-URL:en.
- Om begäran fortfarande är väntande bör vi returnera ett 202-svar som godkänts med ett själv refererande platshuvud, och lägga till en ETA för ett slutfört svar i http Retry-After-huvudet.
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
}
Nästa steg
Följande information kan vara relevant när du implementerar det här mönstret:
- Azure Logic Apps – Utför långvariga uppgifter med åtgärdsmönstret för avsökning.
- Allmänna metodtips när du utformar ett webb-API finns i Web API design ( Webb-API-design).