使用 Quartz 进行调度

Scheduling using Quartz

我们有一个检查远程计算机上文件是否存在的 .Net 项目。 我们需要每天在预定的时间对一个部门内的多台远程计算机(数千台)执行此操作。执行时间是在数据库中指定的,它经常变化,每台远程计算机的执行时间都会不同(或者其中一些可能相同)。 为此,我们计划使用 Quartz 调度程序。由于我们是 Quartz 的新手,我们想知道如何实现这一点。在高层次上,我们需要这些 -

  1. 调度程序应该在每天的特定时间开始 - Quartz 会这样做吗?
  2. 启动后,它应该从数据库中获取每台远程计算机的执行时间
  3. 准备一个 xml 包含所有远程计算机及其执行时间的列表
  4. 为每台远程计算机安排执行
  5. 在预定时间为每台远程计算机执行 .Net 项目

需要什么类型的 projects/components 才能实现上述目标?我们已经有一个检查远程计算机的 .net class lib 项目。我们如何与 Quartz 集成?


更新

非常感谢 granadaCoder

据我了解,有一项主要工作 运行 每天执行一次(基于 quartz.config),并通过从数据库中获取详细信息来安排其他工作。在这种情况下,控制台应用程序需要一直 运行ning... 您对编写控制台应用程序并使用任务计划程序将其安排在每天 运行 每天凌晨 12 点有何看法?

在控制台应用程序中,我们将准备一个(自定义)xml,其中包含作业列表(包含触发时间和作业所需数据等详细信息 class)并将其传递给一个调度程序模块(一个 class lib 项目),它将启动调度程序并排队 xml.

中的所有作业

安排完所有这些作业后,我们将等待(在调度程序模块内)来自所有作业的作业完成通知,然后关闭调度程序并退出控制台应用程序。这可能需要很长时间,具体取决于上次作业的触发时间。

让我知道您对这种方法的看法。

此外,我们有多个部门(总共 4 个),所以我正在考虑编写 4 个控制台应用程序 - 每个部门一个。并使用任务计划程序安排所有这些(同时不同的时间安排可能无济于事,因为每个部门可能都有触发时间跨越一整天的工作)。

或者,我也想知道是否可以在 quartz.config 文件中指定 4 个具有相同触发时间的作业? (不确定这将如何工作,它会创建 4 个特定于部门的调度程序实例并且我们可以将部门级作业排队到每个调度程序实例吗?)

  1. 编写一个在 .xml 中定义的作业,该作业会执行您的作业清理和(重新)调度。 (下面代码中的"ScheduleOtherJobsJob")

  2. ScheduleOtherJobsJob 将清除所有旧条目。它读取一些数据存储并计算出它必须执行的新作业列表。它会将这些作业添加到调度程序。

我写了一个基本的例子。我没有花哨的时间到 运行 基于确切日期的逻辑....我只是推向未来 WithIntervalInSeconds。

我正在使用 GROUP_NAME 找出要从调度程序中删除哪些作业......然后再重新添加它们。

我显示了 ScheduleOtherJobsJob 的 .xml。您可以将它添加到 AdoStore 或任何地方。 .xml 举个例子更简单。

请记住,您需要一个进程来保持调度程序 "alive"。也就是说,您不能向 IScheduler 添加新作业然后终止托管进程。在 Console.App 中,你会写一个 "Console.Readline()"...所以程序不会停止 运行ning.

关键点是……一个作业来安排其他作业。根据某些过滤器(此处为 GROUP_NAME)清除旧作业。基于某些数据存储重新添加作业......并确保主机进程保持 运行ning 以便所有新安排的作业将 运行.

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading;
using System.Threading.Tasks;

using Quartz;
using Quartz.Impl.Matchers;

namespace MyNamespace
{


    public class ScheduleOtherJobsJob : IJob
    {

        private const string GROUP_NAME = "MySpecialGroupName";

        /// <summary> 
        /// Called by the <see cref="IScheduler" /> when a
        /// <see cref="ITrigger" /> fires that is associated with
        /// the <see cref="IJob" />.
        /// </summary>
        public virtual void Execute(IJobExecutionContext context)
        {

            JobKey key = context.JobDetail.Key;

            JobDataMap jbDataMap = context.JobDetail.JobDataMap;

    //            string jobSays = jbDataMap.GetString("KeyOne");

            JobDataMap trgDataMap = context.Trigger.JobDataMap;

            string triggerParameter001 = trgDataMap.GetString("TriggerFileName");

            JobDataMap mergedMap = context.MergedJobDataMap;

            string whoWins = mergedMap.GetString("DefinedInJobDetailAndTriggerKey");

            string msg = string.Format("HasParametersJob : JobKey='{0}', jobSays='{1}', jobDetailParameter001='{2}', triggerParameter001='{3}', triggerParameter002='{4}' , whoWins='{5}' at '{6}'", key, jobSays, jobDetailParameter001, triggerParameter001, triggerParameter002, whoWins, DateTime.Now.ToLongTimeString());
            Console.WriteLine(msg);

            /* */
            context.Scheduler.UnscheduleJobs(GetAllJobTriggerKeys(context.Scheduler));

            /* Schedule Your Jobs  */
            List<OtherJobInfo> infos = new OtherJobInfoData().GetOtherJobs();
            foreach (OtherJobInfo info in infos)
            {
                ScheduleAHasParametersJob(context.Scheduler, info);
            }

        }


        private IList<TriggerKey> GetAllJobTriggerKeys(IScheduler scheduler)
        {
            /* Find all current jobs.....filter here if need be */

            IList<TriggerKey> returnItems = new List<TriggerKey>();

            IList<string> jobGroups = scheduler.GetJobGroupNames();
            //IList<string> triggerGroups = scheduler.GetTriggerGroupNames();

            IList<string> filteredJobGroups = jobGroups.Where(g => g.Equals(GROUP_NAME)).ToList();

            foreach (string group in filteredJobGroups)
            {
                var groupMatcher = GroupMatcher<JobKey>.GroupContains(group);
                var jobKeys = scheduler.GetJobKeys(groupMatcher);
                foreach (var jobKey in jobKeys)
                {
                    var detail = scheduler.GetJobDetail(jobKey);


                    var triggers = scheduler.GetTriggersOfJob(jobKey);
                    foreach (ITrigger trigger in triggers)
                    {
                        returnItems.Add(trigger.Key);

                        Console.WriteLine(group);
                        Console.WriteLine(jobKey.Name);
                        Console.WriteLine(detail.Description);
                        Console.WriteLine(trigger.Key.Name);
                        Console.WriteLine(trigger.Key.Group);
                        Console.WriteLine(trigger.GetType().Name);
                        Console.WriteLine(scheduler.GetTriggerState(trigger.Key));
                        DateTimeOffset? nextFireTime = trigger.GetNextFireTimeUtc();
                        if (nextFireTime.HasValue)
                        {
                            Console.WriteLine(nextFireTime.Value.LocalDateTime.ToString());
                        }

                        DateTimeOffset? previousFireTime = trigger.GetPreviousFireTimeUtc();
                        if (previousFireTime.HasValue)
                        {
                            Console.WriteLine(previousFireTime.Value.LocalDateTime.ToString());
                        }
                    }
                }
            }

            return returnItems;
        }



        private static void ScheduleAHasParametersJob(IScheduler sched, OtherJobInfo info)
        {

            IJobDetail hasParametersJobDetail = JobBuilder.Create<FileNameDoSomethingJob>()
                .WithIdentity(info.UniqueIdentifier + "IJobDetailWithIdentity", GROUP_NAME)
                //.UsingJobData("JobDetailFileName", info.FileName)
                .Build();


            ITrigger hasParametersJobTrigger001 = TriggerBuilder.Create()
              .WithIdentity(info.UniqueIdentifier + "ITriggerWithIdentity", GROUP_NAME)
                .UsingJobData("TriggerFileName", info.FileName)
              .StartNow()
              .WithSimpleSchedule(x => x
                  .WithIntervalInSeconds(info.WithIntervalInSeconds) /* You'll have to do something fancier here with scheduling if you want an exact time */
                  .WithRepeatCount(0))
              .Build();



            sched.ScheduleJob(hasParametersJobDetail, hasParametersJobTrigger001);
        }

    }




    public class FileNameDoSomethingJob : IJob
    {
        public virtual void Execute(IJobExecutionContext context)
        {
            JobKey key = context.JobDetail.Key;

            JobDataMap jbDataMap = context.JobDetail.JobDataMap;

            JobDataMap trgDataMap = context.Trigger.JobDataMap;

            string triggerFileNameParameter = trgDataMap.GetString("TriggerFileName");

            JobDataMap mergedMap = context.MergedJobDataMap;

            string msg = string.Format("HasParametersJob : JobKey='{0}', triggerFileNameParameter='{1}' at '{2}'", key, triggerFileNameParameter, DateTime.Now.ToLongTimeString());
            Console.WriteLine(msg);
        }
    }




    public class OtherJobInfoData
    {

        public List<OtherJobInfo> GetOtherJobs()
        {
            List<OtherJobInfo> returnItems = new List<OtherJobInfo>();

            OtherJobInfo oji1 = new OtherJobInfo() { UniqueIdentifier = "ABC123", WithIntervalInSeconds = 5, FileName = @"C:\file1.xml"};
            OtherJobInfo oji2 = new OtherJobInfo() { UniqueIdentifier = "DEF234", WithIntervalInSeconds = 5, FileName = @"C:\file2.xml" };
            OtherJobInfo oji3 = new OtherJobInfo() { UniqueIdentifier = "GHI345", WithIntervalInSeconds = 5, FileName = @"C:\file3.xml" };

            returnItems.Add(oji1);
            returnItems.Add(oji2);
            returnItems.Add(oji3);

            return returnItems;
        }
    }

    public class OtherJobInfo
    {
        public string UniqueIdentifier { get; set; }
        public int WithIntervalInSeconds { get; set; }
        public string FileName { get; set; }
    }

}

还有一些 xml 到 运行 ScheduleOtherJobsJob

<?xml version="1.0" encoding="UTF-8"?>

<job-scheduling-data xmlns="http://quartznet.sourceforge.net/JobSchedulingData"
        xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" version="2.0">


    <!-- This value wipes out existing jobs...be very careful with it being "true"  -->
    <processing-directives>
        <overwrite-existing-data>true</overwrite-existing-data>
    </processing-directives>

    <schedule>



        <job>
            <name>ScheduleOtherJobsJobName</name>
            <group>ScheduleOtherJobsJobGroupName</group>
            <description>My Description</description>
            <job-type>MyNamespace.ScheduleOtherJobsJob, MyAssembly</job-type>
            <durable>true</durable>
            <recover>false</recover>
            <job-data-map>
            </job-data-map>
        </job>
        <trigger>

            <simple>
                <name>ScheduleOtherJobsJobTriggerName</name>
                <group>ScheduleOtherJobsJobTriggerGroup</group>
                <description>My ScheduleOtherJobsJobTriggerName Description</description>
                <job-name>ScheduleOtherJobsJobName</job-name>
                <job-group>ScheduleOtherJobsJobGroupName</job-group>


                <job-data-map>
                </job-data-map>

                <!--<start-time>1982-06-28T18:15:00.0Z</start-time>-->
                <!--<end-time>2020-05-04T18:13:51.0Z</end-time>-->
                <misfire-instruction>SmartPolicy</misfire-instruction>
                <!-- repeat indefinitely every 5 seconds -->
                <repeat-count>-1</repeat-count>
                <repeat-interval>5000</repeat-interval>



            </simple>

        </trigger>

    </schedule>



</job-scheduling-data>

编辑:追加

确保您的 app.config(或 web.config)指向石英配置文件。这是我的顶......注意,它应该作为一个指南......

<?xml version="1.0" encoding="utf-8"?>
<configuration>


    <configSections>
        <section name="quartz" type="System.Configuration.NameValueSectionHandler, System, Version=1.0.5000.0,Culture=neutral, PublicKeyToken=b77a5c561934e089" />


    </configSections>




    <quartz configSource="MyQuartzConfiguration.config" />

然后 MyQuartzConfiguration.config 文件需要一些东西,最重要的是 "Quartz_Jobs_001.xml"(名称不重要,但此文件包含您的作业和触发器的信息。

<add key="quartz.plugin.jobInitializer.type" value="Quartz.Plugin.Xml.XMLSchedulingDataProcessorPlugin" />
<add key="quartz.scheduler.instanceName" value="DefaultQuartzScheduler" />
<add key="quartz.threadPool.type" value="Quartz.Simpl.SimpleThreadPool, Quartz" />
<add key="quartz.threadPool.threadCount" value="10" />
<add key="quartz.threadPool.threadPriority" value="2" />
<add key="quartz.jobStore.misfireThreshold" value="60000" />
<add key="quartz.jobStore.type" value="Quartz.Simpl.RAMJobStore, Quartz" />
<add key="quartz.plugin.jobInitializer.fileNames" value="Quartz_Jobs_001.xml" />
<add key="quartz.plugin.jobInitializer.failOnFileNotFound" value="true" />
<add key="quartz.plugin.jobInitializer.scanInterval" value="120" />