Alguna vez te has preguntado qué es lo que pasa cuando cancelas una request? Bueno pues desafortunadamente en muchos casos no pasa nada, pero esta no debería ser la norma, por lo menos en C#, descubre en este post que es lo que sucede, y que es lo que debería suceder.
Índice
1 - ¿Qué es el CancellationToken?
Siempre que desarrollamos API, entre otras piezas de software, vemos que hay un tipo que se llama CancellationToken
, pero, desafortunadamente, suele quedar ignorado. Y no solo en código que pueda ser de hobby, sino también en código empresarial. Lo cual es un problema.
El CancellationToken
es un mecanismo o estructura de .NET que nos permite cancelar el proceso actual, técnicamente lo que hace es comprobar si el proceso debe ser interrumpido.
Este proceso debe ser async/await
y lo que hacemos es en cada llamada async
, mandamos nuestro cancelation token, y el propio async/await
comprobará si debe ser interrumpido o no.
Esta interrupción la podemos incluir de forma automática con código (después de X segundos) o en el caso de una API o una web, puede ser el usuario quien cancele la operación de forma manual.
Cuando cargas una web y te sale la `X` para dejar de cargarla. pues eso activa el CancellationToken
, o mismamente al cerrar la pestaña.
Obviamente si fuera un cliente de código se puede replicar esa experiencia.
2 - ¿Cómo funciona el CancellationToken?
Cuando hablamos de APIs cada request viene enlazada con un CancellationToken
, el cual se genera, o más bien lo genera el propio framework y podemos acceder a él en cada uno de nuestros endpoints.
Desde nuestro endpoint lo debemos pasar a cada uno de nuestros procesos o funciones asíncronas que están sucediendo en el proceso.
Por detrás lo que está sucediendo es un patrón descrito en este blog de microsoft:
Lo que sucede es que instanciamos (o bueno .NET) el objeto CancellationTokenSource, el cual entre otros elementos contiene el token, que se va copiando entre los diferentes métodos asíncronos.
Es importante mencionar que es en métodos asíncronos porque es la estructura async/await
quien contiene la lógica para saber si algo tiene que ser cancelado o no.
Otra característica que contiene el CancellationTokenSource
es el elemento .Cancel
, el cual puede cancelar un token, y es el que se ejecuta cuando el usuario pulsa en cancelar, o cierra la pestaña.
Y luego en el código, si el usuario cancela, lo que sucede es que salta la excepción OperationCancelledException
.
3 - Implementación del cancellation token en código
El código que vamos a ver ahora es parte de mi curso de sistemas distribuidos y el código está en GitHub.
En este caso vamos a ir al caso de uso de crear un producto. Donde la lógica es la siguiente:
public async Task<Result<CreateOrderResponse>> Execute(CreateOrderRequest createOrder,
CancellationToken cancellationToken = default(CancellationToken))
{
return await CreateOrder(createOrder)
.Async()
//On a real scenario:
//validate orders
.Bind(x=> ValidateFraudCheck(x, cancellationToken))
.Bind(x => SaveOrder(x, cancellationToken))
.Then(x => MapToOrderResponse(x, cancellationToken)
.Bind(or => PublishDomainEvent(or, cancellationToken)))
.Map(x => new CreateOrderResponse(x.Id, $"order/getorderstatus/{x.Id}"));
}
- Creamos el pedido
- Hacemos validaciones
- Guardamos el pedido en la base de datos
- Creamos el evento de dominio
- Respuesta al usuario
En nuestro caso actual, no tenemos implementación de lo que sería el fraud check,
private async Task<Result<OrderDetails>> ValidateFraudCheck(OrderDetails orderDetails,
CancellationToken cancellationToken)
{
//Validate fraud check
return orderDetails;
}
Pero para los que no habéis trabajado con puntos de venta, mandarías los datos sobre lo que van a comprar, nombre, dirección, etc y ese servicio, evalúa si el pago puede ser fraude o no, si lo es, devuelve un error.
Esa llamada sería con HTTP y como tal, llevaría un cancellation token.
Lo mismo aplica para guardar la información en la base de datos.
private async Task<Result<OrderDetails>> SaveOrder(OrderDetails orderDetails, CancellationToken cancellationToken)
{
await _orderRepository.Save(orderDetails, cancellationToken);
return orderDetails;
}
Lo mismo sucede al publicar un evento de dominio.
private async Task<Result<Guid>> PublishDomainEvent(OrderResponse orderResponse,
CancellationToken cancellationToken)
{
await _domainMessagePublisher.Publish(orderResponse, routingKey: "order", cancellationToken: cancellationToken);
return orderResponse.OrderId;
}
Pero lo que acabamos de ver es el caso de uso, cómo llega el CancellationToken
hasta aquí? La respuesta es simple, desde el controlador.
En el caso de .NET o C# nos lo pone automáticamente el controlador cuando lo creamos, siempre y cuando lo especificamos como uno de los parámetros.
[HttpPost("create")]
[ProducesResponseType(typeof(ResultDto<CreateOrderResponse>), (int)HttpStatusCode.Created)]
public async Task<IActionResult> CreateOrder(CreateOrderRequest createOrderRequest,
CancellationToken cancellationToken = default(CancellationToken))
{
return await _createOrderService.Execute(createOrderRequest, cancellationToken)
.UseSuccessHttpStatusCode(HttpStatusCode.Created)
.ToActionResult();
}
3.1 - Tengo que poner CancellationToken en todas partes?
En este ejemplo hemos visto cómo ha pasado el CancellationToken
por todas partes, pero, ¿es esto lo más recomendable?
Posiblemente no, y ahora vamos a ver por qué. Hay que tener varias cosas en cuenta a la hora de usar cancellation token, cuando cancelas el token, vas a cancelar el proceso, eso implica que, desde donde canceles, no va a funcionar nada, básicamente es una excepción normal.
Por lo que, si cancelas un proceso a mitad, vas a estar en un estado inválido de tu aplicación.
Si volvemos al caso anterior, estamos cancelando en cada paso, y ello está mal, pues una vez validamos el evento y guardamos, el resto de la operación debe completarse. Por lo tanto en nuestro código, debemos hacer modificaciones para que únicamente validar y guardar tengan el cancellation token.
public async Task<Result<CreateOrderResponse>> Execute(CreateOrderRequest createOrder,
CancellationToken cancellationToken = default(CancellationToken))
{
return await CreateOrder(createOrder)
.Async()
//On a real scenario:
//validate orders
.Bind(x=> ValidateFraudCheck(x, cancellationToken))
.Bind(x => SaveOrder(x)) 👈
.Then(x => MapToOrderResponse(x) 👈
.Bind(or => PublishDomainEvent(or))) 👈
.Map(x => new CreateOrderResponse(x.Id, $"order/getorderstatus/{x.Id}"));
}
Esto es sencillo. Si el token se cancela durante la ejecución de guardar debemos ignorarlo, pues debemos asegurarnos que todo el proceso se completa una vez llegados a ese punto.
por el contrario si se cancela durante validar, la excepción saltará antes de guardar, lo que no es un problema porque no se ha modificado información en el sistema.
3.2 - Implementación básica de cancellation token en .NET
Si has llegado a este post buscando información sobre el CancellationToken pero nunca has visto uno de mis posts, no habrás entendido lo de antes, aquí te dejo un ejemplo que se puede replicar de forma sencilla:
app.MapGet("/cancellation-example", async (CancellationToken ct) =>
{
foreach (var i in Enumerable.Range(0,1000))
{
Console.WriteLine($"iteration: {i}");
await Task.Delay(i * 1000, ct); //this simulates a call to a service
Console.WriteLine("Completed;");
}
});
Al hacer la llamada en el navegador verás que la consola muestra la información, mientras que justo cuando canceles se generará la excepción OperationCanceledException
, lo que implica que ninguna otra línea de código se ejecutará, pero que el proceso termine o no, depende de donde estés llamando y de si implementa CancellationToken de forma correcta,
4 - Donde debo usar cancellation token?
Como hemos visto en el punto anterior, no siempre es necesario Utilizar cancellation token, de hecho, su uso está pensado para un caso muy concreto, y es para operaciones que toman un largo tiempo a ser completadas.
Por ejemplo, tienes un proceso que hace una consulta a un servicio o base de datos el cual devuelve mil registros, de esos mil, tienes que consultar de forma individual datos y de ahí volvemos a consultar algo mas. Es un ejemplo un poco caótico pero imaginate una estructura como la siguiente:
Estamos haciendo muchas llamadas a diferentes niveles para recopilar toda la información, eso quiere decir que es un proceso lento, puede tardar uno, dos o yo he visto “reportes” que tardan 10 minutos. Ahí es donde queremos el cancellation token, queremos que si el usuario cierra la ventana, o el usuario cancela el proceso, el sistema deje de recopilar la información.
- Nota: Hay soluciones alternativas a esto, pero como ejemplo nos sirve.
El CancellationToken
no está pensado para CURDS simples que vayan a tomar muy poco tiempo.
4.1 - Qué pasa si estoy obligado a pasar un CancellationToken?
No es muy común, pero es posible que en algún método estés obligado a pasar un CancellationToken, y o bien no tengas ninguno, o no quieras que la operación se pueda cancelar.
En este caso lo que puedes hacer es pasar CancellationToken.None, y a partir de ahí nada se cancelará.
4.2 - El CancellationToken cancela servicios externos?
La respuesta corta es que no, debemos asumir que el CancellationToken
tiene uso exclusivo dentro de nuestro código.
El ejemplo más común es cuando llamamos a la base de datos en una consulta, ya que podemos pasar el CancellationToken. Cuando lo pasamos, cancelaremos la invocación desde Entity Framework Core, pero nada garantiza que eso vaya a suceder a nivel de la base de datos o de la dependencia.
Por ejemplo SQL server soporta CancellationToken, aunque no está garantizado su fucnionamiento.