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.
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