Skip to content

v2.0.0-preview-3

Pre-release
Pre-release
Compare
Choose a tag to compare
@jodydonetti jodydonetti released this 09 Dec 23:43
· 70 commits to release/v2_0_0 since this 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.

See here for the issue.

❤️ 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 are still using them your project will not compile.

They have been obsolete for a long time now, and this should not be a problem for anyone (and, btw, the needed changes are minuscule).

✅ More and better tests

Some tests have been optimize and more tests have been added, including the ones for the new Microsoft HybridCache integration.


⬇️ Stuff from preview-2 ⬇️


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


⬇️ Stuff 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.