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.6.0 age confidence

Release Notes

quartznet/quartznet (Quartz.AspNetCore)

v4.6.0

Quartz.NET 4.6 is about clusters under load. A firing round of work that allows concurrent execution takes no TRIGGER_ACCESS lock, and a round reads each concern once whatever its size. An interrupt reaches the node running the firing, through the store. A node can serve only the execution groups it is configured for, and execution history can write behind. A trimmed or native publish now records no Quartz trim warning, and eight packages are proven under native AOT. An application on 4.5 compiles on 4.6 unchanged, unless it treats warnings as errors and has a cron literal that the new analyzer warning QZ0007 reports. The upgrade has three required database migrations, each one nullable column that can run while 4.5 nodes are up; see Upgrading from 4.5 to 4.6.

dotnet add package Quartz --version 4.6.0

Highlights

  • Lock-free firing rounds. On a clustered ADO.NET store, a round whose candidates all allow concurrent execution claims its triggers with guarded writes instead of taking TRIGGER_ACCESS. It is on by default, with an off switch. While any 4.5 node is live, every round takes the lock as before. SQL Server keeps the lock. Clustering
  • One read per concern. A clustered firing round reads each concern once, whatever its size, and a one-off's lock-free completion is one read and one write. Clustering
  • Interrupts reach every node. On a clustered store, Interrupt and InterruptFireInstance record a request for a firing on another node, and that node cancels the job's token after its next check-in. CancelPrevious now works across nodes too. Clustering
  • Execution-group routing. A node lists the execution groups it serves, or the ones it refuses, and acquires nothing else. Manual, startup and recovered firings keep their job's group. Execution groups
  • History can write behind. With ExecutionHistoryOptions.WriteBehindInterval set, a completion is queued and flushed with one status update per job per flush, at close to history-off throughput. Execution history
  • A trigger's own misfire threshold, and a scheduler-wide default instruction. WithMisfireThreshold lets one trigger tolerate a longer delay, or a shorter one, without moving the store's threshold for every other trigger. DefaultMisfireHandling sets what an unstated misfire instruction becomes, for every trigger type. Triggers
  • No Quartz trim warnings. The trim baseline is gone, and three feature switches turn the reflective paths off under PublishTrimmed and PublishAot. Quartz.Weasel.SQLite joins the packages proven native on every pull request. Trimming and native AOT

Clustering

  • Lock-free firing rounds (#​4123, closes #​4067).
    • AdoJobStoreOptions.LockFreeFiringRounds, default true; flat key quartz.jobStore.lockFreeFiringRounds. false restores 4.5's rounds.
    • Each node stamps its Quartz version in the new QRTZ_SCHEDULER_STATE.QUARTZ_VERSION, from the required migration 4.6/add_scheduler_version. While any 4.5 node is live, every round takes the lock (event 3517); once the last one ages out, rounds go lock-free (event 3518).
    • SQL Server keeps the lock. A round that needs it, such as one with a [DisallowConcurrentExecution] candidate, takes it.
    • A clustered round that loses a claim runs the following rounds locked (reason contention, event 3060) until the backlog clears or 2 s pass, so a drained backlog is no slower than under 4.5.
    • Measured on PostgreSQL: 3–24% more firings per second than 4.5 in the throughput census.
  • One read per concern (#​4086, closes #​4066).
  • Cluster-wide interrupt (#​4130, closes #​4071).
    • A request is a nullable INTERRUPT_REQUESTED_TIME on the firing's QRTZ_FIRED_TRIGGERS row, from migration 4.6/add_fired_trigger_interrupt. The node running the firing reads its requests after every check-in, so a request lands within one CheckinInterval, 7.5 seconds by default.
    • true from Interrupt means interrupted here or asked, not stopped.
    • A 4.5 node never reads the request; a 4.6 node says so once per such node (event 3520).
    • CancelPrevious asks a 4.6 peer instead of waiting as BufferOne. The two firings overlap for up to one check-in interval.
  • A clustered scheduler generates its instance id unless one was chosen (#​4105, closes #​4091). UseClustering() used to leave every node at NON_CLUSTERED.

Execution groups

  • A node serves the execution groups it is configured for (#​4088, closes #​4069).
  • Manual and startup firings carry their job's execution group (#​4109, closes #​4092).
  • A recovered trigger keeps its job's execution group (#​4084, closes #​4081).
  • Names in IConfiguration keep their case, and an execution-limit prefix such as tenant:* can be configured (#​4107, closes #​4093).

Execution history

  • Write-behind (#​4125, closes #​3979). ExecutionHistoryOptions.WriteBehindInterval queues each completion; one writer per store flushes every interval, or at 256 waiting runs, in one transaction with one QRTZ_JOB_STATUS update per job. In the census, history on ran at 276 firings per second, write-behind at 379, and history off at 388.
    • A crash loses the queue, normally at most one interval of runs. Reads of history and job status lag by up to one interval. Off by default.
  • Lookups by exact trigger key and fire instance id, on two indexes of their own (#​4094, closes #​4072). Optional migration 4.6/add_execution_history_lookup_indexes.

Triggers

  • A trigger carries its own misfire threshold (#​4131, closes #​4070).
    • TriggerBuilder<TJob>.WithMisfireThreshold(TimeSpan), ITrigger.MisfireThreshold: positive, whole milliseconds; null is the store's MisfireThreshold, as before.
    • Every surface carries it: [CronTrigger]/[SimpleTrigger] MisfireThreshold (checked by the new analyzer error QZ0008), the JSON and XML schedules, TriggerDetailsUpdate, the HTTP API's update-details and the dashboard's Edit details.
    • Both stores decide every misfire by it. The ADO.NET misfire scan stays one ordered index range on NEXT_FIRE_TIME.
    • Stored in the nullable QRTZ_TRIGGERS.MISFIRE_THRESHOLD, from the required migration 4.6/add_trigger_misfire_threshold. A 4.5 node applies the store's threshold, so a cluster honours per-trigger thresholds once every node is 4.6.
  • A scheduler-wide default misfire instruction (#​4131). QuartzSchedulerOptions.DefaultMisfireHandling (SmartPolicy, IgnoreMisfires, FireAndProceed, DoNothing; flat key quartz.scheduler.defaultMisfireHandling) is mapped to each trigger type's own instruction when a trigger whose instruction was never stated is stored. An explicit WithMisfireHandlingInstructionSmartPolicy() stays smart policy. The stored MISFIRE_INSTR is always explicit, so a 4.5 node reads the same number.
  • A trigger re-applied at startup keeps its previous fire time (#​4126, closes #​4122; also in 3.x).

Configuration and diagnostics

  • A part ConfigureAllQuartzSchedulers chooses after AddQuartz replaces the default, so AddQuartzPersistentStore works in either order (#​4121, closes #​4104). Thanks @​tzdevelopteam for the repro.
  • First-run errors name the culprit and the fix, and a generated job or trigger key in a persistent store warns (#​4113, closes #​4095).
  • QZ0007 flags a * seconds field under a restricted minute or hour, such as * */5 * ? * * (#​4115, closes #​4096). Thanks @​ybai08, whose parallel work in #​4111 reached the same rule.
  • The dashboard's cron preview uses the trigger's zone and calendar, and previews the expression being edited (#​4116, closes #​4099).
  • JobExecutionContextBuilder sets recovery, retry, refire, calendar and cancellation, and JobRunTime uses the TimeProvider (#​4119, closes #​4097).

Trimming and native AOT

  • Registered job types only, when trimmed (#​4087, closes #​4068). Feature switches Quartz.TypeNameResolution.IsSupported and Quartz.LegacyComponentKeys.IsSupported.
  • Job data without TypeConverter (#​4089, closes #​4082; #​4129). Feature switch Quartz.JobData.IsTypeConverterCoercionSupported (MSBuild QuartzJobDataTypeConverterSupport). DateOnly, TimeOnly, Half, Int128 and UInt128 convert on the switch-free path. The trim baseline goes to zero.
  • A trimmed SQLite store retries a busy database (#​4083). The trim baseline went from 21 to 17 entries on the way.
  • Quartz.Weasel.SQLite says IsAotCompatible (#​4129), and the canary proves a DbDataSource store natively. Eight packages are now proven.

Documentation

  • The recurring questions answered: cluster concurrency, why a trigger isn't firing, proxy prefixes, first run, reschedule events (#​4120, closes #​4098).

Thanks

What's Changed

Documentation
  • Docs answer the recurring questions: cluster concurrency, why a trigger isn't firing, proxy prefixes, first run, reschedule events by @​lahma in #​4120
Other changes
  • Main builds 4.6.0 by @​lahma in #​4064
  • A trimmed or native SQLite store retries a busy database, and the trim baseline records 17 entries instead of 21 by @​lahma in #​4083
  • A recovered trigger keeps its job's execution group by @​lahma in #​4084
  • A clustered firing round takes one read per concern, and a one-off completes in one read and one write by @​lahma in #​4086
  • A trimmed or native publish resolves only registered job types, and refuses the quartz.* component keys by @​lahma in #​4087
  • A node serves the execution groups it is configured for by @​lahma in #​4088
  • The Aspire example AppHost resolves MessagePack 2.5.307, past GHSA-qhrr-8q5h-9q3h by @​lahma in #​4090
  • A trimmed or native publish coerces job data without TypeConverter, and Quartz records no trim warning by @​lahma in #​4089
  • A clustered scheduler generates its instance id unless one was chosen by @​lahma in #​4105
  • Execution history, misfires and run statistics are looked up by exact trigger key and fire instance id, on indexes of their own by @​lahma in #​4094
  • A manual or startup firing carries its job's execution group by @​lahma in #​4109
  • Names in IConfiguration keep their case, and an execution-limit prefix can be configured by @​lahma in #​4107
  • First-run errors name the culprit and the fix, and a generated identity in a persistent store warns by @​lahma in #​4113
  • QZ0007 flags a * seconds field under a restricted minute or hour by @​lahma in #​4115
  • The dashboard's cron preview uses the trigger's zone and calendar, and previews the expression being edited by @​lahma in #​4116
  • JobExecutionContextBuilder sets recovery, retry, refire, calendar and cancellation, and JobRunTime uses the TimeProvider by @​lahma in #​4119
  • A part ConfigureAllQuartzSchedulers chooses after AddQuartz replaces the default, so AddQuartzPersistentStore works in either order by @​lahma in #​4121
  • Tests pass in any time zone, and two flaky tests settle by @​lahma in #​4124
  • Execution history can write behind: one status update per job per flush by @​lahma in #​4125
  • A trigger re-applied at startup keeps its previous fire time by @​lahma in #​4126
  • A firing round of concurrent-allowed work takes no TRIGGER_ACCESS lock by @​lahma in #​4123
  • Job data converts DateOnly, TimeOnly, Half and Int128 without TypeConverter, and the canary proves Quartz.Weasel.SQLite and a DbDataSource natively by @​lahma in #​4129
  • An interrupt reaches the node running the firing, through the store by @​lahma in #​4130
  • A trigger carries its own misfire threshold, and a scheduler a default misfire instruction by @​lahma in #​4131

Full Changelog: quartznet/quartznet@v4.5.1...v4.6.0

v4.5.1

Three packages take the fixed versions of dependencies named in Microsoft's October 13 security advisories. There is no public API change and no schema change, and no other package changes.

Fixed
  • Quartz.Dashboard.Agent requires Microsoft.AspNetCore.SignalR.Client 10.0.13. 4.5.0 required 10.0.0, and every SignalR client before 10.0.13 brings a Microsoft.AspNetCore.SignalR.Common covered by GHSA-334f-vvp4-7g6p (CVE-2026-70159). So an application referencing the agent got NU1903 at restore, and that is an error wherever warnings are errors. The fixed client asks for Microsoft.Extensions.DependencyInjection, Logging and Logging.Abstractions 10.0.13 or later, so an application referencing the agent now resolves those at least that high. The agent's own nuspec no longer repeats the framework-extension floors it inherits from Quartz. (#​4065)
  • Quartz.Weasel.Oracle and Quartz.Weasel.SqlServer require System.Security.Cryptography.Pkcs 9.0.21. Their database drivers, reached through Weasel, bring Pkcs 8.0.0 and 9.0.4, which GHSA-2fmv-rv2c-59x7 and GHSA-846f-f764-76mx cover. Neither package calls it. The direct reference lifts an application's graph to the fixed version, and can go once the drivers require a fixed one themselves. (#​4065)

The same SignalR fix ships for 3.x's Quartz.Dashboard in 3.22.6.

What's Changed
Other changes
  • The restore graph takes the October 13 security fixes for SignalR and the cryptography packages by @​lahma in #​4065

Full Changelog: quartznet/quartznet@v4.5.0...v4.5.1

v4.5.0

Quartz.NET 4.5 is one dashboard for a fleet. It fronts schedulers in other processes, on other machines and in other clusters, and tells apart workers that all run QuartzScheduler. Targets fronting one clustered store show as one row with its nodes. A new package, Quartz.Dashboard.Agent, lets a worker dial out to the dashboard, so the worker opens no inbound port. Six more packages are proven under native AOT on every pull request. An application on 4.4 compiles on 4.5 unchanged, and the database schema did not change.

dotnet add package Quartz --version 4.5.0
Highlights
  • A fleet, keyed target/name. Each AddQuartzHttpClient registration names its process with HttpClientOptions.Target, so w1/QuartzScheduler and w2/QuartzScheduler are two rows, beside store-attached windows and the local schedulers. Fleet
  • A cluster shows once. Targets that front one clustered store are detected and merged into one row, w1+w2+w3/QuartzScheduler, whose Cluster page starts, stands by, shuts down and interrupts each node. Fleet
  • Workers dial out: Quartz.Dashboard.Agent. UseDashboardAgent connects a scheduler to a dashboard over SignalR. The dashboard enables it with AcceptAgents. The worker needs no inbound port, no HTTP API and no web framework. Dashboard agent
  • Secure by default. The agent refuses every job type a dashboard names until IsJobTypeAllowed is set. The hub reads its token from the Authorization header only. A hub with neither tokens nor a policy fails at startup. Dashboard agent
  • Native AOT, proven per package. Quartz.Jobs, Quartz.Plugins, Quartz.Plugins.TimeZoneConverter, Quartz.HttpClient, Quartz.AspNetCore and Quartz.Dashboard.Agent now say IsAotCompatible. Each one is published as a native executable and run on Windows, Linux and macOS on every pull request. Trimming and native AOT
Fleet
  • Identity is target/name (#​4042, closes #​3774).
    • The new public SchedulerRef(SchedulerName, Target) is the key a dashboard addresses a scheduler by. Parse splits at the first /. A bare name is the key of a scheduler reached through no target.
    • HttpClientOptions.Target names the process. It becomes the client's service key and the first half of the key. / and + are refused.
    • The Schedulers page shows one row per key, with Target and Reached columns. The picker groups by target, and remembers the selection in the qz_scheduler cookie.
    • The HTTP API's {name} resolves to the untargeted entry only. A name held only by targets answers 404, naming the targets.
    • A local scheduler whose own name contains / keeps that name everywhere: in resolution, authorization, history and live logs.
  • A cluster behind several targets is one row (#​4042).
    • A fleet monitor asks every target which nodes it sees at QuartzDashboardOptions.ClusterDetectionInterval, one minute by default. Targets on one clustered store merge into a+b+c/name.
    • Membership is sticky: a node that stops answering stays in its cluster as unreachable, so the key changes on a topology change, never on an outage. Each round elects a preferred member from the targets that answered.
    • The Cluster page reaches each node through its member key. A cluster-wide lifecycle verb is refused, because a cluster has no single lifecycle.
    • Log events 9105–9110.
  • SchedulerRegistration gains Key, Members and LastSeenUtc. GET …/schedulers rows gain members and lastSeenUtc. SchedulerResource.Target lets an authorization policy see which process it is asked about.
Dashboard agent
  • New package Quartz.Dashboard.Agent (#​4044, closes #​3773).
    • Worker: UseDashboardAgent(o => { o.Endpoint = new("https://ops.example/quartz/agents"); o.Token = …; o.Target = "w1"; }).
    • Dashboard: AcceptAgents(a => a.Tokens.Primary = …).
    • The connection retries forever, from 1 s back to 30 s. The dashboard shows the worker Unknown, with when it was last seen, after three missed heartbeats (45 s by default). It forgets the worker after ForgetAfter, one hour by default. A restarted worker comes back on the same row.
    • History, live logs and every page work over the connection. History is read through the tunnel and the database schema is unchanged.
    • Only the SignalR client is referenced, with no Microsoft.AspNetCore.App.
  • What the agent accepts is decided on the worker.
    • ReadOnly, IsOperationAllowed, MaxConcurrentOperations (a bounded queue; overflow answers 503), MaxPageSize, and a message cap of 4 MiB by default.
    • IsJobTypeAllowed unset refuses every request that stores a job by type name, with 403. Triggering, pausing, rescheduling and deleting existing jobs all work.
    • A cancelled or timed-out operation is cancelled on the worker too, whether it is queued or running.
  • How the hub is authorized.
    • Bearer tokens in two slots, Primary and Secondary, for rotation. They are compared in constant time and read from the Authorization header only, never from a query string, because a URL ends up in access logs.
    • Alternatively, an AuthorizationPolicy.
    • A token-only hub stays reachable under a fail-closed fallback policy.
  • Log events 9111–9117 (dashboard) and 9300–9305 (agent).
  • One dashboard instance per deployment in agent mode: the agent registry lives in the process.
Trimming and native AOT
  • Six side packages say IsAotCompatible (#​4051, part of #​3341).
    • Quartz.Trimming.Canary runs Quartz.Jobs, Quartz.Plugins and Quartz.Plugins.TimeZoneConverter as a native executable.
    • Quartz.Trimming.Canary.Wire runs Quartz.AspNetCore, Quartz.HttpClient and Quartz.Dashboard.Agent the same way.
    • The build fails on any warning a package's baseline does not record. None of the six produces an IL3050.
    • VerifyReferenceAotCompatibility stops listing them.
    • Quartz.Jobs records no trim warning any more.
  • Quartz.Extensions.Redis, Quartz.Aspire and the Quartz.Weasel* packages stay trimmable without the claim, because each needs a server or a database that nothing can run natively here. Which packages say whether they can be trimmed
Scheduling and the dashboard
  • A freed execution-limit slot wakes the scheduler loop (#​4041, closes #​4033). A node parked until its next look used to wait out that look when a firing ended in a group held at its limit, or when a held pinned trigger became free. Now:
    • it looks again as soon as the slot frees;
    • while a held trigger is re-polled early, the look-ahead is bounded so the node acquires nothing that is due after its next look.
  • The Backfill dialog's preview says when Max slots would refuse the range, and how far the spacing spreads it (#​4038, closes #​4037). Thanks to @​dongfo, in a first contribution.
Behaviour changes
Before After What to do
IQuartzApiClient members took a bare scheduler name They take a key; a bare name is the key of a scheduler reached through no target Nothing for a 4.4 deployment. A replacement client parses keys with SchedulerRef.Parse
The HTTP API's {name} resolved to the first entry under the name It resolves to the untargeted entry only Nothing for a 4.4 deployment
AddQuartzHttpClient was keyed by the scheduler name It is keyed by Target when one is set If you set Target, resolve GetRequiredKeyedService<IScheduler>(target)
A second Target of the same name was impossible It throws SchedulerConfigException Give each process a Target of its own
AddQuartzDashboard() registered no scheduler repository It registers Quartz's shared services, so an agent-only dashboard needs no AddQuartz Nothing
The Schedulers page showed one row per name It shows one row per key, and one row per detected cluster Nothing
Upgrading

An application on 4.4 compiles on 4.5 unchanged. No migration to run: the schema is the same, and 4.4 and 4.5 nodes run side by side on one store. Every addition and behaviour change is in the migration guide's Upgrading from 4.4 to 4.5.

Public API

Additions only: +66 lines, none removed, and no interface gained a member.

Assembly Lines added
Quartz.Dashboard 24
Quartz.Dashboard.Agent (new) 23
Quartz 15
Quartz.HttpClient 3
Quartz.AspNetCore 1

Quartz.Dashboard now depends on Quartz.HttpClient, at the same version. The whole delta, member by member: git diff v4.4.0 v4.5.0 -- src/Quartz.Tests.Unit/Verify src/Quartz.Tests.AspNetCore/Verify.

Also
  • The docs build takes patched npm dependencies (#​4040). Test, analyzer and Aspire hosting packages were updated.
  • The comparison page describes 4.5 (#​4050).
  • Marten and Wolverine are held at 9.45 and 6.45 in the tests and examples while the Weasel floor holds at 9.39.0 (#​4056).
What's Changed
Other changes
  • Main builds 4.5.0, and the benchmark notes record what 4.4.0 measured by @​lahma in #​4035
  • The docs build takes the patched shell-quote, proxy-addr, compression, source-map-js, Vue, smol-toml, postcss-selector-parser and KaTeX by @​lahma in #​4040
  • A freed execution-limit slot wakes the scheduler loop, and a node looking again early for a held trigger acquires nothing due after its next look by @​lahma in #​4041
  • Call UpdatePreview on spacing and max slots change by @​dongfo in #​4038
  • The dashboard keys a scheduler by target/name and shows targets fronting one cluster as one row by @​lahma in #​4042
  • A scheduler dials out to a dashboard with Quartz.Dashboard.Agent and is shown and driven over that connection by @​lahma in #​4044
  • The comparison carries what 4.5 added by @​lahma in #​4050
  • Six side packages run as native executables on every pull request and say IsAotCompatible by @​lahma in #​4051
  • Dependabot holds Marten and Wolverine minor and patch updates while the Weasel floor holds by @​lahma in #​4056
  • The agent page and readme agree on the key and the trimming claim, and the CI legs get the time they now take by @​lahma in #​4057
New Contributors

Full Changelog: quartznet/quartznet@v4.4.0...v4.5.0

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

[!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 2 times, most recently from 6458b83 to d3e99b2 Compare October 13, 2026 21:49
@renovate
renovate Bot force-pushed the renovate/major-quartznet-monorepo branch from d3e99b2 to 9279bbe Compare October 15, 2026 16:46
@renovate
renovate Bot force-pushed the renovate/major-quartznet-monorepo branch from 9279bbe to 8d21d43 Compare October 20, 2026 14:07
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