ZiggyCreatures / FusionCache

FusionCache is an easy to use, fast and robust hybrid cache with advanced resiliency features.
MIT License
1.87k stars 96 forks source link

[FEATURE] 🧼 Clear() #331

Open jodydonetti opened 4 days ago

jodydonetti commented 4 days ago

Problem

One of the most requested features for FusionCache has always been a Clear() mechanism.

The reason why it has always been hard to do it is that we are not talking about a simple memory cache: that would've been quite easy.

Instead we need to consider all of these configurations:

Then multiply all of these for these scenarios:

Finally, as a cherry on top, everything should automatically handle transient errors and work with features like fail-safe, soft timeouts, auto-recovery and more.

And... that is a lot!

So how is it possible to do achieve all of this?

Solution

Now that Tagging is finally coming along, I think we have our solution.

By simply picking a "special" tag like "*" we can use Tagging to make a proper Clear() mechanism work (for a detail of Tagging works underneath, please refer to that issue).

Here's an example:

cache.Set("foo", 1);
cache.Set("bar", 2);
cache.Set("baz", 3);

// CLEAR
cache.Clear();

// NOW THE CACHE IS EMPTY

Damn if that is nice 😬

[!NOTE]
In reality, the special tag I currently picked in preview-1 is "__*" so that it would not collide with a nice "*" tag that users may potentially end up using... but I'm trying to understand what HybridCache will use and maybe use the same, so that doing a RemoveByTag("*") in both libraries will get you the same result. I'll take a decision before going GA with v2.

Performance

On one hand, using Tagging to achieve clear is for sure a great design choice: all the plumbing available in FusionCache is used to achieve and empower Tagging, and in turn all the Tagging plumbing is used to achieve and empower Clear() support.

Nice, really.

On the other hand, we can go one step further: since the special tag used for clear is one and one only, we may special-case it and do some things to make it even better.

This is why I'm also saving the expiration timestamp for the special clear tag directly in memory (eg: in a normal variable), so that FusionCache will keep it there forever and every Clear() call would also update it: in this way the speed of checking it would be even greater than checking the cache entry for the special tag.

But wait, in a multi-node scenario a Clear() may happen on another node, and we may receive a backplane notification from that other node!

Correct, and that is why when receiving a backplane notification FusionCache also checks to see if it is for the special tag and, if so, do what is needed so that the expiration timestamp (and the dedicated variable) is updated, automatically.

And what happens in case of transient issues while sending that backplane notification?

No big deal,we are already covered thanks to Auto-Recovery.

Again, really really nice, even if I say so myself.

Raw Clear()

From some time now the standard MemoryCache (currently used as the L1) actually supports a "real" Clear() method that does what I call a "raw clear", meaning a full one like you do with a Dictionary<TKey, TValue> instead of the "simulated" one done thanks for the client-assisted approach of the Tagging feature.

So, can't just FusionCache use it?

Actually no, for the reasons exposed at the beginning, meaning:

But a lot of users use FusionCache without L2 and/or a backplane, and without sharing a MemoryCache instance between multiple FusionCache instances, so... can't FusionCache do a "raw clear" in those cases?

Yes, yes it can!

This is in fact what FusionCache will do. So, if:

then when Clear() is invoked FusionCache will actually call Clear() on the underlying MemoryCache, and immediately wipe out the entire cache.

Can I say, again, nice 🙂 ?

angularsen commented 4 days ago

Sounds terrific!

My only thought was, should there be an option to opt-out of using MemoryCache.Clear()?

We do resolve IMemoryCache from DI in a couple of places and do things with it directly for mostly legacy reasons, and I would not want that to disappear if clearing via FusionCache - I would expect to only clear stuff I have set via FusionCache.

Or does FusionCache use its own instance of MemoryCache so this is not a problem?

jodydonetti commented 4 days ago

Sounds terrific!

Great 😬

My only thought was, should there be an option to opt-out of using MemoryCache.Clear()?

I thought about this, but cannot see a concrete reason why. An actual underlying Clear() is what would be desirable, if not for all the other complications.

We do resolve IMemoryCache from DI in a couple of places and do things with it directly for mostly legacy reasons, and I would not want that to disappear if clearing via FusionCache - I would expect to only clear stuff I have set via FusionCache.

That is exactly what will happen! One of the criteria I listed is "if the underlying MemoryCache is owned", meaning FusionCache created it, so that check solves this problem.

Or does FusionCache use its own instance of MemoryCache so this is not a problem?

Both via DI and directly (eg: via new()) you can pass FusionCache a MemoryCache instance or not (and in that case it will create its own, which is the default behavior): if you don't pass anything it means it's totally owned by FusionCache, and that is when the actual underlying Clear() kicks in.

Thoughts?

angularsen commented 4 days ago

Yes that addresses my concern, we don't specify a MemoryCache instance for FusionCache so then it will create and own it and work as you described. Perfect!