forked from zzzprojects/System.Linq.Dynamic.Core
-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathThreadSafeSlidingCache.cs
More file actions
135 lines (116 loc) · 4.74 KB
/
ThreadSafeSlidingCache.cs
File metadata and controls
135 lines (116 loc) · 4.74 KB
1
2
3
4
5
6
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
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
using System.Collections.Concurrent;
using System.Linq.Dynamic.Core.Validation;
using System.Threading.Tasks;
namespace System.Linq.Dynamic.Core.Util
{
internal static class ThreadSafeSlidingCacheConstants
{
// Default cleanup frequency
public static readonly TimeSpan DefaultCleanupFrequency = TimeSpan.FromMinutes(10);
}
internal class ThreadSafeSlidingCache<TKey, TValue> where TKey : notnull where TValue : notnull
{
private readonly ConcurrentDictionary<TKey, (TValue Value, DateTime ExpirationTime)> _cache;
private readonly TimeSpan _cleanupFrequency;
private readonly IDateTimeUtils _dateTimeProvider;
private readonly Func<Task> _deleteExpiredCachedItemsDelegate;
private readonly long? _minCacheItemsBeforeCleanup;
private DateTime _lastCleanupTime = DateTime.MinValue;
/// <summary>
/// Sliding Thread Safe Cache
/// </summary>
/// <param name="timeToLive">The length of time any object would survive before being removed</param>
/// <param name="cleanupFrequency">Only look for expired objects over specific periods</param>
/// <param name="minCacheItemsBeforeCleanup">
/// If defined, only allow the cleanup process after x number of cached items have
/// been stored
/// </param>
/// <param name="dateTimeProvider">
/// Provides the Time for the Caching object. Default will be created if not supplied. Used
/// for Testing classes
/// </param>
public ThreadSafeSlidingCache(
TimeSpan timeToLive,
TimeSpan? cleanupFrequency = null,
long? minCacheItemsBeforeCleanup = null,
IDateTimeUtils? dateTimeProvider = null)
{
_cache = new ConcurrentDictionary<TKey, (TValue, DateTime)>();
TimeToLive = timeToLive;
_minCacheItemsBeforeCleanup = minCacheItemsBeforeCleanup;
_cleanupFrequency = cleanupFrequency ?? ThreadSafeSlidingCacheConstants.DefaultCleanupFrequency;
_deleteExpiredCachedItemsDelegate = Cleanup;
_dateTimeProvider = dateTimeProvider ?? new DateTimeUtils();
}
public TimeSpan TimeToLive { get; }
/// <summary>
/// Provide the number of items in the cache
/// </summary>
public int Count => _cache.Count;
public void AddOrUpdate(TKey key, TValue value)
{
Check.NotNull(key);
Check.NotNull(value);
var expirationTime = _dateTimeProvider.UtcNow.Add(TimeToLive);
_cache[key] = (value, expirationTime);
CleanupIfNeeded();
}
public bool TryGetValue(TKey key, out TValue value)
{
Check.NotNull(key);
CleanupIfNeeded();
if (_cache.TryGetValue(key, out var valueAndExpiration))
{
if (_dateTimeProvider.UtcNow <= valueAndExpiration.ExpirationTime)
{
value = valueAndExpiration.Value;
_cache[key] = (value, _dateTimeProvider.UtcNow.Add(TimeToLive));
return true;
}
// Remove expired item
_cache.TryRemove(key, out _);
}
value = default!;
return false;
}
public bool Remove(TKey key)
{
Check.NotNull(key);
var removed = _cache.TryRemove(key, out _);
CleanupIfNeeded();
return removed;
}
/// <summary>
/// Check if cache needs to be cleaned up.
/// If it does, span the cleanup as a Task to prevent from blocking
/// </summary>
private void CleanupIfNeeded()
{
if (_dateTimeProvider.UtcNow - _lastCleanupTime > _cleanupFrequency
&& (_minCacheItemsBeforeCleanup == null ||
_cache.Count >=
_minCacheItemsBeforeCleanup) // Only cleanup if we have a minimum number of items in the cache.
)
{
// Set here, so we don't have re-entry due to large collection enumeration.
_lastCleanupTime = _dateTimeProvider.UtcNow;
Task.Run(_deleteExpiredCachedItemsDelegate);
}
}
/// <summary>
/// Cleanup the Cache
/// </summary>
/// <returns></returns>
private Task Cleanup()
{
foreach (var key in _cache.Keys)
{
if (_dateTimeProvider.UtcNow > _cache[key].ExpirationTime)
{
_cache.TryRemove(key, out _);
}
}
return Task.CompletedTask;
}
}
}