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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +101 -3
- data/README.md +21 -14
- data/Rakefile +21 -21
- data/bin/verify_kafka_warnings +39 -0
- data/dist/{librdkafka-2.8.0.tar.gz → librdkafka-2.14.2.tar.gz} +0 -0
- data/docker-compose-ssl.yml +35 -0
- data/docker-compose.yml +2 -2
- data/ext/Rakefile +27 -27
- data/lib/rdkafka/abstract_handle.rb +54 -7
- data/lib/rdkafka/admin/acl_binding_result.rb +5 -5
- data/lib/rdkafka/admin/config_binding_result.rb +2 -2
- data/lib/rdkafka/admin/config_resource_binding_result.rb +1 -0
- data/lib/rdkafka/admin/create_acl_handle.rb +10 -6
- data/lib/rdkafka/admin/create_acl_report.rb +3 -2
- data/lib/rdkafka/admin/create_partitions_handle.rb +8 -7
- data/lib/rdkafka/admin/create_partitions_report.rb +1 -0
- data/lib/rdkafka/admin/create_topic_handle.rb +8 -7
- data/lib/rdkafka/admin/create_topic_report.rb +3 -0
- data/lib/rdkafka/admin/delete_acl_handle.rb +9 -8
- data/lib/rdkafka/admin/delete_acl_report.rb +5 -3
- data/lib/rdkafka/admin/delete_groups_handle.rb +10 -7
- data/lib/rdkafka/admin/delete_groups_report.rb +3 -0
- data/lib/rdkafka/admin/delete_topic_handle.rb +8 -7
- data/lib/rdkafka/admin/delete_topic_report.rb +3 -0
- data/lib/rdkafka/admin/describe_acl_handle.rb +9 -8
- data/lib/rdkafka/admin/describe_acl_report.rb +5 -3
- data/lib/rdkafka/admin/describe_configs_handle.rb +7 -10
- data/lib/rdkafka/admin/describe_configs_report.rb +8 -6
- data/lib/rdkafka/admin/incremental_alter_configs_handle.rb +7 -10
- data/lib/rdkafka/admin/incremental_alter_configs_report.rb +8 -6
- data/lib/rdkafka/admin/list_offsets_handle.rb +31 -0
- data/lib/rdkafka/admin/list_offsets_report.rb +51 -0
- data/lib/rdkafka/admin.rb +357 -159
- data/lib/rdkafka/bindings.rb +205 -111
- data/lib/rdkafka/callbacks/base_handler.rb +62 -0
- data/lib/rdkafka/callbacks/create_acl_handler.rb +37 -0
- data/lib/rdkafka/callbacks/create_partitions_handler.rb +37 -0
- data/lib/rdkafka/callbacks/create_topic_handler.rb +37 -0
- data/lib/rdkafka/callbacks/delete_acl_handler.rb +42 -0
- data/lib/rdkafka/callbacks/delete_groups_handler.rb +37 -0
- data/lib/rdkafka/callbacks/delete_topic_handler.rb +37 -0
- data/lib/rdkafka/callbacks/describe_acl_handler.rb +35 -0
- data/lib/rdkafka/callbacks/describe_configs_handler.rb +42 -0
- data/lib/rdkafka/callbacks/incremental_alter_configs_handler.rb +42 -0
- data/lib/rdkafka/callbacks/list_offsets_handler.rb +42 -0
- data/lib/rdkafka/callbacks.rb +125 -216
- data/lib/rdkafka/config.rb +126 -66
- data/lib/rdkafka/consumer/headers.rb +22 -7
- data/lib/rdkafka/consumer/message.rb +12 -11
- data/lib/rdkafka/consumer/partition.rb +15 -4
- data/lib/rdkafka/consumer/topic_partition_list.rb +64 -38
- data/lib/rdkafka/consumer.rb +513 -59
- data/lib/rdkafka/defaults.rb +125 -0
- data/lib/rdkafka/error.rb +40 -14
- data/lib/rdkafka/helpers/metadata.rb +29 -0
- data/lib/rdkafka/helpers/oauth.rb +45 -13
- data/lib/rdkafka/helpers/time.rb +5 -0
- data/lib/rdkafka/metadata.rb +128 -37
- data/lib/rdkafka/native_kafka.rb +91 -8
- data/lib/rdkafka/producer/delivery_handle.rb +6 -6
- data/lib/rdkafka/producer/delivery_report.rb +10 -6
- data/lib/rdkafka/producer/partitions_count_cache.rb +51 -55
- data/lib/rdkafka/producer.rb +193 -95
- data/lib/rdkafka/version.rb +6 -3
- data/lib/rdkafka.rb +15 -0
- data/package-lock.json +331 -0
- data/package.json +9 -0
- data/rdkafka.gemspec +58 -36
- data/renovate.json +39 -23
- metadata +39 -125
- data/.github/CODEOWNERS +0 -3
- data/.github/FUNDING.yml +0 -1
- data/.github/workflows/ci_linux_x86_64_gnu.yml +0 -271
- data/.github/workflows/ci_linux_x86_64_musl.yml +0 -194
- data/.github/workflows/ci_macos_arm64.yml +0 -284
- data/.github/workflows/push_linux_x86_64_gnu.yml +0 -65
- data/.github/workflows/push_linux_x86_64_musl.yml +0 -79
- data/.github/workflows/push_macos_arm64.yml +0 -54
- data/.github/workflows/push_ruby.yml +0 -37
- data/.github/workflows/verify-action-pins.yml +0 -16
- data/.gitignore +0 -14
- data/.rspec +0 -2
- data/.ruby-gemset +0 -1
- data/.ruby-version +0 -1
- data/.yardopts +0 -2
- data/Gemfile +0 -5
- data/ext/README.md +0 -19
- data/ext/build_common.sh +0 -361
- data/ext/build_linux_x86_64_gnu.sh +0 -306
- data/ext/build_linux_x86_64_musl.sh +0 -763
- data/ext/build_macos_arm64.sh +0 -550
- data/spec/rdkafka/abstract_handle_spec.rb +0 -117
- data/spec/rdkafka/admin/create_acl_handle_spec.rb +0 -56
- data/spec/rdkafka/admin/create_acl_report_spec.rb +0 -18
- data/spec/rdkafka/admin/create_topic_handle_spec.rb +0 -52
- data/spec/rdkafka/admin/create_topic_report_spec.rb +0 -16
- data/spec/rdkafka/admin/delete_acl_handle_spec.rb +0 -85
- data/spec/rdkafka/admin/delete_acl_report_spec.rb +0 -72
- data/spec/rdkafka/admin/delete_topic_handle_spec.rb +0 -52
- data/spec/rdkafka/admin/delete_topic_report_spec.rb +0 -16
- data/spec/rdkafka/admin/describe_acl_handle_spec.rb +0 -85
- data/spec/rdkafka/admin/describe_acl_report_spec.rb +0 -73
- data/spec/rdkafka/admin_spec.rb +0 -971
- data/spec/rdkafka/bindings_spec.rb +0 -199
- data/spec/rdkafka/callbacks_spec.rb +0 -20
- data/spec/rdkafka/config_spec.rb +0 -258
- data/spec/rdkafka/consumer/headers_spec.rb +0 -73
- data/spec/rdkafka/consumer/message_spec.rb +0 -139
- data/spec/rdkafka/consumer/partition_spec.rb +0 -57
- data/spec/rdkafka/consumer/topic_partition_list_spec.rb +0 -248
- data/spec/rdkafka/consumer_spec.rb +0 -1274
- data/spec/rdkafka/error_spec.rb +0 -89
- data/spec/rdkafka/metadata_spec.rb +0 -79
- data/spec/rdkafka/native_kafka_spec.rb +0 -130
- data/spec/rdkafka/producer/delivery_handle_spec.rb +0 -45
- data/spec/rdkafka/producer/delivery_report_spec.rb +0 -25
- data/spec/rdkafka/producer/partitions_count_cache_spec.rb +0 -359
- data/spec/rdkafka/producer_spec.rb +0 -1345
- data/spec/spec_helper.rb +0 -195
data/lib/rdkafka/config.rb
CHANGED
|
@@ -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(
|
|
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
|
|
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]
|
|
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
|
|
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]
|
|
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]
|
|
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
|
|
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
|
-
|
|
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
|
-
# @
|
|
178
|
-
|
|
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
|
-
# @
|
|
194
|
-
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
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
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
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
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
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
|
-
|
|
336
|
-
|
|
373
|
+
# Set log callback
|
|
374
|
+
Rdkafka::Bindings.rd_kafka_conf_set_log_cb(config, Rdkafka::Bindings::LogCallback)
|
|
337
375
|
|
|
338
|
-
|
|
339
|
-
|
|
376
|
+
# Set stats callback
|
|
377
|
+
Rdkafka::Bindings.rd_kafka_conf_set_stats_cb(config, Rdkafka::Bindings::StatsCallback)
|
|
340
378
|
|
|
341
|
-
|
|
342
|
-
|
|
379
|
+
# Set error callback
|
|
380
|
+
Rdkafka::Bindings.rd_kafka_conf_set_error_cb(config, Rdkafka::Bindings::ErrorCallback)
|
|
343
381
|
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
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.
|
|
366
|
-
|
|
367
|
-
|
|
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
|
|
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]
|
|
16
|
-
# @return [Hash
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
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
|
-
|
|
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 !=
|
|
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
|
-
|
|
47
|
-
|
|
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
|
-
|
|
21
|
+
i += if partitions
|
|
22
|
+
partitions.count
|
|
23
23
|
else
|
|
24
|
-
|
|
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,
|
|
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
|
|
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
|
|
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
|
-
|
|
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] ==
|
|
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
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
partition = Partition.new(
|
|
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
|
-
|
|
137
|
-
|
|
138
|
-
partitions
|
|
139
|
-
|
|
140
|
-
|
|
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
|