I have a running quartz job and terminating my BackgroundService
, for some reason despite calling scheduler.Shutdown(true)
the job remains running.
Even when looping through and interrupting jobs, the program closes if before the threads exit.
Aside from my code below, would i be looking at writing a custom IScheduler to ensure runnings jobs are halted on shutdown?
This is my IJob
Execute method:
public async Task Execute(IJobExecutionContext context)
{
var cancellationToken = context.CancellationToken;
while (cancellationToken.IsCancellationRequested == false)
{
// Extension method so we catch TaskCancelled exceptions.
await TaskDelay.Wait(1000, cancellationToken);
Console.WriteLine("keep rollin, rollin, rollin...");
}
Console.WriteLine("Cleaning up.");
await Task.Delay(1000);
Console.WriteLine("Really going now.");
}
This is my shutdown loop (calling shutdown directly doesn't interrupt any running jobs):
internal class QuartzHostedService : IHostedService
{
// These are set by snipped constructor.
private readonly IJobSettings jobSettings;
private readonly ILogger logger;
private readonly IScheduler scheduler;
private async Task AddJobsToScheduler(CancellationToken cancellationToken = default)
{
var schedule = SchedulerBuilder.Create();
var downloadJob = JobBuilder.Create<StreamTickersJob>().Build();
var downloadJobTrigger = TriggerBuilder
.Create()
.ForJob(downloadJob)
.WithDailyTimeIntervalSchedule(
x => x.InTimeZone(serviceTimeZone)
.OnEveryDay()
.StartingDailyAt(new TimeOfDay(8,0))
.EndingDailyAt(new TimeOfDay(9,0)))
.Build();
await this.scheduler.ScheduleJob(downloadJob, downloadJobTrigger, cancellationToken);
}
public QuartzHostedService(IJobSettings jobSettings, IScheduler scheduler, ILogger<QuartzHostedService> logger)
{
this.jobSettings = jobSettings;
this.scheduler = scheduler;
this.logger = logger;
}
public async Task StartAsync(CancellationToken cancellationToken)
{
this.logger.LogInformation("Quartz started...");
await AddJobsToScheduler(cancellationToken);
await this.scheduler.Start(cancellationToken);
}
public async Task StopAsync(CancellationToken cancellationToken)
{
await this.scheduler.PauseAll(cancellationToken);
foreach (var job in await this.scheduler.GetCurrentlyExecutingJobs(cancellationToken))
{
this.logger.LogInformation($"Interrupting job {job.JobDetail}");
await this.scheduler.Interrupt(job.JobDetail.Key, cancellationToken);
}
await this.scheduler.Shutdown(cancellationToken);
}
}
I can confirm IHost
is not killing my app abruptly (at least not for a couple of seconds test pause) as I set a breakpoint at the end of the main program as below:
public static void Main(string[] args)
{
// Wrap IHost in using statement to ensure disposal within scope.
using (var host = CreateHostBuilder(args)
.UseSerilog<Settings>(Settings.Name)
.UseConsoleLifetime()
.Build()
.UseSimpleInjector(container))
{
// Makes no difference if I shutdown jobs here.
// var lifetime = container.GetInstance<IHostApplicationLifetime>();
// lifetime.ApplicationStarted.Register(async () => { });
// lifetime.ApplicationStopping.Register(async () => { });
var logger = container.GetInstance<ILogger<Program>>();
try
{
host.Run();
}
catch (Exception ex)
{
logger.LogCritical(ex, ex.Message);
}
// We reach here, whilst Jobs are still running :(
logger.LogDebug($"Finish {nameof(Main)}().");
}
}
I have also added from what I have found online the below, but still it doenst wait on shutdown:
var props = new NameValueCollection
{
{"quartz.scheduler.interruptJobsOnShutdownWithWait", "true"},
};
var scheduler = AsyncContext.Run(async () => await new StdSchedulerFactory(props).GetScheduler());
My workaround with a delay to allow jobs to terminate below works, but is so dodgy - kindly advise how I can get this working properly without an brittle arbitrary delay:
public async Task StopAsync(CancellationToken cancellationToken)
{
await this.scheduler.PauseAll(cancellationToken);
foreach (var job in await this.scheduler.GetCurrentlyExecutingJobs(cancellationToken))
{
this.logger.LogInformation($"Interrupting job {job.JobDetail}");
await this.scheduler.Interrupt(job.JobDetail.Key, cancellationToken);
}
await Task.Delay(3000);
await this.scheduler.Shutdown(cancellationToken);
}
BackgroundService
. You can find great article. Or try with more simple way, add graceful shutdown inStartup.cs
. Add dependecy in your methodConfigure
-IHostApplicationLifetime lifetime
. And registerApplicationStopping
event -lifetime.ApplicationStopping.Register(() => { Task.Delay(TimeSpan.FromSeconds(10)).GetAwaiter().GetResult(); });
I think 10 seconds will be enough for Quartz to stop all jobs. – EpicediumStopAllJobs
function but this seems to work at least per my use case and allow jobs to shutdown. – Struckawait Task.Delay(2000)
instead ofThread.Sleep(2000)
in async methods. – LikemindedThread.Sleep
if you callTask.Delay
in sync-over-async manner?.GetAwaiter().GetResult()
for not completed yetTask
kills its asynchronous nature. – LikemindedTask.Delay()
. Same issue the worker doesn't shutdown and isn't waited to terminate. – Struck