Search code examples
schedulingquartz.netshutdown

In Quartz, how do I notify a job that the scheduler is shutting down?


I am using Quartz.Net to schedule some long running jobs in a Windows service. I am trying to stop the service gracefully, such as when preparing to reboot. I need to notify the jobs so that they can decide whether to finish or abort based on how close they are to finishing. I have looked at interruptions as well as listeners, but I can't seem to figure out how to communicate the pending shutdown.

I have also tried breaking the long jobs into smaller, sequential jobs, but there is a serious performance hit when I do this.


Solution

  • I don't think a job instance can determine on its own that the scheduler is shutting down. Some coordination is needed between the job (worker) thread and the service control thread.

    Here's an example interruptable job class. Note the use of ManualResetEventSlim to signal that an interruption has been requested, and to signal that the Execute() method has exited accordingly.

    Note: this code is built on the Quartz.Server.2010 sample project code that ships with the source download.

    /// <summary></summary>
    [DisallowConcurrentExecution]
    public class DumbInterruptableJob : IInterruptableJob, IDisposable
    {
        private static readonly ILog logger = LogManager.GetLogger(typeof(DumbInterruptableJob));
    
        private ManualResetEventSlim _interruptRequestToken;
        private ManualResetEventSlim _interruptCompleteToken;
    
        /// <summary></summary>
        public DumbInterruptableJob()
        {
            _interruptRequestToken = new ManualResetEventSlim(false);
            _interruptCompleteToken = new ManualResetEventSlim(false);
        }
    
        /// <summary></summary>
        /// <param name="context"></param>
        public void Execute(IJobExecutionContext context)
        {
            try
            {
                JobKey key = context.JobDetail.Key;
    
                logger.Info(m => m("Instance {0} of DumbInterruptableJob is working.", key));
    
                // The work loop
                for (int i = 0; i < 4; i++)
                {
                    if (_interruptRequestToken.IsSet)
                    {
                        logger.Info(m => m("Work interrupt requested...Exiting."));  // Breakpoint #1
                        return;
                    }
    
                    logger.Info(m => m("Work..."));
                    Thread.Sleep(2000);
                }
    
                logger.Info(m => m("Work complete!"));
            }
            catch (Exception ex)
            {
                logger.Error(m => m(ex.Message));
            }
            finally
            {
                _interruptCompleteToken.Set();
            }
        }
    
        /// <summary></summary>
        public void Interrupt()
        {
            logger.Info(m => m("Setting interrupt flag..."));
            _interruptRequestToken.Set();
    
            logger.Info(m => m("Waiting for work thread to stop..."));
            _interruptCompleteToken.Wait();
            logger.Info(m => m("Work thread stopped."));  // Breakpoint #2
        }
    
        /// <summary></summary>
        public void Dispose()
        {
            _interruptCompleteToken.Dispose();
            _interruptRequestToken.Dispose();
        }
    }
    

    The next step is to interrupt (cancel) all running interruptable jobs when the service is stopping.

        public virtual void Stop()
        {
            try
            {
                // Calling Shutdown(false) stops the scheduler from starting new jobs,
                // but the method doesn't block, allowing us to access any running jobs
                // and attempt to cancel (interrupt) them.
                logger.Info(m => m("Shutting down the scheduler..."));
                scheduler.Shutdown(false);
    
                var interruptableJobs = new List<Task>();
    
                foreach (var ctx in scheduler.GetCurrentlyExecutingJobs())
                {
                    if (ctx.JobInstance is IInterruptableJob)
                    {
                        interruptableJobs.Add(Task.Factory.StartNew(() =>
                        {
                            logger.Info(m => m("Waiting for interruptable job {0} to stop...", ctx.JobDetail.Key));
                            scheduler.Interrupt(ctx.JobDetail.Key);
                            logger.Info(m => m("Interruptable job {0} has stopped.", ctx.JobDetail.Key));
                        }));
                    }
                }
    
                if (interruptableJobs.Count > 0)
                {
                    logger.Info(m => m("Waiting for all interruptable jobs to stop..."));
                    Task.WaitAll(interruptableJobs.ToArray());
                    logger.Info(m => m("All interruptable jobs have stopped."));
                }
    
                logger.Info(m => m("Waiting for all running jobs to complete..."));
                scheduler.Shutdown(true);
                logger.Info(m => m("All running jobs have completed. Scheduler shutdown complete."));  // Breakpoint #3
            }
            catch (Exception ex)
            {
                logger.Error(string.Format("Scheduler stop failed: {0}", ex.Message), ex);
                throw;
            }
    
            logger.Info("Scheduler shutdown complete");
        }
    

    To see the synchronization in action, set breakpoints where indicated in comments, run it, then shutdown the service (Ctrl-C if running from command line), and watch the order that the breakpoints are hit.