ruby-kafka 0.3.17 → 0.3.18.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.
@@ -1,16 +1,20 @@
1
+ require 'kafka/sasl_gssapi_authenticator'
2
+
1
3
  module Kafka
2
4
  class ConnectionBuilder
3
- def initialize(client_id:, logger:, instrumenter:, connect_timeout:, socket_timeout:, ssl_context:)
5
+ def initialize(client_id:, logger:, instrumenter:, connect_timeout:, socket_timeout:, ssl_context:, sasl_gssapi_principal:, sasl_gssapi_keytab:)
4
6
  @client_id = client_id
5
7
  @logger = logger
6
8
  @instrumenter = instrumenter
7
9
  @connect_timeout = connect_timeout
8
10
  @socket_timeout = socket_timeout
9
11
  @ssl_context = ssl_context
12
+ @sasl_gssapi_principal = sasl_gssapi_principal
13
+ @sasl_gssapi_keytab = sasl_gssapi_keytab
10
14
  end
11
15
 
12
16
  def build_connection(host, port)
13
- Connection.new(
17
+ connection = Connection.new(
14
18
  host: host,
15
19
  port: port,
16
20
  client_id: @client_id,
@@ -18,8 +22,31 @@ module Kafka
18
22
  socket_timeout: @socket_timeout,
19
23
  logger: @logger,
20
24
  instrumenter: @instrumenter,
21
- ssl_context: @ssl_context,
25
+ ssl_context: @ssl_context
22
26
  )
27
+
28
+ if authenticate_using_sasl_gssapi?
29
+ sasl_gssapi_authenticate(connection)
30
+ end
31
+
32
+ connection
33
+ end
34
+
35
+ private
36
+
37
+ def sasl_gssapi_authenticate(connection)
38
+ auth = SaslGssapiAuthenticator.new(
39
+ connection: connection,
40
+ logger: @logger,
41
+ sasl_gssapi_principal: @sasl_gssapi_principal,
42
+ sasl_gssapi_keytab: @sasl_gssapi_keytab
43
+ )
44
+
45
+ auth.authenticate!
46
+ end
47
+
48
+ def authenticate_using_sasl_gssapi?
49
+ !@ssl_context && @sasl_gssapi_principal && !@sasl_gssapi_principal.empty?
23
50
  end
24
51
  end
25
52
  end
@@ -167,12 +167,16 @@ module Kafka
167
167
  # is ignored.
168
168
  # @param max_wait_time [Integer, Float] the maximum duration of time to wait before
169
169
  # returning messages from the server, in seconds.
170
+ # @param automatically_mark_as_processed [Boolean] whether to automatically
171
+ # mark a message as successfully processed when the block returns
172
+ # without an exception. Once marked successful, the offsets of processed
173
+ # messages can be committed to Kafka.
170
174
  # @yieldparam message [Kafka::FetchedMessage] a message fetched from Kafka.
171
175
  # @raise [Kafka::ProcessingError] if there was an error processing a message.
172
176
  # The original exception will be returned by calling `#cause` on the
173
177
  # {Kafka::ProcessingError} instance.
174
178
  # @return [nil]
175
- def each_message(min_bytes: 1, max_wait_time: 5)
179
+ def each_message(min_bytes: 1, max_wait_time: 5, automatically_mark_as_processed: true)
176
180
  consumer_loop do
177
181
  batches = fetch_batches(min_bytes: min_bytes, max_wait_time: max_wait_time)
178
182
 
@@ -199,7 +203,7 @@ module Kafka
199
203
  end
200
204
  end
201
205
 
202
- mark_message_as_processed(message)
206
+ mark_message_as_processed(message) if automatically_mark_as_processed
203
207
  @offset_manager.commit_offsets_if_necessary
204
208
 
205
209
  @heartbeat.send_if_necessary
@@ -230,9 +234,13 @@ module Kafka
230
234
  # is ignored.
231
235
  # @param max_wait_time [Integer, Float] the maximum duration of time to wait before
232
236
  # returning messages from the server, in seconds.
237
+ # @param automatically_mark_as_processed [Boolean] whether to automatically
238
+ # mark a batch's messages as successfully processed when the block returns
239
+ # without an exception. Once marked successful, the offsets of processed
240
+ # messages can be committed to Kafka.
233
241
  # @yieldparam batch [Kafka::FetchedBatch] a message batch fetched from Kafka.
234
242
  # @return [nil]
235
- def each_batch(min_bytes: 1, max_wait_time: 5)
243
+ def each_batch(min_bytes: 1, max_wait_time: 5, automatically_mark_as_processed: true)
236
244
  consumer_loop do
237
245
  batches = fetch_batches(min_bytes: min_bytes, max_wait_time: max_wait_time)
238
246
 
@@ -260,7 +268,7 @@ module Kafka
260
268
  end
261
269
  end
262
270
 
263
- mark_message_as_processed(batch.messages.last)
271
+ mark_message_as_processed(batch.messages.last) if automatically_mark_as_processed
264
272
  end
265
273
 
266
274
  @offset_manager.commit_offsets_if_necessary
@@ -272,6 +280,14 @@ module Kafka
272
280
  end
273
281
  end
274
282
 
283
+ def commit_offsets
284
+ @offset_manager.commit_offsets
285
+ end
286
+
287
+ def mark_message_as_processed(message)
288
+ @offset_manager.mark_as_processed(message.topic, message.partition, message.offset)
289
+ end
290
+
275
291
  private
276
292
 
277
293
  def consumer_loop
@@ -293,11 +309,24 @@ module Kafka
293
309
  ensure
294
310
  # In order to quickly have the consumer group re-balance itself, it's
295
311
  # important that members explicitly tell Kafka when they're leaving.
296
- @offset_manager.commit_offsets rescue nil
312
+ make_final_offsets_commit!
297
313
  @group.leave rescue nil
298
314
  @running = false
299
315
  end
300
316
 
317
+ def make_final_offsets_commit!(attempts = 3)
318
+ @offset_manager.commit_offsets
319
+ rescue ConnectionError, Kafka::OffsetCommitError
320
+ # It's important to make sure final offsets commit is done
321
+ # As otherwise messages that have been processed after last auto-commit
322
+ # will be processed again and that may be huge amount of messages
323
+ return if attempts.zero?
324
+
325
+ @logger.error "Retrying to make final offsets commit (#{attempts} attempts left)"
326
+ sleep(0.1)
327
+ make_final_offsets_commit!(attempts - 1)
328
+ end
329
+
301
330
  def join_group
302
331
  old_generation_id = @group.generation_id
303
332
 
@@ -324,7 +353,7 @@ module Kafka
324
353
 
325
354
  @heartbeat.send_if_necessary
326
355
 
327
- raise "No partitions assigned!" if subscribed_partitions.empty?
356
+ raise NoPartitionsAssignedError if subscribed_partitions.empty?
328
357
 
329
358
  operation = FetchOperation.new(
330
359
  cluster: @cluster,
@@ -359,9 +388,5 @@ module Kafka
359
388
 
360
389
  raise FetchError, e
361
390
  end
362
-
363
- def mark_message_as_processed(message)
364
- @offset_manager.mark_as_processed(message.topic, message.partition, message.offset)
365
- end
366
391
  end
367
392
  end
data/lib/kafka/datadog.rb CHANGED
@@ -28,7 +28,7 @@ module Kafka
28
28
  STATSD_NAMESPACE = "ruby_kafka"
29
29
 
30
30
  def self.statsd
31
- @statsd ||= ::Datadog::Statsd.new(::Datadog::Statsd::DEFAULT_HOST, ::Datadog::Statsd::DEFAULT_HOST, namespace: STATSD_NAMESPACE)
31
+ @statsd ||= ::Datadog::Statsd.new(::Datadog::Statsd::DEFAULT_HOST, ::Datadog::Statsd::DEFAULT_PORT, namespace: STATSD_NAMESPACE)
32
32
  end
33
33
 
34
34
  def self.host=(host)
@@ -40,7 +40,11 @@ module Kafka
40
40
  }
41
41
  end
42
42
 
43
- def execute
43
+ def execute(&block)
44
+ if block.nil?
45
+ return to_enum(:execute)
46
+ end
47
+
44
48
  @cluster.add_target_topics(@topics.keys)
45
49
  @cluster.refresh_metadata_if_necessary!
46
50
 
@@ -56,7 +60,7 @@ module Kafka
56
60
  end
57
61
  end
58
62
 
59
- topics_by_broker.flat_map {|broker, topics|
63
+ responses = topics_by_broker.map {|broker, topics|
60
64
  resolve_offsets(broker, topics)
61
65
 
62
66
  options = {
@@ -65,10 +69,14 @@ module Kafka
65
69
  topics: topics,
66
70
  }
67
71
 
68
- response = broker.fetch_messages(**options)
72
+ broker.fetch_messages_async(**options)
73
+ }
74
+
75
+ responses.each {|response_future|
76
+ response = response_future.call
69
77
 
70
- response.topics.flat_map {|fetched_topic|
71
- fetched_topic.partitions.map {|fetched_partition|
78
+ response.topics.each {|fetched_topic|
79
+ fetched_topic.partitions.each {|fetched_partition|
72
80
  begin
73
81
  Protocol.handle_error(fetched_partition.error_code)
74
82
  rescue Kafka::OffsetOutOfRange => e
@@ -93,7 +101,7 @@ module Kafka
93
101
  )
94
102
  }
95
103
 
96
- FetchedBatch.new(
104
+ yield FetchedBatch.new(
97
105
  topic: fetched_topic.name,
98
106
  partition: fetched_partition.partition,
99
107
  highwater_mark_offset: fetched_partition.highwater_mark_offset,
@@ -6,19 +6,35 @@ module Kafka
6
6
  @default_payload = default_payload
7
7
 
8
8
  if defined?(ActiveSupport::Notifications)
9
- @backend = ActiveSupport::Notifications
9
+ @backend = ActiveSupport::Notifications.instrumenter
10
10
  else
11
11
  @backend = nil
12
12
  end
13
13
  end
14
14
 
15
- def instrument(event_name, payload = {}, &block)
15
+ def instrument(event_name, payload = {})
16
16
  if @backend
17
17
  payload.update(@default_payload)
18
18
 
19
- @backend.instrument("#{event_name}.#{NAMESPACE}", payload, &block)
19
+ @backend.instrument("#{event_name}.#{NAMESPACE}", payload) { yield payload if block_given? }
20
20
  else
21
- block.call(payload) if block
21
+ yield payload if block_given?
22
+ end
23
+ end
24
+
25
+ def start(event_name, payload = {})
26
+ if @backend
27
+ payload.update(@default_payload)
28
+
29
+ @backend.start("#{event_name}.#{NAMESPACE}", payload)
30
+ end
31
+ end
32
+
33
+ def finish(event_name, payload = {})
34
+ if @backend
35
+ payload.update(@default_payload)
36
+
37
+ @backend.finish("#{event_name}.#{NAMESPACE}", payload)
22
38
  end
23
39
  end
24
40
  end
@@ -32,5 +48,13 @@ module Kafka
32
48
  def instrument(event_name, payload = {}, &block)
33
49
  @backend.instrument(event_name, @extra_payload.merge(payload), &block)
34
50
  end
51
+
52
+ def start(event_name, payload = {})
53
+ @backend.start(event_name, @extra_payload.merge(payload))
54
+ end
55
+
56
+ def finish(event_name, payload = {})
57
+ @backend.finish(event_name, @extra_payload.merge(payload))
58
+ end
35
59
  end
36
60
  end
@@ -15,19 +15,20 @@ module Kafka
15
15
  12 => :heartbeat,
16
16
  13 => :leave_group,
17
17
  14 => :sync_group,
18
+ 17 => :sasl_handshake
18
19
  }
19
20
 
20
21
  ERRORS = {
21
22
  -1 => UnknownError,
22
- 1 => OffsetOutOfRange,
23
- 2 => CorruptMessage,
24
- 3 => UnknownTopicOrPartition,
25
- 4 => InvalidMessageSize,
26
- 5 => LeaderNotAvailable,
27
- 6 => NotLeaderForPartition,
28
- 7 => RequestTimedOut,
29
- 8 => BrokerNotAvailable,
30
- 9 => ReplicaNotAvailable,
23
+ 1 => OffsetOutOfRange,
24
+ 2 => CorruptMessage,
25
+ 3 => UnknownTopicOrPartition,
26
+ 4 => InvalidMessageSize,
27
+ 5 => LeaderNotAvailable,
28
+ 6 => NotLeaderForPartition,
29
+ 7 => RequestTimedOut,
30
+ 8 => BrokerNotAvailable,
31
+ 9 => ReplicaNotAvailable,
31
32
  10 => MessageSizeTooLarge,
32
33
  12 => OffsetMetadataTooLarge,
33
34
  15 => GroupCoordinatorNotAvailable,
@@ -41,6 +42,21 @@ module Kafka
41
42
  25 => UnknownMemberId,
42
43
  26 => InvalidSessionTimeout,
43
44
  27 => RebalanceInProgress,
45
+ 28 => InvalidCommitOffsetSize,
46
+ 29 => TopicAuthorizationCode,
47
+ 30 => GroupAuthorizationCode,
48
+ 31 => ClusterAuthorizationCode,
49
+ 32 => InvalidTimestamp,
50
+ 33 => UnsupportedSaslMechanism,
51
+ 34 => InvalidSaslState,
52
+ 35 => UnsupportedVersion,
53
+ 36 => TopicAlreadyExists,
54
+ 37 => InvalidPartitions,
55
+ 38 => InvalidReplicationFactor,
56
+ 39 => InvalidReplicaAssignment,
57
+ 40 => InvalidConfig,
58
+ 41 => NotController,
59
+ 42 => InvalidRequest
44
60
  }
45
61
 
46
62
  def self.handle_error(error_code)
@@ -81,3 +97,5 @@ require "kafka/protocol/offset_fetch_request"
81
97
  require "kafka/protocol/offset_fetch_response"
82
98
  require "kafka/protocol/offset_commit_request"
83
99
  require "kafka/protocol/offset_commit_response"
100
+ require "kafka/protocol/sasl_handshake_request"
101
+ require "kafka/protocol/sasl_handshake_response"
@@ -0,0 +1,11 @@
1
+ module Kafka
2
+ module Protocol
3
+
4
+ # A response class used when no response is expected.
5
+ class NullResponse
6
+ def self.decode(decoder)
7
+ nil
8
+ end
9
+ end
10
+ end
11
+ end
@@ -0,0 +1,31 @@
1
+ module Kafka
2
+ module Protocol
3
+
4
+ # SaslHandshake Request (Version: 0) => mechanism
5
+ # mechanism => string
6
+
7
+ class SaslHandshakeRequest
8
+
9
+ SUPPORTED_MECHANISMS = %w(GSSAPI)
10
+
11
+ def initialize(mechanism)
12
+ unless SUPPORTED_MECHANISMS.include?(mechanism)
13
+ raise Kafka::Error, "Unsupported SASL mechanism #{mechanism}. Supported are #{SUPPORTED_MECHANISMS.join(', ')}"
14
+ end
15
+ @mechanism = mechanism
16
+ end
17
+
18
+ def api_key
19
+ 17
20
+ end
21
+
22
+ def response_class
23
+ SaslHandshakeResponse
24
+ end
25
+
26
+ def encode(encoder)
27
+ encoder.write_string(@mechanism)
28
+ end
29
+ end
30
+ end
31
+ end
@@ -0,0 +1,26 @@
1
+ module Kafka
2
+ module Protocol
3
+
4
+ # SaslHandshake Response (Version: 0) => error_code [enabled_mechanisms]
5
+ # error_code => int16
6
+ # enabled_mechanisms => array of strings
7
+
8
+ class SaslHandshakeResponse
9
+ attr_reader :error_code
10
+
11
+ attr_reader :enabled_mechanisms
12
+
13
+ def initialize(error_code:, enabled_mechanisms:)
14
+ @error_code = error_code
15
+ @enabled_mechanisms = enabled_mechanisms
16
+ end
17
+
18
+ def self.decode(decoder)
19
+ new(
20
+ error_code: decoder.int16,
21
+ enabled_mechanisms: decoder.array { decoder.string }
22
+ )
23
+ end
24
+ end
25
+ end
26
+ end
@@ -0,0 +1,69 @@
1
+ require 'gssapi'
2
+
3
+ module Kafka
4
+ class SaslGssapiAuthenticator
5
+ GSSAPI_IDENT = "GSSAPI"
6
+ GSSAPI_CONFIDENTIALITY = false
7
+
8
+ def initialize(connection:, logger:, sasl_gssapi_principal:, sasl_gssapi_keytab:)
9
+ @connection = connection
10
+ @logger = logger
11
+ @principal = sasl_gssapi_principal
12
+ @keytab = sasl_gssapi_keytab
13
+
14
+ initialize_gssapi_context
15
+ end
16
+
17
+ def authenticate!
18
+ proceed_sasl_gssapi_negotiation
19
+ end
20
+
21
+ private
22
+
23
+ def proceed_sasl_gssapi_negotiation
24
+ response = @connection.send_request(Kafka::Protocol::SaslHandshakeRequest.new(GSSAPI_IDENT))
25
+
26
+ @encoder = @connection.encoder
27
+ @decoder = @connection.decoder
28
+
29
+ unless response.error_code == 0 && response.enabled_mechanisms.include?(GSSAPI_IDENT)
30
+ raise Kafka::Error, "#{GSSAPI_IDENT} is not supported."
31
+ end
32
+
33
+ # send gssapi token and receive token to verify
34
+ token_to_verify = send_and_receive_sasl_token
35
+
36
+ # verify incoming token
37
+ unless @gssapi_ctx.init_context(token_to_verify)
38
+ raise Kafka::Error, "GSSAPI context verification failed."
39
+ end
40
+
41
+ # we can continue, so send OK
42
+ @encoder.write([0,2].pack('l>c'))
43
+
44
+ # read wrapped message and return it back with principal
45
+ handshake_messages
46
+ end
47
+
48
+ def handshake_messages
49
+ msg = @decoder.bytes
50
+ raise Kafka::Error, "GSSAPI negotiation failed." unless msg
51
+ # unwrap with integrity only
52
+ msg_unwrapped = @gssapi_ctx.unwrap_message(msg, GSSAPI_CONFIDENTIALITY)
53
+ msg_wrapped = @gssapi_ctx.wrap_message(msg_unwrapped + @principal, GSSAPI_CONFIDENTIALITY)
54
+ @encoder.write_bytes(msg_wrapped)
55
+ end
56
+
57
+ def send_and_receive_sasl_token
58
+ @encoder.write_bytes(@gssapi_token)
59
+ @decoder.bytes
60
+ end
61
+
62
+ def initialize_gssapi_context
63
+ @logger.debug "GSSAPI: Initializing context with #{@connection.to_s}, principal #{@principal}"
64
+
65
+ @gssapi_ctx = GSSAPI::Simple.new(@connection.to_s, @principal, @keytab)
66
+ @gssapi_token = @gssapi_ctx.init_context(nil)
67
+ end
68
+ end
69
+ end