Skip to content

Commit 404334e

Browse files
Fixed concurrency with mapping method cache (#848)
* Fixed concurrency with mapping method cache * remove unnecessary lock
1 parent 5abb4e7 commit 404334e

File tree

3 files changed

+80
-11
lines changed

3 files changed

+80
-11
lines changed

Neo4j.Driver/Neo4j.Driver.Tests/HomeDbCaching/HomeDbCacheTests.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -144,7 +144,7 @@ public async Task ShouldBeThreadSafe()
144144
cache.TryGetCached(key, out _);
145145
break;
146146

147-
case 2: // Remove and re-add
147+
case 2: // both
148148
cache.AddOrUpdate(key, value);
149149
cache.TryGetCached(key, out _);
150150
break;

Neo4j.Driver/Neo4j.Driver.Tests/Mapping/RecordMappingTests.cs

Lines changed: 70 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,10 @@
1313
// See the License for the specific language governing permissions and
1414
// limitations under the License.
1515

16+
using System;
17+
using System.Collections.Concurrent;
1618
using System.Collections.Generic;
19+
using System.Threading;
1720
using System.Threading.Tasks;
1821
using FluentAssertions;
1922
using Neo4j.Driver.Internal.Types;
@@ -512,6 +515,72 @@ public void AsObject_ShouldMapRecordToObjectType()
512515
result.Should().BeEquivalentTo(expectedPerson);
513516
}
514517

518+
[Fact]
519+
public async Task MapMethods_ShouldBeThreadSafe()
520+
{
521+
var typesToTest = new[]
522+
{
523+
typeof(TestPerson),
524+
typeof(SimpleTestPerson),
525+
typeof(PersonInDict),
526+
typeof(Movie),
527+
typeof(Person),
528+
typeof(ProducingCareer),
529+
typeof(CarAndPainting),
530+
typeof(Painting),
531+
typeof(Car),
532+
typeof(PersonWithoutBornSetter),
533+
typeof(TestPersonWithoutBornMapped),
534+
typeof(Book),
535+
typeof(Author),
536+
typeof(Song),
537+
typeof(ClassWithInitProperties),
538+
typeof(ClassWithDefaultConstructor),
539+
typeof(ClassWithDefaultConstructorWithAttributes),
540+
typeof(TestXY)
541+
};
542+
543+
const int numberOfThreads = 4;
544+
var tasks = new List<Task>(numberOfThreads);
545+
var resetEvent = new ManualResetEventSlim(false);
546+
var exceptions = new ConcurrentBag<Exception>();
547+
548+
for (var i = 0; i < numberOfThreads; i++)
549+
{
550+
tasks.Add(
551+
Task.Run(
552+
() =>
553+
{
554+
try
555+
{
556+
resetEvent.Wait(); // Wait for the signal to start
557+
for (var j = 0; j < 100; j++)
558+
{
559+
foreach (var type in typesToTest)
560+
{
561+
RecordObjectMapping.Instance.GetMapMethodForType(type);
562+
}
563+
564+
RecordObjectMapping.Reset();
565+
}
566+
}
567+
catch (Exception ex)
568+
{
569+
exceptions.Add(ex);
570+
}
571+
}));
572+
}
573+
574+
resetEvent.Set(); // Signal all tasks to start
575+
await Task.WhenAll(tasks);
576+
577+
// Fail the test if any exceptions were caught
578+
if (exceptions.Count > 0)
579+
{
580+
throw new AggregateException("Thread safety issues detected.", exceptions);
581+
}
582+
}
583+
515584
private class TestPerson
516585
{
517586
[MappingDefaultValue("A. Test Name")]
@@ -629,7 +698,7 @@ private class PersonWithoutBornSetter
629698
private class TestPersonWithoutBornMapped
630699
{
631700
[MappingSource("name")]
632-
public string Name { get; set; } = "A. Test Name";
701+
public string Name { get; set; } = "A. Test Name";
633702

634703
[MappingIgnored]
635704
public int? Born { get; set; } = 9999;

Neo4j.Driver/Neo4j.Driver/Public/Mapping/RecordObjectMapping.cs

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414
// limitations under the License.
1515

1616
using System;
17+
using System.Collections.Concurrent;
1718
using System.Collections.Generic;
1819
using System.Reflection;
1920
using Neo4j.Driver.Internal.Mapping;
@@ -51,8 +52,8 @@ internal interface IRecordObjectMapping : IMappingRegistry
5152
/// <summary>Controls global record mapping configuration.</summary>
5253
public class RecordObjectMapping : IRecordObjectMapping
5354
{
54-
private readonly Dictionary<Type, MethodInfo> _mapMethods = new();
55-
private readonly Dictionary<Type, object> _mappers = new();
55+
private readonly ConcurrentDictionary<Type, MethodInfo> _mapMethods = new();
56+
private readonly ConcurrentDictionary<Type, object> _mappers = new();
5657
private readonly IMappingTypeConversionManager _typeConversionManager = new MappingTypeConversionManager();
5758
private IConventionTranslator _conventionTranslator = new NoOpConventionTranslator();
5859

@@ -263,15 +264,14 @@ public static void RegisterProvider(IMappingProvider provider)
263264

264265
public MethodInfo GetMapMethodForType(Type type)
265266
{
266-
if (_mapMethods.TryGetValue(type, out var method))
267+
return _mapMethods.AddOrUpdate(type, GetMapMethod, (_, m) => m);
268+
269+
MethodInfo GetMapMethod(Type t)
267270
{
268-
return method;
271+
var typedInterface = typeof(IRecordMapper<>).MakeGenericType(t);
272+
var methodInfo = typedInterface.GetMethod(nameof(IRecordMapper<object>.Map));
273+
return methodInfo;
269274
}
270-
271-
var typedInterface = typeof(IRecordMapper<>).MakeGenericType(type);
272-
var mapMethod = typedInterface.GetMethod(nameof(IRecordMapper<object>.Map));
273-
_mapMethods[type] = mapMethod;
274-
return mapMethod;
275275
}
276276

277277
/// <summary>Maps a record to an object of the given type according to the global mapping configuration.</summary>

0 commit comments

Comments
 (0)