Skip to content

fix(job): run a cron step on a single value to the end of the field - #4526

Merged
bpamiri merged 1 commit into
developfrom
peter/job-cron-steps
Oct 9, 2026
Merged

bpamiri merged 1 commit into
developfrom
peter/job-cron-steps

Conversation

@bpamiri

@bpamiri bpamiri commented Oct 9, 2026

Copy link
Copy Markdown
Collaborator

Problem

A cron step on a single value didn't follow Vixie cron or cronie, the de facto standard:

Expression Wheels before Vixie cron / cronie
5/1 (minute) minute 5 only 5-59
7/2 (day-of-week) refused as a reversed range Sunday
1/2 (day-of-week) Monday, Wednesday, Friday Monday, Wednesday, Friday, Sunday (the field runs 0-7)

Schedules are new in 4.2, and changing what an expression means after release would silently move users' jobs, so this is fixed before the cut.

Change

JobCron.$range(): when a single value has a step (N/S), the range now runs from N to the field's maximum, for any step including 1. Day-of-week's maximum is 7, which $weekdayField() already maps to Sunday.

Unchanged:

  • a bare value (5);
  • a range with a step (5-20/5);
  • */S;
  • 5/15, which was already 5,20,35,50.

The multi-server jobs guide's .cron() line describes the rule with examples, and a changelog fragment (changed) is included.

Tests

JobSchedulerSpec has two new specs.

"runs N/S from N to the end of the field":

  • minute 5/15 → 5,20,35,50;
  • minute 5/1 → 5-59;
  • hour 22/1 → 22,23;
  • day 30/1 → 30,31;
  • month 11/1 → 11,12;
  • the unchanged forms stay unchanged;
  • the next slot after 10:21 for 5/15 is 10:35.

"runs a day-of-week N/S through 7":

  • 7/2 → Sunday;
  • 1/2 and MON/2 → Sunday, Monday, Wednesday, Friday;
  • */2 → 0,2,4,6, unchanged;
  • 7/2 next fires on the following Sunday.

Against the old JobCron: the first spec fails (5/1 gave only 5), and the second errors ("'7/2' is a reversed range").

Local results:

  • Jobs suite: 290/0/0 on Lucee 7, BoxLang and Adobe 2025, all on SQLite.
  • RustCFML: tools/rustcfml/run-suite.sh with TZ=UTC exits 0.
  • The complexity and docblock gates pass.
  • The guides site builds, and verify-docs on the page passes 13/13.

🤖 Generated with Claude Code

A step on a single value now follows Vixie cron and cronie: N/S runs from N to the field's
maximum, every S. "5/1" in the minute field was minute 5 alone and is now 5-59 ("5/15" was
already 5,20,35,50). Day-of-week runs through 7, which is Sunday again: "7/2" is accepted
(it was refused as a reversed range) and means Sunday, and "1/2" now includes Sunday.
Ranges with a step ("5-20/5") and "*/S" are unchanged.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Peter Amiri <peter@alurium.com>
@bpamiri bpamiri added this to the v4.2.0 milestone Oct 9, 2026
@github-actions

github-actions Bot commented Oct 9, 2026

Copy link
Copy Markdown
Contributor

Wheels Test Results

37 180 tests   - 222 870   36 433 ✅  - 217 880   11m 18s ⏱️ - 1h 48m 58s
 2 985 suites  -  17 910      747 💤  -   4 990 
     5 files    -      30        0 ❌ ±      0 

Results for commit 0777715. ± Comparison against base commit 056404d.

This pull request removes 222880 and adds 10 tests. Note that renamed tests count towards both.
adobe2023/cockroachdb :: wheels.tests.specs.Authorization.AuthorizationSpec :: Authorization policy layer (wheels.Policy + authorize()/can()/policyScope()) > authorize() ‑ S1: authorize() throws Wheels.Policy.UnknownAction for a missing policy method
adobe2023/cockroachdb :: wheels.tests.specs.Authorization.AuthorizationSpec :: Authorization policy layer (wheels.Policy + authorize()/can()/policyScope()) > authorize() ‑ S4: authorize() denies the CFML string true
adobe2023/cockroachdb :: wheels.tests.specs.Authorization.AuthorizationSpec :: Authorization policy layer (wheels.Policy + authorize()/can()/policyScope()) > authorize() ‑ S4: authorize() denies the CFML string yes
adobe2023/cockroachdb :: wheels.tests.specs.Authorization.AuthorizationSpec :: Authorization policy layer (wheels.Policy + authorize()/can()/policyScope()) > authorize() ‑ S4: authorize() returns the record when the policy returns boolean true
adobe2023/cockroachdb :: wheels.tests.specs.Authorization.AuthorizationSpec :: Authorization policy layer (wheels.Policy + authorize()/can()/policyScope()) > authorize() ‑ S8: authorize() denial in production is HTTP 403, not a silent allow
adobe2023/cockroachdb :: wheels.tests.specs.Authorization.AuthorizationSpec :: Authorization policy layer (wheels.Policy + authorize()/can()/policyScope()) > authorize() ‑ S9: authorize() with action=init throws Wheels.NotAuthorized and does not Invoke init
adobe2023/cockroachdb :: wheels.tests.specs.Authorization.AuthorizationSpec :: Authorization policy layer (wheels.Policy + authorize()/can()/policyScope()) > authorize() ‑ S9: authorize() with action=scope throws Wheels.NotAuthorized and does not Invoke scope
adobe2023/cockroachdb :: wheels.tests.specs.Authorization.AuthorizationSpec :: Authorization policy layer (wheels.Policy + authorize()/can()/policyScope()) > authorize() ‑ defaults the action from params.action at call time
adobe2023/cockroachdb :: wheels.tests.specs.Authorization.AuthorizationSpec :: Authorization policy layer (wheels.Policy + authorize()/can()/policyScope()) > authorize() ‑ denies a guest (no user)
adobe2023/cockroachdb :: wheels.tests.specs.Authorization.AuthorizationSpec :: Authorization policy layer (wheels.Policy + authorize()/can()/policyScope()) > authorize() ‑ denies the boolean false a missed finder returns
…
adobe2023/sqlite :: wheels.tests.specs.jobs.JobSchedulerSpec :: JobCron ‑ runs N/S from N to the end of the field, as Vixie cron and cronie do
adobe2023/sqlite :: wheels.tests.specs.jobs.JobSchedulerSpec :: JobCron ‑ runs a day-of-week N/S through 7, which is Sunday again
adobe2025/sqlite :: wheels.tests.specs.jobs.JobSchedulerSpec :: JobCron ‑ runs N/S from N to the end of the field, as Vixie cron and cronie do
adobe2025/sqlite :: wheels.tests.specs.jobs.JobSchedulerSpec :: JobCron ‑ runs a day-of-week N/S through 7, which is Sunday again
boxlang/sqlite :: wheels.tests.specs.jobs.JobSchedulerSpec :: JobCron ‑ runs N/S from N to the end of the field, as Vixie cron and cronie do
boxlang/sqlite :: wheels.tests.specs.jobs.JobSchedulerSpec :: JobCron ‑ runs a day-of-week N/S through 7, which is Sunday again
lucee6/sqlite :: wheels.tests.specs.jobs.JobSchedulerSpec :: JobCron ‑ runs N/S from N to the end of the field, as Vixie cron and cronie do
lucee6/sqlite :: wheels.tests.specs.jobs.JobSchedulerSpec :: JobCron ‑ runs a day-of-week N/S through 7, which is Sunday again
lucee7/sqlite :: wheels.tests.specs.jobs.JobSchedulerSpec :: JobCron ‑ runs N/S from N to the end of the field, as Vixie cron and cronie do
lucee7/sqlite :: wheels.tests.specs.jobs.JobSchedulerSpec :: JobCron ‑ runs a day-of-week N/S through 7, which is Sunday again

@bpamiri

bpamiri commented Oct 9, 2026

Copy link
Copy Markdown
Collaborator Author

Review: head 0777715. APPROVE (ready). This also closes the INFO I left on #4493 (5/1 meant only 5, and day-of-week 7/2 was refused).

Semantics against cronie, per field

cronie's get_range() reads a single number followed by / as num1 to the field's maximum, and $range() now does the same (last = arguments.high whenever a single value is stepped):

Field Range Example
minute 0–59 5/15 → 5,20,35,50; 5/1 → 5–59
hour 0–23 22/1 → 22,23
day-of-month 1–31 30/1 → 30,31
month 1–12 11/1 → 11,12
day-of-week 0–7 7/2 → 7 → Sunday; 1/2 → 1,3,5,7 → Mon/Wed/Fri/Sun

On day-of-week, $weekdayField already folds 7 into Sunday ((v MOD 7) + 1), which matches cronie's dow[0] |= dow[7]. Names resolve before the step (MON/2 = 1/2; JAN/3 → 1,4,7,10).

Nothing else changed

  • * and */S take the earlier text == "*" branch (day-of-week 0–6). cronie's star is 0–7, but for every step S ≥ 1 the two yield the same days once 7 is folded into 0: */2 → 0,2,4,6, and */7 → Sunday.
  • Bare N, N-M and N-M/S are untouched, as is the reversed-range check for N-M.
  • Step validation (≥ 1, digits only), the strict part-shape check, and the DOM/DOW rule (AND when either field is starred, OR otherwise) are unchanged.

RED is genuine

Against the old code:

  • 5/1 stopped at 5, because of the old step > 1 guard;
  • 1/2 stopped at 6, so Sunday was missing;
  • 7/2 set last to 6, below first = 7, so it was refused as a reversed range.

The new specs assert all three. They also pin the unchanged forms (5, 5-20/5, */15, */2) and two nextCron results (5/15 from 10:21 → 10:35; 7/2 from Monday → Sunday the 18th).

Docs

The guide line (5/15 = 5,20,35,50, 5/1 = 5-59, day-of-week 0–7 both Sunday, 1/2 = Mon/Wed/Fri/Sun) and the changed changelog fragment both match the code.

Evidence

  • Matrix 37906983478 (all engines × sqlite):
    • 5/5 legs green, 0 fail / 0 error, 597 bundles;
    • JobSchedulerSpec 25 pass + 1 expected skip on every leg;
    • RustCFML 7258/0/0.
  • PR CI: 27 pass, 0 fail, 3 skipped (path-filtered Deploy CLI, reviewer, docs).

@bpamiri
bpamiri merged commit c3f96d7 into develop Oct 9, 2026
41 checks passed
@bpamiri
bpamiri deleted the peter/job-cron-steps branch October 9, 2026 09:05
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.

1 participant