diff --git a/src/AsyncGenerator.yml b/src/AsyncGenerator.yml index accaaeabe72..7022d322eb6 100644 --- a/src/AsyncGenerator.yml +++ b/src/AsyncGenerator.yml @@ -4,8 +4,6 @@ applyChanges: true analyzation: methodConversion: - - conversion: Ignore - hasAttributeName: ObsoleteAttribute - conversion: Ignore name: PostProcessInsert containingTypeName: HqlSqlWalker @@ -96,6 +94,9 @@ - conversion: Ignore name: Exists containingTypeName: AbstractCollectionPersister + - conversion: Ignore + name: QuoteTableAndColumns + containingTypeName: SchemaMetadataUpdater - conversion: ToAsync name: ExecuteReader containingTypeName: IBatcher @@ -119,6 +120,8 @@ - name: GetFieldValue - name: IsDBNull - name: WriteLine + ignoreAsyncCounterparts: + - rule: Obsolete callForwarding: true cancellationTokens: guards: true @@ -259,6 +262,9 @@ methodRules: - containingType: NHibernate.Tool.hbm2ddl.SchemaValidator - containingType: NHibernate.Tool.hbm2ddl.SchemaExport name: PubliclyExposedType +- filters: + - hasAttributeName: ObsoleteAttribute + name: Obsolete typeRules: - filters: - containingAssemblyName: NHibernate diff --git a/src/NHibernate.Test/Async/SecondLevelCacheTest/InvalidationTests.cs b/src/NHibernate.Test/Async/SecondLevelCacheTest/InvalidationTests.cs new file mode 100644 index 00000000000..76e5b040cbb --- /dev/null +++ b/src/NHibernate.Test/Async/SecondLevelCacheTest/InvalidationTests.cs @@ -0,0 +1,115 @@ +//------------------------------------------------------------------------------ +// +// This code was generated by AsyncGenerator. +// +// Changes to this file may cause incorrect behavior and will be lost if +// the code is regenerated. +// +//------------------------------------------------------------------------------ + + +using System.Collections; +using System.Collections.Generic; +using System.Linq; +using System.Reflection; +using NHibernate.Cache; +using NHibernate.Cfg; +using NHibernate.Engine; +using NHibernate.Impl; +using NHibernate.Test.SecondLevelCacheTests; +using NSubstitute; +using NUnit.Framework; + +namespace NHibernate.Test.SecondLevelCacheTest +{ + using System.Threading.Tasks; + using System.Threading; + [TestFixture] + public class InvalidationTestsAsync : TestCase + { + protected override string MappingsAssembly => "NHibernate.Test"; + + protected override IList Mappings => new[] { "SecondLevelCacheTest.Item.hbm.xml" }; + + protected override void Configure(Configuration configuration) + { + configuration.SetProperty(Environment.CacheProvider, typeof(HashtableCacheProvider).AssemblyQualifiedName); + configuration.SetProperty(Environment.UseQueryCache, "true"); + } + + [Test] + public async Task InvalidatesEntitiesAsync() + { + var debugSessionFactory = (DebugSessionFactory) Sfi; + + var cache = Substitute.For(Sfi.Settings, new Dictionary()); + + var updateTimestampsCacheField = typeof(SessionFactoryImpl).GetField( + "updateTimestampsCache", + BindingFlags.Instance | BindingFlags.NonPublic | BindingFlags.Public); + + updateTimestampsCacheField.SetValue(debugSessionFactory.ActualFactory, cache); + + //"Received" assertions can not be used since the collection is reused and cleared between calls. + //The received args are cloned and stored + var preInvalidations = new List>(); + var invalidations = new List>(); + + await (cache.PreInvalidateAsync(Arg.Do>(x => preInvalidations.Add(x.ToList())), CancellationToken.None)); + await (cache.InvalidateAsync(Arg.Do>(x => invalidations.Add(x.ToList())), CancellationToken.None)); + + using (var session = OpenSession()) + { + using (var tx = session.BeginTransaction()) + { + foreach (var i in Enumerable.Range(1, 10)) + { + var item = new Item {Id = i}; + await (session.SaveAsync(item)); + } + + await (tx.CommitAsync()); + } + + using (var tx = session.BeginTransaction()) + { + foreach (var i in Enumerable.Range(1, 10)) + { + var item = await (session.GetAsync(i)); + item.Name = item.Id.ToString(); + } + + await (tx.CommitAsync()); + } + + using (var tx = session.BeginTransaction()) + { + foreach (var i in Enumerable.Range(1, 10)) + { + var item = await (session.GetAsync(i)); + await (session.DeleteAsync(item)); + } + + await (tx.CommitAsync()); + } + } + + //Should receive one preinvalidation and one invalidation per commit + Assert.That(preInvalidations, Has.Count.EqualTo(3)); + Assert.That(preInvalidations, Has.All.Count.EqualTo(1).And.Contains("Item")); + + Assert.That(invalidations, Has.Count.EqualTo(3)); + Assert.That(invalidations, Has.All.Count.EqualTo(1).And.Contains("Item")); + } + + protected override void OnTearDown() + { + using (var s = OpenSession()) + using (var tx = s.BeginTransaction()) + { + s.Delete("from Item"); + tx.Commit(); + } + } + } +} diff --git a/src/NHibernate.Test/SecondLevelCacheTest/InvalidationTests.cs b/src/NHibernate.Test/SecondLevelCacheTest/InvalidationTests.cs new file mode 100644 index 00000000000..050a488bc1e --- /dev/null +++ b/src/NHibernate.Test/SecondLevelCacheTest/InvalidationTests.cs @@ -0,0 +1,102 @@ +using System.Collections; +using System.Collections.Generic; +using System.Linq; +using System.Reflection; +using NHibernate.Cache; +using NHibernate.Cfg; +using NHibernate.Impl; +using NHibernate.Test.SecondLevelCacheTests; +using NSubstitute; +using NUnit.Framework; + +namespace NHibernate.Test.SecondLevelCacheTest +{ + [TestFixture] + public class InvalidationTests : TestCase + { + protected override string MappingsAssembly => "NHibernate.Test"; + + protected override IList Mappings => new[] { "SecondLevelCacheTest.Item.hbm.xml" }; + + protected override void Configure(Configuration configuration) + { + configuration.SetProperty(Environment.CacheProvider, typeof(HashtableCacheProvider).AssemblyQualifiedName); + configuration.SetProperty(Environment.UseQueryCache, "true"); + } + + [Test] + public void InvalidatesEntities() + { + var debugSessionFactory = (DebugSessionFactory) Sfi; + + var cache = Substitute.For(Sfi.Settings, new Dictionary()); + + var updateTimestampsCacheField = typeof(SessionFactoryImpl).GetField( + "updateTimestampsCache", + BindingFlags.Instance | BindingFlags.NonPublic | BindingFlags.Public); + + updateTimestampsCacheField.SetValue(debugSessionFactory.ActualFactory, cache); + + //"Received" assertions can not be used since the collection is reused and cleared between calls. + //The received args are cloned and stored + var preInvalidations = new List>(); + var invalidations = new List>(); + + cache.PreInvalidate(Arg.Do>(x => preInvalidations.Add(x.ToList()))); + cache.Invalidate(Arg.Do>(x => invalidations.Add(x.ToList()))); + + using (var session = OpenSession()) + { + using (var tx = session.BeginTransaction()) + { + foreach (var i in Enumerable.Range(1, 10)) + { + var item = new Item {Id = i}; + session.Save(item); + } + + tx.Commit(); + } + + using (var tx = session.BeginTransaction()) + { + foreach (var i in Enumerable.Range(1, 10)) + { + var item = session.Get(i); + item.Name = item.Id.ToString(); + } + + tx.Commit(); + } + + using (var tx = session.BeginTransaction()) + { + foreach (var i in Enumerable.Range(1, 10)) + { + var item = session.Get(i); + session.Delete(item); + } + + tx.Commit(); + } + } + + //Should receive one preinvalidation and one invalidation per commit + Assert.That(preInvalidations, Has.Count.EqualTo(3)); + Assert.That(preInvalidations, Has.All.Count.EqualTo(1).And.Contains("Item")); + + Assert.That(invalidations, Has.Count.EqualTo(3)); + Assert.That(invalidations, Has.All.Count.EqualTo(1).And.Contains("Item")); + } + + protected override void OnTearDown() + { + using (var s = OpenSession()) + using (var tx = s.BeginTransaction()) + { + s.Delete("from Item"); + tx.Commit(); + } + } + } +} diff --git a/src/NHibernate/Async/Cache/UpdateTimestampsCache.cs b/src/NHibernate/Async/Cache/UpdateTimestampsCache.cs index 5a1867189fd..9a086f16333 100644 --- a/src/NHibernate/Async/Cache/UpdateTimestampsCache.cs +++ b/src/NHibernate/Async/Cache/UpdateTimestampsCache.cs @@ -10,6 +10,7 @@ using System; using System.Collections.Generic; +using System.Linq; using System.Runtime.CompilerServices; using NHibernate.Cfg; @@ -24,7 +25,7 @@ public partial class UpdateTimestampsCache private readonly NHibernate.Util.AsyncLock _invalidate = new NHibernate.Util.AsyncLock(); private readonly NHibernate.Util.AsyncLock _isUpToDate = new NHibernate.Util.AsyncLock(); - public Task ClearAsync(CancellationToken cancellationToken) + public virtual Task ClearAsync(CancellationToken cancellationToken) { if (cancellationToken.IsCancellationRequested) { @@ -33,26 +34,65 @@ public Task ClearAsync(CancellationToken cancellationToken) return updateTimestamps.ClearAsync(cancellationToken); } + //Since v5.1 + [Obsolete("Please use PreInvalidate(IReadOnlyCollection) instead.")] + public Task PreInvalidateAsync(object[] spaces, CancellationToken cancellationToken) + { + if (cancellationToken.IsCancellationRequested) + { + return Task.FromCanceled(cancellationToken); + } + try + { + //Only for backwards compatibility. + return PreInvalidateAsync(spaces.OfType().ToList(), cancellationToken); + } + catch (Exception ex) + { + return Task.FromException(ex); + } + } + [MethodImpl()] - public async Task PreInvalidateAsync(object[] spaces, CancellationToken cancellationToken) + public virtual async Task PreInvalidateAsync(IReadOnlyCollection spaces, CancellationToken cancellationToken) { cancellationToken.ThrowIfCancellationRequested(); using (await _preInvalidate.LockAsync()) { //TODO: to handle concurrent writes correctly, this should return a Lock to the client long ts = updateTimestamps.NextTimestamp() + updateTimestamps.Timeout; - for (int i = 0; i < spaces.Length; i++) + foreach (var space in spaces) { - await (updateTimestamps.PutAsync(spaces[i], ts, cancellationToken)).ConfigureAwait(false); + await (updateTimestamps.PutAsync(space, ts, cancellationToken)).ConfigureAwait(false); } + //TODO: return new Lock(ts); } + //TODO: return new Lock(ts); } - /// + //Since v5.1 + [Obsolete("Please use PreInvalidate(IReadOnlyCollection) instead.")] + public Task InvalidateAsync(object[] spaces, CancellationToken cancellationToken) + { + if (cancellationToken.IsCancellationRequested) + { + return Task.FromCanceled(cancellationToken); + } + try + { + //Only for backwards compatibility. + return InvalidateAsync(spaces.OfType().ToList(), cancellationToken); + } + catch (Exception ex) + { + return Task.FromException(ex); + } + } + [MethodImpl()] - public async Task InvalidateAsync(object[] spaces, CancellationToken cancellationToken) + public virtual async Task InvalidateAsync(IReadOnlyCollection spaces, CancellationToken cancellationToken) { cancellationToken.ThrowIfCancellationRequested(); using (await _invalidate.LockAsync()) @@ -60,16 +100,16 @@ public async Task InvalidateAsync(object[] spaces, CancellationToken cancellatio //TODO: to handle concurrent writes correctly, the client should pass in a Lock long ts = updateTimestamps.NextTimestamp(); //TODO: if lock.getTimestamp().equals(ts) - for (int i = 0; i < spaces.Length; i++) + foreach (var space in spaces) { - log.Debug("Invalidating space [{0}]", spaces[i]); - await (updateTimestamps.PutAsync(spaces[i], ts, cancellationToken)).ConfigureAwait(false); + log.Debug("Invalidating space [{0}]", space); + await (updateTimestamps.PutAsync(space, ts, cancellationToken)).ConfigureAwait(false); } } } [MethodImpl()] - public async Task IsUpToDateAsync(ISet spaces, long timestamp /* H2.1 has Long here */, CancellationToken cancellationToken) + public virtual async Task IsUpToDateAsync(ISet spaces, long timestamp /* H2.1 has Long here */, CancellationToken cancellationToken) { cancellationToken.ThrowIfCancellationRequested(); using (await _isUpToDate.LockAsync()) diff --git a/src/NHibernate/Async/Engine/ActionQueue.cs b/src/NHibernate/Async/Engine/ActionQueue.cs index 26d53443fb2..15a5951dbdd 100644 --- a/src/NHibernate/Async/Engine/ActionQueue.cs +++ b/src/NHibernate/Async/Engine/ActionQueue.cs @@ -13,26 +13,16 @@ using System.Collections.Generic; using System.Linq; using System.Text; - +using System.Threading; +using System.Threading.Tasks; using NHibernate.Action; using NHibernate.Cache; using NHibernate.Type; namespace NHibernate.Engine { - using System.Threading.Tasks; - using System.Threading; public partial class ActionQueue { - - public Task AddActionAsync(BulkOperationCleanupAction cleanupAction, CancellationToken cancellationToken) - { - if (cancellationToken.IsCancellationRequested) - { - return Task.FromCanceled(cancellationToken); - } - return RegisterCleanupActionsAsync(cleanupAction, cancellationToken); - } private async Task ExecuteActionsAsync(IList list, CancellationToken cancellationToken) { @@ -41,50 +31,80 @@ private async Task ExecuteActionsAsync(IList list, CancellationToken cancellatio // It will then fail here due to list being modified. (Some previous code was dodging the // trouble with a for loop which was not failing provided the list was not getting smaller. // But then it was clearing it without having executed added actions (if any), ...) + foreach (IExecutable executable in list) - await (ExecuteAsync(executable, cancellationToken)).ConfigureAwait(false); - + { + await (InnerExecuteAsync(executable, cancellationToken)).ConfigureAwait(false); + } list.Clear(); await (session.Batcher.ExecuteBatchAsync(cancellationToken)).ConfigureAwait(false); } + private Task PreInvalidateCachesAsync(CancellationToken cancellationToken) + { + if (cancellationToken.IsCancellationRequested) + { + return Task.FromCanceled(cancellationToken); + } + try + { + if (session.Factory.Settings.IsQueryCacheEnabled) + { + return session.Factory.UpdateTimestampsCache.PreInvalidateAsync(executedSpaces, cancellationToken); + } + return Task.CompletedTask; + } + catch (Exception ex) + { + return Task.FromException(ex); + } + } + public async Task ExecuteAsync(IExecutable executable, CancellationToken cancellationToken) { cancellationToken.ThrowIfCancellationRequested(); try { - await (executable.ExecuteAsync(cancellationToken)).ConfigureAwait(false); + await (InnerExecuteAsync(executable, cancellationToken)).ConfigureAwait(false); } finally { - await (RegisterCleanupActionsAsync(executable, cancellationToken)).ConfigureAwait(false); + await (PreInvalidateCachesAsync(cancellationToken)).ConfigureAwait(false); } } - - private async Task RegisterCleanupActionsAsync(IExecutable executable, CancellationToken cancellationToken) + + private async Task InnerExecuteAsync(IExecutable executable, CancellationToken cancellationToken) { cancellationToken.ThrowIfCancellationRequested(); - beforeTransactionProcesses.Register(executable.BeforeTransactionCompletionProcess); - if (session.Factory.Settings.IsQueryCacheEnabled) + try { - string[] spaces = executable.PropertySpaces; - afterTransactionProcesses.AddSpacesToInvalidate(spaces); - await (session.Factory.UpdateTimestampsCache.PreInvalidateAsync(spaces, cancellationToken)).ConfigureAwait(false); + await (executable.ExecuteAsync(cancellationToken)).ConfigureAwait(false); + } + finally + { + if (executable.PropertySpaces != null) + { + executedSpaces.UnionWith(executable.PropertySpaces); + } + RegisterCleanupActions(executable); } - afterTransactionProcesses.Register(executable.AfterTransactionCompletionProcess); } /// /// Perform all currently queued entity-insertion actions. /// /// A cancellation token that can be used to cancel the work - public Task ExecuteInsertsAsync(CancellationToken cancellationToken) + public async Task ExecuteInsertsAsync(CancellationToken cancellationToken) { - if (cancellationToken.IsCancellationRequested) + cancellationToken.ThrowIfCancellationRequested(); + try { - return Task.FromCanceled(cancellationToken); + await (ExecuteActionsAsync(insertions, cancellationToken)).ConfigureAwait(false); + } + finally + { + await (PreInvalidateCachesAsync(cancellationToken)).ConfigureAwait(false); } - return ExecuteActionsAsync(insertions, cancellationToken); } /// @@ -94,15 +114,22 @@ public Task ExecuteInsertsAsync(CancellationToken cancellationToken) public async Task ExecuteActionsAsync(CancellationToken cancellationToken) { cancellationToken.ThrowIfCancellationRequested(); - await (ExecuteActionsAsync(insertions, cancellationToken)).ConfigureAwait(false); - await (ExecuteActionsAsync(updates, cancellationToken)).ConfigureAwait(false); - await (ExecuteActionsAsync(collectionRemovals, cancellationToken)).ConfigureAwait(false); - await (ExecuteActionsAsync(collectionUpdates, cancellationToken)).ConfigureAwait(false); - await (ExecuteActionsAsync(collectionCreations, cancellationToken)).ConfigureAwait(false); - await (ExecuteActionsAsync(deletions, cancellationToken)).ConfigureAwait(false); + try + { + await (ExecuteActionsAsync(insertions, cancellationToken)).ConfigureAwait(false); + await (ExecuteActionsAsync(updates, cancellationToken)).ConfigureAwait(false); + await (ExecuteActionsAsync(collectionRemovals, cancellationToken)).ConfigureAwait(false); + await (ExecuteActionsAsync(collectionUpdates, cancellationToken)).ConfigureAwait(false); + await (ExecuteActionsAsync(collectionCreations, cancellationToken)).ConfigureAwait(false); + await (ExecuteActionsAsync(deletions, cancellationToken)).ConfigureAwait(false); + } + finally + { + await (PreInvalidateCachesAsync(cancellationToken)).ConfigureAwait(false); + } } - private async Task PrepareActionsAsync(IList queue, CancellationToken cancellationToken) + private static async Task PrepareActionsAsync(IList queue, CancellationToken cancellationToken) { cancellationToken.ThrowIfCancellationRequested(); foreach (IExecutable executable in queue) @@ -120,7 +147,7 @@ public async Task PrepareActionsAsync(CancellationToken cancellationToken) await (PrepareActionsAsync(collectionUpdates, cancellationToken)).ConfigureAwait(false); await (PrepareActionsAsync(collectionCreations, cancellationToken)).ConfigureAwait(false); } - + /// /// Performs cleanup of any held cache softlocks. /// @@ -132,41 +159,27 @@ public Task AfterTransactionCompletionAsync(bool success, CancellationToken canc { return Task.FromCanceled(cancellationToken); } - return afterTransactionProcesses.AfterTransactionCompletionAsync(success, cancellationToken); + try + { + afterTransactionProcesses.AfterTransactionCompletion(success); + + return InvalidateCachesAsync(cancellationToken); + } + catch (Exception ex) + { + return Task.FromException(ex); + } } - private partial class AfterTransactionCompletionProcessQueue + + private async Task InvalidateCachesAsync(CancellationToken cancellationToken) { - - public async Task AfterTransactionCompletionAsync(bool success, CancellationToken cancellationToken) + cancellationToken.ThrowIfCancellationRequested(); + if (session.Factory.Settings.IsQueryCacheEnabled) { - cancellationToken.ThrowIfCancellationRequested(); - int size = processes.Count; - - for (int i = 0; i < size; i++) - { - try - { - AfterTransactionCompletionProcessDelegate process = processes[i]; - process(success); - } - catch (CacheException e) - { - log.Error(e, "could not release a cache lock"); - // continue loop - } - catch (Exception e) - { - throw new AssertionFailure("Unable to perform AfterTransactionCompletion callback", e); - } - } - processes.Clear(); - - if (session.Factory.Settings.IsQueryCacheEnabled) - { - await (session.Factory.UpdateTimestampsCache.InvalidateAsync(querySpacesToInvalidate.ToArray(), cancellationToken)).ConfigureAwait(false); - } - querySpacesToInvalidate.Clear(); + await (session.Factory.UpdateTimestampsCache.InvalidateAsync(executedSpaces, cancellationToken)).ConfigureAwait(false); } + + executedSpaces.Clear(); } } } diff --git a/src/NHibernate/Async/Engine/Query/NativeSQLQueryPlan.cs b/src/NHibernate/Async/Engine/Query/NativeSQLQueryPlan.cs index 690fdc8af01..8e0aa8630a6 100644 --- a/src/NHibernate/Async/Engine/Query/NativeSQLQueryPlan.cs +++ b/src/NHibernate/Async/Engine/Query/NativeSQLQueryPlan.cs @@ -43,7 +43,7 @@ private async Task CoordinateSharedCacheCleanupAsync(ISessionImplementor session if (session.IsEventSource) { - await (((IEventSource)session).ActionQueue.AddActionAsync(action, cancellationToken)).ConfigureAwait(false); + ((IEventSource)session).ActionQueue.AddAction(action); } } diff --git a/src/NHibernate/Async/Hql/Ast/ANTLR/Exec/AbstractStatementExecutor.cs b/src/NHibernate/Async/Hql/Ast/ANTLR/Exec/AbstractStatementExecutor.cs index 1016b79807b..b52b21c19de 100644 --- a/src/NHibernate/Async/Hql/Ast/ANTLR/Exec/AbstractStatementExecutor.cs +++ b/src/NHibernate/Async/Hql/Ast/ANTLR/Exec/AbstractStatementExecutor.cs @@ -43,7 +43,7 @@ protected virtual async Task CoordinateSharedCacheCleanupAsync(ISessionImplement if (session.IsEventSource) { - await (((IEventSource)session).ActionQueue.AddActionAsync(action, cancellationToken)).ConfigureAwait(false); + ((IEventSource)session).ActionQueue.AddAction(action); } } diff --git a/src/NHibernate/Cache/UpdateTimestampsCache.cs b/src/NHibernate/Cache/UpdateTimestampsCache.cs index 5af4365299c..450ea3ce469 100644 --- a/src/NHibernate/Cache/UpdateTimestampsCache.cs +++ b/src/NHibernate/Cache/UpdateTimestampsCache.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.Linq; using System.Runtime.CompilerServices; using NHibernate.Cfg; @@ -20,7 +21,7 @@ public partial class UpdateTimestampsCache private readonly string regionName = typeof(UpdateTimestampsCache).Name; - public void Clear() + public virtual void Clear() { updateTimestamps.Clear(); } @@ -33,34 +34,50 @@ public UpdateTimestampsCache(Settings settings, IDictionary prop updateTimestamps = settings.CacheProvider.BuildCache(regionName, props); } - [MethodImpl(MethodImplOptions.Synchronized)] + //Since v5.1 + [Obsolete("Please use PreInvalidate(IReadOnlyCollection) instead.")] public void PreInvalidate(object[] spaces) + { + //Only for backwards compatibility. + PreInvalidate(spaces.OfType().ToList()); + } + + [MethodImpl(MethodImplOptions.Synchronized)] + public virtual void PreInvalidate(IReadOnlyCollection spaces) { //TODO: to handle concurrent writes correctly, this should return a Lock to the client long ts = updateTimestamps.NextTimestamp() + updateTimestamps.Timeout; - for (int i = 0; i < spaces.Length; i++) + foreach (var space in spaces) { - updateTimestamps.Put(spaces[i], ts); + updateTimestamps.Put(space, ts); } + //TODO: return new Lock(ts); } - /// - [MethodImpl(MethodImplOptions.Synchronized)] + //Since v5.1 + [Obsolete("Please use PreInvalidate(IReadOnlyCollection) instead.")] public void Invalidate(object[] spaces) + { + //Only for backwards compatibility. + Invalidate(spaces.OfType().ToList()); + } + + [MethodImpl(MethodImplOptions.Synchronized)] + public virtual void Invalidate(IReadOnlyCollection spaces) { //TODO: to handle concurrent writes correctly, the client should pass in a Lock long ts = updateTimestamps.NextTimestamp(); //TODO: if lock.getTimestamp().equals(ts) - for (int i = 0; i < spaces.Length; i++) + foreach (var space in spaces) { - log.Debug("Invalidating space [{0}]", spaces[i]); - updateTimestamps.Put(spaces[i], ts); + log.Debug("Invalidating space [{0}]", space); + updateTimestamps.Put(space, ts); } } [MethodImpl(MethodImplOptions.Synchronized)] - public bool IsUpToDate(ISet spaces, long timestamp /* H2.1 has Long here */) + public virtual bool IsUpToDate(ISet spaces, long timestamp /* H2.1 has Long here */) { foreach (string space in spaces) { @@ -95,7 +112,7 @@ public bool IsUpToDate(ISet spaces, long timestamp /* H2.1 has Long here return true; } - public void Destroy() + public virtual void Destroy() { try { diff --git a/src/NHibernate/Engine/ActionQueue.cs b/src/NHibernate/Engine/ActionQueue.cs index 24bce5c7dda..d4d8344000a 100644 --- a/src/NHibernate/Engine/ActionQueue.cs +++ b/src/NHibernate/Engine/ActionQueue.cs @@ -3,7 +3,8 @@ using System.Collections.Generic; using System.Linq; using System.Text; - +using System.Threading; +using System.Threading.Tasks; using NHibernate.Action; using NHibernate.Cache; using NHibernate.Type; @@ -42,6 +43,7 @@ public partial class ActionQueue private readonly AfterTransactionCompletionProcessQueue afterTransactionProcesses; private readonly BeforeTransactionCompletionProcessQueue beforeTransactionProcesses; + private readonly HashSet executedSpaces; public ActionQueue(ISessionImplementor session) { @@ -54,8 +56,10 @@ public ActionQueue(ISessionImplementor session) collectionUpdates = new List(InitQueueListSize); collectionRemovals = new List(InitQueueListSize); - afterTransactionProcesses = new AfterTransactionCompletionProcessQueue(session); - beforeTransactionProcesses = new BeforeTransactionCompletionProcessQueue(session); + afterTransactionProcesses = new AfterTransactionCompletionProcessQueue(); + beforeTransactionProcesses = new BeforeTransactionCompletionProcessQueue(); + + executedSpaces = new HashSet(); } public virtual void Clear() @@ -108,7 +112,26 @@ public void AddAction(BulkOperationCleanupAction cleanupAction) { RegisterCleanupActions(cleanupAction); } - + + //Since v5.1 + [Obsolete("This method is no longer executed asynchronously and will be removed in a next major version.")] + public Task AddActionAsync(BulkOperationCleanupAction cleanupAction, CancellationToken cancellationToken=default(CancellationToken)) + { + if (cancellationToken.IsCancellationRequested) + { + return Task.FromCanceled(cancellationToken); + } + try + { + AddAction(cleanupAction); + return Task.CompletedTask; + } + catch (Exception e) + { + return Task.FromException(e); + } + } + public void RegisterProcess(AfterTransactionCompletionProcessDelegate process) { afterTransactionProcesses.Register(process); @@ -125,14 +148,36 @@ private void ExecuteActions(IList list) // It will then fail here due to list being modified. (Some previous code was dodging the // trouble with a for loop which was not failing provided the list was not getting smaller. // But then it was clearing it without having executed added actions (if any), ...) + foreach (IExecutable executable in list) - Execute(executable); - + { + InnerExecute(executable); + } list.Clear(); session.Batcher.ExecuteBatch(); } + private void PreInvalidateCaches() + { + if (session.Factory.Settings.IsQueryCacheEnabled) + { + session.Factory.UpdateTimestampsCache.PreInvalidate(executedSpaces); + } + } + public void Execute(IExecutable executable) + { + try + { + InnerExecute(executable); + } + finally + { + PreInvalidateCaches(); + } + } + + private void InnerExecute(IExecutable executable) { try { @@ -140,19 +185,17 @@ public void Execute(IExecutable executable) } finally { + if (executable.PropertySpaces != null) + { + executedSpaces.UnionWith(executable.PropertySpaces); + } RegisterCleanupActions(executable); } } - + private void RegisterCleanupActions(IExecutable executable) { beforeTransactionProcesses.Register(executable.BeforeTransactionCompletionProcess); - if (session.Factory.Settings.IsQueryCacheEnabled) - { - string[] spaces = executable.PropertySpaces; - afterTransactionProcesses.AddSpacesToInvalidate(spaces); - session.Factory.UpdateTimestampsCache.PreInvalidate(spaces); - } afterTransactionProcesses.Register(executable.AfterTransactionCompletionProcess); } @@ -161,7 +204,14 @@ private void RegisterCleanupActions(IExecutable executable) /// public void ExecuteInserts() { - ExecuteActions(insertions); + try + { + ExecuteActions(insertions); + } + finally + { + PreInvalidateCaches(); + } } /// @@ -169,15 +219,22 @@ public void ExecuteInserts() /// public void ExecuteActions() { - ExecuteActions(insertions); - ExecuteActions(updates); - ExecuteActions(collectionRemovals); - ExecuteActions(collectionUpdates); - ExecuteActions(collectionCreations); - ExecuteActions(deletions); + try + { + ExecuteActions(insertions); + ExecuteActions(updates); + ExecuteActions(collectionRemovals); + ExecuteActions(collectionUpdates); + ExecuteActions(collectionCreations); + ExecuteActions(deletions); + } + finally + { + PreInvalidateCaches(); + } } - private void PrepareActions(IList queue) + private static void PrepareActions(IList queue) { foreach (IExecutable executable in queue) executable.BeforeExecutions(); @@ -200,7 +257,7 @@ public void BeforeTransactionCompletion() { beforeTransactionProcesses.BeforeTransactionCompletion(); } - + /// /// Performs cleanup of any held cache softlocks. /// @@ -208,8 +265,20 @@ public void BeforeTransactionCompletion() public void AfterTransactionCompletion(bool success) { afterTransactionProcesses.AfterTransactionCompletion(success); + + InvalidateCaches(); } - + + private void InvalidateCaches() + { + if (session.Factory.Settings.IsQueryCacheEnabled) + { + session.Factory.UpdateTimestampsCache.Invalidate(executedSpaces); + } + + executedSpaces.Clear(); + } + /// /// Check whether the given tables/query-spaces are to be executed against /// given the currently queued actions. @@ -319,7 +388,7 @@ public void SortActions() private void SortInsertActions() { new InsertActionSorter(this).Sort(); - } + } public IList CloneDeletions() { @@ -385,19 +454,13 @@ public override string ToString() [Serializable] private class BeforeTransactionCompletionProcessQueue { - private ISessionImplementor session; - private IList processes = new List(); + private List processes = new List(); public bool HasActions { get { return processes.Count > 0; } } - public BeforeTransactionCompletionProcessQueue(ISessionImplementor session) - { - this.session = session; - } - public void Register(BeforeTransactionCompletionProcessDelegate process) { if (process == null) @@ -417,9 +480,9 @@ public void BeforeTransactionCompletion() BeforeTransactionCompletionProcessDelegate process = processes[i]; process(); } - catch (HibernateException e) + catch (HibernateException) { - throw e; + throw; } catch (Exception e) { @@ -431,39 +494,15 @@ public void BeforeTransactionCompletion() } [Serializable] - private partial class AfterTransactionCompletionProcessQueue + private class AfterTransactionCompletionProcessQueue { - private ISessionImplementor session; - private HashSet querySpacesToInvalidate = new HashSet(); - private IList processes = new List(InitQueueListSize * 3); + private List processes = new List(InitQueueListSize * 3); public bool HasActions { get { return processes.Count > 0; } } - - public AfterTransactionCompletionProcessQueue(ISessionImplementor session) - { - this.session = session; - } - - public void AddSpacesToInvalidate(string[] spaces) - { - if (spaces == null) - { - return; - } - for (int i = 0, max = spaces.Length; i < max; i++) - { - this.AddSpaceToInvalidate(spaces[i]); - } - } - - public void AddSpaceToInvalidate(string space) - { - querySpacesToInvalidate.Add(space); - } - + public void Register(AfterTransactionCompletionProcessDelegate process) { if (process == null) @@ -495,12 +534,6 @@ public void AfterTransactionCompletion(bool success) } } processes.Clear(); - - if (session.Factory.Settings.IsQueryCacheEnabled) - { - session.Factory.UpdateTimestampsCache.Invalidate(querySpacesToInvalidate.ToArray()); - } - querySpacesToInvalidate.Clear(); } }