IContentRepository - Thread safe?

Vote:
 

When using IContentRepository in a service injected by th constructor (Depencency injection).
Our service is registrated as Transient.

When calling:
_contentRepository.GetReferencesToContent(content.ContentLink, includeDecendents);

Sometimes we get the following stacktrace:
System.AggregateException: One or more errors occurred. (Call on database executor not created on current context and on different thread. Possible rooted IDatabaseExecutor, which is not thread safe.)
 ---> System.InvalidOperationException: Call on database executor not created on current context and on different thread. Possible rooted IDatabaseExecutor, which is not thread safe.
   at EPiServer.Data.Internal.ConnectionScopeResolver.AssertThreadSafe()
   at EPiServer.Data.Providers.Internal.SqlDatabaseExecutor.CreateCommand()
   at EPiServer.DataAccess.Internal.ContentSaveDB.<>c__DisplayClass20_0.<GetReferenceInformationForContent>b__0()
   at EPiServer.Data.Providers.Internal.SqlDatabaseExecutor.<>c__DisplayClass24_0`1.<Execute>b__0()
   at EPiServer.Data.Providers.SqlTransientErrorsRetryPolicy.Execute[TResult](Func`1 method)
   at EPiServer.DataAccess.Internal.ContentSaveDB.GetReferenceInformationForContent(ContentReference contentLink, Boolean includeDecendents)
   at EPiServer.Core.Internal.DefaultContentProvider.GetReferencesToLocalContent(ContentReference contentLink, Boolean includeDecendents)
   at EPiServer.Core.Internal.DefaultContentRepository.GetReferencesToContent(ContentReference contentLink, IEnumerable`1 descendents, Boolean includeDescendants)

Is it intended by design that the DbContext inside IContentRepository should not be thread-safe?

Version: 12.4.1
.Net 6.0

#278797
Apr 20, 2022 13:20
Vote:
 

Seems like a bug I created CMS-23353

#279173
Apr 25, 2022 17:12
Vote:
 

I think using IContentSoftLinkRepository is a better way to get content references?

#279226
Apr 26, 2022 3:28
Vote:
 

Thanks for the suggestion.
I did a try to use IContentSoftLinkRepository.
Still got the same/similar issue:


System.AggregateException: One or more errors occurred. (Call on database executor not created on current context and on different thread. Possible rooted IDatabaseExecutor, which is not thread safe.)
 ---> System.InvalidOperationException: Call on database executor not created on current context and on different thread. Possible rooted IDatabaseExecutor, which is not thread safe.
   at EPiServer.Data.Internal.ConnectionScopeResolver.AssertThreadSafe()
   at EPiServer.Data.Providers.Internal.SqlDatabaseExecutor.CreateCommand()
   at EPiServer.DataAccess.DataAccessBase.CreateCommand()
   at EPiServer.DataAccess.Internal.ContentCoreDataDB.<>c__DisplayClass4_0.<LoadInternal>b__0()
   at EPiServer.Data.Providers.Internal.SqlDatabaseExecutor.<>c__DisplayClass24_0`1.<Execute>b__0()
   at EPiServer.Data.Providers.SqlTransientErrorsRetryPolicy.Execute[TResult](Func`1 method)
   at EPiServer.Core.Internal.DefaultContentProvider.ResolveContent(ContentReference contentLink)
   at EPiServer.Web.Internal.PermanentContentLinkMapper.FindInternal(ContentReference contentLink)
   at EPiServer.Web.Internal.PermanentLinkMapper.Find(ContentReference contentReference)
   at EPiServer.DataAbstraction.Internal.DefaultContentSoftLinkRepository.Load(ContentReference contentLink, Boolean reversed)

#279239
Apr 26, 2022 14:02
Vote:
 

IContentRepository is Singleton scoped and should be fine to root inside another singleton (with some exceptions that follows). The underlying IDatabaseExecutor is backed in http context (so it can be resued during transactions). So you might run into this issue if you in a single http request end up with several threads using the same scope, e.g. if you would have some code like:

 var contentLinks = new[] {new ContentReference(1), new ContentReference(2), new ContentReference(3)};
            Parallel.ForEach(contentLinks, link =>
            {
                contentRepository.GetDescendents(link);
            }); 

Reason is that AsyncLocal "flows" from main thread to new thread, and hence HttpContext. The above code could be rewritten as 

 public class NoHttpContextScope : IDisposable
    {
        private readonly IHttpContextAccessor _httpContextAccessor;
        private readonly HttpContext _context;        
        public NoHttpContextScope(IHttpContextAccessor httpContextAccessor)
        {
            _httpContextAccessor = httpContextAccessor;
            _context = httpContextAccessor.HttpContext;
            httpContextAccessor.HttpContext = null;
        }
        public void Dispose() => _httpContextAccessor.HttpContext = _context;
    } 
 var contentLinks = new[] { new ContentReference(1), new ContentReference(2), new ContentReference(3), new ContentReference(4) };
            using (var noHttpContextScope = new NoHttpContextScope(httpContextAccessor))
            {
                var result = Parallel.ForEach(contentLinks, link =>
                {
                    contentRepository.GetDescendents(link);
                });
            } 

So is your case that you spawn up new parallel threads within a http request, and that the exception is thrown within a new treads execution? Or is there another usecase, if so please provide more information, e.g. is the code within a http request or outside a http request (like a scehduled job)? 

#279321
Apr 27, 2022 15:26
Vote:
 

Hi!
Thank you for the great detailed reponse.

Our use-case when this problem arises is when we create a fire and forget task
inside our handler for contentEvents.PublishedContent
Task.Run(async () => await invalidationCacheService.InvalidateCacheByContentAsync(e.Content)

The method running is not spawning up new threads as we are aware of but the thread we are creating ofcourse is a new thread with a new id.
The reason for us creating a fire and forget task is since we dont want to block the UI while invalidating the cache.

By the way is there any easy way to check if you are withing a current HttpRequestContext or not?

Our initial thoughts on the problem was that the problem arises when different editors triggered the PublishedContent event at the same time.
But since you say that the context is backed by the HttpContext we are not so sure anymore?

#279392
Apr 28, 2022 7:53
Vote:
 

If you inside you Task.Run logic run
httpContextAccessor.HttpContext = null;
it should solve your problem.

We'll keep the bug to see if we can improve this, but the above should be a workaround for now.

#280176
May 11, 2022 16:01
* You are NOT allowed to include any hyperlinks in the post because your account hasn't associated to your company. User profile should be updated.