diff --git a/modules/core/src/main/java/org/apache/ignite/internal/processors/cache/GridCacheMapEntry.java b/modules/core/src/main/java/org/apache/ignite/internal/processors/cache/GridCacheMapEntry.java
index 08ac07ba287b1..69f9fc98b07d9 100644
--- a/modules/core/src/main/java/org/apache/ignite/internal/processors/cache/GridCacheMapEntry.java
+++ b/modules/core/src/main/java/org/apache/ignite/internal/processors/cache/GridCacheMapEntry.java
@@ -6928,18 +6928,22 @@ protected void updatePlatformCache(@Nullable CacheObject val, @Nullable Affinity
if (!hasPlatformCache())
return;
- PlatformProcessor proc = this.cctx.kernalContext().platform();
+ PlatformProcessor proc = cctx.kernalContext().platform();
if (!proc.hasContext() || !proc.context().isPlatformCacheSupported())
return;
try {
- CacheObjectContext ctx = this.cctx.cacheObjectContext();
+ CacheObjectContext ctx = cctx.cacheObjectContext();
+ byte[] keyBytes = key.valueBytes(ctx);
// val is null when entry is removed.
- byte[] keyBytes = this.key.valueBytes(ctx);
- byte[] valBytes = val == null ? null : val.valueBytes(ctx);
+ // valid(ver) is false when near cache entry is out of sync.
+ boolean valid = val != null && ver != null && valid(ver);
- proc.context().updatePlatformCache(this.cctx.cacheId(), keyBytes, valBytes, partition(), ver);
+ // null valBytes means that entry should be removed from platform cache.
+ byte[] valBytes = valid ? val.valueBytes(ctx) : null;
+
+ proc.context().updatePlatformCache(cctx.cacheId(), keyBytes, valBytes, partition(), ver);
} catch (Throwable e) {
U.error(log, "Failed to update Platform Cache: " + e);
}
diff --git a/modules/platforms/dotnet/Apache.Ignite.Core.Tests/Cache/Platform/PlatformCachePartialClientConnectionTest.cs b/modules/platforms/dotnet/Apache.Ignite.Core.Tests/Cache/Platform/PlatformCachePartialClientConnectionTest.cs
new file mode 100644
index 0000000000000..e553c188b4da3
--- /dev/null
+++ b/modules/platforms/dotnet/Apache.Ignite.Core.Tests/Cache/Platform/PlatformCachePartialClientConnectionTest.cs
@@ -0,0 +1,142 @@
+/*
+ * Copyright 2023 GridGain Systems, Inc. and Contributors.
+ *
+ * Licensed under the GridGain Community Edition License (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://www.gridgain.com/products/software/community-edition/gridgain-community-edition-license
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+namespace Apache.Ignite.Core.Tests.Cache.Platform
+{
+ using System;
+ using System.Collections.Generic;
+ using System.Linq;
+ using Apache.Ignite.Core.Cache.Configuration;
+ using Apache.Ignite.Core.Cluster;
+ using Apache.Ignite.Core.Discovery.Tcp;
+ using Apache.Ignite.Core.Discovery.Tcp.Static;
+ using NUnit.Framework;
+
+ ///
+ /// Tests platform cache with thick clients connected to different parts of the cluster.
+ ///
+ public class PlatformCachePartialClientConnectionTest
+ {
+ private const string CacheName = "cache1";
+ private const string AttrMacs = "org.apache.ignite.macs";
+
+ private const int Key = 1;
+ private const int InitialValue = 0;
+
+ [TearDown]
+ public void TearDown()
+ {
+ Ignition.StopAll(true);
+ }
+
+ ///
+ /// Tests that thick client connected only to backup node 1 updates a value,
+ /// and another thick client connected to a different backup node sees the update in Platform Cache.
+ ///
+ [Test]
+ public static void TestPutFromOneClientGetFromAnother()
+ {
+ // Start 3 servers.
+ var servers = Enumerable.Range(0, 3)
+ .Select(i => Ignition.Start(GetConfiguration(false, i, 0)))
+ .ToArray();
+
+ CreateCache(servers[0]);
+
+ // Start 2 thick clients, connect to different backup nodes only (not entire cluster).
+ var primaryAndBackups = servers[0].GetAffinity(CacheName).MapKeyToPrimaryAndBackups(Key);
+ var backupServer1Mac = GetMac(primaryAndBackups[1]);
+ var backupServer2Mac = GetMac(primaryAndBackups[2]);
+
+ var client1 = Ignition.Start(GetConfiguration(true, backupServer1Mac, backupServer1Mac));
+ var client2 = Ignition.Start(GetConfiguration(true, backupServer2Mac, backupServer2Mac));
+
+ // Check initial value.
+ var client1Cache = client1.GetOrCreateNearCache(CacheName, new NearCacheConfiguration());
+ var client2Cache = client2.GetOrCreateNearCache(CacheName, new NearCacheConfiguration());
+
+ var client1Value = client1Cache.Get(Key);
+ var client2Value = client2Cache.Get(Key);
+
+ Assert.AreEqual(InitialValue, client1Value);
+ Assert.AreEqual(InitialValue, client2Value);
+
+ // Update value from client 1.
+ const int newValue = 1;
+ client1Cache.Put(Key, newValue);
+
+ // Read value from client 1 and 2.
+ client1Value = client1Cache.Get(Key);
+ client2Value = client2Cache.Get(Key);
+
+ Assert.AreEqual(newValue, client1Value);
+ Assert.AreEqual(newValue, client2Value);
+ }
+
+ private static int GetMac(IClusterNode node) => Convert.ToInt32(node.Attributes[AttrMacs]);
+
+ private static IgniteConfiguration GetConfiguration(bool client, int localMac, int remoteMac)
+ {
+ var name = (client ? "client" : "server") + localMac;
+ var remotePort = 48500 + remoteMac;
+
+ var discoverySpi = new TcpDiscoverySpi
+ {
+ IpFinder = new TcpDiscoveryStaticIpFinder
+ {
+ Endpoints = new List { $"127.0.0.1:{remotePort}" }
+ }
+ };
+
+ if (!client)
+ {
+ discoverySpi.LocalPort = 48500 + localMac;
+ discoverySpi.LocalPortRange = 1;
+ }
+
+ var igniteConfig = new IgniteConfiguration(TestUtils.GetTestConfiguration())
+ {
+ ClientMode = client,
+ IgniteInstanceName = name,
+ // ConsistentId = name,
+ UserAttributes = new Dictionary
+ {
+ [$"override.{AttrMacs}"] = localMac.ToString()
+ },
+ DiscoverySpi = discoverySpi
+ };
+
+ return igniteConfig;
+ }
+
+ private static void CreateCache(IIgnite ignite)
+ {
+ var cacheConfig = new CacheConfiguration(CacheName)
+ {
+ CacheMode = CacheMode.Replicated,
+ ReadFromBackup = true, // Does not reproduce when false.
+ PlatformCacheConfiguration = new PlatformCacheConfiguration
+ {
+ KeyTypeName = typeof(int).FullName,
+ ValueTypeName = typeof(int).FullName
+ }
+ };
+
+ var cache = ignite.GetOrCreateCache(cacheConfig);
+ cache.Put(Key, InitialValue);
+ }
+ }
+}
diff --git a/modules/platforms/dotnet/Apache.Ignite.Core.Tests/Cache/Platform/PlatformCacheTest.cs b/modules/platforms/dotnet/Apache.Ignite.Core.Tests/Cache/Platform/PlatformCacheTest.cs
index 2543b0dc2713a..366e28f75c6ab 100644
--- a/modules/platforms/dotnet/Apache.Ignite.Core.Tests/Cache/Platform/PlatformCacheTest.cs
+++ b/modules/platforms/dotnet/Apache.Ignite.Core.Tests/Cache/Platform/PlatformCacheTest.cs
@@ -1228,13 +1228,14 @@ public void TestPlatformCachingWithBackups()
/// Tests that Replicated cache puts all entries on all nodes to platform cache.
///
[Test]
- public void TestPlatformCachingReplicated()
+ public void TestPlatformCachingReplicated([Values(false, true)] bool readFromBackup)
{
var cfg = new CacheConfiguration(TestUtils.TestName)
{
CacheMode = CacheMode.Replicated,
PlatformCacheConfiguration = new PlatformCacheConfiguration(),
- WriteSynchronizationMode = CacheWriteSynchronizationMode.FullSync
+ WriteSynchronizationMode = CacheWriteSynchronizationMode.FullSync,
+ ReadFromBackup = readFromBackup
};
var cache1 = _grid.CreateCache(cfg);