fix(metrics): export-only clamp for buffer statistics (#5303) - #5467
fix(metrics): export-only clamp for buffer statistics (#5303)#5467VedantMadane wants to merge 6 commits into
Conversation
LocalMetrics sub/dec could drive buffer size gauges below zero when racey buffer accounting under/over-subtracted (issues fluent#5303, fluent#2712). That produced negative Prometheus series such as fluentd_output_status_buffer_total_bytes. Clamp gauge sub/dec at zero in LocalMetrics, and clamp stage/queue sizes when exporting buffer statistics. Fixes fluent#5303 Signed-off-by: Vedant Madane <6527493+VedantMadane@users.noreply.github.com>
a9e5a03 to
50918f2
Compare
|
Thanks for digging into #5303. However, I think clamping at zero is the wrong direction for the buffer counters: it converts a transient, self-healing negative into a permanent over-count.
That leaves a window in which the enqueue thread can run Take a 100-byte chunk in that window:
Under load this repeats on every flush cycle and accumulates. Note that @total_limit_size > @stage_size_metrics.get + @queue_size_metrics.getSo an effectively empty buffer eventually fails this check and the output starts raising Two related points:
I would suggest fixing the add/sub pairing in One more thing unrelated to the design question: |
There was a problem hiding this comment.
Pull request overview
This PR addresses negative buffer size gauge values that can surface under concurrent buffer stage/queue transitions, ensuring exported buffer metrics do not report negative byte sizes (notably impacting Prometheus consumers).
Changes:
- Clamp
LocalMetricsgaugesub/decoperations so gauge values do not fall below zero. - Clamp buffer
statisticsexport forstage_byte_size/queue_byte_sizeto non-negative values. - Add unit tests to verify gauge
sub/decnever produce negative values.
Reviewed changes
Copilot reviewed 3 out of 3 changed files in this pull request and generated 1 comment.
| File | Description |
|---|---|
lib/fluent/plugin/metrics_local.rb |
Floors gauge decrements/subtractions at zero to prevent negative gauges in the default local metrics backend. |
lib/fluent/plugin/buffer.rb |
Ensures exported buffer statistics never publish negative stage/queue byte sizes by clamping at export time. |
test/plugin/test_metrics_local.rb |
Adds unit tests validating non-negative behavior for gauge sub/dec. |
Suppressed comments (1)
lib/fluent/plugin/buffer.rb:925
available_buffer_space_ratiosis described as a ratio of available space, butbuffer_spacecan still go below 0.0 (or above 1.0) if stage/queue counters drift upward beyond@total_limit_size. This would export negative (or >100) ratios; consider clamping the computed ratio to [0.0, 1.0] before publishing.
buffer_space = 1.0 - ((stage_size + queue_size * 1.0) / @total_limit_size)
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
Cover export-path clamping for stage_byte_size/queue_byte_size and non-negative total_queued_size when underlying gauges are negative. Signed-off-by: Vedant Madane <6527493+VedantMadane@users.noreply.github.com>
3b17bb5 to
ce7211a
Compare
When stage/queue counters overshoot total_limit_size the free-space ratio could go negative (or above 100). Clamp the ratio to [0, 1] after non-negative size export. Strengthen #statistics unit tests: - negative gauges export stage/queue/total as 0 and ratio 100.0 - overshoot beyond total_limit_size clamps ratio to 0.0 Refs fluent#5303 Signed-off-by: Vedant Madane <6527493+VedantMadane@users.noreply.github.com>
1eba6ab to
9afb98c
Compare
| # Keep available-space ratio in [0, 1] even if counters overshoot total_limit_size. | ||
| buffer_space = 1.0 - ((stage_size + queue_size * 1.0) / @total_limit_size) | ||
| buffer_space = [[buffer_space, 0.0].max, 1.0].min |
There was a problem hiding this comment.
Addressed in 3fe8d18: no longer uses Array#max/min on the ratio. Zero capacity takes the else branch (0.0); positive denom uses Numeric#clamp on a non-NaN value.
Signed-off-by: Vedant Madane <6527493+VedantMadane@users.noreply.github.com>
|
Addressed remaining Copilot note on NaN when |
…ze is 0 Signed-off-by: Vedant Madane <6527493+VedantMadane@users.noreply.github.com>
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 4 out of 4 changed files in this pull request and generated no new comments.
Suppressed comments (2)
Previously missed (1) — in code that hasn't changed since the last review.
lib/fluent/plugin/metrics_local.rb:68
dec_gaugeno longer reliably returns the updated gauge value because the last expression is the conditional clamp (@store = 0 if ...), which returnsnilwhen no clamp happens. This changes behavior vsinc(and the previousdec_gaugeimplementation) and could break any callers that use the return value. Consider explicitly returning@storeafter clamping.
This issue also appears on line 80 of the same file.
# Buffer size / length gauges must never go negative even if a race
# causes sub/dec to run more times than add/inc (see #5303).
@store -= 1
@store = 0 if @store < 0
lib/fluent/plugin/metrics_local.rb:83
sub_gaugehas the same return-value issue asdec_gauge: the clamp line is the last expression, so the method returnsnilwhen no clamp happens. Returning@storekeeps gauge operations consistent and avoids surprising behavior changes for callers.
# Prevent negative values that leak into Prometheus buffer metrics
# (fluentd_output_status_buffer_total_bytes, etc.). See #5303 / #2712.
@store -= value
@store = 0 if @store < 0
| # Buffer size / length gauges must never go negative even if a race | ||
| # causes sub/dec to run more times than add/inc (see #5303). | ||
| @store -= 1 | ||
| @store = 0 if @store < 0 |
There was a problem hiding this comment.
Clamping sub_gauge/dec_gauge at zero discards a transient negative excursion that used to self-correct, so stage_size stays permanently inflated; Buffer#storable? reads that raw gauge, so the buffer refuses every further write even when it is empty.
There was a problem hiding this comment.
Addressed in 3fe8d18: reverted LocalMetrics sub/dec zero-clamp entirely so the deferred-add vs enqueue_chunk-sub race can still self-correct. Export-only clamp remains in Buffer#statistics.
| # Prevent negative values that leak into Prometheus buffer metrics | ||
| # (fluentd_output_status_buffer_total_bytes, etc.). See #5303 / #2712. | ||
| @store -= value | ||
| @store = 0 if @store < 0 |
There was a problem hiding this comment.
Clamping sub_gauge/dec_gauge at zero discards a transient negative excursion that used to self-correct, so stage_size stays permanently inflated; Buffer#storable? reads that raw gauge, so the buffer refuses every further write even when it is empty.
There was a problem hiding this comment.
Addressed in 3fe8d18: same as sibling — LocalMetrics clamp removed; raw gauge semantics restored.
| # Buffer size / length gauges must never go negative even if a race | ||
| # causes sub/dec to run more times than add/inc (see #5303). | ||
| @store -= 1 | ||
| @store = 0 if @store < 0 |
There was a problem hiding this comment.
@store = 0 if @store < 0 is the last expression in the synchronize block, and a false guard evaluates to nil, so dec/sub return nil on the normal (non-clamped) path instead of the new store value.
There was a problem hiding this comment.
Addressed in 3fe8d18: clamp (and the bad last-expression return) removed with the revert of LocalMetrics flooring.
| # Prevent negative values that leak into Prometheus buffer metrics | ||
| # (fluentd_output_status_buffer_total_bytes, etc.). See #5303 / #2712. | ||
| @store -= value | ||
| @store = 0 if @store < 0 |
There was a problem hiding this comment.
@store = 0 if @store < 0 is the last expression in the synchronize block, and a false guard evaluates to nil, so dec/sub return nil on the normal (non-clamped) path instead of the new store value.
There was a problem hiding this comment.
Addressed in 3fe8d18: clamp (and the bad last-expression return) removed with the revert of LocalMetrics flooring.
| # Keep available-space ratio in [0, 1] even if counters overshoot total_limit_size. | ||
| # Avoid Array#max/min on NaN (e.g. 0/0 when total_limit_size is 0), which raises. | ||
| denom = @total_limit_size.to_f | ||
| if denom > 0.0 | ||
| buffer_space = 1.0 - ((stage_size + queue_size).to_f / denom) | ||
| buffer_space = 0.0 if buffer_space.nan? || buffer_space < 0.0 | ||
| buffer_space = 1.0 if buffer_space > 1.0 | ||
| else | ||
| buffer_space = 0.0 | ||
| end |
There was a problem hiding this comment.
The denom > 0.0 branch already covers the case this comment describes, so the NaN
guard inside it is dead code.
total_limit_size == 0no longer reaches the division at all — it takes theelsebranch.- Inside the
if,stage_sizeandqueue_sizeare already floored at 0 by the
[..., 0].maxcalls above, anddenomis positive and finite. So
(stage_size + queue_size).to_f / denomis always>= 0and neverNaN, which means
neitherbuffer_space.nan?norbuffer_space > 1.0can ever be true.
I'd drop both and keep just the lower bound:
denom = @total_limit_size.to_f
buffer_space = denom > 0.0 ? (1.0 - (stage_size + queue_size) / denom).clamp(0.0, 1.0) : 0.0There was a problem hiding this comment.
Addressed in 3fe8d18: simplified to your suggested form — denom > 0.0 ? (...).clamp(0.0, 1.0) : 0.0; dead NaN guard dropped.
Address Watson1978 review on fluent#5467: - Revert LocalMetrics sub/dec zero-clamping. Flooring the gauge store turns a transient deferred-add vs enqueue_chunk-sub race into a permanent stage_size over-count; Buffer#storable? then refuses writes. Keep self-correcting raw gauge semantics. - Keep export-path [get, 0].max for stage_byte_size/queue_byte_size/ total_queued_size so Prometheus consumers never see negatives. - Simplify available_buffer_space_ratios: denom > 0 ? ratio.clamp(0,1) : 0 (drop dead NaN guard inside the positive-denom branch). - Drop LocalMetrics clamp unit tests; keep statistics export tests. Signed-off-by: Vedant Madane <6527493+VedantMadane@users.noreply.github.com>
|
Thanks @Watson1978 — you were right about the gauge-store clamp. Addressed in 3fe8d18:
I did not move the clamp into the |
Which issue(s) this PR fixes:
Fixes #5303
What this PR does / why we need it:
Buffer size gauges (
stage_byte_size,queue_byte_size, and derivedtotal_queued_size) can go transiently negative when Fluentd core under/over-subtracts during concurrent stage/queue transitions (the deferred@stage_size_metrics.addafter chunk unlock vsenqueue_chunk'ssub— see #2712 / #2734). Those values are mirrored by the Prometheus plugin asfluentd_output_status_buffer_total_bytes/fluentd_output_status_buffer_stage_byte_size, which is what #5303 reports.Clamping the gauge store on
sub/decis the wrong fix: it turns a self-correcting transient negative into a permanent over-count, soBuffer#storable?(which reads the raw gauge) eventually refuses every write. Thanks @Watson1978 for catching that.This PR takes an export-only approach:
LocalMetricsgaugesub/dec/setsemantics unchanged (negatives still allowed so the deferred-add race can self-heal).>= 0only when buildingstatistics(the path Prometheus and the monitor agent consume).available_buffer_space_ratiosto[0, 100]when counters overshoottotal_limit_size, and treattotal_limit_size == 0as 0% free without dividing (no NaN / no dead NaN guard).Docs Changes:
None
General Checklist:
Tests:
test/plugin/test_buffer.rb#statistics: negative underlying gauges export as 0; overshoot clamps ratio to 0;total_limit_size == 0does not raise and ratio stays finite.