I'm looking to listen to the result of a task(s), or timeout if the none are made or completed. Specifically, I'm using Playwright and am adding an event listener for requests and want to timeout if none of those requests include what I'm looking for (or even get created). Example code below:
I've tried looking into WhenAny, but that requires knowing the tasks ahead of time. Also looked into Monitor.TryEnter, but not sure how to have the requests acquire the lock before they're generated? Maybe a semaphore or something?
string? result = null;
// Start listening for new pages (ClickAsync below opens in a new page)
context.Page += (_, newPage) =>
{
// From the new page, start listening for requests that page generates
newPage.Request += async (_, request) =>
{
if (request.Url.Contains("some_identifier"))
{
await newPage.GotoAsync(request.Url);
result = await newPage.InnerTextAsync("body");
// Do something here? We now have the result we want, stop waiting and return it!
}
};
};
await mainPage.Locator("a#somelink").ClickAsync();
// Wait for *up to* 30 seconds here.
return result;
Using ideas from comments on my answer I was able to accomplish the goal. I ended up passing a cancellation token to a Task.Delay(30000, token). Then if I got the data I wanted and set the result value, I just cancelled the token.
string? result = null;
var tokenSource = new CancellationTokenSource();
var cancelToken = tokenSource.Token;
// Start listening for new pages (ClickAsync below opens in a new page)
context.Page += (_, newPage) =>
{
// From the new page, start listening for requests that page generates
newPage.Request += async (_, request) =>
{
if (request.Url.Contains("some_identifier"))
{
await newPage.GotoAsync(request.Url);
result = await newPage.InnerTextAsync("body");
tokenSource.Cancel(); // <-- Got what we needed, cancel 30 second timer
}
};
};
await mainPage.Locator("a#somelink").ClickAsync();
try {
await Task.Delay(30000, cancelToken); // <-- Timeout after 30 seconds.
}
catch
{
// Not a fan of tokenSource.Cancel() throwing an exception, but I can deal with it.
}
return result;
Related
I have several asynec methods.
One of them triggers a POST method which start a process. I then need to 'sample' the results of another GET method every 10 minutes, and check if the status has changed from "pending" to "success" .
I tryed usingSystem.Threading.Timer with no luck, complaining about my method being asynced .
Error CS0407 'Task Campaigns.repeat(object)' has the wrong return type Campaigns
This is my code:
public async Task waitForCampaignLoadAsync(string Uri)
{
...........
var container = JsonConvert.DeserializeObject<CampaignTempleteStatus>(json);
if(container.status == "pending")
{
var autoEvent = new AutoResetEvent(false);
//The next row triggers the error
var stateTimer = new Timer(repeat, autoEvent, 1000, (1000 * 60 * 10));
//How can I keep repeating this, until (bool isFinished = true)??
}
public async Task repeat(Object stateInfo)
{
if(...)
isFinished = true;
}
Another thing is , how do I pass extra info inside repeat function? I need to pass the Uri input for inner ussage ?
When an asynchronous method starts getting complicated it's a sure sign something is wrong. Most of the time async code looks almost the same as synchronous code with the addition of await.
A simple polling loop could be as simple as :
public async Task<string> waitForCampaignLoadAsync(string uri)
{
var client=new HttpClient();
for(int i=0;i<30;i++)
{
token.ThrowIfCancellationRequested();
var json = await client.GetStringAsync(uri);
var container = JsonConvert.DeserializeObject<CampaignTempleteStatus>(json);
if (container.status != "pending")
{
return container.status;
}
await Task.Delay(10000);
}
return "Timed out!";
}
Cancellation in managed threads explains how CancellationTokenSource and CancellationToken can be used to cancel threads, tasks and asynchronous functions. Many asynchronous methods already provide overloads that accept a CancellationToken parameter. The polling function could be modified to accept and check a canellation token :
public async Task<string> waitForCampaignLoadAsync(string uri,CancellationToken token=default)
{
var client=new HttpClient();
for(int i=0;i<30;i++)
{
var json = await client.GetStringAsync(uri);
var container = JsonConvert.DeserializeObject<CampaignTempleteStatus>(json);
if (container.status != "pending")
{
return container.status;
}
await Task.Delay(10000,token);
}
return "Timed out!";
}
A CancellationTokenSource can be used to call this method with an overall timeout of eg, 5 minutes :
var cts=new CancellationTokenSource(TimeSpan.FromMinutes(5));
try
{
var result=waitForCampaignLoadAsync(uri,cts.Token);
//Process the result ....
}
catch(OperationCancelledExcepction ex)
{
//Handle the timeout here
}
This code can be improved. For example, GetStringAsync() doesn't accept a cancellation token. The operation can be broken in two steps though, one call to GetAsync() with a cancellation token that waits for the server to send a result
and another to HttpContent.ReadAsStringAsync() to read the response, eg :
var response=await client.GetAsync(uri,token)
response.EnsureSuccessStatusCode();
var json=await response.Content.ReadAsStringAsync();
...
The first parameter of Timer is a TimerCallback delegate, which should return void
var stateTimer = new Timer(Repeat, autoEvent, 1000, (1000 * 60 * 10));
private void Repeat(object state)
{
....
}
I have a console app, where I need to access some url 200 times, wait for all of the requests to return and work on the 200 results.
I did it like that, in parallel:
var classNameTasks = Enumerable.Range(1, 200).Select(i => webApi.getSplittedClassName()).ToArray();
string[][] splittedClassNames = await Task.WhenAll(classNameTasks);
if (splittedClassNames[0] == null)
result = new TextResult("Error accessing the web");
getSplittedClassName returns a string[], if the internet is off it will return null.
Now, as you can see, after the completion of all the tasks, I do an if to check the content, if its null -> internet issues.
The problem here is that I need to wait for the whole 200 requests to return before I can check the content.
I am looking for a way to right away detect a scenario where there is no internet, and I return null, without having to wait for the 200 requests.
To do this you need
A CancellationTokenSource to signal that the job is done.
The WhenAll method from Tortuga.Anchor
static async Task Test()
{
TextResult result;
var cts = new CancellationTokenSource();
var classNameTasks = Enumerable.Range(1, 200).Select(i => getSplittedClassName(cts)).ToArray();
await classNameTasks.WhenAll(cts.Token);
if (cts.IsCancellationRequested)
result = new TextResult("Error accessing the web");
string[][] splittedClassNames = classNameTasks.Select(t => t.Result).ToArray();
}
private static async Task<string[]> getSplittedClassName(CancellationTokenSource cts)
{
try
{
//real code goes here
await Task.Delay(1000, cts.Token); //the token would be passed to the real web method
return new string[0];
}
catch
{
cts.Cancel(); //stop trying
return null;
}
}
My requirements:
Run method DoWork on a specified interval.
If stop is called between calls to DoWork just stop the timer.
If stop is called while DoWork is running, block until DoWork is finished.
If DoWork takes too long to finish after stop is called, timeout.
I have a solution that seems to work so far, but I'm not super happy with it and think I may be missing something. The following is the void Main from my test app:
var source = new CancellationTokenSource();
// Create an observable sequence for the Cancel event.
var cancelObservable = Observable.Create<Int64>(o =>
{
source.Token.Register(() =>
{
Console.WriteLine("Start on canceled handler.");
o.OnNext(1);
Console.WriteLine("End on canceled handler.");
});
return Disposable.Empty;
});
var observable =
// Create observable timer.
Observable.Timer(TimeSpan.Zero, TimeSpan.FromSeconds(10), Scheduler.Default)
// Merge with the cancel observable so we have a composite that
// generates an event every 10 seconds AND immediately when a cancel is requested.
.Merge(cancelObservable)
// This is what I ended up doing instead of disposing the timer so that I could wait
// for the sequence to finish, including DoWork.
.TakeWhile(i => !source.IsCancellationRequested)
// I could put this in an observer, but this way exceptions could be caught and handled
// or the results of the work could be fed to a subscriber.
.Do(l =>
{
Console.WriteLine("Start DoWork.");
Thread.Sleep(TimeSpan.FromSeconds(5));
Console.WriteLine("Finish DoWork.");
});
var published = observable.Publish();
var disposable = published.Connect();
// Press key between Start DoWork and Finish DoWork to test the cancellation while
// running DoWork.
// Press key between Finish DoWork and Start DoWork to test cancellation between
// events.
Console.ReadKey();
// I doubt this is good practice, but I was finding that o.OnNext was blocking
// inside of register, and the timeout wouldn't work if I blocked here before
// I set it up.
Task.Factory.StartNew(source.Cancel);
// Is there a preferred way to block until a sequence is finished? My experience
// is there's a timing issue if Cancel finishes fast enough the sequence may already
// be finished by the time I get here and .Wait() complains that the sequence contains
// no elements.
published.Timeout(TimeSpan.FromSeconds(1))
.ForEach(i => { });
disposable.Dispose();
Console.WriteLine("All finished! Press any key to continue.");
Console.ReadKey();
First, in your cancelObservable, make sure and return the result of Token.Register as your disposable instead of returning Disposable.Empty.
Here's a good extension method for turning CancellationTokens into observables:
public static IObservable<Unit> AsObservable(this CancellationToken token, IScheduler scheduler)
{
return Observable.Create<Unit>(observer =>
{
var d1 = new SingleAssignmentDisposable();
return new CompositeDisposable(d1, token.Register(() =>
{
d1.Disposable = scheduler.Schedule(() =>
{
observer.OnNext(Unit.Default);
observer.OnCompleted();
});
}));
});
}
Now, to your actual request:
public IObservable<Unit> ScheduleWork(IObservable<Unit> cancelSignal)
{
// Performs work on an interval
// stops the timer (but finishes any work in progress) when the cancelSignal is received
var workTimer = Observable
.Timer(TimeSpan.Zero, TimeSpan.FromSeconds(10))
.TakeUntil(cancelSignal)
.Select(_ =>
{
DoWork();
return Unit.Default;
})
.IgnoreElements();
// starts a timer after cancellation that will eventually throw a timeout exception.
var timeoutAfterCancelSignal = cancelSignal
.SelectMany(c => Observable.Never<Unit>().Timeout(TimeSpan.FromSeconds(5)));
// Use Amb to listen to both the workTimer
// and the timeoutAfterCancelSignal
// Since neither produce any data we are really just
// listening to see which will complete first.
// if the workTimer completes before the timeout
// then Amb will complete without error.
// However if the timeout expires first, then Amb
// will produce an error
return Observable.Amb(workTimer, timeoutAfterCancelSignal);
}
// Usage
var cts = new CancellationTokenSource();
var s = ScheduleWork(cts.Token.AsObservable(Scheduler.Default));
using (var finishedSignal = new ManualResetSlim())
{
s.Finally(finishedSignal.Set).Subscribe(
_ => { /* will never be called */},
error => { /* handle error */ },
() => { /* canceled without error */ } );
Console.ReadKey();
cts.Cancel();
finishedSignal.Wait();
}
Note, instead of cancellation tokens you can also do:
var cancelSignal = new AsyncSubject<Unit>();
var s = ScheduleWork(cancelSignal);
// .. to cancel ..
Console.ReadKey();
cancelSignal.OnNext(Unit.Default);
cancelSignal.OnCompleted();
i have a function that makes a few http requests, and runs in a task.
that function can be interrupted in the middle, since i cant abort the task i added some boolean conditions in the function.
example:
public int foo(ref bool cancel)
{
if(cancel)
{
return null
}
//do some work...
if(cancel)
{
return null
}
//http webrequest
if(cancel)
{
return null
}
}
thisworked pretty good, although this is quite some ugly code.
another problem is when i already executed the web request, and it takes time for me to get the response than the function cncelation takes a lot of time (till i get a response).
is there a better way for me to check this? or mybe i should use threads instead of task?
edit
i added a cancelation token: declared a cancelationTokenSource, and passed its token to the task
CancellationTokenSource cncelToken = new CancellationTokenSource();
Task t = new Task(() => {foo()},cancelToken.token);
when i do cancelToken.Cancel();
i still wait for the response, and the tsk isnt cancelling.
Tasks support cancellation - see here.
Here's a quick snippet.
class Program
{
static void Main(string[] args)
{
var token = new CancellationTokenSource();
var t = Task.Factory.StartNew(
o =>
{
while (true)
Console.WriteLine("{0}: Processing", DateTime.Now);
}, token);
token.CancelAfter(1000);
t.Wait(token.Token);
}
}
Remember to wait for the task using the provided cancellation token. You ought to receive an OperationCanceledException.
How do I cancel all but the last/latest task? For example, suppose I have a time-consuming task that is triggered on a button click. I only want the task from the last button click to run and the previous ones to cancel. Can you show me how this is normally done?
My attempt involves storing all tasks in a list along with their cancellation token and removing them when either the task completes or is cancelled. Creating a list to store both Task and CancellationToken seems like I'm doing too much for what I would've thought is a common requirement (eg. a User does a search for something and clicks the search button multiple times. Shouldn't only the last search be made and all other cancelled?). This is a common scenario, so I'd like to know how this is normally done. What is best practice here?
async void DoStuffAsync()
{
// Store tasks in a list
if (tasksAndCancelTokens == null)
tasksAndCancelTokens = new List<Tuple<Task, CancellationTokenSource>>();
else // we have a new Get request so cancel any previous
tasksAndCancelTokens.ForEach(t => t.Item2.Cancel());
// Create Cancellation Token
var cts = new CancellationTokenSource();
// Method to run asynchonously
Func<int> taskAction = () =>
{
// Something time consuming
Thread.Sleep(5000);
if (cts.Token.IsCancellationRequested)
cts.Token.ThrowIfCancellationRequested();
return 100;
};
// Create Task
Task<int> task = Task<int>.Factory.StartNew(taskAction, cts.Token);
// Create Tuple to store task in list
var tup = new Tuple<Task, CancellationTokenSource>(task, cts);
tasksAndCancelTokens.Add(tup);
try
{
int i = await task;
}
catch (OperationCanceledException)
{
// Don't need to do anything
}
finally
{
tasksAndCancelTokens.Remove(tup);
}
}
Thanks
If you just want to cancel the last task, then that's all you have to do:
CancellationTokenSource cts = null;
async void Button1_Click(...)
{
// Cancel the last task, if any.
if (cts != null)
cts.Cancel();
// Create Cancellation Token for this task.
cts = new CancellationTokenSource();
var token = cts.Token;
// Method to run asynchonously
Func<int> taskAction = () =>
{
// Something time consuming
Thread.Sleep(5000);
token.ThrowIfCancellationRequested();
return 100;
};
try
{
int i = await Task.Run(taskAction);
}
catch (OperationCanceledException)
{
// Don't need to do anything
return;
}
}