rdkafka 0.2.0 → 0.3.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 +4 -4
- data/CHANGELOG.md +5 -0
- data/README.md +1 -1
- data/Rakefile +1 -0
- data/lib/rdkafka.rb +3 -1
- data/lib/rdkafka/{ffi.rb → bindings.rb} +18 -9
- data/lib/rdkafka/config.rb +12 -12
- data/lib/rdkafka/consumer.rb +85 -10
- data/lib/rdkafka/consumer/message.rb +12 -3
- data/lib/rdkafka/consumer/partition.rb +40 -0
- data/lib/rdkafka/consumer/topic_partition_list.rb +93 -0
- data/lib/rdkafka/error.rb +2 -2
- data/lib/rdkafka/producer.rb +11 -11
- data/lib/rdkafka/producer/delivery_handle.rb +1 -1
- data/lib/rdkafka/version.rb +1 -1
- data/spec/rdkafka/bindings_spec.rb +13 -0
- data/spec/rdkafka/consumer/message_spec.rb +32 -4
- data/spec/rdkafka/consumer/partition_spec.rb +35 -0
- data/spec/rdkafka/consumer/topic_partition_list_spec.rb +94 -0
- data/spec/rdkafka/consumer_spec.rb +63 -0
- data/spec/spec_helper.rb +4 -2
- metadata +11 -5
- data/spec/rdkafka/ffi_spec.rb +0 -13
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: cd80c3cdbb5bc790349df27bdef345b86e37d341
|
4
|
+
data.tar.gz: 822bad9c78b508e28cc7030443841b622b4d6df2
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 63f00a3e196e10359220321613b03335bcec067e342155edbe256ef8a04716b435d2444674839e694e423a08a91e240e2e7d0756fb79402a354b8c8ab49cd3d1
|
7
|
+
data.tar.gz: 4e112ca823c2b500187aed86edf540f3eda4fbb45cee3b22ee908a1605696545cff2a77e2b56ba2d73500e185a3c1524992638e6ccb31d8f3c14d32cb783c1bf
|
data/CHANGELOG.md
CHANGED
data/README.md
CHANGED
@@ -54,7 +54,7 @@ end
|
|
54
54
|
|
55
55
|
## Development
|
56
56
|
|
57
|
-
Run `bundle` and `cd ext && bundle exec rake
|
57
|
+
Run `bundle` and `cd ext && bundle exec rake && cd ..`. Then
|
58
58
|
create the topics as expected in the specs: `bundle exec rake create_topics`.
|
59
59
|
|
60
60
|
You can then run `bundle exec rspec` to run the tests. To see rdkafka
|
data/Rakefile
CHANGED
@@ -7,6 +7,7 @@ task :create_topics do
|
|
7
7
|
else
|
8
8
|
'kafka-topics'
|
9
9
|
end
|
10
|
+
`#{kafka_topics} --create --topic=consume_test_topic --zookeeper=127.0.0.1:2181 --partitions=3 --replication-factor=1`
|
10
11
|
`#{kafka_topics} --create --topic=produce_test_topic --zookeeper=127.0.0.1:2181 --partitions=3 --replication-factor=1`
|
11
12
|
`#{kafka_topics} --create --topic=rake_test_topic --zookeeper=127.0.0.1:2181 --partitions=3 --replication-factor=1`
|
12
13
|
end
|
data/lib/rdkafka.rb
CHANGED
@@ -1,10 +1,12 @@
|
|
1
1
|
require "rdkafka/version"
|
2
2
|
|
3
|
+
require "rdkafka/bindings"
|
3
4
|
require "rdkafka/config"
|
4
5
|
require "rdkafka/consumer"
|
5
6
|
require "rdkafka/consumer/message"
|
7
|
+
require "rdkafka/consumer/partition"
|
8
|
+
require "rdkafka/consumer/topic_partition_list"
|
6
9
|
require "rdkafka/error"
|
7
|
-
require "rdkafka/ffi"
|
8
10
|
require "rdkafka/producer"
|
9
11
|
require "rdkafka/producer/delivery_handle"
|
10
12
|
require "rdkafka/producer/delivery_report"
|
@@ -3,8 +3,8 @@ require "logger"
|
|
3
3
|
|
4
4
|
module Rdkafka
|
5
5
|
# @private
|
6
|
-
module
|
7
|
-
extend
|
6
|
+
module Bindings
|
7
|
+
extend FFI::Library
|
8
8
|
|
9
9
|
def self.lib_extension
|
10
10
|
if Gem::Platform.local.os.include?("darwin")
|
@@ -23,7 +23,7 @@ module Rdkafka
|
|
23
23
|
|
24
24
|
# Message struct
|
25
25
|
|
26
|
-
class Message <
|
26
|
+
class Message < FFI::Struct
|
27
27
|
layout :err, :int,
|
28
28
|
:rkt, :pointer,
|
29
29
|
:partition, :int32,
|
@@ -42,8 +42,8 @@ module Rdkafka
|
|
42
42
|
|
43
43
|
# TopicPartition ad TopicPartitionList structs
|
44
44
|
|
45
|
-
class TopicPartition <
|
46
|
-
|
45
|
+
class TopicPartition < FFI::Struct
|
46
|
+
layout :topic, :string,
|
47
47
|
:partition, :int32,
|
48
48
|
:offset, :int64,
|
49
49
|
:metadata, :pointer,
|
@@ -53,15 +53,16 @@ module Rdkafka
|
|
53
53
|
:_private, :pointer
|
54
54
|
end
|
55
55
|
|
56
|
-
class TopicPartitionList <
|
56
|
+
class TopicPartitionList < FFI::Struct
|
57
57
|
layout :cnt, :int,
|
58
58
|
:size, :int,
|
59
|
-
:elems,
|
59
|
+
:elems, :pointer
|
60
60
|
end
|
61
61
|
|
62
62
|
attach_function :rd_kafka_topic_partition_list_new, [:int32], :pointer
|
63
63
|
attach_function :rd_kafka_topic_partition_list_add, [:pointer, :string, :int32], :void
|
64
64
|
attach_function :rd_kafka_topic_partition_list_destroy, [:pointer], :void
|
65
|
+
attach_function :rd_kafka_topic_partition_list_copy, [:pointer], :pointer
|
65
66
|
|
66
67
|
# Errors
|
67
68
|
|
@@ -85,7 +86,7 @@ module Rdkafka
|
|
85
86
|
attach_function :rd_kafka_set_log_queue, [:pointer, :pointer], :void
|
86
87
|
attach_function :rd_kafka_queue_get_main, [:pointer], :pointer
|
87
88
|
|
88
|
-
LogCallback =
|
89
|
+
LogCallback = FFI::Function.new(
|
89
90
|
:void, [:pointer, :int, :string, :string]
|
90
91
|
) do |_client_ptr, level, _level_string, line|
|
91
92
|
severity = case level
|
@@ -118,11 +119,19 @@ module Rdkafka
|
|
118
119
|
# Consumer
|
119
120
|
|
120
121
|
attach_function :rd_kafka_subscribe, [:pointer, :pointer], :int
|
122
|
+
attach_function :rd_kafka_unsubscribe, [:pointer], :int
|
123
|
+
attach_function :rd_kafka_subscription, [:pointer, :pointer], :int
|
124
|
+
attach_function :rd_kafka_assignment, [:pointer, :pointer], :int
|
125
|
+
attach_function :rd_kafka_committed, [:pointer, :pointer, :int], :int
|
121
126
|
attach_function :rd_kafka_commit, [:pointer, :pointer, :bool], :int, blocking: true
|
122
127
|
attach_function :rd_kafka_poll_set_consumer, [:pointer], :void
|
123
128
|
attach_function :rd_kafka_consumer_poll, [:pointer, :int], :pointer, blocking: true
|
124
129
|
attach_function :rd_kafka_consumer_close, [:pointer], :void, blocking: true
|
125
130
|
|
131
|
+
# Stats
|
132
|
+
|
133
|
+
attach_function :rd_kafka_query_watermark_offsets, [:pointer, :string, :int, :pointer, :pointer, :int], :int
|
134
|
+
|
126
135
|
# Producer
|
127
136
|
|
128
137
|
RD_KAFKA_VTYPE_END = 0
|
@@ -141,7 +150,7 @@ module Rdkafka
|
|
141
150
|
callback :delivery_cb, [:pointer, :pointer, :pointer], :void
|
142
151
|
attach_function :rd_kafka_conf_set_dr_msg_cb, [:pointer, :delivery_cb], :void
|
143
152
|
|
144
|
-
DeliveryCallback =
|
153
|
+
DeliveryCallback = FFI::Function.new(
|
145
154
|
:void, [:pointer, :pointer, :pointer]
|
146
155
|
) do |client_ptr, message_ptr, opaque_ptr|
|
147
156
|
message = Message.new(message_ptr)
|
data/lib/rdkafka/config.rb
CHANGED
@@ -74,7 +74,7 @@ module Rdkafka
|
|
74
74
|
def consumer
|
75
75
|
kafka = native_kafka(native_config, :rd_kafka_consumer)
|
76
76
|
# Redirect the main queue to the consumer
|
77
|
-
Rdkafka::
|
77
|
+
Rdkafka::Bindings.rd_kafka_poll_set_consumer(kafka)
|
78
78
|
# Return consumer with Kafka client
|
79
79
|
Rdkafka::Consumer.new(kafka)
|
80
80
|
end
|
@@ -89,7 +89,7 @@ module Rdkafka
|
|
89
89
|
# Create Kafka config
|
90
90
|
config = native_config
|
91
91
|
# Set callback to receive delivery reports on config
|
92
|
-
Rdkafka::
|
92
|
+
Rdkafka::Bindings.rd_kafka_conf_set_dr_msg_cb(config, Rdkafka::Bindings::DeliveryCallback)
|
93
93
|
# Return producer with Kafka client
|
94
94
|
Rdkafka::Producer.new(native_kafka(config, :rd_kafka_producer))
|
95
95
|
end
|
@@ -108,10 +108,10 @@ module Rdkafka
|
|
108
108
|
# This method is only intented to be used to create a client,
|
109
109
|
# using it in another way will leak memory.
|
110
110
|
def native_config
|
111
|
-
Rdkafka::
|
111
|
+
Rdkafka::Bindings.rd_kafka_conf_new.tap do |config|
|
112
112
|
@config_hash.merge(REQUIRED_CONFIG).each do |key, value|
|
113
|
-
error_buffer =
|
114
|
-
result = Rdkafka::
|
113
|
+
error_buffer = FFI::MemoryPointer.from_string(" " * 256)
|
114
|
+
result = Rdkafka::Bindings.rd_kafka_conf_set(
|
115
115
|
config,
|
116
116
|
key.to_s,
|
117
117
|
value.to_s,
|
@@ -123,13 +123,13 @@ module Rdkafka
|
|
123
123
|
end
|
124
124
|
end
|
125
125
|
# Set log callback
|
126
|
-
Rdkafka::
|
126
|
+
Rdkafka::Bindings.rd_kafka_conf_set_log_cb(config, Rdkafka::Bindings::LogCallback)
|
127
127
|
end
|
128
128
|
end
|
129
129
|
|
130
130
|
def native_kafka(config, type)
|
131
|
-
error_buffer =
|
132
|
-
handle = Rdkafka::
|
131
|
+
error_buffer = FFI::MemoryPointer.from_string(" " * 256)
|
132
|
+
handle = Rdkafka::Bindings.rd_kafka_new(
|
133
133
|
type,
|
134
134
|
config,
|
135
135
|
error_buffer,
|
@@ -141,14 +141,14 @@ module Rdkafka
|
|
141
141
|
end
|
142
142
|
|
143
143
|
# Redirect log to handle's queue
|
144
|
-
Rdkafka::
|
144
|
+
Rdkafka::Bindings.rd_kafka_set_log_queue(
|
145
145
|
handle,
|
146
|
-
Rdkafka::
|
146
|
+
Rdkafka::Bindings.rd_kafka_queue_get_main(handle)
|
147
147
|
)
|
148
148
|
|
149
|
-
|
149
|
+
FFI::AutoPointer.new(
|
150
150
|
handle,
|
151
|
-
Rdkafka::
|
151
|
+
Rdkafka::Bindings.method(:rd_kafka_destroy)
|
152
152
|
)
|
153
153
|
end
|
154
154
|
end
|
data/lib/rdkafka/consumer.rb
CHANGED
@@ -16,10 +16,10 @@ module Rdkafka
|
|
16
16
|
# Close this consumer
|
17
17
|
# @return [nil]
|
18
18
|
def close
|
19
|
-
Rdkafka::
|
19
|
+
Rdkafka::Bindings.rd_kafka_consumer_close(@native_kafka)
|
20
20
|
end
|
21
21
|
|
22
|
-
# Subscribe to one or more topics
|
22
|
+
# Subscribe to one or more topics letting Kafka handle partition assignments.
|
23
23
|
#
|
24
24
|
# @param topics [Array<String>] One or more topic names
|
25
25
|
#
|
@@ -28,22 +28,97 @@ module Rdkafka
|
|
28
28
|
# @return [nil]
|
29
29
|
def subscribe(*topics)
|
30
30
|
# Create topic partition list with topics and no partition set
|
31
|
-
tpl = Rdkafka::
|
31
|
+
tpl = Rdkafka::Bindings.rd_kafka_topic_partition_list_new(topics.length)
|
32
32
|
topics.each do |topic|
|
33
|
-
Rdkafka::
|
33
|
+
Rdkafka::Bindings.rd_kafka_topic_partition_list_add(
|
34
34
|
tpl,
|
35
35
|
topic,
|
36
36
|
-1
|
37
37
|
)
|
38
38
|
end
|
39
39
|
# Subscribe to topic partition list and check this was successful
|
40
|
-
response = Rdkafka::
|
40
|
+
response = Rdkafka::Bindings.rd_kafka_subscribe(@native_kafka, tpl)
|
41
41
|
if response != 0
|
42
42
|
raise Rdkafka::RdkafkaError.new(response)
|
43
43
|
end
|
44
44
|
ensure
|
45
45
|
# Clean up the topic partition list
|
46
|
-
Rdkafka::
|
46
|
+
Rdkafka::Bindings.rd_kafka_topic_partition_list_destroy(tpl)
|
47
|
+
end
|
48
|
+
|
49
|
+
# Unsubscribe from all subscribed topics.
|
50
|
+
#
|
51
|
+
# @raise [RdkafkaError] When unsubscribing fails
|
52
|
+
#
|
53
|
+
# @return [nil]
|
54
|
+
def unsubscribe
|
55
|
+
response = Rdkafka::Bindings.rd_kafka_unsubscribe(@native_kafka)
|
56
|
+
if response != 0
|
57
|
+
raise Rdkafka::RdkafkaError.new(response)
|
58
|
+
end
|
59
|
+
end
|
60
|
+
|
61
|
+
# Return the current subscription to topics and partitions
|
62
|
+
#
|
63
|
+
# @raise [RdkafkaError] When getting the subscription fails.
|
64
|
+
#
|
65
|
+
# @return [TopicPartitionList]
|
66
|
+
def subscription
|
67
|
+
tpl = FFI::MemoryPointer.new(:pointer)
|
68
|
+
response = Rdkafka::Bindings.rd_kafka_subscription(@native_kafka, tpl)
|
69
|
+
if response != 0
|
70
|
+
raise Rdkafka::RdkafkaError.new(response)
|
71
|
+
end
|
72
|
+
Rdkafka::Consumer::TopicPartitionList.new(tpl.get_pointer(0))
|
73
|
+
end
|
74
|
+
|
75
|
+
# Return the current committed offset per partition for this consumer group.
|
76
|
+
# The offset field of each requested partition will either be set to stored offset or to -1001 in case there was no stored offset for that partition.
|
77
|
+
#
|
78
|
+
# @param list [TopicPartitionList] The topic with partitions to get the offsets for.
|
79
|
+
# @param timeout_ms [Integer] The timeout for fetching this information.
|
80
|
+
#
|
81
|
+
# @raise [RdkafkaError] When getting the committed positions fails.
|
82
|
+
#
|
83
|
+
# @return [TopicPartitionList]
|
84
|
+
def committed(list, timeout_ms=200)
|
85
|
+
unless list.is_a?(TopicPartitionList)
|
86
|
+
raise TypeError.new("list has to be a TopicPartitionList")
|
87
|
+
end
|
88
|
+
tpl = list.copy_tpl
|
89
|
+
response = Rdkafka::Bindings.rd_kafka_committed(@native_kafka, tpl, timeout_ms)
|
90
|
+
if response != 0
|
91
|
+
raise Rdkafka::RdkafkaError.new(response)
|
92
|
+
end
|
93
|
+
Rdkafka::Consumer::TopicPartitionList.new(tpl)
|
94
|
+
end
|
95
|
+
|
96
|
+
# Query broker for low (oldest/beginning) and high (newest/end) offsets for a partition.
|
97
|
+
#
|
98
|
+
# @param topic [String] The topic to query
|
99
|
+
# @param partition [Integer] The partition to query
|
100
|
+
# @param timeout_ms [Integer] The timeout for querying the broker
|
101
|
+
#
|
102
|
+
# @raise [RdkafkaError] When querying the broker fails.
|
103
|
+
#
|
104
|
+
# @return [Integer] The low and high watermark
|
105
|
+
def query_watermark_offsets(topic, partition, timeout_ms=200)
|
106
|
+
low = FFI::MemoryPointer.new(:int64, 1)
|
107
|
+
high = FFI::MemoryPointer.new(:int64, 1)
|
108
|
+
|
109
|
+
response = Rdkafka::Bindings.rd_kafka_query_watermark_offsets(
|
110
|
+
@native_kafka,
|
111
|
+
topic,
|
112
|
+
partition,
|
113
|
+
low,
|
114
|
+
high,
|
115
|
+
timeout_ms
|
116
|
+
)
|
117
|
+
if response != 0
|
118
|
+
raise Rdkafka::RdkafkaError.new(response)
|
119
|
+
end
|
120
|
+
|
121
|
+
return low.read_int, high.read_int
|
47
122
|
end
|
48
123
|
|
49
124
|
# Commit the current offsets of this consumer
|
@@ -54,7 +129,7 @@ module Rdkafka
|
|
54
129
|
#
|
55
130
|
# @return [nil]
|
56
131
|
def commit(async=false)
|
57
|
-
response = Rdkafka::
|
132
|
+
response = Rdkafka::Bindings.rd_kafka_commit(@native_kafka, nil, async)
|
58
133
|
if response != 0
|
59
134
|
raise Rdkafka::RdkafkaError.new(response)
|
60
135
|
end
|
@@ -68,12 +143,12 @@ module Rdkafka
|
|
68
143
|
#
|
69
144
|
# @return [Message, nil] A message or nil if there was no new message within the timeout
|
70
145
|
def poll(timeout_ms)
|
71
|
-
message_ptr = Rdkafka::
|
146
|
+
message_ptr = Rdkafka::Bindings.rd_kafka_consumer_poll(@native_kafka, timeout_ms)
|
72
147
|
if message_ptr.null?
|
73
148
|
nil
|
74
149
|
else
|
75
150
|
# Create struct wrapper
|
76
|
-
native_message = Rdkafka::
|
151
|
+
native_message = Rdkafka::Bindings::Message.new(message_ptr)
|
77
152
|
# Raise error if needed
|
78
153
|
if native_message[:err] != 0
|
79
154
|
raise Rdkafka::RdkafkaError.new(native_message[:err])
|
@@ -84,7 +159,7 @@ module Rdkafka
|
|
84
159
|
ensure
|
85
160
|
# Clean up rdkafka message if there is one
|
86
161
|
unless message_ptr.null?
|
87
|
-
Rdkafka::
|
162
|
+
Rdkafka::Bindings.rd_kafka_message_destroy(message_ptr)
|
88
163
|
end
|
89
164
|
end
|
90
165
|
|
@@ -29,7 +29,7 @@ module Rdkafka
|
|
29
29
|
# @private
|
30
30
|
def initialize(native_message)
|
31
31
|
unless native_message[:rkt].null?
|
32
|
-
@topic =
|
32
|
+
@topic = Rdkafka::Bindings.rd_kafka_topic_name(native_message[:rkt])
|
33
33
|
end
|
34
34
|
@partition = native_message[:partition]
|
35
35
|
unless native_message[:payload].null?
|
@@ -39,12 +39,21 @@ module Rdkafka
|
|
39
39
|
@key = native_message[:key].read_string(native_message[:key_len])
|
40
40
|
end
|
41
41
|
@offset = native_message[:offset]
|
42
|
-
@timestamp =
|
42
|
+
@timestamp = Rdkafka::Bindings.rd_kafka_message_timestamp(native_message, nil)
|
43
43
|
end
|
44
44
|
|
45
|
+
# Human readable representation of this message.
|
45
46
|
# @return [String]
|
46
47
|
def to_s
|
47
|
-
"Message in '#{topic}' with key '#{key}', payload '#{payload}', partition #{partition}, offset #{offset}, timestamp #{timestamp}"
|
48
|
+
"<Message in '#{topic}' with key '#{truncate(key)}', payload '#{truncate(payload)}', partition #{partition}, offset #{offset}, timestamp #{timestamp}>"
|
49
|
+
end
|
50
|
+
|
51
|
+
def truncate(string)
|
52
|
+
if string && string.length > 40
|
53
|
+
"#{string[0..39]}..."
|
54
|
+
else
|
55
|
+
string
|
56
|
+
end
|
48
57
|
end
|
49
58
|
end
|
50
59
|
end
|
@@ -0,0 +1,40 @@
|
|
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]
|
11
|
+
attr_reader :offset
|
12
|
+
|
13
|
+
# @private
|
14
|
+
def initialize(partition, offset)
|
15
|
+
@partition = partition
|
16
|
+
@offset = offset
|
17
|
+
end
|
18
|
+
|
19
|
+
# Human readable representation of this partition.
|
20
|
+
# @return [String]
|
21
|
+
def to_s
|
22
|
+
"<Partition #{partition} with offset #{offset}>"
|
23
|
+
end
|
24
|
+
|
25
|
+
# Human readable representation of this partition.
|
26
|
+
# @return [String]
|
27
|
+
def inspect
|
28
|
+
to_s
|
29
|
+
end
|
30
|
+
|
31
|
+
# Whether another partition is equal to this
|
32
|
+
# @return [Boolean]
|
33
|
+
def ==(other)
|
34
|
+
self.class == other.class &&
|
35
|
+
self.partition == other.partition &&
|
36
|
+
self.offset == other.offset
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
@@ -0,0 +1,93 @@
|
|
1
|
+
module Rdkafka
|
2
|
+
class Consumer
|
3
|
+
# A list of topics with their partition information
|
4
|
+
class TopicPartitionList
|
5
|
+
# Create a new topic partition list.
|
6
|
+
#
|
7
|
+
# @param pointer [FFI::Pointer, nil] Optional pointer to an existing native list
|
8
|
+
#
|
9
|
+
# @return [TopicPartitionList]
|
10
|
+
def initialize(pointer=nil)
|
11
|
+
@tpl =
|
12
|
+
Rdkafka::Bindings::TopicPartitionList.new(
|
13
|
+
FFI::AutoPointer.new(
|
14
|
+
pointer || Rdkafka::Bindings.rd_kafka_topic_partition_list_new(5),
|
15
|
+
Rdkafka::Bindings.method(:rd_kafka_topic_partition_list_destroy)
|
16
|
+
)
|
17
|
+
)
|
18
|
+
end
|
19
|
+
|
20
|
+
# Number of items in the list
|
21
|
+
# @return [Integer]
|
22
|
+
def count
|
23
|
+
@tpl[:cnt]
|
24
|
+
end
|
25
|
+
|
26
|
+
# Whether this list is empty
|
27
|
+
# @return [Boolean]
|
28
|
+
def empty?
|
29
|
+
count == 0
|
30
|
+
end
|
31
|
+
|
32
|
+
# Add a topic with optionally partitions to the list.
|
33
|
+
#
|
34
|
+
# @param topic [String] The topic's name
|
35
|
+
# @param partition [Array<Integer>] The topic's partition's
|
36
|
+
#
|
37
|
+
# @return [nil]
|
38
|
+
def add_topic(topic, partitions=nil)
|
39
|
+
if partitions.nil?
|
40
|
+
Rdkafka::Bindings.rd_kafka_topic_partition_list_add(
|
41
|
+
@tpl,
|
42
|
+
topic,
|
43
|
+
-1
|
44
|
+
)
|
45
|
+
else
|
46
|
+
partitions.each do |partition|
|
47
|
+
Rdkafka::Bindings.rd_kafka_topic_partition_list_add(
|
48
|
+
@tpl,
|
49
|
+
topic,
|
50
|
+
partition
|
51
|
+
)
|
52
|
+
end
|
53
|
+
end
|
54
|
+
end
|
55
|
+
|
56
|
+
# Return a `Hash` with the topics as keys and and an array of partition information as the value if present.
|
57
|
+
#
|
58
|
+
# @return [Hash<String, [Array<Partition>, nil]>]
|
59
|
+
def to_h
|
60
|
+
{}.tap do |out|
|
61
|
+
count.times do |i|
|
62
|
+
ptr = @tpl[:elems] + (i * Rdkafka::Bindings::TopicPartition.size)
|
63
|
+
elem = Rdkafka::Bindings::TopicPartition.new(ptr)
|
64
|
+
if elem[:partition] == -1
|
65
|
+
out[elem[:topic]] = nil
|
66
|
+
else
|
67
|
+
partitions = out[elem[:topic]] || []
|
68
|
+
partition = Partition.new(elem[:partition], elem[:offset])
|
69
|
+
partitions.push(partition)
|
70
|
+
out[elem[:topic]] = partitions
|
71
|
+
end
|
72
|
+
end
|
73
|
+
end
|
74
|
+
end
|
75
|
+
|
76
|
+
# Human readable representation of this list.
|
77
|
+
# @return [String]
|
78
|
+
def to_s
|
79
|
+
"<TopicPartitionList: #{to_h}>"
|
80
|
+
end
|
81
|
+
|
82
|
+
def ==(other)
|
83
|
+
self.to_h == other.to_h
|
84
|
+
end
|
85
|
+
|
86
|
+
# Return a copy of the internal native list
|
87
|
+
# @private
|
88
|
+
def copy_tpl
|
89
|
+
Rdkafka::Bindings.rd_kafka_topic_partition_list_copy(@tpl)
|
90
|
+
end
|
91
|
+
end
|
92
|
+
end
|
93
|
+
end
|
data/lib/rdkafka/error.rb
CHANGED
@@ -14,7 +14,7 @@ module Rdkafka
|
|
14
14
|
# This error's code, for example `:partition_eof`, `:msg_size_too_large`.
|
15
15
|
# @return [Symbol]
|
16
16
|
def code
|
17
|
-
code = Rdkafka::
|
17
|
+
code = Rdkafka::Bindings.rd_kafka_err2name(@rdkafka_response).downcase
|
18
18
|
if code[0] == "_"
|
19
19
|
code[1..-1].to_sym
|
20
20
|
else
|
@@ -25,7 +25,7 @@ module Rdkafka
|
|
25
25
|
# Human readable representation of this error.
|
26
26
|
# @return [String]
|
27
27
|
def to_s
|
28
|
-
"#{Rdkafka::
|
28
|
+
"#{Rdkafka::Bindings.rd_kafka_err2str(@rdkafka_response)} (#{code})"
|
29
29
|
end
|
30
30
|
|
31
31
|
# Whether this error indicates the partition is EOF.
|
data/lib/rdkafka/producer.rb
CHANGED
@@ -8,9 +8,9 @@ module Rdkafka
|
|
8
8
|
# Start thread to poll client for delivery callbacks
|
9
9
|
@polling_thread = Thread.new do
|
10
10
|
loop do
|
11
|
-
Rdkafka::
|
11
|
+
Rdkafka::Bindings.rd_kafka_poll(@native_kafka, 250)
|
12
12
|
# Exit thread if closing and the poll queue is empty
|
13
|
-
if @closing && Rdkafka::
|
13
|
+
if @closing && Rdkafka::Bindings.rd_kafka_outq_len(@native_kafka) == 0
|
14
14
|
break
|
15
15
|
end
|
16
16
|
end
|
@@ -71,16 +71,16 @@ module Rdkafka
|
|
71
71
|
delivery_handle[:offset] = -1
|
72
72
|
|
73
73
|
# Produce the message
|
74
|
-
response = Rdkafka::
|
74
|
+
response = Rdkafka::Bindings.rd_kafka_producev(
|
75
75
|
@native_kafka,
|
76
|
-
:int, Rdkafka::
|
77
|
-
:int, Rdkafka::
|
78
|
-
:int, Rdkafka::
|
79
|
-
:int, Rdkafka::
|
80
|
-
:int, Rdkafka::
|
81
|
-
:int, Rdkafka::
|
82
|
-
:int, Rdkafka::
|
83
|
-
:int, Rdkafka::
|
76
|
+
:int, Rdkafka::Bindings::RD_KAFKA_VTYPE_TOPIC, :string, topic,
|
77
|
+
:int, Rdkafka::Bindings::RD_KAFKA_VTYPE_MSGFLAGS, :int, Rdkafka::Bindings::RD_KAFKA_MSG_F_COPY,
|
78
|
+
:int, Rdkafka::Bindings::RD_KAFKA_VTYPE_VALUE, :buffer_in, payload, :size_t, payload_size,
|
79
|
+
:int, Rdkafka::Bindings::RD_KAFKA_VTYPE_KEY, :buffer_in, key, :size_t, key_size,
|
80
|
+
:int, Rdkafka::Bindings::RD_KAFKA_VTYPE_PARTITION, :int32, partition,
|
81
|
+
:int, Rdkafka::Bindings::RD_KAFKA_VTYPE_TIMESTAMP, :int64, timestamp,
|
82
|
+
:int, Rdkafka::Bindings::RD_KAFKA_VTYPE_OPAQUE, :pointer, delivery_handle,
|
83
|
+
:int, Rdkafka::Bindings::RD_KAFKA_VTYPE_END
|
84
84
|
)
|
85
85
|
|
86
86
|
# Raise error if the produce call was not successfull
|
data/lib/rdkafka/version.rb
CHANGED
@@ -0,0 +1,13 @@
|
|
1
|
+
require "spec_helper"
|
2
|
+
|
3
|
+
describe Rdkafka::Bindings do
|
4
|
+
it "should load librdkafka" do
|
5
|
+
expect(Rdkafka::Bindings.ffi_libraries.map(&:name).first).to include "librdkafka"
|
6
|
+
end
|
7
|
+
|
8
|
+
it "should successfully call librdkafka" do
|
9
|
+
expect {
|
10
|
+
Rdkafka::Bindings.rd_kafka_conf_new
|
11
|
+
}.not_to raise_error
|
12
|
+
end
|
13
|
+
end
|
@@ -2,7 +2,7 @@ require "spec_helper"
|
|
2
2
|
|
3
3
|
describe Rdkafka::Consumer::Message do
|
4
4
|
let(:native_topic) do
|
5
|
-
Rdkafka::
|
5
|
+
Rdkafka::Bindings.rd_kafka_topic_new(
|
6
6
|
native_client,
|
7
7
|
"topic_name",
|
8
8
|
nil
|
@@ -11,18 +11,18 @@ describe Rdkafka::Consumer::Message do
|
|
11
11
|
let(:payload) { nil }
|
12
12
|
let(:key) { nil }
|
13
13
|
let(:native_message) do
|
14
|
-
Rdkafka::
|
14
|
+
Rdkafka::Bindings::Message.new.tap do |message|
|
15
15
|
message[:rkt] = native_topic
|
16
16
|
message[:partition] = 3
|
17
17
|
message[:offset] = 100
|
18
18
|
if payload
|
19
|
-
ptr =
|
19
|
+
ptr = FFI::MemoryPointer.new(:char, payload.bytesize)
|
20
20
|
ptr.put_bytes(0, payload)
|
21
21
|
message[:payload] = ptr
|
22
22
|
message[:len] = payload.bytesize
|
23
23
|
end
|
24
24
|
if key
|
25
|
-
ptr =
|
25
|
+
ptr = FFI::MemoryPointer.new(:char, key.bytesize)
|
26
26
|
ptr.put_bytes(0, key)
|
27
27
|
message[:key] = ptr
|
28
28
|
message[:key_len] = key.bytesize
|
@@ -78,4 +78,32 @@ describe Rdkafka::Consumer::Message do
|
|
78
78
|
subject.timestamp
|
79
79
|
}.not_to raise_error
|
80
80
|
end
|
81
|
+
|
82
|
+
describe "#to_s" do
|
83
|
+
before do
|
84
|
+
allow(subject).to receive(:timestamp).and_return(1000)
|
85
|
+
end
|
86
|
+
|
87
|
+
it "should have a human readable representation" do
|
88
|
+
expect(subject.to_s).to eq "<Message in 'topic_name' with key '', payload '', partition 3, offset 100, timestamp 1000>"
|
89
|
+
end
|
90
|
+
|
91
|
+
context "with key and payload" do
|
92
|
+
let(:key) { "key" }
|
93
|
+
let(:payload) { "payload" }
|
94
|
+
|
95
|
+
it "should have a human readable representation" do
|
96
|
+
expect(subject.to_s).to eq "<Message in 'topic_name' with key 'key', payload 'payload', partition 3, offset 100, timestamp 1000>"
|
97
|
+
end
|
98
|
+
end
|
99
|
+
|
100
|
+
context "with a very long key and payload" do
|
101
|
+
let(:key) { "k" * 100_000 }
|
102
|
+
let(:payload) { "p" * 100_000 }
|
103
|
+
|
104
|
+
it "should have a human readable representation" do
|
105
|
+
expect(subject.to_s).to eq "<Message in 'topic_name' with key 'kkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkk...', payload 'pppppppppppppppppppppppppppppppppppppppp...', partition 3, offset 100, timestamp 1000>"
|
106
|
+
end
|
107
|
+
end
|
108
|
+
end
|
81
109
|
end
|
@@ -0,0 +1,35 @@
|
|
1
|
+
require "spec_helper"
|
2
|
+
|
3
|
+
describe Rdkafka::Consumer::Partition do
|
4
|
+
subject { Rdkafka::Consumer::Partition.new(1, 100) }
|
5
|
+
|
6
|
+
it "should have a partition" do
|
7
|
+
expect(subject.partition).to eq 1
|
8
|
+
end
|
9
|
+
|
10
|
+
it "should have an offset" do
|
11
|
+
expect(subject.offset).to eq 100
|
12
|
+
end
|
13
|
+
|
14
|
+
describe "#to_s" do
|
15
|
+
it "should return a human readable representation" do
|
16
|
+
expect(subject.to_s).to eq "<Partition 1 with offset 100>"
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
describe "#inspect" do
|
21
|
+
it "should return a human readable representation" do
|
22
|
+
expect(subject.to_s).to eq "<Partition 1 with offset 100>"
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
describe "#==" do
|
27
|
+
it "should equal another partition with the same content" do
|
28
|
+
expect(subject).to eq Rdkafka::Consumer::Partition.new(1, 100)
|
29
|
+
end
|
30
|
+
|
31
|
+
it "should not equal another partition with different content" do
|
32
|
+
expect(subject).not_to eq Rdkafka::Consumer::Partition.new(2, 101)
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
@@ -0,0 +1,94 @@
|
|
1
|
+
require "spec_helper"
|
2
|
+
|
3
|
+
describe Rdkafka::Consumer::TopicPartitionList do
|
4
|
+
it "should create a list from an existing native list" do
|
5
|
+
pointer = Rdkafka::Bindings.rd_kafka_topic_partition_list_new(5)
|
6
|
+
Rdkafka::Bindings.rd_kafka_topic_partition_list_add(
|
7
|
+
pointer,
|
8
|
+
"topic",
|
9
|
+
-1
|
10
|
+
)
|
11
|
+
list = Rdkafka::Consumer::TopicPartitionList.new(pointer)
|
12
|
+
|
13
|
+
other = Rdkafka::Consumer::TopicPartitionList.new.tap do |list|
|
14
|
+
list.add_topic("topic")
|
15
|
+
end
|
16
|
+
|
17
|
+
expect(list).to eq other
|
18
|
+
end
|
19
|
+
|
20
|
+
it "should create a new list and add unassigned topics" do
|
21
|
+
list = Rdkafka::Consumer::TopicPartitionList.new
|
22
|
+
|
23
|
+
expect(list.count).to eq 0
|
24
|
+
expect(list.empty?).to be true
|
25
|
+
|
26
|
+
list.add_topic("topic1")
|
27
|
+
list.add_topic("topic2")
|
28
|
+
|
29
|
+
expect(list.count).to eq 2
|
30
|
+
expect(list.empty?).to be false
|
31
|
+
|
32
|
+
hash = list.to_h
|
33
|
+
expect(hash.count).to eq 2
|
34
|
+
expect(hash).to eq ({
|
35
|
+
"topic1" => nil,
|
36
|
+
"topic2" => nil
|
37
|
+
})
|
38
|
+
end
|
39
|
+
|
40
|
+
it "should create a new list and add assigned topics" do
|
41
|
+
list = Rdkafka::Consumer::TopicPartitionList.new
|
42
|
+
|
43
|
+
expect(list.count).to eq 0
|
44
|
+
expect(list.empty?).to be true
|
45
|
+
|
46
|
+
list.add_topic("topic1", [0, 1, 2])
|
47
|
+
list.add_topic("topic2", [0, 1])
|
48
|
+
|
49
|
+
expect(list.count).to eq 5
|
50
|
+
expect(list.empty?).to be false
|
51
|
+
|
52
|
+
hash = list.to_h
|
53
|
+
expect(hash.count).to eq 2
|
54
|
+
expect(hash["topic1"]).to eq([
|
55
|
+
Rdkafka::Consumer::Partition.new(0, -1001),
|
56
|
+
Rdkafka::Consumer::Partition.new(1, -1001),
|
57
|
+
Rdkafka::Consumer::Partition.new(2, -1001)
|
58
|
+
])
|
59
|
+
expect(hash["topic2"]).to eq([
|
60
|
+
Rdkafka::Consumer::Partition.new(0, -1001),
|
61
|
+
Rdkafka::Consumer::Partition.new(1, -1001)
|
62
|
+
])
|
63
|
+
end
|
64
|
+
|
65
|
+
describe "#to_s" do
|
66
|
+
it "should return a human readable representation" do
|
67
|
+
list = Rdkafka::Consumer::TopicPartitionList.new
|
68
|
+
list.add_topic("topic1", [0, 1])
|
69
|
+
|
70
|
+
expected = "<TopicPartitionList: {\"topic1\"=>[<Partition 0 with offset -1001>, <Partition 1 with offset -1001>]}>"
|
71
|
+
|
72
|
+
expect(list.to_s).to eq expected
|
73
|
+
end
|
74
|
+
end
|
75
|
+
|
76
|
+
describe "#==" do
|
77
|
+
subject do
|
78
|
+
Rdkafka::Consumer::TopicPartitionList.new.tap do |list|
|
79
|
+
list.add_topic("topic1", [0])
|
80
|
+
end
|
81
|
+
end
|
82
|
+
|
83
|
+
it "should equal another partition with the same content" do
|
84
|
+
other = Rdkafka::Consumer::TopicPartitionList.new.tap do |list|
|
85
|
+
list.add_topic("topic1", [0])
|
86
|
+
end
|
87
|
+
expect(subject).to eq other
|
88
|
+
end
|
89
|
+
|
90
|
+
it "should not equal another partition with different content" do
|
91
|
+
expect(subject).not_to eq Rdkafka::Consumer::TopicPartitionList.new
|
92
|
+
end
|
93
|
+
end
|
94
|
+
end
|
@@ -1,5 +1,68 @@
|
|
1
1
|
require "spec_helper"
|
2
2
|
|
3
3
|
describe Rdkafka::Consumer do
|
4
|
+
let(:config) { rdkafka_config }
|
5
|
+
let(:consumer) { config.consumer }
|
6
|
+
let(:producer) { config.producer }
|
4
7
|
|
8
|
+
context "subscription" do
|
9
|
+
it "should subscribe" do
|
10
|
+
expect(consumer.subscription).to be_empty
|
11
|
+
|
12
|
+
consumer.subscribe("consume_test_topic")
|
13
|
+
|
14
|
+
expect(consumer.subscription).not_to be_empty
|
15
|
+
expected_subscription = Rdkafka::Consumer::TopicPartitionList.new.tap do |list|
|
16
|
+
list.add_topic("consume_test_topic")
|
17
|
+
end
|
18
|
+
expect(consumer.subscription).to eq expected_subscription
|
19
|
+
|
20
|
+
consumer.unsubscribe
|
21
|
+
|
22
|
+
expect(consumer.subscription).to be_empty
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
describe "committed" do
|
27
|
+
before do
|
28
|
+
# Make sure there's a stored offset
|
29
|
+
report = producer.produce(
|
30
|
+
topic: "consume_test_topic",
|
31
|
+
payload: "payload 1",
|
32
|
+
key: "key 1",
|
33
|
+
partition: 0
|
34
|
+
).wait
|
35
|
+
message = wait_for_message(
|
36
|
+
topic: "consume_test_topic",
|
37
|
+
delivery_report: report,
|
38
|
+
config: config
|
39
|
+
)
|
40
|
+
end
|
41
|
+
|
42
|
+
it "should fetch the committed offsets for a specified topic partition list" do
|
43
|
+
list = Rdkafka::Consumer::TopicPartitionList.new.tap do |list|
|
44
|
+
list.add_topic("consume_test_topic", [0, 1, 2])
|
45
|
+
end
|
46
|
+
partitions = consumer.committed(list).to_h["consume_test_topic"]
|
47
|
+
expect(partitions[0].offset).to be > 0
|
48
|
+
expect(partitions[1].offset).to eq -1001
|
49
|
+
expect(partitions[2].offset).to eq -1001
|
50
|
+
end
|
51
|
+
end
|
52
|
+
|
53
|
+
describe "watermark offsets" do
|
54
|
+
it "should return the watermark offsets" do
|
55
|
+
# Make sure there's a message
|
56
|
+
producer.produce(
|
57
|
+
topic: "consume_test_topic",
|
58
|
+
payload: "payload 1",
|
59
|
+
key: "key 1",
|
60
|
+
partition: 0
|
61
|
+
).wait
|
62
|
+
|
63
|
+
low, high = consumer.query_watermark_offsets("consume_test_topic", 0, 5000)
|
64
|
+
expect(low).to eq 0
|
65
|
+
expect(high).to be > 0
|
66
|
+
end
|
67
|
+
end
|
5
68
|
end
|
data/spec/spec_helper.rb
CHANGED
@@ -22,8 +22,9 @@ def native_client
|
|
22
22
|
config.send(:native_kafka, config.send(:native_config), :rd_kafka_producer)
|
23
23
|
end
|
24
24
|
|
25
|
-
def wait_for_message(topic:, delivery_report:, timeout_in_seconds: 30)
|
26
|
-
|
25
|
+
def wait_for_message(topic:, delivery_report:, timeout_in_seconds: 30, config: nil)
|
26
|
+
config = rdkafka_config if config.nil?
|
27
|
+
consumer = config.consumer
|
27
28
|
consumer.subscribe(topic)
|
28
29
|
timeout = Time.now.to_i + timeout_in_seconds
|
29
30
|
loop do
|
@@ -38,5 +39,6 @@ def wait_for_message(topic:, delivery_report:, timeout_in_seconds: 30)
|
|
38
39
|
end
|
39
40
|
end
|
40
41
|
ensure
|
42
|
+
consumer.commit
|
41
43
|
consumer.close
|
42
44
|
end
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: rdkafka
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.
|
4
|
+
version: 0.3.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Thijs Cadier
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date: 2017-10-
|
11
|
+
date: 2017-10-17 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: ffi
|
@@ -98,21 +98,25 @@ files:
|
|
98
98
|
- Rakefile
|
99
99
|
- ext/Rakefile
|
100
100
|
- lib/rdkafka.rb
|
101
|
+
- lib/rdkafka/bindings.rb
|
101
102
|
- lib/rdkafka/config.rb
|
102
103
|
- lib/rdkafka/consumer.rb
|
103
104
|
- lib/rdkafka/consumer/message.rb
|
105
|
+
- lib/rdkafka/consumer/partition.rb
|
106
|
+
- lib/rdkafka/consumer/topic_partition_list.rb
|
104
107
|
- lib/rdkafka/error.rb
|
105
|
-
- lib/rdkafka/ffi.rb
|
106
108
|
- lib/rdkafka/producer.rb
|
107
109
|
- lib/rdkafka/producer/delivery_handle.rb
|
108
110
|
- lib/rdkafka/producer/delivery_report.rb
|
109
111
|
- lib/rdkafka/version.rb
|
110
112
|
- rdkafka.gemspec
|
113
|
+
- spec/rdkafka/bindings_spec.rb
|
111
114
|
- spec/rdkafka/config_spec.rb
|
112
115
|
- spec/rdkafka/consumer/message_spec.rb
|
116
|
+
- spec/rdkafka/consumer/partition_spec.rb
|
117
|
+
- spec/rdkafka/consumer/topic_partition_list_spec.rb
|
113
118
|
- spec/rdkafka/consumer_spec.rb
|
114
119
|
- spec/rdkafka/error_spec.rb
|
115
|
-
- spec/rdkafka/ffi_spec.rb
|
116
120
|
- spec/rdkafka/producer/delivery_handle_spec.rb
|
117
121
|
- spec/rdkafka/producer/delivery_report_spec.rb
|
118
122
|
- spec/rdkafka/producer_spec.rb
|
@@ -143,11 +147,13 @@ specification_version: 4
|
|
143
147
|
summary: Kafka client library wrapping librdkafka using the ffi gem and futures from
|
144
148
|
concurrent-ruby for Kafka 0.10+
|
145
149
|
test_files:
|
150
|
+
- spec/rdkafka/bindings_spec.rb
|
146
151
|
- spec/rdkafka/config_spec.rb
|
147
152
|
- spec/rdkafka/consumer/message_spec.rb
|
153
|
+
- spec/rdkafka/consumer/partition_spec.rb
|
154
|
+
- spec/rdkafka/consumer/topic_partition_list_spec.rb
|
148
155
|
- spec/rdkafka/consumer_spec.rb
|
149
156
|
- spec/rdkafka/error_spec.rb
|
150
|
-
- spec/rdkafka/ffi_spec.rb
|
151
157
|
- spec/rdkafka/producer/delivery_handle_spec.rb
|
152
158
|
- spec/rdkafka/producer/delivery_report_spec.rb
|
153
159
|
- spec/rdkafka/producer_spec.rb
|
data/spec/rdkafka/ffi_spec.rb
DELETED
@@ -1,13 +0,0 @@
|
|
1
|
-
require "spec_helper"
|
2
|
-
|
3
|
-
describe Rdkafka::FFI do
|
4
|
-
it "should load librdkafka" do
|
5
|
-
expect(Rdkafka::FFI.ffi_libraries.map(&:name).first).to include "librdkafka"
|
6
|
-
end
|
7
|
-
|
8
|
-
it "should successfully call librdkafka" do
|
9
|
-
expect {
|
10
|
-
Rdkafka::FFI.rd_kafka_conf_new
|
11
|
-
}.not_to raise_error
|
12
|
-
end
|
13
|
-
end
|