11require 'logger'
22require 'thread'
3+ require 'fileutils'
34
45module LogStash
56 module Outputs
67 class CustomSizeBasedBuffer
7- def initialize ( max_size_mb , max_interval , &flush_callback )
8+ def initialize ( max_size_mb , max_interval , buffer_file , &flush_callback )
9+ raise ArgumentError , "buffer_file cannot be nil" if buffer_file . nil?
10+
811 @buffer_config = {
912 max_size : max_size_mb * 1024 * 1024 , # Convert MB to bytes
1013 max_interval : max_interval ,
14+ buffer_file : buffer_file ,
1115 logger : Logger . new ( STDOUT )
1216 }
1317 @buffer_state = {
@@ -25,6 +29,7 @@ def initialize(max_size_mb, max_interval, &flush_callback)
2529 @shutdown = false
2630 @pending_mutex = Mutex . new
2731 @flush_mutex = Mutex . new
32+ load_buffer_from_file
2833 @buffer_config [ :logger ] . info ( "CustomSizeBasedBuffer initialized with max_size: #{ max_size_mb } MB, max_interval: #{ max_interval } seconds" )
2934 end
3035
@@ -46,6 +51,7 @@ def shutdown
4651 @shutdown = true
4752 @buffer_state [ :timer ] . kill
4853 buffer_flush ( final : true )
54+ clear_file_buffer
4955 end
5056
5157 private
@@ -69,30 +75,49 @@ def buffer_flush(options = {})
6975 items_flushed = 0
7076
7177 begin
78+ outgoing_items = [ ]
79+ outgoing_size = 0
80+
7281 @pending_mutex . synchronize do
7382 return 0 if @buffer_state [ :pending_size ] == 0
7483
7584 time_since_last_flush = Time . now . to_i - @buffer_state [ :last_flush ]
7685
77- return 0 if !force && @buffer_state [ :pending_size ] < @buffer_config [ :max_size ] && time_since_last_flush < @buffer_config [ :max_interval ]
86+ if !force && @buffer_state [ :pending_size ] < @buffer_config [ :max_size ] && time_since_last_flush < @buffer_config [ :max_interval ]
87+ return 0
88+ end
7889
7990 if force
8091 @buffer_config [ :logger ] . info ( "Time-based flush triggered after #{ @buffer_config [ :max_interval ] } seconds" )
8192 elsif @buffer_state [ :pending_size ] >= @buffer_config [ :max_size ]
8293 @buffer_config [ :logger ] . info ( "Size-based flush triggered at #{ @buffer_state [ :pending_size ] } bytes was reached" )
94+ else
95+ @buffer_config [ :logger ] . info ( "Flush triggered without specific condition" )
8396 end
8497
8598 outgoing_items = @buffer_state [ :pending_items ] . dup
8699 outgoing_size = @buffer_state [ :pending_size ]
87100 buffer_initialize
101+ end
88102
103+ begin
89104 @flush_callback . call ( outgoing_items ) # Pass the list of events to the callback
105+ clear_flushed_buffer_states ( outgoing_items ) unless ::File . zero? ( @buffer_config [ :buffer_file ] ) # Clear the flushed items from the file
106+ rescue => e
107+ @buffer_config [ :logger ] . error ( "Flush failed: #{ e . message } " )
108+ # Save the items to the file buffer in case of failure
109+ @pending_mutex . synchronize do
110+ @buffer_state [ :pending_items ] = outgoing_items + @buffer_state [ :pending_items ]
111+ @buffer_state [ :pending_size ] += outgoing_size
112+ save_buffer_to_file
113+ end
114+ raise e
115+ end
90116
91- @buffer_state [ :last_flush ] = Time . now . to_i
92- @buffer_config [ :logger ] . info ( "Flush completed. Flushed #{ outgoing_items . size } events, #{ outgoing_size } bytes" )
117+ @buffer_state [ :last_flush ] = Time . now . to_i
118+ @buffer_config [ :logger ] . info ( "Flush completed. Flushed #{ outgoing_items . size } events, #{ outgoing_size } bytes" )
93119
94- items_flushed = outgoing_items . size
95- end
120+ items_flushed = outgoing_items . size
96121 ensure
97122 @flush_mutex . unlock
98123 end
@@ -104,6 +129,78 @@ def buffer_initialize
104129 @buffer_state [ :pending_items ] = [ ]
105130 @buffer_state [ :pending_size ] = 0
106131 end
132+
133+ def clear_flushed_buffer_states ( flushed_items )
134+ remaining_buffer_states = [ ]
135+ ::File . foreach ( @buffer_config [ :buffer_file ] ) do |line |
136+ begin
137+ buffer_state = Marshal . load ( line )
138+ buffer_state [ :pending_items ] -= flushed_items
139+ buffer_state [ :pending_size ] = buffer_state [ :pending_items ] . sum ( &:bytesize )
140+ remaining_buffer_states << buffer_state unless buffer_state [ :pending_items ] . empty?
141+ rescue ArgumentError => e
142+ @buffer_config [ :logger ] . error ( "Failed to load buffer state: #{ e . message } " )
143+ next
144+ end
145+ end
146+
147+ ::File . open ( @buffer_config [ :buffer_file ] , 'w' ) do |file |
148+ remaining_buffer_states . each do |state |
149+ file . write ( Marshal . dump ( state ) + "\n " )
150+ end
151+ end
152+ end
153+
154+ def save_buffer_to_file
155+ buffer_state_copy = @buffer_state . dup
156+ buffer_state_copy . delete ( :timer ) # Exclude the Thread object from serialization
157+
158+ ::FileUtils . mkdir_p ( ::File . dirname ( @buffer_config [ :buffer_file ] ) ) # Ensure directory exists
159+ ::File . open ( @buffer_config [ :buffer_file ] , 'a' ) do |file |
160+ file . write ( Marshal . dump ( buffer_state_copy ) + "\n " )
161+ end
162+ @buffer_config [ :logger ] . info ( "Saved buffer state to file" )
163+ end
164+
165+ def load_buffer_from_file
166+ ::FileUtils . mkdir_p ( ::File . dirname ( @buffer_config [ :buffer_file ] ) ) # Ensure directory exists
167+ ::File . open ( @buffer_config [ :buffer_file ] , 'a' ) { } # Create the file if it doesn't exist
168+
169+ if ::File . file? ( @buffer_config [ :buffer_file ] ) && !::File . zero? ( @buffer_config [ :buffer_file ] )
170+ begin
171+ @pending_mutex . synchronize do
172+ buffer_states = [ ]
173+ ::File . foreach ( @buffer_config [ :buffer_file ] ) do |line |
174+ buffer_states << Marshal . load ( line )
175+ end
176+ @buffer_state = buffer_states . reduce do |acc , state |
177+ acc [ :pending_items ] . concat ( state [ :pending_items ] )
178+ acc [ :pending_size ] += state [ :pending_size ]
179+ acc
180+ end
181+ @buffer_state [ :timer ] = Thread . new do
182+ loop do
183+ sleep ( @buffer_config [ :max_interval ] )
184+ buffer_flush ( force : true )
185+ end
186+ end
187+ # Ensure the buffer does not flush immediately upon loading
188+ @buffer_state [ :last_flush ] = Time . now . to_i
189+ end
190+ @buffer_config [ :logger ] . info ( "Loaded buffer state from file" )
191+ rescue => e
192+ @buffer_config [ :logger ] . error ( "Failed to load buffer from file: #{ e . message } " )
193+ buffer_initialize
194+ end
195+ else
196+ buffer_initialize
197+ end
198+ end
199+
200+ def clear_file_buffer
201+ ::File . open ( @buffer_config [ :buffer_file ] , 'w' ) { } # Truncate the file
202+ @buffer_config [ :logger ] . info ( "File buffer cleared on shutdown" )
203+ end
107204 end
108205 end
109206end
0 commit comments