karafka-rdkafka 0.27.1-aarch64-linux-gnu → 0.28.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 (56) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +36 -0
  3. data/README.md +1 -0
  4. data/docker-compose-ssl.yml +1 -1
  5. data/docker-compose.yml +1 -1
  6. data/ext/librdkafka.so +0 -0
  7. data/karafka-rdkafka.gemspec +1 -0
  8. data/lib/rdkafka/abstract_handle.rb +37 -4
  9. data/lib/rdkafka/admin/config_binding_result.rb +1 -1
  10. data/lib/rdkafka/admin/create_acl_handle.rb +2 -8
  11. data/lib/rdkafka/admin/create_partitions_handle.rb +1 -20
  12. data/lib/rdkafka/admin/create_topic_handle.rb +1 -20
  13. data/lib/rdkafka/admin/delete_acl_handle.rb +2 -10
  14. data/lib/rdkafka/admin/delete_groups_handle.rb +3 -11
  15. data/lib/rdkafka/admin/delete_topic_handle.rb +1 -20
  16. data/lib/rdkafka/admin/describe_acl_handle.rb +2 -10
  17. data/lib/rdkafka/admin/describe_configs_handle.rb +2 -13
  18. data/lib/rdkafka/admin/describe_configs_report.rb +5 -6
  19. data/lib/rdkafka/admin/incremental_alter_configs_handle.rb +2 -13
  20. data/lib/rdkafka/admin/incremental_alter_configs_report.rb +5 -6
  21. data/lib/rdkafka/admin/list_offsets_handle.rb +2 -13
  22. data/lib/rdkafka/admin/list_offsets_report.rb +5 -2
  23. data/lib/rdkafka/admin.rb +56 -137
  24. data/lib/rdkafka/bindings.rb +12 -3
  25. data/lib/rdkafka/callbacks/base_handler.rb +62 -0
  26. data/lib/rdkafka/callbacks/create_acl_handler.rb +37 -0
  27. data/lib/rdkafka/callbacks/create_partitions_handler.rb +37 -0
  28. data/lib/rdkafka/callbacks/create_topic_handler.rb +37 -0
  29. data/lib/rdkafka/callbacks/delete_acl_handler.rb +42 -0
  30. data/lib/rdkafka/callbacks/delete_groups_handler.rb +37 -0
  31. data/lib/rdkafka/callbacks/delete_topic_handler.rb +37 -0
  32. data/lib/rdkafka/callbacks/describe_acl_handler.rb +35 -0
  33. data/lib/rdkafka/callbacks/describe_configs_handler.rb +42 -0
  34. data/lib/rdkafka/callbacks/incremental_alter_configs_handler.rb +42 -0
  35. data/lib/rdkafka/callbacks/list_offsets_handler.rb +42 -0
  36. data/lib/rdkafka/callbacks.rb +56 -244
  37. data/lib/rdkafka/config.rb +50 -33
  38. data/lib/rdkafka/consumer/headers.rb +19 -5
  39. data/lib/rdkafka/consumer/topic_partition_list.rb +43 -33
  40. data/lib/rdkafka/consumer.rb +203 -39
  41. data/lib/rdkafka/defaults.rb +21 -1
  42. data/lib/rdkafka/error.rb +12 -4
  43. data/lib/rdkafka/helpers/list_offsets.rb +127 -0
  44. data/lib/rdkafka/helpers/metadata.rb +29 -0
  45. data/lib/rdkafka/metadata.rb +84 -19
  46. data/lib/rdkafka/producer/delivery_handle.rb +3 -3
  47. data/lib/rdkafka/producer/partitions_count_cache.rb +24 -38
  48. data/lib/rdkafka/producer.rb +72 -51
  49. data/lib/rdkafka/version.rb +3 -3
  50. data/lib/rdkafka.rb +13 -0
  51. data/package-lock.json +6 -6
  52. data/renovate.json +13 -2
  53. metadata +15 -5
  54. data/Gemfile +0 -13
  55. data/Gemfile.lint +0 -14
  56. data/Gemfile.lint.lock +0 -123
@@ -136,51 +136,61 @@ module Rdkafka
136
136
 
137
137
  # Create a native tpl with the contents of this object added.
138
138
  #
139
- # The pointer will be cleaned by `rd_kafka_topic_partition_list_destroy` when GC releases it.
139
+ # The returned pointer is caller-owned and must be released with
140
+ # `rd_kafka_topic_partition_list_destroy`. It is not cleaned up by GC.
140
141
  #
141
142
  # @private
142
143
  # @return [FFI::Pointer]
143
144
  def to_native_tpl
144
145
  tpl = Rdkafka::Bindings.rd_kafka_topic_partition_list_new(count)
145
146
 
146
- @data.each do |topic, partitions|
147
- if partitions
148
- partitions.each do |p|
149
- ref = Rdkafka::Bindings.rd_kafka_topic_partition_list_add(
150
- tpl,
151
- topic,
152
- p.partition
153
- )
154
-
155
- # Remove the respond to check after karafka 2.3.0 is released
156
- if p.respond_to?(:metadata) && p.metadata
157
- part = Rdkafka::Bindings::TopicPartition.new(ref)
158
- str_ptr = FFI::MemoryPointer.from_string(p.metadata)
159
- # released here:
160
- # https://github.com/confluentinc/librdkafka/blob/e03d3bb91ed92a38f38d9806b8d8deffe78a1de5/src/rdkafka_partition.c#L2682C18-L2682C18
161
- str_ptr.autorelease = false
162
- part[:metadata] = str_ptr
163
- part[:metadata_size] = p.metadata.bytesize
164
- end
165
-
166
- if p.offset
167
- offset = p.offset.is_a?(Time) ? p.offset.to_f * 1_000 : p.offset
168
-
169
- Rdkafka::Bindings.rd_kafka_topic_partition_list_set_offset(
147
+ begin
148
+ @data.each do |topic, partitions|
149
+ if partitions
150
+ partitions.each do |p|
151
+ ref = Rdkafka::Bindings.rd_kafka_topic_partition_list_add(
170
152
  tpl,
171
153
  topic,
172
- p.partition,
173
- offset
154
+ p.partition
174
155
  )
156
+
157
+ # Remove the respond to check after karafka 2.3.0 is released
158
+ if p.respond_to?(:metadata) && p.metadata
159
+ part = Rdkafka::Bindings::TopicPartition.new(ref)
160
+ str_ptr = FFI::MemoryPointer.from_string(p.metadata)
161
+ # released here:
162
+ # https://github.com/confluentinc/librdkafka/blob/e03d3bb91ed92a38f38d9806b8d8deffe78a1de5/src/rdkafka_partition.c#L2682C18-L2682C18
163
+ str_ptr.autorelease = false
164
+ part[:metadata] = str_ptr
165
+ part[:metadata_size] = p.metadata.bytesize
166
+ end
167
+
168
+ if p.offset
169
+ offset = p.offset.is_a?(Time) ? p.offset.to_f * 1_000 : p.offset
170
+
171
+ Rdkafka::Bindings.rd_kafka_topic_partition_list_set_offset(
172
+ tpl,
173
+ topic,
174
+ p.partition,
175
+ offset
176
+ )
177
+ end
175
178
  end
179
+ else
180
+ Rdkafka::Bindings.rd_kafka_topic_partition_list_add(
181
+ tpl,
182
+ topic,
183
+ Rdkafka::Bindings::RD_KAFKA_PARTITION_UA
184
+ )
176
185
  end
177
- else
178
- Rdkafka::Bindings.rd_kafka_topic_partition_list_add(
179
- tpl,
180
- topic,
181
- Rdkafka::Bindings::RD_KAFKA_PARTITION_UA
182
- )
183
186
  end
187
+ rescue Exception
188
+ # The native list is caller-owned and only handed back on success. If population raises
189
+ # partway (e.g. non-string metadata, or an offset FFI can't coerce to int64) we must
190
+ # destroy it here, otherwise the half-built list leaks.
191
+ Rdkafka::Bindings.rd_kafka_topic_partition_list_destroy(tpl)
192
+
193
+ raise
184
194
  end
185
195
 
186
196
  tpl
@@ -14,14 +14,46 @@ 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 handle.
39
+ #
40
+ # @private
41
+ # @param native_kafka [NativeKafka] the wrapped native client
42
+ # @param queue_holder [Array] single-element holder carrying the consumer queue pointer (or empty)
43
+ # @return [Proc] finalizer proc that must not reference the consumer instance
44
+ def self.finalizer(native_kafka, queue_holder)
45
+ proc do
46
+ next if native_kafka.closed?
47
+
48
+ native_kafka.synchronize do |inner|
49
+ Rdkafka::Bindings.rd_kafka_consumer_close(inner)
50
+
51
+ queue = queue_holder[0]
52
+ Rdkafka::Bindings.rd_kafka_queue_destroy(queue) if queue
53
+ end
22
54
 
23
- # Makes sure, that native kafka gets closed before it gets GCed by Ruby
24
- ObjectSpace.define_finalizer(self, native_kafka.finalizer)
55
+ native_kafka.close
56
+ end
25
57
  end
26
58
 
27
59
  # Starts the native Kafka polling thread and kicks off the init polling
@@ -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
 
@@ -412,6 +445,8 @@ module Rdkafka
412
445
  #
413
446
  # @raise [RdkafkaError] When getting the positions fails.
414
447
  def position(list = nil)
448
+ closed_consumer_check(__method__)
449
+
415
450
  if list.nil?
416
451
  list = assignment
417
452
  elsif !list.is_a?(TopicPartitionList)
@@ -466,39 +501,77 @@ module Rdkafka
466
501
  # possible to create one yourself, in this case you have to provide a list that
467
502
  # already contains all the partitions you need the lag for.
468
503
  #
504
+ # The end offsets of all requested partitions are fetched in a single batched
505
+ # {#list_offsets} query - librdkafka fans it out to the involved partition leaders
506
+ # internally and concurrently - instead of one blocking {#query_watermark_offsets}
507
+ # broker roundtrip per partition.
508
+ #
469
509
  # @param topic_partition_list [TopicPartitionList] The list to calculate lag for.
470
- # @param watermark_timeout_ms [Integer] The timeout for each query watermark call.
510
+ # @param watermark_timeout_ms [Integer] The timeout for the batched end-offsets query.
471
511
  # @return [Hash{String => Hash{Integer => Integer}}] A hash containing all topics with the lag
472
512
  # per partition
513
+ # @raise [ClosedConsumerError] when the consumer is closed
473
514
  # @raise [RdkafkaError] When querying the broker fails.
474
515
  def lag(topic_partition_list, watermark_timeout_ms = Defaults::CONSUMER_LAG_TIMEOUT_MS)
516
+ closed_consumer_check(__method__)
517
+
475
518
  out = {}
519
+ request = {}
520
+ partitions_by_topic = topic_partition_list.to_h
521
+
522
+ partitions_by_topic.each do |topic, partitions|
523
+ out[topic] = {}
524
+
525
+ partitions.each do |p|
526
+ next if p.offset.nil?
527
+
528
+ (request[topic] ||= []) << { partition: p.partition, offset: :latest }
529
+ end
530
+ end
531
+
532
+ return out if request.empty?
533
+
534
+ report = begin
535
+ # The isolation level is forwarded so the end offsets match what the old per-partition
536
+ # watermark query returned: librdkafka resolves that query with the consumer's configured
537
+ # `isolation.level` (LSO for the default read_committed), while the admin-style batched
538
+ # query would otherwise default to read_uncommitted (true high watermark).
539
+ list_offsets(request, isolation_level: isolation_level)
540
+ .wait(max_wait_timeout_ms: watermark_timeout_ms)
541
+ rescue AbstractHandle::WaitTimeoutError
542
+ # Keep the pre-batching contract: a slow broker surfaced as a timed-out RdkafkaError
543
+ # from the per-partition watermark query, not as a handle wait timeout.
544
+ raise RdkafkaError.new(
545
+ Rdkafka::Bindings::RD_KAFKA_RESP_ERR__TIMED_OUT,
546
+ "Error querying watermark offsets of '#{request.keys.join(", ")}'"
547
+ )
548
+ end
476
549
 
477
- topic_partition_list.to_h.each do |topic, partitions|
478
- # Query high watermarks for this topic's partitions
479
- # and compare to the offset in the list.
480
- topic_out = {}
550
+ end_offsets = {}
551
+ report.offsets.each do |result|
552
+ (end_offsets[result[:topic]] ||= {})[result[:partition]] = result[:offset]
553
+ end
554
+
555
+ partitions_by_topic.each do |topic, partitions|
481
556
  partitions.each do |p|
482
557
  next if p.offset.nil?
483
- _low, high = query_watermark_offsets(
484
- topic,
485
- p.partition,
486
- watermark_timeout_ms
487
- )
488
- topic_out[p.partition] = high - p.offset
558
+
559
+ out[topic][p.partition] = end_offsets.fetch(topic).fetch(p.partition) - p.offset
489
560
  end
490
- out[topic] = topic_out
491
561
  end
562
+
492
563
  out
493
564
  end
494
565
 
495
566
  # Returns the ClusterId as reported in broker metadata.
496
567
  #
497
- # @return [String, nil]
498
- def cluster_id
568
+ # @param timeout_ms [Integer] how long to block waiting for metadata when the cluster id is
569
+ # not already cached (0 for a non-blocking call)
570
+ # @return [String, nil] the cluster id, or nil if it could not be retrieved in time
571
+ def cluster_id(timeout_ms = Defaults::CONSUMER_CLUSTER_ID_TIMEOUT_MS)
499
572
  closed_consumer_check(__method__)
500
573
  @native_kafka.with_inner do |inner|
501
- Rdkafka::Bindings.rd_kafka_clusterid(inner)
574
+ read_and_free_native_string(inner, Rdkafka::Bindings.rd_kafka_clusterid(inner, timeout_ms))
502
575
  end
503
576
  end
504
577
 
@@ -510,7 +583,7 @@ module Rdkafka
510
583
  def member_id
511
584
  closed_consumer_check(__method__)
512
585
  @native_kafka.with_inner do |inner|
513
- Rdkafka::Bindings.rd_kafka_memberid(inner)
586
+ read_and_free_native_string(inner, Rdkafka::Bindings.rd_kafka_memberid(inner))
514
587
  end
515
588
  end
516
589
 
@@ -801,16 +874,21 @@ module Rdkafka
801
874
  # is available, librdkafka fills the buffer with whatever is immediately ready and
802
875
  # returns without further waiting.
803
876
  #
877
+ # Error events (e.g. `:partition_eof`) are returned inline as {RdkafkaError} objects
878
+ # rather than raised, so callers receive the complete batch — both messages and errors —
879
+ # and can decide how to handle each. This is particularly useful when multiple partitions
880
+ # signal EOF simultaneously: all signals appear in the returned array rather than only
881
+ # the first one being raised and the rest silently discarded.
882
+ #
804
883
  # @param timeout_ms [Integer] Timeout waiting for the first message (-1 for infinite)
805
884
  # @param max_items [Integer] Maximum number of messages to return per call
806
- # @return [Array<Message>] Array of messages (empty if none available within timeout)
807
- # @raise [RdkafkaError] When a consumed message contains an error
885
+ # @return [Array<Message, RdkafkaError>] Batch of messages and/or error events in arrival order
808
886
  # @raise [ClosedConsumerError] When called on a closed consumer
809
887
  def poll_batch(timeout_ms, max_items: 100)
810
888
  closed_consumer_check(__method__)
811
889
 
812
890
  buffer = batch_buffer(max_items)
813
- messages = []
891
+ results = []
814
892
 
815
893
  count = @native_kafka.with_inner do |_inner|
816
894
  Rdkafka::Bindings.rd_kafka_consume_batch_queue(
@@ -821,7 +899,7 @@ module Rdkafka
821
899
  )
822
900
  end
823
901
 
824
- return messages if count <= 0
902
+ return results if count <= 0
825
903
 
826
904
  i = 0
827
905
  begin
@@ -836,13 +914,23 @@ module Rdkafka
836
914
  native_message = Rdkafka::Bindings::Message.new(ptr)
837
915
 
838
916
  if native_message[:err] != Rdkafka::Bindings::RD_KAFKA_RESP_ERR_NO_ERROR
839
- @native_kafka.with_inner do |inner|
840
- Rdkafka::RdkafkaError.validate!(native_message, client_ptr: inner)
841
- end
917
+ results << build_batch_error(native_message)
918
+ Rdkafka::Bindings.rd_kafka_message_destroy(ptr)
919
+ i += 1
920
+ next
921
+ end
922
+
923
+ begin
924
+ results << Rdkafka::Consumer::Message.new(native_message)
925
+ rescue Rdkafka::RdkafkaError => e
926
+ # A message that fails to build (e.g. a header read error) is surfaced inline as an
927
+ # error event rather than discarding the whole batch - including the messages already
928
+ # built - and raising, which silently lost them once their offsets had been stored.
929
+ results << e
930
+ ensure
931
+ Rdkafka::Bindings.rd_kafka_message_destroy(ptr)
842
932
  end
843
933
 
844
- messages << Rdkafka::Consumer::Message.new(native_message)
845
- Rdkafka::Bindings.rd_kafka_message_destroy(ptr)
846
934
  i += 1
847
935
  end
848
936
  ensure
@@ -853,7 +941,7 @@ module Rdkafka
853
941
  end
854
942
  end
855
943
 
856
- messages
944
+ results
857
945
  end
858
946
 
859
947
  # Poll for a batch of messages without releasing the GVL (Global VM Lock).
@@ -865,16 +953,17 @@ module Rdkafka
865
953
  # @note Since the GVL is not released, a non-zero timeout_ms will block all Ruby
866
954
  # threads/fibers for the duration. Use {#poll_batch} if you need a blocking wait.
867
955
  #
956
+ # Error events are returned inline as {RdkafkaError} objects; see {#poll_batch} for details.
957
+ #
868
958
  # @param timeout_ms [Integer] Timeout waiting for the first message (default: 0 for non-blocking)
869
959
  # @param max_items [Integer] Maximum number of messages to return per call
870
- # @return [Array<Message>] Array of messages (empty if none available within timeout)
871
- # @raise [RdkafkaError] When a consumed message contains an error
960
+ # @return [Array<Message, RdkafkaError>] Batch of messages and/or error events in arrival order
872
961
  # @raise [ClosedConsumerError] When called on a closed consumer
873
962
  def poll_batch_nb(timeout_ms = 0, max_items: 100)
874
963
  closed_consumer_check(__method__)
875
964
 
876
965
  buffer = batch_buffer(max_items)
877
- messages = []
966
+ results = []
878
967
 
879
968
  count = @native_kafka.with_inner do |_inner|
880
969
  Rdkafka::Bindings.rd_kafka_consume_batch_queue_nb(
@@ -885,7 +974,7 @@ module Rdkafka
885
974
  )
886
975
  end
887
976
 
888
- return messages if count <= 0
977
+ return results if count <= 0
889
978
 
890
979
  i = 0
891
980
  begin
@@ -900,13 +989,23 @@ module Rdkafka
900
989
  native_message = Rdkafka::Bindings::Message.new(ptr)
901
990
 
902
991
  if native_message[:err] != Rdkafka::Bindings::RD_KAFKA_RESP_ERR_NO_ERROR
903
- @native_kafka.with_inner do |inner|
904
- Rdkafka::RdkafkaError.validate!(native_message, client_ptr: inner)
905
- end
992
+ results << build_batch_error(native_message)
993
+ Rdkafka::Bindings.rd_kafka_message_destroy(ptr)
994
+ i += 1
995
+ next
996
+ end
997
+
998
+ begin
999
+ results << Rdkafka::Consumer::Message.new(native_message)
1000
+ rescue Rdkafka::RdkafkaError => e
1001
+ # A message that fails to build (e.g. a header read error) is surfaced inline as an
1002
+ # error event rather than discarding the whole batch - including the messages already
1003
+ # built - and raising, which silently lost them once their offsets had been stored.
1004
+ results << e
1005
+ ensure
1006
+ Rdkafka::Bindings.rd_kafka_message_destroy(ptr)
906
1007
  end
907
1008
 
908
- messages << Rdkafka::Consumer::Message.new(native_message)
909
- Rdkafka::Bindings.rd_kafka_message_destroy(ptr)
910
1009
  i += 1
911
1010
  end
912
1011
  ensure
@@ -917,7 +1016,7 @@ module Rdkafka
917
1016
  end
918
1017
  end
919
1018
 
920
- messages
1019
+ results
921
1020
  end
922
1021
 
923
1022
  # Poll for new messages and yield for each received one. Iteration
@@ -986,18 +1085,83 @@ module Rdkafka
986
1085
 
987
1086
  private
988
1087
 
1088
+ # Copies a librdkafka-allocated C string into a Ruby string and frees the native buffer.
1089
+ #
1090
+ # `rd_kafka_memberid`/`rd_kafka_clusterid` return a string the caller owns and must release
1091
+ # with `rd_kafka_mem_free`; without this the buffer leaks on every call.
1092
+ #
1093
+ # @param inner [FFI::Pointer] the native client handle (needed to free the buffer)
1094
+ # @param ptr [FFI::Pointer] the librdkafka-allocated string, possibly NULL
1095
+ # @return [String, nil] the copied string, or nil when the pointer was NULL
1096
+ def read_and_free_native_string(inner, ptr)
1097
+ return nil if ptr.null?
1098
+
1099
+ ptr.read_string
1100
+ ensure
1101
+ Rdkafka::Bindings.rd_kafka_mem_free(inner, ptr) unless ptr.null?
1102
+ end
1103
+
989
1104
  # Checks if the consumer is closed and raises an error if so
990
1105
  # @param method [Symbol] name of the calling method for error context
991
1106
  # @raise [ClosedConsumerError] when the consumer is closed
992
1107
  def closed_consumer_check(method)
993
1108
  raise Rdkafka::ClosedConsumerError.new(method) if closed?
994
1109
  end
1110
+ alias_method :closed_check, :closed_consumer_check
1111
+
1112
+ # Reads this consumer's effective `isolation.level` from the live librdkafka configuration
1113
+ # and maps it to the numeric isolation level constant. Memoized: the value cannot change
1114
+ # after client creation.
1115
+ #
1116
+ # @return [Integer] `RD_KAFKA_ISOLATION_LEVEL_READ_COMMITTED` or
1117
+ # `RD_KAFKA_ISOLATION_LEVEL_READ_UNCOMMITTED`
1118
+ # @raise [Rdkafka::Config::ConfigError] when the property cannot be read
1119
+ def isolation_level
1120
+ @isolation_level ||= @native_kafka.with_inner do |inner|
1121
+ conf = Rdkafka::Bindings.rd_kafka_conf(inner)
1122
+
1123
+ size_ptr = Rdkafka::Bindings::SizePtr.new
1124
+ size_ptr[:value] = 64
1125
+ value_ptr = FFI::MemoryPointer.new(:char, 64)
1126
+
1127
+ result = Rdkafka::Bindings.rd_kafka_conf_get(conf, "isolation.level", value_ptr, size_ptr)
1128
+
1129
+ if result != :config_ok
1130
+ raise Rdkafka::Config::ConfigError.new("Could not read isolation.level: #{result}")
1131
+ end
1132
+
1133
+ if value_ptr.read_string == "read_committed"
1134
+ Rdkafka::Bindings::RD_KAFKA_ISOLATION_LEVEL_READ_COMMITTED
1135
+ else
1136
+ Rdkafka::Bindings::RD_KAFKA_ISOLATION_LEVEL_READ_UNCOMMITTED
1137
+ end
1138
+ end
1139
+ end
995
1140
 
996
1141
  # Returns the consumer queue pointer, lazily initialized
997
1142
  # @return [FFI::Pointer] consumer queue handle
998
1143
  def consumer_queue
999
1144
  @consumer_queue ||= @native_kafka.with_inner do |inner|
1000
- Rdkafka::Bindings.rd_kafka_queue_get_consumer(inner)
1145
+ queue = Rdkafka::Bindings.rd_kafka_queue_get_consumer(inner)
1146
+ # Share the pointer with the finalizer so it is destroyed even if the consumer is GC'd
1147
+ # without an explicit close.
1148
+ @consumer_queue_holder[0] = queue
1149
+ queue
1150
+ end
1151
+ end
1152
+
1153
+ # Builds an RdkafkaError from a native message, promoting to a fatal error when
1154
+ # librdkafka flagged a fatal condition on the underlying client handle.
1155
+ # @param native_message [Rdkafka::Bindings::Message] the errored native message
1156
+ # @return [RdkafkaError]
1157
+ def build_batch_error(native_message)
1158
+ err = Rdkafka::RdkafkaError.build(native_message)
1159
+ if err && err.rdkafka_response == Rdkafka::Bindings::RD_KAFKA_RESP_ERR__FATAL
1160
+ @native_kafka.with_inner do |inner|
1161
+ Rdkafka::RdkafkaError.build_fatal(inner, fallback_error_code: err.rdkafka_response)
1162
+ end
1163
+ else
1164
+ err
1001
1165
  end
1002
1166
  end
1003
1167
 
@@ -22,6 +22,10 @@ module Rdkafka
22
22
  # @see Consumer#offsets_for_times
23
23
  CONSUMER_OFFSETS_FOR_TIMES_TIMEOUT_MS = 1_000
24
24
 
25
+ # Consumer timeout for fetching the cluster id when it is not already cached from metadata
26
+ # @see Consumer#cluster_id
27
+ CONSUMER_CLUSTER_ID_TIMEOUT_MS = 1_000
28
+
25
29
  # Consumer timeout for poll operations (used in each iteration)
26
30
  # @see Consumer#each
27
31
  CONSUMER_POLL_TIMEOUT_MS = 250
@@ -55,14 +59,30 @@ module Rdkafka
55
59
  # @see Admin#metadata
56
60
  METADATA_TIMEOUT_MS = 2_000
57
61
 
58
- # Maximum retries for metadata requests on transient errors
62
+ # Hard ceiling on metadata fetch attempts on transient errors (backstop; the retry budget
63
+ # normally ends the loop first)
59
64
  # @see Metadata#initialize
60
65
  METADATA_MAX_RETRIES = 10
61
66
 
67
+ # Minimum metadata fetch attempts before the retry budget may end the loop, so a slow broker
68
+ # (whose requests each consume the full timeout) still gets a few tries
69
+ # @see Metadata#initialize
70
+ METADATA_MIN_ATTEMPTS = 3
71
+
72
+ # Soft wall-clock budget for the whole metadata retry loop; past it (and past
73
+ # METADATA_MIN_ATTEMPTS) the loop stops so a synchronous fetch cannot block the caller for long
74
+ # @see Metadata#initialize
75
+ METADATA_RETRY_BUDGET_MS = 5_000
76
+
62
77
  # Base backoff time for metadata retry (100ms = 0.1s)
63
78
  # @see Metadata#initialize
64
79
  METADATA_RETRY_BACKOFF_BASE_MS = 100
65
80
 
81
+ # Maximum backoff time between metadata retries. Caps the exponential backoff so a long retry
82
+ # sequence against an unhealthy cluster cannot block the calling thread for minutes.
83
+ # @see Metadata#initialize
84
+ METADATA_RETRY_BACKOFF_MAX_MS = 1_000
85
+
66
86
  # Default wait timeout for operation handles
67
87
  # @see AbstractHandle#wait
68
88
  HANDLE_WAIT_TIMEOUT_MS = 60_000
data/lib/rdkafka/error.rb CHANGED
@@ -35,8 +35,9 @@ module Rdkafka
35
35
  # @param response_ptr [FFI::Pointer] Pointer to rd_kafka_error_t
36
36
  # @param message_prefix [String, nil] Optional prefix for the error message
37
37
  # @param broker_message [String, nil] Optional broker error message
38
+ # @param instance_name [String, nil] Optional name of the rdkafka instance
38
39
  # @return [RdkafkaError, false] Error instance or false if no error
39
- def build_from_c(response_ptr, message_prefix = nil, broker_message: nil)
40
+ def build_from_c(response_ptr, message_prefix = nil, broker_message: nil, instance_name: nil)
40
41
  code = Rdkafka::Bindings.rd_kafka_error_code(response_ptr)
41
42
 
42
43
  return false if code == Bindings::RD_KAFKA_RESP_ERR_NO_ERROR
@@ -54,7 +55,8 @@ module Rdkafka
54
55
  broker_message: message,
55
56
  fatal: fatal,
56
57
  retryable: retryable,
57
- abortable: abortable
58
+ abortable: abortable,
59
+ instance_name: instance_name
58
60
  )
59
61
  end
60
62
 
@@ -92,10 +94,16 @@ module Rdkafka
92
94
  response_ptr_or_code[:err],
93
95
  message_prefix,
94
96
  broker_message: broker_message,
95
- details: details
97
+ details: details,
98
+ instance_name: instance_name
96
99
  )
97
100
  else
98
- build_from_c(response_ptr_or_code, message_prefix)
101
+ build_from_c(
102
+ response_ptr_or_code,
103
+ message_prefix,
104
+ broker_message: broker_message,
105
+ instance_name: instance_name
106
+ )
99
107
  end
100
108
  end
101
109