Skip to content

Migrate to Quartz 4.2.1 - #467

Merged
Sev7eNup merged 4 commits into
mainfrom
fix/quartz-4-migration
Oct 3, 2026
Merged

Sev7eNup merged 4 commits into
mainfrom
fix/quartz-4-migration

Conversation

@Sev7eNup

@Sev7eNup Sev7eNup commented Oct 3, 2026

Copy link
Copy Markdown
Owner

Summary

Ports the code to Quartz 4.2.1 so the Dependabot bump (#445) can land. Closes #451.

API changes handled:

  • CronExpression is immutable: the zone goes into the constructor (MaintenanceWindowEvaluator, OperationalKnowledgeReader, ScheduleTriggerSource, ScheduleMissedSource).
  • GetTimeAfter → GetNextValidTimeAfter; IsValidExpression → TryParse.
  • IJob.Execute takes a CancellationToken and returns ValueTask; ScheduleJob takes ScheduleJobOptions (default Replace = false, as before); WithMisfireHandlingInstructionDoNothing() → WithMisfireInstruction(CronTriggerMisfireInstruction.DoNothing).
  • GetExpressionSummary is gone without replacement. The summary field is removed from GET /api/triggers/schedule/next-fires, the CLI and MCP mirrors, np cron next (prints the expression instead) and the AI chat forecast. It was Quartz's internal field dump, not a readable description.

Behaviour changes

Measured by running Quartz 3.22 and 4.2.1 side by side over two years of fire times in UTC, Berlin, Los Angeles and Sydney. Only these differ:

  • Spring forward: a fire time that does not exist (02:30 on the switch night) fires at the end of the gap, 03:00, instead of 03:30.
  • Fall back: hourly and more frequent schedules keep firing through the repeated hour. Quartz 3 skipped it (no fire between 02:00 and 02:45 CET). Daily schedules still fire once.
  • Parsing: 0 0 2 * * * (both day fields *) is now accepted and means daily at 02:00. A zero step (0/0) and years after 2199 are now rejected at publish.

Unchanged and pinned by a test: a cron maintenance window evaluated during the repeated hour opens a second time for a daily expression. Both Quartz versions do this.

Test plan

  • Full backend suite (dotnet test NodePilot.slnx) on Quartz 4; the only failures were a CLI assertion fixed here and AuditStagerTests, which passes in isolation and is unrelated
  • MaintenanceWindowEvaluatorTests: 3 new DST cases (spring gap, fall-back sub-hourly, fall-back daily)
  • ScheduleCronValidationTests updated for the new acceptance rules
  • ApiDtoParityTests, EndpointClientCoverageTests
  • Release lab: re-run the schedule cases after merge

🤖 Generated with Claude Code

Quartz 4 made CronExpression immutable and dropped GetTimeAfter,
GetExpressionSummary and IsValidExpression; IJob.Execute and
IScheduler.ScheduleJob gained parameters, and misfire handling moved to
an enum.

Behaviour changes, measured against Quartz 3.22 over two years of fire
times in four time zones:
- A fire time that does not exist on the spring-forward night now fires
  at the end of the gap (03:00) instead of an hour later (03:30).
- Hourly and sub-hourly schedules keep firing through the repeated hour
  on the fall-back night; Quartz 3 skipped that hour.
- Cron expressions with both day fields '*' are accepted; a zero step
  and years after 2199 are rejected.

The cron summary is gone from the next-fires API, the CLI, the MCP tool
and the AI chat forecast: Quartz 4 has no replacement and it was Quartz's
internal field dump rather than a readable description.

Closes #451.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Sev7eNup added a commit that referenced this pull request Oct 3, 2026
SlowReader_AfterProcessExit_DrainsBufferedOutputWithinTheTotalBudget set
a 1 s idle grace for both cases. In the case that must capture all output,
a single read that stalled for a second on a loaded runner ended the drain
early (seen on #467). That case now uses the 5 s production default; the
bounded case keeps 1 s so it still reaches the total limit.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Sev7eNup added a commit that referenced this pull request Oct 3, 2026
#469)

* Stop the redactor from letting secrets through when a pass times out

Every built-in redaction pattern carried a 500 ms match timeout, and a
pass that timed out was skipped, dropping the redactions it had already
made. The timeout also fires on a thread that merely got no CPU: on a
busy host the first password match took 1.6-2.4 s and passed through
unredacted, which is what made AuditStagerTests.Build_AppliesRedactor_
ToDetails fail in parallel runs. Independently of load, a closed private
key followed by many unclosed BEGIN markers made the private-key pattern
quadratic and leaked the key.

The fourteen built-in regexes are linear (no nested quantifiers) and now
run without a timeout on the same engine. The two PEM shapes, whose lazy
body is retried from every BEGIN marker, are paired by hand with the same
semantics in linear time. Operator patterns keep the timeout.

Checked against the previous implementation: identical output on 300,000
generated inputs (208,287 redacted) and on the named-value path; 1 MB
adversarial inputs finish in milliseconds instead of timing out.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Give the cluster recovery tests a realistic batch budget

ClusterFailoverRecoveryHostTests.DeferredSingleExecution_* configured a
1 s lease timeout, which bounds every recovery batch to 0.67 s, including
the retry that has to commit. On a loaded, coverage-instrumented CI
runner the retry missed that budget and the test timed out (seen on
#466). The first attempt now fails with a command timeout, which takes
the same deferral branch as an expired budget, and the retry runs under
the default 2 s budget. The test also asserts that the deferral path was
taken, since the host retries after any error.

StartupRecoveryBatchTests.BudgetExpiry_* had the same shape with 200 ms
for the batches after the deliberate stall; it now uses 2 s. The budget
path itself stays covered by SingleExecutionExceedsBudget_*.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Use the default drain grace where the StartProgram drain must complete

SlowReader_AfterProcessExit_DrainsBufferedOutputWithinTheTotalBudget set
a 1 s idle grace for both cases. In the case that must capture all output,
a single read that stalled for a second on a loaded runner ended the drain
early (seen on #467). That case now uses the 5 s production default; the
bounded case keeps 1 s so it still reaches the total limit.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
@Sev7eNup
Sev7eNup merged commit c057c32 into main Oct 3, 2026
17 checks passed
@Sev7eNup
Sev7eNup deleted the fix/quartz-4-migration branch October 3, 2026 14:28
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

PR-Review #445: the Quartz 4.2.1 major breaks NodePilot.Data — CronExpression lost TimeZone, GetTimeAfter and GetExpressionSummary

1 participant