Skip to content

Legacy GapDetection defaults produce microsecond-scale retries and override Prooph's proposed fix #701

Description

@lifinsky

Summary

This follows up on discussion #605, where I reported a polling account-statement projection occasionally missing 1-2 rows despite low throughput and a single consumer pod.

Ecotone's legacy Prooph-backed projections explicitly configure gap retry delays as [0, 5, 50, 500, 800]. These values are passed unchanged to Prooph and then directly to PHP's usleep(), which takes microseconds.

Consequently, a configured delay of 500 produces a requested sleep of 500 microseconds (0.5 ms), not 500 ms. The default sequence requests only 1.355 ms of sleep in total, excluding database queries and other processing, rather than the 1.355 seconds suggested by Prooph's millisecond documentation.

This affects legacy #[Projection] configurations, including the default synchronous mode. This report does not establish an issue with ProjectionV2.

The configuration in #605 used [0, 5, 50, 500, 800, 1500, 3000, 5000] with PT120S. Those retries request only 10.855 ms of total sleep, not 10.855 seconds. The 120-second detection window does not extend that retry budget. This is a concrete timing defect that can explain the reported missing-row scenario, although the historic missed transactions have not been reproduced under a controlled trace.

Versions

Latest stable releases verified against Packagist on September 11, 2026:

  • ecotone/pdo-event-sourcing: 1.326.1
  • prooph/pdo-event-store: 1.16.5

The reproduction uses these stable versions, not the Ecotone 2.0 beta.

Reproduction

In a project with these dependencies installed:

use Ecotone\EventSourcing\ProjectionRunningConfiguration;

$configuration = ProjectionRunningConfiguration::createEventDriven('example');
$gap = $configuration
    ->getOption(ProjectionRunningConfiguration::OPTION_GAP_DETECTION)
    ->build();

$gap->trackRetry(); // Select the second configured retry.
var_dump($gap->getSleepForNextRetry()); // int(5), passed directly to usleep().

The configured value 5 therefore means 5 microseconds at execution time, not 5 milliseconds. There is no conversion in Ecotone's GapDetection::build(), Prooph's getter, or the projector's sleep call.

detectionWindow: PT10S is separate: it checks the age of the visible event after a gap. It is not a ten-second retry budget and does not compensate for the short sleeps.

Expected Behavior

Gap detection should have explicitly documented units and meaningful default retry delays. If Ecotone exposes milliseconds, it must convert them exactly once before passing them to Prooph. If it exposes microseconds, both documentation and default values must reflect that contract.

Impact

The much shorter retry delays can weaken protection against event visibility gaps during concurrent transactions. Once Prooph exhausts retries, it can advance past a missing event; the legacy detector does not subsequently revisit that skipped position.

This timing mismatch should not be confused with an invisible final event: when no later event is visible, the legacy detector has no gap to detect.

Upstream Status

The mismatch is already reported in Prooph #243. Prooph #263 proposes correcting documentation and Prooph's default delays while leaving custom values unchanged.

Waiting for that Prooph fix alone would not correct Ecotone's defaults: Ecotone passes its own explicit retry array, so Prooph treats those numbers as custom configuration rather than using its corrected defaults.

Test Coverage

The current synchronous gap tests use [0]. The polling tests also exercise [10, 20, 50], but assert read-model results rather than retry duration or converted sleep values. The missing event is restored before a projection reset, not concurrently during the retry wait, so these tests do not catch the units mismatch.

Requested Fix

  • Correct Ecotone's default retry delays and clarify the units accepted by its configuration API, independently of the upstream fix if necessary.
  • Publish an immediate warning that, on the affected unpatched versions, retry values are effectively interpreted as microseconds. For example, an intended 500 ms delay requires 500000.
  • Add a regression test for the values passed through Ecotone to the actual Prooph sleep path.
  • Provide upgrade guidance for applications that already use microseconds or apply a milliseconds-conversion patch, avoiding double conversion and unexpectedly long waits.

Source References

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions