From a8b1ce7bbe5b4d63b8c2bfec4a058486285e4397 Mon Sep 17 00:00:00 2001 From: Jody Donetti Date: Sun, 9 Jul 2023 23:56:03 +0200 Subject: [PATCH] Merge v0.22.0 (#156) * Add Expire method + tests * Add error flag to obsolete props and methods * Better FusionCacheProvider: less and more uniform code + support for lazy named cache instantiations * Better DI-related tests * Add a log message when a FusionCache instance is created + update related tests * Better readonly methods * Add multi-levels tests for Expire() * Null handling fix per Yoshifumi's notes (MemoryPack serializer) * Optimization for when no real factory * Add XUnitLogger * Add cache name to every log message * Better logging tests * Add SkipMemoryCache entry options + tests * Handle zero duration and negative duration * Add backplane notification for background factory executions * Remove backplane and distributed cache when disposing a FusionCache instance * Old unused code cleanup * Add fluent methods to set with zero durations * Xml docs * Better adaptive caching * Add NullFusionCache and related tests * Add tests for adaptive caching and SkipMemoryCache working together * Docs * Add support for multiple cache instances in LoggingScenario * Add backward compatibility tests for deserialization of old version payloads * Timestamping + tests * Better fail-safe activation logic and flow * Add test for FusionCacheEntryOptions.Duplicate() method * Add ext methods for common overloads of method Expire/ExpireAsync --- README.md | 2 +- ZiggyCreatures.FusionCache.sln | 7 + .../OperationIdGenerationBenchmark.cs | 51 --- .../SamplePayload.cs | 37 +- .../SequentialComparisonBenchmarkAsync.cs | 2 - .../SequentialComparisonBenchmarkSync.cs | 2 - docs/CoreMethods.md | 87 ++-- docs/Options.md | 17 +- docs/StepByStep.md | 2 +- docs/Timeouts.md | 2 +- .../MemoryBackplane.cs | 3 - ...atures.FusionCache.Backplane.Memory.csproj | 7 +- .../RedisBackplane.cs | 35 +- ...nCache.Backplane.StackExchangeRedis.csproj | 9 +- .../ZiggyCreatures.FusionCache.Chaos.csproj | 2 +- ...SerializableFusionCacheDistributedEntry.cs | 8 +- .../SerializableFusionCacheEntryMetadata.cs | 1 + ...Fusion.Serialization.CysharpMemoryPack.xml | 1 + ...che.Serialization.CysharpMemoryPack.csproj | 8 +- ...che.Serialization.NeueccMessagePack.csproj | 9 +- ...nCache.Serialization.NewtonsoftJson.csproj | 7 +- .../FusionCacheProtoBufNetSerializer.cs | 1 + ...sionCache.Serialization.ProtoBufNet.csproj | 8 +- ...ache.Serialization.ServiceStackJson.csproj | 9 +- .../FusionCacheSystemTextJsonSerializer.cs | 3 - ...nCache.Serialization.SystemTextJson.csproj | 7 +- .../Backplane/BackplaneMessage.cs | 22 +- .../Backplane/BackplaneMessageAction.cs | 10 +- .../Events/FusionCacheEventsHub.cs | 10 + .../Events/FusionCacheMemoryEventsHub.cs | 10 + src/ZiggyCreatures.FusionCache/FusionCache.cs | 251 ++++++----- .../FusionCacheEntryOptions.cs | 58 ++- .../FusionCacheExtMethods.cs | 49 +++ .../FusionCacheGlobalDefaults.cs | 5 + .../FusionCacheServiceCollectionExtensions.cs | 8 +- .../FusionCache_Async.cs | 332 +++++++++----- .../FusionCache_Sync.cs | 330 +++++++++----- .../GlobalSuppressions.cs | 4 +- .../IFusionCache.cs | 39 +- .../Internals/Backplane/BackplaneAccessor.cs | 103 +++-- .../Backplane/BackplaneAccessor_Async.cs | 20 +- .../Backplane/BackplaneAccessor_Sync.cs | 20 +- .../Internals/Builder/FusionCacheBuilder.cs | 2 +- .../Distributed/DistributedCacheAccessor.cs | 8 +- .../DistributedCacheAccessor_Async.cs | 27 +- .../DistributedCacheAccessor_Sync.cs | 29 +- .../FusionCacheDistributedEntry.cs | 51 ++- .../Internals/FusionCacheInternalUtils.cs | 58 +-- .../Internals/IFusionCacheEntry.cs | 5 + .../Memory/FusionCacheMemoryEntry.cs | 34 +- .../Internals/Memory/MemoryCacheAccessor.cs | 26 +- .../Internals/Provider/FusionCacheProvider.cs | 40 +- ...NamedCacheWrapper.cs => LazyNamedCache.cs} | 6 +- .../NullFusionCache.cs | 208 +++++++++ .../FusionCacheReactorProbabilistic.cs | 32 +- .../Reactors/FusionCacheReactorStandard.cs | 29 +- .../Reactors/FusionCacheReactorUnbounded.cs | 16 +- .../FusionCacheReactorUnboundedConcurrent.cs | 16 +- ...sionCacheReactorUnboundedConcurrentLazy.cs | 16 +- .../FusionCacheReactorUnboundedWithPool.cs | 16 +- .../Reactors/IFusionCacheReactor.cs | 13 +- .../ZiggyCreatures.FusionCache.csproj | 21 +- .../ZiggyCreatures.FusionCache.xml | 292 ++++++++++++- src/ZiggyCreatures.FusionCache/docs/README.md | 2 +- tests/SerializerPayloadGenerator/Program.cs | 102 +++++ ...cysharpmemorypackserializer__v0_20_0_0.bin | Bin 0 -> 43 bytes ...cysharpmemorypackserializer__v0_21_0_0.bin | Bin 0 -> 110 bytes ...neueccmessagepackserializer__v0_20_0_0.bin | Bin 0 -> 40 bytes ...neueccmessagepackserializer__v0_21_0_0.bin | Bin 0 -> 85 bytes ...chenewtonsoftjsonserializer__v0_20_0_0.bin | 1 + ...chenewtonsoftjsonserializer__v0_21_0_0.bin | 1 + ...ncacheprotobufnetserializer__v0_20_0_0.bin | 2 + ...ncacheprotobufnetserializer__v0_21_0_0.bin | 2 + ...eservicestackjsonserializer__v0_20_0_0.bin | 1 + ...eservicestackjsonserializer__v0_21_0_0.bin | 1 + ...chesystemtextjsonserializer__v0_20_0_0.bin | 1 + ...chesystemtextjsonserializer__v0_21_0_0.bin | 1 + .../SerializerPayloadGenerator.csproj | 24 + .../Scenarios/LoggingScenario.cs | 128 +++--- ...ggyCreatures.FusionCache.Playground.csproj | 8 +- .../BackplaneTests.cs | 383 +++++++++++++++- .../DependencyInjectionTests.cs | 323 ++++++++------ .../{LogLevelsTests.cs => LoggingTests.cs} | 51 ++- .../MultiLevelTests.cs | 72 +++ ...cysharpmemorypackserializer__v0_20_0_0.bin | Bin 0 -> 43 bytes ...cysharpmemorypackserializer__v0_21_0_0.bin | Bin 0 -> 110 bytes ...neueccmessagepackserializer__v0_20_0_0.bin | Bin 0 -> 40 bytes ...neueccmessagepackserializer__v0_21_0_0.bin | Bin 0 -> 85 bytes ...chenewtonsoftjsonserializer__v0_20_0_0.bin | 1 + ...chenewtonsoftjsonserializer__v0_21_0_0.bin | 1 + ...ncacheprotobufnetserializer__v0_20_0_0.bin | 2 + ...ncacheprotobufnetserializer__v0_21_0_0.bin | 2 + ...eservicestackjsonserializer__v0_20_0_0.bin | 1 + ...eservicestackjsonserializer__v0_21_0_0.bin | 1 + ...chesystemtextjsonserializer__v0_20_0_0.bin | 1 + ...chesystemtextjsonserializer__v0_21_0_0.bin | 1 + .../SerializationTests.cs | 50 +++ .../SingleLevelTests.cs | 410 ++++++++++++++++++ .../Stuff/ListLogger.cs | 3 +- .../Stuff/XUnitLogger.cs | 44 ++ .../ZiggyCreatures.FusionCache.Tests.csproj | 16 +- 101 files changed, 3206 insertions(+), 973 deletions(-) delete mode 100644 benchmarks/ZiggyCreatures.FusionCache.Benchmarks/OperationIdGenerationBenchmark.cs rename src/ZiggyCreatures.FusionCache/Internals/Provider/{NamedCacheWrapper.cs => LazyNamedCache.cs} (87%) create mode 100644 src/ZiggyCreatures.FusionCache/NullFusionCache.cs create mode 100644 tests/SerializerPayloadGenerator/Program.cs create mode 100644 tests/SerializerPayloadGenerator/Samples/fusioncachecysharpmemorypackserializer__v0_20_0_0.bin create mode 100644 tests/SerializerPayloadGenerator/Samples/fusioncachecysharpmemorypackserializer__v0_21_0_0.bin create mode 100644 tests/SerializerPayloadGenerator/Samples/fusioncacheneueccmessagepackserializer__v0_20_0_0.bin create mode 100644 tests/SerializerPayloadGenerator/Samples/fusioncacheneueccmessagepackserializer__v0_21_0_0.bin create mode 100644 tests/SerializerPayloadGenerator/Samples/fusioncachenewtonsoftjsonserializer__v0_20_0_0.bin create mode 100644 tests/SerializerPayloadGenerator/Samples/fusioncachenewtonsoftjsonserializer__v0_21_0_0.bin create mode 100644 tests/SerializerPayloadGenerator/Samples/fusioncacheprotobufnetserializer__v0_20_0_0.bin create mode 100644 tests/SerializerPayloadGenerator/Samples/fusioncacheprotobufnetserializer__v0_21_0_0.bin create mode 100644 tests/SerializerPayloadGenerator/Samples/fusioncacheservicestackjsonserializer__v0_20_0_0.bin create mode 100644 tests/SerializerPayloadGenerator/Samples/fusioncacheservicestackjsonserializer__v0_21_0_0.bin create mode 100644 tests/SerializerPayloadGenerator/Samples/fusioncachesystemtextjsonserializer__v0_20_0_0.bin create mode 100644 tests/SerializerPayloadGenerator/Samples/fusioncachesystemtextjsonserializer__v0_21_0_0.bin create mode 100644 tests/SerializerPayloadGenerator/SerializerPayloadGenerator.csproj rename tests/ZiggyCreatures.FusionCache.Tests/{LogLevelsTests.cs => LoggingTests.cs} (60%) create mode 100644 tests/ZiggyCreatures.FusionCache.Tests/Samples/fusioncachecysharpmemorypackserializer__v0_20_0_0.bin create mode 100644 tests/ZiggyCreatures.FusionCache.Tests/Samples/fusioncachecysharpmemorypackserializer__v0_21_0_0.bin create mode 100644 tests/ZiggyCreatures.FusionCache.Tests/Samples/fusioncacheneueccmessagepackserializer__v0_20_0_0.bin create mode 100644 tests/ZiggyCreatures.FusionCache.Tests/Samples/fusioncacheneueccmessagepackserializer__v0_21_0_0.bin create mode 100644 tests/ZiggyCreatures.FusionCache.Tests/Samples/fusioncachenewtonsoftjsonserializer__v0_20_0_0.bin create mode 100644 tests/ZiggyCreatures.FusionCache.Tests/Samples/fusioncachenewtonsoftjsonserializer__v0_21_0_0.bin create mode 100644 tests/ZiggyCreatures.FusionCache.Tests/Samples/fusioncacheprotobufnetserializer__v0_20_0_0.bin create mode 100644 tests/ZiggyCreatures.FusionCache.Tests/Samples/fusioncacheprotobufnetserializer__v0_21_0_0.bin create mode 100644 tests/ZiggyCreatures.FusionCache.Tests/Samples/fusioncacheservicestackjsonserializer__v0_20_0_0.bin create mode 100644 tests/ZiggyCreatures.FusionCache.Tests/Samples/fusioncacheservicestackjsonserializer__v0_21_0_0.bin create mode 100644 tests/ZiggyCreatures.FusionCache.Tests/Samples/fusioncachesystemtextjsonserializer__v0_20_0_0.bin create mode 100644 tests/ZiggyCreatures.FusionCache.Tests/Samples/fusioncachesystemtextjsonserializer__v0_21_0_0.bin create mode 100644 tests/ZiggyCreatures.FusionCache.Tests/Stuff/XUnitLogger.cs diff --git a/README.md b/README.md index f3a08532..85926b49 100644 --- a/README.md +++ b/README.md @@ -49,7 +49,7 @@ With [🦄 A Gentle Introduction](docs/AGentleIntroduction.md) you'll get yourse Want to start using it immediately? There's a [â­ Quick Start](#-quick-start) for you. -Curious about what you can achieve from start to finish? There's a [:woman_teacher: Step By Step ](docs/StepByStep.md) guide. +Curious about what you can achieve from start to finish? There's a [👩â€ðŸ« Step By Step ](docs/StepByStep.md) guide. More into videos? The great Anna Hoffman has been so nice to listen to me mumble random stuff on [Data Exposed](https://learn.microsoft.com/en-us/shows/data-exposed/caching-made-easy-in-azure-sql-db-with-fusioncache-data-exposed). diff --git a/ZiggyCreatures.FusionCache.sln b/ZiggyCreatures.FusionCache.sln index 8ca00965..0bf33ad2 100644 --- a/ZiggyCreatures.FusionCache.sln +++ b/ZiggyCreatures.FusionCache.sln @@ -41,6 +41,8 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ZiggyCreatures.FusionCache. EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ZiggyCreatures.FusionCache.Serialization.ServiceStackJson", "src\ZiggyCreatures.FusionCache.Serialization.ServiceStackJson\ZiggyCreatures.FusionCache.Serialization.ServiceStackJson.csproj", "{CE437FB2-510F-4DCE-8A1F-AED747DAA4EB}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SerializerPayloadGenerator", "tests\SerializerPayloadGenerator\SerializerPayloadGenerator.csproj", "{5B1AF24E-90FC-4C21-AF9C-090FE32027E3}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -99,6 +101,10 @@ Global {CE437FB2-510F-4DCE-8A1F-AED747DAA4EB}.Debug|Any CPU.Build.0 = Debug|Any CPU {CE437FB2-510F-4DCE-8A1F-AED747DAA4EB}.Release|Any CPU.ActiveCfg = Release|Any CPU {CE437FB2-510F-4DCE-8A1F-AED747DAA4EB}.Release|Any CPU.Build.0 = Release|Any CPU + {5B1AF24E-90FC-4C21-AF9C-090FE32027E3}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {5B1AF24E-90FC-4C21-AF9C-090FE32027E3}.Debug|Any CPU.Build.0 = Debug|Any CPU + {5B1AF24E-90FC-4C21-AF9C-090FE32027E3}.Release|Any CPU.ActiveCfg = Release|Any CPU + {5B1AF24E-90FC-4C21-AF9C-090FE32027E3}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -117,6 +123,7 @@ Global {FC2C7F03-69E0-4CCE-A582-1FB6D8D11560} = {34B53F49-F5C5-4850-B79E-59AD130379C6} {919CDF8C-463A-4E82-AFFD-DF8A6B904600} = {34B53F49-F5C5-4850-B79E-59AD130379C6} {CE437FB2-510F-4DCE-8A1F-AED747DAA4EB} = {34B53F49-F5C5-4850-B79E-59AD130379C6} + {5B1AF24E-90FC-4C21-AF9C-090FE32027E3} = {C6F3C570-C68C-4A95-960E-82778306BDBA} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {92916FA2-FCAC-406E-BF3F-0A2CE9512EF0} diff --git a/benchmarks/ZiggyCreatures.FusionCache.Benchmarks/OperationIdGenerationBenchmark.cs b/benchmarks/ZiggyCreatures.FusionCache.Benchmarks/OperationIdGenerationBenchmark.cs deleted file mode 100644 index 16dc1ea6..00000000 --- a/benchmarks/ZiggyCreatures.FusionCache.Benchmarks/OperationIdGenerationBenchmark.cs +++ /dev/null @@ -1,51 +0,0 @@ -using BenchmarkDotNet.Attributes; -using BenchmarkDotNet.Columns; -using BenchmarkDotNet.Configs; -using ZiggyCreatures.Caching.Fusion.Internals; - -namespace ZiggyCreatures.Caching.Fusion.Benchmarks -{ - [MemoryDiagnoser] - [Config(typeof(Config))] - public class OperationIdGenerationBenchmark - { - - private class Config : ManualConfig - { - public Config() - { - AddColumn( - StatisticColumn.P95 - ); - } - } - - [Benchmark(Baseline = true)] - public string V1() - { - return FusionCacheInternalUtils.GenerateOperationId_V1(); - } - - [Benchmark] - public string V2() - { - return FusionCacheInternalUtils.GenerateOperationId_V2(); - } - - [Benchmark] - public string V3() - { - return FusionCacheInternalUtils.GenerateOperationId_V3(); - } - } -} - -/* -RESULTS - -| Method | Mean | Error | StdDev | P95 | Ratio | Gen 0 | Allocated | -|----------- |----------:|---------:|---------:|----------:|------:|-------:|----------:| -| Current | 196.82 ns | 0.405 ns | 0.359 ns | 197.32 ns | 1.00 | 0.0210 | 88 B | -| Optimized | 25.36 ns | 0.070 ns | 0.062 ns | 25.46 ns | 0.13 | 0.0249 | 104 B | -| Optimized2 | 32.60 ns | 0.343 ns | 0.268 ns | 33.06 ns | 0.17 | 0.0114 | 48 B | -*/ diff --git a/benchmarks/ZiggyCreatures.FusionCache.Benchmarks/SamplePayload.cs b/benchmarks/ZiggyCreatures.FusionCache.Benchmarks/SamplePayload.cs index 7e6b2f43..8244084e 100644 --- a/benchmarks/ZiggyCreatures.FusionCache.Benchmarks/SamplePayload.cs +++ b/benchmarks/ZiggyCreatures.FusionCache.Benchmarks/SamplePayload.cs @@ -1,6 +1,9 @@ -namespace ZiggyCreatures.Caching.Fusion.Benchmarks +using System; +using System.Collections.Generic; + +namespace ZiggyCreatures.Caching.Fusion.Benchmarks { - public class SamplePayload + public class SamplePayload : IEquatable { public SamplePayload() { @@ -12,5 +15,33 @@ public SamplePayload() public string Foo { get; set; } public string Bar { get; set; } public int Baz { get; set; } + + public override bool Equals(object? obj) + { + return Equals(obj as SamplePayload); + } + + public bool Equals(SamplePayload? other) + { + return other is not null && + Foo == other.Foo && + Bar == other.Bar && + Baz == other.Baz; + } + + public override int GetHashCode() + { + return HashCode.Combine(Foo, Bar, Baz); + } + + public static bool operator ==(SamplePayload? left, SamplePayload? right) + { + return EqualityComparer.Default.Equals(left, right); + } + + public static bool operator !=(SamplePayload? left, SamplePayload? right) + { + return !(left == right); + } } -} \ No newline at end of file +} diff --git a/benchmarks/ZiggyCreatures.FusionCache.Benchmarks/SequentialComparisonBenchmarkAsync.cs b/benchmarks/ZiggyCreatures.FusionCache.Benchmarks/SequentialComparisonBenchmarkAsync.cs index f76319ec..66d9871a 100644 --- a/benchmarks/ZiggyCreatures.FusionCache.Benchmarks/SequentialComparisonBenchmarkAsync.cs +++ b/benchmarks/ZiggyCreatures.FusionCache.Benchmarks/SequentialComparisonBenchmarkAsync.cs @@ -19,7 +19,6 @@ namespace ZiggyCreatures.Caching.Fusion.Benchmarks [Config(typeof(Config))] public class SequentialComparisonBenchmarkAsync { - private class Config : ManualConfig { public Config() @@ -160,6 +159,5 @@ await appcache.GetOrAddAsync( cache.Compact(1); } } - } } diff --git a/benchmarks/ZiggyCreatures.FusionCache.Benchmarks/SequentialComparisonBenchmarkSync.cs b/benchmarks/ZiggyCreatures.FusionCache.Benchmarks/SequentialComparisonBenchmarkSync.cs index 2bcd0bac..3b7725d3 100644 --- a/benchmarks/ZiggyCreatures.FusionCache.Benchmarks/SequentialComparisonBenchmarkSync.cs +++ b/benchmarks/ZiggyCreatures.FusionCache.Benchmarks/SequentialComparisonBenchmarkSync.cs @@ -16,7 +16,6 @@ namespace ZiggyCreatures.Caching.Fusion.Benchmarks [Config(typeof(Config))] public class SequentialComparisonBenchmarkSync { - private class Config : ManualConfig { public Config() @@ -158,6 +157,5 @@ public void LazyCache() cache.Compact(1); } } - } } diff --git a/docs/CoreMethods.md b/docs/CoreMethods.md index 350a1065..bd473660 100644 --- a/docs/CoreMethods.md +++ b/docs/CoreMethods.md @@ -6,13 +6,14 @@ # :joystick: Core Methods -At a high level there are 5 core methods: +At a high level there are 6 core methods: - `Set[Async]` - `Remove[Async]` - `TryGet[Async]` - `GetOrDefault[Async]` - `GetOrSet[Async]` +- `Expire[Async]` All of them work **on both the memory cache and the distributed cache** (if any) in a transparent way: you don't have to do anything extra for it to coordinate the 2 layers. @@ -53,33 +54,6 @@ cache.Remove("foo"); cache.Remove("foo"); ``` -## GetOrDefault[Async] - -It is used to **GET** the value in the cache for the specified key and, if nothing is there, returns a **DEFAULT VALUE**. - -With this method **NOTHING IS SET** in the cache. - -It is useful if you want to use what's in the cache or some default value just for this call, but **you don't want to change the state** of the cache itself. - -Examples: - -```csharp -// THIS WILL GET BACK 42 -foo = cache.GetOrDefault("foo", 42); - -// IF WE IMMEDIATELY CALL THIS, WE WILL GET BACK 21 -foo = cache.GetOrDefault("foo", 21); - -// THIS WILL GET BACK 0, WHICH IS THE DEFAULT VALUE FOR THE TYPE int -foo = cache.GetOrDefault("foo"); - -// ALSO USEFUL FOR USER PREFERENCES: WE CAN USE A DEFAULT VALUE WITHOUT SETTING ONE -var enableUnicorns = cache.GetOrDefault("flags.unicorns", false); - -// AND SINCE false IS THE DEFAULT VALUE FOR THE TYPE bool WE CAN SIMPLY DO THIS -var enableUnicorns = cache.GetOrDefault("flags.unicorns"); -``` - ## TryGet[Async] It is used to **CHECK** if a value is in the cache for the specified key and, if so, to **GET** the value itself all at once. @@ -122,6 +96,33 @@ int result = maybeFoo; 💡 It's not possible to use the classic method signature of `bool TryGet(string key, out TValue value)` to set a value with an `out` parameter because .NET does not allow it on async methods (for good reasons) and I wanted to keep the same signature for every method in both sync/async versions. +## GetOrDefault[Async] + +It is used to **GET** the value in the cache for the specified key and, if nothing is there, returns a **DEFAULT VALUE**. + +With this method **NOTHING IS SET** in the cache. + +It is useful if you want to use what's in the cache or some default value just for this call, but **you don't want to change the state** of the cache itself. + +Examples: + +```csharp +// THIS WILL GET BACK 42 +foo = cache.GetOrDefault("foo", 42); + +// IF WE IMMEDIATELY CALL THIS, WE WILL GET BACK 21 +foo = cache.GetOrDefault("foo", 21); + +// THIS WILL GET BACK 0, WHICH IS THE DEFAULT VALUE FOR THE TYPE int +foo = cache.GetOrDefault("foo"); + +// ALSO USEFUL FOR USER PREFERENCES: WE CAN USE A DEFAULT VALUE WITHOUT SETTING ONE +var enableUnicorns = cache.GetOrDefault("flags.unicorns", false); + +// AND SINCE false IS THE DEFAULT VALUE FOR THE TYPE bool WE CAN SIMPLY DO THIS +var enableUnicorns = cache.GetOrDefault("flags.unicorns"); +``` + ## GetOrSet[Async] This is the most important and powerful method available, and it does **a lot** for you. @@ -204,16 +205,40 @@ var foo = cache.GetOrSet( var foo = cache.GetOrSet("foo", _ => GetFooFromDb(), 42); ``` +## Expire[Async] + +It is used to explicitly **EXPIRE** the value in the cache for the specified key. + +But wait, what is the difference between `Expire()` and `Remove()`? With `Remove()` the value is actually removed, totally. With `Expire()` instead, it depends on how [fail-safe](FailSafe.md) is configured: +- if fail-safe is enabled: the entry will marked as _logically_ expired, but will still be available as a fallback value in case of future problems +- if fail-safe is disabled: the entry will be effectively removed + +This method may be is useful in case we want to threat something as remove, but with the ability to say _"better than nothing"_ in the future, in case of problems (thanks to fail-safe). + +Examples: + +```csharp +cache.Set("foo", 42, opt => opt.SetDuration(TimeSpan.FromSeconds(10)).SetFailSafe(true)); + +cache.Expire("foo", opt => opt.SetFailSafe(true)); + +// THIS WILL GET BACK 0, WHICH IS THE DEFAULT VALUE FOR THE TYPE int +foo = cache.GetOrDefault("foo"); + +// THIS WILL GET BACK 42 +foo = cache.GetOrDefault("foo", opt => opt.SetFailSafe(true)); +``` + ## :recycle: Common overloads Every core method that needs a set of options (`FusionCacheEntryOptions`) for how to behave has different overloads to let you specify these options, for better ease of use. You can choose between passing: -- **Nothing**: you don't pass anything, so the global `DefaultEntryOptions` will be used (also saves some memory allocations) +- **Nothing**: you don't pass anything, so the `cache.DefaultEntryOptions` will be used (also saves some memory allocations) - **Options**: you directly pass a `FusionCacheEntryOptions` object. This gives you total control over each option, but you have to instantiate it yourself and **does not copy** the global `DefaultEntryOptions` - **Setup action**: you pass a `lambda` that receives a duplicate of the `DefaultEntryOptions` so you start from there and modify it as you like (there's also a set of *fluent methods* to do that easily) -- **Duration**: you simply pass a `TimeSpan` value for the duration. This is the same as the previous one (start from the global default + lambda) but for the common scenario of when you only want to change the duration +- **Duration**: you simply pass a `TimeSpan` value for the duration. This is the same as the previous one (start from default + duplicate + lambda) but for the common scenario of when you only want to change the duration ## 🤷â€â™‚ï¸ `MaybeValue` The special type `MaybeValue` is similar to the standard `Nullable` type in .NET, but usable for both value types and reference types. @@ -222,7 +247,7 @@ The type has a `bool HasValue` property to know if it contains a value or not (l The type also is implicitly convertible to and from values of type `T`, so that you don't have to manually do it yourself. -Finally, if you want to get the value inside of it or a default one if case it's not there, you can use the `GetValueOrDefault()` method, with or without the default value to use (again, just like standard nullables. +Finally, if you want to get the value inside of it or a default one in case it's not there, you can use the `GetValueOrDefault()` method, with or without the default value to use (again, just like standard nullables). Currently this type is used inside FusionCache in 2 places: diff --git a/docs/Options.md b/docs/Options.md index 073d0012..753133cc 100644 --- a/docs/Options.md +++ b/docs/Options.md @@ -53,6 +53,7 @@ In general this can be used as a set of options that will act as the *baseline*, | `CacheName` | `string` | `"FusionCache"` | The name of the cache: it can be used for identification, and in a multi-node scenario it is typically shared between nodes to create a logical association. | | `DefaultEntryOptions` | `FusionCacheEntryOptions` | *see below* | This is the default entry options object that will be used when one is not passed to each method call that need one, and as a starting point when duplicating one, either via the explicit `FusionCache.CreateOptions(...)` method or in one of the *overloads* of each *core method*. | | `DistributedCacheCircuitBreakerDuration` | `TimeSpan` | `none` | The duration of the circuit-breaker used when working with the distributed cache. | +| `CacheKeyPrefix` | `string?` | `null` | A prefix that will be added to each cache key for each call: it can be useful when working with multiple named caches. With the builder it can be set using the `WithCacheKeyPrefix(...)` method. | | `DistributedCacheKeyModifierMode` | `CacheKeyModifierMode` | `Prefix` | Specify the mode in which cache key will be changed for the distributed cache (eg: to specify the wire format version). | | `BackplaneCircuitBreakerDuration` | `TimeSpan` | `none` | The duration of the circuit-breaker used when working with the backplane. | | `BackplaneChannelPrefix` | `string?` | `null` | The prefix to use in the backplane channel name: if not specified the `CacheName` will be used. | @@ -100,10 +101,14 @@ For a better **developer experience** and to **consume less memory** (higher per | `AllowTimedOutFactoryBackgroundCompletion` | `bool` | `true` | It enables a factory that has hit a synthetic timeout (both soft/hard) to complete in the background and update the cache with the new value. | | 🧙â€â™‚ï¸ `DistributedCacheDuration` | `TimeSpan?` | `null` | The custom duration to use for the distributed cache: this allows to have different duration between the 1st and 2nd layers. If `null`, the normal `Duration` will be used. | | 🧙â€â™‚ï¸ `DistributedCacheFailSafeMaxDuration` | `TimeSpan?` | `null` | The custom fail-safe max duration to use for the distributed cache: this allows to have different duration between the 1st and 2nd layers. If `null`, the normal `FailSafeMaxDuration` will be used. | -| `DistributedCacheSoftTimeout` | `TimeSpan` | `none` | The maximum execution time allowed for each operation on the distributed cache when is not problematic to simply timeout. | -| `DistributedCacheHardTimeout` | `TimeSpan` | `none` | The maximum execution time allowed for each operation on the distributed cache in any case, even if there is not a stale value to fallback to. | -| `AllowBackgroundDistributedCacheOperations` | `bool` | `false` | Normally operations on the distributed cache are executed in a blocking fashion: setting this flag to true let them run in the background in a kind of fire-and-forget way. This will give a perf boost, but watch out for rare side effects. | -| `ReThrowDistributedCacheExceptions` | `bool` | `false` | Set this to true to allow the bubble up of distributed cache exceptions (default is `false`). Please note that, even if set to true, in some cases you would also need `AllowBackgroundDistributedCacheOperations` set to false and no timeout (neither soft nor hard) specified. | -| `ReThrowSerializationExceptions` | `bool` | `true` | Set this to false to disable the bubble up of serialization exceptions (default is `true`). | +| 🧙â€â™‚ï¸ `DistributedCacheSoftTimeout` | `TimeSpan` | `none` | The maximum execution time allowed for each operation on the distributed cache when is not problematic to simply timeout. | +| 🧙â€â™‚ï¸ `DistributedCacheHardTimeout` | `TimeSpan` | `none` | The maximum execution time allowed for each operation on the distributed cache in any case, even if there is not a stale value to fallback to. | +| 🧙â€â™‚ï¸ `AllowBackgroundDistributedCacheOperations` | `bool` | `false` | Normally operations on the distributed cache are executed in a blocking fashion: setting this flag to true let them run in the background in a kind of fire-and-forget way. This will give a perf boost, but watch out for rare side effects. | +| 🧙â€â™‚ï¸ `ReThrowDistributedCacheExceptions` | `bool` | `false` | Set this to true to allow the bubble up of distributed cache exceptions (default is `false`). Please note that, even if set to true, in some cases you would also need `AllowBackgroundDistributedCacheOperations` set to false and no timeout (neither soft nor hard) specified. | +| 🧙 `ReThrowSerializationExceptions` | `bool` | `true` | Set this to false to disable the bubble up of serialization exceptions (default is `true`). | | 🧙â€â™‚ï¸ `SkipBackplaneNotifications` | `bool` | `false` | Skip sending backplane notifications after some operations, like a SET (via a Set/GetOrSet call) or a REMOVE (via a Remove call). | -| 🧙â€â™‚ï¸ `AllowBackgroundBackplaneOperations` | `bool` | `true` | By default every operation on the backplane is non-blocking: that is to say the FusionCache method call would not wait for each backplane operation to be completed. Setting this flag to `false` will execute these operations in a blocking fashion, typically resulting in worse performance. | \ No newline at end of file +| 🧙â€â™‚ï¸ `AllowBackgroundBackplaneOperations` | `bool` | `true` | By default every operation on the backplane is non-blocking: that is to say the FusionCache method call would not wait for each backplane operation to be completed. Setting this flag to `false` will execute these operations in a blocking fashion, typically resulting in worse performance. | +| 🧙â€â™‚ï¸ `EagerRefreshThreshold` | `float?` | `null` | The threshold to apply when deciding whether to refresh the cache entry eagerly (that is, before the actual expiration). | +| 🧙â€â™‚ï¸ `SkipDistributedCache` | `bool` | `false` | Skip the usage of the distributed cache, if any. | +| 🧙â€â™‚ï¸ `SkipDistributedCacheReadWhenStale` | `bool` | `false` | When a 2nd layer (distributed cache) is used and a cache entry in the 1st layer (memory cache) is found but is stale, a read is done on the distributed cache: the reason is that in a multi-node environment another node may have updated the cache entry, so we may found a newer version of it. | +| 🧙â€â™‚ï¸ `SkipMemoryCache` | `bool` | `false` | Skip the usage of the memory cache. | diff --git a/docs/StepByStep.md b/docs/StepByStep.md index 3bff029e..09d2bfa4 100644 --- a/docs/StepByStep.md +++ b/docs/StepByStep.md @@ -4,7 +4,7 @@ -# :woman_teacher: Step By Step +# 👩â€ðŸ« Step By Step What follows is an example scenario on which we can reason about: we've built a **service** that handle some **requests** by retrieving some data from a **database**, that's it. diff --git a/docs/Timeouts.md b/docs/Timeouts.md index 1099a94e..587e822c 100644 --- a/docs/Timeouts.md +++ b/docs/Timeouts.md @@ -8,7 +8,7 @@ There are different types of timeouts available and it may be useful to know them. -:bulb: For a complete example of how to use them and what results you can achieve there's the [:woman_teacher: Step By Step](StepByStep.md) guide. +:bulb: For a complete example of how to use them and what results you can achieve there's the [👩â€ðŸ« Step By Step](StepByStep.md) guide. ## Factory Timeouts Sometimes your data source (database, webservice, etc) is overloaded, the network is congested or something else bad is happening and the end result is things start to get **:snail: very slow** to get a fresh piece of data. diff --git a/src/ZiggyCreatures.FusionCache.Backplane.Memory/MemoryBackplane.cs b/src/ZiggyCreatures.FusionCache.Backplane.Memory/MemoryBackplane.cs index bb08147f..3f4aeb25 100644 --- a/src/ZiggyCreatures.FusionCache.Backplane.Memory/MemoryBackplane.cs +++ b/src/ZiggyCreatures.FusionCache.Backplane.Memory/MemoryBackplane.cs @@ -110,9 +110,6 @@ public void Publish(BackplaneMessage message, FusionCacheEntryOptions options) if (_backplanes is null) throw new NullReferenceException("Something went wrong :-|"); - if (_logger?.IsEnabled(LogLevel.Debug) ?? false) - _logger.Log(LogLevel.Debug, "A backplane notification has been sent for {CacheKey}", message.CacheKey); - foreach (var backplane in _backplanes) { if (backplane == this) diff --git a/src/ZiggyCreatures.FusionCache.Backplane.Memory/ZiggyCreatures.FusionCache.Backplane.Memory.csproj b/src/ZiggyCreatures.FusionCache.Backplane.Memory/ZiggyCreatures.FusionCache.Backplane.Memory.csproj index 8987a0fc..1f19faa8 100644 --- a/src/ZiggyCreatures.FusionCache.Backplane.Memory/ZiggyCreatures.FusionCache.Backplane.Memory.csproj +++ b/src/ZiggyCreatures.FusionCache.Backplane.Memory/ZiggyCreatures.FusionCache.Backplane.Memory.csproj @@ -4,7 +4,7 @@ netstandard2.0 latest enable - 0.21.0 + 0.22.0 ZiggyCreatures.FusionCache.Backplane.Memory logo-128x128.png FusionCache in memory backplane, used for testing @@ -12,7 +12,10 @@ ZiggyCreatures.Caching.Fusion.Backplane.Memory ZiggyCreatures.FusionCache.Backplane.Memory.xml README.md - Dependencies update + + - Better log messages + - Dependencies update + diff --git a/src/ZiggyCreatures.FusionCache.Backplane.StackExchangeRedis/RedisBackplane.cs b/src/ZiggyCreatures.FusionCache.Backplane.StackExchangeRedis/RedisBackplane.cs index d8a12c5d..e17dabce 100644 --- a/src/ZiggyCreatures.FusionCache.Backplane.StackExchangeRedis/RedisBackplane.cs +++ b/src/ZiggyCreatures.FusionCache.Backplane.StackExchangeRedis/RedisBackplane.cs @@ -132,7 +132,7 @@ private void Disconnect() catch (Exception exc) { if (_logger?.IsEnabled(LogLevel.Error) ?? false) - _logger.Log(LogLevel.Error, exc, "An error occurred while disconnecting from Redis"); + _logger.Log(LogLevel.Error, exc, "FUSION: An error occurred while disconnecting from Redis {Config}", _options.ConfigurationOptions?.ToString() ?? _options.Configuration); } _connection = null; @@ -152,7 +152,7 @@ public void Subscribe(BackplaneSubscriptionOptions subscriptionOptions) _subscriptionOptions = subscriptionOptions; - _channel = _subscriptionOptions.ChannelName; + _channel = new RedisChannel(_subscriptionOptions.ChannelName, RedisChannel.PatternMode.Literal); _handler = _subscriptionOptions.Handler; _ = Task.Run(async () => @@ -187,25 +187,14 @@ public async ValueTask PublishAsync(BackplaneMessage message, FusionCacheEntryOp if (v.IsNull) return; - //try - //{ var receivedCount = await _subscriber!.PublishAsync(_channel, v).ConfigureAwait(false); if (receivedCount == 0) { if (_logger?.IsEnabled(LogLevel.Error) ?? false) - _logger.Log(LogLevel.Error, "An error occurred while trying to send a notification to the Redis backplane"); + _logger.Log(LogLevel.Error, "FUSION (K={CacheKey}): an error occurred while trying to send a notification to the Redis backplane ({Action})", message.CacheKey, message.Action); return; } - - if (_logger?.IsEnabled(LogLevel.Debug) ?? false) - _logger.Log(LogLevel.Debug, "An eviction notification has been sent for {CacheKey}", message.CacheKey); - //} - //catch (Exception exc) - //{ - // if (_logger?.IsEnabled(LogLevel.Error) ?? false) - // _logger.Log(LogLevel.Error, exc, "An error occurred while trying to send a notification to the Redis backplane"); - //} } /// @@ -218,25 +207,17 @@ public void Publish(BackplaneMessage message, FusionCacheEntryOptions options) if (v.IsNull) return; - //try - //{ var receivedCount = _subscriber!.Publish(_channel, v); if (receivedCount == 0) { if (_logger?.IsEnabled(LogLevel.Error) ?? false) - _logger.Log(LogLevel.Error, "An error occurred while trying to send a notification to the Redis backplane"); + _logger.Log(LogLevel.Error, "FUSION (K={CacheKey}): an error occurred while trying to send a notification to the Redis backplane ({Action})", message.CacheKey, message.Action); return; } if (_logger?.IsEnabled(LogLevel.Debug) ?? false) - _logger.Log(LogLevel.Debug, "An eviction notification has been sent for {CacheKey}", message.CacheKey); - //} - //catch (Exception exc) - //{ - // if (_logger?.IsEnabled(LogLevel.Error) ?? false) - // _logger.Log(LogLevel.Error, exc, "An error occurred while trying to send a notification to the Redis backplane"); - //} + _logger.Log(LogLevel.Debug, "FUSION (K={CacheKey}): a notification has been sent ({Action})", message.CacheKey, message.Action); } private static BackplaneMessage? FromRedisValue(RedisValue value, ILogger? logger) @@ -256,7 +237,7 @@ public void Publish(BackplaneMessage message, FusionCacheEntryOptions options) if (version != 0) { if (logger?.IsEnabled(LogLevel.Warning) ?? false) - logger.Log(LogLevel.Warning, "The version header does not have the expected value of 0 (zero): instead the value is " + version); + logger.Log(LogLevel.Warning, "FUSION: the version header does not have the expected value of 0 (zero): instead the value is " + version); return null; } pos++; @@ -286,7 +267,7 @@ public void Publish(BackplaneMessage message, FusionCacheEntryOptions options) catch (Exception exc) { if (logger?.IsEnabled(LogLevel.Warning) ?? false) - logger.Log(LogLevel.Warning, exc, "An error occurred while converting a RedisValue into a BackplaneMessage"); + logger.Log(LogLevel.Warning, exc, "FUSION: an error occurred while converting a RedisValue into a BackplaneMessage"); } return null; @@ -339,7 +320,7 @@ private static RedisValue ToRedisValue(BackplaneMessage message, ILogger? logger catch (Exception exc) { if (logger?.IsEnabled(LogLevel.Warning) ?? false) - logger.Log(LogLevel.Warning, exc, "An error occurred while converting a BackplaneMessage into a RedisValue"); + logger.Log(LogLevel.Warning, exc, "FUSION: an error occurred while converting a BackplaneMessage into a RedisValue"); } return RedisValue.Null; diff --git a/src/ZiggyCreatures.FusionCache.Backplane.StackExchangeRedis/ZiggyCreatures.FusionCache.Backplane.StackExchangeRedis.csproj b/src/ZiggyCreatures.FusionCache.Backplane.StackExchangeRedis/ZiggyCreatures.FusionCache.Backplane.StackExchangeRedis.csproj index 59e44780..33b131e0 100644 --- a/src/ZiggyCreatures.FusionCache.Backplane.StackExchangeRedis/ZiggyCreatures.FusionCache.Backplane.StackExchangeRedis.csproj +++ b/src/ZiggyCreatures.FusionCache.Backplane.StackExchangeRedis/ZiggyCreatures.FusionCache.Backplane.StackExchangeRedis.csproj @@ -4,7 +4,7 @@ netstandard2.0 latest enable - 0.21.0 + 0.22.0 ZiggyCreatures.FusionCache.Backplane.StackExchangeRedis logo-128x128.png FusionCache backplane for Redis based on the StackExchange.Redis library @@ -12,7 +12,10 @@ ZiggyCreatures.Caching.Fusion.Backplane.StackExchangeRedis ZiggyCreatures.FusionCache.Backplane.StackExchangeRedis.xml README.md - Dependencies update + + - Better log messages + - Dependencies update + @@ -25,7 +28,7 @@ - + diff --git a/src/ZiggyCreatures.FusionCache.Chaos/ZiggyCreatures.FusionCache.Chaos.csproj b/src/ZiggyCreatures.FusionCache.Chaos/ZiggyCreatures.FusionCache.Chaos.csproj index 36af8995..a4789f0f 100644 --- a/src/ZiggyCreatures.FusionCache.Chaos/ZiggyCreatures.FusionCache.Chaos.csproj +++ b/src/ZiggyCreatures.FusionCache.Chaos/ZiggyCreatures.FusionCache.Chaos.csproj @@ -4,7 +4,7 @@ netstandard2.0 latest enable - 0.21.0 + 0.22.0 ZiggyCreatures.FusionCache.Chaos logo-128x128.png Chaos-related utilities and implementations of various componenets (like a distributed cache or a backplane), useful for things like testing dependent components' behavior in a controlled failing environment. diff --git a/src/ZiggyCreatures.FusionCache.Serialization.CysharpMemoryPack/Internals/SerializableFusionCacheDistributedEntry.cs b/src/ZiggyCreatures.FusionCache.Serialization.CysharpMemoryPack/Internals/SerializableFusionCacheDistributedEntry.cs index 89141809..953ee090 100644 --- a/src/ZiggyCreatures.FusionCache.Serialization.CysharpMemoryPack/Internals/SerializableFusionCacheDistributedEntry.cs +++ b/src/ZiggyCreatures.FusionCache.Serialization.CysharpMemoryPack/Internals/SerializableFusionCacheDistributedEntry.cs @@ -16,10 +16,13 @@ internal partial class SerializableFusionCacheDistributedEntry [MemoryPackAllowSerialize] public FusionCacheEntryMetadata? Metadata => Entry?.Metadata; + [MemoryPackInclude] + public long? Timestamp => Entry?.Timestamp; + [MemoryPackConstructor] - SerializableFusionCacheDistributedEntry(TValue value, FusionCacheEntryMetadata? metadata) + SerializableFusionCacheDistributedEntry(TValue value, FusionCacheEntryMetadata? metadata, long? timestamp) { - this.Entry = new FusionCacheDistributedEntry(value, metadata); + this.Entry = new FusionCacheDistributedEntry(value, metadata, timestamp); } public SerializableFusionCacheDistributedEntry(FusionCacheDistributedEntry? entry) @@ -45,6 +48,7 @@ public override void Deserialize(ref MemoryPackReader reader, scoped ref FusionC { if (reader.PeekIsNull()) { + reader.Advance(1); value = null; return; } diff --git a/src/ZiggyCreatures.FusionCache.Serialization.CysharpMemoryPack/Internals/SerializableFusionCacheEntryMetadata.cs b/src/ZiggyCreatures.FusionCache.Serialization.CysharpMemoryPack/Internals/SerializableFusionCacheEntryMetadata.cs index 8c12fd93..9ab53bfd 100644 --- a/src/ZiggyCreatures.FusionCache.Serialization.CysharpMemoryPack/Internals/SerializableFusionCacheEntryMetadata.cs +++ b/src/ZiggyCreatures.FusionCache.Serialization.CysharpMemoryPack/Internals/SerializableFusionCacheEntryMetadata.cs @@ -55,6 +55,7 @@ public override void Deserialize(ref MemoryPackReader reader, scoped ref FusionC { if (reader.PeekIsNull()) { + reader.Advance(1); value = null; return; } diff --git a/src/ZiggyCreatures.FusionCache.Serialization.CysharpMemoryPack/ZiggyCreatures.Caching.Fusion.Serialization.CysharpMemoryPack.xml b/src/ZiggyCreatures.FusionCache.Serialization.CysharpMemoryPack/ZiggyCreatures.Caching.Fusion.Serialization.CysharpMemoryPack.xml index b93f26fa..d4727069 100644 --- a/src/ZiggyCreatures.FusionCache.Serialization.CysharpMemoryPack/ZiggyCreatures.Caching.Fusion.Serialization.CysharpMemoryPack.xml +++ b/src/ZiggyCreatures.FusionCache.Serialization.CysharpMemoryPack/ZiggyCreatures.Caching.Fusion.Serialization.CysharpMemoryPack.xml @@ -33,6 +33,7 @@ TValue Value
ZiggyCreatures.Caching.Fusion.Internals.FusionCacheEntryMetadata Metadata
+ long? Timestamp
diff --git a/src/ZiggyCreatures.FusionCache.Serialization.CysharpMemoryPack/ZiggyCreatures.FusionCache.Serialization.CysharpMemoryPack.csproj b/src/ZiggyCreatures.FusionCache.Serialization.CysharpMemoryPack/ZiggyCreatures.FusionCache.Serialization.CysharpMemoryPack.csproj index 1cc7db64..e1b61a87 100644 --- a/src/ZiggyCreatures.FusionCache.Serialization.CysharpMemoryPack/ZiggyCreatures.FusionCache.Serialization.CysharpMemoryPack.csproj +++ b/src/ZiggyCreatures.FusionCache.Serialization.CysharpMemoryPack/ZiggyCreatures.FusionCache.Serialization.CysharpMemoryPack.csproj @@ -4,7 +4,7 @@ netstandard2.1;net7.0 latest enable - 0.21.0 + 0.22.0 ZiggyCreatures.FusionCache.Serialization.CysharpMemoryPack logo-128x128.png FusionCache serializer based on Cysharp's MemoryPack serializer @@ -13,8 +13,8 @@ ZiggyCreatures.Caching.Fusion.Serialization.CysharpMemoryPack.xml README.md - - Added: Eager Refresh support - - Added: Conditional Refresh support + - Added: Timestamping support + - Dependencies update @@ -24,7 +24,7 @@
- + diff --git a/src/ZiggyCreatures.FusionCache.Serialization.NeueccMessagePack/ZiggyCreatures.FusionCache.Serialization.NeueccMessagePack.csproj b/src/ZiggyCreatures.FusionCache.Serialization.NeueccMessagePack/ZiggyCreatures.FusionCache.Serialization.NeueccMessagePack.csproj index dc3d8861..ae9db4b3 100644 --- a/src/ZiggyCreatures.FusionCache.Serialization.NeueccMessagePack/ZiggyCreatures.FusionCache.Serialization.NeueccMessagePack.csproj +++ b/src/ZiggyCreatures.FusionCache.Serialization.NeueccMessagePack/ZiggyCreatures.FusionCache.Serialization.NeueccMessagePack.csproj @@ -4,7 +4,7 @@ netstandard2.0 latest enable - 0.21.0 + 0.22.0 ZiggyCreatures.FusionCache.Serialization.NeueccMessagePack logo-128x128.png FusionCache serializer based on Neuecc's MessagePack serializer @@ -12,7 +12,10 @@ ZiggyCreatures.FusionCache.Serialization.NeueccMessagePack ZiggyCreatures.FusionCache.Serialization.NeueccMessagePack.xml README.md - Dependencies update + + - Added: Timestamping support + - Dependencies update + @@ -21,7 +24,7 @@ - + diff --git a/src/ZiggyCreatures.FusionCache.Serialization.NewtonsoftJson/ZiggyCreatures.FusionCache.Serialization.NewtonsoftJson.csproj b/src/ZiggyCreatures.FusionCache.Serialization.NewtonsoftJson/ZiggyCreatures.FusionCache.Serialization.NewtonsoftJson.csproj index 43e3835b..16f3b592 100644 --- a/src/ZiggyCreatures.FusionCache.Serialization.NewtonsoftJson/ZiggyCreatures.FusionCache.Serialization.NewtonsoftJson.csproj +++ b/src/ZiggyCreatures.FusionCache.Serialization.NewtonsoftJson/ZiggyCreatures.FusionCache.Serialization.NewtonsoftJson.csproj @@ -4,7 +4,7 @@ netstandard2.0 latest enable - 0.21.0 + 0.22.0 ZiggyCreatures.FusionCache.Serialization.NewtonsoftJson logo-128x128.png FusionCache serializer based on Newtonsoft Json.NET @@ -12,7 +12,10 @@ ZiggyCreatures.FusionCache.Serialization.NewtonsoftJson ZiggyCreatures.FusionCache.Serialization.NewtonsoftJson.xml README.md - Dependencies update + + - Added: Timestamping support + - Dependencies update + diff --git a/src/ZiggyCreatures.FusionCache.Serialization.ProtoBufNet/FusionCacheProtoBufNetSerializer.cs b/src/ZiggyCreatures.FusionCache.Serialization.ProtoBufNet/FusionCacheProtoBufNetSerializer.cs index 834aa865..528813e8 100644 --- a/src/ZiggyCreatures.FusionCache.Serialization.ProtoBufNet/FusionCacheProtoBufNetSerializer.cs +++ b/src/ZiggyCreatures.FusionCache.Serialization.ProtoBufNet/FusionCacheProtoBufNetSerializer.cs @@ -97,6 +97,7 @@ private void MaybeRegisterDistributedEntryModel() _model.Add(t, false) .Add(1, nameof(FusionCacheDistributedEntry.Value)) .Add(2, nameof(FusionCacheDistributedEntry.Metadata)) + .Add(3, nameof(FusionCacheDistributedEntry.Timestamp)) ; tmp.Add(t); diff --git a/src/ZiggyCreatures.FusionCache.Serialization.ProtoBufNet/ZiggyCreatures.FusionCache.Serialization.ProtoBufNet.csproj b/src/ZiggyCreatures.FusionCache.Serialization.ProtoBufNet/ZiggyCreatures.FusionCache.Serialization.ProtoBufNet.csproj index 57508ec3..92d23750 100644 --- a/src/ZiggyCreatures.FusionCache.Serialization.ProtoBufNet/ZiggyCreatures.FusionCache.Serialization.ProtoBufNet.csproj +++ b/src/ZiggyCreatures.FusionCache.Serialization.ProtoBufNet/ZiggyCreatures.FusionCache.Serialization.ProtoBufNet.csproj @@ -4,7 +4,7 @@ netstandard2.0 latest enable - 0.21.0 + 0.22.0 ZiggyCreatures.FusionCache.Serialization.ProtoBufNet logo-128x128.png FusionCache serializer based on protobuf-net @@ -13,8 +13,8 @@ ZiggyCreatures.FusionCache.Serialization.ProtoBufNet.xml README.md - - Added: Eager Refresh support - - Added: Conditional Refresh support + - Added: Timestamping support + - Dependencies update @@ -24,7 +24,7 @@ - + diff --git a/src/ZiggyCreatures.FusionCache.Serialization.ServiceStackJson/ZiggyCreatures.FusionCache.Serialization.ServiceStackJson.csproj b/src/ZiggyCreatures.FusionCache.Serialization.ServiceStackJson/ZiggyCreatures.FusionCache.Serialization.ServiceStackJson.csproj index c19d1af8..4d4531af 100644 --- a/src/ZiggyCreatures.FusionCache.Serialization.ServiceStackJson/ZiggyCreatures.FusionCache.Serialization.ServiceStackJson.csproj +++ b/src/ZiggyCreatures.FusionCache.Serialization.ServiceStackJson/ZiggyCreatures.FusionCache.Serialization.ServiceStackJson.csproj @@ -4,7 +4,7 @@ netstandard2.0 latest enable - 0.21.0 + 0.22.0 ZiggyCreatures.FusionCache.Serialization.ServiceStackJson logo-128x128.png FusionCache serializer based on ServiceStack's Json serializer @@ -12,7 +12,10 @@ ZiggyCreatures.FusionCache.Serialization.ServiceStackJson ZiggyCreatures.FusionCache.Serialization.ServiceStackJson.xml README.md - Dependencies update + + - Added: Timestamping support + - Dependencies update + @@ -25,7 +28,7 @@ - + diff --git a/src/ZiggyCreatures.FusionCache.Serialization.SystemTextJson/FusionCacheSystemTextJsonSerializer.cs b/src/ZiggyCreatures.FusionCache.Serialization.SystemTextJson/FusionCacheSystemTextJsonSerializer.cs index 0d417894..24313688 100644 --- a/src/ZiggyCreatures.FusionCache.Serialization.SystemTextJson/FusionCacheSystemTextJsonSerializer.cs +++ b/src/ZiggyCreatures.FusionCache.Serialization.SystemTextJson/FusionCacheSystemTextJsonSerializer.cs @@ -10,7 +10,6 @@ namespace ZiggyCreatures.Caching.Fusion.Serialization.SystemTextJson; public class FusionCacheSystemTextJsonSerializer : IFusionCacheSerializer { - /// /// Create a new instance of a object. /// @@ -52,6 +51,4 @@ public async ValueTask SerializeAsync(T? obj) return await JsonSerializer.DeserializeAsync(stream, Options); } } - } - diff --git a/src/ZiggyCreatures.FusionCache.Serialization.SystemTextJson/ZiggyCreatures.FusionCache.Serialization.SystemTextJson.csproj b/src/ZiggyCreatures.FusionCache.Serialization.SystemTextJson/ZiggyCreatures.FusionCache.Serialization.SystemTextJson.csproj index 3cc53af2..8fec5e64 100644 --- a/src/ZiggyCreatures.FusionCache.Serialization.SystemTextJson/ZiggyCreatures.FusionCache.Serialization.SystemTextJson.csproj +++ b/src/ZiggyCreatures.FusionCache.Serialization.SystemTextJson/ZiggyCreatures.FusionCache.Serialization.SystemTextJson.csproj @@ -4,7 +4,7 @@ netstandard2.0 latest enable - 0.21.0 + 0.22.0 ZiggyCreatures.FusionCache.Serialization.SystemTextJson logo-128x128.png FusionCache serializer based on System.Text.Json @@ -12,7 +12,10 @@ ZiggyCreatures.FusionCache.Serialization.SystemTextJson ZiggyCreatures.FusionCache.Serialization.SystemTextJson.xml README.md - Dependencies update + + - Added: Timestamping support + - Dependencies update + diff --git a/src/ZiggyCreatures.FusionCache/Backplane/BackplaneMessage.cs b/src/ZiggyCreatures.FusionCache/Backplane/BackplaneMessage.cs index 3e7a5522..4fffce7a 100644 --- a/src/ZiggyCreatures.FusionCache/Backplane/BackplaneMessage.cs +++ b/src/ZiggyCreatures.FusionCache/Backplane/BackplaneMessage.cs @@ -53,6 +53,7 @@ public bool IsValid() { case BackplaneMessageAction.EntrySet: case BackplaneMessageAction.EntryRemove: + case BackplaneMessageAction.EntryExpire: if (string.IsNullOrEmpty(CacheKey)) return false; return true; @@ -62,7 +63,7 @@ public bool IsValid() } /// - /// Creates a message for a single cache entry set operation (via either a Set or a GetOrSet method call). + /// Creates a message for a single cache entry set operation (via either a Set() or a GetOrSet() method call). /// /// The cache key. /// The message. @@ -79,7 +80,7 @@ public static BackplaneMessage CreateForEntrySet(string cacheKey) } /// - /// Creates a message for a single cache entry remove (via a Remove method call). + /// Creates a message for a single cache entry remove (via a Remove() method call). /// /// The cache key. /// The message. @@ -94,4 +95,21 @@ public static BackplaneMessage CreateForEntryRemove(string cacheKey) CacheKey = cacheKey }; } + + /// + /// Creates a message for a single cache entry expire operation (via an Expire() method call). + /// + /// The cache key. + /// The message. + public static BackplaneMessage CreateForEntryExpire(string cacheKey) + { + if (string.IsNullOrEmpty(cacheKey)) + throw new ArgumentException("The cache key cannot be null or empty", nameof(cacheKey)); + + return new BackplaneMessage() + { + Action = BackplaneMessageAction.EntryExpire, + CacheKey = cacheKey + }; + } } diff --git a/src/ZiggyCreatures.FusionCache/Backplane/BackplaneMessageAction.cs b/src/ZiggyCreatures.FusionCache/Backplane/BackplaneMessageAction.cs index 06914db2..918188e4 100644 --- a/src/ZiggyCreatures.FusionCache/Backplane/BackplaneMessageAction.cs +++ b/src/ZiggyCreatures.FusionCache/Backplane/BackplaneMessageAction.cs @@ -10,11 +10,15 @@ public enum BackplaneMessageAction : byte /// Unknown = 0, /// - /// A cache entry has been set (via either a Set or a GetOrSet method call). + /// A cache entry has been set (via either a Set() or a GetOrSet() method call). /// EntrySet = 1, /// - /// A cache entry has been removed (via a Remove method call). + /// A cache entry has been removed (via a Remove() method call). /// - EntryRemove = 2 + EntryRemove = 2, + /// + /// A cache entry has been manually expired (via an Expire() method call). + /// + EntryExpire = 3 } diff --git a/src/ZiggyCreatures.FusionCache/Events/FusionCacheEventsHub.cs b/src/ZiggyCreatures.FusionCache/Events/FusionCacheEventsHub.cs index 65f113a9..a70ae6b6 100644 --- a/src/ZiggyCreatures.FusionCache/Events/FusionCacheEventsHub.cs +++ b/src/ZiggyCreatures.FusionCache/Events/FusionCacheEventsHub.cs @@ -74,6 +74,11 @@ public FusionCacheEventsHub(IFusionCache cache, FusionCacheOptions options, ILog /// public event EventHandler? EagerRefresh; + /// + /// The event for a manual cache Expire() call. + /// + public event EventHandler? Expire; + internal void OnFailSafeActivate(string operationId, string key) { FailSafeActivate?.SafeExecute(operationId, key, _cache, () => new FusionCacheEntryEventArgs(key), nameof(FailSafeActivate), _logger, _errorsLogLevel, _syncExecution); @@ -108,4 +113,9 @@ internal void OnEagerRefresh(string operationId, string key) { EagerRefresh?.SafeExecute(operationId, key, _cache, () => new FusionCacheEntryEventArgs(key), nameof(EagerRefresh), _logger, _errorsLogLevel, _syncExecution); } + + internal void OnExpire(string operationId, string key) + { + Expire?.SafeExecute(operationId, key, _cache, () => new FusionCacheEntryEventArgs(key), nameof(Expire), _logger, _errorsLogLevel, _syncExecution); + } } diff --git a/src/ZiggyCreatures.FusionCache/Events/FusionCacheMemoryEventsHub.cs b/src/ZiggyCreatures.FusionCache/Events/FusionCacheMemoryEventsHub.cs index 5f3aa82c..cd75a4a2 100644 --- a/src/ZiggyCreatures.FusionCache/Events/FusionCacheMemoryEventsHub.cs +++ b/src/ZiggyCreatures.FusionCache/Events/FusionCacheMemoryEventsHub.cs @@ -27,6 +27,11 @@ public FusionCacheMemoryEventsHub(IFusionCache cache, FusionCacheOptions options /// public event EventHandler? Eviction; + /// + /// The event for a manual cache Expire() call. + /// + public event EventHandler? Expire; + /// /// Check if the event has subscribers or not. /// @@ -40,4 +45,9 @@ internal void OnEviction(string operationId, string key, EvictionReason reason) { Eviction?.SafeExecute(operationId, key, _cache, () => new FusionCacheEntryEvictionEventArgs(key, reason), nameof(Eviction), _logger, _errorsLogLevel, _syncExecution); } + + internal void OnExpire(string operationId, string key) + { + Expire?.SafeExecute(operationId, key, _cache, () => new FusionCacheEntryEventArgs(key), nameof(Expire), _logger, _errorsLogLevel, _syncExecution); + } } diff --git a/src/ZiggyCreatures.FusionCache/FusionCache.cs b/src/ZiggyCreatures.FusionCache/FusionCache.cs index 7c3b07ce..bbb52940 100644 --- a/src/ZiggyCreatures.FusionCache/FusionCache.cs +++ b/src/ZiggyCreatures.FusionCache/FusionCache.cs @@ -21,7 +21,9 @@ namespace ZiggyCreatures.Caching.Fusion; -/// +/// +/// The standard implementation of . +/// [DebuggerDisplay("NAME: {_options.CacheName} - ID: {InstanceId} - DC: {HasDistributedCache} - BP: {HasBackplane}")] public partial class FusionCache : IFusionCache @@ -87,6 +89,9 @@ public FusionCache(IOptions optionsAccessor, IMemoryCache? m // BACKPLANE _bpa = null; + + if (_logger?.IsEnabled(LogLevel.Trace) ?? false) + _logger.Log(LogLevel.Trace, "FUSION [N={CacheName} I={CacheInstanceId}]: instance created", _options.CacheName, InstanceId); } /// @@ -129,126 +134,166 @@ private string GenerateOperationId() return FusionCacheInternalUtils.MaybeGenerateOperationId(_logger); } + private MemoryCacheAccessor? GetCurrentMemoryAccessor(FusionCacheEntryOptions options) + { + return options.SkipMemoryCache ? null : _mca; + } + private DistributedCacheAccessor? GetCurrentDistributedAccessor(FusionCacheEntryOptions options) { return options.SkipDistributedCache ? null : _dca; } - private IFusionCacheEntry? MaybeGetFallbackEntry(string operationId, string key, FusionCacheDistributedEntry? distributedEntry, FusionCacheMemoryEntry? memoryEntry, FusionCacheEntryOptions options, bool allowFailSafeActivation, out bool failSafeActivated) + private bool TryPickFailSafeFallbackValue(string operationId, string key, FusionCacheDistributedEntry? distributedEntry, FusionCacheMemoryEntry? memoryEntry, MaybeValue failSafeDefaultValue, FusionCacheEntryOptions options, out MaybeValue value, out long? timestamp, out bool failSafeActivated) { - failSafeActivated = false; - - if (options.IsFailSafeEnabled) + // FAIL-SAFE NOT ENABLED + if (options.IsFailSafeEnabled == false) { - if (allowFailSafeActivation) - if (_logger?.IsEnabled(LogLevel.Trace) ?? false) - _logger.LogTrace("FUSION (O={CacheOperationId} K={CacheKey}): trying to activate FAIL-SAFE", operationId, key); - if (distributedEntry is not null) - { - if (allowFailSafeActivation) - { - // FAIL SAFE (FROM DISTRIBUTED) - if (_logger?.IsEnabled(_options.FailSafeActivationLogLevel) ?? false) - _logger.Log(_options.FailSafeActivationLogLevel, "FUSION (O={CacheOperationId} K={CacheKey}): FAIL-SAFE activated (from distributed)", operationId, key); - failSafeActivated = true; + if (_logger?.IsEnabled(LogLevel.Trace) ?? false) + _logger.Log(LogLevel.Trace, "FUSION [N={CacheName}] (O={CacheOperationId} K={CacheKey}): FAIL-SAFE not enabled", CacheName, operationId, key); - // EVENT - _events.OnFailSafeActivate(operationId, key); - } + value = default; + timestamp = default; + failSafeActivated = false; + return false; + } - return distributedEntry; - } - else if (memoryEntry is not null) - { - if (allowFailSafeActivation) - { - // FAIL SAFE (FROM MEMORY) - if (_logger?.IsEnabled(_options.FailSafeActivationLogLevel) ?? false) - _logger.Log(_options.FailSafeActivationLogLevel, "FUSION (O={CacheOperationId} K={CacheKey}): FAIL-SAFE activated (from memory)", operationId, key); - failSafeActivated = true; + // FAIL-SAFE ENABLED + if (_logger?.IsEnabled(LogLevel.Trace) ?? false) + _logger.Log(LogLevel.Trace, "FUSION [N={CacheName}] (O={CacheOperationId} K={CacheKey}): trying to activate FAIL-SAFE", CacheName, operationId, key); - // EVENT - _events.OnFailSafeActivate(operationId, key); - } + // TRY TO PICK A FALLBACK ENTRY + IFusionCacheEntry? entry; + //if (distributedEntry is not null && (memoryEntry is null || distributedEntry.Timestamp > memoryEntry.Timestamp)) + if (distributedEntry is not null) + entry = distributedEntry; + else + entry = memoryEntry; - return memoryEntry; - } - else - { - if (allowFailSafeActivation) - if (_logger?.IsEnabled(LogLevel.Trace) ?? false) - _logger.LogTrace("FUSION (O={CacheOperationId} K={CacheKey}): unable to activate FAIL-SAFE (no entries in memory or distributed)", operationId, key); - return null; - } + if (entry is not null) + { + // ACTIVATE FAIL-SAFE + value = entry.GetValue(); + timestamp = entry.Timestamp; + failSafeActivated = true; + + if (_logger?.IsEnabled(_options.FailSafeActivationLogLevel) ?? false) + _logger.Log(_options.FailSafeActivationLogLevel, "FUSION [N={CacheName}] (O={CacheOperationId} K={CacheKey}): FAIL-SAFE activated (from " + (entry is FusionCacheMemoryEntry ? "memory" : "distributed") + ")", CacheName, operationId, key); + + // EVENT + _events.OnFailSafeActivate(operationId, key); + + return true; } - else + + // TRY WITH THE FAIL-SAFE DEFAULT VALUE + if (failSafeDefaultValue.HasValue) { - if (allowFailSafeActivation) - if (_logger?.IsEnabled(LogLevel.Trace) ?? false) - _logger.LogTrace("FUSION (O={CacheOperationId} K={CacheKey}): FAIL-SAFE not enabled", operationId, key); - return null; + // ACTIVATE FAIL-SAFE + value = failSafeDefaultValue.Value; + timestamp = null; + failSafeActivated = true; + + if (_logger?.IsEnabled(_options.FailSafeActivationLogLevel) ?? false) + _logger.Log(_options.FailSafeActivationLogLevel, "FUSION [N={CacheName}] (O={CacheOperationId} K={CacheKey}): FAIL-SAFE activated (from fail-safe default value)", CacheName, operationId, key); + + // EVENT + _events.OnFailSafeActivate(operationId, key); + + return true; } + + // UNABLE TO ACTIVATE FAIL-SAFE + if (_logger?.IsEnabled(LogLevel.Trace) ?? false) + _logger.Log(LogLevel.Trace, "FUSION [N={CacheName}] (O={CacheOperationId} K={CacheKey}): unable to activate FAIL-SAFE (no entries in memory or distributed)", CacheName, operationId, key); + + value = default; + timestamp = default; + failSafeActivated = false; + return false; } //[MethodImpl(MethodImplOptions.AggressiveInlining)] - private void MaybeBackgroundCompleteTimedOutFactory(string operationId, string key, FusionCacheFactoryExecutionContext ctx, Task? factoryTask, FusionCacheEntryOptions options, DistributedCacheAccessor? dca, CancellationToken token) + private void MaybeBackgroundCompleteTimedOutFactory(string operationId, string key, FusionCacheFactoryExecutionContext ctx, Task? factoryTask, FusionCacheEntryOptions options, CancellationToken token) { if (factoryTask is null || options.AllowTimedOutFactoryBackgroundCompletion == false) return; - CompleteBackgroundFactory(operationId, key, ctx, factoryTask, options, dca, null, token); + CompleteBackgroundFactory(operationId, key, ctx, factoryTask, options, null, token); } //[MethodImpl(MethodImplOptions.AggressiveInlining)] - private void CompleteBackgroundFactory(string operationId, string key, FusionCacheFactoryExecutionContext ctx, Task factoryTask, FusionCacheEntryOptions options, DistributedCacheAccessor? dca, object? lockObj, CancellationToken token) + private void CompleteBackgroundFactory(string operationId, string key, FusionCacheFactoryExecutionContext ctx, Task factoryTask, FusionCacheEntryOptions options, object? lockObj, CancellationToken token) { if (factoryTask.IsFaulted) { - if (_logger?.IsEnabled(_options.FactoryErrorsLogLevel) ?? false) - _logger.Log(_options.FactoryErrorsLogLevel, factoryTask.Exception.GetSingleInnerExceptionOrSelf(), "FUSION (O={CacheOperationId} K={CacheKey}): a background factory thrown an exception", operationId, key); - - // EVENT - _events.OnBackgroundFactoryError(operationId, key); + try + { + if (_logger?.IsEnabled(_options.FactoryErrorsLogLevel) ?? false) + _logger.Log(_options.FactoryErrorsLogLevel, factoryTask.Exception.GetSingleInnerExceptionOrSelf(), "FUSION [N={CacheName}] (O={CacheOperationId} K={CacheKey}): a background factory thrown an exception", CacheName, operationId, key); - ReleaseLock(operationId, key, lockObj); + // EVENT + _events.OnBackgroundFactoryError(operationId, key); + } + finally + { + ReleaseLock(operationId, key, lockObj); + } return; } // CONTINUE IN THE BACKGROUND TO TRY TO KEEP THE RESULT AS SOON AS IT WILL COMPLETE SUCCESSFULLY if (_logger?.IsEnabled(LogLevel.Debug) ?? false) - _logger.LogDebug("FUSION (O={CacheOperationId} K={CacheKey}): trying to complete a background factory", operationId, key); + _logger.Log(LogLevel.Debug, "FUSION [N={CacheName}] (O={CacheOperationId} K={CacheKey}): trying to complete a background factory", CacheName, operationId, key); _ = factoryTask.ContinueWith(antecedent => { - if (antecedent.Status == TaskStatus.Faulted) + try { - if (_logger?.IsEnabled(_options.FactoryErrorsLogLevel) ?? false) - _logger.Log(_options.FactoryErrorsLogLevel, antecedent.Exception.GetSingleInnerExceptionOrSelf(), "FUSION (O={CacheOperationId} K={CacheKey}): a background factory thrown an exception", operationId, key); + if (antecedent.Status == TaskStatus.Faulted) + { + if (_logger?.IsEnabled(_options.FactoryErrorsLogLevel) ?? false) + _logger.Log(_options.FactoryErrorsLogLevel, antecedent.Exception.GetSingleInnerExceptionOrSelf(), "FUSION [N={CacheName}] (O={CacheOperationId} K={CacheKey}): a background factory thrown an exception", CacheName, operationId, key); - // EVENT - _events.OnBackgroundFactoryError(operationId, key); - } - else if (antecedent.Status == TaskStatus.RanToCompletion) - { - if (_logger?.IsEnabled(LogLevel.Debug) ?? false) - _logger.LogDebug("FUSION (O={CacheOperationId} K={CacheKey}): a background factory successfully completed, keeping the result", operationId, key); + // EVENT + _events.OnBackgroundFactoryError(operationId, key); + } + else if (antecedent.Status == TaskStatus.RanToCompletion) + { + if (_logger?.IsEnabled(LogLevel.Debug) ?? false) + _logger.Log(LogLevel.Debug, "FUSION [N={CacheName}] (O={CacheOperationId} K={CacheKey}): a background factory successfully completed, keeping the result", CacheName, operationId, key); - // UPDATE ADAPTIVE OPTIONS - var maybeNewOptions = ctx.GetOptions(); - if (maybeNewOptions is not null && options != maybeNewOptions) - options = maybeNewOptions; + // UPDATE ADAPTIVE OPTIONS + var maybeNewOptions = ctx.GetOptions(); + if (maybeNewOptions is not null && options != maybeNewOptions) + options = maybeNewOptions; - var lateEntry = FusionCacheMemoryEntry.CreateFromOptions(antecedent.Result, options, false, ctx.LastModified, ctx.ETag); - _ = dca?.SetEntryAsync(operationId, key, lateEntry, options, token); - _mca.SetEntry(operationId, key, lateEntry, options); + // ADAPTIVE CACHING UPDATE + var dca = GetCurrentDistributedAccessor(options); + var mca = GetCurrentMemoryAccessor(options); - // EVENT - _events.OnBackgroundFactorySuccess(operationId, key); - _events.OnSet(operationId, key); - } + var lateEntry = FusionCacheMemoryEntry.CreateFromOptions(antecedent.Result, options, false, ctx.LastModified, ctx.ETag, null); + + if (dca.CanBeUsed(operationId, key)) + _ = dca?.SetEntryAsync(operationId, key, lateEntry, options, token); - ReleaseLock(operationId, key, lockObj); + if (mca is not null) + mca.SetEntry(operationId, key, lateEntry, options); + + // BACKPLANE + if (options.SkipBackplaneNotifications == false) + _ = PublishInternalAsync(operationId, BackplaneMessage.CreateForEntrySet(key), options, token); + + // EVENT + _events.OnBackgroundFactorySuccess(operationId, key); + _events.OnSet(operationId, key); + } + } + finally + { + ReleaseLock(operationId, key, lockObj); + } }); } @@ -259,19 +304,19 @@ private void ReleaseLock(string operationId, string key, object? lockObj) return; if (_logger?.IsEnabled(LogLevel.Trace) ?? false) - _logger.LogTrace("FUSION (O={CacheOperationId} K={CacheKey}): releasing LOCK", operationId, key); + _logger.Log(LogLevel.Trace, "FUSION [N={CacheName}] (O={CacheOperationId} K={CacheKey}): releasing LOCK", CacheName, operationId, key); try { - _reactor.ReleaseLock(key, operationId, lockObj, _logger); + _reactor.ReleaseLock(CacheName, key, operationId, lockObj, _logger); if (_logger?.IsEnabled(LogLevel.Trace) ?? false) - _logger.LogTrace("FUSION (O={CacheOperationId} K={CacheKey}): LOCK released", operationId, key); + _logger.Log(LogLevel.Trace, "FUSION [N={CacheName}] (O={CacheOperationId} K={CacheKey}): LOCK released", CacheName, operationId, key); } catch (Exception exc) { if (_logger?.IsEnabled(LogLevel.Warning) ?? false) - _logger.LogWarning(exc, "FUSION (O={CacheOperationId} K={CacheKey}): releasing the LOCK has thrown an exception", operationId, key); + _logger.Log(LogLevel.Warning, exc, "FUSION [N={CacheName}] (O={CacheOperationId} K={CacheKey}): releasing the LOCK has thrown an exception", CacheName, operationId, key); } } @@ -281,7 +326,7 @@ private void ProcessFactoryError(string operationId, string key, Exception exc) if (exc is SyntheticTimeoutException) { if (_logger?.IsEnabled(_options.FactorySyntheticTimeoutsLogLevel) ?? false) - _logger.Log(_options.FactorySyntheticTimeoutsLogLevel, exc, "FUSION (O={CacheOperationId} K={CacheKey}): a synthetic timeout occurred while calling the factory", operationId, key); + _logger.Log(_options.FactorySyntheticTimeoutsLogLevel, exc, "FUSION [N={CacheName}] (O={CacheOperationId} K={CacheKey}): a synthetic timeout occurred while calling the factory", CacheName, operationId, key); // EVENT _events.OnFactorySyntheticTimeout(operationId, key); @@ -290,13 +335,13 @@ private void ProcessFactoryError(string operationId, string key, Exception exc) } if (_logger?.IsEnabled(_options.FactoryErrorsLogLevel) ?? false) - _logger.Log(_options.FactoryErrorsLogLevel, exc, "FUSION (O={CacheOperationId} K={CacheKey}): an error occurred while calling the factory", operationId, key); + _logger.Log(_options.FactoryErrorsLogLevel, exc, "FUSION [N={CacheName}] (O={CacheOperationId} K={CacheKey}): an error occurred while calling the factory", CacheName, operationId, key); // EVENT _events.OnFactoryError(operationId, key); } - internal void EvictInternal(string key, bool allowFailSafe) + internal void ExpireMemoryEntryInternal(string key, bool allowFailSafe) { ValidateCacheKey(key); @@ -306,9 +351,9 @@ internal void EvictInternal(string key, bool allowFailSafe) var operationId = GenerateOperationId(); if (_logger?.IsEnabled(LogLevel.Debug) ?? false) - _logger.LogDebug("FUSION (O={CacheOperationId} K={CacheKey}): calling Evict", operationId, key); + _logger.Log(LogLevel.Debug, "FUSION [N={CacheName}] (O={CacheOperationId} K={CacheKey}): calling ExpireMemoryInternal (allowFailSafe={AllowFailSafe})", CacheName, operationId, key, allowFailSafe); - _mca.EvictEntry(operationId, key, allowFailSafe); + _mca.ExpireEntry(operationId, key, allowFailSafe); } /// @@ -323,7 +368,7 @@ public IFusionCache SetupDistributedCache(IDistributedCache distributedCache, IF _dca = new DistributedCacheAccessor(distributedCache, serializer, _options, _logger, _events.Distributed); if (_logger?.IsEnabled(LogLevel.Debug) ?? false) - _logger.LogDebug("FUSION: setup distributed cache (CACHE={DistributedCacheType} SERIALIZER={SerializerType})", distributedCache.GetType().FullName, serializer.GetType().FullName); + _logger.Log(LogLevel.Debug, "FUSION [N={CacheName}]: setup distributed cache (CACHE={DistributedCacheType} SERIALIZER={SerializerType})", CacheName, distributedCache.GetType().FullName, serializer.GetType().FullName); return this; } @@ -334,7 +379,7 @@ public IFusionCache RemoveDistributedCache() _dca = null; if (_logger?.IsEnabled(LogLevel.Debug) ?? false) - _logger.LogDebug("FUSION: distributed cache removed"); + _logger.Log(LogLevel.Debug, "FUSION [N={CacheName}]: distributed cache removed", CacheName); return this; } @@ -362,7 +407,7 @@ public IFusionCache SetupBackplane(IFusionCacheBackplane backplane) _bpa.Subscribe(); if (_logger?.IsEnabled(LogLevel.Debug) ?? false) - _logger.LogDebug("FUSION: setup backplane (BACKPLANE={BackplaneType})", backplane.GetType().FullName); + _logger.Log(LogLevel.Debug, "FUSION [N={CacheName}]: setup backplane (BACKPLANE={BackplaneType})", CacheName, backplane.GetType().FullName); } // CHECK: WARN THE USER IN CASE OF @@ -373,7 +418,7 @@ public IFusionCache SetupBackplane(IFusionCacheBackplane backplane) if (HasBackplane && HasDistributedCache == false && DefaultEntryOptions.SkipBackplaneNotifications == false) { if (_logger?.IsEnabled(LogLevel.Warning) ?? false) - _logger.LogWarning("FUSION: it has been detected a situation where there *IS* a backplane, there is *NOT* a distributed cache and the DefaultEntryOptions.SkipBackplaneNotifications option is set to false. This will probably cause problems, since a notification will be sent automatically at every change in the cache but there is not a shared state (a distributed cache) that different nodes can use, basically resulting in a situation where the cache will keep invalidating itself at every change. It is suggested to either (1) add a distributed cache or (2) change the DefaultEntryOptions.SkipBackplaneNotifications to true.", backplane.GetType().FullName); + _logger.Log(LogLevel.Warning, "FUSION [N={CacheName}]: it has been detected a situation where there *IS* a backplane, there is *NOT* a distributed cache and the DefaultEntryOptions.SkipBackplaneNotifications option is set to false. This will probably cause problems, since a notification will be sent automatically at every change in the cache but there is not a shared state (a distributed cache) that different nodes can use, basically resulting in a situation where the cache will keep invalidating itself at every change. It is suggested to either (1) add a distributed cache or (2) change the DefaultEntryOptions.SkipBackplaneNotifications to true.", CacheName, backplane.GetType().FullName); } return this; @@ -390,7 +435,7 @@ public IFusionCache RemoveBackplane() _bpa = null; if (_logger?.IsEnabled(LogLevel.Debug) ?? false) - _logger.LogDebug("FUSION: backplane removed"); + _logger.Log(LogLevel.Debug, "FUSION [N={CacheName}]: backplane removed", CacheName); } } @@ -418,9 +463,9 @@ public void AddPlugin(IFusionCachePlugin plugin) if (_plugins.Contains(plugin)) { if (_logger?.IsEnabled(_options.PluginsErrorsLogLevel) ?? false) - _logger?.Log(_options.PluginsErrorsLogLevel, "FUSION: the same plugin instance already exists (TYPE={PluginType})", plugin.GetType().FullName); + _logger?.Log(_options.PluginsErrorsLogLevel, "FUSION [N={CacheName}]: the same plugin instance already exists (TYPE={PluginType})", CacheName, plugin.GetType().FullName); - throw new InvalidOperationException($"FUSION: the same plugin instance already exists (TYPE={plugin.GetType().FullName})"); + throw new InvalidOperationException($"FUSION [N={CacheName}]: the same plugin instance already exists (TYPE={plugin.GetType().FullName})"); } _plugins.Add(plugin); @@ -439,13 +484,13 @@ public void AddPlugin(IFusionCachePlugin plugin) } if (_logger?.IsEnabled(_options.PluginsErrorsLogLevel) ?? false) - _logger.Log(_options.PluginsErrorsLogLevel, exc, "FUSION: an error occurred while starting a plugin (TYPE={PluginType})", plugin.GetType().FullName); + _logger.Log(_options.PluginsErrorsLogLevel, exc, "FUSION [N={CacheName}]: an error occurred while starting a plugin (TYPE={PluginType})", CacheName, plugin.GetType().FullName); - throw new InvalidOperationException($"FUSION: an error occurred while starting a plugin (TYPE={plugin.GetType().FullName})", exc); + throw new InvalidOperationException($"FUSION [N={CacheName}]: an error occurred while starting a plugin (TYPE={plugin.GetType().FullName})", exc); } if (_logger?.IsEnabled(_options.PluginsInfoLogLevel) ?? false) - _logger?.Log(_options.PluginsInfoLogLevel, "FUSION: a plugin has been added and started (TYPE={PluginType})", plugin.GetType().FullName); + _logger?.Log(_options.PluginsInfoLogLevel, "FUSION [N={CacheName}]: a plugin has been added and started (TYPE={PluginType})", CacheName, plugin.GetType().FullName); } /// @@ -459,11 +504,11 @@ public bool RemovePlugin(IFusionCachePlugin plugin) if (_plugins.Contains(plugin) == false) { if (_logger?.IsEnabled(_options.PluginsErrorsLogLevel) ?? false) - _logger?.Log(_options.PluginsErrorsLogLevel, "FUSION: the plugin cannot be removed because is not part of this FusionCache instance (TYPE={PluginType})", plugin.GetType().FullName); + _logger?.Log(_options.PluginsErrorsLogLevel, "FUSION [N={CacheName}]: the plugin cannot be removed because is not part of this FusionCache instance (TYPE={PluginType})", CacheName, plugin.GetType().FullName); // MAYBE WE SHOULD THROW (LIKE IN AddPlugin) INSTEAD OF JUST RETURNING (LIKE IN List.Remove()) ? return false; - //throw new InvalidOperationException($"FUSION: the plugin cannot be removed because is not part of this FusionCache instance (TYPE={plugin.GetType().FullName})"); + //throw new InvalidOperationException($"FUSION [N={CacheName}]: the plugin cannot be removed because is not part of this FusionCache instance (TYPE={plugin.GetType().FullName})"); } // STOP THE PLUGIN @@ -474,9 +519,9 @@ public bool RemovePlugin(IFusionCachePlugin plugin) catch (Exception exc) { if (_logger?.IsEnabled(_options.PluginsErrorsLogLevel) ?? false) - _logger.Log(_options.PluginsErrorsLogLevel, exc, "FUSION: an error occurred while stopping a plugin (TYPE={PluginType})", plugin.GetType().FullName); + _logger.Log(_options.PluginsErrorsLogLevel, exc, "FUSION [N={CacheName}]: an error occurred while stopping a plugin (TYPE={PluginType})", CacheName, plugin.GetType().FullName); - throw new InvalidOperationException($"FUSION: an error occurred while stopping a plugin (TYPE={plugin.GetType().FullName})", exc); + throw new InvalidOperationException($"FUSION [N={CacheName}]: an error occurred while stopping a plugin (TYPE={plugin.GetType().FullName})", exc); } finally { @@ -486,7 +531,7 @@ public bool RemovePlugin(IFusionCachePlugin plugin) } if (_logger?.IsEnabled(_options.PluginsInfoLogLevel) ?? false) - _logger?.Log(_options.PluginsInfoLogLevel, "FUSION: a plugin has been stopped and removed (TYPE={PluginType})", plugin.GetType().FullName); + _logger?.Log(_options.PluginsInfoLogLevel, "FUSION [N={CacheName}]: a plugin has been stopped and removed (TYPE={PluginType})", CacheName, plugin.GetType().FullName); return true; } @@ -512,6 +557,8 @@ protected virtual void Dispose(bool disposing) if (disposing) { RemoveAllPlugins(); + RemoveBackplane(); + RemoveDistributedCache(); _reactor.Dispose(); _mca.Dispose(); } diff --git a/src/ZiggyCreatures.FusionCache/FusionCacheEntryOptions.cs b/src/ZiggyCreatures.FusionCache/FusionCacheEntryOptions.cs index 285221bd..3ca5ccce 100644 --- a/src/ZiggyCreatures.FusionCache/FusionCacheEntryOptions.cs +++ b/src/ZiggyCreatures.FusionCache/FusionCacheEntryOptions.cs @@ -46,6 +46,8 @@ public FusionCacheEntryOptions(TimeSpan? duration = null) SkipDistributedCache = FusionCacheGlobalDefaults.EntryOptionsSkipDistributedCache; SkipDistributedCacheReadWhenStale = FusionCacheGlobalDefaults.EntryOptionsSkipDistributedCacheReadWhenStale; + + SkipMemoryCache = FusionCacheGlobalDefaults.EntryOptionsSkipMemoryCache; } /// @@ -235,7 +237,7 @@ public float? EagerRefreshThreshold ///
/// OBSOLETE NOW: ///
- [Obsolete("Please use the SkipBackplaneNotifications option and invert the value: EnableBackplaneNotifications = true is the same as SkipBackplaneNotifications = false")] + [Obsolete("Please use the SkipBackplaneNotifications option and invert the value: EnableBackplaneNotifications = true is the same as SkipBackplaneNotifications = false", true)] public bool EnableBackplaneNotifications { get { return !SkipBackplaneNotifications; } @@ -280,6 +282,15 @@ public bool EnableBackplaneNotifications /// public bool SkipDistributedCacheReadWhenStale { get; set; } + /// + /// Skip the usage of the memory cache. + ///

+ /// NOTE: this option must be used very carefully and is generally not recommended, as it will not protect you from some problems like Cache Stampede. Also, it can lead to a lot of extra work for the 2nd layer (distributed cache) and a lot of extra network traffic. + ///

+ /// DOCS: + ///
+ public bool SkipMemoryCache { get; set; } + internal bool IsSafeForAdaptiveCaching { get; set; } /// @@ -330,6 +341,16 @@ public FusionCacheEntryOptions SetDuration(TimeSpan duration) return this; } + /// + /// Set the to be zero: this will effectively remove the entry from the cache if fail-safe is disabled, or it will set the entry as logically expired if fail-safe is enabled (so it can be used later as a fallback). + /// + /// The so that additional calls can be chained. + public FusionCacheEntryOptions SetDurationZero() + { + Duration = TimeSpan.Zero; + return this; + } + /// /// Set the to be infinite, so it will never expire. /// NOTE: the expiration will not be literally "infinite", but it will be set to which in turn is Dec 31st 9999 which, I mean, c'mon. If that time will come and you'll have some problems feel free to try and contact me :-) @@ -352,6 +373,16 @@ public FusionCacheEntryOptions SetDistributedCacheDuration(TimeSpan? duration) return this; } + /// + /// Set the to be zero: this will effectively remove the entry from the cache if fail-safe is disabled, or it will set the entry as logically expired if fail-safe is enabled (so it can be used later as a fallback). + /// + /// The so that additional calls can be chained. + public FusionCacheEntryOptions SetDistributedCacheDurationZero() + { + DistributedCacheDuration = TimeSpan.Zero; + return this; + } + /// /// Set the to be infinite, so it will never expire. /// NOTE: the expiration will not be literally "infinite", but it will be set to which in turn is Dec 31st 9999 which, I mean, c'mon. If that time will come and you'll have some problems feel free to try and contact me :-) @@ -501,7 +532,7 @@ public FusionCacheEntryOptions SetDistributedCacheTimeouts(TimeSpan? softTimeout /// /// Set the property. /// The so that additional calls can be chained. - [Obsolete("Please use the SetSkipBackplaneNotifications method and invert the value: EnableBackplaneNotifications = true is the same as SkipBackplaneNotifications = false")] + [Obsolete("Please use the SetSkipBackplaneNotifications method and invert the value: EnableBackplaneNotifications = true is the same as SkipBackplaneNotifications = false", true)] public FusionCacheEntryOptions SetBackplane(bool enableBackplaneNotifications) { return SetSkipBackplaneNotifications(!enableBackplaneNotifications); @@ -549,6 +580,21 @@ public FusionCacheEntryOptions SetSkipDistributedCacheReadWhenStale(bool skip) return this; } + /// + /// Set the option. + ///

+ /// NOTE: this option must be used very carefully and is generally not recommended, as it will not protect you from some problems like Cache Stampede. Also, it can lead to a lot of extra work for the 2nd layer (distributed cache) and a lot of extra network traffic. + ///

+ /// DOCS: + ///
+ /// The value for the property. + /// The so that additional calls can be chained. + public FusionCacheEntryOptions SetSkipMemoryCache(bool skip = true) + { + SkipMemoryCache = skip; + return this; + } + /// /// Creates a new instance based on this instance. /// @@ -598,7 +644,7 @@ internal MemoryCacheEntryOptions ToMemoryCacheEntryOptions(FusionCacheMemoryEven if (incoherentFailSafeMaxDuration) { if (logger?.IsEnabled(options.IncoherentOptionsNormalizationLogLevel) ?? false) - logger.Log(options.IncoherentOptionsNormalizationLogLevel, "FUSION (O={CacheOperationId} K={CacheKey}): FailSafeMaxDuration {{FailSafeMaxDuration}} was lower than the Duration {Duration} on {Options} {MemoryOptions}. Duration has been used instead.", operationId, key, FailSafeMaxDuration.ToLogString(), Duration.ToLogString(), this.ToLogString(), res.ToLogString()); + logger.Log(options.IncoherentOptionsNormalizationLogLevel, "FUSION [N={CacheName}] (O={CacheOperationId} K={CacheKey}): FailSafeMaxDuration {{FailSafeMaxDuration}} was lower than the Duration {Duration} on {Options} {MemoryOptions}. Duration has been used instead.", options.CacheName, operationId, key, FailSafeMaxDuration.ToLogString(), Duration.ToLogString(), this.ToLogString(), res.ToLogString()); } return res; @@ -644,7 +690,7 @@ internal DistributedCacheEntryOptions ToDistributedCacheEntryOptions(FusionCache if (incoherentFailSafeMaxDuration) { if (logger?.IsEnabled(options.IncoherentOptionsNormalizationLogLevel) ?? false) - logger.Log(options.IncoherentOptionsNormalizationLogLevel, "FUSION (O={CacheOperationId} K={CacheKey}): DistributedCacheFailSafeMaxDuration/FailSafeMaxDuration {{FailSafeMaxDuration}} was lower than the DistributedCache/Duration {Duration} on {Options} {MemoryOptions}. Duration has been used instead.", operationId, key, failSafeMaxDurationToUse.ToLogString(), durationToUse.ToLogString(), this.ToLogString(), res.ToLogString()); + logger.Log(options.IncoherentOptionsNormalizationLogLevel, "FUSION [N={CacheName}] (O={CacheOperationId} K={CacheKey}): DistributedCacheFailSafeMaxDuration/FailSafeMaxDuration {{FailSafeMaxDuration}} was lower than the DistributedCache/Duration {Duration} on {Options} {MemoryOptions}. Duration has been used instead.", options.CacheName, operationId, key, failSafeMaxDurationToUse.ToLogString(), durationToUse.ToLogString(), this.ToLogString(), res.ToLogString()); } return res; @@ -750,7 +796,9 @@ public FusionCacheEntryOptions Duplicate(TimeSpan? duration = null) SkipBackplaneNotifications = SkipBackplaneNotifications, SkipDistributedCache = SkipDistributedCache, - SkipDistributedCacheReadWhenStale = SkipDistributedCacheReadWhenStale + SkipDistributedCacheReadWhenStale = SkipDistributedCacheReadWhenStale, + + SkipMemoryCache = SkipMemoryCache, }; } diff --git a/src/ZiggyCreatures.FusionCache/FusionCacheExtMethods.cs b/src/ZiggyCreatures.FusionCache/FusionCacheExtMethods.cs index a57ae0ea..df0bbc5b 100644 --- a/src/ZiggyCreatures.FusionCache/FusionCacheExtMethods.cs +++ b/src/ZiggyCreatures.FusionCache/FusionCacheExtMethods.cs @@ -254,6 +254,55 @@ public static void Remove(this IFusionCache cache, string key, Action + /// Expires the cache entry for the specified . + ///
+ ///
+ /// In the memory cache: + ///
+ /// - if fail-safe is enabled: the entry will marked as logically expired, but will still be available as a fallback value in case of future problems + ///
+ /// - if fail-safe is disabled: the entry will be effectively removed + ///
+ ///
+ /// In the distributed cache (if any), the entry will be effectively removed. + ///
+ /// The instance. + /// The cache key which identifies the entry in the cache. + /// The setup action used to further configure the newly created object, automatically created by duplicating . + /// An optional to cancel the operation. + /// A to await the completion of the operation. + public static ValueTask ExpireAsync(this IFusionCache cache, string key, Action setupAction, CancellationToken token = default) + { + return cache.ExpireAsync(key, cache.CreateEntryOptions(setupAction).SetIsSafeForAdaptiveCaching(), token); + } + + /// + /// Expires the cache entry for the specified . + ///
+ ///
+ /// In the memory cache: + ///
+ /// - if fail-safe is enabled: the entry will marked as logically expired, but will still be available as a fallback value in case of future problems + ///
+ /// - if fail-safe is disabled: the entry will be effectively removed + ///
+ ///
+ /// In the distributed cache (if any), the entry will be effectively removed. + ///
+ /// The instance. + /// The cache key which identifies the entry in the cache. + /// The setup action used to further configure the newly created object, automatically created by duplicating . + /// An optional to cancel the operation. + public static void Expire(this IFusionCache cache, string key, Action setupAction, CancellationToken token = default) + { + cache.Expire(key, cache.CreateEntryOptions(setupAction).SetIsSafeForAdaptiveCaching(), token); + } + + #endregion + #region Dependency Injection /// diff --git a/src/ZiggyCreatures.FusionCache/FusionCacheGlobalDefaults.cs b/src/ZiggyCreatures.FusionCache/FusionCacheGlobalDefaults.cs index be04dc3a..500b6f4c 100644 --- a/src/ZiggyCreatures.FusionCache/FusionCacheGlobalDefaults.cs +++ b/src/ZiggyCreatures.FusionCache/FusionCacheGlobalDefaults.cs @@ -118,4 +118,9 @@ public static class FusionCacheGlobalDefaults /// The global default . /// public static bool EntryOptionsSkipDistributedCacheReadWhenStale { get; set; } = false; + + /// + /// The global default . + /// + public static bool EntryOptionsSkipMemoryCache { get; set; } = false; } diff --git a/src/ZiggyCreatures.FusionCache/FusionCacheServiceCollectionExtensions.cs b/src/ZiggyCreatures.FusionCache/FusionCacheServiceCollectionExtensions.cs index aee9d9a7..77275274 100644 --- a/src/ZiggyCreatures.FusionCache/FusionCacheServiceCollectionExtensions.cs +++ b/src/ZiggyCreatures.FusionCache/FusionCacheServiceCollectionExtensions.cs @@ -36,7 +36,7 @@ private static IServiceCollection AddFusionCacheProvider(this IServiceCollection /// If the registered found is an instance of (typical when using asp.net) it will be ignored, since it is completely useless (and will consume cpu and memory). /// The to configure the newly created instance. /// The so that additional calls can be chained. - [Obsolete("This will be removed in a future release: please use the version of this method that uses the more common and robust Builder approach. The new call corresponding to the parameterless version of this is AddFusionCache().TryWithAutoSetup()")] + [Obsolete("This will be removed in a future release: please use the version of this method that uses the more common and robust Builder approach. The new call corresponding to the parameterless version of this is AddFusionCache().TryWithAutoSetup()", true)] public static IServiceCollection AddFusionCache(this IServiceCollection services, Action? setupOptionsAction = null, bool useDistributedCacheIfAvailable = true, bool ignoreMemoryDistributedCache = true, Action? setupCacheAction = null) { if (services is null) @@ -94,7 +94,7 @@ public static IServiceCollection AddFusionCache(this IServiceCollection services } else { - services.AddSingleton(new NamedCacheWrapper(cache.CacheName, cache)); + services.AddSingleton(new LazyNamedCache(cache.CacheName, cache)); } return services; @@ -138,9 +138,9 @@ public static IFusionCacheBuilder AddFusionCache(this IServiceCollection service } else { - services.AddSingleton(serviceProvider => + services.AddSingleton(serviceProvider => { - return new NamedCacheWrapper(builder.CacheName, () => builder.Build(serviceProvider)); + return new LazyNamedCache(builder.CacheName, () => builder.Build(serviceProvider)); }); } diff --git a/src/ZiggyCreatures.FusionCache/FusionCache_Async.cs b/src/ZiggyCreatures.FusionCache/FusionCache_Async.cs index 8627244f..7b444eac 100644 --- a/src/ZiggyCreatures.FusionCache/FusionCache_Async.cs +++ b/src/ZiggyCreatures.FusionCache/FusionCache_Async.cs @@ -1,5 +1,4 @@ using System; -using System.Runtime.CompilerServices; using System.Threading; using System.Threading.Tasks; using Microsoft.Extensions.Logging; @@ -20,12 +19,14 @@ public partial class FusionCache token.ThrowIfCancellationRequested(); - FusionCacheMemoryEntry? memoryEntry; - bool memoryEntryIsValid; + FusionCacheMemoryEntry? memoryEntry = null; + bool memoryEntryIsValid = false; object? lockObj = null; // DIRECTLY CHECK MEMORY CACHE (TO AVOID LOCKING) - (memoryEntry, memoryEntryIsValid) = _mca.TryGetEntry(operationId, key); + var mca = GetCurrentMemoryAccessor(options); + if (mca is not null) + (memoryEntry, memoryEntryIsValid) = mca.TryGetEntry(operationId, key); IFusionCacheEntry? entry; bool isStale; @@ -41,25 +42,25 @@ public partial class FusionCache if (isRealFactory && (memoryEntry!.Metadata?.ShouldEagerlyRefresh() ?? false)) { if (_logger?.IsEnabled(LogLevel.Trace) ?? false) - _logger.LogTrace("FUSION (O={CacheOperationId} K={CacheKey}): should eagerly refresh", operationId, key); + _logger.Log(LogLevel.Trace, "FUSION [N={CacheName}] (O={CacheOperationId} K={CacheKey}): should eagerly refresh", CacheName, operationId, key); // TRY TO GET THE LOCK WITHOUT WAITING, SO THAT ONLY THE FIRST ONE WILL ACTUALLY REFRESH THE ENTRY - lockObj = await _reactor.AcquireLockAsync(key, operationId, TimeSpan.Zero, _logger, token).ConfigureAwait(false); + lockObj = await _reactor.AcquireLockAsync(CacheName, key, operationId, TimeSpan.Zero, _logger, token).ConfigureAwait(false); if (lockObj is null) { if (_logger?.IsEnabled(LogLevel.Trace) ?? false) - _logger.LogTrace("FUSION (O={CacheOperationId} K={CacheKey}): eager refresh already occurring", operationId, key); + _logger.Log(LogLevel.Trace, "FUSION [N={CacheName}] (O={CacheOperationId} K={CacheKey}): eager refresh already occurring", CacheName, operationId, key); } else { // EXECUTE EAGER REFRESH - await ExecuteEagerRefreshAsync(operationId, key, factory, options, dca, memoryEntry, lockObj, token); + await ExecuteEagerRefreshAsync(operationId, key, factory, options, memoryEntry, lockObj, token); } } // RETURN THE ENTRY if (_logger?.IsEnabled(LogLevel.Trace) ?? false) - _logger.LogTrace("FUSION (O={CacheOperationId} K={CacheKey}): using memory entry", operationId, key); + _logger.Log(LogLevel.Trace, "FUSION [N={CacheName}] (O={CacheOperationId} K={CacheKey}): using memory entry", CacheName, operationId, key); // EVENT _events.OnHit(operationId, key, memoryEntryIsValid == false || (memoryEntry!.Metadata?.IsFromFailSafe ?? false)); @@ -70,7 +71,7 @@ public partial class FusionCache try { // LOCK - lockObj = await _reactor.AcquireLockAsync(key, operationId, options.GetAppropriateLockTimeout(memoryEntry is not null), _logger, token).ConfigureAwait(false); + lockObj = await _reactor.AcquireLockAsync(CacheName, key, operationId, options.GetAppropriateLockTimeout(memoryEntry is not null), _logger, token).ConfigureAwait(false); if (lockObj is null && options.IsFailSafeEnabled && memoryEntry is not null) { @@ -87,11 +88,12 @@ public partial class FusionCache } // TRY AGAIN WITH MEMORY CACHE (AFTER THE LOCK HAS BEEN ACQUIRED, MAYBE SOMETHING CHANGED) - (memoryEntry, memoryEntryIsValid) = _mca.TryGetEntry(operationId, key); + if (mca is not null) + (memoryEntry, memoryEntryIsValid) = mca.TryGetEntry(operationId, key); if (memoryEntryIsValid) { if (_logger?.IsEnabled(LogLevel.Trace) ?? false) - _logger.LogTrace("FUSION (O={CacheOperationId} K={CacheKey}): using memory entry", operationId, key); + _logger.Log(LogLevel.Trace, "FUSION [N={CacheName}] (O={CacheOperationId} K={CacheKey}): using memory entry", CacheName, operationId, key); // EVENT _events.OnHit(operationId, key, memoryEntryIsValid == false || (memoryEntry?.Metadata?.IsFromFailSafe ?? false)); @@ -103,16 +105,17 @@ public partial class FusionCache FusionCacheDistributedEntry? distributedEntry = null; bool distributedEntryIsValid = false; - if (dca?.IsCurrentlyUsable(operationId, key) ?? false) + if (dca.CanBeUsed(operationId, key)) { if ((memoryEntry is not null && options.SkipDistributedCacheReadWhenStale) == false) { - (distributedEntry, distributedEntryIsValid) = await dca.TryGetEntryAsync(operationId, key, options, memoryEntry is not null, token).ConfigureAwait(false); + (distributedEntry, distributedEntryIsValid) = await dca!.TryGetEntryAsync(operationId, key, options, memoryEntry is not null, token).ConfigureAwait(false); } } DateTimeOffset? lastModified = null; string? etag = null; + long? timestamp = null; if (distributedEntryIsValid) { @@ -124,81 +127,88 @@ public partial class FusionCache // FACTORY TValue? value; bool failSafeActivated = false; - Task? factoryTask = null; - var timeout = options.GetAppropriateFactoryTimeout(memoryEntry is not null || distributedEntry is not null); - - if (_logger?.IsEnabled(LogLevel.Debug) ?? false) - _logger.LogDebug("FUSION (O={CacheOperationId} K={CacheKey}): calling the factory (timeout={Timeout})", operationId, key, timeout.ToLogString_Timeout()); - - var ctx = FusionCacheFactoryExecutionContext.CreateFromEntries(options, distributedEntry, memoryEntry); - - try + if (isRealFactory == false) { - if (timeout == Timeout.InfiniteTimeSpan && token == CancellationToken.None) - { - value = await factory(ctx, CancellationToken.None).ConfigureAwait(false); - } - else - { - value = await FusionCacheExecutionUtils.RunAsyncFuncWithTimeoutAsync(ct => factory(ctx, ct), timeout, options.AllowTimedOutFactoryBackgroundCompletion == false, x => factoryTask = x, token).ConfigureAwait(false); - } - + value = await factory(null!, token).ConfigureAwait(false); hasNewValue = true; - - // UPDATE ADAPTIVE OPTIONS - var maybeNewOptions = ctx.GetOptions(); - if (maybeNewOptions is not null && options != maybeNewOptions) - options = maybeNewOptions; - - // UPDATE LASTMODIFIED/ETAG - lastModified = ctx.LastModified; - etag = ctx.ETag; - - // EVENTS - if (isRealFactory) - _events.OnFactorySuccess(operationId, key); - } - catch (OperationCanceledException) - { - throw; } - catch (Exception exc) + else { - ProcessFactoryError(operationId, key, exc); + Task? factoryTask = null; + + var timeout = options.GetAppropriateFactoryTimeout(memoryEntry is not null || distributedEntry is not null); - MaybeBackgroundCompleteTimedOutFactory(operationId, key, ctx, factoryTask, options, dca, token); + if (_logger?.IsEnabled(LogLevel.Debug) ?? false) + _logger.Log(LogLevel.Debug, "FUSION [N={CacheName}] (O={CacheOperationId} K={CacheKey}): calling the factory (timeout={Timeout})", CacheName, operationId, key, timeout.ToLogString_Timeout()); - var fallbackEntry = MaybeGetFallbackEntry(operationId, key, distributedEntry, memoryEntry, options, true, out failSafeActivated); - if (fallbackEntry is not null) + var ctx = FusionCacheFactoryExecutionContext.CreateFromEntries(options, distributedEntry, memoryEntry); + + try { - value = fallbackEntry.GetValue(); + if (timeout == Timeout.InfiniteTimeSpan && token == CancellationToken.None) + { + value = await factory(ctx, CancellationToken.None).ConfigureAwait(false); + } + else + { + value = await FusionCacheExecutionUtils.RunAsyncFuncWithTimeoutAsync(ct => factory(ctx, ct), timeout, options.AllowTimedOutFactoryBackgroundCompletion == false, x => factoryTask = x, token).ConfigureAwait(false); + } + + hasNewValue = true; + + // UPDATE ADAPTIVE OPTIONS + var maybeNewOptions = ctx.GetOptions(); + if (maybeNewOptions is not null && options != maybeNewOptions) + options = maybeNewOptions; + + // UPDATE LASTMODIFIED/ETAG + lastModified = ctx.LastModified; + etag = ctx.ETag; + + // ADAPTIVE CACHING UPDATE + dca = GetCurrentDistributedAccessor(options); + mca = GetCurrentMemoryAccessor(options); + + // EVENTS + _events.OnFactorySuccess(operationId, key); } - else if (options.IsFailSafeEnabled && failSafeDefaultValue.HasValue) + catch (OperationCanceledException) { - failSafeActivated = true; - value = failSafeDefaultValue; + throw; } - else + catch (Exception exc) { - throw; + ProcessFactoryError(operationId, key, exc); + + MaybeBackgroundCompleteTimedOutFactory(operationId, key, ctx, factoryTask, options, token); + + if (TryPickFailSafeFallbackValue(operationId, key, distributedEntry, memoryEntry, failSafeDefaultValue, options, out var maybeFallbackValue, out timestamp, out failSafeActivated)) + { + value = maybeFallbackValue.Value; + } + else + { + throw; + } } } - entry = FusionCacheMemoryEntry.CreateFromOptions(value, options, failSafeActivated, lastModified, etag); + entry = FusionCacheMemoryEntry.CreateFromOptions(value, options, failSafeActivated, lastModified, etag, timestamp); isStale = failSafeActivated; - if ((dca?.IsCurrentlyUsable(operationId, key) ?? false) && failSafeActivated == false) + if (dca.CanBeUsed(operationId, key) && failSafeActivated == false) { // SAVE IN THE DISTRIBUTED CACHE (BUT ONLY IF NO FAIL-SAFE HAS BEEN EXECUTED) - await dca.SetEntryAsync(operationId, key, entry, options, token).ConfigureAwait(false); + await dca!.SetEntryAsync(operationId, key, entry, options, token).ConfigureAwait(false); } } // SAVING THE DATA IN THE MEMORY CACHE (EVEN IF IT IS FROM FAIL-SAFE) if (entry is not null) { - _mca.SetEntry(operationId, key, entry.AsMemoryEntry(options), options); + if (mca is not null) + mca.SetEntry(operationId, key, entry.AsMemoryEntry(options), options); } } finally @@ -229,10 +239,58 @@ public partial class FusionCache return entry; } - private Task ExecuteEagerRefreshAsync(string operationId, string key, Func, CancellationToken, Task> factory, FusionCacheEntryOptions options, DistributedCacheAccessor? dca, FusionCacheMemoryEntry memoryEntry, object lockObj, CancellationToken token) + private async Task ExecuteEagerRefreshAsync(string operationId, string key, Func, CancellationToken, Task> factory, FusionCacheEntryOptions options, FusionCacheMemoryEntry memoryEntry, object lockObj, CancellationToken token) { + // TRY WITH DISTRIBUTED CACHE (IF ANY) + try + { + var dca = GetCurrentDistributedAccessor(options); + if (dca.CanBeUsed(operationId, key)) + { + FusionCacheDistributedEntry? distributedEntry; + bool distributedEntryIsValid; + + (distributedEntry, distributedEntryIsValid) = await dca!.TryGetEntryAsync(operationId, key, options, memoryEntry is not null, token).ConfigureAwait(false); + if (distributedEntryIsValid) + { + if ((distributedEntry?.Timestamp ?? 0) > (memoryEntry?.Timestamp ?? 0)) + { + try + { + // THE DISTRIBUTED ENTRY IS MORE RECENT THAN THE MEMORY ENTRY -> USE IT + + if (_logger?.IsEnabled(LogLevel.Trace) ?? false) + _logger.LogTrace("FUSION [N={CacheName}] (O={CacheOperationId} K={CacheKey}): distributed entry found ({DistributedTimestamp}) is more recent than the current memory entry ({MemoryTimestamp}): using it", CacheName, operationId, key, distributedEntry?.Timestamp, memoryEntry?.Timestamp); + + // SAVING THE DATA IN THE MEMORY CACHE + var mca = GetCurrentMemoryAccessor(options); + if (mca is not null) + mca.SetEntry(operationId, key, FusionCacheMemoryEntry.CreateFromOtherEntry(distributedEntry!, options), options); + } + finally + { + ReleaseLock(operationId, key, lockObj); + } + + return; + } + else + { + if (_logger?.IsEnabled(LogLevel.Trace) ?? false) + _logger.LogTrace("FUSION [N={CacheName}] (O={CacheOperationId} K={CacheKey}): distributed entry found ({DistributedTimestamp}) is less recent than the current memory entry ({MemoryTimestamp}): ignoring it", CacheName, operationId, key, distributedEntry?.Timestamp, memoryEntry?.Timestamp); + } + } + } + } + catch + { + // EMPTY + } + + //var timeout = options.GetAppropriateFactoryTimeout(memoryEntry is not null || distributedEntry is not null); + if (_logger?.IsEnabled(LogLevel.Trace) ?? false) - _logger.LogTrace("FUSION (O={CacheOperationId} K={CacheKey}): eagerly refreshing", operationId, key); + _logger.Log(LogLevel.Trace, "FUSION [N={CacheName}] (O={CacheOperationId} K={CacheKey}): eagerly refreshing", CacheName, operationId, key); // EVENT _events.OnEagerRefresh(operationId, key); @@ -241,9 +299,7 @@ private Task ExecuteEagerRefreshAsync(string operationId, string key, Fu var factoryTask = factory(ctx, token); - CompleteBackgroundFactory(operationId, key, ctx, factoryTask, options, dca, lockObj, token); - - return Task.CompletedTask; + CompleteBackgroundFactory(operationId, key, ctx, factoryTask, options, lockObj, token); } /// @@ -261,19 +317,19 @@ private Task ExecuteEagerRefreshAsync(string operationId, string key, Fu var operationId = GenerateOperationId(); if (_logger?.IsEnabled(LogLevel.Debug) ?? false) - _logger.LogDebug("FUSION (O={CacheOperationId} K={CacheKey}): calling GetOrSetAsync {Options}", operationId, key, options.ToLogString()); + _logger.Log(LogLevel.Debug, "FUSION [N={CacheName}] (O={CacheOperationId} K={CacheKey}): calling GetOrSetAsync {Options}", CacheName, operationId, key, options.ToLogString()); var entry = await GetOrSetEntryInternalAsync(operationId, key, factory, true, failSafeDefaultValue, options, token).ConfigureAwait(false); if (entry is null) { if (_logger?.IsEnabled(LogLevel.Error) ?? false) - _logger.LogError("FUSION (O={CacheOperationId} K={CacheKey}): something went wrong, the resulting entry is null, and it should not be possible", operationId, key); + _logger.Log(LogLevel.Error, "FUSION [N={CacheName}] (O={CacheOperationId} K={CacheKey}): something went wrong, the resulting entry is null, and it should not be possible", CacheName, operationId, key); throw new InvalidOperationException("The resulting FusionCache entry is null"); } if (_logger?.IsEnabled(LogLevel.Debug) ?? false) - _logger.LogDebug("FUSION (O={CacheOperationId} K={CacheKey}): return {Entry}", operationId, key, entry.ToLogString()); + _logger.Log(LogLevel.Debug, "FUSION [N={CacheName}] (O={CacheOperationId} K={CacheKey}): return {Entry}", CacheName, operationId, key, entry.ToLogString()); return entry.GetValue(); } @@ -290,19 +346,19 @@ private Task ExecuteEagerRefreshAsync(string operationId, string key, Fu var operationId = GenerateOperationId(); if (_logger?.IsEnabled(LogLevel.Debug) ?? false) - _logger.LogDebug("FUSION (O={CacheOperationId} K={CacheKey}): calling GetOrSetAsync {Options}", operationId, key, options.ToLogString()); + _logger.Log(LogLevel.Debug, "FUSION [N={CacheName}] (O={CacheOperationId} K={CacheKey}): calling GetOrSetAsync {Options}", CacheName, operationId, key, options.ToLogString()); var entry = await GetOrSetEntryInternalAsync(operationId, key, (_, _) => Task.FromResult(defaultValue), false, default, options, token).ConfigureAwait(false); if (entry is null) { if (_logger?.IsEnabled(LogLevel.Error) ?? false) - _logger.LogError("FUSION (O={CacheOperationId} K={CacheKey}): something went wrong, the resulting entry is null, and it should not be possible", operationId, key); + _logger.Log(LogLevel.Error, "FUSION [N={CacheName}] (O={CacheOperationId} K={CacheKey}): something went wrong, the resulting entry is null, and it should not be possible", CacheName, operationId, key); throw new InvalidOperationException("The resulting FusionCache entry is null"); } if (_logger?.IsEnabled(LogLevel.Debug) ?? false) - _logger.LogDebug("FUSION (O={CacheOperationId} K={CacheKey}): return {Entry}", operationId, key, entry.ToLogString()); + _logger.Log(LogLevel.Debug, "FUSION [N={CacheName}] (O={CacheOperationId} K={CacheKey}): return {Entry}", CacheName, operationId, key, entry.ToLogString()); return entry.GetValue(); } @@ -314,15 +370,18 @@ private Task ExecuteEagerRefreshAsync(string operationId, string key, Fu token.ThrowIfCancellationRequested(); - FusionCacheMemoryEntry? memoryEntry; - bool memoryEntryIsValid; + FusionCacheMemoryEntry? memoryEntry = null; + bool memoryEntryIsValid = false; // DIRECTLY CHECK MEMORY CACHE (TO AVOID LOCKING) - (memoryEntry, memoryEntryIsValid) = _mca.TryGetEntry(operationId, key); + var mca = GetCurrentMemoryAccessor(options); + if (mca is not null) + (memoryEntry, memoryEntryIsValid) = mca.TryGetEntry(operationId, key); + if (memoryEntryIsValid) { if (_logger?.IsEnabled(LogLevel.Trace) ?? false) - _logger.LogTrace("FUSION (O={CacheOperationId} K={CacheKey}): using memory entry", operationId, key); + _logger.Log(LogLevel.Trace, "FUSION [N={CacheName}] (O={CacheOperationId} K={CacheKey}): using memory entry", CacheName, operationId, key); // EVENT _events.OnHit(operationId, key, memoryEntry!.Metadata?.IsFromFailSafe ?? false); @@ -333,12 +392,12 @@ private Task ExecuteEagerRefreshAsync(string operationId, string key, Fu var dca = GetCurrentDistributedAccessor(options); // SHORT-CIRCUIT: NO USABLE DISTRIBUTED CACHE - if (options.SkipDistributedCacheReadWhenStale || (dca?.IsCurrentlyUsable(operationId, key) ?? false) == false) + if (options.SkipDistributedCacheReadWhenStale || dca.CanBeUsed(operationId, key) == false) { if (options.IsFailSafeEnabled && memoryEntry is not null) { if (_logger?.IsEnabled(LogLevel.Trace) ?? false) - _logger.LogTrace("FUSION (O={CacheOperationId} K={CacheKey}): using memory entry (expired)", operationId, key); + _logger.Log(LogLevel.Trace, "FUSION [N={CacheName}] (O={CacheOperationId} K={CacheKey}): using memory entry (expired)", CacheName, operationId, key); // EVENT _events.OnHit(operationId, key, true); @@ -356,16 +415,22 @@ private Task ExecuteEagerRefreshAsync(string operationId, string key, Fu FusionCacheDistributedEntry? distributedEntry; bool distributedEntryIsValid; - (distributedEntry, distributedEntryIsValid) = await dca.TryGetEntryAsync(operationId, key, options, memoryEntry is not null, token).ConfigureAwait(false); + (distributedEntry, distributedEntryIsValid) = await dca!.TryGetEntryAsync(operationId, key, options, memoryEntry is not null, token).ConfigureAwait(false); if (distributedEntryIsValid) { if (_logger?.IsEnabled(LogLevel.Trace) ?? false) - _logger.LogTrace("FUSION (O={CacheOperationId} K={CacheKey}): using distributed entry", operationId, key); + _logger.Log(LogLevel.Trace, "FUSION [N={CacheName}] (O={CacheOperationId} K={CacheKey}): using distributed entry", CacheName, operationId, key); + + memoryEntry = distributedEntry!.AsMemoryEntry(options); + + // SAVING THE DATA IN THE MEMORY CACHE + if (mca is not null) + mca.SetEntry(operationId, key, memoryEntry, options); // EVENT _events.OnHit(operationId, key, distributedEntry!.Metadata?.IsFromFailSafe ?? false); - return distributedEntry; + return memoryEntry; } if (options.IsFailSafeEnabled) @@ -376,19 +441,25 @@ private Task ExecuteEagerRefreshAsync(string operationId, string key, Fu if (distributedEntry is not null) { if (_logger?.IsEnabled(LogLevel.Trace) ?? false) - _logger.LogTrace("FUSION (O={CacheOperationId} K={CacheKey}): using distributed entry (expired)", operationId, key); + _logger.Log(LogLevel.Trace, "FUSION [N={CacheName}] (O={CacheOperationId} K={CacheKey}): using distributed entry (expired)", CacheName, operationId, key); + + memoryEntry = distributedEntry.AsMemoryEntry(options); + + // SAVING THE DATA IN THE MEMORY CACHE + if (mca is not null) + mca.SetEntry(operationId, key, memoryEntry, options); // EVENT _events.OnHit(operationId, key, true); - return distributedEntry; + return memoryEntry; } // IF MEMORY ENTRY IS THERE -> USE IT if (memoryEntry is not null) { if (_logger?.IsEnabled(LogLevel.Trace) ?? false) - _logger.LogTrace("FUSION (O={CacheOperationId} K={CacheKey}): using memory entry (expired)", operationId, key); + _logger.Log(LogLevel.Trace, "FUSION [N={CacheName}] (O={CacheOperationId} K={CacheKey}): using memory entry (expired)", CacheName, operationId, key); // EVENT _events.OnHit(operationId, key, true); @@ -415,20 +486,20 @@ public async ValueTask> TryGetAsync(string key, Fusio var operationId = GenerateOperationId(); if (_logger?.IsEnabled(LogLevel.Debug) ?? false) - _logger.LogDebug("FUSION (O={CacheOperationId} K={CacheKey}): calling TryGetAsync {Options}", operationId, key, options.ToLogString()); + _logger.Log(LogLevel.Debug, "FUSION [N={CacheName}] (O={CacheOperationId} K={CacheKey}): calling TryGetAsync {Options}", CacheName, operationId, key, options.ToLogString()); var entry = await TryGetEntryInternalAsync(operationId, key, options, token).ConfigureAwait(false); if (entry is null) { if (_logger?.IsEnabled(LogLevel.Debug) ?? false) - _logger.LogDebug("FUSION (O={CacheOperationId} K={CacheKey}): return NO SUCCESS", operationId, key); + _logger.Log(LogLevel.Debug, "FUSION [N={CacheName}] (O={CacheOperationId} K={CacheKey}): return NO SUCCESS", CacheName, operationId, key); return default; } if (_logger?.IsEnabled(LogLevel.Debug) ?? false) - _logger.LogDebug("FUSION (O={CacheOperationId} K={CacheKey}): return SUCCESS", operationId, key); + _logger.Log(LogLevel.Debug, "FUSION [N={CacheName}] (O={CacheOperationId} K={CacheKey}): return SUCCESS", CacheName, operationId, key); return entry.GetValue(); } @@ -445,19 +516,19 @@ public async ValueTask> TryGetAsync(string key, Fusio var operationId = GenerateOperationId(); if (_logger?.IsEnabled(LogLevel.Debug) ?? false) - _logger.LogDebug("FUSION (O={CacheOperationId} K={CacheKey}): calling GetOrDefaultAsync {Options}", operationId, key, options.ToLogString()); + _logger.Log(LogLevel.Debug, "FUSION [N={CacheName}] (O={CacheOperationId} K={CacheKey}): calling GetOrDefaultAsync {Options}", CacheName, operationId, key, options.ToLogString()); var entry = await TryGetEntryInternalAsync(operationId, key, options, token).ConfigureAwait(false); if (entry is null) { if (_logger?.IsEnabled(LogLevel.Debug) ?? false) - _logger.LogDebug("FUSION (O={CacheOperationId} K={CacheKey}): return DEFAULT VALUE", operationId, key); + _logger.Log(LogLevel.Debug, "FUSION [N={CacheName}] (O={CacheOperationId} K={CacheKey}): return DEFAULT VALUE", CacheName, operationId, key); return defaultValue; } if (_logger?.IsEnabled(LogLevel.Debug) ?? false) - _logger.LogDebug("FUSION (O={CacheOperationId} K={CacheKey}): return {Entry}", operationId, key, entry.ToLogString()); + _logger.Log(LogLevel.Debug, "FUSION [N={CacheName}] (O={CacheOperationId} K={CacheKey}): return {Entry}", CacheName, operationId, key, entry.ToLogString()); return entry.GetValue(); } @@ -477,18 +548,21 @@ public async ValueTask SetAsync(string key, TValue value, FusionCacheEnt var operationId = GenerateOperationId(); if (_logger?.IsEnabled(LogLevel.Debug) ?? false) - _logger.LogDebug("FUSION (O={CacheOperationId} K={CacheKey}): calling SetAsync {Options}", operationId, key, options.ToLogString()); + _logger.Log(LogLevel.Debug, "FUSION [N={CacheName}] (O={CacheOperationId} K={CacheKey}): calling SetAsync {Options}", CacheName, operationId, key, options.ToLogString()); // TODO: MAYBE FIND A WAY TO PASS LASTMODIFIED/ETAG HERE - var entry = FusionCacheMemoryEntry.CreateFromOptions(value, options, false, null, null); + var entry = FusionCacheMemoryEntry.CreateFromOptions(value, options, false, null, null, null); + + var mca = GetCurrentMemoryAccessor(options); - _mca.SetEntry(operationId, key, entry, options); + if (mca is not null) + mca.SetEntry(operationId, key, entry, options); var dca = GetCurrentDistributedAccessor(options); - if (dca?.IsCurrentlyUsable(operationId, key) ?? false) + if (dca.CanBeUsed(operationId, key)) { - await dca.SetEntryAsync(operationId, key, entry, options, token).ConfigureAwait(false); + await dca!.SetEntryAsync(operationId, key, entry, options, token).ConfigureAwait(false); } // EVENT @@ -514,15 +588,16 @@ public async ValueTask RemoveAsync(string key, FusionCacheEntryOptions? options var operationId = GenerateOperationId(); if (_logger?.IsEnabled(LogLevel.Debug) ?? false) - _logger.LogDebug("FUSION (O={CacheOperationId} K={CacheKey}): calling RemoveAsync {Options}", operationId, key, options.ToLogString()); + _logger.Log(LogLevel.Debug, "FUSION [N={CacheName}] (O={CacheOperationId} K={CacheKey}): calling RemoveAsync {Options}", CacheName, operationId, key, options.ToLogString()); - _mca.RemoveEntry(operationId, key, options); + var mca = GetCurrentMemoryAccessor(options); + if (mca is not null) + mca.RemoveEntry(operationId, key, options); var dca = GetCurrentDistributedAccessor(options); - - if (dca?.IsCurrentlyUsable(operationId, key) ?? false) + if (dca.CanBeUsed(operationId, key)) { - await dca.RemoveEntryAsync(operationId, key, options, token).ConfigureAwait(false); + await dca!.RemoveEntryAsync(operationId, key, options, token).ConfigureAwait(false); } // EVENT @@ -533,21 +608,52 @@ public async ValueTask RemoveAsync(string key, FusionCacheEntryOptions? options await PublishInternalAsync(operationId, BackplaneMessage.CreateForEntryRemove(key), options, token).ConfigureAwait(false); } - [MethodImpl(MethodImplOptions.AggressiveInlining)] - private async ValueTask PublishInternalAsync(string operationId, BackplaneMessage message, FusionCacheEntryOptions options, CancellationToken token) + /// + public async ValueTask ExpireAsync(string key, FusionCacheEntryOptions? options = null, CancellationToken token = default) { - if (_bpa is null) - return false; + ValidateCacheKey(key); - return await _bpa.PublishAsync(operationId, message, options, false, token); - } + MaybePreProcessCacheKey(ref key); + + token.ThrowIfCancellationRequested(); - /// - internal ValueTask PublishAsync(BackplaneMessage message, FusionCacheEntryOptions? options = null, CancellationToken token = default) - { if (options is null) options = _options.DefaultEntryOptions; - return PublishInternalAsync(GenerateOperationId(), message, options, token); + var operationId = GenerateOperationId(); + + if (_logger?.IsEnabled(LogLevel.Debug) ?? false) + _logger.Log(LogLevel.Debug, "FUSION [N={CacheName}] (O={CacheOperationId} K={CacheKey}): calling ExpireAsync {Options}", CacheName, operationId, key, options.ToLogString()); + + var mca = GetCurrentMemoryAccessor(options); + if (mca is not null) + mca.ExpireEntry(operationId, key, options.IsFailSafeEnabled); + + var dca = GetCurrentDistributedAccessor(options); + if (dca.CanBeUsed(operationId, key)) + { + await dca!.RemoveEntryAsync(operationId, key, options, token).ConfigureAwait(false); + } + + // EVENT + _events.OnExpire(operationId, key); + + // BACKPLANE + if (options.SkipBackplaneNotifications == false) + { + if (options.IsFailSafeEnabled) + await PublishInternalAsync(operationId, BackplaneMessage.CreateForEntryExpire(key), options, token).ConfigureAwait(false); + else + await PublishInternalAsync(operationId, BackplaneMessage.CreateForEntryRemove(key), options, token).ConfigureAwait(false); + } + } + + //[MethodImpl(MethodImplOptions.AggressiveInlining)] + private async ValueTask PublishInternalAsync(string operationId, BackplaneMessage message, FusionCacheEntryOptions options, CancellationToken token) + { + if (_bpa is null) + return false; + + return await _bpa.PublishAsync(operationId, message, options, false, token); } } diff --git a/src/ZiggyCreatures.FusionCache/FusionCache_Sync.cs b/src/ZiggyCreatures.FusionCache/FusionCache_Sync.cs index 1cd4ea5b..8deac1af 100644 --- a/src/ZiggyCreatures.FusionCache/FusionCache_Sync.cs +++ b/src/ZiggyCreatures.FusionCache/FusionCache_Sync.cs @@ -1,5 +1,4 @@ using System; -using System.Runtime.CompilerServices; using System.Threading; using System.Threading.Tasks; using Microsoft.Extensions.Logging; @@ -20,12 +19,14 @@ public partial class FusionCache token.ThrowIfCancellationRequested(); - FusionCacheMemoryEntry? memoryEntry; - bool memoryEntryIsValid; + FusionCacheMemoryEntry? memoryEntry = null; + bool memoryEntryIsValid = false; object? lockObj = null; // DIRECTLY CHECK MEMORY CACHE (TO AVOID LOCKING) - (memoryEntry, memoryEntryIsValid) = _mca.TryGetEntry(operationId, key); + var mca = GetCurrentMemoryAccessor(options); + if (mca is not null) + (memoryEntry, memoryEntryIsValid) = mca.TryGetEntry(operationId, key); IFusionCacheEntry? entry; bool isStale; @@ -41,25 +42,25 @@ public partial class FusionCache if (isRealFactory && (memoryEntry!.Metadata?.ShouldEagerlyRefresh() ?? false)) { if (_logger?.IsEnabled(LogLevel.Trace) ?? false) - _logger.LogTrace("FUSION (O={CacheOperationId} K={CacheKey}): should eagerly refresh", operationId, key); + _logger.Log(LogLevel.Trace, "FUSION [N={CacheName}] (O={CacheOperationId} K={CacheKey}): should eagerly refresh", CacheName, operationId, key); // TRY TO GET THE LOCK WITHOUT WAITING, SO THAT ONLY THE FIRST ONE WILL ACTUALLY REFRESH THE ENTRY - lockObj = _reactor.AcquireLock(key, operationId, TimeSpan.Zero, _logger); + lockObj = _reactor.AcquireLock(CacheName, key, operationId, TimeSpan.Zero, _logger); if (lockObj is null) { if (_logger?.IsEnabled(LogLevel.Trace) ?? false) - _logger.LogTrace("FUSION (O={CacheOperationId} K={CacheKey}): eager refresh already occurring", operationId, key); + _logger.Log(LogLevel.Trace, "FUSION [N={CacheName}] (O={CacheOperationId} K={CacheKey}): eager refresh already occurring", CacheName, operationId, key); } else { // EXECUTE EAGER REFRESH - ExecuteEagerRefresh(operationId, key, factory, options, dca, memoryEntry, lockObj, token); + ExecuteEagerRefresh(operationId, key, factory, options, memoryEntry, lockObj, token); } } // RETURN THE ENTRY if (_logger?.IsEnabled(LogLevel.Trace) ?? false) - _logger.LogTrace("FUSION (O={CacheOperationId} K={CacheKey}): using memory entry", operationId, key); + _logger.Log(LogLevel.Trace, "FUSION [N={CacheName}] (O={CacheOperationId} K={CacheKey}): using memory entry", CacheName, operationId, key); // EVENT _events.OnHit(operationId, key, memoryEntryIsValid == false || (memoryEntry!.Metadata?.IsFromFailSafe ?? false)); @@ -70,7 +71,7 @@ public partial class FusionCache try { // LOCK - lockObj = _reactor.AcquireLock(key, operationId, options.GetAppropriateLockTimeout(memoryEntry is not null), _logger); + lockObj = _reactor.AcquireLock(CacheName, key, operationId, options.GetAppropriateLockTimeout(memoryEntry is not null), _logger); if (lockObj is null && options.IsFailSafeEnabled && memoryEntry is not null) { @@ -87,11 +88,12 @@ public partial class FusionCache } // TRY AGAIN WITH MEMORY CACHE (AFTER THE LOCK HAS BEEN ACQUIRED, MAYBE SOMETHING CHANGED) - (memoryEntry, memoryEntryIsValid) = _mca.TryGetEntry(operationId, key); + if (mca is not null) + (memoryEntry, memoryEntryIsValid) = mca.TryGetEntry(operationId, key); if (memoryEntryIsValid) { if (_logger?.IsEnabled(LogLevel.Trace) ?? false) - _logger.LogTrace("FUSION (O={CacheOperationId} K={CacheKey}): using memory entry", operationId, key); + _logger.Log(LogLevel.Trace, "FUSION [N={CacheName}] (O={CacheOperationId} K={CacheKey}): using memory entry", CacheName, operationId, key); // EVENT _events.OnHit(operationId, key, memoryEntryIsValid == false || (memoryEntry?.Metadata?.IsFromFailSafe ?? false)); @@ -103,11 +105,11 @@ public partial class FusionCache FusionCacheDistributedEntry? distributedEntry = null; bool distributedEntryIsValid = false; - if (dca?.IsCurrentlyUsable(operationId, key) ?? false) + if (dca.CanBeUsed(operationId, key)) { if ((memoryEntry is not null && options.SkipDistributedCacheReadWhenStale) == false) { - (distributedEntry, distributedEntryIsValid) = dca.TryGetEntry(operationId, key, options, memoryEntry is not null, token); + (distributedEntry, distributedEntryIsValid) = dca!.TryGetEntry(operationId, key, options, memoryEntry is not null, token); } } @@ -124,81 +126,89 @@ public partial class FusionCache // FACTORY TValue? value; bool failSafeActivated = false; - Task? factoryTask = null; + long? timestamp = null; - var timeout = options.GetAppropriateFactoryTimeout(memoryEntry is not null || distributedEntry is not null); - - if (_logger?.IsEnabled(LogLevel.Debug) ?? false) - _logger.LogDebug("FUSION (O={CacheOperationId} K={CacheKey}): calling the factory (timeout={Timeout})", operationId, key, timeout.ToLogString_Timeout()); - - var ctx = FusionCacheFactoryExecutionContext.CreateFromEntries(options, distributedEntry, memoryEntry); - - try + if (isRealFactory == false) { - if (timeout == Timeout.InfiniteTimeSpan && token == CancellationToken.None) - { - value = factory(ctx, CancellationToken.None); - } - else - { - value = FusionCacheExecutionUtils.RunSyncFuncWithTimeout(ct => factory(ctx, ct), timeout, options.AllowTimedOutFactoryBackgroundCompletion == false, x => factoryTask = x, token); - } - + value = factory(null!, token); hasNewValue = true; - - // UPDATE ADAPTIVE OPTIONS - var maybeNewOptions = ctx.GetOptions(); - if (maybeNewOptions is not null && options != maybeNewOptions) - options = maybeNewOptions; - - // UPDATE LASTMODIFIED/ETAG - lastModified = ctx.LastModified; - etag = ctx.ETag; - - // EVENTS - if (isRealFactory) - _events.OnFactorySuccess(operationId, key); } - catch (OperationCanceledException) - { - throw; - } - catch (Exception exc) + else { - ProcessFactoryError(operationId, key, exc); + Task? factoryTask = null; - MaybeBackgroundCompleteTimedOutFactory(operationId, key, ctx, factoryTask, options, dca, token); + var timeout = options.GetAppropriateFactoryTimeout(memoryEntry is not null || distributedEntry is not null); - var fallbackEntry = MaybeGetFallbackEntry(operationId, key, distributedEntry, memoryEntry, options, true, out failSafeActivated); - if (fallbackEntry is not null) + if (_logger?.IsEnabled(LogLevel.Debug) ?? false) + _logger.Log(LogLevel.Debug, "FUSION [N={CacheName}] (O={CacheOperationId} K={CacheKey}): calling the factory (timeout={Timeout})", CacheName, operationId, key, timeout.ToLogString_Timeout()); + + var ctx = FusionCacheFactoryExecutionContext.CreateFromEntries(options, distributedEntry, memoryEntry); + + try { - value = fallbackEntry.GetValue(); + if (timeout == Timeout.InfiniteTimeSpan && token == CancellationToken.None) + { + value = factory(ctx, CancellationToken.None); + } + else + { + value = FusionCacheExecutionUtils.RunSyncFuncWithTimeout(ct => factory(ctx, ct), timeout, options.AllowTimedOutFactoryBackgroundCompletion == false, x => factoryTask = x, token); + } + + hasNewValue = true; + + // UPDATE ADAPTIVE OPTIONS + var maybeNewOptions = ctx.GetOptions(); + if (maybeNewOptions is not null && options != maybeNewOptions) + options = maybeNewOptions; + + // UPDATE LASTMODIFIED/ETAG + lastModified = ctx.LastModified; + etag = ctx.ETag; + + // ADAPTIVE CACHING UPDATE + dca = GetCurrentDistributedAccessor(options); + mca = GetCurrentMemoryAccessor(options); + + // EVENTS + _events.OnFactorySuccess(operationId, key); } - else if (options.IsFailSafeEnabled && failSafeDefaultValue.HasValue) + catch (OperationCanceledException) { - failSafeActivated = true; - value = failSafeDefaultValue; + throw; } - else + catch (Exception exc) { - throw; + ProcessFactoryError(operationId, key, exc); + + MaybeBackgroundCompleteTimedOutFactory(operationId, key, ctx, factoryTask, options, token); + + if (TryPickFailSafeFallbackValue(operationId, key, distributedEntry, memoryEntry, failSafeDefaultValue, options, out var maybeFallbackValue, out timestamp, out failSafeActivated)) + { + value = maybeFallbackValue.Value; + } + else + { + throw; + } } } - entry = FusionCacheMemoryEntry.CreateFromOptions(value, options, failSafeActivated, lastModified, etag); + entry = FusionCacheMemoryEntry.CreateFromOptions(value, options, failSafeActivated, lastModified, etag, null); isStale = failSafeActivated; - if ((dca?.IsCurrentlyUsable(operationId, key) ?? false) && failSafeActivated == false) + if (dca.CanBeUsed(operationId, key) && failSafeActivated == false) { // SAVE IN THE DISTRIBUTED CACHE (BUT ONLY IF NO FAIL-SAFE HAS BEEN EXECUTED) - dca.SetEntry(operationId, key, entry, options, token); + dca!.SetEntry(operationId, key, entry, options, token); } } // SAVING THE DATA IN THE MEMORY CACHE (EVEN IF IT IS FROM FAIL-SAFE) if (entry is not null) { - _mca.SetEntry(operationId, key, entry.AsMemoryEntry(options), options); + if (mca is not null) + mca.SetEntry(operationId, key, entry.AsMemoryEntry(options), options); } } finally @@ -229,10 +239,58 @@ public partial class FusionCache return entry; } - private void ExecuteEagerRefresh(string operationId, string key, Func, CancellationToken, TValue?> factory, FusionCacheEntryOptions options, DistributedCacheAccessor? dca, FusionCacheMemoryEntry memoryEntry, object lockObj, CancellationToken token) + private void ExecuteEagerRefresh(string operationId, string key, Func, CancellationToken, TValue?> factory, FusionCacheEntryOptions options, FusionCacheMemoryEntry memoryEntry, object lockObj, CancellationToken token) { + // TRY WITH DISTRIBUTED CACHE (IF ANY) + try + { + var dca = GetCurrentDistributedAccessor(options); + if (dca.CanBeUsed(operationId, key)) + { + FusionCacheDistributedEntry? distributedEntry; + bool distributedEntryIsValid; + + (distributedEntry, distributedEntryIsValid) = dca!.TryGetEntry(operationId, key, options, false, token); + if (distributedEntryIsValid) + { + if ((distributedEntry?.Timestamp ?? 0) > (memoryEntry?.Timestamp ?? 0)) + { + try + { + // THE DISTRIBUTED ENTRY IS MORE RECENT THAN THE MEMORY ENTRY -> USE IT + + if (_logger?.IsEnabled(LogLevel.Trace) ?? false) + _logger.LogTrace("FUSION [N={CacheName}] (O={CacheOperationId} K={CacheKey}): distributed entry found ({DistributedTimestamp}) is more recent than the current memory entry ({MemoryTimestamp}): using it", CacheName, operationId, key, distributedEntry?.Timestamp, memoryEntry?.Timestamp); + + // SAVING THE DATA IN THE MEMORY CACHE + var mca = GetCurrentMemoryAccessor(options); + if (mca is not null) + mca.SetEntry(operationId, key, FusionCacheMemoryEntry.CreateFromOtherEntry(distributedEntry!, options), options); + } + finally + { + ReleaseLock(operationId, key, lockObj); + } + + return; + } + else + { + if (_logger?.IsEnabled(LogLevel.Trace) ?? false) + _logger.LogTrace("FUSION [N={CacheName}] (O={CacheOperationId} K={CacheKey}): distributed entry found ({DistributedTimestamp}) is less recent than the current memory entry ({MemoryTimestamp}): ignoring it", CacheName, operationId, key, distributedEntry?.Timestamp, memoryEntry?.Timestamp); + } + } + } + } + catch + { + // EMPTY + } + + //var timeout = options.GetAppropriateFactoryTimeout(memoryEntry is not null || distributedEntry is not null); + if (_logger?.IsEnabled(LogLevel.Trace) ?? false) - _logger.LogTrace("FUSION (O={CacheOperationId} K={CacheKey}): eagerly refreshing", operationId, key); + _logger.Log(LogLevel.Trace, "FUSION [N={CacheName}] (O={CacheOperationId} K={CacheKey}): eagerly refreshing", CacheName, operationId, key); // EVENT _events.OnEagerRefresh(operationId, key); @@ -241,7 +299,7 @@ private void ExecuteEagerRefresh(string operationId, string key, Func factory(ctx, token)); - CompleteBackgroundFactory(operationId, key, ctx, factoryTask, options, dca, lockObj, token); + CompleteBackgroundFactory(operationId, key, ctx, factoryTask, options, lockObj, token); } /// @@ -259,19 +317,19 @@ private void ExecuteEagerRefresh(string operationId, string key, Func {Options}", operationId, key, options.ToLogString()); + _logger.Log(LogLevel.Debug, "FUSION [N={CacheName}] (O={CacheOperationId} K={CacheKey}): calling GetOrSet {Options}", CacheName, operationId, key, options.ToLogString()); var entry = GetOrSetEntryInternal(operationId, key, factory, true, failSafeDefaultValue, options, token); if (entry is null) { if (_logger?.IsEnabled(LogLevel.Error) ?? false) - _logger.LogError("FUSION (O={CacheOperationId} K={CacheKey}): something went wrong, the resulting entry is null, and it should not be possible", operationId, key); + _logger.Log(LogLevel.Error, "FUSION [N={CacheName}] (O={CacheOperationId} K={CacheKey}): something went wrong, the resulting entry is null, and it should not be possible", CacheName, operationId, key); throw new InvalidOperationException("The resulting FusionCache entry is null"); } if (_logger?.IsEnabled(LogLevel.Debug) ?? false) - _logger.LogDebug("FUSION (O={CacheOperationId} K={CacheKey}): return {Entry}", operationId, key, entry.ToLogString()); + _logger.Log(LogLevel.Debug, "FUSION [N={CacheName}] (O={CacheOperationId} K={CacheKey}): return {Entry}", CacheName, operationId, key, entry.ToLogString()); return entry.GetValue(); } @@ -288,19 +346,19 @@ private void ExecuteEagerRefresh(string operationId, string key, Func {Options}", operationId, key, options.ToLogString()); + _logger.Log(LogLevel.Debug, "FUSION [N={CacheName}] (O={CacheOperationId} K={CacheKey}): calling GetOrSet {Options}", CacheName, operationId, key, options.ToLogString()); var entry = GetOrSetEntryInternal(operationId, key, (_, _) => defaultValue, false, default, options, token); if (entry is null) { if (_logger?.IsEnabled(LogLevel.Error) ?? false) - _logger.LogError("FUSION (O={CacheOperationId} K={CacheKey}): something went wrong, the resulting entry is null, and it should not be possible", operationId, key); + _logger.Log(LogLevel.Error, "FUSION [N={CacheName}] (O={CacheOperationId} K={CacheKey}): something went wrong, the resulting entry is null, and it should not be possible", CacheName, operationId, key); throw new InvalidOperationException("The resulting FusionCache entry is null"); } if (_logger?.IsEnabled(LogLevel.Debug) ?? false) - _logger.LogDebug("FUSION (O={CacheOperationId} K={CacheKey}): return {Entry}", operationId, key, entry.ToLogString()); + _logger.Log(LogLevel.Debug, "FUSION [N={CacheName}] (O={CacheOperationId} K={CacheKey}): return {Entry}", CacheName, operationId, key, entry.ToLogString()); return entry.GetValue(); } @@ -312,15 +370,18 @@ private void ExecuteEagerRefresh(string operationId, string key, Func(operationId, key); + var mca = GetCurrentMemoryAccessor(options); + if (mca is not null) + (memoryEntry, memoryEntryIsValid) = mca.TryGetEntry(operationId, key); + if (memoryEntryIsValid) { if (_logger?.IsEnabled(LogLevel.Trace) ?? false) - _logger.LogTrace("FUSION (O={CacheOperationId} K={CacheKey}): using memory entry", operationId, key); + _logger.Log(LogLevel.Trace, "FUSION [N={CacheName}] (O={CacheOperationId} K={CacheKey}): using memory entry", CacheName, operationId, key); // EVENT _events.OnHit(operationId, key, memoryEntry!.Metadata?.IsFromFailSafe ?? false); @@ -331,12 +392,12 @@ private void ExecuteEagerRefresh(string operationId, string key, Func(string operationId, string key, Func? distributedEntry; bool distributedEntryIsValid; - (distributedEntry, distributedEntryIsValid) = dca.TryGetEntry(operationId, key, options, memoryEntry is not null, token); + (distributedEntry, distributedEntryIsValid) = dca!.TryGetEntry(operationId, key, options, memoryEntry is not null, token); if (distributedEntryIsValid) { if (_logger?.IsEnabled(LogLevel.Trace) ?? false) - _logger.LogTrace("FUSION (O={CacheOperationId} K={CacheKey}): using distributed entry", operationId, key); + _logger.Log(LogLevel.Trace, "FUSION [N={CacheName}] (O={CacheOperationId} K={CacheKey}): using distributed entry", CacheName, operationId, key); + + memoryEntry = distributedEntry!.AsMemoryEntry(options); + + // SAVING THE DATA IN THE MEMORY CACHE + if (mca is not null) + mca.SetEntry(operationId, key, memoryEntry, options); // EVENT _events.OnHit(operationId, key, distributedEntry!.Metadata?.IsFromFailSafe ?? false); - return distributedEntry; + return memoryEntry; } if (options.IsFailSafeEnabled) @@ -374,19 +441,25 @@ private void ExecuteEagerRefresh(string operationId, string key, Func(options); + + // SAVING THE DATA IN THE MEMORY CACHE + if (mca is not null) + mca.SetEntry(operationId, key, memoryEntry, options); // EVENT _events.OnHit(operationId, key, true); - return distributedEntry; + return memoryEntry; } // IF MEMORY ENTRY IS THERE -> USE IT if (memoryEntry is not null) { if (_logger?.IsEnabled(LogLevel.Trace) ?? false) - _logger.LogTrace("FUSION (O={CacheOperationId} K={CacheKey}): using memory entry (expired)", operationId, key); + _logger.Log(LogLevel.Trace, "FUSION [N={CacheName}] (O={CacheOperationId} K={CacheKey}): using memory entry (expired)", CacheName, operationId, key); // EVENT _events.OnHit(operationId, key, true); @@ -413,20 +486,20 @@ public MaybeValue TryGet(string key, FusionCacheEntryOptions? op var operationId = GenerateOperationId(); if (_logger?.IsEnabled(LogLevel.Debug) ?? false) - _logger.LogDebug("FUSION (O={CacheOperationId} K={CacheKey}): calling TryGet {Options}", operationId, key, options.ToLogString()); + _logger.Log(LogLevel.Debug, "FUSION [N={CacheName}] (O={CacheOperationId} K={CacheKey}): calling TryGet {Options}", CacheName, operationId, key, options.ToLogString()); var entry = TryGetEntryInternal(operationId, key, options, token); if (entry is null) { if (_logger?.IsEnabled(LogLevel.Debug) ?? false) - _logger.LogDebug("FUSION (O={CacheOperationId} K={CacheKey}): return NO SUCCESS", operationId, key); + _logger.Log(LogLevel.Debug, "FUSION [N={CacheName}] (O={CacheOperationId} K={CacheKey}): return NO SUCCESS", CacheName, operationId, key); return default; } if (_logger?.IsEnabled(LogLevel.Debug) ?? false) - _logger.LogDebug("FUSION (O={CacheOperationId} K={CacheKey}): return SUCCESS", operationId, key); + _logger.Log(LogLevel.Debug, "FUSION [N={CacheName}] (O={CacheOperationId} K={CacheKey}): return SUCCESS", CacheName, operationId, key); return entry.GetValue(); } @@ -443,19 +516,19 @@ public MaybeValue TryGet(string key, FusionCacheEntryOptions? op var operationId = GenerateOperationId(); if (_logger?.IsEnabled(LogLevel.Debug) ?? false) - _logger.LogDebug("FUSION (O={CacheOperationId} K={CacheKey}): calling GetOrDefault {Options}", operationId, key, options.ToLogString()); + _logger.Log(LogLevel.Debug, "FUSION [N={CacheName}] (O={CacheOperationId} K={CacheKey}): calling GetOrDefault {Options}", CacheName, operationId, key, options.ToLogString()); var entry = TryGetEntryInternal(operationId, key, options, token); if (entry is null) { if (_logger?.IsEnabled(LogLevel.Debug) ?? false) - _logger.LogDebug("FUSION (O={CacheOperationId} K={CacheKey}): return DEFAULT VALUE", operationId, key); + _logger.Log(LogLevel.Debug, "FUSION [N={CacheName}] (O={CacheOperationId} K={CacheKey}): return DEFAULT VALUE", CacheName, operationId, key); return defaultValue; } if (_logger?.IsEnabled(LogLevel.Debug) ?? false) - _logger.LogDebug("FUSION (O={CacheOperationId} K={CacheKey}): return {Entry}", operationId, key, entry.ToLogString()); + _logger.Log(LogLevel.Debug, "FUSION [N={CacheName}] (O={CacheOperationId} K={CacheKey}): return {Entry}", CacheName, operationId, key, entry.ToLogString()); return entry.GetValue(); } @@ -475,18 +548,19 @@ public void Set(string key, TValue value, FusionCacheEntryOptions? optio var operationId = GenerateOperationId(); if (_logger?.IsEnabled(LogLevel.Debug) ?? false) - _logger.LogDebug("FUSION (O={CacheOperationId} K={CacheKey}): calling Set {Options}", operationId, key, options.ToLogString()); + _logger.Log(LogLevel.Debug, "FUSION [N={CacheName}] (O={CacheOperationId} K={CacheKey}): calling Set {Options}", CacheName, operationId, key, options.ToLogString()); // TODO: MAYBE FIND A WAY TO PASS LASTMODIFIED/ETAG HERE - var entry = FusionCacheMemoryEntry.CreateFromOptions(value, options, false, null, null); + var entry = FusionCacheMemoryEntry.CreateFromOptions(value, options, false, null, null, null); - _mca.SetEntry(operationId, key, entry, options); + var mca = GetCurrentMemoryAccessor(options); + if (mca is not null) + mca.SetEntry(operationId, key, entry, options); var dca = GetCurrentDistributedAccessor(options); - - if (dca?.IsCurrentlyUsable(operationId, key) ?? false) + if (dca.CanBeUsed(operationId, key)) { - dca.SetEntry(operationId, key, entry, options, token); + dca!.SetEntry(operationId, key, entry, options, token); } // EVENT @@ -512,15 +586,16 @@ public void Remove(string key, FusionCacheEntryOptions? options = null, Cancella var operationId = GenerateOperationId(); if (_logger?.IsEnabled(LogLevel.Debug) ?? false) - _logger.LogDebug("FUSION (O={CacheOperationId} K={CacheKey}): calling Remove {Options}", operationId, key, options.ToLogString()); + _logger.Log(LogLevel.Debug, "FUSION [N={CacheName}] (O={CacheOperationId} K={CacheKey}): calling Remove {Options}", CacheName, operationId, key, options.ToLogString()); - _mca.RemoveEntry(operationId, key, options); + var mca = GetCurrentMemoryAccessor(options); + if (mca is not null) + mca.RemoveEntry(operationId, key, options); var dca = GetCurrentDistributedAccessor(options); - - if (dca?.IsCurrentlyUsable(operationId, key) ?? false) + if (dca.CanBeUsed(operationId, key)) { - dca.RemoveEntry(operationId, key, options, token); + dca!.RemoveEntry(operationId, key, options, token); } // EVENT @@ -531,21 +606,52 @@ public void Remove(string key, FusionCacheEntryOptions? options = null, Cancella PublishInternal(operationId, BackplaneMessage.CreateForEntryRemove(key), options, token); } - [MethodImpl(MethodImplOptions.AggressiveInlining)] - private bool PublishInternal(string operationId, BackplaneMessage message, FusionCacheEntryOptions options, CancellationToken token) + /// + public void Expire(string key, FusionCacheEntryOptions? options = null, CancellationToken token = default) { - if (_bpa is null) - return false; + ValidateCacheKey(key); - return _bpa.Publish(operationId, message, options, false, token); - } + MaybePreProcessCacheKey(ref key); + + token.ThrowIfCancellationRequested(); - /// - internal bool Publish(BackplaneMessage message, FusionCacheEntryOptions? options, CancellationToken token) - { if (options is null) options = _options.DefaultEntryOptions; - return PublishInternal(GenerateOperationId(), message, options, token); + var operationId = GenerateOperationId(); + + if (_logger?.IsEnabled(LogLevel.Debug) ?? false) + _logger.Log(LogLevel.Debug, "FUSION [N={CacheName}] (O={CacheOperationId} K={CacheKey}): calling Expire {Options}", CacheName, operationId, key, options.ToLogString()); + + var mca = GetCurrentMemoryAccessor(options); + if (mca is not null) + mca.ExpireEntry(operationId, key, options.IsFailSafeEnabled); + + var dca = GetCurrentDistributedAccessor(options); + if (dca.CanBeUsed(operationId, key)) + { + dca!.RemoveEntry(operationId, key, options, token); + } + + // EVENT + _events.OnExpire(operationId, key); + + // BACKPLANE + if (options.SkipBackplaneNotifications == false) + { + if (options.IsFailSafeEnabled) + PublishInternal(operationId, BackplaneMessage.CreateForEntryExpire(key), options, token); + else + PublishInternal(operationId, BackplaneMessage.CreateForEntryRemove(key), options, token); + } + } + + //[MethodImpl(MethodImplOptions.AggressiveInlining)] + private bool PublishInternal(string operationId, BackplaneMessage message, FusionCacheEntryOptions options, CancellationToken token) + { + if (_bpa is null) + return false; + + return _bpa.Publish(operationId, message, options, false, token); } } diff --git a/src/ZiggyCreatures.FusionCache/GlobalSuppressions.cs b/src/ZiggyCreatures.FusionCache/GlobalSuppressions.cs index 6f4ab604..2ad79a2a 100644 --- a/src/ZiggyCreatures.FusionCache/GlobalSuppressions.cs +++ b/src/ZiggyCreatures.FusionCache/GlobalSuppressions.cs @@ -8,6 +8,6 @@ [assembly: SuppressMessage("Performance", "HAA0101:Array allocation for params parameter", Justification = "")] [assembly: SuppressMessage("Performance", "HAA0302:Display class allocation to capture closure", Justification = "")] [assembly: SuppressMessage("Performance", "HAA0301:Closure Allocation Source", Justification = "")] -[assembly: SuppressMessage("Performance", "HAA0303:Lambda or anonymous method in a generic method allocates a delegate instance", Justification = "")] -[assembly: SuppressMessage("Performance", "HAA0601:Value type to reference type conversion causing boxing allocation", Justification = "")] +//[assembly: SuppressMessage("Performance", "HAA0303:Lambda or anonymous method in a generic method allocates a delegate instance", Justification = "")] +//[assembly: SuppressMessage("Performance", "HAA0601:Value type to reference type conversion causing boxing allocation", Justification = "")] [assembly: SuppressMessage("Simplification", "RCS1049:Simplify boolean comparison.", Justification = "")] diff --git a/src/ZiggyCreatures.FusionCache/IFusionCache.cs b/src/ZiggyCreatures.FusionCache/IFusionCache.cs index 6244bf7e..23e0e2f9 100644 --- a/src/ZiggyCreatures.FusionCache/IFusionCache.cs +++ b/src/ZiggyCreatures.FusionCache/IFusionCache.cs @@ -10,7 +10,7 @@ namespace ZiggyCreatures.Caching.Fusion; /// -/// Represents an instance of a FusionCache. +/// The shared interface that models what a FusionCache instance can do. /// public interface IFusionCache : IDisposable @@ -160,6 +160,43 @@ public interface IFusionCache /// An optional to cancel the operation. void Remove(string key, FusionCacheEntryOptions? options = null, CancellationToken token = default); + /// + /// Expires the cache entry for the specified . + ///
+ ///
+ /// In the memory cache: + ///
+ /// - if fail-safe is enabled: the entry will marked as logically expired, but will still be available as a fallback value in case of future problems + ///
+ /// - if fail-safe is disabled: the entry will be effectively removed + ///
+ ///
+ /// In the distributed cache (if any), the entry will be effectively removed. + ///
+ /// The cache key which identifies the entry in the cache. + /// The options to adhere during this operation. If null is passed, will be used. + /// An optional to cancel the operation. + /// A to await the completion of the operation. + ValueTask ExpireAsync(string key, FusionCacheEntryOptions? options = null, CancellationToken token = default); + + /// + /// Expires the cache entry for the specified . + ///
+ ///
+ /// In the memory cache: + ///
+ /// - if fail-safe is enabled: the entry will marked as logically expired, but will still be available as a fallback value in case of future problems + ///
+ /// - if fail-safe is disabled: the entry will be effectively removed + ///
+ ///
+ /// In the distributed cache (if any), the entry will be effectively removed. + ///
+ /// The cache key which identifies the entry in the cache. + /// The options to adhere during this operation. If null is passed, will be used. + /// An optional to cancel the operation. + void Expire(string key, FusionCacheEntryOptions? options = null, CancellationToken token = default); + /// /// Sets a secondary caching layer, by providing an instance and an instance to be used to convert from generic values to byte[] and viceversa. /// diff --git a/src/ZiggyCreatures.FusionCache/Internals/Backplane/BackplaneAccessor.cs b/src/ZiggyCreatures.FusionCache/Internals/Backplane/BackplaneAccessor.cs index 1370080e..38d6c42e 100644 --- a/src/ZiggyCreatures.FusionCache/Internals/Backplane/BackplaneAccessor.cs +++ b/src/ZiggyCreatures.FusionCache/Internals/Backplane/BackplaneAccessor.cs @@ -18,7 +18,7 @@ internal sealed partial class BackplaneAccessor private readonly FusionCacheBackplaneEventsHub _events; private readonly SimpleCircuitBreaker _breaker; private readonly SemaphoreSlim _autoRecoveryLock = new SemaphoreSlim(1, 1); - private ConcurrentDictionary _autoRecoveryQueue = new ConcurrentDictionary(); + private readonly ConcurrentDictionary _autoRecoveryQueue = new ConcurrentDictionary(); public BackplaneAccessor(FusionCache cache, IFusionCacheBackplane backplane, FusionCacheOptions options, ILogger? logger, FusionCacheBackplaneEventsHub events) { @@ -51,7 +51,7 @@ private void UpdateLastError(string key, string operationId) if (res && hasChanged) { if (_logger?.IsEnabled(LogLevel.Warning) ?? false) - _logger.LogWarning("FUSION (O={CacheOperationId} K={CacheKey}): backplane temporarily de-activated for {BreakDuration}", operationId, key, _breaker.BreakDuration); + _logger.Log(LogLevel.Warning, "FUSION [N={CacheName}] (O={CacheOperationId} K={CacheKey}): backplane temporarily de-activated for {BreakDuration}", _cache.CacheName, operationId, key, _breaker.BreakDuration); // EVENT _events.OnCircuitBreakerChange(operationId, key, false); @@ -65,7 +65,7 @@ public bool IsCurrentlyUsable(string? operationId, string? key) if (res && hasChanged) { if (_logger?.IsEnabled(LogLevel.Warning) ?? false) - _logger.LogWarning("FUSION (O={CacheOperationId} K={CacheKey}): backplane activated again", operationId, key); + _logger.Log(LogLevel.Warning, "FUSION [N={CacheName}] (O={CacheOperationId} K={CacheKey}): backplane activated again", _cache.CacheName, operationId, key); // EVENT _events.OnCircuitBreakerChange(operationId, key, true); @@ -79,7 +79,7 @@ private void ProcessError(string operationId, string key, Exception exc, string if (exc is SyntheticTimeoutException) { if (_logger?.IsEnabled(_options.BackplaneSyntheticTimeoutsLogLevel) ?? false) - _logger.Log(_options.BackplaneSyntheticTimeoutsLogLevel, exc, "FUSION (O={CacheOperationId} K={CacheKey}): a synthetic timeout occurred while " + actionDescription, operationId, key); + _logger.Log(_options.BackplaneSyntheticTimeoutsLogLevel, exc, "FUSION [N={CacheName}] (O={CacheOperationId} K={CacheKey}): a synthetic timeout occurred while " + actionDescription, _cache.CacheName, operationId, key); return; } @@ -87,13 +87,13 @@ private void ProcessError(string operationId, string key, Exception exc, string UpdateLastError(key, operationId); if (_logger?.IsEnabled(_options.BackplaneErrorsLogLevel) ?? false) - _logger.Log(_options.BackplaneErrorsLogLevel, exc, "FUSION (O={CacheOperationId} K={CacheKey}): an error occurred while " + actionDescription, operationId, key); + _logger.Log(_options.BackplaneErrorsLogLevel, exc, "FUSION [N={CacheName}] (O={CacheOperationId} K={CacheKey}): an error occurred while " + actionDescription, _cache.CacheName, operationId, key); } - private void AddAutoRecoveryItem(BackplaneMessage message, FusionCacheEntryOptions options) + private bool TryAddAutoRecoveryItem(BackplaneMessage message, FusionCacheEntryOptions options) { if (message.CacheKey is null) - return; + return false; if (_options.BackplaneAutoRecoveryMaxItems.HasValue && _autoRecoveryQueue.Count >= _options.BackplaneAutoRecoveryMaxItems.Value && _autoRecoveryQueue.ContainsKey(message.CacheKey) == false) { @@ -115,35 +115,39 @@ private void AddAutoRecoveryItem(BackplaneMessage message, FusionCacheEntryOptio else { // IGNORE THE NEW ITEM - return; + return false; } } - } catch (Exception exc) { if (_logger?.IsEnabled(LogLevel.Error) ?? false) - _logger.Log(LogLevel.Error, exc, "FUSION: an error occurred while deciding which item in the backplane auto-recovery queue to remove to make space for a new one"); + _logger.Log(LogLevel.Error, exc, "FUSION [N={CacheName}]: an error occurred while deciding which item in the backplane auto-recovery queue to remove to make space for a new one", _cache.CacheName); } } _autoRecoveryQueue[message.CacheKey] = (message, options); if (_logger?.IsEnabled(LogLevel.Debug) ?? false) - _logger.Log(LogLevel.Debug, "FUSION (K={CacheKey}): added (or overwrote) an item to the backplane auto-recovery queue", message.CacheKey); + _logger.Log(LogLevel.Debug, "FUSION [N={CacheName}] (K={CacheKey}): added (or overwrote) an item to the backplane auto-recovery queue", _cache.CacheName, message.CacheKey); + + return true; } - private void ProcessAutoRecoveryQueue() + private bool TryProcessAutoRecoveryQueue() { + if (_options.EnableBackplaneAutoRecovery == false) + return false; + var _count = _autoRecoveryQueue.Count; if (_count == 0) - return; + return false; // ACQUIRE THE LOCK if (_autoRecoveryLock.Wait(0) == false) { // IF THE LOCK HAS NOT BEEN ACQUIRED IMMEDIATELY, SOMEONE ELSE IS ALREADY PROCESSING THE QUEUE, SO WE JUST RETURN - return; + return false; } _ = Task.Run(async () => @@ -153,7 +157,7 @@ private void ProcessAutoRecoveryQueue() // NOTE: THE COUNT USAGE HERE IN THE LOG IS JUST AN INDICATION: PER THE MULTI-THREADED NATURE OF THIS THING // IT'S OK IF THE NUMBER IS SINCE CHANGED AND IN THE FOREACH LOOP WE WILL ITERATE OVER MORE (OR LESS) ITEMS if (_logger?.IsEnabled(LogLevel.Debug) ?? false) - _logger.Log(LogLevel.Debug, "FUSION: starting backplane auto-recovery of about {Count} pending notifications", _count); + _logger.Log(LogLevel.Debug, "FUSION [N={CacheName}]: starting backplane auto-recovery of about {Count} pending notifications", _cache.CacheName, _count); _count = 0; foreach (var item in _autoRecoveryQueue) @@ -170,14 +174,14 @@ private void ProcessAutoRecoveryQueue() { // IF A PUBLISH DOESN'T GO THROUGH -> STOP PROCESSING THE QUEUE if (_logger?.IsEnabled(LogLevel.Debug) ?? false) - _logger.Log(LogLevel.Debug, "FUSION (O={CacheOperationId} K={CacheKey}): stopped backplane auto-recovery because of an error after {Count} processed items", _operationId, item.Value.Message.CacheKey, _count); + _logger.Log(LogLevel.Debug, "FUSION [N={CacheName}] (O={CacheOperationId} K={CacheKey}): stopped backplane auto-recovery because of an error after {Count} processed items", _cache.CacheName, _operationId, item.Value.Message.CacheKey, _count); return; } } if (_logger?.IsEnabled(LogLevel.Debug) ?? false) - _logger.Log(LogLevel.Debug, "FUSION: completed backplane auto-recovery of {Count} items", _count); + _logger.Log(LogLevel.Debug, "FUSION [N={CacheName}]: completed backplane auto-recovery of {Count} items", _cache.CacheName, _count); } finally { @@ -185,6 +189,8 @@ private void ProcessAutoRecoveryQueue() _autoRecoveryLock.Release(); } }); + + return true; } private bool CheckIncomingMessageForAutoRecoveryConflicts(BackplaneMessage message) @@ -217,7 +223,7 @@ public void Subscribe() new BackplaneSubscriptionOptions { ChannelName = _options.GetBackplaneChannelName(), - Handler = ProcessMessage + Handler = HandleIncomingMessage } ); } @@ -227,54 +233,73 @@ public void Unsubscribe() _backplane.Unsubscribe(); } - private void ProcessMessage(BackplaneMessage message) + private void HandleIncomingMessage(BackplaneMessage message) { - // AUTO-RECOVERY - if (_options.EnableBackplaneAutoRecovery) + // IGNORE NULL + if (message is null) { - if (CheckIncomingMessageForAutoRecoveryConflicts(message) == false) - { - if (_logger?.IsEnabled(LogLevel.Debug) ?? false) - _logger.Log(LogLevel.Debug, "A backplane notification has been received for {CacheKey}, but has been discarded since there is a newer pending one in the auto-recovery queue", message.CacheKey); - - ProcessAutoRecoveryQueue(); - - return; - } + if (_logger?.IsEnabled(_options.BackplaneErrorsLogLevel) ?? false) + _logger.Log(_options.BackplaneErrorsLogLevel, "FUSION [N={CacheName} I={CacheInstanceId}]: a null backplane notification has been received (what!?)", _cache.CacheName, _cache.InstanceId); - ProcessAutoRecoveryQueue(); + return; } // IGNORE INVALID MESSAGES - if (message is null || message.IsValid() == false) + if (message.IsValid() == false) { if (_logger?.IsEnabled(_options.BackplaneErrorsLogLevel) ?? false) - _logger.Log(_options.BackplaneErrorsLogLevel, "An invalid message has been received on the backplane from cache {CacheInstanceId} for key {CacheKey} and action {Action}", message?.SourceId, message?.CacheKey, message?.Action); + _logger.Log(_options.BackplaneErrorsLogLevel, "FUSION [N={CacheName} I={CacheInstanceId}] (K={CacheKey}): an invalid backplane notification has been received from remote cache {RemoteCacheInstanceId} (A={Action}, T={InstanceTicks})", _cache.CacheName, _cache.InstanceId, message.CacheKey, message.SourceId, message.Action, message.InstantTicks); + TryProcessAutoRecoveryQueue(); return; } // IGNORE MESSAGES FROM THIS SOURCE if (message.SourceId == _cache.InstanceId) + { + TryProcessAutoRecoveryQueue(); return; + } + + // AUTO-RECOVERY + if (_options.EnableBackplaneAutoRecovery) + { + if (CheckIncomingMessageForAutoRecoveryConflicts(message) == false) + { + if (_logger?.IsEnabled(LogLevel.Debug) ?? false) + _logger.Log(LogLevel.Debug, "FUSION [N={CacheName} I={CacheInstanceId}] (K={CacheKey}): a backplane notification has been received from remote cache {RemoteCacheInstanceId}, but has been discarded since there is a pending one in the auto-recovery queue which is more recent", _cache.CacheName, _cache.InstanceId, message.CacheKey, message.SourceId); + TryProcessAutoRecoveryQueue(); + return; + } + + TryProcessAutoRecoveryQueue(); + } + + // PROCESS MESSAGE switch (message.Action) { case BackplaneMessageAction.EntrySet: - _cache.EvictInternal(message.CacheKey!, true); - if (_logger?.IsEnabled(LogLevel.Debug) ?? false) - _logger.Log(LogLevel.Debug, "A backplane notification has been received for {CacheKey} (SET)", message.CacheKey); + _logger.Log(LogLevel.Debug, "FUSION [N={CacheName} I={CacheInstanceId}] (K={CacheKey}): a backplane notification has been received from remote cache {RemoteCacheInstanceId} (SET)", _cache.CacheName, _cache.InstanceId, message.CacheKey, message.SourceId); + + _cache.ExpireMemoryEntryInternal(message.CacheKey!, true); break; case BackplaneMessageAction.EntryRemove: - _cache.EvictInternal(message.CacheKey!, false); + if (_logger?.IsEnabled(LogLevel.Debug) ?? false) + _logger.Log(LogLevel.Debug, "FUSION [N={CacheName} I={CacheInstanceId}] (K={CacheKey}): a backplane notification has been received from remote cache {RemoteCacheInstanceId} (REMOVE)", _cache.CacheName, _cache.InstanceId, message.CacheKey, message.SourceId); + _cache.ExpireMemoryEntryInternal(message.CacheKey!, false); + break; + case BackplaneMessageAction.EntryExpire: if (_logger?.IsEnabled(LogLevel.Debug) ?? false) - _logger.Log(LogLevel.Debug, "A backplane notification has been received for {CacheKey} (REMOVE)", message.CacheKey); + _logger.Log(LogLevel.Debug, "FUSION [N={CacheName} I={CacheInstanceId}] (K={CacheKey}): a backplane notification has been received from remote cache {RemoteCacheInstanceId} (EXPIRE)", _cache.CacheName, _cache.InstanceId, message.CacheKey, message.SourceId); + + _cache.ExpireMemoryEntryInternal(message.CacheKey!, true); break; default: if (_logger?.IsEnabled(_options.BackplaneErrorsLogLevel) ?? false) - _logger.Log(_options.BackplaneErrorsLogLevel, "An unknown backplane notification has been received for {CacheKey}: {Type}", message.CacheKey, message.Action); + _logger.Log(_options.BackplaneErrorsLogLevel, "FUSION [N={CacheName} I={CacheInstanceId}] (K={CacheKey}): an backplane notification has been received from remote cache {RemoteCacheInstanceId} for an unknown action {Action}", _cache.CacheName, _cache.InstanceId, message.CacheKey, message.SourceId, message.Action); break; } diff --git a/src/ZiggyCreatures.FusionCache/Internals/Backplane/BackplaneAccessor_Async.cs b/src/ZiggyCreatures.FusionCache/Internals/Backplane/BackplaneAccessor_Async.cs index cad6a7a6..b8e7e29f 100644 --- a/src/ZiggyCreatures.FusionCache/Internals/Backplane/BackplaneAccessor_Async.cs +++ b/src/ZiggyCreatures.FusionCache/Internals/Backplane/BackplaneAccessor_Async.cs @@ -18,7 +18,7 @@ private async ValueTask ExecuteOperationAsync(string operationId, string key, Fu var actionDescriptionInner = actionDescription + (options.AllowBackgroundBackplaneOperations ? " (background)" : null); if (_logger?.IsEnabled(LogLevel.Debug) ?? false) - _logger.LogDebug("FUSION (O={CacheOperationId} K={CacheKey}): " + actionDescriptionInner, operationId, key); + _logger.Log(LogLevel.Debug, "FUSION [N={CacheName} I={CacheInstanceId}] (O={CacheOperationId} K={CacheKey}): " + actionDescriptionInner, _cache.CacheName, _cache.InstanceId, operationId, key); await FusionCacheExecutionUtils .RunAsyncActionAdvancedAsync( @@ -47,7 +47,7 @@ public async ValueTask PublishAsync(string operationId, BackplaneMessage m { // IGNORE MESSAGES -NOT- FROM THIS SOURCE if (_logger?.IsEnabled(LogLevel.Warning) ?? false) - _logger.Log(LogLevel.Warning, "FUSION (O={CacheOperationId} K={CacheKey}): cannot send a backplane message" + (isFromAutoRecovery ? " (auto-recovery)" : String.Empty) + " with a SourceId different than the local one (IFusionCache.InstanceId)", operationId, message.CacheKey); + _logger.Log(LogLevel.Warning, "FUSION [N={CacheName} I={CacheInstanceId}] (O={CacheOperationId} K={CacheKey}): cannot send a backplane message" + (isFromAutoRecovery ? " (auto-recovery)" : String.Empty) + " with a SourceId different than the local one (IFusionCache.InstanceId)", _cache.CacheName, _cache.InstanceId, operationId, message.CacheKey); return false; } @@ -56,7 +56,7 @@ public async ValueTask PublishAsync(string operationId, BackplaneMessage m { // IGNORE INVALID MESSAGES if (_logger?.IsEnabled(LogLevel.Warning) ?? false) - _logger.Log(LogLevel.Warning, "FUSION (O={CacheOperationId} K={CacheKey}): cannot send an invalid backplane message" + (isFromAutoRecovery ? " (auto-recovery)" : String.Empty), operationId, message.CacheKey); + _logger.Log(LogLevel.Warning, "FUSION [N={CacheName} I={CacheInstanceId}] (O={CacheOperationId} K={CacheKey}): cannot send an invalid backplane message" + (isFromAutoRecovery ? " (auto-recovery)" : String.Empty), _cache.CacheName, _cache.InstanceId, operationId, message.CacheKey); return false; } @@ -72,16 +72,22 @@ await ExecuteOperationAsync( { await _backplane.PublishAsync(message, options, ct).ConfigureAwait(false); + if (_logger?.IsEnabled(LogLevel.Debug) ?? false) + _logger.Log(LogLevel.Debug, "FUSION [N={CacheName} I={CacheInstanceId}] (O={CacheOperationId} K={CacheKey}): a notification has been sent" + (isFromAutoRecovery ? " (auto-recovery)" : String.Empty) + " ({Action})", _cache.CacheName, _cache.InstanceId, operationId, message.CacheKey, message.Action); + if (isFromAutoRecovery == false && _options.EnableBackplaneAutoRecovery) { - ProcessAutoRecoveryQueue(); + TryProcessAutoRecoveryQueue(); } } - catch + catch (Exception exc) { + if (_logger?.IsEnabled(_options.BackplaneErrorsLogLevel) ?? false) + _logger.Log(_options.BackplaneErrorsLogLevel, exc, "FUSION [N={CacheName} I={CacheInstanceId}] (O={CacheOperationId} K={CacheKey}): a notification has been sent" + (isFromAutoRecovery ? " (auto-recovery)" : String.Empty) + " ({Action})", _cache.CacheName, _cache.InstanceId, operationId, message.CacheKey, message.Action); + if (isFromAutoRecovery == false && _options.EnableBackplaneAutoRecovery) { - AddAutoRecoveryItem(message, options); + TryAddAutoRecoveryItem(message, options); } throw; @@ -90,7 +96,7 @@ await ExecuteOperationAsync( // EVENT _events.OnMessagePublished(operationId, message); }, - "sending backplane notification" + (isFromAutoRecovery ? " (auto-recovery)" : String.Empty), + "sending a backplane notification" + (isFromAutoRecovery ? " (auto-recovery)" : String.Empty), options, token ).ConfigureAwait(false); diff --git a/src/ZiggyCreatures.FusionCache/Internals/Backplane/BackplaneAccessor_Sync.cs b/src/ZiggyCreatures.FusionCache/Internals/Backplane/BackplaneAccessor_Sync.cs index 3b603ae3..2de79821 100644 --- a/src/ZiggyCreatures.FusionCache/Internals/Backplane/BackplaneAccessor_Sync.cs +++ b/src/ZiggyCreatures.FusionCache/Internals/Backplane/BackplaneAccessor_Sync.cs @@ -17,7 +17,7 @@ private void ExecuteOperation(string operationId, string key, Action(string operationId, string key, IFu token.ThrowIfCancellationRequested(); + // IF FAIL-SAFE IS DISABLED AND DURATION IS <= ZERO -> REMOVE ENTRY (WILL SAVE RESOURCES) + if (options.IsFailSafeEnabled == false && options.DistributedCacheDuration.GetValueOrDefault(options.Duration) <= TimeSpan.Zero) + { + await RemoveEntryAsync(operationId, key, options, token).ConfigureAwait(false); + return; + } + var distributedEntry = entry.AsDistributedEntry(options); // SERIALIZATION @@ -53,14 +60,14 @@ public async ValueTask SetEntryAsync(string operationId, string key, IFu try { if (_logger?.IsEnabled(LogLevel.Debug) ?? false) - _logger.Log(LogLevel.Debug, "FUSION (O={CacheOperationId} K={CacheKey}): serializing the entry {Entry}", operationId, key, distributedEntry.ToLogString()); + _logger.Log(LogLevel.Debug, "FUSION [N={CacheName}] (O={CacheOperationId} K={CacheKey}): serializing the entry {Entry}", _options.CacheName, operationId, key, distributedEntry.ToLogString()); data = await _serializer.SerializeAsync(distributedEntry).ConfigureAwait(false); } catch (Exception exc) { if (_logger?.IsEnabled(_options.SerializationErrorsLogLevel) ?? false) - _logger.Log(_options.SerializationErrorsLogLevel, exc, "FUSION (O={CacheOperationId} K={CacheKey}): an error occurred while serializing an entry {Entry}", operationId, key, distributedEntry.ToLogString()); + _logger.Log(_options.SerializationErrorsLogLevel, exc, "FUSION [N={CacheName}] (O={CacheOperationId} K={CacheKey}): an error occurred while serializing an entry {Entry}", _options.CacheName, operationId, key, distributedEntry.ToLogString()); if (options.ReThrowSerializationExceptions) throw; @@ -82,7 +89,7 @@ await ExecuteOperationAsync( async ct => { if (_logger?.IsEnabled(LogLevel.Debug) ?? false) - _logger.Log(LogLevel.Debug, "FUSION (O={CacheOperationId} K={CacheKey}): setting the entry in distributed {Entry}", operationId, key, distributedEntry.ToLogString()); + _logger.Log(LogLevel.Debug, "FUSION [N={CacheName}] (O={CacheOperationId} K={CacheKey}): setting the entry in distributed {Entry}", _options.CacheName, operationId, key, distributedEntry.ToLogString()); await _cache.SetAsync(MaybeProcessCacheKey(key), data, distributedOptions, ct).ConfigureAwait(false); @@ -102,7 +109,7 @@ await ExecuteOperationAsync( return (null, false); if (_logger?.IsEnabled(LogLevel.Trace) ?? false) - _logger.LogTrace("FUSION (O={CacheOperationId} K={CacheKey}): trying to get entry from distributed", operationId, key); + _logger.Log(LogLevel.Trace, "FUSION [N={CacheName}] (O={CacheOperationId} K={CacheKey}): trying to get entry from distributed", _options.CacheName, operationId, key); // GET FROM DISTRIBUTED CACHE byte[]? data; @@ -133,19 +140,19 @@ await ExecuteOperationAsync( if (entry is null) { if (_logger?.IsEnabled(LogLevel.Debug) ?? false) - _logger.LogDebug("FUSION (O={CacheOperationId} K={CacheKey}): distributed entry not found", operationId, key); + _logger.Log(LogLevel.Debug, "FUSION [N={CacheName}] (O={CacheOperationId} K={CacheKey}): distributed entry not found", _options.CacheName, operationId, key); } else { if (entry.IsLogicallyExpired()) { if (_logger?.IsEnabled(LogLevel.Debug) ?? false) - _logger.LogDebug("FUSION (O={CacheOperationId} K={CacheKey}): distributed entry found (expired) {Entry}", operationId, key, entry.ToLogString()); + _logger.Log(LogLevel.Debug, "FUSION [N={CacheName}] (O={CacheOperationId} K={CacheKey}): distributed entry found (expired) {Entry}", _options.CacheName, operationId, key, entry.ToLogString()); } else { if (_logger?.IsEnabled(LogLevel.Debug) ?? false) - _logger.LogDebug("FUSION (O={CacheOperationId} K={CacheKey}): distributed entry found {Entry}", operationId, key, entry.ToLogString()); + _logger.Log(LogLevel.Debug, "FUSION [N={CacheName}] (O={CacheOperationId} K={CacheKey}): distributed entry found {Entry}", _options.CacheName, operationId, key, entry.ToLogString()); isValid = true; } @@ -166,7 +173,7 @@ await ExecuteOperationAsync( catch (Exception exc) { if (_logger?.IsEnabled(_options.SerializationErrorsLogLevel) ?? false) - _logger.Log(_options.SerializationErrorsLogLevel, exc, "FUSION (O={CacheOperationId} K={CacheKey}): an error occurred while deserializing an entry", operationId, key); + _logger.Log(_options.SerializationErrorsLogLevel, exc, "FUSION [N={CacheName}] (O={CacheOperationId} K={CacheKey}): an error occurred while deserializing an entry", _options.CacheName, operationId, key); if (options.ReThrowSerializationExceptions) throw; diff --git a/src/ZiggyCreatures.FusionCache/Internals/Distributed/DistributedCacheAccessor_Sync.cs b/src/ZiggyCreatures.FusionCache/Internals/Distributed/DistributedCacheAccessor_Sync.cs index b963a7c0..cdd59bd3 100644 --- a/src/ZiggyCreatures.FusionCache/Internals/Distributed/DistributedCacheAccessor_Sync.cs +++ b/src/ZiggyCreatures.FusionCache/Internals/Distributed/DistributedCacheAccessor_Sync.cs @@ -19,9 +19,9 @@ private void ExecuteOperation(string operationId, string key, Action(string operationId, string key, IFusionCacheEntry e token.ThrowIfCancellationRequested(); + // IF FAIL-SAFE IS DISABLED AND DURATION IS <= ZERO -> REMOVE ENTRY (WILL SAVE RESOURCES) + if (options.IsFailSafeEnabled == false && options.DistributedCacheDuration.GetValueOrDefault(options.Duration) <= TimeSpan.Zero) + { + RemoveEntry(operationId, key, options, token); + return; + } + var distributedEntry = entry.AsDistributedEntry(options); // SERIALIZATION @@ -51,14 +58,14 @@ public void SetEntry(string operationId, string key, IFusionCacheEntry e try { if (_logger?.IsEnabled(LogLevel.Debug) ?? false) - _logger.Log(LogLevel.Debug, "FUSION (O={CacheOperationId} K={CacheKey}): serializing the entry {Entry}", operationId, key, distributedEntry.ToLogString()); + _logger.Log(LogLevel.Debug, "FUSION [N={CacheName}] (O={CacheOperationId} K={CacheKey}): serializing the entry {Entry}", _options.CacheName, operationId, key, distributedEntry.ToLogString()); data = _serializer.Serialize(distributedEntry); } catch (Exception exc) { if (_logger?.IsEnabled(_options.SerializationErrorsLogLevel) ?? false) - _logger.Log(_options.SerializationErrorsLogLevel, exc, "FUSION (O={CacheOperationId} K={CacheKey}): an error occurred while serializing an entry {Entry}", operationId, key, distributedEntry.ToLogString()); + _logger.Log(_options.SerializationErrorsLogLevel, exc, "FUSION [N={CacheName}] (O={CacheOperationId} K={CacheKey}): an error occurred while serializing an entry {Entry}", _options.CacheName, operationId, key, distributedEntry.ToLogString()); if (options.ReThrowSerializationExceptions) throw; @@ -77,10 +84,10 @@ public void SetEntry(string operationId, string key, IFusionCacheEntry e ExecuteOperation( operationId, key, - ct => + _ => { if (_logger?.IsEnabled(LogLevel.Debug) ?? false) - _logger.Log(LogLevel.Debug, "FUSION (O={CacheOperationId} K={CacheKey}): setting the entry in distributed {Entry}", operationId, key, distributedEntry.ToLogString()); + _logger.Log(LogLevel.Debug, "FUSION [N={CacheName}] (O={CacheOperationId} K={CacheKey}): setting the entry in distributed {Entry}", _options.CacheName, operationId, key, distributedEntry.ToLogString()); _cache.Set(MaybeProcessCacheKey(key), data, distributedOptions); @@ -100,7 +107,7 @@ public void SetEntry(string operationId, string key, IFusionCacheEntry e return (null, false); if (_logger?.IsEnabled(LogLevel.Trace) ?? false) - _logger.LogTrace("FUSION (O={CacheOperationId} K={CacheKey}): trying to get entry from distributed", operationId, key); + _logger.Log(LogLevel.Trace, "FUSION [N={CacheName}] (O={CacheOperationId} K={CacheKey}): trying to get entry from distributed", _options.CacheName, operationId, key); // GET FROM DISTRIBUTED CACHE byte[]? data; @@ -131,19 +138,19 @@ public void SetEntry(string operationId, string key, IFusionCacheEntry e if (entry is null) { if (_logger?.IsEnabled(LogLevel.Debug) ?? false) - _logger.LogDebug("FUSION (O={CacheOperationId} K={CacheKey}): distributed entry not found", operationId, key); + _logger.Log(LogLevel.Debug, "FUSION [N={CacheName}] (O={CacheOperationId} K={CacheKey}): distributed entry not found", _options.CacheName, operationId, key); } else { if (entry.IsLogicallyExpired()) { if (_logger?.IsEnabled(LogLevel.Debug) ?? false) - _logger.LogDebug("FUSION (O={CacheOperationId} K={CacheKey}): distributed entry found (expired) {Entry}", operationId, key, entry.ToLogString()); + _logger.Log(LogLevel.Debug, "FUSION [N={CacheName}] (O={CacheOperationId} K={CacheKey}): distributed entry found (expired) {Entry}", _options.CacheName, operationId, key, entry.ToLogString()); } else { if (_logger?.IsEnabled(LogLevel.Debug) ?? false) - _logger.LogDebug("FUSION (O={CacheOperationId} K={CacheKey}): distributed entry found {Entry}", operationId, key, entry.ToLogString()); + _logger.Log(LogLevel.Debug, "FUSION [N={CacheName}] (O={CacheOperationId} K={CacheKey}): distributed entry found {Entry}", _options.CacheName, operationId, key, entry.ToLogString()); isValid = true; } @@ -164,7 +171,7 @@ public void SetEntry(string operationId, string key, IFusionCacheEntry e catch (Exception exc) { if (_logger?.IsEnabled(_options.SerializationErrorsLogLevel) ?? false) - _logger.Log(_options.SerializationErrorsLogLevel, exc, "FUSION (O={CacheOperationId} K={CacheKey}): an error occurred while deserializing an entry", operationId, key); + _logger.Log(_options.SerializationErrorsLogLevel, exc, "FUSION [N={CacheName}] (O={CacheOperationId} K={CacheKey}): an error occurred while deserializing an entry", _options.CacheName, operationId, key); if (options.ReThrowSerializationExceptions) throw; diff --git a/src/ZiggyCreatures.FusionCache/Internals/Distributed/FusionCacheDistributedEntry.cs b/src/ZiggyCreatures.FusionCache/Internals/Distributed/FusionCacheDistributedEntry.cs index 74684bd1..b0780576 100644 --- a/src/ZiggyCreatures.FusionCache/Internals/Distributed/FusionCacheDistributedEntry.cs +++ b/src/ZiggyCreatures.FusionCache/Internals/Distributed/FusionCacheDistributedEntry.cs @@ -1,5 +1,6 @@ using System; using System.Runtime.Serialization; +using ZiggyCreatures.Caching.Fusion.Internals.Memory; namespace ZiggyCreatures.Caching.Fusion.Internals.Distributed; @@ -16,10 +17,12 @@ public sealed class FusionCacheDistributedEntry /// /// The actual value. /// The metadata for the entry. - public FusionCacheDistributedEntry(TValue value, FusionCacheEntryMetadata? metadata) + /// The original timestamp of the entry, see . + public FusionCacheDistributedEntry(TValue value, FusionCacheEntryMetadata? metadata, long? timestamp = null) { Value = value; Metadata = metadata; + Timestamp = timestamp ?? FusionCacheInternalUtils.GetCurrentTimestamp(); } #pragma warning disable CS8618 // Non-nullable field must contain a non-null value when exiting constructor. Consider declaring as nullable. @@ -42,6 +45,10 @@ public FusionCacheDistributedEntry() [DataMember(Name = "m", EmitDefaultValue = false)] public FusionCacheEntryMetadata? Metadata { get; set; } + /// + [DataMember(Name = "t", EmitDefaultValue = false)] + public long? Timestamp { get; set; } + /// public TValue1 GetValue() { @@ -79,13 +86,51 @@ public override string ToString() /// Indicates if the value comes from a fail-safe activation. /// If provided, it's the last modified date of the entry: this may be used in the next refresh cycle (eg: with the use of the "If-Modified-Since" header in an http request) to check if the entry is changed, to avoid getting the entire value. /// If provided, it's the ETag of the entry: this may be used in the next refresh cycle (eg: with the use of the "If-None-Match" header in an http request) to check if the entry is changed, to avoid getting the entire value. + /// The value for the property. /// The newly created entry. - public static FusionCacheDistributedEntry CreateFromOptions(TValue value, FusionCacheEntryOptions options, bool isFromFailSafe, DateTimeOffset? lastModified, string? etag) + public static FusionCacheDistributedEntry CreateFromOptions(TValue value, FusionCacheEntryOptions options, bool isFromFailSafe, DateTimeOffset? lastModified, string? etag, long? timestamp) { var exp = FusionCacheInternalUtils.GetNormalizedAbsoluteExpiration(isFromFailSafe ? options.FailSafeThrottleDuration : options.DistributedCacheDuration.GetValueOrDefault(options.Duration), options, false); var eagerExp = FusionCacheInternalUtils.GetNormalizedEagerExpiration(isFromFailSafe, options.EagerRefreshThreshold, exp); - return new FusionCacheDistributedEntry(value, new FusionCacheEntryMetadata(exp, isFromFailSafe, eagerExp, etag, lastModified)); + return new FusionCacheDistributedEntry( + value, + new FusionCacheEntryMetadata(exp, isFromFailSafe, eagerExp, etag, lastModified), + timestamp ?? FusionCacheInternalUtils.GetCurrentTimestamp() + ); + } + + /// + /// Creates a new instance from another entry and some options. + /// + /// The source entry. + /// The object to configure the entry. + /// The newly created entry. + public static FusionCacheDistributedEntry CreateFromOtherEntry(IFusionCacheEntry entry, FusionCacheEntryOptions options) + { + //if (options.IsFailSafeEnabled == false && entry.Metadata is null && options.EagerRefreshThreshold.HasValue == false) + // return new FusionCacheDistributedEntry(entry.GetValue(), null); + + var isFromFailSafe = entry.Metadata?.IsFromFailSafe ?? false; + + DateTimeOffset exp; + + if (entry.Metadata is not null) + { + exp = entry.Metadata.LogicalExpiration; + } + else + { + exp = FusionCacheInternalUtils.GetNormalizedAbsoluteExpiration(isFromFailSafe ? options.FailSafeThrottleDuration : options.Duration, options, true); + } + + var eagerExp = FusionCacheInternalUtils.GetNormalizedEagerExpiration(isFromFailSafe, options.EagerRefreshThreshold, exp); + + return new FusionCacheDistributedEntry( + entry.GetValue(), + new FusionCacheEntryMetadata(exp, isFromFailSafe, eagerExp, entry.Metadata?.ETag, entry.Metadata?.LastModified), + entry.Timestamp + ); } } diff --git a/src/ZiggyCreatures.FusionCache/Internals/FusionCacheInternalUtils.cs b/src/ZiggyCreatures.FusionCache/Internals/FusionCacheInternalUtils.cs index e9e10cfa..7dc470bf 100644 --- a/src/ZiggyCreatures.FusionCache/Internals/FusionCacheInternalUtils.cs +++ b/src/ZiggyCreatures.FusionCache/Internals/FusionCacheInternalUtils.cs @@ -18,38 +18,12 @@ internal static class FusionCacheInternalUtils private static readonly DateTimeOffset DateTimeOffsetMaxValue = DateTimeOffset.MaxValue; private static readonly TimeSpan TimeSpanMaxValue = TimeSpan.MaxValue; - public static string GenerateOperationId_V1() + public static long GetCurrentTimestamp() { - return Guid.NewGuid().ToString("N"); + return DateTimeOffset.UtcNow.ToUnixTimeMilliseconds(); } - private static string GenerateOperationId_V2(long id) - { - var buffer = new char[13]; - - buffer[12] = _chars[id & 31]; - buffer[11] = _chars[(id >> 5) & 31]; - buffer[10] = _chars[(id >> 10) & 31]; - buffer[9] = _chars[(id >> 15) & 31]; - buffer[8] = _chars[(id >> 20) & 31]; - buffer[7] = _chars[(id >> 25) & 31]; - buffer[6] = _chars[(id >> 30) & 31]; - buffer[5] = _chars[(id >> 35) & 31]; - buffer[4] = _chars[(id >> 40) & 31]; - buffer[3] = _chars[(id >> 45) & 31]; - buffer[2] = _chars[(id >> 50) & 31]; - buffer[1] = _chars[(id >> 55) & 31]; - buffer[0] = _chars[(id >> 60) & 31]; - - return new string(buffer); - } - - public static string GenerateOperationId_V2() - { - return GenerateOperationId_V2(Interlocked.Increment(ref _lastId)); - } - - private static string GenerateOperationId_V3(long id) + private static string GenerateOperationId(long id) { // SEE: https://nimaara.com/2018/10/10/generating-ids-in-csharp.html @@ -72,9 +46,9 @@ private static string GenerateOperationId_V3(long id) return new string(buffer, 0, buffer.Length); } - public static string GenerateOperationId_V3() + public static string GenerateOperationId() { - return GenerateOperationId_V3(Interlocked.Increment(ref _lastId)); + return GenerateOperationId(Interlocked.Increment(ref _lastId)); } public static string MaybeGenerateOperationId(ILogger? logger) @@ -82,7 +56,7 @@ public static string MaybeGenerateOperationId(ILogger? logger) if (logger is null) return string.Empty; - return GenerateOperationId_V3(); + return GenerateOperationId(); } /// @@ -237,15 +211,16 @@ public static FusionCacheDistributedEntry AsDistributedEntry(thi if (entry is FusionCacheDistributedEntry) return (FusionCacheDistributedEntry)entry; - return FusionCacheDistributedEntry.CreateFromOptions(entry.GetValue(), options, entry.Metadata?.IsFromFailSafe ?? false, entry.Metadata?.LastModified, entry.Metadata?.ETag); + return FusionCacheDistributedEntry.CreateFromOptions(entry.GetValue(), options, entry.Metadata?.IsFromFailSafe ?? false, entry.Metadata?.LastModified, entry.Metadata?.ETag, entry.Timestamp); + //return FusionCacheDistributedEntry.CreateFromOtherEntry(entry, options); } - public static FusionCacheMemoryEntry AsMemoryEntry(this IFusionCacheEntry entry, FusionCacheEntryOptions options) + public static FusionCacheMemoryEntry AsMemoryEntry(this IFusionCacheEntry entry, FusionCacheEntryOptions options) { if (entry is FusionCacheMemoryEntry) return (FusionCacheMemoryEntry)entry; - return FusionCacheMemoryEntry.CreateFromOptions(entry.GetValue(), options, entry.Metadata?.IsFromFailSafe ?? false, entry.Metadata?.LastModified, entry.Metadata?.ETag); + return FusionCacheMemoryEntry.CreateFromOtherEntry(entry, options); } public static void SafeExecute(this EventHandler ev, string? operationId, string? key, IFusionCache cache, Func eventArgsBuilder, string eventName, ILogger? logger, LogLevel logLevel, bool syncExecution) @@ -260,7 +235,7 @@ static void ExecuteInvocations(string? operationId, string? key, IFusionCache ca } catch (Exception exc) { - logger?.Log(errorLogLevel, exc, "FUSION (O={CacheOperationId} K={CacheKey}): an error occurred while handling an event handler for {EventName}", operationId, key, eventName); + logger?.Log(errorLogLevel, exc, "FUSION [N={CacheName}] (O={CacheOperationId} K={CacheKey}): an error occurred while handling an event handler for {EventName}", cache.CacheName, operationId, key, eventName); } } } @@ -332,4 +307,15 @@ public static DateTimeOffset GetNormalizedAbsoluteExpiration(TimeSpan duration, return now.AddTicks((long)((normalizedExpiration - now).Ticks * eagerRefreshThreshold.Value)); } + + public static bool CanBeUsed(this DistributedCacheAccessor? dca, string? operationId, string? key) + { + if (dca is null) + return false; + + if (dca.IsCurrentlyUsable(operationId, key)) + return true; + + return false; + } } diff --git a/src/ZiggyCreatures.FusionCache/Internals/IFusionCacheEntry.cs b/src/ZiggyCreatures.FusionCache/Internals/IFusionCacheEntry.cs index 8113e266..230c3393 100644 --- a/src/ZiggyCreatures.FusionCache/Internals/IFusionCacheEntry.cs +++ b/src/ZiggyCreatures.FusionCache/Internals/IFusionCacheEntry.cs @@ -26,4 +26,9 @@ public interface IFusionCacheEntry /// Metadata about the cache entry. /// FusionCacheEntryMetadata? Metadata { get; } + + /// + /// The timestamp at which the cached value has been originally created: memory cache entries created from distributed cache entries will have the exact same timestamp. + /// + public long? Timestamp { get; } } diff --git a/src/ZiggyCreatures.FusionCache/Internals/Memory/FusionCacheMemoryEntry.cs b/src/ZiggyCreatures.FusionCache/Internals/Memory/FusionCacheMemoryEntry.cs index f45414da..e3363f19 100644 --- a/src/ZiggyCreatures.FusionCache/Internals/Memory/FusionCacheMemoryEntry.cs +++ b/src/ZiggyCreatures.FusionCache/Internals/Memory/FusionCacheMemoryEntry.cs @@ -13,10 +13,12 @@ internal sealed class FusionCacheMemoryEntry /// /// The actual value. /// The metadata for the entry. - public FusionCacheMemoryEntry(object? value, FusionCacheEntryMetadata? metadata) + /// The original timestamp of the entry, see . + public FusionCacheMemoryEntry(object? value, FusionCacheEntryMetadata? metadata, long? timestamp) { Value = value; Metadata = metadata; + Timestamp = timestamp; } /// @@ -25,6 +27,9 @@ public FusionCacheMemoryEntry(object? value, FusionCacheEntryMetadata? metadata) /// public FusionCacheEntryMetadata? Metadata { get; } + /// + public long? Timestamp { get; } + /// public TValue GetValue() { @@ -58,17 +63,26 @@ public override string ToString() /// Indicates if the value comes from a fail-safe activation. /// If provided, it's the last modified date of the entry: this may be used in the next refresh cycle (eg: with the use of the "If-Modified-Since" header in an http request) to check if the entry is changed, to avoid getting the entire value. /// If provided, it's the ETag of the entry: this may be used in the next refresh cycle (eg: with the use of the "If-None-Match" header in an http request) to check if the entry is changed, to avoid getting the entire value. + /// The value for the property. /// The newly created entry. - public static FusionCacheMemoryEntry CreateFromOptions(object? value, FusionCacheEntryOptions options, bool isFromFailSafe, DateTimeOffset? lastModified, string? etag) + public static FusionCacheMemoryEntry CreateFromOptions(object? value, FusionCacheEntryOptions options, bool isFromFailSafe, DateTimeOffset? lastModified, string? etag, long? timestamp) { if (options.IsFailSafeEnabled == false && options.EagerRefreshThreshold.HasValue == false) - return new FusionCacheMemoryEntry(value, null); + return new FusionCacheMemoryEntry( + value, + null, + FusionCacheInternalUtils.GetCurrentTimestamp() + ); var exp = FusionCacheInternalUtils.GetNormalizedAbsoluteExpiration(isFromFailSafe ? options.FailSafeThrottleDuration : options.Duration, options, true); var eagerExp = FusionCacheInternalUtils.GetNormalizedEagerExpiration(isFromFailSafe, options.EagerRefreshThreshold, exp); - return new FusionCacheMemoryEntry(value, new FusionCacheEntryMetadata(exp, isFromFailSafe, eagerExp, etag, lastModified)); + return new FusionCacheMemoryEntry( + value, + new FusionCacheEntryMetadata(exp, isFromFailSafe, eagerExp, etag, lastModified), + timestamp ?? FusionCacheInternalUtils.GetCurrentTimestamp() + ); } /// @@ -80,7 +94,11 @@ public static FusionCacheMemoryEntry CreateFromOptions(object? value, FusionCach public static FusionCacheMemoryEntry CreateFromOtherEntry(IFusionCacheEntry entry, FusionCacheEntryOptions options) { if (options.IsFailSafeEnabled == false && entry.Metadata is null && options.EagerRefreshThreshold.HasValue == false) - return new FusionCacheMemoryEntry(entry.GetValue(), null); + return new FusionCacheMemoryEntry( + entry.GetValue(), + null, + entry.Timestamp + ); var isFromFailSafe = entry.Metadata?.IsFromFailSafe ?? false; @@ -97,6 +115,10 @@ public static FusionCacheMemoryEntry CreateFromOtherEntry(IFusionCacheEn var eagerExp = FusionCacheInternalUtils.GetNormalizedEagerExpiration(isFromFailSafe, options.EagerRefreshThreshold, exp); - return new FusionCacheMemoryEntry(entry.GetValue(), new FusionCacheEntryMetadata(exp, isFromFailSafe, eagerExp, entry.Metadata?.ETag, entry.Metadata?.LastModified)); + return new FusionCacheMemoryEntry( + entry.GetValue(), + new FusionCacheEntryMetadata(exp, isFromFailSafe, eagerExp, entry.Metadata?.ETag, entry.Metadata?.LastModified), + entry.Timestamp + ); } } diff --git a/src/ZiggyCreatures.FusionCache/Internals/Memory/MemoryCacheAccessor.cs b/src/ZiggyCreatures.FusionCache/Internals/Memory/MemoryCacheAccessor.cs index c155b365..ca7382a9 100644 --- a/src/ZiggyCreatures.FusionCache/Internals/Memory/MemoryCacheAccessor.cs +++ b/src/ZiggyCreatures.FusionCache/Internals/Memory/MemoryCacheAccessor.cs @@ -32,10 +32,17 @@ public MemoryCacheAccessor(IMemoryCache? memoryCache, FusionCacheOptions options public void SetEntry(string operationId, string key, FusionCacheMemoryEntry entry, FusionCacheEntryOptions options) { + // IF FAIL-SAFE IS DISABLED AND DURATION IS <= ZERO -> REMOVE ENTRY (WILL SAVE RESOURCES) + if (options.IsFailSafeEnabled == false && options.Duration <= TimeSpan.Zero) + { + RemoveEntry(operationId, key, options); + return; + } + var memoryOptions = options.ToMemoryCacheEntryOptions(_events, _options, _logger, operationId, key); if (_logger?.IsEnabled(LogLevel.Debug) ?? false) - _logger.LogDebug("FUSION (O={CacheOperationId} K={CacheKey}): saving entry in memory {Options} {Entry}", operationId, key, memoryOptions.ToLogString(), entry.ToLogString()); + _logger.Log(LogLevel.Debug, "FUSION [N={CacheName}] (O={CacheOperationId} K={CacheKey}): saving entry in memory {Options} {Entry}", _options.CacheName, operationId, key, memoryOptions.ToLogString(), entry.ToLogString()); _cache.Set(key, entry, memoryOptions); @@ -49,24 +56,24 @@ public void SetEntry(string operationId, string key, FusionCacheMemoryEn bool isValid = false; if (_logger?.IsEnabled(LogLevel.Trace) ?? false) - _logger.LogTrace("FUSION (O={CacheOperationId} K={CacheKey}): trying to get from memory", operationId, key); + _logger.Log(LogLevel.Trace, "FUSION [N={CacheName}] (O={CacheOperationId} K={CacheKey}): trying to get from memory", _options.CacheName, operationId, key); if (_cache.TryGetValue(key, out entry) == false) { if (_logger?.IsEnabled(LogLevel.Debug) ?? false) - _logger.LogDebug("FUSION (O={CacheOperationId} K={CacheKey}): memory entry not found", operationId, key); + _logger.Log(LogLevel.Debug, "FUSION [N={CacheName}] (O={CacheOperationId} K={CacheKey}): memory entry not found", _options.CacheName, operationId, key); } else { if (entry.IsLogicallyExpired()) { if (_logger?.IsEnabled(LogLevel.Debug) ?? false) - _logger.LogDebug("FUSION (O={CacheOperationId} K={CacheKey}): memory entry found (expired) {Entry}", operationId, key, entry.ToLogString()); + _logger.Log(LogLevel.Debug, "FUSION [N={CacheName}] (O={CacheOperationId} K={CacheKey}): memory entry found (expired) {Entry}", _options.CacheName, operationId, key, entry.ToLogString()); } else { if (_logger?.IsEnabled(LogLevel.Debug) ?? false) - _logger.LogDebug("FUSION (O={CacheOperationId} K={CacheKey}): memory entry found {Entry}", operationId, key, entry.ToLogString()); + _logger.Log(LogLevel.Debug, "FUSION [N={CacheName}] (O={CacheOperationId} K={CacheKey}): memory entry found {Entry}", _options.CacheName, operationId, key, entry.ToLogString()); isValid = true; } @@ -88,7 +95,7 @@ public void SetEntry(string operationId, string key, FusionCacheMemoryEn public void RemoveEntry(string operationId, string key, FusionCacheEntryOptions options) { if (_logger?.IsEnabled(LogLevel.Debug) ?? false) - _logger.LogDebug("FUSION (O={CacheOperationId} K={CacheKey}): removing data (from memory)", operationId, key); + _logger.Log(LogLevel.Debug, "FUSION [N={CacheName}] (O={CacheOperationId} K={CacheKey}): removing data (from memory)", _options.CacheName, operationId, key); _cache.Remove(key); @@ -96,10 +103,10 @@ public void RemoveEntry(string operationId, string key, FusionCacheEntryOptions _events.OnRemove(operationId, key); } - public void EvictEntry(string operationId, string key, bool allowFailSafe) + public void ExpireEntry(string operationId, string key, bool allowFailSafe) { if (_logger?.IsEnabled(LogLevel.Debug) ?? false) - _logger.LogDebug("FUSION (O={CacheOperationId} K={CacheKey}): evicting data (from memory)", operationId, key); + _logger.Log(LogLevel.Debug, "FUSION [N={CacheName}] (O={CacheOperationId} K={CacheKey}): expiring data (from memory)", _options.CacheName, operationId, key); if (_cache.TryGetValue(key, out var entry) == false) return; @@ -111,6 +118,9 @@ public void EvictEntry(string operationId, string key, bool allowFailSafe) { // MAKE THE ENTRY LOGICALLY EXPIRE entry.Metadata.LogicalExpiration = DateTimeOffset.UtcNow.AddMilliseconds(-10); + + // EVENT + _events.OnExpire(operationId, key); } else { diff --git a/src/ZiggyCreatures.FusionCache/Internals/Provider/FusionCacheProvider.cs b/src/ZiggyCreatures.FusionCache/Internals/Provider/FusionCacheProvider.cs index 6bb2541c..5b2266fb 100644 --- a/src/ZiggyCreatures.FusionCache/Internals/Provider/FusionCacheProvider.cs +++ b/src/ZiggyCreatures.FusionCache/Internals/Provider/FusionCacheProvider.cs @@ -8,44 +8,42 @@ internal class FusionCacheProvider : IFusionCacheProvider { private readonly IFusionCache? _defaultCache; - private readonly IFusionCache[] _namedCaches; + private readonly LazyNamedCache[] _lazyNamedCaches; - public FusionCacheProvider(IEnumerable defaultCaches, IEnumerable namedCaches) + public FusionCacheProvider(IEnumerable defaultCaches, IEnumerable lazyNamedCaches) { _defaultCache = defaultCaches.LastOrDefault(); - _namedCaches = namedCaches.Select(x => x.Cache).ToArray(); + _lazyNamedCaches = lazyNamedCaches.ToArray(); } - public IFusionCache GetCache(string cacheName) + public IFusionCache? GetCacheOrNull(string cacheName) { if (cacheName == FusionCacheOptions.DefaultCacheName) - return _defaultCache ?? throw new InvalidOperationException("No default cache has been registered"); + return _defaultCache; - var matchingCaches = _namedCaches.Where(x => x.CacheName == cacheName).ToArray(); + var matchingLazyNamedCaches = _lazyNamedCaches.Where(x => x.CacheName == cacheName).ToArray(); - if (matchingCaches.Length == 1) - return matchingCaches[0]; + if (matchingLazyNamedCaches.Length == 1) + return matchingLazyNamedCaches[0].Cache; - if (matchingCaches.Length > 1) + if (matchingLazyNamedCaches.Length > 1) throw new InvalidOperationException($"Multiple FusionCache registrations have been found with the provided name ({cacheName})"); - throw new ArgumentException($"No FusionCache registration has been found with the provided name ({cacheName})", nameof(cacheName)); + return null; } - public IFusionCache? GetCacheOrNull(string cacheName) + public IFusionCache GetCache(string cacheName) { - if (cacheName == FusionCacheOptions.DefaultCacheName) - return _defaultCache; - - var matchingCaches = _namedCaches.Where(x => x.CacheName == cacheName).ToArray(); + var maybeCache = GetCacheOrNull(cacheName); - if (matchingCaches.Length == 1) - return matchingCaches[0]; + if (maybeCache is not null) + return maybeCache; - if (matchingCaches.Length > 1) - throw new InvalidOperationException($"Multiple FusionCache registrations have been found with the provided name ({cacheName})"); - - return null; + throw new InvalidOperationException( + cacheName == FusionCacheOptions.DefaultCacheName + ? "No default cache has been registered" + : $"No cache has been registered with name ({cacheName})" + ); } } } diff --git a/src/ZiggyCreatures.FusionCache/Internals/Provider/NamedCacheWrapper.cs b/src/ZiggyCreatures.FusionCache/Internals/Provider/LazyNamedCache.cs similarity index 87% rename from src/ZiggyCreatures.FusionCache/Internals/Provider/NamedCacheWrapper.cs rename to src/ZiggyCreatures.FusionCache/Internals/Provider/LazyNamedCache.cs index 6f8ef504..efdd9bb2 100644 --- a/src/ZiggyCreatures.FusionCache/Internals/Provider/NamedCacheWrapper.cs +++ b/src/ZiggyCreatures.FusionCache/Internals/Provider/LazyNamedCache.cs @@ -4,14 +4,14 @@ namespace ZiggyCreatures.Caching.Fusion.Internals.Provider { [DebuggerDisplay($"{{{nameof(GetDebuggerDisplay)}(),nq}}")] - internal class NamedCacheWrapper : IDisposable + internal class LazyNamedCache : IDisposable { private string GetDebuggerDisplay() { return $"CACHE: {CacheName} - INSTANTIATED: {_cache is not null}"; } - public NamedCacheWrapper(string name, Func cacheFactory) + public LazyNamedCache(string name, Func cacheFactory) { if (name is null) throw new ArgumentNullException(nameof(name)); @@ -23,7 +23,7 @@ public NamedCacheWrapper(string name, Func cacheFactory) _cacheFactory = cacheFactory; } - public NamedCacheWrapper(string name, IFusionCache cache) + public LazyNamedCache(string name, IFusionCache cache) { if (name is null) throw new ArgumentNullException(nameof(name)); diff --git a/src/ZiggyCreatures.FusionCache/NullFusionCache.cs b/src/ZiggyCreatures.FusionCache/NullFusionCache.cs new file mode 100644 index 00000000..8a84dc04 --- /dev/null +++ b/src/ZiggyCreatures.FusionCache/NullFusionCache.cs @@ -0,0 +1,208 @@ +using System; +using System.Diagnostics; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.Extensions.Caching.Distributed; +using Microsoft.Extensions.Options; +using ZiggyCreatures.Caching.Fusion.Backplane; +using ZiggyCreatures.Caching.Fusion.Events; +using ZiggyCreatures.Caching.Fusion.Plugins; +using ZiggyCreatures.Caching.Fusion.Serialization; + +namespace ZiggyCreatures.Caching.Fusion +{ + /// + /// An implementation of that implements the null object pattern, meaning that it does nothing. Consider this a kind of a pass-through implementation. + /// + [DebuggerDisplay("NAME: {_options.CacheName} - ID: {InstanceId}")] + public class NullFusionCache + : IFusionCache + { + private readonly FusionCacheOptions _options; + private readonly FusionCacheEventsHub _events; + + /// + /// Creates a new instance. + /// + /// The set of cache-wide options to use with this instance of . + public NullFusionCache(IOptions optionsAccessor) + { + // GLOBALLY UNIQUE INSTANCE ID + InstanceId = Guid.NewGuid().ToString("N"); + + if (optionsAccessor is null) + throw new ArgumentNullException(nameof(optionsAccessor)); + + // OPTIONS + _options = optionsAccessor.Value ?? throw new ArgumentNullException(nameof(optionsAccessor.Value)); + + // EVENTS + _events = new FusionCacheEventsHub(this, _options, null); + } + + /// + public string CacheName + { + get { return _options.CacheName; } + } + + /// + public string InstanceId { get; } + + /// + public FusionCacheEntryOptions DefaultEntryOptions + { + get { return _options.DefaultEntryOptions; } + } + + /// + public bool HasDistributedCache + { + get { return false; } + } + + /// + public bool HasBackplane + { + get { return false; } + } + + /// + public FusionCacheEventsHub Events + { + get { return _events; } + } + + /// + public void AddPlugin(IFusionCachePlugin plugin) + { + // EMPTY + } + + /// + public FusionCacheEntryOptions CreateEntryOptions(Action? setupAction = null, TimeSpan? duration = null) + { + throw new NotImplementedException(); + } + + /// + public void Dispose() + { + // EMTPY + } + + /// + public void Expire(string key, FusionCacheEntryOptions? options = null, CancellationToken token = default) + { + // EMPTY + } + + /// + public ValueTask ExpireAsync(string key, FusionCacheEntryOptions? options = null, CancellationToken token = default) + { + return new ValueTask(); + } + + /// + public TValue? GetOrDefault(string key, TValue? defaultValue = default, FusionCacheEntryOptions? options = null, CancellationToken token = default) + { + return defaultValue; + } + + /// + public ValueTask GetOrDefaultAsync(string key, TValue? defaultValue = default, FusionCacheEntryOptions? options = null, CancellationToken token = default) + { + return new ValueTask(defaultValue); + } + + /// + public TValue? GetOrSet(string key, Func, CancellationToken, TValue?> factory, MaybeValue failSafeDefaultValue = default, FusionCacheEntryOptions? options = null, CancellationToken token = default) + { + return factory(new FusionCacheFactoryExecutionContext(options ?? DefaultEntryOptions, default, null, null), token); + } + + /// + public TValue? GetOrSet(string key, TValue? defaultValue, FusionCacheEntryOptions? options = null, CancellationToken token = default) + { + return defaultValue; + } + + /// + public async ValueTask GetOrSetAsync(string key, Func, CancellationToken, Task> factory, MaybeValue failSafeDefaultValue = default, FusionCacheEntryOptions? options = null, CancellationToken token = default) + { + return await factory(new FusionCacheFactoryExecutionContext(options ?? DefaultEntryOptions, default, null, null), token); + } + + /// + public ValueTask GetOrSetAsync(string key, TValue? defaultValue, FusionCacheEntryOptions? options = null, CancellationToken token = default) + { + return new ValueTask(defaultValue); + } + + /// + public void Remove(string key, FusionCacheEntryOptions? options = null, CancellationToken token = default) + { + // EMPTY + } + + /// + public ValueTask RemoveAsync(string key, FusionCacheEntryOptions? options = null, CancellationToken token = default) + { + return new ValueTask(); + } + + /// + public IFusionCache RemoveBackplane() + { + return this; + } + + /// + public IFusionCache RemoveDistributedCache() + { + return this; + } + + /// + public bool RemovePlugin(IFusionCachePlugin plugin) + { + return false; + } + + /// + public void Set(string key, TValue value, FusionCacheEntryOptions? options = null, CancellationToken token = default) + { + // EMPTY + } + + /// + public ValueTask SetAsync(string key, TValue value, FusionCacheEntryOptions? options = null, CancellationToken token = default) + { + return new ValueTask(); + } + + /// + public IFusionCache SetupBackplane(IFusionCacheBackplane backplane) + { + return this; + } + + /// + public IFusionCache SetupDistributedCache(IDistributedCache distributedCache, IFusionCacheSerializer serializer) + { + return this; + } + + /// + public MaybeValue TryGet(string key, FusionCacheEntryOptions? options = null, CancellationToken token = default) + { + return default; + } + + /// + public ValueTask> TryGetAsync(string key, FusionCacheEntryOptions? options = null, CancellationToken token = default) + { + return new ValueTask>(); + } + } +} diff --git a/src/ZiggyCreatures.FusionCache/Reactors/FusionCacheReactorProbabilistic.cs b/src/ZiggyCreatures.FusionCache/Reactors/FusionCacheReactorProbabilistic.cs index 277dca56..5b7dc7bd 100644 --- a/src/ZiggyCreatures.FusionCache/Reactors/FusionCacheReactorProbabilistic.cs +++ b/src/ZiggyCreatures.FusionCache/Reactors/FusionCacheReactorProbabilistic.cs @@ -38,7 +38,7 @@ private uint GetLockIndex(string key) } // ACQUIRE LOCK ASYNC - public async ValueTask AcquireLockAsync(string key, string operationId, TimeSpan timeout, ILogger? logger, CancellationToken token) + public async ValueTask AcquireLockAsync(string cacheName, string key, string operationId, TimeSpan timeout, ILogger? logger, CancellationToken token) { token.ThrowIfCancellationRequested(); @@ -46,89 +46,89 @@ private uint GetLockIndex(string key) var semaphore = _lockPool[idx]; if (logger?.IsEnabled(LogLevel.Trace) ?? false) - logger.LogTrace("FUSION (O={CacheOperationId} K={CacheKey}): trying to fast-acquire the LOCK", operationId, key); + logger.Log(LogLevel.Trace, "FUSION [N={CacheName}] (O={CacheOperationId} K={CacheKey}): trying to fast-acquire the LOCK", cacheName, operationId, key); var acquired = semaphore.Wait(0); if (acquired) { _lockPoolKeys[idx] = key; if (logger?.IsEnabled(LogLevel.Trace) ?? false) - logger.LogTrace("FUSION (O={CacheOperationId} K={CacheKey}): LOCK fast-acquired", operationId, key); + logger.Log(LogLevel.Trace, "FUSION [N={CacheName}] (O={CacheOperationId} K={CacheKey}): LOCK fast-acquired", cacheName, operationId, key); return semaphore; } if (logger?.IsEnabled(LogLevel.Trace) ?? false) - logger.LogTrace("FUSION (O={CacheOperationId} K={CacheKey}): LOCK already taken", operationId, key); + logger.Log(LogLevel.Trace, "FUSION [N={CacheName}] (O={CacheOperationId} K={CacheKey}): LOCK already taken", cacheName, operationId, key); var key2 = _lockPoolKeys[idx]; if (key2 != key) { if (logger?.IsEnabled(LogLevel.Trace) ?? false) - logger.LogTrace("FUSION (O={CacheOperationId} K={CacheKey}): LOCK " + (key2 is null ? "maybe " : string.Empty) + "acquired for a different key (current key: " + key + ", other key: " + key2 + ")", operationId, key); + logger.Log(LogLevel.Trace, "FUSION [N={CacheName}] (O={CacheOperationId} K={CacheKey}): LOCK " + (key2 is null ? "maybe " : string.Empty) + "acquired for a different key (current key: " + key + ", other key: " + key2 + ")", cacheName, operationId, key); Interlocked.Increment(ref _lockPoolCollisions); } if (logger?.IsEnabled(LogLevel.Trace) ?? false) - logger.LogTrace("FUSION (O={CacheOperationId} K={CacheKey}): waiting to acquire the LOCK", operationId, key); + logger.Log(LogLevel.Trace, "FUSION [N={CacheName}] (O={CacheOperationId} K={CacheKey}): waiting to acquire the LOCK", cacheName, operationId, key); acquired = await semaphore.WaitAsync(timeout, token).ConfigureAwait(false); _lockPoolKeys[idx] = key; if (logger?.IsEnabled(LogLevel.Trace) ?? false) - logger.LogTrace("FUSION (O={CacheOperationId} K={CacheKey}): LOCK acquired", operationId, key); + logger.Log(LogLevel.Trace, "FUSION [N={CacheName}] (O={CacheOperationId} K={CacheKey}): LOCK acquired", cacheName, operationId, key); return acquired ? semaphore : null; } // ACQUIRE LOCK - public object? AcquireLock(string key, string operationId, TimeSpan timeout, ILogger? logger) + public object? AcquireLock(string cacheName, string key, string operationId, TimeSpan timeout, ILogger? logger) { var idx = GetLockIndex(key); var semaphore = _lockPool[idx]; if (logger?.IsEnabled(LogLevel.Trace) ?? false) - logger.LogTrace("FUSION (O={CacheOperationId} K={CacheKey}): trying to fast-acquire the LOCK", operationId, key); + logger.Log(LogLevel.Trace, "FUSION [N={CacheName}] (O={CacheOperationId} K={CacheKey}): trying to fast-acquire the LOCK", cacheName, operationId, key); var acquired = semaphore.Wait(0); if (acquired) { _lockPoolKeys[idx] = key; if (logger?.IsEnabled(LogLevel.Trace) ?? false) - logger.LogTrace("FUSION (O={CacheOperationId} K={CacheKey}): LOCK fast-acquired", operationId, key); + logger.Log(LogLevel.Trace, "FUSION [N={CacheName}] (O={CacheOperationId} K={CacheKey}): LOCK fast-acquired", cacheName, operationId, key); return semaphore; } if (logger?.IsEnabled(LogLevel.Trace) ?? false) - logger.LogTrace("FUSION (O={CacheOperationId} K={CacheKey}): LOCK already taken", operationId, key); + logger.Log(LogLevel.Trace, "FUSION [N={CacheName}] (O={CacheOperationId} K={CacheKey}): LOCK already taken", cacheName, operationId, key); var key2 = _lockPoolKeys[idx]; if (key2 != key) { if (logger?.IsEnabled(LogLevel.Trace) ?? false) - logger.LogTrace("FUSION (O={CacheOperationId} K={CacheKey}): LOCK " + (key2 is null ? "maybe " : string.Empty) + "acquired for a different key (current key: " + key + ", other key: " + key2 + ")", operationId, key); + logger.Log(LogLevel.Trace, "FUSION [N={CacheName}] (O={CacheOperationId} K={CacheKey}): LOCK " + (key2 is null ? "maybe " : string.Empty) + "acquired for a different key (current key: " + key + ", other key: " + key2 + ")", cacheName, operationId, key); Interlocked.Increment(ref _lockPoolCollisions); } if (logger?.IsEnabled(LogLevel.Trace) ?? false) - logger.LogTrace("FUSION (O={CacheOperationId} K={CacheKey}): waiting to acquire the LOCK", operationId, key); + logger.Log(LogLevel.Trace, "FUSION [N={CacheName}] (O={CacheOperationId} K={CacheKey}): waiting to acquire the LOCK", cacheName, operationId, key); acquired = semaphore.Wait(timeout); _lockPoolKeys[idx] = key; if (logger?.IsEnabled(LogLevel.Trace) ?? false) - logger.LogTrace("FUSION (O={CacheOperationId} K={CacheKey}): LOCK acquired", operationId, key); + logger.Log(LogLevel.Trace, "FUSION [N={CacheName}] (O={CacheOperationId} K={CacheKey}): LOCK acquired", cacheName, operationId, key); return acquired ? semaphore : null; } // RELEASE LOCK ASYNC - public void ReleaseLock(string key, string operationId, object? lockObj, ILogger? logger) + public void ReleaseLock(string cacheName, string key, string operationId, object? lockObj, ILogger? logger) { if (lockObj is null) return; @@ -143,7 +143,7 @@ public void ReleaseLock(string key, string operationId, object? lockObj, ILogger catch (Exception exc) { if (logger?.IsEnabled(LogLevel.Warning) ?? false) - logger.LogWarning(exc, "FUSION (O={CacheOperationId} K={CacheKey}): an error occurred while trying to release a SemaphoreSlim in the reactor", operationId, key); + logger.Log(LogLevel.Warning, exc, "FUSION [N={CacheName}] (O={CacheOperationId} K={CacheKey}): an error occurred while trying to release a SemaphoreSlim in the reactor", cacheName, operationId, key); } } diff --git a/src/ZiggyCreatures.FusionCache/Reactors/FusionCacheReactorStandard.cs b/src/ZiggyCreatures.FusionCache/Reactors/FusionCacheReactorStandard.cs index 561e0c95..c5579a16 100644 --- a/src/ZiggyCreatures.FusionCache/Reactors/FusionCacheReactorStandard.cs +++ b/src/ZiggyCreatures.FusionCache/Reactors/FusionCacheReactorStandard.cs @@ -40,7 +40,7 @@ private uint GetLockIndex(string key) return unchecked((uint)key.GetHashCode()) % (uint)_lockPoolSize; } - private SemaphoreSlim GetSemaphore(string key, string operationId, ILogger? logger) + private SemaphoreSlim GetSemaphore(string cacheName, string key, string operationId, ILogger? logger) { object _semaphore; @@ -66,7 +66,7 @@ private SemaphoreSlim GetSemaphore(string key, string operationId, ILogger? logg catch (Exception exc) { if (logger?.IsEnabled(LogLevel.Warning) ?? false) - logger.LogWarning(exc, "FUSION (K={CacheKey}): an error occurred while trying to dispose a SemaphoreSlim in the reactor", key); + logger.Log(LogLevel.Warning, exc, "FUSION [N={CacheName}] (K={CacheKey}): an error occurred while trying to dispose a SemaphoreSlim in the reactor", cacheName, key); } }); @@ -75,14 +75,14 @@ private SemaphoreSlim GetSemaphore(string key, string operationId, ILogger? logg } // ACQUIRE LOCK ASYNC - public async ValueTask AcquireLockAsync(string key, string operationId, TimeSpan timeout, ILogger? logger, CancellationToken token) + public async ValueTask AcquireLockAsync(string cacheName, string key, string operationId, TimeSpan timeout, ILogger? logger, CancellationToken token) { token.ThrowIfCancellationRequested(); - var semaphore = GetSemaphore(key, operationId, logger); + var semaphore = GetSemaphore(cacheName, key, operationId, logger); if (logger?.IsEnabled(LogLevel.Trace) ?? false) - logger.LogTrace("FUSION (O={CacheOperationId} K={CacheKey}): waiting to acquire the LOCK", operationId, key); + logger.Log(LogLevel.Trace, "FUSION [N={CacheName}] (O={CacheOperationId} K={CacheKey}): waiting to acquire the LOCK", cacheName, operationId, key); var acquired = await semaphore.WaitAsync(timeout, token).ConfigureAwait(false); @@ -90,25 +90,25 @@ private SemaphoreSlim GetSemaphore(string key, string operationId, ILogger? logg { // LOCK ACQUIRED if (logger?.IsEnabled(LogLevel.Trace) ?? false) - logger.LogTrace("FUSION (O={CacheOperationId} K={CacheKey}): LOCK acquired", operationId, key); + logger.Log(LogLevel.Trace, "FUSION [N={CacheName}] (O={CacheOperationId} K={CacheKey}): LOCK acquired", cacheName, operationId, key); } else { // LOCK TIMEOUT if (logger?.IsEnabled(LogLevel.Trace) ?? false) - logger.LogTrace("FUSION (O={CacheOperationId} K={CacheKey}): LOCK timeout", operationId, key); + logger.Log(LogLevel.Trace, "FUSION [N={CacheName}] (O={CacheOperationId} K={CacheKey}): LOCK timeout", cacheName, operationId, key); } return acquired ? semaphore : null; } // ACQUIRE LOCK - public object? AcquireLock(string key, string operationId, TimeSpan timeout, ILogger? logger) + public object? AcquireLock(string cacheName, string key, string operationId, TimeSpan timeout, ILogger? logger) { - var semaphore = GetSemaphore(key, operationId, logger); + var semaphore = GetSemaphore(cacheName, key, operationId, logger); if (logger?.IsEnabled(LogLevel.Trace) ?? false) - logger.LogTrace("FUSION (O={CacheOperationId} K={CacheKey}): waiting to acquire the LOCK", operationId, key); + logger.Log(LogLevel.Trace, "FUSION [N={CacheName}] (O={CacheOperationId} K={CacheKey}): waiting to acquire the LOCK", cacheName, operationId, key); var acquired = semaphore.Wait(timeout); @@ -116,20 +116,20 @@ private SemaphoreSlim GetSemaphore(string key, string operationId, ILogger? logg { // LOCK ACQUIRED if (logger?.IsEnabled(LogLevel.Trace) ?? false) - logger.LogTrace("FUSION (O={CacheOperationId} K={CacheKey}): LOCK acquired", operationId, key); + logger.Log(LogLevel.Trace, "FUSION [N={CacheName}] (O={CacheOperationId} K={CacheKey}): LOCK acquired", cacheName, operationId, key); } else { // LOCK TIMEOUT if (logger?.IsEnabled(LogLevel.Trace) ?? false) - logger.LogTrace("FUSION (O={CacheOperationId} K={CacheKey}): LOCK timeout", operationId, key); + logger.Log(LogLevel.Trace, "FUSION [N={CacheName}] (O={CacheOperationId} K={CacheKey}): LOCK timeout", cacheName, operationId, key); } return acquired ? semaphore : null; } // RELEASE LOCK ASYNC - public void ReleaseLock(string key, string operationId, object? lockObj, ILogger? logger) + public void ReleaseLock(string cacheName, string key, string operationId, object? lockObj, ILogger? logger) { if (lockObj is null) return; @@ -141,7 +141,7 @@ public void ReleaseLock(string key, string operationId, object? lockObj, ILogger catch (Exception exc) { if (logger?.IsEnabled(LogLevel.Warning) ?? false) - logger.LogWarning(exc, "FUSION (O={CacheOperationId} K={CacheKey}): an error occurred while trying to release a SemaphoreSlim in the reactor", operationId, key); + logger.Log(LogLevel.Warning, exc, "FUSION [N={CacheName}] (O={CacheOperationId} K={CacheKey}): an error occurred while trying to release a SemaphoreSlim in the reactor", cacheName, operationId, key); } } @@ -154,7 +154,6 @@ private void Dispose(bool disposing) { if (disposing) { - // TODO: MAYBE FIND A WAY TO CLEAR ALL THE ENTRIES IN THE CACHE (INCLUDING THE ONES WITH A NeverRemove PRIORITY) AND DISPOSE ALL RELATED SEMAPHORES _lockCache.Compact(1.0); _lockCache.Dispose(); } diff --git a/src/ZiggyCreatures.FusionCache/Reactors/FusionCacheReactorUnbounded.cs b/src/ZiggyCreatures.FusionCache/Reactors/FusionCacheReactorUnbounded.cs index 7ac543bd..2a500dae 100644 --- a/src/ZiggyCreatures.FusionCache/Reactors/FusionCacheReactorUnbounded.cs +++ b/src/ZiggyCreatures.FusionCache/Reactors/FusionCacheReactorUnbounded.cs @@ -42,41 +42,41 @@ private SemaphoreSlim GetSemaphore(string key, string operationId, ILogger? logg } // ACQUIRE LOCK ASYNC - public async ValueTask AcquireLockAsync(string key, string operationId, TimeSpan timeout, ILogger? logger, CancellationToken token) + public async ValueTask AcquireLockAsync(string cacheName, string key, string operationId, TimeSpan timeout, ILogger? logger, CancellationToken token) { token.ThrowIfCancellationRequested(); var semaphore = GetSemaphore(key, operationId, logger); if (logger?.IsEnabled(LogLevel.Trace) ?? false) - logger.LogTrace("FUSION (O={CacheOperationId} K={CacheKey}): waiting to acquire the LOCK", operationId, key); + logger.Log(LogLevel.Trace, "FUSION [N={CacheName}] (O={CacheOperationId} K={CacheKey}): waiting to acquire the LOCK", cacheName, operationId, key); var acquired = await semaphore.WaitAsync(timeout, token).ConfigureAwait(false); if (logger?.IsEnabled(LogLevel.Trace) ?? false) - logger.LogTrace("FUSION (O={CacheOperationId} K={CacheKey}): LOCK acquired", operationId, key); + logger.Log(LogLevel.Trace, "FUSION [N={CacheName}] (O={CacheOperationId} K={CacheKey}): LOCK acquired", cacheName, operationId, key); return acquired ? semaphore : null; } // ACQUIRE LOCK - public object? AcquireLock(string key, string operationId, TimeSpan timeout, ILogger? logger) + public object? AcquireLock(string cacheName, string key, string operationId, TimeSpan timeout, ILogger? logger) { var semaphore = GetSemaphore(key, operationId, logger); if (logger?.IsEnabled(LogLevel.Trace) ?? false) - logger.LogTrace("FUSION (O={CacheOperationId} K={CacheKey}): waiting to acquire the LOCK", operationId, key); + logger.Log(LogLevel.Trace, "FUSION [N={CacheName}] (O={CacheOperationId} K={CacheKey}): waiting to acquire the LOCK", cacheName, operationId, key); var acquired = semaphore.Wait(timeout); if (logger?.IsEnabled(LogLevel.Trace) ?? false) - logger.LogTrace("FUSION (O={CacheOperationId} K={CacheKey}): LOCK acquired", operationId, key); + logger.Log(LogLevel.Trace, "FUSION [N={CacheName}] (O={CacheOperationId} K={CacheKey}): LOCK acquired", cacheName, operationId, key); return acquired ? semaphore : null; } // RELEASE LOCK ASYNC - public void ReleaseLock(string key, string operationId, object? lockObj, ILogger? logger) + public void ReleaseLock(string cacheName, string key, string operationId, object? lockObj, ILogger? logger) { if (lockObj is null) return; @@ -88,7 +88,7 @@ public void ReleaseLock(string key, string operationId, object? lockObj, ILogger catch (Exception exc) { if (logger?.IsEnabled(LogLevel.Warning) ?? false) - logger.LogWarning(exc, "FUSION (O={CacheOperationId} K={CacheKey}): an error occurred while trying to release a SemaphoreSlim in the reactor", operationId, key); + logger.Log(LogLevel.Warning, exc, "FUSION [N={CacheName}] (O={CacheOperationId} K={CacheKey}): an error occurred while trying to release a SemaphoreSlim in the reactor", cacheName, operationId, key); } } diff --git a/src/ZiggyCreatures.FusionCache/Reactors/FusionCacheReactorUnboundedConcurrent.cs b/src/ZiggyCreatures.FusionCache/Reactors/FusionCacheReactorUnboundedConcurrent.cs index e8990286..76f50891 100644 --- a/src/ZiggyCreatures.FusionCache/Reactors/FusionCacheReactorUnboundedConcurrent.cs +++ b/src/ZiggyCreatures.FusionCache/Reactors/FusionCacheReactorUnboundedConcurrent.cs @@ -27,41 +27,41 @@ private SemaphoreSlim GetSemaphore(string key, string operationId, ILogger? logg } // ACQUIRE LOCK ASYNC - public async ValueTask AcquireLockAsync(string key, string operationId, TimeSpan timeout, ILogger? logger, CancellationToken token) + public async ValueTask AcquireLockAsync(string cacheName, string key, string operationId, TimeSpan timeout, ILogger? logger, CancellationToken token) { token.ThrowIfCancellationRequested(); var semaphore = GetSemaphore(key, operationId, logger); if (logger?.IsEnabled(LogLevel.Trace) ?? false) - logger.LogTrace("FUSION (O={CacheOperationId} K={CacheKey}): waiting to acquire the LOCK", operationId, key); + logger.Log(LogLevel.Trace, "FUSION [N={CacheName}] (O={CacheOperationId} K={CacheKey}): waiting to acquire the LOCK", cacheName, operationId, key); var acquired = await semaphore.WaitAsync(timeout, token).ConfigureAwait(false); if (logger?.IsEnabled(LogLevel.Trace) ?? false) - logger.LogTrace("FUSION (O={CacheOperationId} K={CacheKey}): LOCK acquired", operationId, key); + logger.Log(LogLevel.Trace, "FUSION [N={CacheName}] (O={CacheOperationId} K={CacheKey}): LOCK acquired", cacheName, operationId, key); return acquired ? semaphore : null; } // ACQUIRE LOCK - public object? AcquireLock(string key, string operationId, TimeSpan timeout, ILogger? logger) + public object? AcquireLock(string cacheName, string key, string operationId, TimeSpan timeout, ILogger? logger) { var semaphore = GetSemaphore(key, operationId, logger); if (logger?.IsEnabled(LogLevel.Trace) ?? false) - logger.LogTrace("FUSION (O={CacheOperationId} K={CacheKey}): waiting to acquire the LOCK", operationId, key); + logger.Log(LogLevel.Trace, "FUSION [N={CacheName}] (O={CacheOperationId} K={CacheKey}): waiting to acquire the LOCK", cacheName, operationId, key); var acquired = semaphore.Wait(timeout); if (logger?.IsEnabled(LogLevel.Trace) ?? false) - logger.LogTrace("FUSION (O={CacheOperationId} K={CacheKey}): LOCK acquired", operationId, key); + logger.Log(LogLevel.Trace, "FUSION [N={CacheName}] (O={CacheOperationId} K={CacheKey}): LOCK acquired", cacheName, operationId, key); return acquired ? semaphore : null; } // RELEASE LOCK ASYNC - public void ReleaseLock(string key, string operationId, object? lockObj, ILogger? logger) + public void ReleaseLock(string cacheName, string key, string operationId, object? lockObj, ILogger? logger) { if (lockObj is null) return; @@ -73,7 +73,7 @@ public void ReleaseLock(string key, string operationId, object? lockObj, ILogger catch (Exception exc) { if (logger?.IsEnabled(LogLevel.Warning) ?? false) - logger.LogWarning(exc, "FUSION (O={CacheOperationId} K={CacheKey}): an error occurred while trying to release a SemaphoreSlim in the reactor", operationId, key); + logger.Log(LogLevel.Warning, exc, "FUSION [N={CacheName}] (O={CacheOperationId} K={CacheKey}): an error occurred while trying to release a SemaphoreSlim in the reactor", cacheName, operationId, key); } } diff --git a/src/ZiggyCreatures.FusionCache/Reactors/FusionCacheReactorUnboundedConcurrentLazy.cs b/src/ZiggyCreatures.FusionCache/Reactors/FusionCacheReactorUnboundedConcurrentLazy.cs index c9be42c7..f2be12a0 100644 --- a/src/ZiggyCreatures.FusionCache/Reactors/FusionCacheReactorUnboundedConcurrentLazy.cs +++ b/src/ZiggyCreatures.FusionCache/Reactors/FusionCacheReactorUnboundedConcurrentLazy.cs @@ -32,41 +32,41 @@ private SemaphoreSlim GetSemaphore(string key, string operationId, ILogger? logg } // ACQUIRE LOCK ASYNC - public async ValueTask AcquireLockAsync(string key, string operationId, TimeSpan timeout, ILogger? logger, CancellationToken token) + public async ValueTask AcquireLockAsync(string cacheName, string key, string operationId, TimeSpan timeout, ILogger? logger, CancellationToken token) { token.ThrowIfCancellationRequested(); var semaphore = GetSemaphore(key, operationId, logger); if (logger?.IsEnabled(LogLevel.Trace) ?? false) - logger.LogTrace("FUSION (O={CacheOperationId} K={CacheKey}): waiting to acquire the LOCK", operationId, key); + logger.Log(LogLevel.Trace, "FUSION [N={CacheName}] (O={CacheOperationId} K={CacheKey}): waiting to acquire the LOCK", cacheName, operationId, key); var acquired = await semaphore.WaitAsync(timeout, token).ConfigureAwait(false); if (logger?.IsEnabled(LogLevel.Trace) ?? false) - logger.LogTrace("FUSION (O={CacheOperationId} K={CacheKey}): LOCK acquired", operationId, key); + logger.Log(LogLevel.Trace, "FUSION [N={CacheName}] (O={CacheOperationId} K={CacheKey}): LOCK acquired", cacheName, operationId, key); return acquired ? semaphore : null; } // ACQUIRE LOCK - public object? AcquireLock(string key, string operationId, TimeSpan timeout, ILogger? logger) + public object? AcquireLock(string cacheName, string key, string operationId, TimeSpan timeout, ILogger? logger) { var semaphore = GetSemaphore(key, operationId, logger); if (logger?.IsEnabled(LogLevel.Trace) ?? false) - logger.LogTrace("FUSION (O={CacheOperationId} K={CacheKey}): waiting to acquire the LOCK", operationId, key); + logger.Log(LogLevel.Trace, "FUSION [N={CacheName}] (O={CacheOperationId} K={CacheKey}): waiting to acquire the LOCK", cacheName, operationId, key); var acquired = semaphore.Wait(timeout); if (logger?.IsEnabled(LogLevel.Trace) ?? false) - logger.LogTrace("FUSION (O={CacheOperationId} K={CacheKey}): LOCK acquired", operationId, key); + logger.Log(LogLevel.Trace, "FUSION [N={CacheName}] (O={CacheOperationId} K={CacheKey}): LOCK acquired", cacheName, operationId, key); return acquired ? semaphore : null; } // RELEASE LOCK ASYNC - public void ReleaseLock(string key, string operationId, object? lockObj, ILogger? logger) + public void ReleaseLock(string cacheName, string key, string operationId, object? lockObj, ILogger? logger) { if (lockObj is null) return; @@ -78,7 +78,7 @@ public void ReleaseLock(string key, string operationId, object? lockObj, ILogger catch (Exception exc) { if (logger?.IsEnabled(LogLevel.Warning) ?? false) - logger.LogWarning(exc, "FUSION (O={CacheOperationId} K={CacheKey}): an error occurred while trying to release a SemaphoreSlim in the reactor", operationId, key); + logger.Log(LogLevel.Warning, exc, "FUSION [N={CacheName}] (O={CacheOperationId} K={CacheKey}): an error occurred while trying to release a SemaphoreSlim in the reactor", cacheName, operationId, key); } } diff --git a/src/ZiggyCreatures.FusionCache/Reactors/FusionCacheReactorUnboundedWithPool.cs b/src/ZiggyCreatures.FusionCache/Reactors/FusionCacheReactorUnboundedWithPool.cs index 46490e24..cfb0680a 100644 --- a/src/ZiggyCreatures.FusionCache/Reactors/FusionCacheReactorUnboundedWithPool.cs +++ b/src/ZiggyCreatures.FusionCache/Reactors/FusionCacheReactorUnboundedWithPool.cs @@ -60,41 +60,41 @@ private SemaphoreSlim GetSemaphore(string key, string operationId, ILogger? logg } // ACQUIRE LOCK ASYNC - public async ValueTask AcquireLockAsync(string key, string operationId, TimeSpan timeout, ILogger? logger, CancellationToken token) + public async ValueTask AcquireLockAsync(string cacheName, string key, string operationId, TimeSpan timeout, ILogger? logger, CancellationToken token) { token.ThrowIfCancellationRequested(); var semaphore = GetSemaphore(key, operationId, logger); if (logger?.IsEnabled(LogLevel.Trace) ?? false) - logger.LogTrace("FUSION (O={CacheOperationId} K={CacheKey}): waiting to acquire the LOCK", operationId, key); + logger.Log(LogLevel.Trace, "FUSION [N={CacheName}] (O={CacheOperationId} K={CacheKey}): waiting to acquire the LOCK", cacheName, operationId, key); var acquired = await semaphore.WaitAsync(timeout, token).ConfigureAwait(false); if (logger?.IsEnabled(LogLevel.Trace) ?? false) - logger.LogTrace("FUSION (O={CacheOperationId} K={CacheKey}): LOCK acquired", operationId, key); + logger.Log(LogLevel.Trace, "FUSION [N={CacheName}] (O={CacheOperationId} K={CacheKey}): LOCK acquired", cacheName, operationId, key); return acquired ? semaphore : null; } // ACQUIRE LOCK - public object? AcquireLock(string key, string operationId, TimeSpan timeout, ILogger? logger) + public object? AcquireLock(string cacheName, string key, string operationId, TimeSpan timeout, ILogger? logger) { var semaphore = GetSemaphore(key, operationId, logger); if (logger?.IsEnabled(LogLevel.Trace) ?? false) - logger.LogTrace("FUSION (O={CacheOperationId} K={CacheKey}): waiting to acquire the LOCK", operationId, key); + logger.Log(LogLevel.Trace, "FUSION [N={CacheName}] (O={CacheOperationId} K={CacheKey}): waiting to acquire the LOCK", cacheName, operationId, key); var acquired = semaphore.Wait(timeout); if (logger?.IsEnabled(LogLevel.Trace) ?? false) - logger.LogTrace("FUSION (O={CacheOperationId} K={CacheKey}): LOCK acquired", operationId, key); + logger.Log(LogLevel.Trace, "FUSION [N={CacheName}] (O={CacheOperationId} K={CacheKey}): LOCK acquired", cacheName, operationId, key); return acquired ? semaphore : null; } // RELEASE LOCK ASYNC - public void ReleaseLock(string key, string operationId, object? lockObj, ILogger? logger) + public void ReleaseLock(string cacheName, string key, string operationId, object? lockObj, ILogger? logger) { if (lockObj is null) return; @@ -106,7 +106,7 @@ public void ReleaseLock(string key, string operationId, object? lockObj, ILogger catch (Exception exc) { if (logger?.IsEnabled(LogLevel.Warning) ?? false) - logger.LogWarning(exc, "FUSION (O={CacheOperationId} K={CacheKey}): an error occurred while trying to release a SemaphoreSlim in the reactor", operationId, key); + logger.Log(LogLevel.Warning, exc, "FUSION [N={CacheName}] (O={CacheOperationId} K={CacheKey}): an error occurred while trying to release a SemaphoreSlim in the reactor", cacheName, operationId, key); } } diff --git a/src/ZiggyCreatures.FusionCache/Reactors/IFusionCacheReactor.cs b/src/ZiggyCreatures.FusionCache/Reactors/IFusionCacheReactor.cs index 1d4be3a4..984cd5c8 100644 --- a/src/ZiggyCreatures.FusionCache/Reactors/IFusionCacheReactor.cs +++ b/src/ZiggyCreatures.FusionCache/Reactors/IFusionCacheReactor.cs @@ -14,32 +14,35 @@ public interface IFusionCacheReactor /// /// Acquire a generic lock, used to synchronize multiple factory operating on the same cache key, and return it. /// + /// The name of the FusionCache instance. /// The key for which to obtain a lock. /// The operation id which uniquely identifies a high-level cache operation. /// The optional timeout for the lock acquisition. /// The to use, if any. - /// An optional to cancel the operation. /// The acquired genericlock object, later released when the critical section is over. - ValueTask AcquireLockAsync(string key, string operationId, TimeSpan timeout, ILogger? logger, CancellationToken token); + /// An optional to cancel the operation. + ValueTask AcquireLockAsync(string cacheName, string key, string operationId, TimeSpan timeout, ILogger? logger, CancellationToken token); /// /// Acquire a generic lock, used to synchronize multiple factory operating on the same cache key, and return it. /// + /// The name of the FusionCache instance. /// The key for which to obtain a lock. /// The operation id which uniquely identifies a high-level cache operation. /// The optional timeout for the lock acquisition. - /// The to use, if any. /// The acquired genericlock object, later released when the critical section is over. - object? AcquireLock(string key, string operationId, TimeSpan timeout, ILogger? logger); + /// The to use, if any. + object? AcquireLock(string cacheName, string key, string operationId, TimeSpan timeout, ILogger? logger); /// /// Release the generic lock object. /// + /// The name of the FusionCache instance. /// The key for which to obtain a lock. /// The operation id which uniquely identifies a high-level cache operation. /// The generic lock object to release. /// The to use, if any. - void ReleaseLock(string key, string operationId, object? lockObj, ILogger? logger); + void ReleaseLock(string cacheName, string key, string operationId, object? lockObj, ILogger? logger); /// /// Exposes the eventual amount ofcollisions happened inside the reactor, for diagnostics purposes. diff --git a/src/ZiggyCreatures.FusionCache/ZiggyCreatures.FusionCache.csproj b/src/ZiggyCreatures.FusionCache/ZiggyCreatures.FusionCache.csproj index b19fd233..63c8c0ea 100644 --- a/src/ZiggyCreatures.FusionCache/ZiggyCreatures.FusionCache.csproj +++ b/src/ZiggyCreatures.FusionCache/ZiggyCreatures.FusionCache.csproj @@ -4,7 +4,7 @@ netstandard2.0 latest enable - 0.21.0 + 0.22.0 ZiggyCreatures.FusionCache logo-128x128.png @@ -15,10 +15,21 @@ ZiggyCreatures.FusionCache.xml README.md - - Added: Eager Refresh - - Added: Conditional Refresh - - Added: WithoutLogger() method for builder - - Added: EagerRefresh event + - Added: Expire()/ExpireAsync() method + - Added: SkipMemoryCache option + - Added: NullFusionCache (null object pattern) + - Added: better timestamping + - Added: special handling of zero duration + - Change: better log messages with CacheName and InstanceId + - Change: Adaptive Caching now supports more use cases + - Change: Eager Refresh now also checks distributed cache + - Tests: added automated tests for serializers' backward compatibility towards old versions' binary payloads + - Perf: better (lazy) handling of named caches instantiation + - Perf: better handling of backplane messages + - Perf: more robus edge-case handling for locks release + - Perf: better handling of distributed cache and backplane while disposing FusionCache + - Perf: various performance optimizations + - Obsolete: some old obsolete members have been marked with the error flag (next step: remove them) true diff --git a/src/ZiggyCreatures.FusionCache/ZiggyCreatures.FusionCache.xml b/src/ZiggyCreatures.FusionCache/ZiggyCreatures.FusionCache.xml index af46704e..2d8216a7 100644 --- a/src/ZiggyCreatures.FusionCache/ZiggyCreatures.FusionCache.xml +++ b/src/ZiggyCreatures.FusionCache/ZiggyCreatures.FusionCache.xml @@ -42,14 +42,21 @@ - Creates a message for a single cache entry set operation (via either a Set or a GetOrSet method call). + Creates a message for a single cache entry set operation (via either a Set() or a GetOrSet() method call). The cache key. The message. - Creates a message for a single cache entry remove (via a Remove method call). + Creates a message for a single cache entry remove (via a Remove() method call). + + The cache key. + The message. + + + + Creates a message for a single cache entry expire operation (via an Expire() method call). The cache key. The message. @@ -66,12 +73,17 @@ - A cache entry has been set (via either a Set or a GetOrSet method call). + A cache entry has been set (via either a Set() or a GetOrSet() method call). - A cache entry has been removed (via a Remove method call). + A cache entry has been removed (via a Remove() method call). + + + + + A cache entry has been manually expired (via an Expire() method call). @@ -412,6 +424,11 @@ The event for when a factory is being executed in advance, because a request came in during the eager refresh window (after the eager refresh threshold and before the expiration). + + + The event for a manual cache Expire() call. + + The events hub for events specific for the memory layer. @@ -430,6 +447,11 @@ The event for a cache eviction. + + + The event for a manual cache Expire() call. + + Check if the event has subscribers or not. @@ -437,7 +459,9 @@ if the event has subscribers, otherwhise . - + + The standard implementation of . + @@ -516,7 +540,7 @@ - + @@ -537,7 +561,7 @@ - + @@ -1168,6 +1192,15 @@ DOCS: + + + Skip the usage of the memory cache. +

+ NOTE: this option must be used very carefully and is generally not recommended, as it will not protect you from some problems like Cache Stampede. Also, it can lead to a lot of extra work for the 2nd layer (distributed cache) and a lot of extra network traffic. +

+ DOCS: +
+
@@ -1193,6 +1226,12 @@ The duration to set. The so that additional calls can be chained. + + + Set the to be zero: this will effectively remove the entry from the cache if fail-safe is disabled, or it will set the entry as logically expired if fail-safe is enabled (so it can be used later as a fallback). + + The so that additional calls can be chained. + Set the to be infinite, so it will never expire. @@ -1207,6 +1246,12 @@ The duration to set. The so that additional calls can be chained. + + + Set the to be zero: this will effectively remove the entry from the cache if fail-safe is disabled, or it will set the entry as logically expired if fail-safe is enabled (so it can be used later as a fallback). + + The so that additional calls can be chained. + Set the to be infinite, so it will never expire. @@ -1331,6 +1376,17 @@ Set the property. The so that additional calls can be chained. + + + Set the option. +

+ NOTE: this option must be used very carefully and is generally not recommended, as it will not protect you from some problems like Cache Stampede. Also, it can lead to a lot of extra work for the 2nd layer (distributed cache) and a lot of extra network traffic. +

+ DOCS: +
+ The value for the property. + The so that additional calls can be chained. +
Creates a new instance based on this instance. @@ -1532,6 +1588,45 @@ The setup action used to further configure the newly created object, automatically created by duplicating . An optional to cancel the operation. + + + Expires the cache entry for the specified . +
+
+ In the memory cache: +
+ - if fail-safe is enabled: the entry will marked as logically expired, but will still be available as a fallback value in case of future problems +
+ - if fail-safe is disabled: the entry will be effectively removed +
+
+ In the distributed cache (if any), the entry will be effectively removed. +
+ The instance. + The cache key which identifies the entry in the cache. + The setup action used to further configure the newly created object, automatically created by duplicating . + An optional to cancel the operation. + A to await the completion of the operation. +
+ + + Expires the cache entry for the specified . +
+
+ In the memory cache: +
+ - if fail-safe is enabled: the entry will marked as logically expired, but will still be available as a fallback value in case of future problems +
+ - if fail-safe is disabled: the entry will be effectively removed +
+
+ In the distributed cache (if any), the entry will be effectively removed. +
+ The instance. + The cache key which identifies the entry in the cache. + The setup action used to further configure the newly created object, automatically created by duplicating . + An optional to cancel the operation. +
Returns the default FusionCache instance, the one with the CacheName equals to . @@ -1988,6 +2083,11 @@ The global default . + + + The global default . + + Represents all the options available for the entire instance. @@ -2170,7 +2270,7 @@ - Represents an instance of a FusionCache. + The shared interface that models what a FusionCache instance can do. @@ -2318,6 +2418,43 @@ The options to adhere during this operation. If null is passed, will be used. An optional to cancel the operation. + + + Expires the cache entry for the specified . +
+
+ In the memory cache: +
+ - if fail-safe is enabled: the entry will marked as logically expired, but will still be available as a fallback value in case of future problems +
+ - if fail-safe is disabled: the entry will be effectively removed +
+
+ In the distributed cache (if any), the entry will be effectively removed. +
+ The cache key which identifies the entry in the cache. + The options to adhere during this operation. If null is passed, will be used. + An optional to cancel the operation. + A to await the completion of the operation. +
+ + + Expires the cache entry for the specified . +
+
+ In the memory cache: +
+ - if fail-safe is enabled: the entry will marked as logically expired, but will still be available as a fallback value in case of future problems +
+ - if fail-safe is disabled: the entry will be effectively removed +
+
+ In the distributed cache (if any), the entry will be effectively removed. +
+ The cache key which identifies the entry in the cache. + The options to adhere during this operation. If null is passed, will be used. + An optional to cancel the operation. +
Sets a secondary caching layer, by providing an instance and an instance to be used to convert from generic values to byte[] and viceversa. @@ -2630,12 +2767,13 @@ The type of the entry's value - + Creates a new instance. The actual value. The metadata for the entry. + The original timestamp of the entry, see . @@ -2648,6 +2786,9 @@ + + + @@ -2657,7 +2798,7 @@ - + Creates a new instance from a value and some options. @@ -2666,6 +2807,15 @@ Indicates if the value comes from a fail-safe activation. If provided, it's the last modified date of the entry: this may be used in the next refresh cycle (eg: with the use of the "If-Modified-Since" header in an http request) to check if the entry is changed, to avoid getting the entire value. If provided, it's the ETag of the entry: this may be used in the next refresh cycle (eg: with the use of the "If-None-Match" header in an http request) to check if the entry is changed, to avoid getting the entire value. + The value for the property. + The newly created entry. + + + + Creates a new instance from another entry and some options. + + The source entry. + The object to configure the entry. The newly created entry. @@ -2852,17 +3002,23 @@ Metadata about the cache entry. + + + The timestamp at which the cached value has been originally created: memory cache entries created from distributed cache entries will have the exact same timestamp. + + An entry in a memory layer. - + Creates a new instance. The actual value. The metadata for the entry. + The original timestamp of the entry, see . @@ -2870,6 +3026,9 @@ + + + @@ -2879,7 +3038,7 @@ - + Creates a new instance from a value and some options. @@ -2888,6 +3047,7 @@ Indicates if the value comes from a fail-safe activation. If provided, it's the last modified date of the entry: this may be used in the next refresh cycle (eg: with the use of the "If-Modified-Since" header in an http request) to check if the entry is changed, to avoid getting the entire value. If provided, it's the ETag of the entry: this may be used in the next refresh cycle (eg: with the use of the "If-None-Match" header in an http request) to check if the entry is changed, to avoid getting the entire value. + The value for the property. The newly created entry. @@ -2999,6 +3159,101 @@ + + + An implementation of that implements the null object pattern, meaning that it does nothing. Consider this a kind of a pass-through implementation. + + + + + Creates a new instance. + + The set of cache-wide options to use with this instance of . + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + The core plugin interface to implement to create a FusionCache plugin. @@ -3021,31 +3276,34 @@ Represents one of the core pieces of an instance of an , dealing with acquiring and releasing locks in a highly optimized way. - + Acquire a generic lock, used to synchronize multiple factory operating on the same cache key, and return it. + The name of the FusionCache instance. The key for which to obtain a lock. The operation id which uniquely identifies a high-level cache operation. The optional timeout for the lock acquisition. The to use, if any. - An optional to cancel the operation. The acquired genericlock object, later released when the critical section is over. + An optional to cancel the operation. - + Acquire a generic lock, used to synchronize multiple factory operating on the same cache key, and return it. + The name of the FusionCache instance. The key for which to obtain a lock. The operation id which uniquely identifies a high-level cache operation. The optional timeout for the lock acquisition. - The to use, if any. The acquired genericlock object, later released when the critical section is over. + The to use, if any. - + Release the generic lock object. + The name of the FusionCache instance. The key for which to obtain a lock. The operation id which uniquely identifies a high-level cache operation. The generic lock object to release. diff --git a/src/ZiggyCreatures.FusionCache/docs/README.md b/src/ZiggyCreatures.FusionCache/docs/README.md index d4e50e59..ceb99238 100644 --- a/src/ZiggyCreatures.FusionCache/docs/README.md +++ b/src/ZiggyCreatures.FusionCache/docs/README.md @@ -27,7 +27,7 @@ With [🦄 A Gentle Introduction](https://github.com/ZiggyCreatures/FusionCache/ Want to start using it immediately? There's a [â­ Quick Start](https://github.com/ZiggyCreatures/FusionCache/blob/main/README.md#-quick-start) for you. -Curious about what you can achieve from start to finish? There's a [:woman_teacher: Step By Step ](https://github.com/ZiggyCreatures/FusionCache/blob/main/docs/StepByStep.md) guide. +Curious about what you can achieve from start to finish? There's a [👩â€ðŸ« Step By Step ](https://github.com/ZiggyCreatures/FusionCache/blob/main/docs/StepByStep.md) guide. More into videos? The great Anna Hoffman has been so nice to listen to me mumble random stuff on [Data Exposed](https://learn.microsoft.com/en-us/shows/data-exposed/caching-made-easy-in-azure-sql-db-with-fusioncache-data-exposed). diff --git a/tests/SerializerPayloadGenerator/Program.cs b/tests/SerializerPayloadGenerator/Program.cs new file mode 100644 index 00000000..f174a149 --- /dev/null +++ b/tests/SerializerPayloadGenerator/Program.cs @@ -0,0 +1,102 @@ +using System.Text.RegularExpressions; +using ZiggyCreatures.Caching.Fusion; +using ZiggyCreatures.Caching.Fusion.Internals; +using ZiggyCreatures.Caching.Fusion.Internals.Distributed; +using ZiggyCreatures.Caching.Fusion.Serialization; +using ZiggyCreatures.Caching.Fusion.Serialization.CysharpMemoryPack; +using ZiggyCreatures.Caching.Fusion.Serialization.NeueccMessagePack; +using ZiggyCreatures.Caching.Fusion.Serialization.NewtonsoftJson; +using ZiggyCreatures.Caching.Fusion.Serialization.ProtoBufNet; +using ZiggyCreatures.Caching.Fusion.Serialization.ServiceStackJson; +using ZiggyCreatures.Caching.Fusion.Serialization.SystemTextJson; + +var serializers = new IFusionCacheSerializer[] { + new FusionCacheProtoBufNetSerializer(), + new FusionCacheCysharpMemoryPackSerializer(), + new FusionCacheNeueccMessagePackSerializer(), + new FusionCacheNewtonsoftJsonSerializer(), + new FusionCacheSystemTextJsonSerializer(), + new FusionCacheServiceStackJsonSerializer() +}; + +GenerateSamples(serializers, CreateEntry()); +//TestSamples>(serializers); + +static void TestSamples(IFusionCacheSerializer[] serializers) +{ + foreach (var serializer in serializers) + { + var assembly = typeof(FusionCache).Assembly; + var fvi = System.Diagnostics.FileVersionInfo.GetVersionInfo(assembly.Location); + string? version = fvi.FileVersion; + + var filePrefix = $"{serializer.GetType().Name}__"; + + var files = Directory.GetFiles("samples\\", filePrefix + "*.bin"); + + Console.WriteLine($"SERIALIZER: {serializer.GetType().Name} v{version ?? "?"}"); + Console.WriteLine("SAMPLES:"); + foreach (var file in files) + { + var payloadVersion = Regex.Match(file, @"\w+__v(\d+_\d+_\d+)_\d+\.bin").Groups[1]?.Value?.Replace('_', '.'); + + var payload = File.ReadAllBytes(file); + Console.Write($"- FROM v{payloadVersion}: "); + try + { + var deserialized = serializer.Deserialize(payload); + Console.WriteLine(deserialized is null ? "FAIL" : "OK"); + } + catch (Exception exc) + { + Console.WriteLine($"FAIL {exc.Message}"); + } + } + Console.WriteLine(); + } +} + +static FusionCacheDistributedEntry CreateEntry() +{ + var logicalExpiration = new DateTimeOffset(641400439934520833, TimeSpan.Zero); + + return new FusionCacheDistributedEntry( + "Sloths are cool!", + new FusionCacheEntryMetadata( + logicalExpiration + , true + , logicalExpiration.AddDays(-10) + , "MyETagValue" + , logicalExpiration.AddDays(-100) + ) + ); +} + +static void GenerateSamples(IFusionCacheSerializer[] serializers, T value) +{ + foreach (var serializer in serializers) + { + var assembly = typeof(FusionCache).Assembly; + var fvi = System.Diagnostics.FileVersionInfo.GetVersionInfo(assembly.Location); + string? version = fvi.FileVersion; + + if (string.IsNullOrWhiteSpace(version)) + { + Console.WriteLine("Cannot establish the serializer version"); + return; + } + + var filePrefix = $"{serializer.GetType().Name}__"; + + var filename = $"{filePrefix}v{version.Replace('.', '_')}.bin".ToLowerInvariant(); + + var payload = serializer.Serialize(value); + + File.WriteAllBytes(filename, payload); + + Console.WriteLine("FusionCache"); + Console.WriteLine($"- VERSION : v{version ?? "?"}"); + Console.WriteLine($"- FILENAME: {filename}"); + Console.WriteLine(); + } +} diff --git a/tests/SerializerPayloadGenerator/Samples/fusioncachecysharpmemorypackserializer__v0_20_0_0.bin b/tests/SerializerPayloadGenerator/Samples/fusioncachecysharpmemorypackserializer__v0_20_0_0.bin new file mode 100644 index 0000000000000000000000000000000000000000..a0a1029973dd064653dc5ec5003e076095ce426e GIT binary patch literal 43 ucmZR2|NsAg0R{$!;GF!DjADhvqEv>`fl2Uvnp3#$ h8OIl(3EV(2-%8hz#PqPloYGVbrF};>??`{f0RVTZ9De`+ literal 0 HcmV?d00001 diff --git a/tests/SerializerPayloadGenerator/Samples/fusioncacheneueccmessagepackserializer__v0_20_0_0.bin b/tests/SerializerPayloadGenerator/Samples/fusioncacheneueccmessagepackserializer__v0_20_0_0.bin new file mode 100644 index 0000000000000000000000000000000000000000..17f5c9cc53cec81c67386898d0669f02f370b901 GIT binary patch literal 40 wcmZo#ShgWJC%+`4SRt_}RUtV)KSyz4ZqvflN!R~ppZGbUyfXhL!@{(~0DJ!v$p8QV literal 0 HcmV?d00001 diff --git a/tests/SerializerPayloadGenerator/Samples/fusioncacheneueccmessagepackserializer__v0_21_0_0.bin b/tests/SerializerPayloadGenerator/Samples/fusioncacheneueccmessagepackserializer__v0_21_0_0.bin new file mode 100644 index 0000000000000000000000000000000000000000..a115c8f19afc2b231a364d756bb0fd83eaafa596 GIT binary patch literal 85 zcmZo#ShgWJC%+`4SRt_}RUtV)KSyz4ZtKF-N!R~ppZGbUyfXhL!@{(~i&7I|A~_B> f85X6MtoE&R4M|K7OUx-vU6hjxl`Z=^^CklTANMR8 literal 0 HcmV?d00001 diff --git a/tests/SerializerPayloadGenerator/Samples/fusioncachenewtonsoftjsonserializer__v0_20_0_0.bin b/tests/SerializerPayloadGenerator/Samples/fusioncachenewtonsoftjsonserializer__v0_20_0_0.bin new file mode 100644 index 00000000..26d7b70a --- /dev/null +++ b/tests/SerializerPayloadGenerator/Samples/fusioncachenewtonsoftjsonserializer__v0_20_0_0.bin @@ -0,0 +1 @@ +{"v":"Sloths are cool!","m":{"e":"2033-07-08T14:53:13.4520833+00:00","f":true}} \ No newline at end of file diff --git a/tests/SerializerPayloadGenerator/Samples/fusioncachenewtonsoftjsonserializer__v0_21_0_0.bin b/tests/SerializerPayloadGenerator/Samples/fusioncachenewtonsoftjsonserializer__v0_21_0_0.bin new file mode 100644 index 00000000..a5003a08 --- /dev/null +++ b/tests/SerializerPayloadGenerator/Samples/fusioncachenewtonsoftjsonserializer__v0_21_0_0.bin @@ -0,0 +1 @@ +{"v":"Sloths are cool!","m":{"e":"2033-07-08T14:53:13.4520833+00:00","f":true,"ea":"2033-06-28T14:53:13.4520833+00:00","et":"MyETagValue","lm":"2033-03-30T14:53:13.4520833+00:00"}} \ No newline at end of file diff --git a/tests/SerializerPayloadGenerator/Samples/fusioncacheprotobufnetserializer__v0_20_0_0.bin b/tests/SerializerPayloadGenerator/Samples/fusioncacheprotobufnetserializer__v0_20_0_0.bin new file mode 100644 index 00000000..674bde43 --- /dev/null +++ b/tests/SerializerPayloadGenerator/Samples/fusioncacheprotobufnetserializer__v0_20_0_0.bin @@ -0,0 +1,2 @@ + +Sloths are cool! œÎ¨ÔÉ­ó \ No newline at end of file diff --git a/tests/SerializerPayloadGenerator/Samples/fusioncacheprotobufnetserializer__v0_21_0_0.bin b/tests/SerializerPayloadGenerator/Samples/fusioncacheprotobufnetserializer__v0_21_0_0.bin new file mode 100644 index 00000000..49afabe3 --- /dev/null +++ b/tests/SerializerPayloadGenerator/Samples/fusioncacheprotobufnetserializer__v0_21_0_0.bin @@ -0,0 +1,2 @@ + +Sloths are cool!-œÎ¨Ôɭ󜒞‹÷™ó" MyETagValue(œÈç™Î«ó \ No newline at end of file diff --git a/tests/SerializerPayloadGenerator/Samples/fusioncacheservicestackjsonserializer__v0_20_0_0.bin b/tests/SerializerPayloadGenerator/Samples/fusioncacheservicestackjsonserializer__v0_20_0_0.bin new file mode 100644 index 00000000..77ae829f --- /dev/null +++ b/tests/SerializerPayloadGenerator/Samples/fusioncacheservicestackjsonserializer__v0_20_0_0.bin @@ -0,0 +1 @@ +{"v":"Sloths are cool!","m":{"e":"\/Date(2004447193452)\/","f":true}} \ No newline at end of file diff --git a/tests/SerializerPayloadGenerator/Samples/fusioncacheservicestackjsonserializer__v0_21_0_0.bin b/tests/SerializerPayloadGenerator/Samples/fusioncacheservicestackjsonserializer__v0_21_0_0.bin new file mode 100644 index 00000000..a5003a08 --- /dev/null +++ b/tests/SerializerPayloadGenerator/Samples/fusioncacheservicestackjsonserializer__v0_21_0_0.bin @@ -0,0 +1 @@ +{"v":"Sloths are cool!","m":{"e":"2033-07-08T14:53:13.4520833+00:00","f":true,"ea":"2033-06-28T14:53:13.4520833+00:00","et":"MyETagValue","lm":"2033-03-30T14:53:13.4520833+00:00"}} \ No newline at end of file diff --git a/tests/SerializerPayloadGenerator/Samples/fusioncachesystemtextjsonserializer__v0_20_0_0.bin b/tests/SerializerPayloadGenerator/Samples/fusioncachesystemtextjsonserializer__v0_20_0_0.bin new file mode 100644 index 00000000..8e550419 --- /dev/null +++ b/tests/SerializerPayloadGenerator/Samples/fusioncachesystemtextjsonserializer__v0_20_0_0.bin @@ -0,0 +1 @@ +{"Value":"Sloths are cool!","Metadata":{"LogicalExpiration":"2033-07-08T14:53:13.4520833+00:00","IsFromFailSafe":true}} \ No newline at end of file diff --git a/tests/SerializerPayloadGenerator/Samples/fusioncachesystemtextjsonserializer__v0_21_0_0.bin b/tests/SerializerPayloadGenerator/Samples/fusioncachesystemtextjsonserializer__v0_21_0_0.bin new file mode 100644 index 00000000..fe23a750 --- /dev/null +++ b/tests/SerializerPayloadGenerator/Samples/fusioncachesystemtextjsonserializer__v0_21_0_0.bin @@ -0,0 +1 @@ +{"Value":"Sloths are cool!","Metadata":{"LogicalExpiration":"2033-07-08T14:53:13.4520833+00:00","IsFromFailSafe":true,"EagerExpiration":"2033-06-28T14:53:13.4520833+00:00","ETag":"MyETagValue","LastModified":"2033-03-30T14:53:13.4520833+00:00"}} \ No newline at end of file diff --git a/tests/SerializerPayloadGenerator/SerializerPayloadGenerator.csproj b/tests/SerializerPayloadGenerator/SerializerPayloadGenerator.csproj new file mode 100644 index 00000000..51f63fed --- /dev/null +++ b/tests/SerializerPayloadGenerator/SerializerPayloadGenerator.csproj @@ -0,0 +1,24 @@ + + + + Exe + net6.0 + enable + enable + + + + + + + + + + + + + + Always + + + diff --git a/tests/ZiggyCreatures.FusionCache.Playground/Scenarios/LoggingScenario.cs b/tests/ZiggyCreatures.FusionCache.Playground/Scenarios/LoggingScenario.cs index 0b8ebe88..8448eefe 100644 --- a/tests/ZiggyCreatures.FusionCache.Playground/Scenarios/LoggingScenario.cs +++ b/tests/ZiggyCreatures.FusionCache.Playground/Scenarios/LoggingScenario.cs @@ -1,4 +1,5 @@ using System; +using System.Collections.Generic; using System.Text; using System.Threading.Tasks; using Microsoft.Extensions.Caching.Distributed; @@ -43,7 +44,9 @@ private static void SetupSerilogLogger(IServiceCollection services, LogEventLeve Log.Logger = new LoggerConfiguration() .MinimumLevel.Is(minLevel) .Enrich.FromLogContext() - .WriteTo.Console() + .WriteTo.Console( + outputTemplate: "[{Timestamp:HH:mm:ss} {Level:u3}] {Message:lj}{NewLine}{Exception}{Properties}{NewLine}" + ) .CreateLogger() ; @@ -87,8 +90,15 @@ public static async Task RunAsync() AllowBackgroundBackplaneOperations = false }, }; - using (var fusionCache = new FusionCache(options, logger: logger)) + + var cachesCount = 1; + var caches = new List(); + IFusionCache fusionCache; + + for (int i = 0; i < cachesCount; i++) { + fusionCache = new FusionCache(options, logger: logger); + if (UseDistributedCache) { // DISTRIBUTED CACHE @@ -110,62 +120,66 @@ public static async Task RunAsync() Console.WriteLine(); } - var tmp0 = await fusionCache.GetOrDefaultAsync("foo", 123); - Console.WriteLine(); - Console.WriteLine($"pre-initial: {tmp0}"); - - await fusionCache.SetAsync("foo", 42, options => options.SetDurationSec(1).SetFailSafe(UseFailSafe, TimeSpan.FromMinutes(1))); - Console.WriteLine(); - Console.WriteLine($"initial: {fusionCache.GetOrDefault("foo")}"); - await Task.Delay(1_500); - Console.WriteLine(); - - var tmp1 = await fusionCache.GetOrSetAsync("foo", async _ => { await Task.Delay(2_000); return 21; }, options => options.SetDurationSec(1).SetFailSafe(UseFailSafe).SetFactoryTimeouts(1_000)); - Console.WriteLine(); - Console.WriteLine($"tmp1: {tmp1}"); - await Task.Delay(2_500); - Console.WriteLine(); - - var tmp2 = await fusionCache.GetOrDefaultAsync("foo", options => options.SetDurationSec(1).SetFailSafe(UseFailSafe).SetFactoryTimeouts(1_000)); - Console.WriteLine(); - Console.WriteLine($"tmp2: {tmp2}"); - await Task.Delay(2_500); - Console.WriteLine(); - - var tmp3 = await fusionCache.GetOrSetAsync("foo", async _ => { await Task.Delay(2_000); throw new Exception("Sloths are cool"); }, options => options.SetDurationSec(1).SetFailSafe(UseFailSafe).SetFactoryTimeouts(1_000)); - Console.WriteLine(); - Console.WriteLine($"tmp3: {tmp3}"); - await Task.Delay(2_500); - Console.WriteLine(); - - var tmp4 = await fusionCache.GetOrSetAsync("foo", async _ => { await Task.Delay(2_000); return 666; }, options => options.SetDurationSec(1).SetFailSafe(UseFailSafe).SetFactoryTimeouts(1_000, keepTimedOutFactoryResult: false)); - Console.WriteLine(); - Console.WriteLine($"tmp4: {tmp4}"); - await Task.Delay(2_500); - Console.WriteLine(); - - var tmp5 = await fusionCache.GetOrDefaultAsync("foo", options => options.SetDurationSec(1).SetFailSafe(UseFailSafe).SetFactoryTimeouts(1_000)); - Console.WriteLine(); - Console.WriteLine($"tmp5: {tmp5}"); - Console.WriteLine(); - - await fusionCache.SetAsync("foo", 123, fusionCache.CreateEntryOptions(entry => entry.SetDurationSec(1).SetFailSafe(UseFailSafe))); - await Task.Delay(1_500); - Console.WriteLine(); - - await fusionCache.GetOrSetAsync("foo", _ => { throw new Exception("Foo"); }, options => options.SetDurationSec(1).SetFailSafe(UseFailSafe).SetFactoryTimeouts(1_000)); - Console.WriteLine(); - - await fusionCache.SetAsync("foo", 123, options => options.SetDurationSec(1).SetFailSafe(UseFailSafe)); - await Task.Delay(1_500); - Console.WriteLine(); - - await fusionCache.GetOrSetAsync("foo", async _ => { await Task.Delay(2_000); throw new Exception("Foo"); }, options => options.SetDurationSec(1).SetFailSafe(UseFailSafe).SetFactoryTimeouts(1_000)); - Console.WriteLine(); - await Task.Delay(2_500); - - Console.WriteLine("\n\nTHE END"); + caches.Add(fusionCache); } + + fusionCache = caches[0]; + + var tmp0 = await fusionCache.GetOrDefaultAsync("foo", 123); + Console.WriteLine(); + Console.WriteLine($"pre-initial: {tmp0}"); + + await fusionCache.SetAsync("foo", 42, options => options.SetDurationSec(1).SetFailSafe(UseFailSafe, TimeSpan.FromMinutes(1))); + Console.WriteLine(); + Console.WriteLine($"initial: {fusionCache.GetOrDefault("foo")}"); + await Task.Delay(1_500); + Console.WriteLine(); + + var tmp1 = await fusionCache.GetOrSetAsync("foo", async _ => { await Task.Delay(2_000); return 21; }, options => options.SetDurationSec(1).SetFailSafe(UseFailSafe).SetFactoryTimeouts(1_000)); + Console.WriteLine(); + Console.WriteLine($"tmp1: {tmp1}"); + await Task.Delay(2_500); + Console.WriteLine(); + + var tmp2 = await fusionCache.GetOrDefaultAsync("foo", options => options.SetDurationSec(1).SetFailSafe(UseFailSafe).SetFactoryTimeouts(1_000)); + Console.WriteLine(); + Console.WriteLine($"tmp2: {tmp2}"); + await Task.Delay(2_500); + Console.WriteLine(); + + var tmp3 = await fusionCache.GetOrSetAsync("foo", async _ => { await Task.Delay(2_000); throw new Exception("Sloths are cool"); }, options => options.SetDurationSec(1).SetFailSafe(UseFailSafe).SetFactoryTimeouts(1_000)); + Console.WriteLine(); + Console.WriteLine($"tmp3: {tmp3}"); + await Task.Delay(2_500); + Console.WriteLine(); + + var tmp4 = await fusionCache.GetOrSetAsync("foo", async _ => { await Task.Delay(2_000); return 666; }, options => options.SetDurationSec(1).SetFailSafe(UseFailSafe).SetFactoryTimeouts(1_000, keepTimedOutFactoryResult: false)); + Console.WriteLine(); + Console.WriteLine($"tmp4: {tmp4}"); + await Task.Delay(2_500); + Console.WriteLine(); + + var tmp5 = await fusionCache.GetOrDefaultAsync("foo", options => options.SetDurationSec(1).SetFailSafe(UseFailSafe).SetFactoryTimeouts(1_000)); + Console.WriteLine(); + Console.WriteLine($"tmp5: {tmp5}"); + Console.WriteLine(); + + await fusionCache.SetAsync("foo", 123, fusionCache.CreateEntryOptions(entry => entry.SetDurationSec(1).SetFailSafe(UseFailSafe))); + await Task.Delay(1_500); + Console.WriteLine(); + + await fusionCache.GetOrSetAsync("foo", _ => { throw new Exception("Foo"); }, options => options.SetDurationSec(1).SetFailSafe(UseFailSafe).SetFactoryTimeouts(1_000)); + Console.WriteLine(); + + await fusionCache.SetAsync("foo", 123, options => options.SetDurationSec(1).SetFailSafe(UseFailSafe)); + await Task.Delay(1_500); + Console.WriteLine(); + + await fusionCache.GetOrSetAsync("foo", async _ => { await Task.Delay(2_000); throw new Exception("Foo"); }, options => options.SetDurationSec(1).SetFailSafe(UseFailSafe).SetFactoryTimeouts(1_000)); + Console.WriteLine(); + await Task.Delay(2_500); + + Console.WriteLine("\n\nTHE END"); } } } diff --git a/tests/ZiggyCreatures.FusionCache.Playground/ZiggyCreatures.FusionCache.Playground.csproj b/tests/ZiggyCreatures.FusionCache.Playground/ZiggyCreatures.FusionCache.Playground.csproj index 191a30b9..66729ec3 100644 --- a/tests/ZiggyCreatures.FusionCache.Playground/ZiggyCreatures.FusionCache.Playground.csproj +++ b/tests/ZiggyCreatures.FusionCache.Playground/ZiggyCreatures.FusionCache.Playground.csproj @@ -10,13 +10,13 @@ - + - - + + - + diff --git a/tests/ZiggyCreatures.FusionCache.Tests/BackplaneTests.cs b/tests/ZiggyCreatures.FusionCache.Tests/BackplaneTests.cs index 74e6dc36..465be0b3 100644 --- a/tests/ZiggyCreatures.FusionCache.Tests/BackplaneTests.cs +++ b/tests/ZiggyCreatures.FusionCache.Tests/BackplaneTests.cs @@ -1,11 +1,14 @@ using System; using System.Threading; using System.Threading.Tasks; +using FusionCacheTests.Stuff; using Microsoft.Extensions.Caching.Distributed; using Microsoft.Extensions.Caching.Memory; using Microsoft.Extensions.Caching.StackExchangeRedis; +using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; using Xunit; +using Xunit.Abstractions; using ZiggyCreatures.Caching.Fusion; using ZiggyCreatures.Caching.Fusion.Backplane; using ZiggyCreatures.Caching.Fusion.Backplane.Memory; @@ -16,18 +19,30 @@ namespace FusionCacheTests { public class BackplaneTests { + private readonly ITestOutputHelper _output; + + public BackplaneTests(ITestOutputHelper output) + { + _output = output; + } + + private XUnitLogger CreateLogger(LogLevel minLevel = LogLevel.Trace) + { + return new XUnitLogger(minLevel, _output); + } + private static readonly string? RedisConnection = null; //private static readonly string? RedisConnection = "127.0.0.1:6379,ssl=False,abortConnect=False"; - private static IFusionCacheBackplane CreateBackplane() + private IFusionCacheBackplane CreateBackplane() { if (string.IsNullOrWhiteSpace(RedisConnection)) - return new MemoryBackplane(new MemoryBackplaneOptions()); + return new MemoryBackplane(new MemoryBackplaneOptions(), logger: CreateLogger()); - return new RedisBackplane(new RedisBackplaneOptions { Configuration = RedisConnection }); + return new RedisBackplane(new RedisBackplaneOptions { Configuration = RedisConnection }, logger: CreateLogger()); } - private static ChaosBackplane CreateChaosBackplane() + private ChaosBackplane CreateChaosBackplane() { return new ChaosBackplane(CreateBackplane()); } @@ -40,7 +55,7 @@ private static IDistributedCache CreateDistributedCache() return new RedisCache(new RedisCacheOptions { Configuration = RedisConnection }); } - private static IFusionCache CreateFusionCache(string? cacheName, SerializerType? serializerType, IDistributedCache? distributedCache, IFusionCacheBackplane? backplane, Action? setupAction = null) + private IFusionCache CreateFusionCache(string? cacheName, SerializerType? serializerType, IDistributedCache? distributedCache, IFusionCacheBackplane? backplane, Action? setupAction = null) { var options = new FusionCacheOptions() { @@ -48,7 +63,7 @@ private static IFusionCache CreateFusionCache(string? cacheName, SerializerType? EnableSyncEventHandlersExecution = true }; setupAction?.Invoke(options); - var fusionCache = new FusionCache(options); + var fusionCache = new FusionCache(options, logger: CreateLogger()); fusionCache.DefaultEntryOptions.AllowBackgroundBackplaneOperations = false; fusionCache.DefaultEntryOptions.AllowBackgroundDistributedCacheOperations = false; if (distributedCache is not null && serializerType.HasValue) @@ -313,17 +328,21 @@ public async Task CanSkipNotificationsAsync() public void CanSkipNotifications() { var key = Guid.NewGuid().ToString("N"); - using var cache1 = CreateFusionCache(null, null, null, CreateBackplane()); - using var cache2 = CreateFusionCache(null, null, null, CreateBackplane()); - using var cache3 = CreateFusionCache(null, null, null, CreateBackplane()); - - cache1.DefaultEntryOptions.SkipBackplaneNotifications = true; - cache2.DefaultEntryOptions.SkipBackplaneNotifications = true; - cache3.DefaultEntryOptions.SkipBackplaneNotifications = true; - - cache1.DefaultEntryOptions.AllowBackgroundBackplaneOperations = false; - cache2.DefaultEntryOptions.AllowBackgroundBackplaneOperations = false; - cache3.DefaultEntryOptions.AllowBackgroundBackplaneOperations = false; + using var cache1 = CreateFusionCache(null, null, null, CreateBackplane(), options => + { + options.DefaultEntryOptions.SkipBackplaneNotifications = true; + options.DefaultEntryOptions.AllowBackgroundBackplaneOperations = false; + }); + using var cache2 = CreateFusionCache(null, null, null, CreateBackplane(), options => + { + options.DefaultEntryOptions.SkipBackplaneNotifications = true; + options.DefaultEntryOptions.AllowBackgroundBackplaneOperations = false; + }); + using var cache3 = CreateFusionCache(null, null, null, CreateBackplane(), options => + { + options.DefaultEntryOptions.SkipBackplaneNotifications = true; + options.DefaultEntryOptions.AllowBackgroundBackplaneOperations = false; + }); Thread.Sleep(1_000); @@ -669,5 +688,335 @@ public void AutoRecoveryRespectsMaxItems(SerializerType serializerType) cache3?.Dispose(); } } + + [Theory] + [ClassData(typeof(SerializerTypesClassData))] + public async Task CanHandleExpireOnMultiNodesAsync(SerializerType serializerType) + { + var duration = TimeSpan.FromMinutes(10); + + var distributedCache = new MemoryDistributedCache(Options.Create(new MemoryDistributedCacheOptions())); + + using var cacheA = new FusionCache(new FusionCacheOptions(), logger: CreateLogger()); + cacheA.SetupDistributedCache(distributedCache, TestsUtils.GetSerializer(serializerType)); + cacheA.SetupBackplane(CreateBackplane()); + cacheA.DefaultEntryOptions.IsFailSafeEnabled = true; + cacheA.DefaultEntryOptions.Duration = duration; + cacheA.DefaultEntryOptions.AllowBackgroundBackplaneOperations = false; + + using var cacheB = new FusionCache(new FusionCacheOptions(), logger: CreateLogger()); + cacheB.SetupDistributedCache(distributedCache, TestsUtils.GetSerializer(serializerType)); + cacheB.SetupBackplane(CreateBackplane()); + cacheB.DefaultEntryOptions.IsFailSafeEnabled = true; + cacheB.DefaultEntryOptions.Duration = duration; + cacheB.DefaultEntryOptions.AllowBackgroundBackplaneOperations = false; + + using var cacheC = new FusionCache(new FusionCacheOptions(), logger: CreateLogger()); + cacheC.SetupDistributedCache(distributedCache, TestsUtils.GetSerializer(serializerType)); + cacheC.SetupBackplane(CreateBackplane()); + cacheC.DefaultEntryOptions.IsFailSafeEnabled = true; + cacheC.DefaultEntryOptions.Duration = duration; + cacheC.DefaultEntryOptions.AllowBackgroundBackplaneOperations = false; + + // SET ON CACHE A + await cacheA.SetAsync("foo", 42); + + // GET ON CACHE A + var maybeFooA1 = await cacheA.TryGetAsync("foo", opt => opt.SetFailSafe(true)); + + // GET ON CACHE B (WILL GET FROM DISTRIBUTED CACHE AND SAVE ON LOCAL MEMORY CACHE) + var maybeFooB1 = await cacheB.TryGetAsync("foo", opt => opt.SetFailSafe(true)); + + // NOW CACHE A + B HAVE THE VALUE CACHED IN THEIR LOCAL MEMORY CACHE, WHILE CACHE C DOES NOT + + // EXPIRE ON CACHE A, WHIS WILL: + // - EXPIRE ON CACHE A + // - REMOVE ON DISTRIBUTED CACHE + // - NOTIFY CACHE B AND CACHE C OF THE EXPIRATION AND THAT, IN TURN, WILL: + // - EXPIRE ON CACHE B + // - DO NOTHING ON CACHE C (IT WAS NOT IN ITS MEMORY CACHE) + await cacheA.ExpireAsync("foo"); + + await Task.Delay(TimeSpan.FromMilliseconds(100)); + + // GET ON CACHE A: SINCE IT'S EXPIRED AND FAIL-SAFE IS DISABLED, NOTHING WILL BE RETURNED + var maybeFooA2 = await cacheA.TryGetAsync("foo", opt => opt.SetFailSafe(false)); + + // GET ON CACHE B: SINCE IT'S EXPIRED AND FAIL-SAFE IS DISABLED, NOTHING WILL BE RETURNED + var maybeFooB2 = await cacheB.TryGetAsync("foo", opt => opt.SetFailSafe(false)); + + // GET ON CACHE C: SINCE NOTHING IS THERE, NOTHING WILL BE RETURNED + var maybeFooC2 = await cacheC.TryGetAsync("foo", opt => opt.SetFailSafe(false)); + + _output.WriteLine($"BEFORE"); + + // GET ON CACHE A: SINCE IT'S EXPIRED BUT FAIL-SAFE IS ENABLED, THE STALE VALUE WILL BE RETURNED + var maybeFooA3 = await cacheA.TryGetAsync("foo", opt => opt.SetFailSafe(true)); + + _output.WriteLine($"AFTER"); + + // GET ON CACHE B: SINCE IT'S EXPIRED BUT FAIL-SAFE IS ENABLED, THE STALE VALUE WILL BE RETURNED + var maybeFooB3 = await cacheB.TryGetAsync("foo", opt => opt.SetFailSafe(true)); + + // GET ON CACHE C: SINCE NOTHING IS THERE, NOTHING WILL BE RETURNED + var maybeFooC3 = await cacheC.TryGetAsync("foo", opt => opt.SetFailSafe(true)); + + Assert.True(maybeFooA1.HasValue); + Assert.Equal(42, maybeFooA1.Value); + + Assert.True(maybeFooB1.HasValue); + Assert.Equal(42, maybeFooB1.Value); + + Assert.False(maybeFooA2.HasValue); + Assert.False(maybeFooB2.HasValue); + Assert.False(maybeFooC2.HasValue); + + Assert.True(maybeFooA3.HasValue); + Assert.Equal(42, maybeFooA3.Value); + + Assert.True(maybeFooB3.HasValue); + Assert.Equal(42, maybeFooB3.Value); + + Assert.False(maybeFooC3.HasValue); + } + + [Theory] + [ClassData(typeof(SerializerTypesClassData))] + public void CanHandleExpireOnMultiNodes(SerializerType serializerType) + { + var duration = TimeSpan.FromMinutes(10); + + var distributedCache = new MemoryDistributedCache(Options.Create(new MemoryDistributedCacheOptions())); + + using var cacheA = new FusionCache(new FusionCacheOptions(), logger: CreateLogger()); + cacheA.SetupDistributedCache(distributedCache, TestsUtils.GetSerializer(serializerType)); + cacheA.SetupBackplane(CreateBackplane()); + cacheA.DefaultEntryOptions.IsFailSafeEnabled = true; + cacheA.DefaultEntryOptions.Duration = duration; + cacheA.DefaultEntryOptions.AllowBackgroundBackplaneOperations = false; + + using var cacheB = new FusionCache(new FusionCacheOptions(), logger: CreateLogger()); + cacheB.SetupDistributedCache(distributedCache, TestsUtils.GetSerializer(serializerType)); + cacheB.SetupBackplane(CreateBackplane()); + cacheB.DefaultEntryOptions.IsFailSafeEnabled = true; + cacheB.DefaultEntryOptions.Duration = duration; + cacheB.DefaultEntryOptions.AllowBackgroundBackplaneOperations = false; + + using var cacheC = new FusionCache(new FusionCacheOptions(), logger: CreateLogger()); + cacheC.SetupDistributedCache(distributedCache, TestsUtils.GetSerializer(serializerType)); + cacheC.SetupBackplane(CreateBackplane()); + cacheC.DefaultEntryOptions.IsFailSafeEnabled = true; + cacheC.DefaultEntryOptions.Duration = duration; + cacheC.DefaultEntryOptions.AllowBackgroundBackplaneOperations = false; + + // SET ON CACHE A + cacheA.Set("foo", 42); + + // GET ON CACHE A + var maybeFooA1 = cacheA.TryGet("foo", opt => opt.SetFailSafe(true)); + + // GET ON CACHE B (WILL GET FROM DISTRIBUTED CACHE AND SAVE ON LOCAL MEMORY CACHE) + var maybeFooB1 = cacheB.TryGet("foo", opt => opt.SetFailSafe(true)); + + // NOW CACHE A + B HAVE THE VALUE CACHED IN THEIR LOCAL MEMORY CACHE, WHILE CACHE C DOES NOT + + // EXPIRE ON CACHE A, WHIS WILL: + // - EXPIRE ON CACHE A + // - REMOVE ON DISTRIBUTED CACHE + // - NOTIFY CACHE B AND CACHE C OF THE EXPIRATION AND THAT, IN TURN, WILL: + // - EXPIRE ON CACHE B + // - DO NOTHING ON CACHE C (IT WAS NOT IN ITS MEMORY CACHE) + cacheA.Expire("foo"); + + Thread.Sleep(TimeSpan.FromMilliseconds(100)); + + // GET ON CACHE A: SINCE IT'S EXPIRED AND FAIL-SAFE IS DISABLED, NOTHING WILL BE RETURNED + var maybeFooA2 = cacheA.TryGet("foo", opt => opt.SetFailSafe(false)); + + // GET ON CACHE B: SINCE IT'S EXPIRED AND FAIL-SAFE IS DISABLED, NOTHING WILL BE RETURNED + var maybeFooB2 = cacheB.TryGet("foo", opt => opt.SetFailSafe(false)); + + // GET ON CACHE C: SINCE NOTHING IS THERE, NOTHING WILL BE RETURNED + var maybeFooC2 = cacheC.TryGet("foo", opt => opt.SetFailSafe(false)); + + // GET ON CACHE A: SINCE IT'S EXPIRED BUT FAIL-SAFE IS ENABLED, THE STALE VALUE WILL BE RETURNED + var maybeFooA3 = cacheA.TryGet("foo", opt => opt.SetFailSafe(true)); + + // GET ON CACHE B: SINCE IT'S EXPIRED BUT FAIL-SAFE IS ENABLED, THE STALE VALUE WILL BE RETURNED + var maybeFooB3 = cacheB.TryGet("foo", opt => opt.SetFailSafe(true)); + + // GET ON CACHE C: SINCE NOTHING IS THERE, NOTHING WILL BE RETURNED + var maybeFooC3 = cacheC.TryGet("foo", opt => opt.SetFailSafe(true)); + + Assert.True(maybeFooA1.HasValue); + Assert.Equal(42, maybeFooA1.Value); + + Assert.True(maybeFooB1.HasValue); + Assert.Equal(42, maybeFooB1.Value); + + Assert.False(maybeFooA2.HasValue); + Assert.False(maybeFooB2.HasValue); + Assert.False(maybeFooC2.HasValue); + + Assert.True(maybeFooA3.HasValue); + Assert.Equal(42, maybeFooA3.Value); + + Assert.True(maybeFooB3.HasValue); + Assert.Equal(42, maybeFooB3.Value); + + Assert.False(maybeFooC3.HasValue); + } + + [Theory] + [ClassData(typeof(SerializerTypesClassData))] + public async Task BackgroundFactoryCompleteNotifyOtherNodesAsync(SerializerType serializerType) + { + var duration1 = TimeSpan.FromSeconds(1); + var duration2 = TimeSpan.FromSeconds(10); + var factorySoftTimeout = TimeSpan.FromMilliseconds(50); + var simulatedFactoryDuration = TimeSpan.FromSeconds(3); + + var distributedCache = new MemoryDistributedCache(Options.Create(new MemoryDistributedCacheOptions())); + + using var cacheA = new FusionCache(new FusionCacheOptions(), logger: CreateLogger()); + cacheA.SetupDistributedCache(distributedCache, TestsUtils.GetSerializer(serializerType)); + cacheA.SetupBackplane(CreateBackplane()); + cacheA.DefaultEntryOptions.IsFailSafeEnabled = true; + cacheA.DefaultEntryOptions.FactorySoftTimeout = factorySoftTimeout; + + using var cacheB = new FusionCache(new FusionCacheOptions(), logger: CreateLogger()); + cacheB.SetupDistributedCache(distributedCache, TestsUtils.GetSerializer(serializerType)); + cacheB.SetupBackplane(CreateBackplane()); + cacheB.DefaultEntryOptions.IsFailSafeEnabled = true; + cacheB.DefaultEntryOptions.FactorySoftTimeout = factorySoftTimeout; + + // SET 10 ON CACHE-A AND DIST CACHE + var fooA1 = await cacheA.GetOrSetAsync("foo", async _ => 10, duration1); + + // GET 10 FROM DIST CACHE AND SET ON CACHE-B + var fooB1 = await cacheB.GetOrSetAsync("foo", async _ => 20, duration1); + + Assert.Equal(10, fooA1); + Assert.Equal(10, fooB1); + + // WAIT FOR THE CACHE ENTRIES TO EXPIRE + await Task.Delay(duration1.PlusALittleBit()); + + // EXECUTE THE FACTORY ON CACHE-A, WHICH WILL TAKE 3 SECONDS, BUT + // THE FACTORY SOFT TIMEOUT IS 50 MILLISECONDS, SO IT WILL FAIL + // AND THE STALE VALUE WILL BE RETURNED + // THE FACTORY WILL BE KEPT RUNNING IN THE BACKGROUND, AND WHEN + // IT WILL COMPLETE SUCCESSFULLY UPDATE CACHE-A, THE DIST + // CACHE AND NOTIFY THE OTHER NODES + // SUCESSFULLY UPDATE CACHE-A, THE DIST CACHE AND NOTIFY THE OTHER NODES + var fooA2 = await cacheA.GetOrSetAsync( + "foo", + async _ => + { + await Task.Delay(simulatedFactoryDuration); + return 30; + }, + duration2 + ); + + // IMMEDIATELY GET OR SET FROM CACHE-B: THE VALUE THERE IS + // EXPIRED, SO THE NEW VALUE WILL BE SAVED AND RETURNED + var fooB2 = await cacheB.GetOrSetAsync( + "foo", + 40, + duration2 + ); + + Assert.Equal(10, fooA2); + Assert.Equal(40, fooB2); + + // WAIT FOR THE SIMULATED FACTORY TO COMPLETE: A NOTIFICATION + // WILL BE SENT TO THE OTHER NODES, WHICH IN TURN WILL UPDATE + // THEIR CACHE ENTRIES + await Task.Delay(simulatedFactoryDuration.PlusALittleBit()); + + // GET THE UPDATED VALUES FROM CACHE-A AND CACHE-B + var fooA3 = await cacheA.GetOrDefaultAsync("foo"); + var fooB3 = await cacheB.GetOrDefaultAsync("foo"); + + Assert.Equal(30, fooA3); + Assert.Equal(30, fooB3); + } + + [Theory] + [ClassData(typeof(SerializerTypesClassData))] + public void BackgroundFactoryCompleteNotifyOtherNodes(SerializerType serializerType) + { + var duration1 = TimeSpan.FromSeconds(1); + var duration2 = TimeSpan.FromSeconds(10); + var factorySoftTimeout = TimeSpan.FromMilliseconds(50); + var simulatedFactoryDuration = TimeSpan.FromSeconds(3); + + var distributedCache = new MemoryDistributedCache(Options.Create(new MemoryDistributedCacheOptions())); + + using var cacheA = new FusionCache(new FusionCacheOptions(), logger: CreateLogger()); + cacheA.SetupDistributedCache(distributedCache, TestsUtils.GetSerializer(serializerType)); + cacheA.SetupBackplane(CreateBackplane()); + cacheA.DefaultEntryOptions.IsFailSafeEnabled = true; + cacheA.DefaultEntryOptions.FactorySoftTimeout = factorySoftTimeout; + + using var cacheB = new FusionCache(new FusionCacheOptions(), logger: CreateLogger()); + cacheB.SetupDistributedCache(distributedCache, TestsUtils.GetSerializer(serializerType)); + cacheB.SetupBackplane(CreateBackplane()); + cacheB.DefaultEntryOptions.IsFailSafeEnabled = true; + cacheB.DefaultEntryOptions.FactorySoftTimeout = factorySoftTimeout; + + // SET 10 ON CACHE-A AND DIST CACHE + var fooA1 = cacheA.GetOrSet("foo", _ => 10, duration1); + + // GET 10 FROM DIST CACHE AND SET ON CACHE-B + var fooB1 = cacheB.GetOrSet("foo", _ => 20, duration1); + + Assert.Equal(10, fooA1); + Assert.Equal(10, fooB1); + + // WAIT FOR THE CACHE ENTRIES TO EXPIRE + Thread.Sleep(duration1.PlusALittleBit()); + + // EXECUTE THE FACTORY ON CACHE-A, WHICH WILL TAKE 3 SECONDS, BUT + // THE FACTORY SOFT TIMEOUT IS 50 MILLISECONDS, SO IT WILL FAIL + // AND THE STALE VALUE WILL BE RETURNED + // THE FACTORY WILL BE KEPT RUNNING IN THE BACKGROUND, AND WHEN + // IT WILL COMPLETE SUCCESSFULLY UPDATE CACHE-A, THE DIST + // CACHE AND NOTIFY THE OTHER NODES + // SUCESSFULLY UPDATE CACHE-A, THE DIST CACHE AND NOTIFY THE OTHER NODES + var fooA2 = cacheA.GetOrSet( + "foo", + _ => + { + Thread.Sleep(simulatedFactoryDuration); + return 30; + }, + duration2 + ); + + // IMMEDIATELY GET OR SET FROM CACHE-B: THE VALUE THERE IS + // EXPIRED, SO THE NEW VALUE WILL BE SAVED AND RETURNED + var fooB2 = cacheB.GetOrSet( + "foo", + 40, + duration2 + ); + + Assert.Equal(10, fooA2); + Assert.Equal(40, fooB2); + + // WAIT FOR THE SIMULATED FACTORY TO COMPLETE: A NOTIFICATION + // WILL BE SENT TO THE OTHER NODES, WHICH IN TURN WILL UPDATE + // THEIR CACHE ENTRIES + Thread.Sleep(simulatedFactoryDuration.PlusALittleBit()); + + // GET THE UPDATED VALUES FROM CACHE-A AND CACHE-B + var fooA3 = cacheA.GetOrDefault("foo"); + var fooB3 = cacheB.GetOrDefault("foo"); + + Assert.Equal(30, fooA3); + Assert.Equal(30, fooB3); + } } } diff --git a/tests/ZiggyCreatures.FusionCache.Tests/DependencyInjectionTests.cs b/tests/ZiggyCreatures.FusionCache.Tests/DependencyInjectionTests.cs index 3f97b2bb..7e007477 100644 --- a/tests/ZiggyCreatures.FusionCache.Tests/DependencyInjectionTests.cs +++ b/tests/ZiggyCreatures.FusionCache.Tests/DependencyInjectionTests.cs @@ -499,44 +499,61 @@ public void DontThrowWhenRequestingAnUnregisteredCache() } [Fact] - public void ThrowsWhenRequestingAnUnregisteredCache() + public void DefaultCacheIsTheSameWhenRequestedInDifferentWays() { var services = new ServiceCollection(); - services.AddFusionCache("FooCache"); + services.AddFusionCache(); services.AddFusionCache(); using var serviceProvider = services.BuildServiceProvider(); var cacheProvider = serviceProvider.GetService()!; - Assert.Throws(() => - { - cacheProvider.GetCache("BarCache"); - }); + Assert.Equal(cacheProvider.GetDefaultCache(), serviceProvider.GetService()); } [Fact] - public void DefaultCacheIsTheSameWhenRequestedInDifferentWays() + public void ThrowsOrNotWhenRequestingUnregisteredNamedCaches() { var services = new ServiceCollection(); - services.AddFusionCache(); - services.AddFusionCache(); + services.AddFusionCache("Foo"); + services.AddFusionCache("Foo"); using var serviceProvider = services.BuildServiceProvider(); var cacheProvider = serviceProvider.GetService()!; - Assert.Equal(cacheProvider.GetDefaultCache(), serviceProvider.GetService()); + Assert.Throws(() => + { + // MULTIPLE Foo CACHES REGISTERED -> THROWS + _ = cacheProvider.GetCache("Foo"); + }); + + Assert.Throws(() => + { + // MULTIPLE Foo CACHES REGISTERED -> THROWS + _ = cacheProvider.GetCacheOrNull("Foo"); + }); + + Assert.Throws(() => + { + // NO Bar CACHE REGISTERED -> THROWS + _ = cacheProvider.GetCache("Bar"); + }); + + // NO Bar CACHE REGISTERED -> RETURNS NULL + var maybeBarCache = cacheProvider.GetCacheOrNull("Bar"); + + Assert.Null(maybeBarCache); } [Fact] - public void ThrowsWhenRequestingANamedCacheRegisteredMultipleTimes() + public void ThrowsOrNotWhenRequestingUnregisteredDefaultCache() { var services = new ServiceCollection(); - services.AddFusionCache("Foo"); services.AddFusionCache("Foo"); using var serviceProvider = services.BuildServiceProvider(); @@ -545,8 +562,41 @@ public void ThrowsWhenRequestingANamedCacheRegisteredMultipleTimes() Assert.Throws(() => { + // NO DEFAULT CACHE REGISTERED -> THROWS _ = cacheProvider.GetDefaultCache(); }); + + // NO DEFAULT CACHE REGISTERED -> RETURNS NULL + var maybeDefaultCache = cacheProvider.GetDefaultCacheOrNull(); + + Assert.Null(maybeDefaultCache); + } + + [Fact] + public void CacheInstancesAreAlwaysTheSame() + { + var services = new ServiceCollection(); + + services.AddFusionCache(); + services.AddFusionCache("Foo"); + services.AddFusionCache("Bar"); + + using var serviceProvider = services.BuildServiceProvider(); + + var cacheProvider = serviceProvider.GetService()!; + + var defaultCache1 = cacheProvider.GetDefaultCache(); + var defaultCache2 = cacheProvider.GetDefaultCache(); + + var fooCache1 = cacheProvider.GetCache("Foo"); + var fooCache2 = cacheProvider.GetCache("Foo"); + + var barCache1 = cacheProvider.GetCache("Bar"); + var barCache2 = cacheProvider.GetCache("Bar"); + + Assert.Same(defaultCache1, defaultCache2); + Assert.Same(fooCache1, fooCache2); + Assert.Same(barCache1, barCache2); } [Fact] @@ -759,131 +809,130 @@ public void BuilderWithSpecificComponentsWorks() Assert.Equal("CONN_DEFAULT", defaultBackplaneOptions.Configuration); } - [Fact] - public void ExistingAndObsoleteCallsStillWork() - { - static ServiceCollection CreateServiceCollection() - { - var services = new ServiceCollection(); - - // REGISTER SOME FUSIONCACHE-RELATED COMPONENTS, TO SEE IFTHEY ARE PICKED UP - services.AddStackExchangeRedisCache(opt => opt.Configuration = "CONN_FOO"); - services.AddFusionCacheSystemTextJsonSerializer(); - - return services; - } - - ServiceCollection services; - - // // 01: BASIC - // // - // // NOTE: VALID ONLY BEFORE V0.20.0 - // var services = CreateServiceCollection(); - - //#pragma warning disable CS0618 // Type or member is obsolete - // services.AddFusionCache(); - //#pragma warning restore CS0618 // Type or member is obsolete - - // using (var serviceProvider = services.BuildServiceProvider()) - // { - // var cache = serviceProvider.GetRequiredService(); - - - // Assert.True(cache.HasDistributedCache); - // } - - // 02: OPTIONS - services = CreateServiceCollection(); - -#pragma warning disable CS0618 // Type or member is obsolete - services.AddFusionCache( - opt => - { - opt.BackplaneAutoRecoveryMaxItems = 123; - } - ); -#pragma warning restore CS0618 // Type or member is obsolete - - using (var serviceProvider = services.BuildServiceProvider()) - { - var cache = serviceProvider.GetRequiredService(); - - Assert.True(cache.HasDistributedCache); - } - - // 03: OPTIONS + FLAG1 - services = CreateServiceCollection(); - -#pragma warning disable CS0618 // Type or member is obsolete - services.AddFusionCache( - opt => - { - opt.BackplaneAutoRecoveryMaxItems = 123; - }, - false - ); -#pragma warning restore CS0618 // Type or member is obsolete - - using (var serviceProvider = services.BuildServiceProvider()) - { - var cache = serviceProvider.GetRequiredService(); - - Assert.False(cache.HasDistributedCache); - } - - // 04: OPTIONS + FLAG1 + FLAG2 - services = CreateServiceCollection(); - -#pragma warning disable CS0618 // Type or member is obsolete - services.AddFusionCache( - opt => - { - opt.BackplaneAutoRecoveryMaxItems = 123; - }, - false, - false - ); -#pragma warning restore CS0618 // Type or member is obsolete - - using (var serviceProvider = services.BuildServiceProvider()) - { - var cache = serviceProvider.GetRequiredService(); - - Assert.False(cache.HasDistributedCache); - } - - // 05: FLAG1 - services = CreateServiceCollection(); - -#pragma warning disable CS0618 // Type or member is obsolete - services.AddFusionCache( - useDistributedCacheIfAvailable: false - ); -#pragma warning restore CS0618 // Type or member is obsolete - - using (var serviceProvider = services.BuildServiceProvider()) - { - var cache = serviceProvider.GetRequiredService(); - - Assert.False(cache.HasDistributedCache); - } - - // 06: FLAG2 - services = CreateServiceCollection(); - -#pragma warning disable CS0618 // Type or member is obsolete - services.AddFusionCache( - ignoreMemoryDistributedCache: false - ); -#pragma warning restore CS0618 // Type or member is obsolete - - using (var serviceProvider = services.BuildServiceProvider()) - { - var cache = serviceProvider.GetRequiredService(); - - Assert.True(cache.HasDistributedCache); - } - } + // [Fact] + // public void ExistingAndObsoleteCallsStillWork() + // { + // static ServiceCollection CreateServiceCollection() + // { + // var services = new ServiceCollection(); + + // // REGISTER SOME FUSIONCACHE-RELATED COMPONENTS, TO SEE IFTHEY ARE PICKED UP + // services.AddStackExchangeRedisCache(opt => opt.Configuration = "CONN_FOO"); + // services.AddFusionCacheSystemTextJsonSerializer(); + + // return services; + // } + + // ServiceCollection services; + + // // // 01: BASIC + // // // + // // // NOTE: VALID ONLY BEFORE V0.20.0 + // // var services = CreateServiceCollection(); + + // //#pragma warning disable CS0618 // Type or member is obsolete + // // services.AddFusionCache(); + // //#pragma warning restore CS0618 // Type or member is obsolete + + // // using (var serviceProvider = services.BuildServiceProvider()) + // // { + // // var cache = serviceProvider.GetRequiredService(); + + // // Assert.True(cache.HasDistributedCache); + // // } + + // // 02: OPTIONS + // services = CreateServiceCollection(); + + //#pragma warning disable CS0618 // Type or member is obsolete + // services.AddFusionCache( + // opt => + // { + // opt.BackplaneAutoRecoveryMaxItems = 123; + // } + // ); + //#pragma warning restore CS0618 // Type or member is obsolete + + // using (var serviceProvider = services.BuildServiceProvider()) + // { + // var cache = serviceProvider.GetRequiredService(); + + // Assert.True(cache.HasDistributedCache); + // } + + // // 03: OPTIONS + FLAG1 + // services = CreateServiceCollection(); + + //#pragma warning disable CS0618 // Type or member is obsolete + // services.AddFusionCache( + // opt => + // { + // opt.BackplaneAutoRecoveryMaxItems = 123; + // }, + // false + // ); + //#pragma warning restore CS0618 // Type or member is obsolete + + // using (var serviceProvider = services.BuildServiceProvider()) + // { + // var cache = serviceProvider.GetRequiredService(); + + // Assert.False(cache.HasDistributedCache); + // } + + // // 04: OPTIONS + FLAG1 + FLAG2 + // services = CreateServiceCollection(); + + //#pragma warning disable CS0618 // Type or member is obsolete + // services.AddFusionCache( + // opt => + // { + // opt.BackplaneAutoRecoveryMaxItems = 123; + // }, + // false, + // false + // ); + //#pragma warning restore CS0618 // Type or member is obsolete + + // using (var serviceProvider = services.BuildServiceProvider()) + // { + // var cache = serviceProvider.GetRequiredService(); + + // Assert.False(cache.HasDistributedCache); + // } + + // // 05: FLAG1 + // services = CreateServiceCollection(); + + //#pragma warning disable CS0618 // Type or member is obsolete + // services.AddFusionCache( + // useDistributedCacheIfAvailable: false + // ); + //#pragma warning restore CS0618 // Type or member is obsolete + + // using (var serviceProvider = services.BuildServiceProvider()) + // { + // var cache = serviceProvider.GetRequiredService(); + + // Assert.False(cache.HasDistributedCache); + // } + + // // 06: FLAG2 + // services = CreateServiceCollection(); + + //#pragma warning disable CS0618 // Type or member is obsolete + // services.AddFusionCache( + // ignoreMemoryDistributedCache: false + // ); + //#pragma warning restore CS0618 // Type or member is obsolete + + // using (var serviceProvider = services.BuildServiceProvider()) + // { + // var cache = serviceProvider.GetRequiredService(); + + // Assert.True(cache.HasDistributedCache); + // } + // } [Fact] public void CanDoWithoutLogger() diff --git a/tests/ZiggyCreatures.FusionCache.Tests/LogLevelsTests.cs b/tests/ZiggyCreatures.FusionCache.Tests/LoggingTests.cs similarity index 60% rename from tests/ZiggyCreatures.FusionCache.Tests/LogLevelsTests.cs rename to tests/ZiggyCreatures.FusionCache.Tests/LoggingTests.cs index ea8b5db3..2d6c35ba 100644 --- a/tests/ZiggyCreatures.FusionCache.Tests/LogLevelsTests.cs +++ b/tests/ZiggyCreatures.FusionCache.Tests/LoggingTests.cs @@ -2,13 +2,18 @@ using System.Linq; using System.Threading.Tasks; using FusionCacheTests.Stuff; +using Microsoft.Extensions.Caching.Distributed; +using Microsoft.Extensions.Caching.Memory; using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; using Xunit; using ZiggyCreatures.Caching.Fusion; +using ZiggyCreatures.Caching.Fusion.Backplane.Memory; +using ZiggyCreatures.Caching.Fusion.Serialization.NewtonsoftJson; namespace FusionCacheTests { - public class LogLevelsTests + public class LoggingTests { private ListLogger CreateListLogger(LogLevel minLogLevel) { @@ -30,8 +35,9 @@ public async Task CommonLogLevelsWork() cache.GetOrSet("qux", _ => throw new Exception("Sloths!"), 123, opt => opt.SetFailSafe(true)); } - Assert.Equal(20, logger.Items.Count); - Assert.Single(logger.Items.Where(x => x.LogLevel == LogLevel.Warning)); + Assert.Equal(22, logger.Items.Count); + Assert.Equal(2, logger.Items.Count(x => x.LogLevel == LogLevel.Warning)); + Assert.Equal(2, logger.Items.Count(x => x.LogLevel == LogLevel.Information)); } [Fact] @@ -90,5 +96,44 @@ public async Task EventsErrorsLogLevelsWork() Assert.Empty(logger.Items); } + + [Fact] + public async Task CacheNameIsAlwaysThere() + { + var cacheName = Guid.NewGuid().ToString("N"); + var logger = CreateListLogger(LogLevel.Trace); + var options = new FusionCacheOptions + { + CacheName = cacheName, + EnableSyncEventHandlersExecution = true + }; + using (var cache = new FusionCache(options, logger: logger)) + { + // PLUGINS + cache.AddPlugin(new NullPlugin()); + + // DISTRIBUTED CACHE + cache.SetupDistributedCache( + new MemoryDistributedCache(Options.Create(new MemoryDistributedCacheOptions())), + new FusionCacheNewtonsoftJsonSerializer() + ); + + // BACKPLANE + cache.SetupBackplane( + new MemoryBackplane(new MemoryBackplaneOptions()) + ); + + // BASIC OPERATIONS + cache.Set("foo", 123); + var foo = cache.GetOrDefault("foo"); + var maybeFoo = cache.TryGet("foo"); + cache.Remove("foo"); + } + + var itemsCountWithoutCacheName = logger.Items.Count(x => x.Message.Contains(cacheName) == false); + + Assert.True(logger.Items.Count > 0); + Assert.Equal(0, itemsCountWithoutCacheName); + } } } diff --git a/tests/ZiggyCreatures.FusionCache.Tests/MultiLevelTests.cs b/tests/ZiggyCreatures.FusionCache.Tests/MultiLevelTests.cs index ea923016..b0e64367 100644 --- a/tests/ZiggyCreatures.FusionCache.Tests/MultiLevelTests.cs +++ b/tests/ZiggyCreatures.FusionCache.Tests/MultiLevelTests.cs @@ -1011,5 +1011,77 @@ public void CanHandleEagerRefresh(SerializerType serializerType) Assert.True(v5 > v4); Assert.Equal(v5, v6); } + + [Theory] + [ClassData(typeof(SerializerTypesClassData))] + public async Task CanSkipMemoryCacheAsync(SerializerType serializerType) + { + var distributedCache = new MemoryDistributedCache(Options.Create(new MemoryDistributedCacheOptions())); + using var cache1 = new FusionCache(new FusionCacheOptions()).SetupDistributedCache(distributedCache, TestsUtils.GetSerializer(serializerType)); + using var cache2 = new FusionCache(new FusionCacheOptions()).SetupDistributedCache(distributedCache, TestsUtils.GetSerializer(serializerType)); + + cache1.DefaultEntryOptions.Duration = TimeSpan.FromMinutes(10); + cache2.DefaultEntryOptions.Duration = TimeSpan.FromMinutes(10); + + // SET ON CACHE 1 AND ON DISTRIBUTED CACHE + var v1 = await cache1.GetOrSetAsync("foo", async _ => 10); + + // GET FROM DISTRIBUTED CACHE AND SET IT ON CACHE 2 + var v2 = await cache2.GetOrSetAsync("foo", async _ => 20); + + // SET ON DISTRIBUTED CACHE BUT SKIP CACHE 1 + await cache1.SetAsync("foo", 30, opt => opt.SetSkipMemoryCache()); + + // GET FROM CACHE 1 (10) AND DON'T CALL THE FACTORY + var v3 = await cache1.GetOrSetAsync("foo", async _ => 40); + + // GET FROM CACHE 2 (10) AND DON'T CALL THE FACTORY + var v4 = await cache2.GetOrSetAsync("foo", async _ => 50); + + // SKIP CACHE 2, GET FROM DISTRIBUTED CACHE (30) + var v5 = await cache2.GetOrSetAsync("foo", async _ => 60, opt => opt.SetSkipMemoryCache()); + + Assert.Equal(10, v1); + Assert.Equal(10, v2); + Assert.Equal(10, v3); + Assert.Equal(10, v4); + Assert.Equal(30, v5); + } + + [Theory] + [ClassData(typeof(SerializerTypesClassData))] + public void CanSkipMemoryCache(SerializerType serializerType) + { + var distributedCache = new MemoryDistributedCache(Options.Create(new MemoryDistributedCacheOptions())); + using var cache1 = new FusionCache(new FusionCacheOptions()).SetupDistributedCache(distributedCache, TestsUtils.GetSerializer(serializerType)); + using var cache2 = new FusionCache(new FusionCacheOptions()).SetupDistributedCache(distributedCache, TestsUtils.GetSerializer(serializerType)); + + cache1.DefaultEntryOptions.Duration = TimeSpan.FromMinutes(10); + cache2.DefaultEntryOptions.Duration = TimeSpan.FromMinutes(10); + + // SET ON CACHE 1 AND ON DISTRIBUTED CACHE + var v1 = cache1.GetOrSet("foo", _ => 10); + + // GET FROM DISTRIBUTED CACHE AND SET IT ON CACHE 2 + var v2 = cache2.GetOrSet("foo", _ => 20); + + // SET ON DISTRIBUTED CACHE BUT SKIP CACHE 1 + cache1.Set("foo", 30, opt => opt.SetSkipMemoryCache()); + + // GET FROM CACHE 1 (10) AND DON'T CALL THE FACTORY + var v3 = cache1.GetOrSet("foo", _ => 40); + + // GET FROM CACHE 2 (10) AND DON'T CALL THE FACTORY + var v4 = cache2.GetOrSet("foo", _ => 50); + + // SKIP CACHE 2, GET FROM DISTRIBUTED CACHE (30) + var v5 = cache2.GetOrSet("foo", _ => 60, opt => opt.SetSkipMemoryCache()); + + Assert.Equal(10, v1); + Assert.Equal(10, v2); + Assert.Equal(10, v3); + Assert.Equal(10, v4); + Assert.Equal(30, v5); + } } } diff --git a/tests/ZiggyCreatures.FusionCache.Tests/Samples/fusioncachecysharpmemorypackserializer__v0_20_0_0.bin b/tests/ZiggyCreatures.FusionCache.Tests/Samples/fusioncachecysharpmemorypackserializer__v0_20_0_0.bin new file mode 100644 index 0000000000000000000000000000000000000000..a0a1029973dd064653dc5ec5003e076095ce426e GIT binary patch literal 43 ucmZR2|NsAg0R{$!;GF!DjADhvqEv>`fl2Uvnp3#$ h8OIl(3EV(2-%8hz#PqPloYGVbrF};>??`{f0RVTZ9De`+ literal 0 HcmV?d00001 diff --git a/tests/ZiggyCreatures.FusionCache.Tests/Samples/fusioncacheneueccmessagepackserializer__v0_20_0_0.bin b/tests/ZiggyCreatures.FusionCache.Tests/Samples/fusioncacheneueccmessagepackserializer__v0_20_0_0.bin new file mode 100644 index 0000000000000000000000000000000000000000..17f5c9cc53cec81c67386898d0669f02f370b901 GIT binary patch literal 40 wcmZo#ShgWJC%+`4SRt_}RUtV)KSyz4ZqvflN!R~ppZGbUyfXhL!@{(~0DJ!v$p8QV literal 0 HcmV?d00001 diff --git a/tests/ZiggyCreatures.FusionCache.Tests/Samples/fusioncacheneueccmessagepackserializer__v0_21_0_0.bin b/tests/ZiggyCreatures.FusionCache.Tests/Samples/fusioncacheneueccmessagepackserializer__v0_21_0_0.bin new file mode 100644 index 0000000000000000000000000000000000000000..a115c8f19afc2b231a364d756bb0fd83eaafa596 GIT binary patch literal 85 zcmZo#ShgWJC%+`4SRt_}RUtV)KSyz4ZtKF-N!R~ppZGbUyfXhL!@{(~i&7I|A~_B> f85X6MtoE&R4M|K7OUx-vU6hjxl`Z=^^CklTANMR8 literal 0 HcmV?d00001 diff --git a/tests/ZiggyCreatures.FusionCache.Tests/Samples/fusioncachenewtonsoftjsonserializer__v0_20_0_0.bin b/tests/ZiggyCreatures.FusionCache.Tests/Samples/fusioncachenewtonsoftjsonserializer__v0_20_0_0.bin new file mode 100644 index 00000000..26d7b70a --- /dev/null +++ b/tests/ZiggyCreatures.FusionCache.Tests/Samples/fusioncachenewtonsoftjsonserializer__v0_20_0_0.bin @@ -0,0 +1 @@ +{"v":"Sloths are cool!","m":{"e":"2033-07-08T14:53:13.4520833+00:00","f":true}} \ No newline at end of file diff --git a/tests/ZiggyCreatures.FusionCache.Tests/Samples/fusioncachenewtonsoftjsonserializer__v0_21_0_0.bin b/tests/ZiggyCreatures.FusionCache.Tests/Samples/fusioncachenewtonsoftjsonserializer__v0_21_0_0.bin new file mode 100644 index 00000000..a5003a08 --- /dev/null +++ b/tests/ZiggyCreatures.FusionCache.Tests/Samples/fusioncachenewtonsoftjsonserializer__v0_21_0_0.bin @@ -0,0 +1 @@ +{"v":"Sloths are cool!","m":{"e":"2033-07-08T14:53:13.4520833+00:00","f":true,"ea":"2033-06-28T14:53:13.4520833+00:00","et":"MyETagValue","lm":"2033-03-30T14:53:13.4520833+00:00"}} \ No newline at end of file diff --git a/tests/ZiggyCreatures.FusionCache.Tests/Samples/fusioncacheprotobufnetserializer__v0_20_0_0.bin b/tests/ZiggyCreatures.FusionCache.Tests/Samples/fusioncacheprotobufnetserializer__v0_20_0_0.bin new file mode 100644 index 00000000..674bde43 --- /dev/null +++ b/tests/ZiggyCreatures.FusionCache.Tests/Samples/fusioncacheprotobufnetserializer__v0_20_0_0.bin @@ -0,0 +1,2 @@ + +Sloths are cool! œÎ¨ÔÉ­ó \ No newline at end of file diff --git a/tests/ZiggyCreatures.FusionCache.Tests/Samples/fusioncacheprotobufnetserializer__v0_21_0_0.bin b/tests/ZiggyCreatures.FusionCache.Tests/Samples/fusioncacheprotobufnetserializer__v0_21_0_0.bin new file mode 100644 index 00000000..49afabe3 --- /dev/null +++ b/tests/ZiggyCreatures.FusionCache.Tests/Samples/fusioncacheprotobufnetserializer__v0_21_0_0.bin @@ -0,0 +1,2 @@ + +Sloths are cool!-œÎ¨Ôɭ󜒞‹÷™ó" MyETagValue(œÈç™Î«ó \ No newline at end of file diff --git a/tests/ZiggyCreatures.FusionCache.Tests/Samples/fusioncacheservicestackjsonserializer__v0_20_0_0.bin b/tests/ZiggyCreatures.FusionCache.Tests/Samples/fusioncacheservicestackjsonserializer__v0_20_0_0.bin new file mode 100644 index 00000000..77ae829f --- /dev/null +++ b/tests/ZiggyCreatures.FusionCache.Tests/Samples/fusioncacheservicestackjsonserializer__v0_20_0_0.bin @@ -0,0 +1 @@ +{"v":"Sloths are cool!","m":{"e":"\/Date(2004447193452)\/","f":true}} \ No newline at end of file diff --git a/tests/ZiggyCreatures.FusionCache.Tests/Samples/fusioncacheservicestackjsonserializer__v0_21_0_0.bin b/tests/ZiggyCreatures.FusionCache.Tests/Samples/fusioncacheservicestackjsonserializer__v0_21_0_0.bin new file mode 100644 index 00000000..a5003a08 --- /dev/null +++ b/tests/ZiggyCreatures.FusionCache.Tests/Samples/fusioncacheservicestackjsonserializer__v0_21_0_0.bin @@ -0,0 +1 @@ +{"v":"Sloths are cool!","m":{"e":"2033-07-08T14:53:13.4520833+00:00","f":true,"ea":"2033-06-28T14:53:13.4520833+00:00","et":"MyETagValue","lm":"2033-03-30T14:53:13.4520833+00:00"}} \ No newline at end of file diff --git a/tests/ZiggyCreatures.FusionCache.Tests/Samples/fusioncachesystemtextjsonserializer__v0_20_0_0.bin b/tests/ZiggyCreatures.FusionCache.Tests/Samples/fusioncachesystemtextjsonserializer__v0_20_0_0.bin new file mode 100644 index 00000000..8e550419 --- /dev/null +++ b/tests/ZiggyCreatures.FusionCache.Tests/Samples/fusioncachesystemtextjsonserializer__v0_20_0_0.bin @@ -0,0 +1 @@ +{"Value":"Sloths are cool!","Metadata":{"LogicalExpiration":"2033-07-08T14:53:13.4520833+00:00","IsFromFailSafe":true}} \ No newline at end of file diff --git a/tests/ZiggyCreatures.FusionCache.Tests/Samples/fusioncachesystemtextjsonserializer__v0_21_0_0.bin b/tests/ZiggyCreatures.FusionCache.Tests/Samples/fusioncachesystemtextjsonserializer__v0_21_0_0.bin new file mode 100644 index 00000000..fe23a750 --- /dev/null +++ b/tests/ZiggyCreatures.FusionCache.Tests/Samples/fusioncachesystemtextjsonserializer__v0_21_0_0.bin @@ -0,0 +1 @@ +{"Value":"Sloths are cool!","Metadata":{"LogicalExpiration":"2033-07-08T14:53:13.4520833+00:00","IsFromFailSafe":true,"EagerExpiration":"2033-06-28T14:53:13.4520833+00:00","ETag":"MyETagValue","LastModified":"2033-03-30T14:53:13.4520833+00:00"}} \ No newline at end of file diff --git a/tests/ZiggyCreatures.FusionCache.Tests/SerializationTests.cs b/tests/ZiggyCreatures.FusionCache.Tests/SerializationTests.cs index 51e48830..81aeb29d 100644 --- a/tests/ZiggyCreatures.FusionCache.Tests/SerializationTests.cs +++ b/tests/ZiggyCreatures.FusionCache.Tests/SerializationTests.cs @@ -1,4 +1,6 @@ using System; +using System.IO; +using System.Text.RegularExpressions; using System.Threading.Tasks; using Xunit; using ZiggyCreatures.Caching.Fusion.Internals; @@ -9,6 +11,8 @@ namespace FusionCacheTests { public class SerializationTests { + private static readonly Regex __re_VersionExtractor = new Regex(@"\w+__v(\d+_\d+_\d+)_\d+\.bin", RegexOptions.Compiled); + private const string SampleString = "Supercalifragilisticexpialidocious"; private static T? LoopDeLoop(IFusionCacheSerializer serializer, T? obj) @@ -118,6 +122,7 @@ public async Task LoopSucceedsWithDistributedEntryAndSimpleTypesAsync(Serializer var looped = await serializer.DeserializeAsync>(data); Assert.NotNull(looped); Assert.Equal(obj.Value, looped.Value); + Assert.Equal(obj.Timestamp, looped.Timestamp); Assert.Equal(obj.Metadata!.IsFromFailSafe, looped.Metadata!.IsFromFailSafe); Assert.Equal(obj.Metadata!.LogicalExpiration, looped.Metadata!.LogicalExpiration); Assert.Equal(obj.Metadata!.EagerExpiration, looped.Metadata!.EagerExpiration); @@ -140,6 +145,7 @@ public void LoopSucceedsWithDistributedEntryAndSimpleTypes(SerializerType serial var looped = serializer.Deserialize>(data); Assert.NotNull(looped); Assert.Equal(obj.Value, looped.Value); + Assert.Equal(obj.Timestamp, looped.Timestamp); Assert.Equal(obj.Metadata!.IsFromFailSafe, looped.Metadata!.IsFromFailSafe); Assert.Equal(obj.Metadata!.LogicalExpiration, looped.Metadata!.LogicalExpiration); Assert.Equal(obj.Metadata!.EagerExpiration, looped.Metadata!.EagerExpiration); @@ -162,6 +168,7 @@ public async Task LoopSucceedsWithDistributedEntryAndNoMetadataAsync(SerializerT var looped = await serializer.DeserializeAsync>(data); Assert.NotNull(looped); Assert.Equal(obj.Value, looped.Value); + Assert.Equal(obj.Timestamp, looped.Timestamp); Assert.Null(looped!.Metadata); } @@ -180,6 +187,7 @@ public void LoopSucceedsWithDistributedEntryAndNoMetadata(SerializerType seriali var looped = serializer.Deserialize>(data); Assert.NotNull(looped); Assert.Equal(obj.Value, looped.Value); + Assert.Equal(obj.Timestamp, looped.Timestamp); Assert.Null(looped!.Metadata); } @@ -198,6 +206,7 @@ public async Task LoopSucceedsWithDistributedEntryAndComplexTypesAsync(Serialize var looped = await serializer.DeserializeAsync>(data); Assert.NotNull(looped); Assert.Equal(obj.Value, looped.Value); + Assert.Equal(obj.Timestamp, looped.Timestamp); Assert.Equal(obj.Metadata!.IsFromFailSafe, looped.Metadata!.IsFromFailSafe); Assert.Equal(obj.Metadata!.LogicalExpiration, looped.Metadata!.LogicalExpiration); Assert.Equal(obj.Metadata!.EagerExpiration, looped.Metadata!.EagerExpiration); @@ -220,11 +229,52 @@ public void LoopSucceedsWithDistributedEntryAndComplexTypes(SerializerType seria var looped = serializer.Deserialize>(data); Assert.NotNull(looped); Assert.Equal(obj.Value, looped.Value); + Assert.Equal(obj.Timestamp, looped.Timestamp); Assert.Equal(obj.Metadata!.IsFromFailSafe, looped.Metadata!.IsFromFailSafe); Assert.Equal(obj.Metadata!.LogicalExpiration, looped.Metadata!.LogicalExpiration); Assert.Equal(obj.Metadata!.EagerExpiration, looped.Metadata!.EagerExpiration); Assert.Equal(obj.Metadata!.ETag, looped.Metadata!.ETag); Assert.Equal(obj.Metadata!.LastModified, looped.Metadata!.LastModified); } + + [Theory] + [ClassData(typeof(SerializerTypesClassData))] + public async Task CanDeserializeOldVersionsAsync(SerializerType serializerType) + { + var serializer = TestsUtils.GetSerializer(serializerType); + + var filePrefix = $"{serializer.GetType().Name}__"; + + var files = Directory.GetFiles("Samples\\", filePrefix + "*.bin"); + + foreach (var file in files) + { + var payloadVersion = __re_VersionExtractor.Match(file).Groups[1]?.Value?.Replace('_', '.'); + + var payload = File.ReadAllBytes(file); + var deserialized = await serializer.DeserializeAsync>(payload); + Assert.False(deserialized is null, $"Failed deserializing payload from v{payloadVersion}"); + } + } + + [Theory] + [ClassData(typeof(SerializerTypesClassData))] + public void CanDeserializeOldVersions(SerializerType serializerType) + { + var serializer = TestsUtils.GetSerializer(serializerType); + + var filePrefix = $"{serializer.GetType().Name}__"; + + var files = Directory.GetFiles("Samples\\", filePrefix + "*.bin"); + + foreach (var file in files) + { + var payloadVersion = __re_VersionExtractor.Match(file).Groups[1]?.Value?.Replace('_', '.'); + + var payload = File.ReadAllBytes(file); + var deserialized = serializer.Deserialize>(payload); + Assert.False(deserialized is null, $"Failed deserializing payload from v{payloadVersion}"); + } + } } } diff --git a/tests/ZiggyCreatures.FusionCache.Tests/SingleLevelTests.cs b/tests/ZiggyCreatures.FusionCache.Tests/SingleLevelTests.cs index cf17f995..ae3702cd 100644 --- a/tests/ZiggyCreatures.FusionCache.Tests/SingleLevelTests.cs +++ b/tests/ZiggyCreatures.FusionCache.Tests/SingleLevelTests.cs @@ -5,6 +5,7 @@ using FusionCacheTests.Stuff; using Microsoft.Extensions.Caching.Distributed; using Microsoft.Extensions.Caching.Memory; +using Newtonsoft.Json; using Xunit; using ZiggyCreatures.Caching.Fusion; @@ -35,6 +36,30 @@ public void CannotAssignNullToDefaultEntryOptions() }); } + [Fact] + public async Task CanRemoveAsync() + { + using var cache = new FusionCache(new FusionCacheOptions()); + await cache.SetAsync("foo", 42); + var foo1 = await cache.GetOrDefaultAsync("foo"); + await cache.RemoveAsync("foo"); + var foo2 = await cache.GetOrDefaultAsync("foo"); + Assert.Equal(42, foo1); + Assert.Equal(0, foo2); + } + + [Fact] + public void CanRemove() + { + using var cache = new FusionCache(new FusionCacheOptions()); + cache.Set("foo", 42); + var foo1 = cache.GetOrDefault("foo"); + cache.Remove("foo"); + var foo2 = cache.GetOrDefault("foo"); + Assert.Equal(42, foo1); + Assert.Equal(0, foo2); + } + [Fact] public async Task ReturnsStaleDataWhenFactoryFailsWithFailSafeAsync() { @@ -735,6 +760,70 @@ public void AdaptiveCachingDoesNotChangeOptions() Assert.Equal(options.Duration, TimeSpan.FromSeconds(10)); } + [Fact] + public async Task AdaptiveCachingCanWorkWithSkipMemoryCacheAsync() + { + using var cache = new FusionCache(new FusionCacheOptions()); + cache.DefaultEntryOptions.IsFailSafeEnabled = true; + cache.DefaultEntryOptions.Duration = TimeSpan.FromSeconds(1); + cache.DefaultEntryOptions.FailSafeThrottleDuration = TimeSpan.FromSeconds(3); + + var foo1 = await cache.GetOrSetAsync("foo", async _ => 1); + + await Task.Delay(TimeSpan.FromSeconds(1).PlusALittleBit()); + + var foo2 = await cache.GetOrSetAsync("foo", async (ctx, _) => + { + ctx.Options.SkipMemoryCache = true; + + return 2; + }); + + var foo3 = await cache.TryGetAsync("foo"); + + await Task.Delay(cache.DefaultEntryOptions.FailSafeThrottleDuration.PlusALittleBit()); + + var foo4 = await cache.GetOrSetAsync("foo", async _ => 4); + + Assert.Equal(1, foo1); + Assert.Equal(2, foo2); + Assert.True(foo3.HasValue); + Assert.Equal(1, foo3.Value); + Assert.Equal(4, foo4); + } + + [Fact] + public void AdaptiveCachingCanWorkWithSkipMemoryCache() + { + using var cache = new FusionCache(new FusionCacheOptions()); + cache.DefaultEntryOptions.IsFailSafeEnabled = true; + cache.DefaultEntryOptions.Duration = TimeSpan.FromSeconds(1); + cache.DefaultEntryOptions.FailSafeThrottleDuration = TimeSpan.FromSeconds(3); + + var foo1 = cache.GetOrSet("foo", _ => 1); + + Thread.Sleep(TimeSpan.FromSeconds(1).PlusALittleBit()); + + var foo2 = cache.GetOrSet("foo", (ctx, _) => + { + ctx.Options.SkipMemoryCache = true; + + return 2; + }); + + var foo3 = cache.TryGet("foo"); + + Thread.Sleep(cache.DefaultEntryOptions.FailSafeThrottleDuration.PlusALittleBit()); + + var foo4 = cache.GetOrSet("foo", _ => 4); + + Assert.Equal(1, foo1); + Assert.Equal(2, foo2); + Assert.True(foo3.HasValue); + Assert.Equal(1, foo3.Value); + Assert.Equal(4, foo4); + } + [Fact] public async Task FailSafeMaxDurationNormalizationOccursAsync() { @@ -815,6 +904,82 @@ public void CanHandleInfiniteOrSimilarDurations() Assert.Equal(42, foo); } + [Fact] + public async Task CanHandleZeroDurationsAsync() + { + using var cache = new FusionCache(new FusionCacheOptions()); + + await cache.SetAsync("foo", 10, opt => opt.SetDuration(TimeSpan.Zero)); + var foo1 = await cache.GetOrDefaultAsync("foo", 1); + + await cache.SetAsync("foo", 20, opt => opt.SetDuration(TimeSpan.FromMinutes(10))); + var foo2 = await cache.GetOrDefaultAsync("foo", 2); + + await cache.SetAsync("foo", 30, opt => opt.SetDuration(TimeSpan.Zero)); + var foo3 = await cache.GetOrDefaultAsync("foo", 3); + + Assert.Equal(1, foo1); + Assert.Equal(20, foo2); + Assert.Equal(3, foo3); + } + + [Fact] + public void CanHandleZeroDurations() + { + using var cache = new FusionCache(new FusionCacheOptions()); + + cache.Set("foo", 10, opt => opt.SetDuration(TimeSpan.Zero)); + var foo1 = cache.GetOrDefault("foo", 1); + + cache.Set("foo", 20, opt => opt.SetDuration(TimeSpan.FromMinutes(10))); + var foo2 = cache.GetOrDefault("foo", 2); + + cache.Set("foo", 30, opt => opt.SetDuration(TimeSpan.Zero)); + var foo3 = cache.GetOrDefault("foo", 3); + + Assert.Equal(1, foo1); + Assert.Equal(20, foo2); + Assert.Equal(3, foo3); + } + + [Fact] + public async Task CanHandleNegativeDurationsAsync() + { + using var cache = new FusionCache(new FusionCacheOptions()); + + await cache.SetAsync("foo", 10, opt => opt.SetDuration(TimeSpan.FromSeconds(-100))); + var foo1 = await cache.GetOrDefaultAsync("foo", 1); + + await cache.SetAsync("foo", 20, opt => opt.SetDuration(TimeSpan.FromMinutes(10))); + var foo2 = await cache.GetOrDefaultAsync("foo", 2); + + await cache.SetAsync("foo", 30, opt => opt.SetDuration(TimeSpan.FromDays(-100))); + var foo3 = await cache.GetOrDefaultAsync("foo", 3); + + Assert.Equal(1, foo1); + Assert.Equal(20, foo2); + Assert.Equal(3, foo3); + } + + [Fact] + public void CanHandleNegativeDurations() + { + using var cache = new FusionCache(new FusionCacheOptions()); + + cache.Set("foo", 10, opt => opt.SetDuration(TimeSpan.FromSeconds(-100))); + var foo1 = cache.GetOrDefault("foo", 1); + + cache.Set("foo", 20, opt => opt.SetDuration(TimeSpan.FromMinutes(10))); + var foo2 = cache.GetOrDefault("foo", 2); + + cache.Set("foo", 30, opt => opt.SetDuration(TimeSpan.FromDays(-100))); + var foo3 = cache.GetOrDefault("foo", 3); + + Assert.Equal(1, foo1); + Assert.Equal(20, foo2); + Assert.Equal(3, foo3); + } + [Fact] public async Task CanHandleConditionalRefreshAsync() { @@ -1220,5 +1385,250 @@ public void NormalFactoryExecutionWaitsForInFlightEagerRefresh() Assert.Equal(2, v5); Assert.Equal(2, value); } + + [Fact] + public async Task CanExpireAsync() + { + using var cache = new FusionCache(new FusionCacheOptions()); + cache.DefaultEntryOptions.IsFailSafeEnabled = true; + cache.DefaultEntryOptions.Duration = TimeSpan.FromMinutes(10); + + await cache.SetAsync("foo", 42); + var maybeFoo1 = await cache.TryGetAsync("foo", opt => opt.SetFailSafe(false)); + await cache.ExpireAsync("foo"); + var maybeFoo2 = await cache.TryGetAsync("foo", opt => opt.SetFailSafe(false)); + var maybeFoo3 = await cache.TryGetAsync("foo", opt => opt.SetFailSafe(true)); + Assert.True(maybeFoo1.HasValue); + Assert.Equal(42, maybeFoo1.Value); + Assert.False(maybeFoo2.HasValue); + Assert.True(maybeFoo3.HasValue); + Assert.Equal(42, maybeFoo3.Value); + } + + [Fact] + public void CanExpire() + { + using var cache = new FusionCache(new FusionCacheOptions()); + cache.DefaultEntryOptions.IsFailSafeEnabled = true; + cache.DefaultEntryOptions.Duration = TimeSpan.FromMinutes(10); + + cache.Set("foo", 42); + var maybeFoo1 = cache.TryGet("foo", opt => opt.SetFailSafe(false)); + cache.Expire("foo"); + var maybeFoo2 = cache.TryGet("foo", opt => opt.SetFailSafe(false)); + var maybeFoo3 = cache.TryGet("foo", opt => opt.SetFailSafe(true)); + Assert.True(maybeFoo1.HasValue); + Assert.Equal(42, maybeFoo1.Value); + Assert.False(maybeFoo2.HasValue); + Assert.True(maybeFoo3.HasValue); + Assert.Equal(42, maybeFoo3.Value); + } + + [Fact] + public async Task CanSkipMemoryCacheAsync() + { + using var cache = new FusionCache(new FusionCacheOptions()); + + await cache.SetAsync("foo", 42, opt => opt.SetSkipMemoryCache()); + var maybeFoo1 = await cache.TryGetAsync("foo"); + await cache.SetAsync("foo", 42); + var maybeFoo2 = await cache.TryGetAsync("foo", opt => opt.SetSkipMemoryCache()); + var maybeFoo3 = await cache.TryGetAsync("foo"); + await cache.RemoveAsync("foo", opt => opt.SetSkipMemoryCache()); + var maybeFoo4 = await cache.TryGetAsync("foo"); + await cache.RemoveAsync("foo"); + var maybeFoo5 = await cache.TryGetAsync("foo"); + + await cache.GetOrSetAsync("bar", 42, opt => opt.SetSkipMemoryCache()); + var maybeBar = await cache.TryGetAsync("bar"); + + Assert.False(maybeFoo1.HasValue); + Assert.False(maybeFoo2.HasValue); + Assert.True(maybeFoo3.HasValue); + Assert.True(maybeFoo4.HasValue); + Assert.False(maybeFoo5.HasValue); + + Assert.False(maybeBar.HasValue); + } + + [Fact] + public void CanSkipMemoryCache() + { + using var cache = new FusionCache(new FusionCacheOptions()); + + cache.Set("foo", 42, opt => opt.SetSkipMemoryCache()); + var maybeFoo1 = cache.TryGet("foo"); + cache.Set("foo", 42); + var maybeFoo2 = cache.TryGet("foo", opt => opt.SetSkipMemoryCache()); + var maybeFoo3 = cache.TryGet("foo"); + cache.Remove("foo", opt => opt.SetSkipMemoryCache()); + var maybeFoo4 = cache.TryGet("foo"); + cache.Remove("foo"); + var maybeFoo5 = cache.TryGet("foo"); + + cache.GetOrSet("bar", 42, opt => opt.SetSkipMemoryCache()); + var maybeBar = cache.TryGet("bar"); + + Assert.False(maybeFoo1.HasValue); + Assert.False(maybeFoo2.HasValue); + Assert.True(maybeFoo3.HasValue); + Assert.True(maybeFoo4.HasValue); + Assert.False(maybeFoo5.HasValue); + + Assert.False(maybeBar.HasValue); + } + + [Fact] + public async Task CanUseNullFusionCacheAsync() + { + using var cache = new NullFusionCache(new FusionCacheOptions() + { + CacheName = "SlothsAreCool42", + DefaultEntryOptions = new FusionCacheEntryOptions() + { + IsFailSafeEnabled = true, + Duration = TimeSpan.FromMinutes(123) + } + }); + + await cache.SetAsync("foo", 42); + + var maybeFoo1 = await cache.TryGetAsync("foo"); + + await cache.RemoveAsync("foo"); + + var maybeBar1 = await cache.TryGetAsync("bar"); + + await cache.ExpireAsync("qux"); + + var qux1 = await cache.GetOrSetAsync("qux", async _ => 1); + var qux2 = await cache.GetOrSetAsync("qux", async _ => 2); + var qux3 = await cache.GetOrSetAsync("qux", async _ => 3); + var qux4 = await cache.GetOrDefaultAsync("qux", 4); + + Assert.Equal("SlothsAreCool42", cache.CacheName); + Assert.False(string.IsNullOrWhiteSpace(cache.InstanceId)); + + Assert.False(cache.HasDistributedCache); + Assert.False(cache.HasBackplane); + + Assert.True(cache.DefaultEntryOptions.IsFailSafeEnabled); + Assert.Equal(TimeSpan.FromMinutes(123), cache.DefaultEntryOptions.Duration); + + Assert.False(maybeFoo1.HasValue); + Assert.False(maybeBar1.HasValue); + + Assert.Equal(1, qux1); + Assert.Equal(2, qux2); + Assert.Equal(3, qux3); + Assert.Equal(4, qux4); + + await Assert.ThrowsAsync(async () => + { + _ = await cache.GetOrSetAsync("qux", async _ => throw new UnreachableException("Sloths")); + }); + } + + [Fact] + public void CanUseNullFusionCache() + { + using var cache = new NullFusionCache(new FusionCacheOptions() + { + CacheName = "SlothsAreCool42", + DefaultEntryOptions = new FusionCacheEntryOptions() + { + IsFailSafeEnabled = true, + Duration = TimeSpan.FromMinutes(123) + } + }); + + cache.Set("foo", 42); + + var maybeFoo1 = cache.TryGet("foo"); + + cache.Remove("foo"); + + var maybeBar1 = cache.TryGet("bar"); + + cache.Expire("qux"); + + var qux1 = cache.GetOrSet("qux", _ => 1); + var qux2 = cache.GetOrSet("qux", _ => 2); + var qux3 = cache.GetOrSet("qux", _ => 3); + var qux4 = cache.GetOrDefault("qux", 4); + + Assert.Equal("SlothsAreCool42", cache.CacheName); + Assert.False(string.IsNullOrWhiteSpace(cache.InstanceId)); + + Assert.False(cache.HasDistributedCache); + Assert.False(cache.HasBackplane); + + Assert.True(cache.DefaultEntryOptions.IsFailSafeEnabled); + Assert.Equal(TimeSpan.FromMinutes(123), cache.DefaultEntryOptions.Duration); + + Assert.False(maybeFoo1.HasValue); + Assert.False(maybeBar1.HasValue); + + Assert.Equal(1, qux1); + Assert.Equal(2, qux2); + Assert.Equal(3, qux3); + Assert.Equal(4, qux4); + + Assert.Throws(() => + { + _ = cache.GetOrSet("qux", _ => throw new UnreachableException("Sloths")); + }); + } + + [Fact] + public void DuplicatEntryOptionsWorksCorrectly() + { + var original = new FusionCacheEntryOptions() + { + IsSafeForAdaptiveCaching = true, + + Duration = TimeSpan.FromSeconds(1), + LockTimeout = TimeSpan.FromSeconds(2), + Size = 123, + Priority = CacheItemPriority.High, + JitterMaxDuration = TimeSpan.FromSeconds(3), + + // NOTE: PERF MICRO-OPT + EagerRefreshThreshold = 0.456f, + + IsFailSafeEnabled = !FusionCacheGlobalDefaults.EntryOptionsIsFailSafeEnabled, + FailSafeMaxDuration = TimeSpan.FromSeconds(4), + FailSafeThrottleDuration = TimeSpan.FromSeconds(5), + + FactorySoftTimeout = TimeSpan.FromSeconds(6), + FactoryHardTimeout = TimeSpan.FromSeconds(7), + AllowTimedOutFactoryBackgroundCompletion = !FusionCacheGlobalDefaults.EntryOptionsAllowTimedOutFactoryBackgroundCompletion, + + DistributedCacheDuration = TimeSpan.FromSeconds(8), + DistributedCacheFailSafeMaxDuration = TimeSpan.FromSeconds(9), + DistributedCacheSoftTimeout = TimeSpan.FromSeconds(10), + DistributedCacheHardTimeout = TimeSpan.FromSeconds(11), + + ReThrowDistributedCacheExceptions = !FusionCacheGlobalDefaults.EntryOptionsReThrowDistributedCacheExceptions, + ReThrowSerializationExceptions = !FusionCacheGlobalDefaults.EntryOptionsReThrowSerializationExceptions, + + AllowBackgroundDistributedCacheOperations = !FusionCacheGlobalDefaults.EntryOptionsAllowBackgroundDistributedCacheOperations, + AllowBackgroundBackplaneOperations = !FusionCacheGlobalDefaults.EntryOptionsAllowBackgroundBackplaneOperations, + + SkipBackplaneNotifications = !FusionCacheGlobalDefaults.EntryOptionsSkipBackplaneNotifications, + + SkipDistributedCache = !FusionCacheGlobalDefaults.EntryOptionsSkipDistributedCache, + SkipDistributedCacheReadWhenStale = !FusionCacheGlobalDefaults.EntryOptionsSkipDistributedCacheReadWhenStale, + + SkipMemoryCache = true, + }; + + var duplicated = original.Duplicate(); + + Assert.Equal( + JsonConvert.SerializeObject(original), + JsonConvert.SerializeObject(duplicated) + ); + } } } diff --git a/tests/ZiggyCreatures.FusionCache.Tests/Stuff/ListLogger.cs b/tests/ZiggyCreatures.FusionCache.Tests/Stuff/ListLogger.cs index 26f603f6..e22efc3a 100644 --- a/tests/ZiggyCreatures.FusionCache.Tests/Stuff/ListLogger.cs +++ b/tests/ZiggyCreatures.FusionCache.Tests/Stuff/ListLogger.cs @@ -7,7 +7,6 @@ namespace FusionCacheTests.Stuff internal class ListLogger : ILogger { - internal class Scope : IDisposable { public void Dispose() @@ -17,7 +16,7 @@ public void Dispose() } private readonly LogLevel _minLogLevel; - public readonly List<(LogLevel LogLevel, string message)> Items = new List<(LogLevel LogLevel, string message)>(); + public readonly List<(LogLevel LogLevel, string Message)> Items = new List<(LogLevel LogLevel, string Message)>(); public ListLogger(LogLevel minLogLevel) { diff --git a/tests/ZiggyCreatures.FusionCache.Tests/Stuff/XUnitLogger.cs b/tests/ZiggyCreatures.FusionCache.Tests/Stuff/XUnitLogger.cs new file mode 100644 index 00000000..03e0118a --- /dev/null +++ b/tests/ZiggyCreatures.FusionCache.Tests/Stuff/XUnitLogger.cs @@ -0,0 +1,44 @@ +using System; +using Microsoft.Extensions.Logging; +using Xunit.Abstractions; + +namespace FusionCacheTests.Stuff +{ + internal class XUnitLogger + : ILogger + { + internal class Scope : IDisposable + { + public void Dispose() + { + // EMPTY + } + } + + private readonly ITestOutputHelper _helper; + private readonly LogLevel _minLogLevel; + + public XUnitLogger(LogLevel minLogLevel, ITestOutputHelper helper) + { + _minLogLevel = minLogLevel; + _helper = helper; + } + + public IDisposable BeginScope(TState state) + where TState : notnull + { + return new Scope(); + } + + public bool IsEnabled(LogLevel logLevel) + { + return logLevel >= _minLogLevel; + } + + public void Log(LogLevel logLevel, EventId eventId, TState state, Exception? exception, Func formatter) + { + if (IsEnabled(logLevel)) + _helper.WriteLine($"{DateTime.UtcNow}: " + formatter(state, exception)); + } + } +} diff --git a/tests/ZiggyCreatures.FusionCache.Tests/ZiggyCreatures.FusionCache.Tests.csproj b/tests/ZiggyCreatures.FusionCache.Tests/ZiggyCreatures.FusionCache.Tests.csproj index 38d0324b..cf8d344c 100644 --- a/tests/ZiggyCreatures.FusionCache.Tests/ZiggyCreatures.FusionCache.Tests.csproj +++ b/tests/ZiggyCreatures.FusionCache.Tests/ZiggyCreatures.FusionCache.Tests.csproj @@ -9,18 +9,24 @@ - - + + Always + + + + + + - - + + runtime; build; native; contentfiles; analyzers; buildtransitive all - + runtime; build; native; contentfiles; analyzers; buildtransitive all