ruby-kafka-custom 0.7.7.26
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/lib/kafka/async_producer.rb +279 -0
- data/lib/kafka/broker.rb +205 -0
- data/lib/kafka/broker_info.rb +16 -0
- data/lib/kafka/broker_pool.rb +41 -0
- data/lib/kafka/broker_uri.rb +43 -0
- data/lib/kafka/client.rb +754 -0
- data/lib/kafka/cluster.rb +455 -0
- data/lib/kafka/compression.rb +43 -0
- data/lib/kafka/compressor.rb +85 -0
- data/lib/kafka/connection.rb +220 -0
- data/lib/kafka/connection_builder.rb +33 -0
- data/lib/kafka/consumer.rb +592 -0
- data/lib/kafka/consumer_group.rb +208 -0
- data/lib/kafka/datadog.rb +413 -0
- data/lib/kafka/fetch_operation.rb +115 -0
- data/lib/kafka/fetched_batch.rb +54 -0
- data/lib/kafka/fetched_batch_generator.rb +117 -0
- data/lib/kafka/fetched_message.rb +47 -0
- data/lib/kafka/fetched_offset_resolver.rb +48 -0
- data/lib/kafka/fetcher.rb +221 -0
- data/lib/kafka/gzip_codec.rb +30 -0
- data/lib/kafka/heartbeat.rb +25 -0
- data/lib/kafka/instrumenter.rb +38 -0
- data/lib/kafka/lz4_codec.rb +23 -0
- data/lib/kafka/message_buffer.rb +87 -0
- data/lib/kafka/offset_manager.rb +248 -0
- data/lib/kafka/partitioner.rb +35 -0
- data/lib/kafka/pause.rb +92 -0
- data/lib/kafka/pending_message.rb +29 -0
- data/lib/kafka/pending_message_queue.rb +41 -0
- data/lib/kafka/produce_operation.rb +205 -0
- data/lib/kafka/producer.rb +504 -0
- data/lib/kafka/protocol.rb +217 -0
- data/lib/kafka/protocol/add_partitions_to_txn_request.rb +34 -0
- data/lib/kafka/protocol/add_partitions_to_txn_response.rb +47 -0
- data/lib/kafka/protocol/alter_configs_request.rb +44 -0
- data/lib/kafka/protocol/alter_configs_response.rb +49 -0
- data/lib/kafka/protocol/api_versions_request.rb +21 -0
- data/lib/kafka/protocol/api_versions_response.rb +53 -0
- data/lib/kafka/protocol/consumer_group_protocol.rb +19 -0
- data/lib/kafka/protocol/create_partitions_request.rb +42 -0
- data/lib/kafka/protocol/create_partitions_response.rb +28 -0
- data/lib/kafka/protocol/create_topics_request.rb +45 -0
- data/lib/kafka/protocol/create_topics_response.rb +26 -0
- data/lib/kafka/protocol/decoder.rb +175 -0
- data/lib/kafka/protocol/delete_topics_request.rb +33 -0
- data/lib/kafka/protocol/delete_topics_response.rb +26 -0
- data/lib/kafka/protocol/describe_configs_request.rb +35 -0
- data/lib/kafka/protocol/describe_configs_response.rb +73 -0
- data/lib/kafka/protocol/describe_groups_request.rb +27 -0
- data/lib/kafka/protocol/describe_groups_response.rb +73 -0
- data/lib/kafka/protocol/encoder.rb +184 -0
- data/lib/kafka/protocol/end_txn_request.rb +29 -0
- data/lib/kafka/protocol/end_txn_response.rb +19 -0
- data/lib/kafka/protocol/fetch_request.rb +70 -0
- data/lib/kafka/protocol/fetch_response.rb +136 -0
- data/lib/kafka/protocol/find_coordinator_request.rb +29 -0
- data/lib/kafka/protocol/find_coordinator_response.rb +29 -0
- data/lib/kafka/protocol/heartbeat_request.rb +27 -0
- data/lib/kafka/protocol/heartbeat_response.rb +17 -0
- data/lib/kafka/protocol/init_producer_id_request.rb +26 -0
- data/lib/kafka/protocol/init_producer_id_response.rb +27 -0
- data/lib/kafka/protocol/join_group_request.rb +41 -0
- data/lib/kafka/protocol/join_group_response.rb +33 -0
- data/lib/kafka/protocol/leave_group_request.rb +25 -0
- data/lib/kafka/protocol/leave_group_response.rb +17 -0
- data/lib/kafka/protocol/list_groups_request.rb +23 -0
- data/lib/kafka/protocol/list_groups_response.rb +35 -0
- data/lib/kafka/protocol/list_offset_request.rb +53 -0
- data/lib/kafka/protocol/list_offset_response.rb +89 -0
- data/lib/kafka/protocol/member_assignment.rb +42 -0
- data/lib/kafka/protocol/message.rb +172 -0
- data/lib/kafka/protocol/message_set.rb +55 -0
- data/lib/kafka/protocol/metadata_request.rb +31 -0
- data/lib/kafka/protocol/metadata_response.rb +185 -0
- data/lib/kafka/protocol/offset_commit_request.rb +47 -0
- data/lib/kafka/protocol/offset_commit_response.rb +29 -0
- data/lib/kafka/protocol/offset_fetch_request.rb +36 -0
- data/lib/kafka/protocol/offset_fetch_response.rb +56 -0
- data/lib/kafka/protocol/produce_request.rb +92 -0
- data/lib/kafka/protocol/produce_response.rb +63 -0
- data/lib/kafka/protocol/record.rb +88 -0
- data/lib/kafka/protocol/record_batch.rb +222 -0
- data/lib/kafka/protocol/request_message.rb +26 -0
- data/lib/kafka/protocol/sasl_handshake_request.rb +33 -0
- data/lib/kafka/protocol/sasl_handshake_response.rb +28 -0
- data/lib/kafka/protocol/sync_group_request.rb +33 -0
- data/lib/kafka/protocol/sync_group_response.rb +23 -0
- data/lib/kafka/round_robin_assignment_strategy.rb +54 -0
- data/lib/kafka/sasl/gssapi.rb +76 -0
- data/lib/kafka/sasl/oauth.rb +64 -0
- data/lib/kafka/sasl/plain.rb +39 -0
- data/lib/kafka/sasl/scram.rb +177 -0
- data/lib/kafka/sasl_authenticator.rb +61 -0
- data/lib/kafka/snappy_codec.rb +25 -0
- data/lib/kafka/socket_with_timeout.rb +96 -0
- data/lib/kafka/ssl_context.rb +66 -0
- data/lib/kafka/ssl_socket_with_timeout.rb +187 -0
- data/lib/kafka/statsd.rb +296 -0
- data/lib/kafka/tagged_logger.rb +72 -0
- data/lib/kafka/transaction_manager.rb +261 -0
- data/lib/kafka/transaction_state_machine.rb +72 -0
- data/lib/kafka/version.rb +5 -0
- metadata +461 -0
@@ -0,0 +1,35 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "zlib"
|
4
|
+
|
5
|
+
module Kafka
|
6
|
+
|
7
|
+
# Assigns partitions to messages.
|
8
|
+
class Partitioner
|
9
|
+
|
10
|
+
# Assigns a partition number based on a partition key. If no explicit
|
11
|
+
# partition key is provided, the message key will be used instead.
|
12
|
+
#
|
13
|
+
# If the key is nil, then a random partition is selected. Otherwise, a digest
|
14
|
+
# of the key is used to deterministically find a partition. As long as the
|
15
|
+
# number of partitions doesn't change, the same key will always be assigned
|
16
|
+
# to the same partition.
|
17
|
+
#
|
18
|
+
# @param partition_count [Integer] the number of partitions in the topic.
|
19
|
+
# @param message [Kafka::PendingMessage] the message that should be assigned
|
20
|
+
# a partition.
|
21
|
+
# @return [Integer] the partition number.
|
22
|
+
def self.partition_for_key(partition_count, message)
|
23
|
+
raise ArgumentError if partition_count == 0
|
24
|
+
|
25
|
+
# If no explicit partition key is specified we use the message key instead.
|
26
|
+
key = message.partition_key || message.key
|
27
|
+
|
28
|
+
if key.nil?
|
29
|
+
rand(partition_count)
|
30
|
+
else
|
31
|
+
Zlib.crc32(key) % partition_count
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
data/lib/kafka/pause.rb
ADDED
@@ -0,0 +1,92 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Kafka
|
4
|
+
# Manages the pause state of a partition.
|
5
|
+
#
|
6
|
+
# The processing of messages in a partition can be paused, e.g. if there was
|
7
|
+
# an exception during processing. This could be caused by a downstream service
|
8
|
+
# not being available. A typical way of solving such an issue is to back off
|
9
|
+
# for a little while and then try again. In order to do that, _pause_ the
|
10
|
+
# partition.
|
11
|
+
class Pause
|
12
|
+
def initialize(clock: Time)
|
13
|
+
@clock = clock
|
14
|
+
@started_at = nil
|
15
|
+
@pauses = 0
|
16
|
+
@timeout = nil
|
17
|
+
@max_timeout = nil
|
18
|
+
@exponential_backoff = false
|
19
|
+
end
|
20
|
+
|
21
|
+
# Mark the partition as paused.
|
22
|
+
#
|
23
|
+
# If exponential backoff is enabled, each subsequent pause of a partition will
|
24
|
+
# cause a doubling of the actual timeout, i.e. for pause number _n_, the actual
|
25
|
+
# timeout will be _2^n * timeout_.
|
26
|
+
#
|
27
|
+
# Only when {#reset!} is called is this state cleared.
|
28
|
+
#
|
29
|
+
# @param timeout [nil, Integer] if specified, the partition will automatically
|
30
|
+
# resume after this many seconds.
|
31
|
+
# @param exponential_backoff [Boolean] whether to enable exponential timeouts.
|
32
|
+
def pause!(timeout: nil, max_timeout: nil, exponential_backoff: false)
|
33
|
+
@started_at = @clock.now
|
34
|
+
@timeout = timeout
|
35
|
+
@max_timeout = max_timeout
|
36
|
+
@exponential_backoff = exponential_backoff
|
37
|
+
@pauses += 1
|
38
|
+
end
|
39
|
+
|
40
|
+
# Resumes the partition.
|
41
|
+
#
|
42
|
+
# The number of pauses is still retained, and if the partition is paused again
|
43
|
+
# it may be with an exponential backoff.
|
44
|
+
def resume!
|
45
|
+
@started_at = nil
|
46
|
+
@timeout = nil
|
47
|
+
@max_timeout = nil
|
48
|
+
end
|
49
|
+
|
50
|
+
# Whether the partition is currently paused. The pause may have expired, in which
|
51
|
+
# case {#expired?} should be checked as well.
|
52
|
+
def paused?
|
53
|
+
# This is nil if we're not currently paused.
|
54
|
+
!@started_at.nil?
|
55
|
+
end
|
56
|
+
|
57
|
+
def pause_duration
|
58
|
+
if paused?
|
59
|
+
Time.now - @started_at
|
60
|
+
else
|
61
|
+
0
|
62
|
+
end
|
63
|
+
end
|
64
|
+
|
65
|
+
# Whether the pause has expired.
|
66
|
+
def expired?
|
67
|
+
# We never expire the pause if timeout is nil.
|
68
|
+
return false if @timeout.nil?
|
69
|
+
|
70
|
+
# Have we passed the end of the pause duration?
|
71
|
+
@clock.now >= ends_at
|
72
|
+
end
|
73
|
+
|
74
|
+
# Resets the pause state, ensuring that the next pause is not exponential.
|
75
|
+
def reset!
|
76
|
+
@pauses = 0
|
77
|
+
end
|
78
|
+
|
79
|
+
private
|
80
|
+
|
81
|
+
def ends_at
|
82
|
+
# Apply an exponential backoff to the timeout.
|
83
|
+
backoff_factor = @exponential_backoff ? 2**(@pauses - 1) : 1
|
84
|
+
timeout = backoff_factor * @timeout
|
85
|
+
|
86
|
+
# If set, don't allow a timeout longer than max_timeout.
|
87
|
+
timeout = @max_timeout if @max_timeout && timeout > @max_timeout
|
88
|
+
|
89
|
+
@started_at + timeout
|
90
|
+
end
|
91
|
+
end
|
92
|
+
end
|
@@ -0,0 +1,29 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Kafka
|
4
|
+
class PendingMessage
|
5
|
+
attr_reader :value, :key, :headers, :topic, :partition, :partition_key, :create_time, :bytesize
|
6
|
+
|
7
|
+
def initialize(value:, key:, headers: {}, topic:, partition:, partition_key:, create_time:)
|
8
|
+
@value = value
|
9
|
+
@key = key
|
10
|
+
@headers = headers
|
11
|
+
@topic = topic
|
12
|
+
@partition = partition
|
13
|
+
@partition_key = partition_key
|
14
|
+
@create_time = create_time
|
15
|
+
@bytesize = key.to_s.bytesize + value.to_s.bytesize
|
16
|
+
end
|
17
|
+
|
18
|
+
def ==(other)
|
19
|
+
@value == other.value &&
|
20
|
+
@key == other.key &&
|
21
|
+
@topic == other.topic &&
|
22
|
+
@headers == other.headers &&
|
23
|
+
@partition == other.partition &&
|
24
|
+
@partition_key == other.partition_key &&
|
25
|
+
@create_time == other.create_time &&
|
26
|
+
@bytesize == other.bytesize
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
@@ -0,0 +1,41 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Kafka
|
4
|
+
|
5
|
+
class PendingMessageQueue
|
6
|
+
attr_reader :size, :bytesize
|
7
|
+
|
8
|
+
def initialize
|
9
|
+
clear
|
10
|
+
end
|
11
|
+
|
12
|
+
def write(message)
|
13
|
+
@messages << message
|
14
|
+
@size += 1
|
15
|
+
@bytesize += message.bytesize
|
16
|
+
end
|
17
|
+
|
18
|
+
def empty?
|
19
|
+
@messages.empty?
|
20
|
+
end
|
21
|
+
|
22
|
+
def clear
|
23
|
+
@messages = []
|
24
|
+
@size = 0
|
25
|
+
@bytesize = 0
|
26
|
+
end
|
27
|
+
|
28
|
+
def replace(messages)
|
29
|
+
clear
|
30
|
+
messages.each {|message| write(message) }
|
31
|
+
end
|
32
|
+
|
33
|
+
# Yields each message in the queue.
|
34
|
+
#
|
35
|
+
# @yieldparam [PendingMessage] message
|
36
|
+
# @return [nil]
|
37
|
+
def each(&block)
|
38
|
+
@messages.each(&block)
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
@@ -0,0 +1,205 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "kafka/protocol/message_set"
|
4
|
+
require "kafka/protocol/record_batch"
|
5
|
+
|
6
|
+
module Kafka
|
7
|
+
# A produce operation attempts to send all messages in a buffer to the Kafka cluster.
|
8
|
+
# Since topics and partitions are spread among all brokers in a cluster, this usually
|
9
|
+
# involves sending requests to several or all of the brokers.
|
10
|
+
#
|
11
|
+
# ## Instrumentation
|
12
|
+
#
|
13
|
+
# When executing the operation, an `ack_message.producer.kafka` notification will be
|
14
|
+
# emitted for each message that was successfully appended to a topic partition.
|
15
|
+
# The following keys will be found in the payload:
|
16
|
+
#
|
17
|
+
# * `:topic` — the topic that was written to.
|
18
|
+
# * `:partition` — the partition that the message set was appended to.
|
19
|
+
# * `:offset` — the offset of the message in the partition.
|
20
|
+
# * `:key` — the message key.
|
21
|
+
# * `:value` — the message value.
|
22
|
+
# * `:delay` — the time between the message was produced and when it was acknowledged.
|
23
|
+
#
|
24
|
+
# In addition to these notifications, a `send_messages.producer.kafka` notification will
|
25
|
+
# be emitted after the operation completes, regardless of whether it succeeds. This
|
26
|
+
# notification will have the following keys:
|
27
|
+
#
|
28
|
+
# * `:message_count` – the total number of messages that the operation tried to
|
29
|
+
# send. Note that not all messages may get delivered.
|
30
|
+
# * `:sent_message_count` – the number of messages that were successfully sent.
|
31
|
+
#
|
32
|
+
class ProduceOperation
|
33
|
+
def initialize(cluster:, transaction_manager:, buffer:, compressor:, required_acks:, ack_timeout:, logger:, instrumenter:)
|
34
|
+
@cluster = cluster
|
35
|
+
@transaction_manager = transaction_manager
|
36
|
+
@buffer = buffer
|
37
|
+
@required_acks = required_acks
|
38
|
+
@ack_timeout = ack_timeout
|
39
|
+
@compressor = compressor
|
40
|
+
@logger = TaggedLogger.new(logger)
|
41
|
+
@instrumenter = instrumenter
|
42
|
+
end
|
43
|
+
|
44
|
+
def execute
|
45
|
+
if (@transaction_manager.idempotent? || @transaction_manager.transactional?) && @required_acks != -1
|
46
|
+
raise 'You must set required_acks option to :all to use idempotent / transactional production'
|
47
|
+
end
|
48
|
+
|
49
|
+
if @transaction_manager.transactional? && !@transaction_manager.in_transaction?
|
50
|
+
raise "Produce operation can only be executed in a pending transaction"
|
51
|
+
end
|
52
|
+
|
53
|
+
@instrumenter.instrument("send_messages.producer") do |notification|
|
54
|
+
message_count = @buffer.size
|
55
|
+
|
56
|
+
notification[:message_count] = message_count
|
57
|
+
|
58
|
+
begin
|
59
|
+
if @transaction_manager.idempotent? || @transaction_manager.transactional?
|
60
|
+
@transaction_manager.init_producer_id
|
61
|
+
end
|
62
|
+
send_buffered_messages
|
63
|
+
ensure
|
64
|
+
notification[:sent_message_count] = message_count - @buffer.size
|
65
|
+
end
|
66
|
+
end
|
67
|
+
end
|
68
|
+
|
69
|
+
private
|
70
|
+
|
71
|
+
def send_buffered_messages
|
72
|
+
messages_for_broker = {}
|
73
|
+
topic_partitions = {}
|
74
|
+
|
75
|
+
@buffer.each do |topic, partition, messages|
|
76
|
+
begin
|
77
|
+
broker = @cluster.get_leader(topic, partition)
|
78
|
+
|
79
|
+
@logger.debug "Current leader for #{topic}/#{partition} is node #{broker}"
|
80
|
+
|
81
|
+
topic_partitions[topic] ||= Set.new
|
82
|
+
topic_partitions[topic].add(partition)
|
83
|
+
|
84
|
+
messages_for_broker[broker] ||= MessageBuffer.new
|
85
|
+
messages_for_broker[broker].concat(messages, topic: topic, partition: partition)
|
86
|
+
rescue Kafka::Error => e
|
87
|
+
@logger.error "Could not connect to leader for partition #{topic}/#{partition}: #{e.message}"
|
88
|
+
|
89
|
+
@instrumenter.instrument("topic_error.producer", {
|
90
|
+
topic: topic,
|
91
|
+
exception: [e.class.to_s, e.message],
|
92
|
+
})
|
93
|
+
|
94
|
+
# We can't send the messages right now, so we'll just keep them in the buffer.
|
95
|
+
# We'll mark the cluster as stale in order to force a metadata refresh.
|
96
|
+
@cluster.mark_as_stale!
|
97
|
+
end
|
98
|
+
end
|
99
|
+
|
100
|
+
# Add topic and partition to transaction
|
101
|
+
if @transaction_manager.transactional?
|
102
|
+
@transaction_manager.add_partitions_to_transaction(topic_partitions)
|
103
|
+
end
|
104
|
+
|
105
|
+
messages_for_broker.each do |broker, message_buffer|
|
106
|
+
begin
|
107
|
+
@logger.info "Sending #{message_buffer.size} messages to #{broker}"
|
108
|
+
|
109
|
+
records_for_topics = {}
|
110
|
+
|
111
|
+
message_buffer.each do |topic, partition, records|
|
112
|
+
record_batch = Protocol::RecordBatch.new(
|
113
|
+
records: records,
|
114
|
+
first_sequence: @transaction_manager.next_sequence_for(
|
115
|
+
topic, partition
|
116
|
+
),
|
117
|
+
in_transaction: @transaction_manager.transactional?,
|
118
|
+
producer_id: @transaction_manager.producer_id,
|
119
|
+
producer_epoch: @transaction_manager.producer_epoch
|
120
|
+
)
|
121
|
+
records_for_topics[topic] ||= {}
|
122
|
+
records_for_topics[topic][partition] = record_batch
|
123
|
+
end
|
124
|
+
|
125
|
+
response = broker.produce(
|
126
|
+
messages_for_topics: records_for_topics,
|
127
|
+
compressor: @compressor,
|
128
|
+
required_acks: @required_acks,
|
129
|
+
timeout: @ack_timeout * 1000, # Kafka expects the timeout in milliseconds.
|
130
|
+
transactional_id: @transaction_manager.transactional_id
|
131
|
+
)
|
132
|
+
|
133
|
+
handle_response(broker, response, records_for_topics) if response
|
134
|
+
rescue ConnectionError => e
|
135
|
+
@logger.error "Could not connect to broker #{broker}: #{e}"
|
136
|
+
|
137
|
+
# Mark the cluster as stale in order to force a cluster metadata refresh.
|
138
|
+
@cluster.mark_as_stale!
|
139
|
+
end
|
140
|
+
end
|
141
|
+
end
|
142
|
+
|
143
|
+
def handle_response(broker, response, records_for_topics)
|
144
|
+
response.each_partition do |topic_info, partition_info|
|
145
|
+
topic = topic_info.topic
|
146
|
+
partition = partition_info.partition
|
147
|
+
record_batch = records_for_topics[topic][partition]
|
148
|
+
records = record_batch.records
|
149
|
+
ack_time = Time.now
|
150
|
+
|
151
|
+
begin
|
152
|
+
begin
|
153
|
+
Protocol.handle_error(partition_info.error_code)
|
154
|
+
rescue ProtocolError => e
|
155
|
+
@instrumenter.instrument("topic_error.producer", {
|
156
|
+
topic: topic,
|
157
|
+
exception: [e.class.to_s, e.message],
|
158
|
+
})
|
159
|
+
|
160
|
+
raise e
|
161
|
+
end
|
162
|
+
|
163
|
+
if @transaction_manager.idempotent? || @transaction_manager.transactional?
|
164
|
+
@transaction_manager.update_sequence_for(
|
165
|
+
topic, partition, record_batch.first_sequence + record_batch.size
|
166
|
+
)
|
167
|
+
end
|
168
|
+
|
169
|
+
records.each_with_index do |record, index|
|
170
|
+
@instrumenter.instrument("ack_message.producer", {
|
171
|
+
key: record.key,
|
172
|
+
value: record.value,
|
173
|
+
topic: topic,
|
174
|
+
partition: partition,
|
175
|
+
offset: partition_info.offset + index,
|
176
|
+
delay: ack_time - record.create_time,
|
177
|
+
})
|
178
|
+
end
|
179
|
+
rescue Kafka::CorruptMessage
|
180
|
+
@logger.error "Corrupt message when writing to #{topic}/#{partition} on #{broker}"
|
181
|
+
rescue Kafka::UnknownTopicOrPartition
|
182
|
+
@logger.error "Unknown topic or partition #{topic}/#{partition} on #{broker}"
|
183
|
+
@cluster.mark_as_stale!
|
184
|
+
rescue Kafka::LeaderNotAvailable
|
185
|
+
@logger.error "Leader currently not available for #{topic}/#{partition}"
|
186
|
+
@cluster.mark_as_stale!
|
187
|
+
rescue Kafka::NotLeaderForPartition
|
188
|
+
@logger.error "Broker #{broker} not currently leader for #{topic}/#{partition}"
|
189
|
+
@cluster.mark_as_stale!
|
190
|
+
rescue Kafka::RequestTimedOut
|
191
|
+
@logger.error "Timed out while writing to #{topic}/#{partition} on #{broker}"
|
192
|
+
rescue Kafka::NotEnoughReplicas
|
193
|
+
@logger.error "Not enough in-sync replicas for #{topic}/#{partition}"
|
194
|
+
rescue Kafka::NotEnoughReplicasAfterAppend
|
195
|
+
@logger.error "Messages written, but to fewer in-sync replicas than required for #{topic}/#{partition}"
|
196
|
+
else
|
197
|
+
@logger.debug "Successfully appended #{records.count} messages to #{topic}/#{partition} on #{broker}"
|
198
|
+
|
199
|
+
# The messages were successfully written; clear them from the buffer.
|
200
|
+
@buffer.clear_messages(topic: topic, partition: partition)
|
201
|
+
end
|
202
|
+
end
|
203
|
+
end
|
204
|
+
end
|
205
|
+
end
|
@@ -0,0 +1,504 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "set"
|
4
|
+
require "kafka/partitioner"
|
5
|
+
require "kafka/message_buffer"
|
6
|
+
require "kafka/produce_operation"
|
7
|
+
require "kafka/pending_message_queue"
|
8
|
+
require "kafka/pending_message"
|
9
|
+
require "kafka/compressor"
|
10
|
+
|
11
|
+
module Kafka
|
12
|
+
# Allows sending messages to a Kafka cluster.
|
13
|
+
#
|
14
|
+
# Typically you won't instantiate this class yourself, but rather have {Kafka::Client}
|
15
|
+
# do it for you, e.g.
|
16
|
+
#
|
17
|
+
# # Will instantiate Kafka::Client
|
18
|
+
# kafka = Kafka.new(["kafka1:9092", "kafka2:9092"])
|
19
|
+
#
|
20
|
+
# # Will instantiate Kafka::Producer
|
21
|
+
# producer = kafka.producer
|
22
|
+
#
|
23
|
+
# This is done in order to share a logger as well as a pool of broker connections across
|
24
|
+
# different producers. This also means that you don't need to pass the `cluster` and
|
25
|
+
# `logger` options to `#producer`. See {#initialize} for the list of other options
|
26
|
+
# you can pass in.
|
27
|
+
#
|
28
|
+
# ## Buffering
|
29
|
+
#
|
30
|
+
# The producer buffers pending messages until {#deliver_messages} is called. Note that there is
|
31
|
+
# a maximum buffer size (default is 1,000 messages) and writing messages after the
|
32
|
+
# buffer has reached this size will result in a BufferOverflow exception. Make sure
|
33
|
+
# to periodically call {#deliver_messages} or set `max_buffer_size` to an appropriate value.
|
34
|
+
#
|
35
|
+
# Buffering messages and sending them in batches greatly improves performance, so
|
36
|
+
# try to avoid sending messages after every write. The tradeoff between throughput and
|
37
|
+
# message delays depends on your use case.
|
38
|
+
#
|
39
|
+
# ## Error Handling and Retries
|
40
|
+
#
|
41
|
+
# The design of the error handling is based on having a {MessageBuffer} hold messages
|
42
|
+
# for all topics/partitions. Whenever we want to send messages to the cluster, we
|
43
|
+
# group the buffered messages by the broker they need to be sent to and fire off a
|
44
|
+
# request to each broker. A request can be a partial success, so we go through the
|
45
|
+
# response and inspect the error code for each partition that we wrote to. If the
|
46
|
+
# write to a given partition was successful, we clear the corresponding messages
|
47
|
+
# from the buffer -- otherwise, we log the error and keep the messages in the buffer.
|
48
|
+
#
|
49
|
+
# After this, we check if the buffer is empty. If it is, we're all done. If it's
|
50
|
+
# not, we do another round of requests, this time with just the remaining messages.
|
51
|
+
# We do this for as long as `max_retries` permits.
|
52
|
+
#
|
53
|
+
# ## Compression
|
54
|
+
#
|
55
|
+
# Depending on what kind of data you produce, enabling compression may yield improved
|
56
|
+
# bandwidth and space usage. Compression in Kafka is done on entire messages sets
|
57
|
+
# rather than on individual messages. This improves the compression rate and generally
|
58
|
+
# means that compressions works better the larger your buffers get, since the message
|
59
|
+
# sets will be larger by the time they're compressed.
|
60
|
+
#
|
61
|
+
# Since many workloads have variations in throughput and distribution across partitions,
|
62
|
+
# it's possible to configure a threshold for when to enable compression by setting
|
63
|
+
# `compression_threshold`. Only if the defined number of messages are buffered for a
|
64
|
+
# partition will the messages be compressed.
|
65
|
+
#
|
66
|
+
# Compression is enabled by passing the `compression_codec` parameter with the
|
67
|
+
# name of one of the algorithms allowed by Kafka:
|
68
|
+
#
|
69
|
+
# * `:snappy` for [Snappy](http://google.github.io/snappy/) compression.
|
70
|
+
# * `:gzip` for [gzip](https://en.wikipedia.org/wiki/Gzip) compression.
|
71
|
+
#
|
72
|
+
# By default, all message sets will be compressed if you specify a compression
|
73
|
+
# codec. To increase the compression threshold, set `compression_threshold` to
|
74
|
+
# an integer value higher than one.
|
75
|
+
#
|
76
|
+
# ## Instrumentation
|
77
|
+
#
|
78
|
+
# Whenever {#produce} is called, the notification `produce_message.producer.kafka`
|
79
|
+
# will be emitted with the following payload:
|
80
|
+
#
|
81
|
+
# * `value` – the message value.
|
82
|
+
# * `key` – the message key.
|
83
|
+
# * `topic` – the topic that was produced to.
|
84
|
+
# * `buffer_size` – the buffer size after adding the message.
|
85
|
+
# * `max_buffer_size` – the maximum allowed buffer size for the producer.
|
86
|
+
#
|
87
|
+
# After {#deliver_messages} completes, the notification
|
88
|
+
# `deliver_messages.producer.kafka` will be emitted with the following payload:
|
89
|
+
#
|
90
|
+
# * `message_count` – the total number of messages that the producer tried to
|
91
|
+
# deliver. Note that not all messages may get delivered.
|
92
|
+
# * `delivered_message_count` – the number of messages that were successfully
|
93
|
+
# delivered.
|
94
|
+
# * `attempts` – the number of attempts made to deliver the messages.
|
95
|
+
#
|
96
|
+
# ## Example
|
97
|
+
#
|
98
|
+
# This is an example of an application which reads lines from stdin and writes them
|
99
|
+
# to Kafka:
|
100
|
+
#
|
101
|
+
# require "kafka"
|
102
|
+
#
|
103
|
+
# logger = Logger.new($stderr)
|
104
|
+
# brokers = ENV.fetch("KAFKA_BROKERS").split(",")
|
105
|
+
#
|
106
|
+
# # Make sure to create this topic in your Kafka cluster or configure the
|
107
|
+
# # cluster to auto-create topics.
|
108
|
+
# topic = "random-messages"
|
109
|
+
#
|
110
|
+
# kafka = Kafka.new(brokers, client_id: "simple-producer", logger: logger)
|
111
|
+
# producer = kafka.producer
|
112
|
+
#
|
113
|
+
# begin
|
114
|
+
# $stdin.each_with_index do |line, index|
|
115
|
+
# producer.produce(line, topic: topic)
|
116
|
+
#
|
117
|
+
# # Send messages for every 10 lines.
|
118
|
+
# producer.deliver_messages if index % 10 == 0
|
119
|
+
# end
|
120
|
+
# ensure
|
121
|
+
# # Make sure to send any remaining messages.
|
122
|
+
# producer.deliver_messages
|
123
|
+
#
|
124
|
+
# producer.shutdown
|
125
|
+
# end
|
126
|
+
#
|
127
|
+
class Producer
|
128
|
+
class AbortTransaction < StandardError; end
|
129
|
+
|
130
|
+
def initialize(cluster:, transaction_manager:, logger:, instrumenter:, compressor:, ack_timeout:, required_acks:, max_retries:, retry_backoff:, max_buffer_size:, max_buffer_bytesize:)
|
131
|
+
@cluster = cluster
|
132
|
+
@transaction_manager = transaction_manager
|
133
|
+
@logger = TaggedLogger.new(logger)
|
134
|
+
@instrumenter = instrumenter
|
135
|
+
@required_acks = required_acks == :all ? -1 : required_acks
|
136
|
+
@ack_timeout = ack_timeout
|
137
|
+
@max_retries = max_retries
|
138
|
+
@retry_backoff = retry_backoff
|
139
|
+
@max_buffer_size = max_buffer_size
|
140
|
+
@max_buffer_bytesize = max_buffer_bytesize
|
141
|
+
@compressor = compressor
|
142
|
+
|
143
|
+
# The set of topics that are produced to.
|
144
|
+
@target_topics = Set.new
|
145
|
+
|
146
|
+
# A buffer organized by topic/partition.
|
147
|
+
@buffer = MessageBuffer.new
|
148
|
+
|
149
|
+
# Messages added by `#produce` but not yet assigned a partition.
|
150
|
+
@pending_message_queue = PendingMessageQueue.new
|
151
|
+
end
|
152
|
+
|
153
|
+
def to_s
|
154
|
+
"Producer #{@target_topics.to_a.join(', ')}"
|
155
|
+
end
|
156
|
+
|
157
|
+
# Produces a message to the specified topic. Note that messages are buffered in
|
158
|
+
# the producer until {#deliver_messages} is called.
|
159
|
+
#
|
160
|
+
# ## Partitioning
|
161
|
+
#
|
162
|
+
# There are several options for specifying the partition that the message should
|
163
|
+
# be written to.
|
164
|
+
#
|
165
|
+
# The simplest option is to not specify a message key, partition key, or
|
166
|
+
# partition number, in which case the message will be assigned a partition at
|
167
|
+
# random.
|
168
|
+
#
|
169
|
+
# You can also specify the `partition` parameter yourself. This requires you to
|
170
|
+
# know which partitions are available, however. Oftentimes the best option is
|
171
|
+
# to specify the `partition_key` parameter: messages with the same partition
|
172
|
+
# key will always be assigned to the same partition, as long as the number of
|
173
|
+
# partitions doesn't change. You can also omit the partition key and specify
|
174
|
+
# a message key instead. The message key is part of the message payload, and
|
175
|
+
# so can carry semantic value--whether you want to have the message key double
|
176
|
+
# as a partition key is up to you.
|
177
|
+
#
|
178
|
+
# @param value [String] the message data.
|
179
|
+
# @param key [String] the message key.
|
180
|
+
# @param headers [Hash<String, String>] the headers for the message.
|
181
|
+
# @param topic [String] the topic that the message should be written to.
|
182
|
+
# @param partition [Integer] the partition that the message should be written to.
|
183
|
+
# @param partition_key [String] the key that should be used to assign a partition.
|
184
|
+
# @param create_time [Time] the timestamp that should be set on the message.
|
185
|
+
#
|
186
|
+
# @raise [BufferOverflow] if the maximum buffer size has been reached.
|
187
|
+
# @return [nil]
|
188
|
+
def produce(value, key: nil, headers: {}, topic:, partition: nil, partition_key: nil, create_time: Time.now)
|
189
|
+
message = PendingMessage.new(
|
190
|
+
value: value && value.to_s,
|
191
|
+
key: key && key.to_s,
|
192
|
+
headers: headers,
|
193
|
+
topic: topic.to_s,
|
194
|
+
partition: partition && Integer(partition),
|
195
|
+
partition_key: partition_key && partition_key.to_s,
|
196
|
+
create_time: create_time
|
197
|
+
)
|
198
|
+
|
199
|
+
if buffer_size >= @max_buffer_size
|
200
|
+
buffer_overflow topic,
|
201
|
+
"Cannot produce to #{topic}, max buffer size (#{@max_buffer_size} messages) reached"
|
202
|
+
end
|
203
|
+
|
204
|
+
if buffer_bytesize + message.bytesize >= @max_buffer_bytesize
|
205
|
+
buffer_overflow topic,
|
206
|
+
"Cannot produce to #{topic}, max buffer bytesize (#{@max_buffer_bytesize} bytes) reached"
|
207
|
+
end
|
208
|
+
|
209
|
+
# If the producer is in transactional mode, all the message production
|
210
|
+
# must be used when the producer is currently in transaction
|
211
|
+
if @transaction_manager.transactional? && !@transaction_manager.in_transaction?
|
212
|
+
raise "Cannot produce to #{topic}: You must trigger begin_transaction before producing messages"
|
213
|
+
end
|
214
|
+
|
215
|
+
@target_topics.add(topic)
|
216
|
+
@pending_message_queue.write(message)
|
217
|
+
|
218
|
+
@instrumenter.instrument("produce_message.producer", {
|
219
|
+
value: value,
|
220
|
+
key: key,
|
221
|
+
topic: topic,
|
222
|
+
create_time: create_time,
|
223
|
+
message_size: message.bytesize,
|
224
|
+
buffer_size: buffer_size,
|
225
|
+
max_buffer_size: @max_buffer_size,
|
226
|
+
})
|
227
|
+
|
228
|
+
nil
|
229
|
+
end
|
230
|
+
|
231
|
+
# Sends all buffered messages to the Kafka brokers.
|
232
|
+
#
|
233
|
+
# Depending on the value of `required_acks` used when initializing the producer,
|
234
|
+
# this call may block until the specified number of replicas have acknowledged
|
235
|
+
# the writes. The `ack_timeout` setting places an upper bound on the amount of
|
236
|
+
# time the call will block before failing.
|
237
|
+
#
|
238
|
+
# @raise [DeliveryFailed] if not all messages could be successfully sent.
|
239
|
+
# @return [nil]
|
240
|
+
def deliver_messages
|
241
|
+
# There's no need to do anything if the buffer is empty.
|
242
|
+
return if buffer_size == 0
|
243
|
+
|
244
|
+
@instrumenter.instrument("deliver_messages.producer") do |notification|
|
245
|
+
message_count = buffer_size
|
246
|
+
|
247
|
+
notification[:message_count] = message_count
|
248
|
+
notification[:attempts] = 0
|
249
|
+
|
250
|
+
begin
|
251
|
+
deliver_messages_with_retries(notification)
|
252
|
+
ensure
|
253
|
+
notification[:delivered_message_count] = message_count - buffer_size
|
254
|
+
end
|
255
|
+
end
|
256
|
+
end
|
257
|
+
|
258
|
+
# Returns the number of messages currently held in the buffer.
|
259
|
+
#
|
260
|
+
# @return [Integer] buffer size.
|
261
|
+
def buffer_size
|
262
|
+
@pending_message_queue.size + @buffer.size
|
263
|
+
end
|
264
|
+
|
265
|
+
def buffer_bytesize
|
266
|
+
@pending_message_queue.bytesize + @buffer.bytesize
|
267
|
+
end
|
268
|
+
|
269
|
+
# Deletes all buffered messages.
|
270
|
+
#
|
271
|
+
# @return [nil]
|
272
|
+
def clear_buffer
|
273
|
+
@buffer.clear
|
274
|
+
@pending_message_queue.clear
|
275
|
+
end
|
276
|
+
|
277
|
+
# Closes all connections to the brokers.
|
278
|
+
#
|
279
|
+
# @return [nil]
|
280
|
+
def shutdown
|
281
|
+
@transaction_manager.close
|
282
|
+
@cluster.disconnect
|
283
|
+
end
|
284
|
+
|
285
|
+
# Initializes the producer to ready for future transactions. This method
|
286
|
+
# should be triggered once, before any tranactions are created.
|
287
|
+
#
|
288
|
+
# @return [nil]
|
289
|
+
def init_transactions
|
290
|
+
@transaction_manager.init_transactions
|
291
|
+
end
|
292
|
+
|
293
|
+
# Mark the beginning of a transaction. This method transitions the state
|
294
|
+
# of the transaction trantiions to IN_TRANSACTION.
|
295
|
+
#
|
296
|
+
# All producing operations can only be executed while the transation is
|
297
|
+
# in this state. The records are persisted by Kafka brokers, but not visible
|
298
|
+
# the consumers until the #commit_transaction method is trigger. After a
|
299
|
+
# timeout period without committed, the transaction is timeout and
|
300
|
+
# considered as aborted.
|
301
|
+
#
|
302
|
+
# @return [nil]
|
303
|
+
def begin_transaction
|
304
|
+
@transaction_manager.begin_transaction
|
305
|
+
end
|
306
|
+
|
307
|
+
# This method commits the pending transaction, marks all the produced
|
308
|
+
# records committed. After that, they are visible to the consumers.
|
309
|
+
#
|
310
|
+
# This method can only be called if and only if the current transaction
|
311
|
+
# is at IN_TRANSACTION state.
|
312
|
+
#
|
313
|
+
# @return [nil]
|
314
|
+
def commit_transaction
|
315
|
+
@transaction_manager.commit_transaction
|
316
|
+
end
|
317
|
+
|
318
|
+
# This method abort the pending transaction, marks all the produced
|
319
|
+
# records aborted. All the records will be wiped out by the brokers and the
|
320
|
+
# cosumers don't have a chance to consume those messages, except they enable
|
321
|
+
# consuming uncommitted option.
|
322
|
+
#
|
323
|
+
# This method can only be called if and only if the current transaction
|
324
|
+
# is at IN_TRANSACTION state.
|
325
|
+
#
|
326
|
+
# @return [nil]
|
327
|
+
def abort_transaction
|
328
|
+
@transaction_manager.abort_transaction
|
329
|
+
end
|
330
|
+
|
331
|
+
# Syntactic sugar to enable easier transaction usage. Do the following steps
|
332
|
+
#
|
333
|
+
# - Start the transaction (with Producer#begin_transaction)
|
334
|
+
# - Yield the given block
|
335
|
+
# - Commit the transaction (with Producer#commit_transaction)
|
336
|
+
#
|
337
|
+
# If the block raises exception, the transaction is automatically aborted
|
338
|
+
# *before* bubble up the exception.
|
339
|
+
#
|
340
|
+
# If the block raises Kafka::Producer::AbortTransaction indicator exception,
|
341
|
+
# it aborts the transaction silently, without throwing up that exception.
|
342
|
+
#
|
343
|
+
# @return [nil]
|
344
|
+
def transaction
|
345
|
+
raise 'This method requires a block' unless block_given?
|
346
|
+
begin_transaction
|
347
|
+
yield
|
348
|
+
commit_transaction
|
349
|
+
rescue Kafka::Producer::AbortTransaction
|
350
|
+
abort_transaction
|
351
|
+
rescue
|
352
|
+
abort_transaction
|
353
|
+
raise
|
354
|
+
end
|
355
|
+
|
356
|
+
private
|
357
|
+
|
358
|
+
def deliver_messages_with_retries(notification)
|
359
|
+
attempt = 0
|
360
|
+
|
361
|
+
@cluster.add_target_topics(@target_topics)
|
362
|
+
|
363
|
+
operation = ProduceOperation.new(
|
364
|
+
cluster: @cluster,
|
365
|
+
transaction_manager: @transaction_manager,
|
366
|
+
buffer: @buffer,
|
367
|
+
required_acks: @required_acks,
|
368
|
+
ack_timeout: @ack_timeout,
|
369
|
+
compressor: @compressor,
|
370
|
+
logger: @logger,
|
371
|
+
instrumenter: @instrumenter,
|
372
|
+
)
|
373
|
+
|
374
|
+
loop do
|
375
|
+
attempt += 1
|
376
|
+
|
377
|
+
notification[:attempts] = attempt
|
378
|
+
|
379
|
+
begin
|
380
|
+
@cluster.refresh_metadata_if_necessary!
|
381
|
+
rescue ConnectionError => e
|
382
|
+
raise DeliveryFailed.new(e, buffer_messages)
|
383
|
+
end
|
384
|
+
|
385
|
+
assign_partitions!
|
386
|
+
operation.execute
|
387
|
+
|
388
|
+
if @required_acks.zero?
|
389
|
+
# No response is returned by the brokers, so we can't know which messages
|
390
|
+
# have been successfully written. Our only option is to assume that they all
|
391
|
+
# have.
|
392
|
+
@buffer.clear
|
393
|
+
end
|
394
|
+
|
395
|
+
if buffer_size.zero?
|
396
|
+
break
|
397
|
+
elsif attempt <= @max_retries
|
398
|
+
@logger.warn "Failed to send all messages to #{pretty_partitions}; attempting retry #{attempt} of #{@max_retries} after #{@retry_backoff}s"
|
399
|
+
|
400
|
+
sleep @retry_backoff
|
401
|
+
else
|
402
|
+
@logger.error "Failed to send all messages to #{pretty_partitions}; keeping remaining messages in buffer"
|
403
|
+
break
|
404
|
+
end
|
405
|
+
end
|
406
|
+
|
407
|
+
unless @pending_message_queue.empty?
|
408
|
+
# Mark the cluster as stale in order to force a cluster metadata refresh.
|
409
|
+
@cluster.mark_as_stale!
|
410
|
+
raise DeliveryFailed.new("Failed to assign partitions to #{@pending_message_queue.size} messages", buffer_messages)
|
411
|
+
end
|
412
|
+
|
413
|
+
unless @buffer.empty?
|
414
|
+
raise DeliveryFailed.new("Failed to send messages to #{pretty_partitions}", buffer_messages)
|
415
|
+
end
|
416
|
+
end
|
417
|
+
|
418
|
+
def pretty_partitions
|
419
|
+
@buffer.map {|topic, partition, _| "#{topic}/#{partition}" }.join(", ")
|
420
|
+
end
|
421
|
+
|
422
|
+
def assign_partitions!
|
423
|
+
failed_messages = []
|
424
|
+
topics_with_failures = Set.new
|
425
|
+
|
426
|
+
@pending_message_queue.each do |message|
|
427
|
+
partition = message.partition
|
428
|
+
|
429
|
+
begin
|
430
|
+
# If a message for a topic fails to receive a partition all subsequent
|
431
|
+
# messages for the topic should be retried to preserve ordering
|
432
|
+
if topics_with_failures.include?(message.topic)
|
433
|
+
failed_messages << message
|
434
|
+
next
|
435
|
+
end
|
436
|
+
|
437
|
+
if partition.nil?
|
438
|
+
partition_count = @cluster.partitions_for(message.topic).count
|
439
|
+
partition = Partitioner.partition_for_key(partition_count, message)
|
440
|
+
end
|
441
|
+
|
442
|
+
@buffer.write(
|
443
|
+
value: message.value,
|
444
|
+
key: message.key,
|
445
|
+
headers: message.headers,
|
446
|
+
topic: message.topic,
|
447
|
+
partition: partition,
|
448
|
+
create_time: message.create_time,
|
449
|
+
)
|
450
|
+
rescue Kafka::Error => e
|
451
|
+
@instrumenter.instrument("topic_error.producer", {
|
452
|
+
topic: message.topic,
|
453
|
+
exception: [e.class.to_s, e.message],
|
454
|
+
})
|
455
|
+
|
456
|
+
topics_with_failures << message.topic
|
457
|
+
failed_messages << message
|
458
|
+
end
|
459
|
+
end
|
460
|
+
|
461
|
+
if failed_messages.any?
|
462
|
+
failed_messages.group_by(&:topic).each do |topic, messages|
|
463
|
+
@logger.error "Failed to assign partitions to #{messages.count} messages in #{topic}"
|
464
|
+
end
|
465
|
+
|
466
|
+
@cluster.mark_as_stale!
|
467
|
+
end
|
468
|
+
|
469
|
+
@pending_message_queue.replace(failed_messages)
|
470
|
+
end
|
471
|
+
|
472
|
+
def buffer_messages
|
473
|
+
messages = []
|
474
|
+
|
475
|
+
@pending_message_queue.each do |message|
|
476
|
+
messages << message
|
477
|
+
end
|
478
|
+
|
479
|
+
@buffer.each do |topic, partition, messages_for_partition|
|
480
|
+
messages_for_partition.each do |message|
|
481
|
+
messages << PendingMessage.new(
|
482
|
+
value: message.value,
|
483
|
+
key: message.key,
|
484
|
+
headers: message.headers,
|
485
|
+
topic: topic,
|
486
|
+
partition: partition,
|
487
|
+
partition_key: nil,
|
488
|
+
create_time: message.create_time
|
489
|
+
)
|
490
|
+
end
|
491
|
+
end
|
492
|
+
|
493
|
+
messages
|
494
|
+
end
|
495
|
+
|
496
|
+
def buffer_overflow(topic, message)
|
497
|
+
@instrumenter.instrument("buffer_overflow.producer", {
|
498
|
+
topic: topic,
|
499
|
+
})
|
500
|
+
|
501
|
+
raise BufferOverflow, message
|
502
|
+
end
|
503
|
+
end
|
504
|
+
end
|