Skip to content

Instantly share code, notes, and snippets.

@ArtemAvramenko
Created May 13, 2024 15:25
Show Gist options
  • Save ArtemAvramenko/4d2c3723c34f62a080eae79bbea5f861 to your computer and use it in GitHub Desktop.
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
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