rdkafka 0.12.0 → 0.15.1
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/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
|