ZiggyCreatures / FusionCache

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

[FEATURE] 🦅 Eager Refresh #92

Closed jodydonetti closed 1 year ago

jodydonetti commented 1 year ago

Scenario

Say we want to "cache something for 10min".

Easy peasy, we can do something like this:

var id = 42;

cache.GetOrSet<Product>(
    $"product:{id}",
    _ => GetProductFromDb(id),
    options => options.SetDuration(TimeSpan.FromMinutes(10))
);

Sometimes though we may want to do something like "cache something for 10min, but start refreshing it some time before expiration so that at the 10min mark there would not be a slowdown because of the refresh operation".

With FusionCache this has always been possible, thanks to fail-safe + soft/hard timeouts: we just have to change the way to pose the requirement to something like "cache something for 10min, but in case the refresh will take more than (say) 10ms just temporarily reuse the stale value so there would not be a slowdown because of the refresh operation".

The code needed would be:

var id = 42;

cache.GetOrSet<Product>(
  $"product:{id}",
  _ => GetProductFromDb(id),
  options => options
    .SetDuration(TimeSpan.FromMinutes(10))
    .SetFailSafe(true)
    .SetFactoryTimeouts(TimeSpan.FromMilliseconds(10))
);

The end result is basically the same (no delays when refreshing), but the thing needed is a mental shift in how to think about what to do.

Problem

Now, here's the deal: it would be nice to be able to just specify "eagerly refresh some time before the expiration" or something like that, instead of having to change the mental model.

This approach is also not completely new: in the caching field there are things like the StaleAfter option in the CacheTower library, or the "Cache Prefreshing" option in the Akamai CDN.

Solution

It seems reasonable to provide a way to obtain the same result but with a direct and more clear approach, even just to lower the mental gymnastics needed and to lower the entry barrier.

Finally, this approach may be used in conjunction with the aforementioned existing features (fail-safe and timeouts), so that we may be able to either use eager refresh without fail-safe (if so desired) or to use all of them together.

Design proposal

A new addition in the FusionCacheEntryOptions class, to be able to specify how eagerly to start the refresh, even if the cache entry is not yet expired.

There are 2 possible ways to specify "how eagerly".

TimeSpan

As a TimeSpan this would be a direct value, like TimeSpan.FromSeconds(10).

Percentage

As a percentage, in the usual floating point notation: an example may be 0.9, meaning 90% (of the Duration).

Because of the reasons above, it seems clear that the percentage approach would be better, so this will be explored in an impl and see how it goes.

Also, although this does not imply anything in particular, it gives some confidence knowing that the Akamai CDN actually uses the percentage approach: this is, at least, a point in favor of such approach, since it has been widely used in a battle tested production environment with success.

One additional idea may be to have support for both: this solution though would mean worse performance (more memory consumed to store both of the values). Also, it would probably create some confusion about what approach to use, and what may happen when setting both values (which one should win? should setting one value reset the other? etc). Finally, for the reasons explained above, it may possibly be more error prone: for example by specifying a Duration of 10min and an eager refresh of 9min, only to later change the Duration to 20min and forgetting to update the eager refresh to 18min (or whatever would be the related new value).

Alternatives

As described at the beginning, the current approach of fail-safe + timeouts may get you the same approach, but it seems to require more mental gymnastics.

Finally, there may be a use-case for using the 3 features together: eager refresh + fail-safe + timeouts, which may be nice.

Technical Details

Of course in a highly concurrent scenario, only one request would start an eager refresh: this is the same Cache Stampede prevention that happens when normally running a factory to refresh the data after expiration, so the same mechanism should also be used here for the same reasons.

Additionally, during an eager refresh the underlying cache entry is not yet expired, so only one call should obtain the mutex and start the background refresh, while all the others should simply skip it: this can be done by trying to acquire the mutex with a timeout of zero. This would allow only the first request arrived after the passing of the eager refresh to get the mutex and start the background refresh, while all the other requests would simply see that the mutex is already "taken" and move on by using the current value.

Some benchmarks should be made to ensure that the performance does not degrade (or anyway, at least in a reasonable way) between a series of calls with and without eager refresh enabled, in each phase (before the "eager threshold" is hit, and after that).

Finally it should be safe to hit the actual expiration even when an eager refresh is still running, and maybe decide what should happen in such an edge case.

jodydonetti commented 1 year ago

Hi all, v0.21.0-preview1 is out which includes this, too.

jodydonetti commented 1 year ago

Hi all, I just release v0.21.0 which includes this 🎉