karafka-rdkafka 0.31.0-aarch64-linux-gnu → 0.32.0-aarch64-linux-gnu

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 (40) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +83 -35
  3. data/README.md +2 -1
  4. data/ext/librdkafka.so +0 -0
  5. data/karafka-rdkafka.gemspec +4 -3
  6. data/lib/rdkafka/admin/acl_binding_result.rb +4 -16
  7. data/lib/rdkafka/admin/alter_consumer_group_offsets_report.rb +2 -2
  8. data/lib/rdkafka/admin/create_acl_report.rb +1 -3
  9. data/lib/rdkafka/admin/create_topic_report.rb +2 -6
  10. data/lib/rdkafka/admin/delete_consumer_group_offsets_report.rb +2 -2
  11. data/lib/rdkafka/admin/delete_groups_report.rb +2 -6
  12. data/lib/rdkafka/admin/delete_topic_report.rb +2 -6
  13. data/lib/rdkafka/admin/describe_cluster_report.rb +3 -0
  14. data/lib/rdkafka/admin/list_consumer_group_offsets_handle.rb +31 -0
  15. data/lib/rdkafka/admin/list_consumer_group_offsets_report.rb +52 -0
  16. data/lib/rdkafka/admin.rb +180 -10
  17. data/lib/rdkafka/bindings/admin/configs.rb +1 -1
  18. data/lib/rdkafka/bindings/admin.rb +14 -0
  19. data/lib/rdkafka/bindings/callbacks.rb +13 -6
  20. data/lib/rdkafka/bindings/configuration.rb +3 -3
  21. data/lib/rdkafka/bindings/consumer.rb +44 -2
  22. data/lib/rdkafka/bindings/errors.rb +1 -1
  23. data/lib/rdkafka/bindings/handle.rb +1 -1
  24. data/lib/rdkafka/bindings.rb +3 -0
  25. data/lib/rdkafka/callbacks/list_consumer_group_offsets_handler.rb +62 -0
  26. data/lib/rdkafka/callbacks.rb +5 -6
  27. data/lib/rdkafka/config.rb +70 -0
  28. data/lib/rdkafka/consumer/allocator.rb +0 -2
  29. data/lib/rdkafka/consumer/topic_partition_list.rb +7 -7
  30. data/lib/rdkafka/consumer.rb +76 -36
  31. data/lib/rdkafka/defaults.rb +6 -4
  32. data/lib/rdkafka/native_kafka.rb +45 -31
  33. data/lib/rdkafka/producer/partitions_count_cache.rb +109 -26
  34. data/lib/rdkafka/producer.rb +141 -71
  35. data/lib/rdkafka/share_consumer.rb +64 -34
  36. data/lib/rdkafka/version.rb +1 -1
  37. data/lib/rdkafka.rb +4 -1
  38. data/package-lock.json +3 -3
  39. data/renovate.json +7 -0
  40. metadata +5 -30
@@ -156,6 +156,25 @@ module Rdkafka
156
156
  )
157
157
  end
158
158
 
159
+ # Returns the time-to-live of the process-wide partitions count cache used by
160
+ # `produce(partition_key:)`.
161
+ #
162
+ # @return [Integer] time-to-live in milliseconds
163
+ def self.partition_count_cache_ttl_ms
164
+ Rdkafka::Producer.partitions_count_cache.ttl_ms
165
+ end
166
+
167
+ # Sets the time-to-live of the process-wide partitions count cache shared by all the producers.
168
+ # Cached counts are kept, so it can be changed at any time.
169
+ #
170
+ # @param value [Integer] time-to-live in milliseconds
171
+ # (defaults to {Defaults::PARTITIONS_COUNT_CACHE_TTL_MS})
172
+ # @return [void]
173
+ # @raise [ArgumentError] when the value is not a positive Integer
174
+ def self.partition_count_cache_ttl_ms=(value)
175
+ Rdkafka::Producer.partitions_count_cache.ttl_ms = value
176
+ end
177
+
159
178
  # @private
160
179
  def self.opaques
161
180
  @@opaques
@@ -180,6 +199,7 @@ module Rdkafka
180
199
 
181
200
  @config_hash = DEFAULT_CONFIG.merge(config_hash)
182
201
  @consumer_rebalance_listener = nil
202
+ @consumer_offset_commit_listener = nil
183
203
  @consumer_poll_set = true
184
204
  end
185
205
 
@@ -207,6 +227,20 @@ module Rdkafka
207
227
  # @return [Object, #on_partitions_assigned, #on_partitions_revoked] listener instance
208
228
  attr_writer :consumer_rebalance_listener
209
229
 
230
+ # Get the result of every offset commit, including async commits that the broker rejects
231
+ #
232
+ # `#on_offsets_committed` gets an {RdkafkaError} (nil on success) and the
233
+ # {Consumer::TopicPartitionList} of the commit, whose partitions carry their own errors. It
234
+ # runs inside {Consumer#poll} (or {Consumer#events_poll} when `consumer_poll_set` is false)
235
+ # and inside {Consumer#close}, on the thread that calls them.
236
+ #
237
+ # `#on_offsets_commit_failed` gets the same arguments, but only for a failed commit: not on
238
+ # success and not on `no_offset` (nothing new to commit). A listener with only this method
239
+ # costs nothing for the other commits.
240
+ #
241
+ # @return [Object, #on_offsets_committed, #on_offsets_commit_failed] listener instance
242
+ attr_writer :consumer_offset_commit_listener
243
+
210
244
  # Should we use a single queue for the underlying consumer and events.
211
245
  #
212
246
  # This is an advanced API that allows for more granular control of the polling process.
@@ -243,6 +277,11 @@ module Rdkafka
243
277
  Rdkafka::Bindings.rd_kafka_conf_set_rebalance_cb(config, Rdkafka::Bindings::RebalanceCallback)
244
278
  end
245
279
 
280
+ if @consumer_offset_commit_listener
281
+ opaque.consumer_offset_commit_listener = @consumer_offset_commit_listener
282
+ Rdkafka::Bindings.rd_kafka_conf_set_offset_commit_cb(config, Rdkafka::Bindings::OffsetCommitCallback)
283
+ end
284
+
246
285
  build_native_client(
247
286
  config,
248
287
  :rd_kafka_consumer,
@@ -598,6 +637,7 @@ module Rdkafka
598
637
  class Opaque
599
638
  attr_accessor :producer
600
639
  attr_accessor :consumer_rebalance_listener
640
+ attr_accessor :consumer_offset_commit_listener
601
641
  # Backreference set for share consumers so conf-level callbacks (which receive the opaque)
602
642
  # can hand instance-scoped data - currently the librdkafka client name - back to the
603
643
  # ShareConsumer, which has no native accessor for it
@@ -635,5 +675,35 @@ module Rdkafka
635
675
 
636
676
  consumer_rebalance_listener.on_partitions_revoked(list)
637
677
  end
678
+
679
+ # Invokes the on_offsets_committed callback on the offset commit listener if set
680
+ # @param error [Rdkafka::RdkafkaError, nil] the commit error or nil on success
681
+ # @param list [Rdkafka::Consumer::TopicPartitionList] the offsets of the commit
682
+ def call_on_offsets_committed(error, list)
683
+ return unless consumer_offset_commit_listener
684
+ return unless consumer_offset_commit_listener.respond_to?(:on_offsets_committed)
685
+
686
+ consumer_offset_commit_listener.on_offsets_committed(error, list)
687
+ end
688
+
689
+ # Invokes the on_offsets_commit_failed callback on the offset commit listener if set
690
+ # @param error [Rdkafka::RdkafkaError] the commit error
691
+ # @param list [Rdkafka::Consumer::TopicPartitionList] the offsets of the commit
692
+ def call_on_offsets_commit_failed(error, list)
693
+ return unless consumer_offset_commit_listener
694
+ return unless consumer_offset_commit_listener.respond_to?(:on_offsets_commit_failed)
695
+
696
+ consumer_offset_commit_listener.on_offsets_commit_failed(error, list)
697
+ end
698
+
699
+ # @return [Boolean] true if the offset commit listener wants the result of every commit
700
+ def offsets_committed_listened?
701
+ consumer_offset_commit_listener.respond_to?(:on_offsets_committed)
702
+ end
703
+
704
+ # @return [Boolean] true if the offset commit listener wants the failed commits
705
+ def offsets_commit_failed_listened?
706
+ consumer_offset_commit_listener.respond_to?(:on_offsets_commit_failed)
707
+ end
638
708
  end
639
709
  end
@@ -28,8 +28,6 @@ module Rdkafka
28
28
  pointer.put_bytes(0, bytes)
29
29
  pointer.put_char(bytes.bytesize, 0) # NUL terminator
30
30
 
31
- pointer.autorelease = false
32
-
33
31
  pointer
34
32
  end
35
33
  end
@@ -38,7 +38,7 @@ module Rdkafka
38
38
  # Calling this method multiple times for the same topic will overwrite the previous
39
39
  # configuraton.
40
40
  #
41
- # @param topic [String] The topic's name
41
+ # @param topic_name [String] The topic's name
42
42
  # @param partitions [Array<Integer>, Range<Integer>, Integer] The topic's partitions or
43
43
  # partition count
44
44
  #
@@ -52,14 +52,14 @@ module Rdkafka
52
52
  #
53
53
  # @example Add a topic with all topics up to a count
54
54
  # tpl.add_topic("topic", 9)
55
- def add_topic(topic, partitions = nil)
55
+ def add_topic(topic_name, partitions = nil)
56
56
  if partitions.nil?
57
- @data[topic.to_s] = nil
57
+ @data[topic_name.to_s] = nil
58
58
  else
59
59
  if partitions.is_a? Integer
60
60
  partitions = (0..partitions - 1)
61
61
  end
62
- @data[topic.to_s] = partitions.map { |p| Partition.new(p, nil, Rdkafka::Bindings::RD_KAFKA_RESP_ERR_NO_ERROR) }
62
+ @data[topic_name.to_s] = partitions.map { |p| Partition.new(p, nil, Rdkafka::Bindings::RD_KAFKA_RESP_ERR_NO_ERROR) }
63
63
  end
64
64
  end
65
65
 
@@ -67,12 +67,12 @@ module Rdkafka
67
67
  # Calling this method multiple times for the same topic will overwrite the previous
68
68
  # configuraton.
69
69
  #
70
- # @param topic [String] The topic's name
70
+ # @param topic_name [String] The topic's name
71
71
  # @param partitions_with_offsets [Hash{Integer => Integer}, Array<Consumer::Partition>] The
72
72
  # topic's partitions and offsets (Hash) or partitions with offsets and metadata (Array)
73
73
  # @return [nil]
74
- def add_topic_and_partitions_with_offsets(topic, partitions_with_offsets)
75
- @data[topic.to_s] = partitions_with_offsets.map do |p, o|
74
+ def add_topic_and_partitions_with_offsets(topic_name, partitions_with_offsets)
75
+ @data[topic_name.to_s] = partitions_with_offsets.map do |p, o|
76
76
  p.is_a?(Partition) ? p : Partition.new(p, o)
77
77
  end
78
78
  end
@@ -17,6 +17,9 @@ module Rdkafka
17
17
  include Helpers::Metadata
18
18
  include Helpers::ListOffsets
19
19
 
20
+ # Reason passed to the error callback when closing the consumer fails
21
+ CLOSE_ERROR_REASON = "Consumer close failed"
22
+
20
23
  # @private
21
24
  # @param native_kafka [NativeKafka] wrapper around the native Kafka consumer handle
22
25
  def initialize(native_kafka)
@@ -31,30 +34,61 @@ module Rdkafka
31
34
  ObjectSpace.define_finalizer(self, self.class.finalizer(native_kafka, @consumer_queue_holder))
32
35
  end
33
36
 
34
- # Builds the GC finalizer for a consumer. It mirrors {#close}: close the consumer, destroy the
35
- # consumer-queue reference, then destroy the native client. The default `NativeKafka#finalizer`
36
- # went straight to `rd_kafka_destroy`, leaving the consumer-queue reference (from
37
- # `rd_kafka_queue_get_consumer`, taken by `poll_batch`) dangling - which can make
38
- # `rd_kafka_destroy` block inside the finalizer (process hang at GC/shutdown) or leak the
39
- # handle.
40
- #
41
- # @private
42
- # @param native_kafka [NativeKafka] the wrapped native client
43
- # @param queue_holder [Array] single-element holder carrying the consumer queue pointer
44
- # (or empty)
45
- # @return [Proc] finalizer proc that must not reference the consumer instance
46
- def self.finalizer(native_kafka, queue_holder)
47
- proc do
48
- next if native_kafka.closed?
49
-
50
- native_kafka.synchronize do |inner|
51
- Rdkafka::Bindings.rd_kafka_consumer_close(inner)
52
-
53
- queue = queue_holder[0]
54
- Rdkafka::Bindings.rd_kafka_queue_destroy(queue) if queue
37
+ class << self
38
+ # Builds the GC finalizer for a consumer. It mirrors {#close}: close the consumer, destroy the
39
+ # consumer-queue reference, then destroy the native client. The default
40
+ # `NativeKafka#finalizer` went straight to `rd_kafka_destroy`, leaving the consumer-queue
41
+ # reference (from `rd_kafka_queue_get_consumer`, taken by `poll_batch`) dangling - which can
42
+ # make `rd_kafka_destroy` block inside the finalizer (process hang at GC/shutdown) or leak the
43
+ # handle.
44
+ #
45
+ # @private
46
+ # @param native_kafka [NativeKafka] the wrapped native client
47
+ # @param queue_holder [Array] single-element holder carrying the consumer queue pointer
48
+ # (or empty)
49
+ # @return [Proc] finalizer proc that must not reference the consumer instance
50
+ def finalizer(native_kafka, queue_holder)
51
+ proc do
52
+ next if native_kafka.closed?
53
+
54
+ close_response = nil
55
+
56
+ native_kafka.synchronize do |inner|
57
+ close_response = Rdkafka::Bindings.rd_kafka_consumer_close(inner)
58
+
59
+ queue = queue_holder[0]
60
+ Rdkafka::Bindings.rd_kafka_queue_destroy(queue) if queue
61
+ end
62
+
63
+ report_close_error(native_kafka, close_response)
64
+
65
+ native_kafka.close
66
+ end
67
+ end
68
+
69
+ # Reports a failed `rd_kafka_consumer_close` (e.g. of a fatally-errored consumer) through
70
+ # {Bindings::ErrorCallback}, the same path as errors emitted by librdkafka, so it reaches
71
+ # {Config.error_callback} in the shape the callback expects (libraries such as karafka-core
72
+ # replace `ErrorCallback`). It runs after the close, but before the native client is
73
+ # destroyed, as the callback reads the client name and fatal error from it. Close runs during
74
+ # shutdown and from the GC finalizer, so the error is reported, never raised.
75
+ #
76
+ # @private
77
+ # @param native_kafka [NativeKafka] the wrapped native client
78
+ # @param response [Integer] result code of `rd_kafka_consumer_close`
79
+ # @return [nil]
80
+ def report_close_error(native_kafka, response)
81
+ return if response == Rdkafka::Bindings::RD_KAFKA_RESP_ERR_NO_ERROR
82
+
83
+ native_kafka.with_inner do |inner|
84
+ Rdkafka::Bindings::ErrorCallback.call(inner, response, CLOSE_ERROR_REASON, nil)
55
85
  end
56
86
 
57
- native_kafka.close
87
+ nil
88
+ rescue Exception => err
89
+ Rdkafka::Config.logger.error("Unhandled exception: #{err.class} - #{err.message}")
90
+
91
+ nil
58
92
  end
59
93
  end
60
94
 
@@ -114,7 +148,7 @@ module Rdkafka
114
148
 
115
149
  # Polls for events in a non-blocking loop, yielding the count after each iteration.
116
150
  #
117
- # This method processes events (stats, errors, etc.) in a single GVL/mutex session,
151
+ # This method processes events (stats, errors, etc.) in a single GVL session,
118
152
  # which is more efficient than repeated individual polls. It uses non-blocking polls
119
153
  # internally (no GVL release between polls).
120
154
  #
@@ -135,8 +169,9 @@ module Rdkafka
135
169
  # consumer.events_poll_nb_each do |_count|
136
170
  # :stop if monotonic_now >= deadline
137
171
  # end
138
- # @note This method holds the inner lock until the queue is empty or `:stop` is returned.
139
- # Other consumer operations will wait until this method returns.
172
+ # @note This method runs inside `with_inner` and does not hold a lock while it loops.
173
+ # Other operations on the same client run concurrently. `close` waits until this
174
+ # method returns.
140
175
  # @note This method is thread-safe as it uses @native_kafka.with_inner synchronization
141
176
  # @note Do NOT use this if `consumer_poll_set` was set to `true`
142
177
  def events_poll_nb_each
@@ -153,7 +188,7 @@ module Rdkafka
153
188
 
154
189
  # Polls for messages in a non-blocking loop, yielding each message to the caller.
155
190
  #
156
- # This method processes messages in a single GVL/mutex session until the queue is empty
191
+ # This method processes messages in a single GVL session until the queue is empty
157
192
  # or the caller returns `:stop`. It handles the message pointer lifecycle internally,
158
193
  # ensuring proper cleanup via `rd_kafka_message_destroy`.
159
194
  #
@@ -180,8 +215,9 @@ module Rdkafka
180
215
  # `events_poll_nb_each` which uses `rd_kafka_poll` for event callbacks (delivery reports,
181
216
  # statistics, etc.). For consumers, use this method to receive messages and
182
217
  # `events_poll_nb_each` for processing background events.
183
- # @note This method holds the inner lock for the duration. Other consumer operations
184
- # will wait until this method returns.
218
+ # @note This method runs inside `with_inner` and does not hold a lock while it loops.
219
+ # Other operations on the same client run concurrently. `close` waits until this
220
+ # method returns.
185
221
  # @note Timeout/max_messages logic should be implemented by the caller
186
222
  def poll_nb_each
187
223
  closed_consumer_check(__method__)
@@ -213,8 +249,10 @@ module Rdkafka
213
249
  return if closed?
214
250
  ObjectSpace.undefine_finalizer(self)
215
251
 
252
+ close_response = nil
253
+
216
254
  @native_kafka.synchronize do |inner|
217
- Rdkafka::Bindings.rd_kafka_consumer_close(inner)
255
+ close_response = Rdkafka::Bindings.rd_kafka_consumer_close(inner)
218
256
 
219
257
  if @consumer_queue
220
258
  Rdkafka::Bindings.rd_kafka_queue_destroy(@consumer_queue)
@@ -223,6 +261,8 @@ module Rdkafka
223
261
  end
224
262
  end
225
263
 
264
+ self.class.report_close_error(@native_kafka, close_response)
265
+
226
266
  @native_kafka.close
227
267
  end
228
268
 
@@ -469,12 +509,12 @@ module Rdkafka
469
509
 
470
510
  # Query broker for low (oldest/beginning) and high (newest/end) offsets for a partition.
471
511
  #
472
- # @param topic [String] The topic to query
512
+ # @param topic_name [String] The topic to query
473
513
  # @param partition [Integer] The partition to query
474
514
  # @param timeout_ms [Integer] The timeout for querying the broker
475
515
  # @return [Integer] The low and high watermark
476
516
  # @raise [RdkafkaError] When querying the broker fails.
477
- def query_watermark_offsets(topic, partition, timeout_ms = Defaults::CONSUMER_QUERY_WATERMARK_TIMEOUT_MS)
517
+ def query_watermark_offsets(topic_name, partition, timeout_ms = Defaults::CONSUMER_QUERY_WATERMARK_TIMEOUT_MS)
478
518
  closed_consumer_check(__method__)
479
519
 
480
520
  low = FFI::MemoryPointer.new(:int64, 1)
@@ -483,13 +523,13 @@ module Rdkafka
483
523
  @native_kafka.with_inner do |inner|
484
524
  response = Rdkafka::Bindings.rd_kafka_query_watermark_offsets(
485
525
  inner,
486
- topic,
526
+ topic_name,
487
527
  partition,
488
528
  low,
489
529
  high,
490
530
  timeout_ms
491
531
  )
492
- Rdkafka::RdkafkaError.validate!(response, "Error querying watermark offsets for partition #{partition} of #{topic}", client_ptr: inner)
532
+ Rdkafka::RdkafkaError.validate!(response, "Error querying watermark offsets for partition #{partition} of #{topic_name}", client_ptr: inner)
493
533
  end
494
534
 
495
535
  [low.read_array_of_int64(1).first, high.read_array_of_int64(1).first]
@@ -651,12 +691,12 @@ module Rdkafka
651
691
  # Seek to a particular message by providing the topic, partition and offset. The next poll on
652
692
  # the topic/partition will return the message at the given offset.
653
693
  #
654
- # @param topic [String] The topic in which to seek
694
+ # @param topic_name [String] The topic in which to seek
655
695
  # @param partition [Integer] The partition number to seek
656
696
  # @param offset [Integer] The partition offset to seek
657
697
  # @return [nil]
658
698
  # @raise [RdkafkaError] When seeking fails
659
- def seek_by(topic, partition, offset)
699
+ def seek_by(topic_name, partition, offset)
660
700
  closed_consumer_check(__method__)
661
701
 
662
702
  # rd_kafka_offset_store is one of the few calls that does not support
@@ -664,7 +704,7 @@ module Rdkafka
664
704
  native_topic = @native_kafka.with_inner do |inner|
665
705
  Rdkafka::Bindings.rd_kafka_topic_new(
666
706
  inner,
667
- topic,
707
+ topic_name,
668
708
  nil
669
709
  )
670
710
  end
@@ -114,12 +114,14 @@ module Rdkafka
114
114
  # @see Config#admin
115
115
  NATIVE_KAFKA_POLL_TIMEOUT_MS = 100
116
116
 
117
- # Sleep interval used in NativeKafka#synchronize wait loop
118
- # @see NativeKafka#synchronize
119
- NATIVE_KAFKA_SYNCHRONIZE_SLEEP_INTERVAL_MS = 10
120
-
121
117
  # TTL for partitions count cache entries (30 seconds)
122
118
  # @see Producer::PartitionsCountCache
123
119
  PARTITIONS_COUNT_CACHE_TTL_MS = 30_000
120
+
121
+ # How long an expired partitions count is served after a refresh failed on a transient
122
+ # (e.g. transport) error, before the next refresh attempt. Longer than one failed metadata
123
+ # refresh (up to ~7 s with retries), so produces are not blocked most of the time
124
+ # @see Producer::PartitionsCountCache#get
125
+ PARTITIONS_COUNT_CACHE_STALE_BACKOFF_MS = 10_000
124
126
  end
125
127
  end
@@ -25,21 +25,15 @@ module Rdkafka
25
25
  @access_mutex = Mutex.new
26
26
  # Lock around internal polling
27
27
  @poll_mutex = Mutex.new
28
- # Lock around decrementing the operations in progress counter
29
- # We have two mutexes - one for increment (`@access_mutex`) and one for decrement mutex
30
- # because they serve different purposes:
31
- #
32
- # - `@access_mutex` allows us to lock the execution and make sure that any operation within
33
- # the `#synchronize` is the only one running and that there are no other running
34
- # operations.
35
- # - `@decrement_mutex` ensures, that our decrement operation is thread-safe for any Ruby
36
- # implementation.
37
- #
38
- # We do not use the same mutex, because it could create a deadlock when an already
39
- # incremented operation cannot decrement because `@access_lock` is now owned by a different
40
- # thread in a synchronized mode and the synchronized mode is waiting on the decrement.
41
- @decrement_mutex = Mutex.new
42
- # counter for operations in progress using inner
28
+ # Signalled when `#synchronize` ends and, only while `#synchronize` waits, when the operations
29
+ # in progress counter reaches zero: no one else waits for zero, so other decrements skip the
30
+ # broadcast. `#synchronize` waits on it with `@access_mutex` released, so operations that
31
+ # started before it can still decrement the counter.
32
+ @operations_done = ConditionVariable.new
33
+ # Set while `#synchronize` runs, so no new operation starts until it ends
34
+ @synchronizing = false
35
+ # counter for operations in progress using inner, changed only under `@access_mutex`, so no
36
+ # update is lost on Rubies without a GVL (JRuby)
43
37
  @operations_in_progress = 0
44
38
 
45
39
  @run_polling_thread = run_polling_thread
@@ -63,8 +57,7 @@ module Rdkafka
63
57
  Rdkafka::Bindings.rd_kafka_poll(@inner, 0)
64
58
 
65
59
  if @run_polling_thread
66
- # Start thread to poll client for delivery callbacks,
67
- # not used in consumer.
60
+ # Start thread to poll client for delivery callbacks, not used in consumer.
68
61
  @polling_thread = Thread.new do
69
62
  loop do
70
63
  @poll_mutex.synchronize do
@@ -90,15 +83,29 @@ module Rdkafka
90
83
  # @return [Object] the result of the block
91
84
  # @raise [ClosedInnerError] when the inner handle is nil
92
85
  def with_inner
93
- if @access_mutex.owned?
86
+ owned = @access_mutex.owned?
87
+
88
+ if owned
94
89
  @operations_in_progress += 1
95
90
  else
96
- @access_mutex.synchronize { @operations_in_progress += 1 }
91
+ @access_mutex.synchronize do
92
+ @operations_done.wait(@access_mutex) while @synchronizing
93
+ @operations_in_progress += 1
94
+ end
97
95
  end
98
96
 
99
- @inner.nil? ? raise(ClosedInnerError) : yield(@inner)
100
- ensure
101
- @decrement_mutex.synchronize { @operations_in_progress -= 1 }
97
+ begin
98
+ @inner.nil? ? raise(ClosedInnerError) : yield(@inner)
99
+ ensure
100
+ if owned
101
+ @operations_in_progress -= 1
102
+ else
103
+ @access_mutex.synchronize do
104
+ @operations_in_progress -= 1
105
+ @operations_done.broadcast if @synchronizing && @operations_in_progress.zero?
106
+ end
107
+ end
108
+ end
102
109
  end
103
110
 
104
111
  # Executes a block while holding exclusive access to the native Kafka handle
@@ -107,13 +114,21 @@ module Rdkafka
107
114
  # @return [Object] the result of the block
108
115
  def synchronize(&block)
109
116
  @access_mutex.synchronize do
110
- # Wait for any commands using the inner to finish
111
- # This can take a while on blocking operations like polling but is essential not to proceed
112
- # with certain types of operations like resources destruction as it can cause the process
113
- # to hang or crash
114
- sleep(Defaults::NATIVE_KAFKA_SYNCHRONIZE_SLEEP_INTERVAL_MS / 1_000.0) until @operations_in_progress.zero?
115
-
116
- with_inner(&block)
117
+ @operations_done.wait(@access_mutex) while @synchronizing
118
+ @synchronizing = true
119
+
120
+ begin
121
+ # Wait for any commands using the inner to finish
122
+ # This can take a while on blocking operations like polling but is essential not to
123
+ # proceed with certain types of operations like resources destruction as it can cause the
124
+ # process to hang or crash
125
+ @operations_done.wait(@access_mutex) until @operations_in_progress.zero?
126
+
127
+ with_inner(&block)
128
+ ensure
129
+ @synchronizing = false
130
+ @operations_done.broadcast
131
+ end
117
132
  end
118
133
  end
119
134
 
@@ -210,8 +225,7 @@ module Rdkafka
210
225
  # Indicate to polling thread that we're closing
211
226
  @polling_thread[:closing] = true
212
227
 
213
- # Wait for the polling thread to finish up,
214
- # this can be aborted in practice if this
228
+ # Wait for the polling thread to finish up, this can be aborted in practice if this
215
229
  # code runs from a finalizer.
216
230
  @polling_thread.join
217
231
  end