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.
- checksums.yaml +4 -4
- data/.gitignore +1 -0
- data/Gemfile.lock +6 -2
- data/README.md +76 -14
- data/lib/kafka.rb +72 -0
- data/lib/kafka/broker.rb +22 -0
- data/lib/kafka/client.rb +29 -1
- data/lib/kafka/cluster.rb +4 -1
- data/lib/kafka/connection.rb +114 -23
- data/lib/kafka/connection_builder.rb +30 -3
- data/lib/kafka/consumer.rb +35 -10
- data/lib/kafka/datadog.rb +1 -1
- data/lib/kafka/fetch_operation.rb +14 -6
- data/lib/kafka/instrumenter.rb +28 -4
- data/lib/kafka/protocol.rb +27 -9
- data/lib/kafka/protocol/null_response.rb +11 -0
- data/lib/kafka/protocol/sasl_handshake_request.rb +31 -0
- data/lib/kafka/protocol/sasl_handshake_response.rb +26 -0
- data/lib/kafka/sasl_gssapi_authenticator.rb +69 -0
- data/lib/kafka/version.rb +1 -1
- data/ruby-kafka.gemspec +2 -0
- data/vendor/bundle/bin/bundler +17 -0
- data/vendor/bundle/bin/coderay +17 -0
- data/vendor/bundle/bin/dotenv +17 -0
- data/vendor/bundle/bin/htmldiff +17 -0
- data/vendor/bundle/bin/ldiff +17 -0
- data/vendor/bundle/bin/pry +17 -0
- data/vendor/bundle/bin/rake +17 -0
- data/vendor/bundle/bin/rspec +17 -0
- data/vendor/bundle/bin/ruby-prof +17 -0
- data/vendor/bundle/bin/ruby-prof-check-trace +17 -0
- metadata +32 -4
@@ -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
|
data/lib/kafka/consumer.rb
CHANGED
@@ -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
|
-
|
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
|
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::
|
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.
|
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
|
-
|
72
|
+
broker.fetch_messages_async(**options)
|
73
|
+
}
|
74
|
+
|
75
|
+
responses.each {|response_future|
|
76
|
+
response = response_future.call
|
69
77
|
|
70
|
-
response.topics.
|
71
|
-
fetched_topic.partitions.
|
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,
|
data/lib/kafka/instrumenter.rb
CHANGED
@@ -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 = {}
|
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
|
19
|
+
@backend.instrument("#{event_name}.#{NAMESPACE}", payload) { yield payload if block_given? }
|
20
20
|
else
|
21
|
-
|
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
|
data/lib/kafka/protocol.rb
CHANGED
@@ -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
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
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,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
|