Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 20 additions & 2 deletions lib/fluent/plugin/buffer.rb
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,9 @@ class BufferChunkOverflowError < BufferError; end # A record size is larger than
desc 'If true, chunks are thrown away when unrecoverable error happens'
config_param :disable_chunk_backup, :bool, default: false

desc 'The total size limit for chunks evacuated on unrecoverable errors. Once evacuation reaches this size, remaining chunks are purged without being evacuated. Set 0 to disable evacuation. nil (default) means no limit.'
config_param :evacuate_limit_size, :size, default: nil

Metadata = Struct.new(:timekey, :tag, :variables, :seq) do
def initialize(timekey, tag, variables)
super(timekey, tag, variables, 0)
Expand Down Expand Up @@ -625,11 +628,15 @@ def clear_queue!
log.on_trace { log.trace "clearing queue", instance: self.object_id }

synchronize do
evacuated_size = 0

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.

[Nit] since the budget actually resets on every clear_queue! call rather than being a true cumulative cap, the name evacuate_limit_size and the desc's "total size limit" phrasing read as more absolute than what's implemented. Worth clarifying in the desc that this is per-clear_queue! call, not a lifetime/cumulative limit — otherwise the naming suggestion (e.g. something like evacuate_limit_size_per_clear) is up to you.

until @queue.empty?
begin
q = @queue.shift
evacuate_chunk(q)
log.trace("purging a chunk in queue"){ {id: dump_unique_id_hex(chunk.unique_id), bytesize: chunk.bytesize, size: chunk.size} }
if evacuate_chunk?(q.bytesize, evacuated_size)
evacuate_chunk(q)
evacuated_size += q.bytesize
Comment on lines +636 to +637

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.

In clear_queue!, evacuated_size += q.bytesize runs unconditionally after evacuate_chunk(q), without checking whether the backup actually succeeded. Since evacuate_chunk in both buf_file.rb and buf_file_single.rb rescues its own exceptions instead of re-raising, a failed backup copy (e.g. ENOSPC, permission error) still counts against the budget.

Worth confirming success (or having evacuate_chunk report bytes actually written) before counting a chunk toward evacuated_size.

end
Comment on lines +635 to +638
log.trace("purging a chunk in queue"){ {id: dump_unique_id_hex(q.unique_id), bytesize: q.bytesize, size: q.size} }
q.purge
rescue => e
log.error "unexpected error while clearing buffer queue", error_class: e.class, error: e
Expand All @@ -640,6 +647,17 @@ def clear_queue!
end
end

# Decide whether a chunk should be evacuated, honoring evacuate_limit_size.
# nil limit means no bound (evacuate everything), 0 disables evacuation, and
# a positive limit only evacuates a chunk if it fits within the remaining budget
# so the total evacuated size never exceeds the limit.
def evacuate_chunk?(chunk_bytesize, evacuated_size)
return true if @evacuate_limit_size.nil?
return false if @evacuate_limit_size <= 0

evacuated_size + chunk_bytesize <= @evacuate_limit_size
end

def evacuate_chunk(chunk)
# Overwrite this on demand.
#
Expand Down
45 changes: 45 additions & 0 deletions test/plugin/test_buffer.rb
Original file line number Diff line number Diff line change
Expand Up @@ -475,6 +475,51 @@ def create_chunk_es(metadata, es)
assert{ qchunks.all?{ |c| c.purged } }
end

test '#clear_queue! evacuates every queued chunk by default (no evacuate_limit_size)' do
evacuated = []
@p.define_singleton_method(:evacuate_chunk) { |chunk| evacuated << chunk.bytesize }
qchunks = @p.queue.dup

assert_nil @p.evacuate_limit_size

@p.clear_queue!

assert_equal [100, 100, 3], evacuated
assert_equal [], @p.queue
assert{ qchunks.all?{ |c| c.purged } }
end

test '#clear_queue! skips evacuation entirely when evacuate_limit_size is 0, but still purges' do
p = create_buffer({'evacuate_limit_size' => 0})
evacuated = []
p.define_singleton_method(:evacuate_chunk) { |chunk| evacuated << chunk.bytesize }
p.start
qchunks = p.queue.dup

p.clear_queue!

assert_equal [], evacuated
assert_equal [], p.queue
assert{ qchunks.all?{ |c| c.purged } }
end

test '#clear_queue! bounds the total evacuated size to evacuate_limit_size' do
p = create_buffer({'evacuate_limit_size' => 200})
evacuated = []
p.define_singleton_method(:evacuate_chunk) { |chunk| evacuated << chunk.bytesize }
p.start
qchunks = p.queue.dup

p.clear_queue!

# queued chunks are 100, 100 and 3 bytes; the first two fill the 200 byte
# budget, so the last one is purged without being evacuated
assert_equal [100, 100], evacuated
assert_equal 200, evacuated.sum
assert_equal [], p.queue
assert{ qchunks.all?{ |c| c.purged } }
end

test '#write returns immediately if argument data is empty array' do
assert_equal [@dm0,@dm1,@dm1], @p.queue.map(&:metadata)
assert_equal [@dm2,@dm3], @p.stage.keys
Expand Down