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
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'susleep(), which takes microseconds.Consequently, a configured delay of
500produces 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 withProjectionV2.The configuration in #605 used
[0, 5, 50, 500, 800, 1500, 3000, 5000]withPT120S. 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.1prooph/pdo-event-store:1.16.5The reproduction uses these stable versions, not the Ecotone 2.0 beta.
Reproduction
In a project with these dependencies installed:
The configured value
5therefore means 5 microseconds at execution time, not 5 milliseconds. There is no conversion in Ecotone'sGapDetection::build(), Prooph's getter, or the projector's sleep call.detectionWindow: PT10Sis 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
500000.Source References