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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (40) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +83 -35
  3. data/README.md +2 -1
  4. data/ext/librdkafka.so +0 -0
  5. data/karafka-rdkafka.gemspec +4 -3
  6. data/lib/rdkafka/admin/acl_binding_result.rb +4 -16
  7. data/lib/rdkafka/admin/alter_consumer_group_offsets_report.rb +2 -2
  8. data/lib/rdkafka/admin/create_acl_report.rb +1 -3
  9. data/lib/rdkafka/admin/create_topic_report.rb +2 -6
  10. data/lib/rdkafka/admin/delete_consumer_group_offsets_report.rb +2 -2
  11. data/lib/rdkafka/admin/delete_groups_report.rb +2 -6
  12. data/lib/rdkafka/admin/delete_topic_report.rb +2 -6
  13. data/lib/rdkafka/admin/describe_cluster_report.rb +3 -0
  14. data/lib/rdkafka/admin/list_consumer_group_offsets_handle.rb +31 -0
  15. data/lib/rdkafka/admin/list_consumer_group_offsets_report.rb +52 -0
  16. data/lib/rdkafka/admin.rb +180 -10
  17. data/lib/rdkafka/bindings/admin/configs.rb +1 -1
  18. data/lib/rdkafka/bindings/admin.rb +14 -0
  19. data/lib/rdkafka/bindings/callbacks.rb +13 -6
  20. data/lib/rdkafka/bindings/configuration.rb +3 -3
  21. data/lib/rdkafka/bindings/consumer.rb +44 -2
  22. data/lib/rdkafka/bindings/errors.rb +1 -1
  23. data/lib/rdkafka/bindings/handle.rb +1 -1
  24. data/lib/rdkafka/bindings.rb +3 -0
  25. data/lib/rdkafka/callbacks/list_consumer_group_offsets_handler.rb +62 -0
  26. data/lib/rdkafka/callbacks.rb +5 -6
  27. data/lib/rdkafka/config.rb +70 -0
  28. data/lib/rdkafka/consumer/allocator.rb +0 -2
  29. data/lib/rdkafka/consumer/topic_partition_list.rb +7 -7
  30. data/lib/rdkafka/consumer.rb +76 -36
  31. data/lib/rdkafka/defaults.rb +6 -4
  32. data/lib/rdkafka/native_kafka.rb +45 -31
  33. data/lib/rdkafka/producer/partitions_count_cache.rb +109 -26
  34. data/lib/rdkafka/producer.rb +141 -71
  35. data/lib/rdkafka/share_consumer.rb +64 -34
  36. data/lib/rdkafka/version.rb +1 -1
  37. data/lib/rdkafka.rb +4 -1
  38. data/package-lock.json +3 -3
  39. data/renovate.json +7 -0
  40. metadata +5 -30
data/lib/rdkafka/admin.rb CHANGED
@@ -95,7 +95,7 @@ module Rdkafka
95
95
 
96
96
  # Polls for events in a non-blocking loop, yielding the count after each iteration.
97
97
  #
98
- # This method processes events (stats, errors, etc.) in a single GVL/mutex session,
98
+ # This method processes events (stats, errors, etc.) in a single GVL session,
99
99
  # which is more efficient than repeated individual polls. It uses non-blocking polls
100
100
  # internally (no GVL release between polls).
101
101
  #
@@ -116,8 +116,9 @@ module Rdkafka
116
116
  # admin.events_poll_nb_each do |_count|
117
117
  # :stop if monotonic_now >= deadline
118
118
  # end
119
- # @note This method holds the inner lock until the queue is empty or `:stop` is returned.
120
- # Other admin operations will wait until this method returns.
119
+ # @note This method runs inside `with_inner` and does not hold a lock while it loops.
120
+ # Other operations on the same client run concurrently. `close` waits until this
121
+ # method returns.
121
122
  # @note This method is thread-safe as it uses @native_kafka.with_inner synchronization
122
123
  def events_poll_nb_each
123
124
  closed_admin_check(__method__)
@@ -149,14 +150,35 @@ module Rdkafka
149
150
  # @param partition_count [Integer] number of partitions for the topic
150
151
  # @param replication_factor [Integer] replication factor for the topic
151
152
  # @param topic_config [Hash] optional topic configuration settings
153
+ # @param validate_only [Boolean] when true, the broker only validates the request and does not
154
+ # create the topic
155
+ # @param operation_timeout_ms [Integer, nil] how long the broker waits for the topic creation
156
+ # to complete before it responds (librdkafka default: 60 seconds)
157
+ # @param request_timeout_ms [Integer, nil] overall request timeout (librdkafka default:
158
+ # `socket.timeout.ms`)
159
+ # @param braceless_topic_config [Hash] topic configuration given as a hash without braces,
160
+ # e.g. `create_topic("t", 1, 1, "retention.ms" => "1000")`, merged into `topic_config`
161
+ # @option braceless_topic_config [String] any topic configuration name, e.g. `"retention.ms"`
152
162
  # @return [CreateTopicHandle] Create topic handle that can be used to wait for the result of
153
163
  # creating the topic
154
164
  # @raise [ConfigError] When the partition count or replication factor are out of valid range
155
165
  # @raise [RdkafkaError] When the topic name is invalid or the topic already exists
156
166
  # @raise [RdkafkaError] When the topic configuration is invalid
157
- def create_topic(topic_name, partition_count, replication_factor, topic_config = {})
167
+ # @raise [RdkafkaError] When librdkafka rejects a timeout value
168
+ def create_topic(
169
+ topic_name,
170
+ partition_count,
171
+ replication_factor,
172
+ topic_config = {},
173
+ validate_only: false,
174
+ operation_timeout_ms: nil,
175
+ request_timeout_ms: nil,
176
+ **braceless_topic_config
177
+ )
158
178
  closed_admin_check(__method__)
159
179
 
180
+ topic_config = (topic_config || {}).merge(braceless_topic_config) unless braceless_topic_config.empty?
181
+
160
182
  # Create a rd_kafka_NewTopic_t representing the new topic
161
183
  error_buffer = FFI::MemoryPointer.from_string(" " * 256)
162
184
  new_topic_ptr = Rdkafka::Bindings.rd_kafka_NewTopic_new(
@@ -203,6 +225,13 @@ module Rdkafka
203
225
  Rdkafka::Bindings.rd_kafka_AdminOptions_set_opaque(admin_options_ptr, create_topic_handle.to_ptr)
204
226
 
205
227
  begin
228
+ set_admin_options(
229
+ admin_options_ptr,
230
+ validate_only: validate_only,
231
+ operation_timeout_ms: operation_timeout_ms,
232
+ request_timeout_ms: request_timeout_ms
233
+ )
234
+
206
235
  @native_kafka.with_inner do |inner|
207
236
  Rdkafka::Bindings.rd_kafka_CreateTopics(
208
237
  inner,
@@ -224,6 +253,51 @@ module Rdkafka
224
253
  create_topic_handle
225
254
  end
226
255
 
256
+ # Reads the committed offsets of a consumer group from outside the group.
257
+ #
258
+ # Unlike {Consumer#committed}, this works for any group and does not join it, so it does not
259
+ # trigger a rebalance of the group it inspects.
260
+ #
261
+ # While the group coordinator is loading, not known yet or has moved (e.g. on a fresh cluster
262
+ # or after a broker restart), the handle raises `coordinator_load_in_progress`,
263
+ # `not_coordinator` or `coordinator_not_available`. Retry the call on these codes;
264
+ # `RdkafkaError#retryable?` is false for them.
265
+ #
266
+ # @param group_id [String] the group whose committed offsets we want to read
267
+ # @param topic_partition_list [TopicPartitionList, nil] partitions to read the offsets for, or
268
+ # nil to read every partition the group has committed offsets for. List the partitions
269
+ # explicitly: a topic without partitions is not expanded to its partitions
270
+ # @param require_stable_offsets [Boolean] pending offsets of an open transaction are never
271
+ # returned. Without this option the last committed offset is returned for such a partition.
272
+ # With it, the call fails with `unstable_offset_commit` until that transaction is committed
273
+ # or aborted; retry on this code
274
+ # @return [ListConsumerGroupOffsetsHandle] handle that can be used to wait for the result
275
+ # @raise [RdkafkaError] when listing the offsets fails
276
+ def list_consumer_group_offsets(group_id, topic_partition_list = nil, require_stable_offsets: false)
277
+ closed_admin_check(__method__)
278
+
279
+ native_tpl = topic_partition_list&.to_native_tpl
280
+
281
+ begin
282
+ list_ptr = Rdkafka::Bindings.rd_kafka_ListConsumerGroupOffsets_new(
283
+ FFI::MemoryPointer.from_string(group_id),
284
+ native_tpl || FFI::Pointer::NULL
285
+ )
286
+ ensure
287
+ # librdkafka copies the list into the request object, so ours is no longer needed
288
+ Rdkafka::Bindings.rd_kafka_topic_partition_list_destroy(native_tpl) if native_tpl
289
+ end
290
+
291
+ enqueue_group_offsets_op(
292
+ request_ptr: list_ptr,
293
+ handle: ListConsumerGroupOffsetsHandle.new,
294
+ admin_op: Rdkafka::Bindings::RD_KAFKA_ADMIN_OP_LISTCONSUMERGROUPOFFSETS,
295
+ destroy: Rdkafka::Bindings.method(:rd_kafka_ListConsumerGroupOffsets_destroy),
296
+ enqueue: Rdkafka::Bindings.method(:rd_kafka_ListConsumerGroupOffsets),
297
+ configure: ->(admin_options_ptr) { set_require_stable_offsets(admin_options_ptr) if require_stable_offsets }
298
+ )
299
+ end
300
+
227
301
  # Sets the committed offsets of a consumer group from outside the group.
228
302
  #
229
303
  # Kafka requires the group to be empty (no active members) for this to be accepted; when it
@@ -748,10 +822,15 @@ module Rdkafka
748
822
  # Deletes the named topic
749
823
  #
750
824
  # @param topic_name [String] name of the topic to delete
825
+ # @param operation_timeout_ms [Integer, nil] how long the broker waits for the topic deletion
826
+ # to complete before it responds (librdkafka default: 60 seconds)
827
+ # @param request_timeout_ms [Integer, nil] overall request timeout (librdkafka default:
828
+ # `socket.timeout.ms`)
751
829
  # @return [DeleteTopicHandle] Delete topic handle that can be used to wait for the result of
752
830
  # deleting the topic
753
831
  # @raise [RdkafkaError] When the topic name is invalid or the topic does not exist
754
- def delete_topic(topic_name)
832
+ # @raise [RdkafkaError] When librdkafka rejects a timeout value
833
+ def delete_topic(topic_name, operation_timeout_ms: nil, request_timeout_ms: nil)
755
834
  closed_admin_check(__method__)
756
835
 
757
836
  # Create a rd_kafka_DeleteTopic_t representing the topic to be deleted
@@ -782,6 +861,12 @@ module Rdkafka
782
861
  Rdkafka::Bindings.rd_kafka_AdminOptions_set_opaque(admin_options_ptr, delete_topic_handle.to_ptr)
783
862
 
784
863
  begin
864
+ set_admin_options(
865
+ admin_options_ptr,
866
+ operation_timeout_ms: operation_timeout_ms,
867
+ request_timeout_ms: request_timeout_ms
868
+ )
869
+
785
870
  @native_kafka.with_inner do |inner|
786
871
  Rdkafka::Bindings.rd_kafka_DeleteTopics(
787
872
  inner,
@@ -807,12 +892,25 @@ module Rdkafka
807
892
  #
808
893
  # @param topic_name [String] name of the topic
809
894
  # @param partition_count [Integer] how many partitions we want to end up with for given topic
895
+ # @param validate_only [Boolean] when true, the broker only validates the request and does not
896
+ # add the partitions
897
+ # @param operation_timeout_ms [Integer, nil] how long the broker waits for the partitions to be
898
+ # created before it responds (librdkafka default: 60 seconds)
899
+ # @param request_timeout_ms [Integer, nil] overall request timeout (librdkafka default:
900
+ # `socket.timeout.ms`)
810
901
  # @return [CreatePartitionsHandle] Create partitions handle that can be used to wait for the
811
902
  # result
812
903
  # @raise [ConfigError] When the partition count or replication factor are out of valid range
813
904
  # @raise [RdkafkaError] When the topic name is invalid or the topic already exists
814
905
  # @raise [RdkafkaError] When the topic configuration is invalid
815
- def create_partitions(topic_name, partition_count)
906
+ # @raise [RdkafkaError] When librdkafka rejects a timeout value
907
+ def create_partitions(
908
+ topic_name,
909
+ partition_count,
910
+ validate_only: false,
911
+ operation_timeout_ms: nil,
912
+ request_timeout_ms: nil
913
+ )
816
914
  closed_admin_check(__method__)
817
915
 
818
916
  @native_kafka.with_inner do |inner|
@@ -847,6 +945,13 @@ module Rdkafka
847
945
  Rdkafka::Bindings.rd_kafka_AdminOptions_set_opaque(admin_options_ptr, create_partitions_handle.to_ptr)
848
946
 
849
947
  begin
948
+ set_admin_options(
949
+ admin_options_ptr,
950
+ validate_only: validate_only,
951
+ operation_timeout_ms: operation_timeout_ms,
952
+ request_timeout_ms: request_timeout_ms
953
+ )
954
+
850
955
  Rdkafka::Bindings.rd_kafka_CreatePartitions(
851
956
  inner,
852
957
  topics_array_ptr,
@@ -1431,8 +1536,8 @@ module Rdkafka
1431
1536
 
1432
1537
  private
1433
1538
 
1434
- # Shared enqueue path for the two consumer group offset admin operations. They differ only
1435
- # in their C entry points, so keeping one body avoids two near identical copies of the
1539
+ # Shared enqueue path for the consumer group offset admin operations. They differ only in
1540
+ # their C entry points, so keeping one body avoids near identical copies of the
1436
1541
  # queue/handle/options dance and its cleanup.
1437
1542
  #
1438
1543
  # @param request_ptr [FFI::Pointer] the op specific request object
@@ -1440,8 +1545,13 @@ module Rdkafka
1440
1545
  # @param admin_op [Integer] the RD_KAFKA_ADMIN_OP_* constant for this operation
1441
1546
  # @param destroy [Method] bound function used to free the request object
1442
1547
  # @param enqueue [Method] bound function used to enqueue the request
1548
+ # @param configure [Proc, nil] called with the `rd_kafka_AdminOptions_t` pointer before the
1549
+ # request is enqueued, to set an option only this operation supports. For example
1550
+ # `#list_consumer_group_offsets` always passes a lambda that takes `admin_options_ptr` and
1551
+ # calls `set_require_stable_offsets(admin_options_ptr)` only when
1552
+ # `require_stable_offsets: true`. nil (alter and delete) sets no extra option
1443
1553
  # @return [AbstractHandle] the registered handle
1444
- def enqueue_group_offsets_op(request_ptr:, handle:, admin_op:, destroy:, enqueue:)
1554
+ def enqueue_group_offsets_op(request_ptr:, handle:, admin_op:, destroy:, enqueue:, configure: nil)
1445
1555
  array_ptr = FFI::MemoryPointer.new(:pointer)
1446
1556
  array_ptr.write_array_of_pointer([request_ptr])
1447
1557
 
@@ -1464,6 +1574,8 @@ module Rdkafka
1464
1574
  Rdkafka::Bindings.rd_kafka_AdminOptions_set_opaque(admin_options_ptr, handle.to_ptr)
1465
1575
 
1466
1576
  begin
1577
+ configure&.call(admin_options_ptr)
1578
+
1467
1579
  @native_kafka.with_inner do |inner|
1468
1580
  enqueue.call(inner, array_ptr, 1, admin_options_ptr, queue_ptr)
1469
1581
  end
@@ -1483,8 +1595,25 @@ module Rdkafka
1483
1595
  # @param admin_options_ptr [FFI::Pointer] the admin options to update
1484
1596
  # @raise [RdkafkaError] when librdkafka rejects the option
1485
1597
  def set_include_authorized_operations(admin_options_ptr)
1486
- error_ptr = Rdkafka::Bindings.rd_kafka_AdminOptions_set_include_authorized_operations(admin_options_ptr, 1)
1598
+ raise_admin_option_error(
1599
+ Rdkafka::Bindings.rd_kafka_AdminOptions_set_include_authorized_operations(admin_options_ptr, 1)
1600
+ )
1601
+ end
1602
+
1603
+ # Makes the broker fail a partition with `unstable_offset_commit` while a transaction with
1604
+ # pending offsets for it is open, instead of returning its last committed offset
1605
+ # @param admin_options_ptr [FFI::Pointer] the admin options to update
1606
+ # @raise [RdkafkaError] when librdkafka rejects the option
1607
+ def set_require_stable_offsets(admin_options_ptr)
1608
+ raise_admin_option_error(
1609
+ Rdkafka::Bindings.rd_kafka_AdminOptions_set_require_stable_offsets(admin_options_ptr, 1)
1610
+ )
1611
+ end
1487
1612
 
1613
+ # Raises and releases the error librdkafka returned from setting an admin option
1614
+ # @param error_ptr [FFI::Pointer] the returned `rd_kafka_error_t*`, NULL on success
1615
+ # @raise [RdkafkaError] when the error pointer is not NULL
1616
+ def raise_admin_option_error(error_ptr)
1488
1617
  return if error_ptr.null?
1489
1618
 
1490
1619
  begin
@@ -1499,6 +1628,47 @@ module Rdkafka
1499
1628
  end
1500
1629
  end
1501
1630
 
1631
+ # Applies the optional request settings to admin options
1632
+ # @param admin_options_ptr [FFI::Pointer] the admin options to update
1633
+ # @param validate_only [Boolean] whether the broker should only validate the request
1634
+ # @param operation_timeout_ms [Integer, nil] broker operation timeout, librdkafka default if nil
1635
+ # @param request_timeout_ms [Integer, nil] request timeout, librdkafka default if nil
1636
+ # @raise [RdkafkaError] when librdkafka rejects a value
1637
+ def set_admin_options(admin_options_ptr, validate_only: false, operation_timeout_ms: nil, request_timeout_ms: nil)
1638
+ error_buffer = FFI::MemoryPointer.new(:char, 256)
1639
+
1640
+ if validate_only
1641
+ check_admin_option(
1642
+ Rdkafka::Bindings.rd_kafka_AdminOptions_set_validate_only(admin_options_ptr, 1, error_buffer, 256),
1643
+ error_buffer
1644
+ )
1645
+ end
1646
+
1647
+ if operation_timeout_ms
1648
+ check_admin_option(
1649
+ Rdkafka::Bindings.rd_kafka_AdminOptions_set_operation_timeout(admin_options_ptr, operation_timeout_ms, error_buffer, 256),
1650
+ error_buffer
1651
+ )
1652
+ end
1653
+
1654
+ if request_timeout_ms
1655
+ check_admin_option(
1656
+ Rdkafka::Bindings.rd_kafka_AdminOptions_set_request_timeout(admin_options_ptr, request_timeout_ms, error_buffer, 256),
1657
+ error_buffer
1658
+ )
1659
+ end
1660
+ end
1661
+
1662
+ # Raises when an admin options setter failed
1663
+ # @param response [Integer] result code of an admin options setter
1664
+ # @param error_buffer [FFI::MemoryPointer] buffer the setter wrote its error into
1665
+ # @raise [RdkafkaError] when the setter failed
1666
+ def check_admin_option(response, error_buffer)
1667
+ return if response == Rdkafka::Bindings::RD_KAFKA_RESP_ERR_NO_ERROR
1668
+
1669
+ raise RdkafkaError.new(response, broker_message: error_buffer.read_string)
1670
+ end
1671
+
1502
1672
  # Checks if the admin is closed and raises an error if so
1503
1673
  # @param method [Symbol] name of the calling method for error context
1504
1674
  # @raise [ClosedAdminError] when the admin is closed
@@ -5,7 +5,7 @@ module Rdkafka
5
5
  attach_function :rd_kafka_DescribeConfigs, [:pointer, :pointer, :size_t, :pointer, :pointer], :void, blocking: true
6
6
  attach_function :rd_kafka_ConfigResource_new, [:int32, :pointer], :pointer
7
7
  attach_function :rd_kafka_ConfigResource_destroy, [:pointer], :void
8
- attach_function :rd_kafka_ConfigResource_destroy_array, [:pointer, :int32], :void
8
+ attach_function :rd_kafka_ConfigResource_destroy_array, [:pointer, :size_t], :void
9
9
  attach_function :rd_kafka_event_DescribeConfigs_result, [:pointer], :pointer
10
10
  attach_function :rd_kafka_DescribeConfigs_result_resources, [:pointer, :pointer], :pointer
11
11
  attach_function :rd_kafka_ConfigResource_configs, [:pointer, :pointer], :pointer
@@ -53,6 +53,17 @@ module Rdkafka
53
53
  attach_function :rd_kafka_event_AlterConsumerGroupOffsets_result, [:pointer], :pointer, blocking: true # rd_kafka_event_t* => rd_kafka_AlterConsumerGroupOffsets_result_t*
54
54
  attach_function :rd_kafka_AlterConsumerGroupOffsets_result_groups, [:pointer, :pointer], :pointer, blocking: true # rd_kafka_AlterConsumerGroupOffsets_result_t*, size_t* => rd_kafka_group_result_t**
55
55
 
56
+ # List Consumer Group Offsets
57
+ RD_KAFKA_ADMIN_OP_LISTCONSUMERGROUPOFFSETS = 14 # rd_kafka_admin_op_t
58
+ RD_KAFKA_EVENT_LISTCONSUMERGROUPOFFSETS_RESULT = 0x8000 # rd_kafka_event_type_t
59
+
60
+ attach_function :rd_kafka_ListConsumerGroupOffsets, [:pointer, :pointer, :size_t, :pointer, :pointer], :void, blocking: true
61
+ attach_function :rd_kafka_ListConsumerGroupOffsets_new, [:pointer, :pointer], :pointer, blocking: true # const char*, const rd_kafka_topic_partition_list_t* => rd_kafka_ListConsumerGroupOffsets_t*
62
+ attach_function :rd_kafka_ListConsumerGroupOffsets_destroy, [:pointer], :void, blocking: true
63
+ attach_function :rd_kafka_event_ListConsumerGroupOffsets_result, [:pointer], :pointer, blocking: true # rd_kafka_event_t* => rd_kafka_ListConsumerGroupOffsets_result_t*
64
+ attach_function :rd_kafka_ListConsumerGroupOffsets_result_groups, [:pointer, :pointer], :pointer, blocking: true # rd_kafka_ListConsumerGroupOffsets_result_t*, size_t* => rd_kafka_group_result_t**
65
+ attach_function :rd_kafka_AdminOptions_set_require_stable_offsets, [:pointer, :int], :pointer, blocking: true # => rd_kafka_error_t*
66
+
56
67
  # Delete Consumer Group Offsets
57
68
  RD_KAFKA_ADMIN_OP_DELETECONSUMERGROUPOFFSETS = 8 # rd_kafka_admin_op_t
58
69
  RD_KAFKA_EVENT_DELETECONSUMERGROUPOFFSETS_RESULT = 107 # rd_kafka_event_type_t
@@ -171,6 +182,9 @@ module Rdkafka
171
182
  attach_function :rd_kafka_AdminOptions_new, [:pointer, :int32], :pointer
172
183
  attach_function :rd_kafka_AdminOptions_set_opaque, [:pointer, :pointer], :void
173
184
  attach_function :rd_kafka_AdminOptions_destroy, [:pointer], :void
185
+ attach_function :rd_kafka_AdminOptions_set_request_timeout, [:pointer, :int, :pointer, :size_t], :int32
186
+ attach_function :rd_kafka_AdminOptions_set_operation_timeout, [:pointer, :int, :pointer, :size_t], :int32
187
+ attach_function :rd_kafka_AdminOptions_set_validate_only, [:pointer, :int, :pointer, :size_t], :int32
174
188
 
175
189
  # Extracting data from event types
176
190
 
@@ -27,7 +27,10 @@ module Rdkafka
27
27
  StatsCallback = FFI::Function.new(
28
28
  :int, [:pointer, :string, :int, :pointer]
29
29
  ) do |_client_ptr, json, _json_len, _opaque|
30
- if Rdkafka::Config.statistics_callback
30
+ # Read the callback once: another thread may clear it while this one runs
31
+ statistics_callback = Rdkafka::Config.statistics_callback
32
+
33
+ if statistics_callback
31
34
  stats = JSON.parse(json)
32
35
 
33
36
  # If user requested statistics callbacks, we can use the statistics data to get the
@@ -52,7 +55,7 @@ module Rdkafka
52
55
  Rdkafka::Producer.partitions_count_cache.set(topic_name, partitions_count)
53
56
  end
54
57
 
55
- Rdkafka::Config.statistics_callback.call(stats)
58
+ statistics_callback.call(stats)
56
59
  end
57
60
 
58
61
  # Return 0 so librdkafka frees the json string
@@ -88,7 +91,9 @@ module Rdkafka
88
91
  ErrorCallback = FFI::Function.new(
89
92
  :void, [:pointer, :int, :string, :pointer]
90
93
  ) do |client_ptr, err_code, reason, _opaque|
91
- if Rdkafka::Config.error_callback
94
+ error_callback = Rdkafka::Config.error_callback
95
+
96
+ if error_callback
92
97
  instance_name = client_ptr.null? ? nil : Rdkafka::Bindings.rd_kafka_name(client_ptr)
93
98
 
94
99
  # Handle fatal errors according to librdkafka documentation:
@@ -106,7 +111,7 @@ module Rdkafka
106
111
  end
107
112
 
108
113
  error.set_backtrace(caller)
109
- Rdkafka::Config.error_callback.call(error)
114
+ error_callback.call(error)
110
115
  end
111
116
  end
112
117
 
@@ -132,7 +137,9 @@ module Rdkafka
132
137
  OAuthbearerTokenRefreshCallback = FFI::Function.new(
133
138
  :void, [:pointer, :string, :pointer]
134
139
  ) do |client_ptr, config, opaque_ptr|
135
- if Rdkafka::Config.oauthbearer_token_refresh_callback && !client_ptr.null?
140
+ refresh_callback = Rdkafka::Config.oauthbearer_token_refresh_callback
141
+
142
+ if refresh_callback && !client_ptr.null?
136
143
  client_name = Rdkafka::Bindings.rd_kafka_name(client_ptr)
137
144
 
138
145
  # Share consumers have no way to query their librdkafka client name (the share handle
@@ -141,7 +148,7 @@ module Rdkafka
141
148
  # ShareConsumer#name.
142
149
  Rdkafka::Config.opaques[opaque_ptr.to_i]&.capture_client_name(client_name)
143
150
 
144
- Rdkafka::Config.oauthbearer_token_refresh_callback.call(config, client_name)
151
+ refresh_callback.call(config, client_name)
145
152
  end
146
153
  end
147
154
  end
@@ -11,7 +11,7 @@ module Rdkafka
11
11
  ]
12
12
 
13
13
  attach_function :rd_kafka_conf_new, [], :pointer
14
- attach_function :rd_kafka_conf_set, [:pointer, :string, :string, :pointer, :int], :kafka_config_response
14
+ attach_function :rd_kafka_conf_set, [:pointer, :string, :string, :pointer, :size_t], :kafka_config_response
15
15
  attach_function :rd_kafka_conf_get, [:pointer, :string, :pointer, :pointer], :kafka_config_response
16
16
  attach_function :rd_kafka_conf, [:pointer], :pointer
17
17
  attach_function :rd_kafka_conf_dump, [:pointer, :pointer], :pointer
@@ -27,7 +27,7 @@ module Rdkafka
27
27
  attach_function :rd_kafka_rebalance_protocol, [:pointer], :string
28
28
  callback :oauthbearer_token_refresh_cb, [:pointer, :string, :pointer], :void
29
29
  attach_function :rd_kafka_conf_set_oauthbearer_token_refresh_cb, [:pointer, :oauthbearer_token_refresh_cb], :void
30
- attach_function :rd_kafka_oauthbearer_set_token, [:pointer, :string, :int64, :pointer, :pointer, :int, :pointer, :int], :int
30
+ attach_function :rd_kafka_oauthbearer_set_token, [:pointer, :string, :int64, :pointer, :pointer, :size_t, :pointer, :size_t], :int
31
31
  attach_function :rd_kafka_oauthbearer_set_token_failure, [:pointer, :string], :int
32
32
  # Log queue
33
33
  attach_function :rd_kafka_set_log_queue, [:pointer, :pointer], :void
@@ -45,6 +45,6 @@ module Rdkafka
45
45
  # Per topic configs
46
46
  attach_function :rd_kafka_topic_conf_new, [], :pointer
47
47
  attach_function :rd_kafka_topic_conf_destroy, [:pointer], :void
48
- attach_function :rd_kafka_topic_conf_set, [:pointer, :string, :string, :pointer, :int], :kafka_config_response
48
+ attach_function :rd_kafka_topic_conf_set, [:pointer, :string, :string, :pointer, :size_t], :kafka_config_response
49
49
  end
50
50
  end
@@ -19,7 +19,7 @@ module Rdkafka
19
19
  # Non-blocking consumer poll variant (does not release GVL)
20
20
  # More efficient for poll(0) calls in fiber schedulers.
21
21
  attach_function :rd_kafka_consumer_poll_nb, :rd_kafka_consumer_poll, [:pointer, :int], :pointer, blocking: false
22
- attach_function :rd_kafka_consumer_close, [:pointer], :void, blocking: true
22
+ attach_function :rd_kafka_consumer_close, [:pointer], :int, blocking: true
23
23
  attach_function :rd_kafka_queue_get_consumer, [:pointer], :pointer
24
24
  attach_function :rd_kafka_consume_batch_queue, [:pointer, :int, :pointer, :size_t], :ssize_t, blocking: true
25
25
  attach_function :rd_kafka_consume_batch_queue_nb, :rd_kafka_consume_batch_queue, [:pointer, :int, :pointer, :size_t], :ssize_t, blocking: false
@@ -61,7 +61,7 @@ module Rdkafka
61
61
  end
62
62
 
63
63
  opaque = Rdkafka::Config.opaques[opaque_ptr.to_i]
64
- return unless opaque
64
+ next unless opaque
65
65
 
66
66
  tpl = Rdkafka::Consumer::TopicPartitionList.from_native_tpl(partitions_ptr).freeze
67
67
  begin
@@ -76,6 +76,48 @@ module Rdkafka
76
76
  end
77
77
  end
78
78
 
79
+ # Offset commit
80
+
81
+ callback :offset_commit_cb_function, [:pointer, :int, :pointer, :pointer], :void
82
+ attach_function :rd_kafka_conf_set_offset_commit_cb, [:pointer, :offset_commit_cb_function], :void, blocking: true
83
+
84
+ # Served from the consumer poll (or the events poll when `consumer_poll_set` is false) and
85
+ # from the consumer close, on the thread that calls them
86
+ OffsetCommitCallback = FFI::Function.new(
87
+ :void, [:pointer, :int, :pointer, :pointer]
88
+ ) do |_client_ptr, code, offsets_ptr, opaque_ptr|
89
+ opaque = Rdkafka::Config.opaques[opaque_ptr.to_i]
90
+ next unless opaque
91
+
92
+ # Success and `no_offset` (a commit with nothing new to commit) are not failures
93
+ failed = code != RD_KAFKA_RESP_ERR_NO_ERROR && code != RD_KAFKA_RESP_ERR__NO_OFFSET
94
+ every = opaque.offsets_committed_listened?
95
+ on_failure = failed && opaque.offsets_commit_failed_listened?
96
+
97
+ # The list has an entry for every assigned partition, so build it only when it is used
98
+ if every || on_failure
99
+ error = code.zero? ? nil : Rdkafka::RdkafkaError.new(code)
100
+ tpl = if offsets_ptr.null?
101
+ Rdkafka::Consumer::TopicPartitionList.new
102
+ else
103
+ Rdkafka::Consumer::TopicPartitionList.from_native_tpl(offsets_ptr)
104
+ end
105
+ tpl.freeze
106
+
107
+ begin
108
+ opaque.call_on_offsets_committed(error, tpl) if every
109
+ rescue Exception => err
110
+ Rdkafka::Config.logger.error("Unhandled exception: #{err.class} - #{err.message}")
111
+ end
112
+
113
+ begin
114
+ opaque.call_on_offsets_commit_failed(error, tpl) if on_failure
115
+ rescue Exception => err
116
+ Rdkafka::Config.logger.error("Unhandled exception: #{err.class} - #{err.message}")
117
+ end
118
+ end
119
+ end
120
+
79
121
  # Watermark offsets
80
122
 
81
123
  attach_function :rd_kafka_query_watermark_offsets, [:pointer, :string, :int, :pointer, :pointer, :int], :int, blocking: true
@@ -11,7 +11,7 @@ module Rdkafka
11
11
  attach_function :rd_kafka_error_code, [:pointer], :int32
12
12
  attach_function :rd_kafka_error_string, [:pointer], :pointer
13
13
  attach_function :rd_kafka_get_err_descs, [:pointer, :pointer], :void
14
- attach_function :rd_kafka_fatal_error, [:pointer, :pointer, :int], :int
14
+ attach_function :rd_kafka_fatal_error, [:pointer, :pointer, :size_t], :int
15
15
  attach_function :rd_kafka_test_fatal_error, [:pointer, :int, :string], :int
16
16
  end
17
17
  end
@@ -9,7 +9,7 @@ module Rdkafka
9
9
  :rd_kafka_consumer
10
10
  ]
11
11
 
12
- attach_function :rd_kafka_new, [:kafka_type, :pointer, :pointer, :int], :pointer
12
+ attach_function :rd_kafka_new, [:kafka_type, :pointer, :pointer, :size_t], :pointer
13
13
 
14
14
  attach_function :rd_kafka_destroy, [:pointer], :void
15
15
  end
@@ -58,13 +58,16 @@ module Rdkafka
58
58
  end
59
59
  end
60
60
 
61
+ RD_KAFKA_RESP_ERR__DESTROY = -197
61
62
  RD_KAFKA_RESP_ERR__TIMED_OUT = -185
62
63
  RD_KAFKA_RESP_ERR__ASSIGN_PARTITIONS = -175
63
64
  RD_KAFKA_RESP_ERR__REVOKE_PARTITIONS = -174
64
65
  RD_KAFKA_RESP_ERR__STATE = -172
66
+ RD_KAFKA_RESP_ERR__NO_OFFSET = -168
65
67
  RD_KAFKA_RESP_ERR__NOENT = -156
66
68
  RD_KAFKA_RESP_ERR__FATAL = -150
67
69
  RD_KAFKA_RESP_ERR_NO_ERROR = 0
70
+ RD_KAFKA_RESP_ERR_INVALID_PRODUCER_EPOCH = 47
68
71
 
69
72
  # Buffer size for fatal error strings, matches librdkafka expectations
70
73
  FATAL_ERROR_BUFFER_SIZE = 256
@@ -0,0 +1,62 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Rdkafka
4
+ module Callbacks
5
+ # Handles `RD_KAFKA_EVENT_LISTCONSUMERGROUPOFFSETS_RESULT` events
6
+ # @private
7
+ class ListConsumerGroupOffsetsHandler < BaseHandler
8
+ class << self
9
+ # Resolves the list consumer group offsets handle from its result event
10
+ # @param event_ptr [FFI::Pointer] pointer to the event
11
+ # @return [void]
12
+ def call(event_ptr)
13
+ result = Rdkafka::Bindings.rd_kafka_event_ListConsumerGroupOffsets_result(event_ptr)
14
+
15
+ pointer_to_size_t = FFI::MemoryPointer.new(:size_t)
16
+ group_result_array = Rdkafka::Bindings.rd_kafka_ListConsumerGroupOffsets_result_groups(result, pointer_to_size_t)
17
+ group_results = GroupResult.create_group_results_from_array(pointer_to_size_t.read_int, group_result_array)
18
+ handle_ptr = Rdkafka::Bindings.rd_kafka_event_opaque(event_ptr)
19
+
20
+ if (handle = Rdkafka::Admin::ListConsumerGroupOffsetsHandle.remove(handle_ptr.address))
21
+ unless resolve_operation_error(event_ptr, handle)
22
+ group_result = group_results[0]
23
+
24
+ # librdkafka only supports one group per invocation
25
+ if group_result.nil?
26
+ handle[:response] = Rdkafka::Bindings::RD_KAFKA_RESP_ERR_NO_ERROR
27
+ handle.result = Rdkafka::Admin::ListConsumerGroupOffsetsReport.new(
28
+ FFI::Pointer::NULL,
29
+ FFI::Pointer::NULL
30
+ )
31
+ else
32
+ report = Rdkafka::Admin::ListConsumerGroupOffsetsReport.new(
33
+ group_result.error_string,
34
+ group_result.result_name,
35
+ Rdkafka::Bindings.rd_kafka_group_result_partitions(group_result_array.read_pointer)
36
+ )
37
+ handle.result = report
38
+
39
+ # The group level error can be clean while a partition failed (e.g.
40
+ # `unstable_offset_commit` with `require_stable_offsets`), which must not look
41
+ # like a committed offset of nil.
42
+ failed = report.partitions.find { |partition| partition[:error] }
43
+
44
+ if !group_result.result_error.zero?
45
+ handle[:response] = group_result.result_error
46
+ handle.broker_message = report.error_string
47
+ elsif failed
48
+ handle[:response] = failed[:error].rdkafka_response
49
+ handle.broker_message = "Failed for partition #{failed[:partition]} of '#{failed[:topic]}'"
50
+ else
51
+ handle[:response] = Rdkafka::Bindings::RD_KAFKA_RESP_ERR_NO_ERROR
52
+ end
53
+ end
54
+
55
+ handle.unlock
56
+ end
57
+ end
58
+ end
59
+ end
60
+ end
61
+ end
62
+ end
@@ -216,6 +216,7 @@ module Rdkafka
216
216
  when Rdkafka::Bindings::RD_KAFKA_ADMIN_OP_CREATEPARTITIONS_RESULT then CreatePartitionsHandler
217
217
  when Rdkafka::Bindings::RD_KAFKA_EVENT_DELETEGROUPS_RESULT then DeleteGroupsHandler
218
218
  when Rdkafka::Bindings::RD_KAFKA_EVENT_ALTERCONSUMERGROUPOFFSETS_RESULT then AlterConsumerGroupOffsetsHandler
219
+ when Rdkafka::Bindings::RD_KAFKA_EVENT_LISTCONSUMERGROUPOFFSETS_RESULT then ListConsumerGroupOffsetsHandler
219
220
  when Rdkafka::Bindings::RD_KAFKA_EVENT_DELETECONSUMERGROUPOFFSETS_RESULT then DeleteConsumerGroupOffsetsHandler
220
221
  when Rdkafka::Bindings::RD_KAFKA_EVENT_DELETERECORDS_RESULT then DeleteRecordsHandler
221
222
  when Rdkafka::Bindings::RD_KAFKA_EVENT_CREATEACLS_RESULT then CreateAclHandler
@@ -250,8 +251,6 @@ module Rdkafka
250
251
  message = Rdkafka::Bindings::Message.new(message_ptr)
251
252
  delivery_handle_ptr_address = message[:_private].address
252
253
  if delivery_handle = Rdkafka::Producer::DeliveryHandle.remove(delivery_handle_ptr_address)
253
- topic_name = Rdkafka::Bindings.rd_kafka_topic_name(message[:rkt])
254
-
255
254
  # Update delivery handle
256
255
  delivery_handle[:response] = message[:err]
257
256
  delivery_handle[:partition] = message[:partition]
@@ -259,9 +258,9 @@ module Rdkafka
259
258
  delivery_handle[:status] = Rdkafka::Bindings.rd_kafka_message_status(message_ptr)
260
259
  delivery_handle[:latency] = Rdkafka::Bindings.rd_kafka_message_latency(message_ptr)
261
260
  delivery_handle[:broker_id] = Rdkafka::Bindings.rd_kafka_message_broker_id(message_ptr)
262
- # The topic is not stored on the handle here: it is already set as the handle's
263
- # `topic` Ruby attribute during produce, which spares a native string copy that
264
- # would otherwise be allocated and retained for every message.
261
+ # The topic is taken from the handle's `topic` Ruby attribute set during produce, not
262
+ # read back from librdkafka, which spares an FFI call and a native string copy for
263
+ # every message.
265
264
 
266
265
  begin
267
266
  # Call delivery callback on opaque
@@ -270,7 +269,7 @@ module Rdkafka
270
269
  Rdkafka::Producer::DeliveryReport.new(
271
270
  message[:partition],
272
271
  message[:offset],
273
- topic_name,
272
+ delivery_handle.topic,
274
273
  message[:err],
275
274
  delivery_handle.label,
276
275
  status: delivery_handle[:status],