karafka-rdkafka 0.12.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- checksums.yaml.gz.sig +2 -0
- data/.gitignore +8 -0
- data/.rspec +1 -0
- data/.semaphore/semaphore.yml +23 -0
- data/.yardopts +2 -0
- data/CHANGELOG.md +104 -0
- data/Gemfile +3 -0
- data/Guardfile +19 -0
- data/LICENSE +21 -0
- data/README.md +114 -0
- data/Rakefile +96 -0
- data/bin/console +11 -0
- data/docker-compose.yml +24 -0
- data/ext/README.md +18 -0
- data/ext/Rakefile +62 -0
- data/lib/rdkafka/abstract_handle.rb +82 -0
- data/lib/rdkafka/admin/create_topic_handle.rb +27 -0
- data/lib/rdkafka/admin/create_topic_report.rb +22 -0
- data/lib/rdkafka/admin/delete_topic_handle.rb +27 -0
- data/lib/rdkafka/admin/delete_topic_report.rb +22 -0
- data/lib/rdkafka/admin.rb +155 -0
- data/lib/rdkafka/bindings.rb +312 -0
- data/lib/rdkafka/callbacks.rb +106 -0
- data/lib/rdkafka/config.rb +299 -0
- data/lib/rdkafka/consumer/headers.rb +63 -0
- data/lib/rdkafka/consumer/message.rb +84 -0
- data/lib/rdkafka/consumer/partition.rb +49 -0
- data/lib/rdkafka/consumer/topic_partition_list.rb +164 -0
- data/lib/rdkafka/consumer.rb +565 -0
- data/lib/rdkafka/error.rb +86 -0
- data/lib/rdkafka/metadata.rb +92 -0
- data/lib/rdkafka/producer/client.rb +47 -0
- data/lib/rdkafka/producer/delivery_handle.rb +22 -0
- data/lib/rdkafka/producer/delivery_report.rb +26 -0
- data/lib/rdkafka/producer.rb +178 -0
- data/lib/rdkafka/version.rb +5 -0
- data/lib/rdkafka.rb +22 -0
- data/rdkafka.gemspec +36 -0
- data/spec/rdkafka/abstract_handle_spec.rb +113 -0
- data/spec/rdkafka/admin/create_topic_handle_spec.rb +52 -0
- data/spec/rdkafka/admin/create_topic_report_spec.rb +16 -0
- data/spec/rdkafka/admin/delete_topic_handle_spec.rb +52 -0
- data/spec/rdkafka/admin/delete_topic_report_spec.rb +16 -0
- data/spec/rdkafka/admin_spec.rb +203 -0
- data/spec/rdkafka/bindings_spec.rb +134 -0
- data/spec/rdkafka/callbacks_spec.rb +20 -0
- data/spec/rdkafka/config_spec.rb +182 -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 +223 -0
- data/spec/rdkafka/consumer_spec.rb +1008 -0
- data/spec/rdkafka/error_spec.rb +89 -0
- data/spec/rdkafka/metadata_spec.rb +78 -0
- data/spec/rdkafka/producer/client_spec.rb +145 -0
- data/spec/rdkafka/producer/delivery_handle_spec.rb +42 -0
- data/spec/rdkafka/producer/delivery_report_spec.rb +17 -0
- data/spec/rdkafka/producer_spec.rb +525 -0
- data/spec/spec_helper.rb +139 -0
- data.tar.gz.sig +0 -0
- metadata +277 -0
- metadata.gz.sig +0 -0
@@ -0,0 +1,299 @@
|
|
1
|
+
require "logger"
|
2
|
+
|
3
|
+
module Rdkafka
|
4
|
+
# Configuration for a Kafka consumer or producer. You can create an instance and use
|
5
|
+
# the consumer and producer methods to create a client. Documentation of the available
|
6
|
+
# configuration options is available on https://github.com/edenhill/librdkafka/blob/master/CONFIGURATION.md.
|
7
|
+
class Config
|
8
|
+
# @private
|
9
|
+
@@logger = Logger.new(STDOUT)
|
10
|
+
# @private
|
11
|
+
@@statistics_callback = nil
|
12
|
+
# @private
|
13
|
+
@@error_callback = nil
|
14
|
+
# @private
|
15
|
+
@@opaques = {}
|
16
|
+
# @private
|
17
|
+
@@log_queue = Queue.new
|
18
|
+
|
19
|
+
Thread.start do
|
20
|
+
loop do
|
21
|
+
severity, msg = @@log_queue.pop
|
22
|
+
@@logger.add(severity, msg)
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
# Returns the current logger, by default this is a logger to stdout.
|
27
|
+
#
|
28
|
+
# @return [Logger]
|
29
|
+
def self.logger
|
30
|
+
@@logger
|
31
|
+
end
|
32
|
+
|
33
|
+
|
34
|
+
# Returns a queue whose contents will be passed to the configured logger. Each entry
|
35
|
+
# should follow the format [Logger::Severity, String]. The benefit over calling the
|
36
|
+
# logger directly is that this is safe to use from trap contexts.
|
37
|
+
#
|
38
|
+
# @return [Queue]
|
39
|
+
def self.log_queue
|
40
|
+
@@log_queue
|
41
|
+
end
|
42
|
+
|
43
|
+
# Set the logger that will be used for all logging output by this library.
|
44
|
+
#
|
45
|
+
# @param logger [Logger] The logger to be used
|
46
|
+
#
|
47
|
+
# @return [nil]
|
48
|
+
def self.logger=(logger)
|
49
|
+
raise NoLoggerError if logger.nil?
|
50
|
+
@@logger=logger
|
51
|
+
end
|
52
|
+
|
53
|
+
# Set a callback that will be called every time the underlying client emits statistics.
|
54
|
+
# You can configure if and how often this happens using `statistics.interval.ms`.
|
55
|
+
# The callback is called with a hash that's documented here: https://github.com/edenhill/librdkafka/blob/master/STATISTICS.md
|
56
|
+
#
|
57
|
+
# @param callback [Proc, #call] The callback
|
58
|
+
#
|
59
|
+
# @return [nil]
|
60
|
+
def self.statistics_callback=(callback)
|
61
|
+
raise TypeError.new("Callback has to be callable") unless callback.respond_to?(:call)
|
62
|
+
@@statistics_callback = callback
|
63
|
+
end
|
64
|
+
|
65
|
+
# Returns the current statistics callback, by default this is nil.
|
66
|
+
#
|
67
|
+
# @return [Proc, nil]
|
68
|
+
def self.statistics_callback
|
69
|
+
@@statistics_callback
|
70
|
+
end
|
71
|
+
|
72
|
+
# Set a callback that will be called every time the underlying client emits an error.
|
73
|
+
# If this callback is not set, global errors such as brokers becoming unavailable will only be sent to the logger, as defined by librdkafka.
|
74
|
+
# The callback is called with an instance of RdKafka::Error.
|
75
|
+
#
|
76
|
+
# @param callback [Proc, #call] The callback
|
77
|
+
#
|
78
|
+
# @return [nil]
|
79
|
+
def self.error_callback=(callback)
|
80
|
+
raise TypeError.new("Callback has to be callable") unless callback.respond_to?(:call)
|
81
|
+
@@error_callback = callback
|
82
|
+
end
|
83
|
+
|
84
|
+
# Returns the current error callback, by default this is nil.
|
85
|
+
#
|
86
|
+
# @return [Proc, nil]
|
87
|
+
def self.error_callback
|
88
|
+
@@error_callback
|
89
|
+
end
|
90
|
+
|
91
|
+
# @private
|
92
|
+
def self.opaques
|
93
|
+
@@opaques
|
94
|
+
end
|
95
|
+
|
96
|
+
# Default config that can be overwritten.
|
97
|
+
DEFAULT_CONFIG = {
|
98
|
+
# Request api version so advanced features work
|
99
|
+
:"api.version.request" => true
|
100
|
+
}.freeze
|
101
|
+
|
102
|
+
# Required config that cannot be overwritten.
|
103
|
+
REQUIRED_CONFIG = {
|
104
|
+
# Enable log queues so we get callbacks in our own Ruby threads
|
105
|
+
:"log.queue" => true
|
106
|
+
}.freeze
|
107
|
+
|
108
|
+
# Returns a new config with the provided options which are merged with {DEFAULT_CONFIG}.
|
109
|
+
#
|
110
|
+
# @param config_hash [Hash{String,Symbol => String}] The config options for rdkafka
|
111
|
+
#
|
112
|
+
# @return [Config]
|
113
|
+
def initialize(config_hash = {})
|
114
|
+
@config_hash = DEFAULT_CONFIG.merge(config_hash)
|
115
|
+
@consumer_rebalance_listener = nil
|
116
|
+
end
|
117
|
+
|
118
|
+
# Set a config option.
|
119
|
+
#
|
120
|
+
# @param key [String] The config option's key
|
121
|
+
# @param value [String] The config option's value
|
122
|
+
#
|
123
|
+
# @return [nil]
|
124
|
+
def []=(key, value)
|
125
|
+
@config_hash[key] = value
|
126
|
+
end
|
127
|
+
|
128
|
+
# Get a config option with the specified key
|
129
|
+
#
|
130
|
+
# @param key [String] The config option's key
|
131
|
+
#
|
132
|
+
# @return [String, nil] The config option or `nil` if it is not present
|
133
|
+
def [](key)
|
134
|
+
@config_hash[key]
|
135
|
+
end
|
136
|
+
|
137
|
+
# Get notifications on partition assignment/revocation for the subscribed topics
|
138
|
+
#
|
139
|
+
# @param listener [Object, #on_partitions_assigned, #on_partitions_revoked] listener instance
|
140
|
+
def consumer_rebalance_listener=(listener)
|
141
|
+
@consumer_rebalance_listener = listener
|
142
|
+
end
|
143
|
+
|
144
|
+
# Create a consumer with this configuration.
|
145
|
+
#
|
146
|
+
# @raise [ConfigError] When the configuration contains invalid options
|
147
|
+
# @raise [ClientCreationError] When the native client cannot be created
|
148
|
+
#
|
149
|
+
# @return [Consumer] The created consumer
|
150
|
+
def consumer
|
151
|
+
opaque = Opaque.new
|
152
|
+
config = native_config(opaque)
|
153
|
+
|
154
|
+
if @consumer_rebalance_listener
|
155
|
+
opaque.consumer_rebalance_listener = @consumer_rebalance_listener
|
156
|
+
Rdkafka::Bindings.rd_kafka_conf_set_rebalance_cb(config, Rdkafka::Bindings::RebalanceCallback)
|
157
|
+
end
|
158
|
+
|
159
|
+
kafka = native_kafka(config, :rd_kafka_consumer)
|
160
|
+
|
161
|
+
# Redirect the main queue to the consumer
|
162
|
+
Rdkafka::Bindings.rd_kafka_poll_set_consumer(kafka)
|
163
|
+
|
164
|
+
# Return consumer with Kafka client
|
165
|
+
Rdkafka::Consumer.new(kafka)
|
166
|
+
end
|
167
|
+
|
168
|
+
# Create a producer with this configuration.
|
169
|
+
#
|
170
|
+
# @raise [ConfigError] When the configuration contains invalid options
|
171
|
+
# @raise [ClientCreationError] When the native client cannot be created
|
172
|
+
#
|
173
|
+
# @return [Producer] The created producer
|
174
|
+
def producer
|
175
|
+
# Create opaque
|
176
|
+
opaque = Opaque.new
|
177
|
+
# Create Kafka config
|
178
|
+
config = native_config(opaque)
|
179
|
+
# Set callback to receive delivery reports on config
|
180
|
+
Rdkafka::Bindings.rd_kafka_conf_set_dr_msg_cb(config, Rdkafka::Callbacks::DeliveryCallbackFunction)
|
181
|
+
# Return producer with Kafka client
|
182
|
+
Rdkafka::Producer.new(Rdkafka::Producer::Client.new(native_kafka(config, :rd_kafka_producer)), self[:partitioner]).tap do |producer|
|
183
|
+
opaque.producer = producer
|
184
|
+
end
|
185
|
+
end
|
186
|
+
|
187
|
+
# Create an admin instance with this configuration.
|
188
|
+
#
|
189
|
+
# @raise [ConfigError] When the configuration contains invalid options
|
190
|
+
# @raise [ClientCreationError] When the native client cannot be created
|
191
|
+
#
|
192
|
+
# @return [Admin] The created admin instance
|
193
|
+
def admin
|
194
|
+
opaque = Opaque.new
|
195
|
+
config = native_config(opaque)
|
196
|
+
Rdkafka::Bindings.rd_kafka_conf_set_background_event_cb(config, Rdkafka::Callbacks::BackgroundEventCallbackFunction)
|
197
|
+
Rdkafka::Admin.new(native_kafka(config, :rd_kafka_producer))
|
198
|
+
end
|
199
|
+
|
200
|
+
# Error that is returned by the underlying rdkafka error if an invalid configuration option is present.
|
201
|
+
class ConfigError < RuntimeError; end
|
202
|
+
|
203
|
+
# Error that is returned by the underlying rdkafka library if the client cannot be created.
|
204
|
+
class ClientCreationError < RuntimeError; end
|
205
|
+
|
206
|
+
# Error that is raised when trying to set a nil logger
|
207
|
+
class NoLoggerError < RuntimeError; end
|
208
|
+
|
209
|
+
private
|
210
|
+
|
211
|
+
# This method is only intended to be used to create a client,
|
212
|
+
# using it in another way will leak memory.
|
213
|
+
def native_config(opaque=nil)
|
214
|
+
Rdkafka::Bindings.rd_kafka_conf_new.tap do |config|
|
215
|
+
# Create config
|
216
|
+
@config_hash.merge(REQUIRED_CONFIG).each do |key, value|
|
217
|
+
error_buffer = FFI::MemoryPointer.from_string(" " * 256)
|
218
|
+
result = Rdkafka::Bindings.rd_kafka_conf_set(
|
219
|
+
config,
|
220
|
+
key.to_s,
|
221
|
+
value.to_s,
|
222
|
+
error_buffer,
|
223
|
+
256
|
224
|
+
)
|
225
|
+
unless result == :config_ok
|
226
|
+
raise ConfigError.new(error_buffer.read_string)
|
227
|
+
end
|
228
|
+
end
|
229
|
+
|
230
|
+
# Set opaque pointer that's used as a proxy for callbacks
|
231
|
+
if opaque
|
232
|
+
pointer = ::FFI::Pointer.new(:pointer, opaque.object_id)
|
233
|
+
Rdkafka::Bindings.rd_kafka_conf_set_opaque(config, pointer)
|
234
|
+
|
235
|
+
# Store opaque with the pointer as key. We use this approach instead
|
236
|
+
# of trying to convert the pointer to a Ruby object because there is
|
237
|
+
# no risk of a segfault this way.
|
238
|
+
Rdkafka::Config.opaques[pointer.to_i] = opaque
|
239
|
+
end
|
240
|
+
|
241
|
+
# Set log callback
|
242
|
+
Rdkafka::Bindings.rd_kafka_conf_set_log_cb(config, Rdkafka::Bindings::LogCallback)
|
243
|
+
|
244
|
+
# Set stats callback
|
245
|
+
Rdkafka::Bindings.rd_kafka_conf_set_stats_cb(config, Rdkafka::Bindings::StatsCallback)
|
246
|
+
|
247
|
+
# Set error callback
|
248
|
+
Rdkafka::Bindings.rd_kafka_conf_set_error_cb(config, Rdkafka::Bindings::ErrorCallback)
|
249
|
+
end
|
250
|
+
end
|
251
|
+
|
252
|
+
def native_kafka(config, type)
|
253
|
+
error_buffer = FFI::MemoryPointer.from_string(" " * 256)
|
254
|
+
handle = Rdkafka::Bindings.rd_kafka_new(
|
255
|
+
type,
|
256
|
+
config,
|
257
|
+
error_buffer,
|
258
|
+
256
|
259
|
+
)
|
260
|
+
|
261
|
+
if handle.null?
|
262
|
+
raise ClientCreationError.new(error_buffer.read_string)
|
263
|
+
end
|
264
|
+
|
265
|
+
# Redirect log to handle's queue
|
266
|
+
Rdkafka::Bindings.rd_kafka_set_log_queue(
|
267
|
+
handle,
|
268
|
+
Rdkafka::Bindings.rd_kafka_queue_get_main(handle)
|
269
|
+
)
|
270
|
+
|
271
|
+
# Return handle which should be closed using rd_kafka_destroy after usage.
|
272
|
+
handle
|
273
|
+
end
|
274
|
+
end
|
275
|
+
|
276
|
+
# @private
|
277
|
+
class Opaque
|
278
|
+
attr_accessor :producer
|
279
|
+
attr_accessor :consumer_rebalance_listener
|
280
|
+
|
281
|
+
def call_delivery_callback(delivery_report, delivery_handle)
|
282
|
+
producer.call_delivery_callback(delivery_report, delivery_handle) if producer
|
283
|
+
end
|
284
|
+
|
285
|
+
def call_on_partitions_assigned(consumer, list)
|
286
|
+
return unless consumer_rebalance_listener
|
287
|
+
return unless consumer_rebalance_listener.respond_to?(:on_partitions_assigned)
|
288
|
+
|
289
|
+
consumer_rebalance_listener.on_partitions_assigned(consumer, list)
|
290
|
+
end
|
291
|
+
|
292
|
+
def call_on_partitions_revoked(consumer, list)
|
293
|
+
return unless consumer_rebalance_listener
|
294
|
+
return unless consumer_rebalance_listener.respond_to?(:on_partitions_revoked)
|
295
|
+
|
296
|
+
consumer_rebalance_listener.on_partitions_revoked(consumer, list)
|
297
|
+
end
|
298
|
+
end
|
299
|
+
end
|
@@ -0,0 +1,63 @@
|
|
1
|
+
module Rdkafka
|
2
|
+
class Consumer
|
3
|
+
# A message headers
|
4
|
+
class Headers
|
5
|
+
# Reads a native kafka's message header into ruby's hash
|
6
|
+
#
|
7
|
+
# @return [Hash<String, String>] a message headers
|
8
|
+
#
|
9
|
+
# @raise [Rdkafka::RdkafkaError] when fail to read headers
|
10
|
+
#
|
11
|
+
# @private
|
12
|
+
def self.from_native(native_message)
|
13
|
+
headers_ptrptr = FFI::MemoryPointer.new(:pointer)
|
14
|
+
err = Rdkafka::Bindings.rd_kafka_message_headers(native_message, headers_ptrptr)
|
15
|
+
|
16
|
+
if err == Rdkafka::Bindings::RD_KAFKA_RESP_ERR__NOENT
|
17
|
+
return {}
|
18
|
+
elsif err != Rdkafka::Bindings::RD_KAFKA_RESP_ERR_NO_ERROR
|
19
|
+
raise Rdkafka::RdkafkaError.new(err, "Error reading message headers")
|
20
|
+
end
|
21
|
+
|
22
|
+
headers_ptr = headers_ptrptr.read_pointer
|
23
|
+
|
24
|
+
name_ptrptr = FFI::MemoryPointer.new(:pointer)
|
25
|
+
value_ptrptr = FFI::MemoryPointer.new(:pointer)
|
26
|
+
size_ptr = Rdkafka::Bindings::SizePtr.new
|
27
|
+
headers = {}
|
28
|
+
|
29
|
+
idx = 0
|
30
|
+
loop do
|
31
|
+
err = Rdkafka::Bindings.rd_kafka_header_get_all(
|
32
|
+
headers_ptr,
|
33
|
+
idx,
|
34
|
+
name_ptrptr,
|
35
|
+
value_ptrptr,
|
36
|
+
size_ptr
|
37
|
+
)
|
38
|
+
|
39
|
+
if err == Rdkafka::Bindings::RD_KAFKA_RESP_ERR__NOENT
|
40
|
+
break
|
41
|
+
elsif err != Rdkafka::Bindings::RD_KAFKA_RESP_ERR_NO_ERROR
|
42
|
+
raise Rdkafka::RdkafkaError.new(err, "Error reading a message header at index #{idx}")
|
43
|
+
end
|
44
|
+
|
45
|
+
name_ptr = name_ptrptr.read_pointer
|
46
|
+
name = name_ptr.respond_to?(:read_string_to_null) ? name_ptr.read_string_to_null : name_ptr.read_string
|
47
|
+
|
48
|
+
size = size_ptr[:value]
|
49
|
+
|
50
|
+
value_ptr = value_ptrptr.read_pointer
|
51
|
+
|
52
|
+
value = value_ptr.read_string(size)
|
53
|
+
|
54
|
+
headers[name.to_sym] = value
|
55
|
+
|
56
|
+
idx += 1
|
57
|
+
end
|
58
|
+
|
59
|
+
headers
|
60
|
+
end
|
61
|
+
end
|
62
|
+
end
|
63
|
+
end
|
@@ -0,0 +1,84 @@
|
|
1
|
+
module Rdkafka
|
2
|
+
class Consumer
|
3
|
+
# A message that was consumed from a topic.
|
4
|
+
class Message
|
5
|
+
# The topic this message was consumed from
|
6
|
+
# @return [String]
|
7
|
+
attr_reader :topic
|
8
|
+
|
9
|
+
# The partition this message was consumed from
|
10
|
+
# @return [Integer]
|
11
|
+
attr_reader :partition
|
12
|
+
|
13
|
+
# This message's payload
|
14
|
+
# @return [String, nil]
|
15
|
+
attr_reader :payload
|
16
|
+
|
17
|
+
# This message's key
|
18
|
+
# @return [String, nil]
|
19
|
+
attr_reader :key
|
20
|
+
|
21
|
+
# This message's offset in it's partition
|
22
|
+
# @return [Integer]
|
23
|
+
attr_reader :offset
|
24
|
+
|
25
|
+
# This message's timestamp, if provided by the broker
|
26
|
+
# @return [Time, nil]
|
27
|
+
attr_reader :timestamp
|
28
|
+
|
29
|
+
# @return [Hash<String, String>] a message headers
|
30
|
+
attr_reader :headers
|
31
|
+
|
32
|
+
# @private
|
33
|
+
def initialize(native_message)
|
34
|
+
# Set topic
|
35
|
+
unless native_message[:rkt].null?
|
36
|
+
@topic = Rdkafka::Bindings.rd_kafka_topic_name(native_message[:rkt])
|
37
|
+
end
|
38
|
+
# Set partition
|
39
|
+
@partition = native_message[:partition]
|
40
|
+
# Set payload
|
41
|
+
unless native_message[:payload].null?
|
42
|
+
@payload = native_message[:payload].read_string(native_message[:len])
|
43
|
+
end
|
44
|
+
# Set key
|
45
|
+
unless native_message[:key].null?
|
46
|
+
@key = native_message[:key].read_string(native_message[:key_len])
|
47
|
+
end
|
48
|
+
# Set offset
|
49
|
+
@offset = native_message[:offset]
|
50
|
+
# Set timestamp
|
51
|
+
raw_timestamp = Rdkafka::Bindings.rd_kafka_message_timestamp(native_message, nil)
|
52
|
+
@timestamp = if raw_timestamp && raw_timestamp > -1
|
53
|
+
# Calculate seconds and microseconds
|
54
|
+
seconds = raw_timestamp / 1000
|
55
|
+
milliseconds = (raw_timestamp - seconds * 1000) * 1000
|
56
|
+
Time.at(seconds, milliseconds)
|
57
|
+
else
|
58
|
+
nil
|
59
|
+
end
|
60
|
+
|
61
|
+
@headers = Headers.from_native(native_message)
|
62
|
+
end
|
63
|
+
|
64
|
+
# Human readable representation of this message.
|
65
|
+
# @return [String]
|
66
|
+
def to_s
|
67
|
+
is_headers = @headers.empty? ? "" : ", headers #{headers.size}"
|
68
|
+
|
69
|
+
"<Message in '#{topic}' with key '#{truncate(key)}', payload '#{truncate(payload)}', partition #{partition}, offset #{offset}, timestamp #{timestamp}#{is_headers}>"
|
70
|
+
end
|
71
|
+
|
72
|
+
def truncate(string)
|
73
|
+
if string && string.length > 40
|
74
|
+
"#{string[0..39]}..."
|
75
|
+
else
|
76
|
+
string
|
77
|
+
end
|
78
|
+
end
|
79
|
+
|
80
|
+
private
|
81
|
+
|
82
|
+
end
|
83
|
+
end
|
84
|
+
end
|
@@ -0,0 +1,49 @@
|
|
1
|
+
module Rdkafka
|
2
|
+
class Consumer
|
3
|
+
# Information about a partition, used in {TopicPartitionList}.
|
4
|
+
class Partition
|
5
|
+
# Partition number
|
6
|
+
# @return [Integer]
|
7
|
+
attr_reader :partition
|
8
|
+
|
9
|
+
# Partition's offset
|
10
|
+
# @return [Integer, nil]
|
11
|
+
attr_reader :offset
|
12
|
+
|
13
|
+
# Partition's error code
|
14
|
+
# @return [Integer]
|
15
|
+
attr_reader :err
|
16
|
+
|
17
|
+
# @private
|
18
|
+
def initialize(partition, offset, err = 0)
|
19
|
+
@partition = partition
|
20
|
+
@offset = offset
|
21
|
+
@err = err
|
22
|
+
end
|
23
|
+
|
24
|
+
# Human readable representation of this partition.
|
25
|
+
# @return [String]
|
26
|
+
def to_s
|
27
|
+
message = "<Partition #{partition}"
|
28
|
+
message += " offset=#{offset}" if offset
|
29
|
+
message += " err=#{err}" if err != 0
|
30
|
+
message += ">"
|
31
|
+
message
|
32
|
+
end
|
33
|
+
|
34
|
+
# Human readable representation of this partition.
|
35
|
+
# @return [String]
|
36
|
+
def inspect
|
37
|
+
to_s
|
38
|
+
end
|
39
|
+
|
40
|
+
# Whether another partition is equal to this
|
41
|
+
# @return [Boolean]
|
42
|
+
def ==(other)
|
43
|
+
self.class == other.class &&
|
44
|
+
self.partition == other.partition &&
|
45
|
+
self.offset == other.offset
|
46
|
+
end
|
47
|
+
end
|
48
|
+
end
|
49
|
+
end
|
@@ -0,0 +1,164 @@
|
|
1
|
+
module Rdkafka
|
2
|
+
class Consumer
|
3
|
+
# A list of topics with their partition information
|
4
|
+
class TopicPartitionList
|
5
|
+
# Create a topic partition list.
|
6
|
+
#
|
7
|
+
# @param data [Hash{String => nil,Partition}] The topic and partition data or nil to create an empty list
|
8
|
+
#
|
9
|
+
# @return [TopicPartitionList]
|
10
|
+
def initialize(data=nil)
|
11
|
+
@data = data || {}
|
12
|
+
end
|
13
|
+
|
14
|
+
# Number of items in the list
|
15
|
+
# @return [Integer]
|
16
|
+
def count
|
17
|
+
i = 0
|
18
|
+
@data.each do |_topic, partitions|
|
19
|
+
if partitions
|
20
|
+
i += partitions.count
|
21
|
+
else
|
22
|
+
i+= 1
|
23
|
+
end
|
24
|
+
end
|
25
|
+
i
|
26
|
+
end
|
27
|
+
|
28
|
+
# Whether this list is empty
|
29
|
+
# @return [Boolean]
|
30
|
+
def empty?
|
31
|
+
@data.empty?
|
32
|
+
end
|
33
|
+
|
34
|
+
# Add a topic with optionally partitions to the list.
|
35
|
+
# Calling this method multiple times for the same topic will overwrite the previous configuraton.
|
36
|
+
#
|
37
|
+
# @example Add a topic with unassigned partitions
|
38
|
+
# tpl.add_topic("topic")
|
39
|
+
#
|
40
|
+
# @example Add a topic with assigned partitions
|
41
|
+
# tpl.add_topic("topic", (0..8))
|
42
|
+
#
|
43
|
+
# @example Add a topic with all topics up to a count
|
44
|
+
# tpl.add_topic("topic", 9)
|
45
|
+
#
|
46
|
+
# @param topic [String] The topic's name
|
47
|
+
# @param partitions [Array<Integer>, Range<Integer>, Integer] The topic's partitions or partition count
|
48
|
+
#
|
49
|
+
# @return [nil]
|
50
|
+
def add_topic(topic, partitions=nil)
|
51
|
+
if partitions.nil?
|
52
|
+
@data[topic.to_s] = nil
|
53
|
+
else
|
54
|
+
if partitions.is_a? Integer
|
55
|
+
partitions = (0..partitions - 1)
|
56
|
+
end
|
57
|
+
@data[topic.to_s] = partitions.map { |p| Partition.new(p, nil, 0) }
|
58
|
+
end
|
59
|
+
end
|
60
|
+
|
61
|
+
# Add a topic with partitions and offsets set to the list
|
62
|
+
# Calling this method multiple times for the same topic will overwrite the previous configuraton.
|
63
|
+
#
|
64
|
+
# @param topic [String] The topic's name
|
65
|
+
# @param partitions_with_offsets [Hash<Integer, Integer>] The topic's partitions and offsets
|
66
|
+
#
|
67
|
+
# @return [nil]
|
68
|
+
def add_topic_and_partitions_with_offsets(topic, partitions_with_offsets)
|
69
|
+
@data[topic.to_s] = partitions_with_offsets.map { |p, o| Partition.new(p, o) }
|
70
|
+
end
|
71
|
+
|
72
|
+
# Return a `Hash` with the topics as keys and and an array of partition information as the value if present.
|
73
|
+
#
|
74
|
+
# @return [Hash{String => Array<Partition>,nil}]
|
75
|
+
def to_h
|
76
|
+
@data
|
77
|
+
end
|
78
|
+
|
79
|
+
# Human readable representation of this list.
|
80
|
+
# @return [String]
|
81
|
+
def to_s
|
82
|
+
"<TopicPartitionList: #{to_h}>"
|
83
|
+
end
|
84
|
+
|
85
|
+
def ==(other)
|
86
|
+
self.to_h == other.to_h
|
87
|
+
end
|
88
|
+
|
89
|
+
# Create a new topic partition list based of a native one.
|
90
|
+
#
|
91
|
+
# @param pointer [FFI::Pointer] Optional pointer to an existing native list. Its contents will be copied.
|
92
|
+
#
|
93
|
+
# @return [TopicPartitionList]
|
94
|
+
#
|
95
|
+
# @private
|
96
|
+
def self.from_native_tpl(pointer)
|
97
|
+
# Data to be moved into the tpl
|
98
|
+
data = {}
|
99
|
+
|
100
|
+
# Create struct and copy its contents
|
101
|
+
native_tpl = Rdkafka::Bindings::TopicPartitionList.new(pointer)
|
102
|
+
native_tpl[:cnt].times do |i|
|
103
|
+
ptr = native_tpl[:elems] + (i * Rdkafka::Bindings::TopicPartition.size)
|
104
|
+
elem = Rdkafka::Bindings::TopicPartition.new(ptr)
|
105
|
+
if elem[:partition] == -1
|
106
|
+
data[elem[:topic]] = nil
|
107
|
+
else
|
108
|
+
partitions = data[elem[:topic]] || []
|
109
|
+
offset = if elem[:offset] == Rdkafka::Bindings::RD_KAFKA_OFFSET_INVALID
|
110
|
+
nil
|
111
|
+
else
|
112
|
+
elem[:offset]
|
113
|
+
end
|
114
|
+
partition = Partition.new(elem[:partition], offset, elem[:err])
|
115
|
+
partitions.push(partition)
|
116
|
+
data[elem[:topic]] = partitions
|
117
|
+
end
|
118
|
+
end
|
119
|
+
|
120
|
+
# Return the created object
|
121
|
+
TopicPartitionList.new(data)
|
122
|
+
end
|
123
|
+
|
124
|
+
# Create a native tpl with the contents of this object added.
|
125
|
+
#
|
126
|
+
# The pointer will be cleaned by `rd_kafka_topic_partition_list_destroy` when GC releases it.
|
127
|
+
#
|
128
|
+
# @return [FFI::Pointer]
|
129
|
+
# @private
|
130
|
+
def to_native_tpl
|
131
|
+
tpl = Rdkafka::Bindings.rd_kafka_topic_partition_list_new(count)
|
132
|
+
|
133
|
+
@data.each do |topic, partitions|
|
134
|
+
if partitions
|
135
|
+
partitions.each do |p|
|
136
|
+
Rdkafka::Bindings.rd_kafka_topic_partition_list_add(
|
137
|
+
tpl,
|
138
|
+
topic,
|
139
|
+
p.partition
|
140
|
+
)
|
141
|
+
|
142
|
+
if p.offset
|
143
|
+
Rdkafka::Bindings.rd_kafka_topic_partition_list_set_offset(
|
144
|
+
tpl,
|
145
|
+
topic,
|
146
|
+
p.partition,
|
147
|
+
p.offset
|
148
|
+
)
|
149
|
+
end
|
150
|
+
end
|
151
|
+
else
|
152
|
+
Rdkafka::Bindings.rd_kafka_topic_partition_list_add(
|
153
|
+
tpl,
|
154
|
+
topic,
|
155
|
+
-1
|
156
|
+
)
|
157
|
+
end
|
158
|
+
end
|
159
|
+
|
160
|
+
tpl
|
161
|
+
end
|
162
|
+
end
|
163
|
+
end
|
164
|
+
end
|