ruby-kafka 0.5.0 → 0.5.1.beta1

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 85fb6cfbef6aefd7ddfd6c414075e73ad9fd4ef1
4
- data.tar.gz: e23aff1782ebc6871e8fab5d24ee88475d557436
3
+ metadata.gz: 872944b4c0fb6b670fac704b827cb2ffa713e8ae
4
+ data.tar.gz: 17507f698aa6eb93007164d72cb24d2f79d9f2d8
5
5
  SHA512:
6
- metadata.gz: 1a9c938f3554e6156f8d1b2315f26fad6b10c09fb70ed5b4228a48a5357833df43c00756873c569439fbbca112121ab857048f26c068ac53eac24a0686d2a576
7
- data.tar.gz: 75211978d2f94eeb4b03c39aad903604d6ab1fdda0b30e4f582d02adb3b7293886e6e381259ad15a392ae6616e4e39aa74591168f9f161bb5b3b7efbbf5aab1d
6
+ metadata.gz: a5ac176d6dfa6db9ac431cf6a3c7bb1eda4183ef2a0f19864d0904ebb3024861b92ef98083fdf06512ab85c5545c2b93db10e83d8539858817953e9fc2689a4b
7
+ data.tar.gz: d72d3db50e98dd12840494c0691943d2f332dca639caf0cda65e750ce3109bd03ad194f720129a887100242f19ef52d2a710dd290fa02ec43eafa3f8fa025c2e
@@ -0,0 +1,33 @@
1
+ version: 2
2
+ jobs:
3
+ build:
4
+ docker:
5
+ - image: circleci/ruby:2.4.1-node
6
+ environment:
7
+ LOG_LEVEL: DEBUG
8
+ - image: wurstmeister/zookeeper
9
+ - image: wurstmeister/kafka:0.10.2.1
10
+ environment:
11
+ KAFKA_ADVERTISED_HOST_NAME: localhost
12
+ KAFKA_ADVERTISED_PORT: 9092
13
+ KAFKA_PORT: 9092
14
+ KAFKA_ZOOKEEPER_CONNECT: localhost:2181
15
+ - image: wurstmeister/kafka:0.10.2.1
16
+ environment:
17
+ KAFKA_ADVERTISED_HOST_NAME: localhost
18
+ KAFKA_ADVERTISED_PORT: 9093
19
+ KAFKA_PORT: 9093
20
+ KAFKA_ZOOKEEPER_CONNECT: localhost:2181
21
+ - image: wurstmeister/kafka:0.10.2.1
22
+ environment:
23
+ KAFKA_ADVERTISED_HOST_NAME: localhost
24
+ KAFKA_ADVERTISED_PORT: 9094
25
+ KAFKA_PORT: 9094
26
+ KAFKA_ZOOKEEPER_CONNECT: localhost:2181
27
+
28
+ steps:
29
+ - checkout
30
+ - run: bundle install --path vendor/bundle
31
+ - run: bundle exec rspec
32
+ - run: bundle exec rspec --profile --tag functional spec/functional
33
+ - run: bundle exec rubocop
data/CHANGELOG.md CHANGED
@@ -4,6 +4,14 @@ Changes and additions to the library will be listed here.
4
4
 
5
5
  ## Unreleased
6
6
 
7
+ Requires Kafka 0.10.1+ due to usage of a few new APIs.
8
+
9
+ - Fix bug when using compression (#458).
10
+ - Update the v3 of the Fetch API, allowing a per-request `max_bytes` setting (#468).
11
+ - Make `#deliver_message` more resilient using retries and backoff.
12
+ - Add support for SASL SCRAM authentication (#465).
13
+ - Refactor and simplify SASL code.
14
+
7
15
  ## v0.5.0
8
16
 
9
17
  - Drops support for Kafka 0.9 in favor of Kafka 0.10 (#381)!
data/README.md CHANGED
@@ -46,6 +46,8 @@ Although parts of this library work with Kafka 0.8 – specifically, the Produce
46
46
  6. [Support and Discussion](#support-and-discussion)
47
47
  7. [Roadmap](#roadmap)
48
48
  8. [Higher level libraries](#higher-level-libraries)
49
+ 1. [Message processing frameworks](#message-processing-framework)
50
+ 2. [Message publishing libraries](#message-publishing-libraries)
49
51
 
50
52
  ## Installation
51
53
 
@@ -91,7 +93,7 @@ Or install it yourself as:
91
93
 
92
94
  This library is targeting Kafka 0.9 with the v0.4.x series and Kafka 0.10 with the v0.5.x series. There's limited support for Kafka 0.8, and things should work with Kafka 0.11, although there may be performance issues due to changes in the protocol.
93
95
 
94
- - **Kafka 0.8:** Full support for the Producer API, but no support for consumer groups. Simple message fetching works.
96
+ - **Kafka 0.8:** Full support for the Producer API in ruby-kafka v0.4.x, but no support for consumer groups. Simple message fetching works.
95
97
  - **Kafka 0.9:** Full support for the Producer and Consumer API in ruby-kafka v0.4.x.
96
98
  - **Kafka 0.10:** Full support for the Producer and Consumer API in ruby-kafka v0.5.x.
97
99
  - **Kafka 0.11:** Everything that works with Kafka 0.10 should still work, but so far no features specific to Kafka 0.11 have been added.
@@ -899,8 +901,9 @@ Typically, Kafka certificates come in the JKS format, which isn't supported by r
899
901
 
900
902
  #### Authentication using SASL
901
903
 
902
- Kafka has support for using SASL to authenticate clients. Currently GSSAPI and PLAIN mechanisms are supported by ruby-kafka.
904
+ Kafka has support for using SASL to authenticate clients. Currently GSSAPI, SCRAM and PLAIN mechanisms are supported by ruby-kafka.
903
905
 
906
+ ##### GSSAPI
904
907
  In order to authenticate using GSSAPI, set your principal and optionally your keytab when initializing the Kafka client:
905
908
 
906
909
  ```ruby
@@ -911,6 +914,7 @@ kafka = Kafka.new(
911
914
  )
912
915
  ```
913
916
 
917
+ ##### PLAIN
914
918
  In order to authenticate using PLAIN, you must set your username and password when initializing the Kafka client:
915
919
 
916
920
  ```ruby
@@ -924,6 +928,18 @@ kafka = Kafka.new(
924
928
 
925
929
  **NOTE**: It is __highly__ recommended that you use SSL for encryption when using SASL_PLAIN
926
930
 
931
+ ##### SCRAM
932
+ Since 0.11 kafka supports [SCRAM](https://kafka.apache.org/documentation.html#security_sasl_scram).
933
+
934
+ ```ruby
935
+ kafka = Kafka.new(
936
+ sasl_scram_username: 'username',
937
+ sasl_scram_password: 'password',
938
+ sasl_scram_mechanism: 'sha256',
939
+ # ...
940
+ )
941
+ ```
942
+
927
943
  ## Design
928
944
 
929
945
  The library has been designed as a layered system, with each layer having a clear responsibility:
@@ -978,24 +994,30 @@ Version 0.4 will be the last minor release with support for the Kafka 0.9 protoc
978
994
 
979
995
  ### v0.4
980
996
 
981
- Current stable release with support for the Kafka 0.9 protocol.
997
+ Last stable release with support for the Kafka 0.9 protocol. Bug and security fixes will be released in patch updates.
982
998
 
983
999
  ### v0.5
984
1000
 
985
- Next stable release, with support for the Kafka 0.10 protocol and eventually newer protocol versions.
1001
+ Latest stable release, with native support for the Kafka 0.10 protocol and eventually newer protocol versions. Kafka 0.9 is no longer supported by this release series.
986
1002
 
987
1003
  ## Higher level libraries
988
1004
 
989
- Currently, there are three actively developed frameworks based on ruby-kafka, that provide higher level API that can be used to work with Kafka messages:
1005
+ Currently, there are three actively developed frameworks based on ruby-kafka, that provide higher level API that can be used to work with Kafka messages and two libraries for publishing messages.
990
1006
 
991
- * [Racecar](https://github.com/zendesk/racecar) - A simple framework that integrates with Ruby on Rails to provide a seamless way to write, test, configure, and run Kafka consumers. It comes with sensible defaults and conventions.
1007
+ ### Message processing frameworks
992
1008
 
993
- * [DeliveryBoy](https://github.com/zendesk/delivery_boy) A library that integrates with Ruby on Rails, making it easy to publish Kafka messages from any Rails application.
1009
+ * [Racecar](https://github.com/zendesk/racecar) - A simple framework that integrates with Ruby on Rails to provide a seamless way to write, test, configure, and run Kafka consumers. It comes with sensible defaults and conventions.
994
1010
 
995
1011
  * [Karafka](https://github.com/karafka/karafka) - Framework used to simplify Apache Kafka based Ruby and Rails applications development. Karafka provides higher abstraction layers, including Capistrano, Docker and Heroku support.
996
1012
 
997
1013
  * [Phobos](https://github.com/klarna/phobos) - Micro framework and library for applications dealing with Apache Kafka. It wraps common behaviors needed by consumers and producers in an easy and convenient API.
998
1014
 
1015
+ ### Message publishing libraries
1016
+
1017
+ * [DeliveryBoy](https://github.com/zendesk/delivery_boy) – A library that integrates with Ruby on Rails, making it easy to publish Kafka messages from any Rails application.
1018
+
1019
+ * [WaterDrop](https://github.com/karafka/waterdrop) – A library for Ruby and Ruby on Rails applications, to easy publish Kafka messages in both sync and async way.
1020
+
999
1021
  ## Why Create A New Library?
1000
1022
 
1001
1023
  There are a few existing Kafka clients in Ruby:
@@ -0,0 +1,39 @@
1
+ version: '2'
2
+ services:
3
+ zookeeper:
4
+ image: wurstmeister/zookeeper
5
+ ports:
6
+ - "2181:2181"
7
+ kafka1:
8
+ image: wurstmeister/kafka:0.10.2.1
9
+ ports:
10
+ - "9092:9092"
11
+ environment:
12
+ KAFKA_BROKER_ID: 1
13
+ KAFKA_ADVERTISED_HOST_NAME: 192.168.99.100
14
+ KAFKA_ADVERTISED_PORT: 9092
15
+ KAFKA_ZOOKEEPER_CONNECT: zookeeper:2181
16
+ volumes:
17
+ - /var/run/docker.sock:/var/run/docker.sock
18
+ kafka2:
19
+ image: wurstmeister/kafka:0.10.2.1
20
+ ports:
21
+ - "9093:9092"
22
+ environment:
23
+ KAFKA_BROKER_ID: 2
24
+ KAFKA_ADVERTISED_HOST_NAME: 192.168.99.100
25
+ KAFKA_ADVERTISED_PORT: 9093
26
+ KAFKA_ZOOKEEPER_CONNECT: zookeeper:2181
27
+ volumes:
28
+ - /var/run/docker.sock:/var/run/docker.sock
29
+ kafka3:
30
+ image: wurstmeister/kafka:0.10.2.1
31
+ ports:
32
+ - "9094:9092"
33
+ environment:
34
+ KAFKA_BROKER_ID: 3
35
+ KAFKA_ADVERTISED_HOST_NAME: 192.168.99.100
36
+ KAFKA_ADVERTISED_PORT: 9094
37
+ KAFKA_ZOOKEEPER_CONNECT: zookeeper:2181
38
+ volumes:
39
+ - /var/run/docker.sock:/var/run/docker.sock
data/lib/kafka.rb CHANGED
@@ -21,6 +21,14 @@ module Kafka
21
21
  class NoPartitionsToFetchFrom < Error
22
22
  end
23
23
 
24
+ # A message in a partition is larger than the maximum we've asked for.
25
+ class MessageTooLargeToRead < Error
26
+ end
27
+
28
+ # A connection has been unused for too long, we assume the server has killed it.
29
+ class IdleConnection < Error
30
+ end
31
+
24
32
  # Subclasses of this exception class map to an error code described in the
25
33
  # Kafka protocol specification.
26
34
  #
@@ -225,6 +233,12 @@ module Kafka
225
233
  class FetchError < Error
226
234
  end
227
235
 
236
+ class SaslScramError < Error
237
+ end
238
+
239
+ class FailedScramAuthentication < SaslScramError
240
+ end
241
+
228
242
  # Initializes a new Kafka client.
229
243
  #
230
244
  # @see Client#initialize
data/lib/kafka/broker.rb CHANGED
@@ -4,24 +4,27 @@ require "kafka/protocol"
4
4
 
5
5
  module Kafka
6
6
  class Broker
7
- def initialize(connection:, node_id: nil, logger:)
8
- @connection = connection
7
+ def initialize(connection_builder:, host:, port:, node_id: nil, logger:)
8
+ @connection_builder = connection_builder
9
+ @connection = nil
10
+ @host = host
11
+ @port = port
9
12
  @node_id = node_id
10
13
  @logger = logger
11
14
  end
12
15
 
13
16
  def address_match?(host, port)
14
- @connection.address_match?(host, port)
17
+ host == @host && port == @port
15
18
  end
16
19
 
17
20
  # @return [String]
18
21
  def to_s
19
- "#{@connection} (node_id=#{@node_id.inspect})"
22
+ "#{connection} (node_id=#{@node_id.inspect})"
20
23
  end
21
24
 
22
25
  # @return [nil]
23
26
  def disconnect
24
- @connection.close
27
+ connection.close
25
28
  end
26
29
 
27
30
  # Fetches cluster metadata from the broker.
@@ -31,7 +34,7 @@ module Kafka
31
34
  def fetch_metadata(**options)
32
35
  request = Protocol::TopicMetadataRequest.new(**options)
33
36
 
34
- @connection.send_request(request)
37
+ send_request(request)
35
38
  end
36
39
 
37
40
  # Fetches messages from a specified topic and partition.
@@ -41,7 +44,7 @@ module Kafka
41
44
  def fetch_messages(**options)
42
45
  request = Protocol::FetchRequest.new(**options)
43
46
 
44
- @connection.send_request(request)
47
+ send_request(request)
45
48
  end
46
49
 
47
50
  # Lists the offset of the specified topics and partitions.
@@ -51,7 +54,7 @@ module Kafka
51
54
  def list_offsets(**options)
52
55
  request = Protocol::ListOffsetRequest.new(**options)
53
56
 
54
- @connection.send_request(request)
57
+ send_request(request)
55
58
  end
56
59
 
57
60
  # Produces a set of messages to the broker.
@@ -61,55 +64,81 @@ module Kafka
61
64
  def produce(**options)
62
65
  request = Protocol::ProduceRequest.new(**options)
63
66
 
64
- @connection.send_request(request)
67
+ send_request(request)
65
68
  end
66
69
 
67
70
  def fetch_offsets(**options)
68
71
  request = Protocol::OffsetFetchRequest.new(**options)
69
72
 
70
- @connection.send_request(request)
73
+ send_request(request)
71
74
  end
72
75
 
73
76
  def commit_offsets(**options)
74
77
  request = Protocol::OffsetCommitRequest.new(**options)
75
78
 
76
- @connection.send_request(request)
79
+ send_request(request)
77
80
  end
78
81
 
79
82
  def join_group(**options)
80
83
  request = Protocol::JoinGroupRequest.new(**options)
81
84
 
82
- @connection.send_request(request)
85
+ send_request(request)
83
86
  end
84
87
 
85
88
  def sync_group(**options)
86
89
  request = Protocol::SyncGroupRequest.new(**options)
87
90
 
88
- @connection.send_request(request)
91
+ send_request(request)
89
92
  end
90
93
 
91
94
  def leave_group(**options)
92
95
  request = Protocol::LeaveGroupRequest.new(**options)
93
96
 
94
- @connection.send_request(request)
97
+ send_request(request)
95
98
  end
96
99
 
97
100
  def find_group_coordinator(**options)
98
101
  request = Protocol::GroupCoordinatorRequest.new(**options)
99
102
 
100
- @connection.send_request(request)
103
+ send_request(request)
101
104
  end
102
105
 
103
106
  def heartbeat(**options)
104
107
  request = Protocol::HeartbeatRequest.new(**options)
105
108
 
106
- @connection.send_request(request)
109
+ send_request(request)
107
110
  end
108
111
 
109
- def sasl_handshake(**options)
110
- request = Protocol::SaslHandshakeRequest(**options)
112
+ def create_topics(**options)
113
+ request = Protocol::CreateTopicsRequest.new(**options)
111
114
 
112
- @connection.send_request(request)
115
+ send_request(request)
116
+ end
117
+
118
+ def api_versions
119
+ request = Protocol::ApiVersionsRequest.new
120
+
121
+ send_request(request)
122
+ end
123
+
124
+ private
125
+
126
+ def send_request(request)
127
+ connection.send_request(request)
128
+ rescue IdleConnection
129
+ @logger.warn "Connection has been unused for too long, re-connecting..."
130
+ @connection.close rescue nil
131
+ @connection = nil
132
+ retry
133
+ rescue ConnectionError
134
+ @connection.close rescue nil
135
+ @connection = nil
136
+
137
+ raise
138
+ end
139
+
140
+ def connection
141
+ @connection ||= @connection_builder.build_connection(@host, @port)
113
142
  end
114
143
  end
115
144
  end
@@ -17,7 +17,9 @@ module Kafka
17
17
  end
18
18
 
19
19
  broker = Broker.new(
20
- connection: @connection_builder.build_connection(host, port),
20
+ connection_builder: @connection_builder,
21
+ host: host,
22
+ port: port,
21
23
  node_id: node_id,
22
24
  logger: @logger,
23
25
  )
data/lib/kafka/client.rb CHANGED
@@ -49,11 +49,18 @@ module Kafka
49
49
  #
50
50
  # @param sasl_gssapi_keytab [String, nil] a KRB5 keytab filepath
51
51
  #
52
+ # @param sasl_scram_username [String, nil] SCRAM username
53
+ #
54
+ # @param sasl_scram_password [String, nil] SCRAM password
55
+ #
56
+ # @param sasl_scram_mechanism [String, nil] Scram mechanism, either "sha256" or "sha512"
57
+ #
52
58
  # @return [Client]
53
59
  def initialize(seed_brokers:, client_id: "ruby-kafka", logger: nil, connect_timeout: nil, socket_timeout: nil,
54
60
  ssl_ca_cert_file_path: nil, ssl_ca_cert: nil, ssl_client_cert: nil, ssl_client_cert_key: nil,
55
61
  sasl_gssapi_principal: nil, sasl_gssapi_keytab: nil,
56
- sasl_plain_authzid: '', sasl_plain_username: nil, sasl_plain_password: nil)
62
+ sasl_plain_authzid: '', sasl_plain_username: nil, sasl_plain_password: nil,
63
+ sasl_scram_username: nil, sasl_scram_password: nil, sasl_scram_mechanism: nil)
57
64
  @logger = logger || Logger.new(nil)
58
65
  @instrumenter = Instrumenter.new(client_id: client_id)
59
66
  @seed_brokers = normalize_seed_brokers(seed_brokers)
@@ -66,6 +73,9 @@ module Kafka
66
73
  sasl_plain_authzid: sasl_plain_authzid,
67
74
  sasl_plain_username: sasl_plain_username,
68
75
  sasl_plain_password: sasl_plain_password,
76
+ sasl_scram_username: sasl_scram_username,
77
+ sasl_scram_password: sasl_scram_password,
78
+ sasl_scram_mechanism: sasl_scram_mechanism,
69
79
  logger: @logger
70
80
  )
71
81
 
@@ -96,8 +106,10 @@ module Kafka
96
106
  # chosen at random.
97
107
  # @param partition_key [String] a value used to deterministically choose a
98
108
  # partition to write to.
109
+ # @param retries [Integer] the number of times to retry the delivery before giving
110
+ # up.
99
111
  # @return [nil]
100
- def deliver_message(value, key: nil, topic:, partition: nil, partition_key: nil)
112
+ def deliver_message(value, key: nil, topic:, partition: nil, partition_key: nil, retries: 1)
101
113
  create_time = Time.now
102
114
 
103
115
  message = PendingMessage.new(
@@ -140,10 +152,27 @@ module Kafka
140
152
  instrumenter: @instrumenter,
141
153
  )
142
154
 
143
- operation.execute
155
+ attempt = 1
156
+
157
+ begin
158
+ operation.execute
159
+
160
+ unless buffer.empty?
161
+ raise DeliveryFailed.new(nil, [message])
162
+ end
163
+ rescue Kafka::Error => e
164
+ @cluster.mark_as_stale!
144
165
 
145
- unless buffer.empty?
146
- raise DeliveryFailed.new(nil, [message])
166
+ if attempt >= (retries + 1)
167
+ raise
168
+ else
169
+ attempt += 1
170
+ @logger.warn "Error while delivering message, #{e.class}: #{e.message}; retrying after 1s..."
171
+
172
+ sleep 1
173
+
174
+ retry
175
+ end
147
176
  end
148
177
  end
149
178
 
@@ -345,17 +374,32 @@ module Kafka
345
374
  # expect messages to be larger than this.
346
375
  #
347
376
  # @return [Array<Kafka::FetchedMessage>] the messages returned from the broker.
348
- def fetch_messages(topic:, partition:, offset: :latest, max_wait_time: 5, min_bytes: 1, max_bytes: 1048576)
377
+ def fetch_messages(topic:, partition:, offset: :latest, max_wait_time: 5, min_bytes: 1, max_bytes: 1048576, retries: 1)
349
378
  operation = FetchOperation.new(
350
379
  cluster: @cluster,
351
380
  logger: @logger,
352
381
  min_bytes: min_bytes,
382
+ max_bytes: max_bytes,
353
383
  max_wait_time: max_wait_time,
354
384
  )
355
385
 
356
386
  operation.fetch_from_partition(topic, partition, offset: offset, max_bytes: max_bytes)
357
387
 
358
- operation.execute.flat_map {|batch| batch.messages }
388
+ attempt = 1
389
+
390
+ begin
391
+ operation.execute.flat_map {|batch| batch.messages }
392
+ rescue Kafka::Error => e
393
+ @cluster.mark_as_stale!
394
+
395
+ if attempt >= (retries + 1)
396
+ raise
397
+ else
398
+ attempt += 1
399
+ @logger.warn "Error while fetching messages, #{e.class}: #{e.message}; retrying..."
400
+ retry
401
+ end
402
+ end
359
403
  end
360
404
 
361
405
  # Enumerate all messages in a topic.
@@ -407,6 +451,10 @@ module Kafka
407
451
  end
408
452
  end
409
453
 
454
+ def create_topic(name, **options)
455
+ @cluster.create_topic(name, **options)
456
+ end
457
+
410
458
  # Lists all topics in the cluster.
411
459
  #
412
460
  # @return [Array<String>] the list of topic names.
@@ -415,6 +463,12 @@ module Kafka
415
463
  @cluster.topics
416
464
  end
417
465
 
466
+ def has_topic?(topic)
467
+ @cluster.clear_target_topics
468
+ @cluster.add_target_topics([topic])
469
+ @cluster.topics.include?(topic)
470
+ end
471
+
418
472
  # Counts the number of partitions in a topic.
419
473
  #
420
474
  # @param topic [String]
@@ -455,6 +509,10 @@ module Kafka
455
509
  }.to_h
456
510
  end
457
511
 
512
+ def apis
513
+ @cluster.apis
514
+ end
515
+
458
516
  # Closes all connections to the Kafka brokers and frees up used resources.
459
517
  #
460
518
  # @return [nil]