-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #2 from vinhch/1.0.2
1.0.2 - Update solution to VS 2017, fix some bugs and clean
- Loading branch information
Showing
22 changed files
with
317 additions
and
296 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file was deleted.
Oops, something went wrong.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,28 @@ | ||
<Project Sdk="Microsoft.NET.Sdk"> | ||
|
||
<PropertyGroup> | ||
<Description>BizwebSharp is a C# and .NET client library for Bizweb.vn API</Description> | ||
<VersionPrefix>1.0.1</VersionPrefix> | ||
<Authors>Cao Ha Vinh</Authors> | ||
<TargetFrameworks>netstandard1.6;netstandard1.3</TargetFrameworks> | ||
<AssemblyName>BizwebSharp</AssemblyName> | ||
<PackageId>BizwebSharp</PackageId> | ||
<RootNamespace>BizwebSharp</RootNamespace> | ||
<NetStandardImplicitPackageVersion>1.6.1</NetStandardImplicitPackageVersion> | ||
<PackageTargetFallback Condition=" '$(TargetFramework)' == 'netstandard1.6' ">$(PackageTargetFallback);dnxcore50</PackageTargetFallback> | ||
<PackageTargetFallback Condition=" '$(TargetFramework)' == 'netstandard1.3' ">$(PackageTargetFallback);dnxcore50</PackageTargetFallback> | ||
<GenerateAssemblyConfigurationAttribute>false</GenerateAssemblyConfigurationAttribute> | ||
<GenerateAssemblyCompanyAttribute>false</GenerateAssemblyCompanyAttribute> | ||
<GenerateAssemblyProductAttribute>false</GenerateAssemblyProductAttribute> | ||
</PropertyGroup> | ||
|
||
<ItemGroup> | ||
<PackageReference Include="FubarCoder.RestSharp.Portable.HttpClient" Version="4.0.8" /> | ||
<PackageReference Include="Microsoft.Extensions.Primitives" Version="1.1.1" /> | ||
<PackageReference Include="Newtonsoft.Json" Version="10.0.3" /> | ||
<PackageReference Include="System.Collections.Specialized" Version="4.3.0" /> | ||
<PackageReference Include="System.Reflection.TypeExtensions" Version="4.3.0" /> | ||
<PackageReference Include="System.Threading.Tasks.Parallel" Version="4.3.0" /> | ||
</ItemGroup> | ||
|
||
</Project> |
This file was deleted.
Oops, something went wrong.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
62 changes: 62 additions & 0 deletions
62
src/BizwebSharp/Infrastructure/RequestPolicies/SmartRetryExecutionPolicy.LeakyBucket.cs
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,62 @@ | ||
using System.Collections.Concurrent; | ||
using System.Threading; | ||
using System.Threading.Tasks; | ||
|
||
namespace BizwebSharp.Infrastructure | ||
{ | ||
public partial class SmartRetryExecutionPolicy | ||
{ | ||
private class LeakyBucket | ||
{ | ||
private const int BUCKET_MAX_SIZE = 40; | ||
|
||
private static readonly ConcurrentBag<LeakyBucket> _allLeakyBuckets = new ConcurrentBag<LeakyBucket>(); | ||
|
||
private static Timer _dripAllBucketsTimer = new Timer(_ => DripAllBuckets(), null, THROTTLE_DELAY, THROTTLE_DELAY); | ||
|
||
private readonly SemaphoreSlim _semaphore = new SemaphoreSlim(BUCKET_MAX_SIZE, BUCKET_MAX_SIZE); | ||
|
||
public LeakyBucket() | ||
{ | ||
_allLeakyBuckets.Add(this); | ||
} | ||
|
||
public Task GrantAsync() | ||
{ | ||
return _semaphore.WaitAsync(); | ||
} | ||
|
||
public void SetContentSize(int contentSize) | ||
{ | ||
//Corrects the grant capacity of the bucket based on the size returned by Shopify. | ||
//Shopify may know that the remaining capacity is less than we think it is (for example if multiple programs are using that same token) | ||
//Shopify may also think that the remaining capacity is more than we know, but we do not ever empty the bucket because Shopify is not | ||
//considering requests that we know are already in flight. | ||
var grantCapacity = BUCKET_MAX_SIZE - contentSize; | ||
|
||
while (_semaphore.CurrentCount > grantCapacity) | ||
{ | ||
//We refill the virtual bucket accordingly. | ||
_semaphore.Wait(); | ||
} | ||
} | ||
|
||
private void ReleaseOne() | ||
{ | ||
if (_semaphore.CurrentCount < BUCKET_MAX_SIZE) | ||
{ | ||
_semaphore.Release(); | ||
} | ||
} | ||
|
||
private static void DripAllBuckets() | ||
{ | ||
//foreach (var bucket in _allLeakyBuckets) | ||
//{ | ||
// bucket.ReleaseOne(); | ||
//} | ||
Parallel.ForEach(_allLeakyBuckets, bucket => bucket.ReleaseOne()); | ||
} | ||
} | ||
} | ||
} |
99 changes: 99 additions & 0 deletions
99
src/BizwebSharp/Infrastructure/RequestPolicies/SmartRetryExecutionPolicy.cs
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,99 @@ | ||
using System; | ||
using System.Collections.Concurrent; | ||
using System.Linq; | ||
using System.Threading.Tasks; | ||
using RestSharp.Portable; | ||
|
||
namespace BizwebSharp.Infrastructure | ||
{ | ||
/// <summary> | ||
/// A retry policy that attemps to pro-actively limit the number of requests that will result in a ShopifyRateLimitException | ||
/// by implementing the leaky bucket algorithm. | ||
/// For example: if 100 requests are created in parallel, only 40 should be immediately sent, and the subsequent 60 requests | ||
/// should be throttled at 1 per 500ms. | ||
/// </summary> | ||
/// <remarks> | ||
/// In comparison, the naive retry policy will issue the 100 requests immediately: | ||
/// 60 requests will fail and be retried after 500ms, | ||
/// 59 requests will fail and be retried after 500ms, | ||
/// 58 requests will fail and be retried after 500ms. | ||
/// See https://help.shopify.com/api/guides/api-call-limit | ||
/// https://en.wikipedia.org/wiki/Leaky_bucket | ||
/// </remarks> | ||
public partial class SmartRetryExecutionPolicy : IRequestExecutionPolicy | ||
{ | ||
private static readonly TimeSpan THROTTLE_DELAY = TimeSpan.FromMilliseconds(500); | ||
|
||
private static readonly ConcurrentDictionary<string, LeakyBucket> _shopAccessTokenToLeakyBucket = new ConcurrentDictionary<string, LeakyBucket>(); | ||
|
||
public async Task<T> Run<T>(IRestClient baseClient, ICustomRestRequest request, ExecuteRequestAsync<T> executeRequestAsync) | ||
{ | ||
var accessToken = GetAccessToken(baseClient); | ||
LeakyBucket bucket = null; | ||
|
||
if (accessToken != null) | ||
{ | ||
bucket = _shopAccessTokenToLeakyBucket.GetOrAdd(accessToken, _ => new LeakyBucket()); | ||
} | ||
|
||
while (true) | ||
{ | ||
if (accessToken != null) | ||
{ | ||
await bucket.GrantAsync(); | ||
} | ||
|
||
try | ||
{ | ||
var fullResult = await executeRequestAsync(baseClient); | ||
var bucketContentSize = GetBucketContentSize(fullResult.Response); | ||
|
||
if (bucketContentSize != null) | ||
{ | ||
bucket?.SetContentSize(bucketContentSize.Value); | ||
} | ||
|
||
return fullResult.Result; | ||
} | ||
catch (BizwebSharpException) | ||
{ | ||
//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 instance of the same program may use the same access token | ||
await Task.Delay(THROTTLE_DELAY); | ||
} | ||
} | ||
} | ||
|
||
private static string GetAccessToken(IRestClient client) | ||
{ | ||
return client.DefaultParameters | ||
.SingleOrDefault(p => p.Type == ParameterType.HttpHeader && | ||
string.Equals(p.Name, ApiConst.HEADER_KEY_ACCESS_TOKEN, | ||
StringComparison.CurrentCultureIgnoreCase)) | ||
?.Value | ||
?.ToString(); | ||
} | ||
|
||
private static int? GetBucketContentSize(IRestResponse response) | ||
{ | ||
var headers = response.Headers.FirstOrDefault(kvp => string.Equals(kvp.Key, ApiConst.HEADER_API_CALL_LIMIT, | ||
StringComparison.CurrentCultureIgnoreCase)); | ||
|
||
var apiCallLimitHeaderValue = headers.Value?.FirstOrDefault(); | ||
|
||
if (apiCallLimitHeaderValue != null) | ||
{ | ||
if (int.TryParse(apiCallLimitHeaderValue.Split('/').First(), out int bucketContentSize)) | ||
{ | ||
return bucketContentSize; | ||
} | ||
} | ||
|
||
return null; | ||
} | ||
} | ||
} |
This file was deleted.
Oops, something went wrong.
Oops, something went wrong.