diff --git a/README.md b/README.md index b818e235..8223aa62 100644 --- a/README.md +++ b/README.md @@ -10,11 +10,11 @@ [](https://opensource.org/licenses/MIT)  -[](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%2Fjodydonetti%2FZiggyCreatures.FusionCache&via=jodydonetti) +[](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) </div> -| 🙋♂️ Updating from before `v0.20.0` ? please [read here](docs/Update_v0_20_0.md). | +| 🙋♂️ 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. @@ -66,10 +66,11 @@ These are the **key features** of FusionCache: - [**🔀 Optional 2nd level**](docs/CacheLevels.md): an optional 2nd level handled transparently, with any implementation of `IDistributedCache` - [**💣 Fail-Safe**](docs/FailSafe.md): a mechanism to avoids transient failures, by reusing an expired entry as a temporary fallback - [**⏱ Soft/Hard timeouts**](docs/Timeouts.md): a slow factory (or distributed cache) will not slow down your application, and no data will be wasted +- [**📢 Backplane**](docs/Backplane.md): in a multi-node scenario, it can notify the other nodes about changes in the cache, so all will be in-sync +- [**↩️ Auto-Recovery**](docs/AutoRecovery.md): automatic handling of transient issues with retries and sync logic - [**🧙♂️ Adaptive Caching**](docs/AdaptiveCaching.md): for when you don't know upfront the cache duration, as it depends on the value being cached itself - [**🔂 Conditional Refresh**](docs/ConditionalRefresh.md): like HTTP Conditional Requests, but for caching - [**🦅 Eager Refresh**](docs/EagerRefresh.md): start a non-blocking background refresh before the expiration occurs -- [**📢 Backplane**](docs/Backplane.md): in a multi-node scenario, it can notify the other nodes about changes in the cache, so all will be in-sync - [**🔃 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 - [**💫 Natively sync/async**](docs/CoreMethods.md): native support for both the synchronous and asynchronous programming model @@ -255,28 +256,6 @@ The `DefaultEntryOptions` we did set before will be duplicated and only the dura </details> -## 📖 Documentation - -The documentation is available in the :open_file_folder: [docs](docs/README.md) folder, with: - -- [**🦄 A Gentle Introduction**](docs/AGentleIntroduction.md): what you need to know first -- [**🔀 Cache Levels**](docs/CacheLevels.md): a bried description of the 2 available caching levels and how to setup them -- [**📢 Backplane**](docs/Backplane.md): how to get an always synchronized cache, even in a multi-node scenario -- [**🚀 Cache Stampede prevention**](docs/CacheStampede.md): no more overloads during a cold start or after an expiration -- [**💣 Fail-Safe**](docs/FailSafe.md): an explanation of how the fail-safe mechanism works -- [**⏱ Timeouts**](docs/Timeouts.md): the various types of timeouts at your disposal (calling a factory, using the distributed cache, etc) -- [**📛 Named Caches**](docs/NamedCaches.md): how to work with multiple named FusionCache instances -- [**🧙♂️ Adaptive Caching**](docs/AdaptiveCaching.md): how to adapt cache duration (and more) based on the object being cached itself -- [**🔂 Conditional Refresh**](ConditionalRefresh.md): how to save resources when the remote data is not changed -- [**🦅 Eager Refresh**](EagerRefresh.md): how to start a background refresh eagerly, before the expiration occurs -- [**🔃 Dependency Injection**](docs/DependencyInjection.md): how to work with FusionCache + DI in .NET -- [**🎚 Options**](docs/Options.md): everything about the available options, both cache-wide and per-call -- [**🕹 Core Methods**](docs/CoreMethods.md): what you need to know about the core methods available -- [**📞 Events**](docs/Events.md): the events hub and how to use it -- [**🧩 Plugins**](docs/Plugins.md): how to create and use plugins -- [**📜 Logging**](docs/Logging.md): logging configuration and usage - - ## **👩🏫 Step By Step** If you are in for a ride you can read a complete [step by step example](docs/StepByStep.md) of why a cache is useful, why FusionCache could be even more so, how to apply most of the options available and what **results** you can expect to obtain. @@ -286,6 +265,13 @@ If you are in for a ride you can read a complete [step by step example](docs/Ste </div> +## 🖥️ Simulator + +Distributed systems are, in general, quite complex to understand. + +When using FusionCache with the [distributed cache](docs/CacheLevels.md), the [backplane](docs/Backplane.md) and [auto-recovery](docs/AutoRecovery.md) the Simulator can help us **seeing** the whole picture. + +[](docs/Simulator.md) ## 🆎 Comparison diff --git a/ZiggyCreatures.FusionCache.sln b/ZiggyCreatures.FusionCache.sln index 0bf33ad2..919dc060 100644 --- a/ZiggyCreatures.FusionCache.sln +++ b/ZiggyCreatures.FusionCache.sln @@ -41,7 +41,9 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ZiggyCreatures.FusionCache. EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ZiggyCreatures.FusionCache.Serialization.ServiceStackJson", "src\ZiggyCreatures.FusionCache.Serialization.ServiceStackJson\ZiggyCreatures.FusionCache.Serialization.ServiceStackJson.csproj", "{CE437FB2-510F-4DCE-8A1F-AED747DAA4EB}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SerializerPayloadGenerator", "tests\SerializerPayloadGenerator\SerializerPayloadGenerator.csproj", "{5B1AF24E-90FC-4C21-AF9C-090FE32027E3}" +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}" EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution @@ -105,6 +107,10 @@ Global {5B1AF24E-90FC-4C21-AF9C-090FE32027E3}.Debug|Any CPU.Build.0 = Debug|Any CPU {5B1AF24E-90FC-4C21-AF9C-090FE32027E3}.Release|Any CPU.ActiveCfg = Release|Any CPU {5B1AF24E-90FC-4C21-AF9C-090FE32027E3}.Release|Any CPU.Build.0 = Release|Any CPU + {BDB46997-84D1-4CB5-B967-7F820820CB8E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {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 EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -124,6 +130,7 @@ Global {919CDF8C-463A-4E82-AFFD-DF8A6B904600} = {34B53F49-F5C5-4850-B79E-59AD130379C6} {CE437FB2-510F-4DCE-8A1F-AED747DAA4EB} = {34B53F49-F5C5-4850-B79E-59AD130379C6} {5B1AF24E-90FC-4C21-AF9C-090FE32027E3} = {C6F3C570-C68C-4A95-960E-82778306BDBA} + {BDB46997-84D1-4CB5-B967-7F820820CB8E} = {C6F3C570-C68C-4A95-960E-82778306BDBA} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {92916FA2-FCAC-406E-BF3F-0A2CE9512EF0} diff --git a/benchmarks/ZiggyCreatures.FusionCache.Benchmarks/ExecutionBenchmarkAsync.cs b/benchmarks/ZiggyCreatures.FusionCache.Benchmarks/ExecutionBenchmarkAsync.cs new file mode 100644 index 00000000..9052ba5e --- /dev/null +++ b/benchmarks/ZiggyCreatures.FusionCache.Benchmarks/ExecutionBenchmarkAsync.cs @@ -0,0 +1,72 @@ +using System; +using System.Threading; +using System.Threading.Tasks; +using BenchmarkDotNet.Attributes; +using BenchmarkDotNet.Columns; +using BenchmarkDotNet.Configs; +using ZiggyCreatures.Caching.Fusion.Internals; + +namespace ZiggyCreatures.Caching.Fusion.Benchmarks +{ + [MemoryDiagnoser] + [Config(typeof(Config))] + public class ExecutionBenchmarkAsync + { + private class Config : ManualConfig + { + public Config() + { + AddColumn( + StatisticColumn.P95 + ); + } + } + + private async Task ExecutorAsync() + { + for (int i = 0; i < 1_000_000_000; i++) + { + i++; + } + } + + [Benchmark(Baseline = true)] + public async Task WithTimeout() + { + await RunUtils.RunAsyncActionAdvancedAsync( + async _ => await ExecutorAsync(), + TimeSpan.FromSeconds(2), + false, + true + ); + } + + [Benchmark] + public async Task WithTimeout2() + { + await RunUtils.RunAsyncActionAdvancedAsync( + async _ => await ExecutorAsync(), + TimeSpan.FromSeconds(2), + true, + true + ); + } + + [Benchmark] + public async Task WithoutTimeout() + { + await RunUtils.RunAsyncActionAdvancedAsync( + async _ => await ExecutorAsync(), + Timeout.InfiniteTimeSpan, + true, + true + ); + } + + [Benchmark] + public async Task Raw() + { + await ExecutorAsync(); + } + } +} diff --git a/benchmarks/ZiggyCreatures.FusionCache.Benchmarks/ExecutionBenchmarkSync.cs b/benchmarks/ZiggyCreatures.FusionCache.Benchmarks/ExecutionBenchmarkSync.cs new file mode 100644 index 00000000..05b13ae6 --- /dev/null +++ b/benchmarks/ZiggyCreatures.FusionCache.Benchmarks/ExecutionBenchmarkSync.cs @@ -0,0 +1,71 @@ +using System; +using System.Threading; +using BenchmarkDotNet.Attributes; +using BenchmarkDotNet.Columns; +using BenchmarkDotNet.Configs; +using ZiggyCreatures.Caching.Fusion.Internals; + +namespace ZiggyCreatures.Caching.Fusion.Benchmarks +{ + [MemoryDiagnoser] + [Config(typeof(Config))] + public class ExecutionBenchmarkSync + { + private class Config : ManualConfig + { + public Config() + { + AddColumn( + StatisticColumn.P95 + ); + } + } + + private void Executor() + { + for (int i = 0; i < 1_000_000_000; i++) + { + i++; + } + } + + [Benchmark(Baseline = true)] + public void WithTimeout() + { + RunUtils.RunSyncActionAdvanced( + _ => Executor(), + TimeSpan.FromSeconds(2), + false, + true + ); + } + + [Benchmark] + public void WithTimeout2() + { + RunUtils.RunSyncActionAdvanced( + _ => Executor(), + TimeSpan.FromSeconds(2), + true, + true + ); + } + + [Benchmark] + public void WithoutTimeout() + { + RunUtils.RunSyncActionAdvanced( + _ => Executor(), + Timeout.InfiniteTimeSpan, + true, + true + ); + } + + [Benchmark] + public void Raw() + { + Executor(); + } + } +} diff --git a/benchmarks/ZiggyCreatures.FusionCache.Benchmarks/ParallelComparisonBenchmark.cs b/benchmarks/ZiggyCreatures.FusionCache.Benchmarks/ParallelComparisonBenchmark.cs index eb7a10c7..ec5b78dd 100644 --- a/benchmarks/ZiggyCreatures.FusionCache.Benchmarks/ParallelComparisonBenchmark.cs +++ b/benchmarks/ZiggyCreatures.FusionCache.Benchmarks/ParallelComparisonBenchmark.cs @@ -45,9 +45,9 @@ public Config() [Params(1, 10)] public int Rounds; - private List<string> Keys; + private List<string> Keys = null!; private TimeSpan CacheDuration = TimeSpan.FromDays(10); - private IServiceProvider ServiceProvider; + private IServiceProvider ServiceProvider = null!; [GlobalSetup] public void Setup() @@ -263,7 +263,7 @@ public void CacheManager() [Benchmark] public async Task CacheTower() { - await using (var cache = new CacheStack(new[] { new MemoryCacheLayer() }, new[] { new AutoCleanupExtension(TimeSpan.FromMinutes(5)) })) + 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); diff --git a/benchmarks/ZiggyCreatures.FusionCache.Benchmarks/SequentialComparisonBenchmarkAsync.cs b/benchmarks/ZiggyCreatures.FusionCache.Benchmarks/SequentialComparisonBenchmarkAsync.cs index 66d9871a..ac2e2f0c 100644 --- a/benchmarks/ZiggyCreatures.FusionCache.Benchmarks/SequentialComparisonBenchmarkAsync.cs +++ b/benchmarks/ZiggyCreatures.FusionCache.Benchmarks/SequentialComparisonBenchmarkAsync.cs @@ -35,9 +35,9 @@ public Config() [Params(1, 50)] public int Rounds; - private List<string> Keys; + private List<string> Keys = null!; private TimeSpan CacheDuration = TimeSpan.FromDays(10); - private IServiceProvider ServiceProvider; + private IServiceProvider ServiceProvider = null!; [GlobalSetup] public void Setup() @@ -82,7 +82,7 @@ await cache.GetOrSetAsync<SamplePayload>( [Benchmark] public async Task CacheTower() { - await using (var cache = new CacheStack(new[] { new MemoryCacheLayer() }, new[] { new AutoCleanupExtension(TimeSpan.FromMinutes(5)) })) + 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); diff --git a/benchmarks/ZiggyCreatures.FusionCache.Benchmarks/SequentialComparisonBenchmarkSync.cs b/benchmarks/ZiggyCreatures.FusionCache.Benchmarks/SequentialComparisonBenchmarkSync.cs index 3b7725d3..00302acd 100644 --- a/benchmarks/ZiggyCreatures.FusionCache.Benchmarks/SequentialComparisonBenchmarkSync.cs +++ b/benchmarks/ZiggyCreatures.FusionCache.Benchmarks/SequentialComparisonBenchmarkSync.cs @@ -32,9 +32,9 @@ public Config() [Params(1, 50)] public int Rounds; - private List<string> Keys; + private List<string> Keys = null!; private TimeSpan CacheDuration = TimeSpan.FromDays(10); - private IServiceProvider ServiceProvider; + private IServiceProvider ServiceProvider = null!; [GlobalSetup] public void Setup() diff --git a/benchmarks/ZiggyCreatures.FusionCache.Benchmarks/ZiggyCreatures.FusionCache.Benchmarks.csproj b/benchmarks/ZiggyCreatures.FusionCache.Benchmarks/ZiggyCreatures.FusionCache.Benchmarks.csproj index 1a300b7f..c79757f5 100644 --- a/benchmarks/ZiggyCreatures.FusionCache.Benchmarks/ZiggyCreatures.FusionCache.Benchmarks.csproj +++ b/benchmarks/ZiggyCreatures.FusionCache.Benchmarks/ZiggyCreatures.FusionCache.Benchmarks.csproj @@ -2,17 +2,17 @@ <PropertyGroup> <OutputType>Exe</OutputType> - <TargetFramework>net6.0</TargetFramework> + <TargetFramework>net7.0</TargetFramework> <LangVersion>latest</LangVersion> <Nullable>enable</Nullable> <RootNamespace>ZiggyCreatures.Caching.Fusion.Benchmarks</RootNamespace> </PropertyGroup> <ItemGroup> - <PackageReference Include="BenchmarkDotNet" Version="0.13.6" /> + <PackageReference Include="BenchmarkDotNet" Version="0.13.10" /> <PackageReference Include="CacheManager.Microsoft.Extensions.Caching.Memory" Version="1.2.0" /> - <PackageReference Include="CacheTower" Version="0.13.0" /> - <PackageReference Include="EasyCaching.InMemory" Version="1.9.0" /> + <PackageReference Include="CacheTower" Version="0.14.0" /> + <PackageReference Include="EasyCaching.InMemory" Version="1.9.2" /> <PackageReference Include="LazyCache" Version="2.4.0" /> </ItemGroup> diff --git a/docs/AGentleIntroduction.md b/docs/AGentleIntroduction.md index aa9c85e4..382b926f 100644 --- a/docs/AGentleIntroduction.md +++ b/docs/AGentleIntroduction.md @@ -109,6 +109,17 @@ In both cases it is possible (and enabled *by default*, so we don't have to do a Read more [**here**](Timeouts.md), or enjoy the complete [**step by step**](StepByStep.md) guide. +## ↩️ Auto-Recovery([more](AutoRecovery.md)) + +As we know from the [Fallacies Of Distributed Computing](https://en.wikipedia.org/wiki/Fallacies_of_distributed_computing), something may go wrong while we are using distributed components like the distributed cache or the backplane, even if only in a transient way. + +Without some extra care what can happen is that data would not be saved in the distributed cache or other nodes may not be notified of changes: this would result in out-of-sync issues. + +wouldn't it be nice if FusionCache would help us is some way when transient error happens? + +Enter **Auto-Recovery**.: everything is done automatically, and it just works. + +Read more [**here**](AutoRecovery.md). ## 🎚️ Options ([more](Options.md)) diff --git a/docs/AdaptiveCaching.md b/docs/AdaptiveCaching.md index 6ed0280e..dcf3975f 100644 --- a/docs/AdaptiveCaching.md +++ b/docs/AdaptiveCaching.md @@ -30,7 +30,7 @@ In the ones **with** the context you can simply change the context's `Options` p Here are 2 examples, with and without the *context* object. -### Example: without adaptive caching +### 👩💻 Example: without adaptive caching As you can see we are specifying the factory as a lambda that takes as input only a cancellation token `ct` (of type `CancellationToken`) and nothing else. @@ -45,7 +45,7 @@ var product = cache.GetOrSet<Product>( ); ``` -### Example: with adaptive caching +### 👩💻 Example: with adaptive caching As you can see we are specifying the factory as a lambda that takes as input both a context `ctx` (of type `FusionCacheFactoryExecutionContext`) and a cancellation token `ct` (of type `CancellationToken`), so that we are able to change the options inside the factory itself. @@ -83,7 +83,7 @@ You may change other options too, like the `Priority` for example. Of course ther are some changes that wouldn't make much sense: if for example we change the `FactorySoftTimeout` after the factory has been already executed we shouldn't expect much to happen, right 😅 ? -## ⏱ Timeouts & Background factory completion +## ⏱ Timeouts & Background Factory Completion Short version: everything works as expected! diff --git a/docs/AutoRecovery.md b/docs/AutoRecovery.md new file mode 100644 index 00000000..b2a38e58 --- /dev/null +++ b/docs/AutoRecovery.md @@ -0,0 +1,108 @@ +<div align="center"> + + + +</div> + +# ↩️ Auto-Recovery + +Both the distributed cache and the backplane are, as the names suggest, distributed components. + +This means that, as we know from the [Fallacies Of Distributed Computing](https://en.wikipedia.org/wiki/Fallacies_of_distributed_computing), something may go wrong while we are using them, even if only in a transient way. + +For example the backplane can loose the connection for some time and each (or some) of the nodes' local memory caches will become out of sync because of some missed backplane notifications. Another example is that the distributed cache can become unavailable for a while because it is restarting or because an unhandled network topology change has disrupted the connectivity for a brief moment, and a value which has been already saved in the local memory cache may not have been saved to the distributed cache. + +Looking at the available methods (like `Set`, `Remove`, `GetOrSet`, etc) we can say that the *intent* of our actions is clear, like *"I want to set the cache entry for this cache key to this value"*: wouldn't it be nice if FusionCache would help us is some way when transient error happens? + +Enter **Auto-Recovery**. + +With auto-recovery FusionCache will automatically detect transient errors for both the distributed cache and the backplane, and it will act accordingly to ensure that the **global state** is kept as much in-sync as possible, without any intervention on our side. + +This is done thanks to an auto-recovery queue, where items are put when something bad happened during the distributed side of things: the queue is then actively processed, periodically, to ensure that as soon as possible everything will be taken care of. + +More errors on a subsequent retry? Again, all taken care of until everything works out well. + +This feature is not implemented **inside** of a specific backplane implementation - of which there are multiple - but inside FusionCache itself: this means that it works with any backplane implementation automatically, which is nice. + +We should also keep in mind that auto-recovery works for both the distributed cache and the backplane, either when using them together or when using only one of them. + +## ❤ Special Care + +Sometimes the transient errors are not so transient after all, and it may happen that before a retry from the auto-recovery queue has been able to succeed a new value for the same cache key is set, on the same node. + +WHat should FusionCache do? + +Another nice one is when, before having been able to process an auto-recovery queue, the backplane came back on and the node received a notification for the same cache key from another node. + +WHat should FusionCache do? + +Special care has been put into correctly handling some common scenarios: +- if an auto-recovery item is about to be queued for a cache key for which there already is another queued item, only the last one will be kept since the result of updating the cache for the same cache key back-to-back would be the same as doing only the last one +- if a backplane notification is received on a node for a cache key for which there is a queued auto-recovery item, only the most recent one is kept: if the incoming one is newer, the local one is discarded and the incoming one is processed, otherwise the incoming one is ignored and the local one is processed to be sent to the other nodes. This avoids, for example, evicting an entry from a local cache if it has been updated after a change in a remote node, which would be useless +- when FusionCache process an item from the auto-recovery queue, it also checks if meanwhile things have changed: if the distributed cache has since been updated and the local value is not the most updated anymore, it will stop procesing the item, remove it from the queue and update the local value from the distributed one. If, on the other hand, it is still the most updated, it will proceed in updating the distributed cache and/or publish a backplane notification to update the other nodes + +These and many more cases are all handled, automatically, without the need to do anything at all. + +An automatic cleanup is also done to handle items in the auto-recovery queue that may have become useless: for example an item for a cache entry with a duration of `2 sec` that has been in the auto-recovery queue for more than that can be safely removed from it since it would have been already expired. + +Keep reading for more "funny" scenarios, it's a ride. + +## 😏 Some Examples (the easy ones) + +Let's say a backplane error occurred while sending a notification: no worries, auto-recovery will automatically retry to send it as soon as possible, after the backplane will become available again. + +Or maybe a value has not been saved to the distributed cache because of some temporary hiccups: again, auto-recovery will handle this automatically, by trying to save it again in the future as soon as the distributed cache will be available again. + +But it's not just as simple as this: if we are using a distributed cache **and** a backplane together, a fail in saving a value to the distributed cache should also avoid sending the notification on the backplane, otherwise the other nodes may be notified of the presence of a new value, but that value has yet to be saved onto the global shared state, which is the distributed cache. By awaiting for both to be available, FusionCache makes sure that the end result is as intended. + +## 😈 Another Example (a harder one) + +Ok, let's up the ante a little bit. + +A value is set on node `N1`: the distributed cache is updated, but the backplane fails so auto-recovery kicks in and queue the item for a later retry. + +Immediately after this, and before the retry on node `N1`, a different value for the **same** cache key is set on node `N2`: here instead the distributed cache fails, so the backplane part is not even tried and, again, auto-recovery kicks in for a later retry. + +Finally a node `N3` sits there, doing nothing, without having a value for that cache key in its local memory cache. + +Now the distributed and the backplane both come back: at slightly different times both `N1` and `N2` will try to process their own auto-recovery queue, not knowing that other nodes may have updated the same cache entry for the same cache key, at slightly different times and with different values. + +Now let's look at 2 different ways this can play out. + +In the first let's say that `N1` starts processing the auto-recovery queue first, and it sees that the value in the distributed cache is still aligned with the local one, so it will proceed sending the backplane notifications to the other nodes to warn them about the update: `N3` receives the notification from `N1`, sees that it is not related to a cache entry that it has in its local memory cache and does nothing, since there's nothing to do. `N2` instead sees the notification from `N1` and notices that it has a queued one which is newer: because of this it will discard the incoming notification, and proceed by processing the pending one. In doing this it sees that the value in the distributed cache is older, and so it updates the distribted cache and sends the notification to the other nodes (`N1` and `N3`). `N3` again sees the incoming notification and does nothing, for the same reason as before. `N1` instead receives the notification from `N2`, checks its local memory cache, sees that it's older and it updates the local value from the distributed cache. + +In the second let's say that `N2` starts processing the auto-recovery queue first, and it sees that the value in the distributed cache is older than the local one so it will proceed updating the distributed cache and, if that succeeds, will also send the backplane notifications to the other nodes to warn them about the update: `N3` receives the notification from `N2`, sees that it is not related to a cache entry that it has in its local memory cache and does nothing, since there's nothing to do. `N1` instead sees the notification from `N2` and notices that it has a queued one which is older: because of this it will discard the queued one and update the local memory cache with the distributed one. + +In both cases now everything is perfectly aligned. + +## 🐲 Hic Sunt Dracones (the epic one) + +Let's say the situation is the same as the previous one, but the nodes are 10, from `N1` to `N10`. + +Now let's say that 6 of them had updates on them and, of those 6, 3 failed with the distributed cache and the other 3 succeeded with the distributed cache but failed with the backplane. + +Now the distributed cache comes back, but not the backplane: the processing of the auto-recovery queue starts on each node, and each node updates the distributed cache if they see that their version is the most updated one or update their local copy in case is the opposite, but since the backplane is still down they will all fail to complete the auto-recovery process. + +After some time the distributed cache goes down again, but - surprise - the backplane comes back up. + +Again the auto-recovery queue processing starts but, again, with no luck. + +While all of this happens and while there are all these queued auto-recovery items to still process, here come some new updates for the same cache key on a couple of nodes, let's say on nodes `N4` and `N6`: in their own local auto-recovery queues the items for that cache key will be replaced by the new ones automatically. + +Then both the distributed cache and the backplane come back up, this time together. + +And now the magic happens, and all the pending checks on the distributed cache and all the pending backplane notifications will be published, an all the conflicts will be resolved based on which piece of data is more fresh and both the data in the distributed cache and all the nodes that have data for that cache key (and only those) will be updated with the latest version. + +And now all the nodes that had a cached value for that cache key are all aligned. + +And the out-of-sync dragon has been defeated, and all of the nodes lived happily ever after. + +*Fin*. + +## 🖥️ Seeing is believing + +Wanna see auto-recovery in action? + +Sure, why not? Thanks to the [Simulator](Simulator.md) it's very easy: + +[](https://youtu.be/6jGX6ePgD3Q) \ No newline at end of file diff --git a/docs/Backplane.md b/docs/Backplane.md index b3eb69cc..e8f14e21 100644 --- a/docs/Backplane.md +++ b/docs/Backplane.md @@ -58,31 +58,15 @@ The second approach (PASSIVE) is not good either, since it has these problems: The third approach (LAZY) is the sweet spot, since it just says to each node _"hey, this data is changed, evict your local copy"_: at the next request for that data, if and only if it ever arrives, the data will be automatically get from the distributed cache and everything will work normally, thanks to the cache stampede prevention and all the other features. -One final thing to notice is that FusionCache automatically differentiates between a notification for a change in a piece of data (eg: with `Set(...)` call) and a notification for the removal of a piece of data (eg: with a `Remove(...)` call): why is that? Because if something has been removed from the cache, it will effectively be removed on all the other nodes, to avoid returning something that does not exist anymore. On the other hand if a piece of data is changed, the other nodes will simply mark their local cached copies (if any) as expired, so that subsequent calls for the same data may return the old version in case of problems, if fail-safe will be enabled for those calls. +One final thing to notice is that FusionCache automatically differentiates between a notification for a change in a piece of data (eg: with `Set(...)` call) and a notification for the removal of a piece of data (eg: with a `Remove(...)` call). +But why is that? -## ↩️ Auto-Recovery - -Since the backplane is implemented on top of a distributed component (in general some sort of message bus, like the Redis Pub/Sub feature) sometimes things can go bad: the message bus can restart or become temporarily unavailable, transient network errors may occur or anything else. - -In those situations each nodes' local memory caches will become out of sync, since they would've missed some notifications. - -Wouldn't it be nice if FusionCache would help us is some way? - -Enter **auto-recovery**. - -With auto-recovery FusionCache will detect notifications that failed to be sent, put them in a local temporary queue and when later on the backplane will become available again, it will try to send them to all the other nodes, to re-sync them correctly. - -Special care has been put into correctly handling some common situations, like: -- if more than one notification is about to be queued for the same cache key, only the last one will be kept since the result of sending 2 notifications for the same cache key back-to-back would be the same -- if a notification is received for a cache key for which there is a queued notification, only the most recent one is kept: if the incoming one is newer, the local one is discarded and the incoming one is processed, otherwise the incoming one is ignored and the local one is sent to the other nodes. This avoids, for example, evicting an entry from a local cache if it has been updated after a change in a remote node, which would be useless -- it is possible to set a limit in how many notifications to keep in the queue via the `BackplaneAutoRecoveryMaxItems` option to avoid consuming too much memory as it will become available again (default value: `null` which means no limits). If a notification is about to be queued but the limit has already been reached, an heuristic is used to remove the notification for the cache entry that will expire sooner (calculated as: instant when the notification has been created + cache entry's `Duration`), to limit as much as possible the impact on the global shared state synchronization -- when a backplane becomes available again, a little amount of time is awaited to avoid small sync issues, to better handle backpressure in an automatic way (configurable via the `BackplaneAutoRecoveryReconnectDelay` option) -- when sending a pending backplane notification from the auto-recovery queue, it is possible to also expire the value on the distributed cache in case the underlying server/service is actually the same (eg: Redis), since when the backplane notification failed it probably also failed the saving of the data in the distributed cache. This can be enabled/disabled via the `EnableDistributedExpireOnBackplaneAutoRecovery` option +Because if something has been removed from the cache, it will effectively be removed on all the other nodes, to avoid returning something that does not exist anymore. On the other hand if a piece of data is changed, the other nodes will simply mark their local cached copies (if any) as expired, so that subsequent calls for the same data may return the old version in case of problems, if fail-safe will be enabled for those calls. -This feature is not implemented **inside** a specific backplane implementation, of which there are multiple, but inside FusionCache itself: this means that it works with any backplane implementation, which is nice. +## ↩️ Auto-Recovery -**ℹ NOTE:** auto-recovery is available since version `0.14.0`, but it's enabled by default only since version `0.17.0`. +Since the backplane is implemented on top of a distributed component (just like the distributed cache), most of the transient errors that may occur on it are also covered by the Auto-Recovery feature: you can read more on the related [docs page](AutoRecovery.md). ## 📦 Packages @@ -96,11 +80,12 @@ Currently there are 2 official packages we can use: If we are already using a Redis instance as a distributed cache, we just have to point the backplane to the same instance and we'll be good to go (but if we share the same Redis instance with multiple caches, please read [some notes](RedisNotes.md)). -### Example +### 👩💻 Example + As an example, we'll use FusionCache with [Redis](https://redis.io/), as both a **distributed cache** and a **backplane**. -To start, just install the Nuget packages: +To start, we just install the Nuget packages: ```PowerShell # CORE PACKAGE @@ -116,7 +101,7 @@ PM> Install-Package Microsoft.Extensions.Caching.StackExchangeRedis PM> Install-Package ZiggyCreatures.FusionCache.Backplane.StackExchangeRedis ``` -Then, to create and setup the cache manually, do this: +Then, to create and setup the cache manually, we can do this: ```csharp // INSTANTIATE FUSION CACHE @@ -158,15 +143,38 @@ services.AddFusionCache() ; ``` -The most common scenario is probably to use both a distributed cache and a backplane, working together: the former used as a shared state that all nodes can use, and the latter used to notify all the nodes about synchronization events so that every node is perfectly updated. +## 🗃 Wire Format Versioning -But is it really necessary to use a distributed cache at all? +When working with the memory cache, everything is easier: at every run of our apps or services everything starts clean, from scratch, so even if there's a change in the structure of the cache entries used by FusionCache there's no problem. + +The backplane, instead, is different: when sending a notification to other nodes that data is shared between different instances of the same applications, between different applications altogether and maybe even with different applications that are using a different version of FusionCache. + +So when the structure of the backplane notification need to change to evolve FusionCache, how can this be managed? -Let's find out. +Easy, by using an additional channel name modifier for the backplane, so that if and when the version of the backplane message needs to change, there will be no issues sending or receiving different versions. +In practice this means that, when creating the name of the channel name for the backplane, a version modifier (eg: an extra piece of string) is used, something like `%CHANNEL_PREFIX%` + `.Backplane:` + `%VERSION%"`. + +This is the way to manage changes in the wire format between updates: it has been designed in this way specifically to support FusionCache to be updated safely and transparently, without interruptions or problems. + +So what happens when there are 2 version of FusionCache running on the same backplane instance, for example when two different apps share the same Redis instance, and one is updated and the other is not? + +Since the old version will send messages to the backplane with a different channel name than the new version, this will not create conflicts during the update, and it means that we don't need to stop all the apps and services that works on it just to do the upgrade. + +At the same time though, if we have different apps and services that use the same distributed cache shared between them, we need to understand that by updating only one app or service and not the others will mean that the ones updated will read/write using the new distributed cache keys, while the non updated ones will keep read/write using the old distributed cache keys. + +Again, nothing catastrophic, but something to consider. ## 🤔 Distributed cache: is it really necessary? +The most common scenario is probably to use both a distributed cache and a backplane, working together: the former used as a shared state that all nodes can use, and the latter used to notify all the nodes about synchronization events so that every node is perfectly updated. + +But is it really necessary to use a distributed cache at all? + +The short answer is yes, that would be the suggested approach. + +A longer answer is that we may even just use a backplane without a distributed cache, if we so choose. + The idea seems like a nice one: in a multi-node scenario we may want to use only memory caches on each node + the backplane for cache synchronization, without having to use a shared distributed cache. But remember: when using a backplane, FusionCache automatically publish notifications everytime something "changes" in the cache, namely when we directly call `Set` or `Remove` (of course) but also when calling `GetOrSet` AND the factory actually go to the database (or whatever) to get the fresh piece of data. @@ -207,7 +215,7 @@ But then, when we **want** to publish a notification, how can we do it? Easy pea Let's look at a concrete example. -### Example +### 👩💻 Example ```csharp // INITIAL SETUP: SKIP AUTOMATIC NOTIFICATIONS @@ -247,7 +255,9 @@ To better understand what would happen otherwise let's look at an example, again Now `N1` and `N2` will have different data cached for `5 min`, see the problem? -So when using a backplane I would **really** suggest using a distributed cache too, otherwise the system may become a little bit too fragile. If, on the other hand, we are comfortable with such a situation, by all means use it. +So when using a backplane I would **really** suggest using a distributed cache too, otherwise the system may become a little bit too fragile. + +If, on the other hand, we are comfortable with such a situation, by all means we can use it. ## Conclusion diff --git a/docs/CacheLevels.md b/docs/CacheLevels.md index 29e8921e..108b7802 100644 --- a/docs/CacheLevels.md +++ b/docs/CacheLevels.md @@ -4,7 +4,7 @@ </div> -# :twisted_rightwards_arrows: Cache Levels: Primary and Secondary +# 🔀 Cache Levels: Primary and Secondary There are 2 caching levels available, transparently handled by FusionCache for you: @@ -27,6 +27,42 @@ Of course in both cases you will also have at your disposal the added ability to Finally, if needed you can also specify a different `Duration` specific for the distributed cache via the `DistributedCacheDuration` option, so that updates to the distributed cache can be picked up more frequently, in case you don't want to use a [backplane](Backplane.md) for some reason. +## 🗃 Wire Format Versioning + +When working with the memory cache, everything is easier: at every run of our apps or services everything starts clean, from scratch, so even if there's a change in the structure of the cache entries used by FusionCache there's no problem. + +The distributed cache, instead, is a different beast: when saving a cache entry in there, that data is shared between different instances of the same applications, between different applications altogether and maybe even with different applications that are using a different version of FusionCache. + +So when the structure of the cache entries need to change to evolve FusionCache, how can this be managed? + +Easy, by using an additional cache key modifier for the distributed cache, so that if and when the version of the cache entry structure changes, there will be no issues serializing or deserializing different versions of the saved data. + +In practice this means that, when saving something for the cache key `"foo"`, in reality in the distributed cache it will be saved with the cache key `"v0:foo"`. + +This has been planned from the beginning, and is the way to manage changes in the wire format used in the distributed cache between updates: it has been designed in this way specifically to support FusionCache to be updated safely and transparently, without interruptions or problems. + +So what happens when there are 2 version of FusionCache running on the same distributed cache instance, for example when two different apps share the same distributed cache and one is updated and the other is not? + +Since the old version will write to the distributed cache with a different cache key than the new version, this will not create conflicts during the update, and it means that we don't need to stop all the apps and services that works on it and wipe all the distributed cache data just to do the upgrade. + +At the same time though, if we have different apps and services that use the same distributed cache shared between them, we need to understand that by updating only one app or service and not the others will mean that the ones updated will read/write using the new distributed cache keys, while the non updated ones will keep read/write using the old distributed cache keys. + +Again, nothing catastrophic, but something to consider. + +## 💾 Disk Cache + +In certain situations we may like to have some of the benefits of a 2nd level like better cold starts (when the memory cache is initially empty) but at the same time we don't want to have a separate **actual** distributed cache to handle, or we simply cannot have it: a good example may be a mobile app, where everything should be self contained. + +In those situations we may want a distributed cache that is "not really distributed", something like an implementation of `IDistributedCache` that reads and writes directly to one or more local files. + +Is this possible? + +Yes, totally, and there's a [dedicated page](DiskCache.md) to learn more. + +## ↩️ Auto-Recovery + +Since the distributed cache is a distributed component (just like the backplane), most of the transient errors that may occur on it are also covered by the Auto-Recovery feature: you can read more on the related [docs page](AutoRecovery.md). + ## 📦 Packages There are a variety of already existing `IDistributedCache` implementations available, just pick one: @@ -40,6 +76,7 @@ There are a variety of already existing `IDistributedCache` implementations avai | [MarkCBB.Extensions.Caching.MongoDB](https://www.nuget.org/packages/MarkCBB.Extensions.Caching.MongoDB/) <br/> Another implementation for MongoDB | `Apache v2` | [](https://www.nuget.org/packages/MarkCBB.Extensions.Caching.MongoDB/) | | [EnyimMemcachedCore](https://www.nuget.org/packages/EnyimMemcachedCore/) <br/> An implementation for Memcached | `Apache v2` | [](https://www.nuget.org/packages/EnyimMemcachedCore/) | | [NeoSmart.Caching.Sqlite](https://www.nuget.org/packages/NeoSmart.Caching.Sqlite/) <br/> An implementation for SQLite | `MIT` | [](https://www.nuget.org/packages/NeoSmart.Caching.Sqlite/) | +| [AWS.AspNetCore.DistributedCacheProvider](https://www.nuget.org/packages/AWS.AspNetCore.DistributedCacheProvider/) <br/> An implementation for AWS DynamoDB | `Apache v2` | [](https://www.nuget.org/packages/AWS.AspNetCore.DistributedCacheProvider/) | | [Microsoft.Extensions.Caching.Memory](https://www.nuget.org/packages/Microsoft.Extensions.Caching.Memory/) <br/> An in-memory implementation | `MIT` | [](https://www.nuget.org/packages/Microsoft.Extensions.Caching.Memory/) | As for an implementation of `IFusionCacheSerializer`, pick one of these: @@ -54,7 +91,7 @@ As for an implementation of `IFusionCacheSerializer`, pick one of these: | [ZiggyCreatures.FusionCache.Serialization.ServiceStackJson](https://www.nuget.org/packages/ZiggyCreatures.FusionCache.Serialization.ServiceStackJson/) <br/> A serializer based on the [ServiceStack](https://servicestack.net/) JSON serializer | `MIT` | [](https://www.nuget.org/packages/ZiggyCreatures.FusionCache.Serialization.ServiceStackJson/) | -### Example +### 👩💻 Example As an example let's use FusionCache with [Redis](https://redis.io/) as a distributed cache and [Newtonsoft Json.NET](https://www.newtonsoft.com/json) as the serializer: @@ -64,7 +101,7 @@ PM> Install-Package ZiggyCreatures.FusionCache.Serialization.NewtonsoftJson PM> Install-Package Microsoft.Extensions.Caching.StackExchangeRedis ``` -Then, to create and setup the cache manually, do this: +Then, to create and setup the cache manually, we can do this: ```csharp // INSTANTIATE A REDIS DISTRIBUTED CACHE @@ -80,7 +117,7 @@ var cache = new FusionCache(new FusionCacheOptions()); cache.SetupDistributedCache(redis, serializer); ``` -If instead you prefer a **DI (Dependency Injection)** approach you can do this: +If instead we prefer a **DI (Dependency Injection)** approach, we should simply do this: ```csharp services.AddFusionCache() @@ -93,84 +130,4 @@ services.AddFusionCache() ; ``` -Easy peasy. - -## 🙋♀️ What about a disk cache? - -In certain situations we may like to have some of the benefits of a 2nd level like better cold starts (when the memory cache is initially empty) but at the same time we don't want to have a separate **actual** distributed cache to handle, or we simply cannot have it. A good example of that may be a mobile app, where everything should be self contained. - -In those situations we may want a distributed cache that is "not really distributed", something like an implementation of `IDistributedCache` that reads and writes directly to one or more local files: makes sense, right? - -Yes, kinda, but there is more to that. - -We should also think about the details, about all the things it should handle for a real-real world usage: -- have the ability to read and write data in a **persistent** way to local files (so the cached data will survive restarts) -- ability to prevent **data corruption** when writing to disk -- support some form of **compression**, to avoid wasting too much space on disk -- support **concurrent** access without deadlocks, starvations and whatnot -- be **fast** and **resource optimized**, so to consume as little cpu cycles and memory as possible -- and probably something more that I'm forgetting - -That's a lot to do... but wait a sec, isn't that exactly what a **database** is? - -Yes, yes it is! - -### An actual database? Are you kidding me? - -Of course I'm not suggesting to install (and manage) a local MySql/SqlServer/PostgreSQL instance or something, that would be hard to do in most cases, impossible in others and frankly overkill. - -So, what should we use? - -### SQLite to the rescue! - -If case you didn't know it yet, [SQLite](https://www.sqlite.org/) is an incredible piece of software: -- it's one of the highest quality software [ever produced](https://www.i-programmer.info/news/84-database/15609-in-praise-of-sqlite.html) -- it's used in production on [billions of devices](https://www.sqlite.org/mostdeployed.html), with a higher instance count than all the other database engines, combined -- it's [fully tested](https://www.sqlite.org/testing.html), with millions of test cases, 100% coverage, fuzz tests and more, way more (the link is a good read, I suggest to take a look at it) -- it's very robust and fully [transactional](https://www.sqlite.org/hirely.html), no worries about [data corruption](https://www.sqlite.org/transactional.html) -- it's fast, like [really really fast](https://www.sqlite.org/fasterthanfs.html). Like, 35% faster than direct file I/O! -- has a very [small footprint](https://www.sqlite.org/footprint.html) -- the [license](https://www.sqlite.org/copyright.html) is as free and open as it can get - -Ok so SQLite is the best, how can we use it as the 2nd level? - -### Ok but how? - -Luckily someone in the community created an implementation of `IDistributedCache` based on SQLite, and released it as the [NeoSmart.Caching.Sqlite](https://www.nuget.org/packages/NeoSmart.Caching.Sqlite/) Nuget package (GitHub repo [here](https://github.com/neosmart/AspSqliteCache)). - -The package: -- supports both the sync and async models natively, meaning it's not doing async-over-sync or vice versa, but a real double impl (like FusionCache does) which is very nice and will use the underlying system resources best -- uses a [pooling mechanism](https://github.com/neosmart/AspSqliteCache/blob/master/SqliteCache/DbCommandPool.cs) which means the memory allocation will be lower since they reuse existing objects instead of creating new ones every time and consequently, because of that, less cpu usage in the long run because less pressure on the GC (Garbage Collector) -- supports `CancellationToken`s, meaning that it will gracefully handle cancellations in case it's needed, like for example a mobile app pause/shutdown events or similar - -So we simply use the `SqliteCache` impl instead of the `RedisCache` we used above and we'll be good to go: - -```csharp -services.AddFusionCache() - .WithSerializer( - new FusionCacheNewtonsoftJsonSerializer() - ) - .WithDistributedCache( - new SqliteCache(new SqliteCacheOptions { CachePath = "CACHE PATH" }) - ) -; -``` - -Alternatively, we can register it as *THE* `IDistributedCache` implementation, and just tell FusionCache to use the registered one, whatever that may be: - -```csharp -// REGISTER SQLITE AS THE IDistributedCache IMPL -services.AddSqliteCache(options => { - options.CachePath = "CACHE PATH"; -}); - -services.AddFusionCache() - .WithSerializer( - new FusionCacheNewtonsoftJsonSerializer() - ) - // USE THE REGISTERED IDistributedCache IMPL - .WithRegisteredDistributedCache() -; -``` - -If you like what you are seeing, remember to give that [repo](https://www.nuget.org/packages/NeoSmart.Caching.Sqlite/) a star ⭐ and share it! +Easy peasy. \ No newline at end of file diff --git a/docs/CacheStampede.md b/docs/CacheStampede.md index 17bd42c1..4d70d98f 100644 --- a/docs/CacheStampede.md +++ b/docs/CacheStampede.md @@ -4,12 +4,10 @@ </div> -# :rocket: Cache Stampede prevention +# 🚀 Cache Stampede prevention A [Cache Stampede](https://en.wikipedia.org/wiki/Cache_stampede) is a typical failure you may encounter while using caching in a high load scenario, and basically it's what happens when a lot of requests comes in for the same data and there's no special handling of that. -## The Problem - Imagine multiple requets coming in, all for the same data. They would: - all go to the cache to check for the same data @@ -21,13 +19,11 @@ all of them at the same time, for the same data. Now imagine that scenario with 100 or 1.000 concurrent requests: that is both a waste of resources and something that may potentially tear down your database during peak traffic time. -## The Solution - FusionCache takes great care in coordinating the execution of concurrent factories for the same cache key, to avoid this type of failure altogether. Inside FusionCache a factory is just a function that you specify when using the main `GetOrSet[Async]` method: basically it's the way you specify **how to get a value** when it is not in the cache or is expired. -Here's an example: +### 👩💻 Example ```csharp var id = 42; @@ -39,9 +35,13 @@ var product = cache.GetOrSet<Product>( ); ``` +## 🔢 Request Coalescing + FusionCache will search for the value in the cache (*memory* and *distributed*, if available) and, if nothing is there, will call the factory to obtain the value: it then saves it into the cache with the specified options, and returns it to the caller, all transparently. -Special care is put into calling just one factory per key, concurrently: this means that if 10 (or 100, or more) concurrent requests for the same key arrive at the same time and the data is not there, **only one factory** will be executed, and the result will be stored and shared with all callers right away. +Special care is put into calling just one factory per key, concurrently, thanks to something known as *request coalescing*, which is a fancy way of saying that multiple identical requests at the same time will be reduced to just one request. + +This means that if 10 (or 100, or more) concurrent requests for the same cache key arrive at the same time and the data is not there, **only one factory** will be executed, and the result will be stored and shared with all callers right away.  @@ -50,10 +50,12 @@ As you can see, when multiple concurrent `GetOrSet[Async]` calls are made for th This ensures that the data source (let's say a database) **will not be overloaded** with multiple requests for the same piece of data at the same time. -## Multiple nodes +## 🔀 Multiple Nodes It's right to point out that this automatic coordination does not extend accross multiple nodes: what this means is that although there's a guarantee only 1 factory will be executed concurrently per-key in each node, if multiple requests for the same cache key arrive at the same time on different nodes, one factory per node will be executed. In practice this is typically not a problem, because for example `1.000` concurrent requests distributed on `5` nodes means, if you are not lucky, at most `5` requests to the database: the cache stampede problem is still solved, you may just have a couple more executions, but not a massive number. -On top of that, one way to mitigate this scenario is to enable jittering via the `JitterMaxDuration` [option](Options.md): basically it's a way to add a little extra random duration to the normal cache duration, so that the same piece of data will expire at a little bit different times on each node, allowing the first node where it will expire to refresh the data from the database and repopulate the distributed cache for everybody else to use right away, and not go to the database. \ No newline at end of file +Additionally, if we are using a distributed cache we should remember that as soon as a value will be returned from a factory it will also be saved to the distributed cache, ready to be used by the other nodes, avoiding the next database request at all. + +On top of that, one extra way to mitigate this scenario is to enable jittering via the `JitterMaxDuration` [option](Options.md): basically it's a way to add a little extra random duration to the normal cache duration, so that the same piece of data will expire at a little bit different times on each node, allowing the first node where it will expire to refresh the data from the database and repopulate the distributed cache for everybody else to use right away, and not go to the database. \ No newline at end of file diff --git a/docs/Comparison.md b/docs/Comparison.md index dbd84361..3d63826a 100644 --- a/docs/Comparison.md +++ b/docs/Comparison.md @@ -4,7 +4,7 @@ </div> -# :ab: Comparison +# 🆎 Comparison FusionCache of course is not the only player in the field of caching libraries. @@ -23,11 +23,11 @@ In the end, any library out there is the materialization of a lot of **passion** And we should all be thankful for that. -| :loudspeaker: A note to other library authors | +| 📢 A note to other library authors | | :--- | | Even though I tried my best to be fair and objective, I'm sure you may have different opinions about each topic or I may have just used your library in the wrong way or maybe a newer version came out with new features. <br/> <br/> If that is the case please [**open an issue**](https://github.com/ZiggyCreatures/FusionCache/issues/new/choose) or send a pr and I will make the necessary changes. | -## :ballot_box_with_check: Features +## ☑ Features Every library has a different set of features, mostly based on each library design: you may find logging support, both a sync and an async api, statistics, events, jittering, cache regions and a lot more so making a 1:1 comparison is hardly possible. @@ -44,13 +44,13 @@ The general features I've identified are: - [**Multi-provider**](CacheLevels.md): the abilty to use the same caching api towards different implementations (memory, Redis, MongoDb, etc) - [**Multi-level**](CacheLevels.md): the ability to handle more than one caching level, transparently. This can give you - at the same time - the benefits of a local in-memory cache (high performance + data locality) and the benefits of a distributed cache (sharing of cached data + better cold start) without having to handle them separately - [**Backplane**](Backplane.md): available with different names, it allows a change in a distributed cache to be reflected in the local memory cache -- [**Auto-Recovery**](Backplane.md): the ability to automatically recover from an out-of-sync scenario due to some transient problems to a globally synchronized state +- [**Auto-Recovery**](AutoRecovery.md): a way to automatically handle transient errors so that they can be solved, automatically, without doing anything - [**Events**](Events.md): the ability to be notified when certain events happen in the cache, useful to collect custom metrics, etc - [**Logging**](Logging.md): when things go bad you would like to have some help investigating what went wrong, and logging is key - **Portable**: the ability to run on both the older **.NET Framework** (full fx) and the new **.NET Core**. As time goes by .NET Core (from v5 now simply **.NET**) is the platform to be on, but it's a nice plus to be able to run on the older one as well - **Tests**: having a suite of tests covering most of the library can greatly reduce the probabilty of bugs or regressions so, in theory, you can count on a more solid and stable library - [**Xml Comments**](https://docs.microsoft.com/en-us/dotnet/csharp/codedoc): having informations always available at your fingertips while you type (Intellisense :tm: or similar) is fundamental for learning as you code and to avoid common pitfalls -- **Docs**: an expanded documentation, a getting started guide or maybe some samples can greatly improve your learning +- [**Docs**](docs/README.md): an expanded documentation, a getting started guide or maybe some samples can greatly improve your learning - [**License**](../LICENSE.md): important to know what are your rights and obligations This is how they compare: @@ -74,9 +74,8 @@ This is how they compare: | **Tests** | ✔ | ✔ | ✔ | ✔ | ✔ | | **Xml Comments** | ✔ | ✔ | ✔ | ✔ | ❌ | | **Docs** | ✔ | ✔ | ✔ | ✔ | ✔ | -| **License** | `MIT` | `Apache 2.0` | `MIT` | `MIT` | `MIT` | +| **License** | `MIT` | `Apache 2.0` | `MIT` | `MIT` | `MIT` | -:information_source: **NOTES** +ℹ **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. - (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 bd473660..8da26b82 100644 --- a/docs/CoreMethods.md +++ b/docs/CoreMethods.md @@ -4,7 +4,7 @@ </div> -# :joystick: Core Methods +# 🕹️ Core Methods At a high level there are 6 core methods: @@ -229,7 +229,7 @@ foo = cache.GetOrDefault<int>("foo"); foo = cache.GetOrDefault<int>("foo", opt => opt.SetFailSafe(true)); ``` -## :recycle: Common overloads +## ♻️ Common overloads Every core method that needs a set of options (`FusionCacheEntryOptions`) for how to behave has different overloads to let you specify these options, for better ease of use. diff --git a/docs/DependencyInjection.md b/docs/DependencyInjection.md index 7e725ed4..05071398 100644 --- a/docs/DependencyInjection.md +++ b/docs/DependencyInjection.md @@ -10,7 +10,7 @@ In .NET there's full support for [Dependency Injection (DI)](https://docs.micros This is a common way to handle creation, dependencies, scopes and disposal of resources that makes it easier and more flexible to work with any _service_ we may need. -| 🙋♂️ Updating from before `v0.20.0` ? please [read here](Update_v0_20_0.md). | +| 🙋♂️ Updating from before `v0.24.0` ? please [read here](Update_v0_24_0.md). | |:-------| ## FusionCache + DI @@ -72,7 +72,7 @@ To configure some cache-wide options we can use: services.AddFusionCache() .WithOptions(opt => { - opt.BackplaneAutoRecoveryMaxItems = 123; + opt.AutoRecoveryMaxItems = 123; }) ; ``` @@ -95,7 +95,7 @@ Of course we can combine them (remember? fluent interface!): services.AddFusionCache() .WithOptions(opt => { - opt.BackplaneAutoRecoveryMaxItems = 123; + opt.AutoRecoveryMaxItems = 123; }) .WithDefaultEntryOptions(opt => { diff --git a/docs/DiskCache.md b/docs/DiskCache.md new file mode 100644 index 00000000..bf0cb7b7 --- /dev/null +++ b/docs/DiskCache.md @@ -0,0 +1,89 @@ +<div align="center"> + + + +</div> + +# 💾 Disk Cache + +In certain situations we may like to have some of the benefits of a 2nd level like better cold starts (when the memory cache is initially empty) but at the same time we don't want to have a separate **actual** distributed cache to handle, or we simply cannot have it. A good example of that may be a mobile app, where everything should be self contained. + +In those situations we may want a distributed cache that is "not really distributed", something like an implementation of `IDistributedCache` that reads and writes directly to one or more local files: makes sense, right? + +Yes, kinda, but there is more to that. + +We should also think about the details, about all the things it should handle for a real-real world usage: +- have the ability to read and write data in a **persistent** way to local files (so the cached data will survive restarts) +- ability to prevent **data corruption** when writing to disk +- support some form of **compression**, to avoid wasting too much space on disk +- support **concurrent** access without deadlocks, starvations and whatnot +- be **fast** and **resource optimized**, so to consume as little cpu cycles and memory as possible +- and probably something more that I'm forgetting + +That's a lot to do... but wait a sec, isn't that exactly what a **database** is? + +Yes, yes it is! + +## 😐 An actual database? Are you kidding me? + +Of course I'm not suggesting to install (and manage) a local MySql/SqlServer/PostgreSQL instance or similar, that would be hard to do in most cases, impossible in others and frankly overkill. + +So, what should we use? + +## 💪 SQLite to the rescue! + +If case you didn't know it yet, [SQLite](https://www.sqlite.org/) is an incredible piece of software: +- it's one of the highest quality software [ever produced](https://www.i-programmer.info/news/84-database/15609-in-praise-of-sqlite.html) +- it's used in production on [billions of devices](https://www.sqlite.org/mostdeployed.html), with a higher instance count than all the other database engines, combined +- it's [fully tested](https://www.sqlite.org/testing.html), with millions of test cases, 100% coverage, fuzz tests and more, way more (the link is a good read, I suggest to take a look at it) +- it's very robust and fully [transactional](https://www.sqlite.org/hirely.html), no worries about [data corruption](https://www.sqlite.org/transactional.html) +- it's fast, like [really really fast](https://www.sqlite.org/fasterthanfs.html). Like, 35% faster than direct file I/O! +- has a very [small footprint](https://www.sqlite.org/footprint.html) +- the [license](https://www.sqlite.org/copyright.html) is as free and open as it can get + +Ok so SQLite is the best, how can we use it as the 2nd level? + +## 👩🏫 Ok but how? + +Luckily someone in the community created an implementation of `IDistributedCache` based on SQLite, and released it as the [NeoSmart.Caching.Sqlite](https://www.nuget.org/packages/NeoSmart.Caching.Sqlite/) Nuget package (GitHub repo [here](https://github.com/neosmart/AspSqliteCache)). + +The package: +- supports both the sync and async models natively, meaning it's not doing async-over-sync or vice versa, but a real double impl (like FusionCache does) which is very nice and will use the underlying system resources best +- uses a [pooling mechanism](https://github.com/neosmart/AspSqliteCache/blob/master/SqliteCache/DbCommandPool.cs) which means the memory allocation will be lower since they reuse existing objects instead of creating new ones every time and consequently, because of that, less cpu usage in the long run because less pressure on the GC (Garbage Collector) +- supports `CancellationToken`s, meaning that it will gracefully handle cancellations in case it's needed, like for example a mobile app pause/shutdown events or similar + +It's a really good package, let's see how to use it. + +### 👩💻 Example + +We simply use the `SqliteCache` implementation and we'll be good to go: + +```csharp +services.AddFusionCache() + .WithSerializer( + new FusionCacheNewtonsoftJsonSerializer() + ) + .WithDistributedCache( + new SqliteCache(new SqliteCacheOptions { CachePath = "CACHE PATH" }) + ) +; +``` + +Alternatively, we can register it as *THE* `IDistributedCache` implementation, and just tell FusionCache to use the registered one, whatever that may be: + +```csharp +// REGISTER SQLITE AS THE IDistributedCache IMPL +services.AddSqliteCache(options => { + options.CachePath = "CACHE PATH"; +}); + +services.AddFusionCache() + .WithSerializer( + new FusionCacheNewtonsoftJsonSerializer() + ) + // USE THE REGISTERED IDistributedCache IMPL + .WithRegisteredDistributedCache() +; +``` + +If you like what you are seeing, remember to give that [repo](https://www.nuget.org/packages/NeoSmart.Caching.Sqlite/) a star ⭐ and share it! diff --git a/docs/Events.md b/docs/Events.md index bcc61a1a..b412f826 100644 --- a/docs/Events.md +++ b/docs/Events.md @@ -4,7 +4,7 @@ </div> -# :telephone_receiver: Events +# 📞 Events FusionCache has a comprehensive set of events you can subscribe to, so you can be notified of core events when they happen. @@ -62,7 +62,7 @@ Here's a non comprehensive list of the available events: 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. -## :gear: On high-level and low-level events +## ⚙️ On high-level and low-level events One thing to consider is that subscribing to high level events (via `cache.Events`) should give you the expected results, whereas using directly events from lower levels (via `cache.Events.Memory` or `cache.Events.Distributed`) may surprise you, at first. @@ -84,14 +84,12 @@ As you can see lower level events may delve into the internals of how FusionCach Also note that newer locking implementations may be possible in the future (I'm actually trying them out) so lower level events may even change one day. -## :construction_worker: Safe execution +## 👷 Safe execution Since an event handler is a normal piece of code that FusionCache runs at a certain point in time, with no special care taken a bad event handler may generate errors or slow everything down. -Thankfully FusionCache takes this into consideration and executes the event handlers in a safe way: each handler is run separately, on a different thread and is guarded against unhandled exceptions (and in case one is thrown you'll find that in the log for later detective work). +Thankfully FusionCache takes this into consideration and executes the event handlers in a safe way: each handler is run separately and is guarded against unhandled exceptions (and in case one is thrown you'll find that in the log for later detective work). -All of this is done to avoid one bad handler from slowing down subsequent handlers and FusionCache itself. - -:bulb: Because of these design decisions, **by default** the order in which the handlers are executed is not guaranteed and it is not possible to know when they will finish running: this should not be a problem, but is good to know. +Also, by default, the event handlers are run in a background thread so not to slow down FusionCache and, in turn, your application. | :warning: WARNING | |:------------------| diff --git a/docs/FactoryOptimization.md b/docs/FactoryOptimization.md index 0ad45033..6d4e9a39 100644 --- a/docs/FactoryOptimization.md +++ b/docs/FactoryOptimization.md @@ -4,6 +4,6 @@ </div> -# :rocket: Cache Stampede prevention +# 🚀 Cache Stampede prevention This content has been moved to the [CacheStampede](CacheStampede.md) page. \ No newline at end of file diff --git a/docs/FailSafe.md b/docs/FailSafe.md index b5e42536..138babac 100644 --- a/docs/FailSafe.md +++ b/docs/FailSafe.md @@ -4,7 +4,7 @@ </div> -# :bomb: Fail-Safe +# 💣 Fail-Safe Using a cache in general - not necessarily FusionCache - is a good thing because it makes our systems **way faster**, even though it means using values that may be **a little bit stale**. diff --git a/docs/Logging.md b/docs/Logging.md index f0c2b88d..66898dcf 100644 --- a/docs/Logging.md +++ b/docs/Logging.md @@ -155,7 +155,7 @@ For example: We can see all of them [here](Options.md) in the Options docs. -### Example +### 👩💻 Example Let's say we want to know about all the problems related to calling the database in our factory calls, except for when synthetic timeouts occur (eg: because we set a soft timeout to a very low `10 ms` value, so it will be hit frequently). Also suppose we set our configuration with a min level of something like `Information` or `Warning`. diff --git a/docs/Options.md b/docs/Options.md index 3ebd101f..9e3c8211 100644 --- a/docs/Options.md +++ b/docs/Options.md @@ -4,11 +4,14 @@ </div> -# :level_slider: Options +# 🎚 Options Even if FusionCache typically *just works* by default, it may be important to fine tune the available options to better suite your needs, and maybe save some memory allocations, too. -**:bulb: NOTE**: all of this information is fully available via Intellisense, thanks to the embedded code comments. +| **💡 NOTE** | +|:----------------| +| all these informations are fully available via IntelliSense, auto-suggest or similar technologies, thanks to the embedded code comments. | + ## FusionCacheOptions @@ -57,11 +60,12 @@ In general this can be used as a set of options that will act as the *baseline*, | `DistributedCacheKeyModifierMode` | `CacheKeyModifierMode` | `Prefix` | Specify the mode in which cache key will be changed for the distributed cache (eg: to specify the wire format version). | | `BackplaneCircuitBreakerDuration` | `TimeSpan` | `none` | The duration of the circuit-breaker used when working with the backplane. | | `BackplaneChannelPrefix` | `string?` | `null` | The prefix to use in the backplane channel name: if not specified the `CacheName` will be used. | -| `EnableBackplaneAutoRecovery` | `bool` | `true` | Enable auto-recovery for the backplane notifications to better handle transient errors without generating synchronization issues: notifications that failed to be sent out will be retried later on, when the backplane becomes responsive again. | -| `BackplaneAutoRecoveryMaxItems` | `int?` | `null` | The maximum number of items in the auto-recovery queue: this can help reducing memory consumption. If set to `null` there will be no limit. | -| `BackplaneAutoRecoveryReconnectDelay` | `TimeSpan` | `2s` | The amount of time to wait, after a backplane reconnection, before trying to process the auto-recovery queue: this may be useful to allow all the other nodes to be ready. | -| `EnableDistributedExpireOnBackplaneAutoRecovery`| `bool` | `true` | Enable expiring a cache entry, only on the distributed cache (if any), when anauto-recovery message is being published on the backplane, to ensure that the value in the distributed cache will not be stale. | +| `EnableAutoRecovery` | `bool` | `true` | Enable auto-recovery for the backplane notifications to better handle transient errors without generating synchronization issues: notifications that failed to be sent out will be retried later on, when the backplane becomes responsive again. | +| `AutoRecoveryMaxItems` | `int?` | `null` | The maximum number of items in the auto-recovery queue: this is usually not needed, but it may help reducing memory consumption in extreme scenarios. | +| `AutoRecoveryDelay` | `TimeSpan` | `2s` | The amount of time to wait before actually processing the auto-recovery queue, to better handle backpressure. | +| `AutoRecoveryMaxRetryCount` | `int?` | `null` | The maximum number of retries for a auto-recovery item: after this amount an item is discarded, to avoid keeping it for too long. Please note though that a cleanup is automatically performed, so in theory there's no need to set this. | | `EnableSyncEventHandlersExecution` | `bool` | `false` | If set to `true` all registered event handlers will be run synchronously: this is really, very, highly discouraged as it may slow down all other handlers and FusionCache itself. | +| `ReThrowOriginalExceptions` | `bool` | `false` | 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. | | `IncoherentOptionsNormalizationLogLevel` | `LogLevel` | `Warning` | Used when some options have incoherent values that have been fixed with a normalization, like for example when a FailSafeMaxDuration is lower than a Duration, so the Duration is used instead. | | `SerializationErrorsLogLevel` | `LogLevel` | `Error` | Used when logging serialization errors (while working with the distributed cache). | | `DistributedCacheSyntheticTimeoutsLogLevel` | `LogLevel` | `Warning` | Used when logging synthetic timeouts (both soft/hard) while using the distributed cache. | @@ -110,6 +114,7 @@ For a better **developer experience** and to **consume less memory** (higher per | 🧙 `ReThrowSerializationExceptions` | `bool` | `true` | Set this to false to disable the bubble up of serialization exceptions (default is `true`). | | 🧙♂️ `SkipBackplaneNotifications` | `bool` | `false` | Skip sending backplane notifications after some operations, like a SET (via a Set/GetOrSet call) or a REMOVE (via a Remove call). | | 🧙♂️ `AllowBackgroundBackplaneOperations` | `bool` | `true` | By default every operation on the backplane is non-blocking: that is to say the FusionCache method call would not wait for each backplane operation to be completed. Setting this flag to `false` will execute these operations in a blocking fashion, typically resulting in worse performance. | +| 🧙♂️ `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. | diff --git a/docs/PluginSample.md b/docs/PluginSample.md index 79563480..b5d2740b 100644 --- a/docs/PluginSample.md +++ b/docs/PluginSample.md @@ -4,7 +4,7 @@ </div> -# :jigsaw: A plugin sample +# 🧩 A plugin sample Let's say we want to create a [plugin](Plugins.md) that sends an email when a fail-safe activation happens. diff --git a/docs/Plugins.md b/docs/Plugins.md index ac200eaf..651f8853 100644 --- a/docs/Plugins.md +++ b/docs/Plugins.md @@ -4,7 +4,7 @@ </div> -# :jigsaw: Plugins +# 🧩 Plugins FusionCache supports extensibility via plugins: it is possible for example to listen to [events](Events.md) and react in any way you want. @@ -13,7 +13,7 @@ In time, the most useful plugins will be listed directly in the homepage. ## How to create a plugin -Simply write a class that implements the [`IFusionCachePlugin`](https://github.com/jodydonetti/ZiggyCreatures.FusionCache/blob/main/src/ZiggyCreatures.FusionCache/Plugins/IFusionCachePlugin.cs) interface, which basically boils down to implement just the `Start` and `Stop` methods: from there you can setup your custom logic and do your thing. +Simply write a class that implements the [`IFusionCachePlugin`](https://github.com/ZiggyCreatures/FusionCache/blob/main/src/ZiggyCreatures.FusionCache/Plugins/IFusionCachePlugin.cs) interface, which basically boils down to implement just the `Start` and `Stop` methods: from there you can setup your custom logic and do your thing. Of course it can also accept its own set of options, typically modelled via `IOptions<MyPluginType>` and friends. diff --git a/docs/README.md b/docs/README.md index 962bf983..a97a1530 100644 --- a/docs/README.md +++ b/docs/README.md @@ -5,42 +5,39 @@ </div> -# :book: Documentation +# 📕 Documentation -Sometimes topics can be explained a little bit more. +Sometimes topics can be explained a little bit more, and the following docs can help you with that. - -### [**🦄 A Gentle Introduction**](AGentleIntroduction.md) +## [**🦄 A Gentle Introduction**](AGentleIntroduction.md) What you need to know first, to make yourself comfortable with FusionCache. +## [**👩🏫 Step By Step**](StepByStep.md) -### Features +A complete step by step example of why a cache is useful, why FusionCache could be even more so, how to apply most of the options available and what results you can expect to obtain. -A deeper description of the main features: -- [**🔀 Cache Levels**](CacheLevels.md): a bried description of the 2 available caching levels and how to setup them -- [**📢 Backplane**](Backplane.md): how to get an always synchronized cache, even in a multi-node scenario -- [**🚀 Cache Stampede prevention**](CacheStampede.md): no more overloads during a cold start or after an expiration -- [**💣 Fail-Safe**](FailSafe.md): an explanation of how the fail-safe mechanism works -- [**⏱ Timeouts**](Timeouts.md): the various types of timeouts at your disposal (calling a factory, using the distributed cache, etc) -- [**🔃 Dependency Injection**](DependencyInjection.md): how to work with FusionCache + DI in .NET -- [**📛 Named Caches**](NamedCaches.md): how to work with multiple named FusionCache instances -- [**🧙♂️ Adaptive Caching**](AdaptiveCaching.md): how to adapt cache duration (and more) based on the object being cached itself -- [**🔂 Conditional Refresh**](ConditionalRefresh.md): how to save resources when the remote data is not changed -- [**🦅 Eager Refresh**](EagerRefresh.md): how to start a background refresh eagerly, before the expiration occurs -- [**🎚 Options**](Options.md): everything about the available options, both cache-wide and per-call -- [**🕹 Core Methods**](CoreMethods.md): what you need to know about the core methods available -- [**📞 Events**](Events.md): the events hub and how to use it -- [**🧩 Plugins**](Plugins.md): how to create and use plugins -- [**📜 Logging**](Logging.md): logging configuration and usage - - -### [**👩🏫 Step By Step**](StepByStep.md) +## [**🆎 Comparison**](Comparison.md) -A complete step by step example of why a cache is useful, why FusionCache could be even more so, how to apply most of the options available and what results you can expect to obtain. +A feature comparison between existing .NET caching solutions, to help you choose which one to use. +## 📖 Features -### [**🆎 Comparison**](Comparison.md) +A deeper description of the main features: -A feature comparison between existing .NET caching solutions, to help you choose which one to use. +- [**🚀 Cache Stampede prevention**](CacheStampede.md): automatic protection from the Cache Stampede problem +- [**🔀 Optional 2nd level**](CacheLevels.md): an optional 2nd level handled transparently, with any implementation of `IDistributedCache` +- [**💣 Fail-Safe**](FailSafe.md): a mechanism to avoids transient failures, by reusing an expired entry as a temporary fallback +- [**⏱ Soft/Hard timeouts**](Timeouts.md): a slow factory (or distributed cache) will not slow down your application, and no data will be wasted +- [**📢 Backplane**](Backplane.md): in a multi-node scenario, it can notify the other nodes about changes in the cache, so all will be in-sync +- [**↩️ Auto-Recovery**](AutoRecovery.md): automatic handling of transient issues with retries and sync logic +- [**🧙♂️ Adaptive Caching**](AdaptiveCaching.md): for when you don't know upfront the cache duration, as it depends on the value being cached itself +- [**🔂 Conditional Refresh**](ConditionalRefresh.md): like HTTP Conditional Requests, but for caching +- [**🦅 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 +- [**💫 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/Simulator.md b/docs/Simulator.md new file mode 100644 index 00000000..40a7d34b --- /dev/null +++ b/docs/Simulator.md @@ -0,0 +1,54 @@ +<div align="center"> + + + +</div> + +# 🖥️ Simulator + +In general it is quite complicated to understand what is going on in a distributed system. + +When using FusionCache with the [distributed cache](CacheLevels.md) and the [backplane](Backplane.md), a lot of stuff is going on at any given time: add to that intermittent transient errors and, even if we can be confident [auto-recovery](AutoRecovery.md) will handle it all automatically, clearly **seeing** the whole picture can become a daunting task. + +It would be very useful to have *something* that let us clearly *see* it all in action, something that would let us configure different components, tweak some options, enable this, disable that and let us *simulate* a realistic workload to see the results. + +Luckily there is, and is called **Simulator**. + +Here's a video showcasing it (and some auto-recovery, too): + +[](https://youtu.be/6jGX6ePgD3Q) + +## 🛝 Play With It + +Of course, we can just clone the repo and play with it ourselves! + +After getting the FusionCache source code by cloning the repo we can look for a project called, well, Simulator and run it: and here we go simulating away all the crazy scenarios we can think of. + +A varied sequence of updates on a multi-cluster and multi-node situation? Sure, why not! + +Remove the distributed cache and just use the backplane? Yep, that's easy. + +Memory only, with no distributed cache and no backplane? Up to you. + +Simulate some broken distributed cache mixed with a flickering backplane and some high frequency random updates to see if auto-recovery holds up? No problem. + +## 👩🏫 How It Works + +After launching the project we just have to specify what we want to simulate: how many clusters, how many nodes per each cluster and if we want [fail-safe](FailSafe.md) enabled or not. + +Then the simulated clusters will be created and, for each cluster, the nodes will also be created, then a simulated distributed cache (if you want) and a simulated backplane (again, if you want) per each cluster, so that they'll be used by all the nodes of each cluster to share data and talk to each others. + +A nice dashboard will then be shown where in general the colors mean: +- 🟩 GREEN: it means the node/cluster is **synchronized** +- 🟥 RED: it means the node/cluster is **out of sync** + +Finally we'll have some shortcuts available to us to do stuff, like: +- `0`: enable/disable periodic updates on a random node on a random cluster +- `1-N`: update a random node on the cluster of your choosing, up to N (the number of cluster you specified) +- `D/d`: enable/disable the simulated distributed cache (to simulate a transient failure) +- `B/b`: enable/disable the simulated backplane (to simulate a transient failure) +- `S/s`: enable/disable the simulated database (to simulate a transient failure) +- `Q/q`: quit + +## ⭐ Credit Where Credit Is Due +One final thing: the Simulator is built using the wonderful [Spectre.Console](https://spectreconsole.net/) library by Patrik Svensson, Phil Scott, Nils Andresen and other contributors: please take a look at the [repo](https://github.com/spectreconsole/spectre.console) and give it a star! \ No newline at end of file diff --git a/docs/StepByStep.md b/docs/StepByStep.md index 09d2bfa4..20507d57 100644 --- a/docs/StepByStep.md +++ b/docs/StepByStep.md @@ -10,7 +10,7 @@ What follows is an example scenario on which we can reason about: we've built a The hypothetical infrastructure involved is somewhat bad on purpose, and is used just to illustrate some points like why a cache is useful in general, what FusionCache in particular can do, and also to have some nice round numbers to play with. -| 🙋♂️ Updating from before `v0.20.0` ? please [read here](Update_v0_20_0.md). | +| 🙋♂️ Updating from before `v0.24.0` ? please [read here](Update_v0_24_0.md). | |:-------| <br/> diff --git a/docs/Timeouts.md b/docs/Timeouts.md index 587e822c..c8f595f3 100644 --- a/docs/Timeouts.md +++ b/docs/Timeouts.md @@ -4,11 +4,13 @@ </div> -# :stopwatch: Timeouts +# ⏱️ Timeouts -There are different types of timeouts available and it may be useful to know them. +There are different types of timeouts available and it may be useful to know them: +- Factory Timeouts +- Distributed Cache Timeouts -:bulb: For a complete example of how to use them and what results you can achieve there's the [👩🏫 Step By Step](StepByStep.md) guide. +💡 For a complete example of how to use them and what results you can achieve there's the [👩🏫 Step By Step](StepByStep.md) guide. ## Factory Timeouts Sometimes your data source (database, webservice, etc) is overloaded, the network is congested or something else bad is happening and the end result is things start to get **:snail: very slow** to get a fresh piece of data. @@ -27,7 +29,7 @@ You can specify them both (the **soft** should be lower than the **hard**, of co In both cases it is possible to set the bool flag `AllowTimedOutFactoryBackgroundCompletion`: it is enabled *by default*, so you don't have to do anything, and it lets the timed-out factory keep running in the background and update the cached value as soon as it finishes. This will give you the best of both worlds: a **fast response** and **fresh data** as soon as possible. -### :bulb: Example +### 👩💻 Example As an example let's say we have a piece of code like this: ```csharp @@ -69,7 +71,7 @@ Now after `100 ms` the factory will timeout and FusionCache will **temporarily** Also, it will complete the factory execution **in the background**: as soon as it will complete, the cached value **will be updated** so that any new request will have the fresh value ready to be used. -## Distributed Cache Timeouts +## ⏱️ Distributed Cache Timeouts When using a distributed cache it is also possible to observe some slowdowns in case of network congestions or something else. diff --git a/docs/Update_v0_20_0.md b/docs/Update_v0_20_0.md index bcdb0f7d..5d523795 100644 --- a/docs/Update_v0_20_0.md +++ b/docs/Update_v0_20_0.md @@ -6,7 +6,7 @@ # 🆙 Update to v0.20.0 -With the `v0.20.0` release, FusionCache introduced an important feature: [Builder](DependencyInjection.md) support. +With the [v0.20.0](https://github.com/ZiggyCreatures/FusionCache/releases/tag/v0.20.0) release, FusionCache introduced an important feature: [Builder](DependencyInjection.md) support. This has made the experience of using FusionCache much easier and clearer, allowing us to be more precise in what we want to do and also allowing us to work with multiple [Named Caches](NamedCaches.md), also introduced in `v0.20.0`. @@ -49,7 +49,7 @@ This could have been used like this: ```csharp services.AddFusionCache(opt => { - opt.BackplaneAutoRecoveryMaxItems = 123; + opt.AutoRecoveryMaxItems = 123; }); ``` @@ -115,7 +115,7 @@ services.AddFusionCache() .TryWithAutoSetup() .WithOptions(opt => { - opt.BackplaneAutoRecoveryMaxItems = 123; + opt.AutoRecoveryMaxItems = 123; }) .WithDefaultEntryOptions(opt => { diff --git a/docs/Update_v0_24_0.md b/docs/Update_v0_24_0.md new file mode 100644 index 00000000..f21d4966 --- /dev/null +++ b/docs/Update_v0_24_0.md @@ -0,0 +1,29 @@ +<div align="center"> + + + +</div> + +# 🆙 Update to v0.24.0 + +With the [v0.24.0](https://github.com/ZiggyCreatures/FusionCache/releases/tag/v0.24.0) release, the feature known as *Backplane Auto-Recovery* has become, simply, *Auto-Recovery*. + +To do all of this I basically rewrote the entire part that handles distributed operations, meaning the handling of distributed cache operations and backplane operations so that they now work together in a more harmonious way, allowing the interaction between the two parts to be more in unison. + +So, a couple of breaking changes were needed: in a lot of scenarios this will not affect an update to this new version, but it's better to know what will happen. + +## ⚠ Breaking Change + +First thing: if your app or service does not use the distributed cache or the backplane, the update will be totally seamless. + +If instead you are using them, you need to know that the internal structure of the cache entries for the distributed cache is slightly changed: because of this the so called internal "wire format cache key modifier" that is used when pre-processing the cache key for the distributed cache, via prefix/suffix, had to be changed. The same thing is true for the backplane, where a wire format modifier is used to build the channel name. + +To read more about "Wire Format Versioning" there are chapters in the respective docs for the [distributed cache](CacheLevels.md) and for the [backplane](Backplane.md). + +Moral of the story: when updating to `v0.24.0` the suggestion is to update all the apps and services that use same distributed cache. + +The other main thing is that, as said, the auto-recovery feature changed from being "Backplane Auto-Recovery" to just "Auto-Recovery": this means that existing options like `EnableBackplaneAutoRecovery` is now simply `EnableAutoRecovery`, `BackplaneAutoRecoveryMaxItems` is now simply `AutoRecoveryMaxItems` and so on. + +Don't worry though, all the old ones are still there, they still works as before (by acting as an adapter from/to the new ones) and are marked with the `[Obsolete]` attribute with useful instructions on what to do. + +Basically after you update just recompile your project, look for a couple of warnings, change the property names and... that's it, very easy. \ No newline at end of file diff --git a/src/ZiggyCreatures.FusionCache.Backplane.Memory/MemoryBackplane.cs b/src/ZiggyCreatures.FusionCache.Backplane.Memory/MemoryBackplane.cs index 15491840..db463a9f 100644 --- a/src/ZiggyCreatures.FusionCache.Backplane.Memory/MemoryBackplane.cs +++ b/src/ZiggyCreatures.FusionCache.Backplane.Memory/MemoryBackplane.cs @@ -23,8 +23,8 @@ public class MemoryBackplane private ConcurrentDictionary<string, List<MemoryBackplane>>? _connection; - private string _channelName = "FusionCache.Notifications"; - private List<MemoryBackplane>? _subscriber; + private string? _channelName = null; + private List<MemoryBackplane>? _subscribers; private Action<BackplaneConnectionInfo>? _connectHandler; private Action<BackplaneMessage>? _incomingMessageHandler; @@ -58,7 +58,7 @@ public MemoryBackplane(IOptions<MemoryBackplaneOptions> optionsAccessor, ILogger if (_options.ConnectionId is null) { if (_logger?.IsEnabled(LogLevel.Warning) ?? false) - _logger.Log(LogLevel.Warning, "FUSION: A MemoryBackplane should be used with an explicit ConnectionId option, otherwise concurrency issues will probably happen"); + _logger.Log(LogLevel.Warning, "FUSION: BACKPLANE - A MemoryBackplane should be used with an explicit ConnectionId option, otherwise concurrency issues will probably happen"); _options.ConnectionId = "_default"; } @@ -69,17 +69,30 @@ private void EnsureConnection() if (_options.ConnectionId is null) throw new NullReferenceException("The ConnectionId is null"); - _connection = _connections.GetOrAdd(_options.ConnectionId, _ => new ConcurrentDictionary<string, List<MemoryBackplane>>()); - - _connectHandler?.Invoke(new BackplaneConnectionInfo(false)); + if (_connection is null) + { + _connection = _connections.GetOrAdd(_options.ConnectionId, _ => new ConcurrentDictionary<string, List<MemoryBackplane>>()); + _connectHandler?.Invoke(new BackplaneConnectionInfo(false)); + } - EnsureSubscriber(); + EnsureSubscribers(); } - private void EnsureSubscriber() + private void EnsureSubscribers() { - if (_subscriber is null && _connection is not null) - _subscriber = _connection.GetOrAdd(_channelName, _ => new List<MemoryBackplane>()); + if (_connection is null) + throw new InvalidOperationException("No connection available"); + + if (_subscribers is null) + { + lock (_connection) + { + if (_channelName is null) + throw new NullReferenceException("The backplane channel name is null"); + + _subscribers = _connection.GetOrAdd(_channelName, _ => new List<MemoryBackplane>()); + } + } } private void Disconnect() @@ -117,26 +130,44 @@ public void Subscribe(BackplaneSubscriptionOptions subscriptionOptions) // CONNECTION EnsureConnection(); - if (_subscriber is null) - throw new NullReferenceException("The subscriber is null"); + if (_subscribers is null) + throw new InvalidOperationException("The subscriber is null"); - lock (_subscriber) + lock (_subscribers) { - _subscriber.Add(this); + if (_logger?.IsEnabled(LogLevel.Trace) ?? false) + _logger.Log(LogLevel.Trace, "FUSION: BACKPLANE - before subscribing (Subscribers: {SubscribersCount})", _subscribers.Count); + + _subscribers.Add(this); + + if (_logger?.IsEnabled(LogLevel.Trace) ?? false) + _logger.Log(LogLevel.Trace, "FUSION: BACKPLANE - after subscribing (Subscribers: {SubscribersCount})", _subscribers.Count); } } /// <inheritdoc/> public void Unsubscribe() { - if (_subscriber is not null) + if (_subscribers is not null) { _incomingMessageHandler = null; - lock (_subscriber) + lock (_subscribers) { - _subscriber.Remove(this); + if (_logger?.IsEnabled(LogLevel.Trace) ?? false) + _logger.Log(LogLevel.Trace, "FUSION: BACKPLANE - before unsubscribing (Subscribers: {SubscribersCount})", _subscribers.Count); + + var removed = _subscribers.Remove(this); + + if (_logger?.IsEnabled(LogLevel.Trace) ?? false) + _logger.Log(LogLevel.Trace, "FUSION: BACKPLANE - after unsubscribing (Subscribers: {SubscribersCount}, Removed: {Removed}", _subscribers.Count, removed); + _subscriptionOptions = null; + _channelName = null; + + _incomingMessageHandler = null; + _connectHandler = null; } + _subscribers = null; } Disconnect(); @@ -160,22 +191,49 @@ public void Publish(BackplaneMessage message, FusionCacheEntryOptions options, C if (message.IsValid() == false) throw new InvalidOperationException("The message is invalid"); - if (_subscriber is null) + if (_subscribers is null) throw new NullReferenceException("Something went wrong :-|"); - foreach (var backplane in _subscriber) + if (_logger?.IsEnabled(LogLevel.Trace) ?? false) + _logger.Log(LogLevel.Trace, "FUSION: BACKPLANE - about to send a backplane notification to {BackplanesCount} backplanes (including self)", _subscribers.Count); + + foreach (var backplane in _subscribers) { token.ThrowIfCancellationRequested(); if (backplane == this) continue; - backplane.OnMessage(message); + try + { + if (_logger?.IsEnabled(LogLevel.Trace) ?? false) + _logger.Log(LogLevel.Trace, "FUSION: BACKPLANE - before sending a backplane notification to {BackplaneChannel}", backplane._channelName); + + var payload = BackplaneMessage.ToByteArray(message); + + backplane.OnMessage(payload); + + if (_logger?.IsEnabled(LogLevel.Trace) ?? false) + _logger.Log(LogLevel.Trace, "FUSION: BACKPLANE - after sending a backplane notification to {BackplaneChannel}", backplane._channelName); + } + catch + { + if (_logger?.IsEnabled(LogLevel.Error) ?? false) + _logger.Log(LogLevel.Error, "FUSION: BACKPLANE - An error occurred while publishing a message to a subscriber"); + } } } - internal void OnMessage(BackplaneMessage message) + internal void OnMessage(byte[] payload) { + var message = BackplaneMessage.FromByteArray(payload); + + if (_logger?.IsEnabled(LogLevel.Trace) ?? false) + _logger.Log(LogLevel.Trace, "FUSION: BACKPLANE - before processing a backplane notification received from {BackplaneMessageSourceId}", message.SourceId); + _incomingMessageHandler?.Invoke(message); + + if (_logger?.IsEnabled(LogLevel.Trace) ?? false) + _logger.Log(LogLevel.Trace, "FUSION: BACKPLANE - after processing a backplane notification received from {BackplaneMessageSourceId}", message.SourceId); } } 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 67dcc2ae..b4717310 100644 --- a/src/ZiggyCreatures.FusionCache.Backplane.Memory/ZiggyCreatures.FusionCache.Backplane.Memory.csproj +++ b/src/ZiggyCreatures.FusionCache.Backplane.Memory/ZiggyCreatures.FusionCache.Backplane.Memory.csproj @@ -4,7 +4,7 @@ <TargetFramework>netstandard2.0</TargetFramework> <LangVersion>latest</LangVersion> <Nullable>enable</Nullable> - <Version>0.23.0</Version> + <Version>0.24.0</Version> <PackageId>ZiggyCreatures.FusionCache.Backplane.Memory</PackageId> <PackageIcon>logo-128x128.png</PackageIcon> <Description>FusionCache in memory backplane, used for testing</Description> @@ -13,9 +13,7 @@ <DocumentationFile>ZiggyCreatures.FusionCache.Backplane.Memory.xml</DocumentationFile> <PackageReadmeFile>README.md</PackageReadmeFile> <PackageReleaseNotes> - - Added support for ConnectionId (better for concurrent tests scenarios) - - Better log messages - - Dependencies update + - Update: dependencies </PackageReleaseNotes> </PropertyGroup> diff --git a/src/ZiggyCreatures.FusionCache.Backplane.StackExchangeRedis/RedisBackplane.cs b/src/ZiggyCreatures.FusionCache.Backplane.StackExchangeRedis/RedisBackplane.cs index 700b6524..31d058c5 100644 --- a/src/ZiggyCreatures.FusionCache.Backplane.StackExchangeRedis/RedisBackplane.cs +++ b/src/ZiggyCreatures.FusionCache.Backplane.StackExchangeRedis/RedisBackplane.cs @@ -1,6 +1,4 @@ using System; -using System.Buffers.Binary; -using System.Text; using System.Threading; using System.Threading.Tasks; using Microsoft.Extensions.Logging; @@ -16,7 +14,6 @@ namespace ZiggyCreatures.Caching.Fusion.Backplane.StackExchangeRedis; public partial class RedisBackplane : IFusionCacheBackplane { - private static readonly Encoding _encoding = Encoding.UTF8; private readonly RedisBackplaneOptions _options; private BackplaneSubscriptionOptions? _subscriptionOptions; private readonly ILogger? _logger; @@ -24,7 +21,7 @@ public partial class RedisBackplane private readonly SemaphoreSlim _connectionLock; private IConnectionMultiplexer? _connection; - private string _channelName = "FusionCache.Notifications"; + private string? _channelName = null; private RedisChannel _channel; private ISubscriber? _subscriber; @@ -124,6 +121,9 @@ public void Subscribe(BackplaneSubscriptionOptions subscriptionOptions) _subscriptionOptions = subscriptionOptions; _channelName = _subscriptionOptions.ChannelName; + if (_channelName is null) + throw new NullReferenceException("The backplane channel name is null"); + _channel = new RedisChannel(_channelName, RedisChannel.PatternMode.Literal); _incomingMessageHandler = _subscriptionOptions.IncomingMessageHandler; @@ -133,7 +133,7 @@ public void Subscribe(BackplaneSubscriptionOptions subscriptionOptions) EnsureConnection(); if (_subscriber is null) - throw new NullReferenceException("The subscriber is null"); + throw new NullReferenceException("The backplane subscriber is null"); _subscriber.Subscribe(_channel, (_, v) => { @@ -167,45 +167,7 @@ internal void OnMessage(BackplaneMessage message) { try { - byte[]? data = value; - - if (data is null || data.Length == 0) - return null; - - var pos = 0; - var res = new BackplaneMessage(); - - // VERSION - var version = data[pos]; - if (version != 0) - { - if (logger?.IsEnabled(LogLevel.Warning) ?? false) - logger.Log(LogLevel.Warning, "FUSION: the version header does not have the expected value of 0 (zero): instead the value is " + version); - return null; - } - pos++; - - // SOURCE ID - var tmp = BinaryPrimitives.ReadInt32LittleEndian(new ReadOnlySpan<byte>(data, pos, 4)); - pos += 4; - res.SourceId = _encoding.GetString(data, pos, tmp); - pos += tmp; - - // INSTANT TICKS - res.InstantTicks = BinaryPrimitives.ReadInt64LittleEndian(new ReadOnlySpan<byte>(data, pos, 8)); - pos += 8; - - // ACTION - res.Action = (BackplaneMessageAction)data[pos]; - pos++; - - // CACHE KEY - tmp = BinaryPrimitives.ReadInt32LittleEndian(new ReadOnlySpan<byte>(data, pos, 4)); - pos += 4; - res.CacheKey = _encoding.GetString(data, pos, tmp); - //pos += tmp; - - return res; + return BackplaneMessage.FromByteArray(value); } catch (Exception exc) { @@ -220,45 +182,7 @@ private static RedisValue GetRedisValueFromMessage(BackplaneMessage message, ILo { try { - var sourceIdByteCount = _encoding.GetByteCount(message.SourceId); - var cacheKeyByteCount = _encoding.GetByteCount(message.CacheKey); - - var size = - 1 // VERSION - + 4 + sourceIdByteCount // SOURCE ID - + 8 // INSTANCE TICKS - + 1 // ACTION - + 4 + cacheKeyByteCount // CACHE KEY - ; - - var pos = 0; - var res = new byte[size]; - - // VERSION - res[pos] = 0; - pos++; - - // SOURCE ID - BinaryPrimitives.WriteInt32LittleEndian(new Span<byte>(res, pos, 4), sourceIdByteCount); - pos += 4; - _encoding.GetBytes(message.SourceId!, 0, message.SourceId!.Length, res, pos); - pos += sourceIdByteCount; - - // INSTANT TICKS - BinaryPrimitives.WriteInt64LittleEndian(new Span<byte>(res, pos, 8), message.InstantTicks); - pos += 8; - - // ACTION - res[pos] = (byte)message.Action; - pos++; - - // CACHE KEY - BinaryPrimitives.WriteInt32LittleEndian(new Span<byte>(res, pos, 4), cacheKeyByteCount); - pos += 4; - _encoding.GetBytes(message.CacheKey, 0, message.CacheKey!.Length, res, pos); - //pos += cacheKeyByteCount; - - return res; + return BackplaneMessage.ToByteArray(message); } catch (Exception exc) { diff --git a/src/ZiggyCreatures.FusionCache.Backplane.StackExchangeRedis/RedisBackplaneOptions.cs b/src/ZiggyCreatures.FusionCache.Backplane.StackExchangeRedis/RedisBackplaneOptions.cs index 48379c73..15d1f368 100644 --- a/src/ZiggyCreatures.FusionCache.Backplane.StackExchangeRedis/RedisBackplaneOptions.cs +++ b/src/ZiggyCreatures.FusionCache.Backplane.StackExchangeRedis/RedisBackplaneOptions.cs @@ -1,4 +1,5 @@ using System; +using System.ComponentModel; using System.Threading.Tasks; using Microsoft.Extensions.Options; using StackExchange.Redis; @@ -23,13 +24,15 @@ public class RedisBackplaneOptions public ConfigurationOptions? ConfigurationOptions { get; set; } /// <summary> - /// Gets or sets a delegate to create the ConnectionMultiplexer instance. + /// A delegate to create the ConnectionMultiplexer instance. /// </summary> - public Func<Task<IConnectionMultiplexer>>? ConnectionMultiplexerFactory { get; set; } + public Func<Task<IConnectionMultiplexer>>? ConnectionMultiplexerFactory { get; set; } /// <summary> - /// The configuration used to connect to Redis. + /// DEPRECATED: verify that at least one clients received the notifications after each publish. /// </summary> + [EditorBrowsable(EditorBrowsableState.Never)] + [Obsolete] public bool VerifyReceivedClientsCountAfterPublish { get; set; } = false; RedisBackplaneOptions IOptions<RedisBackplaneOptions>.Value diff --git a/src/ZiggyCreatures.FusionCache.Backplane.StackExchangeRedis/RedisBackplane_Async.cs b/src/ZiggyCreatures.FusionCache.Backplane.StackExchangeRedis/RedisBackplane_Async.cs index 3cee4153..f380b437 100644 --- a/src/ZiggyCreatures.FusionCache.Backplane.StackExchangeRedis/RedisBackplane_Async.cs +++ b/src/ZiggyCreatures.FusionCache.Backplane.StackExchangeRedis/RedisBackplane_Async.cs @@ -21,7 +21,15 @@ private async ValueTask EnsureConnectionAsync(CancellationToken token = default) if (_connection is not null) return; - _connection = await ConnectionMultiplexer.ConnectAsync(GetConfigurationOptions()).ConfigureAwait(false); + if (_options.ConnectionMultiplexerFactory is not null) + { + _connection = await _options.ConnectionMultiplexerFactory().ConfigureAwait(false); + } + else + { + _connection = ConnectionMultiplexer.Connect(GetConfigurationOptions()); + } + if (_connection is not null) { _connection.ConnectionRestored += OnReconnect; @@ -44,17 +52,13 @@ public async ValueTask PublishAsync(BackplaneMessage message, FusionCacheEntryOp { await EnsureConnectionAsync(token).ConfigureAwait(false); - var v = GetRedisValueFromMessage(message, _logger); + var value = GetRedisValueFromMessage(message, _logger); - if (v.IsNull) + if (value.IsNull) return; token.ThrowIfCancellationRequested(); - var receivedCount = await _subscriber!.PublishAsync(_channel, v).ConfigureAwait(false); - if (_options.VerifyReceivedClientsCountAfterPublish && receivedCount == 0) - { - throw new Exception($"An error occurred while trying to send a notification of type {message.Action} for cache key {message.CacheKey} to the Redis backplane: the received count was {receivedCount}"); - } + await _subscriber!.PublishAsync(_channel, value).ConfigureAwait(false); } } diff --git a/src/ZiggyCreatures.FusionCache.Backplane.StackExchangeRedis/RedisBackplane_Sync.cs b/src/ZiggyCreatures.FusionCache.Backplane.StackExchangeRedis/RedisBackplane_Sync.cs index 208c5031..e0197465 100644 --- a/src/ZiggyCreatures.FusionCache.Backplane.StackExchangeRedis/RedisBackplane_Sync.cs +++ b/src/ZiggyCreatures.FusionCache.Backplane.StackExchangeRedis/RedisBackplane_Sync.cs @@ -14,16 +14,21 @@ private void EnsureConnection(CancellationToken token = default) if (_connection is not null) return; - _connectionLock.Wait(); + _connectionLock.Wait(token); try { if (_connection is not null) return; - _connection = _options.ConnectionMultiplexerFactory is null - ? ConnectionMultiplexer.Connect(GetConfigurationOptions()) - : _options.ConnectionMultiplexerFactory().GetAwaiter().GetResult(); - + if (_options.ConnectionMultiplexerFactory is not null) + { + _connection = _options.ConnectionMultiplexerFactory().GetAwaiter().GetResult(); + } + else + { + _connection = ConnectionMultiplexer.Connect(GetConfigurationOptions()); + } + if (_connection is not null) { _connection.ConnectionRestored += OnReconnect; @@ -46,17 +51,13 @@ public void Publish(BackplaneMessage message, FusionCacheEntryOptions options, C { EnsureConnection(token); - var v = GetRedisValueFromMessage(message, _logger); + var value = GetRedisValueFromMessage(message, _logger); - if (v.IsNull) + if (value.IsNull) return; token.ThrowIfCancellationRequested(); - var receivedCount = _subscriber!.Publish(_channel, v); - if (_options.VerifyReceivedClientsCountAfterPublish && receivedCount == 0) - { - throw new Exception($"An error occurred while trying to send a notification of type {message.Action} for cache key {message.CacheKey} to the Redis backplane: the received count was {receivedCount}"); - } + _subscriber!.Publish(_channel, value); } } 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 597403fc..bc52432a 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 @@ <TargetFramework>netstandard2.0</TargetFramework> <LangVersion>latest</LangVersion> <Nullable>enable</Nullable> - <Version>0.23.0</Version> + <Version>0.24.0</Version> <PackageId>ZiggyCreatures.FusionCache.Backplane.StackExchangeRedis</PackageId> <PackageIcon>logo-128x128.png</PackageIcon> <Description>FusionCache backplane for Redis based on the StackExchange.Redis library</Description> @@ -13,9 +13,8 @@ <DocumentationFile>ZiggyCreatures.FusionCache.Backplane.StackExchangeRedis.xml</DocumentationFile> <PackageReadmeFile>README.md</PackageReadmeFile> <PackageReleaseNotes> - - Add support for re-connection events - - Better log messages - - Dependencies update + - Added: support for MultiplexerFactory in Redis Backplane + - Update: dependencies </PackageReleaseNotes> </PropertyGroup> @@ -29,7 +28,7 @@ </ItemGroup> <ItemGroup> - <PackageReference Include="StackExchange.Redis" Version="2.6.122" /> + <PackageReference Include="StackExchange.Redis" Version="2.7.4" /> <PackageReference Include="System.Memory" Version="4.5.5" /> </ItemGroup> diff --git a/src/ZiggyCreatures.FusionCache.Backplane.StackExchangeRedis/ZiggyCreatures.FusionCache.Backplane.StackExchangeRedis.xml b/src/ZiggyCreatures.FusionCache.Backplane.StackExchangeRedis/ZiggyCreatures.FusionCache.Backplane.StackExchangeRedis.xml index fcdc4ca1..05694f39 100644 --- a/src/ZiggyCreatures.FusionCache.Backplane.StackExchangeRedis/ZiggyCreatures.FusionCache.Backplane.StackExchangeRedis.xml +++ b/src/ZiggyCreatures.FusionCache.Backplane.StackExchangeRedis/ZiggyCreatures.FusionCache.Backplane.StackExchangeRedis.xml @@ -46,12 +46,12 @@ </member> <member name="P:ZiggyCreatures.Caching.Fusion.Backplane.StackExchangeRedis.RedisBackplaneOptions.ConnectionMultiplexerFactory"> <summary> - Gets or sets a delegate to create the ConnectionMultiplexer instance. + A delegate to create the ConnectionMultiplexer instance. </summary> </member> <member name="P:ZiggyCreatures.Caching.Fusion.Backplane.StackExchangeRedis.RedisBackplaneOptions.VerifyReceivedClientsCountAfterPublish"> <summary> - The configuration used to connect to Redis. + DEPRECATED: verify that at least one clients received the notifications after each publish. </summary> </member> <member name="T:Microsoft.Extensions.DependencyInjection.StackExchangeRedisBackplaneExtensions"> diff --git a/src/ZiggyCreatures.FusionCache.Chaos/ChaosBackplane.cs b/src/ZiggyCreatures.FusionCache.Chaos/ChaosBackplane.cs index aa5cb42a..6929e850 100644 --- a/src/ZiggyCreatures.FusionCache.Chaos/ChaosBackplane.cs +++ b/src/ZiggyCreatures.FusionCache.Chaos/ChaosBackplane.cs @@ -3,6 +3,7 @@ using System.Threading.Tasks; using Microsoft.Extensions.Logging; using ZiggyCreatures.Caching.Fusion.Backplane; +using ZiggyCreatures.Caching.Fusion.Chaos.Internals; namespace ZiggyCreatures.Caching.Fusion.Chaos; @@ -10,11 +11,12 @@ namespace ZiggyCreatures.Caching.Fusion.Chaos; /// An implementation of <see cref="IFusionCacheBackplane"/> that acts on behalf of another one, but with a (controllable) amount of chaos in-between. /// </summary> public class ChaosBackplane - : IFusionCacheBackplane + : AbstractChaosComponent + , IFusionCacheBackplane { private readonly IFusionCacheBackplane _innerBackplane; - private readonly ILogger<ChaosBackplane>? _logger; - private Action<BackplaneConnectionInfo>? _connectHandler; + private Action<BackplaneConnectionInfo>? _innerConnectHandler; + private Action<BackplaneMessage>? _innerIncomingMessageHandler; /// <summary> /// Initializes a new instance of the ChaosBackplane class. @@ -22,137 +24,83 @@ public class ChaosBackplane /// <param name="innerBackplane">The actual <see cref="IFusionCacheBackplane"/> used if and when chaos does not happen.</param> /// <param name="logger">The logger to use, or <see langword="null"/>.</param> public ChaosBackplane(IFusionCacheBackplane innerBackplane, ILogger<ChaosBackplane>? logger = null) + : base(logger) { _innerBackplane = innerBackplane ?? throw new ArgumentNullException(nameof(innerBackplane)); - _logger = logger; - - ChaosThrowProbability = 0f; - ChaosMinDelay = TimeSpan.Zero; - ChaosMaxDelay = TimeSpan.Zero; } - /// <summary> - /// A <see cref="float"/> value from 0.0 to 1.0 that represents the probabilty of throwing an exception: set it to 0.0 to never throw or to 1.0 to always throw. - /// </summary> - public float ChaosThrowProbability { get; set; } - - /// <summary> - /// The minimum amount of randomized delay. - /// </summary> - public TimeSpan ChaosMinDelay { get; set; } - - /// <summary> - /// The maximum amount of randomized delay. - /// </summary> - public TimeSpan ChaosMaxDelay { get; set; } - - /// <summary> - /// Force chaos exceptions to never be thrown. - /// </summary> - public void SetNeverThrow() + /// <inheritdoc/> + public void Publish(BackplaneMessage message, FusionCacheEntryOptions options, CancellationToken token = default) { - if (_logger?.IsEnabled(LogLevel.Debug) ?? false) - _logger.Log(LogLevel.Debug, "FUSION ChaosBackplane: SetNeverThrow"); - - var _old = ChaosThrowProbability; - ChaosThrowProbability = 0f; - - if (_old != ChaosThrowProbability) - _connectHandler?.Invoke(new BackplaneConnectionInfo(true)); + MaybeChaos(token); + _innerBackplane.Publish(message, options, token); } - /// <summary> - /// Force chaos exceptions to always be thrown. - /// </summary> - public void SetAlwaysThrow() + /// <inheritdoc/> + public async ValueTask PublishAsync(BackplaneMessage message, FusionCacheEntryOptions options, CancellationToken token = default) { - if (_logger?.IsEnabled(LogLevel.Debug) ?? false) - _logger.Log(LogLevel.Debug, "FUSION ChaosBackplane: SetAlwaysThrow"); - - ChaosThrowProbability = 1f; + await MaybeChaosAsync(token).ConfigureAwait(false); + await _innerBackplane.PublishAsync(message, options, token).ConfigureAwait(false); } - /// <summary> - /// Force chaos delays to never happen. - /// </summary> - public void SetNeverDelay() + /// <inheritdoc/> + public void Subscribe(BackplaneSubscriptionOptions options) { if (_logger?.IsEnabled(LogLevel.Debug) ?? false) - _logger.Log(LogLevel.Debug, "FUSION ChaosBackplane: SetNeverDelay"); + _logger.Log(LogLevel.Debug, "FUSION ChaosBackplane: Subscribe"); - ChaosMinDelay = TimeSpan.Zero; - ChaosMaxDelay = TimeSpan.Zero; - } + MaybeChaos(); - /// <summary> - /// Force chaos delays to always be of exactly this amount. - /// </summary> - /// <param name="delay"></param> - public void SetAlwaysDelayExactly(TimeSpan delay) - { - if (_logger?.IsEnabled(LogLevel.Debug) ?? false) - _logger.Log(LogLevel.Debug, "FUSION ChaosBackplane: SetAlwaysDelayExactly"); + _innerConnectHandler = options.ConnectHandler; + _innerIncomingMessageHandler = options.IncomingMessageHandler; - ChaosMinDelay = delay; - ChaosMaxDelay = delay; + var innerOptions = new BackplaneSubscriptionOptions( + options.ChannelName, + OnConnect, + OnIncomingMessage + ); + + _innerBackplane.Subscribe(innerOptions); } - /// <summary> - /// Force chaos exceptions and delays to never happen. - /// </summary> - public void SetNeverChaos() + /// <inheritdoc/> + public void Unsubscribe() { if (_logger?.IsEnabled(LogLevel.Debug) ?? false) - _logger.Log(LogLevel.Debug, "FUSION ChaosBackplane: SetNeverChaos"); + _logger.Log(LogLevel.Debug, "FUSION ChaosBackplane: Unsubscribe"); - SetNeverThrow(); - SetNeverDelay(); - } + MaybeChaos(); - /// <summary> - /// Force chaos exceptions to always throw, and chaos delays to always be of exactly this amount. - /// </summary> - /// <param name="delay"></param> - public void SetAlwaysChaos(TimeSpan delay) - { - if (_logger?.IsEnabled(LogLevel.Debug) ?? false) - _logger.Log(LogLevel.Debug, "FUSION ChaosBackplane: SetAlwaysChaos"); + _innerConnectHandler = null; + _innerIncomingMessageHandler = null; - SetAlwaysThrow(); - SetAlwaysDelayExactly(delay); + _innerBackplane.Unsubscribe(); } /// <inheritdoc/> - public void Publish(BackplaneMessage message, FusionCacheEntryOptions options, CancellationToken token = default) + public override void SetNeverThrow() { - FusionCacheChaosUtils.MaybeChaos(ChaosMinDelay, ChaosMaxDelay, ChaosThrowProbability); - _innerBackplane.Publish(message, options, token); - } + var _old = ChaosThrowProbability; - /// <inheritdoc/> - public async ValueTask PublishAsync(BackplaneMessage message, FusionCacheEntryOptions options, CancellationToken token = default) - { - await FusionCacheChaosUtils.MaybeChaosAsync(ChaosMinDelay, ChaosMaxDelay, ChaosThrowProbability).ConfigureAwait(false); - await _innerBackplane.PublishAsync(message, options, token).ConfigureAwait(false); + base.SetNeverThrow(); + + if (_old != ChaosThrowProbability) + OnConnect(new BackplaneConnectionInfo(true)); } - /// <inheritdoc/> - public void Subscribe(BackplaneSubscriptionOptions options) + void OnConnect(BackplaneConnectionInfo info) { - if (_logger?.IsEnabled(LogLevel.Debug) ?? false) - _logger.Log(LogLevel.Debug, "FUSION ChaosBackplane: Subscribe"); + if (ShouldThrow()) + return; - _innerBackplane.Subscribe(options); - _connectHandler = options.ConnectHandler; + _innerConnectHandler?.Invoke(info); } - /// <inheritdoc/> - public void Unsubscribe() + void OnIncomingMessage(BackplaneMessage message) { - if (_logger?.IsEnabled(LogLevel.Debug) ?? false) - _logger.Log(LogLevel.Debug, "FUSION ChaosBackplane: Unsubscribe"); + if (ShouldThrow()) + return; - _connectHandler = null; - _innerBackplane.Unsubscribe(); + _innerIncomingMessageHandler?.Invoke(message); } } diff --git a/src/ZiggyCreatures.FusionCache.Chaos/ChaosDistributedCache.cs b/src/ZiggyCreatures.FusionCache.Chaos/ChaosDistributedCache.cs index b8904cc1..c7b5bb00 100644 --- a/src/ZiggyCreatures.FusionCache.Chaos/ChaosDistributedCache.cs +++ b/src/ZiggyCreatures.FusionCache.Chaos/ChaosDistributedCache.cs @@ -2,6 +2,8 @@ using System.Threading; using System.Threading.Tasks; using Microsoft.Extensions.Caching.Distributed; +using Microsoft.Extensions.Logging; +using ZiggyCreatures.Caching.Fusion.Chaos.Internals; namespace ZiggyCreatures.Caching.Fusion.Chaos; @@ -9,7 +11,8 @@ namespace ZiggyCreatures.Caching.Fusion.Chaos; /// An implementation of <see cref="IDistributedCache"/> that acts on behalf of another one, but with a (controllable) amount of chaos in-between. /// </summary> public class ChaosDistributedCache - : IDistributedCache + : AbstractChaosComponent + , IDistributedCache { private readonly IDistributedCache _innerCache; @@ -17,137 +20,66 @@ public class ChaosDistributedCache /// Initializes a new instance of the ChaosDistributedCache class. /// </summary> /// <param name="innerCache">The actual <see cref="IDistributedCache"/> used if and when chaos does not happen.</param> - public ChaosDistributedCache(IDistributedCache innerCache) + /// <param name="logger">The logger to use, or <see langword="null"/>.</param> + public ChaosDistributedCache(IDistributedCache innerCache, ILogger<ChaosDistributedCache>? logger = null) + : base(logger) { _innerCache = innerCache ?? throw new ArgumentNullException(nameof(innerCache)); - - ChaosThrowProbability = 0f; - ChaosMinDelay = TimeSpan.Zero; - ChaosMaxDelay = TimeSpan.Zero; - } - - /// <summary> - /// A <see cref="float"/> value from 0.0 to 1.0 that represents the probabilty of throwing an exception: set it to 0.0 to never throw or to 1.0 to always throw. - /// </summary> - public float ChaosThrowProbability { get; set; } - - /// <summary> - /// The minimum amount of randomized delay. - /// </summary> - public TimeSpan ChaosMinDelay { get; set; } - - /// <summary> - /// The maximum amount of randomized delay. - /// </summary> - public TimeSpan ChaosMaxDelay { get; set; } - - /// <summary> - /// Force chaos exceptions to never be thrown. - /// </summary> - public void SetNeverThrow() - { - ChaosThrowProbability = 0f; - } - - /// <summary> - /// Force chaos exceptions to always be thrown. - /// </summary> - public void SetAlwaysThrow() - { - ChaosThrowProbability = 1f; - } - - /// <summary> - /// Force chaos delays to never happen. - /// </summary> - public void SetNeverDelay() - { - ChaosMinDelay = TimeSpan.Zero; - ChaosMaxDelay = TimeSpan.Zero; - } - - /// <summary> - /// Force chaos delays to always be of exactly this amount. - /// </summary> - /// <param name="delay"></param> - public void SetAlwaysDelayExactly(TimeSpan delay) - { - ChaosMinDelay = delay; - ChaosMaxDelay = delay; - } - - /// <summary> - /// Force chaos exceptions and delays to never happen. - /// </summary> - public void SetNeverChaos() - { - SetNeverThrow(); - SetNeverDelay(); - } - - /// <summary> - /// Force chaos exceptions to always throw, and chaos delays to always be of exactly this amount. - /// </summary> - /// <param name="delay"></param> - public void SetAlwaysChaos(TimeSpan delay) - { - SetAlwaysThrow(); - SetAlwaysDelayExactly(delay); } /// <inheritdoc/> public byte[] Get(string key) { - FusionCacheChaosUtils.MaybeChaos(ChaosMinDelay, ChaosMaxDelay, ChaosThrowProbability); + MaybeChaos(); return _innerCache.Get(key); } /// <inheritdoc/> public async Task<byte[]> GetAsync(string key, CancellationToken token = default) { - await FusionCacheChaosUtils.MaybeChaosAsync(ChaosMinDelay, ChaosMaxDelay, ChaosThrowProbability).ConfigureAwait(false); + await MaybeChaosAsync(token).ConfigureAwait(false); return await _innerCache.GetAsync(key, token).ConfigureAwait(false); } /// <inheritdoc/> public void Refresh(string key) { - FusionCacheChaosUtils.MaybeChaos(ChaosMinDelay, ChaosMaxDelay, ChaosThrowProbability); + MaybeChaos(); _innerCache.Refresh(key); } /// <inheritdoc/> public async Task RefreshAsync(string key, CancellationToken token = default) { - await FusionCacheChaosUtils.MaybeChaosAsync(ChaosMinDelay, ChaosMaxDelay, ChaosThrowProbability).ConfigureAwait(false); + await MaybeChaosAsync(token).ConfigureAwait(false); await _innerCache.RefreshAsync(key, token).ConfigureAwait(false); } /// <inheritdoc/> public void Remove(string key) { - FusionCacheChaosUtils.MaybeChaos(ChaosMinDelay, ChaosMaxDelay, ChaosThrowProbability); + MaybeChaos(); _innerCache.Remove(key); } /// <inheritdoc/> public async Task RemoveAsync(string key, CancellationToken token = default) { - await FusionCacheChaosUtils.MaybeChaosAsync(ChaosMinDelay, ChaosMaxDelay, ChaosThrowProbability).ConfigureAwait(false); + await MaybeChaosAsync(token).ConfigureAwait(false); await _innerCache.RemoveAsync(key, token).ConfigureAwait(false); } /// <inheritdoc/> public void Set(string key, byte[] value, DistributedCacheEntryOptions options) { - FusionCacheChaosUtils.MaybeChaos(ChaosMinDelay, ChaosMaxDelay, ChaosThrowProbability); + MaybeChaos(); _innerCache.Set(key, value, options); } /// <inheritdoc/> public async Task SetAsync(string key, byte[] value, DistributedCacheEntryOptions options, CancellationToken token = default) { - await FusionCacheChaosUtils.MaybeChaosAsync(ChaosMinDelay, ChaosMaxDelay, ChaosThrowProbability).ConfigureAwait(false); + await MaybeChaosAsync(token).ConfigureAwait(false); await _innerCache.SetAsync(key, value, options, token).ConfigureAwait(false); } } diff --git a/src/ZiggyCreatures.FusionCache.Chaos/ChaosPlugin.cs b/src/ZiggyCreatures.FusionCache.Chaos/ChaosPlugin.cs index 936349f8..7c6f417e 100644 --- a/src/ZiggyCreatures.FusionCache.Chaos/ChaosPlugin.cs +++ b/src/ZiggyCreatures.FusionCache.Chaos/ChaosPlugin.cs @@ -1,4 +1,6 @@ using System; +using Microsoft.Extensions.Logging; +using ZiggyCreatures.Caching.Fusion.Chaos.Internals; using ZiggyCreatures.Caching.Fusion.Plugins; namespace ZiggyCreatures.Caching.Fusion.Chaos @@ -7,7 +9,8 @@ namespace ZiggyCreatures.Caching.Fusion.Chaos /// An implementation of <see cref="IFusionCachePlugin"/> with a (controllable) amount of chaos in-between. /// </summary> public class ChaosPlugin - : IFusionCachePlugin + : AbstractChaosComponent + , IFusionCachePlugin { IFusionCachePlugin _innerPlugin; @@ -15,96 +18,24 @@ public class ChaosPlugin /// Initializes a new instance of the ChaosPlugin class. /// </summary> /// <param name="innerPlugin">The actual <see cref="IFusionCachePlugin"/> used if and when chaos does not happen.</param> - public ChaosPlugin(IFusionCachePlugin innerPlugin) + /// <param name="logger">The logger to use, or <see langword="null"/>.</param> + public ChaosPlugin(IFusionCachePlugin innerPlugin, ILogger<ChaosPlugin>? logger = null) + : base(logger) { _innerPlugin = innerPlugin ?? throw new ArgumentNullException(nameof(innerPlugin)); - - ChaosThrowProbability = 0f; - ChaosMinDelay = TimeSpan.Zero; - ChaosMaxDelay = TimeSpan.Zero; - } - - - /// <summary> - /// A <see cref="float"/> value from 0.0 to 1.0 that represents the probabilty of throwing an exception: set it to 0.0 to never throw or to 1.0 to always throw. - /// </summary> - public float ChaosThrowProbability { get; set; } - - /// <summary> - /// The minimum amount of randomized delay. - /// </summary> - public TimeSpan ChaosMinDelay { get; set; } - - /// <summary> - /// The maximum amount of randomized delay. - /// </summary> - public TimeSpan ChaosMaxDelay { get; set; } - - /// <summary> - /// Force chaos exceptions to never be thrown. - /// </summary> - public void SetNeverThrow() - { - ChaosThrowProbability = 0f; - } - - /// <summary> - /// Force chaos exceptions to always be thrown. - /// </summary> - public void SetAlwaysThrow() - { - ChaosThrowProbability = 1f; - } - - /// <summary> - /// Force chaos delays to never happen. - /// </summary> - public void SetNeverDelay() - { - ChaosMinDelay = TimeSpan.Zero; - ChaosMaxDelay = TimeSpan.Zero; - } - - /// <summary> - /// Force chaos delays to always be of exactly this amount. - /// </summary> - /// <param name="delay"></param> - public void SetAlwaysDelayExactly(TimeSpan delay) - { - ChaosMinDelay = delay; - ChaosMaxDelay = delay; - } - - /// <summary> - /// Force chaos exceptions and delays to never happen. - /// </summary> - public void SetNeverChaos() - { - SetNeverThrow(); - SetNeverDelay(); - } - - /// <summary> - /// Force chaos exceptions to always throw, and chaos delays to always be of exactly this amount. - /// </summary> - /// <param name="delay"></param> - public void SetAlwaysChaos(TimeSpan delay) - { - SetAlwaysThrow(); - SetAlwaysDelayExactly(delay); } /// <inheritdoc/> public void Start(IFusionCache cache) { - FusionCacheChaosUtils.MaybeChaos(ChaosMinDelay, ChaosMaxDelay, ChaosThrowProbability); + MaybeChaos(); _innerPlugin.Start(cache); } /// <inheritdoc/> public void Stop(IFusionCache cache) { - FusionCacheChaosUtils.MaybeChaos(ChaosMinDelay, ChaosMaxDelay, ChaosThrowProbability); + MaybeChaos(); _innerPlugin.Stop(cache); } } diff --git a/src/ZiggyCreatures.FusionCache.Chaos/ChaosSerializer.cs b/src/ZiggyCreatures.FusionCache.Chaos/ChaosSerializer.cs index 5851c842..57c210cb 100644 --- a/src/ZiggyCreatures.FusionCache.Chaos/ChaosSerializer.cs +++ b/src/ZiggyCreatures.FusionCache.Chaos/ChaosSerializer.cs @@ -1,5 +1,7 @@ using System; using System.Threading.Tasks; +using Microsoft.Extensions.Logging; +using ZiggyCreatures.Caching.Fusion.Chaos.Internals; using ZiggyCreatures.Caching.Fusion.Serialization; namespace ZiggyCreatures.Caching.Fusion.Chaos; @@ -8,7 +10,8 @@ namespace ZiggyCreatures.Caching.Fusion.Chaos; /// An implementation of <see cref="IFusionCacheSerializer"/> that acts on behalf of another one, but with a (controllable) amount of chaos in-between. /// </summary> public class ChaosSerializer - : IFusionCacheSerializer + : AbstractChaosComponent + , IFusionCacheSerializer { private readonly IFusionCacheSerializer _innerSerializer; @@ -16,109 +19,38 @@ public class ChaosSerializer /// Initializes a new instance of the ChaosSerializer class. /// </summary> /// <param name="innerSerializer">The actual <see cref="IFusionCacheSerializer"/> used if and when chaos does not happen.</param> - public ChaosSerializer(IFusionCacheSerializer innerSerializer) + /// <param name="logger">The logger to use, or <see langword="null"/>.</param> + public ChaosSerializer(IFusionCacheSerializer innerSerializer, ILogger<ChaosSerializer>? logger = null) + : base(logger) { _innerSerializer = innerSerializer ?? throw new ArgumentNullException(nameof(innerSerializer)); - - ChaosThrowProbability = 0f; - ChaosMinDelay = TimeSpan.Zero; - ChaosMaxDelay = TimeSpan.Zero; - } - - /// <summary> - /// A <see cref="float"/> value from 0.0 to 1.0 that represents the probabilty of throwing an exception: set it to 0.0 to never throw or to 1.0 to always throw. - /// </summary> - public float ChaosThrowProbability { get; set; } - - /// <summary> - /// The minimum amount of randomized delay. - /// </summary> - public TimeSpan ChaosMinDelay { get; set; } - - /// <summary> - /// The maximum amount of randomized delay. - /// </summary> - public TimeSpan ChaosMaxDelay { get; set; } - - /// <summary> - /// Force chaos exceptions to never be thrown. - /// </summary> - public void SetNeverThrow() - { - ChaosThrowProbability = 0f; - } - - /// <summary> - /// Force chaos exceptions to always be thrown. - /// </summary> - public void SetAlwaysThrow() - { - ChaosThrowProbability = 1f; - } - - /// <summary> - /// Force chaos delays to never happen. - /// </summary> - public void SetNeverDelay() - { - ChaosMinDelay = TimeSpan.Zero; - ChaosMaxDelay = TimeSpan.Zero; - } - - /// <summary> - /// Force chaos delays to always be of exactly this amount. - /// </summary> - /// <param name="delay"></param> - public void SetAlwaysDelayExactly(TimeSpan delay) - { - ChaosMinDelay = delay; - ChaosMaxDelay = delay; - } - - /// <summary> - /// Force chaos exceptions and delays to never happen. - /// </summary> - public void SetNeverChaos() - { - SetNeverThrow(); - SetNeverDelay(); - } - - /// <summary> - /// Force chaos exceptions to always throw, and chaos delays to always be of exactly this amount. - /// </summary> - /// <param name="delay"></param> - public void SetAlwaysChaos(TimeSpan delay) - { - SetAlwaysThrow(); - SetAlwaysDelayExactly(delay); } /// <inheritdoc/> public T? Deserialize<T>(byte[] data) { - FusionCacheChaosUtils.MaybeChaos(ChaosMinDelay, ChaosMaxDelay, ChaosThrowProbability); + MaybeChaos(); return _innerSerializer.Deserialize<T>(data); } /// <inheritdoc/> public async ValueTask<T?> DeserializeAsync<T>(byte[] data) { - await FusionCacheChaosUtils.MaybeChaosAsync(ChaosMinDelay, ChaosMaxDelay, ChaosThrowProbability); + await MaybeChaosAsync().ConfigureAwait(false); return await _innerSerializer.DeserializeAsync<T>(data); } /// <inheritdoc/> public byte[] Serialize<T>(T? obj) { - FusionCacheChaosUtils.MaybeChaos(ChaosMinDelay, ChaosMaxDelay, ChaosThrowProbability); + MaybeChaos(); return _innerSerializer.Serialize<T>(obj); } /// <inheritdoc/> public async ValueTask<byte[]> SerializeAsync<T>(T? obj) { - await FusionCacheChaosUtils.MaybeChaosAsync(ChaosMinDelay, ChaosMaxDelay, ChaosThrowProbability); + await MaybeChaosAsync().ConfigureAwait(false); return await _innerSerializer.SerializeAsync<T>(obj); } } diff --git a/src/ZiggyCreatures.FusionCache.Chaos/FusionCacheChaosUtils.cs b/src/ZiggyCreatures.FusionCache.Chaos/FusionCacheChaosUtils.cs index a19e6b93..7e139cf7 100644 --- a/src/ZiggyCreatures.FusionCache.Chaos/FusionCacheChaosUtils.cs +++ b/src/ZiggyCreatures.FusionCache.Chaos/FusionCacheChaosUtils.cs @@ -15,7 +15,7 @@ public static class FusionCacheChaosUtils /// </summary> /// <param name="throwProbability">The probabilty that an exception will be thrown.</param> /// <returns><see langword="true"/> if an exception should be thrown, <see langword="false"/> otherwise.</returns> - public static bool ShouldCreateChaos(float throwProbability) + public static bool ShouldThrow(float throwProbability) { if (throwProbability <= 0f) return false; @@ -32,7 +32,7 @@ public static bool ShouldCreateChaos(float throwProbability) /// <param name="throwProbability">The probabilty that an exception will be thrown.</param> public static void MaybeThrow(float throwProbability) { - if (ShouldCreateChaos(throwProbability)) + if (ShouldThrow(throwProbability)) throw new ChaosException("Just a little bit of controlled chaos"); } @@ -42,7 +42,7 @@ public static void MaybeThrow(float throwProbability) /// <param name="minDelay">The minimun amount of delay.</param> /// <param name="maxDelay">The maximum amount of delay.</param> /// <returns>The randomized delay.</returns> - public static TimeSpan RandomizeDelay(TimeSpan minDelay, TimeSpan maxDelay) + public static TimeSpan GetRandomDelay(TimeSpan minDelay, TimeSpan maxDelay) { if (minDelay <= TimeSpan.Zero && maxDelay <= TimeSpan.Zero) return TimeSpan.Zero; @@ -58,10 +58,12 @@ public static TimeSpan RandomizeDelay(TimeSpan minDelay, TimeSpan maxDelay) /// </summary> /// <param name="minDelay">The minimun amount of delay.</param> /// <param name="maxDelay">The maximum amount of delay.</param> - public static void MaybeDelay(TimeSpan minDelay, TimeSpan maxDelay) + /// <param name="token">The cancellation token.</param> + public static void MaybeDelay(TimeSpan minDelay, TimeSpan maxDelay, CancellationToken token = default) { - var delay = RandomizeDelay(minDelay, maxDelay); + var delay = GetRandomDelay(minDelay, maxDelay); + // TODO: FIND A WAY TO CANCEL THE DELAY if (delay > TimeSpan.Zero) Thread.Sleep(delay); } @@ -71,25 +73,27 @@ public static void MaybeDelay(TimeSpan minDelay, TimeSpan maxDelay) /// </summary> /// <param name="minDelay">The minimun amount of delay.</param> /// <param name="maxDelay">The maximum amount of delay.</param> + /// <param name="token">The cancellation token.</param> /// <returns>A <see cref="Task"/> instance to await.</returns> - public static async Task MaybeDelayAsync(TimeSpan minDelay, TimeSpan maxDelay) + public static async Task MaybeDelayAsync(TimeSpan minDelay, TimeSpan maxDelay, CancellationToken token = default) { - var delay = RandomizeDelay(minDelay, maxDelay); + var delay = GetRandomDelay(minDelay, maxDelay); if (delay > TimeSpan.Zero) - await Task.Delay(delay).ConfigureAwait(false); + await Task.Delay(delay, token).ConfigureAwait(false); } /// <summary> /// Randomize an actual delay with a value between <paramref name="minDelay"/> and <paramref name="maxDelay"/>, and waits for it. /// Then, maybe, throw a <see cref="ChaosException"/> based on the specified probabilty. /// </summary> - /// <param name="throwProbability">The probabilty that an exception will be thrown.</param> /// <param name="minDelay">The minimun amount of delay.</param> /// <param name="maxDelay">The maximum amount of delay.</param> - public static void MaybeChaos(TimeSpan minDelay, TimeSpan maxDelay, float throwProbability) + /// <param name="throwProbability">The probabilty that an exception will be thrown.</param> + /// <param name="token">The cancellation token.</param> + public static void MaybeChaos(TimeSpan minDelay, TimeSpan maxDelay, float throwProbability, CancellationToken token = default) { - MaybeDelay(minDelay, maxDelay); + MaybeDelay(minDelay, maxDelay, token); MaybeThrow(throwProbability); } @@ -97,13 +101,14 @@ public static void MaybeChaos(TimeSpan minDelay, TimeSpan maxDelay, float throwP /// Randomize an actual delay with a value between <paramref name="minDelay"/> and <paramref name="maxDelay"/>, and waits for it. /// Then, maybe, throw a <see cref="ChaosException"/> based on the specified probabilty. /// </summary> - /// <param name="throwProbability">The probabilty that an exception will be thrown.</param> /// <param name="minDelay">The minimun amount of delay.</param> /// <param name="maxDelay">The maximum amount of delay.</param> + /// <param name="throwProbability">The probabilty that an exception will be thrown.</param> + /// <param name="token">The cancellation token.</param> /// <returns>A <see cref="Task"/> instance to await.</returns> - public static async Task MaybeChaosAsync(TimeSpan minDelay, TimeSpan maxDelay, float throwProbability) + public static async Task MaybeChaosAsync(TimeSpan minDelay, TimeSpan maxDelay, float throwProbability, CancellationToken token = default) { - await MaybeDelayAsync(minDelay, maxDelay).ConfigureAwait(false); + await MaybeDelayAsync(minDelay, maxDelay, token).ConfigureAwait(false); MaybeThrow(throwProbability); } } diff --git a/src/ZiggyCreatures.FusionCache.Chaos/Internals/AbstractChaosComponent.cs b/src/ZiggyCreatures.FusionCache.Chaos/Internals/AbstractChaosComponent.cs new file mode 100644 index 00000000..89dd3391 --- /dev/null +++ b/src/ZiggyCreatures.FusionCache.Chaos/Internals/AbstractChaosComponent.cs @@ -0,0 +1,177 @@ +using System; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.Extensions.Logging; + +namespace ZiggyCreatures.Caching.Fusion.Chaos.Internals +{ + /// <summary> + /// A base implementation for a component with a controllable amount of chaos. + /// </summary> + public abstract class AbstractChaosComponent + { + private readonly string _className; + + /// <summary> + /// The <see cref="ILogger"/> to use, or <see langword="null"/> for no logging. + /// </summary> + protected readonly ILogger? _logger; + + /// <summary> + /// Initializes a new instance of the AbstractChaosComponent class. + /// </summary> + /// <param name="logger">The logger to use, or <see langword="null"/>.</param> + protected AbstractChaosComponent(ILogger? logger) + { + _className = GetType().Name; + + _logger = logger; + + ChaosThrowProbability = 0f; + ChaosMinDelay = TimeSpan.Zero; + ChaosMaxDelay = TimeSpan.Zero; + } + + /// <summary> + /// The maximum amount of randomized delay. + /// </summary> + public TimeSpan ChaosMaxDelay { get; set; } + + /// <summary> + /// The minimum amount of randomized delay. + /// </summary> + public TimeSpan ChaosMinDelay { get; set; } + + /// <summary> + /// A <see cref="float"/> value from 0.0 to 1.0 that represents the probabilty of throwing an exception: set it to 0.0 to never throw or to 1.0 to always throw. + /// </summary> + public float ChaosThrowProbability { get; set; } + + /// <summary> + /// Force chaos delays to always be between certain amounts. + /// </summary> + /// <param name="minDelay">The minimum amount of delay.</param> + /// <param name="maxDelay">The maximum amount of delay.</param> + public virtual void SetAlwaysDelay(TimeSpan minDelay, TimeSpan maxDelay) + { + if (_logger?.IsEnabled(LogLevel.Debug) ?? false) + _logger.Log(LogLevel.Debug, $"FUSION {_className}: SetDelay"); + + ChaosMinDelay = minDelay; + ChaosMaxDelay = maxDelay; + } + + /// <summary> + /// Force chaos delays to always be of exactly this amount. + /// </summary> + /// <param name="delay">The amount of delay.</param> + public virtual void SetAlwaysDelayExactly(TimeSpan delay) + { + if (_logger?.IsEnabled(LogLevel.Debug) ?? false) + _logger.Log(LogLevel.Debug, $"FUSION {_className}: SetAlwaysDelayExactly"); + + ChaosMinDelay = delay; + ChaosMaxDelay = delay; + } + + /// <summary> + /// Force chaos exceptions to always be thrown. + /// </summary> + public virtual void SetAlwaysThrow() + { + if (_logger?.IsEnabled(LogLevel.Debug) ?? false) + _logger.Log(LogLevel.Debug, $"FUSION {_className}: SetAlwaysThrow"); + + ChaosThrowProbability = 1f; + } + + /// <summary> + /// Force chaos exceptions to always throw, and chaos delays to always be of exactly this amount. + /// </summary> + /// <param name="delay">The amount of delay.</param> + public virtual void SetAlwaysChaos(TimeSpan delay) + { + if (_logger?.IsEnabled(LogLevel.Debug) ?? false) + _logger.Log(LogLevel.Debug, $"FUSION {_className}: SetAlwaysChaos"); + + SetAlwaysThrow(); + SetAlwaysDelayExactly(delay); + } + + /// <summary> + /// Force chaos exceptions to always throw, and chaos delays to always be between certain amounts. + /// </summary> + /// <param name="minDelay">The minimum amount of delay.</param> + /// <param name="maxDelay">The maximum amount of delay.</param> + public virtual void SetAlwaysChaos(TimeSpan minDelay, TimeSpan maxDelay) + { + if (_logger?.IsEnabled(LogLevel.Debug) ?? false) + _logger.Log(LogLevel.Debug, $"FUSION {_className}: SetAlwaysChaos"); + + SetAlwaysThrow(); + SetAlwaysDelay(minDelay, maxDelay); + } + + /// <summary> + /// Force chaos exceptions and delays to never happen. + /// </summary> + public virtual void SetNeverChaos() + { + if (_logger?.IsEnabled(LogLevel.Debug) ?? false) + _logger.Log(LogLevel.Debug, $"FUSION {_className}: SetNeverChaos"); + + SetNeverThrow(); + SetNeverDelay(); + } + + /// <summary> + /// Force chaos delays to never happen. + /// </summary> + public virtual void SetNeverDelay() + { + if (_logger?.IsEnabled(LogLevel.Debug) ?? false) + _logger.Log(LogLevel.Debug, $"FUSION {_className}: SetNeverDelay"); + + ChaosMinDelay = TimeSpan.Zero; + ChaosMaxDelay = TimeSpan.Zero; + } + + /// <summary> + /// Force chaos exceptions to never be thrown. + /// </summary> + public virtual void SetNeverThrow() + { + if (_logger?.IsEnabled(LogLevel.Debug) ?? false) + _logger.Log(LogLevel.Debug, $"FUSION {_className}: SetNeverThrow"); + + ChaosThrowProbability = 0f; + } + + /// <summary> + /// Determines if an exception should be thrown. + /// </summary> + /// <returns><see langword="true"/> if an exception should be thrown, <see langword="false"/> otherwise.</returns> + public virtual bool ShouldThrow() + { + return FusionCacheChaosUtils.ShouldThrow(ChaosThrowProbability); + } + + /// <summary> + /// Randomize an actual delay with a value between the configured min/max values, and if needed waits for it. + /// Then, maybe, throw a <see cref="ChaosException"/> based on the specified probabilty. + /// </summary> + protected void MaybeChaos(CancellationToken token = default) + { + FusionCacheChaosUtils.MaybeChaos(ChaosMinDelay, ChaosMaxDelay, ChaosThrowProbability, token); + } + + /// <summary> + /// Randomize an actual delay with a value between the configured min/max values, and if needed waits for it. + /// Then, maybe, throw a <see cref="ChaosException"/> based on the specified probabilty. + /// </summary> + protected async Task MaybeChaosAsync(CancellationToken token = default) + { + await FusionCacheChaosUtils.MaybeChaosAsync(ChaosMinDelay, ChaosMaxDelay, ChaosThrowProbability, token).ConfigureAwait(false); + } + } +} diff --git a/src/ZiggyCreatures.FusionCache.Chaos/ZiggyCreatures.FusionCache.Chaos.csproj b/src/ZiggyCreatures.FusionCache.Chaos/ZiggyCreatures.FusionCache.Chaos.csproj index 0b3367b9..cc470026 100644 --- a/src/ZiggyCreatures.FusionCache.Chaos/ZiggyCreatures.FusionCache.Chaos.csproj +++ b/src/ZiggyCreatures.FusionCache.Chaos/ZiggyCreatures.FusionCache.Chaos.csproj @@ -4,7 +4,7 @@ <TargetFramework>netstandard2.0</TargetFramework> <LangVersion>latest</LangVersion> <Nullable>enable</Nullable> - <Version>0.23.0</Version> + <Version>0.24.0</Version> <PackageId>ZiggyCreatures.FusionCache.Chaos</PackageId> <PackageIcon>logo-128x128.png</PackageIcon> <Description>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.</Description> @@ -13,10 +13,10 @@ <DocumentationFile>ZiggyCreatures.FusionCache.Chaos.xml</DocumentationFile> <PackageReadmeFile>README.md</PackageReadmeFile> <PackageReleaseNotes> - - Added re-connection support in ChaosBackplane - - Added logging support in ChaosBackplane - - Added ChaosPlugin - - Dependencies update + - 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 </PackageReleaseNotes> </PropertyGroup> diff --git a/src/ZiggyCreatures.FusionCache.Chaos/ZiggyCreatures.FusionCache.Chaos.xml b/src/ZiggyCreatures.FusionCache.Chaos/ZiggyCreatures.FusionCache.Chaos.xml index cae5917c..d8c13fbd 100644 --- a/src/ZiggyCreatures.FusionCache.Chaos/ZiggyCreatures.FusionCache.Chaos.xml +++ b/src/ZiggyCreatures.FusionCache.Chaos/ZiggyCreatures.FusionCache.Chaos.xml @@ -16,53 +16,6 @@ <param name="innerBackplane">The actual <see cref="T:ZiggyCreatures.Caching.Fusion.Backplane.IFusionCacheBackplane"/> used if and when chaos does not happen.</param> <param name="logger">The logger to use, or <see langword="null"/>.</param> </member> - <member name="P:ZiggyCreatures.Caching.Fusion.Chaos.ChaosBackplane.ChaosThrowProbability"> - <summary> - A <see cref="T:System.Single"/> value from 0.0 to 1.0 that represents the probabilty of throwing an exception: set it to 0.0 to never throw or to 1.0 to always throw. - </summary> - </member> - <member name="P:ZiggyCreatures.Caching.Fusion.Chaos.ChaosBackplane.ChaosMinDelay"> - <summary> - The minimum amount of randomized delay. - </summary> - </member> - <member name="P:ZiggyCreatures.Caching.Fusion.Chaos.ChaosBackplane.ChaosMaxDelay"> - <summary> - The maximum amount of randomized delay. - </summary> - </member> - <member name="M:ZiggyCreatures.Caching.Fusion.Chaos.ChaosBackplane.SetNeverThrow"> - <summary> - Force chaos exceptions to never be thrown. - </summary> - </member> - <member name="M:ZiggyCreatures.Caching.Fusion.Chaos.ChaosBackplane.SetAlwaysThrow"> - <summary> - Force chaos exceptions to always be thrown. - </summary> - </member> - <member name="M:ZiggyCreatures.Caching.Fusion.Chaos.ChaosBackplane.SetNeverDelay"> - <summary> - Force chaos delays to never happen. - </summary> - </member> - <member name="M:ZiggyCreatures.Caching.Fusion.Chaos.ChaosBackplane.SetAlwaysDelayExactly(System.TimeSpan)"> - <summary> - Force chaos delays to always be of exactly this amount. - </summary> - <param name="delay"></param> - </member> - <member name="M:ZiggyCreatures.Caching.Fusion.Chaos.ChaosBackplane.SetNeverChaos"> - <summary> - Force chaos exceptions and delays to never happen. - </summary> - </member> - <member name="M:ZiggyCreatures.Caching.Fusion.Chaos.ChaosBackplane.SetAlwaysChaos(System.TimeSpan)"> - <summary> - Force chaos exceptions to always throw, and chaos delays to always be of exactly this amount. - </summary> - <param name="delay"></param> - </member> <member name="M:ZiggyCreatures.Caching.Fusion.Chaos.ChaosBackplane.Publish(ZiggyCreatures.Caching.Fusion.Backplane.BackplaneMessage,ZiggyCreatures.Caching.Fusion.FusionCacheEntryOptions,System.Threading.CancellationToken)"> <inheritdoc/> </member> @@ -75,63 +28,20 @@ <member name="M:ZiggyCreatures.Caching.Fusion.Chaos.ChaosBackplane.Unsubscribe"> <inheritdoc/> </member> + <member name="M:ZiggyCreatures.Caching.Fusion.Chaos.ChaosBackplane.SetNeverThrow"> + <inheritdoc/> + </member> <member name="T:ZiggyCreatures.Caching.Fusion.Chaos.ChaosDistributedCache"> <summary> An implementation of <see cref="T:Microsoft.Extensions.Caching.Distributed.IDistributedCache"/> that acts on behalf of another one, but with a (controllable) amount of chaos in-between. </summary> </member> - <member name="M:ZiggyCreatures.Caching.Fusion.Chaos.ChaosDistributedCache.#ctor(Microsoft.Extensions.Caching.Distributed.IDistributedCache)"> + <member name="M:ZiggyCreatures.Caching.Fusion.Chaos.ChaosDistributedCache.#ctor(Microsoft.Extensions.Caching.Distributed.IDistributedCache,Microsoft.Extensions.Logging.ILogger{ZiggyCreatures.Caching.Fusion.Chaos.ChaosDistributedCache})"> <summary> Initializes a new instance of the ChaosDistributedCache class. </summary> <param name="innerCache">The actual <see cref="T:Microsoft.Extensions.Caching.Distributed.IDistributedCache"/> used if and when chaos does not happen.</param> - </member> - <member name="P:ZiggyCreatures.Caching.Fusion.Chaos.ChaosDistributedCache.ChaosThrowProbability"> - <summary> - A <see cref="T:System.Single"/> value from 0.0 to 1.0 that represents the probabilty of throwing an exception: set it to 0.0 to never throw or to 1.0 to always throw. - </summary> - </member> - <member name="P:ZiggyCreatures.Caching.Fusion.Chaos.ChaosDistributedCache.ChaosMinDelay"> - <summary> - The minimum amount of randomized delay. - </summary> - </member> - <member name="P:ZiggyCreatures.Caching.Fusion.Chaos.ChaosDistributedCache.ChaosMaxDelay"> - <summary> - The maximum amount of randomized delay. - </summary> - </member> - <member name="M:ZiggyCreatures.Caching.Fusion.Chaos.ChaosDistributedCache.SetNeverThrow"> - <summary> - Force chaos exceptions to never be thrown. - </summary> - </member> - <member name="M:ZiggyCreatures.Caching.Fusion.Chaos.ChaosDistributedCache.SetAlwaysThrow"> - <summary> - Force chaos exceptions to always be thrown. - </summary> - </member> - <member name="M:ZiggyCreatures.Caching.Fusion.Chaos.ChaosDistributedCache.SetNeverDelay"> - <summary> - Force chaos delays to never happen. - </summary> - </member> - <member name="M:ZiggyCreatures.Caching.Fusion.Chaos.ChaosDistributedCache.SetAlwaysDelayExactly(System.TimeSpan)"> - <summary> - Force chaos delays to always be of exactly this amount. - </summary> - <param name="delay"></param> - </member> - <member name="M:ZiggyCreatures.Caching.Fusion.Chaos.ChaosDistributedCache.SetNeverChaos"> - <summary> - Force chaos exceptions and delays to never happen. - </summary> - </member> - <member name="M:ZiggyCreatures.Caching.Fusion.Chaos.ChaosDistributedCache.SetAlwaysChaos(System.TimeSpan)"> - <summary> - Force chaos exceptions to always throw, and chaos delays to always be of exactly this amount. - </summary> - <param name="delay"></param> + <param name="logger">The logger to use, or <see langword="null"/>.</param> </member> <member name="M:ZiggyCreatures.Caching.Fusion.Chaos.ChaosDistributedCache.Get(System.String)"> <inheritdoc/> @@ -186,194 +96,201 @@ An implementation of <see cref="T:ZiggyCreatures.Caching.Fusion.Plugins.IFusionCachePlugin"/> with a (controllable) amount of chaos in-between. </summary> </member> - <member name="M:ZiggyCreatures.Caching.Fusion.Chaos.ChaosPlugin.#ctor(ZiggyCreatures.Caching.Fusion.Plugins.IFusionCachePlugin)"> + <member name="M:ZiggyCreatures.Caching.Fusion.Chaos.ChaosPlugin.#ctor(ZiggyCreatures.Caching.Fusion.Plugins.IFusionCachePlugin,Microsoft.Extensions.Logging.ILogger{ZiggyCreatures.Caching.Fusion.Chaos.ChaosPlugin})"> <summary> Initializes a new instance of the ChaosPlugin class. </summary> <param name="innerPlugin">The actual <see cref="T:ZiggyCreatures.Caching.Fusion.Plugins.IFusionCachePlugin"/> used if and when chaos does not happen.</param> + <param name="logger">The logger to use, or <see langword="null"/>.</param> + </member> + <member name="M:ZiggyCreatures.Caching.Fusion.Chaos.ChaosPlugin.Start(ZiggyCreatures.Caching.Fusion.IFusionCache)"> + <inheritdoc/> </member> - <member name="P:ZiggyCreatures.Caching.Fusion.Chaos.ChaosPlugin.ChaosThrowProbability"> + <member name="M:ZiggyCreatures.Caching.Fusion.Chaos.ChaosPlugin.Stop(ZiggyCreatures.Caching.Fusion.IFusionCache)"> + <inheritdoc/> + </member> + <member name="T:ZiggyCreatures.Caching.Fusion.Chaos.ChaosSerializer"> <summary> - A <see cref="T:System.Single"/> value from 0.0 to 1.0 that represents the probabilty of throwing an exception: set it to 0.0 to never throw or to 1.0 to always throw. + An implementation of <see cref="T:ZiggyCreatures.Caching.Fusion.Serialization.IFusionCacheSerializer"/> that acts on behalf of another one, but with a (controllable) amount of chaos in-between. </summary> </member> - <member name="P:ZiggyCreatures.Caching.Fusion.Chaos.ChaosPlugin.ChaosMinDelay"> + <member name="M:ZiggyCreatures.Caching.Fusion.Chaos.ChaosSerializer.#ctor(ZiggyCreatures.Caching.Fusion.Serialization.IFusionCacheSerializer,Microsoft.Extensions.Logging.ILogger{ZiggyCreatures.Caching.Fusion.Chaos.ChaosSerializer})"> <summary> - The minimum amount of randomized delay. + Initializes a new instance of the ChaosSerializer class. </summary> + <param name="innerSerializer">The actual <see cref="T:ZiggyCreatures.Caching.Fusion.Serialization.IFusionCacheSerializer"/> used if and when chaos does not happen.</param> + <param name="logger">The logger to use, or <see langword="null"/>.</param> + </member> + <member name="M:ZiggyCreatures.Caching.Fusion.Chaos.ChaosSerializer.Deserialize``1(System.Byte[])"> + <inheritdoc/> </member> - <member name="P:ZiggyCreatures.Caching.Fusion.Chaos.ChaosPlugin.ChaosMaxDelay"> + <member name="M:ZiggyCreatures.Caching.Fusion.Chaos.ChaosSerializer.DeserializeAsync``1(System.Byte[])"> + <inheritdoc/> + </member> + <member name="M:ZiggyCreatures.Caching.Fusion.Chaos.ChaosSerializer.Serialize``1(``0)"> + <inheritdoc/> + </member> + <member name="M:ZiggyCreatures.Caching.Fusion.Chaos.ChaosSerializer.SerializeAsync``1(``0)"> + <inheritdoc/> + </member> + <member name="T:ZiggyCreatures.Caching.Fusion.Chaos.FusionCacheChaosUtils"> <summary> - The maximum amount of randomized delay. + Various utils to work with randomized controllable chaos. </summary> </member> - <member name="M:ZiggyCreatures.Caching.Fusion.Chaos.ChaosPlugin.SetNeverThrow"> + <member name="M:ZiggyCreatures.Caching.Fusion.Chaos.FusionCacheChaosUtils.ShouldThrow(System.Single)"> <summary> - Force chaos exceptions to never be thrown. + Determines if an exception should be thrown. </summary> + <param name="throwProbability">The probabilty that an exception will be thrown.</param> + <returns><see langword="true"/> if an exception should be thrown, <see langword="false"/> otherwise.</returns> </member> - <member name="M:ZiggyCreatures.Caching.Fusion.Chaos.ChaosPlugin.SetAlwaysThrow"> + <member name="M:ZiggyCreatures.Caching.Fusion.Chaos.FusionCacheChaosUtils.MaybeThrow(System.Single)"> <summary> - Force chaos exceptions to always be thrown. + Maybe throw a <see cref="T:ZiggyCreatures.Caching.Fusion.Chaos.ChaosException"/> based on the specified probabilty. </summary> + <param name="throwProbability">The probabilty that an exception will be thrown.</param> </member> - <member name="M:ZiggyCreatures.Caching.Fusion.Chaos.ChaosPlugin.SetNeverDelay"> + <member name="M:ZiggyCreatures.Caching.Fusion.Chaos.FusionCacheChaosUtils.GetRandomDelay(System.TimeSpan,System.TimeSpan)"> <summary> - Force chaos delays to never happen. + Randomize an actual delay with a value between <paramref name="minDelay"/> and <paramref name="maxDelay"/>. </summary> + <param name="minDelay">The minimun amount of delay.</param> + <param name="maxDelay">The maximum amount of delay.</param> + <returns>The randomized delay.</returns> </member> - <member name="M:ZiggyCreatures.Caching.Fusion.Chaos.ChaosPlugin.SetAlwaysDelayExactly(System.TimeSpan)"> + <member name="M:ZiggyCreatures.Caching.Fusion.Chaos.FusionCacheChaosUtils.MaybeDelay(System.TimeSpan,System.TimeSpan,System.Threading.CancellationToken)"> <summary> - Force chaos delays to always be of exactly this amount. + Randomize an actual delay with a value between <paramref name="minDelay"/> and <paramref name="maxDelay"/>, and waits for it. </summary> - <param name="delay"></param> + <param name="minDelay">The minimun amount of delay.</param> + <param name="maxDelay">The maximum amount of delay.</param> + <param name="token">The cancellation token.</param> </member> - <member name="M:ZiggyCreatures.Caching.Fusion.Chaos.ChaosPlugin.SetNeverChaos"> + <member name="M:ZiggyCreatures.Caching.Fusion.Chaos.FusionCacheChaosUtils.MaybeDelayAsync(System.TimeSpan,System.TimeSpan,System.Threading.CancellationToken)"> <summary> - Force chaos exceptions and delays to never happen. + Randomize an actual delay with a value between <paramref name="minDelay"/> and <paramref name="maxDelay"/>, and waits for it. </summary> + <param name="minDelay">The minimun amount of delay.</param> + <param name="maxDelay">The maximum amount of delay.</param> + <param name="token">The cancellation token.</param> + <returns>A <see cref="T:System.Threading.Tasks.Task"/> instance to await.</returns> </member> - <member name="M:ZiggyCreatures.Caching.Fusion.Chaos.ChaosPlugin.SetAlwaysChaos(System.TimeSpan)"> + <member name="M:ZiggyCreatures.Caching.Fusion.Chaos.FusionCacheChaosUtils.MaybeChaos(System.TimeSpan,System.TimeSpan,System.Single,System.Threading.CancellationToken)"> <summary> - Force chaos exceptions to always throw, and chaos delays to always be of exactly this amount. + Randomize an actual delay with a value between <paramref name="minDelay"/> and <paramref name="maxDelay"/>, and waits for it. + Then, maybe, throw a <see cref="T:ZiggyCreatures.Caching.Fusion.Chaos.ChaosException"/> based on the specified probabilty. </summary> - <param name="delay"></param> - </member> - <member name="M:ZiggyCreatures.Caching.Fusion.Chaos.ChaosPlugin.Start(ZiggyCreatures.Caching.Fusion.IFusionCache)"> - <inheritdoc/> - </member> - <member name="M:ZiggyCreatures.Caching.Fusion.Chaos.ChaosPlugin.Stop(ZiggyCreatures.Caching.Fusion.IFusionCache)"> - <inheritdoc/> + <param name="minDelay">The minimun amount of delay.</param> + <param name="maxDelay">The maximum amount of delay.</param> + <param name="throwProbability">The probabilty that an exception will be thrown.</param> + <param name="token">The cancellation token.</param> </member> - <member name="T:ZiggyCreatures.Caching.Fusion.Chaos.ChaosSerializer"> + <member name="M:ZiggyCreatures.Caching.Fusion.Chaos.FusionCacheChaosUtils.MaybeChaosAsync(System.TimeSpan,System.TimeSpan,System.Single,System.Threading.CancellationToken)"> <summary> - An implementation of <see cref="T:ZiggyCreatures.Caching.Fusion.Serialization.IFusionCacheSerializer"/> that acts on behalf of another one, but with a (controllable) amount of chaos in-between. + Randomize an actual delay with a value between <paramref name="minDelay"/> and <paramref name="maxDelay"/>, and waits for it. + Then, maybe, throw a <see cref="T:ZiggyCreatures.Caching.Fusion.Chaos.ChaosException"/> based on the specified probabilty. </summary> + <param name="minDelay">The minimun amount of delay.</param> + <param name="maxDelay">The maximum amount of delay.</param> + <param name="throwProbability">The probabilty that an exception will be thrown.</param> + <param name="token">The cancellation token.</param> + <returns>A <see cref="T:System.Threading.Tasks.Task"/> instance to await.</returns> </member> - <member name="M:ZiggyCreatures.Caching.Fusion.Chaos.ChaosSerializer.#ctor(ZiggyCreatures.Caching.Fusion.Serialization.IFusionCacheSerializer)"> + <member name="T:ZiggyCreatures.Caching.Fusion.Chaos.Internals.AbstractChaosComponent"> <summary> - Initializes a new instance of the ChaosSerializer class. + A base implementation for a component with a controllable amount of chaos. </summary> - <param name="innerSerializer">The actual <see cref="T:ZiggyCreatures.Caching.Fusion.Serialization.IFusionCacheSerializer"/> used if and when chaos does not happen.</param> </member> - <member name="P:ZiggyCreatures.Caching.Fusion.Chaos.ChaosSerializer.ChaosThrowProbability"> + <member name="F:ZiggyCreatures.Caching.Fusion.Chaos.Internals.AbstractChaosComponent._logger"> <summary> - A <see cref="T:System.Single"/> value from 0.0 to 1.0 that represents the probabilty of throwing an exception: set it to 0.0 to never throw or to 1.0 to always throw. + The <see cref="T:Microsoft.Extensions.Logging.ILogger"/> to use, or <see langword="null"/> for no logging. </summary> </member> - <member name="P:ZiggyCreatures.Caching.Fusion.Chaos.ChaosSerializer.ChaosMinDelay"> + <member name="M:ZiggyCreatures.Caching.Fusion.Chaos.Internals.AbstractChaosComponent.#ctor(Microsoft.Extensions.Logging.ILogger)"> <summary> - The minimum amount of randomized delay. + Initializes a new instance of the AbstractChaosComponent class. </summary> + <param name="logger">The logger to use, or <see langword="null"/>.</param> </member> - <member name="P:ZiggyCreatures.Caching.Fusion.Chaos.ChaosSerializer.ChaosMaxDelay"> + <member name="P:ZiggyCreatures.Caching.Fusion.Chaos.Internals.AbstractChaosComponent.ChaosMaxDelay"> <summary> The maximum amount of randomized delay. </summary> </member> - <member name="M:ZiggyCreatures.Caching.Fusion.Chaos.ChaosSerializer.SetNeverThrow"> + <member name="P:ZiggyCreatures.Caching.Fusion.Chaos.Internals.AbstractChaosComponent.ChaosMinDelay"> <summary> - Force chaos exceptions to never be thrown. + The minimum amount of randomized delay. </summary> </member> - <member name="M:ZiggyCreatures.Caching.Fusion.Chaos.ChaosSerializer.SetAlwaysThrow"> + <member name="P:ZiggyCreatures.Caching.Fusion.Chaos.Internals.AbstractChaosComponent.ChaosThrowProbability"> <summary> - Force chaos exceptions to always be thrown. + A <see cref="T:System.Single"/> value from 0.0 to 1.0 that represents the probabilty of throwing an exception: set it to 0.0 to never throw or to 1.0 to always throw. </summary> </member> - <member name="M:ZiggyCreatures.Caching.Fusion.Chaos.ChaosSerializer.SetNeverDelay"> + <member name="M:ZiggyCreatures.Caching.Fusion.Chaos.Internals.AbstractChaosComponent.SetAlwaysDelay(System.TimeSpan,System.TimeSpan)"> <summary> - Force chaos delays to never happen. + Force chaos delays to always be between certain amounts. </summary> + <param name="minDelay">The minimum amount of delay.</param> + <param name="maxDelay">The maximum amount of delay.</param> </member> - <member name="M:ZiggyCreatures.Caching.Fusion.Chaos.ChaosSerializer.SetAlwaysDelayExactly(System.TimeSpan)"> + <member name="M:ZiggyCreatures.Caching.Fusion.Chaos.Internals.AbstractChaosComponent.SetAlwaysDelayExactly(System.TimeSpan)"> <summary> Force chaos delays to always be of exactly this amount. </summary> - <param name="delay"></param> + <param name="delay">The amount of delay.</param> </member> - <member name="M:ZiggyCreatures.Caching.Fusion.Chaos.ChaosSerializer.SetNeverChaos"> + <member name="M:ZiggyCreatures.Caching.Fusion.Chaos.Internals.AbstractChaosComponent.SetAlwaysThrow"> <summary> - Force chaos exceptions and delays to never happen. + Force chaos exceptions to always be thrown. </summary> </member> - <member name="M:ZiggyCreatures.Caching.Fusion.Chaos.ChaosSerializer.SetAlwaysChaos(System.TimeSpan)"> + <member name="M:ZiggyCreatures.Caching.Fusion.Chaos.Internals.AbstractChaosComponent.SetAlwaysChaos(System.TimeSpan)"> <summary> Force chaos exceptions to always throw, and chaos delays to always be of exactly this amount. </summary> - <param name="delay"></param> - </member> - <member name="M:ZiggyCreatures.Caching.Fusion.Chaos.ChaosSerializer.Deserialize``1(System.Byte[])"> - <inheritdoc/> - </member> - <member name="M:ZiggyCreatures.Caching.Fusion.Chaos.ChaosSerializer.DeserializeAsync``1(System.Byte[])"> - <inheritdoc/> - </member> - <member name="M:ZiggyCreatures.Caching.Fusion.Chaos.ChaosSerializer.Serialize``1(``0)"> - <inheritdoc/> - </member> - <member name="M:ZiggyCreatures.Caching.Fusion.Chaos.ChaosSerializer.SerializeAsync``1(``0)"> - <inheritdoc/> - </member> - <member name="T:ZiggyCreatures.Caching.Fusion.Chaos.FusionCacheChaosUtils"> - <summary> - Various utils to work with randomized controllable chaos. - </summary> + <param name="delay">The amount of delay.</param> </member> - <member name="M:ZiggyCreatures.Caching.Fusion.Chaos.FusionCacheChaosUtils.ShouldCreateChaos(System.Single)"> + <member name="M:ZiggyCreatures.Caching.Fusion.Chaos.Internals.AbstractChaosComponent.SetAlwaysChaos(System.TimeSpan,System.TimeSpan)"> <summary> - Determines if an exception should be thrown. + Force chaos exceptions to always throw, and chaos delays to always be between certain amounts. </summary> - <param name="throwProbability">The probabilty that an exception will be thrown.</param> - <returns><see langword="true"/> if an exception should be thrown, <see langword="false"/> otherwise.</returns> + <param name="minDelay">The minimum amount of delay.</param> + <param name="maxDelay">The maximum amount of delay.</param> </member> - <member name="M:ZiggyCreatures.Caching.Fusion.Chaos.FusionCacheChaosUtils.MaybeThrow(System.Single)"> + <member name="M:ZiggyCreatures.Caching.Fusion.Chaos.Internals.AbstractChaosComponent.SetNeverChaos"> <summary> - Maybe throw a <see cref="T:ZiggyCreatures.Caching.Fusion.Chaos.ChaosException"/> based on the specified probabilty. + Force chaos exceptions and delays to never happen. </summary> - <param name="throwProbability">The probabilty that an exception will be thrown.</param> </member> - <member name="M:ZiggyCreatures.Caching.Fusion.Chaos.FusionCacheChaosUtils.RandomizeDelay(System.TimeSpan,System.TimeSpan)"> + <member name="M:ZiggyCreatures.Caching.Fusion.Chaos.Internals.AbstractChaosComponent.SetNeverDelay"> <summary> - Randomize an actual delay with a value between <paramref name="minDelay"/> and <paramref name="maxDelay"/>. + Force chaos delays to never happen. </summary> - <param name="minDelay">The minimun amount of delay.</param> - <param name="maxDelay">The maximum amount of delay.</param> - <returns>The randomized delay.</returns> </member> - <member name="M:ZiggyCreatures.Caching.Fusion.Chaos.FusionCacheChaosUtils.MaybeDelay(System.TimeSpan,System.TimeSpan)"> + <member name="M:ZiggyCreatures.Caching.Fusion.Chaos.Internals.AbstractChaosComponent.SetNeverThrow"> <summary> - Randomize an actual delay with a value between <paramref name="minDelay"/> and <paramref name="maxDelay"/>, and waits for it. + Force chaos exceptions to never be thrown. </summary> - <param name="minDelay">The minimun amount of delay.</param> - <param name="maxDelay">The maximum amount of delay.</param> </member> - <member name="M:ZiggyCreatures.Caching.Fusion.Chaos.FusionCacheChaosUtils.MaybeDelayAsync(System.TimeSpan,System.TimeSpan)"> + <member name="M:ZiggyCreatures.Caching.Fusion.Chaos.Internals.AbstractChaosComponent.ShouldThrow"> <summary> - Randomize an actual delay with a value between <paramref name="minDelay"/> and <paramref name="maxDelay"/>, and waits for it. + Determines if an exception should be thrown. </summary> - <param name="minDelay">The minimun amount of delay.</param> - <param name="maxDelay">The maximum amount of delay.</param> - <returns>A <see cref="T:System.Threading.Tasks.Task"/> instance to await.</returns> + <returns><see langword="true"/> if an exception should be thrown, <see langword="false"/> otherwise.</returns> </member> - <member name="M:ZiggyCreatures.Caching.Fusion.Chaos.FusionCacheChaosUtils.MaybeChaos(System.TimeSpan,System.TimeSpan,System.Single)"> + <member name="M:ZiggyCreatures.Caching.Fusion.Chaos.Internals.AbstractChaosComponent.MaybeChaos(System.Threading.CancellationToken)"> <summary> - Randomize an actual delay with a value between <paramref name="minDelay"/> and <paramref name="maxDelay"/>, and waits for it. + Randomize an actual delay with a value between the configured min/max values, and if needed waits for it. Then, maybe, throw a <see cref="T:ZiggyCreatures.Caching.Fusion.Chaos.ChaosException"/> based on the specified probabilty. </summary> - <param name="throwProbability">The probabilty that an exception will be thrown.</param> - <param name="minDelay">The minimun amount of delay.</param> - <param name="maxDelay">The maximum amount of delay.</param> </member> - <member name="M:ZiggyCreatures.Caching.Fusion.Chaos.FusionCacheChaosUtils.MaybeChaosAsync(System.TimeSpan,System.TimeSpan,System.Single)"> + <member name="M:ZiggyCreatures.Caching.Fusion.Chaos.Internals.AbstractChaosComponent.MaybeChaosAsync(System.Threading.CancellationToken)"> <summary> - Randomize an actual delay with a value between <paramref name="minDelay"/> and <paramref name="maxDelay"/>, and waits for it. + Randomize an actual delay with a value between the configured min/max values, and if needed waits for it. Then, maybe, throw a <see cref="T:ZiggyCreatures.Caching.Fusion.Chaos.ChaosException"/> based on the specified probabilty. </summary> - <param name="throwProbability">The probabilty that an exception will be thrown.</param> - <param name="minDelay">The minimun amount of delay.</param> - <param name="maxDelay">The maximum amount of delay.</param> - <returns>A <see cref="T:System.Threading.Tasks.Task"/> instance to await.</returns> </member> </members> </doc> diff --git a/src/ZiggyCreatures.FusionCache.Serialization.CysharpMemoryPack/Internals/SerializableFusionCacheDistributedEntry.cs b/src/ZiggyCreatures.FusionCache.Serialization.CysharpMemoryPack/Internals/SerializableFusionCacheDistributedEntry.cs index 953ee090..91088c83 100644 --- a/src/ZiggyCreatures.FusionCache.Serialization.CysharpMemoryPack/Internals/SerializableFusionCacheDistributedEntry.cs +++ b/src/ZiggyCreatures.FusionCache.Serialization.CysharpMemoryPack/Internals/SerializableFusionCacheDistributedEntry.cs @@ -17,10 +17,10 @@ internal partial class SerializableFusionCacheDistributedEntry<TValue> public FusionCacheEntryMetadata? Metadata => Entry?.Metadata; [MemoryPackInclude] - public long? Timestamp => Entry?.Timestamp; + public long Timestamp => Entry?.Timestamp ?? 0; [MemoryPackConstructor] - SerializableFusionCacheDistributedEntry(TValue value, FusionCacheEntryMetadata? metadata, long? timestamp) + SerializableFusionCacheDistributedEntry(TValue value, FusionCacheEntryMetadata? metadata, long timestamp) { this.Entry = new FusionCacheDistributedEntry<TValue>(value, metadata, timestamp); } diff --git a/src/ZiggyCreatures.FusionCache.Serialization.CysharpMemoryPack/ZiggyCreatures.Caching.Fusion.Serialization.CysharpMemoryPack.xml b/src/ZiggyCreatures.FusionCache.Serialization.CysharpMemoryPack/ZiggyCreatures.Caching.Fusion.Serialization.CysharpMemoryPack.xml index d4727069..2cddc09d 100644 --- a/src/ZiggyCreatures.FusionCache.Serialization.CysharpMemoryPack/ZiggyCreatures.Caching.Fusion.Serialization.CysharpMemoryPack.xml +++ b/src/ZiggyCreatures.FusionCache.Serialization.CysharpMemoryPack/ZiggyCreatures.Caching.Fusion.Serialization.CysharpMemoryPack.xml @@ -33,7 +33,7 @@ <code> <b>TValue</b> Value<br/> <b>ZiggyCreatures.Caching.Fusion.Internals.FusionCacheEntryMetadata</b> Metadata<br/> - <b>long?</b> Timestamp<br/> + <b>long</b> Timestamp<br/> </code> </remarks> </member> 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 1bd9caac..7fe4b8db 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 @@ <TargetFrameworks>netstandard2.1;net7.0</TargetFrameworks> <LangVersion>latest</LangVersion> <Nullable>enable</Nullable> - <Version>0.23.0</Version> + <Version>0.24.0</Version> <PackageId>ZiggyCreatures.FusionCache.Serialization.CysharpMemoryPack</PackageId> <PackageIcon>logo-128x128.png</PackageIcon> <Description>FusionCache serializer based on Cysharp's MemoryPack serializer</Description> @@ -13,7 +13,7 @@ <DocumentationFile>ZiggyCreatures.Caching.Fusion.Serialization.CysharpMemoryPack.xml</DocumentationFile> <PackageReadmeFile>README.md</PackageReadmeFile> <PackageReleaseNotes> - - Dependencies update + - Update: dependencies </PackageReleaseNotes> </PropertyGroup> @@ -23,7 +23,7 @@ </ItemGroup> <ItemGroup> - <PackageReference Include="MemoryPack" Version="1.9.16" /> + <PackageReference Include="MemoryPack" Version="1.10.0" /> </ItemGroup> <ItemGroup> 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 6bf2ca4f..14dad52f 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 @@ <TargetFramework>netstandard2.0</TargetFramework> <LangVersion>latest</LangVersion> <Nullable>enable</Nullable> - <Version>0.23.0</Version> + <Version>0.24.0</Version> <PackageId>ZiggyCreatures.FusionCache.Serialization.NeueccMessagePack</PackageId> <PackageIcon>logo-128x128.png</PackageIcon> <Description>FusionCache serializer based on Neuecc's MessagePack serializer</Description> @@ -13,7 +13,7 @@ <DocumentationFile>ZiggyCreatures.FusionCache.Serialization.NeueccMessagePack.xml</DocumentationFile> <PackageReadmeFile>README.md</PackageReadmeFile> <PackageReleaseNotes> - - Dependencies update + - Update: dependencies </PackageReleaseNotes> </PropertyGroup> @@ -23,7 +23,7 @@ </ItemGroup> <ItemGroup> - <PackageReference Include="MessagePack" Version="2.5.124" /> + <PackageReference Include="MessagePack" Version="2.5.129" /> </ItemGroup> <ItemGroup> 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 e3e6bb13..47c6dc85 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 @@ <TargetFramework>netstandard2.0</TargetFramework> <LangVersion>latest</LangVersion> <Nullable>enable</Nullable> - <Version>0.23.0</Version> + <Version>0.24.0</Version> <PackageId>ZiggyCreatures.FusionCache.Serialization.NewtonsoftJson</PackageId> <PackageIcon>logo-128x128.png</PackageIcon> <Description>FusionCache serializer based on Newtonsoft Json.NET</Description> @@ -13,7 +13,7 @@ <DocumentationFile>ZiggyCreatures.FusionCache.Serialization.NewtonsoftJson.xml</DocumentationFile> <PackageReadmeFile>README.md</PackageReadmeFile> <PackageReleaseNotes> - - Dependencies update + - Update: dependencies </PackageReleaseNotes> </PropertyGroup> diff --git a/src/ZiggyCreatures.FusionCache.Serialization.ProtoBufNet/FusionCacheProtoBufNetSerializer.cs b/src/ZiggyCreatures.FusionCache.Serialization.ProtoBufNet/FusionCacheProtoBufNetSerializer.cs index 528813e8..f4a4afbf 100644 --- a/src/ZiggyCreatures.FusionCache.Serialization.ProtoBufNet/FusionCacheProtoBufNetSerializer.cs +++ b/src/ZiggyCreatures.FusionCache.Serialization.ProtoBufNet/FusionCacheProtoBufNetSerializer.cs @@ -112,11 +112,15 @@ private void MaybeRegisterDistributedEntryModel<T>() /// <inheritdoc /> public byte[] Serialize<T>(T? obj) { + //Debug.WriteLine($"{DateTime.UtcNow.ToString("yyyy-MM-dd'T'HH:mm:ss.fffK", CultureInfo.InvariantCulture)} before MaybeRegisterDistributedEntryModel"); MaybeRegisterDistributedEntryModel<T>(); + //Debug.WriteLine($"{DateTime.UtcNow.ToString("yyyy-MM-dd'T'HH:mm:ss.fffK", CultureInfo.InvariantCulture)} after MaybeRegisterDistributedEntryModel"); using (var stream = new MemoryStream()) { + //Debug.WriteLine($"{DateTime.UtcNow.ToString("yyyy-MM-dd'T'HH:mm:ss.fffK", CultureInfo.InvariantCulture)} before Serialize"); _model.Serialize(stream, obj); + //Debug.WriteLine($"{DateTime.UtcNow.ToString("yyyy-MM-dd'T'HH:mm:ss.fffK", CultureInfo.InvariantCulture)} before Serialize"); return stream.ToArray(); } } 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 da083a6b..20b0b46a 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 @@ <TargetFramework>netstandard2.0</TargetFramework> <LangVersion>latest</LangVersion> <Nullable>enable</Nullable> - <Version>0.23.0</Version> + <Version>0.24.0</Version> <PackageId>ZiggyCreatures.FusionCache.Serialization.ProtoBufNet</PackageId> <PackageIcon>logo-128x128.png</PackageIcon> <Description>FusionCache serializer based on protobuf-net</Description> @@ -13,7 +13,7 @@ <DocumentationFile>ZiggyCreatures.FusionCache.Serialization.ProtoBufNet.xml</DocumentationFile> <PackageReadmeFile>README.md</PackageReadmeFile> <PackageReleaseNotes> - - Dependencies update + - Update: dependencies </PackageReleaseNotes> </PropertyGroup> 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 de0668ec..bae02a36 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 @@ <TargetFramework>netstandard2.0</TargetFramework> <LangVersion>latest</LangVersion> <Nullable>enable</Nullable> - <Version>0.23.0</Version> + <Version>0.24.0</Version> <PackageId>ZiggyCreatures.FusionCache.Serialization.ServiceStackJson</PackageId> <PackageIcon>logo-128x128.png</PackageIcon> <Description>FusionCache serializer based on ServiceStack's Json serializer</Description> @@ -13,7 +13,7 @@ <DocumentationFile>ZiggyCreatures.FusionCache.Serialization.ServiceStackJson.xml</DocumentationFile> <PackageReadmeFile>README.md</PackageReadmeFile> <PackageReleaseNotes> - - Dependencies update + - Update: dependencies </PackageReleaseNotes> </PropertyGroup> @@ -27,7 +27,7 @@ </ItemGroup> <ItemGroup> - <PackageReference Include="ServiceStack.Text" Version="6.10.0" /> + <PackageReference Include="ServiceStack.Text" Version="6.11.0" /> </ItemGroup> </Project> 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 35ebaa4d..e1707395 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 @@ <TargetFramework>netstandard2.0</TargetFramework> <LangVersion>latest</LangVersion> <Nullable>enable</Nullable> - <Version>0.23.0</Version> + <Version>0.24.0</Version> <PackageId>ZiggyCreatures.FusionCache.Serialization.SystemTextJson</PackageId> <PackageIcon>logo-128x128.png</PackageIcon> <Description>FusionCache serializer based on System.Text.Json</Description> @@ -13,7 +13,7 @@ <DocumentationFile>ZiggyCreatures.FusionCache.Serialization.SystemTextJson.xml</DocumentationFile> <PackageReadmeFile>README.md</PackageReadmeFile> <PackageReleaseNotes> - - Dependencies update + - Update: dependencies </PackageReleaseNotes> </PropertyGroup> diff --git a/src/ZiggyCreatures.FusionCache/Backplane/BackplaneConnectionInfo.cs b/src/ZiggyCreatures.FusionCache/Backplane/BackplaneConnectionInfo.cs index 38ab89cb..bd6a4783 100644 --- a/src/ZiggyCreatures.FusionCache/Backplane/BackplaneConnectionInfo.cs +++ b/src/ZiggyCreatures.FusionCache/Backplane/BackplaneConnectionInfo.cs @@ -1,22 +1,21 @@ -namespace ZiggyCreatures.Caching.Fusion.Backplane +namespace ZiggyCreatures.Caching.Fusion.Backplane; + +/// <summary> +/// A struct containing information about a backplane connection or re-connection. +/// </summary> +public class BackplaneConnectionInfo { /// <summary> - /// A struct containing information about a backplane connection or re-connection. + /// Creates a new <see cref="BackplaneConnectionInfo"/> instance. /// </summary> - public readonly struct BackplaneConnectionInfo + /// <param name="isReconnection"></param> + public BackplaneConnectionInfo(bool isReconnection) { - /// <summary> - /// Creates a new <see cref="BackplaneConnectionInfo"/> instance. - /// </summary> - /// <param name="isReconnection"></param> - public BackplaneConnectionInfo(bool isReconnection) - { - IsReconnection = isReconnection; - } - - /// <summary> - /// If set to <see langword="true"/>, the connection is a re-connection. - /// </summary> - public readonly bool IsReconnection { get; } + IsReconnection = isReconnection; } + + /// <summary> + /// If set to <see langword="true"/>, the connection is a re-connection. + /// </summary> + public bool IsReconnection { get; } } diff --git a/src/ZiggyCreatures.FusionCache/Backplane/BackplaneMessage.cs b/src/ZiggyCreatures.FusionCache/Backplane/BackplaneMessage.cs index 4fffce7a..0aaee1a1 100644 --- a/src/ZiggyCreatures.FusionCache/Backplane/BackplaneMessage.cs +++ b/src/ZiggyCreatures.FusionCache/Backplane/BackplaneMessage.cs @@ -1,4 +1,7 @@ using System; +using System.Buffers.Binary; +using System.Text; +using ZiggyCreatures.Caching.Fusion.Internals; namespace ZiggyCreatures.Caching.Fusion.Backplane; @@ -7,14 +10,23 @@ namespace ZiggyCreatures.Caching.Fusion.Backplane; /// </summary> public class BackplaneMessage { + private static readonly Encoding _encoding = Encoding.UTF8; + /// <summary> /// Creates a new instance of a backplane message. /// </summary> - ///// <param name="sourceId">The InstanceId of the source cache.</param> - ///// <param name="instantTicks">The instant this message is related to, expressed as ticks amount. If null, DateTimeOffset.UtcNow.Ticks will be used.</param> public BackplaneMessage() { - InstantTicks = DateTimeOffset.UtcNow.Ticks; + Timestamp = FusionCacheInternalUtils.GetCurrentTimestamp(); + } + + /// <summary> + /// Creates a new instance of a backplane message. + /// </summary> + /// <param name="timestamp">The timestamp, or <see langword="null"/> to set it automatically to the current timestamp.</param> + public BackplaneMessage(long? timestamp) + { + Timestamp = timestamp ?? FusionCacheInternalUtils.GetCurrentTimestamp(); } /// <summary> @@ -23,9 +35,9 @@ public BackplaneMessage() public string? SourceId { get; set; } /// <summary> - /// The instant a message was related to, expressed as ticks amount. + /// The timestamp (in ticks) at a message has been created. /// </summary> - public long InstantTicks { get; set; } + public long Timestamp { get; set; } /// <summary> /// The action to broadcast to the backplane. @@ -46,7 +58,7 @@ public bool IsValid() if (string.IsNullOrEmpty(SourceId)) return false; - if (InstantTicks <= 0) + if (Timestamp <= 0) return false; switch (Action) @@ -65,15 +77,18 @@ public bool IsValid() /// <summary> /// Creates a message for a single cache entry set operation (via either a Set() or a GetOrSet() method call). /// </summary> + /// <param name="sourceId">The cache InstanceId of the source.</param> /// <param name="cacheKey">The cache key.</param> + /// <param name="timestamp">The timestamp.</param> /// <returns>The message.</returns> - public static BackplaneMessage CreateForEntrySet(string cacheKey) + public static BackplaneMessage CreateForEntrySet(string sourceId, string cacheKey, long? timestamp) { if (string.IsNullOrEmpty(cacheKey)) throw new ArgumentException("The cache key cannot be null or empty", nameof(cacheKey)); - return new BackplaneMessage() + return new BackplaneMessage(timestamp) { + SourceId = sourceId, Action = BackplaneMessageAction.EntrySet, CacheKey = cacheKey }; @@ -82,15 +97,18 @@ public static BackplaneMessage CreateForEntrySet(string cacheKey) /// <summary> /// Creates a message for a single cache entry remove (via a Remove() method call). /// </summary> + /// <param name="sourceId">The cache InstanceId of the source.</param> /// <param name="cacheKey">The cache key.</param> + /// <param name="timestamp">The timestamp.</param> /// <returns>The message.</returns> - public static BackplaneMessage CreateForEntryRemove(string cacheKey) + public static BackplaneMessage CreateForEntryRemove(string sourceId, string cacheKey, long? timestamp) { if (string.IsNullOrEmpty(cacheKey)) throw new ArgumentException("The cache key cannot be null or empty", nameof(cacheKey)); - return new BackplaneMessage() + return new BackplaneMessage(timestamp) { + SourceId = sourceId, Action = BackplaneMessageAction.EntryRemove, CacheKey = cacheKey }; @@ -99,17 +117,117 @@ public static BackplaneMessage CreateForEntryRemove(string cacheKey) /// <summary> /// Creates a message for a single cache entry expire operation (via an Expire() method call). /// </summary> + /// <param name="sourceId">The cache InstanceId of the source.</param> /// <param name="cacheKey">The cache key.</param> + /// <param name="timestamp">The timestamp.</param> /// <returns>The message.</returns> - public static BackplaneMessage CreateForEntryExpire(string cacheKey) + public static BackplaneMessage CreateForEntryExpire(string sourceId, string cacheKey, long? timestamp) { if (string.IsNullOrEmpty(cacheKey)) throw new ArgumentException("The cache key cannot be null or empty", nameof(cacheKey)); - return new BackplaneMessage() + return new BackplaneMessage(timestamp) { + SourceId = sourceId, Action = BackplaneMessageAction.EntryExpire, CacheKey = cacheKey }; } + + /// <summary> + /// Serializes a backplane message to a byte array. + /// </summary> + /// <param name="message">The backplane message to serialize.</param> + /// <returns></returns> + public static byte[] ToByteArray(BackplaneMessage? message) + { + if (message is null) + throw new ArgumentNullException(nameof(message)); + + var sourceIdByteCount = _encoding.GetByteCount(message.SourceId); + var cacheKeyByteCount = _encoding.GetByteCount(message.CacheKey); + + var size = + 1 // VERSION + + 4 + sourceIdByteCount // SOURCE ID + + 8 // INSTANCE TICKS + + 1 // ACTION + + 4 + cacheKeyByteCount // CACHE KEY + ; + + var res = new byte[size]; + var pos = 0; + + // VERSION + res[pos] = 0; + pos++; + + // SOURCE ID + BinaryPrimitives.WriteInt32LittleEndian(new Span<byte>(res, pos, 4), sourceIdByteCount); + pos += 4; + _encoding.GetBytes(message.SourceId!, 0, message.SourceId!.Length, res, pos); + pos += sourceIdByteCount; + + // TIMESTAMP + BinaryPrimitives.WriteInt64LittleEndian(new Span<byte>(res, pos, 8), message.Timestamp); + pos += 8; + + // ACTION + res[pos] = (byte)message.Action; + pos++; + + // CACHE KEY + BinaryPrimitives.WriteInt32LittleEndian(new Span<byte>(res, pos, 4), cacheKeyByteCount); + pos += 4; + _encoding.GetBytes(message.CacheKey, 0, message.CacheKey!.Length, res, pos); + //pos += cacheKeyByteCount; + + return res; + } + + /// <summary> + /// Deserializes a byte array into a backplane message. + /// </summary> + /// <param name="data">The byte array to deserialize.</param> + /// <returns>An instance of a backplane message, or <see langword="null"/></returns> + /// <exception cref="FormatException"></exception> + public static BackplaneMessage FromByteArray(byte[]? data) + { + if (data is null) + throw new ArgumentNullException(nameof(data)); + + if (data.Length == 0) + throw new InvalidOperationException("The byte array is empty."); + + var res = new BackplaneMessage(); + var pos = 0; + + // VERSION + var version = data[pos]; + if (version != 0) + throw new FormatException($"The backplane message version ({version}) is not supported."); + pos++; + + // SOURCE ID + var tmp = BinaryPrimitives.ReadInt32LittleEndian(new ReadOnlySpan<byte>(data, pos, 4)); + pos += 4; + res.SourceId = _encoding.GetString(data, pos, tmp); + pos += tmp; + + // TIMESTAMP + res.Timestamp = BinaryPrimitives.ReadInt64LittleEndian(new ReadOnlySpan<byte>(data, pos, 8)); + pos += 8; + + // ACTION + res.Action = (BackplaneMessageAction)data[pos]; + pos++; + + // CACHE KEY + tmp = BinaryPrimitives.ReadInt32LittleEndian(new ReadOnlySpan<byte>(data, pos, 4)); + pos += 4; + res.CacheKey = _encoding.GetString(data, pos, tmp); + //pos += tmp; + + return res; + } } diff --git a/src/ZiggyCreatures.FusionCache/Backplane/BackplaneSubscriptionOptions.cs b/src/ZiggyCreatures.FusionCache/Backplane/BackplaneSubscriptionOptions.cs index d70f5235..2d82ce1a 100644 --- a/src/ZiggyCreatures.FusionCache/Backplane/BackplaneSubscriptionOptions.cs +++ b/src/ZiggyCreatures.FusionCache/Backplane/BackplaneSubscriptionOptions.cs @@ -1,4 +1,5 @@ using System; +using System.ComponentModel; namespace ZiggyCreatures.Caching.Fusion.Backplane; @@ -7,7 +8,13 @@ namespace ZiggyCreatures.Caching.Fusion.Backplane; /// </summary> public class BackplaneSubscriptionOptions { - internal BackplaneSubscriptionOptions(string? channelName, Action<BackplaneConnectionInfo>? connectHandler, Action<BackplaneMessage>? incomingMessageHandler) + /// <summary> + /// Creates a new instance of a <see cref="BackplaneSubscriptionOptions"/>. + /// </summary> + /// <param name="channelName">The channel name to be used.</param> + /// <param name="connectHandler">The backplane connection handler that will be used when there's a connection (or reconnection).</param> + /// <param name="incomingMessageHandler">The backplane message handler that will be used to process incoming messages.</param> + public BackplaneSubscriptionOptions(string? channelName, Action<BackplaneConnectionInfo>? connectHandler, Action<BackplaneMessage>? incomingMessageHandler) { ChannelName = channelName; ConnectHandler = connectHandler; @@ -22,6 +29,7 @@ internal BackplaneSubscriptionOptions(string? channelName, Action<BackplaneConne /// <summary> /// The backplane message handler that will be used to process incoming messages. /// </summary> + [EditorBrowsable(EditorBrowsableState.Never)] [Obsolete("Please use MessageHandler instead.")] public Action<BackplaneMessage>? Handler { @@ -29,12 +37,12 @@ public Action<BackplaneMessage>? Handler } /// <summary> - /// The backplane message handler that will be used to process incoming messages. + /// The backplane connection handler that will be used when there's a connection (or reconnection). /// </summary> - public Action<BackplaneMessage>? IncomingMessageHandler { get; } + public Action<BackplaneConnectionInfo>? ConnectHandler { get; } /// <summary> - /// The backplane connection handler that will be used when there's a connection (or reconnection). + /// The backplane message handler that will be used to process incoming messages. /// </summary> - public Action<BackplaneConnectionInfo>? ConnectHandler { get; } + public Action<BackplaneMessage>? IncomingMessageHandler { get; } } diff --git a/src/ZiggyCreatures.FusionCache/Events/FusionCacheBackplaneMessageEventArgs.cs b/src/ZiggyCreatures.FusionCache/Events/FusionCacheBackplaneMessageEventArgs.cs index bdacf229..a363e5ac 100644 --- a/src/ZiggyCreatures.FusionCache/Events/FusionCacheBackplaneMessageEventArgs.cs +++ b/src/ZiggyCreatures.FusionCache/Events/FusionCacheBackplaneMessageEventArgs.cs @@ -4,21 +4,21 @@ namespace ZiggyCreatures.Caching.Fusion.Events; /// <summary> -/// The specific <see cref="EventArgs"/> object for events related to cache entries (eg: with a cache key). +/// The specific <see cref="EventArgs"/> object for events related to backplane messages, either published or received. /// </summary> public class FusionCacheBackplaneMessageEventArgs : EventArgs { /// <summary> /// Initializes a new instance of the <see cref="FusionCacheBackplaneMessageEventArgs"/> class. /// </summary> - /// <param name="message">The backplane message received</param> + /// <param name="message">The backplane message.</param> public FusionCacheBackplaneMessageEventArgs(BackplaneMessage message) { Message = message; } /// <summary> - /// The backplane message received. + /// The backplane message. /// </summary> public BackplaneMessage Message { get; } } diff --git a/src/ZiggyCreatures.FusionCache/Events/FusionCacheCircuitBreakerChangeEventArgs.cs b/src/ZiggyCreatures.FusionCache/Events/FusionCacheCircuitBreakerChangeEventArgs.cs index 065e34be..c24dc877 100644 --- a/src/ZiggyCreatures.FusionCache/Events/FusionCacheCircuitBreakerChangeEventArgs.cs +++ b/src/ZiggyCreatures.FusionCache/Events/FusionCacheCircuitBreakerChangeEventArgs.cs @@ -3,7 +3,7 @@ namespace ZiggyCreatures.Caching.Fusion.Events; /// <summary> -/// The specific <see cref="EventArgs"/> object for events related to opening/closing of the distributed cache circuit breaker. +/// The specific <see cref="EventArgs"/> object for events related to opening/closing of a circuit breaker. /// </summary> public class FusionCacheCircuitBreakerChangeEventArgs : EventArgs { diff --git a/src/ZiggyCreatures.FusionCache/Events/FusionCacheEntryEvictionEventArgs.cs b/src/ZiggyCreatures.FusionCache/Events/FusionCacheEntryEvictionEventArgs.cs index 36d850f2..0f3deb5b 100644 --- a/src/ZiggyCreatures.FusionCache/Events/FusionCacheEntryEvictionEventArgs.cs +++ b/src/ZiggyCreatures.FusionCache/Events/FusionCacheEntryEvictionEventArgs.cs @@ -14,14 +14,21 @@ public class FusionCacheEntryEvictionEventArgs /// </summary> /// <param name="key">The cache key related to the event.</param> /// <param name="reason">The reason for the eviction.</param> - public FusionCacheEntryEvictionEventArgs(string key, EvictionReason reason) + /// <param name="value">The value being evicted from the cache.</param> + public FusionCacheEntryEvictionEventArgs(string key, EvictionReason reason, object? value) : base(key) { Reason = reason; + Value = value; } /// <summary> /// The reason for the eviction. /// </summary> public EvictionReason Reason { get; } + + /// <summary> + /// The value being evicted from the cache. + /// </summary> + public object? Value { get; } } diff --git a/src/ZiggyCreatures.FusionCache/Events/FusionCacheMemoryEventsHub.cs b/src/ZiggyCreatures.FusionCache/Events/FusionCacheMemoryEventsHub.cs index cd75a4a2..2e165ce9 100644 --- a/src/ZiggyCreatures.FusionCache/Events/FusionCacheMemoryEventsHub.cs +++ b/src/ZiggyCreatures.FusionCache/Events/FusionCacheMemoryEventsHub.cs @@ -41,9 +41,9 @@ public bool HasEvictionSubscribers() return Eviction is not null; } - internal void OnEviction(string operationId, string key, EvictionReason reason) + internal void OnEviction(string operationId, string key, EvictionReason reason, object? value) { - Eviction?.SafeExecute(operationId, key, _cache, () => new FusionCacheEntryEvictionEventArgs(key, reason), nameof(Eviction), _logger, _errorsLogLevel, _syncExecution); + Eviction?.SafeExecute(operationId, key, _cache, () => new FusionCacheEntryEvictionEventArgs(key, reason, value), nameof(Eviction), _logger, _errorsLogLevel, _syncExecution); } internal void OnExpire(string operationId, string key) diff --git a/src/ZiggyCreatures.FusionCache/FusionCache.cs b/src/ZiggyCreatures.FusionCache/FusionCache.cs index 1aa21dae..99a24346 100644 --- a/src/ZiggyCreatures.FusionCache/FusionCache.cs +++ b/src/ZiggyCreatures.FusionCache/FusionCache.cs @@ -1,6 +1,7 @@ using System; using System.Collections.Generic; using System.Diagnostics; +using System.Reflection; using System.Runtime.CompilerServices; using System.Threading; using System.Threading.Tasks; @@ -12,6 +13,7 @@ using ZiggyCreatures.Caching.Fusion.Backplane; using ZiggyCreatures.Caching.Fusion.Events; using ZiggyCreatures.Caching.Fusion.Internals; +using ZiggyCreatures.Caching.Fusion.Internals.AutoRecovery; using ZiggyCreatures.Caching.Fusion.Internals.Backplane; using ZiggyCreatures.Caching.Fusion.Internals.Distributed; using ZiggyCreatures.Caching.Fusion.Internals.Memory; @@ -24,20 +26,21 @@ namespace ZiggyCreatures.Caching.Fusion; /// <summary> /// The standard implementation of <see cref="IFusionCache"/>. /// </summary> -[DebuggerDisplay("NAME: {_options.CacheName} - ID: {InstanceId} - DC: {HasDistributedCache} - BP: {HasBackplane}")] +[DebuggerDisplay("NAME: {CacheName} - ID: {InstanceId} - DC: {HasDistributedCache} - BP: {HasBackplane}")] public partial class FusionCache : IFusionCache { private readonly FusionCacheOptions _options; private readonly string? _cacheKeyPrefix; - private readonly ILogger? _logger; - private readonly IFusionCacheReactor _reactor; + private readonly ILogger<FusionCache>? _logger; + private IFusionCacheReactor _reactor; private MemoryCacheAccessor _mca; private DistributedCacheAccessor? _dca; private BackplaneAccessor? _bpa; private readonly object _backplaneLock = new object(); private FusionCacheEventsHub _events; private readonly List<IFusionCachePlugin> _plugins; + private AutoRecoveryService _autoRecovery; /// <summary> /// Creates a new <see cref="FusionCache"/> instance. @@ -48,15 +51,23 @@ public partial class FusionCache /// <param name="reactor">The <see cref="IFusionCacheReactor"/> instance to use (advanced). If null, a standard one will be automatically created and managed.</param> public FusionCache(IOptions<FusionCacheOptions> optionsAccessor, IMemoryCache? memoryCache = null, ILogger<FusionCache>? logger = null, IFusionCacheReactor? reactor = null) { - // GLOBALLY UNIQUE INSTANCE ID - InstanceId = Guid.NewGuid().ToString("N"); - if (optionsAccessor is null) throw new ArgumentNullException(nameof(optionsAccessor)); // OPTIONS _options = optionsAccessor.Value ?? throw new ArgumentNullException(nameof(optionsAccessor.Value)); + // DUPLICATE OPTIONS (TO AVOID EXTERNAL MODIFICATIONS) + _options = _options.Duplicate(); + + // GLOBALLY UNIQUE INSTANCE ID + if (string.IsNullOrWhiteSpace(_options.InstanceId)) + { + _options.SetInstanceId(Guid.NewGuid().ToString("N")); + //_options.SetInstanceId(FusionCacheInternalUtils.GenerateOperationId()); + } + InstanceId = _options.InstanceId!; + // CACHE KEY PREFIX if (string.IsNullOrEmpty(_options.CacheKeyPrefix) == false) _cacheKeyPrefix = _options.CacheKeyPrefix; @@ -91,7 +102,20 @@ public FusionCache(IOptions<FusionCacheOptions> optionsAccessor, IMemoryCache? m _bpa = null; if (_logger?.IsEnabled(LogLevel.Trace) ?? false) - _logger.Log(LogLevel.Trace, "FUSION [N={CacheName} I={CacheInstanceId}]: instance created", _options.CacheName, InstanceId); + _logger.Log(LogLevel.Trace, "FUSION [N={CacheName} I={CacheInstanceId}]: instance created", CacheName, InstanceId); + + // AUTO-RECOVERY + _autoRecovery = new AutoRecoveryService(this, _options, _logger); + + // TRY UPDATE OPTIONS + _tryUpdateOptions = new FusionCacheEntryOptions() + { + DistributedCacheSoftTimeout = Timeout.InfiniteTimeSpan, + DistributedCacheHardTimeout = Timeout.InfiniteTimeSpan, + AllowBackgroundDistributedCacheOperations = false, + ReThrowDistributedCacheExceptions = true, + ReThrowSerializationExceptions = true, + }; } /// <inheritdoc/> @@ -109,6 +133,11 @@ public FusionCacheEntryOptions DefaultEntryOptions get { return _options.DefaultEntryOptions; } } + internal AutoRecoveryService AutoRecovery + { + get { return _autoRecovery; } + } + /// <inheritdoc/> public FusionCacheEntryOptions CreateEntryOptions(Action<FusionCacheEntryOptions>? setupAction = null, TimeSpan? duration = null) { @@ -134,23 +163,39 @@ private string MaybeGenerateOperationId() return FusionCacheInternalUtils.MaybeGenerateOperationId(_logger); } - private MemoryCacheAccessor? GetCurrentMemoryAccessor(FusionCacheEntryOptions options) + internal MemoryCacheAccessor GetCurrentMemoryAccessor() + { + return _mca; + } + + internal MemoryCacheAccessor? GetCurrentMemoryAccessor(FusionCacheEntryOptions options) { return options.SkipMemoryCache ? null : _mca; } - internal DistributedCacheAccessor? GetCurrentDistributedAccessor(FusionCacheEntryOptions options) + internal DistributedCacheAccessor? GetCurrentDistributedAccessor(FusionCacheEntryOptions? options) { + if (options is null) + return _dca; + return options.SkipDistributedCache ? null : _dca; } + internal BackplaneAccessor? GetCurrentBackplaneAccessor(FusionCacheEntryOptions? options) + { + if (options is null) + return _bpa; + + return options.SkipBackplaneNotifications ? null : _bpa; + } + private bool TryPickFailSafeFallbackValue<TValue>(string operationId, string key, FusionCacheDistributedEntry<TValue>? distributedEntry, FusionCacheMemoryEntry? memoryEntry, MaybeValue<TValue?> failSafeDefaultValue, FusionCacheEntryOptions options, out MaybeValue<TValue?> value, out long? timestamp, out bool failSafeActivated) { // FAIL-SAFE NOT ENABLED if (options.IsFailSafeEnabled == false) { if (_logger?.IsEnabled(LogLevel.Trace) ?? false) - _logger.Log(LogLevel.Trace, "FUSION [N={CacheName}] (O={CacheOperationId} K={CacheKey}): FAIL-SAFE not enabled", CacheName, operationId, key); + _logger.Log(LogLevel.Trace, "FUSION [N={CacheName} I={CacheInstanceId}] (O={CacheOperationId} K={CacheKey}): FAIL-SAFE not enabled", CacheName, InstanceId, operationId, key); value = default; timestamp = default; @@ -160,7 +205,7 @@ private bool TryPickFailSafeFallbackValue<TValue>(string operationId, string key // FAIL-SAFE ENABLED if (_logger?.IsEnabled(LogLevel.Trace) ?? false) - _logger.Log(LogLevel.Trace, "FUSION [N={CacheName}] (O={CacheOperationId} K={CacheKey}): trying to activate FAIL-SAFE", CacheName, operationId, key); + _logger.Log(LogLevel.Trace, "FUSION [N={CacheName} I={CacheInstanceId}] (O={CacheOperationId} K={CacheKey}): trying to activate FAIL-SAFE", CacheName, InstanceId, operationId, key); // TRY TO PICK A FALLBACK ENTRY IFusionCacheEntry? entry; @@ -178,7 +223,7 @@ private bool TryPickFailSafeFallbackValue<TValue>(string operationId, string key failSafeActivated = true; if (_logger?.IsEnabled(_options.FailSafeActivationLogLevel) ?? false) - _logger.Log(_options.FailSafeActivationLogLevel, "FUSION [N={CacheName}] (O={CacheOperationId} K={CacheKey}): FAIL-SAFE activated (from " + (entry is FusionCacheMemoryEntry ? "memory" : "distributed") + ")", CacheName, operationId, key); + _logger.Log(_options.FailSafeActivationLogLevel, "FUSION [N={CacheName} I={CacheInstanceId}] (O={CacheOperationId} K={CacheKey}): FAIL-SAFE activated (from " + (entry is FusionCacheMemoryEntry ? "memory" : "distributed") + ")", CacheName, InstanceId, operationId, key); // EVENT _events.OnFailSafeActivate(operationId, key); @@ -195,7 +240,7 @@ private bool TryPickFailSafeFallbackValue<TValue>(string operationId, string key failSafeActivated = true; if (_logger?.IsEnabled(_options.FailSafeActivationLogLevel) ?? false) - _logger.Log(_options.FailSafeActivationLogLevel, "FUSION [N={CacheName}] (O={CacheOperationId} K={CacheKey}): FAIL-SAFE activated (from fail-safe default value)", CacheName, operationId, key); + _logger.Log(_options.FailSafeActivationLogLevel, "FUSION [N={CacheName} I={CacheInstanceId}] (O={CacheOperationId} K={CacheKey}): FAIL-SAFE activated (from fail-safe default value)", CacheName, InstanceId, operationId, key); // EVENT _events.OnFailSafeActivate(operationId, key); @@ -205,7 +250,7 @@ private bool TryPickFailSafeFallbackValue<TValue>(string operationId, string key // UNABLE TO ACTIVATE FAIL-SAFE if (_logger?.IsEnabled(LogLevel.Trace) ?? false) - _logger.Log(LogLevel.Trace, "FUSION [N={CacheName}] (O={CacheOperationId} K={CacheKey}): unable to activate FAIL-SAFE (no entries in memory or distributed)", CacheName, operationId, key); + _logger.Log(LogLevel.Trace, "FUSION [N={CacheName} I={CacheInstanceId}] (O={CacheOperationId} K={CacheKey}): unable to activate FAIL-SAFE (no entries in memory or distributed)", CacheName, InstanceId, operationId, key); value = default; timestamp = default; @@ -213,7 +258,6 @@ private bool TryPickFailSafeFallbackValue<TValue>(string operationId, string key return false; } - //[MethodImpl(MethodImplOptions.AggressiveInlining)] private void MaybeBackgroundCompleteTimedOutFactory<TValue>(string operationId, string key, FusionCacheFactoryExecutionContext<TValue> ctx, Task<TValue?>? factoryTask, FusionCacheEntryOptions options, CancellationToken token) { if (factoryTask is null || options.AllowTimedOutFactoryBackgroundCompletion == false) @@ -222,7 +266,6 @@ private void MaybeBackgroundCompleteTimedOutFactory<TValue>(string operationId, CompleteBackgroundFactory<TValue>(operationId, key, ctx, factoryTask, options, null, token); } - //[MethodImpl(MethodImplOptions.AggressiveInlining)] private void CompleteBackgroundFactory<TValue>(string operationId, string key, FusionCacheFactoryExecutionContext<TValue> ctx, Task<TValue?> factoryTask, FusionCacheEntryOptions options, object? lockObj, CancellationToken token) { if (factoryTask.IsFaulted) @@ -230,7 +273,7 @@ private void CompleteBackgroundFactory<TValue>(string operationId, string key, F try { if (_logger?.IsEnabled(_options.FactoryErrorsLogLevel) ?? false) - _logger.Log(_options.FactoryErrorsLogLevel, factoryTask.Exception.GetSingleInnerExceptionOrSelf(), "FUSION [N={CacheName}] (O={CacheOperationId} K={CacheKey}): a background factory thrown an exception", CacheName, operationId, key); + _logger.Log(_options.FactoryErrorsLogLevel, factoryTask.Exception.GetSingleInnerExceptionOrSelf(), "FUSION [N={CacheName} I={CacheInstanceId}] (O={CacheOperationId} K={CacheKey}): a background factory thrown an exception", CacheName, InstanceId, operationId, key); // EVENT _events.OnBackgroundFactoryError(operationId, key); @@ -245,16 +288,16 @@ private void CompleteBackgroundFactory<TValue>(string operationId, string key, F // CONTINUE IN THE BACKGROUND TO TRY TO KEEP THE RESULT AS SOON AS IT WILL COMPLETE SUCCESSFULLY if (_logger?.IsEnabled(LogLevel.Debug) ?? false) - _logger.Log(LogLevel.Debug, "FUSION [N={CacheName}] (O={CacheOperationId} K={CacheKey}): trying to complete a background factory", CacheName, operationId, key); + _logger.Log(LogLevel.Debug, "FUSION [N={CacheName} I={CacheInstanceId}] (O={CacheOperationId} K={CacheKey}): trying to complete a background factory", CacheName, InstanceId, operationId, key); - _ = factoryTask.ContinueWith(antecedent => + _ = factoryTask.ContinueWith(async antecedent => { try { if (antecedent.Status == TaskStatus.Faulted) { if (_logger?.IsEnabled(_options.FactoryErrorsLogLevel) ?? false) - _logger.Log(_options.FactoryErrorsLogLevel, antecedent.Exception.GetSingleInnerExceptionOrSelf(), "FUSION [N={CacheName}] (O={CacheOperationId} K={CacheKey}): a background factory thrown an exception", CacheName, operationId, key); + _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); // EVENT _events.OnBackgroundFactoryError(operationId, key); @@ -262,21 +305,22 @@ private void CompleteBackgroundFactory<TValue>(string operationId, string key, F else if (antecedent.Status == TaskStatus.RanToCompletion) { if (_logger?.IsEnabled(LogLevel.Debug) ?? false) - _logger.Log(LogLevel.Debug, "FUSION [N={CacheName}] (O={CacheOperationId} K={CacheKey}): a background factory successfully completed, keeping the result", CacheName, operationId, key); + _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); // UPDATE ADAPTIVE OPTIONS var maybeNewOptions = ctx.GetOptions(); if (maybeNewOptions is not null && options != maybeNewOptions) options = maybeNewOptions; - // ADAPTIVE CACHING UPDATE - var lateEntry = FusionCacheMemoryEntry.CreateFromOptions(antecedent.Result, options, false, ctx.LastModified, ctx.ETag, null); + options = options.Duplicate(); + options.AllowBackgroundDistributedCacheOperations = false; + options.ReThrowDistributedCacheExceptions = false; + options.AllowBackgroundBackplaneOperations = false; + options.ReThrowBackplaneExceptions = false; + options.ReThrowSerializationExceptions = false; - var dca = GetCurrentDistributedAccessor(options); - if (dca.CanBeUsed(operationId, key)) - { - _ = dca?.SetEntryAsync<TValue>(operationId, key, lateEntry, options, token); - } + // ADAPTIVE CACHING UPDATE + var lateEntry = FusionCacheMemoryEntry.CreateFromOptions(antecedent.Result, options, false, ctx.LastModified, ctx.ETag, null, typeof(TValue)); var mca = GetCurrentMemoryAccessor(options); if (mca is not null) @@ -284,9 +328,7 @@ private void CompleteBackgroundFactory<TValue>(string operationId, string key, F mca.SetEntry<TValue>(operationId, key, lateEntry, options); } - // BACKPLANE - if (options.SkipBackplaneNotifications == false) - _ = PublishInternalAsync(operationId, BackplaneMessage.CreateForEntrySet(key), options, token); + await DistributedSetEntryAsync<TValue>(operationId, key, lateEntry, options, token).ConfigureAwait(false); // EVENT _events.OnBackgroundFactorySuccess(operationId, key); @@ -300,26 +342,25 @@ private void CompleteBackgroundFactory<TValue>(string operationId, string key, F }); } - [MethodImpl(MethodImplOptions.AggressiveInlining)] private void ReleaseLock(string operationId, string key, object? lockObj) { if (lockObj is null) return; if (_logger?.IsEnabled(LogLevel.Trace) ?? false) - _logger.Log(LogLevel.Trace, "FUSION [N={CacheName}] (O={CacheOperationId} K={CacheKey}): releasing LOCK", CacheName, operationId, key); + _logger.Log(LogLevel.Trace, "FUSION [N={CacheName} I={CacheInstanceId}] (O={CacheOperationId} K={CacheKey}): releasing LOCK", CacheName, InstanceId, operationId, key); try { - _reactor.ReleaseLock(CacheName, key, operationId, lockObj, _logger); + _reactor.ReleaseLock(CacheName, InstanceId, key, operationId, lockObj, _logger); if (_logger?.IsEnabled(LogLevel.Trace) ?? false) - _logger.Log(LogLevel.Trace, "FUSION [N={CacheName}] (O={CacheOperationId} K={CacheKey}): LOCK released", CacheName, operationId, key); + _logger.Log(LogLevel.Trace, "FUSION [N={CacheName} I={CacheInstanceId}] (O={CacheOperationId} K={CacheKey}): LOCK released", CacheName, InstanceId, operationId, key); } catch (Exception exc) { if (_logger?.IsEnabled(LogLevel.Warning) ?? false) - _logger.Log(LogLevel.Warning, exc, "FUSION [N={CacheName}] (O={CacheOperationId} K={CacheKey}): releasing the LOCK has thrown an exception", CacheName, operationId, key); + _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); } } @@ -329,7 +370,7 @@ private void ProcessFactoryError(string operationId, string key, Exception exc) if (exc is SyntheticTimeoutException) { if (_logger?.IsEnabled(_options.FactorySyntheticTimeoutsLogLevel) ?? false) - _logger.Log(_options.FactorySyntheticTimeoutsLogLevel, exc, "FUSION [N={CacheName}] (O={CacheOperationId} K={CacheKey}): a synthetic timeout occurred while calling the factory", CacheName, operationId, key); + _logger.Log(_options.FactorySyntheticTimeoutsLogLevel, exc, "FUSION [N={CacheName} I={CacheInstanceId}] (O={CacheOperationId} K={CacheKey}): a synthetic timeout occurred while calling the factory", CacheName, InstanceId, operationId, key); // EVENT _events.OnFactorySyntheticTimeout(operationId, key); @@ -338,23 +379,21 @@ private void ProcessFactoryError(string operationId, string key, Exception exc) } if (_logger?.IsEnabled(_options.FactoryErrorsLogLevel) ?? false) - _logger.Log(_options.FactoryErrorsLogLevel, exc, "FUSION [N={CacheName}] (O={CacheOperationId} K={CacheKey}): an error occurred while calling the factory", CacheName, operationId, key); + _logger.Log(_options.FactoryErrorsLogLevel, exc, "FUSION [N={CacheName} I={CacheInstanceId}] (O={CacheOperationId} K={CacheKey}): an error occurred while calling the factory", CacheName, InstanceId, operationId, key); // EVENT _events.OnFactoryError(operationId, key); } - internal void ExpireMemoryEntryInternal(string operationId, string key, bool allowFailSafe) + internal bool MaybeExpireMemoryEntryInternal(string operationId, string key, bool allowFailSafe, long? timestampThreshold) { - ValidateCacheKey(key); + if (_logger?.IsEnabled(LogLevel.Debug) ?? false) + _logger.Log(LogLevel.Debug, "FUSION [N={CacheName} I={CacheInstanceId}] (O={CacheOperationId} K={CacheKey}): calling MaybeExpireMemoryEntryInternal (allowFailSafe={AllowFailSafe}, timestampThreshold={TimestampThreshold})", CacheName, InstanceId, operationId, key, allowFailSafe, timestampThreshold); if (_mca is null) - return; - - if (_logger?.IsEnabled(LogLevel.Debug) ?? false) - _logger.Log(LogLevel.Debug, "FUSION [N={CacheName}] (O={CacheOperationId} K={CacheKey}): calling ExpireMemoryInternal (allowFailSafe={AllowFailSafe})", CacheName, operationId, key, allowFailSafe); + return false; - _mca.ExpireEntry(operationId, key, allowFailSafe); + return _mca.ExpireEntry(operationId, key, allowFailSafe, timestampThreshold); } /// <inheritdoc/> @@ -369,7 +408,7 @@ public IFusionCache SetupDistributedCache(IDistributedCache distributedCache, IF _dca = new DistributedCacheAccessor(distributedCache, serializer, _options, _logger, _events.Distributed); if (_logger?.IsEnabled(LogLevel.Debug) ?? false) - _logger.Log(LogLevel.Debug, "FUSION [N={CacheName}]: setup distributed cache (CACHE={DistributedCacheType} SERIALIZER={SerializerType})", CacheName, distributedCache.GetType().FullName, serializer.GetType().FullName); + _logger.Log(LogLevel.Debug, "FUSION [N={CacheName} I={CacheInstanceId}]: setup distributed cache (CACHE={DistributedCacheType} SERIALIZER={SerializerType})", CacheName, InstanceId, distributedCache.GetType().FullName, serializer.GetType().FullName); return this; } @@ -380,7 +419,7 @@ public IFusionCache RemoveDistributedCache() _dca = null; if (_logger?.IsEnabled(LogLevel.Debug) ?? false) - _logger.Log(LogLevel.Debug, "FUSION [N={CacheName}]: distributed cache removed", CacheName); + _logger.Log(LogLevel.Debug, "FUSION [N={CacheName} I={CacheInstanceId}]: distributed cache removed", CacheName, InstanceId); return this; } @@ -404,13 +443,27 @@ public IFusionCache SetupBackplane(IFusionCacheBackplane backplane) lock (_backplaneLock) { - _bpa = new BackplaneAccessor(this, backplane, _options, _logger, _events.Backplane); - _bpa.Subscribe(); - - if (_logger?.IsEnabled(LogLevel.Debug) ?? false) - _logger.Log(LogLevel.Debug, "FUSION [N={CacheName}]: setup backplane (BACKPLANE={BackplaneType})", CacheName, backplane.GetType().FullName); + _bpa = new BackplaneAccessor(this, backplane, _options, _logger); } + RunUtils.RunSyncActionAdvanced( + _ => + { + lock (_backplaneLock) + { + _bpa.Subscribe(); + + if (_logger?.IsEnabled(LogLevel.Debug) ?? false) + _logger.Log(LogLevel.Debug, "FUSION [N={CacheName} I={CacheInstanceId}]: setup backplane (BACKPLANE={BackplaneType})", CacheName, InstanceId, backplane.GetType().FullName); + } + }, + Timeout.InfiniteTimeSpan, + false, + DefaultEntryOptions.AllowBackgroundBackplaneOperations == false, + null, + false + ); + // CHECK: WARN THE USER IN CASE OF // - HAS A MEMORY CACHE (ALWAYS) // - HAS A BACKPLANE @@ -419,7 +472,7 @@ public IFusionCache SetupBackplane(IFusionCacheBackplane backplane) if (HasBackplane && HasDistributedCache == false && DefaultEntryOptions.SkipBackplaneNotifications == false) { if (_logger?.IsEnabled(LogLevel.Warning) ?? false) - _logger.Log(LogLevel.Warning, "FUSION [N={CacheName}]: it has been detected a situation where there *IS* a backplane, there is *NOT* a distributed cache and the DefaultEntryOptions.SkipBackplaneNotifications option is set to false. This will probably cause problems, since a notification will be sent automatically at every change in the cache but there is not a shared state (a distributed cache) that different nodes can use, basically resulting in a situation where the cache will keep invalidating itself at every change. It is suggested to either (1) add a distributed cache or (2) change the DefaultEntryOptions.SkipBackplaneNotifications to true.", CacheName, backplane.GetType().FullName); + _logger.Log(LogLevel.Warning, "FUSION [N={CacheName} I={CacheInstanceId}]: it has been detected a situation where there *IS* a backplane, there is *NOT* a distributed cache and the DefaultEntryOptions.SkipBackplaneNotifications option is set to false. This will probably cause problems, since a notification will be sent automatically at every change in the cache but there is not a shared state (a distributed cache) that different nodes can use, basically resulting in a situation where the cache will keep invalidating itself at every change. It is suggested to either (1) add a distributed cache or (2) change the DefaultEntryOptions.SkipBackplaneNotifications to true.", CacheName, InstanceId, backplane.GetType().FullName); } return this; @@ -436,7 +489,7 @@ public IFusionCache RemoveBackplane() _bpa = null; if (_logger?.IsEnabled(LogLevel.Debug) ?? false) - _logger.Log(LogLevel.Debug, "FUSION [N={CacheName}]: backplane removed", CacheName); + _logger.Log(LogLevel.Debug, "FUSION [N={CacheName} I={CacheInstanceId}]: backplane removed", CacheName, InstanceId); } } @@ -464,7 +517,7 @@ public void AddPlugin(IFusionCachePlugin plugin) if (_plugins.Contains(plugin)) { if (_logger?.IsEnabled(_options.PluginsErrorsLogLevel) ?? false) - _logger?.Log(_options.PluginsErrorsLogLevel, "FUSION [N={CacheName}]: the same plugin instance already exists (TYPE={PluginType})", CacheName, plugin.GetType().FullName); + _logger?.Log(_options.PluginsErrorsLogLevel, "FUSION [N={CacheName} I={CacheInstanceId}]: the same plugin instance already exists (TYPE={PluginType})", CacheName, InstanceId, plugin.GetType().FullName); throw new InvalidOperationException($"FUSION [N={CacheName}]: the same plugin instance already exists (TYPE={plugin.GetType().FullName})"); } @@ -485,13 +538,13 @@ public void AddPlugin(IFusionCachePlugin plugin) } if (_logger?.IsEnabled(_options.PluginsErrorsLogLevel) ?? false) - _logger.Log(_options.PluginsErrorsLogLevel, exc, "FUSION [N={CacheName}]: an error occurred while starting a plugin (TYPE={PluginType})", CacheName, plugin.GetType().FullName); + _logger.Log(_options.PluginsErrorsLogLevel, exc, "FUSION [N={CacheName} I={CacheInstanceId}]: an error occurred while starting a plugin (TYPE={PluginType})", CacheName, InstanceId, plugin.GetType().FullName); throw new InvalidOperationException($"FUSION [N={CacheName}]: an error occurred while starting a plugin (TYPE={plugin.GetType().FullName})", exc); } if (_logger?.IsEnabled(_options.PluginsInfoLogLevel) ?? false) - _logger?.Log(_options.PluginsInfoLogLevel, "FUSION [N={CacheName}]: a plugin has been added and started (TYPE={PluginType})", CacheName, plugin.GetType().FullName); + _logger?.Log(_options.PluginsInfoLogLevel, "FUSION [N={CacheName} I={CacheInstanceId}]: a plugin has been added and started (TYPE={PluginType})", CacheName, InstanceId, plugin.GetType().FullName); } /// <inheritdoc/> @@ -505,7 +558,7 @@ public bool RemovePlugin(IFusionCachePlugin plugin) if (_plugins.Contains(plugin) == false) { if (_logger?.IsEnabled(_options.PluginsErrorsLogLevel) ?? false) - _logger?.Log(_options.PluginsErrorsLogLevel, "FUSION [N={CacheName}]: the plugin cannot be removed because is not part of this FusionCache instance (TYPE={PluginType})", CacheName, plugin.GetType().FullName); + _logger?.Log(_options.PluginsErrorsLogLevel, "FUSION [N={CacheName} I={CacheInstanceId}]: the plugin cannot be removed because is not part of this FusionCache instance (TYPE={PluginType})", CacheName, InstanceId, plugin.GetType().FullName); // MAYBE WE SHOULD THROW (LIKE IN AddPlugin) INSTEAD OF JUST RETURNING (LIKE IN List<T>.Remove()) ? return false; @@ -520,7 +573,7 @@ public bool RemovePlugin(IFusionCachePlugin plugin) catch (Exception exc) { if (_logger?.IsEnabled(_options.PluginsErrorsLogLevel) ?? false) - _logger.Log(_options.PluginsErrorsLogLevel, exc, "FUSION [N={CacheName}]: an error occurred while stopping a plugin (TYPE={PluginType})", CacheName, plugin.GetType().FullName); + _logger.Log(_options.PluginsErrorsLogLevel, exc, "FUSION [N={CacheName} I={CacheInstanceId}]: an error occurred while stopping a plugin (TYPE={PluginType})", CacheName, InstanceId, plugin.GetType().FullName); throw new InvalidOperationException($"FUSION [N={CacheName}]: an error occurred while stopping a plugin (TYPE={plugin.GetType().FullName})", exc); } @@ -532,7 +585,7 @@ public bool RemovePlugin(IFusionCachePlugin plugin) } if (_logger?.IsEnabled(_options.PluginsInfoLogLevel) ?? false) - _logger?.Log(_options.PluginsInfoLogLevel, "FUSION [N={CacheName}]: a plugin has been stopped and removed (TYPE={PluginType})", CacheName, plugin.GetType().FullName); + _logger?.Log(_options.PluginsInfoLogLevel, "FUSION [N={CacheName} I={CacheInstanceId}]: a plugin has been stopped and removed (TYPE={PluginType})", CacheName, InstanceId, plugin.GetType().FullName); return true; } @@ -560,13 +613,21 @@ protected virtual void Dispose(bool disposing) RemoveAllPlugins(); RemoveBackplane(); RemoveDistributedCache(); + +#pragma warning disable CS8625 // Cannot convert null literal to non-nullable reference type. + _autoRecovery.Dispose(); + _autoRecovery = null; + _reactor.Dispose(); + _reactor = null; + _mca.Dispose(); - } -#pragma warning disable CS8625 // Cannot convert null literal to non-nullable reference type. - _mca = null; - _events = null; + _mca = null; + + _events = null; #pragma warning restore CS8625 // Cannot convert null literal to non-nullable reference type. + } + disposedValue = true; } } @@ -577,5 +638,142 @@ protected virtual void Dispose(bool disposing) public void Dispose() { Dispose(true); + GC.SuppressFinalize(this); + } + + internal bool RequiresDistributedOperations(FusionCacheEntryOptions options) + { + if (HasDistributedCache && options.SkipDistributedCache == false) + return true; + + if (HasBackplane && options.SkipBackplaneNotifications == false) + return true; + + return false; + } + + internal bool MustAwaitDistributedOperations(FusionCacheEntryOptions options) + { + if (HasDistributedCache && options.AllowBackgroundDistributedCacheOperations == false) + return true; + + if (HasDistributedCache == false && HasBackplane && options.AllowBackgroundBackplaneOperations == false) + return true; + + return false; + } + + internal bool MustAwaitBackplaneOperations(FusionCacheEntryOptions options) + { + if (HasBackplane && options.AllowBackgroundBackplaneOperations == false) + return true; + + return false; + } + + private static readonly MethodInfo __methodInfoTryUpdateMemoryEntryFromDistributedEntryAsyncOpenGeneric = typeof(FusionCache).GetMethod(nameof(TryUpdateMemoryEntryFromDistributedEntryAsync), BindingFlags.NonPublic | BindingFlags.Instance); + + internal async ValueTask<(bool error, bool isSame, bool hasUpdated)> TryUpdateMemoryEntryFromDistributedEntryUntypedAsync(string operationId, string cacheKey, FusionCacheMemoryEntry memoryEntry) + { + if (_logger?.IsEnabled(LogLevel.Trace) ?? false) + _logger.Log(LogLevel.Trace, "FUSION [N={CacheName} I={CacheInstanceId}] (O={CacheOperationId} K={CacheKey}): trying to update memory entry from distributed entry", CacheName, InstanceId, operationId, cacheKey); + + try + { + if (HasDistributedCache == false) + { + if (_logger?.IsEnabled(LogLevel.Trace) ?? false) + _logger.Log(LogLevel.Trace, "FUSION [N={CacheName} I={CacheInstanceId}] (O={CacheOperationId} K={CacheKey}): cannot update memory from distributed because there's no distributed cache", CacheName, InstanceId, operationId, cacheKey); + + return (false, false, false); + } + + var dca = GetCurrentDistributedAccessor(null); + + if (dca is null) + { + if (_logger?.IsEnabled(LogLevel.Trace) ?? false) + _logger.Log(LogLevel.Trace, "FUSION [N={CacheName} I={CacheInstanceId}] (O={CacheOperationId} K={CacheKey}): cannot update memory from distributed because distributed cache is not enabled for the current operation", CacheName, InstanceId, operationId, cacheKey); + + return (false, false, false); + } + + if (dca.IsCurrentlyUsable(operationId, cacheKey) == false) + { + if (_logger?.IsEnabled(LogLevel.Trace) ?? false) + _logger.Log(LogLevel.Trace, "FUSION [N={CacheName} I={CacheInstanceId}] (O={CacheOperationId} K={CacheKey}): cannot update memory from distributed because distributed cache is not currently usable", CacheName, InstanceId, operationId, cacheKey); + + return (true, false, false); + } + + var methodInfo = __methodInfoTryUpdateMemoryEntryFromDistributedEntryAsyncOpenGeneric.MakeGenericMethod(memoryEntry.ValueType); + // SIGNATURE PARAMS: string operationId, string cacheKey, DistributedCacheAccessor dca, FusionCacheMemoryEntry memoryEntry + return await ((ValueTask<(bool error, bool isSame, bool hasUpdated)>)methodInfo.Invoke(this, new object[] { operationId, cacheKey, dca, memoryEntry })).ConfigureAwait(false); + } + catch (Exception exc) + { + if (_logger?.IsEnabled(LogLevel.Error) ?? false) + _logger.Log(LogLevel.Error, exc, "FUSION [N={CacheName} I={CacheInstanceId}] (O={CacheOperationId} K={CacheKey}): an error occurred while calling TryUpdateMemoryEntryFromDistributedEntryUntypedAsync() to try to update a memory entry from a distributed entry without knowing the TValue type", CacheName, InstanceId, operationId, cacheKey); + + return (true, false, false); + } + } + + private FusionCacheEntryOptions _tryUpdateOptions; + private async ValueTask<(bool error, bool isSame, bool hasUpdated)> TryUpdateMemoryEntryFromDistributedEntryAsync<TValue>(string operationId, string cacheKey, DistributedCacheAccessor dca, FusionCacheMemoryEntry memoryEntry) + { + try + { + (var distributedEntry, var isValid) = await dca.TryGetEntryAsync<TValue>(operationId, cacheKey, _tryUpdateOptions, false, Timeout.InfiniteTimeSpan, default).ConfigureAwait(false); + + if (distributedEntry is null || isValid == false) + { + //_cache.MaybeExpireMemoryEntryInternal(operationId, cacheKey, true, null); + //return; + + if (_logger?.IsEnabled(LogLevel.Trace) ?? false) + _logger.Log(LogLevel.Trace, "FUSION [N={CacheName} I={CacheInstanceId}] (O={CacheOperationId} K={CacheKey}): distributed entry not found or stale, do not update memory entry", CacheName, InstanceId, operationId, cacheKey); + + return (false, false, false); + } + + if (/*distributedEntry.Timestamp is not null &&*/ distributedEntry.Timestamp == memoryEntry.Timestamp) + { + if (_logger?.IsEnabled(LogLevel.Trace) ?? false) + _logger.Log(LogLevel.Trace, "FUSION [N={CacheName} I={CacheInstanceId}] (O={CacheOperationId} K={CacheKey}): memory entry same as distributed entry, do not update memory entry", CacheName, InstanceId, operationId, cacheKey); + + return (false, true, false); + } + + if (distributedEntry.Timestamp < memoryEntry.Timestamp) + { + //return; + if (_logger?.IsEnabled(LogLevel.Trace) ?? false) + _logger.Log(LogLevel.Trace, "FUSION [N={CacheName} I={CacheInstanceId}] (O={CacheOperationId} K={CacheKey}): memory entry more fresh than distributed entry, do not update memory entry", CacheName, InstanceId, operationId, cacheKey); + + return (false, false, false); + } + + if (_logger?.IsEnabled(LogLevel.Trace) ?? false) + _logger.Log(LogLevel.Trace, "FUSION [N={CacheName} I={CacheInstanceId}] (O={CacheOperationId} K={CacheKey}): updating memory entry from distributed entry", CacheName, InstanceId, operationId, cacheKey); + + memoryEntry.UpdateFromDistributedEntry<TValue>(distributedEntry); + + 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); + + _events.Memory.OnSet(operationId, cacheKey); + + return (false, false, true); + } + catch (Exception exc) + { + if (_logger?.IsEnabled(LogLevel.Error) ?? false) + _logger.Log(LogLevel.Error, exc, "FUSION [N={CacheName} I={CacheInstanceId}] (O={CacheOperationId} K={CacheKey}): an error occurred while trying to update a memory entry from a distributed entry", CacheName, InstanceId, operationId, cacheKey); + + //MaybeExpireMemoryEntryInternal(operationId, cacheKey, true, null); + + return (true, false, false); + } } } diff --git a/src/ZiggyCreatures.FusionCache/FusionCacheBackplaneException.cs b/src/ZiggyCreatures.FusionCache/FusionCacheBackplaneException.cs new file mode 100644 index 00000000..6c33a9b0 --- /dev/null +++ b/src/ZiggyCreatures.FusionCache/FusionCacheBackplaneException.cs @@ -0,0 +1,42 @@ +using System; +using System.Runtime.Serialization; + +namespace ZiggyCreatures.Caching.Fusion; + +/// <summary> +/// The generic exception that is thrown when a distributed cache error occurs: the InnerException contains the original exception. +/// </summary> +[Serializable] +public class FusionCacheBackplaneException + : Exception +{ + /// <summary> + /// Initializes a new instance of the <see cref="FusionCacheBackplaneException"/> class. + /// </summary> + public FusionCacheBackplaneException() + { + } + + /// <summary>Initializes a new instance of the <see cref="FusionCacheBackplaneException"/> class with a specified error message.</summary> + /// <param name="message">The message that describes the error.</param> + public FusionCacheBackplaneException(string? message) + : base(message) + { + } + + /// <summary>Initializes a new instance of the <see cref="FusionCacheBackplaneException"/> class with a specified error message and a reference to the inner exception that is the cause of this exception.</summary> + /// <param name="message">The error message that explains the reason for the exception.</param> + /// <param name="innerException">The exception that is the cause of the current exception. If the innerException parameter is not a null reference (Nothing in Visual Basic), the current exception is raised in a catch block that handles the inner exception.</param> + public FusionCacheBackplaneException(string? message, Exception? innerException) + : base(message, innerException) + { + } + + /// <summary>Initializes a new instance of the <see cref="FusionCacheBackplaneException"/> class with serialized data.</summary> + /// <param name="info">The object that holds the serialized object data.</param> + /// <param name="context">The contextual information about the source or destination.</param> + protected FusionCacheBackplaneException(SerializationInfo info, StreamingContext context) + : base(info, context) + { + } +} diff --git a/src/ZiggyCreatures.FusionCache/FusionCacheDistributedCacheException.cs b/src/ZiggyCreatures.FusionCache/FusionCacheDistributedCacheException.cs new file mode 100644 index 00000000..c66525d7 --- /dev/null +++ b/src/ZiggyCreatures.FusionCache/FusionCacheDistributedCacheException.cs @@ -0,0 +1,42 @@ +using System; +using System.Runtime.Serialization; + +namespace ZiggyCreatures.Caching.Fusion; + +/// <summary> +/// The generic exception that is thrown when a distributed cache error occurs: the InnerException contains the original exception. +/// </summary> +[Serializable] +public class FusionCacheDistributedCacheException + : Exception +{ + /// <summary> + /// Initializes a new instance of the <see cref="FusionCacheDistributedCacheException"/> class. + /// </summary> + public FusionCacheDistributedCacheException() + { + } + + /// <summary>Initializes a new instance of the <see cref="FusionCacheDistributedCacheException"/> class with a specified error message.</summary> + /// <param name="message">The message that describes the error.</param> + public FusionCacheDistributedCacheException(string? message) + : base(message) + { + } + + /// <summary>Initializes a new instance of the <see cref="FusionCacheDistributedCacheException"/> class with a specified error message and a reference to the inner exception that is the cause of this exception.</summary> + /// <param name="message">The error message that explains the reason for the exception.</param> + /// <param name="innerException">The exception that is the cause of the current exception. If the innerException parameter is not a null reference (Nothing in Visual Basic), the current exception is raised in a catch block that handles the inner exception.</param> + public FusionCacheDistributedCacheException(string? message, Exception? innerException) + : base(message, innerException) + { + } + + /// <summary>Initializes a new instance of the <see cref="FusionCacheDistributedCacheException"/> class with serialized data.</summary> + /// <param name="info">The object that holds the serialized object data.</param> + /// <param name="context">The contextual information about the source or destination.</param> + protected FusionCacheDistributedCacheException(SerializationInfo info, StreamingContext context) + : base(info, context) + { + } +} diff --git a/src/ZiggyCreatures.FusionCache/FusionCacheEntryOptions.cs b/src/ZiggyCreatures.FusionCache/FusionCacheEntryOptions.cs index 2afca683..5f6a0938 100644 --- a/src/ZiggyCreatures.FusionCache/FusionCacheEntryOptions.cs +++ b/src/ZiggyCreatures.FusionCache/FusionCacheEntryOptions.cs @@ -1,17 +1,19 @@ using System; +using System.ComponentModel; using System.Threading; using Microsoft.Extensions.Caching.Distributed; using Microsoft.Extensions.Caching.Memory; using Microsoft.Extensions.Logging; using ZiggyCreatures.Caching.Fusion.Events; using ZiggyCreatures.Caching.Fusion.Internals; +using ZiggyCreatures.Caching.Fusion.Internals.Memory; namespace ZiggyCreatures.Caching.Fusion; /// <summary> /// Represents all the options available for a single <see cref="IFusionCache"/> entry. /// </summary> -public class FusionCacheEntryOptions +public sealed class FusionCacheEntryOptions { /// <summary> /// Creates a new instance of a <see cref="FusionCacheEntryOptions"/> object. @@ -43,6 +45,7 @@ public FusionCacheEntryOptions(TimeSpan? duration = null) SkipBackplaneNotifications = FusionCacheGlobalDefaults.EntryOptionsSkipBackplaneNotifications; AllowBackgroundBackplaneOperations = FusionCacheGlobalDefaults.EntryOptionsAllowBackgroundBackplaneOperations; + ReThrowBackplaneExceptions = FusionCacheGlobalDefaults.EntryOptionsReThrowBackplaneExceptions; SkipDistributedCache = FusionCacheGlobalDefaults.EntryOptionsSkipDistributedCache; SkipDistributedCacheReadWhenStale = FusionCacheGlobalDefaults.EntryOptionsSkipDistributedCacheReadWhenStale; @@ -223,7 +226,7 @@ public float? EagerRefreshThreshold public bool ReThrowDistributedCacheExceptions { get; set; } /// <summary> - /// Set this to <see langword="true"/> to allow the bubble up of serialization exceptions (default is <see langword="false"/>). + /// Set this to <see langword="true"/> to allow the bubble up of serialization exceptions (default is <see langword="true"/>). /// Please note that, even if set to <see langword="true"/>, in some cases you would also need <see cref="AllowBackgroundDistributedCacheOperations"/> set to <see langword="false"/> and no timeout (neither soft nor hard) specified. /// <br/><br/> /// <strong>DOCS:</strong> <see href="https://github.com/ZiggyCreatures/FusionCache/blob/main/docs/CacheLevels.md"/> @@ -237,6 +240,7 @@ public float? EagerRefreshThreshold /// <br/> /// <strong>OBSOLETE NOW:</strong> <see href="https://github.com/ZiggyCreatures/FusionCache/issues/101"/> /// </summary> + [EditorBrowsable(EditorBrowsableState.Never)] [Obsolete("Please use the SkipBackplaneNotifications option and invert the value: EnableBackplaneNotifications = true is the same as SkipBackplaneNotifications = false", true)] public bool EnableBackplaneNotifications { @@ -264,6 +268,14 @@ public bool EnableBackplaneNotifications /// </summary> public bool AllowBackgroundBackplaneOperations { get; set; } + /// <summary> + /// Set this to <see langword="true"/> to allow the bubble up of backplane exceptions (default is <see langword="false"/>). + /// Please note that, even if set to <see langword="true"/>, in some cases you would also need <see cref="AllowBackgroundBackplaneOperations"/> set to <see langword="false"/> and no timeout (neither soft nor hard) specified. + /// <br/><br/> + /// <strong>DOCS:</strong> <see href="https://github.com/ZiggyCreatures/FusionCache/blob/main/docs/Backplane.md"/> + /// </summary> + public bool ReThrowBackplaneExceptions { get; set; } + /// <summary> /// Skip the usage of the distributed cache, if any. /// <br/><br/> @@ -532,6 +544,7 @@ public FusionCacheEntryOptions SetDistributedCacheTimeouts(TimeSpan? softTimeout /// </summary> /// <param name="enableBackplaneNotifications">Set the <see cref="EnableBackplaneNotifications"/> property.</param> /// <returns>The <see cref="FusionCacheEntryOptions"/> so that additional calls can be chained.</returns> + [EditorBrowsable(EditorBrowsableState.Never)] [Obsolete("Please use the SetSkipBackplaneNotifications method and invert the value: EnableBackplaneNotifications = true is the same as SkipBackplaneNotifications = false", true)] public FusionCacheEntryOptions SetBackplane(bool enableBackplaneNotifications) { @@ -635,7 +648,10 @@ internal MemoryCacheEntryOptions ToMemoryCacheEntryOptions(FusionCacheMemoryEven if (events.HasEvictionSubscribers()) { res.RegisterPostEvictionCallback( - (key, _, reason, state) => ((FusionCacheMemoryEventsHub)state)?.OnEviction(string.Empty, key.ToString(), reason), + (key, entry, reason, state) => + { + ((FusionCacheMemoryEventsHub)state)?.OnEviction(string.Empty, key.ToString(), reason, ((FusionCacheMemoryEntry?)entry)?.Value); + }, events ); } @@ -644,7 +660,7 @@ internal MemoryCacheEntryOptions ToMemoryCacheEntryOptions(FusionCacheMemoryEven if (incoherentFailSafeMaxDuration) { if (logger?.IsEnabled(options.IncoherentOptionsNormalizationLogLevel) ?? false) - logger.Log(options.IncoherentOptionsNormalizationLogLevel, "FUSION [N={CacheName}] (O={CacheOperationId} K={CacheKey}): FailSafeMaxDuration {{FailSafeMaxDuration}} was lower than the Duration {Duration} on {Options} {MemoryOptions}. Duration has been used instead.", options.CacheName, operationId, key, FailSafeMaxDuration.ToLogString(), Duration.ToLogString(), this.ToLogString(), res.ToLogString()); + logger.Log(options.IncoherentOptionsNormalizationLogLevel, "FUSION [N={CacheName} I={CacheInstanceId}] (O={CacheOperationId} K={CacheKey}): FailSafeMaxDuration {{FailSafeMaxDuration}} was lower than the Duration {Duration} on {Options} {MemoryOptions}. Duration has been used instead.", options.CacheName, options.InstanceId, operationId, key, FailSafeMaxDuration.ToLogString(), Duration.ToLogString(), this.ToLogString(), res.ToLogString()); } return res; @@ -690,7 +706,7 @@ internal DistributedCacheEntryOptions ToDistributedCacheEntryOptions(FusionCache if (incoherentFailSafeMaxDuration) { if (logger?.IsEnabled(options.IncoherentOptionsNormalizationLogLevel) ?? false) - logger.Log(options.IncoherentOptionsNormalizationLogLevel, "FUSION [N={CacheName}] (O={CacheOperationId} K={CacheKey}): DistributedCacheFailSafeMaxDuration/FailSafeMaxDuration {{FailSafeMaxDuration}} was lower than the DistributedCache/Duration {Duration} on {Options} {MemoryOptions}. Duration has been used instead.", options.CacheName, operationId, key, failSafeMaxDurationToUse.ToLogString(), durationToUse.ToLogString(), this.ToLogString(), res.ToLogString()); + 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()); } return res; @@ -789,6 +805,7 @@ public FusionCacheEntryOptions Duplicate(TimeSpan? duration = null) ReThrowDistributedCacheExceptions = ReThrowDistributedCacheExceptions, ReThrowSerializationExceptions = ReThrowSerializationExceptions, + ReThrowBackplaneExceptions = ReThrowBackplaneExceptions, AllowBackgroundDistributedCacheOperations = AllowBackgroundDistributedCacheOperations, AllowBackgroundBackplaneOperations = AllowBackgroundBackplaneOperations, @@ -798,7 +815,7 @@ public FusionCacheEntryOptions Duplicate(TimeSpan? duration = null) SkipDistributedCache = SkipDistributedCache, SkipDistributedCacheReadWhenStale = SkipDistributedCacheReadWhenStale, - SkipMemoryCache = SkipMemoryCache, + SkipMemoryCache = SkipMemoryCache }; } diff --git a/src/ZiggyCreatures.FusionCache/FusionCacheGlobalDefaults.cs b/src/ZiggyCreatures.FusionCache/FusionCacheGlobalDefaults.cs index 500b6f4c..68ad7e7a 100644 --- a/src/ZiggyCreatures.FusionCache/FusionCacheGlobalDefaults.cs +++ b/src/ZiggyCreatures.FusionCache/FusionCacheGlobalDefaults.cs @@ -6,6 +6,8 @@ namespace ZiggyCreatures.Caching.Fusion; /// <summary> /// Contains the default values used globally. +/// <br/><br/> +/// <strong>NOTE:</strong> since these values are used *globally*, they should be changed only as a last resort, and if you *really* know what you are doing. /// </summary> public static class FusionCacheGlobalDefaults { @@ -109,6 +111,11 @@ public static class FusionCacheGlobalDefaults /// </summary> public static bool EntryOptionsAllowBackgroundBackplaneOperations { get; set; } = true; + /// <summary> + /// The global default <see cref="FusionCacheEntryOptions.ReThrowBackplaneExceptions"/>. + /// </summary> + public static bool EntryOptionsReThrowBackplaneExceptions { get; set; } = false; + /// <summary> /// The global default <see cref="FusionCacheEntryOptions.SkipDistributedCache"/>. /// </summary> diff --git a/src/ZiggyCreatures.FusionCache/FusionCacheOptions.cs b/src/ZiggyCreatures.FusionCache/FusionCacheOptions.cs index 9d0a3380..d3c46ebe 100644 --- a/src/ZiggyCreatures.FusionCache/FusionCacheOptions.cs +++ b/src/ZiggyCreatures.FusionCache/FusionCacheOptions.cs @@ -1,4 +1,5 @@ using System; +using System.ComponentModel; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; @@ -18,6 +19,26 @@ public class FusionCacheOptions /// </summary> public const string DefaultCacheName = "FusionCache"; + /// <summary> + /// The wire format version identifier for the distributed cache wire format, used in the cache key processing. + /// </summary> + public const string DistributedCacheWireFormatVersion = "v0"; + + /// <summary> + /// The wire format version separator for the distributed cache wire format, used in the cache key processing. + /// </summary> + public const string DistributedCacheWireFormatSeparator = ":"; + + /// <summary> + /// The wire format version identifier for the backplane wire format, used in the channel name. + /// </summary> + public const string BackplaneWireFormatVersion = "v0"; + + /// <summary> + /// The wire format version separator for the backplane wire format, used in the channel name. + /// </summary> + public const string BackplaneWireFormatSeparator = ":"; + /// <summary> /// Creates a new instance of a <see cref="FusionCacheOptions"/> object. /// </summary> @@ -27,11 +48,11 @@ public FusionCacheOptions() _defaultEntryOptions = new FusionCacheEntryOptions(); - // BACKPLANE AUTO-RECOVERY - EnableBackplaneAutoRecovery = true; - BackplaneAutoRecoveryMaxItems = null; - BackplaneAutoRecoveryReconnectDelay = TimeSpan.FromMilliseconds(2_000); - EnableDistributedExpireOnBackplaneAutoRecovery = true; + // AUTO-RECOVERY + EnableAutoRecovery = true; + AutoRecoveryMaxItems = null; + AutoRecoveryMaxRetryCount = null; + AutoRecoveryDelay = TimeSpan.FromMilliseconds(2_000); // LOG LEVELS IncoherentOptionsNormalizationLogLevel = LogLevel.Warning; @@ -74,6 +95,22 @@ public string CacheName } } + /// <summary> + /// The instance id of the cache: it will be used for low-level identification for the same logical cache between different nodes in a multi-node scenario: it is automatically set to a random value. + /// </summary> + public string? InstanceId { get; private set; } + + /// <summary> + /// Set the InstanceId of the cache, but please don't use this. + /// <br/><br/> + /// <strong>⚠ WARNING:</strong> again, this should NOT be set, basically never ever, unless you really know what you are doing. For example by using the same value for two different cache instances they will be considered as the same cache, and this will lead to critical errors. So again, really: you should not use this. + /// </summary> + /// <param name="instanceId"></param> + public void SetInstanceId(string instanceId) + { + InstanceId = instanceId; + } + /// <summary> /// The default <see cref="FusionCacheEntryOptions"/> to use when none will be specified, and as the starting point when duplicating one. /// </summary> @@ -133,35 +170,132 @@ public FusionCacheEntryOptions DefaultEntryOptions public string? BackplaneChannelPrefix { get; set; } /// <summary> + /// DEPRECATED: please use EnableAutoRecovery. + /// <br/><br/> /// Enable auto-recovery for the backplane notifications to better handle transient errors without generating synchronization issues: notifications that failed to be sent out will be retried later on, when the backplane becomes responsive again. /// <br/><br/> - /// <strong>DOCS:</strong> <see href="https://github.com/ZiggyCreatures/FusionCache/blob/main/docs/Backplane.md"/> + /// <strong>DOCS:</strong> <see href="https://github.com/ZiggyCreatures/FusionCache/blob/main/docs/AutoRecovery.md"/> /// </summary> - public bool EnableBackplaneAutoRecovery { get; set; } + [EditorBrowsable(EditorBrowsableState.Never)] + [Obsolete("Backplane auto-recovery is now simply auto-recovery: please use the EnableAutoRecovery property.")] + public bool EnableBackplaneAutoRecovery + { + get { return EnableAutoRecovery; } + set { EnableAutoRecovery = value; } + } /// <summary> + /// Enable auto-recovery to automatically handle transient errors to minimize synchronization issues. + /// <br/><br/> + /// <strong>DOCS:</strong> <see href="https://github.com/ZiggyCreatures/FusionCache/blob/main/docs/AutoRecovery.md"/> + /// </summary> + public bool EnableAutoRecovery { get; set; } + + /// <summary> + /// DEPRECATED: please use AutoRecoveryMaxItems. + /// <br/><br/> /// The maximum number of items in the auto-recovery queue: this can help reducing memory consumption. If set to <see langword="null"/> there will be no limit. /// <br/><br/> - /// <strong>DOCS:</strong> <see href="https://github.com/ZiggyCreatures/FusionCache/blob/main/docs/Backplane.md"/> + /// <strong>DOCS:</strong> <see href="https://github.com/ZiggyCreatures/FusionCache/blob/main/docs/AutoRecovery.md"/> + /// </summary> + [EditorBrowsable(EditorBrowsableState.Never)] + [Obsolete("Backplane auto-recovery is now simply auto-recovery: please use the AutoRecoveryMaxItems property.")] + public int? BackplaneAutoRecoveryMaxItems + { + get { return AutoRecoveryMaxItems; } + set { AutoRecoveryMaxItems = value; } + } + + /// <summary> + /// The maximum number of items in the auto-recovery queue: this is usually not needed, but it may help reducing memory consumption in extreme scenarios. + /// <br/> + /// When set to null <see langword="null"/> there will be no limits. + /// <br/><br/> + /// <strong>DOCS:</strong> <see href="https://github.com/ZiggyCreatures/FusionCache/blob/main/docs/AutoRecovery.md"/> + /// </summary> + public int? AutoRecoveryMaxItems { get; set; } + + /// <summary> + /// DEPRECATED: please use AutoRecoveryMaxRetryCount. + /// <br/><br/> + /// The maximum number of retries for a auto-recovery item: after this amount the item is discarded, to avoid keeping it retrying forever. If set to <see langword="null"/> there will be no limit. + /// <br/><br/> + /// <strong>DOCS:</strong> <see href="https://github.com/ZiggyCreatures/FusionCache/blob/main/docs/AutoRecovery.md"/> + /// </summary> + [EditorBrowsable(EditorBrowsableState.Never)] + [Obsolete("Backplane auto-recovery is now simply auto-recovery: please use the AutoRecoveryMaxRetryCount property.")] + public int? BackplaneAutoRecoveryMaxRetryCount + { + get { return AutoRecoveryMaxRetryCount; } + set { AutoRecoveryMaxRetryCount = value; } + } + + /// <summary> + /// The maximum number of retries for a auto-recovery item: after this amount an item is discarded, to avoid keeping it for too long. + /// Please note though that a cleanup is automatically performed, so in theory there's no need to set this. + /// <br/> + /// When set to <see langword="null"/> there will be no limits. + /// <br/><br/> + /// <strong>DOCS:</strong> <see href="https://github.com/ZiggyCreatures/FusionCache/blob/main/docs/AutoRecovery.md"/> /// </summary> - public int? BackplaneAutoRecoveryMaxItems { get; set; } + public int? AutoRecoveryMaxRetryCount { get; set; } /// <summary> + /// DEPRECATED: please use AutoRecoveryDelay. + /// <br/><br/> /// The amount of time to wait, after a backplane reconnection, before trying to process the auto-recovery queue: this may be useful to allow all the other nodes to be ready. /// <br/> /// Use <see cref="TimeSpan.Zero"/> to avoid any delay (risky). /// <br/><br/> /// <strong>DOCS:</strong> <see href="https://github.com/ZiggyCreatures/FusionCache/blob/main/docs/Backplane.md"/> /// </summary> - public TimeSpan BackplaneAutoRecoveryReconnectDelay { get; set; } + [EditorBrowsable(EditorBrowsableState.Never)] + [Obsolete("Please use AutoRecoveryDelay instead.")] + public TimeSpan BackplaneAutoRecoveryReconnectDelay + { + get { return AutoRecoveryDelay; } + set { AutoRecoveryDelay = value; } + } + + /// <summary> + /// DEPRECATED: please use AutoRecoveryDelay. + /// <br/><br/> + /// The amount of time to wait before actually processing the auto-recovery queue, to better handle backpressure. + /// <br/> + /// Use <see cref="TimeSpan.Zero"/> to avoid any delay (risky). + /// <br/><br/> + /// <strong>DOCS:</strong> <see href="https://github.com/ZiggyCreatures/FusionCache/blob/main/docs/AutoRecovery.md"/> + /// </summary> + [EditorBrowsable(EditorBrowsableState.Never)] + [Obsolete("Backplane auto-recovery is now simply auto-recovery: please use the AutoRecoveryDelay property.")] + public TimeSpan BackplaneAutoRecoveryDelay + { + get; set; + } + + /// <summary> + /// The amount of time to wait before actually processing the auto-recovery queue, to better handle backpressure. + /// <br/> + /// Use <see cref="TimeSpan.Zero"/> to avoid any delay (risky). + /// <br/><br/> + /// <strong>DOCS:</strong> <see href="https://github.com/ZiggyCreatures/FusionCache/blob/main/docs/AutoRecovery.md"/> + /// </summary> + public TimeSpan AutoRecoveryDelay { get; set; } /// <summary> /// Enable expiring a cache entry, only on the distributed cache (if any), when anauto-recovery message is being published on the backplane, to ensure that the value in the distributed cache will not be stale. /// <br/><br/> - /// <strong>DOCS:</strong> <see href="https://github.com/ZiggyCreatures/FusionCache/blob/main/docs/Backplane.md"/> + /// <strong>DOCS:</strong> <see href="https://github.com/ZiggyCreatures/FusionCache/blob/main/docs/AutoRecovery.md"/> /// </summary> + [EditorBrowsable(EditorBrowsableState.Never)] + [Obsolete("This is not needed anymore, everything is handled automatically now.")] public bool EnableDistributedExpireOnBackplaneAutoRecovery { get; set; } + /// <summary> + /// 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 (<see cref="FusionCacheSerializationException"/>, <see cref="FusionCacheDistributedCacheException"/> or <see cref="FusionCacheBackplaneException"/>). + /// </summary> + public bool ReThrowOriginalExceptions { get; set; } + /// <summary> /// Specify the <see cref="LogLevel"/> to use when some options have incoherent values that have been fixed with a normalization, like for example when a FailSafeMaxDuration is lower than a Duration, so the Duration is used instead. /// <br/><br/> @@ -257,14 +391,67 @@ FusionCacheOptions IOptions<FusionCacheOptions>.Value get { return this; } } + ///// <summary> + ///// Set the <see cref="CacheKeyPrefix"/> to the <see cref="CacheName"/>, and a ":" separator. + ///// </summary> + ///// <returns>The <see cref="FusionCacheOptions"/> so that additional calls can be chained.</returns> + //public FusionCacheOptions SetCacheNameAsCacheKeyPrefix() + //{ + // CacheKeyPrefix = $"{CacheName}:"; + + // return this; + //} + /// <summary> - /// Set the <see cref="CacheKeyPrefix"/> to the <see cref="CacheName"/>, and a ":" separator. + /// Creates a new <see cref="FusionCacheOptions"/> object by duplicating all the options of the current one. /// </summary> - /// <returns>The <see cref="FusionCacheOptions"/> so that additional calls can be chained.</returns> - public FusionCacheOptions SetCacheNameAsCacheKeyPrefix() + /// <returns>The newly created <see cref="FusionCacheOptions"/> object.</returns> + public FusionCacheOptions Duplicate() { - CacheKeyPrefix = $"{CacheName}:"; + var res = new FusionCacheOptions + { + CacheName = CacheName, + InstanceId = InstanceId, + + CacheKeyPrefix = CacheKeyPrefix, + + DefaultEntryOptions = DefaultEntryOptions.Duplicate(), + + EnableAutoRecovery = EnableAutoRecovery, + AutoRecoveryDelay = AutoRecoveryDelay, + AutoRecoveryMaxItems = AutoRecoveryMaxItems, + AutoRecoveryMaxRetryCount = AutoRecoveryMaxRetryCount, + + BackplaneChannelPrefix = BackplaneChannelPrefix, + BackplaneCircuitBreakerDuration = BackplaneCircuitBreakerDuration, + + DistributedCacheKeyModifierMode = DistributedCacheKeyModifierMode, + DistributedCacheCircuitBreakerDuration = DistributedCacheCircuitBreakerDuration, + + EnableSyncEventHandlersExecution = EnableSyncEventHandlersExecution, + + ReThrowOriginalExceptions = ReThrowOriginalExceptions, + + // LOG LEVELS + IncoherentOptionsNormalizationLogLevel = IncoherentOptionsNormalizationLogLevel, + + FailSafeActivationLogLevel = FailSafeActivationLogLevel, + FactorySyntheticTimeoutsLogLevel = FactorySyntheticTimeoutsLogLevel, + FactoryErrorsLogLevel = FactoryErrorsLogLevel, + + DistributedCacheSyntheticTimeoutsLogLevel = DistributedCacheSyntheticTimeoutsLogLevel, + DistributedCacheErrorsLogLevel = DistributedCacheErrorsLogLevel, + SerializationErrorsLogLevel = SerializationErrorsLogLevel, + + BackplaneSyntheticTimeoutsLogLevel = BackplaneSyntheticTimeoutsLogLevel, + BackplaneErrorsLogLevel = BackplaneErrorsLogLevel, + + EventHandlingErrorsLogLevel = EventHandlingErrorsLogLevel, + + PluginsErrorsLogLevel = PluginsErrorsLogLevel, + PluginsInfoLogLevel = PluginsInfoLogLevel + }; - return this; + return res; } } diff --git a/src/ZiggyCreatures.FusionCache/FusionCacheSerializationException.cs b/src/ZiggyCreatures.FusionCache/FusionCacheSerializationException.cs new file mode 100644 index 00000000..0d6f8aee --- /dev/null +++ b/src/ZiggyCreatures.FusionCache/FusionCacheSerializationException.cs @@ -0,0 +1,42 @@ +using System; +using System.Runtime.Serialization; + +namespace ZiggyCreatures.Caching.Fusion; + +/// <summary> +/// The generic exception that is thrown when a serialization error occurs: the InnerException contains the original exception. +/// </summary> +[Serializable] +public class FusionCacheSerializationException + : InvalidOperationException +{ + /// <summary> + /// Initializes a new instance of the <see cref="FusionCacheSerializationException"/> class. + /// </summary> + public FusionCacheSerializationException() + { + } + + /// <summary>Initializes a new instance of the <see cref="FusionCacheSerializationException"/> class with a specified error message.</summary> + /// <param name="message">The message that describes the error.</param> + public FusionCacheSerializationException(string? message) + : base(message) + { + } + + /// <summary>Initializes a new instance of the <see cref="FusionCacheSerializationException"/> class with a specified error message and a reference to the inner exception that is the cause of this exception.</summary> + /// <param name="message">The error message that explains the reason for the exception.</param> + /// <param name="innerException">The exception that is the cause of the current exception. If the innerException parameter is not a null reference (Nothing in Visual Basic), the current exception is raised in a catch block that handles the inner exception.</param> + public FusionCacheSerializationException(string? message, Exception? innerException) + : base(message, innerException) + { + } + + /// <summary>Initializes a new instance of the <see cref="FusionCacheSerializationException"/> class with serialized data.</summary> + /// <param name="info">The object that holds the serialized object data.</param> + /// <param name="context">The contextual information about the source or destination.</param> + protected FusionCacheSerializationException(SerializationInfo info, StreamingContext context) + : base(info, context) + { + } +} diff --git a/src/ZiggyCreatures.FusionCache/FusionCacheServiceCollectionExtensions.cs b/src/ZiggyCreatures.FusionCache/FusionCacheServiceCollectionExtensions.cs index 77275274..d2c66d8c 100644 --- a/src/ZiggyCreatures.FusionCache/FusionCacheServiceCollectionExtensions.cs +++ b/src/ZiggyCreatures.FusionCache/FusionCacheServiceCollectionExtensions.cs @@ -1,4 +1,5 @@ using System; +using System.ComponentModel; using Microsoft.Extensions.Caching.Distributed; using Microsoft.Extensions.DependencyInjection.Extensions; using ZiggyCreatures.Caching.Fusion; @@ -36,6 +37,7 @@ private static IServiceCollection AddFusionCacheProvider(this IServiceCollection /// <param name="ignoreMemoryDistributedCache">If the registered <see cref="IDistributedCache"/> found is an instance of <see cref="MemoryDistributedCache"/> (typical when using asp.net) it will be ignored, since it is completely useless (and will consume cpu and memory).</param> /// <param name="setupCacheAction">The <see cref="Action{IServiceProvider,FusionCacheOptions}"/> to configure the newly created <see cref="IFusionCache"/> instance.</param> /// <returns>The <see cref="IServiceCollection"/> so that additional calls can be chained.</returns> + [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)] public static IServiceCollection AddFusionCache(this IServiceCollection services, Action<FusionCacheOptions>? setupOptionsAction = null, bool useDistributedCacheIfAvailable = true, bool ignoreMemoryDistributedCache = true, Action<IServiceProvider, IFusionCache>? setupCacheAction = null) { @@ -86,11 +88,11 @@ public static IServiceCollection AddFusionCache(this IServiceCollection services services.AddFusionCacheProvider(); - services.AddSingleton<IFusionCache>(cache); + //services.AddSingleton<IFusionCache>(cache); if (cache.CacheName == FusionCacheOptions.DefaultCacheName) { - services.AddSingleton<IFusionCache>(cache); + services.TryAddSingleton<IFusionCache>(cache); } else { @@ -131,7 +133,7 @@ public static IFusionCacheBuilder AddFusionCache(this IServiceCollection service if (cacheName == FusionCacheOptions.DefaultCacheName) { - services.AddSingleton<IFusionCache>(serviceProvider => + services.TryAddSingleton<IFusionCache>(serviceProvider => { return builder.Build(serviceProvider); }); diff --git a/src/ZiggyCreatures.FusionCache/FusionCache_Async.cs b/src/ZiggyCreatures.FusionCache/FusionCache_Async.cs index 948c2b5c..cd694498 100644 --- a/src/ZiggyCreatures.FusionCache/FusionCache_Async.cs +++ b/src/ZiggyCreatures.FusionCache/FusionCache_Async.cs @@ -2,8 +2,8 @@ using System.Threading; using System.Threading.Tasks; using Microsoft.Extensions.Logging; -using ZiggyCreatures.Caching.Fusion.Backplane; using ZiggyCreatures.Caching.Fusion.Internals; +using ZiggyCreatures.Caching.Fusion.Internals.Backplane; using ZiggyCreatures.Caching.Fusion.Internals.Distributed; using ZiggyCreatures.Caching.Fusion.Internals.Memory; @@ -12,7 +12,7 @@ namespace ZiggyCreatures.Caching.Fusion; public partial class FusionCache : IFusionCache { - private async ValueTask<IFusionCacheEntry?> GetOrSetEntryInternalAsync<TValue>(string operationId, string key, Func<FusionCacheFactoryExecutionContext<TValue>, CancellationToken, Task<TValue?>> factory, bool isRealFactory, MaybeValue<TValue?> failSafeDefaultValue, FusionCacheEntryOptions? options, CancellationToken token) + private async ValueTask<FusionCacheMemoryEntry?> GetOrSetEntryInternalAsync<TValue>(string operationId, string key, Func<FusionCacheFactoryExecutionContext<TValue>, CancellationToken, Task<TValue?>> factory, bool isRealFactory, MaybeValue<TValue?> failSafeDefaultValue, FusionCacheEntryOptions? options, CancellationToken token) { if (options is null) options = _options.DefaultEntryOptions; @@ -27,12 +27,12 @@ public partial class FusionCache var mca = GetCurrentMemoryAccessor(options); if (mca is not null) { - (memoryEntry, memoryEntryIsValid) = mca.TryGetEntry<TValue>(operationId, key); + (memoryEntry, memoryEntryIsValid) = mca.TryGetEntry(operationId, key); } - IFusionCacheEntry? entry; - bool isStale; - bool hasNewValue = false; + FusionCacheMemoryEntry? entry; + bool isStale = false; + var hasNewValue = false; if (memoryEntryIsValid) { @@ -42,25 +42,25 @@ public partial class FusionCache if (isRealFactory && (memoryEntry!.Metadata?.ShouldEagerlyRefresh() ?? false)) { if (_logger?.IsEnabled(LogLevel.Trace) ?? false) - _logger.Log(LogLevel.Trace, "FUSION [N={CacheName}] (O={CacheOperationId} K={CacheKey}): should eagerly refresh", CacheName, operationId, key); + _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, key, operationId, TimeSpan.Zero, _logger, token).ConfigureAwait(false); + lockObj = await _reactor.AcquireLockAsync(CacheName, InstanceId, key, operationId, TimeSpan.Zero, _logger, token).ConfigureAwait(false); if (lockObj is null) { if (_logger?.IsEnabled(LogLevel.Trace) ?? false) - _logger.Log(LogLevel.Trace, "FUSION [N={CacheName}] (O={CacheOperationId} K={CacheKey}): eager refresh already occurring", CacheName, operationId, key); + _logger.Log(LogLevel.Trace, "FUSION [N={CacheName} I={CacheInstanceId}] (O={CacheOperationId} K={CacheKey}): eager refresh already occurring", CacheName, InstanceId, operationId, key); } else { // EXECUTE EAGER REFRESH - await ExecuteEagerRefreshAsync<TValue>(operationId, key, factory, options, memoryEntry, lockObj, token); + await ExecuteEagerRefreshAsync<TValue>(operationId, key, factory, options, memoryEntry, lockObj, token).ConfigureAwait(false); } } // RETURN THE ENTRY if (_logger?.IsEnabled(LogLevel.Trace) ?? false) - _logger.Log(LogLevel.Trace, "FUSION [N={CacheName}] (O={CacheOperationId} K={CacheKey}): using memory entry", CacheName, operationId, key); + _logger.Log(LogLevel.Trace, "FUSION [N={CacheName} I={CacheInstanceId}] (O={CacheOperationId} K={CacheKey}): using memory entry", CacheName, InstanceId, operationId, key); // EVENT _events.OnHit(operationId, key, memoryEntryIsValid == false || (memoryEntry!.Metadata?.IsFromFailSafe ?? false)); @@ -71,7 +71,7 @@ public partial class FusionCache try { // LOCK - lockObj = await _reactor.AcquireLockAsync(CacheName, key, operationId, options.GetAppropriateLockTimeout(memoryEntry is not null), _logger, token).ConfigureAwait(false); + lockObj = await _reactor.AcquireLockAsync(CacheName, InstanceId, key, operationId, options.GetAppropriateLockTimeout(memoryEntry is not null), _logger, token).ConfigureAwait(false); if (lockObj is null && options.IsFailSafeEnabled && memoryEntry is not null) { @@ -90,13 +90,13 @@ public partial class FusionCache // TRY AGAIN WITH MEMORY CACHE (AFTER THE LOCK HAS BEEN ACQUIRED, MAYBE SOMETHING CHANGED) if (mca is not null) { - (memoryEntry, memoryEntryIsValid) = mca.TryGetEntry<TValue>(operationId, key); + (memoryEntry, memoryEntryIsValid) = mca.TryGetEntry(operationId, key); } if (memoryEntryIsValid) { if (_logger?.IsEnabled(LogLevel.Trace) ?? false) - _logger.Log(LogLevel.Trace, "FUSION [N={CacheName}] (O={CacheOperationId} K={CacheKey}): using memory entry", CacheName, operationId, key); + _logger.Log(LogLevel.Trace, "FUSION [N={CacheName} I={CacheInstanceId}] (O={CacheOperationId} K={CacheKey}): using memory entry", CacheName, InstanceId, operationId, key); // EVENT _events.OnHit(operationId, key, memoryEntryIsValid == false || (memoryEntry?.Metadata?.IsFromFailSafe ?? false)); @@ -113,7 +113,7 @@ public partial class FusionCache { if ((memoryEntry is not null && options.SkipDistributedCacheReadWhenStale) == false) { - (distributedEntry, distributedEntryIsValid) = await dca!.TryGetEntryAsync<TValue>(operationId, key, options, memoryEntry is not null, token).ConfigureAwait(false); + (distributedEntry, distributedEntryIsValid) = await dca!.TryGetEntryAsync<TValue>(operationId, key, options, memoryEntry is not null, null, token).ConfigureAwait(false); } } @@ -130,7 +130,6 @@ public partial class FusionCache { // FACTORY TValue? value; - bool failSafeActivated = false; if (isRealFactory == false) { @@ -144,7 +143,7 @@ public partial class FusionCache var timeout = options.GetAppropriateFactoryTimeout(memoryEntry is not null || distributedEntry is not null); if (_logger?.IsEnabled(LogLevel.Debug) ?? false) - _logger.Log(LogLevel.Debug, "FUSION [N={CacheName}] (O={CacheOperationId} K={CacheKey}): calling the factory (timeout={Timeout})", CacheName, operationId, key, timeout.ToLogString_Timeout()); + _logger.Log(LogLevel.Debug, "FUSION [N={CacheName} I={CacheInstanceId}] (O={CacheOperationId} K={CacheKey}): calling the factory (timeout={Timeout})", CacheName, InstanceId, operationId, key, timeout.ToLogString_Timeout()); var ctx = FusionCacheFactoryExecutionContext<TValue>.CreateFromEntries(options, distributedEntry, memoryEntry); @@ -156,7 +155,7 @@ public partial class FusionCache } else { - value = await FusionCacheExecutionUtils.RunAsyncFuncWithTimeoutAsync(ct => factory(ctx, ct), timeout, options.AllowTimedOutFactoryBackgroundCompletion == false, x => factoryTask = x, token).ConfigureAwait(false); + value = await RunUtils.RunAsyncFuncWithTimeoutAsync(ct => factory(ctx, ct), timeout, options.AllowTimedOutFactoryBackgroundCompletion == false, x => factoryTask = x, token).ConfigureAwait(false); } hasNewValue = true; @@ -188,7 +187,7 @@ public partial class FusionCache MaybeBackgroundCompleteTimedOutFactory<TValue>(operationId, key, ctx, factoryTask, options, token); - if (TryPickFailSafeFallbackValue(operationId, key, distributedEntry, memoryEntry, failSafeDefaultValue, options, out var maybeFallbackValue, out timestamp, out failSafeActivated)) + if (TryPickFailSafeFallbackValue(operationId, key, distributedEntry, memoryEntry, failSafeDefaultValue, options, out var maybeFallbackValue, out timestamp, out isStale)) { value = maybeFallbackValue.Value; } @@ -199,14 +198,7 @@ public partial class FusionCache } } - entry = FusionCacheMemoryEntry.CreateFromOptions(value, options, failSafeActivated, lastModified, etag, timestamp); - isStale = failSafeActivated; - - if (dca.CanBeUsed(operationId, key) && failSafeActivated == false) - { - // SAVE IN THE DISTRIBUTED CACHE (BUT ONLY IF NO FAIL-SAFE HAS BEEN EXECUTED) - await dca!.SetEntryAsync<TValue>(operationId, key, entry, options, token).ConfigureAwait(false); - } + entry = FusionCacheMemoryEntry.CreateFromOptions(value, options, isStale, lastModified, etag, timestamp, typeof(TValue)); } // SAVING THE DATA IN THE MEMORY CACHE (EVEN IF IT IS FROM FAIL-SAFE) @@ -227,12 +219,11 @@ public partial class FusionCache // EVENT if (hasNewValue) { + if (isStale == false) + await DistributedSetEntryAsync<TValue>(operationId, key, entry!, options, token).ConfigureAwait(false); + _events.OnMiss(operationId, key); _events.OnSet(operationId, key); - - // BACKPLANE - if (options.SkipBackplaneNotifications == false) - await PublishInternalAsync(operationId, BackplaneMessage.CreateForEntrySet(key), options, token).ConfigureAwait(false); } else if (entry is not null) { @@ -257,7 +248,7 @@ private async Task ExecuteEagerRefreshAsync<TValue>(string operationId, string k FusionCacheDistributedEntry<TValue>? distributedEntry; bool distributedEntryIsValid; - (distributedEntry, distributedEntryIsValid) = await dca!.TryGetEntryAsync<TValue>(operationId, key, options, memoryEntry is not null, token).ConfigureAwait(false); + (distributedEntry, distributedEntryIsValid) = await dca!.TryGetEntryAsync<TValue>(operationId, key, options, memoryEntry is not null, Timeout.InfiniteTimeSpan, token).ConfigureAwait(false); if (distributedEntryIsValid) { if ((distributedEntry?.Timestamp ?? 0) > (memoryEntry?.Timestamp ?? 0)) @@ -265,14 +256,12 @@ private async Task ExecuteEagerRefreshAsync<TValue>(string operationId, string k try { // THE DISTRIBUTED ENTRY IS MORE RECENT THAN THE MEMORY ENTRY -> USE IT - - if (_logger?.IsEnabled(LogLevel.Trace) ?? false) - _logger.LogTrace("FUSION [N={CacheName}] (O={CacheOperationId} K={CacheKey}): distributed entry found ({DistributedTimestamp}) is more recent than the current memory entry ({MemoryTimestamp}): using it", CacheName, operationId, key, distributedEntry?.Timestamp, memoryEntry?.Timestamp); - - // SAVING THE DATA IN THE MEMORY CACHE var mca = GetCurrentMemoryAccessor(options); if (mca is not null) { + if (_logger?.IsEnabled(LogLevel.Trace) ?? false) + _logger.LogTrace("FUSION [N={CacheName} I={CacheInstanceId}] (O={CacheOperationId} K={CacheKey}): distributed entry found ({DistributedTimestamp}) is more recent than the current memory entry ({MemoryTimestamp}): using it", CacheName, InstanceId, operationId, key, distributedEntry?.Timestamp, memoryEntry?.Timestamp); + mca.SetEntry<TValue>(operationId, key, FusionCacheMemoryEntry.CreateFromOtherEntry<TValue>(distributedEntry!, options), options); } } @@ -286,7 +275,7 @@ private async Task ExecuteEagerRefreshAsync<TValue>(string operationId, string k else { if (_logger?.IsEnabled(LogLevel.Trace) ?? false) - _logger.LogTrace("FUSION [N={CacheName}] (O={CacheOperationId} K={CacheKey}): distributed entry found ({DistributedTimestamp}) is less recent than the current memory entry ({MemoryTimestamp}): ignoring it", CacheName, operationId, key, distributedEntry?.Timestamp, memoryEntry?.Timestamp); + _logger.LogTrace("FUSION [N={CacheName} I={CacheInstanceId}] (O={CacheOperationId} K={CacheKey}): distributed entry found ({DistributedTimestamp}) is less recent than the current memory entry ({MemoryTimestamp}): ignoring it", CacheName, InstanceId, operationId, key, distributedEntry?.Timestamp, memoryEntry?.Timestamp); } } } @@ -296,10 +285,8 @@ private async Task ExecuteEagerRefreshAsync<TValue>(string operationId, string k // EMPTY } - //var timeout = options.GetAppropriateFactoryTimeout(memoryEntry is not null || distributedEntry is not null); - if (_logger?.IsEnabled(LogLevel.Trace) ?? false) - _logger.Log(LogLevel.Trace, "FUSION [N={CacheName}] (O={CacheOperationId} K={CacheKey}): eagerly refreshing", CacheName, operationId, key); + _logger.Log(LogLevel.Trace, "FUSION [N={CacheName} I={CacheInstanceId}] (O={CacheOperationId} K={CacheKey}): eagerly refreshing", CacheName, InstanceId, operationId, key); // EVENT _events.OnEagerRefresh(operationId, key); @@ -326,19 +313,19 @@ private async Task ExecuteEagerRefreshAsync<TValue>(string operationId, string k var operationId = MaybeGenerateOperationId(); if (_logger?.IsEnabled(LogLevel.Debug) ?? false) - _logger.Log(LogLevel.Debug, "FUSION [N={CacheName}] (O={CacheOperationId} K={CacheKey}): calling GetOrSetAsync<T> {Options}", CacheName, operationId, key, options.ToLogString()); + _logger.Log(LogLevel.Debug, "FUSION [N={CacheName} I={CacheInstanceId}] (O={CacheOperationId} K={CacheKey}): calling GetOrSetAsync<T> {Options}", CacheName, InstanceId, operationId, key, options.ToLogString()); var entry = await GetOrSetEntryInternalAsync<TValue>(operationId, key, factory, true, failSafeDefaultValue, options, token).ConfigureAwait(false); if (entry is null) { if (_logger?.IsEnabled(LogLevel.Error) ?? false) - _logger.Log(LogLevel.Error, "FUSION [N={CacheName}] (O={CacheOperationId} K={CacheKey}): something went wrong, the resulting entry is null, and it should not be possible", CacheName, operationId, key); + _logger.Log(LogLevel.Error, "FUSION [N={CacheName} I={CacheInstanceId}] (O={CacheOperationId} K={CacheKey}): something went wrong, the resulting entry is null, and it should not be possible", CacheName, InstanceId, operationId, key); throw new InvalidOperationException("The resulting FusionCache entry is null"); } if (_logger?.IsEnabled(LogLevel.Debug) ?? false) - _logger.Log(LogLevel.Debug, "FUSION [N={CacheName}] (O={CacheOperationId} K={CacheKey}): return {Entry}", CacheName, operationId, key, entry.ToLogString()); + _logger.Log(LogLevel.Debug, "FUSION [N={CacheName} I={CacheInstanceId}] (O={CacheOperationId} K={CacheKey}): return {Entry}", CacheName, InstanceId, operationId, key, entry.ToLogString()); return entry.GetValue<TValue>(); } @@ -355,19 +342,19 @@ private async Task ExecuteEagerRefreshAsync<TValue>(string operationId, string k var operationId = MaybeGenerateOperationId(); if (_logger?.IsEnabled(LogLevel.Debug) ?? false) - _logger.Log(LogLevel.Debug, "FUSION [N={CacheName}] (O={CacheOperationId} K={CacheKey}): calling GetOrSetAsync<T> {Options}", CacheName, operationId, key, options.ToLogString()); + _logger.Log(LogLevel.Debug, "FUSION [N={CacheName} I={CacheInstanceId}] (O={CacheOperationId} K={CacheKey}): calling GetOrSetAsync<T> {Options}", CacheName, InstanceId, operationId, key, options.ToLogString()); var entry = await GetOrSetEntryInternalAsync<TValue>(operationId, key, (_, _) => Task.FromResult(defaultValue), false, default, options, token).ConfigureAwait(false); if (entry is null) { if (_logger?.IsEnabled(LogLevel.Error) ?? false) - _logger.Log(LogLevel.Error, "FUSION [N={CacheName}] (O={CacheOperationId} K={CacheKey}): something went wrong, the resulting entry is null, and it should not be possible", CacheName, operationId, key); + _logger.Log(LogLevel.Error, "FUSION [N={CacheName} I={CacheInstanceId}] (O={CacheOperationId} K={CacheKey}): something went wrong, the resulting entry is null, and it should not be possible", CacheName, InstanceId, operationId, key); throw new InvalidOperationException("The resulting FusionCache entry is null"); } if (_logger?.IsEnabled(LogLevel.Debug) ?? false) - _logger.Log(LogLevel.Debug, "FUSION [N={CacheName}] (O={CacheOperationId} K={CacheKey}): return {Entry}", CacheName, operationId, key, entry.ToLogString()); + _logger.Log(LogLevel.Debug, "FUSION [N={CacheName} I={CacheInstanceId}] (O={CacheOperationId} K={CacheKey}): return {Entry}", CacheName, InstanceId, operationId, key, entry.ToLogString()); return entry.GetValue<TValue>(); } @@ -386,13 +373,13 @@ private async Task ExecuteEagerRefreshAsync<TValue>(string operationId, string k var mca = GetCurrentMemoryAccessor(options); if (mca is not null) { - (memoryEntry, memoryEntryIsValid) = mca.TryGetEntry<TValue>(operationId, key); + (memoryEntry, memoryEntryIsValid) = mca.TryGetEntry(operationId, key); } if (memoryEntryIsValid) { if (_logger?.IsEnabled(LogLevel.Trace) ?? false) - _logger.Log(LogLevel.Trace, "FUSION [N={CacheName}] (O={CacheOperationId} K={CacheKey}): using memory entry", CacheName, operationId, key); + _logger.Log(LogLevel.Trace, "FUSION [N={CacheName} I={CacheInstanceId}] (O={CacheOperationId} K={CacheKey}): using memory entry", CacheName, InstanceId, operationId, key); // EVENT _events.OnHit(operationId, key, memoryEntry!.Metadata?.IsFromFailSafe ?? false); @@ -408,7 +395,7 @@ private async Task ExecuteEagerRefreshAsync<TValue>(string operationId, string k if (options.IsFailSafeEnabled && memoryEntry is not null) { if (_logger?.IsEnabled(LogLevel.Trace) ?? false) - _logger.Log(LogLevel.Trace, "FUSION [N={CacheName}] (O={CacheOperationId} K={CacheKey}): using memory entry (expired)", CacheName, operationId, key); + _logger.Log(LogLevel.Trace, "FUSION [N={CacheName} I={CacheInstanceId}] (O={CacheOperationId} K={CacheKey}): using memory entry (expired)", CacheName, InstanceId, operationId, key); // EVENT _events.OnHit(operationId, key, true); @@ -426,11 +413,11 @@ private async Task ExecuteEagerRefreshAsync<TValue>(string operationId, string k FusionCacheDistributedEntry<TValue>? distributedEntry; bool distributedEntryIsValid; - (distributedEntry, distributedEntryIsValid) = await dca!.TryGetEntryAsync<TValue>(operationId, key, options, memoryEntry is not null, token).ConfigureAwait(false); + (distributedEntry, distributedEntryIsValid) = await dca!.TryGetEntryAsync<TValue>(operationId, key, options, memoryEntry is not null, null, token).ConfigureAwait(false); if (distributedEntryIsValid) { if (_logger?.IsEnabled(LogLevel.Trace) ?? false) - _logger.Log(LogLevel.Trace, "FUSION [N={CacheName}] (O={CacheOperationId} K={CacheKey}): using distributed entry", CacheName, operationId, key); + _logger.Log(LogLevel.Trace, "FUSION [N={CacheName} I={CacheInstanceId}] (O={CacheOperationId} K={CacheKey}): using distributed entry", CacheName, InstanceId, operationId, key); memoryEntry = distributedEntry!.AsMemoryEntry<TValue>(options); @@ -454,7 +441,7 @@ private async Task ExecuteEagerRefreshAsync<TValue>(string operationId, string k if (distributedEntry is not null) { if (_logger?.IsEnabled(LogLevel.Trace) ?? false) - _logger.Log(LogLevel.Trace, "FUSION [N={CacheName}] (O={CacheOperationId} K={CacheKey}): using distributed entry (expired)", CacheName, operationId, key); + _logger.Log(LogLevel.Trace, "FUSION [N={CacheName} I={CacheInstanceId}] (O={CacheOperationId} K={CacheKey}): using distributed entry (expired)", CacheName, InstanceId, operationId, key); memoryEntry = distributedEntry.AsMemoryEntry<TValue>(options); @@ -474,7 +461,7 @@ private async Task ExecuteEagerRefreshAsync<TValue>(string operationId, string k if (memoryEntry is not null) { if (_logger?.IsEnabled(LogLevel.Trace) ?? false) - _logger.Log(LogLevel.Trace, "FUSION [N={CacheName}] (O={CacheOperationId} K={CacheKey}): using memory entry (expired)", CacheName, operationId, key); + _logger.Log(LogLevel.Trace, "FUSION [N={CacheName} I={CacheInstanceId}] (O={CacheOperationId} K={CacheKey}): using memory entry (expired)", CacheName, InstanceId, operationId, key); // EVENT _events.OnHit(operationId, key, true); @@ -501,20 +488,20 @@ public async ValueTask<MaybeValue<TValue>> TryGetAsync<TValue>(string key, Fusio var operationId = MaybeGenerateOperationId(); if (_logger?.IsEnabled(LogLevel.Debug) ?? false) - _logger.Log(LogLevel.Debug, "FUSION [N={CacheName}] (O={CacheOperationId} K={CacheKey}): calling TryGetAsync<T> {Options}", CacheName, operationId, key, options.ToLogString()); + _logger.Log(LogLevel.Debug, "FUSION [N={CacheName} I={CacheInstanceId}] (O={CacheOperationId} K={CacheKey}): calling TryGetAsync<T> {Options}", CacheName, InstanceId, operationId, key, options.ToLogString()); var entry = await TryGetEntryInternalAsync<TValue>(operationId, key, options, token).ConfigureAwait(false); if (entry is null) { if (_logger?.IsEnabled(LogLevel.Debug) ?? false) - _logger.Log(LogLevel.Debug, "FUSION [N={CacheName}] (O={CacheOperationId} K={CacheKey}): return NO SUCCESS", CacheName, operationId, key); + _logger.Log(LogLevel.Debug, "FUSION [N={CacheName} I={CacheInstanceId}] (O={CacheOperationId} K={CacheKey}): return NO SUCCESS", CacheName, InstanceId, operationId, key); return default; } if (_logger?.IsEnabled(LogLevel.Debug) ?? false) - _logger.Log(LogLevel.Debug, "FUSION [N={CacheName}] (O={CacheOperationId} K={CacheKey}): return SUCCESS", CacheName, operationId, key); + _logger.Log(LogLevel.Debug, "FUSION [N={CacheName} I={CacheInstanceId}] (O={CacheOperationId} K={CacheKey}): return SUCCESS", CacheName, InstanceId, operationId, key); return entry.GetValue<TValue>(); } @@ -531,19 +518,19 @@ public async ValueTask<MaybeValue<TValue>> TryGetAsync<TValue>(string key, Fusio var operationId = MaybeGenerateOperationId(); if (_logger?.IsEnabled(LogLevel.Debug) ?? false) - _logger.Log(LogLevel.Debug, "FUSION [N={CacheName}] (O={CacheOperationId} K={CacheKey}): calling GetOrDefaultAsync<T> {Options}", CacheName, operationId, key, options.ToLogString()); + _logger.Log(LogLevel.Debug, "FUSION [N={CacheName} I={CacheInstanceId}] (O={CacheOperationId} K={CacheKey}): calling GetOrDefaultAsync<T> {Options}", CacheName, InstanceId, operationId, key, options.ToLogString()); var entry = await TryGetEntryInternalAsync<TValue>(operationId, key, options, token).ConfigureAwait(false); if (entry is null) { if (_logger?.IsEnabled(LogLevel.Debug) ?? false) - _logger.Log(LogLevel.Debug, "FUSION [N={CacheName}] (O={CacheOperationId} K={CacheKey}): return DEFAULT VALUE", CacheName, operationId, key); + _logger.Log(LogLevel.Debug, "FUSION [N={CacheName} I={CacheInstanceId}] (O={CacheOperationId} K={CacheKey}): return DEFAULT VALUE", CacheName, InstanceId, operationId, key); return defaultValue; } if (_logger?.IsEnabled(LogLevel.Debug) ?? false) - _logger.Log(LogLevel.Debug, "FUSION [N={CacheName}] (O={CacheOperationId} K={CacheKey}): return {Entry}", CacheName, operationId, key, entry.ToLogString()); + _logger.Log(LogLevel.Debug, "FUSION [N={CacheName} I={CacheInstanceId}] (O={CacheOperationId} K={CacheKey}): return {Entry}", CacheName, InstanceId, operationId, key, entry.ToLogString()); return entry.GetValue<TValue>(); } @@ -563,10 +550,10 @@ public async ValueTask SetAsync<TValue>(string key, TValue value, FusionCacheEnt var operationId = MaybeGenerateOperationId(); if (_logger?.IsEnabled(LogLevel.Debug) ?? false) - _logger.Log(LogLevel.Debug, "FUSION [N={CacheName}] (O={CacheOperationId} K={CacheKey}): calling SetAsync<T> {Options}", CacheName, operationId, key, options.ToLogString()); + _logger.Log(LogLevel.Debug, "FUSION [N={CacheName} I={CacheInstanceId}] (O={CacheOperationId} K={CacheKey}): calling SetAsync<T> {Options}", CacheName, InstanceId, operationId, key, options.ToLogString()); // TODO: MAYBE FIND A WAY TO PASS LASTMODIFIED/ETAG HERE - var entry = FusionCacheMemoryEntry.CreateFromOptions(value, options, false, null, null, null); + var entry = FusionCacheMemoryEntry.CreateFromOptions(value, options, false, null, null, null, typeof(TValue)); var mca = GetCurrentMemoryAccessor(options); if (mca is not null) @@ -574,18 +561,10 @@ public async ValueTask SetAsync<TValue>(string key, TValue value, FusionCacheEnt mca.SetEntry<TValue>(operationId, key, entry, options); } - var dca = GetCurrentDistributedAccessor(options); - if (dca.CanBeUsed(operationId, key)) - { - await dca!.SetEntryAsync<TValue>(operationId, key, entry, options, token).ConfigureAwait(false); - } + await DistributedSetEntryAsync<TValue>(operationId, key, entry, options, token).ConfigureAwait(false); // EVENT _events.OnSet(operationId, key); - - // BACKPLANE - if (options.SkipBackplaneNotifications == false) - await PublishInternalAsync(operationId, BackplaneMessage.CreateForEntrySet(key), options, token).ConfigureAwait(false); } /// <inheritdoc/> @@ -603,7 +582,7 @@ public async ValueTask RemoveAsync(string key, FusionCacheEntryOptions? options var operationId = MaybeGenerateOperationId(); if (_logger?.IsEnabled(LogLevel.Debug) ?? false) - _logger.Log(LogLevel.Debug, "FUSION [N={CacheName}] (O={CacheOperationId} K={CacheKey}): calling RemoveAsync {Options}", CacheName, operationId, key, options.ToLogString()); + _logger.Log(LogLevel.Debug, "FUSION [N={CacheName} I={CacheInstanceId}] (O={CacheOperationId} K={CacheKey}): calling RemoveAsync {Options}", CacheName, InstanceId, operationId, key, options.ToLogString()); var mca = GetCurrentMemoryAccessor(options); if (mca is not null) @@ -611,18 +590,10 @@ public async ValueTask RemoveAsync(string key, FusionCacheEntryOptions? options mca.RemoveEntry(operationId, key, options); } - var dca = GetCurrentDistributedAccessor(options); - if (dca.CanBeUsed(operationId, key)) - { - await dca!.RemoveEntryAsync(operationId, key, options, token).ConfigureAwait(false); - } + await DistributedRemoveEntryAsync(operationId, key, options, token).ConfigureAwait(false); // EVENT _events.OnRemove(operationId, key); - - // BACKPLANE - if (options.SkipBackplaneNotifications == false) - await PublishInternalAsync(operationId, BackplaneMessage.CreateForEntryRemove(key), options, token).ConfigureAwait(false); } /// <inheritdoc/> @@ -640,39 +611,159 @@ public async ValueTask ExpireAsync(string key, FusionCacheEntryOptions? options var operationId = MaybeGenerateOperationId(); if (_logger?.IsEnabled(LogLevel.Debug) ?? false) - _logger.Log(LogLevel.Debug, "FUSION [N={CacheName}] (O={CacheOperationId} K={CacheKey}): calling ExpireAsync {Options}", CacheName, operationId, key, options.ToLogString()); + _logger.Log(LogLevel.Debug, "FUSION [N={CacheName} I={CacheInstanceId}] (O={CacheOperationId} K={CacheKey}): calling ExpireAsync {Options}", CacheName, InstanceId, operationId, key, options.ToLogString()); var mca = GetCurrentMemoryAccessor(options); if (mca is not null) { - mca.ExpireEntry(operationId, key, options.IsFailSafeEnabled); + mca.ExpireEntry(operationId, key, options.IsFailSafeEnabled, null); } - var dca = GetCurrentDistributedAccessor(options); - if (dca.CanBeUsed(operationId, key)) - { - await dca!.RemoveEntryAsync(operationId, key, options, token).ConfigureAwait(false); - } + await DistributedExpireEntryAsync(operationId, key, options, token).ConfigureAwait(false); // EVENT _events.OnExpire(operationId, key); + } - // BACKPLANE - if (options.SkipBackplaneNotifications == false) - { - if (options.IsFailSafeEnabled) - await PublishInternalAsync(operationId, BackplaneMessage.CreateForEntryExpire(key), options, token).ConfigureAwait(false); - else - await PublishInternalAsync(operationId, BackplaneMessage.CreateForEntryRemove(key), options, token).ConfigureAwait(false); - } + private async ValueTask ExecuteDistributedActionAsync(string operationId, string key, FusionCacheAction action, long timestamp, Func<DistributedCacheAccessor, bool, CancellationToken, Task<bool>> distributedCacheAction, Func<BackplaneAccessor, bool, CancellationToken, Task<bool>> backplaneAction, FusionCacheEntryOptions options, CancellationToken token) + { + if (RequiresDistributedOperations(options) == false) + return; + + var mustAwaitCompletion = MustAwaitDistributedOperations(options); + var isBackground = !mustAwaitCompletion; + + await RunUtils.RunAsyncActionAdvancedAsync( + async ct1 => + { + // DISTRIBUTED CACHE + var dca = GetCurrentDistributedAccessor(options); + if (dca is not null) + { + var dcaSuccess = false; + try + { + if (dca.IsCurrentlyUsable(operationId, key)) + { + dcaSuccess = await distributedCacheAction(dca, isBackground, ct1).ConfigureAwait(false); + } + } + catch + { + //TryAddAutoRecoveryItem(operationId, key, action, timestamp, options, null); + throw; + } + + if (dcaSuccess == false) + { + _autoRecovery.TryAddItem(operationId, key, action, timestamp, options); + return; + } + } + + var mustAwaitBackplaneCompletion = isBackground || MustAwaitBackplaneOperations(options); + var isBackplaneBackground = isBackground || !mustAwaitBackplaneCompletion; + + await RunUtils.RunAsyncActionAdvancedAsync( + async ct2 => + { + // BACKPLANE + var bpa = GetCurrentBackplaneAccessor(options); + if (bpa is not null) + { + var bpaSuccess = false; + try + { + if (bpa.IsCurrentlyUsable(operationId, key)) + { + bpaSuccess = await backplaneAction(bpa, isBackplaneBackground, ct2).ConfigureAwait(false); + } + } + catch + { + throw; + } + + if (bpaSuccess == false) + { + _autoRecovery.TryAddItem(operationId, key, action, timestamp, options); + } + } + }, + Timeout.InfiniteTimeSpan, + false, + mustAwaitBackplaneCompletion, + null, + true, + token + ).ConfigureAwait(false); + }, + Timeout.InfiniteTimeSpan, + false, + mustAwaitCompletion, + null, + true, + token + ).ConfigureAwait(false); + } + + private ValueTask DistributedSetEntryAsync<TValue>(string operationId, string key, IFusionCacheEntry entry, FusionCacheEntryOptions options, CancellationToken token) + { + return ExecuteDistributedActionAsync( + operationId, + key, + FusionCacheAction.EntrySet, + entry.Timestamp, + async (dca, isBackground, ct) => + { + return await dca!.SetEntryAsync<TValue>(operationId, key, entry, options, isBackground, ct).ConfigureAwait(false); + }, + async (bpa, isBackground, ct) => + { + return await bpa.PublishSetAsync(operationId, key, entry.Timestamp, options, false, isBackground, ct).ConfigureAwait(false); + }, + options, + token + ); } - //[MethodImpl(MethodImplOptions.AggressiveInlining)] - private async ValueTask<bool> PublishInternalAsync(string operationId, BackplaneMessage message, FusionCacheEntryOptions options, CancellationToken token) + private ValueTask DistributedRemoveEntryAsync(string operationId, string key, FusionCacheEntryOptions options, CancellationToken token) { - if (_bpa is null) - return false; + return ExecuteDistributedActionAsync( + operationId, + key, + FusionCacheAction.EntryRemove, + FusionCacheInternalUtils.GetCurrentTimestamp(), + async (dca, isBackground, ct) => + { + return await dca.RemoveEntryAsync(operationId, key, options, isBackground, ct).ConfigureAwait(false); + }, + async (bpa, isBackground, ct) => + { + return await bpa.PublishRemoveAsync(operationId, key, null, options, false, isBackground, ct).ConfigureAwait(false); + }, + options, + token + ); + } - return await _bpa.PublishAsync(operationId, message, options, false, token); + private ValueTask DistributedExpireEntryAsync(string operationId, string key, FusionCacheEntryOptions options, CancellationToken token) + { + return ExecuteDistributedActionAsync( + operationId, + key, + FusionCacheAction.EntryExpire, + FusionCacheInternalUtils.GetCurrentTimestamp(), + async (dca, isBackground, ct) => + { + return await dca.RemoveEntryAsync(operationId, key, options, isBackground, ct).ConfigureAwait(false); + }, + async (bpa, isBackground, ct) => + { + return await bpa.PublishExpireAsync(operationId, key, null, options, false, isBackground, ct).ConfigureAwait(false); + }, + options, + token + ); } } diff --git a/src/ZiggyCreatures.FusionCache/FusionCache_Sync.cs b/src/ZiggyCreatures.FusionCache/FusionCache_Sync.cs index cf265a85..8000ce47 100644 --- a/src/ZiggyCreatures.FusionCache/FusionCache_Sync.cs +++ b/src/ZiggyCreatures.FusionCache/FusionCache_Sync.cs @@ -2,8 +2,8 @@ using System.Threading; using System.Threading.Tasks; using Microsoft.Extensions.Logging; -using ZiggyCreatures.Caching.Fusion.Backplane; using ZiggyCreatures.Caching.Fusion.Internals; +using ZiggyCreatures.Caching.Fusion.Internals.Backplane; using ZiggyCreatures.Caching.Fusion.Internals.Distributed; using ZiggyCreatures.Caching.Fusion.Internals.Memory; @@ -12,7 +12,7 @@ namespace ZiggyCreatures.Caching.Fusion; public partial class FusionCache : IFusionCache { - private IFusionCacheEntry? GetOrSetEntryInternal<TValue>(string operationId, string key, Func<FusionCacheFactoryExecutionContext<TValue>, CancellationToken, TValue?> factory, bool isRealFactory, MaybeValue<TValue?> failSafeDefaultValue, FusionCacheEntryOptions? options, CancellationToken token) + private FusionCacheMemoryEntry? GetOrSetEntryInternal<TValue>(string operationId, string key, Func<FusionCacheFactoryExecutionContext<TValue>, CancellationToken, TValue?> factory, bool isRealFactory, MaybeValue<TValue?> failSafeDefaultValue, FusionCacheEntryOptions? options, CancellationToken token) { if (options is null) options = _options.DefaultEntryOptions; @@ -27,12 +27,12 @@ public partial class FusionCache var mca = GetCurrentMemoryAccessor(options); if (mca is not null) { - (memoryEntry, memoryEntryIsValid) = mca.TryGetEntry<TValue>(operationId, key); + (memoryEntry, memoryEntryIsValid) = mca.TryGetEntry(operationId, key); } - IFusionCacheEntry? entry; - bool isStale; - bool hasNewValue = false; + FusionCacheMemoryEntry? entry; + bool isStale = false; + var hasNewValue = false; if (memoryEntryIsValid) { @@ -42,14 +42,14 @@ public partial class FusionCache if (isRealFactory && (memoryEntry!.Metadata?.ShouldEagerlyRefresh() ?? false)) { if (_logger?.IsEnabled(LogLevel.Trace) ?? false) - _logger.Log(LogLevel.Trace, "FUSION [N={CacheName}] (O={CacheOperationId} K={CacheKey}): should eagerly refresh", CacheName, operationId, key); + _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, key, operationId, TimeSpan.Zero, _logger); + lockObj = _reactor.AcquireLock(CacheName, InstanceId, key, operationId, TimeSpan.Zero, _logger); if (lockObj is null) { if (_logger?.IsEnabled(LogLevel.Trace) ?? false) - _logger.Log(LogLevel.Trace, "FUSION [N={CacheName}] (O={CacheOperationId} K={CacheKey}): eager refresh already occurring", CacheName, operationId, key); + _logger.Log(LogLevel.Trace, "FUSION [N={CacheName} I={CacheInstanceId}] (O={CacheOperationId} K={CacheKey}): eager refresh already occurring", CacheName, InstanceId, operationId, key); } else { @@ -60,7 +60,7 @@ public partial class FusionCache // RETURN THE ENTRY if (_logger?.IsEnabled(LogLevel.Trace) ?? false) - _logger.Log(LogLevel.Trace, "FUSION [N={CacheName}] (O={CacheOperationId} K={CacheKey}): using memory entry", CacheName, operationId, key); + _logger.Log(LogLevel.Trace, "FUSION [N={CacheName} I={CacheInstanceId}] (O={CacheOperationId} K={CacheKey}): using memory entry", CacheName, InstanceId, operationId, key); // EVENT _events.OnHit(operationId, key, memoryEntryIsValid == false || (memoryEntry!.Metadata?.IsFromFailSafe ?? false)); @@ -71,7 +71,7 @@ public partial class FusionCache try { // LOCK - lockObj = _reactor.AcquireLock(CacheName, key, operationId, options.GetAppropriateLockTimeout(memoryEntry is not null), _logger); + lockObj = _reactor.AcquireLock(CacheName, InstanceId, key, operationId, options.GetAppropriateLockTimeout(memoryEntry is not null), _logger); if (lockObj is null && options.IsFailSafeEnabled && memoryEntry is not null) { @@ -90,13 +90,13 @@ public partial class FusionCache // TRY AGAIN WITH MEMORY CACHE (AFTER THE LOCK HAS BEEN ACQUIRED, MAYBE SOMETHING CHANGED) if (mca is not null) { - (memoryEntry, memoryEntryIsValid) = mca.TryGetEntry<TValue>(operationId, key); + (memoryEntry, memoryEntryIsValid) = mca.TryGetEntry(operationId, key); } if (memoryEntryIsValid) { if (_logger?.IsEnabled(LogLevel.Trace) ?? false) - _logger.Log(LogLevel.Trace, "FUSION [N={CacheName}] (O={CacheOperationId} K={CacheKey}): using memory entry", CacheName, operationId, key); + _logger.Log(LogLevel.Trace, "FUSION [N={CacheName} I={CacheInstanceId}] (O={CacheOperationId} K={CacheKey}): using memory entry", CacheName, InstanceId, operationId, key); // EVENT _events.OnHit(operationId, key, memoryEntryIsValid == false || (memoryEntry?.Metadata?.IsFromFailSafe ?? false)); @@ -113,7 +113,7 @@ public partial class FusionCache { if ((memoryEntry is not null && options.SkipDistributedCacheReadWhenStale) == false) { - (distributedEntry, distributedEntryIsValid) = dca!.TryGetEntry<TValue>(operationId, key, options, memoryEntry is not null, token); + (distributedEntry, distributedEntryIsValid) = dca!.TryGetEntry<TValue>(operationId, key, options, memoryEntry is not null, null, token); } } @@ -130,7 +130,6 @@ public partial class FusionCache { // FACTORY TValue? value; - bool failSafeActivated = false; if (isRealFactory == false) { @@ -144,7 +143,7 @@ public partial class FusionCache var timeout = options.GetAppropriateFactoryTimeout(memoryEntry is not null || distributedEntry is not null); if (_logger?.IsEnabled(LogLevel.Debug) ?? false) - _logger.Log(LogLevel.Debug, "FUSION [N={CacheName}] (O={CacheOperationId} K={CacheKey}): calling the factory (timeout={Timeout})", CacheName, operationId, key, timeout.ToLogString_Timeout()); + _logger.Log(LogLevel.Debug, "FUSION [N={CacheName} I={CacheInstanceId}] (O={CacheOperationId} K={CacheKey}): calling the factory (timeout={Timeout})", CacheName, InstanceId, operationId, key, timeout.ToLogString_Timeout()); var ctx = FusionCacheFactoryExecutionContext<TValue>.CreateFromEntries(options, distributedEntry, memoryEntry); @@ -156,7 +155,7 @@ public partial class FusionCache } else { - value = FusionCacheExecutionUtils.RunSyncFuncWithTimeout(ct => factory(ctx, ct), timeout, options.AllowTimedOutFactoryBackgroundCompletion == false, x => factoryTask = x, token); + value = RunUtils.RunSyncFuncWithTimeout(ct => factory(ctx, ct), timeout, options.AllowTimedOutFactoryBackgroundCompletion == false, x => factoryTask = x, token); } hasNewValue = true; @@ -188,7 +187,7 @@ public partial class FusionCache MaybeBackgroundCompleteTimedOutFactory<TValue>(operationId, key, ctx, factoryTask, options, token); - if (TryPickFailSafeFallbackValue(operationId, key, distributedEntry, memoryEntry, failSafeDefaultValue, options, out var maybeFallbackValue, out timestamp, out failSafeActivated)) + if (TryPickFailSafeFallbackValue(operationId, key, distributedEntry, memoryEntry, failSafeDefaultValue, options, out var maybeFallbackValue, out timestamp, out isStale)) { value = maybeFallbackValue.Value; } @@ -199,14 +198,7 @@ public partial class FusionCache } } - entry = FusionCacheMemoryEntry.CreateFromOptions(value, options, failSafeActivated, lastModified, etag, timestamp); - isStale = failSafeActivated; - - if (dca.CanBeUsed(operationId, key) && failSafeActivated == false) - { - // SAVE IN THE DISTRIBUTED CACHE (BUT ONLY IF NO FAIL-SAFE HAS BEEN EXECUTED) - dca!.SetEntry<TValue>(operationId, key, entry, options, token); - } + entry = FusionCacheMemoryEntry.CreateFromOptions(value, options, isStale, lastModified, etag, timestamp, typeof(TValue)); } // SAVING THE DATA IN THE MEMORY CACHE (EVEN IF IT IS FROM FAIL-SAFE) @@ -227,12 +219,11 @@ public partial class FusionCache // EVENT if (hasNewValue) { + if (isStale == false) + DistributedSetEntry<TValue>(operationId, key, entry!, options, token); + _events.OnMiss(operationId, key); _events.OnSet(operationId, key); - - // BACKPLANE - if (options.SkipBackplaneNotifications == false) - PublishInternal(operationId, BackplaneMessage.CreateForEntrySet(key), options, token); } else if (entry is not null) { @@ -257,7 +248,7 @@ private void ExecuteEagerRefresh<TValue>(string operationId, string key, Func<Fu FusionCacheDistributedEntry<TValue>? distributedEntry; bool distributedEntryIsValid; - (distributedEntry, distributedEntryIsValid) = dca!.TryGetEntry<TValue>(operationId, key, options, memoryEntry is not null, token); + (distributedEntry, distributedEntryIsValid) = dca!.TryGetEntry<TValue>(operationId, key, options, memoryEntry is not null, Timeout.InfiniteTimeSpan, token); if (distributedEntryIsValid) { if ((distributedEntry?.Timestamp ?? 0) > (memoryEntry?.Timestamp ?? 0)) @@ -265,14 +256,12 @@ private void ExecuteEagerRefresh<TValue>(string operationId, string key, Func<Fu try { // THE DISTRIBUTED ENTRY IS MORE RECENT THAN THE MEMORY ENTRY -> USE IT - - if (_logger?.IsEnabled(LogLevel.Trace) ?? false) - _logger.LogTrace("FUSION [N={CacheName}] (O={CacheOperationId} K={CacheKey}): distributed entry found ({DistributedTimestamp}) is more recent than the current memory entry ({MemoryTimestamp}): using it", CacheName, operationId, key, distributedEntry?.Timestamp, memoryEntry?.Timestamp); - - // SAVING THE DATA IN THE MEMORY CACHE var mca = GetCurrentMemoryAccessor(options); if (mca is not null) { + if (_logger?.IsEnabled(LogLevel.Trace) ?? false) + _logger.LogTrace("FUSION [N={CacheName} I={CacheInstanceId}] (O={CacheOperationId} K={CacheKey}): distributed entry found ({DistributedTimestamp}) is more recent than the current memory entry ({MemoryTimestamp}): using it", CacheName, InstanceId, operationId, key, distributedEntry?.Timestamp, memoryEntry?.Timestamp); + mca.SetEntry<TValue>(operationId, key, FusionCacheMemoryEntry.CreateFromOtherEntry<TValue>(distributedEntry!, options), options); } } @@ -286,7 +275,7 @@ private void ExecuteEagerRefresh<TValue>(string operationId, string key, Func<Fu else { if (_logger?.IsEnabled(LogLevel.Trace) ?? false) - _logger.LogTrace("FUSION [N={CacheName}] (O={CacheOperationId} K={CacheKey}): distributed entry found ({DistributedTimestamp}) is less recent than the current memory entry ({MemoryTimestamp}): ignoring it", CacheName, operationId, key, distributedEntry?.Timestamp, memoryEntry?.Timestamp); + _logger.LogTrace("FUSION [N={CacheName} I={CacheInstanceId}] (O={CacheOperationId} K={CacheKey}): distributed entry found ({DistributedTimestamp}) is less recent than the current memory entry ({MemoryTimestamp}): ignoring it", CacheName, InstanceId, operationId, key, distributedEntry?.Timestamp, memoryEntry?.Timestamp); } } } @@ -296,10 +285,8 @@ private void ExecuteEagerRefresh<TValue>(string operationId, string key, Func<Fu // EMPTY } - //var timeout = options.GetAppropriateFactoryTimeout(memoryEntry is not null || distributedEntry is not null); - if (_logger?.IsEnabled(LogLevel.Trace) ?? false) - _logger.Log(LogLevel.Trace, "FUSION [N={CacheName}] (O={CacheOperationId} K={CacheKey}): eagerly refreshing", CacheName, operationId, key); + _logger.Log(LogLevel.Trace, "FUSION [N={CacheName} I={CacheInstanceId}] (O={CacheOperationId} K={CacheKey}): eagerly refreshing", CacheName, InstanceId, operationId, key); // EVENT _events.OnEagerRefresh(operationId, key); @@ -326,19 +313,19 @@ private void ExecuteEagerRefresh<TValue>(string operationId, string key, Func<Fu var operationId = MaybeGenerateOperationId(); if (_logger?.IsEnabled(LogLevel.Debug) ?? false) - _logger.Log(LogLevel.Debug, "FUSION [N={CacheName}] (O={CacheOperationId} K={CacheKey}): calling GetOrSet<T> {Options}", CacheName, operationId, key, options.ToLogString()); + _logger.Log(LogLevel.Debug, "FUSION [N={CacheName} I={CacheInstanceId}] (O={CacheOperationId} K={CacheKey}): calling GetOrSet<T> {Options}", CacheName, InstanceId, operationId, key, options.ToLogString()); var entry = GetOrSetEntryInternal<TValue>(operationId, key, factory, true, failSafeDefaultValue, options, token); if (entry is null) { if (_logger?.IsEnabled(LogLevel.Error) ?? false) - _logger.Log(LogLevel.Error, "FUSION [N={CacheName}] (O={CacheOperationId} K={CacheKey}): something went wrong, the resulting entry is null, and it should not be possible", CacheName, operationId, key); + _logger.Log(LogLevel.Error, "FUSION [N={CacheName} I={CacheInstanceId}] (O={CacheOperationId} K={CacheKey}): something went wrong, the resulting entry is null, and it should not be possible", CacheName, InstanceId, operationId, key); throw new InvalidOperationException("The resulting FusionCache entry is null"); } if (_logger?.IsEnabled(LogLevel.Debug) ?? false) - _logger.Log(LogLevel.Debug, "FUSION [N={CacheName}] (O={CacheOperationId} K={CacheKey}): return {Entry}", CacheName, operationId, key, entry.ToLogString()); + _logger.Log(LogLevel.Debug, "FUSION [N={CacheName} I={CacheInstanceId}] (O={CacheOperationId} K={CacheKey}): return {Entry}", CacheName, InstanceId, operationId, key, entry.ToLogString()); return entry.GetValue<TValue>(); } @@ -355,19 +342,19 @@ private void ExecuteEagerRefresh<TValue>(string operationId, string key, Func<Fu var operationId = MaybeGenerateOperationId(); if (_logger?.IsEnabled(LogLevel.Debug) ?? false) - _logger.Log(LogLevel.Debug, "FUSION [N={CacheName}] (O={CacheOperationId} K={CacheKey}): calling GetOrSet<T> {Options}", CacheName, operationId, key, options.ToLogString()); + _logger.Log(LogLevel.Debug, "FUSION [N={CacheName} I={CacheInstanceId}] (O={CacheOperationId} K={CacheKey}): calling GetOrSet<T> {Options}", CacheName, InstanceId, operationId, key, options.ToLogString()); var entry = GetOrSetEntryInternal<TValue>(operationId, key, (_, _) => defaultValue, false, default, options, token); if (entry is null) { if (_logger?.IsEnabled(LogLevel.Error) ?? false) - _logger.Log(LogLevel.Error, "FUSION [N={CacheName}] (O={CacheOperationId} K={CacheKey}): something went wrong, the resulting entry is null, and it should not be possible", CacheName, operationId, key); + _logger.Log(LogLevel.Error, "FUSION [N={CacheName} I={CacheInstanceId}] (O={CacheOperationId} K={CacheKey}): something went wrong, the resulting entry is null, and it should not be possible", CacheName, InstanceId, operationId, key); throw new InvalidOperationException("The resulting FusionCache entry is null"); } if (_logger?.IsEnabled(LogLevel.Debug) ?? false) - _logger.Log(LogLevel.Debug, "FUSION [N={CacheName}] (O={CacheOperationId} K={CacheKey}): return {Entry}", CacheName, operationId, key, entry.ToLogString()); + _logger.Log(LogLevel.Debug, "FUSION [N={CacheName} I={CacheInstanceId}] (O={CacheOperationId} K={CacheKey}): return {Entry}", CacheName, InstanceId, operationId, key, entry.ToLogString()); return entry.GetValue<TValue>(); } @@ -386,13 +373,13 @@ private void ExecuteEagerRefresh<TValue>(string operationId, string key, Func<Fu var mca = GetCurrentMemoryAccessor(options); if (mca is not null) { - (memoryEntry, memoryEntryIsValid) = mca.TryGetEntry<TValue>(operationId, key); + (memoryEntry, memoryEntryIsValid) = mca.TryGetEntry(operationId, key); } if (memoryEntryIsValid) { if (_logger?.IsEnabled(LogLevel.Trace) ?? false) - _logger.Log(LogLevel.Trace, "FUSION [N={CacheName}] (O={CacheOperationId} K={CacheKey}): using memory entry", CacheName, operationId, key); + _logger.Log(LogLevel.Trace, "FUSION [N={CacheName} I={CacheInstanceId}] (O={CacheOperationId} K={CacheKey}): using memory entry", CacheName, InstanceId, operationId, key); // EVENT _events.OnHit(operationId, key, memoryEntry!.Metadata?.IsFromFailSafe ?? false); @@ -408,7 +395,7 @@ private void ExecuteEagerRefresh<TValue>(string operationId, string key, Func<Fu if (options.IsFailSafeEnabled && memoryEntry is not null) { if (_logger?.IsEnabled(LogLevel.Trace) ?? false) - _logger.Log(LogLevel.Trace, "FUSION [N={CacheName}] (O={CacheOperationId} K={CacheKey}): using memory entry (expired)", CacheName, operationId, key); + _logger.Log(LogLevel.Trace, "FUSION [N={CacheName} I={CacheInstanceId}] (O={CacheOperationId} K={CacheKey}): using memory entry (expired)", CacheName, InstanceId, operationId, key); // EVENT _events.OnHit(operationId, key, true); @@ -426,11 +413,11 @@ private void ExecuteEagerRefresh<TValue>(string operationId, string key, Func<Fu FusionCacheDistributedEntry<TValue>? distributedEntry; bool distributedEntryIsValid; - (distributedEntry, distributedEntryIsValid) = dca!.TryGetEntry<TValue>(operationId, key, options, memoryEntry is not null, token); + (distributedEntry, distributedEntryIsValid) = dca!.TryGetEntry<TValue>(operationId, key, options, memoryEntry is not null, null, token); if (distributedEntryIsValid) { if (_logger?.IsEnabled(LogLevel.Trace) ?? false) - _logger.Log(LogLevel.Trace, "FUSION [N={CacheName}] (O={CacheOperationId} K={CacheKey}): using distributed entry", CacheName, operationId, key); + _logger.Log(LogLevel.Trace, "FUSION [N={CacheName} I={CacheInstanceId}] (O={CacheOperationId} K={CacheKey}): using distributed entry", CacheName, InstanceId, operationId, key); memoryEntry = distributedEntry!.AsMemoryEntry<TValue>(options); @@ -454,7 +441,7 @@ private void ExecuteEagerRefresh<TValue>(string operationId, string key, Func<Fu if (distributedEntry is not null) { if (_logger?.IsEnabled(LogLevel.Trace) ?? false) - _logger.Log(LogLevel.Trace, "FUSION [N={CacheName}] (O={CacheOperationId} K={CacheKey}): using distributed entry (expired)", CacheName, operationId, key); + _logger.Log(LogLevel.Trace, "FUSION [N={CacheName} I={CacheInstanceId}] (O={CacheOperationId} K={CacheKey}): using distributed entry (expired)", CacheName, InstanceId, operationId, key); memoryEntry = distributedEntry.AsMemoryEntry<TValue>(options); @@ -474,7 +461,7 @@ private void ExecuteEagerRefresh<TValue>(string operationId, string key, Func<Fu if (memoryEntry is not null) { if (_logger?.IsEnabled(LogLevel.Trace) ?? false) - _logger.Log(LogLevel.Trace, "FUSION [N={CacheName}] (O={CacheOperationId} K={CacheKey}): using memory entry (expired)", CacheName, operationId, key); + _logger.Log(LogLevel.Trace, "FUSION [N={CacheName} I={CacheInstanceId}] (O={CacheOperationId} K={CacheKey}): using memory entry (expired)", CacheName, InstanceId, operationId, key); // EVENT _events.OnHit(operationId, key, true); @@ -501,20 +488,20 @@ public MaybeValue<TValue> TryGet<TValue>(string key, FusionCacheEntryOptions? op var operationId = MaybeGenerateOperationId(); if (_logger?.IsEnabled(LogLevel.Debug) ?? false) - _logger.Log(LogLevel.Debug, "FUSION [N={CacheName}] (O={CacheOperationId} K={CacheKey}): calling TryGet<T> {Options}", CacheName, operationId, key, options.ToLogString()); + _logger.Log(LogLevel.Debug, "FUSION [N={CacheName} I={CacheInstanceId}] (O={CacheOperationId} K={CacheKey}): calling TryGet<T> {Options}", CacheName, InstanceId, operationId, key, options.ToLogString()); var entry = TryGetEntryInternal<TValue>(operationId, key, options, token); if (entry is null) { if (_logger?.IsEnabled(LogLevel.Debug) ?? false) - _logger.Log(LogLevel.Debug, "FUSION [N={CacheName}] (O={CacheOperationId} K={CacheKey}): return NO SUCCESS", CacheName, operationId, key); + _logger.Log(LogLevel.Debug, "FUSION [N={CacheName} I={CacheInstanceId}] (O={CacheOperationId} K={CacheKey}): return NO SUCCESS", CacheName, InstanceId, operationId, key); return default; } if (_logger?.IsEnabled(LogLevel.Debug) ?? false) - _logger.Log(LogLevel.Debug, "FUSION [N={CacheName}] (O={CacheOperationId} K={CacheKey}): return SUCCESS", CacheName, operationId, key); + _logger.Log(LogLevel.Debug, "FUSION [N={CacheName} I={CacheInstanceId}] (O={CacheOperationId} K={CacheKey}): return SUCCESS", CacheName, InstanceId, operationId, key); return entry.GetValue<TValue>(); } @@ -531,19 +518,19 @@ public MaybeValue<TValue> TryGet<TValue>(string key, FusionCacheEntryOptions? op var operationId = MaybeGenerateOperationId(); if (_logger?.IsEnabled(LogLevel.Debug) ?? false) - _logger.Log(LogLevel.Debug, "FUSION [N={CacheName}] (O={CacheOperationId} K={CacheKey}): calling GetOrDefault<T> {Options}", CacheName, operationId, key, options.ToLogString()); + _logger.Log(LogLevel.Debug, "FUSION [N={CacheName} I={CacheInstanceId}] (O={CacheOperationId} K={CacheKey}): calling GetOrDefault<T> {Options}", CacheName, InstanceId, operationId, key, options.ToLogString()); var entry = TryGetEntryInternal<TValue>(operationId, key, options, token); if (entry is null) { if (_logger?.IsEnabled(LogLevel.Debug) ?? false) - _logger.Log(LogLevel.Debug, "FUSION [N={CacheName}] (O={CacheOperationId} K={CacheKey}): return DEFAULT VALUE", CacheName, operationId, key); + _logger.Log(LogLevel.Debug, "FUSION [N={CacheName} I={CacheInstanceId}] (O={CacheOperationId} K={CacheKey}): return DEFAULT VALUE", CacheName, InstanceId, operationId, key); return defaultValue; } if (_logger?.IsEnabled(LogLevel.Debug) ?? false) - _logger.Log(LogLevel.Debug, "FUSION [N={CacheName}] (O={CacheOperationId} K={CacheKey}): return {Entry}", CacheName, operationId, key, entry.ToLogString()); + _logger.Log(LogLevel.Debug, "FUSION [N={CacheName} I={CacheInstanceId}] (O={CacheOperationId} K={CacheKey}): return {Entry}", CacheName, InstanceId, operationId, key, entry.ToLogString()); return entry.GetValue<TValue>(); } @@ -563,10 +550,10 @@ public void Set<TValue>(string key, TValue value, FusionCacheEntryOptions? optio var operationId = MaybeGenerateOperationId(); if (_logger?.IsEnabled(LogLevel.Debug) ?? false) - _logger.Log(LogLevel.Debug, "FUSION [N={CacheName}] (O={CacheOperationId} K={CacheKey}): calling Set<T> {Options}", CacheName, operationId, key, options.ToLogString()); + _logger.Log(LogLevel.Debug, "FUSION [N={CacheName} I={CacheInstanceId}] (O={CacheOperationId} K={CacheKey}): calling Set<T> {Options}", CacheName, InstanceId, operationId, key, options.ToLogString()); // TODO: MAYBE FIND A WAY TO PASS LASTMODIFIED/ETAG HERE - var entry = FusionCacheMemoryEntry.CreateFromOptions(value, options, false, null, null, null); + var entry = FusionCacheMemoryEntry.CreateFromOptions(value, options, false, null, null, null, typeof(TValue)); var mca = GetCurrentMemoryAccessor(options); if (mca is not null) @@ -574,18 +561,10 @@ public void Set<TValue>(string key, TValue value, FusionCacheEntryOptions? optio mca.SetEntry<TValue>(operationId, key, entry, options); } - var dca = GetCurrentDistributedAccessor(options); - if (dca.CanBeUsed(operationId, key)) - { - dca!.SetEntry<TValue>(operationId, key, entry, options, token); - } + DistributedSetEntry<TValue>(operationId, key, entry, options, token); // EVENT _events.OnSet(operationId, key); - - // BACKPLANE - if (options.SkipBackplaneNotifications == false) - PublishInternal(operationId, BackplaneMessage.CreateForEntrySet(key), options, token); } /// <inheritdoc/> @@ -603,7 +582,7 @@ public void Remove(string key, FusionCacheEntryOptions? options = null, Cancella var operationId = MaybeGenerateOperationId(); if (_logger?.IsEnabled(LogLevel.Debug) ?? false) - _logger.Log(LogLevel.Debug, "FUSION [N={CacheName}] (O={CacheOperationId} K={CacheKey}): calling Remove {Options}", CacheName, operationId, key, options.ToLogString()); + _logger.Log(LogLevel.Debug, "FUSION [N={CacheName} I={CacheInstanceId}] (O={CacheOperationId} K={CacheKey}): calling Remove {Options}", CacheName, InstanceId, operationId, key, options.ToLogString()); var mca = GetCurrentMemoryAccessor(options); if (mca is not null) @@ -611,18 +590,10 @@ public void Remove(string key, FusionCacheEntryOptions? options = null, Cancella mca.RemoveEntry(operationId, key, options); } - var dca = GetCurrentDistributedAccessor(options); - if (dca.CanBeUsed(operationId, key)) - { - dca!.RemoveEntry(operationId, key, options, token); - } + DistributedRemoveEntry(operationId, key, options, token); // EVENT _events.OnRemove(operationId, key); - - // BACKPLANE - if (options.SkipBackplaneNotifications == false) - PublishInternal(operationId, BackplaneMessage.CreateForEntryRemove(key), options, token); } /// <inheritdoc/> @@ -640,39 +611,159 @@ public void Expire(string key, FusionCacheEntryOptions? options = null, Cancella var operationId = MaybeGenerateOperationId(); if (_logger?.IsEnabled(LogLevel.Debug) ?? false) - _logger.Log(LogLevel.Debug, "FUSION [N={CacheName}] (O={CacheOperationId} K={CacheKey}): calling Expire {Options}", CacheName, operationId, key, options.ToLogString()); + _logger.Log(LogLevel.Debug, "FUSION [N={CacheName} I={CacheInstanceId}] (O={CacheOperationId} K={CacheKey}): calling Expire {Options}", CacheName, InstanceId, operationId, key, options.ToLogString()); var mca = GetCurrentMemoryAccessor(options); if (mca is not null) { - mca.ExpireEntry(operationId, key, options.IsFailSafeEnabled); + mca.ExpireEntry(operationId, key, options.IsFailSafeEnabled, null); } - var dca = GetCurrentDistributedAccessor(options); - if (dca.CanBeUsed(operationId, key)) - { - dca!.RemoveEntry(operationId, key, options, token); - } + DistributedExpireEntry(operationId, key, options, token); // EVENT _events.OnExpire(operationId, key); + } - // BACKPLANE - if (options.SkipBackplaneNotifications == false) - { - if (options.IsFailSafeEnabled) - PublishInternal(operationId, BackplaneMessage.CreateForEntryExpire(key), options, token); - else - PublishInternal(operationId, BackplaneMessage.CreateForEntryRemove(key), options, token); - } + private void ExecuteDistributedAction(string operationId, string key, FusionCacheAction action, long timestamp, Func<DistributedCacheAccessor, bool, CancellationToken, bool> distributedCacheAction, Func<BackplaneAccessor, bool, CancellationToken, bool> backplaneAction, FusionCacheEntryOptions options, CancellationToken token) + { + if (RequiresDistributedOperations(options) == false) + return; + + var mustAwaitCompletion = MustAwaitDistributedOperations(options); + var isBackground = !mustAwaitCompletion; + + RunUtils.RunSyncActionAdvanced( + ct1 => + { + // DISTRIBUTED CACHE + var dca = GetCurrentDistributedAccessor(options); + if (dca is not null) + { + var dcaSuccess = false; + try + { + if (dca.IsCurrentlyUsable(operationId, key)) + { + dcaSuccess = distributedCacheAction(dca, isBackground, ct1); + } + } + catch + { + //TryAddAutoRecoveryItem(operationId, key, action, timestamp, options, null); + throw; + } + + if (dcaSuccess == false) + { + _autoRecovery.TryAddItem(operationId, key, action, timestamp, options); + return; + } + } + + var mustAwaitBackplaneCompletion = isBackground || MustAwaitBackplaneOperations(options); + var isBackplaneBackground = isBackground || !mustAwaitBackplaneCompletion; + + RunUtils.RunSyncActionAdvanced( + ct2 => + { + // BACKPLANE + var bpa = GetCurrentBackplaneAccessor(options); + if (bpa is not null) + { + var bpaSuccess = false; + try + { + if (bpa.IsCurrentlyUsable(operationId, key)) + { + bpaSuccess = backplaneAction(bpa, isBackplaneBackground, ct2); + } + } + catch + { + throw; + } + + if (bpaSuccess == false) + { + _autoRecovery.TryAddItem(operationId, key, action, timestamp, options); + } + } + }, + Timeout.InfiniteTimeSpan, + false, + mustAwaitBackplaneCompletion, + null, + true, + token + ); + }, + Timeout.InfiniteTimeSpan, + false, + mustAwaitCompletion, + null, + true, + token + ); + } + + private void DistributedSetEntry<TValue>(string operationId, string key, IFusionCacheEntry entry, FusionCacheEntryOptions options, CancellationToken token) + { + ExecuteDistributedAction( + operationId, + key, + FusionCacheAction.EntrySet, + entry.Timestamp, + (dca, isBackground, ct) => + { + return dca!.SetEntry<TValue>(operationId, key, entry, options, isBackground, ct); + }, + (bpa, isBackground, ct) => + { + return bpa.PublishSet(operationId, key, entry.Timestamp, options, false, isBackground, ct); + }, + options, + token + ); } - //[MethodImpl(MethodImplOptions.AggressiveInlining)] - private bool PublishInternal(string operationId, BackplaneMessage message, FusionCacheEntryOptions options, CancellationToken token) + private void DistributedRemoveEntry(string operationId, string key, FusionCacheEntryOptions options, CancellationToken token) { - if (_bpa is null) - return false; + ExecuteDistributedAction( + operationId, + key, + FusionCacheAction.EntryRemove, + FusionCacheInternalUtils.GetCurrentTimestamp(), + (dca, isBackground, ct) => + { + return dca.RemoveEntry(operationId, key, options, isBackground, ct); + }, + (bpa, isBackground, ct) => + { + return bpa.PublishRemove(operationId, key, null, options, false, isBackground, ct); + }, + options, + token + ); + } - return _bpa.Publish(operationId, message, options, false, token); + private void DistributedExpireEntry(string operationId, string key, FusionCacheEntryOptions options, CancellationToken token) + { + ExecuteDistributedAction( + operationId, + key, + FusionCacheAction.EntryExpire, + FusionCacheInternalUtils.GetCurrentTimestamp(), + (dca, isBackground, ct) => + { + return dca.RemoveEntry(operationId, key, options, isBackground, ct); + }, + (bpa, isBackground, ct) => + { + return bpa.PublishExpire(operationId, key, null, options, false, isBackground, ct); + }, + options, + token + ); } } diff --git a/src/ZiggyCreatures.FusionCache/IFusionCacheBuilder.cs b/src/ZiggyCreatures.FusionCache/IFusionCacheBuilder.cs index 6449f238..5d1f1e8d 100644 --- a/src/ZiggyCreatures.FusionCache/IFusionCacheBuilder.cs +++ b/src/ZiggyCreatures.FusionCache/IFusionCacheBuilder.cs @@ -8,221 +8,220 @@ using ZiggyCreatures.Caching.Fusion.Plugins; using ZiggyCreatures.Caching.Fusion.Serialization; -namespace ZiggyCreatures.Caching.Fusion +namespace ZiggyCreatures.Caching.Fusion; + +/// <summary> +/// Represents an instance of a builder object to create FusionCache instances. +/// <br/><br/> +/// <strong>DOCS:</strong> <see href="https://github.com/ZiggyCreatures/FusionCache/blob/main/docs/DependencyInjection.md"/> +/// </summary> +public interface IFusionCacheBuilder { /// <summary> - /// Represents an instance of a builder object to create FusionCache instances. + /// The name of the FusionCache instance. + /// </summary> + string CacheName { get; } + + #region LOGGER + + /// <summary> + /// Indicates if the builder should try find and use an <see cref="ILogger{FusionCache}"/> service registered in the DI container. + /// </summary> + bool UseRegisteredLogger { get; set; } + + /// <summary> + /// A specific <see cref="ILogger{FusionCache}"/> instance to be used. + /// </summary> + ILogger<FusionCache>? Logger { get; set; } + + /// <summary> + /// A factory that creates the <see cref="ILogger{FusionCache}"/> instance to be used. + /// </summary> + Func<IServiceProvider, ILogger<FusionCache>>? LoggerFactory { get; set; } + + /// <summary> + /// Throws an <see cref="InvalidOperationException"/> if a logger (an instance of <see cref="ILogger{FusionCache}"/>) is not specified or is not found in the DI container. + /// </summary> + bool ThrowIfMissingLogger { get; set; } + + #endregion + + #region OPTIONS + + /// <summary> + /// Indicates if the builder should try find and use an <see cref="IOptions{FusionCacheOptions}"/> service registered in the DI container. + /// </summary> + bool UseRegisteredOptions { get; set; } + + /// <summary> + /// A custom <see cref="FusionCacheOptions"/> object to be used. + /// </summary> + FusionCacheOptions? Options { get; set; } + + /// <summary> + /// Indicates if the builder should use the specified <see cref="CacheKeyPrefix"/>, overwriting the one in the options as configured. + /// </summary> + bool UseCacheKeyPrefix { get; set; } + + /// <summary> + /// A prefix that will be added to each cache key for each call: it can be useful when working with multiple named caches. /// <br/><br/> - /// <strong>DOCS:</strong> <see href="https://github.com/ZiggyCreatures/FusionCache/blob/main/docs/DependencyInjection.md"/> + /// <strong>EXAMPLE</strong>: if the CacheKeyPrefix specified is "MyCache:", a later call to cache.GetOrDefault("Product/123") will actually work on the cache key "MyCache:Product/123". /// </summary> - public interface IFusionCacheBuilder - { - /// <summary> - /// The name of the FusionCache instance. - /// </summary> - string CacheName { get; } + string? CacheKeyPrefix { get; set; } - #region LOGGER + /// <summary> + /// A custom setup logic for the <see cref="FusionCacheOptions"/> object, to allow for fine-grained customization. + /// </summary> + Action<FusionCacheOptions>? SetupOptionsAction { get; set; } - /// <summary> - /// Indicates if the builder should try find and use an <see cref="ILogger{FusionCache}"/> service registered in the DI container. - /// </summary> - bool UseRegisteredLogger { get; set; } - - /// <summary> - /// A specific <see cref="ILogger{FusionCache}"/> instance to be used. - /// </summary> - ILogger<FusionCache>? Logger { get; set; } - - /// <summary> - /// A factory that creates the <see cref="ILogger{FusionCache}"/> instance to be used. - /// </summary> - Func<IServiceProvider, ILogger<FusionCache>>? LoggerFactory { get; set; } + #endregion - /// <summary> - /// Throws an <see cref="InvalidOperationException"/> if a logger (an instance of <see cref="ILogger{FusionCache}"/>) is not specified or is not found in the DI container. - /// </summary> - bool ThrowIfMissingLogger { get; set; } + #region DEFAULT ENTRY OPTIONS + + /// <summary> + /// A custom <see cref="FusionCacheEntryOptions"/> object to be used as the <see cref="FusionCacheOptions.DefaultEntryOptions"/>. + /// </summary> + FusionCacheEntryOptions? DefaultEntryOptions { get; set; } + + /// <summary> + /// A custom setup logic for the <see cref="FusionCacheOptions"/> object, to allow for fine-grained customization. + /// </summary> + Action<FusionCacheEntryOptions>? SetupDefaultEntryOptionsAction { get; set; } - #endregion + #endregion - #region OPTIONS + #region MEMORY CACHE - /// <summary> - /// Indicates if the builder should try find and use an <see cref="IOptions{FusionCacheOptions}"/> service registered in the DI container. - /// </summary> - bool UseRegisteredOptions { get; set; } - - /// <summary> - /// A custom <see cref="FusionCacheOptions"/> object to be used. - /// </summary> - FusionCacheOptions? Options { get; set; } - - /// <summary> - /// Indicates if the builder should use the specified <see cref="CacheKeyPrefix"/>, overwriting the one in the options as configured. - /// </summary> - bool UseCacheKeyPrefix { get; set; } - - /// <summary> - /// A prefix that will be added to each cache key for each call: it can be useful when working with multiple named caches. - /// <br/><br/> - /// <strong>EXAMPLE</strong>: if the CacheKeyPrefix specified is "MyCache:", a later call to cache.GetOrDefault("Product/123") will actually work on the cache key "MyCache:Product/123". - /// </summary> - string? CacheKeyPrefix { get; set; } + /// <summary> + /// Indicates if the builder should try find and use an <see cref="IMemoryCache"/> service registered in the DI container. + /// </summary> + bool UseRegisteredMemoryCache { get; set; } - /// <summary> - /// A custom setup logic for the <see cref="FusionCacheOptions"/> object, to allow for fine-grained customization. - /// </summary> - Action<FusionCacheOptions>? SetupOptionsAction { get; set; } + /// <summary> + /// A specific <see cref="IMemoryCache"/> instance to be used. + /// </summary> + IMemoryCache? MemoryCache { get; set; } - #endregion - - #region DEFAULT ENTRY OPTIONS - - /// <summary> - /// A custom <see cref="FusionCacheEntryOptions"/> object to be used as the <see cref="FusionCacheOptions.DefaultEntryOptions"/>. - /// </summary> - FusionCacheEntryOptions? DefaultEntryOptions { get; set; } - - /// <summary> - /// A custom setup logic for the <see cref="FusionCacheOptions"/> object, to allow for fine-grained customization. - /// </summary> - Action<FusionCacheEntryOptions>? SetupDefaultEntryOptionsAction { get; set; } - - #endregion - - #region MEMORY CACHE - - /// <summary> - /// Indicates if the builder should try find and use an <see cref="IMemoryCache"/> service registered in the DI container. - /// </summary> - bool UseRegisteredMemoryCache { get; set; } - - /// <summary> - /// A specific <see cref="IMemoryCache"/> instance to be used. - /// </summary> - IMemoryCache? MemoryCache { get; set; } - - /// <summary> - /// A factory that creates the <see cref="IMemoryCache"/> instance to be used. - /// </summary> - Func<IServiceProvider, IMemoryCache>? MemoryCacheFactory { get; set; } - - /// <summary> - /// Throws an <see cref="InvalidOperationException"/> if a memory cache (an instance of <see cref="IMemoryCache"/>) is not specified or is not found in the DI container. - /// </summary> - bool ThrowIfMissingMemoryCache { get; set; } - - #endregion - - #region SERIALIZER - - /// <summary> - /// Indicates if the builder should try find and use an <see cref="IFusionCacheSerializer"/> service registered in the DI container. - /// </summary> - bool UseRegisteredSerializer { get; set; } - - /// <summary> - /// A specific <see cref="IFusionCacheSerializer"/> instance to be used. - /// </summary> - IFusionCacheSerializer? Serializer { get; set; } - - /// <summary> - /// A factory that creates the <see cref="IFusionCacheSerializer"/> instance to be used. - /// </summary> - Func<IServiceProvider, IFusionCacheSerializer>? SerializerFactory { get; set; } - - /// <summary> - /// When a distributed cache has been specified or found in the DI container, throws an <see cref="InvalidOperationException"/> if a serializer (an instance of <see cref="IFusionCacheSerializer"/>) is not specified or is not found in the DI container, too. - /// </summary> - bool ThrowIfMissingSerializer { get; set; } - - #endregion - - #region DISTRIBUTED CACHE - - /// <summary> - /// Indicates if the builder should try find and use an <see cref="IDistributedCache"/> service registered in the DI container. - /// </summary> - bool UseRegisteredDistributedCache { get; set; } - - /// <summary> - /// When trying to find an <see cref="IDistributedCache"/> service registered in the DI container, ignore it if it is of type <see cref="MemoryDistributedCache"/>, since that is not really a distributed cache and it's automatically registered by ASP.NET MVC without control from the user. - /// </summary> - bool IgnoreRegisteredMemoryDistributedCache { get; set; } - - /// <summary> - /// A specific <see cref="IDistributedCache"/> instance to be used. - /// </summary> - IDistributedCache? DistributedCache { get; set; } - - /// <summary> - /// A factory that creates the <see cref="IDistributedCache"/> instance to be used. - /// </summary> - Func<IServiceProvider, IDistributedCache>? DistributedCacheFactory { get; set; } - - /// <summary> - /// Throws an <see cref="InvalidOperationException"/> if a distributed cache (an instance of <see cref="IDistributedCache"/>) is not specified or is not found in the DI container. - /// </summary> - bool ThrowIfMissingDistributedCache { get; set; } - - #endregion - - #region BACKPLANE - - /// <summary> - /// Indicates if the builder should try find and use an <see cref="IFusionCacheBackplane"/> service registered in the DI container. - /// </summary> - bool UseRegisteredBackplane { get; set; } + /// <summary> + /// A factory that creates the <see cref="IMemoryCache"/> instance to be used. + /// </summary> + Func<IServiceProvider, IMemoryCache>? MemoryCacheFactory { get; set; } - /// <summary> - /// A specific <see cref="IFusionCacheBackplane"/> instance to be used. - /// </summary> - IFusionCacheBackplane? Backplane { get; set; } - - /// <summary> - /// A factory that creates the <see cref="IFusionCacheBackplane"/> instance to be used. - /// </summary> - Func<IServiceProvider, IFusionCacheBackplane>? BackplaneFactory { get; set; } - - /// <summary> - /// Throws an <see cref="InvalidOperationException"/> if a backplane (an instance of <see cref="IFusionCacheBackplane"/>) is not specified or is not found in the DI container. - /// </summary> - bool ThrowIfMissingBackplane { get; set; } - - #endregion - - #region PLUGINS - - /// <summary> - /// Indicates if the builder should try find and use any available <see cref="IFusionCachePlugin"/> services registered in the DI container. - /// </summary> - bool UseAllRegisteredPlugins { get; set; } - - /// <summary> - /// A specific set of <see cref="IFusionCachePlugin"/> instances to be used. - /// </summary> - List<IFusionCachePlugin> Plugins { get; } - - /// <summary> - /// A specific set of <see cref="IFusionCachePlugin"/> factories to be used. - /// </summary> - List<Func<IServiceProvider, IFusionCachePlugin>> PluginsFactories { get; } - - #endregion - - /// <summary> - /// A custom post-setup action, that will be invoked just after the creation of the FusionCache instance, and before returning it to the caller. - /// <br/><br/> - /// <strong>NOTE:</strong> it is possible to add actions multiple times, to add multiple post-setup calls one after the other to combine them for a powerful result. - /// </summary> - Action<IServiceProvider, IFusionCache>? PostSetupAction { get; set; } - - /// <summary> - /// Creates a new FusionCache instance, and set it up based on the configured builder options. - /// </summary> - /// <param name="serviceProvider">The needed <see cref="IServiceProvider"/> instance.</param> - /// <returns>The <see cref="IFusionCache"/> instance created.</returns> - IFusionCache Build(IServiceProvider serviceProvider); - } + /// <summary> + /// Throws an <see cref="InvalidOperationException"/> if a memory cache (an instance of <see cref="IMemoryCache"/>) is not specified or is not found in the DI container. + /// </summary> + bool ThrowIfMissingMemoryCache { get; set; } + + #endregion + + #region SERIALIZER + + /// <summary> + /// Indicates if the builder should try find and use an <see cref="IFusionCacheSerializer"/> service registered in the DI container. + /// </summary> + bool UseRegisteredSerializer { get; set; } + + /// <summary> + /// A specific <see cref="IFusionCacheSerializer"/> instance to be used. + /// </summary> + IFusionCacheSerializer? Serializer { get; set; } + + /// <summary> + /// A factory that creates the <see cref="IFusionCacheSerializer"/> instance to be used. + /// </summary> + Func<IServiceProvider, IFusionCacheSerializer>? SerializerFactory { get; set; } + + /// <summary> + /// When a distributed cache has been specified or found in the DI container, throws an <see cref="InvalidOperationException"/> if a serializer (an instance of <see cref="IFusionCacheSerializer"/>) is not specified or is not found in the DI container, too. + /// </summary> + bool ThrowIfMissingSerializer { get; set; } + + #endregion + + #region DISTRIBUTED CACHE + + /// <summary> + /// Indicates if the builder should try find and use an <see cref="IDistributedCache"/> service registered in the DI container. + /// </summary> + bool UseRegisteredDistributedCache { get; set; } + + /// <summary> + /// When trying to find an <see cref="IDistributedCache"/> service registered in the DI container, ignore it if it is of type <see cref="MemoryDistributedCache"/>, since that is not really a distributed cache and it's automatically registered by ASP.NET MVC without control from the user. + /// </summary> + bool IgnoreRegisteredMemoryDistributedCache { get; set; } + + /// <summary> + /// A specific <see cref="IDistributedCache"/> instance to be used. + /// </summary> + IDistributedCache? DistributedCache { get; set; } + + /// <summary> + /// A factory that creates the <see cref="IDistributedCache"/> instance to be used. + /// </summary> + Func<IServiceProvider, IDistributedCache>? DistributedCacheFactory { get; set; } + + /// <summary> + /// Throws an <see cref="InvalidOperationException"/> if a distributed cache (an instance of <see cref="IDistributedCache"/>) is not specified or is not found in the DI container. + /// </summary> + bool ThrowIfMissingDistributedCache { get; set; } + + #endregion + + #region BACKPLANE + + /// <summary> + /// Indicates if the builder should try find and use an <see cref="IFusionCacheBackplane"/> service registered in the DI container. + /// </summary> + bool UseRegisteredBackplane { get; set; } + + /// <summary> + /// A specific <see cref="IFusionCacheBackplane"/> instance to be used. + /// </summary> + IFusionCacheBackplane? Backplane { get; set; } + + /// <summary> + /// A factory that creates the <see cref="IFusionCacheBackplane"/> instance to be used. + /// </summary> + Func<IServiceProvider, IFusionCacheBackplane>? BackplaneFactory { get; set; } + + /// <summary> + /// Throws an <see cref="InvalidOperationException"/> if a backplane (an instance of <see cref="IFusionCacheBackplane"/>) is not specified or is not found in the DI container. + /// </summary> + bool ThrowIfMissingBackplane { get; set; } + + #endregion + + #region PLUGINS + + /// <summary> + /// Indicates if the builder should try find and use any available <see cref="IFusionCachePlugin"/> services registered in the DI container. + /// </summary> + bool UseAllRegisteredPlugins { get; set; } + + /// <summary> + /// A specific set of <see cref="IFusionCachePlugin"/> instances to be used. + /// </summary> + List<IFusionCachePlugin> Plugins { get; } + + /// <summary> + /// A specific set of <see cref="IFusionCachePlugin"/> factories to be used. + /// </summary> + List<Func<IServiceProvider, IFusionCachePlugin>> PluginsFactories { get; } + + #endregion + + /// <summary> + /// A custom post-setup action, that will be invoked just after the creation of the FusionCache instance, and before returning it to the caller. + /// <br/><br/> + /// <strong>NOTE:</strong> it is possible to add actions multiple times, to add multiple post-setup calls one after the other to combine them for a powerful result. + /// </summary> + Action<IServiceProvider, IFusionCache>? PostSetupAction { get; set; } + + /// <summary> + /// Creates a new FusionCache instance, and set it up based on the configured builder options. + /// </summary> + /// <param name="serviceProvider">The needed <see cref="IServiceProvider"/> instance.</param> + /// <returns>The <see cref="IFusionCache"/> instance created.</returns> + IFusionCache Build(IServiceProvider serviceProvider); } diff --git a/src/ZiggyCreatures.FusionCache/IFusionCacheProvider.cs b/src/ZiggyCreatures.FusionCache/IFusionCacheProvider.cs index cffa10ec..ef360711 100644 --- a/src/ZiggyCreatures.FusionCache/IFusionCacheProvider.cs +++ b/src/ZiggyCreatures.FusionCache/IFusionCacheProvider.cs @@ -1,22 +1,21 @@ -namespace ZiggyCreatures.Caching.Fusion +namespace ZiggyCreatures.Caching.Fusion; + +/// <summary> +/// The provider to work with multiple named FusionCache instances, kinda like Microsoft's HTTP named clients (see https://learn.microsoft.com/en-us/aspnet/core/fundamentals/http-requests#named-clients) +/// </summary> +public interface IFusionCacheProvider { /// <summary> - /// The provider to work with multiple named FusionCache instances, kinda like Microsoft's HTTP named clients (see https://learn.microsoft.com/en-us/aspnet/core/fundamentals/http-requests#named-clients) + /// Returns the FusionCache instance with the corresponding name. /// </summary> - public interface IFusionCacheProvider - { - /// <summary> - /// Returns the FusionCache instance with the corresponding name. - /// </summary> - /// <param name="cacheName">The name of the cache: it must match the one provided during registration.</param> - /// <returns>The FusionCache instance corresponding to the cache name specified.</returns> - IFusionCache GetCache(string cacheName); + /// <param name="cacheName">The name of the cache: it must match the one provided during registration.</param> + /// <returns>The FusionCache instance corresponding to the cache name specified.</returns> + IFusionCache GetCache(string cacheName); - /// <summary> - /// Returns the FusionCache instance with the corresponding name, or <see langword="null"/> if none found. - /// </summary> - /// <param name="cacheName">The name of the cache: it must match the one provided during registration.</param> - /// <returns>The FusionCache instance corresponding to the cache name specified.</returns> - IFusionCache? GetCacheOrNull(string cacheName); - } + /// <summary> + /// Returns the FusionCache instance with the corresponding name, or <see langword="null"/> if none found. + /// </summary> + /// <param name="cacheName">The name of the cache: it must match the one provided during registration.</param> + /// <returns>The FusionCache instance corresponding to the cache name specified.</returns> + IFusionCache? GetCacheOrNull(string cacheName); } diff --git a/src/ZiggyCreatures.FusionCache/Internals/AutoRecovery/AutoRecoveryItem.cs b/src/ZiggyCreatures.FusionCache/Internals/AutoRecovery/AutoRecoveryItem.cs new file mode 100644 index 00000000..3502a59d --- /dev/null +++ b/src/ZiggyCreatures.FusionCache/Internals/AutoRecovery/AutoRecoveryItem.cs @@ -0,0 +1,44 @@ +using System; +using System.Diagnostics; + +namespace ZiggyCreatures.Caching.Fusion.Internals.AutoRecovery; + +[DebuggerDisplay("{" + nameof(Action) + "} ON {" + nameof(CacheKey) + "} AT {" + nameof(Timestamp) + "} (EXP: {" + nameof(ExpirationTicks) + "} RET: {" + nameof(RetryCount) + "})")] +internal sealed class AutoRecoveryItem +{ + public AutoRecoveryItem(string cacheKey, FusionCacheAction action, long timestamp, FusionCacheEntryOptions options, long? expirationTicks, int? maxRetryCount) + { + CacheKey = cacheKey; + Action = action; + Timestamp = timestamp; + Options = options ?? throw new ArgumentNullException(nameof(options)); + ExpirationTicks = expirationTicks; + RetryCount = maxRetryCount; + } + + public string CacheKey { get; } + public FusionCacheAction Action { get; } + public long Timestamp { get; } + public FusionCacheEntryOptions Options { get; } + public long? ExpirationTicks { get; } + public int? RetryCount { get; private set; } + + public bool IsExpired() + { + return ExpirationTicks <= DateTimeOffset.UtcNow.Ticks; + } + + public void RecordRetry() + { + if (RetryCount is not null) + RetryCount--; + } + + public bool CanRetry() + { + if (RetryCount is null) + return true; + + return RetryCount.Value > 0; + } +} diff --git a/src/ZiggyCreatures.FusionCache/Internals/AutoRecovery/AutoRecoveryService.cs b/src/ZiggyCreatures.FusionCache/Internals/AutoRecovery/AutoRecoveryService.cs new file mode 100644 index 00000000..7cde10f5 --- /dev/null +++ b/src/ZiggyCreatures.FusionCache/Internals/AutoRecovery/AutoRecoveryService.cs @@ -0,0 +1,665 @@ +using System; +using System.Collections.Concurrent; +using System.Collections.Generic; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.Extensions.Logging; +using ZiggyCreatures.Caching.Fusion.Backplane; + +namespace ZiggyCreatures.Caching.Fusion.Internals.AutoRecovery; + +internal sealed class AutoRecoveryService + : IDisposable +{ + private readonly FusionCache _cache; + private readonly FusionCacheOptions _options; + private readonly ILogger<FusionCache>? _logger; + + private readonly ConcurrentDictionary<string, AutoRecoveryItem> _queue = new ConcurrentDictionary<string, AutoRecoveryItem>(); + private readonly SemaphoreSlim _lock = new SemaphoreSlim(1, 1); + private readonly int _maxItems; + private readonly int _maxRetryCount; + private readonly TimeSpan _delay; + private static readonly TimeSpan _minDelay = TimeSpan.FromMilliseconds(10); + private CancellationTokenSource? _cts; + private long _barrierTicks = 0; + + public AutoRecoveryService(FusionCache cache, FusionCacheOptions options, ILogger<FusionCache>? logger) + { + _cache = cache; + _options = options; + _logger = logger; + + _delay = _options.AutoRecoveryDelay; + // NOTE: THIS IS PRAGMATIC, SO TO AVOID CHECKING AN int? EVERY TIME, AND int.MaxValue IS HIGH ENOUGH THAT IT WON'T MATTER + _maxItems = _options.AutoRecoveryMaxItems ?? int.MaxValue; + _maxRetryCount = _options.AutoRecoveryMaxRetryCount ?? int.MaxValue; + + // AUTO-RECOVERY + if (_options.EnableAutoRecovery) + { + if (_delay <= TimeSpan.Zero) + { + if (_logger?.IsEnabled(LogLevel.Error) ?? false) + _logger.Log(LogLevel.Error, "FUSION [N={CacheName} I={CacheInstanceId}] (O={CacheOperationId}): auto-recovery is enabled but cannot be started because the AutoRecoveryDelay has been set to zero", _cache.CacheName, _cache.InstanceId, FusionCacheInternalUtils.MaybeGenerateOperationId(_logger)); + } + else + { + _cts = new CancellationTokenSource(); + _ = BackgroundJobAsync(); + } + } + } + + internal bool TryAddItem(string? operationId, string? cacheKey, FusionCacheAction action, long timestamp, FusionCacheEntryOptions options) + { + if (_options.EnableAutoRecovery == false) + return false; + + if (_cache.RequiresDistributedOperations(options) == false) + return false; + + if (cacheKey is null) + return false; + + if (action == FusionCacheAction.Unknown) + return false; + + options = options.Duplicate(); + + // DISTRIBUTED CACHE + if (options.SkipDistributedCache == false) + { + options.AllowBackgroundDistributedCacheOperations = false; + options.DistributedCacheSoftTimeout = Timeout.InfiniteTimeSpan; + options.DistributedCacheHardTimeout = Timeout.InfiniteTimeSpan; + options.ReThrowDistributedCacheExceptions = true; + options.ReThrowSerializationExceptions = true; + options.SkipDistributedCacheReadWhenStale = false; + } + + // BACKPLANE + if (options.SkipBackplaneNotifications == false) + { + options.AllowBackgroundBackplaneOperations = false; + options.ReThrowBackplaneExceptions = true; + } + + var duration = (options.SkipDistributedCache || _cache.HasDistributedCache == false) ? options.Duration : options.DistributedCacheDuration.GetValueOrDefault(options.Duration); + var expirationTicks = FusionCacheInternalUtils.GetNormalizedAbsoluteExpiration(duration, options, false).Ticks; + + if (_queue.Count >= _maxItems && _queue.ContainsKey(cacheKey) == false) + { + // IF: + // - A LIMIT HAS BEEN SET + // - THE LIMIT HAS BEEN REACHED OR SURPASSED + // - THE ITEM TO BE ADDED IS NOT ALREADY THERE (OTHERWISE IT WILL BE AN OVERWRITE AND SIZE WILL NOT GROW) + // THEN: + // - FIND THE ITEM THAT WILL EXPIRE SOONER AND REMOVE IT + // - OR, IF NEW ITEM WILL EXPIRE SOONER, DO NOT ADD IT + if (_logger?.IsEnabled(LogLevel.Trace) ?? false) + _logger.Log(LogLevel.Trace, "FUSION [N={CacheName} I={CacheInstanceId}] (O={CacheOperationId} K={CacheKey}): the auto-recovery queue has reached the max size of {MaxSize}", _cache.CacheName, _cache.InstanceId, operationId, cacheKey, _maxItems); + + try + { + var earliestToExpire = _queue.Values.ToArray().Where(x => x.ExpirationTicks is not null).OrderBy(x => x.ExpirationTicks).FirstOrDefault(); + if (earliestToExpire is not null) + { + if (earliestToExpire.ExpirationTicks < expirationTicks) + { + if (_logger?.IsEnabled(LogLevel.Trace) ?? false) + _logger.Log(LogLevel.Trace, "FUSION [N={CacheName} I={CacheInstanceId}] (O={CacheOperationId} K={CacheKey}): an item with cache key {CacheKeyToRemove} has been removed from the auto-recovery queue to make space for the new one", _cache.CacheName, _cache.InstanceId, operationId, cacheKey, earliestToExpire.CacheKey); + + // REMOVE THE QUEUED ITEM + TryRemoveItem(operationId, earliestToExpire); + } + else + { + if (_logger?.IsEnabled(LogLevel.Trace) ?? false) + _logger.Log(LogLevel.Trace, "FUSION [N={CacheName} I={CacheInstanceId}] (O={CacheOperationId} K={CacheKey}): the item has not been added to the auto-recovery queue because it would have expired earlier than the earliest item already present in the queue (with cache key {CacheKeyEarliest})", _cache.CacheName, _cache.InstanceId, operationId, cacheKey, earliestToExpire.CacheKey); + + // IGNORE THE NEW ITEM + return false; + } + } + } + catch (Exception exc) + { + if (_logger?.IsEnabled(LogLevel.Error) ?? false) + _logger.Log(LogLevel.Error, exc, "FUSION [N={CacheName} I={CacheInstanceId}] (O={CacheOperationId} K={CacheKey}): an error occurred while deciding which item in the auto-recovery queue to remove to make space for a new one", _cache.CacheName, _cache.InstanceId, operationId, cacheKey); + } + } + + _queue[cacheKey] = new AutoRecoveryItem(cacheKey, action, timestamp, options, expirationTicks, _maxRetryCount); + + if (_logger?.IsEnabled(LogLevel.Debug) ?? false) + _logger.Log(LogLevel.Debug, "FUSION [N={CacheName} I={CacheInstanceId}] (O={CacheOperationId} K={CacheKey}): added (or overwrote) an item to the auto-recovery queue", _cache.CacheName, _cache.InstanceId, operationId, cacheKey); + + return true; + } + + internal bool TryRemoveItemByCacheKey(string? operationId, string cacheKey) + { + if (cacheKey is null) + return false; + + if (_queue.TryRemove(cacheKey, out _) == false) + return false; + + 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, cacheKey); + + return true; + } + + internal bool TryRemoveItem(string? operationId, AutoRecoveryItem item) + { + if (item is null) + return false; + + 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<TKey,TValue>) 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 (_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; + } + + internal bool TryCleanUpQueue(string operationId, IList<AutoRecoveryItem> items) + { + if (items.Count == 0) + return false; + + var atLeastOneRemoved = false; + + // NOTE: WE USE THE REVERSE ITERATION TRICK TO AVOID PROBLEMS WITH REMOVING ITEMS WHILE ITERATING + for (int i = items.Count - 1; i >= 0; i--) + { + var item = items[i]; + // IF THE ITEM IS SINCE EXPIRED -> REMOVE IT FROM THE QUEUE *AND* FROM THE LIST + if (item.IsExpired()) + { + TryRemoveItem(operationId, item); + items.RemoveAt(i); + atLeastOneRemoved = true; + + if (_logger?.IsEnabled(LogLevel.Trace) ?? false) + _logger.Log(LogLevel.Trace, "FUSION [N={CacheName} I={CacheInstanceId}] (O={CacheOperationId} K={CacheKey}): auto-cleanup of auto-recovery item", _cache.CacheName, _cache.InstanceId, operationId, item.CacheKey); + } + } + + return atLeastOneRemoved; + } + + internal bool CheckIncomingMessageForConflicts(string operationId, BackplaneMessage message) + { + if (message.CacheKey is null) + { + return true; + } + + if (_queue.TryGetValue(message.CacheKey, out var pendingLocal) == false) + { + // NO PENDING LOCAL MESSAGE WITH THE SAME KEY + return true; + } + + if (pendingLocal.Timestamp <= message.Timestamp) + { + // PENDING LOCAL MESSAGE IS -OLDER- THAN THE INCOMING ONE -> REMOVE THE LOCAL ONE + TryRemoveItem(operationId, pendingLocal); + return true; + } + + // PENDING LOCAL MESSAGE IS -NEWER- THAN THE INCOMING ONE -> DO NOT PROCESS THE INCOMING ONE + return false; + } + + internal bool TryUpdateBarrier(string operationId) + { + if (_options.EnableAutoRecovery == false) + return false; + + if (_queue.Count == 0) + return false; + + var newBarrier = DateTimeOffset.UtcNow.Ticks + _delay.Ticks; + var oldBarrier = Interlocked.Exchange(ref _barrierTicks, newBarrier); + + if (_logger?.IsEnabled(LogLevel.Trace) ?? false) + _logger.Log(LogLevel.Trace, "FUSION [N={CacheName} I={CacheInstanceId}] (O={CacheOperationId}): auto-recovery barrier set from {OldAutoRecoveryBarrier} to {NewAutoRecoveryBarrier}", _cache.CacheName, _cache.InstanceId, operationId, oldBarrier, newBarrier); + + if (_logger?.IsEnabled(LogLevel.Information) ?? false) + _logger.Log(LogLevel.Information, "FUSION [N={CacheName} I={CacheInstanceId}] (O={CacheOperationId}): waiting at least {AutoRecoveryDelay} to start auto-recovery to let the other nodes reconnect, to better handle backpressure", _cache.CacheName, _cache.InstanceId, operationId, _delay); + + return true; + } + + internal bool IsBehindBarrier() + { + var barrierTicks = Interlocked.Read(ref _barrierTicks); + + if (DateTimeOffset.UtcNow.Ticks < barrierTicks) + return true; + + return false; + } + + internal async ValueTask<bool> TryProcessQueueAsync(string operationId, CancellationToken token) + { + if (_options.EnableAutoRecovery == false) + return false; + + if (_queue.Count == 0) + return false; + + // ACQUIRE THE LOCK + if (_lock.Wait(0) == false) + { + // IF THE LOCK HAS NOT BEEN ACQUIRED IMMEDIATELY -> PROCESSING IS ALREADY ONGOING, SO WE JUST RETURN + return false; + } + + // SNAPSHOT THE ITEMS TO PROCESS + var itemsToProcess = _queue.Values.ToList(); + + // INITIAL CLEANUP + _ = TryCleanUpQueue(operationId, itemsToProcess); + + // IF NO REMAINING ITEMS -> JUST RELEASE THE LOCK AND RETURN + if (itemsToProcess.Count == 0) + { + _lock.Release(); + return false; + } + + var processedCount = 0; + var hasStopped = false; + AutoRecoveryItem? lastProcessedItem = null; + + try + { + if (_logger?.IsEnabled(LogLevel.Debug) ?? false) + _logger.Log(LogLevel.Debug, "FUSION [N={CacheName} I={CacheInstanceId}] (O={CacheOperationId}): starting auto-recovery of {Count} pending items", _cache.CacheName, _cache.InstanceId, operationId, itemsToProcess.Count); + + foreach (var item in itemsToProcess) + { + processedCount++; + + token.ThrowIfCancellationRequested(); + + if (IsBehindBarrier()) + { + hasStopped = true; + return false; + } + + lastProcessedItem = item; + + var success = false; + + 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; + } + + if (success) + { + TryRemoveItem(operationId, item); + } + else + { + hasStopped = true; + return false; + } + } + + if (_logger?.IsEnabled(LogLevel.Debug) ?? false) + _logger.Log(LogLevel.Debug, "FUSION [N={CacheName} I={CacheInstanceId}] (O={CacheOperationId}): completed auto-recovery of {Count} items", _cache.CacheName, _cache.InstanceId, operationId, processedCount); + } + catch (OperationCanceledException) + { + hasStopped = true; + + if (_logger?.IsEnabled(LogLevel.Debug) ?? false) + _logger.Log(LogLevel.Debug, "FUSION [N={CacheName} I={CacheInstanceId}] (O={CacheOperationId}): auto-recovery canceled after having processed {Count} items", _cache.CacheName, _cache.InstanceId, operationId, processedCount); + } + catch (Exception exc) + { + hasStopped = true; + + 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); + } + finally + { + if (hasStopped) + { + if (_logger?.IsEnabled(LogLevel.Error) ?? false) + _logger.Log(LogLevel.Error, "FUSION [N={CacheName} I={CacheInstanceId}] (O={CacheOperationId} K={CacheKey}): stopped auto-recovery because of an error after {Count} processed items", _cache.CacheName, _cache.InstanceId, operationId, lastProcessedItem?.CacheKey, processedCount); + + if (lastProcessedItem is not null) + { + // UPDATE RETRY COUNT + lastProcessedItem.RecordRetry(); + + if (lastProcessedItem.CanRetry() == false) + { + TryRemoveItem(operationId, lastProcessedItem); + + if (_logger?.IsEnabled(LogLevel.Debug) ?? false) + _logger.Log(LogLevel.Debug, "FUSION [N={CacheName} I={CacheInstanceId}] (O={CacheOperationId} K={CacheKey}): a auto-recovery item retried too many times, so it has been removed from the queue", _cache.CacheName, _cache.InstanceId, operationId, lastProcessedItem?.CacheKey); + } + } + } + + // RELEASE THE LOCK + _lock.Release(); + } + + return true; + } + + internal async ValueTask<bool> TryProcessItemSetAsync(string operationId, AutoRecoveryItem item, CancellationToken token) + { + // DISTRIBUTED CACHE + var dca = _cache.GetCurrentDistributedAccessor(item.Options); + if (dca is not null) + { + if (dca.IsCurrentlyUsable(operationId, item.CacheKey) == false) + { + return false; + } + + // TRY TO GET THE MEMORY CACHE + var mca = _cache.GetCurrentMemoryAccessor(item.Options); + + if (mca is not null) + { + // TRY TO GET THE MEMORY ENTRY + var memoryEntry = mca.GetEntryOrNull(operationId, item.CacheKey); + + if (memoryEntry is not null) + { + try + { + (var error, var isSame, var hasUpdated) = await _cache.TryUpdateMemoryEntryFromDistributedEntryUntypedAsync(operationId, item.CacheKey, memoryEntry).ConfigureAwait(false); + + if (error) + { + // STOP PROCESSING THE QUEUE + return false; + } + + if (hasUpdated) + { + // IF THE MEMORY ENTRY HAS BEEN UPDATED FROM THE DISTRIBUTED ENTRY, IT MEANS THAT THE DISTRIBUTED ENTRY + // IS NEWER THAN THE MEMORY ENTRY, BECAUSE IT HAS BEEN UPDATED SINCE WE SET IT LOCALLY AND NOW IT'S + // NEWER -> STOP HERE, ALL IS GOOD + return true; + } + + if (isSame == false) + { + // IF THE MEMORY ENTRY IS ALSO NOT THE SAME AS THE DISTRIBUTED ENTRY, IT MEANS THAT THE DISTRIBUTED ENTRY + // IS EITHER OLDER OR IT'S NOT THERE AT ALL -> WE SET IT TO THE CURRENT ONE + + var dcaSuccess = await dca.SetEntryUntypedAsync(operationId, item.CacheKey, memoryEntry, item.Options, true, token).ConfigureAwait(false); + if (dcaSuccess == false) + { + // STOP PROCESSING THE QUEUE + return false; + } + } + } + catch + { + return false; + } + } + } + } + + // BACKPLANE + var bpa = _cache.GetCurrentBackplaneAccessor(item.Options); + if (bpa is not null) + { + var bpaSuccess = false; + try + { + if (bpa.IsCurrentlyUsable(operationId, item.CacheKey)) + { + bpaSuccess = await bpa.PublishSetAsync(operationId, item.CacheKey, item.Timestamp, item.Options, true, true, token).ConfigureAwait(false); + } + } + catch + { + bpaSuccess = false; + } + + if (bpaSuccess == false) + { + return false; + } + } + + return true; + } + + internal async ValueTask<bool> TryProcessItemRemoveAsync(string operationId, AutoRecoveryItem item, CancellationToken token) + { + // DISTRIBUTED CACHE + var dca = _cache.GetCurrentDistributedAccessor(item.Options); + if (dca is not null) + { + var dcaSuccess = false; + try + { + if (dca.IsCurrentlyUsable(operationId, item.CacheKey)) + { + dcaSuccess = await dca.RemoveEntryAsync(operationId, item.CacheKey, item.Options, true, token).ConfigureAwait(false); + } + } + catch + { + dcaSuccess = false; + } + + if (dcaSuccess == false) + { + return false; + } + } + + // BACKPLANE + var bpa = _cache.GetCurrentBackplaneAccessor(item.Options); + if (bpa is not null) + { + var bpaSuccess = false; + try + { + if (bpa.IsCurrentlyUsable(operationId, item.CacheKey)) + { + bpaSuccess = await bpa.PublishRemoveAsync(operationId, item.CacheKey, item.Timestamp, item.Options, true, true, token).ConfigureAwait(false); + } + } + catch + { + bpaSuccess = false; + } + + if (bpaSuccess == false) + { + return false; + } + } + + return true; + } + + internal async ValueTask<bool> TryProcessItemExpireAsync(string operationId, AutoRecoveryItem item, CancellationToken token) + { + // DISTRIBUTED CACHE + var dca = _cache.GetCurrentDistributedAccessor(item.Options); + if (dca is not null) + { + var dcaSuccess = false; + try + { + if (dca.IsCurrentlyUsable(operationId, item.CacheKey)) + { + dcaSuccess = await dca.RemoveEntryAsync(operationId, item.CacheKey, item.Options, true, token).ConfigureAwait(false); + } + } + catch + { + dcaSuccess = false; + } + + if (dcaSuccess == false) + { + return false; + } + } + + // BACKPLANE + var bpa = _cache.GetCurrentBackplaneAccessor(item.Options); + if (bpa is not null) + { + var bpaSuccess = false; + try + { + if (bpa.IsCurrentlyUsable(operationId, item.CacheKey)) + { + bpaSuccess = await bpa.PublishExpireAsync(operationId, item.CacheKey, item.Timestamp, item.Options, true, true, token).ConfigureAwait(false); + } + } + catch + { + bpaSuccess = false; + } + + if (bpaSuccess == false) + { + return false; + } + } + + return true; + } + + internal async Task BackgroundJobAsync() + { + if (_cts is null) + return; + + try + { + var ct = _cts.Token; + while (!ct.IsCancellationRequested) + { + var operationId = FusionCacheInternalUtils.MaybeGenerateOperationId(_logger); + var delay = _delay; + var nowTicks = DateTimeOffset.UtcNow.Ticks; + var barrierTicks = Interlocked.Read(ref _barrierTicks); + if (nowTicks < barrierTicks) + { + // SET THE NEW DELAY TO REACH THE BARRIER (+ A MICROSCOPIC EXTRA) + var oldDelay = delay; + var newDelayTicks = barrierTicks - nowTicks + 1_000; + delay = TimeSpan.FromTicks(newDelayTicks); + + // CHECK IF THE NEW DELAY IS BELOW A SAFETY LIMIT + if (delay < _minDelay) + { + delay = _minDelay; + newDelayTicks = delay.Ticks; + } + + if (_logger?.IsEnabled(LogLevel.Trace) ?? false) + _logger.Log(LogLevel.Trace, "FUSION [N={CacheName} I={CacheInstanceId}] (O={CacheOperationId}): instead of the standard auto-recovery delay of {AutoRecoveryNormalDelay} the new delay is {AutoRecoveryNewDelay} ({AutoRecoveryNewDelayMs} ms, {AutoRecoveryNewDelayTicks} ticks)", _cache.CacheName, _cache.InstanceId, operationId, oldDelay, delay, delay.TotalMilliseconds, newDelayTicks); + } + + if (_queue.Count > 0) + { + if (_logger?.IsEnabled(LogLevel.Trace) ?? false) + _logger.Log(LogLevel.Trace, "FUSION [N={CacheName} I={CacheInstanceId}] (O={CacheOperationId}): waiting {AutoRecoveryCurrentDelay} before the next try of auto-recovery", _cache.CacheName, _cache.InstanceId, operationId, delay); + } + + await Task.Delay(delay, ct).ConfigureAwait(false); + + // AFTER THE DELAY, READ THE BARRIER AGAIN, IN CASE IT HAS BEEN MODIFIED + // WHILE WAITING: IF UPDATED -> SKIP TO THE NEXT LOOP CYCLE + if (IsBehindBarrier()) + { + if (_logger?.IsEnabled(LogLevel.Trace) ?? false) + _logger.Log(LogLevel.Trace, "FUSION [N={CacheName} I={CacheInstanceId}] (O={CacheOperationId}): a barrier has been set after having awaited to start processing the auto-recovery queue: skipping to the next loop cycle", _cache.CacheName, _cache.InstanceId, operationId); + + continue; + } + + ct.ThrowIfCancellationRequested(); + + if (_queue.Count > 0) + { + _ = await TryProcessQueueAsync(operationId, ct).ConfigureAwait(false); + } + } + } + catch (OperationCanceledException) + { + // EMPTY + } + } + + // IDISPOSABLE + private bool disposedValue; + private void Dispose(bool disposing) + { + if (!disposedValue) + { + if (disposing) + { + _queue.Clear(); + _cts?.Cancel(); + _cts = null; + } + + disposedValue = true; + } + } + + public void Dispose() + { + Dispose(disposing: true); + GC.SuppressFinalize(this); + } +} diff --git a/src/ZiggyCreatures.FusionCache/Internals/Backplane/BackplaneAccessor.cs b/src/ZiggyCreatures.FusionCache/Internals/Backplane/BackplaneAccessor.cs index 06a26c78..07315895 100644 --- a/src/ZiggyCreatures.FusionCache/Internals/Backplane/BackplaneAccessor.cs +++ b/src/ZiggyCreatures.FusionCache/Internals/Backplane/BackplaneAccessor.cs @@ -1,7 +1,4 @@ using System; -using System.Collections.Concurrent; -using System.Linq; -using System.Threading; using System.Threading.Tasks; using Microsoft.Extensions.Logging; using ZiggyCreatures.Caching.Fusion.Backplane; @@ -18,12 +15,7 @@ internal sealed partial class BackplaneAccessor private readonly FusionCacheBackplaneEventsHub _events; private readonly SimpleCircuitBreaker _breaker; - // AUTO-RECOVERY - private readonly SemaphoreSlim _autoRecoveryProcessingLock = new SemaphoreSlim(1, 1); - private readonly ConcurrentDictionary<string, BackplaneAutoRecoveryItem> _autoRecoveryQueue = new ConcurrentDictionary<string, BackplaneAutoRecoveryItem>(); - private readonly FusionCacheEntryOptions _autoRecoveryEntryOptions; - - public BackplaneAccessor(FusionCache cache, IFusionCacheBackplane backplane, FusionCacheOptions options, ILogger? logger, FusionCacheBackplaneEventsHub events) + public BackplaneAccessor(FusionCache cache, IFusionCacheBackplane backplane, FusionCacheOptions options, ILogger? logger) { if (cache is null) throw new ArgumentNullException(nameof(cache)); @@ -37,10 +29,7 @@ public BackplaneAccessor(FusionCache cache, IFusionCacheBackplane backplane, Fus _options = options; _logger = logger; - _events = events; - - // AUTO-RECOVERY - _autoRecoveryEntryOptions = new FusionCacheEntryOptions().SetSkipMemoryCache().SetSkipBackplaneNotifications(true); + _events = _cache.Events.Backplane; // CIRCUIT-BREAKER _breaker = new SimpleCircuitBreaker(options.BackplaneCircuitBreakerDuration); @@ -57,7 +46,7 @@ private void UpdateLastError(string key, string operationId) if (res && hasChanged) { if (_logger?.IsEnabled(LogLevel.Warning) ?? false) - _logger.Log(LogLevel.Warning, "FUSION [N={CacheName} I={CacheInstanceId}] (O={CacheOperationId} K={CacheKey}): backplane temporarily de-activated for {BreakDuration}", _cache.CacheName, _cache.InstanceId, operationId, key, _breaker.BreakDuration); + _logger.Log(LogLevel.Warning, "FUSION [N={CacheName} I={CacheInstanceId}] (O={CacheOperationId} K={CacheKey}): [BP] backplane temporarily de-activated for {BreakDuration}", _cache.CacheName, _cache.InstanceId, operationId, key, _breaker.BreakDuration); // EVENT _events.OnCircuitBreakerChange(operationId, key, false); @@ -71,7 +60,7 @@ public bool IsCurrentlyUsable(string? operationId, string? key) if (res && hasChanged) { if (_logger?.IsEnabled(LogLevel.Warning) ?? false) - _logger.Log(LogLevel.Warning, "FUSION [N={CacheName} I={CacheInstanceId}] (O={CacheOperationId} K={CacheKey}): backplane activated again", _cache.CacheName, _cache.InstanceId, operationId, key); + _logger.Log(LogLevel.Warning, "FUSION [N={CacheName} I={CacheInstanceId}] (O={CacheOperationId} K={CacheKey}): [BP] backplane activated again", _cache.CacheName, _cache.InstanceId, operationId, key); // EVENT _events.OnCircuitBreakerChange(operationId, key, true); @@ -85,7 +74,7 @@ private void ProcessError(string operationId, string key, Exception exc, string if (exc is SyntheticTimeoutException) { if (_logger?.IsEnabled(_options.BackplaneSyntheticTimeoutsLogLevel) ?? false) - _logger.Log(_options.BackplaneSyntheticTimeoutsLogLevel, exc, "FUSION [N={CacheName} I={CacheInstanceId}] (O={CacheOperationId} K={CacheKey}): a synthetic timeout occurred while " + actionDescription, _cache.CacheName, _cache.InstanceId, operationId, key); + _logger.Log(_options.BackplaneSyntheticTimeoutsLogLevel, exc, "FUSION [N={CacheName} I={CacheInstanceId}] (O={CacheOperationId} K={CacheKey}): [BP] a synthetic timeout occurred while " + actionDescription, _cache.CacheName, _cache.InstanceId, operationId, key); return; } @@ -93,178 +82,62 @@ private void ProcessError(string operationId, string key, Exception exc, string UpdateLastError(key, operationId); if (_logger?.IsEnabled(_options.BackplaneErrorsLogLevel) ?? false) - _logger.Log(_options.BackplaneErrorsLogLevel, exc, "FUSION [N={CacheName} I={CacheInstanceId}] (O={CacheOperationId} K={CacheKey}): an error occurred while " + actionDescription, _cache.CacheName, _cache.InstanceId, operationId, key); + _logger.Log(_options.BackplaneErrorsLogLevel, exc, "FUSION [N={CacheName} I={CacheInstanceId}] (O={CacheOperationId} K={CacheKey}): [BP] an error occurred while " + actionDescription, _cache.CacheName, _cache.InstanceId, operationId, key); } - private bool TryAddAutoRecoveryItem(string? operationId, BackplaneMessage message, FusionCacheEntryOptions options) + private bool CheckMessage(string operationId, BackplaneMessage message, bool isAutoRecovery) { - if (message.CacheKey is null) - return false; - - var expirationTicks = FusionCacheInternalUtils.GetNormalizedAbsoluteExpiration(options.DistributedCacheDuration.GetValueOrDefault(options.Duration), options, false).Ticks; - - if (_options.BackplaneAutoRecoveryMaxItems.HasValue && _autoRecoveryQueue.Count >= _options.BackplaneAutoRecoveryMaxItems.Value && _autoRecoveryQueue.ContainsKey(message.CacheKey) == false) - { - // IF: - // - A LIMIT HAS BEEN SET - // - THE LIMIT HAS BEEN REACHED OR SURPASSED - // - THE ITEM TO BE ADDED IS NOT ALREADY THERE (OTHERWISE IT WILL BE AN OVERWRITE AND SIZE WILL NOT GROW) - // THEN: - // - FIND THE ITEM THAT WILL EXPIRE SOONER AND REMOVE IT - // - OR, IF NEW ITEM WILL EXPIRE SOONER, DO NOT ADD IT - try - { - var earlierToExpire = _autoRecoveryQueue.Values.OrderBy(x => x.ExpirationTicks).FirstOrDefault(); - if (earlierToExpire.Message is not null) - { - if (earlierToExpire.ExpirationTicks < expirationTicks) - { - if (_logger?.IsEnabled(LogLevel.Trace) ?? false) - _logger.Log(LogLevel.Trace, "FUSION [N={CacheName} I={CacheInstanceId}] (O={CacheOperationId} K={CacheKey}): an item with cache key {CacheKeyToRemove} has been removed from the backplane auto-recovery queue to make space for the new one", _cache.CacheName, _cache.InstanceId, operationId, message?.CacheKey, earlierToExpire.Message.CacheKey); - - // REMOVE THE QUEUED ITEM - _autoRecoveryQueue.TryRemove(earlierToExpire.Message.CacheKey!, out _); - } - else - { - // IGNORE THE NEW ITEM - return false; - } - } - } - catch (Exception exc) - { - if (_logger?.IsEnabled(LogLevel.Error) ?? false) - _logger.Log(LogLevel.Error, exc, "FUSION [N={CacheName} I={CacheInstanceId}] (O={CacheOperationId} K={CacheKey}): an error occurred while deciding which item in the backplane auto-recovery queue to remove to make space for a new one", _cache.CacheName, _cache.InstanceId, operationId, message?.CacheKey); - } - } - + // CHECK: IGNORE NULL if (message is null) - return false; - - _autoRecoveryQueue[message.CacheKey] = new BackplaneAutoRecoveryItem(message, options, expirationTicks); - - if (_logger?.IsEnabled(LogLevel.Debug) ?? false) - _logger.Log(LogLevel.Debug, "FUSION [N={CacheName} I={CacheInstanceId}] (O={CacheOperationId} K={CacheKey}): added (or overwrote) an item to the backplane auto-recovery queue", _cache.CacheName, _cache.InstanceId, operationId, message.CacheKey); - - return true; - } - - private bool TryRemoveAutoRecoveryItemByCacheKey(string? operationId, string? cacheKey) - { - if (cacheKey is null) - return false; - - if (_autoRecoveryQueue.ContainsKey(cacheKey) == false) - return false; - - if (_autoRecoveryQueue.TryRemove(cacheKey, out _)) { - if (_logger?.IsEnabled(LogLevel.Debug) ?? false) - _logger.Log(LogLevel.Debug, "FUSION [N={CacheName} I={CacheInstanceId}] (O={CacheOperationId} K={CacheKey}): removed an item from the backplane auto-recovery queue because a new one is about to be sent", _cache.CacheName, _cache.InstanceId, operationId, cacheKey); - - return true; - } - - return false; - } + if (_logger?.IsEnabled(_options.BackplaneErrorsLogLevel) ?? false) + _logger.Log(_options.BackplaneErrorsLogLevel, "FUSION [N={CacheName} I={CacheInstanceId}] (O={CacheOperationId}): [BP] cannot send a null backplane message (what!?)", _cache.CacheName, _cache.InstanceId, operationId); - private bool CheckIncomingMessageForAutoRecoveryConflicts(BackplaneMessage message) - { - if (message.CacheKey is null) - { - return true; + return false; } - if (_autoRecoveryQueue.TryGetValue(message.CacheKey, out var pendingLocal) == false) + // CHECK: IS VALID + if (message.IsValid() == false) { - // NO PENDING LOCAL MESSAGE WITH THE SAME KEY - return true; - } + // IGNORE INVALID MESSAGES + if (_logger?.IsEnabled(LogLevel.Warning) ?? false) + _logger.Log(LogLevel.Warning, "FUSION [N={CacheName} I={CacheInstanceId}] (O={CacheOperationId} K={CacheKey}): [BP] cannot send an invalid backplane message" + isAutoRecovery.ToString(" (auto-recovery)"), _cache.CacheName, _cache.InstanceId, operationId, message.CacheKey); - if (pendingLocal.Message.InstantTicks <= message.InstantTicks) - { - // PENDING LOCAL MESSAGE IS -OLDER- THAN THE INCOMING ONE -> REMOVE THE LOCAL ONE - _autoRecoveryQueue.TryRemove(message.CacheKey, out _); - return true; + return false; } - // PENDING LOCAL MESSAGE IS -NEWER- THAN THE INCOMING ONE -> DO NOT PROCESS THE INCOMING ONE - return false; - } - - private bool TryProcessAutoRecoveryQueue(string operationId) - { - if (IsCurrentlyUsable(null, null) == false) - return false; + //// CHECK: EMPTY SOURCE ID + //if (string.IsNullOrEmpty(message.SourceId)) + //{ + // //// AUTO-ASSIGN LOCAL SOURCE ID + // //message.SourceId = _cache.InstanceId; - if (_options.EnableBackplaneAutoRecovery == false) - return false; + // if (_logger?.IsEnabled(LogLevel.Warning) ?? false) + // _logger.Log(LogLevel.Warning, "FUSION [N={CacheName} I={CacheInstanceId}] (O={CacheOperationId} K={CacheKey}): cannot send a backplane message" + isAutoRecovery.ToString(" (auto-recovery)") + " with a null/empty SourceId", _cache.CacheName, _cache.InstanceId, operationId, message.CacheKey); - var _count = _autoRecoveryQueue.Count; - if (_count == 0) - return false; + // return false; + //} - // ACQUIRE THE LOCK - if (_autoRecoveryProcessingLock.Wait(0) == false) + // CHECK: WRONG SOURCE ID + if (message.SourceId != _cache.InstanceId) { - // IF THE LOCK HAS NOT BEEN ACQUIRED IMMEDIATELY, SOMEONE ELSE IS ALREADY PROCESSING THE QUEUE, SO WE JUST RETURN + // IGNORE MESSAGES -NOT- FROM THIS SOURCE + if (_logger?.IsEnabled(LogLevel.Warning) ?? false) + _logger.Log(LogLevel.Warning, "FUSION [N={CacheName} I={CacheInstanceId}] (O={CacheOperationId} K={CacheKey}): [BP] cannot send a backplane message" + isAutoRecovery.ToString(" (auto-recovery)") + " with a SourceId different than the local one (IFusionCache.InstanceId)", _cache.CacheName, _cache.InstanceId, operationId, message.CacheKey); + return false; } - FusionCacheExecutionUtils.RunSyncActionAdvanced( - (ct) => - { - try - { - // NOTE: THE COUNT VALUE HERE IS JUST AN APPROXIMATION: PER THE MULTI-THREADED NATURE OF THIS THING IT'S - // OK IF THE NUMBER IS SINCE CHANGED AND IN THE FOREACH LOOP WE WILL ITERATE OVER MORE (OR LESS) ITEMS - if (_logger?.IsEnabled(LogLevel.Debug) ?? false) - _logger.Log(LogLevel.Debug, "FUSION [N={CacheName} I={CacheInstanceId}] (O={CacheOperationId}): starting backplane auto-recovery of about {Count} pending notifications", _cache.CacheName, _cache.InstanceId, operationId, _count); - - _count = 0; - foreach (var item in _autoRecoveryQueue) - { - if (Publish(operationId, item.Value.Message, item.Value.Options, true)) - { - // IF A PUBLISH GO THROUGH -> REMOVE FROM THE QUEUE - _autoRecoveryQueue.TryRemove(item.Key, out _); - - _count++; - } - else - { - // IF A PUBLISH DOESN'T GO THROUGH -> STOP PROCESSING THE QUEUE - if (_logger?.IsEnabled(LogLevel.Debug) ?? false) - _logger.Log(LogLevel.Debug, "FUSION [N={CacheName} I={CacheInstanceId}] (O={CacheOperationId} K={CacheKey}): stopped backplane auto-recovery because of an error after {Count} processed items", _cache.CacheName, _cache.InstanceId, operationId, item.Value.Message.CacheKey, _count); - - return; - } - } - - if (_logger?.IsEnabled(LogLevel.Debug) ?? false) - _logger.Log(LogLevel.Debug, "FUSION [N={CacheName} I={CacheInstanceId}] (O={CacheOperationId}): completed backplane auto-recovery of {Count} items", _cache.CacheName, _cache.InstanceId, operationId, _count); - } - finally - { - // RELEASE THE LOCK - _autoRecoveryProcessingLock.Release(); - } - }, - Timeout.InfiniteTimeSpan, - false, - _options.DefaultEntryOptions.AllowBackgroundBackplaneOperations == false - ); - return true; } public void Subscribe() { - var operationId = FusionCacheInternalUtils.MaybeGenerateOperationId(_logger); - try { + if (_logger?.IsEnabled(LogLevel.Trace) ?? false) + _logger.Log(LogLevel.Trace, "FUSION [N={CacheName} I={CacheInstanceId}]: [BP] before unsubscribing to backplane", _cache.CacheName, _cache.InstanceId); + _backplane.Subscribe( new BackplaneSubscriptionOptions( _options.GetBackplaneChannelName(), @@ -272,32 +145,41 @@ public void Subscribe() HandleIncomingMessage ) ); + + if (_logger?.IsEnabled(LogLevel.Trace) ?? false) + _logger.Log(LogLevel.Trace, "FUSION [N={CacheName} I={CacheInstanceId}]: [BP] after unsubscribing to backplane", _cache.CacheName, _cache.InstanceId); } catch (Exception exc) { + var operationId = FusionCacheInternalUtils.MaybeGenerateOperationId(_logger); + ProcessError(operationId, "", exc, $"subscribing to a backplane of type {_backplane.GetType().FullName}"); - if (_logger?.IsEnabled(_options.BackplaneErrorsLogLevel) ?? false) - _logger.Log(_options.BackplaneErrorsLogLevel, exc, "FUSION [N={CacheName} I={CacheInstanceId}] (O={CacheOperationId} K={CacheKey}): an error occurred while subscribing to a backplane of type {BackplaneType}", _cache.CacheName, _cache.InstanceId, operationId, "", _backplane.GetType().FullName); + //if (_logger?.IsEnabled(_options.BackplaneErrorsLogLevel) ?? false) + // _logger.Log(_options.BackplaneErrorsLogLevel, exc, "FUSION [N={CacheName} I={CacheInstanceId}] (O={CacheOperationId} K={CacheKey}): an error occurred while subscribing to a backplane of type {BackplaneType}", _cache.CacheName, _cache.InstanceId, operationId, "", _backplane.GetType().FullName); } } public void Unsubscribe() { - var operationId = FusionCacheInternalUtils.MaybeGenerateOperationId(_logger); - - _autoRecoveryQueue.Clear(); - try { + if (_logger?.IsEnabled(LogLevel.Trace) ?? false) + _logger.Log(LogLevel.Trace, "FUSION [N={CacheName} I={CacheInstanceId}]: [BP] before unsubscribing to backplane", _cache.CacheName, _cache.InstanceId); + _backplane.Unsubscribe(); + + if (_logger?.IsEnabled(LogLevel.Trace) ?? false) + _logger.Log(LogLevel.Trace, "FUSION [N={CacheName} I={CacheInstanceId}]: [BP] after unsubscribing to backplane", _cache.CacheName, _cache.InstanceId); } catch (Exception exc) { + var operationId = FusionCacheInternalUtils.MaybeGenerateOperationId(_logger); + ProcessError(operationId, "", exc, $"unsubscribing from a backplane of type {_backplane.GetType().FullName}"); - if (_logger?.IsEnabled(_options.BackplaneErrorsLogLevel) ?? false) - _logger.Log(_options.BackplaneErrorsLogLevel, exc, "FUSION [N={CacheName} I={CacheInstanceId}] (O={CacheOperationId} K={CacheKey}): an error occurred while unsubscribing from a backplane of type {BackplaneType}", _cache.CacheName, _cache.InstanceId, operationId, "", _backplane.GetType().FullName); + //if (_logger?.IsEnabled(_options.BackplaneErrorsLogLevel) ?? false) + // _logger.Log(_options.BackplaneErrorsLogLevel, exc, "FUSION [N={CacheName} I={CacheInstanceId}] (O={CacheOperationId} K={CacheKey}): an error occurred while unsubscribing from a backplane of type {BackplaneType}", _cache.CacheName, _cache.InstanceId, operationId, "", _backplane.GetType().FullName); } } @@ -306,111 +188,188 @@ private void HandleConnect(BackplaneConnectionInfo info) var operationId = FusionCacheInternalUtils.MaybeGenerateOperationId(_logger); if (_logger?.IsEnabled(LogLevel.Information) ?? false) - _logger.Log(LogLevel.Information, "FUSION [N={CacheName} I={CacheInstanceId}] (O={CacheOperationId}): backplane " + (info.IsReconnection ? "re-connected" : "connected"), _cache.CacheName, _cache.InstanceId, operationId); + _logger.Log(LogLevel.Information, "FUSION [N={CacheName} I={CacheInstanceId}] (O={CacheOperationId}): [BP] backplane " + (info.IsReconnection ? "re-connected" : "connected"), _cache.CacheName, _cache.InstanceId, operationId); - if (info.IsReconnection && _options.EnableBackplaneAutoRecovery) + if (info.IsReconnection) { - Task.Run(async () => - { - if (_options.BackplaneAutoRecoveryReconnectDelay > TimeSpan.Zero) - { - if (_logger?.IsEnabled(LogLevel.Information) ?? false) - _logger.Log(LogLevel.Information, "FUSION [N={CacheName} I={CacheInstanceId}] (O={CacheOperationId}): waiting {AutoRecoveryDelay} to let the other nodes reconnect, to better handle backpressure", _cache.CacheName, _cache.InstanceId, operationId, _options.BackplaneAutoRecoveryReconnectDelay); - - await Task.Delay(_options.BackplaneAutoRecoveryReconnectDelay).ConfigureAwait(false); - } - - _breaker.Close(out var hasChanged); - - return TryProcessAutoRecoveryQueue(operationId); - }); + _cache.AutoRecovery.TryUpdateBarrier(operationId); } } private void HandleIncomingMessage(BackplaneMessage message) { - _breaker.Close(out var hasChanged); - - var operationId = FusionCacheInternalUtils.MaybeGenerateOperationId(_logger); - - if (hasChanged) + _ = Task.Run(async () => { - if (_logger?.IsEnabled(LogLevel.Warning) ?? false) - _logger.Log(LogLevel.Warning, "FUSION [N={CacheName} I={CacheInstanceId}] (O={CacheOperationId}): backplane activated again", _cache.CacheName, _cache.InstanceId, operationId); + await HandleIncomingMessageAsync(message).ConfigureAwait(false); + }); + } - // EVENT - _events.OnCircuitBreakerChange(null, null, true); - } + private async ValueTask HandleIncomingMessageAsync(BackplaneMessage message) + { + var operationId = FusionCacheInternalUtils.MaybeGenerateOperationId(_logger); // IGNORE NULL if (message is null) { if (_logger?.IsEnabled(_options.BackplaneErrorsLogLevel) ?? false) - _logger.Log(_options.BackplaneErrorsLogLevel, "FUSION [N={CacheName} I={CacheInstanceId}] (O={CacheOperationId}): a null backplane notification has been received (what!?)", _cache.CacheName, _cache.InstanceId, operationId); + _logger.Log(_options.BackplaneErrorsLogLevel, "FUSION [N={CacheName} I={CacheInstanceId}] (O={CacheOperationId}): [BP] a null backplane notification has been received (what!?)", _cache.CacheName, _cache.InstanceId, operationId); return; } - // IGNORE INVALID MESSAGES - if (message.IsValid() == false) + // IGNORE MESSAGES FROM THIS SOURCE + if (message.SourceId == _cache.InstanceId) { - if (_logger?.IsEnabled(_options.BackplaneErrorsLogLevel) ?? false) - _logger.Log(_options.BackplaneErrorsLogLevel, "FUSION [N={CacheName} I={CacheInstanceId}] (O={CacheOperationId} K={CacheKey}): an invalid backplane notification has been received from remote cache {RemoteCacheInstanceId} (A={Action}, T={InstanceTicks})", _cache.CacheName, _cache.InstanceId, operationId, message.CacheKey, message.SourceId, message.Action, message.InstantTicks); - - TryProcessAutoRecoveryQueue(operationId); return; } - // IGNORE MESSAGES FROM THIS SOURCE - if (message.SourceId == _cache.InstanceId) + // CHECK CIRCUIT BREAKER + _breaker.Close(out var hasChanged); + if (hasChanged) + { + if (_logger?.IsEnabled(LogLevel.Warning) ?? false) + _logger.Log(LogLevel.Warning, "FUSION [N={CacheName} I={CacheInstanceId}] (O={CacheOperationId}): [BP] backplane activated again", _cache.CacheName, _cache.InstanceId, operationId); + + // EVENT + _events.OnCircuitBreakerChange(operationId, message.CacheKey, true); + } + + // EVENT + _events.OnMessageReceived(operationId, message); + + // IGNORE INVALID MESSAGES + if (message.IsValid() == false) { - //TryProcessAutoRecoveryQueue(operationId); + if (_logger?.IsEnabled(_options.BackplaneErrorsLogLevel) ?? false) + _logger.Log(_options.BackplaneErrorsLogLevel, "FUSION [N={CacheName} I={CacheInstanceId}] (O={CacheOperationId} K={CacheKey}): [BP] an invalid backplane notification has been received from remote cache {RemoteCacheInstanceId} (A={Action}, T={InstantTimestamp})", _cache.CacheName, _cache.InstanceId, operationId, message.CacheKey, message.SourceId, message.Action, message.Timestamp); + return; } // AUTO-RECOVERY - if (_options.EnableBackplaneAutoRecovery) + if (_options.EnableAutoRecovery) { - if (CheckIncomingMessageForAutoRecoveryConflicts(message) == false) + if (_cache.AutoRecovery.CheckIncomingMessageForConflicts(operationId, message) == false) { if (_logger?.IsEnabled(LogLevel.Debug) ?? false) - _logger.Log(LogLevel.Debug, "FUSION [N={CacheName} I={CacheInstanceId}] (O={CacheOperationId} K={CacheKey}): a backplane notification has been received from remote cache {RemoteCacheInstanceId}, but has been discarded since there is a pending one in the auto-recovery queue which is more recent", _cache.CacheName, _cache.InstanceId, operationId, message.CacheKey, message.SourceId); + _logger.Log(LogLevel.Debug, "FUSION [N={CacheName} I={CacheInstanceId}] (O={CacheOperationId} K={CacheKey}): [BP] a backplane notification has been received from remote cache {RemoteCacheInstanceId}, but has been ignored since there is a pending one in the auto-recovery queue which is more recent", _cache.CacheName, _cache.InstanceId, operationId, message.CacheKey, message.SourceId); - TryProcessAutoRecoveryQueue(operationId); return; } - - TryProcessAutoRecoveryQueue(operationId); } // PROCESS MESSAGE switch (message.Action) { case BackplaneMessageAction.EntrySet: - if (_logger?.IsEnabled(LogLevel.Debug) ?? false) - _logger.Log(LogLevel.Debug, "FUSION [N={CacheName} I={CacheInstanceId}] (O={CacheOperationId} K={CacheKey}): a backplane notification has been received from remote cache {RemoteCacheInstanceId} (SET)", _cache.CacheName, _cache.InstanceId, operationId, message.CacheKey, message.SourceId); + 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} (SET)", _cache.CacheName, _cache.InstanceId, operationId, message.CacheKey, message.SourceId); - _cache.ExpireMemoryEntryInternal(operationId, message.CacheKey!, true); + // HANDLE SET + await HandleIncomingMessageSetAsync(operationId, message).ConfigureAwait(false); break; case BackplaneMessageAction.EntryRemove: - if (_logger?.IsEnabled(LogLevel.Debug) ?? false) - _logger.Log(LogLevel.Debug, "FUSION [N={CacheName} I={CacheInstanceId}] (O={CacheOperationId} K={CacheKey}): a backplane notification has been received from remote cache {RemoteCacheInstanceId} (REMOVE)", _cache.CacheName, _cache.InstanceId, operationId, message.CacheKey, message.SourceId); + 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); - _cache.ExpireMemoryEntryInternal(operationId, message.CacheKey!, false); + // // HANDLE REMOVE: CALLING MaybeExpireMemoryEntryInternal() WITH allowFailSafe SET TO FALSE -> LOCAL REMOVE + _cache.MaybeExpireMemoryEntryInternal(operationId, message.CacheKey!, false, null); break; case BackplaneMessageAction.EntryExpire: - if (_logger?.IsEnabled(LogLevel.Debug) ?? false) - _logger.Log(LogLevel.Debug, "FUSION [N={CacheName} I={CacheInstanceId}] (O={CacheOperationId} K={CacheKey}): a backplane notification has been received from remote cache {RemoteCacheInstanceId} (EXPIRE)", _cache.CacheName, _cache.InstanceId, operationId, message.CacheKey, message.SourceId); + 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} (EXPIRE)", _cache.CacheName, _cache.InstanceId, operationId, message.CacheKey, message.SourceId); - _cache.ExpireMemoryEntryInternal(operationId, message.CacheKey!, true); + // HANDLE EXPIRE: CALLING MaybeExpireMemoryEntryInternal() WITH allowFailSafe SET TO TRUE -> LOCAL EXPIRE + _cache.MaybeExpireMemoryEntryInternal(operationId, message.CacheKey!, true, message.Timestamp); break; default: + // HANDLE UNKNOWN: DO NOTHING if (_logger?.IsEnabled(_options.BackplaneErrorsLogLevel) ?? false) - _logger.Log(_options.BackplaneErrorsLogLevel, "FUSION [N={CacheName} I={CacheInstanceId}] (O={CacheOperationId} K={CacheKey}): an backplane notification has been received from remote cache {RemoteCacheInstanceId} for an unknown action {Action}", _cache.CacheName, _cache.InstanceId, operationId, message.CacheKey, message.SourceId, message.Action); + _logger.Log(_options.BackplaneErrorsLogLevel, "FUSION [N={CacheName} I={CacheInstanceId}] (O={CacheOperationId} K={CacheKey}): [BP] an backplane notification has been received from remote cache {RemoteCacheInstanceId} for an unknown action {Action}", _cache.CacheName, _cache.InstanceId, operationId, message.CacheKey, message.SourceId, message.Action); break; } + } - // EVENT - _events.OnMessageReceived(operationId, message); + private async ValueTask HandleIncomingMessageSetAsync(string operationId, BackplaneMessage message) + { + var cacheKey = message.CacheKey!; + + var mca = _cache.GetCurrentMemoryAccessor(); + + if (mca is null) + { + if (_logger?.IsEnabled(LogLevel.Trace) ?? false) + _logger.Log(LogLevel.Trace, "FUSION [N={CacheName} I={CacheInstanceId}] (O={CacheOperationId} K={CacheKey}): [BP] no memory cache, ignoring incoming backplane message", _cache.CacheName, _cache.InstanceId, operationId, cacheKey); + return; + } + + var memoryEntry = mca.GetEntryOrNull(operationId, cacheKey); + + // IF NO MEMORY ENTRY -> DO NOTHING + if (memoryEntry is null) + { + if (_logger?.IsEnabled(LogLevel.Trace) ?? false) + _logger.Log(LogLevel.Trace, "FUSION [N={CacheName} I={CacheInstanceId}] (O={CacheOperationId} K={CacheKey}): [BP] no memory entry, ignoring incoming backplane message", _cache.CacheName, _cache.InstanceId, operationId, cacheKey); + return; + } + + //// IF NO VALUE -> EXPIRE LOCALLY + //if (memoryEntry.Value is null) + //{ + // _cache.MaybeExpireMemoryEntryInternal(operationId, cacheKey, true, message.Timestamp); + // return; + //} + + // IF MEMORY ENTRY SAME AS REMOTE ENTRY (VIA MESSAGE TIMESTAMP) -> DO NOTHING + if (memoryEntry.Timestamp == message.Timestamp) + { + if (_logger?.IsEnabled(LogLevel.Trace) ?? false) + _logger.Log(LogLevel.Trace, "FUSION [N={CacheName} I={CacheInstanceId}] (O={CacheOperationId} K={CacheKey}): [BP] memory entry same as the incoming backplane message, ignoring incoming backplane message", _cache.CacheName, _cache.InstanceId, operationId, cacheKey); + return; + } + + // IF MEMORY ENTRY MORE FRESH THAN REMOTE ENTRY (VIA MESSAGE TIMESTAMP) -> DO NOTHING + if (memoryEntry.Timestamp > message.Timestamp) + { + if (_logger?.IsEnabled(LogLevel.Trace) ?? false) + _logger.Log(LogLevel.Trace, "FUSION [N={CacheName} I={CacheInstanceId}] (O={CacheOperationId} K={CacheKey}): [BP] memory entry more fresh than the incoming backplane message, ignoring incoming backplane message", _cache.CacheName, _cache.InstanceId, operationId, cacheKey); + return; + } + + if (_cache.HasDistributedCache) + { + var dca = _cache.GetCurrentDistributedAccessor(null); + if (dca.CanBeUsed(operationId, cacheKey) == false) + { + if (_logger?.IsEnabled(LogLevel.Trace) ?? false) + _logger.Log(LogLevel.Trace, "FUSION [N={CacheName} I={CacheInstanceId}] (O={CacheOperationId} K={CacheKey}): [BP] distributed cache not currently usable, expiring local memory entry", _cache.CacheName, _cache.InstanceId, operationId, cacheKey); + + _cache.MaybeExpireMemoryEntryInternal(operationId, cacheKey, true, message.Timestamp); + + return; + } + + (var error, var isSame, var hasUpdated) = await _cache.TryUpdateMemoryEntryFromDistributedEntryUntypedAsync(operationId, cacheKey, memoryEntry).ConfigureAwait(false); + + if (error == false) + { + if (isSame) + { + if (_logger?.IsEnabled(LogLevel.Trace) ?? false) + _logger.Log(LogLevel.Trace, "FUSION [N={CacheName} I={CacheInstanceId}] (O={CacheOperationId} K={CacheKey}): [BP] memory entry is the same as the distributed entry, ignoring incoming backplane message", _cache.CacheName, _cache.InstanceId, operationId, cacheKey); + return; + } + + if (hasUpdated) + { + if (_logger?.IsEnabled(LogLevel.Trace) ?? false) + _logger.Log(LogLevel.Trace, "FUSION [N={CacheName} I={CacheInstanceId}] (O={CacheOperationId} K={CacheKey}): [BP] memory entry updated from the distributed entry, ignoring incoming backplane message", _cache.CacheName, _cache.InstanceId, operationId, cacheKey); + return; + } + } + } + + //_cache.MaybeExpireMemoryEntryInternal(operationId, cacheKey, true, null); + _cache.MaybeExpireMemoryEntryInternal(operationId, cacheKey, true, message.Timestamp); } } diff --git a/src/ZiggyCreatures.FusionCache/Internals/Backplane/BackplaneAccessor_Async.cs b/src/ZiggyCreatures.FusionCache/Internals/Backplane/BackplaneAccessor_Async.cs index c3e988b0..cfe19c13 100644 --- a/src/ZiggyCreatures.FusionCache/Internals/Backplane/BackplaneAccessor_Async.cs +++ b/src/ZiggyCreatures.FusionCache/Internals/Backplane/BackplaneAccessor_Async.cs @@ -8,117 +8,83 @@ namespace ZiggyCreatures.Caching.Fusion.Internals.Backplane; internal partial class BackplaneAccessor { - private async ValueTask ExecuteOperationAsync(string operationId, string key, Func<CancellationToken, Task> action, string actionDescription, FusionCacheEntryOptions options, CancellationToken token) + private async ValueTask<bool> PublishAsync(string operationId, BackplaneMessage message, FusionCacheEntryOptions options, bool isAutoRecovery, bool isBackground, CancellationToken token) { - if (IsCurrentlyUsable(operationId, key) == false) - return; + if (CheckMessage(operationId, message, isAutoRecovery) == false) + return false; - token.ThrowIfCancellationRequested(); + var cacheKey = message.CacheKey!; - var actionDescriptionInner = actionDescription + (options.AllowBackgroundBackplaneOperations ? " (background)" : null); - - await FusionCacheExecutionUtils - .RunAsyncActionAdvancedAsync( - action, - Timeout.InfiniteTimeSpan, - false, - options.AllowBackgroundBackplaneOperations == false, - exc => ProcessError(operationId, key, exc, actionDescriptionInner), - false, - token - ).ConfigureAwait(false) - ; - } - - public async ValueTask<bool> PublishAsync(string operationId, BackplaneMessage message, FusionCacheEntryOptions options, bool isFromAutoRecovery, CancellationToken token = default) - { - if (IsCurrentlyUsable(operationId, message.CacheKey) == false) + // CHECK: CURRENTLY NOT USABLE + if (IsCurrentlyUsable(operationId, cacheKey) == false) + { return false; + } + + token.ThrowIfCancellationRequested(); - if (string.IsNullOrEmpty(message.SourceId)) + if (isAutoRecovery == false) { - // AUTO-ASSIGN LOCAL SOURCE ID - message.SourceId = _cache.InstanceId; + _cache.AutoRecovery.TryRemoveItemByCacheKey(operationId, cacheKey); } - else if (message.SourceId != _cache.InstanceId) - { - // IGNORE MESSAGES -NOT- FROM THIS SOURCE - if (_logger?.IsEnabled(LogLevel.Warning) ?? false) - _logger.Log(LogLevel.Warning, "FUSION [N={CacheName} I={CacheInstanceId}] (O={CacheOperationId} K={CacheKey}): cannot send a backplane message" + (isFromAutoRecovery ? " (auto-recovery)" : String.Empty) + " with a SourceId different than the local one (IFusionCache.InstanceId)", _cache.CacheName, _cache.InstanceId, operationId, message.CacheKey); - return false; - } + var actionDescription = "sending a backplane notification" + isAutoRecovery.ToString(" (auto-recovery)") + isBackground.ToString(" (background)"); - if (message.IsValid() == false) + try { - // IGNORE INVALID MESSAGES - if (_logger?.IsEnabled(LogLevel.Warning) ?? false) - _logger.Log(LogLevel.Warning, "FUSION [N={CacheName} I={CacheInstanceId}] (O={CacheOperationId} K={CacheKey}): cannot send an invalid backplane message" + (isFromAutoRecovery ? " (auto-recovery)" : String.Empty), _cache.CacheName, _cache.InstanceId, operationId, message.CacheKey); + if (_logger?.IsEnabled(LogLevel.Trace) ?? false) + _logger.Log(LogLevel.Trace, "FUSION [N={CacheName} I={CacheInstanceId}] (O={CacheOperationId} K={CacheKey}): [BP] before " + actionDescription, _options.CacheName, _options.InstanceId, operationId, cacheKey); - return false; - } + await _backplane.PublishAsync(message, options, token).ConfigureAwait(false); - token.ThrowIfCancellationRequested(); + // EVENT + _events.OnMessagePublished(operationId, message); - if (isFromAutoRecovery == false) - { - TryRemoveAutoRecoveryItemByCacheKey(operationId, message.CacheKey); + if (_logger?.IsEnabled(LogLevel.Trace) ?? false) + _logger.Log(LogLevel.Trace, "FUSION [N={CacheName} I={CacheInstanceId}] (O={CacheOperationId} K={CacheKey}): [BP] after " + actionDescription, _options.CacheName, _options.InstanceId, operationId, cacheKey); } + catch (Exception exc) + { + ProcessError(operationId, cacheKey, exc, actionDescription); - await ExecuteOperationAsync( - operationId, - message.CacheKey!, - async ct => + if (exc is not SyntheticTimeoutException && options.ReThrowBackplaneExceptions) { - try + if (_options.ReThrowOriginalExceptions) { - // IF: - // - THE MESSAGE IS FROM AUTO-RECOVERY - // - AND EnableDistributedExpireOnBackplaneAutoRecovery IS ENABLED - // - AND THERE IS A DISTRIBUTED CACHE - // THEN: - // - REMOVE THE ENTRY (BUT ONLY FROM THE DISTRIBUTED CACHE) - if (isFromAutoRecovery && _options.EnableDistributedExpireOnBackplaneAutoRecovery && _cache.HasDistributedCache) - { - //await _cache.ExpireAsync(message.CacheKey!, _autoRecoveryEntryOptions, ct).ConfigureAwait(false); - var dca = _cache.GetCurrentDistributedAccessor(_autoRecoveryEntryOptions); - if (dca.CanBeUsed(operationId, message.CacheKey)) - { - await dca!.RemoveEntryAsync(operationId, message.CacheKey!, _autoRecoveryEntryOptions, ct).ConfigureAwait(false); - } - } - - await _backplane.PublishAsync(message, options, ct).ConfigureAwait(false); - - if (_logger?.IsEnabled(LogLevel.Debug) ?? false) - _logger.Log(LogLevel.Debug, "FUSION [N={CacheName} I={CacheInstanceId}] (O={CacheOperationId} K={CacheKey}): a notification has been sent" + (options.AllowBackgroundBackplaneOperations ? " in the background" : "") + (isFromAutoRecovery ? " (auto-recovery)" : "") + " ({Action})", _cache.CacheName, _cache.InstanceId, operationId, message.CacheKey, message.Action); - - if (isFromAutoRecovery == false && _options.EnableBackplaneAutoRecovery) - { - TryProcessAutoRecoveryQueue(operationId); - } + throw; } - catch (Exception exc) + else { - if (_logger?.IsEnabled(_options.BackplaneErrorsLogLevel) ?? false) - _logger.Log(_options.BackplaneErrorsLogLevel, exc, "FUSION [N={CacheName} I={CacheInstanceId}] (O={CacheOperationId} K={CacheKey}): an error occurred while sending a notification" + (options.AllowBackgroundBackplaneOperations ? " in the background" : "") + (isFromAutoRecovery ? " (auto-recovery)" : "") + " ({Action})", _cache.CacheName, _cache.InstanceId, operationId, message.CacheKey, message.Action); - - if (isFromAutoRecovery == false && _options.EnableBackplaneAutoRecovery) - { - TryAddAutoRecoveryItem(operationId, message, options); - } - - throw; + throw new FusionCacheBackplaneException("An error occurred while working with the backplane", exc); } + } - // EVENT - _events.OnMessagePublished(operationId, message); - }, - "sending a backplane notification" + (isFromAutoRecovery ? " (auto-recovery)" : ""), - options, - token - ).ConfigureAwait(false); + return false; + } return true; } + + public async ValueTask<bool> PublishSetAsync(string operationId, string key, long? timestamp, FusionCacheEntryOptions options, bool isAutoRecovery, bool isBackground, CancellationToken token) + { + var message = BackplaneMessage.CreateForEntrySet(_cache.InstanceId, key, timestamp); + + return await PublishAsync(operationId, message, options, isAutoRecovery, isBackground, token).ConfigureAwait(false); + } + + public async ValueTask<bool> PublishRemoveAsync(string operationId, string key, long? timestamp, FusionCacheEntryOptions options, bool isAutoRecovery, bool isBackground, CancellationToken token) + { + var message = BackplaneMessage.CreateForEntryRemove(_cache.InstanceId, key, timestamp); + + return await PublishAsync(operationId, message, options, isAutoRecovery, isBackground, token).ConfigureAwait(false); + } + + public async ValueTask<bool> PublishExpireAsync(string operationId, string key, long? timestamp, FusionCacheEntryOptions options, bool isAutoRecovery, bool isBackground, CancellationToken token) + { + var message = options.IsFailSafeEnabled + ? BackplaneMessage.CreateForEntryExpire(_cache.InstanceId, key, timestamp) + : BackplaneMessage.CreateForEntryRemove(_cache.InstanceId, key, timestamp); + + return await PublishAsync(operationId, message, options, isAutoRecovery, isBackground, token).ConfigureAwait(false); + } } diff --git a/src/ZiggyCreatures.FusionCache/Internals/Backplane/BackplaneAccessor_Sync.cs b/src/ZiggyCreatures.FusionCache/Internals/Backplane/BackplaneAccessor_Sync.cs index 42518071..c31761e6 100644 --- a/src/ZiggyCreatures.FusionCache/Internals/Backplane/BackplaneAccessor_Sync.cs +++ b/src/ZiggyCreatures.FusionCache/Internals/Backplane/BackplaneAccessor_Sync.cs @@ -7,117 +7,83 @@ namespace ZiggyCreatures.Caching.Fusion.Internals.Backplane; internal partial class BackplaneAccessor { - private void ExecuteOperation(string operationId, string key, Action<CancellationToken> action, string actionDescription, FusionCacheEntryOptions options, CancellationToken token) + private bool Publish(string operationId, BackplaneMessage message, FusionCacheEntryOptions options, bool isAutoRecovery, bool isBackground, CancellationToken token) { - if (IsCurrentlyUsable(operationId, key) == false) - return; + if (CheckMessage(operationId, message, isAutoRecovery) == false) + return false; - token.ThrowIfCancellationRequested(); + var cacheKey = message.CacheKey!; - var actionDescriptionInner = actionDescription + (options.AllowBackgroundBackplaneOperations ? " (background)" : null); - - FusionCacheExecutionUtils - .RunSyncActionAdvanced( - action, - Timeout.InfiniteTimeSpan, - false, - options.AllowBackgroundBackplaneOperations == false, - exc => ProcessError(operationId, key, exc, actionDescriptionInner), - false, - token - ) - ; - } - - public bool Publish(string operationId, BackplaneMessage message, FusionCacheEntryOptions options, bool isFromAutoRecovery, CancellationToken token = default) - { - if (IsCurrentlyUsable(operationId, message.CacheKey) == false) + // CHECK: CURRENTLY NOT USABLE + if (IsCurrentlyUsable(operationId, cacheKey) == false) + { return false; + } + + token.ThrowIfCancellationRequested(); - if (string.IsNullOrEmpty(message.SourceId)) + if (isAutoRecovery == false) { - // AUTO-ASSIGN LOCAL SOURCE ID - message.SourceId = _cache.InstanceId; + _cache.AutoRecovery.TryRemoveItemByCacheKey(operationId, cacheKey); } - else if (message.SourceId != _cache.InstanceId) - { - // IGNORE MESSAGES -NOT- FROM THIS SOURCE - if (_logger?.IsEnabled(LogLevel.Warning) ?? false) - _logger.Log(LogLevel.Warning, "FUSION [N={CacheName} I={CacheInstanceId}] (O={CacheOperationId} K={CacheKey}): cannot send a backplane message" + (isFromAutoRecovery ? " (auto-recovery)" : String.Empty) + " with a SourceId different than the local one (IFusionCache.InstanceId)", _cache.CacheName, _cache.InstanceId, operationId, message.CacheKey); - return false; - } + var actionDescription = "sending a backplane notification" + isAutoRecovery.ToString(" (auto-recovery)") + isBackground.ToString(" (background)"); - if (message.IsValid() == false) + try { - // IGNORE INVALID MESSAGES - if (_logger?.IsEnabled(LogLevel.Warning) ?? false) - _logger.Log(LogLevel.Warning, "FUSION [N={CacheName} I={CacheInstanceId}] (O={CacheOperationId} K={CacheKey}): cannot send an invalid backplane message" + (isFromAutoRecovery ? " (auto-recovery)" : String.Empty), _cache.CacheName, _cache.InstanceId, operationId, message.CacheKey); + if (_logger?.IsEnabled(LogLevel.Trace) ?? false) + _logger.Log(LogLevel.Trace, "FUSION [N={CacheName} I={CacheInstanceId}] (O={CacheOperationId} K={CacheKey}): [BP] before " + actionDescription, _options.CacheName, _options.InstanceId, operationId, cacheKey); - return false; - } + _backplane.Publish(message, options, token); - token.ThrowIfCancellationRequested(); + // EVENT + _events.OnMessagePublished(operationId, message); - if (isFromAutoRecovery == false) - { - TryRemoveAutoRecoveryItemByCacheKey(operationId, message.CacheKey); + if (_logger?.IsEnabled(LogLevel.Trace) ?? false) + _logger.Log(LogLevel.Trace, "FUSION [N={CacheName} I={CacheInstanceId}] (O={CacheOperationId} K={CacheKey}): [BP] after " + actionDescription, _options.CacheName, _options.InstanceId, operationId, cacheKey); } + catch (Exception exc) + { + ProcessError(operationId, cacheKey, exc, actionDescription); - ExecuteOperation( - operationId, - message.CacheKey!, - ct => + if (exc is not SyntheticTimeoutException && options.ReThrowBackplaneExceptions) { - try + if (_options.ReThrowOriginalExceptions) { - // IF: - // - THE MESSAGE IS FROM AUTO-RECOVERY - // - AND EnableDistributedExpireOnBackplaneAutoRecovery IS ENABLED - // - AND THERE IS A DISTRIBUTED CACHE - // THEN: - // - REMOVE THE ENTRY (BUT ONLY FROM THE DISTRIBUTED CACHE) - if (isFromAutoRecovery && _options.EnableDistributedExpireOnBackplaneAutoRecovery && _cache.HasDistributedCache) - { - //_cache.Expire(message.CacheKey!, _autoRecoveryEntryOptions, ct); - var dca = _cache.GetCurrentDistributedAccessor(_autoRecoveryEntryOptions); - if (dca.CanBeUsed(operationId, message.CacheKey)) - { - dca!.RemoveEntry(operationId, message.CacheKey!, _autoRecoveryEntryOptions, ct); - } - } - - _backplane.Publish(message, options, ct); - - if (_logger?.IsEnabled(LogLevel.Debug) ?? false) - _logger.Log(LogLevel.Debug, "FUSION [N={CacheName} I={CacheInstanceId}] (O={CacheOperationId} K={CacheKey}): a notification has been sent" + (options.AllowBackgroundBackplaneOperations ? " in the background" : "") + (isFromAutoRecovery ? " (auto-recovery)" : "") + " ({Action})", _cache.CacheName, _cache.InstanceId, operationId, message.CacheKey, message.Action); - - if (isFromAutoRecovery == false && _options.EnableBackplaneAutoRecovery) - { - TryProcessAutoRecoveryQueue(operationId); - } + throw; } - catch (Exception exc) + else { - if (_logger?.IsEnabled(_options.BackplaneErrorsLogLevel) ?? false) - _logger.Log(_options.BackplaneErrorsLogLevel, exc, "FUSION [N={CacheName} I={CacheInstanceId}] (O={CacheOperationId} K={CacheKey}): an error occurred while sending a notification" + (options.AllowBackgroundBackplaneOperations ? " in the background" : "") + (isFromAutoRecovery ? " (auto-recovery)" : "") + " ({Action})", _cache.CacheName, _cache.InstanceId, operationId, message.CacheKey, message.Action); - - if (isFromAutoRecovery == false && _options.EnableBackplaneAutoRecovery) - { - TryAddAutoRecoveryItem(operationId, message, options); - } - - throw; + throw new FusionCacheBackplaneException("An error occurred while working with the backplane", exc); } + } - // EVENT - _events.OnMessagePublished(operationId, message); - }, - "sending a backplane notification" + (isFromAutoRecovery ? " (auto-recovery)" : ""), - options, - token - ); + return false; + } return true; } + + public bool PublishSet(string operationId, string key, long? timestamp, FusionCacheEntryOptions options, bool isAutoRecovery, bool isBackground, CancellationToken token) + { + var message = BackplaneMessage.CreateForEntrySet(_cache.InstanceId, key, timestamp); + + return Publish(operationId, message, options, isAutoRecovery, isBackground, token); + } + + public bool PublishRemove(string operationId, string key, long? timestamp, FusionCacheEntryOptions options, bool isAutoRecovery, bool isBackground, CancellationToken token) + { + var message = BackplaneMessage.CreateForEntryRemove(_cache.InstanceId, key, timestamp); + + return Publish(operationId, message, options, isAutoRecovery, isBackground, token); + } + + public bool PublishExpire(string operationId, string key, long? timestamp, FusionCacheEntryOptions options, bool isAutoRecovery, bool isBackground, CancellationToken token) + { + var message = options.IsFailSafeEnabled + ? BackplaneMessage.CreateForEntryExpire(_cache.InstanceId, key, timestamp) + : BackplaneMessage.CreateForEntryRemove(_cache.InstanceId, key, timestamp); + + return Publish(operationId, message, options, isAutoRecovery, isBackground, token); + } } diff --git a/src/ZiggyCreatures.FusionCache/Internals/Backplane/BackplaneAutoRecoveryItem.cs b/src/ZiggyCreatures.FusionCache/Internals/Backplane/BackplaneAutoRecoveryItem.cs deleted file mode 100644 index 8a33b52b..00000000 --- a/src/ZiggyCreatures.FusionCache/Internals/Backplane/BackplaneAutoRecoveryItem.cs +++ /dev/null @@ -1,19 +0,0 @@ -using System; -using ZiggyCreatures.Caching.Fusion.Backplane; - -namespace ZiggyCreatures.Caching.Fusion.Internals.Backplane -{ - internal sealed class BackplaneAutoRecoveryItem - { - public BackplaneAutoRecoveryItem(BackplaneMessage message, FusionCacheEntryOptions options, long expirationTicks) - { - Message = message ?? throw new ArgumentNullException(nameof(message)); - Options = options ?? throw new ArgumentNullException(nameof(options)); - ExpirationTicks = expirationTicks; - } - - public BackplaneMessage Message { get; } - public FusionCacheEntryOptions Options { get; } - public long ExpirationTicks { get; } - } -} diff --git a/src/ZiggyCreatures.FusionCache/Internals/Builder/FusionCacheBuilder.cs b/src/ZiggyCreatures.FusionCache/Internals/Builder/FusionCacheBuilder.cs index fa2da2a6..78ef325d 100644 --- a/src/ZiggyCreatures.FusionCache/Internals/Builder/FusionCacheBuilder.cs +++ b/src/ZiggyCreatures.FusionCache/Internals/Builder/FusionCacheBuilder.cs @@ -11,302 +11,301 @@ using ZiggyCreatures.Caching.Fusion.Reactors; using ZiggyCreatures.Caching.Fusion.Serialization; -namespace ZiggyCreatures.Caching.Fusion.Internals.Builder +namespace ZiggyCreatures.Caching.Fusion.Internals.Builder; + +internal sealed class FusionCacheBuilder + : IFusionCacheBuilder { - internal class FusionCacheBuilder - : IFusionCacheBuilder + public FusionCacheBuilder(string cacheName) { - public FusionCacheBuilder(string cacheName) - { - CacheName = cacheName; + CacheName = cacheName; - UseRegisteredLogger = true; + UseRegisteredLogger = true; - UseRegisteredOptions = true; + UseRegisteredOptions = true; - UseRegisteredReactor = true; + UseRegisteredReactor = true; - UseRegisteredSerializer = true; + UseRegisteredSerializer = true; - IgnoreRegisteredMemoryDistributedCache = true; + IgnoreRegisteredMemoryDistributedCache = true; - Plugins = new List<IFusionCachePlugin>(); - PluginsFactories = new List<Func<IServiceProvider, IFusionCachePlugin>>(); - } + Plugins = new List<IFusionCachePlugin>(); + PluginsFactories = new List<Func<IServiceProvider, IFusionCachePlugin>>(); + } - public string CacheName { get; } + public string CacheName { get; } - public bool UseRegisteredLogger { get; set; } - public ILogger<FusionCache>? Logger { get; set; } - public Func<IServiceProvider, ILogger<FusionCache>>? LoggerFactory { get; set; } - public bool ThrowIfMissingLogger { get; set; } + public bool UseRegisteredLogger { get; set; } + public ILogger<FusionCache>? Logger { get; set; } + public Func<IServiceProvider, ILogger<FusionCache>>? LoggerFactory { get; set; } + public bool ThrowIfMissingLogger { get; set; } - public bool UseRegisteredMemoryCache { get; set; } - public IMemoryCache? MemoryCache { get; set; } - public Func<IServiceProvider, IMemoryCache>? MemoryCacheFactory { get; set; } - public bool ThrowIfMissingMemoryCache { get; set; } + public bool UseRegisteredMemoryCache { get; set; } + public IMemoryCache? MemoryCache { get; set; } + public Func<IServiceProvider, IMemoryCache>? MemoryCacheFactory { get; set; } + public bool ThrowIfMissingMemoryCache { get; set; } - private bool UseRegisteredReactor { get; set; } + private bool UseRegisteredReactor { get; set; } - public bool UseRegisteredOptions { get; set; } - public FusionCacheOptions? Options { get; set; } - public bool UseCacheKeyPrefix { get; set; } - public string? CacheKeyPrefix { get; set; } - public Action<FusionCacheOptions>? SetupOptionsAction { get; set; } + public bool UseRegisteredOptions { get; set; } + public FusionCacheOptions? Options { get; set; } + public bool UseCacheKeyPrefix { get; set; } + public string? CacheKeyPrefix { get; set; } + public Action<FusionCacheOptions>? SetupOptionsAction { get; set; } - public FusionCacheEntryOptions? DefaultEntryOptions { get; set; } - public Action<FusionCacheEntryOptions>? SetupDefaultEntryOptionsAction { get; set; } + public FusionCacheEntryOptions? DefaultEntryOptions { get; set; } + public Action<FusionCacheEntryOptions>? SetupDefaultEntryOptionsAction { get; set; } - public bool UseRegisteredSerializer { get; set; } - public IFusionCacheSerializer? Serializer { get; set; } - public Func<IServiceProvider, IFusionCacheSerializer>? SerializerFactory { get; set; } - public bool ThrowIfMissingSerializer { get; set; } + public bool UseRegisteredSerializer { get; set; } + public IFusionCacheSerializer? Serializer { get; set; } + public Func<IServiceProvider, IFusionCacheSerializer>? SerializerFactory { get; set; } + public bool ThrowIfMissingSerializer { get; set; } - public bool UseRegisteredDistributedCache { get; set; } - public bool IgnoreRegisteredMemoryDistributedCache { get; set; } - public IDistributedCache? DistributedCache { get; set; } - public Func<IServiceProvider, IDistributedCache>? DistributedCacheFactory { get; set; } - public bool ThrowIfMissingDistributedCache { get; set; } + public bool UseRegisteredDistributedCache { get; set; } + public bool IgnoreRegisteredMemoryDistributedCache { get; set; } + public IDistributedCache? DistributedCache { get; set; } + public Func<IServiceProvider, IDistributedCache>? DistributedCacheFactory { get; set; } + public bool ThrowIfMissingDistributedCache { get; set; } - public bool UseRegisteredBackplane { get; set; } - public IFusionCacheBackplane? Backplane { get; set; } - public Func<IServiceProvider, IFusionCacheBackplane>? BackplaneFactory { get; set; } - public bool ThrowIfMissingBackplane { get; set; } + public bool UseRegisteredBackplane { get; set; } + public IFusionCacheBackplane? Backplane { get; set; } + public Func<IServiceProvider, IFusionCacheBackplane>? BackplaneFactory { get; set; } + public bool ThrowIfMissingBackplane { get; set; } - public bool UseAllRegisteredPlugins { get; set; } - public List<IFusionCachePlugin> Plugins { get; } - public List<Func<IServiceProvider, IFusionCachePlugin>> PluginsFactories { get; } + public bool UseAllRegisteredPlugins { get; set; } + public List<IFusionCachePlugin> Plugins { get; } + public List<Func<IServiceProvider, IFusionCachePlugin>> PluginsFactories { get; } - public Action<IServiceProvider, IFusionCache>? PostSetupAction { get; set; } + public Action<IServiceProvider, IFusionCache>? PostSetupAction { get; set; } - public IFusionCache Build(IServiceProvider serviceProvider) + public IFusionCache Build(IServiceProvider serviceProvider) + { + if (serviceProvider is null) + throw new ArgumentNullException(nameof(serviceProvider)); + + // OPTIONS + FusionCacheOptions? options = null; + + if (UseRegisteredOptions) { - if (serviceProvider is null) - throw new ArgumentNullException(nameof(serviceProvider)); + options = serviceProvider.GetRequiredService<IOptionsMonitor<FusionCacheOptions>>().Get(CacheName); + } - // OPTIONS - FusionCacheOptions? options = null; + if (options is null) + { + options = Options; + } - if (UseRegisteredOptions) - { - options = serviceProvider.GetRequiredService<IOptionsMonitor<FusionCacheOptions>>().Get(CacheName); - } + if (options is null) + { + options = new FusionCacheOptions(); + } - if (options is null) - { - options = Options; - } + // ENSURE CACHE NAME + options.CacheName = CacheName; - if (options is null) - { - options = new FusionCacheOptions(); - } + // CACHE KEY PREFIX + if (UseCacheKeyPrefix) + { + options.CacheKeyPrefix = CacheKeyPrefix; + } - // ENSURE CACHE NAME - options.CacheName = CacheName; + if (SetupOptionsAction is not null) + { + SetupOptionsAction?.Invoke(options); + } - // CACHE KEY PREFIX - if (UseCacheKeyPrefix) - { - options.CacheKeyPrefix = CacheKeyPrefix; - } + // DEFAULT ENTRY OPTIONS + if (DefaultEntryOptions is not null) + { + options.DefaultEntryOptions = DefaultEntryOptions; + } - if (SetupOptionsAction is not null) - { - SetupOptionsAction?.Invoke(options); - } + if (SetupDefaultEntryOptionsAction is not null) + { + SetupDefaultEntryOptionsAction?.Invoke(options.DefaultEntryOptions); + } - // DEFAULT ENTRY OPTIONS - if (DefaultEntryOptions is not null) - { - options.DefaultEntryOptions = DefaultEntryOptions; - } + // LOGGER + ILogger<FusionCache>? logger; - if (SetupDefaultEntryOptionsAction is not null) - { - SetupDefaultEntryOptionsAction?.Invoke(options.DefaultEntryOptions); - } + if (UseRegisteredLogger) + { + logger = serviceProvider.GetService<ILogger<FusionCache>>(); + } + else if (LoggerFactory is not null) + { + logger = LoggerFactory?.Invoke(serviceProvider); + } + else + { + logger = Logger; + } - // LOGGER - ILogger<FusionCache>? logger; + if (logger is null && ThrowIfMissingLogger) + { + throw new InvalidOperationException("A logger has not been specified, or found in the DI container."); + } - if (UseRegisteredLogger) - { - logger = serviceProvider.GetService<ILogger<FusionCache>>(); - } - else if (LoggerFactory is not null) - { - logger = LoggerFactory?.Invoke(serviceProvider); - } - else - { - logger = Logger; - } + // MEMORY CACHE + IMemoryCache? memoryCache; - if (logger is null && ThrowIfMissingLogger) - { - throw new InvalidOperationException("A logger has not been specified, or found in the DI container."); - } + if (UseRegisteredMemoryCache) + { + memoryCache = serviceProvider.GetService<IMemoryCache>(); + } + else if (MemoryCacheFactory is not null) + { + memoryCache = MemoryCacheFactory?.Invoke(serviceProvider); + } + else + { + memoryCache = MemoryCache; + } - // MEMORY CACHE - IMemoryCache? memoryCache; + if (memoryCache is null && ThrowIfMissingMemoryCache) + { + throw new InvalidOperationException("A memory cache has not been specified, or found in the DI container."); + } - if (UseRegisteredMemoryCache) - { - memoryCache = serviceProvider.GetService<IMemoryCache>(); - } - else if (MemoryCacheFactory is not null) - { - memoryCache = MemoryCacheFactory?.Invoke(serviceProvider); - } - else - { - memoryCache = MemoryCache; - } + // REACTOR + IFusionCacheReactor? reactor = null; - if (memoryCache is null && ThrowIfMissingMemoryCache) - { - throw new InvalidOperationException("A memory cache has not been specified, or found in the DI container."); - } + if (UseRegisteredReactor) + { + reactor = serviceProvider.GetService<IFusionCacheReactor>(); + } - // REACTOR - IFusionCacheReactor? reactor = null; + // CREATE THE CACHE + var cache = new FusionCache(options, memoryCache, logger, reactor); - if (UseRegisteredReactor) + // DISTRIBUTED CACHE + IDistributedCache? distributedCache; + if (UseRegisteredDistributedCache) + { + distributedCache = serviceProvider.GetService<IDistributedCache>(); + if (IgnoreRegisteredMemoryDistributedCache && distributedCache is MemoryDistributedCache) { - reactor = serviceProvider.GetService<IFusionCacheReactor>(); + distributedCache = null; } + } + else if (DistributedCacheFactory is not null) + { + distributedCache = DistributedCacheFactory?.Invoke(serviceProvider); + } + else + { + distributedCache = DistributedCache; + } - // CREATE THE CACHE - var cache = new FusionCache(options, memoryCache, logger, reactor); + if (distributedCache is null && ThrowIfMissingDistributedCache) + { + throw new InvalidOperationException("A distributed cache has not been specified, or found in the DI container."); + } - // DISTRIBUTED CACHE - IDistributedCache? distributedCache; - if (UseRegisteredDistributedCache) + if (distributedCache is not null) + { + IFusionCacheSerializer? serializer; + if (UseRegisteredSerializer) { - distributedCache = serviceProvider.GetService<IDistributedCache>(); - if (IgnoreRegisteredMemoryDistributedCache && distributedCache is MemoryDistributedCache) - { - distributedCache = null; - } + serializer = serviceProvider.GetService<IFusionCacheSerializer>(); } - else if (DistributedCacheFactory is not null) + else if (SerializerFactory is not null) { - distributedCache = DistributedCacheFactory?.Invoke(serviceProvider); + serializer = SerializerFactory?.Invoke(serviceProvider); } else { - distributedCache = DistributedCache; + serializer = Serializer; } - if (distributedCache is null && ThrowIfMissingDistributedCache) + if (serializer is not null) { - throw new InvalidOperationException("A distributed cache has not been specified, or found in the DI container."); + cache.SetupDistributedCache(distributedCache, serializer); } - - if (distributedCache is not null) + else { - IFusionCacheSerializer? serializer; - if (UseRegisteredSerializer) - { - serializer = serviceProvider.GetService<IFusionCacheSerializer>(); - } - else if (SerializerFactory is not null) - { - serializer = SerializerFactory?.Invoke(serviceProvider); - } - else - { - serializer = Serializer; - } + if (logger?.IsEnabled(LogLevel.Warning) ?? false) + logger.Log(LogLevel.Warning, "FUSION [N={CacheName} I={CacheInstanceId}]: a usable implementation of IDistributedCache was found (CACHE={DistributedCacheType}) but no implementation of IFusionCacheSerializer was found, so the distributed cache has not been set up", cache.CacheName, cache.InstanceId, distributedCache.GetType().FullName); - if (serializer is not null) + if (ThrowIfMissingSerializer) { - cache.SetupDistributedCache(distributedCache, serializer); - } - else - { - if (logger?.IsEnabled(LogLevel.Warning) ?? false) - logger.Log(LogLevel.Warning, "FUSION [N={CacheName}]: a usable implementation of IDistributedCache was found (CACHE={DistributedCacheType}) but no implementation of IFusionCacheSerializer was found, so the distributed cache subsystem has not been set up", CacheName, distributedCache.GetType().FullName); - - if (ThrowIfMissingSerializer) - { - throw new InvalidOperationException($"A distributed cache was about to be used ({distributedCache.GetType().FullName}) but no implementation of IFusionCacheSerializer has been specified or found, so the distributed cache subsystem has not been set up"); - } + throw new InvalidOperationException($"A distributed cache was about to be used ({distributedCache.GetType().FullName}) but no implementation of IFusionCacheSerializer has been specified or found, so the distributed cache has not been set up"); } } + } - // BACKPLANE - IFusionCacheBackplane? backplane; - if (UseRegisteredBackplane) - { - backplane = serviceProvider.GetService<IFusionCacheBackplane>(); - } - else if (BackplaneFactory is not null) - { - backplane = BackplaneFactory?.Invoke(serviceProvider); - } - else - { - backplane = Backplane; - } + // BACKPLANE + IFusionCacheBackplane? backplane; + if (UseRegisteredBackplane) + { + backplane = serviceProvider.GetService<IFusionCacheBackplane>(); + } + else if (BackplaneFactory is not null) + { + backplane = BackplaneFactory?.Invoke(serviceProvider); + } + else + { + backplane = Backplane; + } - if (backplane is null && ThrowIfMissingBackplane) - { - throw new InvalidOperationException("A backplane has not been specified, or found in the DI container."); - } + if (backplane is null && ThrowIfMissingBackplane) + { + throw new InvalidOperationException("A backplane has not been specified, or found in the DI container."); + } - if (backplane is not null) - { - cache.SetupBackplane(backplane); - } + if (backplane is not null) + { + cache.SetupBackplane(backplane); + } - // PLUGINS - List<IFusionCachePlugin> plugins = new List<IFusionCachePlugin>(); + // PLUGINS + List<IFusionCachePlugin> plugins = new List<IFusionCachePlugin>(); - if (UseAllRegisteredPlugins) - { - plugins.AddRange(serviceProvider.GetServices<IFusionCachePlugin>()); - } + if (UseAllRegisteredPlugins) + { + plugins.AddRange(serviceProvider.GetServices<IFusionCachePlugin>()); + } - if (Plugins?.Any() == true) - { - plugins.AddRange(Plugins); - } + if (Plugins?.Any() == true) + { + plugins.AddRange(Plugins); + } - if (PluginsFactories?.Any() == true) + if (PluginsFactories?.Any() == true) + { + foreach (var pluginFactory in PluginsFactories) { - foreach (var pluginFactory in PluginsFactories) - { - var plugin = pluginFactory?.Invoke(serviceProvider); + var plugin = pluginFactory?.Invoke(serviceProvider); - if (plugin is not null) - plugins.Add(plugin); - } + if (plugin is not null) + plugins.Add(plugin); } + } - if (plugins.Count > 0) + if (plugins.Count > 0) + { + foreach (var plugin in plugins) { - foreach (var plugin in plugins) + try { - try - { - cache.AddPlugin(plugin); - } - catch - { - // EMPTY: EVERYTHING HAS BEEN ALREADY LOGGED, IF NECESSARY - } + cache.AddPlugin(plugin); + } + catch + { + // EMPTY: EVERYTHING HAS BEEN ALREADY LOGGED, IF NECESSARY } } + } - // CUSTOM SETUP ACTION - if (PostSetupAction is not null) - { - PostSetupAction?.Invoke(serviceProvider, cache); - } - - return cache; + // CUSTOM SETUP ACTION + if (PostSetupAction is not null) + { + PostSetupAction?.Invoke(serviceProvider, cache); } + + return cache; } } diff --git a/src/ZiggyCreatures.FusionCache/Internals/Distributed/DistributedCacheAccessor.cs b/src/ZiggyCreatures.FusionCache/Internals/Distributed/DistributedCacheAccessor.cs index 69458d83..e883bb3e 100644 --- a/src/ZiggyCreatures.FusionCache/Internals/Distributed/DistributedCacheAccessor.cs +++ b/src/ZiggyCreatures.FusionCache/Internals/Distributed/DistributedCacheAccessor.cs @@ -1,17 +1,18 @@ using System; +using System.Reflection; using System.Runtime.CompilerServices; +using System.Threading; +using System.Threading.Tasks; using Microsoft.Extensions.Caching.Distributed; using Microsoft.Extensions.Logging; using ZiggyCreatures.Caching.Fusion.Events; +using ZiggyCreatures.Caching.Fusion.Internals.Memory; using ZiggyCreatures.Caching.Fusion.Serialization; namespace ZiggyCreatures.Caching.Fusion.Internals.Distributed; internal sealed partial class DistributedCacheAccessor { - private const string WireFormatVersion = "v1"; - private const char WireFormatSeparator = ':'; - public DistributedCacheAccessor(IDistributedCache distributedCache, IFusionCacheSerializer serializer, FusionCacheOptions options, ILogger? logger, FusionCacheDistributedEventsHub events) { if (distributedCache is null) @@ -33,10 +34,18 @@ public DistributedCacheAccessor(IDistributedCache distributedCache, IFusionCache // WIRE FORMAT SETUP _wireFormatToken = _options.DistributedCacheKeyModifierMode == CacheKeyModifierMode.Prefix - ? (WireFormatVersion + WireFormatSeparator) + ? (FusionCacheOptions.DistributedCacheWireFormatVersion + FusionCacheOptions.DistributedCacheWireFormatSeparator) : _options.DistributedCacheKeyModifierMode == CacheKeyModifierMode.Suffix - ? WireFormatSeparator + WireFormatVersion + ? FusionCacheOptions.DistributedCacheWireFormatSeparator + FusionCacheOptions.DistributedCacheWireFormatVersion : string.Empty; + + _wireFormatToken = _options.DistributedCacheKeyModifierMode switch + { + CacheKeyModifierMode.Prefix => FusionCacheOptions.DistributedCacheWireFormatVersion + FusionCacheOptions.DistributedCacheWireFormatSeparator, + CacheKeyModifierMode.Suffix => FusionCacheOptions.DistributedCacheWireFormatSeparator + FusionCacheOptions.DistributedCacheWireFormatVersion, + CacheKeyModifierMode.None => string.Empty, + _ => throw new NotImplementedException(), + }; } private readonly IDistributedCache _cache; @@ -72,7 +81,7 @@ private void UpdateLastError(string key, string operationId) if (res && hasChanged) { if (_logger?.IsEnabled(LogLevel.Warning) ?? false) - _logger.Log(LogLevel.Warning, "FUSION [N={CacheName}] (O={CacheOperationId} K={CacheKey}): distributed cache temporarily de-activated for {BreakDuration}", _options.CacheName, operationId, key, _breaker.BreakDuration); + _logger.Log(LogLevel.Warning, "FUSION [N={CacheName} I={CacheInstanceId}] (O={CacheOperationId} K={CacheKey}): [DC] distributed cache temporarily de-activated for {BreakDuration}", _options.CacheName, _options.InstanceId, operationId, key, _breaker.BreakDuration); // EVENT _events.OnCircuitBreakerChange(operationId, key, false); @@ -86,7 +95,7 @@ public bool IsCurrentlyUsable(string? operationId, string? key) if (res && hasChanged) { if (_logger?.IsEnabled(LogLevel.Warning) ?? false) - _logger.Log(LogLevel.Warning, "FUSION [N={CacheName}] (O={CacheOperationId} K={CacheKey}): distributed cache activated again", _options.CacheName, operationId, key); + _logger.Log(LogLevel.Warning, "FUSION [N={CacheName} I={CacheInstanceId}] (O={CacheOperationId} K={CacheKey}): [DC] distributed cache activated again", _options.CacheName, _options.InstanceId, operationId, key); // EVENT _events.OnCircuitBreakerChange(operationId, key, true); @@ -101,7 +110,7 @@ private void ProcessError(string operationId, string key, Exception exc, string if (exc is SyntheticTimeoutException) { if (_logger?.IsEnabled(_options.DistributedCacheSyntheticTimeoutsLogLevel) ?? false) - _logger.Log(_options.DistributedCacheSyntheticTimeoutsLogLevel, exc, "FUSION [N={CacheName}] (O={CacheOperationId} K={CacheKey}): a synthetic timeout occurred while " + actionDescription, _options.CacheName, operationId, key); + _logger.Log(_options.DistributedCacheSyntheticTimeoutsLogLevel, exc, "FUSION [N={CacheName} I={CacheInstanceId}] (O={CacheOperationId} K={CacheKey}): [DC] a synthetic timeout occurred while " + actionDescription, _options.CacheName, _options.InstanceId, operationId, key); return; } @@ -109,6 +118,29 @@ private void ProcessError(string operationId, string key, Exception exc, string UpdateLastError(key, operationId); if (_logger?.IsEnabled(_options.DistributedCacheErrorsLogLevel) ?? false) - _logger.Log(_options.DistributedCacheErrorsLogLevel, exc, "FUSION [N={CacheName}] (O={CacheOperationId} K={CacheKey}): an error occurred while " + actionDescription, _options.CacheName, operationId, key); + _logger.Log(_options.DistributedCacheErrorsLogLevel, exc, "FUSION [N={CacheName} I={CacheInstanceId}] (O={CacheOperationId} K={CacheKey}): [DC] an error occurred while " + actionDescription, _options.CacheName, _options.InstanceId, operationId, key); + } + + private static readonly MethodInfo __methodInfoSetEntryAsyncOpenGeneric = typeof(DistributedCacheAccessor).GetMethod(nameof(SetEntryAsync), BindingFlags.NonPublic | BindingFlags.Public | BindingFlags.Instance); + + public async ValueTask<bool> SetEntryUntypedAsync(string operationId, string key, FusionCacheMemoryEntry memoryEntry, FusionCacheEntryOptions options, bool isBackground, CancellationToken token) + { + try + { + if (memoryEntry is null) + return false; + + var methodInfo = __methodInfoSetEntryAsyncOpenGeneric.MakeGenericMethod(memoryEntry.ValueType); + + // SIGNATURE PARAMS: string operationId, string key, IFusionCacheEntry entry, FusionCacheEntryOptions options, bool isBackground, CancellationToken token + return await ((ValueTask<bool>)methodInfo.Invoke(this, new object[] { operationId, key, memoryEntry, options, isBackground, token })).ConfigureAwait(false); + } + catch (Exception exc) + { + if (_logger?.IsEnabled(LogLevel.Error) ?? false) + _logger.Log(LogLevel.Error, exc, "FUSION [N={CacheName} I={CacheInstanceId}] (O={CacheOperationId} K={CacheKey}): [DC] an error occurred while calling SetEntryUntypedAsync() to try to set a distributed entry without knowing the TValue type", _options.CacheName, _options.InstanceId, operationId, key); + + return false; + } } } diff --git a/src/ZiggyCreatures.FusionCache/Internals/Distributed/DistributedCacheAccessor_Async.cs b/src/ZiggyCreatures.FusionCache/Internals/Distributed/DistributedCacheAccessor_Async.cs index 6dd60055..1db6a6fe 100644 --- a/src/ZiggyCreatures.FusionCache/Internals/Distributed/DistributedCacheAccessor_Async.cs +++ b/src/ZiggyCreatures.FusionCache/Internals/Distributed/DistributedCacheAccessor_Async.cs @@ -1,48 +1,61 @@ using System; using System.Threading; using System.Threading.Tasks; -using Microsoft.Extensions.Caching.Distributed; using Microsoft.Extensions.Logging; namespace ZiggyCreatures.Caching.Fusion.Internals.Distributed; internal partial class DistributedCacheAccessor { - private async ValueTask ExecuteOperationAsync(string operationId, string key, Func<CancellationToken, Task> action, string actionDescription, FusionCacheEntryOptions options, DistributedCacheEntryOptions? distributedOptions, CancellationToken token) + private async ValueTask<bool> ExecuteOperationAsync(string operationId, string key, Func<CancellationToken, Task> action, string actionDescription, FusionCacheEntryOptions options, CancellationToken token) { - if (IsCurrentlyUsable(operationId, key) == false) - return; + //if (IsCurrentlyUsable(operationId, key) == false) + // return false; - token.ThrowIfCancellationRequested(); + try + { + if (_logger?.IsEnabled(LogLevel.Trace) ?? false) + _logger.Log(LogLevel.Trace, "FUSION [N={CacheName} I={CacheInstanceId}] (O={CacheOperationId} K={CacheKey}): [DC] before " + actionDescription, _options.CacheName, _options.InstanceId, operationId, key); + + await RunUtils.RunAsyncActionWithTimeoutAsync(action, Timeout.InfiniteTimeSpan, true, token: token).ConfigureAwait(false); + + if (_logger?.IsEnabled(LogLevel.Trace) ?? false) + _logger.Log(LogLevel.Trace, "FUSION [N={CacheName} I={CacheInstanceId}] (O={CacheOperationId} K={CacheKey}): [DC] after " + actionDescription, _options.CacheName, _options.InstanceId, operationId, key); + } + catch (Exception exc) + { + ProcessError(operationId, key, exc, actionDescription); + + if (exc is not SyntheticTimeoutException && options.ReThrowDistributedCacheExceptions) + { + if (_options.ReThrowOriginalExceptions) + { + throw; + } + else + { + throw new FusionCacheDistributedCacheException("An error occurred while working with the distributed cache", exc); + } + } + + return false; + } - var actionDescriptionInner = actionDescription + (options.AllowBackgroundDistributedCacheOperations ? " (background)" : null); - - await FusionCacheExecutionUtils - .RunAsyncActionAdvancedAsync( - action, - options.DistributedCacheHardTimeout, - false, - options.AllowBackgroundDistributedCacheOperations == false, - exc => ProcessError(operationId, key, exc, actionDescriptionInner), - options.ReThrowDistributedCacheExceptions && options.AllowBackgroundDistributedCacheOperations == false && options.DistributedCacheHardTimeout == Timeout.InfiniteTimeSpan, - token - ) - .ConfigureAwait(false) - ; + return true; } - public async ValueTask SetEntryAsync<TValue>(string operationId, string key, IFusionCacheEntry entry, FusionCacheEntryOptions options, CancellationToken token) + public async ValueTask<bool> SetEntryAsync<TValue>(string operationId, string key, IFusionCacheEntry entry, FusionCacheEntryOptions options, bool isBackground, CancellationToken token) { if (IsCurrentlyUsable(operationId, key) == false) - return; + return false; token.ThrowIfCancellationRequested(); // IF FAIL-SAFE IS DISABLED AND DURATION IS <= ZERO -> REMOVE ENTRY (WILL SAVE RESOURCES) if (options.IsFailSafeEnabled == false && options.DistributedCacheDuration.GetValueOrDefault(options.Duration) <= TimeSpan.Zero) { - await RemoveEntryAsync(operationId, key, options, token).ConfigureAwait(false); - return; + await RemoveEntryAsync(operationId, key, options, isBackground, token).ConfigureAwait(false); + return true; } var distributedEntry = entry.AsDistributedEntry<TValue>(options); @@ -52,73 +65,91 @@ public async ValueTask SetEntryAsync<TValue>(string operationId, string key, IFu try { if (_logger?.IsEnabled(LogLevel.Debug) ?? false) - _logger.Log(LogLevel.Debug, "FUSION [N={CacheName}] (O={CacheOperationId} K={CacheKey}): serializing the entry {Entry}", _options.CacheName, operationId, key, distributedEntry.ToLogString()); + _logger.Log(LogLevel.Debug, "FUSION [N={CacheName} I={CacheInstanceId}] (O={CacheOperationId} K={CacheKey}): [DC] serializing the entry {Entry}", _options.CacheName, _options.InstanceId, operationId, key, distributedEntry.ToLogString()); data = await _serializer.SerializeAsync(distributedEntry).ConfigureAwait(false); } catch (Exception exc) { if (_logger?.IsEnabled(_options.SerializationErrorsLogLevel) ?? false) - _logger.Log(_options.SerializationErrorsLogLevel, exc, "FUSION [N={CacheName}] (O={CacheOperationId} K={CacheKey}): an error occurred while serializing an entry {Entry}", _options.CacheName, operationId, key, distributedEntry.ToLogString()); - - if (options.ReThrowSerializationExceptions) - throw; + _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()); // EVENT _events.OnSerializationError(operationId, key); + if (options.ReThrowSerializationExceptions) + { + if (_options.ReThrowOriginalExceptions) + { + throw; + } + else + { + throw new FusionCacheSerializationException("An error occurred while serializing a cache value", exc); + } + } + data = null; } if (data is null) - return; + return false; // SAVE TO DISTRIBUTED CACHE - var distributedOptions = options.ToDistributedCacheEntryOptions(_options, _logger, operationId, key); - await ExecuteOperationAsync( + return await ExecuteOperationAsync( operationId, key, async ct => { - if (_logger?.IsEnabled(LogLevel.Debug) ?? false) - _logger.Log(LogLevel.Debug, "FUSION [N={CacheName}] (O={CacheOperationId} K={CacheKey}): setting the entry in distributed {Entry}", _options.CacheName, operationId, key, distributedEntry.ToLogString()); + var distributedOptions = options.ToDistributedCacheEntryOptions(_options, _logger, operationId, key); await _cache.SetAsync(MaybeProcessCacheKey(key), data, distributedOptions, ct).ConfigureAwait(false); // EVENT _events.OnSet(operationId, key); }, - "saving entry in distributed", + "setting entry in distributed" + isBackground.ToString(" (background)"), options, - distributedOptions, token ).ConfigureAwait(false); } - public async ValueTask<(FusionCacheDistributedEntry<TValue>? entry, bool isValid)> TryGetEntryAsync<TValue>(string operationId, string key, FusionCacheEntryOptions options, bool hasFallbackValue, CancellationToken token) + public async ValueTask<(FusionCacheDistributedEntry<TValue>? entry, bool isValid)> TryGetEntryAsync<TValue>(string operationId, string key, FusionCacheEntryOptions options, bool hasFallbackValue, TimeSpan? timeout, CancellationToken token) { if (IsCurrentlyUsable(operationId, key) == false) return (null, false); if (_logger?.IsEnabled(LogLevel.Trace) ?? false) - _logger.Log(LogLevel.Trace, "FUSION [N={CacheName}] (O={CacheOperationId} K={CacheKey}): trying to get entry from distributed", _options.CacheName, operationId, key); + _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); // GET FROM DISTRIBUTED CACHE byte[]? data; try { - var timeout = options.GetAppropriateDistributedCacheTimeout(hasFallbackValue); - data = await FusionCacheExecutionUtils.RunAsyncFuncWithTimeoutAsync<byte[]?>(async ct => await _cache.GetAsync(MaybeProcessCacheKey(key), ct).ConfigureAwait(false), timeout, true, token: token).ConfigureAwait(false); + timeout ??= options.GetAppropriateDistributedCacheTimeout(hasFallbackValue); + data = await RunUtils.RunAsyncFuncWithTimeoutAsync<byte[]?>( + async ct => await _cache.GetAsync(MaybeProcessCacheKey(key), ct).ConfigureAwait(false), + timeout.Value, + true, + token: token + ).ConfigureAwait(false); } catch (Exception exc) { ProcessError(operationId, key, exc, "getting entry from distributed"); - data = null; - if (exc is not SyntheticTimeoutException && options.ReThrowDistributedCacheExceptions) { - throw; + if (_options.ReThrowOriginalExceptions) + { + throw; + } + else + { + throw new FusionCacheDistributedCacheException("An error occurred while working with the distributed cache", exc); + } } + + data = null; } if (data is null) @@ -132,19 +163,19 @@ await ExecuteOperationAsync( if (entry is null) { if (_logger?.IsEnabled(LogLevel.Debug) ?? false) - _logger.Log(LogLevel.Debug, "FUSION [N={CacheName}] (O={CacheOperationId} K={CacheKey}): distributed entry not found", _options.CacheName, operationId, key); + _logger.Log(LogLevel.Debug, "FUSION [N={CacheName} I={CacheInstanceId}] (O={CacheOperationId} K={CacheKey}): [DC] distributed entry not found", _options.CacheName, _options.InstanceId, operationId, key); } else { if (entry.IsLogicallyExpired()) { if (_logger?.IsEnabled(LogLevel.Debug) ?? false) - _logger.Log(LogLevel.Debug, "FUSION [N={CacheName}] (O={CacheOperationId} K={CacheKey}): distributed entry found (expired) {Entry}", _options.CacheName, operationId, key, entry.ToLogString()); + _logger.Log(LogLevel.Debug, "FUSION [N={CacheName} I={CacheInstanceId}] (O={CacheOperationId} K={CacheKey}): [DC] distributed entry found (expired) {Entry}", _options.CacheName, _options.InstanceId, operationId, key, entry.ToLogString()); } else { if (_logger?.IsEnabled(LogLevel.Debug) ?? false) - _logger.Log(LogLevel.Debug, "FUSION [N={CacheName}] (O={CacheOperationId} K={CacheKey}): distributed entry found {Entry}", _options.CacheName, operationId, key, entry.ToLogString()); + _logger.Log(LogLevel.Debug, "FUSION [N={CacheName} I={CacheInstanceId}] (O={CacheOperationId} K={CacheKey}): [DC] distributed entry found {Entry}", _options.CacheName, _options.InstanceId, operationId, key, entry.ToLogString()); isValid = true; } @@ -165,13 +196,22 @@ await ExecuteOperationAsync( catch (Exception exc) { if (_logger?.IsEnabled(_options.SerializationErrorsLogLevel) ?? false) - _logger.Log(_options.SerializationErrorsLogLevel, exc, "FUSION [N={CacheName}] (O={CacheOperationId} K={CacheKey}): an error occurred while deserializing an entry", _options.CacheName, operationId, key); - - if (options.ReThrowSerializationExceptions) - throw; + _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); // EVENT _events.OnDeserializationError(operationId, key); + + if (options.ReThrowSerializationExceptions) + { + if (_options.ReThrowOriginalExceptions) + { + throw; + } + else + { + throw new FusionCacheSerializationException("An error occurred while deserializing a cache value", exc); + } + } } // EVENT @@ -180,11 +220,24 @@ await ExecuteOperationAsync( return (null, false); } - public async ValueTask RemoveEntryAsync(string operationId, string key, FusionCacheEntryOptions options, CancellationToken token) + public async ValueTask<bool> RemoveEntryAsync(string operationId, string key, FusionCacheEntryOptions options, bool isBackground, CancellationToken token) { - await ExecuteOperationAsync(operationId, key, ct => _cache.RemoveAsync(MaybeProcessCacheKey(key), ct), "removing entry from distributed", options, null, token); + if (IsCurrentlyUsable(operationId, key) == false) + return false; - // EVENT - _events.OnRemove(operationId, key); + return await ExecuteOperationAsync( + operationId, + key, + async ct => + { + await _cache.RemoveAsync(MaybeProcessCacheKey(key), ct); + + // EVENT + _events.OnRemove(operationId, key); + }, + "removing entry from distributed" + isBackground.ToString(" (background)"), + options, + token + ).ConfigureAwait(false); } } diff --git a/src/ZiggyCreatures.FusionCache/Internals/Distributed/DistributedCacheAccessor_Sync.cs b/src/ZiggyCreatures.FusionCache/Internals/Distributed/DistributedCacheAccessor_Sync.cs index ad08207d..d1673b9e 100644 --- a/src/ZiggyCreatures.FusionCache/Internals/Distributed/DistributedCacheAccessor_Sync.cs +++ b/src/ZiggyCreatures.FusionCache/Internals/Distributed/DistributedCacheAccessor_Sync.cs @@ -1,46 +1,60 @@ using System; using System.Threading; -using Microsoft.Extensions.Caching.Distributed; using Microsoft.Extensions.Logging; namespace ZiggyCreatures.Caching.Fusion.Internals.Distributed; internal partial class DistributedCacheAccessor { - private void ExecuteOperation(string operationId, string key, Action<CancellationToken> action, string actionDescription, FusionCacheEntryOptions options, DistributedCacheEntryOptions? distributedOptions, CancellationToken token) + private bool ExecuteOperation(string operationId, string key, Action<CancellationToken> action, string actionDescription, FusionCacheEntryOptions options, CancellationToken token) { - if (IsCurrentlyUsable(operationId, key) == false) - return; + //if (IsCurrentlyUsable(operationId, key) == false) + // return false; - token.ThrowIfCancellationRequested(); + try + { + if (_logger?.IsEnabled(LogLevel.Trace) ?? false) + _logger.Log(LogLevel.Trace, "FUSION [N={CacheName} I={CacheInstanceId}] (O={CacheOperationId} K={CacheKey}): [DC] before " + actionDescription, _options.CacheName, _options.InstanceId, operationId, key); + + RunUtils.RunSyncActionWithTimeout(action, Timeout.InfiniteTimeSpan, true, token: token); + + if (_logger?.IsEnabled(LogLevel.Trace) ?? false) + _logger.Log(LogLevel.Trace, "FUSION [N={CacheName} I={CacheInstanceId}] (O={CacheOperationId} K={CacheKey}): [DC] after " + actionDescription, _options.CacheName, _options.InstanceId, operationId, key); + } + catch (Exception exc) + { + ProcessError(operationId, key, exc, actionDescription); + + if (exc is not SyntheticTimeoutException && options.ReThrowDistributedCacheExceptions) + { + if (_options.ReThrowOriginalExceptions) + { + throw; + } + else + { + throw new FusionCacheDistributedCacheException("An error occurred while working with the distributed cache", exc); + } + } + + return false; + } - var actionDescriptionInner = actionDescription + (options.AllowBackgroundDistributedCacheOperations ? " (background)" : null); - - FusionCacheExecutionUtils - .RunSyncActionAdvanced( - action, - options.DistributedCacheHardTimeout, - false, - options.AllowBackgroundDistributedCacheOperations == false, - exc => ProcessError(operationId, key, exc, actionDescriptionInner), - options.ReThrowDistributedCacheExceptions && options.AllowBackgroundDistributedCacheOperations == false && options.DistributedCacheHardTimeout == Timeout.InfiniteTimeSpan, - token - ) - ; + return true; } - public void SetEntry<TValue>(string operationId, string key, IFusionCacheEntry entry, FusionCacheEntryOptions options, CancellationToken token = default) + public bool SetEntry<TValue>(string operationId, string key, IFusionCacheEntry entry, FusionCacheEntryOptions options, bool isBackground, CancellationToken token) { if (IsCurrentlyUsable(operationId, key) == false) - return; + return false; token.ThrowIfCancellationRequested(); // IF FAIL-SAFE IS DISABLED AND DURATION IS <= ZERO -> REMOVE ENTRY (WILL SAVE RESOURCES) if (options.IsFailSafeEnabled == false && options.DistributedCacheDuration.GetValueOrDefault(options.Duration) <= TimeSpan.Zero) { - RemoveEntry(operationId, key, options, token); - return; + RemoveEntry(operationId, key, options, isBackground, token); + return true; } var distributedEntry = entry.AsDistributedEntry<TValue>(options); @@ -50,73 +64,91 @@ public void SetEntry<TValue>(string operationId, string key, IFusionCacheEntry e try { if (_logger?.IsEnabled(LogLevel.Debug) ?? false) - _logger.Log(LogLevel.Debug, "FUSION [N={CacheName}] (O={CacheOperationId} K={CacheKey}): serializing the entry {Entry}", _options.CacheName, operationId, key, distributedEntry.ToLogString()); + _logger.Log(LogLevel.Debug, "FUSION [N={CacheName} I={CacheInstanceId}] (O={CacheOperationId} K={CacheKey}): [DC] serializing the entry {Entry}", _options.CacheName, _options.InstanceId, operationId, key, distributedEntry.ToLogString()); data = _serializer.Serialize(distributedEntry); } catch (Exception exc) { if (_logger?.IsEnabled(_options.SerializationErrorsLogLevel) ?? false) - _logger.Log(_options.SerializationErrorsLogLevel, exc, "FUSION [N={CacheName}] (O={CacheOperationId} K={CacheKey}): an error occurred while serializing an entry {Entry}", _options.CacheName, operationId, key, distributedEntry.ToLogString()); - - if (options.ReThrowSerializationExceptions) - throw; + _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()); // EVENT _events.OnSerializationError(operationId, key); + if (options.ReThrowSerializationExceptions) + { + if (_options.ReThrowOriginalExceptions) + { + throw; + } + else + { + throw new FusionCacheSerializationException("An error occurred while serializing a cache value", exc); + } + } + data = null; } if (data is null) - return; + return false; // SAVE TO DISTRIBUTED CACHE - var distributedOptions = options.ToDistributedCacheEntryOptions(_options, _logger, operationId, key); - ExecuteOperation( + return ExecuteOperation( operationId, key, _ => { - if (_logger?.IsEnabled(LogLevel.Debug) ?? false) - _logger.Log(LogLevel.Debug, "FUSION [N={CacheName}] (O={CacheOperationId} K={CacheKey}): setting the entry in distributed {Entry}", _options.CacheName, operationId, key, distributedEntry.ToLogString()); + var distributedOptions = options.ToDistributedCacheEntryOptions(_options, _logger, operationId, key); _cache.Set(MaybeProcessCacheKey(key), data, distributedOptions); // EVENT _events.OnSet(operationId, key); }, - "saving entry in distributed", + "setting entry in distributed" + isBackground.ToString(" (background)"), options, - distributedOptions, token ); } - public (FusionCacheDistributedEntry<TValue>? entry, bool isValid) TryGetEntry<TValue>(string operationId, string key, FusionCacheEntryOptions options, bool hasFallbackValue, CancellationToken token) + public (FusionCacheDistributedEntry<TValue>? entry, bool isValid) TryGetEntry<TValue>(string operationId, string key, FusionCacheEntryOptions options, bool hasFallbackValue, TimeSpan? timeout, CancellationToken token) { if (IsCurrentlyUsable(operationId, key) == false) return (null, false); if (_logger?.IsEnabled(LogLevel.Trace) ?? false) - _logger.Log(LogLevel.Trace, "FUSION [N={CacheName}] (O={CacheOperationId} K={CacheKey}): trying to get entry from distributed", _options.CacheName, operationId, key); + _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); // GET FROM DISTRIBUTED CACHE byte[]? data; try { - var timeout = options.GetAppropriateDistributedCacheTimeout(hasFallbackValue); - data = FusionCacheExecutionUtils.RunSyncFuncWithTimeout<byte[]?>(_ => _cache.Get(MaybeProcessCacheKey(key)), timeout, true, token: token); + timeout ??= options.GetAppropriateDistributedCacheTimeout(hasFallbackValue); + data = RunUtils.RunSyncFuncWithTimeout<byte[]?>( + _ => _cache.Get(MaybeProcessCacheKey(key)), + timeout.Value, + true, + token: token + ); } catch (Exception exc) { ProcessError(operationId, key, exc, "getting entry from distributed"); - data = null; - if (exc is not SyntheticTimeoutException && options.ReThrowDistributedCacheExceptions) { - throw; + if (_options.ReThrowOriginalExceptions) + { + throw; + } + else + { + throw new FusionCacheDistributedCacheException("An error occurred while working with the distributed cache", exc); + } } + + data = null; } if (data is null) @@ -130,19 +162,19 @@ public void SetEntry<TValue>(string operationId, string key, IFusionCacheEntry e if (entry is null) { if (_logger?.IsEnabled(LogLevel.Debug) ?? false) - _logger.Log(LogLevel.Debug, "FUSION [N={CacheName}] (O={CacheOperationId} K={CacheKey}): distributed entry not found", _options.CacheName, operationId, key); + _logger.Log(LogLevel.Debug, "FUSION [N={CacheName} I={CacheInstanceId}] (O={CacheOperationId} K={CacheKey}): [DC] distributed entry not found", _options.CacheName, _options.InstanceId, operationId, key); } else { if (entry.IsLogicallyExpired()) { if (_logger?.IsEnabled(LogLevel.Debug) ?? false) - _logger.Log(LogLevel.Debug, "FUSION [N={CacheName}] (O={CacheOperationId} K={CacheKey}): distributed entry found (expired) {Entry}", _options.CacheName, operationId, key, entry.ToLogString()); + _logger.Log(LogLevel.Debug, "FUSION [N={CacheName} I={CacheInstanceId}] (O={CacheOperationId} K={CacheKey}): [DC] distributed entry found (expired) {Entry}", _options.CacheName, _options.InstanceId, operationId, key, entry.ToLogString()); } else { if (_logger?.IsEnabled(LogLevel.Debug) ?? false) - _logger.Log(LogLevel.Debug, "FUSION [N={CacheName}] (O={CacheOperationId} K={CacheKey}): distributed entry found {Entry}", _options.CacheName, operationId, key, entry.ToLogString()); + _logger.Log(LogLevel.Debug, "FUSION [N={CacheName} I={CacheInstanceId}] (O={CacheOperationId} K={CacheKey}): [DC] distributed entry found {Entry}", _options.CacheName, _options.InstanceId, operationId, key, entry.ToLogString()); isValid = true; } @@ -163,13 +195,22 @@ public void SetEntry<TValue>(string operationId, string key, IFusionCacheEntry e catch (Exception exc) { if (_logger?.IsEnabled(_options.SerializationErrorsLogLevel) ?? false) - _logger.Log(_options.SerializationErrorsLogLevel, exc, "FUSION [N={CacheName}] (O={CacheOperationId} K={CacheKey}): an error occurred while deserializing an entry", _options.CacheName, operationId, key); - - if (options.ReThrowSerializationExceptions) - throw; + _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); // EVENT _events.OnDeserializationError(operationId, key); + + if (options.ReThrowSerializationExceptions) + { + if (_options.ReThrowOriginalExceptions) + { + throw; + } + else + { + throw new FusionCacheSerializationException("An error occurred while deserializing a cache value", exc); + } + } } // EVENT @@ -178,11 +219,24 @@ public void SetEntry<TValue>(string operationId, string key, IFusionCacheEntry e return (null, false); } - public void RemoveEntry(string operationId, string key, FusionCacheEntryOptions options, CancellationToken token) + public bool RemoveEntry(string operationId, string key, FusionCacheEntryOptions options, bool isBackground, CancellationToken token) { - ExecuteOperation(operationId, key, _ => _cache.Remove(MaybeProcessCacheKey(key)), "removing entry from distributed", options, null, token); + if (IsCurrentlyUsable(operationId, key) == false) + return false; - // EVENT - _events.OnRemove(operationId, key); + return ExecuteOperation( + operationId, + key, + _ => + { + _cache.Remove(MaybeProcessCacheKey(key)); + + // EVENT + _events.OnRemove(operationId, key); + }, + "removing entry from distributed" + isBackground.ToString(" (background)"), + options, + token + ); } } diff --git a/src/ZiggyCreatures.FusionCache/Internals/Distributed/FusionCacheDistributedEntry.cs b/src/ZiggyCreatures.FusionCache/Internals/Distributed/FusionCacheDistributedEntry.cs index b0780576..498199c4 100644 --- a/src/ZiggyCreatures.FusionCache/Internals/Distributed/FusionCacheDistributedEntry.cs +++ b/src/ZiggyCreatures.FusionCache/Internals/Distributed/FusionCacheDistributedEntry.cs @@ -18,11 +18,11 @@ public sealed class FusionCacheDistributedEntry<TValue> /// <param name="value">The actual value.</param> /// <param name="metadata">The metadata for the entry.</param> /// <param name="timestamp">The original timestamp of the entry, see <see cref="Timestamp"/>.</param> - public FusionCacheDistributedEntry(TValue value, FusionCacheEntryMetadata? metadata, long? timestamp = null) + public FusionCacheDistributedEntry(TValue value, FusionCacheEntryMetadata? metadata, long timestamp) { Value = value; Metadata = metadata; - Timestamp = timestamp ?? FusionCacheInternalUtils.GetCurrentTimestamp(); + Timestamp = timestamp; } #pragma warning disable CS8618 // Non-nullable field must contain a non-null value when exiting constructor. Consider declaring as nullable. @@ -47,7 +47,7 @@ public FusionCacheDistributedEntry() /// <inheritdoc/> [DataMember(Name = "t", EmitDefaultValue = false)] - public long? Timestamp { get; set; } + public long Timestamp { get; set; } /// <inheritdoc/> public TValue1 GetValue<TValue1>() @@ -88,7 +88,7 @@ public override string ToString() /// <param name="etag">If provided, it's the ETag of the entry: this may be used in the next refresh cycle (eg: with the use of the "If-None-Match" header in an http request) to check if the entry is changed, to avoid getting the entire value.</param> /// <param name="timestamp">The value for the <see cref="Timestamp"/> property.</param> /// <returns>The newly created entry.</returns> - public static FusionCacheDistributedEntry<TValue> CreateFromOptions(TValue value, FusionCacheEntryOptions options, bool isFromFailSafe, DateTimeOffset? lastModified, string? etag, long? timestamp) + public static FusionCacheDistributedEntry<TValue> CreateFromOptions(TValue value, FusionCacheEntryOptions options, bool isFromFailSafe, DateTimeOffset? lastModified, string? etag, long timestamp) { var exp = FusionCacheInternalUtils.GetNormalizedAbsoluteExpiration(isFromFailSafe ? options.FailSafeThrottleDuration : options.DistributedCacheDuration.GetValueOrDefault(options.Duration), options, false); @@ -97,7 +97,8 @@ public static FusionCacheDistributedEntry<TValue> CreateFromOptions(TValue value return new FusionCacheDistributedEntry<TValue>( value, new FusionCacheEntryMetadata(exp, isFromFailSafe, eagerExp, etag, lastModified), - timestamp ?? FusionCacheInternalUtils.GetCurrentTimestamp() + //timestamp ?? FusionCacheInternalUtils.GetCurrentTimestamp() + timestamp ); } diff --git a/src/ZiggyCreatures.FusionCache/Internals/FusionCacheAction.cs b/src/ZiggyCreatures.FusionCache/Internals/FusionCacheAction.cs new file mode 100644 index 00000000..4271f61f --- /dev/null +++ b/src/ZiggyCreatures.FusionCache/Internals/FusionCacheAction.cs @@ -0,0 +1,9 @@ +namespace ZiggyCreatures.Caching.Fusion.Internals; + +internal enum FusionCacheAction +{ + Unknown = 0, + EntrySet = 1, + EntryRemove = 2, + EntryExpire = 3 +} diff --git a/src/ZiggyCreatures.FusionCache/Internals/FusionCacheEntryMetadata.cs b/src/ZiggyCreatures.FusionCache/Internals/FusionCacheEntryMetadata.cs index 26f3acb5..1954caa3 100644 --- a/src/ZiggyCreatures.FusionCache/Internals/FusionCacheEntryMetadata.cs +++ b/src/ZiggyCreatures.FusionCache/Internals/FusionCacheEntryMetadata.cs @@ -7,7 +7,7 @@ namespace ZiggyCreatures.Caching.Fusion.Internals; /// Metadata for an entry in a <see cref="FusionCache"/> . /// </summary> [DataContract] -public class FusionCacheEntryMetadata +public sealed class FusionCacheEntryMetadata { /// <summary> /// Creates a new instance. diff --git a/src/ZiggyCreatures.FusionCache/Internals/FusionCacheInternalUtils.cs b/src/ZiggyCreatures.FusionCache/Internals/FusionCacheInternalUtils.cs index 7dc470bf..16affa21 100644 --- a/src/ZiggyCreatures.FusionCache/Internals/FusionCacheInternalUtils.cs +++ b/src/ZiggyCreatures.FusionCache/Internals/FusionCacheInternalUtils.cs @@ -18,9 +18,11 @@ internal static class FusionCacheInternalUtils private static readonly DateTimeOffset DateTimeOffsetMaxValue = DateTimeOffset.MaxValue; private static readonly TimeSpan TimeSpanMaxValue = TimeSpan.MaxValue; + [MethodImpl(MethodImplOptions.AggressiveInlining)] public static long GetCurrentTimestamp() { - return DateTimeOffset.UtcNow.ToUnixTimeMilliseconds(); + //return DateTimeOffset.UtcNow.ToUnixTimeMilliseconds(); + return DateTimeOffset.UtcNow.UtcTicks; } private static string GenerateOperationId(long id) @@ -206,11 +208,18 @@ public static string ToStringYN(this bool b) return b ? "Y" : "N"; } + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static string? ToString(this bool b, string? trueString, string? falseString = null) + { + return b ? trueString : falseString; + } + public static FusionCacheDistributedEntry<TValue> AsDistributedEntry<TValue>(this IFusionCacheEntry entry, FusionCacheEntryOptions options) { if (entry is FusionCacheDistributedEntry<TValue>) return (FusionCacheDistributedEntry<TValue>)entry; + // TODO: CHECK THIS AGAIN return FusionCacheDistributedEntry<TValue>.CreateFromOptions(entry.GetValue<TValue>(), options, entry.Metadata?.IsFromFailSafe ?? false, entry.Metadata?.LastModified, entry.Metadata?.ETag, entry.Timestamp); //return FusionCacheDistributedEntry<TValue>.CreateFromOtherEntry(entry, options); } @@ -235,7 +244,7 @@ static void ExecuteInvocations(string? operationId, string? key, IFusionCache ca } catch (Exception exc) { - logger?.Log(errorLogLevel, exc, "FUSION [N={CacheName}] (O={CacheOperationId} K={CacheKey}): an error occurred while handling an event handler for {EventName}", cache.CacheName, operationId, key, eventName); + logger?.Log(errorLogLevel, exc, "FUSION [N={CacheName} I={CacheInstanceId}] (O={CacheOperationId} K={CacheKey}): an error occurred while handling an event handler for {EventName}", cache.CacheName, cache.InstanceId, operationId, key, eventName); } } } @@ -266,9 +275,9 @@ public static string GetBackplaneChannelName(this FusionCacheOptions options) // SAFETY NET (BUT IT SHOULD NOT HAPPEN) if (string.IsNullOrWhiteSpace(prefix)) - prefix = "FusionCache"; + prefix = FusionCacheOptions.DefaultCacheName; - return $"{prefix}.Backplane"; + return $"{prefix}.Backplane{FusionCacheOptions.BackplaneWireFormatSeparator}{FusionCacheOptions.BackplaneWireFormatVersion}"; } public static DateTimeOffset GetNormalizedAbsoluteExpiration(TimeSpan duration, FusionCacheEntryOptions options, bool allowJittering) @@ -318,4 +327,15 @@ public static bool CanBeUsed(this DistributedCacheAccessor? dca, string? operati return false; } + + //public static bool CanBeUsed(this BackplaneAccessor? bpa, string? operationId, string? key) + //{ + // if (bpa is null) + // return false; + + // if (bpa.IsCurrentlyUsable(operationId, key)) + // return true; + + // return false; + //} } diff --git a/src/ZiggyCreatures.FusionCache/Internals/IFusionCacheEntry.cs b/src/ZiggyCreatures.FusionCache/Internals/IFusionCacheEntry.cs index 230c3393..68a7b371 100644 --- a/src/ZiggyCreatures.FusionCache/Internals/IFusionCacheEntry.cs +++ b/src/ZiggyCreatures.FusionCache/Internals/IFusionCacheEntry.cs @@ -28,7 +28,7 @@ public interface IFusionCacheEntry FusionCacheEntryMetadata? Metadata { get; } /// <summary> - /// The timestamp at which the cached value has been originally created: memory cache entries created from distributed cache entries will have the exact same timestamp. + /// The timestamp (in ticks) at which the cached value has been originally created: memory cache entries created from distributed cache entries will have the exact same timestamp. /// </summary> - public long? Timestamp { get; } + public long Timestamp { get; } } diff --git a/src/ZiggyCreatures.FusionCache/Internals/Memory/FusionCacheMemoryEntry.cs b/src/ZiggyCreatures.FusionCache/Internals/Memory/FusionCacheMemoryEntry.cs index e3363f19..5d8907d7 100644 --- a/src/ZiggyCreatures.FusionCache/Internals/Memory/FusionCacheMemoryEntry.cs +++ b/src/ZiggyCreatures.FusionCache/Internals/Memory/FusionCacheMemoryEntry.cs @@ -1,4 +1,5 @@ using System; +using ZiggyCreatures.Caching.Fusion.Internals.Distributed; namespace ZiggyCreatures.Caching.Fusion.Internals.Memory; @@ -14,21 +15,27 @@ internal sealed class FusionCacheMemoryEntry /// <param name="value">The actual value.</param> /// <param name="metadata">The metadata for the entry.</param> /// <param name="timestamp">The original timestamp of the entry, see <see cref="Timestamp"/>.</param> - public FusionCacheMemoryEntry(object? value, FusionCacheEntryMetadata? metadata, long? timestamp) + /// <param name="valueType">The type of the value in the cache entry (mainly used for serialization/deserialization).</param> + public FusionCacheMemoryEntry(object? value, FusionCacheEntryMetadata? metadata, long timestamp, Type valueType) { Value = value; Metadata = metadata; Timestamp = timestamp; + ValueType = valueType; } /// <inheritdoc/> public object? Value { get; set; } + public Type ValueType { get; } + /// <inheritdoc/> - public FusionCacheEntryMetadata? Metadata { get; } + public FusionCacheEntryMetadata? Metadata { get; private set; } /// <inheritdoc/> - public long? Timestamp { get; } + public long Timestamp { get; private set; } + + public DateTimeOffset PhysicalExpiration { get; set; } /// <inheritdoc/> public TValue GetValue<TValue>() @@ -55,6 +62,13 @@ public override string ToString() return Metadata.ToString(); } + public void UpdateFromDistributedEntry<TValue2>(FusionCacheDistributedEntry<TValue2> distributedEntry) + { + Value = distributedEntry.GetValue<TValue2>(); + Timestamp = distributedEntry.Timestamp; + Metadata = distributedEntry.Metadata; + } + /// <summary> /// Creates a new <see cref="FusionCacheMemoryEntry"/> instance from a value and some options. /// </summary> @@ -64,14 +78,16 @@ public override string ToString() /// <param name="lastModified">If provided, it's the last modified date of the entry: this may be used in the next refresh cycle (eg: with the use of the "If-Modified-Since" header in an http request) to check if the entry is changed, to avoid getting the entire value.</param> /// <param name="etag">If provided, it's the ETag of the entry: this may be used in the next refresh cycle (eg: with the use of the "If-None-Match" header in an http request) to check if the entry is changed, to avoid getting the entire value.</param> /// <param name="timestamp">The value for the <see cref="Timestamp"/> property.</param> + /// <param name="valueType">The type of the value in the cache entry (mainly used for serialization/deserialization).</param> /// <returns>The newly created entry.</returns> - public static FusionCacheMemoryEntry CreateFromOptions(object? value, FusionCacheEntryOptions options, bool isFromFailSafe, DateTimeOffset? lastModified, string? etag, long? timestamp) + public static FusionCacheMemoryEntry CreateFromOptions(object? value, FusionCacheEntryOptions options, bool isFromFailSafe, DateTimeOffset? lastModified, string? etag, long? timestamp, Type valueType) { if (options.IsFailSafeEnabled == false && options.EagerRefreshThreshold.HasValue == false) return new FusionCacheMemoryEntry( value, null, - FusionCacheInternalUtils.GetCurrentTimestamp() + FusionCacheInternalUtils.GetCurrentTimestamp(), + valueType ); var exp = FusionCacheInternalUtils.GetNormalizedAbsoluteExpiration(isFromFailSafe ? options.FailSafeThrottleDuration : options.Duration, options, true); @@ -81,7 +97,8 @@ public static FusionCacheMemoryEntry CreateFromOptions(object? value, FusionCach return new FusionCacheMemoryEntry( value, new FusionCacheEntryMetadata(exp, isFromFailSafe, eagerExp, etag, lastModified), - timestamp ?? FusionCacheInternalUtils.GetCurrentTimestamp() + timestamp ?? FusionCacheInternalUtils.GetCurrentTimestamp(), + valueType ); } @@ -97,7 +114,8 @@ public static FusionCacheMemoryEntry CreateFromOtherEntry<TValue>(IFusionCacheEn return new FusionCacheMemoryEntry( entry.GetValue<TValue>(), null, - entry.Timestamp + entry.Timestamp, + typeof(TValue) ); var isFromFailSafe = entry.Metadata?.IsFromFailSafe ?? false; @@ -118,7 +136,8 @@ public static FusionCacheMemoryEntry CreateFromOtherEntry<TValue>(IFusionCacheEn return new FusionCacheMemoryEntry( entry.GetValue<TValue>(), new FusionCacheEntryMetadata(exp, isFromFailSafe, eagerExp, entry.Metadata?.ETag, entry.Metadata?.LastModified), - entry.Timestamp + entry.Timestamp, + typeof(TValue) ); } } diff --git a/src/ZiggyCreatures.FusionCache/Internals/Memory/MemoryCacheAccessor.cs b/src/ZiggyCreatures.FusionCache/Internals/Memory/MemoryCacheAccessor.cs index ca7382a9..d81012bf 100644 --- a/src/ZiggyCreatures.FusionCache/Internals/Memory/MemoryCacheAccessor.cs +++ b/src/ZiggyCreatures.FusionCache/Internals/Memory/MemoryCacheAccessor.cs @@ -42,7 +42,9 @@ public void SetEntry<TValue>(string operationId, string key, FusionCacheMemoryEn var memoryOptions = options.ToMemoryCacheEntryOptions(_events, _options, _logger, operationId, key); if (_logger?.IsEnabled(LogLevel.Debug) ?? false) - _logger.Log(LogLevel.Debug, "FUSION [N={CacheName}] (O={CacheOperationId} K={CacheKey}): saving entry in memory {Options} {Entry}", _options.CacheName, operationId, key, memoryOptions.ToLogString(), entry.ToLogString()); + _logger.Log(LogLevel.Debug, "FUSION [N={CacheName} I={CacheInstanceId}] (O={CacheOperationId} K={CacheKey}): [MC] saving entry in memory {Options} {Entry}", _options.CacheName, _options.InstanceId, operationId, key, memoryOptions.ToLogString(), entry.ToLogString()); + + entry.PhysicalExpiration = memoryOptions.AbsoluteExpiration!.Value; _cache.Set<FusionCacheMemoryEntry>(key, entry, memoryOptions); @@ -50,30 +52,35 @@ public void SetEntry<TValue>(string operationId, string key, FusionCacheMemoryEn _events.OnSet(operationId, key); } - public (FusionCacheMemoryEntry? entry, bool isValid) TryGetEntry<TValue>(string operationId, string key) + public FusionCacheMemoryEntry? GetEntryOrNull(string operationId, string key) + { + return _cache.Get<FusionCacheMemoryEntry?>(key); + } + + public (FusionCacheMemoryEntry? entry, bool isValid) TryGetEntry(string operationId, string key) { FusionCacheMemoryEntry? entry; bool isValid = false; if (_logger?.IsEnabled(LogLevel.Trace) ?? false) - _logger.Log(LogLevel.Trace, "FUSION [N={CacheName}] (O={CacheOperationId} K={CacheKey}): trying to get from memory", _options.CacheName, operationId, key); + _logger.Log(LogLevel.Trace, "FUSION [N={CacheName} I={CacheInstanceId}] (O={CacheOperationId} K={CacheKey}): [MC] trying to get from memory", _options.CacheName, _options.InstanceId, operationId, key); if (_cache.TryGetValue<FusionCacheMemoryEntry>(key, out entry) == false) { if (_logger?.IsEnabled(LogLevel.Debug) ?? false) - _logger.Log(LogLevel.Debug, "FUSION [N={CacheName}] (O={CacheOperationId} K={CacheKey}): memory entry not found", _options.CacheName, operationId, key); + _logger.Log(LogLevel.Debug, "FUSION [N={CacheName} I={CacheInstanceId}] (O={CacheOperationId} K={CacheKey}): [MC] memory entry not found", _options.CacheName, _options.InstanceId, operationId, key); } else { if (entry.IsLogicallyExpired()) { if (_logger?.IsEnabled(LogLevel.Debug) ?? false) - _logger.Log(LogLevel.Debug, "FUSION [N={CacheName}] (O={CacheOperationId} K={CacheKey}): memory entry found (expired) {Entry}", _options.CacheName, operationId, key, entry.ToLogString()); + _logger.Log(LogLevel.Debug, "FUSION [N={CacheName} I={CacheInstanceId}] (O={CacheOperationId} K={CacheKey}): [MC] memory entry found (expired) {Entry}", _options.CacheName, _options.InstanceId, operationId, key, entry.ToLogString()); } else { if (_logger?.IsEnabled(LogLevel.Debug) ?? false) - _logger.Log(LogLevel.Debug, "FUSION [N={CacheName}] (O={CacheOperationId} K={CacheKey}): memory entry found {Entry}", _options.CacheName, operationId, key, entry.ToLogString()); + _logger.Log(LogLevel.Debug, "FUSION [N={CacheName} I={CacheInstanceId}] (O={CacheOperationId} K={CacheKey}): [MC] memory entry found {Entry}", _options.CacheName, _options.InstanceId, operationId, key, entry.ToLogString()); isValid = true; } @@ -95,7 +102,7 @@ public void SetEntry<TValue>(string operationId, string key, FusionCacheMemoryEn public void RemoveEntry(string operationId, string key, FusionCacheEntryOptions options) { if (_logger?.IsEnabled(LogLevel.Debug) ?? false) - _logger.Log(LogLevel.Debug, "FUSION [N={CacheName}] (O={CacheOperationId} K={CacheKey}): removing data (from memory)", _options.CacheName, operationId, key); + _logger.Log(LogLevel.Debug, "FUSION [N={CacheName} I={CacheInstanceId}] (O={CacheOperationId} K={CacheKey}): [MC] removing data (from memory)", _options.CacheName, _options.InstanceId, operationId, key); _cache.Remove(key); @@ -103,19 +110,34 @@ public void RemoveEntry(string operationId, string key, FusionCacheEntryOptions _events.OnRemove(operationId, key); } - public void ExpireEntry(string operationId, string key, bool allowFailSafe) + public bool ExpireEntry(string operationId, string key, bool allowFailSafe, long? timestampThreshold) { - if (_logger?.IsEnabled(LogLevel.Debug) ?? false) - _logger.Log(LogLevel.Debug, "FUSION [N={CacheName}] (O={CacheOperationId} K={CacheKey}): expiring data (from memory)", _options.CacheName, operationId, key); - - if (_cache.TryGetValue<IFusionCacheEntry>(key, out var entry) == false) - return; + var entry = _cache.Get<FusionCacheMemoryEntry>(key); if (entry is null) - return; + { + if (_logger?.IsEnabled(LogLevel.Trace) ?? false) + _logger.Log(LogLevel.Trace, "FUSION [N={CacheName} I={CacheInstanceId}] (O={CacheOperationId} K={CacheKey}): [MC] memory entry not found: not necessary to expire", _options.CacheName, _options.InstanceId, operationId, key); + + return false; + } + + if (_logger?.IsEnabled(LogLevel.Trace) ?? false) + _logger.Log(LogLevel.Trace, "FUSION [N={CacheName} I={CacheInstanceId}] (O={CacheOperationId} K={CacheKey}): [MC] Metadata {Metadata}", _options.CacheName, _options.InstanceId, operationId, key, entry.Metadata); + + if (timestampThreshold is not null && entry.Timestamp >= timestampThreshold.Value) + { + if (_logger?.IsEnabled(LogLevel.Trace) ?? false) + _logger.Log(LogLevel.Trace, "FUSION [N={CacheName} I={CacheInstanceId}] (O={CacheOperationId} K={CacheKey}): [MC] timestamp of cached entry {TimestampCached} was greater than the specified threshold {TimestampThreshold}", _options.CacheName, _options.InstanceId, operationId, key, entry.Timestamp, timestampThreshold.Value); + + return false; + } if (allowFailSafe && entry.Metadata is not null && entry.Metadata.IsLogicallyExpired() == false) { + if (_logger?.IsEnabled(LogLevel.Debug) ?? false) + _logger.Log(LogLevel.Debug, "FUSION [N={CacheName} I={CacheInstanceId}] (O={CacheOperationId} K={CacheKey}): [MC] expiring data (from memory)", _options.CacheName, _options.InstanceId, operationId, key); + // MAKE THE ENTRY LOGICALLY EXPIRE entry.Metadata.LogicalExpiration = DateTimeOffset.UtcNow.AddMilliseconds(-10); @@ -124,12 +146,17 @@ public void ExpireEntry(string operationId, string key, bool allowFailSafe) } else { + 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); + // REMOVE THE ENTRY _cache.Remove(key); // EVENT _events.OnRemove(operationId, key); } + + return true; } // IDISPOSABLE diff --git a/src/ZiggyCreatures.FusionCache/Internals/Provider/FusionCacheProvider.cs b/src/ZiggyCreatures.FusionCache/Internals/Provider/FusionCacheProvider.cs index 5b2266fb..1fe5fc95 100644 --- a/src/ZiggyCreatures.FusionCache/Internals/Provider/FusionCacheProvider.cs +++ b/src/ZiggyCreatures.FusionCache/Internals/Provider/FusionCacheProvider.cs @@ -2,48 +2,47 @@ using System.Collections.Generic; using System.Linq; -namespace ZiggyCreatures.Caching.Fusion.Internals.Provider +namespace ZiggyCreatures.Caching.Fusion.Internals.Provider; + +internal sealed class FusionCacheProvider + : IFusionCacheProvider { - internal class FusionCacheProvider - : IFusionCacheProvider - { - private readonly IFusionCache? _defaultCache; - private readonly LazyNamedCache[] _lazyNamedCaches; + private readonly IFusionCache? _defaultCache; + private readonly LazyNamedCache[] _lazyNamedCaches; - public FusionCacheProvider(IEnumerable<IFusionCache> defaultCaches, IEnumerable<LazyNamedCache> lazyNamedCaches) - { - _defaultCache = defaultCaches.LastOrDefault(); - _lazyNamedCaches = lazyNamedCaches.ToArray(); - } + public FusionCacheProvider(IEnumerable<IFusionCache> defaultCaches, IEnumerable<LazyNamedCache> lazyNamedCaches) + { + _defaultCache = defaultCaches.LastOrDefault(); + _lazyNamedCaches = lazyNamedCaches.ToArray(); + } - public IFusionCache? GetCacheOrNull(string cacheName) - { - if (cacheName == FusionCacheOptions.DefaultCacheName) - return _defaultCache; + public IFusionCache? GetCacheOrNull(string cacheName) + { + if (cacheName == FusionCacheOptions.DefaultCacheName) + return _defaultCache; - var matchingLazyNamedCaches = _lazyNamedCaches.Where(x => x.CacheName == cacheName).ToArray(); + var matchingLazyNamedCaches = _lazyNamedCaches.Where(x => x.CacheName == cacheName).ToArray(); - if (matchingLazyNamedCaches.Length == 1) - return matchingLazyNamedCaches[0].Cache; + if (matchingLazyNamedCaches.Length == 1) + return matchingLazyNamedCaches[0].Cache; - if (matchingLazyNamedCaches.Length > 1) - throw new InvalidOperationException($"Multiple FusionCache registrations have been found with the provided name ({cacheName})"); + if (matchingLazyNamedCaches.Length > 1) + throw new InvalidOperationException($"Multiple FusionCache registrations have been found with the provided name ({cacheName})"); - return null; - } + return null; + } - public IFusionCache GetCache(string cacheName) - { - var maybeCache = GetCacheOrNull(cacheName); + public IFusionCache GetCache(string cacheName) + { + var maybeCache = GetCacheOrNull(cacheName); - if (maybeCache is not null) - return maybeCache; + if (maybeCache is not null) + return maybeCache; - throw new InvalidOperationException( - cacheName == FusionCacheOptions.DefaultCacheName - ? "No default cache has been registered" - : $"No cache has been registered with name ({cacheName})" - ); - } + throw new InvalidOperationException( + cacheName == FusionCacheOptions.DefaultCacheName + ? "No default cache has been registered" + : $"No cache has been registered with name ({cacheName})" + ); } } diff --git a/src/ZiggyCreatures.FusionCache/Internals/Provider/LazyNamedCache.cs b/src/ZiggyCreatures.FusionCache/Internals/Provider/LazyNamedCache.cs index efdd9bb2..82f98a6c 100644 --- a/src/ZiggyCreatures.FusionCache/Internals/Provider/LazyNamedCache.cs +++ b/src/ZiggyCreatures.FusionCache/Internals/Provider/LazyNamedCache.cs @@ -1,68 +1,67 @@ using System; using System.Diagnostics; -namespace ZiggyCreatures.Caching.Fusion.Internals.Provider +namespace ZiggyCreatures.Caching.Fusion.Internals.Provider; + +[DebuggerDisplay($"{{{nameof(GetDebuggerDisplay)}(),nq}}")] +internal sealed class LazyNamedCache : IDisposable { - [DebuggerDisplay($"{{{nameof(GetDebuggerDisplay)}(),nq}}")] - internal class LazyNamedCache : IDisposable + private string GetDebuggerDisplay() { - private string GetDebuggerDisplay() - { - return $"CACHE: {CacheName} - INSTANTIATED: {_cache is not null}"; - } + return $"CACHE: {CacheName} - INSTANTIATED: {_cache is not null}"; + } - public LazyNamedCache(string name, Func<IFusionCache> cacheFactory) - { - if (name is null) - throw new ArgumentNullException(nameof(name)); + public LazyNamedCache(string name, Func<IFusionCache> cacheFactory) + { + if (name is null) + throw new ArgumentNullException(nameof(name)); - if (cacheFactory is null) - throw new ArgumentNullException(nameof(cacheFactory)); + if (cacheFactory is null) + throw new ArgumentNullException(nameof(cacheFactory)); - CacheName = name; - _cacheFactory = cacheFactory; - } + CacheName = name; + _cacheFactory = cacheFactory; + } - public LazyNamedCache(string name, IFusionCache cache) - { - if (name is null) - throw new ArgumentNullException(nameof(name)); + public LazyNamedCache(string name, IFusionCache cache) + { + if (name is null) + throw new ArgumentNullException(nameof(name)); - if (cache is null) - throw new ArgumentNullException(nameof(cache)); + if (cache is null) + throw new ArgumentNullException(nameof(cache)); - CacheName = name; - _cache = cache; - } + CacheName = name; + _cache = cache; + } - private readonly object _mutex = new object(); - private IFusionCache? _cache; - private readonly Func<IFusionCache>? _cacheFactory; + private readonly object _mutex = new object(); + private IFusionCache? _cache; + private readonly Func<IFusionCache>? _cacheFactory; - public string CacheName { get; } + public string CacheName { get; } - public IFusionCache Cache + public IFusionCache Cache + { + get { - get - { - if (_cache is not null) - return _cache; + if (_cache is not null) + return _cache; - if (_cacheFactory is not null) + if (_cacheFactory is not null) + { + lock (_mutex) { - lock (_mutex) - { - return _cache = _cacheFactory(); - } + return _cache = _cacheFactory(); } - - throw new InvalidOperationException("This should not be possible"); } - } - public void Dispose() - { - _cache?.Dispose(); + throw new InvalidOperationException("This should not be possible"); } } + + public void Dispose() + { + _cache?.Dispose(); + } } diff --git a/src/ZiggyCreatures.FusionCache/Internals/FusionCacheExecutionUtils.cs b/src/ZiggyCreatures.FusionCache/Internals/RunUtils.cs similarity index 95% rename from src/ZiggyCreatures.FusionCache/Internals/FusionCacheExecutionUtils.cs rename to src/ZiggyCreatures.FusionCache/Internals/RunUtils.cs index 2298952e..1c59a025 100644 --- a/src/ZiggyCreatures.FusionCache/Internals/FusionCacheExecutionUtils.cs +++ b/src/ZiggyCreatures.FusionCache/Internals/RunUtils.cs @@ -7,7 +7,7 @@ namespace ZiggyCreatures.Caching.Fusion.Internals; /// <summary> /// A set of utility methods to deal with sync/async execution of actions/functions, with support for timeouts, fire-and-forget execution, etc. /// </summary> -public static class FusionCacheExecutionUtils +internal static class RunUtils { private static readonly TaskFactory _taskFactory = new TaskFactory( CancellationToken.None, @@ -247,19 +247,25 @@ public static TResult RunSyncFuncWithTimeout<TResult>(Func<CancellationToken, TR throw exc; } - Task<TResult> task; - if (timeout == Timeout.InfiniteTimeSpan && token == CancellationToken.None) { - //task = _taskFactory.StartNew(() => syncFunc(token), token); return syncFunc(token); } - else - { - task = RunAsyncFuncWithTimeoutAsync(ct => _taskFactory.StartNew(() => syncFunc(ct), ct), timeout, cancelIfTimeout, timedOutTaskProcessor, token); - } - return task.GetAwaiter().GetResult(); + return + RunAsyncFuncWithTimeoutAsync( + ct => _taskFactory.StartNew( + () => syncFunc(ct), + ct + ), + timeout, + cancelIfTimeout, + timedOutTaskProcessor, + token + ) + .GetAwaiter() + .GetResult() + ; } /// <summary> @@ -282,20 +288,24 @@ public static void RunSyncActionWithTimeout(Action<CancellationToken> syncAction throw exc; } - Task task; - if (timeout == Timeout.InfiniteTimeSpan && token == CancellationToken.None) { - //task = _taskFactory.StartNew(() => syncAction(token), token); syncAction(token); return; } - else - { - task = RunAsyncActionWithTimeoutAsync(ct => _taskFactory.StartNew(() => syncAction(ct), ct), timeout, cancelIfTimeout, timedOutTaskProcessor, token); - } - task.GetAwaiter().GetResult(); + RunAsyncActionWithTimeoutAsync( + ct => _taskFactory.StartNew( + () => syncAction(ct), + ct + ), + timeout, + cancelIfTimeout, + timedOutTaskProcessor, + token + ) + .GetAwaiter() + .GetResult(); } /// <summary> diff --git a/src/ZiggyCreatures.FusionCache/Internals/SimpleCircuitBreaker.cs b/src/ZiggyCreatures.FusionCache/Internals/SimpleCircuitBreaker.cs index 401ca911..ffd5ab07 100644 --- a/src/ZiggyCreatures.FusionCache/Internals/SimpleCircuitBreaker.cs +++ b/src/ZiggyCreatures.FusionCache/Internals/SimpleCircuitBreaker.cs @@ -6,7 +6,7 @@ namespace ZiggyCreatures.Caching.Fusion.Internals; /// <summary> /// A simple, reusable circuit-breaker. /// </summary> -public class SimpleCircuitBreaker +public sealed class SimpleCircuitBreaker { private const int CircuitStateClosed = 0; private const int CircuitStateOpen = 1; diff --git a/src/ZiggyCreatures.FusionCache/NullObjects/NullBackplane.cs b/src/ZiggyCreatures.FusionCache/NullObjects/NullBackplane.cs index edcddd11..c9d8e647 100644 --- a/src/ZiggyCreatures.FusionCache/NullObjects/NullBackplane.cs +++ b/src/ZiggyCreatures.FusionCache/NullObjects/NullBackplane.cs @@ -2,36 +2,35 @@ using System.Threading.Tasks; using ZiggyCreatures.Caching.Fusion.Backplane; -namespace ZiggyCreatures.Caching.Fusion.NullObjects +namespace ZiggyCreatures.Caching.Fusion.NullObjects; + +/// <summary> +/// An implementation of <see cref="IFusionCacheBackplane"/> that implements the null object pattern, meaning that it does nothing. Consider this a kind of a pass-through implementation. +/// </summary> +public class NullBackplane + : IFusionCacheBackplane { - /// <summary> - /// An implementation of <see cref="IFusionCacheBackplane"/> that implements the null object pattern, meaning that it does nothing. Consider this a kind of a pass-through implementation. - /// </summary> - public class NullBackplane - : IFusionCacheBackplane + /// <inheritdoc/> + public void Publish(BackplaneMessage message, FusionCacheEntryOptions options, CancellationToken token = default) { - /// <inheritdoc/> - public void Publish(BackplaneMessage message, FusionCacheEntryOptions options, CancellationToken token = default) - { - // EMPTY - } + // EMPTY + } - /// <inheritdoc/> - public ValueTask PublishAsync(BackplaneMessage message, FusionCacheEntryOptions options, CancellationToken token = default) - { - return new ValueTask(); - } + /// <inheritdoc/> + public ValueTask PublishAsync(BackplaneMessage message, FusionCacheEntryOptions options, CancellationToken token = default) + { + return new ValueTask(); + } - /// <inheritdoc/> - public void Subscribe(BackplaneSubscriptionOptions options) - { - // EMPTY - } + /// <inheritdoc/> + public void Subscribe(BackplaneSubscriptionOptions options) + { + // EMPTY + } - /// <inheritdoc/> - public void Unsubscribe() - { - // EMPTY - } + /// <inheritdoc/> + public void Unsubscribe() + { + // EMPTY } } diff --git a/src/ZiggyCreatures.FusionCache/NullObjects/NullDistributedCache.cs b/src/ZiggyCreatures.FusionCache/NullObjects/NullDistributedCache.cs index 94a0f0b2..b506c209 100644 --- a/src/ZiggyCreatures.FusionCache/NullObjects/NullDistributedCache.cs +++ b/src/ZiggyCreatures.FusionCache/NullObjects/NullDistributedCache.cs @@ -2,60 +2,59 @@ using System.Threading.Tasks; using Microsoft.Extensions.Caching.Distributed; -namespace ZiggyCreatures.Caching.Fusion.NullObjects +namespace ZiggyCreatures.Caching.Fusion.NullObjects; + +/// <summary> +/// An implementation of <see cref="IDistributedCache"/> that implements the null object pattern, meaning that it does nothing. Consider this a kind of a pass-through implementation. +/// </summary> +public class NullDistributedCache + : IDistributedCache { - /// <summary> - /// An implementation of <see cref="IDistributedCache"/> that implements the null object pattern, meaning that it does nothing. Consider this a kind of a pass-through implementation. - /// </summary> - public class NullDistributedCache - : IDistributedCache - { - /// <inheritdoc/> - public byte[] Get(string key) - { - return null; - } - - /// <inheritdoc/> - public Task<byte[]> GetAsync(string key, CancellationToken token = default) - { - return Task.FromResult<byte[]>(null); - } - - /// <inheritdoc/> - public void Refresh(string key) - { - // EMPTY - } - - /// <inheritdoc/> - public Task RefreshAsync(string key, CancellationToken token = default) - { - return Task.CompletedTask; - } - - /// <inheritdoc/> - public void Remove(string key) - { - // EMPTY - } - - /// <inheritdoc/> - public Task RemoveAsync(string key, CancellationToken token = default) - { - return Task.CompletedTask; - } - - /// <inheritdoc/> - public void Set(string key, byte[] value, DistributedCacheEntryOptions options) - { - // EMPTY - } - - /// <inheritdoc/> - public Task SetAsync(string key, byte[] value, DistributedCacheEntryOptions options, CancellationToken token = default) - { - return Task.CompletedTask; - } + /// <inheritdoc/> + public byte[] Get(string key) + { + return null!; + } + + /// <inheritdoc/> + public Task<byte[]> GetAsync(string key, CancellationToken token = default) + { + return Task.FromResult<byte[]>(null!); + } + + /// <inheritdoc/> + public void Refresh(string key) + { + // EMPTY + } + + /// <inheritdoc/> + public Task RefreshAsync(string key, CancellationToken token = default) + { + return Task.CompletedTask; + } + + /// <inheritdoc/> + public void Remove(string key) + { + // EMPTY + } + + /// <inheritdoc/> + public Task RemoveAsync(string key, CancellationToken token = default) + { + return Task.CompletedTask; + } + + /// <inheritdoc/> + public void Set(string key, byte[] value, DistributedCacheEntryOptions options) + { + // EMPTY + } + + /// <inheritdoc/> + public Task SetAsync(string key, byte[] value, DistributedCacheEntryOptions options, CancellationToken token = default) + { + return Task.CompletedTask; } } diff --git a/src/ZiggyCreatures.FusionCache/NullObjects/NullFusionCache.cs b/src/ZiggyCreatures.FusionCache/NullObjects/NullFusionCache.cs index 143c98c6..f9b3c7dd 100644 --- a/src/ZiggyCreatures.FusionCache/NullObjects/NullFusionCache.cs +++ b/src/ZiggyCreatures.FusionCache/NullObjects/NullFusionCache.cs @@ -9,200 +9,201 @@ using ZiggyCreatures.Caching.Fusion.Plugins; using ZiggyCreatures.Caching.Fusion.Serialization; -namespace ZiggyCreatures.Caching.Fusion.NullObjects +namespace ZiggyCreatures.Caching.Fusion.NullObjects; + +/// <summary> +/// An implementation of <see cref="IFusionCache"/> that implements the null object pattern, meaning that it does nothing. Consider this a kind of a pass-through implementation. +/// </summary> +[DebuggerDisplay("NAME: {_options.CacheName} - ID: {InstanceId}")] +public class NullFusionCache +: IFusionCache { + private readonly FusionCacheOptions _options; + private readonly FusionCacheEventsHub _events; + /// <summary> - /// An implementation of <see cref="IFusionCache"/> that implements the null object pattern, meaning that it does nothing. Consider this a kind of a pass-through implementation. + /// Creates a new <see cref="NullFusionCache"/> instance. /// </summary> - [DebuggerDisplay("NAME: {_options.CacheName} - ID: {InstanceId}")] - public class NullFusionCache - : IFusionCache - { - private readonly FusionCacheOptions _options; - private readonly FusionCacheEventsHub _events; - - /// <summary> - /// Creates a new <see cref="NullFusionCache"/> instance. - /// </summary> - /// <param name="optionsAccessor">The set of cache-wide options to use with this instance of <see cref="FusionCache"/>.</param> - public NullFusionCache(IOptions<FusionCacheOptions> optionsAccessor) - { - // GLOBALLY UNIQUE INSTANCE ID - InstanceId = Guid.NewGuid().ToString("N"); - - if (optionsAccessor is null) - throw new ArgumentNullException(nameof(optionsAccessor)); - - // OPTIONS - _options = optionsAccessor.Value ?? throw new ArgumentNullException(nameof(optionsAccessor.Value)); - - // EVENTS - _events = new FusionCacheEventsHub(this, _options, null); - } - - /// <inheritdoc/> - public string CacheName - { - get { return _options.CacheName; } - } - - /// <inheritdoc/> - public string InstanceId { get; } - - /// <inheritdoc/> - public FusionCacheEntryOptions DefaultEntryOptions - { - get { return _options.DefaultEntryOptions; } - } - - /// <inheritdoc/> - public bool HasDistributedCache - { - get { return false; } - } - - /// <inheritdoc/> - public bool HasBackplane - { - get { return false; } - } - - /// <inheritdoc/> - public FusionCacheEventsHub Events - { - get { return _events; } - } - - /// <inheritdoc/> - public void AddPlugin(IFusionCachePlugin plugin) - { - // EMPTY - } - - /// <inheritdoc/> - public FusionCacheEntryOptions CreateEntryOptions(Action<FusionCacheEntryOptions>? setupAction = null, TimeSpan? duration = null) - { - throw new NotImplementedException(); - } - - /// <inheritdoc/> - public void Dispose() - { - // EMTPY - } - - /// <inheritdoc/> - public void Expire(string key, FusionCacheEntryOptions? options = null, CancellationToken token = default) - { - // EMPTY - } - - /// <inheritdoc/> - public ValueTask ExpireAsync(string key, FusionCacheEntryOptions? options = null, CancellationToken token = default) - { - return new ValueTask(); - } - - /// <inheritdoc/> - public TValue? GetOrDefault<TValue>(string key, TValue? defaultValue = default, FusionCacheEntryOptions? options = null, CancellationToken token = default) - { - return defaultValue; - } - - /// <inheritdoc/> - public ValueTask<TValue?> GetOrDefaultAsync<TValue>(string key, TValue? defaultValue = default, FusionCacheEntryOptions? options = null, CancellationToken token = default) - { - return new ValueTask<TValue?>(defaultValue); - } - - /// <inheritdoc/> - public TValue? GetOrSet<TValue>(string key, Func<FusionCacheFactoryExecutionContext<TValue>, CancellationToken, TValue?> factory, MaybeValue<TValue?> failSafeDefaultValue = default, FusionCacheEntryOptions? options = null, CancellationToken token = default) - { - return factory(new FusionCacheFactoryExecutionContext<TValue>(options ?? DefaultEntryOptions, default, null, null), token); - } - - /// <inheritdoc/> - public TValue? GetOrSet<TValue>(string key, TValue? defaultValue, FusionCacheEntryOptions? options = null, CancellationToken token = default) - { - return defaultValue; - } - - /// <inheritdoc/> - public async ValueTask<TValue?> GetOrSetAsync<TValue>(string key, Func<FusionCacheFactoryExecutionContext<TValue>, CancellationToken, Task<TValue?>> factory, MaybeValue<TValue?> failSafeDefaultValue = default, FusionCacheEntryOptions? options = null, CancellationToken token = default) - { - return await factory(new FusionCacheFactoryExecutionContext<TValue>(options ?? DefaultEntryOptions, default, null, null), token); - } - - /// <inheritdoc/> - public ValueTask<TValue?> GetOrSetAsync<TValue>(string key, TValue? defaultValue, FusionCacheEntryOptions? options = null, CancellationToken token = default) - { - return new ValueTask<TValue?>(defaultValue); - } - - /// <inheritdoc/> - public void Remove(string key, FusionCacheEntryOptions? options = null, CancellationToken token = default) - { - // EMPTY - } - - /// <inheritdoc/> - public ValueTask RemoveAsync(string key, FusionCacheEntryOptions? options = null, CancellationToken token = default) - { - return new ValueTask(); - } - - /// <inheritdoc/> - public IFusionCache RemoveBackplane() - { - return this; - } - - /// <inheritdoc/> - public IFusionCache RemoveDistributedCache() - { - return this; - } - - /// <inheritdoc/> - public bool RemovePlugin(IFusionCachePlugin plugin) - { - return false; - } - - /// <inheritdoc/> - public void Set<TValue>(string key, TValue value, FusionCacheEntryOptions? options = null, CancellationToken token = default) - { - // EMPTY - } - - /// <inheritdoc/> - public ValueTask SetAsync<TValue>(string key, TValue value, FusionCacheEntryOptions? options = null, CancellationToken token = default) - { - return new ValueTask(); - } - - /// <inheritdoc/> - public IFusionCache SetupBackplane(IFusionCacheBackplane backplane) - { - return this; - } - - /// <inheritdoc/> - public IFusionCache SetupDistributedCache(IDistributedCache distributedCache, IFusionCacheSerializer serializer) - { - return this; - } - - /// <inheritdoc/> - public MaybeValue<TValue> TryGet<TValue>(string key, FusionCacheEntryOptions? options = null, CancellationToken token = default) - { - return default; - } - - /// <inheritdoc/> - public ValueTask<MaybeValue<TValue>> TryGetAsync<TValue>(string key, FusionCacheEntryOptions? options = null, CancellationToken token = default) - { - return new ValueTask<MaybeValue<TValue>>(); - } + /// <param name="optionsAccessor">The set of cache-wide options to use with this instance of <see cref="FusionCache"/>.</param> + public NullFusionCache(IOptions<FusionCacheOptions> optionsAccessor) + { + if (optionsAccessor is null) + throw new ArgumentNullException(nameof(optionsAccessor)); + + // OPTIONS + _options = optionsAccessor.Value ?? throw new ArgumentNullException(nameof(optionsAccessor.Value)); + + // GLOBALLY UNIQUE INSTANCE ID + InstanceId = _options.InstanceId ?? Guid.NewGuid().ToString("N"); + + // EVENTS + _events = new FusionCacheEventsHub(this, _options, null); + } + + /// <inheritdoc/> + public string CacheName + { + get { return _options.CacheName; } + } + + /// <inheritdoc/> + public string InstanceId { get; } + + /// <inheritdoc/> + public FusionCacheEntryOptions DefaultEntryOptions + { + get { return _options.DefaultEntryOptions; } + } + + /// <inheritdoc/> + public bool HasDistributedCache + { + get { return false; } + } + + /// <inheritdoc/> + public bool HasBackplane + { + get { return false; } + } + + /// <inheritdoc/> + public FusionCacheEventsHub Events + { + get { return _events; } + } + + /// <inheritdoc/> + public void AddPlugin(IFusionCachePlugin plugin) + { + // EMPTY + } + + /// <inheritdoc/> + public FusionCacheEntryOptions CreateEntryOptions(Action<FusionCacheEntryOptions>? setupAction = null, TimeSpan? duration = null) + { + var res = _options.DefaultEntryOptions.Duplicate(duration); + setupAction?.Invoke(res); + return res; + } + + /// <inheritdoc/> + public void Dispose() + { + // EMTPY + } + + /// <inheritdoc/> + public void Expire(string key, FusionCacheEntryOptions? options = null, CancellationToken token = default) + { + // EMPTY + } + + /// <inheritdoc/> + public ValueTask ExpireAsync(string key, FusionCacheEntryOptions? options = null, CancellationToken token = default) + { + return new ValueTask(); + } + + /// <inheritdoc/> + public TValue? GetOrDefault<TValue>(string key, TValue? defaultValue = default, FusionCacheEntryOptions? options = null, CancellationToken token = default) + { + return defaultValue; + } + + /// <inheritdoc/> + public ValueTask<TValue?> GetOrDefaultAsync<TValue>(string key, TValue? defaultValue = default, FusionCacheEntryOptions? options = null, CancellationToken token = default) + { + return new ValueTask<TValue?>(defaultValue); + } + + /// <inheritdoc/> + public TValue? GetOrSet<TValue>(string key, Func<FusionCacheFactoryExecutionContext<TValue>, CancellationToken, TValue?> factory, MaybeValue<TValue?> failSafeDefaultValue = default, FusionCacheEntryOptions? options = null, CancellationToken token = default) + { + return factory(new FusionCacheFactoryExecutionContext<TValue>(options ?? DefaultEntryOptions, default, null, null), token); + } + + /// <inheritdoc/> + public TValue? GetOrSet<TValue>(string key, TValue? defaultValue, FusionCacheEntryOptions? options = null, CancellationToken token = default) + { + return defaultValue; + } + + /// <inheritdoc/> + public async ValueTask<TValue?> GetOrSetAsync<TValue>(string key, Func<FusionCacheFactoryExecutionContext<TValue>, CancellationToken, Task<TValue?>> factory, MaybeValue<TValue?> failSafeDefaultValue = default, FusionCacheEntryOptions? options = null, CancellationToken token = default) + { + return await factory(new FusionCacheFactoryExecutionContext<TValue>(options ?? DefaultEntryOptions, default, null, null), token); + } + + /// <inheritdoc/> + public ValueTask<TValue?> GetOrSetAsync<TValue>(string key, TValue? defaultValue, FusionCacheEntryOptions? options = null, CancellationToken token = default) + { + return new ValueTask<TValue?>(defaultValue); + } + + /// <inheritdoc/> + public void Remove(string key, FusionCacheEntryOptions? options = null, CancellationToken token = default) + { + // EMPTY + } + + /// <inheritdoc/> + public ValueTask RemoveAsync(string key, FusionCacheEntryOptions? options = null, CancellationToken token = default) + { + return new ValueTask(); + } + + /// <inheritdoc/> + public IFusionCache RemoveBackplane() + { + return this; + } + + /// <inheritdoc/> + public IFusionCache RemoveDistributedCache() + { + return this; + } + + /// <inheritdoc/> + public bool RemovePlugin(IFusionCachePlugin plugin) + { + return false; + } + + /// <inheritdoc/> + public void Set<TValue>(string key, TValue value, FusionCacheEntryOptions? options = null, CancellationToken token = default) + { + // EMPTY + } + + /// <inheritdoc/> + public ValueTask SetAsync<TValue>(string key, TValue value, FusionCacheEntryOptions? options = null, CancellationToken token = default) + { + return new ValueTask(); + } + + /// <inheritdoc/> + public IFusionCache SetupBackplane(IFusionCacheBackplane backplane) + { + return this; + } + + /// <inheritdoc/> + public IFusionCache SetupDistributedCache(IDistributedCache distributedCache, IFusionCacheSerializer serializer) + { + return this; + } + + /// <inheritdoc/> + public MaybeValue<TValue> TryGet<TValue>(string key, FusionCacheEntryOptions? options = null, CancellationToken token = default) + { + return default; + } + + /// <inheritdoc/> + public ValueTask<MaybeValue<TValue>> TryGetAsync<TValue>(string key, FusionCacheEntryOptions? options = null, CancellationToken token = default) + { + return new ValueTask<MaybeValue<TValue>>(); } } diff --git a/src/ZiggyCreatures.FusionCache/NullObjects/NullPlugin.cs b/src/ZiggyCreatures.FusionCache/NullObjects/NullPlugin.cs index e024f521..20f9db9c 100644 --- a/src/ZiggyCreatures.FusionCache/NullObjects/NullPlugin.cs +++ b/src/ZiggyCreatures.FusionCache/NullObjects/NullPlugin.cs @@ -1,23 +1,22 @@ using ZiggyCreatures.Caching.Fusion.Plugins; -namespace ZiggyCreatures.Caching.Fusion.NullObjects +namespace ZiggyCreatures.Caching.Fusion.NullObjects; + +/// <summary> +/// An implementation of <see cref="IFusionCachePlugin"/> that implements the null object pattern, meaning that it does nothing. +/// </summary> +public class NullPlugin + : IFusionCachePlugin { - /// <summary> - /// An implementation of <see cref="IFusionCachePlugin"/> that implements the null object pattern, meaning that it does nothing. - /// </summary> - public class NullPlugin - : IFusionCachePlugin + /// <inheritdoc/> + public void Start(IFusionCache cache) { - /// <inheritdoc/> - public void Start(IFusionCache cache) - { - // EMPTY - } + // EMPTY + } - /// <inheritdoc/> - public void Stop(IFusionCache cache) - { - // EMPTY - } + /// <inheritdoc/> + public void Stop(IFusionCache cache) + { + // EMPTY } } diff --git a/src/ZiggyCreatures.FusionCache/NullObjects/NullSerializer.cs b/src/ZiggyCreatures.FusionCache/NullObjects/NullSerializer.cs index 79bd025f..deb227d7 100644 --- a/src/ZiggyCreatures.FusionCache/NullObjects/NullSerializer.cs +++ b/src/ZiggyCreatures.FusionCache/NullObjects/NullSerializer.cs @@ -2,36 +2,35 @@ using System.Threading.Tasks; using ZiggyCreatures.Caching.Fusion.Serialization; -namespace ZiggyCreatures.Caching.Fusion.NullObjects +namespace ZiggyCreatures.Caching.Fusion.NullObjects; + +/// <summary> +/// An implementation of <see cref="IFusionCacheSerializer"/> that implements the null object pattern, meaning that it does nothing. +/// </summary> +public class NullSerializer + : IFusionCacheSerializer { - /// <summary> - /// An implementation of <see cref="IFusionCacheSerializer"/> that implements the null object pattern, meaning that it does nothing. - /// </summary> - public class NullSerializer - : IFusionCacheSerializer + /// <inheritdoc/> + public T? Deserialize<T>(byte[] data) { - /// <inheritdoc/> - public T? Deserialize<T>(byte[] data) - { - return default(T?); - } + return default(T?); + } - /// <inheritdoc/> - public ValueTask<T?> DeserializeAsync<T>(byte[] data) - { - return new ValueTask<T?>(default(T?)); - } + /// <inheritdoc/> + public ValueTask<T?> DeserializeAsync<T>(byte[] data) + { + return new ValueTask<T?>(default(T?)); + } - /// <inheritdoc/> - public byte[] Serialize<T>(T? obj) - { - return Array.Empty<byte>(); - } + /// <inheritdoc/> + public byte[] Serialize<T>(T? obj) + { + return Array.Empty<byte>(); + } - /// <inheritdoc/> - public ValueTask<byte[]> SerializeAsync<T>(T? obj) - { - return new ValueTask<byte[]>(Array.Empty<byte>()); - } + /// <inheritdoc/> + public ValueTask<byte[]> SerializeAsync<T>(T? obj) + { + return new ValueTask<byte[]>(Array.Empty<byte>()); } } diff --git a/src/ZiggyCreatures.FusionCache/Reactors/FusionCacheReactorProbabilistic.cs b/src/ZiggyCreatures.FusionCache/Reactors/FusionCacheReactorProbabilistic.cs index 5b7dc7bd..aa86526a 100644 --- a/src/ZiggyCreatures.FusionCache/Reactors/FusionCacheReactorProbabilistic.cs +++ b/src/ZiggyCreatures.FusionCache/Reactors/FusionCacheReactorProbabilistic.cs @@ -38,7 +38,7 @@ private uint GetLockIndex(string key) } // ACQUIRE LOCK ASYNC - public async ValueTask<object?> AcquireLockAsync(string cacheName, string key, string operationId, TimeSpan timeout, ILogger? logger, CancellationToken token) + public async ValueTask<object?> AcquireLockAsync(string cacheName, string cacheInstanceId, string key, string operationId, TimeSpan timeout, ILogger? logger, CancellationToken token) { token.ThrowIfCancellationRequested(); @@ -46,89 +46,89 @@ private uint GetLockIndex(string key) var semaphore = _lockPool[idx]; if (logger?.IsEnabled(LogLevel.Trace) ?? false) - logger.Log(LogLevel.Trace, "FUSION [N={CacheName}] (O={CacheOperationId} K={CacheKey}): trying to fast-acquire the LOCK", cacheName, operationId, key); + 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}] (O={CacheOperationId} K={CacheKey}): LOCK fast-acquired", cacheName, operationId, key); + 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}] (O={CacheOperationId} K={CacheKey}): LOCK already taken", cacheName, operationId, key); + 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}] (O={CacheOperationId} K={CacheKey}): LOCK " + (key2 is null ? "maybe " : string.Empty) + "acquired for a different key (current key: " + key + ", other key: " + key2 + ")", cacheName, operationId, key); + 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}] (O={CacheOperationId} K={CacheKey}): waiting to acquire the LOCK", cacheName, operationId, key); + 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}] (O={CacheOperationId} K={CacheKey}): LOCK acquired", cacheName, operationId, key); + 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 key, string operationId, TimeSpan timeout, ILogger? logger) + 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}] (O={CacheOperationId} K={CacheKey}): trying to fast-acquire the LOCK", cacheName, operationId, key); + 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}] (O={CacheOperationId} K={CacheKey}): LOCK fast-acquired", cacheName, operationId, key); + 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}] (O={CacheOperationId} K={CacheKey}): LOCK already taken", cacheName, operationId, key); + 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}] (O={CacheOperationId} K={CacheKey}): LOCK " + (key2 is null ? "maybe " : string.Empty) + "acquired for a different key (current key: " + key + ", other key: " + key2 + ")", cacheName, operationId, key); + 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}] (O={CacheOperationId} K={CacheKey}): waiting to acquire the LOCK", cacheName, operationId, key); + 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}] (O={CacheOperationId} K={CacheKey}): LOCK acquired", cacheName, operationId, key); + 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 key, string operationId, object? lockObj, ILogger? logger) + public void ReleaseLock(string cacheName, string cacheInstanceId, string key, string operationId, object? lockObj, ILogger? logger) { if (lockObj is null) return; @@ -143,7 +143,7 @@ public void ReleaseLock(string cacheName, string key, string operationId, object catch (Exception exc) { if (logger?.IsEnabled(LogLevel.Warning) ?? false) - logger.Log(LogLevel.Warning, exc, "FUSION [N={CacheName}] (O={CacheOperationId} K={CacheKey}): an error occurred while trying to release a SemaphoreSlim in the reactor", cacheName, operationId, key); + 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); } } diff --git a/src/ZiggyCreatures.FusionCache/Reactors/FusionCacheReactorStandard.cs b/src/ZiggyCreatures.FusionCache/Reactors/FusionCacheReactorStandard.cs index c5579a16..843bba3f 100644 --- a/src/ZiggyCreatures.FusionCache/Reactors/FusionCacheReactorStandard.cs +++ b/src/ZiggyCreatures.FusionCache/Reactors/FusionCacheReactorStandard.cs @@ -40,7 +40,7 @@ private uint GetLockIndex(string key) return unchecked((uint)key.GetHashCode()) % (uint)_lockPoolSize; } - private SemaphoreSlim GetSemaphore(string cacheName, string key, string operationId, ILogger? logger) + private SemaphoreSlim GetSemaphore(string cacheName, string cacheInstanceId, string key, ILogger? logger) { object _semaphore; @@ -57,7 +57,7 @@ private SemaphoreSlim GetSemaphore(string cacheName, string key, string operatio using ICacheEntry entry = _lockCache.CreateEntry(key); entry.Value = _semaphore; entry.SlidingExpiration = _slidingExpiration; - entry.RegisterPostEvictionCallback((key, value, reason, state) => + entry.RegisterPostEvictionCallback((key, value, _, _) => { try { @@ -66,7 +66,7 @@ private SemaphoreSlim GetSemaphore(string cacheName, string key, string operatio catch (Exception exc) { if (logger?.IsEnabled(LogLevel.Warning) ?? false) - logger.Log(LogLevel.Warning, exc, "FUSION [N={CacheName}] (K={CacheKey}): an error occurred while trying to dispose a SemaphoreSlim in the reactor", cacheName, key); + 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); } }); @@ -75,14 +75,14 @@ private SemaphoreSlim GetSemaphore(string cacheName, string key, string operatio } // ACQUIRE LOCK ASYNC - public async ValueTask<object?> AcquireLockAsync(string cacheName, string key, string operationId, TimeSpan timeout, ILogger? logger, CancellationToken token) + public async ValueTask<object?> AcquireLockAsync(string cacheName, string cacheInstanceId, string key, string operationId, TimeSpan timeout, ILogger? logger, CancellationToken token) { token.ThrowIfCancellationRequested(); - var semaphore = GetSemaphore(cacheName, key, operationId, logger); + var semaphore = GetSemaphore(cacheName, cacheInstanceId, key, logger); if (logger?.IsEnabled(LogLevel.Trace) ?? false) - logger.Log(LogLevel.Trace, "FUSION [N={CacheName}] (O={CacheOperationId} K={CacheKey}): waiting to acquire the LOCK", cacheName, operationId, key); + 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); @@ -90,25 +90,25 @@ private SemaphoreSlim GetSemaphore(string cacheName, string key, string operatio { // LOCK ACQUIRED if (logger?.IsEnabled(LogLevel.Trace) ?? false) - logger.Log(LogLevel.Trace, "FUSION [N={CacheName}] (O={CacheOperationId} K={CacheKey}): LOCK acquired", cacheName, operationId, key); + logger.Log(LogLevel.Trace, "FUSION [N={CacheName} I={CacheInstanceId}] (O={CacheOperationId} K={CacheKey}): LOCK acquired", cacheName, cacheInstanceId, operationId, key); } else { // LOCK TIMEOUT if (logger?.IsEnabled(LogLevel.Trace) ?? false) - logger.Log(LogLevel.Trace, "FUSION [N={CacheName}] (O={CacheOperationId} K={CacheKey}): LOCK timeout", cacheName, operationId, key); + logger.Log(LogLevel.Trace, "FUSION [N={CacheName} I={CacheInstanceId}] (O={CacheOperationId} K={CacheKey}): LOCK timeout", cacheName, cacheInstanceId, operationId, key); } return acquired ? semaphore : null; } // ACQUIRE LOCK - public object? AcquireLock(string cacheName, string key, string operationId, TimeSpan timeout, ILogger? logger) + public object? AcquireLock(string cacheName, string cacheInstanceId, string key, string operationId, TimeSpan timeout, ILogger? logger) { - var semaphore = GetSemaphore(cacheName, key, operationId, logger); + var semaphore = GetSemaphore(cacheName, cacheInstanceId, key, logger); if (logger?.IsEnabled(LogLevel.Trace) ?? false) - logger.Log(LogLevel.Trace, "FUSION [N={CacheName}] (O={CacheOperationId} K={CacheKey}): waiting to acquire the LOCK", cacheName, operationId, key); + 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); @@ -116,20 +116,20 @@ private SemaphoreSlim GetSemaphore(string cacheName, string key, string operatio { // LOCK ACQUIRED if (logger?.IsEnabled(LogLevel.Trace) ?? false) - logger.Log(LogLevel.Trace, "FUSION [N={CacheName}] (O={CacheOperationId} K={CacheKey}): LOCK acquired", cacheName, operationId, key); + logger.Log(LogLevel.Trace, "FUSION [N={CacheName} I={CacheInstanceId}] (O={CacheOperationId} K={CacheKey}): LOCK acquired", cacheName, cacheInstanceId, operationId, key); } else { // LOCK TIMEOUT if (logger?.IsEnabled(LogLevel.Trace) ?? false) - logger.Log(LogLevel.Trace, "FUSION [N={CacheName}] (O={CacheOperationId} K={CacheKey}): LOCK timeout", cacheName, operationId, key); + logger.Log(LogLevel.Trace, "FUSION [N={CacheName} I={CacheInstanceId}] (O={CacheOperationId} K={CacheKey}): LOCK timeout", cacheName, cacheInstanceId, operationId, key); } return acquired ? semaphore : null; } // RELEASE LOCK ASYNC - public void ReleaseLock(string cacheName, string key, string operationId, object? lockObj, ILogger? logger) + public void ReleaseLock(string cacheName, string cacheInstanceId, string key, string operationId, object? lockObj, ILogger? logger) { if (lockObj is null) return; @@ -141,7 +141,7 @@ public void ReleaseLock(string cacheName, string key, string operationId, object catch (Exception exc) { if (logger?.IsEnabled(LogLevel.Warning) ?? false) - logger.Log(LogLevel.Warning, exc, "FUSION [N={CacheName}] (O={CacheOperationId} K={CacheKey}): an error occurred while trying to release a SemaphoreSlim in the reactor", cacheName, operationId, key); + 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); } } diff --git a/src/ZiggyCreatures.FusionCache/Reactors/FusionCacheReactorUnbounded.cs b/src/ZiggyCreatures.FusionCache/Reactors/FusionCacheReactorUnbounded.cs index 2a500dae..927b6528 100644 --- a/src/ZiggyCreatures.FusionCache/Reactors/FusionCacheReactorUnbounded.cs +++ b/src/ZiggyCreatures.FusionCache/Reactors/FusionCacheReactorUnbounded.cs @@ -42,41 +42,41 @@ private SemaphoreSlim GetSemaphore(string key, string operationId, ILogger? logg } // ACQUIRE LOCK ASYNC - public async ValueTask<object?> AcquireLockAsync(string cacheName, string key, string operationId, TimeSpan timeout, ILogger? logger, CancellationToken token) + public async ValueTask<object?> 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}] (O={CacheOperationId} K={CacheKey}): waiting to acquire the LOCK", cacheName, operationId, key); + 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}] (O={CacheOperationId} K={CacheKey}): LOCK acquired", cacheName, operationId, key); + 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 key, string operationId, TimeSpan timeout, ILogger? logger) + 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}] (O={CacheOperationId} K={CacheKey}): waiting to acquire the LOCK", cacheName, operationId, key); + 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}] (O={CacheOperationId} K={CacheKey}): LOCK acquired", cacheName, operationId, key); + 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 key, string operationId, object? lockObj, ILogger? logger) + public void ReleaseLock(string cacheName, string cacheInstanceId, string key, string operationId, object? lockObj, ILogger? logger) { if (lockObj is null) return; @@ -88,7 +88,7 @@ public void ReleaseLock(string cacheName, string key, string operationId, object catch (Exception exc) { if (logger?.IsEnabled(LogLevel.Warning) ?? false) - logger.Log(LogLevel.Warning, exc, "FUSION [N={CacheName}] (O={CacheOperationId} K={CacheKey}): an error occurred while trying to release a SemaphoreSlim in the reactor", cacheName, operationId, key); + 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); } } diff --git a/src/ZiggyCreatures.FusionCache/Reactors/FusionCacheReactorUnboundedConcurrent.cs b/src/ZiggyCreatures.FusionCache/Reactors/FusionCacheReactorUnboundedConcurrent.cs index 76f50891..ade1e7b6 100644 --- a/src/ZiggyCreatures.FusionCache/Reactors/FusionCacheReactorUnboundedConcurrent.cs +++ b/src/ZiggyCreatures.FusionCache/Reactors/FusionCacheReactorUnboundedConcurrent.cs @@ -27,41 +27,41 @@ private SemaphoreSlim GetSemaphore(string key, string operationId, ILogger? logg } // ACQUIRE LOCK ASYNC - public async ValueTask<object?> AcquireLockAsync(string cacheName, string key, string operationId, TimeSpan timeout, ILogger? logger, CancellationToken token) + public async ValueTask<object?> 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}] (O={CacheOperationId} K={CacheKey}): waiting to acquire the LOCK", cacheName, operationId, key); + 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}] (O={CacheOperationId} K={CacheKey}): LOCK acquired", cacheName, operationId, key); + 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 key, string operationId, TimeSpan timeout, ILogger? logger) + 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}] (O={CacheOperationId} K={CacheKey}): waiting to acquire the LOCK", cacheName, operationId, key); + 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}] (O={CacheOperationId} K={CacheKey}): LOCK acquired", cacheName, operationId, key); + 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 key, string operationId, object? lockObj, ILogger? logger) + public void ReleaseLock(string cacheName, string cacheInstanceId, string key, string operationId, object? lockObj, ILogger? logger) { if (lockObj is null) return; @@ -73,7 +73,7 @@ public void ReleaseLock(string cacheName, string key, string operationId, object catch (Exception exc) { if (logger?.IsEnabled(LogLevel.Warning) ?? false) - logger.Log(LogLevel.Warning, exc, "FUSION [N={CacheName}] (O={CacheOperationId} K={CacheKey}): an error occurred while trying to release a SemaphoreSlim in the reactor", cacheName, operationId, key); + 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); } } diff --git a/src/ZiggyCreatures.FusionCache/Reactors/FusionCacheReactorUnboundedConcurrentLazy.cs b/src/ZiggyCreatures.FusionCache/Reactors/FusionCacheReactorUnboundedConcurrentLazy.cs index f2be12a0..dd1f970a 100644 --- a/src/ZiggyCreatures.FusionCache/Reactors/FusionCacheReactorUnboundedConcurrentLazy.cs +++ b/src/ZiggyCreatures.FusionCache/Reactors/FusionCacheReactorUnboundedConcurrentLazy.cs @@ -32,41 +32,41 @@ private SemaphoreSlim GetSemaphore(string key, string operationId, ILogger? logg } // ACQUIRE LOCK ASYNC - public async ValueTask<object?> AcquireLockAsync(string cacheName, string key, string operationId, TimeSpan timeout, ILogger? logger, CancellationToken token) + public async ValueTask<object?> 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}] (O={CacheOperationId} K={CacheKey}): waiting to acquire the LOCK", cacheName, operationId, key); + 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}] (O={CacheOperationId} K={CacheKey}): LOCK acquired", cacheName, operationId, key); + 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 key, string operationId, TimeSpan timeout, ILogger? logger) + 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}] (O={CacheOperationId} K={CacheKey}): waiting to acquire the LOCK", cacheName, operationId, key); + 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}] (O={CacheOperationId} K={CacheKey}): LOCK acquired", cacheName, operationId, key); + 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 key, string operationId, object? lockObj, ILogger? logger) + public void ReleaseLock(string cacheName, string cacheInstanceId, string key, string operationId, object? lockObj, ILogger? logger) { if (lockObj is null) return; @@ -78,7 +78,7 @@ public void ReleaseLock(string cacheName, string key, string operationId, object catch (Exception exc) { if (logger?.IsEnabled(LogLevel.Warning) ?? false) - logger.Log(LogLevel.Warning, exc, "FUSION [N={CacheName}] (O={CacheOperationId} K={CacheKey}): an error occurred while trying to release a SemaphoreSlim in the reactor", cacheName, operationId, key); + 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); } } diff --git a/src/ZiggyCreatures.FusionCache/Reactors/FusionCacheReactorUnboundedWithPool.cs b/src/ZiggyCreatures.FusionCache/Reactors/FusionCacheReactorUnboundedWithPool.cs index cfb0680a..55618d0b 100644 --- a/src/ZiggyCreatures.FusionCache/Reactors/FusionCacheReactorUnboundedWithPool.cs +++ b/src/ZiggyCreatures.FusionCache/Reactors/FusionCacheReactorUnboundedWithPool.cs @@ -60,41 +60,41 @@ private SemaphoreSlim GetSemaphore(string key, string operationId, ILogger? logg } // ACQUIRE LOCK ASYNC - public async ValueTask<object?> AcquireLockAsync(string cacheName, string key, string operationId, TimeSpan timeout, ILogger? logger, CancellationToken token) + public async ValueTask<object?> 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}] (O={CacheOperationId} K={CacheKey}): waiting to acquire the LOCK", cacheName, operationId, key); + 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}] (O={CacheOperationId} K={CacheKey}): LOCK acquired", cacheName, operationId, key); + 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 key, string operationId, TimeSpan timeout, ILogger? logger) + 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}] (O={CacheOperationId} K={CacheKey}): waiting to acquire the LOCK", cacheName, operationId, key); + 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}] (O={CacheOperationId} K={CacheKey}): LOCK acquired", cacheName, operationId, key); + 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 key, string operationId, object? lockObj, ILogger? logger) + public void ReleaseLock(string cacheName, string cacheInstanceId, string key, string operationId, object? lockObj, ILogger? logger) { if (lockObj is null) return; @@ -106,7 +106,7 @@ public void ReleaseLock(string cacheName, string key, string operationId, object catch (Exception exc) { if (logger?.IsEnabled(LogLevel.Warning) ?? false) - logger.Log(LogLevel.Warning, exc, "FUSION [N={CacheName}] (O={CacheOperationId} K={CacheKey}): an error occurred while trying to release a SemaphoreSlim in the reactor", cacheName, operationId, key); + 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); } } diff --git a/src/ZiggyCreatures.FusionCache/Reactors/IFusionCacheReactor.cs b/src/ZiggyCreatures.FusionCache/Reactors/IFusionCacheReactor.cs index 984cd5c8..c6bc86ee 100644 --- a/src/ZiggyCreatures.FusionCache/Reactors/IFusionCacheReactor.cs +++ b/src/ZiggyCreatures.FusionCache/Reactors/IFusionCacheReactor.cs @@ -14,38 +14,41 @@ public interface IFusionCacheReactor /// <summary> /// Acquire a generic lock, used to synchronize multiple factory operating on the same cache key, and return it. /// </summary> - /// <param name="cacheName">The name of the FusionCache instance.</param> + /// <param name="cacheName">The CacheName of the FusionCache instance.</param> + /// <param name="cacheInstanceId">The InstanceId of the FusionCache instance.</param> /// <param name="key">The key for which to obtain a lock.</param> /// <param name="operationId">The operation id which uniquely identifies a high-level cache operation.</param> /// <param name="timeout">The optional timeout for the lock acquisition.</param> /// <param name="logger">The <see cref="ILogger"/> to use, if any.</param> /// <returns>The acquired genericlock object, later released when the critical section is over.</returns> /// <param name="token">An optional <see cref="CancellationToken"/> to cancel the operation.</param> - ValueTask<object?> AcquireLockAsync(string cacheName, string key, string operationId, TimeSpan timeout, ILogger? logger, CancellationToken token); + ValueTask<object?> AcquireLockAsync(string cacheName, string cacheInstanceId, string key, string operationId, TimeSpan timeout, ILogger? logger, CancellationToken token); /// <summary> /// Acquire a generic lock, used to synchronize multiple factory operating on the same cache key, and return it. /// </summary> /// <param name="cacheName">The name of the FusionCache instance.</param> + /// <param name="cacheInstanceId">The InstanceId of the FusionCache instance.</param> /// <param name="key">The key for which to obtain a lock.</param> /// <param name="operationId">The operation id which uniquely identifies a high-level cache operation.</param> /// <param name="timeout">The optional timeout for the lock acquisition.</param> /// <returns>The acquired genericlock object, later released when the critical section is over.</returns> /// <param name="logger">The <see cref="ILogger"/> to use, if any.</param> - object? AcquireLock(string cacheName, string key, string operationId, TimeSpan timeout, ILogger? logger); + object? AcquireLock(string cacheName, string cacheInstanceId, string key, string operationId, TimeSpan timeout, ILogger? logger); /// <summary> /// Release the generic lock object. /// </summary> /// <param name="cacheName">The name of the FusionCache instance.</param> + /// <param name="cacheInstanceId">The InstanceId of the FusionCache instance.</param> /// <param name="key">The key for which to obtain a lock.</param> /// <param name="operationId">The operation id which uniquely identifies a high-level cache operation.</param> /// <param name="lockObj">The generic lock object to release.</param> /// <param name="logger">The <see cref="ILogger"/> to use, if any.</param> - void ReleaseLock(string cacheName, string key, string operationId, object? lockObj, ILogger? logger); + void ReleaseLock(string cacheName, string cacheInstanceId, string key, string operationId, object? lockObj, ILogger? logger); /// <summary> - /// Exposes the eventual amount ofcollisions happened inside the reactor, for diagnostics purposes. + /// Exposes the eventual amount of collisions happened inside the reactor, for diagnostics purposes. /// </summary> int Collisions { get; } } diff --git a/src/ZiggyCreatures.FusionCache/ZiggyCreatures.FusionCache.csproj b/src/ZiggyCreatures.FusionCache/ZiggyCreatures.FusionCache.csproj index 8f78c9bf..da47ab78 100644 --- a/src/ZiggyCreatures.FusionCache/ZiggyCreatures.FusionCache.csproj +++ b/src/ZiggyCreatures.FusionCache/ZiggyCreatures.FusionCache.csproj @@ -4,7 +4,7 @@ <TargetFramework>netstandard2.0</TargetFramework> <LangVersion>latest</LangVersion> <Nullable>enable</Nullable> - <Version>0.23.0</Version> + <Version>0.24.0</Version> <PackageId>ZiggyCreatures.FusionCache</PackageId> <PackageIcon>logo-128x128.png</PackageIcon> <Description> @@ -15,17 +15,17 @@ <DocumentationFile>ZiggyCreatures.FusionCache.xml</DocumentationFile> <PackageReadmeFile>README.md</PackageReadmeFile> <PackageReleaseNotes> - - Added: backplane active re-connection support - - Added: delayed processing on backplane re-connect - - Change: better backplane backpressure handling - - Change: better backplane auto-recovery background processing - - Change: better handling of connection/subscription errors - - Added: NullFusionCache (null object pattern) - - Added: NullFusionCacheBackplane - - Added: NullDistributedCache - - Added: NullFusionCachePlugin - - Change: better log messages - - Tests: better snapshot testing of binary payloads (distributed cache) + - 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 </PackageReleaseNotes> <EnablePackageValidation>true</EnablePackageValidation> diff --git a/src/ZiggyCreatures.FusionCache/ZiggyCreatures.FusionCache.xml b/src/ZiggyCreatures.FusionCache/ZiggyCreatures.FusionCache.xml index dce1275e..1401490b 100644 --- a/src/ZiggyCreatures.FusionCache/ZiggyCreatures.FusionCache.xml +++ b/src/ZiggyCreatures.FusionCache/ZiggyCreatures.FusionCache.xml @@ -30,14 +30,20 @@ Creates a new instance of a backplane message. </summary> </member> + <member name="M:ZiggyCreatures.Caching.Fusion.Backplane.BackplaneMessage.#ctor(System.Nullable{System.Int64})"> + <summary> + Creates a new instance of a backplane message. + </summary> + <param name="timestamp">The timestamp, or <see langword="null"/> to set it automatically to the current timestamp.</param> + </member> <member name="P:ZiggyCreatures.Caching.Fusion.Backplane.BackplaneMessage.SourceId"> <summary> The InstanceId of the source cache. </summary> </member> - <member name="P:ZiggyCreatures.Caching.Fusion.Backplane.BackplaneMessage.InstantTicks"> + <member name="P:ZiggyCreatures.Caching.Fusion.Backplane.BackplaneMessage.Timestamp"> <summary> - The instant a message was related to, expressed as ticks amount. + The timestamp (in ticks) at a message has been created. </summary> </member> <member name="P:ZiggyCreatures.Caching.Fusion.Backplane.BackplaneMessage.Action"> @@ -56,27 +62,48 @@ </summary> <returns><see langword="true"/> if it seems valid, <see langword="false"/> otherwise.</returns> </member> - <member name="M:ZiggyCreatures.Caching.Fusion.Backplane.BackplaneMessage.CreateForEntrySet(System.String)"> + <member name="M:ZiggyCreatures.Caching.Fusion.Backplane.BackplaneMessage.CreateForEntrySet(System.String,System.String,System.Nullable{System.Int64})"> <summary> Creates a message for a single cache entry set operation (via either a Set() or a GetOrSet() method call). </summary> + <param name="sourceId">The cache InstanceId of the source.</param> <param name="cacheKey">The cache key.</param> + <param name="timestamp">The timestamp.</param> <returns>The message.</returns> </member> - <member name="M:ZiggyCreatures.Caching.Fusion.Backplane.BackplaneMessage.CreateForEntryRemove(System.String)"> + <member name="M:ZiggyCreatures.Caching.Fusion.Backplane.BackplaneMessage.CreateForEntryRemove(System.String,System.String,System.Nullable{System.Int64})"> <summary> Creates a message for a single cache entry remove (via a Remove() method call). </summary> + <param name="sourceId">The cache InstanceId of the source.</param> <param name="cacheKey">The cache key.</param> + <param name="timestamp">The timestamp.</param> <returns>The message.</returns> </member> - <member name="M:ZiggyCreatures.Caching.Fusion.Backplane.BackplaneMessage.CreateForEntryExpire(System.String)"> + <member name="M:ZiggyCreatures.Caching.Fusion.Backplane.BackplaneMessage.CreateForEntryExpire(System.String,System.String,System.Nullable{System.Int64})"> <summary> Creates a message for a single cache entry expire operation (via an Expire() method call). </summary> + <param name="sourceId">The cache InstanceId of the source.</param> <param name="cacheKey">The cache key.</param> + <param name="timestamp">The timestamp.</param> <returns>The message.</returns> </member> + <member name="M:ZiggyCreatures.Caching.Fusion.Backplane.BackplaneMessage.ToByteArray(ZiggyCreatures.Caching.Fusion.Backplane.BackplaneMessage)"> + <summary> + Serializes a backplane message to a byte array. + </summary> + <param name="message">The backplane message to serialize.</param> + <returns></returns> + </member> + <member name="M:ZiggyCreatures.Caching.Fusion.Backplane.BackplaneMessage.FromByteArray(System.Byte[])"> + <summary> + Deserializes a byte array into a backplane message. + </summary> + <param name="data">The byte array to deserialize.</param> + <returns>An instance of a backplane message, or <see langword="null"/></returns> + <exception cref="T:System.FormatException"></exception> + </member> <member name="T:ZiggyCreatures.Caching.Fusion.Backplane.BackplaneMessageAction"> <summary> The type of action for a backplane message. @@ -107,6 +134,14 @@ Represents the options available for subscribing to a backplane. </summary> </member> + <member name="M:ZiggyCreatures.Caching.Fusion.Backplane.BackplaneSubscriptionOptions.#ctor(System.String,System.Action{ZiggyCreatures.Caching.Fusion.Backplane.BackplaneConnectionInfo},System.Action{ZiggyCreatures.Caching.Fusion.Backplane.BackplaneMessage})"> + <summary> + Creates a new instance of a <see cref="T:ZiggyCreatures.Caching.Fusion.Backplane.BackplaneSubscriptionOptions"/>. + </summary> + <param name="channelName">The channel name to be used.</param> + <param name="connectHandler">The backplane connection handler that will be used when there's a connection (or reconnection).</param> + <param name="incomingMessageHandler">The backplane message handler that will be used to process incoming messages.</param> + </member> <member name="P:ZiggyCreatures.Caching.Fusion.Backplane.BackplaneSubscriptionOptions.ChannelName"> <summary> The channel name to be used. @@ -117,14 +152,14 @@ The backplane message handler that will be used to process incoming messages. </summary> </member> - <member name="P:ZiggyCreatures.Caching.Fusion.Backplane.BackplaneSubscriptionOptions.IncomingMessageHandler"> + <member name="P:ZiggyCreatures.Caching.Fusion.Backplane.BackplaneSubscriptionOptions.ConnectHandler"> <summary> - The backplane message handler that will be used to process incoming messages. + The backplane connection handler that will be used when there's a connection (or reconnection). </summary> </member> - <member name="P:ZiggyCreatures.Caching.Fusion.Backplane.BackplaneSubscriptionOptions.ConnectHandler"> + <member name="P:ZiggyCreatures.Caching.Fusion.Backplane.BackplaneSubscriptionOptions.IncomingMessageHandler"> <summary> - The backplane connection handler that will be used when there's a connection (or reconnection). + The backplane message handler that will be used to process incoming messages. </summary> </member> <member name="T:ZiggyCreatures.Caching.Fusion.Backplane.IFusionCacheBackplane"> @@ -247,23 +282,23 @@ </member> <member name="T:ZiggyCreatures.Caching.Fusion.Events.FusionCacheBackplaneMessageEventArgs"> <summary> - The specific <see cref="T:System.EventArgs"/> object for events related to cache entries (eg: with a cache key). + The specific <see cref="T:System.EventArgs"/> object for events related to backplane messages, either published or received. </summary> </member> <member name="M:ZiggyCreatures.Caching.Fusion.Events.FusionCacheBackplaneMessageEventArgs.#ctor(ZiggyCreatures.Caching.Fusion.Backplane.BackplaneMessage)"> <summary> Initializes a new instance of the <see cref="T:ZiggyCreatures.Caching.Fusion.Events.FusionCacheBackplaneMessageEventArgs"/> class. </summary> - <param name="message">The backplane message received</param> + <param name="message">The backplane message.</param> </member> <member name="P:ZiggyCreatures.Caching.Fusion.Events.FusionCacheBackplaneMessageEventArgs.Message"> <summary> - The backplane message received. + The backplane message. </summary> </member> <member name="T:ZiggyCreatures.Caching.Fusion.Events.FusionCacheCircuitBreakerChangeEventArgs"> <summary> - The specific <see cref="T:System.EventArgs"/> object for events related to opening/closing of the distributed cache circuit breaker. + The specific <see cref="T:System.EventArgs"/> object for events related to opening/closing of a circuit breaker. </summary> </member> <member name="M:ZiggyCreatures.Caching.Fusion.Events.FusionCacheCircuitBreakerChangeEventArgs.#ctor(System.Boolean)"> @@ -359,18 +394,24 @@ The specific <see cref="T:System.EventArgs"/> object for events related to cache entries' evictions. </summary> </member> - <member name="M:ZiggyCreatures.Caching.Fusion.Events.FusionCacheEntryEvictionEventArgs.#ctor(System.String,Microsoft.Extensions.Caching.Memory.EvictionReason)"> + <member name="M:ZiggyCreatures.Caching.Fusion.Events.FusionCacheEntryEvictionEventArgs.#ctor(System.String,Microsoft.Extensions.Caching.Memory.EvictionReason,System.Object)"> <summary> Initializes a new instance of the <see cref="T:ZiggyCreatures.Caching.Fusion.Events.FusionCacheEntryEvictionEventArgs"/> class. </summary> <param name="key">The cache key related to the event.</param> <param name="reason">The reason for the eviction.</param> + <param name="value">The value being evicted from the cache.</param> </member> <member name="P:ZiggyCreatures.Caching.Fusion.Events.FusionCacheEntryEvictionEventArgs.Reason"> <summary> The reason for the eviction. </summary> </member> + <member name="P:ZiggyCreatures.Caching.Fusion.Events.FusionCacheEntryEvictionEventArgs.Value"> + <summary> + The value being evicted from the cache. + </summary> + </member> <member name="T:ZiggyCreatures.Caching.Fusion.Events.FusionCacheEntryHitEventArgs"> <summary> The specific <see cref="T:System.EventArgs"/> object for events related to cache entries' hits (eg: with a cache key and a stale flag). @@ -591,6 +632,30 @@ <member name="M:ZiggyCreatures.Caching.Fusion.FusionCache.Expire(System.String,ZiggyCreatures.Caching.Fusion.FusionCacheEntryOptions,System.Threading.CancellationToken)"> <inheritdoc/> </member> + <member name="T:ZiggyCreatures.Caching.Fusion.FusionCacheBackplaneException"> + <summary> + The generic exception that is thrown when a distributed cache error occurs: the InnerException contains the original exception. + </summary> + </member> + <member name="M:ZiggyCreatures.Caching.Fusion.FusionCacheBackplaneException.#ctor"> + <summary> + Initializes a new instance of the <see cref="T:ZiggyCreatures.Caching.Fusion.FusionCacheBackplaneException"/> class. + </summary> + </member> + <member name="M:ZiggyCreatures.Caching.Fusion.FusionCacheBackplaneException.#ctor(System.String)"> + <summary>Initializes a new instance of the <see cref="T:ZiggyCreatures.Caching.Fusion.FusionCacheBackplaneException"/> class with a specified error message.</summary> + <param name="message">The message that describes the error.</param> + </member> + <member name="M:ZiggyCreatures.Caching.Fusion.FusionCacheBackplaneException.#ctor(System.String,System.Exception)"> + <summary>Initializes a new instance of the <see cref="T:ZiggyCreatures.Caching.Fusion.FusionCacheBackplaneException"/> class with a specified error message and a reference to the inner exception that is the cause of this exception.</summary> + <param name="message">The error message that explains the reason for the exception.</param> + <param name="innerException">The exception that is the cause of the current exception. If the innerException parameter is not a null reference (Nothing in Visual Basic), the current exception is raised in a catch block that handles the inner exception.</param> + </member> + <member name="M:ZiggyCreatures.Caching.Fusion.FusionCacheBackplaneException.#ctor(System.Runtime.Serialization.SerializationInfo,System.Runtime.Serialization.StreamingContext)"> + <summary>Initializes a new instance of the <see cref="T:ZiggyCreatures.Caching.Fusion.FusionCacheBackplaneException"/> class with serialized data.</summary> + <param name="info">The object that holds the serialized object data.</param> + <param name="context">The contextual information about the source or destination.</param> + </member> <member name="T:ZiggyCreatures.Caching.Fusion.FusionCacheBuilderExtMethods"> <summary> A set of extension methods that add some commonly used setup actions to an instance of a <see cref="T:ZiggyCreatures.Caching.Fusion.IFusionCacheBuilder"/> object. @@ -997,6 +1062,30 @@ <param name="ignoreMemoryDistributedCache">Indicates if the distributed cache found in the DI container should be ignored if it is of type <see cref="T:Microsoft.Extensions.Caching.Distributed.MemoryDistributedCache"/>, since that is not really a distributed cache and it's automatically registered by ASP.NET MVC without control from the user</param> <returns>The <see cref="T:ZiggyCreatures.Caching.Fusion.IFusionCacheBuilder"/> so that additional calls can be chained.</returns> </member> + <member name="T:ZiggyCreatures.Caching.Fusion.FusionCacheDistributedCacheException"> + <summary> + The generic exception that is thrown when a distributed cache error occurs: the InnerException contains the original exception. + </summary> + </member> + <member name="M:ZiggyCreatures.Caching.Fusion.FusionCacheDistributedCacheException.#ctor"> + <summary> + Initializes a new instance of the <see cref="T:ZiggyCreatures.Caching.Fusion.FusionCacheDistributedCacheException"/> class. + </summary> + </member> + <member name="M:ZiggyCreatures.Caching.Fusion.FusionCacheDistributedCacheException.#ctor(System.String)"> + <summary>Initializes a new instance of the <see cref="T:ZiggyCreatures.Caching.Fusion.FusionCacheDistributedCacheException"/> class with a specified error message.</summary> + <param name="message">The message that describes the error.</param> + </member> + <member name="M:ZiggyCreatures.Caching.Fusion.FusionCacheDistributedCacheException.#ctor(System.String,System.Exception)"> + <summary>Initializes a new instance of the <see cref="T:ZiggyCreatures.Caching.Fusion.FusionCacheDistributedCacheException"/> class with a specified error message and a reference to the inner exception that is the cause of this exception.</summary> + <param name="message">The error message that explains the reason for the exception.</param> + <param name="innerException">The exception that is the cause of the current exception. If the innerException parameter is not a null reference (Nothing in Visual Basic), the current exception is raised in a catch block that handles the inner exception.</param> + </member> + <member name="M:ZiggyCreatures.Caching.Fusion.FusionCacheDistributedCacheException.#ctor(System.Runtime.Serialization.SerializationInfo,System.Runtime.Serialization.StreamingContext)"> + <summary>Initializes a new instance of the <see cref="T:ZiggyCreatures.Caching.Fusion.FusionCacheDistributedCacheException"/> class with serialized data.</summary> + <param name="info">The object that holds the serialized object data.</param> + <param name="context">The contextual information about the source or destination.</param> + </member> <member name="T:ZiggyCreatures.Caching.Fusion.FusionCacheEntryOptions"> <summary> Represents all the options available for a single <see cref="T:ZiggyCreatures.Caching.Fusion.IFusionCache"/> entry. @@ -1166,7 +1255,7 @@ </member> <member name="P:ZiggyCreatures.Caching.Fusion.FusionCacheEntryOptions.ReThrowSerializationExceptions"> <summary> - Set this to <see langword="true"/> to allow the bubble up of serialization exceptions (default is <see langword="false"/>). + Set this to <see langword="true"/> to allow the bubble up of serialization exceptions (default is <see langword="true"/>). Please note that, even if set to <see langword="true"/>, in some cases you would also need <see cref="P:ZiggyCreatures.Caching.Fusion.FusionCacheEntryOptions.AllowBackgroundDistributedCacheOperations"/> set to <see langword="false"/> and no timeout (neither soft nor hard) specified. <br/><br/> <strong>DOCS:</strong> <see href="https://github.com/ZiggyCreatures/FusionCache/blob/main/docs/CacheLevels.md"/> @@ -1201,6 +1290,14 @@ <strong>DOCS:</strong> <see href="https://github.com/ZiggyCreatures/FusionCache/blob/main/docs/Backplane.md"/> </summary> </member> + <member name="P:ZiggyCreatures.Caching.Fusion.FusionCacheEntryOptions.ReThrowBackplaneExceptions"> + <summary> + Set this to <see langword="true"/> to allow the bubble up of backplane exceptions (default is <see langword="false"/>). + Please note that, even if set to <see langword="true"/>, in some cases you would also need <see cref="P:ZiggyCreatures.Caching.Fusion.FusionCacheEntryOptions.AllowBackgroundBackplaneOperations"/> set to <see langword="false"/> and no timeout (neither soft nor hard) specified. + <br/><br/> + <strong>DOCS:</strong> <see href="https://github.com/ZiggyCreatures/FusionCache/blob/main/docs/Backplane.md"/> + </summary> + </member> <member name="P:ZiggyCreatures.Caching.Fusion.FusionCacheEntryOptions.SkipDistributedCache"> <summary> Skip the usage of the distributed cache, if any. @@ -1998,6 +2095,8 @@ <member name="T:ZiggyCreatures.Caching.Fusion.FusionCacheGlobalDefaults"> <summary> Contains the default values used globally. + <br/><br/> + <strong>NOTE:</strong> since these values are used *globally*, they should be changed only as a last resort, and if you *really* know what you are doing. </summary> </member> <member name="P:ZiggyCreatures.Caching.Fusion.FusionCacheGlobalDefaults.EntryOptionsDuration"> @@ -2100,6 +2199,11 @@ The global default <see cref="P:ZiggyCreatures.Caching.Fusion.FusionCacheEntryOptions.AllowBackgroundBackplaneOperations"/>. </summary> </member> + <member name="P:ZiggyCreatures.Caching.Fusion.FusionCacheGlobalDefaults.EntryOptionsReThrowBackplaneExceptions"> + <summary> + The global default <see cref="P:ZiggyCreatures.Caching.Fusion.FusionCacheEntryOptions.ReThrowBackplaneExceptions"/>. + </summary> + </member> <member name="P:ZiggyCreatures.Caching.Fusion.FusionCacheGlobalDefaults.EntryOptionsSkipDistributedCache"> <summary> The global default <see cref="P:ZiggyCreatures.Caching.Fusion.FusionCacheEntryOptions.SkipDistributedCache"/>. @@ -2125,6 +2229,26 @@ The default value for <see cref="P:ZiggyCreatures.Caching.Fusion.IFusionCache.CacheName"/>. </summary> </member> + <member name="F:ZiggyCreatures.Caching.Fusion.FusionCacheOptions.DistributedCacheWireFormatVersion"> + <summary> + The wire format version identifier for the distributed cache wire format, used in the cache key processing. + </summary> + </member> + <member name="F:ZiggyCreatures.Caching.Fusion.FusionCacheOptions.DistributedCacheWireFormatSeparator"> + <summary> + The wire format version separator for the distributed cache wire format, used in the cache key processing. + </summary> + </member> + <member name="F:ZiggyCreatures.Caching.Fusion.FusionCacheOptions.BackplaneWireFormatVersion"> + <summary> + The wire format version identifier for the backplane wire format, used in the channel name. + </summary> + </member> + <member name="F:ZiggyCreatures.Caching.Fusion.FusionCacheOptions.BackplaneWireFormatSeparator"> + <summary> + The wire format version separator for the backplane wire format, used in the channel name. + </summary> + </member> <member name="M:ZiggyCreatures.Caching.Fusion.FusionCacheOptions.#ctor"> <summary> Creates a new instance of a <see cref="T:ZiggyCreatures.Caching.Fusion.FusionCacheOptions"/> object. @@ -2137,6 +2261,19 @@ <strong>NOTE:</strong> if you try to set this to a null/whitespace value, that value will be ignored. </summary> </member> + <member name="P:ZiggyCreatures.Caching.Fusion.FusionCacheOptions.InstanceId"> + <summary> + The instance id of the cache: it will be used for low-level identification for the same logical cache between different nodes in a multi-node scenario: it is automatically set to a random value. + </summary> + </member> + <member name="M:ZiggyCreatures.Caching.Fusion.FusionCacheOptions.SetInstanceId(System.String)"> + <summary> + Set the InstanceId of the cache, but please don't use this. + <br/><br/> + <strong>⚠ WARNING:</strong> again, this should NOT be set, basically never ever, unless you really know what you are doing. For example by using the same value for two different cache instances they will be considered as the same cache, and this will lead to critical errors. So again, really: you should not use this. + </summary> + <param name="instanceId"></param> + </member> <member name="P:ZiggyCreatures.Caching.Fusion.FusionCacheOptions.DefaultEntryOptions"> <summary> The default <see cref="T:ZiggyCreatures.Caching.Fusion.FusionCacheEntryOptions"/> to use when none will be specified, and as the starting point when duplicating one. @@ -2187,20 +2324,61 @@ </member> <member name="P:ZiggyCreatures.Caching.Fusion.FusionCacheOptions.EnableBackplaneAutoRecovery"> <summary> + DEPRECATED: please use EnableAutoRecovery. + <br/><br/> Enable auto-recovery for the backplane notifications to better handle transient errors without generating synchronization issues: notifications that failed to be sent out will be retried later on, when the backplane becomes responsive again. <br/><br/> - <strong>DOCS:</strong> <see href="https://github.com/ZiggyCreatures/FusionCache/blob/main/docs/Backplane.md"/> + <strong>DOCS:</strong> <see href="https://github.com/ZiggyCreatures/FusionCache/blob/main/docs/AutoRecovery.md"/> + </summary> + </member> + <member name="P:ZiggyCreatures.Caching.Fusion.FusionCacheOptions.EnableAutoRecovery"> + <summary> + Enable auto-recovery to automatically handle transient errors to minimize synchronization issues. + <br/><br/> + <strong>DOCS:</strong> <see href="https://github.com/ZiggyCreatures/FusionCache/blob/main/docs/AutoRecovery.md"/> </summary> </member> <member name="P:ZiggyCreatures.Caching.Fusion.FusionCacheOptions.BackplaneAutoRecoveryMaxItems"> <summary> + DEPRECATED: please use AutoRecoveryMaxItems. + <br/><br/> The maximum number of items in the auto-recovery queue: this can help reducing memory consumption. If set to <see langword="null"/> there will be no limit. <br/><br/> - <strong>DOCS:</strong> <see href="https://github.com/ZiggyCreatures/FusionCache/blob/main/docs/Backplane.md"/> + <strong>DOCS:</strong> <see href="https://github.com/ZiggyCreatures/FusionCache/blob/main/docs/AutoRecovery.md"/> + </summary> + </member> + <member name="P:ZiggyCreatures.Caching.Fusion.FusionCacheOptions.AutoRecoveryMaxItems"> + <summary> + The maximum number of items in the auto-recovery queue: this is usually not needed, but it may help reducing memory consumption in extreme scenarios. + <br/> + When set to null <see langword="null"/> there will be no limits. + <br/><br/> + <strong>DOCS:</strong> <see href="https://github.com/ZiggyCreatures/FusionCache/blob/main/docs/AutoRecovery.md"/> + </summary> + </member> + <member name="P:ZiggyCreatures.Caching.Fusion.FusionCacheOptions.BackplaneAutoRecoveryMaxRetryCount"> + <summary> + DEPRECATED: please use AutoRecoveryMaxRetryCount. + <br/><br/> + The maximum number of retries for a auto-recovery item: after this amount the item is discarded, to avoid keeping it retrying forever. If set to <see langword="null"/> there will be no limit. + <br/><br/> + <strong>DOCS:</strong> <see href="https://github.com/ZiggyCreatures/FusionCache/blob/main/docs/AutoRecovery.md"/> + </summary> + </member> + <member name="P:ZiggyCreatures.Caching.Fusion.FusionCacheOptions.AutoRecoveryMaxRetryCount"> + <summary> + The maximum number of retries for a auto-recovery item: after this amount an item is discarded, to avoid keeping it for too long. + Please note though that a cleanup is automatically performed, so in theory there's no need to set this. + <br/> + When set to <see langword="null"/> there will be no limits. + <br/><br/> + <strong>DOCS:</strong> <see href="https://github.com/ZiggyCreatures/FusionCache/blob/main/docs/AutoRecovery.md"/> </summary> </member> <member name="P:ZiggyCreatures.Caching.Fusion.FusionCacheOptions.BackplaneAutoRecoveryReconnectDelay"> <summary> + DEPRECATED: please use AutoRecoveryDelay. + <br/><br/> The amount of time to wait, after a backplane reconnection, before trying to process the auto-recovery queue: this may be useful to allow all the other nodes to be ready. <br/> Use <see cref="F:System.TimeSpan.Zero"/> to avoid any delay (risky). @@ -2208,11 +2386,36 @@ <strong>DOCS:</strong> <see href="https://github.com/ZiggyCreatures/FusionCache/blob/main/docs/Backplane.md"/> </summary> </member> + <member name="P:ZiggyCreatures.Caching.Fusion.FusionCacheOptions.BackplaneAutoRecoveryDelay"> + <summary> + DEPRECATED: please use AutoRecoveryDelay. + <br/><br/> + The amount of time to wait before actually processing the auto-recovery queue, to better handle backpressure. + <br/> + Use <see cref="F:System.TimeSpan.Zero"/> to avoid any delay (risky). + <br/><br/> + <strong>DOCS:</strong> <see href="https://github.com/ZiggyCreatures/FusionCache/blob/main/docs/AutoRecovery.md"/> + </summary> + </member> + <member name="P:ZiggyCreatures.Caching.Fusion.FusionCacheOptions.AutoRecoveryDelay"> + <summary> + The amount of time to wait before actually processing the auto-recovery queue, to better handle backpressure. + <br/> + Use <see cref="F:System.TimeSpan.Zero"/> to avoid any delay (risky). + <br/><br/> + <strong>DOCS:</strong> <see href="https://github.com/ZiggyCreatures/FusionCache/blob/main/docs/AutoRecovery.md"/> + </summary> + </member> <member name="P:ZiggyCreatures.Caching.Fusion.FusionCacheOptions.EnableDistributedExpireOnBackplaneAutoRecovery"> <summary> Enable expiring a cache entry, only on the distributed cache (if any), when anauto-recovery message is being published on the backplane, to ensure that the value in the distributed cache will not be stale. <br/><br/> - <strong>DOCS:</strong> <see href="https://github.com/ZiggyCreatures/FusionCache/blob/main/docs/Backplane.md"/> + <strong>DOCS:</strong> <see href="https://github.com/ZiggyCreatures/FusionCache/blob/main/docs/AutoRecovery.md"/> + </summary> + </member> + <member name="P:ZiggyCreatures.Caching.Fusion.FusionCacheOptions.ReThrowOriginalExceptions"> + <summary> + 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 (<see cref="T:ZiggyCreatures.Caching.Fusion.FusionCacheSerializationException"/>, <see cref="T:ZiggyCreatures.Caching.Fusion.FusionCacheDistributedCacheException"/> or <see cref="T:ZiggyCreatures.Caching.Fusion.FusionCacheBackplaneException"/>). </summary> </member> <member name="P:ZiggyCreatures.Caching.Fusion.FusionCacheOptions.IncoherentOptionsNormalizationLogLevel"> @@ -2305,11 +2508,35 @@ <strong>DOCS:</strong> <see href="https://github.com/ZiggyCreatures/FusionCache/blob/main/docs/Logging.md"/> </summary> </member> - <member name="M:ZiggyCreatures.Caching.Fusion.FusionCacheOptions.SetCacheNameAsCacheKeyPrefix"> + <member name="M:ZiggyCreatures.Caching.Fusion.FusionCacheOptions.Duplicate"> + <summary> + Creates a new <see cref="T:ZiggyCreatures.Caching.Fusion.FusionCacheOptions"/> object by duplicating all the options of the current one. + </summary> + <returns>The newly created <see cref="T:ZiggyCreatures.Caching.Fusion.FusionCacheOptions"/> object.</returns> + </member> + <member name="T:ZiggyCreatures.Caching.Fusion.FusionCacheSerializationException"> + <summary> + The generic exception that is thrown when a serialization error occurs: the InnerException contains the original exception. + </summary> + </member> + <member name="M:ZiggyCreatures.Caching.Fusion.FusionCacheSerializationException.#ctor"> <summary> - Set the <see cref="P:ZiggyCreatures.Caching.Fusion.FusionCacheOptions.CacheKeyPrefix"/> to the <see cref="P:ZiggyCreatures.Caching.Fusion.FusionCacheOptions.CacheName"/>, and a ":" separator. + Initializes a new instance of the <see cref="T:ZiggyCreatures.Caching.Fusion.FusionCacheSerializationException"/> class. </summary> - <returns>The <see cref="T:ZiggyCreatures.Caching.Fusion.FusionCacheOptions"/> so that additional calls can be chained.</returns> + </member> + <member name="M:ZiggyCreatures.Caching.Fusion.FusionCacheSerializationException.#ctor(System.String)"> + <summary>Initializes a new instance of the <see cref="T:ZiggyCreatures.Caching.Fusion.FusionCacheSerializationException"/> class with a specified error message.</summary> + <param name="message">The message that describes the error.</param> + </member> + <member name="M:ZiggyCreatures.Caching.Fusion.FusionCacheSerializationException.#ctor(System.String,System.Exception)"> + <summary>Initializes a new instance of the <see cref="T:ZiggyCreatures.Caching.Fusion.FusionCacheSerializationException"/> class with a specified error message and a reference to the inner exception that is the cause of this exception.</summary> + <param name="message">The error message that explains the reason for the exception.</param> + <param name="innerException">The exception that is the cause of the current exception. If the innerException parameter is not a null reference (Nothing in Visual Basic), the current exception is raised in a catch block that handles the inner exception.</param> + </member> + <member name="M:ZiggyCreatures.Caching.Fusion.FusionCacheSerializationException.#ctor(System.Runtime.Serialization.SerializationInfo,System.Runtime.Serialization.StreamingContext)"> + <summary>Initializes a new instance of the <see cref="T:ZiggyCreatures.Caching.Fusion.FusionCacheSerializationException"/> class with serialized data.</summary> + <param name="info">The object that holds the serialized object data.</param> + <param name="context">The contextual information about the source or destination.</param> </member> <member name="T:ZiggyCreatures.Caching.Fusion.IFusionCache"> <summary> @@ -2810,7 +3037,7 @@ </summary> <typeparam name="TValue">The type of the entry's value</typeparam> </member> - <member name="M:ZiggyCreatures.Caching.Fusion.Internals.Distributed.FusionCacheDistributedEntry`1.#ctor(`0,ZiggyCreatures.Caching.Fusion.Internals.FusionCacheEntryMetadata,System.Nullable{System.Int64})"> + <member name="M:ZiggyCreatures.Caching.Fusion.Internals.Distributed.FusionCacheDistributedEntry`1.#ctor(`0,ZiggyCreatures.Caching.Fusion.Internals.FusionCacheEntryMetadata,System.Int64)"> <summary> Creates a new instance. </summary> @@ -2841,7 +3068,7 @@ <member name="M:ZiggyCreatures.Caching.Fusion.Internals.Distributed.FusionCacheDistributedEntry`1.ToString"> <inheritdoc/> </member> - <member name="M:ZiggyCreatures.Caching.Fusion.Internals.Distributed.FusionCacheDistributedEntry`1.CreateFromOptions(`0,ZiggyCreatures.Caching.Fusion.FusionCacheEntryOptions,System.Boolean,System.Nullable{System.DateTimeOffset},System.String,System.Nullable{System.Int64})"> + <member name="M:ZiggyCreatures.Caching.Fusion.Internals.Distributed.FusionCacheDistributedEntry`1.CreateFromOptions(`0,ZiggyCreatures.Caching.Fusion.FusionCacheEntryOptions,System.Boolean,System.Nullable{System.DateTimeOffset},System.String,System.Int64)"> <summary> Creates a new <see cref="T:ZiggyCreatures.Caching.Fusion.Internals.Distributed.FusionCacheDistributedEntry`1"/> instance from a value and some options. </summary> @@ -2918,12 +3145,100 @@ <member name="M:ZiggyCreatures.Caching.Fusion.Internals.FusionCacheEntryMetadata.ToString"> <inheritdoc/> </member> - <member name="T:ZiggyCreatures.Caching.Fusion.Internals.FusionCacheExecutionUtils"> + <member name="M:ZiggyCreatures.Caching.Fusion.Internals.FusionCacheInternalUtils.IsLogicallyExpired(ZiggyCreatures.Caching.Fusion.Internals.IFusionCacheEntry)"> + <summary> + Checks if the entry is logically expired. + </summary> + <returns>A <see cref="T:System.Boolean"/> indicating the logical expiration status.</returns> + </member> + <member name="T:ZiggyCreatures.Caching.Fusion.Internals.IFusionCacheEntry"> + <summary> + Represents an generic entry in a <see cref="T:ZiggyCreatures.Caching.Fusion.FusionCache"/>, which can be either a <see cref="T:ZiggyCreatures.Caching.Fusion.Internals.Memory.FusionCacheMemoryEntry"/> or a <see cref="T:ZiggyCreatures.Caching.Fusion.Internals.Distributed.FusionCacheDistributedEntry`1"/>. + </summary> + </member> + <member name="M:ZiggyCreatures.Caching.Fusion.Internals.IFusionCacheEntry.GetValue``1"> + <summary> + Get the value inside the entry. + </summary> + <typeparam name="TValue">The typeof the value.</typeparam> + <returns>The value.</returns> + </member> + <member name="M:ZiggyCreatures.Caching.Fusion.Internals.IFusionCacheEntry.SetValue``1(``0)"> + <summary> + Set the value inside the entry. + </summary> + <typeparam name="TValue">The typeof the value.</typeparam> + <param name="value">The value.</param> + </member> + <member name="P:ZiggyCreatures.Caching.Fusion.Internals.IFusionCacheEntry.Metadata"> + <summary> + Metadata about the cache entry. + </summary> + </member> + <member name="P:ZiggyCreatures.Caching.Fusion.Internals.IFusionCacheEntry.Timestamp"> + <summary> + The timestamp (in ticks) at which the cached value has been originally created: memory cache entries created from distributed cache entries will have the exact same timestamp. + </summary> + </member> + <member name="T:ZiggyCreatures.Caching.Fusion.Internals.Memory.FusionCacheMemoryEntry"> + <summary> + An entry in a <see cref="T:ZiggyCreatures.Caching.Fusion.FusionCache"/> memory layer. + </summary> + </member> + <member name="M:ZiggyCreatures.Caching.Fusion.Internals.Memory.FusionCacheMemoryEntry.#ctor(System.Object,ZiggyCreatures.Caching.Fusion.Internals.FusionCacheEntryMetadata,System.Int64,System.Type)"> + <summary> + Creates a new instance. + </summary> + <param name="value">The actual value.</param> + <param name="metadata">The metadata for the entry.</param> + <param name="timestamp">The original timestamp of the entry, see <see cref="P:ZiggyCreatures.Caching.Fusion.Internals.Memory.FusionCacheMemoryEntry.Timestamp"/>.</param> + <param name="valueType">The type of the value in the cache entry (mainly used for serialization/deserialization).</param> + </member> + <member name="P:ZiggyCreatures.Caching.Fusion.Internals.Memory.FusionCacheMemoryEntry.Value"> + <inheritdoc/> + </member> + <member name="P:ZiggyCreatures.Caching.Fusion.Internals.Memory.FusionCacheMemoryEntry.Metadata"> + <inheritdoc/> + </member> + <member name="P:ZiggyCreatures.Caching.Fusion.Internals.Memory.FusionCacheMemoryEntry.Timestamp"> + <inheritdoc/> + </member> + <member name="M:ZiggyCreatures.Caching.Fusion.Internals.Memory.FusionCacheMemoryEntry.GetValue``1"> + <inheritdoc/> + </member> + <member name="M:ZiggyCreatures.Caching.Fusion.Internals.Memory.FusionCacheMemoryEntry.SetValue``1(``0)"> + <inheritdoc/> + </member> + <member name="M:ZiggyCreatures.Caching.Fusion.Internals.Memory.FusionCacheMemoryEntry.ToString"> + <inheritdoc/> + </member> + <member name="M:ZiggyCreatures.Caching.Fusion.Internals.Memory.FusionCacheMemoryEntry.CreateFromOptions(System.Object,ZiggyCreatures.Caching.Fusion.FusionCacheEntryOptions,System.Boolean,System.Nullable{System.DateTimeOffset},System.String,System.Nullable{System.Int64},System.Type)"> + <summary> + Creates a new <see cref="T:ZiggyCreatures.Caching.Fusion.Internals.Memory.FusionCacheMemoryEntry"/> instance from a value and some options. + </summary> + <param name="value">The value to be cached.</param> + <param name="options">The <see cref="T:ZiggyCreatures.Caching.Fusion.FusionCacheEntryOptions"/> object to configure the entry.</param> + <param name="isFromFailSafe">Indicates if the value comes from a fail-safe activation.</param> + <param name="lastModified">If provided, it's the last modified date of the entry: this may be used in the next refresh cycle (eg: with the use of the "If-Modified-Since" header in an http request) to check if the entry is changed, to avoid getting the entire value.</param> + <param name="etag">If provided, it's the ETag of the entry: this may be used in the next refresh cycle (eg: with the use of the "If-None-Match" header in an http request) to check if the entry is changed, to avoid getting the entire value.</param> + <param name="timestamp">The value for the <see cref="P:ZiggyCreatures.Caching.Fusion.Internals.Memory.FusionCacheMemoryEntry.Timestamp"/> property.</param> + <param name="valueType">The type of the value in the cache entry (mainly used for serialization/deserialization).</param> + <returns>The newly created entry.</returns> + </member> + <member name="M:ZiggyCreatures.Caching.Fusion.Internals.Memory.FusionCacheMemoryEntry.CreateFromOtherEntry``1(ZiggyCreatures.Caching.Fusion.Internals.IFusionCacheEntry,ZiggyCreatures.Caching.Fusion.FusionCacheEntryOptions)"> + <summary> + Creates a new <see cref="T:ZiggyCreatures.Caching.Fusion.Internals.Memory.FusionCacheMemoryEntry"/> instance from another entry and some options. + </summary> + <param name="entry">The source entry.</param> + <param name="options">The <see cref="T:ZiggyCreatures.Caching.Fusion.FusionCacheEntryOptions"/> object to configure the entry.</param> + <returns>The newly created entry.</returns> + </member> + <member name="T:ZiggyCreatures.Caching.Fusion.Internals.RunUtils"> <summary> A set of utility methods to deal with sync/async execution of actions/functions, with support for timeouts, fire-and-forget execution, etc. </summary> </member> - <member name="M:ZiggyCreatures.Caching.Fusion.Internals.FusionCacheExecutionUtils.RunAsyncFuncWithTimeoutAsync``1(System.Func{System.Threading.CancellationToken,System.Threading.Tasks.Task{``0}},System.TimeSpan,System.Boolean,System.Action{System.Threading.Tasks.Task{``0}},System.Threading.CancellationToken)"> + <member name="M:ZiggyCreatures.Caching.Fusion.Internals.RunUtils.RunAsyncFuncWithTimeoutAsync``1(System.Func{System.Threading.CancellationToken,System.Threading.Tasks.Task{``0}},System.TimeSpan,System.Boolean,System.Action{System.Threading.Tasks.Task{``0}},System.Threading.CancellationToken)"> <summary> Run an async function asynchronously with a timeout and some additional options. </summary> @@ -2935,7 +3250,7 @@ <param name="token">An optional <see cref="T:System.Threading.CancellationToken"/> to cancel the operation.</param> <returns>The resulting <see cref="T:System.Threading.Tasks.Task"/> to await</returns> </member> - <member name="M:ZiggyCreatures.Caching.Fusion.Internals.FusionCacheExecutionUtils.RunAsyncActionWithTimeoutAsync(System.Func{System.Threading.CancellationToken,System.Threading.Tasks.Task},System.TimeSpan,System.Boolean,System.Action{System.Threading.Tasks.Task},System.Threading.CancellationToken)"> + <member name="M:ZiggyCreatures.Caching.Fusion.Internals.RunUtils.RunAsyncActionWithTimeoutAsync(System.Func{System.Threading.CancellationToken,System.Threading.Tasks.Task},System.TimeSpan,System.Boolean,System.Action{System.Threading.Tasks.Task},System.Threading.CancellationToken)"> <summary> Run an async action asynchronously with a timeout and some additional options. </summary> @@ -2946,7 +3261,7 @@ <param name="token">An optional <see cref="T:System.Threading.CancellationToken"/> to cancel the operation.</param> <returns>The resulting <see cref="T:System.Threading.Tasks.Task"/> to await</returns> </member> - <member name="M:ZiggyCreatures.Caching.Fusion.Internals.FusionCacheExecutionUtils.RunAsyncFuncWithTimeout``1(System.Func{System.Threading.CancellationToken,System.Threading.Tasks.Task{``0}},System.TimeSpan,System.Boolean,System.Action{System.Threading.Tasks.Task{``0}},System.Threading.CancellationToken)"> + <member name="M:ZiggyCreatures.Caching.Fusion.Internals.RunUtils.RunAsyncFuncWithTimeout``1(System.Func{System.Threading.CancellationToken,System.Threading.Tasks.Task{``0}},System.TimeSpan,System.Boolean,System.Action{System.Threading.Tasks.Task{``0}},System.Threading.CancellationToken)"> <summary> Run an async function synchoronously with a timeout and some additional options. </summary> @@ -2958,7 +3273,7 @@ <param name="token">An optional <see cref="T:System.Threading.CancellationToken"/> to cancel the operation.</param> <returns>The value returned from the async function</returns> </member> - <member name="M:ZiggyCreatures.Caching.Fusion.Internals.FusionCacheExecutionUtils.RunAsyncActionWithTimeout(System.Func{System.Threading.CancellationToken,System.Threading.Tasks.Task},System.TimeSpan,System.Boolean,System.Action{System.Threading.Tasks.Task},System.Threading.CancellationToken)"> + <member name="M:ZiggyCreatures.Caching.Fusion.Internals.RunUtils.RunAsyncActionWithTimeout(System.Func{System.Threading.CancellationToken,System.Threading.Tasks.Task},System.TimeSpan,System.Boolean,System.Action{System.Threading.Tasks.Task},System.Threading.CancellationToken)"> <summary> Run an async action synchoronously with a timeout and some additional options. </summary> @@ -2968,7 +3283,7 @@ <param name="timedOutTaskProcessor">A lambda to process the task representing the eventually timed out action.</param> <param name="token">An optional <see cref="T:System.Threading.CancellationToken"/> to cancel the operation.</param> </member> - <member name="M:ZiggyCreatures.Caching.Fusion.Internals.FusionCacheExecutionUtils.RunSyncFuncWithTimeout``1(System.Func{System.Threading.CancellationToken,``0},System.TimeSpan,System.Boolean,System.Action{System.Threading.Tasks.Task{``0}},System.Threading.CancellationToken)"> + <member name="M:ZiggyCreatures.Caching.Fusion.Internals.RunUtils.RunSyncFuncWithTimeout``1(System.Func{System.Threading.CancellationToken,``0},System.TimeSpan,System.Boolean,System.Action{System.Threading.Tasks.Task{``0}},System.Threading.CancellationToken)"> <summary> Run a sync function synchoronously with a timeout and some additional options. </summary> @@ -2980,7 +3295,7 @@ <param name="token">An optional <see cref="T:System.Threading.CancellationToken"/> to cancel the operation.</param> <returns>The value returned from the sync function</returns> </member> - <member name="M:ZiggyCreatures.Caching.Fusion.Internals.FusionCacheExecutionUtils.RunSyncActionWithTimeout(System.Action{System.Threading.CancellationToken},System.TimeSpan,System.Boolean,System.Action{System.Threading.Tasks.Task},System.Threading.CancellationToken)"> + <member name="M:ZiggyCreatures.Caching.Fusion.Internals.RunUtils.RunSyncActionWithTimeout(System.Action{System.Threading.CancellationToken},System.TimeSpan,System.Boolean,System.Action{System.Threading.Tasks.Task},System.Threading.CancellationToken)"> <summary> Run a sync action synchoronously with a timeout and some additional ooptions. </summary> @@ -2990,7 +3305,7 @@ <param name="timedOutTaskProcessor">A lambda to process the task representing the eventually timed out action.</param> <param name="token">An optional <see cref="T:System.Threading.CancellationToken"/> to cancel the operation.</param> </member> - <member name="M:ZiggyCreatures.Caching.Fusion.Internals.FusionCacheExecutionUtils.RunAsyncActionAdvancedAsync(System.Func{System.Threading.CancellationToken,System.Threading.Tasks.Task},System.TimeSpan,System.Boolean,System.Boolean,System.Action{System.Exception},System.Boolean,System.Threading.CancellationToken)"> + <member name="M:ZiggyCreatures.Caching.Fusion.Internals.RunUtils.RunAsyncActionAdvancedAsync(System.Func{System.Threading.CancellationToken,System.Threading.Tasks.Task},System.TimeSpan,System.Boolean,System.Boolean,System.Action{System.Exception},System.Boolean,System.Threading.CancellationToken)"> <summary> Run an async function with the ability to optionally set a timeout, await its completion (or run in a fire-and-forget way), process the eventually thrown exception or re-throw it. </summary> @@ -3003,7 +3318,7 @@ <param name="token">An optional <see cref="T:System.Threading.CancellationToken"/> to cancel the operation.</param> <returns>The resulting <see cref="T:System.Threading.Tasks.Task"/> to await</returns> </member> - <member name="M:ZiggyCreatures.Caching.Fusion.Internals.FusionCacheExecutionUtils.RunSyncActionAdvanced(System.Action{System.Threading.CancellationToken},System.TimeSpan,System.Boolean,System.Boolean,System.Action{System.Exception},System.Boolean,System.Threading.CancellationToken)"> + <member name="M:ZiggyCreatures.Caching.Fusion.Internals.RunUtils.RunSyncActionAdvanced(System.Action{System.Threading.CancellationToken},System.TimeSpan,System.Boolean,System.Boolean,System.Action{System.Exception},System.Boolean,System.Threading.CancellationToken)"> <summary> Run a sync action with the ability to optionally set a timeout, await its completion (or run in a fire-and-forget way), process the eventually thrown exception or re-throw it. </summary> @@ -3015,92 +3330,6 @@ <param name="reThrow">Indicates if, in case an exception is intercepted, it should be re-thrown.</param> <param name="token">An optional <see cref="T:System.Threading.CancellationToken"/> to cancel the operation.</param> </member> - <member name="M:ZiggyCreatures.Caching.Fusion.Internals.FusionCacheInternalUtils.IsLogicallyExpired(ZiggyCreatures.Caching.Fusion.Internals.IFusionCacheEntry)"> - <summary> - Checks if the entry is logically expired. - </summary> - <returns>A <see cref="T:System.Boolean"/> indicating the logical expiration status.</returns> - </member> - <member name="T:ZiggyCreatures.Caching.Fusion.Internals.IFusionCacheEntry"> - <summary> - Represents an generic entry in a <see cref="T:ZiggyCreatures.Caching.Fusion.FusionCache"/>, which can be either a <see cref="T:ZiggyCreatures.Caching.Fusion.Internals.Memory.FusionCacheMemoryEntry"/> or a <see cref="T:ZiggyCreatures.Caching.Fusion.Internals.Distributed.FusionCacheDistributedEntry`1"/>. - </summary> - </member> - <member name="M:ZiggyCreatures.Caching.Fusion.Internals.IFusionCacheEntry.GetValue``1"> - <summary> - Get the value inside the entry. - </summary> - <typeparam name="TValue">The typeof the value.</typeparam> - <returns>The value.</returns> - </member> - <member name="M:ZiggyCreatures.Caching.Fusion.Internals.IFusionCacheEntry.SetValue``1(``0)"> - <summary> - Set the value inside the entry. - </summary> - <typeparam name="TValue">The typeof the value.</typeparam> - <param name="value">The value.</param> - </member> - <member name="P:ZiggyCreatures.Caching.Fusion.Internals.IFusionCacheEntry.Metadata"> - <summary> - Metadata about the cache entry. - </summary> - </member> - <member name="P:ZiggyCreatures.Caching.Fusion.Internals.IFusionCacheEntry.Timestamp"> - <summary> - The timestamp at which the cached value has been originally created: memory cache entries created from distributed cache entries will have the exact same timestamp. - </summary> - </member> - <member name="T:ZiggyCreatures.Caching.Fusion.Internals.Memory.FusionCacheMemoryEntry"> - <summary> - An entry in a <see cref="T:ZiggyCreatures.Caching.Fusion.FusionCache"/> memory layer. - </summary> - </member> - <member name="M:ZiggyCreatures.Caching.Fusion.Internals.Memory.FusionCacheMemoryEntry.#ctor(System.Object,ZiggyCreatures.Caching.Fusion.Internals.FusionCacheEntryMetadata,System.Nullable{System.Int64})"> - <summary> - Creates a new instance. - </summary> - <param name="value">The actual value.</param> - <param name="metadata">The metadata for the entry.</param> - <param name="timestamp">The original timestamp of the entry, see <see cref="P:ZiggyCreatures.Caching.Fusion.Internals.Memory.FusionCacheMemoryEntry.Timestamp"/>.</param> - </member> - <member name="P:ZiggyCreatures.Caching.Fusion.Internals.Memory.FusionCacheMemoryEntry.Value"> - <inheritdoc/> - </member> - <member name="P:ZiggyCreatures.Caching.Fusion.Internals.Memory.FusionCacheMemoryEntry.Metadata"> - <inheritdoc/> - </member> - <member name="P:ZiggyCreatures.Caching.Fusion.Internals.Memory.FusionCacheMemoryEntry.Timestamp"> - <inheritdoc/> - </member> - <member name="M:ZiggyCreatures.Caching.Fusion.Internals.Memory.FusionCacheMemoryEntry.GetValue``1"> - <inheritdoc/> - </member> - <member name="M:ZiggyCreatures.Caching.Fusion.Internals.Memory.FusionCacheMemoryEntry.SetValue``1(``0)"> - <inheritdoc/> - </member> - <member name="M:ZiggyCreatures.Caching.Fusion.Internals.Memory.FusionCacheMemoryEntry.ToString"> - <inheritdoc/> - </member> - <member name="M:ZiggyCreatures.Caching.Fusion.Internals.Memory.FusionCacheMemoryEntry.CreateFromOptions(System.Object,ZiggyCreatures.Caching.Fusion.FusionCacheEntryOptions,System.Boolean,System.Nullable{System.DateTimeOffset},System.String,System.Nullable{System.Int64})"> - <summary> - Creates a new <see cref="T:ZiggyCreatures.Caching.Fusion.Internals.Memory.FusionCacheMemoryEntry"/> instance from a value and some options. - </summary> - <param name="value">The value to be cached.</param> - <param name="options">The <see cref="T:ZiggyCreatures.Caching.Fusion.FusionCacheEntryOptions"/> object to configure the entry.</param> - <param name="isFromFailSafe">Indicates if the value comes from a fail-safe activation.</param> - <param name="lastModified">If provided, it's the last modified date of the entry: this may be used in the next refresh cycle (eg: with the use of the "If-Modified-Since" header in an http request) to check if the entry is changed, to avoid getting the entire value.</param> - <param name="etag">If provided, it's the ETag of the entry: this may be used in the next refresh cycle (eg: with the use of the "If-None-Match" header in an http request) to check if the entry is changed, to avoid getting the entire value.</param> - <param name="timestamp">The value for the <see cref="P:ZiggyCreatures.Caching.Fusion.Internals.Memory.FusionCacheMemoryEntry.Timestamp"/> property.</param> - <returns>The newly created entry.</returns> - </member> - <member name="M:ZiggyCreatures.Caching.Fusion.Internals.Memory.FusionCacheMemoryEntry.CreateFromOtherEntry``1(ZiggyCreatures.Caching.Fusion.Internals.IFusionCacheEntry,ZiggyCreatures.Caching.Fusion.FusionCacheEntryOptions)"> - <summary> - Creates a new <see cref="T:ZiggyCreatures.Caching.Fusion.Internals.Memory.FusionCacheMemoryEntry"/> instance from another entry and some options. - </summary> - <param name="entry">The source entry.</param> - <param name="options">The <see cref="T:ZiggyCreatures.Caching.Fusion.FusionCacheEntryOptions"/> object to configure the entry.</param> - <returns>The newly created entry.</returns> - </member> <member name="T:ZiggyCreatures.Caching.Fusion.Internals.SimpleCircuitBreaker"> <summary> A simple, reusable circuit-breaker. @@ -3399,11 +3628,12 @@ Represents one of the core pieces of an instance of an <see cref="T:ZiggyCreatures.Caching.Fusion.FusionCache"/>, dealing with acquiring and releasing locks in a highly optimized way. </summary> </member> - <member name="M:ZiggyCreatures.Caching.Fusion.Reactors.IFusionCacheReactor.AcquireLockAsync(System.String,System.String,System.String,System.TimeSpan,Microsoft.Extensions.Logging.ILogger,System.Threading.CancellationToken)"> + <member name="M:ZiggyCreatures.Caching.Fusion.Reactors.IFusionCacheReactor.AcquireLockAsync(System.String,System.String,System.String,System.String,System.TimeSpan,Microsoft.Extensions.Logging.ILogger,System.Threading.CancellationToken)"> <summary> Acquire a generic lock, used to synchronize multiple factory operating on the same cache key, and return it. </summary> - <param name="cacheName">The name of the FusionCache instance.</param> + <param name="cacheName">The CacheName of the FusionCache instance.</param> + <param name="cacheInstanceId">The InstanceId of the FusionCache instance.</param> <param name="key">The key for which to obtain a lock.</param> <param name="operationId">The operation id which uniquely identifies a high-level cache operation.</param> <param name="timeout">The optional timeout for the lock acquisition.</param> @@ -3411,22 +3641,24 @@ <returns>The acquired genericlock object, later released when the critical section is over.</returns> <param name="token">An optional <see cref="T:System.Threading.CancellationToken"/> to cancel the operation.</param> </member> - <member name="M:ZiggyCreatures.Caching.Fusion.Reactors.IFusionCacheReactor.AcquireLock(System.String,System.String,System.String,System.TimeSpan,Microsoft.Extensions.Logging.ILogger)"> + <member name="M:ZiggyCreatures.Caching.Fusion.Reactors.IFusionCacheReactor.AcquireLock(System.String,System.String,System.String,System.String,System.TimeSpan,Microsoft.Extensions.Logging.ILogger)"> <summary> Acquire a generic lock, used to synchronize multiple factory operating on the same cache key, and return it. </summary> <param name="cacheName">The name of the FusionCache instance.</param> + <param name="cacheInstanceId">The InstanceId of the FusionCache instance.</param> <param name="key">The key for which to obtain a lock.</param> <param name="operationId">The operation id which uniquely identifies a high-level cache operation.</param> <param name="timeout">The optional timeout for the lock acquisition.</param> <returns>The acquired genericlock object, later released when the critical section is over.</returns> <param name="logger">The <see cref="T:Microsoft.Extensions.Logging.ILogger"/> to use, if any.</param> </member> - <member name="M:ZiggyCreatures.Caching.Fusion.Reactors.IFusionCacheReactor.ReleaseLock(System.String,System.String,System.String,System.Object,Microsoft.Extensions.Logging.ILogger)"> + <member name="M:ZiggyCreatures.Caching.Fusion.Reactors.IFusionCacheReactor.ReleaseLock(System.String,System.String,System.String,System.String,System.Object,Microsoft.Extensions.Logging.ILogger)"> <summary> Release the generic lock object. </summary> <param name="cacheName">The name of the FusionCache instance.</param> + <param name="cacheInstanceId">The InstanceId of the FusionCache instance.</param> <param name="key">The key for which to obtain a lock.</param> <param name="operationId">The operation id which uniquely identifies a high-level cache operation.</param> <param name="lockObj">The generic lock object to release.</param> @@ -3434,7 +3666,7 @@ </member> <member name="P:ZiggyCreatures.Caching.Fusion.Reactors.IFusionCacheReactor.Collisions"> <summary> - Exposes the eventual amount ofcollisions happened inside the reactor, for diagnostics purposes. + Exposes the eventual amount of collisions happened inside the reactor, for diagnostics purposes. </summary> </member> <member name="T:ZiggyCreatures.Caching.Fusion.Serialization.IFusionCacheSerializer"> diff --git a/src/ZiggyCreatures.FusionCache/docs/README.md b/src/ZiggyCreatures.FusionCache/docs/README.md index ceb99238..8b1d786b 100644 --- a/src/ZiggyCreatures.FusionCache/docs/README.md +++ b/src/ZiggyCreatures.FusionCache/docs/README.md @@ -2,10 +2,10 @@  -| 🙋♂️ Updating from before `v0.20.0` ? please [read here](https://github.com/ZiggyCreatures/FusionCache/blob/main/docs/Update_v0_20_0.md). | +| 🙋♂️ 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 layer. 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. @@ -29,6 +29,8 @@ Want to start using it immediately? There's a [⭐ Quick Start](https://github.c Curious about what you can achieve from start to finish? There's a [👩🏫 Step By Step ](https://github.com/ZiggyCreatures/FusionCache/blob/main/docs/StepByStep.md) guide. +In search of all the docs? There's a [page](https://github.com/ZiggyCreatures/FusionCache/blob/main/docs/README.md) for that, too. + More into videos? The great Anna Hoffman has been so nice to listen to me mumble random stuff on [Data Exposed](https://learn.microsoft.com/en-us/shows/data-exposed/caching-made-easy-in-azure-sql-db-with-fusioncache-data-exposed). [](https://www.youtube.com/watch?v=V2fCUoJgVAo) @@ -40,10 +42,11 @@ These are the **key features** of FusionCache: - [**🔀 Optional 2nd level**](https://github.com/ZiggyCreatures/FusionCache/blob/main/docs/CacheLevels.md): an optional 2nd level handled transparently, with any implementation of `IDistributedCache` - [**💣 Fail-Safe**](https://github.com/ZiggyCreatures/FusionCache/blob/main/docs/FailSafe.md): a mechanism to avoids transient failures, by reusing an expired entry as a temporary fallback - [**⏱ Soft/Hard timeouts**](https://github.com/ZiggyCreatures/FusionCache/blob/main/docs/Timeouts.md): a slow factory (or distributed cache) will not slow down your application, and no data will be wasted +- [**📢 Backplane**](https://github.com/ZiggyCreatures/FusionCache/blob/main/docs/Backplane.md): in a multi-node scenario, it can notify the other nodes about changes in the cache, so all will be in-sync +- [**↩️ Auto-Recovery**](https://github.com/ZiggyCreatures/FusionCache/blob/main/docs/AutoRecovery.md): automatic handling of transient issues with retries and sync logic - [**🧙♂️ Adaptive Caching**](https://github.com/ZiggyCreatures/FusionCache/blob/main/docs/AdaptiveCaching.md): for when you don't know upfront the cache duration, as it depends on the value being cached itself - [**🔂 Conditional Refresh**](https://github.com/ZiggyCreatures/FusionCache/blob/main/docs/ConditionalRefresh.md): like HTTP Conditional Requests, but for caching - [**🦅 Eager Refresh**](https://github.com/ZiggyCreatures/FusionCache/blob/main/docs/EagerRefresh.md): start a non-blocking background refresh before the expiration occurs -- [**📢 Backplane**](https://github.com/ZiggyCreatures/FusionCache/blob/main/docs/Backplane.md): in a multi-node scenario, it can notify the other nodes about changes in the cache, so all will be in-sync - [**🔃 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 - [**💫 Natively sync/async**](https://github.com/ZiggyCreatures/FusionCache/blob/main/docs/CoreMethods.md): native support for both the synchronous and asynchronous programming model @@ -117,14 +120,25 @@ cache.GetOrSet<Product>( That's it 🎉 +## 🖥️ Simulator -## 📖 Documentation +Distributed systems are, in general, quite complex to understand. -A complete documentation, including examples and common use cases, is available at the [official repo](https://github.com/ZiggyCreatures/FusionCache) page on GitHub. +When using FusionCache with the [distributed cache](https://github.com/ZiggyCreatures/FusionCache/blob/main/docs/CacheLevels.md), the [backplane](https://github.com/ZiggyCreatures/FusionCache/blob/main/docs/Backplane.md) and [auto-recovery](https://github.com/ZiggyCreatures/FusionCache/blob/main/docs/AutoRecovery.md) the Simulator can help us **seeing** the whole picture. +[](docs/Simulator.md) ## 🧰 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+` (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. \ No newline at end of file +**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. + +## 💼 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. + +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/SerializerPayloadGenerator/Program.cs b/tests/SerializerPayloadGenerator/Program.cs index 4bb57ca9..934cee02 100644 --- a/tests/SerializerPayloadGenerator/Program.cs +++ b/tests/SerializerPayloadGenerator/Program.cs @@ -19,8 +19,8 @@ new FusionCacheServiceStackJsonSerializer() }; -GenerateSamples(serializers, CreateEntry()); -//TestSamples<FusionCacheDistributedEntry<string>>(serializers); +//GenerateSamples(serializers, CreateEntry()); +TestSamples<FusionCacheDistributedEntry<string>>(serializers); static void TestSamples<T>(IFusionCacheSerializer[] serializers) { @@ -69,7 +69,7 @@ static FusionCacheDistributedEntry<string> CreateEntry() , "MyETagValue" , logicalExpiration.AddDays(-100) ), - DateTimeOffset.UtcNow.ToUnixTimeMilliseconds() + DateTimeOffset.UtcNow.Ticks ); } diff --git a/tests/SerializerPayloadGenerator/Samples/fusioncachecysharpmemorypackserializer__v0_23_0_0.bin b/tests/SerializerPayloadGenerator/Samples/fusioncachecysharpmemorypackserializer__v0_23_0_0.bin new file mode 100644 index 00000000..c4a8e7f7 Binary files /dev/null and b/tests/SerializerPayloadGenerator/Samples/fusioncachecysharpmemorypackserializer__v0_23_0_0.bin differ diff --git a/tests/SerializerPayloadGenerator/Samples/fusioncacheneueccmessagepackserializer__v0_23_0_0.bin b/tests/SerializerPayloadGenerator/Samples/fusioncacheneueccmessagepackserializer__v0_23_0_0.bin new file mode 100644 index 00000000..5f0bee5b Binary files /dev/null and b/tests/SerializerPayloadGenerator/Samples/fusioncacheneueccmessagepackserializer__v0_23_0_0.bin differ diff --git a/tests/SerializerPayloadGenerator/Samples/fusioncachenewtonsoftjsonserializer__v0_23_0_0.bin b/tests/SerializerPayloadGenerator/Samples/fusioncachenewtonsoftjsonserializer__v0_23_0_0.bin new file mode 100644 index 00000000..5069d84f --- /dev/null +++ b/tests/SerializerPayloadGenerator/Samples/fusioncachenewtonsoftjsonserializer__v0_23_0_0.bin @@ -0,0 +1 @@ +{"v":"Sloths are cool!","m":{"e":"2033-07-08T14:53:13.4520833+00:00","f":true,"ea":"2033-06-28T14:53:13.4520833+00:00","et":"MyETagValue","lm":"2033-03-30T14:53:13.4520833+00:00"},"t":638310588939961396} \ No newline at end of file diff --git a/tests/SerializerPayloadGenerator/Samples/fusioncacheprotobufnetserializer__v0_23_0_0.bin b/tests/SerializerPayloadGenerator/Samples/fusioncacheprotobufnetserializer__v0_23_0_0.bin new file mode 100644 index 00000000..621206cd --- /dev/null +++ b/tests/SerializerPayloadGenerator/Samples/fusioncacheprotobufnetserializer__v0_23_0_0.bin @@ -0,0 +1,2 @@ + +Sloths are cool!-��Ψ�ɭ���������"MyETagValue(����Ϋ��������� \ No newline at end of file diff --git a/tests/SerializerPayloadGenerator/Samples/fusioncacheservicestackjsonserializer__v0_23_0_0.bin b/tests/SerializerPayloadGenerator/Samples/fusioncacheservicestackjsonserializer__v0_23_0_0.bin new file mode 100644 index 00000000..5069d84f --- /dev/null +++ b/tests/SerializerPayloadGenerator/Samples/fusioncacheservicestackjsonserializer__v0_23_0_0.bin @@ -0,0 +1 @@ +{"v":"Sloths are cool!","m":{"e":"2033-07-08T14:53:13.4520833+00:00","f":true,"ea":"2033-06-28T14:53:13.4520833+00:00","et":"MyETagValue","lm":"2033-03-30T14:53:13.4520833+00:00"},"t":638310588939961396} \ No newline at end of file diff --git a/tests/SerializerPayloadGenerator/Samples/fusioncachesystemtextjsonserializer__v0_23_0_0.bin b/tests/SerializerPayloadGenerator/Samples/fusioncachesystemtextjsonserializer__v0_23_0_0.bin new file mode 100644 index 00000000..2f571048 --- /dev/null +++ b/tests/SerializerPayloadGenerator/Samples/fusioncachesystemtextjsonserializer__v0_23_0_0.bin @@ -0,0 +1 @@ +{"Value":"Sloths are cool!","Metadata":{"LogicalExpiration":"2033-07-08T14:53:13.4520833+00:00","IsFromFailSafe":true,"EagerExpiration":"2033-06-28T14:53:13.4520833+00:00","ETag":"MyETagValue","LastModified":"2033-03-30T14:53:13.4520833+00:00"},"Timestamp":638310588939961396} \ No newline at end of file diff --git a/tests/SerializerPayloadGenerator/SerializerPayloadGenerator.csproj b/tests/SerializerPayloadGenerator/SerializerPayloadGenerator.csproj index 1e7ab4c0..ad934181 100644 --- a/tests/SerializerPayloadGenerator/SerializerPayloadGenerator.csproj +++ b/tests/SerializerPayloadGenerator/SerializerPayloadGenerator.csproj @@ -2,18 +2,18 @@ <PropertyGroup> <OutputType>Exe</OutputType> - <TargetFramework>net6.0</TargetFramework> + <TargetFramework>net7.0</TargetFramework> <ImplicitUsings>enable</ImplicitUsings> <Nullable>enable</Nullable> </PropertyGroup> <ItemGroup> - <PackageReference Include="ZiggyCreatures.FusionCache.Serialization.CysharpMemoryPack" Version="0.22.0" /> - <PackageReference Include="ZiggyCreatures.FusionCache.Serialization.NeueccMessagePack" Version="0.22.0" /> - <PackageReference Include="ZiggyCreatures.FusionCache.Serialization.NewtonsoftJson" Version="0.22.0" /> - <PackageReference Include="ZiggyCreatures.FusionCache.Serialization.ProtoBufNet" Version="0.22.0" /> - <PackageReference Include="ZiggyCreatures.FusionCache.Serialization.ServiceStackJson" Version="0.22.0" /> - <PackageReference Include="ZiggyCreatures.FusionCache.Serialization.SystemTextJson" Version="0.22.0" /> + <PackageReference Include="ZiggyCreatures.FusionCache.Serialization.CysharpMemoryPack" Version="0.23.0" /> + <PackageReference Include="ZiggyCreatures.FusionCache.Serialization.NeueccMessagePack" Version="0.23.0" /> + <PackageReference Include="ZiggyCreatures.FusionCache.Serialization.NewtonsoftJson" Version="0.23.0" /> + <PackageReference Include="ZiggyCreatures.FusionCache.Serialization.ProtoBufNet" Version="0.23.0" /> + <PackageReference Include="ZiggyCreatures.FusionCache.Serialization.ServiceStackJson" Version="0.23.0" /> + <PackageReference Include="ZiggyCreatures.FusionCache.Serialization.SystemTextJson" Version="0.23.0" /> </ItemGroup> <ItemGroup> diff --git a/tests/ZiggyCreatures.FusionCache.Playground/Program.cs b/tests/ZiggyCreatures.FusionCache.Playground/Program.cs index ed1b7dd0..ee25fb10 100644 --- a/tests/ZiggyCreatures.FusionCache.Playground/Program.cs +++ b/tests/ZiggyCreatures.FusionCache.Playground/Program.cs @@ -8,7 +8,6 @@ class Program static async Task Main(string[] args) { await LoggingScenario.RunAsync().ConfigureAwait(false); - //await WorkloadScenario.RunAsync().ConfigureAwait(false); } } } diff --git a/tests/ZiggyCreatures.FusionCache.Playground/Scenarios/LoggingScenario.cs b/tests/ZiggyCreatures.FusionCache.Playground/Scenarios/LoggingScenario.cs index 4d395798..0d6420bc 100644 --- a/tests/ZiggyCreatures.FusionCache.Playground/Scenarios/LoggingScenario.cs +++ b/tests/ZiggyCreatures.FusionCache.Playground/Scenarios/LoggingScenario.cs @@ -65,6 +65,8 @@ private static void SetupStandardLogger(IServiceCollection services, LogLevel mi public static async Task RunAsync() { + Console.Title = "FusionCache - Logging"; + Console.OutputEncoding = Encoding.UTF8; // DI diff --git a/tests/ZiggyCreatures.FusionCache.Playground/Scenarios/WorkloadScenario.cs b/tests/ZiggyCreatures.FusionCache.Playground/Scenarios/WorkloadScenario.cs deleted file mode 100644 index ea4da5f2..00000000 --- a/tests/ZiggyCreatures.FusionCache.Playground/Scenarios/WorkloadScenario.cs +++ /dev/null @@ -1,373 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Threading; -using System.Threading.Tasks; -using Microsoft.Extensions.Caching.Distributed; -using Microsoft.Extensions.Caching.Memory; -using Microsoft.Extensions.Caching.StackExchangeRedis; -using Microsoft.Extensions.Options; -using Spectre.Console; -using Spectre.Console.Rendering; -using ZiggyCreatures.Caching.Fusion.Backplane; -using ZiggyCreatures.Caching.Fusion.Backplane.Memory; -using ZiggyCreatures.Caching.Fusion.Backplane.StackExchangeRedis; -using ZiggyCreatures.Caching.Fusion.Serialization.NewtonsoftJson; - -namespace ZiggyCreatures.Caching.Fusion.Playground.Scenarios -{ - public enum DistributedCacheType - { - None = 0, - Memory = 1, - Redis = 2 - } - - public enum BackplaneType - { - None = 0, - Memory = 1, - Redis = 2 - } - - public static class WorkloadScenarioOptions - { - // GENERAL - public static readonly int GroupsCount = 4; - public static readonly int NodesPerGroupCount = 10; - public static readonly TimeSpan CacheDuration = TimeSpan.FromSeconds(10); - public static readonly DistributedCacheType DistributedCacheType = DistributedCacheType.Memory; - public static readonly BackplaneType BackplaneType = BackplaneType.Memory; - - // DISTRIBUTED CACHE - public static readonly bool AllowBackgroundDistributedCacheOperations = false; - public static readonly TimeSpan? DistributedCacheSoftTimeout = TimeSpan.FromMilliseconds(100); - public static readonly TimeSpan? DistributedCacheHardTimeout = TimeSpan.FromMilliseconds(100); - public static readonly string DistributedCacheRedisConnection = "127.0.0.1:6379,ssl=False,abortConnect=False,defaultDatabase={0}"; - - // BACKPLANE - public static readonly bool AllowBackgroundBackplaneOperations = false; - public static readonly TimeSpan BackplaneCircuitBreakerDuration = TimeSpan.FromSeconds(10); - public static readonly string BackplaneRedisConnection = "127.0.0.1:6379,ssl=False,abortConnect=False,defaultDatabase={0}"; - - // OTHERS - public static readonly TimeSpan DataChangesMinDelay = TimeSpan.FromSeconds(1); - public static readonly TimeSpan DataChangesMaxDelay = TimeSpan.FromSeconds(1); - public static readonly bool UpdateCacheOnSaveToDb = true; - public static readonly TimeSpan? PostUpdateCooldownDelay = TimeSpan.FromMilliseconds(150); - } - - public static class WorkloadScenario - { - // INTERNAL - private static string CacheKey = "foo"; - private static readonly Random RNG = new Random(); - private static readonly object LockObj = new object(); - private static int LastValue = 0; - private static int? LastUpdatedGroupIdx = null; - private static readonly Dictionary<int, int?> LastUpdatedCaches = new Dictionary<int, int?>(); - private static readonly List<List<IFusionCache>> CacheGroups = new List<List<IFusionCache>>(); - private static readonly Dictionary<int, int?> Databases = new Dictionary<int, int?>(); - - // STATS - private static int DbWritesCount = 0; - private static int DbReadsCount = 0; - - private static IDistributedCache? CreateDistributedCache(int groupIdx) - { - switch (WorkloadScenarioOptions.DistributedCacheType) - { - case DistributedCacheType.None: - return null; - case DistributedCacheType.Redis: - return new RedisCache(new RedisCacheOptions - { - Configuration = string.Format(WorkloadScenarioOptions.DistributedCacheRedisConnection, groupIdx) - }); - default: - return new MemoryDistributedCache(Options.Create(new MemoryDistributedCacheOptions())); - } - } - - private static IFusionCacheBackplane? CreateBackplane(int groupIdx) - { - switch (WorkloadScenarioOptions.BackplaneType) - { - case BackplaneType.None: - return null; - case BackplaneType.Redis: - return new RedisBackplane(new RedisBackplaneOptions - { - Configuration = string.Format(WorkloadScenarioOptions.BackplaneRedisConnection, groupIdx), - //CircuitBreakerDuration = WorkloadScenarioOptions.BackplaneCircuitBreakerDuration, - //AllowBackgroundOperations = WorkloadScenarioOptions.AllowBackplaneBackgroundOperations - }); - default: - return new MemoryBackplane(new MemoryBackplaneOptions() { ConnectionId = $"connection-{groupIdx}" }); - } - } - - private static void SaveToDb(int groupIdx, int value) - { - Databases[groupIdx] = value; - - Interlocked.Increment(ref DbWritesCount); - } - - private static int? LoadFromDb(int groupIdx) - { - Databases.TryGetValue(groupIdx, out int? res); - - Interlocked.Increment(ref DbReadsCount); - - return res; - } - - private static void Setup() - { - AnsiConsole.MarkupLine("[deepskyblue1]SETUP[/]"); - AnsiConsole.Markup("- [deepskyblue1]SERIALIZER : [/] CREATING..."); - AnsiConsole.MarkupLine("[green3_1]OK[/]"); - - for (int groupIdx = 1; groupIdx <= WorkloadScenarioOptions.GroupsCount; groupIdx++) - { - AnsiConsole.Markup("- [deepskyblue1]DIST. CACHE : [/] CREATING..."); - var distributedCache = CreateDistributedCache(groupIdx); - AnsiConsole.MarkupLine("[green3_1]OK[/]"); - - var nodes = new List<IFusionCache>(); - - for (int nodeIdx = 1; nodeIdx <= WorkloadScenarioOptions.NodesPerGroupCount; nodeIdx++) - { - AnsiConsole.Markup("- [deepskyblue1]FUSION CACHE: [/] CREATING..."); - var options = new FusionCacheOptions() - { - CacheName = $"C{groupIdx}", - DefaultEntryOptions = new FusionCacheEntryOptions(WorkloadScenarioOptions.CacheDuration) - .SetFailSafe(false) - .SetDistributedCacheTimeouts( - WorkloadScenarioOptions.DistributedCacheSoftTimeout, - WorkloadScenarioOptions.DistributedCacheHardTimeout, - WorkloadScenarioOptions.AllowBackgroundDistributedCacheOperations - ) - }; - options.DefaultEntryOptions.AllowBackgroundBackplaneOperations = WorkloadScenarioOptions.AllowBackgroundBackplaneOperations; - options.BackplaneCircuitBreakerDuration = WorkloadScenarioOptions.BackplaneCircuitBreakerDuration; - if (WorkloadScenarioOptions.DistributedCacheType == DistributedCacheType.None && WorkloadScenarioOptions.BackplaneType != BackplaneType.None) - options.DefaultEntryOptions.SkipBackplaneNotifications = true; - - var cache = new FusionCache(options); - AnsiConsole.MarkupLine("[green3_1]OK[/]"); - - if (distributedCache is not null) - { - AnsiConsole.Markup("- [deepskyblue1]FUSION CACHE: [/] ADDING DIST. CACHE..."); - cache.SetupDistributedCache(distributedCache, new FusionCacheNewtonsoftJsonSerializer()); - AnsiConsole.MarkupLine("[green3_1]OK[/]"); - } - - AnsiConsole.Markup("- [deepskyblue1]BACKPLANE : [/] CREATING..."); - var backplane = CreateBackplane(groupIdx); - AnsiConsole.MarkupLine("[green3_1]OK[/]"); - if (backplane is not null) - { - AnsiConsole.Markup("- [deepskyblue1]FUSION CACHE: [/] ADDING BACKPLANE..."); - cache.SetupBackplane(backplane); - AnsiConsole.MarkupLine("[green3_1]OK[/]"); - } - - nodes.Add(cache); - } - - CacheGroups.Add(nodes); - } - } - - private static void DisplayDashboard() - { - var tables = new List<(string Label, Table Table)>(); - - lock (LockObj) - { - for (int groupIdx = 0; groupIdx < CacheGroups.Count; groupIdx++) - { - var nodes = CacheGroups[groupIdx]; - - var table = new Table(); - table.Border = TableBorder.Heavy; - - for (int nodeIdx = 0; nodeIdx < nodes.Count; nodeIdx++) - { - table.AddColumn(new TableColumn($"[deepskyblue1]N {nodeIdx + 1}[/]").Centered()); - } - - LastUpdatedCaches.TryGetValue(groupIdx, out int? lastUpdatedNodeIdx); - - // SNAPSHOT VALUES - var values = new Dictionary<int, int?>(); - for (int nodeIdx = 0; nodeIdx < nodes.Count; nodeIdx++) - { - var cache = nodes[nodeIdx]; - values[nodeIdx] = cache.GetOrSet<int?>(CacheKey, _ => LoadFromDb(groupIdx)); - } - - // BUILD CELLS - var cells = new List<IRenderable>(); - for (int nodeIdx = 0; nodeIdx < nodes.Count; nodeIdx++) - { - var value = values[nodeIdx]; - - var color = "white"; - if (lastUpdatedNodeIdx.HasValue) - { - if (lastUpdatedNodeIdx.Value == nodeIdx) - { - if (LastUpdatedGroupIdx == groupIdx) - color = "green3_1"; - else - color = "green4"; - } - else if (values[lastUpdatedNodeIdx.Value] == value) - { - if (LastUpdatedGroupIdx == groupIdx) - color = "green3_1"; - else - color = "green4"; - } - else - { - if (LastUpdatedGroupIdx == groupIdx) - color = "red1"; - else - color = "red3_1"; - } - } - - var text = (value?.ToString() ?? string.Empty).PadRight(2).PadLeft(3); - if (string.IsNullOrEmpty(text)) - text = " "; - - if (string.IsNullOrWhiteSpace(text) == false && lastUpdatedNodeIdx.HasValue && lastUpdatedNodeIdx.Value == nodeIdx) - { - cells.Add(new Panel(new Markup($"[{color}]{text}[/]")).BorderColor(LastUpdatedGroupIdx != groupIdx ? Color.Green4 : Color.Green3_1)); - } - else - { - cells.Add(new Panel(new Markup($"[{color}]{text}[/]")).BorderColor(Color.Black)); - } - } - - table.AddRow(cells); - - var label = $"CACHE C{groupIdx + 1}"; - var labelColor = "grey84"; - if (LastUpdatedGroupIdx == groupIdx) - { - label += " (UPDATED)"; - labelColor = "springgreen3_1"; - } - - tables.Add(($"[{labelColor}]{label}[/]", table)); - } - - // SUMMARY - AnsiConsole.Clear(); - - AnsiConsole.MarkupLine("SUMMARY"); - AnsiConsole.MarkupLine($"- [deepskyblue1]SIZE :[/] GROUPS = {WorkloadScenarioOptions.GroupsCount} / NODES = {WorkloadScenarioOptions.NodesPerGroupCount}"); - AnsiConsole.MarkupLine($"- [deepskyblue1]CACHE DURATION:[/] {WorkloadScenarioOptions.CacheDuration}"); - AnsiConsole.MarkupLine($"- [deepskyblue1]UPDATE DELAY :[/] {WorkloadScenarioOptions.DataChangesMinDelay} - {WorkloadScenarioOptions.DataChangesMaxDelay}"); - if (WorkloadScenarioOptions.DistributedCacheType == DistributedCacheType.None) - AnsiConsole.MarkupLine("- [deepskyblue1]DIST. CACHE :[/] [red1]X[/]"); - else - AnsiConsole.MarkupLine($"- [deepskyblue1]DIST. CACHE :[/] [green3_1]v[/] ({WorkloadScenarioOptions.DistributedCacheType})"); - - if (WorkloadScenarioOptions.BackplaneType == BackplaneType.None) - AnsiConsole.MarkupLine("- [deepskyblue1]BACKPLANE :[/] [red1]X[/]"); - else - AnsiConsole.MarkupLine($"- [deepskyblue1]BACKPLANE :[/] [green3_1]v[/] ({WorkloadScenarioOptions.BackplaneType})"); - AnsiConsole.WriteLine(); - - // STATS - AnsiConsole.MarkupLine("STATS"); - AnsiConsole.MarkupLine($"- [deepskyblue1]DB WRITES :[/] {DbWritesCount}"); - AnsiConsole.MarkupLine($"- [deepskyblue1]DB READS :[/] {DbReadsCount}"); - - AnsiConsole.WriteLine(); - - // TABLES - foreach (var item in tables) - { - AnsiConsole.MarkupLine(item.Label); - AnsiConsole.Write(item.Table); - } - AnsiConsole.WriteLine(); - } - } - - private static void UpdateSomeRandomData() - { - lock (LockObj) - { - // GET A RANDOM GROUP IDX - var groupIdx = RNG.Next(CacheGroups.Count); - - // CHANGE THE VALUE - LastValue++; - - // SAVE TO DB - SaveToDb(groupIdx, LastValue); - - // UPDATE CACHE - var nodes = CacheGroups[groupIdx]; - var nodeIdx = RNG.Next(nodes.Count); - var cache = nodes[nodeIdx]; - - if (WorkloadScenarioOptions.UpdateCacheOnSaveToDb) - { - cache.Set(CacheKey, LastValue); - } - //else - //{ - // cache.Remove(CacheKey, options => options.SetBackplane(false)); - //} - - // SAVE LAST XYZ - LastUpdatedGroupIdx = groupIdx; - LastUpdatedCaches[groupIdx] = nodeIdx; - } - } - - public static async Task RunAsync() - { - CacheKey = $"foo-{DateTime.UtcNow.Ticks}"; - - AnsiConsole.Clear(); - - Setup(); - - var cts = new CancellationTokenSource(); - var ct = cts.Token; - - while (ct.IsCancellationRequested == false) - { - // DISPLAY DASHBOARD - DisplayDashboard(); - - // WAIT SOME RANDOM TIME - var delay = TimeSpan.FromMilliseconds( - WorkloadScenarioOptions.DataChangesMinDelay.TotalMilliseconds - + (RNG.NextDouble() * (WorkloadScenarioOptions.DataChangesMaxDelay.TotalMilliseconds - WorkloadScenarioOptions.DataChangesMinDelay.TotalMilliseconds)) - ); - - await Task.Delay(delay).ConfigureAwait(false); - - // UPDATE SOME DATA - UpdateSomeRandomData(); - - // WAIT A LITTLE TO LET THE BACKBONE TO ITS JOB - if (WorkloadScenarioOptions.PostUpdateCooldownDelay.HasValue) - await Task.Delay(WorkloadScenarioOptions.PostUpdateCooldownDelay.Value).ConfigureAwait(false); - } - } - } -} diff --git a/tests/ZiggyCreatures.FusionCache.Playground/ZiggyCreatures.FusionCache.Playground.csproj b/tests/ZiggyCreatures.FusionCache.Playground/ZiggyCreatures.FusionCache.Playground.csproj index c40cb7d9..7abd327e 100644 --- a/tests/ZiggyCreatures.FusionCache.Playground/ZiggyCreatures.FusionCache.Playground.csproj +++ b/tests/ZiggyCreatures.FusionCache.Playground/ZiggyCreatures.FusionCache.Playground.csproj @@ -2,7 +2,7 @@ <PropertyGroup> <OutputType>Exe</OutputType> - <TargetFramework>net6.0</TargetFramework> + <TargetFramework>net7.0</TargetFramework> <LangVersion>latest</LangVersion> <Nullable>enable</Nullable> <IsPackable>false</IsPackable> @@ -10,13 +10,10 @@ </PropertyGroup> <ItemGroup> - <PackageReference Include="Microsoft.Extensions.Caching.StackExchangeRedis" Version="7.0.9" /> - <PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="7.0.0" /> - <PackageReference Include="Microsoft.Extensions.Logging" Version="7.0.0" /> - <PackageReference Include="Serilog" Version="3.0.1" /> + <PackageReference Include="Microsoft.Extensions.Caching.StackExchangeRedis" Version="7.0.13" /> + <PackageReference Include="Serilog" Version="3.1.1" /> <PackageReference Include="Serilog.AspNetCore" Version="7.0.0" /> - <PackageReference Include="Serilog.Sinks.Console" Version="4.1.0" /> - <PackageReference Include="Spectre.Console" Version="0.47.0" /> + <PackageReference Include="Serilog.Sinks.Console" Version="5.0.0" /> </ItemGroup> <ItemGroup> diff --git a/tests/ZiggyCreatures.FusionCache.Simulator/Program.cs b/tests/ZiggyCreatures.FusionCache.Simulator/Program.cs new file mode 100644 index 00000000..3007d863 --- /dev/null +++ b/tests/ZiggyCreatures.FusionCache.Simulator/Program.cs @@ -0,0 +1,882 @@ +using System.Collections.Concurrent; +using System.Diagnostics; +using System.Reflection; +using Microsoft.Extensions.Caching.Distributed; +using Microsoft.Extensions.Caching.Memory; +using Microsoft.Extensions.Caching.StackExchangeRedis; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; +using Serilog; +using Serilog.Events; +using Spectre.Console; +using Spectre.Console.Rendering; +using ZiggyCreatures.Caching.Fusion.Backplane; +using ZiggyCreatures.Caching.Fusion.Backplane.Memory; +using ZiggyCreatures.Caching.Fusion.Backplane.StackExchangeRedis; +using ZiggyCreatures.Caching.Fusion.Chaos; +using ZiggyCreatures.Caching.Fusion.Internals; +using ZiggyCreatures.Caching.Fusion.Serialization.NewtonsoftJson; +using ZiggyCreatures.Caching.Fusion.Simulator.Stuff; + +namespace ZiggyCreatures.Caching.Fusion.Playground.Simulator +{ + internal static class SimulatorOptions + { + // GENERAL + public static int ClustersCount = 1; + public static int NodesPerClusterCount = 2; + public static bool EnableFailSafe = false; + public static readonly TimeSpan RandomUpdateDelay = TimeSpan.FromSeconds(1); + public static bool EnableRandomUpdates = false; + public static readonly bool DisplayApproximateExpirationCountdown = false; + + // DURATION + public static readonly TimeSpan CacheDuration = TimeSpan.FromSeconds(30); + + // LOGGING + public static readonly bool EnableLogging = false; + public static readonly bool EnableLoggingExceptions = false; + + // 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 TimeSpan? ChaosDistributedCacheSyntheticMinDelay = null; //TimeSpan.FromMilliseconds(500); + public static readonly TimeSpan? ChaosDistributedCacheSyntheticMaxDelay = null; //TimeSpan.FromMilliseconds(500); + + // BACKPLANE + 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 TimeSpan? ChaosBackplaneSyntheticDelay = null; //TimeSpan.FromMilliseconds(500); + + // OTHERS + public static readonly TimeSpan RefreshDelay = TimeSpan.FromMilliseconds(500); + public static readonly TimeSpan DataChangesMinDelay = TimeSpan.FromSeconds(1); + public static readonly TimeSpan DataChangesMaxDelay = TimeSpan.FromSeconds(1); + public static readonly bool UpdateCacheOnSaveToDb = true; + public static readonly TimeSpan? PostUpdateCooldownDelay = TimeSpan.FromMilliseconds(150); + } + + internal class Program + { + // INTERNAL + private static string CacheKey = "foo"; + private static readonly Random RNG = new Random(); + private static readonly SemaphoreSlim GlobalMutex = new SemaphoreSlim(1, 1); + private static int LastValue = 0; + private static int? LastUpdatedClusterIdx = null; + private static readonly ConcurrentDictionary<int, SimulatedCluster> CacheClusters = new ConcurrentDictionary<int, SimulatedCluster>(); + private static readonly ConcurrentDictionary<int, SimulatedDatabase> Databases = new ConcurrentDictionary<int, SimulatedDatabase>(); + + private static bool DatabaseEnabled = true; + + private static readonly List<ChaosDistributedCache> DistributedCaches = new List<ChaosDistributedCache>(); + private static bool DistributedCachesEnabled = true; + + private static readonly List<ChaosBackplane> Backplanes = new List<ChaosBackplane>(); + private static bool BackplanesEnabled = true; + + // STATS + private static int DbWritesCount = 0; + private static int DbReadsCount = 0; + + // COLORS + private static readonly Color Color_DarkGreen = Color.DarkGreen; + private static readonly Color Color_MidGreen = Color.SpringGreen3; + private static readonly Color Color_LightGreen = Color.SpringGreen2; + private static readonly Color Color_FlashGreen = Color.SpringGreen3_1; + private static readonly Color Color_DarkRed = Color.DarkRed; + private static readonly Color Color_MidRed = Color.DeepPink2; + private static readonly Color Color_LightRed = Color.Red3_1; + private static readonly Color Color_FlashRed = Color.Red1; + + private static IDistributedCache? CreateDistributedCache(int clusterIdx) + { + switch (SimulatorOptions.DistributedCacheType) + { + case DistributedCacheType.None: + return null; + case DistributedCacheType.Redis: + return new RedisCache(new RedisCacheOptions + { + Configuration = string.Format(SimulatorOptions.DistributedCacheRedisConnection, clusterIdx) + }); + default: + return new MemoryDistributedCache(Options.Create(new MemoryDistributedCacheOptions())); + } + } + + private static IFusionCacheBackplane? CreateBackplane(int clusterIdx) + { + switch (SimulatorOptions.BackplaneType) + { + case BackplaneType.None: + return null; + case BackplaneType.Redis: + return new RedisBackplane(new RedisBackplaneOptions + { + Configuration = string.Format(SimulatorOptions.BackplaneRedisConnection, clusterIdx), + //CircuitBreakerDuration = SimulatorScenarioOptions.BackplaneCircuitBreakerDuration, + //AllowBackgroundOperations = SimulatorScenarioOptions.AllowBackplaneBackgroundOperations + }); + default: + return new MemoryBackplane(new MemoryBackplaneOptions() { ConnectionId = $"connection-{clusterIdx}" }); + } + } + + 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()); + db.Value = value; + db.LastUpdateTimestamp = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds(); + LastUpdatedClusterIdx = clusterIdx; + } + + 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()); + return db.Value; + } + + private static async Task UpdateRandomNodeOnClusterAsync(int clusterIdx, ILogger<FusionCache>? logger) + { + var sw = Stopwatch.StartNew(); + await GlobalMutex.WaitAsync(); + sw.Stop(); + logger?.LogInformation($"LOCK (UPDATE) TOOK: {sw.ElapsedMilliseconds} ms"); + + try + { + // CHANGE THE VALUE + LastValue++; + + // SAVE TO DB + try + { + SaveToDb(clusterIdx, LastValue); + + // UPDATE CACHE + if (SimulatorOptions.UpdateCacheOnSaveToDb) + { + var cluster = CacheClusters[clusterIdx]; + var nodeIdx = RNG.Next(cluster.Nodes.Count); + var node = cluster.Nodes[nodeIdx]; + + logger?.LogInformation($"BEFORE CACHE SET ({node.Cache.InstanceId}) TOOK: {sw.ElapsedMilliseconds} ms"); + 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"); + + // SAVE LAST XYZ + node.ExpirationTimestampUnixMs = DateTimeOffset.UtcNow.Add(SimulatorOptions.CacheDuration).ToUnixTimeMilliseconds(); + cluster.LastUpdatedNodeIndex = nodeIdx; + } + } + catch + { + // EMPTY + } + } + finally + { + GlobalMutex.Release(); + } + } + + private static void SetupSerilogLogger(IServiceCollection services, LogEventLevel minLevel = LogEventLevel.Verbose) + { + Log.Logger = new LoggerConfiguration() + .MinimumLevel.Is(minLevel) + .Enrich.FromLogContext() + .WriteTo.Debug( + outputTemplate: SimulatorOptions.EnableLoggingExceptions + ? "[{Timestamp:HH:mm:ss} {Level:u3}] {Message:lj}{NewLine}{Exception}" + : "[{Timestamp:HH:mm:ss} {Level:u3}] {Message:lj}{NewLine}" + ) + .CreateLogger() + ; + + services.AddLogging(configure => configure.AddSerilog()); + } + + private static DateTimeOffset? ExtractCacheEntryExpiration(IFusionCache cache, string cacheKey) + { + var mca = cache.GetType().GetField("_mca", BindingFlags.NonPublic | BindingFlags.Instance)?.GetValue(cache); + + if (mca is null) + { + Debug.WriteLine("MEMORY CACHE ACCESSOR IS NULL"); + return null; + } + + var memoryCache = (IMemoryCache?)mca.GetType().GetField("_cache", BindingFlags.NonPublic | BindingFlags.Instance)?.GetValue(mca); + + if (memoryCache is null) + { + Debug.WriteLine("MEMORY CACHE IS NULL"); + return null; + } + + var entry = memoryCache?.Get(cacheKey); + + if (entry is null) + { + Debug.WriteLine("ENTRY IS NULL"); + return null; + } + + // GET THE LOGICAL EXPIRATION + var meta = (FusionCacheEntryMetadata?)entry.GetType().GetProperty("Metadata")?.GetValue(entry); + var logicalExpiration = meta?.LogicalExpiration; + + // GET THE PHYSICAL EXPIRATION + DateTimeOffset? physicalExpiration = null; + try + { + physicalExpiration = (DateTimeOffset?)entry.GetType().GetProperty("LogicalExpiration")?.GetValue(entry); + } + catch (Exception exc) + { + Debug.WriteLine($"ERROR: {exc.Message}"); + } + + // WE HAVE BOTH: TAKE THE LOWER ONE + if (logicalExpiration is not null && physicalExpiration is not null) + return logicalExpiration.Value < physicalExpiration.Value ? logicalExpiration : physicalExpiration; + + // USE THE PHYSICAL + if (logicalExpiration is not null) + return logicalExpiration; + + // USE THE LOGICAL + if (physicalExpiration is not null) + return physicalExpiration; + + return null; + } + + private static void SetupClusters(IServiceProvider serviceProvider, ILogger<FusionCache>? logger) + { + AnsiConsole.MarkupLine("[deepskyblue1]SETUP[/]"); + + var swAll = Stopwatch.StartNew(); + for (int clusterIdx = 0; clusterIdx < SimulatorOptions.ClustersCount; clusterIdx++) + { + var swCluster = Stopwatch.StartNew(); + + var cluster = new SimulatedCluster(); + var cacheName = $"C{clusterIdx + 1}"; + + var distributedCache = CreateDistributedCache(clusterIdx); + + for (int nodeIdx = 0; nodeIdx < SimulatorOptions.NodesPerClusterCount; nodeIdx++) + { + var swNode = Stopwatch.StartNew(); + + var cacheInstanceId = $"{cacheName}-{nodeIdx + 1}"; + + AnsiConsole.MarkupLine($"CACHE: [deepskyblue1]{cacheName} ({cacheInstanceId})[/]"); + + AnsiConsole.Markup(" - [default]CORE:[/] ..."); + + var options = new FusionCacheOptions() + { + CacheName = cacheName, + DefaultEntryOptions = new FusionCacheEntryOptions(SimulatorOptions.CacheDuration) + }; + options.SetInstanceId(cacheInstanceId); + + var deo = options.DefaultEntryOptions; + + // FAIL-SAFE + deo.IsFailSafeEnabled = SimulatorOptions.EnableFailSafe; + deo.FailSafeMaxDuration = TimeSpan.FromSeconds(60); + deo.FailSafeThrottleDuration = TimeSpan.FromSeconds(2); + + // DISTRIBUTED CACHE + if (SimulatorOptions.DistributedCacheSoftTimeout is not null) + deo.DistributedCacheSoftTimeout = SimulatorOptions.DistributedCacheSoftTimeout.Value; + if (SimulatorOptions.DistributedCacheHardTimeout is not null) + deo.DistributedCacheHardTimeout = SimulatorOptions.DistributedCacheHardTimeout.Value; + deo.AllowBackgroundDistributedCacheOperations = SimulatorOptions.AllowBackgroundDistributedCacheOperations; + options.DistributedCacheCircuitBreakerDuration = SimulatorOptions.DistributedCacheCircuitBreakerDuration; + + // BACKPLANE + deo.AllowBackgroundBackplaneOperations = SimulatorOptions.AllowBackgroundBackplaneOperations; + options.BackplaneCircuitBreakerDuration = SimulatorOptions.BackplaneCircuitBreakerDuration; + + // SPECIAL CACSE HANDLING: BACKPLANE + NO DISTRIBUTED CACHE + if (SimulatorOptions.DistributedCacheType == DistributedCacheType.None && SimulatorOptions.BackplaneType != BackplaneType.None) + deo.SkipBackplaneNotifications = true; + + var cacheLogger = SimulatorOptions.EnableLogging ? serviceProvider.GetService<ILogger<FusionCache>>() : null; + var swCache = Stopwatch.StartNew(); + var cache = new FusionCache(options, logger: cacheLogger); + swCache.Stop(); + logger?.LogInformation($"CACHE CREATION TOOK: {swCache.ElapsedMilliseconds} ms"); + AnsiConsole.MarkupLine($" [black on {Color_DarkGreen}] OK [/]"); + + // DISTRIBUTED CACHE + if (distributedCache is not null) + { + AnsiConsole.Markup(" - [default]DISTRIBUTED CACHE:[/] ..."); + var chaosDistributedCacheLogger = SimulatorOptions.EnableLogging ? serviceProvider.GetService<ILogger<ChaosDistributedCache>>() : null; + var tmp = new ChaosDistributedCache(distributedCache, chaosDistributedCacheLogger); + if (SimulatorOptions.ChaosDistributedCacheSyntheticMinDelay is not null && SimulatorOptions.ChaosDistributedCacheSyntheticMaxDelay is not null) + { + tmp.SetAlwaysDelay(SimulatorOptions.ChaosDistributedCacheSyntheticMinDelay.Value, SimulatorOptions.ChaosDistributedCacheSyntheticMaxDelay.Value); + } + var swDistributedCache = Stopwatch.StartNew(); + cache.SetupDistributedCache(tmp, new FusionCacheNewtonsoftJsonSerializer()); + swDistributedCache.Stop(); + logger?.LogInformation($"DISTRIBUTED CACHE SETUP TOOK: {swDistributedCache.ElapsedMilliseconds} ms"); + DistributedCaches.Add(tmp); + AnsiConsole.MarkupLine($" [black on {Color_DarkGreen}] OK [/]"); + } + + // BACKPLANE + var backplane = CreateBackplane(clusterIdx); + if (backplane is not null) + { + AnsiConsole.Markup(" - [default]BACKPLANE:[/] ..."); + var chaosBackplaneLogger = SimulatorOptions.EnableLogging ? serviceProvider.GetService<ILogger<ChaosBackplane>>() : null; + var tmp = new ChaosBackplane(backplane, chaosBackplaneLogger); + if (SimulatorOptions.ChaosBackplaneSyntheticDelay is not null) + { + tmp.SetAlwaysDelayExactly(SimulatorOptions.ChaosBackplaneSyntheticDelay.Value); + } + var swBackplane = Stopwatch.StartNew(); + cache.SetupBackplane(tmp); + swBackplane.Stop(); + logger?.LogInformation($"BACKPLANE SETUP TOOK: {swBackplane.ElapsedMilliseconds} ms"); + Backplanes.Add(tmp); + AnsiConsole.MarkupLine($" [black on {Color_DarkGreen}] OK [/]"); + } + + AnsiConsole.WriteLine(); + + var node = new SimulatedNode(cache); + + // EVENTS + cache.Events.Memory.Set += (sender, e) => + { + var maybeExpiration = ExtractCacheEntryExpiration((IFusionCache)sender!, CacheKey); + + if (maybeExpiration is not null) + { + node.ExpirationTimestampUnixMs = maybeExpiration.Value.ToUnixTimeMilliseconds(); + } + else + { + //node.ExpirationTimestamp = DateTimeOffset.UtcNow.Add(SimulatorScenarioOptions.CacheDuration).ToUnixTimeMilliseconds(); + node.ExpirationTimestampUnixMs = null; + } + }; + + cluster.Nodes.Add(node); + + swNode.Stop(); + logger?.LogInformation($"SETUP (NODE {nodeIdx + 1}) TOOK: {swNode.ElapsedMilliseconds} ms"); + } + + CacheClusters[clusterIdx] = cluster; + + swCluster.Stop(); + logger?.LogInformation($"SETUP (CLUSTER {clusterIdx + 1}) TOOK: {swCluster.ElapsedMilliseconds} ms"); + } + + swAll.Stop(); + logger?.LogInformation($"SETUP (ALL) TOOK: {swAll.ElapsedMilliseconds} ms"); + } + + private static string GetCountdownMarkup(long nowTimestampUnixMs, long? expirationTimestampUnixMs) + { + if (expirationTimestampUnixMs is null || expirationTimestampUnixMs.Value <= nowTimestampUnixMs) + return "-"; + + var remainingSeconds = (expirationTimestampUnixMs.Value - nowTimestampUnixMs) / 1_000; + var v = (float)(expirationTimestampUnixMs.Value - nowTimestampUnixMs) / (float)SimulatorOptions.CacheDuration.TotalMilliseconds; + if (v <= 0.0f) + return "-"; + + var color = "grey93"; + switch (v) + { + case <= 0.1f: + color = "darkorange3_1"; + break; + case <= 0.2f: + color = "grey23"; + break; + case <= 0.3f: + color = "grey42"; + break; + case <= 0.4f: + color = "grey58"; + break; + case <= 0.6f: + color = "grey66"; + break; + case <= 0.8f: + color = "grey78"; + break; + default: + break; + } + + return $"[{color}]-{remainingSeconds}[/]"; + } + + private static async Task DisplayDashboardAsync(ILogger<FusionCache>? logger, bool getValues) + { + static async Task GetValueFromNode(ConcurrentDictionary<int, int?> clusterValues, int clusterIdx, SimulatedNode node, int nodeIdx, ILogger<FusionCache>? logger) + { + int? value; + try + { + var sw = Stopwatch.StartNew(); + value = node.Cache.GetOrSet<int?>(CacheKey, _ => LoadFromDb(clusterIdx)); + sw.Stop(); + logger?.LogInformation($"CACHE GET ({node.Cache.InstanceId}) TOOK: {sw.ElapsedMilliseconds} ms"); + } + catch + { + value = null; + logger?.LogInformation($"CACHE GET ({node.Cache.InstanceId}) FAILED"); + } + clusterValues[nodeIdx] = value; + } + + var items = new List<(string Label, Table Table)>(); + var nowTimestampUnixMs = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds(); + + var swLock = Stopwatch.StartNew(); + await GlobalMutex.WaitAsync(); + swLock.Stop(); + logger?.LogInformation($"LOCK (DASHBOARD) TOOK: {swLock.ElapsedMilliseconds} ms"); + + try + { + var values = new ConcurrentDictionary<int, ConcurrentDictionary<int, int?>>(); + + if (getValues) + { + logger?.LogInformation("SNAPSHOT VALUES: START"); + + var swClusters = Stopwatch.StartNew(); + + // SNAPSHOT VALUES + var tasks = new ConcurrentBag<Task>(); + + for (int clusterIdx = 0; clusterIdx < CacheClusters.Values.Count; clusterIdx++) + { + var cluster = CacheClusters[clusterIdx]; + + var valueCluster = values[clusterIdx] = new ConcurrentDictionary<int, int?>(); + + for (int nodeIdx = 0; nodeIdx < cluster.Nodes.Count; nodeIdx++) + { + var node = cluster.Nodes[nodeIdx]; + + tasks.Add(GetValueFromNode(valueCluster, clusterIdx, node, nodeIdx, logger)); + } + } + + await Task.WhenAll(tasks); + + swClusters.Stop(); + logger?.LogInformation($"READ ON ALL CLUSTERS TOOK: {swClusters.ElapsedMilliseconds} ms"); + + logger?.LogInformation("SNAPSHOT VALUES: END"); + } + else + { + // NULL FILL VALUES + for (int clusterIdx = 0; clusterIdx < CacheClusters.Values.Count; clusterIdx++) + { + var cluster = CacheClusters[clusterIdx]; + var clusterValues = values[clusterIdx] = new ConcurrentDictionary<int, int?>(); + for (int nodeIdx = 0; nodeIdx < cluster.Nodes.Count; nodeIdx++) + { + clusterValues[nodeIdx] = null; + } + } + } + + logger?.LogInformation("DASHBOARD: START"); + + for (int clusterIdx = 0; clusterIdx < CacheClusters.Count; clusterIdx++) + { + var cluster = CacheClusters[clusterIdx]; + + var table = new Table(); + + for (int nodeIdx = 0; nodeIdx < cluster.Nodes.Count; nodeIdx++) + { + table.AddColumn(new TableColumn($"[deepskyblue1]N {nodeIdx + 1}[/]").Centered()); + } + + var lastUpdatedNodeIdx = cluster.LastUpdatedNodeIndex; + + var clusterValues = values[clusterIdx]; + + // BUILD CELLS + var cells = new List<IRenderable>(); + var isClusterInSync = true; + for (int nodeIdx = 0; nodeIdx < cluster.Nodes.Count; nodeIdx++) + { + var node = cluster.Nodes[nodeIdx]; + var value = clusterValues[nodeIdx]; + + var color = "white"; + if (lastUpdatedNodeIdx.HasValue) + { + if (lastUpdatedNodeIdx.Value == nodeIdx) + { + if (LastUpdatedClusterIdx == clusterIdx) + color = Color_LightGreen.ToString(); + else + color = Color_DarkGreen.ToString(); + } + else if (clusterValues[lastUpdatedNodeIdx.Value] == value) + { + if (LastUpdatedClusterIdx == clusterIdx) + color = Color_LightGreen.ToString(); + else + color = Color_DarkGreen.ToString(); + } + else + { + isClusterInSync = false; + + if (LastUpdatedClusterIdx == clusterIdx) + color = Color_MidRed.ToString(); + else + color = Color_DarkRed.ToString(); + } + } + + var text = (value?.ToString() ?? "-").PadRight(2).PadLeft(3); + if (string.IsNullOrEmpty(text)) + text = " "; + + + var borderColor = Color.Black; + if (string.IsNullOrWhiteSpace(text) == false && lastUpdatedNodeIdx.HasValue && lastUpdatedNodeIdx.Value == nodeIdx) + { + borderColor = LastUpdatedClusterIdx != clusterIdx ? Color_DarkGreen : Color_FlashGreen; + } + + var cellMarkup = $"[{color}]{text}[/]"; + if (SimulatorOptions.DisplayApproximateExpirationCountdown) + { + cellMarkup += $"\n\n{GetCountdownMarkup(nowTimestampUnixMs, node.ExpirationTimestampUnixMs)}"; + } + cells.Add(new Panel(new Markup(cellMarkup)).BorderColor(borderColor)); + } + + table.AddRow(cells); + + // TABLE LABEL + var isLastUpdatedCluster = LastUpdatedClusterIdx == clusterIdx; + var labelColor = isLastUpdatedCluster ? Color_FlashGreen.ToString() : "grey84"; + var label = $"[{labelColor}]CLUSTER C{clusterIdx + 1}[/]"; + + if (isClusterInSync) + { + label += $" [{Color_DarkGreen} on {Color_MidGreen}] IN SYNC [/]"; + } + else + { + label += $" [{Color_DarkRed} on {Color_MidRed}] NO SYNC [/]"; + } + + if (isLastUpdatedCluster) + { + label += $" [{Color_DarkGreen} on {Color_MidGreen}] LAST UPD [/]"; + } + + // TABLE BORDER COLOR + var tableBorderColor = Color.Default; + + if (LastUpdatedClusterIdx is not null) + { + if (values[clusterIdx].Values.Any(x => x is not null)) + { + if (isClusterInSync) + { + if (LastUpdatedClusterIdx == clusterIdx) + tableBorderColor = Color_MidGreen; + else + tableBorderColor = Color_DarkGreen; + } + else + { + if (LastUpdatedClusterIdx == clusterIdx) + tableBorderColor = Color_MidRed; + else + tableBorderColor = Color_DarkRed; + } + } + } + + table.BorderColor(tableBorderColor); + + // TABLE BORDER + var tableBorder = TableBorder.Heavy; + + table.Border(tableBorder); + + items.Add((label, table)); + } + + logger?.LogInformation("DASHBOARD: END"); + + // SUMMARY + AnsiConsole.Clear(); + + AnsiConsole.MarkupLine("SUMMARY"); + AnsiConsole.MarkupLine($"- [deepskyblue1]SIZE :[/] {SimulatorOptions.NodesPerClusterCount} NODES x {SimulatorOptions.ClustersCount} CLUSTERS ({SimulatorOptions.NodesPerClusterCount * SimulatorOptions.ClustersCount} TOTAL NODES)"); + AnsiConsole.MarkupLine($"- [deepskyblue1]CACHE DURATION:[/] {SimulatorOptions.CacheDuration}"); + + AnsiConsole.Markup("- [deepskyblue1]DATABASE :[/] "); + AnsiConsole.Markup($"Memory "); + if (DatabaseEnabled) + AnsiConsole.MarkupLine($"[{Color_DarkGreen} on {Color_MidGreen}] ON [/]"); + else + AnsiConsole.MarkupLine($"[{Color_DarkRed} on {Color_MidRed}] OFF [/]"); + + AnsiConsole.Markup("- [deepskyblue1]DIST. CACHE :[/] "); + if (SimulatorOptions.DistributedCacheType == DistributedCacheType.None) + { + AnsiConsole.MarkupLine("[red1]X NONE[/]"); + } + else + { + AnsiConsole.Markup($"{SimulatorOptions.DistributedCacheType} "); + if (DistributedCachesEnabled) + AnsiConsole.MarkupLine($"[{Color_DarkGreen} on {Color_MidGreen}] ON [/]"); + else + AnsiConsole.MarkupLine($"[{Color_DarkRed} on {Color_MidRed}] OFF [/]"); + } + + AnsiConsole.Markup("- [deepskyblue1]BACKPLANE :[/] "); + if (SimulatorOptions.BackplaneType == BackplaneType.None) + { + AnsiConsole.MarkupLine("[red1]X NONE[/]"); + } + else + { + AnsiConsole.Markup($"{SimulatorOptions.BackplaneType} "); + if (BackplanesEnabled) + AnsiConsole.MarkupLine($"[{Color_DarkGreen} on {Color_MidGreen}] ON [/]"); + else + AnsiConsole.MarkupLine($"[{Color_DarkRed} on {Color_MidRed}] OFF [/]"); + } + AnsiConsole.WriteLine(); + + // STATS + AnsiConsole.MarkupLine("STATS"); + AnsiConsole.MarkupLine($"- [deepskyblue1]DATABASE :[/] {DbWritesCount} WRITES - {DbReadsCount} READS"); + + AnsiConsole.WriteLine(); + + // TABLES + foreach (var item in items) + { + // LABEL + AnsiConsole.Markup(item.Label); + + AnsiConsole.WriteLine(); + + // TABLE + AnsiConsole.Write(item.Table); + } + + AnsiConsole.WriteLine(); + AnsiConsole.MarkupLine($"COMMANDS:"); + AnsiConsole.MarkupLine($"- [deepskyblue1]0[/]: enable/disable random updates (all clusters) [{(SimulatorOptions.EnableRandomUpdates ? Color_DarkGreen.ToString() : "grey78")} on {(SimulatorOptions.EnableRandomUpdates ? Color_MidGreen.ToString() : "grey19")}] {(SimulatorOptions.EnableRandomUpdates ? "ON" : "OFF")} [/]"); + AnsiConsole.MarkupLine($"- [deepskyblue1]1-{CacheClusters.Count}[/]: update a random node on the specified cluster"); + AnsiConsole.MarkupLine($"- [deepskyblue1]D/d[/]: enable/disable distributed cache (all clusters)"); + AnsiConsole.MarkupLine($"- [deepskyblue1]B/b[/]: enable/disable backplane (all clusters)"); + AnsiConsole.MarkupLine($"- [deepskyblue1]S/s[/]: enable/disable database (all clusters)"); + AnsiConsole.MarkupLine($"- [deepskyblue1]Q/q[/]: quit"); + } + finally + { + GlobalMutex.Release(); + } + } + + private static void GetInputs() + { + // INPUTS + bool inputProvided; + + inputProvided = false; + while (inputProvided == false) + { + AnsiConsole.Markup($"[deepskyblue1]CLUSTERS (amount):[/] "); + inputProvided = int.TryParse(Console.ReadLine(), out SimulatorOptions.ClustersCount); + } + + inputProvided = false; + while (inputProvided == false) + { + AnsiConsole.Markup($"[deepskyblue1]NODES PER CLUSTER (amount):[/] "); + inputProvided = int.TryParse(Console.ReadLine(), out SimulatorOptions.NodesPerClusterCount); + } + + inputProvided = false; + while (inputProvided == false) + { + AnsiConsole.Markup($"[deepskyblue1]FAIL-SAFE (y/n):[/] "); + var tmp = Console.ReadKey(); + if (tmp.KeyChar is 'y' or 'n') + { + SimulatorOptions.EnableFailSafe = tmp.KeyChar == 'y'; + inputProvided = true; + } + else + { + AnsiConsole.WriteLine(); + } + } + } + + static async Task Main(string[] args) + { + Console.Title = "FusionCache - Simulator"; + + CacheKey = $"foo-{DateTime.UtcNow.Ticks}"; + + AnsiConsole.Clear(); + + GetInputs(); + + AnsiConsole.WriteLine(); + AnsiConsole.WriteLine(); + + // DI + var services = new ServiceCollection(); + SetupSerilogLogger(services, LogEventLevel.Verbose); + var serviceProvider = services.BuildServiceProvider(); + + var logger = SimulatorOptions.EnableLogging ? serviceProvider.GetService<ILogger<FusionCache>>() : null; + + SetupClusters(serviceProvider, logger); + + using var cts = new CancellationTokenSource(); + var ct = cts.Token; + + _ = Task.Run(async () => + { + var firstRun = true; + while (ct.IsCancellationRequested == false) + { + try + { + // DISPLAY DASHBOARD + await DisplayDashboardAsync(logger, firstRun == false); + firstRun = false; + } + catch (Exception exc) + { + AnsiConsole.Clear(); + AnsiConsole.WriteException(exc); + throw; + } + + await Task.Delay(SimulatorOptions.RefreshDelay); + } + }); + + _ = Task.Run(async () => + { + while (ct.IsCancellationRequested == false) + { + if (SimulatorOptions.EnableRandomUpdates) + await UpdateRandomNodeOnClusterAsync(RNG.Next(CacheClusters.Count), logger); + await Task.Delay(SimulatorOptions.RandomUpdateDelay); + } + }); + + var shouldExit = false; + do + { + var tmp = Console.ReadKey(); + switch (tmp.KeyChar) + { + case '0': + // TOGGLE RANDOM UPDATES + SimulatorOptions.EnableRandomUpdates = !SimulatorOptions.EnableRandomUpdates; + break; + case '1' or '2' or '3' or '4' or '5' or '6' or '7' or '8' or '9': + // SET VALUE + var clusterIdx = int.Parse(tmp.KeyChar.ToString()); + if (clusterIdx > 0 && clusterIdx <= CacheClusters.Count) + { + await UpdateRandomNodeOnClusterAsync(clusterIdx - 1, logger); + } + break; + case 'D' or 'd': + // TOGGLE DISTRIBUTED CACHES + DistributedCachesEnabled = !DistributedCachesEnabled; + foreach (var distributedCache in DistributedCaches) + { + if (DistributedCachesEnabled) + distributedCache.SetNeverThrow(); + else + distributedCache.SetAlwaysThrow(); + } + break; + case 'B' or 'b': + // TOGGLE DISTRIBUTED CACHES + BackplanesEnabled = !BackplanesEnabled; + foreach (var backplane in Backplanes) + { + if (BackplanesEnabled) + backplane.SetNeverThrow(); + else + backplane.SetAlwaysThrow(); + } + break; + case 'S' or 's': + // TOGGLE DATABASE + DatabaseEnabled = !DatabaseEnabled; + break; + case 'Q' or 'q': + // QUIT + shouldExit = true; + break; + default: + break; + } + } while (shouldExit == false); + + cts.Cancel(); + await Task.Delay(1_000); + } + } +} diff --git a/tests/ZiggyCreatures.FusionCache.Simulator/Stuff/BackplaneType.cs b/tests/ZiggyCreatures.FusionCache.Simulator/Stuff/BackplaneType.cs new file mode 100644 index 00000000..28e31f2d --- /dev/null +++ b/tests/ZiggyCreatures.FusionCache.Simulator/Stuff/BackplaneType.cs @@ -0,0 +1,9 @@ +namespace ZiggyCreatures.Caching.Fusion.Simulator.Stuff +{ + public enum BackplaneType + { + None = 0, + Memory = 1, + Redis = 2 + } +} diff --git a/tests/ZiggyCreatures.FusionCache.Simulator/Stuff/DistributedCacheType.cs b/tests/ZiggyCreatures.FusionCache.Simulator/Stuff/DistributedCacheType.cs new file mode 100644 index 00000000..3b41ecda --- /dev/null +++ b/tests/ZiggyCreatures.FusionCache.Simulator/Stuff/DistributedCacheType.cs @@ -0,0 +1,9 @@ +namespace ZiggyCreatures.Caching.Fusion.Simulator.Stuff +{ + public enum DistributedCacheType + { + None = 0, + Memory = 1, + Redis = 2 + } +} diff --git a/tests/ZiggyCreatures.FusionCache.Simulator/Stuff/SimulatedCluster.cs b/tests/ZiggyCreatures.FusionCache.Simulator/Stuff/SimulatedCluster.cs new file mode 100644 index 00000000..2015880f --- /dev/null +++ b/tests/ZiggyCreatures.FusionCache.Simulator/Stuff/SimulatedCluster.cs @@ -0,0 +1,8 @@ +namespace ZiggyCreatures.Caching.Fusion.Simulator.Stuff +{ + public class SimulatedCluster + { + public List<SimulatedNode> Nodes { get; } = new List<SimulatedNode>(); + public int? LastUpdatedNodeIndex { get; set; } + } +} diff --git a/tests/ZiggyCreatures.FusionCache.Simulator/Stuff/SimulatedDatabase.cs b/tests/ZiggyCreatures.FusionCache.Simulator/Stuff/SimulatedDatabase.cs new file mode 100644 index 00000000..45fcbbed --- /dev/null +++ b/tests/ZiggyCreatures.FusionCache.Simulator/Stuff/SimulatedDatabase.cs @@ -0,0 +1,8 @@ +namespace ZiggyCreatures.Caching.Fusion.Simulator.Stuff +{ + public class SimulatedDatabase + { + public int? Value { get; set; } + public long? LastUpdateTimestamp { get; set; } + } +} diff --git a/tests/ZiggyCreatures.FusionCache.Simulator/Stuff/SimulatedNode.cs b/tests/ZiggyCreatures.FusionCache.Simulator/Stuff/SimulatedNode.cs new file mode 100644 index 00000000..84539d90 --- /dev/null +++ b/tests/ZiggyCreatures.FusionCache.Simulator/Stuff/SimulatedNode.cs @@ -0,0 +1,13 @@ +namespace ZiggyCreatures.Caching.Fusion.Simulator.Stuff +{ + public class SimulatedNode + { + public SimulatedNode(IFusionCache cache) + { + Cache = cache; + } + + public IFusionCache Cache { get; } + public long? ExpirationTimestampUnixMs { get; set; } + } +} diff --git a/tests/ZiggyCreatures.FusionCache.Simulator/ZiggyCreatures.FusionCache.Simulator.csproj b/tests/ZiggyCreatures.FusionCache.Simulator/ZiggyCreatures.FusionCache.Simulator.csproj new file mode 100644 index 00000000..83bc25c9 --- /dev/null +++ b/tests/ZiggyCreatures.FusionCache.Simulator/ZiggyCreatures.FusionCache.Simulator.csproj @@ -0,0 +1,28 @@ +<Project Sdk="Microsoft.NET.Sdk"> + + <PropertyGroup> + <OutputType>Exe</OutputType> + <TargetFramework>net7.0</TargetFramework> + <LangVersion>latest</LangVersion> + <ImplicitUsings>enable</ImplicitUsings> + <Nullable>enable</Nullable> + <IsPackable>false</IsPackable> + <RootNamespace>ZiggyCreatures.Caching.Fusion.Simulator</RootNamespace> + </PropertyGroup> + + <ItemGroup> + <PackageReference Include="Microsoft.Extensions.Caching.StackExchangeRedis" Version="7.0.13" /> + <PackageReference Include="Serilog" Version="3.1.1" /> + <PackageReference Include="Serilog.AspNetCore" Version="7.0.0" /> + <PackageReference Include="Spectre.Console" Version="0.47.0" /> + </ItemGroup> + + <ItemGroup> + <ProjectReference Include="..\..\src\ZiggyCreatures.FusionCache.Backplane.Memory\ZiggyCreatures.FusionCache.Backplane.Memory.csproj" /> + <ProjectReference Include="..\..\src\ZiggyCreatures.FusionCache.Backplane.StackExchangeRedis\ZiggyCreatures.FusionCache.Backplane.StackExchangeRedis.csproj" /> + <ProjectReference Include="..\..\src\ZiggyCreatures.FusionCache.Chaos\ZiggyCreatures.FusionCache.Chaos.csproj" /> + <ProjectReference Include="..\..\src\ZiggyCreatures.FusionCache\ZiggyCreatures.FusionCache.csproj" /> + <ProjectReference Include="..\..\src\ZiggyCreatures.FusionCache.Serialization.NewtonsoftJson\ZiggyCreatures.FusionCache.Serialization.NewtonsoftJson.csproj" /> + </ItemGroup> + +</Project> diff --git a/tests/ZiggyCreatures.FusionCache.Tests/AutoRecoveryTests.cs b/tests/ZiggyCreatures.FusionCache.Tests/AutoRecoveryTests.cs new file mode 100644 index 00000000..5abca4b5 --- /dev/null +++ b/tests/ZiggyCreatures.FusionCache.Tests/AutoRecoveryTests.cs @@ -0,0 +1,1146 @@ +using System; +using System.Threading; +using System.Threading.Tasks; +using FusionCacheTests.Stuff; +using Microsoft.Extensions.Caching.Distributed; +using Microsoft.Extensions.Caching.Memory; +using Microsoft.Extensions.Caching.StackExchangeRedis; +using Microsoft.Extensions.Options; +using Xunit; +using Xunit.Abstractions; +using ZiggyCreatures.Caching.Fusion; +using ZiggyCreatures.Caching.Fusion.Backplane; +using ZiggyCreatures.Caching.Fusion.Backplane.Memory; +using ZiggyCreatures.Caching.Fusion.Backplane.StackExchangeRedis; +using ZiggyCreatures.Caching.Fusion.Chaos; + +namespace FusionCacheTests; + +public class AutoRecoveryTests + : AbstractTests +{ + public AutoRecoveryTests(ITestOutputHelper output) + : base(output, "MyCache:") + { + } + + private FusionCacheOptions CreateFusionCacheOptions() + { + var res = new FusionCacheOptions(); + + res.CacheKeyPrefix = TestingCacheKeyPrefix; + + return res; + } + + private static readonly string? RedisConnection = null; + //private static readonly string? RedisConnection = "127.0.0.1:6379,ssl=False,abortConnect=False"; + + private static IDistributedCache CreateDistributedCache() + { + if (string.IsNullOrWhiteSpace(RedisConnection)) + return new MemoryDistributedCache(Options.Create(new MemoryDistributedCacheOptions())); + + return new RedisCache(new RedisCacheOptions { Configuration = RedisConnection }); + } + + private IFusionCacheBackplane CreateBackplane(string connectionId) + { + if (string.IsNullOrWhiteSpace(RedisConnection)) + return new MemoryBackplane(new MemoryBackplaneOptions() { ConnectionId = connectionId }, logger: CreateXUnitLogger<MemoryBackplane>()); + + return new RedisBackplane(new RedisBackplaneOptions { Configuration = RedisConnection }, logger: CreateXUnitLogger<RedisBackplane>()); + } + + private ChaosBackplane CreateChaosBackplane(string connectionId) + { + return new ChaosBackplane(CreateBackplane(connectionId)); + } + + private IFusionCache CreateFusionCache(string? cacheName, SerializerType? serializerType, IDistributedCache? distributedCache, IFusionCacheBackplane? backplane, Action<FusionCacheOptions>? setupAction = null) + { + var options = CreateFusionCacheOptions(); + + options.CacheName = cacheName!; + options.EnableSyncEventHandlersExecution = true; + + setupAction?.Invoke(options); + var fusionCache = new FusionCache(options, logger: CreateXUnitLogger<FusionCache>()); + fusionCache.DefaultEntryOptions.AllowBackgroundBackplaneOperations = false; + fusionCache.DefaultEntryOptions.AllowBackgroundDistributedCacheOperations = false; + if (distributedCache is not null && serializerType.HasValue) + fusionCache.SetupDistributedCache(distributedCache, TestsUtils.GetSerializer(serializerType.Value)); + if (backplane is not null) + fusionCache.SetupBackplane(backplane); + + return fusionCache; + } + + [Theory] + [ClassData(typeof(SerializerTypesClassData))] + public async Task CanRecoverAsync(SerializerType serializerType) + { + var defaultOptions = new FusionCacheOptions(); + + var _value = 0; + + var key = "foo"; + + var distributedCache = CreateDistributedCache(); + + var backplaneConnectionId = Guid.NewGuid().ToString("N"); + + var backplane1 = CreateChaosBackplane(backplaneConnectionId); + var backplane2 = CreateChaosBackplane(backplaneConnectionId); + var backplane3 = CreateChaosBackplane(backplaneConnectionId); + + using var cache1 = CreateFusionCache(null, serializerType, distributedCache, backplane1, opt => { opt.EnableAutoRecovery = true; }); + using var cache2 = CreateFusionCache(null, serializerType, distributedCache, backplane2, opt => { opt.EnableAutoRecovery = true; }); + using var cache3 = CreateFusionCache(null, serializerType, distributedCache, backplane3, opt => { opt.EnableAutoRecovery = true; }); + + cache1.DefaultEntryOptions.AllowBackgroundBackplaneOperations = false; + cache2.DefaultEntryOptions.AllowBackgroundBackplaneOperations = false; + cache3.DefaultEntryOptions.AllowBackgroundBackplaneOperations = false; + + // DISABLE THE BACKPLANE + backplane1.SetAlwaysThrow(); + backplane2.SetAlwaysThrow(); + backplane3.SetAlwaysThrow(); + + await Task.Delay(1_000); + + // 1 + _value = 1; + await cache1.SetAsync(key, _value, TimeSpan.FromMinutes(10)); + await Task.Delay(200); + + // 2 + _value = 2; + await cache2.SetAsync(key, _value, TimeSpan.FromMinutes(10)); + await Task.Delay(200); + + // 3 + _value = 3; + await cache3.SetAsync(key, _value, TimeSpan.FromMinutes(10)); + await Task.Delay(200); + + Assert.Equal(1, await cache1.GetOrSetAsync<int>(key, async _ => _value)); + Assert.Equal(2, await cache2.GetOrSetAsync<int>(key, async _ => _value)); + Assert.Equal(3, await cache3.GetOrSetAsync<int>(key, async _ => _value)); + + // RE-ENABLE THE BACKPLANE + backplane1.SetNeverThrow(); + backplane2.SetNeverThrow(); + backplane3.SetNeverThrow(); + + // WAIT FOR THE AUTO-RECOVERY DELAY + await Task.Delay(defaultOptions.AutoRecoveryDelay.PlusASecond()); + + Assert.Equal(3, await cache1.GetOrSetAsync<int>(key, async _ => _value)); + Assert.Equal(3, await cache2.GetOrSetAsync<int>(key, async _ => _value)); + Assert.Equal(3, await cache3.GetOrSetAsync<int>(key, async _ => _value)); + } + + [Theory] + [ClassData(typeof(SerializerTypesClassData))] + public void CanRecover(SerializerType serializerType) + { + var defaultOptions = new FusionCacheOptions(); + + var _value = 0; + + var key = "foo"; + + var distributedCache = CreateDistributedCache(); + + var backplaneConnectionId = Guid.NewGuid().ToString("N"); + + var backplane1 = CreateChaosBackplane(backplaneConnectionId); + var backplane2 = CreateChaosBackplane(backplaneConnectionId); + var backplane3 = CreateChaosBackplane(backplaneConnectionId); + + using var cache1 = CreateFusionCache(null, serializerType, distributedCache, backplane1, opt => { opt.EnableAutoRecovery = true; }); + using var cache2 = CreateFusionCache(null, serializerType, distributedCache, backplane2, opt => { opt.EnableAutoRecovery = true; }); + using var cache3 = CreateFusionCache(null, serializerType, distributedCache, backplane3, opt => { opt.EnableAutoRecovery = true; }); + + cache1.DefaultEntryOptions.AllowBackgroundBackplaneOperations = false; + cache2.DefaultEntryOptions.AllowBackgroundBackplaneOperations = false; + cache3.DefaultEntryOptions.AllowBackgroundBackplaneOperations = false; + + // DISABLE THE BACKPLANE + backplane1.SetAlwaysThrow(); + backplane2.SetAlwaysThrow(); + backplane3.SetAlwaysThrow(); + + Thread.Sleep(1_000); + + // 1 + _value = 1; + cache1.Set(key, _value, TimeSpan.FromMinutes(10)); + Thread.Sleep(200); + + // 2 + _value = 2; + cache2.Set(key, _value, TimeSpan.FromMinutes(10)); + Thread.Sleep(200); + + // 3 + _value = 3; + cache3.Set(key, _value, TimeSpan.FromMinutes(10)); + Thread.Sleep(200); + + Assert.Equal(1, cache1.GetOrSet<int>(key, _ => _value)); + Assert.Equal(2, cache2.GetOrSet<int>(key, _ => _value)); + Assert.Equal(3, cache3.GetOrSet<int>(key, _ => _value)); + + // RE-ENABLE THE BACKPLANE + backplane1.SetNeverThrow(); + backplane2.SetNeverThrow(); + backplane3.SetNeverThrow(); + + // WAIT FOR THE AUTO-RECOVERY DELAY + Thread.Sleep(defaultOptions.AutoRecoveryDelay.PlusASecond()); + + Assert.Equal(3, cache1.GetOrSet<int>(key, _ => _value)); + Assert.Equal(3, cache2.GetOrSet<int>(key, _ => _value)); + Assert.Equal(3, cache3.GetOrSet<int>(key, _ => _value)); + } + + [Theory] + [ClassData(typeof(SerializerTypesClassData))] + public async Task CanBeDisabledAsync(SerializerType serializerType) + { + var defaultOptions = new FusionCacheOptions(); + + var _value = 0; + + var key = "foo"; + + var distributedCache = CreateDistributedCache(); + + var backplaneConnectionId = Guid.NewGuid().ToString("N"); + + var backplane1 = CreateChaosBackplane(backplaneConnectionId); + var backplane2 = CreateChaosBackplane(backplaneConnectionId); + var backplane3 = CreateChaosBackplane(backplaneConnectionId); + + using var cache1 = CreateFusionCache(null, serializerType, distributedCache, backplane1, opt => { opt.EnableAutoRecovery = false; }); + using var cache2 = CreateFusionCache(null, serializerType, distributedCache, backplane2, opt => { opt.EnableAutoRecovery = false; }); + using var cache3 = CreateFusionCache(null, serializerType, distributedCache, backplane3, opt => { opt.EnableAutoRecovery = false; }); + + cache1.DefaultEntryOptions.AllowBackgroundBackplaneOperations = false; + cache2.DefaultEntryOptions.AllowBackgroundBackplaneOperations = false; + cache3.DefaultEntryOptions.AllowBackgroundBackplaneOperations = false; + + // DISABLE THE BACKPLANE + backplane1.SetAlwaysThrow(); + backplane2.SetAlwaysThrow(); + backplane3.SetAlwaysThrow(); + + await Task.Delay(1_000); + + // 1 + _value = 1; + await cache1.SetAsync(key, _value, TimeSpan.FromMinutes(10)); + await Task.Delay(200); + + // 2 + _value = 2; + await cache2.SetAsync(key, _value, TimeSpan.FromMinutes(10)); + await Task.Delay(200); + + // 3 + _value = 3; + await cache3.SetAsync(key, _value, TimeSpan.FromMinutes(10)); + await Task.Delay(200); + + Assert.Equal(1, await cache1.GetOrSetAsync<int>(key, async _ => _value)); + Assert.Equal(2, await cache2.GetOrSetAsync<int>(key, async _ => _value)); + Assert.Equal(3, await cache3.GetOrSetAsync<int>(key, async _ => _value)); + + // RE-ENABLE THE BACKPLANE + backplane1.SetNeverThrow(); + backplane2.SetNeverThrow(); + backplane3.SetNeverThrow(); + + // WAIT FOR THE AUTO-RECOVERY DELAY + await Task.Delay(defaultOptions.AutoRecoveryDelay.PlusASecond()); + + Assert.Equal(1, await cache1.GetOrSetAsync<int>(key, async _ => _value)); + Assert.Equal(2, await cache2.GetOrSetAsync<int>(key, async _ => _value)); + Assert.Equal(3, await cache3.GetOrSetAsync<int>(key, async _ => _value)); + } + + [Theory] + [ClassData(typeof(SerializerTypesClassData))] + public void CanBeDisabled(SerializerType serializerType) + { + var defaultOptions = new FusionCacheOptions(); + + var _value = 0; + + var key = "foo"; + + var distributedCache = CreateDistributedCache(); + + var backplaneConnectionId = Guid.NewGuid().ToString("N"); + + var backplane1 = CreateChaosBackplane(backplaneConnectionId); + var backplane2 = CreateChaosBackplane(backplaneConnectionId); + var backplane3 = CreateChaosBackplane(backplaneConnectionId); + + using var cache1 = CreateFusionCache(null, serializerType, distributedCache, backplane1, opt => { opt.EnableAutoRecovery = false; }); + using var cache2 = CreateFusionCache(null, serializerType, distributedCache, backplane2, opt => { opt.EnableAutoRecovery = false; }); + using var cache3 = CreateFusionCache(null, serializerType, distributedCache, backplane3, opt => { opt.EnableAutoRecovery = false; }); + + cache1.DefaultEntryOptions.AllowBackgroundBackplaneOperations = false; + cache2.DefaultEntryOptions.AllowBackgroundBackplaneOperations = false; + cache3.DefaultEntryOptions.AllowBackgroundBackplaneOperations = false; + + // DISABLE THE BACKPLANE + backplane1.SetAlwaysThrow(); + backplane2.SetAlwaysThrow(); + backplane3.SetAlwaysThrow(); + + Thread.Sleep(1_000); + + // 1 + _value = 1; + cache1.Set(key, _value, TimeSpan.FromMinutes(10)); + Thread.Sleep(200); + + // 2 + _value = 2; + cache2.Set(key, _value, TimeSpan.FromMinutes(10)); + Thread.Sleep(200); + + // 3 + _value = 3; + cache3.Set(key, _value, TimeSpan.FromMinutes(10)); + Thread.Sleep(200); + + Assert.Equal(1, cache1.GetOrSet<int>(key, _ => _value)); + Assert.Equal(2, cache2.GetOrSet<int>(key, _ => _value)); + Assert.Equal(3, cache3.GetOrSet<int>(key, _ => _value)); + + // RE-ENABLE THE BACKPLANE + backplane1.SetNeverThrow(); + backplane2.SetNeverThrow(); + backplane3.SetNeverThrow(); + + // WAIT FOR THE AUTO-RECOVERY DELAY + Thread.Sleep(defaultOptions.AutoRecoveryDelay.PlusASecond()); + + Assert.Equal(1, cache1.GetOrSet<int>(key, _ => _value)); + Assert.Equal(2, cache2.GetOrSet<int>(key, _ => _value)); + Assert.Equal(3, cache3.GetOrSet<int>(key, _ => _value)); + } + + [Theory] + [ClassData(typeof(SerializerTypesClassData))] + public async Task RespectsMaxItemsAsync(SerializerType serializerType) + { + var _value = 0; + + var key1 = "foo"; + var key2 = "bar"; + + var defaultOptions = new FusionCacheOptions(); + + var distributedCache = CreateDistributedCache(); + + var backplaneConnectionId = Guid.NewGuid().ToString("N"); + + var backplane1 = CreateChaosBackplane(backplaneConnectionId); + var backplane2 = CreateChaosBackplane(backplaneConnectionId); + var backplane3 = CreateChaosBackplane(backplaneConnectionId); + + using var cache1 = CreateFusionCache(null, serializerType, distributedCache, backplane1, opt => { opt.EnableAutoRecovery = true; opt.AutoRecoveryMaxItems = 1; }); + using var cache2 = CreateFusionCache(null, serializerType, distributedCache, backplane2, opt => { opt.EnableAutoRecovery = true; opt.AutoRecoveryMaxItems = 1; }); + using var cache3 = CreateFusionCache(null, serializerType, distributedCache, backplane3, opt => { opt.EnableAutoRecovery = true; opt.AutoRecoveryMaxItems = 1; }); + + cache1.DefaultEntryOptions.AllowBackgroundBackplaneOperations = false; + cache2.DefaultEntryOptions.AllowBackgroundBackplaneOperations = false; + cache3.DefaultEntryOptions.AllowBackgroundBackplaneOperations = false; + + // DISABLE THE BACKPLANE + backplane1.SetAlwaysThrow(); + backplane2.SetAlwaysThrow(); + backplane3.SetAlwaysThrow(); + + await Task.Delay(1_000); + + // 1 + _value = 1; + await cache1.SetAsync(key1, _value, TimeSpan.FromMinutes(10)); + await cache1.SetAsync(key2, _value, TimeSpan.FromMinutes(5)); + await Task.Delay(200); + + // 2 + _value = 2; + await cache2.SetAsync(key1, _value, TimeSpan.FromMinutes(10)); + await cache2.SetAsync(key2, _value, TimeSpan.FromMinutes(5)); + await Task.Delay(200); + + // 3 + _value = 3; + await cache3.SetAsync(key1, _value, TimeSpan.FromMinutes(10)); + await cache3.SetAsync(key2, _value, TimeSpan.FromMinutes(5)); + await Task.Delay(200); + + _value = 21; + + Assert.Equal(1, await cache1.GetOrSetAsync<int>(key1, async _ => _value)); + Assert.Equal(2, await cache2.GetOrSetAsync<int>(key1, async _ => _value)); + Assert.Equal(3, await cache3.GetOrSetAsync<int>(key1, async _ => _value)); + + Assert.Equal(1, await cache1.GetOrSetAsync<int>(key2, async _ => _value)); + Assert.Equal(2, await cache2.GetOrSetAsync<int>(key2, async _ => _value)); + Assert.Equal(3, await cache3.GetOrSetAsync<int>(key2, async _ => _value)); + + // RE-ENABLE THE BACKPLANE + backplane1.SetNeverThrow(); + backplane2.SetNeverThrow(); + backplane3.SetNeverThrow(); + + // WAIT FOR THE AUTO-RECOVERY DELAY + await Task.Delay(defaultOptions.AutoRecoveryDelay.PlusASecond()); + + _value = 42; + + Assert.Equal(3, await cache1.GetOrSetAsync<int>(key1, async _ => _value)); + Assert.Equal(3, await cache2.GetOrSetAsync<int>(key1, async _ => _value)); + Assert.Equal(3, await cache3.GetOrSetAsync<int>(key1, async _ => _value)); + + Assert.Equal(1, await cache1.GetOrSetAsync<int>(key2, async _ => _value)); + Assert.Equal(2, await cache2.GetOrSetAsync<int>(key2, async _ => _value)); + Assert.Equal(3, await cache3.GetOrSetAsync<int>(key2, async _ => _value)); + } + + [Theory] + [ClassData(typeof(SerializerTypesClassData))] + public void RespectsMaxItems(SerializerType serializerType) + { + var _value = 0; + + var key1 = "foo"; + var key2 = "bar"; + + var defaultOptions = new FusionCacheOptions(); + + var distributedCache = CreateDistributedCache(); + + var backplaneConnectionId = Guid.NewGuid().ToString("N"); + + var backplane1 = CreateChaosBackplane(backplaneConnectionId); + var backplane2 = CreateChaosBackplane(backplaneConnectionId); + var backplane3 = CreateChaosBackplane(backplaneConnectionId); + + using var cache1 = CreateFusionCache(null, serializerType, distributedCache, backplane1, opt => { opt.EnableAutoRecovery = true; opt.AutoRecoveryMaxItems = 1; }); + using var cache2 = CreateFusionCache(null, serializerType, distributedCache, backplane2, opt => { opt.EnableAutoRecovery = true; opt.AutoRecoveryMaxItems = 1; }); + using var cache3 = CreateFusionCache(null, serializerType, distributedCache, backplane3, opt => { opt.EnableAutoRecovery = true; opt.AutoRecoveryMaxItems = 1; }); + + cache1.DefaultEntryOptions.AllowBackgroundBackplaneOperations = false; + cache2.DefaultEntryOptions.AllowBackgroundBackplaneOperations = false; + cache3.DefaultEntryOptions.AllowBackgroundBackplaneOperations = false; + + // DISABLE THE BACKPLANE + backplane1.SetAlwaysThrow(); + backplane2.SetAlwaysThrow(); + backplane3.SetAlwaysThrow(); + + Thread.Sleep(1_000); + + // 1 + _value = 1; + cache1.Set(key1, _value, TimeSpan.FromMinutes(10)); + cache1.Set(key2, _value, TimeSpan.FromMinutes(5)); + Thread.Sleep(200); + + // 2 + _value = 2; + cache2.Set(key1, _value, TimeSpan.FromMinutes(10)); + cache2.Set(key2, _value, TimeSpan.FromMinutes(5)); + Thread.Sleep(200); + + // 3 + _value = 3; + cache3.Set(key1, _value, TimeSpan.FromMinutes(10)); + cache3.Set(key2, _value, TimeSpan.FromMinutes(5)); + Thread.Sleep(200); + + _value = 21; + + Assert.Equal(1, cache1.GetOrSet<int>(key1, _ => _value)); + Assert.Equal(2, cache2.GetOrSet<int>(key1, _ => _value)); + Assert.Equal(3, cache3.GetOrSet<int>(key1, _ => _value)); + + Assert.Equal(1, cache1.GetOrSet<int>(key2, _ => _value)); + Assert.Equal(2, cache2.GetOrSet<int>(key2, _ => _value)); + Assert.Equal(3, cache3.GetOrSet<int>(key2, _ => _value)); + + // RE-ENABLE THE BACKPLANE + backplane1.SetNeverThrow(); + backplane2.SetNeverThrow(); + backplane3.SetNeverThrow(); + + // WAIT FOR THE AUTO-RECOVERY DELAY + Thread.Sleep(defaultOptions.AutoRecoveryDelay.PlusASecond()); + + _value = 42; + + Assert.Equal(3, cache1.GetOrSet<int>(key1, _ => _value)); + Assert.Equal(3, cache2.GetOrSet<int>(key1, _ => _value)); + Assert.Equal(3, cache3.GetOrSet<int>(key1, _ => _value)); + + Assert.Equal(1, cache1.GetOrSet<int>(key2, _ => _value)); + Assert.Equal(2, cache2.GetOrSet<int>(key2, _ => _value)); + Assert.Equal(3, cache3.GetOrSet<int>(key2, _ => _value)); + } + + [Theory] + [ClassData(typeof(SerializerTypesClassData))] + public async Task CanHandleIssuesWithBothDistributedCacheAndBackplaneAsync(SerializerType serializerType) + { + var backplaneConnectionId = Guid.NewGuid().ToString("N"); + + var defaultOptions = new FusionCacheOptions(); + + var distributedCache = new MemoryDistributedCache(Options.Create(new MemoryDistributedCacheOptions())); + var chaosDistributedCache = new ChaosDistributedCache(distributedCache, logger: CreateXUnitLogger<ChaosDistributedCache>()); + + // SETUP CACHE A + var backplaneA = new MemoryBackplane(new MemoryBackplaneOptions() { ConnectionId = backplaneConnectionId }); + var chaosBackplaneA = new ChaosBackplane(backplaneA, logger: CreateXUnitLogger<ChaosBackplane>()); + using var cacheA = new FusionCache(CreateFusionCacheOptions(), logger: CreateXUnitLogger<FusionCache>()); + + cacheA.DefaultEntryOptions.Duration = TimeSpan.FromMinutes(10); + cacheA.DefaultEntryOptions.AllowBackgroundBackplaneOperations = false; + + cacheA.SetupDistributedCache(chaosDistributedCache, TestsUtils.GetSerializer(serializerType)); + cacheA.SetupBackplane(chaosBackplaneA); + + // SETUP CACHE B + var backplaneB = new MemoryBackplane(new MemoryBackplaneOptions() { ConnectionId = backplaneConnectionId }); + var chaosBackplaneB = new ChaosBackplane(backplaneB, logger: CreateXUnitLogger<ChaosBackplane>()); + using var cacheB = new FusionCache(CreateFusionCacheOptions(), logger: CreateXUnitLogger<FusionCache>()); + + cacheB.DefaultEntryOptions.Duration = TimeSpan.FromMinutes(10); + cacheB.DefaultEntryOptions.AllowBackgroundBackplaneOperations = false; + + cacheB.SetupDistributedCache(chaosDistributedCache, TestsUtils.GetSerializer(serializerType)); + cacheB.SetupBackplane(chaosBackplaneB); + + // SET ON CACHE A AND ON DISTRIBUTED CACHE + NOTIFY ON BACKPLANE + var vA1 = await cacheA.GetOrSetAsync<int>("foo", async _ => 10); + + // GET FROM DISTRIBUTED CACHE AND SET IT ON CACHE B + var vB1 = await cacheB.GetOrSetAsync<int>("foo", async _ => 20); + + // IN-SYNC + Assert.Equal(10, vA1); + Assert.Equal(10, vB1); + + // DISABLE DISTRIBUTED CACHE AND BACKPLANE + chaosDistributedCache.SetAlwaysThrow(); + chaosBackplaneA.SetAlwaysThrow(); + chaosBackplaneB.SetAlwaysThrow(); + + // SET ON CACHE B (NO DISTRIBUTED CACHE OR BACKPLANE, BECAUSE CHAOS) + await cacheB.SetAsync<int>("foo", 30); + + // GET FROM CACHE A (MEMORY CACHE) + var vA2 = await cacheA.GetOrDefaultAsync<int>("foo", 40); + + // GET FROM CACHE B (MEMORY CACHE) + var vB2 = await cacheB.GetOrDefaultAsync<int>("foo", 50); + + // NOT IN-SYNC + Assert.Equal(10, vA2); + Assert.Equal(30, vB2); + + // RE-ENABLE DISTRIBUTED CACHE AND BACKPLANE (SEND AUTO-RECOVERY NOTIFICATIONS) + chaosDistributedCache.SetNeverThrow(); + chaosBackplaneA.SetNeverThrow(); + chaosBackplaneB.SetNeverThrow(); + + // GIVE IT SOME TIME + await Task.Delay(defaultOptions.AutoRecoveryDelay.PlusASecond()); + + // GET FROM CACHE A (UPDATE FROM DISTRIBUTED) + var vA3 = await cacheA.GetOrSetAsync<int>("foo", async _ => 60); + + // GET FROM CACHE B + var vB3 = await cacheB.GetOrSetAsync<int>("foo", async _ => 70); + + Assert.Equal(30, vA3); + Assert.Equal(30, vB3); + } + + [Theory] + [ClassData(typeof(SerializerTypesClassData))] + public void CanHandleIssuesWithBothDistributedCacheAndBackplane(SerializerType serializerType) + { + var backplaneConnectionId = Guid.NewGuid().ToString("N"); + + var defaultOptions = new FusionCacheOptions(); + + var distributedCache = new MemoryDistributedCache(Options.Create(new MemoryDistributedCacheOptions())); + var chaosDistributedCache = new ChaosDistributedCache(distributedCache, logger: CreateXUnitLogger<ChaosDistributedCache>()); + + // SETUP CACHE A + var backplaneA = new MemoryBackplane(new MemoryBackplaneOptions() { ConnectionId = backplaneConnectionId }); + var chaosBackplaneA = new ChaosBackplane(backplaneA, logger: CreateXUnitLogger<ChaosBackplane>()); + using var cacheA = new FusionCache(CreateFusionCacheOptions(), logger: CreateXUnitLogger<FusionCache>()); + + cacheA.DefaultEntryOptions.Duration = TimeSpan.FromMinutes(10); + cacheA.DefaultEntryOptions.AllowBackgroundBackplaneOperations = false; + + cacheA.SetupDistributedCache(chaosDistributedCache, TestsUtils.GetSerializer(serializerType)); + cacheA.SetupBackplane(chaosBackplaneA); + + // SETUP CACHE B + var backplaneB = new MemoryBackplane(new MemoryBackplaneOptions() { ConnectionId = backplaneConnectionId }); + var chaosBackplaneB = new ChaosBackplane(backplaneB, logger: CreateXUnitLogger<ChaosBackplane>()); + using var cacheB = new FusionCache(CreateFusionCacheOptions(), logger: CreateXUnitLogger<FusionCache>()); + + cacheB.DefaultEntryOptions.Duration = TimeSpan.FromMinutes(10); + cacheB.DefaultEntryOptions.AllowBackgroundBackplaneOperations = false; + + cacheB.SetupDistributedCache(chaosDistributedCache, TestsUtils.GetSerializer(serializerType)); + cacheB.SetupBackplane(chaosBackplaneB); + + // SET ON CACHE A AND ON DISTRIBUTED CACHE + NOTIFY ON BACKPLANE + var vA1 = cacheA.GetOrSet<int>("foo", _ => 10); + + // GET FROM DISTRIBUTED CACHE AND SET IT ON CACHE B + var vB1 = cacheB.GetOrSet<int>("foo", _ => 20); + + // IN-SYNC + Assert.Equal(10, vA1); + Assert.Equal(10, vB1); + + // DISABLE DISTRIBUTED CACHE AND BACKPLANE + chaosDistributedCache.SetAlwaysThrow(); + chaosBackplaneA.SetAlwaysThrow(); + chaosBackplaneB.SetAlwaysThrow(); + + // SET ON CACHE B (NO DISTRIBUTED CACHE OR BACKPLANE, BECAUSE CHAOS) + cacheB.Set<int>("foo", 30); + + // GET FROM CACHE A (MEMORY CACHE) + var vA2 = cacheA.GetOrDefault<int>("foo", 40); + + // GET FROM CACHE B (MEMORY CACHE) + var vB2 = cacheB.GetOrDefault<int>("foo", 50); + + // NOT IN-SYNC + Assert.Equal(10, vA2); + Assert.Equal(30, vB2); + + // RE-ENABLE DISTRIBUTED CACHE AND BACKPLANE (SEND AUTO-RECOVERY NOTIFICATIONS) + chaosDistributedCache.SetNeverThrow(); + chaosBackplaneA.SetNeverThrow(); + chaosBackplaneB.SetNeverThrow(); + + // GIVE IT SOME TIME + Thread.Sleep(defaultOptions.AutoRecoveryDelay.PlusASecond()); + + // GET FROM CACHE A (UPDATE FROM DISTRIBUTED) + var vA3 = cacheA.GetOrSet<int>("foo", _ => 60); + + // GET FROM CACHE B + var vB3 = cacheB.GetOrSet<int>("foo", _ => 70); + + Assert.Equal(30, vA3); + Assert.Equal(30, vB3); + } + + [Theory] + [ClassData(typeof(SerializerTypesClassData))] + public async Task CanHandleReconnectedBackplaneWithoutReconnectedDistributedCacheAsync(SerializerType serializerType) + { + var backplaneConnectionId = Guid.NewGuid().ToString("N"); + + var defaultOptions = new FusionCacheOptions(); + + var distributedCache = new MemoryDistributedCache(Options.Create(new MemoryDistributedCacheOptions())); + var chaosDistributedCache = new ChaosDistributedCache(distributedCache, logger: CreateXUnitLogger<ChaosDistributedCache>()); + + // SETUP CACHE A + var backplaneA = new MemoryBackplane(new MemoryBackplaneOptions() { ConnectionId = backplaneConnectionId }); + var chaosBackplaneA = new ChaosBackplane(backplaneA, logger: CreateXUnitLogger<ChaosBackplane>()); + using var cacheA = new FusionCache(CreateFusionCacheOptions(), logger: CreateXUnitLogger<FusionCache>()); + + cacheA.DefaultEntryOptions.Duration = TimeSpan.FromMinutes(10); + cacheA.DefaultEntryOptions.AllowBackgroundBackplaneOperations = false; + + cacheA.SetupDistributedCache(chaosDistributedCache, TestsUtils.GetSerializer(serializerType)); + cacheA.SetupBackplane(chaosBackplaneA); + + // SETUP CACHE B + var backplaneB = new MemoryBackplane(new MemoryBackplaneOptions() { ConnectionId = backplaneConnectionId }); + var chaosBackplaneB = new ChaosBackplane(backplaneB, logger: CreateXUnitLogger<ChaosBackplane>()); + using var cacheB = new FusionCache(CreateFusionCacheOptions(), logger: CreateXUnitLogger<FusionCache>()); + + cacheB.DefaultEntryOptions.Duration = TimeSpan.FromMinutes(10); + cacheB.DefaultEntryOptions.AllowBackgroundBackplaneOperations = false; + + cacheB.SetupDistributedCache(chaosDistributedCache, TestsUtils.GetSerializer(serializerType)); + cacheB.SetupBackplane(chaosBackplaneB); + + // SET ON CACHE A AND ON DISTRIBUTED CACHE + NOTIFY ON BACKPLANE + var vA1 = await cacheA.GetOrSetAsync<int>("foo", async _ => 10); + + // GET FROM DISTRIBUTED CACHE AND SET IT ON CACHE B + var vB1 = await cacheB.GetOrSetAsync<int>("foo", async _ => 20); + + // IN-SYNC + Assert.Equal(10, vA1); + Assert.Equal(10, vB1); + + // DISABLE DISTRIBUTED CACHE AND BACKPLANE + chaosDistributedCache.SetAlwaysThrow(); + chaosBackplaneA.SetAlwaysThrow(); + chaosBackplaneB.SetAlwaysThrow(); + + // SET ON CACHE B (NO DISTRIBUTED CACHE OR BACKPLANE, BECAUSE CHAOS) + await cacheB.SetAsync<int>("foo", 30); + + // GET FROM CACHE A (MEMORY CACHE) + var vA2 = await cacheA.GetOrDefaultAsync<int>("foo", 40); + + // GET FROM CACHE B (MEMORY CACHE) + var vB2 = await cacheB.GetOrDefaultAsync<int>("foo", 50); + + // NOT IN-SYNC + Assert.Equal(10, vA2); + Assert.Equal(30, vB2); + + // RE-ENABLE BACKPLANE (SEND AUTO-RECOVERY NOTIFICATIONS, BUT SINCE DIST CACHE IS DOWN THEY WILL BE KEPT IN THE QUEUE) + chaosBackplaneA.SetNeverThrow(); + chaosBackplaneB.SetNeverThrow(); + + // GIVE IT SOME TIME + await Task.Delay(defaultOptions.AutoRecoveryDelay.PlusASecond()); + + // SET ON CACHE A AND ON DISTRIBUTED CACHE + NOTIFY ON BACKPLANE + var vA3 = await cacheA.GetOrDefaultAsync<int>("foo"); + + // GET FROM DISTRIBUTED CACHE AND SET IT ON CACHE B + var vB3 = await cacheB.GetOrDefaultAsync<int>("foo"); + + Assert.Equal(10, vA3); + Assert.Equal(30, vB3); + + // RE-ENABLE DISTRIBUTED CACHE + chaosDistributedCache.SetNeverThrow(); + + // GIVE IT SOME TIME TO RETRY AUTOMATICALLY + await Task.Delay(defaultOptions.AutoRecoveryDelay.PlusASecond()); + + // GET FROM CACHE A (UPDATE FROM DISTRIBUTED) + var vA4 = await cacheA.GetOrSetAsync<int>("foo", async _ => 60); + + // GET FROM CACHE B + var vB4 = await cacheB.GetOrSetAsync<int>("foo", async _ => 70); + + await Task.Delay(TimeSpan.FromMilliseconds(500)); + + Assert.Equal(30, vA4); + Assert.Equal(30, vB4); + } + + [Theory] + [ClassData(typeof(SerializerTypesClassData))] + public void CanHandleReconnectedBackplaneWithoutReconnectedDistributedCache(SerializerType serializerType) + { + var backplaneConnectionId = Guid.NewGuid().ToString("N"); + + var defaultOptions = new FusionCacheOptions(); + + var distributedCache = new MemoryDistributedCache(Options.Create(new MemoryDistributedCacheOptions())); + var chaosDistributedCache = new ChaosDistributedCache(distributedCache, logger: CreateXUnitLogger<ChaosDistributedCache>()); + + // SETUP CACHE A + var backplaneA = new MemoryBackplane(new MemoryBackplaneOptions() { ConnectionId = backplaneConnectionId }); + var chaosBackplaneA = new ChaosBackplane(backplaneA, logger: CreateXUnitLogger<ChaosBackplane>()); + using var cacheA = new FusionCache(CreateFusionCacheOptions(), logger: CreateXUnitLogger<FusionCache>()); + + cacheA.DefaultEntryOptions.Duration = TimeSpan.FromMinutes(10); + cacheA.DefaultEntryOptions.AllowBackgroundBackplaneOperations = false; + + cacheA.SetupDistributedCache(chaosDistributedCache, TestsUtils.GetSerializer(serializerType)); + cacheA.SetupBackplane(chaosBackplaneA); + + // SETUP CACHE B + var backplaneB = new MemoryBackplane(new MemoryBackplaneOptions() { ConnectionId = backplaneConnectionId }); + var chaosBackplaneB = new ChaosBackplane(backplaneB, logger: CreateXUnitLogger<ChaosBackplane>()); + using var cacheB = new FusionCache(CreateFusionCacheOptions(), logger: CreateXUnitLogger<FusionCache>()); + + cacheB.DefaultEntryOptions.Duration = TimeSpan.FromMinutes(10); + cacheB.DefaultEntryOptions.AllowBackgroundBackplaneOperations = false; + + cacheB.SetupDistributedCache(chaosDistributedCache, TestsUtils.GetSerializer(serializerType)); + cacheB.SetupBackplane(chaosBackplaneB); + + // SET ON CACHE A AND ON DISTRIBUTED CACHE + NOTIFY ON BACKPLANE + var vA1 = cacheA.GetOrSet<int>("foo", _ => 10); + + // GET FROM DISTRIBUTED CACHE AND SET IT ON CACHE B + var vB1 = cacheB.GetOrSet<int>("foo", _ => 20); + + // IN-SYNC + Assert.Equal(10, vA1); + Assert.Equal(10, vB1); + + // DISABLE DISTRIBUTED CACHE AND BACKPLANE + chaosDistributedCache.SetAlwaysThrow(); + chaosBackplaneA.SetAlwaysThrow(); + chaosBackplaneB.SetAlwaysThrow(); + + // SET ON CACHE B (NO DISTRIBUTED CACHE OR BACKPLANE, BECAUSE CHAOS) + cacheB.Set<int>("foo", 30); + + // GET FROM CACHE A (MEMORY CACHE) + var vA2 = cacheA.GetOrDefault<int>("foo", 40); + + // GET FROM CACHE B (MEMORY CACHE) + var vB2 = cacheB.GetOrDefault<int>("foo", 50); + + // NOT IN-SYNC + Assert.Equal(10, vA2); + Assert.Equal(30, vB2); + + // RE-ENABLE BACKPLANE (SEND AUTO-RECOVERY NOTIFICATIONS, BUT SINCE DIST CACHE IS DOWN THEY WILL BE KEPT IN THE QUEUE) + chaosBackplaneA.SetNeverThrow(); + chaosBackplaneB.SetNeverThrow(); + + // GIVE IT SOME TIME + Thread.Sleep(defaultOptions.AutoRecoveryDelay.PlusASecond()); + + // SET ON CACHE A AND ON DISTRIBUTED CACHE + NOTIFY ON BACKPLANE + var vA3 = cacheA.GetOrDefault<int>("foo"); + + // GET FROM DISTRIBUTED CACHE AND SET IT ON CACHE B + var vB3 = cacheB.GetOrDefault<int>("foo"); + + Assert.Equal(10, vA3); + Assert.Equal(30, vB3); + + // RE-ENABLE DISTRIBUTED CACHE + chaosDistributedCache.SetNeverThrow(); + + // GIVE IT SOME TIME TO RETRY AUTOMATICALLY + Thread.Sleep(defaultOptions.AutoRecoveryDelay.PlusASecond()); + + // GET FROM CACHE A (UPDATE FROM DISTRIBUTED) + var vA4 = cacheA.GetOrSet<int>("foo", _ => 60); + + // GET FROM CACHE B + var vB4 = cacheB.GetOrSet<int>("foo", _ => 70); + + Thread.Sleep(TimeSpan.FromMilliseconds(500)); + + Assert.Equal(30, vA4); + Assert.Equal(30, vB4); + } + + [Theory] + [ClassData(typeof(SerializerTypesClassData))] + public async Task CanHandleDistributedCacheErrorsWithBackplaneRetryAsync(SerializerType serializerType) + { + var backplaneConnectionId = Guid.NewGuid().ToString("N"); + + var defaultOptions = new FusionCacheOptions(); + + var distributedCache = new MemoryDistributedCache(Options.Create(new MemoryDistributedCacheOptions())); + var chaosDistributedCache = new ChaosDistributedCache(distributedCache, logger: CreateXUnitLogger<ChaosDistributedCache>()); + + // SETUP CACHE A + var backplaneA = new MemoryBackplane(new MemoryBackplaneOptions() { ConnectionId = backplaneConnectionId }); + var chaosBackplaneA = new ChaosBackplane(backplaneA, logger: CreateXUnitLogger<ChaosBackplane>()); + using var cacheA = new FusionCache(CreateFusionCacheOptions(), logger: CreateXUnitLogger<FusionCache>()); + + cacheA.DefaultEntryOptions.Duration = TimeSpan.FromMinutes(10); + cacheA.DefaultEntryOptions.AllowBackgroundBackplaneOperations = false; + + cacheA.SetupDistributedCache(chaosDistributedCache, TestsUtils.GetSerializer(serializerType)); + cacheA.SetupBackplane(chaosBackplaneA); + + // SETUP CACHE B + var backplaneB = new MemoryBackplane(new MemoryBackplaneOptions() { ConnectionId = backplaneConnectionId }); + var chaosBackplaneB = new ChaosBackplane(backplaneB, logger: CreateXUnitLogger<ChaosBackplane>()); + using var cacheB = new FusionCache(CreateFusionCacheOptions(), logger: CreateXUnitLogger<FusionCache>()); + + cacheB.DefaultEntryOptions.Duration = TimeSpan.FromMinutes(10); + cacheB.DefaultEntryOptions.AllowBackgroundBackplaneOperations = false; + + cacheB.SetupDistributedCache(chaosDistributedCache, TestsUtils.GetSerializer(serializerType)); + cacheB.SetupBackplane(chaosBackplaneB); + + // SET ON CACHE A AND ON DISTRIBUTED CACHE + NOTIFY ON BACKPLANE + var vA0 = cacheA.GetOrSet<int>("foo", _ => 10); + + // GET FROM DISTRIBUTED CACHE AND SET IT ON CACHE B + var vB0 = cacheB.GetOrSet<int>("foo", _ => 20); + + // IN-SYNC + Assert.Equal(10, vA0); + Assert.Equal(10, vB0); + + // DISABLE DISTRIBUTED CACHE + chaosDistributedCache.SetAlwaysThrow(); + + // SET ON CACHE B + await cacheB.SetAsync<int>("foo", 30); + + // GET FROM CACHE A + var vA1 = await cacheA.GetOrSetAsync<int>("foo", async _ => 31); + + // GET FROM CACHE B + var vB1 = await cacheB.GetOrSetAsync<int>("foo", async _ => 40); + + Assert.Equal(10, vA1); + Assert.Equal(30, vB1); + + // RE-ENABLE DISTRIBUTED CACHE + chaosDistributedCache.SetNeverThrow(); + + // WAIT FOR AUTO-RECOVERY TO KICK IN + await Task.Delay(defaultOptions.AutoRecoveryDelay.PlusASecond()); + + // SET ON CACHE A AND ON DISTRIBUTED CACHE + NOTIFY ON BACKPLANE + var vA2 = await cacheA.GetOrSetAsync<int>("foo", async _ => 50); + + await Task.Delay(TimeSpan.FromMilliseconds(500)); + + // GET FROM DISTRIBUTED CACHE AND SET IT ON CACHE B + var vB2 = await cacheB.GetOrSetAsync<int>("foo", async _ => 60); + + Assert.Equal(30, vA2); + Assert.Equal(30, vB2); + } + + [Theory] + [ClassData(typeof(SerializerTypesClassData))] + public void CanHandleDistributedCacheErrorsWithBackplaneRetry(SerializerType serializerType) + { + var backplaneConnectionId = Guid.NewGuid().ToString("N"); + + var defaultOptions = new FusionCacheOptions(); + + var distributedCache = new MemoryDistributedCache(Options.Create(new MemoryDistributedCacheOptions())); + var chaosDistributedCache = new ChaosDistributedCache(distributedCache, logger: CreateXUnitLogger<ChaosDistributedCache>()); + + // SETUP CACHE A + var backplaneA = new MemoryBackplane(new MemoryBackplaneOptions() { ConnectionId = backplaneConnectionId }); + var chaosBackplaneA = new ChaosBackplane(backplaneA, logger: CreateXUnitLogger<ChaosBackplane>()); + using var cacheA = new FusionCache(CreateFusionCacheOptions(), logger: CreateXUnitLogger<FusionCache>()); + + cacheA.DefaultEntryOptions.Duration = TimeSpan.FromMinutes(10); + cacheA.DefaultEntryOptions.AllowBackgroundBackplaneOperations = false; + + cacheA.SetupDistributedCache(chaosDistributedCache, TestsUtils.GetSerializer(serializerType)); + cacheA.SetupBackplane(chaosBackplaneA); + + // SETUP CACHE B + var backplaneB = new MemoryBackplane(new MemoryBackplaneOptions() { ConnectionId = backplaneConnectionId }); + var chaosBackplaneB = new ChaosBackplane(backplaneB, logger: CreateXUnitLogger<ChaosBackplane>()); + using var cacheB = new FusionCache(CreateFusionCacheOptions(), logger: CreateXUnitLogger<FusionCache>()); + + cacheB.DefaultEntryOptions.Duration = TimeSpan.FromMinutes(10); + cacheB.DefaultEntryOptions.AllowBackgroundBackplaneOperations = false; + + cacheB.SetupDistributedCache(chaosDistributedCache, TestsUtils.GetSerializer(serializerType)); + cacheB.SetupBackplane(chaosBackplaneB); + + // SET ON CACHE A AND ON DISTRIBUTED CACHE + NOTIFY ON BACKPLANE + var vA0 = cacheA.GetOrSet<int>("foo", _ => 10); + + // GET FROM DISTRIBUTED CACHE AND SET IT ON CACHE B + var vB0 = cacheB.GetOrSet<int>("foo", _ => 20); + + // IN-SYNC + Assert.Equal(10, vA0); + Assert.Equal(10, vB0); + + // DISABLE DISTRIBUTED CACHE + chaosDistributedCache.SetAlwaysThrow(); + + // SET ON CACHE B + cacheB.Set<int>("foo", 30); + + // GET FROM CACHE A + var vA1 = cacheA.GetOrSet<int>("foo", _ => 31); + + // GET FROM CACHE B + var vB1 = cacheB.GetOrSet<int>("foo", _ => 40); + + Assert.Equal(10, vA1); + Assert.Equal(30, vB1); + + // RE-ENABLE DISTRIBUTED CACHE + chaosDistributedCache.SetNeverThrow(); + + // WAIT FOR AUTO-RECOVERY TO KICK IN + Thread.Sleep(defaultOptions.AutoRecoveryDelay.PlusASecond()); + + // SET ON CACHE A AND ON DISTRIBUTED CACHE + NOTIFY ON BACKPLANE + var vA2 = cacheA.GetOrSet<int>("foo", _ => 50); + + Thread.Sleep(TimeSpan.FromMilliseconds(500)); + + // GET FROM DISTRIBUTED CACHE AND SET IT ON CACHE B + var vB2 = cacheB.GetOrSet<int>("foo", _ => 60); + + Assert.Equal(30, vA2); + Assert.Equal(30, vB2); + } + + [Fact] + public async Task CanHandleIssuesWithOnlyAndBackplaneAsync() + { + var backplaneConnectionId = Guid.NewGuid().ToString("N"); + + var defaultOptions = new FusionCacheOptions(); + + // SETUP CACHE A + var optionsA = CreateFusionCacheOptions(); + optionsA.DefaultEntryOptions.Duration = TimeSpan.FromMinutes(10); + optionsA.DefaultEntryOptions.AllowBackgroundBackplaneOperations = false; + optionsA.DefaultEntryOptions.SkipBackplaneNotifications = true; + + var backplaneA = new MemoryBackplane(new MemoryBackplaneOptions() { ConnectionId = backplaneConnectionId }); + var chaosBackplaneA = new ChaosBackplane(backplaneA, logger: CreateXUnitLogger<ChaosBackplane>()); + using var cacheA = new FusionCache(optionsA, logger: CreateXUnitLogger<FusionCache>()); + cacheA.SetupBackplane(chaosBackplaneA); + + // SETUP CACHE B + var optionsB = CreateFusionCacheOptions(); + optionsB.DefaultEntryOptions.Duration = TimeSpan.FromMinutes(10); + optionsB.DefaultEntryOptions.AllowBackgroundBackplaneOperations = false; + optionsB.DefaultEntryOptions.SkipBackplaneNotifications = true; + + var backplaneB = new MemoryBackplane(new MemoryBackplaneOptions() { ConnectionId = backplaneConnectionId }); + var chaosBackplaneB = new ChaosBackplane(backplaneB, logger: CreateXUnitLogger<ChaosBackplane>()); + using var cacheB = new FusionCache(optionsB, logger: CreateXUnitLogger<FusionCache>()); + cacheB.SetupBackplane(chaosBackplaneB); + + // SET ON CACHE A AND ON DISTRIBUTED CACHE + NOTIFY ON BACKPLANE + var vA1 = await cacheA.GetOrSetAsync<int>("foo", async _ => 10); + + // GET FROM DISTRIBUTED CACHE AND SET IT ON CACHE B + var vB1 = await cacheB.GetOrSetAsync<int>("foo", async _ => 10); + + // IN-SYNC + Assert.Equal(10, vA1); + Assert.Equal(10, vB1); + + // DISABLE BACKPLANE + chaosBackplaneA.SetAlwaysThrow(); + chaosBackplaneB.SetAlwaysThrow(); + + // SET ON CACHE B (NO BACKPLANE, BECAUSE CHAOS) + await cacheB.SetAsync<int>("foo", 30, opt => opt.SetSkipBackplaneNotifications(false)); + + // GET FROM CACHE A (MEMORY CACHE) + var vA2 = await cacheA.GetOrDefaultAsync<int>("foo", 40); + + // GET FROM CACHE B (MEMORY CACHE) + var vB2 = await cacheB.GetOrDefaultAsync<int>("foo", 50); + + // NOT IN-SYNC + Assert.Equal(10, vA2); + Assert.Equal(30, vB2); + + // RE-ENABLE BACKPLANE (SEND AUTO-RECOVERY NOTIFICATIONS) + chaosBackplaneA.SetNeverThrow(); + chaosBackplaneB.SetNeverThrow(); + + // GIVE IT SOME TIME + await Task.Delay(defaultOptions.AutoRecoveryDelay.PlusASecond()); + + // GET FROM CACHE A (NOTIFICATION FROM CACHE B EXPIRED THE ENTRY, SO IT WILL BE TAKEN AGAIN VIA THE FACTORY) + var vA3 = await cacheA.GetOrSetAsync<int>("foo", async _ => 30); + + // GET FROM CACHE B + var vB3 = await cacheB.GetOrSetAsync<int>("foo", async _ => 30); + + Assert.Equal(30, vA3); + Assert.Equal(30, vB3); + } + + [Fact] + public void CanHandleIssuesWithOnlyAndBackplane() + { + var backplaneConnectionId = Guid.NewGuid().ToString("N"); + + var defaultOptions = new FusionCacheOptions(); + + // SETUP CACHE A + var optionsA = CreateFusionCacheOptions(); + optionsA.DefaultEntryOptions.Duration = TimeSpan.FromMinutes(10); + optionsA.DefaultEntryOptions.AllowBackgroundBackplaneOperations = false; + optionsA.DefaultEntryOptions.SkipBackplaneNotifications = true; + + var backplaneA = new MemoryBackplane(new MemoryBackplaneOptions() { ConnectionId = backplaneConnectionId }); + var chaosBackplaneA = new ChaosBackplane(backplaneA, logger: CreateXUnitLogger<ChaosBackplane>()); + using var cacheA = new FusionCache(optionsA, logger: CreateXUnitLogger<FusionCache>()); + cacheA.SetupBackplane(chaosBackplaneA); + + // SETUP CACHE B + var optionsB = CreateFusionCacheOptions(); + optionsB.DefaultEntryOptions.Duration = TimeSpan.FromMinutes(10); + optionsB.DefaultEntryOptions.AllowBackgroundBackplaneOperations = false; + optionsB.DefaultEntryOptions.SkipBackplaneNotifications = true; + + var backplaneB = new MemoryBackplane(new MemoryBackplaneOptions() { ConnectionId = backplaneConnectionId }); + var chaosBackplaneB = new ChaosBackplane(backplaneB, logger: CreateXUnitLogger<ChaosBackplane>()); + using var cacheB = new FusionCache(optionsB, logger: CreateXUnitLogger<FusionCache>()); + cacheB.SetupBackplane(chaosBackplaneB); + + // SET ON CACHE A AND ON DISTRIBUTED CACHE + NOTIFY ON BACKPLANE + var vA1 = cacheA.GetOrSet<int>("foo", _ => 10); + + // GET FROM DISTRIBUTED CACHE AND SET IT ON CACHE B + var vB1 = cacheB.GetOrSet<int>("foo", _ => 10); + + // IN-SYNC + Assert.Equal(10, vA1); + Assert.Equal(10, vB1); + + // DISABLE BACKPLANE + chaosBackplaneA.SetAlwaysThrow(); + chaosBackplaneB.SetAlwaysThrow(); + + // SET ON CACHE B (NO BACKPLANE, BECAUSE CHAOS) + cacheB.Set<int>("foo", 30, opt => opt.SetSkipBackplaneNotifications(false)); + + // GET FROM CACHE A (MEMORY CACHE) + var vA2 = cacheA.GetOrDefault<int>("foo", 40); + + // GET FROM CACHE B (MEMORY CACHE) + var vB2 = cacheB.GetOrDefault<int>("foo", 50); + + // NOT IN-SYNC + Assert.Equal(10, vA2); + Assert.Equal(30, vB2); + + // RE-ENABLE BACKPLANE (SEND AUTO-RECOVERY NOTIFICATIONS) + chaosBackplaneA.SetNeverThrow(); + chaosBackplaneB.SetNeverThrow(); + + // GIVE IT SOME TIME + Thread.Sleep(defaultOptions.AutoRecoveryDelay.PlusASecond()); + + // GET FROM CACHE A (NOTIFICATION FROM CACHE B EXPIRED THE ENTRY, SO IT WILL BE TAKEN AGAIN VIA THE FACTORY) + var vA3 = cacheA.GetOrSet<int>("foo", _ => 30); + + // GET FROM CACHE B + var vB3 = cacheB.GetOrSet<int>("foo", _ => 30); + + Assert.Equal(30, vA3); + Assert.Equal(30, vB3); + } +} diff --git a/tests/ZiggyCreatures.FusionCache.Tests/BackplaneTests.cs b/tests/ZiggyCreatures.FusionCache.Tests/BackplaneTests.cs index fdac11fe..6908adef 100644 --- a/tests/ZiggyCreatures.FusionCache.Tests/BackplaneTests.cs +++ b/tests/ZiggyCreatures.FusionCache.Tests/BackplaneTests.cs @@ -1,11 +1,11 @@ using System; +using System.Diagnostics; using System.Threading; using System.Threading.Tasks; using FusionCacheTests.Stuff; using Microsoft.Extensions.Caching.Distributed; using Microsoft.Extensions.Caching.Memory; using Microsoft.Extensions.Caching.StackExchangeRedis; -using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; using Xunit; using Xunit.Abstractions; @@ -15,966 +15,785 @@ using ZiggyCreatures.Caching.Fusion.Backplane.StackExchangeRedis; using ZiggyCreatures.Caching.Fusion.Chaos; -namespace FusionCacheTests +namespace FusionCacheTests; + +public class BackplaneTests + : AbstractTests { - public class BackplaneTests + public BackplaneTests(ITestOutputHelper output) + : base(output, "MyCache:") { - private readonly ITestOutputHelper _output; - private static readonly string? _cacheKeyPrefix = "Foo:"; - - public BackplaneTests(ITestOutputHelper output) - { - _output = output; - } - - private XUnitLogger<T> CreateLogger<T>(LogLevel minLevel = LogLevel.Trace) - { - return new XUnitLogger<T>(minLevel, _output); - } - - private static FusionCacheOptions CreateFusionCacheOptions() - { - var res = new FusionCacheOptions(); - - res.CacheKeyPrefix = _cacheKeyPrefix; - - return res; - } - - private static readonly string? RedisConnection = null; - //private static readonly string? RedisConnection = "127.0.0.1:6379,ssl=False,abortConnect=False"; - - private IFusionCacheBackplane CreateBackplane(string connectionId) - { - if (string.IsNullOrWhiteSpace(RedisConnection)) - return new MemoryBackplane(new MemoryBackplaneOptions() { ConnectionId = connectionId }, logger: CreateLogger<MemoryBackplane>()); - - return new RedisBackplane(new RedisBackplaneOptions { Configuration = RedisConnection }, logger: CreateLogger<RedisBackplane>()); - } - - private ChaosBackplane CreateChaosBackplane(string connectionId) - { - return new ChaosBackplane(CreateBackplane(connectionId)); - } - - private static IDistributedCache CreateDistributedCache() - { - if (string.IsNullOrWhiteSpace(RedisConnection)) - return new MemoryDistributedCache(Options.Create(new MemoryDistributedCacheOptions())); - - return new RedisCache(new RedisCacheOptions { Configuration = RedisConnection }); - } - - private IFusionCache CreateFusionCache(string? cacheName, SerializerType? serializerType, IDistributedCache? distributedCache, IFusionCacheBackplane? backplane, Action<FusionCacheOptions>? setupAction = null) - { - var options = CreateFusionCacheOptions(); - - options.CacheName = cacheName!; - options.EnableSyncEventHandlersExecution = true; - - setupAction?.Invoke(options); - var fusionCache = new FusionCache(options, logger: CreateLogger<FusionCache>()); - fusionCache.DefaultEntryOptions.AllowBackgroundBackplaneOperations = false; - fusionCache.DefaultEntryOptions.AllowBackgroundDistributedCacheOperations = false; - if (distributedCache is not null && serializerType.HasValue) - fusionCache.SetupDistributedCache(distributedCache, TestsUtils.GetSerializer(serializerType.Value)); - if (backplane is not null) - fusionCache.SetupBackplane(backplane); - - return fusionCache; - } - - [Theory] - [ClassData(typeof(SerializerTypesClassData))] - public async Task BackplaneWorksAsync(SerializerType serializerType) - { - var key = Guid.NewGuid().ToString("N"); - var distributedCache = CreateDistributedCache(); - using var cache1 = CreateFusionCache(null, serializerType, distributedCache, null); - using var cache2 = CreateFusionCache(null, serializerType, distributedCache, null); - using var cache3 = CreateFusionCache(null, serializerType, distributedCache, null); - - cache1.DefaultEntryOptions.IsFailSafeEnabled = true; - cache2.DefaultEntryOptions.IsFailSafeEnabled = true; - cache3.DefaultEntryOptions.IsFailSafeEnabled = true; - - await cache1.GetOrSetAsync(key, async _ => 1, TimeSpan.FromMinutes(10)); - await cache2.GetOrSetAsync(key, async _ => 2, TimeSpan.FromMinutes(10)); - await cache3.GetOrSetAsync(key, async _ => 3, TimeSpan.FromMinutes(10)); - - Assert.Equal(1, await cache1.GetOrDefaultAsync<int>(key)); - Assert.Equal(1, await cache2.GetOrDefaultAsync<int>(key)); - Assert.Equal(1, await cache3.GetOrDefaultAsync<int>(key)); - - await cache1.SetAsync(key, 21); - - await Task.Delay(1_000); - - Assert.Equal(21, await cache1.GetOrDefaultAsync<int>(key)); - Assert.Equal(1, await cache2.GetOrDefaultAsync<int>(key)); - Assert.Equal(1, await cache3.GetOrDefaultAsync<int>(key)); - - var backplaneConnectionId = Guid.NewGuid().ToString("N"); - - cache1.SetupBackplane(CreateBackplane(backplaneConnectionId)); - cache2.SetupBackplane(CreateBackplane(backplaneConnectionId)); - cache3.SetupBackplane(CreateBackplane(backplaneConnectionId)); - - await Task.Delay(1_000); - - await cache1.SetAsync(key, 42); - - await Task.Delay(1_000); - - Assert.Equal(42, await cache1.GetOrDefaultAsync<int>(key)); - Assert.Equal(42, await cache2.GetOrDefaultAsync<int>(key)); - Assert.Equal(42, await cache3.GetOrDefaultAsync<int>(key)); - - await cache1.RemoveAsync(key); - - await Task.Delay(1_000); + } - Assert.Equal(0, cache1.GetOrDefault<int>(key)); - Assert.Equal(0, cache2.GetOrDefault<int>(key)); - Assert.Equal(0, cache3.GetOrDefault<int>(key)); - } + private FusionCacheOptions CreateFusionCacheOptions() + { + var res = new FusionCacheOptions(); - [Theory] - [ClassData(typeof(SerializerTypesClassData))] - public void BackplaneWorks(SerializerType serializerType) - { - var key = Guid.NewGuid().ToString("N"); - var distributedCache = CreateDistributedCache(); + res.CacheKeyPrefix = TestingCacheKeyPrefix; - using var cache1 = CreateFusionCache(null, serializerType, distributedCache, null); - using var cache2 = CreateFusionCache(null, serializerType, distributedCache, null); - using var cache3 = CreateFusionCache(null, serializerType, distributedCache, null); + return res; + } - cache1.DefaultEntryOptions.IsFailSafeEnabled = true; - cache2.DefaultEntryOptions.IsFailSafeEnabled = true; - cache3.DefaultEntryOptions.IsFailSafeEnabled = true; + private static readonly string? RedisConnection = null; + //private static readonly string? RedisConnection = "127.0.0.1:6379,ssl=False,abortConnect=False"; - cache1.GetOrSet(key, _ => 1, TimeSpan.FromMinutes(10)); - cache2.GetOrSet(key, _ => 2, TimeSpan.FromMinutes(10)); - cache3.GetOrSet(key, _ => 3, TimeSpan.FromMinutes(10)); + private IFusionCacheBackplane CreateBackplane(string connectionId) + { + if (string.IsNullOrWhiteSpace(RedisConnection)) + return new MemoryBackplane(new MemoryBackplaneOptions() { ConnectionId = connectionId }, logger: CreateXUnitLogger<MemoryBackplane>()); - Assert.Equal(1, cache1.GetOrDefault<int>(key)); - Assert.Equal(1, cache2.GetOrDefault<int>(key)); - Assert.Equal(1, cache3.GetOrDefault<int>(key)); + return new RedisBackplane(new RedisBackplaneOptions { Configuration = RedisConnection }, logger: CreateXUnitLogger<RedisBackplane>()); + } - cache1.Set(key, 21, TimeSpan.FromMinutes(10)); + private static IDistributedCache CreateDistributedCache() + { + if (string.IsNullOrWhiteSpace(RedisConnection)) + return new MemoryDistributedCache(Options.Create(new MemoryDistributedCacheOptions())); - Thread.Sleep(1_000); + return new RedisCache(new RedisCacheOptions { Configuration = RedisConnection }); + } - Assert.Equal(21, cache1.GetOrDefault<int>(key)); - Assert.Equal(1, cache2.GetOrDefault<int>(key)); - Assert.Equal(1, cache3.GetOrDefault<int>(key)); + private IFusionCache CreateFusionCache(string? cacheName, SerializerType? serializerType, IDistributedCache? distributedCache, IFusionCacheBackplane? backplane, Action<FusionCacheOptions>? setupAction = null) + { + var options = CreateFusionCacheOptions(); - var backplaneConnectionId = Guid.NewGuid().ToString("N"); + options.CacheName = cacheName!; + options.EnableSyncEventHandlersExecution = true; - cache1.SetupBackplane(CreateBackplane(backplaneConnectionId)); - cache2.SetupBackplane(CreateBackplane(backplaneConnectionId)); - cache3.SetupBackplane(CreateBackplane(backplaneConnectionId)); + setupAction?.Invoke(options); + var fusionCache = new FusionCache(options, logger: CreateXUnitLogger<FusionCache>()); + fusionCache.DefaultEntryOptions.AllowBackgroundBackplaneOperations = false; + fusionCache.DefaultEntryOptions.AllowBackgroundDistributedCacheOperations = false; + if (distributedCache is not null && serializerType.HasValue) + fusionCache.SetupDistributedCache(distributedCache, TestsUtils.GetSerializer(serializerType.Value)); + if (backplane is not null) + fusionCache.SetupBackplane(backplane); - Thread.Sleep(1_000); + return fusionCache; + } - cache1.Set(key, 42, TimeSpan.FromMinutes(10)); + [Theory] + [ClassData(typeof(SerializerTypesClassData))] + public async Task BackplaneWorksAsync(SerializerType serializerType) + { + var key = Guid.NewGuid().ToString("N"); + var distributedCache = CreateDistributedCache(); + using var cache1 = CreateFusionCache(null, serializerType, distributedCache, null); + using var cache2 = CreateFusionCache(null, serializerType, distributedCache, null); + using var cache3 = CreateFusionCache(null, serializerType, distributedCache, null); - Thread.Sleep(1_000); + cache1.DefaultEntryOptions.IsFailSafeEnabled = true; + cache2.DefaultEntryOptions.IsFailSafeEnabled = true; + cache3.DefaultEntryOptions.IsFailSafeEnabled = true; - Assert.Equal(42, cache1.GetOrDefault<int>(key)); - Assert.Equal(42, cache2.GetOrDefault<int>(key)); - Assert.Equal(42, cache3.GetOrDefault<int>(key)); + await cache1.GetOrSetAsync(key, async _ => 1, TimeSpan.FromMinutes(10)); + await cache2.GetOrSetAsync(key, async _ => 2, TimeSpan.FromMinutes(10)); + await cache3.GetOrSetAsync(key, async _ => 3, TimeSpan.FromMinutes(10)); - cache1.Remove(key); + Assert.Equal(1, await cache1.GetOrDefaultAsync<int>(key)); + Assert.Equal(1, await cache2.GetOrDefaultAsync<int>(key)); + Assert.Equal(1, await cache3.GetOrDefaultAsync<int>(key)); - Thread.Sleep(1_000); + await cache1.SetAsync(key, 21); - Assert.Equal(0, cache1.GetOrDefault<int>(key)); - Assert.Equal(0, cache2.GetOrDefault<int>(key)); - Assert.Equal(0, cache3.GetOrDefault<int>(key)); - } + await Task.Delay(1_000); - [Fact] - public async Task WorksWithDifferentCachesAsync() - { - var backplaneConnectionId = Guid.NewGuid().ToString("N"); + Assert.Equal(21, await cache1.GetOrDefaultAsync<int>(key)); + Assert.Equal(1, await cache2.GetOrDefaultAsync<int>(key)); + Assert.Equal(1, await cache3.GetOrDefaultAsync<int>(key)); - var key = Guid.NewGuid().ToString("N"); - using var cache1 = CreateFusionCache("C1", null, null, CreateBackplane(backplaneConnectionId)); - using var cache2 = CreateFusionCache("C2", null, null, CreateBackplane(backplaneConnectionId)); - using var cache2bis = CreateFusionCache("C2", null, null, CreateBackplane(backplaneConnectionId)); + var backplaneConnectionId = Guid.NewGuid().ToString("N"); - await Task.Delay(1_000); + cache1.SetupBackplane(CreateBackplane(backplaneConnectionId)); + cache2.SetupBackplane(CreateBackplane(backplaneConnectionId)); + cache3.SetupBackplane(CreateBackplane(backplaneConnectionId)); - await cache1.GetOrSetAsync(key, async _ => 1, TimeSpan.FromMinutes(10)); - await cache2.GetOrSetAsync(key, async _ => 2, TimeSpan.FromMinutes(10)); - await Task.Delay(1_000); - await cache2bis.GetOrSetAsync(key, async _ => 2, TimeSpan.FromMinutes(10)); - await Task.Delay(1_000); + await Task.Delay(1_000); - Assert.Equal(1, await cache1.GetOrDefaultAsync<int>(key)); - Assert.Equal(0, await cache2.GetOrDefaultAsync<int>(key)); - Assert.Equal(2, await cache2bis.GetOrDefaultAsync<int>(key)); + await cache1.SetAsync(key, 42); - await cache1.SetAsync(key, 21); - await cache2.SetAsync(key, 42); + await Task.Delay(1_000); - await Task.Delay(1_000); + Assert.Equal(42, await cache1.GetOrDefaultAsync<int>(key)); + Assert.Equal(42, await cache2.GetOrDefaultAsync<int>(key)); + Assert.Equal(42, await cache3.GetOrDefaultAsync<int>(key)); - Assert.Equal(21, await cache1.GetOrSetAsync(key, async _ => 78, TimeSpan.FromMinutes(10))); - Assert.Equal(42, await cache2.GetOrSetAsync(key, async _ => 78, TimeSpan.FromMinutes(10))); - await Task.Delay(1_000); - Assert.Equal(78, await cache2bis.GetOrSetAsync(key, async _ => 78, TimeSpan.FromMinutes(10))); - await Task.Delay(1_000); - Assert.Equal(88, await cache2.GetOrSetAsync(key, async _ => 88, TimeSpan.FromMinutes(10))); - } + await cache1.RemoveAsync(key); - [Fact] - public void WorksWithDifferentCaches() - { - var backplaneConnectionId = Guid.NewGuid().ToString("N"); + await Task.Delay(1_000); - var key = Guid.NewGuid().ToString("N"); - using var cache1 = CreateFusionCache("C1", null, null, CreateBackplane(backplaneConnectionId)); - using var cache2 = CreateFusionCache("C2", null, null, CreateBackplane(backplaneConnectionId)); - using var cache2bis = CreateFusionCache("C2", null, null, CreateBackplane(backplaneConnectionId)); + Assert.Equal(0, cache1.GetOrDefault<int>(key)); + Assert.Equal(0, cache2.GetOrDefault<int>(key)); + Assert.Equal(0, cache3.GetOrDefault<int>(key)); + } - Thread.Sleep(1_000); + [Theory] + [ClassData(typeof(SerializerTypesClassData))] + public void BackplaneWorks(SerializerType serializerType) + { + var key = Guid.NewGuid().ToString("N"); + var distributedCache = CreateDistributedCache(); - cache1.GetOrSet(key, _ => 1, TimeSpan.FromMinutes(10)); - cache2.GetOrSet(key, _ => 2, TimeSpan.FromMinutes(10)); - Thread.Sleep(1_000); - cache2bis.GetOrSet(key, _ => 2, TimeSpan.FromMinutes(10)); - Thread.Sleep(1_000); + using var cache1 = CreateFusionCache(null, serializerType, distributedCache, null); + using var cache2 = CreateFusionCache(null, serializerType, distributedCache, null); + using var cache3 = CreateFusionCache(null, serializerType, distributedCache, null); - Assert.Equal(1, cache1.GetOrDefault<int>(key)); - Assert.Equal(0, cache2.GetOrDefault<int>(key)); - Assert.Equal(2, cache2bis.GetOrDefault<int>(key)); + cache1.DefaultEntryOptions.IsFailSafeEnabled = true; + cache2.DefaultEntryOptions.IsFailSafeEnabled = true; + cache3.DefaultEntryOptions.IsFailSafeEnabled = true; - cache1.Set(key, 21); - cache2.Set(key, 42); + cache1.GetOrSet(key, _ => 1, TimeSpan.FromMinutes(10)); + cache2.GetOrSet(key, _ => 2, TimeSpan.FromMinutes(10)); + cache3.GetOrSet(key, _ => 3, TimeSpan.FromMinutes(10)); - Thread.Sleep(1_000); + Assert.Equal(1, cache1.GetOrDefault<int>(key)); + Assert.Equal(1, cache2.GetOrDefault<int>(key)); + Assert.Equal(1, cache3.GetOrDefault<int>(key)); - Assert.Equal(21, cache1.GetOrSet(key, _ => 78, TimeSpan.FromMinutes(10))); - Assert.Equal(42, cache2.GetOrSet(key, _ => 78, TimeSpan.FromMinutes(10))); - Thread.Sleep(1_000); - Assert.Equal(78, cache2bis.GetOrSet(key, _ => 78, TimeSpan.FromMinutes(10))); - Thread.Sleep(1_000); - Assert.Equal(88, cache2.GetOrSet(key, _ => 88, TimeSpan.FromMinutes(10))); - } + cache1.Set(key, 21, TimeSpan.FromMinutes(10)); - [Fact] - public async Task CanSkipNotificationsAsync() - { - var backplaneConnectionId = Guid.NewGuid().ToString("N"); + Thread.Sleep(1_000); - var key = Guid.NewGuid().ToString("N"); - using var cache1 = CreateFusionCache(null, null, null, CreateBackplane(backplaneConnectionId)); - using var cache2 = CreateFusionCache(null, null, null, CreateBackplane(backplaneConnectionId)); - using var cache3 = CreateFusionCache(null, null, null, CreateBackplane(backplaneConnectionId)); + Assert.Equal(21, cache1.GetOrDefault<int>(key)); + Assert.Equal(1, cache2.GetOrDefault<int>(key)); + Assert.Equal(1, cache3.GetOrDefault<int>(key)); - cache1.DefaultEntryOptions.SkipBackplaneNotifications = true; - cache2.DefaultEntryOptions.SkipBackplaneNotifications = true; - cache3.DefaultEntryOptions.SkipBackplaneNotifications = true; + var backplaneConnectionId = Guid.NewGuid().ToString("N"); - cache1.DefaultEntryOptions.AllowBackgroundBackplaneOperations = false; - cache2.DefaultEntryOptions.AllowBackgroundBackplaneOperations = false; - cache3.DefaultEntryOptions.AllowBackgroundBackplaneOperations = false; + cache1.SetupBackplane(CreateBackplane(backplaneConnectionId)); + cache2.SetupBackplane(CreateBackplane(backplaneConnectionId)); + cache3.SetupBackplane(CreateBackplane(backplaneConnectionId)); - await Task.Delay(1_000); + Thread.Sleep(1_000); - await cache1.SetAsync(key, 1, TimeSpan.FromMinutes(10)); - await Task.Delay(200); + cache1.Set(key, 42, TimeSpan.FromMinutes(10)); - await cache2.SetAsync(key, 2, TimeSpan.FromMinutes(10)); - await Task.Delay(200); + Thread.Sleep(1_000); - await cache3.SetAsync(key, 3, TimeSpan.FromMinutes(10)); - await Task.Delay(200); + Assert.Equal(42, cache1.GetOrDefault<int>(key)); + Assert.Equal(42, cache2.GetOrDefault<int>(key)); + Assert.Equal(42, cache3.GetOrDefault<int>(key)); - Assert.Equal(1, await cache1.GetOrDefaultAsync<int>(key)); - Assert.Equal(2, await cache2.GetOrDefaultAsync<int>(key)); - Assert.Equal(3, await cache3.GetOrDefaultAsync<int>(key)); - } + cache1.Remove(key); - [Fact] - public void CanSkipNotifications() - { - var backplaneConnectionId = Guid.NewGuid().ToString("N"); + Thread.Sleep(1_000); - var key = Guid.NewGuid().ToString("N"); - using var cache1 = CreateFusionCache(null, null, null, CreateBackplane(backplaneConnectionId)); - using var cache2 = CreateFusionCache(null, null, null, CreateBackplane(backplaneConnectionId)); - using var cache3 = CreateFusionCache(null, null, null, CreateBackplane(backplaneConnectionId)); + Assert.Equal(0, cache1.GetOrDefault<int>(key)); + Assert.Equal(0, cache2.GetOrDefault<int>(key)); + Assert.Equal(0, cache3.GetOrDefault<int>(key)); + } - cache1.DefaultEntryOptions.SkipBackplaneNotifications = true; - cache2.DefaultEntryOptions.SkipBackplaneNotifications = true; - cache3.DefaultEntryOptions.SkipBackplaneNotifications = true; + [Fact] + public async Task WorksWithDifferentCachesAsync() + { + var backplaneConnectionId = Guid.NewGuid().ToString("N"); - cache1.DefaultEntryOptions.AllowBackgroundBackplaneOperations = false; - cache2.DefaultEntryOptions.AllowBackgroundBackplaneOperations = false; - cache3.DefaultEntryOptions.AllowBackgroundBackplaneOperations = false; + var key = Guid.NewGuid().ToString("N"); + using var cache1 = CreateFusionCache("C1", null, null, CreateBackplane(backplaneConnectionId)); + using var cache2 = CreateFusionCache("C2", null, null, CreateBackplane(backplaneConnectionId)); + using var cache2bis = CreateFusionCache("C2", null, null, CreateBackplane(backplaneConnectionId)); - Thread.Sleep(1_000); + await Task.Delay(1_000); - cache1.Set(key, 1, TimeSpan.FromMinutes(10)); - Thread.Sleep(200); + await cache1.GetOrSetAsync(key, async _ => 1, TimeSpan.FromMinutes(10)); + await cache2.GetOrSetAsync(key, async _ => 2, TimeSpan.FromMinutes(10)); + await Task.Delay(1_000); + await cache2bis.GetOrSetAsync(key, async _ => 2, TimeSpan.FromMinutes(10)); + await Task.Delay(1_000); - cache2.Set(key, 2, TimeSpan.FromMinutes(10)); - Thread.Sleep(200); + Assert.Equal(1, await cache1.GetOrDefaultAsync<int>(key)); + Assert.Equal(0, await cache2.GetOrDefaultAsync<int>(key)); + Assert.Equal(2, await cache2bis.GetOrDefaultAsync<int>(key)); - cache3.Set(key, 3, TimeSpan.FromMinutes(10)); - Thread.Sleep(200); + await cache1.SetAsync(key, 21); + await cache2.SetAsync(key, 42); - Assert.Equal(1, cache1.GetOrDefault<int>(key)); - Assert.Equal(2, cache2.GetOrDefault<int>(key)); - Assert.Equal(3, cache3.GetOrDefault<int>(key)); - } + await Task.Delay(1_000); - [Theory] - [ClassData(typeof(SerializerTypesClassData))] - public async Task AutoRecoveryWorksAsync(SerializerType serializerType) - { - var defaultOptions = new FusionCacheOptions(); + Assert.Equal(21, await cache1.GetOrSetAsync(key, async _ => 78, TimeSpan.FromMinutes(10))); + Assert.Equal(42, await cache2.GetOrSetAsync(key, async _ => 78, TimeSpan.FromMinutes(10))); + await Task.Delay(1_000); + Assert.Equal(78, await cache2bis.GetOrSetAsync(key, async _ => 78, TimeSpan.FromMinutes(10))); + await Task.Delay(1_000); + Assert.Equal(88, await cache2.GetOrSetAsync(key, async _ => 88, TimeSpan.FromMinutes(10))); + } - var _value = 0; + [Fact] + public void WorksWithDifferentCaches() + { + var backplaneConnectionId = Guid.NewGuid().ToString("N"); - var key = "foo"; - var otherKey = "bar"; + var key = Guid.NewGuid().ToString("N"); + using var cache1 = CreateFusionCache("C1", null, null, CreateBackplane(backplaneConnectionId)); + using var cache2 = CreateFusionCache("C2", null, null, CreateBackplane(backplaneConnectionId)); + using var cache2bis = CreateFusionCache("C2", null, null, CreateBackplane(backplaneConnectionId)); - var distributedCache = CreateDistributedCache(); + Thread.Sleep(1_000); - var backplaneConnectionId = Guid.NewGuid().ToString("N"); + cache1.GetOrSet(key, _ => 1, TimeSpan.FromMinutes(10)); + cache2.GetOrSet(key, _ => 2, TimeSpan.FromMinutes(10)); + Thread.Sleep(1_000); + cache2bis.GetOrSet(key, _ => 2, TimeSpan.FromMinutes(10)); + Thread.Sleep(1_000); - var backplane1 = CreateChaosBackplane(backplaneConnectionId); - var backplane2 = CreateChaosBackplane(backplaneConnectionId); - var backplane3 = CreateChaosBackplane(backplaneConnectionId); + Assert.Equal(1, cache1.GetOrDefault<int>(key)); + Assert.Equal(0, cache2.GetOrDefault<int>(key)); + Assert.Equal(2, cache2bis.GetOrDefault<int>(key)); - using var cache1 = CreateFusionCache(null, serializerType, distributedCache, backplane1, opt => { opt.EnableBackplaneAutoRecovery = true; }); - using var cache2 = CreateFusionCache(null, serializerType, distributedCache, backplane2, opt => { opt.EnableBackplaneAutoRecovery = true; }); - using var cache3 = CreateFusionCache(null, serializerType, distributedCache, backplane3, opt => { opt.EnableBackplaneAutoRecovery = true; }); + cache1.Set(key, 21); + cache2.Set(key, 42); - cache1.DefaultEntryOptions.AllowBackgroundBackplaneOperations = false; - cache2.DefaultEntryOptions.AllowBackgroundBackplaneOperations = false; - cache3.DefaultEntryOptions.AllowBackgroundBackplaneOperations = false; + Thread.Sleep(1_000); - // DISABLE THE BACKPLANE - backplane1.SetAlwaysThrow(); - backplane2.SetAlwaysThrow(); - backplane3.SetAlwaysThrow(); + Assert.Equal(21, cache1.GetOrSet(key, _ => 78, TimeSpan.FromMinutes(10))); + Assert.Equal(42, cache2.GetOrSet(key, _ => 78, TimeSpan.FromMinutes(10))); + Thread.Sleep(1_000); + Assert.Equal(78, cache2bis.GetOrSet(key, _ => 78, TimeSpan.FromMinutes(10))); + Thread.Sleep(1_000); + Assert.Equal(88, cache2.GetOrSet(key, _ => 88, TimeSpan.FromMinutes(10))); + } - await Task.Delay(1_000); + [Fact] + public async Task CanSkipNotificationsAsync() + { + var backplaneConnectionId = Guid.NewGuid().ToString("N"); - // 1 - _value = 1; - await cache1.SetAsync(key, _value, TimeSpan.FromMinutes(10)); - await Task.Delay(200); + var key = Guid.NewGuid().ToString("N"); + using var cache1 = CreateFusionCache(null, null, null, CreateBackplane(backplaneConnectionId)); + using var cache2 = CreateFusionCache(null, null, null, CreateBackplane(backplaneConnectionId)); + using var cache3 = CreateFusionCache(null, null, null, CreateBackplane(backplaneConnectionId)); - // 2 - _value = 2; - await cache2.SetAsync(key, _value, TimeSpan.FromMinutes(10)); - await Task.Delay(200); + cache1.DefaultEntryOptions.SkipBackplaneNotifications = true; + cache2.DefaultEntryOptions.SkipBackplaneNotifications = true; + cache3.DefaultEntryOptions.SkipBackplaneNotifications = true; - // 3 - _value = 3; - await cache3.SetAsync(key, _value, TimeSpan.FromMinutes(10)); - await Task.Delay(200); + cache1.DefaultEntryOptions.AllowBackgroundBackplaneOperations = false; + cache2.DefaultEntryOptions.AllowBackgroundBackplaneOperations = false; + cache3.DefaultEntryOptions.AllowBackgroundBackplaneOperations = false; - Assert.Equal(1, await cache1.GetOrSetAsync<int>(key, async _ => _value)); - Assert.Equal(2, await cache2.GetOrSetAsync<int>(key, async _ => _value)); - Assert.Equal(3, await cache3.GetOrSetAsync<int>(key, async _ => _value)); + await Task.Delay(1_000); - // RE-ENABLE THE BACKPLANE - backplane1.SetNeverThrow(); - backplane2.SetNeverThrow(); - backplane3.SetNeverThrow(); + await cache1.SetAsync(key, 1, TimeSpan.FromMinutes(10)); + await Task.Delay(200); - await Task.Delay(defaultOptions.BackplaneAutoRecoveryReconnectDelay.PlusALittleBit()); + await cache2.SetAsync(key, 2, TimeSpan.FromMinutes(10)); + await Task.Delay(200); - // CHANGE ANOTHER KEY (TO RUN AUTO-RECOVERY OPERATIONS) - await cache1.SetAsync(otherKey, 42, TimeSpan.FromMinutes(10)); + await cache3.SetAsync(key, 3, TimeSpan.FromMinutes(10)); + await Task.Delay(200); - await Task.Delay(1_000); + Assert.Equal(1, await cache1.GetOrDefaultAsync<int>(key)); + Assert.Equal(2, await cache2.GetOrDefaultAsync<int>(key)); + Assert.Equal(3, await cache3.GetOrDefaultAsync<int>(key)); + } - Assert.Equal(3, await cache1.GetOrSetAsync<int>(key, async _ => _value)); - Assert.Equal(3, await cache2.GetOrSetAsync<int>(key, async _ => _value)); - Assert.Equal(3, await cache3.GetOrSetAsync<int>(key, async _ => _value)); - } + [Fact] + public void CanSkipNotifications() + { + var backplaneConnectionId = Guid.NewGuid().ToString("N"); - [Theory] - [ClassData(typeof(SerializerTypesClassData))] - public void AutoRecoveryWorks(SerializerType serializerType) - { - var defaultOptions = new FusionCacheOptions(); + var key = Guid.NewGuid().ToString("N"); + using var cache1 = CreateFusionCache(null, null, null, CreateBackplane(backplaneConnectionId)); + using var cache2 = CreateFusionCache(null, null, null, CreateBackplane(backplaneConnectionId)); + using var cache3 = CreateFusionCache(null, null, null, CreateBackplane(backplaneConnectionId)); - var _value = 0; + cache1.DefaultEntryOptions.SkipBackplaneNotifications = true; + cache2.DefaultEntryOptions.SkipBackplaneNotifications = true; + cache3.DefaultEntryOptions.SkipBackplaneNotifications = true; - var key = "foo"; - var otherKey = "bar"; + cache1.DefaultEntryOptions.AllowBackgroundBackplaneOperations = false; + cache2.DefaultEntryOptions.AllowBackgroundBackplaneOperations = false; + cache3.DefaultEntryOptions.AllowBackgroundBackplaneOperations = false; - var distributedCache = CreateDistributedCache(); + Thread.Sleep(1_000); - var backplaneConnectionId = Guid.NewGuid().ToString("N"); + cache1.Set(key, 1, TimeSpan.FromMinutes(10)); + Thread.Sleep(200); - var backplane1 = CreateChaosBackplane(backplaneConnectionId); - var backplane2 = CreateChaosBackplane(backplaneConnectionId); - var backplane3 = CreateChaosBackplane(backplaneConnectionId); + cache2.Set(key, 2, TimeSpan.FromMinutes(10)); + Thread.Sleep(200); - using var cache1 = CreateFusionCache(null, serializerType, distributedCache, backplane1, opt => { opt.EnableBackplaneAutoRecovery = true; }); - using var cache2 = CreateFusionCache(null, serializerType, distributedCache, backplane2, opt => { opt.EnableBackplaneAutoRecovery = true; }); - using var cache3 = CreateFusionCache(null, serializerType, distributedCache, backplane3, opt => { opt.EnableBackplaneAutoRecovery = true; }); + cache3.Set(key, 3, TimeSpan.FromMinutes(10)); + Thread.Sleep(200); - cache1.DefaultEntryOptions.AllowBackgroundBackplaneOperations = false; - cache2.DefaultEntryOptions.AllowBackgroundBackplaneOperations = false; - cache3.DefaultEntryOptions.AllowBackgroundBackplaneOperations = false; + Assert.Equal(1, cache1.GetOrDefault<int>(key)); + Assert.Equal(2, cache2.GetOrDefault<int>(key)); + Assert.Equal(3, cache3.GetOrDefault<int>(key)); + } - // DISABLE THE BACKPLANE - backplane1.SetAlwaysThrow(); - backplane2.SetAlwaysThrow(); - backplane3.SetAlwaysThrow(); + [Theory] + [ClassData(typeof(SerializerTypesClassData))] + public async Task CanHandleExpireOnMultiNodesAsync(SerializerType serializerType) + { + var backplaneConnectionId = Guid.NewGuid().ToString("N"); - Thread.Sleep(1_000); + var duration = TimeSpan.FromMinutes(10); - // 1 - _value = 1; - cache1.Set(key, _value, TimeSpan.FromMinutes(10)); - Thread.Sleep(200); + var distributedCache = new MemoryDistributedCache(Options.Create(new MemoryDistributedCacheOptions())); - // 2 - _value = 2; - cache2.Set(key, _value, TimeSpan.FromMinutes(10)); - Thread.Sleep(200); + using var cacheA = new FusionCache(CreateFusionCacheOptions(), logger: CreateXUnitLogger<FusionCache>()); + cacheA.SetupDistributedCache(distributedCache, TestsUtils.GetSerializer(serializerType)); + cacheA.SetupBackplane(CreateBackplane(backplaneConnectionId)); + cacheA.DefaultEntryOptions.IsFailSafeEnabled = true; + cacheA.DefaultEntryOptions.Duration = duration; + cacheA.DefaultEntryOptions.AllowBackgroundBackplaneOperations = false; - // 3 - _value = 3; - cache3.Set(key, _value, TimeSpan.FromMinutes(10)); - Thread.Sleep(200); + using var cacheB = new FusionCache(CreateFusionCacheOptions(), logger: CreateXUnitLogger<FusionCache>()); + cacheB.SetupDistributedCache(distributedCache, TestsUtils.GetSerializer(serializerType)); + cacheB.SetupBackplane(CreateBackplane(backplaneConnectionId)); + cacheB.DefaultEntryOptions.IsFailSafeEnabled = true; + cacheB.DefaultEntryOptions.Duration = duration; + cacheB.DefaultEntryOptions.AllowBackgroundBackplaneOperations = false; - Assert.Equal(1, cache1.GetOrSet<int>(key, _ => _value)); - Assert.Equal(2, cache2.GetOrSet<int>(key, _ => _value)); - Assert.Equal(3, cache3.GetOrSet<int>(key, _ => _value)); + using var cacheC = new FusionCache(CreateFusionCacheOptions(), logger: CreateXUnitLogger<FusionCache>()); + cacheC.SetupDistributedCache(distributedCache, TestsUtils.GetSerializer(serializerType)); + cacheC.SetupBackplane(CreateBackplane(backplaneConnectionId)); + cacheC.DefaultEntryOptions.IsFailSafeEnabled = true; + cacheC.DefaultEntryOptions.Duration = duration; + cacheC.DefaultEntryOptions.AllowBackgroundBackplaneOperations = false; - // RE-ENABLE THE BACKPLANE - backplane1.SetNeverThrow(); - backplane2.SetNeverThrow(); - backplane3.SetNeverThrow(); + await Task.Delay(TimeSpan.FromMilliseconds(200)); - Thread.Sleep(defaultOptions.BackplaneAutoRecoveryReconnectDelay.PlusALittleBit()); + // SET ON CACHE A + await cacheA.SetAsync<int>("foo", 42); - // CHANGE ANOTHER KEY (TO RUN AUTO-RECOVERY OPERATIONS) - cache1.Set(otherKey, 42, TimeSpan.FromMinutes(10)); + // GET ON CACHE A + var maybeFooA1 = await cacheA.TryGetAsync<int>("foo", opt => opt.SetFailSafe(true)); - Thread.Sleep(1_000); + // GET ON CACHE B (WILL GET FROM DISTRIBUTED CACHE AND SAVE ON LOCAL MEMORY CACHE) + var maybeFooB1 = await cacheB.TryGetAsync<int>("foo", opt => opt.SetFailSafe(true)); - Assert.Equal(3, cache1.GetOrSet<int>(key, _ => _value)); - Assert.Equal(3, cache2.GetOrSet<int>(key, _ => _value)); - Assert.Equal(3, cache3.GetOrSet<int>(key, _ => _value)); - } + // NOW CACHE A + B HAVE THE VALUE CACHED IN THEIR LOCAL MEMORY CACHE, WHILE CACHE C DOES NOT - [Theory] - [ClassData(typeof(SerializerTypesClassData))] - public async Task AutoRecoveryRespectsMaxItemsAsync(SerializerType serializerType) - { - var _value = 0; + // EXPIRE ON CACHE A, WHIS WILL: + // - EXPIRE ON CACHE A + // - REMOVE ON DISTRIBUTED CACHE + // - NOTIFY CACHE B AND CACHE C OF THE EXPIRATION AND THAT, IN TURN, WILL: + // - EXPIRE ON CACHE B + // - DO NOTHING ON CACHE C (IT WAS NOT IN ITS MEMORY CACHE) + await cacheA.ExpireAsync("foo"); - var key1 = "foo"; - var key2 = "bar"; + await Task.Delay(TimeSpan.FromMilliseconds(250)); - var defaultOptions = new FusionCacheOptions(); + // GET ON CACHE A: SINCE IT'S EXPIRED AND FAIL-SAFE IS DISABLED, NOTHING WILL BE RETURNED + var maybeFooA2 = await cacheA.TryGetAsync<int>("foo", opt => opt.SetFailSafe(false)); - var distributedCache = CreateDistributedCache(); + // GET ON CACHE B: SINCE IT'S EXPIRED AND FAIL-SAFE IS DISABLED, NOTHING WILL BE RETURNED + var maybeFooB2 = await cacheB.TryGetAsync<int>("foo", opt => opt.SetFailSafe(false)); - var backplaneConnectionId = Guid.NewGuid().ToString("N"); + // GET ON CACHE C: SINCE NOTHING IS THERE, NOTHING WILL BE RETURNED + var maybeFooC2 = await cacheC.TryGetAsync<int>("foo", opt => opt.SetFailSafe(false)); - var backplane1 = CreateChaosBackplane(backplaneConnectionId); - var backplane2 = CreateChaosBackplane(backplaneConnectionId); - var backplane3 = CreateChaosBackplane(backplaneConnectionId); + TestOutput.WriteLine($"BEFORE"); - using var cache1 = CreateFusionCache(null, serializerType, distributedCache, backplane1, opt => { opt.EnableBackplaneAutoRecovery = true; opt.BackplaneAutoRecoveryMaxItems = 1; }); - using var cache2 = CreateFusionCache(null, serializerType, distributedCache, backplane2, opt => { opt.EnableBackplaneAutoRecovery = true; opt.BackplaneAutoRecoveryMaxItems = 1; }); - using var cache3 = CreateFusionCache(null, serializerType, distributedCache, backplane3, opt => { opt.EnableBackplaneAutoRecovery = true; opt.BackplaneAutoRecoveryMaxItems = 1; }); + // GET ON CACHE A: SINCE IT'S EXPIRED BUT FAIL-SAFE IS ENABLED, THE STALE VALUE WILL BE RETURNED + var maybeFooA3 = await cacheA.TryGetAsync<int>("foo", opt => opt.SetFailSafe(true)); - cache1.DefaultEntryOptions.AllowBackgroundBackplaneOperations = false; - cache2.DefaultEntryOptions.AllowBackgroundBackplaneOperations = false; - cache3.DefaultEntryOptions.AllowBackgroundBackplaneOperations = false; + TestOutput.WriteLine($"AFTER"); - // DISABLE THE BACKPLANE - backplane1.SetAlwaysThrow(); - backplane2.SetAlwaysThrow(); - backplane3.SetAlwaysThrow(); + // GET ON CACHE B: SINCE IT'S EXPIRED BUT FAIL-SAFE IS ENABLED, THE STALE VALUE WILL BE RETURNED + var maybeFooB3 = await cacheB.TryGetAsync<int>("foo", opt => opt.SetFailSafe(true)); - await Task.Delay(1_000); + // GET ON CACHE C: SINCE NOTHING IS THERE, NOTHING WILL BE RETURNED + var maybeFooC3 = await cacheC.TryGetAsync<int>("foo", opt => opt.SetFailSafe(true)); - // 1 - _value = 1; - await cache1.SetAsync(key1, _value, TimeSpan.FromMinutes(10)); - await cache1.SetAsync(key2, _value, TimeSpan.FromMinutes(5)); - await Task.Delay(200); + await Task.Delay(TimeSpan.FromMilliseconds(200)); - // 2 - _value = 2; - await cache2.SetAsync(key1, _value, TimeSpan.FromMinutes(10)); - await cache2.SetAsync(key2, _value, TimeSpan.FromMinutes(5)); - await Task.Delay(200); + Assert.True(maybeFooA1.HasValue); + Assert.Equal(42, maybeFooA1.Value); - // 3 - _value = 3; - await cache3.SetAsync(key1, _value, TimeSpan.FromMinutes(10)); - await cache3.SetAsync(key2, _value, TimeSpan.FromMinutes(5)); - await Task.Delay(200); + Assert.True(maybeFooB1.HasValue); + Assert.Equal(42, maybeFooB1.Value); - _value = 21; + Assert.False(maybeFooA2.HasValue); + Assert.False(maybeFooB2.HasValue); + Assert.False(maybeFooC2.HasValue); - Assert.Equal(1, await cache1.GetOrSetAsync<int>(key1, async _ => _value)); - Assert.Equal(2, await cache2.GetOrSetAsync<int>(key1, async _ => _value)); - Assert.Equal(3, await cache3.GetOrSetAsync<int>(key1, async _ => _value)); + Assert.True(maybeFooA3.HasValue); + Assert.Equal(42, maybeFooA3.Value); - Assert.Equal(1, await cache1.GetOrSetAsync<int>(key2, async _ => _value)); - Assert.Equal(2, await cache2.GetOrSetAsync<int>(key2, async _ => _value)); - Assert.Equal(3, await cache3.GetOrSetAsync<int>(key2, async _ => _value)); + Assert.True(maybeFooB3.HasValue); + Assert.Equal(42, maybeFooB3.Value); - // RE-ENABLE THE BACKPLANE - backplane1.SetNeverThrow(); - backplane2.SetNeverThrow(); - backplane3.SetNeverThrow(); + Assert.False(maybeFooC3.HasValue); + } - // CHANGE ANOTHER KEY (TO RUN AUTO-RECOVERY OPERATIONS) - //await cache1.SetAsync(otherKey, 42, TimeSpan.FromMinutes(10)); + [Theory] + [ClassData(typeof(SerializerTypesClassData))] + public void CanHandleExpireOnMultiNodes(SerializerType serializerType) + { + var backplaneConnectionId = Guid.NewGuid().ToString("N"); - await Task.Delay(defaultOptions.BackplaneAutoRecoveryReconnectDelay.PlusALittleBit()); + var duration = TimeSpan.FromMinutes(10); - _value = 42; + var distributedCache = new MemoryDistributedCache(Options.Create(new MemoryDistributedCacheOptions())); - Assert.Equal(_value, await cache1.GetOrSetAsync<int>(key1, async _ => _value)); - Assert.Equal(_value, await cache2.GetOrSetAsync<int>(key1, async _ => _value)); - Assert.Equal(_value, await cache3.GetOrSetAsync<int>(key1, async _ => _value)); + using var cacheA = new FusionCache(CreateFusionCacheOptions(), logger: CreateXUnitLogger<FusionCache>()); + cacheA.SetupDistributedCache(distributedCache, TestsUtils.GetSerializer(serializerType)); + cacheA.SetupBackplane(CreateBackplane(backplaneConnectionId)); + cacheA.DefaultEntryOptions.IsFailSafeEnabled = true; + cacheA.DefaultEntryOptions.Duration = duration; + cacheA.DefaultEntryOptions.AllowBackgroundBackplaneOperations = false; - Assert.Equal(1, await cache1.GetOrSetAsync<int>(key2, async _ => _value)); - Assert.Equal(2, await cache2.GetOrSetAsync<int>(key2, async _ => _value)); - Assert.Equal(3, await cache3.GetOrSetAsync<int>(key2, async _ => _value)); - } + using var cacheB = new FusionCache(CreateFusionCacheOptions(), logger: CreateXUnitLogger<FusionCache>()); + cacheB.SetupDistributedCache(distributedCache, TestsUtils.GetSerializer(serializerType)); + cacheB.SetupBackplane(CreateBackplane(backplaneConnectionId)); + cacheB.DefaultEntryOptions.IsFailSafeEnabled = true; + cacheB.DefaultEntryOptions.Duration = duration; + cacheB.DefaultEntryOptions.AllowBackgroundBackplaneOperations = false; - [Theory] - [ClassData(typeof(SerializerTypesClassData))] - public void AutoRecoveryRespectsMaxItems(SerializerType serializerType) - { - var _value = 0; + using var cacheC = new FusionCache(CreateFusionCacheOptions(), logger: CreateXUnitLogger<FusionCache>()); + cacheC.SetupDistributedCache(distributedCache, TestsUtils.GetSerializer(serializerType)); + cacheC.SetupBackplane(CreateBackplane(backplaneConnectionId)); + cacheC.DefaultEntryOptions.IsFailSafeEnabled = true; + cacheC.DefaultEntryOptions.Duration = duration; + cacheC.DefaultEntryOptions.AllowBackgroundBackplaneOperations = false; - var key1 = "foo"; - var key2 = "bar"; + Thread.Sleep(TimeSpan.FromMilliseconds(200)); - var defaultOptions = new FusionCacheOptions(); + // SET ON CACHE A + cacheA.Set<int>("foo", 42); - var distributedCache = CreateDistributedCache(); + // GET ON CACHE A + var maybeFooA1 = cacheA.TryGet<int>("foo", opt => opt.SetFailSafe(true)); - var backplaneConnectionId = Guid.NewGuid().ToString("N"); + // GET ON CACHE B (WILL GET FROM DISTRIBUTED CACHE AND SAVE ON LOCAL MEMORY CACHE) + var maybeFooB1 = cacheB.TryGet<int>("foo", opt => opt.SetFailSafe(true)); - var backplane1 = CreateChaosBackplane(backplaneConnectionId); - var backplane2 = CreateChaosBackplane(backplaneConnectionId); - var backplane3 = CreateChaosBackplane(backplaneConnectionId); + // NOW CACHE A + B HAVE THE VALUE CACHED IN THEIR LOCAL MEMORY CACHE, WHILE CACHE C DOES NOT - using var cache1 = CreateFusionCache(null, serializerType, distributedCache, backplane1, opt => { opt.EnableBackplaneAutoRecovery = true; opt.BackplaneAutoRecoveryMaxItems = 1; }); - using var cache2 = CreateFusionCache(null, serializerType, distributedCache, backplane2, opt => { opt.EnableBackplaneAutoRecovery = true; opt.BackplaneAutoRecoveryMaxItems = 1; }); - using var cache3 = CreateFusionCache(null, serializerType, distributedCache, backplane3, opt => { opt.EnableBackplaneAutoRecovery = true; opt.BackplaneAutoRecoveryMaxItems = 1; }); + // EXPIRE ON CACHE A, WHIS WILL: + // - EXPIRE ON CACHE A + // - REMOVE ON DISTRIBUTED CACHE + // - NOTIFY CACHE B AND CACHE C OF THE EXPIRATION AND THAT, IN TURN, WILL: + // - EXPIRE ON CACHE B + // - DO NOTHING ON CACHE C (IT WAS NOT IN ITS MEMORY CACHE) + cacheA.Expire("foo"); - cache1.DefaultEntryOptions.AllowBackgroundBackplaneOperations = false; - cache2.DefaultEntryOptions.AllowBackgroundBackplaneOperations = false; - cache3.DefaultEntryOptions.AllowBackgroundBackplaneOperations = false; + Thread.Sleep(TimeSpan.FromMilliseconds(250)); - // DISABLE THE BACKPLANE - backplane1.SetAlwaysThrow(); - backplane2.SetAlwaysThrow(); - backplane3.SetAlwaysThrow(); + // GET ON CACHE A: SINCE IT'S EXPIRED AND FAIL-SAFE IS DISABLED, NOTHING WILL BE RETURNED + var maybeFooA2 = cacheA.TryGet<int>("foo", opt => opt.SetFailSafe(false)); - Thread.Sleep(1_000); + // GET ON CACHE B: SINCE IT'S EXPIRED AND FAIL-SAFE IS DISABLED, NOTHING WILL BE RETURNED + var maybeFooB2 = cacheB.TryGet<int>("foo", opt => opt.SetFailSafe(false)); - // 1 - _value = 1; - cache1.Set(key1, _value, TimeSpan.FromMinutes(10)); - cache1.Set(key2, _value, TimeSpan.FromMinutes(5)); - Thread.Sleep(200); + // GET ON CACHE C: SINCE NOTHING IS THERE, NOTHING WILL BE RETURNED + var maybeFooC2 = cacheC.TryGet<int>("foo", opt => opt.SetFailSafe(false)); - // 2 - _value = 2; - cache2.Set(key1, _value, TimeSpan.FromMinutes(10)); - cache2.Set(key2, _value, TimeSpan.FromMinutes(5)); - Thread.Sleep(200); + TestOutput.WriteLine($"BEFORE"); - // 3 - _value = 3; - cache3.Set(key1, _value, TimeSpan.FromMinutes(10)); - cache3.Set(key2, _value, TimeSpan.FromMinutes(5)); - Thread.Sleep(200); + // GET ON CACHE A: SINCE IT'S EXPIRED BUT FAIL-SAFE IS ENABLED, THE STALE VALUE WILL BE RETURNED + var maybeFooA3 = cacheA.TryGet<int>("foo", opt => opt.SetFailSafe(true)); - _value = 21; + TestOutput.WriteLine($"AFTER"); - Assert.Equal(1, cache1.GetOrSet<int>(key1, _ => _value)); - Assert.Equal(2, cache2.GetOrSet<int>(key1, _ => _value)); - Assert.Equal(3, cache3.GetOrSet<int>(key1, _ => _value)); + // GET ON CACHE B: SINCE IT'S EXPIRED BUT FAIL-SAFE IS ENABLED, THE STALE VALUE WILL BE RETURNED + var maybeFooB3 = cacheB.TryGet<int>("foo", opt => opt.SetFailSafe(true)); - Assert.Equal(1, cache1.GetOrSet<int>(key2, _ => _value)); - Assert.Equal(2, cache2.GetOrSet<int>(key2, _ => _value)); - Assert.Equal(3, cache3.GetOrSet<int>(key2, _ => _value)); + // GET ON CACHE C: SINCE NOTHING IS THERE, NOTHING WILL BE RETURNED + var maybeFooC3 = cacheC.TryGet<int>("foo", opt => opt.SetFailSafe(true)); - // RE-ENABLE THE BACKPLANE - backplane1.SetNeverThrow(); - backplane2.SetNeverThrow(); - backplane3.SetNeverThrow(); + Thread.Sleep(TimeSpan.FromMilliseconds(200)); - // CHANGE ANOTHER KEY (TO RUN AUTO-RECOVERY OPERATIONS) - //await cache1.SetAsync(otherKey, 42, TimeSpan.FromMinutes(10)); + Assert.True(maybeFooA1.HasValue); + Assert.Equal(42, maybeFooA1.Value); - Thread.Sleep(defaultOptions.BackplaneAutoRecoveryReconnectDelay.PlusALittleBit()); + Assert.True(maybeFooB1.HasValue); + Assert.Equal(42, maybeFooB1.Value); - _value = 42; + Assert.False(maybeFooA2.HasValue); + Assert.False(maybeFooB2.HasValue); + Assert.False(maybeFooC2.HasValue); - Assert.Equal(_value, cache1.GetOrSet<int>(key1, _ => _value)); - Assert.Equal(_value, cache2.GetOrSet<int>(key1, _ => _value)); - Assert.Equal(_value, cache3.GetOrSet<int>(key1, _ => _value)); + Assert.True(maybeFooA3.HasValue); + Assert.Equal(42, maybeFooA3.Value); - Assert.Equal(1, cache1.GetOrSet<int>(key2, _ => _value)); - Assert.Equal(2, cache2.GetOrSet<int>(key2, _ => _value)); - Assert.Equal(3, cache3.GetOrSet<int>(key2, _ => _value)); - } + Assert.True(maybeFooB3.HasValue); + Assert.Equal(42, maybeFooB3.Value); - [Theory] - [ClassData(typeof(SerializerTypesClassData))] - public async Task CanHandleExpireOnMultiNodesAsync(SerializerType serializerType) - { - var backplaneConnectionId = Guid.NewGuid().ToString("N"); + Assert.False(maybeFooC3.HasValue); + } - var duration = TimeSpan.FromMinutes(10); + [Theory] + [ClassData(typeof(SerializerTypesClassData))] + public async Task BackgroundFactoryCompleteNotifyOtherNodesAsync(SerializerType serializerType) + { + var backplaneConnectionId = Guid.NewGuid().ToString("N"); + + var duration1 = TimeSpan.FromSeconds(1); + var duration2 = TimeSpan.FromSeconds(10); + var factorySoftTimeout = TimeSpan.FromMilliseconds(50); + var simulatedFactoryDuration = TimeSpan.FromSeconds(3); + + var distributedCache = new MemoryDistributedCache(Options.Create(new MemoryDistributedCacheOptions())); + + var optionsA = CreateFusionCacheOptions(); + optionsA.SetInstanceId("A"); + //optionsA.DefaultEntryOptions.AllowBackgroundBackplaneOperations = false; + optionsA.DefaultEntryOptions.IsFailSafeEnabled = true; + optionsA.DefaultEntryOptions.FactorySoftTimeout = factorySoftTimeout; + using var cacheA = new FusionCache(optionsA, logger: CreateXUnitLogger<FusionCache>()); + cacheA.SetupDistributedCache(distributedCache, TestsUtils.GetSerializer(serializerType)); + cacheA.SetupBackplane(CreateBackplane(backplaneConnectionId)); + + var optionsB = CreateFusionCacheOptions(); + optionsB.SetInstanceId("B"); + //optionsB.DefaultEntryOptions.AllowBackgroundBackplaneOperations = false; + optionsB.DefaultEntryOptions.IsFailSafeEnabled = true; + optionsB.DefaultEntryOptions.FactorySoftTimeout = factorySoftTimeout; + using var cacheB = new FusionCache(optionsB, logger: CreateXUnitLogger<FusionCache>()); + cacheB.SetupDistributedCache(distributedCache, TestsUtils.GetSerializer(serializerType)); + cacheB.SetupBackplane(CreateBackplane(backplaneConnectionId)); + + // SET 10 ON CACHE-A AND DIST CACHE + var fooA1 = await cacheA.GetOrSetAsync("foo", async _ => 10, duration1); + + // GET 10 FROM DIST CACHE AND SET ON CACHE-B + var fooB1 = await cacheB.GetOrSetAsync("foo", async _ => 20, duration1); + + Assert.Equal(10, fooA1); + Assert.Equal(10, fooB1); + + // WAIT FOR THE CACHE ENTRIES TO EXPIRE + await Task.Delay(duration1.PlusALittleBit()); + + // EXECUTE THE FACTORY ON CACHE-A, WHICH WILL TAKE 3 SECONDS, BUT + // THE FACTORY SOFT TIMEOUT IS 50 MILLISECONDS, SO IT WILL FAIL + // AND THE STALE VALUE WILL BE RETURNED + // THE FACTORY WILL BE KEPT RUNNING IN THE BACKGROUND, AND WHEN + // IT WILL COMPLETE SUCCESSFULLY UPDATE CACHE-A, THE DIST + // CACHE AND NOTIFY THE OTHER NODES + // SUCESSFULLY UPDATE CACHE-A, THE DIST CACHE AND NOTIFY THE OTHER NODES + var fooA2 = await cacheA.GetOrSetAsync( + "foo", + async _ => + { + await Task.Delay(simulatedFactoryDuration); + return 30; + }, + duration2 + ); + + // IMMEDIATELY GET OR SET FROM CACHE-B: THE VALUE THERE IS + // EXPIRED, SO THE NEW VALUE WILL BE SAVED AND RETURNED + var fooB2 = await cacheB.GetOrSetAsync( + "foo", + 40, + duration2 + ); + + Assert.Equal(10, fooA2); + Assert.Equal(40, fooB2); + + // WAIT FOR THE SIMULATED FACTORY TO COMPLETE: A NOTIFICATION + // WILL BE SENT TO THE OTHER NODES, WHICH IN TURN WILL UPDATE + // THEIR CACHE ENTRIES + await Task.Delay(simulatedFactoryDuration.PlusALittleBit()); + + // GET THE UPDATED VALUES FROM CACHE-A AND CACHE-B + var fooA3 = await cacheA.GetOrDefaultAsync<int>("foo"); + var fooB3 = await cacheB.GetOrDefaultAsync<int>("foo"); + + Assert.Equal(30, fooA3); + Assert.Equal(30, fooB3); + } - var distributedCache = new MemoryDistributedCache(Options.Create(new MemoryDistributedCacheOptions())); + [Theory] + [ClassData(typeof(SerializerTypesClassData))] + public void BackgroundFactoryCompleteNotifyOtherNodes(SerializerType serializerType) + { + var backplaneConnectionId = Guid.NewGuid().ToString("N"); + + var duration1 = TimeSpan.FromSeconds(1); + var duration2 = TimeSpan.FromSeconds(10); + var factorySoftTimeout = TimeSpan.FromMilliseconds(50); + var simulatedFactoryDuration = TimeSpan.FromSeconds(3); + + var distributedCache = new MemoryDistributedCache(Options.Create(new MemoryDistributedCacheOptions())); + + var optionsA = CreateFusionCacheOptions(); + optionsA.SetInstanceId("A"); + //optionsA.DefaultEntryOptions.AllowBackgroundBackplaneOperations = false; + optionsA.DefaultEntryOptions.IsFailSafeEnabled = true; + optionsA.DefaultEntryOptions.FactorySoftTimeout = factorySoftTimeout; + using var cacheA = new FusionCache(optionsA, logger: CreateXUnitLogger<FusionCache>()); + cacheA.SetupDistributedCache(distributedCache, TestsUtils.GetSerializer(serializerType)); + cacheA.SetupBackplane(CreateBackplane(backplaneConnectionId)); + + var optionsB = CreateFusionCacheOptions(); + optionsB.SetInstanceId("B"); + //optionsB.DefaultEntryOptions.AllowBackgroundBackplaneOperations = false; + optionsB.DefaultEntryOptions.IsFailSafeEnabled = true; + optionsB.DefaultEntryOptions.FactorySoftTimeout = factorySoftTimeout; + using var cacheB = new FusionCache(optionsB, logger: CreateXUnitLogger<FusionCache>()); + cacheB.SetupDistributedCache(distributedCache, TestsUtils.GetSerializer(serializerType)); + cacheB.SetupBackplane(CreateBackplane(backplaneConnectionId)); + + // SET 10 ON CACHE-A AND DIST CACHE + var fooA1 = cacheA.GetOrSet("foo", _ => 10, duration1); + + // GET 10 FROM DIST CACHE AND SET ON CACHE-B + var fooB1 = cacheB.GetOrSet("foo", _ => 20, duration1); + + Assert.Equal(10, fooA1); + Assert.Equal(10, fooB1); + + // WAIT FOR THE CACHE ENTRIES TO EXPIRE + Thread.Sleep(duration1.PlusALittleBit()); + + // EXECUTE THE FACTORY ON CACHE-A, WHICH WILL TAKE 3 SECONDS, BUT + // THE FACTORY SOFT TIMEOUT IS 50 MILLISECONDS, SO IT WILL FAIL + // AND THE STALE VALUE WILL BE RETURNED + // THE FACTORY WILL BE KEPT RUNNING IN THE BACKGROUND, AND WHEN + // IT WILL COMPLETE SUCCESSFULLY UPDATE CACHE-A, THE DIST + // CACHE AND NOTIFY THE OTHER NODES + // SUCESSFULLY UPDATE CACHE-A, THE DIST CACHE AND NOTIFY THE OTHER NODES + var fooA2 = cacheA.GetOrSet( + "foo", + _ => + { + Thread.Sleep(simulatedFactoryDuration); + return 30; + }, + duration2 + ); + + // IMMEDIATELY GET OR SET FROM CACHE-B: THE VALUE THERE IS + // EXPIRED, SO THE NEW VALUE WILL BE SAVED AND RETURNED + var fooB2 = cacheB.GetOrSet( + "foo", + 40, + duration2 + ); + + Assert.Equal(10, fooA2); + Assert.Equal(40, fooB2); + + // WAIT FOR THE SIMULATED FACTORY TO COMPLETE: A NOTIFICATION + // WILL BE SENT TO THE OTHER NODES, WHICH IN TURN WILL UPDATE + // THEIR CACHE ENTRIES + Thread.Sleep(simulatedFactoryDuration.PlusALittleBit()); + + // GET THE UPDATED VALUES FROM CACHE-A AND CACHE-B + var fooA3 = cacheA.GetOrDefault<int>("foo"); + var fooB3 = cacheB.GetOrDefault<int>("foo"); + + Assert.Equal(30, fooA3); + Assert.Equal(30, fooB3); + } - using var cacheA = new FusionCache(CreateFusionCacheOptions(), logger: CreateLogger<FusionCache>()); - cacheA.SetupDistributedCache(distributedCache, TestsUtils.GetSerializer(serializerType)); - cacheA.SetupBackplane(CreateBackplane(backplaneConnectionId)); - cacheA.DefaultEntryOptions.IsFailSafeEnabled = true; - cacheA.DefaultEntryOptions.Duration = duration; - cacheA.DefaultEntryOptions.AllowBackgroundBackplaneOperations = false; + [Theory] + [ClassData(typeof(SerializerTypesClassData))] + public async Task CanExecuteBackgroundBackplaneOperationsAsync(SerializerType serializerType) + { + var simulatedDelayMs = TimeSpan.FromMilliseconds(2_000); + var backplaneConnectionId = Guid.NewGuid().ToString("N"); - using var cacheB = new FusionCache(CreateFusionCacheOptions(), logger: CreateLogger<FusionCache>()); - cacheB.SetupDistributedCache(distributedCache, TestsUtils.GetSerializer(serializerType)); - cacheB.SetupBackplane(CreateBackplane(backplaneConnectionId)); - cacheB.DefaultEntryOptions.IsFailSafeEnabled = true; - cacheB.DefaultEntryOptions.Duration = duration; - cacheB.DefaultEntryOptions.AllowBackgroundBackplaneOperations = false; + var eo = new FusionCacheEntryOptions().SetDurationSec(10); + eo.AllowBackgroundDistributedCacheOperations = false; + eo.AllowBackgroundBackplaneOperations = true; - using var cacheC = new FusionCache(CreateFusionCacheOptions(), logger: CreateLogger<FusionCache>()); - cacheC.SetupDistributedCache(distributedCache, TestsUtils.GetSerializer(serializerType)); - cacheC.SetupBackplane(CreateBackplane(backplaneConnectionId)); - cacheC.DefaultEntryOptions.IsFailSafeEnabled = true; - cacheC.DefaultEntryOptions.Duration = duration; - cacheC.DefaultEntryOptions.AllowBackgroundBackplaneOperations = false; + var logger = CreateXUnitLogger<FusionCache>(); + using var memoryCache = new MemoryCache(new MemoryCacheOptions()); - // SET ON CACHE A - await cacheA.SetAsync<int>("foo", 42); + using var fusionCache = new FusionCache(CreateFusionCacheOptions(), memoryCache, logger); - // GET ON CACHE A - var maybeFooA1 = await cacheA.TryGetAsync<int>("foo", opt => opt.SetFailSafe(true)); + var distributedCache = new MemoryDistributedCache(Options.Create(new MemoryDistributedCacheOptions())); + var chaosDistributedCache = new ChaosDistributedCache(distributedCache, CreateXUnitLogger<ChaosDistributedCache>()); + fusionCache.SetupDistributedCache(chaosDistributedCache, TestsUtils.GetSerializer(serializerType)); - // GET ON CACHE B (WILL GET FROM DISTRIBUTED CACHE AND SAVE ON LOCAL MEMORY CACHE) - var maybeFooB1 = await cacheB.TryGetAsync<int>("foo", opt => opt.SetFailSafe(true)); + var backplane = new MemoryBackplane(Options.Create(new MemoryBackplaneOptions() { ConnectionId = backplaneConnectionId })); + var chaosBackplane = new ChaosBackplane(backplane, CreateXUnitLogger<ChaosBackplane>()); + fusionCache.SetupBackplane(chaosBackplane); - // NOW CACHE A + B HAVE THE VALUE CACHED IN THEIR LOCAL MEMORY CACHE, WHILE CACHE C DOES NOT + chaosDistributedCache.SetAlwaysDelayExactly(simulatedDelayMs); + chaosBackplane.SetAlwaysDelayExactly(simulatedDelayMs); - // EXPIRE ON CACHE A, WHIS WILL: - // - EXPIRE ON CACHE A - // - REMOVE ON DISTRIBUTED CACHE - // - NOTIFY CACHE B AND CACHE C OF THE EXPIRATION AND THAT, IN TURN, WILL: - // - EXPIRE ON CACHE B - // - DO NOTHING ON CACHE C (IT WAS NOT IN ITS MEMORY CACHE) - await cacheA.ExpireAsync("foo"); + var sw = Stopwatch.StartNew(); + await fusionCache.SetAsync<int>("foo", 21, eo); + sw.Stop(); - await Task.Delay(TimeSpan.FromMilliseconds(100)); + await Task.Delay(TimeSpan.FromMilliseconds(1_000)); - // GET ON CACHE A: SINCE IT'S EXPIRED AND FAIL-SAFE IS DISABLED, NOTHING WILL BE RETURNED - var maybeFooA2 = await cacheA.TryGetAsync<int>("foo", opt => opt.SetFailSafe(false)); + Assert.True(sw.Elapsed >= simulatedDelayMs); + Assert.True(sw.Elapsed < simulatedDelayMs * 2); + } - // GET ON CACHE B: SINCE IT'S EXPIRED AND FAIL-SAFE IS DISABLED, NOTHING WILL BE RETURNED - var maybeFooB2 = await cacheB.TryGetAsync<int>("foo", opt => opt.SetFailSafe(false)); + [Theory] + [ClassData(typeof(SerializerTypesClassData))] + public void CanExecuteBackgroundBackplaneOperations(SerializerType serializerType) + { + var simulatedDelayMs = TimeSpan.FromMilliseconds(2_000); + var backplaneConnectionId = Guid.NewGuid().ToString("N"); - // GET ON CACHE C: SINCE NOTHING IS THERE, NOTHING WILL BE RETURNED - var maybeFooC2 = await cacheC.TryGetAsync<int>("foo", opt => opt.SetFailSafe(false)); + var eo = new FusionCacheEntryOptions().SetDurationSec(10); + eo.AllowBackgroundDistributedCacheOperations = false; + eo.AllowBackgroundBackplaneOperations = true; - _output.WriteLine($"BEFORE"); + var logger = CreateXUnitLogger<FusionCache>(); + using var memoryCache = new MemoryCache(new MemoryCacheOptions()); - // GET ON CACHE A: SINCE IT'S EXPIRED BUT FAIL-SAFE IS ENABLED, THE STALE VALUE WILL BE RETURNED - var maybeFooA3 = await cacheA.TryGetAsync<int>("foo", opt => opt.SetFailSafe(true)); + using var fusionCache = new FusionCache(CreateFusionCacheOptions(), memoryCache, logger); - _output.WriteLine($"AFTER"); + var distributedCache = new MemoryDistributedCache(Options.Create(new MemoryDistributedCacheOptions())); + var chaosDistributedCache = new ChaosDistributedCache(distributedCache, CreateXUnitLogger<ChaosDistributedCache>()); + fusionCache.SetupDistributedCache(chaosDistributedCache, TestsUtils.GetSerializer(serializerType)); - // GET ON CACHE B: SINCE IT'S EXPIRED BUT FAIL-SAFE IS ENABLED, THE STALE VALUE WILL BE RETURNED - var maybeFooB3 = await cacheB.TryGetAsync<int>("foo", opt => opt.SetFailSafe(true)); + var backplane = new MemoryBackplane(Options.Create(new MemoryBackplaneOptions() { ConnectionId = backplaneConnectionId })); + var chaosBackplane = new ChaosBackplane(backplane, CreateXUnitLogger<ChaosBackplane>()); + fusionCache.SetupBackplane(chaosBackplane); - // GET ON CACHE C: SINCE NOTHING IS THERE, NOTHING WILL BE RETURNED - var maybeFooC3 = await cacheC.TryGetAsync<int>("foo", opt => opt.SetFailSafe(true)); + chaosDistributedCache.SetAlwaysDelayExactly(simulatedDelayMs); + chaosBackplane.SetAlwaysDelayExactly(simulatedDelayMs); - Assert.True(maybeFooA1.HasValue); - Assert.Equal(42, maybeFooA1.Value); + var sw = Stopwatch.StartNew(); + fusionCache.Set<int>("foo", 21, eo); + sw.Stop(); - Assert.True(maybeFooB1.HasValue); - Assert.Equal(42, maybeFooB1.Value); + Thread.Sleep(TimeSpan.FromMilliseconds(1_000)); - Assert.False(maybeFooA2.HasValue); - Assert.False(maybeFooB2.HasValue); - Assert.False(maybeFooC2.HasValue); + Assert.True(sw.Elapsed >= simulatedDelayMs); + Assert.True(sw.Elapsed < simulatedDelayMs * 2); + } - Assert.True(maybeFooA3.HasValue); - Assert.Equal(42, maybeFooA3.Value); + [Fact] + public async Task ReThrowsBackplaneExceptionsAsync() + { + var backplane = new MemoryBackplane(Options.Create(new MemoryBackplaneOptions())); + var chaosBackplane = new ChaosBackplane(backplane); - Assert.True(maybeFooB3.HasValue); - Assert.Equal(42, maybeFooB3.Value); + chaosBackplane.SetAlwaysThrow(); + using var fusionCache = new FusionCache(CreateFusionCacheOptions()); + fusionCache.DefaultEntryOptions.AllowBackgroundDistributedCacheOperations = false; + fusionCache.DefaultEntryOptions.AllowBackgroundBackplaneOperations = false; + fusionCache.DefaultEntryOptions.ReThrowBackplaneExceptions = true; - Assert.False(maybeFooC3.HasValue); - } + fusionCache.SetupBackplane(chaosBackplane); - [Theory] - [ClassData(typeof(SerializerTypesClassData))] - public void CanHandleExpireOnMultiNodes(SerializerType serializerType) + await Assert.ThrowsAsync<FusionCacheBackplaneException>(async () => { - var backplaneConnectionId = Guid.NewGuid().ToString("N"); - - var duration = TimeSpan.FromMinutes(10); - - var distributedCache = new MemoryDistributedCache(Options.Create(new MemoryDistributedCacheOptions())); - - using var cacheA = new FusionCache(CreateFusionCacheOptions(), logger: CreateLogger<FusionCache>()); - cacheA.SetupDistributedCache(distributedCache, TestsUtils.GetSerializer(serializerType)); - cacheA.SetupBackplane(CreateBackplane(backplaneConnectionId)); - cacheA.DefaultEntryOptions.IsFailSafeEnabled = true; - cacheA.DefaultEntryOptions.Duration = duration; - cacheA.DefaultEntryOptions.AllowBackgroundBackplaneOperations = false; - - using var cacheB = new FusionCache(CreateFusionCacheOptions(), logger: CreateLogger<FusionCache>()); - cacheB.SetupDistributedCache(distributedCache, TestsUtils.GetSerializer(serializerType)); - cacheB.SetupBackplane(CreateBackplane(backplaneConnectionId)); - cacheB.DefaultEntryOptions.IsFailSafeEnabled = true; - cacheB.DefaultEntryOptions.Duration = duration; - cacheB.DefaultEntryOptions.AllowBackgroundBackplaneOperations = false; - - using var cacheC = new FusionCache(CreateFusionCacheOptions(), logger: CreateLogger<FusionCache>()); - cacheC.SetupDistributedCache(distributedCache, TestsUtils.GetSerializer(serializerType)); - cacheC.SetupBackplane(CreateBackplane(backplaneConnectionId)); - cacheC.DefaultEntryOptions.IsFailSafeEnabled = true; - cacheC.DefaultEntryOptions.Duration = duration; - cacheC.DefaultEntryOptions.AllowBackgroundBackplaneOperations = false; - - // SET ON CACHE A - cacheA.Set<int>("foo", 42); - - // GET ON CACHE A - var maybeFooA1 = cacheA.TryGet<int>("foo", opt => opt.SetFailSafe(true)); - - // GET ON CACHE B (WILL GET FROM DISTRIBUTED CACHE AND SAVE ON LOCAL MEMORY CACHE) - var maybeFooB1 = cacheB.TryGet<int>("foo", opt => opt.SetFailSafe(true)); - - // NOW CACHE A + B HAVE THE VALUE CACHED IN THEIR LOCAL MEMORY CACHE, WHILE CACHE C DOES NOT - - // EXPIRE ON CACHE A, WHIS WILL: - // - EXPIRE ON CACHE A - // - REMOVE ON DISTRIBUTED CACHE - // - NOTIFY CACHE B AND CACHE C OF THE EXPIRATION AND THAT, IN TURN, WILL: - // - EXPIRE ON CACHE B - // - DO NOTHING ON CACHE C (IT WAS NOT IN ITS MEMORY CACHE) - cacheA.Expire("foo"); - - Thread.Sleep(TimeSpan.FromMilliseconds(100)); - - // GET ON CACHE A: SINCE IT'S EXPIRED AND FAIL-SAFE IS DISABLED, NOTHING WILL BE RETURNED - var maybeFooA2 = cacheA.TryGet<int>("foo", opt => opt.SetFailSafe(false)); - - // GET ON CACHE B: SINCE IT'S EXPIRED AND FAIL-SAFE IS DISABLED, NOTHING WILL BE RETURNED - var maybeFooB2 = cacheB.TryGet<int>("foo", opt => opt.SetFailSafe(false)); - - // GET ON CACHE C: SINCE NOTHING IS THERE, NOTHING WILL BE RETURNED - var maybeFooC2 = cacheC.TryGet<int>("foo", opt => opt.SetFailSafe(false)); - - // GET ON CACHE A: SINCE IT'S EXPIRED BUT FAIL-SAFE IS ENABLED, THE STALE VALUE WILL BE RETURNED - var maybeFooA3 = cacheA.TryGet<int>("foo", opt => opt.SetFailSafe(true)); - - // GET ON CACHE B: SINCE IT'S EXPIRED BUT FAIL-SAFE IS ENABLED, THE STALE VALUE WILL BE RETURNED - var maybeFooB3 = cacheB.TryGet<int>("foo", opt => opt.SetFailSafe(true)); - - // GET ON CACHE C: SINCE NOTHING IS THERE, NOTHING WILL BE RETURNED - var maybeFooC3 = cacheC.TryGet<int>("foo", opt => opt.SetFailSafe(true)); - - Assert.True(maybeFooA1.HasValue); - Assert.Equal(42, maybeFooA1.Value); - - Assert.True(maybeFooB1.HasValue); - Assert.Equal(42, maybeFooB1.Value); - - Assert.False(maybeFooA2.HasValue); - Assert.False(maybeFooB2.HasValue); - Assert.False(maybeFooC2.HasValue); + await fusionCache.SetAsync<int>("foo", 42); + }); + } - Assert.True(maybeFooA3.HasValue); - Assert.Equal(42, maybeFooA3.Value); + [Fact] + public void ReThrowsBackplaneExceptions() + { + var backplane = new MemoryBackplane(Options.Create(new MemoryBackplaneOptions())); + var chaosBackplane = new ChaosBackplane(backplane); - Assert.True(maybeFooB3.HasValue); - Assert.Equal(42, maybeFooB3.Value); + chaosBackplane.SetAlwaysThrow(); + using var fusionCache = new FusionCache(CreateFusionCacheOptions()); + fusionCache.DefaultEntryOptions.AllowBackgroundDistributedCacheOperations = false; + fusionCache.DefaultEntryOptions.AllowBackgroundBackplaneOperations = false; + fusionCache.DefaultEntryOptions.ReThrowBackplaneExceptions = true; - Assert.False(maybeFooC3.HasValue); - } + fusionCache.SetupBackplane(chaosBackplane); - [Theory] - [ClassData(typeof(SerializerTypesClassData))] - public async Task BackgroundFactoryCompleteNotifyOtherNodesAsync(SerializerType serializerType) - { - var backplaneConnectionId = Guid.NewGuid().ToString("N"); - - var duration1 = TimeSpan.FromSeconds(1); - var duration2 = TimeSpan.FromSeconds(10); - var factorySoftTimeout = TimeSpan.FromMilliseconds(50); - var simulatedFactoryDuration = TimeSpan.FromSeconds(3); - - var distributedCache = new MemoryDistributedCache(Options.Create(new MemoryDistributedCacheOptions())); - - using var cacheA = new FusionCache(CreateFusionCacheOptions(), logger: CreateLogger<FusionCache>()); - cacheA.SetupDistributedCache(distributedCache, TestsUtils.GetSerializer(serializerType)); - cacheA.SetupBackplane(CreateBackplane(backplaneConnectionId)); - cacheA.DefaultEntryOptions.IsFailSafeEnabled = true; - cacheA.DefaultEntryOptions.FactorySoftTimeout = factorySoftTimeout; - - using var cacheB = new FusionCache(CreateFusionCacheOptions(), logger: CreateLogger<FusionCache>()); - cacheB.SetupDistributedCache(distributedCache, TestsUtils.GetSerializer(serializerType)); - cacheB.SetupBackplane(CreateBackplane(backplaneConnectionId)); - cacheB.DefaultEntryOptions.IsFailSafeEnabled = true; - cacheB.DefaultEntryOptions.FactorySoftTimeout = factorySoftTimeout; - - // SET 10 ON CACHE-A AND DIST CACHE - var fooA1 = await cacheA.GetOrSetAsync("foo", async _ => 10, duration1); - - // GET 10 FROM DIST CACHE AND SET ON CACHE-B - var fooB1 = await cacheB.GetOrSetAsync("foo", async _ => 20, duration1); - - Assert.Equal(10, fooA1); - Assert.Equal(10, fooB1); - - // WAIT FOR THE CACHE ENTRIES TO EXPIRE - await Task.Delay(duration1.PlusALittleBit()); - - // EXECUTE THE FACTORY ON CACHE-A, WHICH WILL TAKE 3 SECONDS, BUT - // THE FACTORY SOFT TIMEOUT IS 50 MILLISECONDS, SO IT WILL FAIL - // AND THE STALE VALUE WILL BE RETURNED - // THE FACTORY WILL BE KEPT RUNNING IN THE BACKGROUND, AND WHEN - // IT WILL COMPLETE SUCCESSFULLY UPDATE CACHE-A, THE DIST - // CACHE AND NOTIFY THE OTHER NODES - // SUCESSFULLY UPDATE CACHE-A, THE DIST CACHE AND NOTIFY THE OTHER NODES - var fooA2 = await cacheA.GetOrSetAsync( - "foo", - async _ => - { - await Task.Delay(simulatedFactoryDuration); - return 30; - }, - duration2 - ); - - // IMMEDIATELY GET OR SET FROM CACHE-B: THE VALUE THERE IS - // EXPIRED, SO THE NEW VALUE WILL BE SAVED AND RETURNED - var fooB2 = await cacheB.GetOrSetAsync( - "foo", - 40, - duration2 - ); - - Assert.Equal(10, fooA2); - Assert.Equal(40, fooB2); - - // WAIT FOR THE SIMULATED FACTORY TO COMPLETE: A NOTIFICATION - // WILL BE SENT TO THE OTHER NODES, WHICH IN TURN WILL UPDATE - // THEIR CACHE ENTRIES - await Task.Delay(simulatedFactoryDuration.PlusALittleBit()); - - // GET THE UPDATED VALUES FROM CACHE-A AND CACHE-B - var fooA3 = await cacheA.GetOrDefaultAsync<int>("foo"); - var fooB3 = await cacheB.GetOrDefaultAsync<int>("foo"); - - Assert.Equal(30, fooA3); - Assert.Equal(30, fooB3); - } - - [Theory] - [ClassData(typeof(SerializerTypesClassData))] - public void BackgroundFactoryCompleteNotifyOtherNodes(SerializerType serializerType) + Assert.Throws<FusionCacheBackplaneException>(() => { - var backplaneConnectionId = Guid.NewGuid().ToString("N"); - - var duration1 = TimeSpan.FromSeconds(1); - var duration2 = TimeSpan.FromSeconds(10); - var factorySoftTimeout = TimeSpan.FromMilliseconds(50); - var simulatedFactoryDuration = TimeSpan.FromSeconds(3); - - var distributedCache = new MemoryDistributedCache(Options.Create(new MemoryDistributedCacheOptions())); - - using var cacheA = new FusionCache(CreateFusionCacheOptions(), logger: CreateLogger<FusionCache>()); - cacheA.SetupDistributedCache(distributedCache, TestsUtils.GetSerializer(serializerType)); - cacheA.SetupBackplane(CreateBackplane(backplaneConnectionId)); - cacheA.DefaultEntryOptions.IsFailSafeEnabled = true; - cacheA.DefaultEntryOptions.FactorySoftTimeout = factorySoftTimeout; - - using var cacheB = new FusionCache(CreateFusionCacheOptions(), logger: CreateLogger<FusionCache>()); - cacheB.SetupDistributedCache(distributedCache, TestsUtils.GetSerializer(serializerType)); - cacheB.SetupBackplane(CreateBackplane(backplaneConnectionId)); - cacheB.DefaultEntryOptions.IsFailSafeEnabled = true; - cacheB.DefaultEntryOptions.FactorySoftTimeout = factorySoftTimeout; - - // SET 10 ON CACHE-A AND DIST CACHE - var fooA1 = cacheA.GetOrSet("foo", _ => 10, duration1); - - // GET 10 FROM DIST CACHE AND SET ON CACHE-B - var fooB1 = cacheB.GetOrSet("foo", _ => 20, duration1); - - Assert.Equal(10, fooA1); - Assert.Equal(10, fooB1); - - // WAIT FOR THE CACHE ENTRIES TO EXPIRE - Thread.Sleep(duration1.PlusALittleBit()); - - // EXECUTE THE FACTORY ON CACHE-A, WHICH WILL TAKE 3 SECONDS, BUT - // THE FACTORY SOFT TIMEOUT IS 50 MILLISECONDS, SO IT WILL FAIL - // AND THE STALE VALUE WILL BE RETURNED - // THE FACTORY WILL BE KEPT RUNNING IN THE BACKGROUND, AND WHEN - // IT WILL COMPLETE SUCCESSFULLY UPDATE CACHE-A, THE DIST - // CACHE AND NOTIFY THE OTHER NODES - // SUCESSFULLY UPDATE CACHE-A, THE DIST CACHE AND NOTIFY THE OTHER NODES - var fooA2 = cacheA.GetOrSet( - "foo", - _ => - { - Thread.Sleep(simulatedFactoryDuration); - return 30; - }, - duration2 - ); - - // IMMEDIATELY GET OR SET FROM CACHE-B: THE VALUE THERE IS - // EXPIRED, SO THE NEW VALUE WILL BE SAVED AND RETURNED - var fooB2 = cacheB.GetOrSet( - "foo", - 40, - duration2 - ); - - Assert.Equal(10, fooA2); - Assert.Equal(40, fooB2); - - // WAIT FOR THE SIMULATED FACTORY TO COMPLETE: A NOTIFICATION - // WILL BE SENT TO THE OTHER NODES, WHICH IN TURN WILL UPDATE - // THEIR CACHE ENTRIES - Thread.Sleep(simulatedFactoryDuration.PlusALittleBit()); - - // GET THE UPDATED VALUES FROM CACHE-A AND CACHE-B - var fooA3 = cacheA.GetOrDefault<int>("foo"); - var fooB3 = cacheB.GetOrDefault<int>("foo"); - - Assert.Equal(30, fooA3); - Assert.Equal(30, fooB3); - } + fusionCache.Set<int>("foo", 42); + }); } } diff --git a/tests/ZiggyCreatures.FusionCache.Tests/CacheStampedeTests.cs b/tests/ZiggyCreatures.FusionCache.Tests/CacheStampedeTests.cs index 85e106a3..4911a7b5 100644 --- a/tests/ZiggyCreatures.FusionCache.Tests/CacheStampedeTests.cs +++ b/tests/ZiggyCreatures.FusionCache.Tests/CacheStampedeTests.cs @@ -5,118 +5,117 @@ using Xunit; using ZiggyCreatures.Caching.Fusion; -namespace FusionCacheTests +namespace FusionCacheTests; + +public class CacheStampedeTests { - public class CacheStampedeTests - { - private static readonly TimeSpan FactoryDuration = TimeSpan.FromMilliseconds(500); + private static readonly TimeSpan FactoryDuration = TimeSpan.FromMilliseconds(500); - [Theory] - [InlineData(10)] - [InlineData(100)] - [InlineData(1_000)] - public async Task OnlyOneFactoryGetsCalledEvenInHighConcurrencyAsync(int accessorsCount) + [Theory] + [InlineData(10)] + [InlineData(100)] + [InlineData(1_000)] + public async Task OnlyOneFactoryGetsCalledEvenInHighConcurrencyAsync(int accessorsCount) + { + using (var cache = new FusionCache(new FusionCacheOptions())) { - using (var cache = new FusionCache(new FusionCacheOptions())) - { - var factoryCallsCount = 0; + var factoryCallsCount = 0; - var tasks = new ConcurrentBag<Task>(); - Parallel.For(0, accessorsCount, _ => - { - var task = cache.GetOrSetAsync<int>( - "foo", - async _ => - { - Interlocked.Increment(ref factoryCallsCount); - await Task.Delay(FactoryDuration).ConfigureAwait(false); - return 42; - }, - new FusionCacheEntryOptions(TimeSpan.FromSeconds(10)) - ); - tasks.Add(task.AsTask()); - }); + var tasks = new ConcurrentBag<Task>(); + Parallel.For(0, accessorsCount, _ => + { + var task = cache.GetOrSetAsync<int>( + "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] - [InlineData(10)] - [InlineData(100)] - [InlineData(1_000)] - public void OnlyOneFactoryGetsCalledEvenInHighConcurrency(int accessorsCount) + [Theory] + [InlineData(10)] + [InlineData(100)] + [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; + var factoryCallsCount = 0; - Parallel.For(0, accessorsCount, _ => - { - cache.GetOrSet<int>( - "foo", - _ => - { - Interlocked.Increment(ref factoryCallsCount); - Thread.Sleep(FactoryDuration); - return 42; - }, - new FusionCacheEntryOptions(TimeSpan.FromSeconds(10)) - ); - }); + Parallel.For(0, accessorsCount, _ => + { + cache.GetOrSet<int>( + "foo", + _ => + { + Interlocked.Increment(ref factoryCallsCount); + Thread.Sleep(FactoryDuration); + return 42; + }, + new FusionCacheEntryOptions(TimeSpan.FromSeconds(10)) + ); + }); - Assert.Equal(1, factoryCallsCount); - } + Assert.Equal(1, factoryCallsCount); } + } - [Theory] - [InlineData(10)] - [InlineData(100)] - [InlineData(1_000)] - public async Task OnlyOneFactoryGetsCalledEvenInMixedHighConcurrencyAsync(int accessorsCount) + [Theory] + [InlineData(10)] + [InlineData(100)] + [InlineData(1_000)] + public async Task OnlyOneFactoryGetsCalledEvenInMixedHighConcurrencyAsync(int accessorsCount) + { + using (var cache = new FusionCache(new FusionCacheOptions())) { - using (var cache = new FusionCache(new FusionCacheOptions())) - { - var factoryCallsCount = 0; + var factoryCallsCount = 0; - var tasks = new ConcurrentBag<Task>(); - Parallel.For(0, accessorsCount, idx => + var tasks = new ConcurrentBag<Task>(); + Parallel.For(0, accessorsCount, idx => + { + if (idx % 2 == 0) { - if (idx % 2 == 0) - { - var task = cache.GetOrSetAsync<int>( - "foo", - async _ => - { - Interlocked.Increment(ref factoryCallsCount); - await Task.Delay(FactoryDuration).ConfigureAwait(false); - return 42; - }, - new FusionCacheEntryOptions(TimeSpan.FromSeconds(10)) - ); - tasks.Add(task.AsTask()); - } - else - { - cache.GetOrSet<int>( - "foo", - _ => - { - Interlocked.Increment(ref factoryCallsCount); - Thread.Sleep(FactoryDuration); - return 42; - }, - new FusionCacheEntryOptions(TimeSpan.FromSeconds(10)) - ); - } - }); + var task = cache.GetOrSetAsync<int>( + "foo", + async _ => + { + Interlocked.Increment(ref factoryCallsCount); + await Task.Delay(FactoryDuration); + return 42; + }, + new FusionCacheEntryOptions(TimeSpan.FromSeconds(10)) + ); + tasks.Add(task.AsTask()); + } + else + { + cache.GetOrSet<int>( + "foo", + _ => + { + Interlocked.Increment(ref factoryCallsCount); + Thread.Sleep(FactoryDuration); + return 42; + }, + new FusionCacheEntryOptions(TimeSpan.FromSeconds(10)) + ); + } + }); - await Task.WhenAll(tasks); + await Task.WhenAll(tasks); - Assert.Equal(1, factoryCallsCount); - } + Assert.Equal(1, factoryCallsCount); } } } diff --git a/tests/ZiggyCreatures.FusionCache.Tests/CacheStampedeTests_CacheManager.cs b/tests/ZiggyCreatures.FusionCache.Tests/CacheStampedeTests_CacheManager.cs deleted file mode 100644 index 886bbf27..00000000 --- a/tests/ZiggyCreatures.FusionCache.Tests/CacheStampedeTests_CacheManager.cs +++ /dev/null @@ -1,46 +0,0 @@ -using System; -using System.Threading; -using System.Threading.Tasks; -using CacheManager.Core; -using Xunit; - -namespace FusionCacheTests -{ - // REMOVE THE abstract MODIFIER TO RUN THESE TESTS - public abstract class CacheStampedeTests_CacheManager - { - private static readonly TimeSpan FactoryDuration = TimeSpan.FromMilliseconds(500); - - [Theory] - [InlineData(10)] - [InlineData(100)] - [InlineData(1_000)] - public void OnlyOneFactoryGetsCalledEvenInHighConcurrency(int accessorsCount) - { - using (var cache = CacheFactory.Build<int>(p => p.WithMicrosoftMemoryCacheHandle())) - { - var factoryCallsCount = 0; - - Parallel.For(0, accessorsCount, _ => - { - cache.GetOrAdd( - "foo", - key => - { - Interlocked.Increment(ref factoryCallsCount); - Thread.Sleep(FactoryDuration); - return new CacheItem<int>( - key, - 42, - ExpirationMode.Absolute, - TimeSpan.FromSeconds(10) - ); - } - ); - }); - - Assert.Equal(1, factoryCallsCount); - } - } - } -} diff --git a/tests/ZiggyCreatures.FusionCache.Tests/CacheStampedeTests_CacheTower.cs b/tests/ZiggyCreatures.FusionCache.Tests/CacheStampedeTests_CacheTower.cs deleted file mode 100644 index ff1bfa06..00000000 --- a/tests/ZiggyCreatures.FusionCache.Tests/CacheStampedeTests_CacheTower.cs +++ /dev/null @@ -1,51 +0,0 @@ -using System; -using System.Collections.Concurrent; -using System.Threading; -using System.Threading.Tasks; -using CacheTower; -using CacheTower.Extensions; -using CacheTower.Providers.Memory; -using Xunit; - -namespace FusionCacheTests -{ - // REMOVE THE abstract MODIFIER TO RUN THESE TESTS - public abstract class CacheStampedeTests_CacheTower - { - private static readonly TimeSpan FactoryDuration = TimeSpan.FromMilliseconds(500); - - [Theory] - [InlineData(10)] - [InlineData(100)] - [InlineData(1_000)] - public async Task OnlyOneFactoryGetsCalledEvenInHighConcurrencyAsync(int accessorsCount) - { - await using (var cache = new CacheStack(new[] { new MemoryCacheLayer() }, new[] { new AutoCleanupExtension(TimeSpan.FromMinutes(5)) })) - { - var cacheSettings = new CacheSettings(TimeSpan.FromSeconds(10)); - - var factoryCallsCount = 0; - - var tasks = new ConcurrentBag<Task>(); - Parallel.For(0, accessorsCount, _ => - { - var task = cache.GetOrSetAsync<int>( - "foo", - async old => - { - Interlocked.Increment(ref factoryCallsCount); - await Task.Delay(FactoryDuration).ConfigureAwait(false); - return 42; - }, - cacheSettings - ); - tasks.Add(task.AsTask()); - }); - - await Task.WhenAll(tasks); - - Assert.Equal(1, factoryCallsCount); - } - } - } -} diff --git a/tests/ZiggyCreatures.FusionCache.Tests/CacheStampedeTests_EasyCaching.cs b/tests/ZiggyCreatures.FusionCache.Tests/CacheStampedeTests_EasyCaching.cs deleted file mode 100644 index acd5bc46..00000000 --- a/tests/ZiggyCreatures.FusionCache.Tests/CacheStampedeTests_EasyCaching.cs +++ /dev/null @@ -1,133 +0,0 @@ -using System; -using System.Collections.Concurrent; -using System.Threading; -using System.Threading.Tasks; -using EasyCaching.Core; -using Microsoft.Extensions.DependencyInjection; -using Xunit; - -namespace FusionCacheTests -{ - // REMOVE THE abstract MODIFIER TO RUN THESE TESTS - public abstract class CacheStampedeTests_EasyCaching - { - private static readonly TimeSpan FactoryDuration = TimeSpan.FromMilliseconds(500); - - [Theory] - [InlineData(10)] - [InlineData(100)] - [InlineData(1_000)] - public async Task OnlyOneFactoryGetsCalledEvenInHighConcurrencyAsync(int accessorsCount) - { - var services = new ServiceCollection(); - services.AddEasyCaching(options => { options.UseInMemory("default"); }); - var serviceProvider = services.BuildServiceProvider(); - var factory = serviceProvider.GetRequiredService<IEasyCachingProviderFactory>(); - var cache = factory.GetCachingProvider("default"); - - var factoryCallsCount = 0; - - var tasks = new ConcurrentBag<Task>(); - Parallel.For(0, accessorsCount, _ => - { - var task = cache.GetAsync<int>( - "foo", - async () => - { - Interlocked.Increment(ref factoryCallsCount); - await Task.Delay(FactoryDuration).ConfigureAwait(false); - return 42; - }, - TimeSpan.FromSeconds(10) - ); - tasks.Add(task); - }); - - await Task.WhenAll(tasks); - - Assert.Equal(1, factoryCallsCount); - } - - [Theory] - [InlineData(10)] - [InlineData(100)] - [InlineData(1_000)] - public void OnlyOneFactoryGetsCalledEvenInHighConcurrency(int accessorsCount) - { - var services = new ServiceCollection(); - services.AddEasyCaching(options => { options.UseInMemory("default"); }); - var serviceProvider = services.BuildServiceProvider(); - var factory = serviceProvider.GetRequiredService<IEasyCachingProviderFactory>(); - var cache = factory.GetCachingProvider("default"); - - var factoryCallsCount = 0; - - Parallel.For(0, accessorsCount, _ => - { - cache.Get<int>( - "foo", - () => - { - Interlocked.Increment(ref factoryCallsCount); - Thread.Sleep(FactoryDuration); - return 42; - }, - TimeSpan.FromSeconds(10) - ); - }); - - Assert.Equal(1, factoryCallsCount); - } - - [Theory] - [InlineData(10)] - [InlineData(100)] - [InlineData(1_000)] - public async Task OnlyOneFactoryGetsCalledEvenInMixedHighConcurrencyAsync(int accessorsCount) - { - var services = new ServiceCollection(); - services.AddEasyCaching(options => { options.UseInMemory("default"); }); - var serviceProvider = services.BuildServiceProvider(); - var factory = serviceProvider.GetRequiredService<IEasyCachingProviderFactory>(); - var cache = factory.GetCachingProvider("default"); - - var factoryCallsCount = 0; - - var tasks = new ConcurrentBag<Task>(); - Parallel.For(0, accessorsCount, idx => - { - if (idx % 2 == 0) - { - var task = cache.GetAsync<int>( - "foo", - async () => - { - Interlocked.Increment(ref factoryCallsCount); - await Task.Delay(FactoryDuration).ConfigureAwait(false); - return 42; - }, - TimeSpan.FromSeconds(10) - ); - tasks.Add(task); - } - else - { - cache.Get<int>( - "foo", - () => - { - Interlocked.Increment(ref factoryCallsCount); - Thread.Sleep(FactoryDuration); - return 42; - }, - TimeSpan.FromSeconds(10) - ); - } - }); - - await Task.WhenAll(tasks); - - Assert.Equal(1, factoryCallsCount); - } - } -} diff --git a/tests/ZiggyCreatures.FusionCache.Tests/CacheStampedeTests_LazyCache.cs b/tests/ZiggyCreatures.FusionCache.Tests/CacheStampedeTests_LazyCache.cs deleted file mode 100644 index c51fe2d9..00000000 --- a/tests/ZiggyCreatures.FusionCache.Tests/CacheStampedeTests_LazyCache.cs +++ /dev/null @@ -1,130 +0,0 @@ -using System; -using System.Collections.Concurrent; -using System.Threading; -using System.Threading.Tasks; -using LazyCache; -using LazyCache.Providers; -using Microsoft.Extensions.Caching.Memory; -using Xunit; - -namespace FusionCacheTests -{ - // REMOVE THE abstract MODIFIER TO RUN THESE TESTS - public abstract class CacheStampedeTests_LazyCache - { - private static readonly TimeSpan FactoryDuration = TimeSpan.FromMilliseconds(500); - - [Theory] - [InlineData(10)] - [InlineData(100)] - [InlineData(1_000)] - public async Task OnlyOneFactoryGetsCalledEvenInHighConcurrencyAsync(int accessorsCount) - { - using (var memoryCache = new MemoryCache(new MemoryCacheOptions())) - { - var cache = new CachingService(new MemoryCacheProvider(memoryCache)); - cache.DefaultCachePolicy = new CacheDefaults { DefaultCacheDurationSeconds = 10 }; - - var factoryCallsCount = 0; - - var tasks = new ConcurrentBag<Task>(); - Parallel.For(0, accessorsCount, _ => - { - var task = cache.GetOrAddAsync<int>( - "foo", - async _ => - { - Interlocked.Increment(ref factoryCallsCount); - await Task.Delay(FactoryDuration).ConfigureAwait(false); - return 42; - } - ); - tasks.Add(task); - }); - - await Task.WhenAll(tasks); - - Assert.Equal(1, factoryCallsCount); - } - } - - [Theory] - [InlineData(10)] - [InlineData(100)] - [InlineData(1_000)] - public void OnlyOneFactoryGetsCalledEvenInHighConcurrency(int accessorsCount) - { - using (var memoryCache = new MemoryCache(new MemoryCacheOptions())) - { - var cache = new CachingService(new MemoryCacheProvider(memoryCache)); - cache.DefaultCachePolicy = new CacheDefaults { DefaultCacheDurationSeconds = 10 }; - - var factoryCallsCount = 0; - - Parallel.For(0, accessorsCount, _ => - { - cache.GetOrAdd<int>( - "foo", - _ => - { - Interlocked.Increment(ref factoryCallsCount); - Thread.Sleep(FactoryDuration); - return 42; - } - ); - }); - - Assert.Equal(1, factoryCallsCount); - } - } - - [Theory] - [InlineData(10)] - [InlineData(100)] - [InlineData(1_000)] - public async Task OnlyOneFactoryGetsCalledEvenInMixedHighConcurrencyAsync(int accessorsCount) - { - using (var memoryCache = new MemoryCache(new MemoryCacheOptions())) - { - var cache = new CachingService(new MemoryCacheProvider(memoryCache)); - cache.DefaultCachePolicy = new CacheDefaults { DefaultCacheDurationSeconds = 10 }; - - var factoryCallsCount = 0; - - var tasks = new ConcurrentBag<Task>(); - Parallel.For(0, accessorsCount, idx => - { - if (idx % 2 == 0) - { - var task = cache.GetOrAddAsync<int>( - "foo", - async _ => - { - Interlocked.Increment(ref factoryCallsCount); - await Task.Delay(FactoryDuration).ConfigureAwait(false); - return 42; - } - ); - tasks.Add(task); - } - else - { - cache.GetOrAdd<int>( - "foo", - _ => - { - Interlocked.Increment(ref factoryCallsCount); - Thread.Sleep(FactoryDuration); - return 42; - } - ); - } - }); - - 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 7e007477..c1d89c43 100644 --- a/tests/ZiggyCreatures.FusionCache.Tests/DependencyInjectionTests.cs +++ b/tests/ZiggyCreatures.FusionCache.Tests/DependencyInjectionTests.cs @@ -10,6 +10,7 @@ using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; using Xunit; +using Xunit.Abstractions; using ZiggyCreatures.Caching.Fusion; using ZiggyCreatures.Caching.Fusion.Backplane; using ZiggyCreatures.Caching.Fusion.Backplane.Memory; @@ -19,938 +20,818 @@ using ZiggyCreatures.Caching.Fusion.Plugins; using ZiggyCreatures.Caching.Fusion.Serialization.SystemTextJson; -namespace FusionCacheTests +namespace FusionCacheTests; + +public class DependencyInjectionTests + : AbstractTests { - public class DependencyInjectionTests + public DependencyInjectionTests(ITestOutputHelper output) + : base(output, null) { - static ILogger? GetLogger(IFusionCache cache) - { - return typeof(FusionCache).GetField("_logger", BindingFlags.NonPublic | BindingFlags.Instance)!.GetValue(cache) as ILogger; - } + } - static IDistributedCache? GetDistributedCache<TDistributedCache>(IFusionCache cache) - where TDistributedCache : class, IDistributedCache - { - var dca = typeof(FusionCache).GetField("_dca", BindingFlags.NonPublic | BindingFlags.Instance)!.GetValue(cache) as DistributedCacheAccessor; - if (dca is null) - return null; + static ILogger? GetLogger(IFusionCache cache) + { + return typeof(FusionCache).GetField("_logger", BindingFlags.NonPublic | BindingFlags.Instance)!.GetValue(cache) as ILogger; + } - return typeof(DistributedCacheAccessor).GetField("_cache", BindingFlags.NonPublic | BindingFlags.Instance)!.GetValue(dca) as TDistributedCache; - } + static IDistributedCache? GetDistributedCache<TDistributedCache>(IFusionCache cache) + where TDistributedCache : class, IDistributedCache + { + var dca = typeof(FusionCache).GetField("_dca", BindingFlags.NonPublic | BindingFlags.Instance)!.GetValue(cache) as DistributedCacheAccessor; + if (dca is null) + return null; - static TBackplane? GetBackplane<TBackplane>(IFusionCache cache) - where TBackplane : class, IFusionCacheBackplane - { - var bpa = typeof(FusionCache).GetField("_bpa", BindingFlags.NonPublic | BindingFlags.Instance)!.GetValue(cache) as BackplaneAccessor; - if (bpa is null) - return null; + return typeof(DistributedCacheAccessor).GetField("_cache", BindingFlags.NonPublic | BindingFlags.Instance)!.GetValue(dca) as TDistributedCache; + } - return typeof(BackplaneAccessor).GetField("_backplane", BindingFlags.NonPublic | BindingFlags.Instance)!.GetValue(bpa) as TBackplane; - } + static TBackplane? GetBackplane<TBackplane>(IFusionCache cache) + where TBackplane : class, IFusionCacheBackplane + { + var bpa = typeof(FusionCache).GetField("_bpa", BindingFlags.NonPublic | BindingFlags.Instance)!.GetValue(cache) as BackplaneAccessor; + if (bpa is null) + return null; - static RedisBackplaneOptions? GetRedisBackplaneOptions(IFusionCache cache) - { - var backplane = GetBackplane<RedisBackplane>(cache); - if (backplane is null) - return null; + return typeof(BackplaneAccessor).GetField("_backplane", BindingFlags.NonPublic | BindingFlags.Instance)!.GetValue(bpa) as TBackplane; + } - return typeof(RedisBackplane).GetField("_options", BindingFlags.NonPublic | BindingFlags.Instance)!.GetValue(backplane) as RedisBackplaneOptions; ; - } + static RedisBackplaneOptions? GetRedisBackplaneOptions(IFusionCache cache) + { + var backplane = GetBackplane<RedisBackplane>(cache); + if (backplane is null) + return null; - [Fact] - public void CanUseDependencyInjection() - { - var services = new ServiceCollection(); + return typeof(RedisBackplane).GetField("_options", BindingFlags.NonPublic | BindingFlags.Instance)!.GetValue(backplane) as RedisBackplaneOptions; ; + } - services.AddFusionCache(); + [Fact] + public void CanUseDependencyInjection() + { + var services = new ServiceCollection(); - using var serviceProvider = services.BuildServiceProvider(); + services.AddFusionCache(); - var cache = serviceProvider.GetRequiredService<IFusionCache>(); + using var serviceProvider = services.BuildServiceProvider(); - Assert.NotNull(cache); - Assert.Equal(FusionCacheOptions.DefaultCacheName, cache.CacheName); - } + var cache = serviceProvider.GetRequiredService<IFusionCache>(); - [Fact] - public void EmptyBuilderDoesNotUseExtraComponents() - { - var services = new ServiceCollection(); + Assert.NotNull(cache); + Assert.Equal(FusionCacheOptions.DefaultCacheName, cache.CacheName); + } + + [Fact] + public void EmptyBuilderDoesNotUseExtraComponents() + { + var services = new ServiceCollection(); - services.AddDistributedMemoryCache(); - services.AddFusionCacheSystemTextJsonSerializer(); - services.AddFusionCacheMemoryBackplane(); + services.AddDistributedMemoryCache(); + services.AddFusionCacheSystemTextJsonSerializer(); + services.AddFusionCacheMemoryBackplane(); - services.AddFusionCache("Foo"); - services.AddFusionCache(); + services.AddFusionCache("Foo"); + services.AddFusionCache(); - using var serviceProvider = services.BuildServiceProvider(); + using var serviceProvider = services.BuildServiceProvider(); - var cacheProvider = serviceProvider.GetRequiredService<IFusionCacheProvider>(); + var cacheProvider = serviceProvider.GetRequiredService<IFusionCacheProvider>(); - var fooCache = cacheProvider.GetCache("Foo"); - var defaultCache = serviceProvider.GetRequiredService<IFusionCache>(); + var fooCache = cacheProvider.GetCache("Foo"); + var defaultCache = serviceProvider.GetRequiredService<IFusionCache>(); - Assert.NotNull(fooCache); - Assert.Equal("Foo", fooCache.CacheName); - Assert.False(fooCache.HasDistributedCache); - Assert.False(fooCache.HasBackplane); + Assert.NotNull(fooCache); + Assert.Equal("Foo", fooCache.CacheName); + Assert.False(fooCache.HasDistributedCache); + Assert.False(fooCache.HasBackplane); - Assert.NotNull(defaultCache); - Assert.Equal(FusionCacheOptions.DefaultCacheName, defaultCache.CacheName); - Assert.False(defaultCache.HasDistributedCache); - Assert.False(defaultCache.HasBackplane); - } + Assert.NotNull(defaultCache); + Assert.Equal(FusionCacheOptions.DefaultCacheName, defaultCache.CacheName); + Assert.False(defaultCache.HasDistributedCache); + Assert.False(defaultCache.HasBackplane); + } + + [Fact] + public void CanConfigureVariousOptions() + { + var services = new ServiceCollection(); - [Fact] - public void CanConfigureVariousOptions() + var options = new FusionCacheOptions { - var services = new ServiceCollection(); + AutoRecoveryMaxItems = 123, + }; - var options = new FusionCacheOptions + services.AddFusionCache() + .WithOptions(options) + .WithOptions(opt => { - BackplaneAutoRecoveryMaxItems = 123, - }; - - services.AddFusionCache() - .WithOptions(options) - .WithOptions(opt => - { - opt.DefaultEntryOptions.DistributedCacheDuration = TimeSpan.FromSeconds(123); - }) - .WithDefaultEntryOptions(opt => - { - opt.Duration = TimeSpan.FromMinutes(123); - }) - ; - - using var serviceProvider = services.BuildServiceProvider(); - - var cache = serviceProvider.GetRequiredService<IFusionCache>(); + opt.DefaultEntryOptions.DistributedCacheDuration = TimeSpan.FromSeconds(123); + }) + .WithDefaultEntryOptions(opt => + { + opt.Duration = TimeSpan.FromMinutes(123); + }) + ; - Assert.NotNull(cache); - Assert.Equal(FusionCacheOptions.DefaultCacheName, cache.CacheName); - Assert.Equal(123, options.BackplaneAutoRecoveryMaxItems); - Assert.Equal(TimeSpan.FromSeconds(123), cache.DefaultEntryOptions.DistributedCacheDuration!.Value); - Assert.Equal(TimeSpan.FromMinutes(123), cache.DefaultEntryOptions.Duration); - } + using var serviceProvider = services.BuildServiceProvider(); - [Fact] - public void CanAddPlugins() - { - var services = new ServiceCollection(); - services.AddTransient<IFusionCachePlugin>(sp => new SimplePlugin("P_1")); + var cache = serviceProvider.GetRequiredService<IFusionCache>(); - services.AddFusionCache() - .WithAllRegisteredPlugins() - .WithPlugin(new SimplePlugin("P_2")) - .WithPlugin(sp => new SimplePlugin("P_3")) - ; + Assert.NotNull(cache); + Assert.Equal(FusionCacheOptions.DefaultCacheName, cache.CacheName); + Assert.Equal(123, options.AutoRecoveryMaxItems); + Assert.Equal(TimeSpan.FromSeconds(123), cache.DefaultEntryOptions.DistributedCacheDuration!.Value); + Assert.Equal(TimeSpan.FromMinutes(123), cache.DefaultEntryOptions.Duration); + } - using var serviceProvider = services.BuildServiceProvider(); + [Fact] + public void CanAddPlugins() + { + var services = new ServiceCollection(); + services.AddTransient<IFusionCachePlugin>(sp => new SimplePlugin("P_1")); - var cache = serviceProvider.GetRequiredService<IFusionCache>(); + services.AddFusionCache() + .WithAllRegisteredPlugins() + .WithPlugin(new SimplePlugin("P_2")) + .WithPlugin(sp => new SimplePlugin("P_3")) + ; - static List<TPlugin> GetAllPlugins<TPlugin>(IFusionCache cache) - where TPlugin : IFusionCachePlugin - { - return (typeof(FusionCache).GetField("_plugins", BindingFlags.NonPublic | BindingFlags.Instance)!.GetValue(cache) as List<IFusionCachePlugin>)!.Cast<TPlugin>().ToList(); - } + using var serviceProvider = services.BuildServiceProvider(); - var allPlugins = GetAllPlugins<SimplePlugin>(cache); + var cache = serviceProvider.GetRequiredService<IFusionCache>(); - Assert.NotNull(cache); - Assert.NotNull(allPlugins); - Assert.Equal(3, allPlugins.Count); - Assert.NotNull(allPlugins.Single(p => p.Name == "P_1")); - Assert.NotNull(allPlugins.Single(p => p.Name == "P_2")); - Assert.NotNull(allPlugins.Single(p => p.Name == "P_3")); + static List<TPlugin> GetAllPlugins<TPlugin>(IFusionCache cache) + where TPlugin : IFusionCachePlugin + { + return (typeof(FusionCache).GetField("_plugins", BindingFlags.NonPublic | BindingFlags.Instance)!.GetValue(cache) as List<IFusionCachePlugin>)!.Cast<TPlugin>().ToList(); } - [Fact] - public void TryAutoSetupWorks() - { - var services = new ServiceCollection(); + var allPlugins = GetAllPlugins<SimplePlugin>(cache); - services.AddDistributedMemoryCache(); - services.AddFusionCacheSystemTextJsonSerializer(); - services.AddFusionCacheMemoryBackplane(); + Assert.NotNull(cache); + Assert.NotNull(allPlugins); + Assert.Equal(3, allPlugins.Count); + Assert.NotNull(allPlugins.Single(p => p.Name == "P_1")); + Assert.NotNull(allPlugins.Single(p => p.Name == "P_2")); + Assert.NotNull(allPlugins.Single(p => p.Name == "P_3")); + } - services.AddFusionCache() - .TryWithAutoSetup(false) - ; + [Fact] + public void TryAutoSetupWorks() + { + var services = new ServiceCollection(); - using var serviceProvider = services.BuildServiceProvider(); + services.AddDistributedMemoryCache(); + services.AddFusionCacheSystemTextJsonSerializer(); + services.AddFusionCacheMemoryBackplane(); - var cache = serviceProvider.GetRequiredService<IFusionCache>(); + services.AddFusionCache() + .TryWithAutoSetup(false) + ; - Assert.NotNull(cache); - Assert.Equal(FusionCacheOptions.DefaultCacheName, cache.CacheName); - Assert.True(cache.HasDistributedCache); - Assert.True(cache.HasBackplane); - } + using var serviceProvider = services.BuildServiceProvider(); - [Fact] - public void ThrowsIfMissingRegisteredLogger() - { - var services = new ServiceCollection(); + var cache = serviceProvider.GetRequiredService<IFusionCache>(); - services.AddFusionCache() - .WithRegisteredLogger() - ; + Assert.NotNull(cache); + Assert.Equal(FusionCacheOptions.DefaultCacheName, cache.CacheName); + Assert.True(cache.HasDistributedCache); + Assert.True(cache.HasBackplane); + } - using var serviceProvider = services.BuildServiceProvider(); + [Fact] + public void ThrowsIfMissingRegisteredLogger() + { + var services = new ServiceCollection(); - Assert.Throws<InvalidOperationException>(() => - { - _ = serviceProvider.GetService<IFusionCache>(); - }); - } + services.AddFusionCache() + .WithRegisteredLogger() + ; - [Fact] - public void DontThrowIfMissingRegisteredLogger() + using var serviceProvider = services.BuildServiceProvider(); + + Assert.Throws<InvalidOperationException>(() => { - var services = new ServiceCollection(); + _ = serviceProvider.GetService<IFusionCache>(); + }); + } - services.AddFusionCache() - .TryWithRegisteredLogger() - ; + [Fact] + public void DontThrowIfMissingRegisteredLogger() + { + var services = new ServiceCollection(); - using var serviceProvider = services.BuildServiceProvider(); + services.AddFusionCache() + .TryWithRegisteredLogger() + ; - var cache = serviceProvider.GetService<IFusionCache>(); + using var serviceProvider = services.BuildServiceProvider(); - Assert.NotNull(cache); - } + var cache = serviceProvider.GetService<IFusionCache>(); - [Fact] - public void ThrowsIfMissingRegisteredDistributedCache() - { - var services = new ServiceCollection(); + Assert.NotNull(cache); + } - services.AddFusionCache() - .WithRegisteredDistributedCache() - ; + [Fact] + public void ThrowsIfMissingRegisteredDistributedCache() + { + var services = new ServiceCollection(); - using var serviceProvider = services.BuildServiceProvider(); + services.AddFusionCache() + .WithRegisteredDistributedCache() + ; - Assert.Throws<InvalidOperationException>(() => - { - var cache = serviceProvider.GetRequiredService<IFusionCache>(); - }); - } + using var serviceProvider = services.BuildServiceProvider(); - [Fact] - public void DontThrowIfMissingRegisteredDistributedCache() + Assert.Throws<InvalidOperationException>(() => { - var services = new ServiceCollection(); + var cache = serviceProvider.GetRequiredService<IFusionCache>(); + }); + } - services.AddFusionCache() - .TryWithRegisteredDistributedCache() - ; + [Fact] + public void DontThrowIfMissingRegisteredDistributedCache() + { + var services = new ServiceCollection(); - using var serviceProvider = services.BuildServiceProvider(); + services.AddFusionCache() + .TryWithRegisteredDistributedCache() + ; - var cache = serviceProvider.GetService<IFusionCache>(); + using var serviceProvider = services.BuildServiceProvider(); - Assert.NotNull(cache); - Assert.False(cache.HasDistributedCache); - } + var cache = serviceProvider.GetService<IFusionCache>(); - [Fact] - public void ThrowsIfMissingSerializerWhenUsingDistributedCache() - { - var services = new ServiceCollection(); + Assert.NotNull(cache); + Assert.False(cache.HasDistributedCache); + } - services.AddDistributedMemoryCache(); + [Fact] + public void ThrowsIfMissingSerializerWhenUsingDistributedCache() + { + var services = new ServiceCollection(); - services.AddFusionCache() - .WithRegisteredDistributedCache(false) - ; + services.AddDistributedMemoryCache(); - using var serviceProvider = services.BuildServiceProvider(); + services.AddFusionCache() + .WithRegisteredDistributedCache(false) + ; - Assert.Throws<InvalidOperationException>(() => - { - _ = serviceProvider.GetService<IFusionCache>(); - }); - } + using var serviceProvider = services.BuildServiceProvider(); - [Fact] - public void CanUseMultipleNamedCachesAndConfigureThem() + Assert.Throws<InvalidOperationException>(() => { - var services = new ServiceCollection(); + _ = serviceProvider.GetService<IFusionCache>(); + }); + } + + [Fact] + public void CanUseMultipleNamedCachesAndConfigureThem() + { + var services = new ServiceCollection(); - services.AddDistributedMemoryCache(); - services.AddFusionCacheNewtonsoftJsonSerializer(); + services.AddDistributedMemoryCache(); + services.AddFusionCacheNewtonsoftJsonSerializer(); - // FOO: 10 MIN DURATION + FAIL-SAFE - services.Configure<FusionCacheOptions>("FooCache", opt => + // FOO: 10 MIN DURATION + FAIL-SAFE + services.Configure<FusionCacheOptions>("FooCache", opt => + { + opt.BackplaneChannelPrefix = "AAA"; + }); + + services.AddFusionCache("FooCache") + .WithDefaultEntryOptions(opt => opt + .SetDuration(TimeSpan.FromMinutes(10)) + .SetFailSafe(true) + ) + ; + + // BAR: 42 SEC DURATION + 3 SEC SOFT TIMEOUT + DIST CACHE + services.AddFusionCache("BarCache") + .WithOptions(opt => { - opt.BackplaneChannelPrefix = "AAA"; - }); - - services.AddFusionCache("FooCache") - .WithDefaultEntryOptions(opt => opt - .SetDuration(TimeSpan.FromMinutes(10)) - .SetFailSafe(true) - ) - ; - - // BAR: 42 SEC DURATION + 3 SEC SOFT TIMEOUT + DIST CACHE - services.AddFusionCache("BarCache") - .WithOptions(opt => - { - opt.BackplaneChannelPrefix = "BBB"; - }) - .WithDefaultEntryOptions(opt => opt - .SetDuration(TimeSpan.FromSeconds(42)) - .SetFactoryTimeouts(TimeSpan.FromSeconds(3)) - ) - .WithRegisteredDistributedCache(false) - ; - - // BAZ: 3 HOURS DURATION + FAIL-SAFE + BACKPLANE (POST-SETUP) - services.AddFusionCache("BazCache") - .WithOptions(opt => - { - opt.BackplaneChannelPrefix = "CCC"; - }) - .WithDefaultEntryOptions(opt => opt - .SetDuration(TimeSpan.FromHours(3)) - .SetFailSafe(true) - ) - .WithPostSetup((sp, c) => - { - c.SetupBackplane(new MemoryBackplane(new MemoryBackplaneOptions())); - }) - ; - - // QUX (CUSTOM INSTANCE): 1 SEC DURATION + 123 DAYS DIST DURATION - var quxCacheOriginal = new FusionCache(new FusionCacheOptions() + opt.BackplaneChannelPrefix = "BBB"; + }) + .WithDefaultEntryOptions(opt => opt + .SetDuration(TimeSpan.FromSeconds(42)) + .SetFactoryTimeouts(TimeSpan.FromSeconds(3)) + ) + .WithRegisteredDistributedCache(false) + ; + + // BAZ: 3 HOURS DURATION + FAIL-SAFE + BACKPLANE (POST-SETUP) + services.AddFusionCache("BazCache") + .WithOptions(opt => { - CacheName = "QuxCache", - DefaultEntryOptions = new FusionCacheEntryOptions() - .SetDuration(TimeSpan.FromSeconds(1)) - .SetDistributedCacheDuration(TimeSpan.FromDays(123)) - }); - services.AddFusionCache(quxCacheOriginal); + opt.BackplaneChannelPrefix = "CCC"; + }) + .WithDefaultEntryOptions(opt => opt + .SetDuration(TimeSpan.FromHours(3)) + .SetFailSafe(true) + ) + .WithPostSetup((sp, c) => + { + c.SetupBackplane(new MemoryBackplane(new MemoryBackplaneOptions())); + }) + ; - using var serviceProvider = services.BuildServiceProvider(); + // QUX (CUSTOM INSTANCE): 1 SEC DURATION + 123 DAYS DIST DURATION + var quxCacheOriginal = new FusionCache(new FusionCacheOptions() + { + CacheName = "QuxCache", + DefaultEntryOptions = new FusionCacheEntryOptions() + .SetDuration(TimeSpan.FromSeconds(1)) + .SetDistributedCacheDuration(TimeSpan.FromDays(123)) + }); + services.AddFusionCache(quxCacheOriginal); - var cacheProvider = serviceProvider.GetService<IFusionCacheProvider>()!; + using var serviceProvider = services.BuildServiceProvider(); - var fooCache = cacheProvider.GetCache("FooCache"); - var barCache = cacheProvider.GetCache("BarCache"); - var bazCache = cacheProvider.GetCache("BazCache"); - var quxCache = cacheProvider.GetCache("QuxCache"); + var cacheProvider = serviceProvider.GetService<IFusionCacheProvider>()!; - static FusionCacheOptions GetOptions(IFusionCache cache) - { - return (FusionCacheOptions)(typeof(FusionCache).GetField("_options", BindingFlags.NonPublic | BindingFlags.Instance)!.GetValue(cache)!); - } - - var fooOptions = GetOptions(fooCache); - var barOptions = GetOptions(barCache); - var bazOptions = GetOptions(bazCache); - - Assert.NotNull(fooCache); - Assert.Equal("FooCache", fooCache.CacheName); - Assert.Equal(TimeSpan.FromMinutes(10), fooCache.DefaultEntryOptions.Duration); - Assert.True(fooCache.DefaultEntryOptions.IsFailSafeEnabled); - Assert.Null(fooCache.DefaultEntryOptions.DistributedCacheDuration); - Assert.False(fooCache.HasDistributedCache); - Assert.False(fooCache.HasBackplane); - Assert.Equal("AAA", fooOptions.BackplaneChannelPrefix); - - Assert.NotNull(barCache); - Assert.Equal("BarCache", barCache.CacheName); - Assert.Equal(TimeSpan.FromSeconds(42), barCache.DefaultEntryOptions.Duration); - Assert.Equal(TimeSpan.FromSeconds(3), barCache.DefaultEntryOptions.FactorySoftTimeout); - Assert.Null(barCache.DefaultEntryOptions.DistributedCacheDuration); - Assert.False(barCache.DefaultEntryOptions.IsFailSafeEnabled); - Assert.True(barCache.HasDistributedCache); - Assert.False(barCache.HasBackplane); - Assert.Equal("BBB", barOptions.BackplaneChannelPrefix); - - Assert.NotNull(bazCache); - Assert.Equal("BazCache", bazCache.CacheName); - Assert.Equal(TimeSpan.FromHours(3), bazCache.DefaultEntryOptions.Duration); - Assert.Null(bazCache.DefaultEntryOptions.DistributedCacheDuration); - Assert.True(bazCache.DefaultEntryOptions.IsFailSafeEnabled); - Assert.False(bazCache.HasDistributedCache); - Assert.True(bazCache.HasBackplane); - Assert.Equal("CCC", bazOptions.BackplaneChannelPrefix); - - Assert.NotNull(quxCache); - Assert.Equal("QuxCache", quxCache.CacheName); - Assert.Equal(quxCacheOriginal, quxCache); - Assert.Equal(TimeSpan.FromSeconds(1), quxCache.DefaultEntryOptions.Duration); - Assert.Equal(TimeSpan.FromDays(123), quxCache.DefaultEntryOptions.DistributedCacheDuration); - Assert.True(bazCache.DefaultEntryOptions.IsFailSafeEnabled); - Assert.False(bazCache.HasDistributedCache); - Assert.True(bazCache.HasBackplane); - } + var fooCache = cacheProvider.GetCache("FooCache"); + var barCache = cacheProvider.GetCache("BarCache"); + var bazCache = cacheProvider.GetCache("BazCache"); + var quxCache = cacheProvider.GetCache("QuxCache"); - [Fact] - public void CanUseDefaultCacheWithMultipleNamedCaches() + static FusionCacheOptions GetOptions(IFusionCache cache) { - var services = new ServiceCollection(); - - services.AddFusionCache().TryWithAutoSetup(); - services.AddFusionCache("FooCache").TryWithAutoSetup(); - services.AddFusionCache("BarCache").TryWithAutoSetup(); - services.AddFusionCache("BazCache").TryWithAutoSetup(); + return (FusionCacheOptions)(typeof(FusionCache).GetField("_options", BindingFlags.NonPublic | BindingFlags.Instance)!.GetValue(cache)!); + } - using var serviceProvider = services.BuildServiceProvider(); + var fooOptions = GetOptions(fooCache); + var barOptions = GetOptions(barCache); + var bazOptions = GetOptions(bazCache); + + Assert.NotNull(fooCache); + Assert.Equal("FooCache", fooCache.CacheName); + Assert.Equal(TimeSpan.FromMinutes(10), fooCache.DefaultEntryOptions.Duration); + Assert.True(fooCache.DefaultEntryOptions.IsFailSafeEnabled); + Assert.Null(fooCache.DefaultEntryOptions.DistributedCacheDuration); + Assert.False(fooCache.HasDistributedCache); + Assert.False(fooCache.HasBackplane); + Assert.Equal("AAA", fooOptions.BackplaneChannelPrefix); + + Assert.NotNull(barCache); + Assert.Equal("BarCache", barCache.CacheName); + Assert.Equal(TimeSpan.FromSeconds(42), barCache.DefaultEntryOptions.Duration); + Assert.Equal(TimeSpan.FromSeconds(3), barCache.DefaultEntryOptions.FactorySoftTimeout); + Assert.Null(barCache.DefaultEntryOptions.DistributedCacheDuration); + Assert.False(barCache.DefaultEntryOptions.IsFailSafeEnabled); + Assert.True(barCache.HasDistributedCache); + Assert.False(barCache.HasBackplane); + Assert.Equal("BBB", barOptions.BackplaneChannelPrefix); + + Assert.NotNull(bazCache); + Assert.Equal("BazCache", bazCache.CacheName); + Assert.Equal(TimeSpan.FromHours(3), bazCache.DefaultEntryOptions.Duration); + Assert.Null(bazCache.DefaultEntryOptions.DistributedCacheDuration); + Assert.True(bazCache.DefaultEntryOptions.IsFailSafeEnabled); + Assert.False(bazCache.HasDistributedCache); + Assert.True(bazCache.HasBackplane); + Assert.Equal("CCC", bazOptions.BackplaneChannelPrefix); + + Assert.NotNull(quxCache); + Assert.Equal("QuxCache", quxCache.CacheName); + Assert.Equal(quxCacheOriginal, quxCache); + Assert.Equal(TimeSpan.FromSeconds(1), quxCache.DefaultEntryOptions.Duration); + Assert.Equal(TimeSpan.FromDays(123), quxCache.DefaultEntryOptions.DistributedCacheDuration); + Assert.True(bazCache.DefaultEntryOptions.IsFailSafeEnabled); + Assert.False(bazCache.HasDistributedCache); + Assert.True(bazCache.HasBackplane); + } - var cacheProvider = serviceProvider.GetService<IFusionCacheProvider>()!; + [Fact] + public void CanUseDefaultCacheWithMultipleNamedCaches() + { + var services = new ServiceCollection(); - var fooCache = cacheProvider.GetCache("FooCache"); - var barCache = cacheProvider.GetCache("BarCache"); - var bazCache = cacheProvider.GetCache("BazCache"); - var defaultCache = cacheProvider.GetDefaultCache(); + services.AddFusionCache().TryWithAutoSetup(); + services.AddFusionCache("FooCache").TryWithAutoSetup(); + services.AddFusionCache("BarCache").TryWithAutoSetup(); + services.AddFusionCache("BazCache").TryWithAutoSetup(); - Assert.NotNull(fooCache); - Assert.Equal("FooCache", fooCache.CacheName); + using var serviceProvider = services.BuildServiceProvider(); - Assert.NotNull(barCache); - Assert.Equal("BarCache", barCache.CacheName); + var cacheProvider = serviceProvider.GetService<IFusionCacheProvider>()!; - Assert.NotNull(bazCache); - Assert.Equal("BazCache", bazCache.CacheName); + var fooCache = cacheProvider.GetCache("FooCache"); + var barCache = cacheProvider.GetCache("BarCache"); + var bazCache = cacheProvider.GetCache("BazCache"); + var defaultCache = cacheProvider.GetDefaultCache(); - Assert.NotNull(defaultCache); - Assert.Equal(FusionCacheOptions.DefaultCacheName, defaultCache.CacheName); - } + Assert.NotNull(fooCache); + Assert.Equal("FooCache", fooCache.CacheName); - [Fact] - public void CanUsePostSetupActions() - { - var services = new ServiceCollection(); + Assert.NotNull(barCache); + Assert.Equal("BarCache", barCache.CacheName); - var entryOptions = new FusionCacheEntryOptions() - .SetDuration(TimeSpan.FromMinutes(1)) - ; + Assert.NotNull(bazCache); + Assert.Equal("BazCache", bazCache.CacheName); - services.AddFusionCache() - .WithDefaultEntryOptions(entryOptions) - .WithPostSetup((sp, c) => - { - c.DefaultEntryOptions.Duration = TimeSpan.FromMinutes(123); - }) - ; + Assert.NotNull(defaultCache); + Assert.Equal(FusionCacheOptions.DefaultCacheName, defaultCache.CacheName); + } - using var serviceProvider = services.BuildServiceProvider(); + [Fact] + public void CanUsePostSetupActions() + { + var services = new ServiceCollection(); - var cacheProvider = serviceProvider.GetService<IFusionCacheProvider>()!; + var entryOptions = new FusionCacheEntryOptions() + .SetDuration(TimeSpan.FromMinutes(1)) + ; - var cache = cacheProvider.GetDefaultCache(); + services.AddFusionCache() + .WithDefaultEntryOptions(entryOptions) + .WithPostSetup((sp, c) => + { + c.DefaultEntryOptions.Duration = TimeSpan.FromMinutes(123); + }) + ; - Assert.NotNull(cache); - Assert.Equal(TimeSpan.FromMinutes(123), cache.DefaultEntryOptions.Duration); - } + using var serviceProvider = services.BuildServiceProvider(); - [Fact] - public void CanResetPostSetupActions() - { - var services = new ServiceCollection(); + var cacheProvider = serviceProvider.GetService<IFusionCacheProvider>()!; - var entryOptions = new FusionCacheEntryOptions() - .SetDuration(TimeSpan.FromMinutes(1)) - ; + var cache = cacheProvider.GetDefaultCache(); - services.AddFusionCache() - .WithDefaultEntryOptions(entryOptions) - .WithPostSetup((sp, c) => - { - c.DefaultEntryOptions.Duration = TimeSpan.FromMinutes(123); - }) - .WithPostSetup((sp, c) => - { - c.DefaultEntryOptions.Duration = TimeSpan.FromMinutes(456); - }) - .WithoutPostSetup() - ; + Assert.NotNull(cache); + Assert.Equal(TimeSpan.FromMinutes(123), cache.DefaultEntryOptions.Duration); + } - using var serviceProvider = services.BuildServiceProvider(); + [Fact] + public void CanResetPostSetupActions() + { + var services = new ServiceCollection(); - var cacheProvider = serviceProvider.GetService<IFusionCacheProvider>()!; + var entryOptions = new FusionCacheEntryOptions() + .SetDuration(TimeSpan.FromMinutes(1)) + ; - var cache = cacheProvider.GetDefaultCache(); + services.AddFusionCache() + .WithDefaultEntryOptions(entryOptions) + .WithPostSetup((sp, c) => + { + c.DefaultEntryOptions.Duration = TimeSpan.FromMinutes(123); + }) + .WithPostSetup((sp, c) => + { + c.DefaultEntryOptions.Duration = TimeSpan.FromMinutes(456); + }) + .WithoutPostSetup() + ; - Assert.NotNull(cache); - Assert.Equal(TimeSpan.FromMinutes(1), cache.DefaultEntryOptions.Duration); - } + using var serviceProvider = services.BuildServiceProvider(); - [Fact] - public void DontThrowWhenRequestingAnUnregisteredCache() - { - var services = new ServiceCollection(); + var cacheProvider = serviceProvider.GetService<IFusionCacheProvider>()!; - services.AddFusionCache("FooCache"); - services.AddFusionCache(); + var cache = cacheProvider.GetDefaultCache(); - using var serviceProvider = services.BuildServiceProvider(); + Assert.NotNull(cache); + Assert.Equal(TimeSpan.FromMinutes(1), cache.DefaultEntryOptions.Duration); + } - var cacheProvider = serviceProvider.GetService<IFusionCacheProvider>()!; + [Fact] + public void DontThrowWhenRequestingAnUnregisteredCache() + { + var services = new ServiceCollection(); - Assert.Null(cacheProvider.GetCacheOrNull("BarCache")); - } + services.AddFusionCache("FooCache"); + services.AddFusionCache(); - [Fact] - public void DefaultCacheIsTheSameWhenRequestedInDifferentWays() - { - var services = new ServiceCollection(); + using var serviceProvider = services.BuildServiceProvider(); - services.AddFusionCache(); - services.AddFusionCache(); + var cacheProvider = serviceProvider.GetService<IFusionCacheProvider>()!; - using var serviceProvider = services.BuildServiceProvider(); + Assert.Null(cacheProvider.GetCacheOrNull("BarCache")); + } - var cacheProvider = serviceProvider.GetService<IFusionCacheProvider>()!; + [Fact] + public void DefaultCacheIsTheSameWhenRequestedInDifferentWays() + { + var services = new ServiceCollection(); - Assert.Equal(cacheProvider.GetDefaultCache(), serviceProvider.GetService<IFusionCache>()); - } + services.AddFusionCache(); + services.AddFusionCache(); - [Fact] - public void ThrowsOrNotWhenRequestingUnregisteredNamedCaches() - { - var services = new ServiceCollection(); + using var serviceProvider = services.BuildServiceProvider(); - services.AddFusionCache("Foo"); - services.AddFusionCache("Foo"); + var cacheProvider = serviceProvider.GetService<IFusionCacheProvider>()!; - using var serviceProvider = services.BuildServiceProvider(); + Assert.Equal(cacheProvider.GetDefaultCache(), serviceProvider.GetService<IFusionCache>()); + } - var cacheProvider = serviceProvider.GetService<IFusionCacheProvider>()!; + [Fact] + public void ThrowsOrNotWhenRequestingUnregisteredNamedCaches() + { + var services = new ServiceCollection(); - Assert.Throws<InvalidOperationException>(() => - { - // MULTIPLE Foo CACHES REGISTERED -> THROWS - _ = cacheProvider.GetCache("Foo"); - }); + services.AddFusionCache("Foo"); + services.AddFusionCache("Foo"); - Assert.Throws<InvalidOperationException>(() => - { - // MULTIPLE Foo CACHES REGISTERED -> THROWS - _ = cacheProvider.GetCacheOrNull("Foo"); - }); + using var serviceProvider = services.BuildServiceProvider(); - Assert.Throws<InvalidOperationException>(() => - { - // NO Bar CACHE REGISTERED -> THROWS - _ = cacheProvider.GetCache("Bar"); - }); + var cacheProvider = serviceProvider.GetService<IFusionCacheProvider>()!; - // NO Bar CACHE REGISTERED -> RETURNS NULL - var maybeBarCache = cacheProvider.GetCacheOrNull("Bar"); + Assert.Throws<InvalidOperationException>(() => + { + // MULTIPLE Foo CACHES REGISTERED -> THROWS + _ = cacheProvider.GetCache("Foo"); + }); - Assert.Null(maybeBarCache); - } + Assert.Throws<InvalidOperationException>(() => + { + // MULTIPLE Foo CACHES REGISTERED -> THROWS + _ = cacheProvider.GetCacheOrNull("Foo"); + }); - [Fact] - public void ThrowsOrNotWhenRequestingUnregisteredDefaultCache() + Assert.Throws<InvalidOperationException>(() => { - var services = new ServiceCollection(); + // NO Bar CACHE REGISTERED -> THROWS + _ = cacheProvider.GetCache("Bar"); + }); - services.AddFusionCache("Foo"); + // NO Bar CACHE REGISTERED -> RETURNS NULL + var maybeBarCache = cacheProvider.GetCacheOrNull("Bar"); - using var serviceProvider = services.BuildServiceProvider(); + Assert.Null(maybeBarCache); + } - var cacheProvider = serviceProvider.GetService<IFusionCacheProvider>()!; + [Fact] + public void ThrowsOrNotWhenRequestingUnregisteredDefaultCache() + { + var services = new ServiceCollection(); - Assert.Throws<InvalidOperationException>(() => - { - // NO DEFAULT CACHE REGISTERED -> THROWS - _ = cacheProvider.GetDefaultCache(); - }); + services.AddFusionCache("Foo"); - // NO DEFAULT CACHE REGISTERED -> RETURNS NULL - var maybeDefaultCache = cacheProvider.GetDefaultCacheOrNull(); + using var serviceProvider = services.BuildServiceProvider(); - Assert.Null(maybeDefaultCache); - } + var cacheProvider = serviceProvider.GetService<IFusionCacheProvider>()!; - [Fact] - public void CacheInstancesAreAlwaysTheSame() + Assert.Throws<InvalidOperationException>(() => { - var services = new ServiceCollection(); + // NO DEFAULT CACHE REGISTERED -> THROWS + _ = cacheProvider.GetDefaultCache(); + }); - services.AddFusionCache(); - services.AddFusionCache("Foo"); - services.AddFusionCache("Bar"); + // NO DEFAULT CACHE REGISTERED -> RETURNS NULL + var maybeDefaultCache = cacheProvider.GetDefaultCacheOrNull(); - using var serviceProvider = services.BuildServiceProvider(); + Assert.Null(maybeDefaultCache); + } - var cacheProvider = serviceProvider.GetService<IFusionCacheProvider>()!; + [Fact] + public void CacheInstancesAreAlwaysTheSame() + { + var services = new ServiceCollection(); - var defaultCache1 = cacheProvider.GetDefaultCache(); - var defaultCache2 = cacheProvider.GetDefaultCache(); + services.AddFusionCache(); + services.AddFusionCache("Foo"); + services.AddFusionCache("Bar"); - var fooCache1 = cacheProvider.GetCache("Foo"); - var fooCache2 = cacheProvider.GetCache("Foo"); + using var serviceProvider = services.BuildServiceProvider(); - var barCache1 = cacheProvider.GetCache("Bar"); - var barCache2 = cacheProvider.GetCache("Bar"); + var cacheProvider = serviceProvider.GetService<IFusionCacheProvider>()!; - Assert.Same(defaultCache1, defaultCache2); - Assert.Same(fooCache1, fooCache2); - Assert.Same(barCache1, barCache2); - } + var defaultCache1 = cacheProvider.GetDefaultCache(); + var defaultCache2 = cacheProvider.GetDefaultCache(); - [Fact] - public void DifferentNamedCachesDoNotShareTheSameMemoryCacheByDefault() - { - var services = new ServiceCollection(); + var fooCache1 = cacheProvider.GetCache("Foo"); + var fooCache2 = cacheProvider.GetCache("Foo"); - services.AddMemoryCache(); + var barCache1 = cacheProvider.GetCache("Bar"); + var barCache2 = cacheProvider.GetCache("Bar"); - // DEFAULT - services.AddFusionCache(); + Assert.Same(defaultCache1, defaultCache2); + Assert.Same(fooCache1, fooCache2); + Assert.Same(barCache1, barCache2); + } - // FOO - services.AddFusionCache("FooCache"); + [Fact] + public void DifferentNamedCachesDoNotShareTheSameMemoryCacheByDefault() + { + var services = new ServiceCollection(); - // BAR - services.AddFusionCache("BarCache"); + services.AddMemoryCache(); - using var serviceProvider = services.BuildServiceProvider(); + // DEFAULT + services.AddFusionCache(); - var cacheProvider = serviceProvider.GetService<IFusionCacheProvider>()!; + // FOO + services.AddFusionCache("FooCache"); - var defaultCache = cacheProvider.GetDefaultCache(); - var fooCache = cacheProvider.GetCache("FooCache"); - var barCache = cacheProvider.GetCache("BarCache"); + // BAR + services.AddFusionCache("BarCache"); - var defaultCacheValue = defaultCache.GetOrSet("sloth", 1); - var fooCacheValue = fooCache.GetOrSet("sloth", 2); - var barCacheValue = barCache.GetOrSet("sloth", 3); + using var serviceProvider = services.BuildServiceProvider(); - Assert.Equal(1, defaultCacheValue); - Assert.Equal(2, fooCacheValue); - Assert.Equal(3, barCacheValue); - } + var cacheProvider = serviceProvider.GetService<IFusionCacheProvider>()!; - [Fact] - public void DifferentNamedCachesCanShareTheSameMemoryCacheWithCollisions() - { - var services = new ServiceCollection(); + var defaultCache = cacheProvider.GetDefaultCache(); + var fooCache = cacheProvider.GetCache("FooCache"); + var barCache = cacheProvider.GetCache("BarCache"); - services.AddMemoryCache(); + var defaultCacheValue = defaultCache.GetOrSet("sloth", 1); + var fooCacheValue = fooCache.GetOrSet("sloth", 2); + var barCacheValue = barCache.GetOrSet("sloth", 3); - // DEFAULT - services.AddFusionCache() - .WithRegisteredMemoryCache() - ; + Assert.Equal(1, defaultCacheValue); + Assert.Equal(2, fooCacheValue); + Assert.Equal(3, barCacheValue); + } - // FOO - services.AddFusionCache("FooCache") - .WithRegisteredMemoryCache() - ; + [Fact] + public void DifferentNamedCachesCanShareTheSameMemoryCacheWithCollisions() + { + var services = new ServiceCollection(); - // BAR - services.AddFusionCache("BarCache") - .WithRegisteredMemoryCache() - ; + services.AddMemoryCache(); - using var serviceProvider = services.BuildServiceProvider(); + // DEFAULT + services.AddFusionCache() + .WithRegisteredMemoryCache() + ; - var cacheProvider = serviceProvider.GetService<IFusionCacheProvider>()!; + // FOO + services.AddFusionCache("FooCache") + .WithRegisteredMemoryCache() + ; - var defaultCache = cacheProvider.GetDefaultCache(); - var fooCache = cacheProvider.GetCache("FooCache"); - var barCache = cacheProvider.GetCache("BarCache"); + // BAR + services.AddFusionCache("BarCache") + .WithRegisteredMemoryCache() + ; - var defaultCacheValue = defaultCache.GetOrSet("sloth", 1); - var fooCacheValue = fooCache.GetOrSet("sloth", 2); - var barCacheValue = barCache.GetOrSet("sloth", 3); + using var serviceProvider = services.BuildServiceProvider(); - Assert.Equal(1, defaultCacheValue); - Assert.Equal(1, fooCacheValue); - Assert.Equal(1, barCacheValue); - } + var cacheProvider = serviceProvider.GetService<IFusionCacheProvider>()!; - [Fact] - public void DifferentNamedCachesCanShareTheSameMemoryCacheWithoutCollisions() - { - var services = new ServiceCollection(); + var defaultCache = cacheProvider.GetDefaultCache(); + var fooCache = cacheProvider.GetCache("FooCache"); + var barCache = cacheProvider.GetCache("BarCache"); + + var defaultCacheValue = defaultCache.GetOrSet("sloth", 1); + var fooCacheValue = fooCache.GetOrSet("sloth", 2); + var barCacheValue = barCache.GetOrSet("sloth", 3); - services.AddMemoryCache(); + Assert.Equal(1, defaultCacheValue); + Assert.Equal(1, fooCacheValue); + Assert.Equal(1, barCacheValue); + } - // DEFAULT - services.AddFusionCache() - .WithRegisteredMemoryCache() - ; + [Fact] + public void DifferentNamedCachesCanShareTheSameMemoryCacheWithoutCollisions() + { + var services = new ServiceCollection(); - // FOO - services.AddFusionCache("FooCache") - .WithRegisteredMemoryCache().WithCacheKeyPrefix() - ; + services.AddMemoryCache(); - // BAR - services.AddFusionCache("BarCache") - .WithRegisteredMemoryCache().WithCacheKeyPrefix() - ; + // DEFAULT + services.AddFusionCache() + .WithRegisteredMemoryCache() + ; - using var serviceProvider = services.BuildServiceProvider(); + // FOO + services.AddFusionCache("FooCache") + .WithRegisteredMemoryCache().WithCacheKeyPrefix() + ; - var cacheProvider = serviceProvider.GetService<IFusionCacheProvider>()!; + // BAR + services.AddFusionCache("BarCache") + .WithRegisteredMemoryCache().WithCacheKeyPrefix() + ; - var defaultCache = cacheProvider.GetDefaultCache(); - var fooCache = cacheProvider.GetCache("FooCache"); - var barCache = cacheProvider.GetCache("BarCache"); + using var serviceProvider = services.BuildServiceProvider(); - var defaultCacheValue = defaultCache.GetOrSet("sloth", 1); - var fooCacheValue = fooCache.GetOrSet("sloth", 2); - var barCacheValue = barCache.GetOrSet("sloth", 3); + var cacheProvider = serviceProvider.GetService<IFusionCacheProvider>()!; - Assert.Equal(1, defaultCacheValue); - Assert.Equal(2, fooCacheValue); - Assert.Equal(3, barCacheValue); - } + var defaultCache = cacheProvider.GetDefaultCache(); + var fooCache = cacheProvider.GetCache("FooCache"); + var barCache = cacheProvider.GetCache("BarCache"); - [Fact] - public void BuilderWithSpecificComponentsWorks() - { - var services = new ServiceCollection(); + var defaultCacheValue = defaultCache.GetOrSet("sloth", 1); + var fooCacheValue = fooCache.GetOrSet("sloth", 2); + var barCacheValue = barCache.GetOrSet("sloth", 3); - services.AddLogging(); + Assert.Equal(1, defaultCacheValue); + Assert.Equal(2, fooCacheValue); + Assert.Equal(3, barCacheValue); + } - // FOO: EXTERNAL (NAMED) OPTIONS + DISTRIBUTED CACHE (MEMORY, DIRECT) + SERIALIZER (FACTORY) + BACKPLANE (REDIS) - services.Configure<RedisBackplaneOptions>("Foo", opt => - { - opt.Configuration = "CONN_FOO"; - }); - - services.AddFusionCache("Foo") - .WithSerializer(sp => new FusionCacheSystemTextJsonSerializer()) - .WithDistributedCache( - new MemoryDistributedCache(Options.Create(new MemoryDistributedCacheOptions())) - ) - .WithStackExchangeRedisBackplane() - ; - - // BAR: PLAIN - services.AddFusionCache("Bar"); - - // BAZ: DISTRIBUTED CACHE (MEMORY, VIA FACTORY) + BACKPLANE (MEMORY) - services.AddFusionCache("Baz") - .WithSystemTextJsonSerializer(new JsonSerializerOptions() - { - IncludeFields = false - }) - .WithDistributedCache(sp => - { - var options = sp.GetService<IOptionsMonitor<MemoryDistributedCacheOptions>>()?.Get("Baz") ?? new MemoryDistributedCacheOptions(); - var loggerFactory = sp.GetService<ILoggerFactory>(); - - return new MemoryDistributedCache( - Options.Create(options), - loggerFactory - ); - }) - .WithMemoryBackplane() - ; - - // DEFAULT: BACKPLANE (REDIS) VIA DIRECT INSTANCE - services.AddFusionCache() - .WithBackplane(new RedisBackplane(new RedisBackplaneOptions - { - Configuration = "CONN_DEFAULT" - })) - ; - - using var serviceProvider = services.BuildServiceProvider(); - - var cacheProvider = serviceProvider.GetRequiredService<IFusionCacheProvider>(); - - var fooCache = cacheProvider.GetCache("Foo"); - var barCache = cacheProvider.GetCache("Bar"); - var bazCache = cacheProvider.GetCache("Baz"); - var defaultCache = serviceProvider.GetRequiredService<IFusionCache>(); - - var fooDistributedCache = GetDistributedCache<MemoryDistributedCache>(fooCache); - var bazDistributedCache = GetDistributedCache<MemoryDistributedCache>(bazCache); - - var fooBackplane = GetBackplane<RedisBackplane>(fooCache); - var fooBackplaneOptions = GetRedisBackplaneOptions(fooCache)!; - var barBackplane = GetBackplane<IFusionCacheBackplane>(barCache); - var bazBackplane = GetBackplane<MemoryBackplane>(bazCache); - var defaultBackplane = GetBackplane<RedisBackplane>(defaultCache); - var defaultBackplaneOptions = GetRedisBackplaneOptions(defaultCache)!; - - Assert.NotNull(fooCache); - Assert.Equal("Foo", fooCache.CacheName); - Assert.True(fooCache.HasDistributedCache); - Assert.NotNull(fooDistributedCache); - Assert.True(fooCache.HasBackplane); - Assert.NotNull(fooBackplane); - Assert.Equal("CONN_FOO", fooBackplaneOptions.Configuration); - - Assert.NotNull(barCache); - Assert.Equal("Bar", barCache.CacheName); - Assert.False(barCache.HasDistributedCache); - Assert.False(barCache.HasBackplane); - Assert.Null(barBackplane); - - Assert.NotNull(bazCache); - Assert.Equal("Baz", bazCache.CacheName); - Assert.True(bazCache.HasDistributedCache); - Assert.NotNull(bazDistributedCache); - Assert.True(bazCache.HasBackplane); - Assert.NotNull(bazBackplane); - - Assert.NotNull(defaultCache); - Assert.Equal(FusionCacheOptions.DefaultCacheName, defaultCache.CacheName); - Assert.False(defaultCache.HasDistributedCache); - Assert.True(defaultCache.HasBackplane); - Assert.NotNull(defaultBackplane); - Assert.Equal("CONN_DEFAULT", defaultBackplaneOptions.Configuration); - } + [Fact] + public void BuilderWithSpecificComponentsWorks() + { + var services = new ServiceCollection(); + + services.AddLogging(); - // [Fact] - // public void ExistingAndObsoleteCallsStillWork() - // { - // static ServiceCollection CreateServiceCollection() - // { - // var services = new ServiceCollection(); - - // // REGISTER SOME FUSIONCACHE-RELATED COMPONENTS, TO SEE IFTHEY ARE PICKED UP - // services.AddStackExchangeRedisCache(opt => opt.Configuration = "CONN_FOO"); - // services.AddFusionCacheSystemTextJsonSerializer(); - - // return services; - // } - - // ServiceCollection services; - - // // // 01: BASIC - // // // - // // // NOTE: VALID ONLY BEFORE V0.20.0 - // // var services = CreateServiceCollection(); - - // //#pragma warning disable CS0618 // Type or member is obsolete - // // services.AddFusionCache(); - // //#pragma warning restore CS0618 // Type or member is obsolete - - // // using (var serviceProvider = services.BuildServiceProvider()) - // // { - // // var cache = serviceProvider.GetRequiredService<IFusionCache>(); - - // // Assert.True(cache.HasDistributedCache); - // // } - - // // 02: OPTIONS - // services = CreateServiceCollection(); - - //#pragma warning disable CS0618 // Type or member is obsolete - // services.AddFusionCache( - // opt => - // { - // opt.BackplaneAutoRecoveryMaxItems = 123; - // } - // ); - //#pragma warning restore CS0618 // Type or member is obsolete - - // using (var serviceProvider = services.BuildServiceProvider()) - // { - // var cache = serviceProvider.GetRequiredService<IFusionCache>(); - - // Assert.True(cache.HasDistributedCache); - // } - - // // 03: OPTIONS + FLAG1 - // services = CreateServiceCollection(); - - //#pragma warning disable CS0618 // Type or member is obsolete - // services.AddFusionCache( - // opt => - // { - // opt.BackplaneAutoRecoveryMaxItems = 123; - // }, - // false - // ); - //#pragma warning restore CS0618 // Type or member is obsolete - - // using (var serviceProvider = services.BuildServiceProvider()) - // { - // var cache = serviceProvider.GetRequiredService<IFusionCache>(); - - // Assert.False(cache.HasDistributedCache); - // } - - // // 04: OPTIONS + FLAG1 + FLAG2 - // services = CreateServiceCollection(); - - //#pragma warning disable CS0618 // Type or member is obsolete - // services.AddFusionCache( - // opt => - // { - // opt.BackplaneAutoRecoveryMaxItems = 123; - // }, - // false, - // false - // ); - //#pragma warning restore CS0618 // Type or member is obsolete - - // using (var serviceProvider = services.BuildServiceProvider()) - // { - // var cache = serviceProvider.GetRequiredService<IFusionCache>(); - - // Assert.False(cache.HasDistributedCache); - // } - - // // 05: FLAG1 - // services = CreateServiceCollection(); - - //#pragma warning disable CS0618 // Type or member is obsolete - // services.AddFusionCache( - // useDistributedCacheIfAvailable: false - // ); - //#pragma warning restore CS0618 // Type or member is obsolete - - // using (var serviceProvider = services.BuildServiceProvider()) - // { - // var cache = serviceProvider.GetRequiredService<IFusionCache>(); - - // Assert.False(cache.HasDistributedCache); - // } - - // // 06: FLAG2 - // services = CreateServiceCollection(); - - //#pragma warning disable CS0618 // Type or member is obsolete - // services.AddFusionCache( - // ignoreMemoryDistributedCache: false - // ); - //#pragma warning restore CS0618 // Type or member is obsolete - - // using (var serviceProvider = services.BuildServiceProvider()) - // { - // var cache = serviceProvider.GetRequiredService<IFusionCache>(); - - // Assert.True(cache.HasDistributedCache); - // } - // } - - [Fact] - public void CanDoWithoutLogger() + // FOO: EXTERNAL (NAMED) OPTIONS + DISTRIBUTED CACHE (MEMORY, DIRECT) + SERIALIZER (FACTORY) + BACKPLANE (REDIS) + services.Configure<RedisBackplaneOptions>("Foo", opt => { - var services = new ServiceCollection(); + opt.Configuration = "CONN_FOO"; + }); + + services.AddFusionCache("Foo") + .WithSerializer(sp => new FusionCacheSystemTextJsonSerializer()) + .WithDistributedCache( + new MemoryDistributedCache(Options.Create(new MemoryDistributedCacheOptions())) + ) + .WithStackExchangeRedisBackplane() + ; + + // BAR: PLAIN + services.AddFusionCache("Bar"); + + // BAZ: DISTRIBUTED CACHE (MEMORY, VIA FACTORY) + BACKPLANE (MEMORY) + services.AddFusionCache("Baz") + .WithSystemTextJsonSerializer(new JsonSerializerOptions() + { + IncludeFields = false + }) + .WithDistributedCache(sp => + { + var options = sp.GetService<IOptionsMonitor<MemoryDistributedCacheOptions>>()?.Get("Baz") ?? new MemoryDistributedCacheOptions(); + var loggerFactory = sp.GetService<ILoggerFactory>(); + + return new MemoryDistributedCache( + Options.Create(options), + loggerFactory + ); + }) + .WithMemoryBackplane() + ; + + // DEFAULT: BACKPLANE (REDIS) VIA DIRECT INSTANCE + services.AddFusionCache() + .WithBackplane(new RedisBackplane(new RedisBackplaneOptions + { + Configuration = "CONN_DEFAULT" + })) + ; + + using var serviceProvider = services.BuildServiceProvider(); + + var cacheProvider = serviceProvider.GetRequiredService<IFusionCacheProvider>(); + + var fooCache = cacheProvider.GetCache("Foo"); + var barCache = cacheProvider.GetCache("Bar"); + var bazCache = cacheProvider.GetCache("Baz"); + var defaultCache = serviceProvider.GetRequiredService<IFusionCache>(); + + var fooDistributedCache = GetDistributedCache<MemoryDistributedCache>(fooCache); + var bazDistributedCache = GetDistributedCache<MemoryDistributedCache>(bazCache); + + var fooBackplane = GetBackplane<RedisBackplane>(fooCache); + var fooBackplaneOptions = GetRedisBackplaneOptions(fooCache)!; + var barBackplane = GetBackplane<IFusionCacheBackplane>(barCache); + var bazBackplane = GetBackplane<MemoryBackplane>(bazCache); + var defaultBackplane = GetBackplane<RedisBackplane>(defaultCache); + var defaultBackplaneOptions = GetRedisBackplaneOptions(defaultCache)!; + + Assert.NotNull(fooCache); + Assert.Equal("Foo", fooCache.CacheName); + Assert.True(fooCache.HasDistributedCache); + Assert.NotNull(fooDistributedCache); + Assert.True(fooCache.HasBackplane); + Assert.NotNull(fooBackplane); + Assert.Equal("CONN_FOO", fooBackplaneOptions.Configuration); + + Assert.NotNull(barCache); + Assert.Equal("Bar", barCache.CacheName); + Assert.False(barCache.HasDistributedCache); + Assert.False(barCache.HasBackplane); + Assert.Null(barBackplane); + + Assert.NotNull(bazCache); + Assert.Equal("Baz", bazCache.CacheName); + Assert.True(bazCache.HasDistributedCache); + Assert.NotNull(bazDistributedCache); + Assert.True(bazCache.HasBackplane); + Assert.NotNull(bazBackplane); + + Assert.NotNull(defaultCache); + Assert.Equal(FusionCacheOptions.DefaultCacheName, defaultCache.CacheName); + Assert.False(defaultCache.HasDistributedCache); + Assert.True(defaultCache.HasBackplane); + Assert.NotNull(defaultBackplane); + Assert.Equal("CONN_DEFAULT", defaultBackplaneOptions.Configuration); + } - services.AddLogging(); + [Fact] + public void CanDoWithoutLogger() + { + var services = new ServiceCollection(); - services.AddFusionCache() - .WithoutLogger() - ; + services.AddLogging(); - using (var serviceProvider = services.BuildServiceProvider()) - { - var cache = serviceProvider.GetRequiredService<IFusionCache>(); + services.AddFusionCache() + .WithoutLogger() + ; + + using (var serviceProvider = services.BuildServiceProvider()) + { + var cache = serviceProvider.GetRequiredService<IFusionCache>(); - Assert.Null(GetLogger(cache)); - } + Assert.Null(GetLogger(cache)); } } } diff --git a/tests/ZiggyCreatures.FusionCache.Tests/DistributedCacheLevelTests.cs b/tests/ZiggyCreatures.FusionCache.Tests/DistributedCacheLevelTests.cs new file mode 100644 index 00000000..c406a879 --- /dev/null +++ b/tests/ZiggyCreatures.FusionCache.Tests/DistributedCacheLevelTests.cs @@ -0,0 +1,1189 @@ +using System; +using System.Diagnostics; +using System.Threading; +using System.Threading.Tasks; +using FusionCacheTests.Stuff; +using Microsoft.Extensions.Caching.Distributed; +using Microsoft.Extensions.Caching.Memory; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; +using Xunit; +using Xunit.Abstractions; +using ZiggyCreatures.Caching.Fusion; +using ZiggyCreatures.Caching.Fusion.Chaos; +using ZiggyCreatures.Caching.Fusion.Internals; + +namespace FusionCacheTests; + +public class DistributedCacheLevelTests + : AbstractTests +{ + public DistributedCacheLevelTests(ITestOutputHelper output) + : base(output, "MyCache:") + { + } + + private FusionCacheOptions CreateFusionCacheOptions() + { + var res = new FusionCacheOptions(); + + res.CacheKeyPrefix = TestingCacheKeyPrefix; + + return res; + } + + [Theory] + [ClassData(typeof(SerializerTypesClassData))] + public async Task ReturnsDataFromDistributedCacheIfNoDataInMemoryCacheAsync(SerializerType serializerType) + { + using var memoryCache = new MemoryCache(new MemoryCacheOptions()); + var distributedCache = new MemoryDistributedCache(Options.Create(new MemoryDistributedCacheOptions())); + using var fusionCache = new FusionCache(CreateFusionCacheOptions(), memoryCache).SetupDistributedCache(distributedCache, TestsUtils.GetSerializer(serializerType)); + var initialValue = await fusionCache.GetOrSetAsync<int>("foo", _ => Task.FromResult(42), new FusionCacheEntryOptions().SetDurationSec(10)); + memoryCache.Remove(TestsUtils.MaybePreProcessCacheKey("foo", TestingCacheKeyPrefix)); + var newValue = await fusionCache.GetOrSetAsync<int>("foo", _ => Task.FromResult(21), new FusionCacheEntryOptions().SetDurationSec(10)); + Assert.Equal(initialValue, newValue); + } + + [Theory] + [ClassData(typeof(SerializerTypesClassData))] + public void ReturnsDataFromDistributedCacheIfNoDataInMemoryCache(SerializerType serializerType) + { + using var memoryCache = new MemoryCache(new MemoryCacheOptions()); + var distributedCache = new MemoryDistributedCache(Options.Create(new MemoryDistributedCacheOptions())); + using var fusionCache = new FusionCache(CreateFusionCacheOptions(), memoryCache).SetupDistributedCache(distributedCache, TestsUtils.GetSerializer(serializerType)); + fusionCache.DefaultEntryOptions.AllowBackgroundDistributedCacheOperations = false; + + var initialValue = fusionCache.GetOrSet<int>("foo", _ => 42, options => options.SetDurationSec(10)); + memoryCache.Remove(TestsUtils.MaybePreProcessCacheKey("foo", TestingCacheKeyPrefix)); + var newValue = fusionCache.GetOrSet<int>("foo", _ => 21, options => options.SetDurationSec(10)); + Assert.Equal(initialValue, newValue); + } + + [Theory] + [ClassData(typeof(SerializerTypesClassData))] + public async Task HandlesDistributedCacheFailuresAsync(SerializerType serializerType) + { + var distributedCache = new MemoryDistributedCache(Options.Create(new MemoryDistributedCacheOptions())); + var chaosDistributedCache = new ChaosDistributedCache(distributedCache); + using var fusionCache = new FusionCache(CreateFusionCacheOptions()).SetupDistributedCache(chaosDistributedCache, TestsUtils.GetSerializer(serializerType)); + var initialValue = await fusionCache.GetOrSetAsync<int>("foo", _ => Task.FromResult(42), new FusionCacheEntryOptions() { Duration = TimeSpan.FromSeconds(1), IsFailSafeEnabled = true }); + await Task.Delay(1_500); + chaosDistributedCache.SetAlwaysThrow(); + var newValue = await fusionCache.GetOrSetAsync<int>("foo", async _ => throw new Exception("Generic error"), new FusionCacheEntryOptions(TimeSpan.FromSeconds(1)) { IsFailSafeEnabled = true }); + Assert.Equal(initialValue, newValue); + } + + [Theory] + [ClassData(typeof(SerializerTypesClassData))] + public void HandlesDistributedCacheFailures(SerializerType serializerType) + { + var distributedCache = new MemoryDistributedCache(Options.Create(new MemoryDistributedCacheOptions())); + var chaosDistributedCache = new ChaosDistributedCache(distributedCache); + using var fusionCache = new FusionCache(CreateFusionCacheOptions()).SetupDistributedCache(chaosDistributedCache, TestsUtils.GetSerializer(serializerType)); + var initialValue = fusionCache.GetOrSet<int>("foo", _ => 42, new FusionCacheEntryOptions() { Duration = TimeSpan.FromSeconds(1), IsFailSafeEnabled = true }); + Thread.Sleep(1_500); + chaosDistributedCache.SetAlwaysThrow(); + var newValue = fusionCache.GetOrSet<int>("foo", _ => throw new Exception("Generic error"), new FusionCacheEntryOptions(TimeSpan.FromSeconds(1)) { IsFailSafeEnabled = true }); + Assert.Equal(initialValue, newValue); + } + + [Theory] + [ClassData(typeof(SerializerTypesClassData))] + public async Task HandlesDistributedCacheRemovalInTheMiddleOfAnOperationAsync(SerializerType serializerType) + { + var distributedCache = new MemoryDistributedCache(Options.Create(new MemoryDistributedCacheOptions())); + using var fusionCache = new FusionCache(CreateFusionCacheOptions()).SetupDistributedCache(distributedCache, TestsUtils.GetSerializer(serializerType)); + var task = fusionCache.GetOrSetAsync<int>("foo", async _ => { await Task.Delay(2_000); return 42; }, new FusionCacheEntryOptions(TimeSpan.FromSeconds(10))); + await Task.Delay(500); + fusionCache.RemoveDistributedCache(); + var value = await task; + Assert.Equal(42, value); + } + + [Theory] + [ClassData(typeof(SerializerTypesClassData))] + public async Task HandlesDistributedCacheFailuresInTheMiddleOfAnOperationAsync(SerializerType serializerType) + { + using var memoryCache = new MemoryCache(new MemoryCacheOptions()); + var distributedCache = new MemoryDistributedCache(Options.Create(new MemoryDistributedCacheOptions())); + var chaosDistributedCache = new ChaosDistributedCache(distributedCache); + var options = CreateFusionCacheOptions(); + options.DistributedCacheKeyModifierMode = CacheKeyModifierMode.None; + using var fusionCache = new FusionCache(options, memoryCache).SetupDistributedCache(chaosDistributedCache, TestsUtils.GetSerializer(serializerType)); + + var preProcessedCacheKey = TestsUtils.MaybePreProcessCacheKey("bar", options.CacheKeyPrefix); + + await fusionCache.GetOrSetAsync<int>("bar", async _ => { return 42; }, new FusionCacheEntryOptions(TimeSpan.FromSeconds(10))); + Assert.NotNull(distributedCache.GetString(preProcessedCacheKey)); + + preProcessedCacheKey = TestsUtils.MaybePreProcessCacheKey("foo", options.CacheKeyPrefix); + var task = fusionCache.GetOrSetAsync<int>("foo", async _ => { await Task.Delay(2_000); return 42; }, new FusionCacheEntryOptions(TimeSpan.FromSeconds(10))); + await Task.Delay(500); + chaosDistributedCache.SetAlwaysThrow(); + var value = await task; + chaosDistributedCache.SetNeverThrow(); + + // END RESULT IS WHAT EXPECTED + Assert.Equal(42, value); + + // MEMORY CACHE HAS BEEN UPDATED + Assert.Equal(42, memoryCache.Get<IFusionCacheEntry>(preProcessedCacheKey)?.GetValue<int>()); + + // DISTRIBUTED CACHE HAS -NOT- BEEN UPDATED + Assert.Null(distributedCache.GetString(preProcessedCacheKey)); + } + + [Theory] + [ClassData(typeof(SerializerTypesClassData))] + public async Task AppliesDistributedCacheHardTimeoutAsync(SerializerType serializerType) + { + var simulatedDelayMs = TimeSpan.FromMilliseconds(2_000); + var softTimeout = TimeSpan.FromMilliseconds(100); + var hardTimeout = TimeSpan.FromMilliseconds(1_000); + var distributedCache = new MemoryDistributedCache(Options.Create(new MemoryDistributedCacheOptions())); + var chaosDistributedCache = new ChaosDistributedCache(distributedCache); + + using var memoryCache = new MemoryCache(new MemoryCacheOptions()); + using var fusionCache = new FusionCache(CreateFusionCacheOptions(), memoryCache); + fusionCache.SetupDistributedCache(chaosDistributedCache, TestsUtils.GetSerializer(serializerType)); + + await fusionCache.SetAsync<int>("foo", 42, new FusionCacheEntryOptions().SetDurationSec(1).SetFailSafe(true)); + await Task.Delay(TimeSpan.FromSeconds(1).PlusALittleBit()); + memoryCache.Remove(TestsUtils.MaybePreProcessCacheKey("foo", TestingCacheKeyPrefix)); + chaosDistributedCache.SetAlwaysDelayExactly(simulatedDelayMs); + await Assert.ThrowsAsync<Exception>(async () => + { + _ = await fusionCache.GetOrSetAsync<int>("foo", _ => throw new Exception("Sloths are cool"), new FusionCacheEntryOptions().SetDurationSec(1).SetFailSafe(true).SetDistributedCacheTimeouts(softTimeout, hardTimeout)); + }); + } + + [Theory] + [ClassData(typeof(SerializerTypesClassData))] + public void AppliesDistributedCacheHardTimeout(SerializerType serializerType) + { + var simulatedDelayMs = TimeSpan.FromMilliseconds(2_000); + var softTimeout = TimeSpan.FromMilliseconds(100); + var hardTimeout = TimeSpan.FromMilliseconds(1_000); + var distributedCache = new MemoryDistributedCache(Options.Create(new MemoryDistributedCacheOptions())); + var chaosDistributedCache = new ChaosDistributedCache(distributedCache); + + using var memoryCache = new MemoryCache(new MemoryCacheOptions()); + using var fusionCache = new FusionCache(CreateFusionCacheOptions(), memoryCache); + fusionCache.SetupDistributedCache(chaosDistributedCache, TestsUtils.GetSerializer(serializerType)); + + fusionCache.Set<int>("foo", 42, new FusionCacheEntryOptions().SetDurationSec(1).SetFailSafe(true)); + Thread.Sleep(TimeSpan.FromSeconds(1).PlusALittleBit()); + memoryCache.Remove(TestsUtils.MaybePreProcessCacheKey("foo", TestingCacheKeyPrefix)); + chaosDistributedCache.SetAlwaysDelayExactly(simulatedDelayMs); + Assert.Throws<Exception>(() => + { + _ = fusionCache.GetOrSet<int>("foo", _ => throw new Exception("Sloths are cool"), new FusionCacheEntryOptions().SetDurationSec(1).SetFailSafe(true).SetDistributedCacheTimeouts(softTimeout, hardTimeout)); + }); + } + + [Theory] + [ClassData(typeof(SerializerTypesClassData))] + public async Task AppliesDistributedCacheSoftTimeoutAsync(SerializerType serializerType) + { + var simulatedDelay = TimeSpan.FromMilliseconds(2_000); + var softTimeout = TimeSpan.FromMilliseconds(100); + var hardTimeout = TimeSpan.FromMilliseconds(1_000); + var duration = TimeSpan.FromSeconds(1); + var distributedCache = new MemoryDistributedCache(Options.Create(new MemoryDistributedCacheOptions())); + var chaosDistributedCache = new ChaosDistributedCache(distributedCache); + + using var memoryCache = new MemoryCache(new MemoryCacheOptions()); + using var fusionCache = new FusionCache(CreateFusionCacheOptions(), memoryCache); + fusionCache.SetupDistributedCache(chaosDistributedCache, TestsUtils.GetSerializer(serializerType)); + await fusionCache.SetAsync<int>("foo", 42, new FusionCacheEntryOptions().SetDuration(duration).SetFailSafe(true)); + await Task.Delay(duration.PlusALittleBit()); + var sw = Stopwatch.StartNew(); + chaosDistributedCache.SetAlwaysDelayExactly(simulatedDelay); + var res = await fusionCache.GetOrSetAsync<int>("foo", async _ => throw new Exception("Sloths are cool"), new FusionCacheEntryOptions().SetDurationSec(1).SetFailSafe(true).SetDistributedCacheTimeouts(TimeSpan.FromMilliseconds(100), TimeSpan.FromMilliseconds(1_000))); + sw.Stop(); + + Assert.Equal(42, res); + Assert.True(sw.ElapsedMilliseconds >= 100, "Distributed cache soft timeout not applied"); + Assert.True(sw.Elapsed < simulatedDelay, "Distributed cache soft timeout not applied"); + } + + [Theory] + [ClassData(typeof(SerializerTypesClassData))] + public void AppliesDistributedCacheSoftTimeout(SerializerType serializerType) + { + var simulatedDelay = TimeSpan.FromMilliseconds(2_000); + var softTimeout = TimeSpan.FromMilliseconds(100); + var hardTimeout = TimeSpan.FromMilliseconds(1_000); + var duration = TimeSpan.FromSeconds(1); + var distributedCache = new MemoryDistributedCache(Options.Create(new MemoryDistributedCacheOptions())); + var chaosDistributedCache = new ChaosDistributedCache(distributedCache); + + using var memoryCache = new MemoryCache(new MemoryCacheOptions()); + using var fusionCache = new FusionCache(CreateFusionCacheOptions(), memoryCache); + fusionCache.SetupDistributedCache(chaosDistributedCache, TestsUtils.GetSerializer(serializerType)); + fusionCache.Set<int>("foo", 42, new FusionCacheEntryOptions().SetDuration(duration).SetFailSafe(true)); + Thread.Sleep(duration.PlusALittleBit()); + var sw = Stopwatch.StartNew(); + chaosDistributedCache.SetAlwaysDelayExactly(simulatedDelay); + var res = fusionCache.GetOrSet<int>("foo", _ => throw new Exception("Sloths are cool"), new FusionCacheEntryOptions().SetDurationSec(1).SetFailSafe(true).SetDistributedCacheTimeouts(softTimeout, hardTimeout)); + sw.Stop(); + + Assert.Equal(42, res); + Assert.True(sw.ElapsedMilliseconds >= 100, "Distributed cache soft timeout not applied"); + Assert.True(sw.Elapsed < simulatedDelay, "Distributed cache soft timeout not applied"); + } + + [Theory] + [ClassData(typeof(SerializerTypesClassData))] + public async Task DistributedCacheCircuitBreakerActuallyWorksAsync(SerializerType serializerType) + { + var circuitBreakerDuration = TimeSpan.FromSeconds(2); + var distributedCache = new MemoryDistributedCache(Options.Create(new MemoryDistributedCacheOptions())); + var chaosDistributedCache = new ChaosDistributedCache(distributedCache); + + using var memoryCache = new MemoryCache(new MemoryCacheOptions()); + var options = CreateFusionCacheOptions(); + options.EnableAutoRecovery = false; + options.DistributedCacheCircuitBreakerDuration = circuitBreakerDuration; + using var fusionCache = new FusionCache(options, memoryCache); + fusionCache.DefaultEntryOptions.AllowBackgroundDistributedCacheOperations = false; + fusionCache.SetupDistributedCache(chaosDistributedCache, TestsUtils.GetSerializer(serializerType)); + + await fusionCache.SetAsync<int>("foo", 1, options => options.SetDurationSec(60).SetFailSafe(true)); + chaosDistributedCache.SetAlwaysThrow(); + await fusionCache.SetAsync<int>("foo", 2, options => options.SetDurationSec(60).SetFailSafe(true)); + chaosDistributedCache.SetNeverThrow(); + await fusionCache.SetAsync<int>("foo", 3, options => options.SetDurationSec(60).SetFailSafe(true)); + await Task.Delay(circuitBreakerDuration.PlusALittleBit()); + memoryCache.Remove(TestsUtils.MaybePreProcessCacheKey("foo", TestingCacheKeyPrefix)); + var res = await fusionCache.GetOrDefaultAsync<int>("foo", -1); + + Assert.Equal(1, res); + } + + [Theory] + [ClassData(typeof(SerializerTypesClassData))] + public void DistributedCacheCircuitBreakerActuallyWorks(SerializerType serializerType) + { + var circuitBreakerDuration = TimeSpan.FromSeconds(2); + var distributedCache = new MemoryDistributedCache(Options.Create(new MemoryDistributedCacheOptions())); + var chaosDistributedCache = new ChaosDistributedCache(distributedCache); + + using var memoryCache = new MemoryCache(new MemoryCacheOptions()); + var options = CreateFusionCacheOptions(); + options.EnableAutoRecovery = false; + options.DistributedCacheCircuitBreakerDuration = circuitBreakerDuration; + using var fusionCache = new FusionCache(options, memoryCache); + fusionCache.DefaultEntryOptions.AllowBackgroundDistributedCacheOperations = false; + fusionCache.SetupDistributedCache(chaosDistributedCache, TestsUtils.GetSerializer(serializerType)); + + fusionCache.Set<int>("foo", 1, options => options.SetDurationSec(60).SetFailSafe(true)); + chaosDistributedCache.SetAlwaysThrow(); + fusionCache.Set<int>("foo", 2, options => options.SetDurationSec(60).SetFailSafe(true)); + chaosDistributedCache.SetNeverThrow(); + fusionCache.Set<int>("foo", 3, options => options.SetDurationSec(60).SetFailSafe(true)); + Thread.Sleep(circuitBreakerDuration.PlusALittleBit()); + memoryCache.Remove(TestsUtils.MaybePreProcessCacheKey("foo", TestingCacheKeyPrefix)); + var res = fusionCache.GetOrDefault<int>("foo", -1); + + Assert.Equal(1, res); + } + + private void _DistributedCacheWireVersionModifierWorks(SerializerType serializerType, CacheKeyModifierMode modifierMode) + { + using var memoryCache = new MemoryCache(new MemoryCacheOptions()); + var distributedCache = new MemoryDistributedCache(Options.Create(new MemoryDistributedCacheOptions())); + var options = CreateFusionCacheOptions(); + options.DistributedCacheKeyModifierMode = modifierMode; + using var fusionCache = new FusionCache(options, memoryCache).SetupDistributedCache(distributedCache, TestsUtils.GetSerializer(serializerType)); + var cacheKey = "foo"; + var preProcessedCacheKey = TestsUtils.MaybePreProcessCacheKey(cacheKey, options.CacheKeyPrefix); + string distributedCacheKey; + switch (modifierMode) + { + case CacheKeyModifierMode.Prefix: + distributedCacheKey = $"{FusionCacheOptions.DistributedCacheWireFormatVersion}{FusionCacheOptions.DistributedCacheWireFormatSeparator}{preProcessedCacheKey}"; + break; + case CacheKeyModifierMode.Suffix: + distributedCacheKey = $"{preProcessedCacheKey}{FusionCacheOptions.DistributedCacheWireFormatSeparator}{FusionCacheOptions.DistributedCacheWireFormatVersion}"; + break; + default: + distributedCacheKey = preProcessedCacheKey; + break; + } + var value = "sloths"; + fusionCache.Set(cacheKey, value, new FusionCacheEntryOptions(TimeSpan.FromHours(24)) { AllowBackgroundDistributedCacheOperations = false }); + var nullValue = distributedCache.Get("foo42"); + var distributedValue = distributedCache.Get(distributedCacheKey); + Assert.Null(nullValue); + Assert.NotNull(distributedValue); + } + + [Theory] + [ClassData(typeof(SerializerTypesClassData))] + public void DistributedCacheWireVersionPrefixModeWorks(SerializerType serializerType) + { + _DistributedCacheWireVersionModifierWorks(serializerType, CacheKeyModifierMode.Prefix); + } + + [Theory] + [ClassData(typeof(SerializerTypesClassData))] + public void DistributedCacheWireVersionSuffixModeWorks(SerializerType serializerType) + { + _DistributedCacheWireVersionModifierWorks(serializerType, CacheKeyModifierMode.Suffix); + } + + [Theory] + [ClassData(typeof(SerializerTypesClassData))] + public void DistributedCacheWireVersionNoneModeWorks(SerializerType serializerType) + { + _DistributedCacheWireVersionModifierWorks(serializerType, CacheKeyModifierMode.None); + } + + [Theory] + [ClassData(typeof(SerializerTypesClassData))] + public async Task ReThrowsOriginalExceptionsAsync(SerializerType serializerType) + { + var distributedCache = new MemoryDistributedCache(Options.Create(new MemoryDistributedCacheOptions())); + var chaosDistributedCache = new ChaosDistributedCache(distributedCache); + + chaosDistributedCache.SetAlwaysThrow(); + var options = CreateFusionCacheOptions(); + options.ReThrowOriginalExceptions = true; + options.DefaultEntryOptions.ReThrowDistributedCacheExceptions = true; + using var fusionCache = new FusionCache(options); + + fusionCache.SetupDistributedCache(chaosDistributedCache, TestsUtils.GetSerializer(serializerType)); + + await Assert.ThrowsAsync<ChaosException>(async () => + { + await fusionCache.SetAsync<int>("foo", 42); + }); + + await Assert.ThrowsAsync<ChaosException>(async () => + { + _ = await fusionCache.TryGetAsync<int>("bar"); + }); + } + + [Theory] + [ClassData(typeof(SerializerTypesClassData))] + public async Task ReThrowsOriginalExceptions(SerializerType serializerType) + { + var distributedCache = new MemoryDistributedCache(Options.Create(new MemoryDistributedCacheOptions())); + var chaosDistributedCache = new ChaosDistributedCache(distributedCache); + + chaosDistributedCache.SetAlwaysThrow(); + var options = CreateFusionCacheOptions(); + options.ReThrowOriginalExceptions = true; + options.DefaultEntryOptions.ReThrowDistributedCacheExceptions = true; + using var fusionCache = new FusionCache(options); + + fusionCache.SetupDistributedCache(chaosDistributedCache, TestsUtils.GetSerializer(serializerType)); + + Assert.Throws<ChaosException>(() => + { + fusionCache.Set<int>("foo", 42); + }); + + Assert.Throws<ChaosException>(() => + { + _ = fusionCache.TryGet<int>("bar"); + }); + } + + [Theory] + [ClassData(typeof(SerializerTypesClassData))] + public async Task ReThrowsDistributedCacheExceptionsAsync(SerializerType serializerType) + { + var distributedCache = new MemoryDistributedCache(Options.Create(new MemoryDistributedCacheOptions())); + var chaosDistributedCache = new ChaosDistributedCache(distributedCache); + + chaosDistributedCache.SetAlwaysThrow(); + using var fusionCache = new FusionCache(CreateFusionCacheOptions()); + fusionCache.DefaultEntryOptions.ReThrowDistributedCacheExceptions = true; + + fusionCache.SetupDistributedCache(chaosDistributedCache, TestsUtils.GetSerializer(serializerType)); + + await Assert.ThrowsAsync<FusionCacheDistributedCacheException>(async () => + { + await fusionCache.SetAsync<int>("foo", 42); + }); + + await Assert.ThrowsAsync<FusionCacheDistributedCacheException>(async () => + { + _ = await fusionCache.TryGetAsync<int>("bar"); + }); + } + + [Theory] + [ClassData(typeof(SerializerTypesClassData))] + public void ReThrowsDistributedCacheExceptions(SerializerType serializerType) + { + var distributedCache = new MemoryDistributedCache(Options.Create(new MemoryDistributedCacheOptions())); + var chaosDistributedCache = new ChaosDistributedCache(distributedCache); + + chaosDistributedCache.SetAlwaysThrow(); + using var fusionCache = new FusionCache(CreateFusionCacheOptions()); + fusionCache.DefaultEntryOptions.ReThrowDistributedCacheExceptions = true; + + fusionCache.SetupDistributedCache(chaosDistributedCache, TestsUtils.GetSerializer(serializerType)); + + Assert.Throws<FusionCacheDistributedCacheException>(() => + { + fusionCache.Set<int>("foo", 42); + }); + + Assert.Throws<FusionCacheDistributedCacheException>(() => + { + _ = fusionCache.TryGet<int>("bar"); + }); + } + + [Theory] + [ClassData(typeof(SerializerTypesClassData))] + public async Task ReThrowsSerializationExceptionsAsync(SerializerType serializerType) + { + var distributedCache = new MemoryDistributedCache(Options.Create(new MemoryDistributedCacheOptions())); + var serializer = new ChaosSerializer(TestsUtils.GetSerializer(serializerType)); + + using var fusionCache = new FusionCache(CreateFusionCacheOptions()); + fusionCache.DefaultEntryOptions.ReThrowSerializationExceptions = true; + + fusionCache.SetupDistributedCache(distributedCache, serializer); + + serializer.SetAlwaysThrow(); + await Assert.ThrowsAsync<FusionCacheSerializationException>(async () => + { + await fusionCache.SetAsync<string>("foo", "sloths, sloths everywhere", x => x.SetDuration(TimeSpan.FromMilliseconds(100)).SetDistributedCacheDuration(TimeSpan.FromSeconds(10))); + }); + + serializer.SetNeverThrow(); + await fusionCache.SetAsync<string>("foo", "sloths, sloths everywhere", x => x.SetDuration(TimeSpan.FromMilliseconds(100)).SetDistributedCacheDuration(TimeSpan.FromSeconds(10))); + + Thread.Sleep(TimeSpan.FromSeconds(1)); + + serializer.SetAlwaysThrow(); + await Assert.ThrowsAsync<FusionCacheSerializationException>(async () => + { + _ = await fusionCache.TryGetAsync<int>("foo"); + }); + } + + [Theory] + [ClassData(typeof(SerializerTypesClassData))] + public void ReThrowsSerializationExceptions(SerializerType serializerType) + { + var distributedCache = new MemoryDistributedCache(Options.Create(new MemoryDistributedCacheOptions())); + var serializer = new ChaosSerializer(TestsUtils.GetSerializer(serializerType)); + + using var fusionCache = new FusionCache(CreateFusionCacheOptions()); + fusionCache.DefaultEntryOptions.ReThrowSerializationExceptions = true; + + fusionCache.SetupDistributedCache(distributedCache, serializer); + + serializer.SetAlwaysThrow(); + Assert.Throws<FusionCacheSerializationException>(() => + { + fusionCache.Set<string>("foo", "sloths, sloths everywhere", x => x.SetDuration(TimeSpan.FromMilliseconds(100)).SetDistributedCacheDuration(TimeSpan.FromSeconds(10))); + }); + + serializer.SetNeverThrow(); + fusionCache.Set<string>("foo", "sloths, sloths everywhere", x => x.SetDuration(TimeSpan.FromMilliseconds(100)).SetDistributedCacheDuration(TimeSpan.FromSeconds(10))); + + Thread.Sleep(TimeSpan.FromSeconds(1)); + + serializer.SetAlwaysThrow(); + Assert.Throws<FusionCacheSerializationException>(() => + { + _ = fusionCache.TryGet<int>("foo"); + }); + } + + [Theory] + [ClassData(typeof(SerializerTypesClassData))] + public async Task DoesNotReThrowsSerializationExceptionsAsync(SerializerType serializerType) + { + var distributedCache = new MemoryDistributedCache(Options.Create(new MemoryDistributedCacheOptions())); + var serializer = new ChaosSerializer(TestsUtils.GetSerializer(serializerType)); + + using var fusionCache = new FusionCache(CreateFusionCacheOptions()); + fusionCache.DefaultEntryOptions.ReThrowSerializationExceptions = false; + + fusionCache.SetupDistributedCache(distributedCache, serializer); + + serializer.SetAlwaysThrow(); + await fusionCache.SetAsync<string>("foo", "sloths, sloths everywhere", x => x.SetDuration(TimeSpan.FromMilliseconds(100)).SetDistributedCacheDuration(TimeSpan.FromSeconds(10))); + + serializer.SetNeverThrow(); + await fusionCache.SetAsync<string>("foo", "sloths, sloths everywhere", x => x.SetDuration(TimeSpan.FromMilliseconds(100)).SetDistributedCacheDuration(TimeSpan.FromSeconds(10))); + + Thread.Sleep(TimeSpan.FromSeconds(1)); + + serializer.SetAlwaysThrow(); + var res = await fusionCache.TryGetAsync<int>("foo"); + + Assert.False(res.HasValue); + } + + [Theory] + [ClassData(typeof(SerializerTypesClassData))] + public void DoesNotReThrowsSerializationExceptions(SerializerType serializerType) + { + var distributedCache = new MemoryDistributedCache(Options.Create(new MemoryDistributedCacheOptions())); + var serializer = new ChaosSerializer(TestsUtils.GetSerializer(serializerType)); + + using var fusionCache = new FusionCache(CreateFusionCacheOptions()); + fusionCache.DefaultEntryOptions.ReThrowSerializationExceptions = false; + + fusionCache.SetupDistributedCache(distributedCache, serializer); + + serializer.SetAlwaysThrow(); + fusionCache.Set<string>("foo", "sloths, sloths everywhere", x => x.SetDuration(TimeSpan.FromMilliseconds(100)).SetDistributedCacheDuration(TimeSpan.FromSeconds(10))); + + serializer.SetNeverThrow(); + fusionCache.Set<string>("foo", "sloths, sloths everywhere", x => x.SetDuration(TimeSpan.FromMilliseconds(100)).SetDistributedCacheDuration(TimeSpan.FromSeconds(10))); + + Thread.Sleep(TimeSpan.FromSeconds(1)); + + serializer.SetAlwaysThrow(); + var res = fusionCache.TryGet<int>("foo"); + + Assert.False(res.HasValue); + } + + [Theory] + [ClassData(typeof(SerializerTypesClassData))] + public async Task SpecificDistributedCacheDurationWorksAsync(SerializerType serializerType) + { + var distributedCache = new MemoryDistributedCache(Options.Create(new MemoryDistributedCacheOptions())); + using var fusionCache = new FusionCache(CreateFusionCacheOptions()).SetupDistributedCache(distributedCache, TestsUtils.GetSerializer(serializerType)); + await fusionCache.SetAsync<int>("foo", 21, opt => opt.SetFailSafe(false).SetDuration(TimeSpan.FromSeconds(1)).SetDistributedCacheDuration(TimeSpan.FromMinutes(1))); + await Task.Delay(TimeSpan.FromSeconds(2)); + var value = await fusionCache.GetOrDefaultAsync<int>("foo"); + Assert.Equal(21, value); + } + + [Theory] + [ClassData(typeof(SerializerTypesClassData))] + public void SpecificDistributedCacheDurationWorks(SerializerType serializerType) + { + var distributedCache = new MemoryDistributedCache(Options.Create(new MemoryDistributedCacheOptions())); + using var fusionCache = new FusionCache(CreateFusionCacheOptions()).SetupDistributedCache(distributedCache, TestsUtils.GetSerializer(serializerType)); + fusionCache.Set<int>("foo", 21, opt => opt.SetFailSafe(false).SetDuration(TimeSpan.FromSeconds(1)).SetDistributedCacheDuration(TimeSpan.FromMinutes(1))); + Thread.Sleep(TimeSpan.FromSeconds(2)); + var value = fusionCache.GetOrDefault<int>("foo"); + Assert.Equal(21, value); + } + + [Theory] + [ClassData(typeof(SerializerTypesClassData))] + public async Task SpecificDistributedCacheDurationWithFailSafeWorksAsync(SerializerType serializerType) + { + var distributedCache = new MemoryDistributedCache(Options.Create(new MemoryDistributedCacheOptions())); + using var fusionCache = new FusionCache(CreateFusionCacheOptions()).SetupDistributedCache(distributedCache, TestsUtils.GetSerializer(serializerType)); + await fusionCache.SetAsync<int>("foo", 21, opt => opt.SetFailSafe(true).SetDuration(TimeSpan.FromSeconds(1)).SetDistributedCacheDuration(TimeSpan.FromMinutes(1))); + await Task.Delay(TimeSpan.FromSeconds(2)); + var value = await fusionCache.GetOrDefaultAsync<int>("foo"); + Assert.Equal(21, value); + } + + [Theory] + [ClassData(typeof(SerializerTypesClassData))] + public void SpecificDistributedCacheDurationWithFailSafeWorks(SerializerType serializerType) + { + var distributedCache = new MemoryDistributedCache(Options.Create(new MemoryDistributedCacheOptions())); + using var fusionCache = new FusionCache(CreateFusionCacheOptions()).SetupDistributedCache(distributedCache, TestsUtils.GetSerializer(serializerType)); + fusionCache.Set<int>("foo", 21, opt => opt.SetFailSafe(true).SetDuration(TimeSpan.FromSeconds(1)).SetDistributedCacheDuration(TimeSpan.FromMinutes(1))); + Thread.Sleep(TimeSpan.FromSeconds(2)); + var value = fusionCache.GetOrDefault<int>("foo"); + Assert.Equal(21, value); + } + + [Theory] + [ClassData(typeof(SerializerTypesClassData))] + public async Task DistributedCacheFailSafeMaxDurationWorksAsync(SerializerType serializerType) + { + var distributedCache = new MemoryDistributedCache(Options.Create(new MemoryDistributedCacheOptions())); + using var fusionCache = new FusionCache(CreateFusionCacheOptions()).SetupDistributedCache(distributedCache, TestsUtils.GetSerializer(serializerType)); + await fusionCache.SetAsync<int>("foo", 21, opt => opt.SetDuration(TimeSpan.FromSeconds(1)).SetFailSafe(true, TimeSpan.FromSeconds(2)).SetDistributedCacheFailSafeOptions(TimeSpan.FromMinutes(10))); + await Task.Delay(TimeSpan.FromSeconds(2)); + var value = await fusionCache.GetOrDefaultAsync<int>("foo", opt => opt.SetFailSafe(true)); + Assert.Equal(21, value); + } + + [Theory] + [ClassData(typeof(SerializerTypesClassData))] + public void DistributedCacheFailSafeMaxDurationWorks(SerializerType serializerType) + { + var distributedCache = new MemoryDistributedCache(Options.Create(new MemoryDistributedCacheOptions())); + using var fusionCache = new FusionCache(CreateFusionCacheOptions()).SetupDistributedCache(distributedCache, TestsUtils.GetSerializer(serializerType)); + fusionCache.Set<int>("foo", 21, opt => opt.SetDuration(TimeSpan.FromSeconds(1)).SetFailSafe(true, TimeSpan.FromSeconds(2)).SetDistributedCacheFailSafeOptions(TimeSpan.FromMinutes(10))); + Thread.Sleep(TimeSpan.FromSeconds(2)); + var value = fusionCache.GetOrDefault<int>("foo", opt => opt.SetFailSafe(true)); + Assert.Equal(21, value); + } + + [Theory] + [ClassData(typeof(SerializerTypesClassData))] + public async Task DistributedCacheFailSafeMaxDurationNormalizationOccursAsync(SerializerType serializerType) + { + var duration = TimeSpan.FromSeconds(5); + var maxDuration = TimeSpan.FromSeconds(1); + + var distributedCache = new MemoryDistributedCache(Options.Create(new MemoryDistributedCacheOptions())); + using var fusionCache = new FusionCache(CreateFusionCacheOptions()).SetupDistributedCache(distributedCache, TestsUtils.GetSerializer(serializerType)); + await fusionCache.SetAsync<int>("foo", 21, opt => opt.SetDuration(duration).SetFailSafe(true, maxDuration).SetDistributedCacheFailSafeOptions(maxDuration)); + await Task.Delay(maxDuration.PlusALittleBit()); + var value = await fusionCache.GetOrDefaultAsync<int>("foo", opt => opt.SetFailSafe(true)); + Assert.Equal(21, value); + } + + [Theory] + [ClassData(typeof(SerializerTypesClassData))] + public void DistributedCacheFailSafeMaxDurationNormalizationOccurs(SerializerType serializerType) + { + var duration = TimeSpan.FromSeconds(5); + var maxDuration = TimeSpan.FromSeconds(1); + + var distributedCache = new MemoryDistributedCache(Options.Create(new MemoryDistributedCacheOptions())); + using var fusionCache = new FusionCache(CreateFusionCacheOptions()).SetupDistributedCache(distributedCache, TestsUtils.GetSerializer(serializerType)); + fusionCache.Set<int>("foo", 21, opt => opt.SetDuration(duration).SetFailSafe(true, maxDuration).SetDistributedCacheFailSafeOptions(maxDuration)); + Thread.Sleep(maxDuration.PlusALittleBit()); + var value = fusionCache.GetOrDefault<int>("foo", opt => opt.SetFailSafe(true)); + Assert.Equal(21, value); + } + + [Theory] + [ClassData(typeof(SerializerTypesClassData))] + public async Task MemoryExpirationAlignedWithDistributedAsync(SerializerType serializerType) + { + var firstDuration = TimeSpan.FromSeconds(4); + var secondDuration = TimeSpan.FromSeconds(10); + + 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)) + ; + + await fusionCache1.SetAsync<int>("foo", 21, opt => opt.SetDuration(firstDuration)); + await Task.Delay(firstDuration / 2); + var v1 = await fusionCache2.GetOrDefaultAsync<int>("foo", 42, opt => opt.SetDuration(secondDuration)); + await Task.Delay(firstDuration + TimeSpan.FromSeconds(1)); + var v2 = await fusionCache2.GetOrDefaultAsync<int>("foo", 42, opt => opt.SetDuration(secondDuration)); + + Assert.Equal(21, v1); + Assert.Equal(42, v2); + } + + [Theory] + [ClassData(typeof(SerializerTypesClassData))] + public void MemoryExpirationAlignedWithDistributed(SerializerType serializerType) + { + var firstDuration = TimeSpan.FromSeconds(4); + var secondDuration = TimeSpan.FromSeconds(10); + + 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.Set<int>("foo", 21, opt => opt.SetDuration(firstDuration)); + Thread.Sleep(firstDuration / 2); + var v1 = fusionCache2.GetOrDefault<int>("foo", 42, opt => opt.SetDuration(secondDuration)); + Thread.Sleep(firstDuration + TimeSpan.FromSeconds(1)); + var v2 = fusionCache2.GetOrDefault<int>("foo", 42, opt => opt.SetDuration(secondDuration)); + + Assert.Equal(21, v1); + Assert.Equal(42, v2); + } + + [Theory] + [ClassData(typeof(SerializerTypesClassData))] + public async Task CanSkipDistributedCacheAsync(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)); + + var v1 = await fusionCache1.GetOrSetAsync<int>("foo", 1, opt => opt.SetDuration(TimeSpan.FromSeconds(10)).SetFailSafe(true).SetSkipDistributedCache(true, true)); + var v2 = await fusionCache2.GetOrSetAsync<int>("foo", 2, opt => opt.SetDuration(TimeSpan.FromSeconds(10)).SetFailSafe(true)); + + Assert.Equal(1, v1); + Assert.Equal(2, v2); + + var v3 = await fusionCache1.GetOrSetAsync<int>("bar", 3, opt => opt.SetDuration(TimeSpan.FromSeconds(2)).SetFailSafe(true)); + var v4 = await fusionCache2.GetOrSetAsync<int>("bar", 4, opt => opt.SetDuration(TimeSpan.FromSeconds(2)).SetFailSafe(true).SetSkipDistributedCache(true, true)); + + Assert.Equal(3, v3); + Assert.Equal(4, v4); + } + + [Theory] + [ClassData(typeof(SerializerTypesClassData))] + public void CanSkipDistributedCache(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)); + + var v1 = fusionCache1.GetOrSet<int>("foo", 1, opt => opt.SetDuration(TimeSpan.FromSeconds(10)).SetFailSafe(true).SetSkipDistributedCache(true, true)); + var v2 = fusionCache2.GetOrSet<int>("foo", 2, opt => opt.SetDuration(TimeSpan.FromSeconds(10)).SetFailSafe(true)); + + Assert.Equal(1, v1); + Assert.Equal(2, v2); + + var v3 = fusionCache1.GetOrSet<int>("bar", 3, opt => opt.SetDuration(TimeSpan.FromSeconds(2)).SetFailSafe(true)); + var v4 = fusionCache2.GetOrSet<int>("bar", 4, opt => opt.SetDuration(TimeSpan.FromSeconds(2)).SetFailSafe(true).SetSkipDistributedCache(true, true)); + + Assert.Equal(3, v3); + Assert.Equal(4, v4); + } + + [Theory] + [ClassData(typeof(SerializerTypesClassData))] + public async Task CanSkipDistributedReadWhenStaleAsync(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)); + + var v1 = await fusionCache1.GetOrSetAsync<int>("foo", 1, opt => opt.SetDuration(TimeSpan.FromSeconds(2)).SetFailSafe(true)); + var v2 = await fusionCache2.GetOrSetAsync<int>("foo", 2, opt => opt.SetDuration(TimeSpan.FromSeconds(2)).SetFailSafe(true)); + + Assert.Equal(1, v1); + Assert.Equal(1, v2); + + await Task.Delay(TimeSpan.FromSeconds(2).PlusALittleBit()); + + v1 = await fusionCache1.GetOrSetAsync<int>("foo", 3, opt => opt.SetDuration(TimeSpan.FromSeconds(2)).SetFailSafe(true)); + v2 = await fusionCache2.GetOrSetAsync<int>("foo", 4, opt => opt.SetDuration(TimeSpan.FromSeconds(2)).SetFailSafe(true).SetSkipDistributedCacheReadWhenStale(true)); + + Assert.Equal(3, v1); + Assert.Equal(4, v2); + } + + [Theory] + [ClassData(typeof(SerializerTypesClassData))] + public void CanSkipDistributedReadWhenStale(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)); + + var v1 = fusionCache1.GetOrSet<int>("foo", 1, opt => opt.SetDuration(TimeSpan.FromSeconds(2)).SetFailSafe(true)); + var v2 = fusionCache2.GetOrSet<int>("foo", 2, opt => opt.SetDuration(TimeSpan.FromSeconds(2)).SetFailSafe(true)); + + Assert.Equal(1, v1); + Assert.Equal(1, v2); + + Thread.Sleep(TimeSpan.FromSeconds(2).PlusALittleBit()); + + v1 = fusionCache1.GetOrSet<int>("foo", 3, opt => opt.SetDuration(TimeSpan.FromSeconds(2)).SetFailSafe(true)); + v2 = fusionCache2.GetOrSet<int>("foo", 4, opt => opt.SetDuration(TimeSpan.FromSeconds(2)).SetFailSafe(true).SetSkipDistributedCacheReadWhenStale(true)); + + Assert.Equal(3, v1); + Assert.Equal(4, v2); + } + + [Theory] + [ClassData(typeof(SerializerTypesClassData))] + public async Task CanHandleConditionalRefreshAsync(SerializerType serializerType) + { + static async Task<int> FakeGetAsync(FusionCacheFactoryExecutionContext<int> ctx, FakeHttpEndpoint endpoint) + { + FakeHttpResponse resp; + + if (ctx.HasETag && ctx.HasStaleValue) + { + // ETAG + STALE VALUE -> TRY WITH A CONDITIONAL GET + resp = endpoint.Get(ctx.ETag); + + if (resp.StatusCode == 304) + { + // NOT MODIFIED -> RETURN STALE VALUE + return ctx.NotModified(); + } + } + else + { + // NO STALE VALUE OR NO ETAG -> NORMAL (FULL) GET + resp = endpoint.Get(); + } + + return ctx.Modified( + resp.Content.GetValueOrDefault(), + resp.ETag + ); + } + + var duration = TimeSpan.FromSeconds(1); + var endpoint = new FakeHttpEndpoint(1); + + var distributedCache = new MemoryDistributedCache(Options.Create(new MemoryDistributedCacheOptions())); + using var cache = new FusionCache(CreateFusionCacheOptions()).SetupDistributedCache(distributedCache, TestsUtils.GetSerializer(serializerType)); + + // TOT REQ + 1 / FULL RESP + 1 + var v1 = await cache.GetOrSetAsync<int>("foo", async (ctx, _) => await FakeGetAsync(ctx, endpoint), opt => opt.SetDuration(duration).SetFailSafe(true)); + + // CACHED -> NO INCR + var v2 = await cache.GetOrSetAsync<int>("foo", async (ctx, _) => await FakeGetAsync(ctx, endpoint), opt => opt.SetDuration(duration).SetFailSafe(true)); + + // LET THE CACHE EXPIRE + await Task.Delay(duration.PlusALittleBit()); + + // TOT REQ + 1 / COND REQ + 1 / NOT MOD RESP + 1 + var v3 = await cache.GetOrSetAsync<int>("foo", async (ctx, _) => await FakeGetAsync(ctx, endpoint), opt => opt.SetDuration(duration).SetFailSafe(true)); + + // LET THE CACHE EXPIRE + await Task.Delay(duration.PlusALittleBit()); + + // TOT REQ + 1 / COND REQ + 1 / NOT MOD RESP + 1 + var v4 = await cache.GetOrSetAsync<int>("foo", async (ctx, _) => await FakeGetAsync(ctx, endpoint), opt => opt.SetDuration(duration).SetFailSafe(true)); + + // SET VALUE -> CHANGE LAST MODIFIED + endpoint.SetValue(42); + + // LET THE CACHE EXPIRE + await Task.Delay(duration.PlusALittleBit()); + + // TOT REQ + 1 / COND REQ + 1 / FULL RESP + 1 + var v5 = await cache.GetOrSetAsync<int>("foo", async (ctx, _) => await FakeGetAsync(ctx, endpoint), opt => opt.SetDuration(duration).SetFailSafe(true)); + + Assert.Equal(4, endpoint.TotalRequestsCount); + Assert.Equal(3, endpoint.ConditionalRequestsCount); + Assert.Equal(2, endpoint.FullResponsesCount); + Assert.Equal(2, endpoint.NotModifiedResponsesCount); + + Assert.Equal(1, v1); + Assert.Equal(1, v2); + Assert.Equal(1, v3); + Assert.Equal(1, v4); + Assert.Equal(42, v5); + } + + [Theory] + [ClassData(typeof(SerializerTypesClassData))] + public void CanHandleConditionalRefresh(SerializerType serializerType) + { + static int FakeGet(FusionCacheFactoryExecutionContext<int> ctx, FakeHttpEndpoint endpoint) + { + FakeHttpResponse resp; + + if (ctx.HasETag && ctx.HasStaleValue) + { + // ETAG + STALE VALUE -> TRY WITH A CONDITIONAL GET + resp = endpoint.Get(ctx.ETag); + + if (resp.StatusCode == 304) + { + // NOT MODIFIED -> RETURN STALE VALUE + return ctx.NotModified(); + } + } + else + { + // NO STALE VALUE OR NO ETAG -> NORMAL (FULL) GET + resp = endpoint.Get(); + } + + return ctx.Modified( + resp.Content.GetValueOrDefault(), + resp.ETag + ); + } + + var duration = TimeSpan.FromSeconds(1); + var endpoint = new FakeHttpEndpoint(1); + + var distributedCache = new MemoryDistributedCache(Options.Create(new MemoryDistributedCacheOptions())); + using var cache = new FusionCache(CreateFusionCacheOptions()).SetupDistributedCache(distributedCache, TestsUtils.GetSerializer(serializerType)); + + // TOT REQ + 1 / FULL RESP + 1 + var v1 = cache.GetOrSet<int>("foo", (ctx, _) => FakeGet(ctx, endpoint), opt => opt.SetDuration(duration).SetFailSafe(true)); + + // CACHED -> NO INCR + var v2 = cache.GetOrSet<int>("foo", (ctx, _) => FakeGet(ctx, endpoint), opt => opt.SetDuration(duration).SetFailSafe(true)); + + // LET THE CACHE EXPIRE + Thread.Sleep(duration.PlusALittleBit()); + + // TOT REQ + 1 / COND REQ + 1 / NOT MOD RESP + 1 + var v3 = cache.GetOrSet<int>("foo", (ctx, _) => FakeGet(ctx, endpoint), opt => opt.SetDuration(duration).SetFailSafe(true)); + + // LET THE CACHE EXPIRE + Thread.Sleep(duration.PlusALittleBit()); + + // TOT REQ + 1 / COND REQ + 1 / NOT MOD RESP + 1 + var v4 = cache.GetOrSet<int>("foo", (ctx, _) => FakeGet(ctx, endpoint), opt => opt.SetDuration(duration).SetFailSafe(true)); + + // SET VALUE -> CHANGE LAST MODIFIED + endpoint.SetValue(42); + + // LET THE CACHE EXPIRE + Thread.Sleep(duration.PlusALittleBit()); + + // TOT REQ + 1 / COND REQ + 1 / FULL RESP + 1 + var v5 = cache.GetOrSet<int>("foo", (ctx, _) => FakeGet(ctx, endpoint), opt => opt.SetDuration(duration).SetFailSafe(true)); + + Assert.Equal(4, endpoint.TotalRequestsCount); + Assert.Equal(3, endpoint.ConditionalRequestsCount); + Assert.Equal(2, endpoint.FullResponsesCount); + Assert.Equal(2, endpoint.NotModifiedResponsesCount); + + Assert.Equal(1, v1); + Assert.Equal(1, v2); + Assert.Equal(1, v3); + Assert.Equal(1, v4); + Assert.Equal(42, v5); + } + + [Theory] + [ClassData(typeof(SerializerTypesClassData))] + public async Task CanHandleEagerRefreshAsync(SerializerType serializerType) + { + var duration = TimeSpan.FromSeconds(2); + var eagerRefreshThreshold = 0.2f; + + var distributedCache = new MemoryDistributedCache(Options.Create(new MemoryDistributedCacheOptions())); + using var cache = new FusionCache(CreateFusionCacheOptions(), logger: CreateXUnitLogger<FusionCache>()); + cache.SetupDistributedCache(distributedCache, TestsUtils.GetSerializer(serializerType)); + + cache.DefaultEntryOptions.Duration = duration; + cache.DefaultEntryOptions.EagerRefreshThreshold = eagerRefreshThreshold; + + // EXECUTE FACTORY + var v1 = await cache.GetOrSetAsync<long>("foo", async _ => DateTimeOffset.UtcNow.Ticks); + + // USE CACHED VALUE + var v2 = await cache.GetOrSetAsync<long>("foo", async _ => DateTimeOffset.UtcNow.Ticks); + + // WAIT FOR EAGER REFRESH THRESHOLD TO BE HIT + var eagerDuration = TimeSpan.FromMilliseconds(duration.TotalMilliseconds * eagerRefreshThreshold).Add(TimeSpan.FromMilliseconds(10)); + await Task.Delay(eagerDuration); + + // EAGER REFRESH KICKS IN + var v3 = await cache.GetOrSetAsync<long>("foo", async _ => DateTimeOffset.UtcNow.Ticks); + + // WAIT FOR THE BACKGROUND FACTORY (EAGER REFRESH) TO COMPLETE + await Task.Delay(TimeSpan.FromMilliseconds(500)); + + // GET THE REFRESHED VALUE + var v4 = await cache.GetOrSetAsync<long>("foo", async _ => DateTimeOffset.UtcNow.Ticks); + + // WAIT FOR EXPIRATION + await Task.Delay(duration.PlusALittleBit()); + + // EXECUTE FACTORY AGAIN + var v5 = await cache.GetOrSetAsync<long>("foo", async _ => DateTimeOffset.UtcNow.Ticks); + + // USE CACHED VALUE + var v6 = await cache.GetOrSetAsync<long>("foo", async _ => DateTimeOffset.UtcNow.Ticks); + + Assert.Equal(v1, v2); + Assert.Equal(v2, v3); + Assert.True(v4 > v3); + Assert.True(v5 > v4); + Assert.Equal(v5, v6); + } + + [Theory] + [ClassData(typeof(SerializerTypesClassData))] + public void CanHandleEagerRefresh(SerializerType serializerType) + { + var duration = TimeSpan.FromSeconds(2); + var eagerRefreshThreshold = 0.2f; + + var distributedCache = new MemoryDistributedCache(Options.Create(new MemoryDistributedCacheOptions())); + using var cache = new FusionCache(CreateFusionCacheOptions(), logger: CreateXUnitLogger<FusionCache>()); + cache.SetupDistributedCache(distributedCache, TestsUtils.GetSerializer(serializerType)); + + cache.DefaultEntryOptions.Duration = duration; + cache.DefaultEntryOptions.EagerRefreshThreshold = eagerRefreshThreshold; + + // EXECUTE FACTORY + var v1 = cache.GetOrSet<long>("foo", _ => DateTimeOffset.UtcNow.Ticks); + + // USE CACHED VALUE + var v2 = cache.GetOrSet<long>("foo", _ => DateTimeOffset.UtcNow.Ticks); + + // WAIT FOR EAGER REFRESH THRESHOLD TO BE HIT + var eagerDuration = TimeSpan.FromMilliseconds(duration.TotalMilliseconds * eagerRefreshThreshold).Add(TimeSpan.FromMilliseconds(10)); + Thread.Sleep(eagerDuration); + + // EAGER REFRESH KICKS IN + var v3 = cache.GetOrSet<long>("foo", _ => DateTimeOffset.UtcNow.Ticks); + + // WAIT FOR THE BACKGROUND FACTORY (EAGER REFRESH) TO COMPLETE + Thread.Sleep(TimeSpan.FromMilliseconds(500)); + + // GET THE REFRESHED VALUE + var v4 = cache.GetOrSet<long>("foo", _ => DateTimeOffset.UtcNow.Ticks); + + // WAIT FOR EXPIRATION + Thread.Sleep(duration.PlusALittleBit()); + + // EXECUTE FACTORY AGAIN + var v5 = cache.GetOrSet<long>("foo", _ => DateTimeOffset.UtcNow.Ticks); + + // USE CACHED VALUE + var v6 = cache.GetOrSet<long>("foo", _ => DateTimeOffset.UtcNow.Ticks); + + Assert.Equal(v1, v2); + Assert.Equal(v2, v3); + Assert.True(v4 > v3); + Assert.True(v5 > v4); + Assert.Equal(v5, v6); + } + + [Theory] + [ClassData(typeof(SerializerTypesClassData))] + public async Task CanSkipMemoryCacheAsync(SerializerType serializerType) + { + var distributedCache = new MemoryDistributedCache(Options.Create(new MemoryDistributedCacheOptions())); + using var cache1 = new FusionCache(CreateFusionCacheOptions()).SetupDistributedCache(distributedCache, TestsUtils.GetSerializer(serializerType)); + using var cache2 = new FusionCache(CreateFusionCacheOptions()).SetupDistributedCache(distributedCache, TestsUtils.GetSerializer(serializerType)); + + cache1.DefaultEntryOptions.Duration = TimeSpan.FromMinutes(10); + cache2.DefaultEntryOptions.Duration = TimeSpan.FromMinutes(10); + + // SET ON CACHE 1 AND ON DISTRIBUTED CACHE + var v1 = await cache1.GetOrSetAsync<int>("foo", async _ => 10); + + // GET FROM DISTRIBUTED CACHE AND SET IT ON CACHE 2 + var v2 = await cache2.GetOrSetAsync<int>("foo", async _ => 20); + + // SET ON DISTRIBUTED CACHE BUT SKIP CACHE 1 + await cache1.SetAsync<int>("foo", 30, opt => opt.SetSkipMemoryCache()); + + // GET FROM CACHE 1 (10) AND DON'T CALL THE FACTORY + var v3 = await cache1.GetOrSetAsync<int>("foo", async _ => 40); + + // GET FROM CACHE 2 (10) AND DON'T CALL THE FACTORY + var v4 = await cache2.GetOrSetAsync<int>("foo", async _ => 50); + + // SKIP CACHE 2, GET FROM DISTRIBUTED CACHE (30) + var v5 = await cache2.GetOrSetAsync<int>("foo", async _ => 60, opt => opt.SetSkipMemoryCache()); + + Assert.Equal(10, v1); + Assert.Equal(10, v2); + Assert.Equal(10, v3); + Assert.Equal(10, v4); + Assert.Equal(30, v5); + } + + [Theory] + [ClassData(typeof(SerializerTypesClassData))] + public void CanSkipMemoryCache(SerializerType serializerType) + { + var distributedCache = new MemoryDistributedCache(Options.Create(new MemoryDistributedCacheOptions())); + using var cache1 = new FusionCache(CreateFusionCacheOptions()).SetupDistributedCache(distributedCache, TestsUtils.GetSerializer(serializerType)); + using var cache2 = new FusionCache(CreateFusionCacheOptions()).SetupDistributedCache(distributedCache, TestsUtils.GetSerializer(serializerType)); + + cache1.DefaultEntryOptions.Duration = TimeSpan.FromMinutes(10); + cache2.DefaultEntryOptions.Duration = TimeSpan.FromMinutes(10); + + // SET ON CACHE 1 AND ON DISTRIBUTED CACHE + var v1 = cache1.GetOrSet<int>("foo", _ => 10); + + // GET FROM DISTRIBUTED CACHE AND SET IT ON CACHE 2 + var v2 = cache2.GetOrSet<int>("foo", _ => 20); + + // SET ON DISTRIBUTED CACHE BUT SKIP CACHE 1 + cache1.Set<int>("foo", 30, opt => opt.SetSkipMemoryCache()); + + // GET FROM CACHE 1 (10) AND DON'T CALL THE FACTORY + var v3 = cache1.GetOrSet<int>("foo", _ => 40); + + // GET FROM CACHE 2 (10) AND DON'T CALL THE FACTORY + var v4 = cache2.GetOrSet<int>("foo", _ => 50); + + // SKIP CACHE 2, GET FROM DISTRIBUTED CACHE (30) + var v5 = cache2.GetOrSet<int>("foo", _ => 60, opt => opt.SetSkipMemoryCache()); + + Assert.Equal(10, v1); + Assert.Equal(10, v2); + Assert.Equal(10, v3); + Assert.Equal(10, v4); + Assert.Equal(30, v5); + } + + [Theory] + [ClassData(typeof(SerializerTypesClassData))] + public async Task CanExecuteBackgroundDistributedCacheOperationsAsync(SerializerType serializerType) + { + var simulatedDelayMs = TimeSpan.FromMilliseconds(2_000); + var eo = new FusionCacheEntryOptions().SetDurationSec(10); + eo.AllowBackgroundDistributedCacheOperations = true; + + var logger = CreateXUnitLogger<FusionCache>(); + using var memoryCache = new MemoryCache(new MemoryCacheOptions()); + var distributedCache = new MemoryDistributedCache(Options.Create(new MemoryDistributedCacheOptions())); + var chaosDistributedCache = new ChaosDistributedCache(distributedCache, CreateXUnitLogger<ChaosDistributedCache>()); + using var fusionCache = new FusionCache(CreateFusionCacheOptions(), memoryCache, logger); + fusionCache.SetupDistributedCache(chaosDistributedCache, TestsUtils.GetSerializer(serializerType)); + + await fusionCache.SetAsync<int>("foo", 21, eo); + await Task.Delay(TimeSpan.FromSeconds(1).PlusALittleBit()); + chaosDistributedCache.SetAlwaysDelayExactly(simulatedDelayMs); + var sw = Stopwatch.StartNew(); + // SHOULD RETURN IMMEDIATELY + await fusionCache.SetAsync<int>("foo", 42, eo); + sw.Stop(); + logger.Log(LogLevel.Information, "ELAPSED: {ElapsedMilliseconds}ms", sw.ElapsedMilliseconds); + await Task.Delay(TimeSpan.FromMilliseconds(200)); + chaosDistributedCache.SetNeverDelay(); + memoryCache.Remove(TestsUtils.MaybePreProcessCacheKey("foo", TestingCacheKeyPrefix)); + var foo1 = await fusionCache.GetOrDefaultAsync<int>("foo", -1, eo); + await Task.Delay(simulatedDelayMs.PlusALittleBit()); + memoryCache.Remove(TestsUtils.MaybePreProcessCacheKey("foo", TestingCacheKeyPrefix)); + var foo2 = await fusionCache.GetOrDefaultAsync<int>("foo", -1, eo); + + Assert.True(sw.Elapsed < simulatedDelayMs); + Assert.Equal(21, foo1); + Assert.Equal(42, foo2); + } + + [Theory] + [ClassData(typeof(SerializerTypesClassData))] + public void CanExecuteBackgroundDistributedCacheOperations(SerializerType serializerType) + { + var simulatedDelayMs = TimeSpan.FromMilliseconds(2_000); + var eo = new FusionCacheEntryOptions().SetDurationSec(10); + eo.AllowBackgroundDistributedCacheOperations = true; + + var logger = CreateXUnitLogger<FusionCache>(); + using var memoryCache = new MemoryCache(new MemoryCacheOptions()); + var distributedCache = new MemoryDistributedCache(Options.Create(new MemoryDistributedCacheOptions())); + var chaosDistributedCache = new ChaosDistributedCache(distributedCache, CreateXUnitLogger<ChaosDistributedCache>()); + using var fusionCache = new FusionCache(CreateFusionCacheOptions(), memoryCache, logger); + fusionCache.SetupDistributedCache(chaosDistributedCache, TestsUtils.GetSerializer(serializerType)); + + fusionCache.Set<int>("foo", 21, eo); + Thread.Sleep(TimeSpan.FromSeconds(1).PlusALittleBit()); + chaosDistributedCache.SetAlwaysDelayExactly(simulatedDelayMs); + var sw = Stopwatch.StartNew(); + // SHOULD RETURN IMMEDIATELY + fusionCache.Set<int>("foo", 42, eo); + sw.Stop(); + logger.Log(LogLevel.Information, "ELAPSED: {ElapsedMilliseconds}ms", sw.ElapsedMilliseconds); + Thread.Sleep(TimeSpan.FromMilliseconds(200)); + chaosDistributedCache.SetNeverDelay(); + memoryCache.Remove(TestsUtils.MaybePreProcessCacheKey("foo", TestingCacheKeyPrefix)); + var foo1 = fusionCache.GetOrDefault<int>("foo", -1, eo); + Thread.Sleep(simulatedDelayMs.PlusALittleBit()); + memoryCache.Remove(TestsUtils.MaybePreProcessCacheKey("foo", TestingCacheKeyPrefix)); + var foo2 = fusionCache.GetOrDefault<int>("foo", -1, eo); + + Assert.True(sw.Elapsed < simulatedDelayMs); + Assert.Equal(21, foo1); + Assert.Equal(42, foo2); + } +} diff --git a/tests/ZiggyCreatures.FusionCache.Tests/EventsTests.cs b/tests/ZiggyCreatures.FusionCache.Tests/EventsTests.cs index f278a7c4..bfb128db 100644 --- a/tests/ZiggyCreatures.FusionCache.Tests/EventsTests.cs +++ b/tests/ZiggyCreatures.FusionCache.Tests/EventsTests.cs @@ -1,1013 +1,993 @@ using System; -using System.Collections.Concurrent; using System.Linq; using System.Threading; using System.Threading.Tasks; +using FusionCacheTests.Stuff; using Xunit; +using Xunit.Abstractions; using ZiggyCreatures.Caching.Fusion; using ZiggyCreatures.Caching.Fusion.Backplane.Memory; using ZiggyCreatures.Caching.Fusion.Events; -namespace FusionCacheTests +namespace FusionCacheTests; + +public class EventsTests + : AbstractTests { - public class EventsTests + public EventsTests(ITestOutputHelper output) + : base(output, null) { - public enum EntryActionKind - { - Miss = 0, - HitNormal = 1, - HitStale = 2, - Set = 3, - Remove = 4, - FailSafeActivate = 5, - FactoryError = 6, - FactorySuccess = 7, - BackplaneMessagePublished = 8, - BackplaneMessageReceived = 9 - } + } - public class EntryActionsStats - { - public EntryActionsStats() - { - Data = new ConcurrentDictionary<EntryActionKind, int>(); - foreach (EntryActionKind kind in Enum.GetValues(typeof(EntryActionKind))) - { - Data[kind] = 0; - } - } - - public ConcurrentDictionary<EntryActionKind, int> Data { get; } - public void RecordAction(EntryActionKind kind) - { - Data.AddOrUpdate(kind, 1, (_, x) => x + 1); - } - } + [Fact] + public async Task EntryEventsWorkAsync() + { + var stats = new EntryActionsStats(); - [Fact] - public async Task EntryEventsWorkAsync() - { - var stats = new EntryActionsStats(); - - var duration = TimeSpan.FromMilliseconds(100); - var maxDuration = TimeSpan.FromDays(1); - var throttleDuration = TimeSpan.FromMilliseconds(200); - - using (var cache = new FusionCache(new FusionCacheOptions() { EnableSyncEventHandlersExecution = true })) - { - cache.DefaultEntryOptions.Duration = duration; - cache.DefaultEntryOptions.IsFailSafeEnabled = true; - cache.DefaultEntryOptions.FailSafeMaxDuration = maxDuration; - cache.DefaultEntryOptions.FailSafeThrottleDuration = throttleDuration; - - EventHandler<FusionCacheEntryEventArgs> onMiss = (s, e) => stats.RecordAction(EntryActionKind.Miss); - EventHandler<FusionCacheEntryHitEventArgs> onHit = (s, e) => stats.RecordAction(e.IsStale ? EntryActionKind.HitStale : EntryActionKind.HitNormal); - EventHandler<FusionCacheEntryEventArgs> onSet = (s, e) => stats.RecordAction(EntryActionKind.Set); - EventHandler<FusionCacheEntryEventArgs> onRemove = (s, e) => stats.RecordAction(EntryActionKind.Remove); - EventHandler<FusionCacheEntryEventArgs> onFailSafeActivate = (s, e) => stats.RecordAction(EntryActionKind.FailSafeActivate); - EventHandler<FusionCacheEntryEventArgs> onFactoryError = (s, e) => stats.RecordAction(EntryActionKind.FactoryError); - EventHandler<FusionCacheEntryEventArgs> onFactorySuccess = (s, e) => stats.RecordAction(EntryActionKind.FactorySuccess); - - // SETUP HANDLERS - cache.Events.Miss += onMiss; - cache.Events.Hit += onHit; - cache.Events.Set += onSet; - cache.Events.Remove += onRemove; - cache.Events.FailSafeActivate += onFailSafeActivate; - cache.Events.FactoryError += onFactoryError; - cache.Events.FactorySuccess += onFactorySuccess; - - // MISS: +1 - await cache.TryGetAsync<int>("foo"); - - // MISS: +1 - await cache.TryGetAsync<int>("bar"); - - // SET: +1 - await cache.SetAsync<int>("foo", 123); - - // HIT: +1 - await cache.TryGetAsync<int>("foo"); - - // HIT: +1 - await cache.TryGetAsync<int>("foo"); - - await Task.Delay(duration.PlusALittleBit()); - - // HIT (STALE): +1 - // FAIL-SAFE: +1 - // FACTORY ERROR: +1 - _ = await cache.GetOrSetAsync<int>("foo", async _ => throw new Exception("Sloths are cool")); - - // MISS: +1 - await cache.TryGetAsync<int>("bar"); - - // LET THE THROTTLE DURATION PASS - await Task.Delay(throttleDuration.PlusALittleBit()); - - // HIT (STALE): +1 - // FAIL-SAFE: +1 - // FACTORY ERROR: +1 - _ = await cache.GetOrSetAsync<int>("foo", async _ => throw new Exception("Sloths are cool")); - - // REMOVE: +1 - await cache.RemoveAsync("foo"); - - // MISS: +1 - // SET: +1 - // FACTORY SUCCESS: +1 - _ = await cache.GetOrSetAsync<int>("foo", async _ => 123); - - // REMOVE: +1 - await cache.RemoveAsync("bar"); - - //await Task.Delay(TimeSpan.FromSeconds(1)); - - // REMOVE HANDLERS - cache.Events.Miss -= onMiss; - cache.Events.Hit -= onHit; - cache.Events.Set -= onSet; - cache.Events.Remove -= onRemove; - cache.Events.FailSafeActivate -= onFailSafeActivate; - cache.Events.FactoryError -= onFactoryError; - cache.Events.FactorySuccess -= onFactorySuccess; - - Assert.Equal(4, stats.Data[EntryActionKind.Miss]); - Assert.Equal(2, stats.Data[EntryActionKind.HitNormal]); - Assert.Equal(2, stats.Data[EntryActionKind.HitStale]); - Assert.Equal(2, stats.Data[EntryActionKind.Set]); - Assert.Equal(2, stats.Data[EntryActionKind.Remove]); - Assert.Equal(2, stats.Data[EntryActionKind.FailSafeActivate]); - Assert.Equal(2, stats.Data[EntryActionKind.FactoryError]); - Assert.Equal(1, stats.Data[EntryActionKind.FactorySuccess]); - } - } + var duration = TimeSpan.FromMilliseconds(100); + var maxDuration = TimeSpan.FromDays(1); + var throttleDuration = TimeSpan.FromMilliseconds(200); - [Fact] - public void EntryEventsWork() + using (var cache = new FusionCache(new FusionCacheOptions() { EnableSyncEventHandlersExecution = true })) { - var stats = new EntryActionsStats(); - - var duration = TimeSpan.FromMilliseconds(100); - var maxDuration = TimeSpan.FromDays(1); - var throttleDuration = TimeSpan.FromMilliseconds(200); - - using (var cache = new FusionCache(new FusionCacheOptions() { EnableSyncEventHandlersExecution = true })) - { - cache.DefaultEntryOptions.Duration = duration; - cache.DefaultEntryOptions.IsFailSafeEnabled = true; - cache.DefaultEntryOptions.FailSafeMaxDuration = maxDuration; - cache.DefaultEntryOptions.FailSafeThrottleDuration = throttleDuration; - - EventHandler<FusionCacheEntryEventArgs> onMiss = (s, e) => stats.RecordAction(EntryActionKind.Miss); - EventHandler<FusionCacheEntryHitEventArgs> onHit = (s, e) => stats.RecordAction(e.IsStale ? EntryActionKind.HitStale : EntryActionKind.HitNormal); - EventHandler<FusionCacheEntryEventArgs> onSet = (s, e) => stats.RecordAction(EntryActionKind.Set); - EventHandler<FusionCacheEntryEventArgs> onRemove = (s, e) => stats.RecordAction(EntryActionKind.Remove); - EventHandler<FusionCacheEntryEventArgs> onFailSafeActivate = (s, e) => stats.RecordAction(EntryActionKind.FailSafeActivate); - EventHandler<FusionCacheEntryEventArgs> onFactoryError = (s, e) => stats.RecordAction(EntryActionKind.FactoryError); - EventHandler<FusionCacheEntryEventArgs> onFactorySuccess = (s, e) => stats.RecordAction(EntryActionKind.FactorySuccess); - - // SETUP HANDLERS - cache.Events.Miss += onMiss; - cache.Events.Hit += onHit; - cache.Events.Set += onSet; - cache.Events.Remove += onRemove; - cache.Events.FailSafeActivate += onFailSafeActivate; - cache.Events.FactoryError += onFactoryError; - cache.Events.FactorySuccess += onFactorySuccess; - - // MISS: +1 - cache.TryGet<int>("foo"); - - // MISS: +1 - cache.TryGet<int>("bar"); - - // SET: +1 - cache.Set<int>("foo", 123); - - // HIT: +1 - cache.TryGet<int>("foo"); - - // HIT: +1 - cache.TryGet<int>("foo"); - - Thread.Sleep(duration.PlusALittleBit()); - - // HIT (STALE): +1 - // FAIL-SAFE: +1 - // FACTORY ERROR: +1 - _ = cache.GetOrSet<int>("foo", _ => throw new Exception("Sloths are cool")); - - // MISS: +1 - cache.TryGet<int>("bar"); - - // LET THE THROTTLE DURATION PASS - Thread.Sleep(throttleDuration.PlusALittleBit()); - - // HIT (STALE): +1 - // FAIL-SAFE: +1 - // FACTORY ERROR: +1 - _ = cache.GetOrSet<int>("foo", _ => throw new Exception("Sloths are cool")); - - // REMOVE: +1 - cache.Remove("foo"); - - // MISS: +1 - // SET: +1 - // FACTORY SUCCESS: +1 - _ = cache.GetOrSet<int>("foo", _ => 123); - - // REMOVE: +1 - cache.Remove("bar"); - - //Thread.Sleep(TimeSpan.FromSeconds(1)); - - // REMOVE HANDLERS - cache.Events.Miss -= onMiss; - cache.Events.Hit -= onHit; - cache.Events.Set -= onSet; - cache.Events.Remove -= onRemove; - cache.Events.FailSafeActivate -= onFailSafeActivate; - cache.Events.FactoryError -= onFactoryError; - cache.Events.FactorySuccess -= onFactorySuccess; - - Assert.Equal(4, stats.Data[EntryActionKind.Miss]); - Assert.Equal(2, stats.Data[EntryActionKind.HitNormal]); - Assert.Equal(2, stats.Data[EntryActionKind.HitStale]); - Assert.Equal(2, stats.Data[EntryActionKind.Set]); - Assert.Equal(2, stats.Data[EntryActionKind.Remove]); - Assert.Equal(2, stats.Data[EntryActionKind.FailSafeActivate]); - Assert.Equal(2, stats.Data[EntryActionKind.FactoryError]); - Assert.Equal(1, stats.Data[EntryActionKind.FactorySuccess]); - } - } + cache.DefaultEntryOptions.Duration = duration; + cache.DefaultEntryOptions.IsFailSafeEnabled = true; + cache.DefaultEntryOptions.FailSafeMaxDuration = maxDuration; + cache.DefaultEntryOptions.FailSafeThrottleDuration = throttleDuration; + + EventHandler<FusionCacheEntryEventArgs> onMiss = (s, e) => stats.RecordAction(EntryActionKind.Miss); + EventHandler<FusionCacheEntryHitEventArgs> onHit = (s, e) => stats.RecordAction(e.IsStale ? EntryActionKind.HitStale : EntryActionKind.HitNormal); + EventHandler<FusionCacheEntryEventArgs> onSet = (s, e) => stats.RecordAction(EntryActionKind.Set); + EventHandler<FusionCacheEntryEventArgs> onRemove = (s, e) => stats.RecordAction(EntryActionKind.Remove); + EventHandler<FusionCacheEntryEventArgs> onFailSafeActivate = (s, e) => stats.RecordAction(EntryActionKind.FailSafeActivate); + EventHandler<FusionCacheEntryEventArgs> onFactoryError = (s, e) => stats.RecordAction(EntryActionKind.FactoryError); + EventHandler<FusionCacheEntryEventArgs> onFactorySuccess = (s, e) => stats.RecordAction(EntryActionKind.FactorySuccess); - [Fact] - public async Task GetOrSetAsync() - { - var stats = new EntryActionsStats(); - - var duration = TimeSpan.FromSeconds(2); - var maxDuration = TimeSpan.FromDays(1); - var throttleDuration = TimeSpan.FromSeconds(3); - - using (var cache = new FusionCache(new FusionCacheOptions() { EnableSyncEventHandlersExecution = true })) - { - cache.DefaultEntryOptions.Duration = duration; - cache.DefaultEntryOptions.IsFailSafeEnabled = true; - cache.DefaultEntryOptions.FailSafeMaxDuration = maxDuration; - cache.DefaultEntryOptions.FailSafeThrottleDuration = throttleDuration; - - EventHandler<FusionCacheEntryEventArgs> onMiss = (s, e) => stats.RecordAction(EntryActionKind.Miss); - EventHandler<FusionCacheEntryHitEventArgs> onHit = (s, e) => stats.RecordAction(e.IsStale ? EntryActionKind.HitStale : EntryActionKind.HitNormal); - EventHandler<FusionCacheEntryEventArgs> onSet = (s, e) => stats.RecordAction(EntryActionKind.Set); - EventHandler<FusionCacheEntryEventArgs> onRemove = (s, e) => stats.RecordAction(EntryActionKind.Remove); - EventHandler<FusionCacheEntryEventArgs> onFailSafeActivate = (s, e) => stats.RecordAction(EntryActionKind.FailSafeActivate); - - // SETUP HANDLERS - cache.Events.Miss += onMiss; - cache.Events.Hit += onHit; - cache.Events.Set += onSet; - cache.Events.Remove += onRemove; - cache.Events.FailSafeActivate += onFailSafeActivate; - - // MISS: +1 - // SET: +1 - _ = await cache.GetOrSetAsync<int>("foo", async _ => 42); - - // MISS: +1 - // SET: +1 - _ = await cache.GetOrSetAsync<int>("foo2", 42); - - // REMOVE HANDLERS - cache.Events.Miss -= onMiss; - cache.Events.Hit -= onHit; - cache.Events.Set -= onSet; - cache.Events.Remove -= onRemove; - cache.Events.FailSafeActivate -= onFailSafeActivate; - - Assert.Equal(2, stats.Data[EntryActionKind.Miss]); - Assert.Equal(2, stats.Data[EntryActionKind.Set]); - Assert.Equal(4, stats.Data.Values.Sum()); - } - } + // SETUP HANDLERS + cache.Events.Miss += onMiss; + cache.Events.Hit += onHit; + cache.Events.Set += onSet; + cache.Events.Remove += onRemove; + cache.Events.FailSafeActivate += onFailSafeActivate; + cache.Events.FactoryError += onFactoryError; + cache.Events.FactorySuccess += onFactorySuccess; - [Fact] - public void GetOrSet() - { - var stats = new EntryActionsStats(); - - var duration = TimeSpan.FromSeconds(2); - var maxDuration = TimeSpan.FromDays(1); - var throttleDuration = TimeSpan.FromSeconds(3); - - using (var cache = new FusionCache(new FusionCacheOptions() { EnableSyncEventHandlersExecution = true })) - { - cache.DefaultEntryOptions.Duration = duration; - cache.DefaultEntryOptions.IsFailSafeEnabled = true; - cache.DefaultEntryOptions.FailSafeMaxDuration = maxDuration; - cache.DefaultEntryOptions.FailSafeThrottleDuration = throttleDuration; - - EventHandler<FusionCacheEntryEventArgs> onMiss = (s, e) => stats.RecordAction(EntryActionKind.Miss); - EventHandler<FusionCacheEntryHitEventArgs> onHit = (s, e) => stats.RecordAction(e.IsStale ? EntryActionKind.HitStale : EntryActionKind.HitNormal); - EventHandler<FusionCacheEntryEventArgs> onSet = (s, e) => stats.RecordAction(EntryActionKind.Set); - EventHandler<FusionCacheEntryEventArgs> onRemove = (s, e) => stats.RecordAction(EntryActionKind.Remove); - EventHandler<FusionCacheEntryEventArgs> onFailSafeActivate = (s, e) => stats.RecordAction(EntryActionKind.FailSafeActivate); - - // SETUP HANDLERS - cache.Events.Miss += onMiss; - cache.Events.Hit += onHit; - cache.Events.Set += onSet; - cache.Events.Remove += onRemove; - cache.Events.FailSafeActivate += onFailSafeActivate; - - // MISS: +1 - // SET: +1 - cache.GetOrSet<int>("foo", _ => 42); - - // MISS: +1 - // SET: +1 - cache.GetOrSet<int>("foo2", 42); - - // REMOVE HANDLERS - cache.Events.Miss -= onMiss; - cache.Events.Hit -= onHit; - cache.Events.Set -= onSet; - cache.Events.Remove -= onRemove; - cache.Events.FailSafeActivate -= onFailSafeActivate; - - Assert.Equal(2, stats.Data[EntryActionKind.Miss]); - Assert.Equal(2, stats.Data[EntryActionKind.Set]); - Assert.Equal(4, stats.Data.Values.Sum()); - } - } + // MISS: +1 + await cache.TryGetAsync<int>("foo"); - [Fact] - public async Task GetOrSetStaleAsync() - { - var stats = new EntryActionsStats(); - - var duration = TimeSpan.FromMilliseconds(200); - var maxDuration = TimeSpan.FromDays(1); - var throttleDuration = TimeSpan.FromMilliseconds(300); - - using (var cache = new FusionCache(new FusionCacheOptions() { EnableSyncEventHandlersExecution = true })) - { - cache.DefaultEntryOptions.Duration = duration; - cache.DefaultEntryOptions.IsFailSafeEnabled = true; - cache.DefaultEntryOptions.FailSafeMaxDuration = maxDuration; - cache.DefaultEntryOptions.FailSafeThrottleDuration = throttleDuration; - - EventHandler<FusionCacheEntryEventArgs> onMiss = (s, e) => stats.RecordAction(EntryActionKind.Miss); - EventHandler<FusionCacheEntryHitEventArgs> onHit = (s, e) => stats.RecordAction(e.IsStale ? EntryActionKind.HitStale : EntryActionKind.HitNormal); - EventHandler<FusionCacheEntryEventArgs> onSet = (s, e) => stats.RecordAction(EntryActionKind.Set); - EventHandler<FusionCacheEntryEventArgs> onRemove = (s, e) => stats.RecordAction(EntryActionKind.Remove); - EventHandler<FusionCacheEntryEventArgs> onFailSafeActivate = (s, e) => stats.RecordAction(EntryActionKind.FailSafeActivate); - - // INITIAL, NON-TRACKED SET - await cache.SetAsync<int>("foo", 42); - - // SETUP HANDLERS - cache.Events.Miss += onMiss; - cache.Events.Hit += onHit; - cache.Events.Set += onSet; - cache.Events.Remove += onRemove; - cache.Events.FailSafeActivate += onFailSafeActivate; - - // LET IT BECOME STALE - await Task.Delay(duration.PlusALittleBit()); - - // MISS: +1 - // SET: +1 - _ = await cache.GetOrSetAsync<int>("foo", async _ => 42); - - // REMOVE HANDLERS - cache.Events.Miss -= onMiss; - cache.Events.Hit -= onHit; - cache.Events.Set -= onSet; - cache.Events.Remove -= onRemove; - cache.Events.FailSafeActivate -= onFailSafeActivate; - - Assert.Equal(0, stats.Data[EntryActionKind.HitStale]); - Assert.Equal(1, stats.Data[EntryActionKind.Miss]); - Assert.Equal(1, stats.Data[EntryActionKind.Set]); - Assert.Equal(2, stats.Data.Values.Sum()); - } + // MISS: +1 + await cache.TryGetAsync<int>("bar"); + + // SET: +1 + await cache.SetAsync<int>("foo", 123); + + // HIT: +1 + await cache.TryGetAsync<int>("foo"); + + // HIT: +1 + await cache.TryGetAsync<int>("foo"); + + await Task.Delay(duration.PlusALittleBit()); + + // HIT (STALE): +1 + // FAIL-SAFE: +1 + // FACTORY ERROR: +1 + _ = await cache.GetOrSetAsync<int>("foo", async _ => throw new Exception("Sloths are cool")); + + // MISS: +1 + await cache.TryGetAsync<int>("bar"); + + // LET THE THROTTLE DURATION PASS + await Task.Delay(throttleDuration.PlusALittleBit()); + + // HIT (STALE): +1 + // FAIL-SAFE: +1 + // FACTORY ERROR: +1 + _ = await cache.GetOrSetAsync<int>("foo", async _ => throw new Exception("Sloths are cool")); + + // REMOVE: +1 + await cache.RemoveAsync("foo"); + + // MISS: +1 + // SET: +1 + // FACTORY SUCCESS: +1 + _ = await cache.GetOrSetAsync<int>("foo", async _ => 123); + + // REMOVE: +1 + await cache.RemoveAsync("bar"); + + //await Task.Delay(TimeSpan.FromSeconds(1)); + + // REMOVE HANDLERS + cache.Events.Miss -= onMiss; + cache.Events.Hit -= onHit; + cache.Events.Set -= onSet; + cache.Events.Remove -= onRemove; + cache.Events.FailSafeActivate -= onFailSafeActivate; + cache.Events.FactoryError -= onFactoryError; + cache.Events.FactorySuccess -= onFactorySuccess; + + Assert.Equal(4, stats.Data[EntryActionKind.Miss]); + Assert.Equal(2, stats.Data[EntryActionKind.HitNormal]); + Assert.Equal(2, stats.Data[EntryActionKind.HitStale]); + Assert.Equal(2, stats.Data[EntryActionKind.Set]); + Assert.Equal(2, stats.Data[EntryActionKind.Remove]); + Assert.Equal(2, stats.Data[EntryActionKind.FailSafeActivate]); + Assert.Equal(2, stats.Data[EntryActionKind.FactoryError]); + Assert.Equal(1, stats.Data[EntryActionKind.FactorySuccess]); } + } - [Fact] - public void GetOrSetStale() + [Fact] + public void EntryEventsWork() + { + var stats = new EntryActionsStats(); + + var duration = TimeSpan.FromMilliseconds(100); + var maxDuration = TimeSpan.FromDays(1); + var throttleDuration = TimeSpan.FromMilliseconds(200); + + using (var cache = new FusionCache(new FusionCacheOptions() { EnableSyncEventHandlersExecution = true })) { - var stats = new EntryActionsStats(); - - var duration = TimeSpan.FromMilliseconds(200); - var maxDuration = TimeSpan.FromDays(1); - var throttleDuration = TimeSpan.FromMilliseconds(300); - - using (var cache = new FusionCache(new FusionCacheOptions() { EnableSyncEventHandlersExecution = true })) - { - cache.DefaultEntryOptions.Duration = duration; - cache.DefaultEntryOptions.IsFailSafeEnabled = true; - cache.DefaultEntryOptions.FailSafeMaxDuration = maxDuration; - cache.DefaultEntryOptions.FailSafeThrottleDuration = throttleDuration; - - EventHandler<FusionCacheEntryEventArgs> onMiss = (s, e) => stats.RecordAction(EntryActionKind.Miss); - EventHandler<FusionCacheEntryHitEventArgs> onHit = (s, e) => stats.RecordAction(e.IsStale ? EntryActionKind.HitStale : EntryActionKind.HitNormal); - EventHandler<FusionCacheEntryEventArgs> onSet = (s, e) => stats.RecordAction(EntryActionKind.Set); - EventHandler<FusionCacheEntryEventArgs> onRemove = (s, e) => stats.RecordAction(EntryActionKind.Remove); - EventHandler<FusionCacheEntryEventArgs> onFailSafeActivate = (s, e) => stats.RecordAction(EntryActionKind.FailSafeActivate); - - // INITIAL, NON-TRACKED SET - cache.Set<int>("foo", 42); - - // SETUP HANDLERS - cache.Events.Miss += onMiss; - cache.Events.Hit += onHit; - cache.Events.Set += onSet; - cache.Events.Remove += onRemove; - cache.Events.FailSafeActivate += onFailSafeActivate; - - // LET IT BECOME STALE - Thread.Sleep(duration.PlusALittleBit()); - - // MISS: +1 - // SET: +1 - cache.GetOrSet<int>("foo", _ => 42); - - // REMOVE HANDLERS - cache.Events.Miss -= onMiss; - cache.Events.Hit -= onHit; - cache.Events.Set -= onSet; - cache.Events.Remove -= onRemove; - cache.Events.FailSafeActivate -= onFailSafeActivate; - - Assert.Equal(0, stats.Data[EntryActionKind.HitStale]); - Assert.Equal(1, stats.Data[EntryActionKind.Miss]); - Assert.Equal(1, stats.Data[EntryActionKind.Set]); - Assert.Equal(2, stats.Data.Values.Sum()); - } + cache.DefaultEntryOptions.Duration = duration; + cache.DefaultEntryOptions.IsFailSafeEnabled = true; + cache.DefaultEntryOptions.FailSafeMaxDuration = maxDuration; + cache.DefaultEntryOptions.FailSafeThrottleDuration = throttleDuration; + + EventHandler<FusionCacheEntryEventArgs> onMiss = (s, e) => stats.RecordAction(EntryActionKind.Miss); + EventHandler<FusionCacheEntryHitEventArgs> onHit = (s, e) => stats.RecordAction(e.IsStale ? EntryActionKind.HitStale : EntryActionKind.HitNormal); + EventHandler<FusionCacheEntryEventArgs> onSet = (s, e) => stats.RecordAction(EntryActionKind.Set); + EventHandler<FusionCacheEntryEventArgs> onRemove = (s, e) => stats.RecordAction(EntryActionKind.Remove); + EventHandler<FusionCacheEntryEventArgs> onFailSafeActivate = (s, e) => stats.RecordAction(EntryActionKind.FailSafeActivate); + EventHandler<FusionCacheEntryEventArgs> onFactoryError = (s, e) => stats.RecordAction(EntryActionKind.FactoryError); + EventHandler<FusionCacheEntryEventArgs> onFactorySuccess = (s, e) => stats.RecordAction(EntryActionKind.FactorySuccess); + + // SETUP HANDLERS + cache.Events.Miss += onMiss; + cache.Events.Hit += onHit; + cache.Events.Set += onSet; + cache.Events.Remove += onRemove; + cache.Events.FailSafeActivate += onFailSafeActivate; + cache.Events.FactoryError += onFactoryError; + cache.Events.FactorySuccess += onFactorySuccess; + + // MISS: +1 + cache.TryGet<int>("foo"); + + // MISS: +1 + cache.TryGet<int>("bar"); + + // SET: +1 + cache.Set<int>("foo", 123); + + // HIT: +1 + cache.TryGet<int>("foo"); + + // HIT: +1 + cache.TryGet<int>("foo"); + + Thread.Sleep(duration.PlusALittleBit()); + + // HIT (STALE): +1 + // FAIL-SAFE: +1 + // FACTORY ERROR: +1 + _ = cache.GetOrSet<int>("foo", _ => throw new Exception("Sloths are cool")); + + // MISS: +1 + cache.TryGet<int>("bar"); + + // LET THE THROTTLE DURATION PASS + Thread.Sleep(throttleDuration.PlusALittleBit()); + + // HIT (STALE): +1 + // FAIL-SAFE: +1 + // FACTORY ERROR: +1 + _ = cache.GetOrSet<int>("foo", _ => throw new Exception("Sloths are cool")); + + // REMOVE: +1 + cache.Remove("foo"); + + // MISS: +1 + // SET: +1 + // FACTORY SUCCESS: +1 + _ = cache.GetOrSet<int>("foo", _ => 123); + + // REMOVE: +1 + cache.Remove("bar"); + + //Thread.Sleep(TimeSpan.FromSeconds(1)); + + // REMOVE HANDLERS + cache.Events.Miss -= onMiss; + cache.Events.Hit -= onHit; + cache.Events.Set -= onSet; + cache.Events.Remove -= onRemove; + cache.Events.FailSafeActivate -= onFailSafeActivate; + cache.Events.FactoryError -= onFactoryError; + cache.Events.FactorySuccess -= onFactorySuccess; + + Assert.Equal(4, stats.Data[EntryActionKind.Miss]); + Assert.Equal(2, stats.Data[EntryActionKind.HitNormal]); + Assert.Equal(2, stats.Data[EntryActionKind.HitStale]); + Assert.Equal(2, stats.Data[EntryActionKind.Set]); + Assert.Equal(2, stats.Data[EntryActionKind.Remove]); + Assert.Equal(2, stats.Data[EntryActionKind.FailSafeActivate]); + Assert.Equal(2, stats.Data[EntryActionKind.FactoryError]); + Assert.Equal(1, stats.Data[EntryActionKind.FactorySuccess]); } + } - [Fact] - public async Task TryGetAsync() + [Fact] + public async Task GetOrSetAsync() + { + var stats = new EntryActionsStats(); + + var duration = TimeSpan.FromSeconds(2); + var maxDuration = TimeSpan.FromDays(1); + var throttleDuration = TimeSpan.FromSeconds(3); + + using (var cache = new FusionCache(new FusionCacheOptions() { EnableSyncEventHandlersExecution = true })) { - var stats = new EntryActionsStats(); - - var duration = TimeSpan.FromSeconds(2); - var maxDuration = TimeSpan.FromDays(1); - var throttleDuration = TimeSpan.FromSeconds(3); - - using (var cache = new FusionCache(new FusionCacheOptions() { EnableSyncEventHandlersExecution = true })) - { - cache.DefaultEntryOptions.Duration = duration; - cache.DefaultEntryOptions.IsFailSafeEnabled = true; - cache.DefaultEntryOptions.FailSafeMaxDuration = maxDuration; - cache.DefaultEntryOptions.FailSafeThrottleDuration = throttleDuration; - - EventHandler<FusionCacheEntryEventArgs> onMiss = (s, e) => stats.RecordAction(EntryActionKind.Miss); - EventHandler<FusionCacheEntryHitEventArgs> onHit = (s, e) => stats.RecordAction(e.IsStale ? EntryActionKind.HitStale : EntryActionKind.HitNormal); - EventHandler<FusionCacheEntryEventArgs> onSet = (s, e) => stats.RecordAction(EntryActionKind.Set); - EventHandler<FusionCacheEntryEventArgs> onRemove = (s, e) => stats.RecordAction(EntryActionKind.Remove); - EventHandler<FusionCacheEntryEventArgs> onFailSafeActivate = (s, e) => stats.RecordAction(EntryActionKind.FailSafeActivate); - - // SETUP HANDLERS - cache.Events.Miss += onMiss; - cache.Events.Hit += onHit; - cache.Events.Set += onSet; - cache.Events.Remove += onRemove; - cache.Events.FailSafeActivate += onFailSafeActivate; - - // MISS: +1 - _ = await cache.TryGetAsync<int>("foo"); - - // REMOVE HANDLERS - cache.Events.Miss -= onMiss; - cache.Events.Hit -= onHit; - cache.Events.Set -= onSet; - cache.Events.Remove -= onRemove; - cache.Events.FailSafeActivate -= onFailSafeActivate; - - Assert.Equal(1, stats.Data[EntryActionKind.Miss]); - Assert.Equal(1, stats.Data.Values.Sum()); - } + cache.DefaultEntryOptions.Duration = duration; + cache.DefaultEntryOptions.IsFailSafeEnabled = true; + cache.DefaultEntryOptions.FailSafeMaxDuration = maxDuration; + cache.DefaultEntryOptions.FailSafeThrottleDuration = throttleDuration; + + EventHandler<FusionCacheEntryEventArgs> onMiss = (s, e) => stats.RecordAction(EntryActionKind.Miss); + EventHandler<FusionCacheEntryHitEventArgs> onHit = (s, e) => stats.RecordAction(e.IsStale ? EntryActionKind.HitStale : EntryActionKind.HitNormal); + EventHandler<FusionCacheEntryEventArgs> onSet = (s, e) => stats.RecordAction(EntryActionKind.Set); + EventHandler<FusionCacheEntryEventArgs> onRemove = (s, e) => stats.RecordAction(EntryActionKind.Remove); + EventHandler<FusionCacheEntryEventArgs> onFailSafeActivate = (s, e) => stats.RecordAction(EntryActionKind.FailSafeActivate); + + // SETUP HANDLERS + cache.Events.Miss += onMiss; + cache.Events.Hit += onHit; + cache.Events.Set += onSet; + cache.Events.Remove += onRemove; + cache.Events.FailSafeActivate += onFailSafeActivate; + + // MISS: +1 + // SET: +1 + _ = await cache.GetOrSetAsync<int>("foo", async _ => 42); + + // MISS: +1 + // SET: +1 + _ = await cache.GetOrSetAsync<int>("foo2", 42); + + // REMOVE HANDLERS + cache.Events.Miss -= onMiss; + cache.Events.Hit -= onHit; + cache.Events.Set -= onSet; + cache.Events.Remove -= onRemove; + cache.Events.FailSafeActivate -= onFailSafeActivate; + + Assert.Equal(2, stats.Data[EntryActionKind.Miss]); + Assert.Equal(2, stats.Data[EntryActionKind.Set]); + Assert.Equal(4, stats.Data.Values.Sum()); } + } + + [Fact] + public void GetOrSet() + { + var stats = new EntryActionsStats(); - [Fact] - public void TryGet() + var duration = TimeSpan.FromSeconds(2); + var maxDuration = TimeSpan.FromDays(1); + var throttleDuration = TimeSpan.FromSeconds(3); + + using (var cache = new FusionCache(new FusionCacheOptions() { EnableSyncEventHandlersExecution = true })) { - var stats = new EntryActionsStats(); - - var duration = TimeSpan.FromSeconds(2); - var maxDuration = TimeSpan.FromDays(1); - var throttleDuration = TimeSpan.FromSeconds(3); - - using (var cache = new FusionCache(new FusionCacheOptions() { EnableSyncEventHandlersExecution = true })) - { - cache.DefaultEntryOptions.Duration = duration; - cache.DefaultEntryOptions.IsFailSafeEnabled = true; - cache.DefaultEntryOptions.FailSafeMaxDuration = maxDuration; - cache.DefaultEntryOptions.FailSafeThrottleDuration = throttleDuration; - - EventHandler<FusionCacheEntryEventArgs> onMiss = (s, e) => stats.RecordAction(EntryActionKind.Miss); - EventHandler<FusionCacheEntryHitEventArgs> onHit = (s, e) => stats.RecordAction(e.IsStale ? EntryActionKind.HitStale : EntryActionKind.HitNormal); - EventHandler<FusionCacheEntryEventArgs> onSet = (s, e) => stats.RecordAction(EntryActionKind.Set); - EventHandler<FusionCacheEntryEventArgs> onRemove = (s, e) => stats.RecordAction(EntryActionKind.Remove); - EventHandler<FusionCacheEntryEventArgs> onFailSafeActivate = (s, e) => stats.RecordAction(EntryActionKind.FailSafeActivate); - - // SETUP HANDLERS - cache.Events.Miss += onMiss; - cache.Events.Hit += onHit; - cache.Events.Set += onSet; - cache.Events.Remove += onRemove; - cache.Events.FailSafeActivate += onFailSafeActivate; - - // MISS: +1 - cache.TryGet<int>("foo"); - - // REMOVE HANDLERS - cache.Events.Miss -= onMiss; - cache.Events.Hit -= onHit; - cache.Events.Set -= onSet; - cache.Events.Remove -= onRemove; - cache.Events.FailSafeActivate -= onFailSafeActivate; - - Assert.Equal(1, stats.Data[EntryActionKind.Miss]); - Assert.Equal(1, stats.Data.Values.Sum()); - } + cache.DefaultEntryOptions.Duration = duration; + cache.DefaultEntryOptions.IsFailSafeEnabled = true; + cache.DefaultEntryOptions.FailSafeMaxDuration = maxDuration; + cache.DefaultEntryOptions.FailSafeThrottleDuration = throttleDuration; + + EventHandler<FusionCacheEntryEventArgs> onMiss = (s, e) => stats.RecordAction(EntryActionKind.Miss); + EventHandler<FusionCacheEntryHitEventArgs> onHit = (s, e) => stats.RecordAction(e.IsStale ? EntryActionKind.HitStale : EntryActionKind.HitNormal); + EventHandler<FusionCacheEntryEventArgs> onSet = (s, e) => stats.RecordAction(EntryActionKind.Set); + EventHandler<FusionCacheEntryEventArgs> onRemove = (s, e) => stats.RecordAction(EntryActionKind.Remove); + EventHandler<FusionCacheEntryEventArgs> onFailSafeActivate = (s, e) => stats.RecordAction(EntryActionKind.FailSafeActivate); + + // SETUP HANDLERS + cache.Events.Miss += onMiss; + cache.Events.Hit += onHit; + cache.Events.Set += onSet; + cache.Events.Remove += onRemove; + cache.Events.FailSafeActivate += onFailSafeActivate; + + // MISS: +1 + // SET: +1 + cache.GetOrSet<int>("foo", _ => 42); + + // MISS: +1 + // SET: +1 + cache.GetOrSet<int>("foo2", 42); + + // REMOVE HANDLERS + cache.Events.Miss -= onMiss; + cache.Events.Hit -= onHit; + cache.Events.Set -= onSet; + cache.Events.Remove -= onRemove; + cache.Events.FailSafeActivate -= onFailSafeActivate; + + Assert.Equal(2, stats.Data[EntryActionKind.Miss]); + Assert.Equal(2, stats.Data[EntryActionKind.Set]); + Assert.Equal(4, stats.Data.Values.Sum()); } + } - [Fact] - public async Task TryGetStaleFailSafeAsync() + [Fact] + public async Task GetOrSetStaleAsync() + { + var stats = new EntryActionsStats(); + + var duration = TimeSpan.FromMilliseconds(200); + var maxDuration = TimeSpan.FromDays(1); + var throttleDuration = TimeSpan.FromMilliseconds(300); + + using (var cache = new FusionCache(new FusionCacheOptions() { EnableSyncEventHandlersExecution = true })) { - var stats = new EntryActionsStats(); - - var duration = TimeSpan.FromMilliseconds(200); - var maxDuration = TimeSpan.FromDays(1); - var throttleDuration = TimeSpan.FromMilliseconds(300); - - using (var cache = new FusionCache(new FusionCacheOptions() { EnableSyncEventHandlersExecution = true })) - { - cache.DefaultEntryOptions.Duration = duration; - cache.DefaultEntryOptions.IsFailSafeEnabled = true; - cache.DefaultEntryOptions.FailSafeMaxDuration = maxDuration; - cache.DefaultEntryOptions.FailSafeThrottleDuration = throttleDuration; - - EventHandler<FusionCacheEntryEventArgs> onMiss = (s, e) => stats.RecordAction(EntryActionKind.Miss); - EventHandler<FusionCacheEntryHitEventArgs> onHit = (s, e) => stats.RecordAction(e.IsStale ? EntryActionKind.HitStale : EntryActionKind.HitNormal); - EventHandler<FusionCacheEntryEventArgs> onSet = (s, e) => stats.RecordAction(EntryActionKind.Set); - EventHandler<FusionCacheEntryEventArgs> onRemove = (s, e) => stats.RecordAction(EntryActionKind.Remove); - EventHandler<FusionCacheEntryEventArgs> onFailSafeActivate = (s, e) => stats.RecordAction(EntryActionKind.FailSafeActivate); - - // INITIAL, NON-TRACKED SET - await cache.SetAsync<int>("foo", 42); - - // SETUP HANDLERS - cache.Events.Miss += onMiss; - cache.Events.Hit += onHit; - cache.Events.Set += onSet; - cache.Events.Remove += onRemove; - cache.Events.FailSafeActivate += onFailSafeActivate; - - // LET IT BECOME STALE - await Task.Delay(duration.PlusALittleBit()); - - // HIT (STALE): +1 - _ = await cache.TryGetAsync<int>("foo"); - - // REMOVE HANDLERS - cache.Events.Miss -= onMiss; - cache.Events.Hit -= onHit; - cache.Events.Set -= onSet; - cache.Events.Remove -= onRemove; - cache.Events.FailSafeActivate -= onFailSafeActivate; - - Assert.Equal(1, stats.Data[EntryActionKind.HitStale]); - Assert.Equal(1, stats.Data.Values.Sum()); - } + cache.DefaultEntryOptions.Duration = duration; + cache.DefaultEntryOptions.IsFailSafeEnabled = true; + cache.DefaultEntryOptions.FailSafeMaxDuration = maxDuration; + cache.DefaultEntryOptions.FailSafeThrottleDuration = throttleDuration; + + EventHandler<FusionCacheEntryEventArgs> onMiss = (s, e) => stats.RecordAction(EntryActionKind.Miss); + EventHandler<FusionCacheEntryHitEventArgs> onHit = (s, e) => stats.RecordAction(e.IsStale ? EntryActionKind.HitStale : EntryActionKind.HitNormal); + EventHandler<FusionCacheEntryEventArgs> onSet = (s, e) => stats.RecordAction(EntryActionKind.Set); + EventHandler<FusionCacheEntryEventArgs> onRemove = (s, e) => stats.RecordAction(EntryActionKind.Remove); + EventHandler<FusionCacheEntryEventArgs> onFailSafeActivate = (s, e) => stats.RecordAction(EntryActionKind.FailSafeActivate); + + // INITIAL, NON-TRACKED SET + await cache.SetAsync<int>("foo", 42); + + // SETUP HANDLERS + cache.Events.Miss += onMiss; + cache.Events.Hit += onHit; + cache.Events.Set += onSet; + cache.Events.Remove += onRemove; + cache.Events.FailSafeActivate += onFailSafeActivate; + + // LET IT BECOME STALE + await Task.Delay(duration.PlusALittleBit()); + + // MISS: +1 + // SET: +1 + _ = await cache.GetOrSetAsync<int>("foo", async _ => 42); + + // REMOVE HANDLERS + cache.Events.Miss -= onMiss; + cache.Events.Hit -= onHit; + cache.Events.Set -= onSet; + cache.Events.Remove -= onRemove; + cache.Events.FailSafeActivate -= onFailSafeActivate; + + Assert.Equal(0, stats.Data[EntryActionKind.HitStale]); + Assert.Equal(1, stats.Data[EntryActionKind.Miss]); + Assert.Equal(1, stats.Data[EntryActionKind.Set]); + Assert.Equal(2, stats.Data.Values.Sum()); } + } - [Fact] - public void TryGetStaleFailSafe() + [Fact] + public void GetOrSetStale() + { + var stats = new EntryActionsStats(); + + var duration = TimeSpan.FromMilliseconds(200); + var maxDuration = TimeSpan.FromDays(1); + var throttleDuration = TimeSpan.FromMilliseconds(300); + + using (var cache = new FusionCache(new FusionCacheOptions() { EnableSyncEventHandlersExecution = true })) { - var stats = new EntryActionsStats(); - - var duration = TimeSpan.FromMilliseconds(200); - var maxDuration = TimeSpan.FromDays(1); - var throttleDuration = TimeSpan.FromMilliseconds(300); - - using (var cache = new FusionCache(new FusionCacheOptions() { EnableSyncEventHandlersExecution = true })) - { - cache.DefaultEntryOptions.Duration = duration; - cache.DefaultEntryOptions.IsFailSafeEnabled = true; - cache.DefaultEntryOptions.FailSafeMaxDuration = maxDuration; - cache.DefaultEntryOptions.FailSafeThrottleDuration = throttleDuration; - - EventHandler<FusionCacheEntryEventArgs> onMiss = (s, e) => stats.RecordAction(EntryActionKind.Miss); - EventHandler<FusionCacheEntryHitEventArgs> onHit = (s, e) => stats.RecordAction(e.IsStale ? EntryActionKind.HitStale : EntryActionKind.HitNormal); - EventHandler<FusionCacheEntryEventArgs> onSet = (s, e) => stats.RecordAction(EntryActionKind.Set); - EventHandler<FusionCacheEntryEventArgs> onRemove = (s, e) => stats.RecordAction(EntryActionKind.Remove); - EventHandler<FusionCacheEntryEventArgs> onFailSafeActivate = (s, e) => stats.RecordAction(EntryActionKind.FailSafeActivate); - - // INITIAL, NON-TRACKED SET - cache.Set<int>("foo", 42); - - // SETUP HANDLERS - cache.Events.Miss += onMiss; - cache.Events.Hit += onHit; - cache.Events.Set += onSet; - cache.Events.Remove += onRemove; - cache.Events.FailSafeActivate += onFailSafeActivate; - - // LET IT BECOME STALE - Thread.Sleep(duration.PlusALittleBit()); - - // HIT (STALE): +1 - cache.TryGet<int>("foo"); - - // REMOVE HANDLERS - cache.Events.Miss -= onMiss; - cache.Events.Hit -= onHit; - cache.Events.Set -= onSet; - cache.Events.Remove -= onRemove; - cache.Events.FailSafeActivate -= onFailSafeActivate; - - Assert.Equal(1, stats.Data[EntryActionKind.HitStale]); - Assert.Equal(1, stats.Data.Values.Sum()); - } + cache.DefaultEntryOptions.Duration = duration; + cache.DefaultEntryOptions.IsFailSafeEnabled = true; + cache.DefaultEntryOptions.FailSafeMaxDuration = maxDuration; + cache.DefaultEntryOptions.FailSafeThrottleDuration = throttleDuration; + + EventHandler<FusionCacheEntryEventArgs> onMiss = (s, e) => stats.RecordAction(EntryActionKind.Miss); + EventHandler<FusionCacheEntryHitEventArgs> onHit = (s, e) => stats.RecordAction(e.IsStale ? EntryActionKind.HitStale : EntryActionKind.HitNormal); + EventHandler<FusionCacheEntryEventArgs> onSet = (s, e) => stats.RecordAction(EntryActionKind.Set); + EventHandler<FusionCacheEntryEventArgs> onRemove = (s, e) => stats.RecordAction(EntryActionKind.Remove); + EventHandler<FusionCacheEntryEventArgs> onFailSafeActivate = (s, e) => stats.RecordAction(EntryActionKind.FailSafeActivate); + + // INITIAL, NON-TRACKED SET + cache.Set<int>("foo", 42); + + // SETUP HANDLERS + cache.Events.Miss += onMiss; + cache.Events.Hit += onHit; + cache.Events.Set += onSet; + cache.Events.Remove += onRemove; + cache.Events.FailSafeActivate += onFailSafeActivate; + + // LET IT BECOME STALE + Thread.Sleep(duration.PlusALittleBit()); + + // MISS: +1 + // SET: +1 + cache.GetOrSet<int>("foo", _ => 42); + + // REMOVE HANDLERS + cache.Events.Miss -= onMiss; + cache.Events.Hit -= onHit; + cache.Events.Set -= onSet; + cache.Events.Remove -= onRemove; + cache.Events.FailSafeActivate -= onFailSafeActivate; + + Assert.Equal(0, stats.Data[EntryActionKind.HitStale]); + Assert.Equal(1, stats.Data[EntryActionKind.Miss]); + Assert.Equal(1, stats.Data[EntryActionKind.Set]); + Assert.Equal(2, stats.Data.Values.Sum()); } + } - [Fact] - public async Task TryGetStaleNoFailSafeAsync() + [Fact] + public async Task TryGetAsync() + { + var stats = new EntryActionsStats(); + + var duration = TimeSpan.FromSeconds(2); + var maxDuration = TimeSpan.FromDays(1); + var throttleDuration = TimeSpan.FromSeconds(3); + + using (var cache = new FusionCache(new FusionCacheOptions() { EnableSyncEventHandlersExecution = true })) { - var stats = new EntryActionsStats(); - - var duration = TimeSpan.FromMilliseconds(200); - var maxDuration = TimeSpan.FromDays(1); - var throttleDuration = TimeSpan.FromMilliseconds(300); - - using (var cache = new FusionCache(new FusionCacheOptions() { EnableSyncEventHandlersExecution = true })) - { - cache.DefaultEntryOptions.Duration = duration; - cache.DefaultEntryOptions.IsFailSafeEnabled = true; - cache.DefaultEntryOptions.FailSafeMaxDuration = maxDuration; - cache.DefaultEntryOptions.FailSafeThrottleDuration = throttleDuration; - - EventHandler<FusionCacheEntryEventArgs> onMiss = (s, e) => stats.RecordAction(EntryActionKind.Miss); - EventHandler<FusionCacheEntryHitEventArgs> onHit = (s, e) => stats.RecordAction(e.IsStale ? EntryActionKind.HitStale : EntryActionKind.HitNormal); - EventHandler<FusionCacheEntryEventArgs> onSet = (s, e) => stats.RecordAction(EntryActionKind.Set); - EventHandler<FusionCacheEntryEventArgs> onRemove = (s, e) => stats.RecordAction(EntryActionKind.Remove); - EventHandler<FusionCacheEntryEventArgs> onFailSafeActivate = (s, e) => stats.RecordAction(EntryActionKind.FailSafeActivate); - - // INITIAL, NON-TRACKED SET - await cache.SetAsync<int>("foo", 42); - - // SETUP HANDLERS - cache.Events.Miss += onMiss; - cache.Events.Hit += onHit; - cache.Events.Set += onSet; - cache.Events.Remove += onRemove; - cache.Events.FailSafeActivate += onFailSafeActivate; - - // LET IT BECOME STALE - await Task.Delay(duration.PlusALittleBit()); - - // MISS: +1 - _ = await cache.TryGetAsync<int>("foo", options => options.SetFailSafe(false)); - - // REMOVE HANDLERS - cache.Events.Miss -= onMiss; - cache.Events.Hit -= onHit; - cache.Events.Set -= onSet; - cache.Events.Remove -= onRemove; - cache.Events.FailSafeActivate -= onFailSafeActivate; - - Assert.Equal(1, stats.Data[EntryActionKind.Miss]); - Assert.Equal(1, stats.Data.Values.Sum()); - } + cache.DefaultEntryOptions.Duration = duration; + cache.DefaultEntryOptions.IsFailSafeEnabled = true; + cache.DefaultEntryOptions.FailSafeMaxDuration = maxDuration; + cache.DefaultEntryOptions.FailSafeThrottleDuration = throttleDuration; + + EventHandler<FusionCacheEntryEventArgs> onMiss = (s, e) => stats.RecordAction(EntryActionKind.Miss); + EventHandler<FusionCacheEntryHitEventArgs> onHit = (s, e) => stats.RecordAction(e.IsStale ? EntryActionKind.HitStale : EntryActionKind.HitNormal); + EventHandler<FusionCacheEntryEventArgs> onSet = (s, e) => stats.RecordAction(EntryActionKind.Set); + EventHandler<FusionCacheEntryEventArgs> onRemove = (s, e) => stats.RecordAction(EntryActionKind.Remove); + EventHandler<FusionCacheEntryEventArgs> onFailSafeActivate = (s, e) => stats.RecordAction(EntryActionKind.FailSafeActivate); + + // SETUP HANDLERS + cache.Events.Miss += onMiss; + cache.Events.Hit += onHit; + cache.Events.Set += onSet; + cache.Events.Remove += onRemove; + cache.Events.FailSafeActivate += onFailSafeActivate; + + // MISS: +1 + _ = await cache.TryGetAsync<int>("foo"); + + // REMOVE HANDLERS + cache.Events.Miss -= onMiss; + cache.Events.Hit -= onHit; + cache.Events.Set -= onSet; + cache.Events.Remove -= onRemove; + cache.Events.FailSafeActivate -= onFailSafeActivate; + + Assert.Equal(1, stats.Data[EntryActionKind.Miss]); + Assert.Equal(1, stats.Data.Values.Sum()); } + } + + [Fact] + public void TryGet() + { + var stats = new EntryActionsStats(); + + var duration = TimeSpan.FromSeconds(2); + var maxDuration = TimeSpan.FromDays(1); + var throttleDuration = TimeSpan.FromSeconds(3); - [Fact] - public void TryGetStaleNoFailSafe() + using (var cache = new FusionCache(new FusionCacheOptions() { EnableSyncEventHandlersExecution = true })) { - var stats = new EntryActionsStats(); - - var duration = TimeSpan.FromMilliseconds(200); - var maxDuration = TimeSpan.FromDays(1); - var throttleDuration = TimeSpan.FromMilliseconds(300); - - using (var cache = new FusionCache(new FusionCacheOptions() { EnableSyncEventHandlersExecution = true })) - { - cache.DefaultEntryOptions.Duration = duration; - cache.DefaultEntryOptions.IsFailSafeEnabled = true; - cache.DefaultEntryOptions.FailSafeMaxDuration = maxDuration; - cache.DefaultEntryOptions.FailSafeThrottleDuration = throttleDuration; - - EventHandler<FusionCacheEntryEventArgs> onMiss = (s, e) => stats.RecordAction(EntryActionKind.Miss); - EventHandler<FusionCacheEntryHitEventArgs> onHit = (s, e) => stats.RecordAction(e.IsStale ? EntryActionKind.HitStale : EntryActionKind.HitNormal); - EventHandler<FusionCacheEntryEventArgs> onSet = (s, e) => stats.RecordAction(EntryActionKind.Set); - EventHandler<FusionCacheEntryEventArgs> onRemove = (s, e) => stats.RecordAction(EntryActionKind.Remove); - EventHandler<FusionCacheEntryEventArgs> onFailSafeActivate = (s, e) => stats.RecordAction(EntryActionKind.FailSafeActivate); - - // INITIAL, NON-TRACKED SET - cache.Set<int>("foo", 42); - - // SETUP HANDLERS - cache.Events.Miss += onMiss; - cache.Events.Hit += onHit; - cache.Events.Set += onSet; - cache.Events.Remove += onRemove; - cache.Events.FailSafeActivate += onFailSafeActivate; - - // LET IT BECOME STALE - Thread.Sleep(duration.PlusALittleBit()); - - // MISS: +1 - cache.TryGet<int>("foo", options => options.SetFailSafe(false)); - - // REMOVE HANDLERS - cache.Events.Miss -= onMiss; - cache.Events.Hit -= onHit; - cache.Events.Set -= onSet; - cache.Events.Remove -= onRemove; - cache.Events.FailSafeActivate -= onFailSafeActivate; - - Assert.Equal(1, stats.Data[EntryActionKind.Miss]); - Assert.Equal(1, stats.Data.Values.Sum()); - } + cache.DefaultEntryOptions.Duration = duration; + cache.DefaultEntryOptions.IsFailSafeEnabled = true; + cache.DefaultEntryOptions.FailSafeMaxDuration = maxDuration; + cache.DefaultEntryOptions.FailSafeThrottleDuration = throttleDuration; + + EventHandler<FusionCacheEntryEventArgs> onMiss = (s, e) => stats.RecordAction(EntryActionKind.Miss); + EventHandler<FusionCacheEntryHitEventArgs> onHit = (s, e) => stats.RecordAction(e.IsStale ? EntryActionKind.HitStale : EntryActionKind.HitNormal); + EventHandler<FusionCacheEntryEventArgs> onSet = (s, e) => stats.RecordAction(EntryActionKind.Set); + EventHandler<FusionCacheEntryEventArgs> onRemove = (s, e) => stats.RecordAction(EntryActionKind.Remove); + EventHandler<FusionCacheEntryEventArgs> onFailSafeActivate = (s, e) => stats.RecordAction(EntryActionKind.FailSafeActivate); + + // SETUP HANDLERS + cache.Events.Miss += onMiss; + cache.Events.Hit += onHit; + cache.Events.Set += onSet; + cache.Events.Remove += onRemove; + cache.Events.FailSafeActivate += onFailSafeActivate; + + // MISS: +1 + cache.TryGet<int>("foo"); + + // REMOVE HANDLERS + cache.Events.Miss -= onMiss; + cache.Events.Hit -= onHit; + cache.Events.Set -= onSet; + cache.Events.Remove -= onRemove; + cache.Events.FailSafeActivate -= onFailSafeActivate; + + Assert.Equal(1, stats.Data[EntryActionKind.Miss]); + Assert.Equal(1, stats.Data.Values.Sum()); } + } + + [Fact] + public async Task TryGetStaleFailSafeAsync() + { + var stats = new EntryActionsStats(); - [Fact] - public async Task MemoryLayerEventsAsync() + var duration = TimeSpan.FromMilliseconds(200); + var maxDuration = TimeSpan.FromDays(1); + var throttleDuration = TimeSpan.FromMilliseconds(300); + + using (var cache = new FusionCache(new FusionCacheOptions() { EnableSyncEventHandlersExecution = true })) { - var stats = new EntryActionsStats(); - - var duration = TimeSpan.FromSeconds(2); - var maxDuration = TimeSpan.FromDays(1); - var throttleDuration = TimeSpan.FromSeconds(3); - - using (var cache = new FusionCache(new FusionCacheOptions() { EnableSyncEventHandlersExecution = true })) - { - cache.DefaultEntryOptions.Duration = duration; - cache.DefaultEntryOptions.IsFailSafeEnabled = true; - cache.DefaultEntryOptions.FailSafeMaxDuration = maxDuration; - cache.DefaultEntryOptions.FailSafeThrottleDuration = throttleDuration; - - EventHandler<FusionCacheEntryEventArgs> onMiss = (s, e) => stats.RecordAction(EntryActionKind.Miss); - EventHandler<FusionCacheEntryHitEventArgs> onHit = (s, e) => stats.RecordAction(e.IsStale ? EntryActionKind.HitStale : EntryActionKind.HitNormal); - EventHandler<FusionCacheEntryEventArgs> onSet = (s, e) => stats.RecordAction(EntryActionKind.Set); - - // SETUP HANDLERS - cache.Events.Memory.Miss += onMiss; - cache.Events.Memory.Hit += onHit; - cache.Events.Memory.Set += onSet; - - // MISS: +2 - // SET: +1 - _ = await cache.GetOrSetAsync<int>("foo", async _ => 42); - - // REMOVE HANDLERS - cache.Events.Memory.Miss -= onMiss; - cache.Events.Memory.Hit -= onHit; - cache.Events.Memory.Set -= onSet; - - Assert.Equal(2, stats.Data[EntryActionKind.Miss]); - Assert.Equal(1, stats.Data[EntryActionKind.Set]); - Assert.Equal(3, stats.Data.Values.Sum()); - } + cache.DefaultEntryOptions.Duration = duration; + cache.DefaultEntryOptions.IsFailSafeEnabled = true; + cache.DefaultEntryOptions.FailSafeMaxDuration = maxDuration; + cache.DefaultEntryOptions.FailSafeThrottleDuration = throttleDuration; + + EventHandler<FusionCacheEntryEventArgs> onMiss = (s, e) => stats.RecordAction(EntryActionKind.Miss); + EventHandler<FusionCacheEntryHitEventArgs> onHit = (s, e) => stats.RecordAction(e.IsStale ? EntryActionKind.HitStale : EntryActionKind.HitNormal); + EventHandler<FusionCacheEntryEventArgs> onSet = (s, e) => stats.RecordAction(EntryActionKind.Set); + EventHandler<FusionCacheEntryEventArgs> onRemove = (s, e) => stats.RecordAction(EntryActionKind.Remove); + EventHandler<FusionCacheEntryEventArgs> onFailSafeActivate = (s, e) => stats.RecordAction(EntryActionKind.FailSafeActivate); + + // INITIAL, NON-TRACKED SET + await cache.SetAsync<int>("foo", 42); + + // SETUP HANDLERS + cache.Events.Miss += onMiss; + cache.Events.Hit += onHit; + cache.Events.Set += onSet; + cache.Events.Remove += onRemove; + cache.Events.FailSafeActivate += onFailSafeActivate; + + // LET IT BECOME STALE + await Task.Delay(duration.PlusALittleBit()); + + // HIT (STALE): +1 + _ = await cache.TryGetAsync<int>("foo"); + + // REMOVE HANDLERS + cache.Events.Miss -= onMiss; + cache.Events.Hit -= onHit; + cache.Events.Set -= onSet; + cache.Events.Remove -= onRemove; + cache.Events.FailSafeActivate -= onFailSafeActivate; + + Assert.Equal(1, stats.Data[EntryActionKind.HitStale]); + Assert.Equal(1, stats.Data.Values.Sum()); } + } - [Fact] - public void MemoryLayerEvents() + [Fact] + public void TryGetStaleFailSafe() + { + var stats = new EntryActionsStats(); + + var duration = TimeSpan.FromMilliseconds(200); + var maxDuration = TimeSpan.FromDays(1); + var throttleDuration = TimeSpan.FromMilliseconds(300); + + using (var cache = new FusionCache(new FusionCacheOptions() { EnableSyncEventHandlersExecution = true })) { - var stats = new EntryActionsStats(); - - var duration = TimeSpan.FromSeconds(2); - var maxDuration = TimeSpan.FromDays(1); - var throttleDuration = TimeSpan.FromSeconds(3); - - using (var cache = new FusionCache(new FusionCacheOptions() { EnableSyncEventHandlersExecution = true })) - { - cache.DefaultEntryOptions.Duration = duration; - cache.DefaultEntryOptions.IsFailSafeEnabled = true; - cache.DefaultEntryOptions.FailSafeMaxDuration = maxDuration; - cache.DefaultEntryOptions.FailSafeThrottleDuration = throttleDuration; - - EventHandler<FusionCacheEntryEventArgs> onMiss = (s, e) => stats.RecordAction(EntryActionKind.Miss); - EventHandler<FusionCacheEntryHitEventArgs> onHit = (s, e) => stats.RecordAction(e.IsStale ? EntryActionKind.HitStale : EntryActionKind.HitNormal); - EventHandler<FusionCacheEntryEventArgs> onSet = (s, e) => stats.RecordAction(EntryActionKind.Set); - - // SETUP HANDLERS - cache.Events.Memory.Miss += onMiss; - cache.Events.Memory.Hit += onHit; - cache.Events.Memory.Set += onSet; - - // MISS: +2 - // SET: +1 - cache.GetOrSet<int>("foo", _ => 42); - - // REMOVE HANDLERS - cache.Events.Memory.Miss -= onMiss; - cache.Events.Memory.Hit -= onHit; - cache.Events.Memory.Set -= onSet; - - Assert.Equal(2, stats.Data[EntryActionKind.Miss]); - Assert.Equal(1, stats.Data[EntryActionKind.Set]); - Assert.Equal(3, stats.Data.Values.Sum()); - } + cache.DefaultEntryOptions.Duration = duration; + cache.DefaultEntryOptions.IsFailSafeEnabled = true; + cache.DefaultEntryOptions.FailSafeMaxDuration = maxDuration; + cache.DefaultEntryOptions.FailSafeThrottleDuration = throttleDuration; + + EventHandler<FusionCacheEntryEventArgs> onMiss = (s, e) => stats.RecordAction(EntryActionKind.Miss); + EventHandler<FusionCacheEntryHitEventArgs> onHit = (s, e) => stats.RecordAction(e.IsStale ? EntryActionKind.HitStale : EntryActionKind.HitNormal); + EventHandler<FusionCacheEntryEventArgs> onSet = (s, e) => stats.RecordAction(EntryActionKind.Set); + EventHandler<FusionCacheEntryEventArgs> onRemove = (s, e) => stats.RecordAction(EntryActionKind.Remove); + EventHandler<FusionCacheEntryEventArgs> onFailSafeActivate = (s, e) => stats.RecordAction(EntryActionKind.FailSafeActivate); + + // INITIAL, NON-TRACKED SET + cache.Set<int>("foo", 42); + + // SETUP HANDLERS + cache.Events.Miss += onMiss; + cache.Events.Hit += onHit; + cache.Events.Set += onSet; + cache.Events.Remove += onRemove; + cache.Events.FailSafeActivate += onFailSafeActivate; + + // LET IT BECOME STALE + Thread.Sleep(duration.PlusALittleBit()); + + // HIT (STALE): +1 + cache.TryGet<int>("foo"); + + // REMOVE HANDLERS + cache.Events.Miss -= onMiss; + cache.Events.Hit -= onHit; + cache.Events.Set -= onSet; + cache.Events.Remove -= onRemove; + cache.Events.FailSafeActivate -= onFailSafeActivate; + + Assert.Equal(1, stats.Data[EntryActionKind.HitStale]); + Assert.Equal(1, stats.Data.Values.Sum()); } + } - [Fact] - public async Task BackplaneEventsAsync() + [Fact] + public async Task TryGetStaleNoFailSafeAsync() + { + var stats = new EntryActionsStats(); + + var duration = TimeSpan.FromMilliseconds(200); + var maxDuration = TimeSpan.FromDays(1); + var throttleDuration = TimeSpan.FromMilliseconds(300); + + using (var cache = new FusionCache(new FusionCacheOptions() { EnableSyncEventHandlersExecution = true })) { - var stats2 = new EntryActionsStats(); - var stats3 = new EntryActionsStats(); + cache.DefaultEntryOptions.Duration = duration; + cache.DefaultEntryOptions.IsFailSafeEnabled = true; + cache.DefaultEntryOptions.FailSafeMaxDuration = maxDuration; + cache.DefaultEntryOptions.FailSafeThrottleDuration = throttleDuration; - var entryOptions = new FusionCacheEntryOptions - { - Duration = TimeSpan.FromMinutes(10), - AllowBackgroundDistributedCacheOperations = false, - AllowBackgroundBackplaneOperations = false - }; + EventHandler<FusionCacheEntryEventArgs> onMiss = (s, e) => stats.RecordAction(EntryActionKind.Miss); + EventHandler<FusionCacheEntryHitEventArgs> onHit = (s, e) => stats.RecordAction(e.IsStale ? EntryActionKind.HitStale : EntryActionKind.HitNormal); + EventHandler<FusionCacheEntryEventArgs> onSet = (s, e) => stats.RecordAction(EntryActionKind.Set); + EventHandler<FusionCacheEntryEventArgs> onRemove = (s, e) => stats.RecordAction(EntryActionKind.Remove); + EventHandler<FusionCacheEntryEventArgs> onFailSafeActivate = (s, e) => stats.RecordAction(EntryActionKind.FailSafeActivate); - using var cache1 = new FusionCache(new FusionCacheOptions() { EnableSyncEventHandlersExecution = true, DefaultEntryOptions = entryOptions }); - using var cache2 = new FusionCache(new FusionCacheOptions() { EnableSyncEventHandlersExecution = true, DefaultEntryOptions = entryOptions }); - using var cache3 = new FusionCache(new FusionCacheOptions() { EnableSyncEventHandlersExecution = true, DefaultEntryOptions = entryOptions }); + // INITIAL, NON-TRACKED SET + await cache.SetAsync<int>("foo", 42); - var backplaneConnectionId = Guid.NewGuid().ToString("N"); + // SETUP HANDLERS + cache.Events.Miss += onMiss; + cache.Events.Hit += onHit; + cache.Events.Set += onSet; + cache.Events.Remove += onRemove; + cache.Events.FailSafeActivate += onFailSafeActivate; - cache1.SetupBackplane(new MemoryBackplane(new MemoryBackplaneOptions() { ConnectionId = backplaneConnectionId })); - cache2.SetupBackplane(new MemoryBackplane(new MemoryBackplaneOptions() { ConnectionId = backplaneConnectionId })); - cache3.SetupBackplane(new MemoryBackplane(new MemoryBackplaneOptions() { ConnectionId = backplaneConnectionId })); + // LET IT BECOME STALE + await Task.Delay(duration.PlusALittleBit()); - EventHandler<FusionCacheBackplaneMessageEventArgs> onMessagePublished2 = (s, e) => stats2.RecordAction(EntryActionKind.BackplaneMessagePublished); - EventHandler<FusionCacheBackplaneMessageEventArgs> onMessageReceived2 = (s, e) => stats2.RecordAction(EntryActionKind.BackplaneMessageReceived); - EventHandler<FusionCacheBackplaneMessageEventArgs> onMessagePublished3 = (s, e) => stats3.RecordAction(EntryActionKind.BackplaneMessagePublished); - EventHandler<FusionCacheBackplaneMessageEventArgs> onMessageReceived3 = (s, e) => stats3.RecordAction(EntryActionKind.BackplaneMessageReceived); + // MISS: +1 + _ = await cache.TryGetAsync<int>("foo", options => options.SetFailSafe(false)); + + // REMOVE HANDLERS + cache.Events.Miss -= onMiss; + cache.Events.Hit -= onHit; + cache.Events.Set -= onSet; + cache.Events.Remove -= onRemove; + cache.Events.FailSafeActivate -= onFailSafeActivate; + + Assert.Equal(1, stats.Data[EntryActionKind.Miss]); + Assert.Equal(1, stats.Data.Values.Sum()); + } + } + + [Fact] + public void TryGetStaleNoFailSafe() + { + var stats = new EntryActionsStats(); + + var duration = TimeSpan.FromMilliseconds(200); + var maxDuration = TimeSpan.FromDays(1); + var throttleDuration = TimeSpan.FromMilliseconds(300); + + using (var cache = new FusionCache(new FusionCacheOptions() { EnableSyncEventHandlersExecution = true })) + { + cache.DefaultEntryOptions.Duration = duration; + cache.DefaultEntryOptions.IsFailSafeEnabled = true; + cache.DefaultEntryOptions.FailSafeMaxDuration = maxDuration; + cache.DefaultEntryOptions.FailSafeThrottleDuration = throttleDuration; + + EventHandler<FusionCacheEntryEventArgs> onMiss = (s, e) => stats.RecordAction(EntryActionKind.Miss); + EventHandler<FusionCacheEntryHitEventArgs> onHit = (s, e) => stats.RecordAction(e.IsStale ? EntryActionKind.HitStale : EntryActionKind.HitNormal); + EventHandler<FusionCacheEntryEventArgs> onSet = (s, e) => stats.RecordAction(EntryActionKind.Set); + EventHandler<FusionCacheEntryEventArgs> onRemove = (s, e) => stats.RecordAction(EntryActionKind.Remove); + EventHandler<FusionCacheEntryEventArgs> onFailSafeActivate = (s, e) => stats.RecordAction(EntryActionKind.FailSafeActivate); + + // INITIAL, NON-TRACKED SET + cache.Set<int>("foo", 42); // SETUP HANDLERS - cache2.Events.Backplane.MessagePublished += onMessagePublished2; - cache2.Events.Backplane.MessageReceived += onMessageReceived2; - cache3.Events.Backplane.MessagePublished += onMessagePublished3; - cache3.Events.Backplane.MessageReceived += onMessageReceived3; + cache.Events.Miss += onMiss; + cache.Events.Hit += onHit; + cache.Events.Set += onSet; + cache.Events.Remove += onRemove; + cache.Events.FailSafeActivate += onFailSafeActivate; - // CACHE 1 - await cache1.SetAsync("foo", 21); - await cache1.SetAsync("foo", 42); + // LET IT BECOME STALE + Thread.Sleep(duration.PlusALittleBit()); - // CACHE 2 - await cache2.RemoveAsync("foo"); + // MISS: +1 + cache.TryGet<int>("foo", options => options.SetFailSafe(false)); // REMOVE HANDLERS - cache2.Events.Backplane.MessagePublished -= onMessagePublished2; - cache2.Events.Backplane.MessageReceived -= onMessageReceived2; - cache3.Events.Backplane.MessagePublished -= onMessagePublished3; - cache3.Events.Backplane.MessageReceived -= onMessageReceived3; - - Assert.Equal(1, stats2.Data[EntryActionKind.BackplaneMessagePublished]); - Assert.Equal(2, stats2.Data[EntryActionKind.BackplaneMessageReceived]); - Assert.Equal(0, stats3.Data[EntryActionKind.BackplaneMessagePublished]); - Assert.Equal(3, stats3.Data[EntryActionKind.BackplaneMessageReceived]); + cache.Events.Miss -= onMiss; + cache.Events.Hit -= onHit; + cache.Events.Set -= onSet; + cache.Events.Remove -= onRemove; + cache.Events.FailSafeActivate -= onFailSafeActivate; + + Assert.Equal(1, stats.Data[EntryActionKind.Miss]); + Assert.Equal(1, stats.Data.Values.Sum()); } + } + + [Fact] + public async Task MemoryLayerEventsAsync() + { + var stats = new EntryActionsStats(); + + var duration = TimeSpan.FromSeconds(2); + var maxDuration = TimeSpan.FromDays(1); + var throttleDuration = TimeSpan.FromSeconds(3); - [Fact] - public void BackplaneEvents() + using (var cache = new FusionCache(new FusionCacheOptions() { EnableSyncEventHandlersExecution = true })) { - var stats2 = new EntryActionsStats(); - var stats3 = new EntryActionsStats(); + cache.DefaultEntryOptions.Duration = duration; + cache.DefaultEntryOptions.IsFailSafeEnabled = true; + cache.DefaultEntryOptions.FailSafeMaxDuration = maxDuration; + cache.DefaultEntryOptions.FailSafeThrottleDuration = throttleDuration; - var entryOptions = new FusionCacheEntryOptions - { - Duration = TimeSpan.FromMinutes(10), - AllowBackgroundDistributedCacheOperations = false, - AllowBackgroundBackplaneOperations = false - }; + EventHandler<FusionCacheEntryEventArgs> onMiss = (s, e) => stats.RecordAction(EntryActionKind.Miss); + EventHandler<FusionCacheEntryHitEventArgs> onHit = (s, e) => stats.RecordAction(e.IsStale ? EntryActionKind.HitStale : EntryActionKind.HitNormal); + EventHandler<FusionCacheEntryEventArgs> onSet = (s, e) => stats.RecordAction(EntryActionKind.Set); - using var cache1 = new FusionCache(new FusionCacheOptions() { EnableSyncEventHandlersExecution = true, DefaultEntryOptions = entryOptions }); - using var cache2 = new FusionCache(new FusionCacheOptions() { EnableSyncEventHandlersExecution = true, DefaultEntryOptions = entryOptions }); - using var cache3 = new FusionCache(new FusionCacheOptions() { EnableSyncEventHandlersExecution = true, DefaultEntryOptions = entryOptions }); + // SETUP HANDLERS + cache.Events.Memory.Miss += onMiss; + cache.Events.Memory.Hit += onHit; + cache.Events.Memory.Set += onSet; - var backplaneConnectionId = Guid.NewGuid().ToString("N"); + // MISS: +2 + // SET: +1 + _ = await cache.GetOrSetAsync<int>("foo", async _ => 42); - cache1.SetupBackplane(new MemoryBackplane(new MemoryBackplaneOptions() { ConnectionId = backplaneConnectionId })); - cache2.SetupBackplane(new MemoryBackplane(new MemoryBackplaneOptions() { ConnectionId = backplaneConnectionId })); - cache3.SetupBackplane(new MemoryBackplane(new MemoryBackplaneOptions() { ConnectionId = backplaneConnectionId })); + // REMOVE HANDLERS + cache.Events.Memory.Miss -= onMiss; + cache.Events.Memory.Hit -= onHit; + cache.Events.Memory.Set -= onSet; - EventHandler<FusionCacheBackplaneMessageEventArgs> onMessagePublished2 = (s, e) => stats2.RecordAction(EntryActionKind.BackplaneMessagePublished); - EventHandler<FusionCacheBackplaneMessageEventArgs> onMessageReceived2 = (s, e) => stats2.RecordAction(EntryActionKind.BackplaneMessageReceived); - EventHandler<FusionCacheBackplaneMessageEventArgs> onMessagePublished3 = (s, e) => stats3.RecordAction(EntryActionKind.BackplaneMessagePublished); - EventHandler<FusionCacheBackplaneMessageEventArgs> onMessageReceived3 = (s, e) => stats3.RecordAction(EntryActionKind.BackplaneMessageReceived); + Assert.Equal(2, stats.Data[EntryActionKind.Miss]); + Assert.Equal(1, stats.Data[EntryActionKind.Set]); + Assert.Equal(3, stats.Data.Values.Sum()); + } + } - // SETUP HANDLERS - cache2.Events.Backplane.MessagePublished += onMessagePublished2; - cache2.Events.Backplane.MessageReceived += onMessageReceived2; - cache3.Events.Backplane.MessagePublished += onMessagePublished3; - cache3.Events.Backplane.MessageReceived += onMessageReceived3; + [Fact] + public void MemoryLayerEvents() + { + var stats = new EntryActionsStats(); - // CACHE 1 - cache1.Set("foo", 21); - cache1.Set("foo", 42); + var duration = TimeSpan.FromSeconds(2); + var maxDuration = TimeSpan.FromDays(1); + var throttleDuration = TimeSpan.FromSeconds(3); - // CACHE 2 - cache2.Remove("foo"); + using (var cache = new FusionCache(new FusionCacheOptions() { EnableSyncEventHandlersExecution = true })) + { + cache.DefaultEntryOptions.Duration = duration; + cache.DefaultEntryOptions.IsFailSafeEnabled = true; + cache.DefaultEntryOptions.FailSafeMaxDuration = maxDuration; + cache.DefaultEntryOptions.FailSafeThrottleDuration = throttleDuration; + + EventHandler<FusionCacheEntryEventArgs> onMiss = (s, e) => stats.RecordAction(EntryActionKind.Miss); + EventHandler<FusionCacheEntryHitEventArgs> onHit = (s, e) => stats.RecordAction(e.IsStale ? EntryActionKind.HitStale : EntryActionKind.HitNormal); + EventHandler<FusionCacheEntryEventArgs> onSet = (s, e) => stats.RecordAction(EntryActionKind.Set); + + // SETUP HANDLERS + cache.Events.Memory.Miss += onMiss; + cache.Events.Memory.Hit += onHit; + cache.Events.Memory.Set += onSet; + + // MISS: +2 + // SET: +1 + cache.GetOrSet<int>("foo", _ => 42); // REMOVE HANDLERS - cache2.Events.Backplane.MessagePublished -= onMessagePublished2; - cache2.Events.Backplane.MessageReceived -= onMessageReceived2; - cache3.Events.Backplane.MessagePublished -= onMessagePublished3; - cache3.Events.Backplane.MessageReceived -= onMessageReceived3; - - Assert.Equal(1, stats2.Data[EntryActionKind.BackplaneMessagePublished]); - Assert.Equal(2, stats2.Data[EntryActionKind.BackplaneMessageReceived]); - Assert.Equal(0, stats3.Data[EntryActionKind.BackplaneMessagePublished]); - Assert.Equal(3, stats3.Data[EntryActionKind.BackplaneMessageReceived]); + cache.Events.Memory.Miss -= onMiss; + cache.Events.Memory.Hit -= onHit; + cache.Events.Memory.Set -= onSet; + + Assert.Equal(2, stats.Data[EntryActionKind.Miss]); + Assert.Equal(1, stats.Data[EntryActionKind.Set]); + Assert.Equal(3, stats.Data.Values.Sum()); } + } + + [Fact] + public async Task BackplaneEventsAsync() + { + var stats2 = new EntryActionsStats(); + var stats3 = new EntryActionsStats(); + + var entryOptions = new FusionCacheEntryOptions + { + Duration = TimeSpan.FromMinutes(10), + AllowBackgroundDistributedCacheOperations = false, + AllowBackgroundBackplaneOperations = false, + SkipBackplaneNotifications = true + }; + + using var cache1 = new FusionCache(new FusionCacheOptions() { EnableSyncEventHandlersExecution = true, DefaultEntryOptions = entryOptions }, logger: CreateXUnitLogger<FusionCache>()); + using var cache2 = new FusionCache(new FusionCacheOptions() { EnableSyncEventHandlersExecution = true, DefaultEntryOptions = entryOptions }, logger: CreateXUnitLogger<FusionCache>()); + using var cache3 = new FusionCache(new FusionCacheOptions() { EnableSyncEventHandlersExecution = true, DefaultEntryOptions = entryOptions }, logger: CreateXUnitLogger<FusionCache>()); + + var backplaneConnectionId = Guid.NewGuid().ToString("N"); + + cache1.SetupBackplane(new MemoryBackplane(new MemoryBackplaneOptions() { ConnectionId = backplaneConnectionId })); + cache2.SetupBackplane(new MemoryBackplane(new MemoryBackplaneOptions() { ConnectionId = backplaneConnectionId })); + cache3.SetupBackplane(new MemoryBackplane(new MemoryBackplaneOptions() { ConnectionId = backplaneConnectionId })); + + EventHandler<FusionCacheBackplaneMessageEventArgs> onMessagePublished2 = (s, e) => stats2.RecordAction(EntryActionKind.BackplaneMessagePublished); + EventHandler<FusionCacheBackplaneMessageEventArgs> onMessageReceived2 = (s, e) => stats2.RecordAction(EntryActionKind.BackplaneMessageReceived); + EventHandler<FusionCacheBackplaneMessageEventArgs> onMessagePublished3 = (s, e) => stats3.RecordAction(EntryActionKind.BackplaneMessagePublished); + EventHandler<FusionCacheBackplaneMessageEventArgs> onMessageReceived3 = (s, e) => stats3.RecordAction(EntryActionKind.BackplaneMessageReceived); + + // SETUP HANDLERS + cache2.Events.Backplane.MessagePublished += onMessagePublished2; + cache2.Events.Backplane.MessageReceived += onMessageReceived2; + cache3.Events.Backplane.MessagePublished += onMessagePublished3; + cache3.Events.Backplane.MessageReceived += onMessageReceived3; + + // CACHE 1 + await cache1.SetAsync("foo", 21, opt => opt.SetSkipBackplaneNotifications(false)); + await cache1.SetAsync("foo", 42, opt => opt.SetSkipBackplaneNotifications(false)); + + // CACHE 2 + await cache2.RemoveAsync("foo", opt => opt.SetSkipBackplaneNotifications(false)); + + Thread.Sleep(TimeSpan.FromMilliseconds(200)); + + // REMOVE HANDLERS + cache2.Events.Backplane.MessagePublished -= onMessagePublished2; + cache2.Events.Backplane.MessageReceived -= onMessageReceived2; + cache3.Events.Backplane.MessagePublished -= onMessagePublished3; + cache3.Events.Backplane.MessageReceived -= onMessageReceived3; + + Assert.Equal(1, stats2.Data[EntryActionKind.BackplaneMessagePublished]); + Assert.Equal(2, stats2.Data[EntryActionKind.BackplaneMessageReceived]); + Assert.Equal(0, stats3.Data[EntryActionKind.BackplaneMessagePublished]); + Assert.Equal(3, stats3.Data[EntryActionKind.BackplaneMessageReceived]); + } + + [Fact] + public void BackplaneEvents() + { + var stats2 = new EntryActionsStats(); + var stats3 = new EntryActionsStats(); + + var entryOptions = new FusionCacheEntryOptions + { + Duration = TimeSpan.FromMinutes(10), + AllowBackgroundDistributedCacheOperations = false, + AllowBackgroundBackplaneOperations = false, + SkipBackplaneNotifications = true + }; + + using var cache1 = new FusionCache(new FusionCacheOptions() { EnableSyncEventHandlersExecution = true, DefaultEntryOptions = entryOptions }, logger: CreateXUnitLogger<FusionCache>()); + using var cache2 = new FusionCache(new FusionCacheOptions() { EnableSyncEventHandlersExecution = true, DefaultEntryOptions = entryOptions }, logger: CreateXUnitLogger<FusionCache>()); + using var cache3 = new FusionCache(new FusionCacheOptions() { EnableSyncEventHandlersExecution = true, DefaultEntryOptions = entryOptions }, logger: CreateXUnitLogger<FusionCache>()); + + var backplaneConnectionId = Guid.NewGuid().ToString("N"); + + cache1.SetupBackplane(new MemoryBackplane(new MemoryBackplaneOptions() { ConnectionId = backplaneConnectionId })); + cache2.SetupBackplane(new MemoryBackplane(new MemoryBackplaneOptions() { ConnectionId = backplaneConnectionId })); + cache3.SetupBackplane(new MemoryBackplane(new MemoryBackplaneOptions() { ConnectionId = backplaneConnectionId })); + + EventHandler<FusionCacheBackplaneMessageEventArgs> onMessagePublished2 = (s, e) => stats2.RecordAction(EntryActionKind.BackplaneMessagePublished); + EventHandler<FusionCacheBackplaneMessageEventArgs> onMessageReceived2 = (s, e) => stats2.RecordAction(EntryActionKind.BackplaneMessageReceived); + EventHandler<FusionCacheBackplaneMessageEventArgs> onMessagePublished3 = (s, e) => stats3.RecordAction(EntryActionKind.BackplaneMessagePublished); + EventHandler<FusionCacheBackplaneMessageEventArgs> onMessageReceived3 = (s, e) => stats3.RecordAction(EntryActionKind.BackplaneMessageReceived); + + // SETUP HANDLERS + cache2.Events.Backplane.MessagePublished += onMessagePublished2; + cache2.Events.Backplane.MessageReceived += onMessageReceived2; + cache3.Events.Backplane.MessagePublished += onMessagePublished3; + cache3.Events.Backplane.MessageReceived += onMessageReceived3; + + // CACHE 1 + cache1.Set("foo", 21, opt => opt.SetSkipBackplaneNotifications(false)); + cache1.Set("foo", 42, opt => opt.SetSkipBackplaneNotifications(false)); + + // CACHE 2 + cache2.Remove("foo", opt => opt.SetSkipBackplaneNotifications(false)); + + Thread.Sleep(TimeSpan.FromMilliseconds(200)); + + // REMOVE HANDLERS + cache2.Events.Backplane.MessagePublished -= onMessagePublished2; + cache2.Events.Backplane.MessageReceived -= onMessageReceived2; + cache3.Events.Backplane.MessagePublished -= onMessagePublished3; + cache3.Events.Backplane.MessageReceived -= onMessageReceived3; + + Assert.Equal(1, stats2.Data[EntryActionKind.BackplaneMessagePublished]); + Assert.Equal(2, stats2.Data[EntryActionKind.BackplaneMessageReceived]); + Assert.Equal(0, stats3.Data[EntryActionKind.BackplaneMessagePublished]); + Assert.Equal(3, stats3.Data[EntryActionKind.BackplaneMessageReceived]); + } + + [Fact] + public async Task StaleHitForOldStaleDataAsync() + { + var stats = new EntryActionsStats(); + var duration = TimeSpan.FromMilliseconds(200); - [Fact] - public async Task StaleHitForOldStaleDataAsync() + using (var cache = new FusionCache(new FusionCacheOptions() { EnableSyncEventHandlersExecution = true })) { - var stats = new EntryActionsStats(); - var duration = TimeSpan.FromMilliseconds(200); - - using (var cache = new FusionCache(new FusionCacheOptions() { EnableSyncEventHandlersExecution = true })) - { - EventHandler<FusionCacheEntryHitEventArgs> onHit = (s, e) => stats.RecordAction(e.IsStale ? EntryActionKind.HitStale : EntryActionKind.HitNormal); - EventHandler<FusionCacheEntryEventArgs> onSet = (s, e) => stats.RecordAction(EntryActionKind.Set); - EventHandler<FusionCacheEntryEventArgs> onFailSafeActivate = (s, e) => stats.RecordAction(EntryActionKind.FailSafeActivate); - - // SETUP HANDLERS - cache.Events.Hit += onHit; - cache.Events.Set += onSet; - cache.Events.FailSafeActivate += onFailSafeActivate; - - // SET: +1 - var firstValue = await cache.GetOrSetAsync<int>("foo", async _ => 21, new FusionCacheEntryOptions(duration).SetFailSafe(true)); - // HIT (NORMAL): +1 - var secondValue = await cache.GetOrSetAsync<int>("foo", async _ => 10, new FusionCacheEntryOptions(duration).SetFailSafe(true)); - await Task.Delay(duration.PlusALittleBit()); - // FAIL-SAFE: +1 - // HIT (STALE): +1 - var thirdValue = await cache.GetOrSetAsync<int>("foo", async _ => throw new Exception("Sloths are cool"), new FusionCacheEntryOptions(duration).SetFailSafe(true)); - // HIT (STALE): +1 - var fourthValue = await cache.GetOrSetAsync<int>("foo", async _ => 42, new FusionCacheEntryOptions(duration).SetFailSafe(true)); - - // REMOVE HANDLERS - cache.Events.Hit -= onHit; - cache.Events.Set -= onSet; - cache.Events.FailSafeActivate -= onFailSafeActivate; - - Assert.Equal(21, firstValue); - Assert.Equal(21, secondValue); - Assert.Equal(21, thirdValue); - Assert.Equal(21, fourthValue); - Assert.Equal(1, stats.Data[EntryActionKind.Set]); - Assert.Equal(1, stats.Data[EntryActionKind.HitNormal]); - Assert.Equal(2, stats.Data[EntryActionKind.HitStale]); - Assert.Equal(1, stats.Data[EntryActionKind.FailSafeActivate]); - } + EventHandler<FusionCacheEntryHitEventArgs> onHit = (s, e) => stats.RecordAction(e.IsStale ? EntryActionKind.HitStale : EntryActionKind.HitNormal); + EventHandler<FusionCacheEntryEventArgs> onSet = (s, e) => stats.RecordAction(EntryActionKind.Set); + EventHandler<FusionCacheEntryEventArgs> onFailSafeActivate = (s, e) => stats.RecordAction(EntryActionKind.FailSafeActivate); + + // SETUP HANDLERS + cache.Events.Hit += onHit; + cache.Events.Set += onSet; + cache.Events.FailSafeActivate += onFailSafeActivate; + + // SET: +1 + var firstValue = await cache.GetOrSetAsync<int>("foo", async _ => 21, new FusionCacheEntryOptions(duration).SetFailSafe(true)); + // HIT (NORMAL): +1 + var secondValue = await cache.GetOrSetAsync<int>("foo", async _ => 10, new FusionCacheEntryOptions(duration).SetFailSafe(true)); + await Task.Delay(duration.PlusALittleBit()); + // FAIL-SAFE: +1 + // HIT (STALE): +1 + var thirdValue = await cache.GetOrSetAsync<int>("foo", async _ => throw new Exception("Sloths are cool"), new FusionCacheEntryOptions(duration).SetFailSafe(true)); + // HIT (STALE): +1 + var fourthValue = await cache.GetOrSetAsync<int>("foo", async _ => 42, new FusionCacheEntryOptions(duration).SetFailSafe(true)); + + // REMOVE HANDLERS + cache.Events.Hit -= onHit; + cache.Events.Set -= onSet; + cache.Events.FailSafeActivate -= onFailSafeActivate; + + Assert.Equal(21, firstValue); + Assert.Equal(21, secondValue); + Assert.Equal(21, thirdValue); + Assert.Equal(21, fourthValue); + Assert.Equal(1, stats.Data[EntryActionKind.Set]); + Assert.Equal(1, stats.Data[EntryActionKind.HitNormal]); + Assert.Equal(2, stats.Data[EntryActionKind.HitStale]); + Assert.Equal(1, stats.Data[EntryActionKind.FailSafeActivate]); } + } - [Fact] - public void StaleHitForOldStaleData() + [Fact] + public void StaleHitForOldStaleData() + { + var stats = new EntryActionsStats(); + var duration = TimeSpan.FromMilliseconds(200); + + using (var cache = new FusionCache(new FusionCacheOptions() { EnableSyncEventHandlersExecution = true })) { - var stats = new EntryActionsStats(); - var duration = TimeSpan.FromMilliseconds(200); - - using (var cache = new FusionCache(new FusionCacheOptions() { EnableSyncEventHandlersExecution = true })) - { - EventHandler<FusionCacheEntryHitEventArgs> onHit = (s, e) => stats.RecordAction(e.IsStale ? EntryActionKind.HitStale : EntryActionKind.HitNormal); - EventHandler<FusionCacheEntryEventArgs> onSet = (s, e) => stats.RecordAction(EntryActionKind.Set); - EventHandler<FusionCacheEntryEventArgs> onFailSafeActivate = (s, e) => stats.RecordAction(EntryActionKind.FailSafeActivate); - - // SETUP HANDLERS - cache.Events.Hit += onHit; - cache.Events.Set += onSet; - cache.Events.FailSafeActivate += onFailSafeActivate; - - // SET: +1 - var firstValue = cache.GetOrSet<int>("foo", _ => 21, new FusionCacheEntryOptions(duration).SetFailSafe(true)); - // HIT (NORMAL): +1 - var secondValue = cache.GetOrSet<int>("foo", _ => 10, new FusionCacheEntryOptions(duration).SetFailSafe(true)); - Thread.Sleep(duration.PlusALittleBit()); - // FAIL-SAFE: +1 - // HIT (STALE): +1 - var thirdValue = cache.GetOrSet<int>("foo", _ => throw new Exception("Sloths are cool"), new FusionCacheEntryOptions(duration).SetFailSafe(true)); - // HIT (STALE): +1 - var fourthValue = cache.GetOrSet<int>("foo", _ => 42, new FusionCacheEntryOptions(duration).SetFailSafe(true)); - - // REMOVE HANDLERS - cache.Events.Hit -= onHit; - cache.Events.Set -= onSet; - cache.Events.FailSafeActivate -= onFailSafeActivate; - - Assert.Equal(21, firstValue); - Assert.Equal(21, secondValue); - Assert.Equal(21, thirdValue); - Assert.Equal(21, fourthValue); - Assert.Equal(1, stats.Data[EntryActionKind.Set]); - Assert.Equal(1, stats.Data[EntryActionKind.HitNormal]); - Assert.Equal(2, stats.Data[EntryActionKind.HitStale]); - Assert.Equal(1, stats.Data[EntryActionKind.FailSafeActivate]); - } + EventHandler<FusionCacheEntryHitEventArgs> onHit = (s, e) => stats.RecordAction(e.IsStale ? EntryActionKind.HitStale : EntryActionKind.HitNormal); + EventHandler<FusionCacheEntryEventArgs> onSet = (s, e) => stats.RecordAction(EntryActionKind.Set); + EventHandler<FusionCacheEntryEventArgs> onFailSafeActivate = (s, e) => stats.RecordAction(EntryActionKind.FailSafeActivate); + + // SETUP HANDLERS + cache.Events.Hit += onHit; + cache.Events.Set += onSet; + cache.Events.FailSafeActivate += onFailSafeActivate; + + // SET: +1 + var firstValue = cache.GetOrSet<int>("foo", _ => 21, new FusionCacheEntryOptions(duration).SetFailSafe(true)); + // HIT (NORMAL): +1 + var secondValue = cache.GetOrSet<int>("foo", _ => 10, new FusionCacheEntryOptions(duration).SetFailSafe(true)); + Thread.Sleep(duration.PlusALittleBit()); + // FAIL-SAFE: +1 + // HIT (STALE): +1 + var thirdValue = cache.GetOrSet<int>("foo", _ => throw new Exception("Sloths are cool"), new FusionCacheEntryOptions(duration).SetFailSafe(true)); + // HIT (STALE): +1 + var fourthValue = cache.GetOrSet<int>("foo", _ => 42, new FusionCacheEntryOptions(duration).SetFailSafe(true)); + + // REMOVE HANDLERS + cache.Events.Hit -= onHit; + cache.Events.Set -= onSet; + cache.Events.FailSafeActivate -= onFailSafeActivate; + + Assert.Equal(21, firstValue); + Assert.Equal(21, secondValue); + Assert.Equal(21, thirdValue); + Assert.Equal(21, fourthValue); + Assert.Equal(1, stats.Data[EntryActionKind.Set]); + Assert.Equal(1, stats.Data[EntryActionKind.HitNormal]); + Assert.Equal(2, stats.Data[EntryActionKind.HitStale]); + Assert.Equal(1, stats.Data[EntryActionKind.FailSafeActivate]); } } } diff --git a/tests/ZiggyCreatures.FusionCache.Tests/ExecutionUtilsTests.cs b/tests/ZiggyCreatures.FusionCache.Tests/ExecutionUtilsTests.cs deleted file mode 100644 index 0e87d410..00000000 --- a/tests/ZiggyCreatures.FusionCache.Tests/ExecutionUtilsTests.cs +++ /dev/null @@ -1,209 +0,0 @@ -using System; -using System.Diagnostics; -using System.Threading; -using System.Threading.Tasks; -using Xunit; -using ZiggyCreatures.Caching.Fusion; -using ZiggyCreatures.Caching.Fusion.Internals; - -namespace FusionCacheTests -{ - public class ExecutionUtilsTests - { - [Fact] - public async Task ZeroTimeoutDoesNotStartAsyncFuncAsync() - { - bool _hasRun = false; - - await Assert.ThrowsAsync<SyntheticTimeoutException>(async () => - { - await FusionCacheExecutionUtils.RunAsyncFuncWithTimeoutAsync(async ct => { _hasRun = true; return 42; }, TimeSpan.Zero, false, t => { }); - }); - Assert.False(_hasRun); - } - - [Fact] - public void ZeroTimeoutDoesNotStartAsyncFunc() - { - bool _hasRun = false; - - Assert.Throws<SyntheticTimeoutException>(() => - { - FusionCacheExecutionUtils.RunAsyncFuncWithTimeout(async ct => { _hasRun = true; return 42; }, TimeSpan.Zero, false, t => { }); - }); - Assert.False(_hasRun); - } - - [Fact] - public async Task ZeroTimeoutDoesNotStartAsyncActionAsync() - { - bool _hasRun = false; - - await Assert.ThrowsAsync<SyntheticTimeoutException>(async () => - { - await FusionCacheExecutionUtils.RunAsyncActionWithTimeoutAsync(async ct => { _hasRun = true; }, TimeSpan.Zero, false, t => { }); - }); - Assert.False(_hasRun); - } - - [Fact] - public void ZeroTimeoutDoesNotStartAsyncAction() - { - bool _hasRun = false; - - Assert.Throws<SyntheticTimeoutException>(() => - { - FusionCacheExecutionUtils.RunAsyncActionWithTimeout(async ct => { _hasRun = true; }, TimeSpan.Zero, false, t => { }); - }); - Assert.False(_hasRun); - } - - [Fact] - public async Task CancelingAsyncFuncActuallyCancelsItAsync() - { - int res = -1; - var factoryTerminated = false; - var outerCancelDelayMs = 500; - var innerDelayMs = 2_000; - await Assert.ThrowsAnyAsync<OperationCanceledException>(async () => - { - var cts = new CancellationTokenSource(outerCancelDelayMs); - res = await FusionCacheExecutionUtils.RunAsyncFuncWithTimeoutAsync(async ct => { await Task.Delay(innerDelayMs); ct.ThrowIfCancellationRequested(); factoryTerminated = true; return 42; }, Timeout.InfiniteTimeSpan, true, token: cts.Token); - }); - await Task.Delay(innerDelayMs); - - Assert.Equal(-1, res); - Assert.False(factoryTerminated); - } - - [Fact] - public void CancelingAsyncFuncActuallyCancelsIt() - { - int res = -1; - var factoryTerminated = false; - var outerCancelDelayMs = 500; - var innerDelayMs = 2_000; - Assert.ThrowsAny<OperationCanceledException>(() => - { - var cts = new CancellationTokenSource(outerCancelDelayMs); - res = FusionCacheExecutionUtils.RunAsyncFuncWithTimeout(async ct => { await Task.Delay(innerDelayMs); ct.ThrowIfCancellationRequested(); factoryTerminated = true; return 42; }, Timeout.InfiniteTimeSpan, true, token: cts.Token); - }); - - Assert.Equal(-1, res); - Assert.False(factoryTerminated); - } - - [Fact] - public void CancelingSyncFuncActuallyCancelsIt() - { - int res = -1; - var factoryTerminated = false; - var outerCancelDelayMs = 500; - var innerDelayMs = 2_000; - Assert.Throws<OperationCanceledException>(() => - { - var cts = new CancellationTokenSource(outerCancelDelayMs); - res = FusionCacheExecutionUtils.RunSyncFuncWithTimeout(ct => { Thread.Sleep(innerDelayMs); ct.ThrowIfCancellationRequested(); factoryTerminated = true; return 42; }, Timeout.InfiniteTimeSpan, true, token: cts.Token); - }); - - Assert.Equal(-1, res); - Assert.False(factoryTerminated); - } - - [Fact] - public async Task TimeoutEffectivelyWorksAsync() - { - int res = -1; - var timeoutMs = 500; - var innerDelayMs = 2_000; - var sw = Stopwatch.StartNew(); - await Assert.ThrowsAnyAsync<TimeoutException>(async () => - { - res = await FusionCacheExecutionUtils.RunAsyncFuncWithTimeoutAsync(async ct => { await Task.Delay(innerDelayMs); ct.ThrowIfCancellationRequested(); return 42; }, TimeSpan.FromMilliseconds(timeoutMs)); - }); - sw.Stop(); - - Assert.Equal(-1, res); - Assert.True(sw.ElapsedMilliseconds >= timeoutMs); - Assert.True(sw.ElapsedMilliseconds < innerDelayMs); - } - - [Fact] - public void TimeoutEffectivelyWorks() - { - int res = -1; - var timeoutMs = 500; - var innerDelayMs = 2_000; - var sw = Stopwatch.StartNew(); - Assert.ThrowsAny<TimeoutException>(() => - { - res = FusionCacheExecutionUtils.RunAsyncFuncWithTimeout(async ct => { await Task.Delay(innerDelayMs); ct.ThrowIfCancellationRequested(); return 42; }, TimeSpan.FromMilliseconds(timeoutMs)); - }); - sw.Stop(); - - Assert.Equal(-1, res); - Assert.True(sw.ElapsedMilliseconds >= timeoutMs); - Assert.True(sw.ElapsedMilliseconds < innerDelayMs); - } - - [Fact] - public async Task CancelWhenTimeoutActuallyWorksAsync() - { - var factoryCompleted = false; - var timeoutMs = 500; - var innerDelayMs = 2_000; - await Assert.ThrowsAnyAsync<TimeoutException>(async () => - { - await FusionCacheExecutionUtils.RunAsyncActionWithTimeoutAsync(async ct => { await Task.Delay(innerDelayMs); ct.ThrowIfCancellationRequested(); factoryCompleted = true; }, TimeSpan.FromMilliseconds(timeoutMs), true); - }); - await Task.Delay(innerDelayMs); - - Assert.False(factoryCompleted); - } - - [Fact] - public void CancelWhenTimeoutActuallyWorks() - { - var factoryCompleted = false; - var timeoutMs = 500; - var innerDelayMs = 2_000; - Assert.ThrowsAny<TimeoutException>(() => - { - FusionCacheExecutionUtils.RunAsyncActionWithTimeout(async ct => { await Task.Delay(innerDelayMs); ct.ThrowIfCancellationRequested(); factoryCompleted = true; }, TimeSpan.FromMilliseconds(timeoutMs), true); - }); - Thread.Sleep(innerDelayMs); - - Assert.False(factoryCompleted); - } - - [Fact] - public async Task DoNotCancelWhenTimeoutActuallyWorksAsync() - { - var factoryCompleted = false; - var timeoutMs = 100; - var innerDelayMs = 2_000; - await Assert.ThrowsAnyAsync<TimeoutException>(async () => - { - await FusionCacheExecutionUtils.RunAsyncActionWithTimeoutAsync(async ct => { await Task.Delay(innerDelayMs); ct.ThrowIfCancellationRequested(); factoryCompleted = true; }, TimeSpan.FromMilliseconds(timeoutMs), false); - }); - await Task.Delay(innerDelayMs + timeoutMs); - - Assert.True(factoryCompleted); - } - - [Fact] - public void DoNotCancelWhenTimeoutActuallyWorks() - { - var factoryCompleted = false; - var timeoutMs = 100; - var innerDelayMs = 2_000; - Assert.ThrowsAny<TimeoutException>(() => - { - FusionCacheExecutionUtils.RunAsyncActionWithTimeout(async ct => { await Task.Delay(innerDelayMs); ct.ThrowIfCancellationRequested(); factoryCompleted = true; }, TimeSpan.FromMilliseconds(timeoutMs), false); - }); - Thread.Sleep(innerDelayMs + timeoutMs); - - Assert.True(factoryCompleted); - } - } -} diff --git a/tests/ZiggyCreatures.FusionCache.Tests/GeneralTests.cs b/tests/ZiggyCreatures.FusionCache.Tests/GeneralTests.cs new file mode 100644 index 00000000..ee23f3b7 --- /dev/null +++ b/tests/ZiggyCreatures.FusionCache.Tests/GeneralTests.cs @@ -0,0 +1,247 @@ +using System; +using System.Diagnostics; +using System.Threading; +using System.Threading.Tasks; +using FusionCacheTests.Stuff; +using Microsoft.Extensions.Caching.Memory; +using Newtonsoft.Json; +using Xunit; +using Xunit.Abstractions; +using ZiggyCreatures.Caching.Fusion; +using ZiggyCreatures.Caching.Fusion.NullObjects; + +namespace FusionCacheTests; + +public class GeneralTests + : AbstractTests +{ + public GeneralTests(ITestOutputHelper output) + : base(output, null) + { + } + + [Fact] + public void CannotAssignNullToDefaultEntryOptions() + { + Assert.Throws<ArgumentNullException>(() => + { + var foo = new FusionCacheOptions() { DefaultEntryOptions = null! }; + }); + } + + [Fact] + public async Task CanUseNullFusionCacheAsync() + { + using var cache = new NullFusionCache(new FusionCacheOptions() + { + CacheName = "SlothsAreCool42", + DefaultEntryOptions = new FusionCacheEntryOptions() + { + IsFailSafeEnabled = true, + Duration = TimeSpan.FromMinutes(123) + } + }); + + await cache.SetAsync<int>("foo", 42); + + var maybeFoo1 = await cache.TryGetAsync<int>("foo"); + + await cache.RemoveAsync("foo"); + + var maybeBar1 = await cache.TryGetAsync<int>("bar"); + + await cache.ExpireAsync("qux"); + + var qux1 = await cache.GetOrSetAsync("qux", async _ => 1); + var qux2 = await cache.GetOrSetAsync("qux", async _ => 2); + var qux3 = await cache.GetOrSetAsync("qux", async _ => 3); + var qux4 = await cache.GetOrDefaultAsync("qux", 4); + + Assert.Equal("SlothsAreCool42", cache.CacheName); + Assert.False(string.IsNullOrWhiteSpace(cache.InstanceId)); + + Assert.False(cache.HasDistributedCache); + Assert.False(cache.HasBackplane); + + Assert.True(cache.DefaultEntryOptions.IsFailSafeEnabled); + Assert.Equal(TimeSpan.FromMinutes(123), cache.DefaultEntryOptions.Duration); + + Assert.False(maybeFoo1.HasValue); + Assert.False(maybeBar1.HasValue); + + Assert.Equal(1, qux1); + Assert.Equal(2, qux2); + Assert.Equal(3, qux3); + Assert.Equal(4, qux4); + + await Assert.ThrowsAsync<UnreachableException>(async () => + { + _ = await cache.GetOrSetAsync<int>("qux", async _ => throw new UnreachableException("Sloths")); + }); + } + + [Fact] + public void CanUseNullFusionCache() + { + using var cache = new NullFusionCache(new FusionCacheOptions() + { + CacheName = "SlothsAreCool42", + DefaultEntryOptions = new FusionCacheEntryOptions() + { + IsFailSafeEnabled = true, + Duration = TimeSpan.FromMinutes(123) + } + }); + + cache.Set<int>("foo", 42); + + var maybeFoo1 = cache.TryGet<int>("foo"); + + cache.Remove("foo"); + + var maybeBar1 = cache.TryGet<int>("bar"); + + cache.Expire("qux"); + + var qux1 = cache.GetOrSet("qux", _ => 1); + var qux2 = cache.GetOrSet("qux", _ => 2); + var qux3 = cache.GetOrSet("qux", _ => 3); + var qux4 = cache.GetOrDefault("qux", 4); + + Assert.Equal("SlothsAreCool42", cache.CacheName); + Assert.False(string.IsNullOrWhiteSpace(cache.InstanceId)); + + Assert.False(cache.HasDistributedCache); + Assert.False(cache.HasBackplane); + + Assert.True(cache.DefaultEntryOptions.IsFailSafeEnabled); + Assert.Equal(TimeSpan.FromMinutes(123), cache.DefaultEntryOptions.Duration); + + Assert.False(maybeFoo1.HasValue); + Assert.False(maybeBar1.HasValue); + + Assert.Equal(1, qux1); + Assert.Equal(2, qux2); + Assert.Equal(3, qux3); + Assert.Equal(4, qux4); + + Assert.Throws<UnreachableException>(() => + { + _ = cache.GetOrSet<int>("qux", _ => throw new UnreachableException("Sloths")); + }); + } + + [Fact] + public void CanDuplicatEntryOptions() + { + var original = new FusionCacheEntryOptions() + { + IsSafeForAdaptiveCaching = true, + + Duration = TimeSpan.FromSeconds(1), + LockTimeout = TimeSpan.FromSeconds(2), + Size = 123, + Priority = CacheItemPriority.High, + JitterMaxDuration = TimeSpan.FromSeconds(3), + + EagerRefreshThreshold = 0.456f, + + IsFailSafeEnabled = !FusionCacheGlobalDefaults.EntryOptionsIsFailSafeEnabled, + FailSafeMaxDuration = TimeSpan.FromSeconds(4), + FailSafeThrottleDuration = TimeSpan.FromSeconds(5), + + FactorySoftTimeout = TimeSpan.FromSeconds(6), + FactoryHardTimeout = TimeSpan.FromSeconds(7), + AllowTimedOutFactoryBackgroundCompletion = !FusionCacheGlobalDefaults.EntryOptionsAllowTimedOutFactoryBackgroundCompletion, + + DistributedCacheDuration = TimeSpan.FromSeconds(8), + DistributedCacheFailSafeMaxDuration = TimeSpan.FromSeconds(9), + DistributedCacheSoftTimeout = TimeSpan.FromSeconds(10), + DistributedCacheHardTimeout = TimeSpan.FromSeconds(11), + + ReThrowDistributedCacheExceptions = !FusionCacheGlobalDefaults.EntryOptionsReThrowDistributedCacheExceptions, + ReThrowSerializationExceptions = !FusionCacheGlobalDefaults.EntryOptionsReThrowSerializationExceptions, + ReThrowBackplaneExceptions = !FusionCacheGlobalDefaults.EntryOptionsReThrowBackplaneExceptions, + + AllowBackgroundDistributedCacheOperations = !FusionCacheGlobalDefaults.EntryOptionsAllowBackgroundDistributedCacheOperations, + AllowBackgroundBackplaneOperations = !FusionCacheGlobalDefaults.EntryOptionsAllowBackgroundBackplaneOperations, + + SkipBackplaneNotifications = !FusionCacheGlobalDefaults.EntryOptionsSkipBackplaneNotifications, + + SkipDistributedCache = !FusionCacheGlobalDefaults.EntryOptionsSkipDistributedCache, + SkipDistributedCacheReadWhenStale = !FusionCacheGlobalDefaults.EntryOptionsSkipDistributedCacheReadWhenStale, + + SkipMemoryCache = !FusionCacheGlobalDefaults.EntryOptionsSkipMemoryCache + }; + + var duplicated = original.Duplicate(); + + Assert.Equal( + JsonConvert.SerializeObject(original), + JsonConvert.SerializeObject(duplicated) + ); + } + + [Fact] + public void CanDisposeEvictedEntries() + { + var duration = TimeSpan.FromSeconds(1); + var memoryCache = new MemoryCache(new MemoryCacheOptions() + { + ExpirationScanFrequency = TimeSpan.FromMilliseconds(100) + }); + + using var cache = new FusionCache( + new FusionCacheOptions() + { + DefaultEntryOptions = new FusionCacheEntryOptions() + { + Duration = duration + } + }, + memoryCache + ); + + cache.Set("foo", new SimpleDisposable()); + + var d1 = cache.GetOrDefault<SimpleDisposable>("foo"); + + Assert.NotNull(d1); + Assert.False(d1.IsDisposed); + + Thread.Sleep(duration.PlusALittleBit()); + + var d2 = cache.GetOrDefault<SimpleDisposable>("foo"); + + memoryCache.Compact(1); + + Thread.Sleep(duration.PlusALittleBit()); + + Assert.Null(d2); + Assert.False(d1.IsDisposed); + + // ADD EVENT TO AUTO-DISPOSE EVICTED ENTRIES + cache.Events.Memory.Eviction += (sender, args) => + { + ((IDisposable?)args.Value)?.Dispose(); + }; + + cache.Set("foo", new SimpleDisposable()); + + var d3 = cache.GetOrDefault<SimpleDisposable>("foo"); + + Assert.NotNull(d3); + Assert.False(d3.IsDisposed); + + Thread.Sleep(duration.PlusALittleBit()); + + var d4 = cache.GetOrDefault<SimpleDisposable>("foo"); + + memoryCache.Compact(1); + + Thread.Sleep(duration.PlusALittleBit()); + + Assert.Null(d4); + Assert.True(d3.IsDisposed); + } +} diff --git a/tests/ZiggyCreatures.FusionCache.Tests/LoggingTests.cs b/tests/ZiggyCreatures.FusionCache.Tests/LoggingTests.cs index 2b4d6b9c..05dd1244 100644 --- a/tests/ZiggyCreatures.FusionCache.Tests/LoggingTests.cs +++ b/tests/ZiggyCreatures.FusionCache.Tests/LoggingTests.cs @@ -7,134 +7,137 @@ using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; using Xunit; +using Xunit.Abstractions; using ZiggyCreatures.Caching.Fusion; using ZiggyCreatures.Caching.Fusion.Backplane.Memory; using ZiggyCreatures.Caching.Fusion.NullObjects; using ZiggyCreatures.Caching.Fusion.Serialization.NewtonsoftJson; -namespace FusionCacheTests +namespace FusionCacheTests; + +public class LoggingTests + : AbstractTests { - public class LoggingTests + public LoggingTests(ITestOutputHelper output) + : base(output, null) + { + } + + [Fact] + public async Task CommonLogLevelsWork() { - private ListLogger<FusionCache> CreateListLogger(LogLevel minLogLevel) + var logger = CreateListLogger<FusionCache>(LogLevel.Debug); + using (var cache = new FusionCache(new FusionCacheOptions(), logger: logger)) { - return new ListLogger<FusionCache>(minLogLevel); + cache.AddPlugin(new NullPlugin()); + + cache.TryGet<int>("foo"); + cache.TryGet<int>("bar"); + cache.Set<int>("foo", 123); + cache.TryGet<int>("foo"); + cache.GetOrSet<int>("qux", _ => throw new Exception("Sloths!"), 123, opt => opt.SetFailSafe(true)); } - [Fact] - public async Task CommonLogLevelsWork() + Assert.Equal(22, logger.Items.Count); + Assert.Equal(2, logger.Items.Count(x => x.LogLevel == LogLevel.Warning)); + Assert.Equal(2, logger.Items.Count(x => x.LogLevel == LogLevel.Information)); + } + + [Fact] + public async Task PluginsInfoWork() + { + var logger = CreateListLogger<FusionCache>(LogLevel.Information); + var options = new FusionCacheOptions(); + using (var cache = new FusionCache(options, logger: logger)) + { + cache.AddPlugin(new NullPlugin()); + } + + Assert.Equal(2, logger.Items.Count); + + logger = CreateListLogger<FusionCache>(LogLevel.Information); + options = new FusionCacheOptions() + { + PluginsInfoLogLevel = LogLevel.Debug + }; + using (var cache = new FusionCache(options, logger: logger)) { - var logger = CreateListLogger(LogLevel.Debug); - using (var cache = new FusionCache(new FusionCacheOptions(), logger: logger)) - { - cache.AddPlugin(new NullPlugin()); - - cache.TryGet<int>("foo"); - cache.TryGet<int>("bar"); - cache.Set<int>("foo", 123); - cache.TryGet<int>("foo"); - cache.GetOrSet<int>("qux", _ => throw new Exception("Sloths!"), 123, opt => opt.SetFailSafe(true)); - } - - Assert.Equal(22, logger.Items.Count); - Assert.Equal(2, logger.Items.Count(x => x.LogLevel == LogLevel.Warning)); - Assert.Equal(2, logger.Items.Count(x => x.LogLevel == LogLevel.Information)); + cache.AddPlugin(new NullPlugin()); } - [Fact] - public async Task PluginsInfoWork() + Assert.Empty(logger.Items); + } + + [Fact] + public async Task EventsErrorsLogLevelsWork() + { + var logger = CreateListLogger<FusionCache>(LogLevel.Information); + var options = new FusionCacheOptions + { + EnableSyncEventHandlersExecution = true + }; + using (var cache = new FusionCache(options, logger: logger)) { - var logger = CreateListLogger(LogLevel.Information); - var options = new FusionCacheOptions(); - using (var cache = new FusionCache(options, logger: logger)) - { - cache.AddPlugin(new NullPlugin()); - } - - Assert.Equal(2, logger.Items.Count); - - logger = CreateListLogger(LogLevel.Information); - options = new FusionCacheOptions() - { - PluginsInfoLogLevel = LogLevel.Debug - }; - using (var cache = new FusionCache(options, logger: logger)) - { - cache.AddPlugin(new NullPlugin()); - } - - Assert.Empty(logger.Items); + cache.Events.FactorySuccess += (sender, e) => throw new Exception("Sloths!"); + cache.GetOrSet<int>("qux", _ => 123); } - [Fact] - public async Task EventsErrorsLogLevelsWork() + Assert.Single(logger.Items); + Assert.Single(logger.Items.Where(x => x.LogLevel == LogLevel.Warning)); + + logger = CreateListLogger<FusionCache>(LogLevel.Information); + options = new FusionCacheOptions + { + EnableSyncEventHandlersExecution = true, + EventHandlingErrorsLogLevel = LogLevel.Debug + }; + using (var cache = new FusionCache(options, logger: logger)) { - var logger = CreateListLogger(LogLevel.Information); - var options = new FusionCacheOptions - { - EnableSyncEventHandlersExecution = true - }; - using (var cache = new FusionCache(options, logger: logger)) - { - cache.Events.FactorySuccess += (sender, e) => throw new Exception("Sloths!"); - cache.GetOrSet<int>("qux", _ => 123); - } - - Assert.Single(logger.Items); - Assert.Single(logger.Items.Where(x => x.LogLevel == LogLevel.Warning)); - - logger = CreateListLogger(LogLevel.Information); - options = new FusionCacheOptions - { - EnableSyncEventHandlersExecution = true, - EventHandlingErrorsLogLevel = LogLevel.Debug - }; - using (var cache = new FusionCache(options, logger: logger)) - { - cache.Events.FactorySuccess += (sender, e) => throw new Exception("Sloths!"); - cache.GetOrSet<int>("qux", _ => 123); - } - - Assert.Empty(logger.Items); + cache.Events.FactorySuccess += (sender, e) => throw new Exception("Sloths!"); + cache.GetOrSet<int>("qux", _ => 123); } - [Fact] - public async Task CacheNameIsAlwaysThere() + Assert.Empty(logger.Items); + } + + [Fact] + public async Task CacheNameIsAlwaysThere() + { + var cacheName = Guid.NewGuid().ToString("N"); + var logger = CreateListLogger<FusionCache>(LogLevel.Trace); + var options = new FusionCacheOptions + { + CacheName = cacheName, + EnableSyncEventHandlersExecution = true + }; + using (var cache = new FusionCache(options, logger: logger)) { - var cacheName = Guid.NewGuid().ToString("N"); - var logger = CreateListLogger(LogLevel.Trace); - var options = new FusionCacheOptions - { - CacheName = cacheName, - EnableSyncEventHandlersExecution = true - }; - using (var cache = new FusionCache(options, logger: logger)) - { - // PLUGINS - cache.AddPlugin(new NullPlugin()); - - // DISTRIBUTED CACHE - cache.SetupDistributedCache( - new MemoryDistributedCache(Options.Create(new MemoryDistributedCacheOptions())), - new FusionCacheNewtonsoftJsonSerializer() - ); - - // BACKPLANE - cache.SetupBackplane( - new MemoryBackplane(new MemoryBackplaneOptions()) - ); - - // BASIC OPERATIONS - cache.Set<int>("foo", 123); - var foo = cache.GetOrDefault<int>("foo"); - var maybeFoo = cache.TryGet<int>("foo"); - cache.Remove("foo"); - } - - var itemsCountWithoutCacheName = logger.Items.Count(x => x.Message.Contains(cacheName) == false); - - Assert.True(logger.Items.Count > 0); - Assert.Equal(0, itemsCountWithoutCacheName); + // PLUGINS + cache.AddPlugin(new NullPlugin()); + + // DISTRIBUTED CACHE + cache.SetupDistributedCache( + new MemoryDistributedCache(Options.Create(new MemoryDistributedCacheOptions())), + new FusionCacheNewtonsoftJsonSerializer() + ); + + // BACKPLANE + cache.SetupBackplane( + new MemoryBackplane(new MemoryBackplaneOptions()) + ); + + // BASIC OPERATIONS + cache.Set<int>("foo", 123); + var foo = cache.GetOrDefault<int>("foo"); + var maybeFoo = cache.TryGet<int>("foo"); + cache.Remove("foo"); } + + await Task.Delay(500); + + var itemsCountWithoutCacheName = logger.Items.Count(x => x.Message.Contains(cacheName) == false); + + Assert.True(logger.Items.Count > 0); + Assert.Equal(0, itemsCountWithoutCacheName); } } diff --git a/tests/ZiggyCreatures.FusionCache.Tests/MemoryLevelTests.cs b/tests/ZiggyCreatures.FusionCache.Tests/MemoryLevelTests.cs new file mode 100644 index 00000000..9f5c3a21 --- /dev/null +++ b/tests/ZiggyCreatures.FusionCache.Tests/MemoryLevelTests.cs @@ -0,0 +1,1537 @@ +using System; +using System.Diagnostics; +using System.Threading; +using System.Threading.Tasks; +using FusionCacheTests.Stuff; +using Microsoft.Extensions.Caching.Distributed; +using Microsoft.Extensions.Caching.Memory; +using Xunit; +using Xunit.Abstractions; +using ZiggyCreatures.Caching.Fusion; +using ZiggyCreatures.Caching.Fusion.NullObjects; + +namespace FusionCacheTests; + +public static class SingleLevelTestsExtMethods +{ + public static FusionCacheEntryOptions SetFactoryTimeoutsMs(this FusionCacheEntryOptions options, int? softTimeoutMs = null, int? hardTimeoutMs = null, bool? keepTimedOutFactoryResult = null) + { + if (softTimeoutMs is not null) + options.FactorySoftTimeout = TimeSpan.FromMilliseconds(softTimeoutMs.Value); + if (hardTimeoutMs is not null) + options.FactoryHardTimeout = TimeSpan.FromMilliseconds(hardTimeoutMs.Value); + if (keepTimedOutFactoryResult is not null) + options.AllowTimedOutFactoryBackgroundCompletion = keepTimedOutFactoryResult.Value; + return options; + } +} + +public class MemoryLevelTests + : AbstractTests +{ + public MemoryLevelTests(ITestOutputHelper output) + : base(output, null) + { + } + + [Fact] + public async Task CanRemoveAsync() + { + using var cache = new FusionCache(new FusionCacheOptions()); + await cache.SetAsync<int>("foo", 42); + var foo1 = await cache.GetOrDefaultAsync<int>("foo"); + await cache.RemoveAsync("foo"); + var foo2 = await cache.GetOrDefaultAsync<int>("foo"); + Assert.Equal(42, foo1); + Assert.Equal(0, foo2); + } + + [Fact] + public void CanRemove() + { + using var cache = new FusionCache(new FusionCacheOptions()); + cache.Set<int>("foo", 42); + var foo1 = cache.GetOrDefault<int>("foo"); + cache.Remove("foo"); + var foo2 = cache.GetOrDefault<int>("foo"); + Assert.Equal(42, foo1); + Assert.Equal(0, foo2); + } + + [Fact] + public async Task ReturnsStaleDataWhenFactoryFailsWithFailSafeAsync() + { + using var cache = new FusionCache(new FusionCacheOptions()); + var initialValue = await cache.GetOrSetAsync<int>("foo", async _ => 42, new FusionCacheEntryOptions(TimeSpan.FromSeconds(1)).SetFailSafe(true)); + await Task.Delay(1_500); + var newValue = await cache.GetOrSetAsync<int>("foo", async _ => throw new Exception("Sloths are cool"), new FusionCacheEntryOptions(TimeSpan.FromSeconds(1)).SetFailSafe(true)); + Assert.Equal(initialValue, newValue); + } + + [Fact] + public void ReturnsStaleDataWhenFactoryFailsWithFailSafe() + { + using var cache = new FusionCache(new FusionCacheOptions()); + var initialValue = cache.GetOrSet<int>("foo", _ => 42, new FusionCacheEntryOptions(TimeSpan.FromSeconds(1)).SetFailSafe(true)); + Thread.Sleep(1_500); + var newValue = cache.GetOrSet<int>("foo", _ => throw new Exception("Sloths are cool"), new FusionCacheEntryOptions(TimeSpan.FromSeconds(1)).SetFailSafe(true)); + Assert.Equal(initialValue, newValue); + } + + [Fact] + public async Task ThrowsWhenFactoryThrowsWithoutFailSafeAsync() + { + using var cache = new FusionCache(new FusionCacheOptions()); + var initialValue = await cache.GetOrSetAsync<int>("foo", async _ => 42, new FusionCacheEntryOptions(TimeSpan.FromSeconds(1)).SetFailSafe(true)); + await Task.Delay(1_100); + await Assert.ThrowsAnyAsync<Exception>(async () => + { + var newValue = await cache.GetOrSetAsync<int>("foo", async _ => throw new Exception("Sloths are cool"), new FusionCacheEntryOptions(TimeSpan.FromSeconds(1)).SetFailSafe(false)); + }); + } + + [Fact] + public void ThrowsWhenFactoryThrowsWithoutFailSafe() + { + using var cache = new FusionCache(new FusionCacheOptions()); + var initialValue = cache.GetOrSet<int>("foo", _ => 42, new FusionCacheEntryOptions(TimeSpan.FromSeconds(1)) { IsFailSafeEnabled = true }); + Thread.Sleep(1_100); + Assert.ThrowsAny<Exception>(() => + { + var newValue = cache.GetOrSet<int>("foo", _ => throw new Exception("Sloths are cool"), new FusionCacheEntryOptions(TimeSpan.FromSeconds(1)) { IsFailSafeEnabled = false }); + }); + } + + [Fact] + public async Task ThrowsOnFactoryHardTimeoutWithoutStaleDataAsync() + { + using var cache = new FusionCache(new FusionCacheOptions()); + await Assert.ThrowsAsync<SyntheticTimeoutException>(async () => + { + var value = await cache.GetOrSetAsync<int>("foo", async _ => { await Task.Delay(1_000); return 21; }, new FusionCacheEntryOptions(TimeSpan.FromSeconds(1)).SetFailSafe(true).SetFactoryTimeoutsMs(2_000, 100)); + }); + } + + [Fact] + public void ThrowsOnFactoryHardTimeoutWithoutStaleData() + { + using var cache = new FusionCache(new FusionCacheOptions()); + Assert.Throws<SyntheticTimeoutException>(() => + { + var value = cache.GetOrSet<int>("foo", _ => { Thread.Sleep(1_000); return 21; }, new FusionCacheEntryOptions(TimeSpan.FromSeconds(1)).SetFailSafe(true).SetFactoryTimeoutsMs(2_000, 100)); + }); + } + + [Fact] + public async Task ReturnsStaleDataWhenFactorySoftTimeoutWithFailSafeAsync() + { + using var cache = new FusionCache(new FusionCacheOptions()); + var initialValue = await cache.GetOrSetAsync<int>("foo", async _ => 42, new FusionCacheEntryOptions(TimeSpan.FromSeconds(1)).SetFailSafe(true)); + await Task.Delay(1_100); + var newValue = await cache.GetOrSetAsync<int>("foo", async _ => { await Task.Delay(1_000); return 21; }, new FusionCacheEntryOptions(TimeSpan.FromSeconds(1)).SetFailSafe(true).SetFactoryTimeoutsMs(100)); + Assert.Equal(initialValue, newValue); + } + + [Fact] + public void ReturnsStaleDataWhenFactorySoftTimeoutWithFailSafe() + { + using var cache = new FusionCache(new FusionCacheOptions()); + var initialValue = cache.GetOrSet<int>("foo", _ => 42, new FusionCacheEntryOptions(TimeSpan.FromSeconds(1)).SetFailSafe(true)); + Thread.Sleep(1_100); + var newValue = cache.GetOrSet<int>("foo", _ => { Thread.Sleep(1_000); return 21; }, new FusionCacheEntryOptions(TimeSpan.FromSeconds(1)).SetFailSafe(true).SetFactoryTimeoutsMs(100)); + Assert.Equal(initialValue, newValue); + } + + [Fact] + public async Task DoesNotSoftTimeoutWithoutStaleDataAsync() + { + using var cache = new FusionCache(new FusionCacheOptions()); + var initialValue = await cache.GetOrSetAsync<int>("foo", async _ => { await Task.Delay(1_000); return 21; }, new FusionCacheEntryOptions(TimeSpan.FromSeconds(1)).SetFailSafe(true).SetFactoryTimeoutsMs(100)); + Assert.Equal(21, initialValue); + } + + [Fact] + public void DoesNotSoftTimeoutWithoutStaleData() + { + using var cache = new FusionCache(new FusionCacheOptions()); + var initialValue = cache.GetOrSet<int>("foo", _ => { Thread.Sleep(1_000); return 21; }, new FusionCacheEntryOptions(TimeSpan.FromSeconds(1)).SetFailSafe(true).SetFactoryTimeoutsMs(100)); + Assert.Equal(21, initialValue); + } + + [Fact] + public async Task DoesHardTimeoutEvenWithoutStaleDataAsync() + { + using var cache = new FusionCache(new FusionCacheOptions()); + await Assert.ThrowsAnyAsync<Exception>(async () => + { + var initialValue = await cache.GetOrSetAsync<int>("foo", async _ => { await Task.Delay(1_000); return 21; }, new FusionCacheEntryOptions(TimeSpan.FromSeconds(1)).SetFailSafe(true).SetFactoryTimeoutsMs(100, 500)); + }); + } + + [Fact] + public void DoesHardTimeoutEvenWithoutStaleData() + { + using var cache = new FusionCache(new FusionCacheOptions()); + Assert.ThrowsAny<Exception>(() => + { + var initialValue = cache.GetOrSet<int>("foo", _ => { Thread.Sleep(1_000); return 21; }, new FusionCacheEntryOptions(TimeSpan.FromSeconds(1)).SetFailSafe(true).SetFactoryTimeoutsMs(100, 500)); + }); + } + + [Fact] + public async Task ReturnsStaleDataWhenFactoryHitHardTimeoutWithFailSafeAsync() + { + using var cache = new FusionCache(new FusionCacheOptions()); + await cache.SetAsync<int>("foo", 42, new FusionCacheEntryOptions(TimeSpan.FromSeconds(1)) { IsFailSafeEnabled = true }); + await Task.Delay(1_100); + var newValue = await cache.GetOrSetAsync<int>("foo", async _ => { await Task.Delay(1_000); return 21; }, new FusionCacheEntryOptions(TimeSpan.FromSeconds(1)).SetFailSafe(true).SetFactoryTimeoutsMs(100, 500)); + Assert.Equal(42, newValue); + } + + [Fact] + public void ReturnsStaleDataWhenFactoryHitHardTimeoutWithFailSafe() + { + using var cache = new FusionCache(new FusionCacheOptions()); + cache.Set<int>("foo", 42, new FusionCacheEntryOptions(TimeSpan.FromSeconds(1)) { IsFailSafeEnabled = true }); + Thread.Sleep(1_100); + var newValue = cache.GetOrSet<int>("foo", _ => { Thread.Sleep(1_000); return 21; }, new FusionCacheEntryOptions(TimeSpan.FromSeconds(1)).SetFailSafe(true).SetFactoryTimeoutsMs(100, 500)); + Assert.Equal(42, newValue); + } + + [Fact] + public async Task SetOverwritesAnExistingValueAsync() + { + using var cache = new FusionCache(new FusionCacheOptions()); + var initialValue = 42; + var newValue = 21; + cache.Set<int>("foo", initialValue, new FusionCacheEntryOptions(TimeSpan.FromSeconds(10))); + cache.Set<int>("foo", newValue, new FusionCacheEntryOptions(TimeSpan.FromSeconds(10))); + var actualValue = await cache.GetOrDefaultAsync<int>("foo", -1, new FusionCacheEntryOptions(TimeSpan.FromSeconds(10))); + Assert.Equal(newValue, actualValue); + } + + [Fact] + public void SetOverwritesAnExistingValue() + { + using var cache = new FusionCache(new FusionCacheOptions()); + var initialValue = 42; + var newValue = 21; + cache.Set<int>("foo", initialValue, new FusionCacheEntryOptions(TimeSpan.FromSeconds(10))); + cache.Set<int>("foo", newValue, new FusionCacheEntryOptions(TimeSpan.FromSeconds(10))); + var actualValue = cache.GetOrDefault<int>("foo", -1, new FusionCacheEntryOptions(TimeSpan.FromSeconds(10))); + Assert.Equal(newValue, actualValue); + } + + [Fact] + public async Task GetOrSetDoesNotOverwriteANonExpiredValueAsync() + { + using var cache = new FusionCache(new FusionCacheOptions()); + var initialValue = await cache.GetOrSetAsync<int>("foo", async _ => 42, new FusionCacheEntryOptions(TimeSpan.FromSeconds(10))); + var newValue = await cache.GetOrSetAsync<int>("foo", async _ => 21, new FusionCacheEntryOptions(TimeSpan.FromSeconds(10))); + Assert.Equal(initialValue, newValue); + } + + [Fact] + public void GetOrSetDoesNotOverwriteANonExpiredValue() + { + using var cache = new FusionCache(new FusionCacheOptions()); + var initialValue = cache.GetOrSet<int>("foo", _ => 42, new FusionCacheEntryOptions(TimeSpan.FromSeconds(10))); + var newValue = cache.GetOrSet<int>("foo", _ => 21, new FusionCacheEntryOptions(TimeSpan.FromSeconds(10))); + Assert.Equal(initialValue, newValue); + } + + [Fact] + public async Task DoesNotReturnStaleDataIfFactorySucceedsAsync() + { + using var cache = new FusionCache(new FusionCacheOptions()); + var initialValue = await cache.GetOrSetAsync<int>("foo", async _ => 42, new FusionCacheEntryOptions(TimeSpan.FromSeconds(1)) { IsFailSafeEnabled = true }); + await Task.Delay(1_500); + var newValue = await cache.GetOrSetAsync<int>("foo", async _ => 21, new FusionCacheEntryOptions(TimeSpan.FromSeconds(1)) { IsFailSafeEnabled = true }); + Assert.NotEqual(initialValue, newValue); + } + + [Fact] + public void DoesNotReturnStaleDataIfFactorySucceeds() + { + using var cache = new FusionCache(new FusionCacheOptions()); + var initialValue = cache.GetOrSet<int>("foo", _ => 42, new FusionCacheEntryOptions(TimeSpan.FromSeconds(1)) { IsFailSafeEnabled = true }); + Thread.Sleep(1_500); + var newValue = cache.GetOrSet<int>("foo", _ => 21, new FusionCacheEntryOptions(TimeSpan.FromSeconds(1)) { IsFailSafeEnabled = true }); + Assert.NotEqual(initialValue, newValue); + } + + [Fact] + public async Task GetOrDefaultDoesReturnStaleDataWithFailSafeAsync() + { + using var cache = new FusionCache(new FusionCacheOptions()); + var initialValue = 42; + await cache.SetAsync<int>("foo", initialValue, new FusionCacheEntryOptions(TimeSpan.FromSeconds(1)) { IsFailSafeEnabled = true }); + await Task.Delay(1_500); + var newValue = await cache.GetOrDefaultAsync<int>("foo", options: new FusionCacheEntryOptions(TimeSpan.FromSeconds(1)) { IsFailSafeEnabled = true }); + Assert.Equal(initialValue, newValue); + } + + [Fact] + public void GetOrDefaultDoesReturnStaleDataWithFailSafe() + { + using var cache = new FusionCache(new FusionCacheOptions()); + var initialValue = 42; + cache.Set<int>("foo", initialValue, new FusionCacheEntryOptions(TimeSpan.FromSeconds(1)) { IsFailSafeEnabled = true }); + Thread.Sleep(1_500); + var newValue = cache.GetOrDefault<int>("foo", options: new FusionCacheEntryOptions(TimeSpan.FromSeconds(1)) { IsFailSafeEnabled = true }); + Assert.Equal(initialValue, newValue); + } + + [Fact] + public async Task GetOrDefaultDoesNotReturnStaleDataWithoutFailSafeAsync() + { + using var cache = new FusionCache(new FusionCacheOptions()); + var initialValue = 42; + await cache.SetAsync<int>("foo", initialValue, new FusionCacheEntryOptions(TimeSpan.FromSeconds(1)) { IsFailSafeEnabled = true }); + await Task.Delay(1_500); + var newValue = await cache.GetOrDefaultAsync<int>("foo", options: new FusionCacheEntryOptions(TimeSpan.FromSeconds(1)) { IsFailSafeEnabled = false }); + Assert.NotEqual(initialValue, newValue); + } + + [Fact] + public void GetOrDefaultDoesNotReturnStaleWithoutFailSafe() + { + using var cache = new FusionCache(new FusionCacheOptions()); + var initialValue = 42; + cache.Set<int>("foo", initialValue, new FusionCacheEntryOptions(TimeSpan.FromSeconds(1)).SetFailSafe(true)); + Thread.Sleep(1_500); + var newValue = cache.GetOrDefault<int>("foo", options: new FusionCacheEntryOptions(TimeSpan.FromSeconds(1)).SetFailSafe(false)); + Assert.NotEqual(initialValue, newValue); + } + + [Fact] + public async Task FactoryTimedOutButSuccessfulDoesUpdateCachedValueAsync() + { + using var cache = new FusionCache(new FusionCacheOptions()); + await cache.SetAsync<int>("foo", 42, new FusionCacheEntryOptions(TimeSpan.FromSeconds(1)).SetFailSafe(true, TimeSpan.FromMinutes(1))); + var initialValue = cache.GetOrDefault<int>("foo", options: new FusionCacheEntryOptions(TimeSpan.FromSeconds(1)).SetFailSafe(true, TimeSpan.FromMinutes(1))); + await Task.Delay(1_500); + var middleValue = await cache.GetOrSetAsync<int>("foo", async ct => { await Task.Delay(2_000); ct.ThrowIfCancellationRequested(); return 21; }, new FusionCacheEntryOptions(TimeSpan.FromSeconds(1)).SetFailSafe(true).SetFactoryTimeoutsMs(500)); + var interstitialValue = await cache.GetOrDefaultAsync<int>("foo", options: new FusionCacheEntryOptions(TimeSpan.FromSeconds(1)).SetFailSafe(true)); + await Task.Delay(3_000); + var finalValue = await cache.GetOrDefaultAsync<int>("foo", options: new FusionCacheEntryOptions(TimeSpan.FromSeconds(1)).SetFailSafe(true)); + + Assert.Equal(42, initialValue); + Assert.Equal(42, middleValue); + Assert.Equal(42, interstitialValue); + Assert.Equal(21, finalValue); + } + + [Fact] + public void FactoryTimedOutButSuccessfulDoesUpdateCachedValue() + { + using var cache = new FusionCache(new FusionCacheOptions()); + cache.Set<int>("foo", 42, new FusionCacheEntryOptions(TimeSpan.FromSeconds(1)).SetFailSafe(true, TimeSpan.FromMinutes(1))); + var initialValue = cache.GetOrDefault<int>("foo", options: new FusionCacheEntryOptions(TimeSpan.FromSeconds(1)).SetFailSafe(true, TimeSpan.FromMinutes(1))); + Thread.Sleep(1_500); + var middleValue = cache.GetOrSet<int>("foo", ct => { Thread.Sleep(2_000); ct.ThrowIfCancellationRequested(); return 21; }, new FusionCacheEntryOptions(TimeSpan.FromSeconds(1)).SetFailSafe(true).SetFactoryTimeoutsMs(500)); + var interstitialValue = cache.GetOrDefault<int>("foo", options: new FusionCacheEntryOptions(TimeSpan.FromSeconds(1)).SetFailSafe(true)); + Thread.Sleep(3_000); + var finalValue = cache.GetOrDefault<int>("foo", options: new FusionCacheEntryOptions(TimeSpan.FromSeconds(1)).SetFailSafe(true)); + + Assert.Equal(42, initialValue); + Assert.Equal(42, middleValue); + Assert.Equal(42, interstitialValue); + Assert.Equal(21, finalValue); + } + + [Fact] + public async Task TryGetReturnsCorrectlyAsync() + { + using var cache = new FusionCache(new FusionCacheOptions()); + var res1 = await cache.TryGetAsync<int>("foo"); + await cache.SetAsync<int>("foo", 42); + var res2 = await cache.TryGetAsync<int>("foo"); + Assert.False(res1.HasValue); + Assert.Throws<InvalidOperationException>(() => + { + var foo = res1.Value; + }); + Assert.True(res2.HasValue); + Assert.Equal(42, res2.Value); + } + + [Fact] + public void TryGetReturnsCorrectly() + { + using var cache = new FusionCache(new FusionCacheOptions()); + var res1 = cache.TryGet<int>("foo"); + cache.Set<int>("foo", 42); + var res2 = cache.TryGet<int>("foo"); + Assert.False(res1.HasValue); + Assert.Throws<InvalidOperationException>(() => + { + var foo = res1.Value; + }); + Assert.True(res2.HasValue); + Assert.Equal(42, res2.Value); + } + + [Fact] + public async Task CancelingAnOperationActuallyCancelsItAsync() + { + using var cache = new FusionCache(new FusionCacheOptions()); + int res = -1; + var sw = Stopwatch.StartNew(); + var outerCancelDelayMs = 500; + var factoryDelayMs = 2_000; + await Assert.ThrowsAnyAsync<OperationCanceledException>(async () => + { + var cts = new CancellationTokenSource(outerCancelDelayMs); + res = await cache.GetOrSetAsync<int>("foo", async ct => { await Task.Delay(factoryDelayMs); ct.ThrowIfCancellationRequested(); return 42; }, options => options.SetDurationSec(60), cts.Token); + }); + sw.Stop(); + + TestOutput.WriteLine($"Outer Cancel: {outerCancelDelayMs} ms"); + TestOutput.WriteLine($"Factory Delay: {factoryDelayMs} ms"); + TestOutput.WriteLine($"Elapsed: {sw.ElapsedMilliseconds} ms"); + + Assert.Equal(-1, res); + Assert.True(sw.ElapsedMilliseconds >= outerCancelDelayMs, "Elapsed is less than outer cancel"); + Assert.True(sw.ElapsedMilliseconds < factoryDelayMs, "Elapsed is not less than factory delay"); + } + + [Fact] + public void CancelingAnOperationActuallyCancelsIt() + { + using var cache = new FusionCache(new FusionCacheOptions()); + int res = -1; + var sw = Stopwatch.StartNew(); + var outerCancelDelayMs = 500; + var factoryDelayMs = 2_000; + Assert.ThrowsAny<OperationCanceledException>(() => + { + var cts = new CancellationTokenSource(outerCancelDelayMs); + res = cache.GetOrSet<int>("foo", ct => { Thread.Sleep(factoryDelayMs); ct.ThrowIfCancellationRequested(); return 42; }, options => options.SetDurationSec(60), cts.Token); + }); + sw.Stop(); + + TestOutput.WriteLine($"Outer Cancel: {outerCancelDelayMs} ms"); + TestOutput.WriteLine($"Factory Delay: {factoryDelayMs} ms"); + TestOutput.WriteLine($"Elapsed: {sw.ElapsedMilliseconds} ms"); + + Assert.Equal(-1, res); + Assert.True(sw.ElapsedMilliseconds >= outerCancelDelayMs, "Elapsed is less than outer cancel"); + Assert.True(sw.ElapsedMilliseconds < factoryDelayMs, "Elapsed is not less than factory delay"); + } + + [Fact] + public async Task HandlesFlexibleSimpleTypeConversionsAsync() + { + using var cache = new FusionCache(new FusionCacheOptions()); + var initialValue = (object)42; + await cache.SetAsync("foo", initialValue, TimeSpan.FromHours(24)); + var newValue = await cache.GetOrDefaultAsync<int>("foo"); + Assert.Equal(initialValue, newValue); + } + + [Fact] + public void HandlesFlexibleSimpleTypeConversions() + { + using var cache = new FusionCache(new FusionCacheOptions()); + var initialValue = (object)42; + cache.Set("foo", initialValue, TimeSpan.FromHours(24)); + var newValue = cache.GetOrDefault<int>("foo"); + Assert.Equal(initialValue, newValue); + } + + [Fact] + public async Task HandlesFlexibleComplexTypeConversionsAsync() + { + using var cache = new FusionCache(new FusionCacheOptions()); + var initialValue = (object)ComplexType.CreateSample(); + await cache.SetAsync("foo", initialValue, TimeSpan.FromHours(24)); + var newValue = await cache.GetOrDefaultAsync<ComplexType>("foo"); + Assert.Equal(initialValue, newValue); + } + + [Fact] + public void HandlesFlexibleComplexTypeConversions() + { + using var cache = new FusionCache(new FusionCacheOptions()); + var initialValue = (object)ComplexType.CreateSample(); + cache.Set("foo", initialValue, TimeSpan.FromHours(24)); + var newValue = cache.GetOrDefault<ComplexType>("foo"); + Assert.Equal(initialValue, newValue); + } + + [Fact] + public async Task GetOrDefaultDoesNotSetAsync() + { + using var cache = new FusionCache(new FusionCacheOptions()); + var foo = await cache.GetOrDefaultAsync<int>("foo", 42, opt => opt.SetDuration(TimeSpan.FromHours(24))); + var bar = await cache.GetOrDefaultAsync<int>("foo", 21, opt => opt.SetDuration(TimeSpan.FromHours(24))); + var baz = await cache.TryGetAsync<int>("foo", opt => opt.SetDuration(TimeSpan.FromHours(24))); + Assert.Equal(42, foo); + Assert.Equal(21, bar); + Assert.False(baz.HasValue); + } + + [Fact] + public void GetOrDefaultDoesNotSet() + { + using var cache = new FusionCache(new FusionCacheOptions()); + var foo = cache.GetOrDefault<int>("foo", 42, opt => opt.SetDuration(TimeSpan.FromHours(24))); + var bar = cache.GetOrDefault<int>("foo", 21, opt => opt.SetDuration(TimeSpan.FromHours(24))); + var baz = cache.TryGet<int>("foo", opt => opt.SetDuration(TimeSpan.FromHours(24))); + Assert.Equal(42, foo); + Assert.Equal(21, bar); + Assert.False(baz.HasValue); + } + + [Fact] + public async Task GetOrSetWithDefaultValueWorksAsync() + { + using var cache = new FusionCache(new FusionCacheOptions()); + var foo = 42; + await cache.GetOrSetAsync<int>("foo", foo, TimeSpan.FromHours(24)); + var bar = await cache.GetOrDefaultAsync<int>("foo", 21); + Assert.Equal(foo, bar); + } + + [Fact] + public void GetOrSetWithDefaultValueWorks() + { + using var cache = new FusionCache(new FusionCacheOptions()); + var foo = 42; + cache.GetOrSet<int>("foo", foo, TimeSpan.FromHours(24)); + var bar = cache.GetOrDefault<int>("foo", 21); + Assert.Equal(foo, bar); + } + + [Fact] + public async Task ThrottleDurationWorksCorrectlyAsync() + { + using var cache = new FusionCache(new FusionCacheOptions()); + var duration = TimeSpan.FromSeconds(1); + var throttleDuration = TimeSpan.FromSeconds(3); + + // SET THE VALUE (WITH FAIL-SAFE ENABLED) + await cache.SetAsync("foo", 42, opt => opt.SetDuration(duration).SetFailSafe(true, throttleDuration: throttleDuration)); + // LET IT EXPIRE + await Task.Delay(duration.PlusALittleBit()); + // CHECK EXPIRED (WITHOUT FAIL-SAFE) + var nope = await cache.TryGetAsync<int>("foo", opt => opt.SetFailSafe(false)); + // DO NOT ACTIVATE FAIL-SAFE AND THROTTLE DURATION + var default1 = await cache.GetOrDefaultAsync("foo", 1); + // ACTIVATE FAIL-SAFE AND RE-STORE THE VALUE WITH THROTTLE DURATION + var throttled1 = await cache.GetOrDefaultAsync("foo", 1, opt => opt.SetFailSafe(true, throttleDuration: throttleDuration)); + // WAIT A LITTLE BIT (LESS THAN THE DURATION) + await Task.Delay(100); + // GET THE THROTTLED (NON EXPIRED) VALUE + var throttled2 = await cache.GetOrDefaultAsync("foo", 2, opt => opt.SetFailSafe(true)); + // LET THE THROTTLE DURATION PASS + await Task.Delay(throttleDuration); + // FALLBACK TO THE DEFAULT VALUE + var default3 = await cache.GetOrDefaultAsync("foo", 3, opt => opt.SetFailSafe(false)); + + Assert.False(nope.HasValue); + Assert.Equal(1, default1); + Assert.Equal(42, throttled1); + Assert.Equal(42, throttled2); + Assert.Equal(3, default3); + } + + [Fact] + public void ThrottleDurationWorksCorrectly() + { + using var cache = new FusionCache(new FusionCacheOptions()); + var duration = TimeSpan.FromSeconds(1); + var throttleDuration = TimeSpan.FromSeconds(3); + + // SET THE VALUE (WITH FAIL-SAFE ENABLED) + cache.Set("foo", 42, opt => opt.SetDuration(duration).SetFailSafe(true, throttleDuration: throttleDuration)); + // LET IT EXPIRE + Thread.Sleep(duration.PlusALittleBit()); + // CHECK EXPIRED (WITHOUT FAIL-SAFE) + var nope = cache.TryGet<int>("foo", opt => opt.SetFailSafe(false)); + // DO NOT ACTIVATE FAIL-SAFE AND THROTTLE DURATION + var default1 = cache.GetOrDefault("foo", 1); + // ACTIVATE FAIL-SAFE AND RE-STORE THE VALUE WITH THROTTLE DURATION + var throttled1 = cache.GetOrDefault("foo", 1, opt => opt.SetFailSafe(true, throttleDuration: throttleDuration)); + // WAIT A LITTLE BIT (LESS THAN THE DURATION) + Thread.Sleep(100); + // GET THE THROTTLED (NON EXPIRED) VALUE + var throttled2 = cache.GetOrDefault("foo", 2, opt => opt.SetFailSafe(true)); + // LET THE THROTTLE DURATION PASS + Thread.Sleep(throttleDuration); + // FALLBACK TO THE DEFAULT VALUE + var default3 = cache.GetOrDefault("foo", 3, opt => opt.SetFailSafe(false)); + + Assert.False(nope.HasValue); + Assert.Equal(1, default1); + Assert.Equal(42, throttled1); + Assert.Equal(42, throttled2); + Assert.Equal(3, default3); + } + + [Fact] + public async Task AdaptiveCachingAsync() + { + using var cache = new FusionCache(new FusionCacheOptions()); + var dur = TimeSpan.FromMinutes(5); + cache.DefaultEntryOptions.Duration = dur; + FusionCacheEntryOptions? innerOpt = null; + + var default3 = await cache.GetOrSetAsync<int>( + "foo", + async (ctx, _) => + { + ctx.Options.Duration = TimeSpan.FromSeconds(1); + + innerOpt = ctx.Options; + + return 3; + }, + opt => opt.SetFailSafe(false) + ); + + await Task.Delay(TimeSpan.FromSeconds(2)); + + var maybeValue = await cache.TryGetAsync<int>("foo"); + + Assert.Equal(dur, TimeSpan.FromMinutes(5)); + Assert.Equal(cache.DefaultEntryOptions.Duration, TimeSpan.FromMinutes(5)); + Assert.Equal(innerOpt!.Duration, TimeSpan.FromSeconds(1)); + Assert.False(maybeValue.HasValue); + } + + [Fact] + public void AdaptiveCaching() + { + using var cache = new FusionCache(new FusionCacheOptions()); + var dur = TimeSpan.FromMinutes(5); + cache.DefaultEntryOptions.Duration = dur; + FusionCacheEntryOptions? innerOpt = null; + + var default3 = cache.GetOrSet<int>( + "foo", + (ctx, _) => + { + ctx.Options.Duration = TimeSpan.FromSeconds(1); + + innerOpt = ctx.Options; + + return 3; + }, + opt => opt.SetFailSafe(false) + ); + + Thread.Sleep(TimeSpan.FromSeconds(2)); + + var maybeValue = cache.TryGet<int>("foo"); + + Assert.Equal(dur, TimeSpan.FromMinutes(5)); + Assert.Equal(cache.DefaultEntryOptions.Duration, TimeSpan.FromMinutes(5)); + Assert.Equal(innerOpt!.Duration, TimeSpan.FromSeconds(1)); + Assert.False(maybeValue.HasValue); + } + + [Fact] + public async Task AdaptiveCachingWithBackgroundFactoryCompletionAsync() + { + using var cache = new FusionCache(new FusionCacheOptions()); + var dur = TimeSpan.FromMinutes(5); + cache.DefaultEntryOptions.Duration = dur; + + // SET WITH 1s DURATION + FAIL-SAFE + await cache.SetAsync("foo", 21, options => options.SetDuration(TimeSpan.FromSeconds(1)).SetFailSafe(true)); + + // LET IT BECOME STALE + await Task.Delay(TimeSpan.FromSeconds(2)); + + // CALL GetOrSET WITH A 1s SOFT TIMEOUT AND A FACTORY RUNNING FOR AT LEAST 3s + var value21 = await cache.GetOrSetAsync<int>( + "foo", + async (ctx, _) => + { + // WAIT 3s + await Task.Delay(TimeSpan.FromSeconds(3)); + + // CHANGE THE OPTIONS (SET THE DURATION TO 5s AND DISABLE FAIL-SAFE + ctx.Options.SetDuration(TimeSpan.FromSeconds(5)).SetFailSafe(false); + + return 42; + }, + opt => opt.SetFactoryTimeouts(TimeSpan.FromSeconds(1)).SetFailSafe(true) + ); + + // WAIT FOR 3s (+ EXTRA 1s) SO THE FACTORY COMPLETES IN THE BACKGROUND + await Task.Delay(TimeSpan.FromSeconds(3 + 1)); + + // GET THE VALUE THAT HAS BEEN SET BY THE BACKGROUND COMPLETION OF THE FACTORY + var value42 = await cache.GetOrDefaultAsync<int>("foo", options => options.SetFailSafe(false)); + + // LET THE CACHE ENTRY EXPIRES + await Task.Delay(TimeSpan.FromSeconds(5)); + + // SEE THAT FAIL-SAFE CANNOT BE ACTIVATED (BECAUSE IT WAS DISABLED IN THE FACTORY) + var noValue = await cache.TryGetAsync<int>("foo", options => options.SetFailSafe(true)); + + Assert.Equal(dur, TimeSpan.FromMinutes(5)); + Assert.Equal(cache.DefaultEntryOptions.Duration, TimeSpan.FromMinutes(5)); + Assert.Equal(21, value21); + Assert.Equal(42, value42); + Assert.False(noValue.HasValue); + } + + [Fact] + public void AdaptiveCachingWithBackgroundFactoryCompletion() + { + using var cache = new FusionCache(new FusionCacheOptions()); + var dur = TimeSpan.FromMinutes(5); + cache.DefaultEntryOptions.Duration = dur; + + // SET WITH 1s DURATION + FAIL-SAFE + cache.Set("foo", 21, options => options.SetDuration(TimeSpan.FromSeconds(1)).SetFailSafe(true)); + + // LET IT BECOME STALE + Thread.Sleep(TimeSpan.FromSeconds(2)); + + // CALL GetOrSET WITH A 1s SOFT TIMEOUT AND A FACTORY RUNNING FOR AT LEAST 3s + var value21 = cache.GetOrSet<int>( + "foo", + (ctx, _) => + { + // WAIT 3s + Thread.Sleep(TimeSpan.FromSeconds(3)); + + // CHANGE THE OPTIONS (SET THE DURATION TO 5s AND DISABLE FAIL-SAFE + ctx.Options.SetDuration(TimeSpan.FromSeconds(5)).SetFailSafe(false); + + return 42; + }, + opt => opt.SetFactoryTimeouts(TimeSpan.FromSeconds(1)).SetFailSafe(true) + ); + + // WAIT FOR 3s (+ EXTRA 1s) SO THE FACTORY COMPLETES IN THE BACKGROUND + Thread.Sleep(TimeSpan.FromSeconds(3 + 1)); + + // GET THE VALUE THAT HAS BEEN SET BY THE BACKGROUND COMPLETION OF THE FACTORY + var value42 = cache.GetOrDefault<int>("foo", options => options.SetFailSafe(false)); + + // LET THE CACHE ENTRY EXPIRES + Thread.Sleep(TimeSpan.FromSeconds(5)); + + // SEE THAT FAIL-SAFE CANNOT BE ACTIVATED (BECAUSE IT WAS DISABLED IN THE FACTORY) + var noValue = cache.TryGet<int>("foo", options => options.SetFailSafe(true)); + + Assert.Equal(dur, TimeSpan.FromMinutes(5)); + Assert.Equal(cache.DefaultEntryOptions.Duration, TimeSpan.FromMinutes(5)); + Assert.Equal(21, value21); + Assert.Equal(42, value42); + Assert.False(noValue.HasValue); + } + + [Fact] + public async Task AdaptiveCachingDoesNotChangeOptionsAsync() + { + using var cache = new FusionCache(new FusionCacheOptions()); + var options = new FusionCacheEntryOptions(TimeSpan.FromSeconds(10)); + + _ = await cache.GetOrSetAsync<int>( + "foo", + async (ctx, _) => + { + ctx.Options.Duration = TimeSpan.FromSeconds(20); + return 42; + }, + options + ); + + Assert.Equal(options.Duration, TimeSpan.FromSeconds(10)); + } + + [Fact] + public void AdaptiveCachingDoesNotChangeOptions() + { + using var cache = new FusionCache(new FusionCacheOptions()); + var options = new FusionCacheEntryOptions(TimeSpan.FromSeconds(10)); + + _ = cache.GetOrSet<int>( + "foo", + (ctx, _) => + { + ctx.Options.Duration = TimeSpan.FromSeconds(20); + return 42; + }, + options + ); + + Assert.Equal(options.Duration, TimeSpan.FromSeconds(10)); + } + + [Fact] + public async Task AdaptiveCachingCanWorkWithSkipMemoryCacheAsync() + { + using var cache = new FusionCache(new FusionCacheOptions()); + cache.DefaultEntryOptions.IsFailSafeEnabled = true; + cache.DefaultEntryOptions.Duration = TimeSpan.FromSeconds(1); + cache.DefaultEntryOptions.FailSafeThrottleDuration = TimeSpan.FromSeconds(3); + + var foo1 = await cache.GetOrSetAsync<int>("foo", async _ => 1); + + await Task.Delay(TimeSpan.FromSeconds(1).PlusALittleBit()); + + var foo2 = await cache.GetOrSetAsync<int>("foo", async (ctx, _) => + { + ctx.Options.SkipMemoryCache = true; + + return 2; + }); + + var foo3 = await cache.TryGetAsync<int>("foo"); + + await Task.Delay(cache.DefaultEntryOptions.FailSafeThrottleDuration.PlusALittleBit()); + + var foo4 = await cache.GetOrSetAsync<int>("foo", async _ => 4); + + Assert.Equal(1, foo1); + Assert.Equal(2, foo2); + Assert.True(foo3.HasValue); + Assert.Equal(1, foo3.Value); + Assert.Equal(4, foo4); + } + + [Fact] + public void AdaptiveCachingCanWorkWithSkipMemoryCache() + { + using var cache = new FusionCache(new FusionCacheOptions()); + cache.DefaultEntryOptions.IsFailSafeEnabled = true; + cache.DefaultEntryOptions.Duration = TimeSpan.FromSeconds(1); + cache.DefaultEntryOptions.FailSafeThrottleDuration = TimeSpan.FromSeconds(3); + + var foo1 = cache.GetOrSet<int>("foo", _ => 1); + + Thread.Sleep(TimeSpan.FromSeconds(1).PlusALittleBit()); + + var foo2 = cache.GetOrSet<int>("foo", (ctx, _) => + { + ctx.Options.SkipMemoryCache = true; + + return 2; + }); + + var foo3 = cache.TryGet<int>("foo"); + + Thread.Sleep(cache.DefaultEntryOptions.FailSafeThrottleDuration.PlusALittleBit()); + + var foo4 = cache.GetOrSet<int>("foo", _ => 4); + + Assert.Equal(1, foo1); + Assert.Equal(2, foo2); + Assert.True(foo3.HasValue); + Assert.Equal(1, foo3.Value); + Assert.Equal(4, foo4); + } + + [Fact] + public async Task FailSafeMaxDurationNormalizationOccursAsync() + { + var duration = TimeSpan.FromSeconds(5); + var maxDuration = TimeSpan.FromSeconds(1); + + using var fusionCache = new FusionCache(new FusionCacheOptions()); + await fusionCache.SetAsync<int>("foo", 21, opt => opt.SetDuration(duration).SetFailSafe(true, maxDuration)); + await Task.Delay(maxDuration.PlusALittleBit()); + var value = await fusionCache.GetOrDefaultAsync<int>("foo", opt => opt.SetFailSafe(true)); + Assert.Equal(21, value); + } + + [Fact] + public void FailSafeMaxDurationNormalizationOccurs() + { + var duration = TimeSpan.FromSeconds(5); + var maxDuration = TimeSpan.FromSeconds(1); + + using var fusionCache = new FusionCache(new FusionCacheOptions()); + fusionCache.Set<int>("foo", 21, opt => opt.SetDuration(duration).SetFailSafe(true, maxDuration)); + Thread.Sleep(maxDuration.PlusALittleBit()); + var value = fusionCache.GetOrDefault<int>("foo", opt => opt.SetFailSafe(true)); + Assert.Equal(21, value); + } + + [Fact] + public async Task ReturnsStaleDataWithoutSavingItWhenNoFactoryAsync() + { + using var cache = new FusionCache(new FusionCacheOptions()); + var initialValue = await cache.GetOrSetAsync<int>("foo", async _ => 42, new FusionCacheEntryOptions(TimeSpan.FromSeconds(1)).SetFailSafe(true, TimeSpan.FromSeconds(10), TimeSpan.FromSeconds(30))); + await Task.Delay(1_500); + var maybeValue = await cache.TryGetAsync<int>("foo", opt => opt.SetDuration(TimeSpan.FromSeconds(1)).SetFailSafe(true)); + var defaultValue1 = await cache.GetOrDefaultAsync<int>("foo", 1); + var defaultValue2 = await cache.GetOrDefaultAsync<int>("foo", 2, opt => opt.SetDuration(TimeSpan.FromSeconds(1)).SetFailSafe(true)); + var defaultValue3 = await cache.GetOrDefaultAsync<int>("foo", 3); + + Assert.True(maybeValue.HasValue); + Assert.Equal(42, maybeValue.Value); + Assert.Equal(1, defaultValue1); + Assert.Equal(42, defaultValue2); + Assert.Equal(3, defaultValue3); + } + + [Fact] + public void ReturnsStaleDataWithoutSavingItWhenNoFactory() + { + using var cache = new FusionCache(new FusionCacheOptions()); + var initialValue = cache.GetOrSet<int>("foo", _ => 42, new FusionCacheEntryOptions(TimeSpan.FromSeconds(1)).SetFailSafe(true, TimeSpan.FromSeconds(10), TimeSpan.FromSeconds(30))); + Thread.Sleep(1_500); + var maybeValue = cache.TryGet<int>("foo", opt => opt.SetDuration(TimeSpan.FromSeconds(1)).SetFailSafe(true)); + var defaultValue1 = cache.GetOrDefault<int>("foo", 1); + var defaultValue2 = cache.GetOrDefault<int>("foo", 2, opt => opt.SetDuration(TimeSpan.FromSeconds(1)).SetFailSafe(true)); + var defaultValue3 = cache.GetOrDefault<int>("foo", 3); + + Assert.True(maybeValue.HasValue); + Assert.Equal(42, maybeValue.Value); + Assert.Equal(1, defaultValue1); + Assert.Equal(42, defaultValue2); + Assert.Equal(3, defaultValue3); + } + + [Fact] + public async Task CanHandleInfiniteOrSimilarDurationsAsync() + { + using var cache = new FusionCache(new FusionCacheOptions()); + await cache.SetAsync<int>("foo", 42, opt => opt.SetDuration(TimeSpan.MaxValue - TimeSpan.FromMilliseconds(1)).SetJittering(TimeSpan.FromMinutes(10))); + var foo = await cache.GetOrDefaultAsync<int>("foo", 0); + Assert.Equal(42, foo); + } + + [Fact] + public void CanHandleInfiniteOrSimilarDurations() + { + using var cache = new FusionCache(new FusionCacheOptions()); + cache.Set<int>("foo", 42, opt => opt.SetDuration(TimeSpan.MaxValue - TimeSpan.FromMilliseconds(1)).SetJittering(TimeSpan.FromMinutes(10))); + var foo = cache.GetOrDefault<int>("foo", 0); + Assert.Equal(42, foo); + } + + [Fact] + public async Task CanHandleZeroDurationsAsync() + { + using var cache = new FusionCache(new FusionCacheOptions()); + + await cache.SetAsync<int>("foo", 10, opt => opt.SetDuration(TimeSpan.Zero)); + var foo1 = await cache.GetOrDefaultAsync<int>("foo", 1); + + await cache.SetAsync<int>("foo", 20, opt => opt.SetDuration(TimeSpan.FromMinutes(10))); + var foo2 = await cache.GetOrDefaultAsync<int>("foo", 2); + + await cache.SetAsync<int>("foo", 30, opt => opt.SetDuration(TimeSpan.Zero)); + var foo3 = await cache.GetOrDefaultAsync<int>("foo", 3); + + Assert.Equal(1, foo1); + Assert.Equal(20, foo2); + Assert.Equal(3, foo3); + } + + [Fact] + public void CanHandleZeroDurations() + { + using var cache = new FusionCache(new FusionCacheOptions()); + + cache.Set<int>("foo", 10, opt => opt.SetDuration(TimeSpan.Zero)); + var foo1 = cache.GetOrDefault<int>("foo", 1); + + cache.Set<int>("foo", 20, opt => opt.SetDuration(TimeSpan.FromMinutes(10))); + var foo2 = cache.GetOrDefault<int>("foo", 2); + + cache.Set<int>("foo", 30, opt => opt.SetDuration(TimeSpan.Zero)); + var foo3 = cache.GetOrDefault<int>("foo", 3); + + Assert.Equal(1, foo1); + Assert.Equal(20, foo2); + Assert.Equal(3, foo3); + } + + [Fact] + public async Task CanHandleNegativeDurationsAsync() + { + using var cache = new FusionCache(new FusionCacheOptions()); + + await cache.SetAsync<int>("foo", 10, opt => opt.SetDuration(TimeSpan.FromSeconds(-100))); + var foo1 = await cache.GetOrDefaultAsync<int>("foo", 1); + + await cache.SetAsync<int>("foo", 20, opt => opt.SetDuration(TimeSpan.FromMinutes(10))); + var foo2 = await cache.GetOrDefaultAsync<int>("foo", 2); + + await cache.SetAsync<int>("foo", 30, opt => opt.SetDuration(TimeSpan.FromDays(-100))); + var foo3 = await cache.GetOrDefaultAsync<int>("foo", 3); + + Assert.Equal(1, foo1); + Assert.Equal(20, foo2); + Assert.Equal(3, foo3); + } + + [Fact] + public void CanHandleNegativeDurations() + { + using var cache = new FusionCache(new FusionCacheOptions()); + + cache.Set<int>("foo", 10, opt => opt.SetDuration(TimeSpan.FromSeconds(-100))); + var foo1 = cache.GetOrDefault<int>("foo", 1); + + cache.Set<int>("foo", 20, opt => opt.SetDuration(TimeSpan.FromMinutes(10))); + var foo2 = cache.GetOrDefault<int>("foo", 2); + + cache.Set<int>("foo", 30, opt => opt.SetDuration(TimeSpan.FromDays(-100))); + var foo3 = cache.GetOrDefault<int>("foo", 3); + + Assert.Equal(1, foo1); + Assert.Equal(20, foo2); + Assert.Equal(3, foo3); + } + + [Fact] + public async Task CanHandleConditionalRefreshAsync() + { + static async Task<int> FakeGetAsync(FusionCacheFactoryExecutionContext<int> ctx, FakeHttpEndpoint endpoint) + { + FakeHttpResponse resp; + + if (ctx.HasETag && ctx.HasStaleValue) + { + // ETAG + STALE VALUE -> TRY WITH A CONDITIONAL GET + resp = endpoint.Get(ctx.ETag); + + if (resp.StatusCode == 304) + { + // NOT MODIFIED -> RETURN STALE VALUE + return ctx.NotModified(); + } + } + else + { + // NO STALE VALUE OR NO ETAG -> NORMAL (FULL) GET + resp = endpoint.Get(); + } + + return ctx.Modified( + resp.Content.GetValueOrDefault(), + resp.ETag + ); + } + + var duration = TimeSpan.FromSeconds(1); + var endpoint = new FakeHttpEndpoint(1); + + using var cache = new FusionCache(new FusionCacheOptions()); + // TOT REQ + 1 / FULL RESP + 1 + var v1 = await cache.GetOrSetAsync<int>("foo", async (ctx, _) => await FakeGetAsync(ctx, endpoint), opt => opt.SetDuration(duration).SetFailSafe(true)); + + // CACHED -> NO INCR + var v2 = await cache.GetOrSetAsync<int>("foo", async (ctx, _) => await FakeGetAsync(ctx, endpoint), opt => opt.SetDuration(duration).SetFailSafe(true)); + + // LET THE CACHE EXPIRE + await Task.Delay(duration.PlusALittleBit()); + + // TOT REQ + 1 / COND REQ + 1 / NOT MOD RESP + 1 + var v3 = await cache.GetOrSetAsync<int>("foo", async (ctx, _) => await FakeGetAsync(ctx, endpoint), opt => opt.SetDuration(duration).SetFailSafe(true)); + + // LET THE CACHE EXPIRE + await Task.Delay(duration.PlusALittleBit()); + + // TOT REQ + 1 / COND REQ + 1 / NOT MOD RESP + 1 + var v4 = await cache.GetOrSetAsync<int>("foo", async (ctx, _) => await FakeGetAsync(ctx, endpoint), opt => opt.SetDuration(duration).SetFailSafe(true)); + + // SET VALUE -> CHANGE LAST MODIFIED + endpoint.SetValue(42); + + // LET THE CACHE EXPIRE + await Task.Delay(duration.PlusALittleBit()); + + // TOT REQ + 1 / COND REQ + 1 / FULL RESP + 1 + var v5 = await cache.GetOrSetAsync<int>("foo", async (ctx, _) => await FakeGetAsync(ctx, endpoint), opt => opt.SetDuration(duration).SetFailSafe(true)); + + Assert.Equal(4, endpoint.TotalRequestsCount); + Assert.Equal(3, endpoint.ConditionalRequestsCount); + Assert.Equal(2, endpoint.FullResponsesCount); + Assert.Equal(2, endpoint.NotModifiedResponsesCount); + + Assert.Equal(1, v1); + Assert.Equal(1, v2); + Assert.Equal(1, v3); + Assert.Equal(1, v4); + Assert.Equal(42, v5); + } + + [Fact] + public void CanHandleConditionalRefresh() + { + static int FakeGet(FusionCacheFactoryExecutionContext<int> ctx, FakeHttpEndpoint endpoint) + { + FakeHttpResponse resp; + + if (ctx.HasETag && ctx.HasStaleValue) + { + // ETAG + STALE VALUE -> TRY WITH A CONDITIONAL GET + resp = endpoint.Get(ctx.ETag); + + if (resp.StatusCode == 304) + { + // NOT MODIFIED -> RETURN STALE VALUE + return ctx.NotModified(); + } + } + else + { + // NO STALE VALUE OR NO ETAG -> NORMAL (FULL) GET + resp = endpoint.Get(); + } + + return ctx.Modified( + resp.Content.GetValueOrDefault(), + resp.ETag + ); + } + + var duration = TimeSpan.FromSeconds(1); + var endpoint = new FakeHttpEndpoint(1); + + using var cache = new FusionCache(new FusionCacheOptions()); + // TOT REQ + 1 / FULL RESP + 1 + var v1 = cache.GetOrSet<int>("foo", (ctx, _) => FakeGet(ctx, endpoint), opt => opt.SetDuration(duration).SetFailSafe(true)); + + // CACHED -> NO INCR + var v2 = cache.GetOrSet<int>("foo", (ctx, _) => FakeGet(ctx, endpoint), opt => opt.SetDuration(duration).SetFailSafe(true)); + + // LET THE CACHE EXPIRE + Thread.Sleep(duration.PlusALittleBit()); + + // TOT REQ + 1 / COND REQ + 1 / NOT MOD RESP + 1 + var v3 = cache.GetOrSet<int>("foo", (ctx, _) => FakeGet(ctx, endpoint), opt => opt.SetDuration(duration).SetFailSafe(true)); + + // LET THE CACHE EXPIRE + Thread.Sleep(duration.PlusALittleBit()); + + // TOT REQ + 1 / COND REQ + 1 / NOT MOD RESP + 1 + var v4 = cache.GetOrSet<int>("foo", (ctx, _) => FakeGet(ctx, endpoint), opt => opt.SetDuration(duration).SetFailSafe(true)); + + // SET VALUE -> CHANGE LAST MODIFIED + endpoint.SetValue(42); + + // LET THE CACHE EXPIRE + Thread.Sleep(duration.PlusALittleBit()); + + // TOT REQ + 1 / COND REQ + 1 / FULL RESP + 1 + var v5 = cache.GetOrSet<int>("foo", (ctx, _) => FakeGet(ctx, endpoint), opt => opt.SetDuration(duration).SetFailSafe(true)); + + Assert.Equal(4, endpoint.TotalRequestsCount); + Assert.Equal(3, endpoint.ConditionalRequestsCount); + Assert.Equal(2, endpoint.FullResponsesCount); + Assert.Equal(2, endpoint.NotModifiedResponsesCount); + + Assert.Equal(1, v1); + Assert.Equal(1, v2); + Assert.Equal(1, v3); + Assert.Equal(1, v4); + Assert.Equal(42, v5); + } + + [Fact] + public async Task CanHandleEagerRefreshAsync() + { + var duration = TimeSpan.FromSeconds(2); + var eagerRefreshThreshold = 0.2f; + + using var cache = new FusionCache(new FusionCacheOptions(), logger: CreateXUnitLogger<FusionCache>()); + + cache.DefaultEntryOptions.Duration = duration; + cache.DefaultEntryOptions.EagerRefreshThreshold = eagerRefreshThreshold; + + // EXECUTE FACTORY + var v1 = await cache.GetOrSetAsync<long>("foo", async _ => DateTimeOffset.UtcNow.Ticks); + + // USE CACHED VALUE + var v2 = await cache.GetOrSetAsync<long>("foo", async _ => DateTimeOffset.UtcNow.Ticks); + + // WAIT FOR EAGER REFRESH THRESHOLD TO BE HIT + var eagerDuration = TimeSpan.FromMilliseconds(duration.TotalMilliseconds * eagerRefreshThreshold).Add(TimeSpan.FromMilliseconds(10)); + await Task.Delay(eagerDuration); + + // EAGER REFRESH KICKS IN + var v3 = await cache.GetOrSetAsync<long>("foo", async _ => DateTimeOffset.UtcNow.Ticks); + + // WAIT FOR THE BACKGROUND FACTORY (EAGER REFRESH) TO COMPLETE + await Task.Delay(TimeSpan.FromMilliseconds(250)); + + // GET THE REFRESHED VALUE + var v4 = await cache.GetOrSetAsync<long>("foo", async _ => DateTimeOffset.UtcNow.Ticks); + + // WAIT FOR EXPIRATION + await Task.Delay(duration.PlusALittleBit()); + + // EXECUTE FACTORY AGAIN + var v5 = await cache.GetOrSetAsync<long>("foo", async _ => DateTimeOffset.UtcNow.Ticks); + + // USE CACHED VALUE + var v6 = await cache.GetOrSetAsync<long>("foo", async _ => DateTimeOffset.UtcNow.Ticks); + + Assert.Equal(v1, v2); + Assert.Equal(v2, v3); + Assert.True(v4 > v3); + Assert.True(v5 > v4); + Assert.Equal(v5, v6); + } + + [Fact] + public void CanHandleEagerRefresh() + { + var duration = TimeSpan.FromSeconds(2); + var eagerRefreshThreshold = 0.2f; + + using var cache = new FusionCache(new FusionCacheOptions()); + + cache.DefaultEntryOptions.Duration = duration; + cache.DefaultEntryOptions.EagerRefreshThreshold = eagerRefreshThreshold; + + // EXECUTE FACTORY + var v1 = cache.GetOrSet<long>("foo", _ => DateTimeOffset.UtcNow.Ticks); + + // USE CACHED VALUE + var v2 = cache.GetOrSet<long>("foo", _ => DateTimeOffset.UtcNow.Ticks); + + // WAIT FOR EAGER REFRESH THRESHOLD TO BE HIT + var eagerDuration = TimeSpan.FromMilliseconds(duration.TotalMilliseconds * eagerRefreshThreshold).Add(TimeSpan.FromMilliseconds(10)); + Thread.Sleep(eagerDuration); + + // EAGER REFRESH KICKS IN + var v3 = cache.GetOrSet<long>("foo", _ => DateTimeOffset.UtcNow.Ticks); + + // WAIT FOR THE BACKGROUND FACTORY (EAGER REFRESH) TO COMPLETE + Thread.Sleep(TimeSpan.FromMilliseconds(250)); + + // GET THE REFRESHED VALUE + var v4 = cache.GetOrSet<long>("foo", _ => DateTimeOffset.UtcNow.Ticks); + + // WAIT FOR EXPIRATION + Thread.Sleep(duration.PlusALittleBit()); + + // EXECUTE FACTORY AGAIN + var v5 = cache.GetOrSet<long>("foo", _ => DateTimeOffset.UtcNow.Ticks); + + // USE CACHED VALUE + var v6 = cache.GetOrSet<long>("foo", _ => DateTimeOffset.UtcNow.Ticks); + + Assert.Equal(v1, v2); + Assert.Equal(v2, v3); + Assert.True(v4 > v3); + Assert.True(v5 > v4); + Assert.Equal(v5, v6); + } + + [Fact] + public async Task CanHandleEagerRefreshWithInfiniteDurationAsync() + { + var duration = TimeSpan.MaxValue; + var eagerRefreshThreshold = 0.5f; + + using var cache = new FusionCache(new FusionCacheOptions()); + + cache.DefaultEntryOptions.Duration = duration; + cache.DefaultEntryOptions.EagerRefreshThreshold = eagerRefreshThreshold; + + // EXECUTE FACTORY + var v1 = await cache.GetOrSetAsync<long>("foo", async _ => DateTimeOffset.UtcNow.Ticks); + + Assert.True(v1 > 0); + } + + [Fact] + public void CanHandleEagerRefreshWithInfiniteDuration() + { + var duration = TimeSpan.MaxValue; + var eagerRefreshThreshold = 0.5f; + + using var cache = new FusionCache(new FusionCacheOptions()); + + cache.DefaultEntryOptions.Duration = duration; + cache.DefaultEntryOptions.EagerRefreshThreshold = eagerRefreshThreshold; + + // EXECUTE FACTORY + var v1 = cache.GetOrSet<long>("foo", _ => DateTimeOffset.UtcNow.Ticks); + + Assert.True(v1 > 0); + } + + [Fact] + public async Task NormalFactoryExecutionWaitsForInFlightEagerRefreshAsync() + { + var duration = TimeSpan.FromSeconds(2); + var eagerRefreshThreshold = 0.2f; + var eagerRefreshThresholdDuration = TimeSpan.FromMilliseconds(duration.TotalMilliseconds * eagerRefreshThreshold); + var simulatedDelay = TimeSpan.FromSeconds(4); + var value = 0; + + using var cache = new FusionCache(new FusionCacheOptions()); + + cache.DefaultEntryOptions.Duration = duration; + cache.DefaultEntryOptions.EagerRefreshThreshold = eagerRefreshThreshold; + + // EXECUTE FACTORY + var v1 = await cache.GetOrSetAsync<long>("foo", async _ => + { + Interlocked.Increment(ref value); + return value; + }); + + // USE CACHED VALUE + var v2 = await cache.GetOrSetAsync<long>("foo", async _ => + { + Interlocked.Increment(ref value); + return value; + }); + + // WAIT FOR EAGER REFRESH THRESHOLD TO BE HIT + await Task.Delay(eagerRefreshThresholdDuration.Add(TimeSpan.FromMilliseconds(10))); + + // EAGER REFRESH KICKS IN (WITH DELAY) + var v3 = await cache.GetOrSetAsync<long>("foo", async _ => + { + await Task.Delay(simulatedDelay); + + Interlocked.Increment(ref value); + return value; + }); + + // WAIT FOR EXPIRATION + await Task.Delay(duration.PlusALittleBit()); + + // TRY TO GET EXPIRED ENTRY: NORMALLY THIS WOULD FIRE THE FACTORY, BUT SINCE IT + // IS ALRADY RUNNING BECAUSE OF EAGER REFRESH, IT WILL WAIT FOR IT TO COMPLETE + // AND USE THE RESULT, SAVING ONE FACTORY EXECUTION + var v4 = await cache.GetOrSetAsync<long>("foo", async _ => + { + Interlocked.Increment(ref value); + return value; + }); + + // USE CACHED VALUE + var v5 = await cache.GetOrSetAsync<long>("foo", async _ => + { + Interlocked.Increment(ref value); + return value; + }); + + Assert.Equal(1, v1); + Assert.Equal(1, v2); + Assert.Equal(1, v3); + Assert.Equal(2, v4); + Assert.Equal(2, v5); + Assert.Equal(2, value); + } + + [Fact] + public void NormalFactoryExecutionWaitsForInFlightEagerRefresh() + { + var duration = TimeSpan.FromSeconds(2); + var eagerRefreshThreshold = 0.2f; + var eagerRefreshThresholdDuration = TimeSpan.FromMilliseconds(duration.TotalMilliseconds * eagerRefreshThreshold); + var simulatedDelay = TimeSpan.FromSeconds(4); + var value = 0; + + using var cache = new FusionCache(new FusionCacheOptions()); + + cache.DefaultEntryOptions.Duration = duration; + cache.DefaultEntryOptions.EagerRefreshThreshold = eagerRefreshThreshold; + + // EXECUTE FACTORY + var v1 = cache.GetOrSet<long>("foo", _ => + { + Interlocked.Increment(ref value); + return value; + }); + + // USE CACHED VALUE + var v2 = cache.GetOrSet<long>("foo", _ => + { + Interlocked.Increment(ref value); + return value; + }); + + // WAIT FOR EAGER REFRESH THRESHOLD TO BE HIT + Thread.Sleep(eagerRefreshThresholdDuration.Add(TimeSpan.FromMilliseconds(10))); + + // EAGER REFRESH KICKS IN (WITH DELAY) + var v3 = cache.GetOrSet<long>("foo", _ => + { + Thread.Sleep(simulatedDelay); + + Interlocked.Increment(ref value); + return value; + }); + + // WAIT FOR EXPIRATION + Thread.Sleep(duration.PlusALittleBit()); + + // TRY TO GET EXPIRED ENTRY: NORMALLY THIS WOULD FIRE THE FACTORY, BUT SINCE IT + // IS ALRADY RUNNING BECAUSE OF EAGER REFRESH, IT WILL WAIT FOR IT TO COMPLETE + // AND USE THE RESULT, SAVING ONE FACTORY EXECUTION + var v4 = cache.GetOrSet<long>("foo", _ => + { + Interlocked.Increment(ref value); + return value; + }); + + // USE CACHED VALUE + var v5 = cache.GetOrSet<long>("foo", _ => + { + Interlocked.Increment(ref value); + return value; + }); + + Assert.Equal(1, v1); + Assert.Equal(1, v2); + Assert.Equal(1, v3); + Assert.Equal(2, v4); + Assert.Equal(2, v5); + Assert.Equal(2, value); + } + + [Fact] + public async Task CanExpireAsync() + { + using var cache = new FusionCache(new FusionCacheOptions()); + cache.DefaultEntryOptions.IsFailSafeEnabled = true; + cache.DefaultEntryOptions.Duration = TimeSpan.FromMinutes(10); + + await cache.SetAsync<int>("foo", 42); + var maybeFoo1 = await cache.TryGetAsync<int>("foo", opt => opt.SetFailSafe(false)); + await cache.ExpireAsync("foo"); + var maybeFoo2 = await cache.TryGetAsync<int>("foo", opt => opt.SetFailSafe(false)); + var maybeFoo3 = await cache.TryGetAsync<int>("foo", opt => opt.SetFailSafe(true)); + Assert.True(maybeFoo1.HasValue); + Assert.Equal(42, maybeFoo1.Value); + Assert.False(maybeFoo2.HasValue); + Assert.True(maybeFoo3.HasValue); + Assert.Equal(42, maybeFoo3.Value); + } + + [Fact] + public void CanExpire() + { + using var cache = new FusionCache(new FusionCacheOptions()); + cache.DefaultEntryOptions.IsFailSafeEnabled = true; + cache.DefaultEntryOptions.Duration = TimeSpan.FromMinutes(10); + + cache.Set<int>("foo", 42); + var maybeFoo1 = cache.TryGet<int>("foo", opt => opt.SetFailSafe(false)); + cache.Expire("foo"); + var maybeFoo2 = cache.TryGet<int>("foo", opt => opt.SetFailSafe(false)); + var maybeFoo3 = cache.TryGet<int>("foo", opt => opt.SetFailSafe(true)); + Assert.True(maybeFoo1.HasValue); + Assert.Equal(42, maybeFoo1.Value); + Assert.False(maybeFoo2.HasValue); + Assert.True(maybeFoo3.HasValue); + Assert.Equal(42, maybeFoo3.Value); + } + + [Fact] + public async Task CanSkipMemoryCacheAsync() + { + using var cache = new FusionCache(new FusionCacheOptions()); + + await cache.SetAsync<int>("foo", 42, opt => opt.SetSkipMemoryCache()); + var maybeFoo1 = await cache.TryGetAsync<int>("foo"); + await cache.SetAsync<int>("foo", 42); + var maybeFoo2 = await cache.TryGetAsync<int>("foo", opt => opt.SetSkipMemoryCache()); + var maybeFoo3 = await cache.TryGetAsync<int>("foo"); + await cache.RemoveAsync("foo", opt => opt.SetSkipMemoryCache()); + var maybeFoo4 = await cache.TryGetAsync<int>("foo"); + await cache.RemoveAsync("foo"); + var maybeFoo5 = await cache.TryGetAsync<int>("foo"); + + await cache.GetOrSetAsync<int>("bar", 42, opt => opt.SetSkipMemoryCache()); + var maybeBar = await cache.TryGetAsync<int>("bar"); + + Assert.False(maybeFoo1.HasValue); + Assert.False(maybeFoo2.HasValue); + Assert.True(maybeFoo3.HasValue); + Assert.True(maybeFoo4.HasValue); + Assert.False(maybeFoo5.HasValue); + + Assert.False(maybeBar.HasValue); + } + + [Fact] + public void CanSkipMemoryCache() + { + using var cache = new FusionCache(new FusionCacheOptions()); + + cache.Set<int>("foo", 42, opt => opt.SetSkipMemoryCache()); + var maybeFoo1 = cache.TryGet<int>("foo"); + cache.Set<int>("foo", 42); + var maybeFoo2 = cache.TryGet<int>("foo", opt => opt.SetSkipMemoryCache()); + var maybeFoo3 = cache.TryGet<int>("foo"); + cache.Remove("foo", opt => opt.SetSkipMemoryCache()); + var maybeFoo4 = cache.TryGet<int>("foo"); + cache.Remove("foo"); + var maybeFoo5 = cache.TryGet<int>("foo"); + + cache.GetOrSet<int>("bar", 42, opt => opt.SetSkipMemoryCache()); + var maybeBar = cache.TryGet<int>("bar"); + + Assert.False(maybeFoo1.HasValue); + Assert.False(maybeFoo2.HasValue); + Assert.True(maybeFoo3.HasValue); + Assert.True(maybeFoo4.HasValue); + Assert.False(maybeFoo5.HasValue); + + Assert.False(maybeBar.HasValue); + } + + [Fact] + public async Task CanUseNullFusionCacheAsync() + { + using var cache = new NullFusionCache(new FusionCacheOptions() + { + CacheName = "SlothsAreCool42", + DefaultEntryOptions = new FusionCacheEntryOptions() + { + IsFailSafeEnabled = true, + Duration = TimeSpan.FromMinutes(123) + } + }); + + await cache.SetAsync<int>("foo", 42); + + var maybeFoo1 = await cache.TryGetAsync<int>("foo"); + + await cache.RemoveAsync("foo"); + + var maybeBar1 = await cache.TryGetAsync<int>("bar"); + + await cache.ExpireAsync("qux"); + + var qux1 = await cache.GetOrSetAsync("qux", async _ => 1); + var qux2 = await cache.GetOrSetAsync("qux", async _ => 2); + var qux3 = await cache.GetOrSetAsync("qux", async _ => 3); + var qux4 = await cache.GetOrDefaultAsync("qux", 4); + + Assert.Equal("SlothsAreCool42", cache.CacheName); + Assert.False(string.IsNullOrWhiteSpace(cache.InstanceId)); + + Assert.False(cache.HasDistributedCache); + Assert.False(cache.HasBackplane); + + Assert.True(cache.DefaultEntryOptions.IsFailSafeEnabled); + Assert.Equal(TimeSpan.FromMinutes(123), cache.DefaultEntryOptions.Duration); + + Assert.False(maybeFoo1.HasValue); + Assert.False(maybeBar1.HasValue); + + Assert.Equal(1, qux1); + Assert.Equal(2, qux2); + Assert.Equal(3, qux3); + Assert.Equal(4, qux4); + + await Assert.ThrowsAsync<UnreachableException>(async () => + { + _ = await cache.GetOrSetAsync<int>("qux", async _ => throw new UnreachableException("Sloths")); + }); + } +} diff --git a/tests/ZiggyCreatures.FusionCache.Tests/MultiLevelTests.cs b/tests/ZiggyCreatures.FusionCache.Tests/MultiLevelTests.cs deleted file mode 100644 index c664e731..00000000 --- a/tests/ZiggyCreatures.FusionCache.Tests/MultiLevelTests.cs +++ /dev/null @@ -1,1321 +0,0 @@ -using System; -using System.Diagnostics; -using System.Threading; -using System.Threading.Tasks; -using FusionCacheTests.Stuff; -using Microsoft.Extensions.Caching.Distributed; -using Microsoft.Extensions.Caching.Memory; -using Microsoft.Extensions.Logging; -using Microsoft.Extensions.Options; -using Xunit; -using Xunit.Abstractions; -using ZiggyCreatures.Caching.Fusion; -using ZiggyCreatures.Caching.Fusion.Backplane.Memory; -using ZiggyCreatures.Caching.Fusion.Chaos; -using ZiggyCreatures.Caching.Fusion.Internals; - -namespace FusionCacheTests -{ - public class MultiLevelTests - { - private readonly ITestOutputHelper _output; - private static readonly string? _cacheKeyPrefix = "Foo:"; - - public MultiLevelTests(ITestOutputHelper output) - { - _output = output; - } - - private XUnitLogger<T> CreateLogger<T>(LogLevel minLevel = LogLevel.Trace) - { - return new XUnitLogger<T>(minLevel, _output); - } - - private static FusionCacheOptions CreateFusionCacheOptions() - { - var res = new FusionCacheOptions(); - - res.CacheKeyPrefix = _cacheKeyPrefix; - - return res; - } - - [Theory] - [ClassData(typeof(SerializerTypesClassData))] - public async Task ReturnsDataFromDistributedCacheIfNoDataInMemoryCacheAsync(SerializerType serializerType) - { - using var memoryCache = new MemoryCache(new MemoryCacheOptions()); - var distributedCache = new MemoryDistributedCache(Options.Create(new MemoryDistributedCacheOptions())); - using var fusionCache = new FusionCache(CreateFusionCacheOptions(), memoryCache).SetupDistributedCache(distributedCache, TestsUtils.GetSerializer(serializerType)); - var initialValue = await fusionCache.GetOrSetAsync<int>("foo", _ => Task.FromResult(42), new FusionCacheEntryOptions().SetDurationSec(10)); - memoryCache.Remove("foo"); - var newValue = await fusionCache.GetOrSetAsync<int>("foo", _ => Task.FromResult(21), new FusionCacheEntryOptions().SetDurationSec(10)); - Assert.Equal(initialValue, newValue); - } - - [Theory] - [ClassData(typeof(SerializerTypesClassData))] - public void ReturnsDataFromDistributedCacheIfNoDataInMemoryCache(SerializerType serializerType) - { - using var memoryCache = new MemoryCache(new MemoryCacheOptions()); - var distributedCache = new MemoryDistributedCache(Options.Create(new MemoryDistributedCacheOptions())); - using var fusionCache = new FusionCache(CreateFusionCacheOptions(), memoryCache).SetupDistributedCache(distributedCache, TestsUtils.GetSerializer(serializerType)); - fusionCache.DefaultEntryOptions.AllowBackgroundDistributedCacheOperations = false; - - var initialValue = fusionCache.GetOrSet<int>("foo", _ => 42, options => options.SetDurationSec(10)); - memoryCache.Remove("foo"); - var newValue = fusionCache.GetOrSet<int>("foo", _ => 21, options => options.SetDurationSec(10)); - Assert.Equal(initialValue, newValue); - } - - [Theory] - [ClassData(typeof(SerializerTypesClassData))] - public async Task HandlesDistributedCacheFailuresAsync(SerializerType serializerType) - { - var distributedCache = new MemoryDistributedCache(Options.Create(new MemoryDistributedCacheOptions())); - var chaosDistributedCache = new ChaosDistributedCache(distributedCache); - using var fusionCache = new FusionCache(CreateFusionCacheOptions()).SetupDistributedCache(chaosDistributedCache, TestsUtils.GetSerializer(serializerType)); - var initialValue = await fusionCache.GetOrSetAsync<int>("foo", _ => Task.FromResult(42), new FusionCacheEntryOptions() { Duration = TimeSpan.FromSeconds(1), IsFailSafeEnabled = true }); - await Task.Delay(1_500); - chaosDistributedCache.SetAlwaysThrow(); - var newValue = await fusionCache.GetOrSetAsync<int>("foo", async _ => throw new Exception("Generic error"), new FusionCacheEntryOptions(TimeSpan.FromSeconds(1)) { IsFailSafeEnabled = true }); - Assert.Equal(initialValue, newValue); - } - - [Theory] - [ClassData(typeof(SerializerTypesClassData))] - public void HandlesDistributedCacheFailures(SerializerType serializerType) - { - var distributedCache = new MemoryDistributedCache(Options.Create(new MemoryDistributedCacheOptions())); - var chaosDistributedCache = new ChaosDistributedCache(distributedCache); - using var fusionCache = new FusionCache(CreateFusionCacheOptions()).SetupDistributedCache(chaosDistributedCache, TestsUtils.GetSerializer(serializerType)); - var initialValue = fusionCache.GetOrSet<int>("foo", _ => 42, new FusionCacheEntryOptions() { Duration = TimeSpan.FromSeconds(1), IsFailSafeEnabled = true }); - Thread.Sleep(1_500); - chaosDistributedCache.SetAlwaysThrow(); - var newValue = fusionCache.GetOrSet<int>("foo", _ => throw new Exception("Generic error"), new FusionCacheEntryOptions(TimeSpan.FromSeconds(1)) { IsFailSafeEnabled = true }); - Assert.Equal(initialValue, newValue); - } - - [Theory] - [ClassData(typeof(SerializerTypesClassData))] - public async Task HandlesDistributedCacheRemovalInTheMiddleOfAnOperationAsync(SerializerType serializerType) - { - var distributedCache = new MemoryDistributedCache(Options.Create(new MemoryDistributedCacheOptions())); - using var fusionCache = new FusionCache(CreateFusionCacheOptions()).SetupDistributedCache(distributedCache, TestsUtils.GetSerializer(serializerType)); - var task = fusionCache.GetOrSetAsync<int>("foo", async _ => { await Task.Delay(2_000); return 42; }, new FusionCacheEntryOptions(TimeSpan.FromSeconds(10))); - await Task.Delay(500); - fusionCache.RemoveDistributedCache(); - var value = await task; - Assert.Equal(42, value); - } - - [Theory] - [ClassData(typeof(SerializerTypesClassData))] - public async Task HandlesDistributedCacheFailuresInTheMiddleOfAnOperationAsync(SerializerType serializerType) - { - using var memoryCache = new MemoryCache(new MemoryCacheOptions()); - var distributedCache = new MemoryDistributedCache(Options.Create(new MemoryDistributedCacheOptions())); - var chaosDistributedCache = new ChaosDistributedCache(distributedCache); - var options = CreateFusionCacheOptions(); - options.DistributedCacheKeyModifierMode = CacheKeyModifierMode.None; - using var fusionCache = new FusionCache(options, memoryCache).SetupDistributedCache(chaosDistributedCache, TestsUtils.GetSerializer(serializerType)); - - var preProcessedCacheKey = TestsUtils.MaybePreProcessCacheKey("bar", options.CacheKeyPrefix); - - await fusionCache.GetOrSetAsync<int>("bar", async _ => { return 42; }, new FusionCacheEntryOptions(TimeSpan.FromSeconds(10))); - Assert.NotNull(distributedCache.GetString(preProcessedCacheKey)); - - preProcessedCacheKey = TestsUtils.MaybePreProcessCacheKey("foo", options.CacheKeyPrefix); - var task = fusionCache.GetOrSetAsync<int>("foo", async _ => { await Task.Delay(2_000); return 42; }, new FusionCacheEntryOptions(TimeSpan.FromSeconds(10))); - await Task.Delay(500); - chaosDistributedCache.SetAlwaysThrow(); - var value = await task; - chaosDistributedCache.SetNeverThrow(); - - // END RESULT IS WHAT EXPECTED - Assert.Equal(42, value); - - // MEMORY CACHE HAS BEEN UPDATED - Assert.Equal(42, memoryCache.Get<IFusionCacheEntry>(preProcessedCacheKey)?.GetValue<int>()); - - // DISTRIBUTED CACHE HAS -NOT- BEEN UPDATED - Assert.Null(distributedCache.GetString(preProcessedCacheKey)); - } - - [Theory] - [ClassData(typeof(SerializerTypesClassData))] - public async Task AppliesDistributedCacheHardTimeoutAsync(SerializerType serializerType) - { - var simulatedDelayMs = TimeSpan.FromMilliseconds(2_000); - var softTimeout = TimeSpan.FromMilliseconds(100); - var hardTimeout = TimeSpan.FromMilliseconds(1_000); - var distributedCache = new MemoryDistributedCache(Options.Create(new MemoryDistributedCacheOptions())); - var chaosDistributedCache = new ChaosDistributedCache(distributedCache); - - using var memoryCache = new MemoryCache(new MemoryCacheOptions()); - using var fusionCache = new FusionCache(CreateFusionCacheOptions(), memoryCache); - fusionCache.SetupDistributedCache(chaosDistributedCache, TestsUtils.GetSerializer(serializerType)); - - await fusionCache.SetAsync<int>("foo", 42, new FusionCacheEntryOptions().SetDurationSec(1).SetFailSafe(true)); - await Task.Delay(TimeSpan.FromSeconds(1).PlusALittleBit()); - memoryCache.Remove(TestsUtils.MaybePreProcessCacheKey("foo", _cacheKeyPrefix)); - chaosDistributedCache.SetAlwaysDelayExactly(simulatedDelayMs); - await Assert.ThrowsAsync<Exception>(async () => - { - _ = await fusionCache.GetOrSetAsync<int>("foo", _ => throw new Exception("Sloths are cool"), new FusionCacheEntryOptions().SetDurationSec(1).SetFailSafe(true).SetDistributedCacheTimeouts(softTimeout, hardTimeout)); - }); - } - - [Theory] - [ClassData(typeof(SerializerTypesClassData))] - public void AppliesDistributedCacheHardTimeout(SerializerType serializerType) - { - var simulatedDelayMs = TimeSpan.FromMilliseconds(2_000); - var softTimeout = TimeSpan.FromMilliseconds(100); - var hardTimeout = TimeSpan.FromMilliseconds(1_000); - var distributedCache = new MemoryDistributedCache(Options.Create(new MemoryDistributedCacheOptions())); - var chaosDistributedCache = new ChaosDistributedCache(distributedCache); - - using var memoryCache = new MemoryCache(new MemoryCacheOptions()); - using var fusionCache = new FusionCache(CreateFusionCacheOptions(), memoryCache); - fusionCache.SetupDistributedCache(chaosDistributedCache, TestsUtils.GetSerializer(serializerType)); - - fusionCache.Set<int>("foo", 42, new FusionCacheEntryOptions().SetDurationSec(1).SetFailSafe(true)); - Thread.Sleep(TimeSpan.FromSeconds(1).PlusALittleBit()); - memoryCache.Remove(TestsUtils.MaybePreProcessCacheKey("foo", _cacheKeyPrefix)); - chaosDistributedCache.SetAlwaysDelayExactly(simulatedDelayMs); - Assert.Throws<Exception>(() => - { - _ = fusionCache.GetOrSet<int>("foo", _ => throw new Exception("Sloths are cool"), new FusionCacheEntryOptions().SetDurationSec(1).SetFailSafe(true).SetDistributedCacheTimeouts(softTimeout, hardTimeout)); - }); - } - - [Theory] - [ClassData(typeof(SerializerTypesClassData))] - public async Task AppliesDistributedCacheSoftTimeoutAsync(SerializerType serializerType) - { - var simulatedDelay = TimeSpan.FromMilliseconds(2_000); - var softTimeout = TimeSpan.FromMilliseconds(100); - var hardTimeout = TimeSpan.FromMilliseconds(1_000); - var duration = TimeSpan.FromSeconds(1); - var distributedCache = new MemoryDistributedCache(Options.Create(new MemoryDistributedCacheOptions())); - var chaosDistributedCache = new ChaosDistributedCache(distributedCache); - - using var memoryCache = new MemoryCache(new MemoryCacheOptions()); - using var fusionCache = new FusionCache(CreateFusionCacheOptions(), memoryCache); - fusionCache.SetupDistributedCache(chaosDistributedCache, TestsUtils.GetSerializer(serializerType)); - await fusionCache.SetAsync<int>("foo", 42, new FusionCacheEntryOptions().SetDuration(duration).SetFailSafe(true)); - await Task.Delay(duration.PlusALittleBit()).ConfigureAwait(false); - var sw = Stopwatch.StartNew(); - chaosDistributedCache.SetAlwaysDelayExactly(simulatedDelay); - var res = await fusionCache.GetOrSetAsync<int>("foo", async _ => throw new Exception("Sloths are cool"), new FusionCacheEntryOptions().SetDurationSec(1).SetFailSafe(true).SetDistributedCacheTimeouts(TimeSpan.FromMilliseconds(100), TimeSpan.FromMilliseconds(1_000))); - sw.Stop(); - - Assert.Equal(42, res); - Assert.True(sw.ElapsedMilliseconds >= 100, "Distributed cache soft timeout not applied"); - Assert.True(sw.Elapsed < simulatedDelay, "Distributed cache soft timeout not applied"); - } - - [Theory] - [ClassData(typeof(SerializerTypesClassData))] - public void AppliesDistributedCacheSoftTimeout(SerializerType serializerType) - { - var simulatedDelay = TimeSpan.FromMilliseconds(2_000); - var softTimeout = TimeSpan.FromMilliseconds(100); - var hardTimeout = TimeSpan.FromMilliseconds(1_000); - var duration = TimeSpan.FromSeconds(1); - var distributedCache = new MemoryDistributedCache(Options.Create(new MemoryDistributedCacheOptions())); - var chaosDistributedCache = new ChaosDistributedCache(distributedCache); - - using var memoryCache = new MemoryCache(new MemoryCacheOptions()); - using var fusionCache = new FusionCache(CreateFusionCacheOptions(), memoryCache); - fusionCache.SetupDistributedCache(chaosDistributedCache, TestsUtils.GetSerializer(serializerType)); - fusionCache.Set<int>("foo", 42, new FusionCacheEntryOptions().SetDuration(duration).SetFailSafe(true)); - Thread.Sleep(duration.PlusALittleBit()); - var sw = Stopwatch.StartNew(); - chaosDistributedCache.SetAlwaysDelayExactly(simulatedDelay); - var res = fusionCache.GetOrSet<int>("foo", _ => throw new Exception("Sloths are cool"), new FusionCacheEntryOptions().SetDurationSec(1).SetFailSafe(true).SetDistributedCacheTimeouts(softTimeout, hardTimeout)); - sw.Stop(); - - Assert.Equal(42, res); - Assert.True(sw.ElapsedMilliseconds >= 100, "Distributed cache soft timeout not applied"); - Assert.True(sw.Elapsed < simulatedDelay, "Distributed cache soft timeout not applied"); - } - - [Theory] - [ClassData(typeof(SerializerTypesClassData))] - public async Task DistributedCacheCircuitBreakerActuallyWorksAsync(SerializerType serializerType) - { - var circuitBreakerDuration = TimeSpan.FromSeconds(2); - var distributedCache = new MemoryDistributedCache(Options.Create(new MemoryDistributedCacheOptions())); - var chaosDistributedCache = new ChaosDistributedCache(distributedCache); - - using var memoryCache = new MemoryCache(new MemoryCacheOptions()); - var options = CreateFusionCacheOptions(); - options.DistributedCacheCircuitBreakerDuration = circuitBreakerDuration; - using var fusionCache = new FusionCache(options, memoryCache); - fusionCache.DefaultEntryOptions.AllowBackgroundDistributedCacheOperations = false; - fusionCache.SetupDistributedCache(chaosDistributedCache, TestsUtils.GetSerializer(serializerType)); - - await fusionCache.SetAsync<int>("foo", 1, options => options.SetDurationSec(60).SetFailSafe(true)); - chaosDistributedCache.SetAlwaysThrow(); - await fusionCache.SetAsync<int>("foo", 2, options => options.SetDurationSec(60).SetFailSafe(true)); - chaosDistributedCache.SetNeverThrow(); - await fusionCache.SetAsync<int>("foo", 3, options => options.SetDurationSec(60).SetFailSafe(true)); - await Task.Delay(circuitBreakerDuration.PlusALittleBit()).ConfigureAwait(false); - memoryCache.Remove(TestsUtils.MaybePreProcessCacheKey("foo", _cacheKeyPrefix)); - var res = await fusionCache.GetOrDefaultAsync<int>("foo", -1); - - Assert.Equal(1, res); - } - - [Theory] - [ClassData(typeof(SerializerTypesClassData))] - public void DistributedCacheCircuitBreakerActuallyWorks(SerializerType serializerType) - { - var circuitBreakerDuration = TimeSpan.FromSeconds(2); - var distributedCache = new MemoryDistributedCache(Options.Create(new MemoryDistributedCacheOptions())); - var chaosDistributedCache = new ChaosDistributedCache(distributedCache); - - using var memoryCache = new MemoryCache(new MemoryCacheOptions()); - var options = CreateFusionCacheOptions(); - options.DistributedCacheCircuitBreakerDuration = circuitBreakerDuration; - using var fusionCache = new FusionCache(options, memoryCache); - fusionCache.DefaultEntryOptions.AllowBackgroundDistributedCacheOperations = false; - fusionCache.SetupDistributedCache(chaosDistributedCache, TestsUtils.GetSerializer(serializerType)); - - fusionCache.Set<int>("foo", 1, options => options.SetDurationSec(60).SetFailSafe(true)); - chaosDistributedCache.SetAlwaysThrow(); - fusionCache.Set<int>("foo", 2, options => options.SetDurationSec(60).SetFailSafe(true)); - chaosDistributedCache.SetNeverThrow(); - fusionCache.Set<int>("foo", 3, options => options.SetDurationSec(60).SetFailSafe(true)); - Thread.Sleep(circuitBreakerDuration.PlusALittleBit()); - memoryCache.Remove(TestsUtils.MaybePreProcessCacheKey("foo", _cacheKeyPrefix)); - var res = fusionCache.GetOrDefault<int>("foo", -1); - - Assert.Equal(1, res); - } - - //[Theory] - //[ClassData(typeof(SerializerTypesClassData))] - //public async Task HandlesFlexibleSimpleTypeConversionsAsync(SerializerType serializerType) - //{ - // using (var memoryCache = new MemoryCache(new MemoryCacheOptions())) - // { - // var distributedCache = new MemoryDistributedCache(Options.Create(new MemoryDistributedCacheOptions())); - // using (var fusionCache = new FusionCache(CreateFusionCacheOptions(), memoryCache).SetupDistributedCache(distributedCache, TestsUtils.GetSerializer(serializerType))) - // { - // int? initialValue = 42; - // await fusionCache.SetAsync("foo", initialValue, TimeSpan.FromHours(24)); - // memoryCache.Remove("foo"); - // var newValue = await fusionCache.GetOrDefaultAsync<int>("foo"); - // Assert.Equal(initialValue, newValue); - // } - // } - //} - - //[Theory] - //[ClassData(typeof(SerializerTypesClassData))] - //public void HandlesFlexibleSimpleTypeConversions(SerializerType serializerType) - //{ - // using (var memoryCache = new MemoryCache(new MemoryCacheOptions())) - // { - // var distributedCache = new MemoryDistributedCache(Options.Create(new MemoryDistributedCacheOptions())); - // using (var fusionCache = new FusionCache(CreateFusionCacheOptions(), memoryCache).SetupDistributedCache(distributedCache, TestsUtils.GetSerializer(serializerType))) - // { - // int? initialValue = 42; - // fusionCache.Set("foo", initialValue, TimeSpan.FromHours(24)); - // memoryCache.Remove("foo"); - // var newValue = fusionCache.GetOrDefault<int>("foo"); - // Assert.Equal(initialValue, newValue); - // } - // } - //} - - //[Theory] - //[ClassData(typeof(SerializerTypesClassData))] - //public async Task HandlesFlexibleComplexTypeConversionsAsync(SerializerType serializerType) - //{ - // using (var memoryCache = new MemoryCache(new MemoryCacheOptions())) - // { - // var distributedCache = new MemoryDistributedCache(Options.Create(new MemoryDistributedCacheOptions())); - // using (var fusionCache = new FusionCache(CreateFusionCacheOptions(), memoryCache).SetupDistributedCache(distributedCache, TestsUtils.GetSerializer(serializerType))) - // { - // var initialValue = (object)ComplexType.CreateSample(); - // await fusionCache.SetAsync("foo", initialValue, TimeSpan.FromHours(24)); - // memoryCache.Remove("foo"); - // var newValue = await fusionCache.GetOrDefaultAsync<ComplexType>("foo"); - // Assert.Equal(initialValue, newValue); - // } - // } - //} - - //[Theory] - //[ClassData(typeof(SerializerTypesClassData))] - //public void HandlesFlexibleComplexTypeConversions(SerializerType serializerType) - //{ - // using (var memoryCache = new MemoryCache(new MemoryCacheOptions())) - // { - // var distributedCache = new MemoryDistributedCache(Options.Create(new MemoryDistributedCacheOptions())); - // using (var fusionCache = new FusionCache(CreateFusionCacheOptions(), memoryCache).SetupDistributedCache(distributedCache, TestsUtils.GetSerializer(serializerType))) - // { - // var initialValue = (object)ComplexType.CreateSample(); - // fusionCache.Set("foo", initialValue, TimeSpan.FromHours(24)); - // memoryCache.Remove("foo"); - // var newValue = fusionCache.GetOrDefault<ComplexType>("foo"); - // Assert.Equal(initialValue, newValue); - // } - // } - //} - - private static void _DistributedCacheWireVersionModifierWorks(SerializerType serializerType, CacheKeyModifierMode modifierMode) - { - using var memoryCache = new MemoryCache(new MemoryCacheOptions()); - var distributedCache = new MemoryDistributedCache(Options.Create(new MemoryDistributedCacheOptions())); - var options = CreateFusionCacheOptions(); - options.DistributedCacheKeyModifierMode = modifierMode; - using var fusionCache = new FusionCache(options, memoryCache).SetupDistributedCache(distributedCache, TestsUtils.GetSerializer(serializerType)); - var cacheKey = "foo"; - var preProcessedCacheKey = TestsUtils.MaybePreProcessCacheKey(cacheKey, options.CacheKeyPrefix); - string distributedCacheKey; - switch (modifierMode) - { - case CacheKeyModifierMode.Prefix: - distributedCacheKey = $"v1:{preProcessedCacheKey}"; - break; - case CacheKeyModifierMode.Suffix: - distributedCacheKey = $"{preProcessedCacheKey}:v1"; - break; - default: - distributedCacheKey = preProcessedCacheKey; - break; - } - var value = "sloths"; - fusionCache.Set(cacheKey, value, new FusionCacheEntryOptions(TimeSpan.FromHours(24)) { AllowBackgroundDistributedCacheOperations = false }); - var nullValue = distributedCache.Get("foo42"); - var distributedValue = distributedCache.Get(distributedCacheKey); - Assert.Null(nullValue); - Assert.NotNull(distributedValue); - } - - [Theory] - [ClassData(typeof(SerializerTypesClassData))] - public void DistributedCacheWireVersionPrefixModeWorks(SerializerType serializerType) - { - _DistributedCacheWireVersionModifierWorks(serializerType, CacheKeyModifierMode.Prefix); - } - - [Theory] - [ClassData(typeof(SerializerTypesClassData))] - public void DistributedCacheWireVersionSuffixModeWorks(SerializerType serializerType) - { - _DistributedCacheWireVersionModifierWorks(serializerType, CacheKeyModifierMode.Suffix); - } - - [Theory] - [ClassData(typeof(SerializerTypesClassData))] - public void DistributedCacheWireVersionNoneModeWorks(SerializerType serializerType) - { - _DistributedCacheWireVersionModifierWorks(serializerType, CacheKeyModifierMode.None); - } - - [Theory] - [ClassData(typeof(SerializerTypesClassData))] - public async Task ReThrowsDistributedCacheErrorsAsync(SerializerType serializerType) - { - var distributedCache = new MemoryDistributedCache(Options.Create(new MemoryDistributedCacheOptions())); - var chaosDistributedCache = new ChaosDistributedCache(distributedCache); - - chaosDistributedCache.SetAlwaysThrow(); - using var fusionCache = new FusionCache(CreateFusionCacheOptions()); - fusionCache.DefaultEntryOptions.ReThrowDistributedCacheExceptions = true; - - fusionCache.SetupDistributedCache(chaosDistributedCache, TestsUtils.GetSerializer(serializerType)); - - await Assert.ThrowsAsync<ChaosException>(async () => - { - await fusionCache.SetAsync<int>("foo", 42); - }); - - await Assert.ThrowsAsync<ChaosException>(async () => - { - _ = await fusionCache.TryGetAsync<int>("bar"); - }); - } - - [Theory] - [ClassData(typeof(SerializerTypesClassData))] - public void ReThrowsDistributedCacheErrors(SerializerType serializerType) - { - var distributedCache = new MemoryDistributedCache(Options.Create(new MemoryDistributedCacheOptions())); - var chaosDistributedCache = new ChaosDistributedCache(distributedCache); - - chaosDistributedCache.SetAlwaysThrow(); - using var fusionCache = new FusionCache(CreateFusionCacheOptions()); - fusionCache.DefaultEntryOptions.ReThrowDistributedCacheExceptions = true; - - fusionCache.SetupDistributedCache(chaosDistributedCache, TestsUtils.GetSerializer(serializerType)); - - Assert.Throws<ChaosException>(() => - { - fusionCache.Set<int>("foo", 42); - }); - - Assert.Throws<ChaosException>(() => - { - _ = fusionCache.TryGet<int>("bar"); - }); - } - - [Theory] - [ClassData(typeof(SerializerTypesClassData))] - public async Task ReThrowsSerializationErrorsAsync(SerializerType serializerType) - { - var distributedCache = new MemoryDistributedCache(Options.Create(new MemoryDistributedCacheOptions())); - var serializer = new ChaosSerializer(TestsUtils.GetSerializer(serializerType)); - - using var fusionCache = new FusionCache(CreateFusionCacheOptions()); - fusionCache.DefaultEntryOptions.ReThrowSerializationExceptions = true; - - fusionCache.SetupDistributedCache(distributedCache, serializer); - - serializer.SetAlwaysThrow(); - await Assert.ThrowsAsync<ChaosException>(async () => - { - await fusionCache.SetAsync<string>("foo", "sloths, sloths everywhere", x => x.SetDuration(TimeSpan.FromMilliseconds(100)).SetDistributedCacheDuration(TimeSpan.FromSeconds(10))); - }); - - serializer.SetNeverThrow(); - await fusionCache.SetAsync<string>("foo", "sloths, sloths everywhere", x => x.SetDuration(TimeSpan.FromMilliseconds(100)).SetDistributedCacheDuration(TimeSpan.FromSeconds(10))); - - Thread.Sleep(TimeSpan.FromSeconds(1)); - - serializer.SetAlwaysThrow(); - await Assert.ThrowsAsync<ChaosException>(async () => - { - _ = await fusionCache.TryGetAsync<int>("foo"); - }); - } - - [Theory] - [ClassData(typeof(SerializerTypesClassData))] - public void ReThrowsSerializationErrors(SerializerType serializerType) - { - var distributedCache = new MemoryDistributedCache(Options.Create(new MemoryDistributedCacheOptions())); - var serializer = new ChaosSerializer(TestsUtils.GetSerializer(serializerType)); - - using var fusionCache = new FusionCache(CreateFusionCacheOptions()); - fusionCache.DefaultEntryOptions.ReThrowSerializationExceptions = true; - - fusionCache.SetupDistributedCache(distributedCache, serializer); - - serializer.SetAlwaysThrow(); - Assert.Throws<ChaosException>(() => - { - fusionCache.Set<string>("foo", "sloths, sloths everywhere", x => x.SetDuration(TimeSpan.FromMilliseconds(100)).SetDistributedCacheDuration(TimeSpan.FromSeconds(10))); - }); - - serializer.SetNeverThrow(); - fusionCache.Set<string>("foo", "sloths, sloths everywhere", x => x.SetDuration(TimeSpan.FromMilliseconds(100)).SetDistributedCacheDuration(TimeSpan.FromSeconds(10))); - - Thread.Sleep(TimeSpan.FromSeconds(1)); - - serializer.SetAlwaysThrow(); - Assert.Throws<ChaosException>(() => - { - _ = fusionCache.TryGet<int>("foo"); - }); - } - - [Theory] - [ClassData(typeof(SerializerTypesClassData))] - public async Task DoesNotReThrowsSerializationErrorsAsync(SerializerType serializerType) - { - var distributedCache = new MemoryDistributedCache(Options.Create(new MemoryDistributedCacheOptions())); - var serializer = new ChaosSerializer(TestsUtils.GetSerializer(serializerType)); - - using var fusionCache = new FusionCache(CreateFusionCacheOptions()); - fusionCache.DefaultEntryOptions.ReThrowSerializationExceptions = false; - - fusionCache.SetupDistributedCache(distributedCache, serializer); - - serializer.SetAlwaysThrow(); - await fusionCache.SetAsync<string>("foo", "sloths, sloths everywhere", x => x.SetDuration(TimeSpan.FromMilliseconds(100)).SetDistributedCacheDuration(TimeSpan.FromSeconds(10))); - - serializer.SetNeverThrow(); - await fusionCache.SetAsync<string>("foo", "sloths, sloths everywhere", x => x.SetDuration(TimeSpan.FromMilliseconds(100)).SetDistributedCacheDuration(TimeSpan.FromSeconds(10))); - - Thread.Sleep(TimeSpan.FromSeconds(1)); - - serializer.SetAlwaysThrow(); - var res = await fusionCache.TryGetAsync<int>("foo"); - - Assert.False(res.HasValue); - } - - [Theory] - [ClassData(typeof(SerializerTypesClassData))] - public void DoesNotReThrowsSerializationErrors(SerializerType serializerType) - { - var distributedCache = new MemoryDistributedCache(Options.Create(new MemoryDistributedCacheOptions())); - var serializer = new ChaosSerializer(TestsUtils.GetSerializer(serializerType)); - - using var fusionCache = new FusionCache(CreateFusionCacheOptions()); - fusionCache.DefaultEntryOptions.ReThrowSerializationExceptions = false; - - fusionCache.SetupDistributedCache(distributedCache, serializer); - - serializer.SetAlwaysThrow(); - fusionCache.Set<string>("foo", "sloths, sloths everywhere", x => x.SetDuration(TimeSpan.FromMilliseconds(100)).SetDistributedCacheDuration(TimeSpan.FromSeconds(10))); - - serializer.SetNeverThrow(); - fusionCache.Set<string>("foo", "sloths, sloths everywhere", x => x.SetDuration(TimeSpan.FromMilliseconds(100)).SetDistributedCacheDuration(TimeSpan.FromSeconds(10))); - - Thread.Sleep(TimeSpan.FromSeconds(1)); - - serializer.SetAlwaysThrow(); - var res = fusionCache.TryGet<int>("foo"); - - Assert.False(res.HasValue); - } - - [Theory] - [ClassData(typeof(SerializerTypesClassData))] - public async Task SpecificDistributedCacheDurationWorksAsync(SerializerType serializerType) - { - var distributedCache = new MemoryDistributedCache(Options.Create(new MemoryDistributedCacheOptions())); - using var fusionCache = new FusionCache(CreateFusionCacheOptions()).SetupDistributedCache(distributedCache, TestsUtils.GetSerializer(serializerType)); - await fusionCache.SetAsync<int>("foo", 21, opt => opt.SetFailSafe(false).SetDuration(TimeSpan.FromSeconds(1)).SetDistributedCacheDuration(TimeSpan.FromMinutes(1))); - await Task.Delay(TimeSpan.FromSeconds(2)); - var value = await fusionCache.GetOrDefaultAsync<int>("foo"); - Assert.Equal(21, value); - } - - [Theory] - [ClassData(typeof(SerializerTypesClassData))] - public void SpecificDistributedCacheDurationWorks(SerializerType serializerType) - { - var distributedCache = new MemoryDistributedCache(Options.Create(new MemoryDistributedCacheOptions())); - using var fusionCache = new FusionCache(CreateFusionCacheOptions()).SetupDistributedCache(distributedCache, TestsUtils.GetSerializer(serializerType)); - fusionCache.Set<int>("foo", 21, opt => opt.SetFailSafe(false).SetDuration(TimeSpan.FromSeconds(1)).SetDistributedCacheDuration(TimeSpan.FromMinutes(1))); - Thread.Sleep(TimeSpan.FromSeconds(2)); - var value = fusionCache.GetOrDefault<int>("foo"); - Assert.Equal(21, value); - } - - [Theory] - [ClassData(typeof(SerializerTypesClassData))] - public async Task SpecificDistributedCacheDurationWithFailSafeWorksAsync(SerializerType serializerType) - { - var distributedCache = new MemoryDistributedCache(Options.Create(new MemoryDistributedCacheOptions())); - using var fusionCache = new FusionCache(CreateFusionCacheOptions()).SetupDistributedCache(distributedCache, TestsUtils.GetSerializer(serializerType)); - await fusionCache.SetAsync<int>("foo", 21, opt => opt.SetFailSafe(true).SetDuration(TimeSpan.FromSeconds(1)).SetDistributedCacheDuration(TimeSpan.FromMinutes(1))); - await Task.Delay(TimeSpan.FromSeconds(2)); - var value = await fusionCache.GetOrDefaultAsync<int>("foo"); - Assert.Equal(21, value); - } - - [Theory] - [ClassData(typeof(SerializerTypesClassData))] - public void SpecificDistributedCacheDurationWithFailSafeWorks(SerializerType serializerType) - { - var distributedCache = new MemoryDistributedCache(Options.Create(new MemoryDistributedCacheOptions())); - using var fusionCache = new FusionCache(CreateFusionCacheOptions()).SetupDistributedCache(distributedCache, TestsUtils.GetSerializer(serializerType)); - fusionCache.Set<int>("foo", 21, opt => opt.SetFailSafe(true).SetDuration(TimeSpan.FromSeconds(1)).SetDistributedCacheDuration(TimeSpan.FromMinutes(1))); - Thread.Sleep(TimeSpan.FromSeconds(2)); - var value = fusionCache.GetOrDefault<int>("foo"); - Assert.Equal(21, value); - } - - [Theory] - [ClassData(typeof(SerializerTypesClassData))] - public async Task DistributedCacheFailSafeMaxDurationWorksAsync(SerializerType serializerType) - { - var distributedCache = new MemoryDistributedCache(Options.Create(new MemoryDistributedCacheOptions())); - using var fusionCache = new FusionCache(CreateFusionCacheOptions()).SetupDistributedCache(distributedCache, TestsUtils.GetSerializer(serializerType)); - await fusionCache.SetAsync<int>("foo", 21, opt => opt.SetDuration(TimeSpan.FromSeconds(1)).SetFailSafe(true, TimeSpan.FromSeconds(2)).SetDistributedCacheFailSafeOptions(TimeSpan.FromMinutes(10))); - await Task.Delay(TimeSpan.FromSeconds(2)); - var value = await fusionCache.GetOrDefaultAsync<int>("foo", opt => opt.SetFailSafe(true)); - Assert.Equal(21, value); - } - - [Theory] - [ClassData(typeof(SerializerTypesClassData))] - public void DistributedCacheFailSafeMaxDurationWorks(SerializerType serializerType) - { - var distributedCache = new MemoryDistributedCache(Options.Create(new MemoryDistributedCacheOptions())); - using var fusionCache = new FusionCache(CreateFusionCacheOptions()).SetupDistributedCache(distributedCache, TestsUtils.GetSerializer(serializerType)); - fusionCache.Set<int>("foo", 21, opt => opt.SetDuration(TimeSpan.FromSeconds(1)).SetFailSafe(true, TimeSpan.FromSeconds(2)).SetDistributedCacheFailSafeOptions(TimeSpan.FromMinutes(10))); - Thread.Sleep(TimeSpan.FromSeconds(2)); - var value = fusionCache.GetOrDefault<int>("foo", opt => opt.SetFailSafe(true)); - Assert.Equal(21, value); - } - - [Theory] - [ClassData(typeof(SerializerTypesClassData))] - public async Task DistributedCacheFailSafeMaxDurationNormalizationOccursAsync(SerializerType serializerType) - { - var duration = TimeSpan.FromSeconds(5); - var maxDuration = TimeSpan.FromSeconds(1); - - var distributedCache = new MemoryDistributedCache(Options.Create(new MemoryDistributedCacheOptions())); - using var fusionCache = new FusionCache(CreateFusionCacheOptions()).SetupDistributedCache(distributedCache, TestsUtils.GetSerializer(serializerType)); - await fusionCache.SetAsync<int>("foo", 21, opt => opt.SetDuration(duration).SetFailSafe(true, maxDuration).SetDistributedCacheFailSafeOptions(maxDuration)); - await Task.Delay(maxDuration.PlusALittleBit()); - var value = await fusionCache.GetOrDefaultAsync<int>("foo", opt => opt.SetFailSafe(true)); - Assert.Equal(21, value); - } - - [Theory] - [ClassData(typeof(SerializerTypesClassData))] - public void DistributedCacheFailSafeMaxDurationNormalizationOccurs(SerializerType serializerType) - { - var duration = TimeSpan.FromSeconds(5); - var maxDuration = TimeSpan.FromSeconds(1); - - var distributedCache = new MemoryDistributedCache(Options.Create(new MemoryDistributedCacheOptions())); - using var fusionCache = new FusionCache(CreateFusionCacheOptions()).SetupDistributedCache(distributedCache, TestsUtils.GetSerializer(serializerType)); - fusionCache.Set<int>("foo", 21, opt => opt.SetDuration(duration).SetFailSafe(true, maxDuration).SetDistributedCacheFailSafeOptions(maxDuration)); - Thread.Sleep(maxDuration.PlusALittleBit()); - var value = fusionCache.GetOrDefault<int>("foo", opt => opt.SetFailSafe(true)); - Assert.Equal(21, value); - } - - [Theory] - [ClassData(typeof(SerializerTypesClassData))] - public async Task MemoryExpirationAlignedWithDistributedAsync(SerializerType serializerType) - { - var firstDuration = TimeSpan.FromSeconds(4); - var secondDuration = TimeSpan.FromSeconds(10); - - 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)) - ; - - await fusionCache1.SetAsync<int>("foo", 21, opt => opt.SetDuration(firstDuration)); - await Task.Delay(firstDuration / 2); - var v1 = await fusionCache2.GetOrDefaultAsync<int>("foo", 42, opt => opt.SetDuration(secondDuration)); - await Task.Delay(firstDuration + TimeSpan.FromSeconds(1)); - var v2 = await fusionCache2.GetOrDefaultAsync<int>("foo", 42, opt => opt.SetDuration(secondDuration)); - - Assert.Equal(21, v1); - Assert.Equal(42, v2); - } - - [Theory] - [ClassData(typeof(SerializerTypesClassData))] - public void MemoryExpirationAlignedWithDistributed(SerializerType serializerType) - { - var firstDuration = TimeSpan.FromSeconds(4); - var secondDuration = TimeSpan.FromSeconds(10); - - 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.Set<int>("foo", 21, opt => opt.SetDuration(firstDuration)); - Thread.Sleep(firstDuration / 2); - var v1 = fusionCache2.GetOrDefault<int>("foo", 42, opt => opt.SetDuration(secondDuration)); - Thread.Sleep(firstDuration + TimeSpan.FromSeconds(1)); - var v2 = fusionCache2.GetOrDefault<int>("foo", 42, opt => opt.SetDuration(secondDuration)); - - Assert.Equal(21, v1); - Assert.Equal(42, v2); - } - - [Theory] - [ClassData(typeof(SerializerTypesClassData))] - public async Task CanSkipDistributedCacheAsync(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)); - - var v1 = await fusionCache1.GetOrSetAsync<int>("foo", 1, opt => opt.SetDuration(TimeSpan.FromSeconds(10)).SetFailSafe(true).SetSkipDistributedCache(true, true)); - var v2 = await fusionCache2.GetOrSetAsync<int>("foo", 2, opt => opt.SetDuration(TimeSpan.FromSeconds(10)).SetFailSafe(true)); - - Assert.Equal(1, v1); - Assert.Equal(2, v2); - - var v3 = await fusionCache1.GetOrSetAsync<int>("bar", 3, opt => opt.SetDuration(TimeSpan.FromSeconds(2)).SetFailSafe(true)); - var v4 = await fusionCache2.GetOrSetAsync<int>("bar", 4, opt => opt.SetDuration(TimeSpan.FromSeconds(2)).SetFailSafe(true).SetSkipDistributedCache(true, true)); - - Assert.Equal(3, v3); - Assert.Equal(4, v4); - } - - [Theory] - [ClassData(typeof(SerializerTypesClassData))] - public void CanSkipDistributedCache(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)); - - var v1 = fusionCache1.GetOrSet<int>("foo", 1, opt => opt.SetDuration(TimeSpan.FromSeconds(10)).SetFailSafe(true).SetSkipDistributedCache(true, true)); - var v2 = fusionCache2.GetOrSet<int>("foo", 2, opt => opt.SetDuration(TimeSpan.FromSeconds(10)).SetFailSafe(true)); - - Assert.Equal(1, v1); - Assert.Equal(2, v2); - - var v3 = fusionCache1.GetOrSet<int>("bar", 3, opt => opt.SetDuration(TimeSpan.FromSeconds(2)).SetFailSafe(true)); - var v4 = fusionCache2.GetOrSet<int>("bar", 4, opt => opt.SetDuration(TimeSpan.FromSeconds(2)).SetFailSafe(true).SetSkipDistributedCache(true, true)); - - Assert.Equal(3, v3); - Assert.Equal(4, v4); - } - - [Theory] - [ClassData(typeof(SerializerTypesClassData))] - public async Task CanSkipDistributedReadWhenStaleAsync(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)); - - var v1 = await fusionCache1.GetOrSetAsync<int>("foo", 1, opt => opt.SetDuration(TimeSpan.FromSeconds(2)).SetFailSafe(true)); - var v2 = await fusionCache2.GetOrSetAsync<int>("foo", 2, opt => opt.SetDuration(TimeSpan.FromSeconds(2)).SetFailSafe(true)); - - Assert.Equal(1, v1); - Assert.Equal(1, v2); - - await Task.Delay(TimeSpan.FromSeconds(2).PlusALittleBit()); - - v1 = await fusionCache1.GetOrSetAsync<int>("foo", 3, opt => opt.SetDuration(TimeSpan.FromSeconds(2)).SetFailSafe(true)); - v2 = await fusionCache2.GetOrSetAsync<int>("foo", 4, opt => opt.SetDuration(TimeSpan.FromSeconds(2)).SetFailSafe(true).SetSkipDistributedCacheReadWhenStale(true)); - - Assert.Equal(3, v1); - Assert.Equal(4, v2); - } - - [Theory] - [ClassData(typeof(SerializerTypesClassData))] - public void CanSkipDistributedReadWhenStale(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)); - - var v1 = fusionCache1.GetOrSet<int>("foo", 1, opt => opt.SetDuration(TimeSpan.FromSeconds(2)).SetFailSafe(true)); - var v2 = fusionCache2.GetOrSet<int>("foo", 2, opt => opt.SetDuration(TimeSpan.FromSeconds(2)).SetFailSafe(true)); - - Assert.Equal(1, v1); - Assert.Equal(1, v2); - - Thread.Sleep(TimeSpan.FromSeconds(2).PlusALittleBit()); - - v1 = fusionCache1.GetOrSet<int>("foo", 3, opt => opt.SetDuration(TimeSpan.FromSeconds(2)).SetFailSafe(true)); - v2 = fusionCache2.GetOrSet<int>("foo", 4, opt => opt.SetDuration(TimeSpan.FromSeconds(2)).SetFailSafe(true).SetSkipDistributedCacheReadWhenStale(true)); - - Assert.Equal(3, v1); - Assert.Equal(4, v2); - } - - [Theory] - [ClassData(typeof(SerializerTypesClassData))] - public async Task CanHandleConditionalRefreshAsync(SerializerType serializerType) - { - static async Task<int> FakeGetAsync(FusionCacheFactoryExecutionContext<int> ctx, FakeHttpEndpoint endpoint) - { - FakeHttpResponse resp; - - if (ctx.HasETag && ctx.HasStaleValue) - { - // ETAG + STALE VALUE -> TRY WITH A CONDITIONAL GET - resp = endpoint.Get(ctx.ETag); - - if (resp.StatusCode == 304) - { - // NOT MODIFIED -> RETURN STALE VALUE - return ctx.NotModified(); - } - } - else - { - // NO STALE VALUE OR NO ETAG -> NORMAL (FULL) GET - resp = endpoint.Get(); - } - - return ctx.Modified( - resp.Content.GetValueOrDefault(), - resp.ETag - ); - } - - var duration = TimeSpan.FromSeconds(1); - var endpoint = new FakeHttpEndpoint(1); - - var distributedCache = new MemoryDistributedCache(Options.Create(new MemoryDistributedCacheOptions())); - using var cache = new FusionCache(CreateFusionCacheOptions()).SetupDistributedCache(distributedCache, TestsUtils.GetSerializer(serializerType)); - - // TOT REQ + 1 / FULL RESP + 1 - var v1 = await cache.GetOrSetAsync<int>("foo", async (ctx, _) => await FakeGetAsync(ctx, endpoint), opt => opt.SetDuration(duration).SetFailSafe(true)); - - // CACHED -> NO INCR - var v2 = await cache.GetOrSetAsync<int>("foo", async (ctx, _) => await FakeGetAsync(ctx, endpoint), opt => opt.SetDuration(duration).SetFailSafe(true)); - - // LET THE CACHE EXPIRE - await Task.Delay(duration.PlusALittleBit()); - - // TOT REQ + 1 / COND REQ + 1 / NOT MOD RESP + 1 - var v3 = await cache.GetOrSetAsync<int>("foo", async (ctx, _) => await FakeGetAsync(ctx, endpoint), opt => opt.SetDuration(duration).SetFailSafe(true)); - - // LET THE CACHE EXPIRE - await Task.Delay(duration.PlusALittleBit()); - - // TOT REQ + 1 / COND REQ + 1 / NOT MOD RESP + 1 - var v4 = await cache.GetOrSetAsync<int>("foo", async (ctx, _) => await FakeGetAsync(ctx, endpoint), opt => opt.SetDuration(duration).SetFailSafe(true)); - - // SET VALUE -> CHANGE LAST MODIFIED - endpoint.SetValue(42); - - // LET THE CACHE EXPIRE - await Task.Delay(duration.PlusALittleBit()); - - // TOT REQ + 1 / COND REQ + 1 / FULL RESP + 1 - var v5 = await cache.GetOrSetAsync<int>("foo", async (ctx, _) => await FakeGetAsync(ctx, endpoint), opt => opt.SetDuration(duration).SetFailSafe(true)); - - Assert.Equal(4, endpoint.TotalRequestsCount); - Assert.Equal(3, endpoint.ConditionalRequestsCount); - Assert.Equal(2, endpoint.FullResponsesCount); - Assert.Equal(2, endpoint.NotModifiedResponsesCount); - - Assert.Equal(1, v1); - Assert.Equal(1, v2); - Assert.Equal(1, v3); - Assert.Equal(1, v4); - Assert.Equal(42, v5); - } - - [Theory] - [ClassData(typeof(SerializerTypesClassData))] - public void CanHandleConditionalRefresh(SerializerType serializerType) - { - static int FakeGet(FusionCacheFactoryExecutionContext<int> ctx, FakeHttpEndpoint endpoint) - { - FakeHttpResponse resp; - - if (ctx.HasETag && ctx.HasStaleValue) - { - // ETAG + STALE VALUE -> TRY WITH A CONDITIONAL GET - resp = endpoint.Get(ctx.ETag); - - if (resp.StatusCode == 304) - { - // NOT MODIFIED -> RETURN STALE VALUE - return ctx.NotModified(); - } - } - else - { - // NO STALE VALUE OR NO ETAG -> NORMAL (FULL) GET - resp = endpoint.Get(); - } - - return ctx.Modified( - resp.Content.GetValueOrDefault(), - resp.ETag - ); - } - - var duration = TimeSpan.FromSeconds(1); - var endpoint = new FakeHttpEndpoint(1); - - var distributedCache = new MemoryDistributedCache(Options.Create(new MemoryDistributedCacheOptions())); - using var cache = new FusionCache(CreateFusionCacheOptions()).SetupDistributedCache(distributedCache, TestsUtils.GetSerializer(serializerType)); - - // TOT REQ + 1 / FULL RESP + 1 - var v1 = cache.GetOrSet<int>("foo", (ctx, _) => FakeGet(ctx, endpoint), opt => opt.SetDuration(duration).SetFailSafe(true)); - - // CACHED -> NO INCR - var v2 = cache.GetOrSet<int>("foo", (ctx, _) => FakeGet(ctx, endpoint), opt => opt.SetDuration(duration).SetFailSafe(true)); - - // LET THE CACHE EXPIRE - Thread.Sleep(duration.PlusALittleBit()); - - // TOT REQ + 1 / COND REQ + 1 / NOT MOD RESP + 1 - var v3 = cache.GetOrSet<int>("foo", (ctx, _) => FakeGet(ctx, endpoint), opt => opt.SetDuration(duration).SetFailSafe(true)); - - // LET THE CACHE EXPIRE - Thread.Sleep(duration.PlusALittleBit()); - - // TOT REQ + 1 / COND REQ + 1 / NOT MOD RESP + 1 - var v4 = cache.GetOrSet<int>("foo", (ctx, _) => FakeGet(ctx, endpoint), opt => opt.SetDuration(duration).SetFailSafe(true)); - - // SET VALUE -> CHANGE LAST MODIFIED - endpoint.SetValue(42); - - // LET THE CACHE EXPIRE - Thread.Sleep(duration.PlusALittleBit()); - - // TOT REQ + 1 / COND REQ + 1 / FULL RESP + 1 - var v5 = cache.GetOrSet<int>("foo", (ctx, _) => FakeGet(ctx, endpoint), opt => opt.SetDuration(duration).SetFailSafe(true)); - - Assert.Equal(4, endpoint.TotalRequestsCount); - Assert.Equal(3, endpoint.ConditionalRequestsCount); - Assert.Equal(2, endpoint.FullResponsesCount); - Assert.Equal(2, endpoint.NotModifiedResponsesCount); - - Assert.Equal(1, v1); - Assert.Equal(1, v2); - Assert.Equal(1, v3); - Assert.Equal(1, v4); - Assert.Equal(42, v5); - } - - [Theory] - [ClassData(typeof(SerializerTypesClassData))] - public async Task CanHandleEagerRefreshAsync(SerializerType serializerType) - { - var duration = TimeSpan.FromSeconds(2); - var eagerRefreshThreshold = 0.2f; - - var distributedCache = new MemoryDistributedCache(Options.Create(new MemoryDistributedCacheOptions())); - using var cache = new FusionCache(CreateFusionCacheOptions()).SetupDistributedCache(distributedCache, TestsUtils.GetSerializer(serializerType)); - - cache.DefaultEntryOptions.Duration = duration; - cache.DefaultEntryOptions.EagerRefreshThreshold = eagerRefreshThreshold; - - // EXECUTE FACTORY - var v1 = await cache.GetOrSetAsync<long>("foo", async _ => DateTimeOffset.UtcNow.Ticks); - - // USE CACHED VALUE - var v2 = await cache.GetOrSetAsync<long>("foo", async _ => DateTimeOffset.UtcNow.Ticks); - - // WAIT FOR EAGER REFRESH THRESHOLD TO BE HIT - var eagerDuration = TimeSpan.FromMilliseconds(duration.TotalMilliseconds * eagerRefreshThreshold).Add(TimeSpan.FromMilliseconds(10)); - await Task.Delay(eagerDuration); - - // EAGER REFRESH KICKS IN - var v3 = await cache.GetOrSetAsync<long>("foo", async _ => DateTimeOffset.UtcNow.Ticks); - - // WAIT FOR THE BACKGROUND FACTORY (EAGER REFRESH) TO COMPLETE - await Task.Delay(TimeSpan.FromMilliseconds(50)); - - // GET THE REFRESHED VALUE - var v4 = await cache.GetOrSetAsync<long>("foo", async _ => DateTimeOffset.UtcNow.Ticks); - - // WAIT FOR EXPIRATION - await Task.Delay(duration.PlusALittleBit()); - - // EXECUTE FACTORY AGAIN - var v5 = await cache.GetOrSetAsync<long>("foo", async _ => DateTimeOffset.UtcNow.Ticks); - - // USE CACHED VALUE - var v6 = await cache.GetOrSetAsync<long>("foo", async _ => DateTimeOffset.UtcNow.Ticks); - - Assert.Equal(v1, v2); - Assert.Equal(v2, v3); - Assert.True(v4 > v3); - Assert.True(v5 > v4); - Assert.Equal(v5, v6); - } - - [Theory] - [ClassData(typeof(SerializerTypesClassData))] - public void CanHandleEagerRefresh(SerializerType serializerType) - { - var duration = TimeSpan.FromSeconds(2); - var eagerRefreshThreshold = 0.2f; - - var distributedCache = new MemoryDistributedCache(Options.Create(new MemoryDistributedCacheOptions())); - using var cache = new FusionCache(CreateFusionCacheOptions()).SetupDistributedCache(distributedCache, TestsUtils.GetSerializer(serializerType)); - - cache.DefaultEntryOptions.Duration = duration; - cache.DefaultEntryOptions.EagerRefreshThreshold = eagerRefreshThreshold; - - // EXECUTE FACTORY - var v1 = cache.GetOrSet<long>("foo", _ => DateTimeOffset.UtcNow.Ticks); - - // USE CACHED VALUE - var v2 = cache.GetOrSet<long>("foo", _ => DateTimeOffset.UtcNow.Ticks); - - // WAIT FOR EAGER REFRESH THRESHOLD TO BE HIT - var eagerDuration = TimeSpan.FromMilliseconds(duration.TotalMilliseconds * eagerRefreshThreshold).Add(TimeSpan.FromMilliseconds(10)); - Thread.Sleep(eagerDuration); - - // EAGER REFRESH KICKS IN - var v3 = cache.GetOrSet<long>("foo", _ => DateTimeOffset.UtcNow.Ticks); - - // WAIT FOR THE BACKGROUND FACTORY (EAGER REFRESH) TO COMPLETE - Thread.Sleep(TimeSpan.FromMilliseconds(50)); - - // GET THE REFRESHED VALUE - var v4 = cache.GetOrSet<long>("foo", _ => DateTimeOffset.UtcNow.Ticks); - - // WAIT FOR EXPIRATION - Thread.Sleep(duration.PlusALittleBit()); - - // EXECUTE FACTORY AGAIN - var v5 = cache.GetOrSet<long>("foo", _ => DateTimeOffset.UtcNow.Ticks); - - // USE CACHED VALUE - var v6 = cache.GetOrSet<long>("foo", _ => DateTimeOffset.UtcNow.Ticks); - - Assert.Equal(v1, v2); - Assert.Equal(v2, v3); - Assert.True(v4 > v3); - Assert.True(v5 > v4); - Assert.Equal(v5, v6); - } - - [Theory] - [ClassData(typeof(SerializerTypesClassData))] - public async Task CanSkipMemoryCacheAsync(SerializerType serializerType) - { - var distributedCache = new MemoryDistributedCache(Options.Create(new MemoryDistributedCacheOptions())); - using var cache1 = new FusionCache(CreateFusionCacheOptions()).SetupDistributedCache(distributedCache, TestsUtils.GetSerializer(serializerType)); - using var cache2 = new FusionCache(CreateFusionCacheOptions()).SetupDistributedCache(distributedCache, TestsUtils.GetSerializer(serializerType)); - - cache1.DefaultEntryOptions.Duration = TimeSpan.FromMinutes(10); - cache2.DefaultEntryOptions.Duration = TimeSpan.FromMinutes(10); - - // SET ON CACHE 1 AND ON DISTRIBUTED CACHE - var v1 = await cache1.GetOrSetAsync<int>("foo", async _ => 10); - - // GET FROM DISTRIBUTED CACHE AND SET IT ON CACHE 2 - var v2 = await cache2.GetOrSetAsync<int>("foo", async _ => 20); - - // SET ON DISTRIBUTED CACHE BUT SKIP CACHE 1 - await cache1.SetAsync<int>("foo", 30, opt => opt.SetSkipMemoryCache()); - - // GET FROM CACHE 1 (10) AND DON'T CALL THE FACTORY - var v3 = await cache1.GetOrSetAsync<int>("foo", async _ => 40); - - // GET FROM CACHE 2 (10) AND DON'T CALL THE FACTORY - var v4 = await cache2.GetOrSetAsync<int>("foo", async _ => 50); - - // SKIP CACHE 2, GET FROM DISTRIBUTED CACHE (30) - var v5 = await cache2.GetOrSetAsync<int>("foo", async _ => 60, opt => opt.SetSkipMemoryCache()); - - Assert.Equal(10, v1); - Assert.Equal(10, v2); - Assert.Equal(10, v3); - Assert.Equal(10, v4); - Assert.Equal(30, v5); - } - - [Theory] - [ClassData(typeof(SerializerTypesClassData))] - public void CanSkipMemoryCache(SerializerType serializerType) - { - var distributedCache = new MemoryDistributedCache(Options.Create(new MemoryDistributedCacheOptions())); - using var cache1 = new FusionCache(CreateFusionCacheOptions()).SetupDistributedCache(distributedCache, TestsUtils.GetSerializer(serializerType)); - using var cache2 = new FusionCache(CreateFusionCacheOptions()).SetupDistributedCache(distributedCache, TestsUtils.GetSerializer(serializerType)); - - cache1.DefaultEntryOptions.Duration = TimeSpan.FromMinutes(10); - cache2.DefaultEntryOptions.Duration = TimeSpan.FromMinutes(10); - - // SET ON CACHE 1 AND ON DISTRIBUTED CACHE - var v1 = cache1.GetOrSet<int>("foo", _ => 10); - - // GET FROM DISTRIBUTED CACHE AND SET IT ON CACHE 2 - var v2 = cache2.GetOrSet<int>("foo", _ => 20); - - // SET ON DISTRIBUTED CACHE BUT SKIP CACHE 1 - cache1.Set<int>("foo", 30, opt => opt.SetSkipMemoryCache()); - - // GET FROM CACHE 1 (10) AND DON'T CALL THE FACTORY - var v3 = cache1.GetOrSet<int>("foo", _ => 40); - - // GET FROM CACHE 2 (10) AND DON'T CALL THE FACTORY - var v4 = cache2.GetOrSet<int>("foo", _ => 50); - - // SKIP CACHE 2, GET FROM DISTRIBUTED CACHE (30) - var v5 = cache2.GetOrSet<int>("foo", _ => 60, opt => opt.SetSkipMemoryCache()); - - Assert.Equal(10, v1); - Assert.Equal(10, v2); - Assert.Equal(10, v3); - Assert.Equal(10, v4); - Assert.Equal(30, v5); - } - - [Theory] - [ClassData(typeof(SerializerTypesClassData))] - public async Task CanHandleIssuesWithBothDistributedCacheAndBackplaneAsync(SerializerType serializerType) - { - var backplaneConnectionId = Guid.NewGuid().ToString("N"); - - var defaultOptions = new FusionCacheOptions(); - - var distributedCache = new MemoryDistributedCache(Options.Create(new MemoryDistributedCacheOptions())); - var chaosDistributedCache = new ChaosDistributedCache(distributedCache); - - // SETUP CACHE A - var backplaneA = new MemoryBackplane(new MemoryBackplaneOptions() { ConnectionId = backplaneConnectionId }); - var chaosBackplaneA = new ChaosBackplane(backplaneA, logger: CreateLogger<ChaosBackplane>()); - using var cacheA = new FusionCache(CreateFusionCacheOptions(), logger: CreateLogger<FusionCache>()); - - cacheA.DefaultEntryOptions.Duration = TimeSpan.FromMinutes(10); - cacheA.DefaultEntryOptions.AllowBackgroundBackplaneOperations = false; - - cacheA.SetupDistributedCache(chaosDistributedCache, TestsUtils.GetSerializer(serializerType)); - cacheA.SetupBackplane(chaosBackplaneA); - - // SETUP CACHE B - var backplaneB = new MemoryBackplane(new MemoryBackplaneOptions() { ConnectionId = backplaneConnectionId }); - var chaosBackplaneB = new ChaosBackplane(backplaneB, logger: CreateLogger<ChaosBackplane>()); - using var cacheB = new FusionCache(CreateFusionCacheOptions(), logger: CreateLogger<FusionCache>()); - - cacheB.DefaultEntryOptions.Duration = TimeSpan.FromMinutes(10); - cacheB.DefaultEntryOptions.AllowBackgroundBackplaneOperations = false; - - cacheB.SetupDistributedCache(chaosDistributedCache, TestsUtils.GetSerializer(serializerType)); - cacheB.SetupBackplane(chaosBackplaneB); - - // SET ON CACHE A AND ON DISTRIBUTED CACHE + NOTIFY ON BACKPLANE - var vA1 = await cacheA.GetOrSetAsync<int>("foo", async _ => 10); - - // GET FROM DISTRIBUTED CACHE AND SET IT ON CACHE B - var vB1 = await cacheB.GetOrSetAsync<int>("foo", async _ => 20); - - // IN-SYNC - Assert.Equal(10, vA1); - Assert.Equal(10, vB1); - - // DISABLE DISTRIBUTED CACHE AND BACKPLANE - chaosDistributedCache.SetAlwaysThrow(); - chaosBackplaneA.SetAlwaysThrow(); - chaosBackplaneB.SetAlwaysThrow(); - - // SET ON CACHE B (NO DISTRIBUTED CACHE OR BACKPLANE, BECAUSE CHAOS) - await cacheB.SetAsync<int>("foo", 30); - - // GET FROM CACHE A (MEMORY CACHE) - var vA2 = await cacheA.GetOrDefaultAsync<int>("foo", 40); - - // GET FROM CACHE B (MEMORY CACHE) - var vB2 = await cacheB.GetOrDefaultAsync<int>("foo", 50); - - // NOT IN-SYNC - Assert.Equal(10, vA2); - Assert.Equal(30, vB2); - - // RE-ENABLE DISTRIBUTED CACHE AND BACKPLANE (SEND AUTO-RECOVERY NOTIFICATIONS) - chaosDistributedCache.SetNeverThrow(); - chaosBackplaneA.SetNeverThrow(); - chaosBackplaneB.SetNeverThrow(); - - // GIVE IT SOME TIME - await Task.Delay(defaultOptions.BackplaneAutoRecoveryReconnectDelay.PlusALittleBit()); - - // SET ON CACHE A AND ON DISTRIBUTED CACHE + NOTIFY ON BACKPLANE - var vA3 = await cacheA.GetOrSetAsync<int>("foo", async _ => 60); - - // GET FROM DISTRIBUTED CACHE AND SET IT ON CACHE B - var vB3 = await cacheB.GetOrSetAsync<int>("foo", async _ => 70); - - Assert.Equal(60, vA3); - Assert.Equal(60, vB3); - - // GET FROM CACHE A (MEMORY CACHE) - var vA4 = await cacheA.GetOrSetAsync<int>("foo", async _ => 120); - - // GET FROM CACHE A (MEMORY CACHE) - var vB4 = await cacheB.GetOrSetAsync<int>("foo", async _ => 130); - - Assert.Equal(60, vA4); - Assert.Equal(60, vB4); - } - - [Theory] - [ClassData(typeof(SerializerTypesClassData))] - public void CanHandleIssuesWithBothDistributedCacheAndBackplane(SerializerType serializerType) - { - var backplaneConnectionId = Guid.NewGuid().ToString("N"); - - var defaultOptions = new FusionCacheOptions(); - - var distributedCache = new MemoryDistributedCache(Options.Create(new MemoryDistributedCacheOptions())); - var chaosDistributedCache = new ChaosDistributedCache(distributedCache); - - // SETUP CACHE A - var backplaneA = new MemoryBackplane(new MemoryBackplaneOptions() { ConnectionId = backplaneConnectionId }); - var chaosBackplaneA = new ChaosBackplane(backplaneA, logger: CreateLogger<ChaosBackplane>()); - using var cacheA = new FusionCache(CreateFusionCacheOptions(), logger: CreateLogger<FusionCache>()); - - cacheA.DefaultEntryOptions.Duration = TimeSpan.FromMinutes(10); - cacheA.DefaultEntryOptions.AllowBackgroundBackplaneOperations = false; - - cacheA.SetupDistributedCache(chaosDistributedCache, TestsUtils.GetSerializer(serializerType)); - cacheA.SetupBackplane(chaosBackplaneA); - - // SETUP CACHE B - var backplaneB = new MemoryBackplane(new MemoryBackplaneOptions() { ConnectionId = backplaneConnectionId }); - var chaosBackplaneB = new ChaosBackplane(backplaneB, logger: CreateLogger<ChaosBackplane>()); - using var cacheB = new FusionCache(CreateFusionCacheOptions(), logger: CreateLogger<FusionCache>()); - - cacheB.DefaultEntryOptions.Duration = TimeSpan.FromMinutes(10); - cacheB.DefaultEntryOptions.AllowBackgroundBackplaneOperations = false; - - cacheB.SetupDistributedCache(chaosDistributedCache, TestsUtils.GetSerializer(serializerType)); - cacheB.SetupBackplane(chaosBackplaneB); - - // SET ON CACHE A AND ON DISTRIBUTED CACHE + NOTIFY ON BACKPLANE - var vA1 = cacheA.GetOrSet<int>("foo", _ => 10); - - // GET FROM DISTRIBUTED CACHE AND SET IT ON CACHE B - var vB1 = cacheB.GetOrSet<int>("foo", _ => 20); - - // IN-SYNC - Assert.Equal(10, vA1); - Assert.Equal(10, vB1); - - // DISABLE DISTRIBUTED CACHE AND BACKPLANE - chaosDistributedCache.SetAlwaysThrow(); - chaosBackplaneA.SetAlwaysThrow(); - chaosBackplaneB.SetAlwaysThrow(); - - // SET ON CACHE B (NO DISTRIBUTED CACHE OR BACKPLANE, BECAUSE CHAOS) - cacheB.Set<int>("foo", 30); - - // GET FROM CACHE A (MEMORY CACHE) - var vA2 = cacheA.GetOrDefault<int>("foo", 40); - - // GET FROM CACHE B (MEMORY CACHE) - var vB2 = cacheB.GetOrDefault<int>("foo", 50); - - // NOT IN-SYNC - Assert.Equal(10, vA2); - Assert.Equal(30, vB2); - - // GIVE IT SOME TIME - Thread.Sleep(defaultOptions.BackplaneAutoRecoveryReconnectDelay.PlusALittleBit()); - - // RE-ENABLE DISTRIBUTED CACHE AND BACKPLANE (SEND AUTO-RECOVERY NOTIFICATIONS) - chaosDistributedCache.SetNeverThrow(); - chaosBackplaneA.SetNeverThrow(); - chaosBackplaneB.SetNeverThrow(); - - // GIVE IT SOME TIME - Thread.Sleep(defaultOptions.BackplaneAutoRecoveryReconnectDelay.PlusALittleBit()); - - // SET ON CACHE A AND ON DISTRIBUTED CACHE + NOTIFY ON BACKPLANE - var vA3 = cacheA.GetOrSet<int>("foo", _ => 60); - - // GET FROM DISTRIBUTED CACHE AND SET IT ON CACHE B - var vB3 = cacheB.GetOrSet<int>("foo", _ => 70); - - Assert.Equal(60, vA3); - Assert.Equal(60, vB3); - - // GET FROM CACHE A (MEMORY CACHE) - var vA4 = cacheA.GetOrSet<int>("foo", _ => 120); - - // GET FROM CACHE A (MEMORY CACHE) - var vB4 = cacheB.GetOrSet<int>("foo", _ => 130); - - Assert.Equal(60, vA4); - Assert.Equal(60, vB4); - } - } -} diff --git a/tests/ZiggyCreatures.FusionCache.Tests/OtherLibs/CacheStampedeTests_CacheManager.cs b/tests/ZiggyCreatures.FusionCache.Tests/OtherLibs/CacheStampedeTests_CacheManager.cs new file mode 100644 index 00000000..8392b1b6 --- /dev/null +++ b/tests/ZiggyCreatures.FusionCache.Tests/OtherLibs/CacheStampedeTests_CacheManager.cs @@ -0,0 +1,45 @@ +using System; +using System.Threading; +using System.Threading.Tasks; +using CacheManager.Core; +using Xunit; + +namespace FusionCacheTests.OtherLibs; + +// REMOVE THE abstract MODIFIER TO RUN THESE TESTS +public abstract class CacheStampedeTests_CacheManager +{ + private static readonly TimeSpan FactoryDuration = TimeSpan.FromMilliseconds(500); + + [Theory] + [InlineData(10)] + [InlineData(100)] + [InlineData(1_000)] + public void OnlyOneFactoryGetsCalledEvenInHighConcurrency(int accessorsCount) + { + using (var cache = CacheFactory.Build<int>(p => p.WithMicrosoftMemoryCacheHandle())) + { + var factoryCallsCount = 0; + + Parallel.For(0, accessorsCount, _ => + { + cache.GetOrAdd( + "foo", + key => + { + Interlocked.Increment(ref factoryCallsCount); + Thread.Sleep(FactoryDuration); + return new CacheItem<int>( + key, + 42, + ExpirationMode.Absolute, + TimeSpan.FromSeconds(10) + ); + } + ); + }); + + Assert.Equal(1, factoryCallsCount); + } + } +} diff --git a/tests/ZiggyCreatures.FusionCache.Tests/OtherLibs/CacheStampedeTests_CacheTower.cs b/tests/ZiggyCreatures.FusionCache.Tests/OtherLibs/CacheStampedeTests_CacheTower.cs new file mode 100644 index 00000000..bb2d0e1f --- /dev/null +++ b/tests/ZiggyCreatures.FusionCache.Tests/OtherLibs/CacheStampedeTests_CacheTower.cs @@ -0,0 +1,50 @@ +using System; +using System.Collections.Concurrent; +using System.Threading; +using System.Threading.Tasks; +using CacheTower; +using CacheTower.Extensions; +using CacheTower.Providers.Memory; +using Xunit; + +namespace FusionCacheTests.OtherLibs; + +// REMOVE THE abstract MODIFIER TO RUN THESE TESTS +public abstract class CacheStampedeTests_CacheTower +{ + private static readonly TimeSpan FactoryDuration = TimeSpan.FromMilliseconds(500); + + [Theory] + [InlineData(10)] + [InlineData(100)] + [InlineData(1_000)] + public async Task OnlyOneFactoryGetsCalledEvenInHighConcurrencyAsync(int accessorsCount) + { + await using (var cache = new CacheStack(null, new CacheStackOptions(new[] { new MemoryCacheLayer() }) { Extensions = new[] { new AutoCleanupExtension(TimeSpan.FromMinutes(5)) } })) + { + var cacheSettings = new CacheSettings(TimeSpan.FromSeconds(10)); + + var factoryCallsCount = 0; + + var tasks = new ConcurrentBag<Task>(); + Parallel.For(0, accessorsCount, _ => + { + var task = cache.GetOrSetAsync<int>( + "foo", + async old => + { + Interlocked.Increment(ref factoryCallsCount); + await Task.Delay(FactoryDuration); + return 42; + }, + cacheSettings + ); + tasks.Add(task.AsTask()); + }); + + await Task.WhenAll(tasks); + + Assert.Equal(1, factoryCallsCount); + } + } +} diff --git a/tests/ZiggyCreatures.FusionCache.Tests/OtherLibs/CacheStampedeTests_EasyCaching.cs b/tests/ZiggyCreatures.FusionCache.Tests/OtherLibs/CacheStampedeTests_EasyCaching.cs new file mode 100644 index 00000000..c6ecdd18 --- /dev/null +++ b/tests/ZiggyCreatures.FusionCache.Tests/OtherLibs/CacheStampedeTests_EasyCaching.cs @@ -0,0 +1,132 @@ +using System; +using System.Collections.Concurrent; +using System.Threading; +using System.Threading.Tasks; +using EasyCaching.Core; +using Microsoft.Extensions.DependencyInjection; +using Xunit; + +namespace FusionCacheTests.OtherLibs; + +// REMOVE THE abstract MODIFIER TO RUN THESE TESTS +public abstract class CacheStampedeTests_EasyCaching +{ + private static readonly TimeSpan FactoryDuration = TimeSpan.FromMilliseconds(500); + + [Theory] + [InlineData(10)] + [InlineData(100)] + [InlineData(1_000)] + public async Task OnlyOneFactoryGetsCalledEvenInHighConcurrencyAsync(int accessorsCount) + { + var services = new ServiceCollection(); + services.AddEasyCaching(options => { options.UseInMemory("default"); }); + var serviceProvider = services.BuildServiceProvider(); + var factory = serviceProvider.GetRequiredService<IEasyCachingProviderFactory>(); + var cache = factory.GetCachingProvider("default"); + + var factoryCallsCount = 0; + + var tasks = new ConcurrentBag<Task>(); + Parallel.For(0, accessorsCount, _ => + { + var task = cache.GetAsync( + "foo", + async () => + { + Interlocked.Increment(ref factoryCallsCount); + await Task.Delay(FactoryDuration); + return 42; + }, + TimeSpan.FromSeconds(10) + ); + tasks.Add(task); + }); + + await Task.WhenAll(tasks); + + Assert.Equal(1, factoryCallsCount); + } + + [Theory] + [InlineData(10)] + [InlineData(100)] + [InlineData(1_000)] + public void OnlyOneFactoryGetsCalledEvenInHighConcurrency(int accessorsCount) + { + var services = new ServiceCollection(); + services.AddEasyCaching(options => { options.UseInMemory("default"); }); + var serviceProvider = services.BuildServiceProvider(); + var factory = serviceProvider.GetRequiredService<IEasyCachingProviderFactory>(); + var cache = factory.GetCachingProvider("default"); + + var factoryCallsCount = 0; + + Parallel.For(0, accessorsCount, _ => + { + cache.Get( + "foo", + () => + { + Interlocked.Increment(ref factoryCallsCount); + Thread.Sleep(FactoryDuration); + return 42; + }, + TimeSpan.FromSeconds(10) + ); + }); + + Assert.Equal(1, factoryCallsCount); + } + + [Theory] + [InlineData(10)] + [InlineData(100)] + [InlineData(1_000)] + public async Task OnlyOneFactoryGetsCalledEvenInMixedHighConcurrencyAsync(int accessorsCount) + { + var services = new ServiceCollection(); + services.AddEasyCaching(options => { options.UseInMemory("default"); }); + var serviceProvider = services.BuildServiceProvider(); + var factory = serviceProvider.GetRequiredService<IEasyCachingProviderFactory>(); + var cache = factory.GetCachingProvider("default"); + + var factoryCallsCount = 0; + + var tasks = new ConcurrentBag<Task>(); + Parallel.For(0, accessorsCount, idx => + { + if (idx % 2 == 0) + { + var task = cache.GetAsync( + "foo", + async () => + { + Interlocked.Increment(ref factoryCallsCount); + await Task.Delay(FactoryDuration); + return 42; + }, + TimeSpan.FromSeconds(10) + ); + tasks.Add(task); + } + else + { + cache.Get( + "foo", + () => + { + Interlocked.Increment(ref factoryCallsCount); + Thread.Sleep(FactoryDuration); + return 42; + }, + TimeSpan.FromSeconds(10) + ); + } + }); + + await Task.WhenAll(tasks); + + Assert.Equal(1, factoryCallsCount); + } +} diff --git a/tests/ZiggyCreatures.FusionCache.Tests/OtherLibs/CacheStampedeTests_LazyCache.cs b/tests/ZiggyCreatures.FusionCache.Tests/OtherLibs/CacheStampedeTests_LazyCache.cs new file mode 100644 index 00000000..b8bbb930 --- /dev/null +++ b/tests/ZiggyCreatures.FusionCache.Tests/OtherLibs/CacheStampedeTests_LazyCache.cs @@ -0,0 +1,129 @@ +using System; +using System.Collections.Concurrent; +using System.Threading; +using System.Threading.Tasks; +using LazyCache; +using LazyCache.Providers; +using Microsoft.Extensions.Caching.Memory; +using Xunit; + +namespace FusionCacheTests.OtherLibs; + +// REMOVE THE abstract MODIFIER TO RUN THESE TESTS +public abstract class CacheStampedeTests_LazyCache +{ + private static readonly TimeSpan FactoryDuration = TimeSpan.FromMilliseconds(500); + + [Theory] + [InlineData(10)] + [InlineData(100)] + [InlineData(1_000)] + public async Task OnlyOneFactoryGetsCalledEvenInHighConcurrencyAsync(int accessorsCount) + { + using (var memoryCache = new MemoryCache(new MemoryCacheOptions())) + { + var cache = new CachingService(new MemoryCacheProvider(memoryCache)); + cache.DefaultCachePolicy = new CacheDefaults { DefaultCacheDurationSeconds = 10 }; + + var factoryCallsCount = 0; + + var tasks = new ConcurrentBag<Task>(); + Parallel.For(0, accessorsCount, _ => + { + var task = cache.GetOrAddAsync( + "foo", + async _ => + { + Interlocked.Increment(ref factoryCallsCount); + await Task.Delay(FactoryDuration); + return 42; + } + ); + tasks.Add(task); + }); + + await Task.WhenAll(tasks); + + Assert.Equal(1, factoryCallsCount); + } + } + + [Theory] + [InlineData(10)] + [InlineData(100)] + [InlineData(1_000)] + public void OnlyOneFactoryGetsCalledEvenInHighConcurrency(int accessorsCount) + { + using (var memoryCache = new MemoryCache(new MemoryCacheOptions())) + { + var cache = new CachingService(new MemoryCacheProvider(memoryCache)); + cache.DefaultCachePolicy = new CacheDefaults { DefaultCacheDurationSeconds = 10 }; + + var factoryCallsCount = 0; + + Parallel.For(0, accessorsCount, _ => + { + cache.GetOrAdd( + "foo", + _ => + { + Interlocked.Increment(ref factoryCallsCount); + Thread.Sleep(FactoryDuration); + return 42; + } + ); + }); + + Assert.Equal(1, factoryCallsCount); + } + } + + [Theory] + [InlineData(10)] + [InlineData(100)] + [InlineData(1_000)] + public async Task OnlyOneFactoryGetsCalledEvenInMixedHighConcurrencyAsync(int accessorsCount) + { + using (var memoryCache = new MemoryCache(new MemoryCacheOptions())) + { + var cache = new CachingService(new MemoryCacheProvider(memoryCache)); + cache.DefaultCachePolicy = new CacheDefaults { DefaultCacheDurationSeconds = 10 }; + + var factoryCallsCount = 0; + + var tasks = new ConcurrentBag<Task>(); + Parallel.For(0, accessorsCount, idx => + { + if (idx % 2 == 0) + { + var task = cache.GetOrAddAsync( + "foo", + async _ => + { + Interlocked.Increment(ref factoryCallsCount); + await Task.Delay(FactoryDuration); + return 42; + } + ); + tasks.Add(task); + } + else + { + cache.GetOrAdd( + "foo", + _ => + { + Interlocked.Increment(ref factoryCallsCount); + Thread.Sleep(FactoryDuration); + return 42; + } + ); + } + }); + + await Task.WhenAll(tasks); + + Assert.Equal(1, factoryCallsCount); + } + } +} diff --git a/tests/ZiggyCreatures.FusionCache.Tests/Overloads/OverloadsCallsTryouts.cs b/tests/ZiggyCreatures.FusionCache.Tests/Overloads/OverloadsCallsTryouts.cs new file mode 100644 index 00000000..bc356b4a --- /dev/null +++ b/tests/ZiggyCreatures.FusionCache.Tests/Overloads/OverloadsCallsTryouts.cs @@ -0,0 +1,21 @@ +using System; +using System.Threading; +using System.Threading.Tasks; +using ZiggyCreatures.Caching.Fusion; + +namespace FusionCacheTests.Overloads; + +// THIS THING IS JUST A WAY TO TEST THAT EVERY NEEDED PERMUTATION OF CALLS+ARGS IS AVAILABLE, BOTH SYNC AND ASYNC +internal static partial class OverloadsCallsTryouts +{ + static readonly string Key = "foo"; + + static readonly Func<CancellationToken, Task<int?>> AsyncFactory = async _ => 42; + static readonly Func<CancellationToken, int?> SyncFactory = _ => 42; + + static readonly int? DefaultValue = 42; + + static readonly TimeSpan Duration = TimeSpan.FromMinutes(10); + static readonly Action<FusionCacheEntryOptions> OptionsLambda = options => options.SetDuration(Duration); + static readonly FusionCacheEntryOptions Options = new FusionCacheEntryOptions(Duration); +} diff --git a/tests/ZiggyCreatures.FusionCache.Tests/Overloads/OverloadsCallsTryouts_Async.cs b/tests/ZiggyCreatures.FusionCache.Tests/Overloads/OverloadsCallsTryouts_Async.cs new file mode 100644 index 00000000..4f9034b1 --- /dev/null +++ b/tests/ZiggyCreatures.FusionCache.Tests/Overloads/OverloadsCallsTryouts_Async.cs @@ -0,0 +1,125 @@ +using System.Threading.Tasks; +using ZiggyCreatures.Caching.Fusion; + +namespace FusionCacheTests.Overloads; + +internal static partial class OverloadsCallsTryouts +{ + private static async Task GetOrSetCallsAsync(IFusionCache cache) + { + // FACTORY / FAIL-SAFE DEFAULT VALUE + _ = await cache.GetOrSetAsync<int?>( + Key, + AsyncFactory, + DefaultValue, + Duration + ); + _ = await cache.GetOrSetAsync<int?>( + Key, + AsyncFactory, + DefaultValue, + OptionsLambda + ); + _ = await cache.GetOrSetAsync<int?>( + Key, + AsyncFactory, + DefaultValue, + Options + ); + _ = await cache.GetOrSetAsync<int?>( + Key, + AsyncFactory, + DefaultValue + ); + + // FACTORY / NO FAIL-SAFE DEFAULT VALUE + _ = await cache.GetOrSetAsync<int?>( + Key, + AsyncFactory, + Duration + ); + _ = await cache.GetOrSetAsync<int?>( + Key, + AsyncFactory, + OptionsLambda + ); + _ = await cache.GetOrSetAsync<int?>( + Key, + AsyncFactory, + Options + ); + _ = await cache.GetOrSetAsync<int?>( + Key, + AsyncFactory + ); + + // NO FACTORY / FAIL-SAFE DEFAULT VALUE + _ = await cache.GetOrSetAsync<int?>( + Key, + DefaultValue, + Duration + ); + _ = await cache.GetOrSetAsync<int?>( + Key, + DefaultValue, + OptionsLambda + ); + _ = await cache.GetOrSetAsync<int?>( + Key, + DefaultValue, + Options + ); + _ = await cache.GetOrSetAsync<int?>( + Key, + DefaultValue + ); + } + + private static async Task GetOrDefaultCallsAsync(IFusionCache cache) + { + _ = await cache.GetOrDefaultAsync<int?>( + Key, + DefaultValue, + OptionsLambda + ); + _ = await cache.GetOrDefaultAsync<int?>( + Key, + DefaultValue, + Options + ); + _ = await cache.GetOrDefaultAsync<int?>( + Key, + DefaultValue + ); + } + + private static async Task SetCallsAsync(IFusionCache cache) + { + await cache.SetAsync<int?>( + Key, + DefaultValue, + OptionsLambda + ); + await cache.SetAsync<int?>( + Key, + DefaultValue, + Options + ); + await cache.SetAsync<int?>( + Key, + DefaultValue + ); + } + + private static async Task RemoveCallsAsync(IFusionCache cache) + { + await cache.RemoveAsync( + Key, + OptionsLambda + ); + await cache.RemoveAsync( + Key, + Options + ); + } +} diff --git a/tests/ZiggyCreatures.FusionCache.Tests/Overloads/OverloadsCallsTryouts_Sync.cs b/tests/ZiggyCreatures.FusionCache.Tests/Overloads/OverloadsCallsTryouts_Sync.cs new file mode 100644 index 00000000..bd47cb58 --- /dev/null +++ b/tests/ZiggyCreatures.FusionCache.Tests/Overloads/OverloadsCallsTryouts_Sync.cs @@ -0,0 +1,124 @@ +using ZiggyCreatures.Caching.Fusion; + +namespace FusionCacheTests.Overloads; + +internal static partial class OverloadsCallsTryouts +{ + private static void GetOrSetCalls(IFusionCache cache) + { + // FACTORY / FAIL-SAFE DEFAULT VALUE + _ = cache.GetOrSet<int?>( + Key, + SyncFactory, + DefaultValue, + Duration + ); + _ = cache.GetOrSet<int?>( + Key, + SyncFactory, + DefaultValue, + OptionsLambda + ); + _ = cache.GetOrSet<int?>( + Key, + SyncFactory, + DefaultValue, + Options + ); + _ = cache.GetOrSet<int?>( + Key, + SyncFactory, + DefaultValue + ); + + // FACTORY / NO FAIL-SAFE DEFAULT VALUE + _ = cache.GetOrSet<int?>( + Key, + SyncFactory, + Duration + ); + _ = cache.GetOrSet<int?>( + Key, + SyncFactory, + OptionsLambda + ); + _ = cache.GetOrSet<int?>( + Key, + SyncFactory, + Options + ); + _ = cache.GetOrSet<int?>( + Key, + SyncFactory + ); + + // NO FACTORY / FAIL-SAFE DEFAULT VALUE + _ = cache.GetOrSet<int?>( + Key, + DefaultValue, + Duration + ); + _ = cache.GetOrSet<int?>( + Key, + DefaultValue, + OptionsLambda + ); + _ = cache.GetOrSet<int?>( + Key, + DefaultValue, + Options + ); + _ = cache.GetOrSet<int?>( + Key, + DefaultValue + ); + } + + private static void GetOrDefaultCalls(IFusionCache cache) + { + _ = cache.GetOrDefault<int?>( + Key, + DefaultValue, + OptionsLambda + ); + _ = cache.GetOrDefault<int?>( + Key, + DefaultValue, + Options + ); + _ = cache.GetOrDefault<int?>( + Key, + DefaultValue + ); + } + + private static void SetCalls(IFusionCache cache) + { + cache.Set<int?>( + Key, + DefaultValue, + OptionsLambda + ); + cache.Set<int?>( + Key, + DefaultValue, + Options + ); + cache.Set<int?>( + Key, + DefaultValue + ); + } + + private static void RemoveCalls(IFusionCache cache) + { + cache.Remove( + Key, + OptionsLambda + ); + cache.Remove( + Key, + Options + ); + } +} diff --git a/tests/ZiggyCreatures.FusionCache.Tests/OverloadsCallsTryouts.cs b/tests/ZiggyCreatures.FusionCache.Tests/OverloadsCallsTryouts.cs deleted file mode 100644 index 7e43a0db..00000000 --- a/tests/ZiggyCreatures.FusionCache.Tests/OverloadsCallsTryouts.cs +++ /dev/null @@ -1,22 +0,0 @@ -using System; -using System.Threading; -using System.Threading.Tasks; -using ZiggyCreatures.Caching.Fusion; - -namespace FusionCacheTests -{ - // THIS THING IS JUST A WAY TO TEST THAT EVERY NEEDED PERMUTATION OF CALLS+ARGS IS AVAILABLE, BOTH SYNC AND ASYNC - internal static partial class OverloadsCallsTryouts - { - static readonly string Key = "foo"; - - static readonly Func<CancellationToken, Task<int?>> AsyncFactory = async _ => 42; - static readonly Func<CancellationToken, int?> SyncFactory = _ => 42; - - static readonly int? DefaultValue = 42; - - static readonly TimeSpan Duration = TimeSpan.FromMinutes(10); - static readonly Action<FusionCacheEntryOptions> OptionsLambda = options => options.SetDuration(Duration); - static readonly FusionCacheEntryOptions Options = new FusionCacheEntryOptions(Duration); - } -} diff --git a/tests/ZiggyCreatures.FusionCache.Tests/OverloadsCallsTryouts_Async.cs b/tests/ZiggyCreatures.FusionCache.Tests/OverloadsCallsTryouts_Async.cs deleted file mode 100644 index 8059307a..00000000 --- a/tests/ZiggyCreatures.FusionCache.Tests/OverloadsCallsTryouts_Async.cs +++ /dev/null @@ -1,126 +0,0 @@ -using System.Threading.Tasks; -using ZiggyCreatures.Caching.Fusion; - -namespace FusionCacheTests -{ - internal static partial class OverloadsCallsTryouts - { - private static async Task GetOrSetCallsAsync(IFusionCache cache) - { - // FACTORY / FAIL-SAFE DEFAULT VALUE - _ = await cache.GetOrSetAsync<int?>( - Key, - AsyncFactory, - DefaultValue, - Duration - ); - _ = await cache.GetOrSetAsync<int?>( - Key, - AsyncFactory, - DefaultValue, - OptionsLambda - ); - _ = await cache.GetOrSetAsync<int?>( - Key, - AsyncFactory, - DefaultValue, - Options - ); - _ = await cache.GetOrSetAsync<int?>( - Key, - AsyncFactory, - DefaultValue - ); - - // FACTORY / NO FAIL-SAFE DEFAULT VALUE - _ = await cache.GetOrSetAsync<int?>( - Key, - AsyncFactory, - Duration - ); - _ = await cache.GetOrSetAsync<int?>( - Key, - AsyncFactory, - OptionsLambda - ); - _ = await cache.GetOrSetAsync<int?>( - Key, - AsyncFactory, - Options - ); - _ = await cache.GetOrSetAsync<int?>( - Key, - AsyncFactory - ); - - // NO FACTORY / FAIL-SAFE DEFAULT VALUE - _ = await cache.GetOrSetAsync<int?>( - Key, - DefaultValue, - Duration - ); - _ = await cache.GetOrSetAsync<int?>( - Key, - DefaultValue, - OptionsLambda - ); - _ = await cache.GetOrSetAsync<int?>( - Key, - DefaultValue, - Options - ); - _ = await cache.GetOrSetAsync<int?>( - Key, - DefaultValue - ); - } - - private static async Task GetOrDefaultCallsAsync(IFusionCache cache) - { - _ = await cache.GetOrDefaultAsync<int?>( - Key, - DefaultValue, - OptionsLambda - ); - _ = await cache.GetOrDefaultAsync<int?>( - Key, - DefaultValue, - Options - ); - _ = await cache.GetOrDefaultAsync<int?>( - Key, - DefaultValue - ); - } - - private static async Task SetCallsAsync(IFusionCache cache) - { - await cache.SetAsync<int?>( - Key, - DefaultValue, - OptionsLambda - ); - await cache.SetAsync<int?>( - Key, - DefaultValue, - Options - ); - await cache.SetAsync<int?>( - Key, - DefaultValue - ); - } - - private static async Task RemoveCallsAsync(IFusionCache cache) - { - await cache.RemoveAsync( - Key, - OptionsLambda - ); - await cache.RemoveAsync( - Key, - Options - ); - } - } -} diff --git a/tests/ZiggyCreatures.FusionCache.Tests/OverloadsCallsTryouts_Sync.cs b/tests/ZiggyCreatures.FusionCache.Tests/OverloadsCallsTryouts_Sync.cs deleted file mode 100644 index 8f70dc0b..00000000 --- a/tests/ZiggyCreatures.FusionCache.Tests/OverloadsCallsTryouts_Sync.cs +++ /dev/null @@ -1,125 +0,0 @@ -using ZiggyCreatures.Caching.Fusion; - -namespace FusionCacheTests -{ - internal static partial class OverloadsCallsTryouts - { - private static void GetOrSetCalls(IFusionCache cache) - { - // FACTORY / FAIL-SAFE DEFAULT VALUE - _ = cache.GetOrSet<int?>( - Key, - SyncFactory, - DefaultValue, - Duration - ); - _ = cache.GetOrSet<int?>( - Key, - SyncFactory, - DefaultValue, - OptionsLambda - ); - _ = cache.GetOrSet<int?>( - Key, - SyncFactory, - DefaultValue, - Options - ); - _ = cache.GetOrSet<int?>( - Key, - SyncFactory, - DefaultValue - ); - - // FACTORY / NO FAIL-SAFE DEFAULT VALUE - _ = cache.GetOrSet<int?>( - Key, - SyncFactory, - Duration - ); - _ = cache.GetOrSet<int?>( - Key, - SyncFactory, - OptionsLambda - ); - _ = cache.GetOrSet<int?>( - Key, - SyncFactory, - Options - ); - _ = cache.GetOrSet<int?>( - Key, - SyncFactory - ); - - // NO FACTORY / FAIL-SAFE DEFAULT VALUE - _ = cache.GetOrSet<int?>( - Key, - DefaultValue, - Duration - ); - _ = cache.GetOrSet<int?>( - Key, - DefaultValue, - OptionsLambda - ); - _ = cache.GetOrSet<int?>( - Key, - DefaultValue, - Options - ); - _ = cache.GetOrSet<int?>( - Key, - DefaultValue - ); - } - - private static void GetOrDefaultCalls(IFusionCache cache) - { - _ = cache.GetOrDefault<int?>( - Key, - DefaultValue, - OptionsLambda - ); - _ = cache.GetOrDefault<int?>( - Key, - DefaultValue, - Options - ); - _ = cache.GetOrDefault<int?>( - Key, - DefaultValue - ); - } - - private static void SetCalls(IFusionCache cache) - { - cache.Set<int?>( - Key, - DefaultValue, - OptionsLambda - ); - cache.Set<int?>( - Key, - DefaultValue, - Options - ); - cache.Set<int?>( - Key, - DefaultValue - ); - } - - private static void RemoveCalls(IFusionCache cache) - { - cache.Remove( - Key, - OptionsLambda - ); - cache.Remove( - Key, - Options - ); - } - } -} diff --git a/tests/ZiggyCreatures.FusionCache.Tests/PluginsTests.cs b/tests/ZiggyCreatures.FusionCache.Tests/PluginsTests.cs index 566bd3ea..6c0db749 100644 --- a/tests/ZiggyCreatures.FusionCache.Tests/PluginsTests.cs +++ b/tests/ZiggyCreatures.FusionCache.Tests/PluginsTests.cs @@ -6,178 +6,177 @@ using ZiggyCreatures.Caching.Fusion; using ZiggyCreatures.Caching.Fusion.Plugins; -namespace FusionCacheTests +namespace FusionCacheTests; + +public class PluginsTests { - public class PluginsTests + [Fact] + public async Task PluginBasicsWorkAsync() { - [Fact] - public async Task PluginBasicsWorkAsync() + using (var cache = new FusionCache(new FusionCacheOptions() { EnableSyncEventHandlersExecution = true })) { - using (var cache = new FusionCache(new FusionCacheOptions() { EnableSyncEventHandlersExecution = true })) - { - var plugin = new SimpleEventsPlugin(); + var plugin = new SimpleEventsPlugin(); - // ADD PLUGIN AND START IT - cache.AddPlugin(plugin); + // ADD PLUGIN AND START IT + cache.AddPlugin(plugin); - // MISS: +1 - await cache.TryGetAsync<int>("foo"); + // MISS: +1 + await cache.TryGetAsync<int>("foo"); - // MISS: +1 - await cache.GetOrDefaultAsync<int>("bar"); + // MISS: +1 + await cache.GetOrDefaultAsync<int>("bar"); - // STOP PLUGIN AND REMOVE IT - cache.RemovePlugin(plugin); + // STOP PLUGIN AND REMOVE IT + cache.RemovePlugin(plugin); - // MISS: NO CHANGE (BECAUSE IN THEORY THE EVENT HANDLERS SHOULD HAVE BEEN REMOVED) - await cache.TryGetAsync<int>("foo"); + // MISS: NO CHANGE (BECAUSE IN THEORY THE EVENT HANDLERS SHOULD HAVE BEEN REMOVED) + await cache.TryGetAsync<int>("foo"); - Assert.True(plugin.IsStarted, "Plugin has not started"); - Assert.True(plugin.IsStopped, "Plugin has not stopped"); - Assert.Equal(2, plugin.MissCount); - } + Assert.True(plugin.IsStarted, "Plugin has not started"); + Assert.True(plugin.IsStopped, "Plugin has not stopped"); + Assert.Equal(2, plugin.MissCount); } + } - [Fact] - public void PluginBasicsWork() + [Fact] + public void PluginBasicsWork() + { + using (var cache = new FusionCache(new FusionCacheOptions() { EnableSyncEventHandlersExecution = true })) { - using (var cache = new FusionCache(new FusionCacheOptions() { EnableSyncEventHandlersExecution = true })) - { - var plugin = new SimpleEventsPlugin(); + var plugin = new SimpleEventsPlugin(); - // ADD PLUGIN AND START IT - cache.AddPlugin(plugin); + // ADD PLUGIN AND START IT + cache.AddPlugin(plugin); - // MISS: +1 - cache.TryGet<int>("foo"); + // MISS: +1 + cache.TryGet<int>("foo"); - // MISS: +1 - cache.GetOrDefault<int>("bar"); + // MISS: +1 + cache.GetOrDefault<int>("bar"); - // STOP PLUGIN AND REMOVE IT - cache.RemovePlugin(plugin); + // STOP PLUGIN AND REMOVE IT + cache.RemovePlugin(plugin); - // MISS: NO CHANGE (BECAUSE IN THEORY THE EVENT HANDLERS SHOULD HAVE BEEN REMOVED) - cache.TryGet<int>("foo"); + // MISS: NO CHANGE (BECAUSE IN THEORY THE EVENT HANDLERS SHOULD HAVE BEEN REMOVED) + cache.TryGet<int>("foo"); - Assert.True(plugin.IsStarted, "Plugin has not started"); - Assert.True(plugin.IsStopped, "Plugin has not stopped"); - Assert.Equal(2, plugin.MissCount); - } + Assert.True(plugin.IsStarted, "Plugin has not started"); + Assert.True(plugin.IsStopped, "Plugin has not stopped"); + Assert.Equal(2, plugin.MissCount); } + } - [Fact] - public async Task ThrowingDuringStartDoesNotAddPluginAsync() + [Fact] + public async Task ThrowingDuringStartDoesNotAddPluginAsync() + { + using (var cache = new FusionCache(new FusionCacheOptions() { EnableSyncEventHandlersExecution = true })) { - using (var cache = new FusionCache(new FusionCacheOptions() { EnableSyncEventHandlersExecution = true })) - { - var plugin = new SimpleEventsPlugin(true); + var plugin = new SimpleEventsPlugin(true); - // ADD PLUGIN AND START IT (AND THROW) - Assert.Throws<InvalidOperationException>(() => - { - cache.AddPlugin(plugin); - }); + // ADD PLUGIN AND START IT (AND THROW) + Assert.Throws<InvalidOperationException>(() => + { + cache.AddPlugin(plugin); + }); - // MISS: NO CHANGE (BECAUSE IN THEORY THE PLUGIN HASN'T BEEN ADDED, BECAUSE EXCEPTION DURING Start()) - await cache.TryGetAsync<int>("foo"); + // MISS: NO CHANGE (BECAUSE IN THEORY THE PLUGIN HASN'T BEEN ADDED, BECAUSE EXCEPTION DURING Start()) + await cache.TryGetAsync<int>("foo"); - // STOP PLUGIN AND REMOVE IT - var isRemoved = cache.RemovePlugin(plugin); + // STOP PLUGIN AND REMOVE IT + var isRemoved = cache.RemovePlugin(plugin); - // MISS: NO CHANGE (BECAUSE IN THEORY THE EVENT HANDLERS SHOULD HAVE BEEN REMOVED) - await cache.TryGetAsync<int>("foo"); + // MISS: NO CHANGE (BECAUSE IN THEORY THE EVENT HANDLERS SHOULD HAVE BEEN REMOVED) + await cache.TryGetAsync<int>("foo"); - Assert.True(plugin.IsStarted, "Plugin has not been started"); - Assert.False(plugin.IsStopped, "Plugin has been stopped"); - Assert.False(isRemoved, "Plugin has been removed"); - Assert.Equal(0, plugin.MissCount); - } + Assert.True(plugin.IsStarted, "Plugin has not been started"); + Assert.False(plugin.IsStopped, "Plugin has been stopped"); + Assert.False(isRemoved, "Plugin has been removed"); + Assert.Equal(0, plugin.MissCount); } + } - [Fact] - public void ThrowingDuringStartDoesNotAddPlugin() + [Fact] + public void ThrowingDuringStartDoesNotAddPlugin() + { + using (var cache = new FusionCache(new FusionCacheOptions() { EnableSyncEventHandlersExecution = true })) { - using (var cache = new FusionCache(new FusionCacheOptions() { EnableSyncEventHandlersExecution = true })) - { - var plugin = new SimpleEventsPlugin(true); + var plugin = new SimpleEventsPlugin(true); - // ADD PLUGIN AND START IT (AND THROW) - Assert.Throws<InvalidOperationException>(() => - { - cache.AddPlugin(plugin); - }); + // ADD PLUGIN AND START IT (AND THROW) + Assert.Throws<InvalidOperationException>(() => + { + cache.AddPlugin(plugin); + }); - // MISS: NO CHANGE (BECAUSE IN THEORY THE PLUGIN HASN'T BEEN ADDED, BECAUSE EXCEPTION DURING Start()) - cache.TryGet<int>("foo"); + // MISS: NO CHANGE (BECAUSE IN THEORY THE PLUGIN HASN'T BEEN ADDED, BECAUSE EXCEPTION DURING Start()) + cache.TryGet<int>("foo"); - // STOP PLUGIN AND REMOVE IT - var isRemoved = cache.RemovePlugin(plugin); + // STOP PLUGIN AND REMOVE IT + var isRemoved = cache.RemovePlugin(plugin); - // MISS: NO CHANGE (BECAUSE IN THEORY THE EVENT HANDLERS SHOULD HAVE BEEN REMOVED) - cache.TryGet<int>("foo"); + // MISS: NO CHANGE (BECAUSE IN THEORY THE EVENT HANDLERS SHOULD HAVE BEEN REMOVED) + cache.TryGet<int>("foo"); - Assert.True(plugin.IsStarted, "Plugin has not been started"); - Assert.False(plugin.IsStopped, "Plugin has been stopped"); - Assert.False(isRemoved, "Plugin has been removed"); - Assert.Equal(0, plugin.MissCount); - } + Assert.True(plugin.IsStarted, "Plugin has not been started"); + Assert.False(plugin.IsStopped, "Plugin has been stopped"); + Assert.False(isRemoved, "Plugin has been removed"); + Assert.Equal(0, plugin.MissCount); } + } - [Fact] - public async Task DependencyInjectionAutoDiscoveryWorksAsync() - { - var services = new ServiceCollection(); + [Fact] + public async Task DependencyInjectionAutoDiscoveryWorksAsync() + { + var services = new ServiceCollection(); - services.AddSingleton<IFusionCachePlugin, SimpleEventsPlugin>(); - services.AddFusionCache() - .TryWithAutoSetup() - .WithOptions(options => - { - options.EnableSyncEventHandlersExecution = true; - }) - ; + services.AddSingleton<IFusionCachePlugin, SimpleEventsPlugin>(); + services.AddFusionCache() + .TryWithAutoSetup() + .WithOptions(options => + { + options.EnableSyncEventHandlersExecution = true; + }) + ; - using var serviceProvider = services.BuildServiceProvider(); + using var serviceProvider = services.BuildServiceProvider(); - var cache = serviceProvider.GetRequiredService<IFusionCache>(); + var cache = serviceProvider.GetRequiredService<IFusionCache>(); - // GET THE PLUGIN (SHOULD RETURN THE SAME INSTANCE, BECAUSE SINGLETON) - var plugin = serviceProvider.GetRequiredService<IFusionCachePlugin>() as SimpleEventsPlugin; + // GET THE PLUGIN (SHOULD RETURN THE SAME INSTANCE, BECAUSE SINGLETON) + var plugin = serviceProvider.GetRequiredService<IFusionCachePlugin>() as SimpleEventsPlugin; - // MISS: +1 - await cache.TryGetAsync<int>("foo"); + // MISS: +1 + await cache.TryGetAsync<int>("foo"); - Assert.True(plugin!.IsStarted, "Plugin has not been started"); - Assert.Equal(1, plugin.MissCount); - } + Assert.True(plugin!.IsStarted, "Plugin has not been started"); + Assert.Equal(1, plugin.MissCount); + } - [Fact] - public void DependencyInjectionAutoDiscoveryWorks() - { - var services = new ServiceCollection(); + [Fact] + public void DependencyInjectionAutoDiscoveryWorks() + { + var services = new ServiceCollection(); - services.AddSingleton<IFusionCachePlugin, SimpleEventsPlugin>(); - services.AddFusionCache() - .TryWithAutoSetup() - .WithOptions(options => - { - options.EnableSyncEventHandlersExecution = true; - }) - ; + services.AddSingleton<IFusionCachePlugin, SimpleEventsPlugin>(); + services.AddFusionCache() + .TryWithAutoSetup() + .WithOptions(options => + { + options.EnableSyncEventHandlersExecution = true; + }) + ; - using var serviceProvider = services.BuildServiceProvider(); + using var serviceProvider = services.BuildServiceProvider(); - var cache = serviceProvider.GetRequiredService<IFusionCache>(); + var cache = serviceProvider.GetRequiredService<IFusionCache>(); - // GET THE PLUGIN (SHOULD RETURN THE SAME INSTANCE, BECAUSE SINGLETON) - var plugin = serviceProvider.GetRequiredService<IFusionCachePlugin>() as SimpleEventsPlugin; + // GET THE PLUGIN (SHOULD RETURN THE SAME INSTANCE, BECAUSE SINGLETON) + var plugin = serviceProvider.GetRequiredService<IFusionCachePlugin>() as SimpleEventsPlugin; - // MISS: +1 - cache.TryGet<int>("foo"); + // MISS: +1 + cache.TryGet<int>("foo"); - Assert.True(plugin!.IsStarted, "Plugin has not been started"); - Assert.Equal(1, plugin.MissCount); - } + Assert.True(plugin!.IsStarted, "Plugin has not been started"); + Assert.Equal(1, plugin.MissCount); } } diff --git a/tests/ZiggyCreatures.FusionCache.Tests/RunUtilsTests.cs b/tests/ZiggyCreatures.FusionCache.Tests/RunUtilsTests.cs new file mode 100644 index 00000000..dc52fc18 --- /dev/null +++ b/tests/ZiggyCreatures.FusionCache.Tests/RunUtilsTests.cs @@ -0,0 +1,208 @@ +using System; +using System.Diagnostics; +using System.Threading; +using System.Threading.Tasks; +using Xunit; +using ZiggyCreatures.Caching.Fusion; +using ZiggyCreatures.Caching.Fusion.Internals; + +namespace FusionCacheTests; + +public class RunUtilsTests +{ + [Fact] + public async Task ZeroTimeoutDoesNotStartAsyncFuncAsync() + { + bool _hasRun = false; + + await Assert.ThrowsAsync<SyntheticTimeoutException>(async () => + { + await RunUtils.RunAsyncFuncWithTimeoutAsync(async ct => { _hasRun = true; return 42; }, TimeSpan.Zero, false, t => { }); + }); + Assert.False(_hasRun); + } + + [Fact] + public void ZeroTimeoutDoesNotStartAsyncFunc() + { + bool _hasRun = false; + + Assert.Throws<SyntheticTimeoutException>(() => + { + RunUtils.RunAsyncFuncWithTimeout(async ct => { _hasRun = true; return 42; }, TimeSpan.Zero, false, t => { }); + }); + Assert.False(_hasRun); + } + + [Fact] + public async Task ZeroTimeoutDoesNotStartAsyncActionAsync() + { + bool _hasRun = false; + + await Assert.ThrowsAsync<SyntheticTimeoutException>(async () => + { + await RunUtils.RunAsyncActionWithTimeoutAsync(async ct => { _hasRun = true; }, TimeSpan.Zero, false, t => { }); + }); + Assert.False(_hasRun); + } + + [Fact] + public void ZeroTimeoutDoesNotStartAsyncAction() + { + bool _hasRun = false; + + Assert.Throws<SyntheticTimeoutException>(() => + { + RunUtils.RunAsyncActionWithTimeout(async ct => { _hasRun = true; }, TimeSpan.Zero, false, t => { }); + }); + Assert.False(_hasRun); + } + + [Fact] + public async Task CancelingAsyncFuncActuallyCancelsItAsync() + { + int res = -1; + var factoryTerminated = false; + var outerCancelDelayMs = 500; + var innerDelayMs = 2_000; + await Assert.ThrowsAnyAsync<OperationCanceledException>(async () => + { + var cts = new CancellationTokenSource(outerCancelDelayMs); + res = await RunUtils.RunAsyncFuncWithTimeoutAsync(async ct => { await Task.Delay(innerDelayMs); ct.ThrowIfCancellationRequested(); factoryTerminated = true; return 42; }, Timeout.InfiniteTimeSpan, true, token: cts.Token); + }); + await Task.Delay(innerDelayMs); + + Assert.Equal(-1, res); + Assert.False(factoryTerminated); + } + + [Fact] + public void CancelingAsyncFuncActuallyCancelsIt() + { + int res = -1; + var factoryTerminated = false; + var outerCancelDelayMs = 500; + var innerDelayMs = 2_000; + Assert.ThrowsAny<OperationCanceledException>(() => + { + var cts = new CancellationTokenSource(outerCancelDelayMs); + res = RunUtils.RunAsyncFuncWithTimeout(async ct => { await Task.Delay(innerDelayMs); ct.ThrowIfCancellationRequested(); factoryTerminated = true; return 42; }, Timeout.InfiniteTimeSpan, true, token: cts.Token); + }); + + Assert.Equal(-1, res); + Assert.False(factoryTerminated); + } + + [Fact] + public void CancelingSyncFuncActuallyCancelsIt() + { + int res = -1; + var factoryTerminated = false; + var outerCancelDelayMs = 500; + var innerDelayMs = 2_000; + Assert.Throws<OperationCanceledException>(() => + { + var cts = new CancellationTokenSource(outerCancelDelayMs); + res = RunUtils.RunSyncFuncWithTimeout(ct => { Thread.Sleep(innerDelayMs); ct.ThrowIfCancellationRequested(); factoryTerminated = true; return 42; }, Timeout.InfiniteTimeSpan, true, token: cts.Token); + }); + + Assert.Equal(-1, res); + Assert.False(factoryTerminated); + } + + [Fact] + public async Task TimeoutEffectivelyWorksAsync() + { + int res = -1; + var timeoutMs = 500; + var innerDelayMs = 2_000; + var sw = Stopwatch.StartNew(); + await Assert.ThrowsAnyAsync<TimeoutException>(async () => + { + res = await RunUtils.RunAsyncFuncWithTimeoutAsync(async ct => { await Task.Delay(innerDelayMs); ct.ThrowIfCancellationRequested(); return 42; }, TimeSpan.FromMilliseconds(timeoutMs)); + }); + sw.Stop(); + + Assert.Equal(-1, res); + Assert.True(sw.ElapsedMilliseconds >= timeoutMs); + Assert.True(sw.ElapsedMilliseconds < innerDelayMs); + } + + [Fact] + public void TimeoutEffectivelyWorks() + { + int res = -1; + var timeoutMs = 500; + var innerDelayMs = 2_000; + var sw = Stopwatch.StartNew(); + Assert.ThrowsAny<TimeoutException>(() => + { + res = RunUtils.RunAsyncFuncWithTimeout(async ct => { await Task.Delay(innerDelayMs); ct.ThrowIfCancellationRequested(); return 42; }, TimeSpan.FromMilliseconds(timeoutMs)); + }); + sw.Stop(); + + Assert.Equal(-1, res); + Assert.True(sw.ElapsedMilliseconds >= timeoutMs); + Assert.True(sw.ElapsedMilliseconds < innerDelayMs); + } + + [Fact] + public async Task CancelWhenTimeoutActuallyWorksAsync() + { + var factoryCompleted = false; + var timeoutMs = 500; + var innerDelayMs = 2_000; + await Assert.ThrowsAnyAsync<TimeoutException>(async () => + { + await RunUtils.RunAsyncActionWithTimeoutAsync(async ct => { await Task.Delay(innerDelayMs); ct.ThrowIfCancellationRequested(); factoryCompleted = true; }, TimeSpan.FromMilliseconds(timeoutMs), true); + }); + await Task.Delay(innerDelayMs); + + Assert.False(factoryCompleted); + } + + [Fact] + public void CancelWhenTimeoutActuallyWorks() + { + var factoryCompleted = false; + var timeoutMs = 500; + var innerDelayMs = 2_000; + Assert.ThrowsAny<TimeoutException>(() => + { + RunUtils.RunAsyncActionWithTimeout(async ct => { await Task.Delay(innerDelayMs); ct.ThrowIfCancellationRequested(); factoryCompleted = true; }, TimeSpan.FromMilliseconds(timeoutMs), true); + }); + Thread.Sleep(innerDelayMs); + + Assert.False(factoryCompleted); + } + + [Fact] + public async Task DoNotCancelWhenTimeoutActuallyWorksAsync() + { + var factoryCompleted = false; + var timeoutMs = 100; + var innerDelayMs = 2_000; + await Assert.ThrowsAnyAsync<TimeoutException>(async () => + { + await RunUtils.RunAsyncActionWithTimeoutAsync(async ct => { await Task.Delay(innerDelayMs); ct.ThrowIfCancellationRequested(); factoryCompleted = true; }, TimeSpan.FromMilliseconds(timeoutMs), false); + }); + await Task.Delay(innerDelayMs + timeoutMs); + + Assert.True(factoryCompleted); + } + + [Fact] + public void DoNotCancelWhenTimeoutActuallyWorks() + { + var factoryCompleted = false; + var timeoutMs = 100; + var innerDelayMs = 2_000; + Assert.ThrowsAny<TimeoutException>(() => + { + RunUtils.RunAsyncActionWithTimeout(async ct => { await Task.Delay(innerDelayMs); ct.ThrowIfCancellationRequested(); factoryCompleted = true; }, TimeSpan.FromMilliseconds(timeoutMs), false); + }); + Thread.Sleep(innerDelayMs + timeoutMs); + + Assert.True(factoryCompleted); + } +} diff --git a/tests/ZiggyCreatures.FusionCache.Tests/Samples/fusioncachecysharpmemorypackserializer__v0_23_0_0.bin b/tests/ZiggyCreatures.FusionCache.Tests/Samples/fusioncachecysharpmemorypackserializer__v0_23_0_0.bin new file mode 100644 index 00000000..c4a8e7f7 Binary files /dev/null and b/tests/ZiggyCreatures.FusionCache.Tests/Samples/fusioncachecysharpmemorypackserializer__v0_23_0_0.bin differ diff --git a/tests/ZiggyCreatures.FusionCache.Tests/Samples/fusioncacheneueccmessagepackserializer__v0_23_0_0.bin b/tests/ZiggyCreatures.FusionCache.Tests/Samples/fusioncacheneueccmessagepackserializer__v0_23_0_0.bin new file mode 100644 index 00000000..5f0bee5b Binary files /dev/null and b/tests/ZiggyCreatures.FusionCache.Tests/Samples/fusioncacheneueccmessagepackserializer__v0_23_0_0.bin differ diff --git a/tests/ZiggyCreatures.FusionCache.Tests/Samples/fusioncachenewtonsoftjsonserializer__v0_23_0_0.bin b/tests/ZiggyCreatures.FusionCache.Tests/Samples/fusioncachenewtonsoftjsonserializer__v0_23_0_0.bin new file mode 100644 index 00000000..5069d84f --- /dev/null +++ b/tests/ZiggyCreatures.FusionCache.Tests/Samples/fusioncachenewtonsoftjsonserializer__v0_23_0_0.bin @@ -0,0 +1 @@ +{"v":"Sloths are cool!","m":{"e":"2033-07-08T14:53:13.4520833+00:00","f":true,"ea":"2033-06-28T14:53:13.4520833+00:00","et":"MyETagValue","lm":"2033-03-30T14:53:13.4520833+00:00"},"t":638310588939961396} \ No newline at end of file diff --git a/tests/ZiggyCreatures.FusionCache.Tests/Samples/fusioncacheprotobufnetserializer__v0_23_0_0.bin b/tests/ZiggyCreatures.FusionCache.Tests/Samples/fusioncacheprotobufnetserializer__v0_23_0_0.bin new file mode 100644 index 00000000..621206cd --- /dev/null +++ b/tests/ZiggyCreatures.FusionCache.Tests/Samples/fusioncacheprotobufnetserializer__v0_23_0_0.bin @@ -0,0 +1,2 @@ + +Sloths are cool!-��Ψ�ɭ���������"MyETagValue(����Ϋ��������� \ No newline at end of file diff --git a/tests/ZiggyCreatures.FusionCache.Tests/Samples/fusioncacheservicestackjsonserializer__v0_23_0_0.bin b/tests/ZiggyCreatures.FusionCache.Tests/Samples/fusioncacheservicestackjsonserializer__v0_23_0_0.bin new file mode 100644 index 00000000..5069d84f --- /dev/null +++ b/tests/ZiggyCreatures.FusionCache.Tests/Samples/fusioncacheservicestackjsonserializer__v0_23_0_0.bin @@ -0,0 +1 @@ +{"v":"Sloths are cool!","m":{"e":"2033-07-08T14:53:13.4520833+00:00","f":true,"ea":"2033-06-28T14:53:13.4520833+00:00","et":"MyETagValue","lm":"2033-03-30T14:53:13.4520833+00:00"},"t":638310588939961396} \ No newline at end of file diff --git a/tests/ZiggyCreatures.FusionCache.Tests/Samples/fusioncachesystemtextjsonserializer__v0_23_0_0.bin b/tests/ZiggyCreatures.FusionCache.Tests/Samples/fusioncachesystemtextjsonserializer__v0_23_0_0.bin new file mode 100644 index 00000000..2f571048 --- /dev/null +++ b/tests/ZiggyCreatures.FusionCache.Tests/Samples/fusioncachesystemtextjsonserializer__v0_23_0_0.bin @@ -0,0 +1 @@ +{"Value":"Sloths are cool!","Metadata":{"LogicalExpiration":"2033-07-08T14:53:13.4520833+00:00","IsFromFailSafe":true,"EagerExpiration":"2033-06-28T14:53:13.4520833+00:00","ETag":"MyETagValue","LastModified":"2033-03-30T14:53:13.4520833+00:00"},"Timestamp":638310588939961396} \ No newline at end of file diff --git a/tests/ZiggyCreatures.FusionCache.Tests/SerializationTests.cs b/tests/ZiggyCreatures.FusionCache.Tests/SerializationTests.cs index 475b058c..ac694dba 100644 --- a/tests/ZiggyCreatures.FusionCache.Tests/SerializationTests.cs +++ b/tests/ZiggyCreatures.FusionCache.Tests/SerializationTests.cs @@ -2,299 +2,278 @@ using System.IO; using System.Text.RegularExpressions; using System.Threading.Tasks; +using FusionCacheTests.Stuff; using Xunit; using Xunit.Abstractions; using ZiggyCreatures.Caching.Fusion.Internals; using ZiggyCreatures.Caching.Fusion.Internals.Distributed; using ZiggyCreatures.Caching.Fusion.Serialization; -namespace FusionCacheTests +namespace FusionCacheTests; + +public class SerializationTests + : AbstractTests { - public class SerializationTests + public SerializationTests(ITestOutputHelper output) + : base(output, null) { - private readonly ITestOutputHelper _output; + } - public SerializationTests(ITestOutputHelper output) - { - _output = output; - } + private static readonly Regex __re_VersionExtractor = new Regex(@"\w+__v(\d+_\d+_\d+)_\d+\.bin", RegexOptions.Compiled); - private static readonly Regex __re_VersionExtractor = new Regex(@"\w+__v(\d+_\d+_\d+)_\d+\.bin", RegexOptions.Compiled); + private const string SampleString = "Supercalifragilisticexpialidocious"; - private const string SampleString = "Supercalifragilisticexpialidocious"; + private static T? LoopDeLoop<T>(IFusionCacheSerializer serializer, T? obj) + { + var data = serializer.Serialize(obj); + return serializer.Deserialize<T>(data); + } - private static T? LoopDeLoop<T>(IFusionCacheSerializer serializer, T? obj) - { - var data = serializer.Serialize(obj); - return serializer.Deserialize<T>(data); - } + private static async Task<T?> LoopDeLoopAsync<T>(IFusionCacheSerializer serializer, T? obj) + { + var data = await serializer.SerializeAsync(obj); + return await serializer.DeserializeAsync<T>(data); + } - private static async Task<T?> LoopDeLoopAsync<T>(IFusionCacheSerializer serializer, T? obj) - { - var data = await serializer.SerializeAsync(obj); - return await serializer.DeserializeAsync<T>(data); - } + [Theory] + [ClassData(typeof(SerializerTypesClassData))] + public async Task LoopSucceedsWithSimpleTypesAsync(SerializerType serializerType) + { + var serializer = TestsUtils.GetSerializer(serializerType); + var looped = await LoopDeLoopAsync(serializer, SampleString); + Assert.Equal(SampleString, looped); + } - [Theory] - [ClassData(typeof(SerializerTypesClassData))] - public async Task LoopSucceedsWithSimpleTypesAsync(SerializerType serializerType) - { - var serializer = TestsUtils.GetSerializer(serializerType); - var looped = await LoopDeLoopAsync(serializer, SampleString); - Assert.Equal(SampleString, looped); - } + [Theory] + [ClassData(typeof(SerializerTypesClassData))] + public void LoopSucceedsWithSimpleTypes(SerializerType serializerType) + { + var serializer = TestsUtils.GetSerializer(serializerType); + var looped = LoopDeLoop(serializer, SampleString); + Assert.Equal(SampleString, looped); + } - [Theory] - [ClassData(typeof(SerializerTypesClassData))] - public void LoopSucceedsWithSimpleTypes(SerializerType serializerType) - { - var serializer = TestsUtils.GetSerializer(serializerType); - var looped = LoopDeLoop(serializer, SampleString); - Assert.Equal(SampleString, looped); - } + [Theory] + [ClassData(typeof(SerializerTypesClassData))] + public async Task LoopSucceedsWithComplexTypesAsync(SerializerType serializerType) + { + var data = ComplexType.CreateSample(); + var serializer = TestsUtils.GetSerializer(serializerType); + var looped = await LoopDeLoopAsync(serializer, data); + Assert.Equal(data, looped); + } - [Theory] - [ClassData(typeof(SerializerTypesClassData))] - public async Task LoopSucceedsWithComplexTypesAsync(SerializerType serializerType) - { - var data = ComplexType.CreateSample(); - var serializer = TestsUtils.GetSerializer(serializerType); - var looped = await LoopDeLoopAsync(serializer, data); - Assert.Equal(data, looped); - } + [Theory] + [ClassData(typeof(SerializerTypesClassData))] + public void LoopSucceedsWithComplexTypes(SerializerType serializerType) + { + var data = ComplexType.CreateSample(); + var serializer = TestsUtils.GetSerializer(serializerType); + var looped = LoopDeLoop(serializer, data); + Assert.Equal(data, looped); + } - [Theory] - [ClassData(typeof(SerializerTypesClassData))] - public void LoopSucceedsWithComplexTypes(SerializerType serializerType) - { - var data = ComplexType.CreateSample(); - var serializer = TestsUtils.GetSerializer(serializerType); - var looped = LoopDeLoop(serializer, data); - Assert.Equal(data, looped); - } + [Theory] + [ClassData(typeof(SerializerTypesClassData))] + public async Task LoopDoesNotFailWithNullAsync(SerializerType serializerType) + { + var serializer = TestsUtils.GetSerializer(serializerType); + var looped = await LoopDeLoopAsync<string>(serializer, null); + Assert.Null(looped); + } - //[Theory] - //[ClassData(typeof(SerializerTypesClassData))] - //public async Task LoopFailsWithIncompatibleTypesAsync(SerializerType serializerType) - //{ - // var serializer = TestsUtils.GetSerializer(serializerType); - // await Assert.ThrowsAnyAsync<Exception>(async () => - // { - // var data = await serializer.SerializeAsync("sloths, sloths everywhere"); - // var res = await serializer.DeserializeAsync<int>(data); - // }); - //} - - //[Theory] - //[ClassData(typeof(SerializerTypesClassData))] - //public void LoopFailsWithIncompatibleTypes(SerializerType serializerType) - //{ - // var serializer = TestsUtils.GetSerializer(serializerType); - // Assert.ThrowsAny<Exception>(() => - // { - // var data = serializer.Serialize("sloths, sloths everywhere"); - // var res = serializer.Deserialize<int>(data); - // }); - //} - - [Theory] - [ClassData(typeof(SerializerTypesClassData))] - public async Task LoopDoesNotFailWithNullAsync(SerializerType serializerType) - { - var serializer = TestsUtils.GetSerializer(serializerType); - var looped = await LoopDeLoopAsync<string>(serializer, null); - Assert.Null(looped); - } + [Theory] + [ClassData(typeof(SerializerTypesClassData))] + public void LoopDoesNotFailWithNull(SerializerType serializerType) + { + var serializer = TestsUtils.GetSerializer(serializerType); + var looped = LoopDeLoop<string>(serializer, null); + Assert.Null(looped); + } - [Theory] - [ClassData(typeof(SerializerTypesClassData))] - public void LoopDoesNotFailWithNull(SerializerType serializerType) - { - var serializer = TestsUtils.GetSerializer(serializerType); - var looped = LoopDeLoop<string>(serializer, null); - Assert.Null(looped); - } + [Theory] + [ClassData(typeof(SerializerTypesClassData))] + public async Task LoopSucceedsWithDistributedEntryAndSimpleTypesAsync(SerializerType serializerType) + { + var serializer = TestsUtils.GetSerializer(serializerType); + var obj = new FusionCacheDistributedEntry<string>(SampleString, new FusionCacheEntryMetadata(DateTimeOffset.UtcNow.AddSeconds(10), true, DateTimeOffset.UtcNow.AddSeconds(9), "abc123", DateTimeOffset.UtcNow), FusionCacheInternalUtils.GetCurrentTimestamp()); + + var data = await serializer.SerializeAsync(obj); + + Assert.NotNull(data); + Assert.NotEmpty(data); + + var looped = await serializer.DeserializeAsync<FusionCacheDistributedEntry<string>>(data); + Assert.NotNull(looped); + Assert.Equal(obj.Value, looped.Value); + Assert.Equal(obj.Timestamp, looped.Timestamp); + Assert.Equal(obj.Metadata!.IsFromFailSafe, looped.Metadata!.IsFromFailSafe); + Assert.Equal(obj.Metadata!.LogicalExpiration, looped.Metadata!.LogicalExpiration); + Assert.Equal(obj.Metadata!.EagerExpiration, looped.Metadata!.EagerExpiration); + Assert.Equal(obj.Metadata!.ETag, looped.Metadata!.ETag); + Assert.Equal(obj.Metadata!.LastModified, looped.Metadata!.LastModified); + } - [Theory] - [ClassData(typeof(SerializerTypesClassData))] - public async Task LoopSucceedsWithDistributedEntryAndSimpleTypesAsync(SerializerType serializerType) - { - var serializer = TestsUtils.GetSerializer(serializerType); - var obj = new FusionCacheDistributedEntry<string>(SampleString, new FusionCacheEntryMetadata(DateTimeOffset.UtcNow.AddSeconds(10), true, DateTimeOffset.UtcNow.AddSeconds(9), "abc123", DateTimeOffset.UtcNow)); - - var data = await serializer.SerializeAsync(obj); - - Assert.NotNull(data); - Assert.NotEmpty(data); - - var looped = await serializer.DeserializeAsync<FusionCacheDistributedEntry<string>>(data); - Assert.NotNull(looped); - Assert.Equal(obj.Value, looped.Value); - Assert.Equal(obj.Timestamp, looped.Timestamp); - Assert.Equal(obj.Metadata!.IsFromFailSafe, looped.Metadata!.IsFromFailSafe); - Assert.Equal(obj.Metadata!.LogicalExpiration, looped.Metadata!.LogicalExpiration); - Assert.Equal(obj.Metadata!.EagerExpiration, looped.Metadata!.EagerExpiration); - Assert.Equal(obj.Metadata!.ETag, looped.Metadata!.ETag); - Assert.Equal(obj.Metadata!.LastModified, looped.Metadata!.LastModified); - } + [Theory] + [ClassData(typeof(SerializerTypesClassData))] + public void LoopSucceedsWithDistributedEntryAndSimpleTypes(SerializerType serializerType) + { + var serializer = TestsUtils.GetSerializer(serializerType); + var obj = new FusionCacheDistributedEntry<string>(SampleString, new FusionCacheEntryMetadata(DateTimeOffset.UtcNow.AddSeconds(10), true, DateTimeOffset.UtcNow.AddSeconds(9), "abc123", DateTimeOffset.UtcNow), FusionCacheInternalUtils.GetCurrentTimestamp()); + + var data = serializer.Serialize(obj); + + Assert.NotNull(data); + Assert.NotEmpty(data); + + var looped = serializer.Deserialize<FusionCacheDistributedEntry<string>>(data); + Assert.NotNull(looped); + Assert.Equal(obj.Value, looped.Value); + Assert.Equal(obj.Timestamp, looped.Timestamp); + Assert.Equal(obj.Metadata!.IsFromFailSafe, looped.Metadata!.IsFromFailSafe); + Assert.Equal(obj.Metadata!.LogicalExpiration, looped.Metadata!.LogicalExpiration); + Assert.Equal(obj.Metadata!.EagerExpiration, looped.Metadata!.EagerExpiration); + Assert.Equal(obj.Metadata!.ETag, looped.Metadata!.ETag); + Assert.Equal(obj.Metadata!.LastModified, looped.Metadata!.LastModified); + } - [Theory] - [ClassData(typeof(SerializerTypesClassData))] - public void LoopSucceedsWithDistributedEntryAndSimpleTypes(SerializerType serializerType) - { - var serializer = TestsUtils.GetSerializer(serializerType); - var obj = new FusionCacheDistributedEntry<string>(SampleString, new FusionCacheEntryMetadata(DateTimeOffset.UtcNow.AddSeconds(10), true, DateTimeOffset.UtcNow.AddSeconds(9), "abc123", DateTimeOffset.UtcNow)); - - var data = serializer.Serialize(obj); - - Assert.NotNull(data); - Assert.NotEmpty(data); - - var looped = serializer.Deserialize<FusionCacheDistributedEntry<string>>(data); - Assert.NotNull(looped); - Assert.Equal(obj.Value, looped.Value); - Assert.Equal(obj.Timestamp, looped.Timestamp); - Assert.Equal(obj.Metadata!.IsFromFailSafe, looped.Metadata!.IsFromFailSafe); - Assert.Equal(obj.Metadata!.LogicalExpiration, looped.Metadata!.LogicalExpiration); - Assert.Equal(obj.Metadata!.EagerExpiration, looped.Metadata!.EagerExpiration); - Assert.Equal(obj.Metadata!.ETag, looped.Metadata!.ETag); - Assert.Equal(obj.Metadata!.LastModified, looped.Metadata!.LastModified); - } + [Theory] + [ClassData(typeof(SerializerTypesClassData))] + public async Task LoopSucceedsWithDistributedEntryAndNoMetadataAsync(SerializerType serializerType) + { + var serializer = TestsUtils.GetSerializer(serializerType); + var obj = new FusionCacheDistributedEntry<string>(SampleString, null, FusionCacheInternalUtils.GetCurrentTimestamp()); - [Theory] - [ClassData(typeof(SerializerTypesClassData))] - public async Task LoopSucceedsWithDistributedEntryAndNoMetadataAsync(SerializerType serializerType) - { - var serializer = TestsUtils.GetSerializer(serializerType); - var obj = new FusionCacheDistributedEntry<string>(SampleString, null); + var data = await serializer.SerializeAsync(obj); - var data = await serializer.SerializeAsync(obj); + Assert.NotNull(data); + Assert.NotEmpty(data); - Assert.NotNull(data); - Assert.NotEmpty(data); + var looped = await serializer.DeserializeAsync<FusionCacheDistributedEntry<string>>(data); + Assert.NotNull(looped); + Assert.Equal(obj.Value, looped.Value); + Assert.Equal(obj.Timestamp, looped.Timestamp); + Assert.Null(looped!.Metadata); + } - var looped = await serializer.DeserializeAsync<FusionCacheDistributedEntry<string>>(data); - Assert.NotNull(looped); - Assert.Equal(obj.Value, looped.Value); - Assert.Equal(obj.Timestamp, looped.Timestamp); - Assert.Null(looped!.Metadata); - } + [Theory] + [ClassData(typeof(SerializerTypesClassData))] + public void LoopSucceedsWithDistributedEntryAndNoMetadata(SerializerType serializerType) + { + var serializer = TestsUtils.GetSerializer(serializerType); + var obj = new FusionCacheDistributedEntry<string>(SampleString, null, FusionCacheInternalUtils.GetCurrentTimestamp()); - [Theory] - [ClassData(typeof(SerializerTypesClassData))] - public void LoopSucceedsWithDistributedEntryAndNoMetadata(SerializerType serializerType) - { - var serializer = TestsUtils.GetSerializer(serializerType); - var obj = new FusionCacheDistributedEntry<string>(SampleString, null); + var data = serializer.Serialize(obj); - var data = serializer.Serialize(obj); + Assert.NotNull(data); + Assert.NotEmpty(data); - Assert.NotNull(data); - Assert.NotEmpty(data); + var looped = serializer.Deserialize<FusionCacheDistributedEntry<string>>(data); + Assert.NotNull(looped); + Assert.Equal(obj.Value, looped.Value); + Assert.Equal(obj.Timestamp, looped.Timestamp); + Assert.Null(looped!.Metadata); + } - var looped = serializer.Deserialize<FusionCacheDistributedEntry<string>>(data); - Assert.NotNull(looped); - Assert.Equal(obj.Value, looped.Value); - Assert.Equal(obj.Timestamp, looped.Timestamp); - Assert.Null(looped!.Metadata); - } + [Theory] + [ClassData(typeof(SerializerTypesClassData))] + public async Task LoopSucceedsWithDistributedEntryAndComplexTypesAsync(SerializerType serializerType) + { + var serializer = TestsUtils.GetSerializer(serializerType); + var obj = new FusionCacheDistributedEntry<ComplexType>(ComplexType.CreateSample(), new FusionCacheEntryMetadata(DateTimeOffset.UtcNow.AddSeconds(10), true, DateTimeOffset.UtcNow.AddSeconds(9), "abc123", DateTimeOffset.UtcNow), FusionCacheInternalUtils.GetCurrentTimestamp()); + + var data = await serializer.SerializeAsync(obj); + + Assert.NotNull(data); + Assert.NotEmpty(data); + + var looped = await serializer.DeserializeAsync<FusionCacheDistributedEntry<ComplexType>>(data); + Assert.NotNull(looped); + Assert.Equal(obj.Value, looped.Value); + Assert.Equal(obj.Timestamp, looped.Timestamp); + Assert.Equal(obj.Metadata!.IsFromFailSafe, looped.Metadata!.IsFromFailSafe); + Assert.Equal(obj.Metadata!.LogicalExpiration, looped.Metadata!.LogicalExpiration); + Assert.Equal(obj.Metadata!.EagerExpiration, looped.Metadata!.EagerExpiration); + Assert.Equal(obj.Metadata!.ETag, looped.Metadata!.ETag); + Assert.Equal(obj.Metadata!.LastModified, looped.Metadata!.LastModified); + } - [Theory] - [ClassData(typeof(SerializerTypesClassData))] - public async Task LoopSucceedsWithDistributedEntryAndComplexTypesAsync(SerializerType serializerType) - { - var serializer = TestsUtils.GetSerializer(serializerType); - var obj = new FusionCacheDistributedEntry<ComplexType>(ComplexType.CreateSample(), new FusionCacheEntryMetadata(DateTimeOffset.UtcNow.AddSeconds(10), true, DateTimeOffset.UtcNow.AddSeconds(9), "abc123", DateTimeOffset.UtcNow)); - - var data = await serializer.SerializeAsync(obj); - - Assert.NotNull(data); - Assert.NotEmpty(data); - - var looped = await serializer.DeserializeAsync<FusionCacheDistributedEntry<ComplexType>>(data); - Assert.NotNull(looped); - Assert.Equal(obj.Value, looped.Value); - Assert.Equal(obj.Timestamp, looped.Timestamp); - Assert.Equal(obj.Metadata!.IsFromFailSafe, looped.Metadata!.IsFromFailSafe); - Assert.Equal(obj.Metadata!.LogicalExpiration, looped.Metadata!.LogicalExpiration); - Assert.Equal(obj.Metadata!.EagerExpiration, looped.Metadata!.EagerExpiration); - Assert.Equal(obj.Metadata!.ETag, looped.Metadata!.ETag); - Assert.Equal(obj.Metadata!.LastModified, looped.Metadata!.LastModified); - } + [Theory] + [ClassData(typeof(SerializerTypesClassData))] + public void LoopSucceedsWithDistributedEntryAndComplexTypes(SerializerType serializerType) + { + var serializer = TestsUtils.GetSerializer(serializerType); + var obj = new FusionCacheDistributedEntry<ComplexType>(ComplexType.CreateSample(), new FusionCacheEntryMetadata(DateTimeOffset.UtcNow.AddSeconds(10), true, DateTimeOffset.UtcNow.AddSeconds(9), "abc123", DateTimeOffset.UtcNow), FusionCacheInternalUtils.GetCurrentTimestamp()); + + var data = serializer.Serialize(obj); + + Assert.NotNull(data); + Assert.NotEmpty(data); + + var looped = serializer.Deserialize<FusionCacheDistributedEntry<ComplexType>>(data); + Assert.NotNull(looped); + Assert.Equal(obj.Value, looped.Value); + Assert.Equal(obj.Timestamp, looped.Timestamp); + Assert.Equal(obj.Metadata!.IsFromFailSafe, looped.Metadata!.IsFromFailSafe); + Assert.Equal(obj.Metadata!.LogicalExpiration, looped.Metadata!.LogicalExpiration); + Assert.Equal(obj.Metadata!.EagerExpiration, looped.Metadata!.EagerExpiration); + Assert.Equal(obj.Metadata!.ETag, looped.Metadata!.ETag); + Assert.Equal(obj.Metadata!.LastModified, looped.Metadata!.LastModified); + } - [Theory] - [ClassData(typeof(SerializerTypesClassData))] - public void LoopSucceedsWithDistributedEntryAndComplexTypes(SerializerType serializerType) - { - var serializer = TestsUtils.GetSerializer(serializerType); - var obj = new FusionCacheDistributedEntry<ComplexType>(ComplexType.CreateSample(), new FusionCacheEntryMetadata(DateTimeOffset.UtcNow.AddSeconds(10), true, DateTimeOffset.UtcNow.AddSeconds(9), "abc123", DateTimeOffset.UtcNow)); - - var data = serializer.Serialize(obj); - - Assert.NotNull(data); - Assert.NotEmpty(data); - - var looped = serializer.Deserialize<FusionCacheDistributedEntry<ComplexType>>(data); - Assert.NotNull(looped); - Assert.Equal(obj.Value, looped.Value); - Assert.Equal(obj.Timestamp, looped.Timestamp); - Assert.Equal(obj.Metadata!.IsFromFailSafe, looped.Metadata!.IsFromFailSafe); - Assert.Equal(obj.Metadata!.LogicalExpiration, looped.Metadata!.LogicalExpiration); - Assert.Equal(obj.Metadata!.EagerExpiration, looped.Metadata!.EagerExpiration); - Assert.Equal(obj.Metadata!.ETag, looped.Metadata!.ETag); - Assert.Equal(obj.Metadata!.LastModified, looped.Metadata!.LastModified); - } + [Theory] + [ClassData(typeof(SerializerTypesClassData))] + public async Task CanDeserializeOldVersionsAsync(SerializerType serializerType) + { + var serializer = TestsUtils.GetSerializer(serializerType); - [Theory] - [ClassData(typeof(SerializerTypesClassData))] - public async Task CanDeserializeOldVersionsAsync(SerializerType serializerType) - { - var serializer = TestsUtils.GetSerializer(serializerType); + var assembly = serializer.GetType().Assembly; + var fvi = System.Diagnostics.FileVersionInfo.GetVersionInfo(assembly.Location); + string? currentVersion = fvi.FileVersion!.Substring(0, fvi.FileVersion.LastIndexOf('.')); - var assembly = serializer.GetType().Assembly; - var fvi = System.Diagnostics.FileVersionInfo.GetVersionInfo(assembly.Location); - string? currentVersion = fvi.FileVersion!.Substring(0, fvi.FileVersion.LastIndexOf('.')); + var filePrefix = $"{serializer.GetType().Name}__"; - var filePrefix = $"{serializer.GetType().Name}__"; + var files = Directory.GetFiles("Samples\\", filePrefix + "*.bin"); - var files = Directory.GetFiles("Samples\\", filePrefix + "*.bin"); + TestOutput.WriteLine($"Found {files.Length} samples for {serializer.GetType().Name}"); - foreach (var file in files) - { - var payloadVersion = __re_VersionExtractor.Match(file).Groups[1]?.Value?.Replace('_', '.'); + foreach (var file in files) + { + var payloadVersion = __re_VersionExtractor.Match(file).Groups[1]?.Value?.Replace('_', '.'); - var payload = File.ReadAllBytes(file); - var deserialized = await serializer.DeserializeAsync<FusionCacheDistributedEntry<string>>(payload); - Assert.False(deserialized is null, $"Failed deserializing payload from v{payloadVersion}"); + var payload = File.ReadAllBytes(file); + var deserialized = await serializer.DeserializeAsync<FusionCacheDistributedEntry<string>>(payload); + Assert.False(deserialized is null, $"Failed deserializing payload from v{payloadVersion}"); - _output.WriteLine($"Correctly deserialized payload from v{payloadVersion} to v{currentVersion} (current) using {serializer.GetType().Name}"); - } + TestOutput.WriteLine($"Correctly deserialized payload from v{payloadVersion} to v{currentVersion} (current) using {serializer.GetType().Name}"); } + } - [Theory] - [ClassData(typeof(SerializerTypesClassData))] - public void CanDeserializeOldVersions(SerializerType serializerType) - { - var serializer = TestsUtils.GetSerializer(serializerType); + [Theory] + [ClassData(typeof(SerializerTypesClassData))] + public void CanDeserializeOldVersions(SerializerType serializerType) + { + var serializer = TestsUtils.GetSerializer(serializerType); - var assembly = serializer.GetType().Assembly; - var fvi = System.Diagnostics.FileVersionInfo.GetVersionInfo(assembly.Location); - string? currentVersion = fvi.FileVersion!.Substring(0, fvi.FileVersion.LastIndexOf('.')); + var assembly = serializer.GetType().Assembly; + var fvi = System.Diagnostics.FileVersionInfo.GetVersionInfo(assembly.Location); + string? currentVersion = fvi.FileVersion!.Substring(0, fvi.FileVersion.LastIndexOf('.')); - var filePrefix = $"{serializer.GetType().Name}__"; + var filePrefix = $"{serializer.GetType().Name}__"; - var files = Directory.GetFiles("Samples\\", filePrefix + "*.bin"); + var files = Directory.GetFiles("Samples\\", filePrefix + "*.bin"); - foreach (var file in files) - { - var payloadVersion = __re_VersionExtractor.Match(file).Groups[1]?.Value?.Replace('_', '.'); + TestOutput.WriteLine($"Found {files.Length} samples for {serializer.GetType().Name}"); + + foreach (var file in files) + { + var payloadVersion = __re_VersionExtractor.Match(file).Groups[1]?.Value?.Replace('_', '.'); - var payload = File.ReadAllBytes(file); - var deserialized = serializer.Deserialize<FusionCacheDistributedEntry<string>>(payload); - Assert.False(deserialized is null, $"Failed deserializing payload from v{payloadVersion}"); + var payload = File.ReadAllBytes(file); + var deserialized = serializer.Deserialize<FusionCacheDistributedEntry<string>>(payload); + Assert.False(deserialized is null, $"Failed deserializing payload from v{payloadVersion}"); - _output.WriteLine($"Correctly deserialized payload from v{payloadVersion} to v{currentVersion} (current) using {serializer.GetType().Name}"); - } + TestOutput.WriteLine($"Correctly deserialized payload from v{payloadVersion} to v{currentVersion} (current) using {serializer.GetType().Name}"); } } } diff --git a/tests/ZiggyCreatures.FusionCache.Tests/SingleLevelTests.cs b/tests/ZiggyCreatures.FusionCache.Tests/SingleLevelTests.cs deleted file mode 100644 index 35e904d3..00000000 --- a/tests/ZiggyCreatures.FusionCache.Tests/SingleLevelTests.cs +++ /dev/null @@ -1,1634 +0,0 @@ -using System; -using System.Diagnostics; -using System.Threading; -using System.Threading.Tasks; -using FusionCacheTests.Stuff; -using Microsoft.Extensions.Caching.Distributed; -using Microsoft.Extensions.Caching.Memory; -using Newtonsoft.Json; -using Xunit; -using ZiggyCreatures.Caching.Fusion; -using ZiggyCreatures.Caching.Fusion.NullObjects; - -namespace FusionCacheTests -{ - public static class SingleLevelTestsExtMethods - { - public static FusionCacheEntryOptions SetFactoryTimeoutsMs(this FusionCacheEntryOptions options, int? softTimeoutMs = null, int? hardTimeoutMs = null, bool? keepTimedOutFactoryResult = null) - { - if (softTimeoutMs is not null) - options.FactorySoftTimeout = TimeSpan.FromMilliseconds(softTimeoutMs.Value); - if (hardTimeoutMs is not null) - options.FactoryHardTimeout = TimeSpan.FromMilliseconds(hardTimeoutMs.Value); - if (keepTimedOutFactoryResult is not null) - options.AllowTimedOutFactoryBackgroundCompletion = keepTimedOutFactoryResult.Value; - return options; - } - } - - public class SingleLevelTests - { - [Fact] - public void CannotAssignNullToDefaultEntryOptions() - { - Assert.Throws<ArgumentNullException>(() => - { - var foo = new FusionCacheOptions() { DefaultEntryOptions = null! }; - }); - } - - [Fact] - public async Task CanRemoveAsync() - { - using var cache = new FusionCache(new FusionCacheOptions()); - await cache.SetAsync<int>("foo", 42); - var foo1 = await cache.GetOrDefaultAsync<int>("foo"); - await cache.RemoveAsync("foo"); - var foo2 = await cache.GetOrDefaultAsync<int>("foo"); - Assert.Equal(42, foo1); - Assert.Equal(0, foo2); - } - - [Fact] - public void CanRemove() - { - using var cache = new FusionCache(new FusionCacheOptions()); - cache.Set<int>("foo", 42); - var foo1 = cache.GetOrDefault<int>("foo"); - cache.Remove("foo"); - var foo2 = cache.GetOrDefault<int>("foo"); - Assert.Equal(42, foo1); - Assert.Equal(0, foo2); - } - - [Fact] - public async Task ReturnsStaleDataWhenFactoryFailsWithFailSafeAsync() - { - using var cache = new FusionCache(new FusionCacheOptions()); - var initialValue = await cache.GetOrSetAsync<int>("foo", async _ => 42, new FusionCacheEntryOptions(TimeSpan.FromSeconds(1)).SetFailSafe(true)); - await Task.Delay(1_500); - var newValue = await cache.GetOrSetAsync<int>("foo", async _ => throw new Exception("Sloths are cool"), new FusionCacheEntryOptions(TimeSpan.FromSeconds(1)).SetFailSafe(true)); - Assert.Equal(initialValue, newValue); - } - - [Fact] - public void ReturnsStaleDataWhenFactoryFailsWithFailSafe() - { - using var cache = new FusionCache(new FusionCacheOptions()); - var initialValue = cache.GetOrSet<int>("foo", _ => 42, new FusionCacheEntryOptions(TimeSpan.FromSeconds(1)).SetFailSafe(true)); - Thread.Sleep(1_500); - var newValue = cache.GetOrSet<int>("foo", _ => throw new Exception("Sloths are cool"), new FusionCacheEntryOptions(TimeSpan.FromSeconds(1)).SetFailSafe(true)); - Assert.Equal(initialValue, newValue); - } - - [Fact] - public async Task ThrowsWhenFactoryThrowsWithoutFailSafeAsync() - { - using var cache = new FusionCache(new FusionCacheOptions()); - var initialValue = await cache.GetOrSetAsync<int>("foo", async _ => 42, new FusionCacheEntryOptions(TimeSpan.FromSeconds(1)).SetFailSafe(true)); - await Task.Delay(1_100); - await Assert.ThrowsAnyAsync<Exception>(async () => - { - var newValue = await cache.GetOrSetAsync<int>("foo", async _ => throw new Exception("Sloths are cool"), new FusionCacheEntryOptions(TimeSpan.FromSeconds(1)).SetFailSafe(false)); - }); - } - - [Fact] - public void ThrowsWhenFactoryThrowsWithoutFailSafe() - { - using var cache = new FusionCache(new FusionCacheOptions()); - var initialValue = cache.GetOrSet<int>("foo", _ => 42, new FusionCacheEntryOptions(TimeSpan.FromSeconds(1)) { IsFailSafeEnabled = true }); - Thread.Sleep(1_100); - Assert.ThrowsAny<Exception>(() => - { - var newValue = cache.GetOrSet<int>("foo", _ => throw new Exception("Sloths are cool"), new FusionCacheEntryOptions(TimeSpan.FromSeconds(1)) { IsFailSafeEnabled = false }); - }); - } - - [Fact] - public async Task ThrowsOnFactoryHardTimeoutWithoutStaleDataAsync() - { - using var cache = new FusionCache(new FusionCacheOptions()); - await Assert.ThrowsAsync<SyntheticTimeoutException>(async () => - { - var value = await cache.GetOrSetAsync<int>("foo", async _ => { await Task.Delay(1_000); return 21; }, new FusionCacheEntryOptions(TimeSpan.FromSeconds(1)).SetFailSafe(true).SetFactoryTimeoutsMs(2_000, 100)); - }); - } - - [Fact] - public void ThrowsOnFactoryHardTimeoutWithoutStaleData() - { - using var cache = new FusionCache(new FusionCacheOptions()); - Assert.Throws<SyntheticTimeoutException>(() => - { - var value = cache.GetOrSet<int>("foo", _ => { Thread.Sleep(1_000); return 21; }, new FusionCacheEntryOptions(TimeSpan.FromSeconds(1)).SetFailSafe(true).SetFactoryTimeoutsMs(2_000, 100)); - }); - } - - [Fact] - public async Task ReturnsStaleDataWhenFactorySoftTimeoutWithFailSafeAsync() - { - using var cache = new FusionCache(new FusionCacheOptions()); - var initialValue = await cache.GetOrSetAsync<int>("foo", async _ => 42, new FusionCacheEntryOptions(TimeSpan.FromSeconds(1)).SetFailSafe(true)); - await Task.Delay(1_100); - var newValue = await cache.GetOrSetAsync<int>("foo", async _ => { await Task.Delay(1_000); return 21; }, new FusionCacheEntryOptions(TimeSpan.FromSeconds(1)).SetFailSafe(true).SetFactoryTimeoutsMs(100)); - Assert.Equal(initialValue, newValue); - } - - [Fact] - public void ReturnsStaleDataWhenFactorySoftTimeoutWithFailSafe() - { - using var cache = new FusionCache(new FusionCacheOptions()); - var initialValue = cache.GetOrSet<int>("foo", _ => 42, new FusionCacheEntryOptions(TimeSpan.FromSeconds(1)).SetFailSafe(true)); - Thread.Sleep(1_100); - var newValue = cache.GetOrSet<int>("foo", _ => { Thread.Sleep(1_000); return 21; }, new FusionCacheEntryOptions(TimeSpan.FromSeconds(1)).SetFailSafe(true).SetFactoryTimeoutsMs(100)); - Assert.Equal(initialValue, newValue); - } - - [Fact] - public async Task DoesNotSoftTimeoutWithoutStaleDataAsync() - { - using var cache = new FusionCache(new FusionCacheOptions()); - var initialValue = await cache.GetOrSetAsync<int>("foo", async _ => { await Task.Delay(1_000); return 21; }, new FusionCacheEntryOptions(TimeSpan.FromSeconds(1)).SetFailSafe(true).SetFactoryTimeoutsMs(100)); - Assert.Equal(21, initialValue); - } - - [Fact] - public void DoesNotSoftTimeoutWithoutStaleData() - { - using var cache = new FusionCache(new FusionCacheOptions()); - var initialValue = cache.GetOrSet<int>("foo", _ => { Thread.Sleep(1_000); return 21; }, new FusionCacheEntryOptions(TimeSpan.FromSeconds(1)).SetFailSafe(true).SetFactoryTimeoutsMs(100)); - Assert.Equal(21, initialValue); - } - - [Fact] - public async Task DoesHardTimeoutEvenWithoutStaleDataAsync() - { - using var cache = new FusionCache(new FusionCacheOptions()); - await Assert.ThrowsAnyAsync<Exception>(async () => - { - var initialValue = await cache.GetOrSetAsync<int>("foo", async _ => { await Task.Delay(1_000); return 21; }, new FusionCacheEntryOptions(TimeSpan.FromSeconds(1)).SetFailSafe(true).SetFactoryTimeoutsMs(100, 500)); - }); - } - - [Fact] - public void DoesHardTimeoutEvenWithoutStaleData() - { - using var cache = new FusionCache(new FusionCacheOptions()); - Assert.ThrowsAny<Exception>(() => - { - var initialValue = cache.GetOrSet<int>("foo", _ => { Thread.Sleep(1_000); return 21; }, new FusionCacheEntryOptions(TimeSpan.FromSeconds(1)).SetFailSafe(true).SetFactoryTimeoutsMs(100, 500)); - }); - } - - [Fact] - public async Task ReturnsStaleDataWhenFactoryHitHardTimeoutWithFailSafeAsync() - { - using var cache = new FusionCache(new FusionCacheOptions()); - await cache.SetAsync<int>("foo", 42, new FusionCacheEntryOptions(TimeSpan.FromSeconds(1)) { IsFailSafeEnabled = true }); - await Task.Delay(1_100); - var newValue = await cache.GetOrSetAsync<int>("foo", async _ => { await Task.Delay(1_000); return 21; }, new FusionCacheEntryOptions(TimeSpan.FromSeconds(1)).SetFailSafe(true).SetFactoryTimeoutsMs(100, 500)); - Assert.Equal(42, newValue); - } - - [Fact] - public void ReturnsStaleDataWhenFactoryHitHardTimeoutWithFailSafe() - { - using var cache = new FusionCache(new FusionCacheOptions()); - cache.Set<int>("foo", 42, new FusionCacheEntryOptions(TimeSpan.FromSeconds(1)) { IsFailSafeEnabled = true }); - Thread.Sleep(1_100); - var newValue = cache.GetOrSet<int>("foo", _ => { Thread.Sleep(1_000); return 21; }, new FusionCacheEntryOptions(TimeSpan.FromSeconds(1)).SetFailSafe(true).SetFactoryTimeoutsMs(100, 500)); - Assert.Equal(42, newValue); - } - - [Fact] - public async Task SetOverwritesAnExistingValueAsync() - { - using var cache = new FusionCache(new FusionCacheOptions()); - var initialValue = 42; - var newValue = 21; - cache.Set<int>("foo", initialValue, new FusionCacheEntryOptions(TimeSpan.FromSeconds(10))); - cache.Set<int>("foo", newValue, new FusionCacheEntryOptions(TimeSpan.FromSeconds(10))); - var actualValue = await cache.GetOrDefaultAsync<int>("foo", -1, new FusionCacheEntryOptions(TimeSpan.FromSeconds(10))); - Assert.Equal(newValue, actualValue); - } - - [Fact] - public void SetOverwritesAnExistingValue() - { - using var cache = new FusionCache(new FusionCacheOptions()); - var initialValue = 42; - var newValue = 21; - cache.Set<int>("foo", initialValue, new FusionCacheEntryOptions(TimeSpan.FromSeconds(10))); - cache.Set<int>("foo", newValue, new FusionCacheEntryOptions(TimeSpan.FromSeconds(10))); - var actualValue = cache.GetOrDefault<int>("foo", -1, new FusionCacheEntryOptions(TimeSpan.FromSeconds(10))); - Assert.Equal(newValue, actualValue); - } - - [Fact] - public async Task GetOrSetDoesNotOverwriteANonExpiredValueAsync() - { - using var cache = new FusionCache(new FusionCacheOptions()); - var initialValue = await cache.GetOrSetAsync<int>("foo", async _ => 42, new FusionCacheEntryOptions(TimeSpan.FromSeconds(10))); - var newValue = await cache.GetOrSetAsync<int>("foo", async _ => 21, new FusionCacheEntryOptions(TimeSpan.FromSeconds(10))); - Assert.Equal(initialValue, newValue); - } - - [Fact] - public void GetOrSetDoesNotOverwriteANonExpiredValue() - { - using var cache = new FusionCache(new FusionCacheOptions()); - var initialValue = cache.GetOrSet<int>("foo", _ => 42, new FusionCacheEntryOptions(TimeSpan.FromSeconds(10))); - var newValue = cache.GetOrSet<int>("foo", _ => 21, new FusionCacheEntryOptions(TimeSpan.FromSeconds(10))); - Assert.Equal(initialValue, newValue); - } - - [Fact] - public async Task DoesNotReturnStaleDataIfFactorySucceedsAsync() - { - using var cache = new FusionCache(new FusionCacheOptions()); - var initialValue = await cache.GetOrSetAsync<int>("foo", async _ => 42, new FusionCacheEntryOptions(TimeSpan.FromSeconds(1)) { IsFailSafeEnabled = true }); - await Task.Delay(1_500); - var newValue = await cache.GetOrSetAsync<int>("foo", async _ => 21, new FusionCacheEntryOptions(TimeSpan.FromSeconds(1)) { IsFailSafeEnabled = true }); - Assert.NotEqual(initialValue, newValue); - } - - [Fact] - public void DoesNotReturnStaleDataIfFactorySucceeds() - { - using var cache = new FusionCache(new FusionCacheOptions()); - var initialValue = cache.GetOrSet<int>("foo", _ => 42, new FusionCacheEntryOptions(TimeSpan.FromSeconds(1)) { IsFailSafeEnabled = true }); - Thread.Sleep(1_500); - var newValue = cache.GetOrSet<int>("foo", _ => 21, new FusionCacheEntryOptions(TimeSpan.FromSeconds(1)) { IsFailSafeEnabled = true }); - Assert.NotEqual(initialValue, newValue); - } - - [Fact] - public async Task GetOrDefaultDoesReturnStaleDataWithFailSafeAsync() - { - using var cache = new FusionCache(new FusionCacheOptions()); - var initialValue = 42; - await cache.SetAsync<int>("foo", initialValue, new FusionCacheEntryOptions(TimeSpan.FromSeconds(1)) { IsFailSafeEnabled = true }); - await Task.Delay(1_500); - var newValue = await cache.GetOrDefaultAsync<int>("foo", options: new FusionCacheEntryOptions(TimeSpan.FromSeconds(1)) { IsFailSafeEnabled = true }); - Assert.Equal(initialValue, newValue); - } - - [Fact] - public void GetOrDefaultDoesReturnStaleDataWithFailSafe() - { - using var cache = new FusionCache(new FusionCacheOptions()); - var initialValue = 42; - cache.Set<int>("foo", initialValue, new FusionCacheEntryOptions(TimeSpan.FromSeconds(1)) { IsFailSafeEnabled = true }); - Thread.Sleep(1_500); - var newValue = cache.GetOrDefault<int>("foo", options: new FusionCacheEntryOptions(TimeSpan.FromSeconds(1)) { IsFailSafeEnabled = true }); - Assert.Equal(initialValue, newValue); - } - - [Fact] - public async Task GetOrDefaultDoesNotReturnStaleDataWithoutFailSafeAsync() - { - using var cache = new FusionCache(new FusionCacheOptions()); - var initialValue = 42; - await cache.SetAsync<int>("foo", initialValue, new FusionCacheEntryOptions(TimeSpan.FromSeconds(1)) { IsFailSafeEnabled = true }); - await Task.Delay(1_500); - var newValue = await cache.GetOrDefaultAsync<int>("foo", options: new FusionCacheEntryOptions(TimeSpan.FromSeconds(1)) { IsFailSafeEnabled = false }); - Assert.NotEqual(initialValue, newValue); - } - - [Fact] - public void GetOrDefaultDoesNotReturnStaleWithoutFailSafe() - { - using var cache = new FusionCache(new FusionCacheOptions()); - var initialValue = 42; - cache.Set<int>("foo", initialValue, new FusionCacheEntryOptions(TimeSpan.FromSeconds(1)).SetFailSafe(true)); - Thread.Sleep(1_500); - var newValue = cache.GetOrDefault<int>("foo", options: new FusionCacheEntryOptions(TimeSpan.FromSeconds(1)).SetFailSafe(false)); - Assert.NotEqual(initialValue, newValue); - } - - [Fact] - public async Task FactoryTimedOutButSuccessfulDoesUpdateCachedValueAsync() - { - using var cache = new FusionCache(new FusionCacheOptions()); - await cache.SetAsync<int>("foo", 42, new FusionCacheEntryOptions(TimeSpan.FromSeconds(1)).SetFailSafe(true, TimeSpan.FromMinutes(1))); - var initialValue = cache.GetOrDefault<int>("foo", options: new FusionCacheEntryOptions(TimeSpan.FromSeconds(1)).SetFailSafe(true, TimeSpan.FromMinutes(1))); - await Task.Delay(1_500); - var middleValue = await cache.GetOrSetAsync<int>("foo", async ct => { await Task.Delay(2_000); ct.ThrowIfCancellationRequested(); return 21; }, new FusionCacheEntryOptions(TimeSpan.FromSeconds(1)).SetFailSafe(true).SetFactoryTimeoutsMs(500)); - var interstitialValue = await cache.GetOrDefaultAsync<int>("foo", options: new FusionCacheEntryOptions(TimeSpan.FromSeconds(1)).SetFailSafe(true)); - await Task.Delay(3_000); - var finalValue = await cache.GetOrDefaultAsync<int>("foo", options: new FusionCacheEntryOptions(TimeSpan.FromSeconds(1)).SetFailSafe(true)); - - Assert.Equal(42, initialValue); - Assert.Equal(42, middleValue); - Assert.Equal(42, interstitialValue); - Assert.Equal(21, finalValue); - } - - [Fact] - public void FactoryTimedOutButSuccessfulDoesUpdateCachedValue() - { - using var cache = new FusionCache(new FusionCacheOptions()); - cache.Set<int>("foo", 42, new FusionCacheEntryOptions(TimeSpan.FromSeconds(1)).SetFailSafe(true, TimeSpan.FromMinutes(1))); - var initialValue = cache.GetOrDefault<int>("foo", options: new FusionCacheEntryOptions(TimeSpan.FromSeconds(1)).SetFailSafe(true, TimeSpan.FromMinutes(1))); - Thread.Sleep(1_500); - var middleValue = cache.GetOrSet<int>("foo", ct => { Thread.Sleep(2_000); ct.ThrowIfCancellationRequested(); return 21; }, new FusionCacheEntryOptions(TimeSpan.FromSeconds(1)).SetFailSafe(true).SetFactoryTimeoutsMs(500)); - var interstitialValue = cache.GetOrDefault<int>("foo", options: new FusionCacheEntryOptions(TimeSpan.FromSeconds(1)).SetFailSafe(true)); - Thread.Sleep(3_000); - var finalValue = cache.GetOrDefault<int>("foo", options: new FusionCacheEntryOptions(TimeSpan.FromSeconds(1)).SetFailSafe(true)); - - Assert.Equal(42, initialValue); - Assert.Equal(42, middleValue); - Assert.Equal(42, interstitialValue); - Assert.Equal(21, finalValue); - } - - [Fact] - public async Task TryGetReturnsCorrectlyAsync() - { - using var cache = new FusionCache(new FusionCacheOptions()); - var res1 = await cache.TryGetAsync<int>("foo"); - await cache.SetAsync<int>("foo", 42); - var res2 = await cache.TryGetAsync<int>("foo"); - Assert.False(res1.HasValue); - Assert.Throws<InvalidOperationException>(() => - { - var foo = res1.Value; - }); - Assert.True(res2.HasValue); - Assert.Equal(42, res2.Value); - } - - [Fact] - public void TryGetReturnsCorrectly() - { - using var cache = new FusionCache(new FusionCacheOptions()); - var res1 = cache.TryGet<int>("foo"); - cache.Set<int>("foo", 42); - var res2 = cache.TryGet<int>("foo"); - Assert.False(res1.HasValue); - Assert.Throws<InvalidOperationException>(() => - { - var foo = res1.Value; - }); - Assert.True(res2.HasValue); - Assert.Equal(42, res2.Value); - } - - [Fact] - public async Task CancelingAnOperationActuallyCancelsItAsync() - { - using var cache = new FusionCache(new FusionCacheOptions()); - int res = -1; - var sw = Stopwatch.StartNew(); - var outerCancelDelayMs = 500; - var factoryDelayMs = 2_000; - await Assert.ThrowsAnyAsync<OperationCanceledException>(async () => - { - var cts = new CancellationTokenSource(outerCancelDelayMs); - res = await cache.GetOrSetAsync<int>("foo", async ct => { await Task.Delay(factoryDelayMs); ct.ThrowIfCancellationRequested(); return 42; }, options => options.SetDurationSec(60), cts.Token); - }); - sw.Stop(); - - Assert.Equal(-1, res); - Assert.True(sw.ElapsedMilliseconds >= outerCancelDelayMs, "Elapsed is less than outer cancel"); - Assert.True(sw.ElapsedMilliseconds < factoryDelayMs, "Elapsed is not less than factory delay"); - } - - [Fact] - public void CancelingAnOperationActuallyCancelsIt() - { - using var cache = new FusionCache(new FusionCacheOptions()); - int res = -1; - var sw = Stopwatch.StartNew(); - var outerCancelDelayMs = 500; - var factoryDelayMs = 2_000; - Assert.ThrowsAny<OperationCanceledException>(() => - { - var cts = new CancellationTokenSource(outerCancelDelayMs); - res = cache.GetOrSet<int>("foo", ct => { Thread.Sleep(factoryDelayMs); ct.ThrowIfCancellationRequested(); return 42; }, options => options.SetDurationSec(60), cts.Token); - }); - sw.Stop(); - - Assert.Equal(-1, res); - Assert.True(sw.ElapsedMilliseconds >= outerCancelDelayMs, "Elapsed is less than outer cancel"); - Assert.True(sw.ElapsedMilliseconds < factoryDelayMs, "Elapsed is not less than factory delay"); - } - - [Fact] - public async Task HandlesFlexibleSimpleTypeConversionsAsync() - { - using var cache = new FusionCache(new FusionCacheOptions()); - var initialValue = (object)42; - await cache.SetAsync("foo", initialValue, TimeSpan.FromHours(24)); - var newValue = await cache.GetOrDefaultAsync<int>("foo"); - Assert.Equal(initialValue, newValue); - } - - [Fact] - public void HandlesFlexibleSimpleTypeConversions() - { - using var cache = new FusionCache(new FusionCacheOptions()); - var initialValue = (object)42; - cache.Set("foo", initialValue, TimeSpan.FromHours(24)); - var newValue = cache.GetOrDefault<int>("foo"); - Assert.Equal(initialValue, newValue); - } - - [Fact] - public async Task HandlesFlexibleComplexTypeConversionsAsync() - { - using var cache = new FusionCache(new FusionCacheOptions()); - var initialValue = (object)ComplexType.CreateSample(); - await cache.SetAsync("foo", initialValue, TimeSpan.FromHours(24)); - var newValue = await cache.GetOrDefaultAsync<ComplexType>("foo"); - Assert.Equal(initialValue, newValue); - } - - [Fact] - public void HandlesFlexibleComplexTypeConversions() - { - using var cache = new FusionCache(new FusionCacheOptions()); - var initialValue = (object)ComplexType.CreateSample(); - cache.Set("foo", initialValue, TimeSpan.FromHours(24)); - var newValue = cache.GetOrDefault<ComplexType>("foo"); - Assert.Equal(initialValue, newValue); - } - - [Fact] - public async Task GetOrDefaultDoesNotSetAsync() - { - using var cache = new FusionCache(new FusionCacheOptions()); - var foo = await cache.GetOrDefaultAsync<int>("foo", 42, opt => opt.SetDuration(TimeSpan.FromHours(24))); - var bar = await cache.GetOrDefaultAsync<int>("foo", 21, opt => opt.SetDuration(TimeSpan.FromHours(24))); - var baz = await cache.TryGetAsync<int>("foo", opt => opt.SetDuration(TimeSpan.FromHours(24))); - Assert.Equal(42, foo); - Assert.Equal(21, bar); - Assert.False(baz.HasValue); - } - - [Fact] - public void GetOrDefaultDoesNotSet() - { - using var cache = new FusionCache(new FusionCacheOptions()); - var foo = cache.GetOrDefault<int>("foo", 42, opt => opt.SetDuration(TimeSpan.FromHours(24))); - var bar = cache.GetOrDefault<int>("foo", 21, opt => opt.SetDuration(TimeSpan.FromHours(24))); - var baz = cache.TryGet<int>("foo", opt => opt.SetDuration(TimeSpan.FromHours(24))); - Assert.Equal(42, foo); - Assert.Equal(21, bar); - Assert.False(baz.HasValue); - } - - [Fact] - public async Task GetOrSetWithDefaultValueWorksAsync() - { - using var cache = new FusionCache(new FusionCacheOptions()); - var foo = 42; - await cache.GetOrSetAsync<int>("foo", foo, TimeSpan.FromHours(24)); - var bar = await cache.GetOrDefaultAsync<int>("foo", 21); - Assert.Equal(foo, bar); - } - - [Fact] - public void GetOrSetWithDefaultValueWorks() - { - using var cache = new FusionCache(new FusionCacheOptions()); - var foo = 42; - cache.GetOrSet<int>("foo", foo, TimeSpan.FromHours(24)); - var bar = cache.GetOrDefault<int>("foo", 21); - Assert.Equal(foo, bar); - } - - [Fact] - public async Task ThrottleDurationWorksCorrectlyAsync() - { - using var cache = new FusionCache(new FusionCacheOptions()); - var duration = TimeSpan.FromSeconds(1); - var throttleDuration = TimeSpan.FromSeconds(3); - - // SET THE VALUE (WITH FAIL-SAFE ENABLED) - await cache.SetAsync("foo", 42, opt => opt.SetDuration(duration).SetFailSafe(true, throttleDuration: throttleDuration)); - // LET IT EXPIRE - await Task.Delay(duration.PlusALittleBit()).ConfigureAwait(false); - // CHECK EXPIRED (WITHOUT FAIL-SAFE) - var nope = await cache.TryGetAsync<int>("foo", opt => opt.SetFailSafe(false)); - // DO NOT ACTIVATE FAIL-SAFE AND THROTTLE DURATION - var default1 = await cache.GetOrDefaultAsync("foo", 1); - // ACTIVATE FAIL-SAFE AND RE-STORE THE VALUE WITH THROTTLE DURATION - var throttled1 = await cache.GetOrDefaultAsync("foo", 1, opt => opt.SetFailSafe(true, throttleDuration: throttleDuration)); - // WAIT A LITTLE BIT (LESS THAN THE DURATION) - await Task.Delay(100).ConfigureAwait(false); - // GET THE THROTTLED (NON EXPIRED) VALUE - var throttled2 = await cache.GetOrDefaultAsync("foo", 2, opt => opt.SetFailSafe(true)); - // LET THE THROTTLE DURATION PASS - await Task.Delay(throttleDuration).ConfigureAwait(false); - // FALLBACK TO THE DEFAULT VALUE - var default3 = await cache.GetOrDefaultAsync("foo", 3, opt => opt.SetFailSafe(false)); - - Assert.False(nope.HasValue); - Assert.Equal(1, default1); - Assert.Equal(42, throttled1); - Assert.Equal(42, throttled2); - Assert.Equal(3, default3); - } - - [Fact] - public void ThrottleDurationWorksCorrectly() - { - using var cache = new FusionCache(new FusionCacheOptions()); - var duration = TimeSpan.FromSeconds(1); - var throttleDuration = TimeSpan.FromSeconds(3); - - // SET THE VALUE (WITH FAIL-SAFE ENABLED) - cache.Set("foo", 42, opt => opt.SetDuration(duration).SetFailSafe(true, throttleDuration: throttleDuration)); - // LET IT EXPIRE - Thread.Sleep(duration.PlusALittleBit()); - // CHECK EXPIRED (WITHOUT FAIL-SAFE) - var nope = cache.TryGet<int>("foo", opt => opt.SetFailSafe(false)); - // DO NOT ACTIVATE FAIL-SAFE AND THROTTLE DURATION - var default1 = cache.GetOrDefault("foo", 1); - // ACTIVATE FAIL-SAFE AND RE-STORE THE VALUE WITH THROTTLE DURATION - var throttled1 = cache.GetOrDefault("foo", 1, opt => opt.SetFailSafe(true, throttleDuration: throttleDuration)); - // WAIT A LITTLE BIT (LESS THAN THE DURATION) - Thread.Sleep(100); - // GET THE THROTTLED (NON EXPIRED) VALUE - var throttled2 = cache.GetOrDefault("foo", 2, opt => opt.SetFailSafe(true)); - // LET THE THROTTLE DURATION PASS - Thread.Sleep(throttleDuration); - // FALLBACK TO THE DEFAULT VALUE - var default3 = cache.GetOrDefault("foo", 3, opt => opt.SetFailSafe(false)); - - Assert.False(nope.HasValue); - Assert.Equal(1, default1); - Assert.Equal(42, throttled1); - Assert.Equal(42, throttled2); - Assert.Equal(3, default3); - } - - [Fact] - public async Task AdaptiveCachingAsync() - { - using var cache = new FusionCache(new FusionCacheOptions()); - var dur = TimeSpan.FromMinutes(5); - cache.DefaultEntryOptions.Duration = dur; - FusionCacheEntryOptions? innerOpt = null; - - var default3 = await cache.GetOrSetAsync<int>( - "foo", - async (ctx, _) => - { - ctx.Options.Duration = TimeSpan.FromSeconds(1); - - innerOpt = ctx.Options; - - return 3; - }, - opt => opt.SetFailSafe(false) - ); - - await Task.Delay(TimeSpan.FromSeconds(2)); - - var maybeValue = await cache.TryGetAsync<int>("foo"); - - Assert.Equal(dur, TimeSpan.FromMinutes(5)); - Assert.Equal(cache.DefaultEntryOptions.Duration, TimeSpan.FromMinutes(5)); - Assert.Equal(innerOpt!.Duration, TimeSpan.FromSeconds(1)); - Assert.False(maybeValue.HasValue); - } - - [Fact] - public void AdaptiveCaching() - { - using var cache = new FusionCache(new FusionCacheOptions()); - var dur = TimeSpan.FromMinutes(5); - cache.DefaultEntryOptions.Duration = dur; - FusionCacheEntryOptions? innerOpt = null; - - var default3 = cache.GetOrSet<int>( - "foo", - (ctx, _) => - { - ctx.Options.Duration = TimeSpan.FromSeconds(1); - - innerOpt = ctx.Options; - - return 3; - }, - opt => opt.SetFailSafe(false) - ); - - Thread.Sleep(TimeSpan.FromSeconds(2)); - - var maybeValue = cache.TryGet<int>("foo"); - - Assert.Equal(dur, TimeSpan.FromMinutes(5)); - Assert.Equal(cache.DefaultEntryOptions.Duration, TimeSpan.FromMinutes(5)); - Assert.Equal(innerOpt!.Duration, TimeSpan.FromSeconds(1)); - Assert.False(maybeValue.HasValue); - } - - [Fact] - public async Task AdaptiveCachingWithBackgroundFactoryCompletionAsync() - { - using var cache = new FusionCache(new FusionCacheOptions()); - var dur = TimeSpan.FromMinutes(5); - cache.DefaultEntryOptions.Duration = dur; - - // SET WITH 1s DURATION + FAIL-SAFE - await cache.SetAsync("foo", 21, options => options.SetDuration(TimeSpan.FromSeconds(1)).SetFailSafe(true)); - - // LET IT BECOME STALE - await Task.Delay(TimeSpan.FromSeconds(2)); - - // CALL GetOrSET WITH A 1s SOFT TIMEOUT AND A FACTORY RUNNING FOR AT LEAST 3s - var value21 = await cache.GetOrSetAsync<int>( - "foo", - async (ctx, _) => - { - // WAIT 3s - await Task.Delay(TimeSpan.FromSeconds(3)); - - // CHANGE THE OPTIONS (SET THE DURATION TO 5s AND DISABLE FAIL-SAFE - ctx.Options.SetDuration(TimeSpan.FromSeconds(5)).SetFailSafe(false); - - return 42; - }, - opt => opt.SetFactoryTimeouts(TimeSpan.FromSeconds(1)).SetFailSafe(true) - ); - - // WAIT FOR 3s (+ EXTRA 1s) SO THE FACTORY COMPLETES IN THE BACKGROUND - await Task.Delay(TimeSpan.FromSeconds(3 + 1)); - - // GET THE VALUE THAT HAS BEEN SET BY THE BACKGROUND COMPLETION OF THE FACTORY - var value42 = await cache.GetOrDefaultAsync<int>("foo", options => options.SetFailSafe(false)); - - // LET THE CACHE ENTRY EXPIRES - await Task.Delay(TimeSpan.FromSeconds(5)); - - // SEE THAT FAIL-SAFE CANNOT BE ACTIVATED (BECAUSE IT WAS DISABLED IN THE FACTORY) - var noValue = await cache.TryGetAsync<int>("foo", options => options.SetFailSafe(true)); - - Assert.Equal(dur, TimeSpan.FromMinutes(5)); - Assert.Equal(cache.DefaultEntryOptions.Duration, TimeSpan.FromMinutes(5)); - Assert.Equal(21, value21); - Assert.Equal(42, value42); - Assert.False(noValue.HasValue); - } - - [Fact] - public void AdaptiveCachingWithBackgroundFactoryCompletion() - { - using var cache = new FusionCache(new FusionCacheOptions()); - var dur = TimeSpan.FromMinutes(5); - cache.DefaultEntryOptions.Duration = dur; - - // SET WITH 1s DURATION + FAIL-SAFE - cache.Set("foo", 21, options => options.SetDuration(TimeSpan.FromSeconds(1)).SetFailSafe(true)); - - // LET IT BECOME STALE - Thread.Sleep(TimeSpan.FromSeconds(2)); - - // CALL GetOrSET WITH A 1s SOFT TIMEOUT AND A FACTORY RUNNING FOR AT LEAST 3s - var value21 = cache.GetOrSet<int>( - "foo", - (ctx, _) => - { - // WAIT 3s - Thread.Sleep(TimeSpan.FromSeconds(3)); - - // CHANGE THE OPTIONS (SET THE DURATION TO 5s AND DISABLE FAIL-SAFE - ctx.Options.SetDuration(TimeSpan.FromSeconds(5)).SetFailSafe(false); - - return 42; - }, - opt => opt.SetFactoryTimeouts(TimeSpan.FromSeconds(1)).SetFailSafe(true) - ); - - // WAIT FOR 3s (+ EXTRA 1s) SO THE FACTORY COMPLETES IN THE BACKGROUND - Thread.Sleep(TimeSpan.FromSeconds(3 + 1)); - - // GET THE VALUE THAT HAS BEEN SET BY THE BACKGROUND COMPLETION OF THE FACTORY - var value42 = cache.GetOrDefault<int>("foo", options => options.SetFailSafe(false)); - - // LET THE CACHE ENTRY EXPIRES - Thread.Sleep(TimeSpan.FromSeconds(5)); - - // SEE THAT FAIL-SAFE CANNOT BE ACTIVATED (BECAUSE IT WAS DISABLED IN THE FACTORY) - var noValue = cache.TryGet<int>("foo", options => options.SetFailSafe(true)); - - Assert.Equal(dur, TimeSpan.FromMinutes(5)); - Assert.Equal(cache.DefaultEntryOptions.Duration, TimeSpan.FromMinutes(5)); - Assert.Equal(21, value21); - Assert.Equal(42, value42); - Assert.False(noValue.HasValue); - } - - [Fact] - public async Task AdaptiveCachingDoesNotChangeOptionsAsync() - { - using var cache = new FusionCache(new FusionCacheOptions()); - var options = new FusionCacheEntryOptions(TimeSpan.FromSeconds(10)); - - _ = await cache.GetOrSetAsync<int>( - "foo", - async (ctx, _) => - { - ctx.Options.Duration = TimeSpan.FromSeconds(20); - return 42; - }, - options - ); - - Assert.Equal(options.Duration, TimeSpan.FromSeconds(10)); - } - - [Fact] - public void AdaptiveCachingDoesNotChangeOptions() - { - using var cache = new FusionCache(new FusionCacheOptions()); - var options = new FusionCacheEntryOptions(TimeSpan.FromSeconds(10)); - - _ = cache.GetOrSet<int>( - "foo", - (ctx, _) => - { - ctx.Options.Duration = TimeSpan.FromSeconds(20); - return 42; - }, - options - ); - - Assert.Equal(options.Duration, TimeSpan.FromSeconds(10)); - } - - [Fact] - public async Task AdaptiveCachingCanWorkWithSkipMemoryCacheAsync() - { - using var cache = new FusionCache(new FusionCacheOptions()); - cache.DefaultEntryOptions.IsFailSafeEnabled = true; - cache.DefaultEntryOptions.Duration = TimeSpan.FromSeconds(1); - cache.DefaultEntryOptions.FailSafeThrottleDuration = TimeSpan.FromSeconds(3); - - var foo1 = await cache.GetOrSetAsync<int>("foo", async _ => 1); - - await Task.Delay(TimeSpan.FromSeconds(1).PlusALittleBit()); - - var foo2 = await cache.GetOrSetAsync<int>("foo", async (ctx, _) => - { - ctx.Options.SkipMemoryCache = true; - - return 2; - }); - - var foo3 = await cache.TryGetAsync<int>("foo"); - - await Task.Delay(cache.DefaultEntryOptions.FailSafeThrottleDuration.PlusALittleBit()); - - var foo4 = await cache.GetOrSetAsync<int>("foo", async _ => 4); - - Assert.Equal(1, foo1); - Assert.Equal(2, foo2); - Assert.True(foo3.HasValue); - Assert.Equal(1, foo3.Value); - Assert.Equal(4, foo4); - } - - [Fact] - public void AdaptiveCachingCanWorkWithSkipMemoryCache() - { - using var cache = new FusionCache(new FusionCacheOptions()); - cache.DefaultEntryOptions.IsFailSafeEnabled = true; - cache.DefaultEntryOptions.Duration = TimeSpan.FromSeconds(1); - cache.DefaultEntryOptions.FailSafeThrottleDuration = TimeSpan.FromSeconds(3); - - var foo1 = cache.GetOrSet<int>("foo", _ => 1); - - Thread.Sleep(TimeSpan.FromSeconds(1).PlusALittleBit()); - - var foo2 = cache.GetOrSet<int>("foo", (ctx, _) => - { - ctx.Options.SkipMemoryCache = true; - - return 2; - }); - - var foo3 = cache.TryGet<int>("foo"); - - Thread.Sleep(cache.DefaultEntryOptions.FailSafeThrottleDuration.PlusALittleBit()); - - var foo4 = cache.GetOrSet<int>("foo", _ => 4); - - Assert.Equal(1, foo1); - Assert.Equal(2, foo2); - Assert.True(foo3.HasValue); - Assert.Equal(1, foo3.Value); - Assert.Equal(4, foo4); - } - - [Fact] - public async Task FailSafeMaxDurationNormalizationOccursAsync() - { - var duration = TimeSpan.FromSeconds(5); - var maxDuration = TimeSpan.FromSeconds(1); - - using var fusionCache = new FusionCache(new FusionCacheOptions()); - await fusionCache.SetAsync<int>("foo", 21, opt => opt.SetDuration(duration).SetFailSafe(true, maxDuration)); - await Task.Delay(maxDuration.PlusALittleBit()); - var value = await fusionCache.GetOrDefaultAsync<int>("foo", opt => opt.SetFailSafe(true)); - Assert.Equal(21, value); - } - - [Fact] - public void FailSafeMaxDurationNormalizationOccurs() - { - var duration = TimeSpan.FromSeconds(5); - var maxDuration = TimeSpan.FromSeconds(1); - - using var fusionCache = new FusionCache(new FusionCacheOptions()); - fusionCache.Set<int>("foo", 21, opt => opt.SetDuration(duration).SetFailSafe(true, maxDuration)); - Thread.Sleep(maxDuration.PlusALittleBit()); - var value = fusionCache.GetOrDefault<int>("foo", opt => opt.SetFailSafe(true)); - Assert.Equal(21, value); - } - - [Fact] - public async Task ReturnsStaleDataWithoutSavingItWhenNoFactoryAsync() - { - using var cache = new FusionCache(new FusionCacheOptions()); - var initialValue = await cache.GetOrSetAsync<int>("foo", async _ => 42, new FusionCacheEntryOptions(TimeSpan.FromSeconds(1)).SetFailSafe(true, TimeSpan.FromSeconds(10), TimeSpan.FromSeconds(30))); - await Task.Delay(1_500); - var maybeValue = await cache.TryGetAsync<int>("foo", opt => opt.SetDuration(TimeSpan.FromSeconds(1)).SetFailSafe(true)); - var defaultValue1 = await cache.GetOrDefaultAsync<int>("foo", 1); - var defaultValue2 = await cache.GetOrDefaultAsync<int>("foo", 2, opt => opt.SetDuration(TimeSpan.FromSeconds(1)).SetFailSafe(true)); - var defaultValue3 = await cache.GetOrDefaultAsync<int>("foo", 3); - - Assert.True(maybeValue.HasValue); - Assert.Equal(42, maybeValue.Value); - Assert.Equal(1, defaultValue1); - Assert.Equal(42, defaultValue2); - Assert.Equal(3, defaultValue3); - } - - [Fact] - public void ReturnsStaleDataWithoutSavingItWhenNoFactory() - { - using var cache = new FusionCache(new FusionCacheOptions()); - var initialValue = cache.GetOrSet<int>("foo", _ => 42, new FusionCacheEntryOptions(TimeSpan.FromSeconds(1)).SetFailSafe(true, TimeSpan.FromSeconds(10), TimeSpan.FromSeconds(30))); - Thread.Sleep(1_500); - var maybeValue = cache.TryGet<int>("foo", opt => opt.SetDuration(TimeSpan.FromSeconds(1)).SetFailSafe(true)); - var defaultValue1 = cache.GetOrDefault<int>("foo", 1); - var defaultValue2 = cache.GetOrDefault<int>("foo", 2, opt => opt.SetDuration(TimeSpan.FromSeconds(1)).SetFailSafe(true)); - var defaultValue3 = cache.GetOrDefault<int>("foo", 3); - - Assert.True(maybeValue.HasValue); - Assert.Equal(42, maybeValue.Value); - Assert.Equal(1, defaultValue1); - Assert.Equal(42, defaultValue2); - Assert.Equal(3, defaultValue3); - } - - [Fact] - public async Task CanHandleInfiniteOrSimilarDurationsAsync() - { - using var cache = new FusionCache(new FusionCacheOptions()); - await cache.SetAsync<int>("foo", 42, opt => opt.SetDuration(TimeSpan.MaxValue - TimeSpan.FromMilliseconds(1)).SetJittering(TimeSpan.FromMinutes(10))); - var foo = await cache.GetOrDefaultAsync<int>("foo", 0); - Assert.Equal(42, foo); - } - - [Fact] - public void CanHandleInfiniteOrSimilarDurations() - { - using var cache = new FusionCache(new FusionCacheOptions()); - cache.Set<int>("foo", 42, opt => opt.SetDuration(TimeSpan.MaxValue - TimeSpan.FromMilliseconds(1)).SetJittering(TimeSpan.FromMinutes(10))); - var foo = cache.GetOrDefault<int>("foo", 0); - Assert.Equal(42, foo); - } - - [Fact] - public async Task CanHandleZeroDurationsAsync() - { - using var cache = new FusionCache(new FusionCacheOptions()); - - await cache.SetAsync<int>("foo", 10, opt => opt.SetDuration(TimeSpan.Zero)); - var foo1 = await cache.GetOrDefaultAsync<int>("foo", 1); - - await cache.SetAsync<int>("foo", 20, opt => opt.SetDuration(TimeSpan.FromMinutes(10))); - var foo2 = await cache.GetOrDefaultAsync<int>("foo", 2); - - await cache.SetAsync<int>("foo", 30, opt => opt.SetDuration(TimeSpan.Zero)); - var foo3 = await cache.GetOrDefaultAsync<int>("foo", 3); - - Assert.Equal(1, foo1); - Assert.Equal(20, foo2); - Assert.Equal(3, foo3); - } - - [Fact] - public void CanHandleZeroDurations() - { - using var cache = new FusionCache(new FusionCacheOptions()); - - cache.Set<int>("foo", 10, opt => opt.SetDuration(TimeSpan.Zero)); - var foo1 = cache.GetOrDefault<int>("foo", 1); - - cache.Set<int>("foo", 20, opt => opt.SetDuration(TimeSpan.FromMinutes(10))); - var foo2 = cache.GetOrDefault<int>("foo", 2); - - cache.Set<int>("foo", 30, opt => opt.SetDuration(TimeSpan.Zero)); - var foo3 = cache.GetOrDefault<int>("foo", 3); - - Assert.Equal(1, foo1); - Assert.Equal(20, foo2); - Assert.Equal(3, foo3); - } - - [Fact] - public async Task CanHandleNegativeDurationsAsync() - { - using var cache = new FusionCache(new FusionCacheOptions()); - - await cache.SetAsync<int>("foo", 10, opt => opt.SetDuration(TimeSpan.FromSeconds(-100))); - var foo1 = await cache.GetOrDefaultAsync<int>("foo", 1); - - await cache.SetAsync<int>("foo", 20, opt => opt.SetDuration(TimeSpan.FromMinutes(10))); - var foo2 = await cache.GetOrDefaultAsync<int>("foo", 2); - - await cache.SetAsync<int>("foo", 30, opt => opt.SetDuration(TimeSpan.FromDays(-100))); - var foo3 = await cache.GetOrDefaultAsync<int>("foo", 3); - - Assert.Equal(1, foo1); - Assert.Equal(20, foo2); - Assert.Equal(3, foo3); - } - - [Fact] - public void CanHandleNegativeDurations() - { - using var cache = new FusionCache(new FusionCacheOptions()); - - cache.Set<int>("foo", 10, opt => opt.SetDuration(TimeSpan.FromSeconds(-100))); - var foo1 = cache.GetOrDefault<int>("foo", 1); - - cache.Set<int>("foo", 20, opt => opt.SetDuration(TimeSpan.FromMinutes(10))); - var foo2 = cache.GetOrDefault<int>("foo", 2); - - cache.Set<int>("foo", 30, opt => opt.SetDuration(TimeSpan.FromDays(-100))); - var foo3 = cache.GetOrDefault<int>("foo", 3); - - Assert.Equal(1, foo1); - Assert.Equal(20, foo2); - Assert.Equal(3, foo3); - } - - [Fact] - public async Task CanHandleConditionalRefreshAsync() - { - static async Task<int> FakeGetAsync(FusionCacheFactoryExecutionContext<int> ctx, FakeHttpEndpoint endpoint) - { - FakeHttpResponse resp; - - if (ctx.HasETag && ctx.HasStaleValue) - { - // ETAG + STALE VALUE -> TRY WITH A CONDITIONAL GET - resp = endpoint.Get(ctx.ETag); - - if (resp.StatusCode == 304) - { - // NOT MODIFIED -> RETURN STALE VALUE - return ctx.NotModified(); - } - } - else - { - // NO STALE VALUE OR NO ETAG -> NORMAL (FULL) GET - resp = endpoint.Get(); - } - - return ctx.Modified( - resp.Content.GetValueOrDefault(), - resp.ETag - ); - } - - var duration = TimeSpan.FromSeconds(1); - var endpoint = new FakeHttpEndpoint(1); - - using var cache = new FusionCache(new FusionCacheOptions()); - // TOT REQ + 1 / FULL RESP + 1 - var v1 = await cache.GetOrSetAsync<int>("foo", async (ctx, _) => await FakeGetAsync(ctx, endpoint), opt => opt.SetDuration(duration).SetFailSafe(true)); - - // CACHED -> NO INCR - var v2 = await cache.GetOrSetAsync<int>("foo", async (ctx, _) => await FakeGetAsync(ctx, endpoint), opt => opt.SetDuration(duration).SetFailSafe(true)); - - // LET THE CACHE EXPIRE - await Task.Delay(duration.PlusALittleBit()); - - // TOT REQ + 1 / COND REQ + 1 / NOT MOD RESP + 1 - var v3 = await cache.GetOrSetAsync<int>("foo", async (ctx, _) => await FakeGetAsync(ctx, endpoint), opt => opt.SetDuration(duration).SetFailSafe(true)); - - // LET THE CACHE EXPIRE - await Task.Delay(duration.PlusALittleBit()); - - // TOT REQ + 1 / COND REQ + 1 / NOT MOD RESP + 1 - var v4 = await cache.GetOrSetAsync<int>("foo", async (ctx, _) => await FakeGetAsync(ctx, endpoint), opt => opt.SetDuration(duration).SetFailSafe(true)); - - // SET VALUE -> CHANGE LAST MODIFIED - endpoint.SetValue(42); - - // LET THE CACHE EXPIRE - await Task.Delay(duration.PlusALittleBit()); - - // TOT REQ + 1 / COND REQ + 1 / FULL RESP + 1 - var v5 = await cache.GetOrSetAsync<int>("foo", async (ctx, _) => await FakeGetAsync(ctx, endpoint), opt => opt.SetDuration(duration).SetFailSafe(true)); - - Assert.Equal(4, endpoint.TotalRequestsCount); - Assert.Equal(3, endpoint.ConditionalRequestsCount); - Assert.Equal(2, endpoint.FullResponsesCount); - Assert.Equal(2, endpoint.NotModifiedResponsesCount); - - Assert.Equal(1, v1); - Assert.Equal(1, v2); - Assert.Equal(1, v3); - Assert.Equal(1, v4); - Assert.Equal(42, v5); - } - - [Fact] - public void CanHandleConditionalRefresh() - { - static int FakeGet(FusionCacheFactoryExecutionContext<int> ctx, FakeHttpEndpoint endpoint) - { - FakeHttpResponse resp; - - if (ctx.HasETag && ctx.HasStaleValue) - { - // ETAG + STALE VALUE -> TRY WITH A CONDITIONAL GET - resp = endpoint.Get(ctx.ETag); - - if (resp.StatusCode == 304) - { - // NOT MODIFIED -> RETURN STALE VALUE - return ctx.NotModified(); - } - } - else - { - // NO STALE VALUE OR NO ETAG -> NORMAL (FULL) GET - resp = endpoint.Get(); - } - - return ctx.Modified( - resp.Content.GetValueOrDefault(), - resp.ETag - ); - } - - var duration = TimeSpan.FromSeconds(1); - var endpoint = new FakeHttpEndpoint(1); - - using var cache = new FusionCache(new FusionCacheOptions()); - // TOT REQ + 1 / FULL RESP + 1 - var v1 = cache.GetOrSet<int>("foo", (ctx, _) => FakeGet(ctx, endpoint), opt => opt.SetDuration(duration).SetFailSafe(true)); - - // CACHED -> NO INCR - var v2 = cache.GetOrSet<int>("foo", (ctx, _) => FakeGet(ctx, endpoint), opt => opt.SetDuration(duration).SetFailSafe(true)); - - // LET THE CACHE EXPIRE - Thread.Sleep(duration.PlusALittleBit()); - - // TOT REQ + 1 / COND REQ + 1 / NOT MOD RESP + 1 - var v3 = cache.GetOrSet<int>("foo", (ctx, _) => FakeGet(ctx, endpoint), opt => opt.SetDuration(duration).SetFailSafe(true)); - - // LET THE CACHE EXPIRE - Thread.Sleep(duration.PlusALittleBit()); - - // TOT REQ + 1 / COND REQ + 1 / NOT MOD RESP + 1 - var v4 = cache.GetOrSet<int>("foo", (ctx, _) => FakeGet(ctx, endpoint), opt => opt.SetDuration(duration).SetFailSafe(true)); - - // SET VALUE -> CHANGE LAST MODIFIED - endpoint.SetValue(42); - - // LET THE CACHE EXPIRE - Thread.Sleep(duration.PlusALittleBit()); - - // TOT REQ + 1 / COND REQ + 1 / FULL RESP + 1 - var v5 = cache.GetOrSet<int>("foo", (ctx, _) => FakeGet(ctx, endpoint), opt => opt.SetDuration(duration).SetFailSafe(true)); - - Assert.Equal(4, endpoint.TotalRequestsCount); - Assert.Equal(3, endpoint.ConditionalRequestsCount); - Assert.Equal(2, endpoint.FullResponsesCount); - Assert.Equal(2, endpoint.NotModifiedResponsesCount); - - Assert.Equal(1, v1); - Assert.Equal(1, v2); - Assert.Equal(1, v3); - Assert.Equal(1, v4); - Assert.Equal(42, v5); - } - - [Fact] - public async Task CanHandleEagerRefreshAsync() - { - var duration = TimeSpan.FromSeconds(2); - var eagerRefreshThreshold = 0.2f; - - using var cache = new FusionCache(new FusionCacheOptions()); - - cache.DefaultEntryOptions.Duration = duration; - cache.DefaultEntryOptions.EagerRefreshThreshold = eagerRefreshThreshold; - - // EXECUTE FACTORY - var v1 = await cache.GetOrSetAsync<long>("foo", async _ => DateTimeOffset.UtcNow.Ticks); - - // USE CACHED VALUE - var v2 = await cache.GetOrSetAsync<long>("foo", async _ => DateTimeOffset.UtcNow.Ticks); - - // WAIT FOR EAGER REFRESH THRESHOLD TO BE HIT - var eagerDuration = TimeSpan.FromMilliseconds(duration.TotalMilliseconds * eagerRefreshThreshold).Add(TimeSpan.FromMilliseconds(10)); - await Task.Delay(eagerDuration); - - // EAGER REFRESH KICKS IN - var v3 = await cache.GetOrSetAsync<long>("foo", async _ => DateTimeOffset.UtcNow.Ticks); - - // WAIT FOR THE BACKGROUND FACTORY (EAGER REFRESH) TO COMPLETE - await Task.Delay(TimeSpan.FromMilliseconds(50)); - - // GET THE REFRESHED VALUE - var v4 = await cache.GetOrSetAsync<long>("foo", async _ => DateTimeOffset.UtcNow.Ticks); - - // WAIT FOR EXPIRATION - await Task.Delay(duration.PlusALittleBit()); - - // EXECUTE FACTORY AGAIN - var v5 = await cache.GetOrSetAsync<long>("foo", async _ => DateTimeOffset.UtcNow.Ticks); - - // USE CACHED VALUE - var v6 = await cache.GetOrSetAsync<long>("foo", async _ => DateTimeOffset.UtcNow.Ticks); - - Assert.Equal(v1, v2); - Assert.Equal(v2, v3); - Assert.True(v4 > v3); - Assert.True(v5 > v4); - Assert.Equal(v5, v6); - } - - [Fact] - public void CanHandleEagerRefresh() - { - var duration = TimeSpan.FromSeconds(2); - var eagerRefreshThreshold = 0.2f; - - using var cache = new FusionCache(new FusionCacheOptions()); - - cache.DefaultEntryOptions.Duration = duration; - cache.DefaultEntryOptions.EagerRefreshThreshold = eagerRefreshThreshold; - - // EXECUTE FACTORY - var v1 = cache.GetOrSet<long>("foo", _ => DateTimeOffset.UtcNow.Ticks); - - // USE CACHED VALUE - var v2 = cache.GetOrSet<long>("foo", _ => DateTimeOffset.UtcNow.Ticks); - - // WAIT FOR EAGER REFRESH THRESHOLD TO BE HIT - var eagerDuration = TimeSpan.FromMilliseconds(duration.TotalMilliseconds * eagerRefreshThreshold).Add(TimeSpan.FromMilliseconds(10)); - Thread.Sleep(eagerDuration); - - // EAGER REFRESH KICKS IN - var v3 = cache.GetOrSet<long>("foo", _ => DateTimeOffset.UtcNow.Ticks); - - // WAIT FOR THE BACKGROUND FACTORY (EAGER REFRESH) TO COMPLETE - Thread.Sleep(TimeSpan.FromMilliseconds(50)); - - // GET THE REFRESHED VALUE - var v4 = cache.GetOrSet<long>("foo", _ => DateTimeOffset.UtcNow.Ticks); - - // WAIT FOR EXPIRATION - Thread.Sleep(duration.PlusALittleBit()); - - // EXECUTE FACTORY AGAIN - var v5 = cache.GetOrSet<long>("foo", _ => DateTimeOffset.UtcNow.Ticks); - - // USE CACHED VALUE - var v6 = cache.GetOrSet<long>("foo", _ => DateTimeOffset.UtcNow.Ticks); - - Assert.Equal(v1, v2); - Assert.Equal(v2, v3); - Assert.True(v4 > v3); - Assert.True(v5 > v4); - Assert.Equal(v5, v6); - } - - [Fact] - public async Task CanHandleEagerRefreshWithInfiniteDurationAsync() - { - var duration = TimeSpan.MaxValue; - var eagerRefreshThreshold = 0.5f; - - using var cache = new FusionCache(new FusionCacheOptions()); - - cache.DefaultEntryOptions.Duration = duration; - cache.DefaultEntryOptions.EagerRefreshThreshold = eagerRefreshThreshold; - - // EXECUTE FACTORY - var v1 = await cache.GetOrSetAsync<long>("foo", async _ => DateTimeOffset.UtcNow.Ticks); - - Assert.True(v1 > 0); - } - - [Fact] - public void CanHandleEagerRefreshWithInfiniteDuration() - { - var duration = TimeSpan.MaxValue; - var eagerRefreshThreshold = 0.5f; - - using var cache = new FusionCache(new FusionCacheOptions()); - - cache.DefaultEntryOptions.Duration = duration; - cache.DefaultEntryOptions.EagerRefreshThreshold = eagerRefreshThreshold; - - // EXECUTE FACTORY - var v1 = cache.GetOrSet<long>("foo", _ => DateTimeOffset.UtcNow.Ticks); - - Assert.True(v1 > 0); - } - - [Fact] - public async Task NormalFactoryExecutionWaitsForInFlightEagerRefreshAsync() - { - var duration = TimeSpan.FromSeconds(2); - var eagerRefreshThreshold = 0.2f; - var eagerRefreshThresholdDuration = TimeSpan.FromMilliseconds(duration.TotalMilliseconds * eagerRefreshThreshold); - var simulatedDelay = TimeSpan.FromSeconds(4); - var value = 0; - - using var cache = new FusionCache(new FusionCacheOptions()); - - cache.DefaultEntryOptions.Duration = duration; - cache.DefaultEntryOptions.EagerRefreshThreshold = eagerRefreshThreshold; - - // EXECUTE FACTORY - var v1 = await cache.GetOrSetAsync<long>("foo", async _ => - { - Interlocked.Increment(ref value); - return value; - }); - - // USE CACHED VALUE - var v2 = await cache.GetOrSetAsync<long>("foo", async _ => - { - Interlocked.Increment(ref value); - return value; - }); - - // WAIT FOR EAGER REFRESH THRESHOLD TO BE HIT - await Task.Delay(eagerRefreshThresholdDuration.Add(TimeSpan.FromMilliseconds(10))); - - // EAGER REFRESH KICKS IN (WITH DELAY) - var v3 = await cache.GetOrSetAsync<long>("foo", async _ => - { - await Task.Delay(simulatedDelay); - - Interlocked.Increment(ref value); - return value; - }); - - // WAIT FOR EXPIRATION - await Task.Delay(duration.PlusALittleBit()); - - // TRY TO GET EXPIRED ENTRY: NORMALLY THIS WOULD FIRE THE FACTORY, BUT SINCE IT - // IS ALRADY RUNNING BECAUSE OF EAGER REFRESH, IT WILL WAIT FOR IT TO COMPLETE - // AND USE THE RESULT, SAVING ONE FACTORY EXECUTION - var v4 = await cache.GetOrSetAsync<long>("foo", async _ => - { - Interlocked.Increment(ref value); - return value; - }); - - // USE CACHED VALUE - var v5 = await cache.GetOrSetAsync<long>("foo", async _ => - { - Interlocked.Increment(ref value); - return value; - }); - - Assert.Equal(1, v1); - Assert.Equal(1, v2); - Assert.Equal(1, v3); - Assert.Equal(2, v4); - Assert.Equal(2, v5); - Assert.Equal(2, value); - } - - [Fact] - public void NormalFactoryExecutionWaitsForInFlightEagerRefresh() - { - var duration = TimeSpan.FromSeconds(2); - var eagerRefreshThreshold = 0.2f; - var eagerRefreshThresholdDuration = TimeSpan.FromMilliseconds(duration.TotalMilliseconds * eagerRefreshThreshold); - var simulatedDelay = TimeSpan.FromSeconds(4); - var value = 0; - - using var cache = new FusionCache(new FusionCacheOptions()); - - cache.DefaultEntryOptions.Duration = duration; - cache.DefaultEntryOptions.EagerRefreshThreshold = eagerRefreshThreshold; - - // EXECUTE FACTORY - var v1 = cache.GetOrSet<long>("foo", _ => - { - Interlocked.Increment(ref value); - return value; - }); - - // USE CACHED VALUE - var v2 = cache.GetOrSet<long>("foo", _ => - { - Interlocked.Increment(ref value); - return value; - }); - - // WAIT FOR EAGER REFRESH THRESHOLD TO BE HIT - Thread.Sleep(eagerRefreshThresholdDuration.Add(TimeSpan.FromMilliseconds(10))); - - // EAGER REFRESH KICKS IN (WITH DELAY) - var v3 = cache.GetOrSet<long>("foo", _ => - { - Thread.Sleep(simulatedDelay); - - Interlocked.Increment(ref value); - return value; - }); - - // WAIT FOR EXPIRATION - Thread.Sleep(duration.PlusALittleBit()); - - // TRY TO GET EXPIRED ENTRY: NORMALLY THIS WOULD FIRE THE FACTORY, BUT SINCE IT - // IS ALRADY RUNNING BECAUSE OF EAGER REFRESH, IT WILL WAIT FOR IT TO COMPLETE - // AND USE THE RESULT, SAVING ONE FACTORY EXECUTION - var v4 = cache.GetOrSet<long>("foo", _ => - { - Interlocked.Increment(ref value); - return value; - }); - - // USE CACHED VALUE - var v5 = cache.GetOrSet<long>("foo", _ => - { - Interlocked.Increment(ref value); - return value; - }); - - Assert.Equal(1, v1); - Assert.Equal(1, v2); - Assert.Equal(1, v3); - Assert.Equal(2, v4); - Assert.Equal(2, v5); - Assert.Equal(2, value); - } - - [Fact] - public async Task CanExpireAsync() - { - using var cache = new FusionCache(new FusionCacheOptions()); - cache.DefaultEntryOptions.IsFailSafeEnabled = true; - cache.DefaultEntryOptions.Duration = TimeSpan.FromMinutes(10); - - await cache.SetAsync<int>("foo", 42); - var maybeFoo1 = await cache.TryGetAsync<int>("foo", opt => opt.SetFailSafe(false)); - await cache.ExpireAsync("foo"); - var maybeFoo2 = await cache.TryGetAsync<int>("foo", opt => opt.SetFailSafe(false)); - var maybeFoo3 = await cache.TryGetAsync<int>("foo", opt => opt.SetFailSafe(true)); - Assert.True(maybeFoo1.HasValue); - Assert.Equal(42, maybeFoo1.Value); - Assert.False(maybeFoo2.HasValue); - Assert.True(maybeFoo3.HasValue); - Assert.Equal(42, maybeFoo3.Value); - } - - [Fact] - public void CanExpire() - { - using var cache = new FusionCache(new FusionCacheOptions()); - cache.DefaultEntryOptions.IsFailSafeEnabled = true; - cache.DefaultEntryOptions.Duration = TimeSpan.FromMinutes(10); - - cache.Set<int>("foo", 42); - var maybeFoo1 = cache.TryGet<int>("foo", opt => opt.SetFailSafe(false)); - cache.Expire("foo"); - var maybeFoo2 = cache.TryGet<int>("foo", opt => opt.SetFailSafe(false)); - var maybeFoo3 = cache.TryGet<int>("foo", opt => opt.SetFailSafe(true)); - Assert.True(maybeFoo1.HasValue); - Assert.Equal(42, maybeFoo1.Value); - Assert.False(maybeFoo2.HasValue); - Assert.True(maybeFoo3.HasValue); - Assert.Equal(42, maybeFoo3.Value); - } - - [Fact] - public async Task CanSkipMemoryCacheAsync() - { - using var cache = new FusionCache(new FusionCacheOptions()); - - await cache.SetAsync<int>("foo", 42, opt => opt.SetSkipMemoryCache()); - var maybeFoo1 = await cache.TryGetAsync<int>("foo"); - await cache.SetAsync<int>("foo", 42); - var maybeFoo2 = await cache.TryGetAsync<int>("foo", opt => opt.SetSkipMemoryCache()); - var maybeFoo3 = await cache.TryGetAsync<int>("foo"); - await cache.RemoveAsync("foo", opt => opt.SetSkipMemoryCache()); - var maybeFoo4 = await cache.TryGetAsync<int>("foo"); - await cache.RemoveAsync("foo"); - var maybeFoo5 = await cache.TryGetAsync<int>("foo"); - - await cache.GetOrSetAsync<int>("bar", 42, opt => opt.SetSkipMemoryCache()); - var maybeBar = await cache.TryGetAsync<int>("bar"); - - Assert.False(maybeFoo1.HasValue); - Assert.False(maybeFoo2.HasValue); - Assert.True(maybeFoo3.HasValue); - Assert.True(maybeFoo4.HasValue); - Assert.False(maybeFoo5.HasValue); - - Assert.False(maybeBar.HasValue); - } - - [Fact] - public void CanSkipMemoryCache() - { - using var cache = new FusionCache(new FusionCacheOptions()); - - cache.Set<int>("foo", 42, opt => opt.SetSkipMemoryCache()); - var maybeFoo1 = cache.TryGet<int>("foo"); - cache.Set<int>("foo", 42); - var maybeFoo2 = cache.TryGet<int>("foo", opt => opt.SetSkipMemoryCache()); - var maybeFoo3 = cache.TryGet<int>("foo"); - cache.Remove("foo", opt => opt.SetSkipMemoryCache()); - var maybeFoo4 = cache.TryGet<int>("foo"); - cache.Remove("foo"); - var maybeFoo5 = cache.TryGet<int>("foo"); - - cache.GetOrSet<int>("bar", 42, opt => opt.SetSkipMemoryCache()); - var maybeBar = cache.TryGet<int>("bar"); - - Assert.False(maybeFoo1.HasValue); - Assert.False(maybeFoo2.HasValue); - Assert.True(maybeFoo3.HasValue); - Assert.True(maybeFoo4.HasValue); - Assert.False(maybeFoo5.HasValue); - - Assert.False(maybeBar.HasValue); - } - - [Fact] - public async Task CanUseNullFusionCacheAsync() - { - using var cache = new NullFusionCache(new FusionCacheOptions() - { - CacheName = "SlothsAreCool42", - DefaultEntryOptions = new FusionCacheEntryOptions() - { - IsFailSafeEnabled = true, - Duration = TimeSpan.FromMinutes(123) - } - }); - - await cache.SetAsync<int>("foo", 42); - - var maybeFoo1 = await cache.TryGetAsync<int>("foo"); - - await cache.RemoveAsync("foo"); - - var maybeBar1 = await cache.TryGetAsync<int>("bar"); - - await cache.ExpireAsync("qux"); - - var qux1 = await cache.GetOrSetAsync("qux", async _ => 1); - var qux2 = await cache.GetOrSetAsync("qux", async _ => 2); - var qux3 = await cache.GetOrSetAsync("qux", async _ => 3); - var qux4 = await cache.GetOrDefaultAsync("qux", 4); - - Assert.Equal("SlothsAreCool42", cache.CacheName); - Assert.False(string.IsNullOrWhiteSpace(cache.InstanceId)); - - Assert.False(cache.HasDistributedCache); - Assert.False(cache.HasBackplane); - - Assert.True(cache.DefaultEntryOptions.IsFailSafeEnabled); - Assert.Equal(TimeSpan.FromMinutes(123), cache.DefaultEntryOptions.Duration); - - Assert.False(maybeFoo1.HasValue); - Assert.False(maybeBar1.HasValue); - - Assert.Equal(1, qux1); - Assert.Equal(2, qux2); - Assert.Equal(3, qux3); - Assert.Equal(4, qux4); - - await Assert.ThrowsAsync<UnreachableException>(async () => - { - _ = await cache.GetOrSetAsync<int>("qux", async _ => throw new UnreachableException("Sloths")); - }); - } - - [Fact] - public void CanUseNullFusionCache() - { - using var cache = new NullFusionCache(new FusionCacheOptions() - { - CacheName = "SlothsAreCool42", - DefaultEntryOptions = new FusionCacheEntryOptions() - { - IsFailSafeEnabled = true, - Duration = TimeSpan.FromMinutes(123) - } - }); - - cache.Set<int>("foo", 42); - - var maybeFoo1 = cache.TryGet<int>("foo"); - - cache.Remove("foo"); - - var maybeBar1 = cache.TryGet<int>("bar"); - - cache.Expire("qux"); - - var qux1 = cache.GetOrSet("qux", _ => 1); - var qux2 = cache.GetOrSet("qux", _ => 2); - var qux3 = cache.GetOrSet("qux", _ => 3); - var qux4 = cache.GetOrDefault("qux", 4); - - Assert.Equal("SlothsAreCool42", cache.CacheName); - Assert.False(string.IsNullOrWhiteSpace(cache.InstanceId)); - - Assert.False(cache.HasDistributedCache); - Assert.False(cache.HasBackplane); - - Assert.True(cache.DefaultEntryOptions.IsFailSafeEnabled); - Assert.Equal(TimeSpan.FromMinutes(123), cache.DefaultEntryOptions.Duration); - - Assert.False(maybeFoo1.HasValue); - Assert.False(maybeBar1.HasValue); - - Assert.Equal(1, qux1); - Assert.Equal(2, qux2); - Assert.Equal(3, qux3); - Assert.Equal(4, qux4); - - Assert.Throws<UnreachableException>(() => - { - _ = cache.GetOrSet<int>("qux", _ => throw new UnreachableException("Sloths")); - }); - } - - [Fact] - public void DuplicatEntryOptionsWorksCorrectly() - { - var original = new FusionCacheEntryOptions() - { - IsSafeForAdaptiveCaching = true, - - Duration = TimeSpan.FromSeconds(1), - LockTimeout = TimeSpan.FromSeconds(2), - Size = 123, - Priority = CacheItemPriority.High, - JitterMaxDuration = TimeSpan.FromSeconds(3), - - EagerRefreshThreshold = 0.456f, - - IsFailSafeEnabled = !FusionCacheGlobalDefaults.EntryOptionsIsFailSafeEnabled, - FailSafeMaxDuration = TimeSpan.FromSeconds(4), - FailSafeThrottleDuration = TimeSpan.FromSeconds(5), - - FactorySoftTimeout = TimeSpan.FromSeconds(6), - FactoryHardTimeout = TimeSpan.FromSeconds(7), - AllowTimedOutFactoryBackgroundCompletion = !FusionCacheGlobalDefaults.EntryOptionsAllowTimedOutFactoryBackgroundCompletion, - - DistributedCacheDuration = TimeSpan.FromSeconds(8), - DistributedCacheFailSafeMaxDuration = TimeSpan.FromSeconds(9), - DistributedCacheSoftTimeout = TimeSpan.FromSeconds(10), - DistributedCacheHardTimeout = TimeSpan.FromSeconds(11), - - ReThrowDistributedCacheExceptions = !FusionCacheGlobalDefaults.EntryOptionsReThrowDistributedCacheExceptions, - ReThrowSerializationExceptions = !FusionCacheGlobalDefaults.EntryOptionsReThrowSerializationExceptions, - - AllowBackgroundDistributedCacheOperations = !FusionCacheGlobalDefaults.EntryOptionsAllowBackgroundDistributedCacheOperations, - AllowBackgroundBackplaneOperations = !FusionCacheGlobalDefaults.EntryOptionsAllowBackgroundBackplaneOperations, - - SkipBackplaneNotifications = !FusionCacheGlobalDefaults.EntryOptionsSkipBackplaneNotifications, - - SkipDistributedCache = !FusionCacheGlobalDefaults.EntryOptionsSkipDistributedCache, - SkipDistributedCacheReadWhenStale = !FusionCacheGlobalDefaults.EntryOptionsSkipDistributedCacheReadWhenStale, - - SkipMemoryCache = !FusionCacheGlobalDefaults.EntryOptionsSkipMemoryCache - }; - - var duplicated = original.Duplicate(); - - Assert.Equal( - JsonConvert.SerializeObject(original), - JsonConvert.SerializeObject(duplicated) - ); - } - } -} diff --git a/tests/ZiggyCreatures.FusionCache.Tests/Stuff/AbstractTests.cs b/tests/ZiggyCreatures.FusionCache.Tests/Stuff/AbstractTests.cs new file mode 100644 index 00000000..320e23bc --- /dev/null +++ b/tests/ZiggyCreatures.FusionCache.Tests/Stuff/AbstractTests.cs @@ -0,0 +1,27 @@ +using Microsoft.Extensions.Logging; +using Xunit.Abstractions; + +namespace FusionCacheTests.Stuff; + +public abstract class AbstractTests +{ + protected AbstractTests(ITestOutputHelper output, string? testingCacheKeyPrefix) + { + TestOutput = output; + TestingCacheKeyPrefix = testingCacheKeyPrefix; + } + + protected ITestOutputHelper TestOutput { get; } + + protected string? TestingCacheKeyPrefix { get; } + + protected XUnitLogger<T> CreateXUnitLogger<T>(LogLevel minLevel = LogLevel.Trace) + { + return new XUnitLogger<T>(minLevel, TestOutput); + } + + protected ListLogger<T> CreateListLogger<T>(LogLevel minLogLevel) + { + return new ListLogger<T>(minLogLevel); + } +} diff --git a/tests/ZiggyCreatures.FusionCache.Tests/Stuff/ComplexType.cs b/tests/ZiggyCreatures.FusionCache.Tests/Stuff/ComplexType.cs index 8dce87dc..87981ea2 100644 --- a/tests/ZiggyCreatures.FusionCache.Tests/Stuff/ComplexType.cs +++ b/tests/ZiggyCreatures.FusionCache.Tests/Stuff/ComplexType.cs @@ -3,59 +3,58 @@ using System.Runtime.Serialization; using MemoryPack; -namespace FusionCacheTests +namespace FusionCacheTests.Stuff; + +[DataContract] +[MemoryPackable] +public partial class ComplexType : IEquatable<ComplexType?> { - [DataContract] - [MemoryPackable] - public partial class ComplexType : IEquatable<ComplexType?> + [DataMember(Name = "pi1", Order = 1)] + public int PropInt { get; set; } + [DataMember(Name = "pi2", Order = 2)] + public int? PropIntNullable { get; set; } + [DataMember(Name = "ps", Order = 3)] + public string? PropString { get; set; } + [DataMember(Name = "pb", Order = 4)] + public bool PropBool { get; set; } + + public override bool Equals(object? obj) { - [DataMember(Name = "pi1", Order = 1)] - public int PropInt { get; set; } - [DataMember(Name = "pi2", Order = 2)] - public int? PropIntNullable { get; set; } - [DataMember(Name = "ps", Order = 3)] - public string? PropString { get; set; } - [DataMember(Name = "pb", Order = 4)] - public bool PropBool { get; set; } - - public override bool Equals(object? obj) - { - return Equals(obj as ComplexType); - } + return Equals(obj as ComplexType); + } - public bool Equals(ComplexType? other) - { - return other is not null && - PropInt == other.PropInt && - PropIntNullable == other.PropIntNullable && - PropString == other.PropString && - PropBool == other.PropBool; - } - - public override int GetHashCode() - { - return HashCode.Combine(PropInt, PropIntNullable, PropString, PropBool); - } + public bool Equals(ComplexType? other) + { + return other is not null && + PropInt == other.PropInt && + PropIntNullable == other.PropIntNullable && + PropString == other.PropString && + PropBool == other.PropBool; + } - public static bool operator ==(ComplexType? left, ComplexType? right) - { - return EqualityComparer<ComplexType>.Default.Equals(left, right); - } + public override int GetHashCode() + { + return HashCode.Combine(PropInt, PropIntNullable, PropString, PropBool); + } - public static bool operator !=(ComplexType? left, ComplexType? right) - { - return !(left == right); - } + public static bool operator ==(ComplexType? left, ComplexType? right) + { + return EqualityComparer<ComplexType>.Default.Equals(left, right); + } - public static ComplexType CreateSample() + public static bool operator !=(ComplexType? left, ComplexType? right) + { + return !(left == right); + } + + public static ComplexType CreateSample() + { + return new ComplexType { - return new ComplexType - { - PropInt = 42, - PropIntNullable = null, - PropString = "sloths!", - PropBool = true - }; - } + PropInt = 42, + PropIntNullable = null, + PropString = "sloths!", + PropBool = true + }; } } diff --git a/tests/ZiggyCreatures.FusionCache.Tests/Stuff/EntryActionKind.cs b/tests/ZiggyCreatures.FusionCache.Tests/Stuff/EntryActionKind.cs new file mode 100644 index 00000000..7efc081a --- /dev/null +++ b/tests/ZiggyCreatures.FusionCache.Tests/Stuff/EntryActionKind.cs @@ -0,0 +1,15 @@ +namespace FusionCacheTests.Stuff; + +public enum EntryActionKind +{ + Miss = 0, + HitNormal = 1, + HitStale = 2, + Set = 3, + Remove = 4, + FailSafeActivate = 5, + FactoryError = 6, + FactorySuccess = 7, + BackplaneMessagePublished = 8, + BackplaneMessageReceived = 9 +} diff --git a/tests/ZiggyCreatures.FusionCache.Tests/Stuff/EntryActionsStats.cs b/tests/ZiggyCreatures.FusionCache.Tests/Stuff/EntryActionsStats.cs new file mode 100644 index 00000000..08b80595 --- /dev/null +++ b/tests/ZiggyCreatures.FusionCache.Tests/Stuff/EntryActionsStats.cs @@ -0,0 +1,22 @@ +using System; +using System.Collections.Concurrent; + +namespace FusionCacheTests.Stuff; + +public class EntryActionsStats +{ + public EntryActionsStats() + { + Data = new ConcurrentDictionary<EntryActionKind, int>(); + foreach (EntryActionKind kind in Enum.GetValues(typeof(EntryActionKind))) + { + Data[kind] = 0; + } + } + + public ConcurrentDictionary<EntryActionKind, int> Data { get; } + public void RecordAction(EntryActionKind kind) + { + Data.AddOrUpdate(kind, 1, (_, x) => x + 1); + } +} diff --git a/tests/ZiggyCreatures.FusionCache.Tests/Stuff/FakeHttpEndpoint.cs b/tests/ZiggyCreatures.FusionCache.Tests/Stuff/FakeHttpEndpoint.cs index 68182288..06ca4c7a 100644 --- a/tests/ZiggyCreatures.FusionCache.Tests/Stuff/FakeHttpEndpoint.cs +++ b/tests/ZiggyCreatures.FusionCache.Tests/Stuff/FakeHttpEndpoint.cs @@ -1,45 +1,44 @@ -namespace FusionCacheTests.Stuff +namespace FusionCacheTests.Stuff; + +internal class FakeHttpEndpoint { - internal class FakeHttpEndpoint + public FakeHttpEndpoint(int initialValue) { - public FakeHttpEndpoint(int initialValue) - { - SetValue(initialValue); - } - - private int Value { get; set; } - private string? ETag { get; set; } + SetValue(initialValue); + } - public int TotalRequestsCount { get; private set; } - public int ConditionalRequestsCount { get; private set; } - public int FullResponsesCount { get; private set; } - public int NotModifiedResponsesCount { get; private set; } + private int Value { get; set; } + private string? ETag { get; set; } - public void SetValue(int value) - { - Value = value; - ETag = Value.GetHashCode().ToString(); - } + public int TotalRequestsCount { get; private set; } + public int ConditionalRequestsCount { get; private set; } + public int FullResponsesCount { get; private set; } + public int NotModifiedResponsesCount { get; private set; } - public FakeHttpResponse Get(string? etag = null) - { - TotalRequestsCount++; + public void SetValue(int value) + { + Value = value; + ETag = Value.GetHashCode().ToString(); + } - var isRequestWithETag = string.IsNullOrWhiteSpace(etag) == false; + public FakeHttpResponse Get(string? etag = null) + { + TotalRequestsCount++; - if (isRequestWithETag) - ConditionalRequestsCount++; + var isRequestWithETag = string.IsNullOrWhiteSpace(etag) == false; - if (isRequestWithETag == false || etag != ETag) - { - // FULL RESPONSE - FullResponsesCount++; - return new FakeHttpResponse(200, Value, ETag); - } + if (isRequestWithETag) + ConditionalRequestsCount++; - // NOT MODIFIED RESPONSE - NotModifiedResponsesCount++; - return new FakeHttpResponse(304, null, ETag); + if (isRequestWithETag == false || etag != ETag) + { + // FULL RESPONSE + FullResponsesCount++; + return new FakeHttpResponse(200, Value, ETag); } + + // NOT MODIFIED RESPONSE + NotModifiedResponsesCount++; + return new FakeHttpResponse(304, null, ETag); } } diff --git a/tests/ZiggyCreatures.FusionCache.Tests/Stuff/FakeHttpResponse.cs b/tests/ZiggyCreatures.FusionCache.Tests/Stuff/FakeHttpResponse.cs index a5c2b192..be882779 100644 --- a/tests/ZiggyCreatures.FusionCache.Tests/Stuff/FakeHttpResponse.cs +++ b/tests/ZiggyCreatures.FusionCache.Tests/Stuff/FakeHttpResponse.cs @@ -1,16 +1,15 @@ -namespace FusionCacheTests.Stuff +namespace FusionCacheTests.Stuff; + +internal class FakeHttpResponse { - internal class FakeHttpResponse + public FakeHttpResponse(int statusCode, int? content, string? etag = null) { - public FakeHttpResponse(int statusCode, int? content, string? etag = null) - { - StatusCode = statusCode; - Content = content; - ETag = etag; - } - - public int StatusCode { get; set; } - public int? Content { get; set; } - public string? ETag { get; set; } + StatusCode = statusCode; + Content = content; + ETag = etag; } + + public int StatusCode { get; set; } + public int? Content { get; set; } + public string? ETag { get; set; } } diff --git a/tests/ZiggyCreatures.FusionCache.Tests/Stuff/ListLogger.cs b/tests/ZiggyCreatures.FusionCache.Tests/Stuff/ListLogger.cs index e22efc3a..1055dfc2 100644 --- a/tests/ZiggyCreatures.FusionCache.Tests/Stuff/ListLogger.cs +++ b/tests/ZiggyCreatures.FusionCache.Tests/Stuff/ListLogger.cs @@ -2,41 +2,40 @@ using System.Collections.Generic; using Microsoft.Extensions.Logging; -namespace FusionCacheTests.Stuff +namespace FusionCacheTests.Stuff; + +public class ListLogger<T> + : ILogger<T> { - internal class ListLogger<T> - : ILogger<T> + internal class Scope : IDisposable { - internal class Scope : IDisposable + public void Dispose() { - public void Dispose() - { - // EMPTY - } + // EMPTY } + } - private readonly LogLevel _minLogLevel; - public readonly List<(LogLevel LogLevel, string Message)> Items = new List<(LogLevel LogLevel, string Message)>(); + private readonly LogLevel _minLogLevel; + public readonly List<(LogLevel LogLevel, string Message)> Items = new List<(LogLevel LogLevel, string Message)>(); - public ListLogger(LogLevel minLogLevel) - { - _minLogLevel = minLogLevel; - } + public ListLogger(LogLevel minLogLevel) + { + _minLogLevel = minLogLevel; + } - public IDisposable BeginScope<TState>(TState state) - where TState : notnull - { - return new Scope(); - } + public IDisposable BeginScope<TState>(TState state) + where TState : notnull + { + return new Scope(); + } - public bool IsEnabled(LogLevel logLevel) - { - return logLevel >= _minLogLevel; - } + public bool IsEnabled(LogLevel logLevel) + { + return logLevel >= _minLogLevel; + } - public void Log<TState>(LogLevel logLevel, EventId eventId, TState state, Exception? exception, Func<TState, Exception?, string> formatter) - { - Items.Add((logLevel, formatter(state, exception))); - } + public void Log<TState>(LogLevel logLevel, EventId eventId, TState state, Exception? exception, Func<TState, Exception?, string> formatter) + { + Items.Add((logLevel, formatter(state, exception))); } } diff --git a/tests/ZiggyCreatures.FusionCache.Tests/Stuff/SimpleDisposable.cs b/tests/ZiggyCreatures.FusionCache.Tests/Stuff/SimpleDisposable.cs new file mode 100644 index 00000000..799631ce --- /dev/null +++ b/tests/ZiggyCreatures.FusionCache.Tests/Stuff/SimpleDisposable.cs @@ -0,0 +1,15 @@ +using System; + +namespace FusionCacheTests.Stuff; + +internal class SimpleDisposable + : IDisposable +{ + + public bool IsDisposed { get; private set; } + + public void Dispose() + { + IsDisposed = true; + } +} diff --git a/tests/ZiggyCreatures.FusionCache.Tests/Stuff/SimpleEventsPlugin.cs b/tests/ZiggyCreatures.FusionCache.Tests/Stuff/SimpleEventsPlugin.cs index e0edd351..092a5a89 100644 --- a/tests/ZiggyCreatures.FusionCache.Tests/Stuff/SimpleEventsPlugin.cs +++ b/tests/ZiggyCreatures.FusionCache.Tests/Stuff/SimpleEventsPlugin.cs @@ -4,46 +4,45 @@ using ZiggyCreatures.Caching.Fusion.Events; using ZiggyCreatures.Caching.Fusion.Plugins; -namespace FusionCacheTests.Stuff +namespace FusionCacheTests.Stuff; + +internal class SimpleEventsPlugin + : IFusionCachePlugin { - internal class SimpleEventsPlugin - : IFusionCachePlugin + private readonly bool _throwOnStart = false; + private int _missCount = 0; + + public SimpleEventsPlugin(bool throwOnStart = false) + { + _throwOnStart = throwOnStart; + } + + public void Start(IFusionCache cache) + { + IsStarted = true; + + if (_throwOnStart) + throw new Exception("Uooops ¯\\_(ツ)_/¯"); + + cache.Events.Miss += OnMiss; + } + + public void Stop(IFusionCache cache) + { + IsStopped = true; + cache.Events.Miss -= OnMiss; + } + + private void OnMiss(object? sender, FusionCacheEntryEventArgs e) + { + Interlocked.Increment(ref _missCount); + } + + public bool IsStarted { get; private set; } + public bool IsStopped { get; private set; } + + public int MissCount { - private readonly bool _throwOnStart = false; - private int _missCount = 0; - - public SimpleEventsPlugin(bool throwOnStart = false) - { - _throwOnStart = throwOnStart; - } - - public void Start(IFusionCache cache) - { - IsStarted = true; - - if (_throwOnStart) - throw new Exception("Uooops ¯\\_(ツ)_/¯"); - - cache.Events.Miss += OnMiss; - } - - public void Stop(IFusionCache cache) - { - IsStopped = true; - cache.Events.Miss -= OnMiss; - } - - private void OnMiss(object? sender, FusionCacheEntryEventArgs e) - { - Interlocked.Increment(ref _missCount); - } - - public bool IsStarted { get; private set; } - public bool IsStopped { get; private set; } - - public int MissCount - { - get { return _missCount; } - } + get { return _missCount; } } } diff --git a/tests/ZiggyCreatures.FusionCache.Tests/Stuff/SimplePlugin.cs b/tests/ZiggyCreatures.FusionCache.Tests/Stuff/SimplePlugin.cs index 5d49ddbf..cbdd4a44 100644 --- a/tests/ZiggyCreatures.FusionCache.Tests/Stuff/SimplePlugin.cs +++ b/tests/ZiggyCreatures.FusionCache.Tests/Stuff/SimplePlugin.cs @@ -1,27 +1,26 @@ using ZiggyCreatures.Caching.Fusion; using ZiggyCreatures.Caching.Fusion.Plugins; -namespace FusionCacheTests.Stuff +namespace FusionCacheTests.Stuff; + +internal class SimplePlugin + : IFusionCachePlugin { - internal class SimplePlugin - : IFusionCachePlugin + public SimplePlugin(string name) { - public SimplePlugin(string name) - { - Name = name; - } + Name = name; + } - public string Name { get; } - public bool IsRunning { get; private set; } + public string Name { get; } + public bool IsRunning { get; private set; } - public void Start(IFusionCache cache) - { - IsRunning = true; - } + public void Start(IFusionCache cache) + { + IsRunning = true; + } - public void Stop(IFusionCache cache) - { - IsRunning = false; - } + public void Stop(IFusionCache cache) + { + IsRunning = false; } } diff --git a/tests/ZiggyCreatures.FusionCache.Tests/Stuff/TestsUtils.cs b/tests/ZiggyCreatures.FusionCache.Tests/Stuff/TestsUtils.cs new file mode 100644 index 00000000..191ae0c0 --- /dev/null +++ b/tests/ZiggyCreatures.FusionCache.Tests/Stuff/TestsUtils.cs @@ -0,0 +1,84 @@ +using System; +using System.Collections; +using System.Collections.Generic; +using ZiggyCreatures.Caching.Fusion.Serialization; +using ZiggyCreatures.Caching.Fusion.Serialization.CysharpMemoryPack; +using ZiggyCreatures.Caching.Fusion.Serialization.NeueccMessagePack; +using ZiggyCreatures.Caching.Fusion.Serialization.NewtonsoftJson; +using ZiggyCreatures.Caching.Fusion.Serialization.ProtoBufNet; +using ZiggyCreatures.Caching.Fusion.Serialization.ServiceStackJson; +using ZiggyCreatures.Caching.Fusion.Serialization.SystemTextJson; + +namespace FusionCacheTests.Stuff; + +public enum SerializerType +{ + // JSON + NewtonsoftJson = 0, + SystemTextJson = 1, + ServiceStackJson = 2, + // MESSAGEPACK + NeueccMessagePack = 10, + // PROTOBUF + ProtoBufNet = 20, + // MEMORYPACK + CysharpMemoryPack = 30, +} + +public static class TestsUtils +{ + public static IFusionCacheSerializer GetSerializer(SerializerType serializerType) + { + switch (serializerType) + { + case SerializerType.NewtonsoftJson: + return new FusionCacheNewtonsoftJsonSerializer(); + case SerializerType.SystemTextJson: + return new FusionCacheSystemTextJsonSerializer(); + case SerializerType.ServiceStackJson: + return new FusionCacheServiceStackJsonSerializer(); + case SerializerType.NeueccMessagePack: + return new FusionCacheNeueccMessagePackSerializer(); + case SerializerType.ProtoBufNet: + return new FusionCacheProtoBufNetSerializer(); + case SerializerType.CysharpMemoryPack: + return new FusionCacheCysharpMemoryPackSerializer(); + default: + throw new ArgumentException("Invalid serializer specified", nameof(serializerType)); + } + } + + public static string MaybePreProcessCacheKey(string key, string? prefix) + { + if (prefix is null) + return key; + + return prefix + key; + } + + public static TimeSpan PlusALittleBit(this TimeSpan ts) + { + return ts + TimeSpan.FromMilliseconds(250); + } + + public static TimeSpan PlusASecond(this TimeSpan ts) + { + return ts + TimeSpan.FromSeconds(1); + } +} + +public class SerializerTypesClassData : IEnumerable<object[]> +{ + public IEnumerator<object[]> GetEnumerator() + { + foreach (var x in Enum.GetValues<SerializerType>()) + { + yield return new object[] { x }; + } + } + + IEnumerator IEnumerable.GetEnumerator() + { + return GetEnumerator(); + } +} diff --git a/tests/ZiggyCreatures.FusionCache.Tests/Stuff/XUnitLogger.cs b/tests/ZiggyCreatures.FusionCache.Tests/Stuff/XUnitLogger.cs index cf5f4a4b..5f11f59f 100644 --- a/tests/ZiggyCreatures.FusionCache.Tests/Stuff/XUnitLogger.cs +++ b/tests/ZiggyCreatures.FusionCache.Tests/Stuff/XUnitLogger.cs @@ -1,55 +1,55 @@ using System; +using System.Globalization; using Microsoft.Extensions.Logging; using Xunit.Abstractions; -namespace FusionCacheTests.Stuff +namespace FusionCacheTests.Stuff; + +public class XUnitLogger<T> + : ILogger<T> { - internal class XUnitLogger<T> - : ILogger<T> + internal class Scope : IDisposable { - internal class Scope : IDisposable + public void Dispose() { - public void Dispose() - { - // EMPTY - } + // EMPTY } + } - private readonly ITestOutputHelper _helper; - private readonly LogLevel _minLogLevel; + private readonly ITestOutputHelper _helper; + private readonly LogLevel _minLogLevel; - public XUnitLogger(LogLevel minLogLevel, ITestOutputHelper helper) - { - _minLogLevel = minLogLevel; - _helper = helper; - } + public XUnitLogger(LogLevel minLogLevel, ITestOutputHelper helper) + { + _minLogLevel = minLogLevel; + _helper = helper; + } - public IDisposable BeginScope<TState>(TState state) - where TState : notnull - { - return new Scope(); - } + public IDisposable BeginScope<TState>(TState state) + where TState : notnull + { + return new Scope(); + } - public bool IsEnabled(LogLevel logLevel) - { - return logLevel >= _minLogLevel; - } + public bool IsEnabled(LogLevel logLevel) + { + return logLevel >= _minLogLevel; + } - public void Log<TState>(LogLevel logLevel, EventId eventId, TState state, Exception? exception, Func<TState, Exception?, string> formatter) + public void Log<TState>(LogLevel logLevel, EventId eventId, TState state, Exception? exception, Func<TState, Exception?, string> formatter) + { + if (IsEnabled(logLevel)) { - if (IsEnabled(logLevel)) - { - _helper.WriteLine( - (logLevel >= LogLevel.Warning ? Environment.NewLine : "") - + $"{logLevel.ToString().ToUpper()} {DateTime.UtcNow}: " - + formatter(state, exception) - + (exception is null - ? "" - : (Environment.NewLine + exception.ToString() + Environment.NewLine) - ) - + (logLevel >= LogLevel.Warning ? Environment.NewLine : "") - ); - } + _helper.WriteLine( + (logLevel >= LogLevel.Warning ? Environment.NewLine : "") + + $"{logLevel.ToString().Substring(0, 4).ToUpper()} {DateTime.UtcNow.ToString("yyyy-MM-dd'T'HH:mm:ss.fffK", CultureInfo.InvariantCulture)}: " + + formatter(state, exception) + + (exception is null + ? "" + : (Environment.NewLine + exception.ToString() + Environment.NewLine) + ) + + (logLevel >= LogLevel.Warning ? Environment.NewLine : "") + ); } } } diff --git a/tests/ZiggyCreatures.FusionCache.Tests/TestsExtMethods.cs b/tests/ZiggyCreatures.FusionCache.Tests/TestsExtMethods.cs deleted file mode 100644 index a165363a..00000000 --- a/tests/ZiggyCreatures.FusionCache.Tests/TestsExtMethods.cs +++ /dev/null @@ -1,12 +0,0 @@ -using System; - -namespace FusionCacheTests -{ - static class TestsExtMethods - { - public static TimeSpan PlusALittleBit(this TimeSpan ts) - { - return ts + TimeSpan.FromMilliseconds(100); - } - } -} diff --git a/tests/ZiggyCreatures.FusionCache.Tests/TestsUtils.cs b/tests/ZiggyCreatures.FusionCache.Tests/TestsUtils.cs deleted file mode 100644 index 2d0611b8..00000000 --- a/tests/ZiggyCreatures.FusionCache.Tests/TestsUtils.cs +++ /dev/null @@ -1,75 +0,0 @@ -using System; -using System.Collections; -using System.Collections.Generic; -using ZiggyCreatures.Caching.Fusion.Serialization; -using ZiggyCreatures.Caching.Fusion.Serialization.CysharpMemoryPack; -using ZiggyCreatures.Caching.Fusion.Serialization.NeueccMessagePack; -using ZiggyCreatures.Caching.Fusion.Serialization.NewtonsoftJson; -using ZiggyCreatures.Caching.Fusion.Serialization.ProtoBufNet; -using ZiggyCreatures.Caching.Fusion.Serialization.ServiceStackJson; -using ZiggyCreatures.Caching.Fusion.Serialization.SystemTextJson; - -namespace FusionCacheTests -{ - public enum SerializerType - { - // JSON - NewtonsoftJson = 0, - SystemTextJson = 1, - ServiceStackJson = 2, - // MESSAGEPACK - NeueccMessagePack = 10, - // PROTOBUF - ProtoBufNet = 20, - // MEMORYPACK - CysharpMemoryPack = 30, - } - - public static class TestsUtils - { - public static IFusionCacheSerializer GetSerializer(SerializerType serializerType) - { - switch (serializerType) - { - case SerializerType.NewtonsoftJson: - return new FusionCacheNewtonsoftJsonSerializer(); - case SerializerType.SystemTextJson: - return new FusionCacheSystemTextJsonSerializer(); - case SerializerType.ServiceStackJson: - return new FusionCacheServiceStackJsonSerializer(); - case SerializerType.NeueccMessagePack: - return new FusionCacheNeueccMessagePackSerializer(); - case SerializerType.ProtoBufNet: - return new FusionCacheProtoBufNetSerializer(); - case SerializerType.CysharpMemoryPack: - return new FusionCacheCysharpMemoryPackSerializer(); - default: - throw new ArgumentException("Invalid serializer specified", nameof(serializerType)); - } - } - - public static string MaybePreProcessCacheKey(string key, string? prefix) - { - if (prefix is null) - return key; - - return prefix + key; - } - } - - public class SerializerTypesClassData : IEnumerable<object[]> - { - public IEnumerator<object[]> GetEnumerator() - { - foreach (var x in Enum.GetValues<SerializerType>()) - { - yield return new object[] { x }; - } - } - - IEnumerator IEnumerable.GetEnumerator() - { - return GetEnumerator(); - } - } -} diff --git a/tests/ZiggyCreatures.FusionCache.Tests/ZiggyCreatures.FusionCache.Tests.csproj b/tests/ZiggyCreatures.FusionCache.Tests/ZiggyCreatures.FusionCache.Tests.csproj index 5a443e18..973dc37a 100644 --- a/tests/ZiggyCreatures.FusionCache.Tests/ZiggyCreatures.FusionCache.Tests.csproj +++ b/tests/ZiggyCreatures.FusionCache.Tests/ZiggyCreatures.FusionCache.Tests.csproj @@ -15,14 +15,14 @@ </ItemGroup> <ItemGroup> - <PackageReference Include="Microsoft.Extensions.Caching.StackExchangeRedis" Version="7.0.9" /> - <PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.6.3" /> + <PackageReference Include="Microsoft.Extensions.Caching.StackExchangeRedis" Version="7.0.13" /> + <PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.8.0" /> <PackageReference Include="CacheManager.Microsoft.Extensions.Caching.Memory" Version="1.2.0" /> - <PackageReference Include="CacheTower" Version="0.13.0" /> - <PackageReference Include="EasyCaching.InMemory" Version="1.9.0" /> + <PackageReference Include="CacheTower" Version="0.14.0" /> + <PackageReference Include="EasyCaching.InMemory" Version="1.9.2" /> <PackageReference Include="LazyCache" Version="2.4.0" /> - <PackageReference Include="xunit" Version="2.5.0" /> - <PackageReference Include="xunit.runner.visualstudio" Version="2.5.0"> + <PackageReference Include="xunit" Version="2.6.1" /> + <PackageReference Include="xunit.runner.visualstudio" Version="2.5.3"> <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets> <PrivateAssets>all</PrivateAssets> </PackageReference>