v2.0.0-preview-4
Pre-releaseImportant
This is most probably the LAST PREVIEW of FusionCache V2, before going GA.
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.
Ⓜ️ Support for Microsoft's new HybridCache, without the extra package
Thanks to a suggestion by community member @pwelter34 the ZiggyCreatures.FusionCache.MicrosoftHybridCache
extra package was not actually needed.
Because of this, 2 things happened:
- the adapter class is now directly in the main FusionCache package
- the extra package has been updated, made empty, and will be marked as deprecated
Less code, less packages and less dependencies to deal with, yeah 🥳
See here for the issue.
🆕 New AllowStaleOnReadOnly
entry option
Historically fail-safe has been used even with read-only methods like TryGet
and GetOrDefault
, where a "fail" cannot actually happen.
This sometimes created some confusion, and lead to people enabling fail-safe globally and sometimes getting back stale values even with read-only operations.
To allow for better and more granular control over this aspect, a new AllowStaleOnReadOnly
entry option has been added, which controls the return of stale values in read-only operations.
ℹ️ Better metadata
Community user @jarodriguez-itsoft noticed that sometimes the payload in the distributed cache was not as small as it could've been.
Because of this the internal shape of the metadata class has been changed, to better reflect the common usage patterns and save memory whenever possible.
This set of changes made it possible to get a metadata-less scenario, which in a lot of scenario will lead to smaller payloads in the distributed cache, less network usage and so on.
On top of this, some fields has been changed to a different type to allocate less and consume less cpu, while at the same time better support has been added for cross-nodes entry priority when scaling horizontally.
See here for the original issue.
⚡ Better serializers
Thanks to a big effort by community user @stebet , serializers are now natively better and allocate less, thanks to some array pools/buffers magic.
Because of this, support for the external RecyclableMemoryStreamManager
has been removed, since it's not necessary anymore.
Less dependencies here, too!
See here for the original PR.
⬇️ Stuff from preview-3 ⬇️
Ⓜ️ 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", ...)
// ASYNCfusionCache.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 totrue
(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.