Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Microsoft.Extensions.Caching.StackExchangeRedis 9.0.3 - when using HybridCache RedisCacheImpl.IsHybridCacheDefined causes infinite recursion #60894

Closed
1 task done
xeonix opened this issue Mar 12, 2025 · 9 comments · Fixed by #60915
Assignees
Labels
area-middleware Includes: URL rewrite, redirect, response cache/compression, session, and other general middlewares

Comments

@xeonix
Copy link

xeonix commented Mar 12, 2025

Is there an existing issue for this?

  • I have searched the existing issues

Describe the bug

Hello guys.
Today I updated all my NuGets and my .NET 9 Back-End hanged.
I'm using the following NuGets to do the caching stuff in my project:
Microsoft.Extensions.Caching.Hybrid 9.3.0
Microsoft.Extensions.Caching.StackExchangeRedis 9.0.3
After investigating the issue and inspecting this GitHub repository I've find out what's happening:
I registered HybridCache like this:

services.AddHybridCache(options =>
        {
            options.DefaultEntryOptions = new HybridCacheEntryOptions
            {
                Expiration = TimeSpan.FromMinutes(5),
                LocalCacheExpiration = TimeSpan.FromMinutes(2.5)
            };
        });

and then added some RedisCache like this:

services.AddStackExchangeRedisCache(options =>
{
    options.Configuration = redisCacheConnectionString;
    options.InstanceName = redisCacheKeyPrefix;
});

The problem here is with IsHybridCacheDefined method:

internal sealed class RedisCacheImpl : RedisCache
{
    public RedisCacheImpl(IOptions<RedisCacheOptions> optionsAccessor, ILogger<RedisCache> logger, IServiceProvider services)
        : base(optionsAccessor, logger)
    {
        HybridCacheActive = IsHybridCacheDefined(services);
    }

    public RedisCacheImpl(IOptions<RedisCacheOptions> optionsAccessor, IServiceProvider services)
        : base(optionsAccessor)
    {
        HybridCacheActive = IsHybridCacheDefined(services);
    }

    // HybridCache optionally uses IDistributedCache; if we're here, then *we are* the DC
    private static bool IsHybridCacheDefined(IServiceProvider services)
        => services.GetService<HybridCache>() is not null;
}

When it resolves HybridCache which then resolves IDistributedCache.
According to StackExchangeRedisCacheServiceCollectionExtensions, RedisCacheImpl is registered as:

services.Add(ServiceDescriptor.Singleton<IDistributedCache, RedisCacheImpl>());

So when resolving IDistributedCache and instantiating RedisCacheImpl, it's ctor calls IsHybridCacheDefined method, which resolves HybridCache, which resolves IDistributedCache resulting into an infinite resolution loop.
With Microsoft.Extensions.Caching.StackExchangeRedis 9.0.2 - RedisCacheImpl has no IsHybridCacheDefined method, so everything works fine.

Expected Behavior

No response

Steps To Reproduce

  1. When configuring DI - call services.AddHybridCache, then services.AddStackExchangeRedisCache
  2. Try to resolve HybridCache anywhere (i.e. Controller/Minimal API)

Exceptions (if any)

No response

.NET Version

9.0.201

Anything else?

No response

@dotnet-issue-labeler dotnet-issue-labeler bot added the area-middleware Includes: URL rewrite, redirect, response cache/compression, session, and other general middlewares label Mar 12, 2025
@milkyjoe90
Copy link

I have also experienced exactly the same issue with HybridCache and Redis

@EsgAtWorkleap
Copy link

FYI this also happens with the 9.2.0 preview of Microsoft.Extensions.Caching.Hybrid. As soon as we update Microsoft.Extensions.Caching.StackExchangeRedis to 9.0.3, we get the issue

@xeonix
Copy link
Author

xeonix commented Mar 12, 2025

@EsgAtWorkleap Yes, because the problem is with Microsoft.Extensions.Caching.StackExchangeRedis 9.0.3 only.
After updating all NuGets I rolled back everything and started updating them one by one to find the problematic one.

@mgravell
Copy link
Member

mgravell commented Mar 12, 2025

Investigating as a priority; some weird DI interplay - will get to the bottom of it. I have checked, and I'm certain that the only change in 9.0.3 is this metadata discovery, so it should be entirely safe and reasonable to use 9.0.2 as a workaround for now, and: any fix is going to be to Microsoft.Extensions.Caching.StackExchangeRedis:

- <PackageReference Include="Microsoft.Extensions.Caching.StackExchangeRedis" Version="9.0.3" />
+ <PackageReference Include="Microsoft.Extensions.Caching.StackExchangeRedis" Version="9.0.2" />

@mgravell
Copy link
Member

mgravell commented Mar 13, 2025

Huge apologies. Fix PR is #60915 (main) and #60916 (backport to 9.x). I'm going to see if I can get 9.0.3 unlisted.

Can I still use HybridCache

Yes: just avoid 9.0.3 of Microsoft.Extensions.Caching.StackExchangeRedis - 9.0.2 is fine, or if you're reading in the future: something >= 9.0.4

Not trying to make excuses here - this was just a stupid and (with hindsight) avoidable error. Both pieces unit tested absolutely fine in isolation, *sigh*. The following is purely for education to stop anyone else making the same mistake, and isn't me trying to dodge accountability:

  • three related components in three different repos - abstractions (runtime), aspnet-cache (aspnetcore) and the cache implementation (extensions)
  • the two non-trivial bits can't directly reference each-other, so a fake was used to prove the handshake
  • the fake did not have the real DI signature, but was left very ... well, fake (trivial .ctor etc)
  • the "real" versions, however, were such that the concrete HybridCache was asking about IDistributedCache and the concrete IDistributedCache was asking questions about HybridCache; this made the CI spin indefinitely

The fix is simple: at least one of the two should defer asking questions until after it has been constructed

The moral / learning point: if you're going to fake your DI: don't make it too fake

Side note: I wonder if the DI could do a better job of detecting stack dives. In an ideal world, the DI would also have a "do you have a service registration for X?" API that doesn't actually force X to be manifested!

@mgravell
Copy link
Member

mgravell commented Mar 13, 2025

Microsoft.Extensions.Caching.StackExchangeRedis 9.0.3 has been delisted - the underlying fixes will be applied presumably for 9.0.4, but until then: HybridCache works fine with 9.0.2 (indeed, the entire point of IDistributedCache is that HybridCache doesn't know what the backend is).

@milkyjoe90
Copy link

Thanks so much for jumping on this so quickly @mgravell!

@mgravell
Copy link
Member

mgravell commented Mar 13, 2025

Note .NET 10 preview 1 and 2 presumably also have this issue; we're discussing whether it is preferable to leave that alone versus delist that - 9.0.2 should work perfectly fine with .NET 10, but it may be confusing that it doesn't exist, and this issue only occurs with the interaction between both libraries.

@mgravell mgravell changed the title Microsoft.Extensions.Caching.StackExchangeRedis 9.3.0 - when using HybridCache RedisCacheImpl.IsHybridCacheDefined causes infinite recursion Microsoft.Extensions.Caching.StackExchangeRedis 9.0.3 - when using HybridCache RedisCacheImpl.IsHybridCacheDefined causes infinite recursion Mar 13, 2025
wtgodbe pushed a commit that referenced this issue Apr 9, 2025
@MatthewSteeples
Copy link

9.0.4 of this library shipped without this fix so 9.0.4 results in a deadlock when using HybridCache

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
area-middleware Includes: URL rewrite, redirect, response cache/compression, session, and other general middlewares
Projects
None yet
Development

Successfully merging a pull request may close this issue.

5 participants