Compartilhar via


Padrão de solicitação/resposta assíncrona

Desacoplar o processamento de back-end de um host front-end quando o processamento de back-end precisa ser executado de forma assíncrona, mas o front-end precisa de uma resposta clara.

Contexto e problema

No desenvolvimento de aplicativos modernos, os aplicativos cliente geralmente dependem de APIs remotas para fornecer lógica de negócios e compor funcionalidades. Muitos aplicativos executam código em um navegador da Web e outros ambientes também hospedam o código do cliente. As APIs podem se relacionar diretamente com o aplicativo ou operar como serviços compartilhados de um serviço externo. A maioria das chamadas à API usa HTTP ou HTTPS e segue a semântica REST.

Na maioria dos casos, as APIs de um aplicativo cliente respondem em cerca de 100 milissegundos (ms) ou menos. Muitos fatores podem afetar a latência de resposta:

  • A pilha de hospedagem do aplicativo
  • Componentes de segurança
  • A localização geográfica relativa do chamador e do backend
  • Infra-estrutura de rede
  • Carga atual
  • O tamanho do payload da solicitação
  • Comprimento da fila de processamento
  • O tempo para o back-end processar a solicitação

Esses fatores podem adicionar latência à resposta. Você pode atenuar alguns fatores dimensionando o back-end. Outros fatores, como a infraestrutura de rede, estão fora do controle do desenvolvedor de aplicativos. A maioria das APIs responde rapidamente o suficiente para que a resposta retorne pela mesma conexão. O código do aplicativo pode fazer uma chamada de API síncrona de forma não desbloqueada para dar a aparência do processamento assíncrono. Recomendamos essa abordagem para operações associadas a entrada e saída (E/S).

Em alguns cenários, o back-end realiza tarefas que são prolongadas e levam alguns segundos. Em outros cenários, o back-end realiza tarefas de longa duração em segundo plano por minutos ou por períodos estendidos. Nesses casos, você não pode esperar que o trabalho seja concluído antes de enviar uma resposta. Essa situação pode criar um problema para padrões síncronos de solicitação-resposta. Para obter diretrizes sobre como projetar o processamento de back-end, consulte Trabalhos em segundo plano.

Algumas arquiteturas resolvem esse problema usando um agente de mensagens para separar os estágios de solicitação e resposta. Muitos sistemas conseguem essa separação por meio do Padrão de Nivelamento de Carga Baseado em Fila. Essa separação permite que o processo do cliente e a API de back-end sejam dimensionados de forma independente. Ele também introduz complexidade extra quando o cliente requer notificação de êxito, pois essa etapa também deve se tornar assíncrona.

Muitas das mesmas considerações que se aplicam a aplicativos cliente também se aplicam a chamadas de API REST de servidor para servidor em sistemas distribuídos, como em uma arquitetura de microsserviços.

Solução

Uma solução para o problema é usar a sondagem HTTP. A técnica de sondagem funciona bem para o código cliente quando os endpoints de callback não estão disponíveis ou quando conexões de longa duração adicionam muita complicação. Mesmo quando os retornos de chamada são possíveis, as bibliotecas e serviços extras necessários podem aumentar a complexidade.

As seguintes etapas descrevem a solução:

  • O aplicativo cliente faz uma chamada síncrona para a API para disparar uma operação de execução prolongada no back-end.

  • A API responde de forma síncrona o mais rápido possível. Ele retorna um código de status HTTP 202 (Aceito) para reconhecer que recebeu a solicitação de processamento.

    Observação

    A API deve validar a solicitação e a ação a ser executada antes de iniciar o processo de execução prolongada. Se a solicitação não for válida, responda imediatamente com um código de erro como HTTP 400 (Solicitação Incorreta).

  • A resposta inclui uma referência de URL que aponta para um endpoint que o cliente pode sondar para verificar o resultado da operação de execução prolongada.

  • A API descarrega o processamento para outro componente, como uma fila de mensagens.

  • Para cada chamada bem-sucedida para o endpoint de status, o endpoint retorna HTTP 200 (OK). Enquanto o trabalho está em progresso, o endpoint de status retorna um recurso que indica esse estado. O corpo da resposta de status deve incluir informações suficientes para o cliente entender o estado atual da operação.

    Quando o trabalho for concluído, o ponto de extremidade de status retornará um recurso que indica a conclusão ou redireciona para outra URL de recurso. Por exemplo, se a operação assíncrona criar um novo recurso, o endpoint de status redirecionará para a URL desse recurso.

O diagrama a seguir mostra um fluxo típico.

Diagrama que mostra o fluxo de solicitação e resposta para solicitações HTTP assíncronas.

  1. O cliente envia uma solicitação e recebe a resposta HTTP 202 (Aceita).

  2. O cliente envia uma solicitação HTTP GET ao endpoint de status. O trabalho ainda está pendente, portanto, a chamada retorna HTTP 200.

  3. Em algum momento, o trabalho é concluído e o endpoint de status retorna HTTP 303 (Veja Outro) para redirecionar ao recurso.

  4. O cliente busca o recurso na URL especificada.

Problemas e considerações

Considere os seguintes pontos ao decidir como implementar esse padrão:

  • Existem várias maneiras de implementar esse padrão por HTTP e os serviços upstream nem sempre usam a mesma semântica. Por exemplo, algumas implementações não usam um endpoint de status separado. Em vez disso, o cliente sonda diretamente a URL do recurso de destino e recebe HTTP 404 (Não Encontrado) até que o recurso seja criado. Essa resposta faz sentido porque o recurso genuinamente ainda não existe. No entanto, essa abordagem poderá ser ambígua se 404 também for retornado para IDs de solicitação inválidas. Um endpoint de status dedicado que retorna HTTP 200 com um corpo de status, conforme descrito neste padrão, evita essa ambiguidade.

  • Uma resposta HTTP 202 indica onde o cliente pesquisa e com que frequência. Ele deve incluir os cabeçalhos a seguir.

    Cabeçalho Descrição Observações
    Location Uma URL que o cliente sonda para obter um status de resposta Essa URL pode ser um token de assinatura de acesso compartilhado. O padrão de chave de manobrista funciona bem quando esse local precisa de controle de acesso. O padrão também se aplica quando é necessário transferir a sondagem de resposta para outro back-end.
    Retry-After A estimativa de quando o processamento será concluído Esse cabeçalho foi projetado para impedir que os clientes de sondagem enviem muitas solicitações para o back-end.

    Considere o comportamento esperado do cliente ao projetar essa resposta. Um cliente que você controla pode seguir esses valores de resposta exatamente. Os clientes que outras pessoas criarem, incluindo clientes criados usando ferramentas sem código ou de baixo código, como Azure Logic Apps, podem aplicar sua própria manipulação para HTTP 202.

  • Considere incluir os campos a seguir na resposta do endpoint de status.

    Campo Descrição Observações
    status O estado atual da operação, como pendente, em execução, bem-sucedida, com falha ou cancelada. Use um conjunto consistente e documentado de valores terminais e não terminais.
    createdAt A hora em que a operação foi aceita. Ajuda os clientes a detectar operações obsoletas ou abandonadas.
    lastUpdatedAt A hora em que o status foi atualizado pela última vez. Permite que os clientes distinguem uma operação paralisada de uma que está em andamento ativamente.
    percentComplete Um indicador de progresso opcional. Útil quando o back-end pode estimar significativamente o progresso.
    error Um objeto de erro estruturado quando o status é Falha. Considere usar o formato RFC 9457 para consistência.
  • Talvez seja necessário usar um proxy de processamento para ajustar os cabeçalhos de resposta ou o conteúdo, dependendo dos serviços subjacentes que você usa.

  • Se o ponto de extremidade de status for redirecionado após a conclusão, use HTTP 303 (Ver Outro). Um 303 instrui o cliente a emitir uma solicitação GET para a URL de redirecionamento, independentemente do método de solicitação original. Esse comportamento é a semântica correta para esse padrão porque o cliente está recuperando um recurso de resultado distinto, não reenviando a operação original. HTTP 302 (Encontrado) não garante uma alteração de método; alguns clientes reproduzem o método original no redirecionamento, o que pode causar efeitos colaterais não intencionais, como solicitações POST duplicadas.

  • Depois que o servidor processa a solicitação com êxito, o recurso especificado Location pelo cabeçalho retorna um código de status HTTP como 200, 201 (Criado) ou 204 (Sem Conteúdo).

  • Se ocorrer um erro durante o processamento, persista o erro na URL do recurso que o Location cabeçalho especifica e retorna um código de status 4xx desse recurso que corresponde à falha. Use um formato de erro estruturado, como RFC 9457 (Detalhes do Problema para APIs HTTP) para que os clientes possam analisar e lidar com falhas programaticamente.

  • O recurso de status e os resultados armazenados consomem armazenamento e computação. Defina uma política de manutenção para limpá-las após um período razoável e considere informar o período de retenção aos clientes por meio de um Expires cabeçalho na resposta de status.

  • As soluções não implementam esse padrão da mesma maneira e alguns serviços incluem cabeçalhos extras ou alternativos. Por exemplo, Azure Resource Manager usa uma variante modificada desse padrão. Para obter mais informações, consulte operações assíncronas do Resource Manager.

  • Clientes legados podem não suportar este padrão. Nesse caso, talvez seja necessário colocar uma fachada sobre a API assíncrona para ocultar o processamento assíncrono do cliente original. Por exemplo, os Aplicativos Lógicos dão suporte a esse padrão nativamente e você pode usá-lo como uma camada de integração entre uma API assíncrona e um cliente que faz chamadas síncronas. Para obter mais informações, consulte o comportamento assíncrono de solicitação-resposta nos Aplicativos Lógicos do Azure.

  • Em alguns cenários, pode ser desejável oferecer aos clientes uma maneira de cancelar uma solicitação de execução prolongada. Nesse caso, exponha uma operação DELETE no recurso de endpoint de status. Essa solicitação deve encaminhar uma instrução de cancelamento para o componente de processamento de back-end. Depois que o back-end manipular o cancelamento, ele deverá atualizar o recurso de status para refletir o estado cancelado. Esse processo ajuda a impedir que o trabalho incompleto consuma recursos indefinidamente. Considere se a operação dá suporte à reversão parcial ou se é melhor tratada como uma transação compensatória.

  • Considere exigir que os clientes forneçam uma chave de idempotência (por exemplo, em um cabeçalho de solicitação Idempotency-Key) ao enviar a solicitação inicial. Se o back-end receber uma chave duplicada, ele deverá retornar o recurso de status existente em vez de enfileirar um segundo item de trabalho. Essa abordagem protege contra falhas de rede que fazem com que o cliente repita um POST que o servidor já aceitou. É especialmente importante nesse padrão porque o cliente não tem como distinguir uma resposta perdida de uma solicitação que nunca foi recebida.

Observação

Esse padrão descreve a sondagem HTTP, em que o cliente emite periodicamente novas solicitações para verificar o status. A sondagem longa é uma técnica relacionada, mas distinta: o cliente envia uma solicitação e o servidor mantém a conexão aberta até que novos dados sejam disponibilizados ou ocorra um tempo limite. A sondagem longa reduz a latência de resposta em comparação com a sondagem periódica, mas introduz complexidade em relação ao gerenciamento de conexões e tempos limite.

Quando usar esse padrão

Use esse padrão quando:

  • Você trabalha com código cliente, como aplicativos de navegador, e essas restrições tornam difícil fornecer endpoints de callback, ou conexões de longa duração adicionam muita complexidade.

  • Você chama um serviço que usa apenas o protocolo HTTP e o serviço de retorno não pode enviar retornos de chamada devido a restrições de firewall no lado do cliente.

  • Você se integra a cargas de trabalho que não dão suporte a mecanismos de retorno de chamada modernos, como WebSockets ou webhooks.

O padrão pode não ser adequado nestes casos:

  • Você pode usar um serviço criado para notificações assíncronas, como Azure Event Grid.

  • É preciso transmitir as respostas ao cliente em tempo real. Considere Server-Sent Events (SSE), que fornecem um canal unidirecional de push leve e nativo de HTTP de servidor para cliente sem exigir que o cliente realize consultas.

  • O cliente precisa coletar muitos resultados e a latência desses resultados é importante. Em vez disso, considere um agente de mensagens.

  • Conexões de rede persistente do lado do servidor, como WebSockets ou SignalR, estão disponíveis. Você pode usar essas conexões para notificar o chamador do resultado.

  • O design de rede dá suporte a portas abertas para receber retornos de chamada assíncronos ou webhooks.

Design de carga de trabalho

Um arquiteto deve avaliar como pode implementar o padrão de Solicitação-Resposta Assíncrona no design de sua carga de trabalho para atender às metas e princípios mencionados nos pilares do Azure Well-Architected Framework.

Pilar Como esse padrão apoia os objetivos do pilar
A Eficiência de Desempenho ajuda sua carga de trabalho a atender com eficiência às demandas por meio de otimizações no dimensionamento, nos dados e no código. Você melhora a capacidade de resposta e a escalabilidade desassociando as fases de solicitação e resposta para processos que não exigem uma resposta imediata. Uma abordagem assíncrona aumenta a simultaneidade e permite que o agendamento do servidor funcione conforme a capacidade fica disponível.

- PE:05 Dimensionamento e particionamento
- PE:07 Código e infraestrutura

Assim como acontece com qualquer decisão de design, considere as compensações em relação às metas dos outros pilares que esse padrão pode introduzir.

Exemplo

O código a seguir mostra trechos de um aplicativo que usa Azure Functions para implementar esse padrão. Esta solução tem três funções:

  • O ponto de extremidade da API assíncrona
  • O endpoint de status
  • Uma função de back-end que toma itens de trabalho da fila e os executa

Diagrama da estrutura do padrão de Resposta à Solicitação Assíncrona em Funções.

GitHub logo. Este exemplo está disponível em GitHub.

A implementação usa a identidade gerenciada para autenticar no Barramento de Serviço do Azure e no Armazenamento de Blobs do Azure, o que evita o armazenamento de strings de conexão ou chaves de acesso. As dependências são registradas no Program.cs usando DefaultAzureCredential e injetadas por meio de construtores primários.

Função AsyncProcessingWorkAcceptor

A AsyncProcessingWorkAcceptor função implementa um ponto de extremidade que aceita o trabalho de um aplicativo cliente e o enfileira para processamento:

  • A função gera uma ID de solicitação e a adiciona como metadados à mensagem da fila.

  • A resposta HTTP inclui um Location cabeçalho que aponta para um endpoint de status e um Retry-After cabeçalho que sugere um intervalo de sondagem. A ID da solicitação aparece no caminho da URL.

public class AsyncProcessingWorkAcceptor(ServiceBusClient _serviceBusClient)
{
    [Function("AsyncProcessingWorkAcceptor")]
    public async Task<IActionResult> Run(
        [HttpTrigger(AuthorizationLevel.Anonymous, "post", Route = null)] HttpRequest req,
        [FromBody] CustomerPOCO customer)
    {
        if (string.IsNullOrEmpty(customer.id) || string.IsNullOrEmpty(customer.customername))
        {
            return new BadRequestResult();
        }

        string requestId = Guid.NewGuid().ToString();

        string statusUrl = $"https://{Environment.GetEnvironmentVariable("WEBSITE_HOSTNAME")}/api/RequestStatus/{requestId}";

        var messagePayload = JsonConvert.SerializeObject(customer);
        var message = new ServiceBusMessage(messagePayload);
        message.ApplicationProperties.Add("RequestGUID", requestId);
        message.ApplicationProperties.Add("RequestSubmittedAt", DateTime.UtcNow);
        message.ApplicationProperties.Add("RequestStatusURL", statusUrl);
        var sender = _serviceBusClient.CreateSender("outqueue");

        await sender.SendMessageAsync(message);

        req.HttpContext.Response.Headers["Retry-After"] = "5";

        return new AcceptedResult(statusUrl, null);
    }
}

Função AsyncProcessingBackgroundWorker

A AsyncProcessingBackgroundWorker função lê a operação da fila, processa-a com base no conteúdo da mensagem e grava o resultado em uma conta de armazenamento.

public class AsyncProcessingBackgroundWorker(BlobContainerClient _blobContainerClient)
{
    [Function("AsyncProcessingBackgroundWorker")]
    public async Task Run(
        [ServiceBusTrigger("outqueue", Connection = "ServiceBusConnection")] ServiceBusReceivedMessage message)
    {
        // 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 requestGuid = message.ApplicationProperties["RequestGUID"].ToString();
        string blobName = $"{requestGuid}.blobdata";

        var blobClient = _blobContainerClient.GetBlobClient(blobName);
        using (MemoryStream memoryStream = new MemoryStream())
        using (StreamWriter writer = new StreamWriter(memoryStream))
        {
            writer.Write(message.Body.ToString());
            writer.Flush();
            memoryStream.Position = 0;

            await blobClient.UploadAsync(memoryStream, overwrite: true);
        }
    }
}

Função AsyncOperationStatusChecker

A função AsyncOperationStatusChecker implementa o endpoint de status. Essa função verifica o status da solicitação:

  • Se a solicitação for concluída, a função retornará HTTP 303 (Ver Outros), redirecionando o cliente para uma URL com um valet key para o resultado.

  • Se a solicitação estiver pendente, a função retornará um código HTTP 200 que inclui o estado atual.

public class AsyncOperationStatusChecker(ILogger<AsyncOperationStatusChecker> _logger)
{
    [Function("AsyncOperationStatusChecker")]
    public async Task<IActionResult> Run(
        [HttpTrigger(AuthorizationLevel.Anonymous, "get", Route = "RequestStatus/{requestId}")] HttpRequest req,
        [BlobInput("data/{requestId}.blobdata", Connection = "DataStorage")] BlockBlobClient inputBlob, string requestId)
    {
        OnCompleteEnum OnComplete = Enum.Parse<OnCompleteEnum>(req.Query["OnComplete"].FirstOrDefault() ?? "Redirect");
        OnPendingEnum OnPending = Enum.Parse<OnPendingEnum>(req.Query["OnPending"].FirstOrDefault() ?? "OK");

        _logger.LogInformation("Received status request for {RequestId} - OnComplete {OnComplete} - OnPending {OnPending}",
            requestId, OnComplete, OnPending);

        // Check whether the blob exists.
        if (await inputBlob.ExistsAsync())
        {
            // If the blob exists, the function uses the OnComplete parameter to determine the next action.
            return await OnCompleted(OnComplete, inputBlob, requestId, req);
        }
        else
        {
            // If the blob doesn't exist, the function uses the OnPending parameter to determine the next action.
            switch (OnPending)
            {
                case OnPendingEnum.OK:
                    {
                        // Return an HTTP 200 status code.
                        return new OkObjectResult(new { status = "In progress", Location = rqs });
                    }

                case OnPendingEnum.Synchronous:
                    {
                        // Long polling example: hold the connection open and check for completion
                        // using exponential backoff. Time out after approximately one minute.
                        int backoff = 250;

                        while (!await inputBlob.ExistsAsync() && backoff < 64000)
                        {
                            _logger.LogInformation("Synchronous mode {RequestId} - retrying in {Backoff} ms", requestId, backoff);
                            backoff = backoff * 2;
                            await Task.Delay(backoff);
                        }

                        if (await inputBlob.ExistsAsync())
                        {
                            _logger.LogInformation("Synchronous mode {RequestId} - completed after {Backoff} ms", requestId, backoff);
                            return await OnCompleted(OnComplete, inputBlob, requestId, req);
                        }
                        else
                        {
                            _logger.LogInformation("Synchronous mode {RequestId} - NOT FOUND after timeout {Backoff} ms", requestId, backoff);
                            return new NotFoundResult();
                        }
                    }

                default:
                    {
                        throw new InvalidOperationException($"Unexpected value: {OnPending}");
                    }
            }
        }
    }

    private async Task<IActionResult> OnCompleted(OnCompleteEnum OnComplete, BlockBlobClient inputBlob, string requestId, HttpRequest req)
    {
        switch (OnComplete)
        {
            case OnCompleteEnum.Redirect:
                {
                    // Generate a user delegation SAS URI using managed identity credentials.
                    BlobServiceClient blobServiceClient = inputBlob.GetParentBlobContainerClient().GetParentBlobServiceClient();
                    var userDelegationKey = await blobServiceClient.GetUserDelegationKeyAsync(DateTimeOffset.UtcNow, DateTimeOffset.UtcNow.AddDays(7));

                    // Return 303 See Other to redirect the client to the result resource.
                    // GenerateUserDelegationSasUri is a custom helper; see the full implementation on GitHub.
                    req.HttpContext.Response.Headers.Location = GenerateUserDelegationSasUri(inputBlob, userDelegationKey);;
                    return new StatusCodeResult(StatusCodes.Status303SeeOther);
                }

            case OnCompleteEnum.Stream:
                {
                    // Download the file and return it directly to the caller.
                    // For larger files, use a stream to minimize RAM usage.
                    return new OkObjectResult(await inputBlob.DownloadContentAsync());
                }

            default:
                {
                    throw new InvalidOperationException($"Unexpected value: {OnComplete}");
                }
        }
    }
}

public enum OnCompleteEnum
{
    Redirect,
    Stream
}

public enum OnPendingEnum
{
    OK,
    Synchronous
}

Próximas Etapas