Cancellation Tokens in .NET : Boosting Performance and Reliability

Cancellation Tokens in .NET : Boosting Performance and Reliability

Featured on Hashnode

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 the CancellationTokenSource.

  • 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.

Did you find this article valuable?

Support StackUp by becoming a sponsor. Any amount is appreciated!