Skip to content

Commit

Permalink
Merge pull request #970 from clement911/master
Browse files Browse the repository at this point in the history
Extracted logic to parse Bucket state information for REST and GraphQL
  • Loading branch information
clement911 authored Dec 13, 2023
2 parents 6111e8f + 5ea115b commit 6cdd3df
Show file tree
Hide file tree
Showing 6 changed files with 99 additions and 70 deletions.
40 changes: 40 additions & 0 deletions ShopifySharp/Infrastructure/BucketStates/GraphQLBucketState.cs
Original file line number Diff line number Diff line change
@@ -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,
};
}
}
}
38 changes: 38 additions & 0 deletions ShopifySharp/Infrastructure/BucketStates/RestBucketState.cs
Original file line number Diff line number Diff line change
@@ -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;
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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<string, MultiShopifyAPIBucket> _shopAccessTokenToLeakyBucket = new ConcurrentDictionary<string, MultiShopifyAPIBucket>();

Expand Down Expand Up @@ -72,20 +71,16 @@ public async Task<RequestResult<T>> Run<T>(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;
}
}

Expand All @@ -110,16 +105,9 @@ public async Task<RequestResult<T>> Run<T>(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;
Expand Down Expand Up @@ -149,13 +137,6 @@ public async Task<RequestResult<T>> Run<T>(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) ?
Expand Down
41 changes: 0 additions & 41 deletions ShopifySharp/Infrastructure/Policies/LeakyBucketState.cs

This file was deleted.

13 changes: 12 additions & 1 deletion ShopifySharp/Infrastructure/RequestResult.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
using System.Net.Http;
using Newtonsoft.Json.Linq;
using System.Net.Http;

namespace ShopifySharp
{
Expand All @@ -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);
}
}
}
2 changes: 1 addition & 1 deletion ShopifySharp/Infrastructure/ShopifyRateLimitException.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down

0 comments on commit 6cdd3df

Please sign in to comment.