Skip to content

Commit 6ebfc50

Browse files
committed
Add "DNS Check Interval" option. Fixes #1201
1 parent 305c621 commit 6ebfc50

File tree

5 files changed

+131
-1
lines changed

5 files changed

+131
-1
lines changed

docs/content/connection-options.md

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -232,6 +232,15 @@ Connection pooling is enabled by default. These options are used to configure it
232232
<td>100</td>
233233
<td>The maximum number of connections allowed in the pool.</td>
234234
</tr>
235+
<tr id="DnsCheckInterval">
236+
<td>DNS Check Interval, DnsCheckInterval</td>
237+
<td>0</td>
238+
<td>The number of seconds between checks for DNS changes, or 0 to disable periodic checks.
239+
If the periodic check determines that one of the <code>Server</code> hostnames resolves to a different IP address, the pool will be cleared.
240+
This is useful in HA scenarios where failover is accomplished by changing the IP address to which a hostname resolves.
241+
Existing connections in the pool may have valid TCP connections to a server that is no longer responding or has been marked readonly;
242+
clearing the pool (when DNS changes) forces all these existing connections to be reestablished.</td>
243+
</tr>
235244
</table>
236245

237246
## Other Options

src/MySqlConnector/Core/ConnectionPool.cs

Lines changed: 96 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,15 @@
11
using System.Collections.Concurrent;
2+
using System.Net;
23
using System.Security.Authentication;
34
using MySqlConnector.Logging;
45
using MySqlConnector.Protocol.Serialization;
56
using MySqlConnector.Utilities;
67

78
namespace MySqlConnector.Core;
89

10+
#pragma warning disable CA1001 // Types that own disposable fields should be disposable
911
internal sealed class ConnectionPool
12+
#pragma warning restore CA1001 // Types that own disposable fields should be disposable
1013
{
1114
public int Id { get; }
1215

@@ -438,6 +441,7 @@ private async ValueTask<ServerSession> ConnectSessionAsync(MySqlConnection conne
438441
var connectionSettings = new ConnectionSettings(connectionStringBuilder);
439442
var pool = new ConnectionPool(connectionSettings);
440443
pool.StartReaperTask();
444+
pool.StartDnsCheckTimer();
441445
return pool;
442446
}
443447

@@ -485,6 +489,7 @@ private async ValueTask<ServerSession> ConnectSessionAsync(MySqlConnection conne
485489
{
486490
s_mruCache = new(connectionString, pool);
487491
pool.StartReaperTask();
492+
pool.StartDnsCheckTimer();
488493

489494
// if we won the race to create the new pool, also store it under the original connection string
490495
if (connectionString != normalizedConnectionString)
@@ -569,6 +574,92 @@ private void StartReaperTask()
569574
}
570575
}
571576

577+
private void StartDnsCheckTimer()
578+
{
579+
if (ConnectionSettings.ConnectionProtocol != MySqlConnectionProtocol.Tcp || ConnectionSettings.DnsCheckInterval <= 0)
580+
return;
581+
582+
var hostNames = ConnectionSettings.HostNames!;
583+
var hostAddresses = new IPAddress[hostNames.Count][];
584+
585+
#if NET6_0_OR_GREATER
586+
m_dnsCheckTimer = new PeriodicTimer(TimeSpan.FromSeconds(ConnectionSettings.DnsCheckInterval));
587+
_ = RunTimer();
588+
589+
async Task RunTimer()
590+
{
591+
while (await m_dnsCheckTimer.WaitForNextTickAsync().ConfigureAwait(false))
592+
{
593+
Log.Trace("Pool{0} checking for DNS changes", m_logArguments);
594+
var hostNamesChanged = false;
595+
for (var hostNameIndex = 0; hostNameIndex < hostNames.Count; hostNameIndex++)
596+
{
597+
try
598+
{
599+
var ipAddresses = await Dns.GetHostAddressesAsync(hostNames[hostNameIndex]).ConfigureAwait(false);
600+
if (hostAddresses[hostNameIndex] is null)
601+
{
602+
hostAddresses[hostNameIndex] = ipAddresses;
603+
}
604+
else if (hostAddresses[hostNameIndex].Except(ipAddresses).Any())
605+
{
606+
Log.Debug("Pool{0} detected DNS change for HostName '{1}': {2} to {3}", m_logArguments[0], hostNames[hostNameIndex], string.Join<IPAddress>(',', hostAddresses[hostNameIndex]), string.Join<IPAddress>(',', ipAddresses));
607+
hostAddresses[hostNameIndex] = ipAddresses;
608+
hostNamesChanged = true;
609+
}
610+
}
611+
catch (Exception ex)
612+
{
613+
// do nothing; we'll try again later
614+
Log.Debug("Pool{0} DNS check failed; ignoring HostName '{1}': {2}", m_logArguments[0], hostNames[hostNameIndex], ex.Message);
615+
}
616+
}
617+
if (hostNamesChanged)
618+
{
619+
Log.Info("Pool{0} clearing pool due to DNS changes", m_logArguments);
620+
await ClearAsync(IOBehavior.Asynchronous, CancellationToken.None).ConfigureAwait(false);
621+
}
622+
}
623+
}
624+
#else
625+
var interval = Math.Min(int.MaxValue / 1000, ConnectionSettings.DnsCheckInterval) * 1000;
626+
m_dnsCheckTimer = new Timer(t =>
627+
{
628+
Log.Trace("Pool{0} checking for DNS changes", m_logArguments);
629+
var hostNamesChanged = false;
630+
for (var hostNameIndex = 0; hostNameIndex < hostNames.Count; hostNameIndex++)
631+
{
632+
try
633+
{
634+
var ipAddresses = Dns.GetHostAddresses(hostNames[hostNameIndex]);
635+
if (hostAddresses[hostNameIndex] is null)
636+
{
637+
hostAddresses[hostNameIndex] = ipAddresses;
638+
}
639+
else if (hostAddresses[hostNameIndex].Except(ipAddresses).Any())
640+
{
641+
Log.Debug("Pool{0} detected DNS change for HostName '{1}': {2} to {3}", m_logArguments[0], hostNames[hostNameIndex], string.Join<IPAddress>(",", hostAddresses[hostNameIndex]), string.Join<IPAddress>(",", ipAddresses));
642+
hostAddresses[hostNameIndex] = ipAddresses;
643+
hostNamesChanged = true;
644+
}
645+
}
646+
catch (Exception ex)
647+
{
648+
// do nothing; we'll try again later
649+
Log.Debug("Pool{0} DNS check failed; ignoring HostName '{1}': {2}", m_logArguments[0], hostNames[hostNameIndex], ex.Message);
650+
}
651+
}
652+
if (hostNamesChanged)
653+
{
654+
Log.Info("Pool{0} clearing pool due to DNS changes", m_logArguments);
655+
ClearAsync(IOBehavior.Synchronous, CancellationToken.None).GetAwaiter().GetResult();
656+
}
657+
((Timer) t!).Change(interval, -1);
658+
});
659+
m_dnsCheckTimer.Change(interval, -1);
660+
#endif
661+
}
662+
572663
private void AdjustHostConnectionCount(ServerSession session, int delta)
573664
{
574665
if (m_hostSessions is not null)
@@ -630,4 +721,9 @@ private static void OnAppDomainShutDown(object? sender, EventArgs e) =>
630721
private uint m_lastRecoveryTime;
631722
private int m_lastSessionId;
632723
private Dictionary<string, CachedProcedure?>? m_procedureCache;
724+
#if NET6_0_OR_GREATER
725+
private PeriodicTimer? m_dnsCheckTimer;
726+
#else
727+
private Timer? m_dnsCheckTimer;
728+
#endif
633729
}

src/MySqlConnector/Core/ConnectionSettings.cs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -119,6 +119,7 @@ public ConnectionSettings(MySqlConnectionStringBuilder csb)
119119
throw new MySqlException("MaximumPoolSize must be greater than or equal to MinimumPoolSize");
120120
MinimumPoolSize = ToSigned(csb.MinimumPoolSize);
121121
MaximumPoolSize = ToSigned(csb.MaximumPoolSize);
122+
DnsCheckInterval = ToSigned(csb.DnsCheckInterval);
122123

123124
// Other Options
124125
AllowLoadLocalInfile = csb.AllowLoadLocalInfile;
@@ -213,6 +214,7 @@ private static MySqlGuidFormat GetEffectiveGuidFormat(MySqlGuidFormat guidFormat
213214
public int ConnectionIdleTimeout { get; }
214215
public int MinimumPoolSize { get; }
215216
public int MaximumPoolSize { get; }
217+
public int DnsCheckInterval { get; }
216218

217219
// Other Options
218220
public bool AllowLoadLocalInfile { get; }
@@ -299,6 +301,7 @@ private ConnectionSettings(ConnectionSettings other, string host, int port, stri
299301
ConnectionIdleTimeout = other.ConnectionIdleTimeout;
300302
MinimumPoolSize = other.MinimumPoolSize;
301303
MaximumPoolSize = other.MaximumPoolSize;
304+
DnsCheckInterval = other.DnsCheckInterval;
302305

303306
AllowLoadLocalInfile = other.AllowLoadLocalInfile;
304307
AllowPublicKeyRetrieval = other.AllowPublicKeyRetrieval;

src/MySqlConnector/MySqlConnectionStringBuilder.cs

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -404,6 +404,19 @@ public uint MaximumPoolSize
404404
set => MySqlConnectionStringOption.MaximumPoolSize.SetValue(this, value);
405405
}
406406

407+
/// <summary>
408+
/// The number of seconds between checks for DNS changes, or 0 to disable periodic checks.
409+
/// </summary>
410+
[Category("Pooling")]
411+
[DefaultValue(0u)]
412+
[Description("The number of seconds between checks for DNS changes.")]
413+
[DisplayName("DNS Check Interval")]
414+
public uint DnsCheckInterval
415+
{
416+
get => MySqlConnectionStringOption.DnsCheckInterval.GetValue(this);
417+
set => MySqlConnectionStringOption.DnsCheckInterval.SetValue(this, value);
418+
}
419+
407420
// Other Options
408421

409422
/// <summary>
@@ -915,6 +928,7 @@ internal abstract partial class MySqlConnectionStringOption
915928
public static readonly MySqlConnectionStringValueOption<uint> ConnectionIdleTimeout;
916929
public static readonly MySqlConnectionStringValueOption<uint> MinimumPoolSize;
917930
public static readonly MySqlConnectionStringValueOption<uint> MaximumPoolSize;
931+
public static readonly MySqlConnectionStringValueOption<uint> DnsCheckInterval;
918932

919933
// Other Options
920934
public static readonly MySqlConnectionStringValueOption<bool> AllowLoadLocalInfile;
@@ -1118,6 +1132,10 @@ static MySqlConnectionStringOption()
11181132
keys: new[] { "Maximum Pool Size", "Max Pool Size", "MaximumPoolSize", "maxpoolsize" },
11191133
defaultValue: 100u));
11201134

1135+
AddOption(DnsCheckInterval = new(
1136+
keys: new[] { "DNS Check Interval", "DnsCheckInterval" },
1137+
defaultValue: 0u));
1138+
11211139
// Other Options
11221140
AddOption(AllowLoadLocalInfile = new(
11231141
keys: new[] { "Allow Load Local Infile", "AllowLoadLocalInfile" },

tests/MySqlConnector.Tests/MySqlConnectionStringBuilderTests.cs

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -144,6 +144,7 @@ public void ParseConnectionString()
144144
"use xa transactions=false;" +
145145
"tls cipher suites=TLS_AES_128_CCM_8_SHA256,TLS_RSA_WITH_RC4_128_MD5;" +
146146
"ignore prepare=true;" +
147+
"dnscheckinterval=15;" +
147148
#endif
148149
"interactive=true;" +
149150
"Keep Alive=90;" +
@@ -207,6 +208,7 @@ public void ParseConnectionString()
207208
Assert.False(csb.UseXaTransactions);
208209
Assert.Equal("TLS_AES_128_CCM_8_SHA256,TLS_RSA_WITH_RC4_128_MD5", csb.TlsCipherSuites);
209210
Assert.True(csb.IgnorePrepare);
211+
Assert.Equal(15u, csb.DnsCheckInterval);
210212
#endif
211213
Assert.True(csb.InteractiveSession);
212214
Assert.Equal(90u, csb.Keepalive);
@@ -239,7 +241,8 @@ public void ParseConnectionString()
239241
"Certificate Store Location=CurrentUser;Certificate Thumbprint=thumbprint123;SSL Cert=client-cert.pem;SSL Key=client-key.pem;" +
240242
"SSL CA=ca.pem;TLS Version=\"TLS 1.2, TLS 1.3\";TLS Cipher Suites=TLS_AES_128_CCM_8_SHA256,TLS_RSA_WITH_RC4_128_MD5;" +
241243
"Pooling=False;Connection Lifetime=15;Connection Reset=False;Defer Connection Reset=True;Connection Idle Timeout=30;" +
242-
"Minimum Pool Size=5;Maximum Pool Size=15;Allow Load Local Infile=True;Allow Public Key Retrieval=True;Allow User Variables=True;" +
244+
"Minimum Pool Size=5;Maximum Pool Size=15;DNS Check Interval=15;" +
245+
"Allow Load Local Infile=True;Allow Public Key Retrieval=True;Allow User Variables=True;" +
243246
"Allow Zero DateTime=True;Application Name=\"My Test Application\";Auto Enlist=False;Cancellation Timeout=-1;Character Set=latin1;" +
244247
"Connection Timeout=30;Convert Zero DateTime=True;DateTime Kind=Utc;Default Command Timeout=123;Force Synchronous=True;" +
245248
"GUID Format=TimeSwapBinary16;Ignore Command Transaction=True;Ignore Prepare=True;Interactive Session=True;Keep Alive=90;" +
@@ -525,6 +528,7 @@ public void ParseInvalidTlsVersion()
525528
[InlineData("Cancellation Timeout", 5)]
526529
[InlineData("Connection Idle Timeout", 10u)]
527530
[InlineData("DateTime Kind", MySqlDateTimeKind.Utc)]
531+
[InlineData("DNS Check Interval", 15u)]
528532
[InlineData("Force Synchronous", true)]
529533
[InlineData("GUID Format", MySqlGuidFormat.Binary16)]
530534
[InlineData("Ignore Command Transaction", true)]

0 commit comments

Comments
 (0)