How to create task on startup and stop it on application stop? - c#

I'm using mvc with .net core, I need to run a task on start, and stop it when application stops. In Startup.cs I registered events for application start and stop. The problem is, I don't know how to run a task that has to be run in a specific class from startup. The task looks like this:
public void PreventStatusChange()
{
while (forceStatusChange)
{
foreach (var ext in GetExtensions())
{
ext.Status = StatusType.Available;
}
Thread.Sleep(1000);
}
}
Variable forceStatusChange is declared in the same class, so I don't see it from my Startup.cs. What is the best way to do it?

You need to create a class that implements IHostedService. This interface defines just two methods, StartAsync which is called when the application starts and StopAsync which is called when it terminates.
You need to register it as a hosted service with :
services.AddHostedService<TimedHostedService>();
Be careful to use AddHostedService, NOT AddSingleton. If you use AddSingleton the runtime won't know to call StartAsync and StopAsync when appropriate.
The article Background tasks with hosted services in ASP.NET Core shows how to implement a service using a timer :
internal class TimedHostedService : IHostedService, IDisposable
{
private readonly ILogger _logger;
private Timer _timer;
public TimedHostedService(ILogger<TimedHostedService> logger)
{
_logger = logger;
}
public Task StartAsync(CancellationToken cancellationToken)
{
_logger.LogInformation("Timed Background Service is starting.");
_timer = new Timer(DoWork, null, TimeSpan.Zero,
TimeSpan.FromSeconds(10));
return Task.CompletedTask;
}
private void DoWork(object state)
{
_logger.LogInformation("Timed Background Service is working.");
}
public Task StopAsync(CancellationToken cancellationToken)
{
_logger.LogInformation("Timed Background Service is stopping.");
_timer?.Change(Timeout.Infinite, 0);
return Task.CompletedTask;
}
public void Dispose()
{
_timer?.Dispose();
}
}
There's nothing particularly interesting in this code - just start a timer when StartAsync is called and stop it on StopAsync
Cancelling long-running tasks
When the runtime needs to recycle or stop, it will call the StopAsync method on all hosted services, wait a while for them to finish gracefully and then warn them to cancel immediatelly. After a short while it will go ahead and terminate the app or recycle it.
The cancellationToken parameter is used to signal that the service should stop immediatelly. Typically, that means that you'd have to write your own code to check it, warn your own task to terminate, wait for all of them to finish etc, similar to the code shown in this article
This is pretty much boilerplate though, which is why the BackgroundService class can be used to create a class that only needs to implement ExecuteAsync(CancellationToken). Starting and stopping that task is provided by BackgroundService, eg :
public class PollingService : BackgroundService
{
private readonly ILogger _logger;
public PollingService(ILogger<PollingService> logger)
{
_logger = logger;
}
protected async override Task ExecuteAsync(
CancellationToken cancellationToken)
{
while (!cancellationToken.IsCancellationRequested)
{
try
{
await DoSomething(cancellationToken);
await Task.Delay(1000,cancellationToken);
}
catch (Exception ex)
{
_logger.LogError(ex,
$"Error occurred executing {nameof(workItem)}.");
}
}
_logger.LogInformation("Queued Hosted Service is stopping.");
}
}
In this case Task.Delay() itself will be cancelled as soon as the runtime raises the cancellation token. DoSomething() itself should be implemented in a way that checks the cancellation token, eg pass it to any asynchronous method that accepts it as a parameter, test the IsCancellationRequested property on every loop and exit it it's raised.
For example :
protected async override Task ExecuteAsync(
CancellationToken cancellationToken)
{
while (!cancellationToken.IsCancellationRequested)
{
try
{
foreach (var ext in GetExtensions())
{
//Oops, time to cancel
if(cancellationToken.IsCancellationRequested)
{
break;
}
//Otherwise, keep working
ext.Status = StatusType.Available;
}
await Task.Delay(1000,cancellationToken);
}
catch (Exception ex)
{
...
}
}
_logger.LogInformation("Hosted Service is stopping.");
}

You can use BackgroundService
public class LongRunningService : BackgroundService
{
public LongRunningService()
{
}
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
{
while (!stoppingToken.IsCancellationRequested && forceStatusChange)
{
foreach (var ext in GetExtensions())
{
ext.Status = StatusType.Available;
}
await Task.Delay(1000, stoppingToken);
}
}
protected override async Task StopAsync (CancellationToken stoppingToken)
{
// Run your graceful clean-up actions
}
}
And register it:
public void ConfigureServices(IServiceCollection services)
{
...
services.AddSingleton<IHostedService, LongRunningService>();
...
}

Related

How to avoid dispose of context in async call in .NET core

I'm having some slow calls in my api so in order not to block my UI, I implemented a background worker service following this tutorial. Inside my _backgroundWorkerQueue I have
_backgroundWorkerQueue.QueueBackgroundWorkItem(async token =>
{
await client.ExecuteAsync(request, CancellationToken.None);
await _projectRepository.Update(id, "Update", "unlock");
});
The second line, await _projectRepository.Update, throws me an error that the context has been disposed and the update fails. I set my service to transient and my context as transient in order to test it out this way but I still got the same error. Any thoughts and ideas of how can I avoid and fix this, if possible, without usage of other libraries as Hangfire etc.
First, you don't need to create a new class for this. The Channel class already does far more than this BackgroundWorkerQueue.
As for the specific problem, it's described in the BackgroundService docs Background tasks with hosted services in ASP.NET Core in the section Consuming a scoped service in a background task. The problem is that a hosted service is essentially a singleton, not transient. It's created when the application starts and disposed when it stops. The service itself is registered as a transient, but since its owner is the application host itself, it acts as a singleton.
The solution shown in the docs is to inject IServiceProvider into your service and use it to construct a new scope and new services from it as needed
To use scoped services within a BackgroundService, create a scope. No scope is created for a hosted service by default.
public class ConsumeScopedServiceHostedService : BackgroundService
{
private readonly ILogger<ConsumeScopedServiceHostedService> _logger;
public ConsumeScopedServiceHostedService(IServiceProvider services,
ILogger<ConsumeScopedServiceHostedService> logger)
{
Services = services;
_logger = logger;
}
public IServiceProvider Services { get; }
...
private async Task DoWork(CancellationToken stoppingToken)
{
_logger.LogInformation(
"Consume Scoped Service Hosted Service is working.");
using (var scope = Services.CreateScope())
{
var scopedProcessingService =
scope.ServiceProvider
.GetRequiredService<IScopedProcessingService>();
await scopedProcessingService.DoWork(stoppingToken);
}
}
The very next section Queued Background Tasks addresses exactly the scenario you described, with a Channel-based Func<CancellationToken, ValueTask> queue
History note: the docs uses QueueBackgroundWorkItem in the past. When the sample code changed, some of the docs remained the same. The QueueBackgroundWorkItemAsync method should probably be named just EnqueueAsync. The sample does abstract the underlying queue implementation though
public interface IBackgroundTaskQueue
{
ValueTask QueueBackgroundWorkItemAsync(Func<CancellationToken, ValueTask> workItem);
ValueTask<Func<CancellationToken, ValueTask>> DequeueAsync(
CancellationToken cancellationToken);
}
public class BackgroundTaskQueue : IBackgroundTaskQueue
{
private readonly Channel<Func<CancellationToken, ValueTask>> _queue;
public BackgroundTaskQueue(int capacity)
{
// Capacity should be set based on the expected application load and
// number of concurrent threads accessing the queue.
// BoundedChannelFullMode.Wait will cause calls to WriteAsync() to return a task,
// which completes only when space became available. This leads to backpressure,
// in case too many publishers/calls start accumulating.
var options = new BoundedChannelOptions(capacity)
{
FullMode = BoundedChannelFullMode.Wait
};
_queue = Channel.CreateBounded<Func<CancellationToken, ValueTask>>(options);
}
public async ValueTask QueueBackgroundWorkItemAsync(
Func<CancellationToken, ValueTask> workItem)
{
if (workItem == null)
{
throw new ArgumentNullException(nameof(workItem));
}
await _queue.Writer.WriteAsync(workItem);
}
public async ValueTask<Func<CancellationToken, ValueTask>> DequeueAsync(
CancellationToken cancellationToken)
{
var workItem = await _queue.Reader.ReadAsync(cancellationToken);
return workItem;
}
}
The IBackgroundTaskQueue service should be injected into producers and the BackgroundService class. Producers will use QueueBackgroundWorkItemAsync to post work and the service will use DequeueAsync to receive tasks:
public class QueuedHostedService : BackgroundService
{
private readonly ILogger<QueuedHostedService> _logger;
public QueuedHostedService(IBackgroundTaskQueue taskQueue,
ILogger<QueuedHostedService> logger)
{
TaskQueue = taskQueue;
_logger = logger;
}
public IBackgroundTaskQueue TaskQueue { get; }
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
{
_logger.LogInformation(
$"Queued Hosted Service is running.{Environment.NewLine}" +
$"{Environment.NewLine}Tap W to add a work item to the " +
$"background queue.{Environment.NewLine}");
await BackgroundProcessing(stoppingToken);
}
private async Task BackgroundProcessing(CancellationToken stoppingToken)
{
while (!stoppingToken.IsCancellationRequested)
{
var workItem =
await TaskQueue.DequeueAsync(stoppingToken);
try
{
await workItem(stoppingToken);
}
catch (Exception ex)
{
_logger.LogError(ex,
"Error occurred executing {WorkItem}.", nameof(workItem));
}
}
}
public override async Task StopAsync(CancellationToken stoppingToken)
{
_logger.LogInformation("Queued Hosted Service is stopping.");
await base.StopAsync(stoppingToken);
}
}
The sample code can be simplified by returning a ChannelReader<Func<CancellationToken, ValueTask>> or an IAsyncEnumerable<Func<CancellationToken, ValueTask>>. Using IAsyncEnumerable<>` hides the internal implementation just like the original example did:
public interface IBackgroundTaskQueue
{
ValueTask QueueBackgroundWorkItemAsync(Func<CancellationToken, ValueTask> workItem);
ValueTask<Func<CancellationToken, ValueTask>> DequeueAsync(
CancellationToken cancellationToken);
IAsyncEnumerable<Func<CancellationToken, ValueTask>> ReadAllAsync(
CancellationToken cancellationToken);
}
public class BackgroundTaskQueue : IBackgroundTaskQueue
{
...
public IAsyncEnumerable<Func<CancellationToken, ValueTask>> ReadAllAsync(
CancellationToken cancellationToken)
{
return _queue.Reader.ReadAllAsync(cancellationToken);
}
}
This allows using await foreach in the BackgroundProcessing method:
private async Task BackgroundProcessing(CancellationToken stoppingToken)
{
await foreach(var workItem in TaskQueue.ReadAllAsync(stoppingToken)
{
try
{
await workItem(stoppingToken);
}
catch (Exception ex)
{
_logger.LogError(ex,
"Error occurred executing {WorkItem}.", nameof(workItem));
}
}
}
Combined with our scoped service, and assuming the queue accepts Func<IProductRepository,CancellationToken, ValueTask> :
private async Task BackgroundProcessing(CancellationToken stoppingToken)
{
await foreach(var workItem in TaskQueue.ReadAllAsync(stoppingToken)
{
try
{
using (var scope = Services.CreateScope())
{
var repo = scope.ServiceProvider
.GetRequiredService<IProjectRepository>();
await workItem(repo,stoppingToken);
}
}
catch (Exception ex)
{
_logger.LogError(ex,
"Error occurred executing {WorkItem}.", nameof(workItem));
}
}
}

BackgroundService Graceful Shutdown - Complete work and write to DB

I have a Background Worker implementing the BackgroundService (provided by MS).
See this simple implementation:
public class MyService : BackgroundService {
private readonly MyDbContext _context;
public MyService(MyDbContext context) {
//...
}
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
{
try {
while (true)
{
stoppingToken.ThrowIfCancellationRequested();
// Do some work
}
} catch(OperationCancelledException) {
_context.Add(new MyLogMessage(){ Error = "MyService cancelled!" });
_context.SaveChanges();
}
// ...
}
}
When the graceful shutdown (in console: CTRL+C) is requested the catch block is triggered, and also the SaveChanges() seems to be executed. But, sometimes the error is stored into the database and the most of the time it is not. Also the EntityFramework is printing an insert statement on the console, but the log is not in the db.
I assume that the shutdown is happening faster then writting the data to the DB?
Can anyone give me a hint how to handle this situation and store the error into the database?
It seems like the stoppingToken isn't cancelled as expected when the application shuts down. I managed to get around this using IHostApplicationLifetime and a new field where I can store if a shutdown is in progress.
public class TestService : BackgroundService {
private readonly IHostApplicationLifetime _lifetime;
private readonly ILogger<TestService> _logger;
private bool _shutownRequested;
public TestService(IHostApplicationLifetime lifetime, ILogger<TestService> logger) {
_lifetime = lifetime;
_logger = logger;
}
public override Task StartAsync(CancellationToken cancellationToken) {
_lifetime.ApplicationStopping.Register(OnShutdown);
return Task.CompletedTask;
}
private void OnShutdown() {
_shutdownRequested = true;
}
protected override async Task ExecuteAsync(CancellationToken stoppingToken) {
try {
while(true) {
stoppingToken.ThrowIfCancellationRequested();
if(_shutdownRequested) {
throw new OperationCanceledException();
}
await Task.Delay(100, CancellationToken.None);
}
} catch(OperationCanceledException) {
_logger.LogWarning("TestService canceled");
}
}
}
Now it might be better to now throw a new exception there, but as an example it will do.
The reason why the log entry doesn't appear in the database is that the host shutdown period is lower than what it takes to process a task in a while loop and send a log to the database. The default timeout is 5 seconds.
What you could do, is to increase the timeout to a larger value, for example a minute a two:
services.Configure<HostOptions>(
opts => opts.ShutdownTimeout = TimeSpan.FromMinutes(2));
Make sure to let enough time for a service to finish the iteration inside a while loop and log the message.
Please check Extending the shutdown timeout setting to ensure graceful IHostedService shutdown for more details.

How to stop windows service when trhow exception? NET CORE

I have an example for Windows service in .NET core 3.1.
I am generating an intentional error to see its behavior, my idea is that when the exception occurs I want to call the StopAsync method to stop the service, however once the StopAsync is executed the ExecuteAsync method is executed again and the service does not stop, entering like this in an infinite loop.
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
{
await Task.CompletedTask;
while (!stoppingToken.IsCancellationRequested)
{
try
{
int num = 233;
int result = num / 0;
//todo
}
catch (Exception ex)
{
_logger.LogError(ex.Message);
await StopAsync(stoppingToken); //this execute, but it keeps running this ExecuteAsync method
}
}
}
public override async Task StopAsync(CancellationToken cancellationToken)
{
_logger.LogInformation("stop service");
await base.StopAsync(cancellationToken);
}
the ExecuteAsync method is executed again and the service does not stop, entering like this in an infinite loop.
Are you sure about that? StopAsync will cancel the token provided to ExecuteAsync, and it looks like your ExecuteAsync method will complete in that case.
The host application itself will continue running, though; perhaps that's what you're seeing. To shut down the host application when the background service exits, you need to explicitly stop the application:
private readonly IHostApplicationLifetime _hostApplicationLifetime;
public MyBackgroundService(IHostApplicationLifetime hostApplicationLifetime) =>
_hostApplicationLifetime = hostApplicationLifetime;
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
{
try
{
...
}
finally
{
_hostApplicationLifetime.StopApplication();
}
}

Migration to ASP CORE 3: how to migrate backround services? (service added through AddHostedService freezes web application startup)

After migration to ASP Core3 this line freezes the web app startup process (with VS debug: browser had pop up but the page loading never ends)
serviceCollection.AddHostedService<BackgroundService>();
It works in Core 2.
I can't see any breaking changes related to AddHostedService in ASP Core3 documentation:
https://learn.microsoft.com/en-us/aspnet/core/migration/22-to-30?view=aspnetcore-3.0&tabs=visual-studio
It seems like blocking background server StartAsync blocks the web app startup. May be something should be configured in WebHost to make StartAsync async again?
The background service code looks like:
public class MyBackgroundService : IHostedService
{
private readonly BackgroundServiceHandle backgroundServiceHandle;
private CancellationTokenSource tokenSource;
public MyBackgroundService (BackgroundServiceHandle backgroundServiceHandle) =>
this.backgroundServiceHandle = backgroundServiceHandle;
public async Task StartAsync(CancellationToken cancellationToken)
{
tokenSource = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken);
while (cancellationToken.IsCancellationRequested == false)
{
try
{
await Task.Delay(TimeSpan.FromSeconds(3), cancellationToken).ConfigureAwait(false);
// IMPORTANT: it seems that next line blocks the web app startup, but why this works in CORE 2?
var taskSettings = backgroundServiceHandle.Dequeue(tokenSource.Token);
// the work
}
catch (OperationCanceledException)
{
// execution cancelled
}
}
}
public Task StopAsync(CancellationToken cancellationToken)
{
tokenSource.Cancel();
return Task.CompletedTask;
}
}
public sealed class BackgroundServiceHandle : IDisposable
{
private readonly BlockingCollection<TaskSettings> blockingCollection;
public BackgroundServiceHandle() => blockingCollection = new BlockingCollection<TaskSettings>();
public void Enqueue(TaskSettings settings) => blockingCollection.Add(settings);
public TaskSettings Dequeue(CancellationToken token) => blockingCollection.Take(token);
public void Dispose()
{
blockingCollection.Dispose();
}
}
Moving base class from IHostedService to BackgroundService and moving StartAsync to
protected override async Task ExecuteAsync(CancellationToken cancellationToken) solves the problem

.Net core IHostedService Background task exception will not terminate application

I have a program that needs to terminate when an IHostedService background task encounters a certain scenario. I was hoping to do this by just throwing an exception in the background task that would get kicked up to the main function. I could then trigger the cancellation token to kill other background tasks.
My problem is that when I throw the exception, it kills the task and that's all. Everything else keeps running. Is there a way to do this, or a better way to do what I'm trying to do? Is there another way to have a backgrounds task trigger the common CancellationToken?
I included a simplified version of my issue in the code below.
If I comment out the await Task.Delay(TimeSpan.FromSeconds(1), stoppingToken); line, the exception does what I want and I can trigger the CancellationToken. When it is in place, the task stops, but the program does not.
NOTE: In my messier code I have more IHostedServices running, which is why I'm trying to trigger cancelSource.Cancel()
public class Program
{
public static async Task Main(string[] args)
{
using (var cancelSource = new CancellationTokenSource())
{
try
{
await new HostBuilder()
.ConfigureServices((hostContext, services) =>
{
services.AddHostedService<TestService>();
})
.Build()
.RunAsync(cancelSource.Token);
}
catch (Exception E)
{
cancelSource.Cancel();
}
}
}
}
public class TestService : BackgroundService
{
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
{
while (!stoppingToken.IsCancellationRequested)
{
await Task.Delay(TimeSpan.FromSeconds(1), stoppingToken);
Console.WriteLine("loop 1");
throw new ApplicationException("OOPS!!");
}
}
}
Commenting the only line in the ExecuteAsync method with await operator makes your code run synchronously. If we look at the sources of BackgroundService.StartAsync, we can see that it checks for _executingTask.IsCompleted and it returns task that will contain your exception in case we don't have any await in ExecuteAsync method, otherwise it will return Task.CompletedTask and you won't be able to catch this exception from ExecuteAsync in Main method.
You can manage your services with IApplicationLifetime that can be injected in all your background services. For example, you can catch exception within ExecuteMethod and call ApplicationLifetime.StopApplication.
Example:
static async Task Main(string[] args)
{
await new HostBuilder()
.ConfigureServices((hostContext, services) =>
{
services.AddHostedService<TestService>();
services.AddHostedService<TestService2>();
})
.Build()
.RunAsync();
Console.WriteLine("App stoped");
}
Service 1
public class TestService : BackgroundService
{
private readonly IApplicationLifetime _applicationLifetime;
public TestService(IApplicationLifetime applicationLifetime)
{
_applicationLifetime = applicationLifetime;
}
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
{
try
{
while (!_applicationLifetime.ApplicationStopping.IsCancellationRequested)
{
await Task.Delay(TimeSpan.FromSeconds(1), _applicationLifetime.ApplicationStopping);
Console.WriteLine("running service 1");
throw new ApplicationException("OOPS!!");
}
}
catch (ApplicationException)
{
_applicationLifetime.StopApplication();
}
}
}
Service 2
public class TestService2 : BackgroundService
{
private readonly IApplicationLifetime _applicationLifetime;
public TestService2(IApplicationLifetime applicationLifetime)
{
_applicationLifetime = applicationLifetime;
}
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
{
try
{
while (!_applicationLifetime.ApplicationStopping.IsCancellationRequested)
{
await Task.Delay(100, _applicationLifetime.ApplicationStopping);
Console.WriteLine("running service 2");
}
}
catch (ApplicationException)
{
_applicationLifetime.StopApplication();
}
}
}
Output:
running service 2
running service 2
running service 1
App stoped

Categories

Resources