Skip to content

Releases: ZiggyCreatures/FusionCache

v2.0.0-preview-3

09 Dec 23:43
Compare
Choose a tag to compare
v2.0.0-preview-3 Pre-release
Pre-release

Important

This is a PREVIEW of a very big and important milestone for FusionCache.
Although all is already in a very good shape, this is still a PREVIEW version.
All help is more than welcome since the main feature, Tagging, is an uber complex beast.

Warning

Because of the MAJOR version change, for now I decided to bump the wire format identifier: read more here and here.

Ⓜ️ Native support for Microsoft's new HybridCache

As already announced when I shared my thoughts on the new Microsoft HybridCache some time ago, I wanted to allow FusionCache to be also usable as a 3rd party HybridCache implementation.

To be clear, this does NOT mean that FusionCache will now be based on HybridCache from Microsoft, but that it will ALSO be available AS an implementation of it, via an adapter class included in a new Nuget package.

So, how can we use it?

Easy peasy, we just add the new package:

dotnet add package ZiggyCreatures.FusionCache.MicrosoftHybridCache --version "2.0.0-preview-3"

and, when setting up FusionCache in our Startup.cs file, we simply add .AsHybridCache():

services.AddFusionCache()
  .WithDefaultEntryOptions(options =>
  {
    options.Duration = TimeSpan.FromSeconds(10);
    options.IsFailSafeEnabled = true;
  })
  .AsHybridCache(); // MAGIC

Now, every time we'll ask for HybridCache via DI (taken as-is from the official docs):

public class SomeService(HybridCache cache)
{
    private HybridCache _cache = cache;

    public async Task<string> GetSomeInfoAsync(string name, int id, CancellationToken token = default)
    {
        return await _cache.GetOrCreateAsync(
            $"{name}-{id}", // Unique key to the cache entry
            async cancel => await GetDataFromTheSourceAsync(name, id, cancel),
            cancellationToken: token
        );
    }

    public async Task<string> GetDataFromTheSourceAsync(string name, int id, CancellationToken token)
    {
        string someInfo = $"someinfo-{name}-{id}";
        return someInfo;
    }
}

we'll be using in reality FusionCache underneath acting as HybridCache, all transparently.

And this also means we'll have the power of FusionCache itself, including the resiliency of fail-safe, the speed of soft/hard timeouts and eager-refresh, the automatic synchronization of the backplane, the self-healing power of auto-recovery, the full observability thanks to native OpenTelemetry support and more.

Oh, and we'll still be able to get IFusionCache too all at the same time, so another SomeService2 in the same app, similarly as the above example, can do this:

public class SomeService2(IFusionCache cache)
{
    private IFusionCache _cache = cache;
    
    // ...

and the same FusionCache instance will be used for both, directly as well as via the HybridCache adapter.

Oh (x2), and we'll be even able to read and write from BOTH at the SAME time, fully protected from Cache Stampede!
Yup, this means that when doing hybridCache.GetOrCreateAsync("foo", ...) at the same time as fusionCache.GetOrSetAsync("foo", ...), they both will do only ONE database call, at all, among the 2 of them.

Oh (x3 😅), and since FusionCache supports both the sync and async programming model, this also means that Cache Stampede protection (and every other feature, of course) will work perfectly well even when calling at the same time:

  • hybridCache.GetOrCreateAsync("foo", ...) // ASYNC
  • fusionCache.GetOrSet("foo", ...) // SYNC

Damn if it feels good 😬

Of course, since the API surface area is more limited (eg: HybridCacheEntryOptions VS FusionCacheEntryOptions) we can enable and configure all of this goodness only at startup and not on a per-call basis: but still, it is a lot of power to have available for when you need/want to depend on the Microsoft abstraction.

Actually, to be more precise: the features available in both HybridCacheEntryOptions and FusionCacheEntryOptions (although with different names) have been automatically mapped and will work flawlessly: an example is using HybridCacheEntryFlags.DisableLocalCacheRead in the HybridCacheEntryOptions which becomes SkipMemoryCacheRead in FusionCacheEntryOptions, all automatically.

❤️ Microsoft (and Marc) and OSS
To me, this is a wonderful example of what it may look like when Microsoft and the OSS community have a constructive dialog. First and foremost many thanks to @mgravell himself for the openness, the back and forth and the time spent reading my mega-comments.

🏷️ Better Tagging

The tagging feature is now even better, way more optimized, and working in all scenarios.
Some bugs have been fixed too, the main being that this (specifying tags in the factory):

var foo = await cache.GetOrSetAsync<int>("foo", async (ctx, _) =>
{
	ctx.Tags = ["x", "y", "z"];
	return 123;
});

worked as intended, while this (specifying tags directly in the call):

var foo = await cache.GetOrSetAsync<int>("foo", async _ => 123, tags: ["x", "y", "z"]);

was not, but now this is fixed.

Tagging now works automatically better even without a backplane (even though it is adviced to use one): of course without it a little out-of-sync delay is to be expected.

See here for the original issue.

🧼 Better Clear

The new Clear feature has been improved too, and it now allows us to choose between a full Clear (eg: "remove all") and a soft Clear (eg: "expire all"), with full support for both via a simple bool param with sensible defaults.

The performance has been drastically improved, too, making it incredibly faster.

See here for the original issue.

🐞 Fix for soft fails in a background factory

It has been noticed by community user @Coelho04 (thanks!) that fail-safe did not activate correctly when the factory failed with ctx.Fail("Oops") and the factory was running in the background (because of a soft timeout or eager refresh).

This is now fixed.

See here for the original issue.

📆 More predictable Expire

Community user @waynebrantley (thanks!) made me think again about the difference between Remove() and Expire() and how to better handle the different expectations between the two.

Now the behaviour is clearer, and hopefully there will be less surprises down the road.

See here for the original issue.

💥 ReThrowSerializationExceptions does not affect serialization, only deserialization

Community user @angularsen (thanks!) had an issue with serialization, and long story short the ReThrowSerializationExceptions option can now be used to ignore only deserialization exceptions, not serialization ones (since basically an error while serializing means that something must be fixed).

See here for the original issue.

♊ Auto-Clone optimized for immutable objects

By taking an inspiration from the upcoming HybridCache from Microsoft (see above), FusionCache now handles immutable objects better when using Auto-Clone: if an object is found to be immutable, it will now skip the cloning part, to get even better perfs.

📞 New events for Tagging/Clear

New events have been added for the Tagging and Clear features.

🔭 Better Observability for Tagging/Clear

It is now possible to include the tags of the new Tagging feature when using observability.

There are 3 new FusionCacheOptions:

  • IncludeTagsInLogs
  • IncludeTagsInTraces
  • IncludeTagsInMetrics

They are pretty self-explanatory, but keep in mind that when in OTEL world we should avoid high-cardinality attributes (eg: attributes with a lot of different values) so, be careful.

Finally, there are also new traces and meters for Tagging/Clear operations.

📜 Shorter auto-generated InstanceId for each cache is now shorter (saves space in logs and traces)

A minor change, but the auto-generated InstanceId for each FusionCache instance are now shorter, saving some space on logs/network.

💀 All [Obsolete] members has been marked as errors

All the members previously marked as [Obsolete("Message")] are now marked as [Obsolete("Message", true)], meaning if you ar...

Read more

v2.0.0-preview-2

14 Nov 21:32
Compare
Choose a tag to compare
v2.0.0-preview-2 Pre-release
Pre-release

Important

This is a PREVIEW of a very big and important milestone for FusionCache.
All help is more than welcome since the main feature, Tagging, is an uber complex beast.

Warning

Although all is already in a very good shape, this is still a PREVIEW version.

Warning

Because of the MAJOR version change, for now I decided to bump the wire format identifier: read more here and here.

🔀 New entry options to precisely skip read/write on memory/distributed levels

New entry options have been added to precisely skip reading and/or writing for both the memory (L1) and the distributed (L2) levels.

Now we have 4 options with more granular control:

  • SkipMemoryCacheRead
  • SkipMemoryCacheWrite
  • SkipDistributedCacheRead
  • SkipDistributedCacheWrite

Previously we had 2 options:

  • SkipMemoryCache
  • SkipDistributedCache

Now these 2 old ones now act in this way:

  • the setter changes both the corresponding read/write ones
  • the getter returns true if both the read and the write options are set to true (eg: return SkipMemoryCacheRead && SkipMemoryCacheWrite)

Even if they work, to avoid future confusion the 2 old ones have been marked as [Obsolete].

Of course the handy ext methods like SetSkipMemoryCache() still work.

📜 New IncludeTagsInLogs option

FusionCache now allows you to include tags when logging a cache entry, via the new IncludeTagsInLogs option.

It is disabled by default since tags can be considered as part of the "content" of the cached entry itself, an so they may contain sensitive informations.

We can just enable it in the FusionCacheOptions and be good to go.


⬇️ Other release notes from preview-1 ⬇️


🏷️ Tagging (docs)

Yep, it's true: FusionCache now has full support for tagging!
This means we can now associate one or more tags to any cache entry and, later on, simply call RemoveByTag("my-tag") to evict all the entries that have the "my-tag" associated to them.

And yes, it works with all the other features of FusionCache like L1+L2, backplane, fail-safe, soft timeouts, eager refresh, adaptive caching and everything else.

Honestly, the end result is a thing of beauty.

Here's an example:

cache.Set("risotto_milanese", 123, tags: ["food", "yellow"]);
cache.Set("kimchi", 123, tags: ["food", "red"]);
cache.Set("trippa", 123, tags: ["food", "red"]);
cache.Set("sunflowers", 123, tags: ["painting", "yellow"]);

// [...]

// REMOVE ENTRIES WITH TAG "red"
cache.RemoveByTag("red");

// NOW ONLY "risotto_milanese" and "sunflowers" ARE IN THE CACHE

// [...]

// REMOVE ENTRIES WITH TAG "food"
cache.RemoveByTag("food");

// NOW ONLY "sunflowers" IS IN THE CACHE

It's really that simple.

How to make it work, to make it work well, and to make it work in a scalable and flexible way including support for all the resiliency features of FusionCache (eg: fail-safe, auto-recovery, etc) that is a completely different thing.

If you want to know more read here for the proposal, including a complete overview of the design I decided to use for the feature, which I think strikes a delicate balance of all considerations.

And, if you like, let me know your thoughts!

🧼 Clear (docs)

Thanks to the underlying usage of the aforementioned Tagging, it is also now possible for FusionCache to support a proper Clear() method, something that the community has been asking for a long time.

And this, too, works with everything else like cache key prefix, backplane notifications, auto-recovery and so on.

Here's an example:

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

// CLEAR
cache.Clear();

// NOW THE CACHE IS EMPTY

Easy peasy.

For more read here for the design behind it, more details and some performance considerations.

And, if you like, let me know your thoughts!

⚠️ Breaking Changes

Since this is a MAJOR version change (v1 -> v2) there have been multiple binary breaking changes.

Having said that, unless someone somehow decided to re-implement FusionCache itself, a simple package update + full recompile should do the trick, since thanks to overloads and whatnot all the existing code should work as before (and, in case that is not the case, please let me know!).

📕 Docs

The docs have not been updated yet with Tagging or Clear() support, but will be done in time for the official v2.0.0 release.

v2.0.0-preview-1

10 Nov 22:57
Compare
Choose a tag to compare
v2.0.0-preview-1 Pre-release
Pre-release

Important

This is the first PREVIEW of a very big and important milestone for FusionCache.
All help is more than welcome since the main feature, Tagging, is an uber complex beast.

Warning

Although all is already in a very good shape, this is still a PREVIEW version.

Warning

Because of the MAJOR version change, for now I decided to bump the wire format identifier: read more here and here.

🏷️ Tagging (docs)

Yep, it's true: FusionCache now has full support for tagging!
This means we can now associate one or more tags to any cache entry and, later on, simply call RemoveByTag("my-tag") to evict all the entries that have the "my-tag" associated to them.

And yes, it works with all the other features of FusionCache like L1+L2, backplane, fail-safe, soft timeouts, eager refresh, adaptive caching and everything else.

Honestly, the end result is a thing of beauty.

Here's an example:

cache.Set("risotto_milanese", 123, tags: ["food", "yellow"]);
cache.Set("kimchi", 123, tags: ["food", "red"]);
cache.Set("trippa", 123, tags: ["food", "red"]);
cache.Set("sunflowers", 123, tags: ["painting", "yellow"]);

// [...]

// REMOVE ENTRIES WITH TAG "red"
cache.RemoveByTag("red");

// NOW ONLY "risotto_milanese" and "sunflowers" ARE IN THE CACHE

// [...]

// REMOVE ENTRIES WITH TAG "food"
cache.RemoveByTag("food");

// NOW ONLY "sunflowers" IS IN THE CACHE

It's really that simple.

How to make it work, to make it work well, and to make it work in a scalable and flexible way including support for all the resiliency features of FusionCache (eg: fail-safe, auto-recovery, etc) that is a completely different thing.

If you want to know more read here for the proposal, including a complete overview of the design I decided to use for the feature, which I think strikes a delicate balance of all considerations.

And, if you like, let me know your thoughts!

🧼 Clear() (docs)

Thanks to the underlying usage of the aforementioned Tagging, it is also now possible for FusionCache to support a proper Clear() method, something that the community has been asking for a long time.

And this, too, works with everything else like cache key prefix, backplane notifications, auto-recovery and so on.

Here's an example:

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

// CLEAR
cache.Clear();

// NOW THE CACHE IS EMPTY

Easy peasy.

For more read here for the design behind it, more details and some performance considerations.

And, if you like, let me know your thoughts!

⚠️ Breaking Changes

Since this is a MAJOR version change (v1 -> v2) there have been multiple binary breaking changes.

Having said that, unless someone somehow decided to re-implement FusionCache itself, a simple package update + full recompile should do the trick, since thanks to overloads and whatnot all the existing code should work as before (and, in case that is not the case, please let me know!).

📕 Docs

The docs have not been updated yet with Tagging or Clear() support, but will be done in time for the official v2.0.0 release.

v1.4.1

27 Oct 21:29
Compare
Choose a tag to compare

This is a small version, just to update some transitive packages with security vulnerabilities.

⚠️ Update vulnerable dependencies

The affected transitive packages are:

  • System.Text.Json
  • MessagePack
  • Microsoft.Extensions.Caching.Memory

To that to update Microsoft.Extensions.Caching.Memory, an update for System.Diagnostics.DiagnosticSource was also needed.

This is all.

PS: if you have some time, please read the Tagging proposal (eg: evict by tag + clear), which is planned for v2.0. This will be one of the biggest and most important features of FusionCache ever. Any help is welcome!

v1.4.0

15 Sep 17:43
Compare
Choose a tag to compare

Important

Although when updating to a new version it's the norm to update all referenced packages at once, this time is even more important. Because of a small change in the IFusionCacheSerializer interface, it is highly suggested to update all packages, including in transitive dependencies.

🚀 Add support for RecyclableMemoryStream to serializers (docs)

It is now possible to use RecyclableMemoryStreams with some of the supported serializers.
It's opt-in, meaning totally optional, and it's possible to use the default configuration or to specify one in the constructor, for maximum control and to fine-tune it however we want.

Thanks @viniciusvarzea for the suggestion!

See here for the original issue.

🔭 Better observability for GET operations (docs)

Community user @dotTrench noticed that when using OpenTelemetry, GET activities (TryGet, GetOrDefault, etc) were not including a useful bit of information: the hit/miss status.
While adding support for it via an extra tag, it was also noticed that it was not including another useful bit of information: the stale/fresh status.
Now this is solved, thanks to 2 new tags.

See here for the original issue.

💣 Throw specific exception when factory fails without fail-safe (docs)

Since v1.3.0 it's possible to trigger a factory fail without throwing an exception, so that fail-safe can kick in and do its thing.

But what happens if fail-safe is not enabled or if there's no stale value to fall back to?

Previously a plain Exception was being thrown, but that is hardly a best practice: now a more specific exception type has been created and will be thrown instead, namely FusionCacheFactoryException.

See here for the original issue.

🛑 Add cancellation support to serializers (docs)

Previously serializers did not support cancellation: now this is supported, via the standard CancellationTokens.

Thanks @b-c-lucas for noticing it!

See here for the original issue.

🚀 Better perf for FusionCacheProvider (docs)

Community user @MarkCiliaVincenti contributed with a PR that improved the performance of FusionCacheProvider, used with Named Caches, thanks to the use of FrozenDictionary.

See here for the original PR.

🐞 Respect the Size of an entry loaded in L1 from L2 (docs)

Community user @Amberg noticed that when the MemoryCache (L1) passed to FusionCache had a SizeLimit configured, and an entry wasbeing saved with a Size specified, that was not used when automatically getting the same entry from another node via the distributed cache, causing an issue.

Now this is solved, in 2 different scenarios:

  • common case: setting an entry WITH a specific size on a cache WITH size limit, and then getting the same entry from another cache (WITH size limit, too), meaning it restores the entry with its size
  • edge case (and probably a wrong setup anyway): setting an entry WITHOUT a specific size on a cache WITHOUT a size limit, and then getting the same entry from another cache (but WITH size limit) works if there's at least a Size specified in the entry options (either for the specific method call or in the DefaultEntryOptions). This is more of an edge case, since usually what is logically the same cache should be configured the same for every instance (a.k.a. on every node)

See here for the original issue.

✅ Way better tests

As always some tests have been added for each new feature.
On top of that though, the testes have been changed to make them even more resilient to microscopic differences between different runtimes, OSs, etc: most of the problems were related to this almost unknown behaviour.
This helped make the entire test suite even more stable and with a predictable outcome, both locally and on GitHub actions.

Overall, FusionCache currently has around 750 tests, including combinatorial params.

See here for some juicy details.

📕 Docs

Updated some docs with the latest new things.

v1.3.0

04 Aug 22:55
Compare
Choose a tag to compare

♊ Auto-Clone (docs)

In general is never a good idea to mutate data retrieved from the cache: it should always be considered immutable/readonly.
To see why, read more in the docs.

Not all the scenarios where mutating a piece of data we got from the cache are necessarily wrong though, as users may have a particular use case where that may be needed, and ideally they should be abe to do that in an easy (and optimized!) way, by following the tried and true "it just works" mindset.

With Auto-Clone this is now possible.

A couple of details:

  • it just works, out of the box
  • is easy to use
  • doesn't require extra coding/setup (it's just a new EnableAutoClone option)
  • uses existing code infrastructure (eg: IFusionCacheSerializer)
  • has granular control on a per-entry basis
  • is performant (as much as possible)

Thanks to community users @JarrodOsborne and @kzkzg !

See here for the original issue.

💣 Fail-Safe Without Exceptions (docs)

Currently the way to activate fail-safe is for a factory to throw an exception.

This makes sense, since the whole point of fail-safe is to protect us when an error occurs while executing a factory.

But there may be other ways to do it, for example by using a variation of the Result Pattern or similar approaches, in which throwing an exception is not necessary.

This is now possible thanks to the new Fail(...) method on the FusionCacheFactoryExecutionContext<TValue> type, which we can access when executing a factory.

A quick example:

var productResult = await cache.GetOrSetAsync<Result<Product>>(
	$"product:{id}",
	async (ctx, ct) =>
	{
		var productResult = GetProductFromDb(id);

		if (productResult.IsSuccess == false)
		{
			return ctx.Fail(productResult.Error);
		}

		return productResult;
	},
	opt => opt.SetDuration(duration).SetFailSafe(true)
);

Thanks to community user @chrisbbe that noticed it!

See here for the original issue.

🧙‍♂️ Adaptive Caching on errors (docs)

Previously it was not possible to use adaptive caching when an error occurred during a factory execution.
This was usually not a big issue, but it left a particular edge case not fully uported: selectively enabling/disabling fail-safe on errors.
Now this is possible, in the usual unified way.

Thanks to community user @cmeyertons for spotting it, and for creating the PR that solved it.

See here for the original issue.

📦 More granular (and less) dependencies with multi-targeting

Thanks to a note by community user @thompson-tomo FusionCache now multi-targets different .NET vesions.
This was not needed per-se, but by doing it FusionCache can now have less dependencies for some TFMs.

See here for the original issue.

🚀 Better perf for FusionCacheProvider (docs)

Community user @0xced contributed with a PR that improved the performance of FusionCacheProvider, used with Named Caches.

See here for the original PR.

⚠️ Update dependencies for CVE-2024-30105

Communicty user @dependabot (😅) noticed CVE-2024-30105 and promptly bumped the referenced version of the System.Text.Json package.

Note that this is only related to the package ZiggyCreatures.FusionCache.Serialization.SystemTextJson.

✅ Better tests

Some tests have been added for each new feature, and overall better snapshot tests.

📕 Docs

Updated some docs with the latest new things.

v1.2.0

02 Jun 19:48
Compare
Choose a tag to compare

🔑 Added DI Keyed Services support (docs)

Since .NET 8 we now have native support for multiple services of the same type, identified by different names, thanks to the addition of so called keyed services.

The idea is basically that we can now register services not only by type but also by specifying the name, like this:

services.AddKeyedSingleton<MyService>("foo");
services.AddKeyedSingleton<MyService>("bar");

and later is possible to resolve it by both the type and a name.

Another way is to simply mark a constructor parameter or web action with the [FromKeyedServices] attribute, like this:

app.MapGet("/foo", ([FromKeyedServices("foo")] MyService myService) => myService.Whatever(123));
app.MapGet("/bar", ([FromKeyedServices("bar")] MyService myService) => myService.Whatever(123));

From now on, when registering a named cache, we can simply add AsKeyedServiceByCacheName() like this:

services.AddFusionCache("MyCache")
  .AsKeyedServiceByCacheName();

and later we'll be able to have the named cache both as usual:

app.MapGet("/foo", (IFusionCacheProvider cacheProvider) => {
  var cache = cacheProvider.GetCache("MyCache");
  cache.Set("key", 123);
});

and as a keyed service, like this:

app.MapGet("/foo", ([FromKeyedServices("MyCache")] IFusionCache cache) => {
  cache.Set("key", 123);
});

We can even use AsKeyedService(object? serviceKey) and specify a custom service key like for any other keyed service in .NET.

On top of being able to register FusionCache as a keyed service, we can even consume keyed services as FusionCache components, like memory cache, distributed cache, serializer, backplane, etc.

For more read at the official docs.

See here for the original issue.

⚡ Add PreferSyncSerialization option

It has been observed that in some situations async serialization and deserialization can be slower than the sync counterpart: this has nothing to do with FusionCache itself, but how serialization works in general.

So I added a new option called PreferSyncSerialization (default: false, fully backward compatible), that can allow the sync version to be preferred.

See here for the original issue.

🔭 Better OpenTelemetry traces for backplane notifications

Community user @imperugo noticed that when using the backplane with OpenTelemetry traces enabled, all the spans for the notifications incoming via the backplane were put under one single parent span, basically creating a single mega-span "containing" all the others.

image

Ideally, each span for each notification should be on their own, and now this is the case.

Also while I was at it I noticed another couple of things that, if added to the traces, could make the developer experience better.
In detail:

  • include a tag with the source id (the InstanceId of the remote FusionCache instance)
  • change the status of the trace in case of errors, like invalid notifications or similar
  • add an event in case of, well, some event occurring during the activity

So yeah, I took this opportunity to make the overall experience better.

Finally, since backplane notifications can create a lot of background noise inside observability tools, I changed the default so that, even when there's a backplane setup, traces for backplane notifications are not enabled: to change this simply enable it at setup time.

See here for the original issue.

🐵 Add ChaosMemoryCache

Among all the chaos-related components already available, one to work with IMemoryCache was missing: not anymore.

✅ Better tests

Some more tests have been added, including better cross-platform snapshot tests.

📕 Docs

Updated some docs with the latest new things.

v1.2.0-preview1

19 May 22:57
Compare
Choose a tag to compare
v1.2.0-preview1 Pre-release
Pre-release

🔑 Added DI Keyed Services support

Since .NET 8 we now have native support for multiple services of the same type, identified by different names, thanks to the addition of so called keyed services.

The idea is basically that we can now register services not only by type but also by specifying the name, like this:

services.AddKeyedSingleton<MyService>("foo");
services.AddKeyedSingleton<MyService>("bar");

and later is possible to resolve it by both the type and a name.

Another way is to simply mark a constructor parameter or web action with the [FromKeyedServices] attribute, like this:

app.MapGet("/foo", ([FromKeyedServices("foo")] MyService myService) => myService.Whatever(123));
app.MapGet("/bar", ([FromKeyedServices("bar")] MyService myService) => myService.Whatever(123));

From now on, when registering a named cache, we can simply add AsKeyedService() like this:

services.AddFusionCache("MyCache")
  .AsKeyedService();

and later we'll be able to have the named cache with something like this:

app.MapGet("/foo", ([FromKeyedServices("MyCache")] IFusionCache cache) => {
  cache.Set("key", 123);
});

Of course the named cache provider way is still available, like this:

app.MapGet("/foo", (IFusionCacheProvider cacheProvider) => {
  var cache = cacheProvider.GetCache("foo");
  cache.Set("key", 123);
});

See here for the original issue.

⚡ Add PreferSyncSerialization option

It has been observed that in some situations async serialization and deserialization can be slower than the sync counterpart: this has nothing to do with FusionCache itself, but how serialization works in general.

So I added a new option called PreferSyncSerialization (default: false, fully backward compatible), that can allow the sync version to be preferred.

See here for the original issue.

🐵 Add ChaosMemoryCache

Among all the chaos-related components already available, one to work with IMemoryCache was missing: not anymore.

✅ Better tests

Some more tests have been added.

📕 Docs

Updated some docs with the latest new things.

v1.1.0

24 Apr 07:56
Compare
Choose a tag to compare

The theme for this release is some bug fixes, general quality of life improvements and some minor perf optimizations.

📞 Added a couple of missing OnMiss events

Community user @ConMur noticed that sometimes, in a couple of code paths related to distributed cache operations, FusionCache was missing some OnMiss events (no pun intended): now this has been fixed.

See here for the original issue.

💣 Better FailSafeMaxDuration handling

User @F2 and user @sabbadino both noticed that fail-safe max duration was not being respected all the times, particularly when multiple fail-safe activations actually occurred in sequence there was in fact the risk of extending the physical duration of the cache more than what should've been correct.
This has been fixed (while also introducing some nice memory and cpu savings!).

See here and here for the original issues.

💀 A rare case of deadlock

While doing some extensive testing community user @martindisch discovered a rare case of deadlock that was happening only when all of these conditions were met simultaneously:

  • Eager Refresh enabled
  • call GetOrSet[Async] while passing a CancellationToken
  • the call that actually triggered the eager refresh is cancelled, after the eager refresh kicked in but before it finished
  • not all the times, but only when the execution flow passed in a certain spot at a certain time

This issue kicked off an experimentation about a reworking of FusionCache internals regarding the general theme of cancellations of background factory executions in general (eager refresh, background factory completion with soft/hard timeouts, etc): I am happy to say that now the deadlock is gone for good.
To do that well I slightly changed the behaviour of FusionCache regarding background factory executions: now they cannot be cancelled anymore by cancelling the original request that generated them, since it doesn't make that much sense to begin with, since a cancellation is used to cancel the current operation, but a background execution (particularly with eager refresh) is basically a side effect, which does have a life of its own, so it doesn't make a lot of sense to cancel that, too.

All in all, there should be realistically no discernible externally observable difference in behaviour (and no more deadlocks!).

Finally, I've added some tests to detect these scenario to avoid future regressions.

See here for the original issue.

📢 Better AutoRecoveryDelay default value

The default value for AutoRecoveryDelay has been changed from 2s to 5s, to better align with the standard reconnect timing of StackExchange.Redis, which is the most commonly used implementation for the distributed cache and the backplane.
The idea is about "sensible defaults" and the overarching theme of "it just works": if the default distributed cache and backplane are Redis, let's just make sure that the defualt experience is better aligned with that (and also, when bad things happen in production, automatically recovering from it with a slightly longer delay is, pragmatically, really not a big deal).

🧽 Some code cleanup

Thanks to @SimonCropp the code has been cleaned up a little bit here, updated to the latest C# features there, plus some other minor tweaks. Thanks Simon!

🚀 Performance

In this release I've been able to squeeze in some minor but nice memory/cpu optimizations.

✅ Better tests

I added some more tests to have a higher code coverage.

📕 Docs

Updated some docs with the latest new things.

v1.0.0

01 Mar 00:10
6179fe5
Compare
Choose a tag to compare

FusionCache is now v1.0 🥳

Yes, it finally happened.

Let's see what this release includes.

🚀 Performance, performance everywhere

FusionCache always tried to be as optimized as possible, but sometimes useful new features took some precedence over micro-optimizing this or that.

Now that all the major features (and then some) are there, it was time to do a deep dive and optimize a cpu cycle here, remove an allocation there and tweak some hot path to achieve the best use of resources.

So here's a non-comprehensive list of nice performance improvements in this release:

  • zero allocations/minimal cpu usage in Get happy path
  • reduced allocations/cpu usage in Set happy path
  • less allocations/cpu usage when not using distributed components
  • less allocations/cpu usage (via closures) when using events
  • zero allocations at all when not using logging (no support structures init for operationId generation)
  • reduced overhead in some async code paths

Oh, and thanks to community member @neon-sunset for the issue highlighting some shortcomings, that now have been solved!

See here for the issue.

🦅 Better Eager Refresh (docs)

When executing an Eager Refresh, the initial check for an updated cache entry on the distributed cache is now totally non-blocking, for even better performance.

🆕 Added IgnoreIncomingBackplaneNotifications option (docs)

FusionCache always allowed to optionally skip sending backplane notifications granularly, for each operation (or globally thanks to DefaultEntryOptions): it was not possible though to ignore receiving them.

Now we may be thinking "why would I want to use a backplane, but not receive its notifications?" and the answer to that can be found in the feature request made by community member @celluj34 .

See here for the issue.

⚠️ Better nullability annotations for generic types

This is linked to the evolution of nullable reference types, nullables with generics and the related static analysis with each new version of c# and its compiler.

Along the years I tried to adjust the annotations to better handle generic types + nullables with each new version, because what the compiler allowed me to do and was able to infer changed at every release (the first version had problems with generics without where T : class/struct constraints, for example).

I've now updated them to reflect the latest behaviour, so that it's now more strict in the generic signatures, mostly for GetOrSet<T> and GetOrSetAsync<T>: in previous versions the return type was always nullable, so when calling GetOrSet<Person> we would have a return value of Person? (nullable) even if the call was not GetOrSet<Person?>.

Now this is better.

Thanks for community member @angularsen for highlighting this.

See here for the issue.

⚠️ Changed FusionCacheEntryOptions.Size to be nullable (docs)

The type of the Size option in the FusionCacheEntryOptions type has been historically long (default: 1): the underlying Size option in the MemoryCacheEntryOption type is instead long? (default: null).

So, to better align them, now the types and default values are the same.

🪞 Reflection no more

Not technically a problem per se, but with the release of the new and improved auto-recovery in v0.24.0, I had to add a little bit of reflection usage to support complex scenario of recovering some of the transient distributed errors.

Now, the small amount of code that was using reflection is gone, and this in turn means:

  • overall better performance
  • be better positioned for, eventually, playing with AOT (where reflection is basically a no-go)

See here for the issue.

🔭 Add eviction reason to Open Telemetry metrics

With v0.26.0 native support for Open Telemetry has been added to FusionCache.

Now community members @JoeShook noticed that the eviction reason was missing from the Eviction counter, which could be in fact useful.

Now it has been added, thanks Joe!

See here for the PR.

🔭 Removed cache instance id from Open Telemetry metrics

Community member @rafalzabrowarny noticed that FusionCache was adding a tag to the metrics, specifically one with the cache instance id: now, since it's a random value generated for every FusionCache instance, it will have a high cardinality and that is usually problematic with APM platforms and tools.

Now it's gone, thanks Rafał!

See here for the issue.

👷‍♂️ Better detection of incoherent CacheNames options

With the introduction of the builder in v0.20 FusionCache got a nice way to configure the various options and components, in a very flexible way.

In one particular scenario though, it was possible to specify something incoherent: a single instance with multiple CacheNames, specified in different ways by using both the high level AddFusionCache("MyCache") and the WithOptions(...) methods.

A couple of examples:

services.AddFusionCache("foo")
  .WithOptions(options => {
    options.CacheName = "bar";
  });

or, more subtly:

services.AddFusionCache()
  .WithOptions(options => {
    options.CacheName = "bar";
  });

Now FusionCache correctly detects this scenario and throws an exception as soon as possible, helping the developer by showing the golden path to follow and how to do to solve it.

Thanks @albx for spotting this!

See here for the issue.

👷‍♂️ Better builder auto-setup

Again with the builder, when using the TryWithAutoSetup() method in the builder it now also try to check for registered memory lockers by calling TryWithMemoryLocker(), automatically.

✅ Better tests

I added some more tests to have a higher code coverage, and made the snapshot tests better.

📜 Better logs

More detailed log messages in some areas where they could've been better (mostly related to the backplane).

📕 Docs

Updated some docs with the latest new things.