XiHan Framework 处于预览阶段

查看获取方式
跳转到内容

XiHan.Framework.Scheduling.Abstractions ​

调度子系统的契约与引擎,包括作业与触发器、计划(间隔、cron、一次、手动)、排除日历、misfire、经内核管道的派发、作业存储契约 IJobStore、集群分发引擎 JobSchedulerEngine 与租约式领导选举 ILeaderElection。能力 scheduling.jobs@1 与 scheduling.leader@1 由 Provider 提供,应用和 Provider 都引用本包。

项内容
包 IDXiHan.Framework.Scheduling.Abstractions
环层Ring 2(抽象)
能力定义 scheduling.jobs@1(SchedulingCapabilities.Jobs)与 scheduling.leader@1(SchedulingCapabilities.Leader)的契约,本包不提供能力
特性入口—
配置节XiHan:Scheduling(SchedulerOptions,由 Provider 读取)
依赖XiHan.Framework.Kernel、XiHan.Framework.Localization.Abstractions、XiHan.Framework.Messaging.Abstractions;Microsoft.Extensions.Configuration.Abstractions、Microsoft.Extensions.DependencyInjection.Abstractions、Microsoft.Extensions.Logging.Abstractions
AOT兼容
稳定性Preview

概述 ​

引擎和全部协议性规则都在本包里(ADR-0020),包括计划推算、misfire、并发阻塞、暂停与恢复、重试与执行时限、失联恢复、运行历史与事件。Provider 只实现两个窄契约:作业存储 IJobStore(作业、触发器、运行、节点的读写,以及原子抢占、独占开始、失联恢复)与领导选举 ILeaderElection,再调用 AddJobSchedulerEngine(options) 装配引擎、在特性的生命周期里启停它。所以内存实现与数据库实现的语义只有一份,由 Scheduling.Conformance 的三个套件约束。

每次运行由 JobDispatcher 在新作用域、新调用上下文里经应用的完整管道执行:关联 ID 是运行 ID,调用来源是 InvocationProtocols.Job,目标是作业名。作业返回失败或抛出异常都记为失败,异常翻译为 SCHEDULING_UNEXPECTED_001。

本包还被 Workflow.Abstractions 引用,用它的 CronExpression 实现延迟与定时启动。

何时使用 ​

  • 编写作业、登记作业与日历、在运行时管理作业:引用本包,再装配一个提供 scheduling.jobs 的 Provider。
  • 实现新的作业存储或领导选举 Provider:实现 IJobStore / ILeaderElection 并继承 Scheduling.Conformance 的套件。
  • 只想运行作业:本包没有可装配的特性,单独引用不会运行任何东西,需要 Scheduling.InMemory 或 Scheduling.Database。

安装与启用 ​

bash
dotnet add package XiHan.Framework.Scheduling.Abstractions --prerelease

当前是预览版,安装要带 --prerelease;从源码或本地包源引用见快速上手。各 Provider 包与 Profiles.Full 都传递引用本包。登记作业:

csharp
using XiHan.Framework.Primitives;
using XiHan.Framework.Scheduling.Abstractions;
using XiHan.Framework.Scheduling.Abstractions.Jobs;
using XiHan.Framework.Scheduling.Abstractions.Schedules;

var builder = WebApplication.CreateBuilder(args);
// 代码作业:同时登记类型
builder.Services.AddJob<CleanupJob>("cleanup.nightly", JobSchedule.FromCron("0 3 * * *", "Asia/Shanghai"));
// 只登记类型:供运行时 ScheduleAsync 新建的作业引用
builder.Services.AddJobType<ArchiveJob>();

public sealed class CleanupJob : IJob
{
    public ValueTask<Result> RunAsync(JobRun run, InvocationContext context, CancellationToken cancellationToken)
        => ValueTask.FromResult(Result.Success());
}

public sealed class ArchiveJob : IJob
{
    public ValueTask<Result> RunAsync(JobRun run, InvocationContext context, CancellationToken cancellationToken)
        => ValueTask.FromResult(Result.Success());
}

配置 ​

SchedulerOptions.FromConfiguration 读 XiHan:Scheduling,值不合法时抛 ArgumentException:

键类型缺省说明
XiHan:Scheduling:PollIntervalSeconds正数1轮询周期
XiHan:Scheduling:NodeId字符串机器名:进程 ID:8 位随机节点身份,进程内固定
XiHan:Scheduling:MaxConcurrentRuns≥ 132本节点同时运行的上限
XiHan:Scheduling:AcquireBatchSize≥ 116一个周期最多抢占的触发器数;每周期名额 = min(AcquireBatchSize, MaxConcurrentRuns − 本节点运行中数量)
XiHan:Scheduling:MisfireThresholdSeconds≥ 060misfire 阈值
XiHan:Scheduling:LeaseResource作业名文法scheduling.jobs领导租约资源名
XiHan:Scheduling:LeaseTimeToLiveSeconds正数30领导租约存活时间
XiHan:Scheduling:NodeTimeoutSeconds正数90心跳超过它即失联
XiHan:Scheduling:HousekeepingIntervalSeconds正数60领导者维护(失联恢复、历史清理)周期
XiHan:Scheduling:HistoryRetentionDays≥ 07运行历史保留天数,0 为不删
json
{
  "XiHan": {
    "Scheduling": {
      "PollIntervalSeconds": 1,
      "NodeId": "node-1",
      "MaxConcurrentRuns": 32,
      "AcquireBatchSize": 16,
      "MisfireThresholdSeconds": 60,
      "HistoryRetentionDays": 7
    }
  }
}

主要类型 ​

类型说明
IJob作业:RunAsync(JobRun, InvocationContext, CancellationToken) → ValueTask<Result>,作用域服务
JobName、TriggerKey作业名(^[a-z0-9][a-z0-9._-]{0,127}$,可从字符串隐式转换);触发器键 (Job, Trigger)
JobDefinition作业定义:Name、JobType、Triggers、Data、AllowConcurrent、RunOnEveryInstance、RequestsRecovery、Retry、Timeout、Description
JobTriggerDefinition触发器:Name、Schedule、Data、Priority(缺省 5)、MisfirePolicy、Description
JobRetryPolicy、JobRetryBackoff重试:MaxRetries 1–100、Delay、Fixed / Exponential、MaxDelay,等待最长 30 天;DelayFor(n)
JobSchedule、JobScheduleKind计划:Manual、Every、FromCron、Once,加 StartAt / EndAt / Calendar;ToString / Parse 文本往返
CronExpression秒精度 cron:5 / 6 / 7 段、?、L、W、#、名字、时区与夏令时;星期 0/7 = 周日
MisfirePolicyRunOnce(缺省)、Skip、RunAll
IJobCalendar、HolidayCalendar、WeeklyCalendar、DailyCalendar、CronCalendar、CompositeCalendar、JobCalendars、JobPlanner排除日历与按日历推算下一次
JobRun、JobTrigger一次运行(Id、Name、TriggerName、ScheduledAt、Trigger、Attempt、RetryOf、Data);触发来源 Schedule / Manual / Recovery / Retry
JobDispatcher经管道执行一次运行;GetRun(context) 在管道内取当前运行
IJobScheduler、JobSchedulerExtensions运行时管理:登记、删除、暂停、恢复、触发、入队、取消、状态、历史、计数、恢复为代码定义;全部批量,单条是扩展方法
JobTriggerRequest、JobTriggerResult手动触发的请求(名字与数据)与逐项结果
JobStatus、JobTriggerStatus、JobOutcome作业状态(含 NextRun、Paused、IsRunning、Last)、触发器状态、一次运行的结果
JobRunRecord、JobRunQuery、JobRunState运行历史记录、查询(Limit 缺省 100、最大 1000;越界由 RunsAsync 返回 SCHEDULING_VALIDATION_002)、状态 Running / Succeeded / Failed / Canceled / Lost
JobSource作业来源:Code、Runtime、Overridden
JobRegistry、JobTypeRegistry代码作业登记表;作业类型登记表(Keys 列出可选类型)
JobSchedulerEngine、SchedulerOptions引擎(同时是 IJobScheduler)与选项;NodeId、IsLeader、IsRunning、RunningCount、PollOnceAsync(测试与一致性套件用:执行一个轮询周期,并等本周期起的运行连同收尾结束)
IJobStore、JobRecord、TriggerRecord、TriggerState、SchedulerNodeRecordProvider 契约与存储形态
ILeaderElection、LeaderLease租约式领导选举
SchedulingTopics、SchedulingEventDatascheduling.job.* / scheduling.run.* 事件
SchedulingErrors、SchedulingMessages、SchedulingCapabilities错误码、消息键(目录 scheduling)、能力 ID

SchedulingServiceCollectionExtensions 提供 AddJob<TJob>(两个重载)、AddJobType<TJob>(两个重载)、AddJobCalendar、GetJobRegistry、GetJobTypeRegistry、GetJobCalendars,以及给 Provider 用的 AddJobSchedulerEngine(options)。

使用示例 ​

带两个触发器、数据与重试的作业:

csharp
using System.Text.Json.Nodes;
using XiHan.Framework.Primitives;
using XiHan.Framework.Scheduling.Abstractions;
using XiHan.Framework.Scheduling.Abstractions.Calendars;
using XiHan.Framework.Scheduling.Abstractions.Jobs;
using XiHan.Framework.Scheduling.Abstractions.Schedules;

var builder = WebApplication.CreateBuilder(args);
builder.Services.AddJobCalendar("weekends", new WeeklyCalendar());
builder.Services.AddJob<ExportJob>(new JobDefinition("exports.daily", typeof(ExportJob))
{
    Triggers =
    [
        new JobTriggerDefinition("morning", JobSchedule.FromCron("0 30 8 * * *", "Asia/Shanghai") with { Calendar = "weekends" }),
        new JobTriggerDefinition("hourly", JobSchedule.Every(TimeSpan.FromHours(1))) { Data = new JsonObject { ["mode"] = "delta" }, Priority = 2 },
    ],
    Data = new JsonObject { ["mode"] = "full" },
    Retry = JobRetryPolicy.Fixed(5, TimeSpan.FromMinutes(2)),
    Timeout = TimeSpan.FromMinutes(15),
});

public interface IExporter
{
    ValueTask<Result> ExportAsync(string mode, CancellationToken cancellationToken);
}

public sealed class ExportJob(IExporter exporter) : IJob
{
    // hourly 触发器的数据覆盖作业数据的同名键 mode
    public ValueTask<Result> RunAsync(JobRun run, InvocationContext context, CancellationToken cancellationToken)
        => exporter.ExportAsync(run.Data["mode"]?.GetValue<string>() ?? "full", cancellationToken);
}

IExporter 的实现由应用注册。

手动触发并读取结果:

csharp
using XiHan.Framework.Primitives;
using XiHan.Framework.Scheduling.Abstractions.Jobs;
using XiHan.Framework.Scheduling.Abstractions.Schedulers;

public sealed class ExportRunner(IJobScheduler scheduler)
{
    public async ValueTask<string?> RunNowAsync(CancellationToken cancellationToken)
    {
        // 在本实例运行并等待;作业正在运行且不允许并发时返回 SCHEDULING_CONFLICT_001
        Result outcome = await scheduler.TriggerAsync(new JobName("exports.daily"), cancellationToken);
        return outcome.Error?.Code;
    }
}

行为与限制 ​

  • 名字、计划、策略的取值在构造时校验,越界抛 ArgumentException / ArgumentOutOfRangeException:作业名最长 128;Every 的间隔为正、repeatCount 至少 1;MaxRetries 1–100;Delay、MaxDelay、Timeout 大于零且不超过 30 天。运行历史查询的分页例外:JobRunQuery 的 Skip 与 Limit 不在构造时校验,越界由 RunsAsync 返回 SCHEDULING_VALIDATION_002(HTTP 400),因为它常由请求绑定而来。
  • cron 向前最多搜索 5 年;排除日历连续排除 10000 个候选时刻即视为没有下一次。
  • 本包不做持久化:运行时登记的作业、历史、节点放在哪里由 IJobStore 决定。IJobStore.IsShared 为假时,集群唯一的作业只在持有领导租约的实例上按计划运行。
  • 引擎在应用不在服务状态时(进入服务状态之前、开始排空之后)不按计划运行,领导租约也不拿不续,持有就立即释放,另一个实例下一个周期接手(ADR-0091);停止时等待进行中的运行,宿主停止令牌触发后取消它们并再等它们收尾,最后释放租约。ILeaderElection 的 Provider 要保证应用不在服务状态时 LeaderLease.ReleaseAsync 照样生效。
  • 执行时限靠取消令牌实现,不响应取消的作业要等它返回才结束。
  • 一次运行用到的数据按「作业数据 ← 触发器数据 ← 手动触发数据」逐键覆盖;超过 Timeout 取消令牌,作业返回之前时限已过的运行记为 SCHEDULING_TIMEOUT_001。
  • 重试:被取消与失联的运行不重试;重试是新的一次运行,JobRun.Trigger 为 Retry、Attempt 加一、RetryOf 指向首次运行。集群唯一的作业把重试写成存储里的一次性触发器,RunOnEveryInstance 的作业在本实例进程内等待后重试。
  • cron 是 Quartz 风格的文法(5 / 6 / 7 段、?、L、W、#、名字、时区与夏令时),但不是 Quartz 的超集:星期数字按 Unix 约定(0 / 7 = 周日、1 = 周一,Quartz 是 1 = 周日、7 = 周六);周段单独的 L(Quartz 里是周六)与反向范围(如 FRI-MON、22-2,Quartz 跨界回绕)不支持、解析报错;多了 5 段写法,日与周同时限定时取并集。
  • 代码登记的作业被运行时 ScheduleAsync 改过后来源是 Overridden:启动同步时代码定义没变(指纹 CodeFingerprint 相同)就保留运行时的修改,代码改了以代码为准并记警告;RestoreAsync 随时恢复为代码定义(ADR-0053)。
  • 每次运行在新作用域、新调用上下文里经完整内核管道执行,JobDispatcher 把关联 ID 设为运行 ID。
  • 事件只在装配了消息能力时发布,发布失败只记日志。
错误码含义
SCHEDULING_NOTFOUND_001作业未登记
SCHEDULING_NOTFOUND_002作业类型未登记
SCHEDULING_NOTFOUND_003触发器不存在
SCHEDULING_NOTFOUND_004运行不存在或已结束
SCHEDULING_NOTFOUND_005作业没有代码定义,不能恢复
SCHEDULING_VALIDATION_001作业定义不合法(触发器名重复、日历未登记)
SCHEDULING_VALIDATION_002运行历史查询的分页越界(SchedulingErrors.RunQueryInvalid,HTTP 400)
SCHEDULING_CONFLICT_001作业正在运行且不允许并发
SCHEDULING_CONFLICT_002领导租约被占用
SCHEDULING_CONFLICT_003运行被取消
SCHEDULING_UNAVAILABLE_001调度器未运行:本实例的调度器尚未启动或已开始停止,TriggerAsync 不起新的运行
SCHEDULING_UNAVAILABLE_002领导选举的存储不可达
SCHEDULING_UNAVAILABLE_003作业存储不可达
SCHEDULING_UNAVAILABLE_004作业存储容量已满
SCHEDULING_TIMEOUT_001运行超过执行时限
SCHEDULING_UNEXPECTED_001作业抛出未处理异常

相关 ​

Released under The MIT License