Skip to content

Update dependency Quartz.AspNetCore to v4 - #1869

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

renovate[bot] wants to merge 1 commit into
mainfrom
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.15.1 → 4.5.0 age confidence

Release Notes

quartznet/quartznet (Quartz.AspNetCore)

v4.4.0

Quartz.NET 4.4 is history you can run a business on. Each run says what it achieved: succeeded, failed,
cancelled or skipped, with a summary and metrics. The history keeps that per job, sweeps it by result and charts
it over time, and a health check degrades when a required job stops succeeding. Recovery respects rolling
deploys, and triggers already due fire in the transaction that acquires them: at 20 triggers a second on
PostgreSQL, 95–99 % of firings land within ±50 ms, up from 54 % on 4.3.0. Quartz.Weasel now manages the job store's schema on six databases, adding MySQL, Oracle and Firebird.

dotnet add package Quartz --version 4.4.0
Highlights
  • A run says what it achieved, and the history keeps it per job. context.Result, a status per job, retention
    by result, and a chart of runs over time. Execution history and outcomes
  • A health check per job. RequireSuccessWithin(jobKey, TimeSpan.FromHours(26)) degrades when a nightly job
    stops succeeding. Execution history and outcomes
  • A retry policy on the job class, and a scheduler-wide default. Also RunAtStartup, scheduling a trigger
    already paused, and pausing a set of keys with a reason in one call. Scheduling
  • Fire on acquire. One transaction per round instead of two: one-off throughput on PostgreSQL goes from 258–272 to
    420–429 firings a second. Reliability and clustering
  • Recovery that respects rolling deploys. A firing cancelled by a graceful shutdown can be replayed by a peer
    or the restarted node. Reliability and clustering
  • Weasel for six databases. Quartz.Weasel.MySQL, Quartz.Weasel.Oracle and Quartz.Weasel.Firebird join
    PostgreSQL, SQL Server and SQLite. Databases and Weasel
  • An HTTP client that reads a newer host, and HTTP API errors as problem details under native AOT.
    HTTP API and dashboard
Execution history and outcomes
  • Job outcomes (#​3971, closes #​3957). A job says what its run achieved:
    context.Result = JobRunReport.Skipped("…").With("released", 3).
    • Results: JobRunResult {Succeeded, Failed, Cancelled, Skipped}.
    • A summary and metrics go with it. Metrics are JSON, AOT-safe, capped at 4,000 characters (log events
      1059/1060).
    • Vetoes are recorded in the misfire feed (MisfireReason.Vetoed). Manual runs (TriggerJob) are marked.
    • Per-job JobRunStatus: last run, last success and failure, consecutive failures, counts.
    • Retention by result (RetentionByResult, MisfireRetention) and MaxEntriesPerJob.
    • How-to: Job Outcomes.
  • The database history keeps all of it (#​3980, closes #​3959; schema #​3972, closes #​3958).
    • Each run's row and its job's status commit in one transaction on the history connection.
    • The sweep is tiered by result, capped per job, and clears status rows of deleted jobs. One elected node
      sweeps per cluster.
    • The misfire feed is filtered by reason. A 4.2 row with no reason reads as Missed.
    • Known cost: runs of one job that complete at once take turns on its status row. On PostgreSQL, one hot job
      with history on went from 113–248 to 77–189 firings a second. Tracked in #​3979.
    • Fixed: the optional-table refusal named database/migrations/database/migrations/…. (#​3972)
  • A run whose job threw records the job's own message (#​3995, closes #​3992). The history, the per-job status
    and the HTTP API's JobExecuted event carry it, not the wrapper's "Job threw an unhandled exception". Log
    templates are unchanged.
  • Health check per job (#​3982, closes #​3961).
    • o.RequireSuccessWithin(new JobKey("nightly-report"), TimeSpan.FromHours(26)), or a RequiredJobs list
      bound from configuration.
    • It reports Degraded by default, per job. A Skipped run counts as a success.
    • One status read per check.
    • The host fails at startup if the execution history is off.
  • Run statistics over time (#​4027, closes #​4015).
    • New IExecutionHistoryStore.QueryExecutionStatistics (a default member), with an HTTP route and a client
      member.
    • It returns time buckets of runs by result, with p50/p95/max duration. Percentiles are exact on every store:
      native PERCENTILE_CONT on PostgreSQL and Oracle, a ranked read elsewhere.
    • The dashboard shows a chart on History and on Job Detail and adds a Window filter (1h/24h/7d/30d). The stat
      cards count the window rather than the page.
    • With the database history, the chart needs SQL Server 2012+, MySQL 8.0+, SQLite 3.25+ or Firebird 3+.
  • Run a failure again with the input it had (#​4021, closes #​4012).
    • Opt-in: ExecutionHistoryOptions.RecordInput records a run's JobInput, up to MaxInputBytes (default
      16 KB). Over the cap the input is not kept, and the row is flagged.
    • The dashboard's Run again, on History and on the Execution page, passes the input back to
      TriggerJob(key, data) and says whether it did.
    • Two nullable columns, JOB_INPUT and JOB_INPUT_TOO_LARGE, in the optional 4.4 migration.
    • The History table fits at 1280 px: Run again is always on screen, and keys show the name over the group.
  • Telemetry carries the run's result.
    • quartz.job.execution.duration gains a quartz.job.result tag: succeeded, failed, cancelled or
      skipped, classified by the same rule as the history row. error.type is unchanged: it is set on a failed
      run that threw. (#​4004, closes #​3967)
    • The Quartz.Job.Execute span gets quartz.job.result too. A failed store span now carries error.type.
      (#​4020, closes #​4016)
    • New QuartzSchedulerOptions.RecordExceptionSpanEvents, since OpenTelemetry deprecated span events in 2026.
      Unset, Quartz follows OTEL_SEMCONV_EXCEPTION_SIGNAL_OPT_IN, so logs turns the events off. The default is
      unchanged. (#​4020)
Scheduling
  • A retry policy on the job class, and a scheduler-wide default (#​4028, closes #​4013).
    • [RetryPolicy(3, "00:00:30")] (Fixed), [RetryPolicy(5, "00:00:10", 2.0)] (Exponential), Explicit delays,
      or [RetryPolicy(0)] (never), plus q.UseDefaultRetryPolicy(…).
    • Precedence: the trigger's own policy, then the job's attribute, then the scheduler default.
      RetryPolicy.None refuses at any level.
    • It is resolved at fire time, so triggers already stored are covered, and nothing is written back.
    • Analyzer QZ0006 checks the attribute's durations.
    • An inherited policy with no room for a retry before the next occurrence is not reported as exhausted (debug
      event 1062).
    • SendMailJob and NativeJob carry [RetryPolicy(0)].
  • Run a job once when the scheduler starts (#​4024, closes #​4019): q.RunAtStartup(jobKey).
    • It runs on the first start, again after a restart, and when a scheduler built in standby is started.
    • In a cluster it runs once on each node that starts.
    • A run that a crash prevented is replaced by the next start's run, not added to it. Registering the same job
      twice gives one run.
    • @reboot is still rejected, and its message names RunAtStartup.
  • Schedule a trigger already paused, with a reason (#​4023, closes #​4018).
    • ScheduleJobOptions/AddTriggerOptions gain Paused, PauseReason and PauseRequestedBy. The shipped
      stores write the trigger paused in the same write, so it never fires first.
    • A custom store answers through the new IJobStore.SupportsStoringPaused default member. Without it, the
      scheduler pauses the trigger straight after storing it; the gap between the two is documented.
    • The HTTP schedule routes take the same fields. A host older than 4.4 is refused.
    • Fixed: replacing a trigger no longer leaves an earlier pause's reason on the row.
  • Pause a set of triggers or jobs with a reason in one call (#​3986, closes #​3964).
    • New: PauseTriggersWith / PauseJobsWith on IScheduler, IJobStore and IQuartzApiClient.
    • One lock and one transaction in the database store.
    • The existing /keys/pause routes take the reason. A reasonless 4.3 client still gets the exact reasonless
      pause.
    • The dashboard's bulk pause is one call, and the Action Log records the reason on every pause.
  • Job listeners hear progress (#​4003, closes #​4001).
    • New IJobListener.JobProgressChanged(context, progress), a default member.
    • It is raised at the cadence the progress is written: the first report at once, then at most once a second
      per firing, and only on a change.
    • The last report is always heard before JobWasExecuted. It is called off the job's thread and is in-process
      only.
    • A listener that throws is logged (event 1061), and the job and the other listeners carry on.
    • Thanks to @​bdovaz, who asked for it in discussion #​3996 and filed #​4001.
Reliability and clustering
  • Fire on acquire (#​4011, closes #​3864). Triggers already due are fired in the transaction that acquires them.
    • The database store makes one transaction per round instead of two, and sends each round's claims and fire
      writes as one batch each.

    • The in-memory store takes one lock per round instead of two.

    • PostgreSQL, against the released 4.3.0 (three alternating sittings; the full table is under Measured):

      Workload 4.3.0 4.4.0
      20 triggers a second, firings within ±50 ms 54 % 95.5–99.2 %
      100 triggers a second, firings within ±250 ms 68–70 % 99.5–100 %
      One-off throughput 258–272 a second 420–429 a second
      Two-node drain 293–298 a second 456–491 a second
    • A custom IJobStore keeps the old two-step path through the new AcquireNextTriggersAndFireDue default
      member.

    • A custom StdAdoDelegate keeps every override it has. The new IDriverDelegate.SupportsFireOnAcquire
      answers true only for the shipped delegates, and a subclass gets the one-transaction round through the
      per-trigger members it may override.

  • A clustered node looks again soon for a trigger that a serial job on another node holds back (#​4032,
    closes #​3988).
    • Before, the node idled a full idle wait (15–30 s), and its pinned serial firings could drop to zero.
    • Now it looks again after 100 ms, backing off to at most 5 s while the trigger stays held. On PostgreSQL the
      worst firing went from 12.9 s late to 0.37 s.
    • Only clustered stores with the shipped delegates do this. Non-clustered stores and custom delegates are
      unchanged.
    • The mixed-version gate no longer needs its heartbeat trigger.
  • A graceful shutdown can hand a cancelled firing back for recovery (#​4030, closes #​4014). Opt-in:
    AdoJobStoreOptions.RecoverFiringsCancelledByShutdown (key
    quartz.jobStore.recoverFiringsCancelledByShutdown).
    • A RequestsRecovery job cancelled by a graceful shutdown gets a recovery trigger instead of completing as
      Cancelled. A peer, or the restarted node, runs it on its next acquisition.
    • Continuations waiting on it follow the replay. The cancelled run's partial job data is not saved.
    • A user Interrupt or [JobTimeout] stays a cancellation. The in-memory store is not affected.
    • Recovery triggers now keep the original firing's markers when recovered again.
  • A trigger that keeps failing to fire is parked (#​3973, closes #​3963). After five non-transient failures
    in a row it is stored ERROR. Before, it was released and acquired again forever, which hot-looped and starved
    its [DisallowConcurrentExecution] job-mates.
    • AdoJobStoreOptions.MaxConsecutiveFireFailures (default 5, 0 = never), flat key
      quartz.jobStore.maxConsecutiveFireFailures.
    • Log event 3050. Recover with ResetTriggerFromErrorState.
  • A calendar that throws fails its own trigger, not the in-memory batch (#​3981, closes #​3974).
    • The in-memory store parks such a trigger after InMemoryJobStoreOptions.MaxConsecutiveFireFailures
      failures in a row (log events 2008/2009).
    • The scheduler releases the triggers it acquired but did not fire when a store fails the batch
      unexpectedly.
  • A calendar that throws while a misfire is handled fails only its own trigger, in both stores (#​4025,
    closes #​3985 and #​4006).
    • In-memory store: the acquisition pass, completion, overlap release and resume carry on.
    • Database store: a completion no longer retries until shutdown with its job stuck BLOCKED.
    • Failures are counted at most once per misfire threshold, so a briefly failing calendar heals instead of
      being parked within milliseconds. They are counted only after the store's own commit, and a successful
      misfire resets the count.
    • The resume calls log a calendar's exception instead of throwing it.
    • Fixed: a notification that threw after a commit made the store retry work it had already committed (log
      event 3053). New in-memory event 2010.
  • An in-memory calendar replacement that throws changes nothing (#​4029, closes #​4026).
    AddCalendar(…, updateTriggers: true) works out every trigger's new fire time on a clone first, and fails as
    the database store does, with JobPersistenceException.
Databases and Weasel
  • Quartz.Weasel.MySQL and Quartz.Weasel.Oracle (#​3987, closes #​3954 and #​3955): UseWeaselForMySql() and
    UseWeaselForOracle().
    • MySQL: names are qualified by the connection's database. The migration takes a GET_LOCK user lock,
      which needs no privilege.
    • Oracle: names are qualified by CURRENT_SCHEMA. There is no lock, because DBMS_LOCK needs a grant. A
      conflicting apply re-reads and retries, as ProvisionSchema() does.
    • Both are proven against real databases. Our fresh-install script and a Weasel-created schema read back
      unchanged; 3.20, 4.2 and 4.3 databases upgrade add-only; an application's own objects survive.
  • Quartz.Weasel.Firebird (#​4005, closes #​3990): UseWeaselForFirebird(), for Firebird 3, 4 and 5.
    • Built on Weasel.Firebird, which we contributed upstream as JasperFx/weasel#666. It first shipped in Weasel
      9.39.0.
    • There is no global lock: a racing apply re-reads the schema.
    • Identifiers are limited to 31 characters by default. MaxIdentifierLength allows up to 63 on Firebird 4 and
      later.
    • Pull requests run the catalog tests on the official Firebird 3, 4 and 5 images.
  • Weasel 9.39.0 (#​4007, closes #​4002). Every Weasel package Quartz references is [9.39.0, 10.0.0).
    • Weasel's Oracle migrator lost the LONG fetch size and rebuilt IDX_QRTZ_T_NFT_ST on every apply. JasperFx/weasel#660
      fixed it upstream, so Quartz.Weasel.Oracle needs no workaround.
    • A lock-free apply (SQLite, Oracle, Firebird) retries only a lost race, recognised by the provider's error
      code. Every failure surfaces as a SchedulerException with the original inside.
  • Quartz.Weasel.SQLite rebuilds a table carrying your own columns, indexes and foreign keys, keeping them and
    their rows. 4.3 refused. Thanks to the 11 upstream fixes in Weasel 9.36.0. (#​3978, closes #​3956)
  • Schema guard rails (#​3989, closes #​3962 and #​3949).
    • All eight fresh-install scripts, SQL Server MOT and pre-2016 included, are held to the schema model:
      columns, types, keys, foreign keys and teardown.
    • Migration chains and column lists are read from the migration folders.
    • The mixed-version gate runs a released 4.3.0 node beside 4.4, with history on.
    • SQL Server's BLOB foreign key: ProvisionSchema() leaves it out too, as the fresh-install script always has.
    • The MySQL script loses its deprecated integer display widths.

Thanks to the JasperFx maintainers for reviewing and merging Weasel.Firebird (JasperFx/weasel#666).
Weasel schema management

Database schema
Script Required Adds
4.4/add_execution_outcome_<dialect>.sql only with execution history on RESULT, SUMMARY, METRICS, MANUAL, FIRE_INSTANCE_ID, JOB_INPUT, JOB_INPUT_TOO_LARGE on QRTZ_EXECUTION_HISTORY; IDX_QRTZ_EH_JOB_TIME; table QRTZ_JOB_STATUS (#​3972, closes #​3958; #​4021)

The new columns are nullable, and the script is safe to run beside 4.3 nodes. The Weasel models carry it.

HTTP API and dashboard
  • History and status over the wire (#​3977, closes #​3960).
    • History filters by job, fire-time window and result.
    • Status routes under /history/job-status, with a version check so a client never sends a 4.4 filter to an
      older host.
    • The misfire listing leaves Vetoed out unless reasons names it, so a 4.3 reader never meets a name it can't
      parse.
    • Dashboard: a Result column (Succeeded/Skipped/Failed/Cancelled), summary and metric chips, a manual badge,
      Last run / Last success / failing ×N on Jobs, and a status panel on Job Detail.
  • The HTTP client tolerates a newer host (#​4022, closes #​4017).
    • An unknown event kind or a frame it can't read is skipped, and the live-event stream stays open.
    • An enum name it doesn't know reads as a fallback or null, or leaves only that item out of a listing. Each
      enum's rule is documented. Log events 9200–9203.
    • Unknown JSON members are always skipped. The host still refuses an unknown name in a request.
    • From 4.4, the server no longer has to hide new names from clients.
  • HTTP API errors are problem details in a trimmed or native AOT host (#​3975, closes #​3965). Every error
    used to be an empty 500. A second native canary runs the wire path on every pull request.
  • Dashboard fixes (#​3986): the paused note stays in the Details column; the Jobs table holds at 1280 px; the
    AspNetCore example's dashboard works from source on .NET 10 (MapStaticAssets), and ExamplesSmoke asserts
    blazor.web.js.
  • The run statistics chart and Run again with input are under
    Execution history and outcomes.
Behaviour changes
Now In 4.3 PR
A cancelled run is recorded as Cancelled (Succeeded false) recorded as a success #​3971
context.Outcome reads Vetoed inside JobExecutionVetoed read Succeeded #​3971
Vetoed firings appear in the misfire feed (MisfireReason.Vetoed) not recorded #​3971
TriggerJob firings carry QRTZ_MANUAL_TRIGGER in the trigger's JobDataMap no marker #​3971
History, job status and the JobExecuted event carry the job's own exception message "Job threw an unhandled exception"; log templates unchanged #​3995
A trigger that fails to fire 5 times in a row is stored ERROR, in both stores (MaxConsecutiveFireFailures, 0 = off) persistent: released and acquired again; in-memory: batch stranded #​3973, #​3981
A calendar's misfire failures count at most once per misfire threshold, after the store's commit not counted #​4025
The resume calls log a calendar's exception thrown #​4025
WithRetryPolicy(null) means "inherit"; manual and recovery runs inherit too cleared the policy #​4028
SendMailJob and NativeJob carry [RetryPolicy(0)]: the scheduler default never retries them; a trigger's own policy still applies no attribute #​4028
A Weasel SQLite apply failure is a SchedulerException with the original inside Weasel's InvalidOperationException #​4007
Quartz.Weasel.SQLite rebuilds a table carrying your own objects refused #​3978
The HTTP misfire listing leaves Vetoed out unless reasons names it — #​3977
Dashboard: Complete is Succeeded; Job Detail's history link opens that one job Complete #​3977
Dashboard stat cards count the window counted the page #​4027
A firing committed on acquisition runs even if Shutdown() was called mid-round — #​4011
A trigger fired on acquisition has no TriggersFired span; the acquisition span carries the count a TriggersFired span #​4011
OTEL_SEMCONV_EXCEPTION_SIGNAL_OPT_IN=logs turns exception span events off (RecordExceptionSpanEvents overrides) ignored #​4020
A clustered node re-polls after 100 ms, up to 5 s, for a trigger a serial job on another node holds waited a full idle wait #​4032
Replacing a trigger clears an earlier pause's reason reason left on the row #​4023
A recovery trigger recovered again keeps the original firing's markers overwritten #​4030
Mixed 4.3 and 4.4 nodes
  • The 4.4 script is safe to run beside 4.3 nodes. A 4.3 node never touches QRTZ_JOB_STATUS, so the
    per-job status counts only runs 4.4 nodes record until every node is 4.4.
  • A 4.3 reader never sees Vetoed in the HTTP misfire listing. A 4.2 misfire row with no reason reads as Missed.
  • A 4.4 client never sends a 4.4 history filter to an older host, and scheduling a trigger paused on a host older
    than 4.4 is refused.
  • A reasonless 4.3 client's key-set pause on a 4.4 host is the exact reasonless pause.
  • CI runs a released 4.3.0 node beside 4.4 on one database, with history on. (#​3989)
Upgrading

An application on 4.3 compiles on 4.4 unchanged. On a persistent store with the execution history on, run
4.4/add_execution_outcome_<dialect>.sql before the first 4.4 node starts. Every change, and the full list of
mixed-version notes, is in the migration guide's
Upgrading from 4.3 to 4.4.

Public API

Additions only: +278 lines, none removed, and every member added to an existing interface has a default implementation.

Assembly Lines added
Quartz 211
Quartz.Dashboard 20
Quartz.Weasel.MySQL (new) 16
Quartz.Weasel.Firebird (new) 14
Quartz.Weasel.Oracle (new) 13
Quartz.HttpClient 2
Quartz.Jobs 2

The whole delta, member by member: git diff v4.3.0 v4.4.0 -- src/Quartz.Tests.Unit/Verify src/Quartz.Tests.AspNetCore/Verify.

Measured

PostgreSQL 15.1 in Docker (fsync and synchronous commit on), pool 10, three alternating sittings of 4.3.0 and 4.4.0 on one quiet machine.

Measure 4.3.0 4.4.0
One-off throughput, defaults 3.67–3.88 ms a firing (258–272/s) 2.33–2.38 ms a firing (420–429/s)
Allocated per one-off firing 72.1 KB 60.8 KB
Statements per firing, history off 16.5 13.8
20 triggers/s, within ±50 ms (max deviation) 53.8–54.3 % (78–83 ms) 95.5–99.2 % (51.5–54.2 ms)
100 triggers/s, within ±250 ms 68.2–70.3 % 99.5–100 %
Clustered drain, 2 nodes 293–298/s 456–491/s
Clustered drain, 4 nodes 287–301/s 379–504/s
In-memory S1 / S4 — not worse; fire-ahead punctuality max 1 s → 15 ms
  • With execution history on and every firing hitting one job, history adds about 7 statements and 2.75 commits a firing and costs about 37 % of one-off throughput (235–239/s), because each completion updates that job's status row in the same transaction. Spread across jobs the cost is far smaller. Tracked in #​3979.
  • A 30-minute two-node soak on durable PostgreSQL ran 6,505 executions with no duplicate firing, no overlap of a serial job, every one-off exactly once, and one elected node sweeping the history.
Dependencies
  • The Quartz.Weasel packages depend on Weasel [9.39.0, 10.0.0), which needs JasperFx 2.76.0 or later. (#​4007)
  • Three new packages: Quartz.Weasel.MySQL, Quartz.Weasel.Oracle and Quartz.Weasel.Firebird. The package
    count goes from 18 to 21. (#​3987, #​4005)
  • coverlet 10.1.0. (#​3947)
  • WolverineFx 6.41.0, the four Weasel floors held so they move together, and nine unreferenced package rows removed.
    (#​3969, closes #​3966)
Also
  • Tests run on NUnit 5 and Microsoft.Testing.Platform (#​3976, closes #​3970, supersedes #​3943; prepared in #​3969).
    Contributors run dotnet test --project ….
    • Rider keeps working through Microsoft.NET.Test.Sdk (RIDER-131530), and a test guards global.json's runner
      (RIDER-140778).
    • GitHub annotations and summaries go through --report-github.
    • Coverage goes through coverlet.MTP (same 16 modules).
    • HangDump/CrashDump replace blame.
    • The thread-pool allocation test measures on one thread.
  • The retired GitHub Models issue-triage workflow is deleted, and the comparison page reads 4.4. (#​3969)

v4.3.0

Quartz.NET 4.3 is an additive minor. Every public change is a new type, a new member on a type we own, or a default
interface member, and its schema change is one generated folder, database/migrations/4.3/, that only adds
nullable columns. A released 4.2 node and a 4.3 node share one PostgreSQL database in CI to prove a mixed cluster
runs safely. A 4.2 application upgrades by changing the version and, on a persistent store, running the 4.3 scripts
before the first 4.3 node starts. The headlines: a scheduler on a database fires one-off jobs 2.8× faster than 4.2
at its shipped defaults; a job can be a lambda; and a schedule can be steered while it runs, with an overlap policy,
a pause that says why, backfill, progress and captured logs, and a dashboard that edits. Four new packages put the job store's tables under Weasel, for applications on Marten or Wolverine.

dotnet add package Quartz --version 4.3.0
Highlights
Faster where it runs
  • A job that takes nothing from the container — no constructor parameters, not registered, no ConfigureScope —
    is built without a DI scope: 1.84 → 1.68 KB a firing on RAMJobStore, a little faster too. Every other job keeps
    its scope exactly as before. (#​3866)

  • RAMJobStore computes a cron trigger's next fire time once per firing, not twice, inside its lock. (#​3866)

  • A scheduler on a database batches what is already due, out of the box. MaxBatchSize now defaults to
    automatic: the thread pool's size on a persistent store, clustered or not, and 1 in memory.
    Nothing fires early (the fire-ahead window is still zero). On PostgreSQL a backlog of one-off jobs goes from
    about 80–91 to 125 firings a second, and from 6.0 to 2.8 commits a firing. MaxBatchSize = 1 restores 4.2.
    On a cluster, the flip was gated on a 2- and 4-node drain against a durable PostgreSQL: 1.7–2.1× faster on
    two nodes and 1.6–2.0× on four, at a lock-wait p99 of about 60 ms. One of four sittings read 0.78× on four nodes
    while every lock wait on that shared VM was slower. (#​3862, #​3900)

  • Scheduling a job costs less. ScheduleJob with a simple trigger allocates 2.76 KB, down from 3.83 KB,
    and a cron one 3.3 KB, down from 4.6 KB. Scheduling a trigger later than the one the scheduler is already
    waiting for no longer wakes it, and listener lists are built when they change, not on every call. (#​3865)

  • A firing that may run beside itself completes without the store's lock. Completions of
    concurrent-allowed jobs no longer queue behind each other on TRIGGER_ACCESS. With the batching default above,
    one-off jobs on PostgreSQL go from 4.2's 91 to 237 firings a second on the same database: 2.6×. Repeating
    triggers fire 1.5–1.9× faster. A 2- and 4-node cluster drains 1.5–1.8× faster at a fifth of the lock wait. A
    [DisallowConcurrentExecution] job, a retry, a trigger with continuations, a buffered overlap policy and a
    non-durable job's last trigger keep the lock. The misfire pass also settles a continuation whose parent no
    longer exists. (#​3863)

  • Measured on the release candidate against 4.2.0, alternating sittings on one box and one durable PostgreSQL:

    4.2.0 4.3.0
    One-off jobs on PostgreSQL, shipped defaults 90–91 a second 251–255 a second (2.8×)
    Repeating triggers on PostgreSQL at 20 a second, within ±50 ms 22 % 52 % (all within ±250 ms; worst 84–96 ms, was 205 ms)
    In-memory throughput, bytes per execution 2.81 KB 2.07 KB, and not slower
    Scheduling one simple trigger 3.4–3.6 KB 2.6–2.7 KB

    In memory every firing still lands within ±50 ms.

A job can be a lambda

q.ScheduleJob("cleanup", static async (IRepo repo, ILogger<Cleanup> log, CancellationToken ct) => …, t => t.WithCronSchedule("0 0 * * * ?"))
— the parameters are what the code needs: the firing (IJobExecutionContext), its token, its scope
(IServiceProvider), or any service from that scope. AddJob(name, handler) adds a durable one. It is stored as
Quartz.Impl.DelegateJob keyed by the job key, so it persists and clusters like any job; every node registers the
handler. (#​3867)

The compiler binds a lambda handler: Quartz's source generator intercepts the call, so a firing resolves its
parameters with generated code, in 14 ns and allocating nothing, instead of by reflection (51.6 ns, 72 B). Any
other handler is bound by reflection once at registration, native AOT included. Delegate jobs (#​3882)

See what a running job is doing

context.ReportProgress(40, "copied 400 of 1,000") shows as a progress bar on the dashboard's Currently Executing
page, cluster-wide (written at most once a second, only on change). q.UseExecutionLogCapture() keeps the log lines
a firing writes — bounded by lines and bytes — with its history entry, and a new execution page shows one run with
its outcome, timings, exception and log. Hangfire.Console's job, in the box. Progress and execution logs (#​3874)

Say what happens when the last firing is still running

t.WithOverlapPolicy(OverlapPolicy.Skip) drops an occurrence that comes due while the trigger's previous firing
runs (recorded in misfire history with reason Overlap); BufferOne holds one and fires it when the previous
ends; CancelPrevious interrupts the running firing and starts the new one (on another node it holds instead);
AllowAll overlaps freely. Default is 4.2's behaviour and costs nothing. [DisallowConcurrentExecution] still
wins. Temporal's and Kubernetes CronJob's knob, per trigger, in every store. New
ITriggerListener.TriggerSkipped. Roll every node to 4.3 before relying on it: a 4.2 node ignores the
policy. Overlap policy (#​3875)

A pause says why

scheduler.PauseTriggerWith(key, new PauseDetails { Reason = "vendor API down", RequestedBy = "ops" }) (and
PauseJobWith, PauseTriggerGroupsWith, PauseJobGroupsWith, PauseAllWith) records why, who and when;
GetTriggerPause(key) reads it back, and the dashboard shows it beside the pause. The HTTP pause routes take an
optional { reason, requestedBy } body, with the requester defaulting to the signed-in user.
q.PauseTriggerWhenRetriesExhausted() pauses a trigger whose retry policy gave up, with the failure as the
reason. A pause without a reason is exactly 4.2's pause. Pausing with a reason (#​3879)

The dashboard is a control panel

Edit a trigger in place: description, priority, calendar, misfire instruction, execution group, retry policy,
preferred node, overlap policy and its job data, with each save written to the action log (now with a User column).
Filter the Triggers list by group, name, job, calendar, state and "next fire before", and Jobs by group and name.
Every filter lives in the URL. Select rows to pause, resume or unschedule them together. It works the same over an
HTTP target and a store-attached window. (#​3878)

Steer who runs, and how often the same thing is enqueued
  • Per-tenant limits. limits.ForGroupsWithPrefix("tenant:", 2, ExecutionLimitScope.Cluster) gives every group
    under the prefix its own allowance, cluster-wide; WithExecutionGroup("tenant:{TenantId}") and
    [ExecutionGroup("tenant:{TenantId}")] fill the group from the trigger's job data; a one-off says
    ExecutionGroup = $"tenant:{input.TenantId}". Concurrency keys, which Oban, Sidekiq, JobRunr and BullMQ
    sell in their paid tiers. (#​3876)
  • Idempotent enqueue and debounce. OneOffJobOptions.OnConflict is Throw (as before), Replace, Keep
    (enqueue unless it is already pending) or KeepEarlier (whichever fires first stays). New
    IScheduler.ScheduleTrigger(trigger, onConflict) / IJobStore.StoreTrigger(trigger, onConflict) report
    whether the trigger was created, replaced or kept; the HTTP API takes onConflict. (#​3877)
Backfill a range you missed

scheduler.Backfill(triggerKey, from, to) schedules one run of the trigger's job for every slot its schedule had in
[from, to): calendar applied, the job data, priority, retry policy and execution group carried, the slot readable
in the job through context.GetBackfillSlot(). Re-running the range skips slots still pending, MaxSlots (1,000)
and Spacing bound it, and a future range is refused. Misfire handling is for when the scheduler was down; backfill
is for a range you choose. Over the HTTP API it is one audited request (POST …/triggers/{group}/{name}/backfill),
and the dashboard's trigger page has a Backfill dialog. Backfill (#​3880)

Quartz's tables under Weasel
  • For applications on Marten or Wolverine: new packages Quartz.Weasel.PostgreSQL, Quartz.Weasel.SqlServer
    and Quartz.Weasel.SQLite put the ADO.NET job store's schema under Weasel. db-apply,
    db-assert, db-patch, resources setup and AutoCreate at startup then create and migrate Quartz's tables beside the
    application's own, instead of ProvisionSchema() and hand-run scripts. One call on the store:
    store.UseWeaselForPostgres(), UseWeaselForSqlServer() or UseWeaselForSqlite(). (#​3941, #​3948)
  • The model is generated from the same source as the store's own scripts, so a database created by database/tables/,
    ProvisionSchema() or the migrations reads as unchanged, and a 3.x or 4.2 schema is brought up to 4.3 in place. Tables
    are add-only: columns, indexes and keys you added are kept. Every apply takes a lock, so nodes starting together take
    turns. On PostgreSQL the tables can instead join a Marten store as a feature schema. (#​3941, #​3948)
  • MySQL and Oracle follow once the fixes we sent to Weasel are released (JasperFx/weasel#639–#​649). Firebird has no Weasel
    provider. Weasel schema management
  • Thanks to Jaedyn, whose Weasel.Quartz was the first Weasel integration for
    Quartz.NET.
Test with a clock you control

Advancing a FakeTimeProvider now wakes the scheduler: clock.Advance(TimeSpan.FromHours(1)) fires what was due
in that hour, with no real waiting. JobExecutionContextBuilder.For(job).WithTrigger(trigger).FiredAt(when).Build()
builds a context for a job's unit test in one line. Testing (#​3869)

Declared where it is used
  • t.WithCronSchedule(cron => cron.AtTime(new TimeOnly(3, 0)).OnWeekdays()) configures the expression inline,
    and CronExpressionBuilder.Every(TimeSpan.FromMinutes(10)) is a clock-anchored interval
    (0 0/10 * ? * *). (#​3868)
  • q.AddJobLogScope() puts the job, trigger and fire instance on every log line a firing writes. Opt-in:
    272 bytes a firing when on, nothing when off. (#​3870)
  • [CronTrigger("0 0 2 * * ?", ConfigurationKey = "Jobs:Cleanup:Cron")] reads the schedule from configuration,
    falling back to the literal the compiler checked; a bad configured value fails at startup. New QZ1005;
    QZ1004 is now Info, because AddDeclaredJobsFrom<Assembly>() is always emitted beside AddDeclaredJobs(). (#​3871)
  • [SimpleTrigger("00:10:00")] declares an interval job the way [CronTrigger] declares a cron one: RepeatCount
    (forever by default), the misfire instruction, priority, execution group and ConfigurationKey. New QZ0005
    refuses an interval that is not a positive TimeSpan at compile time. (#​3924)
Database schema

database/migrations/4.3/ is the second schema move since 4.0, and like 4.2's it only adds nullable columns, so a
4.2 node keeps running beside a 4.3 node while you roll:

Script Required Adds
add_fire_progress_<dialect>.sql yes QRTZ_FIRED_TRIGGERS.PROGRESS, PROGRESS_MESSAGE (#​3874)
add_execution_log_<dialect>.sql only with execution history on QRTZ_EXECUTION_HISTORY.EXECUTION_LOG (#​3874)
add_overlap_policy_<dialect>.sql yes QRTZ_TRIGGERS.OVERLAP_POLICY (#​3875)
add_misfire_reason_<dialect>.sql only with execution history on QRTZ_MISFIRE_HISTORY.REASON (#​3875)
add_pause_reason_<dialect>.sql yes PAUSE_REASON, PAUSED_BY, PAUSED_AT on QRTZ_TRIGGERS, QRTZ_PAUSED_TRIGGER_GRPS, QRTZ_PAUSED_JOB_GRPS (#​3879)

Every table script carries the new columns, the memory-optimized and pre-2016 SQL Server scripts included. Each
script is guarded, so running it twice is safe; SQLite's ADD COLUMN is the exception, as before.

A released 4.2.2 node and a 4.3 node share one PostgreSQL database upgraded from the 4.2.0 schema with these scripts,
in every PostgreSQL CI run: 2,000 one-off jobs run exactly once between them, a [DisallowConcurrentExecution] job
never overlaps, continuations settle whichever node completes the parent, and the 4.3 columns keep their values
when the 4.2 node fires, updates or resumes the rows around them. A 4.2 node ignores what the new columns mean
(an overlap policy, a pause reason, progress), so roll every node before relying on those. (#​3925)

Behaviour changes
  • QuartzSchedulerOptions.MaxBatchSize defaults to 0, meaning automatic (see above); an explicit value wins as
    before, and quartz.scheduler.batchTriggerAcquisitionMaxCount still maps to it. (#​3862)
Public API

Additions only: 271 lines across Quartz (+238), Quartz.Dashboard (+23) and Quartz.HttpClient (+10), none
removed. Every one of the 45 members added to an existing interface (IScheduler, IJobStore, ITrigger,
IJobExecutionContext, ITriggerConfigurator<T>, IQuartzApiClient and others) has a default body, so an
implementation or forwarder written against 4.2 compiles and runs unchanged.

New type For

[!IMPORTANT]
✂ PR body was truncated to here.


Configuration

📅 Schedule: (in timezone Asia/Shanghai)

  • Branch creation
    • "before 5:00am,before 10am,before 3pm,before 8pm"
  • Automerge
    • At any time (no schedule defined)

🚦 Automerge: Disabled by config. Please merge this manually once you are satisfied.

♻ 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 2 times, most recently from a40a671 to db92354 Compare September 13, 2026 13:22
@renovate
renovate Bot force-pushed the renovate/major-quartznet-monorepo branch 2 times, most recently from a094daa to f5ecc47 Compare September 25, 2026 16:49
@renovate
renovate Bot force-pushed the renovate/major-quartznet-monorepo branch 4 times, most recently from e5676e1 to 768cbf4 Compare September 29, 2026 15:53
@renovate
renovate Bot force-pushed the renovate/major-quartznet-monorepo branch from 768cbf4 to 6458b83 Compare October 7, 2026 22:35
@renovate
renovate Bot force-pushed the renovate/major-quartznet-monorepo branch from 6458b83 to d3e99b2 Compare October 13, 2026 21:49
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