rdkafka 0.8.1 → 0.11.1

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.
@@ -25,39 +25,39 @@ describe Rdkafka::Bindings do
25
25
  end
26
26
 
27
27
  describe "log callback" do
28
- let(:log) { StringIO.new }
28
+ let(:log_queue) { Rdkafka::Config.log_queue }
29
29
  before do
30
- Rdkafka::Config.logger = Logger.new(log)
30
+ allow(log_queue).to receive(:<<)
31
31
  end
32
32
 
33
33
  it "should log fatal messages" do
34
34
  Rdkafka::Bindings::LogCallback.call(nil, 0, nil, "log line")
35
- expect(log.string).to include "FATAL -- : rdkafka: log line"
35
+ expect(log_queue).to have_received(:<<).with([Logger::FATAL, "rdkafka: log line"])
36
36
  end
37
37
 
38
38
  it "should log error messages" do
39
39
  Rdkafka::Bindings::LogCallback.call(nil, 3, nil, "log line")
40
- expect(log.string).to include "ERROR -- : rdkafka: log line"
40
+ expect(log_queue).to have_received(:<<).with([Logger::ERROR, "rdkafka: log line"])
41
41
  end
42
42
 
43
43
  it "should log warning messages" do
44
44
  Rdkafka::Bindings::LogCallback.call(nil, 4, nil, "log line")
45
- expect(log.string).to include "WARN -- : rdkafka: log line"
45
+ expect(log_queue).to have_received(:<<).with([Logger::WARN, "rdkafka: log line"])
46
46
  end
47
47
 
48
48
  it "should log info messages" do
49
49
  Rdkafka::Bindings::LogCallback.call(nil, 5, nil, "log line")
50
- expect(log.string).to include "INFO -- : rdkafka: log line"
50
+ expect(log_queue).to have_received(:<<).with([Logger::INFO, "rdkafka: log line"])
51
51
  end
52
52
 
53
53
  it "should log debug messages" do
54
54
  Rdkafka::Bindings::LogCallback.call(nil, 7, nil, "log line")
55
- expect(log.string).to include "DEBUG -- : rdkafka: log line"
55
+ expect(log_queue).to have_received(:<<).with([Logger::DEBUG, "rdkafka: log line"])
56
56
  end
57
57
 
58
58
  it "should log unknown messages" do
59
59
  Rdkafka::Bindings::LogCallback.call(nil, 100, nil, "log line")
60
- expect(log.string).to include "ANY -- : rdkafka: log line"
60
+ expect(log_queue).to have_received(:<<).with([Logger::UNKNOWN, "rdkafka: log line"])
61
61
  end
62
62
  end
63
63
 
@@ -100,4 +100,28 @@ describe Rdkafka::Bindings do
100
100
  end
101
101
  end
102
102
  end
103
+
104
+ describe "error callback" do
105
+ context "without an error callback" do
106
+ it "should do nothing" do
107
+ expect {
108
+ Rdkafka::Bindings::ErrorCallback.call(nil, 1, "error", nil)
109
+ }.not_to raise_error
110
+ end
111
+ end
112
+
113
+ context "with an error callback" do
114
+ before do
115
+ Rdkafka::Config.error_callback = lambda do |error|
116
+ $received_error = error
117
+ end
118
+ end
119
+
120
+ it "should call the error callback with an Rdkafka::Error" do
121
+ Rdkafka::Bindings::ErrorCallback.call(nil, 8, "Broker not available", nil)
122
+ expect($received_error.code).to eq(:broker_not_available)
123
+ expect($received_error.broker_message).to eq("Broker not available")
124
+ end
125
+ end
126
+ end
103
127
  end
@@ -18,25 +18,83 @@ describe Rdkafka::Config do
18
18
  Rdkafka::Config.logger = nil
19
19
  }.to raise_error(Rdkafka::Config::NoLoggerError)
20
20
  end
21
+
22
+ it "supports logging queue" do
23
+ log = StringIO.new
24
+ Rdkafka::Config.logger = Logger.new(log)
25
+
26
+ Rdkafka::Config.log_queue << [Logger::FATAL, "I love testing"]
27
+ 20.times do
28
+ break if log.string != ""
29
+ sleep 0.05
30
+ end
31
+
32
+ expect(log.string).to include "FATAL -- : I love testing"
33
+ end
21
34
  end
22
35
 
23
36
  context "statistics callback" do
24
- it "should set the callback" do
25
- expect {
26
- Rdkafka::Config.statistics_callback = lambda do |stats|
27
- puts stats
37
+ context "with a proc/lambda" do
38
+ it "should set the callback" do
39
+ expect {
40
+ Rdkafka::Config.statistics_callback = lambda do |stats|
41
+ puts stats
42
+ end
43
+ }.not_to raise_error
44
+ expect(Rdkafka::Config.statistics_callback).to respond_to :call
45
+ end
46
+ end
47
+
48
+ context "with a callable object" do
49
+ it "should set the callback" do
50
+ callback = Class.new do
51
+ def call(stats); end
28
52
  end
29
- }.not_to raise_error
30
- expect(Rdkafka::Config.statistics_callback).to be_a Proc
53
+ expect {
54
+ Rdkafka::Config.statistics_callback = callback.new
55
+ }.not_to raise_error
56
+ expect(Rdkafka::Config.statistics_callback).to respond_to :call
57
+ end
31
58
  end
32
59
 
33
- it "should not accept a callback that's not a proc" do
60
+ it "should not accept a callback that's not callable" do
34
61
  expect {
35
62
  Rdkafka::Config.statistics_callback = 'a string'
36
63
  }.to raise_error(TypeError)
37
64
  end
38
65
  end
39
66
 
67
+ context "error callback" do
68
+ context "with a proc/lambda" do
69
+ it "should set the callback" do
70
+ expect {
71
+ Rdkafka::Config.error_callback = lambda do |error|
72
+ puts error
73
+ end
74
+ }.not_to raise_error
75
+ expect(Rdkafka::Config.error_callback).to respond_to :call
76
+ end
77
+ end
78
+
79
+ context "with a callable object" do
80
+ it "should set the callback" do
81
+ callback = Class.new do
82
+ def call(stats); end
83
+ end
84
+ expect {
85
+ Rdkafka::Config.error_callback = callback.new
86
+ }.not_to raise_error
87
+ expect(Rdkafka::Config.error_callback).to respond_to :call
88
+ end
89
+ end
90
+
91
+ it "should not accept a callback that's not callable" do
92
+ expect {
93
+ Rdkafka::Config.error_callback = 'a string'
94
+ }.to raise_error(TypeError)
95
+ end
96
+ end
97
+
40
98
  context "configuration" do
41
99
  it "should store configuration" do
42
100
  config = Rdkafka::Config.new
@@ -50,7 +108,7 @@ describe Rdkafka::Config do
50
108
  end
51
109
 
52
110
  it "should create a consumer with valid config" do
53
- consumer = rdkafka_config.consumer
111
+ consumer = rdkafka_consumer_config.consumer
54
112
  expect(consumer).to be_a Rdkafka::Consumer
55
113
  consumer.close
56
114
  end
@@ -78,7 +136,7 @@ describe Rdkafka::Config do
78
136
  end
79
137
 
80
138
  it "should create a producer with valid config" do
81
- producer = rdkafka_config.producer
139
+ producer = rdkafka_consumer_config.producer
82
140
  expect(producer).to be_a Rdkafka::Producer
83
141
  producer.close
84
142
  end
@@ -1,10 +1,10 @@
1
1
  require "spec_helper"
2
2
  require "ostruct"
3
+ require 'securerandom'
3
4
 
4
5
  describe Rdkafka::Consumer do
5
- let(:config) { rdkafka_config }
6
- let(:consumer) { config.consumer }
7
- let(:producer) { config.producer }
6
+ let(:consumer) { rdkafka_consumer_config.consumer }
7
+ let(:producer) { rdkafka_producer_config.producer }
8
8
 
9
9
  after { consumer.close }
10
10
  after { producer.close }
@@ -271,6 +271,14 @@ describe Rdkafka::Consumer do
271
271
  describe "#close" do
272
272
  it "should close a consumer" do
273
273
  consumer.subscribe("consume_test_topic")
274
+ 100.times do |i|
275
+ report = producer.produce(
276
+ topic: "consume_test_topic",
277
+ payload: "payload #{i}",
278
+ key: "key #{i}",
279
+ partition: 0
280
+ ).wait
281
+ end
274
282
  consumer.close
275
283
  expect {
276
284
  consumer.poll(100)
@@ -319,7 +327,7 @@ describe Rdkafka::Consumer do
319
327
  before :all do
320
328
  # Make sure there are some messages.
321
329
  handles = []
322
- producer = rdkafka_config.producer
330
+ producer = rdkafka_producer_config.producer
323
331
  10.times do
324
332
  (0..2).each do |i|
325
333
  handles << producer.produce(
@@ -395,7 +403,7 @@ describe Rdkafka::Consumer do
395
403
  config = {}
396
404
  config[:'enable.auto.offset.store'] = false
397
405
  config[:'enable.auto.commit'] = false
398
- @new_consumer = rdkafka_config(config).consumer
406
+ @new_consumer = rdkafka_consumer_config(config).consumer
399
407
  @new_consumer.subscribe("consume_test_topic")
400
408
  wait_for_assignment(@new_consumer)
401
409
  end
@@ -450,13 +458,13 @@ describe Rdkafka::Consumer do
450
458
  end
451
459
 
452
460
  describe "#lag" do
453
- let(:config) { rdkafka_config(:"enable.partition.eof" => true) }
461
+ let(:consumer) { rdkafka_consumer_config(:"enable.partition.eof" => true).consumer }
454
462
 
455
463
  it "should calculate the consumer lag" do
456
464
  # Make sure there's a message in every partition and
457
465
  # wait for the message to make sure everything is committed.
458
466
  (0..2).each do |i|
459
- report = producer.produce(
467
+ producer.produce(
460
468
  topic: "consume_test_topic",
461
469
  key: "key lag #{i}",
462
470
  partition: i
@@ -499,7 +507,7 @@ describe Rdkafka::Consumer do
499
507
 
500
508
  # Produce message on every topic again
501
509
  (0..2).each do |i|
502
- report = producer.produce(
510
+ producer.produce(
503
511
  topic: "consume_test_topic",
504
512
  key: "key lag #{i}",
505
513
  partition: i
@@ -670,52 +678,295 @@ describe Rdkafka::Consumer do
670
678
  end
671
679
  end
672
680
 
673
- describe "a rebalance listener" do
674
- it "should get notifications" do
675
- listener = Struct.new(:queue) do
676
- def on_partitions_assigned(consumer, list)
677
- collect(:assign, list)
678
- end
681
+ describe "#each_batch" do
682
+ let(:message_payload) { 'a' * 10 }
679
683
 
680
- def on_partitions_revoked(consumer, list)
681
- collect(:revoke, list)
682
- end
684
+ before do
685
+ @topic = SecureRandom.base64(10).tr('+=/', '')
686
+ end
683
687
 
684
- def collect(name, list)
685
- partitions = list.to_h.map { |key, values| [key, values.map(&:partition)] }.flatten
686
- queue << ([name] + partitions)
687
- end
688
- end.new([])
688
+ after do
689
+ @topic = nil
690
+ end
689
691
 
690
- notify_listener(listener)
692
+ def topic_name
693
+ @topic
694
+ end
691
695
 
692
- expect(listener.queue).to eq([
693
- [:assign, "consume_test_topic", 0, 1, 2],
694
- [:revoke, "consume_test_topic", 0, 1, 2]
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)
696
707
  end
697
708
 
698
- it 'should handle callback exceptions' do
699
- listener = Struct.new(:queue) do
700
- def on_partitions_assigned(consumer, list)
701
- queue << :assigned
702
- raise 'boom'
703
- end
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
704
714
 
705
- def on_partitions_revoked(consumer, list)
706
- queue << :revoked
707
- raise 'boom'
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
708
770
  end
709
- end.new([])
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
710
823
 
711
- notify_listener(listener)
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
+ loop_count = 0
835
+ batches_yielded = []
836
+ exceptions_yielded = []
837
+ each_batch_iterations = 0
838
+ poll_count = 0
839
+ expect(consumer)
840
+ .to receive(:poll)
841
+ .with(anything)
842
+ .exactly(3).times
843
+ .and_wrap_original do |method, *args|
844
+ poll_count = poll_count + 1
845
+ if poll_count == 3
846
+ raise Rdkafka::RdkafkaError.new(27,
847
+ "partitions ... too ... heavy ... must ... rebalance")
848
+ else
849
+ new_message
850
+ end
851
+ end
852
+ expect {
853
+ consumer.each_batch(max_items: 30, yield_on_error: true) do |batch, pending_error|
854
+ batches_yielded << batch
855
+ exceptions_yielded << pending_error
856
+ each_batch_iterations = each_batch_iterations + 1
857
+ end
858
+ }.to raise_error(Rdkafka::RdkafkaError)
859
+ expect(poll_count).to eq 3
860
+ expect(each_batch_iterations).to eq 1
861
+ expect(batches_yielded.size).to eq 1
862
+ expect(batches_yielded.first.size).to eq 2
863
+ expect(exceptions_yielded.flatten.size).to eq 1
864
+ expect(exceptions_yielded.flatten.first).to be_instance_of(Rdkafka::RdkafkaError)
865
+ end
866
+ end
867
+
868
+ context "error raised from poll and yield_on_error is false" do
869
+ it "should yield buffered exceptions on rebalance, then break" do
870
+ config = rdkafka_consumer_config(
871
+ {
872
+ :"enable.auto.commit" => false,
873
+ :"enable.auto.offset.store" => false
874
+ }
875
+ )
876
+ consumer = config.consumer
877
+ consumer.subscribe(topic_name)
878
+ loop_count = 0
879
+ batches_yielded = []
880
+ exceptions_yielded = []
881
+ each_batch_iterations = 0
882
+ poll_count = 0
883
+ expect(consumer)
884
+ .to receive(:poll)
885
+ .with(anything)
886
+ .exactly(3).times
887
+ .and_wrap_original do |method, *args|
888
+ poll_count = poll_count + 1
889
+ if poll_count == 3
890
+ raise Rdkafka::RdkafkaError.new(27,
891
+ "partitions ... too ... heavy ... must ... rebalance")
892
+ else
893
+ new_message
894
+ end
895
+ end
896
+ expect {
897
+ consumer.each_batch(max_items: 30, yield_on_error: false) do |batch, pending_error|
898
+ batches_yielded << batch
899
+ exceptions_yielded << pending_error
900
+ each_batch_iterations = each_batch_iterations + 1
901
+ end
902
+ }.to raise_error(Rdkafka::RdkafkaError)
903
+ expect(poll_count).to eq 3
904
+ expect(each_batch_iterations).to eq 0
905
+ expect(batches_yielded.size).to eq 0
906
+ expect(exceptions_yielded.size).to eq 0
907
+ end
908
+ end
909
+ end
712
910
 
713
- expect(listener.queue).to eq([:assigned, :revoked])
911
+ describe "a rebalance listener" do
912
+ let(:consumer) do
913
+ config = rdkafka_consumer_config
914
+ config.consumer_rebalance_listener = listener
915
+ config.consumer
916
+ end
917
+
918
+ context "with a working listener" do
919
+ let(:listener) do
920
+ Struct.new(:queue) do
921
+ def on_partitions_assigned(consumer, list)
922
+ collect(:assign, list)
923
+ end
924
+
925
+ def on_partitions_revoked(consumer, list)
926
+ collect(:revoke, list)
927
+ end
928
+
929
+ def collect(name, list)
930
+ partitions = list.to_h.map { |key, values| [key, values.map(&:partition)] }.flatten
931
+ queue << ([name] + partitions)
932
+ end
933
+ end.new([])
934
+ end
935
+
936
+ it "should get notifications" do
937
+ notify_listener(listener)
938
+
939
+ expect(listener.queue).to eq([
940
+ [:assign, "consume_test_topic", 0, 1, 2],
941
+ [:revoke, "consume_test_topic", 0, 1, 2]
942
+ ])
943
+ end
944
+ end
945
+
946
+ context "with a broken listener" do
947
+ let(:listener) do
948
+ Struct.new(:queue) do
949
+ def on_partitions_assigned(consumer, list)
950
+ queue << :assigned
951
+ raise 'boom'
952
+ end
953
+
954
+ def on_partitions_revoked(consumer, list)
955
+ queue << :revoked
956
+ raise 'boom'
957
+ end
958
+ end.new([])
959
+ end
960
+
961
+ it 'should handle callback exceptions' do
962
+ notify_listener(listener)
963
+
964
+ expect(listener.queue).to eq([:assigned, :revoked])
965
+ end
714
966
  end
715
967
 
716
968
  def notify_listener(listener)
717
969
  # 1. subscribe and poll
718
- config.consumer_rebalance_listener = listener
719
970
  consumer.subscribe("consume_test_topic")
720
971
  wait_for_assignment(consumer)
721
972
  consumer.poll(100)
@@ -736,6 +987,7 @@ describe Rdkafka::Consumer do
736
987
  {
737
988
  :subscribe => [ nil ],
738
989
  :unsubscribe => nil,
990
+ :each_batch => nil,
739
991
  :pause => [ nil ],
740
992
  :resume => [ nil ],
741
993
  :subscription => nil,
@@ -2,7 +2,7 @@ require "spec_helper"
2
2
  require "securerandom"
3
3
 
4
4
  describe Rdkafka::Metadata do
5
- let(:config) { rdkafka_config }
5
+ let(:config) { rdkafka_consumer_config }
6
6
  let(:native_config) { config.send(:native_config) }
7
7
  let(:native_kafka) { config.send(:native_kafka, native_config, :rd_kafka_consumer) }
8
8
 
@@ -18,7 +18,7 @@ describe Rdkafka::Metadata do
18
18
  it "raises an appropriate exception" do
19
19
  expect {
20
20
  described_class.new(native_kafka, topic_name)
21
- }.to raise_exception(Rdkafka::RdkafkaError, "Broker: Leader not available (leader_not_available)")
21
+ }.to raise_exception(Rdkafka::RdkafkaError, "Broker: Unknown topic or partition (unknown_topic_or_part)")
22
22
  end
23
23
  end
24
24