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.
Files changed (37) hide show
  1. checksums.yaml +4 -4
  2. data/.circleci/config.yml +3 -3
  3. data/.gitignore +1 -0
  4. data/CHANGELOG.md +4 -0
  5. data/lib/kafka.rb +32 -0
  6. data/lib/kafka/broker.rb +18 -0
  7. data/lib/kafka/client.rb +38 -4
  8. data/lib/kafka/cluster.rb +60 -37
  9. data/lib/kafka/consumer.rb +2 -2
  10. data/lib/kafka/fetch_operation.rb +18 -59
  11. data/lib/kafka/fetched_batch.rb +9 -9
  12. data/lib/kafka/fetched_batch_generator.rb +114 -0
  13. data/lib/kafka/fetched_offset_resolver.rb +48 -0
  14. data/lib/kafka/fetcher.rb +2 -2
  15. data/lib/kafka/produce_operation.rb +52 -14
  16. data/lib/kafka/producer.rb +82 -2
  17. data/lib/kafka/protocol.rb +68 -48
  18. data/lib/kafka/protocol/add_partitions_to_txn_request.rb +34 -0
  19. data/lib/kafka/protocol/add_partitions_to_txn_response.rb +47 -0
  20. data/lib/kafka/protocol/decoder.rb +3 -6
  21. data/lib/kafka/protocol/encoder.rb +6 -11
  22. data/lib/kafka/protocol/end_txn_request.rb +29 -0
  23. data/lib/kafka/protocol/end_txn_response.rb +19 -0
  24. data/lib/kafka/protocol/fetch_request.rb +3 -1
  25. data/lib/kafka/protocol/fetch_response.rb +37 -18
  26. data/lib/kafka/protocol/init_producer_id_request.rb +26 -0
  27. data/lib/kafka/protocol/init_producer_id_response.rb +27 -0
  28. data/lib/kafka/protocol/list_offset_request.rb +8 -2
  29. data/lib/kafka/protocol/list_offset_response.rb +11 -6
  30. data/lib/kafka/protocol/record.rb +9 -0
  31. data/lib/kafka/protocol/record_batch.rb +17 -1
  32. data/lib/kafka/ssl_context.rb +19 -5
  33. data/lib/kafka/transaction_manager.rb +261 -0
  34. data/lib/kafka/transaction_state_machine.rb +72 -0
  35. data/lib/kafka/version.rb +1 -1
  36. data/ruby-kafka.gemspec +1 -1
  37. metadata +20 -4
@@ -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 = 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
- DESCRIBE_CONFIGS_API = 32
35
- ALTER_CONFIGS_API = 33
36
- CREATE_PARTITIONS_API = 37
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 => :produce,
41
- FETCH_API => :fetch,
42
- LIST_OFFSET_API => :list_offset,
43
- TOPIC_METADATA_API => :topic_metadata,
44
- OFFSET_COMMIT_API => :offset_commit,
45
- OFFSET_FETCH_API => :offset_fetch,
46
- FIND_COORDINATOR_API => :find_coordinator,
47
- JOIN_GROUP_API => :join_group,
48
- HEARTBEAT_API => :heartbeat,
49
- LEAVE_GROUP_API => :leave_group,
50
- SYNC_GROUP_API => :sync_group,
51
- SASL_HANDSHAKE_API => :sasl_handshake,
52
- API_VERSIONS_API => :api_versions,
53
- CREATE_TOPICS_API => :create_topics,
54
- DELETE_TOPICS_API => :delete_topics,
55
- DESCRIBE_CONFIGS_API => :describe_configs_api,
56
- CREATE_PARTITIONS_API => :create_partitions
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 = 0
104
- RESOURCE_TYPE_ANY = 1
105
- RESOURCE_TYPE_TOPIC = 2
106
- RESOURCE_TYPE_GROUP = 3
107
- RESOURCE_TYPE_CLUSTER = 4
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 => :unknown,
112
- RESOURCE_TYPE_ANY => :any,
113
- RESOURCE_TYPE_TOPIC => :topic,
114
- RESOURCE_TYPE_GROUP => :group,
115
- RESOURCE_TYPE_CLUSTER => :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
- loop do
127
- chunk = int8
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
- loop do
138
- chunk = int & (~VARINT_MASK)
139
- int = int >> 7
140
- if int == 0
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 MessageSetSize MessageSet]]
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
- messages_decoder = Decoder.from_string(decoder.bytes)
90
+ messages_raw = decoder.bytes
76
91
  messages = []
77
92
 
78
- magic_byte = messages_decoder.peek(MAGIC_BYTE_OFFSET, MAGIC_BYTE_LENGTH)[0].to_i
79
- if magic_byte == RecordBatch::MAGIC_BYTE
80
- until messages_decoder.eof?
81
- begin
82
- record_batch = RecordBatch.decode(messages_decoder)
83
- messages += record_batch.records
84
- rescue InsufficientDataMessage
85
- if messages.length > 0
86
- break
87
- else
88
- raise
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