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

Implement minimal implementation of HybridCache #55147

Merged
merged 80 commits into from
Apr 19, 2024
Merged
Show file tree
Hide file tree
Changes from 59 commits
Commits
Show all changes
80 commits
Select commit Hold shift + click to select a range
35e9cea
initial API cut (post review)
mgravell Apr 10, 2024
12b3a0e
basic API test
mgravell Apr 11, 2024
c5d28d4
prove that the API can be configured
mgravell Apr 11, 2024
20e1cb2
demonstrate serializer/factory configuration working
mgravell Apr 11, 2024
4379647
move to NuGet only to make the build happier
mgravell Apr 11, 2024
d382a7a
defer on trimming
mgravell Apr 11, 2024
e8bc9a9
PR review comments
mgravell Apr 11, 2024
7edddb1
tyop
mgravell Apr 11, 2024
dc622a8
Update src/Caching/Hybrid/src/Runtime/IsExternalInit.cs
mgravell Apr 11, 2024
4920182
return a leased array on netfx
mgravell Apr 11, 2024
8841e97
prefer ForEach to Walk
mgravell Apr 11, 2024
6cbe02e
Update src/Caching/Hybrid/src/Internal/InbuiltTypeSerializer.cs
mgravell Apr 11, 2024
0c0d589
Update src/Caching/Hybrid/src/Internal/DefaultJsonSerializerFactory.cs
mgravell Apr 11, 2024
ef0ad56
Update src/Caching/Hybrid/src/Internal/InbuiltTypeSerializer.cs
mgravell Apr 11, 2024
e28f316
comment nits
mgravell Apr 11, 2024
fbedef4
Merge branch 'marc/hybrid-api' of https://github.com/dotnet/aspnetcor…
mgravell Apr 11, 2024
b49151e
use TimeProvider throughout
mgravell Apr 11, 2024
c2326fd
more nits
mgravell Apr 11, 2024
29dcc2e
regen projects list
mgravell Apr 11, 2024
4ddba7f
TryAdd
mgravell Apr 12, 2024
427601c
clarify intent of null on Remove{Tags|Keys}Async
mgravell Apr 12, 2024
cef964f
basic stampede infrastructure
mgravell Apr 12, 2024
6f572f3
streamline the non-canceled scenario
mgravell Apr 15, 2024
a41175c
L2
mgravell Apr 15, 2024
82a34e3
L1
mgravell Apr 15, 2024
e3a9173
L2 tests
mgravell Apr 15, 2024
373aaa8
mutable/immutable tests
mgravell Apr 15, 2024
8c1cc9e
build warnings
mgravell Apr 15, 2024
40e2c47
implement SetValueAsync
mgravell Apr 15, 2024
cede0e9
support ns2.1
mgravell Apr 15, 2024
ad88ef1
prove immutable type behaviours
mgravell Apr 16, 2024
e427ad9
TFM summary
mgravell Apr 16, 2024
b9bc68a
make L2 optional; fast-path L2
mgravell Apr 16, 2024
1d548bf
redis tests
mgravell Apr 16, 2024
18ae584
implement streaming via SqlClient
mgravell Apr 16, 2024
e60099b
add benchmark project
mgravell Apr 17, 2024
5421398
nit
mgravell Apr 17, 2024
c3adf74
fixes
mgravell Apr 17, 2024
0ac4dae
nit
mgravell Apr 17, 2024
1db2758
Update src/Caching/Hybrid/src/Runtime/HybridCache.cs
mgravell Apr 17, 2024
2945ce0
Update src/Caching/Hybrid/src/Runtime/HybridCache.cs
mgravell Apr 17, 2024
94d9fe1
Update src/Caching/Hybrid/src/Runtime/HybridCache.cs
mgravell Apr 17, 2024
a13e1d3
Update src/Caching/Hybrid/src/Runtime/HybridCache.cs
mgravell Apr 17, 2024
2a77920
Update src/Caching/Hybrid/src/Runtime/HybridCache.cs
mgravell Apr 17, 2024
cee8ddc
Update src/Caching/Hybrid/src/Runtime/HybridCache.cs
mgravell Apr 17, 2024
7989f16
Update src/Caching/Hybrid/src/Runtime/HybridCacheEntryFlags.cs
mgravell Apr 17, 2024
49a477f
Update src/Caching/Hybrid/src/Runtime/HybridCache.cs
mgravell Apr 17, 2024
5f8bcb8
Update src/Caching/Hybrid/src/Runtime/HybridCacheEntryOptions.cs
mgravell Apr 17, 2024
7b32e0f
Update src/Caching/Hybrid/src/Runtime/HybridCache.cs
mgravell Apr 17, 2024
62b4e6f
Update src/Caching/Hybrid/src/Runtime/IBufferDistributedCache.cs
mgravell Apr 17, 2024
c6cdeae
Apply suggestions from code review
mgravell Apr 17, 2024
d624726
more PR feedback
mgravell Apr 17, 2024
e831198
Merge branch 'marc/hybrid-api' into marc/hybrid-api-basic
mgravell Apr 17, 2024
83b7a1d
apply _ field convention
mgravell Apr 17, 2024
59bc62a
nit comment
mgravell Apr 17, 2024
0e36776
add tests for advanced buffer scenarios for redis+sql
mgravell Apr 17, 2024
ee15beb
Empty-Commit
mgravell Apr 17, 2024
eb7a294
Merge branch 'main' into marc/hybrid-api-basic
mgravell Apr 17, 2024
ffa0e0a
hybrid cache baseline perf
mgravell Apr 17, 2024
1b12e1a
move benchmarks ala PR feedback
mgravell Apr 18, 2024
3e42524
include max allowed payload size in fault
mgravell Apr 18, 2024
8c0e1b5
add comment on the serializer cache
mgravell Apr 18, 2024
6c10b8d
include type data in the wrong-type message
mgravell Apr 18, 2024
552676f
constify
mgravell Apr 18, 2024
bb1f856
missing lic headers
mgravell Apr 18, 2024
6a25601
use standard Try* pattern
mgravell Apr 18, 2024
17383c4
redundant ?.
mgravell Apr 18, 2024
14bb5fc
TCS: async completions
mgravell Apr 18, 2024
e6b2cca
use double-checked lock to avoid race conditions creating two concurr…
mgravell Apr 18, 2024
96d75fa
move state above methods; use NullLogger
mgravell Apr 18, 2024
366bd82
Update src/Caching/Hybrid/src/Internal/DefaultHybridCache.StampedeSta…
mgravell Apr 18, 2024
256581b
add proper buffer recycling of buffers inside cache items
mgravell Apr 18, 2024
45d31b3
Merge branch 'marc/hybrid-api-basic' of https://github.com/dotnet/asp…
mgravell Apr 18, 2024
3de225e
prefer HashCode when possible
mgravell Apr 18, 2024
c796f52
Merge branch 'main' into marc/hybrid-api-basic
mgravell Apr 18, 2024
ce7ec84
fix bad merge (need longer name for disambiguation)
mgravell Apr 18, 2024
0489dce
look into CI failure; timing
mgravell Apr 19, 2024
e5a080f
nits
mgravell Apr 19, 2024
de2a4b0
disposable support, and fix IDE analyzer nits
mgravell Apr 19, 2024
d4133d1
remove IOptions<T> for now
mgravell Apr 19, 2024
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
22 changes: 22 additions & 0 deletions AspNetCore.sln
Original file line number Diff line number Diff line change
Expand Up @@ -1794,6 +1794,10 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.Extensions.Cachin
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.Extensions.Caching.Hybrid.Tests", "src\Caching\Hybrid\test\Microsoft.Extensions.Caching.Hybrid.Tests.csproj", "{CF63C942-895A-4F6B-888A-7653D7C4991A}"
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Benchmarks", "Benchmarks", "{6469F11E-8CEE-4292-820B-324DFFC88EBC}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.Extensions.Caching.Benchmarks", "src\Caching\perf\Microsoft.Extensions.Caching.Benchmarks\Microsoft.Extensions.Caching.Benchmarks.csproj", "{268CF55F-94A8-4F87-9482-D5B755CFA79C}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Expand Down Expand Up @@ -10827,6 +10831,22 @@ Global
{CF63C942-895A-4F6B-888A-7653D7C4991A}.Release|x64.Build.0 = Release|Any CPU
{CF63C942-895A-4F6B-888A-7653D7C4991A}.Release|x86.ActiveCfg = Release|Any CPU
{CF63C942-895A-4F6B-888A-7653D7C4991A}.Release|x86.Build.0 = Release|Any CPU
{268CF55F-94A8-4F87-9482-D5B755CFA79C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{268CF55F-94A8-4F87-9482-D5B755CFA79C}.Debug|Any CPU.Build.0 = Debug|Any CPU
{268CF55F-94A8-4F87-9482-D5B755CFA79C}.Debug|arm64.ActiveCfg = Debug|Any CPU
{268CF55F-94A8-4F87-9482-D5B755CFA79C}.Debug|arm64.Build.0 = Debug|Any CPU
{268CF55F-94A8-4F87-9482-D5B755CFA79C}.Debug|x64.ActiveCfg = Debug|Any CPU
{268CF55F-94A8-4F87-9482-D5B755CFA79C}.Debug|x64.Build.0 = Debug|Any CPU
{268CF55F-94A8-4F87-9482-D5B755CFA79C}.Debug|x86.ActiveCfg = Debug|Any CPU
{268CF55F-94A8-4F87-9482-D5B755CFA79C}.Debug|x86.Build.0 = Debug|Any CPU
{268CF55F-94A8-4F87-9482-D5B755CFA79C}.Release|Any CPU.ActiveCfg = Release|Any CPU
{268CF55F-94A8-4F87-9482-D5B755CFA79C}.Release|Any CPU.Build.0 = Release|Any CPU
{268CF55F-94A8-4F87-9482-D5B755CFA79C}.Release|arm64.ActiveCfg = Release|Any CPU
{268CF55F-94A8-4F87-9482-D5B755CFA79C}.Release|arm64.Build.0 = Release|Any CPU
{268CF55F-94A8-4F87-9482-D5B755CFA79C}.Release|x64.ActiveCfg = Release|Any CPU
{268CF55F-94A8-4F87-9482-D5B755CFA79C}.Release|x64.Build.0 = Release|Any CPU
{268CF55F-94A8-4F87-9482-D5B755CFA79C}.Release|x86.ActiveCfg = Release|Any CPU
{268CF55F-94A8-4F87-9482-D5B755CFA79C}.Release|x86.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
Expand Down Expand Up @@ -11713,6 +11733,8 @@ Global
{2D64CA23-6E81-488E-A7D3-9BDF87240098} = {0F39820F-F4A5-41C6-9809-D79B68F032EF}
{2B60E6D3-9E7C-427A-AD4E-BBE9A6D935B9} = {2D64CA23-6E81-488E-A7D3-9BDF87240098}
{CF63C942-895A-4F6B-888A-7653D7C4991A} = {2D64CA23-6E81-488E-A7D3-9BDF87240098}
{6469F11E-8CEE-4292-820B-324DFFC88EBC} = {0F39820F-F4A5-41C6-9809-D79B68F032EF}
{268CF55F-94A8-4F87-9482-D5B755CFA79C} = {6469F11E-8CEE-4292-820B-324DFFC88EBC}
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {3E8720B3-DBDD-498C-B383-2CC32A054E8F}
Expand Down
1 change: 1 addition & 0 deletions src/Caching/Caching.slnf
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
"src\\Caching\\SqlServer\\test\\Microsoft.Extensions.Caching.SqlServer.Tests.csproj",
"src\\Caching\\StackExchangeRedis\\src\\Microsoft.Extensions.Caching.StackExchangeRedis.csproj",
"src\\Caching\\StackExchangeRedis\\test\\Microsoft.Extensions.Caching.StackExchangeRedis.Tests.csproj",
"src\\Caching\\perf\\Microsoft.Extensions.Caching.Benchmarks\\Microsoft.Extensions.Caching.Benchmarks.csproj",
"src\\Middleware\\OutputCaching\\src\\Microsoft.AspNetCore.OutputCaching.csproj"
]
}
Expand Down
5 changes: 4 additions & 1 deletion src/Caching/Hybrid/src/HybridCacheOptions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -6,13 +6,14 @@
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using Microsoft.Extensions.Options;

namespace Microsoft.Extensions.Caching.Hybrid;

/// <summary>
/// Options for configuring the default <see cref="HybridCache"/> implementation.
/// </summary>
public class HybridCacheOptions
public class HybridCacheOptions : IOptions<HybridCacheOptions>
Copy link
Member

Choose a reason for hiding this comment

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

Why? Technically API review needed.

Copy link
Member Author

@mgravell mgravell Apr 18, 2024

Choose a reason for hiding this comment

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

great question; this was mostly a reaction to comparisons to the related ones I found while working on this:

public class MemoryCacheOptions : IOptions<MemoryCacheOptions> {...}
public class SqlServerCacheOptions : IOptions<SqlServerCacheOptions> {...}
public class RedisCacheOptions : IOptions<RedisCacheOptions> {...}

I assumed, therefore, that this was intentional and desirable; if I'm missing something, I'm all ears. It makes no difference to me whether we have this - I was simply trying to be consistent with convention

Copy link
Member Author

Choose a reason for hiding this comment

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

@BrennanConroy disabled for now (d4133d1) - any opinions on whether we should implement this?

Copy link
Member

Choose a reason for hiding this comment

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

I don't know what it adds. Like you pointed out we've done it in some places. But I wouldn't call it the convention, there are a lot more options objects without it. We should probably figure out what it does before adding it.

{
/// <summary>
/// Default global options to be applied to <see cref="HybridCache"/> operations; if options are
Expand Down Expand Up @@ -45,4 +46,6 @@ public class HybridCacheOptions
/// tags do not contain data that should not be visible in metrics systems.
/// </summary>
public bool ReportTagMetrics { get; set; }

HybridCacheOptions IOptions<HybridCacheOptions>.Value => this;
}
1 change: 0 additions & 1 deletion src/Caching/Hybrid/src/HybridCacheServiceExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,6 @@ public static IHybridCacheBuilder AddHybridCache(this IServiceCollection service
services.TryAddSingleton(TimeProvider.System);
services.AddOptions();
services.AddMemoryCache();
services.AddDistributedMemoryCache(); // we need a backend; use in-proc by default
services.TryAddSingleton<IHybridCacheSerializerFactory, DefaultJsonSerializerFactory>();
services.TryAddSingleton<IHybridCacheSerializer<string>>(InbuiltTypeSerializer.Instance);
services.TryAddSingleton<IHybridCacheSerializer<byte[]>>(InbuiltTypeSerializer.Instance);
Expand Down
14 changes: 14 additions & 0 deletions src/Caching/Hybrid/src/Internal/DefaultHybridCache.CacheItem.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

namespace Microsoft.Extensions.Caching.Hybrid.Internal;

partial class DefaultHybridCache
{
internal abstract class CacheItem<T>
{
public abstract T GetValue();

public abstract byte[]? TryGetBytes(out int length);
mgravell marked this conversation as resolved.
Show resolved Hide resolved
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

namespace Microsoft.Extensions.Caching.Hybrid.Internal;

partial class DefaultHybridCache
{
private sealed class ImmutableCacheItem<T>(T value) : CacheItem<T> // used to hold types that do not require defensive copies
{
private static ImmutableCacheItem<T>? sharedDefault;
public static ImmutableCacheItem<T> Default => sharedDefault ??= new(default!); // this is only used when the underlying store is disabled

public override T GetValue() => value;

public override byte[]? TryGetBytes(out int length)
{
length = 0;
return null;
}
}
}
110 changes: 110 additions & 0 deletions src/Caching/Hybrid/src/Internal/DefaultHybridCache.L2.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
using System;
mgravell marked this conversation as resolved.
Show resolved Hide resolved
using System.Diagnostics;
using System.Runtime.CompilerServices;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Extensions.Caching.Distributed;
using Microsoft.Extensions.Caching.Memory;

namespace Microsoft.Extensions.Caching.Hybrid.Internal;

partial class DefaultHybridCache
{
internal ValueTask<ArraySegment<byte>> GetFromL2Async(string key, CancellationToken token)
{
switch (GetFeatures(CacheFeatures.BackendCache | CacheFeatures.BackendBuffers))
{
case CacheFeatures.BackendCache: // legacy byte[]-based
var pendingLegacy = _backendCache!.GetAsync(key, token);
#if NETCOREAPP2_0_OR_GREATER || NETSTANDARD2_1_OR_GREATER
if (!pendingLegacy.IsCompletedSuccessfully)
#else
if (pendingLegacy.Status != TaskStatus.RanToCompletion)
#endif
{
return new(AwaitedLegacy(pendingLegacy, MaximumPayloadBytes));
}
var bytes = pendingLegacy.Result; // already complete
if (bytes is not null)
{
if (bytes.Length > MaximumPayloadBytes)
{
ThrowQuota();
}
return new(new ArraySegment<byte>(bytes));
}
break;
case CacheFeatures.BackendCache | CacheFeatures.BackendBuffers: // IBufferWriter<byte>-based
var writer = RecyclableArrayBufferWriter<byte>.Create(MaximumPayloadBytes);
var cache = Unsafe.As<IBufferDistributedCache>(_backendCache!); // type-checked already
var pendingBuffers = cache.TryGetAsync(key, writer, token);
if (!pendingBuffers.IsCompletedSuccessfully)
{
return new(AwaitedBuffers(pendingBuffers, writer));
}
ArraySegment<byte> result = pendingBuffers.GetAwaiter().GetResult()
? new(writer.DetachCommitted(out var length), 0, length)
Copy link
Member

Choose a reason for hiding this comment

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

Isn't this starving the array pool? The caller isn't returning the buffers to the pool.

Copy link
Member Author

Choose a reason for hiding this comment

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

fair; fixed by adding a little extra (but necessary) complexity - we're now hooking the eviction notification on relevant cache entries, to add a "you don't need this buffer any more" signal; this has to be combined (interlocked) with people currently reading the buffer in-situ for defensive deserialize; long story short: this now works, including buffer-counting tests (256581b)

: default;
writer.Dispose(); // it is not accidental that this isn't "using"; avoid recycling if not 100% sure what happened
return new(result);
}
return default;

static async Task<ArraySegment<byte>> AwaitedLegacy(Task<byte[]?> pending, int maximumPayloadBytes)
{
var bytes = await pending.ConfigureAwait(false);
if (bytes is not null)
{
if (bytes.Length > maximumPayloadBytes)
{
ThrowQuota();
}
return new(bytes);
}
return default;
}

static async Task<ArraySegment<byte>> AwaitedBuffers(ValueTask<bool> pending, RecyclableArrayBufferWriter<byte> writer)
{
ArraySegment<byte> result = await pending.ConfigureAwait(false)
? new(writer.DetachCommitted(out var length), 0, length)
: default;
writer.Dispose(); // it is not accidental that this isn't "using"; avoid recycling if not 100% sure what happened
return result;
}

static void ThrowQuota() => throw new InvalidOperationException("Maximum cache length exceeded");
mgravell marked this conversation as resolved.
Show resolved Hide resolved
mgravell marked this conversation as resolved.
Show resolved Hide resolved
}

internal ValueTask SetL2Async(string key, byte[] value, int length, HybridCacheEntryOptions? options, CancellationToken token)
{
Debug.Assert(value.Length >= length);
mgravell marked this conversation as resolved.
Show resolved Hide resolved
switch (GetFeatures(CacheFeatures.BackendCache | CacheFeatures.BackendBuffers))
{
case CacheFeatures.BackendCache: // legacy byte[]-based
if (value.Length > length)
{
Array.Resize(ref value, length);
}
Debug.Assert(value.Length == length);
return new(_backendCache!.SetAsync(key, value, GetOptions(options), token));
case CacheFeatures.BackendCache | CacheFeatures.BackendBuffers: // ReadOnlySequence<byte>-based
var cache = Unsafe.As<IBufferDistributedCache>(_backendCache!); // type-checked already
return cache.SetAsync(key, new(value, 0, length), GetOptions(options), token);
}
return default;
}

private DistributedCacheEntryOptions GetOptions(HybridCacheEntryOptions? options)
{
DistributedCacheEntryOptions? result = null;
if (options is not null && options.Expiration.HasValue && options.Expiration.GetValueOrDefault() != _defaultExpiration)
{
result = options.ToDistributedCacheEntryOptions();
}
return result ?? _defaultDistributedCacheExpiration;
Copy link
Member

Choose a reason for hiding this comment

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

Super nit: I like the use of partial classes but perhaps defaults like these should be centralized into a constants class so it's easier to grok their values?

Copy link
Member Author

Choose a reason for hiding this comment

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

which constant(s) do you mean? the only constant I can see there is 0, and all of the fields are readonly, so: I'm not entirely clear which you mean

}

internal void SetL1<T>(string key, CacheItem<T> value, HybridCacheEntryOptions? options)
=> _localCache.Set(key, value, options?.LocalCacheExpiration ?? _defaultLocalCacheExpiration);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

using System.Buffers;

namespace Microsoft.Extensions.Caching.Hybrid.Internal;

partial class DefaultHybridCache
{
private sealed class MutableCacheItem<T> : CacheItem<T> // used to hold types that require defensive copies
{
public MutableCacheItem(byte[] bytes, int length, IHybridCacheSerializer<T> serializer)
{
_serializer = serializer;
_bytes = bytes;
_length = length;
}

public MutableCacheItem(T value, IHybridCacheSerializer<T> serializer, int maxLength)
{
_serializer = serializer;
var writer = RecyclableArrayBufferWriter<byte>.Create(maxLength);
serializer.Serialize(value, writer);
_bytes = writer.DetachCommitted(out _length);
Copy link
Member

Choose a reason for hiding this comment

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

More array pool starvation?

writer.Dispose(); // only recycle on success
}

private readonly IHybridCacheSerializer<T> _serializer;
mgravell marked this conversation as resolved.
Show resolved Hide resolved
private readonly byte[] _bytes;
private readonly int _length;

public override T GetValue() => _serializer.Deserialize(new ReadOnlySequence<byte>(_bytes, 0, _length));

public override byte[]? TryGetBytes(out int length)
{
length = _length;
return _bytes;
}
}
}
108 changes: 108 additions & 0 deletions src/Caching/Hybrid/src/Internal/DefaultHybridCache.Serialization.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

using System;
using System.Collections.Concurrent;
using System.ComponentModel;
using System.Linq;
using System.Reflection;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
using System.Runtime.Serialization;
using Microsoft.Extensions.DependencyInjection;

namespace Microsoft.Extensions.Caching.Hybrid.Internal;
partial class DefaultHybridCache
{
private readonly ConcurrentDictionary<Type, object> _serializers = new(); // per instance cache of typed serializers
Copy link
Member

Choose a reason for hiding this comment

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

Why object and not some serialize interface or base type?

Copy link
Member Author

Choose a reason for hiding this comment

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

the serializer API itself is typed, i.e. IHybridCacheSerializer<T> - there is no base; we could add one, i.e.

+ public interface IHybridCacheSerializer {}
+ public interface IHybridCacheSerializer<T> : IHybridCacheSerializer
- public interface IHybridCacheSerializer<T>
  { ... }

but... if that is only for this, it seems artificial; added 8c0e1b5 instead


internal int MaximumPayloadBytes { get; }

internal IHybridCacheSerializer<T> GetSerializer<T>()
{
return _serializers.TryGetValue(typeof(T), out var serializer)
? Unsafe.As<IHybridCacheSerializer<T>>(serializer) : ResolveAndAddSerializer(this);

static IHybridCacheSerializer<T> ResolveAndAddSerializer(DefaultHybridCache @this)
{
// it isn't critical that we get only one serializer instance during start-up; what matters
// is that we don't get a new serializer instance *every time*
var serializer = @this._services.GetService<IHybridCacheSerializer<T>>();
if (serializer is null)
{
foreach (var factory in @this._serializerFactories)
{
if (factory.TryCreateSerializer<T>(out var current))
{
serializer = current;
break; // we've already reversed the factories, so: the first hit is what we want
}
}
}
if (serializer is null)
{
throw new InvalidOperationException($"No {nameof(IHybridCacheSerializer<T>)} configured for type '{typeof(T).Name}'");
}
// store the result so we don't repeat this in future
@this._serializers[typeof(T)] = serializer;
return serializer;
}
}

internal static class ImmutableTypeCache<T> // lazy memoize; T doesn't change per cache instance
{
// note for blittable types: a pure struct will be a full copy every time - nothing shared to mutate
public static readonly bool IsImmutable = (typeof(T).IsValueType && IsBlittable<T>()) || IsImmutable(typeof(T));
}

private static bool IsBlittable<T>() // minimize the generic portion
{
#if NETCOREAPP2_0_OR_GREATER || NETSTANDARD2_1_OR_GREATER
return !RuntimeHelpers.IsReferenceOrContainsReferences<T>();
#else
try // down-level: only blittable types can be pinned
{
// get a typed, zeroed, non-null boxed instance of the appropriate type
// (can't use (object)default(T), as that would box to null for nullable types)
var obj = FormatterServices.GetUninitializedObject(Nullable.GetUnderlyingType(typeof(T)) ?? typeof(T));
GCHandle.Alloc(obj, GCHandleType.Pinned).Free();
return true;
}
catch
{
return false;
}
#endif
}

private static bool IsImmutable(Type type)
{
// check for known types
if (type == typeof(string))
{
return true;
}

if (type.IsValueType)
{
// switch from Foo? to Foo if necessary
if (Nullable.GetUnderlyingType(type) is { } nullable)
{
type = nullable;
}
}

if (type.IsValueType || (type.IsClass & type.IsSealed))
{
// check for [ImmutableObject(true)]; note we're looking at this as a statement about
// the overall nullability; for example, a type could contain a private int[] field,
// where the field is mutable and the list is mutable; but if the type is annotated:
// we're trusting that the API and use-case is such that the type is immutable
return type.GetCustomAttribute<ImmutableObjectAttribute>() is { Immutable: true };
}
// don't trust interfaces and non-sealed types; we might have any concrete
// type that has different behaviour
Copy link
Member

Choose a reason for hiding this comment

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

Gallingly, Microsoft's standard spelling doesn't include a 'u'. It doesn't matter in this private context, but I do it anyway to try to make it habitual.

return false;

}
}
Loading
Loading