ruby-kafka 0.7.0 → 0.7.1.beta1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.circleci/config.yml +3 -3
- data/.gitignore +1 -0
- data/CHANGELOG.md +4 -0
- data/lib/kafka.rb +32 -0
- data/lib/kafka/broker.rb +18 -0
- data/lib/kafka/client.rb +38 -4
- data/lib/kafka/cluster.rb +60 -37
- data/lib/kafka/consumer.rb +2 -2
- data/lib/kafka/fetch_operation.rb +18 -59
- data/lib/kafka/fetched_batch.rb +9 -9
- data/lib/kafka/fetched_batch_generator.rb +114 -0
- data/lib/kafka/fetched_offset_resolver.rb +48 -0
- data/lib/kafka/fetcher.rb +2 -2
- data/lib/kafka/produce_operation.rb +52 -14
- data/lib/kafka/producer.rb +82 -2
- data/lib/kafka/protocol.rb +68 -48
- 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/decoder.rb +3 -6
- data/lib/kafka/protocol/encoder.rb +6 -11
- 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 +3 -1
- data/lib/kafka/protocol/fetch_response.rb +37 -18
- 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/list_offset_request.rb +8 -2
- data/lib/kafka/protocol/list_offset_response.rb +11 -6
- data/lib/kafka/protocol/record.rb +9 -0
- data/lib/kafka/protocol/record_batch.rb +17 -1
- data/lib/kafka/ssl_context.rb +19 -5
- data/lib/kafka/transaction_manager.rb +261 -0
- data/lib/kafka/transaction_state_machine.rb +72 -0
- data/lib/kafka/version.rb +1 -1
- data/ruby-kafka.gemspec +1 -1
- metadata +20 -4
data/lib/kafka/protocol.rb
CHANGED
@@ -14,46 +14,52 @@ module Kafka
|
|
14
14
|
# The replica id of non-brokers is always -1.
|
15
15
|
REPLICA_ID = -1
|
16
16
|
|
17
|
-
PRODUCE_API
|
18
|
-
FETCH_API
|
19
|
-
LIST_OFFSET_API
|
20
|
-
TOPIC_METADATA_API
|
21
|
-
OFFSET_COMMIT_API
|
22
|
-
OFFSET_FETCH_API
|
23
|
-
FIND_COORDINATOR_API
|
24
|
-
JOIN_GROUP_API
|
25
|
-
HEARTBEAT_API
|
26
|
-
LEAVE_GROUP_API
|
27
|
-
SYNC_GROUP_API
|
28
|
-
DESCRIBE_GROUPS_API
|
29
|
-
LIST_GROUPS_API
|
30
|
-
SASL_HANDSHAKE_API
|
31
|
-
API_VERSIONS_API
|
32
|
-
CREATE_TOPICS_API
|
33
|
-
DELETE_TOPICS_API
|
34
|
-
|
35
|
-
|
36
|
-
|
17
|
+
PRODUCE_API = 0
|
18
|
+
FETCH_API = 1
|
19
|
+
LIST_OFFSET_API = 2
|
20
|
+
TOPIC_METADATA_API = 3
|
21
|
+
OFFSET_COMMIT_API = 8
|
22
|
+
OFFSET_FETCH_API = 9
|
23
|
+
FIND_COORDINATOR_API = 10
|
24
|
+
JOIN_GROUP_API = 11
|
25
|
+
HEARTBEAT_API = 12
|
26
|
+
LEAVE_GROUP_API = 13
|
27
|
+
SYNC_GROUP_API = 14
|
28
|
+
DESCRIBE_GROUPS_API = 15
|
29
|
+
LIST_GROUPS_API = 16
|
30
|
+
SASL_HANDSHAKE_API = 17
|
31
|
+
API_VERSIONS_API = 18
|
32
|
+
CREATE_TOPICS_API = 19
|
33
|
+
DELETE_TOPICS_API = 20
|
34
|
+
INIT_PRODUCER_ID_API = 22
|
35
|
+
ADD_PARTITIONS_TO_TXN_API = 24
|
36
|
+
END_TXN_API = 26
|
37
|
+
DESCRIBE_CONFIGS_API = 32
|
38
|
+
ALTER_CONFIGS_API = 33
|
39
|
+
CREATE_PARTITIONS_API = 37
|
37
40
|
|
38
41
|
# A mapping from numeric API keys to symbolic API names.
|
39
42
|
APIS = {
|
40
|
-
PRODUCE_API
|
41
|
-
FETCH_API
|
42
|
-
LIST_OFFSET_API
|
43
|
-
TOPIC_METADATA_API
|
44
|
-
OFFSET_COMMIT_API
|
45
|
-
OFFSET_FETCH_API
|
46
|
-
FIND_COORDINATOR_API
|
47
|
-
JOIN_GROUP_API
|
48
|
-
HEARTBEAT_API
|
49
|
-
LEAVE_GROUP_API
|
50
|
-
SYNC_GROUP_API
|
51
|
-
SASL_HANDSHAKE_API
|
52
|
-
API_VERSIONS_API
|
53
|
-
CREATE_TOPICS_API
|
54
|
-
DELETE_TOPICS_API
|
55
|
-
|
56
|
-
|
43
|
+
PRODUCE_API => :produce,
|
44
|
+
FETCH_API => :fetch,
|
45
|
+
LIST_OFFSET_API => :list_offset,
|
46
|
+
TOPIC_METADATA_API => :topic_metadata,
|
47
|
+
OFFSET_COMMIT_API => :offset_commit,
|
48
|
+
OFFSET_FETCH_API => :offset_fetch,
|
49
|
+
FIND_COORDINATOR_API => :find_coordinator,
|
50
|
+
JOIN_GROUP_API => :join_group,
|
51
|
+
HEARTBEAT_API => :heartbeat,
|
52
|
+
LEAVE_GROUP_API => :leave_group,
|
53
|
+
SYNC_GROUP_API => :sync_group,
|
54
|
+
SASL_HANDSHAKE_API => :sasl_handshake,
|
55
|
+
API_VERSIONS_API => :api_versions,
|
56
|
+
CREATE_TOPICS_API => :create_topics,
|
57
|
+
DELETE_TOPICS_API => :delete_topics,
|
58
|
+
INIT_PRODUCER_ID_API => :init_producer_id_api,
|
59
|
+
ADD_PARTITIONS_TO_TXN_API => :add_partitions_to_txn_api,
|
60
|
+
END_TXN_API => :end_txn_api,
|
61
|
+
DESCRIBE_CONFIGS_API => :describe_configs_api,
|
62
|
+
CREATE_PARTITIONS_API => :create_partitions
|
57
63
|
}
|
58
64
|
|
59
65
|
# A mapping from numeric error codes to exception classes.
|
@@ -95,24 +101,32 @@ module Kafka
|
|
95
101
|
39 => InvalidReplicaAssignment,
|
96
102
|
40 => InvalidConfig,
|
97
103
|
41 => NotController,
|
98
|
-
42 => InvalidRequest
|
104
|
+
42 => InvalidRequest,
|
105
|
+
45 => OutOfOrderSequenceNumberError,
|
106
|
+
46 => DuplicateSequenceNumberError,
|
107
|
+
47 => InvalidProducerEpochError,
|
108
|
+
48 => InvalidTxnStateError,
|
109
|
+
49 => InvalidProducerIDMappingError,
|
110
|
+
50 => InvalidTransactionTimeoutError,
|
111
|
+
51 => ConcurrentTransactionError,
|
112
|
+
52 => TransactionCoordinatorFencedError
|
99
113
|
}
|
100
114
|
|
101
115
|
# A mapping from int to corresponding resource type in symbol.
|
102
116
|
# https://github.com/apache/kafka/blob/trunk/clients/src/main/java/org/apache/kafka/common/resource/ResourceType.java
|
103
|
-
RESOURCE_TYPE_UNKNOWN
|
104
|
-
RESOURCE_TYPE_ANY
|
105
|
-
RESOURCE_TYPE_TOPIC
|
106
|
-
RESOURCE_TYPE_GROUP
|
107
|
-
RESOURCE_TYPE_CLUSTER
|
117
|
+
RESOURCE_TYPE_UNKNOWN = 0
|
118
|
+
RESOURCE_TYPE_ANY = 1
|
119
|
+
RESOURCE_TYPE_TOPIC = 2
|
120
|
+
RESOURCE_TYPE_GROUP = 3
|
121
|
+
RESOURCE_TYPE_CLUSTER = 4
|
108
122
|
RESOURCE_TYPE_TRANSACTIONAL_ID = 5
|
109
123
|
RESOURCE_TYPE_DELEGATION_TOKEN = 6
|
110
124
|
RESOURCE_TYPES = {
|
111
|
-
RESOURCE_TYPE_UNKNOWN
|
112
|
-
RESOURCE_TYPE_ANY
|
113
|
-
RESOURCE_TYPE_TOPIC
|
114
|
-
RESOURCE_TYPE_GROUP
|
115
|
-
RESOURCE_TYPE_CLUSTER
|
125
|
+
RESOURCE_TYPE_UNKNOWN => :unknown,
|
126
|
+
RESOURCE_TYPE_ANY => :any,
|
127
|
+
RESOURCE_TYPE_TOPIC => :topic,
|
128
|
+
RESOURCE_TYPE_GROUP => :group,
|
129
|
+
RESOURCE_TYPE_CLUSTER => :cluster,
|
116
130
|
RESOURCE_TYPE_TRANSACTIONAL_ID => :transactional_id,
|
117
131
|
RESOURCE_TYPE_DELEGATION_TOKEN => :delegation_token,
|
118
132
|
}
|
@@ -188,3 +202,9 @@ require "kafka/protocol/list_groups_request"
|
|
188
202
|
require "kafka/protocol/list_groups_response"
|
189
203
|
require "kafka/protocol/describe_groups_request"
|
190
204
|
require "kafka/protocol/describe_groups_response"
|
205
|
+
require "kafka/protocol/init_producer_id_request"
|
206
|
+
require "kafka/protocol/init_producer_id_response"
|
207
|
+
require "kafka/protocol/add_partitions_to_txn_request"
|
208
|
+
require "kafka/protocol/add_partitions_to_txn_response"
|
209
|
+
require "kafka/protocol/end_txn_request"
|
210
|
+
require "kafka/protocol/end_txn_response"
|
@@ -0,0 +1,34 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Kafka
|
4
|
+
module Protocol
|
5
|
+
class AddPartitionsToTxnRequest
|
6
|
+
def initialize(transactional_id: nil, producer_id:, producer_epoch:, topics:)
|
7
|
+
@transactional_id = transactional_id
|
8
|
+
@producer_id = producer_id
|
9
|
+
@producer_epoch = producer_epoch
|
10
|
+
@topics = topics
|
11
|
+
end
|
12
|
+
|
13
|
+
def api_key
|
14
|
+
ADD_PARTITIONS_TO_TXN_API
|
15
|
+
end
|
16
|
+
|
17
|
+
def response_class
|
18
|
+
AddPartitionsToTxnResponse
|
19
|
+
end
|
20
|
+
|
21
|
+
def encode(encoder)
|
22
|
+
encoder.write_string(@transactional_id.to_s)
|
23
|
+
encoder.write_int64(@producer_id)
|
24
|
+
encoder.write_int16(@producer_epoch)
|
25
|
+
encoder.write_array(@topics.to_a) do |topic, partitions|
|
26
|
+
encoder.write_string(topic)
|
27
|
+
encoder.write_array(partitions) do |partition|
|
28
|
+
encoder.write_int32(partition)
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
@@ -0,0 +1,47 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Kafka
|
4
|
+
module Protocol
|
5
|
+
class AddPartitionsToTxnResponse
|
6
|
+
class PartitionError
|
7
|
+
attr_reader :partition, :error_code
|
8
|
+
|
9
|
+
def initialize(partition:, error_code:)
|
10
|
+
@partition = partition
|
11
|
+
@error_code = error_code
|
12
|
+
end
|
13
|
+
end
|
14
|
+
|
15
|
+
class TopicPartitionsError
|
16
|
+
attr_reader :topic, :partitions
|
17
|
+
|
18
|
+
def initialize(topic:, partitions:)
|
19
|
+
@topic = topic
|
20
|
+
@partitions = partitions
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
attr_reader :errors
|
25
|
+
|
26
|
+
def initialize(errors:)
|
27
|
+
@errors = errors
|
28
|
+
end
|
29
|
+
|
30
|
+
def self.decode(decoder)
|
31
|
+
_throttle_time_ms = decoder.int32
|
32
|
+
errors = decoder.array do
|
33
|
+
TopicPartitionsError.new(
|
34
|
+
topic: decoder.string,
|
35
|
+
partitions: decoder.array do
|
36
|
+
PartitionError.new(
|
37
|
+
partition: decoder.int32,
|
38
|
+
error_code: decoder.int16
|
39
|
+
)
|
40
|
+
end
|
41
|
+
)
|
42
|
+
end
|
43
|
+
new(errors: errors)
|
44
|
+
end
|
45
|
+
end
|
46
|
+
end
|
47
|
+
end
|
@@ -6,8 +6,6 @@ module Kafka
|
|
6
6
|
# from it. The Kafka protocol is not self-describing, so a client must call
|
7
7
|
# these methods in just the right order for things to work.
|
8
8
|
class Decoder
|
9
|
-
VARINT_MASK = 0b10000000
|
10
|
-
|
11
9
|
def self.from_string(str)
|
12
10
|
new(StringIO.new(str))
|
13
11
|
end
|
@@ -123,12 +121,11 @@ module Kafka
|
|
123
121
|
def varint
|
124
122
|
group = 0
|
125
123
|
data = 0
|
126
|
-
|
127
|
-
chunk
|
128
|
-
data |= (chunk & (~VARINT_MASK)) << group
|
124
|
+
while (chunk = int8) & 0x80 != 0
|
125
|
+
data |= (chunk & 0x7f) << group
|
129
126
|
group += 7
|
130
|
-
break if (chunk & VARINT_MASK) == 0
|
131
127
|
end
|
128
|
+
data |= chunk << group
|
132
129
|
data & 0b1 != 0 ? ~(data >> 1) : (data >> 1)
|
133
130
|
end
|
134
131
|
|
@@ -7,8 +7,6 @@ module Kafka
|
|
7
7
|
# An encoder wraps an IO object, making it easy to write specific data types
|
8
8
|
# to it.
|
9
9
|
class Encoder
|
10
|
-
VARINT_MASK = 0b10000000
|
11
|
-
|
12
10
|
# Initializes a new encoder.
|
13
11
|
#
|
14
12
|
# @param io [IO] an object that acts as an IO.
|
@@ -134,16 +132,13 @@ module Kafka
|
|
134
132
|
int = int << 1
|
135
133
|
int = ~int | 1 if int < 0
|
136
134
|
|
137
|
-
|
138
|
-
|
139
|
-
|
140
|
-
|
141
|
-
write_int8(chunk)
|
142
|
-
return
|
143
|
-
else
|
144
|
-
write_int8(chunk | VARINT_MASK)
|
145
|
-
end
|
135
|
+
chunks = []
|
136
|
+
while int & 0xff80 != 0
|
137
|
+
chunks << (int & 0x7f | 0x80)
|
138
|
+
int >>= 7
|
146
139
|
end
|
140
|
+
chunks << int
|
141
|
+
write(chunks.pack("C*"))
|
147
142
|
end
|
148
143
|
|
149
144
|
# Writes a byte string to the IO object.
|
@@ -0,0 +1,29 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Kafka
|
4
|
+
module Protocol
|
5
|
+
class EndTxnRequest
|
6
|
+
def initialize(transactional_id:, producer_id:, producer_epoch:, transaction_result:)
|
7
|
+
@transactional_id = transactional_id
|
8
|
+
@producer_id = producer_id
|
9
|
+
@producer_epoch = producer_epoch
|
10
|
+
@transaction_result = transaction_result
|
11
|
+
end
|
12
|
+
|
13
|
+
def api_key
|
14
|
+
END_TXN_API
|
15
|
+
end
|
16
|
+
|
17
|
+
def response_class
|
18
|
+
EndTxnResposne
|
19
|
+
end
|
20
|
+
|
21
|
+
def encode(encoder)
|
22
|
+
encoder.write_string(@transactional_id)
|
23
|
+
encoder.write_int64(@producer_id)
|
24
|
+
encoder.write_int16(@producer_epoch)
|
25
|
+
encoder.write_boolean(@transaction_result)
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
@@ -0,0 +1,19 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Kafka
|
4
|
+
module Protocol
|
5
|
+
class EndTxnResposne
|
6
|
+
attr_reader :error_code
|
7
|
+
|
8
|
+
def initialize(error_code:)
|
9
|
+
@error_code = error_code
|
10
|
+
end
|
11
|
+
|
12
|
+
def self.decode(decoder)
|
13
|
+
_throttle_time_ms = decoder.int32
|
14
|
+
error_code = decoder.int16
|
15
|
+
new(error_code: error_code)
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
@@ -7,10 +7,12 @@ module Kafka
|
|
7
7
|
#
|
8
8
|
# ## API Specification
|
9
9
|
#
|
10
|
-
# FetchRequest => ReplicaId MaxWaitTime MinBytes [TopicName [Partition FetchOffset MaxBytes]]
|
10
|
+
# FetchRequest => ReplicaId MaxWaitTime MinBytes MaxBytes IsolationLevel [TopicName [Partition FetchOffset MaxBytes]]
|
11
11
|
# ReplicaId => int32
|
12
12
|
# MaxWaitTime => int32
|
13
13
|
# MinBytes => int32
|
14
|
+
# MaxBytes => int32
|
15
|
+
# IsolationLevel => int8
|
14
16
|
# TopicName => string
|
15
17
|
# Partition => int32
|
16
18
|
# FetchOffset => int64
|
@@ -10,12 +10,18 @@ module Kafka
|
|
10
10
|
#
|
11
11
|
# ## API Specification
|
12
12
|
#
|
13
|
-
# FetchResponse => [TopicName [Partition ErrorCode HighwaterMarkOffset
|
13
|
+
# FetchResponse => ThrottleTimeMS [TopicName [Partition ErrorCode HighwaterMarkOffset LastStableOffset [AbortedTransaction] Records]]
|
14
|
+
# ThrottleTimeMS => int32
|
14
15
|
# TopicName => string
|
15
16
|
# Partition => int32
|
16
17
|
# ErrorCode => int16
|
17
18
|
# HighwaterMarkOffset => int64
|
19
|
+
# LastStableOffset => int64
|
18
20
|
# MessageSetSize => int32
|
21
|
+
# AbortedTransaction => [
|
22
|
+
# ProducerId => int64
|
23
|
+
# FirstOffset => int64
|
24
|
+
# ]
|
19
25
|
#
|
20
26
|
class FetchResponse
|
21
27
|
MAGIC_BYTE_OFFSET = 16
|
@@ -44,6 +50,15 @@ module Kafka
|
|
44
50
|
end
|
45
51
|
end
|
46
52
|
|
53
|
+
class AbortedTransaction
|
54
|
+
attr_reader :producer_id, :first_offset
|
55
|
+
|
56
|
+
def initialize(producer_id:, first_offset:)
|
57
|
+
@producer_id = producer_id
|
58
|
+
@first_offset = first_offset
|
59
|
+
end
|
60
|
+
end
|
61
|
+
|
47
62
|
attr_reader :topics
|
48
63
|
|
49
64
|
def initialize(topics: [], throttle_time_ms: 0)
|
@@ -66,32 +81,36 @@ module Kafka
|
|
66
81
|
aborted_transactions = decoder.array do
|
67
82
|
producer_id = decoder.int64
|
68
83
|
first_offset = decoder.int64
|
69
|
-
|
84
|
+
AbortedTransaction.new(
|
70
85
|
producer_id: producer_id,
|
71
86
|
first_offset: first_offset
|
72
|
-
|
87
|
+
)
|
73
88
|
end
|
74
89
|
|
75
|
-
|
90
|
+
messages_raw = decoder.bytes
|
76
91
|
messages = []
|
77
92
|
|
78
|
-
|
79
|
-
|
80
|
-
|
81
|
-
|
82
|
-
|
83
|
-
|
84
|
-
|
85
|
-
|
86
|
-
|
87
|
-
|
88
|
-
|
93
|
+
if !messages_raw.nil? && !messages_raw.empty?
|
94
|
+
messages_decoder = Decoder.from_string(messages_raw)
|
95
|
+
|
96
|
+
magic_byte = messages_decoder.peek(MAGIC_BYTE_OFFSET, MAGIC_BYTE_LENGTH)[0].to_i
|
97
|
+
if magic_byte == RecordBatch::MAGIC_BYTE
|
98
|
+
until messages_decoder.eof?
|
99
|
+
begin
|
100
|
+
record_batch = RecordBatch.decode(messages_decoder)
|
101
|
+
messages << record_batch
|
102
|
+
rescue InsufficientDataMessage
|
103
|
+
if messages.length > 0
|
104
|
+
break
|
105
|
+
else
|
106
|
+
raise
|
107
|
+
end
|
89
108
|
end
|
90
109
|
end
|
110
|
+
else
|
111
|
+
message_set = MessageSet.decode(messages_decoder)
|
112
|
+
messages << message_set
|
91
113
|
end
|
92
|
-
else
|
93
|
-
message_set = MessageSet.decode(messages_decoder)
|
94
|
-
messages = message_set.messages
|
95
114
|
end
|
96
115
|
|
97
116
|
FetchedPartition.new(
|
@@ -0,0 +1,26 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Kafka
|
4
|
+
module Protocol
|
5
|
+
class InitProducerIDRequest
|
6
|
+
def initialize(transactional_id: nil, transactional_timeout:)
|
7
|
+
@transactional_id = transactional_id
|
8
|
+
@transactional_timeout = transactional_timeout
|
9
|
+
end
|
10
|
+
|
11
|
+
def api_key
|
12
|
+
INIT_PRODUCER_ID_API
|
13
|
+
end
|
14
|
+
|
15
|
+
def response_class
|
16
|
+
InitProducerIDResponse
|
17
|
+
end
|
18
|
+
|
19
|
+
def encode(encoder)
|
20
|
+
encoder.write_string(@transactional_id)
|
21
|
+
# Timeout is in ms unit
|
22
|
+
encoder.write_int32(@transactional_timeout * 1000)
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|