diff --git a/src/NHibernate.Test/Async/CacheTest/GetQueryCacheFixture.cs b/src/NHibernate.Test/Async/CacheTest/BuildCacheFixture.cs similarity index 87% rename from src/NHibernate.Test/Async/CacheTest/GetQueryCacheFixture.cs rename to src/NHibernate.Test/Async/CacheTest/BuildCacheFixture.cs index e53b9fc4d7b..d944b825c81 100644 --- a/src/NHibernate.Test/Async/CacheTest/GetQueryCacheFixture.cs +++ b/src/NHibernate.Test/Async/CacheTest/BuildCacheFixture.cs @@ -9,12 +9,13 @@ using System; -using System.Collections; using System.Collections.Concurrent; using System.Collections.Generic; using System.Threading; using NHibernate.Cache; using NHibernate.Cfg; +using NHibernate.Engine; +using NHibernate.Util; using NUnit.Framework; using Environment = NHibernate.Cfg.Environment; @@ -22,9 +23,14 @@ namespace NHibernate.Test.CacheTest { using System.Threading.Tasks; [TestFixture] - public class GetQueryCacheFixtureAsync : TestCase + public class BuildCacheFixtureAsync : TestCase { - protected override string[] Mappings => new[] { "Simple.hbm.xml" }; + protected override string MappingsAssembly => "NHibernate.Test"; + + protected override string[] Mappings => new[] { "CacheTest.EntitiesInSameRegion.hbm.xml" }; + + // Disable the TestCase cache overrides. + protected override string CacheConcurrencyStrategy => null; protected override void Configure(Configuration configuration) { diff --git a/src/NHibernate.Test/CacheTest/BuildCacheFixture.cs b/src/NHibernate.Test/CacheTest/BuildCacheFixture.cs new file mode 100644 index 00000000000..f0feea553c7 --- /dev/null +++ b/src/NHibernate.Test/CacheTest/BuildCacheFixture.cs @@ -0,0 +1,198 @@ +using System; +using System.Collections.Concurrent; +using System.Collections.Generic; +using System.Threading; +using NHibernate.Cache; +using NHibernate.Cfg; +using NHibernate.Engine; +using NHibernate.Util; +using NUnit.Framework; +using Environment = NHibernate.Cfg.Environment; + +namespace NHibernate.Test.CacheTest +{ + [TestFixture] + public class BuildCacheFixture : TestCase + { + protected override string MappingsAssembly => "NHibernate.Test"; + + protected override string[] Mappings => new[] { "CacheTest.EntitiesInSameRegion.hbm.xml" }; + + // Disable the TestCase cache overrides. + protected override string CacheConcurrencyStrategy => null; + + protected override void Configure(Configuration configuration) + { + configuration.SetProperty(Environment.UseQueryCache, "true"); + configuration.SetProperty(Environment.CacheProvider, typeof(LockedCacheProvider).AssemblyQualifiedName); + } + + [Theory] + public void CommonRegionHasOneUniqueCacheAndExpectedConcurrency(bool withPrefix) + { + const string prefix = "Prefix"; + const string region = "Common"; + var fullRegion = (withPrefix ? prefix + "." : "") + region; + ISessionFactoryImplementor sfi = null; + if (withPrefix) + cfg.SetProperty(Environment.CacheRegionPrefix, prefix); + try + { + sfi = withPrefix ? BuildSessionFactory() : Sfi; + var commonRegionCache = sfi.GetSecondLevelCacheRegion(fullRegion); + var entityAName = typeof(EntityA).FullName; + var entityAConcurrencyCache = sfi.GetEntityPersister(entityAName).Cache; + var entityACache = entityAConcurrencyCache.Cache; + var entityBName = typeof(EntityB).FullName; + var entityBConcurrencyCache = sfi.GetEntityPersister(entityBName).Cache; + var entityBCache = entityBConcurrencyCache.Cache; + var relatedAConcurrencyCache = + sfi.GetCollectionPersister(StringHelper.Qualify(entityAName, nameof(EntityA.Related))).Cache; + var relatedACache = relatedAConcurrencyCache.Cache; + var relatedBConcurrencyCache = + sfi.GetCollectionPersister(StringHelper.Qualify(entityBName, nameof(EntityB.Related))).Cache; + var relatedBCache = relatedBConcurrencyCache.Cache; + var queryCache = sfi.GetQueryCache(region).Cache; + Assert.Multiple( + () => + { + Assert.That(commonRegionCache.RegionName, Is.EqualTo(fullRegion), "Unexpected region name for common region"); + Assert.That(entityACache.RegionName, Is.EqualTo(fullRegion), "Unexpected region name for EntityA"); + Assert.That(entityBCache.RegionName, Is.EqualTo(fullRegion), "Unexpected region name for EntityB"); + Assert.That(relatedACache.RegionName, Is.EqualTo(fullRegion), "Unexpected region name for RelatedA"); + Assert.That(relatedBCache.RegionName, Is.EqualTo(fullRegion), "Unexpected region name for RelatedB"); + Assert.That(queryCache.RegionName, Is.EqualTo(fullRegion), "Unexpected region name for query cache"); + }); + Assert.Multiple( + () => + { + Assert.That(entityAConcurrencyCache, Is.InstanceOf(), "Unexpected concurrency for EntityA"); + Assert.That(relatedAConcurrencyCache, Is.InstanceOf(), "Unexpected concurrency for RelatedA"); + Assert.That(entityBConcurrencyCache, Is.InstanceOf(), "Unexpected concurrency for EntityB"); + Assert.That(relatedBConcurrencyCache, Is.InstanceOf(), "Unexpected concurrency for RelatedB"); + Assert.That(entityACache, Is.SameAs(commonRegionCache), "Unexpected cache for EntityA"); + Assert.That(entityBCache, Is.SameAs(commonRegionCache), "Unexpected cache for EntityB"); + Assert.That(relatedACache, Is.SameAs(commonRegionCache), "Unexpected cache for RelatedA"); + Assert.That(relatedBCache, Is.SameAs(commonRegionCache), "Unexpected cache for RelatedB"); + Assert.That(queryCache, Is.SameAs(commonRegionCache), "Unexpected cache for query cache"); + }); + } + finally + { + if (withPrefix) + { + cfg.Properties.Remove(Environment.CacheRegionPrefix); + sfi?.Dispose(); + } + } + } + + [Test] + public void RetrievedQueryCacheMatchesGloballyStoredOne() + { + var region = "RetrievedQueryCacheMatchesGloballyStoredOne"; + LockedCache.Semaphore = new SemaphoreSlim(0); + LockedCache.CreationCount = 0; + try + { + var failures = new ConcurrentBag(); + var thread1 = new Thread( + () => + { + try + { + Sfi.GetQueryCache(region); + } + catch (Exception e) + { + failures.Add(e); + } + }); + var thread2 = new Thread( + () => + { + try + { + Sfi.GetQueryCache(region); + } + catch (Exception e) + { + failures.Add(e); + } + }); + thread1.Start(); + thread2.Start(); + + // Give some time to threads for reaching the wait, having all of them ready to do most of their job concurrently. + Thread.Sleep(100); + // Let only one finish its job, it should be the one being stored in query cache dictionary. + LockedCache.Semaphore.Release(1); + // Give some time to released thread to finish its job. + Thread.Sleep(100); + // Release other thread + LockedCache.Semaphore.Release(10); + + thread1.Join(); + thread2.Join(); + Assert.That(failures, Is.Empty, $"{failures.Count} thread(s) failed."); + } + finally + { + LockedCache.Semaphore.Dispose(); + LockedCache.Semaphore = null; + } + + var queryCache = Sfi.GetQueryCache(region).Cache; + var globalCache = Sfi.GetSecondLevelCacheRegion(region); + Assert.That(globalCache, Is.SameAs(queryCache)); + Assert.That(LockedCache.CreationCount, Is.EqualTo(1)); + } + } + + public class LockedCache : HashtableCache + { + public static SemaphoreSlim Semaphore { get; set; } + + private static int _creationCount; + + public static int CreationCount + { + get => _creationCount; + set => _creationCount = value; + } + + public LockedCache(string regionName) : base(regionName) + { + Semaphore?.Wait(); + Interlocked.Increment(ref _creationCount); + } + } + + public class LockedCacheProvider : ICacheProvider + { + // Since 5.2 + [Obsolete] + ICache ICacheProvider.BuildCache(string regionName, IDictionary properties) + { + return BuildCache(regionName, properties); + } + + public CacheBase BuildCache(string regionName, IDictionary properties) + { + return new LockedCache(regionName); + } + + public long NextTimestamp() + { + return Timestamper.Next(); + } + + public void Start(IDictionary properties) + { + } + + public void Stop() + { + } + } +} diff --git a/src/NHibernate.Test/CacheTest/EntitiesInSameRegion.cs b/src/NHibernate.Test/CacheTest/EntitiesInSameRegion.cs new file mode 100644 index 00000000000..3610ede845a --- /dev/null +++ b/src/NHibernate.Test/CacheTest/EntitiesInSameRegion.cs @@ -0,0 +1,20 @@ +using System.Collections.Generic; + +namespace NHibernate.Test.CacheTest +{ + public class EntityInSameRegion + { + public virtual int Id { get; set; } + public virtual string Description { get; set; } + public virtual int Value { get; set; } + public virtual ISet Related { get; set; } + } + + public class EntityA : EntityInSameRegion + { + } + + public class EntityB : EntityInSameRegion + { + } +} diff --git a/src/NHibernate.Test/CacheTest/EntitiesInSameRegion.hbm.xml b/src/NHibernate.Test/CacheTest/EntitiesInSameRegion.hbm.xml new file mode 100644 index 00000000000..67c760f6ef4 --- /dev/null +++ b/src/NHibernate.Test/CacheTest/EntitiesInSameRegion.hbm.xml @@ -0,0 +1,30 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + from EntityA + + diff --git a/src/NHibernate.Test/CacheTest/GetQueryCacheFixture.cs b/src/NHibernate.Test/CacheTest/GetQueryCacheFixture.cs deleted file mode 100644 index 73120bdf537..00000000000 --- a/src/NHibernate.Test/CacheTest/GetQueryCacheFixture.cs +++ /dev/null @@ -1,132 +0,0 @@ -using System; -using System.Collections; -using System.Collections.Concurrent; -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 GetQueryCacheFixture : TestCase - { - protected override string[] Mappings => new[] { "Simple.hbm.xml" }; - - protected override void Configure(Configuration configuration) - { - configuration.SetProperty(Environment.UseQueryCache, "true"); - configuration.SetProperty(Environment.CacheProvider, typeof(LockedCacheProvider).AssemblyQualifiedName); - } - - [Test] - public void RetrievedQueryCacheMatchesGloballyStoredOne() - { - var region = "RetrievedQueryCacheMatchesGloballyStoredOne"; - LockedCache.Semaphore = new SemaphoreSlim(0); - LockedCache.CreationCount = 0; - try - { - var failures = new ConcurrentBag(); - var thread1 = new Thread( - () => - { - try - { - Sfi.GetQueryCache(region); - } - catch (Exception e) - { - failures.Add(e); - } - }); - var thread2 = new Thread( - () => - { - try - { - Sfi.GetQueryCache(region); - } - catch (Exception e) - { - failures.Add(e); - } - }); - thread1.Start(); - thread2.Start(); - - // Give some time to threads for reaching the wait, having all of them ready to do most of their job concurrently. - Thread.Sleep(100); - // Let only one finish its job, it should be the one being stored in query cache dictionary. - LockedCache.Semaphore.Release(1); - // Give some time to released thread to finish its job. - Thread.Sleep(100); - // Release other thread - LockedCache.Semaphore.Release(10); - - thread1.Join(); - thread2.Join(); - Assert.That(failures, Is.Empty, $"{failures.Count} thread(s) failed."); - } - finally - { - LockedCache.Semaphore.Dispose(); - LockedCache.Semaphore = null; - } - - var queryCache = Sfi.GetQueryCache(region).Cache; - var globalCache = Sfi.GetSecondLevelCacheRegion(region); - Assert.That(globalCache, Is.SameAs(queryCache)); - Assert.That(LockedCache.CreationCount, Is.EqualTo(1)); - } - } - - public class LockedCache : HashtableCache - { - public static SemaphoreSlim Semaphore { get; set; } - - private static int _creationCount; - - public static int CreationCount - { - get => _creationCount; - set => _creationCount = value; - } - - public LockedCache(string regionName) : base(regionName) - { - Semaphore?.Wait(); - Interlocked.Increment(ref _creationCount); - } - } - - public class LockedCacheProvider : ICacheProvider - { - // Since 5.2 - [Obsolete] - ICache ICacheProvider.BuildCache(string regionName, IDictionary properties) - { - return BuildCache(regionName, properties); - } - - public CacheBase BuildCache(string regionName, IDictionary properties) - { - return new LockedCache(regionName); - } - - public long NextTimestamp() - { - return Timestamper.Next(); - } - - public void Start(IDictionary properties) - { - } - - public void Stop() - { - } - } -} diff --git a/src/NHibernate/Async/Cache/NonstrictReadWriteCache.cs b/src/NHibernate/Async/Cache/NonstrictReadWriteCache.cs index b0462b1bcf5..d074aeb3c15 100644 --- a/src/NHibernate/Async/Cache/NonstrictReadWriteCache.cs +++ b/src/NHibernate/Async/Cache/NonstrictReadWriteCache.cs @@ -8,7 +8,6 @@ //------------------------------------------------------------------------------ -using System; using System.Collections; using System.Collections.Generic; using System.Linq; @@ -169,7 +168,7 @@ public Task LockAsync(CacheKey key, object version, CancellationToken { return Task.FromResult(Lock(key, version)); } - catch (Exception ex) + catch (System.Exception ex) { return Task.FromException(ex); } @@ -189,7 +188,7 @@ public Task RemoveAsync(CacheKey key, CancellationToken cancellationToken) } return Cache.RemoveAsync(key, cancellationToken); } - catch (Exception ex) + catch (System.Exception ex) { return Task.FromException(ex); } @@ -209,7 +208,7 @@ public Task ClearAsync(CancellationToken cancellationToken) } return Cache.ClearAsync(cancellationToken); } - catch (Exception ex) + catch (System.Exception ex) { return Task.FromException(ex); } @@ -232,7 +231,7 @@ public Task EvictAsync(CacheKey key, CancellationToken cancellationToken) } return Cache.RemoveAsync(key, cancellationToken); } - catch (Exception ex) + catch (System.Exception ex) { return Task.FromException(ex); } @@ -266,7 +265,7 @@ public Task ReleaseAsync(CacheKey key, ISoftLock @lock, CancellationToken cancel return Cache.RemoveAsync(key, cancellationToken); } - catch (Exception ex) + catch (System.Exception ex) { return Task.FromException(ex); } @@ -295,7 +294,7 @@ public Task AfterInsertAsync(CacheKey key, object value, object version, C { return Task.FromResult(AfterInsert(key, value, version)); } - catch (Exception ex) + catch (System.Exception ex) { return Task.FromException(ex); } diff --git a/src/NHibernate/Async/Cache/ReadWriteCache.cs b/src/NHibernate/Async/Cache/ReadWriteCache.cs index 28ec417b861..0813d2d8dd4 100644 --- a/src/NHibernate/Async/Cache/ReadWriteCache.cs +++ b/src/NHibernate/Async/Cache/ReadWriteCache.cs @@ -8,7 +8,6 @@ //------------------------------------------------------------------------------ -using System; using System.Collections; using System.Collections.Generic; using System.Linq; @@ -317,7 +316,7 @@ private Task DecrementLockAsync(object key, CacheLock @lock, CancellationToken c @lock.Unlock(Cache.NextTimestamp()); return Cache.PutAsync(key, @lock, cancellationToken); } - catch (Exception ex) + catch (System.Exception ex) { return Task.FromException(ex); } @@ -368,7 +367,7 @@ internal Task HandleLockExpiryAsync(object key, CancellationToken cancellationTo @lock.Unlock(ts); return Cache.PutAsync(key, @lock, cancellationToken); } - catch (Exception ex) + catch (System.Exception ex) { return Task.FromException(ex); } @@ -490,7 +489,7 @@ public Task EvictAsync(CacheKey key, CancellationToken cancellationToken) Evict(key); return Task.CompletedTask; } - catch (Exception ex) + catch (System.Exception ex) { return Task.FromException(ex); } @@ -506,7 +505,7 @@ public Task UpdateAsync(CacheKey key, object value, object currentVersion, { return Task.FromResult(Update(key, value, currentVersion, previousVersion)); } - catch (Exception ex) + catch (System.Exception ex) { return Task.FromException(ex); } diff --git a/src/NHibernate/Async/Impl/SessionFactoryImpl.cs b/src/NHibernate/Async/Impl/SessionFactoryImpl.cs index 8dcb9b347da..af52013f08b 100644 --- a/src/NHibernate/Async/Impl/SessionFactoryImpl.cs +++ b/src/NHibernate/Async/Impl/SessionFactoryImpl.cs @@ -85,14 +85,15 @@ public sealed partial class SessionFactoryImpl : ISessionFactoryImplementor, IOb if (settings.IsQueryCacheEnabled) { - queryCache.Destroy(); - foreach (var cache in queryCaches.Values) { cache.Value.Destroy(); } + } - updateTimestampsCache.Destroy(); + foreach (var cache in _allCacheRegions.Values) + { + cache.Destroy(); } settings.CacheProvider.Stop(); diff --git a/src/NHibernate/Cache/CacheExtensions.cs b/src/NHibernate/Cache/CacheExtensions.cs new file mode 100644 index 00000000000..1b212572557 --- /dev/null +++ b/src/NHibernate/Cache/CacheExtensions.cs @@ -0,0 +1,16 @@ +using System; + +namespace NHibernate.Cache +{ + //6.0 TODO: Remove + internal static class CacheExtensions + { +#pragma warning disable 618 + public static CacheBase AsCacheBase(this ICache cache) +#pragma warning restore 618 + { + if (cache == null) throw new ArgumentNullException(nameof(cache)); + return cache as CacheBase ?? new ObsoleteCacheWrapper(cache); + } + } +} \ No newline at end of file diff --git a/src/NHibernate/Cache/CacheFactory.cs b/src/NHibernate/Cache/CacheFactory.cs index 8a7d3b08641..b6c5df8549d 100644 --- a/src/NHibernate/Cache/CacheFactory.cs +++ b/src/NHibernate/Cache/CacheFactory.cs @@ -1,6 +1,6 @@ - -using NHibernate.Cfg; +using System; using System.Collections.Generic; +using NHibernate.Cfg; namespace NHibernate.Cache { @@ -30,27 +30,42 @@ public static class CacheFactory /// Used to retrieve the global cache region prefix. /// Properties the cache provider can use to configure the cache. /// An to use for this object in the . - public static ICacheConcurrencyStrategy CreateCache(string usage, string name, bool mutable, Settings settings, - IDictionary properties) + // Since v5.3 + [Obsolete("Please use overload with a CacheBase parameter.")] + public static ICacheConcurrencyStrategy CreateCache( + string usage, + string name, + bool mutable, + Settings settings, + IDictionary properties) { - if (usage == null || !settings.IsSecondLevelCacheEnabled) return null; //no cache + if (usage == null || !settings.IsSecondLevelCacheEnabled) return null; + + var cache = BuildCacheBase(name, settings, properties); + + var ccs = CreateCache(usage, cache); + + if (mutable && usage == ReadOnly) + log.Warn("read-only cache configured for mutable: {0}", name); - string prefix = settings.CacheRegionPrefix; - if (prefix != null) name = prefix + '.' + name; + return ccs; + } + /// + /// Creates an from the parameters. + /// + /// The name of the strategy that should use for the class. + /// The used for this strategy. + /// An to use for this object in the . + public static ICacheConcurrencyStrategy CreateCache(string usage, CacheBase cache) + { if (log.IsDebugEnabled()) - { - log.Debug("cache for: {0} usage strategy: {1}", name, usage); - } + log.Debug("cache for: {0} usage strategy: {1}", cache.RegionName, usage); ICacheConcurrencyStrategy ccs; switch (usage) { case ReadOnly: - if (mutable) - { - log.Warn("read-only cache configured for mutable: {0}", name); - } ccs = new ReadOnlyCache(); break; case ReadWrite: @@ -59,24 +74,29 @@ public static ICacheConcurrencyStrategy CreateCache(string usage, string name, b case NonstrictReadWrite: ccs = new NonstrictReadWriteCache(); break; - //case CacheFactory.Transactional: - // ccs = new TransactionalCache(); - // break; + //case CacheFactory.Transactional: + // ccs = new TransactionalCache(); + // break; default: throw new MappingException( - "cache usage attribute should be read-write, read-only, nonstrict-read-write, or transactional"); + "cache usage attribute should be read-write, read-only or nonstrict-read-write"); } + ccs.Cache = cache; + + return ccs; + } + + internal static CacheBase BuildCacheBase(string name, Settings settings, IDictionary properties) + { try { - ccs.Cache = settings.CacheProvider.BuildCache(name, properties); + return settings.CacheProvider.BuildCache(name, properties).AsCacheBase(); } catch (CacheException e) { throw new HibernateException("Could not instantiate cache implementation", e); } - - return ccs; } } } diff --git a/src/NHibernate/Cache/ICacheConcurrencyStrategy.cs b/src/NHibernate/Cache/ICacheConcurrencyStrategy.cs index 41319d62ad7..3c4e8755384 100644 --- a/src/NHibernate/Cache/ICacheConcurrencyStrategy.cs +++ b/src/NHibernate/Cache/ICacheConcurrencyStrategy.cs @@ -124,9 +124,12 @@ public partial interface ICacheConcurrencyStrategy void Clear(); /// - /// Clean up all resources. + /// Clean up resources. /// /// + /// + /// This method should not destroy . The session factory is responsible for it. + /// void Destroy(); /// @@ -206,10 +209,7 @@ internal static CacheBase GetCacheBase(this ICacheConcurrencyStrategy cache) { if (cache is IBatchableCacheConcurrencyStrategy batchableCache) return batchableCache.Cache; - var concreteCache = cache.Cache; - if (concreteCache == null) - return null; - return concreteCache as CacheBase ?? new ObsoleteCacheWrapper(concreteCache); + return cache.Cache?.AsCacheBase(); } } } diff --git a/src/NHibernate/Cache/IQueryCache.cs b/src/NHibernate/Cache/IQueryCache.cs index b770980c20f..6778c60be78 100644 --- a/src/NHibernate/Cache/IQueryCache.cs +++ b/src/NHibernate/Cache/IQueryCache.cs @@ -44,8 +44,11 @@ public partial interface IQueryCache IList Get(QueryKey key, ICacheAssembler[] returnTypes, bool isNaturalKeyLookup, ISet spaces, ISessionImplementor session); /// - /// Clean up all resources. + /// Clean up resources. /// + /// + /// This method should not destroy . The session factory is responsible for it. + /// void Destroy(); } diff --git a/src/NHibernate/Cache/IQueryCacheFactory.cs b/src/NHibernate/Cache/IQueryCacheFactory.cs index 1b735e4899a..f9c828f22ae 100644 --- a/src/NHibernate/Cache/IQueryCacheFactory.cs +++ b/src/NHibernate/Cache/IQueryCacheFactory.cs @@ -1,3 +1,4 @@ +using System; using System.Collections.Generic; using NHibernate.Cfg; @@ -9,7 +10,59 @@ namespace NHibernate.Cache /// public interface IQueryCacheFactory { - IQueryCache GetQueryCache(string regionName, UpdateTimestampsCache updateTimestampsCache, Settings settings, - IDictionary props); + // Since v5.3 + [Obsolete("Please use extension overload with a CacheBase parameter.")] + IQueryCache GetQueryCache( + string regionName, + UpdateTimestampsCache updateTimestampsCache, + Settings settings, + IDictionary props); } -} \ No newline at end of file + + // 6.0 TODO: move to interface. + public static class QueryCacheFactoryExtension + { + private static readonly INHibernateLogger Logger = NHibernateLogger.For(typeof(QueryCacheFactoryExtension)); + + /// + /// Build a query cache. + /// + /// The query cache factory. + /// The cache of updates timestamps. + /// The NHibernate settings properties. + /// The to use for the region. + /// A query cache. null if does not implement a + /// public IQueryCache GetQueryCache(UpdateTimestampsCache, IDictionary<string, string> props, CacheBase) + /// method. + public static IQueryCache GetQueryCache( + this IQueryCacheFactory factory, + UpdateTimestampsCache updateTimestampsCache, + IDictionary props, + CacheBase regionCache) + { + if (factory is StandardQueryCacheFactory standardFactory) + { + return standardFactory.GetQueryCache(updateTimestampsCache, props, regionCache); + } + + // Use reflection for supporting custom factories. + var factoryType = factory.GetType(); + var getQueryCacheMethod = factoryType.GetMethod( + nameof(StandardQueryCacheFactory.GetQueryCache), + new[] { typeof(UpdateTimestampsCache), typeof(IDictionary), typeof(CacheBase) }); + if (getQueryCacheMethod != null) + { + return (IQueryCache) getQueryCacheMethod.Invoke( + factory, + new object[] { updateTimestampsCache, props, regionCache }); + } + + // Caller has to call the obsolete method. + Logger.Warn( + "{0} does not implement 'IQueryCache GetQueryCache(UpdateTimestampsCache, IDictionary<string, " + + "string> props, CacheBase)', its obsolete overload will be used.", + factoryType); + return null; + } + } +} diff --git a/src/NHibernate/Cache/NonstrictReadWriteCache.cs b/src/NHibernate/Cache/NonstrictReadWriteCache.cs index e6af5d4982a..065d7189ac6 100644 --- a/src/NHibernate/Cache/NonstrictReadWriteCache.cs +++ b/src/NHibernate/Cache/NonstrictReadWriteCache.cs @@ -1,4 +1,3 @@ -using System; using System.Collections; using System.Collections.Generic; using System.Linq; @@ -18,7 +17,6 @@ public partial class NonstrictReadWriteCache : IBatchableCacheConcurrencyStrateg { private static readonly INHibernateLogger log = NHibernateLogger.For(typeof(NonstrictReadWriteCache)); - // 6.0 TODO: remove private CacheBase _cache; /// @@ -35,10 +33,7 @@ public ICache Cache #pragma warning restore 618 { get { return _cache; } - set - { - _cache = value as CacheBase ?? new ObsoleteCacheWrapper(value); - } + set { _cache = value?.AsCacheBase(); } } // 6.0 TODO: make implicit and switch to auto-property @@ -207,14 +202,9 @@ public void Clear() public void Destroy() { - try - { - Cache.Destroy(); - } - catch (Exception e) - { - log.Warn(e, "Could not destroy cache"); - } + // The cache is externally provided and may be shared. Destroying the cache is + // not the responsibility of this class. + Cache = null; } /// diff --git a/src/NHibernate/Cache/ReadOnlyCache.cs b/src/NHibernate/Cache/ReadOnlyCache.cs index dd2fde0e51a..0f71872282f 100644 --- a/src/NHibernate/Cache/ReadOnlyCache.cs +++ b/src/NHibernate/Cache/ReadOnlyCache.cs @@ -13,7 +13,6 @@ public partial class ReadOnlyCache : IBatchableCacheConcurrencyStrategy { private static readonly INHibernateLogger log = NHibernateLogger.For(typeof(ReadOnlyCache)); - // 6.0 TODO: remove private CacheBase _cache; /// @@ -30,10 +29,7 @@ public ICache Cache #pragma warning restore 618 { get { return _cache; } - set - { - _cache = value as CacheBase ?? new ObsoleteCacheWrapper(value); - } + set { _cache = value?.AsCacheBase(); } } // 6.0 TODO: make implicit and switch to auto-property @@ -185,14 +181,9 @@ public void Remove(CacheKey key) public void Destroy() { - try - { - Cache.Destroy(); - } - catch (Exception e) - { - log.Warn(e, "Could not destroy cache"); - } + // The cache is externally provided and may be shared. Destroying the cache is + // not the responsibility of this class. + Cache = null; } /// diff --git a/src/NHibernate/Cache/ReadWriteCache.cs b/src/NHibernate/Cache/ReadWriteCache.cs index 4a8fff66187..1b0ca1f879f 100644 --- a/src/NHibernate/Cache/ReadWriteCache.cs +++ b/src/NHibernate/Cache/ReadWriteCache.cs @@ -1,4 +1,3 @@ -using System; using System.Collections; using System.Collections.Generic; using System.Linq; @@ -35,7 +34,6 @@ public interface ILockable private static readonly INHibernateLogger log = NHibernateLogger.For(typeof(ReadWriteCache)); private readonly object _lockObject = new object(); - // 6.0 TODO: remove private CacheBase _cache; private int _nextLockId; @@ -53,10 +51,7 @@ public ICache Cache #pragma warning restore 618 { get { return _cache; } - set - { - _cache = value as CacheBase ?? new ObsoleteCacheWrapper(value); - } + set { _cache = value?.AsCacheBase(); } } // 6.0 TODO: make implicit and switch to auto-property @@ -417,14 +412,9 @@ public void Remove(CacheKey key) public void Destroy() { - try - { - Cache.Destroy(); - } - catch (Exception e) - { - log.Warn(e, "Could not destroy cache"); - } + // The cache is externally provided and may be shared. Destroying the cache is + // not the responsibility of this class. + Cache = null; } /// diff --git a/src/NHibernate/Cache/StandardQueryCache.cs b/src/NHibernate/Cache/StandardQueryCache.cs index c419995ae29..ee361c1541c 100644 --- a/src/NHibernate/Cache/StandardQueryCache.cs +++ b/src/NHibernate/Cache/StandardQueryCache.cs @@ -20,32 +20,48 @@ public partial class StandardQueryCache : IQueryCache, IBatchableQueryCache private static readonly INHibernateLogger Log = NHibernateLogger.For(typeof (StandardQueryCache)); private readonly string _regionName; private readonly UpdateTimestampsCache _updateTimestampsCache; - // 6.0 TODO: remove private readonly CacheBase _cache; - public StandardQueryCache(Settings settings, IDictionary props, UpdateTimestampsCache updateTimestampsCache, string regionName) + // Since v5.3 + [Obsolete("Please use overload with a CacheBase parameter.")] + public StandardQueryCache( + Settings settings, + IDictionary props, + UpdateTimestampsCache updateTimestampsCache, + string regionName) + : this( + updateTimestampsCache, + CacheFactory.BuildCacheBase( + settings.GetFullCacheRegionName(regionName ?? typeof(StandardQueryCache).FullName), + settings, + props)) { - if (regionName == null) - regionName = typeof(StandardQueryCache).FullName; - - var prefix = settings.CacheRegionPrefix; - if (!string.IsNullOrEmpty(prefix)) - regionName = prefix + '.' + regionName; + } - Log.Info("starting query cache at region: {0}", regionName); + /// + /// Build a query cache. + /// + /// The cache of updates timestamps. + /// The to use for the region. + public StandardQueryCache( + UpdateTimestampsCache updateTimestampsCache, + CacheBase regionCache) + { + if (regionCache == null) + throw new ArgumentNullException(nameof(regionCache)); - Cache = settings.CacheProvider.BuildCache(regionName, props); - _cache = Cache as CacheBase ?? new ObsoleteCacheWrapper(Cache); + _regionName = regionCache.RegionName; + Log.Info("starting query cache at region: {0}", _regionName); + _cache = regionCache; _updateTimestampsCache = updateTimestampsCache; - _regionName = regionName; } #region IQueryCache Members // 6.0 TODO: type as CacheBase instead #pragma warning disable 618 - public ICache Cache { get; } + public ICache Cache => _cache; #pragma warning restore 618 public string RegionName @@ -265,14 +281,8 @@ public IList[] GetMany( public void Destroy() { - try - { - Cache.Destroy(); - } - catch (Exception e) - { - Log.Warn(e, "could not destroy query cache: {0}", _regionName); - } + // The cache is externally provided and may be shared. Destroying the cache is + // not the responsibility of this class. } #endregion diff --git a/src/NHibernate/Cache/StandardQueryCacheFactory.cs b/src/NHibernate/Cache/StandardQueryCacheFactory.cs index efb23d20441..8f453468fef 100644 --- a/src/NHibernate/Cache/StandardQueryCacheFactory.cs +++ b/src/NHibernate/Cache/StandardQueryCacheFactory.cs @@ -1,3 +1,4 @@ +using System; using System.Collections.Generic; using NHibernate.Cfg; @@ -9,6 +10,8 @@ namespace NHibernate.Cache /// public class StandardQueryCacheFactory : IQueryCacheFactory { + // Since v5.3 + [Obsolete("Please use overload with a CacheBase parameter.")] public IQueryCache GetQueryCache(string regionName, UpdateTimestampsCache updateTimestampsCache, Settings settings, @@ -16,5 +19,20 @@ public IQueryCache GetQueryCache(string regionName, { return new StandardQueryCache(settings, props, updateTimestampsCache, regionName); } + + /// + /// Build a query cache. + /// + /// The cache of updates timestamps. + /// The NHibernate settings properties. + /// The to use for the region. + /// A query cache. + public virtual IQueryCache GetQueryCache( + UpdateTimestampsCache updateTimestampsCache, + IDictionary props, + CacheBase regionCache) + { + return new StandardQueryCache(updateTimestampsCache, regionCache); + } } -} \ No newline at end of file +} diff --git a/src/NHibernate/Cache/UpdateTimestampsCache.cs b/src/NHibernate/Cache/UpdateTimestampsCache.cs index b6d462328e9..e98b30d5dc2 100644 --- a/src/NHibernate/Cache/UpdateTimestampsCache.cs +++ b/src/NHibernate/Cache/UpdateTimestampsCache.cs @@ -20,20 +20,30 @@ public partial class UpdateTimestampsCache private static readonly INHibernateLogger log = NHibernateLogger.For(typeof(UpdateTimestampsCache)); private readonly CacheBase _updateTimestamps; - private readonly string regionName = typeof(UpdateTimestampsCache).Name; - public virtual void Clear() { _updateTimestamps.Clear(); } + // Since v5.3 + [Obsolete("Please use overload with a CacheBase parameter.")] public UpdateTimestampsCache(Settings settings, IDictionary props) + : this( + CacheFactory.BuildCacheBase( + settings.GetFullCacheRegionName(nameof(UpdateTimestampsCache)), + settings, + props)) + { + } + + /// + /// Build the update timestamps cache. + /// x + /// The to use. + public UpdateTimestampsCache(CacheBase cache) { - var prefix = settings.CacheRegionPrefix; - regionName = prefix == null ? regionName : prefix + '.' + regionName; - log.Info("starting update timestamps cache at region: {0}", regionName); - var updateTimestamps = settings.CacheProvider.BuildCache(regionName, props); - _updateTimestamps = updateTimestamps as CacheBase ?? new ObsoleteCacheWrapper(updateTimestamps); + log.Info("starting update timestamps cache at region: {0}", cache.RegionName); + _updateTimestamps = cache; } //Since v5.1 @@ -145,16 +155,12 @@ public virtual bool[] AreUpToDate(ISet[] spaces, long[] timestamps) return results; } + // Since v5.3 + [Obsolete("This method has no usages anymore")] public virtual void Destroy() { - try - { - _updateTimestamps.Destroy(); - } - catch (Exception e) - { - log.Warn(e, "could not destroy UpdateTimestamps cache"); - } + // The cache is externally provided and may be shared. Destroying the cache is + // not the responsibility of this class. } private bool IsOutdated(long? lastUpdate, long timestamp) diff --git a/src/NHibernate/Cfg/Settings.cs b/src/NHibernate/Cfg/Settings.cs index 5ad3e3ca8e2..878b9b60605 100644 --- a/src/NHibernate/Cfg/Settings.cs +++ b/src/NHibernate/Cfg/Settings.cs @@ -137,5 +137,13 @@ public Settings() public IQueryModelRewriterFactory QueryModelRewriterFactory { get; internal set; } #endregion + + internal string GetFullCacheRegionName(string name) + { + var prefix = CacheRegionPrefix; + if (!string.IsNullOrEmpty(prefix)) + return prefix + '.' + name; + return name; + } } } \ No newline at end of file diff --git a/src/NHibernate/Impl/SessionFactoryImpl.cs b/src/NHibernate/Impl/SessionFactoryImpl.cs index 4f4b4504423..ae70fe57ad2 100644 --- a/src/NHibernate/Impl/SessionFactoryImpl.cs +++ b/src/NHibernate/Impl/SessionFactoryImpl.cs @@ -95,11 +95,8 @@ public void HandleEntityNotFound(string entityName, object id) private static readonly IIdentifierGenerator UuidGenerator = new UUIDHexGenerator(); [NonSerialized] - // 6.0 TODO: type as CacheBase instead -#pragma warning disable 618 - private readonly ConcurrentDictionary allCacheRegions = - new ConcurrentDictionary(); -#pragma warning restore 618 + private readonly ConcurrentDictionary _allCacheRegions = + new ConcurrentDictionary(); [NonSerialized] private readonly IDictionary classMetadata; @@ -235,7 +232,7 @@ public SessionFactoryImpl(Configuration cfg, IMapping mapping, Settings settings #region Persisters - Dictionary caches = new Dictionary(); + var caches = new Dictionary, ICacheConcurrencyStrategy>(); entityPersisters = new Dictionary(); implementorToEntityName = new Dictionary(); @@ -244,22 +241,12 @@ public SessionFactoryImpl(Configuration cfg, IMapping mapping, Settings settings foreach (PersistentClass model in cfg.ClassMappings) { model.PrepareTemporaryTables(mapping, settings.Dialect); - string cacheRegion = model.RootClazz.CacheRegionName; - ICacheConcurrencyStrategy cache; - if (!caches.TryGetValue(cacheRegion, out cache)) - { - cache = - CacheFactory.CreateCache(model.CacheConcurrencyStrategy, cacheRegion, model.IsMutable, settings, properties); - if (cache != null) - { - caches.Add(cacheRegion, cache); - if (!allCacheRegions.TryAdd(cache.RegionName, cache.Cache)) - { - throw new HibernateException("An item with the same key has already been added to allCacheRegions."); - } - } - } - IEntityPersister cp = PersisterFactory.CreateClassPersister(model, cache, this, mapping); + var cache = GetCacheConcurrencyStrategy( + model.RootClazz.CacheRegionName, + model.CacheConcurrencyStrategy, + model.IsMutable, + caches); + var cp = PersisterFactory.CreateClassPersister(model, cache, this, mapping); entityPersisters[model.EntityName] = cp; classMeta[model.EntityName] = cp.ClassMetadata; @@ -274,14 +261,12 @@ public SessionFactoryImpl(Configuration cfg, IMapping mapping, Settings settings collectionPersisters = new Dictionary(); foreach (Mapping.Collection model in cfg.CollectionMappings) { - ICacheConcurrencyStrategy cache = - CacheFactory.CreateCache(model.CacheConcurrencyStrategy, model.CacheRegionName, model.Owner.IsMutable, settings, - properties); - if (cache != null) - { - allCacheRegions[cache.RegionName] = cache.Cache; - } - ICollectionPersister persister = PersisterFactory.CreateCollectionPersister(model, cache, this); + var cache = GetCacheConcurrencyStrategy( + model.CacheRegionName, + model.CacheConcurrencyStrategy, + model.Owner.IsMutable, + caches); + var persister = PersisterFactory.CreateCollectionPersister(model, cache, this); collectionPersisters[model.Role] = persister; IType indexType = persister.IndexType; if (indexType != null && indexType.IsAssociationType && !indexType.IsAnyType) @@ -382,9 +367,12 @@ public SessionFactoryImpl(Configuration cfg, IMapping mapping, Settings settings if (settings.IsQueryCacheEnabled) { - updateTimestampsCache = new UpdateTimestampsCache(settings, properties); - queryCache = settings.QueryCacheFactory.GetQueryCache(null, updateTimestampsCache, settings, properties); + var updateTimestampsCacheName = typeof(UpdateTimestampsCache).Name; + updateTimestampsCache = new UpdateTimestampsCache(GetCache(updateTimestampsCacheName)); + var queryCacheName = typeof(StandardQueryCache).FullName; + queryCache = BuildQueryCache(queryCacheName); queryCaches = new ConcurrentDictionary>(); + queryCaches.TryAdd(queryCacheName, new Lazy(() => queryCache)); } else { @@ -421,6 +409,44 @@ public SessionFactoryImpl(Configuration cfg, IMapping mapping, Settings settings entityNotFoundDelegate = enfd; } + private IQueryCache BuildQueryCache(string queryCacheName) + { + return + settings.QueryCacheFactory.GetQueryCache( + updateTimestampsCache, + properties, + GetCache(queryCacheName)) + // 6.0 TODO: remove the coalesce once IQueryCacheFactory todos are done +#pragma warning disable 618 + ?? settings.QueryCacheFactory.GetQueryCache( +#pragma warning restore 618 + queryCacheName, + updateTimestampsCache, + settings, + properties); + } + + private ICacheConcurrencyStrategy GetCacheConcurrencyStrategy( + string cacheRegion, + string strategy, + bool isMutable, + Dictionary, ICacheConcurrencyStrategy> caches) + { + if (strategy == null || !settings.IsSecondLevelCacheEnabled) + return null; + + var cacheKey = new Tuple(cacheRegion, strategy); + if (caches.TryGetValue(cacheKey, out var cache)) + return cache; + + cache = CacheFactory.CreateCache(strategy, GetCache(cacheRegion)); + caches.Add(cacheKey, cache); + if (isMutable && strategy == CacheFactory.ReadOnly) + log.Warn("read-only cache configured for mutable: {0}", name); + + return cache; + } + public EventListeners EventListeners { get { return eventListeners; } @@ -848,14 +874,15 @@ public void Close() if (settings.IsQueryCacheEnabled) { - queryCache.Destroy(); - foreach (var cache in queryCaches.Values) { cache.Value.Destroy(); } + } - updateTimestampsCache.Destroy(); + foreach (var cache in _allCacheRegions.Values) + { + cache.Destroy(); } settings.CacheProvider.Stop(); @@ -1041,8 +1068,13 @@ public UpdateTimestampsCache UpdateTimestampsCache public IDictionary GetAllSecondLevelCacheRegions() #pragma warning restore 618 { - // ToArray creates a moment in time snapshot - return allCacheRegions.ToArray().ToDictionary(kv => kv.Key, kv => kv.Value); + return + _allCacheRegions + // ToArray creates a moment in time snapshot + .ToArray() +#pragma warning disable 618 + .ToDictionary(kv => kv.Key, kv => (ICache) kv.Value); +#pragma warning restore 618 } // 6.0 TODO: return CacheBase instead @@ -1050,10 +1082,24 @@ public IDictionary GetAllSecondLevelCacheRegions() public ICache GetSecondLevelCacheRegion(string regionName) #pragma warning restore 618 { - allCacheRegions.TryGetValue(regionName, out var result); + _allCacheRegions.TryGetValue(regionName, out var result); return result; } + private CacheBase GetCache(string cacheRegion) + { + // If run concurrently for the same region and type, this may built many caches for the same region and type. + // Currently only GetQueryCache may be run concurrently, and its implementation prevents + // concurrent creation call for the same region, so this will not happen. + // Otherwise the dictionary will have to be changed for using a lazy, see + // https://stackoverflow.com/a/31637510/1178314 + cacheRegion = settings.GetFullCacheRegionName(cacheRegion); + + return _allCacheRegions.GetOrAdd( + cacheRegion, + cr => CacheFactory.BuildCacheBase(cr, settings, properties)); + } + /// Statistics SPI public IStatisticsImplementor StatisticsImplementor { @@ -1079,15 +1125,12 @@ public IQueryCache GetQueryCache(string cacheRegion) // The factory may be run concurrently by threads trying to get the same region. // But the GetOrAdd will yield the same lazy for all threads, so only one will // initialize. https://stackoverflow.com/a/31637510/1178314 - return queryCaches.GetOrAdd( - cacheRegion, - cr => new Lazy( - () => - { - var currentQueryCache = settings.QueryCacheFactory.GetQueryCache(cr, updateTimestampsCache, settings, properties); - allCacheRegions[currentQueryCache.RegionName] = currentQueryCache.Cache; - return currentQueryCache; - })).Value; + return + queryCaches + .GetOrAdd( + cacheRegion, + cr => new Lazy(() => BuildQueryCache(cr))) + .Value; } public void EvictQueries()