karafka-rdkafka 0.20.0.rc3-x86_64-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 (99) hide show
  1. checksums.yaml +7 -0
  2. data/.github/CODEOWNERS +3 -0
  3. data/.github/FUNDING.yml +1 -0
  4. data/.github/workflows/ci_linux_x86_64_gnu.yml +248 -0
  5. data/.github/workflows/ci_macos_arm64.yml +301 -0
  6. data/.github/workflows/push_linux_x86_64_gnu.yml +60 -0
  7. data/.github/workflows/push_ruby.yml +37 -0
  8. data/.github/workflows/verify-action-pins.yml +16 -0
  9. data/.gitignore +15 -0
  10. data/.rspec +2 -0
  11. data/.ruby-gemset +1 -0
  12. data/.ruby-version +1 -0
  13. data/.yardopts +2 -0
  14. data/CHANGELOG.md +323 -0
  15. data/Gemfile +5 -0
  16. data/MIT-LICENSE +22 -0
  17. data/README.md +177 -0
  18. data/Rakefile +96 -0
  19. data/docker-compose.yml +25 -0
  20. data/ext/README.md +19 -0
  21. data/ext/Rakefile +131 -0
  22. data/ext/build_common.sh +361 -0
  23. data/ext/build_linux_x86_64_gnu.sh +306 -0
  24. data/ext/build_macos_arm64.sh +550 -0
  25. data/ext/librdkafka.so +0 -0
  26. data/karafka-rdkafka.gemspec +61 -0
  27. data/lib/rdkafka/abstract_handle.rb +116 -0
  28. data/lib/rdkafka/admin/acl_binding_result.rb +51 -0
  29. data/lib/rdkafka/admin/config_binding_result.rb +30 -0
  30. data/lib/rdkafka/admin/config_resource_binding_result.rb +18 -0
  31. data/lib/rdkafka/admin/create_acl_handle.rb +28 -0
  32. data/lib/rdkafka/admin/create_acl_report.rb +24 -0
  33. data/lib/rdkafka/admin/create_partitions_handle.rb +30 -0
  34. data/lib/rdkafka/admin/create_partitions_report.rb +6 -0
  35. data/lib/rdkafka/admin/create_topic_handle.rb +32 -0
  36. data/lib/rdkafka/admin/create_topic_report.rb +24 -0
  37. data/lib/rdkafka/admin/delete_acl_handle.rb +30 -0
  38. data/lib/rdkafka/admin/delete_acl_report.rb +23 -0
  39. data/lib/rdkafka/admin/delete_groups_handle.rb +28 -0
  40. data/lib/rdkafka/admin/delete_groups_report.rb +24 -0
  41. data/lib/rdkafka/admin/delete_topic_handle.rb +32 -0
  42. data/lib/rdkafka/admin/delete_topic_report.rb +24 -0
  43. data/lib/rdkafka/admin/describe_acl_handle.rb +30 -0
  44. data/lib/rdkafka/admin/describe_acl_report.rb +24 -0
  45. data/lib/rdkafka/admin/describe_configs_handle.rb +33 -0
  46. data/lib/rdkafka/admin/describe_configs_report.rb +48 -0
  47. data/lib/rdkafka/admin/incremental_alter_configs_handle.rb +33 -0
  48. data/lib/rdkafka/admin/incremental_alter_configs_report.rb +48 -0
  49. data/lib/rdkafka/admin.rb +832 -0
  50. data/lib/rdkafka/bindings.rb +582 -0
  51. data/lib/rdkafka/callbacks.rb +415 -0
  52. data/lib/rdkafka/config.rb +398 -0
  53. data/lib/rdkafka/consumer/headers.rb +79 -0
  54. data/lib/rdkafka/consumer/message.rb +86 -0
  55. data/lib/rdkafka/consumer/partition.rb +57 -0
  56. data/lib/rdkafka/consumer/topic_partition_list.rb +190 -0
  57. data/lib/rdkafka/consumer.rb +663 -0
  58. data/lib/rdkafka/error.rb +201 -0
  59. data/lib/rdkafka/helpers/oauth.rb +58 -0
  60. data/lib/rdkafka/helpers/time.rb +14 -0
  61. data/lib/rdkafka/metadata.rb +115 -0
  62. data/lib/rdkafka/native_kafka.rb +139 -0
  63. data/lib/rdkafka/producer/delivery_handle.rb +48 -0
  64. data/lib/rdkafka/producer/delivery_report.rb +45 -0
  65. data/lib/rdkafka/producer/partitions_count_cache.rb +216 -0
  66. data/lib/rdkafka/producer.rb +492 -0
  67. data/lib/rdkafka/version.rb +7 -0
  68. data/lib/rdkafka.rb +54 -0
  69. data/renovate.json +92 -0
  70. data/spec/rdkafka/abstract_handle_spec.rb +117 -0
  71. data/spec/rdkafka/admin/create_acl_handle_spec.rb +56 -0
  72. data/spec/rdkafka/admin/create_acl_report_spec.rb +18 -0
  73. data/spec/rdkafka/admin/create_topic_handle_spec.rb +54 -0
  74. data/spec/rdkafka/admin/create_topic_report_spec.rb +16 -0
  75. data/spec/rdkafka/admin/delete_acl_handle_spec.rb +85 -0
  76. data/spec/rdkafka/admin/delete_acl_report_spec.rb +72 -0
  77. data/spec/rdkafka/admin/delete_topic_handle_spec.rb +54 -0
  78. data/spec/rdkafka/admin/delete_topic_report_spec.rb +16 -0
  79. data/spec/rdkafka/admin/describe_acl_handle_spec.rb +85 -0
  80. data/spec/rdkafka/admin/describe_acl_report_spec.rb +73 -0
  81. data/spec/rdkafka/admin_spec.rb +769 -0
  82. data/spec/rdkafka/bindings_spec.rb +222 -0
  83. data/spec/rdkafka/callbacks_spec.rb +20 -0
  84. data/spec/rdkafka/config_spec.rb +258 -0
  85. data/spec/rdkafka/consumer/headers_spec.rb +73 -0
  86. data/spec/rdkafka/consumer/message_spec.rb +139 -0
  87. data/spec/rdkafka/consumer/partition_spec.rb +57 -0
  88. data/spec/rdkafka/consumer/topic_partition_list_spec.rb +248 -0
  89. data/spec/rdkafka/consumer_spec.rb +1299 -0
  90. data/spec/rdkafka/error_spec.rb +95 -0
  91. data/spec/rdkafka/metadata_spec.rb +79 -0
  92. data/spec/rdkafka/native_kafka_spec.rb +130 -0
  93. data/spec/rdkafka/producer/delivery_handle_spec.rb +60 -0
  94. data/spec/rdkafka/producer/delivery_report_spec.rb +25 -0
  95. data/spec/rdkafka/producer/partitions_count_cache_spec.rb +359 -0
  96. data/spec/rdkafka/producer/partitions_count_spec.rb +359 -0
  97. data/spec/rdkafka/producer_spec.rb +1234 -0
  98. data/spec/spec_helper.rb +181 -0
  99. metadata +244 -0
@@ -0,0 +1,663 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Rdkafka
4
+ # A consumer of Kafka messages. It uses the high-level consumer approach where the Kafka
5
+ # brokers automatically assign partitions and load balance partitions over consumers that
6
+ # have the same `:"group.id"` set in their configuration.
7
+ #
8
+ # To create a consumer set up a {Config} and call {Config#consumer consumer} on that. It is
9
+ # mandatory to set `:"group.id"` in the configuration.
10
+ #
11
+ # Consumer implements `Enumerable`, so you can use `each` to consume messages, or for example
12
+ # `each_slice` to consume batches of messages.
13
+ class Consumer
14
+ include Enumerable
15
+ include Helpers::Time
16
+ include Helpers::OAuth
17
+
18
+ # @private
19
+ def initialize(native_kafka)
20
+ @native_kafka = native_kafka
21
+ end
22
+
23
+ # Starts the native Kafka polling thread and kicks off the init polling
24
+ # @note Not needed to run unless explicit start was disabled
25
+ def start
26
+ @native_kafka.start
27
+ end
28
+
29
+ # @return [String] consumer name
30
+ def name
31
+ @name ||= @native_kafka.with_inner do |inner|
32
+ ::Rdkafka::Bindings.rd_kafka_name(inner)
33
+ end
34
+ end
35
+
36
+ def finalizer
37
+ ->(_) { close }
38
+ end
39
+
40
+ # Close this consumer
41
+ # @return [nil]
42
+ def close
43
+ return if closed?
44
+ ObjectSpace.undefine_finalizer(self)
45
+
46
+ @native_kafka.synchronize do |inner|
47
+ Rdkafka::Bindings.rd_kafka_consumer_close(inner)
48
+ end
49
+
50
+ @native_kafka.close
51
+ end
52
+
53
+ # Whether this consumer has closed
54
+ def closed?
55
+ @native_kafka.closed?
56
+ end
57
+
58
+ # Subscribes to one or more topics letting Kafka handle partition assignments.
59
+ #
60
+ # @param topics [Array<String>] One or more topic names
61
+ # @return [nil]
62
+ # @raise [RdkafkaError] When subscribing fails
63
+ def subscribe(*topics)
64
+ closed_consumer_check(__method__)
65
+
66
+ # Create topic partition list with topics and no partition set
67
+ tpl = Rdkafka::Bindings.rd_kafka_topic_partition_list_new(topics.length)
68
+
69
+ topics.each do |topic|
70
+ Rdkafka::Bindings.rd_kafka_topic_partition_list_add(tpl, topic, -1)
71
+ end
72
+
73
+ # Subscribe to topic partition list and check this was successful
74
+ response = @native_kafka.with_inner do |inner|
75
+ Rdkafka::Bindings.rd_kafka_subscribe(inner, tpl)
76
+ end
77
+
78
+ Rdkafka::RdkafkaError.validate!(response, "Error subscribing to '#{topics.join(', ')}'")
79
+ ensure
80
+ Rdkafka::Bindings.rd_kafka_topic_partition_list_destroy(tpl) unless tpl.nil?
81
+ end
82
+
83
+ # Unsubscribe from all subscribed topics.
84
+ #
85
+ # @return [nil]
86
+ # @raise [RdkafkaError] When unsubscribing fails
87
+ def unsubscribe
88
+ closed_consumer_check(__method__)
89
+
90
+ response = @native_kafka.with_inner do |inner|
91
+ Rdkafka::Bindings.rd_kafka_unsubscribe(inner)
92
+ end
93
+
94
+ Rdkafka::RdkafkaError.validate!(response)
95
+
96
+ nil
97
+ end
98
+
99
+ # Pause producing or consumption for the provided list of partitions
100
+ #
101
+ # @param list [TopicPartitionList] The topic with partitions to pause
102
+ # @return [nil]
103
+ # @raise [RdkafkaTopicPartitionListError] When pausing subscription fails.
104
+ def pause(list)
105
+ closed_consumer_check(__method__)
106
+
107
+ unless list.is_a?(TopicPartitionList)
108
+ raise TypeError.new("list has to be a TopicPartitionList")
109
+ end
110
+
111
+ tpl = list.to_native_tpl
112
+
113
+ begin
114
+ response = @native_kafka.with_inner do |inner|
115
+ Rdkafka::Bindings.rd_kafka_pause_partitions(inner, tpl)
116
+ end
117
+
118
+ if response != 0
119
+ list = TopicPartitionList.from_native_tpl(tpl)
120
+ raise Rdkafka::RdkafkaTopicPartitionListError.new(response, list, "Error pausing '#{list.to_h}'")
121
+ end
122
+ ensure
123
+ Rdkafka::Bindings.rd_kafka_topic_partition_list_destroy(tpl)
124
+ end
125
+ end
126
+
127
+ # Resumes producing consumption for the provided list of partitions
128
+ #
129
+ # @param list [TopicPartitionList] The topic with partitions to pause
130
+ # @return [nil]
131
+ # @raise [RdkafkaError] When resume subscription fails.
132
+ def resume(list)
133
+ closed_consumer_check(__method__)
134
+
135
+ unless list.is_a?(TopicPartitionList)
136
+ raise TypeError.new("list has to be a TopicPartitionList")
137
+ end
138
+
139
+ tpl = list.to_native_tpl
140
+
141
+ begin
142
+ response = @native_kafka.with_inner do |inner|
143
+ Rdkafka::Bindings.rd_kafka_resume_partitions(inner, tpl)
144
+ end
145
+
146
+ Rdkafka::RdkafkaError.validate!(response, "Error resume '#{list.to_h}'")
147
+
148
+ nil
149
+ ensure
150
+ Rdkafka::Bindings.rd_kafka_topic_partition_list_destroy(tpl)
151
+ end
152
+ end
153
+
154
+ # Returns the current subscription to topics and partitions
155
+ #
156
+ # @return [TopicPartitionList]
157
+ # @raise [RdkafkaError] When getting the subscription fails.
158
+ def subscription
159
+ closed_consumer_check(__method__)
160
+
161
+ ptr = FFI::MemoryPointer.new(:pointer)
162
+ response = @native_kafka.with_inner do |inner|
163
+ Rdkafka::Bindings.rd_kafka_subscription(inner, ptr)
164
+ end
165
+
166
+ Rdkafka::RdkafkaError.validate!(response)
167
+
168
+ native = ptr.read_pointer
169
+
170
+ begin
171
+ Rdkafka::Consumer::TopicPartitionList.from_native_tpl(native)
172
+ ensure
173
+ Rdkafka::Bindings.rd_kafka_topic_partition_list_destroy(native)
174
+ end
175
+ end
176
+
177
+ # Atomic assignment of partitions to consume
178
+ #
179
+ # @param list [TopicPartitionList] The topic with partitions to assign
180
+ # @raise [RdkafkaError] When assigning fails
181
+ def assign(list)
182
+ closed_consumer_check(__method__)
183
+
184
+ unless list.is_a?(TopicPartitionList)
185
+ raise TypeError.new("list has to be a TopicPartitionList")
186
+ end
187
+
188
+ tpl = list.to_native_tpl
189
+
190
+ begin
191
+ response = @native_kafka.with_inner do |inner|
192
+ Rdkafka::Bindings.rd_kafka_assign(inner, tpl)
193
+ end
194
+
195
+ Rdkafka::RdkafkaError.validate!(response, "Error assigning '#{list.to_h}'")
196
+ ensure
197
+ Rdkafka::Bindings.rd_kafka_topic_partition_list_destroy(tpl)
198
+ end
199
+ end
200
+
201
+ # Returns the current partition assignment.
202
+ #
203
+ # @return [TopicPartitionList]
204
+ # @raise [RdkafkaError] When getting the assignment fails.
205
+ def assignment
206
+ closed_consumer_check(__method__)
207
+
208
+ ptr = FFI::MemoryPointer.new(:pointer)
209
+ response = @native_kafka.with_inner do |inner|
210
+ Rdkafka::Bindings.rd_kafka_assignment(inner, ptr)
211
+ end
212
+
213
+ Rdkafka::RdkafkaError.validate!(response)
214
+
215
+ tpl = ptr.read_pointer
216
+
217
+ if !tpl.null?
218
+ begin
219
+ Rdkafka::Consumer::TopicPartitionList.from_native_tpl(tpl)
220
+ ensure
221
+ Rdkafka::Bindings.rd_kafka_topic_partition_list_destroy tpl
222
+ end
223
+ end
224
+ ensure
225
+ ptr.free unless ptr.nil?
226
+ end
227
+
228
+ # @return [Boolean] true if our current assignment has been lost involuntarily.
229
+ def assignment_lost?
230
+ closed_consumer_check(__method__)
231
+
232
+ @native_kafka.with_inner do |inner|
233
+ !Rdkafka::Bindings.rd_kafka_assignment_lost(inner).zero?
234
+ end
235
+ end
236
+
237
+ # Return the current committed offset per partition for this consumer group.
238
+ # The offset field of each requested partition will either be set to stored offset or to -1001
239
+ # in case there was no stored offset for that partition.
240
+ #
241
+ # @param list [TopicPartitionList, nil] The topic with partitions to get the offsets for or nil
242
+ # to use the current subscription.
243
+ # @param timeout_ms [Integer] The timeout for fetching this information.
244
+ # @return [TopicPartitionList]
245
+ # @raise [RdkafkaError] When getting the committed positions fails.
246
+ def committed(list=nil, timeout_ms=2_000)
247
+ closed_consumer_check(__method__)
248
+
249
+ if list.nil?
250
+ list = assignment
251
+ elsif !list.is_a?(TopicPartitionList)
252
+ raise TypeError.new("list has to be nil or a TopicPartitionList")
253
+ end
254
+
255
+ tpl = list.to_native_tpl
256
+
257
+ begin
258
+ response = @native_kafka.with_inner do |inner|
259
+ Rdkafka::Bindings.rd_kafka_committed(inner, tpl, timeout_ms)
260
+ end
261
+
262
+ Rdkafka::RdkafkaError.validate!(response)
263
+
264
+ TopicPartitionList.from_native_tpl(tpl)
265
+ ensure
266
+ Rdkafka::Bindings.rd_kafka_topic_partition_list_destroy(tpl)
267
+ end
268
+ end
269
+
270
+ # Return the current positions (offsets) for topics and partitions.
271
+ # The offset field of each requested partition will be set to the offset of the last consumed message + 1, or nil in case there was no previous message.
272
+ #
273
+ # @param list [TopicPartitionList, nil] The topic with partitions to get the offsets for or nil to use the current subscription.
274
+ #
275
+ # @return [TopicPartitionList]
276
+ #
277
+ # @raise [RdkafkaError] When getting the positions fails.
278
+ def position(list=nil)
279
+ if list.nil?
280
+ list = assignment
281
+ elsif !list.is_a?(TopicPartitionList)
282
+ raise TypeError.new("list has to be nil or a TopicPartitionList")
283
+ end
284
+
285
+ tpl = list.to_native_tpl
286
+
287
+ response = @native_kafka.with_inner do |inner|
288
+ Rdkafka::Bindings.rd_kafka_position(inner, tpl)
289
+ end
290
+
291
+ Rdkafka::RdkafkaError.validate!(response)
292
+
293
+ TopicPartitionList.from_native_tpl(tpl)
294
+ end
295
+
296
+ # Query broker for low (oldest/beginning) and high (newest/end) offsets for a partition.
297
+ #
298
+ # @param topic [String] The topic to query
299
+ # @param partition [Integer] The partition to query
300
+ # @param timeout_ms [Integer] The timeout for querying the broker
301
+ # @return [Integer] The low and high watermark
302
+ # @raise [RdkafkaError] When querying the broker fails.
303
+ def query_watermark_offsets(topic, partition, timeout_ms=1000)
304
+ closed_consumer_check(__method__)
305
+
306
+ low = FFI::MemoryPointer.new(:int64, 1)
307
+ high = FFI::MemoryPointer.new(:int64, 1)
308
+
309
+ response = @native_kafka.with_inner do |inner|
310
+ Rdkafka::Bindings.rd_kafka_query_watermark_offsets(
311
+ inner,
312
+ topic,
313
+ partition,
314
+ low,
315
+ high,
316
+ timeout_ms,
317
+ )
318
+ end
319
+
320
+ Rdkafka::RdkafkaError.validate!(response, "Error querying watermark offsets for partition #{partition} of #{topic}")
321
+
322
+ return low.read_array_of_int64(1).first, high.read_array_of_int64(1).first
323
+ ensure
324
+ low.free unless low.nil?
325
+ high.free unless high.nil?
326
+ end
327
+
328
+ # Calculate the consumer lag per partition for the provided topic partition list.
329
+ # You can get a suitable list by calling {committed} or {position} (TODO). It is also
330
+ # possible to create one yourself, in this case you have to provide a list that
331
+ # already contains all the partitions you need the lag for.
332
+ #
333
+ # @param topic_partition_list [TopicPartitionList] The list to calculate lag for.
334
+ # @param watermark_timeout_ms [Integer] The timeout for each query watermark call.
335
+ # @return [Hash<String, Hash<Integer, Integer>>] A hash containing all topics with the lag
336
+ # per partition
337
+ # @raise [RdkafkaError] When querying the broker fails.
338
+ def lag(topic_partition_list, watermark_timeout_ms=1000)
339
+ out = {}
340
+
341
+ topic_partition_list.to_h.each do |topic, partitions|
342
+ # Query high watermarks for this topic's partitions
343
+ # and compare to the offset in the list.
344
+ topic_out = {}
345
+ partitions.each do |p|
346
+ next if p.offset.nil?
347
+ _, high = query_watermark_offsets(
348
+ topic,
349
+ p.partition,
350
+ watermark_timeout_ms
351
+ )
352
+ topic_out[p.partition] = high - p.offset
353
+ end
354
+ out[topic] = topic_out
355
+ end
356
+ out
357
+ end
358
+
359
+ # Returns the ClusterId as reported in broker metadata.
360
+ #
361
+ # @return [String, nil]
362
+ def cluster_id
363
+ closed_consumer_check(__method__)
364
+ @native_kafka.with_inner do |inner|
365
+ Rdkafka::Bindings.rd_kafka_clusterid(inner)
366
+ end
367
+ end
368
+
369
+ # Returns this client's broker-assigned group member id
370
+ #
371
+ # This currently requires the high-level KafkaConsumer
372
+ #
373
+ # @return [String, nil]
374
+ def member_id
375
+ closed_consumer_check(__method__)
376
+ @native_kafka.with_inner do |inner|
377
+ Rdkafka::Bindings.rd_kafka_memberid(inner)
378
+ end
379
+ end
380
+
381
+ # Store offset of a message to be used in the next commit of this consumer
382
+ #
383
+ # When using this `enable.auto.offset.store` should be set to `false` in the config.
384
+ #
385
+ # @param message [Rdkafka::Consumer::Message] The message which offset will be stored
386
+ # @param metadata [String, nil] commit metadata string or nil if none
387
+ # @return [nil]
388
+ # @raise [RdkafkaError] When storing the offset fails
389
+ def store_offset(message, metadata = nil)
390
+ closed_consumer_check(__method__)
391
+
392
+ list = TopicPartitionList.new
393
+
394
+ # For metadata aware commits we build the partition reference directly to save on
395
+ # objects allocations
396
+ if metadata
397
+ list.add_topic_and_partitions_with_offsets(
398
+ message.topic,
399
+ [
400
+ Consumer::Partition.new(
401
+ message.partition,
402
+ message.offset + 1,
403
+ 0,
404
+ metadata
405
+ )
406
+ ]
407
+ )
408
+ else
409
+ list.add_topic_and_partitions_with_offsets(
410
+ message.topic,
411
+ message.partition => message.offset + 1
412
+ )
413
+ end
414
+
415
+ tpl = list.to_native_tpl
416
+
417
+ response = @native_kafka.with_inner do |inner|
418
+ Rdkafka::Bindings.rd_kafka_offsets_store(
419
+ inner,
420
+ tpl
421
+ )
422
+ end
423
+
424
+ Rdkafka::RdkafkaError.validate!(response)
425
+
426
+ nil
427
+ ensure
428
+ Rdkafka::Bindings.rd_kafka_topic_partition_list_destroy(tpl) if tpl
429
+ end
430
+
431
+ # Seek to a particular message. The next poll on the topic/partition will return the
432
+ # message at the given offset.
433
+ #
434
+ # @param message [Rdkafka::Consumer::Message] The message to which to seek
435
+ # @return [nil]
436
+ # @raise [RdkafkaError] When seeking fails
437
+ def seek(message)
438
+ seek_by(message.topic, message.partition, message.offset)
439
+ end
440
+
441
+ # Seek to a particular message by providing the topic, partition and offset.
442
+ # The next poll on the topic/partition will return the
443
+ # message at the given offset.
444
+ #
445
+ # @param topic [String] The topic in which to seek
446
+ # @param partition [Integer] The partition number to seek
447
+ # @param offset [Integer] The partition offset to seek
448
+ # @return [nil]
449
+ # @raise [RdkafkaError] When seeking fails
450
+ def seek_by(topic, partition, offset)
451
+ closed_consumer_check(__method__)
452
+
453
+ # rd_kafka_offset_store is one of the few calls that does not support
454
+ # a string as the topic, so create a native topic for it.
455
+ native_topic = @native_kafka.with_inner do |inner|
456
+ Rdkafka::Bindings.rd_kafka_topic_new(
457
+ inner,
458
+ topic,
459
+ nil
460
+ )
461
+ end
462
+ response = Rdkafka::Bindings.rd_kafka_seek(
463
+ native_topic,
464
+ partition,
465
+ offset,
466
+ 0 # timeout
467
+ )
468
+ Rdkafka::RdkafkaError.validate!(response)
469
+
470
+ nil
471
+ ensure
472
+ if native_topic && !native_topic.null?
473
+ Rdkafka::Bindings.rd_kafka_topic_destroy(native_topic)
474
+ end
475
+ end
476
+
477
+ # Lookup offset for the given partitions by timestamp.
478
+ #
479
+ # @param list [TopicPartitionList] The TopicPartitionList with timestamps instead of offsets
480
+ #
481
+ # @return [TopicPartitionList]
482
+ #
483
+ # @raise [RdKafkaError] When the OffsetForTimes lookup fails
484
+ def offsets_for_times(list, timeout_ms = 1000)
485
+ closed_consumer_check(__method__)
486
+
487
+ if !list.is_a?(TopicPartitionList)
488
+ raise TypeError.new("list has to be a TopicPartitionList")
489
+ end
490
+
491
+ tpl = list.to_native_tpl
492
+
493
+ response = @native_kafka.with_inner do |inner|
494
+ Rdkafka::Bindings.rd_kafka_offsets_for_times(
495
+ inner,
496
+ tpl,
497
+ timeout_ms # timeout
498
+ )
499
+ end
500
+
501
+ Rdkafka::RdkafkaError.validate!(response)
502
+
503
+ TopicPartitionList.from_native_tpl(tpl)
504
+ ensure
505
+ Rdkafka::Bindings.rd_kafka_topic_partition_list_destroy(tpl) if tpl
506
+ end
507
+
508
+ # Manually commit the current offsets of this consumer.
509
+ #
510
+ # To use this set `enable.auto.commit`to `false` to disable automatic triggering
511
+ # of commits.
512
+ #
513
+ # If `enable.auto.offset.store` is set to `true` the offset of the last consumed
514
+ # message for every partition is used. If set to `false` you can use {store_offset} to
515
+ # indicate when a message has been fully processed.
516
+ #
517
+ # @param list [TopicPartitionList,nil] The topic with partitions to commit
518
+ # @param async [Boolean] Whether to commit async or wait for the commit to finish
519
+ # @return [nil]
520
+ # @raise [RdkafkaError] When committing fails
521
+ def commit(list=nil, async=false)
522
+ closed_consumer_check(__method__)
523
+
524
+ if !list.nil? && !list.is_a?(TopicPartitionList)
525
+ raise TypeError.new("list has to be nil or a TopicPartitionList")
526
+ end
527
+
528
+ tpl = list ? list.to_native_tpl : nil
529
+
530
+ begin
531
+ response = @native_kafka.with_inner do |inner|
532
+ Rdkafka::Bindings.rd_kafka_commit(inner, tpl, async)
533
+ end
534
+
535
+ Rdkafka::RdkafkaError.validate!(response)
536
+
537
+ nil
538
+ ensure
539
+ Rdkafka::Bindings.rd_kafka_topic_partition_list_destroy(tpl) if tpl
540
+ end
541
+ end
542
+
543
+ # Poll for the next message on one of the subscribed topics
544
+ #
545
+ # @param timeout_ms [Integer] Timeout of this poll
546
+ # @return [Message, nil] A message or nil if there was no new message within the timeout
547
+ # @raise [RdkafkaError] When polling fails
548
+ def poll(timeout_ms)
549
+ closed_consumer_check(__method__)
550
+
551
+ message_ptr = @native_kafka.with_inner do |inner|
552
+ Rdkafka::Bindings.rd_kafka_consumer_poll(inner, timeout_ms)
553
+ end
554
+
555
+ return nil if message_ptr.null?
556
+
557
+ # Create struct wrapper
558
+ native_message = Rdkafka::Bindings::Message.new(message_ptr)
559
+
560
+ # Create a message to pass out
561
+ return Rdkafka::Consumer::Message.new(native_message) if native_message[:err].zero?
562
+
563
+ # Raise error if needed
564
+ Rdkafka::RdkafkaError.validate!(native_message)
565
+ ensure
566
+ # Clean up rdkafka message if there is one
567
+ if message_ptr && !message_ptr.null?
568
+ Rdkafka::Bindings.rd_kafka_message_destroy(message_ptr)
569
+ end
570
+ end
571
+
572
+ # Polls the main rdkafka queue (not the consumer one). Do **NOT** use it if `consumer_poll_set`
573
+ # was set to `true`.
574
+ #
575
+ # Events will cause application-provided callbacks to be called.
576
+ #
577
+ # Events (in the context of the consumer):
578
+ # - error callbacks
579
+ # - stats callbacks
580
+ # - any other callbacks supported by librdkafka that are not part of the consumer_poll, that
581
+ # would have a callback configured and activated.
582
+ #
583
+ # This method needs to be called at regular intervals to serve any queued callbacks waiting to
584
+ # be called. When in use, does **NOT** replace `#poll` but needs to run complementary with it.
585
+ #
586
+ # @param timeout_ms [Integer] poll timeout. If set to 0 will run async, when set to -1 will
587
+ # block until any events available.
588
+ #
589
+ # @note This method technically should be called `#poll` and the current `#poll` should be
590
+ # called `#consumer_poll` though we keep the current naming convention to make it backward
591
+ # compatible.
592
+ def events_poll(timeout_ms = 0)
593
+ @native_kafka.with_inner do |inner|
594
+ Rdkafka::Bindings.rd_kafka_poll(inner, timeout_ms)
595
+ end
596
+ end
597
+
598
+ # Poll for new messages and yield for each received one. Iteration
599
+ # will end when the consumer is closed.
600
+ #
601
+ # If `enable.partition.eof` is turned on in the config this will raise an error when an eof is
602
+ # reached, so you probably want to disable that when using this method of iteration.
603
+ #
604
+ # @yieldparam message [Message] Received message
605
+ # @return [nil]
606
+ # @raise [RdkafkaError] When polling fails
607
+ def each
608
+ loop do
609
+ message = poll(250)
610
+ if message
611
+ yield(message)
612
+ else
613
+ if closed?
614
+ break
615
+ else
616
+ next
617
+ end
618
+ end
619
+ end
620
+ end
621
+
622
+ # Deprecated. Please read the error message for more details.
623
+ def each_batch(max_items: 100, bytes_threshold: Float::INFINITY, timeout_ms: 250, yield_on_error: false, &block)
624
+ raise NotImplementedError, <<~ERROR
625
+ `each_batch` has been removed due to data consistency concerns.
626
+
627
+ This method was removed because it did not properly handle partition reassignments,
628
+ which could lead to processing messages from partitions that were no longer owned
629
+ by this consumer, resulting in duplicate message processing and data inconsistencies.
630
+
631
+ Recommended alternatives:
632
+
633
+ 1. Implement your own batching logic using rebalance callbacks to properly handle
634
+ partition revocations and ensure message processing correctness.
635
+
636
+ 2. Use a high-level batching library that supports proper partition reassignment
637
+ handling out of the box (such as the Karafka framework).
638
+ ERROR
639
+ end
640
+
641
+ # Returns pointer to the consumer group metadata. It is used only in the context of
642
+ # exactly-once-semantics in transactions, this is why it is never remapped to Ruby
643
+ #
644
+ # This API is **not** usable by itself from Ruby
645
+ #
646
+ # @note This pointer **needs** to be removed with `#rd_kafka_consumer_group_metadata_destroy`
647
+ #
648
+ # @private
649
+ def consumer_group_metadata_pointer
650
+ closed_consumer_check(__method__)
651
+
652
+ @native_kafka.with_inner do |inner|
653
+ Bindings.rd_kafka_consumer_group_metadata(inner)
654
+ end
655
+ end
656
+
657
+ private
658
+
659
+ def closed_consumer_check(method)
660
+ raise Rdkafka::ClosedConsumerError.new(method) if closed?
661
+ end
662
+ end
663
+ end