Skip to content

[BUG] RemoveByTagAsync doesn't seem to remove entries #515

@ted-mundy

Description

@ted-mundy

Describe the bug

It doesn't look like RemoveByTagAsync is removing entries. I'll explain more below.

To Reproduce

In this example (ran in tests/ZiggyCreatures.FusionCache.Playground), I use the idea of users and "teams". This is a very abstract overview of a REST API where users get added to teams, and the results are cached. If this is not clear, let me know and I can write something that's a bit more "real".

Here, we want to cache user 1's data. The response object would be something like:

{
  "name": "John Doe",
  "teams": [
    {
      "id": 1,
      "name": "John's Team"
    }
  ]
}

As a result, if any of John's teams update, we need to clear the cache for the /users/1 endpoint or else we'll be serving stale data. I use the tagging system to "tag" John with his team, so that whenever his team's cache is invalidated his should be also. It doesn't seem to be working however.

public static class ScratchpadScenario
{
	public static async Task RunAsync()
	{
		Console.Title = "FusionCache - Scratchpad";

		Console.OutputEncoding = Encoding.UTF8;

		var services = new ServiceCollection();

		Log.Logger = new LoggerConfiguration()
			.MinimumLevel.Is(Serilog.Events.LogEventLevel.Information)
			.Enrich.FromLogContext()
			.WriteTo.Console()
			.CreateLogger();

		services.AddLogging(configure => configure.AddSerilog());
		//
		var serviceProvider = services.BuildServiceProvider();

		// CACHE OPTIONS
		var options = new FusionCacheOptions
		{
			CacheKeyPrefix = "MyCachePrefix:",
			DefaultEntryOptions = new FusionCacheEntryOptions
			{
				Duration = TimeSpan.FromMinutes(1),
			},
		};

		var logger = serviceProvider.GetRequiredService<Microsoft.Extensions.Logging.ILogger<FusionCache>>();

		var cache = new FusionCache(options);

		var distributedCache = new MemoryDistributedCache(Options.Create(new MemoryDistributedCacheOptions()));
		var serializer = new FusionCacheNewtonsoftJsonSerializer();
		cache.SetupDistributedCache(distributedCache, serializer);

		var backplane = new MemoryBackplane(new MemoryBackplaneOptions());
		cache.SetupBackplane(backplane);

		var key = "baz";

		Console.WriteLine("GET /user/1");
		var tag = "user:1"; // no team tied to user
		var val = await cache.GetOrSetAsync(key, async ct => await GetUserAsync(), tags: [tag]);

		Console.WriteLine("GET /user/1");
		val = await cache.GetOrSetAsync(key, async ct => await GetUserAsync(), tags: [tag]);

		Console.WriteLine("POST /teams/1/users");
		await cache.RemoveByTagAsync(tag);

		Console.WriteLine("GET /user/1");
		var teamTag = "team:1"; // team tied to user
		val = await cache.GetOrSetAsync(key, async ct => await GetUserAsync(), tags: [tag, teamTag]);

		Console.WriteLine("PUT /teams/1");
		await cache.RemoveByTagAsync(teamTag);

		Console.WriteLine("GET /user/1");
		val = await cache.GetOrSetAsync(key, async ct => await GetUserAsync(), tags: [tag, teamTag]);

		Console.WriteLine("DELETE /teams/1/users/1");
		await cache.RemoveByTagAsync(teamTag);

		Console.WriteLine("GET /user/1");
		val = await cache.GetOrSetAsync(key, async ct => await GetUserAsync(), tags: [tag]); // no team tag as it was removed

		Console.WriteLine("PUT /teams/1");
		await cache.RemoveByTagAsync(teamTag);

		Console.WriteLine("GET /user/1");
		val = await cache.GetOrSetAsync(key, async ct => await GetUserAsync(), tags: [tag]); // same as above, no team tag
	}

	private static async Task<string> GetUserAsync()
	{
		Console.WriteLine("Getting user...");

		// simulate some work
		await Task.Delay(2000).ConfigureAwait(false);

		// return the value
		return "qux"; // can be anything
	}
}

Console output is:

GET /user/1
Getting user...
GET /user/1
POST /teams/1/users
GET /user/1
Getting user...
PUT /teams/1
GET /user/1
DELETE /teams/1/users/1
GET /user/1
PUT /teams/1
GET /user/1

Expected behavior

In the example above, we hit the "database" (GetUserAsync()) twice, but it should have been 4 times:

  • The first GET /user/1
  • The third GET /user/1, just after we POST /teams/1/users as the user has a new team ✅
  • The fourth GET /user/1 after we update the team we are now a part of ❌
  • The fifth GET /user/1 after we remove ourselves from the team ❌

Also, the final GET /user/1 should not force a cache miss, as we are no longer part of that team - we shouldn't have that tag anymore.

Versions

I've encountered this issue on:

  • main branch (ran my test on the playground on main)
  • .NET 8.0.18
  • MacOS 15.5 (24F74)

Additional context

If I've missed anything please let me know!!

Metadata

Metadata

Assignees

Labels

bugSomething isn't working

Type

No type

Projects

No projects

Milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions