karafka-rdkafka 0.20.0.rc3-x86_64-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 +7 -0
- data/.github/CODEOWNERS +3 -0
- data/.github/FUNDING.yml +1 -0
- data/.github/workflows/ci_linux_x86_64_gnu.yml +248 -0
- data/.github/workflows/ci_macos_arm64.yml +301 -0
- data/.github/workflows/push_linux_x86_64_gnu.yml +60 -0
- data/.github/workflows/push_ruby.yml +37 -0
- data/.github/workflows/verify-action-pins.yml +16 -0
- data/.gitignore +15 -0
- data/.rspec +2 -0
- data/.ruby-gemset +1 -0
- data/.ruby-version +1 -0
- data/.yardopts +2 -0
- data/CHANGELOG.md +323 -0
- data/Gemfile +5 -0
- data/MIT-LICENSE +22 -0
- data/README.md +177 -0
- data/Rakefile +96 -0
- data/docker-compose.yml +25 -0
- data/ext/README.md +19 -0
- data/ext/Rakefile +131 -0
- data/ext/build_common.sh +361 -0
- data/ext/build_linux_x86_64_gnu.sh +306 -0
- data/ext/build_macos_arm64.sh +550 -0
- data/ext/librdkafka.so +0 -0
- data/karafka-rdkafka.gemspec +61 -0
- data/lib/rdkafka/abstract_handle.rb +116 -0
- data/lib/rdkafka/admin/acl_binding_result.rb +51 -0
- data/lib/rdkafka/admin/config_binding_result.rb +30 -0
- data/lib/rdkafka/admin/config_resource_binding_result.rb +18 -0
- data/lib/rdkafka/admin/create_acl_handle.rb +28 -0
- data/lib/rdkafka/admin/create_acl_report.rb +24 -0
- data/lib/rdkafka/admin/create_partitions_handle.rb +30 -0
- data/lib/rdkafka/admin/create_partitions_report.rb +6 -0
- data/lib/rdkafka/admin/create_topic_handle.rb +32 -0
- data/lib/rdkafka/admin/create_topic_report.rb +24 -0
- data/lib/rdkafka/admin/delete_acl_handle.rb +30 -0
- data/lib/rdkafka/admin/delete_acl_report.rb +23 -0
- data/lib/rdkafka/admin/delete_groups_handle.rb +28 -0
- data/lib/rdkafka/admin/delete_groups_report.rb +24 -0
- data/lib/rdkafka/admin/delete_topic_handle.rb +32 -0
- data/lib/rdkafka/admin/delete_topic_report.rb +24 -0
- data/lib/rdkafka/admin/describe_acl_handle.rb +30 -0
- data/lib/rdkafka/admin/describe_acl_report.rb +24 -0
- data/lib/rdkafka/admin/describe_configs_handle.rb +33 -0
- data/lib/rdkafka/admin/describe_configs_report.rb +48 -0
- data/lib/rdkafka/admin/incremental_alter_configs_handle.rb +33 -0
- data/lib/rdkafka/admin/incremental_alter_configs_report.rb +48 -0
- data/lib/rdkafka/admin.rb +832 -0
- data/lib/rdkafka/bindings.rb +582 -0
- data/lib/rdkafka/callbacks.rb +415 -0
- data/lib/rdkafka/config.rb +398 -0
- data/lib/rdkafka/consumer/headers.rb +79 -0
- data/lib/rdkafka/consumer/message.rb +86 -0
- data/lib/rdkafka/consumer/partition.rb +57 -0
- data/lib/rdkafka/consumer/topic_partition_list.rb +190 -0
- data/lib/rdkafka/consumer.rb +663 -0
- data/lib/rdkafka/error.rb +201 -0
- data/lib/rdkafka/helpers/oauth.rb +58 -0
- data/lib/rdkafka/helpers/time.rb +14 -0
- data/lib/rdkafka/metadata.rb +115 -0
- data/lib/rdkafka/native_kafka.rb +139 -0
- data/lib/rdkafka/producer/delivery_handle.rb +48 -0
- data/lib/rdkafka/producer/delivery_report.rb +45 -0
- data/lib/rdkafka/producer/partitions_count_cache.rb +216 -0
- data/lib/rdkafka/producer.rb +492 -0
- data/lib/rdkafka/version.rb +7 -0
- data/lib/rdkafka.rb +54 -0
- data/renovate.json +92 -0
- data/spec/rdkafka/abstract_handle_spec.rb +117 -0
- data/spec/rdkafka/admin/create_acl_handle_spec.rb +56 -0
- data/spec/rdkafka/admin/create_acl_report_spec.rb +18 -0
- data/spec/rdkafka/admin/create_topic_handle_spec.rb +54 -0
- data/spec/rdkafka/admin/create_topic_report_spec.rb +16 -0
- data/spec/rdkafka/admin/delete_acl_handle_spec.rb +85 -0
- data/spec/rdkafka/admin/delete_acl_report_spec.rb +72 -0
- data/spec/rdkafka/admin/delete_topic_handle_spec.rb +54 -0
- data/spec/rdkafka/admin/delete_topic_report_spec.rb +16 -0
- data/spec/rdkafka/admin/describe_acl_handle_spec.rb +85 -0
- data/spec/rdkafka/admin/describe_acl_report_spec.rb +73 -0
- data/spec/rdkafka/admin_spec.rb +769 -0
- data/spec/rdkafka/bindings_spec.rb +222 -0
- data/spec/rdkafka/callbacks_spec.rb +20 -0
- data/spec/rdkafka/config_spec.rb +258 -0
- data/spec/rdkafka/consumer/headers_spec.rb +73 -0
- data/spec/rdkafka/consumer/message_spec.rb +139 -0
- data/spec/rdkafka/consumer/partition_spec.rb +57 -0
- data/spec/rdkafka/consumer/topic_partition_list_spec.rb +248 -0
- data/spec/rdkafka/consumer_spec.rb +1299 -0
- data/spec/rdkafka/error_spec.rb +95 -0
- data/spec/rdkafka/metadata_spec.rb +79 -0
- data/spec/rdkafka/native_kafka_spec.rb +130 -0
- data/spec/rdkafka/producer/delivery_handle_spec.rb +60 -0
- data/spec/rdkafka/producer/delivery_report_spec.rb +25 -0
- data/spec/rdkafka/producer/partitions_count_cache_spec.rb +359 -0
- data/spec/rdkafka/producer/partitions_count_spec.rb +359 -0
- data/spec/rdkafka/producer_spec.rb +1234 -0
- data/spec/spec_helper.rb +181 -0
- metadata +244 -0
@@ -0,0 +1,663 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Rdkafka
|
4
|
+
# A consumer of Kafka messages. It uses the high-level consumer approach where the Kafka
|
5
|
+
# brokers automatically assign partitions and load balance partitions over consumers that
|
6
|
+
# have the same `:"group.id"` set in their configuration.
|
7
|
+
#
|
8
|
+
# To create a consumer set up a {Config} and call {Config#consumer consumer} on that. It is
|
9
|
+
# mandatory to set `:"group.id"` in the configuration.
|
10
|
+
#
|
11
|
+
# Consumer implements `Enumerable`, so you can use `each` to consume messages, or for example
|
12
|
+
# `each_slice` to consume batches of messages.
|
13
|
+
class Consumer
|
14
|
+
include Enumerable
|
15
|
+
include Helpers::Time
|
16
|
+
include Helpers::OAuth
|
17
|
+
|
18
|
+
# @private
|
19
|
+
def initialize(native_kafka)
|
20
|
+
@native_kafka = native_kafka
|
21
|
+
end
|
22
|
+
|
23
|
+
# Starts the native Kafka polling thread and kicks off the init polling
|
24
|
+
# @note Not needed to run unless explicit start was disabled
|
25
|
+
def start
|
26
|
+
@native_kafka.start
|
27
|
+
end
|
28
|
+
|
29
|
+
# @return [String] consumer name
|
30
|
+
def name
|
31
|
+
@name ||= @native_kafka.with_inner do |inner|
|
32
|
+
::Rdkafka::Bindings.rd_kafka_name(inner)
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
def finalizer
|
37
|
+
->(_) { close }
|
38
|
+
end
|
39
|
+
|
40
|
+
# Close this consumer
|
41
|
+
# @return [nil]
|
42
|
+
def close
|
43
|
+
return if closed?
|
44
|
+
ObjectSpace.undefine_finalizer(self)
|
45
|
+
|
46
|
+
@native_kafka.synchronize do |inner|
|
47
|
+
Rdkafka::Bindings.rd_kafka_consumer_close(inner)
|
48
|
+
end
|
49
|
+
|
50
|
+
@native_kafka.close
|
51
|
+
end
|
52
|
+
|
53
|
+
# Whether this consumer has closed
|
54
|
+
def closed?
|
55
|
+
@native_kafka.closed?
|
56
|
+
end
|
57
|
+
|
58
|
+
# Subscribes to one or more topics letting Kafka handle partition assignments.
|
59
|
+
#
|
60
|
+
# @param topics [Array<String>] One or more topic names
|
61
|
+
# @return [nil]
|
62
|
+
# @raise [RdkafkaError] When subscribing fails
|
63
|
+
def subscribe(*topics)
|
64
|
+
closed_consumer_check(__method__)
|
65
|
+
|
66
|
+
# Create topic partition list with topics and no partition set
|
67
|
+
tpl = Rdkafka::Bindings.rd_kafka_topic_partition_list_new(topics.length)
|
68
|
+
|
69
|
+
topics.each do |topic|
|
70
|
+
Rdkafka::Bindings.rd_kafka_topic_partition_list_add(tpl, topic, -1)
|
71
|
+
end
|
72
|
+
|
73
|
+
# Subscribe to topic partition list and check this was successful
|
74
|
+
response = @native_kafka.with_inner do |inner|
|
75
|
+
Rdkafka::Bindings.rd_kafka_subscribe(inner, tpl)
|
76
|
+
end
|
77
|
+
|
78
|
+
Rdkafka::RdkafkaError.validate!(response, "Error subscribing to '#{topics.join(', ')}'")
|
79
|
+
ensure
|
80
|
+
Rdkafka::Bindings.rd_kafka_topic_partition_list_destroy(tpl) unless tpl.nil?
|
81
|
+
end
|
82
|
+
|
83
|
+
# Unsubscribe from all subscribed topics.
|
84
|
+
#
|
85
|
+
# @return [nil]
|
86
|
+
# @raise [RdkafkaError] When unsubscribing fails
|
87
|
+
def unsubscribe
|
88
|
+
closed_consumer_check(__method__)
|
89
|
+
|
90
|
+
response = @native_kafka.with_inner do |inner|
|
91
|
+
Rdkafka::Bindings.rd_kafka_unsubscribe(inner)
|
92
|
+
end
|
93
|
+
|
94
|
+
Rdkafka::RdkafkaError.validate!(response)
|
95
|
+
|
96
|
+
nil
|
97
|
+
end
|
98
|
+
|
99
|
+
# Pause producing or consumption for the provided list of partitions
|
100
|
+
#
|
101
|
+
# @param list [TopicPartitionList] The topic with partitions to pause
|
102
|
+
# @return [nil]
|
103
|
+
# @raise [RdkafkaTopicPartitionListError] When pausing subscription fails.
|
104
|
+
def pause(list)
|
105
|
+
closed_consumer_check(__method__)
|
106
|
+
|
107
|
+
unless list.is_a?(TopicPartitionList)
|
108
|
+
raise TypeError.new("list has to be a TopicPartitionList")
|
109
|
+
end
|
110
|
+
|
111
|
+
tpl = list.to_native_tpl
|
112
|
+
|
113
|
+
begin
|
114
|
+
response = @native_kafka.with_inner do |inner|
|
115
|
+
Rdkafka::Bindings.rd_kafka_pause_partitions(inner, tpl)
|
116
|
+
end
|
117
|
+
|
118
|
+
if response != 0
|
119
|
+
list = TopicPartitionList.from_native_tpl(tpl)
|
120
|
+
raise Rdkafka::RdkafkaTopicPartitionListError.new(response, list, "Error pausing '#{list.to_h}'")
|
121
|
+
end
|
122
|
+
ensure
|
123
|
+
Rdkafka::Bindings.rd_kafka_topic_partition_list_destroy(tpl)
|
124
|
+
end
|
125
|
+
end
|
126
|
+
|
127
|
+
# Resumes producing consumption for the provided list of partitions
|
128
|
+
#
|
129
|
+
# @param list [TopicPartitionList] The topic with partitions to pause
|
130
|
+
# @return [nil]
|
131
|
+
# @raise [RdkafkaError] When resume subscription fails.
|
132
|
+
def resume(list)
|
133
|
+
closed_consumer_check(__method__)
|
134
|
+
|
135
|
+
unless list.is_a?(TopicPartitionList)
|
136
|
+
raise TypeError.new("list has to be a TopicPartitionList")
|
137
|
+
end
|
138
|
+
|
139
|
+
tpl = list.to_native_tpl
|
140
|
+
|
141
|
+
begin
|
142
|
+
response = @native_kafka.with_inner do |inner|
|
143
|
+
Rdkafka::Bindings.rd_kafka_resume_partitions(inner, tpl)
|
144
|
+
end
|
145
|
+
|
146
|
+
Rdkafka::RdkafkaError.validate!(response, "Error resume '#{list.to_h}'")
|
147
|
+
|
148
|
+
nil
|
149
|
+
ensure
|
150
|
+
Rdkafka::Bindings.rd_kafka_topic_partition_list_destroy(tpl)
|
151
|
+
end
|
152
|
+
end
|
153
|
+
|
154
|
+
# Returns the current subscription to topics and partitions
|
155
|
+
#
|
156
|
+
# @return [TopicPartitionList]
|
157
|
+
# @raise [RdkafkaError] When getting the subscription fails.
|
158
|
+
def subscription
|
159
|
+
closed_consumer_check(__method__)
|
160
|
+
|
161
|
+
ptr = FFI::MemoryPointer.new(:pointer)
|
162
|
+
response = @native_kafka.with_inner do |inner|
|
163
|
+
Rdkafka::Bindings.rd_kafka_subscription(inner, ptr)
|
164
|
+
end
|
165
|
+
|
166
|
+
Rdkafka::RdkafkaError.validate!(response)
|
167
|
+
|
168
|
+
native = ptr.read_pointer
|
169
|
+
|
170
|
+
begin
|
171
|
+
Rdkafka::Consumer::TopicPartitionList.from_native_tpl(native)
|
172
|
+
ensure
|
173
|
+
Rdkafka::Bindings.rd_kafka_topic_partition_list_destroy(native)
|
174
|
+
end
|
175
|
+
end
|
176
|
+
|
177
|
+
# Atomic assignment of partitions to consume
|
178
|
+
#
|
179
|
+
# @param list [TopicPartitionList] The topic with partitions to assign
|
180
|
+
# @raise [RdkafkaError] When assigning fails
|
181
|
+
def assign(list)
|
182
|
+
closed_consumer_check(__method__)
|
183
|
+
|
184
|
+
unless list.is_a?(TopicPartitionList)
|
185
|
+
raise TypeError.new("list has to be a TopicPartitionList")
|
186
|
+
end
|
187
|
+
|
188
|
+
tpl = list.to_native_tpl
|
189
|
+
|
190
|
+
begin
|
191
|
+
response = @native_kafka.with_inner do |inner|
|
192
|
+
Rdkafka::Bindings.rd_kafka_assign(inner, tpl)
|
193
|
+
end
|
194
|
+
|
195
|
+
Rdkafka::RdkafkaError.validate!(response, "Error assigning '#{list.to_h}'")
|
196
|
+
ensure
|
197
|
+
Rdkafka::Bindings.rd_kafka_topic_partition_list_destroy(tpl)
|
198
|
+
end
|
199
|
+
end
|
200
|
+
|
201
|
+
# Returns the current partition assignment.
|
202
|
+
#
|
203
|
+
# @return [TopicPartitionList]
|
204
|
+
# @raise [RdkafkaError] When getting the assignment fails.
|
205
|
+
def assignment
|
206
|
+
closed_consumer_check(__method__)
|
207
|
+
|
208
|
+
ptr = FFI::MemoryPointer.new(:pointer)
|
209
|
+
response = @native_kafka.with_inner do |inner|
|
210
|
+
Rdkafka::Bindings.rd_kafka_assignment(inner, ptr)
|
211
|
+
end
|
212
|
+
|
213
|
+
Rdkafka::RdkafkaError.validate!(response)
|
214
|
+
|
215
|
+
tpl = ptr.read_pointer
|
216
|
+
|
217
|
+
if !tpl.null?
|
218
|
+
begin
|
219
|
+
Rdkafka::Consumer::TopicPartitionList.from_native_tpl(tpl)
|
220
|
+
ensure
|
221
|
+
Rdkafka::Bindings.rd_kafka_topic_partition_list_destroy tpl
|
222
|
+
end
|
223
|
+
end
|
224
|
+
ensure
|
225
|
+
ptr.free unless ptr.nil?
|
226
|
+
end
|
227
|
+
|
228
|
+
# @return [Boolean] true if our current assignment has been lost involuntarily.
|
229
|
+
def assignment_lost?
|
230
|
+
closed_consumer_check(__method__)
|
231
|
+
|
232
|
+
@native_kafka.with_inner do |inner|
|
233
|
+
!Rdkafka::Bindings.rd_kafka_assignment_lost(inner).zero?
|
234
|
+
end
|
235
|
+
end
|
236
|
+
|
237
|
+
# Return the current committed offset per partition for this consumer group.
|
238
|
+
# The offset field of each requested partition will either be set to stored offset or to -1001
|
239
|
+
# in case there was no stored offset for that partition.
|
240
|
+
#
|
241
|
+
# @param list [TopicPartitionList, nil] The topic with partitions to get the offsets for or nil
|
242
|
+
# to use the current subscription.
|
243
|
+
# @param timeout_ms [Integer] The timeout for fetching this information.
|
244
|
+
# @return [TopicPartitionList]
|
245
|
+
# @raise [RdkafkaError] When getting the committed positions fails.
|
246
|
+
def committed(list=nil, timeout_ms=2_000)
|
247
|
+
closed_consumer_check(__method__)
|
248
|
+
|
249
|
+
if list.nil?
|
250
|
+
list = assignment
|
251
|
+
elsif !list.is_a?(TopicPartitionList)
|
252
|
+
raise TypeError.new("list has to be nil or a TopicPartitionList")
|
253
|
+
end
|
254
|
+
|
255
|
+
tpl = list.to_native_tpl
|
256
|
+
|
257
|
+
begin
|
258
|
+
response = @native_kafka.with_inner do |inner|
|
259
|
+
Rdkafka::Bindings.rd_kafka_committed(inner, tpl, timeout_ms)
|
260
|
+
end
|
261
|
+
|
262
|
+
Rdkafka::RdkafkaError.validate!(response)
|
263
|
+
|
264
|
+
TopicPartitionList.from_native_tpl(tpl)
|
265
|
+
ensure
|
266
|
+
Rdkafka::Bindings.rd_kafka_topic_partition_list_destroy(tpl)
|
267
|
+
end
|
268
|
+
end
|
269
|
+
|
270
|
+
# Return the current positions (offsets) for topics and partitions.
|
271
|
+
# The offset field of each requested partition will be set to the offset of the last consumed message + 1, or nil in case there was no previous message.
|
272
|
+
#
|
273
|
+
# @param list [TopicPartitionList, nil] The topic with partitions to get the offsets for or nil to use the current subscription.
|
274
|
+
#
|
275
|
+
# @return [TopicPartitionList]
|
276
|
+
#
|
277
|
+
# @raise [RdkafkaError] When getting the positions fails.
|
278
|
+
def position(list=nil)
|
279
|
+
if list.nil?
|
280
|
+
list = assignment
|
281
|
+
elsif !list.is_a?(TopicPartitionList)
|
282
|
+
raise TypeError.new("list has to be nil or a TopicPartitionList")
|
283
|
+
end
|
284
|
+
|
285
|
+
tpl = list.to_native_tpl
|
286
|
+
|
287
|
+
response = @native_kafka.with_inner do |inner|
|
288
|
+
Rdkafka::Bindings.rd_kafka_position(inner, tpl)
|
289
|
+
end
|
290
|
+
|
291
|
+
Rdkafka::RdkafkaError.validate!(response)
|
292
|
+
|
293
|
+
TopicPartitionList.from_native_tpl(tpl)
|
294
|
+
end
|
295
|
+
|
296
|
+
# Query broker for low (oldest/beginning) and high (newest/end) offsets for a partition.
|
297
|
+
#
|
298
|
+
# @param topic [String] The topic to query
|
299
|
+
# @param partition [Integer] The partition to query
|
300
|
+
# @param timeout_ms [Integer] The timeout for querying the broker
|
301
|
+
# @return [Integer] The low and high watermark
|
302
|
+
# @raise [RdkafkaError] When querying the broker fails.
|
303
|
+
def query_watermark_offsets(topic, partition, timeout_ms=1000)
|
304
|
+
closed_consumer_check(__method__)
|
305
|
+
|
306
|
+
low = FFI::MemoryPointer.new(:int64, 1)
|
307
|
+
high = FFI::MemoryPointer.new(:int64, 1)
|
308
|
+
|
309
|
+
response = @native_kafka.with_inner do |inner|
|
310
|
+
Rdkafka::Bindings.rd_kafka_query_watermark_offsets(
|
311
|
+
inner,
|
312
|
+
topic,
|
313
|
+
partition,
|
314
|
+
low,
|
315
|
+
high,
|
316
|
+
timeout_ms,
|
317
|
+
)
|
318
|
+
end
|
319
|
+
|
320
|
+
Rdkafka::RdkafkaError.validate!(response, "Error querying watermark offsets for partition #{partition} of #{topic}")
|
321
|
+
|
322
|
+
return low.read_array_of_int64(1).first, high.read_array_of_int64(1).first
|
323
|
+
ensure
|
324
|
+
low.free unless low.nil?
|
325
|
+
high.free unless high.nil?
|
326
|
+
end
|
327
|
+
|
328
|
+
# Calculate the consumer lag per partition for the provided topic partition list.
|
329
|
+
# You can get a suitable list by calling {committed} or {position} (TODO). It is also
|
330
|
+
# possible to create one yourself, in this case you have to provide a list that
|
331
|
+
# already contains all the partitions you need the lag for.
|
332
|
+
#
|
333
|
+
# @param topic_partition_list [TopicPartitionList] The list to calculate lag for.
|
334
|
+
# @param watermark_timeout_ms [Integer] The timeout for each query watermark call.
|
335
|
+
# @return [Hash<String, Hash<Integer, Integer>>] A hash containing all topics with the lag
|
336
|
+
# per partition
|
337
|
+
# @raise [RdkafkaError] When querying the broker fails.
|
338
|
+
def lag(topic_partition_list, watermark_timeout_ms=1000)
|
339
|
+
out = {}
|
340
|
+
|
341
|
+
topic_partition_list.to_h.each do |topic, partitions|
|
342
|
+
# Query high watermarks for this topic's partitions
|
343
|
+
# and compare to the offset in the list.
|
344
|
+
topic_out = {}
|
345
|
+
partitions.each do |p|
|
346
|
+
next if p.offset.nil?
|
347
|
+
_, high = query_watermark_offsets(
|
348
|
+
topic,
|
349
|
+
p.partition,
|
350
|
+
watermark_timeout_ms
|
351
|
+
)
|
352
|
+
topic_out[p.partition] = high - p.offset
|
353
|
+
end
|
354
|
+
out[topic] = topic_out
|
355
|
+
end
|
356
|
+
out
|
357
|
+
end
|
358
|
+
|
359
|
+
# Returns the ClusterId as reported in broker metadata.
|
360
|
+
#
|
361
|
+
# @return [String, nil]
|
362
|
+
def cluster_id
|
363
|
+
closed_consumer_check(__method__)
|
364
|
+
@native_kafka.with_inner do |inner|
|
365
|
+
Rdkafka::Bindings.rd_kafka_clusterid(inner)
|
366
|
+
end
|
367
|
+
end
|
368
|
+
|
369
|
+
# Returns this client's broker-assigned group member id
|
370
|
+
#
|
371
|
+
# This currently requires the high-level KafkaConsumer
|
372
|
+
#
|
373
|
+
# @return [String, nil]
|
374
|
+
def member_id
|
375
|
+
closed_consumer_check(__method__)
|
376
|
+
@native_kafka.with_inner do |inner|
|
377
|
+
Rdkafka::Bindings.rd_kafka_memberid(inner)
|
378
|
+
end
|
379
|
+
end
|
380
|
+
|
381
|
+
# Store offset of a message to be used in the next commit of this consumer
|
382
|
+
#
|
383
|
+
# When using this `enable.auto.offset.store` should be set to `false` in the config.
|
384
|
+
#
|
385
|
+
# @param message [Rdkafka::Consumer::Message] The message which offset will be stored
|
386
|
+
# @param metadata [String, nil] commit metadata string or nil if none
|
387
|
+
# @return [nil]
|
388
|
+
# @raise [RdkafkaError] When storing the offset fails
|
389
|
+
def store_offset(message, metadata = nil)
|
390
|
+
closed_consumer_check(__method__)
|
391
|
+
|
392
|
+
list = TopicPartitionList.new
|
393
|
+
|
394
|
+
# For metadata aware commits we build the partition reference directly to save on
|
395
|
+
# objects allocations
|
396
|
+
if metadata
|
397
|
+
list.add_topic_and_partitions_with_offsets(
|
398
|
+
message.topic,
|
399
|
+
[
|
400
|
+
Consumer::Partition.new(
|
401
|
+
message.partition,
|
402
|
+
message.offset + 1,
|
403
|
+
0,
|
404
|
+
metadata
|
405
|
+
)
|
406
|
+
]
|
407
|
+
)
|
408
|
+
else
|
409
|
+
list.add_topic_and_partitions_with_offsets(
|
410
|
+
message.topic,
|
411
|
+
message.partition => message.offset + 1
|
412
|
+
)
|
413
|
+
end
|
414
|
+
|
415
|
+
tpl = list.to_native_tpl
|
416
|
+
|
417
|
+
response = @native_kafka.with_inner do |inner|
|
418
|
+
Rdkafka::Bindings.rd_kafka_offsets_store(
|
419
|
+
inner,
|
420
|
+
tpl
|
421
|
+
)
|
422
|
+
end
|
423
|
+
|
424
|
+
Rdkafka::RdkafkaError.validate!(response)
|
425
|
+
|
426
|
+
nil
|
427
|
+
ensure
|
428
|
+
Rdkafka::Bindings.rd_kafka_topic_partition_list_destroy(tpl) if tpl
|
429
|
+
end
|
430
|
+
|
431
|
+
# Seek to a particular message. The next poll on the topic/partition will return the
|
432
|
+
# message at the given offset.
|
433
|
+
#
|
434
|
+
# @param message [Rdkafka::Consumer::Message] The message to which to seek
|
435
|
+
# @return [nil]
|
436
|
+
# @raise [RdkafkaError] When seeking fails
|
437
|
+
def seek(message)
|
438
|
+
seek_by(message.topic, message.partition, message.offset)
|
439
|
+
end
|
440
|
+
|
441
|
+
# Seek to a particular message by providing the topic, partition and offset.
|
442
|
+
# The next poll on the topic/partition will return the
|
443
|
+
# message at the given offset.
|
444
|
+
#
|
445
|
+
# @param topic [String] The topic in which to seek
|
446
|
+
# @param partition [Integer] The partition number to seek
|
447
|
+
# @param offset [Integer] The partition offset to seek
|
448
|
+
# @return [nil]
|
449
|
+
# @raise [RdkafkaError] When seeking fails
|
450
|
+
def seek_by(topic, partition, offset)
|
451
|
+
closed_consumer_check(__method__)
|
452
|
+
|
453
|
+
# rd_kafka_offset_store is one of the few calls that does not support
|
454
|
+
# a string as the topic, so create a native topic for it.
|
455
|
+
native_topic = @native_kafka.with_inner do |inner|
|
456
|
+
Rdkafka::Bindings.rd_kafka_topic_new(
|
457
|
+
inner,
|
458
|
+
topic,
|
459
|
+
nil
|
460
|
+
)
|
461
|
+
end
|
462
|
+
response = Rdkafka::Bindings.rd_kafka_seek(
|
463
|
+
native_topic,
|
464
|
+
partition,
|
465
|
+
offset,
|
466
|
+
0 # timeout
|
467
|
+
)
|
468
|
+
Rdkafka::RdkafkaError.validate!(response)
|
469
|
+
|
470
|
+
nil
|
471
|
+
ensure
|
472
|
+
if native_topic && !native_topic.null?
|
473
|
+
Rdkafka::Bindings.rd_kafka_topic_destroy(native_topic)
|
474
|
+
end
|
475
|
+
end
|
476
|
+
|
477
|
+
# Lookup offset for the given partitions by timestamp.
|
478
|
+
#
|
479
|
+
# @param list [TopicPartitionList] The TopicPartitionList with timestamps instead of offsets
|
480
|
+
#
|
481
|
+
# @return [TopicPartitionList]
|
482
|
+
#
|
483
|
+
# @raise [RdKafkaError] When the OffsetForTimes lookup fails
|
484
|
+
def offsets_for_times(list, timeout_ms = 1000)
|
485
|
+
closed_consumer_check(__method__)
|
486
|
+
|
487
|
+
if !list.is_a?(TopicPartitionList)
|
488
|
+
raise TypeError.new("list has to be a TopicPartitionList")
|
489
|
+
end
|
490
|
+
|
491
|
+
tpl = list.to_native_tpl
|
492
|
+
|
493
|
+
response = @native_kafka.with_inner do |inner|
|
494
|
+
Rdkafka::Bindings.rd_kafka_offsets_for_times(
|
495
|
+
inner,
|
496
|
+
tpl,
|
497
|
+
timeout_ms # timeout
|
498
|
+
)
|
499
|
+
end
|
500
|
+
|
501
|
+
Rdkafka::RdkafkaError.validate!(response)
|
502
|
+
|
503
|
+
TopicPartitionList.from_native_tpl(tpl)
|
504
|
+
ensure
|
505
|
+
Rdkafka::Bindings.rd_kafka_topic_partition_list_destroy(tpl) if tpl
|
506
|
+
end
|
507
|
+
|
508
|
+
# Manually commit the current offsets of this consumer.
|
509
|
+
#
|
510
|
+
# To use this set `enable.auto.commit`to `false` to disable automatic triggering
|
511
|
+
# of commits.
|
512
|
+
#
|
513
|
+
# If `enable.auto.offset.store` is set to `true` the offset of the last consumed
|
514
|
+
# message for every partition is used. If set to `false` you can use {store_offset} to
|
515
|
+
# indicate when a message has been fully processed.
|
516
|
+
#
|
517
|
+
# @param list [TopicPartitionList,nil] The topic with partitions to commit
|
518
|
+
# @param async [Boolean] Whether to commit async or wait for the commit to finish
|
519
|
+
# @return [nil]
|
520
|
+
# @raise [RdkafkaError] When committing fails
|
521
|
+
def commit(list=nil, async=false)
|
522
|
+
closed_consumer_check(__method__)
|
523
|
+
|
524
|
+
if !list.nil? && !list.is_a?(TopicPartitionList)
|
525
|
+
raise TypeError.new("list has to be nil or a TopicPartitionList")
|
526
|
+
end
|
527
|
+
|
528
|
+
tpl = list ? list.to_native_tpl : nil
|
529
|
+
|
530
|
+
begin
|
531
|
+
response = @native_kafka.with_inner do |inner|
|
532
|
+
Rdkafka::Bindings.rd_kafka_commit(inner, tpl, async)
|
533
|
+
end
|
534
|
+
|
535
|
+
Rdkafka::RdkafkaError.validate!(response)
|
536
|
+
|
537
|
+
nil
|
538
|
+
ensure
|
539
|
+
Rdkafka::Bindings.rd_kafka_topic_partition_list_destroy(tpl) if tpl
|
540
|
+
end
|
541
|
+
end
|
542
|
+
|
543
|
+
# Poll for the next message on one of the subscribed topics
|
544
|
+
#
|
545
|
+
# @param timeout_ms [Integer] Timeout of this poll
|
546
|
+
# @return [Message, nil] A message or nil if there was no new message within the timeout
|
547
|
+
# @raise [RdkafkaError] When polling fails
|
548
|
+
def poll(timeout_ms)
|
549
|
+
closed_consumer_check(__method__)
|
550
|
+
|
551
|
+
message_ptr = @native_kafka.with_inner do |inner|
|
552
|
+
Rdkafka::Bindings.rd_kafka_consumer_poll(inner, timeout_ms)
|
553
|
+
end
|
554
|
+
|
555
|
+
return nil if message_ptr.null?
|
556
|
+
|
557
|
+
# Create struct wrapper
|
558
|
+
native_message = Rdkafka::Bindings::Message.new(message_ptr)
|
559
|
+
|
560
|
+
# Create a message to pass out
|
561
|
+
return Rdkafka::Consumer::Message.new(native_message) if native_message[:err].zero?
|
562
|
+
|
563
|
+
# Raise error if needed
|
564
|
+
Rdkafka::RdkafkaError.validate!(native_message)
|
565
|
+
ensure
|
566
|
+
# Clean up rdkafka message if there is one
|
567
|
+
if message_ptr && !message_ptr.null?
|
568
|
+
Rdkafka::Bindings.rd_kafka_message_destroy(message_ptr)
|
569
|
+
end
|
570
|
+
end
|
571
|
+
|
572
|
+
# Polls the main rdkafka queue (not the consumer one). Do **NOT** use it if `consumer_poll_set`
|
573
|
+
# was set to `true`.
|
574
|
+
#
|
575
|
+
# Events will cause application-provided callbacks to be called.
|
576
|
+
#
|
577
|
+
# Events (in the context of the consumer):
|
578
|
+
# - error callbacks
|
579
|
+
# - stats callbacks
|
580
|
+
# - any other callbacks supported by librdkafka that are not part of the consumer_poll, that
|
581
|
+
# would have a callback configured and activated.
|
582
|
+
#
|
583
|
+
# This method needs to be called at regular intervals to serve any queued callbacks waiting to
|
584
|
+
# be called. When in use, does **NOT** replace `#poll` but needs to run complementary with it.
|
585
|
+
#
|
586
|
+
# @param timeout_ms [Integer] poll timeout. If set to 0 will run async, when set to -1 will
|
587
|
+
# block until any events available.
|
588
|
+
#
|
589
|
+
# @note This method technically should be called `#poll` and the current `#poll` should be
|
590
|
+
# called `#consumer_poll` though we keep the current naming convention to make it backward
|
591
|
+
# compatible.
|
592
|
+
def events_poll(timeout_ms = 0)
|
593
|
+
@native_kafka.with_inner do |inner|
|
594
|
+
Rdkafka::Bindings.rd_kafka_poll(inner, timeout_ms)
|
595
|
+
end
|
596
|
+
end
|
597
|
+
|
598
|
+
# Poll for new messages and yield for each received one. Iteration
|
599
|
+
# will end when the consumer is closed.
|
600
|
+
#
|
601
|
+
# If `enable.partition.eof` is turned on in the config this will raise an error when an eof is
|
602
|
+
# reached, so you probably want to disable that when using this method of iteration.
|
603
|
+
#
|
604
|
+
# @yieldparam message [Message] Received message
|
605
|
+
# @return [nil]
|
606
|
+
# @raise [RdkafkaError] When polling fails
|
607
|
+
def each
|
608
|
+
loop do
|
609
|
+
message = poll(250)
|
610
|
+
if message
|
611
|
+
yield(message)
|
612
|
+
else
|
613
|
+
if closed?
|
614
|
+
break
|
615
|
+
else
|
616
|
+
next
|
617
|
+
end
|
618
|
+
end
|
619
|
+
end
|
620
|
+
end
|
621
|
+
|
622
|
+
# Deprecated. Please read the error message for more details.
|
623
|
+
def each_batch(max_items: 100, bytes_threshold: Float::INFINITY, timeout_ms: 250, yield_on_error: false, &block)
|
624
|
+
raise NotImplementedError, <<~ERROR
|
625
|
+
`each_batch` has been removed due to data consistency concerns.
|
626
|
+
|
627
|
+
This method was removed because it did not properly handle partition reassignments,
|
628
|
+
which could lead to processing messages from partitions that were no longer owned
|
629
|
+
by this consumer, resulting in duplicate message processing and data inconsistencies.
|
630
|
+
|
631
|
+
Recommended alternatives:
|
632
|
+
|
633
|
+
1. Implement your own batching logic using rebalance callbacks to properly handle
|
634
|
+
partition revocations and ensure message processing correctness.
|
635
|
+
|
636
|
+
2. Use a high-level batching library that supports proper partition reassignment
|
637
|
+
handling out of the box (such as the Karafka framework).
|
638
|
+
ERROR
|
639
|
+
end
|
640
|
+
|
641
|
+
# Returns pointer to the consumer group metadata. It is used only in the context of
|
642
|
+
# exactly-once-semantics in transactions, this is why it is never remapped to Ruby
|
643
|
+
#
|
644
|
+
# This API is **not** usable by itself from Ruby
|
645
|
+
#
|
646
|
+
# @note This pointer **needs** to be removed with `#rd_kafka_consumer_group_metadata_destroy`
|
647
|
+
#
|
648
|
+
# @private
|
649
|
+
def consumer_group_metadata_pointer
|
650
|
+
closed_consumer_check(__method__)
|
651
|
+
|
652
|
+
@native_kafka.with_inner do |inner|
|
653
|
+
Bindings.rd_kafka_consumer_group_metadata(inner)
|
654
|
+
end
|
655
|
+
end
|
656
|
+
|
657
|
+
private
|
658
|
+
|
659
|
+
def closed_consumer_check(method)
|
660
|
+
raise Rdkafka::ClosedConsumerError.new(method) if closed?
|
661
|
+
end
|
662
|
+
end
|
663
|
+
end
|