Repository navigation
RedisOutputCacheStore force reconnect can release the current connection instead of the failed one #70029
Description
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:
- Requests A and B both fail on connection X and both pass the reconnect timing checks.
- A swaps X out of
_cacheand releases it. - Request C calls
ConnectAsync, finds_cacheempty and stores a new connection Y. - B runs
CompareExchange(ref _cache, null, X). It fails, returns Y, andReleaseConnection(Y)closes the connection_cachestill 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
github-actions commentedon Oct 24, 2026
Triage Summary
Area:
area-middleware(RedisOutputCacheStore/ output caching)Type:
Bug(preserved)Potential Duplicates
Notes
RedisOutputCacheStoregarbage collection loop ignores disposal, keeping disposed hosts reachable for ~5 minutes #69849 (output cache store GC loop ignores disposal), both already referenced by the reporter.