Skip to content

Commit

Permalink
Merge pull request #2 from vinhch/1.0.2
Browse files Browse the repository at this point in the history
1.0.2 - Update solution to VS 2017, fix some bugs and clean
  • Loading branch information
vinhch authored Sep 8, 2017
2 parents 88a75d1 + 6ded559 commit acbb7d7
Show file tree
Hide file tree
Showing 22 changed files with 317 additions and 296 deletions.
18 changes: 11 additions & 7 deletions BizwebSharp.sln
Original file line number Diff line number Diff line change
@@ -1,23 +1,24 @@

Microsoft Visual Studio Solution File, Format Version 12.00
# Visual Studio 14
VisualStudioVersion = 14.0.25420.1
# Visual Studio 15
VisualStudioVersion = 15.0.26730.12
MinimumVisualStudioVersion = 10.0.40219.1
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{AA588B91-76DE-441C-822A-168270211EDE}"
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{7B0BA252-0544-4E62-8F91-74DAC7FB4319}"
ProjectSection(SolutionItems) = preProject
global.json = global.json
.gitignore = .gitignore
LICENSE = LICENSE
README.md = README.md
EndProjectSection
EndProject
Project("{8BB2217D-0F2D-49D1-97BC-3654ED321F3B}") = "BizwebSharp", "src\BizwebSharp\BizwebSharp.xproj", "{EEDC2EEE-661D-45E1-AC2A-481CAC21B62E}"
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "test", "test", "{ED6F8E49-B4A1-451E-900B-BC0D61375D6D}"
EndProject
Project("{8BB2217D-0F2D-49D1-97BC-3654ED321F3B}") = "BizwebSharp.Tests.xUnit", "test\BizwebSharp.Tests.xUnit\BizwebSharp.Tests.xUnit.xproj", "{6D67105D-9C20-4780-9178-E139B714BC16}"
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "BizwebSharp", "src\BizwebSharp\BizwebSharp.csproj", "{EEDC2EEE-661D-45E1-AC2A-481CAC21B62E}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "BizwebSharp.Tests.xUnit", "test\BizwebSharp.Tests.xUnit\BizwebSharp.Tests.xUnit.csproj", "{6D67105D-9C20-4780-9178-E139B714BC16}"
EndProject
Project("{8BB2217D-0F2D-49D1-97BC-3654ED321F3B}") = "BizwebSharp.ConsoleTests", "test\BizwebSharp.ConsoleTests\BizwebSharp.ConsoleTests.xproj", "{043BFF7B-40E9-4B94-8F1E-9EB89AF56E3B}"
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "BizwebSharp.ConsoleTests", "test\BizwebSharp.ConsoleTests\BizwebSharp.ConsoleTests.csproj", "{043BFF7B-40E9-4B94-8F1E-9EB89AF56E3B}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Expand Down Expand Up @@ -46,4 +47,7 @@ Global
{6D67105D-9C20-4780-9178-E139B714BC16} = {ED6F8E49-B4A1-451E-900B-BC0D61375D6D}
{043BFF7B-40E9-4B94-8F1E-9EB89AF56E3B} = {ED6F8E49-B4A1-451E-900B-BC0D61375D6D}
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {640EE046-6045-4D27-8586-119834F52EAB}
EndGlobalSection
EndGlobal
6 changes: 0 additions & 6 deletions global.json

This file was deleted.

28 changes: 28 additions & 0 deletions src/BizwebSharp/BizwebSharp.csproj
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>
21 changes: 0 additions & 21 deletions src/BizwebSharp/BizwebSharp.xproj

This file was deleted.

2 changes: 1 addition & 1 deletion src/BizwebSharp/Entities/Base/BaseEntityCanPublishable.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@

namespace BizwebSharp
{
public class BaseEntityCanPublishable : BaseEntityWithTimeline
public abstract class BaseEntityCanPublishable : BaseEntityWithTimeline
{
[JsonProperty("published_on", DefaultValueHandling = DefaultValueHandling.Include,
NullValueHandling = NullValueHandling.Include)]
Expand Down
16 changes: 16 additions & 0 deletions src/BizwebSharp/Entities/Customer.cs
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,22 @@ public class Customer : BaseEntityWithTimeline
[JsonProperty("orders_count")]
public int? OrdersCount { get; set; }

/// <summary>
/// The phone number for the customer. Valid formats can be of different types, for example:
///
/// 6135551212
///
/// +16135551212
///
/// 555-1212
///
/// (613)555-1212
///
/// +1 613-555-1212
/// </summary>
[JsonProperty("phone")]
public string Phone { get; set; }

/// <summary>
/// The state of the customer in a shop. Valid values are 'disabled', 'decline', 'invited' and 'enabled'.
/// </summary>
Expand Down
1 change: 1 addition & 0 deletions src/BizwebSharp/Infrastructure/ApiConst.cs
Original file line number Diff line number Diff line change
Expand Up @@ -5,5 +5,6 @@ internal static class ApiConst
internal const string HEADER_KEY_ACCESS_TOKEN = "X-Bizweb-Access-Token";
internal const string BIZWEB_API_DOMAIN = ".bizwebvietnam.net";
internal const string HEADER_KEY_HMAC_SHA256 = "X-Bizweb-Hmac-Sha256";
internal const string HEADER_API_CALL_LIMIT = "X-Bizweb-Api-Call-Limit";
}
}
56 changes: 30 additions & 26 deletions src/BizwebSharp/Infrastructure/RequestEngine.cs
Original file line number Diff line number Diff line change
Expand Up @@ -74,41 +74,43 @@ public static ICustomRestRequest CreateRequest(string path, Method method, strin

public static void CheckResponseExceptions(IRestResponse response)
{
if ((response.StatusCode != HttpStatusCode.OK) && (response.StatusCode != HttpStatusCode.Created))
var statusCode = (int)response.StatusCode;
if (statusCode < 200 || statusCode >= 300)
{
var rawResponse = response.Content;
var errors = ParseErrorJson(rawResponse);
var requestInfo = new RequestSimpleInfo(response);
return;
}

var rawResponse = response.Content;
var errors = ParseErrorJson(rawResponse);
var requestInfo = new RequestSimpleInfo(response);

var code = response.StatusCode;
var message = $"Response did not indicate success. Status: {(int) code} {response.StatusDescription}.";
var message = $"Response did not indicate success. Status: {statusCode} {response.StatusDescription}.";

if (errors == null)
if (errors == null)
{
errors = new Dictionary<string, IEnumerable<string>>
{
errors = new Dictionary<string, IEnumerable<string>>
{
{
$"{(int) code} {response.StatusDescription}",
new[] {message}
}
};
}
else
{
var firstError = errors.First();

message = $"{firstError.Key}: {string.Join(", ", firstError.Value)}";
}
$"{statusCode} {response.StatusDescription}",
new[] {message}
}
};
}
else
{
var firstError = errors.First();

// If the error was caused by reaching the API rate limit, throw a rate limit exception.
if ((int) code == 429 /* Too many requests */)
{
throw new ApiRateLimitException(code, errors, message, rawResponse, requestInfo);
}
message = $"{firstError.Key}: {string.Join(", ", firstError.Value)}";
}

throw new BizwebSharpException(code, errors, message, rawResponse, requestInfo);
// If the error was caused by reaching the API rate limit, throw a rate limit exception.
if (statusCode == 429 /* Too many requests */)
{
throw new ApiRateLimitException(response.StatusCode, errors, message, rawResponse, requestInfo);
}

throw new BizwebSharpException(response.StatusCode, errors, message, rawResponse, requestInfo);

//if (response.ErrorException != null)
//{
// //Checking this second, because Shopify errors sometimes return incomplete objects along with errors,
Expand Down Expand Up @@ -233,6 +235,8 @@ public static async Task<T> ExecuteRequestAsync<T>(IRestClient baseClient, ICust
{
RootElement = request.RootElement
};

//Notice: deserialize can fails when response body null or empty
var result = deserializer.Deserialize<T>(response);

return new RequestResult<T>(response, result);
Expand Down
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());
}
}
}
}
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;
}
}
}
19 changes: 0 additions & 19 deletions src/BizwebSharp/Properties/AssemblyInfo.cs

This file was deleted.

Loading

0 comments on commit acbb7d7

Please sign in to comment.