rdkafka 0.28.0-aarch64-linux-gnu → 0.29.1-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 (66) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +42 -0
  3. data/README.md +3 -2
  4. data/bin/verify_kafka_warnings +5 -0
  5. data/docker-compose-ssl.yml +4 -1
  6. data/docker-compose.yml +4 -1
  7. data/ext/librdkafka.so +0 -0
  8. data/lib/rdkafka/abstract_handle.rb +31 -2
  9. data/lib/rdkafka/admin/config_binding_result.rb +2 -2
  10. data/lib/rdkafka/admin/create_acl_handle.rb +6 -5
  11. data/lib/rdkafka/admin/create_acl_report.rb +2 -1
  12. data/lib/rdkafka/admin/create_partitions_handle.rb +4 -6
  13. data/lib/rdkafka/admin/create_topic_handle.rb +4 -6
  14. data/lib/rdkafka/admin/delete_acl_handle.rb +5 -7
  15. data/lib/rdkafka/admin/delete_groups_handle.rb +4 -6
  16. data/lib/rdkafka/admin/delete_records_handle.rb +31 -0
  17. data/lib/rdkafka/admin/delete_records_report.rb +25 -0
  18. data/lib/rdkafka/admin/delete_topic_handle.rb +4 -6
  19. data/lib/rdkafka/admin/describe_acl_handle.rb +5 -7
  20. data/lib/rdkafka/admin/describe_acl_report.rb +2 -1
  21. data/lib/rdkafka/admin/describe_configs_handle.rb +4 -10
  22. data/lib/rdkafka/admin/describe_configs_report.rb +1 -5
  23. data/lib/rdkafka/admin/incremental_alter_configs_handle.rb +4 -10
  24. data/lib/rdkafka/admin/incremental_alter_configs_report.rb +1 -5
  25. data/lib/rdkafka/admin/list_consumer_groups_handle.rb +31 -0
  26. data/lib/rdkafka/admin/list_consumer_groups_report.rb +83 -0
  27. data/lib/rdkafka/admin/list_offsets_handle.rb +5 -10
  28. data/lib/rdkafka/admin/list_offsets_report.rb +5 -2
  29. data/lib/rdkafka/admin.rb +238 -145
  30. data/lib/rdkafka/bindings.rb +61 -16
  31. data/lib/rdkafka/callbacks/base_handler.rb +62 -0
  32. data/lib/rdkafka/callbacks/create_acl_handler.rb +37 -0
  33. data/lib/rdkafka/callbacks/create_partitions_handler.rb +37 -0
  34. data/lib/rdkafka/callbacks/create_topic_handler.rb +37 -0
  35. data/lib/rdkafka/callbacks/delete_acl_handler.rb +42 -0
  36. data/lib/rdkafka/callbacks/delete_groups_handler.rb +37 -0
  37. data/lib/rdkafka/callbacks/delete_records_handler.rb +38 -0
  38. data/lib/rdkafka/callbacks/delete_topic_handler.rb +37 -0
  39. data/lib/rdkafka/callbacks/describe_acl_handler.rb +35 -0
  40. data/lib/rdkafka/callbacks/describe_configs_handler.rb +42 -0
  41. data/lib/rdkafka/callbacks/incremental_alter_configs_handler.rb +42 -0
  42. data/lib/rdkafka/callbacks/list_consumer_groups_handler.rb +39 -0
  43. data/lib/rdkafka/callbacks/list_offsets_handler.rb +42 -0
  44. data/lib/rdkafka/callbacks.rb +54 -246
  45. data/lib/rdkafka/config.rb +69 -50
  46. data/lib/rdkafka/consumer/headers.rb +19 -5
  47. data/lib/rdkafka/consumer/partition.rb +8 -1
  48. data/lib/rdkafka/consumer/topic_partition_list.rb +58 -30
  49. data/lib/rdkafka/consumer.rb +211 -52
  50. data/lib/rdkafka/defaults.rb +24 -7
  51. data/lib/rdkafka/helpers/list_offsets.rb +127 -0
  52. data/lib/rdkafka/helpers/metadata.rb +29 -0
  53. data/lib/rdkafka/helpers/oauth.rb +10 -5
  54. data/lib/rdkafka/metadata.rb +86 -19
  55. data/lib/rdkafka/native_kafka.rb +22 -11
  56. data/lib/rdkafka/producer/delivery_handle.rb +4 -5
  57. data/lib/rdkafka/producer/partitions_count_cache.rb +24 -38
  58. data/lib/rdkafka/producer.rb +81 -61
  59. data/lib/rdkafka/version.rb +3 -3
  60. data/lib/rdkafka.rb +19 -0
  61. data/package-lock.json +3 -3
  62. data/rdkafka.gemspec +1 -0
  63. metadata +21 -5
  64. data/Gemfile +0 -13
  65. data/Gemfile.lint +0 -14
  66. data/Gemfile.lint.lock +0 -123
@@ -6,7 +6,8 @@ module Rdkafka
6
6
  class TopicPartitionList
7
7
  # Create a topic partition list.
8
8
  #
9
- # @param data [Hash{String => nil,Partition}] The topic and partition data or nil to create an empty list
9
+ # @param data [Hash{String => nil,Partition}] The topic and partition data or nil to create an
10
+ # empty list
10
11
  #
11
12
  # @return [TopicPartitionList]
12
13
  def initialize(data = nil)
@@ -34,10 +35,12 @@ module Rdkafka
34
35
  end
35
36
 
36
37
  # Add a topic with optionally partitions to the list.
37
- # Calling this method multiple times for the same topic will overwrite the previous configuraton.
38
+ # Calling this method multiple times for the same topic will overwrite the previous
39
+ # configuraton.
38
40
  #
39
41
  # @param topic [String] The topic's name
40
- # @param partitions [Array<Integer>, Range<Integer>, Integer] The topic's partitions or partition count
42
+ # @param partitions [Array<Integer>, Range<Integer>, Integer] The topic's partitions or
43
+ # partition count
41
44
  #
42
45
  # @return [nil]
43
46
  #
@@ -61,11 +64,12 @@ module Rdkafka
61
64
  end
62
65
 
63
66
  # Add a topic with partitions and offsets set to the list
64
- # Calling this method multiple times for the same topic will overwrite the previous configuraton.
67
+ # Calling this method multiple times for the same topic will overwrite the previous
68
+ # configuraton.
65
69
  #
66
70
  # @param topic [String] The topic's name
67
- # @param partitions_with_offsets [Hash{Integer => Integer}, Array<Consumer::Partition>] The topic's
68
- # partitions and offsets (Hash) or partitions with offsets and metadata (Array)
71
+ # @param partitions_with_offsets [Hash{Integer => Integer}, Array<Consumer::Partition>] The
72
+ # topic's partitions and offsets (Hash) or partitions with offsets and metadata (Array)
69
73
  # @return [nil]
70
74
  def add_topic_and_partitions_with_offsets(topic, partitions_with_offsets)
71
75
  @data[topic.to_s] = partitions_with_offsets.map do |p, o|
@@ -73,7 +77,8 @@ module Rdkafka
73
77
  end
74
78
  end
75
79
 
76
- # Return a `Hash` with the topics as keys and and an array of partition information as the value if present.
80
+ # Return a `Hash` with the topics as keys and and an array of partition information as the
81
+ # value if present.
77
82
  #
78
83
  # @return [Hash{String => Array<Partition>,nil}]
79
84
  def to_h
@@ -97,7 +102,8 @@ module Rdkafka
97
102
  #
98
103
  # @private
99
104
  #
100
- # @param pointer [FFI::Pointer] Optional pointer to an existing native list. Its contents will be copied.
105
+ # @param pointer [FFI::Pointer] Optional pointer to an existing native list. Its contents will
106
+ # be copied.
101
107
  #
102
108
  # @return [TopicPartitionList]
103
109
  def self.from_native_tpl(pointer)
@@ -118,7 +124,12 @@ module Rdkafka
118
124
  else
119
125
  elem[:offset]
120
126
  end
121
- partition = Partition.new(elem[:partition], offset, elem[:err])
127
+ partition = Partition.new(
128
+ elem[:partition],
129
+ offset,
130
+ elem[:err],
131
+ elem[:metadata].null? ? nil : elem[:metadata].read_string(elem[:metadata_size])
132
+ )
122
133
  partitions.push(partition)
123
134
  data[elem[:topic]] = partitions
124
135
  end
@@ -137,33 +148,50 @@ module Rdkafka
137
148
  def to_native_tpl
138
149
  tpl = Rdkafka::Bindings.rd_kafka_topic_partition_list_new(count)
139
150
 
140
- @data.each do |topic, partitions|
141
- if partitions
142
- partitions.each do |p|
143
- Rdkafka::Bindings.rd_kafka_topic_partition_list_add(
144
- tpl,
145
- topic,
146
- p.partition
147
- )
148
-
149
- if p.offset
150
- offset = p.offset.is_a?(Time) ? p.offset.to_f * 1_000 : p.offset
151
-
152
- Rdkafka::Bindings.rd_kafka_topic_partition_list_set_offset(
151
+ begin
152
+ @data.each do |topic, partitions|
153
+ if partitions
154
+ partitions.each do |p|
155
+ ref = Rdkafka::Bindings.rd_kafka_topic_partition_list_add(
153
156
  tpl,
154
157
  topic,
155
- p.partition,
156
- offset
158
+ p.partition
157
159
  )
160
+
161
+ if p.metadata
162
+ part = Rdkafka::Bindings::TopicPartition.new(ref)
163
+ str_ptr = FFI::MemoryPointer.from_string(p.metadata)
164
+ # The metadata string is owned by librdkafka once handed over and released here:
165
+ # https://github.com/confluentinc/librdkafka/blob/e03d3bb91ed92a38f38d9806b8d8deffe78a1de5/src/rdkafka_partition.c#L2682C18-L2682C18
166
+ str_ptr.autorelease = false
167
+ part[:metadata] = str_ptr
168
+ part[:metadata_size] = p.metadata.bytesize
169
+ end
170
+
171
+ if p.offset
172
+ offset = p.offset.is_a?(Time) ? p.offset.to_f * 1_000 : p.offset
173
+
174
+ Rdkafka::Bindings.rd_kafka_topic_partition_list_set_offset(
175
+ tpl,
176
+ topic,
177
+ p.partition,
178
+ offset
179
+ )
180
+ end
158
181
  end
182
+ else
183
+ Rdkafka::Bindings.rd_kafka_topic_partition_list_add(
184
+ tpl,
185
+ topic,
186
+ Rdkafka::Bindings::RD_KAFKA_PARTITION_UA
187
+ )
159
188
  end
160
- else
161
- Rdkafka::Bindings.rd_kafka_topic_partition_list_add(
162
- tpl,
163
- topic,
164
- Rdkafka::Bindings::RD_KAFKA_PARTITION_UA
165
- )
166
189
  end
190
+ rescue Exception
191
+ # The native list is caller-owned until it is handed to librdkafka. If population raises
192
+ # partway (e.g. an invalid partition), destroy it here so it is not leaked.
193
+ Rdkafka::Bindings.rd_kafka_topic_partition_list_destroy(tpl)
194
+ raise
167
195
  end
168
196
 
169
197
  tpl
@@ -14,14 +14,48 @@ module Rdkafka
14
14
  include Enumerable
15
15
  include Helpers::Time
16
16
  include Helpers::OAuth
17
+ include Helpers::Metadata
18
+ include Helpers::ListOffsets
17
19
 
18
20
  # @private
19
21
  # @param native_kafka [NativeKafka] wrapper around the native Kafka consumer handle
20
22
  def initialize(native_kafka)
21
23
  @native_kafka = native_kafka
24
+ # Single-element holder shared with the GC finalizer so it can destroy the lazily created
25
+ # consumer queue without capturing `self` (capturing the consumer in its own finalizer would
26
+ # pin it and prevent it from ever being collected).
27
+ @consumer_queue_holder = []
28
+
29
+ # Makes sure the consumer is closed (consumer queue destroyed and native client destroyed)
30
+ # before it gets GCed by Ruby.
31
+ ObjectSpace.define_finalizer(self, self.class.finalizer(native_kafka, @consumer_queue_holder))
32
+ end
33
+
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
55
+ end
22
56
 
23
- # Makes sure, that native kafka gets closed before it gets GCed by Ruby
24
- ObjectSpace.define_finalizer(self, native_kafka.finalizer)
57
+ native_kafka.close
58
+ end
25
59
  end
26
60
 
27
61
  # Starts the native Kafka polling thread and kicks off the init polling
@@ -93,11 +127,6 @@ module Rdkafka
93
127
  # @return [nil]
94
128
  # @raise [Rdkafka::ClosedConsumerError] if called on a closed consumer
95
129
  #
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
130
  # @example Drain all pending events
102
131
  # consumer.events_poll_nb_each { |_count| }
103
132
  #
@@ -106,6 +135,10 @@ module Rdkafka
106
135
  # consumer.events_poll_nb_each do |_count|
107
136
  # :stop if monotonic_now >= deadline
108
137
  # 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.
140
+ # @note This method is thread-safe as it uses @native_kafka.with_inner synchronization
141
+ # @note Do NOT use this if `consumer_poll_set` was set to `true`
109
142
  def events_poll_nb_each
110
143
  closed_consumer_check(__method__)
111
144
 
@@ -131,14 +164,6 @@ module Rdkafka
131
164
  # @raise [Rdkafka::ClosedConsumerError] if called on a closed consumer
132
165
  # @raise [Rdkafka::RdkafkaError] if a Kafka error occurs while polling
133
166
  #
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
167
  # @example Process messages until queue is empty
143
168
  # consumer.poll_nb_each do |message|
144
169
  # process(message)
@@ -151,6 +176,13 @@ module Rdkafka
151
176
  # count += 1
152
177
  # :stop if count >= 10
153
178
  # end
179
+ # @note This method uses `rd_kafka_consumer_poll` to fetch messages, unlike
180
+ # `events_poll_nb_each` which uses `rd_kafka_poll` for event callbacks (delivery reports,
181
+ # statistics, etc.). For consumers, use this method to receive messages and
182
+ # `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.
185
+ # @note Timeout/max_messages logic should be implemented by the caller
154
186
  def poll_nb_each
155
187
  closed_consumer_check(__method__)
156
188
 
@@ -187,6 +219,7 @@ module Rdkafka
187
219
  if @consumer_queue
188
220
  Rdkafka::Bindings.rd_kafka_queue_destroy(@consumer_queue)
189
221
  @consumer_queue = nil
222
+ @consumer_queue_holder[0] = nil
190
223
  end
191
224
  end
192
225
 
@@ -414,14 +447,18 @@ module Rdkafka
414
447
  end
415
448
 
416
449
  # Return the current positions (offsets) for topics and partitions.
417
- # 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.
450
+ # The offset field of each requested partition will be set to the offset of the last consumed
451
+ # message + 1, or nil in case there was no previous message.
418
452
  #
419
- # @param list [TopicPartitionList, nil] The topic with partitions to get the offsets for or nil to use the current subscription.
453
+ # @param list [TopicPartitionList, nil] The topic with partitions to get the offsets for or nil
454
+ # to use the current subscription.
420
455
  #
421
456
  # @return [TopicPartitionList]
422
457
  #
423
458
  # @raise [RdkafkaError] When getting the positions fails.
424
459
  def position(list = nil)
460
+ closed_consumer_check(__method__)
461
+
425
462
  if list.nil?
426
463
  list = assignment
427
464
  elsif !list.is_a?(TopicPartitionList)
@@ -481,39 +518,76 @@ module Rdkafka
481
518
  # possible to create one yourself, in this case you have to provide a list that
482
519
  # already contains all the partitions you need the lag for.
483
520
  #
521
+ # The end offsets of all requested partitions are fetched in a single batched
522
+ # {#list_offsets} query - librdkafka fans it out to the involved partition leaders
523
+ # internally and concurrently - instead of one blocking {#query_watermark_offsets}
524
+ # broker roundtrip per partition.
525
+ #
484
526
  # @param topic_partition_list [TopicPartitionList] The list to calculate lag for.
485
- # @param watermark_timeout_ms [Integer] The timeout for each query watermark call.
527
+ # @param watermark_timeout_ms [Integer] The timeout for the batched end-offsets query.
486
528
  # @return [Hash{String => Hash{Integer => Integer}}] A hash containing all topics with the lag
487
529
  # per partition
530
+ # @raise [ClosedConsumerError] when the consumer is closed
488
531
  # @raise [RdkafkaError] When querying the broker fails.
489
532
  def lag(topic_partition_list, watermark_timeout_ms = Defaults::CONSUMER_LAG_TIMEOUT_MS)
533
+ closed_consumer_check(__method__)
534
+
490
535
  out = {}
536
+ request = {}
537
+ partitions_by_topic = topic_partition_list.to_h
538
+
539
+ partitions_by_topic.each do |topic, partitions|
540
+ out[topic] = {}
491
541
 
492
- topic_partition_list.to_h.each do |topic, partitions|
493
- # Query high watermarks for this topic's partitions
494
- # and compare to the offset in the list.
495
- topic_out = {}
496
542
  partitions.each do |p|
497
543
  next if p.offset.nil?
498
- _low, high = query_watermark_offsets(
499
- topic,
500
- p.partition,
501
- watermark_timeout_ms
502
- )
503
- topic_out[p.partition] = high - p.offset
544
+
545
+ (request[topic] ||= []) << { partition: p.partition, offset: :latest }
504
546
  end
505
- out[topic] = topic_out
506
547
  end
548
+
549
+ return out if request.empty?
550
+
551
+ report = begin
552
+ # The isolation level is forwarded so the end offsets match what the old per-partition
553
+ # watermark query returned: librdkafka resolves that query with the consumer's configured
554
+ # `isolation.level` (LSO for the default read_committed), while the admin-style batched
555
+ # query would otherwise default to read_uncommitted (true high watermark).
556
+ list_offsets(request, isolation_level: isolation_level)
557
+ .wait(max_wait_timeout_ms: watermark_timeout_ms)
558
+ rescue AbstractHandle::WaitTimeoutError
559
+ # Keep the pre-batching contract: a slow broker surfaced as a timed-out RdkafkaError
560
+ # from the per-partition watermark query, not as a handle wait timeout.
561
+ raise RdkafkaError.new(
562
+ Rdkafka::Bindings::RD_KAFKA_RESP_ERR__TIMED_OUT,
563
+ "Error querying watermark offsets of '#{request.keys.join(", ")}'"
564
+ )
565
+ end
566
+
567
+ end_offsets = {}
568
+ report.offsets.each do |result|
569
+ (end_offsets[result[:topic]] ||= {})[result[:partition]] = result[:offset]
570
+ end
571
+
572
+ partitions_by_topic.each do |topic, partitions|
573
+ partitions.each do |p|
574
+ next if p.offset.nil?
575
+
576
+ out[topic][p.partition] = end_offsets.fetch(topic).fetch(p.partition) - p.offset
577
+ end
578
+ end
579
+
507
580
  out
508
581
  end
509
582
 
510
583
  # Returns the ClusterId as reported in broker metadata.
511
584
  #
585
+ # @param timeout_ms [Integer] timeout in milliseconds to wait for the cluster id
512
586
  # @return [String, nil]
513
- def cluster_id
587
+ def cluster_id(timeout_ms = Defaults::CONSUMER_CLUSTER_ID_TIMEOUT_MS)
514
588
  closed_consumer_check(__method__)
515
589
  @native_kafka.with_inner do |inner|
516
- Rdkafka::Bindings.rd_kafka_clusterid(inner)
590
+ read_and_free_native_string(inner, Rdkafka::Bindings.rd_kafka_clusterid(inner, timeout_ms))
517
591
  end
518
592
  end
519
593
 
@@ -525,7 +599,7 @@ module Rdkafka
525
599
  def member_id
526
600
  closed_consumer_check(__method__)
527
601
  @native_kafka.with_inner do |inner|
528
- Rdkafka::Bindings.rd_kafka_memberid(inner)
602
+ read_and_free_native_string(inner, Rdkafka::Bindings.rd_kafka_memberid(inner))
529
603
  end
530
604
  end
531
605
 
@@ -534,16 +608,34 @@ module Rdkafka
534
608
  # When using this `enable.auto.offset.store` should be set to `false` in the config.
535
609
  #
536
610
  # @param message [Rdkafka::Consumer::Message] The message which offset will be stored
611
+ # @param metadata [String, nil] commit metadata string to store alongside the offset
537
612
  # @return [nil]
538
613
  # @raise [RdkafkaError] When storing the offset fails
539
- def store_offset(message)
614
+ def store_offset(message, metadata = nil)
540
615
  closed_consumer_check(__method__)
541
616
 
542
617
  list = TopicPartitionList.new
543
- list.add_topic_and_partitions_with_offsets(
544
- message.topic,
545
- message.partition => message.offset + 1
546
- )
618
+
619
+ # For metadata aware commits we build the partition reference directly to save on
620
+ # objects allocations
621
+ if metadata
622
+ list.add_topic_and_partitions_with_offsets(
623
+ message.topic,
624
+ [
625
+ Consumer::Partition.new(
626
+ message.partition,
627
+ message.offset + 1,
628
+ 0,
629
+ metadata
630
+ )
631
+ ]
632
+ )
633
+ else
634
+ list.add_topic_and_partitions_with_offsets(
635
+ message.topic,
636
+ message.partition => message.offset + 1
637
+ )
638
+ end
547
639
 
548
640
  tpl = list.to_native_tpl
549
641
 
@@ -572,8 +664,7 @@ module Rdkafka
572
664
  end
573
665
 
574
666
  # Seek to a particular message by providing the topic, partition and offset.
575
- # The next poll on the topic/partition will return the
576
- # message at the given offset.
667
+ # The next poll on the topic/partition will return the message at the given offset.
577
668
  #
578
669
  # @param topic [String] The topic in which to seek
579
670
  # @param partition [Integer] The partition number to seek
@@ -641,8 +732,7 @@ module Rdkafka
641
732
 
642
733
  # Manually commit the current offsets of this consumer.
643
734
  #
644
- # To use this set `enable.auto.commit`to `false` to disable automatic triggering
645
- # of commits.
735
+ # To use this set `enable.auto.commit`to `false` to disable automatic triggering of commits.
646
736
  #
647
737
  # If `enable.auto.offset.store` is set to `true` the offset of the last consumed
648
738
  # message for every partition is used. If set to `false` you can use {store_offset} to
@@ -796,7 +886,7 @@ module Rdkafka
796
886
  # returns without further waiting.
797
887
  #
798
888
  # Error events (e.g. `:partition_eof`) are returned inline as {RdkafkaError} objects
799
- # rather than raised, so callers receive the complete batch both messages and errors
889
+ # rather than raised, so callers receive the complete batch - both messages and errors -
800
890
  # and can decide how to handle each. This is particularly useful when multiple partitions
801
891
  # signal EOF simultaneously: all signals appear in the returned array rather than only
802
892
  # the first one being raised and the rest silently discarded.
@@ -841,8 +931,17 @@ module Rdkafka
841
931
  next
842
932
  end
843
933
 
844
- results << Rdkafka::Consumer::Message.new(native_message)
845
- Rdkafka::Bindings.rd_kafka_message_destroy(ptr)
934
+ begin
935
+ results << Rdkafka::Consumer::Message.new(native_message)
936
+ rescue Rdkafka::RdkafkaError => e
937
+ # A message that fails to build (e.g. a header read error) is surfaced inline as an
938
+ # error event rather than discarding the whole batch - including the messages already
939
+ # built - and raising, which silently lost them once their offsets had been stored.
940
+ results << e
941
+ ensure
942
+ Rdkafka::Bindings.rd_kafka_message_destroy(ptr)
943
+ end
944
+
846
945
  i += 1
847
946
  end
848
947
  ensure
@@ -862,15 +961,15 @@ module Rdkafka
862
961
  # particularly useful in fiber scheduler contexts where GVL release/reacquire
863
962
  # overhead is wasteful since we don't expect to wait.
864
963
  #
964
+ # @param timeout_ms [Integer] Timeout waiting for the first message
965
+ # (default: 0 for non-blocking)
966
+ # @param max_items [Integer] Maximum number of messages to return per call
967
+ # @return [Array<Message, RdkafkaError>] Batch of messages and/or error events in arrival order
968
+ # @raise [ClosedConsumerError] When called on a closed consumer
865
969
  # @note Since the GVL is not released, a non-zero timeout_ms will block all Ruby
866
970
  # threads/fibers for the duration. Use {#poll_batch} if you need a blocking wait.
867
971
  #
868
972
  # Error events are returned inline as {RdkafkaError} objects; see {#poll_batch} for details.
869
- #
870
- # @param timeout_ms [Integer] Timeout waiting for the first message (default: 0 for non-blocking)
871
- # @param max_items [Integer] Maximum number of messages to return per call
872
- # @return [Array<Message, RdkafkaError>] Batch of messages and/or error events in arrival order
873
- # @raise [ClosedConsumerError] When called on a closed consumer
874
973
  def poll_batch_nb(timeout_ms = 0, max_items: 100)
875
974
  closed_consumer_check(__method__)
876
975
 
@@ -907,8 +1006,17 @@ module Rdkafka
907
1006
  next
908
1007
  end
909
1008
 
910
- results << Rdkafka::Consumer::Message.new(native_message)
911
- Rdkafka::Bindings.rd_kafka_message_destroy(ptr)
1009
+ begin
1010
+ results << Rdkafka::Consumer::Message.new(native_message)
1011
+ rescue Rdkafka::RdkafkaError => e
1012
+ # A message that fails to build (e.g. a header read error) is surfaced inline as an
1013
+ # error event rather than discarding the whole batch - including the messages already
1014
+ # built - and raising, which silently lost them once their offsets had been stored.
1015
+ results << e
1016
+ ensure
1017
+ Rdkafka::Bindings.rd_kafka_message_destroy(ptr)
1018
+ end
1019
+
912
1020
  i += 1
913
1021
  end
914
1022
  ensure
@@ -994,12 +1102,63 @@ module Rdkafka
994
1102
  def closed_consumer_check(method)
995
1103
  raise Rdkafka::ClosedConsumerError.new(method) if closed?
996
1104
  end
1105
+ alias_method :closed_check, :closed_consumer_check
1106
+
1107
+ # Reads a librdkafka-allocated string and frees the underlying native buffer.
1108
+ #
1109
+ # librdkafka returns heap-allocated, caller-owned strings from functions like
1110
+ # `rd_kafka_clusterid`/`rd_kafka_memberid`. The buffer must be released with
1111
+ # `rd_kafka_mem_free`, otherwise it leaks on every call.
1112
+ #
1113
+ # @param inner [FFI::Pointer] the native client handle used to allocate the string
1114
+ # @param ptr [FFI::Pointer] the native string pointer (may be null)
1115
+ # @return [String, nil] the copied Ruby string, or nil when the pointer is null
1116
+ def read_and_free_native_string(inner, ptr)
1117
+ return nil if ptr.null?
1118
+
1119
+ ptr.read_string
1120
+ ensure
1121
+ Rdkafka::Bindings.rd_kafka_mem_free(inner, ptr) unless ptr.null?
1122
+ end
1123
+
1124
+ # Reads this consumer's effective `isolation.level` from the live librdkafka configuration
1125
+ # and maps it to the numeric isolation level constant. Memoized: the value cannot change
1126
+ # after client creation.
1127
+ #
1128
+ # @return [Integer] `RD_KAFKA_ISOLATION_LEVEL_READ_COMMITTED` or
1129
+ # `RD_KAFKA_ISOLATION_LEVEL_READ_UNCOMMITTED`
1130
+ # @raise [Rdkafka::Config::ConfigError] when the property cannot be read
1131
+ def isolation_level
1132
+ @isolation_level ||= @native_kafka.with_inner do |inner|
1133
+ conf = Rdkafka::Bindings.rd_kafka_conf(inner)
1134
+
1135
+ size_ptr = Rdkafka::Bindings::SizePtr.new
1136
+ size_ptr[:value] = 64
1137
+ value_ptr = FFI::MemoryPointer.new(:char, 64)
1138
+
1139
+ result = Rdkafka::Bindings.rd_kafka_conf_get(conf, "isolation.level", value_ptr, size_ptr)
1140
+
1141
+ if result != :config_ok
1142
+ raise Rdkafka::Config::ConfigError.new("Could not read isolation.level: #{result}")
1143
+ end
1144
+
1145
+ if value_ptr.read_string == "read_committed"
1146
+ Rdkafka::Bindings::RD_KAFKA_ISOLATION_LEVEL_READ_COMMITTED
1147
+ else
1148
+ Rdkafka::Bindings::RD_KAFKA_ISOLATION_LEVEL_READ_UNCOMMITTED
1149
+ end
1150
+ end
1151
+ end
997
1152
 
998
1153
  # Returns the consumer queue pointer, lazily initialized
999
1154
  # @return [FFI::Pointer] consumer queue handle
1000
1155
  def consumer_queue
1001
1156
  @consumer_queue ||= @native_kafka.with_inner do |inner|
1002
- Rdkafka::Bindings.rd_kafka_queue_get_consumer(inner)
1157
+ queue = Rdkafka::Bindings.rd_kafka_queue_get_consumer(inner)
1158
+ # Share the pointer with the finalizer so it is destroyed even if the consumer is GC'd
1159
+ # without an explicit close.
1160
+ @consumer_queue_holder[0] = queue
1161
+ queue
1003
1162
  end
1004
1163
  end
1005
1164
 
@@ -3,19 +3,17 @@
3
3
  module Rdkafka
4
4
  # Default timeout and timing values used throughout rdkafka-ruby.
5
5
  #
6
- # All timeout values can be overridden per-call via method parameters.
7
- # These constants provide a central place to understand and reference
8
- # the default values used across the library.
9
- #
10
- # @note These are rdkafka-ruby defaults, not librdkafka configuration options.
11
- # For librdkafka options, see:
12
- # https://github.com/confluentinc/librdkafka/blob/master/CONFIGURATION.md
6
+ # All timeout values can be overridden per-call via method parameters. These constants provide
7
+ # a central place to understand and reference the default values used across the library.
13
8
  #
14
9
  # @example Overriding a timeout per-call
15
10
  # consumer.committed(timeout_ms: 5_000) # Use 5 seconds instead of default 2 seconds
16
11
  #
17
12
  # @example Checking the default value
18
13
  # Rdkafka::Defaults::CONSUMER_COMMITTED_TIMEOUT_MS # => 2000
14
+ # @note These are rdkafka-ruby defaults, not librdkafka configuration options.
15
+ # For librdkafka options, see:
16
+ # https://github.com/confluentinc/librdkafka/blob/master/CONFIGURATION.md
19
17
  module Defaults
20
18
  # Consumer timeouts (in milliseconds)
21
19
 
@@ -23,6 +21,10 @@ module Rdkafka
23
21
  # @see Consumer#committed
24
22
  CONSUMER_COMMITTED_TIMEOUT_MS = 2_000
25
23
 
24
+ # Default timeout for fetching the cluster id
25
+ # @see Consumer#cluster_id
26
+ CONSUMER_CLUSTER_ID_TIMEOUT_MS = 1_000
27
+
26
28
  # Default timeout for querying watermark offsets
27
29
  # @see Consumer#query_watermark_offsets
28
30
  CONSUMER_QUERY_WATERMARK_TIMEOUT_MS = 1_000
@@ -91,6 +93,16 @@ module Rdkafka
91
93
  # @see Metadata#initialize
92
94
  METADATA_RETRY_BACKOFF_BASE_MS = 100
93
95
 
96
+ # Maximum backoff time between metadata retries. Caps the exponential backoff so a long retry
97
+ # sequence against an unhealthy cluster cannot block the calling thread for minutes.
98
+ # @see Metadata#initialize
99
+ METADATA_RETRY_BACKOFF_MAX_MS = 1_000
100
+
101
+ # Soft wall-clock budget for the whole metadata retry loop; past it (and past
102
+ # METADATA_MIN_ATTEMPTS) the loop stops so a synchronous fetch cannot block the caller for long
103
+ # @see Metadata#initialize
104
+ METADATA_RETRY_BUDGET_MS = 5_000
105
+
94
106
  # Cache settings (in milliseconds)
95
107
 
96
108
  # Default time-to-live for cached partition counts
@@ -102,5 +114,10 @@ module Rdkafka
102
114
  # Maximum number of metadata fetch retry attempts
103
115
  # @see Metadata#initialize
104
116
  METADATA_MAX_RETRIES = 10
117
+
118
+ # Minimum metadata fetch attempts before the retry budget may end the loop, so a slow broker
119
+ # (whose requests each consume the full timeout) still gets a few tries
120
+ # @see Metadata#initialize
121
+ METADATA_MIN_ATTEMPTS = 3
105
122
  end
106
123
  end