Created
May 13, 2024 15:25
-
-
Save ArtemAvramenko/4d2c3723c34f62a080eae79bbea5f861 to your computer and use it in GitHub Desktop.
Queue/cache of items that are stored for a limited period of time
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
using System; | |
using System.Collections.Concurrent; | |
using System.Diagnostics.CodeAnalysis; | |
using System.Threading; | |
namespace ArtemAvramenko.Collections; | |
/// <summary> | |
/// Queue/cache of items that are stored for a limited period of time. | |
/// </summary> | |
public class TemporaryQueue<TKey, TValue> where TKey : IEquatable<TKey> | |
{ | |
private readonly ConcurrentDictionary<TKey, Temp> _queue = new(); | |
private readonly SemaphoreSlim _collectorSync = new(1, 1); | |
private long _minExpiredTicks = long.MaxValue; | |
public void Enqueue(TKey key, TValue value, int lifespanMinutes = 1) => | |
Enqueue(key, value, TimeSpan.FromMinutes(lifespanMinutes)); | |
public void Enqueue(TKey key, TValue value, TimeSpan lifespan) | |
{ | |
var nowTicks = DateTime.UtcNow.Ticks; | |
var minExpiredTicks = Interlocked.Read(ref _minExpiredTicks); | |
if (minExpiredTicks < nowTicks) | |
{ | |
СollectExpiredValues(); | |
nowTicks = DateTime.UtcNow.Ticks; | |
minExpiredTicks = Interlocked.Read(ref _minExpiredTicks); | |
} | |
var expiredTicks = nowTicks + lifespan.Ticks; | |
if (expiredTicks < minExpiredTicks) | |
{ | |
// set the new value only if the previous value has not changed in time | |
Interlocked.CompareExchange(ref _minExpiredTicks, expiredTicks, minExpiredTicks); | |
} | |
_queue[key] = new Temp() { Value = value, ExpiredTicks = expiredTicks }; | |
} | |
public bool TryDequeue(TKey key, [MaybeNullWhen(false)] out TValue? value) | |
{ | |
if (_queue.TryRemove(key, out var temp) && temp.ExpiredTicks >= DateTime.UtcNow.Ticks) | |
{ | |
value = temp.Value; | |
return true; | |
} | |
value = default; | |
return false; | |
} | |
private void СollectExpiredValues() | |
{ | |
_collectorSync.Wait(); | |
try | |
{ | |
var nowTicks = DateTime.UtcNow.Ticks; | |
var minExpiredTicks = long.MaxValue; | |
var previousMinExpiredTicks = Interlocked.Exchange(ref _minExpiredTicks, minExpiredTicks); | |
if (previousMinExpiredTicks < nowTicks) // prevent repeated collection | |
{ | |
foreach (var pair in _queue.ToArray()) | |
{ | |
// when deleting, the Expired value is also checked so that | |
// a newer value is not deleted (see Temp.Equals below) | |
var expiredTicks = pair.Value.ExpiredTicks; | |
if (expiredTicks >= nowTicks || !_queue.TryRemove(pair)) | |
{ | |
if (expiredTicks < minExpiredTicks) | |
{ | |
minExpiredTicks = expiredTicks; | |
} | |
} | |
} | |
previousMinExpiredTicks = Interlocked.Exchange(ref _minExpiredTicks, minExpiredTicks); | |
} | |
if (previousMinExpiredTicks < minExpiredTicks) | |
{ | |
// unlikely, but it is still possible that the old value should be restored | |
Interlocked.Exchange(ref _minExpiredTicks, previousMinExpiredTicks); | |
} | |
} | |
finally | |
{ | |
_collectorSync.Release(); | |
} | |
} | |
private readonly struct Temp : IEquatable<Temp> | |
{ | |
public readonly TValue Value { get; init; } | |
public readonly long ExpiredTicks { get; init; } | |
public override int GetHashCode() => ExpiredTicks.GetHashCode(); | |
public readonly bool Equals(Temp other) => ExpiredTicks == other.ExpiredTicks; | |
public override readonly bool Equals([NotNullWhen(true)] object? obj) => | |
obj is Temp temp && Equals(temp); | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment