Task <IRateLimit> RunAsync2(Func <Task <IRateLimit> > action, object operationDescription) { var githubTask = new GitHubTask(action, operationDescription); RunGitHubTaskThrottled(githubTask); return(githubTask.CompletionSource.Task); }
// Returns true if the request succeeded or failed, false if it hit the rate limit // (and thus should be retried later) async Task <bool> RunGitHubTask(GitHubTask task) { try { _logger.Information("Running {@Operation} ({RemainingRequests}/{RequestLimit})", task.OperationDescription, _remainingRequests, _limit); IRateLimit result = await task.Action(); _logger.Information("Finished {@Operation} ({RemainingRequests}/{RequestLimit})", task.OperationDescription, _remainingRequests, _limit); if (result != null) { lock (_lockObject) { _remainingRequests = result.RateLimit.Remaining; _limit = result.RateLimit.Limit; _resetTime = result.RateLimit.Reset; } } task.CompletionSource.SetResult(result); _requestCompleted.Set(); } catch (RateLimitExceededException ex) { _logger .ForContext("ResetTime", ex.Reset) .Warning("Rate limit exceeded for {@Operation}, resets in {ResetDelay}. {RemainingRequests}/{RequestLimit}", task.OperationDescription, ex.Reset.Subtract(DateTimeOffset.UtcNow), ex.Remaining, ex.Limit); lock (_lockObject) { _remainingRequests = ex.Remaining; _limit = ex.Limit; _resetTime = ex.Reset; } return(false); } catch (Exception ex) { _logger.Error(ex, "Failed {@Operation}", task.OperationDescription); task.CompletionSource.SetException(ex); } finally { lock (_lockObject) { _executingOperations--; } } return(true); }
async void RunGitHubTaskThrottled(GitHubTask task) { try { while (true) { bool canRunNow = false; DateTimeOffset resetTime = DateTimeOffset.MinValue; while (!canRunNow) { lock (_lockObject) { if (_executingOperations < _remainingRequests) { _executingOperations++; canRunNow = true; } else { resetTime = _resetTime; } } if (!canRunNow) { // Wait until the reset time is reached or until another request has completed // (which will update the remaining requests and reset time, possibly allowing // this request to be executed) TimeSpan delayTime = resetTime.Subtract(DateTimeOffset.UtcNow); if (delayTime.TotalMilliseconds < 10) { delayTime = TimeSpan.FromMilliseconds(10); } _logger.ForContext("ResetTime", resetTime) .Information("{@Operation} waiting for rate limit reset (in {ResetDelay} or finished request", task.OperationDescription, delayTime); Task delayTask = Task.Delay(delayTime); Task requestCompletedTask = _requestCompleted.WaitOneAsync(); if (await Task.WhenAny(delayTask, requestCompletedTask) == requestCompletedTask) { // Reset "request completed" WaitHandle // If multiple requests are waiting, then they should all get signalled and // then they'll all Reset the WaitHandle. It only needs to be reset once but // doing it multiple times shouldn't hurt (I think) _requestCompleted.Reset(); } else { lock (_lockObject) { if (_remainingRequests == 0) { _remainingRequests = _limit; } } } } } if (await RunGitHubTask(task)) { break; } } } catch (Exception ex) { task.CompletionSource.SetException(ex); } }