karafka-rdkafka 0.12.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.
Files changed (62) hide show
  1. checksums.yaml +7 -0
  2. checksums.yaml.gz.sig +2 -0
  3. data/.gitignore +8 -0
  4. data/.rspec +1 -0
  5. data/.semaphore/semaphore.yml +23 -0
  6. data/.yardopts +2 -0
  7. data/CHANGELOG.md +104 -0
  8. data/Gemfile +3 -0
  9. data/Guardfile +19 -0
  10. data/LICENSE +21 -0
  11. data/README.md +114 -0
  12. data/Rakefile +96 -0
  13. data/bin/console +11 -0
  14. data/docker-compose.yml +24 -0
  15. data/ext/README.md +18 -0
  16. data/ext/Rakefile +62 -0
  17. data/lib/rdkafka/abstract_handle.rb +82 -0
  18. data/lib/rdkafka/admin/create_topic_handle.rb +27 -0
  19. data/lib/rdkafka/admin/create_topic_report.rb +22 -0
  20. data/lib/rdkafka/admin/delete_topic_handle.rb +27 -0
  21. data/lib/rdkafka/admin/delete_topic_report.rb +22 -0
  22. data/lib/rdkafka/admin.rb +155 -0
  23. data/lib/rdkafka/bindings.rb +312 -0
  24. data/lib/rdkafka/callbacks.rb +106 -0
  25. data/lib/rdkafka/config.rb +299 -0
  26. data/lib/rdkafka/consumer/headers.rb +63 -0
  27. data/lib/rdkafka/consumer/message.rb +84 -0
  28. data/lib/rdkafka/consumer/partition.rb +49 -0
  29. data/lib/rdkafka/consumer/topic_partition_list.rb +164 -0
  30. data/lib/rdkafka/consumer.rb +565 -0
  31. data/lib/rdkafka/error.rb +86 -0
  32. data/lib/rdkafka/metadata.rb +92 -0
  33. data/lib/rdkafka/producer/client.rb +47 -0
  34. data/lib/rdkafka/producer/delivery_handle.rb +22 -0
  35. data/lib/rdkafka/producer/delivery_report.rb +26 -0
  36. data/lib/rdkafka/producer.rb +178 -0
  37. data/lib/rdkafka/version.rb +5 -0
  38. data/lib/rdkafka.rb +22 -0
  39. data/rdkafka.gemspec +36 -0
  40. data/spec/rdkafka/abstract_handle_spec.rb +113 -0
  41. data/spec/rdkafka/admin/create_topic_handle_spec.rb +52 -0
  42. data/spec/rdkafka/admin/create_topic_report_spec.rb +16 -0
  43. data/spec/rdkafka/admin/delete_topic_handle_spec.rb +52 -0
  44. data/spec/rdkafka/admin/delete_topic_report_spec.rb +16 -0
  45. data/spec/rdkafka/admin_spec.rb +203 -0
  46. data/spec/rdkafka/bindings_spec.rb +134 -0
  47. data/spec/rdkafka/callbacks_spec.rb +20 -0
  48. data/spec/rdkafka/config_spec.rb +182 -0
  49. data/spec/rdkafka/consumer/message_spec.rb +139 -0
  50. data/spec/rdkafka/consumer/partition_spec.rb +57 -0
  51. data/spec/rdkafka/consumer/topic_partition_list_spec.rb +223 -0
  52. data/spec/rdkafka/consumer_spec.rb +1008 -0
  53. data/spec/rdkafka/error_spec.rb +89 -0
  54. data/spec/rdkafka/metadata_spec.rb +78 -0
  55. data/spec/rdkafka/producer/client_spec.rb +145 -0
  56. data/spec/rdkafka/producer/delivery_handle_spec.rb +42 -0
  57. data/spec/rdkafka/producer/delivery_report_spec.rb +17 -0
  58. data/spec/rdkafka/producer_spec.rb +525 -0
  59. data/spec/spec_helper.rb +139 -0
  60. data.tar.gz.sig +0 -0
  61. metadata +277 -0
  62. metadata.gz.sig +0 -0
@@ -0,0 +1,1008 @@
1
+ require "spec_helper"
2
+ require "ostruct"
3
+ require 'securerandom'
4
+
5
+ describe Rdkafka::Consumer do
6
+ let(:consumer) { rdkafka_consumer_config.consumer }
7
+ let(:producer) { rdkafka_producer_config.producer }
8
+
9
+ after { consumer.close }
10
+ after { producer.close }
11
+
12
+ describe "#subscribe, #unsubscribe and #subscription" do
13
+ it "should subscribe, unsubscribe and return the subscription" do
14
+ expect(consumer.subscription).to be_empty
15
+
16
+ consumer.subscribe("consume_test_topic")
17
+
18
+ expect(consumer.subscription).not_to be_empty
19
+ expected_subscription = Rdkafka::Consumer::TopicPartitionList.new.tap do |list|
20
+ list.add_topic("consume_test_topic")
21
+ end
22
+ expect(consumer.subscription).to eq expected_subscription
23
+
24
+ consumer.unsubscribe
25
+
26
+ expect(consumer.subscription).to be_empty
27
+ end
28
+
29
+ it "should raise an error when subscribing fails" do
30
+ expect(Rdkafka::Bindings).to receive(:rd_kafka_subscribe).and_return(20)
31
+
32
+ expect {
33
+ consumer.subscribe("consume_test_topic")
34
+ }.to raise_error(Rdkafka::RdkafkaError)
35
+ end
36
+
37
+ it "should raise an error when unsubscribing fails" do
38
+ expect(Rdkafka::Bindings).to receive(:rd_kafka_unsubscribe).and_return(20)
39
+
40
+ expect {
41
+ consumer.unsubscribe
42
+ }.to raise_error(Rdkafka::RdkafkaError)
43
+ end
44
+
45
+ it "should raise an error when fetching the subscription fails" do
46
+ expect(Rdkafka::Bindings).to receive(:rd_kafka_subscription).and_return(20)
47
+
48
+ expect {
49
+ consumer.subscription
50
+ }.to raise_error(Rdkafka::RdkafkaError)
51
+ end
52
+ end
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
+
214
+ describe "#assign and #assignment" do
215
+ it "should return an empty assignment if nothing is assigned" do
216
+ expect(consumer.assignment).to be_empty
217
+ end
218
+
219
+ it "should only accept a topic partition list in assign" do
220
+ expect {
221
+ consumer.assign("list")
222
+ }.to raise_error TypeError
223
+ end
224
+
225
+ it "should raise an error when assigning fails" do
226
+ expect(Rdkafka::Bindings).to receive(:rd_kafka_assign).and_return(20)
227
+ expect {
228
+ consumer.assign(Rdkafka::Consumer::TopicPartitionList.new)
229
+ }.to raise_error Rdkafka::RdkafkaError
230
+ end
231
+
232
+ it "should assign specific topic/partitions and return that assignment" do
233
+ tpl = Rdkafka::Consumer::TopicPartitionList.new
234
+ tpl.add_topic("consume_test_topic", (0..2))
235
+ consumer.assign(tpl)
236
+
237
+ assignment = consumer.assignment
238
+ expect(assignment).not_to be_empty
239
+ expect(assignment.to_h["consume_test_topic"].length).to eq 3
240
+ end
241
+
242
+ it "should return the assignment when subscribed" do
243
+ # Make sure there's a message
244
+ producer.produce(
245
+ topic: "consume_test_topic",
246
+ payload: "payload 1",
247
+ key: "key 1",
248
+ partition: 0
249
+ ).wait
250
+
251
+ # Subscribe and poll until partitions are assigned
252
+ consumer.subscribe("consume_test_topic")
253
+ 100.times do
254
+ consumer.poll(100)
255
+ break unless consumer.assignment.empty?
256
+ end
257
+
258
+ assignment = consumer.assignment
259
+ expect(assignment).not_to be_empty
260
+ expect(assignment.to_h["consume_test_topic"].length).to eq 3
261
+ end
262
+
263
+ it "should raise an error when getting assignment fails" do
264
+ expect(Rdkafka::Bindings).to receive(:rd_kafka_assignment).and_return(20)
265
+ expect {
266
+ consumer.assignment
267
+ }.to raise_error Rdkafka::RdkafkaError
268
+ end
269
+ end
270
+
271
+ describe "#close" do
272
+ it "should close a consumer" do
273
+ consumer.subscribe("consume_test_topic")
274
+ 100.times do |i|
275
+ producer.produce(
276
+ topic: "consume_test_topic",
277
+ payload: "payload #{i}",
278
+ key: "key #{i}",
279
+ partition: 0
280
+ ).wait
281
+ end
282
+ consumer.close
283
+ expect {
284
+ consumer.poll(100)
285
+ }.to raise_error(Rdkafka::ClosedConsumerError, /poll/)
286
+ end
287
+ end
288
+
289
+ describe "#commit, #committed and #store_offset" do
290
+ # Make sure there's a stored offset
291
+ let!(:report) do
292
+ producer.produce(
293
+ topic: "consume_test_topic",
294
+ payload: "payload 1",
295
+ key: "key 1",
296
+ partition: 0
297
+ ).wait
298
+ end
299
+
300
+ let(:message) do
301
+ wait_for_message(
302
+ topic: "consume_test_topic",
303
+ delivery_report: report,
304
+ consumer: consumer
305
+ )
306
+ end
307
+
308
+ it "should only accept a topic partition list in committed" do
309
+ expect {
310
+ consumer.committed("list")
311
+ }.to raise_error TypeError
312
+ end
313
+
314
+ it "should commit in sync mode" do
315
+ expect {
316
+ consumer.commit(nil, true)
317
+ }.not_to raise_error
318
+ end
319
+
320
+ it "should only accept a topic partition list in commit if not nil" do
321
+ expect {
322
+ consumer.commit("list")
323
+ }.to raise_error TypeError
324
+ end
325
+
326
+ context "with a committed consumer" do
327
+ before :all do
328
+ # Make sure there are some messages.
329
+ handles = []
330
+ producer = rdkafka_producer_config.producer
331
+ 10.times do
332
+ (0..2).each do |i|
333
+ handles << producer.produce(
334
+ topic: "consume_test_topic",
335
+ payload: "payload 1",
336
+ key: "key 1",
337
+ partition: i
338
+ )
339
+ end
340
+ end
341
+ handles.each(&:wait)
342
+ producer.close
343
+ end
344
+
345
+ before do
346
+ consumer.subscribe("consume_test_topic")
347
+ wait_for_assignment(consumer)
348
+ list = Rdkafka::Consumer::TopicPartitionList.new.tap do |list|
349
+ list.add_topic_and_partitions_with_offsets("consume_test_topic", 0 => 1, 1 => 1, 2 => 1)
350
+ end
351
+ consumer.commit(list)
352
+ end
353
+
354
+ it "should commit a specific topic partion list" do
355
+ list = Rdkafka::Consumer::TopicPartitionList.new.tap do |list|
356
+ list.add_topic_and_partitions_with_offsets("consume_test_topic", 0 => 1, 1 => 2, 2 => 3)
357
+ end
358
+ consumer.commit(list)
359
+
360
+ partitions = consumer.committed(list).to_h["consume_test_topic"]
361
+ expect(partitions[0].offset).to eq 1
362
+ expect(partitions[1].offset).to eq 2
363
+ expect(partitions[2].offset).to eq 3
364
+ end
365
+
366
+ it "should raise an error when committing fails" do
367
+ expect(Rdkafka::Bindings).to receive(:rd_kafka_commit).and_return(20)
368
+
369
+ expect {
370
+ consumer.commit
371
+ }.to raise_error(Rdkafka::RdkafkaError)
372
+ end
373
+
374
+ it "should fetch the committed offsets for the current assignment" do
375
+ partitions = consumer.committed.to_h["consume_test_topic"]
376
+ expect(partitions).not_to be_nil
377
+ expect(partitions[0].offset).to eq 1
378
+ end
379
+
380
+ it "should fetch the committed offsets for a specified topic partition list" do
381
+ list = Rdkafka::Consumer::TopicPartitionList.new.tap do |list|
382
+ list.add_topic("consume_test_topic", [0, 1, 2])
383
+ end
384
+ partitions = consumer.committed(list).to_h["consume_test_topic"]
385
+ expect(partitions).not_to be_nil
386
+ expect(partitions[0].offset).to eq 1
387
+ expect(partitions[1].offset).to eq 1
388
+ expect(partitions[2].offset).to eq 1
389
+ end
390
+
391
+ it "should raise an error when getting committed fails" do
392
+ expect(Rdkafka::Bindings).to receive(:rd_kafka_committed).and_return(20)
393
+ list = Rdkafka::Consumer::TopicPartitionList.new.tap do |list|
394
+ list.add_topic("consume_test_topic", [0, 1, 2])
395
+ end
396
+ expect {
397
+ consumer.committed(list)
398
+ }.to raise_error Rdkafka::RdkafkaError
399
+ end
400
+
401
+ describe "#store_offset" do
402
+ before do
403
+ config = {}
404
+ config[:'enable.auto.offset.store'] = false
405
+ config[:'enable.auto.commit'] = false
406
+ @new_consumer = rdkafka_consumer_config(config).consumer
407
+ @new_consumer.subscribe("consume_test_topic")
408
+ wait_for_assignment(@new_consumer)
409
+ end
410
+
411
+ after do
412
+ @new_consumer.close
413
+ end
414
+
415
+ it "should store the offset for a message" do
416
+ @new_consumer.store_offset(message)
417
+ @new_consumer.commit
418
+
419
+ list = Rdkafka::Consumer::TopicPartitionList.new.tap do |list|
420
+ list.add_topic("consume_test_topic", [0, 1, 2])
421
+ end
422
+ partitions = @new_consumer.committed(list).to_h["consume_test_topic"]
423
+ expect(partitions).not_to be_nil
424
+ expect(partitions[message.partition].offset).to eq(message.offset + 1)
425
+ end
426
+
427
+ it "should raise an error with invalid input" do
428
+ allow(message).to receive(:partition).and_return(9999)
429
+ expect {
430
+ @new_consumer.store_offset(message)
431
+ }.to raise_error Rdkafka::RdkafkaError
432
+ end
433
+ end
434
+ end
435
+ end
436
+
437
+ describe "#query_watermark_offsets" do
438
+ it "should return the watermark offsets" do
439
+ # Make sure there's a message
440
+ producer.produce(
441
+ topic: "watermarks_test_topic",
442
+ payload: "payload 1",
443
+ key: "key 1",
444
+ partition: 0
445
+ ).wait
446
+
447
+ low, high = consumer.query_watermark_offsets("watermarks_test_topic", 0, 5000)
448
+ expect(low).to eq 0
449
+ expect(high).to be > 0
450
+ end
451
+
452
+ it "should raise an error when querying offsets fails" do
453
+ expect(Rdkafka::Bindings).to receive(:rd_kafka_query_watermark_offsets).and_return(20)
454
+ expect {
455
+ consumer.query_watermark_offsets("consume_test_topic", 0, 5000)
456
+ }.to raise_error Rdkafka::RdkafkaError
457
+ end
458
+ end
459
+
460
+ describe "#lag" do
461
+ let(:consumer) { rdkafka_consumer_config(:"enable.partition.eof" => true).consumer }
462
+
463
+ it "should calculate the consumer lag" do
464
+ # Make sure there's a message in every partition and
465
+ # wait for the message to make sure everything is committed.
466
+ (0..2).each do |i|
467
+ producer.produce(
468
+ topic: "consume_test_topic",
469
+ key: "key lag #{i}",
470
+ partition: i
471
+ ).wait
472
+ end
473
+
474
+ # Consume to the end
475
+ consumer.subscribe("consume_test_topic")
476
+ eof_count = 0
477
+ loop do
478
+ begin
479
+ consumer.poll(100)
480
+ rescue Rdkafka::RdkafkaError => error
481
+ if error.is_partition_eof?
482
+ eof_count += 1
483
+ end
484
+ break if eof_count == 3
485
+ end
486
+ end
487
+
488
+ # Commit
489
+ consumer.commit
490
+
491
+ # Create list to fetch lag for. TODO creating the list will not be necessary
492
+ # after committed uses the subscription.
493
+ list = consumer.committed(Rdkafka::Consumer::TopicPartitionList.new.tap do |l|
494
+ l.add_topic("consume_test_topic", (0..2))
495
+ end)
496
+
497
+ # Lag should be 0 now
498
+ lag = consumer.lag(list)
499
+ expected_lag = {
500
+ "consume_test_topic" => {
501
+ 0 => 0,
502
+ 1 => 0,
503
+ 2 => 0
504
+ }
505
+ }
506
+ expect(lag).to eq(expected_lag)
507
+
508
+ # Produce message on every topic again
509
+ (0..2).each do |i|
510
+ producer.produce(
511
+ topic: "consume_test_topic",
512
+ key: "key lag #{i}",
513
+ partition: i
514
+ ).wait
515
+ end
516
+
517
+ # Lag should be 1 now
518
+ lag = consumer.lag(list)
519
+ expected_lag = {
520
+ "consume_test_topic" => {
521
+ 0 => 1,
522
+ 1 => 1,
523
+ 2 => 1
524
+ }
525
+ }
526
+ expect(lag).to eq(expected_lag)
527
+ end
528
+
529
+ it "returns nil if there are no messages on the topic" do
530
+ list = consumer.committed(Rdkafka::Consumer::TopicPartitionList.new.tap do |l|
531
+ l.add_topic("consume_test_topic", (0..2))
532
+ end)
533
+
534
+ lag = consumer.lag(list)
535
+ expected_lag = {
536
+ "consume_test_topic" => {}
537
+ }
538
+ expect(lag).to eq(expected_lag)
539
+ end
540
+ end
541
+
542
+ describe "#cluster_id" do
543
+ it 'should return the current ClusterId' do
544
+ consumer.subscribe("consume_test_topic")
545
+ wait_for_assignment(consumer)
546
+ expect(consumer.cluster_id).not_to be_empty
547
+ end
548
+ end
549
+
550
+ describe "#member_id" do
551
+ it 'should return the current MemberId' do
552
+ consumer.subscribe("consume_test_topic")
553
+ wait_for_assignment(consumer)
554
+ expect(consumer.member_id).to start_with('rdkafka-')
555
+ end
556
+ end
557
+
558
+ describe "#poll" do
559
+ it "should return nil if there is no subscription" do
560
+ expect(consumer.poll(1000)).to be_nil
561
+ end
562
+
563
+ it "should return nil if there are no messages" do
564
+ consumer.subscribe("empty_test_topic")
565
+ expect(consumer.poll(1000)).to be_nil
566
+ end
567
+
568
+ it "should return a message if there is one" do
569
+ producer.produce(
570
+ topic: "consume_test_topic",
571
+ payload: "payload 1",
572
+ key: "key 1"
573
+ ).wait
574
+ consumer.subscribe("consume_test_topic")
575
+ message = consumer.each {|m| break m}
576
+
577
+ expect(message).to be_a Rdkafka::Consumer::Message
578
+ expect(message.payload).to eq('payload 1')
579
+ expect(message.key).to eq('key 1')
580
+ end
581
+
582
+ it "should raise an error when polling fails" do
583
+ message = Rdkafka::Bindings::Message.new.tap do |message|
584
+ message[:err] = 20
585
+ end
586
+ message_pointer = message.to_ptr
587
+ expect(Rdkafka::Bindings).to receive(:rd_kafka_consumer_poll).and_return(message_pointer)
588
+ expect(Rdkafka::Bindings).to receive(:rd_kafka_message_destroy).with(message_pointer)
589
+ expect {
590
+ consumer.poll(100)
591
+ }.to raise_error Rdkafka::RdkafkaError
592
+ end
593
+ end
594
+
595
+ describe "#poll with headers" do
596
+ it "should return message with headers" do
597
+ report = producer.produce(
598
+ topic: "consume_test_topic",
599
+ key: "key headers",
600
+ headers: { foo: 'bar' }
601
+ ).wait
602
+
603
+ message = wait_for_message(topic: "consume_test_topic", consumer: consumer, delivery_report: report)
604
+ expect(message).to be
605
+ expect(message.key).to eq('key headers')
606
+ expect(message.headers).to include(foo: 'bar')
607
+ end
608
+
609
+ it "should return message with no headers" do
610
+ report = producer.produce(
611
+ topic: "consume_test_topic",
612
+ key: "key no headers",
613
+ headers: nil
614
+ ).wait
615
+
616
+ message = wait_for_message(topic: "consume_test_topic", consumer: consumer, delivery_report: report)
617
+ expect(message).to be
618
+ expect(message.key).to eq('key no headers')
619
+ expect(message.headers).to be_empty
620
+ end
621
+
622
+ it "should raise an error when message headers aren't readable" do
623
+ expect(Rdkafka::Bindings).to receive(:rd_kafka_message_headers).with(any_args) { 1 }
624
+
625
+ report = producer.produce(
626
+ topic: "consume_test_topic",
627
+ key: "key err headers",
628
+ headers: nil
629
+ ).wait
630
+
631
+ expect {
632
+ wait_for_message(topic: "consume_test_topic", consumer: consumer, delivery_report: report)
633
+ }.to raise_error do |err|
634
+ expect(err).to be_instance_of(Rdkafka::RdkafkaError)
635
+ expect(err.message).to start_with("Error reading message headers")
636
+ end
637
+ end
638
+
639
+ it "should raise an error when the first message header aren't readable" do
640
+ expect(Rdkafka::Bindings).to receive(:rd_kafka_header_get_all).with(any_args) { 1 }
641
+
642
+ report = producer.produce(
643
+ topic: "consume_test_topic",
644
+ key: "key err headers",
645
+ headers: { foo: 'bar' }
646
+ ).wait
647
+
648
+ expect {
649
+ wait_for_message(topic: "consume_test_topic", consumer: consumer, delivery_report: report)
650
+ }.to raise_error do |err|
651
+ expect(err).to be_instance_of(Rdkafka::RdkafkaError)
652
+ expect(err.message).to start_with("Error reading a message header at index 0")
653
+ end
654
+ end
655
+ end
656
+
657
+ describe "#each" do
658
+ it "should yield messages" do
659
+ handles = []
660
+ 10.times do
661
+ handles << producer.produce(
662
+ topic: "consume_test_topic",
663
+ payload: "payload 1",
664
+ key: "key 1",
665
+ partition: 0
666
+ )
667
+ end
668
+ handles.each(&:wait)
669
+
670
+ consumer.subscribe("consume_test_topic")
671
+ # Check the first 10 messages. Then close the consumer, which
672
+ # should break the each loop.
673
+ consumer.each_with_index do |message, i|
674
+ expect(message).to be_a Rdkafka::Consumer::Message
675
+ break if i == 10
676
+ end
677
+ consumer.close
678
+ end
679
+ end
680
+
681
+ describe "#each_batch" do
682
+ let(:message_payload) { 'a' * 10 }
683
+
684
+ before do
685
+ @topic = SecureRandom.base64(10).tr('+=/', '')
686
+ end
687
+
688
+ after do
689
+ @topic = nil
690
+ end
691
+
692
+ def topic_name
693
+ @topic
694
+ end
695
+
696
+ def produce_n(n)
697
+ handles = []
698
+ n.times do |i|
699
+ handles << producer.produce(
700
+ topic: topic_name,
701
+ payload: Time.new.to_f.to_s,
702
+ key: i.to_s,
703
+ partition: 0
704
+ )
705
+ end
706
+ handles.each(&:wait)
707
+ end
708
+
709
+ def new_message
710
+ instance_double("Rdkafka::Consumer::Message").tap do |message|
711
+ allow(message).to receive(:payload).and_return(message_payload)
712
+ end
713
+ end
714
+
715
+ it "retrieves messages produced into a topic" do
716
+ # This is the only each_batch test that actually produces real messages
717
+ # into a topic in the real kafka of the container.
718
+ #
719
+ # The other tests stub 'poll' which makes them faster and more reliable,
720
+ # but it makes sense to keep a single test with a fully integrated flow.
721
+ # This will help to catch breaking changes in the behavior of 'poll',
722
+ # libdrkafka, or Kafka.
723
+ #
724
+ # This is, in effect, an integration test and the subsequent specs are
725
+ # unit tests.
726
+ create_topic_handle = rdkafka_config.admin.create_topic(topic_name, 1, 1)
727
+ create_topic_handle.wait(max_wait_timeout: 15.0)
728
+ consumer.subscribe(topic_name)
729
+ produce_n 42
730
+ all_yields = []
731
+ consumer.each_batch(max_items: 10) do |batch|
732
+ all_yields << batch
733
+ break if all_yields.flatten.size >= 42
734
+ end
735
+ expect(all_yields.flatten.first).to be_a Rdkafka::Consumer::Message
736
+ expect(all_yields.flatten.size).to eq 42
737
+ expect(all_yields.size).to be > 4
738
+ expect(all_yields.flatten.map(&:key)).to eq (0..41).map { |x| x.to_s }
739
+ end
740
+
741
+ it "should batch poll results and yield arrays of messages" do
742
+ consumer.subscribe(topic_name)
743
+ all_yields = []
744
+ expect(consumer)
745
+ .to receive(:poll)
746
+ .exactly(10).times
747
+ .and_return(new_message)
748
+ consumer.each_batch(max_items: 10) do |batch|
749
+ all_yields << batch
750
+ break if all_yields.flatten.size >= 10
751
+ end
752
+ expect(all_yields.first).to be_instance_of(Array)
753
+ expect(all_yields.flatten.size).to eq 10
754
+ non_empty_yields = all_yields.reject { |batch| batch.empty? }
755
+ expect(non_empty_yields.size).to be < 10
756
+ end
757
+
758
+ it "should yield a partial batch if the timeout is hit with some messages" do
759
+ consumer.subscribe(topic_name)
760
+ poll_count = 0
761
+ expect(consumer)
762
+ .to receive(:poll)
763
+ .at_least(3).times do
764
+ poll_count = poll_count + 1
765
+ if poll_count > 2
766
+ sleep 0.1
767
+ nil
768
+ else
769
+ new_message
770
+ end
771
+ end
772
+ all_yields = []
773
+ consumer.each_batch(max_items: 10) do |batch|
774
+ all_yields << batch
775
+ break if all_yields.flatten.size >= 2
776
+ end
777
+ expect(all_yields.flatten.size).to eq 2
778
+ end
779
+
780
+ it "should yield [] if nothing is received before the timeout" do
781
+ create_topic_handle = rdkafka_config.admin.create_topic(topic_name, 1, 1)
782
+ create_topic_handle.wait(max_wait_timeout: 15.0)
783
+ consumer.subscribe(topic_name)
784
+ consumer.each_batch do |batch|
785
+ expect(batch).to eq([])
786
+ break
787
+ end
788
+ end
789
+
790
+ it "should yield batchs of max_items in size if messages are already fetched" do
791
+ yielded_batches = []
792
+ expect(consumer)
793
+ .to receive(:poll)
794
+ .with(anything)
795
+ .exactly(20).times
796
+ .and_return(new_message)
797
+
798
+ consumer.each_batch(max_items: 10, timeout_ms: 500) do |batch|
799
+ yielded_batches << batch
800
+ break if yielded_batches.flatten.size >= 20
801
+ break if yielded_batches.size >= 20 # so failure doesn't hang
802
+ end
803
+ expect(yielded_batches.size).to eq 2
804
+ expect(yielded_batches.map(&:size)).to eq 2.times.map { 10 }
805
+ end
806
+
807
+ it "should yield batchs as soon as bytes_threshold is hit" do
808
+ yielded_batches = []
809
+ expect(consumer)
810
+ .to receive(:poll)
811
+ .with(anything)
812
+ .exactly(20).times
813
+ .and_return(new_message)
814
+
815
+ consumer.each_batch(bytes_threshold: message_payload.size * 4, timeout_ms: 500) do |batch|
816
+ yielded_batches << batch
817
+ break if yielded_batches.flatten.size >= 20
818
+ break if yielded_batches.size >= 20 # so failure doesn't hang
819
+ end
820
+ expect(yielded_batches.size).to eq 5
821
+ expect(yielded_batches.map(&:size)).to eq 5.times.map { 4 }
822
+ end
823
+
824
+ context "error raised from poll and yield_on_error is true" do
825
+ it "should yield buffered exceptions on rebalance, then break" do
826
+ config = rdkafka_consumer_config(
827
+ {
828
+ :"enable.auto.commit" => false,
829
+ :"enable.auto.offset.store" => false
830
+ }
831
+ )
832
+ consumer = config.consumer
833
+ consumer.subscribe(topic_name)
834
+ batches_yielded = []
835
+ exceptions_yielded = []
836
+ each_batch_iterations = 0
837
+ poll_count = 0
838
+ expect(consumer)
839
+ .to receive(:poll)
840
+ .with(anything)
841
+ .exactly(3).times
842
+ .and_wrap_original do |method, *args|
843
+ poll_count = poll_count + 1
844
+ if poll_count == 3
845
+ raise Rdkafka::RdkafkaError.new(27,
846
+ "partitions ... too ... heavy ... must ... rebalance")
847
+ else
848
+ new_message
849
+ end
850
+ end
851
+ expect {
852
+ consumer.each_batch(max_items: 30, yield_on_error: true) do |batch, pending_error|
853
+ batches_yielded << batch
854
+ exceptions_yielded << pending_error
855
+ each_batch_iterations = each_batch_iterations + 1
856
+ end
857
+ }.to raise_error(Rdkafka::RdkafkaError)
858
+ expect(poll_count).to eq 3
859
+ expect(each_batch_iterations).to eq 1
860
+ expect(batches_yielded.size).to eq 1
861
+ expect(batches_yielded.first.size).to eq 2
862
+ expect(exceptions_yielded.flatten.size).to eq 1
863
+ expect(exceptions_yielded.flatten.first).to be_instance_of(Rdkafka::RdkafkaError)
864
+ end
865
+ end
866
+
867
+ context "error raised from poll and yield_on_error is false" do
868
+ it "should yield buffered exceptions on rebalance, then break" do
869
+ config = rdkafka_consumer_config(
870
+ {
871
+ :"enable.auto.commit" => false,
872
+ :"enable.auto.offset.store" => false
873
+ }
874
+ )
875
+ consumer = config.consumer
876
+ consumer.subscribe(topic_name)
877
+ batches_yielded = []
878
+ exceptions_yielded = []
879
+ each_batch_iterations = 0
880
+ poll_count = 0
881
+ expect(consumer)
882
+ .to receive(:poll)
883
+ .with(anything)
884
+ .exactly(3).times
885
+ .and_wrap_original do |method, *args|
886
+ poll_count = poll_count + 1
887
+ if poll_count == 3
888
+ raise Rdkafka::RdkafkaError.new(27,
889
+ "partitions ... too ... heavy ... must ... rebalance")
890
+ else
891
+ new_message
892
+ end
893
+ end
894
+ expect {
895
+ consumer.each_batch(max_items: 30, yield_on_error: false) do |batch, pending_error|
896
+ batches_yielded << batch
897
+ exceptions_yielded << pending_error
898
+ each_batch_iterations = each_batch_iterations + 1
899
+ end
900
+ }.to raise_error(Rdkafka::RdkafkaError)
901
+ expect(poll_count).to eq 3
902
+ expect(each_batch_iterations).to eq 0
903
+ expect(batches_yielded.size).to eq 0
904
+ expect(exceptions_yielded.size).to eq 0
905
+ end
906
+ end
907
+ end
908
+
909
+ describe "a rebalance listener" do
910
+ let(:consumer) do
911
+ config = rdkafka_consumer_config
912
+ config.consumer_rebalance_listener = listener
913
+ config.consumer
914
+ end
915
+
916
+ context "with a working listener" do
917
+ let(:listener) do
918
+ Struct.new(:queue) do
919
+ def on_partitions_assigned(consumer, list)
920
+ collect(:assign, list)
921
+ end
922
+
923
+ def on_partitions_revoked(consumer, list)
924
+ collect(:revoke, list)
925
+ end
926
+
927
+ def collect(name, list)
928
+ partitions = list.to_h.map { |key, values| [key, values.map(&:partition)] }.flatten
929
+ queue << ([name] + partitions)
930
+ end
931
+ end.new([])
932
+ end
933
+
934
+ it "should get notifications" do
935
+ notify_listener(listener)
936
+
937
+ expect(listener.queue).to eq([
938
+ [:assign, "consume_test_topic", 0, 1, 2],
939
+ [:revoke, "consume_test_topic", 0, 1, 2]
940
+ ])
941
+ end
942
+ end
943
+
944
+ context "with a broken listener" do
945
+ let(:listener) do
946
+ Struct.new(:queue) do
947
+ def on_partitions_assigned(consumer, list)
948
+ queue << :assigned
949
+ raise 'boom'
950
+ end
951
+
952
+ def on_partitions_revoked(consumer, list)
953
+ queue << :revoked
954
+ raise 'boom'
955
+ end
956
+ end.new([])
957
+ end
958
+
959
+ it 'should handle callback exceptions' do
960
+ notify_listener(listener)
961
+
962
+ expect(listener.queue).to eq([:assigned, :revoked])
963
+ end
964
+ end
965
+
966
+ def notify_listener(listener)
967
+ # 1. subscribe and poll
968
+ consumer.subscribe("consume_test_topic")
969
+ wait_for_assignment(consumer)
970
+ consumer.poll(100)
971
+
972
+ # 2. unsubscribe
973
+ consumer.unsubscribe
974
+ wait_for_unassignment(consumer)
975
+ consumer.close
976
+ end
977
+ end
978
+
979
+ context "methods that should not be called after a consumer has been closed" do
980
+ before do
981
+ consumer.close
982
+ end
983
+
984
+ # Affected methods and a non-invalid set of parameters for the method
985
+ {
986
+ :subscribe => [ nil ],
987
+ :unsubscribe => nil,
988
+ :each_batch => nil,
989
+ :pause => [ nil ],
990
+ :resume => [ nil ],
991
+ :subscription => nil,
992
+ :assign => [ nil ],
993
+ :assignment => nil,
994
+ :committed => [],
995
+ :query_watermark_offsets => [ nil, nil ],
996
+ }.each do |method, args|
997
+ it "raises an exception if #{method} is called" do
998
+ expect {
999
+ if args.nil?
1000
+ consumer.public_send(method)
1001
+ else
1002
+ consumer.public_send(method, *args)
1003
+ end
1004
+ }.to raise_exception(Rdkafka::ClosedConsumerError, /#{method.to_s}/)
1005
+ end
1006
+ end
1007
+ end
1008
+ end