ZiggyCreatures / FusionCache

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

[FEATURE] Please support `TryGetWithTrySet` #299

Open jvmlet opened 1 week ago

jvmlet commented 1 week ago

Problem

Cache entry factory method might return null value, it's easier to just have shortcut like this

MaybeValue<Something> v = _cache.TryGetWithTrySet(key, ()=> return MaybeValue of Something)

rather than

var smthng= _cache.TryGet<Something>(key);
if (!smthng.HasValue) {
      something= // try fetch it 
      if(something.HasValue){
         _cache.Set(key, something.Value);
      }

 }
jvmlet commented 1 week ago

Just noticed the latest Fail-Safe Without Exceptions feature .... so I suggest the same functionality only with syntax sugar by returning MaybeValue.NONE from factory function.

BTW, which value returns GetOrSet if ctx.Fail(message) was called ? default/null?

jodydonetti commented 1 week ago

Problem

Cache entry factory method might return null value

Yes, and NOT caching it is usually a mistake. The usual example is what happens if you have a website with a url like /product/{id} and I keep calling the url /product/999999 ? That is an self-inflicted DoS, beause for every single call you'll go to the database, get back nothing and don't cache it, rinse and repeat.

What you can do instead is simply use GetOrSet() with adaptive caching and, in case of null, you set the Duration to something lower than normal, like:

or something like that. This would still protect you from Cache Stampede and similar problem, while wllowing you to cache nulls for a very short amount of time.

it's easier to just have shortcut like this

MaybeValue<Something> v = _cache.TryGetWithTrySet(key, ()=> return MaybeValue of Something)

rather than

var smthng= _cache.TryGet<Something>(key);
if (!smthng.HasValue) {
      something= // try fetch it 
      if(something.HasValue){
         _cache.Set(key, something.Value);
      }

 }

This code has a couple of problems: 1) by using a GET and SET separate calls (TryGet() + Set()), you are not protected from Cache Stampede (without using GetOrSet() it's impossible to coordinate the 2 calls) 2) as said in the first part of the answer, if you only call Set() when the fetch is successful, 10 consecutive (or even parallel) calls will all go the the database, probably overloading it

Hope this helps!

jodydonetti commented 1 week ago

BTW, which value returns GetOrSet if ctx.Fail(message) was called ? default/null?

If fail-safe is enabled AND there's a stale value to use, the stale value will be returned (that's the whole point of fail-safe).

If instead fail-safe is not enabled or there's no stale value to use, an exception will be thrown (with the specified message).

I'm exploring a future TryGetOrSet() method that is basically a TryGet() + GetOrSet() combination, and that would return a MaybeValue<T> when there's a problem, but it's not ready yet.

Hope this helps.

jodydonetti commented 1 day ago

Hi @jvmlet , any thoughts on this?

jvmlet commented 1 day ago

Too much if... then, IMO. Public API has to be easy to understand and leave no place for ambiguity. I think that retuning Maybe from setter is more friendly than setting flag on context+configuring other flags that define the behavior. User can then use adaptive cache feature to define whether or not to cache empty value returned from setter.

jodydonetti commented 1 day ago

Too much if... then, IMO. Public API has to be easy to understand and leave no place for ambiguity.

Ok but, I mean... hybrid multi-level caching with resiliency features, soft-healing, and so on all in one package is a quite complex beast, and not everything can be reduced to an easy peasy yes/no.

Anyway, how would you design an unambiguous, easy to use public API for this (but, mind you, still support all the options and features available)? Maybe I can get some inspiration for some potential changes.

I think that retuning Maybe from setter is more friendly than setting flag on context+configuring other flags that define the behavior

I think you meant "factory" and not "setter", and in that case: if the factory is (simplifying) Func<T> it cannot return a MaybeValue<T> instead (that's the C# language). Also, how would you define all the other "flags" to tweak the behaviour then?

Anyway, let's play with your idea: returning MaybeValue would mean "don't cache it", right?

Now imagine the next user coming along and saying "Hey, I'm caching MaybeValues, and I noticed that if I return a MaybeValue.None it's not getting cached, why is that?" and than we would need to explain that MaybeValue.None is a magic value that behave in a specific way, so whatever the factory returns will be cached but IF it is this special value then it will not be cached etc...

So they would say "Too much if... then, IMO. Public API has to be easy to understand and leave no place for ambiguity." and we would be back at square one.

Also, by not caching it there's still the problem I mentioned above (self inflicted DoS), so we should instead cache it for a small amount of time, but how much? It should be specifiable, and that is why I choose to do it via adaptive caching.

What do you think?

Since we are here, would you mind letting me know about the other points I've made? I'd like to know your POV on them.

These:

Yes, and NOT caching it is usually a mistake. The usual example is what happens if you have a website with a url like /product/{id} and I keep calling the url /product/999999 ? That is an self-inflicted DoS, beause for every single call you'll go to the database, get back nothing and don't cache it, rinse and repeat.

Also this:

I'm exploring a future TryGetOrSet() method that is basically a TryGet() + GetOrSet() combination, and that would return a MaybeValue when there's a problem, but it's not ready yet.

Would this be more in line with your expectations?