C# Threaded throttle (api call)

Date: 2023-01-19
public class OverviewController : BaseApiController
{
    private static readonly ThrottleDictionary throttleDictionary = new();
    private readonly TimeSpan throttleTime = TimeSpan.FromSeconds(30);

    [HttpGet("api/day/{date}")]
    [ApiCallRight("api.get.day")]
    public async Task<VmCalendarList> GetDayOverview(DateTime date, bool isChanged = false)
    {
        var cacheKey = $"dayoverview-{date:yyyy-MM-dd}";
        var throttle = throttleDictionary.Get(cacheKey, throttleTime);
        var value = (VmCalendarList)throttle.Value;

        if (!isChanged && !throttle.CanFire() && value != null) return value;
        
        await throttle.Build(async () =>
        {
            var absenceInfo = await DomainPorts.OverviewService.DayOverview(date);
            var result = VmCalendarList.FromDomain(absenceInfo);
            throttle.Value = result;
        });
        return (VmCalendarList)throttle.Value;
    }
}


public class ThrottleDictionary
{
    private readonly ConcurrentDictionary<string, Throttle> keyValuePairs = new();
    
    public Throttle Get(string key, TimeSpan throttleTime)
    {
        if (cleanupThrottle.CanFire()) Cleanup();
        return keyValuePairs.GetOrAdd(key, (key) => new Throttle(throttleTime));
    }

    private static readonly TimeSpan cleanupTime = TimeSpan.FromHours(1);
    private readonly Throttle cleanupThrottle = new(cleanupTime);

    private void Cleanup()
    {
        var keysToRemove = keyValuePairs.Where(x => x.Value.LastUsed() < DateTime.Now.Subtract(cleanupTime)).Select(x => x.Key);
        foreach(var key in keysToRemove) {
            keyValuePairs.Remove(key, out _);
        }
    }
}

public class Throttle
{
    public Throttle(TimeSpan throttleTime)
    {
        ThrottleTime = throttleTime;
    }
    private DateTime? Last = null;
    private readonly TimeSpan ThrottleTime;
    private readonly object Locker = new();

    public DateTime? LastUsed() => Last ?? DateTime.Now;

    public object Value { get; set; }

    public Task BuildTask;
    public async Task Build(Func<Task> fn) 
    {
        if (BuildTask != null)
        {
            await BuildTask;
            return;
        }
        try
        {
            BuildTask = fn();
            await BuildTask;
        }
        finally
        {
            BuildTask = null;
        }
    }

    public void Update()
    {
        Last = DateTime.Now;
    }

    public bool CanFire()
    {
        lock(Locker) 
        { 
            bool canFire = Last == null || (DateTime.Now - Last).Value > ThrottleTime;
            if (canFire) Update();
            return canFire;
        }
    }
}

72220cookie-checkC# Threaded throttle (api call)