Repository navigation
Bump the quartz group with 3 updates - #104
Closed
dependabot[bot] wants to merge 1 commit into
Closed
dependabot[bot] wants to merge 1 commit into
dependabot[bot] wants to merge 1 commit into
Conversation
Bumps Quartz from 3.18.0 to 4.1.1 Bumps Quartz.Extensions.DependencyInjection from 3.18.0 to 4.1.1 Bumps Quartz.Extensions.Hosting from 3.18.0 to 4.1.1 --- updated-dependencies: - dependency-name: Quartz dependency-version: 4.1.1 dependency-type: direct:production update-type: version-update:semver-major dependency-group: quartz - dependency-name: Quartz.Extensions.DependencyInjection dependency-version: 4.1.1 dependency-type: direct:production update-type: version-update:semver-major dependency-group: quartz - dependency-name: Quartz.Extensions.Hosting dependency-version: 4.1.1 dependency-type: direct:production update-type: version-update:semver-major dependency-group: quartz ... Signed-off-by: dependabot[bot] <support@github.com>
Contributor
Author
|
Looks like these dependencies are updatable in another way, so this is no longer needed. |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Updated Quartz from 3.18.0 to 4.1.1.
Release notes
Sourced from Quartz's releases.
4.1.1
Quartz.NET 4.1.1 is a maintenance release with one real bug in it: a scheduler that published traces produced one trace per process rather than one per firing, and it grew for as long as the process lived. Nothing about how a trigger fires changed, the schema is 4.0's, and the public API gained two overloads and lost nothing.
What changed
A firing is a trace of its own again — subscribing to
ActivitySource("Quartz")gave a single unbounded trace: every span the scheduler's loop opened became the parent of the next one, and eachQuartz.Job.Executehung off whichever was current when it was dispatched. A day of a quiet staging pod arrived at the backend as two trace ids and a tree several thousand spans deep, with the jobs' own EF Core andHttpClientspans buried under thousands ofQuartz.JobStore.AcquireNextTriggers. The documentation has promised the opposite since 4.0 — the firing is its own trace root — and nothing tested it, because the observability suite reads tags off one span at a time and a span's parent is not a tag.Two independent defects made that trace, either sufficient on its own. The store decorator started its span in the synchronous override and stopped it inside the asynchronous continuation:
Activity.Currentis anAsyncLocal, so the start wrote onto the caller's execution context and the stop put the parent back onto the continuation's, which is discarded — leaving the caller with the span current for good, once per iteration of a loop that lives as long as the process. And the execute span took whatever activity was ambient as its parent, so merely starting the scheduler inside a request decided every firing's trace for the life of the application.Quartz.Job.ExecuteandQuartz.Job.Vetoare now created with an empty parent context — so a parent-based sampler is asked about a root rather than about a context that is being discarded — withActivity.Currentcleared around the start that re-reads it, and restored when the span stops. The store spans are started and stopped on one execution context. The three background loops — the scheduler thread, the cluster manager and the misfire handler — clearActivity.Currenton entry, because their task captured the execution context of whoever calledStart()and they outlive that call by the whole life of the process. Store spans still belong to whoever made the call, soscheduler.ScheduleJob(…)inside a request stays in that request's trace. (#3797, #3799)On the reporter's own repro, which expected 7 traces and 7 roots:
Quartz.Job.Vetoalso gained theActivityLinkback to the call that scheduled the firing, which it never carried; a refused fire is now walked back from exactly as an executed one is.CreateActivityrather than added after it.ActivityListener.Sampleruns while the activity is created, so a link added afterwards was one no sampler ever saw — and sampling a consumer span by the trace that produced it is the reason links are given to samplers at all.An execution limit or a job timeout can be read from the container — 4.x dropped the
AddQuartzoverloads that handed the callback anIServiceProvider, and the replacement the migration guide led with (readIConfigurationat registration time) bypassesConfigure,PostConfigureandIValidateOptions. Everything else onIQuartzBuildereither lands in named options or has aFunc<IServiceProvider, …>shape;UseExecutionLimitsandAddJobTimeoutcomputed their value eagerly and registered an internal type, so neither could be reached from the container at all. Both now have a shape that is handed one when the scheduler is built. Precedence is untouched — both shapesTryAdd, so the first declaration in code wins and code still beats thequartz.executionLimit.*keys. The migration guide now leads with the options pattern rather than with reading a section yourself, and names the caveat that made the old advice misfire: a scheduler's options are configured under its own name, so a bareAddOptions<T>()silently configures nothing of a named scheduler's. 3.x is unaffected — its four(configurator, IServiceProvider)overloads are still there. (#3794, d523c898bc)The documentation's dead links are fixed — the cron pages pointed at FreeFormatter.com, which is gone; they now answer with the library itself, and a handful of other rotted links across the 1.x–4.x trees were fixed with them. (#3796)
Public API — two additions, nothing else
QuartzBuilderExtensions.AddJobTimeout(IQuartzBuilder, Func<IServiceProvider, TimeSpan?>)QuartzBuilderExtensions.UseExecutionLimits(IQuartzBuilder, Action<IServiceProvider, ExecutionLimitsBuilder>)v4.1.0..v4.1.1baseline diff: 2 added lines, 0 removed. Every package's dependency list is byte-identical to 4.1.0 — theMicrosoft.Extensions.*floors stay at10.0.0, so an application pinned a servicing patch lower still restores.One source-level wrinkle the second overload brings:
AddJobTimeout(null)written with a literalnullis now ambiguous between the two shapes. It means whatAddJobTimeout()means; write that. Every other spelling — aTimeSpan, aTimeSpan?variable, the named-argument form, no argument at all — is unchanged, andUseExecutionLimits(null)is unaffected.Upgrading
From 4.1.0:
dotnet add package Quartz --version 4.1.1, and nothing else. There is no schema change, no configuration change, and no code change to make.If you worked around #3797 by leaving
AddSource("Quartz")out and opening a root in anIJobExecutionMiddleware, you can drop the middleware and subscribe normally — and keep theQuartz.JobStore.*spans, which are now short traces of their own rather than noise in yours.Full changelog: quartznet/quartznet@v4.1.0...v4.1.1
4.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.
Highlights
ISchedulerRuntime(it extendsISchedulerRegistry, 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.LookupSchedulernow 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 withinSchedulerRestartOptions.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 nextRestartfinishes 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)MON/2parses and means what2/2means (a step through the week, not 3.x's fortnight), andMON-FRI/2is2-6/2where 4.0 silently read it asMON-FRI. (#3732, #3733 [85214e4499…e29f7c4283]). The century walk ofCronCalendar.GetNextIncludedTimeUtc(#3690) shipped in 4.0.1.POST …/triggers/{group}/{name}/update-detailsedits 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 anullclears it), andHttpScheduler.UpdateTriggerDetailsno longer throws —ContextandListenerManagerare the two members a wire cannot carry (#3681, #3735).takesays in the OpenAPI document why it is a string: a page size, orallfor everything up toMaxPageSize(#3682, #3736).GetStatus(ct)andGetSchedulerInstanceId(ct)are the asynchronous twins of the twoISchedulerproperties a wire cannot answer without blocking — default interface members that answer the property locally, one round trip onHttpScheduler, and on the container's deferred scheduler they build it on first use where the properties throw (#3684, #3738).QuartzHttpApiOptions.IsJobTypeAllowedand 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 a403naming the type and a warning in the log, and the default is unchanged (#3685, #3741).quartz_jobs.json, in theQuartz:Schedulesection 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)AddQuartzDashboard()besideAddQuartzHttpClient("acme", …)now fronts the scheduler in the other process honestly: it is listed with the new originRemote, 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'sStatuswhile 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 reportsUnknownfor the ones that did not answer, the pages page instead of asking for everything, and a secondAddQuartzHttpClientunder the same name is refused rather than silently winning. Execution history is now Quartz's rather than the dashboard's:IExecutionHistoryStorein core with an in-memory default bounded by age and count, a recorderAddQuartzExecutionHistory()installs against every scheduler, threeGET …/schedulers/{name}/history/…routes the HTTP API serves, and a read-only reader of them thatAddQuartzHttpClientregisters per target.QuartzHttpApiOptions.ReadOnlyturns 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)Informationline,9007, naming the caller (HttpContext.User.Identity.Name, or(anonymous)), the operation, the scheduler and the route — until now aPOST …/shutdownthat worked was recorded nowhere. The dashboard's Action Log entries and their9100/9101lines 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)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
ISchedulerRuntime : ISchedulerRegistry—Add,Remove,RestartSchedulerAddOptions,SchedulerRestartOptions(readonly record structs),SchedulerRestartExceptionIScheduler.GetStatus(ct),IScheduler.GetSchedulerInstanceId(ct)— default interface membersHttpScheduler.GetStatus/GetSchedulerInstanceIdoverridesQuartzHttpApiOptions.IsJobTypeAllowed,QuartzDashboardOptions.IsJobTypeAllowedSchedulerOrigin.Remote;SchedulerRegistration.SchedulerInstanceId(init)QuartzHttpApiOptions.ReadOnly403problem details; the two bulk fetches stay readsQuartzHttpApiOptions.EventStreamHeartbeatIntervalIExecutionHistoryStore,ExecutionHistoryEntry,MisfireHistoryEntry,ExecutionHistoryQuery,MisfireHistoryQuery,ExecutionHistoryOptions,AddQuartzExecutionHistory()IDashboardHistoryStorekeeps working as its adapter in both directionsNothing was removed or reshaped (
v4.0.1..v4.1.0baseline diff: 96 added lines, 0 removed). The one dependency change:Quartznow depends onMicrosoft.Extensions.Logging.Abstractionsrather thanMicrosoft.Extensions.Logging; an application that tookLoggerFactoryorAddLoggingtransitively from Quartz adds the package itself (#3730, #3745).Behaviour changes worth noting
MON/2parses (4.0 refused it; 3.x read it as every second week) andMON-FRI/2fires 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. AQuartzSchedulerBuilderwith a provider registered but no factory refuses to build (see #3730).GetNextInvalidTimeAfterreturnsnullwhere it used to return a valid instant (4.0.1). A shut-down container scheduler injected asISchedulerre-points to the generationISchedulerRuntime.Restartbuilt. Mapping the HTTP API now records execution history in memory, bounded (24 h, 2,000 rows per scheduler);ExecutionHistoryOptions.MaxEntriesPerScheduler = 0records nothing.AddQuartzHttpClientthrows on a scheduler name it already registered. A 4.0 client parsingGET …/schedulersfrom a 4.1 host that fronts a proxy sees the origin"Remote"it does not know — upgrade the client.AddQuartzDashboard()no longer registersDashboardLiveEventsPlugin; a reverse proxy needs to forward{DashboardPath}/hubonly for SignalR clients of your own, not for the pages. Log events9100and9101end with(origin …, node …)— a pipeline parsing the old template needs the new one.Also
CheckinInterval+CheckinMisfireThresholdhas 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 atDbRetryInterval, 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 thanDbRetryInterval— 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.CheckinMisfireThresholdpast the timer ceiling is now refused at startup, likeCheckinInterval. (#3777, #3778; 3.22 carries the same change on 3.x)DbRetryIntervalas before.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 — onlyRequestRecoverywould ever have replayed it — and a completion reported after the store had closed was aborted, leaving the triggerBLOCKEDfor 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)Shutdown(waitForJobsToComplete: false)— the hosted service's default — can take up to two seconds longer when a job is in flight.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 — beforeStart(), so the recovery sweep found nothing to recover.ReplaceTriggernow 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 storedBLOCKEDbehind an execution still in flight rather thanWAITINGbeside 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)TRIGGER_ACCESS— returns nothing and throws nothing, so a cluster stopped scheduling in silence.DbLockHandlernow logs event3716once per acquisition that outlivesAdoJobStoreOptions.LockWaitWarningThreshold(30 s by default;nullreports nothing), every acquisition is timed onquartz.jobstore.lock.wait.duration, andquartz.jobStore.commandTimeout— which 3.22 adds — translates toAdoJobStoreOptions.CommandTimeoutwhere 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)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, andthreshold >= DbRetryIntervalis no longer a relation an operator has to know about. The 3.x configuration reference'sclusterCheckinIntervaldefault (7500, not 15000) and the missingclusterCheckinMisfireThresholdrow were fixed on the way. (a5b2197e5d)redisintegration leg now runs two clustered nodes over one PostgreSQL store withRedisLockHandleras 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 listedQuartz.Extensions.Redisas the least exercised package; it no longer is. (#3722, #3744)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, soquartz.jobStore.dbRetryIntrevalstarted the scheduler with the default in force and said nothing — which is also howquartz.jobStore.commandTimeoutwent 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 = falseallows 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)quartz.jobStore.*typo that 4.0 tolerated stops the scheduler from starting.... (truncated)
4.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.
What changed
Microsoft.Extensions.*dependency at10.0.11, the newest patch on the day it was built, so a project pinned at 10.0.9 or 10.0.10 hitNU1605"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.0for the framework extensions,13.0.2for Newtonsoft.Json (13.0.1 reflects overTimeOnlymember by member and a job data map loses its seconds — a test said so),1.15.3for OpenTelemetry.Extensions.Hosting (the first version whoseOpenTelemetry.Apicarries no advisory),3.0.0for StackExchange.Redis and7.0.0for 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, c9ecf892bd)Quartz(Quartz.Extensions.DependencyInjection,Quartz.Extensions.Hosting,Quartz.Serialization.SystemTextJson, andQuartz.Serialization.Json, whose successor isQuartz.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.OpenTracingandQuartz.OpenTelemetry.Instrumentationdeliberately have no such package: neither has a 4.x replacement, and an empty one would hide that. (#3717, c28f213cbd)CronCalendar.GetNextIncludedTimeUtcno longer walks a century one second at a time — it askedCronExpression.GetNextInvalidTimeAfterfor 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 answersnull, and the calendar turns that into aSchedulerExceptionnaming 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, a125809ba9, 4d60904c6f)GetNextInvalidTimeAfterreturnsnullwhere it used to return a valid instant after giving up, and aCronCalendarwhose expression excludes every instant now fails fast withSchedulerException.FS0856onIJob.Execute's arity,FS0041onScheduleJoboverload resolution,Async.AwaitTaskwith aValueTask,FS0039forStdSchedulerFactory), each quoted with its fix, and every sample is copied from a compiled example project the build keeps honest. (#3718, 1b97b49f41, aff4feac69)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
4.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:Highlights
net10.0only — nonetstandard2.0build, no Full Framework, no.configsupport.Quartzpackage;StdSchedulerFactory,quartz.configdiscovery and the process-globalSchedulerRepository.Instance/DBConnectionManager.Instanceare gone. Flatquartz.*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 failsHost.Build()with every failure listed.IJob.Executetakes aCancellationToken,IJobFactoryhands out aJobScoperather than a bare instance, every publicTaskbecameValueTask, and every asynchronous member ends with a cancellation token.QueryJobs/QueryTriggersreturn aPagedResult<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.TriggerState.Executingsays 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.ISchedulerclient 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.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-3in 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.CalendarIntervalTriggerwithPreserveHourOfDayAcrossDaylightSavingssteps 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 noAspire.*dependency; a store can provision its own schema as it starts (ProvisionSchema()), safe under a cluster racing to start.MapQuartzHttpApi()andMapQuartzDashboard()refuse to start unless the endpoints carry authorization or an explicitAllowAnonymous(); a scheduler can be authorized on its own name, and every call the dashboard makes is authorized where it is made.ActivitySource("Quartz")andMeter("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.QuartzdeclaresIsAotCompatible; 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).RAMJobStore(numbers below).JobRunShellare 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
net10.0only.Quartz.Extensions.DependencyInjection,Quartz.Extensions.HostingandQuartz.Serialization.SystemTextJsonare part ofQuartz;Quartz.Serialization.JsonisQuartz.Serialization.Newtonsoft;Quartz.OpenTracinghas no 4.x release, andOpenTelemetry.Instrumentation.Quartzproduces nothing on 4.x — subscribe to the source and meter directly. The first error a mixed project produces is CS0433 (a type in bothQuartz4 and a 3.x satellite): remove the three references.StdSchedulerFactory,DirectSchedulerFactoryandquartz.configare gone;AddQuartz(…)orQuartzSchedulerBuilder.Create(q => …)build a scheduler.Task→ValueTaskon nearly every member, and aCancellationTokenon every asynchronous one.Quartz.SpiisQuartz.Extensibility,Quartz.Simplmerged intoQuartz.Impl, the Newtonsoft types left the core namespaces. A string naming an old namespace still resolves, with a warning.*Supportbase classes are gone (every member has a default body); a listener with a 3.x signature is refused at registration.TimeOnlyandDateOnlyreplaceTimeOfDay;TimeProviderreplacesSystemTime; the semaphores are lock handlers.?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 Sunday1) and both day fields restricted (Quartz fires on the union). The guide's second audit finds them.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:
ResumeAllcould unpause real trigger groups: it deleted the all-groups sentinel with aLIKE, and the sentinel's four underscores are wildcards.... (truncated)
4.0.0-rc.2
The second release candidate. rc.1 was put to the test it was made for — a real application, on 3.19.1 in production, upgraded by the guide — and that upgrade found three things the pre-release gates had not. They are fixed here, and 4.0.0 is this commit once the same upgrade is repeated against it and comes back clean.
Changed since rc.1
RescheduleJobandUpdateTriggerDetailsresolved the job's class in order to decide whether the new trigger could run, and failed in a process without the assembly — theZeroSizeThreadPooladministration node, a web application that edits schedules the worker runs. Both now read the job's two attribute flags fromQRTZ_JOB_DETAILS.IS_NONCONCURRENTandIS_UPDATE_DATA, as every other read already did, and the decision is right without the class: a trigger stored for a[DisallowConcurrentExecution]job that is executing isBLOCKED, notWAITING. Such a process needs noITypeLoaderof its own and no placeholder type; every administration operation — read, query, pause, resume, reschedule, update, trigger, add a trigger, edit job data, delete — is pinned by a test that runs a worker and an administration node over one store with the class visible only to the worker. Only the firing path resolves a job's class.host.StopAsync()whilehost.RunAsync()is pending makes the generic host stop every hosted service twice concurrently;QuartzHostedServiceenumerated a list the other stop was clearing andRunAsyncthrewInvalidOperationException: Collection was modifiedsix runs in eight. There is now one stop, and a second caller joins it.1-7from Sunday) than in Cronos or NCrontab (0-6from Sunday). Cron expressions states the two divergences; the migration guide's second audit is a compiled sample that lists the stored expressions worth reading again.IgnoreDuplicates, without which every restart re-declared the trigger and scheduled it afresh.qsandfast-uriadvisories (#3704).Public API: unchanged from rc.1 — the baselines did not move. Schema: unchanged. Nothing in the packages' dependencies changed.
If you ran rc.1
Nothing to run. The schema and the configuration are rc.1's. If you had written a placeholder-type
ITypeLoaderfor an administration node, delete it: it was always a hazard, because whether the placeholder carried[DisallowConcurrentExecution]decided how a replacement trigger was stored for a job it was standing in for.If you ran an earlier pre-release
The migration guide's appendix If you ran a 4.0 pre-release lists every change between one build and the next, with the section that explains each; the rc.1 notes carry the "If you ran beta.1" list.
What stands behind this build
Everything rc.1's notes describe — the fresh-user journeys against the published packages, the security review, the stable-version pack, the rolling upgrade of a running 3.20 cluster on PostgreSQL and SQL Server, the clustered soaks on both — plus the published-rc.1 check, which built the six documented journeys from the published pages and verified every "If you ran beta.1" item against a beta.1-provisioned schema. The three findings above came from outside those gates: a production application, upgraded by the guide. That is the test 4.0.0 waits for.
Full changes: quartznet/quartznet@v4.0.0-rc.1...v4.0.0-rc.2
4.0.0-rc.1
Quartz.NET 4.0.0-rc.1 — the packages are on NuGet.
Beta.1 made the API a promise. This is the release candidate: 4.0.0 is this commit, tagged as soon
as the remaining hand-run gates are green and no later than a week from now. What changed between the
two is below; what will change between this and 4.0.0 is nothing but the release notes and the site.
The gates this build passed are the ones release day depends on, run against the published beta.1
packages and the documented pages rather than the tree: a developer building every documented journey
from scratch, every example project started, a security review of the HTTP API, the dashboard and the
client, a stable-version pack (this repository had never produced one), the 3.20 → 4.0 upgrade of a
running cluster through its mixed-version window by hand, and a clustered soak. They found things.
Every one is fixed here or named below.
Changed since beta.1
One security fix. With
QuartzDashboardOptions.SchedulerAuthorizationPolicyconfigured — themulti-tenant deployment that option exists for — a dashboard visitor authorized for one scheduler could
read another's job listing by switching the scheduler picker: the picker took the browser's value on
trust, pages already on screen re-read for it on a synchronous event, and the frame's access check
lagged behind on an asynchronous one. Both halves are closed: the picker refuses a name the filtered
listing did not carry, and every call through the dashboard's own
IQuartzApiClientis authorizedbefore the scheduler is looked up, where
ReadOnlywas already enforced. The same review found nohole an unauthenticated caller can reach; its "verified not an issue" list is on the pull request.
A job type named over the wire is a name, on both sides. Beta.1's contract types said a job type
that arrived in a request was stored unresolved and never probed;
JobBuilder.Build()resolved itanyway, on the request thread, and a test that claimed otherwise could not tell. Now: neither side
resolves a name while converting a DTO; the two attribute flags are optional on the wire — an omitted
concurrentExecutionDisallowedused to override[DisallowConcurrentExecution]on the real type withfalse— a job whose type resolves nowhere reads as200with the flags absent instead of a permanent500, a client can schedule a job whose type only the server has, a hostile server can no longerchoose an assembly name a client's runtime goes looking for, and a name is checked against
IJobbefore anything constructs it — a caller-named type used to have its static constructor, module
initializer and instance constructor run, with the scheduler scope's services injected, before the cast
refused it.
Scheduling.IgnoreDuplicates = trueon its own works. On beta.1 it was a startup error, becauseOverwriteExistingDatadefaults totrueand the validator refused the pair — the one-liner the guideitself recommended, and the one this repository's own Worker and ASP.NET Core examples wrote, so neither
example started. The default is a default rather than a statement now: setting
IgnoreDuplicatesturnsoverwriting off, and only writing both down is refused. The XML and JSON scheduling files follow the
same rule.
SendMailJobsends a container credential only to a host the container vouched for. The documentedmitigation — keep the SMTP password out of job data, register an
ICredentialsByHost— handed a bareNetworkCredentialto whateversmtp_hostthe job data named, as base64AUTH LOGIN, to whoever canschedule the job. A host-agnostic credential with a job-data host is refused, with the message naming
CredentialCacheand the host to bind it to; a bound cache answers for its host and nothing else.smtp_enable_sslis new and off by default, which isSmtpClient's own default.The shipped jobs look only where an application put something.
DirectoryScanJobfound its... (truncated)
4.0.0-beta.1
Quartz.NET 4.0.0-beta.1 — the packages are on NuGet.
Alpha.1 was the API becoming complete, alpha.2 the API being used, alpha.3 the release where Quartz is
operated, alpha.4 where it is integrated, alpha.5 where the API is finished. Beta.1 is the release where
the API is a promise: every public member has been read against the code it describes, every
documented behaviour has a test or a named caveat, the upgrade from 3.20 has been rehearsed on data a
released 3.20 wrote, and the operational defaults are safe rather than merely documented. It is the first
4.0 build we ask you to run in production — with the caveats list at the end of this page in hand.
Nobody outside this repository has run 4.0 in production yet: the five alphas total a few hundred
downloads and no external issue. Beta.1's validation is our own, which is why it is a beta. The API is
frozen from here to 4.0 — additive changes only, and any member added to a public interface arrives as a
default interface member. The schema is frozen once the 4.0 script has run. Stored blobs and the wire
format are frozen.
Changed since alpha.5
Six statements the frozen surface made were not true, and were corrected rather than documented around.
Each has a migration-guide row under Between the alphas and beta.1; coming from 3.x, read them as part
of 4.0.
MapQuartzHttpApi()andMapQuartzDashboard()refuse to start when nothing authorizes them. Analpha application that mapped either bare gets an
InvalidOperationExceptionat startup naming the threefixes:
RequireAuthorization()on the returned builder, the options' authorization policy, or an explicitAllowAnonymous(). Both surfaces schedule a job whose type is a string from the request; withQuartz.Jobson the path that is remote code execution, and an open endpoint is no longer the default.MapQuartzDashboard()returns anIEndpointConventionBuildercovering the pages and the SignalRhub, so one
RequireAuthorization()is the whole answer; the old return type protected the pages alone.IScheduler's mutation members raiseArgumentNullExceptionfor a null argument, as the extensionmethods on the same type always did;
SchedulerException("… cannot be null")is gone.CronExpression.TryParse(s, format, out)answersfalsefor aCronFormatit does not know ratherthan throwing out of a
Try; the constructor andParseagree onArgumentNullException.TriggerBase.FireInstanceId/IOperableTrigger.FireInstanceIdandDbMetadata.ParameterDbTypePropertyNamearestring?.QuartzHttpApiOptions.MaxPageSize(1000), and a500no longercarries the exception's message unless
IncludeStackTraceInProblemDetailsis on.Three more corrections change what a running program does without changing a shape:
Quartz:Schedulingrefuses
IgnoreDuplicateswhileOverwriteExistingDatais on (the pair was silently inert); ajob-data string written with a decimal comma (
"3,14") is unreadable by every numeric accessor insteadof reading as
314from the floating-point ones; and a persistent store refuses a repeat interval finerthan a millisecond instead of storing it as zero and wedging the trigger (#3673).
The upgrade is rehearsed
A console project on the released
Quartz3.20.0 packages seeds a database with every persistedtrigger family, blob-stored triggers, all six calendar kinds, a job data map of every admitted value type,
paused trigger and job groups with members added after the pause, and a firing abandoned by killing the
process; the 4.0 script runs over it; a 4.0 scheduler starts and checks every row against the manifest,
recovers the abandoned firing and fires everything. On every dialect, on every pull request. The blobs it
captured are now the unit fixtures the serializers are held to.
... (truncated)
4.0.0-alpha.5
Quartz.NET 4.0.0-alpha.5 — the packages are on NuGet. Alpha.1 was the API becoming
complete, alpha.2 the API being used, alpha.3 the release where Quartz is operated, alpha.4 where
it is integrated; alpha.5 is the release where the API is finished. This is the last alpha:
breaking changes end here, and what ships now is the shape 4.0 keeps.
Cron says what it means
The whole cron surface was put against Vixie cron and Cronos, and every place where Quartz silently
did something other than what the expression said is gone:
0 15 10 1 * *—the natural Unix spelling of "the 1st at 10:15" — used to fire every day of the month; it now
fires on the 1st. A field written
*or?restricts nothing (the two are now full synonymsthere); when both day fields name days, the union fires — the
crontab(5)rule, kept deliberatelywhere Cronos would AND.
IsSatisfiedByagrees it fired, which theold delta-shift could never manage. Fall-back behavior is unchanged from alpha.4's Cronos-aligned
rules.
1-5W(theWwas dropped),L-3inday-of-week (meant "Saturday"),
MON,FRI#3(the Friday was ignored),'C'(never implemented),steps of zero, and
MON/2— whose "every second week" had no stable phase and re-anchored on everymisfire, restart and failover. Fortnights belong to
RecurrenceScheduleBuilder; the migrationguide carries a pre-upgrade audit query for stored expressions.
CronExpression.Parse("30 4 * * 1", CronFormat.Unix)— day-of-week numbered 0–7 the crontab way, normalized to the canonical Quartzform. And the
@macros (@daily,@hourly,@weekly, …) work everywhere an expressionstring is read — code, JSON and XML files, the dashboard, the HTTP API — with no opt-in, including
through the hash-resolving paths.
Parse/TryParse/ParseWithHash/TryParseWithHashreplace fourvalidator members and two constructors that made
new CronExpression(expr, null)ambiguous;GetTimeBeforeis nowGetPreviousValidTimeBefore;GetExpressionSummary's internal-state dumpis gone.
A job's timeout is middleware
JobInterruptMonitorPlugin's stringly"AutoInterruptable"/"MaxRunTime"keys are retired. Atimeout is now
AddJobTimeout(TimeSpan)with a per-job[JobTimeout("00:05:00")]attribute; onexpiry the firing's own cancellation token fires through the standard interrupt path, and the
timeout reports as a retryable failure your
RetryPolicysees.The listener surface earns its place
IListenerManagerdrops its eight runtime matcher-mutation members (matchers are settled atregistration, and the notify path got faster for it); the three
Broadcast*listeners are gone (themanager already fans out);
TriggerMisfiredleads with its trigger like every sibling.Acquisition reads at the speed of its index
IDX_QRTZ_T_NFT_STnow carriesPRIORITY DESC, MISFIRE_INSTR: a 100k-trigger acquisition roundtrip drops from 21.6 ms to 589 µs on SQL Server (20,395 → 8 reads), with MySQL and Postgres wins to
... (truncated)
4.0.0-alpha.4
Quartz.NET 4.0.0-alpha.4
Alpha 4 is the integrator release: a framework that embeds Quartz — a message bus scheduling deferred
messages, a workflow engine firing timeouts, an application framework running background work — now
gets an integration surface designed for it rather than assembled from the app-facing API. The proof
is in this repository: MassTransit's Quartz integration, ported onto these primitives, lost 87 lines
and two whole classes, replaced three store round trips per upsert with one atomic call, and swapped
sixteen stringly-typed
JobDataMapkeys for one typed payload. A runnable Wolverine reference exampleships in the repo (
src/Quartz.Examples.Wolverine).Typed job input
IJob<TInput>— a job that takes a deserialized, typed payload; the input rides the trigger as asingle reserved string entry, survives every store and serializer, and is read back by static type
(#3521, #3540).
scheduler.ScheduleJob<TJob, TInput>(input, at | delay, options)— the one-liner: one durable jobper
TJob, one trigger per call, the trigger group as the correlation axis (#3522).IJobInputSerializerextensibility point; the default is System.Text.Json with the sametype-info-resolver registry the store serializer uses, so native AOT keeps working (#3540).
Scheduling as an integrator needs it
ScheduleJob(trigger, ScheduleJobOptions)replaces theExists → Unschedule → Scheduledance with one store operation under one lock (#3522).DeleteJobs(GroupMatcher<JobKey>)andUnscheduleJobs(GroupMatcher<TriggerKey>)return the removed keys; matching HTTP API endpoints(#3523, #3558).
QuartzHostedServiceOptions.AutoStart = false— the scheduler is built, boundand visible, and the embedding framework decides when it starts; the health check reports
Degraded, notUnhealthy, while it waits (#3525).Activityrecords the W3C tracecontext on the trigger; the firing's span links back to it — an
ActivityLink, not a week-longparent. On by default,
QuartzSchedulerOptions.PropagateTraceContextopts out (#3524, #3565).Job execution middleware
IJobExecutionMiddlewarewraps the execute call itself — something a listener structurally cannot do:open a scope around the job, translate exceptions, short-circuit. Registered per scheduler with
AddJobMiddleware<T>(), ordered, zero overhead when none are registered (#3526, #3553).Retry policy
Triggers carry a first-class retry policy:
TriggerBuilder.WithRetryPolicy(RetryPolicy.Exponential(5, TimeSpan.FromSeconds(30)))re-fires a failed job on its own schedule — persisted, cluster-safe,survives failover mid-wait and mid-execution, never burns a repeat count or an RRULE
COUNTslot,and yields to the next scheduled occurrence rather than double-firing. Exhaustion returns the trigger
to its ordinary schedule: one bad hour must not kill a cron trigger. A 15-case matrix pins the edge
cases on both stores;
quartz.trigger.retryjoins the meters.RefireImmediatelyremains what italways was — an in-process re-execution, not a retry (#3520, #3569).
... (truncated)
4.0.0-alpha.3
Quartz.NET 4.0.0-alpha.3 is the third pre-release of 4.0. Alpha.1 was the API becoming complete; alpha.2
was the API being used. This one is the release where Quartz is operated: a cluster can be read
from outside — its nodes, its execution groups, its misfires, its history and its meters all say which
node they came from; the persistent store is honest about what it stored and about what failed, and it
spends a statement on a set rather than on each of its members; the trimming and native AOT claim
stopped being a compile and became a scheduler that CI publishes natively and runs against a real
database; and the test suite now proves what the documentation had been promising — daylight saving end
to end, misfires through a store, clustering on every engine, and the job-store contract on every
dialect that ships.
This is still an alpha. There is no compatibility promise between pre-releases, and this one moves
several names and changes several behaviours. The
migration guide carries
every change with before and after; the sections named below are its.
There is no schema migration in this release. Nothing under
database/migrations/changed, so adatabase created or upgraded for alpha.2 needs nothing done to it. The fresh-install scripts under
database/tables/did change — all eight can now be told not to drop an existing schema — but thosebuild a new schema rather than upgrade one.
Before you start
An application written against alpha.2 will not build or start until these seven are dealt with.
A registered job may not take a scheduler's parts by constructor, and startup says so. A job type
the container holds is built by the container, which resolves constructor parameters without a
scheduler's service key — so a job on scheduler
acmetakingIScheduler,ISchedulerFactory,IJobStore,IThreadPoolor one of a scheduler's options types was handed the defaultscheduler's, or could not be built at all in a container holding only named schedulers. Options
validation now refuses it, naming the job, the parameter and the three ways to write it instead
(
IJobExecutionContext.Scheduler,IJobExecutionContextAccessor, orAddJobType<T>(provider => …)resolving the part by key). Every public constructor is examined, not the one the container would
pick, because which one that is depends on what else is registered.
TimeProviderandIServiceProviderare deliberately excluded.(closes #3388)
The semaphores are lock handlers. A semaphore is a counted permit; what Quartz has is a
mutual-exclusion lock over a named row, one holder, re-entrant per connection, released on the same
connection. The name came from the Java port rather than from anything the type does, and the builder
method (
UseLockHandler), the key (quartz.jobStore.lockHandler.type) and the how-to have said"lock handler" for as long as they have existed. The types now agree:
ISemaphoreILockHandlerISemaphore.ObtainLockILockHandler.AcquireLockSemaphoreContextLockHandlerContextDbSemaphoreDbLockHandlerSelectForUpdateSemaphoreSelectForUpdateLockHandlerUpdateRowSemaphoreUpdateRowLockHandlerSqlServerMemoryOptimizedUpdateRowSemaphoreSqlServerMemoryOptimizedUpdateRowLockHandler... (truncated)
4.0.0-alpha.2
Quartz.NET 4.0.0-alpha.2 is the second pre-release of 4.0. Alpha.1 was the API becoming complete; this one is
the API being used — by the dashboard's own tests, by trimmed and source-generated builds, by several
schedulers in one host — and reshaped where that use showed a seam. Most of what is below came out of an
audit of the alpha.1 surface and the multi-tenancy story, and a good part of it is fixes for things alpha.1
could not have known were broken.
This is still an alpha. There is no compatibility promise between pre-releases, and this one moves
several names that alpha.1 had. The migration guide
carries every change with before and after; the sections named below are its.
Before you start
QRTZ_PAUSED_JOB_GRPSrecords paused job groups(the ADO store could not answer
IsJobGroupPausedbefore). Re-rundatabase/migrations/4.0/schema_30_to_40_upgrade_<dialect>.sqlfor your database — the scripts are guarded and safe to run again — or run only its
QRTZ_PAUSED_JOB_GRPSstatement. See Database schema changes.ISchedulerListenermember andITriggerListener.TriggerMisfirednow takes the callingISchedulerfirst,and
SchedulerErrortakes aSchedulerErrorContext. Because the members have default bodies, a listenerthat kept the old signature would compile and silently never be called — so the registration now refuses
it with a message naming the member. Listeners are told which scheduler is calling.
IsStarted,InStandbyModeandIsShutdownare gone;IScheduler.Statusis the lifecycle, as oneSchedulerStatus. A scheduler's lifecycle is one value.IServiceProviderinto
scheduler.Context["Quartz.ServiceProvider"]; nothing does now. A plugin or listener that read it backout takes what it needs by constructor instead — it is built by the container. (see #3408)
Highlights
The scheduler's state is one value
IScheduler.Status(Created,Running,Standby,ShuttingDown,Shutdown,Unknown) replaces threebooleans that had to be read in a precedence order only one internal method knew — the reason the HTTP API
and the dashboard once disagreed about what a running scheduler is called.
SchedulerMetadatafollows.Start()on a running scheduler does nothing,Standby()on one that neverstarted does nothing,
Standby()after shutdown throws, and a shutdown no longer announces a standby it didnot enter. A scheduler that is draining reads
ShuttingDownfor as long as it drains, and a teardown thatthrows still ends in
Shutdown.Runningprobes the store,Standbyis Degraded, therest are Unhealthy with a message — and a check registered for a default scheduler in a container that has
only named ones reports Unhealthy instead of throwing out of the pipeline.
Listeners know who is calling
one, and as the first argument where there is not — all 23
ISchedulerListenermembers andTriggerMisfired. A shared listener in a host with several schedulers finally knows which one paused atrigger or raised an error. (closes #3063)
SchedulerErrorcarries aSchedulerErrorContext— message, exception, and the trigger, job and fireinstance the error was for, wherever the scheduler knew them.
... (truncated)
4.0.0-alpha.1
Quartz.NET 4.0 is the first major version since 3.0, and the first that assumes a modern .NET: it targets
net10.0only, it is asynchronous end to end, and the container builds the scheduler instead of a factory reading type names out of a string bag. A public surface that had grown for a decade got a full pass — one word per concept, one shape per operation, and nothing public that was never a contract.This is an alpha. It is complete enough to run and to port an application against, and the API is close to final, but it is a pre-release: names can still move if feedback says they should, and there is no compatibility promise between alpha builds. Do not put it in production yet. Do please try it against a real application and say what breaks or reads badly — that is what this build is for.
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:
Before you start
Four things have to be true before a 3.x application will build and run on 4.0:
net10.0. There is nonetstandard2.0build and no Full Framework...Description has been truncated