Sunday, February 22, 2015

Await an Interval with a Throttle Class in .NET

Are you writing async code but need to control how often a call can be made? Just use this thread safe implementation of a throttle that will return an evenly spaced Task.Delay each time it is invoked. This will allow you to throttle that your application and control how many calls it makes.

Throttle Class

public interface IThrottle
{
    Task GetNext();
    Task GetNext(out TimeSpan delay);
}
 
public class Throttle : IThrottle
{
    private readonly object _lock = new object();
 
    private readonly TimeSpan _interval;
 
    private DateTime _nextTime;
 
    public Throttle(TimeSpan interval)
    {
        _interval = interval;
        _nextTime = DateTime.Now.Subtract(interval);
    }
 
    public Task GetNext()
    {
        TimeSpan delay;
        return GetNext(out delay);
    }
 
    public Task GetNext(out TimeSpan delay)
    {
        lock (_lock)
        {
            var now = DateTime.Now;
 
            _nextTime = _nextTime.Add(_interval);
                
            if (_nextTime > now)
            {
                delay = _nextTime - now;
                return Task.Delay(delay);
            }
 
            _nextTime = now;
 
            delay = TimeSpan.Zero;
            return Task.FromResult(true);
        }
    }
}

Unit Tests

public class ThrottleTests
{
    [Theory]
    [InlineData(100)]
    [InlineData(200)]
    [InlineData(500)]
    [InlineData(1000)]
    public void GetNext(int interval)
    {
        var timeSpan = TimeSpan.FromMilliseconds(interval);
        var throttle = new Throttle(timeSpan);
 
        TimeSpan delay1, delay2, delay3, delay4;
 
        throttle.GetNext(out delay1);
        throttle.GetNext(out delay2);
        Thread.Sleep(interval / 2);
        throttle.GetNext(out delay3);
        throttle.GetNext(out delay4);
 
        Assert.Equal(delay1, TimeSpan.Zero);
        AssertInRange(timeSpan, delay2, 1.0);
        AssertInRange(timeSpan, delay3, 1.5);
        AssertInRange(timeSpan, delay4, 2.5);
    }
 
    [Theory]
    [InlineData(100)]
    [InlineData(200)]
    public async Task AwaitGetNext(int interval)
    {
        var timeSpan = TimeSpan.FromMilliseconds(interval);
        var throttle = new Throttle(timeSpan);
 
        TimeSpan delay1, delay2, delay3;
 
        await throttle.GetNext(out delay1);
        Assert.Equal(TimeSpan.Zero, delay1);
 
        await throttle.GetNext(out delay2);
        AssertInRange(timeSpan, delay2, 1);
 
        await throttle.GetNext(out delay3);
        AssertInRange(timeSpan, delay2, 1);
    }
 
    private static void AssertInRange(
        TimeSpan expected, 
        TimeSpan actual, 
        double multiplier,
        double fudgeFactor = 0.2)
    {
        var expectedMs = expected.TotalMilliseconds;
        var actualMs = actual.TotalMilliseconds;
        var low = expectedMs*(multiplier - fudgeFactor);
        var high = expectedMs*(multiplier + fudgeFactor);
 
        Assert.InRange(actualMs, low, high);
    }
}

Enjoy,
Tom

No comments:

Post a Comment

Real Time Web Analytics