1 Introduction

Suppose there is a time-consuming Action, and if the page is refreshed before the browser makes a request to return a response, the previous request will be terminated for the browser (client). And what about the server? Will the previous request be automatically terminated, or will it continue to run?

Below we find answers through examples.

2. Example demonstration

Create one SlowRequestController, then define a Getrequest and Task.Delay(10_000)simulate time-consuming behavior. code show as below:

public class SlowRequestController : Controller
{
    private readonly ILogger _logger;

    public SlowRequestController(ILogger<SlowRequestController> logger)
    {
        _logger = logger;
    }

    [HttpGet("/slowtest")]
    public async Task<string> Get()
    {
        _logger.LogInformation("Starting to do slow work");

        // slow async action, e.g. call external api
        await Task.Delay(10_000);

        var message = "Finished slow delay of 10 seconds.";

        _logger.LogInformation(message);

        return message;
    }
}

If we initiate a request, the page will take 10s to complete the display.

Page display

If we check the run log, we find that its output is as expected:

ASP.NET_Core__basic_series(12)__Interrupt_request_2.png

Run Log

If the page is refreshed before the first request is returned, what will happen? ?

ASP.NET_Core__basic_series(12)__Interrupt_request_3.png

Run log after refresh

From the log we can see that after the refresh, the first request is canceled on the client, but the server will continue to run.

This can explain the default behavior of MVC: even if the user refreshes the browser, the original request will be canceled, but MVC knows nothing about it. The cancelled request will continue to run on the server, and the final result will be discarded. .

This can result in serious performance waste. It is fine if the server can sense that the user has interrupted the request and terminates the time-consuming task.

Fortunately, the ASP.NET Core development team thoughtfully considered this, allowing us to get the client’s request terminated in two ways.

  1. By HttpContexthe RequestAbortedproperty:
  2. Inject CancellationTokenparameters by method :
if (HttpContext.RequestAborted.IsCancellationRequested)
{
    // can stop working now
}

[HttpGet]
public async Task<ActionResult> GetHardWork(CancellationToken cancellationToken)
{
    // ...
 
    if (cancellationToken.IsCancellationRequested)
    {
        // stop!
    }
     
    // ...
}

And in fact, these two methods are the same as HttpContext.RequestAbortedand cancellationTokencorresponds to the same object:

if(cancellationToken == HttpContext.RequestAborted)
{
    // this is true!
}

Let’s cancellationTokentake a look at how to perceive the client request to terminate and terminate the server service.

3. Use the CancellationToken in the Action

CancellationTokenIs CancellationTokenSourcea lightweight object created by. When a CancellationTokenSourcecancellation is made, it notifies all consumers CancellationToken.

When canceled, CancellationTokenthe IsCancellationRequestedproperty will be set to True, indicating that it CancellationTokenSourcehas been canceled.

Going back to the previous example, we have a long-running method of operation (for example, by calling many other APIs to generate read-only reports). Since it is an expensive method, we want to stop the operation as soon as the user cancels the request.

The following code shows the purpose of synchronously terminating a server request by injecting one into the action method CancellationTokenand passing it to Task.Delay:

public class SlowRequestController : Controller
{
    private readonly ILogger _logger;

    public SlowRequestController(ILogger<SlowRequestController> logger)
    {
        _logger = logger;
    }

    [HttpGet("/slowtest")]
    public async Task<string> Get(CancellationToken cancellationToken)
    {
        _logger.LogInformation("Starting to do slow work");

        // slow async action, e.g. call external api
        await Task.Delay(10_000, cancellationToken);

        var message = "Finished slow delay of 10 seconds.";

        _logger.LogInformation(message);

        return message;
    }
}

MVC will CancellationTokenModelBinderautomatically CancellationTokenbind any parameters in the Action to it HttpContext.RequestAborted. When we Startup.ConfigureServices()call services.AddMvc()or services.AddMvcCore(), the CancellationTokenModelBindermodel binder will be registered automatically.

With this minor change, we try to refresh the page before the first request returns. From the log we find that the first request will not continue. It is thrown when the execution is stopped immediately when it Task.Delaydetects that the CancellationToken.IsCancellationRequestedproperty is true TaskCancelledException.

ASP.NET_Core__basic_series(12)__Interrupt_request_4.png

Running log

In short, the user refreshes the browser and TaskCancelledExceptionterminates the first request on the server by throwing an exception, which is then propagated back through the request pipeline.

In this scenario, Task.Delay()it is monitored CancellationToken, so we don’t need to manually check CancellationTokenif it is cancelled.

4. Manually check the CancellationToken status

If you are calling a supported CancellationTokenbuilt-in method, such as Task.Delay()or HttpClient.SendAsync(), then you can pass CancellationTokenin directly and let the internal method be responsible for the actual cancellation.
In other cases, you may be doing some synchronization work and you want to be able to cancel the work. For example, suppose you are building a report to calculate all commissions for company employees. You loop through each employee and then traverse each of their sales.

The simple solution to be able to cancel this report generation midway is to check inside the for loop and CancellationTokenjump out of the loop if the user cancels the request.
The following example represents this situation by looping 10 times and performing some synchronous (non-cancelable) work, which Thread.Sleep()is simulated by the pair . At the beginning of each loop, we check CancellationTokenand throw an exception if canceled. This allows us to terminate a long-running synchronization task.

public class SlowRequestController : Controller
{
    private readonly ILogger _logger;

    public SlowRequestController(ILogger<SlowRequestController> logger)
    {
        _logger = logger;
    }

    [HttpGet("/slowtest")]
    public async Task<string> Get(CancellationToken cancellationToken)
    {
        _logger.LogInformation("Starting to do slow work");

        for(var i=0; i<10; i++)
        {
            cancellationToken.ThrowIfCancellationRequested();
            // slow non-cancellable work
            Thread.Sleep(1000);
        }
        var message = "Finished slow delay of 10 seconds.";

        _logger.LogInformation(message);

        return message;
    }
}

Now, if you cancel the request, ThrowIfCancelletionRequested()the call to the call will throw one OperationCanceledException, which will propagate back to the filter pipeline and the middleware pipeline again.

5. Use ExceptionFilter to capture the cancellation exception

ExceptionFilters is an MVC concept that can be used to handle exceptions that occur in your action method or action filter. You can refer to the
official documentation

.

Filters can be applied to controller level and operation level, or to global level. For the sake of simplicity, we create a filter and add it to the global filter.

public class OperationCancelledExceptionFilter : ExceptionFilterAttribute
{
    private readonly ILogger _logger;

    public OperationCancelledExceptionFilter(ILoggerFactory loggerFactory)
    {
        _logger = loggerFactory.CreateLogger<OperationCancelledExceptionFilter>();
    }
    public override void OnException(ExceptionContext context)
    {
        if(context.Exception is OperationCanceledException)
        {
            _logger.LogInformation("Request was cancelled");
            context.ExceptionHandled = true;
            context.Result = new StatusCodeResult(499);
        }
    }
}

We can successfully capture the cancel exception by overloading the OnExceptionmethod and handling the OperationCanceledExceptionexception specifically .

Task.Delay()The exception thrown is a TaskCancelledExceptiontype, which is OperationCanceledExceptionthe base class, so the above filters can also be captured correctly.

Then register the filter:

public class Startup
{
    public void ConfigureServices(IServiceCollection services)
    {
        services.AddMvc(options =>
        {
            options.Filters.Add<OperationCancelledExceptionFilter>();
        });
    }
}

Now to test again, we found that the run log will not contain exception information, instead we will customize the information.

ASP.NET_Core__basic_series(12)__Interrupt_request_5.png

6. How does the server know the client’s interrupt request?

This is to mention the four-way flow of FTP. When the client interrupts the request, it will send a FIN segment, and the server will judge whether the request is interrupted.

ASP.NET_Core__basic_series(12)__Interrupt_request_6.png

FTP waved four times

Referring specifically
KestrelHttpServer

in SocketConnectionthe code implemented:

        private async Task DoReceive()
        {
            try
            {
                while (true)
                {
                    // Ensure we have some reasonable amount of buffer space
                    var buffer = _input.Alloc(MinAllocBufferSize);

                    try
                    {
                        var bytesReceived = await _socket.ReceiveAsync(GetArraySegment(buffer.Buffer), SocketFlags.None);

                        if (bytesReceived == 0)
                        {
                            // We receive a FIN so throw an exception so that we cancel the input
                            // with an error
                            throw new TaskCanceledException("The request was aborted");
                        }

                        buffer.Advance(bytesReceived);
                    }
                    finally
                    {
                        buffer.Commit();
                    }

                    var result = await buffer.FlushAsync();
                    if (result.IsCompleted)
                    {
                        // Pipe consumer is shut down, do we stop writing
                        _socket.Shutdown(SocketShutdown.Receive);
                        break;
                    }
                }

                _input.Complete();
            }
            catch (Exception ex)
            {
                _connectionContext.Abort(ex);
                _input.Complete(ex);
            }
        }

7. Finally

Through this article, we know that users can cancel a web application request at any time by clicking the Stop or Reload button on the browser. In fact, it only terminates the client’s request, and the server’s request continues to run. For simple and time-consuming requests, we can ignore them. However, for time-consuming tasks, we can’t turn a blind eye to it because of its high performance loss.

And how to solve it? The key is CancellationTokento capture the state of the user’s request and process it as needed.

References:

CancellationTokens and Aborted ASP.NET Core Requests


Using CancellationTokens in ASP.NET Core MVC controllers


SocketTransport: FIN handling

Orignal link:https://www.jianshu.com/p/9988f2a27f8d