rdkafka 0.12.0 → 0.15.1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- checksums.yaml.gz.sig +0 -0
- data/.github/FUNDING.yml +1 -0
- data/.github/workflows/ci.yml +57 -0
- data/.gitignore +4 -0
- data/.rspec +1 -0
- data/.ruby-gemset +1 -0
- data/.ruby-version +1 -0
- data/CHANGELOG.md +155 -93
- data/Gemfile +2 -0
- data/{LICENSE → MIT-LICENSE} +2 -1
- data/README.md +76 -29
- data/Rakefile +2 -0
- data/certs/cert_chain.pem +26 -0
- data/docker-compose.yml +18 -15
- data/ext/README.md +1 -1
- data/ext/Rakefile +46 -27
- data/lib/rdkafka/abstract_handle.rb +41 -25
- data/lib/rdkafka/admin/acl_binding_result.rb +51 -0
- data/lib/rdkafka/admin/create_acl_handle.rb +28 -0
- data/lib/rdkafka/admin/create_acl_report.rb +24 -0
- data/lib/rdkafka/admin/create_partitions_handle.rb +27 -0
- data/lib/rdkafka/admin/create_partitions_report.rb +6 -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_acl_handle.rb +30 -0
- data/lib/rdkafka/admin/delete_acl_report.rb +23 -0
- data/lib/rdkafka/admin/delete_groups_handle.rb +28 -0
- data/lib/rdkafka/admin/delete_groups_report.rb +24 -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/describe_acl_handle.rb +30 -0
- data/lib/rdkafka/admin/describe_acl_report.rb +23 -0
- data/lib/rdkafka/admin.rb +494 -35
- data/lib/rdkafka/bindings.rb +180 -41
- data/lib/rdkafka/callbacks.rb +202 -1
- data/lib/rdkafka/config.rb +62 -25
- data/lib/rdkafka/consumer/headers.rb +24 -9
- data/lib/rdkafka/consumer/message.rb +3 -1
- data/lib/rdkafka/consumer/partition.rb +2 -0
- data/lib/rdkafka/consumer/topic_partition_list.rb +13 -8
- data/lib/rdkafka/consumer.rb +243 -111
- data/lib/rdkafka/error.rb +15 -0
- data/lib/rdkafka/helpers/time.rb +14 -0
- data/lib/rdkafka/metadata.rb +25 -2
- data/lib/rdkafka/native_kafka.rb +120 -0
- data/lib/rdkafka/producer/delivery_handle.rb +16 -2
- data/lib/rdkafka/producer/delivery_report.rb +22 -2
- data/lib/rdkafka/producer.rb +151 -21
- data/lib/rdkafka/version.rb +5 -3
- data/lib/rdkafka.rb +24 -2
- data/rdkafka.gemspec +21 -5
- data/renovate.json +6 -0
- data/spec/rdkafka/abstract_handle_spec.rb +1 -1
- data/spec/rdkafka/admin/create_acl_handle_spec.rb +56 -0
- data/spec/rdkafka/admin/create_acl_report_spec.rb +18 -0
- data/spec/rdkafka/admin/create_topic_handle_spec.rb +1 -1
- data/spec/rdkafka/admin/create_topic_report_spec.rb +1 -1
- data/spec/rdkafka/admin/delete_acl_handle_spec.rb +85 -0
- data/spec/rdkafka/admin/delete_acl_report_spec.rb +72 -0
- data/spec/rdkafka/admin/delete_topic_handle_spec.rb +1 -1
- data/spec/rdkafka/admin/delete_topic_report_spec.rb +1 -1
- data/spec/rdkafka/admin/describe_acl_handle_spec.rb +85 -0
- data/spec/rdkafka/admin/describe_acl_report_spec.rb +73 -0
- data/spec/rdkafka/admin_spec.rb +209 -5
- data/spec/rdkafka/bindings_spec.rb +2 -1
- data/spec/rdkafka/callbacks_spec.rb +1 -1
- data/spec/rdkafka/config_spec.rb +24 -3
- data/spec/rdkafka/consumer/headers_spec.rb +60 -0
- data/spec/rdkafka/consumer/message_spec.rb +1 -1
- data/spec/rdkafka/consumer/partition_spec.rb +1 -1
- data/spec/rdkafka/consumer/topic_partition_list_spec.rb +20 -1
- data/spec/rdkafka/consumer_spec.rb +352 -61
- data/spec/rdkafka/error_spec.rb +1 -1
- data/spec/rdkafka/metadata_spec.rb +4 -3
- data/spec/rdkafka/{producer/client_spec.rb → native_kafka_spec.rb} +13 -35
- data/spec/rdkafka/producer/delivery_handle_spec.rb +4 -1
- data/spec/rdkafka/producer/delivery_report_spec.rb +11 -3
- data/spec/rdkafka/producer_spec.rb +234 -22
- data/spec/spec_helper.rb +20 -2
- data.tar.gz.sig +0 -0
- metadata +81 -17
- metadata.gz.sig +0 -0
- data/.semaphore/semaphore.yml +0 -23
- data/bin/console +0 -11
- data/lib/rdkafka/producer/client.rb +0 -47
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,120 @@
|
|
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:, opaque:)
|
8
|
+
@inner = inner
|
9
|
+
@opaque = opaque
|
10
|
+
# Lock around external access
|
11
|
+
@access_mutex = Mutex.new
|
12
|
+
# Lock around internal polling
|
13
|
+
@poll_mutex = Mutex.new
|
14
|
+
# Lock around decrementing the operations in progress counter
|
15
|
+
# We have two mutexes - one for increment (`@access_mutex`) and one for decrement mutex
|
16
|
+
# because they serve different purposes:
|
17
|
+
#
|
18
|
+
# - `@access_mutex` allows us to lock the execution and make sure that any operation within
|
19
|
+
# the `#synchronize` is the only one running and that there are no other running
|
20
|
+
# operations.
|
21
|
+
# - `@decrement_mutex` ensures, that our decrement operation is thread-safe for any Ruby
|
22
|
+
# implementation.
|
23
|
+
#
|
24
|
+
# We do not use the same mutex, because it could create a deadlock when an already
|
25
|
+
# incremented operation cannot decrement because `@access_lock` is now owned by a different
|
26
|
+
# thread in a synchronized mode and the synchronized mode is waiting on the decrement.
|
27
|
+
@decrement_mutex = Mutex.new
|
28
|
+
# counter for operations in progress using inner
|
29
|
+
@operations_in_progress = 0
|
30
|
+
|
31
|
+
# Trigger initial poll to make sure oauthbearer cb and other initial cb are handled
|
32
|
+
Rdkafka::Bindings.rd_kafka_poll(inner, 0)
|
33
|
+
|
34
|
+
if run_polling_thread
|
35
|
+
# Start thread to poll client for delivery callbacks,
|
36
|
+
# not used in consumer.
|
37
|
+
@polling_thread = Thread.new do
|
38
|
+
loop do
|
39
|
+
@poll_mutex.synchronize do
|
40
|
+
Rdkafka::Bindings.rd_kafka_poll(inner, 100)
|
41
|
+
end
|
42
|
+
|
43
|
+
# Exit thread if closing and the poll queue is empty
|
44
|
+
if Thread.current[:closing] && Rdkafka::Bindings.rd_kafka_outq_len(inner) == 0
|
45
|
+
break
|
46
|
+
end
|
47
|
+
end
|
48
|
+
end
|
49
|
+
|
50
|
+
@polling_thread.abort_on_exception = true
|
51
|
+
@polling_thread[:closing] = false
|
52
|
+
end
|
53
|
+
|
54
|
+
@closing = false
|
55
|
+
end
|
56
|
+
|
57
|
+
def with_inner
|
58
|
+
if @access_mutex.owned?
|
59
|
+
@operations_in_progress += 1
|
60
|
+
else
|
61
|
+
@access_mutex.synchronize { @operations_in_progress += 1 }
|
62
|
+
end
|
63
|
+
|
64
|
+
@inner.nil? ? raise(ClosedInnerError) : yield(@inner)
|
65
|
+
ensure
|
66
|
+
@decrement_mutex.synchronize { @operations_in_progress -= 1 }
|
67
|
+
end
|
68
|
+
|
69
|
+
def synchronize(&block)
|
70
|
+
@access_mutex.synchronize do
|
71
|
+
# Wait for any commands using the inner to finish
|
72
|
+
# This can take a while on blocking operations like polling but is essential not to proceed
|
73
|
+
# with certain types of operations like resources destruction as it can cause the process
|
74
|
+
# to hang or crash
|
75
|
+
sleep(0.01) until @operations_in_progress.zero?
|
76
|
+
|
77
|
+
with_inner(&block)
|
78
|
+
end
|
79
|
+
end
|
80
|
+
|
81
|
+
def finalizer
|
82
|
+
->(_) { close }
|
83
|
+
end
|
84
|
+
|
85
|
+
def closed?
|
86
|
+
@closing || @inner.nil?
|
87
|
+
end
|
88
|
+
|
89
|
+
def close(object_id=nil)
|
90
|
+
return if closed?
|
91
|
+
|
92
|
+
synchronize do
|
93
|
+
# Indicate to the outside world that we are closing
|
94
|
+
@closing = true
|
95
|
+
|
96
|
+
if @polling_thread
|
97
|
+
# Indicate to polling thread that we're closing
|
98
|
+
@polling_thread[:closing] = true
|
99
|
+
|
100
|
+
# Wait for the polling thread to finish up,
|
101
|
+
# this can be aborted in practice if this
|
102
|
+
# code runs from a finalizer.
|
103
|
+
@polling_thread.join
|
104
|
+
end
|
105
|
+
|
106
|
+
# Destroy the client after locking both mutexes
|
107
|
+
@poll_mutex.lock
|
108
|
+
|
109
|
+
# This check prevents a race condition, where we would enter the close in two threads
|
110
|
+
# and after unlocking the primary one that hold the lock but finished, ours would be unlocked
|
111
|
+
# and would continue to run, trying to destroy inner twice
|
112
|
+
return unless @inner
|
113
|
+
|
114
|
+
Rdkafka::Bindings.rd_kafka_destroy(@inner)
|
115
|
+
@inner = nil
|
116
|
+
@opaque = nil
|
117
|
+
end
|
118
|
+
end
|
119
|
+
end
|
120
|
+
end
|
@@ -1,3 +1,5 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
module Rdkafka
|
2
4
|
class Producer
|
3
5
|
# Handle to wait for a delivery report which is returned when
|
@@ -6,7 +8,11 @@ module Rdkafka
|
|
6
8
|
layout :pending, :bool,
|
7
9
|
:response, :int,
|
8
10
|
:partition, :int,
|
9
|
-
:offset, :int64
|
11
|
+
:offset, :int64,
|
12
|
+
:topic_name, :pointer
|
13
|
+
|
14
|
+
# @return [Object, nil] label set during message production or nil by default
|
15
|
+
attr_accessor :label
|
10
16
|
|
11
17
|
# @return [String] the name of the operation (e.g. "delivery")
|
12
18
|
def operation_name
|
@@ -15,7 +21,15 @@ module Rdkafka
|
|
15
21
|
|
16
22
|
# @return [DeliveryReport] a report on the delivery of the message
|
17
23
|
def create_result
|
18
|
-
DeliveryReport.new(
|
24
|
+
DeliveryReport.new(
|
25
|
+
self[:partition],
|
26
|
+
self[:offset],
|
27
|
+
# For part of errors, we will not get a topic name reference and in cases like this
|
28
|
+
# we should not return it
|
29
|
+
self[:topic_name].null? ? nil : self[:topic_name].read_string,
|
30
|
+
self[:response] != 0 ? RdkafkaError.new(self[:response]) : nil,
|
31
|
+
label
|
32
|
+
)
|
19
33
|
end
|
20
34
|
end
|
21
35
|
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.
|
@@ -10,16 +12,34 @@ module Rdkafka
|
|
10
12
|
# @return [Integer]
|
11
13
|
attr_reader :offset
|
12
14
|
|
15
|
+
# The name of the topic this message was produced to or nil in case of reports with errors
|
16
|
+
# where topic was not reached.
|
17
|
+
#
|
18
|
+
# @return [String, nil]
|
19
|
+
attr_reader :topic_name
|
20
|
+
|
13
21
|
# Error in case happen during produce.
|
14
|
-
# @return [
|
22
|
+
# @return [Integer]
|
15
23
|
attr_reader :error
|
16
24
|
|
25
|
+
# @return [Object, nil] label set during message production or nil by default
|
26
|
+
attr_reader :label
|
27
|
+
|
28
|
+
# We alias the `#topic_name` under `#topic` to make this consistent with `Consumer::Message`
|
29
|
+
# where the topic name is under `#topic` method. That way we have a consistent name that
|
30
|
+
# is present in both places
|
31
|
+
#
|
32
|
+
# We do not remove the original `#topic_name` because of backwards compatibility
|
33
|
+
alias topic topic_name
|
34
|
+
|
17
35
|
private
|
18
36
|
|
19
|
-
def initialize(partition, offset, error = nil)
|
37
|
+
def initialize(partition, offset, topic_name = nil, error = nil, label = nil)
|
20
38
|
@partition = partition
|
21
39
|
@offset = offset
|
40
|
+
@topic_name = topic_name
|
22
41
|
@error = error
|
42
|
+
@label = label
|
23
43
|
end
|
24
44
|
end
|
25
45
|
end
|
data/lib/rdkafka/producer.rb
CHANGED
@@ -1,8 +1,15 @@
|
|
1
|
-
|
1
|
+
# frozen_string_literal: true
|
2
2
|
|
3
3
|
module Rdkafka
|
4
4
|
# A producer for Kafka messages. To create a producer set up a {Config} and call {Config#producer producer} on that.
|
5
5
|
class Producer
|
6
|
+
include Helpers::Time
|
7
|
+
|
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,41 @@ module Rdkafka
|
|
16
23
|
attr_reader :delivery_callback_arity
|
17
24
|
|
18
25
|
# @private
|
19
|
-
|
20
|
-
|
26
|
+
# @param native_kafka [NativeKafka]
|
27
|
+
# @param partitioner_name [String, nil] name of the partitioner we want to use or nil to use
|
28
|
+
# the "consistent_random" default
|
29
|
+
def initialize(native_kafka, partitioner_name)
|
30
|
+
@native_kafka = native_kafka
|
21
31
|
@partitioner_name = partitioner_name || "consistent_random"
|
22
32
|
|
23
|
-
# Makes sure, that
|
24
|
-
ObjectSpace.define_finalizer(self,
|
33
|
+
# Makes sure, that native kafka gets closed before it gets GCed by Ruby
|
34
|
+
ObjectSpace.define_finalizer(self, native_kafka.finalizer)
|
35
|
+
|
36
|
+
@_partitions_count_cache = Hash.new do |cache, topic|
|
37
|
+
topic_metadata = nil
|
38
|
+
|
39
|
+
@native_kafka.with_inner do |inner|
|
40
|
+
topic_metadata = ::Rdkafka::Metadata.new(inner, topic).topics&.first
|
41
|
+
end
|
42
|
+
|
43
|
+
partition_count = topic_metadata ? topic_metadata[:partition_count] : -1
|
44
|
+
|
45
|
+
# This approach caches the failure to fetch only for 1 second. This will make sure, that
|
46
|
+
# we do not cache the failure for too long but also "buys" us a bit of time in case there
|
47
|
+
# would be issues in the cluster so we won't overaload it with consecutive requests
|
48
|
+
cache[topic] = if partition_count.positive?
|
49
|
+
[monotonic_now, partition_count]
|
50
|
+
else
|
51
|
+
[monotonic_now - PARTITIONS_COUNT_TTL + 5, partition_count]
|
52
|
+
end
|
53
|
+
end
|
54
|
+
end
|
55
|
+
|
56
|
+
# @return [String] producer name
|
57
|
+
def name
|
58
|
+
@name ||= @native_kafka.with_inner do |inner|
|
59
|
+
::Rdkafka::Bindings.rd_kafka_name(inner)
|
60
|
+
end
|
25
61
|
end
|
26
62
|
|
27
63
|
# Set a callback that will be called every time a message is successfully produced.
|
@@ -38,21 +74,92 @@ module Rdkafka
|
|
38
74
|
|
39
75
|
# Close this producer and wait for the internal poll queue to empty.
|
40
76
|
def close
|
77
|
+
return if closed?
|
41
78
|
ObjectSpace.undefine_finalizer(self)
|
79
|
+
@native_kafka.close
|
80
|
+
end
|
81
|
+
|
82
|
+
# Whether this producer has closed
|
83
|
+
def closed?
|
84
|
+
@native_kafka.closed?
|
85
|
+
end
|
86
|
+
|
87
|
+
# Wait until all outstanding producer requests are completed, with the given timeout
|
88
|
+
# in seconds. Call this before closing a producer to ensure delivery of all messages.
|
89
|
+
#
|
90
|
+
# @param timeout_ms [Integer] how long should we wait for flush of all messages
|
91
|
+
# @return [Boolean] true if no more data and all was flushed, false in case there are still
|
92
|
+
# outgoing messages after the timeout
|
93
|
+
#
|
94
|
+
# @note We raise an exception for other errors because based on the librdkafka docs, there
|
95
|
+
# should be no other errors.
|
96
|
+
#
|
97
|
+
# @note For `timed_out` we do not raise an error to keep it backwards compatible
|
98
|
+
def flush(timeout_ms=5_000)
|
99
|
+
closed_producer_check(__method__)
|
100
|
+
|
101
|
+
code = nil
|
102
|
+
|
103
|
+
@native_kafka.with_inner do |inner|
|
104
|
+
code = Rdkafka::Bindings.rd_kafka_flush(inner, timeout_ms)
|
105
|
+
end
|
106
|
+
|
107
|
+
# Early skip not to build the error message
|
108
|
+
return true if code.zero?
|
109
|
+
|
110
|
+
error = Rdkafka::RdkafkaError.new(code)
|
42
111
|
|
43
|
-
|
112
|
+
return false if error.code == :timed_out
|
113
|
+
|
114
|
+
raise(error)
|
115
|
+
end
|
116
|
+
|
117
|
+
# Purges the outgoing queue and releases all resources.
|
118
|
+
#
|
119
|
+
# Useful when closing the producer with outgoing messages to unstable clusters or when for
|
120
|
+
# any other reasons waiting cannot go on anymore. This purges both the queue and all the
|
121
|
+
# inflight requests + updates the delivery handles statuses so they can be materialized into
|
122
|
+
# `purge_queue` errors.
|
123
|
+
def purge
|
124
|
+
closed_producer_check(__method__)
|
125
|
+
|
126
|
+
code = nil
|
127
|
+
|
128
|
+
@native_kafka.with_inner do |inner|
|
129
|
+
code = Bindings.rd_kafka_purge(
|
130
|
+
inner,
|
131
|
+
Bindings::RD_KAFKA_PURGE_F_QUEUE | Bindings::RD_KAFKA_PURGE_F_INFLIGHT
|
132
|
+
)
|
133
|
+
end
|
134
|
+
|
135
|
+
code.zero? || raise(Rdkafka::RdkafkaError.new(code))
|
136
|
+
|
137
|
+
# Wait for the purge to affect everything
|
138
|
+
sleep(0.001) until flush(100)
|
139
|
+
|
140
|
+
true
|
44
141
|
end
|
45
142
|
|
46
143
|
# Partition count for a given topic.
|
47
|
-
# NOTE: If 'allow.auto.create.topics' is set to true in the broker, the topic will be auto-created after returning nil.
|
48
144
|
#
|
49
145
|
# @param topic [String] The topic name.
|
146
|
+
# @return [Integer] partition count for a given topic or `-1` if it could not be obtained.
|
50
147
|
#
|
51
|
-
# @
|
148
|
+
# @note If 'allow.auto.create.topics' is set to true in the broker, the topic will be
|
149
|
+
# auto-created after returning nil.
|
52
150
|
#
|
151
|
+
# @note We cache the partition count for a given topic for given time.
|
152
|
+
# This prevents us in case someone uses `partition_key` from querying for the count with
|
153
|
+
# each message. Instead we query once every 30 seconds at most if we have a valid partition
|
154
|
+
# count or every 5 seconds in case we were not able to obtain number of partitions
|
53
155
|
def partition_count(topic)
|
54
156
|
closed_producer_check(__method__)
|
55
|
-
|
157
|
+
|
158
|
+
@_partitions_count_cache.delete_if do |_, cached|
|
159
|
+
monotonic_now - cached.first > PARTITIONS_COUNT_TTL
|
160
|
+
end
|
161
|
+
|
162
|
+
@_partitions_count_cache[topic].last
|
56
163
|
end
|
57
164
|
|
58
165
|
# 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.
|
@@ -67,11 +174,12 @@ module Rdkafka
|
|
67
174
|
# @param partition_key [String, nil] Optional partition key based on which partition assignment can happen
|
68
175
|
# @param timestamp [Time,Integer,nil] Optional timestamp of this message. Integer timestamp is in milliseconds since Jan 1 1970.
|
69
176
|
# @param headers [Hash<String,String>] Optional message headers
|
70
|
-
#
|
71
|
-
# @raise [RdkafkaError] When adding the message to rdkafka's queue failed
|
177
|
+
# @param label [Object, nil] a label that can be assigned when producing a message that will be part of the delivery handle and the delivery report
|
72
178
|
#
|
73
179
|
# @return [DeliveryHandle] Delivery handle that can be used to wait for the result of producing this message
|
74
|
-
|
180
|
+
#
|
181
|
+
# @raise [RdkafkaError] When adding the message to rdkafka's queue failed
|
182
|
+
def produce(topic:, payload: nil, key: nil, partition: nil, partition_key: nil, timestamp: nil, headers: nil, label: nil)
|
75
183
|
closed_producer_check(__method__)
|
76
184
|
|
77
185
|
# Start by checking and converting the input
|
@@ -93,7 +201,7 @@ module Rdkafka
|
|
93
201
|
if partition_key
|
94
202
|
partition_count = partition_count(topic)
|
95
203
|
# If the topic is not present, set to -1
|
96
|
-
partition = Rdkafka::Bindings.partitioner(partition_key, partition_count, @partitioner_name) if partition_count
|
204
|
+
partition = Rdkafka::Bindings.partitioner(partition_key, partition_count, @partitioner_name) if partition_count.positive?
|
97
205
|
end
|
98
206
|
|
99
207
|
# If partition is nil, use -1 to let librdafka set the partition randomly or
|
@@ -113,6 +221,7 @@ module Rdkafka
|
|
113
221
|
end
|
114
222
|
|
115
223
|
delivery_handle = DeliveryHandle.new
|
224
|
+
delivery_handle.label = label
|
116
225
|
delivery_handle[:pending] = true
|
117
226
|
delivery_handle[:response] = -1
|
118
227
|
delivery_handle[:partition] = -1
|
@@ -143,10 +252,12 @@ module Rdkafka
|
|
143
252
|
args << :int << Rdkafka::Bindings::RD_KAFKA_VTYPE_END
|
144
253
|
|
145
254
|
# Produce the message
|
146
|
-
response =
|
147
|
-
|
148
|
-
|
149
|
-
|
255
|
+
response = @native_kafka.with_inner do |inner|
|
256
|
+
Rdkafka::Bindings.rd_kafka_producev(
|
257
|
+
inner,
|
258
|
+
*args
|
259
|
+
)
|
260
|
+
end
|
150
261
|
|
151
262
|
# Raise error if the produce call was not successful
|
152
263
|
if response != 0
|
@@ -157,22 +268,41 @@ module Rdkafka
|
|
157
268
|
delivery_handle
|
158
269
|
end
|
159
270
|
|
160
|
-
#
|
271
|
+
# Calls (if registered) the delivery callback
|
272
|
+
#
|
273
|
+
# @param delivery_report [Producer::DeliveryReport]
|
274
|
+
# @param delivery_handle [Producer::DeliveryHandle]
|
161
275
|
def call_delivery_callback(delivery_report, delivery_handle)
|
162
276
|
return unless @delivery_callback
|
163
277
|
|
164
|
-
|
165
|
-
|
278
|
+
case @delivery_callback_arity
|
279
|
+
when 0
|
280
|
+
@delivery_callback.call
|
281
|
+
when 1
|
282
|
+
@delivery_callback.call(delivery_report)
|
283
|
+
else
|
284
|
+
@delivery_callback.call(delivery_report, delivery_handle)
|
285
|
+
end
|
166
286
|
end
|
167
287
|
|
288
|
+
# Figures out the arity of a given block/method
|
289
|
+
#
|
290
|
+
# @param callback [#call, Proc]
|
291
|
+
# @return [Integer] arity of the provided block/method
|
168
292
|
def arity(callback)
|
169
293
|
return callback.arity if callback.respond_to?(:arity)
|
170
294
|
|
171
295
|
callback.method(:call).arity
|
172
296
|
end
|
173
297
|
|
298
|
+
private
|
299
|
+
|
300
|
+
# Ensures, no operations can happen on a closed producer
|
301
|
+
#
|
302
|
+
# @param method [Symbol] name of the method that invoked producer
|
303
|
+
# @raise [Rdkafka::ClosedProducerError]
|
174
304
|
def closed_producer_check(method)
|
175
|
-
raise Rdkafka::ClosedProducerError.new(method) if
|
305
|
+
raise Rdkafka::ClosedProducerError.new(method) if closed?
|
176
306
|
end
|
177
307
|
end
|
178
308
|
end
|
data/lib/rdkafka/version.rb
CHANGED
@@ -1,5 +1,7 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
module Rdkafka
|
2
|
-
VERSION = "0.
|
3
|
-
LIBRDKAFKA_VERSION = "
|
4
|
-
LIBRDKAFKA_SOURCE_SHA256 = "
|
4
|
+
VERSION = "0.15.1"
|
5
|
+
LIBRDKAFKA_VERSION = "2.3.0"
|
6
|
+
LIBRDKAFKA_SOURCE_SHA256 = "2d49c35c77eeb3d42fa61c43757fcbb6a206daa560247154e60642bcdcc14d12"
|
5
7
|
end
|
data/lib/rdkafka.rb
CHANGED
@@ -1,11 +1,29 @@
|
|
1
|
-
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "logger"
|
4
|
+
require "objspace"
|
5
|
+
require "ffi"
|
6
|
+
require "json"
|
2
7
|
|
8
|
+
require "rdkafka/version"
|
9
|
+
require "rdkafka/helpers/time"
|
3
10
|
require "rdkafka/abstract_handle"
|
4
11
|
require "rdkafka/admin"
|
5
12
|
require "rdkafka/admin/create_topic_handle"
|
6
13
|
require "rdkafka/admin/create_topic_report"
|
14
|
+
require "rdkafka/admin/delete_groups_handle"
|
15
|
+
require "rdkafka/admin/delete_groups_report"
|
7
16
|
require "rdkafka/admin/delete_topic_handle"
|
8
17
|
require "rdkafka/admin/delete_topic_report"
|
18
|
+
require "rdkafka/admin/create_partitions_handle"
|
19
|
+
require "rdkafka/admin/create_partitions_report"
|
20
|
+
require "rdkafka/admin/create_acl_handle"
|
21
|
+
require "rdkafka/admin/create_acl_report"
|
22
|
+
require "rdkafka/admin/delete_acl_handle"
|
23
|
+
require "rdkafka/admin/delete_acl_report"
|
24
|
+
require "rdkafka/admin/describe_acl_handle"
|
25
|
+
require "rdkafka/admin/describe_acl_report"
|
26
|
+
require "rdkafka/admin/acl_binding_result"
|
9
27
|
require "rdkafka/bindings"
|
10
28
|
require "rdkafka/callbacks"
|
11
29
|
require "rdkafka/config"
|
@@ -16,7 +34,11 @@ require "rdkafka/consumer/partition"
|
|
16
34
|
require "rdkafka/consumer/topic_partition_list"
|
17
35
|
require "rdkafka/error"
|
18
36
|
require "rdkafka/metadata"
|
37
|
+
require "rdkafka/native_kafka"
|
19
38
|
require "rdkafka/producer"
|
20
|
-
require "rdkafka/producer/client"
|
21
39
|
require "rdkafka/producer/delivery_handle"
|
22
40
|
require "rdkafka/producer/delivery_report"
|
41
|
+
|
42
|
+
# Main Rdkafka namespace of this gem
|
43
|
+
module Rdkafka
|
44
|
+
end
|
data/rdkafka.gemspec
CHANGED
@@ -1,12 +1,13 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
require File.expand_path('lib/rdkafka/version', __dir__)
|
2
4
|
|
3
5
|
Gem::Specification.new do |gem|
|
4
|
-
gem.authors = ['Thijs Cadier']
|
5
|
-
gem.email = ["
|
6
|
+
gem.authors = ['Thijs Cadier', 'Maciej Mensfeld']
|
7
|
+
gem.email = ["contact@karafka.io"]
|
6
8
|
gem.description = "Modern Kafka client library for Ruby based on librdkafka"
|
7
|
-
gem.summary = "The rdkafka gem is a modern Kafka client library for Ruby based on librdkafka. It wraps the production-ready C client using the ffi gem and targets Kafka 1.0+ and Ruby 2.
|
9
|
+
gem.summary = "The rdkafka gem is a modern Kafka client library for Ruby based on librdkafka. It wraps the production-ready C client using the ffi gem and targets Kafka 1.0+ and Ruby 2.7+."
|
8
10
|
gem.license = 'MIT'
|
9
|
-
gem.homepage = 'https://github.com/thijsc/rdkafka-ruby'
|
10
11
|
|
11
12
|
gem.files = `git ls-files`.split($\)
|
12
13
|
gem.executables = gem.files.grep(%r{^bin/}).map{ |f| File.basename(f) }
|
@@ -14,8 +15,13 @@ Gem::Specification.new do |gem|
|
|
14
15
|
gem.name = 'rdkafka'
|
15
16
|
gem.require_paths = ['lib']
|
16
17
|
gem.version = Rdkafka::VERSION
|
17
|
-
gem.required_ruby_version = '>= 2.
|
18
|
+
gem.required_ruby_version = '>= 2.7'
|
18
19
|
gem.extensions = %w(ext/Rakefile)
|
20
|
+
gem.cert_chain = %w[certs/cert_chain.pem]
|
21
|
+
|
22
|
+
if $PROGRAM_NAME.end_with?('gem')
|
23
|
+
gem.signing_key = File.expand_path('~/.ssh/gem-private_key.pem')
|
24
|
+
end
|
19
25
|
|
20
26
|
gem.add_dependency 'ffi', '~> 1.15'
|
21
27
|
gem.add_dependency 'mini_portile2', '~> 2.6'
|
@@ -27,4 +33,14 @@ Gem::Specification.new do |gem|
|
|
27
33
|
gem.add_development_dependency 'simplecov'
|
28
34
|
gem.add_development_dependency 'guard'
|
29
35
|
gem.add_development_dependency 'guard-rspec'
|
36
|
+
|
37
|
+
gem.metadata = {
|
38
|
+
'funding_uri' => 'https://karafka.io/#become-pro',
|
39
|
+
'homepage_uri' => 'https://karafka.io',
|
40
|
+
'changelog_uri' => 'https://github.com/karafka/rdkafka-ruby/blob/main/CHANGELOG.md',
|
41
|
+
'bug_tracker_uri' => 'https://github.com/karafka/rdkafka-ruby/issues',
|
42
|
+
'source_code_uri' => 'https://github.com/karafka/rdkafka-ruby',
|
43
|
+
'documentation_uri' => 'https://github.com/karafka/rdkafka-ruby/blob/main/README.md',
|
44
|
+
'rubygems_mfa_required' => 'true'
|
45
|
+
}
|
30
46
|
end
|
data/renovate.json
ADDED
@@ -0,0 +1,56 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "spec_helper"
|
4
|
+
|
5
|
+
describe Rdkafka::Admin::CreateAclHandle do
|
6
|
+
# If create acl was successful there is no error object
|
7
|
+
# the error code is set to RD_KAFKA_RESP_ERR_NO_ERRORa
|
8
|
+
# https://github.com/confluentinc/librdkafka/blob/1f9f245ac409f50f724695c628c7a0d54a763b9a/src/rdkafka_error.c#L169
|
9
|
+
let(:response) { Rdkafka::Bindings::RD_KAFKA_RESP_ERR_NO_ERROR }
|
10
|
+
|
11
|
+
subject do
|
12
|
+
Rdkafka::Admin::CreateAclHandle.new.tap do |handle|
|
13
|
+
handle[:pending] = pending_handle
|
14
|
+
handle[:response] = response
|
15
|
+
# If create acl was successful there is no error object and the error_string is set to ""
|
16
|
+
# https://github.com/confluentinc/librdkafka/blob/1f9f245ac409f50f724695c628c7a0d54a763b9a/src/rdkafka_error.c#L178
|
17
|
+
handle[:response_string] = FFI::MemoryPointer.from_string("")
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
describe "#wait" do
|
22
|
+
let(:pending_handle) { true }
|
23
|
+
|
24
|
+
it "should wait until the timeout and then raise an error" do
|
25
|
+
expect {
|
26
|
+
subject.wait(max_wait_timeout: 0.1)
|
27
|
+
}.to raise_error Rdkafka::Admin::CreateAclHandle::WaitTimeoutError, /create acl/
|
28
|
+
end
|
29
|
+
|
30
|
+
context "when not pending anymore and no error" do
|
31
|
+
let(:pending_handle) { false }
|
32
|
+
|
33
|
+
it "should return a create acl report" do
|
34
|
+
report = subject.wait
|
35
|
+
|
36
|
+
expect(report.rdkafka_response_string).to eq("")
|
37
|
+
end
|
38
|
+
|
39
|
+
it "should wait without a timeout" do
|
40
|
+
report = subject.wait(max_wait_timeout: nil)
|
41
|
+
|
42
|
+
expect(report.rdkafka_response_string).to eq("")
|
43
|
+
end
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
47
|
+
describe "#raise_error" do
|
48
|
+
let(:pending_handle) { false }
|
49
|
+
|
50
|
+
it "should raise the appropriate error" do
|
51
|
+
expect {
|
52
|
+
subject.raise_error
|
53
|
+
}.to raise_exception(Rdkafka::RdkafkaError, /Success \(no_error\)/)
|
54
|
+
end
|
55
|
+
end
|
56
|
+
end
|