rdkafka 0.8.1 → 0.9.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.
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+