public void With_no_param_should_set_default_config() { var sut = new ThrottleConfiguration(); Assert.AreEqual(20, sut.MessagePerTime); Assert.AreEqual(15, sut.IntervalInMinutes); }
public ThrottledWorker(Throttle throttle) { Settings = throttle.Settings; _throttle = throttle; int workerThreads; ThreadPool.GetMaxThreads(out workerThreads, out int completionPortThreads); if (Settings.MaxConcurrentThreads != null) { workerThreads = Settings.MaxConcurrentThreads.Value; } if (workerThreads <= 0) { throw new ArgumentNullException($"There must be at least 1 worker thread available."); } _cancellationTokenSource = new CancellationTokenSource(); _queue = new BlockingCollection <TRequest>(); _workers = Enumerable.Range(0, workerThreads).Select(i => BackgroundWorker(_cancellationTokenSource.Token)).ToArray(); _throttle = new Throttle(Settings); }
public void With_param_should_configure_throttling() { var sut = new ThrottleConfiguration() { IntervalInMinutes = 50, MessagePerTime = 20 }; Assert.AreEqual(20, sut.MessagePerTime); Assert.AreEqual(50, sut.IntervalInMinutes); }
public void ThrottledWorker_CanPreventConcurrentRequests() { // no more than 2 concurrent threads. ThrottleConfiguration configuration = new ThrottleConfiguration { MaxConcurrentThreads = 2 }; Throttle throttle = new Throttle(configuration); ThrottledWorker throttledWorker = new ThrottledWorker(throttle); ManualResetEvent awaiter = new ManualResetEvent(false); CancellationTokenSource outerCancellationSource = new CancellationTokenSource(5 * 1000); // 5 seconds const long requestCount = 5; long success = 0; for (int i = 0; i < requestCount; i++) { int instance = i; Stopwatch sw = new Stopwatch(); sw.Start(); throttledWorker.Enqueue(async() => { CancellationTokenSource innerCancellationSource = CancellationTokenSource.CreateLinkedTokenSource( outerCancellationSource.Token, new CancellationTokenSource(5 * 1000).Token // 5 seconds ); Log.WriteLine($"[{DateTime.Now:yyyy-MM-dd HH:mm:ss.fff}] Request {instance}: acquired lease (waitTime: {sw.Elapsed})"); sw.Restart(); awaiter.WaitOne(); await Task.Delay(1000, innerCancellationSource.Token); // do some work Log.WriteLine($"[{DateTime.Now:yyyy-MM-dd HH:mm:ss.fff}] Request {instance}: completed work (runTime: {sw.Elapsed})"); Assert.True(throttle.ActiveThreads <= configuration.MaxConcurrentThreads, $"Instance {instance}: Thread count exceeds max concurrent threads (actual: {throttle.ActiveThreads}, max: {configuration.MaxConcurrentThreads})"); long current = Interlocked.Increment(ref success); Log.WriteLine($"[{DateTime.Now:yyyy-MM-dd HH:mm:ss.fff}] Request {instance}: thread complete (runTime: {sw.Elapsed}, success: {current})"); }); } Log.WriteLine($"[{DateTime.Now:yyyy-MM-dd HH:mm:ss.fff}] set....."); awaiter.Set(); throttledWorker.Wait(outerCancellationSource.Token); Log.WriteLine($"[{DateTime.Now:yyyy-MM-dd HH:mm:ss.fff}] complete....."); long actual = Interlocked.Read(ref success); Assert.Equal(requestCount, actual); }
public void Throttle_CanWaitBetweenRequests_FromEndOfLastRequest() { // no more than 1 thread per second from end of last request ThrottleConfiguration configuration = new ThrottleConfiguration { WaitTimeBetweenLease = TimeSpan.FromSeconds(1), WaitStrategy = ThrottleWaitStrategy.FromEndOfLastRequest }; Throttle throttle = new Throttle(configuration); Task[] threads = new Task[5]; ManualResetEvent awaiter = new ManualResetEvent(false); for (int i = 0; i < threads.Length; i++) { int instance = i; threads[i] = Task.Run(() => { awaiter.WaitOne(); Log.WriteLine($"[{DateTime.Now:yyyy-MM-dd HH:mm:ss.fff}] Instance {instance}: thread started"); using (throttle.AcquireLease()) { TimeSpan?timeSinceLastRequest = DateTimeOffset.Now - throttle.LastFinishTime; Log.WriteLine($"[{DateTime.Now:yyyy-MM-dd HH:mm:ss.fff}] Instance {instance}: aquired lease (threads: {throttle.ActiveThreads}, timeSinceLastRequest: {timeSinceLastRequest})"); Assert.True(throttle.ActiveThreads <= configuration.MaxConcurrentThreads, $"Instance {instance}: Thread count exceeds max concurrent threads (actual: {throttle.ActiveThreads}, max: {configuration.MaxConcurrentThreads})"); if (timeSinceLastRequest != null) { Assert.True(timeSinceLastRequest >= configuration.WaitTimeBetweenLease, $"Instance {instance}: Wait time exceeds min wait time (actual: {timeSinceLastRequest}, min: {configuration.WaitTimeBetweenLease})"); } Thread.Sleep(1000); // do some work Log.WriteLine($"[{DateTime.Now:yyyy-MM-dd HH:mm:ss.fff}] Instance {instance}: completed work"); } Log.WriteLine($"[{DateTime.Now:yyyy-MM-dd HH:mm:ss.fff}] Instance {instance}: released lease (threads: {throttle.ActiveThreads})"); }); } Log.WriteLine($"[{DateTime.Now:yyyy-MM-dd HH:mm:ss.fff}] set....."); awaiter.Set(); Task.WaitAll(threads); }
public void Throttle_CanWaitBetweenRequests_TumblingWindow() { // No more than 2 requests per 5 seconds and only 1 concurrent thread. ThrottleConfiguration configuration = new ThrottleConfiguration { WaitTimeBetweenLease = TimeSpan.FromSeconds(5), WaitStrategy = ThrottleWaitStrategy.TumblingWindow, MaxConcurrentThreads = 1, MaxRequests = 2 }; Throttle throttle = new Throttle(configuration); Task[] threads = new Task[5]; ManualResetEvent awaiter = new ManualResetEvent(false); for (int i = 0; i < threads.Length; i++) { int instance = i; threads[i] = Task.Run(() => { awaiter.WaitOne(); Log.WriteLine($"[{DateTime.Now:yyyy-MM-dd HH:mm:ss.fff}] Instance {instance}: thread started"); using (throttle.AcquireLease()) { TimeSpan?timeSinceLastRequest = DateTimeOffset.Now - throttle.LastFinishTime; Log.WriteLine($"[{DateTime.Now:yyyy-MM-dd HH:mm:ss.fff}] Instance {instance}: acquired lease (window: {throttle.RateWindow?.DateTime:yyyy-MM-dd HH:mm:ss.fff}, threads: {throttle.ActiveThreads}, requests: {throttle.RequestCount})"); Assert.True(throttle.ActiveThreads <= configuration.MaxConcurrentThreads, $"Instance {instance}: Thread count exceeds max concurrent threads (actual: {throttle.ActiveThreads}, max: {configuration.MaxConcurrentThreads})"); Assert.True(throttle.RequestCount <= configuration.MaxRequests, $"Instance {instance}: Request count exceeds max request count (actual: {throttle.RequestCount}, max: {configuration.MaxRequests})"); Thread.Sleep(1000); // do some work Log.WriteLine($"[{DateTime.Now:yyyy-MM-dd HH:mm:ss.fff}] Instance {instance}: completed work"); } Log.WriteLine($"[{DateTime.Now:yyyy-MM-dd HH:mm:ss.fff}] Instance {instance}: released lease (threads: {throttle.ActiveThreads})"); }); } Log.WriteLine($"[{DateTime.Now:yyyy-MM-dd HH:mm:ss.fff}] set....."); awaiter.Set(); Task.WaitAll(threads); }
public void ThrottledWorker_CanWaitBetweenRequests_SlidingWindow() { // no more than 2 request per 1 second ThrottleConfiguration configuration = new ThrottleConfiguration { WaitTimeBetweenLease = TimeSpan.FromSeconds(1), WaitStrategy = ThrottleWaitStrategy.SlidingWindow, MaxRequests = 2, //RecursionStrategy = ThrottleRecursionStrategy.AllowsRecursion }; Throttle throttle = new Throttle(configuration); ThrottledWorker throttledWorker = new ThrottledWorker(throttle); const int requestCount = 10; ManualResetEvent awaiter = new ManualResetEvent(false); for (int i = 0; i < requestCount; i++) { int instance = i; throttledWorker.Enqueue(() => { awaiter.WaitOne(); Log.WriteLine($"[{DateTime.Now:yyyy-MM-dd HH:mm:ss.fff}] Instance {instance}: thread started"); //using (throttle.AcquireLease()) { TimeSpan?timeSinceLastRequest = DateTimeOffset.Now - throttle.LastFinishTime; Assert.True(throttle.ActiveThreads <= configuration.MaxConcurrentThreads, $"Instance {instance}: Thread count exceeds max concurrent threads (actual: {throttle.ActiveThreads}, max: {configuration.MaxConcurrentThreads})"); Assert.True(throttle.RequestCount <= configuration.MaxRequests, $"Instance {instance}: Request count exceeds max request count (actual: {throttle.RequestCount}, max: {configuration.MaxRequests})"); Thread.Sleep(1000); // do some work Log.WriteLine($"[{DateTime.Now:yyyy-MM-dd HH:mm:ss.fff}] Instance {instance}: completed work"); } }); } Log.WriteLine($"[{DateTime.Now:yyyy-MM-dd HH:mm:ss.fff}] set....."); awaiter.Set(); throttledWorker.Wait(); }
public void ThrottledWorker_CanWaitBetweenRequests_FromStartOfLastRequest() { // no more than 1 thread per second from start of last request. ThrottleConfiguration configuration = new ThrottleConfiguration { WaitTimeBetweenLease = TimeSpan.FromSeconds(1), WaitStrategy = ThrottleWaitStrategy.FromStartOfLastRequest }; Throttle throttle = new Throttle(configuration); ThrottledWorker throttledWorker = new ThrottledWorker(throttle); const int requestCount = 5; ManualResetEvent awaiter = new ManualResetEvent(false); CancellationTokenSource outerCancellationSource = new CancellationTokenSource(12 * 1000); // 12 seconds DateTimeOffset?lastRequestTime = null; object _lock = new object(); long success = 0; for (int i = 0; i < requestCount; i++) { int instance = i; Stopwatch sw = new Stopwatch(); sw.Start(); throttledWorker.Enqueue(() => { awaiter.WaitOne(); TimeSpan?timeSinceLastRequest; lock (_lock) { timeSinceLastRequest = DateTimeOffset.Now - lastRequestTime; if (lastRequestTime == null || throttle.LastRequestTime > lastRequestTime) { lastRequestTime = throttle.LastRequestTime; } } Log.WriteLine($"[{DateTime.Now:yyyy-MM-dd HH:mm:ss.fff}] Instance {instance}: aquired lease (threads: {throttle.ActiveThreads}, timeSinceLastRequest: {timeSinceLastRequest})"); Assert.True(throttle.ActiveThreads <= configuration.MaxConcurrentThreads, $"Instance {instance}: Thread count exceeds max concurrent threads (actual: {throttle.ActiveThreads}, max: {configuration.MaxConcurrentThreads})"); if (timeSinceLastRequest != null) { Assert.True(timeSinceLastRequest >= configuration.WaitTimeBetweenLease, $"Instance {instance}: Wait time exceeds min wait time (actual: {timeSinceLastRequest}, min: {configuration.WaitTimeBetweenLease})"); } Thread.Sleep(1000); // do some work long current = Interlocked.Increment(ref success); Log.WriteLine($"[{DateTime.Now:yyyy-MM-dd HH:mm:ss.fff}] Instance {instance}: completed work"); }); } Log.WriteLine($"[{DateTime.Now:yyyy-MM-dd HH:mm:ss.fff}] set....."); awaiter.Set(); throttledWorker.Wait(); long actual = Interlocked.Read(ref success); Assert.Equal(requestCount, actual); }
public Throttle(ThrottleConfiguration configuration) { Settings = configuration; if (configuration.MaxRequests != null) { if (configuration.WaitStrategy == null) { throw new ArgumentNullException($"{nameof(configuration.WaitStrategy)} is required when {nameof(configuration.MaxRequests)} is specified."); } if (configuration.WaitTimeBetweenLease == null) { throw new ArgumentNullException($"{nameof(configuration.WaitTimeBetweenLease)} is required when {nameof(configuration.MaxRequests)} is specified."); } if (configuration.MaxRequests <= 0) { throw new ArgumentOutOfRangeException($"{nameof(configuration.MaxRequests)} must be greater than 0."); } if (configuration.MaxConcurrentThreads == null) { configuration.MaxConcurrentThreads = configuration.MaxRequests; } if (configuration.MaxConcurrentThreads > configuration.MaxRequests) { throw new ArgumentOutOfRangeException($"{nameof(configuration.MaxConcurrentThreads)} must be less than or equal to {nameof(configuration.MaxRequests)}."); } } if (configuration.WaitTimeBetweenLease != null || configuration.WaitStrategy != null) { if (configuration.WaitStrategy == null) { throw new ArgumentNullException($"{nameof(configuration.WaitStrategy)} is required when {nameof(configuration.WaitTimeBetweenLease)} is specified."); } if (configuration.WaitTimeBetweenLease == null) { throw new ArgumentNullException($"{nameof(configuration.WaitTimeBetweenLease)} is required when {nameof(configuration.WaitStrategy)} is specified."); } if (configuration.WaitTimeBetweenLease <= TimeSpan.Zero) { throw new ArgumentOutOfRangeException($"{nameof(configuration.WaitTimeBetweenLease)} must be greater than 0."); } switch (configuration.WaitStrategy) { case ThrottleWaitStrategy.FromEndOfLastRequest: case ThrottleWaitStrategy.FromStartOfLastRequest: configuration.MaxRequests = 1; configuration.MaxConcurrentThreads = 1; break; case ThrottleWaitStrategy.SlidingWindow: case ThrottleWaitStrategy.TumblingWindow: if (configuration.MaxRequests == null) { throw new ArgumentNullException($"{nameof(configuration.MaxRequests)} is required when {nameof(configuration.WaitStrategy)} is {configuration.WaitStrategy}."); } _requests = new List <long>(); break; } } if (configuration.MaxConcurrentThreads != null) { if (configuration.MaxConcurrentThreads <= 0) { throw new ArgumentOutOfRangeException($"{nameof(configuration.MaxConcurrentThreads)} must be greater than 0."); } _semaphore = new SemaphoreSlim(configuration.MaxConcurrentThreads.Value, configuration.MaxConcurrentThreads.Value); } }
public ThrottledWorker(ThrottleConfiguration configuration) : this(new Throttle(configuration)) { }
public void Throttle_CanPreventConcurrentRequests() { // no more than 2 concurrent threads. ThrottleConfiguration configuration = new ThrottleConfiguration { MaxConcurrentThreads = 2 }; Throttle throttle = new Throttle(configuration); Task[] threads = new Task[5]; ManualResetEvent awaiter = new ManualResetEvent(false); CancellationTokenSource outerCancellationSource = new CancellationTokenSource(5 * 1000); // 5 seconds long success = 0; for (int i = 0; i < threads.Length; i++) { int instance = i; Stopwatch sw = new Stopwatch(); sw.Start(); threads[i] = Task.Run(async() => { CancellationTokenSource innerCancellationSource = CancellationTokenSource.CreateLinkedTokenSource( outerCancellationSource.Token, new CancellationTokenSource(5 * 1000).Token // 5 seconds ); Log.WriteLine($"[{DateTime.Now:yyyy-MM-dd HH:mm:ss.fff}] Instance {instance}: thread activated (activateTime: {sw.Elapsed})"); sw.Restart(); awaiter.WaitOne(); Log.WriteLine($"[{DateTime.Now:yyyy-MM-dd HH:mm:ss.fff}] Instance {instance}: thread started (runTime: {sw.Elapsed})"); try { IDisposable lease = await throttle.AcquireLeaseAsync(innerCancellationSource.Token); using (lease) { Log.WriteLine($"[{DateTime.Now:yyyy-MM-dd HH:mm:ss.fff}] Instance {instance}: acquired lease (threads: {throttle.ActiveThreads}, runTime: {sw.Elapsed})"); await Task.Delay(1000, innerCancellationSource.Token); // do some work Log.WriteLine($"[{DateTime.Now:yyyy-MM-dd HH:mm:ss.fff}] Instance {instance}: completed work (runTime: {sw.Elapsed})"); Assert.True(throttle.ActiveThreads <= configuration.MaxConcurrentThreads, $"Instance {instance}: Thread count exceeds max concurrent threads (actual: {throttle.ActiveThreads}, max: {configuration.MaxConcurrentThreads})"); } long current = Interlocked.Increment(ref success); Log.WriteLine($"[{DateTime.Now:yyyy-MM-dd HH:mm:ss.fff}] Instance {instance}: thread complete (runTime: {sw.Elapsed}, success: {current})"); } catch (Exception ex) { Log.WriteLine($"[{DateTime.Now:yyyy-MM-dd HH:mm:ss.fff}] Instance {instance}: thread failed (runTime: {sw.Elapsed}) {ex}"); throw new Exception("Operation failed", ex); } }); } Log.WriteLine($"[{DateTime.Now:yyyy-MM-dd HH:mm:ss.fff}] set....."); awaiter.Set(); Task.WaitAll(threads, outerCancellationSource.Token); Log.WriteLine($"[{DateTime.Now:yyyy-MM-dd HH:mm:ss.fff}] complete....."); long actual = Interlocked.Read(ref success); Assert.Equal(threads.Length, actual); }