From 212e9b76b99e832cb2511e7bb0ea6608ade7fa44 Mon Sep 17 00:00:00 2001 From: Alexey Kulakov Date: Wed, 28 Dec 2022 18:07:18 +0500 Subject: [PATCH 1/4] Add test for stale query cache --- .../StaleQueryCacheForReAddedNodes.cs | 208 ++++++++++++++++++ 1 file changed, 208 insertions(+) create mode 100644 Orm/Xtensive.Orm.Tests/Storage/Multinode/StaleQueryCacheForReAddedNodes.cs diff --git a/Orm/Xtensive.Orm.Tests/Storage/Multinode/StaleQueryCacheForReAddedNodes.cs b/Orm/Xtensive.Orm.Tests/Storage/Multinode/StaleQueryCacheForReAddedNodes.cs new file mode 100644 index 0000000000..d5380775aa --- /dev/null +++ b/Orm/Xtensive.Orm.Tests/Storage/Multinode/StaleQueryCacheForReAddedNodes.cs @@ -0,0 +1,208 @@ +// Copyright (C) 2022 Xtensive LLC. +// This code is distributed under MIT license terms. +// See the License.txt file in the project root for more information. + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using NUnit.Framework; +using Xtensive.Orm.Configuration; +using Xtensive.Orm.Tests.Storage.Multinode.QueryCachingTestModel; + +namespace Xtensive.Orm.Tests.Storage.Multinode +{ + public sealed class StaleQueryCacheForReAddedNodes : MultinodeTest + { + private const string DefaultSchema = WellKnownSchemas.Schema1; + private const string Schema1 = WellKnownSchemas.Schema2; + private const string Schema2 = WellKnownSchemas.Schema3; + + private readonly object SimpleQueryKey = new object(); + + protected override void CheckRequirements() => + Require.AllFeaturesSupported(Orm.Providers.ProviderFeatures.Multischema); + + protected override DomainConfiguration BuildConfiguration() + { + CustomUpgradeHandler.TypeIdPerNode.Add(TestNodeId2, 100); + CustomUpgradeHandler.TypeIdPerNode.Add(TestNodeId3, 100); + + var configuration = base.BuildConfiguration(); + configuration.Types.Register(typeof(BaseTestEntity).Assembly, typeof(BaseTestEntity).Namespace); + configuration.UpgradeMode = DomainUpgradeMode.Recreate; + configuration.DefaultSchema = DefaultSchema; + return configuration; + } + + protected override void PopulateNodes() + { + CustomUpgradeHandler.CurrentNodeId = TestNodeId2; + var nodeConfiguration = new NodeConfiguration(TestNodeId2); + nodeConfiguration.SchemaMapping.Add(DefaultSchema, Schema1); + nodeConfiguration.UpgradeMode = DomainUpgradeMode.Recreate; + _ = Domain.StorageNodeManager.AddNode(nodeConfiguration); + + CustomUpgradeHandler.CurrentNodeId = TestNodeId3; + nodeConfiguration = new NodeConfiguration(TestNodeId3); + nodeConfiguration.SchemaMapping.Add(DefaultSchema, Schema2); + nodeConfiguration.UpgradeMode = DomainUpgradeMode.Recreate; + _ = Domain.StorageNodeManager.AddNode(nodeConfiguration); + } + + protected override void PopulateData() + { + var nodes = new[] { WellKnown.DefaultNodeId, TestNodeId2, TestNodeId3 }; + + foreach (var nodeId in nodes) { + var selectedNode = Domain.StorageNodeManager.GetNode(nodeId); + using (var session = selectedNode.OpenSession()) + using (var tx = session.OpenTransaction()) { + + var nodeIdName = string.IsNullOrEmpty(nodeId) ? "" : nodeId; + + _ = new BaseTestEntity(session) { BaseName = "A", BaseOwnerNodeId = nodeIdName }; + _ = new MiddleTestEntity(session) { + BaseName = "AA", + MiddleName = "AAM", + BaseOwnerNodeId = nodeIdName, + MiddleOwnerNodeId = nodeIdName + }; + _ = new LeafTestEntity(session) { + BaseName = "AAA", + MiddleName = "AAAM", + LeafName = "AAAL", + BaseOwnerNodeId = nodeIdName, + MiddleOwnerNodeId = nodeIdName, + LeafOwnerNodeId = nodeIdName + }; + + _ = new BaseTestEntity(session) { BaseName = "B", BaseOwnerNodeId = nodeIdName }; + _ = new MiddleTestEntity(session) { + BaseName = "BB", + MiddleName = "BBM", + BaseOwnerNodeId = nodeIdName, + MiddleOwnerNodeId = nodeIdName + }; + _ = new LeafTestEntity(session) { + BaseName = "BBB", + MiddleName = "BBBM", + LeafName = "BBBL", + BaseOwnerNodeId = nodeIdName, + MiddleOwnerNodeId = nodeIdName, + LeafOwnerNodeId = nodeIdName + }; + + _ = new BaseTestEntity(session) { BaseName = "C", BaseOwnerNodeId = nodeIdName }; + _ = new MiddleTestEntity(session) { + BaseName = "CC", + MiddleName = "CCM", + BaseOwnerNodeId = nodeIdName, + MiddleOwnerNodeId = nodeIdName + }; + _ = new LeafTestEntity(session) { + BaseName = "CCC", + MiddleName = "CCCM", + LeafName = "CCCL", + BaseOwnerNodeId = nodeIdName, + MiddleOwnerNodeId = nodeIdName, + LeafOwnerNodeId = nodeIdName + }; + + _ = new BaseTestEntity(session) { BaseName = "D", BaseOwnerNodeId = nodeIdName }; + _ = new MiddleTestEntity(session) { + BaseName = "DD", + MiddleName = "DDM", + BaseOwnerNodeId = nodeIdName, + MiddleOwnerNodeId = nodeIdName + }; + _ = new LeafTestEntity(session) { + BaseName = "DDD", + MiddleName = "DDDM", + LeafName = "DDDL", + BaseOwnerNodeId = nodeIdName, + MiddleOwnerNodeId = nodeIdName, + LeafOwnerNodeId = nodeIdName + }; + + // puts one query per each node to the query cache + _ = ExecuteSimpleQueryCaching(session); + + tx.Complete(); + } + } + } + + [Test] + public void ReAddNodeTest() + { + var node = Domain.StorageNodeManager.GetNode(TestNodeId2); + var queryCacheSize = Domain.QueryCache.Count; + + using (var session = node.OpenSession()) + using (var tx = session.OpenTransaction()) { + _ = session.Query.Execute(SimpleQueryKey, q => q.All().Where(e => e.BaseName.Contains("B"))).ToList(); + } + Assert.That(Domain.QueryCache.Count, Is.EqualTo(queryCacheSize)); + + _ = Domain.StorageNodeManager.RemoveNode(TestNodeId2); + Assert.That(Domain.QueryCache.Count, Is.EqualTo(queryCacheSize)); + + CustomUpgradeHandler.CurrentNodeId = TestNodeId2; + var nodeConfiguration = new NodeConfiguration(TestNodeId2); + nodeConfiguration.SchemaMapping.Add(DefaultSchema, Schema1); + nodeConfiguration.UpgradeMode = DomainUpgradeMode.Validate; + _ = Domain.StorageNodeManager.AddNode(nodeConfiguration); + + node = Domain.StorageNodeManager.GetNode(TestNodeId2); + + using (var session = node.OpenSession()) + using (var tx = session.OpenTransaction()) { + _ = session.Query.Execute(SimpleQueryKey, q => q.All().Where(e => e.BaseName.Contains("B"))).ToList(); + } + Assert.That(Domain.QueryCache.Count, Is.EqualTo(queryCacheSize)); + + CustomUpgradeHandler.CurrentNodeId = TestNodeId3; + nodeConfiguration = new NodeConfiguration(TestNodeId3); + nodeConfiguration.SchemaMapping.Add(DefaultSchema, Schema2); + nodeConfiguration.UpgradeMode = DomainUpgradeMode.Recreate; + _ = Domain.StorageNodeManager.AddNode(nodeConfiguration); + } + + [Test] + public void ReAddNodeWithAnotherSchemaMappingNoCacheCleanTest() + { + var node = Domain.StorageNodeManager.GetNode(TestNodeId2); + var queryCacheSize = Domain.QueryCache.Count; + + using (var session = node.OpenSession()) + using (var tx = session.OpenTransaction()) { + _ = session.Query.Execute(SimpleQueryKey, q => q.All().Where(e => e.BaseName.Contains("B"))).ToList(); + } + Assert.That(Domain.QueryCache.Count, Is.EqualTo(queryCacheSize)); + + _ = Domain.StorageNodeManager.RemoveNode(TestNodeId2); + Assert.That(Domain.QueryCache.Count, Is.EqualTo(queryCacheSize)); + + CustomUpgradeHandler.CurrentNodeId = TestNodeId2; + var nodeConfiguration = new NodeConfiguration(TestNodeId2); + nodeConfiguration.SchemaMapping.Add(DefaultSchema, Schema2);// uses schema of TestNodeId3 + nodeConfiguration.UpgradeMode = DomainUpgradeMode.Validate; + _ = Domain.StorageNodeManager.AddNode(nodeConfiguration); + + node = Domain.StorageNodeManager.GetNode(TestNodeId2); + + using (var session = node.OpenSession()) + using (var tx = session.OpenTransaction()) { + var results = session.Query.Execute(SimpleQueryKey, q => q.All().Where(e => e.BaseName.Contains("B"))).ToList(); + foreach(var item in results) { + Assert.That(item.BaseOwnerNodeId, Is.EqualTo(node.Id)); // gets result from old schema + } + } + Assert.That(Domain.QueryCache.Count, Is.EqualTo(queryCacheSize)); + } + + private List ExecuteSimpleQueryCaching(Session session) => + session.Query.Execute(SimpleQueryKey, q => q.All().Where(e => e.BaseName.Contains("B"))).ToList(); + } +} From 0e4b6ac061a2fbe56b6960005d4fff5f27d53715 Mon Sep 17 00:00:00 2001 From: Alexey Kulakov Date: Wed, 28 Dec 2022 18:08:05 +0500 Subject: [PATCH 2/4] Query cache cleaning on node removal if required --- .../Caching/LruCache{TKey, TItem}.cs | 6 ++++++ Orm/Xtensive.Orm/Orm/StorageNodeManager.cs | 18 ++++++++++++++++-- 2 files changed, 22 insertions(+), 2 deletions(-) diff --git a/Orm/Xtensive.Orm/Caching/LruCache{TKey, TItem}.cs b/Orm/Xtensive.Orm/Caching/LruCache{TKey, TItem}.cs index b25a69a9bf..449f8ab64d 100644 --- a/Orm/Xtensive.Orm/Caching/LruCache{TKey, TItem}.cs +++ b/Orm/Xtensive.Orm/Caching/LruCache{TKey, TItem}.cs @@ -239,6 +239,12 @@ protected virtual void Cleared() { } #endregion + internal IEnumerable GetKeysInternal() + { + foreach (KeyValuePair cachedItem in deque) + yield return cachedItem.Key; + } + // Constructors diff --git a/Orm/Xtensive.Orm/Orm/StorageNodeManager.cs b/Orm/Xtensive.Orm/Orm/StorageNodeManager.cs index 7966637612..b02f79ec81 100644 --- a/Orm/Xtensive.Orm/Orm/StorageNodeManager.cs +++ b/Orm/Xtensive.Orm/Orm/StorageNodeManager.cs @@ -5,7 +5,9 @@ // Created: 2014.03.13 using System; +using System.Linq; using JetBrains.Annotations; +using Xtensive.Core; using Xtensive.Orm.Configuration; using Xtensive.Orm.Providers; using Xtensive.Orm.Upgrade; @@ -34,12 +36,24 @@ public bool AddNode([NotNull] NodeConfiguration configuration) /// Removes node with specified . /// /// Node identifier. + /// + /// if then cached queries dedicated to the removing node will be removed from cache as well. By default . + /// /// True if node was removed, otherwise false. - public bool RemoveNode([NotNull] string nodeId) + public bool RemoveNode([NotNull] string nodeId, bool clearQueryCache = false) { - return handlers.StorageNodeRegistry.Remove(nodeId); + var removeResult = handlers.StorageNodeRegistry.Remove(nodeId); + + if (removeResult && clearQueryCache) { + var queryCache = (Caching.LruCache>) handlers.Domain.QueryCache; + foreach (var key in queryCache.GetKeysInternal().Where(k => k is Pair p && p.Second == nodeId).ToChainedBuffer()) { + queryCache.RemoveKey(key, true); + } + } + return removeResult; } + /// /// Gets node with the specified /// From 384ed89e9655beb9f8b473f3d7c69ef2aca13366 Mon Sep 17 00:00:00 2001 From: Alexey Kulakov Date: Wed, 28 Dec 2022 18:10:08 +0500 Subject: [PATCH 3/4] Add test case for cache cleaning on node removal --- .../StaleQueryCacheForReAddedNodes.cs | 33 +++++++++++++++++++ 1 file changed, 33 insertions(+) diff --git a/Orm/Xtensive.Orm.Tests/Storage/Multinode/StaleQueryCacheForReAddedNodes.cs b/Orm/Xtensive.Orm.Tests/Storage/Multinode/StaleQueryCacheForReAddedNodes.cs index d5380775aa..6294de0380 100644 --- a/Orm/Xtensive.Orm.Tests/Storage/Multinode/StaleQueryCacheForReAddedNodes.cs +++ b/Orm/Xtensive.Orm.Tests/Storage/Multinode/StaleQueryCacheForReAddedNodes.cs @@ -202,6 +202,39 @@ public void ReAddNodeWithAnotherSchemaMappingNoCacheCleanTest() Assert.That(Domain.QueryCache.Count, Is.EqualTo(queryCacheSize)); } + [Test] + public void ReAddNodeWithAnotherSchemaMappingWithCacheCleanTest() + { + var node = Domain.StorageNodeManager.GetNode(TestNodeId2); + var queryCacheSize = Domain.QueryCache.Count; + + using (var session = node.OpenSession()) + using (var tx = session.OpenTransaction()) { + _ = session.Query.Execute(SimpleQueryKey, q => q.All().Where(e => e.BaseName.Contains("B"))).ToList(); + } + Assert.That(Domain.QueryCache.Count, Is.EqualTo(queryCacheSize)); + + _ = Domain.StorageNodeManager.RemoveNode(TestNodeId2, true); + Assert.That(Domain.QueryCache.Count, Is.LessThan(queryCacheSize)); + + CustomUpgradeHandler.CurrentNodeId = TestNodeId2; + var nodeConfiguration = new NodeConfiguration(TestNodeId2); + nodeConfiguration.SchemaMapping.Add(DefaultSchema, Schema2);// uses schema of TestNodeId3 + nodeConfiguration.UpgradeMode = DomainUpgradeMode.Validate; + _ = Domain.StorageNodeManager.AddNode(nodeConfiguration); + + node = Domain.StorageNodeManager.GetNode(TestNodeId2); + + using (var session = node.OpenSession()) + using (var tx = session.OpenTransaction()) { + var results = session.Query.Execute(SimpleQueryKey, q => q.All().Where(e => e.BaseName.Contains("B"))).ToList(); + foreach (var item in results) { + Assert.That(item.BaseOwnerNodeId, Is.Not.EqualTo(node.Id)); // gets result from correct schema but data was added by TestNodeId3 + } + } + Assert.That(Domain.QueryCache.Count, Is.EqualTo(queryCacheSize)); + } + private List ExecuteSimpleQueryCaching(Session session) => session.Query.Execute(SimpleQueryKey, q => q.All().Where(e => e.BaseName.Contains("B"))).ToList(); } From c3632e7b69055463375721cc83bc5f788b3e1e58 Mon Sep 17 00:00:00 2001 From: Alexey Kulakov Date: Tue, 10 Jan 2023 20:04:45 +0500 Subject: [PATCH 4/4] Improve changelog --- ChangeLog/6.0.11_dev.txt | 1 + 1 file changed, 1 insertion(+) diff --git a/ChangeLog/6.0.11_dev.txt b/ChangeLog/6.0.11_dev.txt index ec63f531ee..405cc64e48 100644 --- a/ChangeLog/6.0.11_dev.txt +++ b/ChangeLog/6.0.11_dev.txt @@ -1,2 +1,3 @@ +[main] Added an option to remove cached queries of removing storage node along with the node, default behavior remains the same [postgresql] Dedicated exception when an extracting schema doesn't exist or it belongs to another user [weaver] Fixed ctor processing with try...catch where there was broken catch section leaving