Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[release/6.0] Cache LastAccessed during MemoryCache compaction #62286

Merged
merged 2 commits into from
Dec 15, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -395,9 +395,10 @@ public void Compact(double percentage)
private void Compact(long removalSizeTarget, Func<CacheEntry, long> computeEntrySize)
{
var entriesToRemove = new List<CacheEntry>();
var lowPriEntries = new List<CacheEntry>();
var normalPriEntries = new List<CacheEntry>();
var highPriEntries = new List<CacheEntry>();
// cache LastAccessed outside of the CacheEntry so it is stable during compaction
var lowPriEntries = new List<CompactPriorityEntry>();
var normalPriEntries = new List<CompactPriorityEntry>();
var highPriEntries = new List<CompactPriorityEntry>();
long removedSize = 0;

// Sort items by expired & priority status
Expand All @@ -415,13 +416,13 @@ private void Compact(long removalSizeTarget, Func<CacheEntry, long> computeEntry
switch (entry.Priority)
{
case CacheItemPriority.Low:
lowPriEntries.Add(entry);
lowPriEntries.Add(new CompactPriorityEntry(entry, entry.LastAccessed));
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I assume there will not be any boxing operation that causes additional allocations. right?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Correct. We are using a List<CompactPriorityEntry>, so no boxing will occur. The small structs will just be copied in and out of the list. This is the same with the 7.0+ implementation, but there we are using ValueTuple instead of a custom struct.

break;
case CacheItemPriority.Normal:
normalPriEntries.Add(entry);
normalPriEntries.Add(new CompactPriorityEntry(entry, entry.LastAccessed));
break;
case CacheItemPriority.High:
highPriEntries.Add(entry);
highPriEntries.Add(new CompactPriorityEntry(entry, entry.LastAccessed));
break;
case CacheItemPriority.NeverRemove:
break;
Expand All @@ -445,7 +446,7 @@ private void Compact(long removalSizeTarget, Func<CacheEntry, long> computeEntry
// ?. Items with the soonest absolute expiration.
// ?. Items with the soonest sliding expiration.
// ?. Larger objects - estimated by object graph size, inaccurate.
static void ExpirePriorityBucket(ref long removedSize, long removalSizeTarget, Func<CacheEntry, long> computeEntrySize, List<CacheEntry> entriesToRemove, List<CacheEntry> priorityEntries)
static void ExpirePriorityBucket(ref long removedSize, long removalSizeTarget, Func<CacheEntry, long> computeEntrySize, List<CacheEntry> entriesToRemove, List<CompactPriorityEntry> priorityEntries)
{
// Do we meet our quota by just removing expired entries?
if (removalSizeTarget <= removedSize)
Expand All @@ -458,9 +459,10 @@ static void ExpirePriorityBucket(ref long removedSize, long removalSizeTarget, F
// TODO: Refine policy

// LRU
priorityEntries.Sort((e1, e2) => e1.LastAccessed.CompareTo(e2.LastAccessed));
foreach (CacheEntry entry in priorityEntries)
priorityEntries.Sort(static (e1, e2) => e1.LastAccessed.CompareTo(e2.LastAccessed));
foreach (CompactPriorityEntry priorityEntry in priorityEntries)
{
CacheEntry entry = priorityEntry.Entry;
entry.SetExpired(EvictionReason.Capacity);
entriesToRemove.Add(entry);
removedSize += computeEntrySize(entry);
Expand All @@ -473,6 +475,20 @@ static void ExpirePriorityBucket(ref long removedSize, long removalSizeTarget, F
}
}

// use a struct instead of a ValueTuple to avoid adding a new dependency
// on System.ValueTuple on .NET Framework in a servicing release
private readonly struct CompactPriorityEntry
{
public readonly CacheEntry Entry;
public readonly DateTimeOffset LastAccessed;

public CompactPriorityEntry(CacheEntry entry, DateTimeOffset lastAccessed)
{
Entry = entry;
LastAccessed = lastAccessed;
}
}

public void Dispose()
{
Dispose(true);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@
<TargetFrameworks>netstandard2.0;net461</TargetFrameworks>
<EnableDefaultItems>true</EnableDefaultItems>
<PackageDescription>In-memory cache implementation of Microsoft.Extensions.Caching.Memory.IMemoryCache.</PackageDescription>
<GeneratePackageOnBuild>true</GeneratePackageOnBuild>
<ServicingVersion>1</ServicingVersion>
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@safern - this is all I need to do to service this package, correct?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes 😄

</PropertyGroup>

<ItemGroup>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@
// The .NET Foundation licenses this file to you under the MIT license.

using System;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Extensions.Internal;
using Xunit;

Expand Down Expand Up @@ -83,4 +85,60 @@ public void CompactPrioritizesLRU()
Assert.Equal("value4", cache.Get("key4"));
}
}

[CollectionDefinition(nameof(DisableParallelization), DisableParallelization = true)]
public class DisableParallelization { }

[Collection(nameof(DisableParallelization))]
public class CompactTestsDisableParallelization
{
/// <summary>
/// Tests a race condition in Compact where CacheEntry.LastAccessed is getting updated
/// by a different thread than what is doing the Compact, leading to sorting failing.
///
/// See https://github.com/dotnet/runtime/issues/61032.
/// </summary>
[Fact]
public void CompactLastAccessedRaceCondition()
{
const int numEntries = 100;
MemoryCache cache = new MemoryCache(new MemoryCacheOptions());
Random random = new Random();

void FillCache()
{
for (int i = 0; i < numEntries; i++)
{
cache.Set($"key{i}", $"value{i}");
}
}

// start a few tasks to access entries in the background
Task[] backgroundAccessTasks = new Task[Environment.ProcessorCount];
bool done = false;

for (int i = 0; i < backgroundAccessTasks.Length; i++)
{
backgroundAccessTasks[i] = Task.Run(async () =>
{
while (!done)
{
cache.TryGetValue($"key{random.Next(numEntries)}", out _);
await Task.Yield();
}
});
}

for (int i = 0; i < 1000; i++)
{
FillCache();

cache.Compact(1);
}

done = true;

Task.WaitAll(backgroundAccessTasks);
}
}
}