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

Quartz.NET作业调度异常:启动作业后无法通过API停止指定作业

问题描述

需要实现以下功能:

  • 调用Start API并传入Job ID时,调度一个Quartz作业,该作业从1开始每隔10秒打印数字
  • 调用Stop API并传入相同Job ID时,停止该作业

测试时出现异常:执行Start方法后,_schedulers字典已存储对应Job ID的调度器,但调用Stop API时,_schedulers字典为空,抛出Job with ID: id1 not found异常。


相关代码

JobScheduler类

using Quartz;
using Quartz.Impl;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;

public class JobScheduler
{
    public Dictionary<string, IScheduler> _schedulers;
    private bool _isSchedulerStarted;

    public JobScheduler()
    {
        _schedulers = new Dictionary<string, IScheduler>();
        _isSchedulerStarted = false;
    }

    public async Task Start(string jobId, int startIndex)
    {
        try
        {
            IScheduler scheduler = await new StdSchedulerFactory().GetScheduler();
            await scheduler.Start();
            _isSchedulerStarted = true;

            if (_schedulers.ContainsKey(jobId))
            {
                throw new ArgumentException($"A job with ID '{jobId}' already exists.");
            }
            else
            {
                IJobDetail job = JobBuilder.Create<PrintNumbersJob>()
                    .WithIdentity(jobId, "group1")
                    .UsingJobData("startIndex", startIndex)
                    .Build();

                ITrigger trigger = TriggerBuilder.Create()
                    .WithIdentity($"{jobId}_Trigger", "group1")
                    .StartNow()
                    .Build();

                await scheduler.ScheduleJob(job, trigger);

                _schedulers.Add(jobId, scheduler);

                Console.WriteLine(_schedulers);
            }

            Console.WriteLine("Schedulers before starting job:");
            foreach (var kvp in _schedulers)
            {
                Console.WriteLine($"Key: {kvp.Key}, Value: {kvp.Value}");
            }
        }
        catch (SchedulerException ex)
        {
            Console.WriteLine($"Error starting scheduler for job ID {jobId}: {ex.Message}");
            _isSchedulerStarted = false;
            throw;
        }
    }

    public bool IsSchedulerStarted()
    {
        return _isSchedulerStarted;
    }

    public async Task Stop(string jobId)
    {
        try
        {
            Console.WriteLine("Schedulers before stopping job:");
            foreach (var kvp in _schedulers)
            {
                Console.WriteLine($"Key: {kvp.Key}, Value: {kvp.Value}");
            }

            if (IsSchedulerStarted())
            {
                IScheduler scheduler = _schedulers[jobId];
                Console.WriteLine("YES");
                await scheduler.Shutdown();
                _schedulers.Remove(jobId);

                Console.WriteLine($"Job with ID: {jobId} stopped successfully.");
            }
            else
            {
                throw new ArgumentException($"Job with ID: {jobId} not found.");
            }
        }
        catch (Exception ex)
        {
            Console.WriteLine($"Error stopping job with job ID {jobId}: {ex.Message}");
            throw;
        }
    }
}

Job控制器

using Microsoft.AspNetCore.Mvc;
using System;
using System.Threading.Tasks;
using trash.Models;

[ApiController]
[Route("[controller]")]
public class JobController : ControllerBase
{
    private JobScheduler _jobScheduler;

    public JobController(JobScheduler jobScheduler)
    {
        _jobScheduler = jobScheduler;
    }

    [HttpPost("start")]
    public async Task<IActionResult> Start([FromBody] JobModel request)
    {
        await _jobScheduler.Start(request.JobId, 1);
        return Ok($"Job scheduled successfully with ID: {request.JobId}!");
    }

    [HttpPost("stop")]
    public async Task<IActionResult> Stop([FromBody] JobModel request)
    {
        await _jobScheduler.Stop(request.JobId);
        return Ok($"Job with ID: {request.JobId} stopped successfully!");
    }
}

PrintNumbersJob作业

using Quartz;
using System;
using System.Threading.Tasks;

public class PrintNumbersJob : IJob
{
    private int _currentIndex;

    public Task Execute(IJobExecutionContext context)
    {
        var dataMap = context.JobDetail.JobDataMap;
        _currentIndex = dataMap.GetInt("startIndex");

        while (true)
        {
            Console.WriteLine($"{context.JobDetail.Key} "+_currentIndex++);
            Task.Delay(TimeSpan.FromSeconds(10)).Wait();
        }
    }
}

程序输出

Schedulers before starting job:
Key: id1, Value: Quartz.Impl.StdScheduler
group1.id1 1
group1.id1 2
group1.id1 3
group1.id1 4
Schedulers before stopping job:
Exception thrown: 'System.ArgumentException' in trash.dll
Error stopping job with job ID id1: Job with ID: id1 not found.

问题原因分析

  1. 依赖注入生命周期错误:如果JobScheduler在ASP.NET Core中注册为瞬时(Transient)或范围(Scoped)服务,Start和Stop请求会拿到不同的JobScheduler实例。Start请求往实例A的_schedulers添加数据,Stop请求用的是实例B,自然找不到对应Job ID。
  2. 作业执行阻塞线程:PrintNumbersJob的Execute方法用while(true)+Task.Delay().Wait()同步阻塞,会占用Quartz的作业线程,可能导致后续调度操作异常。
  3. 状态判断逻辑不合理:全局的_isSchedulerStarted变量无法准确反映单个作业的状态,且多线程环境下存在线程安全问题。

修复方案

1. 修正依赖注入生命周期

在Program.cs中将JobScheduler注册为单例服务,确保整个应用只有一个实例:

builder.Services.AddSingleton<JobScheduler>();

2. 修复作业阻塞问题

将作业的同步阻塞改为异步等待,利用Quartz的取消令牌实现优雅停止:

public async Task Execute(IJobExecutionContext context)
{
    var dataMap = context.JobDetail.JobDataMap;
    int currentIndex = dataMap.GetInt("startIndex");

    while (!context.CancellationToken.IsCancellationRequested)
    {
        Console.WriteLine($"{context.JobDetail.Key} {currentIndex++}");
        await Task.Delay(TimeSpan.FromSeconds(10), context.CancellationToken);
    }
}

3. 优化Stop方法的判断逻辑

去掉全局状态依赖,直接检查_schedulers中是否存在目标Job ID,并添加线程安全锁:

public class JobScheduler
{
    private readonly Dictionary<string, IScheduler> _schedulers;
    private readonly object _lockObj = new object();

    public JobScheduler()
    {
        _schedulers = new Dictionary<string, IScheduler>();
    }

    public async Task Start(string jobId, int startIndex)
    {
        try
        {
            IScheduler scheduler = await new StdSchedulerFactory().GetScheduler();
            await scheduler.Start();

            lock (_lockObj)
            {
                if (_schedulers.ContainsKey(jobId))
                {
                    throw new ArgumentException($"A job with ID '{jobId}' already exists.");
                }
            }

            IJobDetail job = JobBuilder.Create<PrintNumbersJob>()
                .WithIdentity(jobId, "group1")
                .UsingJobData("startIndex", startIndex)
                .Build();

            ITrigger trigger = TriggerBuilder.Create()
                .WithIdentity($"{jobId}_Trigger", "group1")
                .StartNow()
                .Build();

            await scheduler.ScheduleJob(job, trigger);

            lock (_lockObj)
            {
                _schedulers.Add(jobId, scheduler);
            }

            Console.WriteLine("Schedulers after starting job:");
            foreach (var kvp in _schedulers)
            {
                Console.WriteLine($"Key: {kvp.Key}, Value: {kvp.Value}");
            }
        }
        catch (SchedulerException ex)
        {
            Console.WriteLine($"Error starting scheduler for job ID {jobId}: {ex.Message}");
            throw;
        }
    }

    public async Task Stop(string jobId)
    {
        try
        {
            Console.WriteLine("Schedulers before stopping job:");
            lock (_lockObj)
            {
                foreach (var kvp in _schedulers)
                {
                    Console.WriteLine($"Key: {kvp.Key}, Value: {kvp.Value}");
                }

                if (_schedulers.TryGetValue(jobId, out IScheduler scheduler))
                {
                    await scheduler.Shutdown();
                    _schedulers.Remove(jobId);
                    Console.WriteLine($"Job with ID: {jobId} stopped successfully.");
                }
                else
                {
                    throw new ArgumentException($"Job with ID: {jobId} not found.");
                }
            }
        }
        catch (Exception ex)
        {
            Console.WriteLine($"Error stopping job with job ID {jobId}: {ex.Message}");
            throw;
        }
    }
}

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.26 17:43:09