Going async with worker threads
31st of October, 2016 0 comments

Going async with worker threads

There are many cases where you need a relatively long running task in Kentico. It could be a data import, processing existing data, doing calculations, etc.

One common approach is to write code, in a webpart or an .aspx page, visit the page, and wait for things to happen. This usually ends in a "Request timed out" exception. Then you start the hunt for web.config settings to prevent the timeout from occurring.

There are various techniques used for constantly retrieving information from a server, without user interaction (such as a refresh button) or page loads (F5 until you get what you expected). One common technique is AJAX polling, mainly due to it's compatibility with older browsers and technology.

The trick I usually use to detect this is to open up Chrome Developer Tools, and click on the Network tab. If there are requests constantly popping up in regular intervals, it's probably AJAX polling.

Async callbacks

The AsyncControl .. control.

The "AsyncControl" user control is used throughout the Kentico administration area for long running operations. Whenever you see a loading bar or loading animation, it's most likely an async process taking place.

Macro signatures hashing

Let's give it a go. We can start by adding the control to a new custom webpart.

<cms:AsyncControl runat="server" ID="elemAsync" ProvideLogContext="True" OnOnFinished="elemAsync_OnOnFinished" />

Setting the ProvideLogContext property to true ensures a log is created for our progress messages. The OnOnFinished event is fired when our async operation is completed.

How do we run our async operation then?

Let's expand our webpart layout with a few more controls:

<cms:AsyncControl runat="server" ID="elemAsync" ProvideLogContext="True" OnOnFinished="elemAsync_OnOnFinished" />

<asp:Button runat="server" ID="btnRun" Text="Run" OnClick="btnRun_OnClick" />

<asp:PlaceHolder runat="server" ID="plcSpinner" Visible="False">
  <i class="icon-spinner spinning"></i>
</asp:PlaceHolder>

Alongisde the AsyncControl, we now have a 'Run' button to trigger the process, and a loading spinner to indicate progress.

Now onto the codebehind:

// Executes when the 'Run' button is clicked
protected void btnRun_OnClick(object sender, EventArgs e)
{
    // Show the loading spinner
    plcSpinner.Visible = true;

    // Run the async method
    elemAsync.RunAsync(LongRunningMethod, WindowsIdentity.GetCurrent());
}

// Our long-running async method
private void LongRunningMethod(object parameter)
{
    for (var i = 1; i <= 5; i++)
    {
        Thread.Sleep(10000);
        elemAsync.AddLog("Iteration " + i);
    }
}

// Executes after our async method (LongRunningMethod) completes its execution
protected void elemAsync_OnOnFinished(object sender, EventArgs e)
{
    plcSpinner.Visible = false;
}

Async control

What about cancellation?

The control has a GetCancelScript method which returns JavaScript code to stop the async method from running any longer.

We can then add a simple link or button to stop the execution:

<asp:HyperLink runat="server" ID="btnCancel" Text="Cancel"></asp:HyperLink>

And in the codebehind:

btnCancel.Attributes.Add("onclick", elemAsync.GetCancelScript(true));

Async control with cancellation

Your own worker thread

If you are unable to use the AsyncControl for your asynchronous work, you could use the AsyncWorker class directly.

// Create a new async worker
var worker = new AsyncWorker();

// Run your async method
worker.RunAsync(x = {
  for(var i=0; i<5; i++)
  {
    Thread.Sleep(5000);
  }
}, WindowsIdentity.GetCurrent());

The AsyncWorker class has a Stop method, as well as OnError and OnFinished events. It is important that you keep track of your worker instance, in case you need to perform actions later.

A good way would be to set the ProcessGUID property when creating an instance of the AsyncWorker class. You can then save the GUID somewhere (possibly in the database), and access the same worker process just by creating an instance of the AsyncWorker class with the exact same GUID.

// Generate a GUID
var guid = Guid.NewGuid();

// Create a worker
var worker = new AsyncWorker()
{
  ProcessGUID = guid
};

// Run it
worker.RunAsync(parameter => {
  for(var i=0; i<5; i++)
  {
    Thread.Sleep(5000);
  }
}, WindowsIdentity.GetCurrent());

// Create another worker with the same GUID
var worker2 = new AsyncWorker()
{
  ProcessGUID = guid
};

if(worker2.Status == AsyncWorkerStatusEnum.Running)
{
  // The worker is already running!
}

See what's running

The Debug application has a "Worker threads" tab, where you can see which threads are running, and see the thread progress messages. This is useful information for debugging purposes and performance optimisation.

Worker threads

Thread progress

Scheduled Tasks

Out of the box, most scheduled tasks in Kentico run on the main thread. Most custom-built scheduled tasks also run on the main thread, as this approach is easier than figuring out what the "run task in separate thread" checkbox does. As per the Kentico docs, as long as you do not need access to context variables (current user, current page, etc), the application performance can be improved by ticking this checkbox.

A very useful benefit is the ability to log information during the execution of your scheduled task. A 'live feed' of this information can then be viewed in the aformentioned Debug application.

public class MyScheduledTask : ITask
{
  public string Execute(TaskInfo task)
  {
    // Get the log context of the currently running asynchronous task
    var logContext = LogContext.Current;

    // Add a message to the log
    logContext.AppendText("Running task...");

    for(var i=0; i<10; i++)
    {
      // Run some complex operations
      // . . .

      // Add a message to the log
      logContext.AppendText("Processed 1234 objects");
    }
  }

  return "Done!";
}

Here it is in action:

Async scheduled task log

Written by Kristian Bortnik


Tags

Comments