Skip to content

Commit 6ae30d5

Browse files
NH-3977 - Thread safety weaknesses of MapBasedSessionContext, test case and fix.
1 parent 07c654b commit 6ae30d5

File tree

3 files changed

+106
-14
lines changed

3 files changed

+106
-14
lines changed
Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
using System.Collections;
2+
using System.Threading;
3+
using NHibernate.Cfg;
4+
using NHibernate.Context;
5+
using NHibernate.Engine;
6+
using NUnit.Framework;
7+
8+
namespace NHibernate.Test.ConnectionTest
9+
{
10+
[TestFixture]
11+
public class MapBasedSessionContextFixture : ConnectionManagementTestCase
12+
{
13+
protected override ISession GetSessionUnderTest()
14+
{
15+
ISession session = OpenSession();
16+
session.BeginTransaction();
17+
return session;
18+
}
19+
20+
protected override void Configure(Configuration configuration)
21+
{
22+
base.Configure(cfg);
23+
cfg.SetProperty(Environment.CurrentSessionContextClass, typeof(TestableMapBasedSessionContext).AssemblyQualifiedName);
24+
}
25+
26+
protected override void OnSetUp()
27+
{
28+
TestableMapBasedSessionContext._map = null;
29+
}
30+
31+
[Test]
32+
public void MapContextThreadSafety()
33+
{
34+
using (var factory1 = cfg.BuildSessionFactory())
35+
using (var session1 = factory1.OpenSession())
36+
using (var factory2 = cfg.BuildSessionFactory())
37+
using (var session2 = factory2.OpenSession())
38+
{
39+
var thread1 = new Thread(() =>
40+
{
41+
CurrentSessionContext.Bind(session1);
42+
});
43+
44+
var thread2 = new Thread(() =>
45+
{
46+
CurrentSessionContext.Bind(session2);
47+
});
48+
49+
thread1.Start();
50+
thread2.Start();
51+
thread1.Join();
52+
thread2.Join();
53+
54+
Assert.IsTrue(CurrentSessionContext.HasBind(factory1), $"No session bound to \"{nameof(factory1)}\" factory.");
55+
Assert.IsTrue(CurrentSessionContext.HasBind(factory2), $"No session bound to \"{nameof(factory2)}\" factory.");
56+
}
57+
}
58+
}
59+
60+
public class TestableMapBasedSessionContext : MapBasedSessionContext
61+
{
62+
public TestableMapBasedSessionContext(ISessionFactoryImplementor factory) : base(factory) { }
63+
64+
// Context is the app with such implementation. Just for the test case.
65+
internal static IDictionary _map;
66+
67+
protected override IDictionary GetMap()
68+
{
69+
return _map;
70+
}
71+
72+
protected override void SetMap(IDictionary value)
73+
{
74+
// Give a fair chance to have a concurrency bug if base implementation is not thread safe.
75+
Thread.Sleep(100);
76+
_map = value;
77+
}
78+
}
79+
}

src/NHibernate.Test/NHibernate.Test.csproj

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -206,6 +206,7 @@
206206
<Compile Include="ConnectionTest\ConnectionManagementTestCase.cs" />
207207
<Compile Include="ConnectionTest\Other.cs" />
208208
<Compile Include="ConnectionTest\Silly.cs" />
209+
<Compile Include="ConnectionTest\MapBasedSessionContextFixture.cs" />
209210
<Compile Include="ConnectionTest\ThreadLocalCurrentSessionTest.cs" />
210211
<Compile Include="ConventionsTestCase.cs" />
211212
<Compile Include="Criteria\AddNumberProjection.cs" />

src/NHibernate/Context/MapBasedSessionContext.cs

Lines changed: 26 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
using System.Collections;
2-
2+
using System.Collections.Concurrent;
33
using NHibernate.Engine;
44

55
namespace NHibernate.Context
@@ -8,6 +8,9 @@ public abstract class MapBasedSessionContext : CurrentSessionContext
88
{
99
private readonly ISessionFactoryImplementor _factory;
1010

11+
// Must be static, different instances of MapBasedSessionContext may have to yield the same map.
12+
private static readonly object _locker = new object();
13+
1114
protected MapBasedSessionContext(ISessionFactoryImplementor factory)
1215
{
1316
_factory = factory;
@@ -20,30 +23,39 @@ protected override ISession Session
2023
{
2124
get
2225
{
23-
IDictionary map = GetMap();
24-
if (map == null)
25-
{
26-
return null;
27-
}
28-
else
29-
{
30-
return map[_factory] as ISession;
31-
}
26+
ISession value = null;
27+
GetConcreteMap()?.TryGetValue(_factory, out value);
28+
// We want null if no value was there, no need to explicitly handle false outcome of TryGetValue.
29+
return value;
3230
}
3331
set
3432
{
35-
IDictionary map = GetMap();
33+
var map = GetConcreteMap();
3634
if (map == null)
3735
{
38-
map = new Hashtable();
39-
SetMap(map);
36+
// Double check locking. Cannot use a Lazy<T> for such a semantic: the map can be bound
37+
// to some execution context through SetMap/GetMap, a Lazy<T> would defeat those methods purpose.
38+
lock (_locker)
39+
{
40+
map = GetConcreteMap();
41+
if (map == null)
42+
{
43+
map = new ConcurrentDictionary<ISessionFactoryImplementor, ISession>();
44+
SetMap(map);
45+
}
46+
}
4047
}
4148
map[_factory] = value;
4249
}
4350
}
4451

52+
private ConcurrentDictionary<ISessionFactoryImplementor, ISession> GetConcreteMap()
53+
{
54+
return (ConcurrentDictionary<ISessionFactoryImplementor, ISession>)GetMap();
55+
}
56+
4557
/// <summary>
46-
/// Get the dictionary mapping session factory to its current session.
58+
/// Get the dictionary mapping session factory to its current session. Yield <c>null</c> if none have been set.
4759
/// </summary>
4860
protected abstract IDictionary GetMap();
4961

0 commit comments

Comments
 (0)