Skip to content

[BUG] Distributed lock is not released when the memory-cache write fails after a successful factory #646

Description

@joaopbnogueira

Describe the bug

With a Redis distributed locker and L2 configured, a GetOrSetAsync factory succeeds but its result cannot be stored in a size-limited memory cache because the entry has no Size.

The expected InvalidOperationException is thrown, but the distributed lock acquired before the factory remains held after the call completes.

To Reproduce

Requires the .NET 10 SDK and a running Docker-compatible engine. Testcontainers starts an isolated Redis instance on a random host port and removes it after the run. No manually started Redis or connection string is needed.

Save these two files in an empty directory.

Repro.csproj:

<Project Sdk="Microsoft.NET.Sdk">
  <PropertyGroup>
    <OutputType>Exe</OutputType>
    <TargetFramework>net10.0</TargetFramework>
    <ImplicitUsings>enable</ImplicitUsings>
    <Nullable>enable</Nullable>
  </PropertyGroup>
  <ItemGroup>
    <PackageReference Include="ZiggyCreatures.FusionCache" Version="2.8.0" />
    <PackageReference Include="ZiggyCreatures.FusionCache.Locking.Distributed.Redis" Version="2.8.0" />
    <PackageReference Include="ZiggyCreatures.FusionCache.Serialization.SystemTextJson" Version="2.8.0" />
    <PackageReference Include="Microsoft.Extensions.Caching.StackExchangeRedis" Version="10.0.11" />
    <PackageReference Include="Testcontainers.Redis" Version="4.14.0" />
  </ItemGroup>
</Project>

Program.cs:

using Microsoft.Extensions.Caching.Memory;
using Microsoft.Extensions.Caching.StackExchangeRedis;
using StackExchange.Redis;
using Testcontainers.Redis;
using ZiggyCreatures.Caching.Fusion;
using ZiggyCreatures.Caching.Fusion.Locking.Distributed.Redis;
using ZiggyCreatures.Caching.Fusion.Serialization.SystemTextJson;

await using var redis = new RedisBuilder("redis:7-alpine").Build();
await redis.StartAsync();
using var connection = await ConnectionMultiplexer.ConnectAsync(redis.GetConnectionString());
using var locker = new RedisDistributedLocker(new RedisDistributedLockerOptions
{
    ConnectionMultiplexerFactory = () => Task.FromResult<IConnectionMultiplexer>(connection)
}, null);
using var memory = new MemoryCache(new MemoryCacheOptions { SizeLimit = 100 });
using var l2 = new RedisCache(new RedisCacheOptions
{
    ConnectionMultiplexerFactory = () => Task.FromResult<IConnectionMultiplexer>(connection)
});
using var cache = new FusionCache(new FusionCacheOptions(), memory);
cache.SetupDistributedLocker(locker);
cache.SetupDistributedCache(l2, new FusionCacheSystemTextJsonSerializer());

// Run with --control to supply the required entry size.
var options = new FusionCacheEntryOptions();
if (args.Contains("--control")) options.Size = 1;

var key = $"memory-write-{Guid.NewGuid():N}";
var server = connection.GetServer(connection.GetEndPoints()[0]);
RedisKey lockKey = default;
try
{
    await cache.GetOrSetAsync<int>(key, (_, _) =>
    {
        // Confirm acquisition for this key before the memory write.
        lockKey = server.Keys(pattern: $"*{key}*:lock").Single();
        return Task.FromResult(42);
    }, options: options);
    Console.WriteLine("Memory write succeeded");
}
catch (InvalidOperationException)
{
    Console.WriteLine("Memory write failed: entry size is required");
}

bool held = await connection.GetDatabase().KeyExistsAsync(lockKey);
Console.WriteLine($"Distributed lock still held: {held} (expected False)");
Environment.ExitCode = held ? 1 : 0;

Run:

dotnet run --project Repro.csproj

Observed output, excluding Testcontainers lifecycle logs:

Memory write failed: entry size is required
Distributed lock still held: True (expected False)

The reproduction exits with code 1 when it detects the bug. The same program should exit with code 0 once the behavior is corrected.

Run the control:

dotnet run --project Repro.csproj -- --control

The control changes only the entry options by setting Size = 1, which lets the memory write succeed. It runs in a fresh container.

Observed control output, exit code 0:

Memory write succeeded
Distributed lock still held: False (expected False)

Versions

  • FusionCache: 2.8.0, official unmodified package.
  • Redis distributed locker: 2.8.0.
  • .NET: 10.0.
  • Host OS: macOS 26.6.2.
  • Redis: redis:7-alpine, running locally in Docker.
  • Integration tests use Redis Testcontainers.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Labels

bugSomething isn't working

Type

No type

Projects

No projects

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions