SIGN IN SIGN UP

feat(promhttp): add CoalesceGather option to deduplicate concurrent Gather calls (#1969)

* feat(promhttp): add CoalesceGather option to deduplicate concurrent Gather calls

When a collector's Collect() is slower than the scrape interval, each
incoming HTTP request triggers an independent Gather() that spawns its
own goroutine pipeline. With no upper bound, this causes goroutine
pile-up proportional to (scrape rate / collection time) — the apparent
"goroutine leak" reported in #1477.

Add HandlerOpts.CoalesceGather bool. When true, HandlerForTransactional
wraps the underlying TransactionalGatherer in a coalescingGatherer that
allows only one Gather to run at a time. Concurrent requests join the
in-flight cycle and receive the same result once it completes. Reference
counting ensures the TransactionalGatherer done() callback is called
exactly once, after the last handler finishes encoding.

Design decisions:
- Per-cycle gatherCycle object (not fields on the struct) prevents the
  race where a new cycle overwrites result fields while prior waiters are
  still reading them.
- Mutex-based ref counting (not atomic) ensures c.cycle = nil is cleared
  before cy.done() is called, ruling out double-done on a stale pointer.
- close(cy.ready) happens-before <-cy.ready returns (Go memory model),
  so cy.mfs/err/done are safely readable without additional locking.
- Zero overhead when disabled: single if-branch in HandlerForTransactional
  setup, identical code path when CoalesceGather is false.

Fixes #1477

Signed-off-by: Kemal Akkoyun <kemal.akkoyun@datadoghq.com>

* fix(promhttp): fail coalesced joiners when the gatherer panics

When CoalesceGather is enabled and the wrapped gatherer itself panics, requests that joined the in-flight cycle were unblocked with a nil error and returned an empty but successful response. They now receive a sentinel error and fail like the panicking request instead of silently reporting no metrics. The leader's panic still propagates and is handled by net/http, matching the non-coalesced path; no recover is introduced.

Also document the panic behaviour on the option, make the coalescing invariant test deterministic instead of timing-dependent, and align the goroutine-leak test with the repository's WaitGroup convention.

Signed-off-by: Kemal Akkoyun <kemal.akkoyun@datadoghq.com>

* fix(promhttp): give coalesced callers independent metric slices

Concurrent callers sharing a coalesced Gather cycle previously received the same slice header. A caller that filtered or reordered its slice (for example applying name[] query parameters) could then race another caller of the same cycle. Each caller now gets its own slices.Clone; the MetricFamily values stay shared and read-only.

Also clarify the CoalesceGather documentation (shared-snapshot staleness, the Timeout interaction, and slice-versus-contents mutation) and tidy the sentinel error and coalescer test helpers.

Signed-off-by: Kemal Akkoyun <kemal.akkoyun@datadoghq.com>

* docs(promhttp): mark CoalesceGather as experimental

Signed-off-by: Kemal Akkoyun <kemal.akkoyun@datadoghq.com>

---------

Signed-off-by: Kemal Akkoyun <kemal.akkoyun@datadoghq.com>
K
Kemal Akkoyun committed
78262a77b89922f94a19ecd25eaeb849fb58f2cc
Parent: 34e9a7f
Committed by GitHub <noreply@github.com> on 7/16/2026, 9:47:04 AM