rdkafka 0.22.2 → 0.29.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (120) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +101 -3
  3. data/README.md +21 -14
  4. data/Rakefile +21 -21
  5. data/bin/verify_kafka_warnings +39 -0
  6. data/dist/{librdkafka-2.8.0.tar.gz → librdkafka-2.14.2.tar.gz} +0 -0
  7. data/docker-compose-ssl.yml +35 -0
  8. data/docker-compose.yml +2 -2
  9. data/ext/Rakefile +27 -27
  10. data/lib/rdkafka/abstract_handle.rb +54 -7
  11. data/lib/rdkafka/admin/acl_binding_result.rb +5 -5
  12. data/lib/rdkafka/admin/config_binding_result.rb +2 -2
  13. data/lib/rdkafka/admin/config_resource_binding_result.rb +1 -0
  14. data/lib/rdkafka/admin/create_acl_handle.rb +10 -6
  15. data/lib/rdkafka/admin/create_acl_report.rb +3 -2
  16. data/lib/rdkafka/admin/create_partitions_handle.rb +8 -7
  17. data/lib/rdkafka/admin/create_partitions_report.rb +1 -0
  18. data/lib/rdkafka/admin/create_topic_handle.rb +8 -7
  19. data/lib/rdkafka/admin/create_topic_report.rb +3 -0
  20. data/lib/rdkafka/admin/delete_acl_handle.rb +9 -8
  21. data/lib/rdkafka/admin/delete_acl_report.rb +5 -3
  22. data/lib/rdkafka/admin/delete_groups_handle.rb +10 -7
  23. data/lib/rdkafka/admin/delete_groups_report.rb +3 -0
  24. data/lib/rdkafka/admin/delete_topic_handle.rb +8 -7
  25. data/lib/rdkafka/admin/delete_topic_report.rb +3 -0
  26. data/lib/rdkafka/admin/describe_acl_handle.rb +9 -8
  27. data/lib/rdkafka/admin/describe_acl_report.rb +5 -3
  28. data/lib/rdkafka/admin/describe_configs_handle.rb +7 -10
  29. data/lib/rdkafka/admin/describe_configs_report.rb +8 -6
  30. data/lib/rdkafka/admin/incremental_alter_configs_handle.rb +7 -10
  31. data/lib/rdkafka/admin/incremental_alter_configs_report.rb +8 -6
  32. data/lib/rdkafka/admin/list_offsets_handle.rb +31 -0
  33. data/lib/rdkafka/admin/list_offsets_report.rb +51 -0
  34. data/lib/rdkafka/admin.rb +357 -159
  35. data/lib/rdkafka/bindings.rb +205 -111
  36. data/lib/rdkafka/callbacks/base_handler.rb +62 -0
  37. data/lib/rdkafka/callbacks/create_acl_handler.rb +37 -0
  38. data/lib/rdkafka/callbacks/create_partitions_handler.rb +37 -0
  39. data/lib/rdkafka/callbacks/create_topic_handler.rb +37 -0
  40. data/lib/rdkafka/callbacks/delete_acl_handler.rb +42 -0
  41. data/lib/rdkafka/callbacks/delete_groups_handler.rb +37 -0
  42. data/lib/rdkafka/callbacks/delete_topic_handler.rb +37 -0
  43. data/lib/rdkafka/callbacks/describe_acl_handler.rb +35 -0
  44. data/lib/rdkafka/callbacks/describe_configs_handler.rb +42 -0
  45. data/lib/rdkafka/callbacks/incremental_alter_configs_handler.rb +42 -0
  46. data/lib/rdkafka/callbacks/list_offsets_handler.rb +42 -0
  47. data/lib/rdkafka/callbacks.rb +125 -216
  48. data/lib/rdkafka/config.rb +126 -66
  49. data/lib/rdkafka/consumer/headers.rb +22 -7
  50. data/lib/rdkafka/consumer/message.rb +12 -11
  51. data/lib/rdkafka/consumer/partition.rb +15 -4
  52. data/lib/rdkafka/consumer/topic_partition_list.rb +64 -38
  53. data/lib/rdkafka/consumer.rb +513 -59
  54. data/lib/rdkafka/defaults.rb +125 -0
  55. data/lib/rdkafka/error.rb +40 -14
  56. data/lib/rdkafka/helpers/metadata.rb +29 -0
  57. data/lib/rdkafka/helpers/oauth.rb +45 -13
  58. data/lib/rdkafka/helpers/time.rb +5 -0
  59. data/lib/rdkafka/metadata.rb +128 -37
  60. data/lib/rdkafka/native_kafka.rb +91 -8
  61. data/lib/rdkafka/producer/delivery_handle.rb +6 -6
  62. data/lib/rdkafka/producer/delivery_report.rb +10 -6
  63. data/lib/rdkafka/producer/partitions_count_cache.rb +51 -55
  64. data/lib/rdkafka/producer.rb +193 -95
  65. data/lib/rdkafka/version.rb +6 -3
  66. data/lib/rdkafka.rb +15 -0
  67. data/package-lock.json +331 -0
  68. data/package.json +9 -0
  69. data/rdkafka.gemspec +58 -36
  70. data/renovate.json +39 -23
  71. metadata +39 -125
  72. data/.github/CODEOWNERS +0 -3
  73. data/.github/FUNDING.yml +0 -1
  74. data/.github/workflows/ci_linux_x86_64_gnu.yml +0 -271
  75. data/.github/workflows/ci_linux_x86_64_musl.yml +0 -194
  76. data/.github/workflows/ci_macos_arm64.yml +0 -284
  77. data/.github/workflows/push_linux_x86_64_gnu.yml +0 -65
  78. data/.github/workflows/push_linux_x86_64_musl.yml +0 -79
  79. data/.github/workflows/push_macos_arm64.yml +0 -54
  80. data/.github/workflows/push_ruby.yml +0 -37
  81. data/.github/workflows/verify-action-pins.yml +0 -16
  82. data/.gitignore +0 -14
  83. data/.rspec +0 -2
  84. data/.ruby-gemset +0 -1
  85. data/.ruby-version +0 -1
  86. data/.yardopts +0 -2
  87. data/Gemfile +0 -5
  88. data/ext/README.md +0 -19
  89. data/ext/build_common.sh +0 -361
  90. data/ext/build_linux_x86_64_gnu.sh +0 -306
  91. data/ext/build_linux_x86_64_musl.sh +0 -763
  92. data/ext/build_macos_arm64.sh +0 -550
  93. data/spec/rdkafka/abstract_handle_spec.rb +0 -117
  94. data/spec/rdkafka/admin/create_acl_handle_spec.rb +0 -56
  95. data/spec/rdkafka/admin/create_acl_report_spec.rb +0 -18
  96. data/spec/rdkafka/admin/create_topic_handle_spec.rb +0 -52
  97. data/spec/rdkafka/admin/create_topic_report_spec.rb +0 -16
  98. data/spec/rdkafka/admin/delete_acl_handle_spec.rb +0 -85
  99. data/spec/rdkafka/admin/delete_acl_report_spec.rb +0 -72
  100. data/spec/rdkafka/admin/delete_topic_handle_spec.rb +0 -52
  101. data/spec/rdkafka/admin/delete_topic_report_spec.rb +0 -16
  102. data/spec/rdkafka/admin/describe_acl_handle_spec.rb +0 -85
  103. data/spec/rdkafka/admin/describe_acl_report_spec.rb +0 -73
  104. data/spec/rdkafka/admin_spec.rb +0 -971
  105. data/spec/rdkafka/bindings_spec.rb +0 -199
  106. data/spec/rdkafka/callbacks_spec.rb +0 -20
  107. data/spec/rdkafka/config_spec.rb +0 -258
  108. data/spec/rdkafka/consumer/headers_spec.rb +0 -73
  109. data/spec/rdkafka/consumer/message_spec.rb +0 -139
  110. data/spec/rdkafka/consumer/partition_spec.rb +0 -57
  111. data/spec/rdkafka/consumer/topic_partition_list_spec.rb +0 -248
  112. data/spec/rdkafka/consumer_spec.rb +0 -1274
  113. data/spec/rdkafka/error_spec.rb +0 -89
  114. data/spec/rdkafka/metadata_spec.rb +0 -79
  115. data/spec/rdkafka/native_kafka_spec.rb +0 -130
  116. data/spec/rdkafka/producer/delivery_handle_spec.rb +0 -45
  117. data/spec/rdkafka/producer/delivery_report_spec.rb +0 -25
  118. data/spec/rdkafka/producer/partitions_count_cache_spec.rb +0 -359
  119. data/spec/rdkafka/producer_spec.rb +0 -1345
  120. data/spec/spec_helper.rb +0 -195
@@ -6,7 +6,7 @@ module Rdkafka
6
6
  # configuration options is available on https://github.com/confluentinc/librdkafka/blob/master/CONFIGURATION.md.
7
7
  class Config
8
8
  # @private
9
- @@logger = Logger.new(STDOUT)
9
+ @@logger = Logger.new($stdout)
10
10
  # @private
11
11
  @@statistics_callback = nil
12
12
  # @private
@@ -33,7 +33,7 @@ module Rdkafka
33
33
  # Makes sure that there is a thread for consuming logs
34
34
  # We do not spawn thread immediately and we need to check if it operates to support forking
35
35
  def self.ensure_log_thread
36
- return if @@log_thread && @@log_thread.alive?
36
+ return if @@log_thread&.alive?
37
37
 
38
38
  @@log_mutex.synchronize do
39
39
  # Restart if dead (fork, crash)
@@ -71,11 +71,10 @@ module Rdkafka
71
71
  # You can configure if and how often this happens using `statistics.interval.ms`.
72
72
  # The callback is called with a hash that's documented here: https://github.com/confluentinc/librdkafka/blob/master/STATISTICS.md
73
73
  #
74
- # @param callback [Proc, #call] The callback
75
- #
74
+ # @param callback [Proc, #call, nil] callable object or nil to clear
76
75
  # @return [nil]
77
76
  def self.statistics_callback=(callback)
78
- raise TypeError.new("Callback has to be callable") unless callback.respond_to?(:call) || callback == nil
77
+ raise TypeError.new("Callback has to be callable") unless callback.respond_to?(:call) || callback.nil?
79
78
  @@statistics_callback = callback
80
79
  end
81
80
 
@@ -90,11 +89,10 @@ module Rdkafka
90
89
  # If this callback is not set, global errors such as brokers becoming unavailable will only be sent to the logger, as defined by librdkafka.
91
90
  # The callback is called with an instance of RdKafka::Error.
92
91
  #
93
- # @param callback [Proc, #call] The callback
94
- #
92
+ # @param callback [Proc, #call, nil] callable object to handle errors or nil to clear
95
93
  # @return [nil]
96
94
  def self.error_callback=(callback)
97
- raise TypeError.new("Callback has to be callable") unless callback.respond_to?(:call)
95
+ raise TypeError.new("Callback has to be callable") unless callback.respond_to?(:call) || callback.nil?
98
96
  @@error_callback = callback
99
97
  end
100
98
 
@@ -108,11 +106,10 @@ module Rdkafka
108
106
  # Sets the SASL/OAUTHBEARER token refresh callback.
109
107
  # This callback will be triggered when it is time to refresh the client's OAUTHBEARER token
110
108
  #
111
- # @param callback [Proc, #call] The callback
112
- #
109
+ # @param callback [Proc, #call, nil] callable object to handle token refresh or nil to clear
113
110
  # @return [nil]
114
111
  def self.oauthbearer_token_refresh_callback=(callback)
115
- raise TypeError.new("Callback has to be callable") unless callback.respond_to?(:call) || callback == nil
112
+ raise TypeError.new("Callback has to be callable") unless callback.respond_to?(:call) || callback.nil?
116
113
  @@oauthbearer_token_refresh_callback = callback
117
114
  end
118
115
 
@@ -129,15 +126,12 @@ module Rdkafka
129
126
  end
130
127
 
131
128
  # Default config that can be overwritten.
132
- DEFAULT_CONFIG = {
133
- # Request api version so advanced features work
134
- :"api.version.request" => true
135
- }.freeze
129
+ DEFAULT_CONFIG = {}.freeze
136
130
 
137
131
  # Required config that cannot be overwritten.
138
132
  REQUIRED_CONFIG = {
139
133
  # Enable log queues so we get callbacks in our own Ruby threads
140
- :"log.queue" => true
134
+ "log.queue": true
141
135
  }.freeze
142
136
 
143
137
  # Returns a new config with the provided options which are merged with {DEFAULT_CONFIG}.
@@ -174,10 +168,8 @@ module Rdkafka
174
168
 
175
169
  # Get notifications on partition assignment/revocation for the subscribed topics
176
170
  #
177
- # @param listener [Object, #on_partitions_assigned, #on_partitions_revoked] listener instance
178
- def consumer_rebalance_listener=(listener)
179
- @consumer_rebalance_listener = listener
180
- end
171
+ # @return [Object, #on_partitions_assigned, #on_partitions_revoked] listener instance
172
+ attr_writer :consumer_rebalance_listener
181
173
 
182
174
  # Should we use a single queue for the underlying consumer and events.
183
175
  #
@@ -190,10 +182,8 @@ module Rdkafka
190
182
  # It is recommended to use the defaults and only set it to `false` in advance multi-threaded
191
183
  # and complex cases where granular events handling control is needed.
192
184
  #
193
- # @param poll_set [Boolean]
194
- def consumer_poll_set=(poll_set)
195
- @consumer_poll_set = poll_set
196
- end
185
+ # @return [Boolean]
186
+ attr_writer :consumer_poll_set
197
187
 
198
188
  # Creates a consumer with this configuration.
199
189
  #
@@ -234,11 +224,13 @@ module Rdkafka
234
224
  # @param native_kafka_auto_start [Boolean] should the native kafka operations be started
235
225
  # automatically. Defaults to true. Set to false only when doing complex initialization.
236
226
  # @param native_kafka_poll_timeout_ms [Integer] ms poll time of the native Kafka
227
+ # @param run_polling_thread [Boolean] should the background polling thread be started.
228
+ # Defaults to true. Set to false when using the FD API for fiber scheduler integration.
237
229
  # @return [Producer] The created producer
238
230
  #
239
231
  # @raise [ConfigError] When the configuration contains invalid options
240
232
  # @raise [ClientCreationError] When the native client cannot be created
241
- def producer(native_kafka_auto_start: true, native_kafka_poll_timeout_ms: 100)
233
+ def producer(native_kafka_auto_start: true, native_kafka_poll_timeout_ms: Defaults::NATIVE_KAFKA_POLL_TIMEOUT_MS, run_polling_thread: true)
242
234
  # Create opaque
243
235
  opaque = Opaque.new
244
236
  # Create Kafka config
@@ -253,7 +245,7 @@ module Rdkafka
253
245
  Rdkafka::Producer.new(
254
246
  Rdkafka::NativeKafka.new(
255
247
  kafka,
256
- run_polling_thread: true,
248
+ run_polling_thread: run_polling_thread,
257
249
  opaque: opaque,
258
250
  auto_start: native_kafka_auto_start,
259
251
  timeout_ms: native_kafka_poll_timeout_ms
@@ -269,11 +261,13 @@ module Rdkafka
269
261
  # @param native_kafka_auto_start [Boolean] should the native kafka operations be started
270
262
  # automatically. Defaults to true. Set to false only when doing complex initialization.
271
263
  # @param native_kafka_poll_timeout_ms [Integer] ms poll time of the native Kafka
264
+ # @param run_polling_thread [Boolean] should the background polling thread be started.
265
+ # Defaults to true. Set to false when using the FD API for fiber scheduler integration.
272
266
  # @return [Admin] The created admin instance
273
267
  #
274
268
  # @raise [ConfigError] When the configuration contains invalid options
275
269
  # @raise [ClientCreationError] When the native client cannot be created
276
- def admin(native_kafka_auto_start: true, native_kafka_poll_timeout_ms: 100)
270
+ def admin(native_kafka_auto_start: true, native_kafka_poll_timeout_ms: Defaults::NATIVE_KAFKA_POLL_TIMEOUT_MS, run_polling_thread: true)
277
271
  opaque = Opaque.new
278
272
  config = native_config(opaque)
279
273
  Rdkafka::Bindings.rd_kafka_conf_set_background_event_cb(config, Rdkafka::Callbacks::BackgroundEventCallbackFunction)
@@ -283,7 +277,7 @@ module Rdkafka
283
277
  Rdkafka::Admin.new(
284
278
  Rdkafka::NativeKafka.new(
285
279
  kafka,
286
- run_polling_thread: true,
280
+ run_polling_thread: run_polling_thread,
287
281
  opaque: opaque,
288
282
  auto_start: native_kafka_auto_start,
289
283
  timeout_ms: native_kafka_poll_timeout_ms
@@ -291,6 +285,46 @@ module Rdkafka
291
285
  )
292
286
  end
293
287
 
288
+ # Returns all configuration properties and their current values for this config.
289
+ #
290
+ # Uses `rd_kafka_conf_dump` to retrieve every property (including defaults and
291
+ # internal properties like `client.software.name`) as a flat Hash.
292
+ #
293
+ # @note The librdkafka C API does not distinguish between producer-only, consumer-only,
294
+ # and global properties at the configuration level. All properties are returned
295
+ # regardless of the intended client type.
296
+ #
297
+ # @note The returned Hash may include sensitive values such as authentication
298
+ # credentials and key passwords. Do not log or serialize the returned data
299
+ # unless you have explicitly redacted secret entries.
300
+ #
301
+ # @return [Hash{Symbol => String}] property names mapped to their current values
302
+ #
303
+ # @raise [ConfigError] When the configuration contains invalid options
304
+ def describe_properties
305
+ config = nil
306
+ dump_ptr = nil
307
+ count = 0
308
+
309
+ config = native_config
310
+ count_ptr = Rdkafka::Bindings::SizePtr.new
311
+ dump_ptr = Rdkafka::Bindings.rd_kafka_conf_dump(config, count_ptr)
312
+
313
+ count = count_ptr[:value]
314
+ result = {}
315
+
316
+ (0...count).step(2) do |i|
317
+ key = dump_ptr.get_pointer(i * FFI::Pointer.size).read_string
318
+ value = dump_ptr.get_pointer((i + 1) * FFI::Pointer.size).read_string
319
+ result[key.to_sym] = value
320
+ end
321
+
322
+ result
323
+ ensure
324
+ Rdkafka::Bindings.rd_kafka_conf_dump_free(dump_ptr, count) if dump_ptr
325
+ Rdkafka::Bindings.rd_kafka_conf_destroy(config) if config
326
+ end
327
+
294
328
  # Error that is returned by the underlying rdkafka error if an invalid configuration option is present.
295
329
  class ConfigError < RuntimeError; end
296
330
 
@@ -304,50 +338,66 @@ module Rdkafka
304
338
 
305
339
  # This method is only intended to be used to create a client,
306
340
  # using it in another way will leak memory.
341
+ #
342
+ # @param opaque [Object, nil] optional opaque pointer for callbacks
343
+ # @return [FFI::Pointer] native rdkafka configuration pointer
307
344
  def native_config(opaque = nil)
308
- Rdkafka::Bindings.rd_kafka_conf_new.tap do |config|
309
- # Create config
310
- @config_hash.merge(REQUIRED_CONFIG).each do |key, value|
311
- error_buffer = FFI::MemoryPointer.from_string(" " * 256)
312
- result = Rdkafka::Bindings.rd_kafka_conf_set(
313
- config,
314
- key.to_s,
315
- value.to_s,
316
- error_buffer,
317
- 256
318
- )
319
- unless result == :config_ok
320
- raise ConfigError.new(error_buffer.read_string)
321
- end
345
+ config = Rdkafka::Bindings.rd_kafka_conf_new
346
+
347
+ # Create config
348
+ @config_hash.merge(REQUIRED_CONFIG).each do |key, value|
349
+ error_buffer = FFI::MemoryPointer.from_string(" " * 256)
350
+ result = Rdkafka::Bindings.rd_kafka_conf_set(
351
+ config,
352
+ key.to_s,
353
+ value.to_s,
354
+ error_buffer,
355
+ 256
356
+ )
357
+ unless result == :config_ok
358
+ raise ConfigError.new(error_buffer.read_string)
322
359
  end
360
+ end
323
361
 
324
- # Set opaque pointer that's used as a proxy for callbacks
325
- if opaque
326
- pointer = ::FFI::Pointer.new(:pointer, opaque.object_id)
327
- Rdkafka::Bindings.rd_kafka_conf_set_opaque(config, pointer)
362
+ # Set opaque pointer that's used as a proxy for callbacks
363
+ if opaque
364
+ pointer = ::FFI::Pointer.new(:pointer, opaque.object_id)
365
+ Rdkafka::Bindings.rd_kafka_conf_set_opaque(config, pointer)
328
366
 
329
- # Store opaque with the pointer as key. We use this approach instead
330
- # of trying to convert the pointer to a Ruby object because there is
331
- # no risk of a segfault this way.
332
- Rdkafka::Config.opaques[pointer.to_i] = opaque
333
- end
367
+ # Store opaque with the pointer as key. We use this approach instead
368
+ # of trying to convert the pointer to a Ruby object because there is
369
+ # no risk of a segfault this way.
370
+ Rdkafka::Config.opaques[pointer.to_i] = opaque
371
+ end
334
372
 
335
- # Set log callback
336
- Rdkafka::Bindings.rd_kafka_conf_set_log_cb(config, Rdkafka::Bindings::LogCallback)
373
+ # Set log callback
374
+ Rdkafka::Bindings.rd_kafka_conf_set_log_cb(config, Rdkafka::Bindings::LogCallback)
337
375
 
338
- # Set stats callback
339
- Rdkafka::Bindings.rd_kafka_conf_set_stats_cb(config, Rdkafka::Bindings::StatsCallback)
376
+ # Set stats callback
377
+ Rdkafka::Bindings.rd_kafka_conf_set_stats_cb(config, Rdkafka::Bindings::StatsCallback)
340
378
 
341
- # Set error callback
342
- Rdkafka::Bindings.rd_kafka_conf_set_error_cb(config, Rdkafka::Bindings::ErrorCallback)
379
+ # Set error callback
380
+ Rdkafka::Bindings.rd_kafka_conf_set_error_cb(config, Rdkafka::Bindings::ErrorCallback)
343
381
 
344
- # Set oauth callback
345
- if Rdkafka::Config.oauthbearer_token_refresh_callback
346
- Rdkafka::Bindings.rd_kafka_conf_set_oauthbearer_token_refresh_cb(config, Rdkafka::Bindings::OAuthbearerTokenRefreshCallback)
347
- end
382
+ # Set oauth callback
383
+ if Rdkafka::Config.oauthbearer_token_refresh_callback
384
+ Rdkafka::Bindings.rd_kafka_conf_set_oauthbearer_token_refresh_cb(config, Rdkafka::Bindings::OAuthbearerTokenRefreshCallback)
348
385
  end
386
+
387
+ config
388
+ rescue Exception
389
+ # rd_kafka_conf_new allocates a conf the caller owns until it is handed to rd_kafka_new. If
390
+ # we raise before returning it (e.g. an invalid option in the loop above), destroy it here
391
+ # so the native config is not leaked.
392
+ Rdkafka::Bindings.rd_kafka_conf_destroy(config) if config
393
+ raise
349
394
  end
350
395
 
396
+ # Creates a native Kafka handle
397
+ # @param config [FFI::Pointer] pointer to the native config
398
+ # @param type [Symbol] type of client (:rd_kafka_producer or :rd_kafka_consumer)
399
+ # @return [FFI::Pointer] pointer to the native Kafka handle
400
+ # @private
351
401
  def native_kafka(config, type)
352
402
  error_buffer = FFI::MemoryPointer.from_string(" " * 256)
353
403
  handle = Rdkafka::Bindings.rd_kafka_new(
@@ -358,14 +408,17 @@ module Rdkafka
358
408
  )
359
409
 
360
410
  if handle.null?
411
+ # On success rd_kafka_new takes ownership of the conf and frees it itself; on failure
412
+ # librdkafka keeps application ownership (see rdkafka.c), so we must destroy it here to
413
+ # avoid leaking the conf on every failed client creation.
414
+ Rdkafka::Bindings.rd_kafka_conf_destroy(config)
361
415
  raise ClientCreationError.new(error_buffer.read_string)
362
416
  end
363
417
 
364
418
  # Redirect log to handle's queue
365
- Rdkafka::Bindings.rd_kafka_set_log_queue(
366
- handle,
367
- Rdkafka::Bindings.rd_kafka_queue_get_main(handle)
368
- )
419
+ main_queue = Rdkafka::Bindings.rd_kafka_queue_get_main(handle)
420
+ Rdkafka::Bindings.rd_kafka_set_log_queue(handle, main_queue)
421
+ Rdkafka::Bindings.rd_kafka_queue_destroy(main_queue)
369
422
 
370
423
  # Return handle which should be closed using rd_kafka_destroy after usage.
371
424
  handle
@@ -377,10 +430,15 @@ module Rdkafka
377
430
  attr_accessor :producer
378
431
  attr_accessor :consumer_rebalance_listener
379
432
 
433
+ # Invokes the delivery callback on the producer if one is set
434
+ # @param delivery_report [Rdkafka::Producer::DeliveryReport] the delivery report
435
+ # @param delivery_handle [Rdkafka::Producer::DeliveryHandle] the delivery handle
380
436
  def call_delivery_callback(delivery_report, delivery_handle)
381
- producer.call_delivery_callback(delivery_report, delivery_handle) if producer
437
+ producer&.call_delivery_callback(delivery_report, delivery_handle)
382
438
  end
383
439
 
440
+ # Invokes the on_partitions_assigned callback on the rebalance listener if set
441
+ # @param list [Rdkafka::Consumer::TopicPartitionList] the assigned partitions
384
442
  def call_on_partitions_assigned(list)
385
443
  return unless consumer_rebalance_listener
386
444
  return unless consumer_rebalance_listener.respond_to?(:on_partitions_assigned)
@@ -388,6 +446,8 @@ module Rdkafka
388
446
  consumer_rebalance_listener.on_partitions_assigned(list)
389
447
  end
390
448
 
449
+ # Invokes the on_partitions_revoked callback on the rebalance listener if set
450
+ # @param list [Rdkafka::Consumer::TopicPartitionList] the revoked partitions
391
451
  def call_on_partitions_revoked(list)
392
452
  return unless consumer_rebalance_listener
393
453
  return unless consumer_rebalance_listener.respond_to?(:on_partitions_revoked)
@@ -4,19 +4,38 @@ module Rdkafka
4
4
  class Consumer
5
5
  # Interface to return headers for a consumer message
6
6
  module Headers
7
+ # Empty frozen hash used when there are no headers
7
8
  EMPTY_HEADERS = {}.freeze
8
9
 
10
+ # Key under which reusable scratch pointers are stored on the current thread/fiber
11
+ SCRATCH_KEY = :rdkafka_headers_scratch
12
+
13
+ private_constant :SCRATCH_KEY
14
+
9
15
  # Reads a librdkafka native message's headers and returns them as a Ruby Hash
10
16
  # where each key maps to either a String (single value) or Array<String> (multiple values)
11
17
  # to support duplicate headers per KIP-82
12
18
  #
13
19
  # @private
14
20
  #
15
- # @param [Rdkafka::Bindings::Message] native_message
16
- # @return [Hash<String, String|Array<String>>] headers Hash for the native_message
21
+ # @param native_message [Rdkafka::Bindings::Message] the native message to read headers from
22
+ # @return [Hash{String => String, Array<String>}] headers Hash for the native_message
17
23
  # @raise [Rdkafka::RdkafkaError] when fail to read headers
18
24
  def self.from_native(native_message)
19
- headers_ptrptr = FFI::MemoryPointer.new(:pointer)
25
+ # Scratch output pointers for the header FFI calls, reused across messages. They are
26
+ # stored in fiber-local storage (`Thread.current[]` is fiber-local by design), so each
27
+ # thread and each fiber gets its own set and reuse is safe with fiber schedulers. They
28
+ # never escape this method and all data is read out of them before returning.
29
+ # Allocating them per message would cost several native allocations for every consumed
30
+ # message.
31
+ headers_ptrptr, name_ptrptr, value_ptrptr, size_ptr =
32
+ Thread.current[SCRATCH_KEY] ||= [
33
+ FFI::MemoryPointer.new(:pointer),
34
+ FFI::MemoryPointer.new(:pointer),
35
+ FFI::MemoryPointer.new(:pointer),
36
+ Rdkafka::Bindings::SizePtr.new
37
+ ].freeze
38
+
20
39
  err = Rdkafka::Bindings.rd_kafka_message_headers(native_message, headers_ptrptr)
21
40
 
22
41
  if err == Rdkafka::Bindings::RD_KAFKA_RESP_ERR__NOENT
@@ -27,10 +46,6 @@ module Rdkafka
27
46
 
28
47
  headers_ptr = headers_ptrptr.read_pointer
29
48
 
30
- name_ptrptr = FFI::MemoryPointer.new(:pointer)
31
- value_ptrptr = FFI::MemoryPointer.new(:pointer)
32
- size_ptr = Rdkafka::Bindings::SizePtr.new
33
-
34
49
  headers = {}
35
50
 
36
51
  idx = 0
@@ -28,10 +28,11 @@ module Rdkafka
28
28
  # @return [Time, nil]
29
29
  attr_reader :timestamp
30
30
 
31
- # @return [Hash<String, String>] a message headers
31
+ # @return [Hash{String => String}] message headers
32
32
  attr_reader :headers
33
33
 
34
34
  # @private
35
+ # @param native_message [Rdkafka::Bindings::Message] native message struct from librdkafka
35
36
  def initialize(native_message)
36
37
  # Set topic
37
38
  unless native_message[:rkt].null?
@@ -52,13 +53,11 @@ module Rdkafka
52
53
  # Set timestamp
53
54
  raw_timestamp = Rdkafka::Bindings.rd_kafka_message_timestamp(native_message, nil)
54
55
  @timestamp = if raw_timestamp && raw_timestamp > -1
55
- # Calculate seconds and microseconds
56
- seconds = raw_timestamp / 1000
57
- milliseconds = (raw_timestamp - seconds * 1000) * 1000
58
- Time.at(seconds, milliseconds)
59
- else
60
- nil
61
- end
56
+ # Calculate seconds and microseconds
57
+ seconds = raw_timestamp / 1000
58
+ milliseconds = (raw_timestamp - seconds * 1000) * 1000
59
+ Time.at(seconds, milliseconds)
60
+ end
62
61
 
63
62
  @headers = Headers.from_native(native_message)
64
63
  end
@@ -71,6 +70,11 @@ module Rdkafka
71
70
  "<Message in '#{topic}' with key '#{truncate(key)}', payload '#{truncate(payload)}', partition #{partition}, offset #{offset}, timestamp #{timestamp}#{is_headers}>"
72
71
  end
73
72
 
73
+ private
74
+
75
+ # Truncates a string for display purposes
76
+ # @param string [String, nil] the string to truncate
77
+ # @return [String, nil] truncated string or nil
74
78
  def truncate(string)
75
79
  if string && string.length > 40
76
80
  "#{string[0..39]}..."
@@ -78,9 +82,6 @@ module Rdkafka
78
82
  string
79
83
  end
80
84
  end
81
-
82
- private
83
-
84
85
  end
85
86
  end
86
87
  end
@@ -16,11 +16,20 @@ module Rdkafka
16
16
  # @return [Integer]
17
17
  attr_reader :err
18
18
 
19
+ # Partition metadata in the context of a consumer
20
+ # @return [String, nil]
21
+ attr_reader :metadata
22
+
19
23
  # @private
20
- def initialize(partition, offset, err = 0)
24
+ # @param partition [Integer] partition number
25
+ # @param offset [Integer, nil] partition offset
26
+ # @param err [Integer] error code from librdkafka
27
+ # @param metadata [String, nil] partition metadata
28
+ def initialize(partition, offset, err = Rdkafka::Bindings::RD_KAFKA_RESP_ERR_NO_ERROR, metadata = nil)
21
29
  @partition = partition
22
30
  @offset = offset
23
31
  @err = err
32
+ @metadata = metadata
24
33
  end
25
34
 
26
35
  # Human readable representation of this partition.
@@ -28,7 +37,8 @@ module Rdkafka
28
37
  def to_s
29
38
  message = "<Partition #{partition}"
30
39
  message += " offset=#{offset}" if offset
31
- message += " err=#{err}" if err != 0
40
+ message += " err=#{err}" if err != Rdkafka::Bindings::RD_KAFKA_RESP_ERR_NO_ERROR
41
+ message += " metadata=#{metadata}" if !metadata.nil?
32
42
  message += ">"
33
43
  message
34
44
  end
@@ -40,11 +50,12 @@ module Rdkafka
40
50
  end
41
51
 
42
52
  # Whether another partition is equal to this
53
+ # @param other [Object] object to compare with
43
54
  # @return [Boolean]
44
55
  def ==(other)
45
56
  self.class == other.class &&
46
- self.partition == other.partition &&
47
- self.offset == other.offset
57
+ partition == other.partition &&
58
+ offset == other.offset
48
59
  end
49
60
  end
50
61
  end
@@ -9,7 +9,7 @@ module Rdkafka
9
9
  # @param data [Hash{String => nil,Partition}] The topic and partition data or nil to create an empty list
10
10
  #
11
11
  # @return [TopicPartitionList]
12
- def initialize(data=nil)
12
+ def initialize(data = nil)
13
13
  @data = data || {}
14
14
  end
15
15
 
@@ -18,10 +18,10 @@ module Rdkafka
18
18
  def count
19
19
  i = 0
20
20
  @data.each do |_topic, partitions|
21
- if partitions
22
- i += partitions.count
21
+ i += if partitions
22
+ partitions.count
23
23
  else
24
- i+= 1
24
+ 1
25
25
  end
26
26
  end
27
27
  i
@@ -49,15 +49,14 @@ module Rdkafka
49
49
  #
50
50
  # @example Add a topic with all topics up to a count
51
51
  # tpl.add_topic("topic", 9)
52
- #
53
- def add_topic(topic, partitions=nil)
52
+ def add_topic(topic, partitions = nil)
54
53
  if partitions.nil?
55
54
  @data[topic.to_s] = nil
56
55
  else
57
56
  if partitions.is_a? Integer
58
57
  partitions = (0..partitions - 1)
59
58
  end
60
- @data[topic.to_s] = partitions.map { |p| Partition.new(p, nil, 0) }
59
+ @data[topic.to_s] = partitions.map { |p| Partition.new(p, nil, Rdkafka::Bindings::RD_KAFKA_RESP_ERR_NO_ERROR) }
61
60
  end
62
61
  end
63
62
 
@@ -65,11 +64,13 @@ module Rdkafka
65
64
  # Calling this method multiple times for the same topic will overwrite the previous configuraton.
66
65
  #
67
66
  # @param topic [String] The topic's name
68
- # @param partitions_with_offsets [Hash<Integer, Integer>] The topic's partitions and offsets
69
- #
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)
70
69
  # @return [nil]
71
70
  def add_topic_and_partitions_with_offsets(topic, partitions_with_offsets)
72
- @data[topic.to_s] = partitions_with_offsets.map { |p, o| Partition.new(p, o) }
71
+ @data[topic.to_s] = partitions_with_offsets.map do |p, o|
72
+ p.is_a?(Partition) ? p : Partition.new(p, o)
73
+ end
73
74
  end
74
75
 
75
76
  # Return a `Hash` with the topics as keys and and an array of partition information as the value if present.
@@ -85,8 +86,11 @@ module Rdkafka
85
86
  "<TopicPartitionList: #{to_h}>"
86
87
  end
87
88
 
89
+ # Check equality with another TopicPartitionList
90
+ # @param other [TopicPartitionList] object to compare with
91
+ # @return [Boolean]
88
92
  def ==(other)
89
- self.to_h == other.to_h
93
+ to_h == other.to_h
90
94
  end
91
95
 
92
96
  # Create a new topic partition list based of a native one.
@@ -105,16 +109,21 @@ module Rdkafka
105
109
  native_tpl[:cnt].times do |i|
106
110
  ptr = native_tpl[:elems] + (i * Rdkafka::Bindings::TopicPartition.size)
107
111
  elem = Rdkafka::Bindings::TopicPartition.new(ptr)
108
- if elem[:partition] == -1
112
+ if elem[:partition] == Rdkafka::Bindings::RD_KAFKA_PARTITION_UA
109
113
  data[elem[:topic]] = nil
110
114
  else
111
115
  partitions = data[elem[:topic]] || []
112
116
  offset = if elem[:offset] == Rdkafka::Bindings::RD_KAFKA_OFFSET_INVALID
113
- nil
114
- else
115
- elem[:offset]
116
- end
117
- partition = Partition.new(elem[:partition], offset, elem[:err])
117
+ nil
118
+ else
119
+ elem[:offset]
120
+ end
121
+ partition = Partition.new(
122
+ elem[:partition],
123
+ offset,
124
+ elem[:err],
125
+ elem[:metadata].null? ? nil : elem[:metadata].read_string(elem[:metadata_size])
126
+ )
118
127
  partitions.push(partition)
119
128
  data[elem[:topic]] = partitions
120
129
  end
@@ -133,33 +142,50 @@ module Rdkafka
133
142
  def to_native_tpl
134
143
  tpl = Rdkafka::Bindings.rd_kafka_topic_partition_list_new(count)
135
144
 
136
- @data.each do |topic, partitions|
137
- if partitions
138
- partitions.each do |p|
139
- Rdkafka::Bindings.rd_kafka_topic_partition_list_add(
140
- tpl,
141
- topic,
142
- p.partition
143
- )
144
-
145
- if p.offset
146
- offset = p.offset.is_a?(Time) ? p.offset.to_f * 1_000 : p.offset
147
-
148
- Rdkafka::Bindings.rd_kafka_topic_partition_list_set_offset(
145
+ begin
146
+ @data.each do |topic, partitions|
147
+ if partitions
148
+ partitions.each do |p|
149
+ ref = Rdkafka::Bindings.rd_kafka_topic_partition_list_add(
149
150
  tpl,
150
151
  topic,
151
- p.partition,
152
- offset
152
+ p.partition
153
153
  )
154
+
155
+ if p.metadata
156
+ part = Rdkafka::Bindings::TopicPartition.new(ref)
157
+ str_ptr = FFI::MemoryPointer.from_string(p.metadata)
158
+ # The metadata string is owned by librdkafka once handed over and released here:
159
+ # https://github.com/confluentinc/librdkafka/blob/e03d3bb91ed92a38f38d9806b8d8deffe78a1de5/src/rdkafka_partition.c#L2682C18-L2682C18
160
+ str_ptr.autorelease = false
161
+ part[:metadata] = str_ptr
162
+ part[:metadata_size] = p.metadata.bytesize
163
+ end
164
+
165
+ if p.offset
166
+ offset = p.offset.is_a?(Time) ? p.offset.to_f * 1_000 : p.offset
167
+
168
+ Rdkafka::Bindings.rd_kafka_topic_partition_list_set_offset(
169
+ tpl,
170
+ topic,
171
+ p.partition,
172
+ offset
173
+ )
174
+ end
154
175
  end
176
+ else
177
+ Rdkafka::Bindings.rd_kafka_topic_partition_list_add(
178
+ tpl,
179
+ topic,
180
+ Rdkafka::Bindings::RD_KAFKA_PARTITION_UA
181
+ )
155
182
  end
156
- else
157
- Rdkafka::Bindings.rd_kafka_topic_partition_list_add(
158
- tpl,
159
- topic,
160
- -1
161
- )
162
183
  end
184
+ rescue Exception
185
+ # The native list is caller-owned until it is handed to librdkafka. If population raises
186
+ # partway (e.g. an invalid partition), destroy it here so it is not leaked.
187
+ Rdkafka::Bindings.rd_kafka_topic_partition_list_destroy(tpl)
188
+ raise
163
189
  end
164
190
 
165
191
  tpl