Johan Björnfot
Aug 31, 2023
  3064
(7 votes)

Parallel Tasks and BackgroundContext

We have recently (CMS.Core 12.16.0) made a change to better support asynchronous async/await execution flows as well as isolation of service resolving from IOC container in parallel contexts.

This change can affect code that spawns up multiple parallel tasks. Such code might face errors like “Cannot create a command without an existing connection” or “The connection is closed”. The reason for this is that to support asynchronous async/await execution is the database context “flowed” together with the other execution context. But as for async/await flows are the execution context also preserved through for example Task.Run or Task.Factory.StartNew calls. This has the consequence that if new tasks are created in parallel then multiple simultaneous tasks will share the same database context, which might cause problem since the database context is not thread-safe. This can for example lead to the exceptions mentioned earlier.

BackgroundContext

To support the scenarios where new tasks/threads are spawned up in parallel we have introduced a service IBackgroundContextFactory (it was introduced in CMS.Core.12.16.0 but there is bug that has been resolved in CMS.Core.12.17.1 so that version or later is recommended). It has a single method like:

    /// <summary>
    /// Used to create a new context that has its own scope for services and request caches.
    /// Recommended to use for background task/threads
    /// </summary>
    public interface IBackgroundContextFactory
    {
        /// <summary>
        /// Creates a new context that has its own scope for services and request caches.
        /// </summary>
        /// <returns>A disposable context.</returns>
        IBackgroundContext Create();
    }

The created context will contain an isolated context for the execution meaning it will have an own scoped IServiceProvider (that will be disposed when the context is disposed) and an isolated execution context (including database context). The execution within this background context also supports asynchronous async/await execution.

The scoped service provider will also ensure the services are preserved as long as the context. This protected from things like ObjectDisposedException which you prior to this could get if you created a parallel task within a http request (like an event handler to for example IContentEvents that starts a background task). Reason for the ObjectDisposedException is that the task will use the scoped container from the http request and then if the http request ends before the background task then you might get ObjectDisposedException.

Recommended usage

If you have code that spawn up new tasks/threads (like for example Task.Run, Task.Factory.StartNew, Task.WhenAll, Parallel.ForEach or unawaited tasks), then if the code to be executed in the task is accessing other services, then the recommendation is to create a background context for the thread. So, say for example you have code like:

        var tasks = new List<Task>();
        foreach (var thing in things)
        {
            tasks.Add(Task.Run(() =>
            {
	           //Some code that run in parallel
            }));
        }

        Task.WaitAll(tasks.ToArray());

Then the suggestion would be to change it to:

        var tasks = new List<Task>();
        foreach (var thing in things)
        {
            tasks.Add(Task.Run(() =>
            {
                using (var backgroundContext = _backgroundContextFactory.Create())
                //below is example on how to get a scoped service
                //ServiceLocator.Current and Injected<> also works with background context (even if usage of those are not recommended as best practice)
                var isolatedScopedService = backgroundContext.Service.Get<IAScopedService>();
                //Some code that run in parallel
            }));
        }

        Task.WaitAll(tasks.ToArray());

So the recommendation is that the first statement within the task/thread should be to create a background context for the task to execute within.

Note that you do not need to (and should not) create a new background context within an “normal” async/await execution flow (that is when you are awaiting the tasks and not executing them in parallel). Because in that case is there only one thread executing simultaneously and, in that case, you do want the execution context (including database context) to flow between the potential different threads that are part of the asynchronous async/await execution flow.

Aug 31, 2023

Comments

Tommy Kihlstrøm
Tommy Kihlstrøm Aug 31, 2023 07:29 PM

How would this be different than using IServi eScopeFactory.CreateScope() to that gives you a scoped service provider. Api looks almost the same and seems to behave the same way.

Johan Björnfot
Johan Björnfot Sep 1, 2023 06:52 AM

Hi,

It will call IServiceScopeFactory.CreateScope() to create a new scope but in addition to that it will also ensure ServiceLocator.Current and Injected<T> is aware and functions with the new scope. It will also create a scoped context for IRequestCache (which is used for example for the database context) that will "flow" with the new context (that is it will work with async/await across threads). 

Quan Mai
Quan Mai Sep 11, 2023 02:09 PM

Just want to add that the bug is fixed in EPiServer.CMS.AspNetCore 12.17.1 (optimizely.com) , not CMS.Core. as CMS Core packages have fixed version dependency so it should just work, but you probably want to make sure that all packages are updated properly 

Please login to comment.
Latest blogs
Copy Optimizely SaaS CMS Settings to ENV Format Via Bookmarklet

Do you work with multiple Optimizely SaaS CMS instances? Use a bookmarklet to automatically copy them to your clipboard, ready to paste into your e...

Daniel Isaacs | Dec 22, 2024 | Syndicated blog

Increase timeout for long running SQL queries using SQL addon

Learn how to increase the timeout for long running SQL queries using the SQL addon.

Tomas Hensrud Gulla | Dec 20, 2024 | Syndicated blog

Overriding the help text for the Name property in Optimizely CMS

I recently received a question about how to override the Help text for the built-in Name property in Optimizely CMS, so I decided to document my...

Tomas Hensrud Gulla | Dec 20, 2024 | Syndicated blog

Resize Images on the Fly with Optimizely DXP's New CDN Feature

With the latest release, you can now resize images on demand using the Content Delivery Network (CDN). This means no more storing multiple versions...

Satata Satez | Dec 19, 2024

Simplify Optimizely CMS Configuration with JSON Schema

Optimizely CMS is a powerful and versatile platform for content management, offering extensive configuration options that allow developers to...

Hieu Nguyen | Dec 19, 2024

Useful Optimizely CMS Web Components

A list of useful Optimizely CMS components that can be used in add-ons.

Bartosz Sekula | Dec 18, 2024 | Syndicated blog