Blog post Kentico, Technical, .NET, Custom Development

Kentico scheduled tasks and how to run them on a separate thread

Being involved with Kentico for 8 years, I can confidently say that any developer should be well versed with the scheduled tasks feature. They play a huge role in process automation; from database maintenance to content updates, streamlining workflows and generally boosting development efficiency.

Writing a scheduled task in Kentico is straightforward: just implement the ITask interface and add your logic to the Execute method.

public class TestScheduler : ITask
    {
        public string Execute(TaskInfo task)
        {
            //Task code goes here
            return "Result of the task";
        }
    }

Finally, we need to register the task in Kentico

Creating a synchronous task is simple and easy to do but consider a task that demands significant processing time. That’s where asynchronous tasks come into play. They free up the main thread and that way we can avoid potential performance bottlenecks.

Kentico facilitates asynchronous tasks with the “Run task in separate thread” option. However, there is a downside to this feature: access to context data like current page or user information is not shared between the threads.

Now, let's delve into a practical scenario where the demand for synchronizing extensive data from a third-party API arises. In a recent project I was involved in, our client required the synchronization of thousands of data entries from a third-party API daily. Additionally, content had to be created/modified using that data.

The scheduled task that was created for this job is listed below.

public class TestScheduler : ITask
    {
        public string Execute(TaskInfo task)
        {
            string result = "Check event logs for more info!";

            if (task.TaskEnabled)
            {
                var eventLogService = Service.Resolve<IEventLogService>();
                try
                {
                    this.GetDataFromAPI(eventLogService).GetAwaiter().GetResult();
                }
                catch (Exception ex)
                {
                    eventLogService.LogException("TEST_SCHEDULER", nameof(TestScheduler), ex);
                }
            }
            else
            {
                result = "TASK DISABLED";
            }

            return result;
        }

        private async Task GetDataFromAPI(IEventLogService eventLogService)
        {
            var testService = new TestService();
            var result = await testService.CallClientAsync();

            eventLogService.LogInformation("TEST_SCHEDULER", nameof(TestScheduler),
                $"Sync for a site: {SiteContext.CurrentSiteName} completed");
        }
    }


  public class TestService
    {
        public async Task<string> CallClientAsync()
        {
            TestClient client = new TestClient();
            var result = await client.GetAsync();
            return result;
        }

    }



public class TestClient
    {
        public async Task<string> GetAsync()
        {
            HttpResponseMessage response = new HttpResponseMessage();

            using (HttpClient client = new HttpClient())
            {
                try
                {
                    
response = await client.GetAsync("https://google.com").ConfigureAwait(false);

                    if (response.IsSuccessStatusCode)
                    {
                        return await (response.Content?.ReadAsStringAsync()).ConfigureAwait(false);
                    }
                    else
                    {
                        return await response.Content.ReadAsStringAsync();
                    }
                }
                catch (Exception ex)
                {
                    //Log here
                }
            }

            return string.Empty;
        }
    }

By inheriting from ITask and implementing the Execute method the scheduled task is properly created. However, within the Execute method, we're using GetAwaiter to call an asynchronous method called GetDataFromAPI. In GetDataFromAPI, we're utilizing a dummy TestService class, which contains an asynchronous method to initialize a TestClient and fetch data from a third-party service using GetAsync.

After registering the scheduler task in Kentico, we encountered intermittent issues where it would either work correctly or become unresponsive. Upon investigating in Kentico's Debug -> Worker Threads section, we observed our task starting alongside several others, but it failed to complete as intended.

Debugging of the code revealed that the problem originated from the final asynchronous call, specifically within the TestService class; the client.GetAsync() method. The TestService invokes the test client method, which executes, but fails to return the expected result.

To resolve this issue, one could simply check the "Run task in separate thread" checkbox within the scheduler task settings. This would ensure the task would never get stuck again and consistently function as intended. However, this option comes with the drawback mentioned earlier of losing access to the context data.

Fortunately, there is another solution. If you take a closer look you will see that we are using GetAwaiter().GetResult() in the Execute method of the scheduled task which blocks the main thread and creates a deadlock. This is the reason why we got in the Kentico Debug -> Worker Threads app that the other tasks have been started and ours hasn't finished.

To resolve the deadlock issue, simply incorporate ConfigureAwait(false) when calling async methods. By doing so, the Worker Thread will handle the continuation Task without blocking the Main Thread.

In our scenario, adding ConfigureAwait(false) to the GetAsync() call within the GetDataFromAPI method will ensure smooth execution. Here's how the updated GetDataFromAPI method will look:

private async Task GetDataFromAPI(IEventLogService eventLogService)
        {
            var testService = new TestService();
            var result = await testService.CallClientAsync().ConfigureAwait(false);

            eventLogService.LogInformation("TEST_SCHEDULER", nameof(TestScheduler),
                $"Sync for a site: {SiteContext.CurrentSiteName} completed");
        }

Also, the method in TestService should be updated to look like this:

public async Task<string> CallClientAsync()
        {
            TestClient client = new TestClient();
            var result = await client.GetAsync().ConfigureAwait(false);
            return result;
        }

With those changes, there is no need to select Run task in separate thread and the context data is kept.

Contact us to discuss your project.
We're ready to work with you.
Let's talk