From f10e22ade4360c35f64b3cf5c0957fdd922a70a6 Mon Sep 17 00:00:00 2001 From: Charles Macanka Date: Mon, 11 Mar 2019 00:14:47 -0400 Subject: [PATCH] More gracefully handle exceeding the API limit. If another tool that uses the GGG API is used alongside Procurement, it is very easy to exceed the API limit. This change more gracefully handles that case, but is very cautious about it. If an overflow is detected, completely fill up the window of active tasks, and wait for at least as long as the window size. --- .../Events/ThottledEventArgs.cs | 8 ++++++- POEApi.Transport/HttpTransport.cs | 21 ++++++++++++++++--- POEApi.Transport/TaskThrottle.cs | 17 +++++++++++++++ Procurement/View/RefreshView.xaml.cs | 9 ++++++-- Procurement/ViewModel/LoginWindowViewModel.cs | 10 +++++++-- 5 files changed, 57 insertions(+), 8 deletions(-) diff --git a/POEApi.Infrastructure/Events/ThottledEventArgs.cs b/POEApi.Infrastructure/Events/ThottledEventArgs.cs index d6beabd3..635ea707 100644 --- a/POEApi.Infrastructure/Events/ThottledEventArgs.cs +++ b/POEApi.Infrastructure/Events/ThottledEventArgs.cs @@ -5,9 +5,15 @@ namespace POEApi.Infrastructure.Events public class ThottledEventArgs : EventArgs { public TimeSpan WaitTime { get; private set; } - public ThottledEventArgs(TimeSpan waitTime) + /// + /// Whether the throttling event was expected. If it was not expected, there might be other agents or + /// untracked actions using up resources towards the limit. + /// + public bool Expected { get; private set; } + public ThottledEventArgs(TimeSpan waitTime, bool expected = true) { WaitTime = waitTime; + Expected = expected; } } } diff --git a/POEApi.Transport/HttpTransport.cs b/POEApi.Transport/HttpTransport.cs index 5aedc942..e3be693f 100644 --- a/POEApi.Transport/HttpTransport.cs +++ b/POEApi.Transport/HttpTransport.cs @@ -206,11 +206,26 @@ protected HttpWebResponse BuildHttpRequestAndGetResponse(HttpMethod method, stri protected MemoryStream PerformHttpRequest(HttpMethod method, string url, bool? allowAutoRedirects = null, string requestData = null) { - using (var response = BuildHttpRequestAndGetResponse(method, url, allowAutoRedirects, requestData)) + HttpWebResponse response = null; + // TODO: Don't retry an infinite number of times. + bool retry = true; + while (retry) { - MemoryStream responseStream = GetMemoryStreamFromResponse(response); - return responseStream; + try + { + response = BuildHttpRequestAndGetResponse(method, url, allowAutoRedirects, requestData); + retry = false; + } + catch (System.Net.WebException ex) when ( + !string.IsNullOrWhiteSpace(ex.Message) && ex.Message.Contains("(429) Too Many Requests.")) + { + Logger.Log("Exceeded API limit while performing HTTP request: " + ex.ToString()); + _taskThrottle.HandleUnexpectedOverload(); + } } + + MemoryStream responseStream = GetMemoryStreamFromResponse(response); + return responseStream; } // The refresh parameter in this ITransport implementation is ignored. diff --git a/POEApi.Transport/TaskThrottle.cs b/POEApi.Transport/TaskThrottle.cs index e762b4d7..ca23c695 100644 --- a/POEApi.Transport/TaskThrottle.cs +++ b/POEApi.Transport/TaskThrottle.cs @@ -88,5 +88,22 @@ protected void RemvoeExpiredTasks() CurrentTasks.Dequeue(); } } + + public void HandleUnexpectedOverload() + { + lock (_lockObject) + { + // We've unexpectedly gone over the number of allowed requests in a time period. Fill up the set of + // current tasks as if we had started tasks. + while (CurrentTasks.Count < WindowLimit) + CurrentTasks.Enqueue(DateTime.Now); + + // Wait at least as long as the WindowSize before trying another task. + var timeUntilNextTask = CurrentTasks.Peek() - DateTime.Now; + TimeSpan waitTime = timeUntilNextTask > WindowSize ? timeUntilNextTask : WindowSize; + Throttled?.Invoke(this, new ThottledEventArgs(waitTime, false)); // Not an expected throttle event. + System.Threading.Thread.Sleep(waitTime); + } + } } } diff --git a/Procurement/View/RefreshView.xaml.cs b/Procurement/View/RefreshView.xaml.cs index 60978714..82a0a794 100644 --- a/Procurement/View/RefreshView.xaml.cs +++ b/Procurement/View/RefreshView.xaml.cs @@ -78,9 +78,14 @@ private void model_StashLoading(POEApi.Model.POEModel sender, POEApi.Model.Event void model_Throttled(object sender, ThottledEventArgs e) { - if (e.WaitTime.TotalSeconds > 4) + if (!e.Expected) + update(string.Format("Exceeded GGG Server request limit; throttling activated. Waiting {0} " + + "seconds. Ensure you do not have other instances of Procurement running or other apps using " + + "the GGG API with your account.", Convert.ToInt32(e.WaitTime.TotalSeconds)), + new POEEventArgs(POEEventState.BeforeEvent)); + else if (e.WaitTime.TotalSeconds > 4) update(string.Format("GGG Server request limit hit, throttling activated. Please wait {0} seconds", - e.WaitTime.Seconds), new POEEventArgs(POEEventState.BeforeEvent)); + Convert.ToInt32(e.WaitTime.TotalSeconds)), new POEEventArgs(POEEventState.BeforeEvent)); } private void update(string message, POEEventArgs e) diff --git a/Procurement/ViewModel/LoginWindowViewModel.cs b/Procurement/ViewModel/LoginWindowViewModel.cs index 979dbb3c..456c6076 100644 --- a/Procurement/ViewModel/LoginWindowViewModel.cs +++ b/Procurement/ViewModel/LoginWindowViewModel.cs @@ -345,8 +345,14 @@ void Model_Authenticating(POEModel sender, AuthenticateEventArgs e) void Model_Throttled(object sender, ThottledEventArgs e) { - if (e.WaitTime.TotalSeconds > 4) - Update(string.Format("GGG Server request limit hit, throttling activated. Please wait {0} seconds", e.WaitTime.Seconds), new POEEventArgs(POEEventState.BeforeEvent)); + if (!e.Expected) + Update(string.Format("Exceeded GGG Server request limit; throttling activated. Waiting {0} " + + "seconds. Ensure you do not have other instances of Procurement running or other apps using " + + "the GGG API with your account.", Convert.ToInt32(e.WaitTime.TotalSeconds)), + new POEEventArgs(POEEventState.BeforeEvent)); + else if (e.WaitTime.TotalSeconds > 4) + Update(string.Format("GGG Server request limit hit, throttling activated. Please wait {0} seconds", + Convert.ToInt32(e.WaitTime.TotalSeconds)), new POEEventArgs(POEEventState.BeforeEvent)); } private void Update(string message, POEEventArgs e)