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 6eba48de80e IGNITE-26894 .NET: Improve DNS resolution logic (#7261)
6eba48de80e is described below

commit 6eba48de80e2963bc181d775bb850a27303916d3
Author: Pavel Tupitsyn <[email protected]>
AuthorDate: Thu Dec 18 13:55:16 2025 +0200

    IGNITE-26894 .NET: Improve DNS resolution logic (#7261)
    
    * Public API: add `IgniteClientConfiguration.ReResolveAddressesInterval`
    * Make endpoint list dynamic in `ClientFailoverSocket`
    * Resolve DNS asynchronously
    * Re-resolve on disconnect, on timer, on partition assignment update
---
 .../ignite/client/IgniteClientConfiguration.java   |   2 +-
 .../Apache.Ignite.Tests/ClientSocketTests.cs       |   7 +-
 .../dotnet/Apache.Ignite.Tests/DnsResolveTests.cs  | 166 ++++++++++++
 .../dotnet/Apache.Ignite.Tests/FakeServer.cs       |   9 +-
 .../dotnet/Apache.Ignite.Tests/FakeServerGroup.cs  |   2 +-
 .../dotnet/Apache.Ignite.Tests/IgniteServerBase.cs |   4 +-
 .../Sql/IgniteDbConnectionStringBuilderTests.cs    |   7 +-
 .../TestDnsResolver.cs}                            |  20 +-
 .../platforms/dotnet/Apache.Ignite/IgniteClient.cs |  55 ++--
 .../Apache.Ignite/IgniteClientConfiguration.cs     |  20 +-
 .../Apache.Ignite/Internal/ClientFailoverSocket.cs | 291 +++++++++++++++++----
 .../dotnet/Apache.Ignite/Internal/ClientSocket.cs  |   2 +
 ...ientConfigurationInternal.cs => DnsResolver.cs} |  16 +-
 .../Internal/IClientSocketEventListener.cs         |  10 +-
 ...entConfigurationInternal.cs => IDnsResolver.cs} |  15 +-
 .../Internal/IgniteClientConfigurationInternal.cs  |   6 +-
 .../dotnet/Apache.Ignite/Internal/LogMessages.cs   |  12 +
 .../Apache.Ignite/Internal/SocketEndpoint.cs       |  18 +-
 ...tEventListener.cs => SocketEndpointComparer.cs} |  46 +++-
 .../Sql/IgniteDbConnectionStringBuilder.cs         |  19 +-
 20 files changed, 607 insertions(+), 120 deletions(-)

diff --git 
a/modules/client/src/main/java/org/apache/ignite/client/IgniteClientConfiguration.java
 
b/modules/client/src/main/java/org/apache/ignite/client/IgniteClientConfiguration.java
index e11f0ddd0f4..74ad5e1d2d6 100644
--- 
a/modules/client/src/main/java/org/apache/ignite/client/IgniteClientConfiguration.java
+++ 
b/modules/client/src/main/java/org/apache/ignite/client/IgniteClientConfiguration.java
@@ -230,7 +230,7 @@ public interface IgniteClientConfiguration {
      * Gets how long the resolved addresses will be considered valid, in 
milliseconds. Set to {@code 0} for infinite validity.
      * Default is {@link #DFLT_BACKGROUND_RE_RESOLVE_ADDRESSES_INTERVAL}.
      *
-     * <p>Ignite client resolve the provided hostnames into multiple IP 
addresses, each corresponds to an active cluster node.
+     * <p>Ignite client resolves the provided hostnames into multiple IP 
addresses, each corresponds to an active cluster node.
      * However, additional IP addresses can be collected after updating the 
DNS records. This property controls how often Ignite
      * client will try to re-resolve provided hostnames and connect to newly 
discovered addresses.
      *
diff --git a/modules/platforms/dotnet/Apache.Ignite.Tests/ClientSocketTests.cs 
b/modules/platforms/dotnet/Apache.Ignite.Tests/ClientSocketTests.cs
index 9dd42bc9557..22614e48d77 100644
--- a/modules/platforms/dotnet/Apache.Ignite.Tests/ClientSocketTests.cs
+++ b/modules/platforms/dotnet/Apache.Ignite.Tests/ClientSocketTests.cs
@@ -87,7 +87,7 @@ namespace Apache.Ignite.Tests
             new(new(IPAddress.Loopback, serverPort ?? ServerPort), 
string.Empty, string.Empty);
 
         private static IgniteClientConfigurationInternal GetConfigInternal() =>
-            new(new(), Task.FromResult((IgniteApiAccessor)null!));
+            new(new(), Task.FromResult<IgniteApiAccessor>(null!), 
DnsResolver.Instance);
 
         private class NoOpListener : IClientSocketEventListener
         {
@@ -100,6 +100,11 @@ namespace Apache.Ignite.Tests
             {
                 // No-op.
             }
+
+            public void OnDisconnect(Exception? ex)
+            {
+                // No-op.
+            }
         }
     }
 }
diff --git a/modules/platforms/dotnet/Apache.Ignite.Tests/DnsResolveTests.cs 
b/modules/platforms/dotnet/Apache.Ignite.Tests/DnsResolveTests.cs
new file mode 100644
index 00000000000..0c4c8c787e6
--- /dev/null
+++ b/modules/platforms/dotnet/Apache.Ignite.Tests/DnsResolveTests.cs
@@ -0,0 +1,166 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * 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.Tests;
+
+using System;
+using System.Collections.Concurrent;
+using System.Collections.Generic;
+using System.Linq;
+using System.Net;
+using System.Threading;
+using System.Threading.Tasks;
+using Common;
+using Microsoft.Extensions.Logging;
+using Network;
+using NUnit.Framework;
+
+/// <summary>
+/// Tests DNS resolution behavior.
+/// </summary>
+public class DnsResolveTests
+{
+    private const string HostName = "fake-host";
+
+    private const int Port = 10902;
+
+    private ConsoleLogger _logger;
+
+    private FakeServerGroup _servers;
+
+    private ConcurrentDictionary<string, string[]> _dnsMap;
+
+    [SetUp]
+    public void SetUp()
+    {
+        _logger = new ConsoleLogger(LogLevel.Trace);
+
+        _servers = FakeServerGroup.Create(
+            count: 6,
+            x => new FakeServer(nodeName: "fake-node-" + x, address: 
IPAddress.Parse("127.0.0.1" + x), port: Port));
+
+        _dnsMap = new ConcurrentDictionary<string, string[]>
+        {
+            [HostName] = ["127.0.0.10", "127.0.0.11"]
+        };
+    }
+
+    [TearDown]
+    public void TearDown()
+    {
+        _servers.Dispose();
+        _logger.Flush();
+    }
+
+    [Test]
+    public async Task TestClientResolvesHostNamesToAllIps()
+    {
+        var cfg = new IgniteClientConfiguration($"{HostName}:{Port}")
+        {
+            LoggerFactory = _logger
+        };
+
+        using var client = await IgniteClient.StartInternalAsync(cfg, new 
TestDnsResolver(_dnsMap));
+        client.WaitForConnections(2);
+
+        var conns = client.GetConnections().OrderBy(x => x.Node.Name).ToList();
+
+        Assert.AreEqual("127.0.0.10:10902", conns[0].Node.Address.ToString());
+        Assert.AreEqual("fake-node-0", conns[0].Node.Name);
+
+        Assert.AreEqual("127.0.0.11:10902", conns[1].Node.Address.ToString());
+        Assert.AreEqual("fake-node-1", conns[1].Node.Name);
+    }
+
+    [Test]
+    public async Task TestClientReResolvesHostNamesOnDisconnect()
+    {
+        var cfg = new IgniteClientConfiguration($"{HostName}:{Port}")
+        {
+            ReResolveAddressesInterval = Timeout.InfiniteTimeSpan,
+            LoggerFactory = _logger
+        };
+
+        using var client = await IgniteClient.StartInternalAsync(cfg, new 
TestDnsResolver(_dnsMap));
+        client.WaitForConnections(2, timeoutMs: 3000);
+
+        _dnsMap[HostName] = ["127.0.0.12", "127.0.0.13", "127.0.0.14", 
"127.0.0.15"];
+
+        // Close one of the existing connections to trigger re-resolve.
+        _servers.Servers[0].Dispose();
+
+        client.WaitForConnections(5, timeoutMs: 3000);
+    }
+
+    [Test]
+    public async Task TestClientReResolvesHostNamesPeriodically()
+    {
+        var cfg = new IgniteClientConfiguration($"{HostName}:{Port}")
+        {
+            ReResolveAddressesInterval = TimeSpan.FromMilliseconds(300),
+            LoggerFactory = _logger
+        };
+
+        using var client = await IgniteClient.StartInternalAsync(cfg, new 
TestDnsResolver(_dnsMap));
+        client.WaitForConnections(2, timeoutMs: 3000);
+
+        _dnsMap[HostName] = ["127.0.0.12", "127.0.0.13", "127.0.0.14", 
"127.0.0.15"];
+        client.WaitForConnections(6, timeoutMs: 3000);
+    }
+
+    [Test]
+    public async Task 
TestClientReResolvesHostNamesOnPrimaryReplicaAssignmentChange()
+    {
+        var cfg = new IgniteClientConfiguration($"{HostName}:{Port}")
+        {
+            ReResolveAddressesInterval = Timeout.InfiniteTimeSpan,
+            LoggerFactory = _logger,
+            HeartbeatInterval = TimeSpan.FromMilliseconds(100)
+        };
+
+        using var client = await IgniteClient.StartInternalAsync(cfg, new 
TestDnsResolver(_dnsMap));
+        client.WaitForConnections(2, timeoutMs: 3000);
+
+        _dnsMap[HostName] = ["127.0.0.12", "127.0.0.13", "127.0.0.14", 
"127.0.0.15"];
+
+        // Heartbeat will trigger re-resolve on assignment change.
+        _servers.Servers[0].PartitionAssignmentTimestamp = 42;
+
+        client.WaitForConnections(6, timeoutMs: 3000);
+    }
+
+    [Test]
+    public async Task TestClientRetainsExistingConnectionsOnEndpointRefresh()
+    {
+        var cfg = new IgniteClientConfiguration($"{HostName}:{Port}")
+        {
+            ReResolveAddressesInterval = TimeSpan.FromMilliseconds(300),
+            LoggerFactory = _logger
+        };
+
+        using var client = await IgniteClient.StartInternalAsync(cfg, new 
TestDnsResolver(_dnsMap));
+        client.WaitForConnections(2, timeoutMs: 3000);
+        List<IClusterNode> initialConns = client.GetConnections().Select(x => 
x.Node).OrderBy(x => x.Name).ToList();
+
+        _dnsMap[HostName] = ["127.0.0.12", "127.0.0.11", "127.0.0.10"]; // 
Same two + new one
+        client.WaitForConnections(3, timeoutMs: 3000);
+        List<IClusterNode> updatedConns = client.GetConnections().Select(x => 
x.Node).OrderBy(x => x.Name).ToList();
+
+        Assert.AreSame(initialConns[0], updatedConns[0]);
+        Assert.AreSame(initialConns[1], updatedConns[1]);
+    }
+}
diff --git a/modules/platforms/dotnet/Apache.Ignite.Tests/FakeServer.cs 
b/modules/platforms/dotnet/Apache.Ignite.Tests/FakeServer.cs
index 63a504a583f..789a045a4b2 100644
--- a/modules/platforms/dotnet/Apache.Ignite.Tests/FakeServer.cs
+++ b/modules/platforms/dotnet/Apache.Ignite.Tests/FakeServer.cs
@@ -82,14 +82,15 @@ namespace Apache.Ignite.Tests
             Func<RequestContext, bool>? shouldDropConnection = null,
             string nodeName = "fake-server",
             bool disableOpsTracking = false,
-            int port = 0)
-            : base(port)
+            int port = 0,
+            IPAddress? address = null)
+            : base(address, port)
         {
             _shouldDropConnection = shouldDropConnection ?? (_ => false);
 
             Node = new ClusterNode(Guid.NewGuid(), nodeName, 
IPEndPoint.Parse("127.0.0.1:" + Port));
-            PartitionAssignment = new[] { nodeName };
-            ClusterNodes = new[] { Node };
+            PartitionAssignment = [nodeName];
+            ClusterNodes = [Node];
 
             if (!disableOpsTracking)
             {
diff --git a/modules/platforms/dotnet/Apache.Ignite.Tests/FakeServerGroup.cs 
b/modules/platforms/dotnet/Apache.Ignite.Tests/FakeServerGroup.cs
index 18e852c4fdd..77cae79b404 100644
--- a/modules/platforms/dotnet/Apache.Ignite.Tests/FakeServerGroup.cs
+++ b/modules/platforms/dotnet/Apache.Ignite.Tests/FakeServerGroup.cs
@@ -48,7 +48,7 @@ public sealed class FakeServerGroup : IDisposable
     }
 
     public static FakeServerGroup Create(int count) =>
-        new(Enumerable.Range(0, count).Select(_ => new FakeServer()).ToList());
+        new(Enumerable.Range(0, count).Select(idx => new FakeServer(nodeName: 
"fake-server-" + idx)).ToList());
 
     public static FakeServerGroup Create(int count, Func<int, FakeServer> 
factory) =>
         new(Enumerable.Range(0, count).Select(factory).ToList());
diff --git a/modules/platforms/dotnet/Apache.Ignite.Tests/IgniteServerBase.cs 
b/modules/platforms/dotnet/Apache.Ignite.Tests/IgniteServerBase.cs
index 60a25d40e50..a8cb7682a36 100644
--- a/modules/platforms/dotnet/Apache.Ignite.Tests/IgniteServerBase.cs
+++ b/modules/platforms/dotnet/Apache.Ignite.Tests/IgniteServerBase.cs
@@ -42,12 +42,12 @@ public abstract class IgniteServerBase : IDisposable
 
     private volatile bool _dropNewConnections;
 
-    protected IgniteServerBase(int port = 0)
+    protected IgniteServerBase(IPAddress? address = null, int port = 0)
     {
         _listener = new Socket(IPAddress.Loopback.AddressFamily, 
SocketType.Stream, ProtocolType.Tcp);
         _listener.NoDelay = true;
 
-        _listener.Bind(new IPEndPoint(IPAddress.Any, port));
+        _listener.Bind(new IPEndPoint(address ?? IPAddress.Any, port));
         _listener.Listen();
 
         Name = TestContext.CurrentContext.Test.Name;
diff --git 
a/modules/platforms/dotnet/Apache.Ignite.Tests/Sql/IgniteDbConnectionStringBuilderTests.cs
 
b/modules/platforms/dotnet/Apache.Ignite.Tests/Sql/IgniteDbConnectionStringBuilderTests.cs
index a2ccb31e012..4c1d58dac4b 100644
--- 
a/modules/platforms/dotnet/Apache.Ignite.Tests/Sql/IgniteDbConnectionStringBuilderTests.cs
+++ 
b/modules/platforms/dotnet/Apache.Ignite.Tests/Sql/IgniteDbConnectionStringBuilderTests.cs
@@ -30,7 +30,8 @@ public class IgniteDbConnectionStringBuilderTests
     {
         var connStr =
             
"Endpoints=localhost:10800,localhost:10801;SocketTimeout=00:00:02.5000000;OperationTimeout=00:01:14.0700000;"
 +
-            
"HeartbeatInterval=00:00:01.3640000;ReconnectInterval=00:00:00.5432100;SslEnabled=True;Username=user1;Password=hunter2";
+            
"HeartbeatInterval=00:00:01.3640000;ReconnectInterval=00:00:00.5432100;SslEnabled=True;Username=user1;Password=hunter2;"
 +
+            "ReResolveAddressesInterval=11:22:33";
 
         var builder = new IgniteDbConnectionStringBuilder(connStr);
 
@@ -39,6 +40,7 @@ public class IgniteDbConnectionStringBuilderTests
         Assert.AreEqual(TimeSpan.FromMinutes(1.2345), 
builder.OperationTimeout);
         Assert.AreEqual(TimeSpan.FromSeconds(1.364), 
builder.HeartbeatInterval);
         Assert.AreEqual(TimeSpan.FromSeconds(0.54321), 
builder.ReconnectInterval);
+        Assert.AreEqual(new TimeSpan(0, 11, 22, 33), 
builder.ReResolveAddressesInterval);
         Assert.IsTrue(builder.SslEnabled);
         Assert.AreEqual("user1", builder.Username);
         Assert.AreEqual("hunter2", builder.Password);
@@ -52,6 +54,7 @@ public class IgniteDbConnectionStringBuilderTests
         Assert.AreEqual(TimeSpan.FromMinutes(1.2345), 
clientConfig.OperationTimeout);
         Assert.AreEqual(TimeSpan.FromSeconds(1.364), 
clientConfig.HeartbeatInterval);
         Assert.AreEqual(TimeSpan.FromSeconds(0.54321), 
clientConfig.ReconnectInterval);
+        Assert.AreEqual(new TimeSpan(0, 11, 22, 33), 
clientConfig.ReResolveAddressesInterval);
         Assert.IsNotNull(clientConfig.SslStreamFactory);
         Assert.AreEqual("user1", 
((BasicAuthenticator)clientConfig.Authenticator!).Username);
         Assert.AreEqual("hunter2", 
((BasicAuthenticator)clientConfig.Authenticator!).Password);
@@ -69,6 +72,7 @@ public class IgniteDbConnectionStringBuilderTests
         Assert.AreEqual(IgniteClientConfiguration.DefaultOperationTimeout, 
builder.OperationTimeout);
         Assert.AreEqual(IgniteClientConfiguration.DefaultHeartbeatInterval, 
builder.HeartbeatInterval);
         Assert.AreEqual(IgniteClientConfiguration.DefaultReconnectInterval, 
builder.ReconnectInterval);
+        
Assert.AreEqual(IgniteClientConfiguration.DefaultReResolveAddressesInterval, 
builder.ReResolveAddressesInterval);
         Assert.IsFalse(builder.SslEnabled);
         Assert.IsNull(builder.Username);
         Assert.IsNull(builder.Password);
@@ -82,6 +86,7 @@ public class IgniteDbConnectionStringBuilderTests
         Assert.AreEqual(IgniteClientConfiguration.DefaultOperationTimeout, 
clientConfig.OperationTimeout);
         Assert.AreEqual(IgniteClientConfiguration.DefaultHeartbeatInterval, 
clientConfig.HeartbeatInterval);
         Assert.AreEqual(IgniteClientConfiguration.DefaultReconnectInterval, 
clientConfig.ReconnectInterval);
+        
Assert.AreEqual(IgniteClientConfiguration.DefaultReResolveAddressesInterval, 
clientConfig.ReResolveAddressesInterval);
         Assert.IsNull(clientConfig.SslStreamFactory);
         Assert.IsNull(clientConfig.Authenticator);
     }
diff --git 
a/modules/platforms/dotnet/Apache.Ignite/Internal/IgniteClientConfigurationInternal.cs
 b/modules/platforms/dotnet/Apache.Ignite.Tests/TestDnsResolver.cs
similarity index 63%
copy from 
modules/platforms/dotnet/Apache.Ignite/Internal/IgniteClientConfigurationInternal.cs
copy to modules/platforms/dotnet/Apache.Ignite.Tests/TestDnsResolver.cs
index 34476411c91..ff437488973 100644
--- 
a/modules/platforms/dotnet/Apache.Ignite/Internal/IgniteClientConfigurationInternal.cs
+++ b/modules/platforms/dotnet/Apache.Ignite.Tests/TestDnsResolver.cs
@@ -15,13 +15,19 @@
  * limitations under the License.
  */
 
-namespace Apache.Ignite.Internal;
+namespace Apache.Ignite.Tests;
 
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Net;
 using System.Threading.Tasks;
+using Internal;
 
-/// <summary>
-/// Internal Ignite client configuration.
-/// </summary>
-/// <param name="Configuration">Configuration.</param>
-/// <param name="ApiTask">API accessor task.</param>
-internal sealed record 
IgniteClientConfigurationInternal(IgniteClientConfiguration Configuration, 
Task<IgniteApiAccessor> ApiTask);
+public sealed class TestDnsResolver(IReadOnlyDictionary<string, string[]> map) 
: IDnsResolver
+{
+    public Task<IPAddress[]> GetHostAddressesAsync(string hostName) =>
+        map.TryGetValue(hostName, out var ips) ?
+            Task.FromResult(ips.Select(IPAddress.Parse).ToArray()) :
+            throw new Exception("Unknown host: " + hostName);
+}
diff --git a/modules/platforms/dotnet/Apache.Ignite/IgniteClient.cs 
b/modules/platforms/dotnet/Apache.Ignite/IgniteClient.cs
index 1be9ffe83ed..bcf4b64be39 100644
--- a/modules/platforms/dotnet/Apache.Ignite/IgniteClient.cs
+++ b/modules/platforms/dotnet/Apache.Ignite/IgniteClient.cs
@@ -15,36 +15,45 @@
  * limitations under the License.
  */
 
-namespace Apache.Ignite
+namespace Apache.Ignite;
+
+using System.Threading.Tasks;
+using Internal;
+using Internal.Common;
+
+/// <summary>
+/// Ignite client builder.
+/// </summary>
+public static class IgniteClient
 {
-    using System.Threading.Tasks;
-    using Internal;
-    using Internal.Common;
+    /// <summary>
+    /// Starts the client.
+    /// </summary>
+    /// <param name="configuration">Configuration.</param>
+    /// <returns>Started client.</returns>
+    public static async Task<IIgniteClient> 
StartAsync(IgniteClientConfiguration configuration) =>
+        await StartInternalAsync(configuration, 
DnsResolver.Instance).ConfigureAwait(false);
 
     /// <summary>
-    /// Ignite client builder.
+    /// Starts the client.
     /// </summary>
-    public static class IgniteClient
+    /// <param name="configuration">Configuration.</param>
+    /// <param name="dnsResolver">DNS resolver.</param>
+    /// <returns>Started client.</returns>
+    internal static async Task<IIgniteClient> 
StartInternalAsync(IgniteClientConfiguration configuration, IDnsResolver 
dnsResolver)
     {
-        /// <summary>
-        /// Starts the client.
-        /// </summary>
-        /// <param name="configuration">Configuration.</param>
-        /// <returns>Started client.</returns>
-        public static async Task<IIgniteClient> 
StartAsync(IgniteClientConfiguration configuration)
-        {
-            IgniteArgumentCheck.NotNull(configuration);
+        IgniteArgumentCheck.NotNull(configuration);
 
-            var apiTaskSource = new TaskCompletionSource<IgniteApiAccessor>();
-            var internalConfig = new IgniteClientConfigurationInternal(
-                new(configuration), // Defensive copy.
-                apiTaskSource.Task);
+        var apiTaskSource = new TaskCompletionSource<IgniteApiAccessor>();
+        var internalConfig = new IgniteClientConfigurationInternal(
+            new(configuration), // Defensive copy.
+            apiTaskSource.Task,
+            dnsResolver);
 
-            var socket = await 
ClientFailoverSocket.ConnectAsync(internalConfig).ConfigureAwait(false);
-            var client = new IgniteClientInternal(socket);
+        var socket = await 
ClientFailoverSocket.ConnectAsync(internalConfig).ConfigureAwait(false);
+        var client = new IgniteClientInternal(socket);
 
-            apiTaskSource.SetResult(new IgniteApiAccessor(client));
-            return client;
-        }
+        apiTaskSource.SetResult(new IgniteApiAccessor(client));
+        return client;
     }
 }
diff --git 
a/modules/platforms/dotnet/Apache.Ignite/IgniteClientConfiguration.cs 
b/modules/platforms/dotnet/Apache.Ignite/IgniteClientConfiguration.cs
index 2790a9a7c08..eaf84a0d3bb 100644
--- a/modules/platforms/dotnet/Apache.Ignite/IgniteClientConfiguration.cs
+++ b/modules/platforms/dotnet/Apache.Ignite/IgniteClientConfiguration.cs
@@ -56,6 +56,11 @@ namespace Apache.Ignite
         /// </summary>
         public static readonly TimeSpan DefaultReconnectInterval = 
TimeSpan.FromSeconds(30);
 
+        /// <summary>
+        /// Default DNS re-resolve interval.
+        /// </summary>
+        public static readonly TimeSpan DefaultReResolveAddressesInterval = 
TimeSpan.FromSeconds(30);
+
         /// <summary>
         /// Initializes a new instance of the <see 
cref="IgniteClientConfiguration"/> class.
         /// </summary>
@@ -96,6 +101,7 @@ namespace Apache.Ignite
             ReconnectInterval = other.ReconnectInterval;
             SslStreamFactory = other.SslStreamFactory;
             Authenticator = other.Authenticator;
+            ReResolveAddressesInterval = other.ReResolveAddressesInterval;
         }
 
         /// <summary>
@@ -179,10 +185,22 @@ namespace Apache.Ignite
         [DefaultValue("00:00:30")]
         public TimeSpan ReconnectInterval { get; set; } = 
DefaultReconnectInterval;
 
+        /// <summary>
+        /// Gets or sets how long the resolved addresses will be considered 
valid.
+        /// <para />
+        /// Default is <see cref="DefaultReResolveAddressesInterval"/>.
+        /// Set to <see cref="TimeSpan.Zero"/> to disable periodic DNS 
re-resolve.
+        /// <para />
+        /// Ignite client resolves the provided hostnames into multiple IP 
addresses, each corresponds to a cluster node.
+        /// This property controls how often the client will re-resolve 
provided hostnames.
+        /// </summary>
+        [DefaultValue("00:00:30")]
+        public TimeSpan ReResolveAddressesInterval { get; set; } = 
DefaultReResolveAddressesInterval;
+
         /// <summary>
         /// Gets or sets the SSL stream factory.
         /// <para />
-        /// When not null, secure socket connection will be established.
+        /// When not null, a secure socket connection will be established.
         /// <para />
         /// See <see cref="SslStreamFactory"/>.
         /// </summary>
diff --git 
a/modules/platforms/dotnet/Apache.Ignite/Internal/ClientFailoverSocket.cs 
b/modules/platforms/dotnet/Apache.Ignite/Internal/ClientFailoverSocket.cs
index baa187de943..96b41d8cacf 100644
--- a/modules/platforms/dotnet/Apache.Ignite/Internal/ClientFailoverSocket.cs
+++ b/modules/platforms/dotnet/Apache.Ignite/Internal/ClientFailoverSocket.cs
@@ -49,9 +49,6 @@ namespace Apache.Ignite.Internal
         /** Logger. */
         private readonly ILogger _logger;
 
-        /** Endpoints with corresponding hosts - from configuration. */
-        private readonly IReadOnlyList<SocketEndpoint> _endpoints;
-
         /** Cluster node unique name to endpoint map. */
         private readonly ConcurrentDictionary<string, SocketEndpoint> 
_endpointsByName = new();
 
@@ -62,6 +59,16 @@ namespace Apache.Ignite.Internal
             Justification = "WaitHandle is not used in SemaphoreSlim, no need 
to dispose.")]
         private readonly SemaphoreSlim _socketLock = new(1);
 
+        /** Socket connection lock. */
+        [SuppressMessage(
+            "Microsoft.Design",
+            "CA2213:DisposableFieldsShouldBeDisposed",
+            Justification = "WaitHandle is not used in SemaphoreSlim, no need 
to dispose.")]
+        private readonly SemaphoreSlim _initEndpointsLock = new(1);
+
+        /** Endpoints with corresponding hosts - from configuration. */
+        private volatile IReadOnlyList<SocketEndpoint> _endpoints = [];
+
         /** Disposed flag. */
         private volatile bool _disposed;
 
@@ -93,8 +100,9 @@ namespace Apache.Ignite.Internal
             }
 
             _logger = logger;
-            _endpoints = GetIpEndPoints(configuration.Configuration).ToList();
 
+            ClientId = Guid.NewGuid();
+            ClientIdString = ClientId.ToString();
             Configuration = configuration;
         }
 
@@ -116,7 +124,12 @@ namespace Apache.Ignite.Internal
         /// <summary>
         /// Gets the client ID.
         /// </summary>
-        public Guid ClientId { get; } = Guid.NewGuid();
+        public Guid ClientId { get; }
+
+        /// <summary>
+        /// Gets the client ID.
+        /// </summary>
+        public string ClientIdString { get; }
 
         /// <summary>
         /// Gets a value indicating whether the socket is disposed.
@@ -135,12 +148,16 @@ namespace Apache.Ignite.Internal
 
             var socket = new ClientFailoverSocket(configuration, logger);
 
+            await socket.InitEndpointsAsync().ConfigureAwait(false);
             await socket.GetNextSocketAsync().ConfigureAwait(false);
 
             // Because this call is not awaited, execution of the current 
method continues before the call is completed.
             // Secondary connections are established in the background.
             _ = socket.ConnectAllSockets();
 
+            // Re-resolve DNS names in the background periodically.
+            _ = socket.ReResolveDnsPeriodically();
+
             return socket;
         }
 
@@ -280,6 +297,7 @@ namespace Apache.Ignite.Internal
         [SuppressMessage("Design", "CA1031:Do not catch general exception 
types", Justification = "Reviewed.")]
         public void Dispose()
         {
+            _initEndpointsLock.Wait();
             _socketLock.Wait();
 
             try
@@ -306,6 +324,7 @@ namespace Apache.Ignite.Internal
             finally
             {
                 _socketLock.Release();
+                _initEndpointsLock.Release();
             }
         }
 
@@ -315,9 +334,10 @@ namespace Apache.Ignite.Internal
         /// <returns>Active connections.</returns>
         public IList<IConnectionInfo> GetConnections()
         {
-            var res = new List<IConnectionInfo>(_endpoints.Count);
+            var endpoints = _endpoints;
+            var res = new List<IConnectionInfo>(endpoints.Count);
 
-            foreach (var endpoint in _endpoints)
+            foreach (var endpoint in endpoints)
             {
                 if (endpoint.Socket is { IsDisposed: false, ConnectionContext: 
{ } ctx })
                 {
@@ -341,6 +361,7 @@ namespace Apache.Ignite.Internal
 
                 if (Interlocked.CompareExchange(ref _assignmentTimestamp, 
value: timestamp, comparand: oldTimestamp) == oldTimestamp)
                 {
+                    ScheduleReResolveDns();
                     return;
                 }
             }
@@ -366,15 +387,26 @@ namespace Apache.Ignite.Internal
             }
         }
 
+        /// <inheritdoc/>
+        void IClientSocketEventListener.OnDisconnect(Exception? ex)
+        {
+            if (ex is IgniteClientConnectionException)
+            {
+                // Connection failed => potential topology change.
+                ScheduleReResolveDns();
+            }
+        }
+
         /// <summary>
         /// Gets active sockets.
         /// </summary>
         /// <returns>Active sockets.</returns>
         internal IEnumerable<ClientSocket> GetSockets()
         {
-            var res = new List<ClientSocket>(_endpoints.Count);
+            var endpoints = _endpoints;
+            var res = new List<ClientSocket>(endpoints.Count);
 
-            foreach (var endpoint in _endpoints)
+            foreach (var endpoint in endpoints)
             {
                 if (endpoint.Socket is { IsDisposed: false })
                 {
@@ -385,6 +417,91 @@ namespace Apache.Ignite.Internal
             return res;
         }
 
+        private async Task<bool> InitEndpointsAsync(int lockWaitTimeoutMs = 
Timeout.Infinite)
+        {
+            bool lockAcquired = await 
_initEndpointsLock.WaitAsync(lockWaitTimeoutMs).ConfigureAwait(false);
+            if (!lockAcquired)
+            {
+                return false;
+            }
+
+            try
+            {
+                if (_disposed)
+                {
+                    return false;
+                }
+
+                HashSet<SocketEndpoint> newEndpoints = await 
GetIpEndPointsAsync(Configuration.Configuration).ConfigureAwait(false);
+                IReadOnlyList<SocketEndpoint> oldEndpoints = _endpoints;
+
+                var resList = new List<SocketEndpoint>(newEndpoints.Count);
+                List<SocketEndpoint>? removed = null;
+
+                // Retain existing sockets for endpoints that are still 
present.
+                foreach (var oldEndpoint in oldEndpoints)
+                {
+                    if (newEndpoints.Remove(oldEndpoint))
+                    {
+                        oldEndpoint.IsAbandoned = false; // Revive if it was 
abandoned before.
+                        resList.Add(oldEndpoint);
+                    }
+                    else
+                    {
+                        removed ??= new List<SocketEndpoint>();
+                        removed.Add(oldEndpoint);
+
+                        // Lock to avoid concurrent reconnect of abandoned 
endpoint.
+                        await _socketLock.WaitAsync().ConfigureAwait(false);
+
+                        try
+                        {
+                            var oldSock = oldEndpoint.Socket;
+                            if (oldSock == null || oldSock.IsDisposed)
+                            {
+                                // Disconnected old endpoint. Throw away.
+                                if (oldSock != null)
+                                {
+                                    _endpointsByName.TryRemove(
+                                        new KeyValuePair<string, 
SocketEndpoint>(oldSock.ConnectionContext.ClusterNode.Name, oldEndpoint));
+                                }
+
+                                continue;
+                            }
+                        }
+                        finally
+                        {
+                            _socketLock.Release();
+                        }
+
+                        // Mark as abandoned but keep the socket alive for 
existing operations.
+                        oldEndpoint.IsAbandoned = true;
+                        resList.Add(oldEndpoint);
+                    }
+                }
+
+                if (_logger.IsEnabled(LogLevel.Trace) && (newEndpoints.Count > 
0 || removed != null))
+                {
+                    var addedStr = newEndpoints.Select(e => 
e.EndPointString).StringJoin();
+                    var removedStr = removed?.Select(e => 
e.EndPointString).StringJoin();
+
+                    _logger.LogEndpointListUpdatedTrace(addedStr, removedStr 
?? string.Empty);
+                }
+
+                // Add remaining endpoints that were not known before.
+                resList.AddRange(newEndpoints);
+
+                // Apply the new endpoint list.
+                _endpoints = resList;
+
+                return newEndpoints.Count > 0;
+            }
+            finally
+            {
+                _initEndpointsLock.Release();
+            }
+        }
+
         /// <summary>
         /// Gets a socket. Reconnects if necessary.
         /// </summary>
@@ -421,43 +538,22 @@ namespace Apache.Ignite.Internal
             return await GetNextSocketAsync().ConfigureAwait(false);
         }
 
-        [SuppressMessage(
-            "Microsoft.Design",
-            "CA1031:DoNotCatchGeneralExceptionTypes",
-            Justification = "Secondary connection errors can be ignored.")]
         private async Task ConnectAllSockets()
         {
-            if (_endpoints.Count == 1)
-            {
-                // No secondary connections to establish.
-                return;
-            }
-
             while (!_disposed)
             {
+                var endpoints = _endpoints;
+
                 if (_logger.IsEnabled(LogLevel.Debug))
                 {
-                    
_logger.LogTryingToEstablishSecondaryConnectionsDebug(_endpoints.Count);
+                    
_logger.LogTryingToEstablishSecondaryConnectionsDebug(endpoints.Count);
                 }
 
-                int failed = 0;
-
-                foreach (var endpoint in _endpoints)
-                {
-                    try
-                    {
-                        await ConnectAsync(endpoint).ConfigureAwait(false);
-                    }
-                    catch (Exception e)
-                    {
-                        
_logger.LogErrorWhileEstablishingSecondaryConnectionsWarn(e, e.Message);
-                        failed++;
-                    }
-                }
+                var failed = await 
ConnectAllSocketsInnerAsync(endpoints).ConfigureAwait(false);
 
                 if (_logger.IsEnabled(LogLevel.Debug))
                 {
-                    
_logger.LogSecondaryConnectionsEstablishedDebug(_endpoints.Count - failed, 
failed);
+                    
_logger.LogSecondaryConnectionsEstablishedDebug(endpoints.Count - failed, 
failed);
                 }
 
                 if (Configuration.Configuration.ReconnectInterval <= 
TimeSpan.Zero)
@@ -470,6 +566,77 @@ namespace Apache.Ignite.Internal
             }
         }
 
+        [SuppressMessage(
+            "Microsoft.Design",
+            "CA1031:DoNotCatchGeneralExceptionTypes",
+            Justification = "Secondary connection errors can be ignored.")]
+        private async Task<int> 
ConnectAllSocketsInnerAsync(IReadOnlyList<SocketEndpoint> endpoints)
+        {
+            int failed = 0;
+
+            foreach (var endpoint in endpoints)
+            {
+                if (endpoint.IsAbandoned)
+                {
+                    continue;
+                }
+
+                try
+                {
+                    await ConnectAsync(endpoint).ConfigureAwait(false);
+                }
+                catch (Exception e)
+                {
+                    
_logger.LogErrorWhileEstablishingSecondaryConnectionsWarn(e, e.Message);
+                    failed++;
+                }
+            }
+
+            return failed;
+        }
+
+        private async Task ReResolveDnsPeriodically()
+        {
+            var interval = 
Configuration.Configuration.ReResolveAddressesInterval;
+
+            if (interval <= TimeSpan.Zero)
+            {
+                // Re-resolve is disabled.
+                return;
+            }
+
+            while (!_disposed)
+            {
+                await Task.Delay(interval).ConfigureAwait(false);
+                await ReResolveDns().ConfigureAwait(false);
+            }
+        }
+
+        [SuppressMessage(
+            "Microsoft.Design",
+            "CA1031:DoNotCatchGeneralExceptionTypes",
+            Justification = "Re-resolve errors are logged and skipped.")]
+        private async Task ReResolveDns()
+        {
+            try
+            {
+                // Skip if another operation is in progress.
+                var changed = await InitEndpointsAsync(lockWaitTimeoutMs: 
1).ConfigureAwait(false);
+
+                if (changed)
+                {
+                    await 
ConnectAllSocketsInnerAsync(_endpoints).ConfigureAwait(false);
+                }
+            }
+            catch (Exception e)
+            {
+                _logger.LogErrorWhileReResolvingDnsWarn(e, e.Message);
+            }
+        }
+
+        private void ScheduleReResolveDns() =>
+            ThreadPool.QueueUserWorkItem(static thisSock => _ = 
thisSock.ReResolveDns(), this, false);
+
         /// <summary>
         /// Throws if disposed.
         /// </summary>
@@ -483,17 +650,23 @@ namespace Apache.Ignite.Internal
         {
             List<Exception>? errors = null;
             var startIdx = unchecked((int) Interlocked.Increment(ref 
_endPointIndex));
+            var endpoints = _endpoints;
 
-            for (var i = 0; i < _endpoints.Count; i++)
+            for (var i = 0; i < endpoints.Count; i++)
             {
-                var idx = Math.Abs(startIdx + i) % _endpoints.Count;
-                var endPoint = _endpoints[idx];
+                var idx = Math.Abs(startIdx + i) % endpoints.Count;
+                var endPoint = endpoints[idx];
 
                 if (endPoint.Socket is { IsDisposed: false })
                 {
                     return endPoint.Socket;
                 }
 
+                if (endPoint.IsAbandoned)
+                {
+                    continue;
+                }
+
                 try
                 {
                     return await ConnectAsync(endPoint).ConfigureAwait(false);
@@ -518,11 +691,12 @@ namespace Apache.Ignite.Internal
         private ClientSocket? GetNextSocketWithoutReconnect()
         {
             var startIdx = unchecked((int) Interlocked.Increment(ref 
_endPointIndex));
+            var endpoints = _endpoints;
 
-            for (var i = 0; i < _endpoints.Count; i++)
+            for (var i = 0; i < endpoints.Count; i++)
             {
-                var idx = Math.Abs(startIdx + i) % _endpoints.Count;
-                var endPoint = _endpoints[idx];
+                var idx = Math.Abs(startIdx + i) % endpoints.Count;
+                var endPoint = endpoints[idx];
 
                 if (endPoint.Socket is { IsDisposed: false })
                 {
@@ -547,11 +721,20 @@ namespace Apache.Ignite.Internal
 
             try
             {
+                ThrowIfDisposed();
+
                 if (endpoint.Socket?.IsDisposed == false)
                 {
                     return endpoint.Socket;
                 }
 
+                if (endpoint.IsAbandoned)
+                {
+                    throw new IgniteClientConnectionException(
+                        ErrorGroups.Client.Connection,
+                        $"Endpoint {endpoint.EndPoint} is abandoned and cannot 
be used for new connections.");
+                }
+
                 var socket = await ClientSocket.ConnectAsync(endpoint, 
Configuration, this).ConfigureAwait(false);
 
                 if (_clusterId == null)
@@ -582,33 +765,41 @@ namespace Apache.Ignite.Internal
         /// <summary>
         /// Gets the endpoints: all combinations of IP addresses and ports 
according to configuration.
         /// </summary>
-        private IEnumerable<SocketEndpoint> 
GetIpEndPoints(IgniteClientConfiguration cfg)
+        private async Task<HashSet<SocketEndpoint>> 
GetIpEndPointsAsync(IgniteClientConfiguration cfg)
         {
-            // Metric collection tools expect numbers and strings, don't pass 
Guid.
-            var clientId = ClientId.ToString();
+            var res = new HashSet<SocketEndpoint>(new 
SocketEndpointComparer());
 
             foreach (var e in Endpoint.GetEndpoints(cfg))
             {
                 var host = e.Host;
                 Debug.Assert(host != null, "host != null"); // Checked by 
GetEndpoints.
 
-                foreach (var ip in GetIps(host))
+                IPAddress[] ips = await 
GetIpsAsync(host).ConfigureAwait(false);
+
+                foreach (var ip in ips)
                 {
-                    yield return new SocketEndpoint(new IPEndPoint(ip, 
e.Port), host, clientId);
+                    res.Add(new SocketEndpoint(new IPEndPoint(ip, e.Port), 
host, ClientIdString));
                 }
             }
+
+            return res;
         }
 
         /// <summary>
-        /// Gets IP address list from a given host.
+        /// Gets an IP address list from a given host.
         /// When host is an IP already - parses it. Otherwise, resolves DNS 
name to IPs.
         /// </summary>
-        private IEnumerable<IPAddress> GetIps(string host, bool 
suppressExceptions = false)
+        private async Task<IPAddress[]> GetIpsAsync(string host, bool 
suppressExceptions = false)
         {
             try
             {
                 // GetHostEntry accepts IPs, but TryParse is a more efficient 
shortcut.
-                return IPAddress.TryParse(host, out var ip) ? new[] { ip } : 
Dns.GetHostEntry(host).AddressList;
+                if (IPAddress.TryParse(host, out var ip))
+                {
+                    return [ip];
+                }
+
+                return await 
Configuration.DnsResolver.GetHostAddressesAsync(host).ConfigureAwait(false);
             }
             catch (SocketException e)
             {
@@ -616,7 +807,7 @@ namespace Apache.Ignite.Internal
 
                 if (suppressExceptions)
                 {
-                    return Enumerable.Empty<IPAddress>();
+                    return [];
                 }
 
                 throw;
diff --git a/modules/platforms/dotnet/Apache.Ignite/Internal/ClientSocket.cs 
b/modules/platforms/dotnet/Apache.Ignite/Internal/ClientSocket.cs
index f6c37f72e28..e4f92b2ab8c 100644
--- a/modules/platforms/dotnet/Apache.Ignite/Internal/ClientSocket.cs
+++ b/modules/platforms/dotnet/Apache.Ignite/Internal/ClientSocket.cs
@@ -1101,6 +1101,8 @@ namespace Apache.Ignite.Internal
                     Environment.Exit(0);
                 }
             }
+
+            _listener.OnDisconnect(ex);
         }
 
         private readonly record struct PendingRequest(
diff --git 
a/modules/platforms/dotnet/Apache.Ignite/Internal/IgniteClientConfigurationInternal.cs
 b/modules/platforms/dotnet/Apache.Ignite/Internal/DnsResolver.cs
similarity index 71%
copy from 
modules/platforms/dotnet/Apache.Ignite/Internal/IgniteClientConfigurationInternal.cs
copy to modules/platforms/dotnet/Apache.Ignite/Internal/DnsResolver.cs
index 34476411c91..ca00ad02d8f 100644
--- 
a/modules/platforms/dotnet/Apache.Ignite/Internal/IgniteClientConfigurationInternal.cs
+++ b/modules/platforms/dotnet/Apache.Ignite/Internal/DnsResolver.cs
@@ -17,11 +17,19 @@
 
 namespace Apache.Ignite.Internal;
 
+using System.Net;
 using System.Threading.Tasks;
 
 /// <summary>
-/// Internal Ignite client configuration.
+/// DNS resolver.
 /// </summary>
-/// <param name="Configuration">Configuration.</param>
-/// <param name="ApiTask">API accessor task.</param>
-internal sealed record 
IgniteClientConfigurationInternal(IgniteClientConfiguration Configuration, 
Task<IgniteApiAccessor> ApiTask);
+internal sealed class DnsResolver : IDnsResolver
+{
+    /// <summary>
+    /// Get the singleton instance.
+    /// </summary>
+    public static readonly DnsResolver Instance = new();
+
+    /// <inheritdoc/>
+    public Task<IPAddress[]> GetHostAddressesAsync(string hostName) => 
Dns.GetHostAddressesAsync(hostName);
+}
diff --git 
a/modules/platforms/dotnet/Apache.Ignite/Internal/IClientSocketEventListener.cs 
b/modules/platforms/dotnet/Apache.Ignite/Internal/IClientSocketEventListener.cs
index 3dbbd4bb95b..c8c0ead9493 100644
--- 
a/modules/platforms/dotnet/Apache.Ignite/Internal/IClientSocketEventListener.cs
+++ 
b/modules/platforms/dotnet/Apache.Ignite/Internal/IClientSocketEventListener.cs
@@ -17,13 +17,15 @@
 
 namespace Apache.Ignite.Internal;
 
+using System;
+
 /// <summary>
 /// <see cref="ClientSocket"/> event listener.
 /// </summary>
 internal interface IClientSocketEventListener
 {
     /// <summary>
-    /// Called when partition assignment changes.
+    /// Called when the partition assignment changes.
     /// </summary>
     /// <param name="timestamp">Timestamp.</param>
     void OnAssignmentChanged(long timestamp);
@@ -33,4 +35,10 @@ internal interface IClientSocketEventListener
     /// </summary>
     /// <param name="timestamp">Timestamp.</param>
     void OnObservableTimestampChanged(long timestamp);
+
+    /// <summary>
+    /// Called when the socket is disconnected.
+    /// </summary>
+    /// <param name="ex">Exception that caused the disconnect or null if 
disconnected gracefully.</param>
+    void OnDisconnect(Exception? ex);
 }
diff --git 
a/modules/platforms/dotnet/Apache.Ignite/Internal/IgniteClientConfigurationInternal.cs
 b/modules/platforms/dotnet/Apache.Ignite/Internal/IDnsResolver.cs
similarity index 72%
copy from 
modules/platforms/dotnet/Apache.Ignite/Internal/IgniteClientConfigurationInternal.cs
copy to modules/platforms/dotnet/Apache.Ignite/Internal/IDnsResolver.cs
index 34476411c91..20b425397e1 100644
--- 
a/modules/platforms/dotnet/Apache.Ignite/Internal/IgniteClientConfigurationInternal.cs
+++ b/modules/platforms/dotnet/Apache.Ignite/Internal/IDnsResolver.cs
@@ -17,11 +17,18 @@
 
 namespace Apache.Ignite.Internal;
 
+using System.Net;
 using System.Threading.Tasks;
 
 /// <summary>
-/// Internal Ignite client configuration.
+/// DNS resolver.
 /// </summary>
-/// <param name="Configuration">Configuration.</param>
-/// <param name="ApiTask">API accessor task.</param>
-internal sealed record 
IgniteClientConfigurationInternal(IgniteClientConfiguration Configuration, 
Task<IgniteApiAccessor> ApiTask);
+internal interface IDnsResolver
+{
+    /// <summary>
+    /// Resolves the specified host name into an array of IP addresses.
+    /// </summary>
+    /// <param name="hostName">Host name.</param>
+    /// <returns>Address list.</returns>
+    Task<IPAddress[]> GetHostAddressesAsync(string hostName);
+}
diff --git 
a/modules/platforms/dotnet/Apache.Ignite/Internal/IgniteClientConfigurationInternal.cs
 
b/modules/platforms/dotnet/Apache.Ignite/Internal/IgniteClientConfigurationInternal.cs
index 34476411c91..5a37b1499df 100644
--- 
a/modules/platforms/dotnet/Apache.Ignite/Internal/IgniteClientConfigurationInternal.cs
+++ 
b/modules/platforms/dotnet/Apache.Ignite/Internal/IgniteClientConfigurationInternal.cs
@@ -24,4 +24,8 @@ using System.Threading.Tasks;
 /// </summary>
 /// <param name="Configuration">Configuration.</param>
 /// <param name="ApiTask">API accessor task.</param>
-internal sealed record 
IgniteClientConfigurationInternal(IgniteClientConfiguration Configuration, 
Task<IgniteApiAccessor> ApiTask);
+/// <param name="DnsResolver">DNS resolver.</param>
+internal sealed record IgniteClientConfigurationInternal(
+    IgniteClientConfiguration Configuration,
+    Task<IgniteApiAccessor> ApiTask,
+    IDnsResolver DnsResolver);
diff --git a/modules/platforms/dotnet/Apache.Ignite/Internal/LogMessages.cs 
b/modules/platforms/dotnet/Apache.Ignite/Internal/LogMessages.cs
index d3ce076f73c..ff6c8149a2d 100644
--- a/modules/platforms/dotnet/Apache.Ignite/Internal/LogMessages.cs
+++ b/modules/platforms/dotnet/Apache.Ignite/Internal/LogMessages.cs
@@ -267,4 +267,16 @@ internal static partial class LogMessages
         Level = LogLevel.Trace,
         EventId = 1036)]
     internal static partial void LogTxRollbackTrace(this ILogger logger, long 
txId);
+
+    [LoggerMessage(
+        Message = "Error while re-resolving addresses: {Message}",
+        Level = LogLevel.Warning,
+        EventId = 1037)]
+    internal static partial void LogErrorWhileReResolvingDnsWarn(this ILogger 
logger, Exception e, string message);
+
+    [LoggerMessage(
+        Message = "Endpoints updated [added=[{Added}], removed=[{Removed}]]",
+        Level = LogLevel.Trace,
+        EventId = 1038)]
+    internal static partial void LogEndpointListUpdatedTrace(this ILogger 
logger, string added, string removed);
 }
diff --git a/modules/platforms/dotnet/Apache.Ignite/Internal/SocketEndpoint.cs 
b/modules/platforms/dotnet/Apache.Ignite/Internal/SocketEndpoint.cs
index a476ea123af..28461d3fd6d 100644
--- a/modules/platforms/dotnet/Apache.Ignite/Internal/SocketEndpoint.cs
+++ b/modules/platforms/dotnet/Apache.Ignite/Internal/SocketEndpoint.cs
@@ -28,6 +28,8 @@ namespace Apache.Ignite.Internal
     {
         private volatile ClientSocket? _socket;
 
+        private volatile bool _isAbandoned;
+
         /// <summary>
         /// Initializes a new instance of the <see cref="SocketEndpoint"/> 
class.
         /// </summary>
@@ -42,11 +44,10 @@ namespace Apache.Ignite.Internal
             // Cache endpoint string for metrics and logging.
             EndPointString = endPoint.ToString();
 
-            MetricsContext = new MetricsContext(new[]
-            {
+            MetricsContext = new MetricsContext([
                 new KeyValuePair<string, object?>(MetricTags.ClientId, 
clientId),
                 new KeyValuePair<string, object?>(MetricTags.NodeAddress, 
EndPointString)
-            });
+            ]);
         }
 
         /// <summary>
@@ -86,5 +87,16 @@ namespace Apache.Ignite.Internal
         /// Gets the metrics context.
         /// </summary>
         public MetricsContext MetricsContext { get; }
+
+        /// <summary>
+        /// Gets or sets a value indicating whether this endpoint has been 
abandoned:
+        /// it is no longer present in the list of discovered endpoints and 
should be removed once disconnected.
+        /// We don't remove it immediately to avoid breaking in-flight 
operations.
+        /// </summary>
+        public bool IsAbandoned
+        {
+            get => _isAbandoned;
+            set => _isAbandoned = value;
+        }
     }
 }
diff --git 
a/modules/platforms/dotnet/Apache.Ignite/Internal/IClientSocketEventListener.cs 
b/modules/platforms/dotnet/Apache.Ignite/Internal/SocketEndpointComparer.cs
similarity index 53%
copy from 
modules/platforms/dotnet/Apache.Ignite/Internal/IClientSocketEventListener.cs
copy to 
modules/platforms/dotnet/Apache.Ignite/Internal/SocketEndpointComparer.cs
index 3dbbd4bb95b..3967d4d5b2a 100644
--- 
a/modules/platforms/dotnet/Apache.Ignite/Internal/IClientSocketEventListener.cs
+++ b/modules/platforms/dotnet/Apache.Ignite/Internal/SocketEndpointComparer.cs
@@ -17,20 +17,40 @@
 
 namespace Apache.Ignite.Internal;
 
+using System;
+using System.Collections.Generic;
+
 /// <summary>
-/// <see cref="ClientSocket"/> event listener.
+/// Equality comparer for <see cref="SocketEndpoint"/>.
 /// </summary>
-internal interface IClientSocketEventListener
+internal sealed class SocketEndpointComparer : 
IEqualityComparer<SocketEndpoint>
 {
-    /// <summary>
-    /// Called when partition assignment changes.
-    /// </summary>
-    /// <param name="timestamp">Timestamp.</param>
-    void OnAssignmentChanged(long timestamp);
-
-    /// <summary>
-    /// Called when observable timestamp changes.
-    /// </summary>
-    /// <param name="timestamp">Timestamp.</param>
-    void OnObservableTimestampChanged(long timestamp);
+    /// <inheritdoc/>
+    public bool Equals(SocketEndpoint? x, SocketEndpoint? y)
+    {
+        if (ReferenceEquals(x, y))
+        {
+            return true;
+        }
+
+        if (x is null)
+        {
+            return false;
+        }
+
+        if (y is null)
+        {
+            return false;
+        }
+
+        if (x.GetType() != y.GetType())
+        {
+            return false;
+        }
+
+        return x.EndPoint.Equals(y.EndPoint) && x.Host == y.Host;
+    }
+
+    /// <inheritdoc/>
+    public int GetHashCode(SocketEndpoint obj) => 
HashCode.Combine(obj.EndPoint, obj.Host);
 }
diff --git 
a/modules/platforms/dotnet/Apache.Ignite/Sql/IgniteDbConnectionStringBuilder.cs 
b/modules/platforms/dotnet/Apache.Ignite/Sql/IgniteDbConnectionStringBuilder.cs
index cb49013fbbd..27cffd343fb 100644
--- 
a/modules/platforms/dotnet/Apache.Ignite/Sql/IgniteDbConnectionStringBuilder.cs
+++ 
b/modules/platforms/dotnet/Apache.Ignite/Sql/IgniteDbConnectionStringBuilder.cs
@@ -43,7 +43,8 @@ public sealed class IgniteDbConnectionStringBuilder : 
DbConnectionStringBuilder
         nameof(ReconnectInterval),
         nameof(SslEnabled),
         nameof(Username),
-        nameof(Password)
+        nameof(Password),
+        nameof(ReResolveAddressesInterval)
     };
 
     /// <summary>
@@ -89,7 +90,7 @@ public sealed class IgniteDbConnectionStringBuilder : 
DbConnectionStringBuilder
     }
 
     /// <summary>
-    /// Gets or sets the socket timeout. See <see 
cref="IgniteClientConfiguration.OperationTimeout"/> for more details.
+    /// Gets or sets the operation timeout. See <see 
cref="IgniteClientConfiguration.OperationTimeout"/> for more details.
     /// </summary>
     public TimeSpan OperationTimeout
     {
@@ -148,6 +149,17 @@ public sealed class IgniteDbConnectionStringBuilder : 
DbConnectionStringBuilder
         set => this[nameof(Password)] = value;
     }
 
+    /// <summary>
+    /// Gets or sets the re-resolve interval. See <see 
cref="IgniteClientConfiguration.ReResolveAddressesInterval"/> for more details.
+    /// </summary>
+    public TimeSpan ReResolveAddressesInterval
+    {
+        get => GetString(nameof(ReResolveAddressesInterval)) is { } s
+            ? TimeSpan.Parse(s, CultureInfo.InvariantCulture)
+            : IgniteClientConfiguration.DefaultReResolveAddressesInterval;
+        set => this[nameof(ReResolveAddressesInterval)] = value.ToString();
+    }
+
     /// <inheritdoc />
     [AllowNull]
     public override object this[string keyword]
@@ -181,7 +193,8 @@ public sealed class IgniteDbConnectionStringBuilder : 
DbConnectionStringBuilder
             {
                 Username = Username ?? string.Empty,
                 Password = Password ?? string.Empty
-            }
+            },
+            ReResolveAddressesInterval = ReResolveAddressesInterval
         };
     }
 

Reply via email to