Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Cross-thread conflicts in StructureMap

I've got an API application that uses multiple database shards, with StructureMap for dependency injection. One of the required headers in each API call is a ShardKey, which tells me which database this call is addressing. To effect this, I have an OwinMiddleware class called ShardingMiddleware, which contains the following code (snipped for clarity):

var nestedContainer = container.GetNestedContainer();
using (var db = MyDbContext.ForShard(shardKey)) // creates a new MyDbContext with connection string appropriate to shardKey
{
    nestedContainer.Configure(cfg => cfg.For<MyDbContext>().Use(db));
    await Next.Invoke(context);
}

This works beautifully in my test environment and passes a battery of integration tests.

But the integration tests are effectively single-threaded. When I deployed this into a QA environment, where a real app is hitting away at my API with multiple simultaneous calls, things start to go pear-shaped. Ferinstance:

System.ObjectDisposedException: Cannot access a disposed object. A common cause of this error is disposing a context that was resolved from dependency injection and then later trying to use the same context instance elsewhere in your application. This may occur is you are calling Dispose() on the context, or wrapping the context in a using statement. If you are using dependency injection, you should let the dependency injection container take care of disposing context instances.

Or other exceptions indicating that StructureMap does not have a valid instance of MyDbContext available.

To me it seems that the multiple threads are somehow messing up each other's configuration, but for the life of me I can't understand how, seeing as I'm using a nested container to store the database context for each API call.

Any ideas what might be going wrong here?

Update: I also tried abstracting my Db context into an interface. Made no real difference; I'm still getting the error

System.InvalidOperationException: An error occurred when trying to create a controller of type 'SomeController'. Make sure that the controller has a parameterless public constructor. ---> StructureMap.StructureMapConfigurationException: No default Instance is registered and cannot be automatically determined for type 'MyNamespace.IMyDbContext'

Update 2: I solved the problem, but the bounty is still open. Please see my answer below.

like image 821
Shaul Behr Avatar asked Aug 04 '16 18:08

Shaul Behr


2 Answers

Well... I solved the problem, but I don't understand why this made a difference.

It boils down to some subtle differences from what I originally posted, which I left out because I thought the details were inconsequential and would have distracted from the question. My container was not, in fact, defined locally; rather it was a protected property of my middleware (it's inherited for integration testing purposes):

protected IContainer Container { get; private set; }

Then there was an initialization call inside the Invoke() method:

Container = context.GetNestedContainer(); // gets the nested container created by a previous middleware class, using the context.Environment dictionary

Using logging statements throughout the method, I got down to the following code (as mentioned in the question, with logging added):

_logger.Debug($"Line 1 Context={context.GetHashCode}, Container={Container.GetHashCode()}");
var db = MyDbContext.ForShard(shardKey.Value);  // no need for "using", since DI will automatically dispose
_logger.Debug($"Line 2 Context={context.GetHashCode}, Container={Container.GetHashCode()}");
Container.Configure(cfg => cfg.For<MyDbContext>().Use(db));
await Next.Invoke(context);

And astoundingly, here's what came out of the logs:

Line 1 Context=56852305, Container=48376271

Line 1 Context=88275661, Container=85736099

Line 2 Context=56852305, Container=85736099

Line 2 Context=88275661, Container=85736099

Amazing! The Container property of my middleware got magically replaced! This, despite the fact that it is defined with a private set, and anyway, just to be safe, I checked through the code for MyDbContext.ForShard() and found nothing that could have messed up the reference for Container.

So what was the solution? I declared a local container variable just after the initialization, and used that instead.

It works now, but I don't understand why or how this could have made a difference.

Bounty goes to the person who can explain this.

like image 159
Shaul Behr Avatar answered Nov 13 '22 15:11

Shaul Behr


You should rewrite this:

using (var db = MyDbContext.ForShard(shardKey)) // creates a new MyDbContext with connection string appropriate to shardKey
{
    nestedContainer.Configure(cfg => cfg.For<MyDbContext>().Use(db));
    await Next.Invoke(context);
}

cause using dispose your dbcontext at the end of using.

You should register factory instead:

var dbFactory = ()=>MyDbContext.ForShard(shardKey);
nestedContainer.Configure(cfg => cfg.For<Func<MyDbContext>>().Use(dbFactory));
await Next.Invoke(context);

and inject this Func instead of dbcontext instance.

like image 44
Kirill Bestemyanov Avatar answered Nov 13 '22 14:11

Kirill Bestemyanov