ruby-kafka 0.7.6 → 1.1.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (44) hide show
  1. checksums.yaml +4 -4
  2. data/.circleci/config.yml +135 -3
  3. data/.github/workflows/stale.yml +19 -0
  4. data/CHANGELOG.md +34 -0
  5. data/README.md +27 -0
  6. data/lib/kafka/async_producer.rb +3 -0
  7. data/lib/kafka/broker.rb +12 -0
  8. data/lib/kafka/client.rb +53 -4
  9. data/lib/kafka/cluster.rb +52 -0
  10. data/lib/kafka/compression.rb +13 -11
  11. data/lib/kafka/compressor.rb +1 -0
  12. data/lib/kafka/connection.rb +3 -0
  13. data/lib/kafka/consumer.rb +56 -11
  14. data/lib/kafka/consumer_group.rb +10 -1
  15. data/lib/kafka/datadog.rb +18 -11
  16. data/lib/kafka/fetched_batch.rb +5 -1
  17. data/lib/kafka/fetched_batch_generator.rb +4 -1
  18. data/lib/kafka/fetched_message.rb +1 -0
  19. data/lib/kafka/fetcher.rb +5 -2
  20. data/lib/kafka/gzip_codec.rb +4 -0
  21. data/lib/kafka/lz4_codec.rb +4 -0
  22. data/lib/kafka/offset_manager.rb +12 -1
  23. data/lib/kafka/producer.rb +20 -1
  24. data/lib/kafka/prometheus.rb +316 -0
  25. data/lib/kafka/protocol.rb +8 -0
  26. data/lib/kafka/protocol/add_offsets_to_txn_request.rb +29 -0
  27. data/lib/kafka/protocol/add_offsets_to_txn_response.rb +19 -0
  28. data/lib/kafka/protocol/join_group_request.rb +8 -2
  29. data/lib/kafka/protocol/metadata_response.rb +1 -1
  30. data/lib/kafka/protocol/offset_fetch_request.rb +3 -1
  31. data/lib/kafka/protocol/produce_request.rb +3 -1
  32. data/lib/kafka/protocol/record_batch.rb +5 -4
  33. data/lib/kafka/protocol/txn_offset_commit_request.rb +46 -0
  34. data/lib/kafka/protocol/txn_offset_commit_response.rb +18 -0
  35. data/lib/kafka/sasl/scram.rb +15 -12
  36. data/lib/kafka/snappy_codec.rb +4 -0
  37. data/lib/kafka/ssl_context.rb +5 -1
  38. data/lib/kafka/ssl_socket_with_timeout.rb +1 -0
  39. data/lib/kafka/tagged_logger.rb +25 -20
  40. data/lib/kafka/transaction_manager.rb +25 -0
  41. data/lib/kafka/version.rb +1 -1
  42. data/lib/kafka/zstd_codec.rb +27 -0
  43. data/ruby-kafka.gemspec +5 -3
  44. metadata +48 -7
@@ -0,0 +1,29 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Kafka
4
+ module Protocol
5
+ class AddOffsetsToTxnRequest
6
+ def initialize(transactional_id: nil, producer_id:, producer_epoch:, group_id:)
7
+ @transactional_id = transactional_id
8
+ @producer_id = producer_id
9
+ @producer_epoch = producer_epoch
10
+ @group_id = group_id
11
+ end
12
+
13
+ def api_key
14
+ ADD_OFFSETS_TO_TXN_API
15
+ end
16
+
17
+ def response_class
18
+ AddOffsetsToTxnResponse
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_string(@group_id)
26
+ end
27
+ end
28
+ end
29
+ end
@@ -0,0 +1,19 @@
1
+ module Kafka
2
+ module Protocol
3
+ class AddOffsetsToTxnResponse
4
+
5
+ attr_reader :error_code
6
+
7
+ def initialize(error_code:)
8
+ @error_code = error_code
9
+ end
10
+
11
+ def self.decode(decoder)
12
+ _throttle_time_ms = decoder.int32
13
+ error_code = decoder.int16
14
+ new(error_code: error_code)
15
+ end
16
+
17
+ end
18
+ end
19
+ end
@@ -7,13 +7,14 @@ module Kafka
7
7
  class JoinGroupRequest
8
8
  PROTOCOL_TYPE = "consumer"
9
9
 
10
- def initialize(group_id:, session_timeout:, member_id:, topics: [])
10
+ def initialize(group_id:, session_timeout:, rebalance_timeout:, member_id:, topics: [])
11
11
  @group_id = group_id
12
12
  @session_timeout = session_timeout * 1000 # Kafka wants ms.
13
+ @rebalance_timeout = rebalance_timeout * 1000 # Kafka wants ms.
13
14
  @member_id = member_id || ""
14
15
  @protocol_type = PROTOCOL_TYPE
15
16
  @group_protocols = {
16
- "standard" => ConsumerGroupProtocol.new(topics: ["test-messages"]),
17
+ "roundrobin" => ConsumerGroupProtocol.new(topics: topics),
17
18
  }
18
19
  end
19
20
 
@@ -21,6 +22,10 @@ module Kafka
21
22
  JOIN_GROUP_API
22
23
  end
23
24
 
25
+ def api_version
26
+ 1
27
+ end
28
+
24
29
  def response_class
25
30
  JoinGroupResponse
26
31
  end
@@ -28,6 +33,7 @@ module Kafka
28
33
  def encode(encoder)
29
34
  encoder.write_string(@group_id)
30
35
  encoder.write_int32(@session_timeout)
36
+ encoder.write_int32(@rebalance_timeout)
31
37
  encoder.write_string(@member_id)
32
38
  encoder.write_string(@protocol_type)
33
39
 
@@ -34,7 +34,7 @@ module Kafka
34
34
  #
35
35
  class MetadataResponse
36
36
  class PartitionMetadata
37
- attr_reader :partition_id, :leader
37
+ attr_reader :partition_id, :leader, :replicas
38
38
 
39
39
  attr_reader :partition_error_code
40
40
 
@@ -12,8 +12,10 @@ module Kafka
12
12
  OFFSET_FETCH_API
13
13
  end
14
14
 
15
+ # setting topics to nil fetches all offsets for a consumer group
16
+ # and that feature is only available in API version 2+
15
17
  def api_version
16
- 1
18
+ @topics.nil? ? 2 : 1
17
19
  end
18
20
 
19
21
  def response_class
@@ -27,6 +27,8 @@ module Kafka
27
27
  # Value => bytes
28
28
  #
29
29
  class ProduceRequest
30
+ API_MIN_VERSION = 3
31
+
30
32
  attr_reader :transactional_id, :required_acks, :timeout, :messages_for_topics, :compressor
31
33
 
32
34
  # @param required_acks [Integer]
@@ -45,7 +47,7 @@ module Kafka
45
47
  end
46
48
 
47
49
  def api_version
48
- 3
50
+ compressor.codec.nil? ? API_MIN_VERSION : [compressor.codec.produce_api_min_version, API_MIN_VERSION].max
49
51
  end
50
52
 
51
53
  def response_class
@@ -1,3 +1,4 @@
1
+ require 'bigdecimal'
1
2
  require 'digest/crc32'
2
3
  require 'kafka/protocol/record'
3
4
 
@@ -131,7 +132,7 @@ module Kafka
131
132
 
132
133
  records.each_with_index do |record, index|
133
134
  record.offset_delta = index
134
- record.timestamp_delta = (record.create_time - first_timestamp).to_i
135
+ record.timestamp_delta = ((record.create_time - first_timestamp) * 1000).to_i
135
136
  end
136
137
  @last_offset_delta = records.length - 1
137
138
  end
@@ -167,8 +168,8 @@ module Kafka
167
168
  log_append_time = (attributes & TIMESTAMP_TYPE_MASK) != 0
168
169
 
169
170
  last_offset_delta = record_batch_decoder.int32
170
- first_timestamp = Time.at(record_batch_decoder.int64 / 1000)
171
- max_timestamp = Time.at(record_batch_decoder.int64 / 1000)
171
+ first_timestamp = Time.at(record_batch_decoder.int64 / BigDecimal(1000))
172
+ max_timestamp = Time.at(record_batch_decoder.int64 / BigDecimal(1000))
172
173
 
173
174
  producer_id = record_batch_decoder.int64
174
175
  producer_epoch = record_batch_decoder.int16
@@ -188,7 +189,7 @@ module Kafka
188
189
  until records_array_decoder.eof?
189
190
  record = Record.decode(records_array_decoder)
190
191
  record.offset = first_offset + record.offset_delta
191
- record.create_time = log_append_time && max_timestamp ? max_timestamp : first_timestamp + record.timestamp_delta
192
+ record.create_time = log_append_time && max_timestamp ? max_timestamp : first_timestamp + record.timestamp_delta / BigDecimal(1000)
192
193
  records_array << record
193
194
  end
194
195
 
@@ -0,0 +1,46 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Kafka
4
+ module Protocol
5
+ class TxnOffsetCommitRequest
6
+
7
+ def api_key
8
+ TXN_OFFSET_COMMIT_API
9
+ end
10
+
11
+ def api_version
12
+ 2
13
+ end
14
+
15
+ def response_class
16
+ TxnOffsetCommitResponse
17
+ end
18
+
19
+ def initialize(transactional_id:, group_id:, producer_id:, producer_epoch:, offsets:)
20
+ @transactional_id = transactional_id
21
+ @producer_id = producer_id
22
+ @producer_epoch = producer_epoch
23
+ @group_id = group_id
24
+ @offsets = offsets
25
+ end
26
+
27
+ def encode(encoder)
28
+ encoder.write_string(@transactional_id.to_s)
29
+ encoder.write_string(@group_id)
30
+ encoder.write_int64(@producer_id)
31
+ encoder.write_int16(@producer_epoch)
32
+
33
+ encoder.write_array(@offsets) do |topic, partitions|
34
+ encoder.write_string(topic)
35
+ encoder.write_array(partitions) do |partition, offset|
36
+ encoder.write_int32(partition)
37
+ encoder.write_int64(offset[:offset])
38
+ encoder.write_string(nil) # metadata
39
+ encoder.write_int32(offset[:leader_epoch])
40
+ end
41
+ end
42
+ end
43
+
44
+ end
45
+ end
46
+ end
@@ -0,0 +1,18 @@
1
+ module Kafka
2
+ module Protocol
3
+ class TxnOffsetCommitResponse
4
+
5
+ attr_reader :error_code
6
+
7
+ def initialize(error_code:)
8
+ @error_code = error_code
9
+ end
10
+
11
+ def self.decode(decoder)
12
+ _throttle_time_ms = decoder.int32
13
+ error_code = decoder.int16
14
+ new(error_code: error_code)
15
+ end
16
+ end
17
+ end
18
+ end
@@ -12,6 +12,7 @@ module Kafka
12
12
  }.freeze
13
13
 
14
14
  def initialize(username:, password:, mechanism: 'sha256', logger:)
15
+ @semaphore = Mutex.new
15
16
  @username = username
16
17
  @password = password
17
18
  @logger = TaggedLogger.new(logger)
@@ -35,22 +36,24 @@ module Kafka
35
36
  @logger.debug "Authenticating #{@username} with SASL #{@mechanism}"
36
37
 
37
38
  begin
38
- msg = first_message
39
- @logger.debug "Sending first client SASL SCRAM message: #{msg}"
40
- encoder.write_bytes(msg)
39
+ @semaphore.synchronize do
40
+ msg = first_message
41
+ @logger.debug "Sending first client SASL SCRAM message: #{msg}"
42
+ encoder.write_bytes(msg)
41
43
 
42
- @server_first_message = decoder.bytes
43
- @logger.debug "Received first server SASL SCRAM message: #{@server_first_message}"
44
+ @server_first_message = decoder.bytes
45
+ @logger.debug "Received first server SASL SCRAM message: #{@server_first_message}"
44
46
 
45
- msg = final_message
46
- @logger.debug "Sending final client SASL SCRAM message: #{msg}"
47
- encoder.write_bytes(msg)
47
+ msg = final_message
48
+ @logger.debug "Sending final client SASL SCRAM message: #{msg}"
49
+ encoder.write_bytes(msg)
48
50
 
49
- response = parse_response(decoder.bytes)
50
- @logger.debug "Received last server SASL SCRAM message: #{response}"
51
+ response = parse_response(decoder.bytes)
52
+ @logger.debug "Received last server SASL SCRAM message: #{response}"
51
53
 
52
- raise FailedScramAuthentication, response['e'] if response['e']
53
- raise FailedScramAuthentication, "Invalid server signature" if response['v'] != server_signature
54
+ raise FailedScramAuthentication, response['e'] if response['e']
55
+ raise FailedScramAuthentication, "Invalid server signature" if response['v'] != server_signature
56
+ end
54
57
  rescue EOFError => e
55
58
  raise FailedScramAuthentication, e.message
56
59
  end
@@ -6,6 +6,10 @@ module Kafka
6
6
  2
7
7
  end
8
8
 
9
+ def produce_api_min_version
10
+ 0
11
+ end
12
+
9
13
  def load
10
14
  require "snappy"
11
15
  rescue LoadError
@@ -6,7 +6,7 @@ module Kafka
6
6
  module SslContext
7
7
  CLIENT_CERT_DELIMITER = "\n-----END CERTIFICATE-----\n"
8
8
 
9
- def self.build(ca_cert_file_path: nil, ca_cert: nil, client_cert: nil, client_cert_key: nil, client_cert_key_password: nil, client_cert_chain: nil, ca_certs_from_system: nil)
9
+ def self.build(ca_cert_file_path: nil, ca_cert: nil, client_cert: nil, client_cert_key: nil, client_cert_key_password: nil, client_cert_chain: nil, ca_certs_from_system: nil, verify_hostname: true)
10
10
  return nil unless ca_cert_file_path || ca_cert || client_cert || client_cert_key || client_cert_key_password || client_cert_chain || ca_certs_from_system
11
11
 
12
12
  ssl_context = OpenSSL::SSL::SSLContext.new
@@ -56,6 +56,10 @@ module Kafka
56
56
  ssl_context.cert_store = store
57
57
  end
58
58
 
59
+ ssl_context.verify_mode = OpenSSL::SSL::VERIFY_PEER
60
+ # Verify certificate hostname if supported (ruby >= 2.4.0)
61
+ ssl_context.verify_hostname = verify_hostname if ssl_context.respond_to?(:verify_hostname=)
62
+
59
63
  ssl_context
60
64
  end
61
65
  end
@@ -57,6 +57,7 @@ module Kafka
57
57
 
58
58
  # once that's connected, we can start initiating the ssl socket
59
59
  @ssl_socket = OpenSSL::SSL::SSLSocket.new(@tcp_socket, ssl_context)
60
+ @ssl_socket.hostname = host
60
61
 
61
62
  begin
62
63
  # Initiate the socket connection in the background. If it doesn't fail
@@ -1,13 +1,20 @@
1
- require 'forwardable'
2
-
3
1
  # Basic implementation of a tagged logger that matches the API of
4
2
  # ActiveSupport::TaggedLogging.
5
3
 
4
+ require 'delegate'
5
+ require 'logger'
6
+
6
7
  module Kafka
7
- module TaggedFormatter
8
+ class TaggedLogger < SimpleDelegator
8
9
 
9
- def call(severity, timestamp, progname, msg)
10
- super(severity, timestamp, progname, "#{tags_text}#{msg}")
10
+ %i(debug info warn error).each do |method|
11
+ define_method method do |msg_or_progname, &block|
12
+ if block_given?
13
+ super(msg_or_progname, &block)
14
+ else
15
+ super("#{tags_text}#{msg_or_progname}")
16
+ end
17
+ end
11
18
  end
12
19
 
13
20
  def tagged(*tags)
@@ -44,23 +51,21 @@ module Kafka
44
51
  end
45
52
  end
46
53
 
47
- end
48
-
49
- module TaggedLogger
50
- extend Forwardable
51
- delegate [:push_tags, :pop_tags, :clear_tags!] => :formatter
52
-
53
- def self.new(logger)
54
- logger ||= Logger.new(nil)
55
- return logger if logger.respond_to?(:push_tags) # already included
56
- # Ensure we set a default formatter so we aren't extending nil!
57
- logger.formatter ||= Logger::Formatter.new
58
- logger.formatter.extend TaggedFormatter
59
- logger.extend(self)
54
+ def self.new(logger_or_stream = nil)
55
+ # don't keep wrapping the same logger over and over again
56
+ return logger_or_stream if logger_or_stream.is_a?(TaggedLogger)
57
+ super
60
58
  end
61
59
 
62
- def tagged(*tags)
63
- formatter.tagged(*tags) { yield self }
60
+ def initialize(logger_or_stream = nil)
61
+ logger = if %w(info debug warn error).all? { |s| logger_or_stream.respond_to?(s) }
62
+ logger_or_stream
63
+ elsif logger_or_stream
64
+ ::Logger.new(logger_or_stream)
65
+ else
66
+ ::Logger.new(nil)
67
+ end
68
+ super(logger)
64
69
  end
65
70
 
66
71
  def flush
@@ -217,6 +217,31 @@ module Kafka
217
217
  raise
218
218
  end
219
219
 
220
+ def send_offsets_to_txn(offsets:, group_id:)
221
+ force_transactional!
222
+
223
+ unless @transaction_state.in_transaction?
224
+ raise 'Transaction is not valid to send offsets'
225
+ end
226
+
227
+ add_response = transaction_coordinator.add_offsets_to_txn(
228
+ transactional_id: @transactional_id,
229
+ producer_id: @producer_id,
230
+ producer_epoch: @producer_epoch,
231
+ group_id: group_id
232
+ )
233
+ Protocol.handle_error(add_response.error_code)
234
+
235
+ send_response = transaction_coordinator.txn_offset_commit(
236
+ transactional_id: @transactional_id,
237
+ group_id: group_id,
238
+ producer_id: @producer_id,
239
+ producer_epoch: @producer_epoch,
240
+ offsets: offsets
241
+ )
242
+ Protocol.handle_error(send_response.error_code)
243
+ end
244
+
220
245
  def in_transaction?
221
246
  @transaction_state.in_transaction?
222
247
  end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Kafka
4
- VERSION = "0.7.6"
4
+ VERSION = "1.1.0"
5
5
  end
@@ -0,0 +1,27 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Kafka
4
+ class ZstdCodec
5
+ def codec_id
6
+ 4
7
+ end
8
+
9
+ def produce_api_min_version
10
+ 7
11
+ end
12
+
13
+ def load
14
+ require "zstd-ruby"
15
+ rescue LoadError
16
+ raise LoadError, "using zstd compression requires adding a dependency on the `zstd-ruby` gem to your Gemfile."
17
+ end
18
+
19
+ def compress(data)
20
+ Zstd.compress(data)
21
+ end
22
+
23
+ def decompress(data)
24
+ Zstd.decompress(data)
25
+ end
26
+ end
27
+ end
@@ -18,7 +18,7 @@ Gem::Specification.new do |spec|
18
18
  DESC
19
19
 
20
20
  spec.homepage = "https://github.com/zendesk/ruby-kafka"
21
- spec.license = "Apache License Version 2.0"
21
+ spec.license = "Apache-2.0"
22
22
 
23
23
  spec.required_ruby_version = '>= 2.1.0'
24
24
 
@@ -36,13 +36,15 @@ Gem::Specification.new do |spec|
36
36
  spec.add_development_dependency "dotenv"
37
37
  spec.add_development_dependency "docker-api"
38
38
  spec.add_development_dependency "rspec-benchmark"
39
- spec.add_development_dependency "activesupport"
39
+ spec.add_development_dependency "activesupport", ">= 4.0", "< 6.1"
40
40
  spec.add_development_dependency "snappy"
41
41
  spec.add_development_dependency "extlz4"
42
+ spec.add_development_dependency "zstd-ruby"
42
43
  spec.add_development_dependency "colored"
43
44
  spec.add_development_dependency "rspec_junit_formatter", "0.2.2"
44
- spec.add_development_dependency "dogstatsd-ruby", ">= 3.0.0", "< 5.0.0"
45
+ spec.add_development_dependency "dogstatsd-ruby", ">= 4.0.0", "< 5.0.0"
45
46
  spec.add_development_dependency "statsd-ruby"
47
+ spec.add_development_dependency "prometheus-client", "~> 0.10.0"
46
48
  spec.add_development_dependency "ruby-prof"
47
49
  spec.add_development_dependency "timecop"
48
50
  spec.add_development_dependency "rubocop", "~> 0.49.1"