I wrote an async method with retry logic. It works just fine, however recently I wanted to add a timeout for each try in case the operation takes too long.
public static async Task<Result> PerformAsync(Func<Task> Delegate,
Func<Exception, bool> FailureCallback = null, int Timeout = 30000,
int Delay = 1000, int Threshold = 10)
{
if (Delegate == null)
{
throw new ArgumentNullException(nameof(Delegate));
}
if (Threshold < 1)
{
throw new ArgumentOutOfRangeException(nameof(Threshold));
}
CancellationTokenSource Source = new CancellationTokenSource();
CancellationToken Token = Source.Token;
bool IsSuccess = false;
for (int Attempt = 0; Attempt <= Threshold && !Source.IsCancellationRequested;
Attempt++)
{
try
{
await Delegate();
Source.Cancel();
IsSuccess = true;
break;
}
catch (Exception E)
{
Exceptions.Add(E);
if (FailureCallback != null)
{
bool IsCanceled =
Application.Current.Dispatcher.Invoke(new Func<bool>(() =>
{
return !FailureCallback(E);
}));
if (IsCanceled)
{
Source.Cancel();
IsSuccess = false;
break;
}
}
}
await Task.Delay(Delay);
}
return new Result(IsSuccess, new AggregateException(Exceptions));
}
I've been trying various solutions all over the web, but for whatever reason I've never managed to set timeout for each try individually.
I tried to do this using Task.WhenAny() with Task.Delay(Timeout), but when I launch my program, FailureCallback is called only once and if another try fails, FailureCallback is not called.
Ok, lets start. First of all, the intended usage of a CancellationToken isn't to cancel locally a loop, that's a waste, a CancellationToken reserves some resources and in your case you can simply usea boolean.
bool IsSuccess = false;
bool IsCancelled = false;
for (int Attempt = 0; Attempt <= Threshold; Attempt++)
{
try
{
await Delegate();
IsSuccess = true;
//You are breaking the for loop, no need to test the boolean
//in the for conditions
break;
}
catch (Exception E)
{
Exceptions.Add(E);
if (FailureCallback != null)
{
IsCancelled = Application.Current.Dispatcher.Invoke(new Func<bool>(() =>
{
return !FailureCallback(E);
}));
//You are breaking the for loop, no need to test the boolean
//in the for conditions
if(IsCancelled)
break;
}
}
await Task.Delay(Delay);
}
//Here you have "IsSuccess" and "IsCancelled" to know what happened in the loop
//If IsCancelled is true the operation was cancelled, if IsSuccess is true
//the operation was success, if both are false the attempt surpased threshold.
Second, you must update your delegate to be cancellable, that's the real intended usage of CancellationToken, make your delegate to expect a CancellationToken and use it properly inside the function.
public static async Task<Result> PerformAsync(Func<CancellationToken, Task> Delegate, //..
//This is an example of the Delegate function
public Task MyDelegateImplemented(CancellationToken Token)
{
//If you have a loop check if it's cancelled in each iteration
while(true)
{
//Throw a TaskCanceledException if the cancellation has been requested
Token.ThrowIfCancellationRequested();
//Now you must propagate the token to any async function
//that accepts it
//Let's suppose you are downloading a web page
HttpClient client;
//...
await client.SendAsync(message, Token)
}
}
Finally, now that your task is cancellable you can implement the timeout like this:
//This is the "try" in your loop
try
{
var tokenSource = new CancellationTokenSource();
var call = Delegate(tokenSource.Token);
var delay = Task.Delay(timeout, tokenSource.Token);
var finishedTask = await Task.WaitAny(new Task[]{ call, delay });
//Here call has finished or delay has finished, one will
//still be running so you need to cancel it
tokenSource.Cancel();
tokenSource.Dispose();
//WaitAny will return the task index that has finished
//so if it's 0 is the call to your function, else it's the timeout
if(finishedTask == 0)
{
IsSuccess = true;
break;
}
else
{
//Task has timed out, handle the retry as you need.
}
}
Related
Good day.
I'm having a problem exiting a task with the cancellation token.
My program freezes when I get to the token2.ThrowIfCancellationRequested();.
Following it with the breakpoints is shows that the token2 is cancelled, but the program doesn't revert back to the previous sub routine where I try and catch
try
{
Task.Run(() => SendData_DoWork(_tokenSource3));
}
catch (OperationCanceledException ex)
{
SetText("Communivation error with device");
SetText("");
}
finally
{
token.Dispose();
}
}//comms routine
//send Meter Address to communicate to meter
private void SendData_DoWork(CancellationTokenSource token)
{
var token2 = token.Token;
var _tokenSource4 = new CancellationTokenSource();
try
{
timer.Interval = 10000;
timer.Start();
timer.Elapsed += OnTimerElapsed;
NetworkStream stream = client.GetStream();
SerialConverter serialConverter = new SerialConverter();
Thread.Sleep(1000);
string newtext = null;
newtext = $"/?{address}!\r\n";
SetText("TX: " + newtext);
byte[] newData = stringSend(newtext);
stream.Write(newData, 0, newData.Length);
Thread.Sleep(50);
byte[] message = new byte[23];
int byteRead;
while (true)
{
byteRead = 0;
try
{
byteRead = stream.Read(message, 0, 23);
if (message[0] == (char)0x15)
{
token.Cancel();
}
}
catch
{
token.Cancel();
}
if ((byteRead == 0))
{
token.Cancel();
}
timer.Stop();
timer.Dispose();
ASCIIEncoding encoder = new ASCIIEncoding();
string newresponse = encoder.GetString(serialConverter.convertFromSerial(message));
SetText("RX: " + newresponse);
if (newresponse[0].ToString() == SOH)
{
token.Cancel();
}
if (newresponse != null)
{
/* NEXT SUB ROUTINE*/
}
else { break; }
}//while looop
}//try
catch (Exception ex)
{
token.Cancel();
}
if (token2.IsCancellationRequested)
{
timer.Stop();
timer.Dispose();
token2.ThrowIfCancellationRequested();
}
}//sendData subroutine
You are launching a Task, and ignoring the result; the only time Task.Run would throw is if the task-method is invalid, or enqueuing the operation itself failed. If you want to know how SendData_DoWork ended, you'll need to actually check the result of the task, by capturing the result of Task.Run and awaiting it (preferably asynchronously, although if we're talking async, SendData_DoWork should probably also be async and return a Task).
Your catch/finally will probably be exited long before SendData_DoWork even starts - again: Task.Run just takes the time required to validate and enqueue the operation; not wait for it to happen.
I think you have missunderstood how cancellation tokens are supposed to work. Your work method should take a CancellationToken, not a CancellationTokenSource. And it should call ThrowIfCancellationRequested inside the loop, not after. I would suspect that you would get some issues with multiple cancel calls to the same cancellation token.
Typically you would use a pattern something like like this:
public void MyCancelButtonHandler(...) => cts.Cancel();
public async void MyButtonHandler(...){
try{
cts = new CancellationTokenSource(); // update shared field
await Task.Run(() => MyBackgroundWork(cts.Token));
}
catch(OperationCancelledException){} // Ignore
catch(Exception){} // handle other exceptions
}
private void MyBackgroundWork(CancellationToken cancel){
while(...){
cancel.ThrowIfCancellationRequested();
// Do actual work
}
}
So in my particular case it seems like changing the sub-routines from private async void ... to private async Task fixes the particular issue that I'm having.
I have Web Api which gets CancellationToken from users, the method inside it (DoWork) also get CancellationToken:
[HttpPost]
public async Task<long> GetInfo(CancellationToken cancellationToken)
{
long result = 0;
bool notDone = true;
Task<long> t = Task.Run(async () =>
{
if (cancellationToken.IsCancellationRequested)
cancellationToken.ThrowIfCancellationRequested();
while (notDone && !cancellationToken.IsCancellationRequested)
{
result = await DoWork(cancellationToken);
notDone = false;
}
return result;
}, cancellationToken);
try
{
return await t;
}
catch (AggregateException e)
{
Debug.WriteLine("Exception messages:");
foreach (var ie in e.InnerExceptions)
Debug.WriteLine(" {0}: {1}", ie.GetType().Name, ie.Message);
Debug.WriteLine("\nTask status: {0}", t.Status);
throw;
}
catch (Exception ex)
{
throw;
}
}
private Task<long> DoWork(CancellationToken token)
{
long result = 0;
bool notDone = true;
Task<long> task = Task.Run(() =>
{
if (token.IsCancellationRequested)
token.ThrowIfCancellationRequested();
while (notDone && !token.IsCancellationRequested)
{
Thread.Sleep(8000);
result = 2;
notDone = false;
}
return result;
}, token);
return task;
}
I expect when the user cancels the request it aborts the DoWork method and not continue the function, but after sending an Exception, when "Thread.Sleep" complete, the DoWork method continue.
the user can call the API service like this method "cc" as you can see it cancel after 5 seconds and in the DoWork method "Thread.Sleep" is 9 seconds. the user gets an Exception but the method still running.
private async Task<bool> cc()
{
UriBuilder builder = new UriBuilder("http://localhost:12458/api/Test/GetInfo");
ServicePointManager.SecurityProtocol = SecurityProtocolType.Tls12;
HttpClient client = new HttpClient();
System.Threading.CancellationTokenSource s = new System.Threading.CancellationTokenSource();
s.CancelAfter(5000);
try
{
var result = client.PostAsJsonAsync<model1>(builder.ToString(), new model1 { }, s.Token).Result;
string tmp = result.Content.ReadAsStringAsync().Result;
long ApiResult = JsonConvert.DeserializeObject<long>(tmp);
}
catch (TaskCanceledException ex)
{
}
catch (OperationCanceledException ex)
{
}
catch (Exception ex)
{
}
finally
{
s.Dispose();
}
return false;
}
When use Thread.Sleep(8000) actually hold the main thread for 8 seconds and It can't check the cancellation token. you should use Task.Delay with cancellation token like this:
while (notDone && !token.IsCancellationRequested)
{
await Task.Delay(8000, token);
result = 2;
notDone = false;
}
Task.Delay check the cancellation token itself.
Ok, in your code you should process CancellationToken.IsCancellationRequested too. It's not kinda of magic, you should do this work.
public void DoWork(CancellationToken ctsToken)
{
ctsToken.ThrowIfCancellationRequested();
DoSomething();
ctsToken.ThrowIfCancellationRequested();
DoSomethingElse();
// end so on with checking CancellationToken before every part of work
}
And your Task should look like this
Task<long> t = Task.Run(async () =>
{
cancellationToken.ThrowIfCancellationRequested();
result = await DoWork(cancellationToken);
notDone = false;
cancellationToken.ThrowIfCancellationRequested();
return result;
}, cancellationToken);
In my case it was because of fiddler. When I closed the fiddler app, it started working like a charm.
I've an existing code I wrote some time ago, that works but I dislike the fact that the thread I start remains in loop.
This piece of code is a consumer on an IBMMQ code, waiting for messages to be processed.The problem I've is that with the following code
private Task ExecuteQueuePolling(CancellationToken cancellationToken)
{
return Task.Factory.StartNew(() =>
{
ConnectToAccessQueue();
Logger.Debug($"Accessed to the queue {queueName}");
Logger.DebugFormat("Repeating timer started, checking frequency: {checkingFrequency}",
checkingFrequency);
while (!cancellationToken.IsCancellationRequested)
{
Logger.Trace( () => "Listening on queues for new messages");
// isChecking = true;
var mqMsg = new MQMessage();
var mqGetMsgOpts = new MQGetMessageOptions
{ WaitInterval = (int)checkingFrequency.TotalMilliseconds };
// 15 second limit for waiting
mqGetMsgOpts.Options |= MQC.MQGMO_WAIT | MQC.MQGMO_FAIL_IF_QUIESCING |
MQC.MQCNO_RECONNECT_Q_MGR | MQC.MQOO_INPUT_AS_Q_DEF;
try
{
mqQueue.Get(mqMsg, mqGetMsgOpts);
if (string.Compare(mqMsg.Format, MQC.MQFMT_STRING, StringComparison.Ordinal) == 0)
{
var text = mqMsg.ReadString(mqMsg.MessageLength);
Logger.Debug($"Message received : [{text}]");
Message message = new Message { Content = text };
foreach (var observer in observers)
observer.OnNext(message);
}
else
{
Logger.Warn("Non-text message");
}
}
catch (MQException ex)
{
if (ex.Message == MQC.MQRC_NO_MSG_AVAILABLE.ToString())
{
Logger.Trace("No messages available");
//nothing to do, emtpy queue
}
else if (ex.Message == MQC.MQRC_CONNECTION_BROKEN.ToString())
{
Logger.ErrorException("MQ Exception, trying to recconect", ex);
throw new ReconnectException();
}
}
Thread.Sleep(100);
}
},cancellationToken);
}
//Calling method
try
{
string queueManagerName = configuration.GetValue<string>("IBMMQ:QUEUE_MANAGER_NAME");
// var queueManager = new MQQueueManager(queueManagerName,dictionary2);
QueueMonitor monitor = new QueueMonitor(configuration, "IMPORTER_RECEIVER_TEST");
//_subscription = monitor.Subscribe(receiver);
await monitor.StartAsync(cts.Token).ConfigureAwait(false);
}
catch (Exception e)
{
log.Error(e, "Error creating the queue monitor or it's subscription");
}
finally
{
WaitForCancel(cts);
}
The call to await monitor.StartAsync(cts.Token).ConfigureAwait(false); remains pending.
How should I modify my code, so that the call returns and in background the task continue to loop?
Thanks in advance
Here is how you can simplify your code by replacing Thread.Sleep with Task.Delay:
private async Task ExecuteQueuePolling(CancellationToken cancellationToken)
{
while (true)
{
// Process mqQueue here
await Task.Delay(100, cancellationToken);
}
}
Task.Delay has the advantage that accepts a CancellationToken, so in case of cancellation the loop will exit immediately. This could be important if the pooling of the MQ was lazier (for example every 5 seconds).
private static Task _runningTask;
static void Main(string[] args)
{
var cts = new CancellationTokenSource();
_runningTask = ExecuteQueuePolling(cts.Token);
WaitForCancel(cts);
}
private static void WaitForCancel(CancellationTokenSource cts)
{
var spinner = new SpinWait();
while (!cts.IsCancellationRequested
&& _runningTask.Status == TaskStatus.Running) spinner.SpinOnce();
}
private static Task ExecuteQueuePolling(CancellationToken cancellationToken)
{
var t = new Task(() =>
{
while (!cancellationToken.IsCancellationRequested)
; // your code
if (cancellationToken.IsCancellationRequested)
throw new OperationCanceledException();
}, cancellationToken, TaskCreationOptions.LongRunning);
t.Start();
return t;
}
I need to change current code to not block current thread when EventWaitHandle.WaitOne is called. Problem is that I am awaiting system-wide event. I did not find any proper replacement yet.
Code:
EventWaitHandle handle = new EventWaitHandle(false, EventResetMode.AutoReset, "Local event", out screenLoadedSignalMutexWasCreated);
StartOtherApp();
if (screenLoadedSignalMutexWasCreated)
{
isOtherAppFullyLoaded = handle.WaitOne(45000, true);
if (isOtherAppFullyLoaded )
{
// do stuff
}
else
{
// do stuff
}
handle.Dispose();
signalingCompleted = true;
}
else
{
isOtherAppFullyLoaded = false;
throw new Exception(" ");
}
I need app to continue and not stop on the line where I call WaitOne, ideally there would be await. How can I implement this ?
You can use AsyncFactory.FromWaitHandle, in my AsyncEx library:
isOtherAppFullyLoaded = await AsyncFactory.FromWaitHandle(handle,
TimeSpan.FromMilliseconds(45000));
The implementation uses ThreadPool.RegisterWaitForSingleObject:
public static Task<bool> FromWaitHandle(WaitHandle handle, TimeSpan timeout)
{
// Handle synchronous cases.
var alreadySignalled = handle.WaitOne(0);
if (alreadySignalled)
return Task.FromResult(true);
if (timeout == TimeSpan.Zero)
return Task.FromResult(false);
// Register all asynchronous cases.
var tcs = new TaskCompletionSource<bool>();
var threadPoolRegistration = ThreadPool.RegisterWaitForSingleObject(handle,
(state, timedOut) => ((TaskCompletionSource<bool>)state).TrySetResult(!timedOut),
tcs, timeout);
tcs.Task.ContinueWith(_ =>
{
threadPoolRegistration.Dispose();
}, TaskScheduler.Default);
return tcs.Task;
}
I have the async code that implements cancellation token. It's working but Im not pretty sure if this is the right way to do it so I just want feedback about it.
Here is the actual code:
/// <summary>
///
/// </summary>
private async void SaveData() {
if (GetActiveServiceRequest() != null)
{
var tokenSource = new System.Threading.CancellationTokenSource();
this.ShowWizardPleaseWait("Saving data...");
var someTask = System.Threading.Tasks.Task<bool>.Factory.StartNew(() =>
{
bool returnVal = false;
// Set sleep of 7 seconds to test the 5 seconds timeout.
System.Threading.Thread.Sleep(7000);
if (!tokenSource.IsCancellationRequested)
{
// if not cancelled then save data
App.Data.EmployeeWCF ws = new App.Data.EmployeeWCF ();
returnVal = ws.UpdateData(_employee.Data);
ws.Dispose();
}
return returnVal;
}, tokenSource.Token);
if (await System.Threading.Tasks.Task.WhenAny(someTask, System.Threading.Tasks.Task.Delay(5000)) == someTask)
{
// Completed
this.HideWizardPleaseWait();
if (someTask.Result)
{
this.DialogResult = System.Windows.Forms.DialogResult.OK;
}
else
{
this.DialogResult = System.Windows.Forms.DialogResult.Abort;
}
btnOK.Enabled = true;
this.Close();
}
else
{
tokenSource.Cancel();
// Timeout logic
this.HideWizardPleaseWait();
MessageBox.Show("Timeout. Please try again.")
}
}
}
Does async / await / cancellation code is well implemented?
Thanks and appreciate the feedback.
In general, you should use ThrowIfCancellationRequested. That will complete the returned Task in a canceled state, rather than in a "ran to completion successfully" state with a false result.
Other points:
Avoid async void. This should be async Task unless it's an event handler.
Prefer Task.Run over TaskFactory.StartNew.
Use using.
If you're just using CancellationTokenSource as a timeout, then it has special capabilities for that. Creating a separate task via Task.Delay and Task.WhenAny isn't necessary.
Here's what the updated code would look like:
private async Task SaveData()
{
if (GetActiveServiceRequest() != null)
{
var tokenSource = new System.Threading.CancellationTokenSource(TimeSpan.FromSeconds(5));
var token = tokenSource.Token;
this.ShowWizardPleaseWait("Saving data...");
var someTask = System.Threading.Tasks.Task.Run(() =>
{
// Set sleep of 7 seconds to test the 5 seconds timeout.
System.Threading.Thread.Sleep(7000);
// if not cancelled then save data
token.ThrowIfCancellationRequested();
using (App.Data.EmployeeWCF ws = new App.Data.EmployeeWCF())
{
return ws.UpdateData(_employee.Data);
}
}, token);
try
{
var result = await someTask;
// Completed
this.HideWizardPleaseWait();
if (result)
{
this.DialogResult = System.Windows.Forms.DialogResult.OK;
}
else
{
this.DialogResult = System.Windows.Forms.DialogResult.Abort;
}
btnOK.Enabled = true;
this.Close();
}
catch (OperationCanceledException)
{
// Timeout logic
this.HideWizardPleaseWait();
MessageBox.Show("Timeout. Please try again.")
}
}
}