## Problem and proof
A late appender can increment its stripe during collection A, then read
`activeGeneration` only after A has completed and collection B has
started.
B already includes that observation in `expectedCount`. If the appender
puts it into B's
buffer, B waits for an observation that will only be replayed after B's
wait ends.
This deterministically causes the five-second collection timeout.
The old late-appender test paused after reading the generation, missing
this window.
Moving the existing injected pause hook before that read, without the
fix, reproduced:
```text
BufferTest.lateAppenderCountedByNextGenerationMustNotBeBufferedAgain
Time elapsed: 5.037 s
ExecutionException: IllegalStateException:
Timed out while waiting for in-flight observations.
```
The production hook remains a no-op; the test forces a scheduling window
that real threads can hit.
This has the same failure signature as the histogram failures observed
in
[#2468](#2468) and
[#2441](#2441).
The reproduction proves this race exists, not which interleaving
occurred in any particular CI run.
## Fix
- Record each stripe's absolute observation count at generation
activation, under `appendLock`.
- Under that same lock, reject buffering an observation whose ticket is
already included in the
current generation's starting count. It completes through the existing
direct observation path.
- Reuse a single cutoff array per Buffer: one additional long per
stripe, plus array overhead,
with no new per-collection allocation or additional fast-path lock.
- Keep cutoffs independent of the reset-adjusted aggregate observation
count.
- Do not change the five-second timeout, buffer capacity, or public API.
## Regression coverage
Strengthen the late-appender test to force the previously untested
window and capture worker
exceptions through futures, with bounded waits and executor cleanup.
Cover both normal collection and a preceding reset. Each case also
verifies that a genuinely new
observation buffers normally, stays out of the current snapshot, replays
exactly once, and appears
in the following collection.
## Validation
- Before fix: deterministic timeout reproduced as above.
- After fix: `BufferTest`, `HistogramTest`, and `SummaryTest` — 50 tests
passed.
- Temporary local JUnit launcher: both regression cases and the original
`HistogramTest.testObserveMultithreaded`, repeated 50 times with each of
`-XX:ActiveProcessorCount=1`, `2`, and `4` — **450 successful test
executions** on JDK 25.
- `mise run lint:fix` — passed; formatter changes retained.
- `mise run build -- -DskipITs=true` — passed. Docker-backed integration
tests skipped because
Docker is unavailable locally.
- `mise run test` — full unit-test suite passed.
- `./mvnw verify -pl prometheus-metrics-core -am` — passed, including
coverage checks.
- `git diff --check` — passed.
Kept separate from the benchmark-only PR.
## Current scope after update onto main
The original production fix is already present on `main` through
[#2441](#2441) and
[#2468](#2468). This PR
now contains the remaining merge-update and review hardening: explicit
coverage for both generation handoff windows, fail-closed absolute
cutoffs, clearer test seams, and non-masking executor cleanup
assertions. The historical race reproduction and validation context
above are retained.
---------
Signed-off-by: Gregor Zeitlinger <gregor.zeitlinger@grafana.com>
Summary
Benchmark-only follow-up for the release review; no production metric changes.
OpenTelemetry, Codahale, and legacy simpleclient comparisons remain in full/local/nightly runs.
Keep all client_java exposition benchmarks, including OpenMetrics.
with one-thread and four-thread variants. One invocation is one metric update.
base/head allocation deltas only when configurations match.
Related PR
Label lookup optimization #2442 is pending this
benchmark infrastructure. After this lands, update that branch and rerun the benchmarks so base and
head contain identical lookup benchmark code. The new methods in this PR itself have head-only
results and do not establish the optimization's benefit.
Validation
mise run lint:fix— passed; formatter changes retained.mise run test— passed.mise run build -- -DskipITs=true— passed.mise run buildcompiled the benchmark module, but Docker-backed integration tests randespite
-DskipTestsand failed because no Docker environment was available.OpenMetrics/Prometheus exposition cases.
JDK 25.0.3,
-Xms128m -Xmx256m. This was a smoke test on a shared development host, not acontrolled base/head performance comparison. Repeated lookup reported about 64 B/update;
cached increments were near zero.
git diff --check— passed.