From 5dd9d678d9335c5ea3ea25ed1b0ca287de719d1f Mon Sep 17 00:00:00 2001 From: Philo Date: Tue, 16 Sep 2025 13:57:11 -0700 Subject: [PATCH] Add DefaultOptionsProvider.AfterDisconnectAsync() --- docs/ReleaseNotes.md | 2 +- .../Configuration/DefaultOptionsProvider.cs | 6 +++++ .../ConfigurationOptions.cs | 10 +++++--- .../ConnectionMultiplexer.cs | 10 ++++++++ .../PublicAPI/PublicAPI.Shipped.txt | 1 + .../DefaultOptionsTests.cs | 25 +++++++++++++++++++ 6 files changed, 49 insertions(+), 5 deletions(-) diff --git a/docs/ReleaseNotes.md b/docs/ReleaseNotes.md index 893ec2de0..6fe54fcfe 100644 --- a/docs/ReleaseNotes.md +++ b/docs/ReleaseNotes.md @@ -8,7 +8,7 @@ Current package versions: ## Unreleased -- (none) +- Add overrideable `AfterDisconnectAsync()` callback on `DefaultOptionsProvider` ([#2952 by philon-msft](https://github.com/StackExchange/StackExchange.Redis/pull/2952)) ## 2.9.17 diff --git a/src/StackExchange.Redis/Configuration/DefaultOptionsProvider.cs b/src/StackExchange.Redis/Configuration/DefaultOptionsProvider.cs index 703adbcac..261a99e1e 100644 --- a/src/StackExchange.Redis/Configuration/DefaultOptionsProvider.cs +++ b/src/StackExchange.Redis/Configuration/DefaultOptionsProvider.cs @@ -307,6 +307,12 @@ protected virtual string GetDefaultClientName() => /// The logger for the connection, to emit to the connection output log. public virtual Task AfterConnectAsync(ConnectionMultiplexer multiplexer, Action log) => Task.CompletedTask; + /// + /// The action to perform, if any, immediately after a connection is closed. + /// + /// The multiplexer that just disconnected. + public virtual Task AfterDisconnectAsync(ConnectionMultiplexer multiplexer) => Task.CompletedTask; + /// /// Gets the default SSL "enabled or not" based on a set of endpoints. /// Note: this setting then applies for *all* endpoints. diff --git a/src/StackExchange.Redis/ConfigurationOptions.cs b/src/StackExchange.Redis/ConfigurationOptions.cs index c0021f024..731b83289 100644 --- a/src/StackExchange.Redis/ConfigurationOptions.cs +++ b/src/StackExchange.Redis/ConfigurationOptions.cs @@ -210,6 +210,8 @@ public DefaultOptionsProvider Defaults internal Func, Task> AfterConnectAsync => Defaults.AfterConnectAsync; + internal Func AfterDisconnectAsync => Defaults.AfterDisconnectAsync; + /// /// Gets or sets whether connect/configuration timeouts should be explicitly notified via a TimeoutException. /// @@ -305,8 +307,8 @@ public bool HighIntegrity /// /// Supply a user certificate from a PEM file pair and enable TLS. /// - /// The path for the the user certificate (commonly a .crt file). - /// The path for the the user key (commonly a .key file). + /// The path for the user certificate (commonly a .crt file). + /// The path for the user key (commonly a .key file). public void SetUserPemCertificate(string userCertificatePath, string? userKeyPath = null) { CertificateSelectionCallback = CreatePemUserCertificateCallback(userCertificatePath, userKeyPath); @@ -317,7 +319,7 @@ public void SetUserPemCertificate(string userCertificatePath, string? userKeyPat /// /// Supply a user certificate from a PFX file and optional password and enable TLS. /// - /// The path for the the user certificate (commonly a .pfx file). + /// The path for the user certificate (commonly a .pfx file). /// The password for the certificate file. public void SetUserPfxCertificate(string userCertificatePath, string? password = null) { @@ -383,7 +385,7 @@ private static bool CheckTrustedIssuer(X509Certificate2 certificateToValidate, X chain.ChainPolicy.VerificationFlags = X509VerificationFlags.AllowUnknownCertificateAuthority; chain.ChainPolicy.VerificationTime = chainToValidate?.ChainPolicy?.VerificationTime ?? DateTime.Now; chain.ChainPolicy.UrlRetrievalTimeout = new TimeSpan(0, 0, 0); - // Ensure entended key usage checks are run and that we're observing a server TLS certificate + // Ensure intended key usage checks are run and that we're observing a server TLS certificate chain.ChainPolicy.ApplicationPolicy.Add(_serverAuthOid); chain.ChainPolicy.ExtraStore.Add(authority); diff --git a/src/StackExchange.Redis/ConnectionMultiplexer.cs b/src/StackExchange.Redis/ConnectionMultiplexer.cs index bf6b66674..56c1bbc0b 100644 --- a/src/StackExchange.Redis/ConnectionMultiplexer.cs +++ b/src/StackExchange.Redis/ConnectionMultiplexer.cs @@ -2294,9 +2294,11 @@ public void Close(bool allowCommandsToComplete = true) var quits = QuitAllServers(); WaitAllIgnoreErrors(quits); } + DisposeAndClearServers(); OnCloseReaderWriter(); OnClosing(true); + RawConfig.AfterDisconnectAsync?.Invoke(this).Wait(SyncConnectTimeout(true)); Interlocked.Increment(ref _connectionCloseCount); } @@ -2306,7 +2308,11 @@ public void Close(bool allowCommandsToComplete = true) /// Whether to allow all in-queue commands to complete first. public async Task CloseAsync(bool allowCommandsToComplete = true) { + if (_isDisposed) return; + + OnClosing(false); _isDisposed = true; + _profilingSessionProvider = null; using (var tmp = pulse) { pulse = null; @@ -2319,6 +2325,10 @@ public async Task CloseAsync(bool allowCommandsToComplete = true) } DisposeAndClearServers(); + OnCloseReaderWriter(); + OnClosing(true); + await RawConfig.AfterDisconnectAsync(this).ForAwait(); + Interlocked.Increment(ref _connectionCloseCount); } private void DisposeAndClearServers() diff --git a/src/StackExchange.Redis/PublicAPI/PublicAPI.Shipped.txt b/src/StackExchange.Redis/PublicAPI/PublicAPI.Shipped.txt index 10044dc9b..43413ecd2 100644 --- a/src/StackExchange.Redis/PublicAPI/PublicAPI.Shipped.txt +++ b/src/StackExchange.Redis/PublicAPI/PublicAPI.Shipped.txt @@ -1848,6 +1848,7 @@ static StackExchange.Redis.StreamPosition.Beginning.get -> StackExchange.Redis.R static StackExchange.Redis.StreamPosition.NewMessages.get -> StackExchange.Redis.RedisValue virtual StackExchange.Redis.Configuration.DefaultOptionsProvider.AbortOnConnectFail.get -> bool virtual StackExchange.Redis.Configuration.DefaultOptionsProvider.AfterConnectAsync(StackExchange.Redis.ConnectionMultiplexer! multiplexer, System.Action! log) -> System.Threading.Tasks.Task! +virtual StackExchange.Redis.Configuration.DefaultOptionsProvider.AfterDisconnectAsync(StackExchange.Redis.ConnectionMultiplexer! multiplexer) -> System.Threading.Tasks.Task! virtual StackExchange.Redis.Configuration.DefaultOptionsProvider.AllowAdmin.get -> bool virtual StackExchange.Redis.Configuration.DefaultOptionsProvider.BacklogPolicy.get -> StackExchange.Redis.BacklogPolicy! virtual StackExchange.Redis.Configuration.DefaultOptionsProvider.CheckCertificateRevocation.get -> bool diff --git a/tests/StackExchange.Redis.Tests/DefaultOptionsTests.cs b/tests/StackExchange.Redis.Tests/DefaultOptionsTests.cs index be80fd9c5..81223adbd 100644 --- a/tests/StackExchange.Redis.Tests/DefaultOptionsTests.cs +++ b/tests/StackExchange.Redis.Tests/DefaultOptionsTests.cs @@ -151,6 +151,31 @@ public async Task AfterConnectAsyncHandler() Assert.Equal(1, provider.Calls); } + public class TestAfterDisconnectOptionsProvider : DefaultOptionsProvider + { + public int Calls; + + public override Task AfterDisconnectAsync(ConnectionMultiplexer muxer) + { + Interlocked.Increment(ref Calls); + return Task.CompletedTask; + } + } + + [Fact] + public async Task AfterDisconnectAsyncHandler() + { + var options = ConfigurationOptions.Parse(GetConfiguration()); + var provider = new TestAfterDisconnectOptionsProvider(); + options.Defaults = provider; + + await using var conn = await ConnectionMultiplexer.ConnectAsync(options, Writer); + await conn.CloseAsync(); + + Assert.False(conn.IsConnected); + Assert.Equal(1, provider.Calls); + } + public class TestClientNameOptionsProvider : DefaultOptionsProvider { protected override string GetDefaultClientName() => "Hey there";