基于NCrontab实现Covarel扩展秒级任务调度

Covarel 虽然作为一个轻量级调度框架已经满足需求,但是对于Corn 表达式的支持相对较弱,作者实现对秒级的支持实际是通过特殊属性标记字段进行处理,也不支持秒级表达式* * * * * 这种6位写法,只支持到5位。

对于需要秒级cron的开发者来说,需要自定扩展,笔者采用的是使用NCrontab 实现对cron 秒级的解析。官方传送门,如果对NCrontab 不熟悉,可以自行查阅。以下是官方描述:

It does not provide any scheduler or is not a scheduling facility like cron from Unix platforms. What it provides is parsing, formatting and an algorithm to produce occurrences of time based on a give schedule expressed in the crontab format.

翻译过来就是,他并不是提供了任何调度能力,也不像是Unix 平台那样的cron功能。它提供的是解析、格式化以及根据用 crontab 格式表示的给定计划生成时间的算法。简单说就是,它能解析Cron表达式并按照算法转换为对应的时间。以官方描述为例。

cs 复制代码
using System;
using NCrontab;
var s = CrontabSchedule.Parse("0 12 * */2 Mon"); // Monday of every other month
var start = new DateTime(2000, 1, 1);            // Starting January 1, 2000
var end = start.AddYears(1);                     // Through the year 2000
var occurrences = s.GetNextOccurrences(start, end);
Console.WriteLine(string.Join(Environment.NewLine,
                  from t in occurrences
                  select $"{t:ddd, dd MMM yyyy HH:mm}"));

输出结果。

bash 复制代码
周一, 03 1月 2000 12:00
周一, 10 1月 2000 12:00
周一, 17 1月 2000 12:00
周一, 24 1月 2000 12:00

主要实现思路如下,案例的主体内容依托于笔者的上一篇文章Coravel 自动加载Invocable调度任务,通过外观者模式以及一个秒级cron任务调度引擎类,通过判定调度任务类[ScheduledJob(cron)]中对应的cron 表达式位数进行逻辑选择,控制该类是分配给Covarel还是自定调度。

新增项目CovarelCronLab,主要用于保留CronLab 扩展代码,对应依赖如下:

xml 复制代码
<Project Sdk="Microsoft.NET.Sdk">
  <PropertyGroup>
    <TargetFramework>net9.0</TargetFramework>
    <ImplicitUsings>enable</ImplicitUsings>
    <Nullable>enable</Nullable>
  </PropertyGroup>
	<ItemGroup>
		<PackageReference Include="Microsoft.Extensions.DependencyModel" Version="10.0.0" />
		<PackageReference Include="Coravel" Version="6.0.2" />
		<PackageReference Include="NCrontab" Version="3.4.0" />
	</ItemGroup>
</Project>

对应类功能用途如下:

bash 复制代码
NCrontab/
	# 实际调度选择类
    HybridScheduleInterval.cs
    # 混合调度外包类(外观模式实现类)
    HybridScheduler.cs        
    # 混合调度扩展类(用于平替Covarel.UseScheduler)
	HybridSchedulerExtensions.cs 
	# 混合调度外包接口(外部调用入口)
    IHybridScheduler.cs
    # 秒级调度接口(仿Covarel)
    ISecCronScheduler.cs
    # 本地定义空ScheduledEventConfiguration
    NullScheduledEventConfiguration.cs
    # 秒级调度实现类+后台任务实现
    SecCrontabSchedulerEngine.cs

代码改造

对原有代码进行改造,其中包括任务类、服务注册以及调度配置部分。

任务调度类

设置DateTimeJob 调度类作为演示案例,修改原有5位分钟级cron 为秒级6位,暂且定义每20s 执行一次。

cs 复制代码
[ScheduledJob("*/20 * * * * *")]
public class DateTimeJob : IInvocable
{
    private readonly ILogger<DateTimeJob> _logger;
    public DateTimeJob(ILogger<DateTimeJob> logger)
    {
        _logger = logger;
    }
    public Task Invoke()
    {
        _logger.LogInformation("datetimejob run at {date}", DateTime.Now);
        return Task.CompletedTask;
    }
}

服务注册与调度配置

使用HybridSchedulerExtensions 中预先创建的扩展,实现服务注册和调度配置。

cs 复制代码
    public static class HybridSchedulerExtensions
    {
        /// <summary>
        /// 添加混合调度器服务(秒级)
        /// </summary>
        /// <param name="services"></param>
        /// <returns></returns>
        public static IServiceCollection AddHybridScheduler(this IServiceCollection services)
        {
            //注册秒级调度器 as IHostedService
            services.AddHostedService<SecCrontabSchedulerEngine>();
            //注册秒级调度器实现
            services.AddSingleton<ISecCronScheduler, SecCrontabSchedulerEngine>();

            // 注册混合调度器(供业务使用)
            services.AddSingleton<IHybridScheduler>(sp =>
                new HybridScheduler(
                    coravel: sp.GetRequiredService<IScheduler>(),
                    sec: sp.GetRequiredService<ISecCronScheduler>(),
                    sp: sp));
            services.AddHostedService<SecCrontabSchedulerEngine>();

            return services;
        }

        /// <summary>
        /// 配置混合调度器
        /// </summary>
        /// <param name="provider"></param>
        /// <param name="configure"></param>
        /// <exception cref="ArgumentNullException"></exception>
        public static void UseHybridScheduler(this IServiceProvider provider, Action<IHybridScheduler> configure)
        {
            if (provider == null) throw new ArgumentNullException(nameof(provider));
            if (configure == null) throw new ArgumentNullException(nameof(configure));
            // 获取混合调度器实例
            var scheduler = provider.GetRequiredService<IHybridScheduler>();
            configure(scheduler);
        }
    }

主类修改

修改Program.cs,将扩展应用于控制台程序中。

cs 复制代码
    internal class Program
    {
        static void Main(string[] args)
        {
            var builder = Host.CreateApplicationBuilder(args);
            ILogger<Program> logger = builder.Services.BuildServiceProvider().GetRequiredService<ILogger<Program>>();
            // 注册调度服务
            builder.Services.AddScheduler();
            // 新增扩展->注册混合调度服务
            builder.Services.AddHybridScheduler();

            var app = builder.Build();
            // 修改调度->UseScheduler 替换为 UseHybridScheduler
            // 去除.LogScheduledTaskProgress()
            app.Services.UseHybridScheduler(scheduler => {
                if (jobTypes != null)
                {
                    // 遍历类型将其添加到调度类型中
                    foreach (var jobType in jobTypes)
                    {
                        var attribute = jobType.GetCustomAttribute<ScheduledJobAttribute>();
                        if (attribute != null)
                        {
                            scheduler.ScheduleInvocableType(jobType).Cron(attribute.CronExpression);
                        }
                    }
                }
            });
            // 注册应用生命周期结束时调度任务
            var appLifetime = app.Services.GetService<IHostApplicationLifetime>();
            appLifetime?.ApplicationStopping.Register(() =>
            {
                // 结束调度
            });
            app.Run();
        }
    }

核心代码解析

类调用依赖图

CrontabSchedule HybridScheduleInterval HybridScheduler HybridSchedulerExtensions IInvocable IScheduleInterval IScheduler ISecCronScheduler NullScheduledEventConfiguration SecCrontabSchedulerEngine

IHybridScheduler

调度外包入口接口IHybridScheduler.cs,主要用于上层外包,保留几个Coravel 调度同名函数,用于封装避免函数差异。

cs 复制代码
/// <summary>
/// 混合调度器接口,支持秒级和分钟级调度
/// </summary>
public interface IHybridScheduler
{
    IScheduleInterval Schedule(Action action);
    IScheduleInterval ScheduleAsync(Func<Task> asyncTask);
    IScheduleInterval Schedule<T>() where T : IInvocable;
    IScheduleInterval ScheduleWithParams<T>(params object[] parameters) where T : IInvocable;
    IScheduleInterval ScheduleWithParams(Type invocableType, params object[] parameters);
    IScheduleInterval ScheduleInvocableType(Type invocableType);
}

HybridScheduler

实现类HybridScheduler.cs作为IHybridScheduler实现类,内部包含对应的实际调度转换逻辑类HybridScheduleInterval

cs 复制代码
public class HybridScheduler : IHybridScheduler
{
    private readonly IScheduler _coravel;
    private readonly ISecCronScheduler _sec;
    private readonly IServiceProvider _sp;
	// 依赖注入时,将`Covarel`调度类于秒级调度类进行实例传入
    public HybridScheduler(IScheduler coravel, ISecCronScheduler sec, IServiceProvider sp)
    {
        _coravel = coravel;
        _sec = sec;
        _sp = sp;
    }
    public IScheduleInterval Schedule(Action action) =>
        new HybridScheduleInterval(_coravel, _sec, _sp, action);

    public IScheduleInterval ScheduleAsync(Func<Task> asyncTask) =>
        new HybridScheduleInterval(_coravel, _sec, _sp, asyncTask, isAsync: true);

    public IScheduleInterval Schedule<T>() where T : IInvocable =>
        new HybridScheduleInterval(_coravel, _sec, _sp, typeof(T), isInvocable: true);

    public IScheduleInterval ScheduleWithParams<T>(params object[] parameters) where T : IInvocable =>
        new HybridScheduleInterval(_coravel, _sec, _sp, typeof(T), parameters, isInvocable: true, hasParams: true);

    public IScheduleInterval ScheduleWithParams(Type type, params object[] parameters) =>
        new HybridScheduleInterval(_coravel, _sec, _sp, type, parameters, isInvocable: true, hasParams: true);

    public IScheduleInterval ScheduleInvocableType(Type type) =>
        new HybridScheduleInterval(_coravel, _sec, _sp, type, isInvocable: true);
}

ISecCronScheduler

自定义秒级调度接口ISecCronScheduler.cs,与Covarel 中的部分函数同名便于切换。

cs 复制代码
    public interface ISecCronScheduler
    {
        void ScheduleSync(Action action, string cronExpression);
        void ScheduleAsync(Func<Task> task, string cronExpression);
        void ScheduleInvocableByType(IServiceProvider sp, Type type, object[] parameters, string cronExpression);
    }

SecCrontabSchedulerEngine

秒级调度实现引擎类SecCrontabSchedulerEngine.cs,既包含了后台任务实现,也包含了调度实现,其中CreateTimer(Func<Task> callback, string cronExpression) 为核心调度任务生成类,主要通过委托方式和NCronLab生成调度逻辑,按照Cron表达式生成调度触发时刻,交由Timer 进行执行,命中条件即执行委托函数。

cs 复制代码
public class SecCrontabSchedulerEngine : ISecCronScheduler, IHostedService, IDisposable
{
    private readonly IServiceProvider _serviceProvider;
    private readonly ConcurrentDictionary<Guid, Timer> _timers = new();
    private readonly ILogger<SecCrontabSchedulerEngine> _logger;
    private readonly ConcurrentBag<Task> _runningTasks = new();
    private bool _disposed = false;
    private readonly object _lock = new();

    public SecCrontabSchedulerEngine(IServiceProvider serviceProvider)
    {
        _serviceProvider = serviceProvider;
        _logger = serviceProvider.GetService<ILogger<SecCrontabSchedulerEngine>>();
    }
	// 核心函数
    private Timer CreateTimer(Func<Task> callback, string cronExpression)
    {
        if (_disposed) throw new ObjectDisposedException(nameof(SecCrontabSchedulerEngine));

        var schedule = CrontabSchedule.Parse(cronExpression, new CrontabSchedule.ParseOptions { IncludingSeconds = true });
        Guid id = Guid.NewGuid();

        async void OnTick(object? _)
        {
            Task? task = null;
            try
            {
                task = callback();
                if (task != null)
                {
                    _runningTasks.Add(task);
                    await task;
                }
            }
            catch (Exception ex)
            {
                // 可替换为 ILogger
                _logger.LogInformation($"[SecCron] Error: {ex}");
            }
            finally
            {
                if (task != null) _runningTasks.TryTake(out Task _);
            }

            if (_disposed) return;

            var now = DateTime.Now;
            var next = schedule.GetNextOccurrence(now);
            var delayMs = Math.Max(0, (long)(next - now).TotalMilliseconds);

            if (_timers.TryGetValue(id, out var timer))
            {
                try { timer.Change(delayMs, Timeout.Infinite); }
                catch (ObjectDisposedException) { }
            }
        }

        var first = schedule.GetNextOccurrence(DateTime.Now);
        var firstDelayMs = Math.Max(0, (long)(first - DateTime.Now).TotalMilliseconds);
        var timer = new Timer(OnTick, null, firstDelayMs, Timeout.Infinite);

        lock (_lock)
        {
            if (!_disposed)
                _timers[id] = timer;
            else
                timer.Dispose();
        }

        return timer;
    }

    public void ScheduleSync(Action action, string cron) =>
        CreateTimer(() => { action(); return Task.CompletedTask; }, cron);

    public void ScheduleAsync(Func<Task> task, string cron) =>
        CreateTimer(task, cron);

    public void ScheduleInvocableByType(IServiceProvider sp, Type type, object[] parameters, string cronExpression)
    {
        if (!typeof(IInvocable).IsAssignableFrom(type))
            throw new ArgumentException($"{type} must implement IInvocable");

        CreateTimer(async () =>
        {
            using var scope = sp.CreateScope();
            var instance = (IInvocable)ActivatorUtilities.CreateInstance(scope.ServiceProvider, type, parameters);
            await instance.Invoke();
        }, cronExpression);
    }

    public Task StartAsync(CancellationToken ct) => Task.CompletedTask;

    public async Task StopAsync(CancellationToken ct)
    {
        _disposed = true;
        foreach (var (_, timer) in _timers) timer?.Dispose();
        _timers.Clear();

        // 等待所有任务完成(最多 30 秒)
        var timeout = TimeSpan.FromSeconds(30);
        var startTime = DateTime.UtcNow;
        while (_runningTasks.Count > 0 && DateTime.UtcNow - startTime < timeout)
        {
            await Task.Delay(50, ct);
        }
    }

    public void Dispose()
    {
        _disposed = true;
        foreach (var (_, timer) in _timers) timer?.Dispose();
        _timers.Clear();
    }
}

HybridScheduleInterval

混合秒和分钟调度类HybridScheduleInterval.cs 实现Coravel中的IScheduleInterval接口,用于同等构建调度逻辑,将分钟配置都代理到Coravel调度实例,秒级由自定义秒级调度处理类进行处理。核心代码类主要有两个,一个是Cron(string cronExpression) 用于处理cron表达式,一个ProxyToCoravel(Func<IScheduleInterval, IScheduledEventConfiguration> f) 用于将固有分钟级别调度逻辑均代理到Coravel调度实例。

cs 复制代码
public class HybridScheduleInterval : IScheduleInterval
{
    private readonly IScheduler _coravel;
    private readonly ISecCronScheduler _sec;
    private readonly IServiceProvider _sp;
    private readonly object _task;
    private readonly object[] _params;
    private readonly bool _isAsync;
    private readonly bool _isInvocable;
    private readonly bool _hasParams;

    public HybridScheduleInterval(
        IScheduler coravel,
        ISecCronScheduler sec,
        IServiceProvider sp,
        object task,
        object[]? parameters = null,
        bool isAsync = false,
        bool isInvocable = false,
        bool hasParams = false)
    {
        _coravel = coravel;
        _sec = sec;
        _sp = sp;
        _task = task;
        _params = parameters ?? Array.Empty<object>();
        _isAsync = isAsync;
        _isInvocable = isInvocable;
        _hasParams = hasParams;
    }

    private bool IsSixField(string cron)
    {
        var parts = cron.Trim().Split(new char[] { ' ', '\t' }, StringSplitOptions.RemoveEmptyEntries);
        return parts.Length == 6;
    }
	// 核心类 用于区分当前cron是5位分钟还是6位秒级
    public IScheduledEventConfiguration Cron(string cronExpression)
    {
        if (string.IsNullOrWhiteSpace(cronExpression))
            throw new ArgumentException("Cron expression is required.", nameof(cronExpression));

        bool isSixField = IsSixField(cronExpression);

        if (isSixField)
        {
            if (_isInvocable && _task is Type invocableType)
            {
                _sec.ScheduleInvocableByType(_sp, invocableType, _params, cronExpression);
            }
            else if (_isAsync && _task is Func<Task> asyncFunc)
            {
                _sec.ScheduleAsync(asyncFunc, cronExpression);
            }
            else if (_task is Action action)
            {
                _sec.ScheduleSync(action, cronExpression);
            }
            else
            {
                throw new InvalidOperationException("Unsupported task type for second-precision scheduling.");
            }
            return new NullScheduledEventConfiguration();
        }
        else
        {
            // Delegate to Coravel for 5-field cron
            if (_task is Action a)
                return _coravel.Schedule(a).Cron(cronExpression);
            if (_task is Func<Task> f)
                return _coravel.ScheduleAsync(f).Cron(cronExpression);
            if (_isInvocable && _task is Type t)
            {
                if (_hasParams)
                    return _coravel.ScheduleWithParams(t, _params).Cron(cronExpression);
                else
                    return _coravel.ScheduleInvocableType(t).Cron(cronExpression);
            }
        }

        return new NullScheduledEventConfiguration();
    }

    // --- 频率方法:秒级走自定义,分钟级走 Coravel ---

    public IScheduledEventConfiguration EverySecond() => Cron("*/1 * * * * *");
    public IScheduledEventConfiguration EveryFiveSeconds() => Cron("*/5 * * * * *");
    public IScheduledEventConfiguration EveryTenSeconds() => Cron("*/10 * * * * *");
    public IScheduledEventConfiguration EveryFifteenSeconds() => Cron("*/15 * * * * *");
    public IScheduledEventConfiguration EveryThirtySeconds() => Cron("*/30 * * * * *");
    public IScheduledEventConfiguration EverySeconds(int s) => Cron($"*/{s} * * * * *");

    public IScheduledEventConfiguration EveryMinute() => ProxyToCoravel(x => x.EveryMinute());
    public IScheduledEventConfiguration EveryFiveMinutes() => ProxyToCoravel(x => x.EveryFiveMinutes());
    public IScheduledEventConfiguration EveryTenMinutes() => ProxyToCoravel(x => x.EveryTenMinutes());
    public IScheduledEventConfiguration EveryFifteenMinutes() => ProxyToCoravel(x => x.EveryFifteenMinutes());
    public IScheduledEventConfiguration EveryThirtyMinutes() => ProxyToCoravel(x => x.EveryThirtyMinutes());
    public IScheduledEventConfiguration Hourly() => ProxyToCoravel(x => x.Hourly());
    public IScheduledEventConfiguration HourlyAt(int m) => ProxyToCoravel(x => x.HourlyAt(m));
    public IScheduledEventConfiguration Daily() => ProxyToCoravel(x => x.Daily());
    public IScheduledEventConfiguration DailyAtHour(int h) => ProxyToCoravel(x => x.DailyAtHour(h));
    public IScheduledEventConfiguration DailyAt(int h, int m) => ProxyToCoravel(x => x.DailyAt(h, m));
    public IScheduledEventConfiguration Weekly() => ProxyToCoravel(x => x.Weekly());
    public IScheduledEventConfiguration Monthly() => ProxyToCoravel(x => x.Monthly());

    private IScheduledEventConfiguration ProxyToCoravel(Func<IScheduleInterval, IScheduledEventConfiguration> f)
    {
        if (_task is Action a) return f(_coravel.Schedule(a));
        if (_task is Func<Task> func) return f(_coravel.ScheduleAsync(func));
        if (_isInvocable && _task is Type t)
        {
            if (_hasParams)
                return f(_coravel.ScheduleWithParams(t, _params));
            else
                return f(_coravel.ScheduleInvocableType(t));
        }
        return new NullScheduledEventConfiguration();
    }
}

运行效果如下:

bash 复制代码
info: ConsoleCorave.Jobs.DateTimeJob[0]
      datetimejob run at 12/05/2025 09:44:00
info: ConsoleCorave.Jobs.HelloJob[0]
      hellojob run at 12/05/2025 09:44:00
info: ConsoleCorave.Jobs.DateTimeJob[0]
      datetimejob run at 12/05/2025 09:44:20
info: ConsoleCorave.Jobs.DateTimeJob[0]
      datetimejob run at 12/05/2025 09:44:40
info: ConsoleCorave.Jobs.DateTimeJob[0]
      datetimejob run at 12/05/2025 09:45:00
info: ConsoleCorave.Jobs.HelloJob[0]
      hellojob run at 12/05/2025 09:45:00
info: ConsoleCorave.Jobs.DateTimeJob[0]
      datetimejob run at 12/05/2025 09:45:20

案例总结

通过以上方式对Covarel 也有了更加深入的理解,如果不需要Covarel 那些高级功能,完全可以按照上方案例实现一个简版的自定义秒级框架,而不是依托Coravel

相关推荐
足球中国2 小时前
什么情况下会发生跨域
c#·dataexcel·cfucion
MoFe12 小时前
【.net/.net core】【报错处理】另一个 SqlParameterCollection 中已包含 SqlParameter。
java·.net·.netcore
yue0082 小时前
C# 实现电脑锁屏功能
开发语言·c#·电脑·电脑锁屏
2501_930707783 小时前
如何在 C# 中分离饼图的某个区域
开发语言·c#
c#上位机3 小时前
halcon图像膨胀—dilation1
图像处理·算法·c#·halcon
缺点内向3 小时前
如何在C#中添加Excel文档属性?
开发语言·数据库·c#·.net·excel
车载测试工程师4 小时前
CAPL学习-ETH功能函数-方法类2
网络·网络协议·学习·c#·以太网·capl·canoe
在路上看风景4 小时前
1.12 多线程和异步编程
c#
曹牧4 小时前
Java:list<map<string,sting>>与C#互操作
java·c#·list