Skip to content

fix: wait legacy projection gap retries in milliseconds, not microseconds (fixes #701) - #704

Merged
dgafka merged 1 commit into
mainfrom
dgafka/gap-detection
Sep 27, 2026
Merged

dgafka merged 1 commit into
mainfrom
dgafka/gap-detection

Conversation

@dgafka

@dgafka dgafka commented Sep 27, 2026

Copy link
Copy Markdown
Member

Why is this change proposed?

Why

Warning

Applications that worked around this bug by configuring microseconds (e.g. 500000) must switch back to milliseconds (500). Otherwise each retry now waits 1000× longer.

Legacy #[Projection] gap detection now waits the configured retry delays in milliseconds.

Before this change, Ecotone passed the delays unchanged to Prooph, which hands them to usleep() in microseconds. The default [0, 5, 50, 500, 800] waited about 1.4 ms instead of about 1.4 s. Events from slower concurrent transactions were therefore often skipped for good, which explains rows missing from read models (#701, discussion #605). Prooph resolved its side in prooph/pdo-event-store#263 by keeping microseconds as its contract and only rescaling its own defaults. Ecotone always passes its own delays and never uses Prooph's defaults, so Ecotone has to convert them itself.

Before / after

Configured delays Before After
Default [0, 5, 50, 500, 800] ~1.4 ms total wait ~1.4 s total wait
[0, 300] against a real gap command finished in ~20 ms command waits ≥ 300 ms
null (Prooph defaults) Prooph defaults unchanged

Out of scope

  • Rejecting implausibly large delays, which would catch applications still configured in microseconds.
  • Letting withOption() accept custom GapDetector implementations (today only withOptions() does).
  • ProjectionV2, which fills gaps later instead of sleeping and is not affected.

Example

ProjectionRunningConfiguration::createPolling('account_statement')
    ->withOption(
        ProjectionRunningConfiguration::OPTION_GAP_DETECTION,
        new GapDetection([0, 5, 50, 500, 800], new DateInterval('PT10S')), // milliseconds
    );

Description of Changes

  • GapDetection::build() converts retry delays from milliseconds to microseconds once, before handing them to Prooph. The stored configuration stays in milliseconds, so compiled containers are unaffected.
  • A new test in GapDetectionInSynchronousProjectionTest runs a synchronous projection over a stream with a real gap and asserts that the retry delay is actually waited. It failed before the fix (~20 ms elapsed).
  • Behaviour change: in synchronous event-driven projections, a gap that never fills (e.g. a rolled-back transaction) now adds up to ~1.4 s to the first command that hits it, within the detection window.

Verification: the full PdoEventSourcing suite passes (263 tests, 1 skipped); PHPStan and php-cs-fixer report nothing.

Pull Request Contribution Terms

  • I have read and agree to the contribution terms outlined in CONTRIBUTING.

@dgafka
dgafka merged commit 343824b into main Sep 27, 2026
9 checks passed
@dgafka
dgafka deleted the dgafka/gap-detection branch September 27, 2026 18:00
@dgafka

dgafka commented Sep 27, 2026

Copy link
Copy Markdown
Member Author

cc: @lifinsky

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