kafka 0.5.0
Sign up to get free protection for your applications and to get access to all the features.
- 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,52 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "ffi"
|
4
|
+
|
5
|
+
module Kafka::FFI
|
6
|
+
class GroupMemberInfo < ::FFI::Struct
|
7
|
+
layout(
|
8
|
+
:member_id, :string,
|
9
|
+
:client_id, :string,
|
10
|
+
:client_host, :string,
|
11
|
+
:member_metadata, :pointer,
|
12
|
+
:member_metadata_size, :int,
|
13
|
+
:member_assignment, :pointer,
|
14
|
+
:member_assignment_size, :int
|
15
|
+
)
|
16
|
+
|
17
|
+
# Returns the broker generated member id for the consumer.
|
18
|
+
#
|
19
|
+
# @return [String] Member ID
|
20
|
+
def member_id
|
21
|
+
self[:member_id]
|
22
|
+
end
|
23
|
+
|
24
|
+
# Returns the consumer's client.id config setting
|
25
|
+
#
|
26
|
+
# @return [String] Client ID
|
27
|
+
def client_id
|
28
|
+
self[:client_id]
|
29
|
+
end
|
30
|
+
|
31
|
+
# Returns the hostname of the consumer
|
32
|
+
#
|
33
|
+
# @return [String] Consumer's hostname
|
34
|
+
def client_host
|
35
|
+
self[:client_host]
|
36
|
+
end
|
37
|
+
|
38
|
+
# Returns the binary metadata for the consumer
|
39
|
+
#
|
40
|
+
# @return [String] Consumer metadata
|
41
|
+
def member_metadata
|
42
|
+
self[:member_metadata].read_string(self[:member_metadata_size])
|
43
|
+
end
|
44
|
+
|
45
|
+
# Returns the binary assignment data for the consumer
|
46
|
+
#
|
47
|
+
# @return [String] Assignments
|
48
|
+
def member_assignment
|
49
|
+
self[:member_assignment].read_string(self[:member_assignment_size])
|
50
|
+
end
|
51
|
+
end
|
52
|
+
end
|
@@ -0,0 +1,205 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "ffi"
|
4
|
+
require "kafka/ffi/opaque_pointer"
|
5
|
+
|
6
|
+
module Kafka::FFI
|
7
|
+
class Message::Header < OpaquePointer
|
8
|
+
def self.new(count = 0)
|
9
|
+
if count.is_a?(::FFI::Pointer)
|
10
|
+
return super(count)
|
11
|
+
end
|
12
|
+
|
13
|
+
::Kafka::FFI.rd_kafka_headers_new(count)
|
14
|
+
end
|
15
|
+
|
16
|
+
# Count returns the number of headers in the set.
|
17
|
+
#
|
18
|
+
# @return [Integer] Number of headers
|
19
|
+
def count
|
20
|
+
::Kafka::FFI.rd_kafka_header_cnt(self)
|
21
|
+
end
|
22
|
+
alias size count
|
23
|
+
alias length count
|
24
|
+
|
25
|
+
# Add header with name and value.
|
26
|
+
#
|
27
|
+
# @param name [String] Header key name
|
28
|
+
# @param value [#to_s, nil] Header key value
|
29
|
+
#
|
30
|
+
# @raise [Kafka::ResponseError] Error that occurred adding the header
|
31
|
+
def add(name, value)
|
32
|
+
name = name.to_s
|
33
|
+
|
34
|
+
value_size = 0
|
35
|
+
if value
|
36
|
+
value = value.to_s
|
37
|
+
value_size = value.bytesize
|
38
|
+
end
|
39
|
+
|
40
|
+
err = ::Kafka::FFI.rd_kafka_header_add(self, name, name.length, value, value_size)
|
41
|
+
if err != :ok
|
42
|
+
raise ::Kafka::ResponseError, err
|
43
|
+
end
|
44
|
+
|
45
|
+
nil
|
46
|
+
end
|
47
|
+
|
48
|
+
# Make a copy of the headers list
|
49
|
+
#
|
50
|
+
# @return [Header] Copy of the headers
|
51
|
+
def copy
|
52
|
+
::Kafka::FFI.rd_kafka_headers_copy(self)
|
53
|
+
end
|
54
|
+
|
55
|
+
# Remove all headers with the given name.
|
56
|
+
#
|
57
|
+
# @param name [String] Header key name to remove
|
58
|
+
#
|
59
|
+
# @raise [Kafka::ResponseError] Error that occurred removing the header
|
60
|
+
# @raise [Kafka::ResponseError<RD_KAFKA_RESP_ERR__READ_ONLY>] Header is
|
61
|
+
# read only.
|
62
|
+
def remove(name)
|
63
|
+
name = name.to_s
|
64
|
+
|
65
|
+
err = ::Kafka::FFI.rd_kafka_header_remove(self, name)
|
66
|
+
case err
|
67
|
+
when :ok
|
68
|
+
nil
|
69
|
+
when ::Kafka::FFI::RD_KAFKA_RESP_ERR__NOENT
|
70
|
+
# Header field does not exist. Just return nil since the effect (key
|
71
|
+
# doesn't exist) is the same.
|
72
|
+
nil
|
73
|
+
else
|
74
|
+
raise ::Kafka::ResponseError, err
|
75
|
+
end
|
76
|
+
end
|
77
|
+
|
78
|
+
# Retrieve all headers that match the given name
|
79
|
+
#
|
80
|
+
# @param name [String] Header key name
|
81
|
+
#
|
82
|
+
# @raise [Kafka::ResponseError] Error that occurred retrieving the header
|
83
|
+
# values
|
84
|
+
#
|
85
|
+
# @return [Array<String, nil>] List of values for the header
|
86
|
+
def get(name)
|
87
|
+
name = name.to_s
|
88
|
+
|
89
|
+
idx = 0
|
90
|
+
values = []
|
91
|
+
|
92
|
+
value = ::FFI::MemoryPointer.new(:pointer)
|
93
|
+
size = ::FFI::MemoryPointer.new(:pointer)
|
94
|
+
|
95
|
+
loop do
|
96
|
+
err = ::Kafka::FFI.rd_kafka_header_get(self, idx, name, value, size)
|
97
|
+
|
98
|
+
case err
|
99
|
+
when :ok
|
100
|
+
# Read the returned value and add it to the result list.
|
101
|
+
idx += 1
|
102
|
+
ptr = value.read_pointer
|
103
|
+
|
104
|
+
values << (ptr.null? ? nil : ptr.read_string(size.read(:size_t)))
|
105
|
+
when ::Kafka::FFI::RD_KAFKA_RESP_ERR__NOENT
|
106
|
+
# Reached the end of the list of values so break and return the set
|
107
|
+
# of found values.
|
108
|
+
break
|
109
|
+
else
|
110
|
+
raise ::Kafka::ResponseError, err
|
111
|
+
end
|
112
|
+
end
|
113
|
+
|
114
|
+
values
|
115
|
+
ensure
|
116
|
+
value.free if value
|
117
|
+
size.free if size
|
118
|
+
end
|
119
|
+
|
120
|
+
# rubocop:disable Naming/AccessorMethodName
|
121
|
+
|
122
|
+
# Retrieve all of the headers and their values
|
123
|
+
#
|
124
|
+
# @raise [Kafka::ResponseError] Error if occurred retrieving the headers.
|
125
|
+
#
|
126
|
+
# @return [Hash<String, Array<String>>] Set of header keys and their values
|
127
|
+
# @return [Hash{}] Header is empty
|
128
|
+
def get_all
|
129
|
+
name = ::FFI::MemoryPointer.new(:pointer)
|
130
|
+
value = ::FFI::MemoryPointer.new(:pointer)
|
131
|
+
size = ::FFI::MemoryPointer.new(:pointer)
|
132
|
+
|
133
|
+
idx = 0
|
134
|
+
result = {}
|
135
|
+
|
136
|
+
loop do
|
137
|
+
err = ::Kafka::FFI.rd_kafka_header_get_all(self, idx, name, value, size)
|
138
|
+
|
139
|
+
case err
|
140
|
+
when :ok
|
141
|
+
# Read the returned value and add it to the result list.
|
142
|
+
idx += 1
|
143
|
+
|
144
|
+
key = name.read_pointer.read_string
|
145
|
+
val = value.read_pointer
|
146
|
+
|
147
|
+
result[key] ||= []
|
148
|
+
result[key] << (val.null? ? nil : val.read_string(size.read(:size_t)))
|
149
|
+
when ::Kafka::FFI::RD_KAFKA_RESP_ERR__NOENT
|
150
|
+
# Reached the end of the list of values so break and return the set
|
151
|
+
# of found values.
|
152
|
+
break
|
153
|
+
else
|
154
|
+
raise ::Kafka::ResponseError, err
|
155
|
+
end
|
156
|
+
end
|
157
|
+
|
158
|
+
result
|
159
|
+
ensure
|
160
|
+
name.free if name
|
161
|
+
value.free if value
|
162
|
+
size.free if size
|
163
|
+
end
|
164
|
+
alias to_hash get_all
|
165
|
+
alias to_h get_all
|
166
|
+
# rubocop:enable Naming/AccessorMethodName
|
167
|
+
|
168
|
+
# Find the last header in the list that matches the given name.
|
169
|
+
#
|
170
|
+
# @param name [String] Header key name
|
171
|
+
#
|
172
|
+
# @raise [Kafka::ResponseError] Error that occurred retrieving the header
|
173
|
+
# value
|
174
|
+
#
|
175
|
+
# @return [String] Value of the last matching header with name
|
176
|
+
# @return [nil] No header with that name exists
|
177
|
+
def get_last(name)
|
178
|
+
name = name.to_s
|
179
|
+
value = ::FFI::MemoryPointer.new(:pointer)
|
180
|
+
size = ::FFI::MemoryPointer.new(:pointer)
|
181
|
+
|
182
|
+
err = ::Kafka::FFI.rd_kafka_header_get_last(self, name, value, size)
|
183
|
+
if err != :ok
|
184
|
+
# No header with that name exists so just return nil
|
185
|
+
if err == ::Kafka::FFI::RD_KAFKA_RESP_ERR__NOENT
|
186
|
+
return nil
|
187
|
+
end
|
188
|
+
|
189
|
+
raise ::Kafka::ResponseError, err
|
190
|
+
end
|
191
|
+
|
192
|
+
ptr = value.read_pointer
|
193
|
+
ptr.null? ? nil : ptr.read_string(size.read(:size_t))
|
194
|
+
ensure
|
195
|
+
value.free
|
196
|
+
size.free
|
197
|
+
end
|
198
|
+
|
199
|
+
def destroy
|
200
|
+
if !pointer.null?
|
201
|
+
::Kafka::FFI.rd_kafka_headers_destroy(self)
|
202
|
+
end
|
203
|
+
end
|
204
|
+
end
|
205
|
+
end
|
@@ -0,0 +1,205 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Kafka::FFI
|
4
|
+
class Message < ::FFI::Struct
|
5
|
+
require "kafka/ffi/message/header"
|
6
|
+
|
7
|
+
layout(
|
8
|
+
:err, :error_code,
|
9
|
+
:rkt, Topic,
|
10
|
+
:partition, :int32,
|
11
|
+
:payload, :pointer,
|
12
|
+
:len, :size_t,
|
13
|
+
:key, :pointer,
|
14
|
+
:key_len, :size_t,
|
15
|
+
:offset, :int64,
|
16
|
+
:private, Opaque
|
17
|
+
)
|
18
|
+
|
19
|
+
# Retrieve the error associated with this message. For consumers this is
|
20
|
+
# used to report per-topic+partition consumer errors. For producers this is
|
21
|
+
# set when received in the dr_msg_cb callback to signify a fatal error
|
22
|
+
# publishing the message.
|
23
|
+
#
|
24
|
+
# @return [nil] Message does not have an error
|
25
|
+
# @return [Kafka::ResponseError] RD_KAFKA_RESP_ERR__* error code
|
26
|
+
def error
|
27
|
+
if self[:err] != :ok
|
28
|
+
::Kafka::ResponseError.new(self[:err])
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
# Returns the name of the Topic the Message was published to.
|
33
|
+
#
|
34
|
+
# @return [nil] Topic information was not available
|
35
|
+
# @return [String] Name of the Topic the message was published to.
|
36
|
+
def topic
|
37
|
+
if self[:rkt].nil?
|
38
|
+
return nil
|
39
|
+
end
|
40
|
+
|
41
|
+
self[:rkt].name
|
42
|
+
end
|
43
|
+
|
44
|
+
# Returns the optional message key used to publish the message. This key is
|
45
|
+
# used for partition assignment based on the `partitioner` or
|
46
|
+
# `partitioner_cb` config options.
|
47
|
+
#
|
48
|
+
# @return [nil] No partitioning key was provided
|
49
|
+
# @return [String] The partitioning key
|
50
|
+
def key
|
51
|
+
if self[:key].null?
|
52
|
+
return nil
|
53
|
+
end
|
54
|
+
|
55
|
+
self[:key].read_string(self[:key_len])
|
56
|
+
end
|
57
|
+
|
58
|
+
# Returns the partition the message was published to.
|
59
|
+
#
|
60
|
+
# @return [Integer] Partition
|
61
|
+
def partition
|
62
|
+
self[:partition]
|
63
|
+
end
|
64
|
+
|
65
|
+
# Returns the message's offset as published in the topic's partition. When
|
66
|
+
# error != nil, offset the error occurred at.
|
67
|
+
#
|
68
|
+
# @return [Integer] Message offset
|
69
|
+
# @return [RD_KAFKA_OFFSET_INVALID] Message was retried and idempotence is
|
70
|
+
# enabled.
|
71
|
+
def offset
|
72
|
+
self[:offset]
|
73
|
+
end
|
74
|
+
|
75
|
+
# Returns the per message opaque pointer that was given to produce. This is
|
76
|
+
# a pointer to a Ruby object owned by the application.
|
77
|
+
#
|
78
|
+
# @note Using the opaque is dangerous and requires that the application
|
79
|
+
# maintain a reference to the object passed to produce. Failing to do so
|
80
|
+
# will cause segfaults due to the object having been garbage collected.
|
81
|
+
#
|
82
|
+
# @example Retrieve object from opaque
|
83
|
+
# require "fiddle"
|
84
|
+
# obj = Fiddle::Pointer.new(msg.opaque.to_i).to_value
|
85
|
+
#
|
86
|
+
# @return [nil] Opaque was not set
|
87
|
+
# @return [FFI::Pointer] Pointer to opaque address
|
88
|
+
def opaque
|
89
|
+
self[:private]
|
90
|
+
end
|
91
|
+
|
92
|
+
# Returns the message's payload. When error != nil, will contain a string
|
93
|
+
# describing the error.
|
94
|
+
#
|
95
|
+
# @return [String] Message payload or error string.
|
96
|
+
def payload
|
97
|
+
if self[:payload].null?
|
98
|
+
return nil
|
99
|
+
end
|
100
|
+
|
101
|
+
self[:payload].read_string(self[:len])
|
102
|
+
end
|
103
|
+
|
104
|
+
# Get the message header list
|
105
|
+
#
|
106
|
+
# @raise [Kafka::ResponseError] Error occurred parsing headers
|
107
|
+
#
|
108
|
+
# @return [nil] Message does not have any headers
|
109
|
+
# @return [Message::Headers] Set of headers
|
110
|
+
def headers
|
111
|
+
ptr = ::FFI::MemoryPointer.new(:pointer)
|
112
|
+
|
113
|
+
err = ::Kafka::FFI.rd_kafka_message_headers(self, ptr)
|
114
|
+
case err
|
115
|
+
when :ok
|
116
|
+
if ptr.null?
|
117
|
+
nil
|
118
|
+
else
|
119
|
+
Message::Headers.new(ptr)
|
120
|
+
end
|
121
|
+
when RD_KAFKA_RESP_ERR__NOENT
|
122
|
+
# Messages does not have headers
|
123
|
+
nil
|
124
|
+
else
|
125
|
+
raise ::Kafka::ResponseError, err
|
126
|
+
end
|
127
|
+
ensure
|
128
|
+
ptr.free
|
129
|
+
end
|
130
|
+
|
131
|
+
# Get the Message's headers and detach them from the Message (setting its
|
132
|
+
# headers to nil). Calling detach_headers means the applicaiton is now the
|
133
|
+
# owner of the returned Message::Header and must eventually call #destroy
|
134
|
+
# when the application is done with them.
|
135
|
+
#
|
136
|
+
# @raise [Kafka::ResponseError] Error occurred parsing headers
|
137
|
+
#
|
138
|
+
# @return [nil] Message does not have any headers
|
139
|
+
# @return [Message::Headers] Set of headers
|
140
|
+
def detach_headers
|
141
|
+
ptr = ::FFI::MemoryPointer.new(:pointer)
|
142
|
+
|
143
|
+
err = ::Kafka::FFI.rd_kafka_message_detach_headers(self, ptr)
|
144
|
+
case err
|
145
|
+
when :ok
|
146
|
+
if ptr.null?
|
147
|
+
nil
|
148
|
+
else
|
149
|
+
Message::Headers.new(ptr)
|
150
|
+
end
|
151
|
+
when RD_KAFKA_RESP_ERR__NOENT
|
152
|
+
# Messages does not have headers
|
153
|
+
nil
|
154
|
+
else
|
155
|
+
raise ::Kafka::ResponseError, err
|
156
|
+
end
|
157
|
+
ensure
|
158
|
+
ptr.free
|
159
|
+
end
|
160
|
+
|
161
|
+
# rubocop:disable Naming/AccessorMethodName
|
162
|
+
|
163
|
+
# Replace the Message's headers with a new set.
|
164
|
+
#
|
165
|
+
# @note The Message takes ownership of the headers and they will be
|
166
|
+
# destroyed automatically with the Message.
|
167
|
+
def set_headers(headers)
|
168
|
+
::Kafka::FFI.rd_kafka_set_headers(self, headers)
|
169
|
+
end
|
170
|
+
alias headers= set_headers
|
171
|
+
# rubocop:enable Naming/AccessorMethodName
|
172
|
+
|
173
|
+
# Retrieve the timestamp for a consumed message.
|
174
|
+
#
|
175
|
+
# @example Convert timestamp to a Time
|
176
|
+
# ts = message.timestamp
|
177
|
+
# ts = ts && Time.at(0, ts, :millisecond).utc
|
178
|
+
#
|
179
|
+
# @return [Integer] Message timestamp as milliseconds since unix epoch
|
180
|
+
# @return [nil] Timestamp is not available
|
181
|
+
def timestamp
|
182
|
+
# @todo: Type (second param) [rd_kafka_timestamp_type_t enum]
|
183
|
+
ts = ::Kafka::FFI.rd_kafka_message_timestamp(self, nil)
|
184
|
+
ts == -1 ? nil : ts
|
185
|
+
end
|
186
|
+
|
187
|
+
# Retrieve the latency since the Message was published to Kafka.
|
188
|
+
#
|
189
|
+
# @return [Integer] Latency since produce() call in microseconds
|
190
|
+
# @return [nil] Latency not available
|
191
|
+
def latency
|
192
|
+
latency = ::Kafka::FFI.rd_kafka_message_latency(self)
|
193
|
+
latency == -1 ? nil : latency
|
194
|
+
end
|
195
|
+
|
196
|
+
# Frees resources used by the messages and hands ownership by to
|
197
|
+
# librdkafka. The application should call destroy when done processing the
|
198
|
+
# message.
|
199
|
+
def destroy
|
200
|
+
if !null?
|
201
|
+
::Kafka::FFI.rd_kafka_message_destroy(self)
|
202
|
+
end
|
203
|
+
end
|
204
|
+
end
|
205
|
+
end
|
@@ -0,0 +1,58 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Kafka::FFI
|
4
|
+
class Metadata < ::FFI::Struct
|
5
|
+
layout(
|
6
|
+
:broker_cnt, :int,
|
7
|
+
:brokers, :pointer, # *rd_kafka_metadata_broker
|
8
|
+
:topic_cnt, :int,
|
9
|
+
:topics, :pointer, # *rd_kafka_metadata_topic
|
10
|
+
:orig_broker_id, :int32,
|
11
|
+
:orig_broker_name, :string
|
12
|
+
)
|
13
|
+
|
14
|
+
# Returns detailed metadata for the Brokers in the cluster.
|
15
|
+
#
|
16
|
+
# @return [Array<BrokerMetadata>] Metadata about known Brokers.
|
17
|
+
def brokers
|
18
|
+
ptr = self[:brokers]
|
19
|
+
|
20
|
+
self[:broker_cnt].times.map do |i|
|
21
|
+
BrokerMetadata.new(ptr + (i * BrokerMetadata.size))
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
# Returns detailed metadata about the topics and their partitions.
|
26
|
+
#
|
27
|
+
# @return [Array<TopicMetadata>] Metadata about known Topics.
|
28
|
+
def topics
|
29
|
+
ptr = self[:topics]
|
30
|
+
|
31
|
+
self[:topic_cnt].times.map do |i|
|
32
|
+
TopicMetadata.new(ptr + (i * TopicMetadata.size))
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
# Returns the ID of the Broker that the metadata request was served by.
|
37
|
+
#
|
38
|
+
# @return [Integer] Broker ID
|
39
|
+
def broker_id
|
40
|
+
self[:orig_broker_id]
|
41
|
+
end
|
42
|
+
|
43
|
+
# Returns the name of the Broker that the metadata request was served by.
|
44
|
+
#
|
45
|
+
# @return [String] Broker name
|
46
|
+
def broker_name
|
47
|
+
self[:orig_broker_name]
|
48
|
+
end
|
49
|
+
|
50
|
+
# Destroy the Metadata response, returning it's resources back to the
|
51
|
+
# system.
|
52
|
+
def destroy
|
53
|
+
if !null?
|
54
|
+
::Kafka::FFI.rd_kafka_metadata_destroy(self)
|
55
|
+
end
|
56
|
+
end
|
57
|
+
end
|
58
|
+
end
|
@@ -0,0 +1,81 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Kafka::FFI
|
4
|
+
# Opaque provides a safe mechanism for providing Ruby objects as opaque
|
5
|
+
# pointers.
|
6
|
+
#
|
7
|
+
# Opaque pointers are used heavily in librdkafka to allow for passing
|
8
|
+
# references to application state into callbacks, configs, and other
|
9
|
+
# contexts. Ruby's garbage collector cannot check for references held in
|
10
|
+
# external FFI memory and will garbage collect objects that are otherwise not
|
11
|
+
# referenced leading to a segmentation fault.
|
12
|
+
#
|
13
|
+
# Opaque solves this by allocated a memory address which is used as a hash
|
14
|
+
# key to look up the Ruby object when needed. This keeps a reference to the
|
15
|
+
# object in Ruby so it is not garbage collected. This method allows for Ruby
|
16
|
+
# objects to be moved in memory during compaction (in Ruby 2.7+) compared to
|
17
|
+
# storing a referece to a Ruby object.
|
18
|
+
class Opaque
|
19
|
+
extend ::FFI::DataConverter
|
20
|
+
native_type :pointer
|
21
|
+
|
22
|
+
# Registry holds references to all known Opaque instances in the system
|
23
|
+
# keyed by the pointer address associated with it.
|
24
|
+
@registry = {}
|
25
|
+
|
26
|
+
class << self
|
27
|
+
# Register the Opaque the registry, keeping a reference to it to avoid it
|
28
|
+
# being garbage collected. This will replace any existing Opaque with the
|
29
|
+
# same address.
|
30
|
+
#
|
31
|
+
# @param opaque [Opaque]
|
32
|
+
def register(opaque)
|
33
|
+
@registry[opaque.pointer.address] = opaque
|
34
|
+
end
|
35
|
+
|
36
|
+
# Remove the Opaque from the registry, putting it back in contention for
|
37
|
+
# garbage collection.
|
38
|
+
#
|
39
|
+
# @param opaque [Opaque]
|
40
|
+
def remove(opaque)
|
41
|
+
@registry.delete(opaque.pointer.address)
|
42
|
+
end
|
43
|
+
|
44
|
+
# @param value [Opaque]
|
45
|
+
def to_native(value, _ctx)
|
46
|
+
value.pointer
|
47
|
+
end
|
48
|
+
|
49
|
+
# @param value [FFI::Pointer]
|
50
|
+
def from_native(value, _ctx)
|
51
|
+
if value.null?
|
52
|
+
return nil
|
53
|
+
end
|
54
|
+
|
55
|
+
@registry.fetch(value.address, nil)
|
56
|
+
end
|
57
|
+
end
|
58
|
+
|
59
|
+
attr_reader :value
|
60
|
+
attr_reader :pointer
|
61
|
+
|
62
|
+
def initialize(value)
|
63
|
+
@value = value
|
64
|
+
@pointer = ::FFI::MemoryPointer.new(:int8)
|
65
|
+
|
66
|
+
Opaque.register(self)
|
67
|
+
end
|
68
|
+
|
69
|
+
# Free releases the pointer back to the system and removes the Opaque from
|
70
|
+
# the registry. free should only be called when the Opaque is no longer
|
71
|
+
# stored in librdkafka as it frees the backing pointer which could cause a
|
72
|
+
# segfault if still referenced.
|
73
|
+
def free
|
74
|
+
Opaque.remove(self)
|
75
|
+
@pointer.free
|
76
|
+
|
77
|
+
@value = nil
|
78
|
+
@pointer = nil
|
79
|
+
end
|
80
|
+
end
|
81
|
+
end
|
@@ -0,0 +1,73 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "ffi"
|
4
|
+
|
5
|
+
module Kafka::FFI
|
6
|
+
# OpaquePointer pointer provides a common pattern where we receive a pointer
|
7
|
+
# to a struct but don't care about the structure and need to pass it to
|
8
|
+
# functions. OpaquePointer gives type safety by checking types before
|
9
|
+
# converting.
|
10
|
+
#
|
11
|
+
# @note Kafka has several options for `opaque` pointers that get passed to
|
12
|
+
# callbacks. Those opaque pointers are not related to this opaque pointer.
|
13
|
+
class OpaquePointer
|
14
|
+
extend ::FFI::DataConverter
|
15
|
+
|
16
|
+
# @attr pointer [FFI::Pointer] Pointer to the implementing class
|
17
|
+
attr_reader :pointer
|
18
|
+
|
19
|
+
class << self
|
20
|
+
# Convert from a FFI::Pointer to the implementing class.
|
21
|
+
#
|
22
|
+
# @param value [FFI::Pointer]
|
23
|
+
#
|
24
|
+
# @return [nil] Passed ::FFI::Pointer::NULL
|
25
|
+
# @return Instance of the class backed by the pointer.
|
26
|
+
def from_native(value, _ctx)
|
27
|
+
if !value.is_a?(::FFI::Pointer)
|
28
|
+
raise TypeError, "from_native can only convert from a ::FFI::Pointer to #{self}"
|
29
|
+
end
|
30
|
+
|
31
|
+
# The equivalent of a native NULL pointer is nil.
|
32
|
+
if value.null?
|
33
|
+
return nil
|
34
|
+
end
|
35
|
+
|
36
|
+
obj = allocate
|
37
|
+
obj.send(:initialize, value)
|
38
|
+
obj
|
39
|
+
end
|
40
|
+
|
41
|
+
# Convert from a Kafka::FFI type to a native FFI type.
|
42
|
+
#
|
43
|
+
# @param value [Object] Instance to retrieve a pointer for.
|
44
|
+
#
|
45
|
+
# @return [FFI::Pointer] Pointer to the opaque struct
|
46
|
+
def to_native(value, _ctx)
|
47
|
+
if value.nil?
|
48
|
+
return ::FFI::Pointer::NULL
|
49
|
+
end
|
50
|
+
|
51
|
+
if !value.is_a?(self)
|
52
|
+
raise TypeError, "expected a kind of #{self}, was #{value.class}"
|
53
|
+
end
|
54
|
+
|
55
|
+
value.pointer
|
56
|
+
end
|
57
|
+
|
58
|
+
# Provide ::FFI::Struct API compatility for consistency with
|
59
|
+
# attach_function is called with an OpaquePointer.
|
60
|
+
def by_ref
|
61
|
+
self
|
62
|
+
end
|
63
|
+
|
64
|
+
def inherited(subclass)
|
65
|
+
subclass.native_type :pointer
|
66
|
+
end
|
67
|
+
end
|
68
|
+
|
69
|
+
def initialize(pointer)
|
70
|
+
@pointer = pointer
|
71
|
+
end
|
72
|
+
end
|
73
|
+
end
|