Skip to content

Setting Size on MemoryCacheEntry after insertion causes faulty CurrentEstimatedSize #88733

@Lilk

Description

@Lilk

Description

If holding onto a Microsoft.Extensions.Caching.Memory.CacheEntry, in order to update the size after it is known (If e.g. caching AsyncLazy values, and needing to update the size once the value has been materialized), the overall CurrentEstimatedSize is not updated.

If the size is updated to a smaller value than the initally set value, this will lead to emptying of the cache and busy-loop of compaction upon reaching the size limit.

Seen here is the result of a compaction with the parameter 0.5, causing half of the entries being dropped (lower graph), but seemingly no to little impact on the reported size.
image

As the overcapacitycompaction scheduling is based on this metric, such a scenario will cause repeated scheduling of OvercapacityCompaction (for every insertion), and will cause a busy CPU loop, quickly dumping all of the entries in the cache, but never stopping the compaction cycle. See here graphs for patterns of entry count vs estimated size as well as cache misses.

image

And the CPU busy iterating the coherent state:

image

I am at this point unsure of the symptoms if Size were to be update to a higher value, the reported CurrentEstimatedSize would be less than the actual size, but what effects would it have to the MemoryCache, what effects will take place if the CurrentEstimatedSize is less than zero?

Reproduction Steps

// See https://aka.ms/new-console-template for more information
using Microsoft.Extensions.Caching.Memory;

Console.WriteLine("Hello, World!");
var _cache = new MemoryCache(new MemoryCacheOptions()
{
    TrackStatistics = true,
    ExpirationScanFrequency = TimeSpan.FromMinutes(1),
    CompactionPercentage = 0.1,
    SizeLimit = 10,

});

Task task = null;

_cache.GetOrCreate("test", (ICacheEntry entry) =>
{
    entry.Size = 4;
    task = Task.Delay(1000);
    task.ContinueWith((t) =>
    {
        try
        {
            entry.Size = 2;
        }
        catch (Exception e)
        {
            Console.WriteLine(e.Message);
        }
        return;
    });

    return new object();
});

var stats1 = _cache.GetCurrentStatistics();
Console.WriteLine($"Cache size after GetOrCreate {stats1.CurrentEntryCount}, {stats1.CurrentEstimatedSize}");
await task;
var stats2 = _cache.GetCurrentStatistics();
Console.WriteLine($"Cache size after GetOrCreate {stats2.CurrentEntryCount}, {stats2.CurrentEstimatedSize}");

Expected behavior

Hello, World!
Cache size after GetOrCreate 1, 4
Cache size after GetOrCreate 1, 2

I would assume one of two options:

  1. The CurrentEstimatedSize is updated correctly, by applying an interlocked add on the cache size using the delta of the entries new and old size.
  2. If 1) is not possible, assigning Size or calling SetSize() should throw if the CacheEntry has been disposed (ingested into the cache), since the cache obviously doesnt support such properties being set after ingestion.

At the very least this behaviour should be documented, and it should be noted that calling SetSize after the callback passed to GetOrCreate has returned is undefined/dangerous.

Actual behavior

Hello, World!
Cache size after GetOrCreate 1, 4
Cache size after GetOrCreate 1, 4

CurrentEstimatedSize is not updated, but the entries size is.

Regression?

I do not know if this worked on earlier builds.

Known Workarounds

No response

Configuration

dotnet 7, windows and linux.

Other information

No response

Metadata

Metadata

Assignees

No one assigned

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions