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 +4 -4
- data/.semaphore/semaphore.yml +3 -3
- data/CHANGELOG.md +8 -0
- data/ext/README.md +7 -0
- data/ext/Rakefile +1 -0
- data/lib/rdkafka/admin.rb +12 -1
- data/lib/rdkafka/bindings.rb +2 -1
- data/lib/rdkafka/config.rb +20 -2
- data/lib/rdkafka/consumer.rb +90 -0
- data/lib/rdkafka/producer.rb +7 -9
- data/lib/rdkafka/version.rb +1 -1
- data/spec/rdkafka/admin_spec.rb +12 -1
- data/spec/rdkafka/bindings_spec.rb +8 -8
- data/spec/rdkafka/config_spec.rb +34 -7
- data/spec/rdkafka/consumer_spec.rb +228 -0
- data/spec/rdkafka/producer_spec.rb +76 -31
- metadata +6 -6
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 22bda74a815bebd487179ac5ec3faa4566752f2c87945ca5c461c427633daf1f
|
4
|
+
data.tar.gz: '0802ca4118ed248fa11226a2c1b2140d073d230c02e3696a03e462fe59cb129e'
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 4d3b7042fa2290386d4feaaa01c4289bc858042d66806157066257f282c84db459e80e32a2613efd3452a46c63faaff7fad9f110fb153c3385cef00fb6017f27
|
7
|
+
data.tar.gz: e431a320535df8293e099e1b25bf0e6da68caf9a641621faf847dd206093dd9973b41f03f95412c43a740b50a0ef70e7caad418ff20c07218f19342d1cb3826a
|
data/.semaphore/semaphore.yml
CHANGED
@@ -3,7 +3,7 @@ name: Rdkafka Ruby
|
|
3
3
|
|
4
4
|
agent:
|
5
5
|
machine:
|
6
|
-
type: e1-standard-
|
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/
|
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
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)
|
data/lib/rdkafka/bindings.rb
CHANGED
@@ -130,7 +130,7 @@ module Rdkafka
|
|
130
130
|
else
|
131
131
|
Logger::UNKNOWN
|
132
132
|
end
|
133
|
-
Rdkafka::Config.
|
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
|
data/lib/rdkafka/config.rb
CHANGED
@@ -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
|
58
|
+
raise TypeError.new("Callback has to be callable") unless callback.respond_to?(:call)
|
41
59
|
@@statistics_callback = callback
|
42
60
|
end
|
43
61
|
|
data/lib/rdkafka/consumer.rb
CHANGED
@@ -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
|
data/lib/rdkafka/producer.rb
CHANGED
@@ -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
|
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
|
-
|
145
|
-
|
146
|
-
|
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
|
150
|
+
args << :int << Rdkafka::Bindings::RD_KAFKA_VTYPE_END
|
153
151
|
|
154
152
|
# Produce the message
|
155
153
|
response = Rdkafka::Bindings.rd_kafka_producev(
|
data/lib/rdkafka/version.rb
CHANGED
data/spec/rdkafka/admin_spec.rb
CHANGED
@@ -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(:
|
28
|
+
let(:log_queue) { Rdkafka::Config.log_queue }
|
29
29
|
before do
|
30
|
-
|
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(
|
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(
|
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(
|
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(
|
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(
|
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(
|
60
|
+
expect(log_queue).to have_received(:<<).with([Logger::UNKNOWN, "rdkafka: log line"])
|
61
61
|
end
|
62
62
|
end
|
63
63
|
|
data/spec/rdkafka/config_spec.rb
CHANGED
@@ -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
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
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
|
-
|
30
|
-
|
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
|
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
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
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
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
35
|
+
# Produce a message
|
36
|
+
handle = producer.produce(
|
37
|
+
topic: "produce_test_topic",
|
38
|
+
payload: "payload",
|
39
|
+
key: "key"
|
40
|
+
)
|
29
41
|
|
30
|
-
|
31
|
-
|
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
|
-
|
35
|
-
expect(
|
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
|
-
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
|
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
|
-
|
49
|
-
|
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
|
-
|
52
|
-
|
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
|
-
|
55
|
-
|
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.
|
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:
|
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.
|
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+
|