buffer: stage_size gauge goes transiently negative when write and enqueue_chunk overlap
Describe the bug
Buffer#write cannot update @stage_size_metrics while it still holds chunk locks, so it defers the add until every chunk lock has been released:
That leaves a window between chunk.mon_exit and the deferred add where the bytes are already committed into the chunk and visible through @stage[metadata], but have not been counted yet. If a flush thread calls enqueue_chunk inside that window it subtracts the current chunk.bytesize, including the bytes the writer has not added:
So sub runs before the matching add and stage_size drops below zero until the deferred add lands.
#5467 clamps the values at export time so exporters no longer publish negative sizes. It deliberately does not touch the gauge store, because clamping LocalMetrics#sub/#dec turns the self-correcting negative into a permanent over-count and makes Buffer#storable? reject every write. This issue tracks the underlying accounting window that #5467 masks.
To Reproduce
The window is deterministically reproducible by pausing the writer on the staged chunk's mon_exit. This test passes on master today, i.e. it asserts the negative intermediate value:
require_relative '../helper'
require 'fluent/plugin/buffer'
require 'fluent/plugin/buffer/memory_chunk'
require 'fluent/plugin_id'
require 'fluent/log'
module StageSizeRace
class Owner < Fluent::Plugin::Base
include Fluent::PluginId
include Fluent::PluginLoggerMixin
end
class Buf < Fluent::Plugin::Buffer
def create_metadata(timekey = nil, tag = nil, variables = nil)
Fluent::Plugin::Buffer::Metadata.new(timekey, tag, variables)
end
def resume
return {}, []
end
def generate_chunk(metadata)
Fluent::Plugin::Buffer::MemoryChunk.new(metadata)
end
end
end
class StageSizeRaceTest < ::Test::Unit::TestCase
test 'stage_size goes negative while the deferred add is pending' do
b = StageSizeRace::Buf.new
b.owner = StageSizeRace::Owner.new
b.configure(config_element('buffer', '', { 'total_limit_size' => 1024, 'chunk_limit_size' => 4096 }))
b.start
m = b.create_metadata
b.write({ m => ['a' * 400] })
chunk = b.stage[m]
assert_equal 400, b.stage_size
reached = Queue.new
resume = Queue.new
armed = false
# pause the writer right after it releases the chunk lock (buffer.rb L386)
# and before the deferred add (buffer.rb L414-L419)
chunk.define_singleton_method(:mon_exit) do
r = super()
if armed
armed = false
reached << true
resume.pop
end
r
end
armed = true
writer = Thread.new { b.write({ m => ['b' * 400] }) }
reached.pop
b.enqueue_chunk(m)
assert_equal(-400, b.stage_size) # nothing is staged, yet the gauge is negative
resume << true
writer.join
assert_equal 0, b.stage.size
assert_equal 0, b.stage_size # self-heals once the deferred add lands
end
endOutput at the two checkpoints:
mid: stage.size=0 stage_size=-400 queue_size=800
end: stage.size=0 stage_size=0 queue_size=800Expected behavior
stage_size should never be negative, and it should equal the total bytesize of the chunks currently in @stage once concurrent operations have settled.
Your Environment
- Fluentd version:
- Package version:
- Operating system:
- Kernel version:Your Configuration
N/AYour Error Log
N/AAdditional context
No response
Source: fluent/fluentd