diff --git a/src/CoreTests/Bugs/Bug_3083_concurrent_type_generation.cs b/src/CoreTests/Bugs/Bug_3083_concurrent_type_generation.cs new file mode 100644 index 0000000000..d3f07e7120 --- /dev/null +++ b/src/CoreTests/Bugs/Bug_3083_concurrent_type_generation.cs @@ -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>>(); + + for (var i = 0; i < 15; ++i) + { + var task = Task.Run(() => graph.StorageFor()); + + tasks.Add(task); + } + + var storages = new HashSet>(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(); + + var documentProvider1 = new MockDocumentProvider(); + var documentProvider2 = new MockDocumentProvider(); + var documentProvider3 = new MockDocumentProvider(); + var documentProvider4 = new MockDocumentProvider(); + + 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().ShouldBeTheSameAs(documentProvider1); + graph.StorageFor().ShouldBeTheSameAs(documentProvider2); + graph.StorageFor().ShouldBeTheSameAs(documentProvider3); + graph.StorageFor().ShouldBeTheSameAs(documentProvider4); + } + + public class MockDocumentProvider: DocumentProvider where T : notnull + { + public MockDocumentProvider(): this(null, null, null, null, null) + { + } + + public MockDocumentProvider(IBulkLoader bulkLoader, IDocumentStorage queryOnly, + IDocumentStorage lightweight, IDocumentStorage identityMap, IDocumentStorage 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; + } +} diff --git a/src/Marten/Internal/ProviderGraph.cs b/src/Marten/Internal/ProviderGraph.cs index 234639d208..8c41948e54 100644 --- a/src/Marten/Internal/ProviderGraph.cs +++ b/src/Marten/Internal/ProviderGraph.cs @@ -14,6 +14,7 @@ namespace Marten.Internal; public class ProviderGraph: IProviderGraph { private readonly StoreOptions _options; + private readonly object _storageLock = new(); private ImHashMap _storage = ImHashMap.Empty; public ProviderGraph(StoreOptions options) @@ -23,7 +24,10 @@ public ProviderGraph(StoreOptions options) public void Append(DocumentProvider provider) where T : notnull { - _storage = _storage.AddOrUpdate(typeof(T), provider); + lock (_storageLock) + { + _storage = _storage.AddOrUpdate(typeof(T), provider); + } } public DocumentProvider StorageFor() where T : notnull @@ -35,6 +39,21 @@ public DocumentProvider StorageFor() where T : notnull return stored.As>(); } + lock (_storageLock) + { + if (_storage.TryFind(documentType, out stored)) + { + return stored.As>(); + } + + return CreateDocumentProvider(); + } + } + + private DocumentProvider CreateDocumentProvider() where T : notnull + { + var documentType = typeof(T); + if (documentType == typeof(IEvent)) { var rules = _options.CreateGenerationRules();