From 57881cc7cd4a025eb7f9b190aa5aa123e6810a29 Mon Sep 17 00:00:00 2001 From: Reuben Bond Date: Thu, 28 May 2026 14:38:07 -0700 Subject: [PATCH 1/4] Fix Redis multiplexer ownership Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../Hosting/RedisClusteringProviderBuilder.cs | 4 +-- .../Providers/RedisClusteringOptions.cs | 11 +++++--- .../Storage/RedisMembershipTable.cs | 12 +++++++-- .../RedisGrainDirectoryProviderBuilder.cs | 2 +- .../Options/RedisGrainDirectoryOptions.cs | 10 ++++--- .../RedisGrainDirectory.cs | 27 ++++++++++++++----- .../RedisGrainStorageProviderBuilder.cs | 2 +- .../Providers/RedisStorageOptions.cs | 10 ++++--- .../Storage/RedisGrainStorage.cs | 19 ++++++++++--- .../Hosting/RedisRemindersProviderBuilder.cs | 2 +- .../Providers/RedisReminderTableOptions.cs | 10 ++++--- .../Storage/RedisReminderTable.cs | 17 ++++++++++-- .../Orleans.Clustering.Redis.cs | 4 +-- .../Orleans.GrainDirectory.Redis.cs | 4 +-- .../Orleans.Persistence.Redis.cs | 4 +-- .../Orleans.Reminders.Redis.cs | 4 +-- 16 files changed, 103 insertions(+), 39 deletions(-) diff --git a/src/Redis/Orleans.Clustering.Redis/Hosting/RedisClusteringProviderBuilder.cs b/src/Redis/Orleans.Clustering.Redis/Hosting/RedisClusteringProviderBuilder.cs index 2a22e57e063..caa6613fc87 100644 --- a/src/Redis/Orleans.Clustering.Redis/Hosting/RedisClusteringProviderBuilder.cs +++ b/src/Redis/Orleans.Clustering.Redis/Hosting/RedisClusteringProviderBuilder.cs @@ -31,7 +31,7 @@ public void Configure(ISiloBuilder builder, string name, IConfigurationSection c { // Get a connection multiplexer instance by name. var multiplexer = services.GetRequiredKeyedService(serviceKey); - options.CreateMultiplexer = _ => Task.FromResult(multiplexer); + options.CreateMultiplexer = _ => Task.FromResult((Multiplexer: multiplexer, IsShared: true)); options.ConfigurationOptions = new ConfigurationOptions(); } else @@ -64,7 +64,7 @@ public void Configure(IClientBuilder builder, string name, IConfigurationSection { // Get a connection multiplexer instance by name. var multiplexer = services.GetRequiredKeyedService(serviceKey); - options.CreateMultiplexer = _ => Task.FromResult(multiplexer); + options.CreateMultiplexer = _ => Task.FromResult((Multiplexer: multiplexer, IsShared: true)); options.ConfigurationOptions = new ConfigurationOptions(); } else diff --git a/src/Redis/Orleans.Clustering.Redis/Providers/RedisClusteringOptions.cs b/src/Redis/Orleans.Clustering.Redis/Providers/RedisClusteringOptions.cs index 76c3e7e2228..376ed054e59 100644 --- a/src/Redis/Orleans.Clustering.Redis/Providers/RedisClusteringOptions.cs +++ b/src/Redis/Orleans.Clustering.Redis/Providers/RedisClusteringOptions.cs @@ -21,9 +21,12 @@ public class RedisClusteringOptions public ConfigurationOptions ConfigurationOptions { get; set; } /// - /// The delegate used to create a Redis connection multiplexer. + /// The delegate used to create a Redis connection multiplexer and indicate whether it is shared. /// - public Func> CreateMultiplexer { get; set; } = DefaultCreateMultiplexer; + /// + /// When IsShared is , the provider will not dispose the returned multiplexer. + /// + public Func> CreateMultiplexer { get; set; } = DefaultCreateMultiplexer; /// /// The delegate used to create redis key for RedisMembershipTable. @@ -39,9 +42,9 @@ public class RedisClusteringOptions /// /// The default multiplexer creation delegate. /// - public static async Task DefaultCreateMultiplexer(RedisClusteringOptions options) + public static async Task<(IConnectionMultiplexer Multiplexer, bool IsShared)> DefaultCreateMultiplexer(RedisClusteringOptions options) { - return await ConnectionMultiplexer.ConnectAsync(options.ConfigurationOptions); + return (await ConnectionMultiplexer.ConnectAsync(options.ConfigurationOptions), false); } /// diff --git a/src/Redis/Orleans.Clustering.Redis/Storage/RedisMembershipTable.cs b/src/Redis/Orleans.Clustering.Redis/Storage/RedisMembershipTable.cs index 6ac7dccb8a2..de57e7b0a49 100644 --- a/src/Redis/Orleans.Clustering.Redis/Storage/RedisMembershipTable.cs +++ b/src/Redis/Orleans.Clustering.Redis/Storage/RedisMembershipTable.cs @@ -21,6 +21,7 @@ internal class RedisMembershipTable : IMembershipTable, IDisposable private readonly RedisKey _clusterKey; private IConnectionMultiplexer _muxer = null!; private IDatabase _db = null!; + private bool _muxerIsShared; public RedisMembershipTable(IOptions redisOptions, IOptions clusterOptions) { @@ -39,7 +40,7 @@ public async Task DeleteMembershipTableEntries(string clusterId) public async Task InitializeMembershipTable(bool tryInitTableVersion) { - _muxer = await _redisOptions.CreateMultiplexer(_redisOptions); + (_muxer, _muxerIsShared) = await _redisOptions.CreateMultiplexer(_redisOptions); _db = _muxer.GetDatabase(); if (tryInitTableVersion) @@ -215,7 +216,14 @@ public async Task CleanupDefunctSiloEntries(DateTimeOffset beforeDate) public void Dispose() { - _muxer?.Dispose(); + if (!_muxerIsShared) + { + _muxer?.Dispose(); + } + + _muxer = null!; + _db = null!; + _muxerIsShared = false; } private enum UpsertResult diff --git a/src/Redis/Orleans.GrainDirectory.Redis/Hosting/RedisGrainDirectoryProviderBuilder.cs b/src/Redis/Orleans.GrainDirectory.Redis/Hosting/RedisGrainDirectoryProviderBuilder.cs index b5010b729d2..2d7d9d183df 100644 --- a/src/Redis/Orleans.GrainDirectory.Redis/Hosting/RedisGrainDirectoryProviderBuilder.cs +++ b/src/Redis/Orleans.GrainDirectory.Redis/Hosting/RedisGrainDirectoryProviderBuilder.cs @@ -28,7 +28,7 @@ public void Configure(ISiloBuilder builder, string name, IConfigurationSection c { // Get a connection multiplexer instance by name. var multiplexer = services.GetRequiredKeyedService(serviceKey); - options.CreateMultiplexer = _ => Task.FromResult(multiplexer); + options.CreateMultiplexer = _ => Task.FromResult((Multiplexer: multiplexer, IsShared: true)); options.ConfigurationOptions = new ConfigurationOptions(); } else diff --git a/src/Redis/Orleans.GrainDirectory.Redis/Options/RedisGrainDirectoryOptions.cs b/src/Redis/Orleans.GrainDirectory.Redis/Options/RedisGrainDirectoryOptions.cs index 56814133d51..57e1a42c81b 100644 --- a/src/Redis/Orleans.GrainDirectory.Redis/Options/RedisGrainDirectoryOptions.cs +++ b/src/Redis/Orleans.GrainDirectory.Redis/Options/RedisGrainDirectoryOptions.cs @@ -19,9 +19,12 @@ public class RedisGrainDirectoryOptions public ConfigurationOptions ConfigurationOptions { get; set; } /// - /// The delegate used to create a Redis connection multiplexer. + /// The delegate used to create a Redis connection multiplexer and indicate whether it is shared. /// - public Func> CreateMultiplexer { get; set; } = DefaultCreateMultiplexer; + /// + /// When IsShared is , the provider will not dispose the returned multiplexer. + /// + public Func> CreateMultiplexer { get; set; } = DefaultCreateMultiplexer; /// /// Entry expiry, null by default. A value should be set ONLY for ephemeral environments (like in tests). @@ -32,7 +35,8 @@ public class RedisGrainDirectoryOptions /// /// The default multiplexer creation delegate. /// - public static async Task DefaultCreateMultiplexer(RedisGrainDirectoryOptions options) => await ConnectionMultiplexer.ConnectAsync(options.ConfigurationOptions); + public static async Task<(IConnectionMultiplexer Multiplexer, bool IsShared)> DefaultCreateMultiplexer(RedisGrainDirectoryOptions options) + => (Multiplexer: await ConnectionMultiplexer.ConnectAsync(options.ConfigurationOptions), IsShared: false); } internal class RedactRedisConfigurationOptions : RedactAttribute diff --git a/src/Redis/Orleans.GrainDirectory.Redis/RedisGrainDirectory.cs b/src/Redis/Orleans.GrainDirectory.Redis/RedisGrainDirectory.cs index 104a8668134..09977cbcdde 100644 --- a/src/Redis/Orleans.GrainDirectory.Redis/RedisGrainDirectory.cs +++ b/src/Redis/Orleans.GrainDirectory.Redis/RedisGrainDirectory.cs @@ -25,6 +25,7 @@ public partial class RedisGrainDirectory : IGrainDirectory, ILifecycleParticipan // Both are initialized in the Initialize method. private IConnectionMultiplexer _redis = null!; private IDatabase _database = null!; + private bool _redisIsShared; private bool _disposed; @@ -177,7 +178,7 @@ public void Participate(ISiloLifecycle lifecycle) public async Task Initialize(CancellationToken ct = default) { - _redis = await _directoryOptions.CreateMultiplexer(_directoryOptions); + (_redis, _redisIsShared) = await _directoryOptions.CreateMultiplexer(_directoryOptions); // Configure logging _redis.ConnectionRestored += LogConnectionRestored; @@ -190,14 +191,28 @@ public async Task Initialize(CancellationToken ct = default) private async Task Uninitialize(CancellationToken arg) { - if (_redis != null && _redis.IsConnected) + if (_redis != null) { _disposed = true; - await _redis.CloseAsync(); - _redis.Dispose(); - _redis = null!; - _database = null!; + try + { + _redis.ConnectionRestored -= LogConnectionRestored; + _redis.ConnectionFailed -= LogConnectionFailed; + _redis.ErrorMessage -= LogErrorMessage; + _redis.InternalError -= LogInternalError; + + if (!_redisIsShared) + { + await _redis.DisposeAsync(); + } + } + finally + { + _redis = null!; + _database = null!; + _redisIsShared = false; + } } } diff --git a/src/Redis/Orleans.Persistence.Redis/Hosting/RedisGrainStorageProviderBuilder.cs b/src/Redis/Orleans.Persistence.Redis/Hosting/RedisGrainStorageProviderBuilder.cs index f3a3d4716df..da33f29dfd7 100644 --- a/src/Redis/Orleans.Persistence.Redis/Hosting/RedisGrainStorageProviderBuilder.cs +++ b/src/Redis/Orleans.Persistence.Redis/Hosting/RedisGrainStorageProviderBuilder.cs @@ -29,7 +29,7 @@ public void Configure(ISiloBuilder builder, string name, IConfigurationSection c { // Get a connection multiplexer instance by name. var multiplexer = services.GetRequiredKeyedService(serviceKey); - options.CreateMultiplexer = _ => Task.FromResult(multiplexer); + options.CreateMultiplexer = _ => Task.FromResult((Multiplexer: multiplexer, IsShared: true)); options.ConfigurationOptions = new ConfigurationOptions(); } else diff --git a/src/Redis/Orleans.Persistence.Redis/Providers/RedisStorageOptions.cs b/src/Redis/Orleans.Persistence.Redis/Providers/RedisStorageOptions.cs index 94f1a09b23f..f1cbbf32adc 100644 --- a/src/Redis/Orleans.Persistence.Redis/Providers/RedisStorageOptions.cs +++ b/src/Redis/Orleans.Persistence.Redis/Providers/RedisStorageOptions.cs @@ -34,9 +34,12 @@ public class RedisStorageOptions : IStorageProviderSerializerOptions public ConfigurationOptions? ConfigurationOptions { get; set; } /// - /// The delegate used to create a Redis connection multiplexer. + /// The delegate used to create a Redis connection multiplexer and indicate whether it is shared. /// - public Func> CreateMultiplexer { get; set; } = DefaultCreateMultiplexer; + /// + /// When IsShared is , the provider will not dispose the returned multiplexer. + /// + public Func> CreateMultiplexer { get; set; } = DefaultCreateMultiplexer; /// /// Entry expiry, null by default. A value should be set only for ephemeral environments, such as testing environments. @@ -52,7 +55,8 @@ public class RedisStorageOptions : IStorageProviderSerializerOptions /// /// The default multiplexer creation delegate. /// - public static async Task DefaultCreateMultiplexer(RedisStorageOptions options) => await ConnectionMultiplexer.ConnectAsync(options.ConfigurationOptions!); + public static async Task<(IConnectionMultiplexer Multiplexer, bool IsShared)> DefaultCreateMultiplexer(RedisStorageOptions options) + => (Multiplexer: await ConnectionMultiplexer.ConnectAsync(options.ConfigurationOptions!), IsShared: false); } /// diff --git a/src/Redis/Orleans.Persistence.Redis/Storage/RedisGrainStorage.cs b/src/Redis/Orleans.Persistence.Redis/Storage/RedisGrainStorage.cs index a4dfa46f7db..dc2590d49d8 100644 --- a/src/Redis/Orleans.Persistence.Redis/Storage/RedisGrainStorage.cs +++ b/src/Redis/Orleans.Persistence.Redis/Storage/RedisGrainStorage.cs @@ -34,6 +34,7 @@ public partial class RedisGrainStorage : IGrainStorage, ILifecycleParticipant _getKeyFunc; private IConnectionMultiplexer _connection; private IDatabase _db; + private bool _connectionIsShared; /// /// Creates a new instance of the type. @@ -72,7 +73,7 @@ private async Task Init(CancellationToken cancellationToken) { LogDebugInitializing(_name, _serviceId, _options.DeleteStateOnClear); - _connection = await _options.CreateMultiplexer(_options).ConfigureAwait(false); + (_connection, _connectionIsShared) = await _options.CreateMultiplexer(_options).ConfigureAwait(false); _db = _connection.GetDatabase(); var elapsed = Stopwatch.GetElapsedTime(startTime); @@ -256,8 +257,20 @@ private async Task Close(CancellationToken cancellationToken) { if (_connection is null) return; - await _connection.CloseAsync().ConfigureAwait(false); - _connection.Dispose(); + try + { + if (!_connectionIsShared) + { + await _connection.CloseAsync().ConfigureAwait(false); + _connection.Dispose(); + } + } + finally + { + _connection = null; + _db = null; + _connectionIsShared = false; + } } private T CreateInstance() => _activatorProvider.GetActivator().Create(); diff --git a/src/Redis/Orleans.Reminders.Redis/Hosting/RedisRemindersProviderBuilder.cs b/src/Redis/Orleans.Reminders.Redis/Hosting/RedisRemindersProviderBuilder.cs index 25d0c19b5e1..b1f08579e2b 100644 --- a/src/Redis/Orleans.Reminders.Redis/Hosting/RedisRemindersProviderBuilder.cs +++ b/src/Redis/Orleans.Reminders.Redis/Hosting/RedisRemindersProviderBuilder.cs @@ -27,7 +27,7 @@ public void Configure(ISiloBuilder builder, string name, IConfigurationSection c { // Get a connection multiplexer instance by name. var multiplexer = services.GetRequiredKeyedService(serviceKey); - options.CreateMultiplexer = _ => Task.FromResult(multiplexer); + options.CreateMultiplexer = _ => Task.FromResult((Multiplexer: multiplexer, IsShared: true)); options.ConfigurationOptions = new ConfigurationOptions(); } else diff --git a/src/Redis/Orleans.Reminders.Redis/Providers/RedisReminderTableOptions.cs b/src/Redis/Orleans.Reminders.Redis/Providers/RedisReminderTableOptions.cs index 6aad60abeb8..a9ee387c285 100644 --- a/src/Redis/Orleans.Reminders.Redis/Providers/RedisReminderTableOptions.cs +++ b/src/Redis/Orleans.Reminders.Redis/Providers/RedisReminderTableOptions.cs @@ -20,9 +20,12 @@ public class RedisReminderTableOptions public ConfigurationOptions ConfigurationOptions { get; set; } /// - /// The delegate used to create a Redis connection multiplexer. + /// The delegate used to create a Redis connection multiplexer and indicate whether it is shared. /// - public Func> CreateMultiplexer { get; set; } = DefaultCreateMultiplexer; + /// + /// When IsShared is , the provider will not dispose the returned multiplexer. + /// + public Func> CreateMultiplexer { get; set; } = DefaultCreateMultiplexer; /// /// Entry expiry, null by default. A value should be set ONLY for ephemeral environments (like in tests). @@ -33,7 +36,8 @@ public class RedisReminderTableOptions /// /// The default multiplexer creation delegate. /// - public static async Task DefaultCreateMultiplexer(RedisReminderTableOptions options) => await ConnectionMultiplexer.ConnectAsync(options.ConfigurationOptions); + public static async Task<(IConnectionMultiplexer Multiplexer, bool IsShared)> DefaultCreateMultiplexer(RedisReminderTableOptions options) + => (Multiplexer: await ConnectionMultiplexer.ConnectAsync(options.ConfigurationOptions), IsShared: false); } internal class RedactRedisConfigurationOptions : RedactAttribute diff --git a/src/Redis/Orleans.Reminders.Redis/Storage/RedisReminderTable.cs b/src/Redis/Orleans.Reminders.Redis/Storage/RedisReminderTable.cs index 66d9f9b24f7..4b8a70dca05 100644 --- a/src/Redis/Orleans.Reminders.Redis/Storage/RedisReminderTable.cs +++ b/src/Redis/Orleans.Reminders.Redis/Storage/RedisReminderTable.cs @@ -17,7 +17,7 @@ #nullable disable namespace Orleans.Reminders.Redis { - internal partial class RedisReminderTable : IReminderTable + internal partial class RedisReminderTable : IReminderTable, IDisposable { private readonly RedisKey _hashSetKey; private readonly RedisReminderTableOptions _redisOptions; @@ -25,6 +25,7 @@ internal partial class RedisReminderTable : IReminderTable private readonly ILogger _logger; private IConnectionMultiplexer _muxer; private IDatabase _db; + private bool _muxerIsShared; public RedisReminderTable( ILogger logger, @@ -42,7 +43,7 @@ public async Task Init() { try { - _muxer = await _redisOptions.CreateMultiplexer(_redisOptions); + (_muxer, _muxerIsShared) = await _redisOptions.CreateMultiplexer(_redisOptions); _db = _muxer.GetDatabase(); if (_redisOptions.EntryExpiry is { } expiry) @@ -179,6 +180,18 @@ public async Task UpsertRow(ReminderEntry entry) } } + public void Dispose() + { + if (!_muxerIsShared) + { + _muxer?.Dispose(); + } + + _muxer = null; + _db = null; + _muxerIsShared = false; + } + private static ReminderEntry ConvertToEntry(string reminderValue) { string[] segments = RedisReminderSerializer.DeserializeMember(reminderValue); diff --git a/src/api/Redis/Orleans.Clustering.Redis/Orleans.Clustering.Redis.cs b/src/api/Redis/Orleans.Clustering.Redis/Orleans.Clustering.Redis.cs index 75768ce3b49..29468ccc101 100644 --- a/src/api/Redis/Orleans.Clustering.Redis/Orleans.Clustering.Redis.cs +++ b/src/api/Redis/Orleans.Clustering.Redis/Orleans.Clustering.Redis.cs @@ -41,13 +41,13 @@ public partial class RedisClusteringOptions { public StackExchange.Redis.ConfigurationOptions ConfigurationOptions { get { throw null; } set { } } - public System.Func> CreateMultiplexer { get { throw null; } set { } } + public System.Func> CreateMultiplexer { get { throw null; } set { } } public System.Func CreateRedisKey { get { throw null; } set { } } public System.TimeSpan? EntryExpiry { get { throw null; } set { } } - public static System.Threading.Tasks.Task DefaultCreateMultiplexer(RedisClusteringOptions options) { throw null; } + public static System.Threading.Tasks.Task<(StackExchange.Redis.IConnectionMultiplexer Multiplexer, bool IsShared)> DefaultCreateMultiplexer(RedisClusteringOptions options) { throw null; } public static StackExchange.Redis.RedisKey DefaultCreateRedisKey(Configuration.ClusterOptions clusterOptions) { throw null; } } diff --git a/src/api/Redis/Orleans.GrainDirectory.Redis/Orleans.GrainDirectory.Redis.cs b/src/api/Redis/Orleans.GrainDirectory.Redis/Orleans.GrainDirectory.Redis.cs index c97618fd4ca..08e259e4854 100644 --- a/src/api/Redis/Orleans.GrainDirectory.Redis/Orleans.GrainDirectory.Redis.cs +++ b/src/api/Redis/Orleans.GrainDirectory.Redis/Orleans.GrainDirectory.Redis.cs @@ -12,11 +12,11 @@ public partial class RedisGrainDirectoryOptions { public StackExchange.Redis.ConfigurationOptions ConfigurationOptions { get { throw null; } set { } } - public System.Func> CreateMultiplexer { get { throw null; } set { } } + public System.Func> CreateMultiplexer { get { throw null; } set { } } public System.TimeSpan? EntryExpiry { get { throw null; } set { } } - public static System.Threading.Tasks.Task DefaultCreateMultiplexer(RedisGrainDirectoryOptions options) { throw null; } + public static System.Threading.Tasks.Task<(StackExchange.Redis.IConnectionMultiplexer Multiplexer, bool IsShared)> DefaultCreateMultiplexer(RedisGrainDirectoryOptions options) { throw null; } } public partial class RedisGrainDirectoryOptionsValidator : IConfigurationValidator diff --git a/src/api/Redis/Orleans.Persistence.Redis/Orleans.Persistence.Redis.cs b/src/api/Redis/Orleans.Persistence.Redis/Orleans.Persistence.Redis.cs index 51ad0a14f4c..cb8fecec97b 100644 --- a/src/api/Redis/Orleans.Persistence.Redis/Orleans.Persistence.Redis.cs +++ b/src/api/Redis/Orleans.Persistence.Redis/Orleans.Persistence.Redis.cs @@ -59,7 +59,7 @@ public partial class RedisStorageOptions : Storage.IStorageProviderSerializerOpt { public StackExchange.Redis.ConfigurationOptions? ConfigurationOptions { get { throw null; } set { } } - public System.Func> CreateMultiplexer { get { throw null; } set { } } + public System.Func> CreateMultiplexer { get { throw null; } set { } } public bool DeleteStateOnClear { get { throw null; } set { } } @@ -71,7 +71,7 @@ public partial class RedisStorageOptions : Storage.IStorageProviderSerializerOpt public int InitStage { get { throw null; } set { } } - public static System.Threading.Tasks.Task DefaultCreateMultiplexer(RedisStorageOptions options) { throw null; } + public static System.Threading.Tasks.Task<(StackExchange.Redis.IConnectionMultiplexer Multiplexer, bool IsShared)> DefaultCreateMultiplexer(RedisStorageOptions options) { throw null; } } public static partial class RedisStorageOptionsExtensions diff --git a/src/api/Redis/Orleans.Reminders.Redis/Orleans.Reminders.Redis.cs b/src/api/Redis/Orleans.Reminders.Redis/Orleans.Reminders.Redis.cs index 7b4edf044e9..e5c7e168903 100644 --- a/src/api/Redis/Orleans.Reminders.Redis/Orleans.Reminders.Redis.cs +++ b/src/api/Redis/Orleans.Reminders.Redis/Orleans.Reminders.Redis.cs @@ -12,11 +12,11 @@ public partial class RedisReminderTableOptions { public StackExchange.Redis.ConfigurationOptions ConfigurationOptions { get { throw null; } set { } } - public System.Func> CreateMultiplexer { get { throw null; } set { } } + public System.Func> CreateMultiplexer { get { throw null; } set { } } public System.TimeSpan? EntryExpiry { get { throw null; } set { } } - public static System.Threading.Tasks.Task DefaultCreateMultiplexer(RedisReminderTableOptions options) { throw null; } + public static System.Threading.Tasks.Task<(StackExchange.Redis.IConnectionMultiplexer Multiplexer, bool IsShared)> DefaultCreateMultiplexer(RedisReminderTableOptions options) { throw null; } } public partial class RedisReminderTableOptionsValidator : IConfigurationValidator From eee110474b374242d98b69901ada3b2d6592ccee Mon Sep 17 00:00:00 2001 From: Reuben Bond Date: Thu, 28 May 2026 16:37:14 -0700 Subject: [PATCH 2/4] fix(redis)!: use standard provider disposal Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../Storage/RedisMembershipTable.cs | 32 +++++++++- .../RedisGrainDirectory.cs | 59 +++++++++++-------- .../Storage/RedisGrainStorage.cs | 46 ++++++++++----- .../Storage/RedisReminderTable.cs | 32 +++++++++- .../Orleans.GrainDirectory.Redis.cs | 6 +- .../Orleans.Persistence.Redis.cs | 6 +- 6 files changed, 136 insertions(+), 45 deletions(-) diff --git a/src/Redis/Orleans.Clustering.Redis/Storage/RedisMembershipTable.cs b/src/Redis/Orleans.Clustering.Redis/Storage/RedisMembershipTable.cs index de57e7b0a49..de5df1393a8 100644 --- a/src/Redis/Orleans.Clustering.Redis/Storage/RedisMembershipTable.cs +++ b/src/Redis/Orleans.Clustering.Redis/Storage/RedisMembershipTable.cs @@ -11,7 +11,7 @@ namespace Orleans.Clustering.Redis { - internal class RedisMembershipTable : IMembershipTable, IDisposable + internal class RedisMembershipTable : IMembershipTable, IDisposable, IAsyncDisposable { private const string TableVersionKey = "Version"; private static readonly TableVersion DefaultTableVersion = new TableVersion(0, "0"); @@ -216,14 +216,40 @@ public async Task CleanupDefunctSiloEntries(DateTimeOffset beforeDate) public void Dispose() { - if (!_muxerIsShared) + var muxer = _muxer; + if (muxer is null) { - _muxer?.Dispose(); + return; } + var muxerIsShared = _muxerIsShared; _muxer = null!; _db = null!; _muxerIsShared = false; + + if (!muxerIsShared) + { + muxer.Dispose(); + } + } + + public async ValueTask DisposeAsync() + { + var muxer = _muxer; + if (muxer is null) + { + return; + } + + var muxerIsShared = _muxerIsShared; + _muxer = null!; + _db = null!; + _muxerIsShared = false; + + if (!muxerIsShared) + { + await muxer.DisposeAsync().ConfigureAwait(false); + } } private enum UpsertResult diff --git a/src/Redis/Orleans.GrainDirectory.Redis/RedisGrainDirectory.cs b/src/Redis/Orleans.GrainDirectory.Redis/RedisGrainDirectory.cs index 09977cbcdde..9008e5203c4 100644 --- a/src/Redis/Orleans.GrainDirectory.Redis/RedisGrainDirectory.cs +++ b/src/Redis/Orleans.GrainDirectory.Redis/RedisGrainDirectory.cs @@ -14,7 +14,7 @@ namespace Orleans.GrainDirectory.Redis { - public partial class RedisGrainDirectory : IGrainDirectory, ILifecycleParticipant + public partial class RedisGrainDirectory : IGrainDirectory, ILifecycleParticipant, IDisposable, IAsyncDisposable { private readonly RedisGrainDirectoryOptions _directoryOptions; private readonly ClusterOptions _clusterOptions; @@ -173,7 +173,7 @@ public Task UnregisterSilos(List siloAddresses) public void Participate(ISiloLifecycle lifecycle) { - lifecycle.Subscribe(nameof(RedisGrainDirectory), ServiceLifecycleStage.RuntimeInitialize, Initialize, Uninitialize); + lifecycle.Subscribe(nameof(RedisGrainDirectory), ServiceLifecycleStage.RuntimeInitialize, Initialize); } public async Task Initialize(CancellationToken ct = default) @@ -189,30 +189,43 @@ public async Task Initialize(CancellationToken ct = default) _database = _redis.GetDatabase(); } - private async Task Uninitialize(CancellationToken arg) + public void Dispose() { - if (_redis != null) + var redis = _redis; + if (redis is null) { - _disposed = true; + return; + } - try - { - _redis.ConnectionRestored -= LogConnectionRestored; - _redis.ConnectionFailed -= LogConnectionFailed; - _redis.ErrorMessage -= LogErrorMessage; - _redis.InternalError -= LogInternalError; - - if (!_redisIsShared) - { - await _redis.DisposeAsync(); - } - } - finally - { - _redis = null!; - _database = null!; - _redisIsShared = false; - } + var redisIsShared = _redisIsShared; + _disposed = true; + _redis = null!; + _database = null!; + _redisIsShared = false; + + if (!redisIsShared) + { + redis.Dispose(); + } + } + + public async ValueTask DisposeAsync() + { + var redis = _redis; + if (redis is null) + { + return; + } + + var redisIsShared = _redisIsShared; + _disposed = true; + _redis = null!; + _database = null!; + _redisIsShared = false; + + if (!redisIsShared) + { + await redis.DisposeAsync().ConfigureAwait(false); } } diff --git a/src/Redis/Orleans.Persistence.Redis/Storage/RedisGrainStorage.cs b/src/Redis/Orleans.Persistence.Redis/Storage/RedisGrainStorage.cs index dc2590d49d8..107e16c8d9c 100644 --- a/src/Redis/Orleans.Persistence.Redis/Storage/RedisGrainStorage.cs +++ b/src/Redis/Orleans.Persistence.Redis/Storage/RedisGrainStorage.cs @@ -21,7 +21,7 @@ namespace Orleans.Persistence /// /// Redis-based grain storage provider /// - public partial class RedisGrainStorage : IGrainStorage, ILifecycleParticipant + public partial class RedisGrainStorage : IGrainStorage, ILifecycleParticipant, IDisposable, IAsyncDisposable { private readonly string _serviceId; private readonly RedisValue _ttl; @@ -62,7 +62,7 @@ public RedisGrainStorage( public void Participate(ISiloLifecycle lifecycle) { var name = OptionFormattingUtilities.Name(_name); - lifecycle.Subscribe(name, _options.InitStage, Init, Close); + lifecycle.Subscribe(name, _options.InitStage, Init); } private async Task Init(CancellationToken cancellationToken) @@ -253,23 +253,41 @@ public async Task ClearStateAsync(string grainType, GrainId grainId, IGrainSt } } - private async Task Close(CancellationToken cancellationToken) + public void Dispose() { - if (_connection is null) return; + var connection = _connection; + if (connection is null) + { + return; + } - try + var connectionIsShared = _connectionIsShared; + _connection = null; + _db = null; + _connectionIsShared = false; + + if (!connectionIsShared) { - if (!_connectionIsShared) - { - await _connection.CloseAsync().ConfigureAwait(false); - _connection.Dispose(); - } + connection.Dispose(); } - finally + } + + public async ValueTask DisposeAsync() + { + var connection = _connection; + if (connection is null) + { + return; + } + + var connectionIsShared = _connectionIsShared; + _connection = null; + _db = null; + _connectionIsShared = false; + + if (!connectionIsShared) { - _connection = null; - _db = null; - _connectionIsShared = false; + await connection.DisposeAsync().ConfigureAwait(false); } } diff --git a/src/Redis/Orleans.Reminders.Redis/Storage/RedisReminderTable.cs b/src/Redis/Orleans.Reminders.Redis/Storage/RedisReminderTable.cs index 4b8a70dca05..5cde112bec8 100644 --- a/src/Redis/Orleans.Reminders.Redis/Storage/RedisReminderTable.cs +++ b/src/Redis/Orleans.Reminders.Redis/Storage/RedisReminderTable.cs @@ -17,7 +17,7 @@ #nullable disable namespace Orleans.Reminders.Redis { - internal partial class RedisReminderTable : IReminderTable, IDisposable + internal partial class RedisReminderTable : IReminderTable, IDisposable, IAsyncDisposable { private readonly RedisKey _hashSetKey; private readonly RedisReminderTableOptions _redisOptions; @@ -182,14 +182,40 @@ public async Task UpsertRow(ReminderEntry entry) public void Dispose() { - if (!_muxerIsShared) + var muxer = _muxer; + if (muxer is null) { - _muxer?.Dispose(); + return; } + var muxerIsShared = _muxerIsShared; _muxer = null; _db = null; _muxerIsShared = false; + + if (!muxerIsShared) + { + muxer.Dispose(); + } + } + + public async ValueTask DisposeAsync() + { + var muxer = _muxer; + if (muxer is null) + { + return; + } + + var muxerIsShared = _muxerIsShared; + _muxer = null; + _db = null; + _muxerIsShared = false; + + if (!muxerIsShared) + { + await muxer.DisposeAsync().ConfigureAwait(false); + } } private static ReminderEntry ConvertToEntry(string reminderValue) diff --git a/src/api/Redis/Orleans.GrainDirectory.Redis/Orleans.GrainDirectory.Redis.cs b/src/api/Redis/Orleans.GrainDirectory.Redis/Orleans.GrainDirectory.Redis.cs index 08e259e4854..62a50b072b1 100644 --- a/src/api/Redis/Orleans.GrainDirectory.Redis/Orleans.GrainDirectory.Redis.cs +++ b/src/api/Redis/Orleans.GrainDirectory.Redis/Orleans.GrainDirectory.Redis.cs @@ -29,10 +29,14 @@ public void ValidateConfiguration() { } namespace Orleans.GrainDirectory.Redis { - public partial class RedisGrainDirectory : IGrainDirectory, ILifecycleParticipant + public partial class RedisGrainDirectory : IGrainDirectory, ILifecycleParticipant, System.IDisposable, System.IAsyncDisposable { public RedisGrainDirectory(Configuration.RedisGrainDirectoryOptions directoryOptions, Microsoft.Extensions.Options.IOptions clusterOptions, Microsoft.Extensions.Logging.ILogger logger) { } + public void Dispose() { } + + public System.Threading.Tasks.ValueTask DisposeAsync() { throw null; } + public System.Threading.Tasks.Task Initialize(System.Threading.CancellationToken ct = default) { throw null; } public System.Threading.Tasks.Task Lookup(Runtime.GrainId grainId) { throw null; } diff --git a/src/api/Redis/Orleans.Persistence.Redis/Orleans.Persistence.Redis.cs b/src/api/Redis/Orleans.Persistence.Redis/Orleans.Persistence.Redis.cs index cb8fecec97b..c2ec21f7493 100644 --- a/src/api/Redis/Orleans.Persistence.Redis/Orleans.Persistence.Redis.cs +++ b/src/api/Redis/Orleans.Persistence.Redis/Orleans.Persistence.Redis.cs @@ -37,12 +37,16 @@ public static partial class RedisSiloBuilderExtensions namespace Orleans.Persistence { - public partial class RedisGrainStorage : Storage.IGrainStorage, ILifecycleParticipant + public partial class RedisGrainStorage : Storage.IGrainStorage, ILifecycleParticipant, System.IDisposable, System.IAsyncDisposable { public RedisGrainStorage(string name, RedisStorageOptions options, Storage.IGrainStorageSerializer grainStorageSerializer, Microsoft.Extensions.Options.IOptions clusterOptions, Serialization.Serializers.IActivatorProvider activatorProvider, Microsoft.Extensions.Logging.ILogger logger) { } public System.Threading.Tasks.Task ClearStateAsync(string grainType, Runtime.GrainId grainId, IGrainState grainState) { throw null; } + public void Dispose() { } + + public System.Threading.Tasks.ValueTask DisposeAsync() { throw null; } + public void Participate(Runtime.ISiloLifecycle lifecycle) { } public System.Threading.Tasks.Task ReadStateAsync(string grainType, Runtime.GrainId grainId, IGrainState grainState) { throw null; } From 326095a2818d577ca762bb95be23fd2aea87cd2c Mon Sep 17 00:00:00 2001 From: Reuben Bond <203839+ReubenBond@users.noreply.github.com> Date: Fri, 29 May 2026 19:49:00 -0700 Subject: [PATCH 3/4] Potential fix for pull request finding Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> --- src/Redis/Orleans.GrainDirectory.Redis/RedisGrainDirectory.cs | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/Redis/Orleans.GrainDirectory.Redis/RedisGrainDirectory.cs b/src/Redis/Orleans.GrainDirectory.Redis/RedisGrainDirectory.cs index 9008e5203c4..4e3b7e5138e 100644 --- a/src/Redis/Orleans.GrainDirectory.Redis/RedisGrainDirectory.cs +++ b/src/Redis/Orleans.GrainDirectory.Redis/RedisGrainDirectory.cs @@ -198,6 +198,10 @@ public void Dispose() } var redisIsShared = _redisIsShared; + redis.ConnectionRestored -= LogConnectionRestored; + redis.ConnectionFailed -= LogConnectionFailed; + redis.ErrorMessage -= LogErrorMessage; + redis.InternalError -= LogInternalError; _disposed = true; _redis = null!; _database = null!; From bfc302852fd7659b1c197774365acbfed400bb9e Mon Sep 17 00:00:00 2001 From: Reuben Bond Date: Mon, 1 Jun 2026 11:18:29 -0700 Subject: [PATCH 4/4] fix(redis): unsubscribe directory events on async dispose Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- src/Redis/Orleans.GrainDirectory.Redis/RedisGrainDirectory.cs | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/Redis/Orleans.GrainDirectory.Redis/RedisGrainDirectory.cs b/src/Redis/Orleans.GrainDirectory.Redis/RedisGrainDirectory.cs index 4e3b7e5138e..3e12a2ba5bb 100644 --- a/src/Redis/Orleans.GrainDirectory.Redis/RedisGrainDirectory.cs +++ b/src/Redis/Orleans.GrainDirectory.Redis/RedisGrainDirectory.cs @@ -222,6 +222,10 @@ public async ValueTask DisposeAsync() } var redisIsShared = _redisIsShared; + redis.ConnectionRestored -= LogConnectionRestored; + redis.ConnectionFailed -= LogConnectionFailed; + redis.ErrorMessage -= LogErrorMessage; + redis.InternalError -= LogInternalError; _disposed = true; _redis = null!; _database = null!;