| Name | Modified | Size | Downloads / Week |
|---|---|---|---|
| Parent folder | |||
| Quartz.NET-3.22.0.zip | 2026-09-14 | 51.7 MB | |
| README.md | 2026-09-11 | 5.6 kB | |
| v3.22.0 source code.tar.gz | 2026-09-11 | 5.5 MB | |
| v3.22.0 source code.zip | 2026-09-11 | 6.2 MB | |
| Totals: 4 Items | 63.5 MB | 0 | |
Quartz.NET 3.22.0 carries three fixes to the persistent store's recovery and clustering paths, each found on the 4.0 line and ported here. One of them adds a setting the store never had — a timeout on the statements it issues — which is the public-surface addition that makes this a minor rather than a patch. The schema is 3.20's. Two of the changes alter behaviour, each marked Behavior change worth noting below.
:::shell
dotnet add package Quartz --version 3.22.0
What changed
- A job that requested recovery is recovered after a hard kill when the application declares its jobs and triggers through
AddJob/AddTriggerand the store already holds them. The declared trigger was applied as a reschedule, which went throughReplaceTriggerand deleted every fired-trigger row of the key — beforeStart(), so by the time the first cluster check-in or the non-clustered sweep looked for the interrupted execution, its row was gone. A replacement is not a removal:ReplaceTriggernow deletes the trigger row only, and unscheduling still takes the fired rows with it. The replacement of a trigger whose job disallows concurrent execution is storedBLOCKEDbehind an execution still in flight, as any trigger of that job is, where it used to be storedWAITINGand could fire alongside. A fast restart under a stable instance id had a second defect: on its first check-in a node handed its own state row to recovery, and the deferral grace period judged that row by its old timestamp, so theEXECUTINGrow of a serial job was preserved for a second detection that never came — the node's own record is never deferred now. (#3759, port of e60bd26eeb) - Behavior change worth noting: replacing a trigger keeps its fired-trigger rows, and the replacement of a non-concurrent job's trigger is stored
BLOCKEDwhile an execution is in flight. quartz.jobStore.commandTimeoutbounds every statement the store issues, the lock statement included. A row lock belongs to the database session that took it, so a node that loses its network without the server noticing keepsTRIGGER_ACCESSlocked, and every other node's next lock statement queues behind that dead session — and nothing on this branch bounded the wait: the statement never failed, so the lock handler's retries,DbRetryIntervalback-off and theSchedulerErrornotification never ran, and the cluster stopped firing without logging anything (#3763). The setting is a millisecond count like every other duration on the store;0means the provider's own default (30 seconds for most), a negative value is refused where it is configured, and a value past what ADO.NET can hold in whole seconds is refused for the same reason. It is rounded up to whole seconds, because rounding down would turn a sub-second value into "wait forever". It reaches the driver delegate throughDelegateInitializationArgsand the lock handler throughDBSemaphore.CommandTimeout, which the store writes once its handler is known — so a handler named byquartz.jobStore.lockHandler.typeis bounded too. An unconfigured store imposes nothing.SchedulerBuilder's persistent-store options set it fluently. The troubleshooting page gains the dead-session case: how to tell it apart in each database, why killing a process does not reproduce it, and the server-side settings that are the only thing that frees the lock. 4.x has the same setting asJobStore:CommandTimeout. (#3764, [#3765])- A failed check-in is retried inside the window its peers give it. A peer writes a node off once interval + threshold has passed since the row it last wrote — 15 s on the defaults — and the cluster manager's sleep after a failed check-in was floored at
DbRetryInterval, also 15 s, so a check-in that failed at 7.5 s was next attempted after the peers had already recovered the node: its fired-trigger rows deleted, its recovering jobs fired again,[DisallowConcurrentExecution]no longer holding. Java Quartz sleeps the same way, so this is inherited rather than a regression. While the window is still open a failed check-in is now retried inside it — half of what is left each time, never later thanDbRetryInterval, never sooner than the loop's pause — and only once it has closed does the ordinary back-off apply. The manager times the window from its own record of the last check-in that reached the database, not from the store'sLastCheckin, which a failed read also stamps. Defaults and the 15-second failover latency are unchanged, andthreshold >= DbRetryIntervalis no longer a relation an operator has to know about. The configuration reference'sclusterCheckinIntervaldefault (7500, not 15000) and the missingclusterCheckinMisfireThresholdrow were fixed on the way. (port of a5b2197e5d) - Behavior change worth noting: after a failed check-in the next attempt comes sooner than
DbRetryIntervalwhile the peers' window is still open.
Public API — additive only
JobStoreSupport.CommandTimeout,DBSemaphore.CommandTimeout,AdoUtil.CommandTimeout,DelegateInitializationArgs.CommandTimeout,SchedulerBuilder.PersistentStoreOptions.CommandTimeout— the one setting, wherever a statement is prepared.
Upgrading
dotnet add package Quartz --version 3.22.0. Nothing to migrate; the schema is unchanged. The 4.x line is the current major and carries all three fixes as well; the 4.x migration guide is the way there.
Full changelog: https://github.com/quartznet/quartznet/compare/v3.21.0...v3.22.0