You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Quartz.Net触发器重复触发问题排查求助

Hey there, let's break down why your Quartz triggers are firing multiple times and how to fix it. Based on your code and the behavior you described, here are the most likely culprits and actionable fixes:

Key Issues Identified

  1. Multiple Scheduler Instances Running
    Your current setup creates a new JobScheduler every time the app starts (via Application_Start). If IIS recycles the app pool (especially with overlapping recycling enabled), old scheduler processes might not shut down properly, leaving multiple instances running—each firing the same triggers. That's why you saw the count jump from 1 to 2 and eventually stabilize at 4.

  2. Potential Trigger/Job Identity Collisions
    If your 8 additional triggers/jobs use the same WithIdentity("job", "Information") and WithIdentity("trigger", "Information") as the first one, Quartz might be registering duplicates or overwriting entries in unexpected ways.

  3. Static JobDataMap Values
    You're setting toDate and fromDate to values calculated when the scheduler starts, not when the trigger fires. This means every email will use the app start time's window, not the actual trigger time's window—this isn't causing the duplicate fires, but it's a critical bug to fix.

Step-by-Step Fixes

1. Make the Scheduler a Singleton and Handle Shutdown Properly

Quartz works best with a single scheduler instance. Modify your JobScheduler to be a singleton, and ensure it shuts down cleanly when the app stops:

JobScheduler.cs (Singleton Update)

public class JobScheduler {
    private static readonly JobScheduler _instance = new JobScheduler();
    private readonly IScheduler _scheduler;
    private readonly LoggingRepository logging = new LoggingRepository();

    // Private constructor to enforce singleton
    private JobScheduler() {
        // Initialize scheduler once
        _scheduler = StdSchedulerFactory.GetDefaultScheduler().Result;
    }

    public static JobScheduler Instance => _instance;

    public void Start(string serverPath, string httpPath) {
        // Don't restart if already running
        if (_scheduler.IsStarted) {
            logging.WriteLog(1, "JobScheduler", "Start", "Scheduler already running", "Skipping start", LogType.Info, "");
            return;
        }

        try {
            var to = "clm@thansen.dk";
            var serverMapPath = serverPath;
            var httpRunPath = httpPath;
            var workingDays = new DayOfWeek[] { 
                DayOfWeek.Monday, DayOfWeek.Tuesday, DayOfWeek.Wednesday, 
                DayOfWeek.Thursday, DayOfWeek.Friday 
            };

            _scheduler.Start();
            logging.WriteLog(1, "JobScheduler", "Start", "Scheduler started", 
                $"InstanceId: {_scheduler.SchedulerInstanceId}", LogType.Info, "");

            // Create triggers for each hour (8-16) with unique identities
            for (int hour = 8; hour <= 16; hour++) {
                var jobIdentity = new JobKey($"brokenImgJob_{hour}", "Information");
                var triggerIdentity = new TriggerKey($"brokenImgTrigger_{hour}", "Information");

                // Skip if job/trigger already exists (prevents duplicates on app restart)
                if (_scheduler.CheckExists(jobIdentity).Result) continue;

                IJobDetail emailerJob = JobBuilder.Create<BrokenIMGMailJob>()
                    .WithIdentity(jobIdentity)
                    .WithDescription($"Broken image email job for {hour}:00")
                    .Build();

                ITrigger emailerTrigger = TriggerBuilder.Create()
                    .WithIdentity(triggerIdentity)
                    .UsingJobData("to", to)
                    .UsingJobData("serverMapPath", serverMapPath)
                    .UsingJobData("httpRunPath", httpRunPath)
                    .WithSchedule(CronScheduleBuilder
                        .AtHourAndMinuteOnGivenDaysOfWeek(hour, 0, workingDays))
                    .Build();

                _scheduler.ScheduleJob(emailerJob, emailerTrigger).Wait();
            }
        } catch (Exception ex) {
            logging.WriteLog(1, "JobScheduler", "Start", "Start trigger failed", 
                $"EXCEPTION MESSAGE: {ex.Message} | INNER: {ex.InnerException}", LogType.Exception, "");
        }
    }

    public void Shutdown() {
        if (_scheduler.IsStarted) {
            // Wait for running jobs to finish before shutting down
            _scheduler.Shutdown(true).Wait();
            logging.WriteLog(1, "JobScheduler", "Shutdown", "Scheduler stopped", "", LogType.Info, "");
        }
    }
}

Update Global.asax to Use Singleton and Handle Shutdown

protected void Application_Start() {
    AreaRegistration.RegisterAllAreas();
    GlobalConfiguration.Configure(WebApiConfig.Register);
    FilterConfig.RegisterGlobalFilters(GlobalFilters.Filters);
    RouteConfig.RegisterRoutes(RouteTable.Routes);
    BundleConfig.RegisterBundles(BundleTable.Bundles);
    
    var httpRunPath = HttpRuntime.AppDomainAppPath;
    var serverMapPath = Server.MapPath(".");
    JobScheduler.Instance.Start(serverMapPath, httpRunPath);
}

protected void Application_End() {
    JobScheduler.Instance.Shutdown();
}

2. Disable IIS App Pool Overlapping Recycling

IIS's default overlapping recycling keeps old app processes running while new ones start—this can lead to multiple scheduler instances. Turn it off:

  • Open IIS Manager → Your App Pool → Advanced Settings
  • Set Overlapped Recycling to False
  • Restart the app pool

3. Fix JobDataMap Time Values

Calculate the time window when the job runs, not when the scheduler starts. Update your job's Execute method:

[DisallowConcurrentExecution]
public class BrokenIMGMailJob : IJob {
    LoggingRepository logging = new LoggingRepository();
    Email _email = new Email();
    
    public async Task Execute(IJobExecutionContext context) {
        try {
            JobDataMap dataMap = context.MergedJobDataMap;
            var tmpTo = dataMap.GetString("to");
            
            // Calculate time window at trigger time, not app start
            var toDate = DateTime.Now;
            var fromDate = DateTime.Now.AddHours(-7);
            
            // Rest of your email logic...
        } catch (Exception ex) {
            logging.WriteLog(1, "JobClass", "BrokenIMGMailJob","Send broken images mail", 
                $"EXCEPTION MESSAGE: {ex.Message} | INNER: {ex.InnerException}", LogType.Exception, "");
        }
    }
}

4. Verify Trigger Uniqueness

The loop in the singleton JobScheduler creates unique identities for each job/trigger (brokenImgJob_8, brokenImgTrigger_8, etc.), which prevents Quartz from registering duplicate triggers.

Why [DisallowConcurrentExecution] Didn't Fix It

This attribute only prevents the same job from running concurrently—it doesn't stop multiple triggers from firing the job multiple times. It's useful for ensuring your email logic doesn't run in parallel, but it won't solve the duplicate trigger issue.

After implementing these fixes, your triggers should fire exactly once per scheduled hour.

内容的提问来源于stack exchange,提问作者Carsten

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.05.15 07:58:56