kafka 0.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/.gitignore +14 -0
- data/.rubocop.yml +210 -0
- data/.travis.yml +45 -0
- data/CHANGELOG.md +3 -0
- data/CODE_OF_CONDUCT.md +74 -0
- data/Gemfile +5 -0
- data/LICENSE.txt +21 -0
- data/README.md +182 -0
- data/Rakefile +69 -0
- data/examples/consumer.rb +55 -0
- data/examples/producer.rb +46 -0
- data/ext/Rakefile +69 -0
- data/kafka.gemspec +39 -0
- data/lib/kafka/admin.rb +141 -0
- data/lib/kafka/config.rb +145 -0
- data/lib/kafka/consumer.rb +87 -0
- data/lib/kafka/error.rb +44 -0
- data/lib/kafka/ffi/admin/admin_options.rb +121 -0
- data/lib/kafka/ffi/admin/config_entry.rb +97 -0
- data/lib/kafka/ffi/admin/config_resource.rb +101 -0
- data/lib/kafka/ffi/admin/delete_topic.rb +19 -0
- data/lib/kafka/ffi/admin/new_partitions.rb +77 -0
- data/lib/kafka/ffi/admin/new_topic.rb +91 -0
- data/lib/kafka/ffi/admin/result.rb +66 -0
- data/lib/kafka/ffi/admin/topic_result.rb +32 -0
- data/lib/kafka/ffi/admin.rb +16 -0
- data/lib/kafka/ffi/broker_metadata.rb +32 -0
- data/lib/kafka/ffi/client.rb +640 -0
- data/lib/kafka/ffi/config.rb +382 -0
- data/lib/kafka/ffi/consumer.rb +342 -0
- data/lib/kafka/ffi/error.rb +25 -0
- data/lib/kafka/ffi/event.rb +215 -0
- data/lib/kafka/ffi/group_info.rb +75 -0
- data/lib/kafka/ffi/group_list.rb +27 -0
- data/lib/kafka/ffi/group_member_info.rb +52 -0
- data/lib/kafka/ffi/message/header.rb +205 -0
- data/lib/kafka/ffi/message.rb +205 -0
- data/lib/kafka/ffi/metadata.rb +58 -0
- data/lib/kafka/ffi/opaque.rb +81 -0
- data/lib/kafka/ffi/opaque_pointer.rb +73 -0
- data/lib/kafka/ffi/partition_metadata.rb +61 -0
- data/lib/kafka/ffi/producer.rb +144 -0
- data/lib/kafka/ffi/queue.rb +65 -0
- data/lib/kafka/ffi/topic.rb +32 -0
- data/lib/kafka/ffi/topic_config.rb +126 -0
- data/lib/kafka/ffi/topic_metadata.rb +42 -0
- data/lib/kafka/ffi/topic_partition.rb +43 -0
- data/lib/kafka/ffi/topic_partition_list.rb +167 -0
- data/lib/kafka/ffi.rb +624 -0
- data/lib/kafka/poller.rb +28 -0
- data/lib/kafka/producer/delivery_report.rb +120 -0
- data/lib/kafka/producer.rb +127 -0
- data/lib/kafka/version.rb +8 -0
- data/lib/kafka.rb +11 -0
- metadata +159 -0
@@ -0,0 +1,61 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Kafka::FFI
|
4
|
+
class PartitionMetadata < ::FFI::Struct
|
5
|
+
layout(
|
6
|
+
:id, :int32,
|
7
|
+
:err, :error_code,
|
8
|
+
:leader, :int32,
|
9
|
+
:replica_cnt, :int,
|
10
|
+
:replicas, :pointer, # *int32_t
|
11
|
+
:isr_cnt, :int,
|
12
|
+
:isrs, :pointer # *int32_t
|
13
|
+
)
|
14
|
+
|
15
|
+
# Returns the Partition's ID
|
16
|
+
#
|
17
|
+
# @return [Integer] Partition ID
|
18
|
+
def id
|
19
|
+
self[:id]
|
20
|
+
end
|
21
|
+
|
22
|
+
# Returns the error for the Partition as reported by the Broker.
|
23
|
+
#
|
24
|
+
# @return [nil] No error reported by Broker
|
25
|
+
# @return [Kafka::ResponseError] Error reported by Broker
|
26
|
+
def error
|
27
|
+
if self[:err] != :ok
|
28
|
+
return ::Kafka::ResponseError.new(self[:err])
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
# ID of the Leader Broker for this Partition
|
33
|
+
#
|
34
|
+
# @return [Integer] Leader Broker ID
|
35
|
+
def leader
|
36
|
+
self[:leader]
|
37
|
+
end
|
38
|
+
|
39
|
+
# Returns the Broker IDs of the Brokers with replicas of this Partition.
|
40
|
+
#
|
41
|
+
# @return [Array<Integer>] IDs for Brokers with replicas
|
42
|
+
def replicas
|
43
|
+
if self[:replica_cnt] == 0 || self[:replicas].null?
|
44
|
+
return []
|
45
|
+
end
|
46
|
+
|
47
|
+
self[:replicas].read_array_of_int32(self[:replica_cnt])
|
48
|
+
end
|
49
|
+
|
50
|
+
# Returns the Broker IDs of the in-sync replicas for this Partition.
|
51
|
+
#
|
52
|
+
# @return [Array<Integer>] IDs of Brokers that have in-sync replicas.
|
53
|
+
def in_sync_replicas
|
54
|
+
if self[:isr_cnt] == 0 || self[:isrs].null?
|
55
|
+
return []
|
56
|
+
end
|
57
|
+
|
58
|
+
self[:isrs].read_array_of_int32(self[:isr_cnt])
|
59
|
+
end
|
60
|
+
end
|
61
|
+
end
|
@@ -0,0 +1,144 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "ffi"
|
4
|
+
require "kafka/ffi/client"
|
5
|
+
|
6
|
+
module Kafka::FFI
|
7
|
+
class Producer < Kafka::FFI::Client
|
8
|
+
def self.new(config = nil)
|
9
|
+
super(:producer, config)
|
10
|
+
end
|
11
|
+
|
12
|
+
# Produce and send a single message to the Kafka cluster.
|
13
|
+
#
|
14
|
+
# @param topic [Topic, String] Topic (or name of topic) to receive the
|
15
|
+
# message.
|
16
|
+
#
|
17
|
+
# @param payload [String, nil] Content of the message.
|
18
|
+
#
|
19
|
+
# @param key [String] Message partitioning key
|
20
|
+
#
|
21
|
+
# @param partition [nil, -1] Use the configured partitioner to determine
|
22
|
+
# which partition to publish the message to.
|
23
|
+
# @param partition [Integer] Partition of the topic that should receive the
|
24
|
+
# message.
|
25
|
+
#
|
26
|
+
# @param headers [Kafka::FFI::Message::Header]
|
27
|
+
#
|
28
|
+
# @param timestamp [Time] Timestamp as Time
|
29
|
+
# @param timestamp [Integer] Timestamp as milliseconds since unix epoch
|
30
|
+
# @param timestamp [nil] Timestamp is assigned by librdkafka
|
31
|
+
#
|
32
|
+
# @param opaque [Opaque] Reference to an object owned by the application
|
33
|
+
# which will be available as Message#opaque in callbacks. The application
|
34
|
+
# MUST call #free on the Opaque once the final callback has been
|
35
|
+
# triggered to avoid leaking memory.
|
36
|
+
def produce(topic, payload, key: nil, partition: nil, headers: nil, timestamp: nil, opaque: nil)
|
37
|
+
args = [
|
38
|
+
# Ensure librdkafka copies the payload into its own memory since the
|
39
|
+
# string backing it could be garbage collected.
|
40
|
+
:vtype, :msgflags, :int, Kafka::FFI::RD_KAFKA_MSG_F_COPY,
|
41
|
+
]
|
42
|
+
|
43
|
+
if payload
|
44
|
+
args.append(:vtype, :value, :buffer_in, payload, :size_t, payload.bytesize)
|
45
|
+
end
|
46
|
+
|
47
|
+
# The partitioning key is optional
|
48
|
+
if key
|
49
|
+
args.append(:vtype, :key, :buffer_in, key, :size_t, key.bytesize)
|
50
|
+
end
|
51
|
+
|
52
|
+
# Partition will default to being auto assigned by the configured
|
53
|
+
# partitioning strategy.
|
54
|
+
if partition
|
55
|
+
args.append(:vtype, :partition, :int32, partition)
|
56
|
+
end
|
57
|
+
|
58
|
+
# Headers are optional and can be passed as either a reference to a
|
59
|
+
# Header object or individual key/value pairs. This only supports the
|
60
|
+
# Header object because supporting key + valu
|
61
|
+
if headers
|
62
|
+
args.append(:vtype, :headers, :pointer, headers.pointer)
|
63
|
+
end
|
64
|
+
|
65
|
+
case topic
|
66
|
+
when Topic
|
67
|
+
args.append(:vtype, :rkt, :pointer, topic.pointer)
|
68
|
+
when String
|
69
|
+
args.append(:vtype, :topic, :string, topic)
|
70
|
+
else
|
71
|
+
raise ArgumentError, "topic must be either a Topic or String"
|
72
|
+
end
|
73
|
+
|
74
|
+
if opaque
|
75
|
+
args.append(:vtype, :opaque, :pointer, opaque.pointer)
|
76
|
+
end
|
77
|
+
|
78
|
+
if timestamp
|
79
|
+
ts =
|
80
|
+
case timestamp
|
81
|
+
when Time then ((timestamp.to_i * 1000) + (timestamp.nsec / 1000))
|
82
|
+
when Integer then timestamp
|
83
|
+
else
|
84
|
+
raise ArgumentError, "timestamp must be nil, a Time, or an Integer"
|
85
|
+
end
|
86
|
+
|
87
|
+
args.append(:vtype, :timestamp, :int64, ts)
|
88
|
+
end
|
89
|
+
|
90
|
+
# Add the sentinel value to denote the end of the argument list.
|
91
|
+
args.append(:vtype, :end)
|
92
|
+
|
93
|
+
err = ::Kafka::FFI.rd_kafka_producev(self, *args)
|
94
|
+
if err != :ok
|
95
|
+
# The only documented error is RD_KAFKA_RESP_ERR__CONFLICT should both
|
96
|
+
# HEADER and HEADERS keys be passed in. There is no way for HEADER to
|
97
|
+
# be passed to producev based on the above implementation.
|
98
|
+
raise ::Kafka::ResponseError, err
|
99
|
+
end
|
100
|
+
|
101
|
+
nil
|
102
|
+
end
|
103
|
+
|
104
|
+
# Wait until all outstanding produce requests are completed. This should
|
105
|
+
# typically be done prior to destroying a producer to ensure all queued and
|
106
|
+
# in-flight requests are completed before terminating.
|
107
|
+
#
|
108
|
+
# @raise [Kafka::ResponseError] Timeout was reached before all
|
109
|
+
# outstanding requests were completed.
|
110
|
+
def flush(timeout: 1000)
|
111
|
+
err = ::Kafka::FFI.rd_kafka_flush(self, timeout)
|
112
|
+
if err != :ok
|
113
|
+
raise ::Kafka::ResponseError, err
|
114
|
+
end
|
115
|
+
|
116
|
+
nil
|
117
|
+
end
|
118
|
+
|
119
|
+
# Purge messages currently handled by the Producer. By default this will
|
120
|
+
# purge all queued and inflight messages asyncronously.
|
121
|
+
#
|
122
|
+
# @param queued [Boolean] Purge any queued messages
|
123
|
+
# @param inflight [Boolean] Purge messages that are inflight
|
124
|
+
# @param blocking [Boolean] When true don't wait for background thread
|
125
|
+
# queue purging to finish.
|
126
|
+
#
|
127
|
+
# @raise [Kafka::ResponseError] Error occurred purging state. This is
|
128
|
+
# unlikely as the documented error are not possible with this
|
129
|
+
# implementation.
|
130
|
+
def purge(queued: true, inflight: true, blocking: false)
|
131
|
+
mask = 0
|
132
|
+
mask |= RD_KAFKA_PURGE_F_QUEUE if queued
|
133
|
+
mask |= RD_KAFKA_PURGE_F_INFLIGHT if inflight
|
134
|
+
mask |= RD_KAFKA_PURGE_F_NON_BLOCKING if blocking
|
135
|
+
|
136
|
+
err = ::Kafka::FFI.rd_kafka_purge(self, mask)
|
137
|
+
if err != :ok
|
138
|
+
raise ::Kafka::ResponseError, err
|
139
|
+
end
|
140
|
+
|
141
|
+
nil
|
142
|
+
end
|
143
|
+
end
|
144
|
+
end
|
@@ -0,0 +1,65 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "ffi"
|
4
|
+
require "kafka/ffi/opaque_pointer"
|
5
|
+
|
6
|
+
module Kafka::FFI
|
7
|
+
class Queue < OpaquePointer
|
8
|
+
def self.new(client)
|
9
|
+
::Kafka::FFI.rd_kafka_queue_new(client)
|
10
|
+
end
|
11
|
+
|
12
|
+
# Poll a queue for an event, waiting up to timeout milliseconds. Takes an
|
13
|
+
# optional block which will handle destroying the event at the completion
|
14
|
+
# of the block.
|
15
|
+
#
|
16
|
+
# @param timeout [Integer] Max time to wait in millseconds for an Event.
|
17
|
+
#
|
18
|
+
# @yield [event]
|
19
|
+
# @yieldparam event [Event] Polled event
|
20
|
+
#
|
21
|
+
# @return [nil] No event was available within the timeout
|
22
|
+
# @return [Event] Event polled from the queue, application is responsible
|
23
|
+
# for calling #destroy on the Event when finished with it.
|
24
|
+
# @return When passed a block, the result returned by the block
|
25
|
+
def poll(timeout: 1000)
|
26
|
+
event = ::Kafka::FFI.rd_kafka_queue_poll(self, timeout)
|
27
|
+
if event.nil?
|
28
|
+
return nil
|
29
|
+
end
|
30
|
+
|
31
|
+
if block_given?
|
32
|
+
begin
|
33
|
+
yield(event)
|
34
|
+
ensure
|
35
|
+
event.destroy
|
36
|
+
end
|
37
|
+
else
|
38
|
+
event
|
39
|
+
end
|
40
|
+
end
|
41
|
+
|
42
|
+
# Forward events meant for this Queue to the destination Queue instead.
|
43
|
+
#
|
44
|
+
# @param dest [Queue] Destination queue to forward
|
45
|
+
# @param dest [nil] Remove forwarding for this queue.
|
46
|
+
def forward(dest)
|
47
|
+
::Kafka::FFI.rd_kafka_queue_forward(self, dest)
|
48
|
+
end
|
49
|
+
|
50
|
+
# Retrieve the current number of elemens in the queue.
|
51
|
+
#
|
52
|
+
# @return [Integer] Number of elements in the queue
|
53
|
+
def length
|
54
|
+
::Kafka::FFI.rd_kafka_queue_length(self)
|
55
|
+
end
|
56
|
+
|
57
|
+
# Release the applications reference on the queue, possibly destroying it
|
58
|
+
# and releasing it's resources.
|
59
|
+
def destroy
|
60
|
+
if !pointer.null?
|
61
|
+
::Kafka::FFI.rd_kafka_queue_destroy(self)
|
62
|
+
end
|
63
|
+
end
|
64
|
+
end
|
65
|
+
end
|
@@ -0,0 +1,32 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "kafka/ffi/opaque_pointer"
|
4
|
+
|
5
|
+
module Kafka::FFI
|
6
|
+
class Topic < OpaquePointer
|
7
|
+
# Retrieve the name of the topic
|
8
|
+
#
|
9
|
+
# @return [String] Name of the topic
|
10
|
+
def name
|
11
|
+
::Kafka::FFI.rd_kafka_topic_name(self)
|
12
|
+
end
|
13
|
+
|
14
|
+
# Seek consumer for topic_partition to offset.
|
15
|
+
#
|
16
|
+
# @param partition [Integer] Partition to set offset for
|
17
|
+
# @param offset [Integer] Absolute or logical offset
|
18
|
+
# @param timeout [Integer] Maximum time to wait in milliseconds
|
19
|
+
#
|
20
|
+
# @return [Boolean] True when the consumer's offset was changed
|
21
|
+
def seek(partition, offset, timeout: 1000)
|
22
|
+
::Kafka::FFI.rd_kafka_seek(self, partition, offset, timeout)
|
23
|
+
end
|
24
|
+
|
25
|
+
# Release the application's hold on the backing topic in librdkafka.
|
26
|
+
def destroy
|
27
|
+
if !pointer.null?
|
28
|
+
::Kafka::FFI.rd_kafka_topic_destroy(self)
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
@@ -0,0 +1,126 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "ffi"
|
4
|
+
require "kafka/ffi/opaque_pointer"
|
5
|
+
|
6
|
+
module Kafka::FFI
|
7
|
+
# TopicConfig can be passed to Topic.new to configure how the client
|
8
|
+
# interacts with the Topic.
|
9
|
+
class TopicConfig < OpaquePointer
|
10
|
+
def self.new
|
11
|
+
Kafka::FFI.rd_kafka_topic_conf_new
|
12
|
+
end
|
13
|
+
|
14
|
+
# Set the config option at `key` to `value`. The configuration options
|
15
|
+
# match those used by librdkafka (and the Java client).
|
16
|
+
#
|
17
|
+
# @see https://github.com/edenhill/librdkafka/blob/master/CONFIGURATION.md
|
18
|
+
#
|
19
|
+
# @param key [String] Configuration key
|
20
|
+
# @param value [String] Value to set
|
21
|
+
#
|
22
|
+
# @raise [Kafka::FFI::UnknownConfigKey]
|
23
|
+
# @raise [Kafka::FFI::InvalidConfigValue]
|
24
|
+
def set(key, value)
|
25
|
+
key = key.to_s
|
26
|
+
value = value.to_s
|
27
|
+
|
28
|
+
error = ::FFI::MemoryPointer.new(:char, 512)
|
29
|
+
result = ::Kafka::FFI.rd_kafka_topic_conf_set(self, key, value, error, error.size)
|
30
|
+
|
31
|
+
# See config_result enum in ffi.rb
|
32
|
+
case result
|
33
|
+
when :ok
|
34
|
+
nil
|
35
|
+
when :unknown
|
36
|
+
raise Kafka::FFI::UnknownConfigKey.new(key, value, error.read_string)
|
37
|
+
when :invalid
|
38
|
+
raise Kafka::FFI::InvalidConfigValue.new(key, value, error.read_string)
|
39
|
+
end
|
40
|
+
ensure
|
41
|
+
error.free if error
|
42
|
+
end
|
43
|
+
|
44
|
+
# Get the current config value for the given key.
|
45
|
+
#
|
46
|
+
# @param key [String] Config key to fetch the setting for.
|
47
|
+
#
|
48
|
+
# @return [String, :unknown] Value for the key or :unknown if not already
|
49
|
+
# set.
|
50
|
+
def get(key)
|
51
|
+
key = key.to_s
|
52
|
+
|
53
|
+
# Will contain the size of the value at key
|
54
|
+
size = ::FFI::MemoryPointer.new(:size_t)
|
55
|
+
|
56
|
+
# Make an initial request for the size of buffer we need to allocate.
|
57
|
+
# When trying to make a guess at the potential size the code would often
|
58
|
+
# segfault due to rd_kafka_conf_get reallocating the buffer.
|
59
|
+
err = ::Kafka::FFI.rd_kafka_topic_conf_get(self, key, ::FFI::Pointer::NULL, size)
|
60
|
+
if err != :ok
|
61
|
+
return err
|
62
|
+
end
|
63
|
+
|
64
|
+
# Allocate a string long enough to contain the whole value.
|
65
|
+
value = ::FFI::MemoryPointer.new(:char, size.read(:size_t))
|
66
|
+
err = ::Kafka::FFI.rd_kafka_topic_conf_get(self, key, value, size)
|
67
|
+
if err != :ok
|
68
|
+
return err
|
69
|
+
end
|
70
|
+
|
71
|
+
value.read_string
|
72
|
+
ensure
|
73
|
+
size.free if size
|
74
|
+
value.free if value
|
75
|
+
end
|
76
|
+
|
77
|
+
# Duplicate the current config
|
78
|
+
#
|
79
|
+
# @return [TopicConfig] Duplicated config
|
80
|
+
def dup
|
81
|
+
::Kafka::FFI.rd_kafka_topic_conf_dup(self)
|
82
|
+
end
|
83
|
+
|
84
|
+
# Sets a custom partitioner callback that is called for each message to
|
85
|
+
# determine which partition to publish the message to.
|
86
|
+
#
|
87
|
+
# @example Random partitioner
|
88
|
+
# set_partitioner_cb do |_topic, _key, parts|
|
89
|
+
# rand(parts)
|
90
|
+
# end
|
91
|
+
#
|
92
|
+
# @see "partitioner" config option for predefined strategies
|
93
|
+
#
|
94
|
+
# @yield [topic, key, partition_count]
|
95
|
+
# @yieldparam topic [Topic] Topic the message is being published to
|
96
|
+
# @yieldparam key [String] Partitioning key provided when publishing
|
97
|
+
# @yieldparam partition_count [Integer] Number of partitions the topic has
|
98
|
+
# @yieldreturn [Integer] The partition to publish the message to
|
99
|
+
def set_partitioner_cb
|
100
|
+
if !block_given?
|
101
|
+
raise ArgumentError, "set_partitioner_cb must be called with a block"
|
102
|
+
end
|
103
|
+
|
104
|
+
# @todo How do we guarantee the block does not get garbage collected?
|
105
|
+
# @todo Support opaque pointers?
|
106
|
+
cb = ::FFI::Function.new(:int, [:pointer, :string, :size_t, :int32, :pointer, :pointer]) do |topic, key, _, partitions, _, _|
|
107
|
+
topic = Topic.new(topic)
|
108
|
+
|
109
|
+
yield(topic, key, partitions)
|
110
|
+
end
|
111
|
+
|
112
|
+
::Kafka::FFI.rd_kafka_topic_conf_set_partitioner_cb(self, cb)
|
113
|
+
end
|
114
|
+
|
115
|
+
# Free all resources used by the topic config.
|
116
|
+
#
|
117
|
+
# @note Never call #destroy on a Config that has been passed to
|
118
|
+
# Kafka::FFI.rd_kafka_topic_new since the handle will take ownership of
|
119
|
+
# the config.
|
120
|
+
def destroy
|
121
|
+
if !pointer.null?
|
122
|
+
::Kafka::FFI.rd_kafka_topic_conf_destroy(self)
|
123
|
+
end
|
124
|
+
end
|
125
|
+
end
|
126
|
+
end
|
@@ -0,0 +1,42 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Kafka::FFI
|
4
|
+
class TopicMetadata < ::FFI::Struct
|
5
|
+
layout(
|
6
|
+
:topic, :string,
|
7
|
+
:partition_cnt, :int,
|
8
|
+
:partitions, :pointer, # *rd_kafka_metadata_partition
|
9
|
+
:err, :error_code
|
10
|
+
)
|
11
|
+
|
12
|
+
# Returns the name of the topic
|
13
|
+
#
|
14
|
+
# @return [String] Name of the topic
|
15
|
+
def topic
|
16
|
+
self[:topic]
|
17
|
+
end
|
18
|
+
alias name topic
|
19
|
+
|
20
|
+
# Returns any Broker reported errors.
|
21
|
+
#
|
22
|
+
# @return [nil] Broker reported no errors for the topic
|
23
|
+
# @return [Kafka::ResponseError] Error reported by Broker
|
24
|
+
def error
|
25
|
+
if self[:err] != :ok
|
26
|
+
return ::Kafka::ResponseError.new(self[:err])
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
# Returns the set of PartitionMetadata for the Topic
|
31
|
+
#
|
32
|
+
# @return [Array<PartitionMetadata>] Details about individual Topic
|
33
|
+
# Partitions.
|
34
|
+
def partitions
|
35
|
+
ptr = self[:partitions]
|
36
|
+
|
37
|
+
self[:partition_cnt].times.map do |i|
|
38
|
+
PartitionMetadata.new(ptr + (i * PartitionMetadata.size))
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
@@ -0,0 +1,43 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "ffi"
|
4
|
+
|
5
|
+
module Kafka::FFI
|
6
|
+
class TopicPartition < ::FFI::Struct
|
7
|
+
layout(
|
8
|
+
:topic, :string,
|
9
|
+
:partition, :int32,
|
10
|
+
:offset, :int64,
|
11
|
+
:metadata, :pointer,
|
12
|
+
:metadata_size, :size_t,
|
13
|
+
:opaque, :pointer,
|
14
|
+
:err, :error_code,
|
15
|
+
:_private, :pointer # DO NOT TOUCH. Internal to librdkafka
|
16
|
+
)
|
17
|
+
|
18
|
+
# @return [nil] The TopicPartition does not have an error set
|
19
|
+
# @return [Kafka::ResponseError] Error for this topic occurred related to
|
20
|
+
# the action the TopicPartition (or TopicPartitionList) was passed to.
|
21
|
+
def error
|
22
|
+
if self[:err] != :ok
|
23
|
+
::Kafka::ResponseError.new(self[:err])
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
# @return [String] Name of the topic
|
28
|
+
def topic
|
29
|
+
self[:topic]
|
30
|
+
end
|
31
|
+
|
32
|
+
# @return [Integer] Partition number
|
33
|
+
def partition
|
34
|
+
self[:partition]
|
35
|
+
end
|
36
|
+
|
37
|
+
# @return [Integer] Known offset for the consumer group for topic +
|
38
|
+
# partition.
|
39
|
+
def offset
|
40
|
+
self[:offset]
|
41
|
+
end
|
42
|
+
end
|
43
|
+
end
|
@@ -0,0 +1,167 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "ffi"
|
4
|
+
require "kafka/ffi/topic_partition"
|
5
|
+
|
6
|
+
module Kafka::FFI
|
7
|
+
class TopicPartitionList < ::FFI::Struct
|
8
|
+
layout(
|
9
|
+
:cnt, :int,
|
10
|
+
:size, :int,
|
11
|
+
:elems, :pointer
|
12
|
+
)
|
13
|
+
|
14
|
+
# New initializes a new TopicPartitionList with an initial capacity to hold
|
15
|
+
# `count` items.
|
16
|
+
#
|
17
|
+
# @param count [Integer] Initial capacity
|
18
|
+
#
|
19
|
+
# @return [TopicPartitionList] An empty TopicPartitionList
|
20
|
+
def self.new(count = 0)
|
21
|
+
# Handle initialization through FFI. This will be called by
|
22
|
+
# rd_kafka_topic_partition_list_new.
|
23
|
+
if count.is_a?(::FFI::Pointer)
|
24
|
+
return super(count)
|
25
|
+
end
|
26
|
+
|
27
|
+
::Kafka::FFI.rd_kafka_topic_partition_list_new(count)
|
28
|
+
end
|
29
|
+
|
30
|
+
# Returns the number of elements in the TopicPartitionList.
|
31
|
+
#
|
32
|
+
# @return [Integer] Number of elements
|
33
|
+
def size
|
34
|
+
self[:cnt]
|
35
|
+
end
|
36
|
+
|
37
|
+
# Returns true when the TopicPartitionList is empty
|
38
|
+
#
|
39
|
+
# @return [Boolean] True when the list is empty
|
40
|
+
def empty?
|
41
|
+
size == 0
|
42
|
+
end
|
43
|
+
|
44
|
+
# Add a topic + partition combination to the list
|
45
|
+
#
|
46
|
+
# @param topic [String] Name of the topic to add
|
47
|
+
# @param partition [Integer] Partition of the topic to add to the list.
|
48
|
+
# @param partition [-1] Add all partitions of the topic to the list.
|
49
|
+
#
|
50
|
+
# @return [TopicPartition] TopicPartition for the combination
|
51
|
+
def add(topic, partition = -1)
|
52
|
+
::Kafka::FFI.rd_kafka_topic_partition_list_add(self, topic.to_s, partition)
|
53
|
+
end
|
54
|
+
|
55
|
+
# Add a range of TopicPartitions to the list.
|
56
|
+
#
|
57
|
+
# @param topic [String] Name of the topic to add
|
58
|
+
# @param range_or_lower [Range, Integer] Either a Range specifying the
|
59
|
+
# Range of partitions or the lower bound for the range. When providing a
|
60
|
+
# Range any value for upper is ignored.
|
61
|
+
# @param upper [Integer, nil] The upper bound of the set of partitions
|
62
|
+
# (inclusive). Required unless range_or_lower is a Range.
|
63
|
+
def add_range(topic, range_or_lower, upper = nil)
|
64
|
+
lower = range_or_lower
|
65
|
+
|
66
|
+
# Allows passing a Range for convenience.
|
67
|
+
if range_or_lower.is_a?(Range)
|
68
|
+
lower = range_or_lower.min
|
69
|
+
upper = range_or_lower.max
|
70
|
+
elsif upper.nil?
|
71
|
+
raise ArgumentError, "upper was nil but must be provided when lower is not a Range"
|
72
|
+
end
|
73
|
+
|
74
|
+
::Kafka::FFI.rd_kafka_topic_partition_list_add_range(self, topic.to_s, lower.to_i, upper.to_i)
|
75
|
+
end
|
76
|
+
|
77
|
+
# Remove a TopicPartition by partition
|
78
|
+
#
|
79
|
+
# @param topic [String] Name of the topic to remove
|
80
|
+
# @param partition [Integer] Partition to remove
|
81
|
+
#
|
82
|
+
# @return [Boolean] True when the partition was found and removed
|
83
|
+
def del(topic, partition)
|
84
|
+
::Kafka::FFI.rd_kafka_topic_partition_list_del(self, topic.to_s, partition) == 1
|
85
|
+
end
|
86
|
+
|
87
|
+
# Remove a TopicPartition by index
|
88
|
+
#
|
89
|
+
# @param idx [Integer] Index in elements to remove
|
90
|
+
#
|
91
|
+
# @return [Boolean] True when the TopicPartition was found and removed
|
92
|
+
def del_by_idx(idx)
|
93
|
+
::Kafka::FFI.rd_kafka_topic_partition_list_del_by_idx(self, idx) == 1
|
94
|
+
end
|
95
|
+
|
96
|
+
alias delete del
|
97
|
+
alias delete_by_index del_by_idx
|
98
|
+
|
99
|
+
# Duplicate the TopicPartitionList as a new TopicPartitionList that is
|
100
|
+
# identical to the current one.
|
101
|
+
#
|
102
|
+
# @return [TopicPartitionList] New clone of this TopicPartitionList
|
103
|
+
def copy
|
104
|
+
::Kafka::FFI.rd_kafka_topic_partition_list_copy(self)
|
105
|
+
end
|
106
|
+
|
107
|
+
# Set the consumed offset for topic and partition
|
108
|
+
#
|
109
|
+
# @param topic [String] Name of the topic to set the offset for
|
110
|
+
# @param partition [Integer] Partition to set the offset for
|
111
|
+
# @param offset [Integer] Offset of the topic+partition to set
|
112
|
+
#
|
113
|
+
# @return [Integer] 0 for success otherwise rd_kafka_resp_err_t code
|
114
|
+
def set_offset(topic, partition, offset)
|
115
|
+
::Kafka::FFI.rd_kafka_topic_partition_list_set_offset(self, topic, partition, offset)
|
116
|
+
end
|
117
|
+
|
118
|
+
# Sort the TopicPartitionList. Sort can take a block that should implement
|
119
|
+
# a standard comparison function that returns -1, 0, or 1 depending on if
|
120
|
+
# left is less than, equal to, or greater than the right argument.
|
121
|
+
#
|
122
|
+
# @example Custom sorting function
|
123
|
+
# sort do |left, right|
|
124
|
+
# left.partition <=> right.partition
|
125
|
+
# end
|
126
|
+
def sort(&block)
|
127
|
+
::Kafka::FFI.rd_kafka_topic_partition_list_sort(self, block, nil)
|
128
|
+
end
|
129
|
+
|
130
|
+
# Find the TopicPartition in the set for the given topic + partition. Will
|
131
|
+
# return nil if the list does not include the combination.
|
132
|
+
#
|
133
|
+
# @param topic [String] Name of the topic
|
134
|
+
# @param partition [Integer] Topic partition
|
135
|
+
#
|
136
|
+
# @return [TopicPartition, nil] The TopicPartion for the topic + partition
|
137
|
+
# combination.
|
138
|
+
def find(topic, partition)
|
139
|
+
result = ::Kafka::FFI.rd_kafka_topic_partition_list_find(self, topic, partition)
|
140
|
+
|
141
|
+
if result.null?
|
142
|
+
return nil
|
143
|
+
end
|
144
|
+
|
145
|
+
result
|
146
|
+
end
|
147
|
+
|
148
|
+
# Retrieves the set of TopicPartitions for the list.
|
149
|
+
#
|
150
|
+
# @return [Array<TopicPartition>]
|
151
|
+
def elements
|
152
|
+
self[:cnt].times.map do |i|
|
153
|
+
TopicPartition.new(self[:elems] + (i * TopicPartition.size))
|
154
|
+
end
|
155
|
+
end
|
156
|
+
|
157
|
+
# Free all resources used by the list and the list itself. Usage it
|
158
|
+
# dependent on the semantics of librdkafka, so make sure to only call on
|
159
|
+
# TopicPartitionLists that are not owned by objects. Generally, if you
|
160
|
+
# constructed the object it should be safe to destroy.
|
161
|
+
def destroy
|
162
|
+
if !null?
|
163
|
+
::Kafka::FFI.rd_kafka_topic_partition_list_destroy(self)
|
164
|
+
end
|
165
|
+
end
|
166
|
+
end
|
167
|
+
end
|