public void ReplenishUpToMaximum() { now = DateTime.UtcNow; var b = new LeakyBucket(10, 2, () => now); Assert.Equal(10, b.ComputedCurrentlyAvailable); Assert.True(b.WaitForAvailableAsync(5).IsCompleted); Assert.Equal(5, b.ComputedCurrentlyAvailable); now = now.AddSeconds(1); Assert.Equal(7, b.ComputedCurrentlyAvailable); now = now.AddSeconds(1); Assert.Equal(9, b.ComputedCurrentlyAvailable); now = now.AddSeconds(1); Assert.Equal(10, b.ComputedCurrentlyAvailable); b.SetState(20, 3, b.ComputedCurrentlyAvailable); now = now.AddSeconds(1); Assert.Equal(13, b.ComputedCurrentlyAvailable); now = now.AddSeconds(2); Assert.Equal(19, b.ComputedCurrentlyAvailable); now = now.AddSeconds(1); Assert.Equal(20, b.ComputedCurrentlyAvailable); }
public async Task <RequestResult <T> > Run <T>(CloneableRequestMessage baseRequest, ExecuteRequestAsync <T> executeRequestAsync, CancellationToken cancellationToken) { var accessToken = GetAccessToken(baseRequest); LeakyBucket bucket = null; if (accessToken != null) { bucket = _shopAccessTokenToLeakyBucket.GetOrAdd(accessToken, _ => new LeakyBucket()); } while (true) { var request = baseRequest.Clone(); if (accessToken != null) { await bucket.GrantAsync(); } try { var fullResult = await executeRequestAsync(request); var bucketState = LeakyBucketState.Get(fullResult.Response); if (bucketState != null) { bucket?.SetState(bucketState); } return(fullResult); } catch (ShopifyRateLimitException ex) when(ex.Reason == ShopifyRateLimitReason.BucketFull || !_retryOnlyIfLeakyBucketFull) { //Only retry if breach caused by full bucket //Other limits will bubble the exception because it's not clear how long the program should wait //Even if there is a Retry-After header, we probably don't want the thread to sleep for potentially many hours // //An exception may still occur: //-Shopify may have a slightly different algorithm //-Shopify may change to a different algorithm in the future //-There may be timing and latency delays //-Multiple programs may use the same access token //-Multiple instances of the same program may use the same access token await Task.Delay(THROTTLE_DELAY, cancellationToken); } } }
public async Task <T> Run <T>(CloneableRequestMessage baseRequest, ExecuteRequestAsync <T> executeRequestAsync) { var accessToken = GetAccessToken(baseRequest); LeakyBucket bucket = null; if (accessToken != null) { bucket = _shopAccessTokenToLeakyBucket.GetOrAdd(accessToken, _ => new LeakyBucket()); } while (true) { var request = baseRequest.Clone(); if (accessToken != null) { await bucket.GrantAsync(); } try { var fullResult = await executeRequestAsync(request); var bucketState = GetBucketState(fullResult.Response); if (bucketState != null) { bucket?.SetState(bucketState); } return(fullResult.Result); } catch (SquareSpaceRateLimitException) { //An exception may still occur: //-SquareSpace may have a slightly different algorithm //-SquareSpace may change to a different algorithm in the future //-There may be timing and latency delays //-Multiple programs may use the same access token //-Multiple instances of the same program may use the same access token await Task.Delay(THROTTLE_DELAY); } } }