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
};
}