GithubHelp home page GithubHelp logo

ziggycreatures / fusioncache Goto Github PK

View Code? Open in Web Editor NEW
1.3K 16.0 75.0 4.77 MB

FusionCache is an easy to use, fast and robust cache with advanced resiliency features and an optional distributed 2nd level.

License: MIT License

C# 99.98% Batchfile 0.01% Shell 0.01%
caching cache multi-level performance async dotnet csharp stampede cache-stampede redis

fusioncache's Introduction

FusionCache logo

FusionCache

License: MIT Nuget

๐Ÿ™‹โ€โ™‚๏ธ Updating to v1.0.0 ? please read here.

FusionCache is an easy to use, fast and robust cache with advanced resiliency features and an optional distributed 2nd level.

It was born after years of dealing with all sorts of different types of caches: memory caching, distributed caching, http caching, CDNs, browser cache, offline cache, you name it. So I've tried to put together these experiences and came up with FusionCache.

FusionCache diagram

It uses a memory cache (any impl of the standard IMemoryCache interface) as the primary backing store and optionally a distributed, 2nd level cache (any impl of the standard IDistributedCache interface) as a secondary backing store for better resilience and higher performance, for example in a multi-node scenario or to avoid the typical effects of a cold start (initial empty cache, maybe after a restart).

Optionally, it can also use a backplane: in a multi-node scenario this will send notifications to the other nodes to keep all the memory caches involved perfectly synchronized, without any additional work.

FusionCache also includes some advanced resiliency features like cache stampede prevention, a fail-safe mechanism, fine grained soft/hard timeouts with background factory completion, customizable extensive logging and more (see below).

๐Ÿ† Award

Google Award

On August 2021, FusionCache received the Google Open Source Peer Bonus Award: here is the official blogpost.

๐Ÿ“• Getting Started

With ๐Ÿฆ„ A Gentle Introduction you'll get yourself comfortable with the overall concepts.

Want to start using it immediately? There's a โญ Quick Start for you.

Curious about what you can achieve from start to finish? There's a ๐Ÿ‘ฉโ€๐Ÿซ Step By Step guide.

More into videos? The fine folks at On .NET have been kind enough to invite me on the show and listen to me mumbling random caching stuff.

On .NET Talk

โœ” Features

These are the key features of FusionCache:

Something more ๐Ÿ˜ ?

Also, FusionCache has some nice additional features:

  • โœ… Portable: targets .NET Standard 2.0, so it can run almost everywhere
  • โœ… High Performance: FusionCache is optimized to minimize CPU usage and memory allocations to get better performance and lower the cost of your infrastructure all while obtaining a more stable, error resilient application
  • โœ… Null caching: explicitly supports caching of null values differently than "no value". This creates a less ambiguous usage, and typically leads to better performance because it avoids the classic problem of not being able to differentiate between "the value was not in the cache, go check the database" and "the value was in the cache, and it was null"
  • โœ… Circuit-breaker: it is possible to enable a simple circuit-breaker for when the distributed cache or the backplane become temporarily unavailable. This will prevent those components to be hit with an excessive load of requests (that would probably fail anyway) in a problematic moment, so it can gracefully get back on its feet. More advanced scenarios can be covered using a dedicated solution, like Polly
  • โœ… Dynamic Jittering: setting JitterMaxDuration will add a small randomized extra duration to a cache entry's normal duration. This is useful to prevent variations of the Cache Stampede problem in a multi-node scenario
  • โœ… Cancellation: every method supports cancellation via the standard CancellationToken, so it is easy to cancel an entire pipeline of operation gracefully
  • โœ… Code comments: every property and method is fully documented in code, with useful informations provided via IntelliSense or similar technologies
  • โœ… Fully annotated for nullability: every usage of nullable references has been annotated for a better flow analysis by the compiler

๐Ÿ“ฆ Packages

Main packages:

Package Name Version Downloads
ZiggyCreatures.FusionCache
The core package
NuGet Nuget
ZiggyCreatures.FusionCache.OpenTelemetry
Adds native support for OpenTelemetry setup
NuGet Nuget
ZiggyCreatures.FusionCache.Chaos
A package to add some controlled chaos, for testing
NuGet Nuget

Serializers:

Package Name Version Downloads
ZiggyCreatures.FusionCache.Serialization.NewtonsoftJson
A serializer, based on Newtonsoft Json.NET
NuGet Nuget
ZiggyCreatures.FusionCache.Serialization.SystemTextJson
A serializer, based on the new System.Text.Json
NuGet Nuget
ZiggyCreatures.FusionCache.Serialization.NeueccMessagePack
A MessagePack serializer, based on the most used MessagePack serializer on .NET
NuGet Nuget
ZiggyCreatures.FusionCache.Serialization.ProtoBufNet
A Protobuf serializer, based on one of the most used protobuf-net serializer on .NET
NuGet Nuget
ZiggyCreatures.FusionCache.Serialization.CysharpMemoryPack
A serializer based on the uber fast new serializer by Neuecc, MemoryPack
NuGet Nuget
ZiggyCreatures.FusionCache.Serialization.ServiceStackJson
A serializer based on the ServiceStack JSON serializer
NuGet Nuget

Backplanes:

Package Name Version Downloads
ZiggyCreatures.FusionCache.Backplane.Memory
An in-memory backplane (mainly for testing)
NuGet Nuget
ZiggyCreatures.FusionCache.Backplane.StackExchangeRedis
A Redis backplane, based on StackExchange.Redis
NuGet Nuget

Third-party packages:

Package Name Version Downloads
JoeShook.ZiggyCreatures.FusionCache.Metrics.Core NuGet Nuget
JoeShook.ZiggyCreatures.FusionCache.Metrics.EventCounters NuGet Nuget
JoeShook.ZiggyCreatures.FusionCache.Metrics.AppMetrics NuGet Nuget

โญ Quick Start

FusionCache can be installed via the nuget UI (search for the ZiggyCreatures.FusionCache package) or via the nuget package manager console:

PM> Install-Package ZiggyCreatures.FusionCache

As an example, imagine having a method that retrieves a product from your database:

Product GetProductFromDb(int id) {
	// YOUR DATABASE CALL HERE
}

๐Ÿ’ก This is using the sync programming model, but it would be equally valid with the newer async one for even better performance.

To start using FusionCache the first thing is create a cache instance:

var cache = new FusionCache(new FusionCacheOptions());

If instead you are using DI (Dependency Injection) use this:

services.AddFusionCache();

We can also specify some global options, like a default FusionCacheEntryOptions object to serve as a default for each call we'll make, with a duration of 2 minutes:

var cache = new FusionCache(new FusionCacheOptions() {
	DefaultEntryOptions = new FusionCacheEntryOptions {
		Duration = TimeSpan.FromMinutes(2)
	}
});

Or, using DI, like this:

services.AddFusionCache()
	.WithDefaultEntryOptions(new FusionCacheEntryOptions {
		Duration = TimeSpan.FromMinutes(2)
	})
;

Now, to get the product from the cache and, if not there, get it from the database in an optimized way and cache it for 30 sec (overriding the default 2 min we set above) simply do this:

var id = 42;

cache.GetOrSet<Product>(
	$"product:{id}",
	_ => GetProductFromDb(id),
	TimeSpan.FromSeconds(30)
);

That's it ๐ŸŽ‰

Want a little bit more ๐Ÿ˜ ?

Now, imagine we want to do the same, but also:

  • set the priority of the cache item to High (mainly used in the underlying memory cache)
  • enable fail-safe for 2 hours, to allow an expired value to be used again in case of problems with the database (read more)
  • set a factory soft timeout of 100 ms, to avoid too slow factories crumbling your application when there's a fallback value readily available (read more)
  • set a factory hard timeout of 2 sec, so that, even if there is no fallback value to use, you will not wait undefinitely but instead an exception will be thrown to let you handle it however you want (read more)

To do all of that we simply have to change the last line (reformatted for better readability):

cache.GetOrSet<Product>(
	$"product:{id}",
	_ => GetProductFromDb(id),
	// THIS IS WHERE THE MAGIC HAPPENS
	options => options
		.SetDuration(TimeSpan.FromSeconds(30))
		.SetPriority(CacheItemPriority.High)
		.SetFailSafe(true, TimeSpan.FromHours(2))
		.SetFactoryTimeouts(TimeSpan.FromMilliseconds(100), TimeSpan.FromSeconds(2))
);

Basically, on top of specifying the cache key and the factory, instead of specifying just a duration as a TimeSpan we specify a FusionCacheEntryOptions object - which contains all the options needed to control the behavior of FusionCache during each operation - in the form of a lambda that automatically duplicates the default entry options defined before (to copy all our defaults) while giving us a chance to modify it as we like for this specific call.

Now let's say we really like these set of options (priority, fail-safe and factory timeouts) and we want them to be the overall defaults, while keeping the ability to change something on a per-call basis (like the duration).

To do that we simply move the customization of the entry options where we created the DefaultEntryOptions, by changing it to something like this (the same is true for the DI way):

var cache = new FusionCache(new FusionCacheOptions() {
	DefaultEntryOptions = new FusionCacheEntryOptions()
		.SetDuration(TimeSpan.FromMinutes(2))
		.SetPriority(CacheItemPriority.High)
		.SetFailSafe(true, TimeSpan.FromHours(2))
		.SetFactoryTimeouts(TimeSpan.FromMilliseconds(100), TimeSpan.FromSeconds(2))
});

Now these options will serve as the cache-wide default, usable in every method call as a "starting point".

Then, we just change our method call to simply this:

var id = 42;

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

The DefaultEntryOptions we did set before will be duplicated and only the duration will be changed for this call.

๐Ÿ‘ฉโ€๐Ÿซ Step By Step

If you are in for a ride you can read a complete step by step example of why a cache is useful, why FusionCache could be even more so, how to apply most of the options available and what results you can expect to obtain.

FusionCache diagram

๐Ÿ–ฅ๏ธ Simulator

Distributed systems are, in general, quite complex to understand.

When using FusionCache with the distributed cache, the backplane and auto-recovery the Simulator can help us seeing the whole picture.

FusionCache Simulator

๐Ÿงฐ Supported Platforms

FusionCache targets .NET Standard 2.0 so any compatible .NET implementation is fine: this means .NET Framework (the old one), .NET Core 2+ and .NET 5/6/7/8+ (the new ones), Mono 5.4+ and more (see here for a complete rundown).

NOTE: if you are running on .NET Framework 4.6.1 and want to use .NET Standard packages Microsoft suggests to upgrade to .NET Framework 4.7.2 or higher (see the .NET Standard Documentation) to avoid some known dependency issues.

๐Ÿ†Ž Comparison

There are various alternatives out there with different features, different performance characteristics (cpu/memory) and in general a different set of pros/cons.

A feature comparison between existing .NET caching solutions may help you choose which one to use.

๐Ÿ’ฐ Support

Nothing to do here.

After years of using a lot of open source stuff for free, this is just me trying to give something back to the community.

If you find FusionCache useful just โœ‰ drop me a line, I would be interested in knowing how you're using it.

And if you really want to talk about money, please consider making โค a donation to a good cause of your choosing, and let me know about that.

๐Ÿ’ผ Is it Production Ready โ„ข๏ธ ?

Yes!

FusionCache is being used in production on real world projects for years, happily handling millions of requests.

Considering that the FusionCache packages have been downloaded more than 4 million times (thanks everybody!) it may very well be used even more.

And again, if you are using it please โœ‰ drop me a line, I'd like to know!

fusioncache's People

Contributors

alb-xss avatar alexmaek avatar conmur avatar jaxelr avatar jeffreym avatar jensenbw avatar jodydonetti avatar joeshook avatar luizhtm avatar martindisch avatar martinobordin avatar mauroservienti avatar michaellwest avatar phaza avatar seyfeb avatar simo-pro avatar simoncropp avatar thompson-tomo avatar turnerj avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

fusioncache's Issues

Cache keys prefix

Hi.

I've noticed that all my keys are getting prefixed with v1. Why is this happening? Is it possible to remove the v1 prefix?

Function results can set `FusionCacheEntryOptions.Duration`

I have a use case where the returned data from a function passed to FusionCache contains the TTL (time to live), kind of like a DNS query answer. I would like to use that result to set the duration from calls like GetOrSetAsync .

I see MemoryOptionsModifier was something that might have been the way to go but the property is marked obsolete and not wired in.

Of course I can call TryGetAsync and then SetAsync in the meantime. @jodydonetti curious about your thoughts on this?

[BUG] Absolute expiration time is not preserved during recovering from distributed cache to in-memory cache

The current implementation does not take into account the AbsolutExpiration set during the addition of the entry to the IDistributeCache

https://github.com/jodydonetti/ZiggyCreatures.FusionCache/blob/f1380b7c562eeb610c58490fd7e7f63bf69e0569/src/ZiggyCreatures.FusionCache/FusionCache_Sync.cs#L145

This may lead to the hidden extension of the lifetime of the entry (especially for the TryGet[Async] method).

As IDistributedCache does not return back the expiration time, it's possible to extend the Metadata payload with the calculated absolute expiration and use it during the creation of the entry in the in-memory cache.

Also, it is worth reflecting in the documentation that the current implementation uses FusionCacheEntryOptions for the restored entires.

Fail-safe getting or setting featuring a default value

Hi, if the cache is initally empty, even a fail-safe GetOrSet still throws an exception, since there is no expired value for the key which can be retrieved. There is a GetOrDefault, but from the otherwise excellent documentation and the example code alone it is not clear, how to combine these features. Is that really some kind of missing method (like a GetOrSetOrDefault feature or an overload with a default value) or is this by design? Either way, it would be awesome if this case is mentioned. Thank you very much!

[FEATURE] ๐Ÿง™โ€โ™‚๏ธ Adaptive Caching

Scenario

Almost every FusionCache method that act on cache entries - like Set, Remove, GetOrSet, etc - can receive various options via a param of type FusionCacheEntryOptions which contains things like Duration, Priority, FailSafeMaxDuration and so on.

This is all well and good but in the case of the GetOrSet method, there could be one more opportunity: the abilty to adapt the caching options to the value about to be cached when returned by the factory, if and when that is called.

Examples

For example if our factory retrieves a Product from a database, the cache duration may be small in case the product has just been created/updated (since it may receive additional updates soon) or very large in case a product is old and no more available to sell (since it most probably won't change anymore).
In this case the information to base the adaptive logic would be inside the product instance itself, making it self-contained.

A different example can be a resource, let's say a News piece in an online newspaper, gathered not via a database call but via a remote webservice call: in the http response we may have a Cache-Control header which can drive the cache duration inside FusionCache itself.
In this case the information to base the adaptive logic would NOT be inside the news instance itself, making it not self-contained.

Solution

I want to add a way to change the FusionCacheEntryOptions param passed to a GetOrSet method call based on the value produced by a factory (or adjacent data, see the previous News example).

There are different ways to achieve this, and I'd like to highlight them, with pros and cons.

๐Ÿ’ก NOTE
Regardless of which proposal will be choosen, new overloads will be added to match the currently available methods, so that method calls in already existing code will not need to be fixed.

Proposal 01: additional lambda

The idea here is to have an additional param of type Action<TValue, FusionCacheEntryOptions> so to be able to have a way to receive the value produced by the factory + the options param currently being used and change something there.

An example can be something like this:

var product = cache.GetOrSet<Product>(
    $"product:{id}",
    _ => GetProductFromDb(id),
    options => options.SetDuration(TimeSpan.FromMinutes(1)),
    (product, options) => {
      // IF THE PRODUCT IS EXPIRED -> CHANGE THE DURATION TO 1 HOUR
      if (product.ExpirationDate < DateTimeOffset.UtcNow) {
        options.SetDuration(TimeSpan.FromHours(1));
      }
    }
);

๐Ÿ‘ PROS:

  • we can easily take existing code and just add a new param to existing method calls to make the options adaptive
  • since we are talking about a separate param, we can define one or more and reuse the same delegate instance multiple times (eg: public static ProductUtils.AdaptCachingOptions(Product product, FusionCacheEntryOptions options) )
  • since the adaptive part here would be explicit, we would be easily able to save some allocations and avoid unwanted options change (see the "โš  Warning" section below)

๐Ÿ‘Ž CONS:

  • an additional lambda would need to be allocated, but only in case adaptivity is needed: when not needed, there would be no new allocations
  • in case the piece of data needed to adapt the options is not part of the returned value itself (eg: a Product instance) there would be no way to pass that to the adaptive logic. Practical example: the remote source for the factory is a webservice, and we would like to use the Cache-Control header of the http response to drive the cache duration: in the factory we would have access to the http response, before deserializing the response body to a Product instance, but not anymore in the separate lambda

โ„น NOTES:

  • the additional param would be optional with a default value of null which would indicate not to change the options, basically disabling adaptivity (and the extra allocation)

Proposal 02: new factory signature

The idea is to change the common signature for the factory param, from:

Func<CancellationToken, Task<TValue>>

to:

Func<CancellationToken, FusionCacheEntryOptions, Task<TValue>>

Again, new overloads to map the currently existing factory signature would be created, so existing code would continue working as usual.

๐Ÿ‘ PROS:

  • having everything directly in the same lambda (the factory itself) would make it possible to adapt the options based on data not part of the return value (eg: an http header not part of a Product instance, like in the 2nd cons of Proposal 01)
  • the signature would be quite similar to what is currently used in the common MemoryCache interface, therefore possibly lowering the entry barrier for new users
  • in theory the extra lambda allocation in the proposal 01 would not be needed, but...

๐Ÿ‘Ž CONS:

  • ... but, since internally the implementation would be just one (with the factory param with the new signature), any call with the old signature would need to be automatically "wrapped" in a lambda created on the fly that would turn one call into the other (eg: (token, options) => factory(token) ) and there it is another invisible allocation
  • since the adaptive part here would NOT be explicit, we wouldn't know if a factory will change options or not, making it harder to save some allocations and avoid unwanted options change (see the "โš  Warning" section below)

โ„น NOTES:

  • A possible way out of this would be to mark all the new overloads that map the old factory definition to the new one as [Obsolete] with an explanation message to use the new signature, and just move on. But I'm not that sure this is the right way to handle this situation, I'm still thinking about this

Proposal 03 (additional): new IObjectWithFusionCacheAdaptiveOptions interface

This is not a substitute for any of the 2 previous proposals, but may be a nice addition.
The idea is to have a new IObjectWithFusionCacheAdaptiveOptions interface modeled like this:

public interface IObjectWithFusionCacheAdaptiveOptions
{
  void AdaptFusionCacheEntryOptions(FusionCacheEntryOptions options);
}

and it would be used, following the Product example above, like this:

public class Product
  : IObjectWithFusionCacheAdaptiveOptions
{

  [...]

  public DateTimeOffset ExpirationDate { get; set; }

  [...]

  public void AdaptFusionCacheEntryOptions(FusionCacheEntryOptions options)
  {
    // IF THE PRODUCT IS EXPIRED -> CHANGE THE DURATION TO 1 HOUR
    if (ExpirationDate < DateTimeOffset.UtcNow) {
      options.SetDuration(TimeSpan.FromHours(1));
    }
  }
}

๐Ÿ‘ PROS:

  • quick and easy to use
  • in case proposal 1 is choosen, this can be used as a fallback when no logic is passed explicitly via the extra param. Otherwise that would win (since it would be explicitly passed per-call, instead of being per-type). As a comparison, think of this as the before/after serialization callbacks approach in Json.NET.

๐Ÿ‘Ž CONS:

  • none actually, since it's an extra way to achieve the goal

โ„น NOTES:

  • it may be considered a little bit more invasive, since you would have to directly touch your own classes. But, and this is important, since it would be an additional way to get the result, I don't see it as a problem, really
  • if this would be the only way to do this, the problem would be that it would not be usable with types we don't control, with sealed types or with primitive types. But since this would be just an additional way to do it, I see no problem with this approach also being available

โš  Warning

Special care must be put into NOT changing a FusionCacheEntryOptions instance when that is not the intention.

What do I mean?

A good way to save some allocations is to define and reuse the same options object over and over again, passing it to the same method call all the time.

Example:

public static class CachingDefaults
{
  public static FusionCacheEntryOptions ProductOptions = new FusionCacheEntryOptions()
  {
    Duration = TimeSpan.FromMinutes(1),
    IsFailSafeEnabled = true,
    FailSafeMaxDuration = TimeSpan.FromHours(24),
    FailSafeThrottleDuration = TimeSpan.FromSeconds(30),
    FactorySoftTimeout = TimeSpan.FromMilliseconds(100)
  };
  
  public static FusionCacheEntryOptions CategoryOptions = new FusionCacheEntryOptions()
  {
    Duration = TimeSpan.FromMinutes(10),
    IsFailSafeEnabled = true,
    FailSafeMaxDuration = TimeSpan.FromHours(12)
  };
}

[...]

var product = cache.GetOrSet<Product>(
  $"product:{42}",
  _ => GetProductFromDb(42),
  CachingDefaults.ProductOptions // THIS SAME INSTANCE WILL BE PASSED EVERY TIME
);

var category = cache.GetOrSet<Category>(
  $"category:{42}",
  _ => GetCategoryFromDb(42),
  CachingDefaults.CategoryOptions // THIS SAME INSTANCE WILL BE PASSED EVERY TIME
);

In this case the productOptions or the categoryOptions instances will be reused, greatly reducing the number of allocations.
But if we later decides to let the options be adaptive (in any of the proposed ways) and forget to create a new FusionCacheEntryOptions object every time, the options instance passed in would be changed, even for the following requests. A practical example is that the ProductOptions is set with a Duration of 1 min, but as soon as an old product will get requested with adaptive caching, the Duration would become 1 hour for all subsequent requests.

To avoid this an idea may be to just duplicate the options object, only in case adaptive caching needs to be applied (which would be possible btw only with Proposal 01, since the adaptive logic is explicitly in a separate param). This would solve the problem, but it would needlessly duplicate those options even in case the instance passed in is already a "throwaway" one. Ideally I should find a way to correctly identify which are reusable and which are not, but I need to experiment a little bit because I can already see some troubling edge cases.

Thoughts?

Any suggestion or observation would be really, really appreciated ๐Ÿ™.

[FEATURE] Add Protobuf support

Is your feature request related to a problem? Please describe.
FusionCache provides a way to add custom serialization format support, by implementing the IFusionCacheSerializer interface.

It already provides 2 implementations, both for the JSON format:

It recently added (but not yet released) a third one: MessagePack.

It would be great to add support for the Protobuf serialization format, too.

Describe the solution you'd like
A new package that add supports for the Protobuf format, probably based on protobuf-net by @mgravell which is probably the most used implementation on .NET and the most performant.

Describe alternatives you've considered
Everyone that needs it should otherwise implement their own, which is meh ๐Ÿ˜

Sliding Expiration

Hello,

I'd like to know if there is the possibility to set a sliding expiration so that if the cached object is updated or read, the expiration counter is restarted.

Is this possible? If not, are there other ways to acheive it?

Thanks in advance and keep up the good work!

[FEATURE] ๐Ÿ“ž Events

Scenario

While playing with the implementation of the backplane (see #11) and talking about adding metrics (see #9) the need emerged to be able to react to core events happening inside FusionCache.

Proposal

The plan is to add a set of core events to FusionCache (like hit, miss, etc) so it will be possible to subscribe to them.

An example of these events (still a draft):

  • Hit: high-level, "the entry was in the cache" (either memory or distributed)
  • Miss: high-level, "the entry was not in the cache" (both memory and distributed)
  • HitMemory: level-specific, the entry was in the memory cache
  • FactoryTimeout: the factory was run, but it timed out
  • FailSafeActivation: fail-safe has been activated
  • etc...

These events will NOT be used for the core flow, that is FusionCache will call subscribers to them but it will NOT need them to function properly or wait for them to return some results.

Also, the order in which subscribers will be called will not be guaranteed (for potential perf optimizations).

Design for Subscribe/Unsubscribe flow

There are different ways to model this flow:

  • keeping the handler around: the ubiquitous StackExchange.Redis package for example uses a model where both the Subscribe and Unsubscribe methods accept the handler simply as a lambda param (see here). This is very simple and lightweight (no extra allocations), but when using it with anonymous methods (created on the fly) it requires storing them in a variable to be able to later unbubscribe
  • disposables: when subscribing to an event you get back an IDisposable that can be used to unsubscribe later on, by simply calling Dispose() on it. This is very straightforward an it allows to simply collect the results of each Subscribe() call, maybe in a list, and later on just call Dispose() on all of them to unsubscrive from them all, but it incurs in an additional allocation (the disposable itself). Maybe a struct implementing IDisposable may alleaviate the allocation cost? Does it feel right?
  • Rx: I used it in the past to handle something like this, with subjects and whatnot. It worked well, uses the same disposable approach if I remember correctly and it also gives you composability, but it seems a little bit overkill for this. Also, I don't know if nowadays it is widly used in the .NET ecosystem ๐Ÿคท

Thoughts?

Any suggestion is more than welcome.

[FEATURE] TrySet api method

Hi @jodydonetti
firstly thanks for your contribution to the community, we have just started to use FusionCache on production and has been working flawesly since then. We are constantly trying to use it in different case scenarios, like the one I'm exposing to you right now. In our case, we are using the backplane with several nodes synchronized with redis.

We are considering using fusion cache as a deduplicator for streaming messages coming from different sources. Right now the best solution we have came was using the two core methods TryGet and Set in a way that it mimics the sliding expiration, but if the same request happens in different nodes with the same key, the set method will be called twice and the deduplicator won't be able to handle correctly that scenario.

public async Task<bool> IsDuplicated(string deviceName, DateTime timestamp)
{
    var key = LocationCacheKey(deviceName, timestamp);
    var isProcessed = await _cache.TryGetAsync<int>(key);
    if (_logger.IsEnabled(LogLevel.Information))
    {
        if (isProcessed.HasValue)
        {
            _logger.LogInformation("Device {Device} location with timestamp {Timestamp} duplicated, refreshing duration", deviceName, timestamp.ToString("O"));
        }
        else
        {
            _logger.LogInformation("Device {Device} location with timestamp {Timestamp} not duplicated, processing location and setting the cache", deviceName, timestamp.ToString("O"));
        }
    }
    await _cache.SetAsync<int>(key, 1, EntryOptions);
    return isProcessed.HasValue;
}

The GetOrSet method won't be a fit to this case because as per the documentation, this 'problem' also exists. And we also need the "sliding expiration" we mimic using the Set method.

We have considered using just the StackExchange redis client, but we would lost the in memory cache.
The solution that we would like to have is the one I expose below, using the TrySet method. In that case the value won't be set twice because the TrySet would return false/null and the other nodes will know that is a duplicate.

public async Task<bool> IsDuplicated(string deviceName, DateTime timestamp)
{
    var key = LocationCacheKey(deviceName, timestamp);
    var isProcessed = await _cache.TryGetAsync<int>(key);
    if (isProcessed.HasValue)
    {
        // Refresh duration
        _logger.LogInformation("Device {Device} location with timestamp {Timestamp} duplicated, refreshing duration", deviceName, timestamp.ToString("O"));
        await _cache.SetAsync<int>(key, 1, EntryOptions);
        return true;
    }
    
    // Try set the key
    var isSet = await _cache.TrySetAsync<int>(key, 1, EntryOptions);
    if (isSet)
    {
        _logger.LogInformation("Device {Device} location with timestamp {Timestamp} not duplicated, processing location and setting the cache", deviceName, timestamp.ToString("O"));
    }
    else
    {
        _logger.LogInformation("Device {Device} location with timestamp {Timestamp} duplicated when trying to set the key", deviceName, timestamp.ToString("O"));
    }
    
    return !isSet;
}

I know using just the Redis client would be possible because of the SET command which has the NX option
NX -- Only set the key if it does not already exist. but, since fusion cache is using IDistributedCache, I believe that won't be easy to fit, but I'm making this proposal so you can consider it.

Please let me know if you need more context or I can help you.

[FEATURE] Add MemoryPack support

Is your feature request related to a problem? Please describe.
FusionCache provides a way to add custom serialization format support, by implementing the IFusionCacheSerializer interface.

It already provides 4 implementations:

It would be great to add support for the new super optimized binary format by @neuecc , MemoryPack !

Describe the solution you'd like
A new package that add supports for the MemoryPack format, based on MemoryPack.

Describe alternatives you've considered
Everyone that needs it should otherwise implement their own, which is meh ๐Ÿ˜

FusionCacheEntryOptions.IsFailSafeEnabled Ignored

Hey @jodydonetti,

I don't think is a major issue, but it did trip up a unit test I was working on. In some scenarios it seems like IsFailsSafeEnabled = false is ignored. Here's an example.

[Fact]
public void IsFailSafeEnabledIgnoredHere()
{
	using var cache = new FusionCache(new FusionCacheOptions()
	{
		DefaultEntryOptions = new FusionCacheEntryOptions()
		{
			IsFailSafeEnabled = true,
			Duration = TimeSpan.FromSeconds(1),
			FailSafeMaxDuration = TimeSpan.FromSeconds(2)
		}
	});

	const string cacheKey = "cacheKey";
	cache.Set(cacheKey, "someValue");

	var cacheItem = cache.TryGet<string>(cacheKey);
	Assert.True(cacheItem.HasValue);

	Thread.Sleep(1500);

	cacheItem = cache.TryGet<string>(cacheKey);
	Assert.True(cacheItem.HasValue);

	cacheItem = cache.TryGet<string>(cacheKey, new FusionCacheEntryOptions() { IsFailSafeEnabled = false });
	Assert.False(cacheItem.HasValue); //Fails here

	Thread.Sleep(2001);

	cacheItem = cache.TryGet<string>(cacheKey);
	Assert.False(cacheItem.HasValue);
}

[Fact]
public void IsFailSafeEnabledWorksAsExpected()
{
	using var cache = new FusionCache(new FusionCacheOptions()
	{
		DefaultEntryOptions = new FusionCacheEntryOptions()
		{
			IsFailSafeEnabled = true,
			Duration = TimeSpan.FromSeconds(1),
			FailSafeMaxDuration = TimeSpan.FromSeconds(2)
		}
	});

	const string cacheKey = "cacheKey";
	cache.Set(cacheKey, "someValue");

	var cacheItem = cache.TryGet<string>(cacheKey);
	Assert.True(cacheItem.HasValue);

	Thread.Sleep(1500);

	cacheItem = cache.TryGet<string>(cacheKey, new FusionCacheEntryOptions() { IsFailSafeEnabled = false });
	Assert.False(cacheItem.HasValue); //Is false here

	Thread.Sleep(2001);

	cacheItem = cache.TryGet<string>(cacheKey);
	Assert.False(cacheItem.HasValue);
}

[Fact]
public void IsFailSafeEnabledWorksAsExpected2()
{
	using var cache = new FusionCache(new FusionCacheOptions()
	{
		DefaultEntryOptions = new FusionCacheEntryOptions()
		{
			IsFailSafeEnabled = true,
			Duration = TimeSpan.FromSeconds(1),
			FailSafeMaxDuration = TimeSpan.FromSeconds(2)
		}
	});

	const string cacheKey = "cacheKey";
	cache.Set(cacheKey, "someValue");

	var cacheItem = cache.TryGet<string>(cacheKey);
	Assert.True(cacheItem.HasValue);

	Thread.Sleep(1500);

	cacheItem = cache.TryGet<string>(cacheKey, new FusionCacheEntryOptions());
	Assert.False(cacheItem.HasValue);

	cacheItem = cache.TryGet<string>(cacheKey, new FusionCacheEntryOptions() { IsFailSafeEnabled = false });
	Assert.False(cacheItem.HasValue); //Is false here

	Thread.Sleep(2001);

	cacheItem = cache.TryGet<string>(cacheKey);
	Assert.False(cacheItem.HasValue);
}

Are my assumptions incorrect on how to use FusionCacheEntryOptions? This was only an issue if a null FusionCacheEntryOptions is provided on the first TryGet

Question: Are Memory Cache Updates Shared Between Multiple Nodes?

Thanks for sharing your amazing library!

Pretend Scenario:

I have FusionCache configured with a secondary cache. I am running multiple instances/nodes of my ASP.NET Core web application. I have a value I am caching for 10 minutes.

In Node A this value is updated from X to Z and I call FusionCache's SetAsync() to update the value in the primary and secondary cache. In Node B the old value, X, was read and cached 1 minute before it was updated in Node A.

Question:

If GetOrSetAsync() is called 2 minutes later in Node B, will it still return the old value, X, from it's primary cache? Or, is there some magical mechanism to push primary cache invalidations across all nodes?

"Don't do that" is a perfectly valid response. Open to any insights on how to handle this corner case or how to avoid getting into this case. Thanks!

Minimum log level

Is your feature request related to a problem? Please describe.

Despite configuring all minimum log levels to Error, there are still lots of logs spamming my Serilog sinks with Debug level like these:

[12:03:15 DBG] FUSION (O=9ff57fa21f0b4df5968ceda1f0289538 K=MyKey): memory entry found FE[]
[12:03:15 DBG] FUSION (O=9ff57fa21f0b4df5968ceda1f0289538 K=MyKey): return FE[]
[12:03:15 DBG] FUSION (O=e4c2667cd74d49198de1f16e981e0484 K=MyKey): calling GetOrSetAsync<T> FEO[LKTO=/ DUR=06:00:00 JIT=0 PR=N FS=N FSMA
X=1.00:00:00 FSTHR=30s FSTO=0 FHTO=/ TOFC=Y DSTO=/ DHTO=/ ABDO=N BN=Y BBO=Y]

I would like to configure a minimum log level for FusionCache, in addition to the existing log level options for individual groups of logs.

Describe the solution you'd like
Add FusionCacheOptions.MinimumLogLevel, which controls the minimum log level. Defaults to Verbose.

Describe alternatives you've considered
Followed instructions in https://github.com/jodydonetti/ZiggyCreatures.FusionCache/blob/main/docs/StepByStep.md#9-logging.
Explored all configurable options, could not find any.

Workaround is to configure Serilog to set a minimun log level override for these loggers.

.MinimumLevel.Override("ZiggyCreatures.Caching.Fusion.FusionCache", LogEventLevel.Information)

How to use FusionCache with prefetching?

(This is my first time using GitHub, so if I'm doing something wrong, please don't hesitate to tell me)

Hello,

I am considering using FusionCache for a project and I am currently testing it out.
I have a scenario that I don't know how to solve with my current knowledge about FusionCache, hence this question.

The scenario is that I want to prefill the cache with data, but then let the GetOrSet factory stuff handle cache misses, timeouts, cache refreshes and all that good stuff after the cache have been filled.
I want to prefill the cache from a database and the important detail about this scenario is that i only want to call the database once, when prefilling.

Example:

I have 10.000.000 key value pairs in a db, and i know the ids upfront.
I have a method that takes a single id called GetValue(int id), which accesses the db.

I could prefill the cache by doing GetOrSet({id}, _ => GetValue(id)) for each of the ids.
But to my understanding this would go to the db 10M times, which is the reason I want to go to the db once.

So conceptually I would like to do something like

  • Call a method called GetAllValues()
  • Use Set to set them individually for each of them
  • Call GetOrSet({id}, _ => GetValue(id)) for each of them

But that just seems... a bit hacky to me as i neither want to get nor set the value, i just want to set the factory up

I hope it makes sense, else I am happy to provide further information or input :)

Cache Regions?

A discussion has started around the possibility of introducing the concept of "cache regions" (see #33 ): there it is possible to see the potential problems involved, and some possible ways around those.

Thanks to @jasenf to for bringing that up and providing his experience on the subject.

This issue will be used to track the hypothetical design, limitations and feature set around regions.

Question : How are cache invalidations handled?

Say I have a web application running on 4 servers that uses FusionCache with a Redis secondary cache and I remove a cache entry from one of the web applications. It looks like the local FusionCache will be removed and the Redis secondary cache will have it's entry removed but how will the other 3 servers know to clear their FusionCache entries?

๐Ÿš€ Switch from Task<T> to ValueTask<T>

A good way to reduce the amount of allocations would be to switch from Task<T> to ValueTask<T>.

In theory (and in practice, at least on a couple of projects where I tested it) there should be no change in code already using FusionCache, at least if that code is not doing something esoteric.

When calling async methods by using the standard await keyword the changes required are effectively none.

More info here https://blog.marcgravell.com/2019/08/prefer-valuetask-to-task-always-and.html , with the great Marc Gravell explaining everything way better than me.

Opinions?

ZiggyCreatures.FusionCache.Backplane.Memory Seems to Prevent Cache From Being Used

I had tried out ZiggyCreatures.FusionCache.Backplane.Memory for local development with a single instance. Maybe I didn't set it up right, but if I set a breakpoint in events for cache.Events.Set and either cache.Events.Remove or cache.Events.Memory.Eviction (sorry, I don't remember which it was hitting), then each time a cache value was set, it was immediately removed. Effectively, the cache was never used and the factory was always called.

After I removed this line, then everything worked fine and dandy:
services.AddFusionCacheMemoryBackplane();

I didn't really have any good reason for using it for local dev and am fine without it, so I'm good with closing this issue immediately if you like.

SetAsync<TValue> not throwing when failed

I'm using the library to store values in Redis. When saving data with the SetAsync method I've found that it doesn't fail the task when an exception occurs when saving the data. I would like to be able to recover from this scenario as I need to ensure that the data was saved successfully. Whats the best way of handling this scenario?, digging into the code I see that the RunAsyncActionAdvancedAsync in FusionCacheExecutionUtils has a reThrow attribute that would solve the issue, but it is always being passed as false with no way to override it. I believe I could simply fetch the value afterwards with TryGet but think it would be easier if there was a way to know if the Set method failed.

Thanks for your help. I'm really liking the library so far.

Newtonsoft.Json.JsonSerializationException

Hi,
The more we use the library, the more we love it, but there is odd issue happens from time to time:

Newtonsoft.Json.JsonSerializationException: Type specified in JSON 'ZiggyCreatures.Caching.Fusion.Internals.Distributed.FusionCacheDistributedEntry`1[[System.Object, System.Private.CoreLib, Version=6.0.0.0, Culture=neutral, PublicKeyToken=7cec85d7bea7798e]], ZiggyCreatures.FusionCache, Version=0.1.9.0, Culture=neutral, PublicKeyToken=null' is not compatible with 'ZiggyCreatures.Caching.Fusion.Internals.Distributed.FusionCacheDistributedEntry`1[[System.Collections.Generic.IEnumerable`1[[infra.Models.Notification, infra.Models, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null]], System.Private.CoreLib, Version=6.0.0.0, Culture=neutral, PublicKeyToken=7cec85d7bea7798e]], ZiggyCreatures.FusionCache, Version=0.1.9.0, Culture=neutral, PublicKeyToken=null'. Path '$type', line 1, position 171.
   at Newtonsoft.Json.Serialization.JsonSerializerInternalReader.ResolveTypeName(JsonReader reader, Type& objectType, JsonContract& contract, JsonProperty member, JsonContainerContract containerContract, JsonProperty containerMember, String qualifiedTypeName)
   at Newtonsoft.Json.Serialization.JsonSerializerInternalReader.ReadMetadataProperties(JsonReader reader, Type& objectType, JsonContract& contract, JsonProperty member, JsonContainerContract containerContract, JsonProperty containerMember, Object existingValue, Object& newValue, String& id)
   at Newtonsoft.Json.Serialization.JsonSerializerInternalReader.CreateObject(JsonReader reader, Type objectType, JsonContract contract, JsonProperty member, JsonContainerContract containerContract, JsonProperty containerMember, Object existingValue)
   at Newtonsoft.Json.Serialization.JsonSerializerInternalReader.CreateValueInternal(JsonReader reader, Type objectType, JsonContract contract, JsonProperty member, JsonContainerContract containerContract, JsonProperty containerMember, Object existingValue)
   at Newtonsoft.Json.Serialization.JsonSerializerInternalReader.Deserialize(JsonReader reader, Type objectType, Boolean checkAdditionalContent)
   at Newtonsoft.Json.JsonSerializer.DeserializeInternal(JsonReader reader, Type objectType)
   at Newtonsoft.Json.JsonSerializer.Deserialize(JsonReader reader, Type objectType)
   at Newtonsoft.Json.JsonConvert.DeserializeObject(String value, Type type, JsonSerializerSettings settings)
   at Newtonsoft.Json.JsonConvert.DeserializeObject[T](String value, JsonSerializerSettings settings)
   at ZiggyCreatures.Caching.Fusion.Serialization.NewtonsoftJson.FusionCacheNewtonsoftJsonSerializer.Deserialize[T](Byte[] data)
   at ZiggyCreatures.Caching.Fusion.Serialization.NewtonsoftJson.FusionCacheNewtonsoftJsonSerializer.DeserializeAsync[T](Byte[] data)
   at ZiggyCreatures.Caching.Fusion.Internals.Distributed.DistributedCacheAccessor.TryGetEntryAsync[TValue](String operationId, String key, FusionCacheEntryOptions options, Boolean hasFallbackValue, CancellationToken token)
ZiggyCreatures.Caching.Fusion.FusionCache: Warning: FUSION (O=0005a0db3f0742339f84e64d84f56196 K=f35741d8-a1aa-425e-89d9-eec64e028789:NotificationRepository.All): unable to activate FAIL-SAFE (no entries in memory or distributed)
ZiggyCreatures.Caching.Fusion.FusionCache: Warning: FUSION (O=4a99e5456567499a859d49af206e8082 K=f35741d8-a1aa-425e-89d9-eec64e028789:SettingsDBRepository.All): an error occurred while deserializing an entry

This is the entry:

{
    "$id": "1",
    "$type": "ZiggyCreatures.Caching.Fusion.Internals.Distributed.FusionCacheDistributedEntry`1[[System.Object, System.Private.CoreLib]], ZiggyCreatures.FusionCache",
    "Value": {
        "$type": "System.Collections.Generic.List`1[[infra.Models.Notification, infra.Models]], System.Private.CoreLib",
        "$values": [
            {
                "$id": "2",
                "$type": "infra.Models.Notification, infra.Models",
                "Id": 2,
                "Title": "Test",
                "Url": "https://www.youtube.com/watch?v=UBUArSZWZeg",
                "EventAlertConfigId": null,
                "ExternalClickUrl": null,
                "CreatedBy": null
            }
        ]
    }
}

This is the configuration:

services.AddFusionCacheNewtonsoftJsonSerializer(new JsonSerializerSettings
            {
                ContractResolver = new JsonIgnoreAttributeIgnorerContractResolver(),
                ReferenceLoopHandling = ReferenceLoopHandling.Serialize,
                PreserveReferencesHandling = PreserveReferencesHandling.Objects,
                TypeNameHandling = TypeNameHandling.All
            });

We use In Memory and Redis with .net6

[FEATURE] ๐Ÿ“ข Add Backplane auto-recovery

Is your feature request related to a problem? Please describe.
Since the backplane is implemented on top of a distributed component (in general some sort of message bus, like the Redis Pub/Sub feature) sometimes things can go bad: the message bus can restart or become temporarily unavailable, transient network errors may occur or anything else. In those situations each local nodes' memory caches will become out of sync, since they missed some notifications.

FusionCache should help in handling these situations, ideally automatically.

Describe the solution you'd like
The idea is to add an auto-recovery feature, that will detect notifications that failed to be sent, put them in a local temporary queue and later on, as soon as the backplane will become available again, will try to send them to all the other nodes to re-sync them correctly.

It should also handle specific scenarios like:

  • if more than one notification is about to be queued for the same cache key, only the last one will be kept since the result of sending 2 notifications for the same cache key back-to-back would be the same
  • if a notification is received for a cache key for which there is a queued notification, only the most recent one is kept: if the incoming one is newer, the local one is discarded and the incoming one is processed, otherwise the incoming one is ignored and the local one is sent to the other nodes. This avoids, for example, evicting an entry from a local cache if it has been updated after a change in a remote node, which would be useless
  • it should be possible to set a limit in how many notifications to keep in the queue to avoid consuming too much memory or to bombard the backplane as soon as it will become available again. If a notification is about to be queued but the limit has already been reached, find an heuristic to handle which notification to remove/ignore (probably: the one that would expire sooner, to lower sync problems to a minimum)

[FEATURE] ๐Ÿ“ข Backplane

Scenario

In a multi-node scenario the typical multi-level cache configuration uses a different number of local memory caches and one distributed cache, used to share entries between the different nodes.

When an entry is set on a local memory cache it is also set in the distributed cache, so that other nodes will get the entry from there when they see it's not in their local memory cache.

Problem

A problem may arise when an entry is already in one or more nodes' memory cache and the entry is overwritten on another node: in this situation the memory cache for which the Set method has been called will be updated and the same can be said for the distributed cache, but the other nodes with the old entries would still use those old entries until they expire.

There are 2 ways to alleviate this situation:

  1. use a very low cache duration, but that in turn may increase the load of the data source (eg: a database)

  2. use a lower duration for the memory cache and a higher one for the distributed cache so that the shared (updated) entries are frequently read by the nodes, but that is not (currently) possible in FusionCache and may also lead to a potentially higher load on the distributed cache (instead of on the datasource), on top of still using stale data even if for shorter amount of time

Both of these solutions may be good in some use cases, and thanks to FusionCache combo of fail-safe and advanced timeouts with background factory completion the result for your end users would be good, but it's not a real solution to the problem.

Solution

The idea is to introduce the concept of a backplane which would allow a communication between all the nodes involved about update/removal of entries, so that they can stay up to date about the state of the system.

Design proposal

A new IFusionCacheBackplane interface to model a generic backplane, which could then be implemented in various ways on top of different systems.

It should contain a couple of core methods to notify the change or removal of an entry with 2 different semantics for them because:

  • an explicit remove on a node (eg: a call to Remove(key, ...)) should actually remove the entries on the other nodes to avoid finding a value that should not be there anymore

  • an update (eg: a call to Set(key, ...)) should not remove the entries on the other nodes but just mark those entries - if there - as "logically expired" (eg: change their FusionCacheEntryMetadata.LogicalExpiration) so that at the next access the factory would be executed to get the new value, while still keeping the ability to use the stale value in case of problems or timeouts during the factory execution, which is an added bonus of using FusionCache

It should be noted that directly sending the updated values with the notifications themselves is not considered for various reasons:

  • it would send the new value to every node, even the ones that do not have that entry in their memory cache
  • doing so would consume a lot of bandwidth unnecessarily
  • the same can be said for memory consumption
  • even the nodes which have that entry in their memory cache may not actually require the new value, since it may not be needed anymore before expiring completely

Additionally a small circuit-breaker like the one already present in FusionCache when talking to the distributed cache would be a nice addition, since the same problems of intermittent conection can potentially happen with the backplane.

Ideally I would also explore a form of batching to allow sending an invalidation notification for multiple keys at once, to save some bandwidth (but that may introduce a higher complexity in the codebase which I would like to keep as readable as possible).

First implementation

The first implementation would be on Redis, because:

  • it is already a ubiquitous and rock solid key component in a lot of infrastructures out there
  • when a distributed cache is needed, Redis is typically the one being used
  • it natively contains a pub/sub mechanism which would be the backbone of the implementation

One thing to know about the pub/sub mechanism in Redis is that any message sent will be received by all the nodes connected, including the sender itself. To avoid the eviction of the entry in the same node that originated the notification a form of sender identifier (like a UUID/ULID or similar) should be included in the message payload.

Also the design should be evolvable, to avoid a situation in the future where a new protocol design would break the system when introduced into a live system where nodes are communicating with the v1 and v2 is being introduced.

Of course other implementations may be done with different tecnologies.

๐Ÿ™‹โ€โ™‚๏ธ Memory + Backplane, but no distributed cache?

The Idea

The next version of FusionCache will have an important new component: a backplane.

In the design phase and while discussing it with the community (@jasenf and @sanllanta in particular) a question arose: would it be possible to use just a memory cache + a backplane, without having a distributed cache?

We'll use this issue to discuss the problems with this approach, tentative ideas around it and potential solutions.

So the question is: is this possible?

Short Answer

No.
Well, maybe. With some limitations, but maybe. See this thread.

Longer Answer

This idea in fact seems like a nice one!

In a multi-node scenario we would like to use only the memory cache on each node + the backplane for cache synchronization, without having to use a shared distributed cache.

Technically you can in fact setup a FusionCache instance without a distributed cache but with a backplane.

But don't do it โ›”.

But Why?

You see, the problem with this approach is that it will continually evict cache entries, all the time, on all nodes basically overloading your datasource (eg: the database).

This is because every time a cache is set or removed, it will automatically send eviction notifications on the backplane, which in turn will evict local caches on the other nodes, which in turn - the next time someone asks for that same cache entry on those other nodes - will set the cache, sending notifications and so on, going on like that forever.

Example

To better illustrate this scenario imagine a multi-node setup with 3 nodes (N1, N2, N3), each with a memory cache, initially empty:

  1. somebody on N1 calls GetOrSet for "product/123"
  2. nothing is there, so it calls the factory to grab the product from the database
  3. it then saves the product in the memory cache (on N1) for 5 min, and notify everybody about the change
  4. N2 and N3 receive the notification, and evict their local cache for "product/123"
  5. then somebody on N2 calls GetOrSet for "product/123"
  6. nothing is there, so it calls the factory to grab the product from the database
  7. it then saves the product in the memory cache (on N2) for 5 min, and notify everybody about the change
  8. N1 and N3 receive the notification, and evict their local cache for "product/123"
  9. and so on...

As you can see this basically means that every time somebody directly SET a cache entry (eg: when calling the Set method) or call a GetOrSet (logically a GET + a SET) the entry will be evicted, rendering the entire thing useless.

A different approach

One idea we may think about is to send notifications only after a Remove call or a Set call, and not when calling GetOrSet: the problem now is that, apart from being not logical (a GetOrSet is a GET + SET and the SET part is logically the same as the one in a Set method call), it would also end up NOT keeping all the caches synchronized.

Example

Why? Let us follow this scenario:

  1. somebody on N1 calls GetOrSet for "product/123"
  2. nothing is there, so it calls the factory to grab the product from the database, as it is right now
  3. it then saves the product in the memory cache (on N1) for 5 min, without notifying everybody about the change (since it is not a Set method call)
  4. the product is updated, outside of the app(s) that are using FusionCache (eg: a background service, another app in another programming language, etc...)
  5. then somebody on N2 calls GetOrSet for "product/123"
  6. nothing is there, so it calls the factory to grab the product from the database, as it is right now
  7. it then saves the product in the memory cache (on N2) for 5 min, without notifying everybody about the change (since it is not a Set method call)
  8. now, for around 5 min, N1 and N3 will see different versions of "product/123"

As you can see there's no way to escape this, at least that I'm aware of.

A different approach (reprise)

Finally, in theory we may say that if we establish that ALL changes to the data are done via a piece of code that uses FusionCache, and ALL of those changes to the database are ALWAYS followed by a direct Set call and we ONLY consider the direct Set calls (+ the Remove ones) to send notifications then yeah, maybe it should work.

Well... yes, maybe, in theory that would be the case, but it would also be a very a brittle system, IMHO.

So What?

Anyway, I'm absolutely open to new ideas or point of views.

If you have a brilliant proposition that works and is not brittle please let me know so we may be able to work something out!

Support for setting entry expiry at renewal

Hello, I'm interested in adding FusionCache into one of my libraries as a replacement for LazyCache. However it's missing a feature I use from the base Microsoft cache abstractions, namely the ability to set the cache expiry dynamically at the time of entry renewal via the ICacheEntry interface like so:

 await _cache.GetOrAddAsync("key", async e =>
            {            
                var response = await _client.GetRemoteItemAsync();     

                e.SetAbsoluteExpiration(response.Expiry);

                return response.Item;
            });        

This is super useful if you don't know the expiry of the item upfront. Are there any plans to support something like this?

ZiggyCreatures.FusionCache.Abstractions?

Hi @jodydonetti,
have you ever considered extracting interfaces and common classes to separate library like ZiggyCreatures.FusionCache.Abstractions?
This may allow plugins and higher app layers not to depend on main package.

[BUG] Distributed cache not being checked for key for unknown reason

Describe the bug

I have FusionCache setup with Redis and backplane, all seems to be working ok on the surface, can see the keys being added to Redis and used (on occasion) however on closer inspection (while investigating cold start behaviour I noticed unexpected behaviour.

After cold start some keys are looked up in the distributed cache and collected while others are not.

To Reproduce

var cacheEntry = await _cache.GetOrSetAsync("User:UserViewModel:1_40181", async _ => await BuildUserModelFromContext(), options => options.SetDuration(TimeSpan.FromMinutes(5)));


var search = await _cache.GetOrSetAsync("User:Search:MainSearchResult:1_40181", async _ => {... code... return data;}, options => options.SetDuration(TimeSpan.FromMinutes(5)));

There are 2 uses of the cache using same options called shortly after one another but they behave differently

2022-08-17 15:57:38.8322|DEBUG|ZiggyCreatures.Caching.Fusion.FusionCache|FUSION (O=aca34cf0429f4dd4a9bedfbbf5755552 K=User:UserViewModel:1_40181): calling GetOrSetAsync<T> FEO[LKTO=/ DUR=5m DDUR=/ JIT=2s PR=L FS=Y FSMAX=02:00:00 FSTHR=30s FSTO=100ms FHTO=2s TOFC=Y DSTO=1s D
HTO=2s ABDO=Y BN=Y BBO=Y] 
2022-08-17 15:57:38.8322|DEBUG|ZiggyCreatures.Caching.Fusion.FusionCache|FUSION (O=aca34cf0429f4dd4a9bedfbbf5755552 K=User:UserViewModel:1_40181): memory entry not found 
2022-08-17 15:57:38.8322|DEBUG|ZiggyCreatures.Caching.Fusion.FusionCache|FUSION (O=aca34cf0429f4dd4a9bedfbbf5755552 K=User:UserViewModel:1_40181): memory entry not found 
2022-08-17 15:57:38.8667|DEBUG|ZiggyCreatures.Caching.Fusion.FusionCache|FUSION (O=aca34cf0429f4dd4a9bedfbbf5755552 K=User:UserViewModel:1_40181): distributed entry found FE[FFS=N LEXP=33s] 
2022-08-17 15:57:38.8667|DEBUG|ZiggyCreatures.Caching.Fusion.FusionCache|FUSION (O=aca34cf0429f4dd4a9bedfbbf5755552 K=User:UserViewModel:1_40181): saving entry in memory MEO[CEXP=2022-08-17T16:57:39.7442166+00:00 PR=L S=1] FE[FFS=N LEXP=34s] 
2022-08-17 15:57:38.8667|DEBUG|ZiggyCreatures.Caching.Fusion.FusionCache|FUSION (O=aca34cf0429f4dd4a9bedfbbf5755552 K=User:UserViewModel:1_40181): return FE[FFS=N LEXP=34s] 
2022-08-17 15:57:38.8667|DEBUG|ZiggyCreatures.Caching.Fusion.FusionCache|FUSION (O=0d112accd53c4b0ea88eb15c08e59dc2 K=User:Search:MainSearchResult:1_40181): calling GetOrSetAsync<T> FEO[LKTO=/ DUR=5m DDUR=/ JIT=2s PR=L FS=Y FSMAX=02:00:00 FSTHR=30s FSTO=100ms FHTO=2s TOFC=
Y DSTO=1s DHTO=2s ABDO=Y BN=Y BBO=Y] 
2022-08-17 15:57:38.8667|DEBUG|ZiggyCreatures.Caching.Fusion.FusionCache|FUSION (O=0d112accd53c4b0ea88eb15c08e59dc2 K=User:Search:MainSearchResult:1_40181): memory entry not found 
2022-08-17 15:57:38.8667|DEBUG|ZiggyCreatures.Caching.Fusion.FusionCache|FUSION (O=0d112accd53c4b0ea88eb15c08e59dc2 K=User:Search:MainSearchResult:1_40181): memory entry not found 
2022-08-17 15:57:38.9551|DEBUG|ZiggyCreatures.Caching.Fusion.FusionCache|FUSION (O=0d112accd53c4b0ea88eb15c08e59dc2 K=User:Search:MainSearchResult:1_40181): calling the factory (timeout=2s) 
2022-08-17 15:57:38.9551|DEBUG|Microsoft.EntityFrameworkCore.Query|Compiling query expression: 
'DbSet<usp_GetCatalogueSummaryListResult>().FromSql(EXEC [Products].[usp_GetCatalogueSummaryList] @FormatId, __p_0)
...

Why does User:UserViewModel:1_40181 look in distributed cache (and find it and use it) but User:Search:MainSearchResult:1_40181 does not, I can see it is in the Redis database and has not expired.

Expected behavior
For User:Search:MainSearchResult:1_40181 to be checked in distributed cache, found and used.

Versions
I've encountered this issue on:

  • FusionCache version 0.13
  • .NET version 6
  • OS version Windows 11

Add any other context about the problem here.

IDistributedCache only option

I like all of the functionality currently provided within FusionCache, but I would like the ability to turn off the memorycache option.

When using Azure Functions I would prefer to just go straight to Redis and not have the overhead of memorycache due to their stateless nature. I still want to have the fallback value capability in the event Redis is unavailable, but I just don't need the extra step of dealing with memorycache.

This would also help with longer-lived data which could change very infrequently but is accessed often. If I have a long TTL on a value I could have an instance where a load-balanced environment could be out of sync. Let's say I have a TTL of 90 minutes on data but due to a production problem I need to update the current value in Redis. I have no way of evicting the current memorycache values even though Redis has a more current (and correct) value.

This does touch a bit on your backplane idea, but in that case, I would still like to have a preferred cache type for a value. I think this could go in the FusionCacheEntryOptions as a PreferredCache property. This value could be an enum of Local or Distributed. This way I could determine which cache to prefer for a value.

So, this could be seen as 1 of 2 proposals to the FusionCacheEntryOptions:

public CacheLocation CacheLocation { get; set; } = CacheLocation.All;

public enum CacheLocation
{
	All,
	LocalOnly,
	RemoteOnly
}
	
public PreferredCache PreferredCache { get; set; } = PreferredCache.Local;

public enum PreferredCache
{
	Local,
	Remote
}

Looking at the GetOrSetEntryInteral I am not sure if one will be easier than the other. If I had to pick only one option I would say option 1 would do what I really want at the moment. Since I know I would be losing performance by going to Redis first then memorycache would have a smaller performance benefit since I can still use the fallback option.

What are your thoughts?

WireFormatVersionPrefix interfering with redis user permissions.

Hello, thanks for this library!
I'm getting an error when trying to use the redis layer.

Type Error: NOPERM this user has no permissions to access one of the keys used as arguments.
https://redis.io/topics/acl

I think I've tracked down the problem to this line of code:
https://github.com/jodydonetti/ZiggyCreatures.FusionCache/blob/1cb0c169d56898746f6823405a3fe3f54bfa8389/src/ZiggyCreatures.FusionCache/Internals/Distributed/DistributedCacheAccessor.cs#L16

My connection is limited to only read and write from keys that have a prefix in the form foo:bar:* so, having interactions like v1:foo:bar break. Is there a way around that? I'm thinking of implementing my own IDistributedCache that uses the normal implementation but overrides that key prefix, it looks hacky but should work.

[FEATURE] ๐Ÿงฉ Plugins

Scenario

While playing with the implementation of the backplane (see #11) and talking about adding metrics (see #9) the need emerged to be able to add functionalities around FusionCache via external code.

In some cases a specific interface is needed because it is a core part of the system (like the already existing IFusionCacheSerializer), but in a lot of other cases a more generic one would probably suffice.

The objective is to:

  • let other people extend FusionCache with custom needs for their projects
  • open the door to more contributions by the community for common functionalities not present in the core packages

Proposal

The idea is to create a plugin subsystem where multiple plugins can be implemented using a common interface that would allow the coordination with a FusionCache instance.

As a first draft I'm thinking about something like this:

public interface IFusionCachePlugin
{
    void Start(IFusionCache cache);
    void Stop(IFusionCache cache);
}

In the Start method a plugin implementer will receive a cache instance to then, for example, subscribe to the events they'd like (see #14) or do something else, while in the Stop they can remove the events subscriptions they've created before, to keep a system clean.

A plugin will be added to a cache with a method like IFusioCache.AddPlugin(IFusionCachePlugin plugin), similarly to the one for the distributed cache which would add the plugin instance to an internal list of plugins, and call its Start method. In the same vein, a method IFusioCache.RemovePlugin(IFusionCachePlugin plugin) can be called to remove a plugin (which in turn will call the Stop method on the plugin) for housekeeping purposes.

Dependency Injection

In a DI scenario the method will be called automatically for all the registered services implementing the IFusionCachePlugin type, like what is already happening for the IDistributedCache type here, but with potentially multiple implementations.

The code may be something like this:

[...]
var plugins = serviceProvider.GetServices<IFusionCachePlugin>();
foreach (var plugin in plugins)
{
    cache.AddPlugin(plugin);
}
[...]

Specialized plugin types

In case specific functionalities may be needed by FusionCache, a more specialized plugin type may be created simply inheriting from the base IFusionCachePlugin type and adding the specific apis needed.

If, for example, the metrics plugins need a special metrics-related method to be called for metrics-related things, we would have something like this:

public interface IFusionCacheMetricsPlugin:
    : IFusionCachePlugin
{
    Task<bool> DoMetricsStuffAsync(int param1, string param2, [etc...]);
}

NOTE: I'm still playing with the backplane impl, and it could very well fit into this as a "normal" plugin.

Other stuff to decide

Should there be a way to identify a plugin, apart from its clr type? Something like a string Id { get; set; }? It may be useful in some contexts (eg: logging).

Thoughts?

Any suggestion is more than welcome.

What is the difference between 2nd level option and backplane, when using distributed cache?

hi,

I would like to understand the difference between using the 2nd level option (of using a distributed cache) and Backplane with distributed cache? Somehow both of them looks same to me. How are they different?

Also, for distributed cache, you mention that any backend that supports IDistributedCache can be used. However for backplane, only Redis is supported, not the others which support IDistributedCache. Why is that?

Thanks!

Implementing FusionCache with EF Core 5

Hi! First, thank you for making such a great library! I am trying to implement FusionCache into an API running on EF Core 5, Mysql and Redis. The issue is that when the FactorySoftTimeout is triggered and the factory is supposed to continue in the background and update the cache when finished, we are instead presented by an System.ObjectDisposedException error:

image

It looks like the DbContext gets disposed before the factory running in the background is finished working with it. How can I keep the DbContext open for the factory to finish its task?

This is my EF Core database configuration:

image

This is the FusionCache configuration:

image

And this is how FusionCache is used inside a Controller:

image

In the Service, DbContext object is accessed via a dependency injection.

If you need any additional information or code snippets I will be happy to provide.

I would be very thankful for any help or advice you can offer.

Kind regards,
Tomaลพ

.Clear() mechanism - Support MemoryCacheEntryOptions.ExpirationTokens

First off, the library looks great, especially with the introduction of the backplane!
This may be an extension to the ongoing cache region discussion, but I think deems it's own topic.

I am working on a PoC utilising this library and came across a gap compared to our current implementation on top of LazyCache.

It is the equivalent of a Clear mechanism affecting all or part of the cache.

MemoryCacheEntryOptions of IMemoryCache supports entries with attached ExpiryTokens. Each entry can have multiple tokens assigned to it and when the token is cancelled, all associated cache values are expired.

Currently we track the issued tokens in a ConcurrentDictionary keyed on a string.

This allows for us to do 2 things

  1. Apply a shared token to all entries, providing a .Clear() equivalent
  2. Apply a token to groups of related entries, providing a .Clear("CacheGroup")

The primary reason for this approach was to solve cache invalidation across services.

ServiceA caches

  • ServiceA data - CacheGroup = ServiceA
  • ServiceB data (retrieved via API call) - CacheGroup = ServiceB

ServiceB caches

  • ServiceB data - CacheGroup = ServiceB

Now if I update data in ServiceB, I raise an event (Rest API, or Message Queue) that ServiceA listens to and thus calls .Clear("ServiceB"). (note only one listener responds on ServiceA due to load balancing).

The second part of this implementation is segmentation/relations inside of ServiceA
Where CacheKey2 & CacheKey3 data is built on top of CacheKey1 data. (I understand this may not be the 'right' way but it addresses our situation, where cacheKey1 is computationally intensive as is CacheKey2 & CaheKey3)
cacheKey1 = "baseCacheData" eg "template"
cacheKey2 = "UserID:baseCacheData+UserSpecificEnhancements" eg "123:template:nhanced"
cacheKey3 = "UserID:baseCacheData+UserSpecificEnhancements" eg "234:templateEnhanced"

Now if I change cacheKey1 it should invalidate cacheKey2 and cacheKey3.

Currently I would use a cacheGroup token to relate these cache entries together, and expire cacheKey1 before setting a new value for it.

This approach also supports nested relations like
"{GroupID}:{UserID}:Data"

Each entry has 2 change tokens, 1 for the group and 1 for the user.

With this I can invalidate a variety of data subsets, either on user change to invalidate across all groups, or on group change to invalidate across all users.

I am not sure of the side effects of this in FusionCache, especially with Background and Backplane support.

I would happily keep the the .Clear() functionality and tracking of ExpiryTokens outside of FuctionCache, but would be great if I could easily add the token when creating an entry.

Would be great to discuss this possibility.

Possible FusionCacheServiceCollectionExtensions improvement

Currently the AddFusionCache has the following parameters:

  • Action<FusionCacheOptions>? setupOptionsAction = null,
  • bool useDistributedCacheIfAvailable = true,
  • bool ignoreMemoryDistributedCache = true,
  • Action<IServiceProvider, IFusionCache>? setupCacheAction = null

Problem

Imagine some wants to control caching behavior/setup (memory or memory+distributed or memory+distributed+backplane).
useDistributedCacheIfAvailable allows to skip distributed cache setup during AddFusionCache call, so that it can be setup later using setupCacheAction. Thus, defered setup through setupCacheAction allows to e.g. resolve a IOptions<CacheSettings> that may store whether to use distributed caching or not. Unfortunately, backplane "auto" setup cannot be disabled through a parameter.

Solution 1

Write custom AddFusionCache extension method in application codebase.

Solution 2

Change Action<IServiceProvider, IFusionCache>? setupCacheAction to Action<FusionCacheFactoryContext>? setupCacheAction

public class FusionCacheFactoryContext
{
    public IServiceProvider ServiceProvider { get; }

    public IFusionCache Cache { get; }
    
    public FusionCacheContext(IServiceProvider serviceProvider, IFusionCache cache)
    {
        ServiceProvider = serviceProvider;
        Cache = cache;
    }
}

Then, having a FusionCacheFactoryContext that wraps both IServiceProvider and IFusionCache allows writing extensions to setup e.g.:

public static FusionCacheFactoryContext SetupBackplaneFromServices(this FusionCacheFactoryContext context)
{
    var backplane = context.ServiceProvider.GetService<IFusionCacheBackplane>();

    if (backplane is not null)
    {
         context.Cache.SetupBackplane(backplane);
    }
}

Finally there can be two AddFusionCache overloads:

  1. (this IServiceCollection services, Action<FusionCacheOptions>? setupOptionsAction = null) that setups FusionCache using default/current "auto" behavior
  2. (this IServiceCollection services, Action<FusionCacheOptions>? setupOptionsAction = null, Action<FusionCacheFactoryContext> setupCacheAction, bool setupDefaults = true) that allows to freely customize FusionCache setup

Unable to activate FAIL-SAFE (no entries in memory or distributed) on multiple requests

Hi,
We are testing your cache implementation, well done for a great work. During Load testing on One API call we started to get the Unable to activate FAIL-SAFE (no entries in memory or distributed) warning.
It managed to set data on high volume of requests, but after many many misses
Here is my code:

public async Task<Data> GetDataAsync(string identifier, IdentifyBy identifyBy)
        {
            var cacheKey = $"Info:{identifier}:{identifyBy}";
            var data = await _cache.GetAsync<Data>(cacheKey);

            if(data == null)
            {
               data = new Data { Id = 1 };
               await _cache.SetAsync(cacheKey , data, TimeSpan.FromMinutes(10));
            } else
            {
                _logger.LogInformation($"Successfully got Data Information  from Cache. identifier: {identifier} by { identifyBy }");
            }
        }

[FEATURE] ๐Ÿฆ… Eager Refresh

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).

  • ๐ŸŸข PRO: easy to reason about the exact amount of time
  • ๐Ÿ”ด CON: it needs to be specified directly for each entry options where you would like to use it
  • ๐Ÿ”ด CON: cannot be set in the DefaultEntryOptions, to automatically adapt to each call's specific Duration
  • ๐Ÿ”ด CON: since it's not a relative but an absolute value, we need to remember to always updated it in case we'll update a Duration in the future (error prone)

Percentage

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

  • ๐ŸŸข PRO: since it's a relative value, it can automatically adapt itself to each specific Duration used. For example by saying 0.9 you will know that it will be "90% of the Duration", without having to do mental calculations (most probably the mental approach is not tied to a specific TimeSpan value, but more something like "I would like it to happen at 90% of the Duration")
  • ๐ŸŸข PRO: if in the future you will need to change a Duration in a specific call, you would NOT need to remember to also change the eager duration (as a TimeSpan) to keep the 2 aligned (less error prone)
  • ๐ŸŸข PRO: it can be set once in the DefaultEntryOptions and automatically applied to every call, dynamically adapting to each call's Duration
  • ๐Ÿ”ด CON: (kinda) it may be less quick to know at a glance the exact eager duration. In reality though would this actually be needed? Meaning, just knowing "at 90% of the Duration the data will be refreshed" would most probably be more than enough. Also, if we think about debugging/logging, it's really easy to log the eager duration as both a percentage AND as the resulting (calculated) TimeSpan, for ease of use

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.

Using `MaybeValue<T>` also as the return value of TryGet[Async] method

I'm thinking about using the new type MaybeValue<T> - introduced for the new failSafeDefaultValue in the GetOrSet method - also as the return value of the TryGetResult<T> method.

The basic rationale behind this is unification of the "maybe a value, maybe not" concept.

The main difference from now would be that TryGetResult<T> implicitly converts to a bool (the idea was to use that in an if statement) whereas MaybeValue<T> implicitly converts to/from a T value, much like a Nullable<T>. But, just like a Nullable<T>, it does have an HasValue property for easy checks, so it should not be that problematic.

In practice, before we should have done one of these:

// ASSIGNMENT + IMPLICIT CONVERSION TO bool, KINDA WEIRD
TryGetResult<int> foo;
if (foo = cache.TryGet<int>("foo"))
{
  var value = foo.Value;
}

// ASSIGNMENT DONE BEFORE
var foo = cache.TryGet<int>("foo");
if (foo)
{
  var value = foo.Value;
}

whereas now we can do one of these:

var foo = cache.TryGet<int>("foo");
if (foo.HasValue) // EXPLICIT .HasValue CHECK
{
  // EXPLICIT .Value USAGE
  var value = foo.Value;
  // OR IMPLICIT CONVERSION, LIKE WITH A Nullable<T>
  var value = foo;
}

// OR .GetValueOrDefault(123) USAGE, LIKE A Nullable<T>
var foo = cache.TryGet<int>("foo");
var value = foo.GetValueOrDefault(123);

Even though FusionCache is still in the 0.X version phase (so can have breaking changes), just in case of existing usage of the TryGet[Async] method in someone else's code I can easily add the old Success prop marked as Obsolete with an explanation on how to use the new thing. Maybe some pieces of code with a direct check in an if statement would stop compiling, but that would be resolved by simply adding .HasValue.

Opinions?

[FEATURE] Add MessagePack support

Is your feature request related to a problem? Please describe.
FusionCache provides a way to add custom serialization format support, by implementing the IFusionCacheSerializer interface.

It already provides 2 implementations, both for the JSON format:

It would be great to add support for the MessagePack serialization format, too.

Describe the solution you'd like
A new package that add supports for the MessagePack format, probably based on the most used implementation on .NET, the one by Neuecc.

Describe alternatives you've considered
Everyone that needs it should implement their own, which is meh ๐Ÿ˜

Batch support for GetOrSet

Hi,

I am curious whether you have explicitly decided not to support batch methods (if so what are the reasons - implementation burden / other patterns to use / no need etc.) or is it planned for the future?

BTW. Neat project ;)

Distributed cache separate TTL option.

Hi! I'd like to have a different configuration for the duration of the in memory values than the ttl in redis. Is there a way around that?

The idea is that the memory cache refreshes more often than the redis cache, so if other node updates the value in the distributed cache, the memory one will only be stalled by the duration of the in memory configuration.

Thanks! :)

[FEATURE] Add `ReThrowSerializationExceptions` option, just like for the distributed cache

Is your feature request related to a problem? Please describe.
Sometimes it may be useful to be able to directly catch serialization/deserialization exceptions, instead of just logging them: this may be useful to better dignose serialization issues.

Describe the solution you'd like
Add the ability to re-throw exceptions happened during serialization/deserialization instead of just logging them, via a new ReThrowSerializationExceptions option.

The very same feature is already available for the distributed cache exceptions, via the ReThrowDistributedCacheExceptions option.

Different duration in distributed cache

We have two sources of data, primary and secondary.

  • If primary is fetched, we want to
    • Renew local cache after 6 hours (Duration=6h)
    • Reuse local value up to 24 hours if problems. (FailSafeMaxDuration=24h)
    • Expire distributed cache after 24 hours, instead of 6 hours. โ“
  • If primary fails and secondary is fetched, we want to
    • Renew local cache immediately (Duration=0s + some throttling)
    • Reuse local value up to 24 hours if problems. (FailSafeMaxDuration=24h)
    • Expire distributed cache after 24 hours, instead of immediately. โ“

โ“ How can we configure distributed cache to a different value than the local cache?

Also, setting Duration = TimeSpan.Zero results in an exception for distributed cache, which would be solved by setting distributed cache duration differently:

System.ArgumentOutOfRangeException: The absolute expiration value must be in the future. (Parameter 'AbsoluteExpiration')
Actual value was 04.07.2022 12:17:49 +00:00.
   at Microsoft.Extensions.Caching.StackExchangeRedis.RedisCache.GetAbsoluteExpiration(DateTimeOffset creationTime, DistributedCacheEntryOptions options)
   at Microsoft.Extensions.Caching.StackExchangeRedis.RedisCache.SetAsync(String key, Byte[] value, DistributedCacheEntryOptions options, CancellationToken token)
   at ZiggyCreatures.Caching.Fusion.Internals.Distributed.DistributedCacheAccessor.<>c__DisplayClass15_0`1.<<SetEntryAsync>b__0>d.MoveNext()
--- End of stack trace from previous location ---
   at ZiggyCreatures.Caching.Fusion.Internals.FusionCacheExecutionUtils.RunAsyncActionAdvancedAsync(Func`2 asyncAction, TimeSpan timeout, Boolean cancelIfTimeout, Boolean awaitCompletion, Action`1 exceptionProcessor, Boolean reThrow, CancellationToken token)

โš  Breaking change proposal: removing `CacheKeyPrefix`, opinions needed

Hi there FusionCache users, I need some help from you all.

image

Recently, while designing some new features and working on the backplane (#11) I realized that, in terms of design, the FusionCacheOptions.CacheKeyPrefix feature is probably not that good to have inside on FusionCache itself.

Let me explain.

Brief History

The idea to introduce the CacheKeyPrefix option came up because sometimes we want to use the same distributed cache instance (eg. Redis) for multiple different sets of data at the same time.

For example in one of my projects I'm setting a prefix based on the environment type, like "d:" for development, "s:" for staging and "p:" for production, so I can use the same Redis instance and save some bucks.

With this little "trick" I can have the same "logical" piece of data in the cache like the product with id 123 (which would normally have a cache key of "product/123") but for 2 different environments, without one inadvertently overwrite the other (because with the CacheKeyPrefix we will have 2 different cache keys: "d:product/123" and "p:product/123").

Because of this I initially thought that having such a small feature would be a nice addition, so I went on and implemented it.

From "the outside" you would be able to specify your cache keys, and work with every method specifying your "logical" cache keys (eg: "product/123") knowing that on "the inside" the little magic will happen (eg: turn it to "p:product/123").

Why remove it?

The thing is, cache key generation is NOT FusionCache's concern, but your own: it is outside the scope and responsibility of FusionCache, and it should be only yours.

The moment FusionCache modify a cache key it receives, it starts messing around with a critical piece of data (the cache key itself) and this may have unintended consequences.

For example right now each single method in FusionCache receives a cache key and, before doing anything else, modifies it by applying the specified CacheKeyPrefix (if any) and then moving on, forgetting about the original cache key altogether and working only on the modified one.

This means that everything that happens inside of FusionCache (and, in turn, in the memory and distributed cache) will be tied to the modified cache keys, including logging, events and whatnot.

A practical example

Right now I'm working on the Backplane (#11): suppose we have 3 nodes (N1, N2 and N3), suppose we use the "logical" cache key of "product/123" and a CacheKeyPrefix of "foo:".

Then this would happen:

  1. I call Set("product/123", product) on N1
  2. internally the cache key gets turned to "foo:product/123"
  3. the data is saved in the memory cache on N1 with the key "foo:product/123"
  4. the data is saved in the distributed cache with the key "foo:product/123"
  5. an event is raised for a set operation for the key "foo:product/123"
  6. the backplane sends an eviction notification for the key "foo:product/123"
  7. the nodes N2 and N3 receive the notification for the key "foo:product/123", and try to evict the entries from their local cache for that key
  8. to do so, a public FusionCache method is called (like Set, Remove, etc) and that, in turn, would re-apply the prefix, obtaining a cache key of "foo:foo:product/123" (see the double "foo:" there?)

This is a nice case of Leaky Abstraction we got, amirite?

Now, of course I may "simply" un-apply the prefix to the cache key before sending the notification (point 6) by doing some substring magic or something, or I may "keep" the original cache key + the modified one, but both of those would mean extra work at runtime (cpu/memory), it would smell really bad and, as said, it's not really a good thing design-wise.

So, what are the possible alternatives?

Alternatives

The fact is that every distributed cache out there (and every impl of IDistributedCache) is already natively able to specify different "sets" to be able to split the data in the same service instance, but in a more idiomatic and native way:

  • in the Redis world there's DefaultDatabase
  • in the SqlServer world there are SchemaName and TableName
  • in the CosmosDB world there are DatabaseName and ContainerName
  • in the MongoDB world (various impls) there are DatabaseName and CollectionName
  • etc...

Each of them is surely the best way to handle such a scenario in each specific service.

How it will be removed

I'd like to remove support for the feature altogether, since:

  • best I know basically nobody is using it (but please let me know about this!)
  • FusionCache is still in v0.X and per semantic versioning it's ok to make breaking changes freely, even though I'd like to point out that as of today I've only made 1 of them, and in the very early days after the very first release
  • there are alternatives to keep it working in the same way (see below)
  • it would mean extra work at runtime (cpu/memory)
  • it would smell really bad and, as said, it's not really a good thing design-wise

Now, to be more precise, I'd like to do it NOT by removing the CacheKeyPrefix prop itself, otherwise dependent code would not compile anymore and people wouldn't know why, but by decorating the prop as [Obsolete] with the error param set to true: this would make the dependent code not compile, BUT with the ability to specify a descriptive message to explain things and suggest how to handle the removal, maybe even with a link to an online page with a thorough explanation and some examples.

How to keep our current cache keys

If you are actually using this option (let me know!) and you really like your cache keys with the prefix they currently have, here's an easy way to keep them as they are.

Before

I can imagine that, probably in a Startup.cs file you will have something like this:

services.AddFusionCache(options =>
{
  [...]
  if (Env.IsProduction())
    options.CacheKeyPrefix = "prod:";
});

Later on you are using FusionCache like this:

cache.GetOrSet<Product>($"product/{id}", ...);

After

What you would have to do is declare some public static prop like this:

public static class MyCache {
  public static string? Prefix { get; set; }
}

change the startup code to something like this:

if (Env.IsProduction())
  MyCache.Prefix = "prod:";
services.AddFusionCache(options =>
{
  [...]
});

and finally when you later use FusionCache like this:

cache.GetOrSet<Product>($"{MyCache.Prefix}product/{id}", ...);

If instead you just like to keep data apart without using the prefix, simply set a Database, DatabaseName, CollectionName or ContainerName based on the technology you are using (Redis, MongoDB, SqlServer, etc...) like mentioned in the Alternatives chapter above.

Ok but is anybody actually using the CacheKeyPrefix option?

Right now I'm the only one I know of that is using this obscure little feature.

I've talked with about a dozen people who are using FusionCache, and none of them are using it.

But since the downloads for the main package alone just crossed 17K (thanks all ๐ŸŽ‰) I'm here asking you all how you feel about it.

What can you do?

If you agree with this change, simply vote with a ๐Ÿ‘.

If you disagree, please comment with the reasoning behind it so I can understand more about your use case.

Thanks to anyone who will participate!

GetOrSet, but do not cache null values from factory.

I've read through most documentation I can find and a good amount of discussions on here in the issues. Perhaps I'm not thinking through my scenario clearly, but I feel like GetOrSet should have a option of some kind to ignore null values and just return null.

In a previous issue, someone had mentioned something along the lines of GetOrSetOrDefault ... which may be what I'm thinking.
The issue I see is this:

return _cache.GetOrSet(
    "key",
    ctx => {
        _repository.Get(...);
    },
    opt => { ... }
);

If _repository.Get(..) returns null, I don't want to cache this. My only option is to throw an exception from within the factory func. Is this the intended design?

I could, of course, break this down into separate TryGetAsync, followed by a SetAsync.

First cached entry using GetOrSet should trigger Miss event

The following code will trigger the SET event and I thing that is good and correct. But it does not trigger the Miss event. I think it should trigger the Miss event if it did not previously exist in the cache at anytime. Think about it this way. If you have a cache item in a cache it had to get there because once upon a time it as not in the cache, thus the rational that it should have historicaly been a cache miss. I discovered this while building my OpenTelemetry plugin examples. I am using the GetOrSet now that I have the Adaptive Caching feature in my hands. That is when I realized couldn't find a Miss events in my prometheus (time series) database.

await cache.GetOrSetAsync<int>(
    "foo",
    async (_) =>
    {
        await Task.Delay(1);
        return 123;
    });

[Question] How factory call is synchronized between nodes?

In the docs there's a FactoryOptimization.md which says

Special care is put into calling just one factory per key, concurrently

Can you please provide more details on how this works? I'm curious what happens within FusionCache when:

  • we use SQL as storage
  • there are more than 1 application node
  • 2 or more nodes call GetOrSet with the same key concurrently
  • factory is non-trivial, eg. takes time and can fail

Does FusionCache implement some sort of distributed lock on a given storage to synchronize nodes? For instance, some atomic operation is needed in SQL to create a "locking" row for a key, call factory and insert value, or remove lock if factory failed.

[BUG] Bad deserialization with System.Text.Json

Thank you so much for the library. I'm trying to use it on my project and faced an issue when using default .net 3.1 json serializer and IDistributedCache backed by Redis.

Steps to reproduce:

  1. Add services.AddFusionCacheSystemTextJsonSerializer
  2. Create a model
class LastModified
  {
    public DateTime Date { get; set; }
  }
  1. Try get the data
await _cache.GetOrSetAsync<LastModified>(_cacheKey, ReadFromDb, token: token);

You'll notice a warning written to the console:
image

Here is what is read from the Redis
image
Notice the Metadata object. It doesn't have a parameterless constructor and that's what exception is about.

Unit Testing

I was hoping you could add documentation on how to unit test when using this.

I am unable to moq the task being passed to the cache or to moq the cache method that would return the data. Any advice would be greatly appreciated.

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    ๐Ÿ–– Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. ๐Ÿ“Š๐Ÿ“ˆ๐ŸŽ‰

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google โค๏ธ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.