6 |
7 | ## 🐛 I found a bug!
8 |
9 | Great! You can either open a [**bug report**](#bug-report) or submit a [**fix**](#fix-bug) for it.
10 | Both are useful options for maintaining this project - choose whichever you feel comfortable doing.
11 |
12 |
13 |
14 | ### ✍ I'll report the bug!
15 |
16 | Detailed issues are very helpful for tracking down the source of the problem.
17 | We don't want double-ups of issues so make sure you check that there isn't [an existing issue already open](https://github.com/TurnerSoftware/CacheTower/issues)!
18 |
19 | If there are no existing issues match the bug you've found, you'll need to write out a new one.
20 | The more useful the information you provide, the faster the problem can be solved.
21 |
22 | Ideally you would want to include:
23 |
24 | - NuGet package version
25 | - .NET runtime version
26 | - Steps to reproduce the issue
27 | - A _minimal_, reproducible example
28 | - Operating system (Windows/Linux/Mac)
29 |
30 | If you think you're ready, [**submit your bug report**](https://github.com/TurnerSoftware/CacheTower/issues/new?labels=bug&template=BUG_REPORT.md)!
31 |
32 |
33 |
34 | ### 💻 I'll fix the bug!
35 |
36 | That's great to hear! Here are some tips for a helpful bug fix:
37 |
38 | - It is a good idea to add a test that triggers this specific bug so we can confirm the fix works (also helpful for preventing regressions later on)
39 | - When developing a fix, make sure you follow the coding styles you see within the repository otherwise you might have to redo the changes!
40 | - If there is an issue open for the bug, make sure to tag that issue in your PR description
41 |
42 | You probably want to run the tests locally too.
43 | Cache Tower has [some requirements for local testing](#requirements-for-local-testing) which may affect your ability to run the full test suite.
44 |
45 | Got that bug fixed? [**Submit your pull request**](https://github.com/TurnerSoftware/CacheTower/compare)!
46 |
47 |
48 |
49 | ## 💡 I've got an idea for a new feature or change!
50 |
51 | Features are great! Is yours a [**small feature**](#idea-small) or a [**big feature**](#idea-big)?
52 | Both are welcome though smaller features are likely to be handled quicker than bigger features.
53 |
54 |
55 |
56 | ### 🤏 It's a small feature
57 |
58 | Small features usually don't take much time on the maintainer's side and not a lot of time on your side.
59 | Do you want to [**suggest the feature**](#idea-small-suggestion) or try a hand at [**implementing the feature**](#idea-small-implementation)?
60 |
61 |
62 |
63 | #### ✍ I'll suggest the small feature
64 |
65 | Nothing wrong with suggesting a feature!
66 | Keep in mind though that there is only so many hours in the day - even small features may take a while before they are reviewed.
67 |
68 | Sometimes features just aren't meant to be and won't get implemented.
69 | It isn't a personal statement if your feature isn't implemented - it might simply not fit in with "the vision" of the project or even conflict with planned changes.
70 |
71 | Here are some tips for a good small feature suggestion:
72 |
73 | - Describe what problem the feature is solving
74 | - Show an example of how you might use/interact with the feature
75 |
76 | If you still want to go ahead, [**submit your feature request**](https://github.com/TurnerSoftware/CacheTower/issues/new?labels=enhancement&template=FEATURE_REQUEST.md)!
77 |
78 |
79 |
80 | #### 💻 I'll implement the small feature
81 |
82 | Nice! Here are some tips for a useful feature implementation:
83 |
84 | - Features are only as good as the documentation around them. Make sure the documentation is updated appropriately.
85 | - Please add tests! It doesn't need to be perfect code coverage but the bulk behaviour of the change should be tested.
86 | - Keep to the coding styles you see within the repository otherwise you might have to redo the changes!
87 | - If there is an issue open for the feature, make sure to tag that issue in your PR description
88 |
89 | You probably want to run the tests locally too.
90 | Cache Tower has [some requirements for local testing](#requirements-for-local-testing) which may affect your ability to run the full test suite.
91 |
92 | Finished your implementation? [**Submit your pull request**](https://github.com/TurnerSoftware/CacheTower/compare)!
93 |
94 |
95 |
96 | ### 🙌 It's a big feature!
97 |
98 | Big features can either be awesome for a project or a burden to it.
99 | It is **highly** recommended to raise an issue about a big feature rather than open a pull request for it.
100 |
101 | Big features have to be carefully considered - whether they fit in with "the vision" of the project or potentially conflict with planned changes.
102 | Architectural decisions about the implementation may also need to be discussed to avoid breaking changes or performance penalities.
103 |
104 | With these things in mind, a big feature could be pending for months or may _never_ be implemented.
105 |
106 | Here are some tips for a good big feature suggestion:
107 |
108 | - Describe what problem the feature is solving
109 | - Show an example of how you might use/interact with the feature
110 |
111 | If you still want to go ahead, [**submit your feature request**](https://github.com/TurnerSoftware/CacheTower/issues/new?labels=enhancement&template=FEATURE_REQUEST.md)!
112 |
113 |
114 |
115 | ## 🙋 I just want to help out!
116 |
117 | Helpers are always welcome! Feel free to [**triage any open issues**](https://github.com/TurnerSoftware/CacheTower/issues) or make sure the documentation is up-to-date!
118 |
119 | If you want to do some coding, you could [**implement any open small suggested features**](#idea-small-implementation) which can bring the idea to reality.
120 |
121 |
122 |
123 |
124 |
125 |
126 |
127 |
128 | ## Miscellaneous
129 |
130 | ### Requirements for Local Testing
131 |
132 | Cache Tower uses external services to perform integration testing.
133 | To run all of the tests, you will need both Redis (or [compatible software](https://www.memurai.com/)) and MongoDB installed.
134 |
135 | For Redis, it needs to be at least version 5 compatible.
136 | The tests use the default connection of `localhost:6379` but can be overriden by environment variable `REDIS_ENDPOINT`.
137 |
138 | For MongoDB, it needs to be at least version 3.
139 | The tests use the default connection string of `mongodb://localhost` but can be overridden by environment variable `MONGODB_URI`.
140 |
141 | Back to [**fixing a bug**](#bug-fix) or [**implementing a small feature**](#idea-small-implementation).
142 |
143 |
144 |
--------------------------------------------------------------------------------
/CodeCoverage.runsettings:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 | cobertura
8 | [CacheTower.Tests]*,[CacheTower.Benchmarks]*,[CacheTower.AlternativesBenchmark]*
9 | [CacheTower]*,[CacheTower.*]*
10 | Obsolete
11 | true
12 | true
13 |
14 |
15 |
16 |
17 |
--------------------------------------------------------------------------------
/License.txt:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2019 Turner Software
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/benchmarks/CacheTower.AlternativesBenchmark/BaseBenchmark.cs:
--------------------------------------------------------------------------------
1 | using BenchmarkDotNet.Attributes;
2 | using BenchmarkDotNet.Columns;
3 | using BenchmarkDotNet.Configs;
4 | using BenchmarkDotNet.Diagnosers;
5 | using BenchmarkDotNet.Environments;
6 | using BenchmarkDotNet.Jobs;
7 | using BenchmarkDotNet.Loggers;
8 | using BenchmarkDotNet.Order;
9 | using BenchmarkDotNet.Validators;
10 |
11 | namespace CacheTower.AlternativesBenchmark;
12 |
13 | [Config(typeof(Config))]
14 | public abstract class BaseBenchmark
15 | {
16 | public class Config : ManualConfig
17 | {
18 | public Config()
19 | {
20 | AddLogger(ConsoleLogger.Default);
21 |
22 | AddDiagnoser(MemoryDiagnoser.Default);
23 | AddColumn(StatisticColumn.OperationsPerSecond);
24 | AddColumnProvider(DefaultColumnProviders.Instance);
25 |
26 | WithOrderer(new DefaultOrderer(SummaryOrderPolicy.FastestToSlowest));
27 |
28 | AddValidator(JitOptimizationsValidator.FailOnError);
29 |
30 | AddJob(Job.Default
31 | .WithRuntime(CoreRuntime.Core60)
32 | .WithMaxIterationCount(200));
33 | }
34 | }
35 | }
36 |
--------------------------------------------------------------------------------
/benchmarks/CacheTower.AlternativesBenchmark/CacheAlternatives_File_Benchmark.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.IO;
3 | using System.Threading;
4 | using System.Threading.Tasks;
5 | using BenchmarkDotNet.Attributes;
6 | using CacheTower.Providers.FileSystem;
7 | using CacheTower.Serializers.NewtonsoftJson;
8 | using CacheTower.Serializers.Protobuf;
9 | using CacheTower.Serializers.SystemTextJson;
10 | using EasyCaching.Disk;
11 | using Microsoft.Extensions.Logging;
12 |
13 | namespace CacheTower.AlternativesBenchmark
14 | {
15 | public class CacheAlternatives_File_Benchmark : BaseBenchmark
16 | {
17 | private const string DirectoryPath = "CacheAlternatives/FileCache";
18 |
19 | private readonly CacheStack CacheTowerNewtonsoftJson;
20 | private readonly CacheStack CacheTowerSystemTextJson;
21 | private readonly CacheStack CacheTowerProtobuf;
22 | private DefaultDiskCachingProvider EasyCaching;
23 |
24 | public CacheAlternatives_File_Benchmark()
25 | {
26 | CacheTowerNewtonsoftJson = new CacheStack(null, new(new[] { new FileCacheLayer(new(DirectoryPath, NewtonsoftJsonCacheSerializer.Instance)) }));
27 | CacheTowerSystemTextJson = new CacheStack(null, new(new[] { new FileCacheLayer(new(DirectoryPath, SystemTextJsonCacheSerializer.Instance)) }));
28 | CacheTowerProtobuf = new CacheStack(null, new(new[] { new FileCacheLayer(new(DirectoryPath, ProtobufCacheSerializer.Instance)) }));
29 | }
30 |
31 | private static void CleanupFileSystem()
32 | {
33 | var attempts = 0;
34 | while (attempts < 5)
35 | {
36 | try
37 | {
38 | if (Directory.Exists(DirectoryPath))
39 | {
40 | Directory.Delete(DirectoryPath, true);
41 | }
42 |
43 | break;
44 | }
45 | catch
46 | {
47 | Thread.Sleep(200);
48 | }
49 | attempts++;
50 | }
51 | }
52 |
53 | [GlobalSetup]
54 | public void Setup()
55 | {
56 | CleanupFileSystem();
57 |
58 | // Easy Caching seems to generate a folder structure at initialization - this is required to be established for benchmarking.
59 | EasyCaching = new DefaultDiskCachingProvider("EasyCaching", new[] { new EasyCaching.Serialization.Protobuf.DefaultProtobufSerializer("EasyCaching") }, new DiskOptions
60 | {
61 | DBConfig = new DiskDbOptions
62 | {
63 | BasePath = DirectoryPath
64 | }
65 | }, (ILoggerFactory)null);
66 | }
67 |
68 | [Benchmark(Baseline = true)]
69 | public async Task CacheTower_FileCacheLayer_NewtonsoftJson()
70 | {
71 | return await CacheTowerNewtonsoftJson.GetOrSetAsync("GetOrSet_TestKey", (old) =>
72 | {
73 | return Task.FromResult("Hello World");
74 | }, new CacheSettings(TimeSpan.FromDays(1), TimeSpan.FromDays(1)));
75 | }
76 |
77 | [Benchmark]
78 | public async Task CacheTower_FileCacheLayer_SystemTextJson()
79 | {
80 | return await CacheTowerSystemTextJson.GetOrSetAsync("GetOrSet_TestKey", (old) =>
81 | {
82 | return Task.FromResult("Hello World");
83 | }, new CacheSettings(TimeSpan.FromDays(1), TimeSpan.FromDays(1)));
84 | }
85 |
86 | [Benchmark]
87 | public async Task CacheTower_FileCacheLayer_Protobuf()
88 | {
89 | return await CacheTowerProtobuf.GetOrSetAsync("GetOrSet_TestKey", (old) =>
90 | {
91 | return Task.FromResult("Hello World");
92 | }, new CacheSettings(TimeSpan.FromDays(1), TimeSpan.FromDays(1)));
93 | }
94 |
95 | [Benchmark]
96 | public async Task EasyCaching_Disk()
97 | {
98 | return (await EasyCaching.GetAsync("GetOrSet_TestKey", () => Task.FromResult("Hello World"), TimeSpan.FromDays(1))).Value;
99 | }
100 | }
101 | }
102 |
--------------------------------------------------------------------------------
/benchmarks/CacheTower.AlternativesBenchmark/CacheAlternatives_Memory_Benchmark.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Threading.Tasks;
3 | using BenchmarkDotNet.Attributes;
4 | using CacheManager.Core;
5 | using CacheTower.Providers.Memory;
6 | using EasyCaching.InMemory;
7 | using LazyCache;
8 | using Microsoft.Extensions.Logging;
9 | using ZiggyCreatures.Caching.Fusion;
10 |
11 | namespace CacheTower.AlternativesBenchmark
12 | {
13 | public class CacheAlternatives_Memory_Benchmark : BaseBenchmark
14 | {
15 | private readonly CacheStack CacheTower;
16 | private readonly ICacheManager CacheManager;
17 | private readonly DefaultInMemoryCachingProvider EasyCaching;
18 | private readonly CachingService LazyCache;
19 | private readonly FusionCache FusionCache;
20 | private readonly IntelligentHack.IntelligentCache.MemoryCache IntelligentCache;
21 |
22 | public CacheAlternatives_Memory_Benchmark()
23 | {
24 | CacheTower = new CacheStack(null, new(new[] { new MemoryCacheLayer() }));
25 | CacheManager = CacheFactory.Build(b =>
26 | {
27 | b.WithMicrosoftMemoryCacheHandle();
28 | });
29 | EasyCaching = new DefaultInMemoryCachingProvider(
30 | "EasyCaching",
31 | new[] { new InMemoryCaching("EasyCaching", new InMemoryCachingOptions()) },
32 | new InMemoryOptions(),
33 | (ILoggerFactory)null
34 | );
35 | LazyCache = new CachingService();
36 | FusionCache = new FusionCache(new FusionCacheOptions());
37 | IntelligentCache = new IntelligentHack.IntelligentCache.MemoryCache("IntelligentCache");
38 | }
39 |
40 | [Benchmark(Baseline = true)]
41 | public async Task CacheTower_MemoryCacheLayer()
42 | {
43 | return await CacheTower.GetOrSetAsync("GetOrSet_TestKey", (old) =>
44 | {
45 | return Task.FromResult("Hello World");
46 | }, new CacheSettings(TimeSpan.FromDays(1), TimeSpan.FromDays(1)));
47 | }
48 |
49 | [Benchmark]
50 | public string CacheManager_MicrosoftMemoryCache()
51 | {
52 | return CacheManager.GetOrAdd("GetOrSet_TestKey", (key) =>
53 | {
54 | return new CacheItem(key, "Hello World");
55 | }).Value;
56 | }
57 |
58 | [Benchmark]
59 | public string EasyCaching_InMemory()
60 | {
61 | return EasyCaching.Get("GetOrSet_TestKey", () => "Hello World", TimeSpan.FromDays(1)).Value;
62 | }
63 |
64 | [Benchmark]
65 | public string LazyCache_MemoryProvider()
66 | {
67 | return LazyCache.GetOrAdd("GetOrSet_TestKey", () => "Hello World", TimeSpan.FromDays(1));
68 | }
69 |
70 | [Benchmark]
71 | public string FusionCache_MemoryProvider()
72 | {
73 | return FusionCache.GetOrSet("GetOrSet_TestKey", (cancellationToken) => "Hello World", TimeSpan.FromDays(1));
74 | }
75 |
76 | [Benchmark]
77 | public string IntelligentCache_MemoryCache()
78 | {
79 | return IntelligentCache.GetSet("GetOrSet_TestKey", () => "Hello World", TimeSpan.FromDays(1));
80 | }
81 | }
82 | }
83 |
--------------------------------------------------------------------------------
/benchmarks/CacheTower.AlternativesBenchmark/CacheAlternatives_Memory_Parallel_Benchmark.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Threading.Tasks;
3 | using BenchmarkDotNet.Attributes;
4 | using CacheManager.Core;
5 | using CacheTower.Providers.Memory;
6 | using EasyCaching.InMemory;
7 | using LazyCache;
8 | using Microsoft.Extensions.Logging;
9 | using ZiggyCreatures.Caching.Fusion;
10 |
11 | namespace CacheTower.AlternativesBenchmark
12 | {
13 | public class CacheAlternatives_Memory_Parallel_Benchmark : BaseBenchmark
14 | {
15 | private readonly int ParallelIterations = 1000;
16 |
17 | private readonly CacheStack CacheTower;
18 | private readonly ICacheManager CacheManager;
19 | private readonly DefaultInMemoryCachingProvider EasyCaching;
20 | private readonly CachingService LazyCache;
21 | private readonly FusionCache FusionCache;
22 | private readonly IntelligentHack.IntelligentCache.MemoryCache IntelligentCache;
23 |
24 | public CacheAlternatives_Memory_Parallel_Benchmark()
25 | {
26 | CacheTower = new CacheStack(null, new(new[] { new MemoryCacheLayer() }));
27 | CacheManager = CacheFactory.Build(b =>
28 | {
29 | b.WithMicrosoftMemoryCacheHandle();
30 | });
31 | EasyCaching = new DefaultInMemoryCachingProvider(
32 | "EasyCaching",
33 | new[] { new InMemoryCaching("EasyCaching", new InMemoryCachingOptions()) },
34 | new InMemoryOptions(),
35 | (ILoggerFactory)null
36 | );
37 | LazyCache = new CachingService();
38 | FusionCache = new FusionCache(new FusionCacheOptions());
39 | IntelligentCache = new IntelligentHack.IntelligentCache.MemoryCache("IntelligentCache");
40 | }
41 |
42 | [Benchmark(Baseline = true)]
43 | public void CacheTower_MemoryCacheLayer()
44 | {
45 | Parallel.For(0, ParallelIterations, async i =>
46 | {
47 | await CacheTower.GetOrSetAsync("GetOrSet_TestKey", (old) =>
48 | {
49 | return Task.FromResult("Hello World");
50 | }, new CacheSettings(TimeSpan.FromDays(1), TimeSpan.FromDays(1)));
51 | });
52 | }
53 |
54 | [Benchmark]
55 | public void CacheManager_MicrosoftMemoryCache()
56 | {
57 | Parallel.For(0, ParallelIterations, i =>
58 | {
59 | var _ = CacheManager.GetOrAdd("GetOrSet_TestKey", (key) =>
60 | {
61 | return new CacheItem(key, "Hello World");
62 | }).Value;
63 | });
64 | }
65 |
66 | [Benchmark]
67 | public void EasyCaching_InMemory()
68 | {
69 | Parallel.For(0, ParallelIterations, i =>
70 | {
71 | _ = EasyCaching.Get("GetOrSet_TestKey", () => "Hello World", TimeSpan.FromDays(1)).Value;
72 | });
73 | }
74 |
75 | [Benchmark]
76 | public void LazyCache_MemoryProvider()
77 | {
78 | Parallel.For(0, ParallelIterations, i =>
79 | {
80 | LazyCache.GetOrAdd("GetOrSet_TestKey", () => "Hello World", TimeSpan.FromDays(1));
81 | });
82 | }
83 |
84 | [Benchmark]
85 | public void FusionCache_MemoryProvider()
86 | {
87 | Parallel.For(0, ParallelIterations, i =>
88 | {
89 | FusionCache.GetOrSet("GetOrSet_TestKey", (cancellationToken) => "Hello World", TimeSpan.FromDays(1));
90 | });
91 | }
92 |
93 | [Benchmark]
94 | public void IntelligentCache_MemoryCache()
95 | {
96 | Parallel.For(0, ParallelIterations, i =>
97 | {
98 | IntelligentCache.GetSet("GetOrSet_TestKey", () => "Hello World", TimeSpan.FromDays(1));
99 | });
100 | }
101 | }
102 | }
103 |
--------------------------------------------------------------------------------
/benchmarks/CacheTower.AlternativesBenchmark/CacheAlternatives_Redis_Benchmark.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Threading.Tasks;
3 | using BenchmarkDotNet.Attributes;
4 | using CacheManager.Core;
5 | using CacheTower.AlternativesBenchmark.Utils;
6 | using CacheTower.Providers.Redis;
7 | using CacheTower.Serializers.Protobuf;
8 | using EasyCaching.Redis;
9 | using EasyCaching.Serialization.Protobuf;
10 | using Microsoft.Extensions.Logging;
11 | using ProtoBuf;
12 |
13 | namespace CacheTower.AlternativesBenchmark
14 | {
15 | public class CacheAlternatives_Redis_Benchmark : BaseBenchmark
16 | {
17 | private readonly CacheStack CacheTower;
18 | private readonly ICacheManager CacheManager;
19 | private readonly DefaultRedisCachingProvider EasyCaching;
20 | private readonly IntelligentHack.IntelligentCache.RedisCache IntelligentCache;
21 |
22 | public CacheAlternatives_Redis_Benchmark()
23 | {
24 | CacheTower = new CacheStack(null, new(new[] { new RedisCacheLayer(RedisHelper.GetConnection(), new RedisCacheLayerOptions(ProtobufCacheSerializer.Instance)) }));
25 | CacheManager = CacheFactory.Build(b =>
26 | {
27 | b.WithRedisConfiguration("redisLocal", "localhost:6379,ssl=false");
28 | b.WithRedisCacheHandle("redisLocal", true);
29 | b.WithProtoBufSerializer();
30 | });
31 |
32 | var easyCachingRedisOptions = new RedisOptions
33 | {
34 | DBConfig = new RedisDBOptions
35 | {
36 | Configuration = "localhost:6379,ssl=false"
37 | }
38 | };
39 | EasyCaching = new DefaultRedisCachingProvider("EasyCaching",
40 | new[] { new RedisDatabaseProvider("EasyCaching", easyCachingRedisOptions) },
41 | new[] { new DefaultProtobufSerializer("EasyCaching") },
42 | easyCachingRedisOptions,
43 | (ILoggerFactory)null
44 | );
45 | IntelligentCache = new IntelligentHack.IntelligentCache.RedisCache(RedisHelper.GetConnection(), string.Empty);
46 | }
47 |
48 | [GlobalSetup]
49 | public void Setup()
50 | {
51 | RedisHelper.FlushDatabase();
52 | }
53 |
54 | [Benchmark(Baseline = true)]
55 | public async Task CacheTower_RedisCacheLayer()
56 | {
57 | return await CacheTower.GetOrSetAsync("GetOrSet_TestKey", (old) =>
58 | {
59 | return Task.FromResult("Hello World");
60 | }, new CacheSettings(TimeSpan.FromDays(1), TimeSpan.FromDays(1)));
61 | }
62 |
63 | [Serializable]
64 | [ProtoContract]
65 | public class ProtobufCacheItem
66 | {
67 | [ProtoMember(1)]
68 | public string Value { get; set; }
69 | }
70 |
71 | [Benchmark]
72 | public string CacheManager_Redis()
73 | {
74 | return CacheManager.GetOrAdd("GetOrSet_TestKey", (key) =>
75 | {
76 | return new ProtobufCacheItem
77 | {
78 | Value = "Hello World"
79 | };
80 | }).Value;
81 | }
82 |
83 | [Benchmark]
84 | public async Task EasyCaching_Redis()
85 | {
86 | return (await EasyCaching.GetAsync("GetOrSet_TestKey", () => Task.FromResult("Hello World"), TimeSpan.FromDays(1))).Value;
87 | }
88 |
89 | [Benchmark]
90 | public async Task IntelligentCache_Redis()
91 | {
92 | return await IntelligentCache.GetSetAsync("GetOrSet_TestKey", (cancellationToken) => Task.FromResult("Hello World"), TimeSpan.FromDays(1));
93 | }
94 | }
95 | }
96 |
--------------------------------------------------------------------------------
/benchmarks/CacheTower.AlternativesBenchmark/CacheAlternatives_Redis_Parallel_Benchmark.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Threading;
3 | using System.Threading.Tasks;
4 | using BenchmarkDotNet.Attributes;
5 | using CacheManager.Core;
6 | using CacheTower.AlternativesBenchmark.Utils;
7 | using CacheTower.Providers.Redis;
8 | using CacheTower.Serializers.Protobuf;
9 | using EasyCaching.Redis;
10 | using EasyCaching.Serialization.Protobuf;
11 | using Microsoft.Extensions.Logging;
12 | using ProtoBuf;
13 |
14 | namespace CacheTower.AlternativesBenchmark
15 | {
16 | public class CacheAlternatives_Redis_Parallel_Benchmark : BaseBenchmark
17 | {
18 | private readonly int ParallelIterations = 100;
19 |
20 | private readonly CacheStack CacheTower;
21 | private readonly ICacheManager CacheManager;
22 | private readonly DefaultRedisCachingProvider EasyCaching;
23 | private readonly IntelligentHack.IntelligentCache.RedisCache IntelligentCache;
24 |
25 | public CacheAlternatives_Redis_Parallel_Benchmark()
26 | {
27 | CacheTower = new CacheStack(null, new(new[] { new RedisCacheLayer(RedisHelper.GetConnection(), new RedisCacheLayerOptions(ProtobufCacheSerializer.Instance)) }));
28 | CacheManager = CacheFactory.Build(b =>
29 | {
30 | b.WithRedisConfiguration("redisLocal", "localhost:6379,ssl=false");
31 | b.WithRedisCacheHandle("redisLocal", true);
32 | b.WithProtoBufSerializer();
33 | });
34 |
35 | var easyCachingRedisOptions = new RedisOptions
36 | {
37 | DBConfig = new RedisDBOptions
38 | {
39 | Configuration = "localhost:6379,ssl=false"
40 | }
41 | };
42 | EasyCaching = new DefaultRedisCachingProvider("EasyCaching",
43 | new[] { new RedisDatabaseProvider("EasyCaching", easyCachingRedisOptions) },
44 | new[] { new DefaultProtobufSerializer("EasyCaching") },
45 | easyCachingRedisOptions,
46 | (ILoggerFactory)null
47 | );
48 | IntelligentCache = new IntelligentHack.IntelligentCache.RedisCache(RedisHelper.GetConnection(), string.Empty);
49 | }
50 |
51 | [GlobalSetup]
52 | public void Setup()
53 | {
54 | RedisHelper.FlushDatabase();
55 | Thread.Sleep(TimeSpan.FromSeconds(5));
56 | }
57 |
58 | [Benchmark(Baseline = true)]
59 | public void CacheTower_RedisCacheLayer()
60 | {
61 | Parallel.For(0, ParallelIterations, async i =>
62 | {
63 | await CacheTower.GetOrSetAsync("GetOrSet_TestKey", (old) =>
64 | {
65 | return Task.FromResult("Hello World");
66 | }, new CacheSettings(TimeSpan.FromDays(1), TimeSpan.FromDays(1)));
67 | });
68 | }
69 |
70 | [Serializable]
71 | [ProtoContract]
72 | public class ProtobufCacheItem
73 | {
74 | [ProtoMember(1)]
75 | public string Value { get; set; }
76 | }
77 |
78 | [Benchmark]
79 | public void CacheManager_Redis()
80 | {
81 | Parallel.For(0, ParallelIterations, i =>
82 | {
83 | var _ = CacheManager.GetOrAdd("GetOrSet_TestKey", (key) =>
84 | {
85 | return new ProtobufCacheItem
86 | {
87 | Value = "Hello World"
88 | };
89 | }).Value;
90 | });
91 | }
92 |
93 | [Benchmark]
94 | public void EasyCaching_Redis()
95 | {
96 | Parallel.For(0, ParallelIterations, async i =>
97 | {
98 | var _ = (await EasyCaching.GetAsync("GetOrSet_TestKey", () => Task.FromResult("Hello World"), TimeSpan.FromDays(1))).Value;
99 | });
100 | }
101 |
102 | [Benchmark]
103 | public void IntelligentCache_Redis()
104 | {
105 | Parallel.For(0, ParallelIterations, async i =>
106 | {
107 | await IntelligentCache.GetSetAsync("GetOrSet_TestKey", (cancellationToken) => Task.FromResult("Hello World"), TimeSpan.FromDays(1));
108 | });
109 | }
110 | }
111 | }
112 |
--------------------------------------------------------------------------------
/benchmarks/CacheTower.AlternativesBenchmark/CacheTower.AlternativesBenchmark.csproj:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | Exe
5 | net6.0
6 | false
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
--------------------------------------------------------------------------------
/benchmarks/CacheTower.AlternativesBenchmark/Program.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using BenchmarkDotNet.Running;
3 |
4 | namespace CacheTower.AlternativesBenchmark
5 | {
6 | class Program
7 | {
8 | static void Main(string[] args)
9 | {
10 | BenchmarkSwitcher.FromAssembly(typeof(Program).Assembly).Run(args);
11 | }
12 | }
13 | }
14 |
--------------------------------------------------------------------------------
/benchmarks/CacheTower.AlternativesBenchmark/Utils/RedisHelper.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Collections.Generic;
3 | using System.Text;
4 | using StackExchange.Redis;
5 |
6 | namespace CacheTower.AlternativesBenchmark.Utils
7 | {
8 | public static class RedisHelper
9 | {
10 | public static string Endpoint => Environment.GetEnvironmentVariable("REDIS_ENDPOINT") ?? "localhost:6379";
11 |
12 | private static ConnectionMultiplexer Connection { get; set; }
13 |
14 | public static ConnectionMultiplexer GetConnection()
15 | {
16 | if (Connection == null)
17 | {
18 | var config = new ConfigurationOptions
19 | {
20 | AllowAdmin = true
21 | };
22 | config.EndPoints.Add(Endpoint);
23 | Connection = ConnectionMultiplexer.Connect(config);
24 | }
25 | return Connection;
26 | }
27 |
28 | public static void FlushDatabase()
29 | {
30 | GetConnection().GetServer(Endpoint).FlushDatabase();
31 | }
32 | }
33 | }
34 |
--------------------------------------------------------------------------------
/benchmarks/CacheTower.Benchmarks/CacheStackBenchmark.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Threading.Tasks;
3 | using BenchmarkDotNet.Attributes;
4 | using BenchmarkDotNet.Columns;
5 | using BenchmarkDotNet.Configs;
6 | using BenchmarkDotNet.Diagnosers;
7 | using BenchmarkDotNet.Environments;
8 | using BenchmarkDotNet.Jobs;
9 | using CacheTower.Providers.Memory;
10 | using Perfolizer.Horology;
11 |
12 | namespace CacheTower.Benchmarks
13 | {
14 | [Config(typeof(ConfigSettings))]
15 | public class CacheStackBenchmark
16 | {
17 | [Params(100)]
18 | public int WorkIterations { get; set; }
19 |
20 | public class ConfigSettings : ManualConfig
21 | {
22 | public ConfigSettings()
23 | {
24 | AddJob(Job.Default.WithRuntime(CoreRuntime.Core60).WithMaxIterationCount(50));
25 | AddDiagnoser(MemoryDiagnoser.Default);
26 |
27 | AddColumn(StatisticColumn.OperationsPerSecond);
28 | SummaryStyle = BenchmarkDotNet.Reports.SummaryStyle.Default
29 | .WithSizeUnit(SizeUnit.B)
30 | .WithTimeUnit(TimeUnit.Nanosecond);
31 | }
32 | }
33 |
34 | [Benchmark]
35 | public async Task Set()
36 | {
37 | await using (var cacheStack = new CacheStack(null, new(new[] { new MemoryCacheLayer() })))
38 | {
39 | for (var i = 0; i < WorkIterations; i++)
40 | {
41 | await cacheStack.SetAsync("Set", 15, TimeSpan.FromDays(1));
42 | }
43 | }
44 | }
45 | [Benchmark]
46 | public async Task Set_TwoLayers()
47 | {
48 | await using (var cacheStack = new CacheStack(null, new(new[] { new MemoryCacheLayer(), new MemoryCacheLayer() })))
49 | {
50 | for (var i = 0; i < WorkIterations; i++)
51 | {
52 | await cacheStack.SetAsync("Set", 15, TimeSpan.FromDays(1));
53 | }
54 | }
55 | }
56 | [Benchmark]
57 | public async Task Evict()
58 | {
59 | await using (var cacheStack = new CacheStack(null, new(new[] { new MemoryCacheLayer() })))
60 | {
61 | for (var i = 0; i < WorkIterations; i++)
62 | {
63 | await cacheStack.SetAsync("Evict", 15, TimeSpan.FromDays(1));
64 | await cacheStack.EvictAsync("Evict");
65 | }
66 | }
67 | }
68 | [Benchmark]
69 | public async Task Evict_TwoLayers()
70 | {
71 | await using (var cacheStack = new CacheStack(null, new(new[] { new MemoryCacheLayer(), new MemoryCacheLayer() })))
72 | {
73 | for (var i = 0; i < WorkIterations; i++)
74 | {
75 | await cacheStack.SetAsync("Evict", 15, TimeSpan.FromDays(1));
76 | await cacheStack.EvictAsync("Evict");
77 | }
78 | }
79 | }
80 | [Benchmark]
81 | public async Task Cleanup()
82 | {
83 | await using (var cacheStack = new CacheStack(null, new(new[] { new MemoryCacheLayer() })))
84 | {
85 | for (var i = 0; i < WorkIterations; i++)
86 | {
87 | await cacheStack.SetAsync("Cleanup", 15, TimeSpan.FromDays(1));
88 | await cacheStack.CleanupAsync();
89 | }
90 | }
91 | }
92 | [Benchmark]
93 | public async Task Cleanup_TwoLayers()
94 | {
95 | await using (var cacheStack = new CacheStack(null, new(new[] { new MemoryCacheLayer(), new MemoryCacheLayer() })))
96 | {
97 | for (var i = 0; i < WorkIterations; i++)
98 | {
99 | await cacheStack.SetAsync("Cleanup", 15, TimeSpan.FromDays(1));
100 | await cacheStack.CleanupAsync();
101 | }
102 | }
103 | }
104 | [Benchmark]
105 | public async Task GetMiss()
106 | {
107 | await using (var cacheStack = new CacheStack(null, new(new[] { new MemoryCacheLayer() })))
108 | {
109 | for (var i = 0; i < WorkIterations; i++)
110 | {
111 | await cacheStack.GetAsync("GetMiss");
112 | }
113 | }
114 | }
115 | [Benchmark]
116 | public async Task GetHit()
117 | {
118 | await using (var cacheStack = new CacheStack(null, new(new[] { new MemoryCacheLayer() })))
119 | {
120 | await cacheStack.SetAsync("GetHit", 15, TimeSpan.FromDays(1));
121 |
122 | for (var i = 0; i < WorkIterations; i++)
123 | {
124 | await cacheStack.GetAsync("GetHit");
125 | }
126 | }
127 | }
128 | [Benchmark]
129 | public async Task GetOrSet_NeverStale()
130 | {
131 | await using (var cacheStack = new CacheStack(null, new(new[] { new MemoryCacheLayer() })))
132 | {
133 | for (var i = 0; i < WorkIterations; i++)
134 | {
135 | await cacheStack.GetOrSetAsync("GetOrSet", (old) =>
136 | {
137 | return Task.FromResult(12);
138 | }, new CacheSettings(TimeSpan.FromDays(1), TimeSpan.FromDays(1)));
139 | }
140 | }
141 | }
142 | [Benchmark]
143 | public async Task GetOrSet_AlwaysStale()
144 | {
145 | await using (var cacheStack = new CacheStack(null, new(new[] { new MemoryCacheLayer() })))
146 | {
147 | for (var i = 0; i < WorkIterations; i++)
148 | {
149 | await cacheStack.GetOrSetAsync("GetOrSet", (old) =>
150 | {
151 | return Task.FromResult(12);
152 | }, new CacheSettings(TimeSpan.FromDays(1)));
153 | }
154 | }
155 | }
156 | [Benchmark]
157 | public async Task GetOrSet_UnderLoad()
158 | {
159 | await using (var cacheStack = new CacheStack(null, new(new[] { new MemoryCacheLayer() })))
160 | {
161 | await cacheStack.SetAsync("GetOrSet", new CacheEntry(15, DateTime.UtcNow.AddDays(-1)));
162 |
163 | Parallel.For(0, WorkIterations, async value =>
164 | {
165 | await cacheStack.GetOrSetAsync("GetOrSet", async (old) =>
166 | {
167 | await Task.Delay(30);
168 | return 12;
169 | }, new CacheSettings(TimeSpan.FromDays(1)));
170 | });
171 | }
172 | }
173 | }
174 | }
175 |
--------------------------------------------------------------------------------
/benchmarks/CacheTower.Benchmarks/CacheTower.Benchmarks.csproj:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | Exe
5 | net6.0
6 | false
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
--------------------------------------------------------------------------------
/benchmarks/CacheTower.Benchmarks/Extensions/BaseCacheChangeExtensionBenchmark.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Threading.Tasks;
3 | using BenchmarkDotNet.Attributes;
4 |
5 | namespace CacheTower.Benchmarks.Extensions
6 | {
7 | public abstract class BaseCacheChangeExtensionBenchmark : BaseExtensionsBenchmark
8 | {
9 | public DateTime BenchmarkValue;
10 |
11 | protected override void SetupBenchmark()
12 | {
13 | BenchmarkValue = DateTime.UtcNow;
14 | }
15 |
16 | [Benchmark]
17 | public async Task OnCacheUpdate()
18 | {
19 | var extension = CacheExtension as ICacheChangeExtension;
20 | await extension.OnCacheUpdateAsync("OnCacheUpdate_CacheKey", BenchmarkValue, CacheUpdateType.AddOrUpdateEntry);
21 | }
22 |
23 | [Benchmark]
24 | public async Task OnCacheEviction()
25 | {
26 | var extension = CacheExtension as ICacheChangeExtension;
27 | await extension.OnCacheEvictionAsync("OnCacheEviction_CacheKey");
28 | }
29 |
30 | [Benchmark]
31 | public async Task OnCacheFlush()
32 | {
33 | var extension = CacheExtension as ICacheChangeExtension;
34 | await extension.OnCacheFlushAsync();
35 | }
36 | }
37 | }
38 |
--------------------------------------------------------------------------------
/benchmarks/CacheTower.Benchmarks/Extensions/BaseDistributedLockExtensionBenchmark.cs:
--------------------------------------------------------------------------------
1 | using System.Threading.Tasks;
2 | using BenchmarkDotNet.Attributes;
3 |
4 | namespace CacheTower.Benchmarks.Extensions;
5 |
6 | public abstract class BaseDistributedLockExtensionBenchmark : BaseExtensionsBenchmark
7 | {
8 | [Benchmark]
9 | public async Task AwaitAccessAndRelease()
10 | {
11 | var extension = CacheExtension as IDistributedLockExtension;
12 | await using var _ = await extension.AwaitAccessAsync("RefreshValue");
13 | }
14 | }
15 |
--------------------------------------------------------------------------------
/benchmarks/CacheTower.Benchmarks/Extensions/BaseExtensionsBenchmark.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Threading.Tasks;
3 | using BenchmarkDotNet.Attributes;
4 | using BenchmarkDotNet.Columns;
5 | using BenchmarkDotNet.Configs;
6 | using BenchmarkDotNet.Diagnosers;
7 | using BenchmarkDotNet.Environments;
8 | using BenchmarkDotNet.Jobs;
9 | using CacheTower.Providers.Memory;
10 | using Perfolizer.Horology;
11 |
12 | namespace CacheTower.Benchmarks.Extensions
13 | {
14 | [Config(typeof(ConfigSettings))]
15 | public abstract class BaseExtensionsBenchmark
16 | {
17 | public class ConfigSettings : ManualConfig
18 | {
19 | public ConfigSettings()
20 | {
21 | AddJob(Job.Default.WithRuntime(CoreRuntime.Core60).WithMaxIterationCount(200));
22 | AddDiagnoser(MemoryDiagnoser.Default);
23 |
24 | SummaryStyle = new BenchmarkDotNet.Reports.SummaryStyle(CultureInfo, true, SizeUnit.B, TimeUnit.Nanosecond);
25 | }
26 | }
27 |
28 | protected ICacheExtension CacheExtension { get; set; }
29 |
30 | protected virtual void SetupBenchmark() { }
31 | protected virtual void CleanupBenchmark() { }
32 |
33 | protected static CacheStack CacheStack { get; } = new CacheStack(null, new(new[] { new MemoryCacheLayer() }));
34 |
35 | [GlobalSetup]
36 | public void Setup()
37 | {
38 | SetupBenchmark();
39 | CacheExtension.Register(CacheStack);
40 | }
41 |
42 | [GlobalCleanup]
43 | public async Task CleanupAsync()
44 | {
45 | CleanupBenchmark();
46 | await CacheStack.DisposeAsync();
47 | }
48 | }
49 | }
50 |
--------------------------------------------------------------------------------
/benchmarks/CacheTower.Benchmarks/Extensions/Redis/RedisLockExtensionBenchmark.cs:
--------------------------------------------------------------------------------
1 | using BenchmarkDotNet.Attributes;
2 | using CacheTower.Benchmarks.Utils;
3 | using CacheTower.Extensions.Redis;
4 |
5 | namespace CacheTower.Benchmarks.Extensions.Redis
6 | {
7 | public class RedisLockExtensionBenchmark : BaseDistributedLockExtensionBenchmark
8 | {
9 | protected override void SetupBenchmark()
10 | {
11 | base.SetupBenchmark();
12 |
13 | CacheExtension = new RedisLockExtension(RedisHelper.GetConnection());
14 | RedisHelper.FlushDatabase();
15 | }
16 |
17 | protected override void CleanupBenchmark()
18 | {
19 | base.CleanupBenchmark();
20 | RedisHelper.FlushDatabase();
21 | }
22 | }
23 | }
24 |
--------------------------------------------------------------------------------
/benchmarks/CacheTower.Benchmarks/Extensions/Redis/RedisRemoteEvictionExtensionBenchmark.cs:
--------------------------------------------------------------------------------
1 | using BenchmarkDotNet.Attributes;
2 | using CacheTower.Benchmarks.Utils;
3 | using CacheTower.Extensions.Redis;
4 |
5 | namespace CacheTower.Benchmarks.Extensions.Redis
6 | {
7 | public class RedisRemoteEvictionExtensionBenchmark : BaseCacheChangeExtensionBenchmark
8 | {
9 | protected override void SetupBenchmark()
10 | {
11 | base.SetupBenchmark();
12 |
13 | CacheExtension = new RedisRemoteEvictionExtension(RedisHelper.GetConnection());
14 | RedisHelper.FlushDatabase();
15 | }
16 |
17 | protected override void CleanupBenchmark()
18 | {
19 | base.CleanupBenchmark();
20 | RedisHelper.FlushDatabase();
21 | }
22 | }
23 | }
24 |
--------------------------------------------------------------------------------
/benchmarks/CacheTower.Benchmarks/Program.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using BenchmarkDotNet.Running;
3 |
4 | namespace CacheTower.Benchmarks
5 | {
6 | class Program
7 | {
8 | static void Main(string[] args)
9 | {
10 | BenchmarkSwitcher.FromAssembly(typeof(Program).Assembly).Run(args);
11 | }
12 | }
13 | }
14 |
--------------------------------------------------------------------------------
/benchmarks/CacheTower.Benchmarks/Providers/BaseCacheLayerBenchmark.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Collections.Generic;
3 | using System.Threading.Tasks;
4 | using BenchmarkDotNet.Attributes;
5 | using BenchmarkDotNet.Columns;
6 | using BenchmarkDotNet.Configs;
7 | using BenchmarkDotNet.Diagnosers;
8 | using BenchmarkDotNet.Environments;
9 | using BenchmarkDotNet.Jobs;
10 | using Perfolizer.Horology;
11 |
12 | namespace CacheTower.Benchmarks.Providers
13 | {
14 | [Config(typeof(ConfigSettings))]
15 | public abstract class BaseCacheLayerBenchmark
16 | {
17 | public class ConfigSettings : ManualConfig
18 | {
19 | public ConfigSettings()
20 | {
21 | AddJob(Job.Default.WithRuntime(CoreRuntime.Core60).WithMaxIterationCount(200));
22 | AddDiagnoser(MemoryDiagnoser.Default);
23 |
24 | AddColumn(StatisticColumn.OperationsPerSecond);
25 | SummaryStyle = BenchmarkDotNet.Reports.SummaryStyle.Default
26 | .WithSizeUnit(SizeUnit.B)
27 | .WithTimeUnit(TimeUnit.Nanosecond);
28 | }
29 | }
30 |
31 | [Params(1, 100)]
32 | public int WorkIterations { get; set; }
33 |
34 | protected Func CacheLayerProvider { get; set; }
35 |
36 | protected static async Task DisposeOf(ICacheLayer cacheLayer)
37 | {
38 | if (cacheLayer is IDisposable disposableLayer)
39 | {
40 | disposableLayer.Dispose();
41 | }
42 | else if (cacheLayer is IAsyncDisposable asyncDisposableLayer)
43 | {
44 | await asyncDisposableLayer.DisposeAsync();
45 | }
46 | }
47 |
48 | [Benchmark]
49 | public async Task GetMiss()
50 | {
51 | var cacheLayer = CacheLayerProvider.Invoke();
52 | for (var i = 0; i < WorkIterations; i++)
53 | {
54 | await cacheLayer.GetAsync("GetMiss");
55 | }
56 | await DisposeOf(cacheLayer);
57 | }
58 |
59 | [Benchmark]
60 | public async Task GetHit()
61 | {
62 | var cacheLayer = CacheLayerProvider.Invoke();
63 | await cacheLayer.SetAsync("GetHit", new CacheEntry(1, TimeSpan.FromDays(1)));
64 | for (var i = 0; i < WorkIterations; i++)
65 | {
66 | await cacheLayer.GetAsync("GetHit");
67 | }
68 | await DisposeOf(cacheLayer);
69 | }
70 |
71 | [Benchmark]
72 | public async Task SetExisting()
73 | {
74 | var cacheLayer = CacheLayerProvider.Invoke();
75 | await cacheLayer.SetAsync("SetExisting", new CacheEntry(1, TimeSpan.FromDays(1)));
76 | for (var i = 0; i < WorkIterations; i++)
77 | {
78 | await cacheLayer.SetAsync("SetExisting", new CacheEntry(1, TimeSpan.FromDays(1)));
79 | }
80 | await DisposeOf(cacheLayer);
81 | }
82 |
83 | [Benchmark]
84 | public async Task EvictMiss()
85 | {
86 | var cacheLayer = CacheLayerProvider.Invoke();
87 | for (var i = 0; i < WorkIterations; i++)
88 | {
89 | await cacheLayer.EvictAsync("EvictMiss");
90 | }
91 | await DisposeOf(cacheLayer);
92 | }
93 |
94 | [Benchmark]
95 | public async Task EvictHit()
96 | {
97 | var cacheLayer = CacheLayerProvider.Invoke();
98 | for (var i = 0; i < WorkIterations; i++)
99 | {
100 | await cacheLayer.SetAsync("EvictHit", new CacheEntry(1, TimeSpan.FromDays(1)));
101 | await cacheLayer.EvictAsync("EvictHit");
102 | }
103 | await DisposeOf(cacheLayer);
104 | }
105 |
106 | [Benchmark]
107 | public async Task Cleanup()
108 | {
109 | var expiredDate = DateTime.UtcNow.AddDays(-1);
110 | var cacheLayer = CacheLayerProvider.Invoke();
111 | for (var i = 0; i < WorkIterations; i++)
112 | {
113 | await cacheLayer.SetAsync($"Cleanup_{i}", new CacheEntry(1, expiredDate));
114 | }
115 | await cacheLayer.CleanupAsync();
116 | await DisposeOf(cacheLayer);
117 | }
118 |
119 | [Benchmark]
120 | public async Task GetHitSimultaneous()
121 | {
122 | var cacheLayer = CacheLayerProvider.Invoke();
123 |
124 | await cacheLayer.SetAsync("GetHitSimultaneous", new CacheEntry(1, TimeSpan.FromDays(1)));
125 |
126 | var tasks = new List();
127 |
128 | for (var i = 0; i < WorkIterations; i++)
129 | {
130 | var task = cacheLayer.GetAsync("GetHitSimultaneous");
131 | tasks.Add(task.AsTask());
132 | }
133 |
134 | await Task.WhenAll(tasks);
135 |
136 | await DisposeOf(cacheLayer);
137 | }
138 |
139 | [Benchmark]
140 | public async Task SetExistingSimultaneous()
141 | {
142 | var cacheLayer = CacheLayerProvider.Invoke();
143 |
144 | await cacheLayer.SetAsync("SetExistingSimultaneous", new CacheEntry(1, TimeSpan.FromDays(1)));
145 |
146 | var tasks = new List();
147 |
148 | for (var i = 0; i < WorkIterations; i++)
149 | {
150 | var task = cacheLayer.SetAsync("SetExistingSimultaneous", new CacheEntry(1, TimeSpan.FromDays(1)));
151 | tasks.Add(task.AsTask());
152 | }
153 |
154 | await Task.WhenAll(tasks);
155 |
156 | await DisposeOf(cacheLayer);
157 | }
158 | }
159 | }
160 |
--------------------------------------------------------------------------------
/benchmarks/CacheTower.Benchmarks/Providers/CacheLayerComparisonBenchmark.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Collections.Generic;
3 | using System.IO;
4 | using System.Threading.Tasks;
5 | using BenchmarkDotNet.Attributes;
6 | using BenchmarkDotNet.Columns;
7 | using BenchmarkDotNet.Configs;
8 | using BenchmarkDotNet.Diagnosers;
9 | using BenchmarkDotNet.Environments;
10 | using BenchmarkDotNet.Jobs;
11 | using BenchmarkDotNet.Order;
12 | using CacheTower.Benchmarks.Utils;
13 | using CacheTower.Providers.Database.MongoDB;
14 | using CacheTower.Providers.FileSystem;
15 | using CacheTower.Providers.Memory;
16 | using CacheTower.Providers.Redis;
17 | using CacheTower.Serializers.NewtonsoftJson;
18 | using CacheTower.Serializers.Protobuf;
19 | using Perfolizer.Horology;
20 | using ProtoBuf;
21 |
22 | namespace CacheTower.Benchmarks.Providers
23 | {
24 | [Config(typeof(ConfigSettings))]
25 | public class CacheLayerComparisonBenchmark
26 | {
27 | public class ConfigSettings : ManualConfig
28 | {
29 | public ConfigSettings()
30 | {
31 | AddJob(Job.Default.WithRuntime(CoreRuntime.Core60));
32 | AddDiagnoser(MemoryDiagnoser.Default);
33 |
34 | WithOrderer(new DefaultOrderer(SummaryOrderPolicy.FastestToSlowest));
35 |
36 | AddColumn(StatisticColumn.OperationsPerSecond);
37 | SummaryStyle = BenchmarkDotNet.Reports.SummaryStyle.Default
38 | .WithSizeUnit(SizeUnit.B)
39 | .WithTimeUnit(TimeUnit.Nanosecond);
40 | }
41 | }
42 |
43 |
44 | [Params(1, 10)]
45 | public int WorkIterations { get; set; }
46 |
47 | [ProtoContract]
48 | private class ComplexType
49 | {
50 | [ProtoMember(1)]
51 | public string ExampleString { get; set; }
52 | [ProtoMember(2)]
53 | public int ExampleNumber { get; set; }
54 | [ProtoMember(3)]
55 | public DateTime ExampleDate { get; set; }
56 | [ProtoMember(4)]
57 | public Dictionary DictionaryOfNumbers { get; set; }
58 | }
59 |
60 | protected async ValueTask BenchmarkWork(ICacheLayer cacheLayer)
61 | {
62 | for (var iterationCount = 0; iterationCount < WorkIterations; iterationCount++)
63 | {
64 | //Get 100 misses
65 | for (var i = 0; i < 100; i++)
66 | {
67 | await cacheLayer.GetAsync("GetMiss_" + i);
68 | }
69 |
70 | var startDate = DateTime.UtcNow.AddDays(-50);
71 |
72 | //Set first 100 (simple type)
73 | for (var i = 0; i < 100; i++)
74 | {
75 | await cacheLayer.SetAsync("Comparison_" + i, new CacheEntry(1, startDate.AddDays(i) + TimeSpan.FromDays(1)));
76 | }
77 | //Set last 100 (complex type)
78 | for (var i = 100; i < 200; i++)
79 | {
80 | await cacheLayer.SetAsync("Comparison_" + i, new CacheEntry(new ComplexType
81 | {
82 | ExampleString = "Hello World",
83 | ExampleNumber = 42,
84 | ExampleDate = new DateTime(2000, 1, 1),
85 | DictionaryOfNumbers = new Dictionary() { { "A", 1 }, { "B", 2 }, { "C", 3 } }
86 | }, startDate.AddDays(i - 100) + TimeSpan.FromDays(1)));
87 | }
88 |
89 | //Get first 50 (simple type)
90 | for (var i = 0; i < 50; i++)
91 | {
92 | await cacheLayer.GetAsync("Comparison_" + i);
93 | }
94 | //Get last 50 (complex type)
95 | for (var i = 150; i < 200; i++)
96 | {
97 | await cacheLayer.GetAsync("Comparison_" + i);
98 | }
99 |
100 | //Evict middle 100
101 | for (var i = 50; i < 150; i++)
102 | {
103 | await cacheLayer.EvictAsync("Comparison_" + i);
104 | }
105 |
106 | //Cleanup outer 100
107 | await cacheLayer.CleanupAsync();
108 | }
109 | }
110 |
111 | [GlobalSetup]
112 | public void Setup()
113 | {
114 | MongoDbHelper.DropDatabase();
115 | }
116 |
117 | [Benchmark(Baseline = true)]
118 | public async Task MemoryCacheLayer()
119 | {
120 | var cacheLayer = new MemoryCacheLayer();
121 | await BenchmarkWork(cacheLayer);
122 | }
123 |
124 | [Benchmark]
125 | public async Task JsonFileCacheLayer()
126 | {
127 | var directoryPath = "CacheLayerComparison/NewtonsoftJson";
128 | await using (var cacheLayer = new FileCacheLayer(new FileCacheLayerOptions(directoryPath, NewtonsoftJsonCacheSerializer.Instance)))
129 | {
130 | await BenchmarkWork(cacheLayer);
131 | }
132 | Directory.Delete(directoryPath, true);
133 | }
134 |
135 | [Benchmark]
136 | public async Task ProtobufFileCacheLayer()
137 | {
138 | var directoryPath = "CacheLayerComparison/Protobuf";
139 | await using (var cacheLayer = new FileCacheLayer(new FileCacheLayerOptions(directoryPath, ProtobufCacheSerializer.Instance)))
140 | {
141 | await BenchmarkWork(cacheLayer);
142 | }
143 | Directory.Delete(directoryPath, true);
144 | }
145 |
146 | [Benchmark]
147 | public async Task MongoDbCacheLayer()
148 | {
149 | var cacheLayer = new MongoDbCacheLayer(MongoDbHelper.GetConnection());
150 | await BenchmarkWork(cacheLayer);
151 | await MongoDbHelper.DropDatabaseAsync();
152 | }
153 |
154 | [Benchmark]
155 | public async Task RedisCacheLayer()
156 | {
157 | var cacheLayer = new RedisCacheLayer(RedisHelper.GetConnection(), new RedisCacheLayerOptions(ProtobufCacheSerializer.Instance));
158 | await BenchmarkWork(cacheLayer);
159 | RedisHelper.FlushDatabase();
160 | }
161 | }
162 | }
163 |
--------------------------------------------------------------------------------
/benchmarks/CacheTower.Benchmarks/Providers/Database/MongoDbCacheLayerBenchmark.cs:
--------------------------------------------------------------------------------
1 | using BenchmarkDotNet.Attributes;
2 | using CacheTower.Benchmarks.Utils;
3 | using CacheTower.Providers.Database.MongoDB;
4 | using MongoFramework;
5 |
6 | namespace CacheTower.Benchmarks.Providers.Database
7 | {
8 | public class MongoDbCacheLayerBenchmark : BaseCacheLayerBenchmark
9 | {
10 | private IMongoDbConnection Connection { get; set; }
11 |
12 | [GlobalSetup]
13 | public void Setup()
14 | {
15 | Connection = MongoDbHelper.GetConnection();
16 | CacheLayerProvider = () => new MongoDbCacheLayer(Connection);
17 | MongoDbHelper.DropDatabase();
18 | }
19 |
20 | [IterationCleanup]
21 | public void IterationCleanup()
22 | {
23 | MongoDbHelper.DropDatabase();
24 | }
25 | }
26 | }
27 |
--------------------------------------------------------------------------------
/benchmarks/CacheTower.Benchmarks/Providers/FileSystem/BaseFileCacheLayerBenchmark.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Collections.Generic;
3 | using System.IO;
4 | using System.Text;
5 | using System.Threading;
6 | using BenchmarkDotNet.Attributes;
7 |
8 | namespace CacheTower.Benchmarks.Providers.FileSystem
9 | {
10 | public abstract class BaseFileCacheLayerBenchmark : BaseCacheLayerBenchmark
11 | {
12 | protected string DirectoryPath { get; set; }
13 |
14 | private void CleanupFileSystem()
15 | {
16 | var attempts = 0;
17 | while (attempts < 5)
18 | {
19 | try
20 | {
21 | if (Directory.Exists(DirectoryPath))
22 | {
23 | Directory.Delete(DirectoryPath, true);
24 | }
25 |
26 | break;
27 | }
28 | catch
29 | {
30 | Thread.Sleep(200);
31 | }
32 | attempts++;
33 | }
34 | }
35 |
36 | [IterationSetup]
37 | public void PreIterationDirectoryCleanup()
38 | {
39 | CleanupFileSystem();
40 | }
41 |
42 | [IterationCleanup]
43 | public void PostIterationDirectoryCleanup()
44 | {
45 | CleanupFileSystem();
46 | }
47 | }
48 | }
49 |
--------------------------------------------------------------------------------
/benchmarks/CacheTower.Benchmarks/Providers/FileSystem/NewtonsoftJsonFileCacheBenchmark.cs:
--------------------------------------------------------------------------------
1 | using BenchmarkDotNet.Attributes;
2 | using CacheTower.Providers.FileSystem;
3 | using CacheTower.Serializers.NewtonsoftJson;
4 |
5 | namespace CacheTower.Benchmarks.Providers.FileSystem
6 | {
7 | public class NewtonsoftJsonFileCacheBenchmark : BaseFileCacheLayerBenchmark
8 | {
9 | [GlobalSetup]
10 | public void Setup()
11 | {
12 | DirectoryPath = "FileCache/NewtonsoftJson";
13 | CacheLayerProvider = () => new FileCacheLayer(new(DirectoryPath, NewtonsoftJsonCacheSerializer.Instance));
14 | }
15 | }
16 | }
17 |
--------------------------------------------------------------------------------
/benchmarks/CacheTower.Benchmarks/Providers/FileSystem/ProtobufFileCacheBenchmark.cs:
--------------------------------------------------------------------------------
1 | using BenchmarkDotNet.Attributes;
2 | using CacheTower.Providers.FileSystem;
3 | using CacheTower.Serializers.Protobuf;
4 |
5 | namespace CacheTower.Benchmarks.Providers.FileSystem
6 | {
7 | public class ProtobufFileCacheBenchmark : BaseFileCacheLayerBenchmark
8 | {
9 | [GlobalSetup]
10 | public void Setup()
11 | {
12 | DirectoryPath = "FileCache/Protobuf";
13 | CacheLayerProvider = () => new FileCacheLayer(new(DirectoryPath, ProtobufCacheSerializer.Instance));
14 | }
15 | }
16 | }
17 |
--------------------------------------------------------------------------------
/benchmarks/CacheTower.Benchmarks/Providers/Memory/MemoryCacheBenchmark.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Collections.Generic;
3 | using System.Text;
4 | using System.Threading.Tasks;
5 | using BenchmarkDotNet.Attributes;
6 | using CacheTower.Providers.Memory;
7 |
8 | namespace CacheTower.Benchmarks.Providers.Memory
9 | {
10 | public class MemoryCacheBenchmark : BaseCacheLayerBenchmark
11 | {
12 | [GlobalSetup]
13 | public void Setup()
14 | {
15 | CacheLayerProvider = () => new MemoryCacheLayer();
16 | }
17 | }
18 | }
19 |
--------------------------------------------------------------------------------
/benchmarks/CacheTower.Benchmarks/Providers/Redis/RedisCacheLayerBenchmark.cs:
--------------------------------------------------------------------------------
1 | using BenchmarkDotNet.Attributes;
2 | using CacheTower.Benchmarks.Utils;
3 | using CacheTower.Providers.Redis;
4 | using CacheTower.Serializers.Protobuf;
5 |
6 | namespace CacheTower.Benchmarks.Providers.Redis
7 | {
8 | public class RedisCacheLayerBenchmark : BaseCacheLayerBenchmark
9 | {
10 | [GlobalSetup]
11 | public void Setup()
12 | {
13 | CacheLayerProvider = () => new RedisCacheLayer(RedisHelper.GetConnection(), new RedisCacheLayerOptions(ProtobufCacheSerializer.Instance));
14 | }
15 |
16 | [IterationSetup]
17 | public void PreIterationRedisCleanup()
18 | {
19 | RedisHelper.FlushDatabase();
20 | }
21 |
22 | [IterationCleanup]
23 | public void PostIterationRedisCleanup()
24 | {
25 | RedisHelper.FlushDatabase();
26 | }
27 | }
28 | }
29 |
--------------------------------------------------------------------------------
/benchmarks/CacheTower.Benchmarks/Utils/MongoDbHelper.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Threading.Tasks;
3 | using MongoDB.Driver;
4 | using MongoFramework;
5 |
6 | namespace CacheTower.Benchmarks.Utils
7 | {
8 | public static class MongoDbHelper
9 | {
10 | public static string ConnectionString => Environment.GetEnvironmentVariable("MONGODB_URI") ?? "mongodb://localhost";
11 |
12 | public static string GetDatabaseName()
13 | {
14 | return "CacheTowerBenchmarks";
15 | }
16 |
17 | public static IMongoDbConnection GetConnection()
18 | {
19 | var urlBuilder = new MongoUrlBuilder(ConnectionString)
20 | {
21 | DatabaseName = GetDatabaseName()
22 | };
23 | return MongoDbConnection.FromUrl(urlBuilder.ToMongoUrl());
24 | }
25 |
26 | public static async Task DropDatabaseAsync()
27 | {
28 | await GetConnection().Client.DropDatabaseAsync(GetDatabaseName());
29 | }
30 | public static void DropDatabase()
31 | {
32 | GetConnection().Client.DropDatabase(GetDatabaseName());
33 | }
34 | }
35 | }
36 |
--------------------------------------------------------------------------------
/benchmarks/CacheTower.Benchmarks/Utils/RedisHelper.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using StackExchange.Redis;
3 |
4 | namespace CacheTower.Benchmarks.Utils
5 | {
6 | public static class RedisHelper
7 | {
8 | public static string Endpoint => Environment.GetEnvironmentVariable("REDIS_ENDPOINT") ?? "localhost:6379";
9 |
10 | private static ConnectionMultiplexer Connection { get; set; }
11 |
12 | public static ConnectionMultiplexer GetConnection()
13 | {
14 | if (Connection == null)
15 | {
16 | var config = new ConfigurationOptions
17 | {
18 | AllowAdmin = true,
19 | SyncTimeout = (int)TimeSpan.FromSeconds(20).TotalMilliseconds
20 | };
21 | config.EndPoints.Add(Endpoint);
22 | Connection = ConnectionMultiplexer.Connect(config);
23 | }
24 | return Connection;
25 | }
26 |
27 | public static void FlushDatabase()
28 | {
29 | GetConnection().GetServer(Endpoint).FlushDatabase();
30 | }
31 | }
32 | }
33 |
--------------------------------------------------------------------------------
/docs/Comparison.md:
--------------------------------------------------------------------------------
1 | # Caching Performance Comparison
2 |
3 | **Test Machine**
4 |
5 | ```
6 | BenchmarkDotNet=v0.13.1, OS=Windows 10.0.19043.1766 (21H1/May2021Update)
7 | Intel Core i7-6700HQ CPU 2.60GHz (Skylake), 1 CPU, 8 logical and 4 physical cores
8 | .NET SDK=6.0.300
9 | [Host] : .NET 6.0.5 (6.0.522.21309), X64 RyuJIT
10 | Job-BJQIPU : .NET 6.0.5 (6.0.522.21309), X64 RyuJIT
11 |
12 | Runtime=.NET 6.0 MaxIterationCount=200
13 | ```
14 |
15 | _Note: The performance figures below are as a guide only. Different systems and configurations can drastically change performance results._
16 |
17 | ## Sequential In-Memory Caching
18 |
19 | | Method | Mean | Error | StdDev | Op/s | Ratio | RatioSD | Gen 0 | Allocated |
20 | |---------------------------------- |---------:|--------:|--------:|------------:|------:|--------:|-------:|----------:|
21 | | IntelligentCache_MemoryCache | 177.1 ns | 3.43 ns | 3.36 ns | 5,647,113.9 | 0.78 | 0.02 | 0.0279 | 88 B |
22 | | CacheTower_MemoryCacheLayer | 226.5 ns | 4.35 ns | 4.28 ns | 4,415,623.1 | 1.00 | 0.00 | 0.0229 | 72 B |
23 | | CacheManager_MicrosoftMemoryCache | 263.9 ns | 5.12 ns | 5.26 ns | 3,789,315.3 | 1.16 | 0.03 | 0.0277 | 88 B |
24 | | FusionCache_MemoryProvider | 295.7 ns | 5.60 ns | 5.50 ns | 3,382,054.7 | 1.31 | 0.04 | 0.1016 | 320 B |
25 | | LazyCache_MemoryProvider | 297.3 ns | 4.90 ns | 5.45 ns | 3,363,940.7 | 1.31 | 0.04 | 0.1144 | 360 B |
26 | | EasyCaching_InMemory | 301.1 ns | 5.86 ns | 7.41 ns | 3,321,052.2 | 1.33 | 0.03 | 0.0482 | 152 B |
27 |
28 | ## Parallel In-Memory Caching
29 |
30 | | Method | Mean | Error | StdDev | Op/s | Ratio | RatioSD | Gen 0 | Allocated |
31 | |---------------------------------- |----------:|----------:|----------:|---------:|------:|--------:|---------:|----------:|
32 | | IntelligentCache_MemoryCache | 66.12 us | 0.479 us | 0.425 us | 15,124.8 | 0.89 | 0.01 | 29.2969 | 89 KB |
33 | | CacheTower_MemoryCacheLayer | 74.29 us | 1.292 us | 1.208 us | 13,460.5 | 1.00 | 0.00 | 0.9766 | 3 KB |
34 | | CacheManager_MicrosoftMemoryCache | 93.83 us | 0.639 us | 0.566 us | 10,657.6 | 1.27 | 0.02 | 29.2969 | 89 KB |
35 | | EasyCaching_InMemory | 119.71 us | 1.501 us | 1.404 us | 8,353.7 | 1.61 | 0.02 | 49.9268 | 151 KB |
36 | | FusionCache_MemoryProvider | 132.27 us | 0.775 us | 0.605 us | 7,560.2 | 1.79 | 0.02 | 104.2480 | 316 KB |
37 | | LazyCache_MemoryProvider | 917.56 us | 16.636 us | 15.561 us | 1,089.9 | 12.35 | 0.14 | 118.1641 | 356 KB |
38 |
39 |
40 | ## Sequential Redis Caching
41 |
42 | - Redis benchmarks unable to run due to [bug in StackExchange.Redis](https://github.com/StackExchange/StackExchange.Redis/pull/2166).
43 | The results below are from an older version of .NET
44 |
45 | | Method | Mean | Error | StdDev | Op/s | Ratio | Gen 0 | Gen 1 | Gen 2 | Allocated |
46 | |--------------------------- |---------:|--------:|--------:|--------:|------:|-------:|------:|------:|----------:|
47 | | CacheManager_Redis | 134.7 us | 0.72 us | 0.67 us | 7,425.3 | 0.84 | 0.7324 | - | - | 2376 B |
48 | | IntelligentCache_Redis | 156.2 us | 0.91 us | 0.85 us | 6,403.4 | 0.97 | 0.9766 | - | - | 3456 B |
49 | | EasyCaching_Redis | 158.1 us | 0.50 us | 0.47 us | 6,326.3 | 0.99 | 0.2441 | - | - | 1144 B |
50 | | CacheTower_RedisCacheLayer | 160.3 us | 0.47 us | 0.44 us | 6,238.4 | 1.00 | 0.2441 | - | - | 936 B |
51 |
52 | ## Parallel Redis Caching
53 |
54 | - Redis benchmarks unable to run due to [bug in StackExchange.Redis](https://github.com/StackExchange/StackExchange.Redis/pull/2166)
55 | The results below are from an older version of .NET
56 |
57 | | Method | Mean | Error | StdDev | Op/s | Ratio | RatioSD | Gen 0 | Gen 1 | Gen 2 | Allocated |
58 | |--------------------------- |-----------:|---------:|---------:|--------:|------:|--------:|--------:|--------:|-------:|----------:|
59 | | CacheTower_RedisCacheLayer | 413.2 us | 9.73 us | 40.90 us | 2,420.0 | 1.00 | 0.00 | 24.4141 | 9.7656 | 1.9531 | 110.38 KB |
60 | | EasyCaching_Redis | 452.3 us | 8.99 us | 31.63 us | 2,211.0 | 1.11 | 0.13 | 22.4609 | 9.7656 | 2.9297 | 108.74 KB |
61 | | IntelligentCache_Redis | 506.8 us | 10.02 us | 25.85 us | 1,973.1 | 1.26 | 0.15 | 72.2656 | 23.9258 | 3.9063 | 332.72 KB |
62 | | CacheManager_Redis | 2,999.1 us | 52.07 us | 46.16 us | 333.4 | 7.94 | 0.80 | 82.0313 | - | - | 241.57 KB |
63 |
64 |
65 | ## File Caching
66 |
67 | | Method | Mean | Error | StdDev | Op/s | Ratio | RatioSD | Gen 0 | Gen 1 | Allocated |
68 | |----------------------------------------- |---------:|--------:|--------:|--------:|------:|--------:|-------:|-------:|----------:|
69 | | CacheTower_FileCacheLayer_SystemTextJson | 333.9 us | 6.40 us | 8.32 us | 2,994.5 | 0.99 | 0.03 | 0.9766 | 0.4883 | 3 KB |
70 | | CacheTower_FileCacheLayer_Protobuf | 337.1 us | 6.71 us | 7.45 us | 2,966.5 | 0.99 | 0.03 | 0.9766 | 0.4883 | 3 KB |
71 | | CacheTower_FileCacheLayer_NewtonsoftJson | 340.1 us | 3.43 us | 3.21 us | 2,940.5 | 1.00 | 0.00 | 2.9297 | 1.4648 | 9 KB |
72 | | EasyCaching_Disk | 359.7 us | 5.32 us | 4.97 us | 2,780.4 | 1.06 | 0.02 | 1.4648 | - | 5 KB |
--------------------------------------------------------------------------------
/images/icon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/TurnerSoftware/CacheTower/4da8de4a637d6a04b0055365a11278619267a412/images/icon.png
--------------------------------------------------------------------------------
/src/CacheTower.Extensions.Redis/AssemblyInternals.cs:
--------------------------------------------------------------------------------
1 | using System.Runtime.CompilerServices;
2 |
3 | [assembly: InternalsVisibleTo("CacheTower.Tests")]
--------------------------------------------------------------------------------
/src/CacheTower.Extensions.Redis/CacheTower.Extensions.Redis.csproj:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | netstandard2.0
5 | Redis Extensions for Cache Tower
6 | Provides Distributed Locking & Eviction for Cache Tower
7 | redis;$(PackageBaseTags)
8 | James Turner
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
--------------------------------------------------------------------------------
/src/CacheTower.Extensions.Redis/RedisLockExtension.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Collections.Concurrent;
3 | using System.Threading.Tasks;
4 | using StackExchange.Redis;
5 |
6 | namespace CacheTower.Extensions.Redis;
7 |
8 | ///
9 | /// Provides distributed cache locking via Redis.
10 | ///
11 | ///
12 | /// Based on Loris Cro's RedisMemoLock"
13 | ///
14 | public class RedisLockExtension : IDistributedLockExtension
15 | {
16 | private ISubscriber Subscriber { get; }
17 | private IDatabaseAsync Database { get; }
18 | private RedisLockOptions Options { get; }
19 |
20 | private ICacheStack? RegisteredStack { get; set; }
21 |
22 | internal ConcurrentDictionary> LockedOnKeyRefresh { get; }
23 |
24 | ///
25 | /// Creates a new instance of with the given and default lock options.
26 | ///
27 | /// The primary connection to Redis where the distributed lock will be co-ordinated through.
28 | public RedisLockExtension(IConnectionMultiplexer connection) : this(connection, RedisLockOptions.Default) { }
29 |
30 | ///
31 | /// Creates a new instance of with the given and .
32 | ///
33 | /// The primary connection to Redis where the distributed lock will be co-ordinated through.
34 | /// The lock options to configure the behaviour of locking.
35 | public RedisLockExtension(IConnectionMultiplexer connection, RedisLockOptions options)
36 | {
37 | if (connection == null)
38 | {
39 | throw new ArgumentNullException(nameof(connection));
40 | }
41 |
42 | Options = options;
43 | Database = connection.GetDatabase(options.DatabaseIndex);
44 | Subscriber = connection.GetSubscriber();
45 |
46 | LockedOnKeyRefresh = new ConcurrentDictionary>(StringComparer.Ordinal);
47 |
48 | Subscriber.Subscribe(GetRedisChannel(), (channel, value) =>
49 | {
50 | if (!value.IsNull)
51 | {
52 | UnlockWaitingTasks(value!);
53 | }
54 | });
55 | }
56 |
57 | private RedisChannel GetRedisChannel() => new(Options.RedisChannel, RedisChannel.PatternMode.Auto);
58 |
59 | ///
60 | public void Register(ICacheStack cacheStack)
61 | {
62 | if (RegisteredStack != null)
63 | {
64 | throw new InvalidOperationException($"{nameof(RedisLockExtension)} can only be registered to one {nameof(ICacheStack)}");
65 | }
66 |
67 | RegisteredStack = cacheStack;
68 | }
69 |
70 | private async ValueTask ReleaseLockAsync(string cacheKey)
71 | {
72 | var lockKey = string.Format(Options.KeyFormat, cacheKey);
73 | await Subscriber.PublishAsync(GetRedisChannel(), cacheKey, CommandFlags.FireAndForget).ConfigureAwait(false);
74 | await Database.KeyDeleteAsync(lockKey, CommandFlags.FireAndForget).ConfigureAwait(false);
75 | UnlockWaitingTasks(cacheKey);
76 | }
77 |
78 | private async ValueTask SpinWaitAsync(TaskCompletionSource taskCompletionSource, string lockKey)
79 | {
80 | var spinAttempt = 0;
81 | var maxSpinAttempts = Options.LockCheckStrategy.CalculateSpinAttempts(Options.LockTimeout);
82 | while (spinAttempt <= maxSpinAttempts && !taskCompletionSource.Task.IsCanceled && !taskCompletionSource.Task.IsCompleted)
83 | {
84 | spinAttempt++;
85 |
86 | var lockExists = await Database.KeyExistsAsync(lockKey).ConfigureAwait(false);
87 | if (lockExists)
88 | {
89 | await Task.Delay(Options.LockCheckStrategy.SpinTime).ConfigureAwait(false);
90 | continue;
91 | }
92 |
93 | taskCompletionSource.TrySetResult(true);
94 | return;
95 | }
96 |
97 | taskCompletionSource.TrySetCanceled();
98 | }
99 |
100 | private async ValueTask DelayWaitAsync(TaskCompletionSource taskCompletionSource)
101 | {
102 | await Task.Delay(Options.LockTimeout).ConfigureAwait(false);
103 | taskCompletionSource.TrySetCanceled();
104 | }
105 |
106 | ///
107 | public async ValueTask AwaitAccessAsync(string cacheKey)
108 | {
109 | var lockKey = string.Format(Options.KeyFormat, cacheKey);
110 | var hasLock = await Database.StringSetAsync(lockKey, RedisValue.EmptyString, expiry: Options.LockTimeout, when: When.NotExists).ConfigureAwait(false);
111 |
112 | if (hasLock)
113 | {
114 | return DistributedLock.Locked(cacheKey, ReleaseLockAsync);
115 | }
116 | else
117 | {
118 | var completionSource = LockedOnKeyRefresh.GetOrAdd(cacheKey, key =>
119 | {
120 | var taskCompletionSource = new TaskCompletionSource();
121 |
122 | if (Options.LockCheckStrategy.UseSpinLock)
123 | {
124 | _ = SpinWaitAsync(taskCompletionSource, lockKey);
125 | }
126 | else
127 | {
128 | _ = DelayWaitAsync(taskCompletionSource);
129 | }
130 |
131 | return taskCompletionSource;
132 | });
133 |
134 | //Last minute check to confirm whether waiting is required (in case the notification is missed)
135 | if (!await Database.KeyExistsAsync(lockKey).ConfigureAwait(false))
136 | {
137 | UnlockWaitingTasks(cacheKey);
138 | return DistributedLock.Unlocked(cacheKey);
139 | }
140 |
141 | await completionSource.Task.ConfigureAwait(false);
142 | return DistributedLock.Unlocked(cacheKey);
143 | }
144 | }
145 |
146 | private void UnlockWaitingTasks(string cacheKey)
147 | {
148 | if (LockedOnKeyRefresh.TryRemove(cacheKey, out var waitingTasks))
149 | {
150 | waitingTasks.TrySetResult(true);
151 | }
152 | }
153 | }
154 |
--------------------------------------------------------------------------------
/src/CacheTower.Extensions.Redis/RedisLockOptions.cs:
--------------------------------------------------------------------------------
1 | using System;
2 |
3 | namespace CacheTower.Extensions.Redis;
4 |
5 | ///
6 | /// Lock options for use by the .
7 | ///
8 | /// How long to wait on the lock before having it expire.
9 | /// The Redis channel to communicate unlocking events across.
10 | ///
11 | /// A compatible string used to create the lock key stored in Redis.
12 | /// The cache key is provided as argument {0}.
13 | ///
14 | ///
15 | /// The database index used for the Redis lock.
16 | /// If not specified, uses the default database as configured on the connection.
17 | ///
18 | ///
19 | /// The lock checking strategy to use, like pub/sub or spin lock, to detect lock release locally.
20 | /// The waiter on the lock also performs a tight loop to check for lock release.
21 | /// This can avoid the situation of a missed message on Redis pub/sub.
22 | ///
23 | public record struct RedisLockOptions(
24 | TimeSpan LockTimeout,
25 | string RedisChannel,
26 | string KeyFormat,
27 | int DatabaseIndex,
28 | LockCheckStrategy LockCheckStrategy
29 | )
30 | {
31 | ///
32 | /// The default options for .
33 | ///
34 | ///
35 | ///
36 | /// - : 1 minute
37 | /// - : "CacheTower.CacheLock"
38 | /// - : "Lock:{0}"
39 | /// - : The default database configured on the connection.
40 | /// - : Use Redis pub/sub notification to determine end of lock.
41 | ///
42 | ///
43 | public static readonly RedisLockOptions Default = new(
44 | LockTimeout: TimeSpan.FromMinutes(1),
45 | RedisChannel: "CacheTower.CacheLock",
46 | KeyFormat: "Lock:{0}",
47 | DatabaseIndex: -1,
48 | LockCheckStrategy: LockCheckStrategy.WithPubSubNotification()
49 | );
50 | }
51 |
52 | ///
53 | /// The lock checking strategy to use for the .
54 | ///
55 | public readonly struct LockCheckStrategy
56 | {
57 | ///
58 | /// Whether a "spin lock" strategy will be used.
59 | ///
60 | public readonly bool UseSpinLock { get; private init; }
61 | ///
62 | /// For spin lock strategies, the time to wait between lock checks.
63 | ///
64 | public readonly TimeSpan SpinTime { get; private init; }
65 |
66 | ///
67 | /// Use a Redis pub/sub notification lock checking strategy.
68 | ///
69 | ///
70 | public static LockCheckStrategy WithPubSubNotification() => new();
71 |
72 | ///
73 | /// Use a "spin lock" lock checking strategy.
74 | /// This can avoid the situation of a missed message on Redis pub/sub.
75 | ///
76 | /// The time to wait between lock checks.
77 | ///
78 | public static LockCheckStrategy WithSpinLock(TimeSpan spinTime)
79 | {
80 | return new LockCheckStrategy
81 | {
82 | UseSpinLock = true,
83 | SpinTime = spinTime
84 | };
85 | }
86 |
87 | internal int CalculateSpinAttempts(TimeSpan lockTimeout)
88 | {
89 | if (!UseSpinLock)
90 | {
91 | return 0;
92 | }
93 |
94 | return (int)Math.Ceiling(lockTimeout.TotalMilliseconds / SpinTime.TotalMilliseconds);
95 | }
96 | }
--------------------------------------------------------------------------------
/src/CacheTower.Extensions.Redis/RedisRemoteEvictionExtension.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Collections.Generic;
3 | using System.Threading.Tasks;
4 | using StackExchange.Redis;
5 |
6 | namespace CacheTower.Extensions.Redis
7 | {
8 | ///
9 | /// The broadcasts cache updates, evictions and flushes to Redis to allow for remote eviction of old cache data.
10 | /// When one of these events is received, it will perform that action locally to the configured cache layers.
11 | ///
12 | public class RedisRemoteEvictionExtension : ICacheChangeExtension
13 | {
14 | private ISubscriber Subscriber { get; }
15 | private RedisChannel FlushChannel { get; }
16 | private RedisChannel EvictionChannel { get; }
17 |
18 | private bool IsRegistered { get; set; }
19 |
20 | private readonly object LockObj = new object();
21 | private HashSet FlaggedEvictions { get; }
22 | private bool HasFlushTriggered { get; set; }
23 |
24 | ///
25 | /// Creates a new instance of .
26 | ///
27 | /// The primary connection to the Redis instance where the messages will be broadcast and received through.
28 | /// The channel prefix to use for the Redis communication.
29 | public RedisRemoteEvictionExtension(IConnectionMultiplexer connection, string channelPrefix = "CacheTower")
30 | {
31 | if (connection == null)
32 | {
33 | throw new ArgumentNullException(nameof(connection));
34 | }
35 |
36 | if (channelPrefix == null)
37 | {
38 | throw new ArgumentNullException(nameof(channelPrefix));
39 | }
40 |
41 | Subscriber = connection.GetSubscriber();
42 | FlushChannel = new($"{channelPrefix}.RemoteFlush", RedisChannel.PatternMode.Auto);
43 | EvictionChannel = new($"{channelPrefix}.RemoteEviction", RedisChannel.PatternMode.Auto);
44 | FlaggedEvictions = new HashSet(StringComparer.Ordinal);
45 | }
46 |
47 | ///
48 | /// This will broadcast to Redis that the cache entry belonging to is now out-of-date and should be evicted.
49 | ///
50 | ///
51 | public ValueTask OnCacheUpdateAsync(string cacheKey, DateTime expiry, CacheUpdateType cacheUpdateType)
52 | {
53 | if (cacheUpdateType == CacheUpdateType.AddOrUpdateEntry)
54 | {
55 | return FlagEvictionAsync(cacheKey);
56 | }
57 | return default;
58 | }
59 | ///
60 | /// This will broadcast to Redis that the cache entry belonging to is to be evicted.
61 | ///
62 | ///
63 | public ValueTask OnCacheEvictionAsync(string cacheKey)
64 | {
65 | return FlagEvictionAsync(cacheKey);
66 | }
67 |
68 | private async ValueTask FlagEvictionAsync(string cacheKey)
69 | {
70 | lock (LockObj)
71 | {
72 | FlaggedEvictions.Add(cacheKey);
73 | }
74 |
75 | await Subscriber.PublishAsync(EvictionChannel, cacheKey, CommandFlags.FireAndForget).ConfigureAwait(false);
76 | }
77 |
78 | ///
79 | /// This will broadcast to Redis that the cache should be flushed.
80 | ///
81 | ///
82 | public async ValueTask OnCacheFlushAsync()
83 | {
84 | lock (LockObj)
85 | {
86 | HasFlushTriggered = true;
87 | }
88 |
89 | await Subscriber.PublishAsync(FlushChannel, RedisValue.EmptyString, CommandFlags.FireAndForget).ConfigureAwait(false);
90 | }
91 |
92 | ///
93 | public void Register(ICacheStack cacheStack)
94 | {
95 | if (IsRegistered)
96 | {
97 | throw new InvalidOperationException($"{nameof(RedisRemoteEvictionExtension)} can only be registered to one {nameof(ICacheStack)}");
98 | }
99 | IsRegistered = true;
100 |
101 | Subscriber.Subscribe(EvictionChannel)
102 | .OnMessage(async (channelMessage) =>
103 | {
104 | if (channelMessage.Message.IsNull)
105 | {
106 | return;
107 | }
108 |
109 | string cacheKey = channelMessage.Message!;
110 |
111 | var shouldEvictLocally = false;
112 | lock (LockObj)
113 | {
114 | shouldEvictLocally = FlaggedEvictions.Remove(cacheKey) == false;
115 | }
116 |
117 | if (shouldEvictLocally)
118 | {
119 | var cacheLayers = ((IExtendableCacheStack)cacheStack).GetCacheLayers();
120 | for (var i = 0; i < cacheLayers.Count; i++)
121 | {
122 | var cacheLayer = cacheLayers[i];
123 | if (cacheLayer is ILocalCacheLayer)
124 | {
125 | await cacheLayer.EvictAsync(cacheKey).ConfigureAwait(false);
126 | }
127 | }
128 | }
129 | });
130 |
131 | Subscriber.Subscribe(FlushChannel)
132 | .OnMessage(async (channelMessage) =>
133 | {
134 | var shouldFlushLocally = false;
135 | lock (LockObj)
136 | {
137 | shouldFlushLocally = !HasFlushTriggered;
138 | HasFlushTriggered = false;
139 | }
140 |
141 | if (shouldFlushLocally)
142 | {
143 | var cacheLayers = ((IExtendableCacheStack)cacheStack).GetCacheLayers();
144 | for (var i = 0; i < cacheLayers.Count; i++)
145 | {
146 | var cacheLayer = cacheLayers[i];
147 | if (cacheLayer is ILocalCacheLayer)
148 | {
149 | await cacheLayer.FlushAsync().ConfigureAwait(false);
150 | }
151 | }
152 | }
153 | });
154 | }
155 | }
156 | }
157 |
--------------------------------------------------------------------------------
/src/CacheTower.Extensions.Redis/ServiceCollectionExtensions.cs:
--------------------------------------------------------------------------------
1 | using CacheTower;
2 | using CacheTower.Extensions.Redis;
3 | using StackExchange.Redis;
4 |
5 | namespace Microsoft.Extensions.DependencyInjection;
6 |
7 | ///
8 | /// Microsoft extensions for Cache Tower.
9 | ///
10 | public static class ServiceCollectionExtensions
11 | {
12 | ///
13 | /// Adds the to the with the specified and .
14 | ///
15 | ///
16 | /// The connection to the Redis server.
17 | ///
18 | public static ICacheStackBuilder WithRedisDistributedLocking(this ICacheStackBuilder builder, IConnectionMultiplexer connection)
19 | {
20 | return builder.WithRedisDistributedLocking(connection, RedisLockOptions.Default);
21 | }
22 |
23 | ///
24 | /// Adds the to the with the specified and .
25 | ///
26 | ///
27 | /// The connection to the Redis server.
28 | /// Options to configure the Redis distributed locking extension.
29 | ///
30 | public static ICacheStackBuilder WithRedisDistributedLocking(this ICacheStackBuilder builder, IConnectionMultiplexer connection, RedisLockOptions options)
31 | {
32 | builder.Extensions.Add(new RedisLockExtension(connection, options));
33 | return builder;
34 | }
35 |
36 | ///
37 | /// Adds the to the with the specified and .
38 | ///
39 | ///
40 | /// The extension will only evict from cache layers in the that implement .
41 | ///
42 | ///
43 | /// The connection to the Redis server.
44 | /// The channel prefix to use for the pub/sub calls to other instances.
45 | ///
46 | public static ICacheStackBuilder WithRedisRemoteEviction(this ICacheStackBuilder builder, IConnectionMultiplexer connection, string channelPrefix = "CacheTower")
47 | {
48 | builder.Extensions.Add(new RedisRemoteEvictionExtension(connection, channelPrefix));
49 | return builder;
50 | }
51 | }
52 |
--------------------------------------------------------------------------------
/src/CacheTower.Providers.Database.MongoDB/CacheTower.Providers.Database.MongoDB.csproj:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | netstandard2.0
5 | MongoDB Provider for Cache Tower
6 | Use MongoDB for caching with Cache Tower
7 | mongodb;$(PackageBaseTags)
8 | James Turner
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 | <_Parameter1>false
23 |
24 |
25 |
26 |
27 |
--------------------------------------------------------------------------------
/src/CacheTower.Providers.Database.MongoDB/Commands/CleanupCommand.cs:
--------------------------------------------------------------------------------
1 | using CacheTower.Providers.Database.MongoDB.Entities;
2 | using System;
3 | using System.Collections.Generic;
4 | using MongoDB.Driver;
5 | using MongoFramework.Infrastructure.Commands;
6 |
7 | namespace CacheTower.Providers.Database.MongoDB.Commands
8 | {
9 | internal class CleanupCommand : IWriteCommand
10 | {
11 | public Type EntityType => typeof(DbCachedEntry);
12 |
13 | public IEnumerable> GetModel(WriteModelOptions options)
14 | {
15 | var filter = Builders.Filter.Lt(e => e.Expiry, DateTime.UtcNow);
16 | yield return new DeleteManyModel(filter);
17 | }
18 | }
19 | }
20 |
--------------------------------------------------------------------------------
/src/CacheTower.Providers.Database.MongoDB/Commands/EvictCommand.cs:
--------------------------------------------------------------------------------
1 | using CacheTower.Providers.Database.MongoDB.Entities;
2 | using System;
3 | using System.Collections.Generic;
4 | using MongoDB.Driver;
5 | using MongoFramework.Infrastructure.Commands;
6 |
7 | namespace CacheTower.Providers.Database.MongoDB.Commands
8 | {
9 | internal class EvictCommand : IWriteCommand
10 | {
11 | private string CacheKey { get; }
12 |
13 | public Type EntityType => typeof(DbCachedEntry);
14 |
15 | public EvictCommand(string cacheKey)
16 | {
17 | CacheKey = cacheKey;
18 | }
19 |
20 | public IEnumerable> GetModel(WriteModelOptions options)
21 | {
22 | var filter = Builders.Filter.Eq(e => e.CacheKey, CacheKey);
23 | yield return new DeleteManyModel(filter);
24 | }
25 | }
26 | }
27 |
--------------------------------------------------------------------------------
/src/CacheTower.Providers.Database.MongoDB/Commands/FlushCommand.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Collections.Generic;
3 | using CacheTower.Providers.Database.MongoDB.Entities;
4 | using MongoDB.Driver;
5 | using MongoFramework.Infrastructure.Commands;
6 |
7 | namespace CacheTower.Providers.Database.MongoDB.Commands
8 | {
9 | internal class FlushCommand : IWriteCommand
10 | {
11 | public Type EntityType => typeof(DbCachedEntry);
12 |
13 | public IEnumerable> GetModel(WriteModelOptions options)
14 | {
15 | var filter = Builders.Filter.Empty;
16 | yield return new DeleteManyModel(filter);
17 | }
18 | }
19 | }
20 |
--------------------------------------------------------------------------------
/src/CacheTower.Providers.Database.MongoDB/Commands/SetCommand.cs:
--------------------------------------------------------------------------------
1 | using CacheTower.Providers.Database.MongoDB.Entities;
2 | using System;
3 | using System.Collections.Generic;
4 | using MongoDB.Driver;
5 | using MongoFramework.Infrastructure.Commands;
6 |
7 | namespace CacheTower.Providers.Database.MongoDB.Commands
8 | {
9 | internal class SetCommand : IWriteCommand
10 | {
11 | public DbCachedEntry Entry { get; }
12 |
13 | public Type EntityType => typeof(DbCachedEntry);
14 |
15 | public SetCommand(DbCachedEntry dbCachedEntry)
16 | {
17 | Entry = dbCachedEntry;
18 | }
19 |
20 | public IEnumerable> GetModel(WriteModelOptions options)
21 | {
22 | var filter = Builders.Filter.Eq(e => e.CacheKey, Entry.CacheKey);
23 | var updateDefinition = Builders.Update
24 | .Set(e => e.CacheKey, Entry.CacheKey)
25 | .Set(e => e.Expiry, Entry.Expiry)
26 | .Set(e => e.Value, Entry.Value);
27 |
28 | var model = new UpdateOneModel(filter, updateDefinition)
29 | {
30 | IsUpsert = true
31 | };
32 |
33 | yield return model;
34 | }
35 | }
36 | }
37 |
--------------------------------------------------------------------------------
/src/CacheTower.Providers.Database.MongoDB/Entities/DbCachedEntry.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Collections.Generic;
3 | using System.Text;
4 | using MongoDB.Bson;
5 | using MongoDB.Bson.Serialization.Attributes;
6 | using MongoDB.Bson.Serialization.IdGenerators;
7 | using MongoFramework.Attributes;
8 |
9 | namespace CacheTower.Providers.Database.MongoDB.Entities
10 | {
11 | internal class DbCachedEntry
12 | {
13 | public ObjectId Id { get; set; }
14 |
15 | [Index(MongoFramework.IndexSortOrder.Ascending)]
16 | public string? CacheKey { get; set; }
17 |
18 | [Index(MongoFramework.IndexSortOrder.Ascending)]
19 | public DateTime Expiry { get; set; }
20 |
21 | public object? Value { get; set; }
22 | }
23 | }
24 |
--------------------------------------------------------------------------------
/src/CacheTower.Providers.Database.MongoDB/MongoDbCacheLayer.cs:
--------------------------------------------------------------------------------
1 | using System.Linq;
2 | using System.Threading.Tasks;
3 | using CacheTower.Providers.Database.MongoDB.Commands;
4 | using CacheTower.Providers.Database.MongoDB.Entities;
5 | using MongoFramework;
6 | using MongoFramework.Infrastructure;
7 | using MongoFramework.Infrastructure.Indexing;
8 | using MongoFramework.Infrastructure.Linq;
9 | using MongoFramework.Infrastructure.Mapping;
10 |
11 | namespace CacheTower.Providers.Database.MongoDB
12 | {
13 | ///
14 | /// The allows caching through a MongoDB server.
15 | /// Cache entries are serialized to BSON using .
16 | ///
17 | ///
18 | public class MongoDbCacheLayer : IDistributedCacheLayer
19 | {
20 | private bool? IsDatabaseAvailable { get; set; }
21 |
22 | private IMongoDbConnection Connection { get; }
23 |
24 | private bool HasSetIndexes = false;
25 |
26 | static MongoDbCacheLayer()
27 | {
28 | //Due to a change in 2.19.0, we need to ensure that DbCachedEntry is registered early.
29 | //More importantly than the type itself is that the TypeDiscoverySerializer is registered
30 | //which is done automatically with use of type discovery serialization.
31 | //This may need to be revisited later with a future update to MongoFramework.
32 | _ = EntityMapping.RegisterType(typeof(DbCachedEntry));
33 | }
34 |
35 | ///
36 | /// Creates a new instance of with the given .
37 | ///
38 | /// The connection to the MongoDB database.
39 | public MongoDbCacheLayer(IMongoDbConnection connection)
40 | {
41 | Connection = connection;
42 | }
43 |
44 | private async ValueTask TryConfigureIndexes()
45 | {
46 | if (!HasSetIndexes)
47 | {
48 | HasSetIndexes = true;
49 | await EntityIndexWriter.ApplyIndexingAsync(Connection).ConfigureAwait(false);
50 | }
51 | }
52 |
53 | ///
54 | public async ValueTask CleanupAsync()
55 | {
56 | await TryConfigureIndexes().ConfigureAwait(false);
57 | await EntityCommandWriter.WriteAsync(Connection, new[] { new CleanupCommand() }, default).ConfigureAwait(false);
58 | }
59 |
60 | ///
61 | public async ValueTask EvictAsync(string cacheKey)
62 | {
63 | await TryConfigureIndexes().ConfigureAwait(false);
64 | await EntityCommandWriter.WriteAsync(Connection, new[] { new EvictCommand(cacheKey) }, default).ConfigureAwait(false);
65 | }
66 |
67 | ///
68 | public async ValueTask FlushAsync()
69 | {
70 | await EntityCommandWriter.WriteAsync(Connection, new[] { new FlushCommand() }, default).ConfigureAwait(false);
71 | }
72 |
73 | ///
74 | public async ValueTask?> GetAsync(string cacheKey)
75 | {
76 | await TryConfigureIndexes().ConfigureAwait(false);
77 |
78 | var provider = new MongoFrameworkQueryProvider(Connection);
79 | var queryable = new MongoFrameworkQueryable(provider);
80 |
81 | var dbEntry = queryable.Where(e => e.CacheKey == cacheKey).FirstOrDefault();
82 | var cacheEntry = default(CacheEntry);
83 |
84 | if (dbEntry != default)
85 | {
86 | cacheEntry = new CacheEntry((T)dbEntry.Value!, dbEntry.Expiry);
87 | }
88 |
89 | return cacheEntry;
90 | }
91 |
92 | ///
93 | public async ValueTask SetAsync(string cacheKey, CacheEntry cacheEntry)
94 | {
95 | await TryConfigureIndexes().ConfigureAwait(false);
96 | var command = new SetCommand(new DbCachedEntry
97 | {
98 | CacheKey = cacheKey,
99 | Expiry = cacheEntry.Expiry,
100 | Value = cacheEntry.Value!
101 | });
102 |
103 | await EntityCommandWriter.WriteAsync(Connection, new[] { command }, default).ConfigureAwait(false);
104 | }
105 |
106 | ///
107 | public async ValueTask IsAvailableAsync(string cacheKey)
108 | {
109 | if (IsDatabaseAvailable == null)
110 | {
111 | try
112 | {
113 | await TryConfigureIndexes().ConfigureAwait(false);
114 | IsDatabaseAvailable = true;
115 | }
116 | catch
117 | {
118 | IsDatabaseAvailable = false;
119 | }
120 | }
121 |
122 | return IsDatabaseAvailable.Value;
123 | }
124 | }
125 | }
126 |
--------------------------------------------------------------------------------
/src/CacheTower.Providers.Database.MongoDB/ServiceCollectionExtensions.cs:
--------------------------------------------------------------------------------
1 | using CacheTower;
2 | using CacheTower.Providers.Database.MongoDB;
3 | using MongoFramework;
4 |
5 | namespace Microsoft.Extensions.DependencyInjection;
6 |
7 | ///
8 | /// Microsoft extensions for Cache Tower.
9 | ///
10 | public static class ServiceCollectionExtensions
11 | {
12 | ///
13 | /// Adds a to the with the specified .
14 | ///
15 | ///
16 | /// The connection to the MongoDB server.
17 | ///
18 | public static ICacheStackBuilder AddMongoDbCacheLayer(this ICacheStackBuilder builder, IMongoDbConnection connection)
19 | {
20 | builder.CacheLayers.Add(new MongoDbCacheLayer(connection));
21 | return builder;
22 | }
23 | }
24 |
--------------------------------------------------------------------------------
/src/CacheTower.Providers.FileSystem.Json/CacheTower.Providers.FileSystem.Json.csproj:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | netstandard2.0
5 | JSON File Provider for Cache Tower
6 | Use JSON serialized files for caching with Cache Tower
7 | json;filesystem;$(PackageBaseTags)
8 | James Turner
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
--------------------------------------------------------------------------------
/src/CacheTower.Providers.FileSystem.Json/JsonFileCacheLayer.cs:
--------------------------------------------------------------------------------
1 | using CacheTower.Serializers.NewtonsoftJson;
2 | using System;
3 |
4 | namespace CacheTower.Providers.FileSystem.Json
5 | {
6 | ///
7 | /// The uses Newtonsoft.Json to serialize and deserialize the cache items to the file system.
8 | ///
9 | ///
10 | [Obsolete("Use FileCacheLayer and specify the NewtonsoftJsonCacheSerializer. This cache layer (and the associated package) will be discontinued in a future release.")]
11 | public class JsonFileCacheLayer : FileCacheLayer, ICacheLayer
12 | {
13 | ///
14 | /// Creates a , using the given as the location to store the cache.
15 | ///
16 | ///
17 | public JsonFileCacheLayer(string directoryPath) : base(new FileCacheLayerOptions(directoryPath, NewtonsoftJsonCacheSerializer.Instance)) { }
18 | }
19 | }
20 |
--------------------------------------------------------------------------------
/src/CacheTower.Providers.FileSystem.Protobuf/CacheTower.Providers.FileSystem.Protobuf.csproj:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | netstandard2.0
5 | Protobuf File Provider for Cache Tower
6 | Use Protobuf serialized files for caching with Cache Tower
7 | protobuf;filesystem;$(PackageBaseTags)
8 | James Turner
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
--------------------------------------------------------------------------------
/src/CacheTower.Providers.FileSystem.Protobuf/ProtobufFileCacheLayer.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using CacheTower.Serializers.Protobuf;
3 |
4 | namespace CacheTower.Providers.FileSystem.Protobuf
5 | {
6 | ///
7 | /// The uses protobuf-net to serialize and deserialize the cache items to the file system.
8 | ///
9 | /// When caching custom types, you will need to decorate your class with [ProtoContact] and [ProtoMember] attributes per protobuf-net's documentation.
10 | /// While this can be inconvienent, using protobuf-net ensures high performance and low allocations for serializing.
11 | ///
12 | ///
13 | ///
14 | [Obsolete("Use FileCacheLayer directly and specify the ProtobufCacheSerializer. This cache layer (and the associated package) will be discontinued in a future release.")]
15 | public class ProtobufFileCacheLayer : FileCacheLayer
16 | {
17 | ///
18 | /// Creates a , using the given as the location to store the cache.
19 | ///
20 | ///
21 | public ProtobufFileCacheLayer(string directoryPath) : base(new FileCacheLayerOptions(directoryPath, ProtobufCacheSerializer.Instance)) { }
22 | }
23 | }
24 |
--------------------------------------------------------------------------------
/src/CacheTower.Providers.Redis/CacheTower.Providers.Redis.csproj:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | netstandard2.0
5 | Redis Provider for Cache Tower
6 | Use Redis for caching with Cache Tower
7 | redis;$(PackageBaseTags)
8 | James Turner
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
--------------------------------------------------------------------------------
/src/CacheTower.Providers.Redis/IsExternalInit.cs:
--------------------------------------------------------------------------------
1 | using System.ComponentModel;
2 |
3 | namespace System.Runtime.CompilerServices
4 | {
5 | ///
6 | /// Reserved to be used by the compiler for tracking metadata.
7 | /// This class should not be used by developers in source code.
8 | ///
9 | [EditorBrowsable(EditorBrowsableState.Never)]
10 | internal static class IsExternalInit
11 | {
12 | }
13 | }
--------------------------------------------------------------------------------
/src/CacheTower.Providers.Redis/RedisCacheLayer.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.IO;
3 | using System.Threading.Tasks;
4 | using CacheTower.Serializers.Protobuf;
5 | using StackExchange.Redis;
6 |
7 | namespace CacheTower.Providers.Redis
8 | {
9 | ///
10 | public class RedisCacheLayer : IDistributedCacheLayer
11 | {
12 | private IConnectionMultiplexer Connection { get; }
13 | private IDatabaseAsync Database { get; }
14 | private readonly RedisCacheLayerOptions Options;
15 |
16 | ///
17 | /// Creates a new instance of with the given and .
18 | /// If using this constructor, Protobuf encoding will be used.
19 | ///
20 | /// The primary connection to Redis where the cache will be stored.
21 | ///
22 | /// The database index to use for Redis.
23 | /// If not specified, uses the default database as configured on the .
24 | ///
25 | [Obsolete("Use other constructor. Specifying cache serializers will become the default behaviour going forward.")]
26 | public RedisCacheLayer(IConnectionMultiplexer connection, int databaseIndex = -1) : this(connection, new RedisCacheLayerOptions(ProtobufCacheSerializer.Instance, databaseIndex))
27 | {
28 | }
29 |
30 | ///
31 | /// Creates a new instance of with the given and .
32 | ///
33 | /// The primary connection to Redis where the cache will be stored.
34 | /// Various options that control the behaviour of the .
35 | public RedisCacheLayer(IConnectionMultiplexer connection, RedisCacheLayerOptions options)
36 | {
37 | Connection = connection;
38 | Database = connection.GetDatabase(options.DatabaseIndex);
39 | Options = options;
40 | }
41 |
42 | ///
43 | ///
44 | /// Cleanup is unnecessary for the as Redis handles removing expired keys automatically.
45 | ///
46 | public ValueTask CleanupAsync()
47 | {
48 | //Noop as Redis handles this directly
49 | return new ValueTask();
50 | }
51 |
52 | ///
53 | public async ValueTask EvictAsync(string cacheKey)
54 | {
55 | await Database.KeyDeleteAsync(cacheKey).ConfigureAwait(false);
56 | }
57 |
58 | ///
59 | ///
60 | /// Flushing the performs a database flush in Redis.
61 | /// Every key associated to the database index will be removed.
62 | ///
63 | public async ValueTask FlushAsync()
64 | {
65 | var redisEndpoints = Connection.GetEndPoints();
66 | foreach (var endpoint in redisEndpoints)
67 | {
68 | await Connection.GetServer(endpoint).FlushDatabaseAsync(Options.DatabaseIndex).ConfigureAwait(false);
69 | }
70 | }
71 |
72 | ///
73 | public async ValueTask?> GetAsync(string cacheKey)
74 | {
75 | var redisValue = await Database.StringGetAsync(cacheKey).ConfigureAwait(false);
76 | if (redisValue != RedisValue.Null)
77 | {
78 | using var stream = new MemoryStream(redisValue);
79 | return Options.Serializer.Deserialize>(stream);
80 | }
81 |
82 | return default;
83 | }
84 |
85 | ///
86 | public ValueTask IsAvailableAsync(string cacheKey)
87 | {
88 | return new ValueTask(Connection.IsConnected);
89 | }
90 |
91 | ///
92 | public async ValueTask SetAsync(string cacheKey, CacheEntry cacheEntry)
93 | {
94 | var expiryOffset = cacheEntry.Expiry - DateTime.UtcNow;
95 | if (expiryOffset < TimeSpan.Zero)
96 | {
97 | return;
98 | }
99 |
100 | using var stream = new MemoryStream();
101 | Options.Serializer.Serialize(stream, cacheEntry);
102 | stream.Seek(0, SeekOrigin.Begin);
103 | var redisValue = RedisValue.CreateFrom(stream);
104 | await Database.StringSetAsync(cacheKey, redisValue, expiryOffset).ConfigureAwait(false);
105 | }
106 | }
107 | }
108 |
--------------------------------------------------------------------------------
/src/CacheTower.Providers.Redis/RedisCacheLayerOptions.cs:
--------------------------------------------------------------------------------
1 | namespace CacheTower.Providers.Redis;
2 |
3 | ///
4 | /// Options for controlling a .
5 | ///
6 | /// The serializer to use for the data.
7 | ///
8 | /// The database index used for the cached data.
9 | /// If none is specified, uses the default database as configured on the connection.
10 | ///
11 | public readonly record struct RedisCacheLayerOptions(
12 | ICacheSerializer Serializer,
13 | int DatabaseIndex = -1
14 | );
15 |
--------------------------------------------------------------------------------
/src/CacheTower.Providers.Redis/ServiceCollectionExtensions.cs:
--------------------------------------------------------------------------------
1 | using CacheTower;
2 | using CacheTower.Providers.Redis;
3 | using StackExchange.Redis;
4 |
5 | namespace Microsoft.Extensions.DependencyInjection;
6 |
7 | ///
8 | /// Microsoft extensions for Cache Tower.
9 | ///
10 | public static class ServiceCollectionExtensions
11 | {
12 | ///
13 | /// Adds a to the with the specified and .
14 | ///
15 | ///
16 | /// The connection to the MongoDB server.
17 | /// The options for configuring serializer and database index.
18 | ///
19 | public static ICacheStackBuilder AddRedisCacheLayer(this ICacheStackBuilder builder, IConnectionMultiplexer connection, RedisCacheLayerOptions options)
20 | {
21 | builder.CacheLayers.Add(new RedisCacheLayer(connection, options));
22 | return builder;
23 | }
24 | }
25 |
--------------------------------------------------------------------------------
/src/CacheTower.Serializers.NewtonsoftJson/CacheTower.Serializers.NewtonsoftJson.csproj:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | netstandard2.0
5 | Newtonsoft.Json Serializer for Cache Tower
6 | Newtonsoft.Json cache serialization for Cache Tower
7 | newtonsoft;json;$(PackageBaseTags)
8 | James Turner
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
--------------------------------------------------------------------------------
/src/CacheTower.Serializers.NewtonsoftJson/NewtonsoftJsonCacheSerializer.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.IO;
3 | using System.Text;
4 | using Newtonsoft.Json;
5 |
6 | namespace CacheTower.Serializers.NewtonsoftJson;
7 |
8 | ///
9 | /// Allows serializing to and from JSON via Newtonsoft.Json
10 | ///
11 | public class NewtonsoftJsonCacheSerializer : ICacheSerializer
12 | {
13 | private readonly JsonSerializer serializer;
14 |
15 | ///
16 | /// An existing instance of .
17 | ///
18 | public static NewtonsoftJsonCacheSerializer Instance { get; } = new();
19 |
20 | private NewtonsoftJsonCacheSerializer()
21 | {
22 | serializer = new JsonSerializer();
23 | }
24 |
25 | ///
26 | /// Creates a new instance of with the specified .
27 | ///
28 | public NewtonsoftJsonCacheSerializer(JsonSerializerSettings settings)
29 | {
30 | serializer = JsonSerializer.Create(settings);
31 | }
32 |
33 | ///
34 | public void Serialize(Stream stream, T? value)
35 | {
36 | try
37 | {
38 | using var streamWriter = new StreamWriter(stream, Encoding.UTF8, 1024, true);
39 | using var jsonWriter = new JsonTextWriter(streamWriter);
40 | serializer.Serialize(jsonWriter, value);
41 | }
42 | catch (Exception ex)
43 | {
44 | throw new CacheSerializationException("A serialization error has occurred when serializing with Newtonsoft.Json", ex);
45 | }
46 | }
47 |
48 | ///
49 | public T? Deserialize(Stream stream)
50 | {
51 | try
52 | {
53 | using var streamReader = new StreamReader(stream, Encoding.UTF8, false, 1024);
54 | using var jsonReader = new JsonTextReader(streamReader);
55 | return serializer.Deserialize(jsonReader);
56 | }
57 | catch (Exception ex)
58 | {
59 | throw new CacheSerializationException("A serialization error has occurred when deserializing with Newtonsoft.Json", ex);
60 | }
61 | }
62 | }
63 |
--------------------------------------------------------------------------------
/src/CacheTower.Serializers.Protobuf/CacheTower.Serializers.Protobuf.csproj:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | netstandard2.0
5 | Protobuf Serializer for Cache Tower
6 | Protobuf cache serialization for Cache Tower
7 | protobuf;$(PackageBaseTags)
8 | James Turner
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
--------------------------------------------------------------------------------
/src/CacheTower.Serializers.Protobuf/ProtobufCacheSerializer.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.IO;
3 | using CacheTower.Providers.FileSystem;
4 | using ProtoBuf;
5 | using ProtoBuf.Meta;
6 |
7 | namespace CacheTower.Serializers.Protobuf
8 | {
9 | ///
10 | /// Allows serializing to and from Protobuf format via protobuf-net.
11 | ///
12 | ///
13 | ///
14 | /// When caching custom types, you will need to decorate your class with [ProtoContact] and [ProtoMember] attributes per protobuf-net's documentation.
15 | /// Additionally, as the Protobuf format doesn't have a way to represent an empty collection, these will be returned as null.
16 | ///
17 | ///
18 | public class ProtobufCacheSerializer : ICacheSerializer
19 | {
20 | static ProtobufCacheSerializer()
21 | {
22 | RuntimeTypeModel.Default.Add(applyDefaultBehaviour: false)
23 | .Add(1, nameof(ManifestEntry.FileName))
24 | .Add(2, nameof(ManifestEntry.Expiry));
25 | }
26 |
27 | ///
28 | /// An existing instance of .
29 | ///
30 | public static ProtobufCacheSerializer Instance { get; } = new();
31 |
32 | //Because we can't use an open generic for protobuf-net (see https://github.com/protobuf-net/protobuf-net/issues/802)
33 | //we instead use/abuse a static class with a generic parameter to dynamically create the serialization config for us.
34 | private static class SerializerConfig
35 | {
36 | static SerializerConfig()
37 | {
38 | if (typeof(ICacheEntry).IsAssignableFrom(typeof(T)))
39 | {
40 | RuntimeTypeModel.Default.Add(typeof(T), applyDefaultBehaviour: false)
41 | .Add(1, nameof(CacheEntry