Arquitetura Corporativa para Todos

Artigo / Post

CancellationToken – O que você precisa e deveria saber antes de usar

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)

[HttpPut]
[Route(“/Call/OpenDb”)]
public async Task<Results<Ok, ProblemHttpResult>> Put()
{
    var sw = Stopwatch.StartNew();
    try
    {
        await OpenCloseDb();
        return TypedResults.Ok();
    }
    catch (SqlException)
    {
        logger.LogInformation($”Response /CallCT/DbModelSampleafter after {sw.Elapsed}”);
        return TypedResults.Problem(“Error”);
    }
}private async Task OpenCloseDb()
{
    await db.Database.OpenConnectionAsync();
    db.Database.CloseConnection();
}

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.

Compatilhe

0 0 votos
Avaliação Global
0 Comentários
Feedbacks embutidos
Ver todos os comentários
Categorias

Sobre o Autor

Picture of Fernando Cerqueira

Fernando Cerqueira

Eu sou Fernando Cerqueira e entrego estratégias digitais para os desafios do presente, com propostas de inovação para um futuro sustentável. Como arquiteto sênior, aproveito meus mais de 20 anos de experiência em arquitetura e desenvolvimento de software para projetar e implementar soluções baseadas em nuvem que ajudam os clientes a transformar seus negócios com tecnologia.

Outros Posts

Categorias

UUID (GUID)- Algumas provocações Praticas

Tive uma reunião a algum tempo para avaliar uma solução de alta volumetria com persistência de dados; para entendimento das grandezas envolvidas, são alguns terabytes de armazenamento e algo próximo de 1 bilhão de registros.  São números significativos onde, entre outros temas relevantes e uma excelente apresentação pelo time de

Adotar classes seladas em .NET por padrão é uma ótima escolha

Adotar classes seladas em .NET por padrão é uma ótima escolha. Saiba por que! Em .net todas as classes que são criadas por padrão (template), permitem herança. Esta opção padrão deixa mais flexível qualquer implementação por herança, mas sendo sincero, acredito não ser esta a melhor opção : Uma classe

IA e o terno feito sobre medida

Só quem já vestiu um terno feito sobre medida feito por um bom alfaiate sabe a diferença de usar um terno comprado em uma loja. No mercado de TI, a personalização e a adaptação às necessidades específicas de cada empresa são igualmente relevantes para o sucesso. Não basta simplesmente replicar

Uso de IA nas Empresas

“Não podemos controlar os ventos, mas podemos sempre ajustar as velas” Esta é a frase (Costuma ser atribuído a muitas pessoas, como Confúcio e Dolly Parton, mas acredita-se que seja uma variação de um pensamento de Cora L. V. Hatch, proferido em uma palestra em 1859) que vem à mente

O Futuro do Tempo Livre e da Desigualdade (Uma Crônica gerada por IA)

No ano de 2030, a inteligência artificial havia se tornado onipresente, transformando todos os aspectos da sociedade. As máquinas realizavam tarefas complexas e rotineiras, liberando os humanos para explorar novas possibilidades. Em meio a essa revolução, duas histórias se destacavam: a de Ana e a de João. Ana era uma

"Devemos ser mais criteriosos nas nossas escolhas de como implementar HealthCheck  olhando para o nosso ecossistema e não apenas para necessidade de nossa aplicação. A forma que devemos tratar as dependências a outras aplicações precisam ser analisadas em separado das dependências diretas de midlleware dentro do contexto de nossa solução."

Fernando Cerqueira | Arquiteto Corporativo

Sua Reflexão

0
Adoraria saber sua opinião, comente.x