How to handle updates in the right way using signalr? - c#

I have a client application Angular and a signalR hub, and also I have a service that take a timestamp as a parameter.
I want to invoke a method in the hub when I press on a start button in the client, and when the method is invoked I want to keep listing to all the changes (create a timer) until the client press on the stop button then I will stop the timer.
So I want to ask which is better:
1- Call the invoked method from the client with time stamp and then create a setInterval to call the method in it and when the stop button is pressed I can stop it.
Pros:
It is easy to start and stop the timer.
Cons:
I am invoking the method each 1 sec, and then I am checking on the client if there are response to update the UI.
2- Invoke the method once and then create a timer for each client on the server and when the client press on stop button I can invoke another method to stop the timer for that client.
Pros:
I am checking the timestamp in the hub and I will send the data to the client only if the timeStamp from the service > timeStamp locally
Cons:
I actually don't know how to create a timer for each client, so if this is the right way please help me

You are using SignalR for real time data communication. Invoking a method every second is just joking on the SignalR face... So this is not a solution.
The best solution would be using the group feature.
Example:
You start button will add the user to an group.
While your user is on the group it will receive all the data you need. await this.Clients.Group("someGroup").BroadcastMessage(message);
Your stop button will remove the user from the group so it will not receive data anymore.
Some code example on the hub:
public async Task Start()
{
// Add user to the data group
await this.Groups.AddToGroupAsync(this.Context.ConnectionId, "dataGroup");
}
public async Task Stop()
{
// Add user to the data group
await this.Groups.RemoveFromGroupAsync(this.Context.ConnectionId, "dataGroup");
}
Worker example that sends data to the users that pressed start and receive real time data.
private readonly IHubContext<SignalRHub, ISignalRHub> hub;
private readonly IServiceProvider serviceProvider;
public Worker(IServiceProvider serviceProvider, IHubContext<SignalRHub, ISignalRHub> hub)
{
this.serviceProvider = serviceProvider;
this.hub = hub;
}
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
{
while (!stoppingToken.IsCancellationRequested)
{
await this.hub.Clients.Group("dataGroup").BroadcastMessage(DataManager.GetData());
this.Logger.LogDebug("Sent data to all users at {0}", DateTime.UtcNow);
await Task.Delay(1000, stoppingToken);
}
}
PS: Where you have the worker, I assume you have some manager that gets the data or something to be sent to the user.
Edit: If you don't want to user worker, you can always just the timer like:
public class TimerManager
{
private Timer _timer;
private AutoResetEvent _autoResetEvent;
private Action _action;
public DateTime TimerStarted { get; }
public TimerManager(Action action)
{
_action = action;
_autoResetEvent = new AutoResetEvent(false);
_timer = new Timer(Execute, _autoResetEvent, 1000, 2000);
TimerStarted = DateTime.Now;
}
public void Execute(object stateInfo)
{
_action();
if((DateTime.Now - TimerStarted).Seconds > 60)
{
_timer.Dispose();
}
}
}
And then use it somewhere like:
var timerManager = new TimerManager(() => this.hub.Clients.Group("dataGroup").BroadcastMessage(DataManager.GetData()));

Option #1 isn't available since SignalR exists to remove the need for polling. Frequent polling doesn't scale either. If every client polled the server every 1 second, the web site would end up paying a lot of CPU and bandwidth for nothing. Business people don't like frequent polling either, as all hosters and cloud providers charge for egress.
The SignalR streaming examples use timed notifications as a simple example of streaming notifications using IAsyncEnumerable<T>. In the simplest example, a counter increments every delay milliseconds :
public class AsyncEnumerableHub : Hub
{
public async IAsyncEnumerable<int> Counter(
int count,
int delay,
[EnumeratorCancellation]
CancellationToken cancellationToken)
{
for (var i = 0; i < count; i++)
{
// Check the cancellation token regularly so that the server will stop
// producing items if the client disconnects.
cancellationToken.ThrowIfCancellationRequested();
yield return i;
// Use the cancellationToken in other APIs that accept cancellation
// tokens so the cancellation can flow down to them.
await Task.Delay(delay, cancellationToken);
}
}
}
The client can call this action passing the desired delay and just start receiving notifications. SignalR knows this is a stream of notifications because it returns IAsyncEnumerable.
The next, more advanced example uses Channels to allow the publisher method WriteItemsAsync to send a stream of notifications to the hub.
The action itself is simpler, it just returns the Channel's reader:
public ChannelReader<int> Counter(
int count,
int delay,
CancellationToken cancellationToken)
{
var channel = Channel.CreateUnbounded<int>();
// We don't want to await WriteItemsAsync, otherwise we'd end up waiting
// for all the items to be written before returning the channel back to
// the client.
_ = WriteItemsAsync(channel.Writer, count, delay, cancellationToken);
return channel.Reader;
}
The publisher method writes to the ChannelWriter instead of returning an IAsyncEnumerable :
private async Task WriteItemsAsync(
ChannelWriter<int> writer,
int count,
int delay,
CancellationToken cancellationToken)
{
Exception localException = null;
try
{
for (var i = 0; i < count; i++)
{
await writer.WriteAsync(i, cancellationToken);
// Use the cancellationToken in other APIs that accept cancellation
// tokens so the cancellation can flow down to them.
await Task.Delay(delay, cancellationToken);
}
}
catch (Exception ex)
{
localException = ex;
}
writer.Complete(localException);
}
This method can easily be in a different class. All that's needed is to pass ChannelWriter to the publisher.

Related

Why does it take so long to cancel a Cosmos Query?

I'm trying to cancel a cosmos query using the cancellation token using the NuGet package Microsoft.Azure.Cosmos (3.13.0).
Can anyone help explain why it takes so long to cancel?
This test shows that it takes 2000+ Milliseconds to cancel. I was expecting it to fail milliseconds after I cancelled it.
00158: Reading next
00160: Read next
00188: Cancelling
02492: The operation was canceled.
public class CosmosCancelationTests
{
private readonly ITestOutputHelper testOutputHelper;
public CosmosCancelationTests(ITestOutputHelper testOutputHelper)
{
this.testOutputHelper = testOutputHelper;
}
[Fact]
public async Task TestCancelationTime()
{
Stopwatch stopwatch = Stopwatch.StartNew();
try
{
var client = new CosmosClient(
"https://localhost:8081/",
"C2y6yDjf5/R+ob0N8A7Cgv30VRDJIWEHLM+4QDU5DE2nQ9nDuVTqobD4b8mGGyPMbIZnqyMsEcaGQy67XIw/Jw=="
);
var database = client.GetDatabase("CosmosDBPackage"); // Make sure this database is created in the cosmos instance
var container = database.GetContainer("SampleEntity"); // Make sure this container is created in the database
var cancellationTokenSource = new CancellationTokenSource();
var cancellationToken = cancellationTokenSource.Token;
using var feedIterator = container.GetItemLinqQueryable<SampleEntity>()
.Where(x => false)
.ToFeedIterator();
if (feedIterator.HasMoreResults)
{
testOutputHelper.WriteLine($"{stopwatch.ElapsedMilliseconds:D5}: Reading next");
var task = feedIterator.ReadNextAsync(cancellationToken);
testOutputHelper.WriteLine($"{stopwatch.ElapsedMilliseconds:D5}: Read next");
await Task.Delay(20);
cancellationTokenSource.Cancel();
testOutputHelper.WriteLine($"{stopwatch.ElapsedMilliseconds:D5}: Cancelling");
await task;
}
}
catch (CosmosOperationCanceledException e)
{
testOutputHelper.WriteLine($"{stopwatch.ElapsedMilliseconds:D5}: {e.Message}");
}
}
}
CancellationTokens in .NET is a mechanism that by definition, won't cause any library to stop what it's doing immediately, it is a cooperative cancellation. The library will cancel the operation when it's safe and won't cause a corrupt or invalid state. For example, HttpClient, when you call SendAsync and the token cancels, if the client already started buffering the response, it won't cancel, it will wait until it completes.
In the case of the query, it will check the token, when it is safe and won't cause an invalid state, maybe the cancellation is happening while requests are on the wire, or responses are being aggregated.
Reference: https://learn.microsoft.com/en-us/dotnet/standard/threading/cancellation-in-managed-threads

In ASP.NET Core 3.1, how can I schedule a background task (Cron Jobs) with hosted services for a specific date and time in the future?

I am working on a project based on ASP.NET Core 3.1 and I want to add a specific functionality to it to schedule publishing a post in the future in a date and time specified by post author (something like what Wordpress does for scheduled posts through its cron jobs). For example, if we receive this date and time from user :
2020-09-07 14:08:07
Then, how can I schedule a background task for it by using hosted services to run only for one time and to change a flag in database and save changes after that?
I've read some articles about it but they didn't specify date and time and just mentioned repeated tasks for every 5 second and stuff like that with cron expressions, but, the thing I need to know is how can I schedule a background task for a specific date and time?
Thank you in advance.
I combined CrontabSchedule with IHostedService. The implementation below is lightweight (no architecture imposing libs) and no polling.
public class SomeScheduledService: IHostedService
{
private readonly CrontabSchedule _crontabSchedule;
private DateTime _nextRun;
private const string Schedule = "0 0 1 * * *"; // run day at 1 am
private readonly SomeTask _task;
public SomeScheduledService(SomeTask task)
{
_task = Task;
_crontabSchedule = CrontabSchedule.Parse(Schedule, new CrontabSchedule.ParseOptions{IncludingSeconds = true});
_nextRun = _crontabSchedule.GetNextOccurrence(DateTime.Now);
}
public Task StartAsync(CancellationToken cancellationToken)
{
Task.Run(async () =>
{
while (!cancellationToken.IsCancellationRequested)
{
await Task.Delay(UntilNextExecution(), cancellationToken); // wait until next time
await _task.Execute(); //execute some task
_nextRun = _crontabSchedule.GetNextOccurrence(DateTime.Now);
}
}, cancellationToken);
return Task.CompletedTask;
}
private int UntilNextExecution() => Math.Max(0, (int)_nextRun.Subtract(DateTime.Now).TotalMilliseconds);
public Task StopAsync(CancellationToken cancellationToken) => Task.CompletedTask;
}
After some trial and error I found a way to schedule a background task for specific date and time by using hosted service as I asked in the question, and, I did that with System.Threading.Timer and Timespan like this:
public class ScheduleTask : IScheduler, IDisposable
{
private Timer _timer;
private IBackgroundTaskQueue TaskQueue { get; }
// Set task to schedule for a specific date and time
public async Task SetAndQueueTaskAsync(ScheduleTypeEnum scheduleType, DateTime scheduleFor, Guid scheduledItemId)
{
// Omitted for simplicity
// ....
TaskQueue.QueueBackgroundWorkItem(SetTimer);
}
// ......
// lines omitted for simplicity
// .....
// Set timer for schedule item
private Task SetTimer(CancellationToken stoppingToken)
{
// ......
// lines omitted for simplicity
// .....
_timer = new Timer(DoWork, null, (item.ScheduledFor - DateTime.UtcNow).Duration(), TimeSpan.Zero);
return Task.CompletedTask;
}
private void DoWork(object state)
{
ScheduledItemChangeState(DateTime.UtcNow).Wait();
}
// Changes after the scheduled time comes
private async Task ScheduledItemChangeState(DateTime scheduleFor)
{
using (var scope = Services.CreateScope())
{
var context =
scope.ServiceProvider
.GetRequiredService<DataContext>();
// Changing some data in database
}
}
public void Dispose()
{
_timer?.Dispose();
}
}
If you look at the part of the above code in which I passed (item.ScheduledFor - DateTime.UtcNow) as Timer class constructor's third parameter to initialize a new instance of it, I actually ask the timer to do a specific work in a specific time I stored as a DateTime in item.ScheduledFor.
You could read more about background tasks with hosted services in ASP.NET Core here from official Microsoft docs:
https://learn.microsoft.com/en-us/aspnet/core/fundamentals/host/hosted-services?view=aspnetcore-3.1&tabs=visual-studio
To see the full implementation in my Github repo which has the possibility to recover the scheduled tasks from database after restarting the server, use the following link:
https://github.com/aspian-io/aspian/tree/master/Infrastructure/Schedule
I want to add a specific functionality to it to schedule publishing a post in the future in a date and time specified by post author.For example, if we receive this date and time from user : 2020-09-07 14:08:07 .
Then, how can I schedule a background task for it by using hosted services to run only for one time and to change a flag in database and save changes after that?
It seems that you'd like to execute a background task/job at a user specified datetime, to achieve the requirement, you can try to use some message queue services, such as Azure Queue Storage, which enable us to specify how long the message should be invisible to Dequeue and Peek operations by setting visibilityTimeout.
While your application user want to create a new post and specify a publishing date time, you can insert a new message (with specified visibilityTimeout based on user expected datetime) into the queue, so that this new inserted message would be only visible at specified date time in the queue.
QueueClient theQueue = new QueueClient(connectionString, "mystoragequeue");
if (null != await theQueue.CreateIfNotExistsAsync())
{
//The queue was created...
}
var newPost = "Post Content Here";
var user_specified_datetime = new DateTime(2020, 9, 9, 20, 50, 25);
var datetime_now = DateTime.Now;
TimeSpan duration = user_specified_datetime.Subtract(datetime_now);
await theQueue.SendMessageAsync(newPost, duration, default);
Then you can implement a queue triggered background task to retrieve message(s) from the queue and update your database record(s).
Note: Microsoft Azure Storage Emulator is a tool that emulates the Azure Queue etc services for local development and testing purposes, you can try to test code against the storage services locally without creating an Azure subscription or incurring any costs.
Use DNTScheduler and set specific date and time
services.AddDNTScheduler(options =>
{
// DNTScheduler needs a ping service to keep it alive. Set it to false if you don't need it. Its default value is true.
// options.AddPingTask = false;
options.AddScheduledTask<DoBackupTask>(
runAt: utcNow =>
{
var now = utcNow.AddHours(3.5);
return now.Hour == 14 && now.Minute == 08 && now.Second == 07;
},
order: 1);
});

How to cancel await Task that perform multiple task in C#

I have a function at idle time when at certain interval e.g. every 30 seconds, it will perform a polling to hardware for status.
The method name is public static async Task PollCurrentHardwareStatus() which inside this method, it will go through status of every hardwares e.g. I have 4 devices to get connection status (printer module, cash module, coin module, terminal module).
If public touch on main screen, it will divert to next page which I need to cancel the poll status. How to cancel poll status of device in await Task?
I have come through Cancel an Async Task or a List of Tasks (C#) but I seems cannot get the idea where to put the CancellationToken.
My code at PollCurrentHardwareStatus:-
public static async Task PollCurrentHardwareStatus()
{
try
{
//POLLING CARD READER
if (GlobVars.HwIDTech.Enabled)
{
if (IDTechDevice.PingForReply())
{
LogEvents($"[App] Terminal OK.", EventLogEntryType.Information);
AppDeviceStatus.strIDTechStatus = StatusMessageIDTech.strSuccessID;
}
else
{
LogEvents($"[App] IDTechDevice: Not found/Disconnected", EventLogEntryType.Information);
AppDeviceStatus.strIDTechStatus = StatusMessageIDTech.strErrorID;
}
}
//POLLING PRINTER
if (GlobVars.HwCustom.Enabled)
{
string description = string.Empty;
int status = 0;
PrintMain.PrinterGetStatus(ref description, ref status);
if (status == 0)
{
AppDeviceStatus.strPrinterStatus = StatusMessagePrinter.strSuccessID;
}
else
{
LogEvents($"[App] Printer error: {description}", EventLogEntryType.Information);
AppDeviceStatus.strPrinterStatus = StatusMessagePrinter.strErrorID;
}
}
//POLLING CASH COIN MODULE
if (GlobVars.HwB2B.Enabled && GlobVars.HwBCR.Enabled)
{
string B2BStatus = await CCMain.GetCurrentDeviceStatus();
if (B2BStatus == "DISABLED")
{
AppDeviceStatus.strB2BStatus = StatusMessageB2B.strSuccessID;
LogEvents($"[App] Poll B2B device: Status - OK.", EventLogEntryType.Information);
}
else
{
LogEvents($"[App] Poll B2B device: Status - {B2BStatus}.", EventLogEntryType.Information);
AppDeviceStatus.strB2BStatus = StatusMessageB2B.strErrorID;
}
if (ModuleCoins.OpenConnection())
{
await ModuleCoins.PerformSelfTest();
AppDeviceStatus.strBCRStatus = StatusMessageBCR.strSuccessID;
}
else
{
AppDeviceStatus.strBCRStatus = StatusMessageBCR.strErrorID;
}
}
UpdateErrorStatus();
}
catch (Exception ex)
{
LogEvents($"[App] Poll hardware status : Ex-{ex.Message}. Stack Trace-{ex.StackTrace}", EventLogEntryType.Error);
}
await Task.Delay(100);
}
I think you can create the CancellationTokenSource from the method which you are calling PollCurrentHardwareStatus(). Please check bellow example:
Add CancellationTokenSource as a parameter in PollCurrentHardwareStatus method
public static async Task PollCurrentHardwareStatus(CancellationToken cts)
{
// your logic code
// ...............
}
Create a CancellationTokenSource and call it on your Page class:
public class Page
{
private CancellationTokenSource cancellationTokenSource;
public Page()
{
cancellationTokenSource = new CancellationTokenSource();
}
public async void CallPoll()
{
await PollCurrentHardwareStatus(cancellationTokenSource.Token);
}
public void OnCancelPoll(object sender, EventArgs e)
{
cancellationTokenSource.Cancel();
}
}
According to MSDN: Cancellation in managed threads
Cancellation is cooperative and is not forced on the listener. The
listener determines how to gracefully terminate in response to a
cancellation request.
You'll have to create an overload of PollCurrentHardwareStatus that takes a CancellationToken object as input. The function should regularly check whether cancellation is requested and cancel the function gracefully.
There are several problems in this: what is reqularly? what to do when cancellation is requested.
The answers are up to your requirements. It depends on the kine of interruption on wether you should have cancelled within 50 msec, or whether the cancellation may take upon a second. For example, if your process is interrupted when a operator touches your screen for the first time, this operator might be wiling to wait half a second before the screen responds. But if your process is interrupted every time the operator types a letter, then one second to cancel might be annoying.
So the question how often you should check for cancellation depends on the usage.
async Task PollCurrentHardwareStatus(CancellatinToken token)
{
token.ThrowIfCancellationRequested();
DoSomeShortProcessing();
token.ThrowIfCancellationRequested();
DoSomeOtherProcessing();
token.ThrowIfcancellationRequested();
etc.
The problem arises if you call a function that would take longer time to process. Best thing would be to pass the token to the other process:
LongProcessingFunction(token);
The other function should regularly check the token.
Gracefully cancellation depends on this.
If you can't change the other function, you can't guarantee proper and fast cancellation.
async-await does not help you in this. It is up to programmers who create awaitable functions to provide versions that accept a CancellationToken.
What you'll find is that all basic awaitable functions (Read / Write file, fetch information form a database or the internet, etc) have a version that accepts a CancellationToken.
You could start a Thread and kill this thread when cancellation is requested, but that is fairly dangerous, because you don't know the status of the object when the thread is killed. I would not advise that.

Use ReliableQueue in ServiceFabric without polling?

I've been looking at stateful services within Service Fabric. I've been digging through the examples, specifically the WordCount. They have a RunAsync method that looks like this inside of the WordCountService:
protected override async Task RunAsync(CancellationToken cancellationToken)
{
IReliableQueue<string> inputQueue = await this.StateManager.GetOrAddAsync<IReliableQueue<string>>("inputQueue");
while (true)
{
cancellationToken.ThrowIfCancellationRequested();
try
{
using (ITransaction tx = this.StateManager.CreateTransaction())
{
ConditionalValue<string> dequeuReply = await inputQueue.TryDequeueAsync(tx);
if (dequeuReply.HasValue)
{
//... {more example code here }
}
await Task.Delay(TimeSpan.FromMilliseconds(100), cancellationToken);
}
catch (TimeoutException)
{
//Service Fabric uses timeouts on collection operations to prevent deadlocks.
//If this exception is thrown, it means that this transaction was waiting the default
//amount of time (4 seconds) but was unable to acquire the lock. In this case we simply
//retry after a random backoff interval. You can also control the timeout via a parameter
//on the collection operation.
Thread.Sleep(TimeSpan.FromSeconds(new Random().Next(100, 300)));
continue;
}
catch (Exception exception)
{
//For sample code only: simply trace the exception.
ServiceEventSource.Current.MessageEvent(exception.ToString());
}
}
}
Essentially, in this example, the service is polling the ReliableQueue every 100ms for messages. Is there a way to do this without the poll? Can we subscribe to an event or something that gets triggered when a message is successfully added to the ReliableQueue?
I'd recommend using a ReliableDispatcher in your service, or just use a Dispatcher Service.
Using the Dispatcher Service allows you to write a method that is invoked whenever an item is enqueued on the underlying reliable queue.
For example:
public override async Task OnItemDispatchedAsync(
ITransaction transaction,
int value,
CancellationToken cancellationToken)
{
// Do something with the value that has been dequeued
}
Both the Reliable Dispatcher and Dispatcher Service can be used via a NuGet package and there's full documentation and samples on GitHub to get you started:
Example of using Dispatcher Service
Example of using Reliable Dispatcher
No, currently there are no events you can use for ReliableQueue. You have to poll for new items.

Azure Worker Role to process jobs Asynchronously

I am trying to implement the following Use Case. I have an Azure Worker Role that will monitor the Azure Storage Queue, and when a message comes in, this will trigger a job to run Asynchronously. I want to use the TPL if possible, and need the operations to support cancellation, so that when the Azure Role OnStop fires, jobs can exit gracefully if possible. The MyFixIt example posted by Scott Guthrie is almost exactly what I need, and I have used this as the template for my project. The one critical aspect not supported is the requirement to run the jobs asynchronously. In the FixIt code, once a job is launched, no other jobs will process until that one finishes. Some of the jobs my application will process are long running, and I need the worker role to be able to notice other incoming jobs and run those while the long running job is running.
The 2 key methods here are ProcessMessagesAsync, which monitors the queue, and ProcessMessage, which will run the job when a message comes in. Here is what I have, and it mostly works except it does not handle the CancellationRequest properly, and the Azure Worker Role will shut down without waiting for jobs to complete.
/// <summary>
/// Continuous loop that monitors the queue and launches jobs when they are retrieved.
/// </summary>
/// <param name="token"></param>
/// <returns></returns>
public virtual async Task ProcessMessagesAsync(CancellationToken token)
{
CloudQueue queue = _queueClient.GetQueueReference(_queueName);
await queue.CreateIfNotExistsAsync(token);
while (!token.IsCancellationRequested)
{
Debug.WriteLine("inLoop");
// The default timeout is 90 seconds, so we won’t continuously poll the queue if there are no messages.
// Pass in a cancellation token, because the operation can be long-running.
CloudQueueMessage message = await queue.GetMessageAsync(token);
if (message != null)
{
ProcessMessage(message, queue, token);
}
else
{
await Task.Delay(500, token);
}
}
}
protected virtual async Task ProcessMessage(CloudQueueMessage message, CloudQueue queue, CancellationToken token)
{
var jobDetails = JobDetails.DeserializeJson(message.AsString);
var result = await _jobRunner.RunJob(jobDetails, token);
//todo handle error
//if (result.Status == JobStatus.Error)
await queue.DeleteMessageAsync(message);
}
Then the JobRunner runs the job requested. I have written a TestJob in which I am trying to simulate a long running job that can notice the CancellationRequest, and after a short cleanup period, exit the job early.
public virtual async Task<JobResult> RunJob(JobDetails jobDetails, CancellationToken token)
{
switch (jobDetails.JobName.ToLower())
{
case "testjob":
return await TestJob(jobDetails.Args, token);
}
return new JobResult(JobStatus.Error) { ErrorMessage = "The job requested does not exist." };
}
protected virtual async Task<JobResult> TestJob(List<string> jobArgs, CancellationToken token)
{
var message = "no message";
if (jobArgs != null && jobArgs.Any())
message = jobArgs[0];
return await Task.Run(async () =>
{
Debug.WriteLine(string.Format("Start:{0}", message));
for (int i = 1; i <= 800; i++)
{
if (token.IsCancellationRequested)
{
Debug.WriteLine("CancelationRequest in TestJob");
//simulate short time to cleanup and exit early
Thread.Sleep(1500);
Debug.WriteLine("Cancelation Job Cleanup finsihed.");
token.ThrowIfCancellationRequested();
}
Thread.Sleep(10);
}
Debug.WriteLine(string.Format("Finish:{0}", message));
return new JobResult(JobStatus.Success);
});
}
I have been searching and researching for 2 days now, including the TPL DataFlow library, and have not yet been able to come up with a way to make this work properly. I feel like the Call to ProcessMessage(message, queue, token) is not being done correctly, there even is a compiler warning 'Because this call is not awaited...'. But I DON'T want to await (which is what the FixIt example does), because then no other jobs get noticed until the running one is finished. This seems like it would not be an uncommon use case, though I cannot seem to find anyone describing it.
Thank you in advance for any help!
Danny Green
The reason this is happening is because you are not honouring the task returned from ProcessMessage. Because of this ProcessMessageAsync can finish before ProcessMessage gracefully completes or cancels. Keeping in mind that you don't want to await ProcessMessage because it will make message processing sequential, I would suggest that you keep a list of running tasks.
In other words, create a List in ProcessMessageAsync and add the task returned from ProcessMessage to this list. Then at the end of while loop you should loop through this list to cancel all pending tasks if token was cancelled.
Sorry I don't have VS handy but I hope you get the point.
Thank you Sanjay, Based on your suggestion I have come up with the following.
/// <summary>
/// Continuous loop that monitors the queue and launches jobs when they are retrieved.
/// </summary>
/// <param name="token"></param>
/// <returns></returns>
public virtual async Task ProcessMessagesAsync(CancellationToken token)
{
CloudQueue queue = _queueClient.GetQueueReference(_queueName);
await queue.CreateIfNotExistsAsync(token);
var runningTasks = new ConcurrentDictionary<int, Task>();
while (!token.IsCancellationRequested)
{
Debug.WriteLine("inLoop");
// The default timeout is 90 seconds, so we won’t continuously poll the queue if there are no messages.
// Pass in a cancellation token, because the operation can be long-running.
CloudQueueMessage message = await queue.GetMessageAsync(token);
if (message != null)
{
var t = ProcessMessage(message, queue, token);
var c = t.ContinueWith(z => RemoveRunningTask(t.Id, runningTasks));
while (true)
{
if (runningTasks.TryAdd(t.Id, t))
break;
Task.Delay(25);
}
}
else
{
try
{
await Task.Delay(500, token);
}
catch (Exception ex)
{
Debug.WriteLine(ex.Message);
}
}
}
while (!runningTasks.IsEmpty)
{
Debug.WriteLine("Waiting for running tasks");
Task.Delay(500);
}
}
private static void RemoveRunningTask(int id, ConcurrentDictionary<int, Task> runningTasks)
{
while (true)
{
Task outTask;
if (runningTasks.TryRemove(id, out outTask))
break;
Task.Delay(25);
}
}
This seems to work, though I feel it is a little clumsy. I started out coding the 'ContinueWith' like this, but was surprised that the incoming task had a different Id value (I expected it to be the same Task):
var task = ProcessMessage(message, queue, token).ContinueWith(x =>
{
while (true)
{
Task outTask;
if (runningTasks.TryRemove(x.Id, out outTask))
break;
Task.Delay(25);
}
});
UPDATE:
It turns out that this still does not quite work, I somehow misread the results when testing earlier. Based on the MyFixIt example, in the Work Role OnStop I have the following code:
public override void OnStop()
{
Debug.WriteLine("OnStop_Begin");
tokenSource.Cancel();
tokenSource.Token.WaitHandle.WaitOne();
base.OnStop();
Debug.WriteLine("Onstop_End");
tokenSource.Dispose();
}
It appears that the tokenSource.Token.WaitHandle.WaitOne isn't really able to wait until all of the tasks that have a reference to the token have finished, so the role continues and stops even when tasks are still in the processing of finishing up. Is there some way to properly use the token to signal when the cancellation is actually completed?
Thanks!
UPDATE 2
Ok, I think I have a solution that is now working. It appears that the CancellationToken.WaitHandle is signaled when the .Cancel is called, so I'm not sure what the purpose of having it immediately after the .Cancel is called, it seems like it would always just continue immediately through that code? This is how it is in the FixIt example, but I don't really understand it. For my purpose, I have changed ProcessMessagesAsync to now get passed in a ManualResetEventSlim, and then set that after all tasks have finished. Then in OnStop I wait on that before finishing the Stop.
/// <summary>
/// Continuous loop that monitors the queue and launches jobs when they are retrieved.
/// </summary>
/// <param name="token"></param>
/// <returns></returns>
public virtual async Task ProcessMessagesAsync(CancellationToken token, ManualResetEventSlim reset)
{
CloudQueue queue = _queueClient.GetQueueReference(_queueName);
await queue.CreateIfNotExistsAsync(token);
var runningTasks = new ConcurrentDictionary<int, Task>();
while (!token.IsCancellationRequested)
{
Debug.WriteLine("inLoop");
// The default timeout is 90 seconds, so we won’t continuously poll the queue if there are no messages.
// Pass in a cancellation token, because the operation can be long-running.
CloudQueueMessage message = await queue.GetMessageAsync(token);
if (message != null)
{
var t = ProcessMessage(message, queue, token);
var c = t.ContinueWith(z => RemoveRunningTask(t.Id, runningTasks));
while (true)
{
if (runningTasks.TryAdd(t.Id, t))
break;
await Task.Delay(25);
}
}
else
{
try
{
await Task.Delay(500, token);
}
catch (Exception ex)
{
Debug.WriteLine(ex.Message);
}
}
}
while (!runningTasks.IsEmpty)
{
Debug.WriteLine("Waiting for running tasks");
await Task.Delay(500);
}
Debug.WriteLine("All tasks have finished, exiting ProcessMessagesAsync.");
reset.Set();
}
public override void OnStop()
{
Debug.WriteLine("OnStop_Begin");
tokenSource.Cancel();
tokenSource.Token.WaitHandle.WaitOne();
_reset.Wait();
base.OnStop();
Debug.WriteLine("Onstop_End");
tokenSource.Dispose();
}

Categories

Resources