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.
@@ -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