Cancellation Tokens in .NET : Boosting Performance and Reliability
Table of contents
Introduction
In the world of web development, responsiveness and efficiency are paramount. .NET Core provides developers with powerful tools to build robust and scalable applications. One such tool is the Cancellation Token, a mechanism that allows for graceful cancellation of long-running operations.
In this blog post, we'll dive deep into the use of Cancellation Tokens in .NET apps, exploring their purpose, implementation, and real-world applications.
What are Cancellation Tokens?
Cancellation Tokens are objects that enable cooperative cancellation between threads, tasks, and asynchronous operations. They are part of the System.Threading
namespace in .NET and play a crucial role in managing the lifecycle of asynchronous operations.
Why Use Cancellation Tokens?
Resource Management: Prevent unnecessary resource consumption when an operation is no longer needed.
Improved User Experience: Allow users to cancel long-running operations, enhancing responsiveness.
Error Handling: Provide a clean way to handle cancellation scenarios without throwing exceptions.
Timeouts: Implement custom timeout logic for operations that may take too long.
When to Use Cancellation Tokens
Cancellation Tokens are particularly useful in scenarios involving:
Long-running database queries
External API calls
File I/O operations
Complex computations
Streaming large amounts of data
Use Case 1: Cancellation Tokens for Fallback
Let's consider a practical scenario where Cancellation Tokens can be incredibly useful: a payment gateway with a fallback mechanism.
Imagine we are building an e-commerce platform that uses multiple payment gateways. The primary gateway might occasionally experience slowdowns. To ensure a smooth checkout experience, we can implement a fallback mechanism that switches to a backup gateway if the primary one takes too long.
Before we dive into the implementation, let's understand how to create and use a Cancellation Token:
Creating a CancellationTokenSource: A
CancellationTokenSource
is the control object that manages the lifetime of a Cancellation Token.Getting the CancellationToken: The
CancellationToken
is obtained from theCancellationTokenSource
.Setting a Timeout: We can set a timeout on the
CancellationTokenSource
, after which it will automatically cancel.Passing the Token: The token is passed to methods that support cancellation.
Handling Cancellation: Our code should handle the
OperationCanceledException
that's thrown when a cancellation occurs.
public class PaymentController : ControllerBase
{
private readonly IPaymentService _paymentService;
public PaymentController(IPaymentService paymentService)
{
_paymentService = paymentService;
}
[HttpPost("process")]
public async Task<IActionResult> ProcessPayment([FromBody] PaymentRequest request)
{
// Create a new CancellationTokenSource
using var cts = new CancellationTokenSource();
// Set a timeout of 30 seconds
cts.CancelAfter(TimeSpan.FromSeconds(30));
try
{
// Pass the token to the ProcessPaymentAsync method
var result = await _paymentService.ProcessPaymentAsync(request, cts.Token);
return Ok(result);
}
catch (OperationCanceledException)
{
// Handle cancellation (either due to timeout or manual cancellation)
return StatusCode(408, "Payment processing timed out");
}
}
}
public class PaymentService : IPaymentService
{
private readonly IPrimaryGateway _primaryGateway;
private readonly ISecondaryGateway _secondaryGateway;
public PaymentService(IPrimaryGateway primaryGateway, ISecondaryGateway secondaryGateway)
{
_primaryGateway = primaryGateway;
_secondaryGateway = secondaryGateway;
}
public async Task<PaymentResult> ProcessPaymentAsync(PaymentRequest request, CancellationToken cancellationToken)
{
// Set up a timeout for the primary gateway
using var cts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken);
cts.CancelAfter(TimeSpan.FromSeconds(5)); // 5-second timeout for primary gateway
try
{
// Attempt to process payment with the primary gateway
return await _primaryGateway.ProcessPaymentAsync(request, cts.Token);
}
catch (OperationCanceledException) when (!cancellationToken.IsCancellationRequested)
{
// If the operation was cancelled due to our timeout (not the client disconnecting),
// fall back to the secondary gateway
return await _secondaryGateway.ProcessPaymentAsync(request, cancellationToken);
}
}
}
Token Creation: In the controller, we create a
CancellationTokenSource
with a 30-second timeout. This represents the overall timeout for the entire payment process.Token Passing: We pass the token from the controller to the
ProcessPaymentAsync
method in the service.Linked Token: In the service, we create a linked token source. This combines the original token (which will be cancelled if the client disconnects or the 30-second timeout is reached) with a new 5-second timeout specifically for the primary gateway.
Fallback Mechanism: If the primary gateway times out (5 seconds), we catch the
OperationCanceledException
and try the secondary gateway. We use the original token for this, so it will still cancel if the overall 30-second timeout is reached.Exception Handling: The controller handles any
OperationCanceledException
, which could be due to either the 30-second overall timeout or a client disconnection.
The implementation allows for multiple levels of timeout control:
A global timeout for the entire operation (30 seconds)
A specific timeout for the primary gateway (5 seconds)
The ability for the client to cancel at any time (by closing the connection)
Why Not Just Use HttpClient's Timeout Property?
When a timeout occurs using the Timeout
property, the entire operation is aborted. There's no way to partially complete an operation or gracefully handle the timeout. Also, it only applies to the HTTP request itself. It doesn't cover any processing time on the server before or after the HTTP request is made.
Additionally, The Timeout
property only allows to specify a maximum duration for the request post which an HttpRequestException
is thrown. This can be less specific and harder to handle than the OperationCanceledException
thrown when using Cancellation Tokens.
Cancellation Tokens on the other hand can be passed down through multiple layers of the application, allowing for coordinated cancellation of complex operations which can be cancelled manually (e.g., by user action) or automatically (e.g., after a timeout), providing more flexibility.
UseCase 2: Cancellation Tokens in Background Services
Cancellation Tokens are particularly useful in long-running background services, where they can be used to gracefully stop the service when the application is shutting down or for any other scenarios
public class QueueProcessorService : BackgroundService
{
private readonly ILogger<QueueProcessorService> _logger;
private readonly IServiceProvider _serviceProvider;
public QueueProcessorService(
ILogger<QueueProcessorService> logger,
IServiceProvider serviceProvider)
{
_logger = logger;
_serviceProvider = serviceProvider;
}
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
{
_logger.LogInformation("Queue Processor Service is starting.");
while (!stoppingToken.IsCancellationRequested)
{
try
{
await ProcessQueueItem(stoppingToken);
}
catch (Exception ex)
{
_logger.LogError(ex, "Error occurred while processing queue item.");
}
// Delay before processing the next item
await Task.Delay(TimeSpan.FromSeconds(5), stoppingToken);
}
_logger.LogInformation("Queue Processor Service is stopping.");
}
}
The stoppingToken
: The ExecuteAsync
method receives a CancellationToken
parameter. This token is triggered when the application is shutting down, allowing the service to stop gracefully.
The main processing loop continually checks !stoppingToken.IsCancellationRequested
to determine whether it should continue running.
he stoppingToken
is passed down to other methods (ProcessQueueItem
) to ensure that cancellation can be propagated throughout the entire processing chain.
Best Practices for Using Cancellation Tokens
Always provide a CancellationToken parameter: Even if we don't use it immediately, it allows for future extensibility.
Propagate the token: Pass the token down to lower-level methods and services.
Check for cancellation regularly: In long-running operations, periodically call
cancellationToken.ThrowIfCancellationRequested()
.Handle OperationCanceledException: Catch and handle this exception appropriately in the controllers or middleware.
Use timeouts judiciously: While useful, be cautious about setting timeouts that are too short, which could lead to unnecessary cancellations.
Conclusion
Cancellation Tokens are a powerful feature in .NET that can significantly enhance the performance, reliability, and user experience. By allowing for graceful cancellation of long-running operations, they help manage resources more efficiently and provide better control over asynchronous processes.
Whether we are dealing with complex database queries, integrating with external services, or implementing fallback mechanisms, Cancellation Tokens offer a standardised and effective way to handle cancellations and timeouts.