From cba6d77f352a2d5da058c2f46e75f26b1788e891 Mon Sep 17 00:00:00 2001 From: Roman Artiukhin Date: Mon, 4 Oct 2021 12:54:46 +0300 Subject: [PATCH 1/9] Add monitor sync only locker for ReadWrite cache --- src/AsyncGenerator.yml | 2 + .../CacheTest/SyncOnlyCacheFixture.cs | 180 ++++++++++++++++++ src/NHibernate/Cache/CacheFactory.cs | 19 +- src/NHibernate/Cache/ICacheLock.cs | 35 ++++ src/NHibernate/Cache/ReadWriteCache.cs | 12 +- src/NHibernate/Cache/SyncCacheLock.cs | 49 +++++ src/NHibernate/Cache/UpdateTimestampsCache.cs | 16 +- src/NHibernate/Cfg/Environment.cs | 1 + src/NHibernate/Cfg/Settings.cs | 1 + src/NHibernate/Cfg/SettingsFactory.cs | 23 +++ src/NHibernate/Impl/SessionFactoryImpl.cs | 6 +- src/NHibernate/Util/AsyncReaderWriterLock.cs | 22 ++- src/NHibernate/nhibernate-configuration.xsd | 1 + 13 files changed, 357 insertions(+), 10 deletions(-) create mode 100644 src/NHibernate.Test/CacheTest/SyncOnlyCacheFixture.cs create mode 100644 src/NHibernate/Cache/ICacheLock.cs create mode 100644 src/NHibernate/Cache/SyncCacheLock.cs 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/CacheTest/SyncOnlyCacheFixture.cs b/src/NHibernate.Test/CacheTest/SyncOnlyCacheFixture.cs new file mode 100644 index 00000000000..9a733c1b15b --- /dev/null +++ b/src/NHibernate.Test/CacheTest/SyncOnlyCacheFixture.cs @@ -0,0 +1,180 @@ +using System; +using System.Collections.Generic; +using System.Threading; +using NHibernate.Cache; +using NHibernate.Cache.Access; +using NHibernate.Cfg; +using NUnit.Framework; +using Environment = NHibernate.Cfg.Environment; + +namespace NHibernate.Test.CacheTest +{ + [TestFixture] + public class SyncOnlyCacheFixture : TestCase + { + protected override void Configure(Configuration cfg) + { + base.Configure(cfg); + cfg.SetProperty(Environment.CacheReadWriteLockFactory, "sync"); + } + + [Test] + public void TestSimpleCache() + { + DoTestCache(new HashtableCacheProvider()); + } + + private CacheKey CreateCacheKey(string text) + { + return new CacheKey(text, NHibernateUtil.String, "Foo", null, null); + } + + public void DoTestCache(ICacheProvider cacheProvider) + { + var cache = (CacheBase) cacheProvider.BuildCache(typeof(String).FullName, new Dictionary()); + + long longBefore = Timestamper.Next(); + + Thread.Sleep(15); + + long before = Timestamper.Next(); + + Thread.Sleep(15); + + ICacheConcurrencyStrategy ccs = CreateCache(cache); + + // cache something + CacheKey fooKey = CreateCacheKey("foo"); + + Assert.IsTrue(ccs.Put(fooKey, "foo", before, null, null, false)); + + Thread.Sleep(15); + + long after = Timestamper.Next(); + + Assert.IsNull(ccs.Get(fooKey, longBefore)); + Assert.AreEqual("foo", ccs.Get(fooKey, after)); + Assert.IsFalse(ccs.Put(fooKey, "foo", before, null, null, false)); + + // update it; + + ISoftLock fooLock = ccs.Lock(fooKey, null); + + Assert.IsNull(ccs.Get(fooKey, after)); + Assert.IsNull(ccs.Get(fooKey, longBefore)); + Assert.IsFalse(ccs.Put(fooKey, "foo", before, null, null, false)); + + Thread.Sleep(15); + + long whileLocked = Timestamper.Next(); + + Assert.IsFalse(ccs.Put(fooKey, "foo", whileLocked, null, null, false)); + + Thread.Sleep(15); + + ccs.Release(fooKey, fooLock); + + Assert.IsNull(ccs.Get(fooKey, after)); + Assert.IsNull(ccs.Get(fooKey, longBefore)); + Assert.IsFalse(ccs.Put(fooKey, "bar", whileLocked, null, null, false)); + Assert.IsFalse(ccs.Put(fooKey, "bar", after, null, null, false)); + + Thread.Sleep(15); + + long longAfter = Timestamper.Next(); + + Assert.IsTrue(ccs.Put(fooKey, "baz", longAfter, null, null, false)); + Assert.IsNull(ccs.Get(fooKey, after)); + Assert.IsNull(ccs.Get(fooKey, whileLocked)); + + Thread.Sleep(15); + + long longLongAfter = Timestamper.Next(); + + Assert.AreEqual("baz", ccs.Get(fooKey, longLongAfter)); + + // update it again, with multiple locks + + ISoftLock fooLock1 = ccs.Lock(fooKey, null); + ISoftLock fooLock2 = ccs.Lock(fooKey, null); + + Assert.IsNull(ccs.Get(fooKey, longLongAfter)); + + Thread.Sleep(15); + + whileLocked = Timestamper.Next(); + + Assert.IsFalse(ccs.Put(fooKey, "foo", whileLocked, null, null, false)); + + Thread.Sleep(15); + + ccs.Release(fooKey, fooLock2); + + Thread.Sleep(15); + + long betweenReleases = Timestamper.Next(); + + Assert.IsFalse(ccs.Put(fooKey, "bar", betweenReleases, null, null, false)); + Assert.IsNull(ccs.Get(fooKey, betweenReleases)); + + Thread.Sleep(15); + + ccs.Release(fooKey, fooLock1); + + Assert.IsFalse(ccs.Put(fooKey, "bar", whileLocked, null, null, false)); + + Thread.Sleep(15); + + longAfter = Timestamper.Next(); + + Assert.IsTrue(ccs.Put(fooKey, "baz", longAfter, null, null, false)); + Assert.IsNull(ccs.Get(fooKey, whileLocked)); + + Thread.Sleep(15); + + longLongAfter = Timestamper.Next(); + + Assert.AreEqual("baz", ccs.Get(fooKey, longLongAfter)); + } + + private ICacheConcurrencyStrategy CreateCache(CacheBase cache, string strategy = CacheFactory.ReadWrite) + { + return CacheFactory.CreateCache(strategy, cache, Sfi.Settings); + } + + private void DoTestMinValueTimestampOnStrategy(CacheBase cache, ICacheConcurrencyStrategy strategy) + { + CacheKey key = CreateCacheKey("key"); + strategy.Cache = cache; + strategy.Put(key, "value", long.MinValue, 0, null, false); + + Assert.IsNull(strategy.Get(key, long.MinValue), "{0} strategy fails the test", strategy.GetType()); + Assert.IsNull(strategy.Get(key, long.MaxValue), "{0} strategy fails the test", strategy.GetType()); + } + + [Test] + public void MinValueTimestamp() + { + var cache = new HashtableCacheProvider().BuildCache("region", new Dictionary()); + + DoTestMinValueTimestampOnStrategy(cache, CreateCache(cache)); + DoTestMinValueTimestampOnStrategy(cache, CreateCache(cache, CacheFactory.NonstrictReadWrite)); + DoTestMinValueTimestampOnStrategy(cache, CreateCache(cache, CacheFactory.ReadOnly)); + } + + [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))); + } + + protected override string[] Mappings => Array.Empty(); + } +} diff --git a/src/NHibernate/Cache/CacheFactory.cs b/src/NHibernate/Cache/CacheFactory.cs index b6c5df8549d..fdcda0997a7 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.CacheReadWriteReadWriteLockFactory.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..4a6e508a43a --- /dev/null +++ b/src/NHibernate/Cache/ICacheLock.cs @@ -0,0 +1,35 @@ +using System; +using System.Threading.Tasks; +using NHibernate.Util; + +namespace NHibernate.Cache +{ + public interface ICacheLock : IDisposable + { + IDisposable ReadLock(); + IDisposable WriteLock(); + Task ReadLockAsync(); + Task WriteLockAsync(); + } + + public interface ICacheReadWriteLockFactory + { + ICacheLock Create(); + } + + class AsyncCacheReadWriteLockFactory : ICacheReadWriteLockFactory + { + public ICacheLock Create() + { + return new AsyncReaderWriterLock(); + } + } + + 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..989a21b57d2 100644 --- a/src/NHibernate/Cache/ReadWriteCache.cs +++ b/src/NHibernate/Cache/ReadWriteCache.cs @@ -36,7 +36,17 @@ public interface ILockable private CacheBase _cache; private int _nextLockId; - private readonly AsyncReaderWriterLock _asyncReaderWriterLock = new AsyncReaderWriterLock(); + private readonly ICacheLock _asyncReaderWriterLock; + + public ReadWriteCache() + { + _asyncReaderWriterLock = 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..bca32c77666 --- /dev/null +++ b/src/NHibernate/Cache/SyncCacheLock.cs @@ -0,0 +1,49 @@ +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 new InvalidOperationException("This locker supports only sync operations."); + } + + public Task WriteLockAsync() + { + throw new InvalidOperationException("This locker supports only sync 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..c7f436f44ff 100644 --- a/src/NHibernate/Cfg/Settings.cs +++ b/src/NHibernate/Cfg/Settings.cs @@ -101,6 +101,7 @@ public Settings() public ConnectionReleaseMode ConnectionReleaseMode { get; internal set; } public ICacheProvider CacheProvider { get; internal set; } + public ICacheReadWriteLockFactory CacheReadWriteReadWriteLockFactory { get; internal set; } public IQueryCacheFactory QueryCacheFactory { get; internal set; } diff --git a/src/NHibernate/Cfg/SettingsFactory.cs b/src/NHibernate/Cfg/SettingsFactory.cs index f1f1a0ffb29..e29bc60c58f 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.CacheReadWriteReadWriteLockFactory = 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..63623899cc6 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.CacheReadWriteReadWriteLockFactory.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..d12e930da84 100644 --- a/src/NHibernate/nhibernate-configuration.xsd +++ b/src/NHibernate/nhibernate-configuration.xsd @@ -103,6 +103,7 @@ + From 3c2aefd8933afd62821d3dcedbedacc8be2fd8f4 Mon Sep 17 00:00:00 2001 From: Roman Artiukhin Date: Thu, 25 Nov 2021 12:07:05 +0200 Subject: [PATCH 2/9] Apply suggestions from code review MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Frédéric Delaporte <12201973+fredericDelaporte@users.noreply.github.com> --- src/NHibernate/Cache/ICacheLock.cs | 33 +++++++++++++++++++-- src/NHibernate/Cfg/Settings.cs | 2 +- src/NHibernate/nhibernate-configuration.xsd | 10 ++++++- 3 files changed, 41 insertions(+), 4 deletions(-) diff --git a/src/NHibernate/Cache/ICacheLock.cs b/src/NHibernate/Cache/ICacheLock.cs index 4a6e508a43a..41fd0f88c3d 100644 --- a/src/NHibernate/Cache/ICacheLock.cs +++ b/src/NHibernate/Cache/ICacheLock.cs @@ -4,20 +4,49 @@ 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(); } - class AsyncCacheReadWriteLockFactory : ICacheReadWriteLockFactory + internal class AsyncCacheReadWriteLockFactory : ICacheReadWriteLockFactory { public ICacheLock Create() { @@ -25,7 +54,7 @@ public ICacheLock Create() } } - class SyncCacheReadWriteLockFactory : ICacheReadWriteLockFactory + internal class SyncCacheReadWriteLockFactory : ICacheReadWriteLockFactory { public ICacheLock Create() { diff --git a/src/NHibernate/Cfg/Settings.cs b/src/NHibernate/Cfg/Settings.cs index c7f436f44ff..3d1e6b5e0c7 100644 --- a/src/NHibernate/Cfg/Settings.cs +++ b/src/NHibernate/Cfg/Settings.cs @@ -101,7 +101,7 @@ public Settings() public ConnectionReleaseMode ConnectionReleaseMode { get; internal set; } public ICacheProvider CacheProvider { get; internal set; } - public ICacheReadWriteLockFactory CacheReadWriteReadWriteLockFactory { get; internal set; } + public ICacheReadWriteLockFactory CacheReadWriteLockFactory { get; internal set; } public IQueryCacheFactory QueryCacheFactory { get; internal set; } diff --git a/src/NHibernate/nhibernate-configuration.xsd b/src/NHibernate/nhibernate-configuration.xsd index d12e930da84..8143722c098 100644 --- a/src/NHibernate/nhibernate-configuration.xsd +++ b/src/NHibernate/nhibernate-configuration.xsd @@ -103,7 +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. + + + From 4f49372050e220f4d1c6ca791c80f1315995d79e Mon Sep 17 00:00:00 2001 From: Roman Artiukhin Date: Thu, 25 Nov 2021 12:25:09 +0200 Subject: [PATCH 3/9] Fix build --- src/NHibernate/Cache/CacheFactory.cs | 2 +- src/NHibernate/Cfg/SettingsFactory.cs | 2 +- src/NHibernate/Impl/SessionFactoryImpl.cs | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/NHibernate/Cache/CacheFactory.cs b/src/NHibernate/Cache/CacheFactory.cs index fdcda0997a7..96c44ff3f88 100644 --- a/src/NHibernate/Cache/CacheFactory.cs +++ b/src/NHibernate/Cache/CacheFactory.cs @@ -84,7 +84,7 @@ public static ICacheConcurrencyStrategy CreateCache(string usage, CacheBase cach ccs = new ReadOnlyCache(); break; case ReadWrite: - ccs = new ReadWriteCache(settings == null ? new AsyncReaderWriterLock() : settings.CacheReadWriteReadWriteLockFactory.Create()); + ccs = new ReadWriteCache(settings == null ? new AsyncReaderWriterLock() : settings.CacheReadWriteLockFactory.Create()); break; case NonstrictReadWrite: ccs = new NonstrictReadWriteCache(); diff --git a/src/NHibernate/Cfg/SettingsFactory.cs b/src/NHibernate/Cfg/SettingsFactory.cs index e29bc60c58f..d9377773fb9 100644 --- a/src/NHibernate/Cfg/SettingsFactory.cs +++ b/src/NHibernate/Cfg/SettingsFactory.cs @@ -206,7 +206,7 @@ public Settings BuildSettings(IDictionary properties) if (useSecondLevelCache || useQueryCache) { - settings.CacheReadWriteReadWriteLockFactory = GetReadWriteLockFactory(PropertiesHelper.GetString(Environment.CacheReadWriteLockFactory, properties, null)); + 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); diff --git a/src/NHibernate/Impl/SessionFactoryImpl.cs b/src/NHibernate/Impl/SessionFactoryImpl.cs index 63623899cc6..6892792c285 100644 --- a/src/NHibernate/Impl/SessionFactoryImpl.cs +++ b/src/NHibernate/Impl/SessionFactoryImpl.cs @@ -388,7 +388,7 @@ public SessionFactoryImpl(Configuration cfg, IMapping mapping, Settings settings if (settings.IsQueryCacheEnabled) { var updateTimestampsCacheName = nameof(Cache.UpdateTimestampsCache); - updateTimestampsCache = new UpdateTimestampsCache(GetCache(updateTimestampsCacheName), settings.CacheReadWriteReadWriteLockFactory.Create()); + updateTimestampsCache = new UpdateTimestampsCache(GetCache(updateTimestampsCacheName), settings.CacheReadWriteLockFactory.Create()); var queryCacheName = typeof(StandardQueryCache).FullName; queryCache = BuildQueryCache(queryCacheName); queryCaches = new ConcurrentDictionary>(); From 625092c62f9bda2e01cfd2a4a55a992c78171474 Mon Sep 17 00:00:00 2001 From: Roman Artiukhin Date: Thu, 25 Nov 2021 12:27:30 +0200 Subject: [PATCH 4/9] DRY test case --- .../Async/CacheTest/CacheFixture.cs | 24 +-- src/NHibernate.Test/CacheTest/CacheFixture.cs | 24 +-- .../CacheTest/SyncOnlyCacheFixture.cs | 149 +----------------- 3 files changed, 29 insertions(+), 168 deletions(-) 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 index 9a733c1b15b..267b74d015a 100644 --- a/src/NHibernate.Test/CacheTest/SyncOnlyCacheFixture.cs +++ b/src/NHibernate.Test/CacheTest/SyncOnlyCacheFixture.cs @@ -2,7 +2,6 @@ using System.Collections.Generic; using System.Threading; using NHibernate.Cache; -using NHibernate.Cache.Access; using NHibernate.Cfg; using NUnit.Framework; using Environment = NHibernate.Cfg.Environment; @@ -10,7 +9,7 @@ namespace NHibernate.Test.CacheTest { [TestFixture] - public class SyncOnlyCacheFixture : TestCase + public class SyncOnlyCacheFixture : CacheFixture { protected override void Configure(Configuration cfg) { @@ -18,150 +17,6 @@ protected override void Configure(Configuration cfg) cfg.SetProperty(Environment.CacheReadWriteLockFactory, "sync"); } - [Test] - public void TestSimpleCache() - { - DoTestCache(new HashtableCacheProvider()); - } - - private CacheKey CreateCacheKey(string text) - { - return new CacheKey(text, NHibernateUtil.String, "Foo", null, null); - } - - public void DoTestCache(ICacheProvider cacheProvider) - { - var cache = (CacheBase) cacheProvider.BuildCache(typeof(String).FullName, new Dictionary()); - - long longBefore = Timestamper.Next(); - - Thread.Sleep(15); - - long before = Timestamper.Next(); - - Thread.Sleep(15); - - ICacheConcurrencyStrategy ccs = CreateCache(cache); - - // cache something - CacheKey fooKey = CreateCacheKey("foo"); - - Assert.IsTrue(ccs.Put(fooKey, "foo", before, null, null, false)); - - Thread.Sleep(15); - - long after = Timestamper.Next(); - - Assert.IsNull(ccs.Get(fooKey, longBefore)); - Assert.AreEqual("foo", ccs.Get(fooKey, after)); - Assert.IsFalse(ccs.Put(fooKey, "foo", before, null, null, false)); - - // update it; - - ISoftLock fooLock = ccs.Lock(fooKey, null); - - Assert.IsNull(ccs.Get(fooKey, after)); - Assert.IsNull(ccs.Get(fooKey, longBefore)); - Assert.IsFalse(ccs.Put(fooKey, "foo", before, null, null, false)); - - Thread.Sleep(15); - - long whileLocked = Timestamper.Next(); - - Assert.IsFalse(ccs.Put(fooKey, "foo", whileLocked, null, null, false)); - - Thread.Sleep(15); - - ccs.Release(fooKey, fooLock); - - Assert.IsNull(ccs.Get(fooKey, after)); - Assert.IsNull(ccs.Get(fooKey, longBefore)); - Assert.IsFalse(ccs.Put(fooKey, "bar", whileLocked, null, null, false)); - Assert.IsFalse(ccs.Put(fooKey, "bar", after, null, null, false)); - - Thread.Sleep(15); - - long longAfter = Timestamper.Next(); - - Assert.IsTrue(ccs.Put(fooKey, "baz", longAfter, null, null, false)); - Assert.IsNull(ccs.Get(fooKey, after)); - Assert.IsNull(ccs.Get(fooKey, whileLocked)); - - Thread.Sleep(15); - - long longLongAfter = Timestamper.Next(); - - Assert.AreEqual("baz", ccs.Get(fooKey, longLongAfter)); - - // update it again, with multiple locks - - ISoftLock fooLock1 = ccs.Lock(fooKey, null); - ISoftLock fooLock2 = ccs.Lock(fooKey, null); - - Assert.IsNull(ccs.Get(fooKey, longLongAfter)); - - Thread.Sleep(15); - - whileLocked = Timestamper.Next(); - - Assert.IsFalse(ccs.Put(fooKey, "foo", whileLocked, null, null, false)); - - Thread.Sleep(15); - - ccs.Release(fooKey, fooLock2); - - Thread.Sleep(15); - - long betweenReleases = Timestamper.Next(); - - Assert.IsFalse(ccs.Put(fooKey, "bar", betweenReleases, null, null, false)); - Assert.IsNull(ccs.Get(fooKey, betweenReleases)); - - Thread.Sleep(15); - - ccs.Release(fooKey, fooLock1); - - Assert.IsFalse(ccs.Put(fooKey, "bar", whileLocked, null, null, false)); - - Thread.Sleep(15); - - longAfter = Timestamper.Next(); - - Assert.IsTrue(ccs.Put(fooKey, "baz", longAfter, null, null, false)); - Assert.IsNull(ccs.Get(fooKey, whileLocked)); - - Thread.Sleep(15); - - longLongAfter = Timestamper.Next(); - - Assert.AreEqual("baz", ccs.Get(fooKey, longLongAfter)); - } - - private ICacheConcurrencyStrategy CreateCache(CacheBase cache, string strategy = CacheFactory.ReadWrite) - { - return CacheFactory.CreateCache(strategy, cache, Sfi.Settings); - } - - private void DoTestMinValueTimestampOnStrategy(CacheBase cache, ICacheConcurrencyStrategy strategy) - { - CacheKey key = CreateCacheKey("key"); - strategy.Cache = cache; - strategy.Put(key, "value", long.MinValue, 0, null, false); - - Assert.IsNull(strategy.Get(key, long.MinValue), "{0} strategy fails the test", strategy.GetType()); - Assert.IsNull(strategy.Get(key, long.MaxValue), "{0} strategy fails the test", strategy.GetType()); - } - - [Test] - public void MinValueTimestamp() - { - var cache = new HashtableCacheProvider().BuildCache("region", new Dictionary()); - - DoTestMinValueTimestampOnStrategy(cache, CreateCache(cache)); - DoTestMinValueTimestampOnStrategy(cache, CreateCache(cache, CacheFactory.NonstrictReadWrite)); - DoTestMinValueTimestampOnStrategy(cache, CreateCache(cache, CacheFactory.ReadOnly)); - } - [Test] public void AsyncOperationsThrow() { @@ -174,7 +29,5 @@ public void AsyncOperationsThrow() strategy.PutAsync(key, "value", stamp, 0, null, false, default(CancellationToken))); Assert.ThrowsAsync(() => strategy.GetAsync(key, stamp, default(CancellationToken))); } - - protected override string[] Mappings => Array.Empty(); } } From b1fe614ce42e3caf9464a0d86fb9545bd5f4e3c4 Mon Sep 17 00:00:00 2001 From: Roman Artiukhin Date: Thu, 25 Nov 2021 12:32:59 +0200 Subject: [PATCH 5/9] Better exception --- src/NHibernate/Cache/SyncCacheLock.cs | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/src/NHibernate/Cache/SyncCacheLock.cs b/src/NHibernate/Cache/SyncCacheLock.cs index bca32c77666..129c469cfd2 100644 --- a/src/NHibernate/Cache/SyncCacheLock.cs +++ b/src/NHibernate/Cache/SyncCacheLock.cs @@ -38,12 +38,17 @@ public IDisposable WriteLock() public Task ReadLockAsync() { - throw new InvalidOperationException("This locker supports only sync operations."); + throw AsyncNotSupporteException(); } public Task WriteLockAsync() { - throw new InvalidOperationException("This locker supports only sync operations."); + 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."); } } } From 843ba1834e57c5008502f3dca5c5f4c1b54a895b Mon Sep 17 00:00:00 2001 From: Roman Artiukhin Date: Thu, 25 Nov 2021 12:42:04 +0200 Subject: [PATCH 6/9] Docs --- doc/reference/modules/configuration.xml | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) 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 From f4582d77429a0edc72e37e28b7611a59180c338d Mon Sep 17 00:00:00 2001 From: Roman Artiukhin Date: Thu, 25 Nov 2021 12:44:06 +0200 Subject: [PATCH 7/9] whites --- src/NHibernate/Cache/ICacheLock.cs | 1 - 1 file changed, 1 deletion(-) diff --git a/src/NHibernate/Cache/ICacheLock.cs b/src/NHibernate/Cache/ICacheLock.cs index 41fd0f88c3d..4fbf16c7180 100644 --- a/src/NHibernate/Cache/ICacheLock.cs +++ b/src/NHibernate/Cache/ICacheLock.cs @@ -20,7 +20,6 @@ public interface ICacheLock : IDisposable /// /// A write lock. IDisposable WriteLock(); - /// /// Acquire asynchronously a read lock. From 23dfac5c1a471402a254d30868213e16a5df9e8b Mon Sep 17 00:00:00 2001 From: Roman Artiukhin Date: Wed, 19 Jan 2022 08:34:59 +0200 Subject: [PATCH 8/9] Code review --- src/NHibernate/Cache/ReadWriteCache.cs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/NHibernate/Cache/ReadWriteCache.cs b/src/NHibernate/Cache/ReadWriteCache.cs index 989a21b57d2..fbeff50c540 100644 --- a/src/NHibernate/Cache/ReadWriteCache.cs +++ b/src/NHibernate/Cache/ReadWriteCache.cs @@ -38,9 +38,8 @@ public interface ILockable private int _nextLockId; private readonly ICacheLock _asyncReaderWriterLock; - public ReadWriteCache() + public ReadWriteCache() : this(new AsyncReaderWriterLock()) { - _asyncReaderWriterLock = new AsyncReaderWriterLock(); } public ReadWriteCache(ICacheLock locker) From dff55a9d42d5c050611f8c8ddc46e37c812e7cb8 Mon Sep 17 00:00:00 2001 From: Roman Artiukhin Date: Wed, 19 Jan 2022 08:36:10 +0200 Subject: [PATCH 9/9] whitespaces --- src/NHibernate/Cfg/Settings.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/src/NHibernate/Cfg/Settings.cs b/src/NHibernate/Cfg/Settings.cs index 3d1e6b5e0c7..f1c9c03fa8c 100644 --- a/src/NHibernate/Cfg/Settings.cs +++ b/src/NHibernate/Cfg/Settings.cs @@ -101,6 +101,7 @@ public Settings() public ConnectionReleaseMode ConnectionReleaseMode { get; internal set; } public ICacheProvider CacheProvider { get; internal set; } + public ICacheReadWriteLockFactory CacheReadWriteLockFactory { get; internal set; } public IQueryCacheFactory QueryCacheFactory { get; internal set; }