Retry pattern swallows the exceptions in a Task.Run - c#

I've got two Transient Fault Handling/Retry pattern implementations.
The issue is that theTask.Run swallows the exception and it doesn't rethrow it out of the Task.Run scope.
If I await the Task.Run it would work, but I cannot do that in my real use case.
public static class PollyRetry
{
public static T Do<T>(Func<T> action, TimeSpan retryWait, int retryCount = 0)
{
var policyResult = Policy
.Handle<Exception>()
.WaitAndRetry(retryCount, retryAttempt => retryWait)
.ExecuteAndCapture(action);
if (policyResult.Outcome == OutcomeType.Failure)
{
throw policyResult.FinalException;
}
return policyResult.Result;
}
public static async Task<T> DoAsync<T>(Func<Task<T>> action, TimeSpan retryWait, int retryCount = 0)
{
var policyResult = await Policy
.Handle<Exception>()
.WaitAndRetryAsync(retryCount, retryAttempt => retryWait)
.ExecuteAndCaptureAsync(action);
if (policyResult.Outcome == OutcomeType.Failure)
{
throw policyResult.FinalException;
}
return policyResult.Result;
}
}
public static class Retry
{
public static void Do(Action action, TimeSpan retryInterval, int retryCount = 3)
{
Do<object?>(() =>
{
action();
return null;
}, retryInterval, retryCount);
}
public static async Task DoAsync(Func<Task> action, TimeSpan retryInterval, int retryCount = 3)
{
await DoAsync<object?>(async () =>
{
await action();
return null;
}, retryInterval, retryCount);
}
public static T Do<T>(Func<T> action, TimeSpan retryInterval, int retryCount = 3)
{
var exceptions = new List<Exception>();
for (var count = 1; count <= retryCount; count++)
{
try
{
return action();
}
catch (Exception ex)
{
exceptions.Add(ex);
if (count < retryCount)
{
Thread.Sleep(retryInterval);
}
}
}
throw new AggregateException(exceptions);
}
public static async Task<T> DoAsync<T>(Func<Task<T>> func, TimeSpan retryInterval, int retryCount = 3)
{
var exceptions = new List<Exception>();
for (var count = 1; count <= retryCount; count++)
{
try
{
return await func();
}
catch (Exception ex)
{
exceptions.Add(ex);
if (count < retryCount)
{
await Task.Delay(retryInterval);
}
}
}
throw new AggregateException(exceptions);
}
}
public sealed class WebSocketClient
{
private readonly Channel<string> _receiveChannel;
private readonly Channel<string> _sendChannel;
public WebSocketClient()
{
_receiveChannel = Channel.CreateBounded<string>(new BoundedChannelOptions(10)
{
SingleWriter = true,
SingleReader = false,
FullMode = BoundedChannelFullMode.DropOldest
});
_sendChannel = Channel.CreateBounded<string>(new BoundedChannelOptions(10)
{
SingleReader = true,
SingleWriter = false,
FullMode = BoundedChannelFullMode.Wait
});
}
public async Task StartWithRetry(Uri uri)
{
await Retry.DoAsync(() => Task.FromResult(StartAsync(uri)), TimeSpan.FromSeconds(5), 5);
}
public async Task StartAsync(Uri uri)
{
using var ws = new ClientWebSocket();
await ws.ConnectAsync(uri, default);
if (ws.State == WebSocketState.Open)
{
const string message = "{\"op\": \"subscribe\", \"args\": [\"orderBookL2_25:XBTUSD\"]}";
var buffer = new ArraySegment<byte>(Encoding.UTF8.GetBytes(message));
await ws.SendAsync(buffer, WebSocketMessageType.Text, true, default);
}
_ = Task.Run(async () =>
{
while (await _receiveChannel.Reader.WaitToReadAsync())
{
while (_receiveChannel.Reader.TryRead(out var message))
{
Console.WriteLine($"Message: {message}");
}
}
});
_ = Task.Run(async () =>
{
// This throws WebSocketException with ex.WebSocketErrorCode == WebSocketError.ConnectionClosedPrematurely
while (true)
{
ValueWebSocketReceiveResult receiveResult;
using var buffer = MemoryPool<byte>.Shared.Rent(4096);
await using var ms = new MemoryStream(buffer.Memory.Length);
do
{
receiveResult = await ws.ReceiveAsync(buffer.Memory, default);
if (receiveResult.MessageType == WebSocketMessageType.Close)
{
break;
}
await ms.WriteAsync(buffer.Memory[..receiveResult.Count]);
} 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();
await _receiveChannel.Writer.WriteAsync(message);
}
else if (receiveResult.MessageType == WebSocketMessageType.Close)
{
break;
}
}
});
}
}
Minimal Reproducible Example
var code = new MinimalReproducibleCode();
await code.StartWithRetry();
public sealed class MinimalReproducibleCode
{
public async Task StartWithRetry()
{
await Retry.DoAsync(() => Task.FromResult(StartAsync()), TimeSpan.FromSeconds(5), 5);
}
public Task StartAsync()
{
Console.WriteLine("This has just started");
_ = Task.Run(() =>
{
while (true)
{
Console.WriteLine("Code is working");
throw new DivideByZeroException();
}
});
return Task.CompletedTask;
}
}
public static class Retry
{
public static void Do(Action action, TimeSpan retryInterval, int retryCount = 3)
{
_ = Do<object?>(() =>
{
action();
return null;
}, retryInterval, retryCount);
}
public static async Task DoAsync(Func<Task> action, TimeSpan retryInterval, int retryCount = 3)
{
_ = await DoAsync<object?>(async () =>
{
await action();
return null;
}, retryInterval, retryCount);
}
public static async Task DoAsync<TException>(
Func<Task> action,
Func<TException, bool> exceptionFilter,
TimeSpan retryInterval,
int retryCount = 3) where TException : Exception
{
_ = await DoAsync<object?>(async () =>
{
await action();
return null;
}, retryInterval, retryCount);
}
public static T Do<T>(Func<T> action, TimeSpan retryWait, int retryCount = 3)
{
var policyResult = Policy
.Handle<Exception>()
.WaitAndRetry(retryCount, retryAttempt => retryWait)
.ExecuteAndCapture(action);
if (policyResult.Outcome == OutcomeType.Failure)
{
throw policyResult.FinalException;
}
return policyResult.Result;
}
public static async Task<T> DoAsync<T>(Func<Task<T>> action, TimeSpan retryWait, int retryCount = 3)
{
var policyResult = await Policy
.Handle<Exception>()
.WaitAndRetryAsync(retryCount, retryAttempt => retryWait)
.ExecuteAndCaptureAsync(action);
if (policyResult.Outcome == OutcomeType.Failure)
{
throw policyResult.FinalException;
}
return policyResult.Result;
}
public static async Task<T> DoAsync<T, TException>(
Func<Task<T>> action,
Func<TException, bool> exceptionFilter,
TimeSpan retryWait,
int retryCount = 0) where TException : Exception
{
var policyResult = await Policy
.Handle(exceptionFilter)
.WaitAndRetryAsync(retryCount, retryAttempt => retryWait)
.ExecuteAndCaptureAsync(action);
if (policyResult.Outcome == OutcomeType.Failure)
{
throw policyResult.FinalException;
}
return policyResult.Result;
}
}

OK, based on your code, here's how to make it work:
public async Task StartWithRetry()
{
await Retry.DoAsync(() => StartAsync(), TimeSpan.FromSeconds(5), 5);
}
public async Task StartAsync()
{
Console.WriteLine("This has just started");
await Task.Run(() =>
{
while (true)
{
Console.WriteLine("Code is working");
throw new DivideByZeroException();
}
});
}
You need to await the Task.Run and not fire-and-forget it.
When I run the above code I get:
This has just started
Code is working
This has just started
Code is working
This has just started
Code is working
This has just started
Code is working
This has just started
Code is working
This has just started
Code is working
DivideByZeroException
Attempted to divide by zero.

Related

this.Clients are disposed SignalR

At the time of the user's connection, everything is going well. When I have a product update (it becomes available for sale), the OnProductStartSales event is called.
But there is one problem, the message does not come to the client because the list of clients in the hub is disposed.
Here is the code of my hub.
public class SalesHub : Hub
{
private readonly ProductDatabaseListener _listener;
public SalesHub(ProductDatabaseListener listener)
{
_listener = listener ?? throw new ArgumentNullException(nameof(listener));
_listener.OnProductStartSales += (s, p) => ProductStartSales(p);
_listener.OnProductDataChanged += (s, p) => ProductDataChanged(p);
}
public async Task ListenProduct(string productId)
{
await this.Groups.AddToGroupAsync(Context.ConnectionId, productId);
}
private async Task ProductStartSales(Product product)
{
await this.Clients.Group(product.Id).SendAsync("StartSales", product.Id);
// await this.Clients.All.SendAsync("StartSales", product.Id);
}
private async Task ProductDataChanged(Product product)
{
await this.Clients.Group(product.Id).SendAsync("DataChanged", product);
}
}
Here is the code of listener.
public class ProductDatabaseListener
{
private readonly IRepository<Product> _repository;
private readonly object _locker = new object();
public ProductDatabaseListener(IServiceProvider serviceProvider)
{
using (var scope = serviceProvider.CreateScope())
{
_repository = scope.ServiceProvider.GetRequiredService<IRepository<Product>>() ?? throw new ArgumentNullException(nameof(_repository));
}
}
public event EventHandler<Product> OnProductStartSales;
public event EventHandler<Product> OnProductDataChanged;
// Need better performance...
public async Task ListenMongo()
{
while (true)
{
var products = await _repository.GetRange(0, int.MaxValue);
var date = DateTime.Now;
List<Task> tasks = new List<Task>();
foreach (var product in products)
{
if (product.IsSalesStart)
{
continue;
}
if (product.StartOfSales <= date)
{
product.IsSalesStart = true;
OnProductStartSales?.Invoke(this, product);
tasks.Add(_repository.Update(product));
}
}
Task.WaitAll(tasks.ToArray());
await Task.Delay(1000);
}
}
}
Here is the client code
"use strict";
var connection = new signalR.HubConnectionBuilder().withUrl("/salesHub").build();
connection.on("ReceiveMessage", function (id) {
var li = document.createElement("li")
document.getElementById("fromHub").appendChild(li)
li.textContent = id;
});
connection.on("startSales", function (id) {
var productId = document.getElementById("objectId").getAttribute("value");
if (productId == id) {
var button = document.getElementById("buy")
button.hidden = false
}
});
connection.logging = true;
connection.start().then(function () {
var productId = document.getElementById("objectId").getAttribute("value");
connection.invoke("ListenProduct", productId).catch(function (err) {
return console.error(err.toString());
});
event.preventDefault();
}).catch(function (err) {
return console.error(err.toString());
});

JSON-RPC - Handling exceptions with TaskCompletionSource (SetException)

There is a JSON-RPC API, which I'm currently implementing. It can be tested here.
The problem is that if an incorrect DTO model is passed to SendAsync<TResponse>, JsonSerializer.Deserialize is going to throw a JsonException, which is not handled by my code. I know I've got to use SetException in some way, but I don't know how to do it, so here is the question. The exception message should be printed in the console as well.
public sealed class Client : IDisposable
{
...
private readonly ConcurrentDictionary<long, IResponseHandler> _handlers = new();
...
public Task StartAsync(CancellationToken cancellationToken)
{
_ = Task.Run(async () =>
{
await foreach (var message in _client.Start(cancellationToken))
{
using var response = JsonDocument.Parse(message);
try
{
var requestId = response.RootElement.GetProperty("id").GetInt32();
// TODO: Handle JsonException errors via `SetException`?
// TODO: Show error when incorrect input parameters are filled
if (_handlers.ContainsKey(requestId))
{
_handlers[requestId].SetResult(message);
_handlers.TryRemove(requestId, out _);
}
}
catch (KeyNotFoundException)
{
// My point is that a message should be processed only if it doesn't include `id`,
// because that means that the message is an actual web socket subscription.
_messageReceivedSubject.OnNext(message);
}
}
}, cancellationToken);
...
return Task.CompletedTask;
}
public Task<TResponse> SendAsync<TResponse>(string method, object #params)
{
var request = new JsonRpcRequest<object>
{
JsonRpc = "2.0",
Id = NextId(),
Method = method,
Params = #params
};
//var tcs = new TaskCompletionSource<TResponse>();
//_requestManager.Add(request.Id, request, tcs);
var handler = new ResponseHandlerBase<TResponse>();
_handlers[request.Id] = handler;
var message = JsonSerializer.Serialize(request);
_ = _client.SendAsync(message);
return handler.Task;
//return tcs.Task;
}
public async Task<AuthResponse?> AuthenticateAsync(string clientId, string clientSecret)
{
var #params = new Dictionary<string, string>
{
{"grant_type", "client_credentials"},
{"client_id", clientId},
{"client_secret", clientSecret}
};
var response = await SendAsync<SocketResponse<AuthResponse>>("public/auth", #params).ConfigureAwait(false);
return response.Result;
}
...
private interface IResponseHandler
{
void SetResult(string payload);
}
private class ResponseHandlerBase<TRes> : IResponseHandler
{
private readonly TaskCompletionSource<TRes> _tcs = new();
public Task<TRes> Task => _tcs.Task;
public void SetResult(string payload)
{
var result = JsonSerializer.Deserialize(payload, typeof(TRes));
_tcs.SetResult((TRes) result);
}
}
}
Coincidentally, I did something very similar while live-coding a TCP/IP chat application last week.
Since in this case you already have an IAsyncEnumerable<string> - and since you can get messages other than responses - I recommend also exposing that IAsyncEnumerable<string>:
public sealed class Client : IDisposable
{
public async IAsyncEnumerable<string> Start(CancellationToken cancellationToken)
{
await foreach (var message in _client.Start(cancellationToken))
{
// TODO: parse and handle responses for our requests
yield return message;
}
}
}
You can change this to be Rx-based if you want (_messageReceivedSubject.OnNext), but I figure if you already have IAsyncEnumerable<T>, then you may as well keep the same abstraction.
Then, you can parse and detect responses, passing along all other messages:
public sealed class Client : IDisposable
{
public async IAsyncEnumerable<string> Start(CancellationToken cancellationToken)
{
await foreach (var message in _client.Start(cancellationToken))
{
var (requestId, response) = TryParseResponse(message);
if (requestId != null)
{
// TODO: handle
}
else
{
yield return message;
}
}
(long? RequestId, JsonDocument? Response) TryParseResponse(string message)
{
try
{
var document = JsonDocument.Parse(message);
var requestId = response.RootElement.GetProperty("id").GetInt32();
return (document, requestId);
}
catch
{
return (null, null);
}
}
}
}
Then, you can define your collection of outstanding requests and handle messages that are for those requests:
public sealed class Client : IDisposable
{
private readonly ConcurrentDictionary<int, TaskCompletionSource<JsonDocument>> _requests = new();
public async IAsyncEnumerable<string> Start(CancellationToken cancellationToken)
{
await foreach (var message in _client.Start(cancellationToken))
{
var (requestId, response) = TryParseResponse(message);
if (requestId != null && _requests.TryRemove(requestId.Value, out var tcs))
{
tcs.TrySetResult(response);
}
else
{
yield return message;
}
}
(long? RequestId, JsonDocument? Response) TryParseResponse(string message)
{
try
{
var document = JsonDocument.Parse(message);
var requestId = response.RootElement.GetProperty("id").GetInt32();
return (document, requestId);
}
catch
{
return (null, null);
}
}
}
}
Note the usage of ConcurrentDictionary.TryRemove, which is safer than accessing the value and then removing it.
Now you can write your general SendAsync. As I note in my video, I prefer to split up the code that runs synchronously in SendAsync and the code that awaits the response:
public sealed class Client : IDisposable
{
...
public Task<TResponse> SendAsync<TResponse>(string method, object #params)
{
var request = new JsonRpcRequest<object>
{
JsonRpc = "2.0",
Id = NextId(),
Method = method,
Params = #params,
};
var tcs = new TaskCompletionSource<JsonDocument>(TaskCreationOptions.RunContinuationsAsynchronously);
_requests.TryAdd(request.Id, tcs);
return SendRequestAndWaitForResponseAsync();
async Task<TResponse> SendRequestAndWaitForResponseAsync()
{
var message = JsonSerializer.Serialize(request);
await _client.SendAsync(message);
var response = await tcs.Task;
return JsonSerializer.Deserialize(response, typeof(TResponse));
}
}
}
I've removed the "handler" concept completely, since it was just providing the type for JsonSerializer.Deserialize. Also, by using a local async method, I can use the async state machine to propagate exceptions naturally.
Then, your higher-level methods can be built on this:
public sealed class Client : IDisposable
{
...
public async Task<AuthResponse?> AuthenticateAsync(string clientId, string clientSecret)
{
var #params = new Dictionary<string, string>
{
{"grant_type", "client_credentials"},
{"client_id", clientId},
{"client_secret", clientSecret}
};
var response = await SendAsync<SocketResponse<AuthResponse>>("public/auth", #params);
return response.Result;
}
}
So the final code ends up being:
public sealed class Client : IDisposable
{
private readonly ConcurrentDictionary<int, TaskCompletionSource<JsonDocument>> _requests = new();
public async IAsyncEnumerable<string> Start(CancellationToken cancellationToken)
{
await foreach (var message in _client.Start(cancellationToken))
{
var (requestId, response) = TryParseResponse(message);
if (requestId != null && _requests.TryRemove(requestId.Value, out var tcs))
{
tcs.TrySetResult(response);
}
else
{
yield return message;
}
}
(long? RequestId, JsonDocument? Response) TryParseResponse(string message)
{
try
{
var document = JsonDocument.Parse(message);
var requestId = response.RootElement.GetProperty("id").GetInt32();
return (document, requestId);
}
catch
{
return (null, null);
}
}
}
public Task<TResponse> SendAsync<TResponse>(string method, object #params)
{
var request = new JsonRpcRequest<object>
{
JsonRpc = "2.0",
Id = NextId(),
Method = method,
Params = #params,
};
var tcs = new TaskCompletionSource<JsonDocument>(TaskCreationOptions.RunContinuationsAsynchronously);
_requests.TryAdd(request.Id, tcs);
return SendRequestAndWaitForResponseAsync();
async Task<TResponse> SendRequestAndWaitForResponseAsync()
{
var message = JsonSerializer.Serialize(request);
await _client.SendAsync(message);
var response = await tcs.Task;
return JsonSerializer.Deserialize(response, typeof(TResponse));
}
}
public async Task<AuthResponse?> AuthenticateAsync(string clientId, string clientSecret)
{
var #params = new Dictionary<string, string>
{
{"grant_type", "client_credentials"},
{"client_id", clientId},
{"client_secret", clientSecret}
};
var response = await SendAsync<SocketResponse<AuthResponse>>("public/auth", #params);
return response.Result;
}
}
You may also want to check out David Fowler's Project Bedrock, which may simplify this code quite a bit.

Requests are timing-out when light payload is applied to .Net Core Web Api

I have an API which basically receives a request and pushes it to an SQS queue, nothing complicated.
[HttpPost]
public ActionResult Post([FromBody]object message, [FromHeader] string source)
{
if (message== null)
return new UnsupportedMediaTypeResult();
if (PublishMessageToSQS(JsonConvert.SerializeObject(message),source))
return StatusCode(201);
return StatusCode(500);
}
private bool PublishMessage(string message, string source)
{
try
{
RetryWhenException.Do(
() =>
{
SendMessageRequest request = new SendMessageRequest()
{
MessageBody = message,
MessageAttributes = new Dictionary<string, MessageAttributeValue>(),
QueueUrl = "my queue",
};
if (!string.IsNullOrEmpty(source))
request.MessageAttributes.Add("source", new MessageAttributeValue()
{
StringValue = source,
DataType = "String"
});
var result = sqsClient.SendMessageAsync(request).Result;
}, 3, 1000);
return true;
}
catch (Exception e)
{
//log
throw;
}
}
This API is containerized and deployed to AWS ECS on low resources machine (0.25 VCpu, 512 MB RAM).
When applying a light load on the API (10 requests per second), requests start to timeout after a while.
I stopped receiving timeouts when applying one of:
1- Use more resources (2 VCPU, 4GB RAM)
2- make my action async.
[HttpPost]
public async Task<ActionResult> Post([FromBody]object message, [FromHeader] string source)
{
if (message== null)
return new UnsupportedMediaTypeResult();
if (await PublishMessageToSQS(JsonConvert.SerializeObject(message), source))
return StatusCode(201);
return StatusCode(500);
}
private async Task<bool> PublishMessage(string message, string source)
{
try
{
await RetryWhenException.Do(
async () =>
{
SendMessageRequest request = new SendMessageRequest()
{
MessageBody = message,
MessageAttributes = new Dictionary<string, MessageAttributeValue>(),
QueueUrl = "my queue",
};
if (!string.IsNullOrEmpty(source))
request.MessageAttributes.Add("source", new MessageAttributeValue()
{
StringValue = source,
DataType = "String"
});
var result = await sqsClient.SendMessageAsync(request);
}, 3, 1000);
return true;
}
catch (Exception e)
{
//log
throw;
}
}
RetryWhenException code:
public static class RetryWhenException
{
public static void Do(Action action, int maxAttemptCount = 3, int retryInterval = 1000)
{
var exceptions = new List<Exception>();
for (var attempted = 0; attempted < maxAttemptCount; attempted++)
{
try
{
if (attempted > 0)
{
Thread.Sleep(retryInterval);
}
action();
return;
}
catch (Exception ex)
{
exceptions.Add(ex);
}
}
throw new AggregateException(exceptions);
}
}
I know that Async frees threads, but pushing to SQS is not that costy and the number of requests is not that high.
I really don't understand why I was getting timeouts when applying such low payload, and why async did the trick, any explanation?

ContinueConversationAsync() requires users to chat first after publishing bot

After publishing the bot, the user needs to chat with the bot again first in order send a proactive message. I followed this sample but instead of storing the conversation reference in the variable, I stored it in cosmosDB. However I still can't send proactive message if the user has not chatted with the bot after publishing. Is there a way to send proactive message even when the user has not chatted with the bot after publishing?
DialogBot
private async void AddConversationReference(ITurnContext turnContext)
{
var userstate = await _userProfileAccessor.GetAsync(turnContext, () => new BasicUserState());
userstate.SavedConversationReference = turnContext.Activity.GetConversationReference();
_conversationReferences.AddOrUpdate(userstate.SavedConversationReference.User.Id, userstate.SavedConversationReference, (key, newValue) => userstate.SavedConversationReference);
}
protected override Task OnConversationUpdateActivityAsync(ITurnContext<IConversationUpdateActivity> turnContext, CancellationToken cancellationToken)
{
AddConversationReference(turnContext);
return base.OnConversationUpdateActivityAsync(turnContext, cancellationToken);
}
protected override async Task OnMessageActivityAsync(ITurnContext<IMessageActivity> turnContext, CancellationToken cancellationToken)
{
Logger.LogInformation("Running dialog with Message Activity.");
AddConversationReference(turnContext);
await Dialog.RunAsync(turnContext, ConversationState.CreateProperty<DialogState>("DialogState"), cancellationToken);
}
api/notify
namespace SabikoBotV2.Controllers
{
[Route("api/notify")]
public class NotifyController : ControllerBase
{
private readonly IBotFrameworkHttpAdapter _adapter;
private readonly string _appId;
private readonly ConcurrentDictionary<string, ConversationReference> _conversationReferences;
private readonly IStatePropertyAccessor<BasicUserState> _userProfileAccessor;
public NotifyController(UserState userState, IBotFrameworkHttpAdapter adapter, ICredentialProvider credentials, ConcurrentDictionary<string, ConversationReference> conversationReferences)
{
_userProfileAccessor = userState.CreateProperty<BasicUserState>("UserProfile");
_adapter = adapter;
_conversationReferences = conversationReferences;
_appId = ((SimpleCredentialProvider)credentials).AppId;
if (string.IsNullOrEmpty(_appId))
{
_appId = Guid.NewGuid().ToString(); //if no AppId, use a random Guid
}
}
public async Task<IActionResult> Get()
{
try
{
foreach (var conversationReference in _conversationReferences.Values)
{
await ((BotAdapter)_adapter).ContinueConversationAsync(_appId, conversationReference, BotCallback, default(CancellationToken));
}
return new ContentResult()
{
Content = "<html><body><h1>Proactive messages have been sent.</h1></body></html>",
ContentType = "text/html",
StatusCode = (int)HttpStatusCode.OK,
};
}
catch (Exception ex)
{
return BadRequest(ex.Message);
}
}
private async Task BotCallback(ITurnContext turnContext, CancellationToken cancellationToken)
{
var userstate = await _userProfileAccessor.GetAsync(turnContext, () => new BasicUserState(), cancellationToken);
if (userstate.SavedConversationReference.ServiceUrl != null && userstate.SavedConversationReference.ServiceUrl != string.Empty)
{
MicrosoftAppCredentials.TrustServiceUrl(userstate.SavedConversationReference.ServiceUrl);
}
else if (turnContext.Activity.ServiceUrl != null && turnContext.Activity.ServiceUrl != string.Empty)
{
MicrosoftAppCredentials.TrustServiceUrl(turnContext.Activity.ServiceUrl);
}
else
{
MicrosoftAppCredentials.TrustServiceUrl("https://facebook.botframework.com/");
}
if(userstate.Reminders != null)
{
foreach (var reminder in userstate.Reminders)
{
if (reminder.DateAndTime.TrimMilliseconds() == DateTimeNowInGmt().TrimMilliseconds())
{
var timeProperty = new TimexProperty(reminder.DateAndTimeTimex);
var naturalDate = timeProperty.ToNaturalLanguage(DateTimeNowInGmt().TrimMilliseconds());
await turnContext.SendActivityAsync($"It's {DateTimeNowInGmt().ToLongDateString()}. \n\nReminding you to {reminder.Subject}.");
}
}
}
}
public static DateTime DateTimeNowInGmt()
{
TimeZoneInfo phTimeZone = TimeZoneInfo.FindSystemTimeZoneById("Taipei Standard Time");
var t = DateTime.Now;
DateTime phTime = TimeZoneInfo.ConvertTime(t, phTimeZone);
return phTime;
}
}
}

AsyncInterceptor with Polly (AOP retry style)

For some part of my system I need to add retry logic for reading from the database. I have a number of repositories with async and sync read methods that I can't change. I found a simple solution - interception of all read methods with AsyncInterceptor and add retry read policy with Polly when database exception caught. Polly retries reading with some intervals.
Interceptor code:
public class RetriableReadAsyncInterceptor : IAsyncInterceptor
{
public void InterceptSynchronous(IInvocation invocation)
{
invocation.ReturnValue = InternalInterceptSync(invocation);
}
public void InterceptAsynchronous(IInvocation invocation)
{
throw new NotImplementedException();
}
public void InterceptAsynchronous<TResult>(IInvocation invocation)
{
invocation.ReturnValue = InternalInterceptAsync<TResult>(invocation);
}
private IEnumerable<TimeSpan> RetryIntervals =>
new[]
{
TimeSpan.FromSeconds(1),
TimeSpan.FromSeconds(5),
TimeSpan.FromSeconds(10),
TimeSpan.FromSeconds(15)
};
private object InternalInterceptSync(IInvocation invocation)
{
return Policy
.Handle<DatabaseException>()
.WaitAndRetry(RetryIntervals, (exception, timeSpan) =>
{
Console.WriteLine($"Exception {timeSpan}");
})
.Execute(() =>
{
invocation.Proceed();
return invocation.ReturnValue;
});
}
private async Task<TResult> InternalInterceptAsync<TResult>(IInvocation invocation)
{
return await Policy
.Handle<DatabaseException>()
.WaitAndRetryAsync(RetryIntervals, (exception, timeSpan) =>
{
Console.WriteLine($"Exception {timeSpan}");
})
.ExecuteAsync(async () =>
{
invocation.Proceed();
var task = (Task<TResult>)invocation.ReturnValue;
return await task;
});
}
}
Repository code:
public class Repository : IRepository
{
private int _exceptionsCoutner;
public Entity GetById(int id)
{
if (_exceptionsCoutner <= 2)
{
_exceptionsCoutner++;
throw new DatabaseException();
}
//read from db
return new Entity {Id = id};
}
public async Task<Entity> GetByIdAsync(int id)
{
if (_exceptionsCoutner <= 2)
{
_exceptionsCoutner++;
throw new DatabaseException();
}
//read from db
return await Task.FromResult(new Entity { Id = id });
}
}
Sync version of GetById works as expected (retries with intervals):
Exception 00:00:01
Exception 00:00:05
Exception 00:00:10
Async version of GetById retries but not waits for time interval elapsed:
Exception 00:00:01
Exception 00:00:01
Exception 00:00:01
I can't understand where is the problem. If you have any thoughts - please share.
Full example can be found here.
It was a kind of 'chicken and egg' problem which can be solved now with newer version of Castle.Core (I tried version 4.4.0) leveraging the invocation.CaptureProceedInfo method:
private Task<TResult> InternalInterceptAsync<TResult>(IInvocation invocation)
{
var capture = invocation.CaptureProceedInfo();
return Policy
.Handle<DatabaseException>()
.WaitAndRetryAsync(RetryIntervals, (exception, timeSpan) =>
{
Console.WriteLine($"Exception {timeSpan}");
})
.ExecuteAsync(async () =>
{
capture.Invoke();
var task = (Task<TResult>)invocation.ReturnValue;
return await task;
});
}
OK, here is my naive implementation of retry:
public class Retry
{
public static async Task<TResult> DoAsync<TResult, TException>(
Func<Task<TResult>> action,
TimeSpan retryInterval,
int maxAttemptCount = 3)
where TException : Exception
{
TException exception = null;
var startDateTime = DateTime.UtcNow;
for (var attempted = 0; attempted < maxAttemptCount; attempted++)
{
try
{
return await action().ConfigureAwait(false);
}
catch (TException ex)
{
exception = ex;
Console.WriteLine($"Exception {DateTime.UtcNow - startDateTime}");
await Task.Delay(retryInterval); //doesnt work
//Thread.Sleep(retryInterval); works!
}
}
throw exception;
}
}
And interceptor:
private async Task<TResult> InternalInterceptAsync<TResult>(IInvocation invocation)
{
return await Retry.DoAsync<TResult, DatabaseException>(async () =>
{
invocation.Proceed();
var task = (Task<TResult>) invocation.ReturnValue;
return await task.ConfigureAwait(false);
},
TimeSpan.FromSeconds(3),
4);
}
Implementation with blocking Tread.Sleep works well, but with Task.Delay dont.

Categories

Resources