karafka-rdkafka 0.30.2-aarch64-linux-gnu → 0.32.0-aarch64-linux-gnu
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/CHANGELOG.md +100 -36
- data/README.md +3 -1
- data/docker-compose-ssl.yml +1 -0
- data/docker-compose.yml +1 -0
- data/ext/librdkafka.so +0 -0
- data/karafka-rdkafka.gemspec +5 -4
- data/lib/rdkafka/admin/acl_binding_result.rb +4 -16
- data/lib/rdkafka/admin/alter_consumer_group_offsets_report.rb +2 -2
- data/lib/rdkafka/admin/create_acl_report.rb +1 -3
- data/lib/rdkafka/admin/create_topic_report.rb +2 -6
- data/lib/rdkafka/admin/delete_consumer_group_offsets_report.rb +2 -2
- data/lib/rdkafka/admin/delete_groups_report.rb +2 -6
- data/lib/rdkafka/admin/delete_topic_report.rb +2 -6
- data/lib/rdkafka/admin/describe_cluster_handle.rb +31 -0
- data/lib/rdkafka/admin/describe_cluster_report.rb +51 -0
- data/lib/rdkafka/admin/describe_consumer_groups_handle.rb +31 -0
- data/lib/rdkafka/admin/describe_consumer_groups_report.rb +151 -0
- data/lib/rdkafka/admin/describe_result_parsing.rb +47 -0
- data/lib/rdkafka/admin/describe_topics_handle.rb +31 -0
- data/lib/rdkafka/admin/describe_topics_report.rb +118 -0
- data/lib/rdkafka/admin/list_consumer_group_offsets_handle.rb +31 -0
- data/lib/rdkafka/admin/list_consumer_group_offsets_report.rb +52 -0
- data/lib/rdkafka/admin.rb +424 -9
- data/lib/rdkafka/bindings/admin/acls.rb +85 -0
- data/lib/rdkafka/bindings/admin/configs.rb +37 -0
- data/lib/rdkafka/bindings/admin/list_offsets.rb +26 -0
- data/lib/rdkafka/bindings/admin.rb +211 -0
- data/lib/rdkafka/bindings/callbacks.rb +155 -0
- data/lib/rdkafka/bindings/configuration.rb +50 -0
- data/lib/rdkafka/bindings/consumer.rb +125 -0
- data/lib/rdkafka/bindings/errors.rb +17 -0
- data/lib/rdkafka/bindings/handle.rb +16 -0
- data/lib/rdkafka/bindings/messages.rb +28 -0
- data/lib/rdkafka/bindings/metadata.rb +17 -0
- data/lib/rdkafka/bindings/polling.rb +17 -0
- data/lib/rdkafka/bindings/producer.rb +82 -0
- data/lib/rdkafka/bindings/share_consumer.rb +67 -0
- data/lib/rdkafka/bindings/structs.rb +62 -0
- data/lib/rdkafka/bindings.rb +16 -775
- data/lib/rdkafka/callbacks/describe_cluster_handler.rb +36 -0
- data/lib/rdkafka/callbacks/describe_consumer_groups_handler.rb +36 -0
- data/lib/rdkafka/callbacks/describe_topics_handler.rb +36 -0
- data/lib/rdkafka/callbacks/list_consumer_group_offsets_handler.rb +62 -0
- data/lib/rdkafka/callbacks.rb +15 -7
- data/lib/rdkafka/clients.rb +13 -46
- data/lib/rdkafka/config.rb +70 -0
- data/lib/rdkafka/consumer/allocator.rb +0 -2
- data/lib/rdkafka/consumer/message.rb +6 -9
- data/lib/rdkafka/consumer/topic_partition_list.rb +7 -7
- data/lib/rdkafka/consumer.rb +76 -36
- data/lib/rdkafka/defaults.rb +6 -4
- data/lib/rdkafka/native_kafka.rb +45 -31
- data/lib/rdkafka/producer/delivery_handle.rb +18 -16
- data/lib/rdkafka/producer/delivery_report.rb +51 -1
- data/lib/rdkafka/producer/partitions_count_cache.rb +109 -26
- data/lib/rdkafka/producer.rb +141 -71
- data/lib/rdkafka/share_consumer.rb +70 -34
- data/lib/rdkafka/version.rb +3 -3
- data/lib/rdkafka.rb +14 -1
- data/package-lock.json +9 -9
- data/renovate.json +7 -0
- metadata +31 -31
|
@@ -42,9 +42,30 @@ module Rdkafka
|
|
|
42
42
|
# an authoritative metadata read and adopts the lower count. Within the TTL window a lower
|
|
43
43
|
# value is still ignored, so a transient or racy lower read cannot clobber a correct higher
|
|
44
44
|
# count.
|
|
45
|
+
#
|
|
46
|
+
# 7. Cluster outages
|
|
47
|
+
# If the refresh of an expired entry fails on a transient error (for example all brokers
|
|
48
|
+
# are unreachable), the last known count is served instead of raising, and the next refresh
|
|
49
|
+
# is attempted after {Defaults::PARTITIONS_COUNT_CACHE_STALE_BACKOFF_MS}. This lets
|
|
50
|
+
# `partition_key` produces keep buffering in librdkafka during an outage. Without a known
|
|
51
|
+
# count the error is raised. Only one caller refreshes an expired entry at a time; the
|
|
52
|
+
# others get the known count meanwhile.
|
|
45
53
|
class PartitionsCountCache
|
|
46
54
|
include Helpers::Time
|
|
47
55
|
|
|
56
|
+
# Refresh errors after which the last known count is still served
|
|
57
|
+
STALE_FALLBACK_ERRORS = %i[
|
|
58
|
+
transport
|
|
59
|
+
timed_out
|
|
60
|
+
all_brokers_down
|
|
61
|
+
resolve
|
|
62
|
+
].freeze
|
|
63
|
+
|
|
64
|
+
private_constant :STALE_FALLBACK_ERRORS
|
|
65
|
+
|
|
66
|
+
# @return [Integer] time-to-live in milliseconds for cached values
|
|
67
|
+
attr_reader :ttl_ms
|
|
68
|
+
|
|
48
69
|
# Creates a new partition count cache
|
|
49
70
|
#
|
|
50
71
|
# @param ttl [Integer, nil] DEPRECATED: Use ttl_ms instead.
|
|
@@ -73,33 +94,44 @@ module Rdkafka
|
|
|
73
94
|
end
|
|
74
95
|
end
|
|
75
96
|
|
|
97
|
+
# Changes the time-to-live of cached values. Cached entries are kept and expire based on the
|
|
98
|
+
# new value.
|
|
99
|
+
#
|
|
100
|
+
# @param value [Integer] time-to-live in milliseconds
|
|
101
|
+
# @raise [ArgumentError] when the value is not a positive Integer
|
|
102
|
+
def ttl_ms=(value)
|
|
103
|
+
raise ArgumentError.new("ttl_ms has to be a positive Integer") unless value.is_a?(Integer) && value.positive?
|
|
104
|
+
|
|
105
|
+
@ttl_ms = value
|
|
106
|
+
end
|
|
107
|
+
|
|
76
108
|
# Reads partition count for a topic with automatic refresh when expired
|
|
77
109
|
#
|
|
78
110
|
# This method will return the cached partition count if available and not expired.
|
|
79
111
|
# If the value is expired or not available, it will execute the provided block
|
|
80
112
|
# to fetch the current value from Kafka.
|
|
81
113
|
#
|
|
82
|
-
# @param
|
|
114
|
+
# @param topic_name [String] Kafka topic name
|
|
83
115
|
# @yield Block that returns the current partition count when cache needs refreshing
|
|
84
116
|
# @yieldreturn [Integer] Current partition count retrieved from Kafka
|
|
85
117
|
# @return [Integer] Partition count for the topic
|
|
118
|
+
# @raise [RdkafkaError] when the refresh fails and there is no last known count to serve, or
|
|
119
|
+
# when it fails on a non-transient error
|
|
86
120
|
#
|
|
87
121
|
# @note The implementation prioritizes read performance over write consistency
|
|
88
122
|
# since partition counts typically only increase during normal operation.
|
|
89
|
-
def get(
|
|
90
|
-
current_info = @counts[
|
|
91
|
-
|
|
92
|
-
if current_info.nil?
|
|
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.
|
|
123
|
+
def get(topic_name)
|
|
124
|
+
current_info = @counts[topic_name]
|
|
125
|
+
|
|
126
|
+
if current_info.nil?
|
|
97
127
|
new_count = yield
|
|
98
|
-
set(
|
|
128
|
+
set(topic_name, new_count)
|
|
99
129
|
|
|
100
|
-
return @counts[
|
|
130
|
+
return @counts[topic_name][1]
|
|
101
131
|
end
|
|
102
132
|
|
|
133
|
+
return refresh(topic_name, current_info) { yield } if expired?(current_info[0])
|
|
134
|
+
|
|
103
135
|
current_info[1]
|
|
104
136
|
end
|
|
105
137
|
|
|
@@ -108,34 +140,34 @@ module Rdkafka
|
|
|
108
140
|
# This method updates the partition count for a topic in the cache.
|
|
109
141
|
# It uses a mutex to ensure thread-safety during updates.
|
|
110
142
|
#
|
|
111
|
-
# @param
|
|
143
|
+
# @param topic_name [String] Kafka topic name
|
|
112
144
|
# @param new_count [Integer] New partition count value
|
|
113
145
|
#
|
|
114
146
|
# @note We prioritize higher partition counts and only accept them when using
|
|
115
147
|
# a mutex to ensure consistency. This design decision is based on the fact that
|
|
116
148
|
# partition counts in Kafka only increase during normal operation.
|
|
117
|
-
def set(
|
|
149
|
+
def set(topic_name, new_count)
|
|
118
150
|
# First check outside mutex to avoid unnecessary locking
|
|
119
|
-
current_info = @counts[
|
|
151
|
+
current_info = @counts[topic_name]
|
|
120
152
|
|
|
121
153
|
# Within the TTL window a lower value is treated as a stale/racy read and ignored, since
|
|
122
154
|
# partition counts only grow during normal operation. Once the entry has expired a lower
|
|
123
155
|
# value is an authoritative refresh (e.g. the topic was recreated with fewer partitions),
|
|
124
156
|
# so we fall through and adopt it below.
|
|
125
157
|
if current_info && new_count < current_info[1] && !expired?(current_info[0])
|
|
126
|
-
refresh_timestamp(
|
|
158
|
+
refresh_timestamp(topic_name)
|
|
127
159
|
|
|
128
160
|
return
|
|
129
161
|
end
|
|
130
162
|
|
|
131
163
|
# Only lock the specific topic mutex
|
|
132
|
-
mutex_for(
|
|
164
|
+
mutex_for(topic_name).synchronize do
|
|
133
165
|
# Check again inside the lock as another thread might have updated
|
|
134
|
-
current_info = @counts[
|
|
166
|
+
current_info = @counts[topic_name]
|
|
135
167
|
|
|
136
168
|
if current_info.nil?
|
|
137
169
|
# Create new entry
|
|
138
|
-
@counts[
|
|
170
|
+
@counts[topic_name] = [monotonic_now_ms, new_count]
|
|
139
171
|
elsif new_count > current_info[1] || expired?(current_info[0])
|
|
140
172
|
# A higher count always wins; a lower count is accepted only when the existing entry
|
|
141
173
|
# has expired, so a concurrent fresh higher value (which reset the timestamp) is never
|
|
@@ -161,24 +193,24 @@ module Rdkafka
|
|
|
161
193
|
# This method ensures that each topic has its own mutex,
|
|
162
194
|
# allowing operations on different topics to proceed in parallel.
|
|
163
195
|
#
|
|
164
|
-
# @param
|
|
196
|
+
# @param topic_name [String] Kafka topic name
|
|
165
197
|
# @return [Mutex] Mutex for the specified topic
|
|
166
198
|
#
|
|
167
199
|
# @note We use a separate mutex (@mutex_for_hash) to protect the creation
|
|
168
200
|
# of new topic mutexes. This pattern allows fine-grained locking while
|
|
169
201
|
# maintaining thread-safety.
|
|
170
|
-
def mutex_for(
|
|
171
|
-
mutex = @mutex_hash[
|
|
202
|
+
def mutex_for(topic_name)
|
|
203
|
+
mutex = @mutex_hash[topic_name]
|
|
172
204
|
|
|
173
205
|
return mutex if mutex
|
|
174
206
|
|
|
175
207
|
# Use a separate mutex to protect the creation of new topic mutexes
|
|
176
208
|
@mutex_for_hash.synchronize do
|
|
177
209
|
# Check again in case another thread created it
|
|
178
|
-
@mutex_hash[
|
|
210
|
+
@mutex_hash[topic_name] ||= Mutex.new
|
|
179
211
|
end
|
|
180
212
|
|
|
181
|
-
@mutex_hash[
|
|
213
|
+
@mutex_hash[topic_name]
|
|
182
214
|
end
|
|
183
215
|
|
|
184
216
|
# Update the timestamp without acquiring the mutex
|
|
@@ -186,13 +218,13 @@ module Rdkafka
|
|
|
186
218
|
# This is an optimization that allows refreshing the TTL of existing entries
|
|
187
219
|
# without the overhead of mutex acquisition.
|
|
188
220
|
#
|
|
189
|
-
# @param
|
|
221
|
+
# @param topic_name [String] Kafka topic name
|
|
190
222
|
#
|
|
191
223
|
# @note This method is safe for refreshing existing data regardless of count
|
|
192
224
|
# because it only updates the timestamp, which doesn't affect the correctness
|
|
193
225
|
# of concurrent operations.
|
|
194
|
-
def refresh_timestamp(
|
|
195
|
-
current_info = @counts[
|
|
226
|
+
def refresh_timestamp(topic_name)
|
|
227
|
+
current_info = @counts[topic_name]
|
|
196
228
|
|
|
197
229
|
return unless current_info
|
|
198
230
|
|
|
@@ -200,6 +232,57 @@ module Rdkafka
|
|
|
200
232
|
current_info[0] = monotonic_now_ms
|
|
201
233
|
end
|
|
202
234
|
|
|
235
|
+
# Refreshes an expired entry. While the refresh runs, other callers see a fresh entry and get
|
|
236
|
+
# the known count instead of running their own metadata request.
|
|
237
|
+
#
|
|
238
|
+
# The block performs an authoritative metadata read. We hand it to `set`, which adopts a
|
|
239
|
+
# higher count always and a lower count once the entry has expired (e.g. the topic was
|
|
240
|
+
# recreated with fewer partitions). We then return whatever `set` settled on so a concurrent
|
|
241
|
+
# refresh that wrote a higher value still wins.
|
|
242
|
+
#
|
|
243
|
+
# @param topic_name [String] Kafka topic name
|
|
244
|
+
# @param current_info [Array<Integer>] cached timestamp and partition count
|
|
245
|
+
# @yieldreturn [Integer] Current partition count retrieved from Kafka
|
|
246
|
+
# @return [Integer] Partition count for the topic
|
|
247
|
+
# @raise [RdkafkaError] when the refresh fails on a non-transient error
|
|
248
|
+
def refresh(topic_name, current_info)
|
|
249
|
+
expired_at = current_info[0]
|
|
250
|
+
# Outlasts a failed refresh even with a TTL shorter than one metadata request
|
|
251
|
+
lease = monotonic_now_ms + Defaults::PARTITIONS_COUNT_CACHE_STALE_BACKOFF_MS
|
|
252
|
+
current_info[0] = lease
|
|
253
|
+
|
|
254
|
+
begin
|
|
255
|
+
new_count = yield
|
|
256
|
+
rescue RdkafkaError => e
|
|
257
|
+
raise unless STALE_FALLBACK_ERRORS.include?(e.code)
|
|
258
|
+
ensure
|
|
259
|
+
# Hand the entry back as expired, unless a concurrent write refreshed it meanwhile
|
|
260
|
+
current_info[0] = expired_at if current_info[0] == lease
|
|
261
|
+
end
|
|
262
|
+
|
|
263
|
+
if new_count.nil?
|
|
264
|
+
# A concurrent write refreshed the entry, so it keeps its full TTL
|
|
265
|
+
return current_info[1] unless current_info[0] == expired_at
|
|
266
|
+
|
|
267
|
+
return serve_stale(current_info)
|
|
268
|
+
end
|
|
269
|
+
|
|
270
|
+
set(topic_name, new_count)
|
|
271
|
+
|
|
272
|
+
@counts[topic_name][1]
|
|
273
|
+
end
|
|
274
|
+
|
|
275
|
+
# Keeps serving an expired entry after a failed refresh and schedules the next refresh
|
|
276
|
+
# after {Defaults::PARTITIONS_COUNT_CACHE_STALE_BACKOFF_MS}
|
|
277
|
+
#
|
|
278
|
+
# @param current_info [Array<Integer>] cached timestamp and partition count
|
|
279
|
+
# @return [Integer] last known partition count
|
|
280
|
+
def serve_stale(current_info)
|
|
281
|
+
current_info[0] = monotonic_now_ms - @ttl_ms + Defaults::PARTITIONS_COUNT_CACHE_STALE_BACKOFF_MS
|
|
282
|
+
|
|
283
|
+
current_info[1]
|
|
284
|
+
end
|
|
285
|
+
|
|
203
286
|
# Check if a timestamp has expired based on the TTL
|
|
204
287
|
#
|
|
205
288
|
# @param timestamp [Integer] Monotonic timestamp in milliseconds to check
|
data/lib/rdkafka/producer.rb
CHANGED
|
@@ -33,6 +33,27 @@ module Rdkafka
|
|
|
33
33
|
|
|
34
34
|
private_constant :EMPTY_HASH
|
|
35
35
|
|
|
36
|
+
# FFI types of the `rd_kafka_producev` varargs are resolved once. A variadic call resolves each
|
|
37
|
+
# type argument it gets on every call, unless it already is an `FFI::Type`
|
|
38
|
+
|
|
39
|
+
# `:int` vararg type
|
|
40
|
+
FFI_INT = Rdkafka::Bindings.find_type(:int)
|
|
41
|
+
# `:pointer` vararg type
|
|
42
|
+
FFI_POINTER = Rdkafka::Bindings.find_type(:pointer)
|
|
43
|
+
# `:buffer_in` vararg type
|
|
44
|
+
FFI_BUFFER_IN = Rdkafka::Bindings.find_type(:buffer_in)
|
|
45
|
+
# `:size_t` vararg type
|
|
46
|
+
FFI_SIZE_T = Rdkafka::Bindings.find_type(:size_t)
|
|
47
|
+
# `:int32` vararg type
|
|
48
|
+
FFI_INT32 = Rdkafka::Bindings.find_type(:int32)
|
|
49
|
+
# `:int64` vararg type
|
|
50
|
+
FFI_INT64 = Rdkafka::Bindings.find_type(:int64)
|
|
51
|
+
# `:string` vararg type
|
|
52
|
+
FFI_STRING = Rdkafka::Bindings.find_type(:string)
|
|
53
|
+
|
|
54
|
+
private_constant :FFI_INT, :FFI_POINTER, :FFI_BUFFER_IN, :FFI_SIZE_T, :FFI_INT32, :FFI_INT64,
|
|
55
|
+
:FFI_STRING
|
|
56
|
+
|
|
36
57
|
# Raised when there was a critical issue when invoking rd_kafka_topic_new
|
|
37
58
|
# This is a temporary solution until https://github.com/karafka/rdkafka-ruby/issues/451 is
|
|
38
59
|
# resolved and this is normalized in all the places
|
|
@@ -55,8 +76,9 @@ module Rdkafka
|
|
|
55
76
|
# @param partitioner [String, nil] name of the partitioner we want to use or nil to use
|
|
56
77
|
# the "consistent_random" default
|
|
57
78
|
def initialize(native_kafka, partitioner)
|
|
58
|
-
@topics_refs_map =
|
|
59
|
-
@topics_configs =
|
|
79
|
+
@topics_refs_map = EMPTY_HASH
|
|
80
|
+
@topics_configs = EMPTY_HASH
|
|
81
|
+
@topics_mutex = Mutex.new
|
|
60
82
|
@native_kafka = native_kafka
|
|
61
83
|
@partitioner = partitioner || "consistent_random"
|
|
62
84
|
|
|
@@ -66,54 +88,27 @@ module Rdkafka
|
|
|
66
88
|
|
|
67
89
|
# Sets alternative set of configuration details that can be set per topic
|
|
68
90
|
#
|
|
69
|
-
# @param
|
|
91
|
+
# @param topic_name [String] The topic name
|
|
70
92
|
# @param config [Hash] config we want to use per topic basis
|
|
71
93
|
# @param config_hash [Integer] hash of the config. We expect it here instead of computing it,
|
|
72
94
|
# because it is already computed during the retrieval attempt in the `#produce` flow.
|
|
73
95
|
# @note It is not allowed to re-set the same topic config twice because of the underlying
|
|
74
96
|
# librdkafka caching
|
|
75
|
-
def set_topic_config(
|
|
76
|
-
#
|
|
97
|
+
def set_topic_config(topic_name, config, config_hash)
|
|
98
|
+
# `with_inner` only counts this as an operation in progress; it takes no lock. Concurrent
|
|
99
|
+
# first produces to the same topic are serialized by @topics_mutex, so librdkafka gets one
|
|
100
|
+
# `rd_kafka_topic_new` per topic and config.
|
|
77
101
|
@native_kafka.with_inner do |inner|
|
|
78
|
-
@
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
Rdkafka::Bindings.rd_kafka_topic_conf_new.tap do |topic_config|
|
|
88
|
-
config.each do |key, value|
|
|
89
|
-
error_buffer = FFI::MemoryPointer.new(:char, 256)
|
|
90
|
-
result = Rdkafka::Bindings.rd_kafka_topic_conf_set(
|
|
91
|
-
topic_config,
|
|
92
|
-
key.to_s,
|
|
93
|
-
value.to_s,
|
|
94
|
-
error_buffer,
|
|
95
|
-
256
|
|
96
|
-
)
|
|
97
|
-
|
|
98
|
-
unless result == :config_ok
|
|
99
|
-
# rd_kafka_topic_new has not been called yet, so librdkafka has not taken
|
|
100
|
-
# ownership of the config. Destroy it ourselves before raising, otherwise the
|
|
101
|
-
# partially built rd_kafka_topic_conf_t leaks. (On the rd_kafka_topic_new path
|
|
102
|
-
# librdkafka frees the conf on both success and failure, so we never destroy it
|
|
103
|
-
# there.)
|
|
104
|
-
Rdkafka::Bindings.rd_kafka_topic_conf_destroy(topic_config)
|
|
105
|
-
raise Config::ConfigError.new(error_buffer.read_string)
|
|
106
|
-
end
|
|
107
|
-
end
|
|
108
|
-
end
|
|
102
|
+
@topics_mutex.synchronize do
|
|
103
|
+
return if @topics_refs_map.dig(topic_name, config_hash)
|
|
104
|
+
|
|
105
|
+
store_topic_handle(
|
|
106
|
+
topic_name,
|
|
107
|
+
config,
|
|
108
|
+
config_hash,
|
|
109
|
+
create_topic_handle(inner, topic_name, config)
|
|
110
|
+
)
|
|
109
111
|
end
|
|
110
|
-
|
|
111
|
-
topic_handle = Bindings.rd_kafka_topic_new(inner, topic, rd_topic_config)
|
|
112
|
-
|
|
113
|
-
raise TopicHandleCreationError.new("Error creating topic handle for topic #{topic}") if topic_handle.null?
|
|
114
|
-
|
|
115
|
-
@topics_configs[topic][config_hash] = config
|
|
116
|
-
@topics_refs_map[topic][config_hash] = topic_handle
|
|
117
112
|
end
|
|
118
113
|
end
|
|
119
114
|
|
|
@@ -254,14 +249,16 @@ module Rdkafka
|
|
|
254
249
|
@native_kafka.close do
|
|
255
250
|
# We need to remove the topics references objects before we destroy the producer,
|
|
256
251
|
# otherwise they would leak out
|
|
257
|
-
@
|
|
258
|
-
|
|
259
|
-
|
|
252
|
+
@topics_mutex.synchronize do
|
|
253
|
+
@topics_refs_map.each_value do |refs|
|
|
254
|
+
refs.each_value do |ref|
|
|
255
|
+
Rdkafka::Bindings.rd_kafka_topic_destroy(ref)
|
|
256
|
+
end
|
|
260
257
|
end
|
|
258
|
+
|
|
259
|
+
@topics_refs_map = EMPTY_HASH
|
|
261
260
|
end
|
|
262
261
|
end
|
|
263
|
-
|
|
264
|
-
@topics_refs_map.clear
|
|
265
262
|
end
|
|
266
263
|
|
|
267
264
|
# Whether this producer has closed
|
|
@@ -346,7 +343,7 @@ module Rdkafka
|
|
|
346
343
|
|
|
347
344
|
# Polls for events in a non-blocking loop, yielding the count after each iteration.
|
|
348
345
|
#
|
|
349
|
-
# This method processes delivery callbacks in a single GVL
|
|
346
|
+
# This method processes delivery callbacks in a single GVL session, which is more
|
|
350
347
|
# efficient than repeated individual polls. It uses non-blocking polls internally
|
|
351
348
|
# (no GVL release between polls).
|
|
352
349
|
#
|
|
@@ -367,8 +364,9 @@ module Rdkafka
|
|
|
367
364
|
# producer.events_poll_nb_each do |_count|
|
|
368
365
|
# :stop if monotonic_now >= deadline
|
|
369
366
|
# end
|
|
370
|
-
# @note This method
|
|
371
|
-
# Other
|
|
367
|
+
# @note This method runs inside `with_inner` and does not hold a lock while it loops.
|
|
368
|
+
# Other operations on the same client run concurrently. `close` waits until this
|
|
369
|
+
# method returns.
|
|
372
370
|
# @note This method is thread-safe as it uses @native_kafka.with_inner synchronization
|
|
373
371
|
def events_poll_nb_each
|
|
374
372
|
closed_producer_check(__method__)
|
|
@@ -384,7 +382,7 @@ module Rdkafka
|
|
|
384
382
|
|
|
385
383
|
# Partition count for a given topic.
|
|
386
384
|
#
|
|
387
|
-
# @param
|
|
385
|
+
# @param topic_name [String] The topic name.
|
|
388
386
|
# @return [Integer] partition count for a given topic or `RD_KAFKA_PARTITION_UA (-1)` if it
|
|
389
387
|
# could not be obtained.
|
|
390
388
|
#
|
|
@@ -399,14 +397,14 @@ module Rdkafka
|
|
|
399
397
|
# `RD_KAFKA_PARTITION_UA` too, so producing to a not-yet-created topic does not run a
|
|
400
398
|
# blocking metadata query on every message; the cache TTL still bounds how long that result
|
|
401
399
|
# is reused before we look again.
|
|
402
|
-
def partition_count(
|
|
400
|
+
def partition_count(topic_name)
|
|
403
401
|
closed_producer_check(__method__)
|
|
404
402
|
|
|
405
|
-
self.class.partitions_count_cache.get(
|
|
403
|
+
self.class.partitions_count_cache.get(topic_name) do
|
|
406
404
|
topic_metadata = nil
|
|
407
405
|
|
|
408
406
|
@native_kafka.with_inner do |inner|
|
|
409
|
-
topic_metadata = ::Rdkafka::Metadata.new(inner,
|
|
407
|
+
topic_metadata = ::Rdkafka::Metadata.new(inner, topic_name).topics&.first
|
|
410
408
|
end
|
|
411
409
|
|
|
412
410
|
topic_metadata ? topic_metadata[:partition_count] : Rdkafka::Bindings::RD_KAFKA_PARTITION_UA
|
|
@@ -486,6 +484,9 @@ module Rdkafka
|
|
|
486
484
|
set_topic_config(topic, topic_config, topic_config_hash) unless @topics_refs_map.dig(topic, topic_config_hash)
|
|
487
485
|
topic_ref = @topics_refs_map.dig(topic, topic_config_hash)
|
|
488
486
|
|
|
487
|
+
# A NULL topic handle would crash librdkafka, so never hand one over
|
|
488
|
+
raise TopicHandleCreationError.new("No topic handle for topic #{topic}") if topic_ref.nil?
|
|
489
|
+
|
|
489
490
|
if partition_key
|
|
490
491
|
partition_count = partition_count(topic)
|
|
491
492
|
|
|
@@ -535,13 +536,15 @@ module Rdkafka
|
|
|
535
536
|
# would orphan the handle in the registry forever, since nothing else will ever resolve it.
|
|
536
537
|
begin
|
|
537
538
|
args = [
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
539
|
+
FFI_INT, Rdkafka::Bindings::RD_KAFKA_VTYPE_RKT, FFI_POINTER, topic_ref,
|
|
540
|
+
FFI_INT, Rdkafka::Bindings::RD_KAFKA_VTYPE_MSGFLAGS,
|
|
541
|
+
FFI_INT, Rdkafka::Bindings::RD_KAFKA_MSG_F_COPY,
|
|
542
|
+
FFI_INT, Rdkafka::Bindings::RD_KAFKA_VTYPE_VALUE,
|
|
543
|
+
FFI_BUFFER_IN, payload, FFI_SIZE_T, payload_size,
|
|
544
|
+
FFI_INT, Rdkafka::Bindings::RD_KAFKA_VTYPE_KEY, FFI_BUFFER_IN, key, FFI_SIZE_T, key_size,
|
|
545
|
+
FFI_INT, Rdkafka::Bindings::RD_KAFKA_VTYPE_PARTITION, FFI_INT32, partition,
|
|
546
|
+
FFI_INT, Rdkafka::Bindings::RD_KAFKA_VTYPE_TIMESTAMP, FFI_INT64, raw_timestamp,
|
|
547
|
+
FFI_INT, Rdkafka::Bindings::RD_KAFKA_VTYPE_OPAQUE, FFI_POINTER, delivery_handle
|
|
545
548
|
]
|
|
546
549
|
|
|
547
550
|
if headers && !headers.empty?
|
|
@@ -553,26 +556,26 @@ module Rdkafka
|
|
|
553
556
|
value0.each do |v|
|
|
554
557
|
value = v.to_s
|
|
555
558
|
args.push(
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
559
|
+
FFI_INT, Rdkafka::Bindings::RD_KAFKA_VTYPE_HEADER,
|
|
560
|
+
FFI_STRING, key,
|
|
561
|
+
FFI_POINTER, value,
|
|
562
|
+
FFI_SIZE_T, value.bytesize
|
|
560
563
|
)
|
|
561
564
|
end
|
|
562
565
|
else
|
|
563
566
|
# Handle single value
|
|
564
567
|
value = value0.to_s
|
|
565
568
|
args.push(
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
569
|
+
FFI_INT, Rdkafka::Bindings::RD_KAFKA_VTYPE_HEADER,
|
|
570
|
+
FFI_STRING, key,
|
|
571
|
+
FFI_POINTER, value,
|
|
572
|
+
FFI_SIZE_T, value.bytesize
|
|
570
573
|
)
|
|
571
574
|
end
|
|
572
575
|
end
|
|
573
576
|
end
|
|
574
577
|
|
|
575
|
-
args.push(
|
|
578
|
+
args.push(FFI_INT, Rdkafka::Bindings::RD_KAFKA_VTYPE_END)
|
|
576
579
|
|
|
577
580
|
# Produce the message
|
|
578
581
|
response = @native_kafka.with_inner do |inner|
|
|
@@ -636,5 +639,72 @@ module Rdkafka
|
|
|
636
639
|
raise Rdkafka::ClosedProducerError.new(method) if closed?
|
|
637
640
|
end
|
|
638
641
|
alias_method :closed_check, :closed_producer_check
|
|
642
|
+
|
|
643
|
+
# Creates a librdkafka topic handle. Callers hold @topics_mutex.
|
|
644
|
+
#
|
|
645
|
+
# @param inner [FFI::Pointer] native producer handle
|
|
646
|
+
# @param topic_name [String] topic name
|
|
647
|
+
# @param config [Hash] per-topic config; empty for the defaults
|
|
648
|
+
# @return [FFI::Pointer] the topic handle
|
|
649
|
+
# @raise [Config::ConfigError] when a per-topic config value is rejected
|
|
650
|
+
# @raise [TopicHandleCreationError] when librdkafka cannot create the handle
|
|
651
|
+
def create_topic_handle(inner, topic_name, config)
|
|
652
|
+
# If config is empty, we create an empty reference that will be used with defaults
|
|
653
|
+
rd_topic_config = if config.empty?
|
|
654
|
+
nil
|
|
655
|
+
else
|
|
656
|
+
Rdkafka::Bindings.rd_kafka_topic_conf_new.tap do |topic_config|
|
|
657
|
+
config.each do |key, value|
|
|
658
|
+
error_buffer = FFI::MemoryPointer.new(:char, 256)
|
|
659
|
+
result = Rdkafka::Bindings.rd_kafka_topic_conf_set(
|
|
660
|
+
topic_config,
|
|
661
|
+
key.to_s,
|
|
662
|
+
value.to_s,
|
|
663
|
+
error_buffer,
|
|
664
|
+
256
|
|
665
|
+
)
|
|
666
|
+
|
|
667
|
+
unless result == :config_ok
|
|
668
|
+
# rd_kafka_topic_new has not been called yet, so librdkafka has not taken
|
|
669
|
+
# ownership of the config. Destroy it ourselves before raising, otherwise the
|
|
670
|
+
# partially built rd_kafka_topic_conf_t leaks. (On the rd_kafka_topic_new path
|
|
671
|
+
# librdkafka frees the conf on both success and failure, so we never destroy it
|
|
672
|
+
# there.)
|
|
673
|
+
Rdkafka::Bindings.rd_kafka_topic_conf_destroy(topic_config)
|
|
674
|
+
raise Config::ConfigError.new(error_buffer.read_string)
|
|
675
|
+
end
|
|
676
|
+
end
|
|
677
|
+
end
|
|
678
|
+
end
|
|
679
|
+
|
|
680
|
+
topic_handle = Bindings.rd_kafka_topic_new(inner, topic_name, rd_topic_config)
|
|
681
|
+
|
|
682
|
+
raise TopicHandleCreationError.new("Error creating topic handle for topic #{topic_name}") if topic_handle.null?
|
|
683
|
+
|
|
684
|
+
topic_handle
|
|
685
|
+
end
|
|
686
|
+
|
|
687
|
+
# Publishes a topic handle and its config. Callers hold @topics_mutex. Both maps are
|
|
688
|
+
# replaced with frozen copies rather than mutated, and the config goes in first, so a
|
|
689
|
+
# lock-free reader that finds the handle also finds its config.
|
|
690
|
+
#
|
|
691
|
+
# @param topic_name [String] topic name
|
|
692
|
+
# @param config [Hash] per-topic config
|
|
693
|
+
# @param config_hash [Integer] hash of the config
|
|
694
|
+
# @param topic_handle [FFI::Pointer] the topic handle
|
|
695
|
+
def store_topic_handle(topic_name, config, config_hash, topic_handle)
|
|
696
|
+
@topics_configs = with_topic_entry(@topics_configs, topic_name, config_hash, config)
|
|
697
|
+
@topics_refs_map = with_topic_entry(@topics_refs_map, topic_name, config_hash, topic_handle)
|
|
698
|
+
end
|
|
699
|
+
|
|
700
|
+
# @param map [Hash{String => Hash{Integer => Object}}] a frozen topic map
|
|
701
|
+
# @param topic_name [String] topic name
|
|
702
|
+
# @param config_hash [Integer] hash of the config
|
|
703
|
+
# @param value [Object] value to store
|
|
704
|
+
# @return [Hash{String => Hash{Integer => Object}}] a frozen copy of the map with the value
|
|
705
|
+
def with_topic_entry(map, topic_name, config_hash, value)
|
|
706
|
+
entries = map.fetch(topic_name, EMPTY_HASH).merge(config_hash => value).freeze
|
|
707
|
+
map.merge(topic_name => entries).freeze
|
|
708
|
+
end
|
|
639
709
|
end
|
|
640
710
|
end
|