ruby-kafka 0.7.0 → 0.7.1.beta1

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