diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 9f00509e..9ccb0524 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -11,7 +11,7 @@ jobs: - name: Set up .NET Core uses: actions/setup-dotnet@v1 with: - dotnet-version: '7.0.x' + dotnet-version: '8.0.x' - run: dotnet --info - name: Build solution and run all tests run: ./build.sh \ No newline at end of file diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml index e387b8e0..623cb8fd 100644 --- a/.github/workflows/codeql-analysis.yml +++ b/.github/workflows/codeql-analysis.yml @@ -66,5 +66,5 @@ jobs: - name: Perform CodeQL Analysis uses: github/codeql-action/analyze@v2 - - name: Upload coverage reports to Codecov - uses: codecov/codecov-action@v3 + #- name: Upload coverage reports to Codecov + # uses: codecov/codecov-action@v3 diff --git a/Directory.Build.props b/Directory.Build.props index 08078db7..6e910efb 100644 --- a/Directory.Build.props +++ b/Directory.Build.props @@ -22,7 +22,7 @@ - + all runtime; build; native; contentfiles; analyzers; buildtransitive diff --git a/README.md b/README.md index 64ee844d..cae12a1f 100644 --- a/README.md +++ b/README.md @@ -10,14 +10,13 @@ [![License: MIT](https://img.shields.io/badge/license-MIT-blue.svg)](https://opensource.org/licenses/MIT) ![Nuget](https://img.shields.io/nuget/dt/ZiggyCreatures.FusionCache) -[![Twitter](https://img.shields.io/twitter/url/http/shields.io.svg?style=flat&logo=twitter)](https://twitter.com/intent/tweet?hashtags=fusioncache,caching,cache,dotnet,oss,csharp&text=πŸš€+FusionCache:+a+new+cache+with+an+optional+2nd+layer+and+some+advanced+features&url=https%3A%2F%2Fgithub.com%2FZiggyCreatures%2FFusionCache&via=jodydonetti) | πŸ™‹β€β™‚οΈ Updating from before `v0.24.0` ? please [read here](docs/Update_v0_24_0.md). | |:-------| -### FusionCache is an easy to use, fast and robust cache with advanced resiliency features and an optional distributed 2nd layer. +### FusionCache is an easy to use, fast and robust cache with advanced resiliency features and an optional distributed 2nd level. It was born after years of dealing with all sorts of different types of caches: memory caching, distributed caching, http caching, CDNs, browser cache, offline cache, you name it. So I've tried to put together these experiences and came up with FusionCache. @@ -73,10 +72,11 @@ These are the **key features** of FusionCache: - [**πŸ¦… Eager Refresh**](docs/EagerRefresh.md): start a non-blocking background refresh before the expiration occurs - [**πŸ”ƒ Dependency Injection**](docs/DependencyInjection.md): native support for Dependency Injection, with a nice fluent interface including a Builder support - [**πŸ“› Named Caches**](docs/NamedCaches.md): easily work with multiple named caches, even if differently configured +- [**πŸ”­ OpenTelemetry**](docs/OpenTelemetry.md): native observability support via OpenTelemetry +- [**πŸ“œ Logging**](docs/Logging.md): comprehensive, structured and customizable, via the standard `ILogger` interface - [**πŸ’« Natively sync/async**](docs/CoreMethods.md): native support for both the synchronous and asynchronous programming model - [**πŸ“ž Events**](docs/Events.md): a comprehensive set of events, both at a high level and at lower levels (memory/distributed) - [**🧩 Plugins**](docs/Plugins.md): extend FusionCache with additional behavior like adding support for metrics, statistics, etc... -- [**πŸ“œ Logging**](docs/Logging.md): comprehensive, structured and customizable, via the standard `ILogger` interface
Something more 😏 ? @@ -90,7 +90,6 @@ Also, FusionCache has some nice **additional features**: - **βœ… Null caching**: explicitly supports caching of `null` values differently than "no value". This creates a less ambiguous usage, and typically leads to better performance because it avoids the classic problem of not being able to differentiate between *"the value was not in the cache, go check the database"* and *"the value was in the cache, and it was `null`"* - **βœ… Circuit-breaker**: it is possible to enable a simple circuit-breaker for when the distributed cache or the backplane become temporarily unavailable. This will prevent those components to be hit with an excessive load of requests (that would probably fail anyway) in a problematic moment, so it can gracefully get back on its feet. More advanced scenarios can be covered using a dedicated solution, like Polly - **βœ… Dynamic Jittering**: setting `JitterMaxDuration` will add a small randomized extra duration to a cache entry's normal duration. This is useful to prevent variations of the Cache Stampede problem in a multi-node scenario -- **βœ… Hot Swap**: supports thread-safe changes of the entire distributed cache or backplane implementation (add/swap/removal) - **βœ… Cancellation**: every method supports cancellation via the standard `CancellationToken`, so it is easy to cancel an entire pipeline of operation gracefully - **βœ… Code comments**: every property and method is fully documented in code, with useful informations provided via IntelliSense or similar technologies - **βœ… Fully annotated for [nullability](https://docs.microsoft.com/en-us/dotnet/csharp/nullable-references)**: every usage of nullable references has been annotated for a better flow analysis by the compiler @@ -105,7 +104,8 @@ Main packages: | Package Name | Version | Downloads | |--------------------------------|:---------------:|:---------:| | [ZiggyCreatures.FusionCache](https://www.nuget.org/packages/ZiggyCreatures.FusionCache/)
The core package | [![NuGet](https://img.shields.io/nuget/v/ZiggyCreatures.FusionCache.svg)](https://www.nuget.org/packages/ZiggyCreatures.FusionCache/) | ![Nuget](https://img.shields.io/nuget/dt/ZiggyCreatures.FusionCache) | -| [ZiggyCreatures.FusionCache.Chaos](https://www.nuget.org/packages/ZiggyCreatures.FusionCache.Chaos/)
A package with additional chaos-related utilities and implementations | [![NuGet](https://img.shields.io/nuget/v/ZiggyCreatures.FusionCache.Chaos.svg)](https://www.nuget.org/packages/ZiggyCreatures.FusionCache.Chaos/) | ![Nuget](https://img.shields.io/nuget/dt/ZiggyCreatures.FusionCache.Chaos) | +| [ZiggyCreatures.FusionCache.OpenTelemetry](https://www.nuget.org/packages/ZiggyCreatures.FusionCache.OpenTelemetry/)
Adds native support for OpenTelemetry setup | [![NuGet](https://img.shields.io/nuget/v/ZiggyCreatures.FusionCache.OpenTelemetry.svg)](https://www.nuget.org/packages/ZiggyCreatures.FusionCache.OpenTelemetry/) | ![Nuget](https://img.shields.io/nuget/dt/ZiggyCreatures.FusionCache.OpenTelemetry) | +| [ZiggyCreatures.FusionCache.Chaos](https://www.nuget.org/packages/ZiggyCreatures.FusionCache.Chaos/)
A package to add some controller chaos, for testing | [![NuGet](https://img.shields.io/nuget/v/ZiggyCreatures.FusionCache.Chaos.svg)](https://www.nuget.org/packages/ZiggyCreatures.FusionCache.Chaos/) | ![Nuget](https://img.shields.io/nuget/dt/ZiggyCreatures.FusionCache.Chaos) | Serializers: @@ -273,21 +273,17 @@ When using FusionCache with the [distributed cache](docs/CacheLevels.md), the [b [![FusionCache Simulator](https://img.youtube.com/vi/6jGX6ePgD3Q/maxresdefault.jpg)](docs/Simulator.md) -## πŸ†Ž Comparison - -There are various alternatives out there with different features, different performance characteristics (cpu/memory) and in general a different set of pros/cons. - -A [feature comparison](docs/Comparison.md) between existing .NET caching solutions may help you choose which one to use. - ## 🧰 Supported Platforms FusionCache targets `.NET Standard 2.0` so any compatible .NET implementation is fine: this means `.NET Framework` (the old one), `.NET Core 2+` and `.NET 5/6/7/8+` (the new ones), `Mono` 5.4+ and more (see [here](https://docs.microsoft.com/en-us/dotnet/standard/net-standard#net-implementation-support) for a complete rundown). **NOTE**: if you are running on **.NET Framework 4.6.1** and want to use **.NET Standard** packages Microsoft suggests to upgrade to .NET Framework 4.7.2 or higher (see the [.NET Standard Documentation](https://docs.microsoft.com/en-us/dotnet/standard/net-standard#net-implementation-support)) to avoid some known dependency issues. -## πŸ–Ό Logo +## πŸ†Ž Comparison + +There are various alternatives out there with different features, different performance characteristics (cpu/memory) and in general a different set of pros/cons. -The logo is an [original creation](https://dribbble.com/shots/14854206-FusionCache-logo) and is a [sloth](https://en.wikipedia.org/wiki/Sloth) because, you know, speed. +A [feature comparison](docs/Comparison.md) between existing .NET caching solutions may help you choose which one to use. ## πŸ’° Support @@ -295,15 +291,15 @@ Nothing to do here. After years of using a lot of open source stuff for free, this is just me trying to give something back to the community. -If you find FusionCache useful please just [**βœ‰ drop me a line**](https://twitter.com/jodydonetti), I would be interested in knowing about your usage. +If you find FusionCache useful just [**βœ‰ drop me a line**](https://twitter.com/jodydonetti), I would be interested in knowing how you're using it. -And if you really want to talk about money, please consider making **❀ a donation to a good cause** of your choosing, and maybe let me know about that. +And if you really want to talk about money, please consider making **❀ a donation to a good cause** of your choosing, and let me know about that. ## πŸ’Ό Is it Production Ready :tm: ? Yes! Even though the current version is `0.X` for an excess of caution, FusionCache is already used **in production** on multiple **real world projects** happily handling millions of requests per day, or at least these are the projects I'm aware of. -Considering that the FusionCache packages have been downloaded more than **2 million times** (thanks everybody!) it may very well be used even more. +Considering that the FusionCache packages have been downloaded more than **3 million times** (thanks everybody!) it may very well be used even more. And again, if you are using it please [**βœ‰ drop me a line**](https://twitter.com/jodydonetti), I'd like to know! diff --git a/ZiggyCreatures.FusionCache.sln b/ZiggyCreatures.FusionCache.sln index 919dc060..89ce7b2d 100644 --- a/ZiggyCreatures.FusionCache.sln +++ b/ZiggyCreatures.FusionCache.sln @@ -43,7 +43,9 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ZiggyCreatures.FusionCache. EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "SerializerPayloadGenerator", "tests\SerializerPayloadGenerator\SerializerPayloadGenerator.csproj", "{5B1AF24E-90FC-4C21-AF9C-090FE32027E3}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ZiggyCreatures.FusionCache.Simulator", "tests\ZiggyCreatures.FusionCache.Simulator\ZiggyCreatures.FusionCache.Simulator.csproj", "{BDB46997-84D1-4CB5-B967-7F820820CB8E}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ZiggyCreatures.FusionCache.Simulator", "tests\ZiggyCreatures.FusionCache.Simulator\ZiggyCreatures.FusionCache.Simulator.csproj", "{BDB46997-84D1-4CB5-B967-7F820820CB8E}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ZiggyCreatures.FusionCache.OpenTelemetry", "src\ZiggyCreatures.FusionCache.OpenTelemetry\ZiggyCreatures.FusionCache.OpenTelemetry.csproj", "{DA78EB72-93B1-4A77-8525-79AF3EEC4C8D}" EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution @@ -111,6 +113,10 @@ Global {BDB46997-84D1-4CB5-B967-7F820820CB8E}.Debug|Any CPU.Build.0 = Debug|Any CPU {BDB46997-84D1-4CB5-B967-7F820820CB8E}.Release|Any CPU.ActiveCfg = Release|Any CPU {BDB46997-84D1-4CB5-B967-7F820820CB8E}.Release|Any CPU.Build.0 = Release|Any CPU + {DA78EB72-93B1-4A77-8525-79AF3EEC4C8D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {DA78EB72-93B1-4A77-8525-79AF3EEC4C8D}.Debug|Any CPU.Build.0 = Debug|Any CPU + {DA78EB72-93B1-4A77-8525-79AF3EEC4C8D}.Release|Any CPU.ActiveCfg = Release|Any CPU + {DA78EB72-93B1-4A77-8525-79AF3EEC4C8D}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -131,6 +137,7 @@ Global {CE437FB2-510F-4DCE-8A1F-AED747DAA4EB} = {34B53F49-F5C5-4850-B79E-59AD130379C6} {5B1AF24E-90FC-4C21-AF9C-090FE32027E3} = {C6F3C570-C68C-4A95-960E-82778306BDBA} {BDB46997-84D1-4CB5-B967-7F820820CB8E} = {C6F3C570-C68C-4A95-960E-82778306BDBA} + {DA78EB72-93B1-4A77-8525-79AF3EEC4C8D} = {34B53F49-F5C5-4850-B79E-59AD130379C6} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {92916FA2-FCAC-406E-BF3F-0A2CE9512EF0} diff --git a/benchmarks/ZiggyCreatures.FusionCache.Benchmarks/HappyPathBenchmark.cs b/benchmarks/ZiggyCreatures.FusionCache.Benchmarks/HappyPathBenchmark.cs new file mode 100644 index 00000000..d8e0313c --- /dev/null +++ b/benchmarks/ZiggyCreatures.FusionCache.Benchmarks/HappyPathBenchmark.cs @@ -0,0 +1,99 @@ +ο»Ώusing System; +using System.Diagnostics.CodeAnalysis; +using BenchmarkDotNet.Attributes; +using BenchmarkDotNet.Columns; +using BenchmarkDotNet.Configs; +using BenchmarkDotNet.Jobs; +using FastCache; +using LazyCache; +using Microsoft.Extensions.Caching.Memory; + +namespace ZiggyCreatures.Caching.Fusion.Benchmarks +{ + [RankColumn] + [MemoryDiagnoser] + [Config(typeof(Config))] + [ShortRunJob(RuntimeMoniker.Net60)] + [ShortRunJob(RuntimeMoniker.Net80)] + [Orderer(BenchmarkDotNet.Order.SummaryOrderPolicy.FastestToSlowest)] + public class HappyPathBenchmark + { + private class Config : ManualConfig + { + public Config() + { + AddColumn( + StatisticColumn.P95 + ); + } + } + + const string Key = "test key"; + const string Value = "test value"; + + readonly FusionCache FusionCache = new(new FusionCacheOptions()); + readonly MemoryCache MemoryCache = new(new MemoryCacheOptions()); + readonly CachingService LazyCache = new(); + + [GlobalSetup] + public void Setup() + { + FusionCache.Set(Key, Value); + MemoryCache.Set(Key, Value); + LazyCache.Add(Key, Value); + Cached.Save(Key, Value, TimeSpan.FromDays(1)); + } + + public class HappyPathReads : HappyPathBenchmark + { + [Benchmark(Baseline = true)] + public string? GetFusionCache() + { + return FusionCache.TryGet(Key) + .GetValueOrDefault(null); + } + + [Benchmark] + public string? GetMemoryCache() + { + return MemoryCache.TryGetValue(Key, out var value) + ? value + : Unreachable(); + } + + [Benchmark] + public string? GetLazyCache() + { + return LazyCache.TryGetValue(Key, out var value) + ? value + : Unreachable(); + } + + [Benchmark] + public string? GetFastCache() + { + return Cached.TryGet(Key, out var value) + ? value + : Unreachable(); + } + } + + public class HappyPathWrites : HappyPathBenchmark + { + [Benchmark(Baseline = true)] + public void SetFusionCache() => FusionCache.Set(Key, Value); + + [Benchmark] + public void SetMemoryCache() => MemoryCache.Set(Key, Value); + + [Benchmark] + public void SetLazyCache() => LazyCache.Add(Key, Value); + + [Benchmark] + public void SetFastCache() => Cached.Save(Key, Value, TimeSpan.FromDays(1)); + } + + [DoesNotReturn] + static string Unreachable() => throw new Exception("Unreachable code"); + } +} diff --git a/benchmarks/ZiggyCreatures.FusionCache.Benchmarks/ParallelComparisonBenchmark.cs b/benchmarks/ZiggyCreatures.FusionCache.Benchmarks/ParallelComparisonBenchmark.cs index ec5b78dd..f352801e 100644 --- a/benchmarks/ZiggyCreatures.FusionCache.Benchmarks/ParallelComparisonBenchmark.cs +++ b/benchmarks/ZiggyCreatures.FusionCache.Benchmarks/ParallelComparisonBenchmark.cs @@ -15,7 +15,7 @@ using LazyCache.Providers; using Microsoft.Extensions.Caching.Memory; using Microsoft.Extensions.DependencyInjection; -using ZiggyCreatures.Caching.Fusion.Reactors; +using ZiggyCreatures.Caching.Fusion.Locking; namespace ZiggyCreatures.Caching.Fusion.Benchmarks { @@ -53,7 +53,7 @@ public Config() public void Setup() { // SETUP KEYS - Keys = new List(); + Keys = []; for (int i = 0; i < KeysCount; i++) { var key = Guid.NewGuid().ToString("N") + "-" + i.ToString(); @@ -69,232 +69,132 @@ public void Setup() [Benchmark(Baseline = true)] public async Task FusionCache() { - using (var cache = new FusionCache(new FusionCacheOptions { DefaultEntryOptions = new FusionCacheEntryOptions(CacheDuration) })) - { - for (int i = 0; i < Rounds; i++) - { - var tasks = new ConcurrentBag(); - - Parallel.ForEach(Keys, key => - { - Parallel.For(0, Accessors, _ => - { - var t = cache.GetOrSetAsync( - key, - async ct => - { - await Task.Delay(FactoryDurationMs).ConfigureAwait(false); - return new SamplePayload(); - } - ); - tasks.Add(t.AsTask()); - }); - }); - - await Task.WhenAll(tasks).ConfigureAwait(false); - } + using var cache = new FusionCache(new FusionCacheOptions { DefaultEntryOptions = new FusionCacheEntryOptions(CacheDuration) }); - // NO NEED TO CLEANUP, AUTOMATICALLY DONE WHEN DISPOSING - } - } - - //[Benchmark] - public async Task FusionCacheUnbounded() - { - using (var cache = new FusionCache(new FusionCacheOptions { DefaultEntryOptions = new FusionCacheEntryOptions(CacheDuration) }, reactor: new FusionCacheReactorUnbounded())) + for (int i = 0; i < Rounds; i++) { - for (int i = 0; i < Rounds; i++) - { - var tasks = new ConcurrentBag(); - - Parallel.ForEach(Keys, key => - { - Parallel.For(0, Accessors, _ => - { - var t = cache.GetOrSetAsync( - key, - async ct => - { - await Task.Delay(FactoryDurationMs).ConfigureAwait(false); - return new SamplePayload(); - } - ); - tasks.Add(t.AsTask()); - }); - }); - - await Task.WhenAll(tasks).ConfigureAwait(false); - } - - // NO NEED TO CLEANUP, AUTOMATICALLY DONE WHEN DISPOSING - } - } + var tasks = new ConcurrentBag(); - //[Benchmark] - public async Task FusionCacheUnboundedWithPool() - { - using (var cache = new FusionCache(new FusionCacheOptions { DefaultEntryOptions = new FusionCacheEntryOptions(CacheDuration) }, reactor: new FusionCacheReactorUnboundedWithPool())) - { - for (int i = 0; i < Rounds; i++) + Parallel.ForEach(Keys, key => { - var tasks = new ConcurrentBag(); - - Parallel.ForEach(Keys, key => - { - Parallel.For(0, Accessors, _ => - { - var t = cache.GetOrSetAsync( - key, - async ct => - { - await Task.Delay(FactoryDurationMs).ConfigureAwait(false); - return new SamplePayload(); - } - ); - tasks.Add(t.AsTask()); - }); - }); - - await Task.WhenAll(tasks).ConfigureAwait(false); - } + Parallel.For(0, Accessors, _ => + { + var t = cache.GetOrSetAsync( + key, + async ct => + { + await Task.Delay(FactoryDurationMs).ConfigureAwait(false); + return new SamplePayload(); + } + ); + tasks.Add(t.AsTask()); + }); + }); - // NO NEED TO CLEANUP, AUTOMATICALLY DONE WHEN DISPOSING + await Task.WhenAll(tasks).ConfigureAwait(false); } - } - - //[Benchmark] - public async Task FusionCacheUnboundedConcurrent() - { - using (var cache = new FusionCache(new FusionCacheOptions { DefaultEntryOptions = new FusionCacheEntryOptions(CacheDuration) }, reactor: new FusionCacheReactorUnboundedConcurrent())) - { - for (int i = 0; i < Rounds; i++) - { - var tasks = new ConcurrentBag(); - - Parallel.ForEach(Keys, key => - { - Parallel.For(0, Accessors, _ => - { - var t = cache.GetOrSetAsync( - key, - async ct => - { - await Task.Delay(FactoryDurationMs).ConfigureAwait(false); - return new SamplePayload(); - } - ); - tasks.Add(t.AsTask()); - }); - }); - - await Task.WhenAll(tasks).ConfigureAwait(false); - } - // NO NEED TO CLEANUP, AUTOMATICALLY DONE WHEN DISPOSING - } + // NO NEED TO CLEANUP, AUTOMATICALLY DONE WHEN DISPOSING } - //[Benchmark] - public async Task FusionCacheUnboundedConcurrentLazy() + [Benchmark] + public async Task FusionCacheProbabilistic() { - using (var cache = new FusionCache(new FusionCacheOptions { DefaultEntryOptions = new FusionCacheEntryOptions(CacheDuration) }, reactor: new FusionCacheReactorUnboundedConcurrentLazy())) + using var cache = new FusionCache(new FusionCacheOptions { DefaultEntryOptions = new FusionCacheEntryOptions(CacheDuration) }, memoryLocker: new ProbabilisticMemoryLocker()); + + for (int i = 0; i < Rounds; i++) { - for (int i = 0; i < Rounds; i++) - { - var tasks = new ConcurrentBag(); + var tasks = new ConcurrentBag(); - Parallel.ForEach(Keys, key => + Parallel.ForEach(Keys, key => + { + Parallel.For(0, Accessors, _ => { - Parallel.For(0, Accessors, _ => - { - var t = cache.GetOrSetAsync( - key, - async ct => - { - await Task.Delay(FactoryDurationMs).ConfigureAwait(false); - return new SamplePayload(); - } - ); - tasks.Add(t.AsTask()); - }); + var t = cache.GetOrSetAsync( + key, + async ct => + { + await Task.Delay(FactoryDurationMs).ConfigureAwait(false); + return new SamplePayload(); + } + ); + tasks.Add(t.AsTask()); }); + }); - await Task.WhenAll(tasks).ConfigureAwait(false); - } - - // NO NEED TO CLEANUP, AUTOMATICALLY DONE WHEN DISPOSING + await Task.WhenAll(tasks).ConfigureAwait(false); } + + // NO NEED TO CLEANUP, AUTOMATICALLY DONE WHEN DISPOSING } // NOTE: EXCLUDED BECAUSE IT DOES NOT SUPPORT CACHE STAMPEDE PREVENTION, SO IT WOULD NOT BE COMPARABLE //[Benchmark] public void CacheManager() { - using (var cache = CacheFactory.Build(p => p.WithMicrosoftMemoryCacheHandle())) + using var cache = CacheFactory.Build(p => p.WithMicrosoftMemoryCacheHandle()); + + for (int i = 0; i < Rounds; i++) { - for (int i = 0; i < Rounds; i++) + Parallel.ForEach(Keys, key => { - Parallel.ForEach(Keys, key => + Parallel.For(0, Accessors, _ => { - Parallel.For(0, Accessors, _ => - { - cache.GetOrAdd( - key, - _ => - { - Thread.Sleep(FactoryDurationMs); - return new CacheItem( - key, - new SamplePayload(), - global::CacheManager.Core.ExpirationMode.Absolute, - CacheDuration - ); - } - ); - }); + cache.GetOrAdd( + key, + _ => + { + Thread.Sleep(FactoryDurationMs); + return new CacheItem( + key, + new SamplePayload(), + global::CacheManager.Core.ExpirationMode.Absolute, + CacheDuration + ); + } + ); }); - } - - // CLEANUP - cache.Clear(); + }); } + + // CLEANUP + cache.Clear(); } [Benchmark] public async Task CacheTower() { - await using (var cache = new CacheStack(null, new CacheStackOptions(new[] { new MemoryCacheLayer() }) { Extensions = new[] { new AutoCleanupExtension(TimeSpan.FromMinutes(5)) } })) - { - var cacheSettings = new CacheSettings(CacheDuration, CacheDuration); + await using var cache = new CacheStack(null, new CacheStackOptions(new[] { new MemoryCacheLayer() }) { Extensions = new[] { new AutoCleanupExtension(TimeSpan.FromMinutes(5)) } }); - for (int i = 0; i < Rounds; i++) - { - var tasks = new ConcurrentBag(); + var cacheSettings = new CacheSettings(CacheDuration, CacheDuration); - Parallel.ForEach(Keys, key => - { - Parallel.For(0, Accessors, _ => - { - var t = cache.GetOrSetAsync( - key, - async (old) => - { - await Task.Delay(FactoryDurationMs).ConfigureAwait(false); - return new SamplePayload(); - }, - cacheSettings - ).AsTask(); - tasks.Add(t); - }); - }); + for (int i = 0; i < Rounds; i++) + { + var tasks = new ConcurrentBag(); - await Task.WhenAll(tasks).ConfigureAwait(false); - } + Parallel.ForEach(Keys, key => + { + Parallel.For(0, Accessors, _ => + { + var t = cache.GetOrSetAsync( + key, + async (old) => + { + await Task.Delay(FactoryDurationMs).ConfigureAwait(false); + return new SamplePayload(); + }, + cacheSettings + ).AsTask(); + tasks.Add(t); + }); + }); - // CLEANUP - await cache.CleanupAsync(); - await cache.FlushAsync(); + await Task.WhenAll(tasks).ConfigureAwait(false); } + + // CLEANUP + await cache.CleanupAsync(); + await cache.FlushAsync(); } [Benchmark] @@ -334,38 +234,37 @@ public async Task EasyCaching() [Benchmark] public async Task LazyCache() { - using (var cache = new MemoryCache(new MemoryCacheOptions())) - { - var appcache = new CachingService(new MemoryCacheProvider(cache)); + using var cache = new MemoryCache(new MemoryCacheOptions()); - appcache.DefaultCachePolicy = new CacheDefaults { DefaultCacheDurationSeconds = (int)(CacheDuration.TotalSeconds) }; + var appcache = new CachingService(new MemoryCacheProvider(cache)); - for (int i = 0; i < Rounds; i++) - { - var tasks = new ConcurrentBag(); + appcache.DefaultCachePolicy = new CacheDefaults { DefaultCacheDurationSeconds = (int)(CacheDuration.TotalSeconds) }; - Parallel.ForEach(Keys, key => - { - Parallel.For(0, Accessors, _ => - { - var t = appcache.GetOrAddAsync( - key, - async () => - { - await Task.Delay(FactoryDurationMs).ConfigureAwait(false); - return new SamplePayload(); - } - ); - tasks.Add(t); - }); - }); + for (int i = 0; i < Rounds; i++) + { + var tasks = new ConcurrentBag(); - await Task.WhenAll(tasks).ConfigureAwait(false); - } + Parallel.ForEach(Keys, key => + { + Parallel.For(0, Accessors, _ => + { + var t = appcache.GetOrAddAsync( + key, + async () => + { + await Task.Delay(FactoryDurationMs).ConfigureAwait(false); + return new SamplePayload(); + } + ); + tasks.Add(t); + }); + }); - // CLEANUP - cache.Compact(1); + await Task.WhenAll(tasks).ConfigureAwait(false); } + + // CLEANUP + cache.Compact(1); } } } diff --git a/benchmarks/ZiggyCreatures.FusionCache.Benchmarks/ZiggyCreatures.FusionCache.Benchmarks.csproj b/benchmarks/ZiggyCreatures.FusionCache.Benchmarks/ZiggyCreatures.FusionCache.Benchmarks.csproj index 5353d315..e8eaebfd 100644 --- a/benchmarks/ZiggyCreatures.FusionCache.Benchmarks/ZiggyCreatures.FusionCache.Benchmarks.csproj +++ b/benchmarks/ZiggyCreatures.FusionCache.Benchmarks/ZiggyCreatures.FusionCache.Benchmarks.csproj @@ -2,17 +2,19 @@ Exe - net8.0 + + net6.0;net8.0 latest enable ZiggyCreatures.Caching.Fusion.Benchmarks - + + diff --git a/docs/AGentleIntroduction.md b/docs/AGentleIntroduction.md index 382b926f..74802f8d 100644 --- a/docs/AGentleIntroduction.md +++ b/docs/AGentleIntroduction.md @@ -7,7 +7,7 @@ # πŸ¦„ A Gentle Introduction -FusionCache is an easy to use, fast and robust cache with advanced resiliency features and an optional distributed 2nd layer. +FusionCache is an easy to use, fast and robust cache with advanced resiliency features and an optional distributed 2nd level. It uses a memory cache (any impl of the standard `IMemoryCache` interface) as the **primary** backing store and, optionally, a distributed cache (any impl of the standard `IDistributedCache` interface) as a **secondary** backing store for better resilience and higher performance, for example in a multi-node scenario or to avoid the typical effects of a cold start (initial empty cache, maybe after a restart). @@ -52,7 +52,7 @@ Read more [**here**](CacheLevels.md), or enjoy the complete [**step by step**](S ## πŸ“’ Backplane ([more](Backplane.md)) -If we are in a scenario with multiple nodes, each with their own local memory cache, we typically also use a distributed cache as a secondary layer (see above). +If we are in a scenario with multiple nodes, each with their own local memory cache, we typically also use a distributed cache as a secondary level (see above). Even using that, we may find that each memory cache may not be necessarily in-sync with the others, because when a value is cached locally it will stay the same until the `Duration` passes and expiration occurs. @@ -144,7 +144,7 @@ At a high level there are 6 core methods: - `GetOrSet[Async]` - `Expire[Async]` -All of them work **on both the memory cache and the distributed cache** (if any) in a transparent way: we don't have to do anything extra for it to coordinate the 2 layers. +All of them work **on both the memory cache and the distributed cache** (if any) in a transparent way: we don't have to do anything extra for it to coordinate the 2 levels. All of them are available in both a **sync** and an **async** version. @@ -179,7 +179,7 @@ Read more [**here**](NamedCaches.md). ## πŸ“ž Events ([more](Events.md)) -There's a comprehensive set of events to subscribe to regarding core events inside of a FusionCache instance, both at a high level and at lower levels (memory/distributed layers). +There's a comprehensive set of events to subscribe to regarding core events inside of a FusionCache instance, both at a high level and at lower levels (memory/distributed levels). Read more [**here**](Events.md). diff --git a/docs/Backplane.md b/docs/Backplane.md index 20539081..4ddc53a5 100644 --- a/docs/Backplane.md +++ b/docs/Backplane.md @@ -7,7 +7,7 @@ # πŸ“’ Backplane -If we are in a scenario with multiple nodes, each with their own local memory cache, we typically also use a distributed cache as a secondary layer (see [here](CacheLevels.md)). +If we are in a scenario with multiple nodes, each with their own local memory cache, we typically also use a distributed cache as a secondary level (see [here](CacheLevels.md)). But even when using that, we may find that each memory cache on each node may not be in-sync with the others, because when a value is cached locally it will stay the same until the `Duration` passes and expiration occurs. @@ -115,7 +115,7 @@ var redis = new RedisCache(new RedisCacheOptions() { // INSTANTIATE THE FUSION CACHE SERIALIZER var serializer = new FusionCacheNewtonsoftJsonSerializer(); -// SETUP THE DISTRIBUTED 2ND LAYER +// SETUP THE DISTRIBUTED 2ND LEVEL cache.SetupDistributedCache(redis, serializer); // CREATE THE BACKPLANE diff --git a/docs/CacheLevels.md b/docs/CacheLevels.md index 2e9717df..ca54e6cd 100644 --- a/docs/CacheLevels.md +++ b/docs/CacheLevels.md @@ -19,9 +19,9 @@ On top of this you also need to specify a *serializer* to use, by providing an i Basically it boils down to 2 possible ways: -- **1️⃣ MEMORY ONLY:** if you don't setup a 2nd layer, FusionCache will act as a **normal memory cache** (`IMemoryCache`) +- **1️⃣ MEMORY ONLY:** if you don't setup a 2nd level, FusionCache will act as a **normal memory cache** (`IMemoryCache`) -- **2️⃣ MEMORY + DISTRIBUTED:** if you also setup a 2nd layer, FusionCache will automatically coordinate the 2 layers (`IMemoryCache` + `IDistributedCache`) gracefully handling all edge cases to get a smooth experience +- **2️⃣ MEMORY + DISTRIBUTED:** if you also setup a 2nd level, FusionCache will automatically coordinate the 2 levels (`IMemoryCache` + `IDistributedCache`) gracefully handling all edge cases to get a smooth experience Of course in both cases you will also have at your disposal the added ability to enable extra features, like [fail-safe](FailSafe.md), [advanced timeouts](Timeouts.md) and so on. @@ -113,7 +113,7 @@ var serializer = new FusionCacheNewtonsoftJsonSerializer(); // INSTANTIATE FUSION CACHE var cache = new FusionCache(new FusionCacheOptions()); -// SETUP THE DISTRIBUTED 2ND LAYER +// SETUP THE DISTRIBUTED 2ND LEVEL cache.SetupDistributedCache(redis, serializer); ``` diff --git a/docs/Comparison.md b/docs/Comparison.md index 3d63826a..5152d90f 100644 --- a/docs/Comparison.md +++ b/docs/Comparison.md @@ -77,5 +77,5 @@ This is how they compare: | **License** | `MIT` | `Apache 2.0` | `MIT` | `MIT` | `MIT` | β„Ή **NOTES** -- (1): **EasyCaching** supports an `HybridCachingProvider` to handle 2 layers transparently, but it's implemented in a way that checks the distributed cache before the in-memory one, kind of invalidating the benefits of the latter, which is important to know. +- (1): **EasyCaching** supports an `HybridCachingProvider` to handle 2 levels transparently, but it's implemented in a way that checks the distributed cache before the in-memory one, kind of invalidating the benefits of the latter, which is important to know. - (2): **LazyCache** does have both sync and async support, but not for all the available methods (eg. `Remove`). This may be perfectly fine for you or not, but it's good to know. diff --git a/docs/CoreMethods.md b/docs/CoreMethods.md index 8da26b82..0e84f3a2 100644 --- a/docs/CoreMethods.md +++ b/docs/CoreMethods.md @@ -15,7 +15,7 @@ At a high level there are 6 core methods: - `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. +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 levels. All of them are available in both a **sync** and an **async** version. diff --git a/docs/Events.md b/docs/Events.md index b412f826..500fa638 100644 --- a/docs/Events.md +++ b/docs/Events.md @@ -8,15 +8,15 @@ FusionCache has a comprehensive set of events you can subscribe to, so you can be notified of core events when they happen. -They cover both high-level things related to the FusionCache instance as a whole such as cache hits/misses, fail-safe activations or factory timeouts but also more lower level things related to each specific layer (memory/distributed) such as evictions in the memory cache, serialization/deserialization errors or cache hits/misses, but specific for each specific layer. +They cover both high-level things related to the FusionCache instance as a whole such as cache hits/misses, fail-safe activations or factory timeouts but also more lower level things related to each specific level (memory/distributed) such as evictions in the memory cache, serialization/deserialization errors or cache hits/misses, but specific for each specific level. Each event is modeled with native .NET `event`s so if you know how to use them you'll feel at home subscribing and unsubscribing to them. They are grouped into "hubs": - **general**: useful for high level events related to the FusionCache instance as a whole. It is accessible via the `cache.Events` object -- **memory layer**: useful for the lower level memory layer. It is accessible via the `cache.Events.Memory` object -- **distributed layer**: useful for the lower level distributed layer (if any). It is accessible via the `cache.Events.Distributed` object +- **memory level**: useful for the lower level memory level. It is accessible via the `cache.Events.Memory` object +- **distributed level**: useful for the lower level distributed level (if any). It is accessible via the `cache.Events.Distributed` object An event handler is a simple .NET function you can express in the usual ways (lambda, etc...). @@ -56,7 +56,7 @@ Here's a non comprehensive list of the available events: - **Hit**: when a value was in the cache (there's also a flag to indicate if the data was stale or not) - **Miss**: when a value was not in the cache - **Remove**: when an entry has been removed -- **Eviction**: when an eviction occurred, along with the reason (only for the memory layer) +- **Eviction**: when an eviction occurred, along with the reason (only for the memory level) - **FailSafeActivation**: when the fail-safe mechanism kicked in There are more, and you easily discover them with code completion by just typing `cache.Events.` or `cache.Events.Memory` / `cache.Events.Distributed` in your code editor. @@ -73,7 +73,7 @@ A single high level `GetOrSet` method call may result in one of these two set of - **🟒 1 HIT**: if found - **πŸ”΄ 1 MISS + πŸ”΅ 1 SET**: if not found, the factory is called and the returned value is then set into the cache -But if you subscribe to the lower **memory layer** events you may fall into one of these situations: +But if you subscribe to the lower **memory level** events you may fall into one of these situations: - **🟒 1 HIT**: if immediately found - **πŸ”΄ 1 MISS + 🟒 1 HIT**: if not immediately found, a lock is acquired to call the factory in an optimized way, then another cache read is done and this time the entry is found (because another thread set it while acquiring the lock) diff --git a/docs/OpenTelemetry.md b/docs/OpenTelemetry.md new file mode 100644 index 00000000..91998181 --- /dev/null +++ b/docs/OpenTelemetry.md @@ -0,0 +1,93 @@ +
+ +![FusionCache logo](logo-128x128.png) + +
+ + +# πŸ”­ OpenTelemetry + +Observability is a key feature of modern software systems that allows us to clearly see what is going on at any given time. + +It is composed from 3 main _signals_: +- Logs +- Traces +- Metrics + +FusionCache has rich [logging](Logging.md) support via the standard [ILogger](https://learn.microsoft.com/en-us/dotnet/core/extensions/logging) interface, with various options available to granularly configure it. + +For metrics and traces though, no unified abstraction has ever been available for app and library authors to produce signals, and 3rd party services to consume them, in a consistent and cross-platform way. + +That changed when [OpenTelemetry](https://opentelemetry.io/) has been introduced. + +From some time now OpenTelemetry has become a fundamental staple in modern software development to bring complete observability to our applications: after the official standard, the semantic conventions and all the needed pieces finally become `v1.0`, FusionCache happily adopted it as *the* observability mechanism. + +This is an example of the visibility we can achieve, in this case in [Honeycomb.io](Honeycomb): + +![An example of the visibility obtainable by using OpenTelemetry, in this case thanks to the Honeycomb SAAS](images/opentelemetry-example.png) + +ℹ️ Please note that I don't have any affiliation, partnership or else with Honeycomb: I just created a free trial to test the OpenTelemetry integration. + +Other than that I've used a couple of their free [Observability Office Hours](https://www.honeycomb.io/devrel/observability-office-hours) to chat with the awesome [Martin Thwaites](https://twitter.com/MartinDotNet) to better understand the right way to name activity sources, which tags to use in the metrics and so on: thanks Martin! + +## OpenTelemetry in .NET + +Normally in other languages there's an OpenTelemetry SDK being made available but the kind folks working on it, so that library authors can use that SDK to integrate into the OpenTelemetry world and communicate with processors, exporters and so no. + +This is also true for .NET, but with a nice catch: in .NET there's always been some form of observability support via core primitives like `Activity`, `ActivitySource`, `Meter` and similar classes already part of the BCL, so instead of creating new primitives the OpenTelemetry team decided to use the existing abstractions and "talk" to them: in this way it's possible for library authors **not** to take a hard dependency on the OpenTelemetry packages and simply use the existing primitives and have the consuming side of the whole OpenTelemetry pipeline interact with them. + +Nice, very nice, and this is what FusionCache did. + +## How to use it + +It is possible to opt-in to generate traces and metrics for both: +- **high-level operations**: things like `GetOrSet`/`Set`/`Remove` operations, `Hit`/`Miss` events, etc +- **low-level operations**: things like memory/distributed level operations, backplane events, etc + +There's also a new [package](https://www.nuget.org/packages/ZiggyCreatures.FusionCache.OpenTelemetry/) specific for OpenTelemetry that adds native support to instrument our applications, with FusionCache specific options ready to use, like including low-level signals or not. + +It's then possible to simply plug one of the existing OpenTelemetry-compatible exporters for systems like [Jaeger](https://www.jaegertracing.io/), [Prometheus](https://prometheus.io/) or [Honeycomb](https://www.honeycomb.io/) and voilΓ , there we have full observability of our FusionCache instances. + +### πŸ‘©β€πŸ’» Example + +Add the [package](https://www.nuget.org/packages/ZiggyCreatures.FusionCache.OpenTelemetry/): + +```PowerShell +PM> Install-Package ZiggyCreatures.FusionCache.OpenTelemetry +``` + +and enable either traces, metrics or both for FusionCache. + +Without dependency injection we can do this: + +```csharp +// SETUP TRACES +using var tracerProvider = Sdk.CreateTracerProviderBuilder() + .AddFusionCacheInstrumentation() + .AddConsoleExporter() + .Build(); + +// SETUP METRICS +using var meterProvider = Sdk.CreateMeterProviderBuilder() + .AddFusionCacheInstrumentation() + .AddConsoleExporter() + .Build(); +``` + +Or via dependency injection, like this: + +```csharp +services.AddOpenTelemetry() + // SETUP TRACES + .WithTracing(tracing => tracing + .AddFusionCacheInstrumentation() + .AddConsoleExporter() + ) + // SETUP METRICS + .WithMetrics(metrics => metrics + .AddFusionCacheInstrumentation() + .AddConsoleExporter() + ); +``` + +Easy peasy. \ No newline at end of file diff --git a/docs/Options.md b/docs/Options.md index 9e3c8211..ac4e7eaa 100644 --- a/docs/Options.md +++ b/docs/Options.md @@ -105,8 +105,8 @@ For a better **developer experience** and to **consume less memory** (higher per | `FactorySoftTimeout` | `TimeSpan` | `none` | The maximum execution time allowed for the factory, applied only if fail-safe is enabled and there is a fallback value to return. | | `FactoryHardTimeout` | `TimeSpan` | `none` | The maximum execution time allowed for the factory in any case, even if there is not a stale value to fallback to. | | `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. | +| πŸ§™β€β™‚οΈ `DistributedCacheDuration` | `TimeSpan?` | `null` | The custom duration to use for the distributed cache: this allows to have different duration between the 1st and 2nd levels. 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 levels. 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. | @@ -117,5 +117,5 @@ For a better **developer experience** and to **consume less memory** (higher per | πŸ§™β€β™‚οΈ `ReThrowBackplaneExceptions` | `bool` | `false` | Set this to true to allow the bubble up of backplane exceptions (default is `false`). Please note that, even if set to true, in some cases you would also need `AllowBackgroundBackplaneOperations` set to false. | | πŸ§™β€β™‚οΈ `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. | +| πŸ§™β€β™‚οΈ `SkipDistributedCacheReadWhenStale` | `bool` | `false` | When a 2nd level (distributed cache) is used and a cache entry in the 1st level (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/README.md b/docs/README.md index 80c47c2d..2df2b319 100644 --- a/docs/README.md +++ b/docs/README.md @@ -37,7 +37,8 @@ A deeper description of the main features: - [**πŸ¦… Eager Refresh**](EagerRefresh.md): start a non-blocking background refresh before the expiration occurs - [**πŸ”ƒ Dependency Injection**](DependencyInjection.md): native support for Dependency Injection, with a nice fluent interface including a Builder support - [**πŸ“› Named Caches**](NamedCaches.md): easily work with multiple named caches, even if differently configured +- [**πŸ”­ OpenTelemetry**](OpenTelemetry.md): native observability support via OpenTelemetry +- [**πŸ“œ Logging**](Logging.md): comprehensive, structured and customizable, via the standard `ILogger` interface - [**πŸ’« Natively sync/async**](CoreMethods.md): native support for both the synchronous and asynchronous programming model - [**πŸ“ž Events**](Events.md): a comprehensive set of events, both at a high level and at lower levels (memory/distributed) - [**🧩 Plugins**](Plugins.md): extend FusionCache with additional behavior like adding support for metrics, statistics, etc... -- [**πŸ“œ Logging**](Logging.md): comprehensive, structured and customizable, via the standard `ILogger` interface diff --git a/docs/Timeouts.md b/docs/Timeouts.md index c8f595f3..fdfc04a9 100644 --- a/docs/Timeouts.md +++ b/docs/Timeouts.md @@ -95,7 +95,7 @@ var serializer = new FusionCacheNewtonsoftJsonSerializer(); // INSTANTIATE FUSION CACHE var cache = new FusionCache(new FusionCacheOptions()); -// SETUP THE DISTRIBUTED 2ND LAYER +// SETUP THE DISTRIBUTED 2ND LEVEL cache.SetupDistributedCache(redis, serializer); // SET A VALUE IN THE CACHE VIA FUSION CACHE, WITH BACKGROUND DISTRIBUTED OPERATIONS diff --git a/docs/images/opentelemetry-example.png b/docs/images/opentelemetry-example.png new file mode 100644 index 00000000..3215f585 Binary files /dev/null and b/docs/images/opentelemetry-example.png differ diff --git a/src/ZiggyCreatures.FusionCache.Backplane.Memory/GlobalSuppressions.cs b/src/ZiggyCreatures.FusionCache.Backplane.Memory/GlobalSuppressions.cs index b961566c..3389b8d5 100644 --- a/src/ZiggyCreatures.FusionCache.Backplane.Memory/GlobalSuppressions.cs +++ b/src/ZiggyCreatures.FusionCache.Backplane.Memory/GlobalSuppressions.cs @@ -5,4 +5,4 @@ using System.Diagnostics.CodeAnalysis; -[assembly: SuppressMessage("Simplification", "RCS1049:Simplify boolean comparison.", Justification = "")] +[assembly: SuppressMessage("Simplification", "RCS1049:Simplify boolean comparison.")] diff --git a/src/ZiggyCreatures.FusionCache.Backplane.Memory/MemoryBackplaneExtensions.cs b/src/ZiggyCreatures.FusionCache.Backplane.Memory/MemoryBackplaneExtensions.cs index 4ec8f41d..ebd2d3cb 100644 --- a/src/ZiggyCreatures.FusionCache.Backplane.Memory/MemoryBackplaneExtensions.cs +++ b/src/ZiggyCreatures.FusionCache.Backplane.Memory/MemoryBackplaneExtensions.cs @@ -49,7 +49,11 @@ public static IFusionCacheBuilder WithMemoryBackplane(this IFusionCacheBuilder b return builder .WithBackplane(sp => { - var options = sp.GetService>().Get(builder.CacheName); + var options = sp.GetService>()?.Get(builder.CacheName); + + if (options is null) + throw new NullReferenceException($"Unable to find an instance of {nameof(MemoryBackplaneOptions)} for the cache named '{builder.CacheName}'."); + if (setupOptionsAction is not null) setupOptionsAction?.Invoke(options); var logger = sp.GetService>(); 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 b4717310..c34ec142 100644 --- a/src/ZiggyCreatures.FusionCache.Backplane.Memory/ZiggyCreatures.FusionCache.Backplane.Memory.csproj +++ b/src/ZiggyCreatures.FusionCache.Backplane.Memory/ZiggyCreatures.FusionCache.Backplane.Memory.csproj @@ -4,16 +4,16 @@ netstandard2.0 latest enable - 0.24.0 + 0.25.0 ZiggyCreatures.FusionCache.Backplane.Memory logo-128x128.png FusionCache in memory backplane, used for testing - backplane;memory;stackexchange;caching;cache;multi-level;multilevel;fusion;fusioncache;fusion-cache;performance;async;ziggy + backplane;memory;caching;cache;multi-level;multilevel;fusion;fusioncache;fusion-cache;performance;async;ziggy ZiggyCreatures.Caching.Fusion.Backplane.Memory ZiggyCreatures.FusionCache.Backplane.Memory.xml README.md - - Update: dependencies + - Update: package dependencies diff --git a/src/ZiggyCreatures.FusionCache.Backplane.Memory/docs/README.md b/src/ZiggyCreatures.FusionCache.Backplane.Memory/docs/README.md index 0377f29c..fe5e2358 100644 --- a/src/ZiggyCreatures.FusionCache.Backplane.Memory/docs/README.md +++ b/src/ZiggyCreatures.FusionCache.Backplane.Memory/docs/README.md @@ -2,7 +2,7 @@ ![FusionCache logo](https://raw.githubusercontent.com/ZiggyCreatures/FusionCache/main/docs/logo-256x256.png) -### FusionCache is an easy to use, fast and robust cache with advanced resiliency features and an optional distributed 2nd layer. +### FusionCache is an easy to use, fast and robust cache with advanced resiliency features and an optional distributed 2nd level. It was born after years of dealing with all sorts of different types of caches: memory caching, distributed caching, http caching, CDNs, browser cache, offline cache, you name it. So I've tried to put together these experiences and came up with FusionCache. diff --git a/src/ZiggyCreatures.FusionCache.Backplane.StackExchangeRedis/RedisBackplaneOptions.cs b/src/ZiggyCreatures.FusionCache.Backplane.StackExchangeRedis/RedisBackplaneOptions.cs index 15d1f368..3ff8b64c 100644 --- a/src/ZiggyCreatures.FusionCache.Backplane.StackExchangeRedis/RedisBackplaneOptions.cs +++ b/src/ZiggyCreatures.FusionCache.Backplane.StackExchangeRedis/RedisBackplaneOptions.cs @@ -32,7 +32,7 @@ public class RedisBackplaneOptions /// DEPRECATED: verify that at least one clients received the notifications after each publish. /// [EditorBrowsable(EditorBrowsableState.Never)] - [Obsolete] + [Obsolete("Please stop using this, it is now obsolete.")] public bool VerifyReceivedClientsCountAfterPublish { get; set; } = false; RedisBackplaneOptions IOptions.Value diff --git a/src/ZiggyCreatures.FusionCache.Backplane.StackExchangeRedis/StackExchangeRedisBackplaneExtensions.cs b/src/ZiggyCreatures.FusionCache.Backplane.StackExchangeRedis/StackExchangeRedisBackplaneExtensions.cs index dc783b6b..06617de9 100644 --- a/src/ZiggyCreatures.FusionCache.Backplane.StackExchangeRedis/StackExchangeRedisBackplaneExtensions.cs +++ b/src/ZiggyCreatures.FusionCache.Backplane.StackExchangeRedis/StackExchangeRedisBackplaneExtensions.cs @@ -49,7 +49,10 @@ public static IFusionCacheBuilder WithStackExchangeRedisBackplane(this IFusionCa return builder .WithBackplane(sp => { - var options = sp.GetService>().Get(builder.CacheName); + var options = sp.GetService>()?.Get(builder.CacheName); + if (options is null) + throw new InvalidOperationException($"Unable to find a valid {nameof(RedisBackplaneOptions)} instance for the current cache name '{builder.CacheName}'."); + if (setupOptionsAction is not null) setupOptionsAction?.Invoke(options); var logger = sp.GetService>(); 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 bc52432a..79ccc563 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.24.0 + 0.25.0 ZiggyCreatures.FusionCache.Backplane.StackExchangeRedis logo-128x128.png FusionCache backplane for Redis based on the StackExchange.Redis library @@ -13,8 +13,7 @@ ZiggyCreatures.FusionCache.Backplane.StackExchangeRedis.xml README.md - - Added: support for MultiplexerFactory in Redis Backplane - - Update: dependencies + - Update: package dependencies @@ -28,7 +27,7 @@ - + diff --git a/src/ZiggyCreatures.FusionCache.Backplane.StackExchangeRedis/docs/README.md b/src/ZiggyCreatures.FusionCache.Backplane.StackExchangeRedis/docs/README.md index e0e595ae..544a5d40 100644 --- a/src/ZiggyCreatures.FusionCache.Backplane.StackExchangeRedis/docs/README.md +++ b/src/ZiggyCreatures.FusionCache.Backplane.StackExchangeRedis/docs/README.md @@ -2,7 +2,7 @@ ![FusionCache logo](https://raw.githubusercontent.com/ZiggyCreatures/FusionCache/main/docs/logo-256x256.png) -### FusionCache is an easy to use, fast and robust cache with advanced resiliency features and an optional distributed 2nd layer. +### FusionCache is an easy to use, fast and robust cache with advanced resiliency features and an optional distributed 2nd level. It was born after years of dealing with all sorts of different types of caches: memory caching, distributed caching, http caching, CDNs, browser cache, offline cache, you name it. So I've tried to put together these experiences and came up with FusionCache. diff --git a/src/ZiggyCreatures.FusionCache.Chaos/ChaosDistributedCache.cs b/src/ZiggyCreatures.FusionCache.Chaos/ChaosDistributedCache.cs index c7b5bb00..3c853690 100644 --- a/src/ZiggyCreatures.FusionCache.Chaos/ChaosDistributedCache.cs +++ b/src/ZiggyCreatures.FusionCache.Chaos/ChaosDistributedCache.cs @@ -28,14 +28,14 @@ public ChaosDistributedCache(IDistributedCache innerCache, ILogger - public byte[] Get(string key) + public byte[]? Get(string key) { MaybeChaos(); return _innerCache.Get(key); } /// - public async Task GetAsync(string key, CancellationToken token = default) + public async Task GetAsync(string key, CancellationToken token = default) { await MaybeChaosAsync(token).ConfigureAwait(false); return await _innerCache.GetAsync(key, token).ConfigureAwait(false); diff --git a/src/ZiggyCreatures.FusionCache.Chaos/ChaosMemoryLocker.cs b/src/ZiggyCreatures.FusionCache.Chaos/ChaosMemoryLocker.cs new file mode 100644 index 00000000..3c64d3c7 --- /dev/null +++ b/src/ZiggyCreatures.FusionCache.Chaos/ChaosMemoryLocker.cs @@ -0,0 +1,57 @@ +ο»Ώusing System; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.Extensions.Logging; +using ZiggyCreatures.Caching.Fusion.Chaos.Internals; +using ZiggyCreatures.Caching.Fusion.Locking; + +namespace ZiggyCreatures.Caching.Fusion.Chaos +{ + /// + /// An implementation of with a (controllable) amount of chaos in-between. + /// + public class ChaosMemoryLocker + : AbstractChaosComponent + , IFusionCacheMemoryLocker + { + private readonly IFusionCacheMemoryLocker _innerMemoryLocker; + + /// + /// Initializes a new instance of the ChaosMemoryLocker class. + /// + /// The actual used if and when chaos does not happen. + /// The logger to use, or . + public ChaosMemoryLocker(IFusionCacheMemoryLocker innerMemoryLocker, ILogger? logger = null) + : base(logger) + { + _innerMemoryLocker = innerMemoryLocker ?? throw new ArgumentNullException(nameof(innerMemoryLocker)); + } + + /// + public object? AcquireLock(string cacheName, string cacheInstanceId, string key, string operationId, TimeSpan timeout, ILogger? logger, CancellationToken token) + { + MaybeChaos(); + return _innerMemoryLocker.AcquireLock(cacheName, cacheInstanceId, key, operationId, timeout, logger, token); + } + + /// + public async ValueTask AcquireLockAsync(string cacheName, string cacheInstanceId, string key, string operationId, TimeSpan timeout, ILogger? logger, CancellationToken token) + { + MaybeChaos(); + return await _innerMemoryLocker.AcquireLockAsync(cacheName, cacheInstanceId, key, operationId, timeout, logger, token); + } + + /// + public void ReleaseLock(string cacheName, string cacheInstanceId, string key, string operationId, object? lockObj, ILogger? logger) + { + MaybeChaos(); + _innerMemoryLocker.ReleaseLock(cacheName, cacheInstanceId, key, operationId, lockObj, logger); + } + + /// + public void Dispose() + { + // EMPTY + } + } +} diff --git a/src/ZiggyCreatures.FusionCache.Chaos/ChaosPlugin.cs b/src/ZiggyCreatures.FusionCache.Chaos/ChaosPlugin.cs index 7c6f417e..2a3a4c6e 100644 --- a/src/ZiggyCreatures.FusionCache.Chaos/ChaosPlugin.cs +++ b/src/ZiggyCreatures.FusionCache.Chaos/ChaosPlugin.cs @@ -12,7 +12,7 @@ public class ChaosPlugin : AbstractChaosComponent , IFusionCachePlugin { - IFusionCachePlugin _innerPlugin; + private readonly IFusionCachePlugin _innerPlugin; /// /// Initializes a new instance of the ChaosPlugin class. diff --git a/src/ZiggyCreatures.FusionCache.Chaos/FusionCacheChaosUtils.cs b/src/ZiggyCreatures.FusionCache.Chaos/FusionCacheChaosUtils.cs index 7e139cf7..91fc3ead 100644 --- a/src/ZiggyCreatures.FusionCache.Chaos/FusionCacheChaosUtils.cs +++ b/src/ZiggyCreatures.FusionCache.Chaos/FusionCacheChaosUtils.cs @@ -63,7 +63,6 @@ public static void MaybeDelay(TimeSpan minDelay, TimeSpan maxDelay, Cancellation { var delay = GetRandomDelay(minDelay, maxDelay); - // TODO: FIND A WAY TO CANCEL THE DELAY if (delay > TimeSpan.Zero) Thread.Sleep(delay); } diff --git a/src/ZiggyCreatures.FusionCache.Chaos/ZiggyCreatures.FusionCache.Chaos.csproj b/src/ZiggyCreatures.FusionCache.Chaos/ZiggyCreatures.FusionCache.Chaos.csproj index cc470026..094e629c 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.24.0 + 0.25.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. @@ -13,10 +13,7 @@ ZiggyCreatures.FusionCache.Chaos.xml README.md - - Added: cancellation support - - Added: new AbstractChaosComponent acting as a base class for all chaos-related components - - Changed: all chaos-related components now inherit from AbstractChaosComponent - - Update: dependencies + - Update: package dependencies diff --git a/src/ZiggyCreatures.FusionCache.Chaos/ZiggyCreatures.FusionCache.Chaos.xml b/src/ZiggyCreatures.FusionCache.Chaos/ZiggyCreatures.FusionCache.Chaos.xml index d8c13fbd..a91038f2 100644 --- a/src/ZiggyCreatures.FusionCache.Chaos/ZiggyCreatures.FusionCache.Chaos.xml +++ b/src/ZiggyCreatures.FusionCache.Chaos/ZiggyCreatures.FusionCache.Chaos.xml @@ -91,6 +91,30 @@ The object that holds the serialized object data. The contextual information about the source or destination. + + + An implementation of with a (controllable) amount of chaos in-between. + + + + + Initializes a new instance of the ChaosMemoryLocker class. + + The actual used if and when chaos does not happen. + The logger to use, or . + + + + + + + + + + + + + An implementation of with a (controllable) amount of chaos in-between. diff --git a/src/ZiggyCreatures.FusionCache.Chaos/docs/README.md b/src/ZiggyCreatures.FusionCache.Chaos/docs/README.md index 872dede8..388c2b87 100644 --- a/src/ZiggyCreatures.FusionCache.Chaos/docs/README.md +++ b/src/ZiggyCreatures.FusionCache.Chaos/docs/README.md @@ -2,7 +2,7 @@ ![FusionCache logo](https://raw.githubusercontent.com/ZiggyCreatures/FusionCache/main/docs/logo-256x256.png) -### FusionCache is an easy to use, fast and robust cache with advanced resiliency features and an optional distributed 2nd layer. +### FusionCache is an easy to use, fast and robust cache with advanced resiliency features and an optional distributed 2nd level. It was born after years of dealing with all sorts of different types of caches: memory caching, distributed caching, http caching, CDNs, browser cache, offline cache, you name it. So I've tried to put together these experiences and came up with FusionCache. diff --git a/src/ZiggyCreatures.FusionCache.OpenTelemetry/FusionCacheMetricsInstrumentationOptions.cs b/src/ZiggyCreatures.FusionCache.OpenTelemetry/FusionCacheMetricsInstrumentationOptions.cs new file mode 100644 index 00000000..45b38276 --- /dev/null +++ b/src/ZiggyCreatures.FusionCache.OpenTelemetry/FusionCacheMetricsInstrumentationOptions.cs @@ -0,0 +1,23 @@ +ο»Ώnamespace ZiggyCreatures.FusionCache.OpenTelemetry +{ + /// + /// Represents the options available for the metrics instrumentation of FusionCache. + /// + public class FusionCacheMetricsInstrumentationOptions + { + /// + /// Include metrics for the memory level. (default: ) + /// + public bool IncludeMemoryLevel { get; set; } = false; + + /// + /// Include metrics for the distributed level. (default: ) + /// + public bool IncludeDistributedLevel { get; set; } = false; + + /// + /// Include metrics for the backplane. (default: ) + /// + public bool IncludeBackplane { get; set; } = false; + } +} diff --git a/src/ZiggyCreatures.FusionCache.OpenTelemetry/FusionCacheTracesInstrumentationOptions.cs b/src/ZiggyCreatures.FusionCache.OpenTelemetry/FusionCacheTracesInstrumentationOptions.cs new file mode 100644 index 00000000..e5e6c2a3 --- /dev/null +++ b/src/ZiggyCreatures.FusionCache.OpenTelemetry/FusionCacheTracesInstrumentationOptions.cs @@ -0,0 +1,23 @@ +ο»Ώnamespace ZiggyCreatures.FusionCache.OpenTelemetry +{ + /// + /// Represents the options available for the traces instrumentation of FusionCache. + /// + public class FusionCacheTracesInstrumentationOptions + { + /// + /// Include traces for the memory level. (default: ) + /// + public bool IncludeMemoryLevel { get; set; } = false; + + /// + /// Include traces for the distributed level. (default: ) + /// + public bool IncludeDistributedLevel { get; set; } = true; + + /// + /// Include traces for the backplane. (default: ) + /// + public bool IncludeBackplane { get; set; } = true; + } +} diff --git a/src/ZiggyCreatures.FusionCache.OpenTelemetry/MeterProviderBuilderExtensions.cs b/src/ZiggyCreatures.FusionCache.OpenTelemetry/MeterProviderBuilderExtensions.cs new file mode 100644 index 00000000..f24ee0b3 --- /dev/null +++ b/src/ZiggyCreatures.FusionCache.OpenTelemetry/MeterProviderBuilderExtensions.cs @@ -0,0 +1,37 @@ +ο»Ώusing System; +using ZiggyCreatures.Caching.Fusion; +using ZiggyCreatures.FusionCache.OpenTelemetry; + +namespace OpenTelemetry.Metrics +{ + /// + /// Contains extension methods to for enabling FusionCache metrics instrumentation. + /// + public static class MeterProviderBuilderExtensions + { + /// + /// Enables metrics instrumentation for FusionCache. + /// + /// being configured. + /// Callback action for configuring the available options. + /// The instance of to chain the calls. + public static MeterProviderBuilder AddFusionCacheInstrumentation(this MeterProviderBuilder builder, Action? configure = null) + { + if (builder is null) + throw new ArgumentNullException(nameof(builder)); + + var options = new FusionCacheMetricsInstrumentationOptions(); + configure?.Invoke(options); + + builder.AddMeter(FusionCacheDiagnostics.MeterName); + if (options.IncludeMemoryLevel) + builder.AddMeter(FusionCacheDiagnostics.MeterNameMemoryLevel); + if (options.IncludeDistributedLevel) + builder.AddMeter(FusionCacheDiagnostics.MeterNameDistributedLevel); + if (options.IncludeBackplane) + builder.AddMeter(FusionCacheDiagnostics.MeterNameBackplane); + + return builder; + } + } +} diff --git a/src/ZiggyCreatures.FusionCache.OpenTelemetry/TracerProviderBuilderExtensions.cs b/src/ZiggyCreatures.FusionCache.OpenTelemetry/TracerProviderBuilderExtensions.cs new file mode 100644 index 00000000..2fa010c0 --- /dev/null +++ b/src/ZiggyCreatures.FusionCache.OpenTelemetry/TracerProviderBuilderExtensions.cs @@ -0,0 +1,37 @@ +ο»Ώusing System; +using ZiggyCreatures.Caching.Fusion; +using ZiggyCreatures.FusionCache.OpenTelemetry; + +namespace OpenTelemetry.Trace +{ + /// + /// Contains extension methods to for enabling FusionCache traces instrumentation. + /// + public static class TracerProviderBuilderExtensions + { + /// + /// Enables traces instrumentation for FusionCache. + /// + /// being configured. + /// Callback action for configuring the available options. + /// The instance of to chain the calls. + public static TracerProviderBuilder AddFusionCacheInstrumentation(this TracerProviderBuilder builder, Action? configure = null) + { + if (builder is null) + throw new ArgumentNullException(nameof(builder)); + + var options = new FusionCacheTracesInstrumentationOptions(); + configure?.Invoke(options); + + builder.AddSource(FusionCacheDiagnostics.ActivitySourceName); + if (options.IncludeMemoryLevel) + builder.AddSource(FusionCacheDiagnostics.ActivitySourceNameMemoryLevel); + if (options.IncludeDistributedLevel) + builder.AddSource(FusionCacheDiagnostics.ActivitySourceNameDistributedLevel); + if (options.IncludeBackplane) + builder.AddSource(FusionCacheDiagnostics.ActivitySourceNameBackplane); + + return builder; + } + } +} diff --git a/src/ZiggyCreatures.FusionCache.OpenTelemetry/ZiggyCreatures.FusionCache.OpenTelemetry.csproj b/src/ZiggyCreatures.FusionCache.OpenTelemetry/ZiggyCreatures.FusionCache.OpenTelemetry.csproj new file mode 100644 index 00000000..61bdde85 --- /dev/null +++ b/src/ZiggyCreatures.FusionCache.OpenTelemetry/ZiggyCreatures.FusionCache.OpenTelemetry.csproj @@ -0,0 +1,30 @@ +ο»Ώ + + + netstandard2.0 + latest + enable + 0.25.0 + ZiggyCreatures.FusionCache.OpenTelemetry + logo-128x128.png + Add native OpenTelemetry support to FusionCache. + telemetry;observability;opentelemetry;open-telemetry;chaos;caching;cache;multi-level;multilevel;fusion;fusioncache;fusion-cache;performance;async;ziggy + ZiggyCreatures.FusionCache.OpenTelemetry + ZiggyCreatures.FusionCache.OpenTelemetry.xml + README.md + + - Initial release + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/ZiggyCreatures.FusionCache.OpenTelemetry/ZiggyCreatures.FusionCache.OpenTelemetry.xml b/src/ZiggyCreatures.FusionCache.OpenTelemetry/ZiggyCreatures.FusionCache.OpenTelemetry.xml new file mode 100644 index 00000000..2a585734 --- /dev/null +++ b/src/ZiggyCreatures.FusionCache.OpenTelemetry/ZiggyCreatures.FusionCache.OpenTelemetry.xml @@ -0,0 +1,74 @@ + + + + ZiggyCreatures.FusionCache.OpenTelemetry + + + + + Represents the options available for the metrics instrumentation of FusionCache. + + + + + Include metrics for the memory level. (default: ) + + + + + Include metrics for the distributed level. (default: ) + + + + + Include metrics for the backplane. (default: ) + + + + + Represents the options available for the traces instrumentation of FusionCache. + + + + + Include traces for the memory level. (default: ) + + + + + Include traces for the distributed level. (default: ) + + + + + Include traces for the backplane. (default: ) + + + + + Contains extension methods to for enabling FusionCache metrics instrumentation. + + + + + Enables metrics instrumentation for FusionCache. + + being configured. + Callback action for configuring the available options. + The instance of to chain the calls. + + + + Contains extension methods to for enabling FusionCache traces instrumentation. + + + + + Enables traces instrumentation for FusionCache. + + being configured. + Callback action for configuring the available options. + The instance of to chain the calls. + + + diff --git a/src/ZiggyCreatures.FusionCache.OpenTelemetry/artwork/logo-128x128.png b/src/ZiggyCreatures.FusionCache.OpenTelemetry/artwork/logo-128x128.png new file mode 100644 index 00000000..ce400a79 Binary files /dev/null and b/src/ZiggyCreatures.FusionCache.OpenTelemetry/artwork/logo-128x128.png differ diff --git a/src/ZiggyCreatures.FusionCache.OpenTelemetry/docs/README.md b/src/ZiggyCreatures.FusionCache.OpenTelemetry/docs/README.md new file mode 100644 index 00000000..62728118 --- /dev/null +++ b/src/ZiggyCreatures.FusionCache.OpenTelemetry/docs/README.md @@ -0,0 +1,13 @@ +ο»Ώ# FusionCache + +![FusionCache logo](https://raw.githubusercontent.com/ZiggyCreatures/FusionCache/main/docs/logo-256x256.png) + +### FusionCache is an easy to use, fast and robust cache with advanced resiliency features and an optional distributed 2nd level. + +It was born after years of dealing with all sorts of different types of caches: memory caching, distributed caching, http caching, CDNs, browser cache, offline cache, you name it. So I've tried to put together these experiences and came up with FusionCache. + +Find out [more](https://github.com/ZiggyCreatures/FusionCache). + +## πŸ“¦ This package + +This package adds native [OpenTelemetry](https://opentelemetry.io/) instrumentation to FusionCache. \ No newline at end of file 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 7fe4b8db..6e74ed09 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.24.0 + 0.25.0 ZiggyCreatures.FusionCache.Serialization.CysharpMemoryPack logo-128x128.png FusionCache serializer based on Cysharp's MemoryPack serializer @@ -13,7 +13,7 @@ ZiggyCreatures.Caching.Fusion.Serialization.CysharpMemoryPack.xml README.md - - Update: dependencies + - Update: package dependencies diff --git a/src/ZiggyCreatures.FusionCache.Serialization.CysharpMemoryPack/docs/README.md b/src/ZiggyCreatures.FusionCache.Serialization.CysharpMemoryPack/docs/README.md index 4d3df9a6..93b03ff7 100644 --- a/src/ZiggyCreatures.FusionCache.Serialization.CysharpMemoryPack/docs/README.md +++ b/src/ZiggyCreatures.FusionCache.Serialization.CysharpMemoryPack/docs/README.md @@ -2,7 +2,7 @@ ![FusionCache logo](https://raw.githubusercontent.com/ZiggyCreatures/FusionCache/main/docs/logo-256x256.png) -### FusionCache is an easy to use, fast and robust cache with advanced resiliency features and an optional distributed 2nd layer. +### FusionCache is an easy to use, fast and robust cache with advanced resiliency features and an optional distributed 2nd level. It was born after years of dealing with all sorts of different types of caches: memory caching, distributed caching, http caching, CDNs, browser cache, offline cache, you name it. So I've tried to put together these experiences and came up with FusionCache. @@ -10,4 +10,4 @@ Find out [more](https://github.com/ZiggyCreatures/FusionCache). ## πŸ“¦ This package -This package is an implementation for a FusionCache serializer to be used with the optional distributed cache layer, based on the uber fast new serializer [MemoryPack](https://github.com/Cysharp/MemoryPack). \ No newline at end of file +This package is an implementation for a FusionCache serializer to be used with the optional distributed cache level, based on the uber fast new serializer [MemoryPack](https://github.com/Cysharp/MemoryPack). \ No newline at end of file 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 14dad52f..9b239420 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.24.0 + 0.25.0 ZiggyCreatures.FusionCache.Serialization.NeueccMessagePack logo-128x128.png FusionCache serializer based on Neuecc's MessagePack serializer @@ -13,7 +13,7 @@ ZiggyCreatures.FusionCache.Serialization.NeueccMessagePack.xml README.md - - Update: dependencies + - Update: package dependencies @@ -23,7 +23,7 @@ - + diff --git a/src/ZiggyCreatures.FusionCache.Serialization.NeueccMessagePack/docs/README.md b/src/ZiggyCreatures.FusionCache.Serialization.NeueccMessagePack/docs/README.md index 40b54fbb..ee07e069 100644 --- a/src/ZiggyCreatures.FusionCache.Serialization.NeueccMessagePack/docs/README.md +++ b/src/ZiggyCreatures.FusionCache.Serialization.NeueccMessagePack/docs/README.md @@ -2,7 +2,7 @@ ![FusionCache logo](https://raw.githubusercontent.com/ZiggyCreatures/FusionCache/main/docs/logo-256x256.png) -### FusionCache is an easy to use, fast and robust cache with advanced resiliency features and an optional distributed 2nd layer. +### FusionCache is an easy to use, fast and robust cache with advanced resiliency features and an optional distributed 2nd level. It was born after years of dealing with all sorts of different types of caches: memory caching, distributed caching, http caching, CDNs, browser cache, offline cache, you name it. So I've tried to put together these experiences and came up with FusionCache. @@ -10,4 +10,4 @@ Find out [more](https://github.com/ZiggyCreatures/FusionCache). ## πŸ“¦ This package -This package is an implementation for a FusionCache serializer to be used with the optional distributed cache layer, based on the famous [Neuecc's MessagePack](https://github.com/neuecc/MessagePack-CSharp). \ No newline at end of file +This package is an implementation for a FusionCache serializer to be used with the optional distributed cache level, based on the famous [Neuecc's MessagePack](https://github.com/neuecc/MessagePack-CSharp). \ No newline at end of file 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 47c6dc85..a76b40ee 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.24.0 + 0.25.0 ZiggyCreatures.FusionCache.Serialization.NewtonsoftJson logo-128x128.png FusionCache serializer based on Newtonsoft Json.NET @@ -13,7 +13,7 @@ ZiggyCreatures.FusionCache.Serialization.NewtonsoftJson.xml README.md - - Update: dependencies + - Update: package dependencies diff --git a/src/ZiggyCreatures.FusionCache.Serialization.NewtonsoftJson/docs/README.md b/src/ZiggyCreatures.FusionCache.Serialization.NewtonsoftJson/docs/README.md index bfdc05ea..f3a60004 100644 --- a/src/ZiggyCreatures.FusionCache.Serialization.NewtonsoftJson/docs/README.md +++ b/src/ZiggyCreatures.FusionCache.Serialization.NewtonsoftJson/docs/README.md @@ -2,7 +2,7 @@ ![FusionCache logo](https://raw.githubusercontent.com/ZiggyCreatures/FusionCache/main/docs/logo-256x256.png) -### FusionCache is an easy to use, fast and robust cache with advanced resiliency features and an optional distributed 2nd layer. +### FusionCache is an easy to use, fast and robust cache with advanced resiliency features and an optional distributed 2nd level. It was born after years of dealing with all sorts of different types of caches: memory caching, distributed caching, http caching, CDNs, browser cache, offline cache, you name it. So I've tried to put together these experiences and came up with FusionCache. @@ -10,4 +10,4 @@ Find out [more](https://github.com/ZiggyCreatures/FusionCache). ## πŸ“¦ This package -This package is an implementation for a FusionCache serializer to be used with the optional distributed cache layer, based on [Newtonsoft Json.NET](https://www.newtonsoft.com/json). \ No newline at end of file +This package is an implementation for a FusionCache serializer to be used with the optional distributed cache level, based on [Newtonsoft Json.NET](https://www.newtonsoft.com/json). \ No newline at end of file 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 20b0b46a..bad98631 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.24.0 + 0.25.0 ZiggyCreatures.FusionCache.Serialization.ProtoBufNet logo-128x128.png FusionCache serializer based on protobuf-net @@ -13,7 +13,7 @@ ZiggyCreatures.FusionCache.Serialization.ProtoBufNet.xml README.md - - Update: dependencies + - Update: package dependencies @@ -23,7 +23,7 @@ - + diff --git a/src/ZiggyCreatures.FusionCache.Serialization.ProtoBufNet/docs/README.md b/src/ZiggyCreatures.FusionCache.Serialization.ProtoBufNet/docs/README.md index a2519642..34aad628 100644 --- a/src/ZiggyCreatures.FusionCache.Serialization.ProtoBufNet/docs/README.md +++ b/src/ZiggyCreatures.FusionCache.Serialization.ProtoBufNet/docs/README.md @@ -2,7 +2,7 @@ ![FusionCache logo](https://raw.githubusercontent.com/ZiggyCreatures/FusionCache/main/docs/logo-256x256.png) -### FusionCache is an easy to use, fast and robust cache with advanced resiliency features and an optional distributed 2nd layer. +### FusionCache is an easy to use, fast and robust cache with advanced resiliency features and an optional distributed 2nd level. It was born after years of dealing with all sorts of different types of caches: memory caching, distributed caching, http caching, CDNs, browser cache, offline cache, you name it. So I've tried to put together these experiences and came up with FusionCache. @@ -10,4 +10,4 @@ Find out [more](https://github.com/ZiggyCreatures/FusionCache). ## πŸ“¦ This package -This package is an implementation for a FusionCache serializer to be used with the optional distributed cache layer, based on [protobuf-net](https://github.com/protobuf-net/protobuf-net), one of the most used Protobuf serializer on .NET. \ No newline at end of file +This package is an implementation for a FusionCache serializer to be used with the optional distributed cache level, based on [protobuf-net](https://github.com/protobuf-net/protobuf-net), one of the most used Protobuf serializer on .NET. \ No newline at end of file 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 bae02a36..6f93e8f6 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.24.0 + 0.25.0 ZiggyCreatures.FusionCache.Serialization.ServiceStackJson logo-128x128.png FusionCache serializer based on ServiceStack's Json serializer @@ -13,7 +13,7 @@ ZiggyCreatures.FusionCache.Serialization.ServiceStackJson.xml README.md - - Update: dependencies + - Update: package dependencies @@ -27,7 +27,7 @@ - + diff --git a/src/ZiggyCreatures.FusionCache.Serialization.ServiceStackJson/docs/README.md b/src/ZiggyCreatures.FusionCache.Serialization.ServiceStackJson/docs/README.md index f47aecac..2ae2e911 100644 --- a/src/ZiggyCreatures.FusionCache.Serialization.ServiceStackJson/docs/README.md +++ b/src/ZiggyCreatures.FusionCache.Serialization.ServiceStackJson/docs/README.md @@ -2,7 +2,7 @@ ![FusionCache logo](https://raw.githubusercontent.com/ZiggyCreatures/FusionCache/main/docs/logo-256x256.png) -### FusionCache is an easy to use, fast and robust cache with advanced resiliency features and an optional distributed 2nd layer. +### FusionCache is an easy to use, fast and robust cache with advanced resiliency features and an optional distributed 2nd level. It was born after years of dealing with all sorts of different types of caches: memory caching, distributed caching, http caching, CDNs, browser cache, offline cache, you name it. So I've tried to put together these experiences and came up with FusionCache. @@ -10,4 +10,4 @@ Find out [more](https://github.com/ZiggyCreatures/FusionCache). ## πŸ“¦ This package -This package is an implementation for a FusionCache serializer to be used with the optional distributed cache layer, based on the JSON serializer by [ServiceStack](https://docs.servicestack.net/json-format). \ No newline at end of file +This package is an implementation for a FusionCache serializer to be used with the optional distributed cache level, based on the JSON serializer by [ServiceStack](https://docs.servicestack.net/json-format). \ No newline at end of file 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 e1707395..c8cfef40 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.24.0 + 0.25.0 ZiggyCreatures.FusionCache.Serialization.SystemTextJson logo-128x128.png FusionCache serializer based on System.Text.Json @@ -13,7 +13,7 @@ ZiggyCreatures.FusionCache.Serialization.SystemTextJson.xml README.md - - Update: dependencies + - Update: package dependencies @@ -27,7 +27,7 @@ - + diff --git a/src/ZiggyCreatures.FusionCache.Serialization.SystemTextJson/docs/README.md b/src/ZiggyCreatures.FusionCache.Serialization.SystemTextJson/docs/README.md index bc2a4aee..e56adbdc 100644 --- a/src/ZiggyCreatures.FusionCache.Serialization.SystemTextJson/docs/README.md +++ b/src/ZiggyCreatures.FusionCache.Serialization.SystemTextJson/docs/README.md @@ -2,7 +2,7 @@ ![FusionCache logo](https://raw.githubusercontent.com/ZiggyCreatures/FusionCache/main/docs/logo-256x256.png) -### FusionCache is an easy to use, fast and robust cache with advanced resiliency features and an optional distributed 2nd layer. +### FusionCache is an easy to use, fast and robust cache with advanced resiliency features and an optional distributed 2nd level. It was born after years of dealing with all sorts of different types of caches: memory caching, distributed caching, http caching, CDNs, browser cache, offline cache, you name it. So I've tried to put together these experiences and came up with FusionCache. @@ -10,4 +10,4 @@ Find out [more](https://github.com/ZiggyCreatures/FusionCache). ## πŸ“¦ This package -This package is an implementation for a FusionCache serializer to be used with the optional distributed cache layer, based on [System.Text.Json](https://docs.microsoft.com/en-us/dotnet/standard/serialization/system-text-json-how-to). \ No newline at end of file +This package is an implementation for a FusionCache serializer to be used with the optional distributed cache level, based on [System.Text.Json](https://docs.microsoft.com/en-us/dotnet/standard/serialization/system-text-json-how-to). \ No newline at end of file diff --git a/src/ZiggyCreatures.FusionCache/Events/FusionCacheBackplaneEventsHub.cs b/src/ZiggyCreatures.FusionCache/Events/FusionCacheBackplaneEventsHub.cs index c595ef3f..04366649 100644 --- a/src/ZiggyCreatures.FusionCache/Events/FusionCacheBackplaneEventsHub.cs +++ b/src/ZiggyCreatures.FusionCache/Events/FusionCacheBackplaneEventsHub.cs @@ -1,14 +1,16 @@ ο»Ώusing System; +using System.Collections.Generic; using Microsoft.Extensions.Logging; using ZiggyCreatures.Caching.Fusion.Backplane; using ZiggyCreatures.Caching.Fusion.Internals; +using ZiggyCreatures.Caching.Fusion.Internals.Diagnostics; namespace ZiggyCreatures.Caching.Fusion.Events; /// /// The events hub for events specific for the backplane. /// -public class FusionCacheBackplaneEventsHub +public sealed class FusionCacheBackplaneEventsHub : FusionCacheAbstractEventsHub { /// @@ -40,16 +42,22 @@ public FusionCacheBackplaneEventsHub(IFusionCache cache, FusionCacheOptions opti internal void OnCircuitBreakerChange(string? operationId, string? key, bool isClosed) { + Metrics.CounterBackplaneCircuitBreakerChange.Maybe()?.AddWithCommonTags(1, _cache.CacheName, _cache.InstanceId, new KeyValuePair("fusioncache.backplane.circuit_breaker.closed", isClosed)); + CircuitBreakerChange?.SafeExecute(operationId, key, _cache, () => new FusionCacheCircuitBreakerChangeEventArgs(isClosed), nameof(CircuitBreakerChange), _logger, _errorsLogLevel, _syncExecution); } internal void OnMessagePublished(string operationId, BackplaneMessage message) { + Metrics.CounterBackplanePublish.Maybe()?.AddWithCommonTags(1, _cache.CacheName, _cache.InstanceId, new KeyValuePair("fusioncache.backplane.message_action", message.Action.ToString())); + MessagePublished?.SafeExecute(operationId, message.CacheKey, _cache, () => new FusionCacheBackplaneMessageEventArgs(message), nameof(MessagePublished), _logger, _errorsLogLevel, _syncExecution); } internal void OnMessageReceived(string operationId, BackplaneMessage message) { + Metrics.CounterBackplaneReceive.Maybe()?.AddWithCommonTags(1, _cache.CacheName, _cache.InstanceId, new KeyValuePair("fusioncache.backplane.message_action", message.Action.ToString())); + MessageReceived?.SafeExecute(operationId, message.CacheKey, _cache, () => new FusionCacheBackplaneMessageEventArgs(message), nameof(MessageReceived), _logger, _errorsLogLevel, _syncExecution); } } diff --git a/src/ZiggyCreatures.FusionCache/Events/FusionCacheCommonEventsHub.cs b/src/ZiggyCreatures.FusionCache/Events/FusionCacheCommonEventsHub.cs index b485f4f7..b5704217 100644 --- a/src/ZiggyCreatures.FusionCache/Events/FusionCacheCommonEventsHub.cs +++ b/src/ZiggyCreatures.FusionCache/Events/FusionCacheCommonEventsHub.cs @@ -5,7 +5,7 @@ namespace ZiggyCreatures.Caching.Fusion.Events; /// -/// A class with base events that are common to any cache layer (general, memroy or distributed) +/// A class with base events that are common to any cache level (general, memroy or distributed) /// public abstract class FusionCacheCommonEventsHub : FusionCacheAbstractEventsHub @@ -42,22 +42,22 @@ protected FusionCacheCommonEventsHub(IFusionCache cache, FusionCacheOptions opti /// public event EventHandler? Remove; - internal void OnHit(string operationId, string key, bool isStale) + internal virtual void OnHit(string operationId, string key, bool isStale) { Hit?.SafeExecute(operationId, key, _cache, () => new FusionCacheEntryHitEventArgs(key, isStale), nameof(Hit), _logger, _errorsLogLevel, _syncExecution); } - internal void OnMiss(string operationId, string key) + internal virtual void OnMiss(string operationId, string key) { Miss?.SafeExecute(operationId, key, _cache, () => new FusionCacheEntryEventArgs(key), nameof(Miss), _logger, _errorsLogLevel, _syncExecution); } - internal void OnSet(string operationId, string key) + internal virtual void OnSet(string operationId, string key) { Set?.SafeExecute(operationId, key, _cache, () => new FusionCacheEntryEventArgs(key), nameof(Set), _logger, _errorsLogLevel, _syncExecution); } - internal void OnRemove(string operationId, string key) + internal virtual void OnRemove(string operationId, string key) { Remove?.SafeExecute(operationId, key, _cache, () => new FusionCacheEntryEventArgs(key), nameof(Remove), _logger, _errorsLogLevel, _syncExecution); } diff --git a/src/ZiggyCreatures.FusionCache/Events/FusionCacheDistributedEventsHub.cs b/src/ZiggyCreatures.FusionCache/Events/FusionCacheDistributedEventsHub.cs index 581ca333..40fa4440 100644 --- a/src/ZiggyCreatures.FusionCache/Events/FusionCacheDistributedEventsHub.cs +++ b/src/ZiggyCreatures.FusionCache/Events/FusionCacheDistributedEventsHub.cs @@ -1,13 +1,15 @@ ο»Ώusing System; +using System.Collections.Generic; using Microsoft.Extensions.Logging; using ZiggyCreatures.Caching.Fusion.Internals; +using ZiggyCreatures.Caching.Fusion.Internals.Diagnostics; namespace ZiggyCreatures.Caching.Fusion.Events; /// -/// The events hub for events specific for the distributed layer. +/// The events hub for events specific for the distributed level. /// -public class FusionCacheDistributedEventsHub +public sealed class FusionCacheDistributedEventsHub : FusionCacheCommonEventsHub { /// @@ -38,16 +40,50 @@ public FusionCacheDistributedEventsHub(IFusionCache cache, FusionCacheOptions op internal void OnCircuitBreakerChange(string? operationId, string? key, bool isClosed) { + Metrics.CounterDistributedCircuitBreakerChange.Maybe()?.AddWithCommonTags(1, _cache.CacheName, _cache.InstanceId, new KeyValuePair("fusioncache.distributed.circuit_breaker.closed", isClosed)); + CircuitBreakerChange?.SafeExecute(operationId, key, _cache, () => new FusionCacheCircuitBreakerChangeEventArgs(isClosed), nameof(CircuitBreakerChange), _logger, _errorsLogLevel, _syncExecution); } internal void OnSerializationError(string? operationId, string? key) { + Metrics.CounterSerializationError.Maybe()?.AddWithCommonTags(1, _cache.CacheName, _cache.InstanceId); + SerializationError?.SafeExecute(operationId, key, _cache, () => new FusionCacheEntryEventArgs(key ?? string.Empty), nameof(SerializationError), _logger, _errorsLogLevel, _syncExecution); } internal void OnDeserializationError(string? operationId, string? key) { + Metrics.CounterDeserializationError.Maybe()?.AddWithCommonTags(1, _cache.CacheName, _cache.InstanceId); + DeserializationError?.SafeExecute(operationId, key, _cache, () => new FusionCacheEntryEventArgs(key ?? string.Empty), nameof(DeserializationError), _logger, _errorsLogLevel, _syncExecution); } + + internal override void OnHit(string operationId, string key, bool isStale) + { + Metrics.CounterDistributedHit.Maybe()?.AddWithCommonTags(1, _cache.CacheName, _cache.InstanceId, new KeyValuePair("fusioncache.stale", isStale)); + + base.OnHit(operationId, key, isStale); + } + + internal override void OnMiss(string operationId, string key) + { + Metrics.CounterDistributedMiss.Maybe()?.AddWithCommonTags(1, _cache.CacheName, _cache.InstanceId); + + base.OnMiss(operationId, key); + } + + internal override void OnSet(string operationId, string key) + { + Metrics.CounterDistributedSet.Maybe()?.AddWithCommonTags(1, _cache.CacheName, _cache.InstanceId); + + base.OnSet(operationId, key); + } + + internal override void OnRemove(string operationId, string key) + { + Metrics.CounterDistributedRemove.Maybe()?.AddWithCommonTags(1, _cache.CacheName, _cache.InstanceId); + + base.OnRemove(operationId, key); + } } diff --git a/src/ZiggyCreatures.FusionCache/Events/FusionCacheEventsHub.cs b/src/ZiggyCreatures.FusionCache/Events/FusionCacheEventsHub.cs index a70ae6b6..9b4d45ee 100644 --- a/src/ZiggyCreatures.FusionCache/Events/FusionCacheEventsHub.cs +++ b/src/ZiggyCreatures.FusionCache/Events/FusionCacheEventsHub.cs @@ -1,6 +1,8 @@ ο»Ώusing System; +using System.Collections.Generic; using Microsoft.Extensions.Logging; using ZiggyCreatures.Caching.Fusion.Internals; +using ZiggyCreatures.Caching.Fusion.Internals.Diagnostics; namespace ZiggyCreatures.Caching.Fusion.Events; @@ -25,12 +27,12 @@ public FusionCacheEventsHub(IFusionCache cache, FusionCacheOptions options, ILog } /// - /// The events hub for the memory layer. + /// The events hub for the memory level. /// public FusionCacheMemoryEventsHub Memory { get; } /// - /// The events hub for the distributed layer. + /// The events hub for the distributed level. /// public FusionCacheDistributedEventsHub Distributed { get; } @@ -81,41 +83,85 @@ public FusionCacheEventsHub(IFusionCache cache, FusionCacheOptions options, ILog internal void OnFailSafeActivate(string operationId, string key) { + Metrics.CounterFailSafeActivate.Maybe()?.AddWithCommonTags(1, _cache.CacheName, _cache.InstanceId); + FailSafeActivate?.SafeExecute(operationId, key, _cache, () => new FusionCacheEntryEventArgs(key), nameof(FailSafeActivate), _logger, _errorsLogLevel, _syncExecution); } internal void OnFactorySyntheticTimeout(string operationId, string key) { + Metrics.CounterFactorySyntheticTimeout.Maybe()?.AddWithCommonTags(1, _cache.CacheName, _cache.InstanceId); + FactorySyntheticTimeout?.SafeExecute(operationId, key, _cache, () => new FusionCacheEntryEventArgs(key), nameof(FactorySyntheticTimeout), _logger, _errorsLogLevel, _syncExecution); } internal void OnFactoryError(string operationId, string key) { + Metrics.CounterFactoryError.Maybe()?.AddWithCommonTags(1, _cache.CacheName, _cache.InstanceId, new KeyValuePair("fusioncache.operation.background", false)); + FactoryError?.SafeExecute(operationId, key, _cache, () => new FusionCacheEntryEventArgs(key), nameof(FactoryError), _logger, _errorsLogLevel, _syncExecution); } internal void OnFactorySuccess(string operationId, string key) { + Metrics.CounterFactorySuccess.Maybe()?.AddWithCommonTags(1, _cache.CacheName, _cache.InstanceId, new KeyValuePair("fusioncache.operation.background", false)); + FactorySuccess?.SafeExecute(operationId, key, _cache, () => new FusionCacheEntryEventArgs(key), nameof(FactorySuccess), _logger, _errorsLogLevel, _syncExecution); } internal void OnBackgroundFactoryError(string operationId, string key) { + Metrics.CounterFactoryError.Maybe()?.AddWithCommonTags(1, _cache.CacheName, _cache.InstanceId, new KeyValuePair("fusioncache.operation.background", true)); + BackgroundFactoryError?.SafeExecute(operationId, key, _cache, () => new FusionCacheEntryEventArgs(key), nameof(BackgroundFactoryError), _logger, _errorsLogLevel, _syncExecution); } internal void OnBackgroundFactorySuccess(string operationId, string key) { + Metrics.CounterFactorySuccess.Maybe()?.AddWithCommonTags(1, _cache.CacheName, _cache.InstanceId, new KeyValuePair("fusioncache.operation.background", true)); + BackgroundFactorySuccess?.SafeExecute(operationId, key, _cache, () => new FusionCacheEntryEventArgs(key), nameof(BackgroundFactorySuccess), _logger, _errorsLogLevel, _syncExecution); } internal void OnEagerRefresh(string operationId, string key) { + Metrics.CounterEagerRefresh.Maybe()?.AddWithCommonTags(1, _cache.CacheName, _cache.InstanceId); + EagerRefresh?.SafeExecute(operationId, key, _cache, () => new FusionCacheEntryEventArgs(key), nameof(EagerRefresh), _logger, _errorsLogLevel, _syncExecution); } internal void OnExpire(string operationId, string key) { + Metrics.CounterExpire.Maybe()?.AddWithCommonTags(1, _cache.CacheName, _cache.InstanceId); + Expire?.SafeExecute(operationId, key, _cache, () => new FusionCacheEntryEventArgs(key), nameof(Expire), _logger, _errorsLogLevel, _syncExecution); } + + internal override void OnHit(string operationId, string key, bool isStale) + { + Metrics.CounterHit.Maybe()?.AddWithCommonTags(1, _cache.CacheName, _cache.InstanceId, new KeyValuePair("fusioncache.stale", isStale)); + + base.OnHit(operationId, key, isStale); + } + + internal override void OnMiss(string operationId, string key) + { + Metrics.CounterMiss.Maybe()?.AddWithCommonTags(1, _cache.CacheName, _cache.InstanceId); + + base.OnMiss(operationId, key); + } + + internal override void OnSet(string operationId, string key) + { + Metrics.CounterSet.Maybe()?.AddWithCommonTags(1, _cache.CacheName, _cache.InstanceId); + + base.OnSet(operationId, key); + } + + internal override void OnRemove(string operationId, string key) + { + Metrics.CounterRemove.Maybe()?.AddWithCommonTags(1, _cache.CacheName, _cache.InstanceId); + + base.OnRemove(operationId, key); + } } diff --git a/src/ZiggyCreatures.FusionCache/Events/FusionCacheMemoryEventsHub.cs b/src/ZiggyCreatures.FusionCache/Events/FusionCacheMemoryEventsHub.cs index 2e165ce9..79b615a7 100644 --- a/src/ZiggyCreatures.FusionCache/Events/FusionCacheMemoryEventsHub.cs +++ b/src/ZiggyCreatures.FusionCache/Events/FusionCacheMemoryEventsHub.cs @@ -1,14 +1,16 @@ ο»Ώusing System; +using System.Collections.Generic; using Microsoft.Extensions.Caching.Memory; using Microsoft.Extensions.Logging; using ZiggyCreatures.Caching.Fusion.Internals; +using ZiggyCreatures.Caching.Fusion.Internals.Diagnostics; namespace ZiggyCreatures.Caching.Fusion.Events; /// -/// The events hub for events specific for the memory layer. +/// The events hub for events specific for the memory level. /// -public class FusionCacheMemoryEventsHub +public sealed class FusionCacheMemoryEventsHub : FusionCacheCommonEventsHub { /// @@ -43,11 +45,43 @@ public bool HasEvictionSubscribers() internal void OnEviction(string operationId, string key, EvictionReason reason, object? value) { + Metrics.CounterMemoryEvict.Maybe()?.AddWithCommonTags(1, _cache.CacheName, _cache.InstanceId); + Eviction?.SafeExecute(operationId, key, _cache, () => new FusionCacheEntryEvictionEventArgs(key, reason, value), nameof(Eviction), _logger, _errorsLogLevel, _syncExecution); } internal void OnExpire(string operationId, string key) { + Metrics.CounterMemoryExpire.Maybe()?.AddWithCommonTags(1, _cache.CacheName, _cache.InstanceId); + Expire?.SafeExecute(operationId, key, _cache, () => new FusionCacheEntryEventArgs(key), nameof(Expire), _logger, _errorsLogLevel, _syncExecution); } + + internal override void OnHit(string operationId, string key, bool isStale) + { + Metrics.CounterMemoryHit.Maybe()?.AddWithCommonTags(1, _cache.CacheName, _cache.InstanceId, new KeyValuePair("fusioncache.stale", isStale)); + + base.OnHit(operationId, key, isStale); + } + + internal override void OnMiss(string operationId, string key) + { + Metrics.CounterMemoryMiss.Maybe()?.AddWithCommonTags(1, _cache.CacheName, _cache.InstanceId); + + base.OnMiss(operationId, key); + } + + internal override void OnSet(string operationId, string key) + { + Metrics.CounterMemorySet.Maybe()?.AddWithCommonTags(1, _cache.CacheName, _cache.InstanceId); + + base.OnSet(operationId, key); + } + + internal override void OnRemove(string operationId, string key) + { + Metrics.CounterMemoryRemove.Maybe()?.AddWithCommonTags(1, _cache.CacheName, _cache.InstanceId); + + base.OnRemove(operationId, key); + } } diff --git a/src/ZiggyCreatures.FusionCache/FusionCache.cs b/src/ZiggyCreatures.FusionCache/FusionCache.cs index 3db2832c..1feeb615 100644 --- a/src/ZiggyCreatures.FusionCache/FusionCache.cs +++ b/src/ZiggyCreatures.FusionCache/FusionCache.cs @@ -1,6 +1,7 @@ ο»Ώusing System; using System.Collections.Concurrent; using System.Collections.Generic; +using System.ComponentModel; using System.Diagnostics; using System.Reflection; using System.Runtime.CompilerServices; @@ -16,8 +17,10 @@ using ZiggyCreatures.Caching.Fusion.Internals; using ZiggyCreatures.Caching.Fusion.Internals.AutoRecovery; using ZiggyCreatures.Caching.Fusion.Internals.Backplane; +using ZiggyCreatures.Caching.Fusion.Internals.Diagnostics; using ZiggyCreatures.Caching.Fusion.Internals.Distributed; using ZiggyCreatures.Caching.Fusion.Internals.Memory; +using ZiggyCreatures.Caching.Fusion.Locking; using ZiggyCreatures.Caching.Fusion.Plugins; using ZiggyCreatures.Caching.Fusion.Reactors; using ZiggyCreatures.Caching.Fusion.Serialization; @@ -34,16 +37,16 @@ public partial class FusionCache private readonly FusionCacheOptions _options; private readonly string? _cacheKeyPrefix; private readonly ILogger? _logger; - private IFusionCacheReactor _reactor; + private IFusionCacheMemoryLocker _memoryLocker; private MemoryCacheAccessor _mca; private DistributedCacheAccessor? _dca; private BackplaneAccessor? _bpa; private readonly object _backplaneLock = new object(); + private AutoRecoveryService _autoRecovery; private FusionCacheEventsHub _events; private readonly List _plugins; - private AutoRecoveryService _autoRecovery; - private FusionCacheEntryOptions _tryUpdateOptions; + private readonly FusionCacheEntryOptions _tryUpdateOptions; private static readonly MethodInfo __methodInfoTryUpdateMemoryEntryFromDistributedEntryAsyncOpenGeneric = typeof(FusionCache).GetMethod(nameof(TryUpdateMemoryEntryFromDistributedEntryAsync), BindingFlags.NonPublic | BindingFlags.Instance); private static readonly ConcurrentDictionary __methodInfoTryUpdateMemoryEntryFromDistributedEntryAsyncCache = new ConcurrentDictionary(); @@ -54,7 +57,22 @@ public partial class FusionCache /// The instance to use. If null, one will be automatically created and managed. /// The instance to use. If null, logging will be completely disabled. /// The instance to use (advanced). If null, a standard one will be automatically created and managed. - public FusionCache(IOptions optionsAccessor, IMemoryCache? memoryCache = null, ILogger? logger = null, IFusionCacheReactor? reactor = null) + [EditorBrowsable(EditorBrowsableState.Never)] + [Obsolete("Please stop using this constructor, it will be removed in future versions.")] + public FusionCache(IOptions optionsAccessor, IMemoryCache? memoryCache, ILogger? logger, IFusionCacheReactor? reactor) + : this(optionsAccessor, memoryCache, logger, (IFusionCacheMemoryLocker?)null) + { + // EMPTY + } + + /// + /// Creates a new instance. + /// + /// The set of cache-wide options to use with this instance of . + /// The instance to use. If null, one will be automatically created and managed. + /// The instance to use. If null, logging will be completely disabled. + /// The instance to use. If , a standard one will be automatically created and managed. + public FusionCache(IOptions optionsAccessor, IMemoryCache? memoryCache = null, ILogger? logger = null, IFusionCacheMemoryLocker? memoryLocker = null) { if (optionsAccessor is null) throw new ArgumentNullException(nameof(optionsAccessor)); @@ -88,14 +106,14 @@ public FusionCache(IOptions optionsAccessor, IMemoryCache? m _logger = logger; } - // REACTOR - _reactor = reactor ?? new FusionCacheReactorStandard(); + // MEMORY LOCKER + _memoryLocker = memoryLocker ?? new StandardMemoryLocker(); // EVENTS _events = new FusionCacheEventsHub(this, _options, _logger); // PLUGINS - _plugins = new List(); + _plugins = []; // MEMORY CACHE _mca = new MemoryCacheAccessor(memoryCache, _options, _logger, _events.Memory); @@ -263,15 +281,41 @@ private bool TryPickFailSafeFallbackValue(string operationId, string key return false; } - private void MaybeBackgroundCompleteTimedOutFactory(string operationId, string key, FusionCacheFactoryExecutionContext ctx, Task? factoryTask, FusionCacheEntryOptions options, CancellationToken token) + private void MaybeBackgroundCompleteTimedOutFactory(string operationId, string key, FusionCacheFactoryExecutionContext ctx, Task? factoryTask, FusionCacheEntryOptions options, Activity? activity, CancellationToken token) { - if (factoryTask is null || options.AllowTimedOutFactoryBackgroundCompletion == false) + if (factoryTask is null) + { + // ACTIVITY + activity?.Dispose(); + + return; + } + + if (factoryTask.IsFaulted) + { + // ACTIVITY + activity?.SetStatus(ActivityStatusCode.Error, factoryTask?.Exception?.Message); + activity?.Dispose(); + return; + } + + if (options.AllowTimedOutFactoryBackgroundCompletion == false) + { + // ACTIVITY + activity?.AddEvent(new ActivityEvent(Activities.EventNames.FactoryBackgroundMoveNotAllowed)); + activity?.Dispose(); - CompleteBackgroundFactory(operationId, key, ctx, factoryTask, options, null, token); + return; + } + + token.ThrowIfCancellationRequested(); + + activity?.AddEvent(new ActivityEvent(Activities.EventNames.FactoryBackgroundMove)); + CompleteBackgroundFactory(operationId, key, ctx, factoryTask, options, null, activity, token); } - private void CompleteBackgroundFactory(string operationId, string key, FusionCacheFactoryExecutionContext ctx, Task factoryTask, FusionCacheEntryOptions options, object? lockObj, CancellationToken token) + private void CompleteBackgroundFactory(string operationId, string key, FusionCacheFactoryExecutionContext ctx, Task factoryTask, FusionCacheEntryOptions options, object? memoryLockObj, Activity? activity, CancellationToken token) { if (factoryTask.IsFaulted) { @@ -285,7 +329,13 @@ private void CompleteBackgroundFactory(string operationId, string key, F } finally { - ReleaseLock(operationId, key, lockObj); + // MEMORY LOCK + if (memoryLockObj is not null) + ReleaseMemoryLock(operationId, key, memoryLockObj); + + // ACTIVITY + activity?.SetStatus(ActivityStatusCode.Error, factoryTask?.Exception?.Message); + activity?.Dispose(); } return; @@ -304,6 +354,10 @@ private void CompleteBackgroundFactory(string operationId, string key, F if (_logger?.IsEnabled(_options.FactoryErrorsLogLevel) ?? false) _logger.Log(_options.FactoryErrorsLogLevel, antecedent.Exception.GetSingleInnerExceptionOrSelf(), "FUSION [N={CacheName} I={CacheInstanceId}] (O={CacheOperationId} K={CacheKey}): a background factory thrown an exception", CacheName, InstanceId, operationId, key); + // ACTIVITY + activity?.SetStatus(ActivityStatusCode.Error, factoryTask?.Exception?.Message); + activity?.Dispose(); + // EVENT _events.OnBackgroundFactoryError(operationId, key); } @@ -312,6 +366,9 @@ private void CompleteBackgroundFactory(string operationId, string key, F if (_logger?.IsEnabled(LogLevel.Debug) ?? false) _logger.Log(LogLevel.Debug, "FUSION [N={CacheName} I={CacheInstanceId}] (O={CacheOperationId} K={CacheKey}): a background factory successfully completed, keeping the result", CacheName, InstanceId, operationId, key); + // ACTIVITY + activity?.Dispose(); + // UPDATE ADAPTIVE OPTIONS var maybeNewOptions = ctx.GetOptions(); if (maybeNewOptions is not null && options != maybeNewOptions) @@ -342,30 +399,32 @@ private void CompleteBackgroundFactory(string operationId, string key, F } finally { - ReleaseLock(operationId, key, lockObj); + // MEMORY LOCK + if (memoryLockObj is not null) + ReleaseMemoryLock(operationId, key, memoryLockObj); } }); } - private void ReleaseLock(string operationId, string key, object? lockObj) + private void ReleaseMemoryLock(string operationId, string key, object? memoryLockObj) { - if (lockObj is null) + if (memoryLockObj is null) return; if (_logger?.IsEnabled(LogLevel.Trace) ?? false) - _logger.Log(LogLevel.Trace, "FUSION [N={CacheName} I={CacheInstanceId}] (O={CacheOperationId} K={CacheKey}): releasing LOCK", CacheName, InstanceId, operationId, key); + _logger.Log(LogLevel.Trace, "FUSION [N={CacheName} I={CacheInstanceId}] (O={CacheOperationId} K={CacheKey}): releasing MEMORY LOCK", CacheName, InstanceId, operationId, key); try { - _reactor.ReleaseLock(CacheName, InstanceId, key, operationId, lockObj, _logger); + _memoryLocker.ReleaseLock(CacheName, InstanceId, key, operationId, memoryLockObj, _logger); if (_logger?.IsEnabled(LogLevel.Trace) ?? false) - _logger.Log(LogLevel.Trace, "FUSION [N={CacheName} I={CacheInstanceId}] (O={CacheOperationId} K={CacheKey}): LOCK released", CacheName, InstanceId, operationId, key); + _logger.Log(LogLevel.Trace, "FUSION [N={CacheName} I={CacheInstanceId}] (O={CacheOperationId} K={CacheKey}): MEMORY LOCK released", CacheName, InstanceId, operationId, key); } catch (Exception exc) { if (_logger?.IsEnabled(LogLevel.Warning) ?? false) - _logger.Log(LogLevel.Warning, exc, "FUSION [N={CacheName} I={CacheInstanceId}] (O={CacheOperationId} K={CacheKey}): releasing the LOCK has thrown an exception", CacheName, InstanceId, operationId, key); + _logger.Log(LogLevel.Warning, exc, "FUSION [N={CacheName} I={CacheInstanceId}] (O={CacheOperationId} K={CacheKey}): releasing the MEMORY LOCK has thrown an exception", CacheName, InstanceId, operationId, key); } } @@ -623,8 +682,8 @@ protected virtual void Dispose(bool disposing) _autoRecovery.Dispose(); _autoRecovery = null; - _reactor.Dispose(); - _reactor = null; + _memoryLocker.Dispose(); + _memoryLocker = null; _mca.Dispose(); _mca = null; @@ -765,6 +824,7 @@ internal bool MustAwaitBackplaneOperations(FusionCacheEntryOptions options) if (_logger?.IsEnabled(LogLevel.Trace) ?? false) _logger.Log(LogLevel.Trace, "FUSION [N={CacheName} I={CacheInstanceId}] (O={CacheOperationId} K={CacheKey}): memory entry updated from distributed", CacheName, InstanceId, operationId, cacheKey); + // EVENT _events.Memory.OnSet(operationId, cacheKey); return (false, false, true); diff --git a/src/ZiggyCreatures.FusionCache/FusionCacheBuilderExtMethods.cs b/src/ZiggyCreatures.FusionCache/FusionCacheBuilderExtMethods.cs index c8a90eaa..b46ecf22 100644 --- a/src/ZiggyCreatures.FusionCache/FusionCacheBuilderExtMethods.cs +++ b/src/ZiggyCreatures.FusionCache/FusionCacheBuilderExtMethods.cs @@ -3,6 +3,7 @@ using Microsoft.Extensions.Caching.Memory; using Microsoft.Extensions.Logging; using ZiggyCreatures.Caching.Fusion.Backplane; +using ZiggyCreatures.Caching.Fusion.Locking; using ZiggyCreatures.Caching.Fusion.Plugins; using ZiggyCreatures.Caching.Fusion.Serialization; @@ -340,7 +341,7 @@ public static IFusionCacheBuilder WithMemoryCache(this IFusionCacheBuilder build /// DOCS: /// /// The to act upon. - /// The factory used to create the serializer, with access to the . + /// The factory used to create the memory cache, with access to the . /// The so that additional calls can be chained. public static IFusionCacheBuilder WithMemoryCache(this IFusionCacheBuilder builder, Func factory) { @@ -360,6 +361,138 @@ public static IFusionCacheBuilder WithMemoryCache(this IFusionCacheBuilder build #endregion + #region MEMORY LOCKER + + /// + /// The standard implementation of an will be used (this is the default behaviour). + ///

+ /// DOCS: + ///
+ /// The to act upon. + /// The so that additional calls can be chained. + public static IFusionCacheBuilder WithStandardMemoryLocker(this IFusionCacheBuilder builder) + { + if (builder is null) + throw new ArgumentNullException(nameof(builder)); + + builder.UseRegisteredMemoryLocker = false; + builder.MemoryLocker = null; + builder.MemoryLockerFactory = null; + builder.ThrowIfMissingMemoryLocker = false; + + return builder; + } + + /// + /// The builder will look for an service registered in the DI container and use it, and throws if it cannot find one. + ///

+ /// DOCS: + ///
+ /// The to act upon. + /// The so that additional calls can be chained. + public static IFusionCacheBuilder WithRegisteredMemoryLocker(this IFusionCacheBuilder builder) + { + if (builder is null) + throw new ArgumentNullException(nameof(builder)); + + builder.UseRegisteredMemoryLocker = true; + builder.MemoryLocker = null; + builder.MemoryLockerFactory = null; + builder.ThrowIfMissingMemoryLocker = true; + + return builder; + } + + /// + /// Indicates if the builder should try to find and use an service registered in the DI container. + ///

+ /// DOCS: + ///
+ /// The to act upon. + /// The so that additional calls can be chained. + public static IFusionCacheBuilder TryWithRegisteredMemoryLocker(this IFusionCacheBuilder builder) + { + if (builder is null) + throw new ArgumentNullException(nameof(builder)); + + builder.WithRegisteredMemoryLocker(); + builder.ThrowIfMissingMemoryLocker = false; + + return builder; + } + + /// + /// Specify a custom instance to be used. + ///

+ /// DOCS: + ///
+ /// The to act upon. + /// The instance to use. + /// The so that additional calls can be chained. + public static IFusionCacheBuilder WithMemoryLocker(this IFusionCacheBuilder builder, IFusionCacheMemoryLocker memoryLocker) + { + if (builder is null) + throw new ArgumentNullException(nameof(builder)); + + if (memoryLocker is null) + throw new ArgumentNullException(nameof(memoryLocker)); + + builder.UseRegisteredMemoryLocker = false; + builder.MemoryLocker = memoryLocker; + builder.MemoryLockerFactory = null; + builder.ThrowIfMissingMemoryLocker = true; + + return builder; + } + + /// + /// Specify a custom factory to be used. + ///

+ /// DOCS: + ///
+ /// The to act upon. + /// The factory used to create the memory locker, with access to the . + /// The so that additional calls can be chained. + public static IFusionCacheBuilder WithMemoryLocker(this IFusionCacheBuilder builder, Func factory) + { + if (builder is null) + throw new ArgumentNullException(nameof(builder)); + + if (factory is null) + throw new ArgumentNullException(nameof(factory)); + + builder.UseRegisteredMemoryLocker = false; + builder.MemoryLocker = null; + builder.MemoryLockerFactory = factory; + builder.ThrowIfMissingMemoryLocker = true; + + return builder; + } + + ///// + ///// Indicates that the builder should not use a memory locker at all. + /////

+ ///// ⚠️ WARNING: if you don't use any memory locker at all, you will NOT be protected from cache stampede. + /////

+ ///// DOCS: + /////
+ ///// + ///// The so that additional calls can be chained. + //public static IFusionCacheBuilder WithoutMemoryLocker(this IFusionCacheBuilder builder) + //{ + // if (builder is null) + // throw new ArgumentNullException(nameof(builder)); + + // builder.UseRegisteredMemoryLocker = false; + // builder.MemoryLocker = null; + // builder.MemoryLockerFactory = null; + // builder.ThrowIfMissingMemoryLocker = false; + + // return builder; + //} + + #endregion + #region SERIALIZER /// diff --git a/src/ZiggyCreatures.FusionCache/FusionCacheDiagnostics.cs b/src/ZiggyCreatures.FusionCache/FusionCacheDiagnostics.cs new file mode 100644 index 00000000..8d95dfe5 --- /dev/null +++ b/src/ZiggyCreatures.FusionCache/FusionCacheDiagnostics.cs @@ -0,0 +1,54 @@ +ο»Ώnamespace ZiggyCreatures.Caching.Fusion +{ + /// + /// A support class for FusionCache diagnostics. + /// + public static class FusionCacheDiagnostics + { + /// + /// The current version of FusionCache. + /// + public const string FusionCacheVersion = "0.25.0"; + + /// + /// The activity source name for FusionCache. + /// + public const string ActivitySourceName = "ZiggyCreatures.Caching.Fusion"; + + /// + /// The activity source name for the FusionCache memory level. + /// + public const string ActivitySourceNameMemoryLevel = "ZiggyCreatures.Caching.Fusion.Memory"; + + /// + /// The activity source name for the FusionCache distributed level. + /// + public const string ActivitySourceNameDistributedLevel = "ZiggyCreatures.Caching.Fusion.Distributed"; + + /// + /// The activity source name for the FusionCache backplane. + /// + public const string ActivitySourceNameBackplane = "ZiggyCreatures.Caching.Fusion.Backplane"; + + + /// + /// The meter name for FusionCache. + /// + public const string MeterName = "ZiggyCreatures.Caching.Fusion"; + + /// + /// The meter name for the FusionCache memory level. + /// + public const string MeterNameMemoryLevel = "ZiggyCreatures.Caching.Fusion.Memory"; + + /// + /// The meter name for the FusionCache distributed level. + /// + public const string MeterNameDistributedLevel = "ZiggyCreatures.Caching.Fusion.Distributed"; + + /// + /// The meter name for the FusionCache backplane. + /// + public const string MeterNameBackplane = "ZiggyCreatures.Caching.Fusion.Backplane"; + } +} diff --git a/src/ZiggyCreatures.FusionCache/FusionCacheEntryOptions.cs b/src/ZiggyCreatures.FusionCache/FusionCacheEntryOptions.cs index da781d5c..99b4cb45 100644 --- a/src/ZiggyCreatures.FusionCache/FusionCacheEntryOptions.cs +++ b/src/ZiggyCreatures.FusionCache/FusionCacheEntryOptions.cs @@ -96,7 +96,7 @@ public float? EagerRefreshThreshold } /// - /// The timeout to apply when trying to acquire a lock during a factory execution. + /// The timeout to apply when trying to acquire a memory lock during a factory execution. ///

/// DOCS: ///
@@ -205,7 +205,7 @@ public float? EagerRefreshThreshold public TimeSpan DistributedCacheHardTimeout { get; set; } /// - /// Even if the distributed cache is a secondary layer, by default every operation on it (get/set/remove/etc) is blocking: that is to say the FusionCache method call would not return until the inner distributed cache operation is completed. + /// Even if the distributed cache is a secondary level, by default every operation on it (get/set/remove/etc) is blocking: that is to say the FusionCache method call would not return until the inner distributed cache operation is completed. ///
/// This is to avoid rare edge cases like saving a value in the cache and immediately cheking the underlying distributed cache directly, not finding the value (because it is still being saved): very very rare, but still. ///
@@ -241,7 +241,7 @@ public float? EagerRefreshThreshold /// OBSOLETE NOW: ///
[EditorBrowsable(EditorBrowsableState.Never)] - [Obsolete("Please use the SkipBackplaneNotifications option and invert the value: EnableBackplaneNotifications = true is the same as SkipBackplaneNotifications = false", true)] + [Obsolete("Please use the SkipBackplaneNotifications option and invert the value: EnableBackplaneNotifications = true is the same as SkipBackplaneNotifications = false")] public bool EnableBackplaneNotifications { get { return !SkipBackplaneNotifications; } @@ -284,9 +284,9 @@ public bool EnableBackplaneNotifications public bool SkipDistributedCache { get; set; } /// - /// 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. + /// When a 2nd level (distributed cache) is used and a cache entry in the 1st level (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. ///

- /// There are situations though, like in a mobile app with a SQLite 2nd layer, where the 2nd layer is not really "distributed" but just "out of process" (to ease cold starts): in situations like this noone can have updated the 2nd layer, so we can skip that extra read for a perf boost (of course the write part will still be done). + /// There are situations though, like in a mobile app with a SQLite 2nd level, where the 2nd level is not really "distributed" but just "out of process" (to ease cold starts): in situations like this noone can have updated the 2nd level, so we can skip that extra read for a perf boost (of course the write part will still be done). ///

/// TL/DR: if your 2nd level is not "distributed" but only "out of process", setting this to can give you a nice performance boost. ///

@@ -297,7 +297,7 @@ public bool EnableBackplaneNotifications /// /// 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. + /// 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 level (distributed cache) and a lot of extra network traffic. ///

/// DOCS: ///
@@ -545,7 +545,7 @@ public FusionCacheEntryOptions SetDistributedCacheTimeouts(TimeSpan? softTimeout /// Set the property. /// The so that additional calls can be chained. [EditorBrowsable(EditorBrowsableState.Never)] - [Obsolete("Please use the SetSkipBackplaneNotifications method and invert the value: EnableBackplaneNotifications = true is the same as SkipBackplaneNotifications = false", true)] + [Obsolete("Please use the SetSkipBackplaneNotifications method and invert the value: EnableBackplaneNotifications = true is the same as SkipBackplaneNotifications = false")] public FusionCacheEntryOptions SetBackplane(bool enableBackplaneNotifications) { return SetSkipBackplaneNotifications(!enableBackplaneNotifications); @@ -596,7 +596,7 @@ public FusionCacheEntryOptions SetSkipDistributedCacheReadWhenStale(bool skip) /// /// 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. + /// 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 level (distributed cache) and a lot of extra network traffic. ///

/// DOCS: ///
@@ -650,7 +650,7 @@ internal MemoryCacheEntryOptions ToMemoryCacheEntryOptions(FusionCacheMemoryEven res.RegisterPostEvictionCallback( (key, entry, reason, state) => { - ((FusionCacheMemoryEventsHub)state)?.OnEviction(string.Empty, key.ToString(), reason, ((FusionCacheMemoryEntry?)entry)?.Value); + ((FusionCacheMemoryEventsHub?)state)?.OnEviction(string.Empty, key.ToString(), reason, ((FusionCacheMemoryEntry?)entry)?.Value); }, events ); @@ -706,22 +706,22 @@ internal DistributedCacheEntryOptions ToDistributedCacheEntryOptions(FusionCache if (incoherentFailSafeMaxDuration) { if (logger?.IsEnabled(options.IncoherentOptionsNormalizationLogLevel) ?? false) - logger.Log(options.IncoherentOptionsNormalizationLogLevel, "FUSION [N={CacheName} I={CacheInstanceId}] (O={CacheOperationId} K={CacheKey}): DistributedCacheFailSafeMaxDuration/FailSafeMaxDuration {{FailSafeMaxDuration}} was lower than the DistributedCache/Duration {Duration} on {Options} {MemoryOptions}. Duration has been used instead.", options.CacheName, options.InstanceId, operationId, key, failSafeMaxDurationToUse.ToLogString(), durationToUse.ToLogString(), this.ToLogString(), res.ToLogString()); + logger.Log(options.IncoherentOptionsNormalizationLogLevel, "FUSION [N={CacheName} I={CacheInstanceId}] (O={CacheOperationId} K={CacheKey}): DistributedCacheFailSafeMaxDuration/FailSafeMaxDuration {FailSafeMaxDuration} was lower than the DistributedCacheDuration/Duration {Duration} on {Options} {MemoryOptions}. Duration has been used instead.", options.CacheName, options.InstanceId, operationId, key, failSafeMaxDurationToUse.ToLogString(), durationToUse.ToLogString(), this.ToLogString(), res.ToLogString()); } return res; } - internal TimeSpan GetAppropriateLockTimeout(bool hasFallbackValue) + internal TimeSpan GetAppropriateMemoryLockTimeout(bool hasFallbackValue) { var res = LockTimeout; if (res == Timeout.InfiniteTimeSpan && hasFallbackValue && IsFailSafeEnabled && FactorySoftTimeout != Timeout.InfiniteTimeSpan) { - // IF THERE IS NO SPECIFIC LOCK TIMEOUT + // IF THERE IS NO SPECIFIC MEMORY LOCK TIMEOUT // + THERE IS A FALLBACK ENTRY // + FAIL-SAFE IS ENABLED // + THERE IS A FACTORY SOFT TIMEOUT - // --> USE IT AS A LOCK TIMEOUT + // --> USE IT AS A MEMORY LOCK TIMEOUT res = FactorySoftTimeout; } diff --git a/src/ZiggyCreatures.FusionCache/FusionCacheOptions.cs b/src/ZiggyCreatures.FusionCache/FusionCacheOptions.cs index d3c46ebe..55b9ddb8 100644 --- a/src/ZiggyCreatures.FusionCache/FusionCacheOptions.cs +++ b/src/ZiggyCreatures.FusionCache/FusionCacheOptions.cs @@ -250,7 +250,7 @@ public int? BackplaneAutoRecoveryMaxRetryCount /// DOCS: ///
[EditorBrowsable(EditorBrowsableState.Never)] - [Obsolete("Please use AutoRecoveryDelay instead.")] + [Obsolete("Backplane auto-recovery is now simply auto-recovery: please use the AutoRecoveryDelay property.")] public TimeSpan BackplaneAutoRecoveryReconnectDelay { get { return AutoRecoveryDelay; } @@ -292,7 +292,7 @@ public TimeSpan BackplaneAutoRecoveryDelay public bool EnableDistributedExpireOnBackplaneAutoRecovery { get; set; } /// - /// If enabled, and re-throwing of exceptions is also enabled, it will re-throw the original exception as-is instead of wrapping it into one of the available specific exceptions (, or ). + /// If enabled, and re-throwing of exceptions is also enabled (see , or ), it will re-throw the original exception as-is instead of wrapping it into one of the available specific exceptions (, or ). /// public bool ReThrowOriginalExceptions { get; set; } diff --git a/src/ZiggyCreatures.FusionCache/FusionCacheServiceCollectionExtensions.cs b/src/ZiggyCreatures.FusionCache/FusionCacheServiceCollectionExtensions.cs index d2c66d8c..777f9e50 100644 --- a/src/ZiggyCreatures.FusionCache/FusionCacheServiceCollectionExtensions.cs +++ b/src/ZiggyCreatures.FusionCache/FusionCacheServiceCollectionExtensions.cs @@ -38,7 +38,7 @@ private static IServiceCollection AddFusionCacheProvider(this IServiceCollection /// The to configure the newly created instance. /// The so that additional calls can be chained. [EditorBrowsable(EditorBrowsableState.Never)] - [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)] + [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()")] public static IServiceCollection AddFusionCache(this IServiceCollection services, Action? setupOptionsAction = null, bool useDistributedCacheIfAvailable = true, bool ignoreMemoryDistributedCache = true, Action? setupCacheAction = null) { if (services is null) diff --git a/src/ZiggyCreatures.FusionCache/FusionCache_Async.cs b/src/ZiggyCreatures.FusionCache/FusionCache_Async.cs index cd694498..e5eb1c28 100644 --- a/src/ZiggyCreatures.FusionCache/FusionCache_Async.cs +++ b/src/ZiggyCreatures.FusionCache/FusionCache_Async.cs @@ -1,9 +1,11 @@ ο»Ώusing System; +using System.Diagnostics; using System.Threading; using System.Threading.Tasks; using Microsoft.Extensions.Logging; using ZiggyCreatures.Caching.Fusion.Internals; using ZiggyCreatures.Caching.Fusion.Internals.Backplane; +using ZiggyCreatures.Caching.Fusion.Internals.Diagnostics; using ZiggyCreatures.Caching.Fusion.Internals.Distributed; using ZiggyCreatures.Caching.Fusion.Internals.Memory; @@ -17,11 +19,9 @@ public partial class FusionCache if (options is null) options = _options.DefaultEntryOptions; - token.ThrowIfCancellationRequested(); - FusionCacheMemoryEntry? memoryEntry = null; bool memoryEntryIsValid = false; - object? lockObj = null; + object? memoryLockObj = null; // DIRECTLY CHECK MEMORY CACHE (TO AVOID LOCKING) var mca = GetCurrentMemoryAccessor(options); @@ -44,9 +44,9 @@ public partial class FusionCache if (_logger?.IsEnabled(LogLevel.Trace) ?? false) _logger.Log(LogLevel.Trace, "FUSION [N={CacheName} I={CacheInstanceId}] (O={CacheOperationId} K={CacheKey}): should eagerly refresh", CacheName, InstanceId, operationId, key); - // TRY TO GET THE LOCK WITHOUT WAITING, SO THAT ONLY THE FIRST ONE WILL ACTUALLY REFRESH THE ENTRY - lockObj = await _reactor.AcquireLockAsync(CacheName, InstanceId, key, operationId, TimeSpan.Zero, _logger, token).ConfigureAwait(false); - if (lockObj is null) + // TRY TO GET THE MEMORY LOCK WITHOUT WAITING, SO THAT ONLY THE FIRST ONE WILL ACTUALLY REFRESH THE ENTRY + memoryLockObj = await _memoryLocker.AcquireLockAsync(CacheName, InstanceId, key, operationId, TimeSpan.Zero, _logger, token).ConfigureAwait(false); + if (memoryLockObj is null) { if (_logger?.IsEnabled(LogLevel.Trace) ?? false) _logger.Log(LogLevel.Trace, "FUSION [N={CacheName} I={CacheInstanceId}] (O={CacheOperationId} K={CacheKey}): eager refresh already occurring", CacheName, InstanceId, operationId, key); @@ -54,7 +54,9 @@ public partial class FusionCache else { // EXECUTE EAGER REFRESH - await ExecuteEagerRefreshAsync(operationId, key, factory, options, memoryEntry, lockObj, token).ConfigureAwait(false); + await ExecuteEagerRefreshAsync(operationId, key, factory, options, memoryEntry, memoryLockObj, token).ConfigureAwait(false); + // RESET MEMORY LOCK (WILL BE RELEASED BY THE EAGER REFRESH FACTORY) + memoryLockObj = null; } } @@ -70,12 +72,12 @@ public partial class FusionCache try { - // LOCK - lockObj = await _reactor.AcquireLockAsync(CacheName, InstanceId, key, operationId, options.GetAppropriateLockTimeout(memoryEntry is not null), _logger, token).ConfigureAwait(false); + // MEMORY LOCK + memoryLockObj = await _memoryLocker.AcquireLockAsync(CacheName, InstanceId, key, operationId, options.GetAppropriateMemoryLockTimeout(memoryEntry is not null), _logger, token).ConfigureAwait(false); - if (lockObj is null && options.IsFailSafeEnabled && memoryEntry is not null) + if (memoryLockObj is null && options.IsFailSafeEnabled && memoryEntry is not null) { - // IF THE LOCK HAS NOT BEEN ACQUIRED + // IF THE MEMORY LOCK HAS NOT BEEN ACQUIRED // + THERE IS A FALLBACK ENTRY // + FAIL-SAFE IS ENABLED @@ -87,7 +89,7 @@ public partial class FusionCache return memoryEntry; } - // TRY AGAIN WITH MEMORY CACHE (AFTER THE LOCK HAS BEEN ACQUIRED, MAYBE SOMETHING CHANGED) + // TRY AGAIN WITH MEMORY CACHE (AFTER THE MEMORY LOCK HAS BEEN ACQUIRED, MAYBE SOMETHING CHANGED) if (mca is not null) { (memoryEntry, memoryEntryIsValid) = mca.TryGetEntry(operationId, key); @@ -113,6 +115,8 @@ public partial class FusionCache { if ((memoryEntry is not null && options.SkipDistributedCacheReadWhenStale) == false) { + token.ThrowIfCancellationRequested(); + (distributedEntry, distributedEntryIsValid) = await dca!.TryGetEntryAsync(operationId, key, options, memoryEntry is not null, null, token).ConfigureAwait(false); } } @@ -147,8 +151,13 @@ public partial class FusionCache var ctx = FusionCacheFactoryExecutionContext.CreateFromEntries(options, distributedEntry, memoryEntry); + // ACTIVITY + var activityForFactory = Activities.Source.StartActivityWithCommonTags(Activities.Names.ExecuteFactory, CacheName, InstanceId, key, operationId); + try { + token.ThrowIfCancellationRequested(); + if (timeout == Timeout.InfiniteTimeSpan && token == CancellationToken.None) { value = await factory(ctx, CancellationToken.None).ConfigureAwait(false); @@ -158,6 +167,8 @@ public partial class FusionCache value = await RunUtils.RunAsyncFuncWithTimeoutAsync(ct => factory(ctx, ct), timeout, options.AllowTimedOutFactoryBackgroundCompletion == false, x => factoryTask = x, token).ConfigureAwait(false); } + activityForFactory?.Dispose(); + hasNewValue = true; // UPDATE ADAPTIVE OPTIONS @@ -177,15 +188,19 @@ public partial class FusionCache // EVENTS _events.OnFactorySuccess(operationId, key); } - catch (OperationCanceledException) + catch (OperationCanceledException exc) { + // ACTIVITY + activityForFactory?.SetStatus(ActivityStatusCode.Error, exc.Message); + activityForFactory?.Dispose(); + throw; } catch (Exception exc) { ProcessFactoryError(operationId, key, exc); - MaybeBackgroundCompleteTimedOutFactory(operationId, key, ctx, factoryTask, options, token); + MaybeBackgroundCompleteTimedOutFactory(operationId, key, ctx, factoryTask, options, activityForFactory, token); if (TryPickFailSafeFallbackValue(operationId, key, distributedEntry, memoryEntry, failSafeDefaultValue, options, out var maybeFallbackValue, out timestamp, out isStale)) { @@ -212,8 +227,9 @@ public partial class FusionCache } finally { - if (lockObj is not null) - ReleaseLock(operationId, key, lockObj); + // MEMORY LOCK + if (memoryLockObj is not null) + ReleaseMemoryLock(operationId, key, memoryLockObj); } // EVENT @@ -237,8 +253,11 @@ public partial class FusionCache return entry; } - private async Task ExecuteEagerRefreshAsync(string operationId, string key, Func, CancellationToken, Task> factory, FusionCacheEntryOptions options, FusionCacheMemoryEntry memoryEntry, object lockObj, CancellationToken token) + private async Task ExecuteEagerRefreshAsync(string operationId, string key, Func, CancellationToken, Task> factory, FusionCacheEntryOptions options, FusionCacheMemoryEntry memoryEntry, object memoryLockObj, CancellationToken token) { + // EVENT + _events.OnEagerRefresh(operationId, key); + // TRY WITH DISTRIBUTED CACHE (IF ANY) try { @@ -267,7 +286,9 @@ private async Task ExecuteEagerRefreshAsync(string operationId, string k } finally { - ReleaseLock(operationId, key, lockObj); + // MEMORY LOCK + if (memoryLockObj is not null) + ReleaseMemoryLock(operationId, key, memoryLockObj); } return; @@ -288,19 +309,22 @@ private async Task ExecuteEagerRefreshAsync(string operationId, string k if (_logger?.IsEnabled(LogLevel.Trace) ?? false) _logger.Log(LogLevel.Trace, "FUSION [N={CacheName} I={CacheInstanceId}] (O={CacheOperationId} K={CacheKey}): eagerly refreshing", CacheName, InstanceId, operationId, key); - // EVENT - _events.OnEagerRefresh(operationId, key); + // ACTIVITY + var activity = Activities.Source.StartActivityWithCommonTags(Activities.Names.ExecuteFactory, CacheName, InstanceId, key, operationId); + activity?.SetTag("fusioncache.factory.eager_refresh", true); var ctx = FusionCacheFactoryExecutionContext.CreateFromEntries(options, null, memoryEntry); var factoryTask = factory(ctx, token); - CompleteBackgroundFactory(operationId, key, ctx, factoryTask, options, lockObj, token); + CompleteBackgroundFactory(operationId, key, ctx, factoryTask, options, memoryLockObj, activity, token); } /// public async ValueTask GetOrSetAsync(string key, Func, CancellationToken, Task> factory, MaybeValue failSafeDefaultValue = default, FusionCacheEntryOptions? options = null, CancellationToken token = default) { + Metrics.CounterGetOrSet.Maybe()?.AddWithCommonTags(1, _options.CacheName, _options.InstanceId!); + ValidateCacheKey(key); MaybePreProcessCacheKey(ref key); @@ -315,6 +339,9 @@ private async Task ExecuteEagerRefreshAsync(string operationId, string k if (_logger?.IsEnabled(LogLevel.Debug) ?? false) _logger.Log(LogLevel.Debug, "FUSION [N={CacheName} I={CacheInstanceId}] (O={CacheOperationId} K={CacheKey}): calling GetOrSetAsync {Options}", CacheName, InstanceId, operationId, key, options.ToLogString()); + // ACTIVITY + using var activity = Activities.Source.StartActivityWithCommonTags(Activities.Names.GetOrSet, CacheName, InstanceId, key, operationId); + var entry = await GetOrSetEntryInternalAsync(operationId, key, factory, true, failSafeDefaultValue, options, token).ConfigureAwait(false); if (entry is null) @@ -333,6 +360,8 @@ private async Task ExecuteEagerRefreshAsync(string operationId, string k /// public async ValueTask GetOrSetAsync(string key, TValue? defaultValue, FusionCacheEntryOptions? options = null, CancellationToken token = default) { + Metrics.CounterGetOrSet.Maybe()?.AddWithCommonTags(1, _options.CacheName, _options.InstanceId!); + ValidateCacheKey(key); MaybePreProcessCacheKey(ref key); @@ -344,6 +373,9 @@ private async Task ExecuteEagerRefreshAsync(string operationId, string k if (_logger?.IsEnabled(LogLevel.Debug) ?? false) _logger.Log(LogLevel.Debug, "FUSION [N={CacheName} I={CacheInstanceId}] (O={CacheOperationId} K={CacheKey}): calling GetOrSetAsync {Options}", CacheName, InstanceId, operationId, key, options.ToLogString()); + // ACTIVITY + using var activity = Activities.Source.StartActivityWithCommonTags(Activities.Names.GetOrSet, CacheName, InstanceId, key, operationId); + var entry = await GetOrSetEntryInternalAsync(operationId, key, (_, _) => Task.FromResult(defaultValue), false, default, options, token).ConfigureAwait(false); if (entry is null) @@ -369,7 +401,6 @@ private async Task ExecuteEagerRefreshAsync(string operationId, string k FusionCacheMemoryEntry? memoryEntry = null; bool memoryEntryIsValid = false; - // DIRECTLY CHECK MEMORY CACHE (TO AVOID LOCKING) var mca = GetCurrentMemoryAccessor(options); if (mca is not null) { @@ -389,8 +420,8 @@ private async Task ExecuteEagerRefreshAsync(string operationId, string k var dca = GetCurrentDistributedAccessor(options); - // SHORT-CIRCUIT: NO USABLE DISTRIBUTED CACHE - if (options.SkipDistributedCacheReadWhenStale || dca.CanBeUsed(operationId, key) == false) + // EARLY RETURN: NO USABLE DISTRIBUTED CACHE + if ((memoryEntry is not null && options.SkipDistributedCacheReadWhenStale) || dca.CanBeUsed(operationId, key) == false) { if (options.IsFailSafeEnabled && memoryEntry is not null) { @@ -479,6 +510,8 @@ private async Task ExecuteEagerRefreshAsync(string operationId, string k /// public async ValueTask> TryGetAsync(string key, FusionCacheEntryOptions? options = null, CancellationToken token = default) { + Metrics.CounterTryGet.Maybe()?.AddWithCommonTags(1, _options.CacheName, _options.InstanceId!); + ValidateCacheKey(key); MaybePreProcessCacheKey(ref key); @@ -490,6 +523,9 @@ public async ValueTask> TryGetAsync(string key, Fusio if (_logger?.IsEnabled(LogLevel.Debug) ?? false) _logger.Log(LogLevel.Debug, "FUSION [N={CacheName} I={CacheInstanceId}] (O={CacheOperationId} K={CacheKey}): calling TryGetAsync {Options}", CacheName, InstanceId, operationId, key, options.ToLogString()); + // ACTIVITY + using var activity = Activities.Source.StartActivityWithCommonTags(Activities.Names.TryGet, CacheName, InstanceId, key, operationId); + var entry = await TryGetEntryInternalAsync(operationId, key, options, token).ConfigureAwait(false); if (entry is null) @@ -509,6 +545,8 @@ public async ValueTask> TryGetAsync(string key, Fusio /// public async ValueTask GetOrDefaultAsync(string key, TValue? defaultValue = default, FusionCacheEntryOptions? options = null, CancellationToken token = default) { + Metrics.CounterGetOrDefault.Maybe()?.AddWithCommonTags(1, _options.CacheName, _options.InstanceId!); + ValidateCacheKey(key); MaybePreProcessCacheKey(ref key); @@ -520,6 +558,9 @@ public async ValueTask> TryGetAsync(string key, Fusio if (_logger?.IsEnabled(LogLevel.Debug) ?? false) _logger.Log(LogLevel.Debug, "FUSION [N={CacheName} I={CacheInstanceId}] (O={CacheOperationId} K={CacheKey}): calling GetOrDefaultAsync {Options}", CacheName, InstanceId, operationId, key, options.ToLogString()); + // ACTIVITY + using var activity = Activities.Source.StartActivityWithCommonTags(Activities.Names.GetOrDefault, CacheName, InstanceId, key, operationId); + var entry = await TryGetEntryInternalAsync(operationId, key, options, token).ConfigureAwait(false); if (entry is null) @@ -552,6 +593,9 @@ public async ValueTask SetAsync(string key, TValue value, FusionCacheEnt if (_logger?.IsEnabled(LogLevel.Debug) ?? false) _logger.Log(LogLevel.Debug, "FUSION [N={CacheName} I={CacheInstanceId}] (O={CacheOperationId} K={CacheKey}): calling SetAsync {Options}", CacheName, InstanceId, operationId, key, options.ToLogString()); + // ACTIVITY + using var activity = Activities.Source.StartActivityWithCommonTags(Activities.Names.Set, CacheName, InstanceId, key, operationId); + // TODO: MAYBE FIND A WAY TO PASS LASTMODIFIED/ETAG HERE var entry = FusionCacheMemoryEntry.CreateFromOptions(value, options, false, null, null, null, typeof(TValue)); @@ -561,7 +605,10 @@ public async ValueTask SetAsync(string key, TValue value, FusionCacheEnt mca.SetEntry(operationId, key, entry, options); } - await DistributedSetEntryAsync(operationId, key, entry, options, token).ConfigureAwait(false); + if (RequiresDistributedOperations(options)) + { + await DistributedSetEntryAsync(operationId, key, entry, options, token).ConfigureAwait(false); + } // EVENT _events.OnSet(operationId, key); @@ -584,6 +631,9 @@ public async ValueTask RemoveAsync(string key, FusionCacheEntryOptions? options if (_logger?.IsEnabled(LogLevel.Debug) ?? false) _logger.Log(LogLevel.Debug, "FUSION [N={CacheName} I={CacheInstanceId}] (O={CacheOperationId} K={CacheKey}): calling RemoveAsync {Options}", CacheName, InstanceId, operationId, key, options.ToLogString()); + // ACTIVITY + using var activity = Activities.Source.StartActivityWithCommonTags(Activities.Names.Remove, CacheName, InstanceId, key, operationId); + var mca = GetCurrentMemoryAccessor(options); if (mca is not null) { @@ -613,6 +663,9 @@ public async ValueTask ExpireAsync(string key, FusionCacheEntryOptions? options if (_logger?.IsEnabled(LogLevel.Debug) ?? false) _logger.Log(LogLevel.Debug, "FUSION [N={CacheName} I={CacheInstanceId}] (O={CacheOperationId} K={CacheKey}): calling ExpireAsync {Options}", CacheName, InstanceId, operationId, key, options.ToLogString()); + // ACTIVITY + using var activity = Activities.Source.StartActivityWithCommonTags(Activities.Names.Expire, CacheName, InstanceId, key, operationId); + var mca = GetCurrentMemoryAccessor(options); if (mca is not null) { @@ -628,7 +681,9 @@ public async ValueTask ExpireAsync(string key, FusionCacheEntryOptions? options private async ValueTask ExecuteDistributedActionAsync(string operationId, string key, FusionCacheAction action, long timestamp, Func> distributedCacheAction, Func> backplaneAction, FusionCacheEntryOptions options, CancellationToken token) { if (RequiresDistributedOperations(options) == false) + { return; + } var mustAwaitCompletion = MustAwaitDistributedOperations(options); var isBackground = !mustAwaitCompletion; diff --git a/src/ZiggyCreatures.FusionCache/FusionCache_Sync.cs b/src/ZiggyCreatures.FusionCache/FusionCache_Sync.cs index 8000ce47..83ff6883 100644 --- a/src/ZiggyCreatures.FusionCache/FusionCache_Sync.cs +++ b/src/ZiggyCreatures.FusionCache/FusionCache_Sync.cs @@ -1,9 +1,11 @@ ο»Ώusing System; +using System.Diagnostics; using System.Threading; using System.Threading.Tasks; using Microsoft.Extensions.Logging; using ZiggyCreatures.Caching.Fusion.Internals; using ZiggyCreatures.Caching.Fusion.Internals.Backplane; +using ZiggyCreatures.Caching.Fusion.Internals.Diagnostics; using ZiggyCreatures.Caching.Fusion.Internals.Distributed; using ZiggyCreatures.Caching.Fusion.Internals.Memory; @@ -17,11 +19,9 @@ public partial class FusionCache if (options is null) options = _options.DefaultEntryOptions; - token.ThrowIfCancellationRequested(); - FusionCacheMemoryEntry? memoryEntry = null; bool memoryEntryIsValid = false; - object? lockObj = null; + object? memoryLockObj = null; // DIRECTLY CHECK MEMORY CACHE (TO AVOID LOCKING) var mca = GetCurrentMemoryAccessor(options); @@ -44,9 +44,9 @@ public partial class FusionCache if (_logger?.IsEnabled(LogLevel.Trace) ?? false) _logger.Log(LogLevel.Trace, "FUSION [N={CacheName} I={CacheInstanceId}] (O={CacheOperationId} K={CacheKey}): should eagerly refresh", CacheName, InstanceId, operationId, key); - // TRY TO GET THE LOCK WITHOUT WAITING, SO THAT ONLY THE FIRST ONE WILL ACTUALLY REFRESH THE ENTRY - lockObj = _reactor.AcquireLock(CacheName, InstanceId, key, operationId, TimeSpan.Zero, _logger); - if (lockObj is null) + // TRY TO GET THE MEMORY LOCK WITHOUT WAITING, SO THAT ONLY THE FIRST ONE WILL ACTUALLY REFRESH THE ENTRY + memoryLockObj = _memoryLocker.AcquireLock(CacheName, InstanceId, key, operationId, TimeSpan.Zero, _logger, token); + if (memoryLockObj is null) { if (_logger?.IsEnabled(LogLevel.Trace) ?? false) _logger.Log(LogLevel.Trace, "FUSION [N={CacheName} I={CacheInstanceId}] (O={CacheOperationId} K={CacheKey}): eager refresh already occurring", CacheName, InstanceId, operationId, key); @@ -54,7 +54,9 @@ public partial class FusionCache else { // EXECUTE EAGER REFRESH - ExecuteEagerRefresh(operationId, key, factory, options, memoryEntry, lockObj, token); + ExecuteEagerRefresh(operationId, key, factory, options, memoryEntry, memoryLockObj, token); + // RESET MEMORY LOCK (WILL BE RELEASED BY THE EAGER REFRESH FACTORY) + memoryLockObj = null; } } @@ -70,12 +72,12 @@ public partial class FusionCache try { - // LOCK - lockObj = _reactor.AcquireLock(CacheName, InstanceId, key, operationId, options.GetAppropriateLockTimeout(memoryEntry is not null), _logger); + // MEMORY LOCK + memoryLockObj = _memoryLocker.AcquireLock(CacheName, InstanceId, key, operationId, options.GetAppropriateMemoryLockTimeout(memoryEntry is not null), _logger, token); - if (lockObj is null && options.IsFailSafeEnabled && memoryEntry is not null) + if (memoryLockObj is null && options.IsFailSafeEnabled && memoryEntry is not null) { - // IF THE LOCK HAS NOT BEEN ACQUIRED + // IF THE MEMORY LOCK HAS NOT BEEN ACQUIRED // + THERE IS A FALLBACK ENTRY // + FAIL-SAFE IS ENABLED @@ -87,7 +89,7 @@ public partial class FusionCache return memoryEntry; } - // TRY AGAIN WITH MEMORY CACHE (AFTER THE LOCK HAS BEEN ACQUIRED, MAYBE SOMETHING CHANGED) + // TRY AGAIN WITH MEMORY CACHE (AFTER THE MEMORY LOCK HAS BEEN ACQUIRED, MAYBE SOMETHING CHANGED) if (mca is not null) { (memoryEntry, memoryEntryIsValid) = mca.TryGetEntry(operationId, key); @@ -113,6 +115,8 @@ public partial class FusionCache { if ((memoryEntry is not null && options.SkipDistributedCacheReadWhenStale) == false) { + token.ThrowIfCancellationRequested(); + (distributedEntry, distributedEntryIsValid) = dca!.TryGetEntry(operationId, key, options, memoryEntry is not null, null, token); } } @@ -147,8 +151,13 @@ public partial class FusionCache var ctx = FusionCacheFactoryExecutionContext.CreateFromEntries(options, distributedEntry, memoryEntry); + // ACTIVITY + var activityForFactory = Activities.Source.StartActivityWithCommonTags(Activities.Names.ExecuteFactory, CacheName, InstanceId, key, operationId); + try { + token.ThrowIfCancellationRequested(); + if (timeout == Timeout.InfiniteTimeSpan && token == CancellationToken.None) { value = factory(ctx, CancellationToken.None); @@ -158,6 +167,8 @@ public partial class FusionCache value = RunUtils.RunSyncFuncWithTimeout(ct => factory(ctx, ct), timeout, options.AllowTimedOutFactoryBackgroundCompletion == false, x => factoryTask = x, token); } + activityForFactory?.Dispose(); + hasNewValue = true; // UPDATE ADAPTIVE OPTIONS @@ -177,15 +188,19 @@ public partial class FusionCache // EVENTS _events.OnFactorySuccess(operationId, key); } - catch (OperationCanceledException) + catch (OperationCanceledException exc) { + // ACTIVITY + activityForFactory?.SetStatus(ActivityStatusCode.Error, exc.Message); + activityForFactory?.Dispose(); + throw; } catch (Exception exc) { ProcessFactoryError(operationId, key, exc); - MaybeBackgroundCompleteTimedOutFactory(operationId, key, ctx, factoryTask, options, token); + MaybeBackgroundCompleteTimedOutFactory(operationId, key, ctx, factoryTask, options, activityForFactory, token); if (TryPickFailSafeFallbackValue(operationId, key, distributedEntry, memoryEntry, failSafeDefaultValue, options, out var maybeFallbackValue, out timestamp, out isStale)) { @@ -212,8 +227,9 @@ public partial class FusionCache } finally { - if (lockObj is not null) - ReleaseLock(operationId, key, lockObj); + // MEMORY LOCK + if (memoryLockObj is not null) + ReleaseMemoryLock(operationId, key, memoryLockObj); } // EVENT @@ -237,8 +253,11 @@ public partial class FusionCache return entry; } - private void ExecuteEagerRefresh(string operationId, string key, Func, CancellationToken, TValue?> factory, FusionCacheEntryOptions options, FusionCacheMemoryEntry memoryEntry, object lockObj, CancellationToken token) + private void ExecuteEagerRefresh(string operationId, string key, Func, CancellationToken, TValue?> factory, FusionCacheEntryOptions options, FusionCacheMemoryEntry memoryEntry, object memoryLockObj, CancellationToken token) { + // EVENT + _events.OnEagerRefresh(operationId, key); + // TRY WITH DISTRIBUTED CACHE (IF ANY) try { @@ -267,7 +286,9 @@ private void ExecuteEagerRefresh(string operationId, string key, Func(string operationId, string key, Func.CreateFromEntries(options, null, memoryEntry); var factoryTask = Task.Run(() => factory(ctx, token)); - CompleteBackgroundFactory(operationId, key, ctx, factoryTask, options, lockObj, token); + CompleteBackgroundFactory(operationId, key, ctx, factoryTask, options, memoryLockObj, activity, token); } /// public TValue? GetOrSet(string key, Func, CancellationToken, TValue?> factory, MaybeValue failSafeDefaultValue = default, FusionCacheEntryOptions? options = null, CancellationToken token = default) { + Metrics.CounterGetOrSet.Maybe()?.AddWithCommonTags(1, _options.CacheName, _options.InstanceId!); + ValidateCacheKey(key); MaybePreProcessCacheKey(ref key); @@ -315,6 +339,9 @@ private void ExecuteEagerRefresh(string operationId, string key, Func {Options}", CacheName, InstanceId, operationId, key, options.ToLogString()); + // ACTIVITY + using var activity = Activities.Source.StartActivityWithCommonTags(Activities.Names.GetOrSet, CacheName, InstanceId, key, operationId); + var entry = GetOrSetEntryInternal(operationId, key, factory, true, failSafeDefaultValue, options, token); if (entry is null) @@ -333,6 +360,8 @@ private void ExecuteEagerRefresh(string operationId, string key, Func public TValue? GetOrSet(string key, TValue? defaultValue, FusionCacheEntryOptions? options = null, CancellationToken token = default) { + Metrics.CounterGetOrSet.Maybe()?.AddWithCommonTags(1, _options.CacheName, _options.InstanceId!); + ValidateCacheKey(key); MaybePreProcessCacheKey(ref key); @@ -344,6 +373,9 @@ private void ExecuteEagerRefresh(string operationId, string key, Func {Options}", CacheName, InstanceId, operationId, key, options.ToLogString()); + // ACTIVITY + using var activity = Activities.Source.StartActivityWithCommonTags(Activities.Names.GetOrSet, CacheName, InstanceId, key, operationId); + var entry = GetOrSetEntryInternal(operationId, key, (_, _) => defaultValue, false, default, options, token); if (entry is null) @@ -369,7 +401,6 @@ private void ExecuteEagerRefresh(string operationId, string key, Func(string operationId, string key, Func(string operationId, string key, Func public MaybeValue TryGet(string key, FusionCacheEntryOptions? options = null, CancellationToken token = default) { + Metrics.CounterTryGet.Maybe()?.AddWithCommonTags(1, _options.CacheName, _options.InstanceId!); + ValidateCacheKey(key); MaybePreProcessCacheKey(ref key); @@ -490,6 +523,9 @@ public MaybeValue TryGet(string key, FusionCacheEntryOptions? op if (_logger?.IsEnabled(LogLevel.Debug) ?? false) _logger.Log(LogLevel.Debug, "FUSION [N={CacheName} I={CacheInstanceId}] (O={CacheOperationId} K={CacheKey}): calling TryGet {Options}", CacheName, InstanceId, operationId, key, options.ToLogString()); + // ACTIVITY + using var activity = Activities.Source.StartActivityWithCommonTags(Activities.Names.TryGet, CacheName, InstanceId, key, operationId); + var entry = TryGetEntryInternal(operationId, key, options, token); if (entry is null) @@ -509,6 +545,8 @@ public MaybeValue TryGet(string key, FusionCacheEntryOptions? op /// public TValue? GetOrDefault(string key, TValue? defaultValue = default, FusionCacheEntryOptions? options = null, CancellationToken token = default) { + Metrics.CounterGetOrDefault.Maybe()?.AddWithCommonTags(1, _options.CacheName, _options.InstanceId!); + ValidateCacheKey(key); MaybePreProcessCacheKey(ref key); @@ -520,6 +558,9 @@ public MaybeValue TryGet(string key, FusionCacheEntryOptions? op if (_logger?.IsEnabled(LogLevel.Debug) ?? false) _logger.Log(LogLevel.Debug, "FUSION [N={CacheName} I={CacheInstanceId}] (O={CacheOperationId} K={CacheKey}): calling GetOrDefault {Options}", CacheName, InstanceId, operationId, key, options.ToLogString()); + // ACTIVITY + using var activity = Activities.Source.StartActivityWithCommonTags(Activities.Names.GetOrDefault, CacheName, InstanceId, key, operationId); + var entry = TryGetEntryInternal(operationId, key, options, token); if (entry is null) @@ -552,6 +593,9 @@ public void Set(string key, TValue value, FusionCacheEntryOptions? optio if (_logger?.IsEnabled(LogLevel.Debug) ?? false) _logger.Log(LogLevel.Debug, "FUSION [N={CacheName} I={CacheInstanceId}] (O={CacheOperationId} K={CacheKey}): calling Set {Options}", CacheName, InstanceId, operationId, key, options.ToLogString()); + // ACTIVITY + using var activity = Activities.Source.StartActivityWithCommonTags(Activities.Names.Set, CacheName, InstanceId, key, operationId); + // TODO: MAYBE FIND A WAY TO PASS LASTMODIFIED/ETAG HERE var entry = FusionCacheMemoryEntry.CreateFromOptions(value, options, false, null, null, null, typeof(TValue)); @@ -561,7 +605,10 @@ public void Set(string key, TValue value, FusionCacheEntryOptions? optio mca.SetEntry(operationId, key, entry, options); } - DistributedSetEntry(operationId, key, entry, options, token); + if (RequiresDistributedOperations(options)) + { + DistributedSetEntry(operationId, key, entry, options, token); + } // EVENT _events.OnSet(operationId, key); @@ -584,6 +631,9 @@ public void Remove(string key, FusionCacheEntryOptions? options = null, Cancella if (_logger?.IsEnabled(LogLevel.Debug) ?? false) _logger.Log(LogLevel.Debug, "FUSION [N={CacheName} I={CacheInstanceId}] (O={CacheOperationId} K={CacheKey}): calling Remove {Options}", CacheName, InstanceId, operationId, key, options.ToLogString()); + // ACTIVITY + using var activity = Activities.Source.StartActivityWithCommonTags(Activities.Names.Remove, CacheName, InstanceId, key, operationId); + var mca = GetCurrentMemoryAccessor(options); if (mca is not null) { @@ -613,6 +663,9 @@ public void Expire(string key, FusionCacheEntryOptions? options = null, Cancella if (_logger?.IsEnabled(LogLevel.Debug) ?? false) _logger.Log(LogLevel.Debug, "FUSION [N={CacheName} I={CacheInstanceId}] (O={CacheOperationId} K={CacheKey}): calling Expire {Options}", CacheName, InstanceId, operationId, key, options.ToLogString()); + // ACTIVITY + using var activity = Activities.Source.StartActivityWithCommonTags(Activities.Names.Expire, CacheName, InstanceId, key, operationId); + var mca = GetCurrentMemoryAccessor(options); if (mca is not null) { @@ -628,7 +681,9 @@ public void Expire(string key, FusionCacheEntryOptions? options = null, Cancella private void ExecuteDistributedAction(string operationId, string key, FusionCacheAction action, long timestamp, Func distributedCacheAction, Func backplaneAction, FusionCacheEntryOptions options, CancellationToken token) { if (RequiresDistributedOperations(options) == false) + { return; + } var mustAwaitCompletion = MustAwaitDistributedOperations(options); var isBackground = !mustAwaitCompletion; diff --git a/src/ZiggyCreatures.FusionCache/GlobalSuppressions.cs b/src/ZiggyCreatures.FusionCache/GlobalSuppressions.cs index 2ad79a2a..67e4748e 100644 --- a/src/ZiggyCreatures.FusionCache/GlobalSuppressions.cs +++ b/src/ZiggyCreatures.FusionCache/GlobalSuppressions.cs @@ -5,9 +5,11 @@ using System.Diagnostics.CodeAnalysis; -[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("Simplification", "RCS1049:Simplify boolean comparison.", Justification = "")] +[assembly: SuppressMessage("Performance", "HAA0101:Array allocation for params parameter")] +[assembly: SuppressMessage("Performance", "HAA0302:Display class allocation to capture closure")] +[assembly: SuppressMessage("Performance", "HAA0301:Closure Allocation Source")] +//[assembly: SuppressMessage("Performance", "HAA0303:Lambda or anonymous method in a generic method allocates a delegate instance")] +//[assembly: SuppressMessage("Performance", "HAA0601:Value type to reference type conversion causing boxing allocation")] +[assembly: SuppressMessage("Simplification", "RCS1049:Simplify boolean comparison.")] +[assembly: SuppressMessage("Style", "IDE0090:Use 'new(...)'")] +[assembly: SuppressMessage("Style", "IDE0290:Use primary constructor")] diff --git a/src/ZiggyCreatures.FusionCache/IFusionCache.cs b/src/ZiggyCreatures.FusionCache/IFusionCache.cs index 23e0e2f9..aca03f91 100644 --- a/src/ZiggyCreatures.FusionCache/IFusionCache.cs +++ b/src/ZiggyCreatures.FusionCache/IFusionCache.cs @@ -198,7 +198,7 @@ public interface IFusionCache 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. + /// Sets a secondary caching level, by providing an instance and an instance to be used to convert from generic values to byte[] and viceversa. /// /// The instance to use. /// The instance to use. @@ -206,7 +206,7 @@ public interface IFusionCache IFusionCache SetupDistributedCache(IDistributedCache distributedCache, IFusionCacheSerializer serializer); /// - /// Removes the secondary caching layer. + /// Removes the secondary caching level. /// /// The same instance, usable in a fluent api way. IFusionCache RemoveDistributedCache(); @@ -234,24 +234,6 @@ public interface IFusionCache ///
bool HasBackplane { get; } - ///// - ///// Tries to send a message to other nodes connected to the same backplane, if any. - ///// - ///// The message to send. It can be created using one of the static methods like BackplaneMessage.CreateForXyz(). - ///// The options to use. - ///// An optional to cancel the operation. - ///// if there was at least one backplane to send a notification to, otherwise . - //ValueTask PublishAsync(BackplaneMessage message, FusionCacheEntryOptions? options = null, CancellationToken token = default); - - ///// - ///// Tries to send a message to other nodes connected to the same backplane, if any. - ///// - ///// The message to send. It can be created using one of the static methods like BackplaneMessage.CreateForXyz(). - ///// The options to use. - ///// An optional to cancel the operation. - ///// if there was at least one backplane to send a notification to, otherwise . - //bool Publish(BackplaneMessage message, FusionCacheEntryOptions? options = null, CancellationToken token = default); - /// /// The central place for all events handling of this FusionCache instance. /// diff --git a/src/ZiggyCreatures.FusionCache/IFusionCacheBuilder.cs b/src/ZiggyCreatures.FusionCache/IFusionCacheBuilder.cs index 5d1f1e8d..3e639dc6 100644 --- a/src/ZiggyCreatures.FusionCache/IFusionCacheBuilder.cs +++ b/src/ZiggyCreatures.FusionCache/IFusionCacheBuilder.cs @@ -5,6 +5,7 @@ using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; using ZiggyCreatures.Caching.Fusion.Backplane; +using ZiggyCreatures.Caching.Fusion.Locking; using ZiggyCreatures.Caching.Fusion.Plugins; using ZiggyCreatures.Caching.Fusion.Serialization; @@ -115,6 +116,30 @@ public interface IFusionCacheBuilder #endregion + #region MEMORY LOCKER + + /// + /// Indicates if the builder should try find and use an service registered in the DI container. + /// + bool UseRegisteredMemoryLocker { get; set; } + + /// + /// A specific instance to be used. + /// + IFusionCacheMemoryLocker? MemoryLocker { get; set; } + + /// + /// A factory that creates the instance to be used. + /// + Func? MemoryLockerFactory { get; set; } + + /// + /// Throws an if a memory locker (an instance of ) is not specified or is not found in the DI container. + /// + bool ThrowIfMissingMemoryLocker { get; set; } + + #endregion + #region SERIALIZER /// diff --git a/src/ZiggyCreatures.FusionCache/Internals/AutoRecovery/AutoRecoveryService.cs b/src/ZiggyCreatures.FusionCache/Internals/AutoRecovery/AutoRecoveryService.cs index 7cde10f5..a9f291d5 100644 --- a/src/ZiggyCreatures.FusionCache/Internals/AutoRecovery/AutoRecoveryService.cs +++ b/src/ZiggyCreatures.FusionCache/Internals/AutoRecovery/AutoRecoveryService.cs @@ -1,11 +1,13 @@ ο»Ώusing System; using System.Collections.Concurrent; using System.Collections.Generic; +using System.Diagnostics; using System.Linq; using System.Threading; using System.Threading.Tasks; using Microsoft.Extensions.Logging; using ZiggyCreatures.Caching.Fusion.Backplane; +using ZiggyCreatures.Caching.Fusion.Internals.Diagnostics; namespace ZiggyCreatures.Caching.Fusion.Internals.AutoRecovery; @@ -161,25 +163,15 @@ internal bool TryRemoveItem(string? operationId, AutoRecoveryItem item) if (item.CacheKey is null) return false; - if (_queue.TryGetValue(item.CacheKey, out var pendingLocal) == false) - return false; - - // NOTE: HERE WE SHOULD USE THE NEW OVERLOAD TryRemove(KeyValuePair) BUT THAT IS NOT AVAILABLE UNTIL .NET 5 - // SO WE DO THE NEXT BEST THING WE CAN: TRY TO GET THE VALUE AND, IF IT IS THE SAME AS THE ONE WE HAVE, THEN REMOVE IT - // OTHERWISE SKIP THE REMOVAL - // - // SEE: https://learn.microsoft.com/en-us/dotnet/api/system.collections.concurrent.concurrentdictionary-2.tryremove?view=net-7.0#system-collections-concurrent-concurrentdictionary-2-tryremove(system-collections-generic-keyvaluepair((-0-1))) - - if (ReferenceEquals(item, pendingLocal) == false) - return false; - - if (_queue.TryRemove(item.CacheKey, out _) == false) - return false; + if (_queue.TryRemove(item.CacheKey, item)) + { + if (_logger?.IsEnabled(LogLevel.Trace) ?? false) + _logger.Log(LogLevel.Trace, "FUSION [N={CacheName} I={CacheInstanceId}] (O={CacheOperationId} K={CacheKey}): removed an item from the auto-recovery queue", _cache.CacheName, _cache.InstanceId, operationId, item.CacheKey); - if (_logger?.IsEnabled(LogLevel.Debug) ?? false) - _logger.Log(LogLevel.Debug, "FUSION [N={CacheName} I={CacheInstanceId}] (O={CacheOperationId} K={CacheKey}): removed an item from the auto-recovery queue", _cache.CacheName, _cache.InstanceId, operationId, item.CacheKey); + return true; + } - return true; + return false; } internal bool TryCleanUpQueue(string operationId, IList items) @@ -294,6 +286,9 @@ internal async ValueTask TryProcessQueueAsync(string operationId, Cancella var hasStopped = false; AutoRecoveryItem? lastProcessedItem = null; + // ACTIVITY + using var activity = Activities.Source.StartActivityWithCommonTags(Activities.Names.AutoRecoveryProcessQueue, _cache.CacheName, _cache.InstanceId, null, operationId); + try { if (_logger?.IsEnabled(LogLevel.Debug) ?? false) @@ -315,20 +310,31 @@ internal async ValueTask TryProcessQueueAsync(string operationId, Cancella var success = false; - switch (item.Action) + // ACTIVITY + using var activityForItem = Activities.Source.StartActivityWithCommonTags(Activities.Names.AutoRecoveryProcessItem, _cache.CacheName, _cache.InstanceId, item.CacheKey, operationId); + + try { - case FusionCacheAction.EntrySet: - success = await TryProcessItemSetAsync(operationId, item, token).ConfigureAwait(false); - break; - case FusionCacheAction.EntryRemove: - success = await TryProcessItemRemoveAsync(operationId, item, token).ConfigureAwait(false); - break; - case FusionCacheAction.EntryExpire: - success = await TryProcessItemExpireAsync(operationId, item, token).ConfigureAwait(false); - break; - default: - success = true; - break; + switch (item.Action) + { + case FusionCacheAction.EntrySet: + success = await TryProcessItemSetAsync(operationId, item, token).ConfigureAwait(false); + break; + case FusionCacheAction.EntryRemove: + success = await TryProcessItemRemoveAsync(operationId, item, token).ConfigureAwait(false); + break; + case FusionCacheAction.EntryExpire: + success = await TryProcessItemExpireAsync(operationId, item, token).ConfigureAwait(false); + break; + default: + success = true; + break; + } + } + catch (Exception exc) + { + activityForItem?.SetStatus(ActivityStatusCode.Error, exc.Message); + throw; } if (success) @@ -358,6 +364,8 @@ internal async ValueTask TryProcessQueueAsync(string operationId, Cancella if (_logger?.IsEnabled(LogLevel.Error) ?? false) _logger.Log(LogLevel.Error, exc, "FUSION [N={CacheName} I={CacheInstanceId}] (O={CacheOperationId} K={CacheKey}): an error occurred during a auto-recovery of an item ({RetryCount} retries left)", _cache.CacheName, _cache.InstanceId, operationId, lastProcessedItem?.CacheKey, lastProcessedItem?.RetryCount); + + activity?.SetStatus(ActivityStatusCode.Error, exc.Message); } finally { diff --git a/src/ZiggyCreatures.FusionCache/Internals/Backplane/BackplaneAccessor.cs b/src/ZiggyCreatures.FusionCache/Internals/Backplane/BackplaneAccessor.cs index 07315895..48486fe8 100644 --- a/src/ZiggyCreatures.FusionCache/Internals/Backplane/BackplaneAccessor.cs +++ b/src/ZiggyCreatures.FusionCache/Internals/Backplane/BackplaneAccessor.cs @@ -3,6 +3,7 @@ using Microsoft.Extensions.Logging; using ZiggyCreatures.Caching.Fusion.Backplane; using ZiggyCreatures.Caching.Fusion.Events; +using ZiggyCreatures.Caching.Fusion.Internals.Diagnostics; namespace ZiggyCreatures.Caching.Fusion.Internals.Backplane; @@ -234,6 +235,10 @@ private async ValueTask HandleIncomingMessageAsync(BackplaneMessage message) _events.OnCircuitBreakerChange(operationId, message.CacheKey, true); } + // ACTIVITY + using var activity = Activities.SourceBackplane.StartActivityWithCommonTags(Activities.Names.BackplaneReceive, _options.CacheName, _options.InstanceId!, message.CacheKey!, operationId); + activity?.SetTag("fusioncache.backplane.message_action", message.Action.ToString()); + // EVENT _events.OnMessageReceived(operationId, message); @@ -272,7 +277,7 @@ private async ValueTask HandleIncomingMessageAsync(BackplaneMessage message) if (_logger?.IsEnabled(LogLevel.Trace) ?? false) _logger.Log(LogLevel.Trace, "FUSION [N={CacheName} I={CacheInstanceId}] (O={CacheOperationId} K={CacheKey}): [BP] a backplane notification has been received from remote cache {RemoteCacheInstanceId} (REMOVE)", _cache.CacheName, _cache.InstanceId, operationId, message.CacheKey, message.SourceId); - // // HANDLE REMOVE: CALLING MaybeExpireMemoryEntryInternal() WITH allowFailSafe SET TO FALSE -> LOCAL REMOVE + // HANDLE REMOVE: CALLING MaybeExpireMemoryEntryInternal() WITH allowFailSafe SET TO FALSE -> LOCAL REMOVE _cache.MaybeExpireMemoryEntryInternal(operationId, message.CacheKey!, false, null); break; case BackplaneMessageAction.EntryExpire: diff --git a/src/ZiggyCreatures.FusionCache/Internals/Backplane/BackplaneAccessor_Async.cs b/src/ZiggyCreatures.FusionCache/Internals/Backplane/BackplaneAccessor_Async.cs index cfe19c13..42cafbb2 100644 --- a/src/ZiggyCreatures.FusionCache/Internals/Backplane/BackplaneAccessor_Async.cs +++ b/src/ZiggyCreatures.FusionCache/Internals/Backplane/BackplaneAccessor_Async.cs @@ -1,8 +1,10 @@ ο»Ώusing System; +using System.Diagnostics; using System.Threading; using System.Threading.Tasks; using Microsoft.Extensions.Logging; using ZiggyCreatures.Caching.Fusion.Backplane; +using ZiggyCreatures.Caching.Fusion.Internals.Diagnostics; namespace ZiggyCreatures.Caching.Fusion.Internals.Backplane; @@ -23,6 +25,10 @@ private async ValueTask PublishAsync(string operationId, BackplaneMessage token.ThrowIfCancellationRequested(); + // ACTIVITY + using var activity = Activities.SourceBackplane.StartActivityWithCommonTags(Activities.Names.BackplanePublish, _options.CacheName, _options.InstanceId!, message.CacheKey!, operationId); + activity?.SetTag("fusioncache.backplane.message_action", message.Action.ToString()); + if (isAutoRecovery == false) { _cache.AutoRecovery.TryRemoveItemByCacheKey(operationId, cacheKey); @@ -47,6 +53,9 @@ private async ValueTask PublishAsync(string operationId, BackplaneMessage { ProcessError(operationId, cacheKey, exc, actionDescription); + // ACTIVITY + Activity.Current?.SetStatus(ActivityStatusCode.Error, exc.Message); + if (exc is not SyntheticTimeoutException && options.ReThrowBackplaneExceptions) { if (_options.ReThrowOriginalExceptions) diff --git a/src/ZiggyCreatures.FusionCache/Internals/Backplane/BackplaneAccessor_Sync.cs b/src/ZiggyCreatures.FusionCache/Internals/Backplane/BackplaneAccessor_Sync.cs index c31761e6..87d88c33 100644 --- a/src/ZiggyCreatures.FusionCache/Internals/Backplane/BackplaneAccessor_Sync.cs +++ b/src/ZiggyCreatures.FusionCache/Internals/Backplane/BackplaneAccessor_Sync.cs @@ -1,7 +1,9 @@ ο»Ώusing System; +using System.Diagnostics; using System.Threading; using Microsoft.Extensions.Logging; using ZiggyCreatures.Caching.Fusion.Backplane; +using ZiggyCreatures.Caching.Fusion.Internals.Diagnostics; namespace ZiggyCreatures.Caching.Fusion.Internals.Backplane; @@ -22,6 +24,10 @@ private bool Publish(string operationId, BackplaneMessage message, FusionCacheEn token.ThrowIfCancellationRequested(); + // ACTIVITY + using var activity = Activities.SourceBackplane.StartActivityWithCommonTags(Activities.Names.BackplanePublish, _options.CacheName, _options.InstanceId!, message.CacheKey!, operationId); + activity?.SetTag("fusioncache.backplane.message_action", message.Action.ToString()); + if (isAutoRecovery == false) { _cache.AutoRecovery.TryRemoveItemByCacheKey(operationId, cacheKey); @@ -46,6 +52,9 @@ private bool Publish(string operationId, BackplaneMessage message, FusionCacheEn { ProcessError(operationId, cacheKey, exc, actionDescription); + // ACTIVITY + Activity.Current?.SetStatus(ActivityStatusCode.Error, exc.Message); + if (exc is not SyntheticTimeoutException && options.ReThrowBackplaneExceptions) { if (_options.ReThrowOriginalExceptions) diff --git a/src/ZiggyCreatures.FusionCache/Internals/Builder/FusionCacheBuilder.cs b/src/ZiggyCreatures.FusionCache/Internals/Builder/FusionCacheBuilder.cs index 78ef325d..88cd5aa8 100644 --- a/src/ZiggyCreatures.FusionCache/Internals/Builder/FusionCacheBuilder.cs +++ b/src/ZiggyCreatures.FusionCache/Internals/Builder/FusionCacheBuilder.cs @@ -7,8 +7,8 @@ using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; using ZiggyCreatures.Caching.Fusion.Backplane; +using ZiggyCreatures.Caching.Fusion.Locking; using ZiggyCreatures.Caching.Fusion.Plugins; -using ZiggyCreatures.Caching.Fusion.Reactors; using ZiggyCreatures.Caching.Fusion.Serialization; namespace ZiggyCreatures.Caching.Fusion.Internals.Builder; @@ -24,14 +24,14 @@ public FusionCacheBuilder(string cacheName) UseRegisteredOptions = true; - UseRegisteredReactor = true; + UseRegisteredMemoryLocker = true; UseRegisteredSerializer = true; IgnoreRegisteredMemoryDistributedCache = true; - Plugins = new List(); - PluginsFactories = new List>(); + Plugins = []; + PluginsFactories = []; } public string CacheName { get; } @@ -46,7 +46,10 @@ public FusionCacheBuilder(string cacheName) public Func? MemoryCacheFactory { get; set; } public bool ThrowIfMissingMemoryCache { get; set; } - private bool UseRegisteredReactor { get; set; } + public bool UseRegisteredMemoryLocker { get; set; } + public IFusionCacheMemoryLocker? MemoryLocker { get; set; } + public Func? MemoryLockerFactory { get; set; } + public bool ThrowIfMissingMemoryLocker { get; set; } public bool UseRegisteredOptions { get; set; } public FusionCacheOptions? Options { get; set; } @@ -169,16 +172,29 @@ public IFusionCache Build(IServiceProvider serviceProvider) throw new InvalidOperationException("A memory cache has not been specified, or found in the DI container."); } - // REACTOR - IFusionCacheReactor? reactor = null; + // MEMORY LOCKER + IFusionCacheMemoryLocker? memoryLocker = null; - if (UseRegisteredReactor) + if (UseRegisteredMemoryLocker) { - reactor = serviceProvider.GetService(); + memoryLocker = serviceProvider.GetService(); + } + else if (MemoryLockerFactory is not null) + { + memoryLocker = MemoryLockerFactory?.Invoke(serviceProvider); + } + else + { + memoryLocker = MemoryLocker; + } + + if (memoryLocker is null && ThrowIfMissingMemoryLocker) + { + throw new InvalidOperationException("A memory locker has not been specified, or found in the DI container."); } // CREATE THE CACHE - var cache = new FusionCache(options, memoryCache, logger, reactor); + var cache = new FusionCache(options, memoryCache, logger, memoryLocker); // DISTRIBUTED CACHE IDistributedCache? distributedCache; @@ -262,7 +278,7 @@ public IFusionCache Build(IServiceProvider serviceProvider) } // PLUGINS - List plugins = new List(); + List plugins = []; if (UseAllRegisteredPlugins) { diff --git a/src/ZiggyCreatures.FusionCache/Internals/Diagnostics/Activities.cs b/src/ZiggyCreatures.FusionCache/Internals/Diagnostics/Activities.cs new file mode 100644 index 00000000..b2330ec7 --- /dev/null +++ b/src/ZiggyCreatures.FusionCache/Internals/Diagnostics/Activities.cs @@ -0,0 +1,97 @@ +ο»Ώusing System.Collections.Generic; +using System.Diagnostics; + +namespace ZiggyCreatures.Caching.Fusion.Internals.Diagnostics +{ + internal static class Activities + { + public static readonly ActivitySource Source = new ActivitySource(FusionCacheDiagnostics.ActivitySourceName, FusionCacheDiagnostics.FusionCacheVersion); + public static readonly ActivitySource SourceMemoryLevel = new ActivitySource(FusionCacheDiagnostics.ActivitySourceNameMemoryLevel, FusionCacheDiagnostics.FusionCacheVersion); + public static readonly ActivitySource SourceDistributedLevel = new ActivitySource(FusionCacheDiagnostics.ActivitySourceNameDistributedLevel, FusionCacheDiagnostics.FusionCacheVersion); + public static readonly ActivitySource SourceBackplane = new ActivitySource(FusionCacheDiagnostics.ActivitySourceNameBackplane, FusionCacheDiagnostics.FusionCacheVersion); + + internal static class Names + { + // HIGH-LEVEL + public const string Set = "set to cache"; + public const string TryGet = "get from cache"; + public const string GetOrDefault = "get from cache"; + public const string GetOrSet = "get or set from cache"; + public const string Expire = "expire from cache"; + public const string Remove = "remove from cache"; + + // MEMORY + public const string MemorySet = "set to cache level"; + public const string MemoryGet = "get from cache level"; + public const string MemoryExpire = "expire from cache level"; + public const string MemoryRemove = "remove from cache level"; + + // DISTRIBUTED + public const string DistributedSet = "set to cache level"; + public const string DistributedGet = "get from cache level"; + public const string DistributedRemove = "remove from cache level"; + + // BACKPLANE + public const string BackplanePublish = "publish to backplane"; + public const string BackplaneReceive = "receive from backplane"; + + // FACTORY + public const string ExecuteFactory = "execute factory"; + + // AUTO-RECOVERY + public const string AutoRecoveryProcessQueue = "process auto-recovery queue"; + public const string AutoRecoveryProcessItem = "process auto-recovery item"; + } + + internal static class EventNames + { + public const string FactoryBackgroundMove = "factory moved to the background"; + public const string FactoryBackgroundMoveNotAllowed = "factory not allowed to be moved to the background"; + } + + public static IEnumerable> GetCommonTags(string? cacheName, string? cacheInstanceId, string? key, string? operationId, CacheLevelKind? levelKind) + { + var res = new List> + { + new KeyValuePair("fusioncache.cache.name", cacheName), + new KeyValuePair("fusioncache.cache.instance_id", cacheInstanceId), + new KeyValuePair("fusioncache.operation.key", key), + new KeyValuePair("fusioncache.operation.operation_id", operationId), + }; + + if (levelKind is not null) + res.Add(new KeyValuePair("fusioncache.operation.level", levelKind.ToString().ToLowerInvariant())); + + return res; + } + + public static Activity? StartActivityWithCommonTags(this ActivitySource source, string activityName, string? cacheName, string? cacheInstanceId, string? key, string? operationId, CacheLevelKind? levelKind = null) + { + if (source.HasListeners() == false) + return null; + + return source.StartActivity( + ActivityKind.Internal, + tags: GetCommonTags(cacheName, cacheInstanceId, key, operationId, levelKind), + name: activityName + ); + } + + //public static Activity? StartActivityWithCommonTags(this ActivitySource source, bool isLinked, string activityName, string? cacheName, string? cacheInstanceId, string? key, string? operationId) + //{ + // if (source.HasListeners() == false) + // return null; + + // // THIS OK? + // return source.StartActivity( + // name, + // ActivityKind.Internal, + // new ActivityContext(), + // tags: GetCommonTags(cacheName, cacheInstanceId, key, operationId), + // links: isLinked + // ? new List { new ActivityLink(Activity.Current?.Context ?? new ActivityContext()) } + // : null + // ); + //} + } +} diff --git a/src/ZiggyCreatures.FusionCache/Internals/Diagnostics/CacheLevelKind.cs b/src/ZiggyCreatures.FusionCache/Internals/Diagnostics/CacheLevelKind.cs new file mode 100644 index 00000000..10674eb8 --- /dev/null +++ b/src/ZiggyCreatures.FusionCache/Internals/Diagnostics/CacheLevelKind.cs @@ -0,0 +1,8 @@ +ο»Ώnamespace ZiggyCreatures.Caching.Fusion.Internals.Diagnostics +{ + internal enum CacheLevelKind + { + Memory = 0, + Distributed = 1 + } +} diff --git a/src/ZiggyCreatures.FusionCache/Internals/Diagnostics/Metrics.cs b/src/ZiggyCreatures.FusionCache/Internals/Diagnostics/Metrics.cs new file mode 100644 index 00000000..29c28cc5 --- /dev/null +++ b/src/ZiggyCreatures.FusionCache/Internals/Diagnostics/Metrics.cs @@ -0,0 +1,91 @@ +ο»Ώusing System.Collections.Generic; +using System.Diagnostics.Metrics; + +namespace ZiggyCreatures.Caching.Fusion.Internals.Diagnostics +{ + internal static class Metrics + { + public static readonly Meter Meter = new Meter(FusionCacheDiagnostics.MeterName, FusionCacheDiagnostics.FusionCacheVersion); + public static readonly Meter MeterMemoryLevel = new Meter(FusionCacheDiagnostics.MeterNameMemoryLevel, FusionCacheDiagnostics.FusionCacheVersion); + public static readonly Meter MeterDistributedLevel = new Meter(FusionCacheDiagnostics.MeterNameDistributedLevel, FusionCacheDiagnostics.FusionCacheVersion); + public static readonly Meter MeterBackplane = new Meter(FusionCacheDiagnostics.MeterNameBackplane, FusionCacheDiagnostics.FusionCacheVersion); + + // HIGH-LEVEL + public static readonly Counter CounterSet = Meter.CreateCounter("fusioncache.cache.set"); + public static readonly Counter CounterTryGet = Meter.CreateCounter("fusioncache.cache.try_get"); + public static readonly Counter CounterGetOrDefault = Meter.CreateCounter("fusioncache.cache.get_or_default"); + public static readonly Counter CounterGetOrSet = Meter.CreateCounter("fusioncache.cache.get_or_set"); + public static readonly Counter CounterExpire = Meter.CreateCounter("fusioncache.cache.expire"); + public static readonly Counter CounterRemove = Meter.CreateCounter("fusioncache.cache.remove"); + + public static readonly Counter CounterHit = Meter.CreateCounter("fusioncache.cache.hit"); + public static readonly Counter CounterMiss = Meter.CreateCounter("fusioncache.cache.miss"); + + // FACTORY + public static readonly Counter CounterFactorySyntheticTimeout = Meter.CreateCounter("fusioncache.factory.synthetic_timeout"); + public static readonly Counter CounterFactoryError = Meter.CreateCounter("fusioncache.factory.error"); + public static readonly Counter CounterFactorySuccess = Meter.CreateCounter("fusioncache.factory.success"); + + // FAIL-SAFE + public static readonly Counter CounterFailSafeActivate = Meter.CreateCounter("fusioncache.failsafe_activate"); + + // EAGER REFRESH + public static readonly Counter CounterEagerRefresh = Meter.CreateCounter("fusioncache.eager_refresh"); + + // MEMORY + public static readonly Counter CounterMemorySet = MeterMemoryLevel.CreateCounter("fusioncache.memory.set"); + public static readonly Counter CounterMemoryGet = MeterMemoryLevel.CreateCounter("fusioncache.memory.get"); + public static readonly Counter CounterMemoryExpire = MeterMemoryLevel.CreateCounter("fusioncache.memory.expire"); + public static readonly Counter CounterMemoryRemove = MeterMemoryLevel.CreateCounter("fusioncache.memory.remove"); + public static readonly Counter CounterMemoryEvict = MeterMemoryLevel.CreateCounter("fusioncache.memory.evict"); + public static readonly Counter CounterMemoryHit = MeterMemoryLevel.CreateCounter("fusioncache.memory.hit"); + public static readonly Counter CounterMemoryMiss = MeterMemoryLevel.CreateCounter("fusioncache.memory.miss"); + + // DISTRIBUTED + public static readonly Counter CounterDistributedSet = MeterDistributedLevel.CreateCounter("fusioncache.distributed.set"); + public static readonly Counter CounterDistributedGet = MeterDistributedLevel.CreateCounter("fusioncache.distributed.get"); + public static readonly Counter CounterDistributedRemove = MeterDistributedLevel.CreateCounter("fusioncache.distributed.remove"); + public static readonly Counter CounterDistributedHit = MeterDistributedLevel.CreateCounter("fusioncache.distributed.hit"); + public static readonly Counter CounterDistributedMiss = MeterDistributedLevel.CreateCounter("fusioncache.distributed.miss"); + public static readonly Counter CounterDistributedCircuitBreakerChange = MeterDistributedLevel.CreateCounter("fusioncache.distributed.circuit_breaker_change"); + + // SERIALIZATION + public static readonly Counter CounterSerializationError = MeterDistributedLevel.CreateCounter("fusioncache.serialize_error"); + public static readonly Counter CounterDeserializationError = MeterDistributedLevel.CreateCounter("fusioncache.deserialize_error"); + + // BACKPLANE + public static readonly Counter CounterBackplanePublish = MeterBackplane.CreateCounter("fusioncache.backplane.publish"); + public static readonly Counter CounterBackplaneReceive = MeterBackplane.CreateCounter("fusioncache.backplane.receive"); + public static readonly Counter CounterBackplaneCircuitBreakerChange = MeterBackplane.CreateCounter("fusioncache.backplane.circuit_breaker_change"); + + public static Counter? Maybe(this Counter counter) + where T : struct + { + if (counter.Enabled == false) + return null; + + return counter; + } + + public static KeyValuePair[] GetCommonTags(string name, string? instanceId, params KeyValuePair[] extraTags) + { + return [ + new KeyValuePair("fusioncache.cache.name", name), + new KeyValuePair("fusioncache.cache.instance_id", instanceId), + // NOTE: NOT THE NEXT ONES SINCE, WITH METRICS, PEOPLE ARE USUALLY CHARGED PER UNIQUE ATTRIBUTES + //new KeyValuePair("fusioncache.operation.key", key), + //new KeyValuePair("fusioncache.operation.operation_id", operationId), + .. extraTags ?? [] + ]; + } + + public static void AddWithCommonTags(this Counter counter, T delta, string cacheName, string cacheInstanceId, params KeyValuePair[] extraTags) + where T : struct + { + if (counter.Enabled == false) + return; + + counter.Add(delta, GetCommonTags(cacheName, cacheInstanceId, extraTags)); + } + } +} diff --git a/src/ZiggyCreatures.FusionCache/Internals/Distributed/DistributedCacheAccessor_Async.cs b/src/ZiggyCreatures.FusionCache/Internals/Distributed/DistributedCacheAccessor_Async.cs index 1db6a6fe..a39e3d59 100644 --- a/src/ZiggyCreatures.FusionCache/Internals/Distributed/DistributedCacheAccessor_Async.cs +++ b/src/ZiggyCreatures.FusionCache/Internals/Distributed/DistributedCacheAccessor_Async.cs @@ -1,7 +1,9 @@ ο»Ώusing System; +using System.Diagnostics; using System.Threading; using System.Threading.Tasks; using Microsoft.Extensions.Logging; +using ZiggyCreatures.Caching.Fusion.Internals.Diagnostics; namespace ZiggyCreatures.Caching.Fusion.Internals.Distributed; @@ -26,6 +28,9 @@ private async ValueTask ExecuteOperationAsync(string operationId, string k { ProcessError(operationId, key, exc, actionDescription); + // ACTIVITY + Activity.Current?.SetStatus(ActivityStatusCode.Error, exc.Message); + if (exc is not SyntheticTimeoutException && options.ReThrowDistributedCacheExceptions) { if (_options.ReThrowOriginalExceptions) @@ -51,6 +56,9 @@ public async ValueTask SetEntryAsync(string operationId, string ke token.ThrowIfCancellationRequested(); + // ACTIVITY + using var activity = Activities.SourceDistributedLevel.StartActivityWithCommonTags(Activities.Names.DistributedSet, _options.CacheName, _options.InstanceId!, key, operationId, CacheLevelKind.Distributed); + // 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) { @@ -74,6 +82,9 @@ public async ValueTask SetEntryAsync(string operationId, string ke if (_logger?.IsEnabled(_options.SerializationErrorsLogLevel) ?? false) _logger.Log(_options.SerializationErrorsLogLevel, exc, "FUSION [N={CacheName} I={CacheInstanceId}] (O={CacheOperationId} K={CacheKey}): [DC] an error occurred while serializing an entry {Entry}", _options.CacheName, _options.InstanceId, operationId, key, distributedEntry.ToLogString()); + // ACTIVITY + activity?.SetStatus(ActivityStatusCode.Error, exc.Message); + // EVENT _events.OnSerializationError(operationId, key); @@ -116,12 +127,17 @@ public async ValueTask SetEntryAsync(string operationId, string ke public async ValueTask<(FusionCacheDistributedEntry? entry, bool isValid)> TryGetEntryAsync(string operationId, string key, FusionCacheEntryOptions options, bool hasFallbackValue, TimeSpan? timeout, CancellationToken token) { + Metrics.CounterDistributedGet.Maybe()?.AddWithCommonTags(1, _options.CacheName, _options.InstanceId!); + if (IsCurrentlyUsable(operationId, key) == false) return (null, false); if (_logger?.IsEnabled(LogLevel.Trace) ?? false) _logger.Log(LogLevel.Trace, "FUSION [N={CacheName} I={CacheInstanceId}] (O={CacheOperationId} K={CacheKey}): [DC] trying to get entry from distributed", _options.CacheName, _options.InstanceId, operationId, key); + // ACTIVITY + using var activity = Activities.SourceDistributedLevel.StartActivityWithCommonTags(Activities.Names.DistributedGet, _options.CacheName, _options.InstanceId!, key, operationId, CacheLevelKind.Distributed); + // GET FROM DISTRIBUTED CACHE byte[]? data; try @@ -137,6 +153,10 @@ public async ValueTask SetEntryAsync(string operationId, string ke catch (Exception exc) { ProcessError(operationId, key, exc, "getting entry from distributed"); + + // ACTIVITY + activity?.SetStatus(ActivityStatusCode.Error, exc.Message); + if (exc is not SyntheticTimeoutException && options.ReThrowDistributedCacheExceptions) { if (_options.ReThrowOriginalExceptions) @@ -153,7 +173,12 @@ public async ValueTask SetEntryAsync(string operationId, string ke } if (data is null) + { + if (_logger?.IsEnabled(LogLevel.Debug) ?? false) + _logger.Log(LogLevel.Debug, "FUSION [N={CacheName} I={CacheInstanceId}] (O={CacheOperationId} K={CacheKey}): [DC] distributed entry not found", _options.CacheName, _options.InstanceId, operationId, key); + return (null, false); + } // DESERIALIZATION try @@ -198,6 +223,9 @@ public async ValueTask SetEntryAsync(string operationId, string ke if (_logger?.IsEnabled(_options.SerializationErrorsLogLevel) ?? false) _logger.Log(_options.SerializationErrorsLogLevel, exc, "FUSION [N={CacheName} I={CacheInstanceId}] (O={CacheOperationId} K={CacheKey}): [DC] an error occurred while deserializing an entry", _options.CacheName, _options.InstanceId, operationId, key); + // ACTIVITY + activity?.SetStatus(ActivityStatusCode.Error, exc.Message); + // EVENT _events.OnDeserializationError(operationId, key); @@ -225,6 +253,9 @@ public async ValueTask RemoveEntryAsync(string operationId, string key, Fu if (IsCurrentlyUsable(operationId, key) == false) return false; + // ACTIVITY + using var activity = Activities.SourceDistributedLevel.StartActivityWithCommonTags(Activities.Names.DistributedRemove, _options.CacheName, _options.InstanceId!, key, operationId, CacheLevelKind.Distributed); + return await ExecuteOperationAsync( operationId, key, diff --git a/src/ZiggyCreatures.FusionCache/Internals/Distributed/DistributedCacheAccessor_Sync.cs b/src/ZiggyCreatures.FusionCache/Internals/Distributed/DistributedCacheAccessor_Sync.cs index d1673b9e..a5038da2 100644 --- a/src/ZiggyCreatures.FusionCache/Internals/Distributed/DistributedCacheAccessor_Sync.cs +++ b/src/ZiggyCreatures.FusionCache/Internals/Distributed/DistributedCacheAccessor_Sync.cs @@ -1,6 +1,8 @@ ο»Ώusing System; +using System.Diagnostics; using System.Threading; using Microsoft.Extensions.Logging; +using ZiggyCreatures.Caching.Fusion.Internals.Diagnostics; namespace ZiggyCreatures.Caching.Fusion.Internals.Distributed; @@ -25,6 +27,9 @@ private bool ExecuteOperation(string operationId, string key, Action(string operationId, string key, IFusionCacheEntry e token.ThrowIfCancellationRequested(); + // ACTIVITY + using var activity = Activities.SourceDistributedLevel.StartActivityWithCommonTags(Activities.Names.DistributedSet, _options.CacheName, _options.InstanceId!, key, operationId, CacheLevelKind.Distributed); + // 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) { @@ -73,6 +81,9 @@ public bool SetEntry(string operationId, string key, IFusionCacheEntry e if (_logger?.IsEnabled(_options.SerializationErrorsLogLevel) ?? false) _logger.Log(_options.SerializationErrorsLogLevel, exc, "FUSION [N={CacheName} I={CacheInstanceId}] (O={CacheOperationId} K={CacheKey}): [DC] an error occurred while serializing an entry {Entry}", _options.CacheName, _options.InstanceId, operationId, key, distributedEntry.ToLogString()); + // ACTIVITY + activity?.SetStatus(ActivityStatusCode.Error, exc.Message); + // EVENT _events.OnSerializationError(operationId, key); @@ -115,12 +126,17 @@ public bool SetEntry(string operationId, string key, IFusionCacheEntry e public (FusionCacheDistributedEntry? entry, bool isValid) TryGetEntry(string operationId, string key, FusionCacheEntryOptions options, bool hasFallbackValue, TimeSpan? timeout, CancellationToken token) { + Metrics.CounterDistributedGet.Maybe()?.AddWithCommonTags(1, _options.CacheName, _options.InstanceId!); + if (IsCurrentlyUsable(operationId, key) == false) return (null, false); if (_logger?.IsEnabled(LogLevel.Trace) ?? false) _logger.Log(LogLevel.Trace, "FUSION [N={CacheName} I={CacheInstanceId}] (O={CacheOperationId} K={CacheKey}): [DC] trying to get entry from distributed", _options.CacheName, _options.InstanceId, operationId, key); + // ACTIVITY + using var activity = Activities.SourceDistributedLevel.StartActivityWithCommonTags(Activities.Names.DistributedGet, _options.CacheName, _options.InstanceId!, key, operationId, CacheLevelKind.Distributed); + // GET FROM DISTRIBUTED CACHE byte[]? data; try @@ -136,6 +152,10 @@ public bool SetEntry(string operationId, string key, IFusionCacheEntry e catch (Exception exc) { ProcessError(operationId, key, exc, "getting entry from distributed"); + + // ACTIVITY + activity?.SetStatus(ActivityStatusCode.Error, exc.Message); + if (exc is not SyntheticTimeoutException && options.ReThrowDistributedCacheExceptions) { if (_options.ReThrowOriginalExceptions) @@ -152,7 +172,12 @@ public bool SetEntry(string operationId, string key, IFusionCacheEntry e } if (data is null) + { + if (_logger?.IsEnabled(LogLevel.Debug) ?? false) + _logger.Log(LogLevel.Debug, "FUSION [N={CacheName} I={CacheInstanceId}] (O={CacheOperationId} K={CacheKey}): [DC] distributed entry not found", _options.CacheName, _options.InstanceId, operationId, key); + return (null, false); + } // DESERIALIZATION try @@ -197,6 +222,9 @@ public bool SetEntry(string operationId, string key, IFusionCacheEntry e if (_logger?.IsEnabled(_options.SerializationErrorsLogLevel) ?? false) _logger.Log(_options.SerializationErrorsLogLevel, exc, "FUSION [N={CacheName} I={CacheInstanceId}] (O={CacheOperationId} K={CacheKey}): [DC] an error occurred while deserializing an entry", _options.CacheName, _options.InstanceId, operationId, key); + // ACTIVITY + activity?.SetStatus(ActivityStatusCode.Error, exc.Message); + // EVENT _events.OnDeserializationError(operationId, key); @@ -224,6 +252,9 @@ public bool RemoveEntry(string operationId, string key, FusionCacheEntryOptions if (IsCurrentlyUsable(operationId, key) == false) return false; + // ACTIVITY + using var activity = Activities.SourceDistributedLevel.StartActivityWithCommonTags(Activities.Names.DistributedRemove, _options.CacheName, _options.InstanceId!, key, operationId, CacheLevelKind.Distributed); + return ExecuteOperation( operationId, key, diff --git a/src/ZiggyCreatures.FusionCache/Internals/Distributed/FusionCacheDistributedEntry.cs b/src/ZiggyCreatures.FusionCache/Internals/Distributed/FusionCacheDistributedEntry.cs index 498199c4..7e71a070 100644 --- a/src/ZiggyCreatures.FusionCache/Internals/Distributed/FusionCacheDistributedEntry.cs +++ b/src/ZiggyCreatures.FusionCache/Internals/Distributed/FusionCacheDistributedEntry.cs @@ -5,7 +5,7 @@ namespace ZiggyCreatures.Caching.Fusion.Internals.Distributed; /// -/// An entry in a distributed layer. +/// An entry in a distributed level. /// /// The type of the entry's value [DataContract] diff --git a/src/ZiggyCreatures.FusionCache/Internals/FusionCacheInternalUtils.cs b/src/ZiggyCreatures.FusionCache/Internals/FusionCacheInternalUtils.cs index 16affa21..69c76e9c 100644 --- a/src/ZiggyCreatures.FusionCache/Internals/FusionCacheInternalUtils.cs +++ b/src/ZiggyCreatures.FusionCache/Internals/FusionCacheInternalUtils.cs @@ -1,4 +1,6 @@ ο»Ώusing System; +using System.Collections.Concurrent; +using System.Collections.Generic; using System.Runtime.CompilerServices; using System.Threading; using System.Threading.Tasks; @@ -61,6 +63,15 @@ public static string MaybeGenerateOperationId(ILogger? logger) return GenerateOperationId(); } + // SEE HERE: https://devblogs.microsoft.com/pfxteam/little-known-gems-atomic-conditional-removals-from-concurrentdictionary/ + public static bool TryRemove(this ConcurrentDictionary dictionary, TKey key, TValue value) + { + if (dictionary is null) + throw new ArgumentNullException(nameof(dictionary)); + + return ((ICollection>)dictionary).Remove(new KeyValuePair(key, value)); + } + /// /// Checks if the entry is logically expired. /// diff --git a/src/ZiggyCreatures.FusionCache/Internals/Memory/FusionCacheMemoryEntry.cs b/src/ZiggyCreatures.FusionCache/Internals/Memory/FusionCacheMemoryEntry.cs index 5d8907d7..02c8b33f 100644 --- a/src/ZiggyCreatures.FusionCache/Internals/Memory/FusionCacheMemoryEntry.cs +++ b/src/ZiggyCreatures.FusionCache/Internals/Memory/FusionCacheMemoryEntry.cs @@ -4,7 +4,7 @@ namespace ZiggyCreatures.Caching.Fusion.Internals.Memory; /// -/// An entry in a memory layer. +/// An entry in a memory level. /// internal sealed class FusionCacheMemoryEntry : IFusionCacheEntry diff --git a/src/ZiggyCreatures.FusionCache/Internals/Memory/MemoryCacheAccessor.cs b/src/ZiggyCreatures.FusionCache/Internals/Memory/MemoryCacheAccessor.cs index d81012bf..d95ce39f 100644 --- a/src/ZiggyCreatures.FusionCache/Internals/Memory/MemoryCacheAccessor.cs +++ b/src/ZiggyCreatures.FusionCache/Internals/Memory/MemoryCacheAccessor.cs @@ -2,6 +2,7 @@ using Microsoft.Extensions.Caching.Memory; using Microsoft.Extensions.Logging; using ZiggyCreatures.Caching.Fusion.Events; +using ZiggyCreatures.Caching.Fusion.Internals.Diagnostics; namespace ZiggyCreatures.Caching.Fusion.Internals.Memory; @@ -32,6 +33,9 @@ public MemoryCacheAccessor(IMemoryCache? memoryCache, FusionCacheOptions options public void SetEntry(string operationId, string key, FusionCacheMemoryEntry entry, FusionCacheEntryOptions options) { + // ACTIVITY + using var activity = Activities.SourceMemoryLevel.StartActivityWithCommonTags(Activities.Names.MemorySet, _options.CacheName, _options.InstanceId!, key, operationId, CacheLevelKind.Memory); + // IF FAIL-SAFE IS DISABLED AND DURATION IS <= ZERO -> REMOVE ENTRY (WILL SAVE RESOURCES) if (options.IsFailSafeEnabled == false && options.Duration <= TimeSpan.Zero) { @@ -54,11 +58,33 @@ public void SetEntry(string operationId, string key, FusionCacheMemoryEn public FusionCacheMemoryEntry? GetEntryOrNull(string operationId, string key) { - return _cache.Get(key); + Metrics.CounterMemoryGet.Maybe()?.AddWithCommonTags(1, _options.CacheName, _options.InstanceId!); + + // ACTIVITY + using var activity = Activities.SourceMemoryLevel.StartActivityWithCommonTags(Activities.Names.MemoryGet, _options.CacheName, _options.InstanceId!, key, operationId, CacheLevelKind.Memory); + + var entry = _cache.Get(key); + + // EVENT + if (entry is not null) + { + _events.OnHit(operationId, key, entry.IsLogicallyExpired()); + } + else + { + _events.OnMiss(operationId, key); + } + + return entry; } public (FusionCacheMemoryEntry? entry, bool isValid) TryGetEntry(string operationId, string key) { + Metrics.CounterMemoryGet.Maybe()?.AddWithCommonTags(1, _options.CacheName, _options.InstanceId!); + + // ACTIVITY + using var activity = Activities.SourceMemoryLevel.StartActivityWithCommonTags(Activities.Names.MemoryGet, _options.CacheName, _options.InstanceId!, key, operationId, CacheLevelKind.Memory); + FusionCacheMemoryEntry? entry; bool isValid = false; @@ -101,6 +127,9 @@ public void SetEntry(string operationId, string key, FusionCacheMemoryEn public void RemoveEntry(string operationId, string key, FusionCacheEntryOptions options) { + // ACTIVITY + using var activity = Activities.SourceMemoryLevel.StartActivityWithCommonTags(Activities.Names.MemoryRemove, _options.CacheName, _options.InstanceId!, key, operationId, CacheLevelKind.Memory); + if (_logger?.IsEnabled(LogLevel.Debug) ?? false) _logger.Log(LogLevel.Debug, "FUSION [N={CacheName} I={CacheInstanceId}] (O={CacheOperationId} K={CacheKey}): [MC] removing data (from memory)", _options.CacheName, _options.InstanceId, operationId, key); @@ -112,6 +141,9 @@ public void RemoveEntry(string operationId, string key, FusionCacheEntryOptions public bool ExpireEntry(string operationId, string key, bool allowFailSafe, long? timestampThreshold) { + // ACTIVITY + using var activity = Activities.SourceMemoryLevel.StartActivityWithCommonTags(Activities.Names.MemoryExpire, _options.CacheName, _options.InstanceId!, key, operationId, CacheLevelKind.Memory); + var entry = _cache.Get(key); if (entry is null) diff --git a/src/ZiggyCreatures.FusionCache/Locking/IFusionCacheMemoryLocker.cs b/src/ZiggyCreatures.FusionCache/Locking/IFusionCacheMemoryLocker.cs new file mode 100644 index 00000000..d6032cf9 --- /dev/null +++ b/src/ZiggyCreatures.FusionCache/Locking/IFusionCacheMemoryLocker.cs @@ -0,0 +1,50 @@ +ο»Ώusing System; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.Extensions.Logging; + +namespace ZiggyCreatures.Caching.Fusion.Locking; + +/// +/// A FusionCache component to handle acquiring and releasing memory locks in a highly optimized way. +/// +public interface IFusionCacheMemoryLocker + : IDisposable +{ + /// + /// Acquire a generic lock, used to synchronize multiple factory operating on the same cache key, and return it. + /// + /// The CacheName of the FusionCache instance. + /// The InstanceId 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 generic lock object, later released when the critical section is over. + /// An optional to cancel the operation. + ValueTask AcquireLockAsync(string cacheName, string cacheInstanceId, 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 InstanceId 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 acquired genericlock object, later released when the critical section is over. + /// The to use, if any. + /// An optional to cancel the operation. + object? AcquireLock(string cacheName, string cacheInstanceId, string key, string operationId, TimeSpan timeout, ILogger? logger, CancellationToken token); + + /// + /// Release the generic lock object. + /// + /// The name of the FusionCache instance. + /// The InstanceId 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 cacheName, string cacheInstanceId, string key, string operationId, object? lockObj, ILogger? logger); +} diff --git a/src/ZiggyCreatures.FusionCache/Locking/ProbabilisticMemoryLocker.cs b/src/ZiggyCreatures.FusionCache/Locking/ProbabilisticMemoryLocker.cs new file mode 100644 index 00000000..6a3e9f15 --- /dev/null +++ b/src/ZiggyCreatures.FusionCache/Locking/ProbabilisticMemoryLocker.cs @@ -0,0 +1,178 @@ +ο»Ώusing System; +using System.Runtime.CompilerServices; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.Extensions.Logging; + +namespace ZiggyCreatures.Caching.Fusion.Locking; + +/// +/// An implementation of based on a probabilistic approach. +///

+/// ⚠️ WARNING: this type of locker may lead to deadlocks, so be careful. +///
+internal sealed class ProbabilisticMemoryLocker + : IFusionCacheMemoryLocker +{ + private readonly int _poolSize; + private SemaphoreSlim[] _pool; + + /// + /// Initializes a new instance of the class. + /// + /// The size of the pool used internally. + public ProbabilisticMemoryLocker(int poolSize = 8_440) + { + _poolSize = poolSize; + _pool = new SemaphoreSlim[_poolSize]; + for (var i = 0; i < _pool.Length; i++) + { + _pool[i] = new SemaphoreSlim(1, 1); + } + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private uint GetLockIndex(string key) + { + return unchecked((uint)key.GetHashCode()) % (uint)_poolSize; + } + + /// + public async ValueTask AcquireLockAsync(string cacheName, string cacheInstanceId, string key, string operationId, TimeSpan timeout, ILogger? logger, CancellationToken token) + { + var idx = GetLockIndex(key); + var semaphore = _pool[idx]; + + //if (logger?.IsEnabled(LogLevel.Trace) ?? false) + // logger.Log(LogLevel.Trace, "FUSION [N={CacheName} I={CacheInstanceId}] (O={CacheOperationId} K={CacheKey}): trying to fast-acquire the LOCK", cacheName, cacheInstanceId, operationId, key); + + //var acquired = semaphore.Wait(0); + //if (acquired) + //{ + // _lockPoolKeys[idx] = key; + // if (logger?.IsEnabled(LogLevel.Trace) ?? false) + // logger.Log(LogLevel.Trace, "FUSION [N={CacheName} I={CacheInstanceId}] (O={CacheOperationId} K={CacheKey}): LOCK fast-acquired", cacheName, cacheInstanceId, operationId, key); + + // return semaphore; + //} + + //if (logger?.IsEnabled(LogLevel.Trace) ?? false) + // logger.Log(LogLevel.Trace, "FUSION [N={CacheName} I={CacheInstanceId}] (O={CacheOperationId} K={CacheKey}): LOCK already taken", cacheName, cacheInstanceId, operationId, key); + + //var key2 = _lockPoolKeys[idx]; + //if (key2 != key) + //{ + // if (logger?.IsEnabled(LogLevel.Trace) ?? false) + // logger.Log(LogLevel.Trace, "FUSION [N={CacheName} I={CacheInstanceId}] (O={CacheOperationId} K={CacheKey}): LOCK " + (key2 is null ? "maybe " : string.Empty) + "acquired for a different key (current key: " + key + ", other key: " + key2 + ")", cacheName, cacheInstanceId, operationId, key); + + // Interlocked.Increment(ref _lockPoolCollisions); + //} + + if (logger?.IsEnabled(LogLevel.Trace) ?? false) + logger.Log(LogLevel.Trace, "FUSION [N={CacheName} I={CacheInstanceId}] (O={CacheOperationId} K={CacheKey}): waiting to acquire the LOCK", cacheName, cacheInstanceId, operationId, key); + + var acquired = await semaphore.WaitAsync(timeout, token).ConfigureAwait(false); + + //_lockPoolKeys[idx] = key; + + if (logger?.IsEnabled(LogLevel.Trace) ?? false) + logger.Log(LogLevel.Trace, "FUSION [N={CacheName} I={CacheInstanceId}] (O={CacheOperationId} K={CacheKey}): LOCK acquired", cacheName, cacheInstanceId, operationId, key); + + return acquired ? semaphore : null; + } + + /// + public object? AcquireLock(string cacheName, string cacheInstanceId, string key, string operationId, TimeSpan timeout, ILogger? logger, CancellationToken token) + { + var idx = GetLockIndex(key); + var semaphore = _pool[idx]; + + //if (logger?.IsEnabled(LogLevel.Trace) ?? false) + // logger.Log(LogLevel.Trace, "FUSION [N={CacheName} I={CacheInstanceId}] (O={CacheOperationId} K={CacheKey}): trying to fast-acquire the LOCK", cacheName, cacheInstanceId, operationId, key); + + //var acquired = semaphore.Wait(0); + //if (acquired) + //{ + // _lockPoolKeys[idx] = key; + // if (logger?.IsEnabled(LogLevel.Trace) ?? false) + // logger.Log(LogLevel.Trace, "FUSION [N={CacheName} I={CacheInstanceId}] (O={CacheOperationId} K={CacheKey}): LOCK fast-acquired", cacheName, cacheInstanceId, operationId, key); + + // return semaphore; + //} + + //if (logger?.IsEnabled(LogLevel.Trace) ?? false) + // logger.Log(LogLevel.Trace, "FUSION [N={CacheName} I={CacheInstanceId}] (O={CacheOperationId} K={CacheKey}): LOCK already taken", cacheName, cacheInstanceId, operationId, key); + + //var key2 = _lockPoolKeys[idx]; + //if (key2 != key) + //{ + // if (logger?.IsEnabled(LogLevel.Trace) ?? false) + // logger.Log(LogLevel.Trace, "FUSION [N={CacheName} I={CacheInstanceId}] (O={CacheOperationId} K={CacheKey}): LOCK " + (key2 is null ? "maybe " : string.Empty) + "acquired for a different key (current key: " + key + ", other key: " + key2 + ")", cacheName, cacheInstanceId, operationId, key); + + // Interlocked.Increment(ref _lockPoolCollisions); + //} + + //if (logger?.IsEnabled(LogLevel.Trace) ?? false) + // logger.Log(LogLevel.Trace, "FUSION [N={CacheName} I={CacheInstanceId}] (O={CacheOperationId} K={CacheKey}): waiting to acquire the LOCK", cacheName, cacheInstanceId, operationId, key); + + var acquired = semaphore.Wait(timeout, token); + + //_lockPoolKeys[idx] = key; + + if (logger?.IsEnabled(LogLevel.Trace) ?? false) + logger.Log(LogLevel.Trace, "FUSION [N={CacheName} I={CacheInstanceId}] (O={CacheOperationId} K={CacheKey}): LOCK acquired", cacheName, cacheInstanceId, operationId, key); + + return acquired ? semaphore : null; + } + + /// + public void ReleaseLock(string cacheName, string cacheInstanceId, string key, string operationId, object? lockObj, ILogger? logger) + { + if (lockObj is null) + return; + + //var idx = GetLockIndex(key); + //_lockPoolKeys[idx] = null; + + try + { + ((SemaphoreSlim)lockObj).Release(); + } + catch (Exception exc) + { + if (logger?.IsEnabled(LogLevel.Warning) ?? false) + logger.Log(LogLevel.Warning, exc, "FUSION [N={CacheName} I={CacheInstanceId}] (O={CacheOperationId} K={CacheKey}): an error occurred while trying to release a SemaphoreSlim in the memory locker", cacheName, cacheInstanceId, operationId, key); + } + } + + // IDISPOSABLE + private bool disposedValue; + private void Dispose(bool disposing) + { + if (!disposedValue) + { + if (disposing) + { + if (_pool is not null) + { + foreach (var semaphore in _pool) + { + semaphore.Dispose(); + } + } + } + +#pragma warning disable CS8625 // Cannot convert null literal to non-nullable reference type. + _pool = null; +#pragma warning restore CS8625 // Cannot convert null literal to non-nullable reference type. + disposedValue = true; + } + } + + /// + public void Dispose() + { + Dispose(disposing: true); + GC.SuppressFinalize(this); + } +} diff --git a/src/ZiggyCreatures.FusionCache/Reactors/FusionCacheReactorStandard.cs b/src/ZiggyCreatures.FusionCache/Locking/StandardMemoryLocker.cs similarity index 80% rename from src/ZiggyCreatures.FusionCache/Reactors/FusionCacheReactorStandard.cs rename to src/ZiggyCreatures.FusionCache/Locking/StandardMemoryLocker.cs index 843bba3f..71cdc1e5 100644 --- a/src/ZiggyCreatures.FusionCache/Reactors/FusionCacheReactorStandard.cs +++ b/src/ZiggyCreatures.FusionCache/Locking/StandardMemoryLocker.cs @@ -5,10 +5,13 @@ using Microsoft.Extensions.Caching.Memory; using Microsoft.Extensions.Logging; -namespace ZiggyCreatures.Caching.Fusion.Reactors; +namespace ZiggyCreatures.Caching.Fusion.Locking; -internal sealed class FusionCacheReactorStandard - : IFusionCacheReactor +/// +/// A standard implementation of . +/// +internal sealed class StandardMemoryLocker + : IFusionCacheMemoryLocker { private MemoryCache _lockCache; @@ -16,24 +19,23 @@ internal sealed class FusionCacheReactorStandard private readonly object[] _lockPool; private TimeSpan _slidingExpiration = TimeSpan.FromMinutes(5); - public FusionCacheReactorStandard(int reactorSize = 8_440) + /// + /// Initializes a new instance of the class. + /// + /// The size of the pool used internally for the 1st level locking strategy. + public StandardMemoryLocker(int size = 8_440) { _lockCache = new MemoryCache(new MemoryCacheOptions()); // LOCKING - _lockPoolSize = reactorSize; + _lockPoolSize = size; _lockPool = new object[_lockPoolSize]; - for (int i = 0; i < _lockPool.Length; i++) + for (var i = 0; i < _lockPool.Length; i++) { _lockPool[i] = new object(); } } - public int Collisions - { - get { return 0; } - } - [MethodImpl(MethodImplOptions.AggressiveInlining)] private uint GetLockIndex(string key) { @@ -42,31 +44,31 @@ private uint GetLockIndex(string key) private SemaphoreSlim GetSemaphore(string cacheName, string cacheInstanceId, string key, ILogger? logger) { - object _semaphore; + object? _semaphore; if (_lockCache.TryGetValue(key, out _semaphore)) - return (SemaphoreSlim)_semaphore; + return (SemaphoreSlim)_semaphore!; lock (_lockPool[GetLockIndex(key)]) { if (_lockCache.TryGetValue(key, out _semaphore)) - return (SemaphoreSlim)_semaphore; + return (SemaphoreSlim)_semaphore!; _semaphore = new SemaphoreSlim(1, 1); - using ICacheEntry entry = _lockCache.CreateEntry(key); + using var entry = _lockCache.CreateEntry(key); entry.Value = _semaphore; entry.SlidingExpiration = _slidingExpiration; entry.RegisterPostEvictionCallback((key, value, _, _) => { try { - ((SemaphoreSlim)value).Dispose(); + ((SemaphoreSlim?)value)?.Dispose(); } catch (Exception exc) { if (logger?.IsEnabled(LogLevel.Warning) ?? false) - logger.Log(LogLevel.Warning, exc, "FUSION [N={CacheName} I={CacheInstanceId}] (K={CacheKey}): an error occurred while trying to dispose a SemaphoreSlim in the reactor", cacheName, cacheInstanceId, key); + logger.Log(LogLevel.Warning, exc, "FUSION [N={CacheName} I={CacheInstanceId}] (K={CacheKey}): an error occurred while trying to dispose a SemaphoreSlim in the memory locker", cacheName, cacheInstanceId, key); } }); @@ -74,11 +76,9 @@ private SemaphoreSlim GetSemaphore(string cacheName, string cacheInstanceId, str } } - // ACQUIRE LOCK ASYNC + /// public async ValueTask AcquireLockAsync(string cacheName, string cacheInstanceId, string key, string operationId, TimeSpan timeout, ILogger? logger, CancellationToken token) { - token.ThrowIfCancellationRequested(); - var semaphore = GetSemaphore(cacheName, cacheInstanceId, key, logger); if (logger?.IsEnabled(LogLevel.Trace) ?? false) @@ -102,15 +102,15 @@ private SemaphoreSlim GetSemaphore(string cacheName, string cacheInstanceId, str return acquired ? semaphore : null; } - // ACQUIRE LOCK - public object? AcquireLock(string cacheName, string cacheInstanceId, string key, string operationId, TimeSpan timeout, ILogger? logger) + /// + public object? AcquireLock(string cacheName, string cacheInstanceId, string key, string operationId, TimeSpan timeout, ILogger? logger, CancellationToken token) { var semaphore = GetSemaphore(cacheName, cacheInstanceId, key, logger); if (logger?.IsEnabled(LogLevel.Trace) ?? false) logger.Log(LogLevel.Trace, "FUSION [N={CacheName} I={CacheInstanceId}] (O={CacheOperationId} K={CacheKey}): waiting to acquire the LOCK", cacheName, cacheInstanceId, operationId, key); - var acquired = semaphore.Wait(timeout); + var acquired = semaphore.Wait(timeout, token); if (acquired) { @@ -128,7 +128,7 @@ private SemaphoreSlim GetSemaphore(string cacheName, string cacheInstanceId, str return acquired ? semaphore : null; } - // RELEASE LOCK ASYNC + /// public void ReleaseLock(string cacheName, string cacheInstanceId, string key, string operationId, object? lockObj, ILogger? logger) { if (lockObj is null) @@ -141,13 +141,12 @@ public void ReleaseLock(string cacheName, string cacheInstanceId, string key, st catch (Exception exc) { if (logger?.IsEnabled(LogLevel.Warning) ?? false) - logger.Log(LogLevel.Warning, exc, "FUSION [N={CacheName} I={CacheInstanceId}] (O={CacheOperationId} K={CacheKey}): an error occurred while trying to release a SemaphoreSlim in the reactor", cacheName, cacheInstanceId, operationId, key); + logger.Log(LogLevel.Warning, exc, "FUSION [N={CacheName} I={CacheInstanceId}] (O={CacheOperationId} K={CacheKey}): an error occurred while trying to release a SemaphoreSlim in the memory locker", cacheName, cacheInstanceId, operationId, key); } } // IDISPOSABLE private bool disposedValue; - /*protected virtual*/ private void Dispose(bool disposing) { if (!disposedValue) @@ -165,6 +164,7 @@ private void Dispose(bool disposing) } } + /// public void Dispose() { Dispose(disposing: true); diff --git a/src/ZiggyCreatures.FusionCache/NullObjects/NullDistributedCache.cs b/src/ZiggyCreatures.FusionCache/NullObjects/NullDistributedCache.cs index b506c209..56bdf75d 100644 --- a/src/ZiggyCreatures.FusionCache/NullObjects/NullDistributedCache.cs +++ b/src/ZiggyCreatures.FusionCache/NullObjects/NullDistributedCache.cs @@ -11,15 +11,15 @@ public class NullDistributedCache : IDistributedCache { /// - public byte[] Get(string key) + public byte[]? Get(string key) { return null!; } /// - public Task GetAsync(string key, CancellationToken token = default) + public Task GetAsync(string key, CancellationToken token = default) { - return Task.FromResult(null!); + return Task.FromResult(null!); } /// diff --git a/src/ZiggyCreatures.FusionCache/NullObjects/NullFusionCache.cs b/src/ZiggyCreatures.FusionCache/NullObjects/NullFusionCache.cs index f9b3c7dd..29b5132c 100644 --- a/src/ZiggyCreatures.FusionCache/NullObjects/NullFusionCache.cs +++ b/src/ZiggyCreatures.FusionCache/NullObjects/NullFusionCache.cs @@ -33,6 +33,9 @@ public NullFusionCache(IOptions optionsAccessor) // OPTIONS _options = optionsAccessor.Value ?? throw new ArgumentNullException(nameof(optionsAccessor.Value)); + // DUPLICATE OPTIONS (TO AVOID EXTERNAL MODIFICATIONS) + _options = _options.Duplicate(); + // GLOBALLY UNIQUE INSTANCE ID InstanceId = _options.InstanceId ?? Guid.NewGuid().ToString("N"); @@ -87,12 +90,6 @@ public FusionCacheEntryOptions CreateEntryOptions(Action - public void Dispose() - { - // EMTPY - } - /// public void Expire(string key, FusionCacheEntryOptions? options = null, CancellationToken token = default) { @@ -206,4 +203,10 @@ public ValueTask> TryGetAsync(string key, FusionCache { return new ValueTask>(); } + + /// + public void Dispose() + { + // EMTPY + } } diff --git a/src/ZiggyCreatures.FusionCache/NullObjects/NullMemoryLocker.cs b/src/ZiggyCreatures.FusionCache/NullObjects/NullMemoryLocker.cs new file mode 100644 index 00000000..f6c1c583 --- /dev/null +++ b/src/ZiggyCreatures.FusionCache/NullObjects/NullMemoryLocker.cs @@ -0,0 +1,41 @@ +ο»Ώusing System; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.Extensions.Logging; +using ZiggyCreatures.Caching.Fusion.Locking; + +namespace ZiggyCreatures.Caching.Fusion.NullObjects +{ + /// + /// An implementation of that implements the null object pattern, meaning that it does nothing. Consider this a kind of a pass-through implementation. + /// + public class NullMemoryLocker + : IFusionCacheMemoryLocker + { + private bool disposedValue; + + /// + public object? AcquireLock(string cacheName, string cacheInstanceId, string key, string operationId, TimeSpan timeout, ILogger? logger, CancellationToken token) + { + return null; + } + + /// + public ValueTask AcquireLockAsync(string cacheName, string cacheInstanceId, string key, string operationId, TimeSpan timeout, ILogger? logger, CancellationToken token) + { + return new ValueTask(null); + } + + /// + public void ReleaseLock(string cacheName, string cacheInstanceId, string key, string operationId, object? lockObj, ILogger? logger) + { + throw new NotImplementedException(); + } + + /// + public void Dispose() + { + // EMTPY + } + } +} diff --git a/src/ZiggyCreatures.FusionCache/Reactors/FusionCacheReactorProbabilistic.cs b/src/ZiggyCreatures.FusionCache/Reactors/FusionCacheReactorProbabilistic.cs deleted file mode 100644 index aa86526a..00000000 --- a/src/ZiggyCreatures.FusionCache/Reactors/FusionCacheReactorProbabilistic.cs +++ /dev/null @@ -1,180 +0,0 @@ -ο»Ώusing System; -using System.Runtime.CompilerServices; -using System.Threading; -using System.Threading.Tasks; -using Microsoft.Extensions.Logging; - -namespace ZiggyCreatures.Caching.Fusion.Reactors; - -internal sealed class FusionCacheReactorProbabilistic - : IFusionCacheReactor -{ - private readonly int _lockPoolSize; - private SemaphoreSlim[] _lockPool; - private readonly string?[] _lockPoolKeys; - private int _lockPoolCollisions; - - public FusionCacheReactorProbabilistic(int reactorSize = 8_440) - { - _lockPoolSize = reactorSize; - - _lockPoolKeys = new string[_lockPoolSize]; - _lockPool = new SemaphoreSlim[_lockPoolSize]; - for (int i = 0; i < _lockPool.Length; i++) - { - _lockPool[i] = new SemaphoreSlim(1, 1); - } - } - - public int Collisions - { - get { return _lockPoolCollisions; } - } - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - private uint GetLockIndex(string key) - { - return unchecked((uint)key.GetHashCode()) % (uint)_lockPoolSize; - } - - // ACQUIRE LOCK ASYNC - public async ValueTask AcquireLockAsync(string cacheName, string cacheInstanceId, string key, string operationId, TimeSpan timeout, ILogger? logger, CancellationToken token) - { - token.ThrowIfCancellationRequested(); - - var idx = GetLockIndex(key); - var semaphore = _lockPool[idx]; - - if (logger?.IsEnabled(LogLevel.Trace) ?? false) - logger.Log(LogLevel.Trace, "FUSION [N={CacheName} I={CacheInstanceId}] (O={CacheOperationId} K={CacheKey}): trying to fast-acquire the LOCK", cacheName, cacheInstanceId, operationId, key); - - var acquired = semaphore.Wait(0); - if (acquired) - { - _lockPoolKeys[idx] = key; - if (logger?.IsEnabled(LogLevel.Trace) ?? false) - logger.Log(LogLevel.Trace, "FUSION [N={CacheName} I={CacheInstanceId}] (O={CacheOperationId} K={CacheKey}): LOCK fast-acquired", cacheName, cacheInstanceId, operationId, key); - - return semaphore; - } - - if (logger?.IsEnabled(LogLevel.Trace) ?? false) - logger.Log(LogLevel.Trace, "FUSION [N={CacheName} I={CacheInstanceId}] (O={CacheOperationId} K={CacheKey}): LOCK already taken", cacheName, cacheInstanceId, operationId, key); - - var key2 = _lockPoolKeys[idx]; - if (key2 != key) - { - if (logger?.IsEnabled(LogLevel.Trace) ?? false) - logger.Log(LogLevel.Trace, "FUSION [N={CacheName} I={CacheInstanceId}] (O={CacheOperationId} K={CacheKey}): LOCK " + (key2 is null ? "maybe " : string.Empty) + "acquired for a different key (current key: " + key + ", other key: " + key2 + ")", cacheName, cacheInstanceId, operationId, key); - - Interlocked.Increment(ref _lockPoolCollisions); - } - - if (logger?.IsEnabled(LogLevel.Trace) ?? false) - logger.Log(LogLevel.Trace, "FUSION [N={CacheName} I={CacheInstanceId}] (O={CacheOperationId} K={CacheKey}): waiting to acquire the LOCK", cacheName, cacheInstanceId, operationId, key); - - acquired = await semaphore.WaitAsync(timeout, token).ConfigureAwait(false); - - _lockPoolKeys[idx] = key; - - if (logger?.IsEnabled(LogLevel.Trace) ?? false) - logger.Log(LogLevel.Trace, "FUSION [N={CacheName} I={CacheInstanceId}] (O={CacheOperationId} K={CacheKey}): LOCK acquired", cacheName, cacheInstanceId, operationId, key); - - return acquired ? semaphore : null; - } - - // ACQUIRE LOCK - public object? AcquireLock(string cacheName, string cacheInstanceId, string key, string operationId, TimeSpan timeout, ILogger? logger) - { - var idx = GetLockIndex(key); - var semaphore = _lockPool[idx]; - - if (logger?.IsEnabled(LogLevel.Trace) ?? false) - logger.Log(LogLevel.Trace, "FUSION [N={CacheName} I={CacheInstanceId}] (O={CacheOperationId} K={CacheKey}): trying to fast-acquire the LOCK", cacheName, cacheInstanceId, operationId, key); - - var acquired = semaphore.Wait(0); - if (acquired) - { - _lockPoolKeys[idx] = key; - if (logger?.IsEnabled(LogLevel.Trace) ?? false) - logger.Log(LogLevel.Trace, "FUSION [N={CacheName} I={CacheInstanceId}] (O={CacheOperationId} K={CacheKey}): LOCK fast-acquired", cacheName, cacheInstanceId, operationId, key); - - return semaphore; - } - - if (logger?.IsEnabled(LogLevel.Trace) ?? false) - logger.Log(LogLevel.Trace, "FUSION [N={CacheName} I={CacheInstanceId}] (O={CacheOperationId} K={CacheKey}): LOCK already taken", cacheName, cacheInstanceId, operationId, key); - - var key2 = _lockPoolKeys[idx]; - if (key2 != key) - { - if (logger?.IsEnabled(LogLevel.Trace) ?? false) - logger.Log(LogLevel.Trace, "FUSION [N={CacheName} I={CacheInstanceId}] (O={CacheOperationId} K={CacheKey}): LOCK " + (key2 is null ? "maybe " : string.Empty) + "acquired for a different key (current key: " + key + ", other key: " + key2 + ")", cacheName, cacheInstanceId, operationId, key); - - Interlocked.Increment(ref _lockPoolCollisions); - } - - if (logger?.IsEnabled(LogLevel.Trace) ?? false) - logger.Log(LogLevel.Trace, "FUSION [N={CacheName} I={CacheInstanceId}] (O={CacheOperationId} K={CacheKey}): waiting to acquire the LOCK", cacheName, cacheInstanceId, operationId, key); - - acquired = semaphore.Wait(timeout); - - _lockPoolKeys[idx] = key; - - if (logger?.IsEnabled(LogLevel.Trace) ?? false) - logger.Log(LogLevel.Trace, "FUSION [N={CacheName} I={CacheInstanceId}] (O={CacheOperationId} K={CacheKey}): LOCK acquired", cacheName, cacheInstanceId, operationId, key); - - return acquired ? semaphore : null; - } - - // RELEASE LOCK ASYNC - public void ReleaseLock(string cacheName, string cacheInstanceId, string key, string operationId, object? lockObj, ILogger? logger) - { - if (lockObj is null) - return; - - var idx = GetLockIndex(key); - _lockPoolKeys[idx] = null; - - try - { - ((SemaphoreSlim)lockObj).Release(); - } - catch (Exception exc) - { - if (logger?.IsEnabled(LogLevel.Warning) ?? false) - logger.Log(LogLevel.Warning, exc, "FUSION [N={CacheName} I={CacheInstanceId}] (O={CacheOperationId} K={CacheKey}): an error occurred while trying to release a SemaphoreSlim in the reactor", cacheName, cacheInstanceId, operationId, key); - } - } - - // IDISPOSABLE - private bool disposedValue; - /*protected virtual*/ - private void Dispose(bool disposing) - { - if (!disposedValue) - { - if (disposing) - { - if (_lockPool is not null) - { - foreach (var semaphore in _lockPool) - { - semaphore.Dispose(); - } - } - } - -#pragma warning disable CS8625 // Cannot convert null literal to non-nullable reference type. - _lockPool = null; -#pragma warning restore CS8625 // Cannot convert null literal to non-nullable reference type. - disposedValue = true; - } - } - - public void Dispose() - { - Dispose(disposing: true); - System.GC.SuppressFinalize(this); - } -} diff --git a/src/ZiggyCreatures.FusionCache/Reactors/FusionCacheReactorUnbounded.cs b/src/ZiggyCreatures.FusionCache/Reactors/FusionCacheReactorUnbounded.cs deleted file mode 100644 index 927b6528..00000000 --- a/src/ZiggyCreatures.FusionCache/Reactors/FusionCacheReactorUnbounded.cs +++ /dev/null @@ -1,130 +0,0 @@ -ο»Ώusing System; -using System.Collections.Generic; -using System.Threading; -using System.Threading.Tasks; -using Microsoft.Extensions.Logging; - -namespace ZiggyCreatures.Caching.Fusion.Reactors; - -internal sealed class FusionCacheReactorUnbounded - : IFusionCacheReactor -{ - private Dictionary _lockCache; - - public FusionCacheReactorUnbounded(int reactorSize = 100) - { - _lockCache = new Dictionary(reactorSize); - } - - public int Collisions - { - get { return 0; } - } - - private SemaphoreSlim GetSemaphore(string key, string operationId, ILogger? logger) - { - SemaphoreSlim _semaphore; - - if (_lockCache.TryGetValue(key, out _semaphore)) - return _semaphore; - - lock (_lockCache) - { - if (_lockCache.TryGetValue(key, out _semaphore)) - return _semaphore; - - _semaphore = new SemaphoreSlim(1, 1); - - _lockCache[key] = _semaphore; - - return _semaphore; - } - } - - // ACQUIRE LOCK ASYNC - public async ValueTask AcquireLockAsync(string cacheName, string cacheInstanceId, 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.Log(LogLevel.Trace, "FUSION [N={CacheName} I={CacheInstanceId}] (O={CacheOperationId} K={CacheKey}): waiting to acquire the LOCK", cacheName, cacheInstanceId, operationId, key); - - var acquired = await semaphore.WaitAsync(timeout, token).ConfigureAwait(false); - - if (logger?.IsEnabled(LogLevel.Trace) ?? false) - logger.Log(LogLevel.Trace, "FUSION [N={CacheName} I={CacheInstanceId}] (O={CacheOperationId} K={CacheKey}): LOCK acquired", cacheName, cacheInstanceId, operationId, key); - - return acquired ? semaphore : null; - } - - // ACQUIRE LOCK - public object? AcquireLock(string cacheName, string cacheInstanceId, string key, string operationId, TimeSpan timeout, ILogger? logger) - { - var semaphore = GetSemaphore(key, operationId, logger); - - if (logger?.IsEnabled(LogLevel.Trace) ?? false) - logger.Log(LogLevel.Trace, "FUSION [N={CacheName} I={CacheInstanceId}] (O={CacheOperationId} K={CacheKey}): waiting to acquire the LOCK", cacheName, cacheInstanceId, operationId, key); - - var acquired = semaphore.Wait(timeout); - - if (logger?.IsEnabled(LogLevel.Trace) ?? false) - logger.Log(LogLevel.Trace, "FUSION [N={CacheName} I={CacheInstanceId}] (O={CacheOperationId} K={CacheKey}): LOCK acquired", cacheName, cacheInstanceId, operationId, key); - - return acquired ? semaphore : null; - } - - // RELEASE LOCK ASYNC - public void ReleaseLock(string cacheName, string cacheInstanceId, string key, string operationId, object? lockObj, ILogger? logger) - { - if (lockObj is null) - return; - - try - { - ((SemaphoreSlim)lockObj).Release(); - } - catch (Exception exc) - { - if (logger?.IsEnabled(LogLevel.Warning) ?? false) - logger.Log(LogLevel.Warning, exc, "FUSION [N={CacheName} I={CacheInstanceId}] (O={CacheOperationId} K={CacheKey}): an error occurred while trying to release a SemaphoreSlim in the reactor", cacheName, cacheInstanceId, operationId, key); - } - } - - // IDISPOSABLE - private bool disposedValue; - /*protected virtual*/ - private void Dispose(bool disposing) - { - if (!disposedValue) - { - if (disposing) - { - foreach (var semaphore in _lockCache.Values) - { - try - { - semaphore.Dispose(); - } - catch - { - // EMPTY - } - } - _lockCache.Clear(); - } - -#pragma warning disable CS8625 // Cannot convert null literal to non-nullable reference type. - _lockCache = null; -#pragma warning restore CS8625 // Cannot convert null literal to non-nullable reference type. - disposedValue = true; - } - } - - public void Dispose() - { - Dispose(disposing: true); - GC.SuppressFinalize(this); - } -} diff --git a/src/ZiggyCreatures.FusionCache/Reactors/FusionCacheReactorUnboundedConcurrent.cs b/src/ZiggyCreatures.FusionCache/Reactors/FusionCacheReactorUnboundedConcurrent.cs deleted file mode 100644 index ade1e7b6..00000000 --- a/src/ZiggyCreatures.FusionCache/Reactors/FusionCacheReactorUnboundedConcurrent.cs +++ /dev/null @@ -1,115 +0,0 @@ -ο»Ώusing System; -using System.Collections.Concurrent; -using System.Threading; -using System.Threading.Tasks; -using Microsoft.Extensions.Logging; - -namespace ZiggyCreatures.Caching.Fusion.Reactors; - -internal sealed class FusionCacheReactorUnboundedConcurrent - : IFusionCacheReactor -{ - private ConcurrentDictionary _lockCache; - - public FusionCacheReactorUnboundedConcurrent(int reactorSize = 100) - { - _lockCache = new ConcurrentDictionary(Environment.ProcessorCount, reactorSize); - } - - public int Collisions - { - get { return 0; } - } - - private SemaphoreSlim GetSemaphore(string key, string operationId, ILogger? logger) - { - return _lockCache.GetOrAdd(key, _ => new SemaphoreSlim(1, 1)); - } - - // ACQUIRE LOCK ASYNC - public async ValueTask AcquireLockAsync(string cacheName, string cacheInstanceId, 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.Log(LogLevel.Trace, "FUSION [N={CacheName} I={CacheInstanceId}] (O={CacheOperationId} K={CacheKey}): waiting to acquire the LOCK", cacheName, cacheInstanceId, operationId, key); - - var acquired = await semaphore.WaitAsync(timeout, token).ConfigureAwait(false); - - if (logger?.IsEnabled(LogLevel.Trace) ?? false) - logger.Log(LogLevel.Trace, "FUSION [N={CacheName} I={CacheInstanceId}] (O={CacheOperationId} K={CacheKey}): LOCK acquired", cacheName, cacheInstanceId, operationId, key); - - return acquired ? semaphore : null; - } - - // ACQUIRE LOCK - public object? AcquireLock(string cacheName, string cacheInstanceId, string key, string operationId, TimeSpan timeout, ILogger? logger) - { - var semaphore = GetSemaphore(key, operationId, logger); - - if (logger?.IsEnabled(LogLevel.Trace) ?? false) - logger.Log(LogLevel.Trace, "FUSION [N={CacheName} I={CacheInstanceId}] (O={CacheOperationId} K={CacheKey}): waiting to acquire the LOCK", cacheName, cacheInstanceId, operationId, key); - - var acquired = semaphore.Wait(timeout); - - if (logger?.IsEnabled(LogLevel.Trace) ?? false) - logger.Log(LogLevel.Trace, "FUSION [N={CacheName} I={CacheInstanceId}] (O={CacheOperationId} K={CacheKey}): LOCK acquired", cacheName, cacheInstanceId, operationId, key); - - return acquired ? semaphore : null; - } - - // RELEASE LOCK ASYNC - public void ReleaseLock(string cacheName, string cacheInstanceId, string key, string operationId, object? lockObj, ILogger? logger) - { - if (lockObj is null) - return; - - try - { - ((SemaphoreSlim)lockObj).Release(); - } - catch (Exception exc) - { - if (logger?.IsEnabled(LogLevel.Warning) ?? false) - logger.Log(LogLevel.Warning, exc, "FUSION [N={CacheName} I={CacheInstanceId}] (O={CacheOperationId} K={CacheKey}): an error occurred while trying to release a SemaphoreSlim in the reactor", cacheName, cacheInstanceId, operationId, key); - } - } - - // IDISPOSABLE - private bool disposedValue; - /*protected virtual*/ - private void Dispose(bool disposing) - { - if (!disposedValue) - { - if (disposing) - { - foreach (var semaphore in _lockCache.Values) - { - try - { - semaphore.Dispose(); - } - catch - { - // EMPTY - } - } - _lockCache.Clear(); - } - -#pragma warning disable CS8625 // Cannot convert null literal to non-nullable reference type. - _lockCache = null; -#pragma warning restore CS8625 // Cannot convert null literal to non-nullable reference type. - disposedValue = true; - } - } - - public void Dispose() - { - Dispose(disposing: true); - GC.SuppressFinalize(this); - } -} diff --git a/src/ZiggyCreatures.FusionCache/Reactors/FusionCacheReactorUnboundedConcurrentLazy.cs b/src/ZiggyCreatures.FusionCache/Reactors/FusionCacheReactorUnboundedConcurrentLazy.cs deleted file mode 100644 index dd1f970a..00000000 --- a/src/ZiggyCreatures.FusionCache/Reactors/FusionCacheReactorUnboundedConcurrentLazy.cs +++ /dev/null @@ -1,120 +0,0 @@ -ο»Ώusing System; -using System.Collections.Concurrent; -using System.Threading; -using System.Threading.Tasks; -using Microsoft.Extensions.Logging; - -namespace ZiggyCreatures.Caching.Fusion.Reactors; - -internal sealed class FusionCacheReactorUnboundedConcurrentLazy - : IFusionCacheReactor -{ - private ConcurrentDictionary> _lockCache; - - public FusionCacheReactorUnboundedConcurrentLazy(int reactorSize = 100) - { - _lockCache = new ConcurrentDictionary>(Environment.ProcessorCount, reactorSize); - } - - public int Collisions - { - get { return 0; } - } - - private SemaphoreSlim GetSemaphore(string key, string operationId, ILogger? logger) - { - return _lockCache.GetOrAdd( - key, - _ => new Lazy( - () => new SemaphoreSlim(1, 1) - ) - ).Value; - } - - // ACQUIRE LOCK ASYNC - public async ValueTask AcquireLockAsync(string cacheName, string cacheInstanceId, 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.Log(LogLevel.Trace, "FUSION [N={CacheName} I={CacheInstanceId}] (O={CacheOperationId} K={CacheKey}): waiting to acquire the LOCK", cacheName, cacheInstanceId, operationId, key); - - var acquired = await semaphore.WaitAsync(timeout, token).ConfigureAwait(false); - - if (logger?.IsEnabled(LogLevel.Trace) ?? false) - logger.Log(LogLevel.Trace, "FUSION [N={CacheName} I={CacheInstanceId}] (O={CacheOperationId} K={CacheKey}): LOCK acquired", cacheName, cacheInstanceId, operationId, key); - - return acquired ? semaphore : null; - } - - // ACQUIRE LOCK - public object? AcquireLock(string cacheName, string cacheInstanceId, string key, string operationId, TimeSpan timeout, ILogger? logger) - { - var semaphore = GetSemaphore(key, operationId, logger); - - if (logger?.IsEnabled(LogLevel.Trace) ?? false) - logger.Log(LogLevel.Trace, "FUSION [N={CacheName} I={CacheInstanceId}] (O={CacheOperationId} K={CacheKey}): waiting to acquire the LOCK", cacheName, cacheInstanceId, operationId, key); - - var acquired = semaphore.Wait(timeout); - - if (logger?.IsEnabled(LogLevel.Trace) ?? false) - logger.Log(LogLevel.Trace, "FUSION [N={CacheName} I={CacheInstanceId}] (O={CacheOperationId} K={CacheKey}): LOCK acquired", cacheName, cacheInstanceId, operationId, key); - - return acquired ? semaphore : null; - } - - // RELEASE LOCK ASYNC - public void ReleaseLock(string cacheName, string cacheInstanceId, string key, string operationId, object? lockObj, ILogger? logger) - { - if (lockObj is null) - return; - - try - { - ((SemaphoreSlim)lockObj).Release(); - } - catch (Exception exc) - { - if (logger?.IsEnabled(LogLevel.Warning) ?? false) - logger.Log(LogLevel.Warning, exc, "FUSION [N={CacheName} I={CacheInstanceId}] (O={CacheOperationId} K={CacheKey}): an error occurred while trying to release a SemaphoreSlim in the reactor", cacheName, cacheInstanceId, operationId, key); - } - } - - // IDISPOSABLE - private bool disposedValue; - /*protected virtual*/ - private void Dispose(bool disposing) - { - if (!disposedValue) - { - if (disposing) - { - foreach (var lazy in _lockCache.Values) - { - try - { - lazy.Value.Dispose(); - } - catch - { - // EMPTY - } - } - _lockCache.Clear(); - } - -#pragma warning disable CS8625 // Cannot convert null literal to non-nullable reference type. - _lockCache = null; -#pragma warning restore CS8625 // Cannot convert null literal to non-nullable reference type. - disposedValue = true; - } - } - - public void Dispose() - { - Dispose(disposing: true); - GC.SuppressFinalize(this); - } -} diff --git a/src/ZiggyCreatures.FusionCache/Reactors/FusionCacheReactorUnboundedWithPool.cs b/src/ZiggyCreatures.FusionCache/Reactors/FusionCacheReactorUnboundedWithPool.cs deleted file mode 100644 index 55618d0b..00000000 --- a/src/ZiggyCreatures.FusionCache/Reactors/FusionCacheReactorUnboundedWithPool.cs +++ /dev/null @@ -1,156 +0,0 @@ -ο»Ώusing System; -using System.Collections.Generic; -using System.Runtime.CompilerServices; -using System.Threading; -using System.Threading.Tasks; -using Microsoft.Extensions.Logging; - -namespace ZiggyCreatures.Caching.Fusion.Reactors; - -internal sealed class FusionCacheReactorUnboundedWithPool - : IFusionCacheReactor -{ - private Dictionary _lockCache; - - private readonly int _lockPoolSize; - private object[] _lockPool; - - public FusionCacheReactorUnboundedWithPool(int reactorSize = 100) - { - _lockCache = new Dictionary(reactorSize); - - // LOCKING - _lockPoolSize = reactorSize; - _lockPool = new object[_lockPoolSize]; - for (int i = 0; i < _lockPool.Length; i++) - { - _lockPool[i] = new object(); - } - } - - public int Collisions - { - get { return 0; } - } - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - private uint GetLockIndex(string key) - { - return unchecked((uint)key.GetHashCode()) % (uint)_lockPoolSize; - } - - private SemaphoreSlim GetSemaphore(string key, string operationId, ILogger? logger) - { - SemaphoreSlim _semaphore; - - if (_lockCache.TryGetValue(key, out _semaphore)) - return _semaphore; - - lock (_lockPool[GetLockIndex(key)]) - { - if (_lockCache.TryGetValue(key, out _semaphore)) - return _semaphore; - - _semaphore = new SemaphoreSlim(1, 1); - - _lockCache[key] = _semaphore; - - return _semaphore; - } - } - - // ACQUIRE LOCK ASYNC - public async ValueTask AcquireLockAsync(string cacheName, string cacheInstanceId, 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.Log(LogLevel.Trace, "FUSION [N={CacheName} I={CacheInstanceId}] (O={CacheOperationId} K={CacheKey}): waiting to acquire the LOCK", cacheName, cacheInstanceId, operationId, key); - - var acquired = await semaphore.WaitAsync(timeout, token).ConfigureAwait(false); - - if (logger?.IsEnabled(LogLevel.Trace) ?? false) - logger.Log(LogLevel.Trace, "FUSION [N={CacheName} I={CacheInstanceId}] (O={CacheOperationId} K={CacheKey}): LOCK acquired", cacheName, cacheInstanceId, operationId, key); - - return acquired ? semaphore : null; - } - - // ACQUIRE LOCK - public object? AcquireLock(string cacheName, string cacheInstanceId, string key, string operationId, TimeSpan timeout, ILogger? logger) - { - var semaphore = GetSemaphore(key, operationId, logger); - - if (logger?.IsEnabled(LogLevel.Trace) ?? false) - logger.Log(LogLevel.Trace, "FUSION [N={CacheName} I={CacheInstanceId}] (O={CacheOperationId} K={CacheKey}): waiting to acquire the LOCK", cacheName, cacheInstanceId, operationId, key); - - var acquired = semaphore.Wait(timeout); - - if (logger?.IsEnabled(LogLevel.Trace) ?? false) - logger.Log(LogLevel.Trace, "FUSION [N={CacheName} I={CacheInstanceId}] (O={CacheOperationId} K={CacheKey}): LOCK acquired", cacheName, cacheInstanceId, operationId, key); - - return acquired ? semaphore : null; - } - - // RELEASE LOCK ASYNC - public void ReleaseLock(string cacheName, string cacheInstanceId, string key, string operationId, object? lockObj, ILogger? logger) - { - if (lockObj is null) - return; - - try - { - ((SemaphoreSlim)lockObj).Release(); - } - catch (Exception exc) - { - if (logger?.IsEnabled(LogLevel.Warning) ?? false) - logger.Log(LogLevel.Warning, exc, "FUSION [N={CacheName} I={CacheInstanceId}] (O={CacheOperationId} K={CacheKey}): an error occurred while trying to release a SemaphoreSlim in the reactor", cacheName, cacheInstanceId, operationId, key); - } - } - - // IDISPOSABLE - private bool disposedValue; - /*protected virtual*/ - private void Dispose(bool disposing) - { - if (!disposedValue) - { - if (disposing) - { - foreach (var semaphore in _lockCache.Values) - { - try - { - semaphore.Dispose(); - } - catch - { - // EMPTY - } - } - try - { - _lockCache.Clear(); - } - catch - { - // EMPTY - } - } - -#pragma warning disable CS8625 // Cannot convert null literal to non-nullable reference type. - _lockCache = null; - _lockPool = null; -#pragma warning restore CS8625 // Cannot convert null literal to non-nullable reference type. - disposedValue = true; - } - } - - public void Dispose() - { - Dispose(disposing: true); - GC.SuppressFinalize(this); - } -} diff --git a/src/ZiggyCreatures.FusionCache/Reactors/IFusionCacheReactor.cs b/src/ZiggyCreatures.FusionCache/Reactors/IFusionCacheReactor.cs index c6bc86ee..d88eab06 100644 --- a/src/ZiggyCreatures.FusionCache/Reactors/IFusionCacheReactor.cs +++ b/src/ZiggyCreatures.FusionCache/Reactors/IFusionCacheReactor.cs @@ -1,4 +1,5 @@ ο»Ώusing System; +using System.ComponentModel; using System.Threading; using System.Threading.Tasks; using Microsoft.Extensions.Logging; @@ -8,6 +9,8 @@ namespace ZiggyCreatures.Caching.Fusion.Reactors; /// /// Represents one of the core pieces of an instance of an , dealing with acquiring and releasing locks in a highly optimized way. /// +[EditorBrowsable(EditorBrowsableState.Never)] +[Obsolete("This interface is obsolete and will be removed in the next major version of the library: please use IFusionCacheMemoryLocker instead.")] public interface IFusionCacheReactor : IDisposable { diff --git a/src/ZiggyCreatures.FusionCache/ZiggyCreatures.FusionCache.csproj b/src/ZiggyCreatures.FusionCache/ZiggyCreatures.FusionCache.csproj index da47ab78..ccbd8168 100644 --- a/src/ZiggyCreatures.FusionCache/ZiggyCreatures.FusionCache.csproj +++ b/src/ZiggyCreatures.FusionCache/ZiggyCreatures.FusionCache.csproj @@ -4,29 +4,21 @@ netstandard2.0 latest enable - 0.24.0 + 0.25.0 ZiggyCreatures.FusionCache logo-128x128.png - - FusionCache is an easy to use, fast and robust cache with advanced resiliency features and an optional distributed 2nd layer. - + FusionCache is an easy to use, fast and robust cache with advanced resiliency features and an optional distributed 2nd level. caching;cache;multi-level;multilevel;fusion;fusioncache;fusion-cache;performance;async;ziggy ZiggyCreatures.Caching.Fusion ZiggyCreatures.FusionCache.xml README.md - - Added: all new Auto-Recovery (ex: Backplane Auto-Recovery) supporting both distributed cache and backplane, more robust, with continuous background processing, auto-cleanup and more - - Added: Simulator app - - Added: custom exceptions - - Added: eviction event now pass the cache value - - Added: ReThrowBackplaneExceptions entry option - - Changed: better distributed workflow - - Changed: better handling of backplane messages - - Changed: wire format version (distributed cache + backplane) - - Changed: better logging (InstanceId everywhere) - - Changed: better timestamps (more precise) - - Changed: NullFusionCache now correctly handles CreateEntryOptions - - Perf: various performance optimizations + - Added: full observability with OpenTelemetry, including traces and metrics (logging was already there) + - Added: memory locker abstraction via IFusionCacheMemoryLocker, to allow more extensibility + - Changed: better items cleanup in auto-recovery + - Update: package dependencies + - Fixed: SkipDistributedCacheReadWhenStale was not working as expected in get-only operations (TryGet, GetOrDefault) + - Changed: better xml docs true @@ -38,7 +30,8 @@ - + + diff --git a/src/ZiggyCreatures.FusionCache/ZiggyCreatures.FusionCache.xml b/src/ZiggyCreatures.FusionCache/ZiggyCreatures.FusionCache.xml index 1401490b..0e84e0f1 100644 --- a/src/ZiggyCreatures.FusionCache/ZiggyCreatures.FusionCache.xml +++ b/src/ZiggyCreatures.FusionCache/ZiggyCreatures.FusionCache.xml @@ -314,7 +314,7 @@ - A class with base events that are common to any cache layer (general, memroy or distributed) + A class with base events that are common to any cache level (general, memroy or distributed) @@ -347,7 +347,7 @@ - The events hub for events specific for the distributed layer. + The events hub for events specific for the distributed level. @@ -444,12 +444,12 @@ - The events hub for the memory layer. + The events hub for the memory level. - The events hub for the distributed layer. + The events hub for the distributed level. @@ -499,7 +499,7 @@ - The events hub for events specific for the memory layer. + The events hub for events specific for the memory level. @@ -540,6 +540,15 @@ The instance to use. If null, logging will be completely disabled. The instance to use (advanced). If null, a standard one will be automatically created and managed. + + + Creates a new instance. + + The set of cache-wide options to use with this instance of . + The instance to use. If null, one will be automatically created and managed. + The instance to use. If null, logging will be completely disabled. + The instance to use. If , a standard one will be automatically created and managed. + @@ -823,7 +832,54 @@ DOCS:
The to act upon. - The factory used to create the serializer, with access to the . + The factory used to create the memory cache, with access to the . + The so that additional calls can be chained. + + + + The standard implementation of an will be used (this is the default behaviour). +

+ DOCS: +
+ The to act upon. + The so that additional calls can be chained. +
+ + + The builder will look for an service registered in the DI container and use it, and throws if it cannot find one. +

+ DOCS: +
+ The to act upon. + The so that additional calls can be chained. +
+ + + Indicates if the builder should try to find and use an service registered in the DI container. +

+ DOCS: +
+ The to act upon. + The so that additional calls can be chained. +
+ + + Specify a custom instance to be used. +

+ DOCS: +
+ The to act upon. + The instance to use. + The so that additional calls can be chained. +
+ + + Specify a custom factory to be used. +

+ DOCS: +
+ The to act upon. + The factory used to create the memory locker, with access to the . The so that additional calls can be chained.
@@ -1062,6 +1118,56 @@ Indicates if the distributed cache found in the DI container should be ignored if it is of type , since that is not really a distributed cache and it's automatically registered by ASP.NET MVC without control from the user The so that additional calls can be chained. + + + A support class for FusionCache diagnostics. + + + + + The current version of FusionCache. + + + + + The activity source name for FusionCache. + + + + + The activity source name for the FusionCache memory level. + + + + + The activity source name for the FusionCache distributed level. + + + + + The activity source name for the FusionCache backplane. + + + + + The meter name for FusionCache. + + + + + The meter name for the FusionCache memory level. + + + + + The meter name for the FusionCache distributed level. + + + + + The meter name for the FusionCache backplane. + + The generic exception that is thrown when a distributed cache error occurs: the InnerException contains the original exception. @@ -1125,7 +1231,7 @@ - The timeout to apply when trying to acquire a lock during a factory execution. + The timeout to apply when trying to acquire a memory lock during a factory execution.

DOCS:
@@ -1234,7 +1340,7 @@
- Even if the distributed cache is a secondary layer, by default every operation on it (get/set/remove/etc) is blocking: that is to say the FusionCache method call would not return until the inner distributed cache operation is completed. + Even if the distributed cache is a secondary level, by default every operation on it (get/set/remove/etc) is blocking: that is to say the FusionCache method call would not return until the inner distributed cache operation is completed.
This is to avoid rare edge cases like saving a value in the cache and immediately cheking the underlying distributed cache directly, not finding the value (because it is still being saved): very very rare, but still.
@@ -1307,9 +1413,9 @@ - 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. + When a 2nd level (distributed cache) is used and a cache entry in the 1st level (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.

- There are situations though, like in a mobile app with a SQLite 2nd layer, where the 2nd layer is not really "distributed" but just "out of process" (to ease cold starts): in situations like this noone can have updated the 2nd layer, so we can skip that extra read for a perf boost (of course the write part will still be done). + There are situations though, like in a mobile app with a SQLite 2nd level, where the 2nd level is not really "distributed" but just "out of process" (to ease cold starts): in situations like this noone can have updated the 2nd level, so we can skip that extra read for a perf boost (of course the write part will still be done).

TL/DR: if your 2nd level is not "distributed" but only "out of process", setting this to can give you a nice performance boost.

@@ -1320,7 +1426,7 @@ 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. + 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 level (distributed cache) and a lot of extra network traffic.

DOCS:
@@ -1504,7 +1610,7 @@ 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. + 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 level (distributed cache) and a lot of extra network traffic.

DOCS:
@@ -2415,7 +2521,7 @@ - If enabled, and re-throwing of exceptions is also enabled, it will re-throw the original exception as-is instead of wrapping it into one of the available specific exceptions (, or ). + If enabled, and re-throwing of exceptions is also enabled (see , or ), it will re-throw the original exception as-is instead of wrapping it into one of the available specific exceptions (, or ). @@ -2727,7 +2833,7 @@ - Sets a secondary caching layer, by providing an instance and an instance to be used to convert from generic values to byte[] and viceversa. + Sets a secondary caching level, by providing an instance and an instance to be used to convert from generic values to byte[] and viceversa. The instance to use. The instance to use. @@ -2735,7 +2841,7 @@ - Removes the secondary caching layer. + Removes the secondary caching level. The same instance, usable in a fluent api way. @@ -2869,6 +2975,26 @@ Throws an if a memory cache (an instance of ) is not specified or is not found in the DI container.
+ + + Indicates if the builder should try find and use an service registered in the DI container. + + + + + A specific instance to be used. + + + + + A factory that creates the instance to be used. + + + + + Throws an if a memory locker (an instance of ) is not specified or is not found in the DI container. + + Indicates if the builder should try find and use an service registered in the DI container. @@ -3033,7 +3159,7 @@ - An entry in a distributed layer. + An entry in a distributed level. The type of the entry's value @@ -3182,7 +3308,7 @@ - An entry in a memory layer. + An entry in a memory level. @@ -3366,6 +3492,96 @@ Indicates if the circuit has been closed with this operation. if the circuit is closed, either because it was already closed or because it has been closed with this operation. otherwise. + + + A FusionCache component to handle acquiring and releasing memory 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 CacheName of the FusionCache instance. + The InstanceId 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 generic lock 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 InstanceId 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 acquired genericlock object, later released when the critical section is over. + The to use, if any. + An optional to cancel the operation. + + + + Release the generic lock object. + + The name of the FusionCache instance. + The InstanceId 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. + + + + An implementation of based on a probabilistic approach. +

+ ⚠️ WARNING: this type of locker may lead to deadlocks, so be careful. +
+
+ + + Initializes a new instance of the class. + + The size of the pool used internally. + + + + + + + + + + + + + + + + A standard implementation of . + + + + + Initializes a new instance of the class. + + The size of the pool used internally for the 1st level locking strategy. + + + + + + + + + + + + + Represents maybe a value, maybe not. @@ -3518,9 +3734,6 @@ - - - @@ -3578,6 +3791,26 @@ + + + + + + An implementation of that implements the null object pattern, meaning that it does nothing. Consider this a kind of a pass-through implementation. + + + + + + + + + + + + + + An implementation of that implements the null object pattern, meaning that it does nothing. diff --git a/src/ZiggyCreatures.FusionCache/docs/README.md b/src/ZiggyCreatures.FusionCache/docs/README.md index 33ba22a4..2c41b442 100644 --- a/src/ZiggyCreatures.FusionCache/docs/README.md +++ b/src/ZiggyCreatures.FusionCache/docs/README.md @@ -5,7 +5,7 @@ | πŸ™‹β€β™‚οΈ Updating from before `v0.24.0` ? please [read here](https://github.com/ZiggyCreatures/FusionCache/blob/main/docs/Update_v0_24_0.md). | |:-------| -## FusionCache is an easy to use, fast and robust cache with advanced resiliency features and an optional distributed 2nd layer. +## FusionCache is an easy to use, fast and robust cache with advanced resiliency features and an optional distributed 2nd level. It was born after years of dealing with all sorts of different types of caches: memory caching, distributed caching, http caching, CDNs, browser cache, offline cache, you name it. So I've tried to put together these experiences and came up with FusionCache. @@ -49,10 +49,11 @@ These are the **key features** of FusionCache: - [**πŸ¦… Eager Refresh**](https://github.com/ZiggyCreatures/FusionCache/blob/main/docs/EagerRefresh.md): start a non-blocking background refresh before the expiration occurs - [**πŸ”ƒ Dependency Injection**](https://github.com/ZiggyCreatures/FusionCache/blob/main/docs/DependencyInjection.md): native support for Dependency Injection, with a nice fluent interface including a Builder support - [**πŸ“› Named Caches**](https://github.com/ZiggyCreatures/FusionCache/blob/main/docs/NamedCaches.md): easily work with multiple named caches, even if differently configured +- [**πŸ”­ OpenTelemetry**](https://github.com/ZiggyCreatures/FusionCache/blob/main/docs/OpenTelemetry.md): native observability support via OpenTelemetry +- [**πŸ“œ Logging**](https://github.com/ZiggyCreatures/FusionCache/blob/main/docs/Logging.md): comprehensive, structured and customizable, via the standard `ILogger` interface - [**πŸ’« Natively sync/async**](https://github.com/ZiggyCreatures/FusionCache/blob/main/docs/CoreMethods.md): native support for both the synchronous and asynchronous programming model - [**πŸ“ž Events**](https://github.com/ZiggyCreatures/FusionCache/blob/main/docs/Events.md): a comprehensive set of events, both at a high level and at lower levels (memory/distributed) - [**🧩 Plugins**](https://github.com/ZiggyCreatures/FusionCache/blob/main/docs/Plugins.md): extend FusionCache with additional behavior like adding support for metrics, statistics, etc... -- [**πŸ“œ Logging**](https://github.com/ZiggyCreatures/FusionCache/blob/main/docs/Logging.md): comprehensive, structured and customizable, via the standard `ILogger` interface ## ⭐ Quick Start @@ -139,6 +140,6 @@ Yes! Even though the current version is `0.X` for an excess of caution, FusionCache is already used **in production** on multiple **real world projects** happily handling millions of requests per day, or at least these are the projects I'm aware of. -Considering that the FusionCache packages have been downloaded more than **2 million times** (thanks everybody!) it may very well be used even more. +Considering that the FusionCache packages have been downloaded more than **3 million times** (thanks everybody!) it may very well be used even more. And again, if you are using it please [**βœ‰ drop me a line**](https://twitter.com/jodydonetti), I'd like to know! diff --git a/tests/ZiggyCreatures.FusionCache.Playground/GlobalSuppressions.cs b/tests/ZiggyCreatures.FusionCache.Playground/GlobalSuppressions.cs index e08c38ea..719ad651 100644 --- a/tests/ZiggyCreatures.FusionCache.Playground/GlobalSuppressions.cs +++ b/tests/ZiggyCreatures.FusionCache.Playground/GlobalSuppressions.cs @@ -7,3 +7,5 @@ [assembly: SuppressMessage("AsyncUsage", "AsyncFixer01:Unnecessary async/await usage", Justification = "")] [assembly: SuppressMessage("Simplification", "RCS1049:Simplify boolean comparison.", Justification = "")] +[assembly: SuppressMessage("Style", "IDE0290:Use primary constructor", Justification = "")] +[assembly: SuppressMessage("Style", "IDE0090:Use 'new(...)'", Justification = "")] diff --git a/tests/ZiggyCreatures.FusionCache.Playground/Program.cs b/tests/ZiggyCreatures.FusionCache.Playground/Program.cs index ee25fb10..6167bdc1 100644 --- a/tests/ZiggyCreatures.FusionCache.Playground/Program.cs +++ b/tests/ZiggyCreatures.FusionCache.Playground/Program.cs @@ -7,7 +7,9 @@ class Program { static async Task Main(string[] args) { - await LoggingScenario.RunAsync().ConfigureAwait(false); + //await ScratchpadScenario.RunAsync().ConfigureAwait(false); + //await LoggingScenario.RunAsync().ConfigureAwait(false); + await OpenTelemetryScenario.RunAsync().ConfigureAwait(false); } } } diff --git a/tests/ZiggyCreatures.FusionCache.Playground/Scenarios/OpenTelemetryScenario.cs b/tests/ZiggyCreatures.FusionCache.Playground/Scenarios/OpenTelemetryScenario.cs new file mode 100644 index 00000000..46434547 --- /dev/null +++ b/tests/ZiggyCreatures.FusionCache.Playground/Scenarios/OpenTelemetryScenario.cs @@ -0,0 +1,357 @@ +ο»Ώusing System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Diagnostics.Tracing; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using Microsoft.Extensions.Caching.Distributed; +using Microsoft.Extensions.Caching.Memory; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; +using OpenTelemetry; +using OpenTelemetry.Metrics; +using OpenTelemetry.Resources; +using OpenTelemetry.Trace; +using ZiggyCreatures.Caching.Fusion.Backplane.Memory; +using ZiggyCreatures.Caching.Fusion.Chaos; +using ZiggyCreatures.Caching.Fusion.Serialization.NewtonsoftJson; + +namespace ZiggyCreatures.Caching.Fusion.Playground.Scenarios +{ + public class LoggingOpenTelemetryListener : EventListener + { + private readonly ILogger _logger; + + public LoggingOpenTelemetryListener(ILogger logger) + { + _logger = logger; + } + protected override void OnEventSourceCreated(EventSource eventSource) + { + if (eventSource.Name.StartsWith("OpenTelemetry")) + EnableEvents(eventSource, EventLevel.Error); + } + + protected override void OnEventWritten(EventWrittenEventArgs eventData) + { + _logger.LogWarning("WARN: Message = {Message}, Payload = {Payload}", eventData.Message, eventData.Payload?.Select(p => p?.ToString())?.ToArray()!); + } + } + + public class FusionCacheInstrumentationTracesOptions + { + } + + public static class OpenTelemetryScenario + { + private static readonly string ServiceName = "FusionCachePlayground.OpenTelemetryScenario"; + private static readonly ActivitySource Source = new ActivitySource("FusionCachePlayground.OpenTelemetryScenario"); + + private static readonly TimeSpan CacheDuration = TimeSpan.FromSeconds(5); + private static readonly TimeSpan FailSafeMaxDuration = TimeSpan.FromSeconds(30); + private static readonly TimeSpan FailSafeThrottleDuration = TimeSpan.FromSeconds(3); + private static readonly TimeSpan FactoryTimeout = TimeSpan.FromSeconds(2); + private static readonly bool UseFailSafe = true; + private static readonly bool UseDistributedCache = true; + private static readonly bool UseBackplane = true; + + private static void SetupOtlp(IServiceCollection services) + { + services.AddOpenTelemetry().WithTracing(builder => + { + builder + //.AddAspNetCoreInstrumentation() + //.AddHttpClientInstrumentation() + //.AddSource(nameof(OtlpScenario)) + .AddSource("ZiggyCreatures.Caching.Fusion") + //.SetResourceBuilder(ResourceBuilder.CreateDefault().AddService("ZiggyCreatures.Caching.Fusion")) + .AddConsoleExporter() + //.AddOtlpExporter(o => + //{ + // o.Protocol = OpenTelemetry.Exporter.OtlpExportProtocol.Grpc; + // o.Endpoint = new Uri($"http://localhost:4317"); + //}) + ; + }); + } + + private static readonly ActivitySource Activity = new ActivitySource(nameof(OpenTelemetryScenario)); + + public static async Task RunAsync() + { + Console.Title = "FusionCache - Open Telemetry"; + + Console.OutputEncoding = Encoding.UTF8; + + // SETUP TRACES + using var tracerProvider = Sdk.CreateTracerProviderBuilder() + .ConfigureResource(rb => rb + .AddService( + ServiceName, + serviceVersion: "0.1.0", + serviceInstanceId: Environment.MachineName + ) + ) + .AddSource("FusionCachePlayground.OpenTelemetryScenario") + + .AddFusionCacheInstrumentation(options => + { + options.IncludeMemoryLevel = true; + options.IncludeDistributedLevel = true; + options.IncludeBackplane = true; + }) + + //.AddOtlpExporter() + //.AddHoneycomb(new HoneycombOptions + //{ + // ServiceName = ServiceName, + // ApiKey = "*********" + //}) + //.AddConsoleExporter() + .Build(); + + // SETUP METRICS + using var meterProvider = Sdk.CreateMeterProviderBuilder() + .ConfigureResource(rb => rb + .AddService( + ServiceName, + serviceVersion: "0.1.0", + serviceInstanceId: Environment.MachineName + ) + ) + + .AddFusionCacheInstrumentation(options => + { + options.IncludeMemoryLevel = true; + options.IncludeDistributedLevel = true; + options.IncludeBackplane = true; + }) + + //.AddHoneycomb(new HoneycombOptions + //{ + // ServiceName = ServiceName, + // ApiKey = "*********" + //}) + + //.AddOtlpExporter(o => + //{ + // o.Endpoint = new Uri("https://api.honeycomb.io:443"); + // o.Headers = $"x-honeycomb-team="*********,x-honeycomb-dataset=fushioncache-metrics"; + //}) + + //.AddOtlpExporter((exporterOptions, metricReaderOptions) => + //{ + // exporterOptions.Endpoint = new Uri("http://localhost:9090/api/v1/otlp/v1/metrics"); + // exporterOptions.Protocol = OtlpExportProtocol.HttpProtobuf; + // metricReaderOptions.PeriodicExportingMetricReaderOptions.ExportIntervalMilliseconds = 1000; + //}) + + //.AddConsoleExporter() + .Build(); + + // DI + var builder = Host.CreateDefaultBuilder(); + builder.ConfigureServices(services => + { + services.AddSingleton(); + }); + + var app = builder.Build(); + + var openTelemetryDebugLogger = app.Services.GetRequiredService(); + + //app.Run(); + + var cachesCount = 1; + var caches = new List(); + IFusionCache fusionCache; + + IDistributedCache? distributedCache = null; + if (UseDistributedCache) + { + // MEMORY + //distributedCache = new MemoryDistributedCache(Options.Create(new MemoryDistributedCacheOptions())); + + // CHAOS + MEMORY + var chaosDistributedCache = new ChaosDistributedCache(new MemoryDistributedCache(Options.Create(new MemoryDistributedCacheOptions()))); + chaosDistributedCache.SetAlwaysDelay(TimeSpan.FromMilliseconds(10), TimeSpan.FromMilliseconds(100)); + + distributedCache = chaosDistributedCache; + } + + for (int i = 0; i < cachesCount; i++) + { + var name = $"CACHE-OTLP"; + + // CACHE OPTIONS + var options = new FusionCacheOptions + { + CacheName = name, + DefaultEntryOptions = new FusionCacheEntryOptions + { + Duration = CacheDuration, + Priority = CacheItemPriority.NeverRemove, + + IsFailSafeEnabled = UseFailSafe, + FailSafeMaxDuration = FailSafeMaxDuration, + FailSafeThrottleDuration = FailSafeThrottleDuration, + + FactorySoftTimeout = FactoryTimeout, + + AllowBackgroundDistributedCacheOperations = false, + AllowBackgroundBackplaneOperations = false + }, + }; + + var cache = new FusionCache(options); + + // DISTRIBUTED CACHE + if (UseDistributedCache && distributedCache is not null) + { + var serializer = new FusionCacheNewtonsoftJsonSerializer(); + Console.WriteLine(); + cache.SetupDistributedCache(distributedCache, serializer); + Console.WriteLine(); + } + + // BACKPLANE + if (UseBackplane) + { + var backplane = new MemoryBackplane(new MemoryBackplaneOptions()); + + Console.WriteLine(); + cache.SetupBackplane(backplane); + Console.WriteLine(); + } + + caches.Add(cache); + } + + fusionCache = caches[0]; + + using (var activity = Source.StartActivity("Top-Level Action (GetOrDefault)")) + { + var tmp0 = await fusionCache.GetOrDefaultAsync("foo", 123); + + Console.WriteLine(); + Console.WriteLine($"pre-initial: {tmp0}"); + } + + using (var activity = Source.StartActivity("Top-Level Action (Set + Eager Refresh)")) + { + await fusionCache.SetAsync("foo", 42, options => options.SetDuration(TimeSpan.FromSeconds(4)).SetEagerRefresh(0.1f)); + Console.WriteLine("waiting for eager refresh..."); + await Task.Delay(400); + await fusionCache.GetOrSetAsync("foo", async _ => { await Task.Delay(1_000); return 42; }); + Console.WriteLine("eager refresh running..."); + await Task.Delay(4_000); + } + + await Task.Delay(1_500); + Console.WriteLine(); + + using (var activity = Source.StartActivity("Top-Level Action (Set)")) + { + 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(); + + using (var activity = Source.StartActivity("Top-Level Action (GetOrSet + Timeout)")) + { + 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(); + + using (var activity = Source.StartActivity("Top-Level Action (GetOrDefault)")) + { + 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(); + + using (var activity = Source.StartActivity("Top-Level Action (GetOrSet + Timeout + Fail)")) + { + 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(); + + using (var activity = Source.StartActivity("Top-Level Action (GetOrSet + Timeout)")) + { + 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(); + + using (var activity = Source.StartActivity("Top-Level Action (GetOrDefault)")) + { + var tmp5 = await fusionCache.GetOrDefaultAsync("foo", options => options.SetDurationSec(1).SetFailSafe(UseFailSafe).SetFactoryTimeouts(1_000)); + Console.WriteLine(); + Console.WriteLine($"tmp5: {tmp5}"); + } + + Console.WriteLine(); + + using (var activity = Source.StartActivity("Top-Level Action (Set)")) + { + await fusionCache.SetAsync("foo", 123, fusionCache.CreateEntryOptions(entry => entry.SetDurationSec(1).SetFailSafe(UseFailSafe))); + } + + await Task.Delay(1_500); + Console.WriteLine(); + + for (int i = 0; i < 10; i++) + { + using (var activity = Source.StartActivity("Top-Level Action (GetOrSet + Immediate Fail)")) + { + await fusionCache.GetOrSetAsync("foo", _ => { throw new Exception("Foo"); }, options => options.SetDurationSec(1).SetFailSafe(UseFailSafe).SetFactoryTimeouts(1_000)); + } + + Console.WriteLine(); + } + + using (var activity = Source.StartActivity("Top-Level Action (Set)")) + { + await fusionCache.SetAsync("foo", 123, options => options.SetDurationSec(1).SetFailSafe(UseFailSafe)); + } + + await Task.Delay(1_500); + Console.WriteLine(); + + using (var activity = Source.StartActivity("Top-Level Action (GetOrSet + Timeout + Fail)")) + { + 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(); + Console.WriteLine("Press any key to exit..."); + + _ = Console.ReadKey(); + + Console.WriteLine("\n\nTHE END"); + } + } +} diff --git a/tests/ZiggyCreatures.FusionCache.Playground/Scenarios/ScratchpadScenario.cs b/tests/ZiggyCreatures.FusionCache.Playground/Scenarios/ScratchpadScenario.cs new file mode 100644 index 00000000..6465bbfb --- /dev/null +++ b/tests/ZiggyCreatures.FusionCache.Playground/Scenarios/ScratchpadScenario.cs @@ -0,0 +1,45 @@ +ο»Ώusing System; +using System.Text; +using System.Threading.Tasks; +using Microsoft.Extensions.Caching.Memory; + +namespace ZiggyCreatures.Caching.Fusion.Playground.Scenarios +{ + public static class ScratchpadScenario + { + public static async Task RunAsync() + { + Console.Title = "FusionCache - Scratchpad"; + + Console.OutputEncoding = Encoding.UTF8; + + // CACHE OPTIONS + var options = new FusionCacheOptions + { + DefaultEntryOptions = new FusionCacheEntryOptions + { + Duration = TimeSpan.FromMinutes(1), + Priority = CacheItemPriority.NeverRemove, + + //IsFailSafeEnabled = true, + //FailSafeMaxDuration = TimeSpan.FromMinutes(10), + //FailSafeThrottleDuration = TimeSpan.FromSeconds(10), + + //FactorySoftTimeout = TimeSpan.FromMilliseconds(100), + + //AllowBackgroundDistributedCacheOperations = false, + //AllowBackgroundBackplaneOperations = false + }, + }; + + var cache = new FusionCache(options); + + const string Key = "test key"; + const string Value = "test value"; + + cache.Set(Key, Value); + + var foo = cache.TryGet(Key).GetValueOrDefault(null); + } + } +} diff --git a/tests/ZiggyCreatures.FusionCache.Playground/ZiggyCreatures.FusionCache.Playground.csproj b/tests/ZiggyCreatures.FusionCache.Playground/ZiggyCreatures.FusionCache.Playground.csproj index 0d75158f..587574b2 100644 --- a/tests/ZiggyCreatures.FusionCache.Playground/ZiggyCreatures.FusionCache.Playground.csproj +++ b/tests/ZiggyCreatures.FusionCache.Playground/ZiggyCreatures.FusionCache.Playground.csproj @@ -7,18 +7,25 @@ enable false ZiggyCreatures.Caching.Fusion.Playground + 1f4e47b6-6dd9-49b0-af63-058750249662 - + + + + + - - + + + + diff --git a/tests/ZiggyCreatures.FusionCache.Simulator/Program.cs b/tests/ZiggyCreatures.FusionCache.Simulator/Program.cs index 1560cdb7..1cf31560 100644 --- a/tests/ZiggyCreatures.FusionCache.Simulator/Program.cs +++ b/tests/ZiggyCreatures.FusionCache.Simulator/Program.cs @@ -11,6 +11,7 @@ using Serilog.Events; using Spectre.Console; using Spectre.Console.Rendering; +using StackExchange.Redis; using ZiggyCreatures.Caching.Fusion.Backplane; using ZiggyCreatures.Caching.Fusion.Backplane.Memory; using ZiggyCreatures.Caching.Fusion.Backplane.StackExchangeRedis; @@ -38,13 +39,15 @@ internal static class SimulatorOptions public static readonly bool EnableLogging = false; public static readonly bool EnableLoggingExceptions = false; + private static readonly string RedisConnection = "127.0.0.1:6379,ssl=False,abortConnect=false,defaultDatabase={0},connectTimeout=1000,syncTimeout=500"; + // DISTRIBUTED CACHE public static readonly DistributedCacheType DistributedCacheType = DistributedCacheType.Memory; public static readonly bool AllowBackgroundDistributedCacheOperations = true; public static readonly TimeSpan? DistributedCacheSoftTimeout = null; //TimeSpan.FromMilliseconds(100); public static readonly TimeSpan? DistributedCacheHardTimeout = null; //TimeSpan.FromMilliseconds(500); public static readonly TimeSpan DistributedCacheCircuitBreakerDuration = TimeSpan.Zero; - public static readonly string DistributedCacheRedisConnection = "127.0.0.1:6379,ssl=False,abortConnect=False,defaultDatabase={0}"; + public static readonly string DistributedCacheRedisConnection = RedisConnection; public static readonly TimeSpan? ChaosDistributedCacheSyntheticMinDelay = null; //TimeSpan.FromMilliseconds(500); public static readonly TimeSpan? ChaosDistributedCacheSyntheticMaxDelay = null; //TimeSpan.FromMilliseconds(500); @@ -52,7 +55,7 @@ internal static class SimulatorOptions public static readonly BackplaneType BackplaneType = BackplaneType.Memory; public static readonly bool AllowBackgroundBackplaneOperations = true; public static readonly TimeSpan BackplaneCircuitBreakerDuration = TimeSpan.Zero; - public static readonly string BackplaneRedisConnection = "127.0.0.1:6379,ssl=False,abortConnect=False,defaultDatabase={0}"; + public static readonly string BackplaneRedisConnection = RedisConnection; public static readonly TimeSpan? ChaosBackplaneSyntheticDelay = null; //TimeSpan.FromMilliseconds(500); // OTHERS @@ -71,15 +74,15 @@ internal class Program private static readonly SemaphoreSlim GlobalMutex = new SemaphoreSlim(1, 1); private static int LastValue = 0; private static int? LastUpdatedClusterIdx = null; - private static readonly ConcurrentDictionary CacheClusters = new ConcurrentDictionary(); - private static readonly ConcurrentDictionary Databases = new ConcurrentDictionary(); + private static readonly ConcurrentDictionary CacheClusters = []; + private static readonly ConcurrentDictionary Databases = []; private static bool DatabaseEnabled = true; - private static readonly List DistributedCaches = new List(); + private static readonly List DistributedCaches = []; private static bool DistributedCachesEnabled = true; - private static readonly List Backplanes = new List(); + private static readonly List Backplanes = []; private static bool BackplanesEnabled = true; // STATS @@ -96,6 +99,12 @@ internal class Program private static readonly Color Color_LightRed = Color.Red3_1; private static readonly Color Color_FlashRed = Color.Red1; + private static ConcurrentDictionary _connectionMultiplexerCache = new ConcurrentDictionary(); + private static IConnectionMultiplexer GetRedisConnectionMultiplexer(string configuration) + { + return _connectionMultiplexerCache.GetOrAdd(configuration, x => ConnectionMultiplexer.Connect(x)); + } + private static IDistributedCache? CreateDistributedCache(int clusterIdx) { switch (SimulatorOptions.DistributedCacheType) @@ -105,7 +114,8 @@ internal class Program case DistributedCacheType.Redis: return new RedisCache(new RedisCacheOptions { - Configuration = string.Format(SimulatorOptions.DistributedCacheRedisConnection, clusterIdx) + //Configuration = string.Format(SimulatorOptions.DistributedCacheRedisConnection, clusterIdx) + ConnectionMultiplexerFactory = async () => GetRedisConnectionMultiplexer(string.Format(SimulatorOptions.BackplaneRedisConnection, clusterIdx)) }); default: return new MemoryDistributedCache(Options.Create(new MemoryDistributedCacheOptions())); @@ -121,9 +131,8 @@ internal class Program case BackplaneType.Redis: return new RedisBackplane(new RedisBackplaneOptions { - Configuration = string.Format(SimulatorOptions.BackplaneRedisConnection, clusterIdx), - //CircuitBreakerDuration = SimulatorScenarioOptions.BackplaneCircuitBreakerDuration, - //AllowBackgroundOperations = SimulatorScenarioOptions.AllowBackplaneBackgroundOperations + //Configuration = string.Format(SimulatorOptions.DistributedCacheRedisConnection, clusterIdx) + ConnectionMultiplexerFactory = async () => GetRedisConnectionMultiplexer(string.Format(SimulatorOptions.BackplaneRedisConnection, clusterIdx)) }); default: return new MemoryBackplane(new MemoryBackplaneOptions() { ConnectionId = $"connection-{clusterIdx}" }); @@ -133,13 +142,11 @@ internal class Program private static void SaveToDb(int clusterIdx, int value) { if (DatabaseEnabled == false) - { throw new Exception("Synthetic database exception"); - } Interlocked.Increment(ref DbWritesCount); - var db = Databases.GetOrAdd(clusterIdx, new SimulatedDatabase()); + var db = Databases.GetOrAdd(clusterIdx, _ => new SimulatedDatabase()); db.Value = value; db.LastUpdateTimestamp = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds(); LastUpdatedClusterIdx = clusterIdx; @@ -148,13 +155,11 @@ private static void SaveToDb(int clusterIdx, int value) private static int? LoadFromDb(int clusterIdx) { if (DatabaseEnabled == false) - { throw new Exception("Synthetic database exception"); - } Interlocked.Increment(ref DbReadsCount); - var db = Databases.GetOrAdd(clusterIdx, new SimulatedDatabase()); + var db = Databases.GetOrAdd(clusterIdx, _ => new SimulatedDatabase()); return db.Value; } @@ -163,7 +168,7 @@ private static async Task UpdateRandomNodeOnClusterAsync(int clusterIdx, ILogger var sw = Stopwatch.StartNew(); await GlobalMutex.WaitAsync(); sw.Stop(); - logger?.LogInformation($"LOCK (UPDATE) TOOK: {sw.ElapsedMilliseconds} ms"); + logger?.LogInformation("LOCK (UPDATE) TOOK: {ElapsedMs} ms", sw.ElapsedMilliseconds); try { @@ -182,11 +187,11 @@ private static async Task UpdateRandomNodeOnClusterAsync(int clusterIdx, ILogger var nodeIdx = RNG.Next(cluster.Nodes.Count); var node = cluster.Nodes[nodeIdx]; - logger?.LogInformation($"BEFORE CACHE SET ({node.Cache.InstanceId}) TOOK: {sw.ElapsedMilliseconds} ms"); + logger?.LogInformation("BEFORE CACHE SET ({CacheInstanceId}) TOOK: {ElapsedMs} ms", node.Cache.InstanceId, sw.ElapsedMilliseconds); sw.Restart(); await node.Cache.SetAsync(CacheKey, LastValue, opt => opt.SetSkipBackplaneNotifications(false)); sw.Stop(); - logger?.LogInformation($"AFTER CACHE SET ({node.Cache.InstanceId}) TOOK: {sw.ElapsedMilliseconds} ms"); + logger?.LogInformation("AFTER CACHE SET ({CacheInstanceId}) TOOK: {ElapsedMs} ms", node.Cache.InstanceId, sw.ElapsedMilliseconds); // SAVE LAST XYZ node.ExpirationTimestampUnixMs = DateTimeOffset.UtcNow.Add(SimulatorOptions.CacheDuration).ToUnixTimeMilliseconds(); @@ -334,7 +339,7 @@ private static void SetupClusters(IServiceProvider serviceProvider, ILogger(CacheKey, _ => LoadFromDb(clusterIdx)); + //// SYNC + //item.Value = node.Cache.GetOrSet(CacheKey, _ => LoadFromDb(clusterIdx)); + // ASYNC + item.Value = await node.Cache.GetOrSetAsync(CacheKey, async _ => LoadFromDb(clusterIdx)).ConfigureAwait(false); item.Error = false; sw.Stop(); - logger?.LogInformation($"CACHE GET ({node.Cache.InstanceId}) TOOK: {sw.ElapsedMilliseconds} ms"); + logger?.LogInformation("CACHE GET ({CacheInstanceId}) TOOK: {ElapsedMs} ms", node.Cache.InstanceId, sw.ElapsedMilliseconds); } catch { item = (true, null); - logger?.LogInformation($"CACHE GET ({node.Cache.InstanceId}) FAILED"); + logger?.LogInformation("CACHE GET ({CacheInstanceId}) FAILED", node.Cache.InstanceId); } clusterValues[nodeIdx] = item; } @@ -475,7 +483,7 @@ static async Task GetValueFromNode(ConcurrentDictionary - + - - + + diff --git a/tests/ZiggyCreatures.FusionCache.Tests/AutoRecoveryTests.cs b/tests/ZiggyCreatures.FusionCache.Tests/AutoRecoveryTests.cs index 5abca4b5..8dc997f6 100644 --- a/tests/ZiggyCreatures.FusionCache.Tests/AutoRecoveryTests.cs +++ b/tests/ZiggyCreatures.FusionCache.Tests/AutoRecoveryTests.cs @@ -26,9 +26,10 @@ public AutoRecoveryTests(ITestOutputHelper output) private FusionCacheOptions CreateFusionCacheOptions() { - var res = new FusionCacheOptions(); - - res.CacheKeyPrefix = TestingCacheKeyPrefix; + var res = new FusionCacheOptions + { + CacheKeyPrefix = TestingCacheKeyPrefix + }; return res; } diff --git a/tests/ZiggyCreatures.FusionCache.Tests/BackplaneTests.cs b/tests/ZiggyCreatures.FusionCache.Tests/BackplaneTests.cs index 6908adef..71575758 100644 --- a/tests/ZiggyCreatures.FusionCache.Tests/BackplaneTests.cs +++ b/tests/ZiggyCreatures.FusionCache.Tests/BackplaneTests.cs @@ -27,9 +27,10 @@ public BackplaneTests(ITestOutputHelper output) private FusionCacheOptions CreateFusionCacheOptions() { - var res = new FusionCacheOptions(); - - res.CacheKeyPrefix = TestingCacheKeyPrefix; + var res = new FusionCacheOptions + { + CacheKeyPrefix = TestingCacheKeyPrefix + }; return res; } @@ -336,6 +337,7 @@ public async Task CanHandleExpireOnMultiNodesAsync(SerializerType serializerType cacheA.SetupBackplane(CreateBackplane(backplaneConnectionId)); cacheA.DefaultEntryOptions.IsFailSafeEnabled = true; cacheA.DefaultEntryOptions.Duration = duration; + cacheA.DefaultEntryOptions.AllowBackgroundDistributedCacheOperations = false; cacheA.DefaultEntryOptions.AllowBackgroundBackplaneOperations = false; using var cacheB = new FusionCache(CreateFusionCacheOptions(), logger: CreateXUnitLogger()); @@ -343,6 +345,7 @@ public async Task CanHandleExpireOnMultiNodesAsync(SerializerType serializerType cacheB.SetupBackplane(CreateBackplane(backplaneConnectionId)); cacheB.DefaultEntryOptions.IsFailSafeEnabled = true; cacheB.DefaultEntryOptions.Duration = duration; + cacheB.DefaultEntryOptions.AllowBackgroundDistributedCacheOperations = false; cacheB.DefaultEntryOptions.AllowBackgroundBackplaneOperations = false; using var cacheC = new FusionCache(CreateFusionCacheOptions(), logger: CreateXUnitLogger()); @@ -350,6 +353,7 @@ public async Task CanHandleExpireOnMultiNodesAsync(SerializerType serializerType cacheC.SetupBackplane(CreateBackplane(backplaneConnectionId)); cacheC.DefaultEntryOptions.IsFailSafeEnabled = true; cacheC.DefaultEntryOptions.Duration = duration; + cacheC.DefaultEntryOptions.AllowBackgroundDistributedCacheOperations = false; cacheC.DefaultEntryOptions.AllowBackgroundBackplaneOperations = false; await Task.Delay(TimeSpan.FromMilliseconds(200)); @@ -433,6 +437,7 @@ public void CanHandleExpireOnMultiNodes(SerializerType serializerType) cacheA.SetupBackplane(CreateBackplane(backplaneConnectionId)); cacheA.DefaultEntryOptions.IsFailSafeEnabled = true; cacheA.DefaultEntryOptions.Duration = duration; + cacheA.DefaultEntryOptions.AllowBackgroundDistributedCacheOperations = false; cacheA.DefaultEntryOptions.AllowBackgroundBackplaneOperations = false; using var cacheB = new FusionCache(CreateFusionCacheOptions(), logger: CreateXUnitLogger()); @@ -440,6 +445,7 @@ public void CanHandleExpireOnMultiNodes(SerializerType serializerType) cacheB.SetupBackplane(CreateBackplane(backplaneConnectionId)); cacheB.DefaultEntryOptions.IsFailSafeEnabled = true; cacheB.DefaultEntryOptions.Duration = duration; + cacheB.DefaultEntryOptions.AllowBackgroundDistributedCacheOperations = false; cacheB.DefaultEntryOptions.AllowBackgroundBackplaneOperations = false; using var cacheC = new FusionCache(CreateFusionCacheOptions(), logger: CreateXUnitLogger()); @@ -447,6 +453,7 @@ public void CanHandleExpireOnMultiNodes(SerializerType serializerType) cacheC.SetupBackplane(CreateBackplane(backplaneConnectionId)); cacheC.DefaultEntryOptions.IsFailSafeEnabled = true; cacheC.DefaultEntryOptions.Duration = duration; + cacheC.DefaultEntryOptions.AllowBackgroundDistributedCacheOperations = false; cacheC.DefaultEntryOptions.AllowBackgroundBackplaneOperations = false; Thread.Sleep(TimeSpan.FromMilliseconds(200)); diff --git a/tests/ZiggyCreatures.FusionCache.Tests/CacheStampedeTests.cs b/tests/ZiggyCreatures.FusionCache.Tests/CacheStampedeTests.cs index 4911a7b5..2d18593a 100644 --- a/tests/ZiggyCreatures.FusionCache.Tests/CacheStampedeTests.cs +++ b/tests/ZiggyCreatures.FusionCache.Tests/CacheStampedeTests.cs @@ -17,30 +17,29 @@ public class CacheStampedeTests [InlineData(1_000)] public async Task OnlyOneFactoryGetsCalledEvenInHighConcurrencyAsync(int accessorsCount) { - using (var cache = new FusionCache(new FusionCacheOptions())) - { - var factoryCallsCount = 0; + using var cache = new FusionCache(new FusionCacheOptions()); - var tasks = new ConcurrentBag(); - Parallel.For(0, accessorsCount, _ => - { - var task = cache.GetOrSetAsync( - "foo", - async _ => - { - Interlocked.Increment(ref factoryCallsCount); - await Task.Delay(FactoryDuration); - return 42; - }, - new FusionCacheEntryOptions(TimeSpan.FromSeconds(10)) - ); - tasks.Add(task.AsTask()); - }); + var factoryCallsCount = 0; + + var tasks = new ConcurrentBag(); + Parallel.For(0, accessorsCount, _ => + { + var task = cache.GetOrSetAsync( + "foo", + async _ => + { + Interlocked.Increment(ref factoryCallsCount); + await Task.Delay(FactoryDuration); + return 42; + }, + new FusionCacheEntryOptions(TimeSpan.FromSeconds(10)) + ); + tasks.Add(task.AsTask()); + }); - await Task.WhenAll(tasks); + await Task.WhenAll(tasks); - Assert.Equal(1, factoryCallsCount); - } + Assert.Equal(1, factoryCallsCount); } [Theory] @@ -49,26 +48,25 @@ public async Task OnlyOneFactoryGetsCalledEvenInHighConcurrencyAsync(int accesso [InlineData(1_000)] public void OnlyOneFactoryGetsCalledEvenInHighConcurrency(int accessorsCount) { - using (var cache = new FusionCache(new FusionCacheOptions())) + using var cache = new FusionCache(new FusionCacheOptions()); + + var factoryCallsCount = 0; + + Parallel.For(0, accessorsCount, _ => { - var factoryCallsCount = 0; + cache.GetOrSet( + "foo", + _ => + { + Interlocked.Increment(ref factoryCallsCount); + Thread.Sleep(FactoryDuration); + return 42; + }, + new FusionCacheEntryOptions(TimeSpan.FromSeconds(10)) + ); + }); - Parallel.For(0, accessorsCount, _ => - { - cache.GetOrSet( - "foo", - _ => - { - Interlocked.Increment(ref factoryCallsCount); - Thread.Sleep(FactoryDuration); - return 42; - }, - new FusionCacheEntryOptions(TimeSpan.FromSeconds(10)) - ); - }); - - Assert.Equal(1, factoryCallsCount); - } + Assert.Equal(1, factoryCallsCount); } [Theory] @@ -77,45 +75,44 @@ public void OnlyOneFactoryGetsCalledEvenInHighConcurrency(int accessorsCount) [InlineData(1_000)] public async Task OnlyOneFactoryGetsCalledEvenInMixedHighConcurrencyAsync(int accessorsCount) { - using (var cache = new FusionCache(new FusionCacheOptions())) - { - var factoryCallsCount = 0; + using var cache = new FusionCache(new FusionCacheOptions()); + + var factoryCallsCount = 0; - var tasks = new ConcurrentBag(); - Parallel.For(0, accessorsCount, idx => + var tasks = new ConcurrentBag(); + Parallel.For(0, accessorsCount, idx => + { + if (idx % 2 == 0) { - if (idx % 2 == 0) - { - var task = cache.GetOrSetAsync( - "foo", - async _ => - { - Interlocked.Increment(ref factoryCallsCount); - await Task.Delay(FactoryDuration); - return 42; - }, - new FusionCacheEntryOptions(TimeSpan.FromSeconds(10)) - ); - tasks.Add(task.AsTask()); - } - else - { - cache.GetOrSet( - "foo", - _ => - { - Interlocked.Increment(ref factoryCallsCount); - Thread.Sleep(FactoryDuration); - return 42; - }, - new FusionCacheEntryOptions(TimeSpan.FromSeconds(10)) - ); - } - }); - - await Task.WhenAll(tasks); - - Assert.Equal(1, factoryCallsCount); - } + var task = cache.GetOrSetAsync( + "foo", + async _ => + { + Interlocked.Increment(ref factoryCallsCount); + await Task.Delay(FactoryDuration); + return 42; + }, + new FusionCacheEntryOptions(TimeSpan.FromSeconds(10)) + ); + tasks.Add(task.AsTask()); + } + else + { + cache.GetOrSet( + "foo", + _ => + { + Interlocked.Increment(ref factoryCallsCount); + Thread.Sleep(FactoryDuration); + return 42; + }, + new FusionCacheEntryOptions(TimeSpan.FromSeconds(10)) + ); + } + }); + + await Task.WhenAll(tasks); + + Assert.Equal(1, factoryCallsCount); } } diff --git a/tests/ZiggyCreatures.FusionCache.Tests/DependencyInjectionTests.cs b/tests/ZiggyCreatures.FusionCache.Tests/DependencyInjectionTests.cs index c1d89c43..081be532 100644 --- a/tests/ZiggyCreatures.FusionCache.Tests/DependencyInjectionTests.cs +++ b/tests/ZiggyCreatures.FusionCache.Tests/DependencyInjectionTests.cs @@ -17,6 +17,7 @@ using ZiggyCreatures.Caching.Fusion.Backplane.StackExchangeRedis; using ZiggyCreatures.Caching.Fusion.Internals.Backplane; using ZiggyCreatures.Caching.Fusion.Internals.Distributed; +using ZiggyCreatures.Caching.Fusion.Locking; using ZiggyCreatures.Caching.Fusion.Plugins; using ZiggyCreatures.Caching.Fusion.Serialization.SystemTextJson; @@ -174,6 +175,94 @@ static List GetAllPlugins(IFusionCache cache) Assert.NotNull(allPlugins.Single(p => p.Name == "P_3")); } + [Fact] + public void CanUseRegisteredMemoryLocker() + { + var services = new ServiceCollection(); + services.AddTransient(sp => new SimpleMemoryLocker()); + + services.AddFusionCache() + .WithRegisteredMemoryLocker() + ; + + using var serviceProvider = services.BuildServiceProvider(); + + var cache = serviceProvider.GetRequiredService(); + + static IFusionCacheMemoryLocker GetMemoryLocker(IFusionCache cache) + { + return (IFusionCacheMemoryLocker)(typeof(FusionCache).GetField("_memoryLocker", BindingFlags.NonPublic | BindingFlags.Instance)!.GetValue(cache)!); + } + + var memoryLocker = GetMemoryLocker(cache); + + Assert.NotNull(cache); + Assert.IsType(memoryLocker); + } + + [Fact] + public void CanThrowWithoutRegisteredMemoryLocker() + { + var services = new ServiceCollection(); + + services.AddFusionCache() + .WithRegisteredMemoryLocker() + ; + + using var serviceProvider = services.BuildServiceProvider(); + + Assert.Throws(() => + { + _ = serviceProvider.GetRequiredService(); + }); + } + + [Fact] + public void CanUseCustomMemoryLocker() + { + var services = new ServiceCollection(); + + services.AddFusionCache() + .WithMemoryLocker(new SimpleMemoryLocker()) + ; + + using var serviceProvider = services.BuildServiceProvider(); + + var cache = serviceProvider.GetRequiredService(); + + static IFusionCacheMemoryLocker GetMemoryLocker(IFusionCache cache) + { + return (IFusionCacheMemoryLocker)(typeof(FusionCache).GetField("_memoryLocker", BindingFlags.NonPublic | BindingFlags.Instance)!.GetValue(cache)!); + } + + var memoryLocker = GetMemoryLocker(cache); + + Assert.NotNull(cache); + Assert.IsType(memoryLocker); + } + + [Fact] + public void UsesStandardMemoryLockerByDefault() + { + var services = new ServiceCollection(); + + services.AddFusionCache(); + + using var serviceProvider = services.BuildServiceProvider(); + + var cache = serviceProvider.GetRequiredService(); + + static IFusionCacheMemoryLocker GetMemoryLocker(IFusionCache cache) + { + return (IFusionCacheMemoryLocker)(typeof(FusionCache).GetField("_memoryLocker", BindingFlags.NonPublic | BindingFlags.Instance)!.GetValue(cache)!); + } + + var memoryLocker = GetMemoryLocker(cache); + + Assert.NotNull(cache); + Assert.IsType(memoryLocker); + } + [Fact] public void TryAutoSetupWorks() { @@ -752,10 +841,10 @@ public void BuilderWithSpecificComponentsWorks() var options = sp.GetService>()?.Get("Baz") ?? new MemoryDistributedCacheOptions(); var loggerFactory = sp.GetService(); - return new MemoryDistributedCache( - Options.Create(options), - loggerFactory - ); + if (loggerFactory is null) + return new MemoryDistributedCache(Options.Create(options)); + + return new MemoryDistributedCache(Options.Create(options), loggerFactory); }) .WithMemoryBackplane() ; diff --git a/tests/ZiggyCreatures.FusionCache.Tests/DistributedCacheLevelTests.cs b/tests/ZiggyCreatures.FusionCache.Tests/DistributedCacheLevelTests.cs index c406a879..7710cb66 100644 --- a/tests/ZiggyCreatures.FusionCache.Tests/DistributedCacheLevelTests.cs +++ b/tests/ZiggyCreatures.FusionCache.Tests/DistributedCacheLevelTests.cs @@ -755,8 +755,8 @@ public async Task CanSkipDistributedReadWhenStaleAsync(SerializerType serializer using var fusionCache1 = new FusionCache(CreateFusionCacheOptions()).SetupDistributedCache(distributedCache, TestsUtils.GetSerializer(serializerType)); using var fusionCache2 = new FusionCache(CreateFusionCacheOptions()).SetupDistributedCache(distributedCache, TestsUtils.GetSerializer(serializerType)); - var v1 = await fusionCache1.GetOrSetAsync("foo", 1, opt => opt.SetDuration(TimeSpan.FromSeconds(2)).SetFailSafe(true)); - var v2 = await fusionCache2.GetOrSetAsync("foo", 2, opt => opt.SetDuration(TimeSpan.FromSeconds(2)).SetFailSafe(true)); + var v1 = await fusionCache1.GetOrSetAsync("foo", 1, opt => opt.SetDuration(TimeSpan.FromSeconds(2)).SetFailSafe(true).SetSkipDistributedCacheReadWhenStale(true)); + var v2 = await fusionCache2.GetOrSetAsync("foo", 2, opt => opt.SetDuration(TimeSpan.FromSeconds(2)).SetFailSafe(true).SetSkipDistributedCacheReadWhenStale(true)); Assert.Equal(1, v1); Assert.Equal(1, v2); @@ -778,8 +778,8 @@ public void CanSkipDistributedReadWhenStale(SerializerType serializerType) using var fusionCache1 = new FusionCache(CreateFusionCacheOptions()).SetupDistributedCache(distributedCache, TestsUtils.GetSerializer(serializerType)); using var fusionCache2 = new FusionCache(CreateFusionCacheOptions()).SetupDistributedCache(distributedCache, TestsUtils.GetSerializer(serializerType)); - var v1 = fusionCache1.GetOrSet("foo", 1, opt => opt.SetDuration(TimeSpan.FromSeconds(2)).SetFailSafe(true)); - var v2 = fusionCache2.GetOrSet("foo", 2, opt => opt.SetDuration(TimeSpan.FromSeconds(2)).SetFailSafe(true)); + var v1 = fusionCache1.GetOrSet("foo", 1, opt => opt.SetDuration(TimeSpan.FromSeconds(2)).SetFailSafe(true).SetSkipDistributedCacheReadWhenStale(true)); + var v2 = fusionCache2.GetOrSet("foo", 2, opt => opt.SetDuration(TimeSpan.FromSeconds(2)).SetFailSafe(true).SetSkipDistributedCacheReadWhenStale(true)); Assert.Equal(1, v1); Assert.Equal(1, v2); @@ -793,6 +793,46 @@ public void CanSkipDistributedReadWhenStale(SerializerType serializerType) Assert.Equal(4, v2); } + [Theory] + [ClassData(typeof(SerializerTypesClassData))] + public async Task DoesNotSkipOnMemoryCacheMissWhenSkipDistributedCacheReadWhenStaleIsTrueAsync(SerializerType serializerType) + { + var distributedCache = new MemoryDistributedCache(Options.Create(new MemoryDistributedCacheOptions())); + using var fusionCache1 = new FusionCache(CreateFusionCacheOptions()).SetupDistributedCache(distributedCache, TestsUtils.GetSerializer(serializerType)); + using var fusionCache2 = new FusionCache(CreateFusionCacheOptions()).SetupDistributedCache(distributedCache, TestsUtils.GetSerializer(serializerType)); + + fusionCache1.DefaultEntryOptions.SkipDistributedCacheReadWhenStale = true; + fusionCache2.DefaultEntryOptions.SkipDistributedCacheReadWhenStale = true; + + await fusionCache1.SetAsync("foo", 21); + + var v1 = await fusionCache1.TryGetAsync("foo"); + var v2 = await fusionCache2.TryGetAsync("foo"); + + Assert.True(v1.HasValue); + Assert.True(v2.HasValue); + } + + [Theory] + [ClassData(typeof(SerializerTypesClassData))] + public void DoesNotSkipOnMemoryCacheMissWhenSkipDistributedCacheReadWhenStaleIsTrue(SerializerType serializerType) + { + var distributedCache = new MemoryDistributedCache(Options.Create(new MemoryDistributedCacheOptions())); + using var fusionCache1 = new FusionCache(CreateFusionCacheOptions()).SetupDistributedCache(distributedCache, TestsUtils.GetSerializer(serializerType)); + using var fusionCache2 = new FusionCache(CreateFusionCacheOptions()).SetupDistributedCache(distributedCache, TestsUtils.GetSerializer(serializerType)); + + fusionCache1.DefaultEntryOptions.SkipDistributedCacheReadWhenStale = true; + fusionCache2.DefaultEntryOptions.SkipDistributedCacheReadWhenStale = true; + + fusionCache1.Set("foo", 21); + + var v1 = fusionCache1.TryGet("foo"); + var v2 = fusionCache2.TryGet("foo"); + + Assert.True(v1.HasValue); + Assert.True(v2.HasValue); + } + [Theory] [ClassData(typeof(SerializerTypesClassData))] public async Task CanHandleConditionalRefreshAsync(SerializerType serializerType) diff --git a/tests/ZiggyCreatures.FusionCache.Tests/EventsTests.cs b/tests/ZiggyCreatures.FusionCache.Tests/EventsTests.cs index bfb128db..28de24c1 100644 --- a/tests/ZiggyCreatures.FusionCache.Tests/EventsTests.cs +++ b/tests/ZiggyCreatures.FusionCache.Tests/EventsTests.cs @@ -712,7 +712,7 @@ public void TryGetStaleNoFailSafe() } [Fact] - public async Task MemoryLayerEventsAsync() + public async Task MemoryLevelEventsAsync() { var stats = new EntryActionsStats(); @@ -752,7 +752,7 @@ public async Task MemoryLayerEventsAsync() } [Fact] - public void MemoryLayerEvents() + public void MemoryLevelEvents() { var stats = new EntryActionsStats(); diff --git a/tests/ZiggyCreatures.FusionCache.Tests/GlobalSuppressions.cs b/tests/ZiggyCreatures.FusionCache.Tests/GlobalSuppressions.cs index 30d8d3ff..97578978 100644 --- a/tests/ZiggyCreatures.FusionCache.Tests/GlobalSuppressions.cs +++ b/tests/ZiggyCreatures.FusionCache.Tests/GlobalSuppressions.cs @@ -5,5 +5,6 @@ using System.Diagnostics.CodeAnalysis; -[assembly: SuppressMessage("Performance", "HAA0301:Closure Allocation Source", Justification = "")] -[assembly: SuppressMessage("Performance", "HAA0302:Display class allocation to capture closure", Justification = "")] +[assembly: SuppressMessage("Performance", "HAA0301:Closure Allocation Source")] +[assembly: SuppressMessage("Performance", "HAA0302:Display class allocation to capture closure")] +[assembly: SuppressMessage("Style", "IDE0290:Use primary constructor")] diff --git a/tests/ZiggyCreatures.FusionCache.Tests/Stuff/SimpleMemoryLocker.cs b/tests/ZiggyCreatures.FusionCache.Tests/Stuff/SimpleMemoryLocker.cs new file mode 100644 index 00000000..0f399034 --- /dev/null +++ b/tests/ZiggyCreatures.FusionCache.Tests/Stuff/SimpleMemoryLocker.cs @@ -0,0 +1,31 @@ +ο»Ώusing System; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.Extensions.Logging; +using ZiggyCreatures.Caching.Fusion.Locking; + +namespace FusionCacheTests.Stuff; + +internal class SimpleMemoryLocker + : IFusionCacheMemoryLocker +{ + public object? AcquireLock(string cacheName, string cacheInstanceId, string key, string operationId, TimeSpan timeout, ILogger? logger, CancellationToken token) + { + throw new NotImplementedException(); + } + + public ValueTask AcquireLockAsync(string cacheName, string cacheInstanceId, string key, string operationId, TimeSpan timeout, ILogger? logger, CancellationToken token) + { + throw new NotImplementedException(); + } + + public void ReleaseLock(string cacheName, string cacheInstanceId, string key, string operationId, object? lockObj, ILogger? logger) + { + throw new NotImplementedException(); + } + + public void Dispose() + { + // EMPTY + } +} diff --git a/tests/ZiggyCreatures.FusionCache.Tests/ZiggyCreatures.FusionCache.Tests.csproj b/tests/ZiggyCreatures.FusionCache.Tests/ZiggyCreatures.FusionCache.Tests.csproj index b0b0c410..2d785e2a 100644 --- a/tests/ZiggyCreatures.FusionCache.Tests/ZiggyCreatures.FusionCache.Tests.csproj +++ b/tests/ZiggyCreatures.FusionCache.Tests/ZiggyCreatures.FusionCache.Tests.csproj @@ -15,14 +15,14 @@ - + - - + + runtime; build; native; contentfiles; analyzers; buildtransitive all