waterdrop 2.8.14 → 2.8.16

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.
Files changed (60) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/ci.yml +215 -36
  3. data/.github/workflows/push.yml +3 -3
  4. data/.github/workflows/trigger-wiki-refresh.yml +1 -1
  5. data/.github/workflows/verify-action-pins.yml +1 -1
  6. data/.gitignore +0 -1
  7. data/.rubocop.yml +87 -0
  8. data/.ruby-version +1 -1
  9. data/.yard-lint.yml +172 -72
  10. data/CHANGELOG.md +13 -0
  11. data/Gemfile +8 -9
  12. data/Gemfile.lint +14 -0
  13. data/Gemfile.lint.lock +123 -0
  14. data/Gemfile.lock +27 -28
  15. data/README.md +1 -1
  16. data/Rakefile +2 -2
  17. data/bin/integrations +28 -29
  18. data/bin/verify_topics_naming +8 -8
  19. data/config/locales/errors.yml +12 -0
  20. data/docker-compose.oauth.yml +56 -0
  21. data/docker-compose.yml +1 -1
  22. data/lib/waterdrop/clients/dummy.rb +9 -0
  23. data/lib/waterdrop/clients/rdkafka.rb +13 -2
  24. data/lib/waterdrop/config.rb +32 -5
  25. data/lib/waterdrop/connection_pool.rb +13 -11
  26. data/lib/waterdrop/contracts/config.rb +30 -6
  27. data/lib/waterdrop/contracts/message.rb +2 -2
  28. data/lib/waterdrop/contracts/poller_config.rb +26 -0
  29. data/lib/waterdrop/contracts/transactional_offset.rb +2 -2
  30. data/lib/waterdrop/contracts/variant.rb +18 -18
  31. data/lib/waterdrop/errors.rb +3 -0
  32. data/lib/waterdrop/instrumentation/callbacks/delivery.rb +8 -8
  33. data/lib/waterdrop/instrumentation/callbacks/error.rb +5 -5
  34. data/lib/waterdrop/instrumentation/callbacks/oauthbearer_token_refresh.rb +4 -4
  35. data/lib/waterdrop/instrumentation/callbacks/statistics.rb +18 -5
  36. data/lib/waterdrop/instrumentation/idle_disconnector_listener.rb +4 -4
  37. data/lib/waterdrop/instrumentation/logger_listener.rb +10 -10
  38. data/lib/waterdrop/instrumentation/notifications.rb +3 -0
  39. data/lib/waterdrop/instrumentation/vendors/datadog/metrics_listener.rb +19 -19
  40. data/lib/waterdrop/polling/config.rb +52 -0
  41. data/lib/waterdrop/polling/latch.rb +49 -0
  42. data/lib/waterdrop/polling/poller.rb +415 -0
  43. data/lib/waterdrop/polling/queue_pipe.rb +63 -0
  44. data/lib/waterdrop/polling/state.rb +151 -0
  45. data/lib/waterdrop/polling.rb +22 -0
  46. data/lib/waterdrop/producer/async.rb +6 -6
  47. data/lib/waterdrop/producer/buffer.rb +8 -8
  48. data/lib/waterdrop/producer/idempotence.rb +3 -3
  49. data/lib/waterdrop/producer/sync.rb +15 -8
  50. data/lib/waterdrop/producer/testing.rb +1 -1
  51. data/lib/waterdrop/producer/transactions.rb +6 -6
  52. data/lib/waterdrop/producer.rb +113 -30
  53. data/lib/waterdrop/version.rb +1 -1
  54. data/lib/waterdrop.rb +15 -10
  55. data/package-lock.json +331 -0
  56. data/package.json +9 -0
  57. data/renovate.json +25 -6
  58. data/waterdrop.gemspec +23 -23
  59. metadata +17 -5
  60. data/.coditsu/ci.yml +0 -3
@@ -13,7 +13,7 @@ module WaterDrop
13
13
  ensure_active!
14
14
 
15
15
  @monitor.instrument(
16
- 'message.buffered',
16
+ "message.buffered",
17
17
  producer_id: id,
18
18
  message: message,
19
19
  buffer: @messages
@@ -30,7 +30,7 @@ module WaterDrop
30
30
  ensure_active!
31
31
 
32
32
  @monitor.instrument(
33
- 'messages.buffered',
33
+ "messages.buffered",
34
34
  producer_id: id,
35
35
  messages: messages,
36
36
  buffer: @messages
@@ -45,18 +45,18 @@ module WaterDrop
45
45
  # flushed
46
46
  def flush_async
47
47
  @monitor.instrument(
48
- 'buffer.flushed_async',
48
+ "buffer.flushed_async",
49
49
  producer_id: id,
50
50
  messages: @messages
51
51
  ) { flush(false) }
52
52
  end
53
53
 
54
54
  # Flushes the internal buffer to Kafka in a sync way
55
- # @return [Array<Rdkafka::Producer::DeliveryReport>] delivery reports for messages that were
56
- # flushed
55
+ # @return [Array<Rdkafka::Producer::DeliveryHandle>] delivery handles for messages that were
56
+ # flushed (handles are in final state, call `#create_result` to get delivery report)
57
57
  def flush_sync
58
58
  @monitor.instrument(
59
- 'buffer.flushed_sync',
59
+ "buffer.flushed_sync",
60
60
  producer_id: id,
61
61
  messages: @messages
62
62
  ) { flush(true) }
@@ -66,8 +66,8 @@ module WaterDrop
66
66
 
67
67
  # Method for triggering the buffer
68
68
  # @param sync [Boolean] should it flush in a sync way
69
- # @return [Array<Rdkafka::Producer::DeliveryHandle, Rdkafka::Producer::DeliveryReport>]
70
- # delivery handles for async or delivery reports for sync
69
+ # @return [Array<Rdkafka::Producer::DeliveryHandle>] delivery handles (in final state for
70
+ # sync, pending for async)
71
71
  # @raise [Errors::ProduceManyError] when there was a failure in flushing
72
72
  # @note We use this method underneath to provide a different instrumentation for sync and
73
73
  # async flushing within the public API
@@ -10,7 +10,7 @@ module WaterDrop
10
10
  return true if transactional?
11
11
  return @idempotent unless @idempotent.nil?
12
12
 
13
- @idempotent = config.kafka.to_h.fetch(:'enable.idempotence', false)
13
+ @idempotent = config.kafka.to_h.fetch(:"enable.idempotence", false)
14
14
  end
15
15
 
16
16
  # Checks if the given error should trigger an idempotent producer reload
@@ -61,7 +61,7 @@ module WaterDrop
61
61
  # Users can subscribe to this event and modify event[:caller].config.kafka to change
62
62
  # producer config
63
63
  @monitor.instrument(
64
- 'producer.reload',
64
+ "producer.reload",
65
65
  producer_id: id,
66
66
  error: error,
67
67
  attempt: attempt,
@@ -73,7 +73,7 @@ module WaterDrop
73
73
  @idempotent = nil
74
74
 
75
75
  @monitor.instrument(
76
- 'producer.reloaded',
76
+ "producer.reloaded",
77
77
  producer_id: id,
78
78
  attempt: attempt
79
79
  ) do
@@ -8,7 +8,7 @@ module WaterDrop
8
8
  #
9
9
  # @param message [Hash] hash that complies with the {Contracts::Message} contract
10
10
  #
11
- # @return [Rdkafka::Producer::DeliveryHandle] delivery report
11
+ # @return [Rdkafka::Producer::DeliveryReport] delivery report
12
12
  #
13
13
  # @raise [Rdkafka::RdkafkaError] When adding the message to rdkafka's queue failed
14
14
  # @raise [Rdkafka::Producer::WaitTimeoutError] When the timeout has been reached and the
@@ -20,7 +20,7 @@ module WaterDrop
20
20
  validate_message!(message)
21
21
 
22
22
  @monitor.instrument(
23
- 'message.produced_sync',
23
+ "message.produced_sync",
24
24
  producer_id: id,
25
25
  message: message
26
26
  ) do
@@ -33,11 +33,11 @@ module WaterDrop
33
33
  raise Errors::ProduceError, e.inspect
34
34
  rescue Errors::ProduceError => ex
35
35
  @monitor.instrument(
36
- 'error.occurred',
36
+ "error.occurred",
37
37
  producer_id: id,
38
38
  message: message,
39
39
  error: ex,
40
- type: 'message.produce_sync'
40
+ type: "message.produce_sync"
41
41
  )
42
42
 
43
43
  raise ex
@@ -49,13 +49,20 @@ module WaterDrop
49
49
  # @param messages [Array<Hash>] array with messages that comply with the
50
50
  # {Contracts::Message} contract
51
51
  #
52
- # @return [Array<Rdkafka::Producer::DeliveryHandle>] delivery reports
52
+ # @return [Array<Rdkafka::Producer::DeliveryHandle>] delivery handles for messages that were
53
+ # sent (can be used to verify offset, partition, etc via `#create_result`)
53
54
  #
54
55
  # @raise [Rdkafka::RdkafkaError] When adding the messages to rdkafka's queue failed
55
56
  # @raise [Rdkafka::Producer::WaitTimeoutError] When the timeout has been reached and some
56
57
  # handles are still pending
57
58
  # @raise [Errors::MessageInvalidError] When any of the provided messages details are invalid
58
59
  # and the message could not be sent to Kafka
60
+ #
61
+ # @note We return delivery handles instead of delivery reports to ensure consistent return
62
+ # types for both success and error flows. In case of partial failures (e.g., mid-batch
63
+ # errors), returning handles guarantees the same type in the `dispatched` collection,
64
+ # allowing uniform error handling. Each handle is in its final state after this method
65
+ # returns, so you can call `handle.create_result` to obtain the delivery report if needed.
59
66
  def produce_many_sync(messages)
60
67
  messages = middleware.run_many(messages)
61
68
  messages.each { |message| validate_message!(message) }
@@ -63,7 +70,7 @@ module WaterDrop
63
70
  dispatched = []
64
71
  inline_error = nil
65
72
 
66
- @monitor.instrument('messages.produced_sync', producer_id: id, messages: messages) do
73
+ @monitor.instrument("messages.produced_sync", producer_id: id, messages: messages) do
67
74
  # While most of the librdkafka errors are async and not inline, there are some like
68
75
  # buffer overflow that can leak in during the `#produce` itself. When this happens, we
69
76
  # still (since it's a sync mode) need to wait on deliveries of things that were
@@ -99,7 +106,7 @@ module WaterDrop
99
106
  re_raised = Errors::ProduceManyError.new(dispatched, e.inspect)
100
107
 
101
108
  @monitor.instrument(
102
- 'error.occurred',
109
+ "error.occurred",
103
110
  producer_id: id,
104
111
  messages: messages,
105
112
  # If it is a transactional producer nothing was successfully dispatched on error, thus
@@ -108,7 +115,7 @@ module WaterDrop
108
115
  # isolation level.
109
116
  dispatched: transactional? ? EMPTY_ARRAY : dispatched,
110
117
  error: re_raised,
111
- type: 'messages.produce_many_sync'
118
+ type: "messages.produce_many_sync"
112
119
  )
113
120
 
114
121
  raise re_raised
@@ -106,7 +106,7 @@ module WaterDrop
106
106
  return if client.respond_to?(:trigger_test_fatal_error)
107
107
 
108
108
  # Require the rdkafka testing module if not already loaded
109
- require 'rdkafka/producer/testing' unless defined?(::Rdkafka::Testing)
109
+ require "rdkafka/producer/testing" unless defined?(::Rdkafka::Testing)
110
110
 
111
111
  client.singleton_class.include(::Rdkafka::Testing)
112
112
  end
@@ -80,7 +80,7 @@ module WaterDrop
80
80
  if !e && !finished
81
81
  raise(
82
82
  Errors::EarlyTransactionExitNotAllowedError,
83
- <<~ERROR_MSG.tr("\n", ' ')
83
+ <<~ERROR_MSG.tr("\n", " ")
84
84
  Using `return`, `break` or `throw` to exit a transaction block is not allowed.
85
85
  If the `throw` came from `Timeout.timeout(duration)`, pass an exception class as
86
86
  a second argument so it doesn't use `throw` to abort its block.
@@ -110,7 +110,7 @@ module WaterDrop
110
110
  client.abort_transaction
111
111
  end
112
112
  end
113
- rescue StandardError => e
113
+ rescue => e
114
114
  # If something from rdkafka leaks here, it means there was a non-retryable error that
115
115
  # bubbled up. In such cases if we should, we do reload the underling client
116
116
  transactional_reload_client_if_needed(e)
@@ -134,7 +134,7 @@ module WaterDrop
134
134
  def transactional?
135
135
  return @transactional unless @transactional.nil?
136
136
 
137
- @transactional = config.kafka.to_h.key?(:'transactional.id')
137
+ @transactional = config.kafka.to_h.key?(:"transactional.id")
138
138
  end
139
139
 
140
140
  # Checks if we can still retry reloading after a transactional fatal error
@@ -229,7 +229,7 @@ module WaterDrop
229
229
  do_retry = e.retryable? && attempt < config.max_attempts_on_transaction_command
230
230
 
231
231
  @monitor.instrument(
232
- 'error.occurred',
232
+ "error.occurred",
233
233
  producer_id: id,
234
234
  caller: self,
235
235
  error: e,
@@ -299,7 +299,7 @@ module WaterDrop
299
299
  # Users can subscribe to this event and modify event[:caller].config.kafka to change
300
300
  # producer config. This is useful for escaping fencing loops by changing transactional.id
301
301
  @monitor.instrument(
302
- 'producer.reload',
302
+ "producer.reload",
303
303
  producer_id: id,
304
304
  error: rd_error,
305
305
  attempt: @transaction_fatal_error_attempts,
@@ -311,7 +311,7 @@ module WaterDrop
311
311
  @transactional = nil
312
312
 
313
313
  @monitor.instrument(
314
- 'producer.reloaded',
314
+ "producer.reloaded",
315
315
  producer_id: id,
316
316
  attempt: @transaction_fatal_error_attempts
317
317
  ) do
@@ -62,6 +62,8 @@ module WaterDrop
62
62
  @closing_thread_id = nil
63
63
  @idempotent = nil
64
64
  @transactional = nil
65
+ @fd_polling = nil
66
+ @poller = nil
65
67
  @idempotent_fatal_error_attempts = 0
66
68
  @transaction_fatal_error_attempts = 0
67
69
 
@@ -70,7 +72,7 @@ module WaterDrop
70
72
 
71
73
  # Instrument producer creation for global listeners
72
74
  class_monitor.instrument(
73
- 'producer.created',
75
+ "producer.created",
74
76
  producer: self,
75
77
  producer_id: @id
76
78
  )
@@ -85,9 +87,9 @@ module WaterDrop
85
87
  raise Errors::ProducerAlreadyConfiguredError, id unless @status.initial?
86
88
 
87
89
  @config = Config
88
- .new
89
- .setup(...)
90
- .config
90
+ .new
91
+ .setup(...)
92
+ .config
91
93
 
92
94
  @id = @config.id
93
95
  @monitor = @config.monitor
@@ -99,7 +101,7 @@ module WaterDrop
99
101
 
100
102
  # Instrument producer configuration for global listeners
101
103
  class_monitor.instrument(
102
- 'producer.configured',
104
+ "producer.configured",
103
105
  producer: self,
104
106
  producer_id: @id,
105
107
  config: @config
@@ -121,7 +123,7 @@ module WaterDrop
121
123
 
122
124
  # Instrument producer configuration for global listeners
123
125
  class_monitor.instrument(
124
- 'producer.configured',
126
+ "producer.configured",
125
127
  producer: self,
126
128
  producer_id: @id,
127
129
  config: @config
@@ -166,12 +168,47 @@ module WaterDrop
166
168
  @client = Builder.new.call(self, @config)
167
169
 
168
170
  @status.connected!
169
- @monitor.instrument('producer.connected', producer_id: id)
171
+ @monitor.instrument("producer.connected", producer_id: id)
170
172
  end
171
173
 
172
174
  @client
173
175
  end
174
176
 
177
+ # Returns the number of messages in the librdkafka producer queue.
178
+ #
179
+ # This count includes:
180
+ # - Messages waiting to be sent to the broker
181
+ # - Messages currently in-flight (being transmitted)
182
+ # - Delivery reports waiting to be processed
183
+ #
184
+ # @return [Integer] the number of pending messages in the rdkafka queue, or 0 if the
185
+ # producer is not connected
186
+ #
187
+ # @note This only counts messages in the rdkafka queue, not the internal WaterDrop buffer.
188
+ # To get the internal buffer count, use `messages.size`.
189
+ #
190
+ # @note Returns 0 when the producer is not connected as there cannot be any pending
191
+ # messages if we haven't connected.
192
+ #
193
+ # @example Check pending messages
194
+ # producer.queue_size #=> 42
195
+ #
196
+ # @example Check total pending work (buffer + rdkafka queue)
197
+ # internal_buffer = producer.messages.size
198
+ # rdkafka_queue = producer.queue_size
199
+ # total_pending = internal_buffer + rdkafka_queue
200
+ def queue_size
201
+ return 0 unless @status.connected?
202
+
203
+ @connecting_mutex.synchronize do
204
+ return 0 unless @client
205
+
206
+ @client.queue_size
207
+ end
208
+ end
209
+
210
+ alias_method :queue_length, :queue_size
211
+
175
212
  # Fetches and caches the partition count of a topic
176
213
  #
177
214
  # @param topic [String] topic for which we want to get the number of partitions
@@ -189,7 +226,7 @@ module WaterDrop
189
226
  # purge the internal WaterDrop buffer but will also purge the librdkafka queue as well as
190
227
  # will cancel any outgoing messages dispatches.
191
228
  def purge
192
- @monitor.instrument('buffer.purged', producer_id: id) do
229
+ @monitor.instrument("buffer.purged", producer_id: id) do
193
230
  @buffer_mutex.synchronize do
194
231
  @messages = []
195
232
  end
@@ -218,7 +255,7 @@ module WaterDrop
218
255
  Variant.new(self, **args)
219
256
  end
220
257
 
221
- alias variant with
258
+ alias_method :variant, :with
222
259
 
223
260
  # Returns and caches the middleware object that may be used
224
261
  # @return [WaterDrop::Producer::Middleware]
@@ -261,9 +298,12 @@ module WaterDrop
261
298
  return false unless @operations_in_progress.value.zero?
262
299
 
263
300
  @status.disconnecting!
264
- @monitor.instrument('producer.disconnecting', producer_id: id)
301
+ @monitor.instrument("producer.disconnecting", producer_id: id)
302
+
303
+ @monitor.instrument("producer.disconnected", producer_id: id) do
304
+ # Unregister from poller before closing if fiber polling is enabled
305
+ unregister_from_poller
265
306
 
266
- @monitor.instrument('producer.disconnected', producer_id: id) do
267
307
  # Close the client
268
308
  @client.close
269
309
  @client = nil
@@ -298,6 +338,16 @@ module WaterDrop
298
338
  # @param force [Boolean] should we force closing even with outstanding messages after the
299
339
  # max wait timeout
300
340
  def close(force: false)
341
+ # When closing from within the FD poller thread (e.g., from a callback like
342
+ # message.acknowledged or error.occurred), we must delegate to a background thread.
343
+ # Close performs flush which waits for delivery reports, but delivery reports require
344
+ # the poller to poll. Since we're ON the poller thread inside a callback, this would
345
+ # deadlock. Spawning a thread allows the callback to return, letting the poller continue.
346
+ if fd_polling? && poller.in_poller_thread?
347
+ Thread.new { close(force: force) }
348
+ return
349
+ end
350
+
301
351
  # If we already own the transactional mutex, it means we are inside of a transaction and
302
352
  # it should not we allowed to close the producer in such a case.
303
353
  if @transaction_mutex.locked? && @transaction_mutex.owned?
@@ -311,11 +361,11 @@ module WaterDrop
311
361
  return unless @status.active?
312
362
 
313
363
  @monitor.instrument(
314
- 'producer.closed',
364
+ "producer.closed",
315
365
  producer_id: id
316
366
  ) do
317
367
  @status.closing!
318
- @monitor.instrument('producer.closing', producer_id: id)
368
+ @monitor.instrument("producer.closing", producer_id: id)
319
369
 
320
370
  # No need for auto-gc if everything got closed by us
321
371
  # This should be used only in case a producer was not closed properly and forgotten
@@ -355,6 +405,9 @@ module WaterDrop
355
405
  purge if force
356
406
  end
357
407
 
408
+ # Unregister from poller before closing if fiber polling is enabled
409
+ unregister_from_poller
410
+
358
411
  @client.close
359
412
 
360
413
  @client = nil
@@ -391,20 +444,38 @@ module WaterDrop
391
444
  @buffer_mutex.unlock
392
445
  end
393
446
  else
394
- parts << 'buffer_size=busy'
447
+ parts << "buffer_size=busy"
395
448
  end
396
449
 
397
450
  # Check if client is connected without triggering connection
398
451
  parts << if @status.connected?
399
- 'connected=true'
400
- else
401
- 'connected=false'
402
- end
452
+ "connected=true"
453
+ else
454
+ "connected=false"
455
+ end
403
456
 
404
457
  parts << "operations=#{@operations_in_progress.value}"
405
- parts << 'in_transaction=true' if @transaction_mutex.locked?
458
+ parts << "in_transaction=true" if @transaction_mutex.locked?
459
+
460
+ "#<#{self.class.name}:#{format("%#x", object_id)} #{parts.join(" ")}>"
461
+ end
462
+
463
+ # @return [Boolean] true if FD-based polling mode is enabled
464
+ def fd_polling?
465
+ return @fd_polling unless @fd_polling.nil?
466
+ return false unless config
467
+
468
+ @fd_polling = config.polling.mode == :fd
469
+ end
470
+
471
+ # Returns the poller instance for this producer
472
+ # @return [WaterDrop::Polling::Poller] custom poller if configured, otherwise the global
473
+ # singleton poller
474
+ def poller
475
+ return @poller unless @poller.nil?
476
+ return nil unless config
406
477
 
407
- "#<#{self.class.name}:#{format('%#x', object_id)} #{parts.join(' ')}>"
478
+ @poller = config.polling.poller || Polling::Poller.instance
408
479
  end
409
480
 
410
481
  private
@@ -438,8 +509,7 @@ module WaterDrop
438
509
  # final result and it is an error.
439
510
  def wait(handler, raise_response_error: true)
440
511
  handler.wait(
441
- # rdkafka max_wait_timeout is in seconds and we use ms
442
- max_wait_timeout: current_variant.max_wait_timeout / 1_000.0,
512
+ max_wait_timeout_ms: current_variant.max_wait_timeout,
443
513
  raise_response_error: raise_response_error
444
514
  )
445
515
  end
@@ -479,10 +549,10 @@ module WaterDrop
479
549
  end
480
550
 
481
551
  result = if transactional?
482
- transaction { client.produce(**message) }
483
- else
484
- client.produce(**message)
485
- end
552
+ transaction { client.produce(**message) }
553
+ else
554
+ client.produce(**message)
555
+ end
486
556
 
487
557
  # Reset attempts counter on successful produce
488
558
  @idempotent_fatal_error_attempts = 0
@@ -499,10 +569,10 @@ module WaterDrop
499
569
 
500
570
  # Instrument error.occurred before attempting reload for visibility
501
571
  @monitor.instrument(
502
- 'error.occurred',
572
+ "error.occurred",
503
573
  producer_id: id,
504
574
  error: e,
505
- type: 'librdkafka.idempotent_fatal_error',
575
+ type: "librdkafka.idempotent_fatal_error",
506
576
  attempt: @idempotent_fatal_error_attempts
507
577
  )
508
578
 
@@ -526,7 +596,7 @@ module WaterDrop
526
596
  # in an infinite loop, effectively hanging the processing
527
597
  raise unless monotonic_now - produce_time < @config.wait_timeout_on_queue_full
528
598
 
529
- label = caller_locations(2, 1)[0].label.split.last.split('#').last
599
+ label = caller_locations(2, 1)[0].label.split.last.split("#").last
530
600
 
531
601
  # We use this syntax here because we want to preserve the original `#cause` when we
532
602
  # instrument the error and there is no way to manually assign `#cause` value. We want to keep
@@ -546,7 +616,7 @@ module WaterDrop
546
616
  # If this type of event happens too often, it may indicate that the buffer settings are
547
617
  # not well configured.
548
618
  @monitor.instrument(
549
- 'error.occurred',
619
+ "error.occurred",
550
620
  producer_id: id,
551
621
  message: message,
552
622
  error: e,
@@ -570,9 +640,22 @@ module WaterDrop
570
640
  def reload!
571
641
  @client.flush(current_variant.max_wait_timeout)
572
642
  purge
643
+ # Unregister from poller before closing if fiber polling is enabled
644
+ unregister_from_poller
573
645
  @client.close
574
646
  @client = nil
575
647
  @status.configured!
576
648
  end
649
+
650
+ # Unregisters this producer from its poller
651
+ #
652
+ # @note We only unregister when fd_polling? is true because thread-mode producers never
653
+ # register with the Poller. The Poller.unregister method handles unregistered producers
654
+ # gracefully, but this guard avoids making unnecessary unregister calls in thread mode.
655
+ def unregister_from_poller
656
+ return unless fd_polling?
657
+
658
+ poller.unregister(self)
659
+ end
577
660
  end
578
661
  end
@@ -3,5 +3,5 @@
3
3
  # WaterDrop library
4
4
  module WaterDrop
5
5
  # Current WaterDrop version
6
- VERSION = '2.8.14'
6
+ VERSION = "2.8.16"
7
7
  end
data/lib/waterdrop.rb CHANGED
@@ -1,20 +1,21 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  # External components
4
- require 'delegate'
5
- require 'forwardable'
6
- require 'json'
7
- require 'zeitwerk'
8
- require 'securerandom'
9
- require 'karafka-core'
10
- require 'pathname'
4
+ require "delegate"
5
+ require "forwardable"
6
+ require "json"
7
+ require "zeitwerk"
8
+ require "securerandom"
9
+ require "singleton"
10
+ require "karafka-core"
11
+ require "pathname"
11
12
 
12
13
  # WaterDrop library
13
14
  module WaterDrop
14
15
  class << self
15
16
  # @return [String] root path of this gem
16
17
  def gem_root
17
- Pathname.new(File.expand_path('..', __dir__))
18
+ Pathname.new(File.expand_path("..", __dir__))
18
19
  end
19
20
 
20
21
  # @return [WaterDrop::Instrumentation::ClassMonitor] global monitor for
@@ -34,15 +35,19 @@ module WaterDrop
34
35
  end
35
36
 
36
37
  # @deprecated Use #monitor instead. This method is provided for backward compatibility only.
37
- alias instrumentation monitor
38
+ alias_method :instrumentation, :monitor
38
39
  end
39
40
  end
40
41
 
41
42
  loader = Zeitwerk::Loader.for_gem
42
- loader.inflector.inflect('waterdrop' => 'WaterDrop')
43
+ loader.inflector.inflect("waterdrop" => "WaterDrop")
43
44
  # Do not load vendors instrumentation components. Those need to be required manually if needed
44
45
  loader.ignore("#{__dir__}/waterdrop/instrumentation/vendors/**/*.rb")
45
46
  # Do not load testing components. Those need to be required manually in test environments
46
47
  loader.ignore("#{__dir__}/waterdrop/producer/testing.rb")
47
48
  loader.setup
48
49
  loader.eager_load
50
+
51
+ # Setup the poller. Even if users do not use it, it is few objects and we prevent any potential
52
+ # race conditions later
53
+ WaterDrop::Polling::Poller.instance