Skip to content

RedisOutputCacheStore force reconnect can release the current connection instead of the failed one #70029

Open

Description

@GrantTotinov

Is there an existing issue for this?

  • I have searched the existing issues

Describe the bug

Hi! I was looking at the force reconnect path in RedisOutputCacheStore and noticed something that I think is a leftover from when the output cache store was split out.

With force reconnect enabled (Microsoft.AspNetCore.Caching.StackExchangeRedis.UseForceReconnect), OnRedisError clears the shared connection with:

ReleaseConnection(Interlocked.CompareExchange(ref _cache, null, cache));

Since Interlocked.CompareExchange returns the value _cache had before the call, if another caller has already reconnected in the meantime, this returns the new connection, and that one gets released while _cache still points to it. The comment above the line describes the intended behavior (only clear the field if it still holds the failed connection), so I believe this is just the older version of the line.

RedisCache had the same line, and it was updated in #52608 to only release when ReferenceEquals(tmp, cache). It looks like RedisOutputCacheStore was split into its own package in #50457 shortly before that, so the update didn't make it over. Otherwise OnRedisError is identical in both files.

When this happens, ConnectAsync keeps returning the disposed connection from _cache, so every later call fails with ObjectDisposedException. Since OnRedisError only reacts to RedisConnectionException and SocketException, the store doesn't recover on its own. The repro below shows this with real ConnectionMultiplexer instances.

Expected Behavior

I think OnRedisError should mirror RedisCache here and only release the connection it actually removed from _cache:

var tmp = Interlocked.CompareExchange(ref _cache, null, cache);
if (ReferenceEquals(tmp, cache))
{
    ReleaseConnection(tmp);
}

Steps To Reproduce

I haven't hit this in practice, I found it while reading the code. The interleaving I have in mind:

  1. Requests A and B both fail on connection X and both pass the reconnect timing checks.
  2. A swaps X out of _cache and releases it.
  3. Request C calls ConnectAsync, finds _cache empty and stores a new connection Y.
  4. B runs CompareExchange(ref _cache, null, X). It fails, returns Y, and ReleaseConnection(Y) closes the connection _cache still references.

The window is pretty narrow (B would have to stall between its checks and the exchange for as long as the reconnect takes), so it's unlikely to show up often. When it does, though, it would most likely be during a Redis outage under load, which is exactly when force reconnect is needed.

Exceptions (if any)

ObjectDisposedException from every request after the late OnRedisError call (see the repro output).

.NET Version

main (ceb0eec). The line is unchanged since the package was created in #50457.

Anything else?

Related, but I think separate: #55898 (factory-provided connection disposed on force reconnect in RedisCache) and #69849 (GC loop ignores disposal).

I'd be happy to open a PR with the fix if that sounds good. Thanks!

Activity

  1. added theissue type on Oct 24, 2026
  2. github-actions commented on Oct 24, 2026

    @github-actions
    Contributor

    Triage Summary

    Area: area-middleware (RedisOutputCacheStore / output caching)
    Type: Bug (preserved)

    Potential Duplicates

    • None found

    Notes

    Generated by Issue Triage Agent for dotnet/aspnetcore for #70029 · copilot · auto · 20.9 AIC · ⌖ 1.01 AIC · ⊞ 7.3K · ◷

  3. added
    area-middlewareIncludes: URL rewrite, redirect, response cache/compression, session, and other general middlewares
    on Oct 24, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    area-middlewareIncludes: URL rewrite, redirect, response cache/compression, session, and other general middlewares

    Type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions