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.
- 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
|