logbrew-sdk 0.1.1 → 0.1.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,493 @@
1
+ # frozen_string_literal: true
2
+
3
+ module LogBrew
4
+ class DeliveryHealth
5
+ FIELDS = %i[
6
+ state queued_events queued_bytes dropped_events in_flight last_outcome
7
+ consecutive_failures pause_reason successful_flushes failed_flushes retry_delay_ms
8
+ ].freeze
9
+
10
+ attr_reader(*FIELDS)
11
+
12
+ def initialize(**values)
13
+ FIELDS.each { |field| instance_variable_set("@#{field}", values.fetch(field)) }
14
+ freeze
15
+ end
16
+
17
+ def to_h
18
+ FIELDS.each_with_object({}) do |field, result|
19
+ result[field.to_s] = public_send(field)
20
+ end
21
+ end
22
+ end
23
+
24
+ class AutomaticDelivery
25
+ COUNTER_MAX = (2**63) - 1
26
+ STATES = %w[idle running retrying paused stopped closing closed].freeze
27
+ OUTCOMES = %w[none empty accepted retryable_failure terminal_failure stopped].freeze
28
+ PAUSE_REASONS = %w[authentication quota validation nonretryable].freeze
29
+
30
+ attr_reader :transport
31
+
32
+ def self.validate!(
33
+ transport:,
34
+ flush_interval:,
35
+ flush_threshold:,
36
+ retry_base_delay:,
37
+ retry_max_delay:,
38
+ max_queue_size:
39
+ )
40
+ unless transport.respond_to?(:send)
41
+ raise SdkError.new("validation_error", "automatic transport must respond to send")
42
+ end
43
+ validate_positive_number("flush_interval", flush_interval)
44
+ validate_positive_integer("flush_threshold", flush_threshold)
45
+ if flush_threshold > max_queue_size
46
+ raise SdkError.new("validation_error", "flush_threshold must not exceed max_queue_size")
47
+ end
48
+ validate_positive_number("retry_base_delay", retry_base_delay)
49
+ validate_positive_number("retry_max_delay", retry_max_delay)
50
+ if retry_base_delay > retry_max_delay
51
+ raise SdkError.new("validation_error", "retry_base_delay must not exceed retry_max_delay")
52
+ end
53
+ end
54
+
55
+ def self.validate_positive_number(label, value)
56
+ valid = (value.is_a?(Integer) || value.is_a?(Float)) && value.positive?
57
+ valid &&= !value.is_a?(Float) || (!value.nan? && !value.infinite?)
58
+ raise SdkError.new("validation_error", "#{label} must be a positive finite number") unless valid
59
+ end
60
+ private_class_method :validate_positive_number
61
+
62
+ def self.validate_positive_integer(label, value)
63
+ return if value.is_a?(Integer) && value.positive?
64
+
65
+ raise SdkError.new("validation_error", "#{label} must be a positive integer")
66
+ end
67
+ private_class_method :validate_positive_integer
68
+
69
+ def initialize(
70
+ client:,
71
+ transport:,
72
+ flush_interval:,
73
+ flush_threshold:,
74
+ retry_base_delay:,
75
+ retry_max_delay:
76
+ )
77
+ @client = client
78
+ @transport = transport
79
+ @flush_interval = flush_interval.to_f
80
+ @flush_threshold = flush_threshold
81
+ @retry_base_delay = retry_base_delay.to_f
82
+ @retry_max_delay = retry_max_delay.to_f
83
+ @owner_process_id = current_process_id
84
+ @mutex = Mutex.new
85
+ @condition = ConditionVariable.new
86
+ @worker = nil
87
+ @stop_requested = false
88
+ @generation = 0
89
+ @state = "idle"
90
+ @last_outcome = "none"
91
+ @pause_reason = nil
92
+ @in_flight = false
93
+ @consecutive_failures = 0
94
+ @successful_flushes = 0
95
+ @failed_flushes = 0
96
+ @retry_delay = nil
97
+ @retry_deadline = nil
98
+ @next_interval_deadline = nil
99
+ @wake_requested = false
100
+ @observed_queue_count = live_queue_metrics.fetch(:queued_events)
101
+ activate_recovered_work if @observed_queue_count.positive?
102
+ end
103
+
104
+ def assert_process_ownership!
105
+ return if current_process_id == @owner_process_id
106
+
107
+ raise SdkError.new(
108
+ "process_ownership_error",
109
+ "automatic delivery client must be created in the current process"
110
+ )
111
+ end
112
+
113
+ def notify_capture
114
+ assert_process_ownership!
115
+ @mutex.synchronize do
116
+ update_queue_count(live_queue_metrics)
117
+ return if @stop_requested || @state == "paused" || @state == "closed"
118
+
119
+ return unless ensure_worker
120
+ return if @state == "retrying"
121
+
122
+ @next_interval_deadline ||= monotonic_time + @flush_interval
123
+ @wake_requested = true if @observed_queue_count >= @flush_threshold
124
+ @condition.signal
125
+ end
126
+ end
127
+
128
+ def queue_changed
129
+ assert_process_ownership!
130
+ @mutex.synchronize do
131
+ update_queue_count(live_queue_metrics)
132
+ if @observed_queue_count.zero?
133
+ @wake_requested = false
134
+ @next_interval_deadline = nil
135
+ @retry_deadline = nil
136
+ @retry_delay = nil
137
+ end
138
+ @condition.signal
139
+ end
140
+ end
141
+
142
+ def begin_explicit_flush
143
+ assert_process_ownership!
144
+ @mutex.synchronize do
145
+ @generation += 1
146
+ @retry_deadline = nil
147
+ @retry_delay = nil
148
+ previous = [@state, @pause_reason, @generation]
149
+ unless @state == "stopped" || @state == "closed"
150
+ @state = "running"
151
+ @in_flight = true
152
+ end
153
+ previous
154
+ end
155
+ end
156
+
157
+ def complete_explicit_flush(previous, response)
158
+ @mutex.synchronize do
159
+ @in_flight = false
160
+ update_queue_count(live_queue_metrics)
161
+ return unless previous.fetch(2) == @generation
162
+
163
+ if previous.fetch(0) == "stopped"
164
+ @state = "stopped"
165
+ else
166
+ record_success(response)
167
+ return if @observed_queue_count.positive? && !ensure_worker
168
+
169
+ schedule_pending_work
170
+ end
171
+ end
172
+ end
173
+
174
+ def fail_explicit_flush(previous, error)
175
+ @mutex.synchronize do
176
+ @in_flight = false
177
+ update_queue_count(live_queue_metrics)
178
+ return unless previous.fetch(2) == @generation
179
+
180
+ @failed_flushes = increment(@failed_flushes)
181
+ @last_outcome = retryable_failure?(error) ? "retryable_failure" : "terminal_failure"
182
+ if previous.fetch(0) == "paused"
183
+ @state = "paused"
184
+ @pause_reason = previous.fetch(1)
185
+ @wake_requested = false
186
+ @next_interval_deadline = nil
187
+ @retry_delay = nil
188
+ @retry_deadline = nil
189
+ elsif previous.fetch(0) == "stopped"
190
+ @state = "stopped"
191
+ @wake_requested = false
192
+ else
193
+ record_failure(error)
194
+ ensure_worker if retryable_failure?(error) && @observed_queue_count.positive?
195
+ end
196
+ end
197
+ end
198
+
199
+ def complete_automatic_flush(generation, response)
200
+ @mutex.synchronize do
201
+ @in_flight = false
202
+ update_queue_count(live_queue_metrics)
203
+ return unless generation == @generation
204
+ return if @stop_requested || %w[closing closed stopped].include?(@state)
205
+
206
+ record_success(response)
207
+ schedule_pending_work
208
+ end
209
+ end
210
+
211
+ def fail_automatic_flush(generation, error)
212
+ @mutex.synchronize do
213
+ @in_flight = false
214
+ update_queue_count(live_queue_metrics)
215
+ return unless generation == @generation
216
+ return if @stop_requested || %w[closing closed stopped].include?(@state)
217
+
218
+ @failed_flushes = increment(@failed_flushes)
219
+ @last_outcome = retryable_failure?(error) ? "retryable_failure" : "terminal_failure"
220
+ record_failure(error)
221
+ end
222
+ end
223
+
224
+ def prepare_shutdown
225
+ assert_process_ownership!
226
+ worker = @mutex.synchronize do
227
+ if @state == "closing" || @state == "closed"
228
+ raise SdkError.new("shutdown_error", "automatic delivery is already shutting down")
229
+ end
230
+
231
+ @generation += 1
232
+ @state = "closing"
233
+ @stop_requested = true
234
+ @wake_requested = false
235
+ @retry_deadline = nil
236
+ @retry_delay = nil
237
+ @condition.broadcast
238
+ @worker
239
+ end
240
+ join_worker(worker)
241
+ end
242
+
243
+ def complete_shutdown(response)
244
+ @mutex.synchronize do
245
+ @in_flight = false
246
+ queue_metrics = live_queue_metrics
247
+ update_queue_count(queue_metrics)
248
+ record_success(response)
249
+ @state = "closed"
250
+ @worker = nil
251
+ end
252
+ end
253
+
254
+ def resume_after_failed_shutdown(error)
255
+ @mutex.synchronize do
256
+ queue_metrics = live_queue_metrics
257
+ update_queue_count(queue_metrics)
258
+ @stop_requested = false
259
+ @failed_flushes = increment(@failed_flushes)
260
+ @consecutive_failures = increment(@consecutive_failures)
261
+ @worker = nil
262
+ if retryable_failure?(error)
263
+ @state = "retrying"
264
+ @last_outcome = "retryable_failure"
265
+ @pause_reason = nil
266
+ @retry_delay = bounded_retry_delay
267
+ @retry_deadline = monotonic_time + @retry_delay
268
+ ensure_worker if @observed_queue_count.positive?
269
+ @condition.signal
270
+ else
271
+ @state = "paused"
272
+ @last_outcome = "terminal_failure"
273
+ @pause_reason = pause_reason(error)
274
+ @retry_delay = nil
275
+ @retry_deadline = nil
276
+ end
277
+ end
278
+ end
279
+
280
+ def stop
281
+ assert_process_ownership!
282
+ worker = @mutex.synchronize do
283
+ return if @state == "stopped" || @state == "closed"
284
+
285
+ @generation += 1
286
+ @state = "stopped"
287
+ @last_outcome = "stopped"
288
+ @stop_requested = true
289
+ @wake_requested = false
290
+ @retry_deadline = nil
291
+ @retry_delay = nil
292
+ @condition.broadcast
293
+ @worker
294
+ end
295
+ join_worker(worker)
296
+ @mutex.synchronize do
297
+ @worker = nil
298
+ @in_flight = false
299
+ end
300
+ nil
301
+ end
302
+
303
+ def snapshot
304
+ assert_process_ownership!
305
+ @mutex.synchronize do
306
+ queue_metrics = live_queue_metrics
307
+ DeliveryHealth.new(
308
+ state: @state,
309
+ queued_events: queue_metrics.fetch(:queued_events),
310
+ queued_bytes: queue_metrics.fetch(:queued_bytes),
311
+ dropped_events: queue_metrics.fetch(:dropped_events),
312
+ in_flight: @in_flight,
313
+ last_outcome: @last_outcome,
314
+ consecutive_failures: @consecutive_failures,
315
+ pause_reason: @pause_reason,
316
+ successful_flushes: @successful_flushes,
317
+ failed_flushes: @failed_flushes,
318
+ retry_delay_ms: @retry_delay.nil? ? 0 : bounded_milliseconds(@retry_delay)
319
+ )
320
+ end
321
+ end
322
+
323
+ private
324
+
325
+ def activate_recovered_work
326
+ @mutex.synchronize do
327
+ return unless ensure_worker
328
+
329
+ @wake_requested = true
330
+ @condition.signal
331
+ end
332
+ end
333
+
334
+ def ensure_worker
335
+ return false if @stop_requested || @state == "paused" || @state == "closed"
336
+ return true if @worker&.alive?
337
+
338
+ @worker = Thread.new { run }
339
+ @worker.name = "logbrew-delivery" if @worker.respond_to?(:name=)
340
+ @worker.report_on_exception = false if @worker.respond_to?(:report_on_exception=)
341
+ true
342
+ rescue ThreadError
343
+ @worker = nil
344
+ @state = "stopped"
345
+ @last_outcome = "terminal_failure"
346
+ @pause_reason = "nonretryable"
347
+ @stop_requested = true
348
+ false
349
+ end
350
+
351
+ def run
352
+ loop do
353
+ generation = wait_for_wake
354
+ return if generation.nil?
355
+
356
+ @client.send(:perform_automatic_flush, self, generation, @transport)
357
+ end
358
+ rescue StandardError
359
+ @mutex.synchronize do
360
+ @in_flight = false
361
+ @state = "stopped" unless @state == "closed"
362
+ @last_outcome = "terminal_failure"
363
+ @pause_reason = "nonretryable"
364
+ @stop_requested = true
365
+ end
366
+ end
367
+
368
+ def wait_for_wake
369
+ @mutex.synchronize do
370
+ loop do
371
+ return nil if @stop_requested
372
+
373
+ now = monotonic_time
374
+ retry_ready = !@retry_deadline.nil? && now >= @retry_deadline
375
+ interval_ready = !@next_interval_deadline.nil? && now >= @next_interval_deadline
376
+ if @state != "paused" && (@wake_requested || retry_ready || interval_ready)
377
+ @wake_requested = false
378
+ @retry_deadline = nil
379
+ @retry_delay = nil if retry_ready
380
+ @next_interval_deadline = nil
381
+ @state = retry_ready ? "retrying" : "running"
382
+ @in_flight = true
383
+ return @generation
384
+ end
385
+
386
+ deadline = [@retry_deadline, @next_interval_deadline].compact.min
387
+ timeout = deadline.nil? ? nil : [deadline - now, 0].max
388
+ @condition.wait(@mutex, timeout)
389
+ end
390
+ end
391
+ end
392
+
393
+ def record_success(response)
394
+ @consecutive_failures = 0
395
+ @pause_reason = nil
396
+ @retry_delay = nil
397
+ @retry_deadline = nil
398
+ @last_outcome = response.batches.zero? ? "empty" : "accepted"
399
+ @successful_flushes = increment(@successful_flushes) if response.batches.positive?
400
+ @state = "idle"
401
+ end
402
+
403
+ def record_failure(error)
404
+ @consecutive_failures = increment(@consecutive_failures)
405
+ @wake_requested = false
406
+ @next_interval_deadline = nil
407
+ if retryable_failure?(error)
408
+ @pause_reason = nil
409
+ @retry_delay = bounded_retry_delay
410
+ @retry_deadline = monotonic_time + @retry_delay
411
+ @state = "retrying"
412
+ @condition.signal
413
+ else
414
+ @pause_reason = pause_reason(error)
415
+ @retry_delay = nil
416
+ @retry_deadline = nil
417
+ @state = "paused"
418
+ end
419
+ end
420
+
421
+ def schedule_pending_work
422
+ if @observed_queue_count.zero?
423
+ @wake_requested = false
424
+ @next_interval_deadline = nil
425
+ elsif @observed_queue_count >= @flush_threshold
426
+ @wake_requested = true
427
+ @next_interval_deadline = nil
428
+ @condition.signal
429
+ else
430
+ @wake_requested = false
431
+ @next_interval_deadline = monotonic_time + @flush_interval
432
+ @condition.signal
433
+ end
434
+ end
435
+
436
+ def update_queue_count(queue_metrics)
437
+ @observed_queue_count = queue_metrics.fetch(:queued_events)
438
+ end
439
+
440
+ def live_queue_metrics
441
+ @client.send(:queue_metrics)
442
+ end
443
+
444
+ def retryable_failure?(error)
445
+ error.respond_to?(:automatic_retryable?) && error.automatic_retryable?
446
+ end
447
+
448
+ def pause_reason(error)
449
+ reason = error.respond_to?(:automatic_pause_reason) ? error.automatic_pause_reason : nil
450
+ return reason if PAUSE_REASONS.include?(reason)
451
+ return "authentication" if error.is_a?(SdkError) && error.code == "unauthenticated"
452
+ return "quota" if error.is_a?(SdkError) && error.code == "rate_limited"
453
+ return "validation" if error.is_a?(SdkError) && error.code == "validation_error"
454
+
455
+ "nonretryable"
456
+ end
457
+
458
+ def bounded_retry_delay
459
+ exponent = [[@consecutive_failures - 1, 0].max, 30].min
460
+ cap = [@retry_base_delay * (2**exponent), @retry_max_delay].min
461
+ half = cap / 2.0
462
+ half + (Random.rand * half)
463
+ end
464
+
465
+ def bounded_milliseconds(seconds)
466
+ [[(seconds * 1000).ceil, 0].max, (COUNTER_MAX)].min
467
+ end
468
+
469
+ def increment(value)
470
+ value >= COUNTER_MAX ? COUNTER_MAX : value + 1
471
+ end
472
+
473
+ def join_worker(worker)
474
+ return if worker.nil? || worker == Thread.current
475
+
476
+ worker.join
477
+ end
478
+
479
+ def current_process_id
480
+ process_id = Process.pid
481
+ unless process_id.is_a?(Integer) && process_id.positive?
482
+ raise SdkError.new("process_ownership_error", "automatic delivery process identity is unavailable")
483
+ end
484
+
485
+ process_id
486
+ end
487
+
488
+ def monotonic_time
489
+ Process.clock_gettime(Process::CLOCK_MONOTONIC)
490
+ end
491
+ end
492
+ private_constant :AutomaticDelivery
493
+ end
@@ -0,0 +1,196 @@
1
+ # frozen_string_literal: true
2
+
3
+ module LogBrew
4
+ class DroppedEvent
5
+ attr_reader :event_id, :event_type, :reason, :dropped_events,
6
+ :pending_events, :pending_event_bytes
7
+
8
+ def initialize(event_id:, event_type:, reason:, dropped_events:, pending_events:, pending_event_bytes:)
9
+ @event_id = event_id.dup.freeze
10
+ @event_type = event_type.dup.freeze
11
+ @reason = reason.dup.freeze
12
+ @dropped_events = dropped_events
13
+ @pending_events = pending_events
14
+ @pending_event_bytes = pending_event_bytes
15
+ freeze
16
+ end
17
+ end
18
+
19
+ class BoundedEventQueue
20
+ DEFAULT_MAX_SIZE = 1_000
21
+ DEFAULT_MAX_BYTES = 4_194_304
22
+ CALLBACK_GUARD_KEY = :logbrew_drop_callback_guards
23
+
24
+ EventRecord = Struct.new(:json, :bytes, :storage_record)
25
+ Snapshot = Struct.new(:records, :event_count, :event_bytes) do
26
+ def events
27
+ records.map { |record| JSON.parse(record.json) }
28
+ end
29
+
30
+ def serialized_events
31
+ records.map(&:json)
32
+ end
33
+ end
34
+ private_constant :EventRecord
35
+ private_constant :Snapshot
36
+
37
+ def initialize(max_size:, max_bytes:, max_event_bytes: max_bytes, on_event_dropped:, event_store: nil)
38
+ validate_positive_integer("max_queue_size", max_size)
39
+ validate_positive_integer("max_queue_bytes", max_bytes)
40
+ validate_positive_integer("max_event_bytes", max_event_bytes)
41
+ unless on_event_dropped.nil? || on_event_dropped.respond_to?(:call)
42
+ raise SdkError.new("validation_error", "on_event_dropped must respond to call")
43
+ end
44
+
45
+ @max_size = max_size
46
+ @max_bytes = max_bytes
47
+ @max_event_bytes = max_event_bytes
48
+ @on_event_dropped = on_event_dropped
49
+ @event_store = event_store
50
+ stored_records = event_store.nil? ? [] : event_store.records
51
+ @records = stored_records.map do |record|
52
+ EventRecord.new(record.json, record.bytes, record).freeze
53
+ end
54
+ @pending_bytes = @records.sum(&:bytes)
55
+ if @records.length > @max_size || @pending_bytes > @max_bytes || @records.any? { |record| record.bytes > @max_event_bytes }
56
+ raise SdkError.new("persistent_queue_error", "persistent queue exceeds configured bounds")
57
+ end
58
+ @dropped_events = 0
59
+ @mutex = Mutex.new
60
+ end
61
+
62
+ def enqueue(event_id:, event_type:, event:)
63
+ @mutex.synchronize do
64
+ return record_drop(event_id, event_type, "queue_overflow") if @records.length >= @max_size
65
+
66
+ serialized = serialize_event(event)
67
+ event_bytes = serialized.bytesize
68
+ return record_drop(event_id, event_type, "event_too_large") if event_bytes > @max_bytes || event_bytes > @max_event_bytes
69
+ return record_drop(event_id, event_type, "queue_overflow") if @pending_bytes + event_bytes > @max_bytes
70
+
71
+ storage_record = begin
72
+ @event_store&.append(serialized)
73
+ rescue SdkError => error
74
+ raise error if %w[process_ownership_error persistence_commit_error].include?(error.code)
75
+
76
+ return record_drop(event_id, event_type, "persistence_failure")
77
+ end
78
+
79
+ @records << EventRecord.new(serialized, event_bytes, storage_record).freeze
80
+ @pending_bytes += event_bytes
81
+ unless storage_record.nil? || storage_record.durable
82
+ raise SdkError.new("persistence_commit_error", "persistent queue admission durability is unconfirmed")
83
+ end
84
+
85
+ nil
86
+ end
87
+ end
88
+
89
+ def notify_drop(notice)
90
+ return if notice.nil? || @on_event_dropped.nil?
91
+
92
+ guards = Thread.current.thread_variable_get(CALLBACK_GUARD_KEY)
93
+ unless guards
94
+ guards = {}
95
+ Thread.current.thread_variable_set(CALLBACK_GUARD_KEY, guards)
96
+ end
97
+ return if guards[object_id]
98
+
99
+ guards[object_id] = true
100
+ begin
101
+ @on_event_dropped.call(notice)
102
+ rescue StandardError
103
+ nil
104
+ ensure
105
+ guards.delete(object_id)
106
+ Thread.current.thread_variable_set(CALLBACK_GUARD_KEY, nil) if guards.empty?
107
+ end
108
+ end
109
+
110
+ def length
111
+ @mutex.synchronize { @records.length }
112
+ end
113
+
114
+ def pending_bytes
115
+ @mutex.synchronize { @pending_bytes }
116
+ end
117
+
118
+ def dropped_events
119
+ @mutex.synchronize { @dropped_events }
120
+ end
121
+
122
+ def metrics
123
+ @mutex.synchronize do
124
+ {
125
+ queued_events: @records.length,
126
+ queued_bytes: @pending_bytes,
127
+ dropped_events: @dropped_events
128
+ }.freeze
129
+ end
130
+ end
131
+
132
+ def snapshot
133
+ @mutex.synchronize do
134
+ @event_store&.prepare_delivery unless @records.empty?
135
+ Snapshot.new(@records.dup.freeze, @records.length, @pending_bytes).freeze
136
+ end
137
+ end
138
+
139
+ def acknowledge(snapshot)
140
+ return nil if snapshot.event_count.zero?
141
+
142
+ @mutex.synchronize do
143
+ removed = @records.first(snapshot.event_count)
144
+ compaction_error = @event_store&.acknowledge(removed.map(&:storage_record))
145
+ @records.shift(snapshot.event_count)
146
+ @pending_bytes -= removed.sum(&:bytes)
147
+ compaction_error
148
+ end
149
+ end
150
+
151
+ def purge
152
+ @mutex.synchronize do
153
+ return 0 if @records.empty?
154
+
155
+ removed = @records.dup
156
+ compaction_error = @event_store&.acknowledge(removed.map(&:storage_record))
157
+ @records.clear
158
+ @pending_bytes = 0
159
+ raise compaction_error unless compaction_error.nil?
160
+
161
+ removed.length
162
+ end
163
+ end
164
+
165
+ def close
166
+ @event_store&.close
167
+ end
168
+
169
+ private
170
+
171
+ def validate_positive_integer(label, value)
172
+ return if value.is_a?(Integer) && value.positive?
173
+
174
+ raise SdkError.new("validation_error", "#{label} must be a positive integer")
175
+ end
176
+
177
+ def serialize_event(event)
178
+ JSON.generate(event).freeze
179
+ rescue JSON::GeneratorError, EncodingError
180
+ raise SdkError.new("validation_error", "event must be JSON serializable")
181
+ end
182
+
183
+ def record_drop(event_id, event_type, reason)
184
+ @dropped_events += 1
185
+ DroppedEvent.new(
186
+ event_id: event_id,
187
+ event_type: event_type,
188
+ reason: reason,
189
+ dropped_events: @dropped_events,
190
+ pending_events: @records.length,
191
+ pending_event_bytes: @pending_bytes
192
+ )
193
+ end
194
+ end
195
+ private_constant :BoundedEventQueue
196
+ end