ruby-kafka 0.3.17 → 0.3.18.beta1

Sign up to get free protection for your applications and to get access to all the features.
@@ -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