rdkafka 0.4.2 → 0.8.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/.travis.yml +17 -4
- data/CHANGELOG.md +23 -0
- data/README.md +5 -8
- data/docker-compose.yml +15 -11
- data/ext/README.md +3 -15
- data/ext/Rakefile +23 -3
- data/lib/rdkafka.rb +2 -0
- data/lib/rdkafka/bindings.rb +76 -2
- data/lib/rdkafka/config.rb +38 -7
- data/lib/rdkafka/consumer.rb +185 -40
- data/lib/rdkafka/consumer/headers.rb +63 -0
- data/lib/rdkafka/consumer/message.rb +11 -1
- data/lib/rdkafka/consumer/partition.rb +11 -6
- data/lib/rdkafka/consumer/topic_partition_list.rb +33 -30
- data/lib/rdkafka/error.rb +17 -0
- data/lib/rdkafka/metadata.rb +91 -0
- data/lib/rdkafka/producer.rb +54 -18
- data/lib/rdkafka/producer/delivery_handle.rb +12 -8
- data/lib/rdkafka/producer/delivery_report.rb +7 -2
- data/lib/rdkafka/version.rb +3 -3
- data/rdkafka.gemspec +3 -3
- data/spec/rdkafka/bindings_spec.rb +20 -2
- data/spec/rdkafka/config_spec.rb +6 -2
- data/spec/rdkafka/consumer/message_spec.rb +23 -1
- data/spec/rdkafka/consumer/partition_spec.rb +17 -4
- data/spec/rdkafka/consumer/topic_partition_list_spec.rb +1 -1
- data/spec/rdkafka/consumer_spec.rb +322 -16
- data/spec/rdkafka/error_spec.rb +3 -3
- data/spec/rdkafka/producer/delivery_handle_spec.rb +2 -2
- data/spec/rdkafka/producer/delivery_report_spec.rb +5 -1
- data/spec/rdkafka/producer_spec.rb +163 -68
- data/spec/spec_helper.rb +21 -3
- metadata +11 -9
@@ -10,6 +10,10 @@ module Rdkafka
|
|
10
10
|
|
11
11
|
REGISTRY = {}
|
12
12
|
|
13
|
+
CURRENT_TIME = -> { Process.clock_gettime(Process::CLOCK_MONOTONIC) }.freeze
|
14
|
+
|
15
|
+
private_constant :CURRENT_TIME
|
16
|
+
|
13
17
|
def self.register(address, handle)
|
14
18
|
REGISTRY[address] = handle
|
15
19
|
end
|
@@ -29,25 +33,25 @@ module Rdkafka
|
|
29
33
|
# If there is a timeout this does not mean the message is not delivered, rdkafka might still be working on delivering the message.
|
30
34
|
# In this case it is possible to call wait again.
|
31
35
|
#
|
32
|
-
# @param
|
36
|
+
# @param max_wait_timeout [Numeric, nil] Amount of time to wait before timing out. If this is nil it does not time out.
|
37
|
+
# @param wait_timeout [Numeric] Amount of time we should wait before we recheck if there is a delivery report available
|
33
38
|
#
|
34
39
|
# @raise [RdkafkaError] When delivering the message failed
|
35
40
|
# @raise [WaitTimeoutError] When the timeout has been reached and the handle is still pending
|
36
41
|
#
|
37
42
|
# @return [DeliveryReport]
|
38
|
-
def wait(
|
39
|
-
timeout = if
|
40
|
-
|
43
|
+
def wait(max_wait_timeout: 60, wait_timeout: 0.1)
|
44
|
+
timeout = if max_wait_timeout
|
45
|
+
CURRENT_TIME.call + max_wait_timeout
|
41
46
|
else
|
42
47
|
nil
|
43
48
|
end
|
44
49
|
loop do
|
45
50
|
if pending?
|
46
|
-
if timeout && timeout <=
|
47
|
-
raise WaitTimeoutError.new("Waiting for delivery timed out after #{
|
51
|
+
if timeout && timeout <= CURRENT_TIME.call
|
52
|
+
raise WaitTimeoutError.new("Waiting for delivery timed out after #{max_wait_timeout} seconds")
|
48
53
|
end
|
49
|
-
sleep
|
50
|
-
next
|
54
|
+
sleep wait_timeout
|
51
55
|
elsif self[:response] != 0
|
52
56
|
raise RdkafkaError.new(self[:response])
|
53
57
|
else
|
@@ -1,6 +1,6 @@
|
|
1
1
|
module Rdkafka
|
2
2
|
class Producer
|
3
|
-
# Delivery report for a
|
3
|
+
# Delivery report for a successfully produced message.
|
4
4
|
class DeliveryReport
|
5
5
|
# The partition this message was produced to.
|
6
6
|
# @return [Integer]
|
@@ -10,11 +10,16 @@ module Rdkafka
|
|
10
10
|
# @return [Integer]
|
11
11
|
attr_reader :offset
|
12
12
|
|
13
|
+
# Error in case happen during produce.
|
14
|
+
# @return [string]
|
15
|
+
attr_reader :error
|
16
|
+
|
13
17
|
private
|
14
18
|
|
15
|
-
def initialize(partition, offset)
|
19
|
+
def initialize(partition, offset, error = nil)
|
16
20
|
@partition = partition
|
17
21
|
@offset = offset
|
22
|
+
@error = error
|
18
23
|
end
|
19
24
|
end
|
20
25
|
end
|
data/lib/rdkafka/version.rb
CHANGED
@@ -1,5 +1,5 @@
|
|
1
1
|
module Rdkafka
|
2
|
-
VERSION = "0.
|
3
|
-
LIBRDKAFKA_VERSION = "
|
4
|
-
LIBRDKAFKA_SOURCE_SHA256 =
|
2
|
+
VERSION = "0.8.0"
|
3
|
+
LIBRDKAFKA_VERSION = "1.4.0"
|
4
|
+
LIBRDKAFKA_SOURCE_SHA256 = "ae27ea3f3d0d32d29004e7f709efbba2666c5383a107cc45b3a1949486b2eb84"
|
5
5
|
end
|
data/rdkafka.gemspec
CHANGED
@@ -4,7 +4,7 @@ Gem::Specification.new do |gem|
|
|
4
4
|
gem.authors = ['Thijs Cadier']
|
5
5
|
gem.email = ["thijs@appsignal.com"]
|
6
6
|
gem.description = "Modern Kafka client library for Ruby based on librdkafka"
|
7
|
-
gem.summary = "Kafka client library
|
7
|
+
gem.summary = "The rdkafka gem is a modern Kafka client library for Ruby based on librdkafka. It wraps the production-ready C client using the ffi gem and targets Kafka 1.0+ and Ruby 2.4+."
|
8
8
|
gem.license = 'MIT'
|
9
9
|
gem.homepage = 'https://github.com/thijsc/rdkafka-ruby'
|
10
10
|
|
@@ -14,12 +14,12 @@ Gem::Specification.new do |gem|
|
|
14
14
|
gem.name = 'rdkafka'
|
15
15
|
gem.require_paths = ['lib']
|
16
16
|
gem.version = Rdkafka::VERSION
|
17
|
-
gem.required_ruby_version = '>= 2.
|
17
|
+
gem.required_ruby_version = '>= 2.4'
|
18
18
|
gem.extensions = %w(ext/Rakefile)
|
19
19
|
|
20
20
|
gem.add_dependency 'ffi', '~> 1.9'
|
21
21
|
gem.add_dependency 'mini_portile2', '~> 2.1'
|
22
|
-
gem.add_dependency 'rake', '
|
22
|
+
gem.add_dependency 'rake', '>= 12.3'
|
23
23
|
|
24
24
|
gem.add_development_dependency 'pry', '~> 0.10'
|
25
25
|
gem.add_development_dependency 'rspec', '~> 3.5'
|
@@ -1,4 +1,5 @@
|
|
1
1
|
require "spec_helper"
|
2
|
+
require 'zlib'
|
2
3
|
|
3
4
|
describe Rdkafka::Bindings do
|
4
5
|
it "should load librdkafka" do
|
@@ -7,12 +8,12 @@ describe Rdkafka::Bindings do
|
|
7
8
|
|
8
9
|
describe ".lib_extension" do
|
9
10
|
it "should know the lib extension for darwin" do
|
10
|
-
|
11
|
+
stub_const('RbConfig::CONFIG', 'host_os' =>'darwin')
|
11
12
|
expect(Rdkafka::Bindings.lib_extension).to eq "dylib"
|
12
13
|
end
|
13
14
|
|
14
15
|
it "should know the lib extension for linux" do
|
15
|
-
|
16
|
+
stub_const('RbConfig::CONFIG', 'host_os' =>'linux')
|
16
17
|
expect(Rdkafka::Bindings.lib_extension).to eq "so"
|
17
18
|
end
|
18
19
|
end
|
@@ -60,6 +61,23 @@ describe Rdkafka::Bindings do
|
|
60
61
|
end
|
61
62
|
end
|
62
63
|
|
64
|
+
describe "partitioner" do
|
65
|
+
let(:partition_key) { ('a'..'z').to_a.shuffle.take(15).join('') }
|
66
|
+
let(:partition_count) { rand(50) + 1 }
|
67
|
+
|
68
|
+
it "should return the same partition for a similar string and the same partition count" do
|
69
|
+
result_1 = Rdkafka::Bindings.partitioner(partition_key, partition_count)
|
70
|
+
result_2 = Rdkafka::Bindings.partitioner(partition_key, partition_count)
|
71
|
+
expect(result_1).to eq(result_2)
|
72
|
+
end
|
73
|
+
|
74
|
+
it "should match the old partitioner" do
|
75
|
+
result_1 = Rdkafka::Bindings.partitioner(partition_key, partition_count)
|
76
|
+
result_2 = (Zlib.crc32(partition_key) % partition_count)
|
77
|
+
expect(result_1).to eq(result_2)
|
78
|
+
end
|
79
|
+
end
|
80
|
+
|
63
81
|
describe "stats callback" do
|
64
82
|
context "without a stats callback" do
|
65
83
|
it "should do nothing" do
|
data/spec/rdkafka/config_spec.rb
CHANGED
@@ -50,7 +50,9 @@ describe Rdkafka::Config do
|
|
50
50
|
end
|
51
51
|
|
52
52
|
it "should create a consumer with valid config" do
|
53
|
-
|
53
|
+
consumer = rdkafka_config.consumer
|
54
|
+
expect(consumer).to be_a Rdkafka::Consumer
|
55
|
+
consumer.close
|
54
56
|
end
|
55
57
|
|
56
58
|
it "should raise an error when creating a consumer with invalid config" do
|
@@ -76,7 +78,9 @@ describe Rdkafka::Config do
|
|
76
78
|
end
|
77
79
|
|
78
80
|
it "should create a producer with valid config" do
|
79
|
-
|
81
|
+
producer = rdkafka_config.producer
|
82
|
+
expect(producer).to be_a Rdkafka::Producer
|
83
|
+
producer.close
|
80
84
|
end
|
81
85
|
|
82
86
|
it "should raise an error when creating a producer with invalid config" do
|
@@ -1,7 +1,8 @@
|
|
1
1
|
require "spec_helper"
|
2
2
|
|
3
3
|
describe Rdkafka::Consumer::Message do
|
4
|
-
let(:
|
4
|
+
let(:native_client) { new_native_client }
|
5
|
+
let(:native_topic) { new_native_topic(native_client: native_client) }
|
5
6
|
let(:payload) { nil }
|
6
7
|
let(:key) { nil }
|
7
8
|
let(:native_message) do
|
@@ -23,8 +24,29 @@ describe Rdkafka::Consumer::Message do
|
|
23
24
|
end
|
24
25
|
end
|
25
26
|
end
|
27
|
+
|
28
|
+
after(:each) do
|
29
|
+
Rdkafka::Bindings.rd_kafka_destroy(native_client)
|
30
|
+
end
|
31
|
+
|
26
32
|
subject { Rdkafka::Consumer::Message.new(native_message) }
|
27
33
|
|
34
|
+
before do
|
35
|
+
# mock headers, because it produces 'segmentation fault' while settings or reading headers for
|
36
|
+
# a message which is created from scratch
|
37
|
+
#
|
38
|
+
# Code dump example:
|
39
|
+
#
|
40
|
+
# ```
|
41
|
+
# frame #7: 0x000000010dacf5ab librdkafka.dylib`rd_list_destroy + 11
|
42
|
+
# frame #8: 0x000000010dae5a7e librdkafka.dylib`rd_kafka_headers_destroy + 14
|
43
|
+
# frame #9: 0x000000010da9ab40 librdkafka.dylib`rd_kafka_message_set_headers + 32
|
44
|
+
# ```
|
45
|
+
expect( Rdkafka::Bindings).to receive(:rd_kafka_message_headers).with(any_args) do
|
46
|
+
Rdkafka::Bindings::RD_KAFKA_RESP_ERR__NOENT
|
47
|
+
end
|
48
|
+
end
|
49
|
+
|
28
50
|
it "should have a topic" do
|
29
51
|
expect(subject.topic).to eq "topic_name"
|
30
52
|
end
|
@@ -2,7 +2,8 @@ require "spec_helper"
|
|
2
2
|
|
3
3
|
describe Rdkafka::Consumer::Partition do
|
4
4
|
let(:offset) { 100 }
|
5
|
-
|
5
|
+
let(:err) { 0 }
|
6
|
+
subject { Rdkafka::Consumer::Partition.new(1, offset, err) }
|
6
7
|
|
7
8
|
it "should have a partition" do
|
8
9
|
expect(subject.partition).to eq 1
|
@@ -12,22 +13,34 @@ describe Rdkafka::Consumer::Partition do
|
|
12
13
|
expect(subject.offset).to eq 100
|
13
14
|
end
|
14
15
|
|
16
|
+
it "should have an err code" do
|
17
|
+
expect(subject.err).to eq 0
|
18
|
+
end
|
19
|
+
|
15
20
|
describe "#to_s" do
|
16
21
|
it "should return a human readable representation" do
|
17
|
-
expect(subject.to_s).to eq "<Partition 1
|
22
|
+
expect(subject.to_s).to eq "<Partition 1 offset=100>"
|
18
23
|
end
|
19
24
|
end
|
20
25
|
|
21
26
|
describe "#inspect" do
|
22
27
|
it "should return a human readable representation" do
|
23
|
-
expect(subject.to_s).to eq "<Partition 1
|
28
|
+
expect(subject.to_s).to eq "<Partition 1 offset=100>"
|
24
29
|
end
|
25
30
|
|
26
31
|
context "without offset" do
|
27
32
|
let(:offset) { nil }
|
28
33
|
|
29
34
|
it "should return a human readable representation" do
|
30
|
-
expect(subject.to_s).to eq "<Partition 1
|
35
|
+
expect(subject.to_s).to eq "<Partition 1>"
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
39
|
+
context "with err code" do
|
40
|
+
let(:err) { 1 }
|
41
|
+
|
42
|
+
it "should return a human readable representation" do
|
43
|
+
expect(subject.to_s).to eq "<Partition 1 offset=100 err=1>"
|
31
44
|
end
|
32
45
|
end
|
33
46
|
end
|
@@ -118,7 +118,7 @@ describe Rdkafka::Consumer::TopicPartitionList do
|
|
118
118
|
list = Rdkafka::Consumer::TopicPartitionList.new
|
119
119
|
list.add_topic("topic1", [0, 1])
|
120
120
|
|
121
|
-
expected = "<TopicPartitionList: {\"topic1\"=>[<Partition 0
|
121
|
+
expected = "<TopicPartitionList: {\"topic1\"=>[<Partition 0>, <Partition 1>]}>"
|
122
122
|
|
123
123
|
expect(list.to_s).to eq expected
|
124
124
|
end
|
@@ -1,11 +1,15 @@
|
|
1
1
|
require "spec_helper"
|
2
|
+
require "ostruct"
|
2
3
|
|
3
4
|
describe Rdkafka::Consumer do
|
4
5
|
let(:config) { rdkafka_config }
|
5
6
|
let(:consumer) { config.consumer }
|
6
7
|
let(:producer) { config.producer }
|
7
8
|
|
8
|
-
|
9
|
+
after { consumer.close }
|
10
|
+
after { producer.close }
|
11
|
+
|
12
|
+
describe "#subscribe, #unsubscribe and #subscription" do
|
9
13
|
it "should subscribe, unsubscribe and return the subscription" do
|
10
14
|
expect(consumer.subscription).to be_empty
|
11
15
|
|
@@ -47,6 +51,166 @@ describe Rdkafka::Consumer do
|
|
47
51
|
end
|
48
52
|
end
|
49
53
|
|
54
|
+
describe "#pause and #resume" do
|
55
|
+
context "subscription" do
|
56
|
+
let(:timeout) { 1000 }
|
57
|
+
|
58
|
+
before { consumer.subscribe("consume_test_topic") }
|
59
|
+
after { consumer.unsubscribe }
|
60
|
+
|
61
|
+
it "should pause and then resume" do
|
62
|
+
# 1. partitions are assigned
|
63
|
+
wait_for_assignment(consumer)
|
64
|
+
expect(consumer.assignment).not_to be_empty
|
65
|
+
|
66
|
+
# 2. send a first message
|
67
|
+
send_one_message
|
68
|
+
|
69
|
+
# 3. ensure that message is successfully consumed
|
70
|
+
records = consumer.poll(timeout)
|
71
|
+
expect(records).not_to be_nil
|
72
|
+
consumer.commit
|
73
|
+
|
74
|
+
# 4. send a second message
|
75
|
+
send_one_message
|
76
|
+
|
77
|
+
# 5. pause the subscription
|
78
|
+
tpl = Rdkafka::Consumer::TopicPartitionList.new
|
79
|
+
tpl.add_topic("consume_test_topic", (0..2))
|
80
|
+
consumer.pause(tpl)
|
81
|
+
|
82
|
+
# 6. ensure that messages are not available
|
83
|
+
records = consumer.poll(timeout)
|
84
|
+
expect(records).to be_nil
|
85
|
+
|
86
|
+
# 7. resume the subscription
|
87
|
+
tpl = Rdkafka::Consumer::TopicPartitionList.new
|
88
|
+
tpl.add_topic("consume_test_topic", (0..2))
|
89
|
+
consumer.resume(tpl)
|
90
|
+
|
91
|
+
# 8. ensure that message is successfully consumed
|
92
|
+
records = consumer.poll(timeout)
|
93
|
+
expect(records).not_to be_nil
|
94
|
+
end
|
95
|
+
end
|
96
|
+
|
97
|
+
it "should raise when not TopicPartitionList" do
|
98
|
+
expect { consumer.pause(true) }.to raise_error(TypeError)
|
99
|
+
expect { consumer.resume(true) }.to raise_error(TypeError)
|
100
|
+
end
|
101
|
+
|
102
|
+
it "should raise an error when pausing fails" do
|
103
|
+
list = Rdkafka::Consumer::TopicPartitionList.new.tap { |tpl| tpl.add_topic('topic', (0..1)) }
|
104
|
+
|
105
|
+
expect(Rdkafka::Bindings).to receive(:rd_kafka_pause_partitions).and_return(20)
|
106
|
+
expect {
|
107
|
+
consumer.pause(list)
|
108
|
+
}.to raise_error do |err|
|
109
|
+
expect(err).to be_instance_of(Rdkafka::RdkafkaTopicPartitionListError)
|
110
|
+
expect(err.topic_partition_list).to be
|
111
|
+
end
|
112
|
+
end
|
113
|
+
|
114
|
+
it "should raise an error when resume fails" do
|
115
|
+
expect(Rdkafka::Bindings).to receive(:rd_kafka_resume_partitions).and_return(20)
|
116
|
+
expect {
|
117
|
+
consumer.resume(Rdkafka::Consumer::TopicPartitionList.new)
|
118
|
+
}.to raise_error Rdkafka::RdkafkaError
|
119
|
+
end
|
120
|
+
|
121
|
+
def send_one_message
|
122
|
+
producer.produce(
|
123
|
+
topic: "consume_test_topic",
|
124
|
+
payload: "payload 1",
|
125
|
+
key: "key 1"
|
126
|
+
).wait
|
127
|
+
end
|
128
|
+
end
|
129
|
+
|
130
|
+
describe "#seek" do
|
131
|
+
it "should raise an error when seeking fails" do
|
132
|
+
fake_msg = OpenStruct.new(topic: "consume_test_topic", partition: 0, offset: 0)
|
133
|
+
|
134
|
+
expect(Rdkafka::Bindings).to receive(:rd_kafka_seek).and_return(20)
|
135
|
+
expect {
|
136
|
+
consumer.seek(fake_msg)
|
137
|
+
}.to raise_error Rdkafka::RdkafkaError
|
138
|
+
end
|
139
|
+
|
140
|
+
context "subscription" do
|
141
|
+
let(:timeout) { 1000 }
|
142
|
+
|
143
|
+
before do
|
144
|
+
consumer.subscribe("consume_test_topic")
|
145
|
+
|
146
|
+
# 1. partitions are assigned
|
147
|
+
wait_for_assignment(consumer)
|
148
|
+
expect(consumer.assignment).not_to be_empty
|
149
|
+
|
150
|
+
# 2. eat unrelated messages
|
151
|
+
while(consumer.poll(timeout)) do; end
|
152
|
+
end
|
153
|
+
after { consumer.unsubscribe }
|
154
|
+
|
155
|
+
def send_one_message(val)
|
156
|
+
producer.produce(
|
157
|
+
topic: "consume_test_topic",
|
158
|
+
payload: "payload #{val}",
|
159
|
+
key: "key 1",
|
160
|
+
partition: 0
|
161
|
+
).wait
|
162
|
+
end
|
163
|
+
|
164
|
+
it "works when a partition is paused" do
|
165
|
+
# 3. get reference message
|
166
|
+
send_one_message(:a)
|
167
|
+
message1 = consumer.poll(timeout)
|
168
|
+
expect(message1&.payload).to eq "payload a"
|
169
|
+
|
170
|
+
# 4. pause the subscription
|
171
|
+
tpl = Rdkafka::Consumer::TopicPartitionList.new
|
172
|
+
tpl.add_topic("consume_test_topic", 1)
|
173
|
+
consumer.pause(tpl)
|
174
|
+
|
175
|
+
# 5. seek to previous message
|
176
|
+
consumer.seek(message1)
|
177
|
+
|
178
|
+
# 6. resume the subscription
|
179
|
+
tpl = Rdkafka::Consumer::TopicPartitionList.new
|
180
|
+
tpl.add_topic("consume_test_topic", 1)
|
181
|
+
consumer.resume(tpl)
|
182
|
+
|
183
|
+
# 7. ensure same message is read again
|
184
|
+
message2 = consumer.poll(timeout)
|
185
|
+
consumer.commit
|
186
|
+
expect(message1.offset).to eq message2.offset
|
187
|
+
expect(message1.payload).to eq message2.payload
|
188
|
+
end
|
189
|
+
|
190
|
+
it "allows skipping messages" do
|
191
|
+
# 3. send messages
|
192
|
+
send_one_message(:a)
|
193
|
+
send_one_message(:b)
|
194
|
+
send_one_message(:c)
|
195
|
+
|
196
|
+
# 4. get reference message
|
197
|
+
message = consumer.poll(timeout)
|
198
|
+
expect(message&.payload).to eq "payload a"
|
199
|
+
|
200
|
+
# 5. seek over one message
|
201
|
+
fake_msg = message.dup
|
202
|
+
fake_msg.instance_variable_set(:@offset, fake_msg.offset + 2)
|
203
|
+
consumer.seek(fake_msg)
|
204
|
+
|
205
|
+
# 6. ensure that only one message is available
|
206
|
+
records = consumer.poll(timeout)
|
207
|
+
expect(records&.payload).to eq "payload c"
|
208
|
+
records = consumer.poll(timeout)
|
209
|
+
expect(records).to be_nil
|
210
|
+
end
|
211
|
+
end
|
212
|
+
end
|
213
|
+
|
50
214
|
describe "#assign and #assignment" do
|
51
215
|
it "should return an empty assignment if nothing is assigned" do
|
52
216
|
expect(consumer.assignment).to be_empty
|
@@ -149,11 +313,11 @@ describe Rdkafka::Consumer do
|
|
149
313
|
}.to raise_error TypeError
|
150
314
|
end
|
151
315
|
|
152
|
-
context "with a
|
316
|
+
context "with a committed consumer" do
|
153
317
|
before :all do
|
154
|
-
# Make sure there are some
|
155
|
-
producer = rdkafka_config.producer
|
318
|
+
# Make sure there are some messages.
|
156
319
|
handles = []
|
320
|
+
producer = rdkafka_config.producer
|
157
321
|
10.times do
|
158
322
|
(0..2).each do |i|
|
159
323
|
handles << producer.produce(
|
@@ -165,6 +329,7 @@ describe Rdkafka::Consumer do
|
|
165
329
|
end
|
166
330
|
end
|
167
331
|
handles.each(&:wait)
|
332
|
+
producer.close
|
168
333
|
end
|
169
334
|
|
170
335
|
before do
|
@@ -225,20 +390,26 @@ describe Rdkafka::Consumer do
|
|
225
390
|
|
226
391
|
describe "#store_offset" do
|
227
392
|
before do
|
393
|
+
config = {}
|
228
394
|
config[:'enable.auto.offset.store'] = false
|
229
395
|
config[:'enable.auto.commit'] = false
|
230
|
-
|
231
|
-
|
396
|
+
@new_consumer = rdkafka_config(config).consumer
|
397
|
+
@new_consumer.subscribe("consume_test_topic")
|
398
|
+
wait_for_assignment(@new_consumer)
|
399
|
+
end
|
400
|
+
|
401
|
+
after do
|
402
|
+
@new_consumer.close
|
232
403
|
end
|
233
404
|
|
234
405
|
it "should store the offset for a message" do
|
235
|
-
|
236
|
-
|
406
|
+
@new_consumer.store_offset(message)
|
407
|
+
@new_consumer.commit
|
237
408
|
|
238
409
|
list = Rdkafka::Consumer::TopicPartitionList.new.tap do |list|
|
239
410
|
list.add_topic("consume_test_topic", [0, 1, 2])
|
240
411
|
end
|
241
|
-
partitions =
|
412
|
+
partitions = @new_consumer.committed(list).to_h["consume_test_topic"]
|
242
413
|
expect(partitions).not_to be_nil
|
243
414
|
expect(partitions[message.partition].offset).to eq(message.offset + 1)
|
244
415
|
end
|
@@ -246,7 +417,7 @@ describe Rdkafka::Consumer do
|
|
246
417
|
it "should raise an error with invalid input" do
|
247
418
|
allow(message).to receive(:partition).and_return(9999)
|
248
419
|
expect {
|
249
|
-
|
420
|
+
@new_consumer.store_offset(message)
|
250
421
|
}.to raise_error Rdkafka::RdkafkaError
|
251
422
|
end
|
252
423
|
end
|
@@ -257,13 +428,13 @@ describe Rdkafka::Consumer do
|
|
257
428
|
it "should return the watermark offsets" do
|
258
429
|
# Make sure there's a message
|
259
430
|
producer.produce(
|
260
|
-
topic: "
|
431
|
+
topic: "watermarks_test_topic",
|
261
432
|
payload: "payload 1",
|
262
433
|
key: "key 1",
|
263
434
|
partition: 0
|
264
435
|
).wait
|
265
436
|
|
266
|
-
low, high = consumer.query_watermark_offsets("
|
437
|
+
low, high = consumer.query_watermark_offsets("watermarks_test_topic", 0, 5000)
|
267
438
|
expect(low).to eq 0
|
268
439
|
expect(high).to be > 0
|
269
440
|
end
|
@@ -358,6 +529,22 @@ describe Rdkafka::Consumer do
|
|
358
529
|
end
|
359
530
|
end
|
360
531
|
|
532
|
+
describe "#cluster_id" do
|
533
|
+
it 'should return the current ClusterId' do
|
534
|
+
consumer.subscribe("consume_test_topic")
|
535
|
+
wait_for_assignment(consumer)
|
536
|
+
expect(consumer.cluster_id).not_to be_empty
|
537
|
+
end
|
538
|
+
end
|
539
|
+
|
540
|
+
describe "#member_id" do
|
541
|
+
it 'should return the current MemberId' do
|
542
|
+
consumer.subscribe("consume_test_topic")
|
543
|
+
wait_for_assignment(consumer)
|
544
|
+
expect(consumer.member_id).to start_with('rdkafka-')
|
545
|
+
end
|
546
|
+
end
|
547
|
+
|
361
548
|
describe "#poll" do
|
362
549
|
it "should return nil if there is no subscription" do
|
363
550
|
expect(consumer.poll(1000)).to be_nil
|
@@ -374,12 +561,12 @@ describe Rdkafka::Consumer do
|
|
374
561
|
payload: "payload 1",
|
375
562
|
key: "key 1"
|
376
563
|
).wait
|
377
|
-
|
378
564
|
consumer.subscribe("consume_test_topic")
|
379
|
-
message = consumer.
|
380
|
-
expect(message).to be_a Rdkafka::Consumer::Message
|
565
|
+
message = consumer.each {|m| break m}
|
381
566
|
|
382
|
-
|
567
|
+
expect(message).to be_a Rdkafka::Consumer::Message
|
568
|
+
expect(message.payload).to eq('payload 1')
|
569
|
+
expect(message.key).to eq('key 1')
|
383
570
|
end
|
384
571
|
|
385
572
|
it "should raise an error when polling fails" do
|
@@ -395,6 +582,68 @@ describe Rdkafka::Consumer do
|
|
395
582
|
end
|
396
583
|
end
|
397
584
|
|
585
|
+
describe "#poll with headers" do
|
586
|
+
it "should return message with headers" do
|
587
|
+
report = producer.produce(
|
588
|
+
topic: "consume_test_topic",
|
589
|
+
key: "key headers",
|
590
|
+
headers: { foo: 'bar' }
|
591
|
+
).wait
|
592
|
+
|
593
|
+
message = wait_for_message(topic: "consume_test_topic", consumer: consumer, delivery_report: report)
|
594
|
+
expect(message).to be
|
595
|
+
expect(message.key).to eq('key headers')
|
596
|
+
expect(message.headers).to include(foo: 'bar')
|
597
|
+
end
|
598
|
+
|
599
|
+
it "should return message with no headers" do
|
600
|
+
report = producer.produce(
|
601
|
+
topic: "consume_test_topic",
|
602
|
+
key: "key no headers",
|
603
|
+
headers: nil
|
604
|
+
).wait
|
605
|
+
|
606
|
+
message = wait_for_message(topic: "consume_test_topic", consumer: consumer, delivery_report: report)
|
607
|
+
expect(message).to be
|
608
|
+
expect(message.key).to eq('key no headers')
|
609
|
+
expect(message.headers).to be_empty
|
610
|
+
end
|
611
|
+
|
612
|
+
it "should raise an error when message headers aren't readable" do
|
613
|
+
expect(Rdkafka::Bindings).to receive(:rd_kafka_message_headers).with(any_args) { 1 }
|
614
|
+
|
615
|
+
report = producer.produce(
|
616
|
+
topic: "consume_test_topic",
|
617
|
+
key: "key err headers",
|
618
|
+
headers: nil
|
619
|
+
).wait
|
620
|
+
|
621
|
+
expect {
|
622
|
+
wait_for_message(topic: "consume_test_topic", consumer: consumer, delivery_report: report)
|
623
|
+
}.to raise_error do |err|
|
624
|
+
expect(err).to be_instance_of(Rdkafka::RdkafkaError)
|
625
|
+
expect(err.message).to start_with("Error reading message headers")
|
626
|
+
end
|
627
|
+
end
|
628
|
+
|
629
|
+
it "should raise an error when the first message header aren't readable" do
|
630
|
+
expect(Rdkafka::Bindings).to receive(:rd_kafka_header_get_all).with(any_args) { 1 }
|
631
|
+
|
632
|
+
report = producer.produce(
|
633
|
+
topic: "consume_test_topic",
|
634
|
+
key: "key err headers",
|
635
|
+
headers: { foo: 'bar' }
|
636
|
+
).wait
|
637
|
+
|
638
|
+
expect {
|
639
|
+
wait_for_message(topic: "consume_test_topic", consumer: consumer, delivery_report: report)
|
640
|
+
}.to raise_error do |err|
|
641
|
+
expect(err).to be_instance_of(Rdkafka::RdkafkaError)
|
642
|
+
expect(err.message).to start_with("Error reading a message header at index 0")
|
643
|
+
end
|
644
|
+
end
|
645
|
+
end
|
646
|
+
|
398
647
|
describe "#each" do
|
399
648
|
it "should yield messages" do
|
400
649
|
handles = []
|
@@ -417,4 +666,61 @@ describe Rdkafka::Consumer do
|
|
417
666
|
end
|
418
667
|
end
|
419
668
|
end
|
669
|
+
|
670
|
+
describe "a rebalance listener" do
|
671
|
+
it "should get notifications" do
|
672
|
+
listener = Struct.new(:queue) do
|
673
|
+
def on_partitions_assigned(consumer, list)
|
674
|
+
collect(:assign, list)
|
675
|
+
end
|
676
|
+
|
677
|
+
def on_partitions_revoked(consumer, list)
|
678
|
+
collect(:revoke, list)
|
679
|
+
end
|
680
|
+
|
681
|
+
def collect(name, list)
|
682
|
+
partitions = list.to_h.map { |key, values| [key, values.map(&:partition)] }.flatten
|
683
|
+
queue << ([name] + partitions)
|
684
|
+
end
|
685
|
+
end.new([])
|
686
|
+
|
687
|
+
notify_listener(listener)
|
688
|
+
|
689
|
+
expect(listener.queue).to eq([
|
690
|
+
[:assign, "consume_test_topic", 0, 1, 2],
|
691
|
+
[:revoke, "consume_test_topic", 0, 1, 2]
|
692
|
+
])
|
693
|
+
end
|
694
|
+
|
695
|
+
it 'should handle callback exceptions' do
|
696
|
+
listener = Struct.new(:queue) do
|
697
|
+
def on_partitions_assigned(consumer, list)
|
698
|
+
queue << :assigned
|
699
|
+
raise 'boom'
|
700
|
+
end
|
701
|
+
|
702
|
+
def on_partitions_revoked(consumer, list)
|
703
|
+
queue << :revoked
|
704
|
+
raise 'boom'
|
705
|
+
end
|
706
|
+
end.new([])
|
707
|
+
|
708
|
+
notify_listener(listener)
|
709
|
+
|
710
|
+
expect(listener.queue).to eq([:assigned, :revoked])
|
711
|
+
end
|
712
|
+
|
713
|
+
def notify_listener(listener)
|
714
|
+
# 1. subscribe and poll
|
715
|
+
config.consumer_rebalance_listener = listener
|
716
|
+
consumer.subscribe("consume_test_topic")
|
717
|
+
wait_for_assignment(consumer)
|
718
|
+
consumer.poll(100)
|
719
|
+
|
720
|
+
# 2. unsubscribe
|
721
|
+
consumer.unsubscribe
|
722
|
+
wait_for_unassignment(consumer)
|
723
|
+
consumer.close
|
724
|
+
end
|
725
|
+
end
|
420
726
|
end
|