|
1 | 1 | using System.Collections.Concurrent;
|
| 2 | +using System.Diagnostics; |
| 3 | +using System.Net; |
2 | 4 | using System.Security.Authentication;
|
3 | 5 | using MySqlConnector.Logging;
|
4 | 6 | using MySqlConnector.Protocol.Serialization;
|
5 | 7 | using MySqlConnector.Utilities;
|
6 | 8 |
|
7 | 9 | namespace MySqlConnector.Core;
|
8 | 10 |
|
9 |
| -internal sealed class ConnectionPool |
| 11 | +internal sealed class ConnectionPool : IDisposable |
10 | 12 | {
|
11 | 13 | public int Id { get; }
|
12 | 14 |
|
@@ -219,6 +221,30 @@ public async Task ReapAsync(IOBehavior ioBehavior, CancellationToken cancellatio
|
219 | 221 | return procedureCache;
|
220 | 222 | }
|
221 | 223 |
|
| 224 | + public void Dispose() |
| 225 | + { |
| 226 | + Log.Debug("Pool{0} disposing connection pool", m_logArguments); |
| 227 | +#if NET6_0_OR_GREATER |
| 228 | + m_dnsCheckTimer?.Dispose(); |
| 229 | + m_dnsCheckTimer = null; |
| 230 | + m_reaperTimer?.Dispose(); |
| 231 | + m_reaperTimer = null; |
| 232 | +#else |
| 233 | + if (m_dnsCheckTimer is not null) |
| 234 | + { |
| 235 | + using var dnsCheckWaitHandle = new ManualResetEvent(false); |
| 236 | + m_dnsCheckTimer.Dispose(dnsCheckWaitHandle); |
| 237 | + dnsCheckWaitHandle.WaitOne(); |
| 238 | + } |
| 239 | + if (m_reaperTimer is not null) |
| 240 | + { |
| 241 | + using var reaperWaitHandle = new ManualResetEvent(false); |
| 242 | + m_reaperTimer.Dispose(reaperWaitHandle); |
| 243 | + reaperWaitHandle.WaitOne(); |
| 244 | + } |
| 245 | +#endif |
| 246 | + } |
| 247 | + |
222 | 248 | /// <summary>
|
223 | 249 | /// Examines all the <see cref="ServerSession"/> objects in <see cref="m_leasedSessions"/> to determine if any
|
224 | 250 | /// have an owning <see cref="MySqlConnection"/> that has been garbage-collected. If so, assumes that the connection
|
@@ -438,6 +464,7 @@ private async ValueTask<ServerSession> ConnectSessionAsync(MySqlConnection conne
|
438 | 464 | var connectionSettings = new ConnectionSettings(connectionStringBuilder);
|
439 | 465 | var pool = new ConnectionPool(connectionSettings);
|
440 | 466 | pool.StartReaperTask();
|
| 467 | + pool.StartDnsCheckTimer(); |
441 | 468 | return pool;
|
442 | 469 | }
|
443 | 470 |
|
@@ -485,6 +512,7 @@ private async ValueTask<ServerSession> ConnectSessionAsync(MySqlConnection conne
|
485 | 512 | {
|
486 | 513 | s_mruCache = new(connectionString, pool);
|
487 | 514 | pool.StartReaperTask();
|
| 515 | + pool.StartDnsCheckTimer(); |
488 | 516 |
|
489 | 517 | // if we won the race to create the new pool, also store it under the original connection string
|
490 | 518 | if (connectionString != normalizedConnectionString)
|
@@ -546,27 +574,136 @@ private ConnectionPool(ConnectionSettings cs)
|
546 | 574 |
|
547 | 575 | private void StartReaperTask()
|
548 | 576 | {
|
549 |
| - if (ConnectionSettings.ConnectionIdleTimeout > 0) |
| 577 | + if (ConnectionSettings.ConnectionIdleTimeout <= 0) |
| 578 | + return; |
| 579 | + |
| 580 | + var reaperInterval = TimeSpan.FromSeconds(Math.Max(1, Math.Min(60, ConnectionSettings.ConnectionIdleTimeout / 2))); |
| 581 | + |
| 582 | +#if NET6_0_OR_GREATER |
| 583 | + m_reaperTimer = new PeriodicTimer(reaperInterval); |
| 584 | + _ = RunTimer(); |
| 585 | + |
| 586 | + async Task RunTimer() |
| 587 | + { |
| 588 | + while (await m_reaperTimer.WaitForNextTickAsync().ConfigureAwait(false)) |
| 589 | + { |
| 590 | + try |
| 591 | + { |
| 592 | + using var source = new CancellationTokenSource(reaperInterval); |
| 593 | + await ReapAsync(IOBehavior.Asynchronous, source.Token).ConfigureAwait(false); |
| 594 | + } |
| 595 | + catch |
| 596 | + { |
| 597 | + // do nothing; we'll try to reap again |
| 598 | + } |
| 599 | + } |
| 600 | + } |
| 601 | +#else |
| 602 | + m_reaperTimer = new Timer(t => |
| 603 | + { |
| 604 | + var stopwatch = Stopwatch.StartNew(); |
| 605 | + try |
| 606 | + { |
| 607 | + using var source = new CancellationTokenSource(reaperInterval); |
| 608 | + ReapAsync(IOBehavior.Synchronous, source.Token).GetAwaiter().GetResult(); |
| 609 | + } |
| 610 | + catch |
| 611 | + { |
| 612 | + // do nothing; we'll try to reap again |
| 613 | + } |
| 614 | + |
| 615 | + // restart the timer, accounting for the time spent reaping |
| 616 | + var delay = reaperInterval - stopwatch.Elapsed; |
| 617 | + ((Timer) t!).Change(delay < TimeSpan.Zero ? TimeSpan.Zero : delay, TimeSpan.FromMilliseconds(-1)); |
| 618 | + }); |
| 619 | + m_reaperTimer.Change(reaperInterval, TimeSpan.FromMilliseconds(-1)); |
| 620 | +#endif |
| 621 | + } |
| 622 | + |
| 623 | + private void StartDnsCheckTimer() |
| 624 | + { |
| 625 | + if (ConnectionSettings.ConnectionProtocol != MySqlConnectionProtocol.Tcp || ConnectionSettings.DnsCheckInterval <= 0) |
| 626 | + return; |
| 627 | + |
| 628 | + var hostNames = ConnectionSettings.HostNames!; |
| 629 | + var hostAddresses = new IPAddress[hostNames.Count][]; |
| 630 | + |
| 631 | +#if NET6_0_OR_GREATER |
| 632 | + m_dnsCheckTimer = new PeriodicTimer(TimeSpan.FromSeconds(ConnectionSettings.DnsCheckInterval)); |
| 633 | + _ = RunTimer(); |
| 634 | + |
| 635 | + async Task RunTimer() |
550 | 636 | {
|
551 |
| - var reaperInterval = TimeSpan.FromSeconds(Math.Max(1, Math.Min(60, ConnectionSettings.ConnectionIdleTimeout / 2))); |
552 |
| - m_reaperTask = Task.Run(async () => |
| 637 | + while (await m_dnsCheckTimer.WaitForNextTickAsync().ConfigureAwait(false)) |
553 | 638 | {
|
554 |
| - while (true) |
| 639 | + Log.Trace("Pool{0} checking for DNS changes", m_logArguments); |
| 640 | + var hostNamesChanged = false; |
| 641 | + for (var hostNameIndex = 0; hostNameIndex < hostNames.Count; hostNameIndex++) |
555 | 642 | {
|
556 |
| - var task = Task.Delay(reaperInterval); |
557 | 643 | try
|
558 | 644 | {
|
559 |
| - using var source = new CancellationTokenSource(reaperInterval); |
560 |
| - await ReapAsync(IOBehavior.Asynchronous, source.Token).ConfigureAwait(false); |
| 645 | + var ipAddresses = await Dns.GetHostAddressesAsync(hostNames[hostNameIndex]).ConfigureAwait(false); |
| 646 | + if (hostAddresses[hostNameIndex] is null) |
| 647 | + { |
| 648 | + hostAddresses[hostNameIndex] = ipAddresses; |
| 649 | + } |
| 650 | + else if (hostAddresses[hostNameIndex].Except(ipAddresses).Any()) |
| 651 | + { |
| 652 | + 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)); |
| 653 | + hostAddresses[hostNameIndex] = ipAddresses; |
| 654 | + hostNamesChanged = true; |
| 655 | + } |
561 | 656 | }
|
562 |
| - catch |
| 657 | + catch (Exception ex) |
563 | 658 | {
|
564 |
| - // do nothing; we'll try to reap again |
| 659 | + // do nothing; we'll try again later |
| 660 | + Log.Debug("Pool{0} DNS check failed; ignoring HostName '{1}': {2}", m_logArguments[0], hostNames[hostNameIndex], ex.Message); |
565 | 661 | }
|
566 |
| - await task.ConfigureAwait(false); |
567 | 662 | }
|
568 |
| - }); |
| 663 | + if (hostNamesChanged) |
| 664 | + { |
| 665 | + Log.Info("Pool{0} clearing pool due to DNS changes", m_logArguments); |
| 666 | + await ClearAsync(IOBehavior.Asynchronous, CancellationToken.None).ConfigureAwait(false); |
| 667 | + } |
| 668 | + } |
569 | 669 | }
|
| 670 | +#else |
| 671 | + var interval = Math.Min(int.MaxValue / 1000, ConnectionSettings.DnsCheckInterval) * 1000; |
| 672 | + m_dnsCheckTimer = new Timer(t => |
| 673 | + { |
| 674 | + Log.Trace("Pool{0} checking for DNS changes", m_logArguments); |
| 675 | + var hostNamesChanged = false; |
| 676 | + for (var hostNameIndex = 0; hostNameIndex < hostNames.Count; hostNameIndex++) |
| 677 | + { |
| 678 | + try |
| 679 | + { |
| 680 | + var ipAddresses = Dns.GetHostAddresses(hostNames[hostNameIndex]); |
| 681 | + if (hostAddresses[hostNameIndex] is null) |
| 682 | + { |
| 683 | + hostAddresses[hostNameIndex] = ipAddresses; |
| 684 | + } |
| 685 | + else if (hostAddresses[hostNameIndex].Except(ipAddresses).Any()) |
| 686 | + { |
| 687 | + 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)); |
| 688 | + hostAddresses[hostNameIndex] = ipAddresses; |
| 689 | + hostNamesChanged = true; |
| 690 | + } |
| 691 | + } |
| 692 | + catch (Exception ex) |
| 693 | + { |
| 694 | + // do nothing; we'll try again later |
| 695 | + Log.Debug("Pool{0} DNS check failed; ignoring HostName '{1}': {2}", m_logArguments[0], hostNames[hostNameIndex], ex.Message); |
| 696 | + } |
| 697 | + } |
| 698 | + if (hostNamesChanged) |
| 699 | + { |
| 700 | + Log.Info("Pool{0} clearing pool due to DNS changes", m_logArguments); |
| 701 | + ClearAsync(IOBehavior.Synchronous, CancellationToken.None).GetAwaiter().GetResult(); |
| 702 | + } |
| 703 | + ((Timer) t!).Change(interval, -1); |
| 704 | + }); |
| 705 | + m_dnsCheckTimer.Change(interval, -1); |
| 706 | +#endif |
570 | 707 | }
|
571 | 708 |
|
572 | 709 | private void AdjustHostConnectionCount(ServerSession session, int delta)
|
@@ -626,8 +763,14 @@ private static void OnAppDomainShutDown(object? sender, EventArgs e) =>
|
626 | 763 | private readonly Dictionary<string, int>? m_hostSessions;
|
627 | 764 | private readonly object[] m_logArguments;
|
628 | 765 | private int m_generation;
|
629 |
| - private Task? m_reaperTask; |
630 | 766 | private uint m_lastRecoveryTime;
|
631 | 767 | private int m_lastSessionId;
|
632 | 768 | private Dictionary<string, CachedProcedure?>? m_procedureCache;
|
| 769 | +#if NET6_0_OR_GREATER |
| 770 | + private PeriodicTimer? m_dnsCheckTimer; |
| 771 | + private PeriodicTimer? m_reaperTimer; |
| 772 | +#else |
| 773 | + private Timer? m_dnsCheckTimer; |
| 774 | + private Timer? m_reaperTimer; |
| 775 | +#endif |
633 | 776 | }
|
0 commit comments