rdkafka 0.22.2 → 0.27.0

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 (109) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +63 -3
  3. data/Gemfile +8 -0
  4. data/Gemfile.lint +14 -0
  5. data/Gemfile.lint.lock +123 -0
  6. data/README.md +19 -14
  7. data/Rakefile +21 -21
  8. data/bin/verify_kafka_warnings +39 -0
  9. data/dist/{librdkafka-2.8.0.tar.gz → librdkafka-2.14.0.tar.gz} +0 -0
  10. data/docker-compose-ssl.yml +35 -0
  11. data/docker-compose.yml +2 -2
  12. data/ext/Rakefile +27 -27
  13. data/lib/rdkafka/abstract_handle.rb +23 -5
  14. data/lib/rdkafka/admin/acl_binding_result.rb +5 -5
  15. data/lib/rdkafka/admin/config_resource_binding_result.rb +1 -0
  16. data/lib/rdkafka/admin/create_acl_handle.rb +7 -4
  17. data/lib/rdkafka/admin/create_acl_report.rb +3 -2
  18. data/lib/rdkafka/admin/create_partitions_handle.rb +8 -5
  19. data/lib/rdkafka/admin/create_partitions_report.rb +1 -0
  20. data/lib/rdkafka/admin/create_topic_handle.rb +8 -5
  21. data/lib/rdkafka/admin/create_topic_report.rb +3 -0
  22. data/lib/rdkafka/admin/delete_acl_handle.rb +9 -6
  23. data/lib/rdkafka/admin/delete_acl_report.rb +5 -3
  24. data/lib/rdkafka/admin/delete_groups_handle.rb +10 -5
  25. data/lib/rdkafka/admin/delete_groups_report.rb +3 -0
  26. data/lib/rdkafka/admin/delete_topic_handle.rb +8 -5
  27. data/lib/rdkafka/admin/delete_topic_report.rb +3 -0
  28. data/lib/rdkafka/admin/describe_acl_handle.rb +9 -6
  29. data/lib/rdkafka/admin/describe_acl_report.rb +5 -3
  30. data/lib/rdkafka/admin/describe_configs_handle.rb +7 -4
  31. data/lib/rdkafka/admin/describe_configs_report.rb +7 -1
  32. data/lib/rdkafka/admin/incremental_alter_configs_handle.rb +7 -4
  33. data/lib/rdkafka/admin/incremental_alter_configs_report.rb +7 -1
  34. data/lib/rdkafka/admin/list_offsets_handle.rb +36 -0
  35. data/lib/rdkafka/admin/list_offsets_report.rb +51 -0
  36. data/lib/rdkafka/admin.rb +301 -135
  37. data/lib/rdkafka/bindings.rb +199 -110
  38. data/lib/rdkafka/callbacks.rb +124 -21
  39. data/lib/rdkafka/config.rb +81 -33
  40. data/lib/rdkafka/consumer/headers.rb +3 -2
  41. data/lib/rdkafka/consumer/message.rb +12 -11
  42. data/lib/rdkafka/consumer/partition.rb +8 -4
  43. data/lib/rdkafka/consumer/topic_partition_list.rb +21 -17
  44. data/lib/rdkafka/consumer.rb +397 -45
  45. data/lib/rdkafka/defaults.rb +106 -0
  46. data/lib/rdkafka/error.rb +40 -14
  47. data/lib/rdkafka/helpers/oauth.rb +45 -13
  48. data/lib/rdkafka/helpers/time.rb +5 -0
  49. data/lib/rdkafka/metadata.rb +45 -21
  50. data/lib/rdkafka/native_kafka.rb +89 -4
  51. data/lib/rdkafka/producer/delivery_handle.rb +5 -5
  52. data/lib/rdkafka/producer/delivery_report.rb +10 -6
  53. data/lib/rdkafka/producer/partitions_count_cache.rb +29 -19
  54. data/lib/rdkafka/producer.rb +168 -82
  55. data/lib/rdkafka/version.rb +6 -3
  56. data/lib/rdkafka.rb +3 -0
  57. data/package-lock.json +331 -0
  58. data/package.json +9 -0
  59. data/rdkafka.gemspec +57 -36
  60. data/renovate.json +29 -24
  61. metadata +29 -124
  62. data/.github/CODEOWNERS +0 -3
  63. data/.github/FUNDING.yml +0 -1
  64. data/.github/workflows/ci_linux_x86_64_gnu.yml +0 -271
  65. data/.github/workflows/ci_linux_x86_64_musl.yml +0 -194
  66. data/.github/workflows/ci_macos_arm64.yml +0 -284
  67. data/.github/workflows/push_linux_x86_64_gnu.yml +0 -65
  68. data/.github/workflows/push_linux_x86_64_musl.yml +0 -79
  69. data/.github/workflows/push_macos_arm64.yml +0 -54
  70. data/.github/workflows/push_ruby.yml +0 -37
  71. data/.github/workflows/verify-action-pins.yml +0 -16
  72. data/.gitignore +0 -14
  73. data/.rspec +0 -2
  74. data/.ruby-gemset +0 -1
  75. data/.ruby-version +0 -1
  76. data/.yardopts +0 -2
  77. data/ext/README.md +0 -19
  78. data/ext/build_common.sh +0 -361
  79. data/ext/build_linux_x86_64_gnu.sh +0 -306
  80. data/ext/build_linux_x86_64_musl.sh +0 -763
  81. data/ext/build_macos_arm64.sh +0 -550
  82. data/spec/rdkafka/abstract_handle_spec.rb +0 -117
  83. data/spec/rdkafka/admin/create_acl_handle_spec.rb +0 -56
  84. data/spec/rdkafka/admin/create_acl_report_spec.rb +0 -18
  85. data/spec/rdkafka/admin/create_topic_handle_spec.rb +0 -52
  86. data/spec/rdkafka/admin/create_topic_report_spec.rb +0 -16
  87. data/spec/rdkafka/admin/delete_acl_handle_spec.rb +0 -85
  88. data/spec/rdkafka/admin/delete_acl_report_spec.rb +0 -72
  89. data/spec/rdkafka/admin/delete_topic_handle_spec.rb +0 -52
  90. data/spec/rdkafka/admin/delete_topic_report_spec.rb +0 -16
  91. data/spec/rdkafka/admin/describe_acl_handle_spec.rb +0 -85
  92. data/spec/rdkafka/admin/describe_acl_report_spec.rb +0 -73
  93. data/spec/rdkafka/admin_spec.rb +0 -971
  94. data/spec/rdkafka/bindings_spec.rb +0 -199
  95. data/spec/rdkafka/callbacks_spec.rb +0 -20
  96. data/spec/rdkafka/config_spec.rb +0 -258
  97. data/spec/rdkafka/consumer/headers_spec.rb +0 -73
  98. data/spec/rdkafka/consumer/message_spec.rb +0 -139
  99. data/spec/rdkafka/consumer/partition_spec.rb +0 -57
  100. data/spec/rdkafka/consumer/topic_partition_list_spec.rb +0 -248
  101. data/spec/rdkafka/consumer_spec.rb +0 -1274
  102. data/spec/rdkafka/error_spec.rb +0 -89
  103. data/spec/rdkafka/metadata_spec.rb +0 -79
  104. data/spec/rdkafka/native_kafka_spec.rb +0 -130
  105. data/spec/rdkafka/producer/delivery_handle_spec.rb +0 -45
  106. data/spec/rdkafka/producer/delivery_report_spec.rb +0 -25
  107. data/spec/rdkafka/producer/partitions_count_cache_spec.rb +0 -359
  108. data/spec/rdkafka/producer_spec.rb +0 -1345
  109. data/spec/spec_helper.rb +0 -195
@@ -16,8 +16,12 @@ module Rdkafka
16
16
  include Helpers::OAuth
17
17
 
18
18
  # @private
19
+ # @param native_kafka [NativeKafka] wrapper around the native Kafka consumer handle
19
20
  def initialize(native_kafka)
20
21
  @native_kafka = native_kafka
22
+
23
+ # Makes sure, that native kafka gets closed before it gets GCed by Ruby
24
+ ObjectSpace.define_finalizer(self, native_kafka.finalizer)
21
25
  end
22
26
 
23
27
  # Starts the native Kafka polling thread and kicks off the init polling
@@ -33,8 +37,142 @@ module Rdkafka
33
37
  end
34
38
  end
35
39
 
36
- def finalizer
37
- ->(_) { close }
40
+ # Enable IO event notifications for fiber scheduler integration
41
+ # When the consumer queue has messages, librdkafka will write to your FD
42
+ #
43
+ # @param fd [Integer] file descriptor to signal (from IO.pipe or eventfd)
44
+ # @param payload [String] data to write to fd (default: "\x01")
45
+ # @return [nil]
46
+ # @raise [ClosedInnerError] when the consumer is closed
47
+ #
48
+ # @example Using with fiber scheduler
49
+ # consumer = config.consumer
50
+ # consumer.subscribe("topic")
51
+ #
52
+ # # Create notification FD
53
+ # signal_r, signal_w = IO.pipe
54
+ #
55
+ # # Enable librdkafka to signal when messages arrive
56
+ # consumer.enable_queue_io_events(signal_w.fileno)
57
+ #
58
+ # # Monitor with select/poll
59
+ # loop do
60
+ # readable, = IO.select([signal_r], nil, nil, timeout)
61
+ # if readable
62
+ # signal_r.read_nonblock(1024) rescue nil # Drain signal
63
+ # while msg = consumer.poll(0)
64
+ # process(msg)
65
+ # end
66
+ # end
67
+ # end
68
+ def enable_queue_io_events(fd, payload = "\x01")
69
+ @native_kafka.enable_main_queue_io_events(fd, payload)
70
+ end
71
+
72
+ # Enable IO event notifications for background events
73
+ # @param fd [Integer] file descriptor to signal (from IO.pipe or eventfd)
74
+ # @param payload [String] data to write to fd (default: "\x01")
75
+ # @return [nil]
76
+ # @raise [ClosedInnerError] when the consumer is closed
77
+ def enable_background_queue_io_events(fd, payload = "\x01")
78
+ @native_kafka.enable_background_queue_io_events(fd, payload)
79
+ end
80
+
81
+ # Polls for events in a non-blocking loop, yielding the count after each iteration.
82
+ #
83
+ # This method processes events (stats, errors, etc.) in a single GVL/mutex session,
84
+ # which is more efficient than repeated individual polls. It uses non-blocking polls
85
+ # internally (no GVL release between polls).
86
+ #
87
+ # Yields the count of events processed after each poll iteration, allowing the caller
88
+ # to implement timeout or other termination logic by returning `:stop`.
89
+ #
90
+ # @yield [count] Called after each poll iteration
91
+ # @yieldparam count [Integer] Number of events processed in this iteration
92
+ # @yieldreturn [Symbol, Object] Return `:stop` to break the loop, any other value continues
93
+ # @return [nil]
94
+ # @raise [Rdkafka::ClosedConsumerError] if called on a closed consumer
95
+ #
96
+ # @note This method holds the inner lock until the queue is empty or `:stop` is returned.
97
+ # Other consumer operations will wait until this method returns.
98
+ # @note This method is thread-safe as it uses @native_kafka.with_inner synchronization
99
+ # @note Do NOT use this if `consumer_poll_set` was set to `true`
100
+ #
101
+ # @example Drain all pending events
102
+ # consumer.events_poll_nb_each { |_count| }
103
+ #
104
+ # @example With timeout control
105
+ # deadline = monotonic_now + timeout_ms
106
+ # consumer.events_poll_nb_each do |_count|
107
+ # :stop if monotonic_now >= deadline
108
+ # end
109
+ def events_poll_nb_each
110
+ closed_consumer_check(__method__)
111
+
112
+ @native_kafka.with_inner do |inner|
113
+ loop do
114
+ count = Rdkafka::Bindings.rd_kafka_poll_nb(inner, 0)
115
+ break if count.zero?
116
+ break if yield(count) == :stop
117
+ end
118
+ end
119
+ end
120
+
121
+ # Polls for messages in a non-blocking loop, yielding each message to the caller.
122
+ #
123
+ # This method processes messages in a single GVL/mutex session until the queue is empty
124
+ # or the caller returns `:stop`. It handles the message pointer lifecycle internally,
125
+ # ensuring proper cleanup via `rd_kafka_message_destroy`.
126
+ #
127
+ # @yield [message] Called for each message received
128
+ # @yieldparam message [Consumer::Message] The received message
129
+ # @yieldreturn [Symbol, Object] Return `:stop` to break the loop, any other value continues
130
+ # @return [nil]
131
+ # @raise [Rdkafka::ClosedConsumerError] if called on a closed consumer
132
+ # @raise [Rdkafka::RdkafkaError] if a Kafka error occurs while polling
133
+ #
134
+ # @note This method uses `rd_kafka_consumer_poll` to fetch messages, unlike
135
+ # `events_poll_nb_each` which uses `rd_kafka_poll` for event callbacks (delivery reports,
136
+ # statistics, etc.). For consumers, use this method to receive messages and
137
+ # `events_poll_nb_each` for processing background events.
138
+ # @note This method holds the inner lock for the duration. Other consumer operations
139
+ # will wait until this method returns.
140
+ # @note Timeout/max_messages logic should be implemented by the caller
141
+ #
142
+ # @example Process messages until queue is empty
143
+ # consumer.poll_nb_each do |message|
144
+ # process(message)
145
+ # end
146
+ #
147
+ # @example Process with early termination
148
+ # count = 0
149
+ # consumer.poll_nb_each do |message|
150
+ # process(message)
151
+ # count += 1
152
+ # :stop if count >= 10
153
+ # end
154
+ def poll_nb_each
155
+ closed_consumer_check(__method__)
156
+
157
+ @native_kafka.with_inner do |inner|
158
+ loop do
159
+ message_ptr = Rdkafka::Bindings.rd_kafka_consumer_poll_nb(inner, 0)
160
+ break if message_ptr.null?
161
+
162
+ begin
163
+ native_message = Rdkafka::Bindings::Message.new(message_ptr)
164
+
165
+ if native_message[:err] != Rdkafka::Bindings::RD_KAFKA_RESP_ERR_NO_ERROR
166
+ raise Rdkafka::RdkafkaError.new(native_message[:err])
167
+ end
168
+
169
+ result = yield Consumer::Message.new(native_message)
170
+ break if result == :stop
171
+ ensure
172
+ Rdkafka::Bindings.rd_kafka_message_destroy(message_ptr)
173
+ end
174
+ end
175
+ end
38
176
  end
39
177
 
40
178
  # Close this consumer
@@ -45,6 +183,11 @@ module Rdkafka
45
183
 
46
184
  @native_kafka.synchronize do |inner|
47
185
  Rdkafka::Bindings.rd_kafka_consumer_close(inner)
186
+
187
+ if @consumer_queue
188
+ Rdkafka::Bindings.rd_kafka_queue_destroy(@consumer_queue)
189
+ @consumer_queue = nil
190
+ end
48
191
  end
49
192
 
50
193
  @native_kafka.close
@@ -67,15 +210,15 @@ module Rdkafka
67
210
  tpl = Rdkafka::Bindings.rd_kafka_topic_partition_list_new(topics.length)
68
211
 
69
212
  topics.each do |topic|
70
- Rdkafka::Bindings.rd_kafka_topic_partition_list_add(tpl, topic, -1)
213
+ Rdkafka::Bindings.rd_kafka_topic_partition_list_add(tpl, topic, Rdkafka::Bindings::RD_KAFKA_PARTITION_UA)
71
214
  end
72
215
 
73
216
  # Subscribe to topic partition list and check this was successful
74
217
  response = @native_kafka.with_inner do |inner|
75
218
  Rdkafka::Bindings.rd_kafka_subscribe(inner, tpl)
76
219
  end
77
- if response != 0
78
- raise Rdkafka::RdkafkaError.new(response, "Error subscribing to '#{topics.join(', ')}'")
220
+ if response != Rdkafka::Bindings::RD_KAFKA_RESP_ERR_NO_ERROR
221
+ raise Rdkafka::RdkafkaError.new(response, "Error subscribing to '#{topics.join(", ")}'")
79
222
  end
80
223
  ensure
81
224
  Rdkafka::Bindings.rd_kafka_topic_partition_list_destroy(tpl) unless tpl.nil?
@@ -91,7 +234,7 @@ module Rdkafka
91
234
  response = @native_kafka.with_inner do |inner|
92
235
  Rdkafka::Bindings.rd_kafka_unsubscribe(inner)
93
236
  end
94
- if response != 0
237
+ if response != Rdkafka::Bindings::RD_KAFKA_RESP_ERR_NO_ERROR
95
238
  raise Rdkafka::RdkafkaError.new(response)
96
239
  end
97
240
  end
@@ -115,7 +258,7 @@ module Rdkafka
115
258
  Rdkafka::Bindings.rd_kafka_pause_partitions(inner, tpl)
116
259
  end
117
260
 
118
- if response != 0
261
+ if response != Rdkafka::Bindings::RD_KAFKA_RESP_ERR_NO_ERROR
119
262
  list = TopicPartitionList.from_native_tpl(tpl)
120
263
  raise Rdkafka::RdkafkaTopicPartitionListError.new(response, list, "Error pausing '#{list.to_h}'")
121
264
  end
@@ -142,7 +285,7 @@ module Rdkafka
142
285
  response = @native_kafka.with_inner do |inner|
143
286
  Rdkafka::Bindings.rd_kafka_resume_partitions(inner, tpl)
144
287
  end
145
- if response != 0
288
+ if response != Rdkafka::Bindings::RD_KAFKA_RESP_ERR_NO_ERROR
146
289
  raise Rdkafka::RdkafkaError.new(response, "Error resume '#{list.to_h}'")
147
290
  end
148
291
  ensure
@@ -162,7 +305,7 @@ module Rdkafka
162
305
  Rdkafka::Bindings.rd_kafka_subscription(inner, ptr)
163
306
  end
164
307
 
165
- if response != 0
308
+ if response != Rdkafka::Bindings::RD_KAFKA_RESP_ERR_NO_ERROR
166
309
  raise Rdkafka::RdkafkaError.new(response)
167
310
  end
168
311
 
@@ -192,7 +335,7 @@ module Rdkafka
192
335
  response = @native_kafka.with_inner do |inner|
193
336
  Rdkafka::Bindings.rd_kafka_assign(inner, tpl)
194
337
  end
195
- if response != 0
338
+ if response != Rdkafka::Bindings::RD_KAFKA_RESP_ERR_NO_ERROR
196
339
  raise Rdkafka::RdkafkaError.new(response, "Error assigning '#{list.to_h}'")
197
340
  end
198
341
  ensure
@@ -211,7 +354,7 @@ module Rdkafka
211
354
  response = @native_kafka.with_inner do |inner|
212
355
  Rdkafka::Bindings.rd_kafka_assignment(inner, ptr)
213
356
  end
214
- if response != 0
357
+ if response != Rdkafka::Bindings::RD_KAFKA_RESP_ERR_NO_ERROR
215
358
  raise Rdkafka::RdkafkaError.new(response)
216
359
  end
217
360
 
@@ -225,7 +368,7 @@ module Rdkafka
225
368
  end
226
369
  end
227
370
  ensure
228
- ptr.free unless ptr.nil?
371
+ ptr&.free
229
372
  end
230
373
 
231
374
  # @return [Boolean] true if our current assignment has been lost involuntarily.
@@ -246,7 +389,7 @@ module Rdkafka
246
389
  # @param timeout_ms [Integer] The timeout for fetching this information.
247
390
  # @return [TopicPartitionList]
248
391
  # @raise [RdkafkaError] When getting the committed positions fails.
249
- def committed(list=nil, timeout_ms=2000)
392
+ def committed(list = nil, timeout_ms = Defaults::CONSUMER_COMMITTED_TIMEOUT_MS)
250
393
  closed_consumer_check(__method__)
251
394
 
252
395
  if list.nil?
@@ -261,7 +404,7 @@ module Rdkafka
261
404
  response = @native_kafka.with_inner do |inner|
262
405
  Rdkafka::Bindings.rd_kafka_committed(inner, tpl, timeout_ms)
263
406
  end
264
- if response != 0
407
+ if response != Rdkafka::Bindings::RD_KAFKA_RESP_ERR_NO_ERROR
265
408
  raise Rdkafka::RdkafkaError.new(response)
266
409
  end
267
410
  TopicPartitionList.from_native_tpl(tpl)
@@ -278,7 +421,7 @@ module Rdkafka
278
421
  # @return [TopicPartitionList]
279
422
  #
280
423
  # @raise [RdkafkaError] When getting the positions fails.
281
- def position(list=nil)
424
+ def position(list = nil)
282
425
  if list.nil?
283
426
  list = assignment
284
427
  elsif !list.is_a?(TopicPartitionList)
@@ -291,11 +434,13 @@ module Rdkafka
291
434
  Rdkafka::Bindings.rd_kafka_position(inner, tpl)
292
435
  end
293
436
 
294
- if response != 0
437
+ if response != Rdkafka::Bindings::RD_KAFKA_RESP_ERR_NO_ERROR
295
438
  raise Rdkafka::RdkafkaError.new(response)
296
439
  end
297
440
 
298
441
  TopicPartitionList.from_native_tpl(tpl)
442
+ ensure
443
+ Rdkafka::Bindings.rd_kafka_topic_partition_list_destroy(tpl) if tpl
299
444
  end
300
445
 
301
446
  # Query broker for low (oldest/beginning) and high (newest/end) offsets for a partition.
@@ -305,7 +450,7 @@ module Rdkafka
305
450
  # @param timeout_ms [Integer] The timeout for querying the broker
306
451
  # @return [Integer] The low and high watermark
307
452
  # @raise [RdkafkaError] When querying the broker fails.
308
- def query_watermark_offsets(topic, partition, timeout_ms=1000)
453
+ def query_watermark_offsets(topic, partition, timeout_ms = Defaults::CONSUMER_QUERY_WATERMARK_TIMEOUT_MS)
309
454
  closed_consumer_check(__method__)
310
455
 
311
456
  low = FFI::MemoryPointer.new(:int64, 1)
@@ -318,17 +463,17 @@ module Rdkafka
318
463
  partition,
319
464
  low,
320
465
  high,
321
- timeout_ms,
466
+ timeout_ms
322
467
  )
323
468
  end
324
- if response != 0
469
+ if response != Rdkafka::Bindings::RD_KAFKA_RESP_ERR_NO_ERROR
325
470
  raise Rdkafka::RdkafkaError.new(response, "Error querying watermark offsets for partition #{partition} of #{topic}")
326
471
  end
327
472
 
328
- return low.read_array_of_int64(1).first, high.read_array_of_int64(1).first
473
+ [low.read_array_of_int64(1).first, high.read_array_of_int64(1).first]
329
474
  ensure
330
- low.free unless low.nil?
331
- high.free unless high.nil?
475
+ low&.free
476
+ high&.free
332
477
  end
333
478
 
334
479
  # Calculate the consumer lag per partition for the provided topic partition list.
@@ -338,10 +483,10 @@ module Rdkafka
338
483
  #
339
484
  # @param topic_partition_list [TopicPartitionList] The list to calculate lag for.
340
485
  # @param watermark_timeout_ms [Integer] The timeout for each query watermark call.
341
- # @return [Hash<String, Hash<Integer, Integer>>] A hash containing all topics with the lag
486
+ # @return [Hash{String => Hash{Integer => Integer}}] A hash containing all topics with the lag
342
487
  # per partition
343
488
  # @raise [RdkafkaError] When querying the broker fails.
344
- def lag(topic_partition_list, watermark_timeout_ms=1000)
489
+ def lag(topic_partition_list, watermark_timeout_ms = Defaults::CONSUMER_LAG_TIMEOUT_MS)
345
490
  out = {}
346
491
 
347
492
  topic_partition_list.to_h.each do |topic, partitions|
@@ -350,7 +495,7 @@ module Rdkafka
350
495
  topic_out = {}
351
496
  partitions.each do |p|
352
497
  next if p.offset.nil?
353
- low, high = query_watermark_offsets(
498
+ _low, high = query_watermark_offsets(
354
499
  topic,
355
500
  p.partition,
356
501
  watermark_timeout_ms
@@ -409,7 +554,7 @@ module Rdkafka
409
554
  )
410
555
  end
411
556
 
412
- if response != 0
557
+ if response != Rdkafka::Bindings::RD_KAFKA_RESP_ERR_NO_ERROR
413
558
  raise Rdkafka::RdkafkaError.new(response)
414
559
  end
415
560
  ensure
@@ -451,9 +596,9 @@ module Rdkafka
451
596
  native_topic,
452
597
  partition,
453
598
  offset,
454
- 0 # timeout
599
+ Defaults::CONSUMER_SEEK_TIMEOUT_MS
455
600
  )
456
- if response != 0
601
+ if response != Rdkafka::Bindings::RD_KAFKA_RESP_ERR_NO_ERROR
457
602
  raise Rdkafka::RdkafkaError.new(response)
458
603
  end
459
604
  ensure
@@ -465,11 +610,10 @@ module Rdkafka
465
610
  # Lookup offset for the given partitions by timestamp.
466
611
  #
467
612
  # @param list [TopicPartitionList] The TopicPartitionList with timestamps instead of offsets
468
- #
613
+ # @param timeout_ms [Integer] timeout in milliseconds for the operation
469
614
  # @return [TopicPartitionList]
470
- #
471
615
  # @raise [RdKafkaError] When the OffsetForTimes lookup fails
472
- def offsets_for_times(list, timeout_ms = 1000)
616
+ def offsets_for_times(list, timeout_ms = Defaults::CONSUMER_OFFSETS_FOR_TIMES_TIMEOUT_MS)
473
617
  closed_consumer_check(__method__)
474
618
 
475
619
  if !list.is_a?(TopicPartitionList)
@@ -486,7 +630,7 @@ module Rdkafka
486
630
  )
487
631
  end
488
632
 
489
- if response != 0
633
+ if response != Rdkafka::Bindings::RD_KAFKA_RESP_ERR_NO_ERROR
490
634
  raise Rdkafka::RdkafkaError.new(response)
491
635
  end
492
636
 
@@ -508,20 +652,20 @@ module Rdkafka
508
652
  # @param async [Boolean] Whether to commit async or wait for the commit to finish
509
653
  # @return [nil]
510
654
  # @raise [RdkafkaError] When committing fails
511
- def commit(list=nil, async=false)
655
+ def commit(list = nil, async = false)
512
656
  closed_consumer_check(__method__)
513
657
 
514
658
  if !list.nil? && !list.is_a?(TopicPartitionList)
515
659
  raise TypeError.new("list has to be nil or a TopicPartitionList")
516
660
  end
517
661
 
518
- tpl = list ? list.to_native_tpl : nil
662
+ tpl = list&.to_native_tpl
519
663
 
520
664
  begin
521
665
  response = @native_kafka.with_inner do |inner|
522
666
  Rdkafka::Bindings.rd_kafka_commit(inner, tpl, async)
523
667
  end
524
- if response != 0
668
+ if response != Rdkafka::Bindings::RD_KAFKA_RESP_ERR_NO_ERROR
525
669
  raise Rdkafka::RdkafkaError.new(response)
526
670
  end
527
671
  ensure
@@ -546,7 +690,48 @@ module Rdkafka
546
690
  # Create struct wrapper
547
691
  native_message = Rdkafka::Bindings::Message.new(message_ptr)
548
692
  # Raise error if needed
549
- if native_message[:err] != 0
693
+ if native_message[:err] != Rdkafka::Bindings::RD_KAFKA_RESP_ERR_NO_ERROR
694
+ raise Rdkafka::RdkafkaError.new(native_message[:err])
695
+ end
696
+ # Create a message to pass out
697
+ Rdkafka::Consumer::Message.new(native_message)
698
+ end
699
+ ensure
700
+ # Clean up rdkafka message if there is one
701
+ if message_ptr && !message_ptr.null?
702
+ Rdkafka::Bindings.rd_kafka_message_destroy(message_ptr)
703
+ end
704
+ end
705
+
706
+ # Poll for the next message without releasing the GVL (Global VM Lock).
707
+ #
708
+ # This is more efficient than regular polling for non-blocking poll(0) calls,
709
+ # particularly useful in fiber scheduler contexts where GVL release/reacquire
710
+ # overhead is wasteful since we don't expect to wait.
711
+ #
712
+ # @param timeout_ms [Integer] Timeout of this poll (default: 0 for non-blocking)
713
+ # @return [Message, nil] A message or nil if there was no new message within the timeout
714
+ # @raise [RdkafkaError] When polling fails
715
+ #
716
+ # @example Using with fiber scheduler
717
+ # # After receiving IO notification that messages are available
718
+ # while msg = consumer.poll_nb
719
+ # process(msg)
720
+ # end
721
+ def poll_nb(timeout_ms = 0)
722
+ closed_consumer_check(__method__)
723
+
724
+ message_ptr = @native_kafka.with_inner do |inner|
725
+ Rdkafka::Bindings.rd_kafka_consumer_poll_nb(inner, timeout_ms)
726
+ end
727
+
728
+ if message_ptr.null?
729
+ nil
730
+ else
731
+ # Create struct wrapper
732
+ native_message = Rdkafka::Bindings::Message.new(message_ptr)
733
+ # Raise error if needed
734
+ if native_message[:err] != Rdkafka::Bindings::RD_KAFKA_RESP_ERR_NO_ERROR
550
735
  raise Rdkafka::RdkafkaError.new(native_message[:err])
551
736
  end
552
737
  # Create a message to pass out
@@ -579,37 +764,182 @@ module Rdkafka
579
764
  # @note This method technically should be called `#poll` and the current `#poll` should be
580
765
  # called `#consumer_poll` though we keep the current naming convention to make it backward
581
766
  # compatible.
582
- def events_poll(timeout_ms = 0)
767
+ def events_poll(timeout_ms = Defaults::CONSUMER_EVENTS_POLL_TIMEOUT_MS)
583
768
  @native_kafka.with_inner do |inner|
584
769
  Rdkafka::Bindings.rd_kafka_poll(inner, timeout_ms)
585
770
  end
586
771
  end
587
772
 
773
+ # Polls the main rdkafka queue without releasing the GVL (Global VM Lock).
774
+ #
775
+ # This is more efficient than regular events_poll for non-blocking poll(0) calls,
776
+ # particularly useful in fiber scheduler contexts where GVL release/reacquire
777
+ # overhead is wasteful since we don't expect to wait.
778
+ #
779
+ # @param timeout_ms [Integer] poll timeout (default: 0 for non-blocking)
780
+ # @return [Integer] the number of events served
781
+ #
782
+ # @see #events_poll for more details on when to use this method
783
+ def events_poll_nb(timeout_ms = 0)
784
+ @native_kafka.with_inner do |inner|
785
+ Rdkafka::Bindings.rd_kafka_poll_nb(inner, timeout_ms)
786
+ end
787
+ end
788
+
789
+ # Poll for a batch of messages from the consumer queue in a single FFI call.
790
+ #
791
+ # This is more efficient than calling {#poll} in a loop because it crosses the FFI
792
+ # boundary only once to fetch up to `max_items` messages.
793
+ #
794
+ # The timeout controls how long to wait for the **first** message. Once any message
795
+ # is available, librdkafka fills the buffer with whatever is immediately ready and
796
+ # returns without further waiting.
797
+ #
798
+ # @param timeout_ms [Integer] Timeout waiting for the first message (-1 for infinite)
799
+ # @param max_items [Integer] Maximum number of messages to return per call
800
+ # @return [Array<Message>] Array of messages (empty if none available within timeout)
801
+ # @raise [RdkafkaError] When a consumed message contains an error
802
+ # @raise [ClosedConsumerError] When called on a closed consumer
803
+ def poll_batch(timeout_ms, max_items: 100)
804
+ closed_consumer_check(__method__)
805
+
806
+ buffer = batch_buffer(max_items)
807
+ messages = []
808
+
809
+ count = @native_kafka.with_inner do |_inner|
810
+ Rdkafka::Bindings.rd_kafka_consume_batch_queue(
811
+ consumer_queue,
812
+ timeout_ms,
813
+ buffer,
814
+ max_items
815
+ )
816
+ end
817
+
818
+ return messages if count <= 0
819
+
820
+ i = 0
821
+ begin
822
+ while i < count
823
+ ptr = buffer.get_pointer(i * FFI::Pointer.size)
824
+
825
+ if ptr.null?
826
+ i += 1
827
+ next
828
+ end
829
+
830
+ native_message = Rdkafka::Bindings::Message.new(ptr)
831
+
832
+ if native_message[:err] != Rdkafka::Bindings::RD_KAFKA_RESP_ERR_NO_ERROR
833
+ raise Rdkafka::RdkafkaError.new(native_message[:err])
834
+ end
835
+
836
+ messages << Rdkafka::Consumer::Message.new(native_message)
837
+ Rdkafka::Bindings.rd_kafka_message_destroy(ptr)
838
+ i += 1
839
+ end
840
+ ensure
841
+ while i < count
842
+ ptr = buffer.get_pointer(i * FFI::Pointer.size)
843
+ Rdkafka::Bindings.rd_kafka_message_destroy(ptr) unless ptr.null?
844
+ i += 1
845
+ end
846
+ end
847
+
848
+ messages
849
+ end
850
+
851
+ # Poll for a batch of messages without releasing the GVL (Global VM Lock).
852
+ #
853
+ # This is more efficient than {#poll_batch} for non-blocking poll(0) calls,
854
+ # particularly useful in fiber scheduler contexts where GVL release/reacquire
855
+ # overhead is wasteful since we don't expect to wait.
856
+ #
857
+ # @note Since the GVL is not released, a non-zero timeout_ms will block all Ruby
858
+ # threads/fibers for the duration. Use {#poll_batch} if you need a blocking wait.
859
+ #
860
+ # @param timeout_ms [Integer] Timeout waiting for the first message (default: 0 for non-blocking)
861
+ # @param max_items [Integer] Maximum number of messages to return per call
862
+ # @return [Array<Message>] Array of messages (empty if none available within timeout)
863
+ # @raise [RdkafkaError] When a consumed message contains an error
864
+ # @raise [ClosedConsumerError] When called on a closed consumer
865
+ def poll_batch_nb(timeout_ms = 0, max_items: 100)
866
+ closed_consumer_check(__method__)
867
+
868
+ buffer = batch_buffer(max_items)
869
+ messages = []
870
+
871
+ count = @native_kafka.with_inner do |_inner|
872
+ Rdkafka::Bindings.rd_kafka_consume_batch_queue_nb(
873
+ consumer_queue,
874
+ timeout_ms,
875
+ buffer,
876
+ max_items
877
+ )
878
+ end
879
+
880
+ return messages if count <= 0
881
+
882
+ i = 0
883
+ begin
884
+ while i < count
885
+ ptr = buffer.get_pointer(i * FFI::Pointer.size)
886
+
887
+ if ptr.null?
888
+ i += 1
889
+ next
890
+ end
891
+
892
+ native_message = Rdkafka::Bindings::Message.new(ptr)
893
+
894
+ if native_message[:err] != Rdkafka::Bindings::RD_KAFKA_RESP_ERR_NO_ERROR
895
+ raise Rdkafka::RdkafkaError.new(native_message[:err])
896
+ end
897
+
898
+ messages << Rdkafka::Consumer::Message.new(native_message)
899
+ Rdkafka::Bindings.rd_kafka_message_destroy(ptr)
900
+ i += 1
901
+ end
902
+ ensure
903
+ while i < count
904
+ ptr = buffer.get_pointer(i * FFI::Pointer.size)
905
+ Rdkafka::Bindings.rd_kafka_message_destroy(ptr) unless ptr.null?
906
+ i += 1
907
+ end
908
+ end
909
+
910
+ messages
911
+ end
912
+
588
913
  # Poll for new messages and yield for each received one. Iteration
589
914
  # will end when the consumer is closed.
590
915
  #
591
916
  # If `enable.partition.eof` is turned on in the config this will raise an error when an eof is
592
917
  # reached, so you probably want to disable that when using this method of iteration.
593
918
  #
919
+ # @param timeout_ms [Integer] Timeout for each poll iteration
594
920
  # @yieldparam message [Message] Received message
595
921
  # @return [nil]
596
922
  # @raise [RdkafkaError] When polling fails
597
- def each
923
+ def each(timeout_ms: Defaults::CONSUMER_POLL_TIMEOUT_MS)
598
924
  loop do
599
- message = poll(250)
925
+ message = poll(timeout_ms)
600
926
  if message
601
927
  yield(message)
928
+ elsif closed?
929
+ break
602
930
  else
603
- if closed?
604
- break
605
- else
606
- next
607
- end
931
+ next
608
932
  end
609
933
  end
610
934
  end
611
935
 
612
- # Deprecated. Please read the error message for more details.
936
+ # @deprecated This method has been removed due to data consistency concerns
937
+ # @param max_items [Integer] unused
938
+ # @param bytes_threshold [Numeric] unused
939
+ # @param timeout_ms [Integer] unused
940
+ # @param yield_on_error [Boolean] unused
941
+ # @param block [Proc] unused block
942
+ # @raise [NotImplementedError] Always raises as this method is no longer supported
613
943
  def each_batch(max_items: 100, bytes_threshold: Float::INFINITY, timeout_ms: 250, yield_on_error: false, &block)
614
944
  raise NotImplementedError, <<~ERROR
615
945
  `each_batch` has been removed due to data consistency concerns.
@@ -646,8 +976,30 @@ module Rdkafka
646
976
 
647
977
  private
648
978
 
979
+ # Checks if the consumer is closed and raises an error if so
980
+ # @param method [Symbol] name of the calling method for error context
981
+ # @raise [ClosedConsumerError] when the consumer is closed
649
982
  def closed_consumer_check(method)
650
983
  raise Rdkafka::ClosedConsumerError.new(method) if closed?
651
984
  end
985
+
986
+ # Returns the consumer queue pointer, lazily initialized
987
+ # @return [FFI::Pointer] consumer queue handle
988
+ def consumer_queue
989
+ @consumer_queue ||= @native_kafka.with_inner do |inner|
990
+ Rdkafka::Bindings.rd_kafka_queue_get_consumer(inner)
991
+ end
992
+ end
993
+
994
+ # Returns a reusable FFI buffer for batch polling, growing if needed
995
+ # @param max_items [Integer] minimum buffer capacity
996
+ # @return [FFI::MemoryPointer] pointer buffer
997
+ def batch_buffer(max_items)
998
+ if @batch_buffer.nil? || @batch_buffer_size < max_items
999
+ @batch_buffer = FFI::MemoryPointer.new(:pointer, max_items)
1000
+ @batch_buffer_size = max_items
1001
+ end
1002
+ @batch_buffer
1003
+ end
652
1004
  end
653
1005
  end