How to implement CancellationToken support in DropNet? - c#

I want to asynchronously access DropBox API in a MonoTouch app.
I thought it would be convenient to use DropNet which itself relies on RestSharp.
Both libraries work well but DropNet overloads that return Tasks don't give you a way to associate requests with cancellation tokens.
This is how their implementation looks:
public Task<IRestResponse> GetThumbnailTask(string path, ThumbnailSize size)
{
if (!path.StartsWith("/")) path = "/" + path;
var request = _requestHelper.CreateThumbnailRequest(path, size, Root);
return ExecuteTask(ApiType.Content, request, cancel);
}
ExecuteTask implementation is based on TaskCompletionSource and was originally written by Laurent Kempé:
public static class RestClientExtensions
{
public static Task<TResult> ExecuteTask<TResult>(
this IRestClient client, IRestRequest request
) where TResult : new()
{
var tcs = new TaskCompletionSource<TResult>();
WaitCallback asyncWork = _ => {
try {
client.ExecuteAsync<TResult>(request,
(response, asynchandle) => {
if (response.StatusCode != HttpStatusCode.OK) {
tcs.SetException(new DropboxException(response));
} else {
tcs.SetResult(response.Data);
}
}
);
} catch (Exception exc) {
tcs.SetException(exc);
}
};
return ExecuteTask(asyncWork, tcs);
}
public static Task<IRestResponse> ExecuteTask(
this IRestClient client, IRestRequest request
)
{
var tcs = new TaskCompletionSource<IRestResponse>();
WaitCallback asyncWork = _ => {
try {
client.ExecuteAsync(request,
(response, asynchandle) => {
if (response.StatusCode != HttpStatusCode.OK) {
tcs.SetException(new DropboxException(response));
} else {
tcs.SetResult(response);
}
}
);
} catch (Exception exc) {
tcs.SetException(exc);
}
};
return ExecuteTask(asyncWork, tcs);
}
private static Task<TResult> ExecuteTask<TResult>(
WaitCallback asyncWork, TaskCompletionSource<TResult> tcs
)
{
ThreadPool.QueueUserWorkItem(asyncWork);
return tcs.Task;
}
}
How do I change or extend this code to support cancellation with CancellationToken?
I'd like to call it like this:
var task = dropbox.GetThumbnailTask(
"/test.jpg", ThumbnailSize.ExtraLarge2, _token
);

Because CancellationToken is a value type, we can add it as an optional parameter to the APIs with default value and avoid null checks, which is sweet.
public Task<IRestResponse> GetThumbnailTask(
string path, ThumbnailSize size, CancellationToken cancel = default(CancellationToken)
) {
if (!path.StartsWith("/")) path = "/" + path;
var request = _requestHelper.CreateThumbnailRequest(path, size, Root);
return ExecuteTask(ApiType.Content, request, cancel);
}
Now, RestSharp ExecuteAsync method returns RestRequestAsyncHandle that encapsulates underlying HttpWebRequest, along with Abort method. This is how we cancel things.
public static Task<TResult> ExecuteTask<TResult>(
this IRestClient client, IRestRequest request, CancellationToken cancel = default(CancellationToken)
) where TResult : new()
{
var tcs = new TaskCompletionSource<TResult>();
try {
var async = client.ExecuteAsync<TResult>(request, (response, _) => {
if (cancel.IsCancellationRequested || response == null)
return;
if (response.StatusCode != HttpStatusCode.OK) {
tcs.TrySetException(new DropboxException(response));
} else {
tcs.TrySetResult(response.Data);
}
});
cancel.Register(() => {
async.Abort();
tcs.TrySetCanceled();
});
} catch (Exception ex) {
tcs.TrySetException(ex);
}
return tcs.Task;
}
public static Task<IRestResponse> ExecuteTask(this IRestClient client, IRestRequest request, CancellationToken cancel = default(CancellationToken))
{
var tcs = new TaskCompletionSource<IRestResponse>();
try {
var async = client.ExecuteAsync<IRestResponse>(request, (response, _) => {
if (cancel.IsCancellationRequested || response == null)
return;
if (response.StatusCode != HttpStatusCode.OK) {
tcs.TrySetException(new DropboxException(response));
} else {
tcs.TrySetResult(response);
}
});
cancel.Register(() => {
async.Abort();
tcs.TrySetCanceled();
});
} catch (Exception ex) {
tcs.TrySetException(ex);
}
return tcs.Task;
}
Finally, Lauren's implementation puts requests in thread pool but I don't see a reason to do so—ExecuteAsync is asynchronous itself. So I don't do that.
And that's it for cancelling DropNet operations.
I also made a few tweaks which may be useful to you.
Because I had no way of scheduling DropBox Tasks without resorting to wrapping ExecuteTask calls into another Tasks, I decided to find an optimal concurrency level for requests, which turned out to be 4 for me, and set it explicitly:
static readonly Uri DropboxContentHost = new Uri("https://api-content.dropbox.com");
static DropboxService()
{
var point = ServicePointManager.FindServicePoint(DropboxContentHost);
point.ConnectionLimit = 4;
}
I was also content with letting unhandled task exceptions rot in hell, so that's what I did:
TaskScheduler.UnobservedTaskException += (sender, e) => {
e.SetObserved();
};
One last observation is you shouldn't call Start() on a task returned by DropNet because the task starts right away. If you don't like that, you'll need to wrap ExecuteTask in yet another “real” task not backed by TaskCompletionSource.

Related

Restricting SendAsync calls to 5 messages per second

I'm implementing Binance's API.
The documentation says:
WebSocket connections have a limit of 5 incoming messages per second. A message is considered:
A PING frame
A PONG frame
A JSON controlled message (e.g. subscribe, unsubscribe)
For ex. there is a simple web socket wrapper such as the one from the official Binance Connector. According to the limitation above, SendAsync should be restricted 5 messages per second. If a few threads call SendAsync 5 times at the same time (including PING frame which is built-in the ClientWebSocket class), it's going to fail. How can I solve the issue with that limitation gracefully? Using bounded channels is a solution?
public class BinanceWebSocket : IDisposable
{
private IBinanceWebSocketHandler handler;
private List<Func<string, Task>> onMessageReceivedFunctions;
private List<CancellationTokenRegistration> onMessageReceivedCancellationTokenRegistrations;
private CancellationTokenSource loopCancellationTokenSource;
private Uri url;
private int receiveBufferSize;
public BinanceWebSocket(IBinanceWebSocketHandler handler, string url, int receiveBufferSize = 8192)
{
this.handler = handler;
this.url = new Uri(url);
this.receiveBufferSize = receiveBufferSize;
this.onMessageReceivedFunctions = new List<Func<string, Task>>();
this.onMessageReceivedCancellationTokenRegistrations = new List<CancellationTokenRegistration>();
}
public async Task ConnectAsync(CancellationToken cancellationToken)
{
if (this.handler.State != WebSocketState.Open)
{
this.loopCancellationTokenSource = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken);
await this.handler.ConnectAsync(this.url, cancellationToken);
await Task.Factory.StartNew(() => this.ReceiveLoop(loopCancellationTokenSource.Token, this.receiveBufferSize), loopCancellationTokenSource.Token, TaskCreationOptions.LongRunning, TaskScheduler.Default);
}
}
public async Task DisconnectAsync(CancellationToken cancellationToken)
{
if (this.loopCancellationTokenSource != null)
{
this.loopCancellationTokenSource.Cancel();
}
if (this.handler.State == WebSocketState.Open)
{
await this.handler.CloseOutputAsync(WebSocketCloseStatus.NormalClosure, null, cancellationToken);
await this.handler.CloseAsync(WebSocketCloseStatus.NormalClosure, null, cancellationToken);
}
}
public void OnMessageReceived(Func<string, Task> onMessageReceived, CancellationToken cancellationToken)
{
this.onMessageReceivedFunctions.Add(onMessageReceived);
if (cancellationToken != CancellationToken.None)
{
var reg = cancellationToken.Register(() =>
this.onMessageReceivedFunctions.Remove(onMessageReceived));
this.onMessageReceivedCancellationTokenRegistrations.Add(reg);
}
}
public async Task SendAsync(string message, CancellationToken cancellationToken)
{
byte[] byteArray = Encoding.ASCII.GetBytes(message);
await this.handler.SendAsync(new ArraySegment<byte>(byteArray), WebSocketMessageType.Text, true, cancellationToken);
}
public void Dispose()
{
this.DisconnectAsync(CancellationToken.None).Wait();
this.handler.Dispose();
this.onMessageReceivedCancellationTokenRegistrations.ForEach(ct => ct.Dispose());
this.loopCancellationTokenSource.Dispose();
}
private async Task ReceiveLoop(CancellationToken cancellationToken, int receiveBufferSize = 8192)
{
WebSocketReceiveResult receiveResult = null;
try
{
while (!cancellationToken.IsCancellationRequested)
{
var buffer = new ArraySegment<byte>(new byte[receiveBufferSize]);
receiveResult = await this.handler.ReceiveAsync(buffer, cancellationToken);
if (receiveResult.MessageType == WebSocketMessageType.Close)
{
break;
}
string content = Encoding.UTF8.GetString(buffer.ToArray());
this.onMessageReceivedFunctions.ForEach(omrf => omrf(content));
}
}
catch (TaskCanceledException)
{
await this.DisconnectAsync(CancellationToken.None);
}
}
}
Second way which I'm not 100% sure it solves it
SendAsync is being called in a loop using Channels. SingleReader is set to true, which means there will be only one consumer at a time. It technically should solve the issue, but I'm not 100% sure because the channel might only be limiting the amount in the buffer.
private readonly Channel<string> _messagesTextToSendQueue = Channel.CreateUnbounded<string>(new UnboundedChannelOptions()
{
SingleReader = true,
SingleWriter = false
});
public ValueTask SendAsync(string message)
{
Validations.Validations.ValidateInput(message, nameof(message));
return _messagesTextToSendQueue.Writer.WriteAsync(message);
}
public void Send(string message)
{
Validations.Validations.ValidateInput(message, nameof(message));
_messagesTextToSendQueue.Writer.TryWrite(message);
}
private async Task SendTextFromQueue()
{
try
{
while (await _messagesTextToSendQueue.Reader.WaitToReadAsync())
{
while (_messagesTextToSendQueue.Reader.TryRead(out var message))
{
try
{
await SendInternalSynchronized(message).ConfigureAwait(false);
}
catch (Exception e)
{
Logger.Error(e, L($"Failed to send text message: '{message}'. Error: {e.Message}"));
}
}
}
}
catch (TaskCanceledException)
{
// task was canceled, ignore
}
catch (OperationCanceledException)
{
// operation was canceled, ignore
}
catch (Exception e)
{
if (_cancellationTotal.IsCancellationRequested || _disposing)
{
// disposing/canceling, do nothing and exit
return;
}
Logger.Trace(L($"Sending text thread failed, error: {e.Message}. Creating a new sending thread."));
StartBackgroundThreadForSendingText();
}
}
I would try to keep it as simple as possible and use Semaphore Slim to achieve this, I have created a class to perform this task.
public class ThrottlingLimiter
{
private readonly SemaphoreSlim _semaphore;
private readonly TimeSpan _timeUnit;
public ThrottlingLimiter(int maxActionsPerTimeUnit, TimeSpan timeUnit)
{
if (maxActionsPerTimeUnit < 1)
throw new ArgumentOutOfRangeException(nameof(maxActionsPerTimeUnit));
if (timeUnit < TimeSpan.Zero || timeUnit.TotalMilliseconds > int.MaxValue)
throw new ArgumentOutOfRangeException(nameof(timeUnit));
_semaphore = new SemaphoreSlim(maxActionsPerTimeUnit, maxActionsPerTimeUnit);
_timeUnit = timeUnit;
}
public async Task WaitAsync(CancellationToken cancellationToken = default)
{
await _semaphore.WaitAsync(cancellationToken).ConfigureAwait(false);
ScheduleSemaphoreRelease();
}
private async void ScheduleSemaphoreRelease()
{
await Task.Delay(_timeUnit).ConfigureAwait(false);
_semaphore.Release();
}
}
Now to Use this class, all you have to do is set your limit and timeSpan
public async Task SendData(List<string> allMessages)
{
// Limiting 5 calls per second
ThrottlingLimiter throttlingLimiter = new ThrottlingLimiter(5, TimeSpan.FromSeconds(1));
await Task.WhenAll(allMessages.Select(async message =>
{
await throttlingLimiter.WaitAsync();
try {
await SendInternalSynchronized(message);
// I am not sure what this SendInternalSynchronized returns but I would return some thing to keep a track if this call is successful or not
}
catch (Exception e)
{
Logger.Error(e, L($"Failed to send text message: {message}'. Error: {e.Message}"));
}
});
}
so basically what will happen here is, no matter how big your list is, the ThrottlingLimiter will only send 5 messages per second and wait for the next second to send the next 5 messages.
so, in your case, get all the data from your call to
await _messagesTextToSendQueue.Reader.WaitToReadAsync();
store that into a list or any collection and pass that to the SendData function.

Polly doesn't handle an exception in a task because of Task.WhenAny

When our connection drops, ReceiveAsync is throwing WebSocketException (ex.WebSocketErrorCode == WebSocketError.ConnectionClosedPrematurely).
The issue is that it's not handled by Polly for some reason. I believe it doesn't handle it, because it's in a separate Task, although I'm doing Task.WhenAny.
The expected behavior is to trigger the reconnect if WebSocketException is thrown.
public sealed class ChannelWebSocketClient : IDisposable
{
private readonly Uri _uri;
private readonly ILogger<ChannelWebSocketClient> _logger;
private readonly Channel<string> _output;
private CancellationTokenSource? _cancellationTokenSource;
public ChannelWebSocketClient(Uri uri, ILoggerFactory loggerFactory)
{
_uri = uri ?? throw new ArgumentNullException(nameof(uri));
_logger = loggerFactory.CreateLogger<ChannelWebSocketClient>();
_output = Channel.CreateUnbounded<string>(new UnboundedChannelOptions
{
SingleReader = true,
SingleWriter = false
});
}
public void Dispose()
{
_output.Writer.TryComplete();
}
public Task StartAsync()
{
return Policy.Handle<Exception>(ex => ex is not (TaskCanceledException or OperationCanceledException))
.WaitAndRetryForeverAsync(
(_, _) => TimeSpan.FromSeconds(5),
(ex, retryCount, calculatedWaitDuration, _) => { _logger.LogError(ex, "Unable to connect to the web socket server. Retry count: {RetryCount} | Retry in {Seconds} seconds", retryCount, calculatedWaitDuration.TotalSeconds); })
.ExecuteAsync(ConnectAsync);
}
public void Stop()
{
_cancellationTokenSource?.Cancel();
}
private async Task ConnectAsync()
{
_logger.LogDebug("Connecting");
using var ws = new ClientWebSocket();
// WebSocketException, TaskCanceledException
await ws.ConnectAsync(_uri, CancellationToken.None).ConfigureAwait(false);
_logger.LogDebug("Connected to {Host}", _uri.AbsoluteUri);
_cancellationTokenSource = new CancellationTokenSource();
var receiving = ReceiveLoopAsync(ws, _cancellationTokenSource.Token);
var sending = SendLoopAsync(ws, _cancellationTokenSource.Token);
var trigger = await Task.WhenAny(receiving, sending).ConfigureAwait(false);
if (trigger == receiving)
{
_cancellationTokenSource?.Cancel();
await sending.ConfigureAwait(false);
}
_logger.LogDebug("END");
}
public async Task SendAsync(string message)
{
await _output.Writer.WriteAsync(message, CancellationToken.None).ConfigureAwait(false);
}
private async Task SendLoopAsync(WebSocket webSocket, CancellationToken cancellationToken)
{
_logger.LogDebug("SendLoopAsync BEGIN");
try
{
while (await _output.Reader.WaitToReadAsync(cancellationToken).ConfigureAwait(false))
{
while (_output.Reader.TryRead(out var message))
{
// WebSocketException, TaskCanceledException, ObjectDisposedException
await webSocket.SendAsync(new ArraySegment<byte>(Encoding.UTF8.GetBytes(message)),
WebSocketMessageType.Text, true, cancellationToken).ConfigureAwait(false);
}
}
}
catch (OperationCanceledException)
{
}
finally
{
_logger.LogDebug("SendLoopAsync END");
}
}
private async Task ReceiveLoopAsync(WebSocket webSocket, CancellationToken cancellationToken)
{
_logger.LogDebug("ReceiveLoopAsync BEGIN");
try
{
while (true)
{
ValueWebSocketReceiveResult receiveResult;
using var buffer = MemoryPool<byte>.Shared.Rent(4096);
await using var ms = new MemoryStream(buffer.Memory.Length);
do
{
// WebSocketException, TaskCanceledException, ObjectDisposedException
receiveResult = await webSocket.ReceiveAsync(buffer.Memory, cancellationToken).ConfigureAwait(false);
if (receiveResult.MessageType == WebSocketMessageType.Close)
{
break;
}
await ms.WriteAsync(buffer.Memory[..receiveResult.Count], cancellationToken).ConfigureAwait(false);
} while (!receiveResult.EndOfMessage);
ms.Seek(0, SeekOrigin.Begin);
if (receiveResult.MessageType == WebSocketMessageType.Text)
{
using var reader = new StreamReader(ms, Encoding.UTF8);
var message = await reader.ReadToEndAsync().ConfigureAwait(false);
_logger.LogInformation("Message received: {Message}", message);
}
else if (receiveResult.MessageType == WebSocketMessageType.Close)
{
break;
}
}
}
catch (WebSocketException ex) when (ex.WebSocketErrorCode == WebSocketError.ConnectionClosedPrematurely)
{
_logger.LogError(ex, "");
throw;
}
finally
{
_logger.LogDebug("ReceiveLoopAsync END");
}
}
}
The Task.WhenAll works differently than Task.WhenAny.
Former throws exception is any of the tasks failed with an exception
Latter does not throw exception even if all of the tasks fail
So either you use call two twice the .GetAwaiter().GetResult() since WhenAny returns a Task<Task>
Task.WhenAny(receiving, sending).ConfigureAwait(false)
.GetAwaiter().GetResult()
.GetAwaiter().GetResult();
Or you can re-throw the exception
var trigger = await Task.WhenAny(receiving, sending).ConfigureAwait(false);
if (trigger.Exception != null)
{
throw trigger.Exception;
}
None of these solutions is perfect, but they will trigger your policy.
UPDATE #1
As Monsieur Merso pointed out you can call twice await
await await Task.WhenAny(receiving, sending).ConfigureAwait(false);
This is much better than the above two approaches.
UPDATE #2
If you want to
trigger the policy if faster task failed
or want to know which one has finished sooner with success
then you can "avoid" the double await
var trigger = await Task.WhenAny(receiving, sending).ConfigureAwait(false);
await trigger; //Throws exception if the faster Task has failed
if (trigger == receiving) //Determines which Task finished sooner
{
}

AsyncRetryPolicy (with wait) not retry the delegate when handling exception thrown from the action (works fine for the AsyncRetry without wait)

I am using Polly 7.1.0 in my Xamarin mobile solution for the REST API call. As it is a mobile app, the key problem for us is to retry a few times when the network connection exceptions (HttpRequestException, WebException, etc...) occurs.
Basically, for most of the service calls, I am applying a policy wrap with Fallback -> Timeout -> RetryAndWait policies. The most inner policy will do the retry with wait when HttpRequestException/WebException etc happens, then Timeout policy for the total time for the retries, and then the outer fallback policy to get a failure return with error message depends on the Exception type.
But when I do the test with the internet disconnected, only a few of the API calls work as expected, most of other will not execute the retry policy (just timeout and then fallback).
If I put try-catch (with throw) statement in the HandleRequestAsync method of the base class, HttpRequestException was thrown, technically the retry policy should handle the exception and execute the action again, but for some reason, it doesn't work as I expected.
If I use retry (instead of retry and wait) for the inner policy, it does retry as expected.
A few major classes:
PolicyHelper (to get the policies)
public static class PolicyHelper
{
public static AsyncFallbackPolicy<TResponse> GetFallbackPolicy<TResponse>(TResponse fallbackValue,
Action<Exception, IDictionary<string,string>> fallbackHandler = null,
bool showAlert = true)
{
return Policy<TResponse>.Handle<WebException>()
.Or<HttpRequestException>()
.Or<TaskCanceledException>()
.Or<TimeoutRejectedException>()
.Or<Exception>(IsAndroidHttpClientException)
.FallbackAsync
(
fallbackAction : (context, ct) =>
{
return Task.FromResult(fallbackValue);
},
onFallbackAsync : async (result, context) =>
{
Exception exception = result?.Exception ?? new APIServiceException("API call failed.");
if (fallbackHandler != null)
{
IDictionary<string, string> properties = null;
if (context != null)
{
properties = new Dictionary<string, string>();
if (context.TryGetValue("CallerMethod", out object callMethod) && callMethod != null)
properties.Add("CallerMethod", callMethod.ToString());
if (context.TryGetValue("EndPoint", out object endpoint) && endpoint != null)
properties.Add("EndPoint", endpoint.ToString());
}
fallbackHandler.Invoke(exception, properties);
}
//Always to dismiss the possible loading
UserDialogs.Instance.HideLoading();
//Manipulate the err message according to the exception type.
if (fallbackValue != null && typeof(UbiApiResonse).IsAssignableFrom(typeof(TResponse)))
{
if (exception is TimeoutRejectedException)
(fallbackValue as UbiApiResonse).ErrMsg = GlobalConfig.TimeoutErrorMessage;
else if (exception is WebException ||
(exception.InnerException != null && exception.InnerException is WebException) ||
IsAndroidHttpClientException(exception))
(fallbackValue as UbiApiResonse).ErrMsg = GlobalConfig.NetworkErrorMessage;
if (showAlert)
UserDialogs.Instance.Alert((fallbackValue as UbiApiResonse).ErrMsg);
}
else
{
if (showAlert)
UserDialogs.Instance.Alert(GlobalConfig.GeneralErrorMessage);
}
await Task.FromResult(true);
}
);
}
public static AsyncTimeoutPolicy<TResponse> GetTimeoutPolicy<TResponse>(int timeoutInMilliSecs)
{
return Policy.TimeoutAsync<TResponse>(TimeSpan.FromMilliseconds(timeoutInMilliSecs));
}
public static AsyncRetryPolicy<TResponse> GetWaitAndRetryPolicy<TResponse>(int maxRetryCount, TimeSpan retryInterval)
{
return Policy<TResponse>.Handle<WebException>()
.Or<HttpRequestException>()
.Or<TaskCanceledException>()
.Or<TimeoutRejectedException>()
.Or<Exception>(IsAndroidHttpClientException)
.WaitAndRetryAsync
(
maxRetryCount,
retryAttempt => retryInterval
);
}
public static AsyncRetryPolicy<TResponse> GetRetryPolicy<TResponse>(int maxRetryCount)
{
return Policy<TResponse>.Handle<WebException>()
.Or<HttpRequestException>()
.Or<TaskCanceledException>()
.Or<TimeoutRejectedException>()
.Or<Exception>(IsAndroidHttpClientException)
.RetryAsync
(
maxRetryCount
);
}
//As Xamarin.Android http request handler only throw java excepotion
//we need catch all Excetpion by check the type name with string comparison
private static bool IsAndroidHttpClientException(Exception exception)
{
string exceptionTypeName = exception.GetType().FullName.ToLower();
return exceptionTypeName.Contains("javax.net.ssl.sslexception") ||
exceptionTypeName.Contains("java.net.socketexception") ||
exceptionTypeName.Contains("java.net.unknownhostexception") ||
exceptionTypeName.Contains("java.io.ioexception") ||
exceptionTypeName.Contains("java.lang.exception");
}
}
APIBaseService (the base class of all the service class. this class has the actual policy executions for the service methods)
public abstract class APIBaseService
{
private static HttpClient _httpClient = new HttpClient();
private object _httpClientLocker = new object();
//private int _tryCounter;
protected APIBaseService()
{
}
protected async Task<TResponse> HandleRequestWithPolicyAndFallbackAsync<TResponse>(string endPoint,
object payload = null,
AsyncPolicy<TResponse> asyncPolicy = null,
bool showAlert = false,
[CallerMemberName] string callerMethod = null) where TResponse : new()
{
var fallbackValue = GetFallbackValue<TResponse>();
var fallbackPolicy = PolicyHelper.GetFallbackPolicy<TResponse>(fallbackValue, HandleExceptionAfterPolicy, showAlert);
//default inner policy
if (asyncPolicy == null)
{
asyncPolicy = Policy.WrapAsync<TResponse>(PolicyHelper.GetTimeoutPolicy<TResponse>(5000),
PolicyHelper.GetWaitAndRetryPolicy<TResponse>(3, TimeSpan.FromSeconds(100)));
}
//Wrap fallback and inner policy
var policyWrap = Policy.WrapAsync<TResponse>(fallbackPolicy, asyncPolicy);
try
{
return await DoRequestWithPolicy(endPoint, payload, policyWrap, callerMethod);
}
//ideally other exception type should be caught by
catch (Exception ex)
{
IDictionary<string, string> properties = GetCallerMetaData(endPoint, callerMethod);
HandleExceptionAfterPolicy(ex, properties);
return GetFallbackValue<TResponse>(ex.Message);
}
}
private async Task<TResponse> DoRequestWithPolicy<TResponse>(string endPoint,
object payload,
AsyncPolicy<TResponse> asyncPolicy,
string callerMethod)
{
//_tryCounter = 0;
Context context = new Context("HandleRequest", new Dictionary<string, object>());
if (!string.IsNullOrEmpty(callerMethod))
context.Add("CallerMethod", callerMethod);
context.Add("EndPoint", endPoint);
using (CancellationTokenSource cts = new CancellationTokenSource())
{
TResponse response = await asyncPolicy.ExecuteAsync(async (ctx, ct) =>
{
return await HandleRequestAsync<TResponse>(endPoint, payload, ct);
}, context, cts.Token);
return response;
}
}
private async Task<TResponse> HandleRequestAsync<TResponse>(string endPoint, object payload, CancellationToken cancellationToken)
{
HttpClientSetup();
using (var httpContent = CreateHttpContent(payload))
{
using (var response = await _httpClient.PostAsync(endPoint, httpContent, cancellationToken))
using (var stream = await response.Content.ReadAsStreamAsync())
{
response.EnsureSuccessStatusCode();
if (response.IsSuccessStatusCode)
return DeserializeJsonFromStream<TResponse>(stream);
//Ideally, here should throw an exception.
//To keep backwards compatibility, we just return a failure response or a value for model
var errorDetails = await StreamToStringAsync(stream);
return GetFallbackValue<TResponse>(errorDetails);
}
}
}
The service classes
The below service method works fine as expected (ie when network is disconnected, it will retry 3 times for the HttpRequestException and then return a fallback value with the proper error message)
public class SearchAPIService : APIBaseService, ISearchAPIService
{
public async Task<SearchResponse> SearchAsync(double latitude, double longitude, DateTime timeIn, DateTime timeOut, BoundsModel bounds, FilterModel filter)
{
var endPoint = AppConstants.BaseURL + GlobalConfig.APIVersion + "/search";
var request = new SearchRequest
{
Location = new LatLngModel(latitude, longitude),
Filter = filter,
Bounds = bounds,
ArrivalTime = timeIn,
DepartureTime = timeOut,
};
var asyncPolicy = Policy.WrapAsync(PolicyHelper.GetTimeoutPolicy<SearchResponse>(10000),
PolicyHelper.GetWaitAndRetryPolicy<SearchResponse>(3, TimeSpan.FromMilliseconds(500)));
return await HandleRequestWithPolicyAndFallbackAsync(endPoint, request, asyncPolicy, true);
}
}
However, another service which has exactly policy applied will never do the retry and JUST get timeout by the outer Timeout policy
If I put try-catch (with throw) statement in the HandleRequestAsync method of the base class, HttpRequestException was thrown, technically the retry policy should handle the exception and execute the action again, but for some reason, it doesn't work as I expected.
If I use retry (instead of retry and wait) for the inner policy, it does retry as expected.
Not sure if I did anything wrong.
public class PaymentAPIService : APIBaseService, IPaymentAPIService
{
public async Task<PayStatusResponse> PayStatusAsync()
{
var endPoint = AppConstants.BaseURL + GlobalConfig.APIVersion + "/pay/status";
var asyncPolicy = Policy.WrapAsync(PolicyHelper.GetTimeoutPolicy<PayStatusResponse>(3500),
PolicyHelper.GetWaitAndRetryPolicy<PayStatusResponse>(3, TimeSpan.FromSeconds(100)));
return await HandleRequestWithPolicyAndFallbackAsync<PayStatusResponse>(endPoint, null, asyncPolicy, true);
}
}

Make Async Rest Request

I am using RestSharp to make some Rest Requests on Windows Phone.
But I am struggling to use "Aync/await" with the functions that I use :
For example with this function :
private void Function()
{
var client = new RestSharp.RestClient("https://exampleapi.com");
client.Authenticator = [....]
var request = new RestSharp.RestRequest("/example.json", Method.GET);
try
{
client.ExecuteAsync(request, reponse =>
{
if (reponse.StatusCode == HttpStatusCode.OK)
{
// Operations with the json...
}
else
{
MessageBox.Show("Error");
}
});
}
catch (Exception error)
{
MessageBox.Show(error.Message);
}
}
I tried to add the word async like this :
private async void restsharptest()
{
var client = new RestSharp.RestClient("https://exampleapi.com");
client.Authenticator = [....]
var request = new RestSharp.RestRequest("/example.json", Method.GET);
try
{
client.ExecuteAsync(request, reponse =>
{
if (reponse.StatusCode == HttpStatusCode.OK)
{
var timeline = JsonConvert.DeserializeObject<List<dynamic>>(reponse.Content);
}
else
{
MessageBox.Show("Error");
}
});
}
catch (Exception error)
{
MessageBox.Show(error.Message);
}
}
Buth then when I try add await :
var timeline = await JsonConvert.DeserializeObject<List<dynamic>>(reponse.Content);
I get the following errors :
Impossible to reach 'System.Collections.Generic.List
and :
Operator 'await' can only be used in a lambda expression async. Mark
this with lambda expression modifier 'async'.
How can I use async/await my "Function1" ?
EDIT :
client.ExecuteAsync(request, response =>
{
if (response.StatusCode == HttpStatusCode.OK)
{
List_ = new List<Myobject>();
List_ = await Task.Factory.StartNew(() => JsonConvert.DeserializeObject<List<Myobject>>(response.Content));
tcs.SetResult(RefreshList_);
}
else
{
MessageBox.Show("Error");
}
});
I have this error again :
Operator 'await' can only be used in a lambda expression async. Mark
this with lambda expression modifier 'async'.
How can I solve this ?
In order to use async you should do the following:
private async void Function()
{
var client = new RestSharp.RestClient("https://exampleapi.com");
client.Authenticator = [....]
var request = new RestSharp.RestRequest("/example.json", Method.GET);
try
{
var response = await client.ExecuteTaskAsync(request);
if (reponse.StatusCode == HttpStatusCode.OK)
{
// Operations with the json...
}
else
{
MessageBox.Show("Error");
}
}
catch (Exception error)
{
MessageBox.Show(error.Message);
}
}
Notice the difference between your ExecuteAsync and the ExecuteTaskAsync with the await before.
You can only await (wait) completion of methods marked as async where return type is either Task or Task<T> (and in special cases void). async keyword allows the use of await keyword inside the method/lambda.
Marking lambda as async
class.Method(() => { /* Do something sync*/ };
class.Method(async () => { /* Do something async */ };
In your case you could do something like
{
...
var client = new RestClient(serviceBaseUrl);
var request = new RestRequest(serviceUrl, Method.GET);
var content = await client.ExecuteTaskAsync(request);
var somethingToReturn = await Task.Factory.StartNew(() => JsonConvert.DeserializeObject<List<MyClass>>(content));
...
}

Abort GetResponseAsync() and Exception in Task

I have written a method which get ETag from eq. XML file on server. Am I correct wrote abort task if timeout (GetResponseAsync() doesn't have CancellationToken) and I have no other idea how to do Exception.
Here's code:
public static async Task<string> GetETagAsync(Uri feedLink)
{
const int millisecondsTimeout = 2500;
WebRequest webRequest = WebRequest.Create(feedLink);
webRequest.Method = "HEAD";
try
{
Task<WebResponse> webResponse = webRequest.GetResponseAsync();
if (await Task.WhenAny(webResponse, Task.Delay(millisecondsTimeout)) == webResponse)
{
using (var result = webResponse.Result)
{
return result.Headers["ETag"];
}
}
else
{
webRequest.Abort();
return null;
}
}
catch (Exception)
{
return null;
}
}
Edit
I have made some changes. Rewrite exceptions and use class from this topic: GetResponseAsync does not accept cancellationToken
Code:
public static async Task<string> GetETagAsync(Uri feedLink)
{
const int millisecondsTimeout = 2500;
var cancellationTokenSource = new CancellationTokenSource();
WebRequest webRequest = WebRequest.Create(feedLink);
webRequest.Method = "HEAD";
try
{
Task<WebResponse> webResponse = WebRequestExtensions.GetResponseAsync(webRequest, cancellationTokenSource.Token);
if (await Task.WhenAny(webResponse, Task.Delay(millisecondsTimeout)) == webResponse)
{
using (var result = webResponse.Result)
{
return result.Headers["ETag"];
}
}
else
{
cancellationTokenSource.Cancel();
return null;
}
}
catch (AggregateException ex)
{
if (ex.InnerException is WebException)
return null;
throw;
}
}
public static class WebRequestExtensions
{
public static async Task<WebResponse> GetResponseAsync(this WebRequest request, CancellationToken cancellationToken)
{
using (cancellationToken.Register(() => request.Abort(), useSynchronizationContext: false))
{
try
{
var response = await request.GetResponseAsync();
cancellationToken.ThrowIfCancellationRequested();
return (WebResponse)response;
}
catch (WebException ex)
{
if (ex.Status == WebExceptionStatus.RequestCanceled)
{
cancellationToken.ThrowIfCancellationRequested();
}
if (cancellationToken.IsCancellationRequested)
{
throw new TaskCanceledException(ex.Message, ex);
}
throw;
}
}
}
}
Is it correct now?
You can use the WebRequestExtensions class I wrote here to resolve the problem:
https://github.com/openstacknetsdk/openstack.net/blob/master/src/corelib/Core/WebRequestExtensions.cs
This class fully supports both a CancellationToken and the WebRequest.Timeout property.
Since the method doesn't natively support any means of stopping the task, it can't be stopped. There is nothing that you can do that will prevent it from continuing to do whatever it plans to do. What you can do is ensure that this async method's task doesn't wait for it to finish. While you can just return a value, as you're doing, this seems to represent an exceptional case; you should probably be throwing an exception to indicate that the operation timed out.

Categories