diff --git a/ShopifySharp/Infrastructure/BucketStates/GraphQLBucketState.cs b/ShopifySharp/Infrastructure/BucketStates/GraphQLBucketState.cs new file mode 100644 index 000000000..95980f2bf --- /dev/null +++ b/ShopifySharp/Infrastructure/BucketStates/GraphQLBucketState.cs @@ -0,0 +1,40 @@ +using Newtonsoft.Json.Linq; + +namespace ShopifySharp +{ + public class GraphQLBucketState + { + public int MaxAvailable { get; private set; } + + public int RestoreRate { get; private set; } + + public int CurrentlyAvailable { get; private set; } + + public int RequestedQueryCost { get; private set; } + + public int? ActualQueryCost { get; private set; } + + public static GraphQLBucketState Get(JToken response) + { + var cost = response.SelectToken("extensions.cost"); + if (cost == null) + return null; + + var throttleStatus = cost["throttleStatus"]; + int maximumAvailable = (int)throttleStatus["maximumAvailable"]; + int restoreRate = (int)throttleStatus["restoreRate"]; + int currentlyAvailable = (int)throttleStatus["currentlyAvailable"]; + int requestedQueryCost = (int)cost["requestedQueryCost"]; + int? actualQueryCost = (int?)cost["actualQueryCost"];//actual query cost is null if THROTTLED + + return new GraphQLBucketState + { + MaxAvailable = maximumAvailable, + RestoreRate = restoreRate, + CurrentlyAvailable = currentlyAvailable, + RequestedQueryCost = requestedQueryCost, + ActualQueryCost = actualQueryCost, + }; + } + } +} diff --git a/ShopifySharp/Infrastructure/BucketStates/RestBucketState.cs b/ShopifySharp/Infrastructure/BucketStates/RestBucketState.cs new file mode 100644 index 000000000..d91fc0ce2 --- /dev/null +++ b/ShopifySharp/Infrastructure/BucketStates/RestBucketState.cs @@ -0,0 +1,38 @@ +using System.Linq; +using System.Net.Http; + +namespace ShopifySharp +{ + public class RestBucketState + { + public int CurrentlyUsed { get; private set; } + + public int MaxAvailable { get; private set; } + + public const string RESPONSE_HEADER_API_CALL_LIMIT = "X-Shopify-Shop-Api-Call-Limit"; + + public static RestBucketState Get(HttpResponseMessage response) + { + string headerValue = response.Headers.FirstOrDefault(kvp => kvp.Key == RESPONSE_HEADER_API_CALL_LIMIT) + .Value + ?.FirstOrDefault(); + + if (headerValue == null) + return null; + + + var split = headerValue.Split('/'); + if (split.Length == 2 && int.TryParse(split[0], out int currentlyUsed) && + int.TryParse(split[1], out int maxAvailable)) + { + return new RestBucketState + { + CurrentlyUsed = currentlyUsed, + MaxAvailable = maxAvailable + }; + } + + return null; + } + } +} diff --git a/ShopifySharp/Infrastructure/Policies/LeakyBucketPolicy/LeakyBucketExecutionPolicy.cs b/ShopifySharp/Infrastructure/Policies/LeakyBucketPolicy/LeakyBucketExecutionPolicy.cs index 10beb9821..2d344981b 100644 --- a/ShopifySharp/Infrastructure/Policies/LeakyBucketPolicy/LeakyBucketExecutionPolicy.cs +++ b/ShopifySharp/Infrastructure/Policies/LeakyBucketPolicy/LeakyBucketExecutionPolicy.cs @@ -18,7 +18,6 @@ namespace ShopifySharp public class LeakyBucketExecutionPolicy : IRequestExecutionPolicy { private const string REQUEST_HEADER_ACCESS_TOKEN = "X-Shopify-Access-Token"; - public const string RESPONSE_HEADER_API_CALL_LIMIT = "X-Shopify-Shop-Api-Call-Limit"; private static ConcurrentDictionary _shopAccessTokenToLeakyBucket = new ConcurrentDictionary(); @@ -72,20 +71,16 @@ public async Task> Run(CloneableRequestMessage baseRequest, if (bucket != null) { - var cost = json.SelectToken("extensions.cost"); - if (cost != null) + var graphBucketState = graphRes.GetGraphQLBucketState(json); + if (graphBucketState != null) { - var throttleStatus = cost["throttleStatus"]; - int maximumAvailable = (int)throttleStatus["maximumAvailable"]; - int restoreRate = (int)throttleStatus["restoreRate"]; - int currentlyAvailable = (int)throttleStatus["currentlyAvailable"]; - int actualQueryCost = (int?)cost["actualQueryCost"] ?? graphqlQueryCost.Value;//actual query cost is null if THROTTLED + int actualQueryCost = graphBucketState.ActualQueryCost ?? graphqlQueryCost.Value;//actual query cost is null if THROTTLED int refund = graphqlQueryCost.Value - actualQueryCost;//may be negative if user didn't supply query cost - bucket.SetGraphQLBucketState(maximumAvailable, restoreRate, currentlyAvailable, refund); + bucket.SetGraphQLBucketState(graphBucketState.MaxAvailable, graphBucketState.RestoreRate, graphBucketState.CurrentlyAvailable, refund); //The user might have supplied no cost or an invalid cost //We fix the query cost so the correct value is used if a retry is needed - graphqlQueryCost = (int)cost["requestedQueryCost"]; + graphqlQueryCost = graphBucketState.RequestedQueryCost; } } @@ -110,16 +105,9 @@ public async Task> Run(CloneableRequestMessage baseRequest, if (bucket != null) { - var apiCallLimitHeaderValue = GetRestCallLimit(restRes.Response); - if (apiCallLimitHeaderValue != null) - { - var split = apiCallLimitHeaderValue.Split('/'); - if (split.Length == 2 && int.TryParse(split[0], out int currentlyUsed) && - int.TryParse(split[1], out int maxAvailable)) - { - bucket.SetRESTBucketState(maxAvailable, maxAvailable - currentlyUsed); - } - } + var restBucketState = restRes.GetRestBucketState(); + if (restBucketState != null) + bucket.SetRESTBucketState(restBucketState.MaxAvailable, restBucketState.MaxAvailable - restBucketState.CurrentlyUsed); } return restRes; @@ -149,13 +137,6 @@ public async Task> Run(CloneableRequestMessage baseRequest, } } - private string GetRestCallLimit(HttpResponseMessage response) - { - return response.Headers.FirstOrDefault(kvp => kvp.Key == RESPONSE_HEADER_API_CALL_LIMIT) - .Value - ?.FirstOrDefault(); - } - private string GetAccessToken(HttpRequestMessage client) { return client.Headers.TryGetValues(REQUEST_HEADER_ACCESS_TOKEN, out var values) ? diff --git a/ShopifySharp/Infrastructure/Policies/LeakyBucketState.cs b/ShopifySharp/Infrastructure/Policies/LeakyBucketState.cs deleted file mode 100644 index 53574f83f..000000000 --- a/ShopifySharp/Infrastructure/Policies/LeakyBucketState.cs +++ /dev/null @@ -1,41 +0,0 @@ -using System.Linq; -using System.Net.Http; - -namespace ShopifySharp -{ - public class LeakyBucketState - { - private const string RESPONSE_HEADER_API_CALL_LIMIT = "X-Shopify-Shop-Api-Call-Limit"; - - public int Capacity { get; } - - public int CurrentFillLevel { get; } - - public bool IsFull => CurrentFillLevel == Capacity; - - private LeakyBucketState(int capacity, int currentFillLevel) - { - this.Capacity = capacity; - this.CurrentFillLevel = currentFillLevel; - } - - public static LeakyBucketState Get(HttpResponseMessage response) - { - var headers = response.Headers.FirstOrDefault(kvp => kvp.Key == RESPONSE_HEADER_API_CALL_LIMIT); - var apiCallLimitHeaderValue = headers.Value?.FirstOrDefault(); - - if (apiCallLimitHeaderValue != null) - { - var split = apiCallLimitHeaderValue.Split('/'); - if (split.Length == 2 && - int.TryParse(split[0], out int currentFillLevel) && - int.TryParse(split[1], out int capacity)) - { - return new LeakyBucketState(capacity, currentFillLevel); - } - } - - return null; - } - } -} diff --git a/ShopifySharp/Infrastructure/RequestResult.cs b/ShopifySharp/Infrastructure/RequestResult.cs index 2c9f26fd8..b2b28e814 100644 --- a/ShopifySharp/Infrastructure/RequestResult.cs +++ b/ShopifySharp/Infrastructure/RequestResult.cs @@ -1,4 +1,5 @@ -using System.Net.Http; +using Newtonsoft.Json.Linq; +using System.Net.Http; namespace ShopifySharp { @@ -22,5 +23,15 @@ public RequestResult(HttpResponseMessage response, T result, string rawResult, s this.RawResult = rawResult; this.RawLinkHeaderValue = rawLinkHeaderValue; } + + public RestBucketState GetRestBucketState() + { + return RestBucketState.Get(this.Response); + } + + public GraphQLBucketState GetGraphQLBucketState(JToken response) + { + return GraphQLBucketState.Get(response); + } } } diff --git a/ShopifySharp/Infrastructure/ShopifyRateLimitException.cs b/ShopifySharp/Infrastructure/ShopifyRateLimitException.cs index 4abea07aa..0dfecf6e6 100644 --- a/ShopifySharp/Infrastructure/ShopifyRateLimitException.cs +++ b/ShopifySharp/Infrastructure/ShopifyRateLimitException.cs @@ -13,7 +13,7 @@ public class ShopifyRateLimitException : ShopifyException public int? RetryAfterSeconds { get; private set; } //When a 429 is returned because the bucket is full, Shopify doesn't include the X-Shopify-Shop-Api-Call-Limit header in the response - public ShopifyRateLimitReason Reason => HttpResponse.Headers.Contains(LeakyBucketExecutionPolicy.RESPONSE_HEADER_API_CALL_LIMIT) ? ShopifyRateLimitReason.Other : ShopifyRateLimitReason.BucketFull; + public ShopifyRateLimitReason Reason => HttpResponse.Headers.Contains(RestBucketState.RESPONSE_HEADER_API_CALL_LIMIT) ? ShopifyRateLimitReason.Other : ShopifyRateLimitReason.BucketFull; public ShopifyRateLimitException(HttpResponseMessage response, HttpStatusCode httpStatusCode,