• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

Aldaviva / Unfucked / 24123555785

08 Apr 2026 07:31AM UTC coverage: 47.989% (+4.1%) from 43.864%
24123555785

push

github

Aldaviva
Fix CS8634 warning due to compiler not emitting [Nullability(2)] annotation on extension block generic type placeholder constraint "class?"

663 of 1750 branches covered (37.89%)

0 of 9 new or added lines in 1 file covered. (0.0%)

4 existing lines in 2 files now uncovered.

1169 of 2436 relevant lines covered (47.99%)

191.79 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

97.46
/Caching/InMemoryCache.cs
1
using System.Collections.Concurrent;
2
using System.Timers;
3
using Timer = System.Timers.Timer;
4

5
namespace Unfucked.Caching;
6

7
public sealed class InMemoryCache<K, V>: Cache<K, V> where K: notnull {
8

9
    public event RemovalNotification<K, V>? Removal;
10

11
    private readonly CacheOptions                           options;
12
    private readonly Func<K, ValueTask<V>>?                 defaultLoader;
13
    private readonly ConcurrentDictionary<K, CacheEntry<V>> cache;
14
    private readonly Timer?                                 expirationTimer;
15

16
    private volatile bool isDisposed;
17

18
    public InMemoryCache(CacheOptions? options = null, Func<K, ValueTask<V>>? loader = null) {
15✔
19
        this.options = (options ?? new CacheOptions()) with {
15!
20
            ConcurrencyLevel = this.options.ConcurrencyLevel is > 0 and var c ? c : Environment.ProcessorCount,
15✔
21
            InitialCapacity = this.options.InitialCapacity is > 0 and var i ? i : 31
15✔
22
        };
15✔
23

24
        defaultLoader = loader;
15✔
25
        cache         = new ConcurrentDictionary<K, CacheEntry<V>>(this.options.ConcurrencyLevel, this.options.InitialCapacity);
15✔
26

27
        if (this.options.ExpireAfterWrite > TimeSpan.Zero || this.options.ExpireAfterRead > TimeSpan.Zero) {
15✔
28
            TimeSpan expirationScanInterval = this.options.ExpirationScanInterval is { Ticks: > 0 } interval ? interval : TimeSpan.FromMinutes(1);
5✔
29
            expirationTimer         =  new Timer(expirationScanInterval.TotalMilliseconds) { AutoReset = false };
5✔
30
            expirationTimer.Elapsed += ScanForExpirations;
5✔
31
            expirationTimer.Start();
5✔
32
        }
33
    }
15✔
34

35
    /// <inheritdoc />
36
    public long Count => cache.Count;
14✔
37

38
    /// <inheritdoc />
39
    public async Task<V> Get(K key, Func<K, ValueTask<V>>? loader = null) {
40
        CacheEntry<V> cacheEntry = cache.GetOrAdd(key, ValueFactory);
18✔
41
        if (cacheEntry.IsNew) {
18✔
42
            await cacheEntry.ValueLock.WaitAsync().ConfigureAwait(false);
12✔
43
            try {
44
                if (cacheEntry.IsNew) {
12✔
45
                    cacheEntry.Value = await LoadValue(key, loader ?? defaultLoader).ConfigureAwait(false);
12✔
46
                    cacheEntry.LastWritten.Start();
5✔
47
                    cacheEntry.RefreshTimer?.Start();
5✔
48
                    cacheEntry.IsNew = false;
5✔
49
                }
50
            } catch (KeyNotFoundException) {
12✔
51
                cache.TryRemove(key, out _);
7✔
52
                cacheEntry.Dispose();
7✔
53
                throw;
7✔
54
            } finally {
55
                if (!cacheEntry.IsDisposed) {
12✔
56
                    cacheEntry.ValueLock.Release();
5✔
57
                }
58
            }
59
        } else if (IsExpired(cacheEntry)) {
6✔
60
            V? oldValue = default;
2✔
61
            await cacheEntry.ValueLock.WaitAsync().ConfigureAwait(false);
2✔
62
            try {
63
                if (IsExpired(cacheEntry)) {
2✔
64
                    cacheEntry.RefreshTimer?.Stop();
2!
65
                    try {
66
                        oldValue         = cacheEntry.Value;
2✔
67
                        cacheEntry.Value = await LoadValue(key, loader ?? defaultLoader).ConfigureAwait(false);
2✔
68
                        cacheEntry.LastWritten.Restart();
1✔
69
                    } finally {
1✔
70
                        cacheEntry.RefreshTimer?.Start();
2!
71
                    }
72
                }
73
            } finally {
1✔
74
                cacheEntry.ValueLock.Release();
2✔
75
            }
76

77
            if (oldValue is not null) {
1✔
78
                Removal?.Invoke(this, key, oldValue, RemovalCause.Expired);
1!
79
            }
80
        }
1✔
81

82
        cacheEntry.LastRead.Restart();
10✔
83
        return cacheEntry.Value;
10✔
84
    }
10✔
85

86
    /// <inheritdoc />
87
    public async Task Put(K key, V value) {
88
        CacheEntry<V> cacheEntry = cache.GetOrAdd(key, ValueFactory);
15✔
89
        await cacheEntry.ValueLock.WaitAsync().ConfigureAwait(false);
15✔
90
        V? removedValue = default;
15✔
91
        try {
92
            cacheEntry.RefreshTimer?.Stop();
15!
93
            if (cacheEntry.IsNew) {
15✔
94
                cacheEntry.IsNew = false;
14✔
95
            } else {
96
                removedValue = cacheEntry.Value;
1✔
97
            }
98

99
            cacheEntry.Value = value;
15✔
100
            cacheEntry.LastWritten.Restart();
15✔
101
            cacheEntry.RefreshTimer?.Start();
15!
102
        } finally {
15✔
103
            cacheEntry.ValueLock.Release();
15✔
104
        }
105

106
        if (removedValue is not null) {
15✔
107
            Removal?.Invoke(this, key, removedValue, RemovalCause.Replaced);
15!
108
        }
109
    }
15✔
110

111
    private CacheEntry<V> ValueFactory(K key) {
112
        bool   hasLoader    = defaultLoader != null;
26✔
113
        Timer? refreshTimer = options.RefreshAfterWrite > TimeSpan.Zero && hasLoader ? new Timer(options.RefreshAfterWrite.TotalMilliseconds) { AutoReset = false, Enabled = false } : null;
26✔
114
        var    entry        = new CacheEntry<V>(refreshTimer);
26✔
115

116
        if (entry.RefreshTimer != null) {
26✔
117
            async void refreshEntry(object o, ElapsedEventArgs elapsedEventArgs) {
118
                if (!entry.IsDisposed) {
3!
119
                    try {
120
                        V oldValue;
121
                        await entry.ValueLock.WaitAsync().ConfigureAwait(false);
3✔
122
                        try {
123
                            oldValue    = entry.Value;
3✔
124
                            entry.Value = await defaultLoader!(key).ConfigureAwait(false);
3✔
125
                            entry.LastWritten.Restart();
3✔
126
                            entry.RefreshTimer.Start();
3✔
127
                        } finally {
2✔
128
                            entry.ValueLock.Release();
3✔
129
                        }
130
                        Removal?.Invoke(this, key, oldValue, RemovalCause.Replaced);
2!
131
                    } catch (ObjectDisposedException) {}
4✔
132
                } else {
133
                    entry.RefreshTimer.Elapsed -= refreshEntry;
×
134
                }
135
            }
3✔
136

137
            entry.RefreshTimer.Elapsed += refreshEntry;
1✔
138
        }
139

140
        return entry;
26✔
141
    }
142

143
    /// <exception cref="System.Collections.Generic.KeyNotFoundException">a value with the key <typeparamref name="K"/> was not found, and no <paramref name="loader"/> was not provided</exception>
144
    private static ValueTask<V> LoadValue(K key, Func<K, ValueTask<V>>? loader) {
145
        if (loader != null) {
14✔
146
            return loader(key);
6✔
147
        } else {
148
            throw KeyNotFoundException(key);
8✔
149
        }
150
    }
151

152
    private static KeyNotFoundException KeyNotFoundException(K key) => new(
8✔
153
        $"Value with key {key} not found in cache, and a loader function was not provided when constructing the {nameof(InMemoryCache<K, V>)} or getting the value.");
8✔
154

155
    private bool IsExpired(CacheEntry<V> cacheEntry) =>
156
        (options.ExpireAfterWrite > TimeSpan.Zero && options.ExpireAfterWrite <= cacheEntry.LastWritten.Elapsed)
15!
157
        || (options.ExpireAfterRead > TimeSpan.Zero && options.ExpireAfterRead <= cacheEntry.LastRead.Elapsed);
15✔
158

159
    /// <inheritdoc />
160
    public void CleanUp() {
161
        foreach (KeyValuePair<K, CacheEntry<V>> entry in cache.Where(pair => IsExpired(pair.Value))) {
16✔
162
            entry.Value.ValueLock.Wait();
3✔
163
            bool removed = false;
3✔
164
            if (IsExpired(entry.Value)) {
3✔
165
                //this will probably throw a concurrent modification exception
166
                removed = cache.TryRemove(entry.Key, out _);
3✔
167
                if (removed) {
3✔
168
                    entry.Value.Dispose();
3✔
169
                    Removal?.Invoke(this, entry.Key, entry.Value.Value, RemovalCause.Expired);
3✔
170
                }
171
            }
172

173
            if (!removed) {
3!
174
                /*
175
                 * First pass showed entry as expired, but it was concurrently loaded or refreshed before this second pass check, so don't actually remove it.
176
                 * Only release if we didn't remove, because if we removed the entry and its lock were already disposed.
177
                 */
178
                entry.Value.ValueLock.Release();
×
179
            }
180
        }
181
    }
3✔
182

183
    private void ScanForExpirations(object? sender = null, ElapsedEventArgs? e = null) {
184
        CleanUp();
1✔
185
        expirationTimer!.Start();
1✔
UNCOV
186
    }
×
187

188
    /// <inheritdoc />
189
    public void Invalidate(params IEnumerable<K> keys) {
190
        foreach (K key in keys) {
10✔
191
            if (cache.TryRemove(key, out CacheEntry<V>? removedEntry)) {
3✔
192
                Removal?.Invoke(this, key, removedEntry.Value, RemovalCause.Explicit);
3!
193
                removedEntry.Dispose();
3✔
194
            }
195
        }
196
    }
2✔
197

198
    /// <inheritdoc />
199
    public void InvalidateAll() {
200
        KeyValuePair<K, CacheEntry<V>>[] toDispose = cache.ToArray();
16✔
201
        cache.Clear();
16✔
202
        foreach (KeyValuePair<K, CacheEntry<V>> entry in toDispose) {
58✔
203
            if (!isDisposed) {
13✔
204
                Removal?.Invoke(this, entry.Key, entry.Value.Value, RemovalCause.Explicit);
3!
205
            }
206
            entry.Value.Dispose();
13✔
207
        }
208
    }
16✔
209

210
    /// <inheritdoc />
211
    public void Dispose() {
212
        isDisposed = true;
15✔
213
        if (expirationTimer != null) {
15✔
214
            expirationTimer.Elapsed -= ScanForExpirations;
5✔
215
            expirationTimer.Dispose();
5✔
216
        }
217
        InvalidateAll();
15✔
218
    }
15✔
219

220
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc