ZiggyCreatures / FusionCache

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

[FEATURE] Restoring Calculated Cache Entry Size When Loading from Distributed Cache #301

Closed Amberg closed 2 months ago

Amberg commented 2 months ago

Problem

The entry size is not stored in Distributed cache.

Solution

Store the calculated cache entry size in the distributed cache.

Alternatives

A callback function to recalculate the entry size whenever it is loaded from the distributed cache.

Additional context

I am attempting to use the Distributed Cache with a defined SizeLimit to manage memory usage effectively. However, this results in the following exception: System.InvalidOperationException: Cache entry must specify a value for Size when SizeLimit is set Because the SizeLimitcalculated during entry creation (adaptive caching) is not stored in the distributed cache.

Workaround

The only workaround I've found is setting an approximate size in the DefaultOptions. Unfortunately, this approach leads to imprecise memory limits, especially when the sizes of cached objects vary significantly.

Is there another way or best practice to manage different cache entry sizes while using a MemoryCache with SizeLimit set?

jodydonetti commented 2 months ago

Hi @Amberg , I think you may have a point here... let me do some checks, will get back to you as soon as possible.

Amberg commented 2 months ago

Hi @Amberg , I think you may have a point here... let me do some checks, will get back to you as soon as possible.

Thank you It seems to set the Size in the DefaultOptions does not help.

If i can support you somehow, let me know.

jodydonetti commented 2 months ago

Hi @Amberg , thanks! I'm working on this, will update during the weekend.

jodydonetti commented 2 months ago

Hi @Amberg , I've added support for this.

Here's the test that verify this:

[Theory]
[ClassData(typeof(SerializerTypesClassData))]
public async Task CanUseMultiNodeCachesWithSizeLimitAsync(SerializerType serializerType)
{
  var backplaneConnectionId = Guid.NewGuid().ToString("N");
  var key1 = Guid.NewGuid().ToString("N");
  var key2 = Guid.NewGuid().ToString("N");

  var distributedCache = CreateDistributedCache();
  using var memoryCache1 = new MemoryCache(new MemoryCacheOptions()
  {
      SizeLimit = 10
  });
  using var memoryCache2 = new MemoryCache(new MemoryCacheOptions()
  {
      SizeLimit = 10
  });
  using var memoryCache3 = new MemoryCache(new MemoryCacheOptions()
  {
      //SizeLimit = 10
  });
  using var cache1 = CreateFusionCache(null, serializerType, distributedCache, CreateBackplane(backplaneConnectionId), memoryCache: memoryCache1);
  using var cache2 = CreateFusionCache(null, serializerType, distributedCache, CreateBackplane(backplaneConnectionId), memoryCache: memoryCache2);
  using var cache3 = CreateFusionCache(null, serializerType, distributedCache, CreateBackplane(backplaneConnectionId), memoryCache: memoryCache3);

  // SET THE ENTRY (WITH SIZE) ON CACHE 1 (WITH SIZE LIMIT)
  await cache1.SetAsync(key1, 1, options => options.SetSize(1));

  await Task.Delay(1_000);

  // GET THE ENTRY (WITH SIZE) ON CACHE 2 (WITH SIZE LIMIT)
  var maybe2 = await cache2.TryGetAsync<int>(key1);

  Assert.True(maybe2.HasValue);
  Assert.Equal(1, maybe2.Value);

  // SET THE ENTRY (WITH NO SIZE) ON CACHE 3 (WITH NO SIZE LIMIT)
  await cache3.SetAsync(key2, 2);

  await Task.Delay(1_000);

  // GET THE ENTRY (WITH NO SIZE) ON CACHE 1 (WITH SIZE LIMIT)
  // -> FALLBACK TO THE SIZE IN THE ENTRY OPTIONS
  var maybe1 = await cache1.TryGetAsync<int>(key2, options => options.SetSize(1));

  Assert.True(maybe1.HasValue);
  Assert.Equal(2, maybe1.Value);
}

The first part (first 2 Asserts) verifies that 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) works, meaning it restores the entry with its size. This is the "normal" situation.

The second part (second 2 Asserts) verifies that 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).

Let me know what you think.

Will include in the next version, releasing probably tomorrow.

Amberg commented 2 months ago

That looks rock solid and really promising

Thanks for the great work

jodydonetti commented 2 months ago

Hi, v1.4.0 has been released 🥳