Download Latest Version v4.1.1 source code.zip (9.1 MB) Google Add to Preferred Sources
Home / v3.22.0
Name Modified Size InfoDownloads / 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/AddTrigger and the store already holds them. The declared trigger was applied as a reschedule, which went through ReplaceTrigger and deleted every fired-trigger row of the key — before Start(), 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: ReplaceTrigger now 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 stored BLOCKED behind an execution still in flight, as any trigger of that job is, where it used to be stored WAITING and 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 the EXECUTING row 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 BLOCKED while an execution is in flight.
  • quartz.jobStore.commandTimeout bounds 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 keeps TRIGGER_ACCESS locked, 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, DbRetryInterval back-off and the SchedulerError notification never ran, and the cluster stopped firing without logging anything (#3763). The setting is a millisecond count like every other duration on the store; 0 means 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 through DelegateInitializationArgs and the lock handler through DBSemaphore.CommandTimeout, which the store writes once its handler is known — so a handler named by quartz.jobStore.lockHandler.type is 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 as JobStore: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 than DbRetryInterval, 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's LastCheckin, which a failed read also stamps. Defaults and the 15-second failover latency are unchanged, and threshold >= DbRetryInterval is no longer a relation an operator has to know about. The configuration reference's clusterCheckinInterval default (7500, not 15000) and the missing clusterCheckinMisfireThreshold row were fixed on the way. (port of a5b2197e5d)
  • Behavior change worth noting: after a failed check-in the next attempt comes sooner than DbRetryInterval while 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

Source: README.md, updated 2026-09-11