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. + + +