This commit is contained in:
Marc Gravell 2024-07-08 14:59:11 +01:00
Родитель db96936619
Коммит ee1b427019
3 изменённых файлов: 205 добавлений и 0 удалений

Просмотреть файл

@ -111,8 +111,40 @@ internal sealed partial class DefaultHybridCache : HybridCache
private HybridCacheEntryFlags GetEffectiveFlags(HybridCacheEntryOptions? options)
=> (options?.Flags | _hardFlags) ?? _defaultFlags;
private static void Validate(string key, IReadOnlyCollection<string>? tags)
{
ValidateKeyOrTag(key, nameof(key));
if (tags is not null)
{
foreach (var tag in tags)
{
ValidateKeyOrTag(tag, nameof(tag));
}
}
}
[System.Diagnostics.CodeAnalysis.SuppressMessage("Usage", "CA2249:Consider using 'string.Contains' instead of 'string.IndexOf'", Justification = "Multi-targeting makes impractical")]
private static void ValidateKeyOrTag(string value, string paramName)
{
if (string.IsNullOrWhiteSpace(value))
{
ThrowNullOrWhiteSpace(paramName);
}
if (value.IndexOf('\0') >= 0)
{
ThrowInvalid(paramName);
}
static void ThrowNullOrWhiteSpace(string paramName)
=> throw new ArgumentException("Value must be non-empty", paramName);
static void ThrowInvalid(string paramName)
=> throw new ArgumentException("Value contains invalid token", paramName);
}
public override ValueTask<T> GetOrCreateAsync<TState, T>(string key, TState state, Func<TState, CancellationToken, ValueTask<T>> underlyingDataCallback, HybridCacheEntryOptions? options = null, IReadOnlyCollection<string>? tags = null, CancellationToken token = default)
{
Validate(key, tags);
var canBeCanceled = token.CanBeCanceled;
if (canBeCanceled)
{
@ -149,12 +181,16 @@ internal sealed partial class DefaultHybridCache : HybridCache
public override ValueTask RemoveKeyAsync(string key, CancellationToken token = default)
{
ValidateKeyOrTag(key, nameof(key));
_localCache.Remove(key);
return _backendCache is null ? default : new(_backendCache.RemoveAsync(key, token));
}
public override ValueTask SetAsync<T>(string key, T value, HybridCacheEntryOptions? options = null, IReadOnlyCollection<string>? tags = null, CancellationToken token = default)
{
Validate(key, tags);
// since we're forcing a write: disable L1+L2 read; we'll use a direct pass-thru of the value as the callback, to reuse all the code;
// note also that stampede token is not shared with anyone else
var flags = GetEffectiveFlags(options) | (HybridCacheEntryFlags.DisableLocalCacheRead | HybridCacheEntryFlags.DisableDistributedCacheRead);

Просмотреть файл

@ -0,0 +1,109 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
using Microsoft.Extensions.Caching.Hybrid.Internal;
using Xunit.Abstractions;
namespace Microsoft.Extensions.Caching.Hybrid.Tests;
public class FrameTests(ITestOutputHelper Log)
{
[Fact]
public void BasicFrameRoundTrip()
{
DefaultHybridCache.PayloadHeader header = new(42, 1000000, 99, 12345, "some key", []);
using var writer = RecyclableArrayBufferWriter<byte>.Create(100);
header.Write(writer);
var original = writer.GetCommittedMemory().Span;
var remaining = original;
Assert.True(DefaultHybridCache.PayloadHeader.TryParse(ref remaining, out var parsed));
Assert.Equal(42, parsed.Flags);
Assert.Equal(1000000UL, parsed.EntropyAndCreationTime);
Assert.Equal(99U, parsed.PayloadSize);
Assert.Equal(12345UL, parsed.TTL);
Assert.Equal("some key", parsed.Key);
Assert.Empty(parsed.Tags);
Assert.True(remaining.IsEmpty);
var hex = BitConverter.ToString(writer.GetBuffer(out int length), 0, length);
Log.WriteLine(hex);
Assert.Equal("03-01-2A-00-40-42-0F-00-00-00-00-00-63-00-00-00-39-30-00-00-00-00-10-73-6F-6D-65-20-6B-65-79", hex);
// 03-01 sentinel+version
// 2A-00 flags
// 40-42-0F-00-00-00-00-00 entropy+creationtime
// 63-00-00-00 payload size
// 39-30-00-00-00 ttl
// 00 tag count
// 10 key length + marker
// 73-6F-6D-65-20-6B-65-79 key
}
[Fact]
public void FrameRoundTripWithTags()
{
DefaultHybridCache.PayloadHeader header = new(42, 1000000, 99, 12345, "some key", ["abc", "def"]);
using var writer = RecyclableArrayBufferWriter<byte>.Create(100);
header.Write(writer);
var original = writer.GetCommittedMemory().Span;
var remaining = original;
Assert.True(DefaultHybridCache.PayloadHeader.TryParse(ref remaining, out var parsed));
Assert.Equal(42, parsed.Flags);
Assert.Equal(1000000UL, parsed.EntropyAndCreationTime);
Assert.Equal(99U, parsed.PayloadSize);
Assert.Equal(12345UL, parsed.TTL);
Assert.Equal("some key", parsed.Key);
Assert.Equal(["abc", "def"], parsed.Tags);
Assert.True(remaining.IsEmpty);
var hex = BitConverter.ToString(writer.GetBuffer(out int length), 0, length);
Log.WriteLine(hex);
Assert.Equal("03-01-2A-00-40-42-0F-00-00-00-00-00-63-00-00-00-39-30-00-00-00-02-10-73-6F-6D-65-20-6B-65-79-06-61-62-63-06-64-65-66", hex);
// 03-01 sentinel+version
// 2A-00 flags
// 40-42-0F-00-00-00-00-00 entropy+creationtime
// 63-00-00-00 payload size
// 39-30-00-00-00 ttl
// 02 tag count
// 10 key length + marker
// 73-6F-6D-65-20-6B-65-79 key
// 06-61-62-63 "abc"
// 06-64-65-66 "def"
}
[Fact]
public void FrameRoundTripWithLongKey()
{
const string ALPHABET = "abcdefghijklmnopqrstuvwxyz";
var key = ALPHABET + ALPHABET + ALPHABET + ALPHABET + ALPHABET + ALPHABET;
Assert.True(key.Length > 130);
DefaultHybridCache.PayloadHeader header = new(42, 1000000, 99, 12345, key, []);
using var writer = RecyclableArrayBufferWriter<byte>.Create(1000);
header.Write(writer);
var original = writer.GetCommittedMemory().Span;
var remaining = original;
Assert.True(DefaultHybridCache.PayloadHeader.TryParse(ref remaining, out var parsed));
Assert.Equal(42, parsed.Flags);
Assert.Equal(1000000UL, parsed.EntropyAndCreationTime);
Assert.Equal(99U, parsed.PayloadSize);
Assert.Equal(12345UL, parsed.TTL);
Assert.Equal(key, parsed.Key);
Assert.Empty(parsed.Tags);
Assert.True(remaining.IsEmpty);
var hex = BitConverter.ToString(writer.GetBuffer(out int length), 0, length);
Log.WriteLine(hex);
Assert.Equal("03-01-2A-00-40-42-0F-00-00-00-00-00-63-00-00-00-39-30-00-00-00-00-39-01-61-62-63-64-65-66-67-68-69-6A-6B-6C-6D-6E-6F-70-71-72-73-74-75-76-77-78-79-7A-61-62-63-64-65-66-67-68-69-6A-6B-6C-6D-6E-6F-70-71-72-73-74-75-76-77-78-79-7A-61-62-63-64-65-66-67-68-69-6A-6B-6C-6D-6E-6F-70-71-72-73-74-75-76-77-78-79-7A-61-62-63-64-65-66-67-68-69-6A-6B-6C-6D-6E-6F-70-71-72-73-74-75-76-77-78-79-7A-61-62-63-64-65-66-67-68-69-6A-6B-6C-6D-6E-6F-70-71-72-73-74-75-76-77-78-79-7A-61-62-63-64-65-66-67-68-69-6A-6B-6C-6D-6E-6F-70-71-72-73-74-75-76-77-78-79-7A", hex);
}
}

Просмотреть файл

@ -0,0 +1,60 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
using Microsoft.Extensions.Caching.Hybrid.Internal;
using Microsoft.Extensions.DependencyInjection;
namespace Microsoft.Extensions.Caching.Hybrid.Tests;
public class ValidationTests
{
ServiceProvider GetDefaultCache(out DefaultHybridCache cache)
{
var services = new ServiceCollection();
services.AddHybridCache();
var provider = services.BuildServiceProvider();
cache = Assert.IsType<DefaultHybridCache>(provider.GetRequiredService<HybridCache>());
return provider;
}
[Fact]
public async Task ValidKeyWorks()
{
using var provider = GetDefaultCache(out var cache);
var id = await cache.GetOrCreateAsync<int>("some key", _ => new(42));
Assert.Equal(42, id);
}
[Theory]
[InlineData(null)]
[InlineData("")]
[InlineData(" ")]
[InlineData("\t\t")]
public async Task EmptyKeyIsInvalid(string key)
{
using var provider = GetDefaultCache(out var cache);
var ex = await Assert.ThrowsAsync<ArgumentException>(async () => await cache.GetOrCreateAsync<int>("", _ => new(42)));
Assert.Equal("key", ex.ParamName);
}
[Theory]
[InlineData("abc", "ABC")] // case sensitivity
[InlineData("abc", "a%2Db")] // %-encoding
[InlineData("abc", "aḃc")] // accented b
public async Task KeyIsNotAliased(string primary, string alt)
{
using var provider = GetDefaultCache(out var cache);
Assert.Equal(42, await cache.GetOrCreateAsync<int>(primary, _ => new(42)));
Assert.Equal(96, await cache.GetOrCreateAsync<int>(alt, _ => new(96)));
Assert.Equal(42, await cache.GetOrCreateAsync<int>(primary, _ => new(42)));
Assert.Equal(96, await cache.GetOrCreateAsync<int>(alt, _ => new(96)));
}
[Theory]
[InlineData("a\0bc")]
public async Task InvalidKeyDetected(string key)
{
using var provider = GetDefaultCache(out var cache);
var ex = await Assert.ThrowsAsync<ArgumentException>(async () => await cache.GetOrCreateAsync<int>(key, _ => new(42)));
Assert.Equal("key", ex.ParamName);
}
}