Skip to content

Ingester: fix cortex_ingester_ingestion_delay_seconds losing most observations - #7744

Open
ankit090701 wants to merge 3 commits into
cortexproject:masterfrom
ankit090701:fix-ingestion-delay-native-histogram-reset-duration
Open

Ingester: fix cortex_ingester_ingestion_delay_seconds losing most observations#7744
ankit090701 wants to merge 3 commits into
cortexproject:masterfrom
ankit090701:fix-ingestion-delay-native-histogram-reset-duration

Conversation

@ankit090701

@ankit090701 ankit090701 commented Aug 2, 2026

Copy link
Copy Markdown

What this PR does:

cortex_ingester_ingestion_delay_seconds (added in #7443) is registered with NativeHistogramMinResetDuration: 1. Since the field is a time.Duration, the untyped constant 1 means 1 nanosecond, not 1 hour — every other native histogram in pkg/ingester/metrics.go (and repo-wide, ~60 other occurrences) uses time.Hour.

Root cause, verified by reading the vendored client_golang source (vendor/github.com/prometheus/client_golang/prometheus/histogram.go):

  • Once a native histogram's bucket count exceeds NativeHistogramMaxBucketNumber (100 here), limitBuckets() calls maybeReset() first.
  • maybeReset() fully resets the histogram (resetCounts() zeroes sumBits, count, and all classic bucket counts, plus native buckets — it keeps only the single latest observation) if now - lastResetTime >= NativeHistogramMinResetDuration.
  • With the min reset duration at ~1ns, that condition is true on essentially every call, so instead of the intended graceful degradation (bucket-width doubling / zero-bucket widening, which reduces resolution but keeps counts intact), the histogram performs a full reset every time it crosses the bucket limit — silently discarding almost all prior observations.

This is not confined to the native representation: resetCounts() also zeroes the classic Buckets counts, so histogram_quantile() over the classic buckets and _count/_sum are equally corrupted.

Fix: change NativeHistogramMinResetDuration: 1 to NativeHistogramMinResetDuration: 1 * time.Hour, matching every other histogram in the file.

Testing:

Added TestIngestionDelaySecondsHistogram_DoesNotLoseObservationsOnNativeBucketLimit in pkg/ingester/metrics_test.go, which observes 500 widely-spread values (forcing the native bucket count past the 100 limit) into the real ingesterMetrics.ingestionDelaySeconds built by newIngesterMetrics, then asserts the histogram's SampleCount still equals 500.

  • With the bug (NativeHistogramMinResetDuration: 1): the test fails, SampleCount gets stuck at 100 (repeated full resets right at the bucket-limit boundary) instead of 500.
  • With the fix: the test passes, SampleCount is exactly 500 (bucket count is reduced by merging/widening, not by discarding samples).

I verified this by reverting only the one-line fix (keeping the new test) with git stash and confirming the test fails exactly as described, then restoring the fix and confirming it passes. go vet ./pkg/ingester/... is clean and the full pkg/ingester suite (go test -tags "netgo slicelabels" ./pkg/ingester/...) passes.

Which issue(s) this PR fixes:
Fixes #7731

Checklist

  • Tests updated
  • Documentation added
  • CHANGELOG.md updated - the order of entries should be [CHANGE], [FEATURE], [ENHANCEMENT], [BUGFIX]
  • docs/configuration/v1-guarantees.md updated if this PR introduces experimental flags

(No user-facing flags/config changed, so no v1-guarantees.md update needed. Per @yeya24's review, the CHANGELOG entry was removed: #7443, which introduced this metric, hasn't shipped in a release yet (latest release v1.21.1 predates it), so there's nothing for a "bugfix" note to inform users about.)

…ervations

The histogram was registered with NativeHistogramMinResetDuration: 1,
an untyped constant that Go implicitly converts to time.Duration(1),
i.e. 1 nanosecond, instead of the intended 1 hour used by every other
native histogram in this file.

When the native histogram's bucket count exceeds
NativeHistogramMaxBucketNumber (100), client_golang's limitBuckets()
first tries maybeReset(), which fully resets the histogram (wiping
both native and classic bucket counts, keeping only the latest
observation) if at least NativeHistogramMinResetDuration has elapsed
since the last reset. With an effectively-zero duration, that
condition is satisfied on virtually every call, so instead of
gracefully reducing resolution (bucket width doubling / zero bucket
widening), the histogram repeatedly self-resets and silently drops
the large majority of observations. In a 100k-sample simulation this
loses ~86% of observations, corrupting both _count/_sum and the
classic le="600" bucket that operators alert on for ingestion lag.

Fix it to 1 * time.Hour, matching every other histogram in this file.

Fixes cortexproject#7731

Signed-off-by: ankit090701 <ankitanku090701@gmail.com>
Signed-off-by: ankit090701 <ankitanku090701@gmail.com>
Comment thread CHANGELOG.md Outdated
* [BUGFIX] Ring: Fix DynamoDB KV CAS not retrying on transactional conditional check failures. `TransactWriteItems` reports condition failures as `TransactionCanceledException` with a `ConditionalCheckFailed` cancellation reason, which was not recognized as retryable, so any concurrent ring update conflict (e.g. many ingesters joining during a rolling update) failed immediately instead of re-reading and retrying. `TransactionConflict` cancellation reasons are also treated as retryable. #7706
* [BUGFIX] Distributor: Return HTTP 499 (Client Closed Request) instead of 500 when a remote-write or OTLP push is canceled by the client, so client-side cancellations are no longer counted as server-side errors. #7717
* [BUGFIX] Querier: Fix gRPC `codes.Canceled` errors being mapped to HTTP 500 instead of 499 when a client cancels a query. #7738
* [BUGFIX] Ingester: Fix `cortex_ingester_ingestion_delay_seconds` losing the large majority of observations. `NativeHistogramMinResetDuration` was set to an untyped `1` (1 nanosecond) instead of `1 * time.Hour`, causing the native histogram to fully reset instead of gracefully reducing resolution every time it exceeded its bucket limit. #7744

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we can skip it. The PR that introduced this bug is not released yet.

@ankit090701 ankit090701 Aug 5, 2026

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good catch, and checked — #7443 merged 2026-07-07, after the latest release tag (v1.21.1, cut 2026-06-04), so this never shipped. Removed the entry in 8db764f.

// so instead of gracefully reducing resolution (bucket width doubling / zero
// bucket widening), the histogram silently drops the vast majority of prior
// observations on every bucket-limit breach.
func TestIngestionDelaySecondsHistogram_DoesNotLoseObservationsOnNativeBucketLimit(t *testing.T) {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Umm do we need this test? I think it is an obvious bug when defining the metric so maybe fine to omit it.

@ankit090701 ankit090701 Aug 5, 2026

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fair to question — happy to drop it if you'd still rather not have it after this. My case for keeping it: the typo itself (1 vs 1 * time.Hour) is obvious once you're looking right at it, but the consequence isn't — I didn't expect "wrong reset duration" to mean "the histogram fully resets and throws away 86% of observations on every bucket-limit breach" until I actually read maybeReset() in the vendored client. Without a test pinning that behavior, a future refactor of this metric block (or someone copying it as a template for a new histogram, which is how the other ~60 correct instances of 1 * time.Hour presumably multiplied in the first place) could reintroduce the exact same bug and nothing would catch it — the type system doesn't, since an untyped 1 silently converts to time.Duration(1).

That said, it's a genuinely fine call to make either way, and I don't want to hold up the PR over it — let me know and I'll remove it if you still think it's not worth keeping.

- Drop the CHANGELOG entry: the bug was introduced in cortexproject#7443, which was
  merged after the latest release (v1.21.1, 2026-06-04) and has never
  shipped, so there's nothing for users to be informed about fixing.
- Fix check-modernize lint failure: use `for range numObservations`
  instead of `for i := 0; i < numObservations; i++` in the new test,
  since the loop index was never used.

Signed-off-by: ankit090701 <ankitanku090701@gmail.com>

@SungJin1212 SungJin1212 left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for fixing it
I also don't think the test is necessary, but either way I'm fine.

@dosubot dosubot Bot added the lgtm This PR has been approved by a maintainer label Aug 5, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

component/ingester lgtm This PR has been approved by a maintainer size/M type/bug

Projects

None yet

Development

Successfully merging this pull request may close these issues.

cortex_ingester_ingestion_delay_seconds loses ~86% of observations: NativeHistogramMinResetDuration is 1ns, not 1h

3 participants