Skip to content

Commit

Permalink
HybridCache : implement the tag expiration feature (#5785)
Browse files Browse the repository at this point in the history
* rebase hc-tags work from dev (easier to re-branch due to drift)

* detect malformed unicode in tags/keys

* avoid try/finally in unicode validation step

* make build happy

* happier

* normalize and test how per-entry vs global options are inherited

* add loggine of rejected data

* event log for tag invalidations

* make the CI overlord happy

* rebase hc-tags work from dev (easier to re-branch due to drift)

* detect malformed unicode in tags/keys

* avoid try/finally in unicode validation step

* make build happy

* happier

* normalize and test how per-entry vs global options are inherited

* add loggine of rejected data

* event log for tag invalidations

* make the CI overlord happy

* add event-source into more tests (proves logging-related code branches work)

* add inbuilt type serializer test

* add license header

* improving code coverage

* incorporate PR feedback re thread-static array

* make the robots happy

* Update src/Libraries/Microsoft.Extensions.Caching.Hybrid/Internal/TagSet.cs

Co-authored-by: Sébastien Ros <[email protected]>

* tag-based invalidate: pass multiple tags, to ensure code paths

* more coverage

---------

Co-authored-by: Sébastien Ros <[email protected]>
  • Loading branch information
mgravell and sebastienros authored Feb 4, 2025
1 parent ab94e0b commit bf3ac53
Show file tree
Hide file tree
Showing 34 changed files with 2,288 additions and 100 deletions.
2 changes: 1 addition & 1 deletion eng/MSBuild/LegacySupport.props
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@
<Compile Include="$(MSBuildThisFileDirectory)\..\..\src\LegacySupport\CallerAttributes\*.cs" LinkBase="LegacySupport\CallerAttributes" />
</ItemGroup>

<ItemGroup Condition="'$(InjectSkipLocalsInitAttributeOnLegacy)' == 'true' AND ('$(TargetFramework)' == 'net462' or '$(TargetFramework)' == 'netstandard2.0' or '$(TargetFramework)' == 'netcoreapp3.1')">
<ItemGroup Condition="'$(InjectSkipLocalsInitAttributeOnLegacy)' == 'true' AND ('$(TargetFramework)' == 'net462' or '$(TargetFramework)' == 'netstandard2.0' or '$(TargetFramework)' == 'netstandard2.1' or '$(TargetFramework)' == 'netcoreapp3.1')">
<Compile Include="$(MSBuildThisFileDirectory)\..\..\src\LegacySupport\SkipLocalsInitAttribute\*.cs" LinkBase="LegacySupport\SkipLocalsInitAttribute" />
</ItemGroup>

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,20 +15,23 @@ namespace Microsoft.Extensions.Caching.Hybrid.Internal;
internal readonly struct BufferChunk
{
private const int FlagReturnToPool = (1 << 31);

private readonly int _lengthAndPoolFlag;

public byte[]? Array { get; } // null for default
public byte[]? OversizedArray { get; } // null for default

public bool HasValue => OversizedArray is not null;

public int Offset { get; }
public int Length => _lengthAndPoolFlag & ~FlagReturnToPool;

public bool ReturnToPool => (_lengthAndPoolFlag & FlagReturnToPool) != 0;

public BufferChunk(byte[] array)
{
Debug.Assert(array is not null, "expected valid array input");
Array = array;
OversizedArray = array;
_lengthAndPoolFlag = array!.Length;
Offset = 0;

// assume not pooled, if exact-sized
// (we don't expect array.Length to be negative; we're really just saying
Expand All @@ -39,11 +42,12 @@ public BufferChunk(byte[] array)
Debug.Assert(Length == array.Length, "array length not respected");
}

public BufferChunk(byte[] array, int length, bool returnToPool)
public BufferChunk(byte[] array, int offset, int length, bool returnToPool)
{
Debug.Assert(array is not null, "expected valid array input");
Debug.Assert(length >= 0, "expected valid length");
Array = array;
OversizedArray = array;
Offset = offset;
_lengthAndPoolFlag = length | (returnToPool ? FlagReturnToPool : 0);
Debug.Assert(ReturnToPool == returnToPool, "return-to-pool not respected");
Debug.Assert(Length == length, "length not respected");
Expand All @@ -58,7 +62,7 @@ public byte[] ToArray()
}

var copy = new byte[length];
Buffer.BlockCopy(Array!, 0, copy, 0, length);
Buffer.BlockCopy(OversizedArray!, Offset, copy, 0, length);
return copy;

// Note on nullability of Array; the usage here is that a non-null array
Expand All @@ -73,15 +77,19 @@ internal void RecycleIfAppropriate()
{
if (ReturnToPool)
{
ArrayPool<byte>.Shared.Return(Array!);
ArrayPool<byte>.Shared.Return(OversizedArray!);
}

Unsafe.AsRef(in this) = default; // anti foot-shotgun double-return guard; not 100%, but worth doing
Debug.Assert(Array is null && !ReturnToPool, "expected clean slate after recycle");
Debug.Assert(OversizedArray is null && !ReturnToPool, "expected clean slate after recycle");
}

internal ArraySegment<byte> AsArraySegment() => Length == 0 ? default! : new(OversizedArray!, Offset, Length);

internal ReadOnlySpan<byte> AsSpan() => Length == 0 ? default : new(OversizedArray!, Offset, Length);

// get the data as a ROS; for note on null-logic of Array!, see comment in ToArray
internal ReadOnlySequence<byte> AsSequence() => Length == 0 ? default : new ReadOnlySequence<byte>(Array!, 0, Length);
internal ReadOnlySequence<byte> AsSequence() => Length == 0 ? default : new ReadOnlySequence<byte>(OversizedArray!, Offset, Length);

internal BufferChunk DoNotReturnToPool()
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@

using System;
using System.Diagnostics;
using System.Runtime.CompilerServices;
using System.Threading;
using Microsoft.Extensions.Caching.Memory;
using Microsoft.Extensions.Logging;
Expand All @@ -13,10 +14,22 @@ internal partial class DefaultHybridCache
{
internal abstract class CacheItem
{
private readonly long _creationTimestamp;

protected CacheItem(long creationTimestamp, TagSet tags)
{
Tags = tags;
_creationTimestamp = creationTimestamp;
}

private int _refCount = 1; // the number of pending operations against this cache item

public abstract bool DebugIsImmutable { get; }

public long CreationTimestamp => _creationTimestamp;

public TagSet Tags { get; }

// Note: the ref count is the number of callers anticipating this value at any given time. Initially,
// it is one for a simple "get the value" flow, but if another call joins with us, it'll be incremented.
// If either cancels, it will get decremented, with the entire flow being cancelled if it ever becomes
Expand All @@ -27,6 +40,9 @@ internal abstract class CacheItem

internal int RefCount => Volatile.Read(ref _refCount);

internal void UnsafeSetCreationTimestamp(long timestamp)
=> Unsafe.AsRef(in _creationTimestamp) = timestamp;

internal static readonly PostEvictionDelegate SharedOnEviction = static (key, value, reason, state) =>
{
if (value is CacheItem item)
Expand Down Expand Up @@ -88,6 +104,11 @@ protected virtual void OnFinalRelease() // any required release semantics

internal abstract class CacheItem<T> : CacheItem
{
protected CacheItem(long creationTimestamp, TagSet tags)
: base(creationTimestamp, tags)
{
}

public abstract bool TryGetSize(out long size);

// Attempt to get a value that was *not* previously reserved.
Expand All @@ -112,6 +133,7 @@ public T GetReservedValue(ILogger log)
static void Throw() => throw new ObjectDisposedException("The cache item has been recycled before the value was obtained");
}

internal static CacheItem<T> Create() => ImmutableTypeCache<T>.IsImmutable ? new ImmutableCacheItem<T>() : new MutableCacheItem<T>();
internal static CacheItem<T> Create(long creationTimestamp, TagSet tags) => ImmutableTypeCache<T>.IsImmutable
? new ImmutableCacheItem<T>(creationTimestamp, tags) : new MutableCacheItem<T>(creationTimestamp, tags);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -47,14 +47,13 @@ internal void DebugOnlyIncrementOutstandingBuffers()
}
#endif

private partial class MutableCacheItem<T>
internal partial class MutableCacheItem<T>
{
#if DEBUG
private DefaultHybridCache? _cache; // for buffer-tracking - only needed in DEBUG
#endif

[Conditional("DEBUG")]
[SuppressMessage("Performance", "CA1822:Mark members as static", Justification = "Instance state used in debug")]
internal void DebugOnlyTrackBuffer(DefaultHybridCache cache)
{
#if DEBUG
Expand All @@ -63,18 +62,21 @@ internal void DebugOnlyTrackBuffer(DefaultHybridCache cache)
{
_cache?.DebugOnlyIncrementOutstandingBuffers();
}
#else
_ = this; // dummy just to prevent CA1822, never hit
#endif
}

[Conditional("DEBUG")]
[SuppressMessage("Performance", "CA1822:Mark members as static", Justification = "Instance state used in debug")]
private void DebugOnlyDecrementOutstandingBuffers()
{
#if DEBUG
if (_buffer.ReturnToPool)
{
_cache?.DebugOnlyDecrementOutstandingBuffers();
}
#else
_ = this; // dummy just to prevent CA1822, never hit
#endif
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,10 +8,15 @@ namespace Microsoft.Extensions.Caching.Hybrid.Internal;

internal partial class DefaultHybridCache
{
private sealed class ImmutableCacheItem<T> : CacheItem<T> // used to hold types that do not require defensive copies
internal sealed class ImmutableCacheItem<T> : CacheItem<T> // used to hold types that do not require defensive copies
{
private static ImmutableCacheItem<T>? _sharedDefault;

public ImmutableCacheItem(long creationTimestamp, TagSet tags)
: base(creationTimestamp, tags)
{
}

private T _value = default!; // deferred until SetValue

public long Size { get; private set; } = -1;
Expand All @@ -25,7 +30,7 @@ public static ImmutableCacheItem<T> GetReservedShared()
ImmutableCacheItem<T>? obj = Volatile.Read(ref _sharedDefault);
if (obj is null || !obj.TryReserve())
{
obj = new();
obj = new(0, TagSet.Empty); // timestamp doesn't matter - not used in L1/L2
_ = obj.TryReserve(); // this is reliable on a new instance
Volatile.Write(ref _sharedDefault, obj);
}
Expand Down
Loading

0 comments on commit bf3ac53

Please sign in to comment.