diff --git a/doc/reference/modules/configuration.xml b/doc/reference/modules/configuration.xml
index 8540764df70..1c4d75e800b 100644
--- a/doc/reference/modules/configuration.xml
+++ b/doc/reference/modules/configuration.xml
@@ -610,6 +610,26 @@ var session = sessions.OpenSession(conn);
+
+
+ cache.read_write_lock_factory
+
+
+ Specify the cache lock factory to use for read-write cache regions.
+ Defaults to the built-in async cache lock factory.
+
+ eg.
+ async, or sync, or classname.of.CacheLockFactory, assembly with custom implementation of ICacheReadWriteLockFactory
+
+
+ async uses a single writer multiple readers locking mechanism supporting asynchronous operations.
+
+
+ sync uses a single access locking mechanism which will throw on asynchronous
+ operations but may have better performances than the async provider for applications using the .Net Framework (4.8 and below).
+
+
+
cache.region_prefix
diff --git a/src/AsyncGenerator.yml b/src/AsyncGenerator.yml
index 5c7754819fb..97e7104beff 100644
--- a/src/AsyncGenerator.yml
+++ b/src/AsyncGenerator.yml
@@ -222,6 +222,8 @@
- conversion: Ignore
anyBaseTypeRule: IsTestCase
executionPhase: PostProviders
+ - conversion: Ignore
+ name: SyncOnlyCacheFixture
ignoreDocuments:
- filePathEndsWith: Linq/MathTests.cs
- filePathEndsWith: Linq/ExpressionSessionLeakTest.cs
diff --git a/src/NHibernate.Test/Async/CacheTest/CacheFixture.cs b/src/NHibernate.Test/Async/CacheTest/CacheFixture.cs
index 5d12747ec68..642d66e631b 100644
--- a/src/NHibernate.Test/Async/CacheTest/CacheFixture.cs
+++ b/src/NHibernate.Test/Async/CacheTest/CacheFixture.cs
@@ -19,7 +19,7 @@ namespace NHibernate.Test.CacheTest
{
using System.Threading.Tasks;
[TestFixture]
- public class CacheFixtureAsync
+ public class CacheFixtureAsync: TestCase
{
[Test]
public async Task TestSimpleCacheAsync()
@@ -27,14 +27,14 @@ public async Task TestSimpleCacheAsync()
await (DoTestCacheAsync(new HashtableCacheProvider()));
}
- private CacheKey CreateCacheKey(string text)
+ protected CacheKey CreateCacheKey(string text)
{
return new CacheKey(text, NHibernateUtil.String, "Foo", null, null);
}
public async Task DoTestCacheAsync(ICacheProvider cacheProvider, CancellationToken cancellationToken = default(CancellationToken))
{
- var cache = cacheProvider.BuildCache(typeof(String).FullName, new Dictionary());
+ var cache = (CacheBase) cacheProvider.BuildCache(typeof(String).FullName, new Dictionary());
long longBefore = Timestamper.Next();
@@ -44,8 +44,7 @@ private CacheKey CreateCacheKey(string text)
await (Task.Delay(15, cancellationToken));
- ICacheConcurrencyStrategy ccs = new ReadWriteCache();
- ccs.Cache = cache;
+ ICacheConcurrencyStrategy ccs = CreateCache(cache);
// cache something
CacheKey fooKey = CreateCacheKey("foo");
@@ -155,12 +154,17 @@ private CacheKey CreateCacheKey(string text)
public async Task MinValueTimestampAsync()
{
var cache = new HashtableCacheProvider().BuildCache("region", new Dictionary());
- ICacheConcurrencyStrategy strategy = new ReadWriteCache();
- strategy.Cache = cache;
- await (DoTestMinValueTimestampOnStrategyAsync(cache, new ReadWriteCache()));
- await (DoTestMinValueTimestampOnStrategyAsync(cache, new NonstrictReadWriteCache()));
- await (DoTestMinValueTimestampOnStrategyAsync(cache, new ReadOnlyCache()));
+ await (DoTestMinValueTimestampOnStrategyAsync(cache, CreateCache(cache)));
+ await (DoTestMinValueTimestampOnStrategyAsync(cache, CreateCache(cache, CacheFactory.NonstrictReadWrite)));
+ await (DoTestMinValueTimestampOnStrategyAsync(cache, CreateCache(cache, CacheFactory.ReadOnly)));
}
+
+ protected virtual ICacheConcurrencyStrategy CreateCache(CacheBase cache, string strategy = CacheFactory.ReadWrite)
+ {
+ return CacheFactory.CreateCache(strategy, cache, Sfi.Settings);
+ }
+
+ protected override string[] Mappings => Array.Empty();
}
}
diff --git a/src/NHibernate.Test/CacheTest/CacheFixture.cs b/src/NHibernate.Test/CacheTest/CacheFixture.cs
index 5c86f6ced46..5419955ee13 100644
--- a/src/NHibernate.Test/CacheTest/CacheFixture.cs
+++ b/src/NHibernate.Test/CacheTest/CacheFixture.cs
@@ -8,7 +8,7 @@
namespace NHibernate.Test.CacheTest
{
[TestFixture]
- public class CacheFixture
+ public class CacheFixture: TestCase
{
[Test]
public void TestSimpleCache()
@@ -16,14 +16,14 @@ public void TestSimpleCache()
DoTestCache(new HashtableCacheProvider());
}
- private CacheKey CreateCacheKey(string text)
+ protected CacheKey CreateCacheKey(string text)
{
return new CacheKey(text, NHibernateUtil.String, "Foo", null, null);
}
public void DoTestCache(ICacheProvider cacheProvider)
{
- var cache = cacheProvider.BuildCache(typeof(String).FullName, new Dictionary());
+ var cache = (CacheBase) cacheProvider.BuildCache(typeof(String).FullName, new Dictionary());
long longBefore = Timestamper.Next();
@@ -33,8 +33,7 @@ public void DoTestCache(ICacheProvider cacheProvider)
Thread.Sleep(15);
- ICacheConcurrencyStrategy ccs = new ReadWriteCache();
- ccs.Cache = cache;
+ ICacheConcurrencyStrategy ccs = CreateCache(cache);
// cache something
CacheKey fooKey = CreateCacheKey("foo");
@@ -144,12 +143,17 @@ private void DoTestMinValueTimestampOnStrategy(CacheBase cache, ICacheConcurrenc
public void MinValueTimestamp()
{
var cache = new HashtableCacheProvider().BuildCache("region", new Dictionary());
- ICacheConcurrencyStrategy strategy = new ReadWriteCache();
- strategy.Cache = cache;
- DoTestMinValueTimestampOnStrategy(cache, new ReadWriteCache());
- DoTestMinValueTimestampOnStrategy(cache, new NonstrictReadWriteCache());
- DoTestMinValueTimestampOnStrategy(cache, new ReadOnlyCache());
+ DoTestMinValueTimestampOnStrategy(cache, CreateCache(cache));
+ DoTestMinValueTimestampOnStrategy(cache, CreateCache(cache, CacheFactory.NonstrictReadWrite));
+ DoTestMinValueTimestampOnStrategy(cache, CreateCache(cache, CacheFactory.ReadOnly));
}
+
+ protected virtual ICacheConcurrencyStrategy CreateCache(CacheBase cache, string strategy = CacheFactory.ReadWrite)
+ {
+ return CacheFactory.CreateCache(strategy, cache, Sfi.Settings);
+ }
+
+ protected override string[] Mappings => Array.Empty();
}
}
diff --git a/src/NHibernate.Test/CacheTest/SyncOnlyCacheFixture.cs b/src/NHibernate.Test/CacheTest/SyncOnlyCacheFixture.cs
new file mode 100644
index 00000000000..267b74d015a
--- /dev/null
+++ b/src/NHibernate.Test/CacheTest/SyncOnlyCacheFixture.cs
@@ -0,0 +1,33 @@
+using System;
+using System.Collections.Generic;
+using System.Threading;
+using NHibernate.Cache;
+using NHibernate.Cfg;
+using NUnit.Framework;
+using Environment = NHibernate.Cfg.Environment;
+
+namespace NHibernate.Test.CacheTest
+{
+ [TestFixture]
+ public class SyncOnlyCacheFixture : CacheFixture
+ {
+ protected override void Configure(Configuration cfg)
+ {
+ base.Configure(cfg);
+ cfg.SetProperty(Environment.CacheReadWriteLockFactory, "sync");
+ }
+
+ [Test]
+ public void AsyncOperationsThrow()
+ {
+ var cache = new HashtableCacheProvider().BuildCache("region", new Dictionary());
+ var strategy = CreateCache(cache);
+ CacheKey key = CreateCacheKey("key");
+ var stamp = Timestamper.Next();
+ Assert.ThrowsAsync(
+ () =>
+ strategy.PutAsync(key, "value", stamp, 0, null, false, default(CancellationToken)));
+ Assert.ThrowsAsync(() => strategy.GetAsync(key, stamp, default(CancellationToken)));
+ }
+ }
+}
diff --git a/src/NHibernate/Cache/CacheFactory.cs b/src/NHibernate/Cache/CacheFactory.cs
index b6c5df8549d..96c44ff3f88 100644
--- a/src/NHibernate/Cache/CacheFactory.cs
+++ b/src/NHibernate/Cache/CacheFactory.cs
@@ -1,6 +1,7 @@
using System;
using System.Collections.Generic;
using NHibernate.Cfg;
+using NHibernate.Util;
namespace NHibernate.Cache
{
@@ -43,7 +44,7 @@ public static ICacheConcurrencyStrategy CreateCache(
var cache = BuildCacheBase(name, settings, properties);
- var ccs = CreateCache(usage, cache);
+ var ccs = CreateCache(usage, cache, settings);
if (mutable && usage == ReadOnly)
log.Warn("read-only cache configured for mutable: {0}", name);
@@ -57,7 +58,21 @@ public static ICacheConcurrencyStrategy CreateCache(
/// The name of the strategy that should use for the class.
/// The used for this strategy.
/// An to use for this object in the .
+ // TODO: Since v5.4
+ //[Obsolete("Please use overload with a CacheBase and Settings parameters.")]
public static ICacheConcurrencyStrategy CreateCache(string usage, CacheBase cache)
+ {
+ return CreateCache(usage, cache, null);
+ }
+
+ ///
+ /// Creates an from the parameters.
+ ///
+ /// The name of the strategy that should use for the class.
+ /// The used for this strategy.
+ /// NHibernate settings
+ /// An to use for this object in the .
+ public static ICacheConcurrencyStrategy CreateCache(string usage, CacheBase cache, Settings settings)
{
if (log.IsDebugEnabled())
log.Debug("cache for: {0} usage strategy: {1}", cache.RegionName, usage);
@@ -69,7 +84,7 @@ public static ICacheConcurrencyStrategy CreateCache(string usage, CacheBase cach
ccs = new ReadOnlyCache();
break;
case ReadWrite:
- ccs = new ReadWriteCache();
+ ccs = new ReadWriteCache(settings == null ? new AsyncReaderWriterLock() : settings.CacheReadWriteLockFactory.Create());
break;
case NonstrictReadWrite:
ccs = new NonstrictReadWriteCache();
diff --git a/src/NHibernate/Cache/ICacheLock.cs b/src/NHibernate/Cache/ICacheLock.cs
new file mode 100644
index 00000000000..4fbf16c7180
--- /dev/null
+++ b/src/NHibernate/Cache/ICacheLock.cs
@@ -0,0 +1,63 @@
+using System;
+using System.Threading.Tasks;
+using NHibernate.Util;
+
+namespace NHibernate.Cache
+{
+ ///
+ /// Implementors provide a locking mechanism for the cache.
+ ///
+ public interface ICacheLock : IDisposable
+ {
+ ///
+ /// Acquire synchronously a read lock.
+ ///
+ /// A read lock.
+ IDisposable ReadLock();
+
+ ///
+ /// Acquire synchronously a write lock.
+ ///
+ /// A write lock.
+ IDisposable WriteLock();
+
+ ///
+ /// Acquire asynchronously a read lock.
+ ///
+ /// A read lock.
+ Task ReadLockAsync();
+
+ ///
+ /// Acquire asynchronously a write lock.
+ ///
+ /// A write lock.
+ Task WriteLockAsync();
+ }
+
+ ///
+ /// Define a factory for cache locks.
+ ///
+ public interface ICacheReadWriteLockFactory
+ {
+ ///
+ /// Create a cache lock provider.
+ ///
+ ICacheLock Create();
+ }
+
+ internal class AsyncCacheReadWriteLockFactory : ICacheReadWriteLockFactory
+ {
+ public ICacheLock Create()
+ {
+ return new AsyncReaderWriterLock();
+ }
+ }
+
+ internal class SyncCacheReadWriteLockFactory : ICacheReadWriteLockFactory
+ {
+ public ICacheLock Create()
+ {
+ return new SyncCacheLock();
+ }
+ }
+}
diff --git a/src/NHibernate/Cache/ReadWriteCache.cs b/src/NHibernate/Cache/ReadWriteCache.cs
index 9bb25e51048..fbeff50c540 100644
--- a/src/NHibernate/Cache/ReadWriteCache.cs
+++ b/src/NHibernate/Cache/ReadWriteCache.cs
@@ -36,7 +36,16 @@ public interface ILockable
private CacheBase _cache;
private int _nextLockId;
- private readonly AsyncReaderWriterLock _asyncReaderWriterLock = new AsyncReaderWriterLock();
+ private readonly ICacheLock _asyncReaderWriterLock;
+
+ public ReadWriteCache() : this(new AsyncReaderWriterLock())
+ {
+ }
+
+ public ReadWriteCache(ICacheLock locker)
+ {
+ _asyncReaderWriterLock = locker;
+ }
///
/// Gets the cache region name.
diff --git a/src/NHibernate/Cache/SyncCacheLock.cs b/src/NHibernate/Cache/SyncCacheLock.cs
new file mode 100644
index 00000000000..129c469cfd2
--- /dev/null
+++ b/src/NHibernate/Cache/SyncCacheLock.cs
@@ -0,0 +1,54 @@
+using System;
+using System.Threading;
+using System.Threading.Tasks;
+
+namespace NHibernate.Cache
+{
+ class SyncCacheLock : ICacheLock
+ {
+ class MonitorLock : IDisposable
+ {
+ private readonly object _lockObj;
+
+ public MonitorLock(object lockObj)
+ {
+ Monitor.Enter(lockObj);
+ _lockObj = lockObj;
+ }
+
+ public void Dispose()
+ {
+ Monitor.Exit(_lockObj);
+ }
+ }
+
+ public void Dispose()
+ {
+ }
+
+ public IDisposable ReadLock()
+ {
+ return new MonitorLock(this);
+ }
+
+ public IDisposable WriteLock()
+ {
+ return new MonitorLock(this);
+ }
+
+ public Task ReadLockAsync()
+ {
+ throw AsyncNotSupporteException();
+ }
+
+ public Task WriteLockAsync()
+ {
+ throw AsyncNotSupporteException();
+ }
+
+ private static InvalidOperationException AsyncNotSupporteException()
+ {
+ return new InvalidOperationException("This locker supports only sync operations. Change 'cache.read_write_lock_factory' setting to `async` to support async operations.");
+ }
+ }
+}
diff --git a/src/NHibernate/Cache/UpdateTimestampsCache.cs b/src/NHibernate/Cache/UpdateTimestampsCache.cs
index f6851f5ed44..b7c0f5cdba6 100644
--- a/src/NHibernate/Cache/UpdateTimestampsCache.cs
+++ b/src/NHibernate/Cache/UpdateTimestampsCache.cs
@@ -19,7 +19,7 @@ public partial class UpdateTimestampsCache
{
private static readonly INHibernateLogger log = NHibernateLogger.For(typeof(UpdateTimestampsCache));
private readonly CacheBase _updateTimestamps;
- private readonly AsyncReaderWriterLock _asyncReaderWriterLock = new AsyncReaderWriterLock();
+ private readonly ICacheLock _asyncReaderWriterLock;
public virtual void Clear()
{
@@ -40,11 +40,21 @@ public UpdateTimestampsCache(Settings settings, IDictionary prop
///
/// Build the update timestamps cache.
/// x
- /// The to use.
- public UpdateTimestampsCache(CacheBase cache)
+ /// The to use.
+ public UpdateTimestampsCache(CacheBase cache) : this(cache, new AsyncReaderWriterLock())
+ {
+ }
+
+ ///
+ /// Build the update timestamps cache.
+ ///
+ /// The to use.
+ /// Locker to use.
+ public UpdateTimestampsCache(CacheBase cache, ICacheLock locker)
{
log.Info("starting update timestamps cache at region: {0}", cache.RegionName);
_updateTimestamps = cache;
+ _asyncReaderWriterLock = locker;
}
//Since v5.1
diff --git a/src/NHibernate/Cfg/Environment.cs b/src/NHibernate/Cfg/Environment.cs
index 973657f803d..10b83be17a1 100644
--- a/src/NHibernate/Cfg/Environment.cs
+++ b/src/NHibernate/Cfg/Environment.cs
@@ -165,6 +165,7 @@ public static string Version
public const string CacheProvider = "cache.provider_class";
public const string UseQueryCache = "cache.use_query_cache";
public const string QueryCacheFactory = "cache.query_cache_factory";
+ public const string CacheReadWriteLockFactory = "cache.read_write_lock_factory";
public const string UseSecondLevelCache = "cache.use_second_level_cache";
public const string CacheRegionPrefix = "cache.region_prefix";
public const string UseMinimalPuts = "cache.use_minimal_puts";
diff --git a/src/NHibernate/Cfg/Settings.cs b/src/NHibernate/Cfg/Settings.cs
index f973766013e..f1c9c03fa8c 100644
--- a/src/NHibernate/Cfg/Settings.cs
+++ b/src/NHibernate/Cfg/Settings.cs
@@ -102,6 +102,8 @@ public Settings()
public ICacheProvider CacheProvider { get; internal set; }
+ public ICacheReadWriteLockFactory CacheReadWriteLockFactory { get; internal set; }
+
public IQueryCacheFactory QueryCacheFactory { get; internal set; }
public IConnectionProvider ConnectionProvider { get; internal set; }
diff --git a/src/NHibernate/Cfg/SettingsFactory.cs b/src/NHibernate/Cfg/SettingsFactory.cs
index f1f1a0ffb29..d9377773fb9 100644
--- a/src/NHibernate/Cfg/SettingsFactory.cs
+++ b/src/NHibernate/Cfg/SettingsFactory.cs
@@ -206,6 +206,7 @@ public Settings BuildSettings(IDictionary properties)
if (useSecondLevelCache || useQueryCache)
{
+ settings.CacheReadWriteLockFactory = GetReadWriteLockFactory(PropertiesHelper.GetString(Environment.CacheReadWriteLockFactory, properties, null));
// The cache provider is needed when we either have second-level cache enabled
// or query cache enabled. Note that useSecondLevelCache is enabled by default
settings.CacheProvider = CreateCacheProvider(properties);
@@ -337,6 +338,28 @@ public Settings BuildSettings(IDictionary properties)
return settings;
}
+ private ICacheReadWriteLockFactory GetReadWriteLockFactory(string lockFactory)
+ {
+ switch (lockFactory)
+ {
+ case null:
+ case "async":
+ return new AsyncCacheReadWriteLockFactory();
+ case "sync":
+ return new SyncCacheReadWriteLockFactory();
+ default:
+ try
+ {
+ var type = ReflectHelper.ClassForName(lockFactory);
+ return (ICacheReadWriteLockFactory) Environment.ObjectsFactory.CreateInstance(type);
+ }
+ catch (Exception e)
+ {
+ throw new HibernateException($"Could not instantiate cache lock factory: `{lockFactory}`. Use either `sync` or `async` values or type name implementing {nameof(ICacheReadWriteLockFactory)} interface", e);
+ }
+ }
+ }
+
private static IBatcherFactory CreateBatcherFactory(IDictionary properties, int batchSize, IConnectionProvider connectionProvider)
{
System.Type tBatcher = typeof (NonBatchingBatcherFactory);
diff --git a/src/NHibernate/Impl/SessionFactoryImpl.cs b/src/NHibernate/Impl/SessionFactoryImpl.cs
index e8fae2d201c..6892792c285 100644
--- a/src/NHibernate/Impl/SessionFactoryImpl.cs
+++ b/src/NHibernate/Impl/SessionFactoryImpl.cs
@@ -387,8 +387,8 @@ public SessionFactoryImpl(Configuration cfg, IMapping mapping, Settings settings
if (settings.IsQueryCacheEnabled)
{
- var updateTimestampsCacheName = typeof(UpdateTimestampsCache).Name;
- updateTimestampsCache = new UpdateTimestampsCache(GetCache(updateTimestampsCacheName));
+ var updateTimestampsCacheName = nameof(Cache.UpdateTimestampsCache);
+ updateTimestampsCache = new UpdateTimestampsCache(GetCache(updateTimestampsCacheName), settings.CacheReadWriteLockFactory.Create());
var queryCacheName = typeof(StandardQueryCache).FullName;
queryCache = BuildQueryCache(queryCacheName);
queryCaches = new ConcurrentDictionary>();
@@ -459,7 +459,7 @@ private ICacheConcurrencyStrategy GetCacheConcurrencyStrategy(
if (caches.TryGetValue(cacheKey, out var cache))
return cache;
- cache = CacheFactory.CreateCache(strategy, GetCache(cacheRegion));
+ cache = CacheFactory.CreateCache(strategy, GetCache(cacheRegion), settings);
caches.Add(cacheKey, cache);
if (isMutable && strategy == CacheFactory.ReadOnly)
log.Warn("read-only cache configured for mutable: {0}", name);
diff --git a/src/NHibernate/Util/AsyncReaderWriterLock.cs b/src/NHibernate/Util/AsyncReaderWriterLock.cs
index bd533bc4172..203de8812ee 100644
--- a/src/NHibernate/Util/AsyncReaderWriterLock.cs
+++ b/src/NHibernate/Util/AsyncReaderWriterLock.cs
@@ -7,7 +7,7 @@ namespace NHibernate.Util
// Idea from:
// https://github.com/kpreisser/AsyncReaderWriterLockSlim
// https://devblogs.microsoft.com/pfxteam/building-async-coordination-primitives-part-7-asyncreaderwriterlock/
- internal class AsyncReaderWriterLock : IDisposable
+ internal class AsyncReaderWriterLock : IDisposable, Cache.ICacheLock
{
private readonly SemaphoreSlim _writeLockSemaphore = new SemaphoreSlim(1, 1);
private readonly SemaphoreSlim _readLockSemaphore = new SemaphoreSlim(0, 1);
@@ -38,6 +38,11 @@ public AsyncReaderWriterLock()
internal bool AcquiredWriteLock => _writeLockSemaphore.CurrentCount == 0;
+ IDisposable Cache.ICacheLock.WriteLock()
+ {
+ return WriteLock();
+ }
+
public Releaser WriteLock()
{
if (!CanEnterWriteLock(out var waitForReadLocks))
@@ -59,6 +64,11 @@ public Releaser WriteLock()
return _writerReleaser;
}
+ async Task Cache.ICacheLock.WriteLockAsync()
+ {
+ return await WriteLockAsync().ConfigureAwait(false);
+ }
+
public async Task WriteLockAsync()
{
if (!CanEnterWriteLock(out var waitForReadLocks))
@@ -80,6 +90,11 @@ public async Task WriteLockAsync()
return _writerReleaser;
}
+ IDisposable Cache.ICacheLock.ReadLock()
+ {
+ return ReadLock();
+ }
+
public Releaser ReadLock()
{
if (CanEnterReadLock(out var waitingReadLockSemaphore))
@@ -92,6 +107,11 @@ public Releaser ReadLock()
return _readerReleaser;
}
+ async Task Cache.ICacheLock.ReadLockAsync()
+ {
+ return await ReadLockAsync().ConfigureAwait(false);
+ }
+
public Task ReadLockAsync()
{
return CanEnterReadLock(out var waitingReadLockSemaphore) ? _readerReleaserTask : ReadLockInternalAsync();
diff --git a/src/NHibernate/nhibernate-configuration.xsd b/src/NHibernate/nhibernate-configuration.xsd
index 178be1bfe37..8143722c098 100644
--- a/src/NHibernate/nhibernate-configuration.xsd
+++ b/src/NHibernate/nhibernate-configuration.xsd
@@ -103,6 +103,15 @@
+
+
+
+ Specify the cache lock factory to use for read-write cache regions.
+ Defaults to the built-in async cache lock factory.
+ Use async, or sync, or classname.of.CacheLockFactory, assembly.
+
+
+