This is an automated email from the ASF dual-hosted git repository.

ptupitsyn pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/ignite-3.git


The following commit(s) were added to refs/heads/main by this push:
     new a385120aca IGNITE-20900 .NET: Handle replica assignment timestamp 
(#2900)
a385120aca is described below

commit a385120aca9b4c3062da8feea5c1ae37fcc8cf24
Author: Pavel Tupitsyn <[email protected]>
AuthorDate: Wed Nov 29 14:00:12 2023 +0200

    IGNITE-20900 .NET: Handle replica assignment timestamp (#2900)
---
 .../Apache.Ignite.Tests/ClientSocketTests.cs       |  2 +-
 .../dotnet/Apache.Ignite.Tests/FakeServer.cs       | 13 ++-----
 .../dotnet/Apache.Ignite.Tests/MetricsTests.cs     |  2 +-
 .../Apache.Ignite.Tests/PartitionAwarenessTests.cs | 45 +++++++++++++++++++---
 .../Apache.Ignite/Internal/ClientFailoverSocket.cs | 29 +++++++-------
 .../dotnet/Apache.Ignite/Internal/ClientSocket.cs  |  3 +-
 .../Internal/IClientSocketEventListener.cs         |  4 +-
 .../dotnet/Apache.Ignite/Internal/Table/Table.cs   | 37 ++++++++++--------
 8 files changed, 83 insertions(+), 52 deletions(-)

diff --git a/modules/platforms/dotnet/Apache.Ignite.Tests/ClientSocketTests.cs 
b/modules/platforms/dotnet/Apache.Ignite.Tests/ClientSocketTests.cs
index 64d23585d5..58ed2ec36d 100644
--- a/modules/platforms/dotnet/Apache.Ignite.Tests/ClientSocketTests.cs
+++ b/modules/platforms/dotnet/Apache.Ignite.Tests/ClientSocketTests.cs
@@ -88,7 +88,7 @@ namespace Apache.Ignite.Tests
 
         private class NoOpListener : IClientSocketEventListener
         {
-            public void OnAssignmentChanged(ClientSocket clientSocket)
+            public void OnAssignmentChanged(long timestamp)
             {
                 // No-op.
             }
diff --git a/modules/platforms/dotnet/Apache.Ignite.Tests/FakeServer.cs 
b/modules/platforms/dotnet/Apache.Ignite.Tests/FakeServer.cs
index 3de2bdba55..8dc99ec868 100644
--- a/modules/platforms/dotnet/Apache.Ignite.Tests/FakeServer.cs
+++ b/modules/platforms/dotnet/Apache.Ignite.Tests/FakeServer.cs
@@ -95,7 +95,7 @@ namespace Apache.Ignite.Tests
 
         public string[] PartitionAssignment { get; set; }
 
-        public bool PartitionAssignmentChanged { get; set; }
+        public long PartitionAssignmentTimestamp { get; set; }
 
         public TimeSpan HandshakeDelay { get; set; }
 
@@ -348,15 +348,8 @@ namespace Apache.Ignite.Tests
             writer.Write(0); // Message type.
             writer.Write(requestId);
 
-            if (PartitionAssignmentChanged)
-            {
-                writer.Write((int)ResponseFlags.PartitionAssignmentChanged);
-                writer.Write(DateTime.UtcNow.Ticks);
-            }
-            else
-            {
-                writer.Write(0);
-            }
+            writer.Write((int)ResponseFlags.PartitionAssignmentChanged);
+            writer.Write(PartitionAssignmentTimestamp);
 
             writer.Write(ObservableTimestamp); // Observable timestamp.
 
diff --git a/modules/platforms/dotnet/Apache.Ignite.Tests/MetricsTests.cs 
b/modules/platforms/dotnet/Apache.Ignite.Tests/MetricsTests.cs
index 0eacdd5aa8..f2dd0fffec 100644
--- a/modules/platforms/dotnet/Apache.Ignite.Tests/MetricsTests.cs
+++ b/modules/platforms/dotnet/Apache.Ignite.Tests/MetricsTests.cs
@@ -87,7 +87,7 @@ public class MetricsTests
         await client.Tables.GetTablesAsync();
 
         AssertMetric("bytes-sent", 17);
-        AssertMetric("bytes-received", 73);
+        AssertMetric("bytes-received", 74);
     }
 
     [Test]
diff --git 
a/modules/platforms/dotnet/Apache.Ignite.Tests/PartitionAwarenessTests.cs 
b/modules/platforms/dotnet/Apache.Ignite.Tests/PartitionAwarenessTests.cs
index 4cc45ffde5..980bf7c9c0 100644
--- a/modules/platforms/dotnet/Apache.Ignite.Tests/PartitionAwarenessTests.cs
+++ b/modules/platforms/dotnet/Apache.Ignite.Tests/PartitionAwarenessTests.cs
@@ -54,8 +54,13 @@ public class PartitionAwarenessTests
         _server2 = new FakeServer(nodeName: "srv2");
 
         var assignment = new[] { _server1.Node.Name, _server2.Node.Name };
-        _server1.PartitionAssignment = assignment;
-        _server2.PartitionAssignment = assignment;
+        var assignmentTimestamp = DateTime.UtcNow.AddDays(-1).Ticks; // Old 
assignment.
+
+        foreach (var server in new[] { _server1, _server2 })
+        {
+            server.PartitionAssignment = assignment;
+            server.PartitionAssignmentTimestamp = assignmentTimestamp;
+        }
     }
 
     [TearDown]
@@ -325,6 +330,34 @@ public class PartitionAwarenessTests
             expectedNode);
     }
 
+    [Test]
+    public async Task TestOldAssignmentIsIgnored()
+    {
+        using var client = await GetClient();
+        var recordView = (await 
client.Tables.GetTableAsync(FakeServer.ExistingTableName))!.GetRecordView<int>();
+
+        // Check default assignment.
+        await recordView.UpsertAsync(null, 1);
+        await AssertOpOnNode(() => recordView.UpsertAsync(null, 1), 
ClientOp.TupleUpsert, _server2);
+
+        // One server has old assignment
+        _server1.PartitionAssignment = 
_server1.PartitionAssignment.Reverse().ToArray();
+        _server1.PartitionAssignmentTimestamp -= 1000;
+
+        // Multiple requests to receive timestamp from all servers.
+        for (int i = 0; i < 10; i++)
+        {
+            await client.Tables.GetTablesAsync();
+        }
+
+        // Check that assignment has not changed - update with old timestamp 
was ignored.
+        _server1.ClearOps();
+        _server2.ClearOps();
+
+        await recordView.UpsertAsync(null, 1);
+        await AssertOpOnNode(() => recordView.UpsertAsync(null, 1), 
ClientOp.TupleUpsert, _server2);
+    }
+
     private static async Task AssertOpOnNode(
         Func<Task> action,
         ClientOp op,
@@ -362,16 +395,16 @@ public class PartitionAwarenessTests
         await AssertOpOnNode(() => func(recordView), op, _server2);
 
         // Update assignment.
+        var assignmentTimestamp = DateTime.UtcNow.Ticks;
+
         foreach (var server in new[] { _server1, _server2 })
         {
             server.ClearOps();
             server.PartitionAssignment = 
server.PartitionAssignment.Reverse().ToArray();
-            server.PartitionAssignmentChanged = true;
+            server.PartitionAssignmentTimestamp = assignmentTimestamp;
         }
 
-        // First request on default node receives update flag.
-        // Make two requests because balancing uses round-robin node.
-        await client.Tables.GetTablesAsync();
+        // First request receives update flag.
         await client.Tables.GetTablesAsync();
 
         // Second request loads and uses new assignment.
diff --git 
a/modules/platforms/dotnet/Apache.Ignite/Internal/ClientFailoverSocket.cs 
b/modules/platforms/dotnet/Apache.Ignite/Internal/ClientFailoverSocket.cs
index 4650b2d150..2ee5c4b2e9 100644
--- a/modules/platforms/dotnet/Apache.Ignite/Internal/ClientFailoverSocket.cs
+++ b/modules/platforms/dotnet/Apache.Ignite/Internal/ClientFailoverSocket.cs
@@ -59,15 +59,12 @@ namespace Apache.Ignite.Internal
             Justification = "WaitHandle is not used in SemaphoreSlim, no need 
to dispose.")]
         private readonly SemaphoreSlim _socketLock = new(1);
 
-        /** Last connected socket. Used to track partition assignment updates. 
*/
-        private volatile ClientSocket? _lastConnectedSocket;
-
         /** Disposed flag. */
         private volatile bool _disposed;
 
         /** Local topology assignment version. Instead of using event handlers 
to notify all tables about assignment change,
          * the table will compare its version with channel version to detect 
an update. */
-        private int _assignmentVersion;
+        private long _assignmentTimestamp;
 
         /** Cluster id from the first handshake. */
         private Guid? _clusterId;
@@ -104,9 +101,9 @@ namespace Apache.Ignite.Internal
         public IgniteClientConfiguration Configuration { get; }
 
         /// <summary>
-        /// Gets the partition assignment version.
+        /// Gets the partition assignment timestamp.
         /// </summary>
-        public int PartitionAssignmentVersion => 
Interlocked.CompareExchange(ref _assignmentVersion, -1, -1);
+        public long PartitionAssignmentTimestamp => Interlocked.Read(ref 
_assignmentTimestamp);
 
         /// <summary>
         /// Gets the observable timestamp.
@@ -255,15 +252,20 @@ namespace Apache.Ignite.Internal
         }
 
         /// <inheritdoc/>
-        void IClientSocketEventListener.OnAssignmentChanged(ClientSocket 
clientSocket)
+        void IClientSocketEventListener.OnAssignmentChanged(long timestamp)
         {
-            // NOTE: Multiple channels will send the same update to us, 
resulting in multiple cache invalidations.
-            // This could be solved with a cluster-wide AssignmentVersion, but 
we don't have that.
-            // So we only react to updates from the last known good channel. 
When no user-initiated operations are performed on that
-            // channel, heartbeat messages will trigger updates.
-            if (clientSocket == _lastConnectedSocket)
+            while (true)
             {
-                Interlocked.Increment(ref _assignmentVersion);
+                var oldTimestamp = Interlocked.Read(ref _assignmentTimestamp);
+                if (oldTimestamp >= timestamp)
+                {
+                    return;
+                }
+
+                if (Interlocked.CompareExchange(ref _assignmentTimestamp, 
value: timestamp, comparand: oldTimestamp) == oldTimestamp)
+                {
+                    return;
+                }
             }
         }
 
@@ -491,7 +493,6 @@ namespace Apache.Ignite.Internal
                 endpoint.Socket = socket;
 
                 _endpointsByName[socket.ConnectionContext.ClusterNode.Name] = 
endpoint;
-                _lastConnectedSocket = socket;
 
                 return socket;
             }
diff --git a/modules/platforms/dotnet/Apache.Ignite/Internal/ClientSocket.cs 
b/modules/platforms/dotnet/Apache.Ignite/Internal/ClientSocket.cs
index 9a119c7aa5..2d211b0476 100644
--- a/modules/platforms/dotnet/Apache.Ignite/Internal/ClientSocket.cs
+++ b/modules/platforms/dotnet/Apache.Ignite/Internal/ClientSocket.cs
@@ -726,8 +726,7 @@ namespace Apache.Ignite.Internal
 
                 
_logger.LogPartitionAssignmentChangeNotificationInfo(ConnectionContext.ClusterNode.Address,
 timestamp);
 
-                // TODO IGNITE-20900: Propagate assignment timestamp.
-                _listener.OnAssignmentChanged(this);
+                _listener.OnAssignmentChanged(timestamp);
             }
 
             var observableTimestamp = reader.ReadInt64();
diff --git 
a/modules/platforms/dotnet/Apache.Ignite/Internal/IClientSocketEventListener.cs 
b/modules/platforms/dotnet/Apache.Ignite/Internal/IClientSocketEventListener.cs
index 469afbfd2d..3dbbd4bb95 100644
--- 
a/modules/platforms/dotnet/Apache.Ignite/Internal/IClientSocketEventListener.cs
+++ 
b/modules/platforms/dotnet/Apache.Ignite/Internal/IClientSocketEventListener.cs
@@ -25,8 +25,8 @@ internal interface IClientSocketEventListener
     /// <summary>
     /// Called when partition assignment changes.
     /// </summary>
-    /// <param name="clientSocket">Source socket.</param>
-    void OnAssignmentChanged(ClientSocket clientSocket);
+    /// <param name="timestamp">Timestamp.</param>
+    void OnAssignmentChanged(long timestamp);
 
     /// <summary>
     /// Called when observable timestamp changes.
diff --git a/modules/platforms/dotnet/Apache.Ignite/Internal/Table/Table.cs 
b/modules/platforms/dotnet/Apache.Ignite/Internal/Table/Table.cs
index 80e5e97502..9e1fc51842 100644
--- a/modules/platforms/dotnet/Apache.Ignite/Internal/Table/Table.cs
+++ b/modules/platforms/dotnet/Apache.Ignite/Internal/Table/Table.cs
@@ -74,7 +74,7 @@ namespace Apache.Ignite.Internal.Table
         private volatile int _latestSchemaVersion = SchemaVersionUnknown;
 
         /** */
-        private volatile int _partitionAssignmentVersion = -1;
+        private long _partitionAssignmentTimestamp = -1;
 
         /** */
         private volatile string?[]? _partitionAssignment;
@@ -215,11 +215,11 @@ namespace Apache.Ignite.Internal.Table
         /// <returns>Partition assignment.</returns>
         internal async ValueTask<string?[]?> GetPartitionAssignmentAsync()
         {
-            var socketVer = _socket.PartitionAssignmentVersion;
+            var latestKnownTimestamp = _socket.PartitionAssignmentTimestamp;
             var assignment = _partitionAssignment;
 
             // Async double-checked locking. Assignment changes rarely, so we 
avoid the lock if possible.
-            if (_partitionAssignmentVersion == socketVer && assignment != null)
+            if (Interlocked.Read(ref _partitionAssignmentTimestamp) >= 
latestKnownTimestamp && assignment != null)
             {
                 return assignment;
             }
@@ -228,20 +228,27 @@ namespace Apache.Ignite.Internal.Table
 
             try
             {
-                socketVer = _socket.PartitionAssignmentVersion;
+                latestKnownTimestamp = _socket.PartitionAssignmentTimestamp;
                 assignment = _partitionAssignment;
 
-                if (_partitionAssignmentVersion == socketVer && assignment != 
null)
+                if (Interlocked.Read(ref _partitionAssignmentTimestamp) >= 
latestKnownTimestamp && assignment != null)
                 {
                     return assignment;
                 }
 
-                assignment = await 
LoadPartitionAssignmentAsync().ConfigureAwait(false);
+                var res = await 
LoadPartitionAssignmentAsync(latestKnownTimestamp).ConfigureAwait(false);
+                if (res == null)
+                {
+                    // Assignment for the given timestamp is not available yet 
(some nodes can lag behind).
+                    return null;
+                }
 
-                _partitionAssignment = assignment;
-                _partitionAssignmentVersion = socketVer;
+                Debug.Assert(res.Value.Timestamp >= latestKnownTimestamp, 
"res.Value.Timestamp >= socketTimestamp");
 
-                return assignment;
+                _partitionAssignment = res.Value.Assignment;
+                Interlocked.Exchange(ref _partitionAssignmentTimestamp, 
res.Value.Timestamp);
+
+                return res.Value.Assignment;
             }
             finally
             {
@@ -389,7 +396,7 @@ namespace Apache.Ignite.Internal.Table
         /// Loads the partition assignment.
         /// </summary>
         /// <returns>Partition assignment.</returns>
-        private async Task<string?[]?> LoadPartitionAssignmentAsync()
+        private async Task<(string?[]? Assignment, long Timestamp)?> 
LoadPartitionAssignmentAsync(long timestamp)
         {
             using var writer = ProtoCommon.GetMessageWriter();
             Write(writer.MessageWriter);
@@ -400,10 +407,10 @@ namespace Apache.Ignite.Internal.Table
             void Write(MsgPackWriter w)
             {
                 w.Write(Id);
-                w.Write(0); // TODO IGNITE-20900: Send timestamp.
+                w.Write(timestamp);
             }
 
-            string?[]? Read()
+            (string?[] Assignment, long Timestamp)? Read()
             {
                 var r = resBuf.GetReader();
                 var count = r.ReadInt32();
@@ -413,9 +420,7 @@ namespace Apache.Ignite.Internal.Table
                     return null;
                 }
 
-                // TODO IGNITE-20900: Handle timestamp.
-                _ = r.ReadInt64();
-
+                var resTimestamp = r.ReadInt64();
                 var res = new string?[count];
 
                 for (int i = 0; i < count; i++)
@@ -423,7 +428,7 @@ namespace Apache.Ignite.Internal.Table
                     res[i] = r.ReadStringNullable();
                 }
 
-                return res;
+                return (res, resTimestamp);
             }
         }
     }

Reply via email to