Skip to content

Commit 849ac52

Browse files
committed
fix(events): keep earliest time on out-of-order buffer merges
Bring the implementation in line with the docblock/PR description promise: when two collect() calls fold into the same buffer key with different queued times, keep the earlier one. Previously the first non-null time won regardless of value, so an out-of-order redelivery could stamp the merged row with a later timestamp than the true earliest one.
1 parent 58d33c0 commit 849ac52

2 files changed

Lines changed: 22 additions & 2 deletions

File tree

src/Usage/Accumulator.php

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -95,9 +95,10 @@ public function collect(string $tenant, string $metric, int $value, string $type
9595
if ($type === Usage::TYPE_EVENT && isset($this->buffer[$key])) {
9696
// Additive: sum values for the same tenant + metric + tags combination.
9797
// Preserve the earliest queued time — later calls fold in without
98-
// moving the bucket's timestamp forward.
98+
// moving the bucket's timestamp forward, even when they arrive
99+
// out of order (e.g. an earlier event redelivered after a later one).
99100
$this->buffer[$key]['value'] += $value;
100-
if ($time !== null && !isset($this->buffer[$key]['time'])) {
101+
if ($time !== null && (!isset($this->buffer[$key]['time']) || $time < $this->buffer[$key]['time'])) {
101102
$this->buffer[$key]['time'] = $time;
102103
}
103104
} else {

tests/Usage/AccumulatorTest.php

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -329,6 +329,25 @@ public function testEventCollectPreservesEarliestQueuedTime(): void
329329
$this->assertSame($earlier, $entry['time']);
330330
}
331331

332+
public function testEventCollectKeepsEarliestTimeOnOutOfOrderMerge(): void
333+
{
334+
// Out-of-order redelivery: the later time arrives FIRST, the earlier
335+
// time second. The merged bucket must carry the earlier time so
336+
// buckets never slide forward when late-arriving events fold in.
337+
$later = new \DateTime('2026-04-15 12:05:00');
338+
$earlier = new \DateTime('2026-04-15 12:00:00');
339+
340+
$this->accumulator->collect('t1', 'requests', 10, Usage::TYPE_EVENT, [], $later);
341+
$this->accumulator->collect('t1', 'requests', 5, Usage::TYPE_EVENT, [], $earlier);
342+
343+
$this->assertEquals(1, $this->accumulator->count());
344+
$this->assertTrue($this->accumulator->flush());
345+
346+
$entry = $this->adapter->batches[0]['metrics'][0];
347+
$this->assertEquals(15, $entry['value']);
348+
$this->assertSame($earlier, $entry['time']);
349+
}
350+
332351
public function testGaugeCollectUsesLastWriteWinsForTime(): void
333352
{
334353
// Gauge collect() is last-write-wins on value; the queued time

0 commit comments

Comments
 (0)