ruby-kafka 0.1.7 → 0.2.0
Sign up to get free protection for your applications and to get access to all the features.
- 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
|