Featured image of post Cancelando solicitações abandonadas no ASP.NET Core

Cancelando solicitações abandonadas no ASP.NET Core

Usando CancellationToken para ser notificado de solicitações abandonadas por clientes

Introdução Link to this section

Quando um cliente faz uma solicitação HTTP, o cliente pode abortar a solicitação, deixando o servidor processando se não estiver preparado para lidar com este cenário; desperdiçando seus recursos que poderiam ser usados para processar outros trabalhos.

Neste post, mostrarei como usar Cancellation Tokens para cancelar solicitações em execução que foram abortadas por clientes.

Tokens de cancelamento Link to this section

Não explicarei como os Tokens de cancelamento funcionam neste post. Recomendo ler este ótimo post de Mitesh Shah que explica como eles funcionam de uma forma simples.

Solicitações abortadas Link to this section

Existem dois cenários principais pelos quais as solicitações são abortadas pelo cliente:

  • Ao carregar uma página no navegador, um usuário pode clicar no botão parar para abortar a solicitação ou clicar no botão atualizar para recarregar a página.

  • Quando ocorre um tempo limite no lado do cliente. Um tempo limite acontece quando o tempo que o cliente está disposto a esperar pela resposta expira. Quando isso acontece, o cliente abandona a solicitação e retorna um erro. É uma boa prática ter um tempo limite configurado no lado do cliente ao fazer uma solicitação através da rede, para que não fique pendurado por muito tempo quando o servidor demorar muito para responder.

Por que parar de processar solicitações abandonadas no servidor? Link to this section

Primeiro, é um desperdício de recursos. Memória e CPU que poderiam ser usados para processar outras solicitações são usados para processar uma solicitação que já foi descartada pelo cliente. Este desperdício de recursos se estende a dependências, como bancos de dados e APIs que o sistema consome.

Segundo, a solicitação abandonada pode diminuir a velocidade de outras solicitações, competindo por recursos compartilhados, como tabelas de banco de dados.

🚨 Tenha cuidado com quais solicitações você cancela. Pode não ser uma boa ideia abortar uma solicitação que faz alterações no estado do sistema, mesmo que as solicitações sejam idempotentes, pois pode tornar o estado inconsistente. Pode ser melhor deixar a solicitação terminar.

Aqui estão boas práticas ao usar Tokens de Cancelamento. (Extraído do post de Andrew Arnott):

1 - Saiba quando você passou do ponto de não cancelamento. Não cancele se você já incorreu em efeitos colaterais que seu método não está preparado para reverter na saída que o deixaria em um estado inconsistente. Portanto, se você já fez algum trabalho e tem muito mais a fazer, e o token é cancelado, você deve cancelar apenas quando e se puder fazê-lo deixando os objetos em um estado válido. Isso pode significar que você tem que terminar a grande quantidade de trabalho ou desfazer todo o seu trabalho anterior (ou seja, reverter os efeitos colaterais) ou encontrar um lugar conveniente onde você possa parar no meio do caminho, mas em uma condição válida, antes de lançar OperationCanceledException. Em outras palavras, o chamador deve ser capaz de recuperar para um estado consistente conhecido após cancelar seu trabalho ou perceber que o cancelamento não foi respondido e que o chamador deve decidir se aceita o trabalho ou reverte sua conclusão bem-sucedida por conta própria.

2 - Propague seu CancellationToken para todos os métodos que você chama que aceitam um, exceto após o “ponto de não cancelamento” mencionado no ponto anterior. Na verdade, se o seu método orquestra principalmente chamadas para outros métodos que eles mesmos usam CancellationTokens, você pode descobrir que não precisa chamar CancellationToken.ThrowIfCancellationRequested() pessoalmente, já que os métodos assíncronos que você está chamando geralmente farão isso por você.

3 - Não lance OperationCanceledException depois de concluir o trabalho, só porque o token foi sinalizado. Retorne um resultado bem-sucedido e deixe o chamador decidir o que fazer a seguir. O chamador não pode presumir que você é cancelável em um determinado ponto de qualquer maneira, então eles têm que estar preparados para um resultado bem-sucedido, mesmo após o cancelamento.

4 - A validação de entrada certamente pode ir antes das verificações de cancelamento (já que isso ajuda a destacar bugs no código de chamada).

5 - Considere não verificar o token se seu trabalho for muito rápido ou você propagá-lo para os métodos que você chama. Dito isso, chamar CancellationToken.ThrowIfCancellationRequested() é bem leve, então não pense muito sobre isso, a menos que você veja isso em rastreamentos de desempenho.

6 - Verifique CancellationToken.CanBeCanceled quando você puder fazer seu trabalho de forma mais eficiente se puder presumir que nunca será cancelado. CanBeCanceled retorna falso para CancellationToken.None e, no futuro, possivelmente para outros casos também.

Cancelando as solicitações no ASP.NET Core Link to this section

1 - Adicione um objeto CancellationToken como um parâmetro no método Controller. O ASP.NET fornecerá o objeto através do model binding para nós.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
[ApiController]
[Route("[controller]")]
public class DogImageController : ControllerBase
{
    private readonly IDogImageUseCase _dogImageUseCase;
    private readonly ILogger<DogImageController> _logger;

    public DogImageController(IDogImageUseCase dogImageUseCase, ILogger<DogImageController> logger)
    {
        _dogImageUseCase = dogImageUseCase;
        _logger = logger;
    }

    [HttpGet]
    public async Task<ActionResult<string>> GetAsync(CancellationToken cancellationToken)
    {
        try
        {
            return await _dogImageUseCase.GetRandomDogImage(cancellationToken);
        }
        catch(TaskCanceledException ex)
        {
            _logger.LogError(ex, ex.Message);

            return StatusCode(StatusCodes.Status500InternalServerError, "Request cancelled");
        }
    }
}

2 - Passe o CancellationToken para todos os métodos assíncronos em seu código.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
public class DogImageUseCase: IDogImageUseCase
{
    private readonly IDogApi _dogApi;

    public DogImageUseCase(IDogApi dogApi)
    {
        _dogApi = dogApi;
    }

    public async Task<string> GetRandomDogImage(CancellationToken cancellationToken)
    {
        var dog = await _dogApi.GetRandomDog(cancellationToken);

        return dog.message;
    }
}

3 - O CancellationToken lançará um TaskCanceledException quando a solicitação for abortada. É importante registrar este erro para medir o quanto isso está acontecendo.

CancellationToken com Refit Link to this section

Refit é uma biblioteca .NET que facilita o consumo de APIs REST. Ele gera um cliente tipado baseado em uma interface. Mais detalhes aqui.

Para passar um CancellationToken para um cliente Refit como no exemplo acima, basta inserir um parâmetro CancellationToken na interface:

1
2
3
4
5
public interface IDogApi
{
    [Get("/breeds/image/random")]
    Task<Dog> GetRandomDog(CancellationToken cancellationToken);
}

⚠️ Por convenção, o CancellationToken deve ser o último parâmetro no método. Existe até um analisador estático para esta regra (CA1068: CancellationToken parameters must come last). Mais sobre analisadores estáticos neste post.

Testando a solução Link to this section

Podemos testar manualmente se a solução funciona inserindo um atraso em nosso código e fazendo uma solicitação através do navegador para a rota /DogImage.

1 - Insira um atraso de 2 minutos para que tenhamos tempo de parar a solicitação no lado do cliente. É necessário também passar o CancellationToken para o método Delay para que ele possa sair em um cancelamento de solicitação:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
public class DogImageUseCase: IDogImageUseCase
{
    private readonly IDogApi _dogApi;

    public DogImageUseCase(IDogApi dogApi)
    {
        _dogApi = dogApi;
    }

    public async Task<string> GetRandomDogImage(CancellationToken cancellationToken)
    {
        var dog = await _dogApi.GetRandomDog(cancellationToken);

        await Task.Delay(TimeSpan.FromMinutes(2), cancellationToken);

        return dog.message;
    }
}

2 - Insira um breakpoint dentro do bloco catch TaskCanceledException;

3 - Execute o aplicativo e abra o URL da rota no navegador (neste exemplo, /DogImage);

4 - Clique no botão parar do navegador ou pressione a tecla esc.

Atingiremos o breakpoint, confirmando que a solução está funcionando.

Problemas com esta solução Link to this section

  • O AWS API Gateway tem um tempo limite, mas não relata o evento de tempo limite para a função AWS lambda subjacente. Se ocorrer um tempo limite no API Gateway, os usuários receberão um erro de tempo limite, mas as métricas e os logs não mostrarão esses erros, fazendo parecer que não há problemas acontecendo.
  • Algumas bibliotecas de cliente HTTP (por exemplo, Python requests e Go net/http) não têm um tempo limite padrão, se o consumidor não definir um, ele esperará indefinidamente pela resposta do servidor. Isso pode deixar o servidor processando por um longo tempo em caso de um bug, por exemplo.

Neste post, explico uma solução complementar que mitiga os problemas acima.

💬 Like or have something to add? Leave a comment below.
Ko-fi
GitHub Sponsor
Licensed under CC BY-NC-SA 4.0
Criado com Hugo
Tema Stack desenvolvido por Jimmy