rdkafka 0.4.2 → 0.8.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -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 timeout_in_seconds [Integer, nil] Number of seconds to wait before timing out. If this is nil it does not time out.
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(timeout_in_seconds=60)
39
- timeout = if timeout_in_seconds
40
- Time.now.to_i + timeout_in_seconds
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 <= Time.now.to_i
47
- raise WaitTimeoutError.new("Waiting for delivery timed out after #{timeout_in_seconds} seconds")
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 0.1
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 succesfully produced message.
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
@@ -1,5 +1,5 @@
1
1
  module Rdkafka
2
- VERSION = "0.4.2"
3
- LIBRDKAFKA_VERSION = "0.11.6"
4
- LIBRDKAFKA_SOURCE_SHA256 = '9c0afb8b53779d968225edf1e79da48a162895ad557900f75e7978f65e642032'
2
+ VERSION = "0.8.0"
3
+ LIBRDKAFKA_VERSION = "1.4.0"
4
+ LIBRDKAFKA_SOURCE_SHA256 = "ae27ea3f3d0d32d29004e7f709efbba2666c5383a107cc45b3a1949486b2eb84"
5
5
  end
@@ -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 wrapping librdkafka using the ffi gem and futures from concurrent-ruby for Kafka 0.10+"
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.1'
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', '~> 12.3'
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
- expect(Gem::Platform.local).to receive(:os).and_return("darwin-aaa")
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
- expect(Gem::Platform.local).to receive(:os).and_return("linux")
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
@@ -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
- expect(rdkafka_config.consumer).to be_a Rdkafka::Consumer
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
- expect(rdkafka_config.producer).to be_a Rdkafka::Producer
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(:native_topic) { new_native_topic }
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
- subject { Rdkafka::Consumer::Partition.new(1, offset) }
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 with offset 100>"
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 with offset 100>"
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 without offset>"
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 without offset>, <Partition 1 without offset>]}>"
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
- describe "#subscripe, #unsubscribe and #subscription" do
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 commited consumer" do
316
+ context "with a committed consumer" do
153
317
  before :all do
154
- # Make sure there are some message
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
- consumer.subscribe("consume_test_topic")
231
- wait_for_assignment(consumer)
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
- consumer.store_offset(message)
236
- consumer.commit
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 = consumer.committed(list).to_h["consume_test_topic"]
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
- consumer.store_offset(message)
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: "consume_test_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("consume_test_topic", 0, 5000)
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.poll(5000)
380
- expect(message).to be_a Rdkafka::Consumer::Message
565
+ message = consumer.each {|m| break m}
381
566
 
382
- # Message content is tested in producer spec
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