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 +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+
|