Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Web API and Cancellation Token

I'm trying to have a Web API and Cancellation Token working together, but it doesn't seem to play nicely for some reason.

Here is my code for my Web API. It contains cancellation token properties, and methods

public class TokenCancellationApiController : ApiController
{
    private static CancellationTokenSource cTokenSource = new CancellationTokenSource();
    // Create a cancellation token from CancellationTokenSource
    private static CancellationToken cToken = cTokenSource.Token;
    // Create a task and pass the cancellation token

    [HttpGet]
    public string BeginLongProcess()
    {
        string returnMessage = "The running process has finished!";
        try
        {
            LongRunningFunc(cToken, 6);
        }
        catch (OperationCanceledException cancelEx)
        {
            returnMessage = "The running process has been cancelled.";
        }
        finally
        {
            cTokenSource.Dispose();
        }
        return returnMessage;
    }

    [HttpGet]
    public string CancelLongProcess()
    {
        // cancelling task
        cTokenSource.Cancel();
        return "Cancellation Requested";
    }

    private static void LongRunningFunc(CancellationToken token, int seconds)
    {
        Console.WriteLine("Long running method");
        for (int j = 0; j < seconds; j++)
        {
            Thread.Sleep(1000);
            if (token.IsCancellationRequested)
            {
                Console.WriteLine("Cancellation observed.");
                throw new OperationCanceledException(token); // acknowledge cancellation
            }
        }
        Console.WriteLine("Done looping");
    }
}

And I have the below HTML code:

<script>
    function BeginLongProcess()
    {
        alert("Will now send AJAX request to start long 6 second process...");
        $.ajax({
            url: "/api/TokenCancellationApi/BeginLongProcess",
            type: "GET",
            dataType: 'json',
            success: function (result) {
                alert(result);
            },
            error: function (xhr, status, error) {
                var err = eval("(" + xhr.responseText + ")");
                console.error(err.Message)
            }
        });
    }
    function CancelLongProcess() {
        $.ajax({
            url: "/api/TokenCancellationApi/CancelLongProcess",
            type: "GET",
            dataType: 'json',
            success: function (result) {
                alert(result);
            },
            error: function (xhr, status, error) {
                var err = eval("(" + xhr.responseText + ")");
                console.error(err.Message)
            }
        });
    }
</script>

<form id="aForm" runat="server">
    <p>
        <button type="button" onclick="BeginLongProcess()">Begin Long Process</button>
    </p>
    <p>
        <button type="button" onclick="CancelLongProcess()">Cancel Long Process</button>
    </p>
</form>

The Web API methods gets called fine. When I click the button to begin the long process, and then hit cancel, I expected it to cancel the long process, and return an alert message telling me it was cancelled.

But that's not the case. Although there was a token cancel request, it doesn't seem to be registering, and the long process keeps on running until it is finished.

Can anyone please tell me why is this not working as I want it to?

like image 427
MTran Avatar asked Dec 06 '15 03:12

MTran


1 Answers

Your thread sleep is deterministic, as such thread won't wake up when you click Cancel. Also, check should be done during iteration if cancel was requested. As you have made the token source static, you can only run one long running call at a time. That is why you have to also check if one has already been kicked off or not before starting your long running process. Added necessary locks to ensure your instances are synchronized properly.

Modified your code a bit, but works as expected. Made it run for configured iterations to test easily. Also, increased sleep to 5 seconds. Change them as needed.

This will also work if you want to run long running method asynchronously. Uncomment the commented code in the begin method.

public class TokenCancellationApiController : ApiController
{
    private static object _lock = new object();
    public static string _lastError;

    // Static types will mean that you can only run 
    // one long running process at a time.
    // If more than 1 needs to run, you will have to 
    // make them instance variable and manage 
    // threading and lifecycle
    private static CancellationTokenSource cTokenSource;       
    private static CancellationToken cToken;

    [HttpGet]
    [Route("api/TokenCancellationApi/BeginLongProcess/{seconds}")]
    public string BeginLongProcess(int seconds)
    {
        //Lock and check if process has already started or not.
        lock (_lock)
        {
            if (null != cTokenSource)
            {
                return "A long running is already underway.";
            }
            cTokenSource = new CancellationTokenSource();
        }

        //if running asynchronously
        //var task = Task.Factory.StartNew(() => LongRunningFunc(cTokenSource.Token, seconds));
        //task.ContinueWith(Cleanup);
        //return "Long running process has started!";

        //if running synchronusly
        try
        {
            LongRunningFunc(cTokenSource.Token, seconds);            
        }
        catch(OperationCanceledException)
        {
            return "The running process has been cancelled";
        }
        catch(Exception ex)
        {
            _lastError = ex.Message;
            return ex.Message;
        }
        finally
        {
            Cleanup(null);
        }
        return "Long running process has completed!";

    }

    [HttpGet]
    public string CancelLongProcess()
    {
        // cancelling task
        if (null != cTokenSource)
        {
            lock (_lock)
            {
                if (null != cTokenSource)
                {
                    cTokenSource.Cancel();
                }
                return "Cancellation Requested";
            }
        }
        else
        {
            return "Long running task already completed";
        }
    }

    [HttpGet]
    public string GetLastError()
    {
        return (string.IsNullOrEmpty(_lastError)) ? "No Error" : _lastError;
    }

    private static void Cleanup(Task task)
    {
        if (null != task && task.IsFaulted)
        {
            System.Diagnostics.Debug.WriteLine("Error encountered while running task");
            _lastError = task.Exception.GetBaseException().Message;
        }

        lock (_lock)
        {
            if (null != cTokenSource)
            {
                cTokenSource.Dispose();
            }
            cTokenSource = null;
        }
    }

    private static void LongRunningFunc(CancellationToken token, int seconds)
    {
        System.Diagnostics.Debug.WriteLine("Long running method");
        int j = 0;

        //Long running loop should always check if cancellation requested.
        while(!token.IsCancellationRequested && j < seconds)            
        {
            //Wait on token instead of deterministic sleep
            //This way, thread will wakeup as soon as canellation
            //is requested even if sleep time hasn't elapsed.
            //Waiting 5 seconds
            token.WaitHandle.WaitOne(5000);
            j++;
        }

        if (token.IsCancellationRequested)
        {
            throw new OperationCanceledException();
        }

        System.Diagnostics.Debug.WriteLine("Done looping");
    }
}

HTML part

<script>
    function BeginLongProcess()
    {
        alert("Will now send AJAX request to start long 6 second process...");
        var seconds = $("#seconds").val();
        $.ajax({
            url: "/api/TokenCancellationApi/BeginLongProcess/"+seconds,
            type: "GET",
            dataType: 'json',
            success: function (result) {
                alert(result);
            },
            error: function (xhr, status, error) {
                var err = eval("(" + xhr.responseText + ")");
                console.error(err.Message)
            }
        });
    }
    function CancelLongProcess() {
        $.ajax({
            url: "/api/TokenCancellationApi/CancelLongProcess",
            type: "GET",
            dataType: 'json',
            success: function (result) {
                alert(result);
            },
            error: function (xhr, status, error) {
                var err = eval("(" + xhr.responseText + ")");
                console.error(err.Message)
            }
        });
    }

    function GetLastError() {
        $.ajax({
            url: "/api/TokenCancellationApi/GetLastError",
            type: "GET",
            dataType: 'json',
            success: function (result) {
                alert(result);
            },
            error: function (xhr, status, error) {
                var err = eval("(" + xhr.responseText + ")");
                console.error(err.Message)
            }
        });
    }
    </script>


    <form id="form1" runat="server">
    <div>
    <p>
       Iterations: <input id="seconds" type="text" value="10" /> <br />
        <button type="button" onclick="BeginLongProcess()">Begin Long Process</button>
    </p>
    <p>
        <button type="button" onclick="CancelLongProcess()">Cancel Long Process</button>
    </p>

    <p>
        <button type="button" onclick="GetLastError()">Get Last Error</button>
    </p>
    </div>
    </form>
like image 194
loopedcode Avatar answered Oct 05 '22 20:10

loopedcode