Skip to content

Fix RedisGrainDirectory uninitialize exception#9940

Closed
miguelhasse wants to merge 3 commits into
dotnet:mainfrom
miguelhasse:fix/redis-grain-directory-uninitialize-exception
Closed

Fix RedisGrainDirectory uninitialize exception#9940
miguelhasse wants to merge 3 commits into
dotnet:mainfrom
miguelhasse:fix/redis-grain-directory-uninitialize-exception

Conversation

@miguelhasse

@miguelhasse miguelhasse commented Feb 18, 2026

Copy link
Copy Markdown
Contributor

Updated the Uninitialize method to replace CloseAsync and synchronous Dispose calls with await _redis.DisposeAsync(), ensuring proper asynchronous disposal. Cleanup of _redis and _database fields is now handled in a finally block to guarantee resource release even if disposal fails.

This change is a fix to avoid NullReferenceException being thrown during calls to RedisGrainDirectory .Uninitialize

image
Microsoft Reviewers: Open in CodeFlow

Updated the Uninitialize method to replace CloseAsync and synchronous Dispose calls with await _redis.DisposeAsync(), ensuring proper asynchronous disposal. Cleanup of _redis and _database fields is now handled in a finally block to guarantee resource release even if disposal fails.
Copilot AI review requested due to automatic review settings February 18, 2026 07:00

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Updates the Redis grain directory shutdown path to dispose the Redis connection asynchronously and ensure _redis/_database fields are cleared even if disposal throws, addressing a NullReferenceException during RedisGrainDirectory.Uninitialize.

Changes:

  • Replace CloseAsync() + synchronous Dispose() with await _redis.DisposeAsync().
  • Move cleanup of _redis and _database into a finally block to guarantee field cleanup.

Comment thread src/Redis/Orleans.GrainDirectory.Redis/RedisGrainDirectory.cs Outdated
Comment thread src/Redis/Orleans.GrainDirectory.Redis/RedisGrainDirectory.cs
Comment on lines 192 to +206
private async Task Uninitialize(CancellationToken arg)
{
if (_redis != null && _redis.IsConnected)
{
_disposed = true;

await _redis.CloseAsync();
_redis.Dispose();
_redis = null!;
_database = null!;
try
{
await _redis.DisposeAsync();
}
finally
{
_redis = null!;
_database = null!;
}

Copilot AI Feb 18, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This change targets a shutdown-time exception path, but there’s no coverage ensuring Uninitialize is safe to call when initialization is partial/failed or when it’s invoked multiple times. Consider adding a Redis grain directory test which exercises the shutdown path (e.g., invoke Uninitialize via reflection or lifecycle and assert it doesn’t throw).

Copilot uses AI. Check for mistakes.
Removed IsConnected check before disposing _redis and added unsubscription from ConnectionRestored, ConnectionFailed, ErrorMessage, and InternalError event handlers to prevent memory leaks and ensure proper cleanup.
Comment thread src/Redis/Orleans.GrainDirectory.Redis/RedisGrainDirectory.cs
@ReubenBond

ReubenBond commented Feb 18, 2026

Copy link
Copy Markdown
Member

What is the source of the NullReferenceException in this case, as in: what is null here? From your exception, it looks like it's null on line 199 (_redis.Dispose();). If that's the case, are there overlapping calls to Uninitialize that are setting _redis to null? If that's an issue (it might be, I'm not sure yet), then it would likely be good to guard against that in code (eg, capture _redis and set it to null atomically, possibly via an interlocked operation or using a lock statement)

@miguelhasse

Copy link
Copy Markdown
Contributor Author

What is the source of the NullReferenceException in this case, as in: what is null here? From your exception, it looks like it's null on line 199 (_redis.Dispose();). If that's the case, are there overlapping calls to Uninitialize that are setting _redis to null? If that's an issue (it might be, I'm not sure yet), then it would likely be good to guard against that in code (eg, capture _redis and set it to null atomically, possibly via an interlocked operation or using a lock statement)

From the logged exception it look like it it thrown from inside StackExchange's implementation, but you may be right about the possible race condition. Using an interlocked operation might be a good idea.

@ReubenBond

Copy link
Copy Markdown
Member

@miguelhasse did this get resolved? It is not clear to me that this is an Orleans issue yet.

@miguelhasse

Copy link
Copy Markdown
Contributor Author

@miguelhasse did this get resolved? It is not clear to me that this is an Orleans issue yet.

It's not resolved. Logging shows frequent exceptions like the one I posted here.

@miguelhasse

miguelhasse commented Apr 28, 2026

Copy link
Copy Markdown
Contributor Author

@ReubenBond logs keep showing frequent exceptions due to this issue. Orleans running on AKS across regions with a single Redis distributed grain directory.

image

@ReubenBond

Copy link
Copy Markdown
Member

@miguelhasse I believe the core issue here is related to ownership of the Redis instance and hence the disposal of it. Fixing it will result in a small breaking change to how Redis is configured.
I made a change to RedisStreamingOptions to account for shared ownership, changing the CreateMultiplexer signature to return an IsShared value:

    /// <summary>
    /// The delegate used to create a Redis connection multiplexer and indicate whether it is shared.
    /// </summary>
    public Func<RedisStreamingOptions, Task<(IConnectionMultiplexer Multiplexer, bool IsShared)>> CreateMultiplexer { get; set; } = DefaultCreateMultiplexer;

We need to make the changes to the other Redis providers.

@ReubenBond

Copy link
Copy Markdown
Member

I believe this was fixed by #10146. I'll close this for now and we can revisit if it's still an issue.

@ReubenBond ReubenBond closed this Jun 11, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants