Skip to content

Update dependency Quartz.AspNetCore to v4 - #1373

Open
renovate[bot] wants to merge 1 commit into
dev8from
renovate/major-quartznet-monorepo
Open

Update dependency Quartz.AspNetCore to v4#1373
renovate[bot] wants to merge 1 commit into
dev8from
renovate/major-quartznet-monorepo

Conversation

@renovate

@renovate renovate Bot commented Sep 3, 2026

Copy link
Copy Markdown
Contributor

ℹ️ Note

This PR body was truncated due to platform limits.

This PR contains the following updates:

Package Change Age Confidence
Quartz.AspNetCore (source) 3.14.04.1.0 age confidence

Release Notes

quartznet/quartznet (Quartz.AspNetCore)

v4.1.0

Quartz.NET 4.1 is an additive minor: every public change is a new type, a new member on a type we own, or a default interface member; the schema is 4.0's; a 4.0 application upgrades by changing the version. The headline is that a scheduler can now be added to, removed from and restarted in a running container — the one item the 4.0 roadmap deferred as "a 4.1 concern" — and around it the additive follow-ups filed during the 4.0 release candidates, an API reference for 4.x, and an honest Wolverine page now that Wolverine has a cron of its own.

dotnet add package Quartz --version 4.1.0
Highlights
  • Schedulers can be added, removed and restarted while the host runsISchedulerRuntime (it extends ISchedulerRegistry, as the 4.0 guide promised) builds a scheduler from a recipe into a container of its own that resolves the application's services, jobs and options from the application's container, binds it where the HTTP API, the dashboard and the health checks already look, drains it before it is replaced, and refuses the cases that cannot be made safe: a name the container registered, a bound name, a recipe that supplies an instance part, a restart whose drain gave up. ISchedulerFactory.LookupScheduler now builds a registered-but-unstarted scheduler on lookup. A restart builds the next generation first (a configuration error leaves the old one running), drains the old one within SchedulerRestartOptions.DrainTimeout (30 s by default) and only then initialises the new store — because the store's recovery sweep is per scheduler name, not per instance — and a drain that gives up leaves the name shut down and reported with no status until the next Restart finishes the job. A recipe that hands over a job store, thread pool, job factory or instance-id generator as an object is refused before anything is built, since a shut-down instance cannot be re-initialised. (#​3338; add/remove #​3725, restart #​3734)
  • CronMON/2 parses and means what 2/2 means (a step through the week, not 3.x's fortnight), and MON-FRI/2 is 2-6/2 where 4.0 silently read it as MON-FRI. (#​3732, #​3733 [85214e4…e29f7c4283]). The century walk of CronCalendar.GetNextIncludedTimeUtc (#​3690) shipped in 4.0.1.
  • HTTP APIPOST …/triggers/{group}/{name}/update-details edits a trigger in place (description, priority, job data, calendar, misfire instruction with its family, preferred node, execution group, retry policy; a field absent from the body is left alone and a null clears it), and HttpScheduler.UpdateTriggerDetails no longer throws — Context and ListenerManager are the two members a wire cannot carry (#​3681, #​3735). take says in the OpenAPI document why it is a string: a page size, or all for everything up to MaxPageSize (#​3682, #​3736). GetStatus(ct) and GetSchedulerInstanceId(ct) are the asynchronous twins of the two IScheduler properties a wire cannot answer without blocking — default interface members that answer the property locally, one round trip on HttpScheduler, and on the container's deferred scheduler they build it on first use where the properties throw (#​3684, #​3738). QuartzHttpApiOptions.IsJobTypeAllowed and its dashboard twin let an operator name which job types a request may schedule — a predicate over the type name as the request spelled it, never a resolved type; a refused name is a 403 naming the type and a warning in the log, and the default is unchanged (#​3685, #​3741).
  • Scheduling files — a trigger's preferred node can be declared in quartz_jobs.json, in the Quartz:Schedule section and in the XML file (<preferred-node>; * pins it to whichever node fires it first), and the XML format catches up with <execution-group> and <retry-policy>. The schema stays 2.0: the elements are optional and a 3.x file loads unchanged. The freeze on the XML format is redrawn as a freeze on trigger kinds, not settings. (#​3683, #​3739)
  • API reference for 4.x is published again at https://docs.quartz-scheduler.net/apidoc/4.x/, generated with docfx from the XML documentation every public member carries, and a doc comment that no longer matches its member fails the docs build. (#​3686, #​3740)
  • A scheduler reached over HTTP is a supported dashboard target, history includedAddQuartzDashboard() beside AddQuartzHttpClient("acme", …) now fronts the scheduler in the other process honestly: it is listed with the new origin Remote, every page drives it, the History page and the misfire tile read the history the target's process keeps, and Live Logs says what a wire does not carry yet. Underneath: the scheduler repository no longer reads a remote scheduler's Status while holding its lock — one unreachable target used to stall every scheduler lookup in the process, the HTTP API's own included, for the client's timeout — the registry asks all schedulers concurrently under one two-second budget and reports Unknown for the ones that did not answer, the pages page instead of asking for everything, and a second AddQuartzHttpClient under the same name is refused rather than silently winning. Execution history is now Quartz's rather than the dashboard's: IExecutionHistoryStore in core with an in-memory default bounded by age and count, a recorder AddQuartzExecutionHistory() installs against every scheduler, three GET …/schedulers/{name}/history/… routes the HTTP API serves, and a read-only reader of them that AddQuartzHttpClient registers per target. QuartzHttpApiOptions.ReadOnly turns the whole mutating surface of the API off in one switch — mutation is a property of the route, so the two bulk fetches stay reads. The design for the rest — live events over the wire, an audit of operator actions, store-attached and dial-out targets, a fleet pane — is recorded on #​3387 and filed as 4.2 issues (#​3768#​3775). (#​3387, #​3776)
  • Who changed what is on record. Every mutating HTTP API request that succeeds logs one Information line, 9007, naming the caller (HttpContext.User.Identity.Name, or (anonymous)), the operation, the scheduler and the route — until now a POST …/shutdown that worked was recorded nowhere. The dashboard's Action Log entries and their 9100/9101 lines now say where an action landed: the scheduler's origin, the node behind it, and whether the action was node-local (start, stand-by, shutdown and interrupting a firing) — and interrupting a firing from Currently Executing is recorded at all, which it was not. (#​3769, #​3784)
  • The Wolverine page no longer says Wolverine has no cron: since 6.34 it does (opts.Schedules.ScheduleRecurring, over Cronos), and the page now puts the two side by side in one table, says plainly when Wolverine's own schedule is the right answer, and keeps the recipe for what a Quartz trigger adds — with a seventh example part running Wolverine's schedule beside Quartz's. (#​3719, #​3737)
Public API — additive only
Added What it is
ISchedulerRuntime : ISchedulerRegistryAdd, Remove, Restart schedulers in a running container; resolve either interface and you get the same object
SchedulerAddOptions, SchedulerRestartOptions (readonly record structs), SchedulerRestartException their options and the one refusal a caller can act on
IScheduler.GetStatus(ct), IScheduler.GetSchedulerInstanceId(ct)default interface members the asynchronous twins of the two properties; a scheduler written outside this repository needs no change
HttpScheduler.GetStatus / GetSchedulerInstanceId overrides one round trip each, no blocked thread
QuartzHttpApiOptions.IsJobTypeAllowed, QuartzDashboardOptions.IsJobTypeAllowed an allow-list over the job-type name a request spells
SchedulerOrigin.Remote; SchedulerRegistration.SchedulerInstanceId (init) a scheduler reached through a proxy, and the node a listing asked it for — asynchronously, under a deadline, never through the blocking property
QuartzHttpApiOptions.ReadOnly every mutating route answers 403 problem details; the two bulk fetches stay reads
QuartzHttpApiOptions.EventStreamHeartbeatInterval how long the event stream may be silent before a heartbeat frame; 15 s by default, set it below the proxy's idle timeout
IExecutionHistoryStore, ExecutionHistoryEntry, MisfireHistoryEntry, ExecutionHistoryQuery, MisfireHistoryQuery, ExecutionHistoryOptions, AddQuartzExecutionHistory() the history seam in core; IDashboardHistoryStore keeps working as its adapter in both directions

Nothing was removed or reshaped (v4.0.1..v4.1.0 baseline diff: 96 added lines, 0 removed). The one dependency change: Quartz now depends on Microsoft.Extensions.Logging.Abstractions rather than Microsoft.Extensions.Logging; an application that took LoggerFactory or AddLogging transitively from Quartz adds the package itself (#​3730, #​3745).

Behaviour changes worth noting
  • MON/2 parses (4.0 refused it; 3.x read it as every second week) and MON-FRI/2 fires on three days, not five — neither can be reported to you, both parse; the migration guide's 4.0 → 4.1 section has the audit query. A QuartzSchedulerBuilder with a provider registered but no factory refuses to build (see #​3730). GetNextInvalidTimeAfter returns null where it used to return a valid instant (4.0.1). A shut-down container scheduler injected as IScheduler re-points to the generation ISchedulerRuntime.Restart built. Mapping the HTTP API now records execution history in memory, bounded (24 h, 2,000 rows per scheduler); ExecutionHistoryOptions.MaxEntriesPerScheduler = 0 records nothing. AddQuartzHttpClient throws on a scheduler name it already registered. A 4.0 client parsing GET …/schedulers from a 4.1 host that fronts a proxy sees the origin "Remote" it does not know — upgrade the client. AddQuartzDashboard() no longer registers DashboardLiveEventsPlugin; a reverse proxy needs to forward {DashboardPath}/hub only for SignalR clients of your own, not for the pages. Log events 9100 and 9101 end with (origin …, node …) — a pipeline parsing the old template needs the new one.
Also
  • A failed cluster check-in is retried inside the window its peers give it — a peer writes a node off once CheckinInterval + CheckinMisfireThreshold 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 one database blip during a check-in had a live node recovered by its peers: its next row went out at 22.5 s, 7.5 s after they had stopped trusting it. Inherited from Java Quartz. A failed check-in is now retried inside what is left of the window — half of it each time, never later than DbRetryInterval — and only once the window has closed does the ordinary back-off apply; the retries are timed from the last check-in that reached the database, not from the stamp a failed read of the state table leaves. Defaults are unchanged, so the 15-second failover latency stays. CheckinMisfireThreshold past the timer ceiling is now refused at startup, like CheckinInterval. (#​3777, #​3778; 3.22 carries the same change on 3.x)
    • Behavior change worth noting: a node whose check-in fails makes up to ~9 connection attempts and logs three error lines in the first 15 s of an outage instead of one, then backs off DbRetryInterval as before.
  • A shutdown that does not wait for its jobs no longer loses a firing — the new two-node Redis fixture caught it: Shutdown(waitForJobsToComplete: false) closed the thread pool to new work before the scheduler loop had finished its iteration, so a firing the loop had already committed to the store (fired-trigger row written, trigger advanced) was refused by the pool and never ran — only RequestRecovery would ever have replayed it — and a completion reported after the store had closed was aborted, leaving the trigger BLOCKED for a peer's cluster recovery to settle. The loop is now halted and awaited before anything it depends on is torn down, and an unwaited shutdown gives the executions it dispatched two seconds to report their completions before the store closes; the waited form drains as before. On the fixture: 160 of 160 firings with a node leaving mid-run without waiting. (#​3746, #​3754)
    • Behavior change worth noting: Shutdown(waitForJobsToComplete: false) — the hosted service's default — can take up to two seconds longer when a job is in flight.
  • A job that requested recovery is recovered after a hard kill when the application declares its jobs and triggers through AddJob/AddTrigger. Declared content is applied at creation, and a trigger the store already held was applied as a reschedule that deleted every fired-trigger row of the key — before Start(), so the recovery sweep found nothing to recover. ReplaceTrigger now deletes the trigger row only; unscheduling still takes the fired rows with it, and the replacement of a trigger whose job disallows concurrent execution is stored BLOCKED behind an execution still in flight rather than WAITING beside it. The clustered case had a second defect: a node's first check-in handed its own state row to recovery, and the deferral grace period judged that row by its old timestamp — the node's own record is never deferred now. A SQLite reproduction of the restart runs without Docker. (#​3759)
  • A lock wait says how long it has been waiting, and every statement can be bounded. A lock statement queued behind a dead database session — a node whose network was cut while it held TRIGGER_ACCESS — returns nothing and throws nothing, so a cluster stopped scheduling in silence. DbLockHandler now logs event 3716 once per acquisition that outlives AdoJobStoreOptions.LockWaitWarningThreshold (30 s by default; null reports nothing), every acquisition is timed on quartz.jobstore.lock.wait.duration, and quartz.jobStore.commandTimeout — which 3.22 adds — translates to AdoJobStoreOptions.CommandTimeout where it used to be accepted and silently dropped. Neither ends the wait; a command timeout does, and the troubleshooting page says how to tell the case apart in each database and which server-side settings free the lock. (#​3764, #​3766)
  • A failed check-in is retried inside the window its peers give it. A peer writes a node off once interval + threshold has passed (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: fired rows deleted, recovering jobs fired again, [DisallowConcurrentExecution] no longer holding. Inherited from Java Quartz rather than a regression. While the window is open a failed check-in is retried inside it, half of what is left each time; only once it has closed does the ordinary back-off apply. Defaults and the documented 15-second failover latency are unchanged, and threshold >= DbRetryInterval is no longer a relation an operator has to know about. The 3.x configuration reference's clusterCheckinInterval default (7500, not 15000) and the missing clusterCheckinMisfireThreshold row were fixed on the way. (a5b2197)
  • Two schedulers locking through Redis are proven not to double-fire — the redis integration leg now runs two clustered nodes over one PostgreSQL store with RedisLockHandler as the only mutual exclusion, 250 firings across a [DisallowConcurrentExecution] job and an ordinary one, and asserts no firing doubled, none was lost, the non-concurrent job never overlapped and both nodes fired; a second fixture shuts one node down mid-run and checks the survivor and the closed multiplexer. 4.0.0 listed Quartz.Extensions.Redis as the least exercised package; it no longer is. (#​3722, #​3744)
  • 3.21.0 on the 3.x line carries the three fixes held back from 3.20.1 (see its notes).
  • A misspelled quartz.jobStore.* key fails startup instead of being ignored. 4.0 read the keys it knew into typed options and did nothing with the rest, so quartz.jobStore.dbRetryIntreval started the scheduler with the default in force and said nothing — which is also how quartz.jobStore.commandTimeout went missing before it was bridged. A persistent store now refuses a key under that prefix that nothing reads, by name, where a misconfiguration already fails: Unknown configuration property 'quartz.jobStore.dbRetryIntreval'. It is not a setting of the ADO.NET job store …. quartz.checkConfiguration = false allows it through, as for every other unknown key; a store of your own keeps failing in the binder that writes leftover keys onto it, as 3.x did. (#​3767)
    • Behavior change worth noting: a configuration file carrying a quartz.jobStore.* typo that 4.0 tolerated stops the scheduler from starting.
  • A zero-dependency Quartz.Cron package was sized (#​3720): mechanical at ~5,000 lines on net8.0;net10.0, and not shipped — no consumer would take it today. The report is on the issue.
  • Quartz depends on the logging abstractions, not the logging package — Quartz writes log events and never builds the pipeline that carries them, so AddQuartz registers ILogger<> and no ILoggerFactory at all; wherever it needs a factory it asks the container and falls back to LogProvider. Nine tests pin that the order of AddQuartz and AddLogging does not matter and that a host's providers are never dropped. A QuartzSchedulerBuilder handed a bare ILoggerProvider without AddLogging now refuses at Build() with the fix in the message, where it used to log through a factory Quartz no longer supplies. (#​3730, #​3745)

v4.0.1

Quartz.NET 4.0.1 is a maintenance release about getting to 4.0: nothing about how a trigger fires changed, the public API is untouched (the baselines did not move), and the schema is 4.0's. Five days after 4.0.0 an audit of every public dependency-bot pull request that touched a Quartz package found two reasons an upgrade never got as far as compiling, both of them ours, both fixable in a patch — one gap in the migration guide for F# — and, found by the rc.1 security gate and moved forward, a cron calendar that could take a century to answer.

dotnet add package Quartz --version 4.0.1

What changed

  • A consumer one servicing patch behind restores 4.0.1 — 4.0.0's package manifest floored every Microsoft.Extensions.* dependency at 10.0.11, the newest patch on the day it was built, so a project pinned at 10.0.9 or 10.0.10 hit NU1605 "detected package downgrade" before a line of source compiled. Every floor is now the lowest version of its major that the code compiles against and that carries no advisory: 10.0.0 for the framework extensions, 13.0.2 for Newtonsoft.Json (13.0.1 reflects over TimeOnly member by member and a job data map loses its seconds — a test said so), 1.15.3 for OpenTelemetry.Extensions.Hosting (the first version whose OpenTelemetry.Api carries no advisory), 3.0.0 for StackExchange.Redis and 7.0.0 for TimeZoneConverter. The library is built against those floors, so the claim is checked on every build, and a test refuses a floor that creeps up. (#​3717, c9ecf89)
  • A grouped dependency update can reach 4.x — the four packages 4.0 folded into Quartz (Quartz.Extensions.DependencyInjection, Quartz.Extensions.Hosting, Quartz.Serialization.SystemTextJson, and Quartz.Serialization.Json, whose successor is Quartz.Serialization.Newtonsoft) had no 4.0.0, so a bot that groups Quartz with any of them resolved the group to the newest version every member has — 3.20.1 — and closed the 4.0.0 pull request it had already opened as superseded, with green checks. From 4.0.1 those four ids carry an empty package at every 4.x version: no assembly, one dependency on the replacement, a readme that says to remove the reference. The migration guide's instruction stands — remove them — but the upgrade is now offered. Quartz.OpenTracing and Quartz.OpenTelemetry.Instrumentation deliberately have no such package: neither has a 4.x replacement, and an empty one would hide that. (#​3717, c28f213)
  • CronCalendar.GetNextIncludedTimeUtc no longer walks a century one second at a time — it asked CronExpression.GetNextInvalidTimeAfter for the end of an excluded run, and that method stepped through the run one second per full cron computation; for an expression that excludes everything (* * * * * ?) the walk ran to the give-up year, roughly three billion computations, on a public member. The next non-matching instant is now read off the expression's own field sets — second, minute, hour, day, month, year — with each skip verified against the time zone's clock so a repeated fall-back hour is never stepped over. An expression that fires every second answers null, and the calendar turns that into a SchedulerException naming the expression instead of hanging. The unchanged next-fire-time path measures the same as before; the changed method is 76–86 % faster on the benchmark corpus. (#​3690, a125809, 4d60904)
    • Behavior change worth noting: GetNextInvalidTimeAfter returns null where it used to return a valid instant after giving up, and a CronCalendar whose expression excludes every instant now fails fast with SchedulerException.
  • The migration guide has a section for F# — an F# implementer sees four errors no C# project does (FS0856 on IJob.Execute's arity, FS0041 on ScheduleJob overload resolution, Async.AwaitTask with a ValueTask, FS0039 for StdSchedulerFactory), each quoted with its fix, and every sample is copied from a compiled example project the build keeps honest. (#​3718, 1b97b49, aff4fea)

Upgrading

From 4.0.0: dotnet add package Quartz --version 4.0.1; nothing else. From 3.x: the 4.x migration guide is unchanged in substance; if your bot has been landing 3.20.1 "upgrades", this is the release that lets it offer 4.x.

Full changelog: quartznet/quartznet@v4.0.0...v4.0.1

v4.0.0

Quartz.NET 4.0 targets net10.0, is asynchronous and container-built throughout, and trims a public surface that had accumulated for a decade. It is a major version with extensive breaking changes and a mandatory schema migration. This page is the short form; the detail lives in the docs:

  • 4.x migration guide — start at Start here; every breaking change with before and after, an ordered runbook for a running deployment, and an appendix that indexes every removed name
  • Database schema changes — what to run, per version and per database
  • Before you go live — the production checklist
  • Quick start — if you are starting something new, none of the above applies
dotnet add package Quartz

Highlights

  • net10.0 only — no netstandard2.0 build, no Full Framework, no .config support.
  • The container builds the scheduler. Dependency injection and hosting are part of the core Quartz package; StdSchedulerFactory, quartz.config discovery and the process-global SchedulerRepository.Instance / DBConnectionManager.Instance are gone. Flat quartz.* keys still work, translated to typed options by the one component that understands them, and a misspelled key is refused with a message rather than ignored. Options are validated at startup, so a bad value fails Host.Build() with every failure listed.
  • IJob.Execute takes a CancellationToken, IJobFactory hands out a JobScope rather than a bare instance, every public Task became ValueTask, and every asynchronous member ends with a cancellation token.
  • Job store listings became queriesQueryJobs / QueryTriggers return a PagedResult<T> whose rows already carry what a listing needs, so a dashboard over a large schema no longer pays for the whole schema. The old call shapes remain as extension methods.
  • A cluster can be read from outside. TriggerState.Executing says whether a trigger's job is running anywhere in the cluster; fire instances, cluster nodes, execution groups, misfires and history are listings that say which node they came from; the health check notices a node whose own check-in has stopped.
  • Recurrence triggers (RFC 5545 RRULE), an HTTP API with an IScheduler client over it (the replacement for .NET Remoting, which is gone), a dashboard, typed job input (IJob<TInput>; UsingInput(input) on a registration, ScheduleJob<TJob, TInput>(input, at) for a one-off), a retry policy a trigger carries — persisted, cluster-safe, never burning a repeat count — job execution middleware, a [JobTimeout] attribute, execution groups with per-node or cluster-wide limits, node affinity, and a firing that links back to the trace that scheduled it.
  • Joining a transaction the application owns — the ADO job store can enlist in a transaction you started, so saving your data and scheduling the job that acts on it commit together or not at all.
  • Cron says what it means. A wildcard day field no longer swallows the other day field's restriction (0 15 10 1 * * is the 1st, not every day); a time the clocks skip fires when the gap ends; the parser refuses what it used to quietly reinterpret (1-5W, L-3 in day-of-week, MON,FRI#3, MON/2, steps of zero); the five-field Unix form and the @daily-style macros work everywhere an expression is read.
  • Daylight saving fire times changed. Interval cron expressions fire through both halves of a repeated fall-back hour; CalendarIntervalTrigger with PreserveHourOfDayAcrossDaylightSavings steps in local wall-clock time and no longer drifts in zones whose delta is not a whole hour; calendars mean the local day even where midnight itself moves. Review any schedule that crosses a transition.
  • Quartz.Aspire: builder.AddQuartzPersistentStore("quartz") turns an Aspire connection name into a configured persistent store with its telemetry and health check, with no Aspire.* dependency; a store can provision its own schema as it starts (ProvisionSchema()), safe under a cluster racing to start.
  • Safe by default. MapQuartzHttpApi() and MapQuartzDashboard() refuse to start unless the endpoints carry authorization or an explicit AllowAnonymous(); a scheduler can be authorized on its own name, and every call the dashboard makes is authorized where it is made.
  • Observability that a backend can subscribe to by name. ActivitySource("Quartz") and Meter("Quartz") (the names are public constants), nine instruments, spans on every store the same way, and every log line carrying a stable event id — 278 of them, catalogued on a generated page.
  • Trimming and native AOT are something CI runs. Quartz declares IsAotCompatible; a canary is published natively and run against a real store on three operating systems on every pull request; the remaining string-named paths are recorded and each has a documented alternative (#​3341).
  • Faster per firing than 3.20 on both stores — 1.5× faster and 2.4× less allocation on PostgreSQL, faster and 21 % less allocation on RAMJobStore (numbers below).
  • Every public member is documented and the compiler holds it there; every collaborator is handed a context object; any member added to a public interface in 4.x arrives as a default interface member. The seams are registered in Extending Quartz.
  • A much smaller public surface — the scheduler core, the ADO SQL and JobRunShell are internal, and non-public types are sealed. If something you relied on is gone, say so in an issue; these can be reopened.

Breaking changes, the ten to know before the guide

  1. net10.0 only.
  2. Quartz.Extensions.DependencyInjection, Quartz.Extensions.Hosting and Quartz.Serialization.SystemTextJson are part of Quartz; Quartz.Serialization.Json is Quartz.Serialization.Newtonsoft; Quartz.OpenTracing has no 4.x release, and OpenTelemetry.Instrumentation.Quartz produces nothing on 4.x — subscribe to the source and meter directly. The first error a mixed project produces is CS0433 (a type in both Quartz 4 and a 3.x satellite): remove the three references.
  3. StdSchedulerFactory, DirectSchedulerFactory and quartz.config are gone; AddQuartz(…) or QuartzSchedulerBuilder.Create(q => …) build a scheduler.
  4. TaskValueTask on nearly every member, and a CancellationToken on every asynchronous one.
  5. Quartz.Spi is Quartz.Extensibility, Quartz.Simpl merged into Quartz.Impl, the Newtonsoft types left the core namespaces. A string naming an old namespace still resolves, with a warning.
  6. Every listener callback is told which scheduler is calling; the three *Support base classes are gone (every member has a default body); a listener with a 3.x signature is refused at registration.
  7. Misfire instructions are per-family enums; TimeOnly and DateOnly replace TimeOfDay; TimeProvider replaces SystemTime; the semaphores are lock handlers.
  8. The cron rules above — audit stored expressions with the guide's query before upgrading; a newly refused form fails at load, loudly. The reverse is quiet: an expression from another six-field dialect that 3.x refused (no ? in either day field) now parses, and two shapes fire on a different day than Cronos would — a bare digit in day-of-week (Quartz numbers Sunday 1) and both day fields restricted (Quartz fires on the union). The guide's second audit finds them.
  9. The schema: columns that were optional on 3.x are required, the acquisition index is reshaped, one index is dropped, one table is added. The upgrade script runs while 3.x nodes are up; the index script runs once the last one is gone.
  10. The trigger's end time is the last instant at which it may fire, uniformly, and the daylight-saving fire times above.

Everything else — every renamed member, every sealed type, every removed constant — is in the guide, with the name you would have typed.

Fixes worth knowing about

The full list is spread over the six pre-release notes below. These change what a running cluster does without saying so, and most of them are as old as 3.x:

  • ResumeAll could unpause real trigger groups: it deleted the all-groups sentinel with a LIKE, and the sentinel's four underscores are wildcards.
  • Recovered triggers lost their priority, so recovered work was acquired last.
  • The misfire threshold instant was a misfire on one store and not on the other, and the ADO store disagreed with itself; one rule now, and the acquisition predicate is its exact complement (#​3462).
  • A trigger kept the wall clock whatever TimeProvider the scheduler was given (#​3456).
  • Calendars were the wrong day where midnight moves, and DailyCalendar could crawl for minutes to answer months late (#​3457, #​3466).
  • A job listener that threw synchronously wedged the firing, and its [DisallowConcurrentExecution] siblings behind it (#​3502); a firing whose listener notification failed was listed as executing forever; a trigger with nothing left to fire was left behind when its last firing was abandoned (#​3507).
  • Firebird's serialization failures were never retried (#​3454).
  • A trigger stored as a plain object graph lost its time zone under Newtonsoft (#​3494), and a job data value the JSON format could not read back went into the database anyway (#​3495) — both serializers now refuse at write time.
  • A job whose trigger was re-applied by the XML processor fired twice at startup (#​3554).
  • A sub-millisecond repeat interval was stored as zero and wedged the trigger in ACQUIRED for good (#​3673, on every 3.x version too).
  • RAMJobStore notified its listeners inside its own lock, so a listener that touched the store deadlocked (#​3472).
  • A paused job group did not bind jobs added to it afterwards on the persistent store; a blank calendar name silently killed a trigger (#​3294); MySQLDelegate forced an index the misfire sweep could not use.
  • A process that cannot load the job classes can still edit their schedules. Rescheduling and updating a trigger on the persistent store resolved the job class and failed without it; the two attribute flags now come from the row, so an administration node needs no ITypeLoader of its own (#​3705). Stopping the host twice at once — which host.StopAsync() during RunAsync() does — no longer throws out of RunAsync (#​3701).

Numbers

Measured on one shared machine with other work running, the same harness on both branches (src/Quartz.Benchmark; the 3.20 half is in the tree under baseline-3x/):

Store MaxConcurrency 3.20 per firing 4.0 per firing
PostgreSQL 10 9.87 ms / 136 KB 6.18 ms / 56 KB
PostgreSQL 50 9.21 ms / 132 KB 6.13 ms / 53 KB
RAMJobStore 10 2.58 µs / 3.25 KB 2.16 µs / 2.57 KB
RAMJobStore 50 2.71 µs / 3.29 KB 1.81 µs / 2.57 KB

The persistent-store fire path is batched and counts 1.27 commits per firing at the database. Two clustered nodes ran a mixed workload for thirty minutes on PostgreSQL and on SQL Server — every trigger family, a serial job behind an overlap detector, a retry policy, a job timeout, induced misfires, a node killed mid-run and recovered — with peak serial concurrency 1, exactly one recovery, nothing left behind and a flat heap, on the beta.1 build and again on the 4.0.0 commit itself. The upgrade from a running 3.20 cluster was rehearsed by hand through its mixed-version window on both engines, and the offline upgrade over rows a released 3.20 wrote runs on every dialect on every pull request.

Known limitations

Each is documented on the page that owns it; this is the list, not the explanation.

  1. No external production user is known at the time of this release; the validation is ours, and it is described in the pre-release notes below.
  2. An authorized caller of the HTTP API or the dashboard is trusted with code execution on the host (HTTP API, SECURITY.md).
  3. SendMailJob reads SMTP credentials out of job data when nothing is registered (Quartz.Jobs).
  4. No rate limiting on any Quartz surface, by design.
  5. On shutdown, in-flight jobs are neither waited for nor cancelled by default, and the scheduler says so (hosted services).
  6. MaxConcurrency defaults to 10 on the shared thread pool (configuration reference).
  7. HttpScheduler.Status and SchedulerInstanceId block on an HTTP round trip (HTTP client).
  8. OpenTelemetry.Instrumentation.Quartz produces nothing on 4.x, and 4.x warns at start if it is present (OpenTelemetry).
  9. SQLite cannot join an ambient TransactionScope (job stores).
  10. Reflective string-named paths remain under trimming; each has a documented alternative (#​3341).
  11. Least exercised: Quartz.Extensions.Redis (one unit and one container fixture); Quartz.Aspire under an AppHost was run by hand, not in CI.
  12. The strong-name key pair is committed and public; nuget.org's repository signature and trusted publishing are what stand behind a package.

Upgrading a running deployment

The runbook is short and every step links the page that owns it. In one breath: retarget to net10.0 and drop the merged packages; on 3.x, turn binary blobs off and run the cron audit; run schema_30_to_40_upgrade_<database>.sql while 3.x nodes are still up; configure and start the 4.0 nodes one at a time (route AddCalendar through the 3.x nodes until the last one is gone); run schema_30_to_40_indexes_<database>.sql once it is; pause any job group you relied on being paused (3.x never persisted one). A 4.0 node against a schema you have not migrated refuses to start and names the column and the script.

If you ran a 4.0 pre-release

Everything that changed between one pre-release and the next is one appendix in the guide, build by build: If you ran a 4.0 pre-release. 4.0.0 is the rc.2 commit: between the two tags, nothing changed but this page and the site.

Thanks

Full changes

Important

✂ PR body was truncated to here.


Configuration

📅 Schedule: (in timezone Asia/Shanghai)

  • Branch creation
    • "before 1am,before 5am,before 9am"
  • Automerge
    • At any time (no schedule defined)

🚦 Automerge: Enabled.

Rebasing: Whenever PR is behind base branch, or you tick the rebase/retry checkbox.

🔕 Ignore: Close this PR and you won't be reminded about this update again.


  • If you want to rebase/retry this PR, check this box

This PR was generated by Mend Renovate. View the repository job log.

@renovate
renovate Bot force-pushed the renovate/major-quartznet-monorepo branch from cfb06c6 to 82c6548 Compare September 9, 2026 03:11
@renovate
renovate Bot force-pushed the renovate/major-quartznet-monorepo branch from 82c6548 to 125ee2e Compare September 13, 2026 13:28
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

0 participants