Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Locking in ProviderGraph to prevent duplicate code generation #3109

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
97 changes: 97 additions & 0 deletions src/CoreTests/Bugs/Bug_3083_concurrent_type_generation.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
using System.Collections.Generic;
using System.Threading.Tasks;
using Marten;
using Marten.Internal;
using Marten.Internal.Storage;
using Marten.Schema.BulkLoading;
using Marten.Testing.Harness;
using Shouldly;
using Xunit;

namespace CoreTests.Bugs;

public class Bug_3083_concurrent_type_generation: BugIntegrationContext
{
[Fact]
public async Task concurrent_type_generation()
{
var graph = new ProviderGraph(new StoreOptions());

var tasks = new List<Task<DocumentProvider<SomeDocument>>>();

for (var i = 0; i < 15; ++i)
{
var task = Task.Run(() => graph.StorageFor<SomeDocument>());

tasks.Add(task);
}

var storages = new HashSet<DocumentProvider<SomeDocument>>(ReferenceEqualityComparer.Instance);

foreach (var task in tasks)
{
var storage = await task;
storages.Add(storage);
}

storages.ShouldHaveSingleItem();
}

[Fact]
public async Task concurrent_append_providers()
{
var graph = new ProviderGraph(new StoreOptions());

var tasks = new List<Task>();

var documentProvider1 = new MockDocumentProvider<SomeDocument>();
var documentProvider2 = new MockDocumentProvider<OtherDocument>();
var documentProvider3 = new MockDocumentProvider<ThirdDocument>();
var documentProvider4 = new MockDocumentProvider<ForthDocument>();

tasks.Add(Task.Run(() => graph.Append(documentProvider1)));
tasks.Add(Task.Run(() => graph.Append(documentProvider2)));
tasks.Add(Task.Run(() => graph.Append(documentProvider3)));
tasks.Add(Task.Run(() => graph.Append(documentProvider4)));

await Task.WhenAll(tasks);

graph.StorageFor<SomeDocument>().ShouldBeTheSameAs(documentProvider1);
graph.StorageFor<OtherDocument>().ShouldBeTheSameAs(documentProvider2);
graph.StorageFor<ThirdDocument>().ShouldBeTheSameAs(documentProvider3);
graph.StorageFor<ForthDocument>().ShouldBeTheSameAs(documentProvider4);
}

public class MockDocumentProvider<T>: DocumentProvider<T> where T : notnull
{
public MockDocumentProvider(): this(null, null, null, null, null)
{
}

public MockDocumentProvider(IBulkLoader<T> bulkLoader, IDocumentStorage<T> queryOnly,
IDocumentStorage<T> lightweight, IDocumentStorage<T> identityMap, IDocumentStorage<T> dirtyTracking): base(
bulkLoader, queryOnly, lightweight, identityMap, dirtyTracking)
{
}
}

public class SomeDocument
{
public string Id { get; set; } = string.Empty;
}

public class OtherDocument
{
public string Id { get; set; } = string.Empty;
}

public class ThirdDocument
{
public string Id { get; set; } = string.Empty;
}

public class ForthDocument
{
public string Id { get; set; } = string.Empty;
}
}
21 changes: 20 additions & 1 deletion src/Marten/Internal/ProviderGraph.cs
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ namespace Marten.Internal;
public class ProviderGraph: IProviderGraph
{
private readonly StoreOptions _options;
private readonly object _storageLock = new();
private ImHashMap<Type, object> _storage = ImHashMap<Type, object>.Empty;

public ProviderGraph(StoreOptions options)
Expand All @@ -23,7 +24,10 @@ public ProviderGraph(StoreOptions options)

public void Append<T>(DocumentProvider<T> provider) where T : notnull
{
_storage = _storage.AddOrUpdate(typeof(T), provider);
lock (_storageLock)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Don't do the lock here, it's unnecessary w/ the immutable hash map

Copy link
Contributor Author

@felixkmh felixkmh Apr 2, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not sure how ImHashMaps work exactly, but the reason I also locked it is because I thought that if two threads enter this method concurrently, then they call AddOrUpdate on the same instance. Then they each would create a new map based on that same map instance, resulting in two separate instances that do not contain the item the other thread tried to add. The assignment only happens after AddOrUpdate returns, so whoever assigns to _storage last "wins".

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe I'll just add that as another test case and see if that can happen. Although tests of that nature aren't the most reliable.

Copy link
Contributor Author

@felixkmh felixkmh Apr 2, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Seems that it is at least possible:
image
I can't speak to how likely that is in practice, but I'd argue that the impact of keeping the lock would be negligible.

{
_storage = _storage.AddOrUpdate(typeof(T), provider);
}
}

public DocumentProvider<T> StorageFor<T>() where T : notnull
Expand All @@ -35,6 +39,21 @@ public DocumentProvider<T> StorageFor<T>() where T : notnull
return stored.As<DocumentProvider<T>>();
}

lock (_storageLock)
felixkmh marked this conversation as resolved.
Show resolved Hide resolved
{
if (_storage.TryFind(documentType, out stored))
{
return stored.As<DocumentProvider<T>>();
}

return CreateDocumentProvider<T>();
}
}

private DocumentProvider<T> CreateDocumentProvider<T>() where T : notnull
{
var documentType = typeof(T);

if (documentType == typeof(IEvent))
{
var rules = _options.CreateGenerationRules();
Expand Down
Loading