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.
Files changed (36) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +10 -0
  3. data/README.md +12 -1
  4. data/lib/kafka.rb +18 -0
  5. data/lib/kafka/broker.rb +42 -0
  6. data/lib/kafka/client.rb +35 -5
  7. data/lib/kafka/cluster.rb +30 -0
  8. data/lib/kafka/compressor.rb +59 -0
  9. data/lib/kafka/connection.rb +1 -0
  10. data/lib/kafka/consumer.rb +211 -0
  11. data/lib/kafka/consumer_group.rb +172 -0
  12. data/lib/kafka/fetch_operation.rb +2 -2
  13. data/lib/kafka/produce_operation.rb +4 -8
  14. data/lib/kafka/producer.rb +7 -5
  15. data/lib/kafka/protocol.rb +27 -0
  16. data/lib/kafka/protocol/consumer_group_protocol.rb +17 -0
  17. data/lib/kafka/protocol/group_coordinator_request.rb +21 -0
  18. data/lib/kafka/protocol/group_coordinator_response.rb +25 -0
  19. data/lib/kafka/protocol/heartbeat_request.rb +25 -0
  20. data/lib/kafka/protocol/heartbeat_response.rb +15 -0
  21. data/lib/kafka/protocol/join_group_request.rb +39 -0
  22. data/lib/kafka/protocol/join_group_response.rb +31 -0
  23. data/lib/kafka/protocol/leave_group_request.rb +23 -0
  24. data/lib/kafka/protocol/leave_group_response.rb +15 -0
  25. data/lib/kafka/protocol/member_assignment.rb +40 -0
  26. data/lib/kafka/protocol/message_set.rb +5 -37
  27. data/lib/kafka/protocol/metadata_response.rb +5 -1
  28. data/lib/kafka/protocol/offset_commit_request.rb +42 -0
  29. data/lib/kafka/protocol/offset_commit_response.rb +27 -0
  30. data/lib/kafka/protocol/offset_fetch_request.rb +34 -0
  31. data/lib/kafka/protocol/offset_fetch_response.rb +51 -0
  32. data/lib/kafka/protocol/sync_group_request.rb +31 -0
  33. data/lib/kafka/protocol/sync_group_response.rb +21 -0
  34. data/lib/kafka/round_robin_assignment_strategy.rb +40 -0
  35. data/lib/kafka/version.rb +1 -1
  36. 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:, max_wait_time:)
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:, max_bytes:)
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:, compression_codec:, compression_threshold:, required_acks:, ack_timeout:, logger:)
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
- @compression_codec = compression_codec
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
- messages: messages,
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
@@ -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/compression"
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
- @compression_codec = Compression.find_codec(compression_codec)
177
- @compression_threshold = compression_threshold
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
- compression_codec: @compression_codec,
307
- compression_threshold: @compression_threshold,
309
+ compressor: @compressor,
308
310
  logger: @logger,
309
311
  )
310
312
 
@@ -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,15 @@
1
+ module Kafka
2
+ module Protocol
3
+ class HeartbeatResponse
4
+ attr_reader :error_code
5
+
6
+ def initialize(error_code:)
7
+ @error_code = error_code
8
+ end
9
+
10
+ def self.decode(decoder)
11
+ new(error_code: decoder.int16)
12
+ end
13
+ end
14
+ end
15
+ 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