ZiggyCreatures / FusionCache

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

[BUG] AllowBackgroundDistributedCacheOperations serialization and cache operations seem to be happening in the foreground #213

Closed Jordan-Osborn closed 5 months ago

Jordan-Osborn commented 6 months ago

AllowBackgroundDistributedCacheOperations serialization and cache operations seem to be happening in the foreground

Enabling AllowBackgroundDistributedCacheOperations in the fusion cache options doesn't seem to run distributed cache operations in the backround.

I'm running with a second level sqlite disk cache, and see requests being slowed down due to waiting on serialization/setting of the distributed cache layer (this works out to an appreciable fraction of total request time, as it's caching a relatively large data set).

I traced the callstack to here: https://github.com/ZiggyCreatures/FusionCache/blob/d1a36c287e40b160396672bf0905c8eb1d0a573b/src/ZiggyCreatures.FusionCache/Internals/Distributed/DistributedCacheAccessor_Async.cs#L72

It looks like serialization is not being run in the background and the passed in isBackground flag is only being used for setting descriptions.

Is there something I'm missing here or is this a bug?

To Reproduce

The code below

using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Caching.Distributed;
using NeoSmart.Caching.Sqlite;
using ZiggyCreatures.Caching.Fusion;
using ZiggyCreatures.Caching.Fusion.Serialization;
using ZiggyCreatures.Caching.Fusion.Serialization.SystemTextJson;

var builder = WebApplication.CreateBuilder(args);
builder.Services.AddSingleton<IFusionCacheSerializer, FusionCacheSystemTextJsonSerializer>();
builder.Services.AddFusionCache()
    .WithDistributedCache(s => s.GetRequiredService<IDistributedCache>())
    .WithSerializer(s => s.GetRequiredService<IFusionCacheSerializer>());
SQLitePCL.Batteries.Init();
builder.Services.AddSqliteCache(o => { o.CachePath = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), "sqlite-cache.db"); }, null);
var app = builder.Build();

var fusionCacheEntryOptionsEnabled = new FusionCacheEntryOptions
{
    SkipDistributedCache = false,
    AllowBackgroundDistributedCacheOperations = true,
};

var fusionCacheEntryOptionsDisabled = new FusionCacheEntryOptions
{
    SkipDistributedCache = true,
    AllowBackgroundDistributedCacheOperations = true,
};

var rawData = Enumerable.Range(0, 10_000_000).Select(i => $"Item {i}").ToList();

app.MapGet("/get-data-enabled", async ([FromServices] IFusionCache cache, CancellationToken cancellationToken) =>
{
    var data = await cache.GetOrSetAsync<List<string>>(Guid.NewGuid().ToString(), ct => Task.FromResult(rawData), options: fusionCacheEntryOptionsEnabled, cancellationToken);
    return data.Count;
});

app.MapGet("/get-data-disabled", async ([FromServices] IFusionCache cache, CancellationToken cancellationToken) =>
{
    var data = await cache.GetOrSetAsync<List<string>>(Guid.NewGuid().ToString(), ct => Task.FromResult(rawData), options: fusionCacheEntryOptionsDisabled, cancellationToken);
    return data.Count;
});

await app.RunAsync();

image

Expected behavior

Enabling AllowBackgroundDistributedCacheOperations should offload significant work to the background (serialisation/setting 2nd level cache), I'd expect the queries above to take a similar amount of time.

Versions

I've encountered this issue on:

jodydonetti commented 6 months ago

Hi @Jordan-Osborn , your expectations are right, mostly: let me explain.

Some distributed cache operations will, in fact, run in the background when the AllowBackgroundDistributedCacheOperations option is set to true, but the catch is that this is not possible for all operations. Why?

There are 2 different classes of operations:

Of course GetOrSet is a mix of them, being composed of both a "get phase" and, maybe, a "set phase".

When you think about it, for "set operations" FusionCache is not waiting for a return value, and so they can be executed in a fire-and-forget way.

The same cannot be said for "get operations", where FusionCache is asking for some data, and so it cannot be executed in a fire-and-forget way. It would be like asking a barman for a drink and immediately going away 😅.

The test you should do is with a Set operation: in that case you should see very similar timings.

Hope this helps, let me know how it goes.

jodydonetti commented 6 months ago

Oh, also: a tangential thing I'd like to mention is that you can also set 2 additional options:

They act like the corresponding soft/hard timeouts for the factory (FactorySoftTimeout and FactoryHardTimeout), meaning they'll be used when there is VS there is not a fallback value.

By setting it you are basically saying to FusionCache "hey, if it takes too much to check in the distributed cache, just forget about it and go to the database".

Watch out though that by setting them to a value which is too low may mean that the distributed cache will basically never have time to answer with a value.

Hope this helps, let me know.

jodydonetti commented 5 months ago

I'm closing this for now: in case you need it I'll re-open.