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

Aldaviva / Unfucked / 28753990451

05 Jul 2026 08:14PM UTC coverage: 43.937% (-0.4%) from 44.313%
28753990451

push

github

Aldaviva
Added Process.CommandLine and .CommandLineSplit extension properties to get the command line (filename and arguments) of a process not started by this process

666 of 1901 branches covered (35.03%)

3 of 40 new or added lines in 2 files covered. (7.5%)

1 existing line in 1 file now uncovered.

1163 of 2647 relevant lines covered (43.94%)

176.64 hits per line

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

96.88
/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
/// <summary>Strongly-typed asynchronous in-memory cache that can encapsulate automatic value loading logic into the cache itself, instead of duplicating it across every single call site. Supports expiration after read or write, and periodic refresh for expired values.</summary>
8
/// <remarks>Inspired by Guava Cache.</remarks>
9
/// <typeparam name="K">Type of the cache key.</typeparam>
10
/// <typeparam name="V">Type of the cached values.</typeparam>
11
public sealed class InMemoryCache<K, V>: Cache<K, V> where K: notnull {
12

13
    /// <inheritdoc />
14
    public event RemovalNotification<K, V>? Removal;
15

16
    private readonly CacheOptions                              options;
17
    private readonly Func<K, CancellationToken, ValueTask<V>>? defaultLoader;
18
    private readonly ConcurrentDictionary<K, CacheEntry<V>>    cache;
19
    private readonly Timer?                                    expirationTimer;
20

21
    private volatile bool isDisposed;
22

23
    /// <summary>Create a new in-memory cache for specific key and value types, with the given optional <paramref name="options"/> and value <paramref name="loader"/>.</summary>
24
    /// <param name="options">Customize the caching behavior, including expiration durations and automatic refreshes.</param>
25
    /// <param name="loader">Cache-wide callback used to generate a cached value when a key is requested that doesn't already have an unexpired value cached. Can be <c>null</c> if values are always manually cached with <see cref="Put"/>, and can be overridden on a per-read basis by supplying a <c>loader</c> callback to <see cref="Get"/>. Also used when automatically refreshing values. Can throw exceptions, which will not be cached.</param>
26
    public InMemoryCache(CacheOptions? options = null, Func<K, CancellationToken, ValueTask<V>>? loader = null) {
15✔
27
        this.options = (options ?? new CacheOptions()) with {
15!
28
            ConcurrencyLevel = this.options.ConcurrencyLevel is > 0 and var c ? c : Environment.ProcessorCount,
15✔
29
            InitialCapacity = this.options.InitialCapacity is > 0 and var i ? i : 31
15✔
30
        };
15✔
31

32
        defaultLoader = loader;
15✔
33
        cache         = new ConcurrentDictionary<K, CacheEntry<V>>(this.options.ConcurrencyLevel, this.options.InitialCapacity);
15✔
34

35
        if (this.options.ExpireAfterWrite > TimeSpan.Zero || this.options.ExpireAfterRead > TimeSpan.Zero) {
15✔
36
            TimeSpan expirationScanInterval = this.options.ExpirationScanInterval is { Ticks: > 0 } interval ? interval : TimeSpan.FromMinutes(1);
5✔
37
            expirationTimer         =  new Timer(expirationScanInterval.TotalMilliseconds) { AutoReset = false };
5✔
38
            expirationTimer.Elapsed += ScanForExpirations;
5✔
39
            expirationTimer.Start();
5✔
40
        }
41
    }
15✔
42

43
    /// <inheritdoc />
44
    public long Count => cache.Count;
14✔
45

46
    /// <inheritdoc />
47
    public async Task<V> Get(K key, Func<K, CancellationToken, ValueTask<V>>? loader = null, CancellationToken cancellationToken = default) {
48
        CacheEntry<V> cacheEntry = cache.GetOrAdd(key, ValueFactory);
18✔
49
        if (cacheEntry.IsNew) {
18✔
50
            await cacheEntry.ValueLock.WaitAsync(cancellationToken).ConfigureAwait(false);
12✔
51
            try {
52
                if (cacheEntry.IsNew) {
12✔
53
                    cacheEntry.Value = await LoadValue(key, loader ?? defaultLoader, cancellationToken).ConfigureAwait(false);
12✔
54
                    cacheEntry.LastWritten.Start();
5✔
55
                    cacheEntry.RefreshTimer?.Start();
5✔
56
                    cacheEntry.IsNew = false;
5✔
57
                }
58
            } catch (OperationCanceledException) {
5✔
59
                // don't remove or dispose cache entry
60
            } catch (Exception e) when (e is not OutOfMemoryException) {
7✔
61
                cache.TryRemove(key, out _);
7✔
62
                cacheEntry.Dispose();
7✔
63
                throw;
7✔
64
            } finally {
65
                if (!cacheEntry.IsDisposed) {
12✔
66
                    cacheEntry.ValueLock.Release();
5✔
67
                }
68
            }
69
        } else if (IsExpired(cacheEntry)) {
6✔
70
            V    oldValue = default!;
2✔
71
            bool written  = false;
2✔
72
            await cacheEntry.ValueLock.WaitAsync(cancellationToken).ConfigureAwait(false);
2✔
73
            try {
74
                if (IsExpired(cacheEntry)) {
2✔
75
                    cacheEntry.RefreshTimer?.Stop();
2!
76
                    try {
77
                        oldValue         = cacheEntry.Value;
2✔
78
                        cacheEntry.Value = await LoadValue(key, loader ?? defaultLoader, cancellationToken).ConfigureAwait(false);
2✔
79
                        cacheEntry.LastWritten.Restart();
1✔
80
                        written = true;
1✔
81
                    } finally {
1✔
82
                        cacheEntry.RefreshTimer?.Start();
2!
83
                    }
84
                }
85
            } finally {
1✔
86
                cacheEntry.ValueLock.Release();
2✔
87
            }
88

89
            if (written) {
1✔
90
                Removal?.Invoke(this, key, oldValue, RemovalCause.Expired);
1!
91
            }
92
        }
1✔
93

94
        cacheEntry.LastRead.Restart();
10✔
95
        return cacheEntry.Value;
10✔
96
    }
10✔
97

98
    /// <inheritdoc />
99
    public async Task Put(K key, V value) {
100
        V             removedValue = default!;
15✔
101
        bool          written      = false;
15✔
102
        CacheEntry<V> cacheEntry   = cache.GetOrAdd(key, ValueFactory);
15✔
103
        await cacheEntry.ValueLock.WaitAsync().ConfigureAwait(false);
15✔
104
        try {
105
            cacheEntry.RefreshTimer?.Stop();
15!
106
            if (cacheEntry.IsNew) {
15✔
107
                cacheEntry.IsNew = false;
14✔
108
            } else {
109
                removedValue = cacheEntry.Value;
1✔
110
                written      = true;
1✔
111
            }
112

113
            cacheEntry.Value = value;
15✔
114
            cacheEntry.LastWritten.Restart();
15✔
115
            cacheEntry.RefreshTimer?.Start();
15!
116
        } finally {
15✔
117
            cacheEntry.ValueLock.Release();
15✔
118
        }
119

120
        if (written) {
15✔
121
            Removal?.Invoke(this, key, removedValue, RemovalCause.Replaced);
1!
122
        }
123
    }
15✔
124

125
    private CacheEntry<V> ValueFactory(K key) {
126
        bool   hasLoader    = defaultLoader != null;
26✔
127
        Timer? refreshTimer = options.RefreshAfterWrite > TimeSpan.Zero && hasLoader ? new Timer(options.RefreshAfterWrite.TotalMilliseconds) { AutoReset = false, Enabled = false } : null;
26✔
128
        var    entry        = new CacheEntry<V>(refreshTimer);
26✔
129

130
        if (entry.RefreshTimer != null) {
26✔
131
            async void refreshEntry(object sender, ElapsedEventArgs elapsedEventArgs) {
132
                if (!entry.IsDisposed) {
3!
133
                    try {
134
                        V    oldValue = default!;
3✔
135
                        bool written  = false;
3✔
136
                        try {
137
                            await entry.ValueLock.WaitAsync().ConfigureAwait(false);
3✔
138
                            oldValue    = entry.Value;
3✔
139
                            entry.Value = await defaultLoader!(key, CancellationToken.None).ConfigureAwait(false);
3✔
140
                            entry.LastWritten.Restart();
3✔
141
                            written = true;
3✔
142
                        } catch (Exception e) when (e is not OutOfMemoryException) {
3✔
143
                            // try again next timer execution
144
                        } finally {
×
145
                            entry.RefreshTimer.Start();
3✔
146
                            entry.ValueLock.Release();
3✔
147
                        }
148

149
                        if (written) {
3✔
150
                            Removal?.Invoke(this, key, oldValue, RemovalCause.Replaced);
3!
151
                        }
152
                    } catch (ObjectDisposedException) {}
3✔
153
                } else {
154
                    entry.RefreshTimer.Elapsed -= refreshEntry;
×
155
                }
156
            }
3✔
157

158
            entry.RefreshTimer.Elapsed += refreshEntry;
1✔
159
        }
160

161
        return entry;
26✔
162
    }
163

164
    /// <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>
165
    private static ValueTask<V> LoadValue(K key, Func<K, CancellationToken, ValueTask<V>>? loader, CancellationToken ct) {
166
        if (loader != null) {
14✔
167
            return loader(key, ct);
6✔
168
        } else {
169
            throw KeyNotFoundException(key);
8✔
170
        }
171
    }
172

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

176
    private bool IsExpired(CacheEntry<V> cacheEntry) =>
177
        (options.ExpireAfterWrite > TimeSpan.Zero && options.ExpireAfterWrite <= cacheEntry.LastWritten.Elapsed)
15!
178
        || (options.ExpireAfterRead > TimeSpan.Zero && options.ExpireAfterRead <= cacheEntry.LastRead.Elapsed);
15✔
179

180
    /// <inheritdoc />
181
    public void CleanUp() {
182
        foreach (KeyValuePair<K, CacheEntry<V>> entry in cache.Where(pair => IsExpired(pair.Value))) {
16✔
183
            entry.Value.ValueLock.Wait();
3✔
184
            bool removed = false;
3✔
185
            if (IsExpired(entry.Value)) {
3✔
186
                //this will probably throw a concurrent modification exception
187
                removed = cache.TryRemove(entry.Key, out _);
3✔
188
                if (removed) {
3✔
189
                    entry.Value.Dispose();
3✔
190
                    Removal?.Invoke(this, entry.Key, entry.Value.Value, RemovalCause.Expired);
3✔
191
                }
192
            }
193

194
            if (!removed) {
3!
195
                /*
196
                 * First pass showed entry as expired, but it was concurrently loaded or refreshed before this second pass check, so don't actually remove it.
197
                 * Only release if we didn't remove, because if we removed the entry and its lock were already disposed.
198
                 */
199
                entry.Value.ValueLock.Release();
×
200
            }
201
        }
202
    }
3✔
203

204
    private void ScanForExpirations(object? sender = null, ElapsedEventArgs? e = null) {
205
        CleanUp();
1✔
206
        expirationTimer!.Start();
1✔
UNCOV
207
    }
×
208

209
    /// <inheritdoc />
210
    public void Invalidate(params IEnumerable<K> keys) {
211
        foreach (K key in keys) {
10✔
212
            if (cache.TryRemove(key, out CacheEntry<V>? removedEntry)) {
3✔
213
                Removal?.Invoke(this, key, removedEntry.Value, RemovalCause.Explicit);
3!
214
                removedEntry.Dispose();
3✔
215
            }
216
        }
217
    }
2✔
218

219
    /// <inheritdoc />
220
    public void InvalidateAll() {
221
        KeyValuePair<K, CacheEntry<V>>[] toDispose = cache.ToArray();
16✔
222
        cache.Clear();
16✔
223
        foreach (KeyValuePair<K, CacheEntry<V>> entry in toDispose) {
58✔
224
            if (!isDisposed) {
13✔
225
                Removal?.Invoke(this, entry.Key, entry.Value.Value, RemovalCause.Explicit);
3!
226
            }
227
            entry.Value.Dispose();
13✔
228
        }
229
    }
16✔
230

231
    /// <inheritdoc />
232
    public void Dispose() {
233
        isDisposed = true;
15✔
234
        if (expirationTimer != null) {
15✔
235
            expirationTimer.Elapsed -= ScanForExpirations;
5✔
236
            expirationTimer.Dispose();
5✔
237
        }
238
        InvalidateAll();
15✔
239
    }
15✔
240

241
}
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