Skip to content
Open
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 @@ -25,6 +25,18 @@ public class HybridCacheOptions
/// </summary>
public bool DisableCompression { get; set; }

/// <summary>
/// Gets or sets a value indicating whether values stored in the local cache should be returned by reference
/// instead of using serialization round-trips to create defensive copies.
/// </summary>
/// <remarks>
/// When enabled, mutable values may be shared between callers and changes made to a cached value are visible
/// to subsequent callers. Values are still serialized when required for the distributed cache. Local cache
/// size accounting continues to use the serialized payload size and may not reflect the size of the retained
/// object graph.
/// </remarks>
public bool DisableLocalCacheSerialization { get; set; }

/// <summary>
/// Gets or sets the maximum size of cache items.
/// </summary>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -133,7 +133,9 @@ 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(long creationTimestamp, TagSet tags) => ImmutableTypeCache<T>.IsImmutable
? new ImmutableCacheItem<T>(creationTimestamp, tags) : new MutableCacheItem<T>(creationTimestamp, tags);
internal static CacheItem<T> Create(long creationTimestamp, TagSet tags, bool disableLocalCacheSerialization)
=> ImmutableTypeCache<T>.IsImmutable || disableLocalCacheSerialization
? new ImmutableCacheItem<T>(creationTimestamp, tags)
: new MutableCacheItem<T>(creationTimestamp, tags);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -31,13 +31,13 @@ internal void SetResultDirect(CacheItem<T> value)
=> _result?.TrySetResult(value);

public StampedeState(DefaultHybridCache cache, in StampedeKey key, TagSet tags, bool canBeCanceled)
: base(cache, key, CacheItem<T>.Create(cache.CurrentTimestamp(), tags), canBeCanceled)
: base(cache, key, CacheItem<T>.Create(cache.CurrentTimestamp(), tags, cache.Options.DisableLocalCacheSerialization), canBeCanceled)
{
_result = new(TaskCreationOptions.RunContinuationsAsynchronously);
}

public StampedeState(DefaultHybridCache cache, in StampedeKey key, TagSet tags, CancellationToken token)
: base(cache, key, CacheItem<T>.Create(cache.CurrentTimestamp(), tags), token)
: base(cache, key, CacheItem<T>.Create(cache.CurrentTimestamp(), tags, cache.Options.DisableLocalCacheSerialization), token)
{
// no TCS in this case - this is for SetValue only
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,10 @@
"Member": "bool Microsoft.Extensions.Caching.Hybrid.HybridCacheOptions.DisableCompression { get; set; }",
"Stage": "Stable"
},
{
"Member": "bool Microsoft.Extensions.Caching.Hybrid.HybridCacheOptions.DisableLocalCacheSerialization { get; set; }",
"Stage": "Stable"
},
{
"Member": "object? Microsoft.Extensions.Caching.Hybrid.HybridCacheOptions.DistributedCacheServiceKey { get; set; }",
"Stage": "Stable"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -243,7 +243,7 @@ private static bool Write(IBufferWriter<byte> destination, byte[]? buffer)
[Fact]
public void ImmutableCacheItem_Reservation()
{
var obj = Assert.IsType<ImmutableCacheItem<string>>(CacheItem<string>.Create(12345, TagSet.Empty));
var obj = Assert.IsType<ImmutableCacheItem<string>>(CacheItem<string>.Create(12345, TagSet.Empty, false));
Assert.True(obj.DebugIsImmutable);
obj.SetValue("abc", 3);
Assert.False(obj.TryReserveBuffer(out _));
Expand All @@ -258,7 +258,7 @@ public void MutableCacheItem_Reservation()
{
using ServiceProvider services = new ServiceCollection().BuildServiceProvider();

var obj = Assert.IsType<MutableCacheItem<Customer>>(CacheItem<Customer>.Create(12345, TagSet.Empty));
var obj = Assert.IsType<MutableCacheItem<Customer>>(CacheItem<Customer>.Create(12345, TagSet.Empty, false));

Assert.True(new DefaultJsonSerializerFactory(services).TryCreateSerializer<Customer>(out var serializer));
var target = RecyclableArrayBufferWriter<byte>.Create(int.MaxValue);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -32,13 +32,13 @@ private class Options<T>(T value) : IOptions<T>
T IOptions<T>.Value => value;
}

private ServiceProvider GetDefaultCache(bool buffers, out DefaultHybridCache cache)
private ServiceProvider GetDefaultCache(bool buffers, out DefaultHybridCache cache, bool disableLocalCacheSerialization = false)
{
var services = new ServiceCollection();
var localCacheOptions = new Options<MemoryDistributedCacheOptions>(new());
var localCache = new MemoryDistributedCache(localCacheOptions);
services.AddSingleton<IDistributedCache>(buffers ? new BufferLoggingCache(Log, localCache) : new LoggingCache(Log, localCache));
services.AddHybridCache();
services.AddHybridCache(options => options.DisableLocalCacheSerialization = disableLocalCacheSerialization);
ServiceProvider provider = services.BuildServiceProvider();
cache = Assert.IsType<DefaultHybridCache>(provider.GetRequiredService<HybridCache>());
return provider;
Expand Down Expand Up @@ -155,6 +155,54 @@ public async Task AssertL2Operations_Mutable(bool buffers)
Assert.Equal(12, backend.OpCount); // GET, SET
}

[Theory]
[InlineData(true)]
[InlineData(false)]
public async Task DisableLocalCacheSerialization_ReturnsSameMutableInstance(bool buffers)
{
using var provider = GetDefaultCache(buffers, out var cache, disableLocalCacheSerialization: true);
var backend = Assert.IsAssignableFrom<LoggingCache>(cache.BackendCache);

Foo original = await cache.GetOrCreateAsync(Me(), _ => new ValueTask<Foo>(new Foo { Value = "value" }));
Foo fromL1 = await cache.GetOrCreateAsync(Me(), _ => new ValueTask<Foo>(new Foo { Value = "unused" }));

Assert.Same(original, fromL1);

cache.LocalCache.Remove(Me());
Foo fromL2 = await cache.GetOrCreateAsync(Me(), _ => new ValueTask<Foo>(new Foo { Value = "unused" }));
Foo fromL1AfterL2 = await cache.GetOrCreateAsync(Me(), _ => new ValueTask<Foo>(new Foo { Value = "unused" }));

Assert.NotSame(original, fromL2);
Assert.Same(fromL2, fromL1AfterL2);
Assert.Equal(4, backend.OpCount); // wildcard timestamp GET, GET, SET, GET
}

[Fact]
public async Task DisableLocalCacheSerialization_DoesNotSerializeWhenCacheWritesAreDisabled()
{
var services = new ServiceCollection();
services.AddHybridCache(options => options.DisableLocalCacheSerialization = true);
services.AddSingleton<IHybridCacheSerializer<Foo>>(new ThrowingFooSerializer());
using ServiceProvider provider = services.BuildServiceProvider();
HybridCache cache = provider.GetRequiredService<HybridCache>();
var value = new Foo { Value = "value" };
var options = new HybridCacheEntryOptions
{
Flags = HybridCacheEntryFlags.DisableLocalCacheWrite | HybridCacheEntryFlags.DisableDistributedCacheWrite,
};

Foo actual = await cache.GetOrCreateAsync(Me(), _ => new ValueTask<Foo>(value), options);

Assert.Same(value, actual);
}

private sealed class ThrowingFooSerializer : IHybridCacheSerializer<Foo>
{
public Foo Deserialize(ReadOnlySequence<byte> source) => throw new NotSupportedException();

public void Serialize(Foo value, IBufferWriter<byte> target) => throw new NotSupportedException();
}

private class BufferLoggingCache : LoggingCache, IBufferDistributedCache
{
public BufferLoggingCache(ITestOutputHelper log, IDistributedCache tail)
Expand Down
Loading