Repository navigation
fix(schedules): parse and match cron with cron-utils - #549
Merged
Merged
Conversation
The scheduler matched schedules with a hand-written matcher that knew only *, */n, a,b, a-b and numbers, while POST and PUT saved any string. So "0 9 * * MON", "@daily" and "0-30/5 * * * *" were saved with a 201 and never fired, day-of-week 7 (Sunday) never matched, */2 in the month field picked February, April, ... instead of January, March, ..., and "0 9 1 * 1" fired only on a 1st that is a Monday, not on either. An expression of 65 to 120 characters passed @SiZe(max = 120) and then failed with a 500 at the 64-character column. The matcher is gone: TestScheduler parses with cron-utils' UNIX definition and matches with ExecutionTime.isMatch on the minute, still in UTC. POST and PUT parse the expression with the same parser and refuse one it can't read with a 400 keyed cronExpression; PUT now also applies the field's @notblank and @SiZe, which is 64 to match the column. Macros such as @daily, six-field expressions and Quartz's ? and L are refused rather than saved. A stored expression that no longer parses throws inside the scheduler's per-schedule try, so it is logged and skipped without stopping the other schedules. Disruption schedules had a copy of the same matcher and saved any non-blank string, so they had the same bugs. DisruptionScheduler now matches with TestScheduler's parser, and the disruption schedules' POST and PUT give the same 400, and refuse an expression wider than its 64-character column. cron-utils already came in through quarkus-scheduler; it is declared now that Kates uses it directly, its version left to the Quarkus BOM. The API reference, CLI reference, the --cron help of kates schedule create and kates disruption schedule create, and their doc entries say which cron the Kates API takes, and four MCP source refs into the edited files are re-pointed.
…ron-with-cron-utils # Conflicts: # docs/book/11-api-reference.md
bmscomp
added a commit
that referenced
this pull request
Oct 29, 2026
…s it found (#553) * test(schedules): ScheduleApiIT against real Postgres, and the two bugs it found The schedules API was only tested on H2, where Hibernate generates the table from the entity. ScheduleApiIT runs it against the Flyway schema: round trip, rename, refused request, column widths and the audit trail. Two of its tests failed on main: - The request allowed a 255-char name and a 120-char cron, but V2 makes the columns VARCHAR(128) and VARCHAR(64), so a longer value reached Postgres and came back as a 500. The @SiZe limits now match the columns. - enabled defaulted to true, so a PUT that left it out (a rename) switched a disabled schedule back on. It is now a Boolean: POST still defaults to enabled, PUT keeps the stored flag when it is absent. * fix(schedules): a PUT holds the name to its column width too #549 gave PUT the cronExpression's @notblank and @SiZe; name had the same gap, so a PUT with a name over 128 characters still reached Postgres and came back as a 500. ScheduleApiIT now sends both over-wide values through PUT as well as POST.
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.
Why
TestSchedulerandDisruptionSchedulereach matched cron expressions with the same hand-written matcher. It did not follow cron.0 9 * * MONnever fired, because names were not read.0-30/5was not understood. When day-of-month and day-of-week were both set, it required both to match (AND), where cron fires when either matches (OR). Neither the schedule API nor the disruption schedule API checked the expression before saving it.@daily,0 0 ? * MONor a 6-field expression was stored with a 201 and then never ran, and nothing said so.What changed
TestScheduleris deleted. Schedules are parsed with cron-utilsCronType.UNIXthrough one staticCRONparser.matchesCroncallsExecutionTime.isMatchonnowtruncated to the minute. Matching is still in UTC (ZoneOffset.UTC).DisruptionSchedulerhad a byte-for-byte copy of the same matcher. It drops itsmatchesCronandmatchesFieldand callsTestScheduler.matchesCroninstead, which is now public, so both schedulers share one parser.evaluateSchedules. The scheduler logs it and skips that schedule.ScheduleResourcePOST and PUT parsecronExpressionwith the same parser through oneinvalidCronhelper. If it does not parse, they return a 400validationFailedkeyedcronExpression. The message is cron-utils' message plus a hint about the 5-field format.@Sizeis now 64, matching the column.@Valid, so it callsvalidator.validateProperty(request, "cronExpression"). The PUT and POST 400 bodies are identical.DisruptionScheduleResourcePOST and PUT reuseScheduleResource.invalidCron. They also refuse expressions longer than 64 characters, the width ofcron_expression VARCHAR(64)inV4__create_disruption_schedules.sql. PUT checks only whencronExpressionis in the body.@APIResponse.kates/pom.xmlnow declarescom.cronutils:cron-utilswith no version, because the Quarkus 3.20.6 BOM manages it. It was already on the classpath at 9.2.1, transitively throughquarkus-scheduler-common.MON,JAN-MAR,mon-fri),0-30/5, and7as Sunday. When both day fields are set, it uses OR semantics.*/2in the month field means Jan, Mar, and so on.@daily,@hourlyand@reboot("Nicknames not supported!"), 6-field expressions,?andL.docs/book/11-api-reference.md)--cronrow (docs/book/10-cli-reference.md)--cronhelp incli/cmd/schedule.goandcli/cmd/disruption_watch.goscheduleanddisruption schedule createentries incli/cmd/data/doc_entries.yamlTestScheduler.java:99-105→104-110. This is an anchored ref,Actor.SCHEDULER.ScheduleResource.java:137-138→224-226inmcp_tools_runs_list.go. This ref was already stale on main. It now points at the PUT "can't take the @Valid" comment it describes.DisruptionScheduler.java:107-113→109-115and95-107→97-109inmcp_tools_runs_activity.go.Tests
TestSchedulerCronTest:fullCronExpressionis removed, because it asserted the AND semantics that were the bug.invalidCronReturnsFalseis nowinvalidCronThrows.MON,0-30/5, day7,*/2month, day-of-month OR day-of-week, and seconds within the minute.ScheduleResourceTest:@daily, 4 fields, 6 fields,60,?, and a 65-character expression that does parse, so only its length refuses it.0 9 * * MONis saved with a 201.DisruptionReportPersistenceTest.aCronExpressionTheSchedulerCannotReadIsRefused:0 9 * * MONgets a 201.@daily,0 0 ? * MONand a 65-character valid expression get a 400 with messagecronExpression: ...on both POST and PUT.Run locally:
cd kates && ./mvnw test -Dquarkus.grpc.codegen.skip=true -Dquarkus.http.test-port=0: BUILD SUCCESS. 1760 tests, 0 failures, 0 errors, 3 skipped.cd kates && ./mvnw -o -q spotless:check(JDK 25): pass.cd kates && ./mvnw dependency:tree, compared with the tree for origin/main's pom: the same resolved artifact set.com.cronutils:cron-utils:jar:9.2.1:compileis now a direct dependency and nothing else is new. For Dependency review: no new artifact or version.cd cli && go vet ./... && go test ./...: pass, including theTestMCPCaveat*ref anchors.cd cli && golangci-lint run --new-from-rev=$(git merge-base HEAD origin/main) ./...: 0 issues../scripts/gen-chart-table.sh --check,./scripts/gen-version-matrix.sh --check,./scripts/check-book-style.sh,./scripts/check-book-contrast.py,./scripts/check-mermaid.sh --quiet: all pass.lychee --offline --no-progress --include-fragments README.md "docs/**/*.md": 0 errors.Only CI runs these: integration tests, the native image build and smoke test, the Kind/Docker jobs, the book render, CodeQL and Dependency review.
Deliberately not done