rdkafka 0.8.1 → 0.9.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: d09f2751b883400c550fee28fca85ffdd3e9f8090a9ba8e7bb22eadcba5c05c6
4
- data.tar.gz: 107a5b723b40ec43b10f02b1993f42ff4b30a4ba16efc21611efb860de798a3a
3
+ metadata.gz: 22bda74a815bebd487179ac5ec3faa4566752f2c87945ca5c461c427633daf1f
4
+ data.tar.gz: '0802ca4118ed248fa11226a2c1b2140d073d230c02e3696a03e462fe59cb129e'
5
5
  SHA512:
6
- metadata.gz: 373bd71c9e1f3c1fadd4deb9e67bec7aa5821e0a7835c851f1bb64fb1ad142692145e01efa62ac357d333eebb76cef7d676ddfa53ef3a9cd08720060b4bc0937
7
- data.tar.gz: ec6d8932a2987e419f6fd731ff038cb88d9d898eab75d7eff82a6e606fcb71c249dd20f6d200bda44629f4bb81550c06ef4e8083ac1b4736c57932ab2486fdbc
6
+ metadata.gz: 4d3b7042fa2290386d4feaaa01c4289bc858042d66806157066257f282c84db459e80e32a2613efd3452a46c63faaff7fad9f110fb153c3385cef00fb6017f27
7
+ data.tar.gz: e431a320535df8293e099e1b25bf0e6da68caf9a641621faf847dd206093dd9973b41f03f95412c43a740b50a0ef70e7caad418ff20c07218f19342d1cb3826a
@@ -3,7 +3,7 @@ name: Rdkafka Ruby
3
3
 
4
4
  agent:
5
5
  machine:
6
- type: e1-standard-2
6
+ type: e1-standard-4
7
7
  os_image: ubuntu1804
8
8
 
9
9
  blocks:
@@ -13,11 +13,11 @@ blocks:
13
13
  - name: bundle exec rspec
14
14
  matrix:
15
15
  - env_var: RUBY_VERSION
16
- values: [ "2.5.8", "2.6.6", "2.7.2", "jruby-9.2.13.0" ]
16
+ values: [ "2.5.8", "2.6.6", "2.7.2", "3.0.0", "jruby-9.2.13.0" ]
17
17
  commands:
18
18
  - sem-version ruby $RUBY_VERSION
19
19
  - checkout
20
- - bundle install --path vendor/bundl
20
+ - bundle install --path vendor/bundle
21
21
  - cd ext && bundle exec rake && cd ..
22
22
  - docker-compose up -d --no-recreate
23
23
  - bundle exec rspec
data/CHANGELOG.md CHANGED
@@ -1,3 +1,11 @@
1
+ # 0.9.0
2
+ * Fixes for Ruby 3.0
3
+ * Allow any callable object for callbacks (gremerritt)
4
+ * Reduce memory allocations in Rdkafka::Producer#produce (jturkel)
5
+ * Use queue as log callback to avoid unsafe calls from trap context (breunigs)
6
+ * Allow passing in topic configuration on create_topic (dezka)
7
+ * Add each_batch method to consumer (mgrosso)
8
+
1
9
  # 0.8.1
2
10
  * Fix topic_flag behaviour and add tests for Metadata (geoff2k)
3
11
  * Add topic admin interface (geoff2k)
data/ext/README.md CHANGED
@@ -9,3 +9,10 @@ To update the `librdkafka` version follow the following steps:
9
9
  version number and asset checksum for `tar.gz`.
10
10
  * Change the version in `lib/rdkafka/version.rb`
11
11
  * Change the `sha256` in `lib/rdkafka/version.rb`
12
+ * Run `bundle exec rake` in the `ext` directory to download and build
13
+ the new version
14
+ * Run `docker-compose pull` in the main gem directory to ensure the docker
15
+ images used by the tests and run `docker-compose up`
16
+ * Finally, run `bundle exec rspec` in the main gem directory to execute
17
+ the test suite to detect any regressions that may have been introduced
18
+ by the update
data/ext/Rakefile CHANGED
@@ -1,6 +1,7 @@
1
1
  require File.expand_path('../../lib/rdkafka/version', __FILE__)
2
2
  require "mini_portile2"
3
3
  require "fileutils"
4
+ require "open-uri"
4
5
 
5
6
  task :default => :clean do
6
7
  # MiniPortile#download_file_http is a monkey patch that removes the download
data/lib/rdkafka/admin.rb CHANGED
@@ -34,9 +34,10 @@ module Rdkafka
34
34
  #
35
35
  # @raise [ConfigError] When the partition count or replication factor are out of valid range
36
36
  # @raise [RdkafkaError] When the topic name is invalid or the topic already exists
37
+ # @raise [RdkafkaError] When the topic configuration is invalid
37
38
  #
38
39
  # @return [CreateTopicHandle] Create topic handle that can be used to wait for the result of creating the topic
39
- def create_topic(topic_name, partition_count, replication_factor)
40
+ def create_topic(topic_name, partition_count, replication_factor, topic_config={})
40
41
 
41
42
  # Create a rd_kafka_NewTopic_t representing the new topic
42
43
  error_buffer = FFI::MemoryPointer.from_string(" " * 256)
@@ -51,6 +52,16 @@ module Rdkafka
51
52
  raise Rdkafka::Config::ConfigError.new(error_buffer.read_string)
52
53
  end
53
54
 
55
+ unless topic_config.nil?
56
+ topic_config.each do |key, value|
57
+ Rdkafka::Bindings.rd_kafka_NewTopic_set_config(
58
+ new_topic_ptr,
59
+ key.to_s,
60
+ value.to_s
61
+ )
62
+ end
63
+ end
64
+
54
65
  # Note that rd_kafka_CreateTopics can create more than one topic at a time
55
66
  pointer_array = [new_topic_ptr]
56
67
  topics_array_ptr = FFI::MemoryPointer.new(:pointer)
@@ -130,7 +130,7 @@ module Rdkafka
130
130
  else
131
131
  Logger::UNKNOWN
132
132
  end
133
- Rdkafka::Config.logger.add(severity) { "rdkafka: #{line}" }
133
+ Rdkafka::Config.log_queue << [severity, "rdkafka: #{line}"]
134
134
  end
135
135
 
136
136
  StatsCallback = FFI::Function.new(
@@ -252,6 +252,7 @@ module Rdkafka
252
252
 
253
253
  attach_function :rd_kafka_CreateTopics, [:pointer, :pointer, :size_t, :pointer, :pointer], :void
254
254
  attach_function :rd_kafka_NewTopic_new, [:pointer, :size_t, :size_t, :pointer, :size_t], :pointer
255
+ attach_function :rd_kafka_NewTopic_set_config, [:pointer, :string, :string], :int32
255
256
  attach_function :rd_kafka_NewTopic_destroy, [:pointer], :void
256
257
  attach_function :rd_kafka_event_CreateTopics_result, [:pointer], :pointer
257
258
  attach_function :rd_kafka_CreateTopics_result_topics, [:pointer, :pointer], :pointer
@@ -11,6 +11,15 @@ module Rdkafka
11
11
  @@statistics_callback = nil
12
12
  # @private
13
13
  @@opaques = {}
14
+ # @private
15
+ @@log_queue = Queue.new
16
+
17
+ Thread.start do
18
+ loop do
19
+ severity, msg = @@log_queue.pop
20
+ @@logger.add(severity, msg)
21
+ end
22
+ end
14
23
 
15
24
  # Returns the current logger, by default this is a logger to stdout.
16
25
  #
@@ -19,6 +28,15 @@ module Rdkafka
19
28
  @@logger
20
29
  end
21
30
 
31
+ # Returns a queue whose contents will be passed to the configured logger. Each entry
32
+ # should follow the format [Logger::Severity, String]. The benefit over calling the
33
+ # logger directly is that this is safe to use from trap contexts.
34
+ #
35
+ # @return [Queue]
36
+ def self.log_queue
37
+ @@log_queue
38
+ end
39
+
22
40
  # Set the logger that will be used for all logging output by this library.
23
41
  #
24
42
  # @param logger [Logger] The logger to be used
@@ -33,11 +51,11 @@ module Rdkafka
33
51
  # You can configure if and how often this happens using `statistics.interval.ms`.
34
52
  # The callback is called with a hash that's documented here: https://github.com/edenhill/librdkafka/blob/master/STATISTICS.md
35
53
  #
36
- # @param callback [Proc] The callback
54
+ # @param callback [Proc, #call] The callback
37
55
  #
38
56
  # @return [nil]
39
57
  def self.statistics_callback=(callback)
40
- raise TypeError.new("Callback has to be a proc or lambda") unless callback.is_a? Proc
58
+ raise TypeError.new("Callback has to be callable") unless callback.respond_to?(:call)
41
59
  @@statistics_callback = callback
42
60
  end
43
61
 
@@ -471,5 +471,95 @@ module Rdkafka
471
471
  def closed_consumer_check(method)
472
472
  raise Rdkafka::ClosedConsumerError.new(method) if @native_kafka.nil?
473
473
  end
474
+
475
+ # Poll for new messages and yield them in batches that may contain
476
+ # messages from more than one partition.
477
+ #
478
+ # Rather than yield each message immediately as soon as it is received,
479
+ # each_batch will attempt to wait for as long as `timeout_ms` in order
480
+ # to create a batch of up to but no more than `max_items` in size.
481
+ #
482
+ # Said differently, if more than `max_items` are available within
483
+ # `timeout_ms`, then `each_batch` will yield early with `max_items` in the
484
+ # array, but if `timeout_ms` passes by with fewer messages arriving, it
485
+ # will yield an array of fewer messages, quite possibly zero.
486
+ #
487
+ # In order to prevent wrongly auto committing many messages at once across
488
+ # possibly many partitions, callers must explicitly indicate which messages
489
+ # have been successfully processed as some consumed messages may not have
490
+ # been yielded yet. To do this, the caller should set
491
+ # `enable.auto.offset.store` to false and pass processed messages to
492
+ # {store_offset}. It is also possible, though more complex, to set
493
+ # 'enable.auto.commit' to false and then pass a manually assembled
494
+ # TopicPartitionList to {commit}.
495
+ #
496
+ # As with `each`, iteration will end when the consumer is closed.
497
+ #
498
+ # Exception behavior is more complicated than with `each`, in that if
499
+ # :yield_on_error is true, and an exception is raised during the
500
+ # poll, and messages have already been received, they will be yielded to
501
+ # the caller before the exception is allowed to propogate.
502
+ #
503
+ # If you are setting either auto.commit or auto.offset.store to false in
504
+ # the consumer configuration, then you should let yield_on_error keep its
505
+ # default value of false because you are gauranteed to see these messages
506
+ # again. However, if both auto.commit and auto.offset.store are set to
507
+ # true, you should set yield_on_error to true so you can process messages
508
+ # that you may or may not see again.
509
+ #
510
+ # @param max_items [Integer] Maximum size of the yielded array of messages
511
+ #
512
+ # @param bytes_threshold [Integer] Threshold number of total message bytes in the yielded array of messages
513
+ #
514
+ # @param timeout_ms [Integer] max time to wait for up to max_items
515
+ #
516
+ # @raise [RdkafkaError] When polling fails
517
+ #
518
+ # @yield [messages, pending_exception]
519
+ # @yieldparam messages [Array] An array of received Message
520
+ # @yieldparam pending_exception [Exception] normally nil, or an exception
521
+ # which will be propogated after processing of the partial batch is complete.
522
+ #
523
+ # @return [nil]
524
+ def each_batch(max_items: 100, bytes_threshold: Float::INFINITY, timeout_ms: 250, yield_on_error: false, &block)
525
+ closed_consumer_check(__method__)
526
+ slice = []
527
+ bytes = 0
528
+ end_time = monotonic_now + timeout_ms / 1000.0
529
+ loop do
530
+ break if @closing
531
+ max_wait = end_time - monotonic_now
532
+ max_wait_ms = if max_wait <= 0
533
+ 0 # should not block, but may retrieve a message
534
+ else
535
+ (max_wait * 1000).floor
536
+ end
537
+ message = nil
538
+ begin
539
+ message = poll max_wait_ms
540
+ rescue Rdkafka::RdkafkaError => error
541
+ raise unless yield_on_error
542
+ raise if slice.empty?
543
+ yield slice.dup, error
544
+ raise
545
+ end
546
+ if message
547
+ slice << message
548
+ bytes += message.payload.bytesize
549
+ end
550
+ if slice.size == max_items || bytes >= bytes_threshold || monotonic_now >= end_time - 0.001
551
+ yield slice.dup, nil
552
+ slice.clear
553
+ bytes = 0
554
+ end_time = monotonic_now + timeout_ms / 1000.0
555
+ end
556
+ end
557
+ end
558
+
559
+ private
560
+ def monotonic_now
561
+ # needed because Time.now can go backwards
562
+ Process.clock_gettime(Process::CLOCK_MONOTONIC)
563
+ end
474
564
  end
475
565
  end
@@ -31,11 +31,11 @@ module Rdkafka
31
31
  # Set a callback that will be called every time a message is successfully produced.
32
32
  # The callback is called with a {DeliveryReport}
33
33
  #
34
- # @param callback [Proc] The callback
34
+ # @param callback [Proc, #call] The callback
35
35
  #
36
36
  # @return [nil]
37
37
  def delivery_callback=(callback)
38
- raise TypeError.new("Callback has to be a proc or lambda") unless callback.is_a? Proc
38
+ raise TypeError.new("Callback has to be callable") unless callback.respond_to?(:call)
39
39
  @delivery_callback = callback
40
40
  end
41
41
 
@@ -140,16 +140,14 @@ module Rdkafka
140
140
  headers.each do |key0, value0|
141
141
  key = key0.to_s
142
142
  value = value0.to_s
143
- args += [
144
- :int, Rdkafka::Bindings::RD_KAFKA_VTYPE_HEADER,
145
- :string, key,
146
- :pointer, value,
147
- :size_t, value.bytes.size
148
- ]
143
+ args << :int << Rdkafka::Bindings::RD_KAFKA_VTYPE_HEADER
144
+ args << :string << key
145
+ args << :pointer << value
146
+ args << :size_t << value.bytes.size
149
147
  end
150
148
  end
151
149
 
152
- args += [:int, Rdkafka::Bindings::RD_KAFKA_VTYPE_END]
150
+ args << :int << Rdkafka::Bindings::RD_KAFKA_VTYPE_END
153
151
 
154
152
  # Produce the message
155
153
  response = Rdkafka::Bindings.rd_kafka_producev(
@@ -1,5 +1,5 @@
1
1
  module Rdkafka
2
- VERSION = "0.8.1"
2
+ VERSION = "0.9.0"
3
3
  LIBRDKAFKA_VERSION = "1.4.0"
4
4
  LIBRDKAFKA_SOURCE_SHA256 = "ae27ea3f3d0d32d29004e7f709efbba2666c5383a107cc45b3a1949486b2eb84"
5
5
  end
@@ -14,6 +14,8 @@ describe Rdkafka::Admin do
14
14
  let(:topic_name) { "test-topic-#{Random.new.rand(0..1_000_000)}" }
15
15
  let(:topic_partition_count) { 3 }
16
16
  let(:topic_replication_factor) { 1 }
17
+ let(:topic_config) { {"cleanup.policy" => "compact", "min.cleanable.dirty.ratio" => 0.8} }
18
+ let(:invalid_topic_config) { {"cleeeeenup.policee" => "campact"} }
17
19
 
18
20
  describe "#create_topic" do
19
21
  describe "called with invalid input" do
@@ -68,6 +70,15 @@ describe Rdkafka::Admin do
68
70
  }.to raise_error Rdkafka::Config::ConfigError, /replication_factor out of expected range/
69
71
  end
70
72
  end
73
+
74
+ describe "with an invalid topic configuration" do
75
+ it "doesn't create the topic" do
76
+ create_topic_handle = admin.create_topic(topic_name, topic_partition_count, topic_replication_factor, invalid_topic_config)
77
+ expect {
78
+ create_topic_handle.wait(max_wait_timeout: 15.0)
79
+ }.to raise_error Rdkafka::RdkafkaError, /Broker: Configuration is invalid \(invalid_config\)/
80
+ end
81
+ end
71
82
  end
72
83
 
73
84
  context "edge case" do
@@ -97,7 +108,7 @@ describe Rdkafka::Admin do
97
108
  end
98
109
 
99
110
  it "creates a topic" do
100
- create_topic_handle = admin.create_topic(topic_name, topic_partition_count, topic_replication_factor)
111
+ create_topic_handle = admin.create_topic(topic_name, topic_partition_count, topic_replication_factor, topic_config)
101
112
  create_topic_report = create_topic_handle.wait(max_wait_timeout: 15.0)
102
113
  expect(create_topic_report.error_string).to be_nil
103
114
  expect(create_topic_report.result_name).to eq(topic_name)
@@ -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
 
@@ -18,19 +18,46 @@ 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)
@@ -1,5 +1,6 @@
1
1
  require "spec_helper"
2
2
  require "ostruct"
3
+ require 'securerandom'
3
4
 
4
5
  describe Rdkafka::Consumer do
5
6
  let(:config) { rdkafka_config }
@@ -271,6 +272,14 @@ describe Rdkafka::Consumer do
271
272
  describe "#close" do
272
273
  it "should close a consumer" do
273
274
  consumer.subscribe("consume_test_topic")
275
+ 100.times do |i|
276
+ report = producer.produce(
277
+ topic: "consume_test_topic",
278
+ payload: "payload #{i}",
279
+ key: "key #{i}",
280
+ partition: 0
281
+ ).wait
282
+ end
274
283
  consumer.close
275
284
  expect {
276
285
  consumer.poll(100)
@@ -670,6 +679,224 @@ describe Rdkafka::Consumer do
670
679
  end
671
680
  end
672
681
 
682
+ describe "#each_batch" do
683
+ let(:message_payload) { 'a' * 10 }
684
+
685
+ before do
686
+ @topic = SecureRandom.base64(10).tr('+=/', '')
687
+ end
688
+
689
+ after do
690
+ @topic = nil
691
+ end
692
+
693
+ def topic_name
694
+ @topic
695
+ end
696
+
697
+ def produce_n(n)
698
+ handles = []
699
+ n.times do |i|
700
+ handles << producer.produce(
701
+ topic: topic_name,
702
+ payload: Time.new.to_f.to_s,
703
+ key: i.to_s,
704
+ partition: 0
705
+ )
706
+ end
707
+ handles.each(&:wait)
708
+ end
709
+
710
+ def new_message
711
+ instance_double("Rdkafka::Consumer::Message").tap do |message|
712
+ allow(message).to receive(:payload).and_return(message_payload)
713
+ end
714
+ end
715
+
716
+ it "retrieves messages produced into a topic" do
717
+ # This is the only each_batch test that actually produces real messages
718
+ # into a topic in the real kafka of the container.
719
+ #
720
+ # The other tests stub 'poll' which makes them faster and more reliable,
721
+ # but it makes sense to keep a single test with a fully integrated flow.
722
+ # This will help to catch breaking changes in the behavior of 'poll',
723
+ # libdrkafka, or Kafka.
724
+ #
725
+ # This is, in effect, an integration test and the subsequent specs are
726
+ # unit tests.
727
+ consumer.subscribe(topic_name)
728
+ produce_n 42
729
+ all_yields = []
730
+ consumer.each_batch(max_items: 10) do |batch|
731
+ all_yields << batch
732
+ break if all_yields.flatten.size >= 42
733
+ end
734
+ expect(all_yields.flatten.first).to be_a Rdkafka::Consumer::Message
735
+ expect(all_yields.flatten.size).to eq 42
736
+ expect(all_yields.size).to be > 4
737
+ expect(all_yields.flatten.map(&:key)).to eq (0..41).map { |x| x.to_s }
738
+ end
739
+
740
+ it "should batch poll results and yield arrays of messages" do
741
+ consumer.subscribe(topic_name)
742
+ all_yields = []
743
+ expect(consumer)
744
+ .to receive(:poll)
745
+ .exactly(10).times
746
+ .and_return(new_message)
747
+ consumer.each_batch(max_items: 10) do |batch|
748
+ all_yields << batch
749
+ break if all_yields.flatten.size >= 10
750
+ end
751
+ expect(all_yields.first).to be_instance_of(Array)
752
+ expect(all_yields.flatten.size).to eq 10
753
+ non_empty_yields = all_yields.reject { |batch| batch.empty? }
754
+ expect(non_empty_yields.size).to be < 10
755
+ end
756
+
757
+ it "should yield a partial batch if the timeout is hit with some messages" do
758
+ consumer.subscribe(topic_name)
759
+ poll_count = 0
760
+ expect(consumer)
761
+ .to receive(:poll)
762
+ .at_least(3).times do
763
+ poll_count = poll_count + 1
764
+ if poll_count > 2
765
+ sleep 0.1
766
+ nil
767
+ else
768
+ new_message
769
+ end
770
+ end
771
+ all_yields = []
772
+ consumer.each_batch(max_items: 10) do |batch|
773
+ all_yields << batch
774
+ break if all_yields.flatten.size >= 2
775
+ end
776
+ expect(all_yields.flatten.size).to eq 2
777
+ end
778
+
779
+ it "should yield [] if nothing is received before the timeout" do
780
+ consumer.subscribe(topic_name)
781
+ consumer.each_batch do |batch|
782
+ expect(batch).to eq([])
783
+ break
784
+ end
785
+ end
786
+
787
+ it "should yield batchs of max_items in size if messages are already fetched" do
788
+ yielded_batches = []
789
+ expect(consumer)
790
+ .to receive(:poll)
791
+ .with(anything)
792
+ .exactly(20).times
793
+ .and_return(new_message)
794
+
795
+ consumer.each_batch(max_items: 10, timeout_ms: 500) do |batch|
796
+ yielded_batches << batch
797
+ break if yielded_batches.flatten.size >= 20
798
+ break if yielded_batches.size >= 20 # so failure doesn't hang
799
+ end
800
+ expect(yielded_batches.size).to eq 2
801
+ expect(yielded_batches.map(&:size)).to eq 2.times.map { 10 }
802
+ end
803
+
804
+ it "should yield batchs as soon as bytes_threshold is hit" do
805
+ yielded_batches = []
806
+ expect(consumer)
807
+ .to receive(:poll)
808
+ .with(anything)
809
+ .exactly(20).times
810
+ .and_return(new_message)
811
+
812
+ consumer.each_batch(bytes_threshold: message_payload.size * 4, timeout_ms: 500) do |batch|
813
+ yielded_batches << batch
814
+ break if yielded_batches.flatten.size >= 20
815
+ break if yielded_batches.size >= 20 # so failure doesn't hang
816
+ end
817
+ expect(yielded_batches.size).to eq 5
818
+ expect(yielded_batches.map(&:size)).to eq 5.times.map { 4 }
819
+ end
820
+
821
+ context "error raised from poll and yield_on_error is true" do
822
+ it "should yield buffered exceptions on rebalance, then break" do
823
+ config = rdkafka_config({:"enable.auto.commit" => false,
824
+ :"enable.auto.offset.store" => false })
825
+ consumer = config.consumer
826
+ consumer.subscribe(topic_name)
827
+ loop_count = 0
828
+ batches_yielded = []
829
+ exceptions_yielded = []
830
+ each_batch_iterations = 0
831
+ poll_count = 0
832
+ expect(consumer)
833
+ .to receive(:poll)
834
+ .with(anything)
835
+ .exactly(3).times
836
+ .and_wrap_original do |method, *args|
837
+ poll_count = poll_count + 1
838
+ if poll_count == 3
839
+ raise Rdkafka::RdkafkaError.new(27,
840
+ "partitions ... too ... heavy ... must ... rebalance")
841
+ else
842
+ new_message
843
+ end
844
+ end
845
+ expect {
846
+ consumer.each_batch(max_items: 30, yield_on_error: true) do |batch, pending_error|
847
+ batches_yielded << batch
848
+ exceptions_yielded << pending_error
849
+ each_batch_iterations = each_batch_iterations + 1
850
+ end
851
+ }.to raise_error(Rdkafka::RdkafkaError)
852
+ expect(poll_count).to eq 3
853
+ expect(each_batch_iterations).to eq 1
854
+ expect(batches_yielded.size).to eq 1
855
+ expect(batches_yielded.first.size).to eq 2
856
+ expect(exceptions_yielded.flatten.size).to eq 1
857
+ expect(exceptions_yielded.flatten.first).to be_instance_of(Rdkafka::RdkafkaError)
858
+ end
859
+ end
860
+
861
+ context "error raised from poll and yield_on_error is false" do
862
+ it "should yield buffered exceptions on rebalance, then break" do
863
+ config = rdkafka_config({:"enable.auto.commit" => false,
864
+ :"enable.auto.offset.store" => false })
865
+ consumer = config.consumer
866
+ consumer.subscribe(topic_name)
867
+ loop_count = 0
868
+ batches_yielded = []
869
+ exceptions_yielded = []
870
+ each_batch_iterations = 0
871
+ poll_count = 0
872
+ expect(consumer)
873
+ .to receive(:poll)
874
+ .with(anything)
875
+ .exactly(3).times
876
+ .and_wrap_original do |method, *args|
877
+ poll_count = poll_count + 1
878
+ if poll_count == 3
879
+ raise Rdkafka::RdkafkaError.new(27,
880
+ "partitions ... too ... heavy ... must ... rebalance")
881
+ else
882
+ new_message
883
+ end
884
+ end
885
+ expect {
886
+ consumer.each_batch(max_items: 30, yield_on_error: false) do |batch, pending_error|
887
+ batches_yielded << batch
888
+ exceptions_yielded << pending_error
889
+ each_batch_iterations = each_batch_iterations + 1
890
+ end
891
+ }.to raise_error(Rdkafka::RdkafkaError)
892
+ expect(poll_count).to eq 3
893
+ expect(each_batch_iterations).to eq 0
894
+ expect(batches_yielded.size).to eq 0
895
+ expect(exceptions_yielded.size).to eq 0
896
+ end
897
+ end
898
+ end
899
+
673
900
  describe "a rebalance listener" do
674
901
  it "should get notifications" do
675
902
  listener = Struct.new(:queue) do
@@ -736,6 +963,7 @@ describe Rdkafka::Consumer do
736
963
  {
737
964
  :subscribe => [ nil ],
738
965
  :unsubscribe => nil,
966
+ :each_batch => nil,
739
967
  :pause => [ nil ],
740
968
  :resume => [ nil ],
741
969
  :subscription => nil,
@@ -12,47 +12,92 @@ describe Rdkafka::Producer do
12
12
  end
13
13
 
14
14
  context "delivery callback" do
15
- it "should set the callback" do
16
- expect {
17
- producer.delivery_callback = lambda do |delivery_handle|
18
- puts stats
15
+ context "with a proc/lambda" do
16
+ it "should set the callback" do
17
+ expect {
18
+ producer.delivery_callback = lambda do |delivery_handle|
19
+ puts delivery_handle
20
+ end
21
+ }.not_to raise_error
22
+ expect(producer.delivery_callback).to respond_to :call
23
+ end
24
+
25
+ it "should call the callback when a message is delivered" do
26
+ @callback_called = false
27
+
28
+ producer.delivery_callback = lambda do |report|
29
+ expect(report).not_to be_nil
30
+ expect(report.partition).to eq 1
31
+ expect(report.offset).to be >= 0
32
+ @callback_called = true
19
33
  end
20
- }.not_to raise_error
21
- expect(producer.delivery_callback).to be_a Proc
22
- end
23
34
 
24
- it "should not accept a callback that's not a proc" do
25
- expect {
26
- producer.delivery_callback = 'a string'
27
- }.to raise_error(TypeError)
28
- end
35
+ # Produce a message
36
+ handle = producer.produce(
37
+ topic: "produce_test_topic",
38
+ payload: "payload",
39
+ key: "key"
40
+ )
29
41
 
30
- it "should call the callback when a message is delivered" do
31
- @callback_called = false
42
+ # Wait for it to be delivered
43
+ handle.wait(max_wait_timeout: 15)
32
44
 
45
+ # Join the producer thread.
46
+ producer.close
33
47
 
34
- producer.delivery_callback = lambda do |report|
35
- expect(report).not_to be_nil
36
- expect(report.partition).to eq 1
37
- expect(report.offset).to be >= 0
38
- @callback_called = true
48
+ # Callback should have been called
49
+ expect(@callback_called).to be true
39
50
  end
51
+ end
40
52
 
41
- # Produce a message
42
- handle = producer.produce(
43
- topic: "produce_test_topic",
44
- payload: "payload",
45
- key: "key"
46
- )
53
+ context "with a callable object" do
54
+ it "should set the callback" do
55
+ callback = Class.new do
56
+ def call(stats); end
57
+ end
58
+ expect {
59
+ producer.delivery_callback = callback.new
60
+ }.not_to raise_error
61
+ expect(producer.delivery_callback).to respond_to :call
62
+ end
47
63
 
48
- # Wait for it to be delivered
49
- handle.wait(max_wait_timeout: 15)
64
+ it "should call the callback when a message is delivered" do
65
+ called_report = []
66
+ callback = Class.new do
67
+ def initialize(called_report)
68
+ @called_report = called_report
69
+ end
50
70
 
51
- # Join the producer thread.
52
- producer.close
71
+ def call(report)
72
+ @called_report << report
73
+ end
74
+ end
75
+ producer.delivery_callback = callback.new(called_report)
76
+
77
+ # Produce a message
78
+ handle = producer.produce(
79
+ topic: "produce_test_topic",
80
+ payload: "payload",
81
+ key: "key"
82
+ )
83
+
84
+ # Wait for it to be delivered
85
+ handle.wait(max_wait_timeout: 15)
86
+
87
+ # Join the producer thread.
88
+ producer.close
53
89
 
54
- # Callback should have been called
55
- expect(@callback_called).to be true
90
+ # Callback should have been called
91
+ expect(called_report.first).not_to be_nil
92
+ expect(called_report.first.partition).to eq 1
93
+ expect(called_report.first.offset).to be >= 0
94
+ end
95
+ end
96
+
97
+ it "should not accept a callback that's not callable" do
98
+ expect {
99
+ producer.delivery_callback = 'a string'
100
+ }.to raise_error(TypeError)
56
101
  end
57
102
  end
58
103
 
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: rdkafka
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.8.1
4
+ version: 0.9.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Thijs Cadier
8
- autorequire:
8
+ autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2020-12-07 00:00:00.000000000 Z
11
+ date: 2021-06-23 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: ffi
@@ -172,7 +172,7 @@ homepage: https://github.com/thijsc/rdkafka-ruby
172
172
  licenses:
173
173
  - MIT
174
174
  metadata: {}
175
- post_install_message:
175
+ post_install_message:
176
176
  rdoc_options: []
177
177
  require_paths:
178
178
  - lib
@@ -187,8 +187,8 @@ required_rubygems_version: !ruby/object:Gem::Requirement
187
187
  - !ruby/object:Gem::Version
188
188
  version: '0'
189
189
  requirements: []
190
- rubygems_version: 3.1.2
191
- signing_key:
190
+ rubygems_version: 3.2.3
191
+ signing_key:
192
192
  specification_version: 4
193
193
  summary: The rdkafka gem is a modern Kafka client library for Ruby based on librdkafka.
194
194
  It wraps the production-ready C client using the ffi gem and targets Kafka 1.0+