rdkafka 0.28.0-aarch64-linux-gnu → 0.29.1-aarch64-linux-gnu
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/CHANGELOG.md +42 -0
- data/README.md +3 -2
- data/bin/verify_kafka_warnings +5 -0
- data/docker-compose-ssl.yml +4 -1
- data/docker-compose.yml +4 -1
- data/ext/librdkafka.so +0 -0
- data/lib/rdkafka/abstract_handle.rb +31 -2
- data/lib/rdkafka/admin/config_binding_result.rb +2 -2
- data/lib/rdkafka/admin/create_acl_handle.rb +6 -5
- data/lib/rdkafka/admin/create_acl_report.rb +2 -1
- data/lib/rdkafka/admin/create_partitions_handle.rb +4 -6
- data/lib/rdkafka/admin/create_topic_handle.rb +4 -6
- data/lib/rdkafka/admin/delete_acl_handle.rb +5 -7
- data/lib/rdkafka/admin/delete_groups_handle.rb +4 -6
- data/lib/rdkafka/admin/delete_records_handle.rb +31 -0
- data/lib/rdkafka/admin/delete_records_report.rb +25 -0
- data/lib/rdkafka/admin/delete_topic_handle.rb +4 -6
- data/lib/rdkafka/admin/describe_acl_handle.rb +5 -7
- data/lib/rdkafka/admin/describe_acl_report.rb +2 -1
- data/lib/rdkafka/admin/describe_configs_handle.rb +4 -10
- data/lib/rdkafka/admin/describe_configs_report.rb +1 -5
- data/lib/rdkafka/admin/incremental_alter_configs_handle.rb +4 -10
- data/lib/rdkafka/admin/incremental_alter_configs_report.rb +1 -5
- data/lib/rdkafka/admin/list_consumer_groups_handle.rb +31 -0
- data/lib/rdkafka/admin/list_consumer_groups_report.rb +83 -0
- data/lib/rdkafka/admin/list_offsets_handle.rb +5 -10
- data/lib/rdkafka/admin/list_offsets_report.rb +5 -2
- data/lib/rdkafka/admin.rb +238 -145
- data/lib/rdkafka/bindings.rb +61 -16
- 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_records_handler.rb +38 -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_consumer_groups_handler.rb +39 -0
- data/lib/rdkafka/callbacks/list_offsets_handler.rb +42 -0
- data/lib/rdkafka/callbacks.rb +54 -246
- data/lib/rdkafka/config.rb +69 -50
- data/lib/rdkafka/consumer/headers.rb +19 -5
- data/lib/rdkafka/consumer/partition.rb +8 -1
- data/lib/rdkafka/consumer/topic_partition_list.rb +58 -30
- data/lib/rdkafka/consumer.rb +211 -52
- data/lib/rdkafka/defaults.rb +24 -7
- data/lib/rdkafka/helpers/list_offsets.rb +127 -0
- data/lib/rdkafka/helpers/metadata.rb +29 -0
- data/lib/rdkafka/helpers/oauth.rb +10 -5
- data/lib/rdkafka/metadata.rb +86 -19
- data/lib/rdkafka/native_kafka.rb +22 -11
- data/lib/rdkafka/producer/delivery_handle.rb +4 -5
- data/lib/rdkafka/producer/partitions_count_cache.rb +24 -38
- data/lib/rdkafka/producer.rb +81 -61
- data/lib/rdkafka/version.rb +3 -3
- data/lib/rdkafka.rb +19 -0
- data/package-lock.json +3 -3
- data/rdkafka.gemspec +1 -0
- metadata +21 -5
- data/Gemfile +0 -13
- data/Gemfile.lint +0 -14
- data/Gemfile.lint.lock +0 -123
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Rdkafka
|
|
4
|
+
module Helpers
|
|
5
|
+
# Shared `#list_offsets` implementation for Admin and Consumer.
|
|
6
|
+
#
|
|
7
|
+
# The librdkafka Admin API is handle-agnostic: `rd_kafka_ListOffsets()` can be issued on any
|
|
8
|
+
# `rd_kafka_t` instance regardless of its type, so this Ruby-level implementation is identical
|
|
9
|
+
# for admin and consumer clients. Includers must provide a private `#closed_check(method)`
|
|
10
|
+
# that raises their own `Closed*Error`, and must have the background event callback
|
|
11
|
+
# registered on their native config (see `Config#admin` and `Config#consumer`) so the result
|
|
12
|
+
# event is dispatched back to the returned handle.
|
|
13
|
+
module ListOffsets
|
|
14
|
+
# Queries partition offsets by specification (earliest, latest, max_timestamp, or by
|
|
15
|
+
# timestamp) without requiring a consumer group.
|
|
16
|
+
#
|
|
17
|
+
# The query is batched: all requested partitions are carried in one `ListOffsets` request
|
|
18
|
+
# that librdkafka fans out to the involved partition leaders internally and concurrently.
|
|
19
|
+
#
|
|
20
|
+
# @param topic_partition_offsets [Hash{String => Array<Hash>}] hash mapping topic names to
|
|
21
|
+
# arrays of partition offset specifications. Each specification is a hash with:
|
|
22
|
+
# - `:partition` [Integer] partition number
|
|
23
|
+
# - `:offset` [Symbol, Integer] offset specification - `:earliest`, `:latest`,
|
|
24
|
+
# `:max_timestamp`, or an integer timestamp in milliseconds
|
|
25
|
+
# @param isolation_level [Integer, nil] optional isolation level:
|
|
26
|
+
# - `RD_KAFKA_ISOLATION_LEVEL_READ_UNCOMMITTED` (0) - default
|
|
27
|
+
# - `RD_KAFKA_ISOLATION_LEVEL_READ_COMMITTED` (1)
|
|
28
|
+
#
|
|
29
|
+
# @return [Admin::ListOffsetsHandle] handle that can be used to wait for the result
|
|
30
|
+
#
|
|
31
|
+
# @raise [ClosedAdminError, ClosedConsumerError] when the client is closed
|
|
32
|
+
# @raise [ConfigError] when the background queue is unavailable
|
|
33
|
+
#
|
|
34
|
+
# @example Query earliest and latest offsets
|
|
35
|
+
# handle = client.list_offsets(
|
|
36
|
+
# { "my_topic" => [
|
|
37
|
+
# { partition: 0, offset: :earliest },
|
|
38
|
+
# { partition: 1, offset: :latest }
|
|
39
|
+
# ] }
|
|
40
|
+
# )
|
|
41
|
+
# report = handle.wait(max_wait_timeout_ms: 15_000)
|
|
42
|
+
# report.offsets
|
|
43
|
+
# # => [{ topic: "my_topic", partition: 0, offset: 0, ... }, ...]
|
|
44
|
+
def list_offsets(topic_partition_offsets, isolation_level: nil)
|
|
45
|
+
closed_check(__method__)
|
|
46
|
+
|
|
47
|
+
# Parse and validate every offset spec before allocating the native list, so a missing key
|
|
48
|
+
# or an unknown offset specification raises with nothing to clean up. Previously the
|
|
49
|
+
# ArgumentError (or KeyError) was raised after `rd_kafka_topic_partition_list_new`, leaking
|
|
50
|
+
# the native list.
|
|
51
|
+
parsed = topic_partition_offsets.flat_map do |topic, partitions|
|
|
52
|
+
partitions.map do |spec|
|
|
53
|
+
offset = spec.fetch(:offset)
|
|
54
|
+
|
|
55
|
+
native_offset = case offset
|
|
56
|
+
when :earliest then Rdkafka::Bindings::RD_KAFKA_OFFSET_SPEC_EARLIEST
|
|
57
|
+
when :latest then Rdkafka::Bindings::RD_KAFKA_OFFSET_SPEC_LATEST
|
|
58
|
+
when :max_timestamp then Rdkafka::Bindings::RD_KAFKA_OFFSET_SPEC_MAX_TIMESTAMP
|
|
59
|
+
when Integer then offset
|
|
60
|
+
else
|
|
61
|
+
raise ArgumentError, "Unknown offset specification: #{offset.inspect}"
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
[topic, spec.fetch(:partition), native_offset]
|
|
65
|
+
end
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
# Build native topic partition list
|
|
69
|
+
tpl = Rdkafka::Bindings.rd_kafka_topic_partition_list_new(parsed.size)
|
|
70
|
+
|
|
71
|
+
parsed.each do |topic, partition, native_offset|
|
|
72
|
+
Rdkafka::Bindings.rd_kafka_topic_partition_list_add(tpl, topic, partition)
|
|
73
|
+
Rdkafka::Bindings.rd_kafka_topic_partition_list_set_offset(tpl, topic, partition, native_offset)
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
# Get a pointer to the queue that our request will be enqueued on
|
|
77
|
+
queue_ptr = @native_kafka.with_inner do |inner|
|
|
78
|
+
Rdkafka::Bindings.rd_kafka_queue_get_background(inner)
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
if queue_ptr.null?
|
|
82
|
+
Rdkafka::Bindings.rd_kafka_topic_partition_list_destroy(tpl)
|
|
83
|
+
raise Rdkafka::Config::ConfigError.new("rd_kafka_queue_get_background was NULL")
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
# Create and register the handle we will return to the caller
|
|
87
|
+
handle = Admin::ListOffsetsHandle.new
|
|
88
|
+
handle[:pending] = true
|
|
89
|
+
handle[:response] = Rdkafka::Bindings::RD_KAFKA_PARTITION_UA
|
|
90
|
+
|
|
91
|
+
admin_options_ptr = @native_kafka.with_inner do |inner|
|
|
92
|
+
Rdkafka::Bindings.rd_kafka_AdminOptions_new(
|
|
93
|
+
inner,
|
|
94
|
+
Rdkafka::Bindings::RD_KAFKA_ADMIN_OP_LISTOFFSETS
|
|
95
|
+
)
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
if isolation_level
|
|
99
|
+
Rdkafka::Bindings.rd_kafka_AdminOptions_set_isolation_level(admin_options_ptr, isolation_level)
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
Admin::ListOffsetsHandle.register(handle)
|
|
103
|
+
Rdkafka::Bindings.rd_kafka_AdminOptions_set_opaque(admin_options_ptr, handle.to_ptr)
|
|
104
|
+
|
|
105
|
+
begin
|
|
106
|
+
@native_kafka.with_inner do |inner|
|
|
107
|
+
Rdkafka::Bindings.rd_kafka_ListOffsets(
|
|
108
|
+
inner,
|
|
109
|
+
tpl,
|
|
110
|
+
admin_options_ptr,
|
|
111
|
+
queue_ptr
|
|
112
|
+
)
|
|
113
|
+
end
|
|
114
|
+
rescue Exception
|
|
115
|
+
Admin::ListOffsetsHandle.remove(handle.to_ptr.address)
|
|
116
|
+
raise
|
|
117
|
+
ensure
|
|
118
|
+
Rdkafka::Bindings.rd_kafka_AdminOptions_destroy(admin_options_ptr)
|
|
119
|
+
Rdkafka::Bindings.rd_kafka_queue_destroy(queue_ptr)
|
|
120
|
+
Rdkafka::Bindings.rd_kafka_topic_partition_list_destroy(tpl)
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
handle
|
|
124
|
+
end
|
|
125
|
+
end
|
|
126
|
+
end
|
|
127
|
+
end
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Rdkafka
|
|
4
|
+
module Helpers
|
|
5
|
+
# Shared `#metadata` implementation for Admin, Consumer and Producer.
|
|
6
|
+
#
|
|
7
|
+
# `rd_kafka_metadata()` is handle-agnostic in librdkafka - it works on any `rd_kafka_t`
|
|
8
|
+
# (consumer, producer or admin) - so this Ruby-level implementation is identical across all
|
|
9
|
+
# three client types. Includers must provide a private `#closed_check(method)` that raises
|
|
10
|
+
# their own `Closed*Error`.
|
|
11
|
+
module Metadata
|
|
12
|
+
# Performs the metadata request using this client
|
|
13
|
+
#
|
|
14
|
+
# @param topic_name [String, nil] metadata about particular topic or all if nil
|
|
15
|
+
# @param timeout_ms [Integer] metadata request timeout
|
|
16
|
+
# @return [Rdkafka::Metadata] requested metadata
|
|
17
|
+
def metadata(topic_name = nil, timeout_ms = Defaults::METADATA_TIMEOUT_MS)
|
|
18
|
+
closed_check(__method__)
|
|
19
|
+
|
|
20
|
+
@native_kafka.with_inner do |inner|
|
|
21
|
+
# Must stay fully qualified: this module is itself named `Metadata`, so a bare
|
|
22
|
+
# `Metadata.new` here would resolve to `Rdkafka::Helpers::Metadata` (no `.new`) instead
|
|
23
|
+
# of this class.
|
|
24
|
+
Rdkafka::Metadata.new(inner, topic_name, timeout_ms)
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
end
|
|
@@ -4,10 +4,14 @@ module Rdkafka
|
|
|
4
4
|
module OAuth
|
|
5
5
|
# Set the OAuthBearer token
|
|
6
6
|
#
|
|
7
|
-
# @param token [String] the mandatory token value to set, often (but not necessarily) a JWS
|
|
8
|
-
#
|
|
7
|
+
# @param token [String] the mandatory token value to set, often (but not necessarily) a JWS
|
|
8
|
+
# compact serialization as per https://tools.ietf.org/html/rfc7515#section-3.1.
|
|
9
|
+
# @param lifetime_ms [Integer] when the token expires, in terms of the number of milliseconds
|
|
10
|
+
# since the epoch. See https://currentmillis.com/.
|
|
9
11
|
# @param principal_name [String] the mandatory Kafka principal name associated with the token.
|
|
10
|
-
# @param extensions [Hash] optional SASL extensions key-value pairs to be communicated to the
|
|
12
|
+
# @param extensions [Hash] optional SASL extensions key-value pairs to be communicated to the
|
|
13
|
+
# broker as additional key-value pairs during the initial client response as per
|
|
14
|
+
# https://tools.ietf.org/html/rfc7628#section-3.1.
|
|
11
15
|
# @return [Integer] 0 on success
|
|
12
16
|
def oauthbearer_set_token(token:, lifetime_ms:, principal_name:, extensions: nil)
|
|
13
17
|
error_buffer = FFI::MemoryPointer.from_string(" " * 256)
|
|
@@ -49,7 +53,8 @@ module Rdkafka
|
|
|
49
53
|
# Convert extensions hash to FFI::MemoryPointer (`const char **`).
|
|
50
54
|
#
|
|
51
55
|
# @param extensions [Hash, nil] extension key-value pairs
|
|
52
|
-
# @return [Array<FFI::MemoryPointer, Array<FFI::MemoryPointer>>] array pointer and string
|
|
56
|
+
# @return [Array<FFI::MemoryPointer, Array<FFI::MemoryPointer>>] array pointer and string
|
|
57
|
+
# pointers
|
|
53
58
|
# @note The returned pointers must be freed manually (autorelease = false).
|
|
54
59
|
def map_extensions(extensions)
|
|
55
60
|
return [nil, nil] if extensions.nil? || extensions.empty?
|
|
@@ -80,7 +85,7 @@ module Rdkafka
|
|
|
80
85
|
#
|
|
81
86
|
# @param extensions [Hash, nil] extension key-value pairs
|
|
82
87
|
# @return [Integer] non-negative even number representing keys + values count
|
|
83
|
-
# @see
|
|
88
|
+
# @see github.com/confluentinc/librdkafka/blob/master/src/rdkafka_sasl_oauthbearer.c#L327-L347
|
|
84
89
|
def extension_size(extensions)
|
|
85
90
|
return 0 unless extensions
|
|
86
91
|
extensions.size * 2
|
data/lib/rdkafka/metadata.rb
CHANGED
|
@@ -23,12 +23,57 @@ module Rdkafka
|
|
|
23
23
|
# @param timeout_ms [Integer] timeout in milliseconds
|
|
24
24
|
# @raise [RdkafkaError] when metadata fetch fails
|
|
25
25
|
def initialize(native_client, topic_name = nil, timeout_ms = Defaults::METADATA_TIMEOUT_MS)
|
|
26
|
-
attempt
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
26
|
+
attempt = 0
|
|
27
|
+
deadline = ::Process.clock_gettime(::Process::CLOCK_MONOTONIC) +
|
|
28
|
+
Defaults::METADATA_RETRY_BUDGET_MS / 1_000.0
|
|
29
|
+
|
|
30
|
+
begin
|
|
31
|
+
attempt += 1
|
|
32
|
+
fetch_metadata(native_client, topic_name, timeout_ms)
|
|
33
|
+
rescue ::Rdkafka::RdkafkaError => e
|
|
34
|
+
raise unless RETRIED_ERRORS.include?(e.code)
|
|
35
|
+
raise if attempt > Defaults::METADATA_MAX_RETRIES
|
|
36
|
+
|
|
37
|
+
# Stop once the wall-clock retry budget is spent, but only after at least
|
|
38
|
+
# METADATA_MIN_ATTEMPTS tries so a slow broker (whose requests each consume the full
|
|
39
|
+
# timeout) still gets a few tries rather than being cut off after one or two.
|
|
40
|
+
raise if attempt >= Defaults::METADATA_MIN_ATTEMPTS &&
|
|
41
|
+
::Process.clock_gettime(::Process::CLOCK_MONOTONIC) >= deadline
|
|
42
|
+
|
|
43
|
+
# Exponential backoff between attempts, capped so a long retry sequence cannot block for
|
|
44
|
+
# minutes. The request timeout (`timeout_ms`) is intentionally left unchanged: it used to be
|
|
45
|
+
# overwritten with the backoff value, which shrank the first retries below the configured
|
|
46
|
+
# timeout (near-guaranteeing another timeout) and then inflated later ones to ~100s.
|
|
47
|
+
backoff_ms = [
|
|
48
|
+
(2**attempt) * Defaults::METADATA_RETRY_BACKOFF_BASE_MS,
|
|
49
|
+
Defaults::METADATA_RETRY_BACKOFF_MAX_MS
|
|
50
|
+
].min
|
|
51
|
+
|
|
52
|
+
sleep(backoff_ms / 1_000.0)
|
|
53
|
+
|
|
54
|
+
retry
|
|
31
55
|
end
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
private
|
|
59
|
+
|
|
60
|
+
# Performs a single metadata fetch attempt, freeing this attempt's native resources.
|
|
61
|
+
#
|
|
62
|
+
# Kept separate from {#initialize} so each retried attempt frees its own `native_topic` and
|
|
63
|
+
# metadata struct. `retry` re-enters `initialize`'s `begin` without running an `ensure` placed
|
|
64
|
+
# there, so doing the cleanup per attempt here is what prevents the per-retry native leak. The
|
|
65
|
+
# metadata struct is only allocated on success, so it is read and destroyed only after the
|
|
66
|
+
# result has been validated (avoids destroying a NULL/garbage pointer on a failed fetch).
|
|
67
|
+
#
|
|
68
|
+
# @param native_client [FFI::Pointer] pointer to the native Kafka client
|
|
69
|
+
# @param topic_name [String, nil] specific topic to fetch metadata for, or nil for all topics
|
|
70
|
+
# @param timeout_ms [Integer] timeout in milliseconds
|
|
71
|
+
# @raise [RdkafkaError] when the metadata fetch fails
|
|
72
|
+
def fetch_metadata(native_client, topic_name, timeout_ms)
|
|
73
|
+
native_topic = nil
|
|
74
|
+
metadata_ptr = nil
|
|
75
|
+
|
|
76
|
+
native_topic = Rdkafka::Bindings.rd_kafka_topic_new(native_client, topic_name, nil) if topic_name
|
|
32
77
|
|
|
33
78
|
ptr = FFI::MemoryPointer.new(:pointer)
|
|
34
79
|
|
|
@@ -42,24 +87,16 @@ module Rdkafka
|
|
|
42
87
|
# Error Handling
|
|
43
88
|
raise Rdkafka::RdkafkaError.new(result) unless result.zero?
|
|
44
89
|
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
raise if attempt > Defaults::METADATA_MAX_RETRIES
|
|
90
|
+
# rd_kafka_metadata only allocates the struct on success, so we read the pointer to destroy
|
|
91
|
+
# only after the result has been confirmed successful.
|
|
92
|
+
metadata_ptr = ptr.read_pointer
|
|
49
93
|
|
|
50
|
-
|
|
51
|
-
timeout_ms = backoff_factor * Defaults::METADATA_RETRY_BACKOFF_BASE_MS
|
|
52
|
-
|
|
53
|
-
sleep(timeout_ms / 1000.0)
|
|
54
|
-
|
|
55
|
-
retry
|
|
94
|
+
metadata_from_native(metadata_ptr)
|
|
56
95
|
ensure
|
|
57
|
-
Rdkafka::Bindings.rd_kafka_topic_destroy(native_topic) if
|
|
58
|
-
Rdkafka::Bindings.rd_kafka_metadata_destroy(
|
|
96
|
+
Rdkafka::Bindings.rd_kafka_topic_destroy(native_topic) if native_topic
|
|
97
|
+
Rdkafka::Bindings.rd_kafka_metadata_destroy(metadata_ptr) if metadata_ptr && !metadata_ptr.null?
|
|
59
98
|
end
|
|
60
99
|
|
|
61
|
-
private
|
|
62
|
-
|
|
63
100
|
# Extracts metadata from native pointer
|
|
64
101
|
# @param ptr [FFI::Pointer] pointer to native metadata
|
|
65
102
|
def metadata_from_native(ptr)
|
|
@@ -134,6 +171,36 @@ module Rdkafka
|
|
|
134
171
|
:replicas, :pointer,
|
|
135
172
|
:in_sync_replica_brokers, :int,
|
|
136
173
|
:isrs, :pointer
|
|
174
|
+
|
|
175
|
+
# The base `#to_h` skips FFI pointer members, which would drop the replica and in-sync
|
|
176
|
+
# replica assignments entirely. We dereference those pointers here so the partition hash
|
|
177
|
+
# exposes the broker ids backing the partition (needed e.g. to plan replication changes).
|
|
178
|
+
#
|
|
179
|
+
# @return [Hash{Symbol => Integer, Array<Integer>}] partition metadata:
|
|
180
|
+
# * +:partition_id+ (Integer) - partition id
|
|
181
|
+
# * +:leader+ (Integer) - broker id of the partition leader
|
|
182
|
+
# * +:replica_count+ (Integer) - number of assigned replicas
|
|
183
|
+
# * +:in_sync_replica_brokers+ (Integer) - number of in-sync replicas
|
|
184
|
+
# * +:replicas+ (Array<Integer>) - broker ids of the assigned replicas
|
|
185
|
+
# * +:isrs+ (Array<Integer>) - broker ids of the in-sync replicas
|
|
186
|
+
def to_h
|
|
187
|
+
super.merge(
|
|
188
|
+
replicas: read_broker_ids(self[:replicas], self[:replica_count]),
|
|
189
|
+
isrs: read_broker_ids(self[:isrs], self[:in_sync_replica_brokers])
|
|
190
|
+
)
|
|
191
|
+
end
|
|
192
|
+
|
|
193
|
+
private
|
|
194
|
+
|
|
195
|
+
# Reads `count` broker ids (int32) from a replicas/isrs pointer.
|
|
196
|
+
# @param pointer [FFI::Pointer] pointer to the broker ids array
|
|
197
|
+
# @param count [Integer] number of broker ids to read
|
|
198
|
+
# @return [Array<Integer>] broker ids (empty when there are none)
|
|
199
|
+
def read_broker_ids(pointer, count)
|
|
200
|
+
return [] if count.zero? || pointer.null?
|
|
201
|
+
|
|
202
|
+
pointer.read_array_of_int32(count)
|
|
203
|
+
end
|
|
137
204
|
end
|
|
138
205
|
end
|
|
139
206
|
end
|
data/lib/rdkafka/native_kafka.rb
CHANGED
|
@@ -13,6 +13,14 @@ module Rdkafka
|
|
|
13
13
|
def initialize(inner, run_polling_thread:, opaque:, auto_start: true, timeout_ms: Defaults::NATIVE_KAFKA_POLL_TIMEOUT_MS)
|
|
14
14
|
@inner = inner
|
|
15
15
|
@opaque = opaque
|
|
16
|
+
# Process that owns `@inner`. librdkafka is not fork-safe: `fork` copies only the calling
|
|
17
|
+
# thread, so the background/broker threads backing this handle do not exist in a child
|
|
18
|
+
# process. An inherited handle therefore must not be polled or destroyed in the child -
|
|
19
|
+
# `rd_kafka_destroy` would walk thread state that no longer exists (segfault) and the
|
|
20
|
+
# inherited mutexes may have been copied in a locked state (deadlock). We record the creator
|
|
21
|
+
# pid so a forked child can recognise an inherited handle and leave its teardown to the
|
|
22
|
+
# parent, which still owns the running threads.
|
|
23
|
+
@creator_pid = Process.pid
|
|
16
24
|
# Lock around external access
|
|
17
25
|
@access_mutex = Mutex.new
|
|
18
26
|
# Lock around internal polling
|
|
@@ -55,8 +63,7 @@ module Rdkafka
|
|
|
55
63
|
Rdkafka::Bindings.rd_kafka_poll(@inner, 0)
|
|
56
64
|
|
|
57
65
|
if @run_polling_thread
|
|
58
|
-
# Start thread to poll client for delivery callbacks,
|
|
59
|
-
# not used in consumer.
|
|
66
|
+
# Start thread to poll client for delivery callbacks, not used in consumer.
|
|
60
67
|
@polling_thread = Thread.new do
|
|
61
68
|
loop do
|
|
62
69
|
@poll_mutex.synchronize do
|
|
@@ -116,17 +123,21 @@ module Rdkafka
|
|
|
116
123
|
end
|
|
117
124
|
|
|
118
125
|
# Returns whether this native Kafka handle is closed or closing
|
|
126
|
+
#
|
|
127
|
+
# A handle inherited across `fork` is reported as closed in the child: it belongs to another
|
|
128
|
+
# process whose threads back the native client, so it is not usable here and must not be
|
|
129
|
+
# destroyed here (see the `@creator_pid` note in `#initialize`). This makes every `#close`
|
|
130
|
+
# path - including the GC finalizers that run during a child's exit - skip the native teardown
|
|
131
|
+
# for inherited handles, which is what would otherwise segfault the child.
|
|
132
|
+
#
|
|
119
133
|
# @return [Boolean] true if closed or closing
|
|
120
134
|
def closed?
|
|
121
|
-
@closing || @inner.nil?
|
|
135
|
+
@closing || @inner.nil? || @creator_pid != Process.pid
|
|
122
136
|
end
|
|
123
137
|
|
|
124
138
|
# Enable IO event notifications on the main queue
|
|
125
139
|
# Librdkafka will write to your FD when the queue transitions from empty to non-empty
|
|
126
140
|
#
|
|
127
|
-
# @note This method is incompatible with background polling threads.
|
|
128
|
-
# If background polling is enabled, use manual polling instead (e.g., consumer.poll)
|
|
129
|
-
#
|
|
130
141
|
# @param fd [Integer] your file descriptor (from IO.pipe or eventfd)
|
|
131
142
|
# @param payload [String] data to write to fd when queue has data (default: "\x01")
|
|
132
143
|
# @return [nil]
|
|
@@ -143,6 +154,8 @@ module Rdkafka
|
|
|
143
154
|
# if readable
|
|
144
155
|
# consumer.poll(0) # Get messages
|
|
145
156
|
# end
|
|
157
|
+
# @note This method is incompatible with background polling threads.
|
|
158
|
+
# If background polling is enabled, use manual polling instead (e.g., consumer.poll)
|
|
146
159
|
def enable_main_queue_io_events(fd, payload = "\x01")
|
|
147
160
|
if @run_polling_thread
|
|
148
161
|
raise "Cannot enable IO events while background polling thread is active. " \
|
|
@@ -160,14 +173,13 @@ module Rdkafka
|
|
|
160
173
|
# Enable IO event notifications on the background queue
|
|
161
174
|
# Librdkafka will write to your FD when the background queue transitions from empty to non-empty
|
|
162
175
|
#
|
|
163
|
-
# @note This method is incompatible with background polling threads.
|
|
164
|
-
# If background polling is enabled, use manual polling instead (e.g., consumer.poll)
|
|
165
|
-
#
|
|
166
176
|
# @param fd [Integer] your file descriptor (from IO.pipe or eventfd)
|
|
167
177
|
# @param payload [String] data to write to fd when queue has data (default: "\x01")
|
|
168
178
|
# @return [nil]
|
|
169
179
|
# @raise [ClosedInnerError] when the handle is closed
|
|
170
180
|
# @raise [RuntimeError] when background polling thread is active
|
|
181
|
+
# @note This method is incompatible with background polling threads.
|
|
182
|
+
# If background polling is enabled, use manual polling instead (e.g., consumer.poll)
|
|
171
183
|
def enable_background_queue_io_events(fd, payload = "\x01")
|
|
172
184
|
if @run_polling_thread
|
|
173
185
|
raise "Cannot enable IO events while background polling thread is active. " \
|
|
@@ -197,8 +209,7 @@ module Rdkafka
|
|
|
197
209
|
# Indicate to polling thread that we're closing
|
|
198
210
|
@polling_thread[:closing] = true
|
|
199
211
|
|
|
200
|
-
# Wait for the polling thread to finish up,
|
|
201
|
-
# this can be aborted in practice if this
|
|
212
|
+
# Wait for the polling thread to finish up, this can be aborted in practice if this
|
|
202
213
|
# code runs from a finalizer.
|
|
203
214
|
@polling_thread.join
|
|
204
215
|
end
|
|
@@ -2,20 +2,19 @@
|
|
|
2
2
|
|
|
3
3
|
module Rdkafka
|
|
4
4
|
class Producer
|
|
5
|
-
# Handle to wait for a delivery report which is returned when
|
|
6
|
-
# producing a message.
|
|
5
|
+
# Handle to wait for a delivery report which is returned when producing a message.
|
|
7
6
|
class DeliveryHandle < Rdkafka::AbstractHandle
|
|
8
7
|
layout :pending, :bool,
|
|
9
8
|
:response, :int,
|
|
10
9
|
:partition, :int,
|
|
11
|
-
:offset, :int64
|
|
12
|
-
:topic_name, :pointer
|
|
10
|
+
:offset, :int64
|
|
13
11
|
|
|
14
12
|
# @return [Object, nil] label set during message production or nil by default
|
|
15
13
|
attr_accessor :label
|
|
16
14
|
|
|
17
15
|
# @return [String] topic where we are trying to send the message
|
|
18
|
-
#
|
|
16
|
+
# Set in `#produce`, where the topic is known upfront. Keeping it as a Ruby attribute
|
|
17
|
+
# spares a per-message native string copy in the delivery callback.
|
|
19
18
|
attr_accessor :topic
|
|
20
19
|
|
|
21
20
|
# @return [String] the name of the operation (e.g. "delivery")
|
|
@@ -37,12 +37,11 @@ module Rdkafka
|
|
|
37
37
|
# contention in multi-threaded environments while ensuring data consistency.
|
|
38
38
|
#
|
|
39
39
|
# 6. Topic recreation handling
|
|
40
|
-
# If a topic is deleted and recreated with fewer partitions, the cache
|
|
41
|
-
#
|
|
42
|
-
#
|
|
43
|
-
#
|
|
44
|
-
#
|
|
45
|
-
# This also aligns with the previous cache implementation.
|
|
40
|
+
# If a topic is deleted and recreated with fewer partitions, the cache keeps reporting the
|
|
41
|
+
# higher count only until the entry's TTL expires. The first refresh after expiry performs
|
|
42
|
+
# an authoritative metadata read and adopts the lower count. Within the TTL window a lower
|
|
43
|
+
# value is still ignored, so a transient or racy lower read cannot clobber a correct higher
|
|
44
|
+
# count.
|
|
46
45
|
class PartitionsCountCache
|
|
47
46
|
include Helpers::Time
|
|
48
47
|
|
|
@@ -91,28 +90,14 @@ module Rdkafka
|
|
|
91
90
|
current_info = @counts[topic]
|
|
92
91
|
|
|
93
92
|
if current_info.nil? || expired?(current_info[0])
|
|
93
|
+
# The cached entry is missing or expired, so the block performs an authoritative metadata
|
|
94
|
+
# read. We hand it to `set`, which adopts a higher count always and a lower count once the
|
|
95
|
+
# entry has expired (e.g. the topic was recreated with fewer partitions). We then return
|
|
96
|
+
# whatever `set` settled on so a concurrent refresh that wrote a higher value still wins.
|
|
94
97
|
new_count = yield
|
|
98
|
+
set(topic, new_count)
|
|
95
99
|
|
|
96
|
-
|
|
97
|
-
# No existing data, create a new entry with mutex
|
|
98
|
-
set(topic, new_count)
|
|
99
|
-
|
|
100
|
-
return new_count
|
|
101
|
-
else
|
|
102
|
-
current_count = current_info[1]
|
|
103
|
-
|
|
104
|
-
if new_count > current_count
|
|
105
|
-
# Higher value needs mutex to update both timestamp and count
|
|
106
|
-
set(topic, new_count)
|
|
107
|
-
|
|
108
|
-
return new_count
|
|
109
|
-
else
|
|
110
|
-
# Same or lower value, just update timestamp without mutex
|
|
111
|
-
refresh_timestamp(topic)
|
|
112
|
-
|
|
113
|
-
return current_count
|
|
114
|
-
end
|
|
115
|
-
end
|
|
100
|
+
return @counts[topic][1]
|
|
116
101
|
end
|
|
117
102
|
|
|
118
103
|
current_info[1]
|
|
@@ -133,8 +118,11 @@ module Rdkafka
|
|
|
133
118
|
# First check outside mutex to avoid unnecessary locking
|
|
134
119
|
current_info = @counts[topic]
|
|
135
120
|
|
|
136
|
-
#
|
|
137
|
-
|
|
121
|
+
# Within the TTL window a lower value is treated as a stale/racy read and ignored, since
|
|
122
|
+
# partition counts only grow during normal operation. Once the entry has expired a lower
|
|
123
|
+
# value is an authoritative refresh (e.g. the topic was recreated with fewer partitions),
|
|
124
|
+
# so we fall through and adopt it below.
|
|
125
|
+
if current_info && new_count < current_info[1] && !expired?(current_info[0])
|
|
138
126
|
refresh_timestamp(topic)
|
|
139
127
|
|
|
140
128
|
return
|
|
@@ -148,17 +136,15 @@ module Rdkafka
|
|
|
148
136
|
if current_info.nil?
|
|
149
137
|
# Create new entry
|
|
150
138
|
@counts[topic] = [monotonic_now_ms, new_count]
|
|
139
|
+
elsif new_count > current_info[1] || expired?(current_info[0])
|
|
140
|
+
# A higher count always wins; a lower count is accepted only when the existing entry
|
|
141
|
+
# has expired, so a concurrent fresh higher value (which reset the timestamp) is never
|
|
142
|
+
# clobbered by a stale lower one.
|
|
143
|
+
current_info[0] = monotonic_now_ms
|
|
144
|
+
current_info[1] = new_count
|
|
151
145
|
else
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
if new_count > current_count
|
|
155
|
-
# Update to higher count value
|
|
156
|
-
current_info[0] = monotonic_now_ms
|
|
157
|
-
current_info[1] = new_count
|
|
158
|
-
else
|
|
159
|
-
# Same or lower count, update timestamp only
|
|
160
|
-
current_info[0] = monotonic_now_ms
|
|
161
|
-
end
|
|
146
|
+
# Same or lower count within the TTL window: refresh the timestamp only
|
|
147
|
+
current_info[0] = monotonic_now_ms
|
|
162
148
|
end
|
|
163
149
|
end
|
|
164
150
|
end
|