Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Introduce Cancellation Token support across all public service APIs. (Resolves #238) #476

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion ShopifySharp.Experimental/ApplicationCredit.fs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
namespace ShopifySharp.Experimental

open System.Net.Http
open System.Threading
open ShopifySharp
open ShopifySharp.Infrastructure

Expand Down Expand Up @@ -62,7 +63,7 @@ module ApplicationCredits =
let req = base.PrepareRequest "application_credits.json"
let data = dict [ "application_credit" => data ]
let content = new JsonContent(data)
base.ExecuteRequestAsync<ApplicationCredit>(req, HttpMethod.Post, content, "usage_charge")
base.ExecuteRequestAsync<ApplicationCredit>(req, HttpMethod.Post, CancellationToken.None, content, "usage_charge")
|> mapTask (fun response -> response.Result)

static member NewService domain accessToken = Service(domain, accessToken)
Expand Down
3 changes: 2 additions & 1 deletion ShopifySharp.Experimental/Charges.fs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
namespace ShopifySharp.Experimental

open System.Net.Http
open System.Threading
open ShopifySharp
open ShopifySharp.Infrastructure

Expand Down Expand Up @@ -63,7 +64,7 @@ module Charges =
let req = base.PrepareRequest "application_charges.json"
let data = dict [ "application_charge" => data ]
let content = new JsonContent(data)
base.ExecuteRequestAsync<Charge>(req, HttpMethod.Post, content, "application_charge")
base.ExecuteRequestAsync<Charge>(req, HttpMethod.Post, CancellationToken.None, content, "application_charge")
|> mapTask (fun response -> response.Result)

static member NewService domain accessToken = Service(domain, accessToken)
Expand Down
9 changes: 6 additions & 3 deletions ShopifySharp.Experimental/Orders.fs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,9 @@

open System.Collections.Generic
open System.Net.Http
open System.Threading
open System.Threading
open System.Threading
open ShopifySharp
open ShopifySharp.Infrastructure

Expand Down Expand Up @@ -274,21 +277,21 @@ module Orders =
let req = base.PrepareRequest "orders.json"
let data = dict [ "order" => order ]
let content = new JsonContent(data)
base.ExecuteRequestAsync<Order>(req, HttpMethod.Post, content, "order")
base.ExecuteRequestAsync<Order>(req, HttpMethod.Post, CancellationToken.None, content, "order")
|> mapTask (fun response -> response.Result)

member x.CreateAsync(order: OrderProperties, options: CreationOptions.CreationOptionProperties) =
let req = base.PrepareRequest "orders.json"
let data = dict [ "order", mergeOrderAndCreationOptions order options |> JsonValue.MapPropertyValuesToObjects ]
let content = new JsonContent(data)
base.ExecuteRequestAsync<Order>(req, HttpMethod.Post, content, "order")
base.ExecuteRequestAsync<Order>(req, HttpMethod.Post, CancellationToken.None, content, "order")
|> mapTask (fun response -> response.Result)

member x.UpdateAsync (id: int64, order: OrderProperties) =
let req = base.PrepareRequest (sprintf "orders/%i.json" id)
let data = dict [ "order" => order ]
let content = new JsonContent(data)
base.ExecuteRequestAsync<Order>(req, HttpMethod.Put, content, "order")
base.ExecuteRequestAsync<Order>(req, HttpMethod.Put, CancellationToken.None, content, "order")
|> mapTask (fun response -> response.Result)

static member NewService domain accessToken = Service(domain, accessToken)
Expand Down
3 changes: 2 additions & 1 deletion ShopifySharp.Experimental/RecurringCharges.fs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
namespace ShopifySharp.Experimental

open System.Net.Http
open System.Threading
open ShopifySharp
open ShopifySharp.Infrastructure

Expand Down Expand Up @@ -75,7 +76,7 @@ module RecurringCharges =
let req = base.PrepareRequest "recurring_application_charges.json"
let data = dict [ "recurring_application_charge" => data ]
let content = new JsonContent(data)
base.ExecuteRequestAsync<RecurringCharge>(req, HttpMethod.Post, content, "recurring_application_charge")
base.ExecuteRequestAsync<RecurringCharge>(req, HttpMethod.Post, CancellationToken.None, content, "recurring_application_charge")
|> mapTask (fun response -> response.Result)

static member NewService domain accessToken = Service(domain, accessToken)
Expand Down
6 changes: 4 additions & 2 deletions ShopifySharp.Experimental/ScriptTags.fs
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
namespace ShopifySharp.Experimental

open System.Net.Http
open System.Threading
open System.Threading
open ShopifySharp
open ShopifySharp.Infrastructure

Expand Down Expand Up @@ -80,14 +82,14 @@ module ScriptTags =
let req = base.PrepareRequest "script_tags.json"
let data = dict [ "script_tag" => tag ]
let content = new JsonContent(data)
base.ExecuteRequestAsync<ScriptTag>(req, HttpMethod.Post, content, "script_tag")
base.ExecuteRequestAsync<ScriptTag>(req, HttpMethod.Post, CancellationToken.None, content, "script_tag")
|> mapTask (fun response -> response.Result)

member x.UpdateAsync (id : int64, tag : ScriptTagProperties) =
let req = base.PrepareRequest (sprintf "script_tags/%i.json" id)
let data = dict [ "script_tag" => tag ]
let content = new JsonContent(data)
base.ExecuteRequestAsync<ScriptTag>(req, HttpMethod.Put, content, "script_tag")
base.ExecuteRequestAsync<ScriptTag>(req, HttpMethod.Put, CancellationToken.None, content, "script_tag")
|> mapTask (fun response -> response.Result)

static member NewService domain accessToken = Service(domain, accessToken)
Expand Down
3 changes: 2 additions & 1 deletion ShopifySharp.Experimental/UsageCharges.fs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
namespace ShopifySharp.Experimental

open System.Net.Http
open System.Threading
open ShopifySharp
open ShopifySharp.Infrastructure

Expand Down Expand Up @@ -55,7 +56,7 @@ module UsageCharges =
let req = base.PrepareRequest (sprintf "recurring_application_charges/%i/usage_charges.json" recurringChargeId)
let data = dict [ "usage_charge" => data ]
let content = new JsonContent(data)
base.ExecuteRequestAsync<UsageCharge>(req, HttpMethod.Post, content, "usage_charge")
base.ExecuteRequestAsync<UsageCharge>(req, HttpMethod.Post, CancellationToken.None, content, "usage_charge")
|> mapTask (fun response -> response.Result)

static member NewService domain accessToken = Service(domain, accessToken)
Expand Down
6 changes: 4 additions & 2 deletions ShopifySharp.Experimental/Webhooks.fs
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
namespace ShopifySharp.Experimental

open System.Net.Http
open System.Threading
open System.Threading
open System.Threading.Tasks
open ShopifySharp
open ShopifySharp.Infrastructure
Expand Down Expand Up @@ -82,15 +84,15 @@ module Webhooks =
let data = dict [ "webhook" => webhook ]
let content = new JsonContent(data)

base.ExecuteRequestAsync<Webhook>(req, HttpMethod.Post, content, "webhook")
base.ExecuteRequestAsync<Webhook>(req, HttpMethod.Post, CancellationToken.None, content, "webhook")
|> mapTask (fun response -> response.Result)

member x.UpdateAsync (id: int64, webhook: WebhookProperties) =
let req = base.PrepareRequest (sprintf "webhooks/%i.json" id)
let data = dict [ "webhook" => webhook ]
let content = new JsonContent(data)

base.ExecuteRequestAsync<Webhook>(req, HttpMethod.Put, content, "webhook")
base.ExecuteRequestAsync<Webhook>(req, HttpMethod.Put, CancellationToken.None, content, "webhook")
|> mapTask (fun response -> response.Result)

static member NewService domain accessToken = Service(domain, accessToken)
Expand Down
188 changes: 188 additions & 0 deletions ShopifySharp.Tests/ExecuteRequestCancellation_Tests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,188 @@
using System;
using System.Net.Http;
using System.Threading;
using System.Threading.Tasks;
using Newtonsoft.Json.Linq;
using ShopifySharp.Filters;
using ShopifySharp.Infrastructure;
using ShopifySharp.Lists;
using Xunit;

namespace ShopifySharp.Tests
{
[Trait("Category", "ExecuteRequestCancellation")]
public class ExecuteRequestCancellation_Tests
{
private class TestService : ShopifyService
{
public TestService() : base("someurl", "sometoken")
{
}

// these are what services call to execute their public API requests
public new Task<T> ExecuteGetAsync<T>(string path, string resultRootElt, string fields, CancellationToken cancellationToken = default)
{
return base.ExecuteGetAsync<T>(path, resultRootElt, fields, cancellationToken);
}

public new Task<T> ExecuteGetAsync<T>(string path, string resultRootElt, Parameterizable queryParams = null, CancellationToken cancellationToken = default)
{
return base.ExecuteGetAsync<T>(path, resultRootElt, queryParams, cancellationToken);
}

public new Task<ListResult<T>> ExecuteGetListAsync<T>(string path, string resultRootElt, ListFilter<T> filter, CancellationToken cancellationToken = default)
{
return base.ExecuteGetListAsync(path, resultRootElt, filter, cancellationToken);
}

public new Task<T> ExecutePostAsync<T>(string path, string resultRootElt, object jsonContent = null, CancellationToken cancellationToken = default)
{
return base.ExecutePostAsync<T>(path, resultRootElt, cancellationToken, jsonContent);
}

public new Task<T> ExecutePutAsync<T>(string path, string resultRootElt, object jsonContent = null, CancellationToken cancellationToken = default)
{
return base.ExecutePutAsync<T>(path, resultRootElt, cancellationToken, jsonContent);
}

public new Task ExecuteDeleteAsync(string path, CancellationToken cancellationToken)
{
return base.ExecuteDeleteAsync(path, cancellationToken);
}

public new Task<RequestResult<JToken>> ExecuteRequestAsync(RequestUri uri, HttpMethod method, HttpContent content = null, CancellationToken cancellationToken = default)
{
return base.ExecuteRequestAsync(uri, method, cancellationToken, content);
}
}

[Fact]
public async Task Cancelling_An_ExecuteGetListAsync_Terminates_HttpRequest()
{
var service = new TestService();
var cts = new CancellationTokenSource();

await Assert.ThrowsAsync<TaskCanceledException>(async () =>
{
var task = service.ExecuteGetListAsync<object>(string.Empty, string.Empty, null, cts.Token);

cts.Cancel();

await task;
});
}

[Fact]
public async Task Cancelling_An_ExecuteGetAsync_Terminates_HttpRequest()
{
var service = new TestService();
var cts = new CancellationTokenSource();

await Assert.ThrowsAsync<TaskCanceledException>(async () =>
{
var task = service.ExecuteGetAsync<object>(string.Empty, string.Empty, string.Empty, cts.Token);

cts.Cancel();

await task;
});
}

[Fact]
public async Task Cancelling_An_ExecuteGetAsyncFilter_Terminates_HttpRequest()
{
var service = new TestService();
var cts = new CancellationTokenSource();

await Assert.ThrowsAsync<TaskCanceledException>(async () =>
{
var task = service.ExecuteGetAsync<object>(string.Empty, string.Empty, new PageCountFilter(), cts.Token);

cts.Cancel();

await task;
});
}

[Fact]
public async Task Cancelling_An_ExecutePutAsync_Terminates_HttpRequest()
{
var service = new TestService();
var cts = new CancellationTokenSource();

await Assert.ThrowsAsync<TaskCanceledException>(async () =>
{
var task = service.ExecutePutAsync<object>(string.Empty, string.Empty, null, cts.Token);

cts.Cancel();

await task;
});
}

[Fact]
public async Task Cancelling_An_ExecutePostAsync_Terminates_HttpRequest()
{
var service = new TestService();
var cts = new CancellationTokenSource();

await Assert.ThrowsAsync<TaskCanceledException>(async () =>
{
var task = service.ExecutePostAsync<object>(string.Empty, string.Empty, null, cts.Token);

cts.Cancel();

await task;
});
}

[Fact]
public async Task Cancelling_An_ExecuteDeleteAsync_Terminates_HttpRequest()
{
var service = new TestService();
var cts = new CancellationTokenSource();

await Assert.ThrowsAsync<TaskCanceledException>(async () =>
{
var task = service.ExecuteDeleteAsync(string.Empty, cts.Token);

cts.Cancel();

await task;
});
}

[Fact]
public async Task Cancelling_An_ExecuteRequestAsync_Terminates_HttpRequest()
{
var service = new TestService();
var cts = new CancellationTokenSource();

await Assert.ThrowsAsync<TaskCanceledException>(async () =>
{
var task = service.ExecuteRequestAsync(new RequestUri(new Uri("http://unreachable")), HttpMethod.Get, null, cts.Token);

cts.Cancel();

await task;
});
}

private static OrderService OrderService => new OrderService(Utils.MyShopifyUrl, Utils.AccessToken);

[Fact]
public async Task Cancelling_An_OrderService_List_Terminates_HttpRequest()
{
var cts = new CancellationTokenSource();

await Assert.ThrowsAsync<TaskCanceledException>(async () =>
{
var task = OrderService.ListAsync(cancellationToken: cts.Token);

cts.Cancel();

await task;
});
}
}
}
Original file line number Diff line number Diff line change
@@ -1,12 +1,13 @@
using System.Net.Http;
using System.Threading;
using System.Threading.Tasks;
using ShopifySharp.Infrastructure;

namespace ShopifySharp
{
public class DefaultRequestExecutionPolicy : IRequestExecutionPolicy
{
public async Task<RequestResult<T>> Run<T>(CloneableRequestMessage request, ExecuteRequestAsync<T> executeRequestAsync)
public async Task<RequestResult<T>> Run<T>(CloneableRequestMessage request, ExecuteRequestAsync<T> executeRequestAsync, CancellationToken cancellationToken)
{
var fullResult = await executeRequestAsync(request);

Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
using System.Net.Http;
using System.Threading;
using System.Threading.Tasks;
using ShopifySharp.Infrastructure;

Expand All @@ -14,6 +15,7 @@ public interface IRequestExecutionPolicy
{
/// <param name="baseRequest">The base request that was built by a service to execute.</param>
/// <param name="executeRequestAsync">A delegate that executes the request you pass to it.</param>
Task<RequestResult<T>> Run<T>(CloneableRequestMessage requestMessage, ExecuteRequestAsync<T> executeRequestAsync);
/// <param name="cancellationToken">Cancellation token</param>
Task<RequestResult<T>> Run<T>(CloneableRequestMessage requestMessage, ExecuteRequestAsync<T> executeRequestAsync, CancellationToken cancellationToken);
}
}
5 changes: 3 additions & 2 deletions ShopifySharp/Infrastructure/Policies/RetryExecutionPolicy.cs
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
using System;
using System.Net.Http;
using System.Threading;
using System.Threading.Tasks;
using ShopifySharp.Infrastructure;

Expand All @@ -19,7 +20,7 @@ public RetryExecutionPolicy(bool retryOnlyIfLeakyBucketFull = true)
_retryOnlyIfLeakyBucketFull = retryOnlyIfLeakyBucketFull;
}

public async Task<RequestResult<T>> Run<T>(CloneableRequestMessage baseRequest, ExecuteRequestAsync<T> executeRequestAsync)
public async Task<RequestResult<T>> Run<T>(CloneableRequestMessage baseRequest, ExecuteRequestAsync<T> executeRequestAsync, CancellationToken cancellationToken)
{
while (true)
{
Expand All @@ -36,7 +37,7 @@ public async Task<RequestResult<T>> Run<T>(CloneableRequestMessage baseRequest,
//Only retry if breach caused by full bucket
//Other limits will bubble the exception because it's not clear how long the program should wait
//Even if there is a Retry-After header, we probably don't want the thread to sleep for potentially many hours
await Task.Delay(RETRY_DELAY);
await Task.Delay(RETRY_DELAY, cancellationToken);
}
}
}
Expand Down
Loading