ruby-kafka-temp-fork 0.0.1

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 (144) hide show
  1. checksums.yaml +7 -0
  2. data/.circleci/config.yml +393 -0
  3. data/.github/workflows/stale.yml +19 -0
  4. data/.gitignore +13 -0
  5. data/.readygo +1 -0
  6. data/.rspec +3 -0
  7. data/.rubocop.yml +44 -0
  8. data/.ruby-version +1 -0
  9. data/.yardopts +3 -0
  10. data/CHANGELOG.md +310 -0
  11. data/Gemfile +5 -0
  12. data/ISSUE_TEMPLATE.md +23 -0
  13. data/LICENSE.txt +176 -0
  14. data/Procfile +2 -0
  15. data/README.md +1342 -0
  16. data/Rakefile +8 -0
  17. data/benchmarks/message_encoding.rb +23 -0
  18. data/bin/console +8 -0
  19. data/bin/setup +5 -0
  20. data/docker-compose.yml +39 -0
  21. data/examples/consumer-group.rb +35 -0
  22. data/examples/firehose-consumer.rb +64 -0
  23. data/examples/firehose-producer.rb +54 -0
  24. data/examples/simple-consumer.rb +34 -0
  25. data/examples/simple-producer.rb +42 -0
  26. data/examples/ssl-producer.rb +44 -0
  27. data/lib/kafka.rb +373 -0
  28. data/lib/kafka/async_producer.rb +291 -0
  29. data/lib/kafka/broker.rb +217 -0
  30. data/lib/kafka/broker_info.rb +16 -0
  31. data/lib/kafka/broker_pool.rb +41 -0
  32. data/lib/kafka/broker_uri.rb +43 -0
  33. data/lib/kafka/client.rb +833 -0
  34. data/lib/kafka/cluster.rb +513 -0
  35. data/lib/kafka/compression.rb +45 -0
  36. data/lib/kafka/compressor.rb +86 -0
  37. data/lib/kafka/connection.rb +223 -0
  38. data/lib/kafka/connection_builder.rb +33 -0
  39. data/lib/kafka/consumer.rb +642 -0
  40. data/lib/kafka/consumer_group.rb +231 -0
  41. data/lib/kafka/consumer_group/assignor.rb +63 -0
  42. data/lib/kafka/crc32_hash.rb +15 -0
  43. data/lib/kafka/datadog.rb +420 -0
  44. data/lib/kafka/digest.rb +22 -0
  45. data/lib/kafka/fetch_operation.rb +115 -0
  46. data/lib/kafka/fetched_batch.rb +58 -0
  47. data/lib/kafka/fetched_batch_generator.rb +120 -0
  48. data/lib/kafka/fetched_message.rb +48 -0
  49. data/lib/kafka/fetched_offset_resolver.rb +48 -0
  50. data/lib/kafka/fetcher.rb +224 -0
  51. data/lib/kafka/gzip_codec.rb +34 -0
  52. data/lib/kafka/heartbeat.rb +25 -0
  53. data/lib/kafka/instrumenter.rb +38 -0
  54. data/lib/kafka/interceptors.rb +33 -0
  55. data/lib/kafka/lz4_codec.rb +27 -0
  56. data/lib/kafka/message_buffer.rb +87 -0
  57. data/lib/kafka/murmur2_hash.rb +17 -0
  58. data/lib/kafka/offset_manager.rb +259 -0
  59. data/lib/kafka/partitioner.rb +40 -0
  60. data/lib/kafka/pause.rb +92 -0
  61. data/lib/kafka/pending_message.rb +29 -0
  62. data/lib/kafka/pending_message_queue.rb +41 -0
  63. data/lib/kafka/produce_operation.rb +205 -0
  64. data/lib/kafka/producer.rb +528 -0
  65. data/lib/kafka/prometheus.rb +316 -0
  66. data/lib/kafka/protocol.rb +225 -0
  67. data/lib/kafka/protocol/add_offsets_to_txn_request.rb +29 -0
  68. data/lib/kafka/protocol/add_offsets_to_txn_response.rb +21 -0
  69. data/lib/kafka/protocol/add_partitions_to_txn_request.rb +34 -0
  70. data/lib/kafka/protocol/add_partitions_to_txn_response.rb +47 -0
  71. data/lib/kafka/protocol/alter_configs_request.rb +44 -0
  72. data/lib/kafka/protocol/alter_configs_response.rb +49 -0
  73. data/lib/kafka/protocol/api_versions_request.rb +21 -0
  74. data/lib/kafka/protocol/api_versions_response.rb +53 -0
  75. data/lib/kafka/protocol/consumer_group_protocol.rb +19 -0
  76. data/lib/kafka/protocol/create_partitions_request.rb +42 -0
  77. data/lib/kafka/protocol/create_partitions_response.rb +28 -0
  78. data/lib/kafka/protocol/create_topics_request.rb +45 -0
  79. data/lib/kafka/protocol/create_topics_response.rb +26 -0
  80. data/lib/kafka/protocol/decoder.rb +175 -0
  81. data/lib/kafka/protocol/delete_topics_request.rb +33 -0
  82. data/lib/kafka/protocol/delete_topics_response.rb +26 -0
  83. data/lib/kafka/protocol/describe_configs_request.rb +35 -0
  84. data/lib/kafka/protocol/describe_configs_response.rb +73 -0
  85. data/lib/kafka/protocol/describe_groups_request.rb +27 -0
  86. data/lib/kafka/protocol/describe_groups_response.rb +73 -0
  87. data/lib/kafka/protocol/encoder.rb +184 -0
  88. data/lib/kafka/protocol/end_txn_request.rb +29 -0
  89. data/lib/kafka/protocol/end_txn_response.rb +19 -0
  90. data/lib/kafka/protocol/fetch_request.rb +70 -0
  91. data/lib/kafka/protocol/fetch_response.rb +136 -0
  92. data/lib/kafka/protocol/find_coordinator_request.rb +29 -0
  93. data/lib/kafka/protocol/find_coordinator_response.rb +29 -0
  94. data/lib/kafka/protocol/heartbeat_request.rb +27 -0
  95. data/lib/kafka/protocol/heartbeat_response.rb +17 -0
  96. data/lib/kafka/protocol/init_producer_id_request.rb +26 -0
  97. data/lib/kafka/protocol/init_producer_id_response.rb +27 -0
  98. data/lib/kafka/protocol/join_group_request.rb +47 -0
  99. data/lib/kafka/protocol/join_group_response.rb +41 -0
  100. data/lib/kafka/protocol/leave_group_request.rb +25 -0
  101. data/lib/kafka/protocol/leave_group_response.rb +17 -0
  102. data/lib/kafka/protocol/list_groups_request.rb +23 -0
  103. data/lib/kafka/protocol/list_groups_response.rb +35 -0
  104. data/lib/kafka/protocol/list_offset_request.rb +53 -0
  105. data/lib/kafka/protocol/list_offset_response.rb +89 -0
  106. data/lib/kafka/protocol/member_assignment.rb +42 -0
  107. data/lib/kafka/protocol/message.rb +172 -0
  108. data/lib/kafka/protocol/message_set.rb +55 -0
  109. data/lib/kafka/protocol/metadata_request.rb +31 -0
  110. data/lib/kafka/protocol/metadata_response.rb +185 -0
  111. data/lib/kafka/protocol/offset_commit_request.rb +47 -0
  112. data/lib/kafka/protocol/offset_commit_response.rb +29 -0
  113. data/lib/kafka/protocol/offset_fetch_request.rb +38 -0
  114. data/lib/kafka/protocol/offset_fetch_response.rb +56 -0
  115. data/lib/kafka/protocol/produce_request.rb +94 -0
  116. data/lib/kafka/protocol/produce_response.rb +63 -0
  117. data/lib/kafka/protocol/record.rb +88 -0
  118. data/lib/kafka/protocol/record_batch.rb +223 -0
  119. data/lib/kafka/protocol/request_message.rb +26 -0
  120. data/lib/kafka/protocol/sasl_handshake_request.rb +33 -0
  121. data/lib/kafka/protocol/sasl_handshake_response.rb +28 -0
  122. data/lib/kafka/protocol/sync_group_request.rb +33 -0
  123. data/lib/kafka/protocol/sync_group_response.rb +26 -0
  124. data/lib/kafka/protocol/txn_offset_commit_request.rb +46 -0
  125. data/lib/kafka/protocol/txn_offset_commit_response.rb +47 -0
  126. data/lib/kafka/round_robin_assignment_strategy.rb +52 -0
  127. data/lib/kafka/sasl/gssapi.rb +76 -0
  128. data/lib/kafka/sasl/oauth.rb +64 -0
  129. data/lib/kafka/sasl/plain.rb +39 -0
  130. data/lib/kafka/sasl/scram.rb +180 -0
  131. data/lib/kafka/sasl_authenticator.rb +61 -0
  132. data/lib/kafka/snappy_codec.rb +29 -0
  133. data/lib/kafka/socket_with_timeout.rb +96 -0
  134. data/lib/kafka/ssl_context.rb +66 -0
  135. data/lib/kafka/ssl_socket_with_timeout.rb +188 -0
  136. data/lib/kafka/statsd.rb +296 -0
  137. data/lib/kafka/tagged_logger.rb +77 -0
  138. data/lib/kafka/transaction_manager.rb +306 -0
  139. data/lib/kafka/transaction_state_machine.rb +72 -0
  140. data/lib/kafka/version.rb +5 -0
  141. data/lib/kafka/zstd_codec.rb +27 -0
  142. data/lib/ruby-kafka-temp-fork.rb +5 -0
  143. data/ruby-kafka-temp-fork.gemspec +54 -0
  144. metadata +520 -0
@@ -0,0 +1,47 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Kafka
4
+ module Protocol
5
+ class OffsetCommitRequest
6
+ # This value signals to the broker that its default configuration should be used.
7
+ DEFAULT_RETENTION_TIME = -1
8
+
9
+ def api_key
10
+ OFFSET_COMMIT_API
11
+ end
12
+
13
+ def api_version
14
+ 2
15
+ end
16
+
17
+ def response_class
18
+ OffsetCommitResponse
19
+ end
20
+
21
+ def initialize(group_id:, generation_id:, member_id:, retention_time: DEFAULT_RETENTION_TIME, offsets:)
22
+ @group_id = group_id
23
+ @generation_id = generation_id
24
+ @member_id = member_id
25
+ @retention_time = retention_time
26
+ @offsets = offsets
27
+ end
28
+
29
+ def encode(encoder)
30
+ encoder.write_string(@group_id)
31
+ encoder.write_int32(@generation_id)
32
+ encoder.write_string(@member_id)
33
+ encoder.write_int64(@retention_time)
34
+
35
+ encoder.write_array(@offsets) do |topic, partitions|
36
+ encoder.write_string(topic)
37
+
38
+ encoder.write_array(partitions) do |partition, offset|
39
+ encoder.write_int32(partition)
40
+ encoder.write_int64(offset)
41
+ encoder.write_string(nil) # metadata
42
+ end
43
+ end
44
+ end
45
+ end
46
+ end
47
+ end
@@ -0,0 +1,29 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Kafka
4
+ module Protocol
5
+ class OffsetCommitResponse
6
+ attr_reader :topics
7
+
8
+ def initialize(topics:)
9
+ @topics = topics
10
+ end
11
+
12
+ def self.decode(decoder)
13
+ topics = decoder.array {
14
+ topic = decoder.string
15
+ partitions = decoder.array {
16
+ partition = decoder.int32
17
+ error_code = decoder.int16
18
+
19
+ [partition, error_code]
20
+ }
21
+
22
+ [topic, Hash[partitions]]
23
+ }
24
+
25
+ new(topics: Hash[topics])
26
+ end
27
+ end
28
+ end
29
+ end
@@ -0,0 +1,38 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Kafka
4
+ module Protocol
5
+ class OffsetFetchRequest
6
+ def initialize(group_id:, topics:)
7
+ @group_id = group_id
8
+ @topics = topics
9
+ end
10
+
11
+ def api_key
12
+ OFFSET_FETCH_API
13
+ end
14
+
15
+ # setting topics to nil fetches all offsets for a consumer group
16
+ # and that feature is only available in API version 2+
17
+ def api_version
18
+ @topics.nil? ? 2 : 1
19
+ end
20
+
21
+ def response_class
22
+ OffsetFetchResponse
23
+ end
24
+
25
+ def encode(encoder)
26
+ encoder.write_string(@group_id)
27
+
28
+ encoder.write_array(@topics) do |topic, partitions|
29
+ encoder.write_string(topic)
30
+
31
+ encoder.write_array(partitions) do |partition|
32
+ encoder.write_int32(partition)
33
+ end
34
+ end
35
+ end
36
+ end
37
+ end
38
+ end
@@ -0,0 +1,56 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Kafka
4
+ module Protocol
5
+ class OffsetFetchResponse
6
+ class PartitionOffsetInfo
7
+ attr_reader :offset, :metadata, :error_code
8
+
9
+ def initialize(offset:, metadata:, error_code:)
10
+ @offset = offset
11
+ @metadata = metadata
12
+ @error_code = error_code
13
+ end
14
+ end
15
+
16
+ attr_reader :topics
17
+
18
+ def initialize(topics:)
19
+ @topics = topics
20
+ end
21
+
22
+ def offset_for(topic, partition)
23
+ offset_info = topics.fetch(topic).fetch(partition, nil)
24
+
25
+ if offset_info
26
+ Protocol.handle_error(offset_info.error_code)
27
+ offset_info.offset
28
+ else
29
+ -1
30
+ end
31
+ end
32
+
33
+ def self.decode(decoder)
34
+ topics = decoder.array {
35
+ topic = decoder.string
36
+
37
+ partitions = decoder.array {
38
+ partition = decoder.int32
39
+
40
+ info = PartitionOffsetInfo.new(
41
+ offset: decoder.int64,
42
+ metadata: decoder.string,
43
+ error_code: decoder.int16,
44
+ )
45
+
46
+ [partition, info]
47
+ }
48
+
49
+ [topic, Hash[partitions]]
50
+ }
51
+
52
+ new(topics: Hash[topics])
53
+ end
54
+ end
55
+ end
56
+ end
@@ -0,0 +1,94 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "stringio"
4
+
5
+ module Kafka
6
+ module Protocol
7
+
8
+ # A produce request sends a message set to the server.
9
+ #
10
+ # ## API Specification
11
+ #
12
+ # ProduceRequest => RequiredAcks Timeout [TopicName [Partition MessageSetSize MessageSet]]
13
+ # RequiredAcks => int16
14
+ # Timeout => int32
15
+ # Partition => int32
16
+ # MessageSetSize => int32
17
+ #
18
+ # MessageSet => [Offset MessageSize Message]
19
+ # Offset => int64
20
+ # MessageSize => int32
21
+ #
22
+ # Message => Crc MagicByte Attributes Key Value
23
+ # Crc => int32
24
+ # MagicByte => int8
25
+ # Attributes => int8
26
+ # Key => bytes
27
+ # Value => bytes
28
+ #
29
+ class ProduceRequest
30
+ API_MIN_VERSION = 3
31
+
32
+ attr_reader :transactional_id, :required_acks, :timeout, :messages_for_topics, :compressor
33
+
34
+ # @param required_acks [Integer]
35
+ # @param timeout [Integer]
36
+ # @param messages_for_topics [Hash]
37
+ def initialize(transactional_id: nil, required_acks:, timeout:, messages_for_topics:, compressor: nil)
38
+ @transactional_id = transactional_id
39
+ @required_acks = required_acks
40
+ @timeout = timeout
41
+ @messages_for_topics = messages_for_topics
42
+ @compressor = compressor
43
+ end
44
+
45
+ def api_key
46
+ PRODUCE_API
47
+ end
48
+
49
+ def api_version
50
+ compressor.codec.nil? ? API_MIN_VERSION : [compressor.codec.produce_api_min_version, API_MIN_VERSION].max
51
+ end
52
+
53
+ def response_class
54
+ requires_acks? ? Protocol::ProduceResponse : nil
55
+ end
56
+
57
+ # Whether this request requires any acknowledgements at all. If no acknowledgements
58
+ # are required, the server will not send back a response at all.
59
+ #
60
+ # @return [Boolean] true if acknowledgements are required, false otherwise.
61
+ def requires_acks?
62
+ @required_acks != 0
63
+ end
64
+
65
+ def encode(encoder)
66
+ encoder.write_string(@transactional_id)
67
+ encoder.write_int16(@required_acks)
68
+ encoder.write_int32(@timeout)
69
+
70
+ encoder.write_array(@messages_for_topics) do |topic, messages_for_partition|
71
+ encoder.write_string(topic)
72
+
73
+ encoder.write_array(messages_for_partition) do |partition, record_batch|
74
+ encoder.write_int32(partition)
75
+
76
+ record_batch.fulfill_relative_data
77
+ encoded_record_batch = compress(record_batch)
78
+ encoder.write_bytes(encoded_record_batch)
79
+ end
80
+ end
81
+ end
82
+
83
+ private
84
+
85
+ def compress(record_batch)
86
+ if @compressor.nil?
87
+ Protocol::Encoder.encode_with(record_batch)
88
+ else
89
+ @compressor.compress(record_batch)
90
+ end
91
+ end
92
+ end
93
+ end
94
+ end
@@ -0,0 +1,63 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Kafka
4
+ module Protocol
5
+ class ProduceResponse
6
+ class TopicInfo
7
+ attr_reader :topic, :partitions
8
+
9
+ def initialize(topic:, partitions:)
10
+ @topic = topic
11
+ @partitions = partitions
12
+ end
13
+ end
14
+
15
+ class PartitionInfo
16
+ attr_reader :partition, :error_code, :offset, :timestamp
17
+
18
+ def initialize(partition:, error_code:, offset:, timestamp:)
19
+ @partition = partition
20
+ @error_code = error_code
21
+ @offset = offset
22
+ @timestamp = timestamp
23
+ end
24
+ end
25
+
26
+ attr_reader :topics, :throttle_time_ms
27
+
28
+ def initialize(topics: [], throttle_time_ms: 0)
29
+ @topics = topics
30
+ @throttle_time_ms = throttle_time_ms
31
+ end
32
+
33
+ def each_partition
34
+ @topics.each do |topic_info|
35
+ topic_info.partitions.each do |partition_info|
36
+ yield topic_info, partition_info
37
+ end
38
+ end
39
+ end
40
+
41
+ def self.decode(decoder)
42
+ topics = decoder.array do
43
+ topic = decoder.string
44
+
45
+ partitions = decoder.array do
46
+ PartitionInfo.new(
47
+ partition: decoder.int32,
48
+ error_code: decoder.int16,
49
+ offset: decoder.int64,
50
+ timestamp: Time.at(decoder.int64 / 1000.0),
51
+ )
52
+ end
53
+
54
+ TopicInfo.new(topic: topic, partitions: partitions)
55
+ end
56
+
57
+ throttle_time_ms = decoder.int32
58
+
59
+ new(topics: topics, throttle_time_ms: throttle_time_ms)
60
+ end
61
+ end
62
+ end
63
+ end
@@ -0,0 +1,88 @@
1
+ module Kafka
2
+ module Protocol
3
+ class Record
4
+ attr_reader :key, :value, :headers, :attributes, :bytesize
5
+ attr_accessor :offset_delta, :timestamp_delta, :offset, :create_time, :is_control_record
6
+
7
+ def initialize(
8
+ key: nil,
9
+ value:,
10
+ headers: {},
11
+ attributes: 0,
12
+ offset_delta: 0,
13
+ offset: 0,
14
+ timestamp_delta: 0,
15
+ create_time: Time.now,
16
+ is_control_record: false
17
+ )
18
+ @key = key
19
+ @value = value
20
+ @headers = headers
21
+ @attributes = attributes
22
+
23
+ @offset_delta = offset_delta
24
+ @offset = offset
25
+ @timestamp_delta = timestamp_delta
26
+ @create_time = create_time
27
+ @is_control_record = is_control_record
28
+
29
+ @bytesize = @key.to_s.bytesize + @value.to_s.bytesize
30
+ end
31
+
32
+ def encode(encoder)
33
+ record_buffer = StringIO.new
34
+
35
+ record_encoder = Encoder.new(record_buffer)
36
+
37
+ record_encoder.write_int8(@attributes)
38
+ record_encoder.write_varint(@timestamp_delta)
39
+ record_encoder.write_varint(@offset_delta)
40
+
41
+ record_encoder.write_varint_string(@key)
42
+ record_encoder.write_varint_bytes(@value)
43
+
44
+ record_encoder.write_varint_array(@headers.to_a) do |header_key, header_value|
45
+ record_encoder.write_varint_string(header_key.to_s)
46
+ record_encoder.write_varint_bytes(header_value.to_s)
47
+ end
48
+
49
+ encoder.write_varint_bytes(record_buffer.string)
50
+ end
51
+
52
+ def ==(other)
53
+ offset_delta == other.offset_delta &&
54
+ timestamp_delta == other.timestamp_delta &&
55
+ offset == other.offset &&
56
+ is_control_record == other.is_control_record
57
+ end
58
+
59
+ def self.decode(decoder)
60
+ record_decoder = Decoder.from_string(decoder.varint_bytes)
61
+
62
+ attributes = record_decoder.int8
63
+ timestamp_delta = record_decoder.varint
64
+ offset_delta = record_decoder.varint
65
+
66
+ key = record_decoder.varint_string
67
+ value = record_decoder.varint_bytes
68
+
69
+ headers = {}
70
+ record_decoder.varint_array do
71
+ header_key = record_decoder.varint_string
72
+ header_value = record_decoder.varint_bytes
73
+
74
+ headers[header_key] = header_value
75
+ end
76
+
77
+ new(
78
+ key: key,
79
+ value: value,
80
+ headers: headers,
81
+ attributes: attributes,
82
+ offset_delta: offset_delta,
83
+ timestamp_delta: timestamp_delta
84
+ )
85
+ end
86
+ end
87
+ end
88
+ end
@@ -0,0 +1,223 @@
1
+ require 'bigdecimal'
2
+ require 'digest/crc32'
3
+ require 'kafka/protocol/record'
4
+
5
+ module Kafka
6
+ module Protocol
7
+ class RecordBatch
8
+ MAGIC_BYTE = 2
9
+ # The size of metadata before the real record data
10
+ RECORD_BATCH_OVERHEAD = 49
11
+ # Masks to extract information from attributes
12
+ CODEC_ID_MASK = 0b00000111
13
+ IN_TRANSACTION_MASK = 0b00010000
14
+ IS_CONTROL_BATCH_MASK = 0b00100000
15
+ TIMESTAMP_TYPE_MASK = 0b001000
16
+
17
+ attr_reader :records, :first_offset, :first_timestamp, :partition_leader_epoch, :in_transaction, :is_control_batch, :last_offset_delta, :max_timestamp, :producer_id, :producer_epoch, :first_sequence
18
+
19
+ attr_accessor :codec_id
20
+
21
+ def initialize(
22
+ records: [],
23
+ first_offset: 0,
24
+ first_timestamp: Time.now,
25
+ partition_leader_epoch: 0,
26
+ codec_id: 0,
27
+ in_transaction: false,
28
+ is_control_batch: false,
29
+ last_offset_delta: 0,
30
+ producer_id: -1,
31
+ producer_epoch: 0,
32
+ first_sequence: 0,
33
+ max_timestamp: Time.now
34
+ )
35
+ @records = Array(records)
36
+ @first_offset = first_offset
37
+ @first_timestamp = first_timestamp
38
+ @codec_id = codec_id
39
+
40
+ # Records verification
41
+ @last_offset_delta = last_offset_delta
42
+ @max_timestamp = max_timestamp
43
+
44
+ # Transaction information
45
+ @producer_id = producer_id
46
+ @producer_epoch = producer_epoch
47
+
48
+ @first_sequence = first_sequence
49
+ @partition_leader_epoch = partition_leader_epoch
50
+ @in_transaction = in_transaction
51
+ @is_control_batch = is_control_batch
52
+
53
+ mark_control_record
54
+ end
55
+
56
+ def size
57
+ @records.size
58
+ end
59
+
60
+ def last_offset
61
+ @first_offset + @last_offset_delta
62
+ end
63
+
64
+ def attributes
65
+ 0x0000 | @codec_id |
66
+ (@in_transaction ? IN_TRANSACTION_MASK : 0x0) |
67
+ (@is_control_batch ? IS_CONTROL_BATCH_MASK : 0x0)
68
+ end
69
+
70
+ def encode(encoder)
71
+ encoder.write_int64(@first_offset)
72
+
73
+ record_batch_buffer = StringIO.new
74
+ record_batch_encoder = Encoder.new(record_batch_buffer)
75
+
76
+ record_batch_encoder.write_int32(@partition_leader_epoch)
77
+ record_batch_encoder.write_int8(MAGIC_BYTE)
78
+
79
+ body = encode_record_batch_body
80
+ crc = ::Digest::CRC32c.checksum(body)
81
+
82
+ record_batch_encoder.write_int32(crc)
83
+ record_batch_encoder.write(body)
84
+
85
+ encoder.write_bytes(record_batch_buffer.string)
86
+ end
87
+
88
+ def encode_record_batch_body
89
+ buffer = StringIO.new
90
+ encoder = Encoder.new(buffer)
91
+
92
+ encoder.write_int16(attributes)
93
+ encoder.write_int32(@last_offset_delta)
94
+ encoder.write_int64((@first_timestamp.to_f * 1000).to_i)
95
+ encoder.write_int64((@max_timestamp.to_f * 1000).to_i)
96
+
97
+ encoder.write_int64(@producer_id)
98
+ encoder.write_int16(@producer_epoch)
99
+ encoder.write_int32(@first_sequence)
100
+
101
+ encoder.write_int32(@records.length)
102
+
103
+ records_array = encode_record_array
104
+ if compressed?
105
+ codec = Compression.find_codec_by_id(@codec_id)
106
+ records_array = codec.compress(records_array)
107
+ end
108
+ encoder.write(records_array)
109
+
110
+ buffer.string
111
+ end
112
+
113
+ def encode_record_array
114
+ buffer = StringIO.new
115
+ encoder = Encoder.new(buffer)
116
+ @records.each do |record|
117
+ record.encode(encoder)
118
+ end
119
+ buffer.string
120
+ end
121
+
122
+ def compressed?
123
+ @codec_id != 0
124
+ end
125
+
126
+ def fulfill_relative_data
127
+ first_record = records.min_by { |record| record.create_time }
128
+ @first_timestamp = first_record.nil? ? Time.now : first_record.create_time
129
+
130
+ last_record = records.max_by { |record| record.create_time }
131
+ @max_timestamp = last_record.nil? ? Time.now : last_record.create_time
132
+
133
+ records.each_with_index do |record, index|
134
+ record.offset_delta = index
135
+ record.timestamp_delta = ((record.create_time - first_timestamp) * 1000).to_i
136
+ end
137
+ @last_offset_delta = records.length - 1
138
+ end
139
+
140
+ def ==(other)
141
+ records == other.records &&
142
+ first_offset == other.first_offset &&
143
+ partition_leader_epoch == other.partition_leader_epoch &&
144
+ in_transaction == other.in_transaction &&
145
+ is_control_batch == other.is_control_batch &&
146
+ last_offset_delta == other.last_offset_delta &&
147
+ producer_id == other.producer_id &&
148
+ producer_epoch == other.producer_epoch &&
149
+ first_sequence == other.first_sequence
150
+ end
151
+
152
+ def self.decode(decoder)
153
+ first_offset = decoder.int64
154
+
155
+ record_batch_raw = decoder.bytes
156
+ record_batch_decoder = Decoder.from_string(record_batch_raw)
157
+
158
+ partition_leader_epoch = record_batch_decoder.int32
159
+ # Currently, the magic byte is used to distingush legacy MessageSet and
160
+ # RecordBatch. Therefore, we don't care about magic byte here yet.
161
+ _magic_byte = record_batch_decoder.int8
162
+ _crc = record_batch_decoder.int32
163
+
164
+ attributes = record_batch_decoder.int16
165
+ codec_id = attributes & CODEC_ID_MASK
166
+ in_transaction = (attributes & IN_TRANSACTION_MASK) > 0
167
+ is_control_batch = (attributes & IS_CONTROL_BATCH_MASK) > 0
168
+ log_append_time = (attributes & TIMESTAMP_TYPE_MASK) != 0
169
+
170
+ last_offset_delta = record_batch_decoder.int32
171
+ first_timestamp = Time.at(record_batch_decoder.int64 / BigDecimal(1000))
172
+ max_timestamp = Time.at(record_batch_decoder.int64 / BigDecimal(1000))
173
+
174
+ producer_id = record_batch_decoder.int64
175
+ producer_epoch = record_batch_decoder.int16
176
+ first_sequence = record_batch_decoder.int32
177
+
178
+ records_array_length = record_batch_decoder.int32
179
+ records_array_raw = record_batch_decoder.read(
180
+ record_batch_raw.size - RECORD_BATCH_OVERHEAD
181
+ )
182
+ if codec_id != 0
183
+ codec = Compression.find_codec_by_id(codec_id)
184
+ records_array_raw = codec.decompress(records_array_raw)
185
+ end
186
+
187
+ records_array_decoder = Decoder.from_string(records_array_raw)
188
+ records_array = []
189
+ until records_array_decoder.eof?
190
+ record = Record.decode(records_array_decoder)
191
+ record.offset = first_offset + record.offset_delta
192
+ record.create_time = log_append_time && max_timestamp ? max_timestamp : first_timestamp + record.timestamp_delta / BigDecimal(1000)
193
+ records_array << record
194
+ end
195
+
196
+ raise InsufficientDataMessage if records_array.length != records_array_length
197
+
198
+ new(
199
+ records: records_array,
200
+ first_offset: first_offset,
201
+ first_timestamp: first_timestamp,
202
+ partition_leader_epoch: partition_leader_epoch,
203
+ in_transaction: in_transaction,
204
+ is_control_batch: is_control_batch,
205
+ last_offset_delta: last_offset_delta,
206
+ producer_id: producer_id,
207
+ producer_epoch: producer_epoch,
208
+ first_sequence: first_sequence,
209
+ max_timestamp: max_timestamp
210
+ )
211
+ rescue EOFError
212
+ raise InsufficientDataMessage, 'Partial trailing record detected!'
213
+ end
214
+
215
+ def mark_control_record
216
+ if is_control_batch
217
+ record = @records.first
218
+ record.is_control_record = true unless record.nil?
219
+ end
220
+ end
221
+ end
222
+ end
223
+ end