Skip to content

Answer a bounded part of a cron sequence too - #42

Open
KalimeroMK wants to merge 4 commits into
feat/trigger-rangesfrom
feat/cron-trigger-ranges
Open

KalimeroMK wants to merge 4 commits into
feat/trigger-rangesfrom
feat/cron-trigger-ranges

Conversation

@KalimeroMK

@KalimeroMK KalimeroMK commented Nov 2, 2026 •

Copy link
Copy Markdown
Contributor
Q A
Is bugfix? ❌
New feature? ✔️
Breaks BC? ❌
Fixed issues closes #36
Tests added? ✔️

Based on #41, which adds the interface this implements.

What does this PR do?

CronExpressionTrigger answers the two range questions as well. The library walks the expression itself, so a batch costs its own length rather than the span it is taken from: a month of backlog opens as cheaply as an hour of it.

Asked for the first run at or after a date, it has to be asked twice. An expression names a minute, and the library answers with the start of that minute when told the current date counts, so half a minute past a run it answers with the run just missed, which is behind the date asked about. The guard against that is taken from #12, where the same question is asked the same way.

The timezone the expression is read in was worked out inside getNextRunDate() and is now shared by all three methods, because a range has to be read in the same one: not the timezone of the date asked about, which for a date restored from a checkpoint is UTC whatever it was saved in.

What it is held to

The same property as #41: a range holds exactly the dates the trigger gives one at a time. The provider covers every hour, 0 2 * * * in Skopje across the spring change — the day 02:00 does not exist and the library answers 03:00 — and 29 February over nine years.

The clock-change sweep that caught the defect in #41's first commit is run against this trigger too, over three zones that move the clock by half an hour and by a whole one, four days on which they move it, every hour of each: no answer is ever before the date asked about.

What is left for #34

The generator's use of a range. By this ticket's own Related work, #34 owns the checkpoint and replay decisions, and TaskGenerator::getTasks() is rewritten by #17 and #19 besides.

A trigger implementing TriggerInterface alone is asked one date at a time, as before. That was the behaviour already; it is now written down, in the interface and in the README.

Summary by Sourcery

Add efficient bounded cron-sequence queries while preserving the existing one-run trigger behavior and sequence semantics.

New Features:

  • Enable CronExpressionTrigger to return bounded batches of cron runs and the first run at or after a date, including support for continuation phases.

Bug Fixes:

  • Ensure cron range queries never return a run before the requested date, including during timezone transitions and when the requested time falls within a scheduled minute.

Enhancements:

  • Use the expression timezone consistently across individual and range-based trigger queries.
  • Document range-based trigger queries and clarify that triggers without range support continue to be queried one run at a time.

Documentation:

  • Document bounded trigger queries, continuation phases, range semantics, and custom-trigger behavior.

Tests:

  • Add coverage for cron range equivalence, batching, limits, phases, timezone handling, leap days, and daylight-saving transitions.

Chores:

  • Record the cron range-query capability in the changelog.

`CronExpressionTrigger` now implements `TriggerRangeInterface`. The library walks the expression
itself, so a batch costs its own length rather than the span it is taken from: a month of
backlog opens as cheaply as an hour of it.

Asked for the first run at or after a date, it has to be asked twice. An expression names a
minute, and the library answers with the start of that minute when told the current date
counts, so half a minute past a run it answers with the run just missed, which is behind the
date asked about. The guard against that is taken from #12, where the same question is asked
the same way.

The timezone the expression is read in was worked out in `getNextRunDate()` and is now shared,
because a range has to be read in the same one: not the timezone of the date asked about, which
for a date restored from a checkpoint is UTC whatever it was saved in.
A trigger that implements TriggerInterface alone is asked one date at a time, whatever the size
of the backlog. That was the behaviour before a range existed and it is unchanged, but it was
nowhere written down, and a reader of the new interface has no way to tell whether implementing
it is expected of them.
The periodic trigger answered a date half an hour into a run with the run it had just missed,
at every change of the clock. The cron trigger is asked the same question a different way, so
the same sweep is run against it: three zones that move the clock by half an hour and by a
whole one, four days on which they move it, every hour of each.
@sourcery-ai

sourcery-ai Bot commented Nov 2, 2026 •

Copy link
Copy Markdown

Reviewer's Guide

This PR adds efficient bounded-sequence support to CronExpressionTrigger by implementing range queries directly through the cron library, while preserving the trigger’s existing sequence, phase, and expression-timezone semantics. Documentation, changelog entries, and extensive edge-case tests establish the API contract and validate DST, leap-year, batching, and boundary behavior.

Sequence diagram for cron bounded batch retrieval

sequenceDiagram
    participant Caller
    participant CronExpressionTrigger
    participant CronLibrary

    Caller->>CronExpressionTrigger: getRunDates(after, before, limit, phase)
    CronExpressionTrigger->>CronLibrary: getMultipleRunDates(limit, cursor, false, false, timezone)
    loop Up to limit
        CronLibrary-->>CronExpressionTrigger: next cron date
        alt date >= before
            CronExpressionTrigger-->>Caller: bounded dates
        else date < before
            CronExpressionTrigger->>CronExpressionTrigger: append date
        end
    end
    CronExpressionTrigger-->>Caller: bounded dates
Loading

Sequence diagram for cron next-run boundary handling

sequenceDiagram
    participant Caller
    participant CronExpressionTrigger
    participant CronLibrary

    Caller->>CronExpressionTrigger: getNextRunDateAtOrAfter(date, phase)
    alt phase >= date
        CronExpressionTrigger->>CronExpressionTrigger: getNextRunDate(phase)
        CronExpressionTrigger-->>Caller: next run after phase
    else no usable phase
        CronExpressionTrigger->>CronLibrary: getNextRunDate(date, 0, true, timezone)
        CronLibrary-->>CronExpressionTrigger: candidate minute
        alt candidate < date
            CronExpressionTrigger->>CronExpressionTrigger: getNextRunDate(date)
            CronExpressionTrigger-->>Caller: next run after date
        else candidate >= date
            CronExpressionTrigger-->>Caller: candidate
        end
    end
Loading

File-Level Changes

Change Details Files
Extends cron triggers with efficient bounded-sequence queries while preserving the existing one-at-a-time sequence and expression timezone semantics.
  • Implements TriggerRangeInterface on CronExpressionTrigger.
  • Adds at-or-after lookup with phase handling and protection against returning a run already missed within a cron minute.
  • Adds bounded batch enumeration using the cron library’s multiple-date API, exclusive bounds, limits, and phase continuation.
  • Centralizes expression timezone resolution for all trigger queries.
src/Trigger/CronExpressionTrigger.php
Documents the range-query contract and the continued behavior for custom triggers that only implement the base interface.
  • Documents batch queries, exclusive bounds, phases, ordering, and performance characteristics.
  • Clarifies that TriggerInterface implementations continue to be queried one date at a time.
  • Records the new cron range feature in the changelog.
src/Trigger/TriggerInterface.php
README.md
CHANGELOG.md
Adds comprehensive coverage for range equivalence, batching, phase semantics, cron-minute boundaries, timezones, DST transitions, leap days, and invalid limits.
  • Verifies range results match repeated getNextRunDate() results and do not duplicate or skip dates across batches.
  • Covers DST gaps and clock changes across multiple timezone rules, plus leap-day schedules.
  • Tests exclusive range bounds, maximum batch size, reversed ranges, timezone independence from input dates, and invalid limits.
tests/Trigger/CronExpressionTriggerTest.php

Possibly linked issues


Tips and commands

Interacting with Sourcery

  • Trigger a new review: Comment @sourcery-ai review on the pull request.
  • Continue discussions: Reply directly to Sourcery's review comments.
  • Generate a GitHub issue from a review comment: Ask Sourcery to create an
    issue from a review comment by replying to it. You can also reply to a
    review comment with @sourcery-ai issue to create an issue from it.
  • Generate a pull request title: Write @sourcery-ai anywhere in the pull
    request title to generate a title at any time. You can also comment
    @sourcery-ai title on the pull request to (re-)generate the title at any time.
  • Generate a pull request summary: Write @sourcery-ai summary anywhere in
    the pull request body to generate a PR summary at any time exactly where you
    want it. You can also comment @sourcery-ai summary on the pull request to
    (re-)generate the summary at any time.
  • Generate reviewer's guide: Comment @sourcery-ai guide on the pull
    request to (re-)generate the reviewer's guide at any time.
  • Resolve all Sourcery comments: Comment @sourcery-ai resolve on the
    pull request to resolve all Sourcery comments. Useful if you've already
    addressed all the comments and don't want to see them anymore.
  • Dismiss all Sourcery reviews: Comment @sourcery-ai dismiss on the pull
    request to dismiss all existing Sourcery reviews. Especially useful if you
    want to start fresh with a new review - don't forget to comment
    @sourcery-ai review to trigger a new review!

Customizing Your Experience

Access your dashboard to:

  • Enable or disable review features such as the Sourcery-generated pull request
    summary, the reviewer's guide, and others.
  • Change the review language.
  • Add, remove or edit custom review instructions.
  • Adjust other review settings.

Getting Help

@sourcery-ai sourcery-ai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hey - I've found 1 issue

Prompt for AI Agents
Please address the comments from this code review:

## Individual Comments

### Comment 1
<location path="src/Trigger/CronExpressionTrigger.php" line_range="100" />
<code_context>
+
+        // The library walks the expression itself, so the cost is the batch and not the span it
+        // is taken from: a backlog of a month is as cheap to open as a backlog of an hour.
+        foreach ($this->cron->getMultipleRunDates($limit, $cursor, false, false, $this->timezone()) as $date) {
+            $date = DateTimeImmutable::createFromInterface($date);
+
</code_context>
<issue_to_address>
**Cron batches contain incorrect dates**

When a caller requests a cron range with a non-default expression timezone or a matching run at the start, `getMultipleRunDates()` receives the timezone in the wrong positional slot, so the library does not evaluate the range with the requested timezone; batches contain shifted, repeated, or otherwise unintended dates.

Pass the timezone in the library’s timezone argument position.
</issue_to_address>

Sourcery assessment

Approval pending. 1 finding to address first.

Blocking findings: src/Trigger/CronExpressionTrigger.php:100


Sourcery is free for open source - if you like our reviews please consider sharing them ✨

Comment thread src/Trigger/CronExpressionTrigger.php Resolved
@KalimeroMK
KalimeroMK added this pull request to stack #43 November 2, 2026 11:00
@KalimeroMK
KalimeroMK requested a review from a team November 2, 2026 11:01
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.

1 participant