ruby-kafka 0.1.7 → 0.2.0
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
- data/CHANGELOG.md +10 -0
- data/README.md +12 -1
- data/lib/kafka.rb +18 -0
- data/lib/kafka/broker.rb +42 -0
- data/lib/kafka/client.rb +35 -5
- data/lib/kafka/cluster.rb +30 -0
- data/lib/kafka/compressor.rb +59 -0
- data/lib/kafka/connection.rb +1 -0
- data/lib/kafka/consumer.rb +211 -0
- data/lib/kafka/consumer_group.rb +172 -0
- data/lib/kafka/fetch_operation.rb +2 -2
- data/lib/kafka/produce_operation.rb +4 -8
- data/lib/kafka/producer.rb +7 -5
- data/lib/kafka/protocol.rb +27 -0
- data/lib/kafka/protocol/consumer_group_protocol.rb +17 -0
- data/lib/kafka/protocol/group_coordinator_request.rb +21 -0
- data/lib/kafka/protocol/group_coordinator_response.rb +25 -0
- data/lib/kafka/protocol/heartbeat_request.rb +25 -0
- data/lib/kafka/protocol/heartbeat_response.rb +15 -0
- data/lib/kafka/protocol/join_group_request.rb +39 -0
- data/lib/kafka/protocol/join_group_response.rb +31 -0
- data/lib/kafka/protocol/leave_group_request.rb +23 -0
- data/lib/kafka/protocol/leave_group_response.rb +15 -0
- data/lib/kafka/protocol/member_assignment.rb +40 -0
- data/lib/kafka/protocol/message_set.rb +5 -37
- data/lib/kafka/protocol/metadata_response.rb +5 -1
- data/lib/kafka/protocol/offset_commit_request.rb +42 -0
- data/lib/kafka/protocol/offset_commit_response.rb +27 -0
- data/lib/kafka/protocol/offset_fetch_request.rb +34 -0
- data/lib/kafka/protocol/offset_fetch_response.rb +51 -0
- data/lib/kafka/protocol/sync_group_request.rb +31 -0
- data/lib/kafka/protocol/sync_group_response.rb +21 -0
- data/lib/kafka/round_robin_assignment_strategy.rb +40 -0
- data/lib/kafka/version.rb +1 -1
- metadata +23 -2
@@ -0,0 +1,172 @@
|
|
1
|
+
require "set"
|
2
|
+
require "kafka/round_robin_assignment_strategy"
|
3
|
+
|
4
|
+
module Kafka
|
5
|
+
class ConsumerGroup
|
6
|
+
attr_reader :assigned_partitions
|
7
|
+
|
8
|
+
def initialize(cluster:, logger:, group_id:, session_timeout:)
|
9
|
+
@cluster = cluster
|
10
|
+
@logger = logger
|
11
|
+
@group_id = group_id
|
12
|
+
@session_timeout = session_timeout
|
13
|
+
@member_id = ""
|
14
|
+
@generation_id = nil
|
15
|
+
@members = {}
|
16
|
+
@topics = Set.new
|
17
|
+
@assigned_partitions = {}
|
18
|
+
@assignment_strategy = RoundRobinAssignmentStrategy.new(cluster: @cluster)
|
19
|
+
end
|
20
|
+
|
21
|
+
def subscribe(topic)
|
22
|
+
@topics.add(topic)
|
23
|
+
@cluster.add_target_topics([topic])
|
24
|
+
end
|
25
|
+
|
26
|
+
def member?
|
27
|
+
!@generation_id.nil?
|
28
|
+
end
|
29
|
+
|
30
|
+
def join
|
31
|
+
join_group
|
32
|
+
synchronize
|
33
|
+
rescue NotCoordinatorForGroup
|
34
|
+
@logger.error "Failed to find coordinator for group `#{@group_id}`; retrying..."
|
35
|
+
sleep 1
|
36
|
+
@coordinator = nil
|
37
|
+
retry
|
38
|
+
rescue ConnectionError
|
39
|
+
@logger.error "Connection error while trying to join group `#{@group_id}`; retrying..."
|
40
|
+
@coordinator = nil
|
41
|
+
retry
|
42
|
+
end
|
43
|
+
|
44
|
+
def leave
|
45
|
+
@logger.info "[#{@member_id}] Leaving group `#{@group_id}`"
|
46
|
+
coordinator.leave_group(group_id: @group_id, member_id: @member_id)
|
47
|
+
end
|
48
|
+
|
49
|
+
def fetch_offsets
|
50
|
+
coordinator.fetch_offsets(
|
51
|
+
group_id: @group_id,
|
52
|
+
topics: @assigned_partitions,
|
53
|
+
)
|
54
|
+
end
|
55
|
+
|
56
|
+
def commit_offsets(offsets)
|
57
|
+
response = coordinator.commit_offsets(
|
58
|
+
group_id: @group_id,
|
59
|
+
member_id: @member_id,
|
60
|
+
generation_id: @generation_id,
|
61
|
+
offsets: offsets,
|
62
|
+
)
|
63
|
+
|
64
|
+
response.topics.each do |topic, partitions|
|
65
|
+
partitions.each do |partition, error_code|
|
66
|
+
Protocol.handle_error(error_code)
|
67
|
+
end
|
68
|
+
end
|
69
|
+
rescue UnknownMemberId
|
70
|
+
@logger.error "Kicked out of group; rejoining"
|
71
|
+
join
|
72
|
+
retry
|
73
|
+
rescue IllegalGeneration
|
74
|
+
@logger.error "Illegal generation #{@generation_id}; rejoining group"
|
75
|
+
join
|
76
|
+
retry
|
77
|
+
end
|
78
|
+
|
79
|
+
def heartbeat
|
80
|
+
@logger.info "[#{@member_id}] Sending heartbeat..."
|
81
|
+
|
82
|
+
response = coordinator.heartbeat(
|
83
|
+
group_id: @group_id,
|
84
|
+
generation_id: @generation_id,
|
85
|
+
member_id: @member_id,
|
86
|
+
)
|
87
|
+
|
88
|
+
Protocol.handle_error(response.error_code)
|
89
|
+
rescue ConnectionError => e
|
90
|
+
@logger.error "Connection error while sending heartbeat; rejoining"
|
91
|
+
join
|
92
|
+
rescue UnknownMemberId
|
93
|
+
@logger.error "Kicked out of group; rejoining"
|
94
|
+
join
|
95
|
+
rescue RebalanceInProgress
|
96
|
+
@logger.error "Group is rebalancing; rejoining"
|
97
|
+
join
|
98
|
+
end
|
99
|
+
|
100
|
+
private
|
101
|
+
|
102
|
+
def join_group
|
103
|
+
@logger.info "Joining group `#{@group_id}`"
|
104
|
+
|
105
|
+
response = coordinator.join_group(
|
106
|
+
group_id: @group_id,
|
107
|
+
session_timeout: @session_timeout,
|
108
|
+
member_id: @member_id,
|
109
|
+
)
|
110
|
+
|
111
|
+
Protocol.handle_error(response.error_code)
|
112
|
+
|
113
|
+
@generation_id = response.generation_id
|
114
|
+
@member_id = response.member_id
|
115
|
+
@leader_id = response.leader_id
|
116
|
+
@members = response.members
|
117
|
+
|
118
|
+
@logger.info "[#{@member_id}] Joined group `#{@group_id}` with member id `#{@member_id}`"
|
119
|
+
rescue UnknownMemberId
|
120
|
+
@logger.error "Failed to join group; resetting member id and retrying in 1s..."
|
121
|
+
|
122
|
+
@member_id = nil
|
123
|
+
sleep 1
|
124
|
+
|
125
|
+
retry
|
126
|
+
end
|
127
|
+
|
128
|
+
def group_leader?
|
129
|
+
@member_id == @leader_id
|
130
|
+
end
|
131
|
+
|
132
|
+
def synchronize
|
133
|
+
@logger.info "[#{@member_id}] Synchronizing group"
|
134
|
+
|
135
|
+
group_assignment = {}
|
136
|
+
|
137
|
+
if group_leader?
|
138
|
+
@logger.info "[#{@member_id}] Chosen as leader of group `#{@group_id}`"
|
139
|
+
|
140
|
+
group_assignment = @assignment_strategy.assign(
|
141
|
+
members: @members.keys,
|
142
|
+
topics: @topics,
|
143
|
+
)
|
144
|
+
end
|
145
|
+
|
146
|
+
response = coordinator.sync_group(
|
147
|
+
group_id: @group_id,
|
148
|
+
generation_id: @generation_id,
|
149
|
+
member_id: @member_id,
|
150
|
+
group_assignment: group_assignment,
|
151
|
+
)
|
152
|
+
|
153
|
+
Protocol.handle_error(response.error_code)
|
154
|
+
|
155
|
+
response.member_assignment.topics.each do |topic, assigned_partitions|
|
156
|
+
@logger.info "[#{@member_id}] Partitions assigned for `#{topic}`: #{assigned_partitions.join(', ')}"
|
157
|
+
end
|
158
|
+
|
159
|
+
@assigned_partitions.replace(response.member_assignment.topics)
|
160
|
+
end
|
161
|
+
|
162
|
+
def coordinator
|
163
|
+
@coordinator ||= @cluster.get_group_coordinator(group_id: @group_id)
|
164
|
+
rescue GroupCoordinatorNotAvailable
|
165
|
+
@logger.error "Group coordinator not available for group `#{@group_id}`"
|
166
|
+
|
167
|
+
sleep 1
|
168
|
+
|
169
|
+
retry
|
170
|
+
end
|
171
|
+
end
|
172
|
+
end
|
@@ -16,7 +16,7 @@ module Kafka
|
|
16
16
|
# operation.execute
|
17
17
|
#
|
18
18
|
class FetchOperation
|
19
|
-
def initialize(cluster:, logger:, min_bytes
|
19
|
+
def initialize(cluster:, logger:, min_bytes: 1, max_wait_time: 5)
|
20
20
|
@cluster = cluster
|
21
21
|
@logger = logger
|
22
22
|
@min_bytes = min_bytes
|
@@ -24,7 +24,7 @@ module Kafka
|
|
24
24
|
@topics = {}
|
25
25
|
end
|
26
26
|
|
27
|
-
def fetch_from_partition(topic, partition, offset
|
27
|
+
def fetch_from_partition(topic, partition, offset: :latest, max_bytes: 1048576)
|
28
28
|
if offset == :earliest
|
29
29
|
offset = -2
|
30
30
|
elsif offset == :latest
|
@@ -25,13 +25,12 @@ module Kafka
|
|
25
25
|
# * `sent_message_count` – the number of messages that were successfully sent.
|
26
26
|
#
|
27
27
|
class ProduceOperation
|
28
|
-
def initialize(cluster:, buffer:,
|
28
|
+
def initialize(cluster:, buffer:, compressor:, required_acks:, ack_timeout:, logger:)
|
29
29
|
@cluster = cluster
|
30
30
|
@buffer = buffer
|
31
31
|
@required_acks = required_acks
|
32
32
|
@ack_timeout = ack_timeout
|
33
|
-
@
|
34
|
-
@compression_threshold = compression_threshold
|
33
|
+
@compressor = compressor
|
35
34
|
@logger = logger
|
36
35
|
end
|
37
36
|
|
@@ -78,11 +77,8 @@ module Kafka
|
|
78
77
|
messages_for_topics = {}
|
79
78
|
|
80
79
|
message_buffer.each do |topic, partition, messages|
|
81
|
-
message_set = Protocol::MessageSet.new(
|
82
|
-
|
83
|
-
compression_codec: @compression_codec,
|
84
|
-
compression_threshold: @compression_threshold,
|
85
|
-
)
|
80
|
+
message_set = Protocol::MessageSet.new(messages: messages)
|
81
|
+
message_set = @compressor.compress(message_set)
|
86
82
|
|
87
83
|
messages_for_topics[topic] ||= {}
|
88
84
|
messages_for_topics[topic][partition] = message_set
|
data/lib/kafka/producer.rb
CHANGED
@@ -4,7 +4,7 @@ require "kafka/message_buffer"
|
|
4
4
|
require "kafka/produce_operation"
|
5
5
|
require "kafka/pending_message_queue"
|
6
6
|
require "kafka/pending_message"
|
7
|
-
require "kafka/
|
7
|
+
require "kafka/compressor"
|
8
8
|
|
9
9
|
module Kafka
|
10
10
|
|
@@ -173,8 +173,11 @@ module Kafka
|
|
173
173
|
@retry_backoff = retry_backoff
|
174
174
|
@max_buffer_size = max_buffer_size
|
175
175
|
@max_buffer_bytesize = max_buffer_bytesize
|
176
|
-
|
177
|
-
@
|
176
|
+
|
177
|
+
@compressor = Compressor.new(
|
178
|
+
codec_name: @compression_codec,
|
179
|
+
threshold: @compression_threshold,
|
180
|
+
)
|
178
181
|
|
179
182
|
# The set of topics that are produced to.
|
180
183
|
@target_topics = Set.new
|
@@ -303,8 +306,7 @@ module Kafka
|
|
303
306
|
buffer: @buffer,
|
304
307
|
required_acks: @required_acks,
|
305
308
|
ack_timeout: @ack_timeout,
|
306
|
-
|
307
|
-
compression_threshold: @compression_threshold,
|
309
|
+
compressor: @compressor,
|
308
310
|
logger: @logger,
|
309
311
|
)
|
310
312
|
|
data/lib/kafka/protocol.rb
CHANGED
@@ -8,6 +8,13 @@ module Kafka
|
|
8
8
|
1 => :fetch,
|
9
9
|
2 => :list_offset,
|
10
10
|
3 => :topic_metadata,
|
11
|
+
8 => :offset_commit,
|
12
|
+
9 => :offset_fetch,
|
13
|
+
10 => :group_coordinator,
|
14
|
+
11 => :join_group,
|
15
|
+
12 => :heartbeat,
|
16
|
+
13 => :leave_group,
|
17
|
+
14 => :sync_group,
|
11
18
|
}
|
12
19
|
|
13
20
|
ERRORS = {
|
@@ -23,11 +30,17 @@ module Kafka
|
|
23
30
|
9 => ReplicaNotAvailable,
|
24
31
|
10 => MessageSizeTooLarge,
|
25
32
|
12 => OffsetMetadataTooLarge,
|
33
|
+
15 => GroupCoordinatorNotAvailable,
|
34
|
+
16 => NotCoordinatorForGroup,
|
26
35
|
17 => InvalidTopic,
|
27
36
|
18 => RecordListTooLarge,
|
28
37
|
19 => NotEnoughReplicas,
|
29
38
|
20 => NotEnoughReplicasAfterAppend,
|
30
39
|
21 => InvalidRequiredAcks,
|
40
|
+
22 => IllegalGeneration,
|
41
|
+
25 => UnknownMemberId,
|
42
|
+
26 => InvalidSessionTimeout,
|
43
|
+
27 => RebalanceInProgress,
|
31
44
|
}
|
32
45
|
|
33
46
|
def self.handle_error(error_code)
|
@@ -54,3 +67,17 @@ require "kafka/protocol/fetch_request"
|
|
54
67
|
require "kafka/protocol/fetch_response"
|
55
68
|
require "kafka/protocol/list_offset_request"
|
56
69
|
require "kafka/protocol/list_offset_response"
|
70
|
+
require "kafka/protocol/group_coordinator_request"
|
71
|
+
require "kafka/protocol/group_coordinator_response"
|
72
|
+
require "kafka/protocol/join_group_request"
|
73
|
+
require "kafka/protocol/join_group_response"
|
74
|
+
require "kafka/protocol/sync_group_request"
|
75
|
+
require "kafka/protocol/sync_group_response"
|
76
|
+
require "kafka/protocol/leave_group_request"
|
77
|
+
require "kafka/protocol/leave_group_response"
|
78
|
+
require "kafka/protocol/heartbeat_request"
|
79
|
+
require "kafka/protocol/heartbeat_response"
|
80
|
+
require "kafka/protocol/offset_fetch_request"
|
81
|
+
require "kafka/protocol/offset_fetch_response"
|
82
|
+
require "kafka/protocol/offset_commit_request"
|
83
|
+
require "kafka/protocol/offset_commit_response"
|
@@ -0,0 +1,17 @@
|
|
1
|
+
module Kafka
|
2
|
+
module Protocol
|
3
|
+
class ConsumerGroupProtocol
|
4
|
+
def initialize(version: 0, topics:, user_data: nil)
|
5
|
+
@version = version
|
6
|
+
@topics = topics
|
7
|
+
@user_data = user_data
|
8
|
+
end
|
9
|
+
|
10
|
+
def encode(encoder)
|
11
|
+
encoder.write_int16(@version)
|
12
|
+
encoder.write_array(@topics) {|topic| encoder.write_string(topic) }
|
13
|
+
encoder.write_bytes(@user_data)
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
@@ -0,0 +1,21 @@
|
|
1
|
+
module Kafka
|
2
|
+
module Protocol
|
3
|
+
class GroupCoordinatorRequest
|
4
|
+
def initialize(group_id:)
|
5
|
+
@group_id = group_id
|
6
|
+
end
|
7
|
+
|
8
|
+
def api_key
|
9
|
+
10
|
10
|
+
end
|
11
|
+
|
12
|
+
def encode(encoder)
|
13
|
+
encoder.write_string(@group_id)
|
14
|
+
end
|
15
|
+
|
16
|
+
def response_class
|
17
|
+
GroupCoordinatorResponse
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
@@ -0,0 +1,25 @@
|
|
1
|
+
module Kafka
|
2
|
+
module Protocol
|
3
|
+
class GroupCoordinatorResponse
|
4
|
+
attr_reader :error_code
|
5
|
+
|
6
|
+
attr_reader :coordinator_id, :coordinator_host, :coordinator_port
|
7
|
+
|
8
|
+
def initialize(error_code:, coordinator_id:, coordinator_host:, coordinator_port:)
|
9
|
+
@error_code = error_code
|
10
|
+
@coordinator_id = coordinator_id
|
11
|
+
@coordinator_host = coordinator_host
|
12
|
+
@coordinator_port = coordinator_port
|
13
|
+
end
|
14
|
+
|
15
|
+
def self.decode(decoder)
|
16
|
+
new(
|
17
|
+
error_code: decoder.int16,
|
18
|
+
coordinator_id: decoder.int32,
|
19
|
+
coordinator_host: decoder.string,
|
20
|
+
coordinator_port: decoder.int32,
|
21
|
+
)
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
@@ -0,0 +1,25 @@
|
|
1
|
+
module Kafka
|
2
|
+
module Protocol
|
3
|
+
class HeartbeatRequest
|
4
|
+
def initialize(group_id:, generation_id:, member_id:)
|
5
|
+
@group_id = group_id
|
6
|
+
@generation_id = generation_id
|
7
|
+
@member_id = member_id
|
8
|
+
end
|
9
|
+
|
10
|
+
def api_key
|
11
|
+
12
|
12
|
+
end
|
13
|
+
|
14
|
+
def response_class
|
15
|
+
HeartbeatResponse
|
16
|
+
end
|
17
|
+
|
18
|
+
def encode(encoder)
|
19
|
+
encoder.write_string(@group_id)
|
20
|
+
encoder.write_int32(@generation_id)
|
21
|
+
encoder.write_string(@member_id)
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
@@ -0,0 +1,39 @@
|
|
1
|
+
require "kafka/protocol/consumer_group_protocol"
|
2
|
+
|
3
|
+
module Kafka
|
4
|
+
module Protocol
|
5
|
+
class JoinGroupRequest
|
6
|
+
PROTOCOL_TYPE = "consumer"
|
7
|
+
|
8
|
+
def initialize(group_id:, session_timeout:, member_id:, topics: [])
|
9
|
+
@group_id = group_id
|
10
|
+
@session_timeout = session_timeout * 1000 # Kafka wants ms.
|
11
|
+
@member_id = member_id || ""
|
12
|
+
@protocol_type = PROTOCOL_TYPE
|
13
|
+
@group_protocols = {
|
14
|
+
"standard" => ConsumerGroupProtocol.new(topics: ["test-messages"]),
|
15
|
+
}
|
16
|
+
end
|
17
|
+
|
18
|
+
def api_key
|
19
|
+
11
|
20
|
+
end
|
21
|
+
|
22
|
+
def response_class
|
23
|
+
JoinGroupResponse
|
24
|
+
end
|
25
|
+
|
26
|
+
def encode(encoder)
|
27
|
+
encoder.write_string(@group_id)
|
28
|
+
encoder.write_int32(@session_timeout)
|
29
|
+
encoder.write_string(@member_id)
|
30
|
+
encoder.write_string(@protocol_type)
|
31
|
+
|
32
|
+
encoder.write_array(@group_protocols) do |name, metadata|
|
33
|
+
encoder.write_string(name)
|
34
|
+
encoder.write_bytes(Encoder.encode_with(metadata))
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|