Friday, 17 November 2017

interlocked vs lock. specific case.

The article is about of tricky usage of INTERLOCKED!! Lets consider following scenario...

  • We use any service or several services for running/triggering scheduled tasks (for example VisualCron) and we have a Web API which starts some logic invocation which is aimed to do some long-running background work.
  • There is no matter where API is being hosted, it can be hosted within Windows Service or Web Application.
  • We need to be sure that only one command is running at any specific time.
  • Also, we need understand that we can consider a more advanced scenario where Web API can be have multiple instances in other words it can be organized in Web Farm.

So, How can we handle this.

The most straightforward solution is to use lock() statement, in that case all Web API calls will be added to queue right in place of lock statement. Of course, for that particular case is it's not a good idea because of 2 things:

  • we will use additional resource in term of web server. There is a limit of I/O threads. It is not so worth thing if we use async/await statements, but nevertheless, if the API process is pretty loaded it can reduce bandwidth much more
  • we can wait response for some time and even it can fall on server by server timeout.

There is an option to skip that queue and use Interlocked in C#. Consider the following example..


   
    [AllowAnonymous]
    public class TaskController
    {
        private readonly MyTaskCommand _myTaskCommand;
        private static long _myTaskRunsCount = 0;

        [HttpPost]
        [Route("api/Task/RunMyTask")]
        public async Task RunMyTask()
        {
            try
            {
                var prevValue = Interlocked.Read(ref _myTaskRunsCount);
                Interlocked.CompareExchange(ref _myTaskRunsCount, 1, 0);
                var newValue = Interlocked.Read(ref _myTaskRunsCount);

                if (prevValue != newValue)
                {
                    var commandResult = await _myTaskCommand.Execute();

                    if (commandResult.Success)
                        return Ok(commandResult.Result);
                    else
                        return InternalServerError(
                          commandResult.GetException());
                }
                else
                {
                    return Ok($@"Finished without any process. 
One instance is currently running.");
                }
            }
            finally
            {
                Interlocked.CompareExchange(ref _myTaskRunsCount, 0, 1);
            }
        }
}

This example sets flag to Int64 static variable. The flag determines that the command is being run by any application thread. If another thread tries to set it and fails we have a message that "the work is already being done by another thread" in output, otherwise it executes the command. One important thing here is that on any exception we set our flag into initial state, so future threads can run command correctly.

Of course in terms of WEB Farm, this approach will work only within the level of any single WEB Server instance. At the same time we need to admit that collision can take place in very specific case of WEB Farm, viz., calls to WEB API can be routed to different WEB Server instances at any specific time on concurrent WEB API calls from different sources (by source in that case we consider any Service that calls API or any work station that calls API through browser). That behavior of balancing incoming calls is base on how load balancer is tunned.

Possible solutions:

  • Create a level of security for calling this specific URL (can be done in different ways). The main idea is to give access to that action only for particular work instance of task scheduling tool (task scheduling tool can be a SAAS solution or solution written by yourself
  • Tune balancer that way that it will route all calling to API action on a specific web server instance
Note that solution to store duplicating flag (storing 2 separate flags for Web Farm level and instance level) in any distributed cache or in any database can cause collisions too.
Hope you enjoyed!

No comments:

Post a Comment