-
Notifications
You must be signed in to change notification settings - Fork 0
Working with Threads and Async Methods
As indicated in Understanding Retry Program Flow Proteus.Retry
provides no inherent multi-threading or async/await
capability. When you call .Invoke(...)
on your method, the current program flow is blocked until the call to .Invoke(...)
either returns successfully or one of the several exceptions is thrown. Retry
does not attempt to spawn e.g., background threads, use async/await
, or other non-blocking techniques to perform its retry attempts.
This design decision was made so as to ensure that the behavior of invoking your method with Proteus.Retry
was identical to the behavior of invoking your method without Proteus.Retry
-- except of course with the method being retried per the governing RetryPolicy
as needed.
One of the fundamental design goals for Proteus.Retry
was that it be possible to add its functionality to existing code without the target method having to be aware that its being invoked by Proteus.Retry
. If Proteus.Retry
unexpectedly attempted to use async/await
, background threads, etc. internally for its invocation retries, developers would have to design all methods such that they might be invoked by Proteus.Retry
on a different thread.
However, while Proteus.Retry
does not directly make use of multi-threading or async/await
internally, Proteus.Retry
is still entirely compatible with the async/await
pattern and can also be used to invoke your method on other threads as desired. This means that its possible to use Proteus.Retry
in asynchronous and/or multi-threaded scenarios that support the invocation of your method in a manner that won't block the flow of the calling program.
Proteus.Retry
is able to determine when your method is async
(e.g., Task
-returning) and will invoke it asynchronously automatically. This means that its possible to await
an invocation of an async
method with Proteus.Retry
using the following pattern:
//assume a class with a few async methods
public class MyClass
{
public async Task<string> GetSomeValueAsync()
{
return Task.FromResult("hello");
}
public async Task WriteLogAsync(string message)
{
return Task.Run(() => WriteLog(message));
}
public async void FireAndForget()
{
//TODO: do something here
}
public void WriteLog(string message)
{
//TODO: write message to log
}
}
//elsewhere in your code...
var myClass = new MyClass();
var policy = new RetryPolicy();
//TODO: configure the policy as needed
var retry = new Retry(policy);
//await async method that returns Task<T>
var result = await retry.Invoke(() => myClass.GetSomeValueAsync());
//await async method that returns Task
await retry.Invoke(() => myClass.WriteLogAsync("Something Happened"));
//valid syntax, but also pointless since awaiting a void-returning async method has no effect on program flow
await retry.Invoke(() => myClass.FireAndForget());
Note that with async
methods you do not await
your method or declare the lambda itself as async
, but instead you await
the .Invoke(...)
of your async
method. The syntax of the call to .Invoke(...)
is the same for async
and non-async
method invocations; the difference is that you await
the result of the invocation by Proteus.Retry
.
Invoking your method on a specific thread, .NET ThreadPool thread, or otherwise is entirely supported by Proteus.Retry
(assuming of course that your method has been designed to expect to run on other than the main thread of your program -- sadly Proteus.Retry
is unable to magically make your single-threaded code a well-behaved multi-threaded citizen).
The technique for this is very straightforward: simply make the call to .Invoke(...)
on whatever thread you choose. The following code snippet demonstrates an approach that uses the .NET ThreadPool
, but the same technique is generally applicable for other multi-threading scenarios (e.g., Thread.Start(...)
)
//define some callback for the ThreadPool to invoke
public void ThreadPoolCallback(object threadContext)
{
var policy = new RetryPolicy();
//TODO: configure the policy
var retry = new Retry();
var myClass = new MyClass();
retry.Invoke(() => myClass.DoSomeWork());
//TODO: probably signal the main thread that the work is complete :)
}
//elsewhere in your code...
ThreadPool.QueueUserWorkItem(ThreadPoolCallback);
Note in the above sample that there is nothing Proteus.Retry
-specific in the threading parts of the code sample; in the ThreadPoolCallback
method where you would have otherwise called your method without Proteus.Retry
, you instead simply use Proteus.Retry
to invoke your method. Since the ThreadPoolCallback
method executes on a ThreadPool
thread, so too does the invocation of your method by Proteus.Retry
.
This is how Proteus.Retry
adds value in the least-invasive manner possible: you can continue to code using 100% of the same paradigms, patterns, and techniques as before, substituting your direct method calls with invocations of your methods by Proteus.Retry
where you want to make your methods easily retriable.