No mundo moderno da engenharia de software os processos , atividades e dependências ocorrem a todo o momento de forma concorrente!
Falar de concorrência é ter consciência que várias tarefas são executadas simultaneamente e/ou concorrentes com os ciclos/processadores disponíveis e que estes recursos são finitos e/ou tem custos associados à sua utilização.Obter performance é bem mais sobre saber trabalhar com concorrência e como usar de forma consciente os recursos que sobre saber aplicar padrões arquiteturais.
De um modo geral falhamos miseravelmente em criar aplicações / bibliotecas e funcionalidades que fazem bom uso de recursos! A “nuvem” até pode ser “infinita”, mas certamente o cartão de crédito e sua infraestrutura NÃO!
Onde entra o CancellationToken neste contexto? Então vamos lá…, antes vou alinhar junto com você, alguns conceitos fundamentais que precisam ser compreendidos e serão aplicados no decorrer deste artigo:
CancellationToken é um componente desenhado para informar que não é mais necessário continuar uma tarefa, fornecendo um mecanismo para cancelamento cooperativo de operações assíncrona e propagando a informação de solicitação de cancelamento por todas as tarefas que a utilizam.
Alguns aspectos importantes
- O cancelamento não é imposto/forçado é uma solicitação
- As tarefas podem e devem determinar como e quando encerrar em resposta a uma solicitação de cancelamento.
- Não existe mágica, o CancellationToken não faz nada além de informar uma solicitação de cancelamento, isso é muito importante.
- A solicitação de cancelamento refere-se sempre a operações e não a objetos
- Em outras palavras, devemos criar “operações canceláveis” para poder tirar proveito do CancellationToken.
- O CancellationToken informa
- Você precisa e deve saber o que fazer com esta solicitação utilizando componentes adequados
- Isso é ainda mais importante!
- Operações canceláveis são tarefas que escolhem uma estratégia de como encerrar e como responder a uma solicitação de cancelamento.
- Normalmente, executam alguma limpeza quando necessário e respondem o mais breve possível.
- Novamente não existe mágica, o que precisa ser feito após o recebimento da solicitação de cancelamento deve estar bem definido e alinhado com as regras de negócio e os componentes utilizados.
- Isso é mais que importante é fundamental!
Mas como sei que foi enviado solicitação de cancelamento?
- Através da propriedade IsCancellationRequested
Agora que alinhamos alguns conceitos, vamos detalhar e demostrar como tornar as nossas tarefas assíncronas canceláveis, apresentando alguns cenários mais comuns. Todos os exemplos apresentados são apenas didáticos e necessariamente não representam as melhores prática (O projeto e exemplos completo pode ser baixado pelo link no final do artigo).
Tornando sua API “Cancelável”
Quando uma solicitação de “request” é enviada para sua aplicação, é mantida uma conexão entre as duas partes envolvidas (quem está consumindo / quem está processando a solicitação).
O que acontece se a parte que solicitou abandonar a solicitação?
- A conexão é perdida e o processamento continua na sua aplicação!
- Desperdício de processamento , recursos de Infraestrutura/Midllewares e os custos associados : financeiros e performance.
Toda vez que é estabelecida uma conexão, “por baixo dos panos” é disponibilizado um parâmetro de CancellationToken (opcional – deve ser declarado) associado a esta conexão.
Quando ocorre uma “quebra” de conexão é enviado uma solicitação de cancelamento (IsCancellationRequested = true) para que sua aplicação tenha a oportunidade de tomar uma ação para não continuar a operação, uma vez que a outra parte não tem mais interesse e nem ira receber o resultado da operação.
Dica : Sempre declare e propague o CancellationToken para as chamadas subjacentes, oferecendo a oportunidade as demais dependências de tomar uma ação para não continuar a operação o mais breve possível.O .NET já fornece diversos métodos associados a cada recurso que estão preparados para cancelar a operação caso recebam uma notificação de cancelamento.
A solicitação de cancelamento refere-se sempre a operações e não a objetos, então o fato de seu recurso(objeto) de api ou tarefa/função(objeto) ser ou não “Async” é irrelevante!Normalmente os métodos associados a cada recurso que aceitam o cancelamento esperam uma operação assíncrona criando esta relação com “Async/await”, então ao executar uma operação assíncrona sobre uma operação síncrona, embora seja possível, deve ser feita com muito critério! Sendo recomendado esta leitura.
Acessando outra dependência(operação não cancelável) sem Cancellationtoken
Para testar use a aplicação “CancellationTokenApiSample” e chame o ‘Endpoint’ “Call/HelloWord” . Em seguida execute um refresh no browser; esta ação interrompe a conexão e ativa a solicitação de cancelamento.
Neste exemplo é feito uma chamada a outro “Endpoint” (que demora 10 segundos para recuperar a informação) . Como não tem a declaração de cancelamento e a dependência também não é uma tarefa cancelável a execução continua até o final na aplicação e na dependência sem ser interrompida. (Veja os tempos)
| [HttpGet] [Route(“/Call/HelloWord”)] public async Task GetSemToken() { var client = clientFactory.CreateClient(); client.BaseAddress = new Uri(“https://localhost:7004/”); var sw = Stopwatch.StartNew(); var respose = await client.GetAsync(“/HelloWord”); var aux = await respose.Content.ReadAsStringAsync(); logger.LogInformation($”Response /Call/HelloWord after {sw.Elapsed}”); return aux!; } |
Resultado do log
info: CancellationTokenApiSample.Controllers.Delay10SecondsController[0] Hello Word after 00:00:10.0233020
info: CancellationTokenApiSample.Controllers.SampleController[0] Response /Call/HelloWord after 00:00:10.1766273
Acessando outra dependência(operação parcialmente cancelável) com CancellationToken
Para testar use a aplicação “CancellationTokenApiSample” e chame o ‘Endpoint’ “CallCT /HelloWord’ em seguida execute um refresh no browser;
Como tem a declaração de cancelamento e utilização no componente e a dependência não é uma tarefa cancelável a execução continua até o final na dependência sendo interrompida na aplicação. (Veja os tempos)
| [HttpGet] [Route(“/CallCT/HelloWord”)] public async Task GetHelloWord(CancellationToken token) { var client = clientFactory.CreateClient(); client.BaseAddress = new Uri(“https://localhost:7004/”); var aux = string.Empty; var sw = Stopwatch.StartNew(); try { //propagando o token var respose = await client.GetAsync(“/HelloWord”, token); aux = await respose.Content.ReadAsStringAsync(token); logger.LogInformation($”Response /CallCT/HelloWord after {sw.Elapsed}”); } catch (TaskCanceledException) { logger.LogInformation($”Response /CallCT/HelloWord after {sw.Elapsed}”); } return aux; } |
Resultado do log
info: CancellationTokenApiSample.Controllers.SampleController[0] Response /CallCT/HelloWord after 00:00:01.0720614
info: CancellationTokenApiSample.Controllers.Delay10SecondsController[0] Hello Word after 00:00:10.0064256
Acessando outra dependência (operação cancelável) com CancellationToken
Para testar use a aplicação “CancellationTokenApiSample” e chame o ‘Endpoint’ “CallCT / HelloWordCancelation” em seguida execute um refresh no browser;
Como tem a declaração de cancelamento e utilização no componente e a dependência é uma tarefa cancelável a execução é interrompida na aplicação e na dependência. (Veja os tempos)
| [HttpGet] [Route(“/CallCT/HelloWordCancelation”)] public async Task GetHelloWordCancelation(CancellationToken token) { var client = clientFactory.CreateClient(); client.BaseAddress = new Uri(“https://localhost:7004/”); var aux = string.Empty; var sw = Stopwatch.StartNew(); try { //propagando o token var respose = await client.GetAsync(“/HelloWordCancelation”, token); aux = await respose.Content.ReadAsStringAsync(token); logger.LogInformation($”Response /CallCT/HelloWordCancelation after {sw.Elapsed}”); } catch (TaskCanceledException) { logger.LogInformation($”Response /CallCT/HelloWordCancelation after {sw.Elapsed}”); } return aux; } |
Resultado do log
info: CancellationTokenApiSample.Controllers.Delay10SecondsController[0] Hello Word after 00:00:00.9054228
info: CancellationTokenApiSample.Controllers.SampleController[0] Response /CallCT/HelloWordCancelation after 00:00:01.2128591
Acessando um banco de dados (operação não cancelável) sem CancellationToken
Para testar use a aplicação “CancellationTokenApiSample” e chame o ‘Endpoint’ “Call/OpenDb” em seguida execute um refresh no browser;
Neste exemplo e feito um acesso ao um banco de dados abrindo e fechando a conexão (Para simular um atraso, a conexão aponta para um endereço “errado” para tentar 6 vezes). Quando ocorre o a solicitação de cancelamento a consulta ao banco continua .(veja o tempo)
|
Resultado do log
info: CancellationTokenApiSample.Controllers.SampleController[0] Response /CallCT/DbModelSampleafter after 00:00:15.3860227
Acessando um banco de dados (operação cancelável) e com CancellationToken
Para testar use a aplicação “CancellationTokenApiSample” e chame o ‘Endpoint’ “CallCT/OpenDb” em seguida execute um refresh no browser;
Neste exemplo e feito um acesso ao um banco de dados abrindo e fechando a conexão. Quando ocorre o a solicitação de cancelamento a consulta ao banco é interrompida .(veja o tempo)
| [Route(“/CallCT/OpenDb”)] public async Task<Results<Ok, NotFound,ProblemHttpResult >> Put(CancellationToken token) { var sw = Stopwatch.StartNew(); try { await OpenCloseDbCancelavel(token); return TypedResults.Ok(); } catch (SqlException) { logger.LogInformation($”Response /CallCT/DbModelSample after {sw.Elapsed}”); return TypedResults.Problem(“Error”); } catch (TaskCanceledException) { logger.LogInformation($”Response /CallCT/DbModelSample after {sw.Elapsed}”); return TypedResults.NotFound(); } }private async Task OpenCloseDbCancelavel(CancellationToken token) { await db.Database.OpenConnectionAsync(token); db.Database.CloseConnection(); } |
Resultado do log
info: CancellationTokenApiSample.Controllers.SampleController[0] Response /CallCT/DbModelSample after 00:00:01.2435753
Tornando uma operação com temporização “Cancelável”
Nos exemplos anteriores é feito chamadas a dois “Endpoint” diferentes
/HelloWord
/HelloWordCancelation
O “Endpoint” HelloWord é uma operação não cancelável
| [HttpGet] [Route(“/HelloWord”)] public string Get() { var sw = Stopwatch.StartNew(); Thread.Sleep(10000); sw.Stop(); logger.LogInformation($”Hello Word after {sw.Elapsed}”); return $”Hello Word”; } |
O “Endpoint” HelloWordCancelation é uma operação cancelável.
Atenção: não é porque recebe o parâmetro CancellationToken se torna cancelável!
| [HttpGet] [Route(“/HelloWordCancelation”)] public string Get(CancellationToken token) { var sw = Stopwatch.StartNew(); token.WaitHandle.WaitOne(10000); sw.Stop(); logger.LogInformation($”Hello Word after {sw.Elapsed}”); return $”Hello Word”; } |
Comparando os dois códigos, você pode notar que existem algumas diferenças:
- Recebe um parâmetro CancellationToken (Propagação! lembra?)
- Aproveita o Token para executar o Delay: ‘token.WaitHandle.WaitOne(10000);’
Lembra da definição de uma operação cancelável ? Uma ajudinha :
- Operações canceláveis são tarefas que escolhem uma estratégia de como encerrar e como responder a uma solicitação de cancelamento. Normalmente, executam alguma limpeza quando necessário e respondem o mais breve possível.
O código original usa o comando ‘Thread.Sleep(10000)’. Este comando interrompe a execução Thread por um tempo determinado para simular um trabalhado demorado/ custoso de um recurso.
Queremos que isso aconteça, porém somente quando não houver uma solicitação de cancelamento, então precisamos de uma outra estratégia para fazer isso e responder o mais breve possível quando ocorre o cancelamento.
A propriedade ‘WaitHandle’ disponibiliza o manipulador do token que é responsável por controlar a sincronização entre threads e aplicar os sinalizadores (semáforos) para que isso aconteça.
O Legal da propriedade ‘WaitHandle’ é que possui alguns métodos para interromper a execução de forma temporizada. Quando ocorre uma solicitação de cancelamento esta temporização é interrompida e “voilà”! temos agora uma estratégia eficiente e rápida!
Dica : Utilize sempre que possível WaitHandle e seus métodos como sinalizadores de tempo(WaitOne ou seus semelhantes). Quando ocorre uma solicitação de cancelamento seus métodos também são avisados e interrompem a espera.
Tornando uma operação em background “Cancelável”
Uma operação em “background” normalmente é iniciada junto com aplicação e permanece ativa durante o ciclo de vida dela, sobre um “loop” baseado em uma condição/regra qualquer.
Em cenários atuais, onde “container é vida” e os conceitos de “HPA’ (“upscale” / ”downscale”) são necessários para fazem uso consciente de recursos as operações em “background” precisam saber a hora de parar de forma controlada evitando “travamentos” e/ou inconsistências , fazendo as limpezas necessárias. Isso é válido mesmo sem o uso de “container”.
O uso de CancellationToken é um bom “aliado” para garantir uma saída deste “loop” de uma forma controlada (“Graceful Shutdown”).
Exemplo: Operação em background (cancelável) com CancellationToken:
Para testar use a aplicação “CancellationTokenBackGroudServiceSample” e acompanhe o log por cerca de 30 segundos e depois chame o ‘Endpoint’ “StopApplication” ; esta ação envia uma solicitação para finalizar a aplicação que por sua vez dispara uma solicitação de cancelamento. Os resultados podem ser vistos e avaliados pelo log.
Analisando o código, você poderá ver um loop temporizado pela classe ‘PeriodicTimer’. Antes de executar a ação desejada é feita uma validação de negócio ‘AnyRule’ que quando atendida executa uma tarefa cancelável ‘DoWork’. Quando chega a solicitação de cancelamento o “loop” encerra a operação de forma controlada (“Graceful Shutdown”).
| public class TimedHostedService(ILogger logger) : BackgroundService { protected override async Task ExecuteAsync(CancellationToken stoppingToken) { logger.LogInformation(“Timed Hosted Service running.”); using PeriodicTimer timer = new(TimeSpan.FromSeconds(10)); try { var sw = new Stopwatch(); sw.Start(); while (await timer.WaitForNextTickAsync(stoppingToken)) { //validate user rule if (AnyRule()) { await DoWork(sw.Elapsed, logger, stoppingToken); } sw.Restart(); } } catch (OperationCanceledException) { logger.LogInformation(“Timed Hosted Service is stopped.”); } } private bool AnyRule() { return true; } private static async Task DoWork(TimeSpan elapsedtime, ILogger logger, CancellationToken token) { token.ThrowIfCancellationRequested(); logger.LogInformation(“Timed Hosted Service is working. elapsedtime: {elapsedtime}”, elapsedtime); await Task.CompletedTask; } } |
Uma boa prática que pode ser observada ,como já dito, é a propagação do CancellationToken para as dependências subjacentes , que no nosso caso é o método ‘DoWork’.
Outra observação é o uso do comando ‘ThrowIfCancellationRequested()’ que notifica ao chamador uma solicitação de cancelamento abortando (neste exemplo faz sentido) a execução.
- ThrowIfCancellationRequested : Gera um OperationCanceledException se esse token tiver tido o cancelamento solicitado.
- É equivalente ao código abaixo :
if (token.IsCancellationRequested)
throw new OperationCanceledException(token);
Assumindo o controle do “Cancelamento”
Sempre temos disponível pela Infraestrutura do .NET o CancellationToken de encerramento da aplicação. Outras necessidades aparecem em cenários que queremos cancelar alguma tarefa com base em um evento, estado ou regra de negócio. Sendo assim precisamos assumir o controle! Nesta hora entra em cena outro componente:
- CancellationTokenSource: É uma classe desenhada para ser uma fonte de notificação de cancelamento controlada pela aplicação.
Adivinha qual é a principal propriedade desta classe? O CancellationToken! Esta classe implementa a interface IDisposable então lembre-se de utilizar o método Dispose() quando não for mais necessário ou encapsular sua utilização em um bloco de “using”.
O CancellationTokenSource possui alguns métodos para atender ao seu propósito e vamos abordar alguns deles (Existem outros métodos que abordaremos adiante)
- Cancel
- CancelAfter
- Register
O método Cancel
O que faz o método Cancel? Comunica uma solicitação de cancelamento, em outras palavras, tornam a propriedade IsCancellationRequested = true!
Para testar use a aplicação “CancellationTokenCancelConsole” , siga as instruções que apareceram no console e acompanhe o log.
Neste exemplo temos um serviço em background que inicia duas tarefas assíncronas:
- TaskUI: Responsável pela interação do console para enviar um comando de processar mensagens ou uma solicitação de cancelamento e encerrar a aplicação.
- TaskProcess: Responsável por processar uma mensagem.
Analisando o código , você pode observar o uso do CancellationTokenSource com o método Cancel na ‘TaskUI’ para enviar uma solicitação de cancelamento para Task ‘TaskProcess’; Propagação!
| internal class ConsoleInterationService(ILogger logger, IHostApplicationLifetime applicationLifetime) : BackgroundService { private Task? TaskUI; private Task? TaskProcess; private bool AtivarProcesso; private readonly CancellationTokenSource cts = new(); protected override async Task ExecuteAsync(CancellationToken stoppingToken) { logger.LogInformation(“ConsoleInteration Hosted Service running.”); TaskUI = Task.Run(() => { Console.WriteLine(“Digite 1 para processar uma tarefa e 2 para Terminar”); while (!applicationLifetime.ApplicationStopping.IsCancellationRequested) { if (Console.KeyAvailable) { var kp = Console.ReadKey(true); switch (kp.KeyChar) { case ‘1’: if (!AtivarProcesso) { AtivarProcesso = true; while (AtivarProcesso) { stoppingToken.WaitHandle.WaitOne(1); } Console.WriteLine(“Digite 1 para processar uma tarefa e 2 para Terminar”); } break; case ‘2’: cts.Cancel(); break; } } //avoid cpu consumption(possible upscale) stoppingToken.WaitHandle.WaitOne(1); } }, stoppingToken); TaskProcess = Task.Run(() => { while (!cts.Token.IsCancellationRequested) { if (AtivarProcesso) { logger?.LogInformation($”TaskProcess is working {DateTime.Now}”); AtivarProcesso = false; } else { cts.Token.WaitHandle.WaitOne(1); } } //after stop loop end Application (business rule) applicationLifetime.StopApplication(); }, stoppingToken); await Task.CompletedTask; } public override async Task StopAsync(CancellationToken cancellationToken) { if (!TaskUI?.IsCompleted ?? false) { //when Ctrl-C logger.LogWarning(“TaskUI not stopped!. Waiting stop”); while (!TaskUI?.IsCompleted ?? false) { cancellationToken.WaitHandle.WaitOne(1); } } logger.LogInformation(“TaskUI stopped.”); TaskUI?.Dispose(); if (!TaskProcess?.IsCompleted ?? false) { //when Ctrl-C logger.LogWarning(“TaskProcess not stopped!. send cancel”); cts.Cancel(); while (!TaskProcess?.IsCompleted ?? false) { cancellationToken.WaitHandle.WaitOne(1); } } logger.LogInformation(“TaskProcess stopped.”); TaskProcess?.Dispose(); cts.Dispose(); logger.LogInformation(“ConsoleInteration Hosted Service exit.”); await base.StopAsync(cancellationToken); } } |
Nota : Existe um problema nesta exemplo; Quando ocorre um Ctrl-C a Task ‘TaskProcess’ não é encerrada automaticamente, apenas observa o CancellationTokenSource. É preciso enviar outro Cancel durante o método ‘StopAsync’ e aguarda o término da Task para termos uma saída controlada (“Graceful Shutdown”) e um fluxo correto de sua lógica(executar o comando ‘applicationLifetime.StopApplication()’ após o loop).
O método CancelAfter
O que faz o método CancelAfter? Comunica uma solicitação de cancelamento após o número especificado de tempo.
Um exemplo prático para este método é o uso em regras de TIMEOUT para execuções de qualquer atividade. Para testar use a aplicação “CancellationTokenCancelAfterConsole” siga as instruções que apareceram no console e acompanhe o log.
Neste exemplo temos um serviço em background que inicia uma tarefa assíncrona que chama outra tarefa cancelável para processar a mensagem com um “flag” para ficar em espera além do tempo máximo definido ,demostrando um controle de timeout (após um tempo pré-definido a tarefa é cancelada).
Observe que o parâmetro de CancellationToken é do CancellationTokenSource e foi usado o método CancelAfter(10000) antes da chamada.
| internal class ConsoleInterationService(ILogger logger, IHostApplicationLifetime applicationLifetime) : BackgroundService { private Task? TaskUI; protected override async Task ExecuteAsync(CancellationToken stoppingToken) { logger.LogInformation(“ConsoleInteration Hosted Service running.”); TaskUI = Task.Run(async () => { … if (Console.KeyAvailable) { var kp = Console.ReadKey(true); long result = -1; switch (kp.KeyChar) { … case ‘2’: { using var ctstimeout = new CancellationTokenSource(); ctstimeout.CancelAfter(10000); result = await ProcessaMensagem(true, ctstimeout.Token); } break; … } … } }, stoppingToken); await Task.CompletedTask; } private async Task ProcessaMensagem(bool exectimeout, CancellationToken token) { logger?.LogInformation($”TaskProcess waiting start…”); long Elapsed = 0; if (exectimeout) { var sw = Stopwatch.StartNew(); try { token.WaitHandle.WaitOne(11000); token.ThrowIfCancellationRequested(); } catch (OperationCanceledException) { Elapsed = sw.ElapsedMilliseconds; } } return await Task.FromResult(Elapsed); } … } |
O método Register
O que faz o método Register()? Registra um ‘action’ que será chamado quando este CancellationToken for cancelado. Se este token já estiver no estado cancelado, a action será executado imediatamente e de forma síncrona. Qualquer exceção gerada pelo action será propagada a partir desta chamada de método.
Para testar use a aplicação “CancellationTokenRegisterConsole” siga as instruções que apareceram no console e acompanhe o log.
Neste exemplo temos um serviço em background que inicia uma tarefa assíncrona que processa a mensagem. Antes de iniciar o “loop” é feito um registro de uma ‘action’ para ser executada quando ocorrer o cancelamento.
Esta ‘action’ executa o fim da aplicação que por sua vez envia uma solicitação de cancelamento e encerra o “loop”.
| internal class ConsoleInterationService(ILogger logger, IHostApplicationLifetime applicationLifetime) : BackgroundService { private Task? TaskUI; private readonly CancellationTokenSource cts = new(); protected override async Task ExecuteAsync(CancellationToken stoppingToken) { cts.Token.Register(RegraDeCancelamento); logger.LogInformation(“ConsoleInteration Hosted Service running.”); TaskUI = Task.Run(() => { … if (Console.KeyAvailable) { var kp = Console.ReadKey(true); switch (kp.KeyChar) { … case ‘2’: cts.Cancel(); break; } } //avoid cpu consumption(possible upscale) stoppingToken.WaitHandle.WaitOne(1); }, stoppingToken); await Task.CompletedTask; } private void RegraDeCancelamento() { //after stop loop end Application (business rule) logger?.LogInformation($”RegraDeCancelamento invoke by Register”); applicationLifetime.StopApplication(); } … } |
Tornando uma operação “Cancelável” associada a uma ou mais regras de negócio
Nem tudo é tão simples, muita das vezes precisamos implementar regras mais complexas que requer mais de uma única fonte de cancelamento. Vamos a um cenário real:
- Você possui um método que já recebe um CancellationToken vindo de um “request” .
- Agora imagina que este método além de interromper quando a conexão é perdida interrompa também quando ultrapasse um tempo limite ou ainda melhor, quando uma regra de negócio não seja atendida durante seu processamento, o que acontecer primeiro.
“Sacou o drama ?”, nesta hora entra em cena outra classe :
CreateLinkedToken: É uma classe desenhada para ser uma fonte de notificação de cancelamento que unifica vários tokens de cancelamento.
Esta classe simplifica a codificação (basta verificar a propriedade IsCancellationRequested independente que que a originou), porém é possível saber qual token de cancelamento foi a origem (caso seja preciso tomar ações diferentes). Esta classe implementa a interface IDisposable então lembre-se de utilizar o método Dispose() quando não for mais necessário ou encapsular sua utilização em um bloco de “using”.
Acessando um Endpoint(operação cancelável) com CreateLinkedToken
Para testar use a aplicação “CancellationTokenApiCreateLinkedToken” e chame o ‘Endpoint’ “Call/HelloWordWithTimeout?timeout=true” e aguarde o resultado (será um timeout com status 499). Em seguida chame o mesmo ‘Endpoint’ e execute um refresh no browser; esta ação interrompe a conexão e ativa a solicitação de cancelamento. (Acompanhe log)
Neste exemplo é feito uma chamada a outro “Endpoint” (que demora 10 segundos para recuperar a informação ) . antes da execução é criando um CancellationTokenSource e um CreateLinkedTokenSource utilizando o CancellationToken do request e o CancellationTokenSource.
É simulado uma espera maior de que o tempo máximo definido para demostrar o timeout.
Observe que o código faz isso utilizando CreateLinkedTokenSource! Isso significa que caso ocorra um cancelamento pelo cliente ou um timeout a tarefa será cancelada!
| [HttpGet] [Route(“/HelloWordWithTimeout”)]public async Task<Results<Ok,NoContent>> Get([FromQuery] bool timeout, CancellationToken token) { using var ctstimeout = new CancellationTokenSource(); using var lnkcts = CancellationTokenSource.CreateLinkedTokenSource(token,ctstimeout.Token); var tm = 1; if (timeout) { tm = 11000; } ctstimeout.CancelAfter(10000); var sw = Stopwatch.StartNew(); var hascancel = false; if (lnkcts.Token.WaitHandle.WaitOne(tm)) { if (token.IsCancellationRequested) { logger.LogInformation($”Hello Word has cancled by client”); } else { logger.LogInformation($”Hello Word has timeout {sw.Elapsed}”); } hascancel = true; } else { logger.LogInformation($”Hello Word after {sw.Elapsed}”); } sw.Stop(); if (hascancel) { return await Task.FromResult(TypedResults.NoContent()); } return await Task.FromResult(TypedResults.Ok(“Hello Word”)); } |
Tornando os retornos de um API “Cancelável” rastreáveis
Para finalizar vou falar de outro aspecto fundamental e muito relevante que propositalmente não citei no início do artigo
Dica: Mantenha sempre os retornos coerentes de sua API quando ocorre um cancelamento por parte do cliente.
Mas porque isso é importante? Foi dito que quando ocorreu cancelamento o cliente não tem mais interesse , e não recebe o resultado… Não é apenas cancelar e se preocupar em performance e custo é fazer bom uso de recursos existentes também! vamos detalhar isso melhor criando um cenário:
- “Você trabalha em uma empresa de comércio eletrônico/banco
- possui um gateway interno para fazer a comunicações entre as diversas apis.
- Além de rotear estas comunicações é gerado um log de infraestrutura com os tempos e status de cada requisição para uma análise pelo time de produtos e estratégia da empresa.”
- Quando ocorre um cancelamento por parte do cliente se você não escolher um status correto
- As métricas de status ficarão “poluídas”, podendo induzir a uma análise “enganosa”, mesmo que funcionalmente não tenha erros!
Acessando um Endpoint(operação cancelável) com CreateLinkedToken
A aplicação “CancellationTokenApiCreateLinkedToken” faz este tratamento retornando um status não padrão (499 – Mesmo do nginx) .
Analisando o código, você vai observar que quando ocorre o cancelamento o status de retorno é
- 499 para cancelamento, 200 para tudo ok .
- 204 para timeout ,
- 500 para um erro e outro status caso Endpoint ”tenha um novo status não mapeado – falha de comunicação/documentação entre times.”
Para verificar que isso acontece de fato é necessário colocar um breakpoint e acompanhar a execução.
| [HttpGet] [Route(“/Call/HelloWordWithTimeout”)]public async Task<Results<Ok, NoContent, ProblemHttpResult>> Get ([FromQuery] bool timeout, CancellationToken token) { var client = clientFactory.CreateClient(); client.BaseAddress = new Uri(“https://localhost:7084/”); var sw = Stopwatch.StartNew(); string aux = string.Empty; int status; try { var respose = await client.GetAsync($”/HelloWordWithTimeout?timeout={timeout}”, token); if (respose.StatusCode == HttpStatusCode.OK) { //runs only when necessary aux = await respose.Content.ReadAsStringAsync(token); } status = (int)respose.StatusCode; } catch (OperationCanceledException) { logger.LogWarning($”Response /Call/HelloWordWithTimeout client aborted”); return TypedResults.Problem(“Client Closed Request”, statusCode: 499); } catch (Exception ex) { logger.LogError($”Response /Call/HelloWordWithTimeout error: {ex}”); return TypedResults.Problem(“Erro interno”,statusCode:500); } if (status == 200) { logger.LogInformation($”Response /Call/HelloWordWithTimeout after {sw.Elapsed}”); return TypedResults.Ok(aux); } //timeout if (status == 204) { logger.LogInformation($”Response /Call/HelloWordWithTimeout timeout {sw.Elapsed}”); return TypedResults.NoContent(); } return TypedResults.Problem(“Não mapeado”, statusCode: status); } |
Conclusão
Existem muitos outros cenários (e métodos) que podem ser explorados , mas tornaria bem mais extenso este artigo que o desejado.
CancellationToken é um instrumento poderoso para otimizar o uso de recursos concorrentes , tornando sua aplicação muito mais eficiente , rápida, aproveita corretamente os recursos de Infraestrutura/Midllewares e com custos menores.
Espero que com este “overview” e com os códigos disponibilizados no projeto exemplo, você possa aproveitar melhor o CancellationToken!
Referências
Código fonte com todos os exemplos:
https://github.com/FRACerqueira/CancellationTokenSamples
CancellationToken
https://learn.microsoft.com/en-us/dotnet/api/system.threading.cancellationtoken?view=net-8.0
CancellationTokenSource
https://learn.microsoft.com/pt-br/dotnet/api/system.threading.cancellationtokensource?view=net-8.0
Tenham um excelente dia! Eu sou Fernando Cerqueira e entrego estratégias digitais para os desafios do presente, com propostas de inovação para um futuro sustentável.






