karafka-rdkafka 0.12.4 → 0.13.0.beta1
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
- checksums.yaml.gz.sig +0 -0
- data/.github/workflows/ci.yml +1 -1
- data/CHANGELOG.md +21 -2
- data/Gemfile +2 -0
- data/README.md +26 -0
- data/Rakefile +2 -0
- data/certs/cert_chain.pem +21 -21
- data/certs/karafka-pro.pem +11 -0
- data/ext/Rakefile +26 -53
- data/karafka-rdkafka.gemspec +2 -0
- data/lib/rdkafka/abstract_handle.rb +2 -0
- data/lib/rdkafka/admin/create_topic_handle.rb +2 -0
- data/lib/rdkafka/admin/create_topic_report.rb +2 -0
- data/lib/rdkafka/admin/delete_topic_handle.rb +2 -0
- data/lib/rdkafka/admin/delete_topic_report.rb +2 -0
- data/lib/rdkafka/admin.rb +95 -73
- data/lib/rdkafka/bindings.rb +52 -37
- data/lib/rdkafka/callbacks.rb +2 -0
- data/lib/rdkafka/config.rb +13 -10
- data/lib/rdkafka/consumer/headers.rb +24 -7
- data/lib/rdkafka/consumer/message.rb +3 -1
- data/lib/rdkafka/consumer/partition.rb +2 -0
- data/lib/rdkafka/consumer/topic_partition_list.rb +2 -0
- data/lib/rdkafka/consumer.rb +100 -44
- data/lib/rdkafka/error.rb +9 -0
- data/lib/rdkafka/metadata.rb +25 -2
- data/lib/rdkafka/native_kafka.rb +83 -0
- data/lib/rdkafka/producer/delivery_handle.rb +2 -0
- data/lib/rdkafka/producer/delivery_report.rb +3 -1
- data/lib/rdkafka/producer.rb +75 -12
- data/lib/rdkafka/version.rb +3 -1
- data/lib/rdkafka.rb +3 -1
- data/spec/rdkafka/abstract_handle_spec.rb +2 -0
- data/spec/rdkafka/admin/create_topic_handle_spec.rb +2 -0
- data/spec/rdkafka/admin/create_topic_report_spec.rb +2 -0
- data/spec/rdkafka/admin/delete_topic_handle_spec.rb +2 -0
- data/spec/rdkafka/admin/delete_topic_report_spec.rb +2 -0
- data/spec/rdkafka/admin_spec.rb +4 -3
- data/spec/rdkafka/bindings_spec.rb +2 -0
- data/spec/rdkafka/callbacks_spec.rb +2 -0
- data/spec/rdkafka/config_spec.rb +17 -2
- data/spec/rdkafka/consumer/headers_spec.rb +62 -0
- data/spec/rdkafka/consumer/message_spec.rb +2 -0
- data/spec/rdkafka/consumer/partition_spec.rb +2 -0
- data/spec/rdkafka/consumer/topic_partition_list_spec.rb +2 -0
- data/spec/rdkafka/consumer_spec.rb +124 -22
- data/spec/rdkafka/error_spec.rb +2 -0
- data/spec/rdkafka/metadata_spec.rb +2 -0
- data/spec/rdkafka/{producer/client_spec.rb → native_kafka_spec.rb} +13 -34
- data/spec/rdkafka/producer/delivery_handle_spec.rb +2 -0
- data/spec/rdkafka/producer/delivery_report_spec.rb +4 -2
- data/spec/rdkafka/producer_spec.rb +118 -17
- data/spec/spec_helper.rb +17 -1
- data.tar.gz.sig +0 -0
- metadata +33 -33
- metadata.gz.sig +0 -0
- data/bin/console +0 -11
- data/dist/librdkafka_2.0.2.tar.gz +0 -0
- data/lib/rdkafka/producer/client.rb +0 -47
data/lib/rdkafka/error.rb
CHANGED
@@ -1,3 +1,5 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
module Rdkafka
|
2
4
|
# Base error class.
|
3
5
|
class BaseError < RuntimeError; end
|
@@ -83,4 +85,11 @@ module Rdkafka
|
|
83
85
|
super("Illegal call to #{method.to_s} on a closed producer")
|
84
86
|
end
|
85
87
|
end
|
88
|
+
|
89
|
+
# Error class for public consumer method calls on a closed admin.
|
90
|
+
class ClosedAdminError < BaseError
|
91
|
+
def initialize(method)
|
92
|
+
super("Illegal call to #{method.to_s} on a closed admin")
|
93
|
+
end
|
94
|
+
end
|
86
95
|
end
|
data/lib/rdkafka/metadata.rb
CHANGED
@@ -1,8 +1,21 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
module Rdkafka
|
2
4
|
class Metadata
|
3
5
|
attr_reader :brokers, :topics
|
4
6
|
|
5
|
-
|
7
|
+
# Errors upon which we retry the metadata fetch
|
8
|
+
RETRIED_ERRORS = %i[
|
9
|
+
timed_out
|
10
|
+
leader_not_available
|
11
|
+
].freeze
|
12
|
+
|
13
|
+
private_constant :RETRIED_ERRORS
|
14
|
+
|
15
|
+
def initialize(native_client, topic_name = nil, timeout_ms = 2_000)
|
16
|
+
attempt ||= 0
|
17
|
+
attempt += 1
|
18
|
+
|
6
19
|
native_topic = if topic_name
|
7
20
|
Rdkafka::Bindings.rd_kafka_topic_new(native_client, topic_name, nil)
|
8
21
|
end
|
@@ -14,12 +27,22 @@ module Rdkafka
|
|
14
27
|
topic_flag = topic_name.nil? ? 1 : 0
|
15
28
|
|
16
29
|
# Retrieve the Metadata
|
17
|
-
result = Rdkafka::Bindings.rd_kafka_metadata(native_client, topic_flag, native_topic, ptr,
|
30
|
+
result = Rdkafka::Bindings.rd_kafka_metadata(native_client, topic_flag, native_topic, ptr, timeout_ms)
|
18
31
|
|
19
32
|
# Error Handling
|
20
33
|
raise Rdkafka::RdkafkaError.new(result) unless result.zero?
|
21
34
|
|
22
35
|
metadata_from_native(ptr.read_pointer)
|
36
|
+
rescue ::Rdkafka::RdkafkaError => e
|
37
|
+
raise unless RETRIED_ERRORS.include?(e.code)
|
38
|
+
raise if attempt > 10
|
39
|
+
|
40
|
+
backoff_factor = 2**attempt
|
41
|
+
timeout = backoff_factor * 0.1
|
42
|
+
|
43
|
+
sleep(timeout)
|
44
|
+
|
45
|
+
retry
|
23
46
|
ensure
|
24
47
|
Rdkafka::Bindings.rd_kafka_topic_destroy(native_topic) if topic_name
|
25
48
|
Rdkafka::Bindings.rd_kafka_metadata_destroy(ptr.read_pointer)
|
@@ -0,0 +1,83 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Rdkafka
|
4
|
+
# @private
|
5
|
+
# A wrapper around a native kafka that polls and cleanly exits
|
6
|
+
class NativeKafka
|
7
|
+
def initialize(inner, run_polling_thread:)
|
8
|
+
@inner = inner
|
9
|
+
# Lock around external access
|
10
|
+
@access_mutex = Mutex.new
|
11
|
+
# Lock around internal polling
|
12
|
+
@poll_mutex = Mutex.new
|
13
|
+
|
14
|
+
if run_polling_thread
|
15
|
+
# Start thread to poll client for delivery callbacks,
|
16
|
+
# not used in consumer.
|
17
|
+
@polling_thread = Thread.new do
|
18
|
+
loop do
|
19
|
+
@poll_mutex.synchronize do
|
20
|
+
Rdkafka::Bindings.rd_kafka_poll(inner, 100)
|
21
|
+
end
|
22
|
+
|
23
|
+
# Exit thread if closing and the poll queue is empty
|
24
|
+
if Thread.current[:closing] && Rdkafka::Bindings.rd_kafka_outq_len(inner) == 0
|
25
|
+
break
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
@polling_thread.abort_on_exception = true
|
31
|
+
@polling_thread[:closing] = false
|
32
|
+
end
|
33
|
+
|
34
|
+
@closing = false
|
35
|
+
end
|
36
|
+
|
37
|
+
def with_inner
|
38
|
+
return if @inner.nil?
|
39
|
+
|
40
|
+
@access_mutex.synchronize do
|
41
|
+
yield @inner
|
42
|
+
end
|
43
|
+
end
|
44
|
+
|
45
|
+
def finalizer
|
46
|
+
->(_) { close }
|
47
|
+
end
|
48
|
+
|
49
|
+
def closed?
|
50
|
+
@closing || @inner.nil?
|
51
|
+
end
|
52
|
+
|
53
|
+
def close(object_id=nil)
|
54
|
+
return if closed?
|
55
|
+
|
56
|
+
@access_mutex.lock
|
57
|
+
|
58
|
+
# Indicate to the outside world that we are closing
|
59
|
+
@closing = true
|
60
|
+
|
61
|
+
if @polling_thread
|
62
|
+
# Indicate to polling thread that we're closing
|
63
|
+
@polling_thread[:closing] = true
|
64
|
+
|
65
|
+
# Wait for the polling thread to finish up,
|
66
|
+
# this can be aborted in practice if this
|
67
|
+
# code runs from a finalizer.
|
68
|
+
@polling_thread.join
|
69
|
+
end
|
70
|
+
|
71
|
+
# Destroy the client after locking both mutexes
|
72
|
+
@poll_mutex.lock
|
73
|
+
|
74
|
+
# This check prevents a race condition, where we would enter the close in two threads
|
75
|
+
# and after unlocking the primary one that hold the lock but finished, ours would be unlocked
|
76
|
+
# and would continue to run, trying to destroy inner twice
|
77
|
+
return unless @inner
|
78
|
+
|
79
|
+
Rdkafka::Bindings.rd_kafka_destroy(@inner)
|
80
|
+
@inner = nil
|
81
|
+
end
|
82
|
+
end
|
83
|
+
end
|
@@ -1,3 +1,5 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
module Rdkafka
|
2
4
|
class Producer
|
3
5
|
# Delivery report for a successfully produced message.
|
@@ -15,7 +17,7 @@ module Rdkafka
|
|
15
17
|
attr_reader :topic_name
|
16
18
|
|
17
19
|
# Error in case happen during produce.
|
18
|
-
# @return [
|
20
|
+
# @return [Integer]
|
19
21
|
attr_reader :error
|
20
22
|
|
21
23
|
private
|
data/lib/rdkafka/producer.rb
CHANGED
@@ -1,8 +1,15 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
require "objspace"
|
2
4
|
|
3
5
|
module Rdkafka
|
4
6
|
# A producer for Kafka messages. To create a producer set up a {Config} and call {Config#producer producer} on that.
|
5
7
|
class Producer
|
8
|
+
# Cache partitions count for 30 seconds
|
9
|
+
PARTITIONS_COUNT_TTL = 30
|
10
|
+
|
11
|
+
private_constant :PARTITIONS_COUNT_TTL
|
12
|
+
|
6
13
|
# @private
|
7
14
|
# Returns the current delivery callback, by default this is nil.
|
8
15
|
#
|
@@ -16,12 +23,32 @@ module Rdkafka
|
|
16
23
|
attr_reader :delivery_callback_arity
|
17
24
|
|
18
25
|
# @private
|
19
|
-
def initialize(
|
20
|
-
@
|
26
|
+
def initialize(native_kafka, partitioner_name)
|
27
|
+
@native_kafka = native_kafka
|
21
28
|
@partitioner_name = partitioner_name || "consistent_random"
|
22
29
|
|
23
|
-
# Makes sure, that
|
24
|
-
ObjectSpace.define_finalizer(self,
|
30
|
+
# Makes sure, that native kafka gets closed before it gets GCed by Ruby
|
31
|
+
ObjectSpace.define_finalizer(self, native_kafka.finalizer)
|
32
|
+
|
33
|
+
@_partitions_count_cache = Hash.new do |cache, topic|
|
34
|
+
topic_metadata = nil
|
35
|
+
|
36
|
+
@native_kafka.with_inner do |inner|
|
37
|
+
topic_metadata = ::Rdkafka::Metadata.new(inner, topic).topics&.first
|
38
|
+
end
|
39
|
+
|
40
|
+
cache[topic] = [
|
41
|
+
monotonic_now,
|
42
|
+
topic_metadata ? topic_metadata[:partition_count] : nil
|
43
|
+
]
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
47
|
+
# @return [String] producer name
|
48
|
+
def name
|
49
|
+
@name ||= @native_kafka.with_inner do |inner|
|
50
|
+
::Rdkafka::Bindings.rd_kafka_name(inner)
|
51
|
+
end
|
25
52
|
end
|
26
53
|
|
27
54
|
# Set a callback that will be called every time a message is successfully produced.
|
@@ -38,9 +65,26 @@ module Rdkafka
|
|
38
65
|
|
39
66
|
# Close this producer and wait for the internal poll queue to empty.
|
40
67
|
def close
|
68
|
+
return if closed?
|
41
69
|
ObjectSpace.undefine_finalizer(self)
|
70
|
+
@native_kafka.close
|
71
|
+
end
|
42
72
|
|
43
|
-
|
73
|
+
# Whether this producer has closed
|
74
|
+
def closed?
|
75
|
+
@native_kafka.closed?
|
76
|
+
end
|
77
|
+
|
78
|
+
# Wait until all outstanding producer requests are completed, with the given timeout
|
79
|
+
# in seconds. Call this before closing a producer to ensure delivery of all messages.
|
80
|
+
#
|
81
|
+
# @param timeout_ms [Integer] how long should we wait for flush of all messages
|
82
|
+
def flush(timeout_ms=5_000)
|
83
|
+
closed_producer_check(__method__)
|
84
|
+
|
85
|
+
@native_kafka.with_inner do |inner|
|
86
|
+
Rdkafka::Bindings.rd_kafka_flush(inner, timeout_ms)
|
87
|
+
end
|
44
88
|
end
|
45
89
|
|
46
90
|
# Partition count for a given topic.
|
@@ -50,9 +94,20 @@ module Rdkafka
|
|
50
94
|
#
|
51
95
|
# @return partition count [Integer,nil]
|
52
96
|
#
|
97
|
+
# We cache the partition count for a given topic for given time
|
98
|
+
# This prevents us in case someone uses `partition_key` from querying for the count with
|
99
|
+
# each message. Instead we query once every 30 seconds at most
|
100
|
+
#
|
101
|
+
# @param topic [String] topic name
|
102
|
+
# @return [Integer] partition count for a given topic
|
53
103
|
def partition_count(topic)
|
54
104
|
closed_producer_check(__method__)
|
55
|
-
|
105
|
+
|
106
|
+
@_partitions_count_cache.delete_if do |_, cached|
|
107
|
+
monotonic_now - cached.first > PARTITIONS_COUNT_TTL
|
108
|
+
end
|
109
|
+
|
110
|
+
@_partitions_count_cache[topic].last
|
56
111
|
end
|
57
112
|
|
58
113
|
# Produces a message to a Kafka topic. The message is added to rdkafka's queue, call {DeliveryHandle#wait wait} on the returned delivery handle to make sure it is delivered.
|
@@ -143,10 +198,12 @@ module Rdkafka
|
|
143
198
|
args << :int << Rdkafka::Bindings::RD_KAFKA_VTYPE_END
|
144
199
|
|
145
200
|
# Produce the message
|
146
|
-
response =
|
147
|
-
|
148
|
-
|
149
|
-
|
201
|
+
response = @native_kafka.with_inner do |inner|
|
202
|
+
Rdkafka::Bindings.rd_kafka_producev(
|
203
|
+
inner,
|
204
|
+
*args
|
205
|
+
)
|
206
|
+
end
|
150
207
|
|
151
208
|
# Raise error if the produce call was not successful
|
152
209
|
if response != 0
|
@@ -157,7 +214,6 @@ module Rdkafka
|
|
157
214
|
delivery_handle
|
158
215
|
end
|
159
216
|
|
160
|
-
# @private
|
161
217
|
def call_delivery_callback(delivery_report, delivery_handle)
|
162
218
|
return unless @delivery_callback
|
163
219
|
|
@@ -171,8 +227,15 @@ module Rdkafka
|
|
171
227
|
callback.method(:call).arity
|
172
228
|
end
|
173
229
|
|
230
|
+
private
|
231
|
+
|
232
|
+
def monotonic_now
|
233
|
+
# needed because Time.now can go backwards
|
234
|
+
Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
235
|
+
end
|
236
|
+
|
174
237
|
def closed_producer_check(method)
|
175
|
-
raise Rdkafka::ClosedProducerError.new(method) if
|
238
|
+
raise Rdkafka::ClosedProducerError.new(method) if closed?
|
176
239
|
end
|
177
240
|
end
|
178
241
|
end
|
data/lib/rdkafka/version.rb
CHANGED
data/lib/rdkafka.rb
CHANGED
@@ -1,3 +1,5 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
require "rdkafka/version"
|
2
4
|
|
3
5
|
require "rdkafka/abstract_handle"
|
@@ -18,7 +20,7 @@ require "rdkafka/consumer/partition"
|
|
18
20
|
require "rdkafka/consumer/topic_partition_list"
|
19
21
|
require "rdkafka/error"
|
20
22
|
require "rdkafka/metadata"
|
23
|
+
require "rdkafka/native_kafka"
|
21
24
|
require "rdkafka/producer"
|
22
|
-
require "rdkafka/producer/client"
|
23
25
|
require "rdkafka/producer/delivery_handle"
|
24
26
|
require "rdkafka/producer/delivery_report"
|
data/spec/rdkafka/admin_spec.rb
CHANGED
@@ -1,9 +1,11 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
require "spec_helper"
|
2
4
|
require "ostruct"
|
3
5
|
|
4
6
|
describe Rdkafka::Admin do
|
5
|
-
let(:config)
|
6
|
-
let(:admin)
|
7
|
+
let(:config) { rdkafka_config }
|
8
|
+
let(:admin) { config.admin }
|
7
9
|
|
8
10
|
after do
|
9
11
|
# Registry should always end up being empty
|
@@ -174,7 +176,6 @@ describe Rdkafka::Admin do
|
|
174
176
|
end
|
175
177
|
end
|
176
178
|
|
177
|
-
|
178
179
|
it "deletes a topic that was newly created" do
|
179
180
|
create_topic_handle = admin.create_topic(topic_name, topic_partition_count, topic_replication_factor)
|
180
181
|
create_topic_report = create_topic_handle.wait(max_wait_timeout: 15.0)
|
data/spec/rdkafka/config_spec.rb
CHANGED
@@ -1,3 +1,5 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
require "spec_helper"
|
2
4
|
|
3
5
|
describe Rdkafka::Config do
|
@@ -148,11 +150,24 @@ describe Rdkafka::Config do
|
|
148
150
|
}.to raise_error(Rdkafka::Config::ConfigError, "No such configuration property: \"invalid.key\"")
|
149
151
|
end
|
150
152
|
|
153
|
+
it "allows string partitioner key" do
|
154
|
+
expect(Rdkafka::Producer).to receive(:new).with(kind_of(Rdkafka::NativeKafka), "murmur2").and_call_original
|
155
|
+
config = Rdkafka::Config.new("partitioner" => "murmur2")
|
156
|
+
config.producer.close
|
157
|
+
end
|
158
|
+
|
159
|
+
it "allows symbol partitioner key" do
|
160
|
+
expect(Rdkafka::Producer).to receive(:new).with(kind_of(Rdkafka::NativeKafka), "murmur2").and_call_original
|
161
|
+
config = Rdkafka::Config.new(:partitioner => "murmur2")
|
162
|
+
config.producer.close
|
163
|
+
end
|
164
|
+
|
151
165
|
it "should allow configuring zstd compression" do
|
152
166
|
config = Rdkafka::Config.new('compression.codec' => 'zstd')
|
153
167
|
begin
|
154
|
-
|
155
|
-
|
168
|
+
producer = config.producer
|
169
|
+
expect(producer).to be_a Rdkafka::Producer
|
170
|
+
producer.close
|
156
171
|
rescue Rdkafka::Config::ConfigError => ex
|
157
172
|
pending "Zstd compression not supported on this machine"
|
158
173
|
raise ex
|
@@ -0,0 +1,62 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "spec_helper"
|
4
|
+
|
5
|
+
describe Rdkafka::Consumer::Headers do
|
6
|
+
let(:headers) do
|
7
|
+
{ # Note String keys!
|
8
|
+
"version" => "2.1.3",
|
9
|
+
"type" => "String"
|
10
|
+
}
|
11
|
+
end
|
12
|
+
let(:native_message) { double('native message') }
|
13
|
+
let(:headers_ptr) { double('headers pointer') }
|
14
|
+
|
15
|
+
describe '.from_native' do
|
16
|
+
before do
|
17
|
+
expect(Rdkafka::Bindings).to receive(:rd_kafka_message_headers).with(native_message, anything) do |_, headers_ptrptr|
|
18
|
+
expect(headers_ptrptr).to receive(:read_pointer).and_return(headers_ptr)
|
19
|
+
Rdkafka::Bindings::RD_KAFKA_RESP_ERR_NO_ERROR
|
20
|
+
end
|
21
|
+
|
22
|
+
expect(Rdkafka::Bindings).to \
|
23
|
+
receive(:rd_kafka_header_get_all)
|
24
|
+
.with(headers_ptr, 0, anything, anything, anything) do |_, _, name_ptrptr, value_ptrptr, size_ptr|
|
25
|
+
expect(name_ptrptr).to receive(:read_pointer).and_return(double("pointer 0", read_string_to_null: headers.keys[0]))
|
26
|
+
expect(size_ptr).to receive(:[]).with(:value).and_return(headers.keys[0].size)
|
27
|
+
expect(value_ptrptr).to receive(:read_pointer).and_return(double("value pointer 0", read_string: headers.values[0]))
|
28
|
+
Rdkafka::Bindings::RD_KAFKA_RESP_ERR_NO_ERROR
|
29
|
+
end
|
30
|
+
|
31
|
+
expect(Rdkafka::Bindings).to \
|
32
|
+
receive(:rd_kafka_header_get_all)
|
33
|
+
.with(headers_ptr, 1, anything, anything, anything) do |_, _, name_ptrptr, value_ptrptr, size_ptr|
|
34
|
+
expect(name_ptrptr).to receive(:read_pointer).and_return(double("pointer 1", read_string_to_null: headers.keys[1]))
|
35
|
+
expect(size_ptr).to receive(:[]).with(:value).and_return(headers.keys[1].size)
|
36
|
+
expect(value_ptrptr).to receive(:read_pointer).and_return(double("value pointer 1", read_string: headers.values[1]))
|
37
|
+
Rdkafka::Bindings::RD_KAFKA_RESP_ERR_NO_ERROR
|
38
|
+
end
|
39
|
+
|
40
|
+
expect(Rdkafka::Bindings).to \
|
41
|
+
receive(:rd_kafka_header_get_all)
|
42
|
+
.with(headers_ptr, 2, anything, anything, anything)
|
43
|
+
.and_return(Rdkafka::Bindings::RD_KAFKA_RESP_ERR__NOENT)
|
44
|
+
end
|
45
|
+
|
46
|
+
subject { described_class.from_native(native_message) }
|
47
|
+
|
48
|
+
it { is_expected.to eq(headers) }
|
49
|
+
it { is_expected.to be_frozen }
|
50
|
+
|
51
|
+
it 'allows String key' do
|
52
|
+
expect(subject['version']).to eq("2.1.3")
|
53
|
+
end
|
54
|
+
|
55
|
+
it 'allows Symbol key, but warns' do
|
56
|
+
expect(Kernel).to \
|
57
|
+
receive(:warn).with("rdkafka deprecation warning: header access with Symbol key :version treated as a String. " \
|
58
|
+
"Please change your code to use String keys to avoid this warning. Symbol keys will break in version 1.")
|
59
|
+
expect(subject[:version]).to eq("2.1.3")
|
60
|
+
end
|
61
|
+
end
|
62
|
+
end
|