ruby-kafka 1.3.0 → 1.4.0

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
  SHA256:
3
- metadata.gz: b16e9e52014e784610725bb2ba5c5a431694ce6da7878b6c1ff024504d6ef6c4
4
- data.tar.gz: '039026011e9cd5e5dce59aed4f53b2a8970aa8025fe3b008620bdef8a802bbf5'
3
+ metadata.gz: fd754956b907d18ac2a17b3d8f5bc374a8851dea21826277b8941f163403161d
4
+ data.tar.gz: 7a1b74d4c3d3f8cfb0772bd6521a8a5c726bc1efc5ade83f814802ce08fb0f24
5
5
  SHA512:
6
- metadata.gz: 1d13c2032f4bd38e09a714fe40d59467ff61b2c51e0a604daf7f9e1a3431af32f77e62433b863862a68e6bc74f53ff4eceed7e88145acc102613ac6fa2600b9f
7
- data.tar.gz: f424e6e2bee5f318766880f3b7eb338ce706783ea647908902a8a6d409fcf8f9ac3f0afe7ca8a932710a9d8f192e44a25a346da6639861fb7d377f3dd3257309
6
+ metadata.gz: 45038342acd388b7b797b64261addb3bcd90f43fb27d9dfe43a5071f5cb2a5ab6e25df1557fcadd528c5c40056384183cdd427d4fdac66de5cb93c67d246d45f
7
+ data.tar.gz: af738b6a58f1cc7d1ee8c0fda72edad9fc94eb21c409f8b48a935a990b563736d30060ea209ef25d11f46ef2252f0d4adae726455c295637c2ef71522473c712
data/.circleci/config.yml CHANGED
@@ -7,6 +7,7 @@ jobs:
7
7
  LOG_LEVEL: DEBUG
8
8
  steps:
9
9
  - checkout
10
+ - run: sudo apt-get update && sudo apt-get install -y cmake # For installing snappy
10
11
  - run: bundle install --path vendor/bundle
11
12
  - run: bundle exec rspec
12
13
  - run: bundle exec rubocop
@@ -40,6 +41,7 @@ jobs:
40
41
  KAFKA_DELETE_TOPIC_ENABLE: true
41
42
  steps:
42
43
  - checkout
44
+ - run: sudo apt-get update && sudo apt-get install -y cmake # For installing snappy
43
45
  - run: bundle install --path vendor/bundle
44
46
  - run: bundle exec rspec --profile --tag functional spec/functional
45
47
 
@@ -72,6 +74,7 @@ jobs:
72
74
  KAFKA_DELETE_TOPIC_ENABLE: true
73
75
  steps:
74
76
  - checkout
77
+ - run: sudo apt-get update && sudo apt-get install -y cmake # For installing snappy
75
78
  - run: bundle install --path vendor/bundle
76
79
  - run: bundle exec rspec --profile --tag functional spec/functional
77
80
 
@@ -104,6 +107,7 @@ jobs:
104
107
  KAFKA_DELETE_TOPIC_ENABLE: true
105
108
  steps:
106
109
  - checkout
110
+ - run: sudo apt-get update && sudo apt-get install -y cmake # For installing snappy
107
111
  - run: bundle install --path vendor/bundle
108
112
  - run: bundle exec rspec --profile --tag functional spec/functional
109
113
 
@@ -136,6 +140,7 @@ jobs:
136
140
  KAFKA_DELETE_TOPIC_ENABLE: true
137
141
  steps:
138
142
  - checkout
143
+ - run: sudo apt-get update && sudo apt-get install -y cmake # For installing snappy
139
144
  - run: bundle install --path vendor/bundle
140
145
  - run: bundle exec rspec --profile --tag functional spec/functional
141
146
 
@@ -168,6 +173,7 @@ jobs:
168
173
  KAFKA_DELETE_TOPIC_ENABLE: true
169
174
  steps:
170
175
  - checkout
176
+ - run: sudo apt-get update && sudo apt-get install -y cmake # For installing snappy
171
177
  - run: bundle install --path vendor/bundle
172
178
  - run: bundle exec rspec --profile --tag functional spec/functional
173
179
 
@@ -200,6 +206,7 @@ jobs:
200
206
  KAFKA_DELETE_TOPIC_ENABLE: true
201
207
  steps:
202
208
  - checkout
209
+ - run: sudo apt-get update && sudo apt-get install -y cmake # For installing snappy
203
210
  - run: bundle install --path vendor/bundle
204
211
  - run: bundle exec rspec --profile --tag functional spec/functional
205
212
 
@@ -232,6 +239,7 @@ jobs:
232
239
  KAFKA_DELETE_TOPIC_ENABLE: true
233
240
  steps:
234
241
  - checkout
242
+ - run: sudo apt-get update && sudo apt-get install -y cmake # For installing snappy
235
243
  - run: bundle install --path vendor/bundle
236
244
  - run: bundle exec rspec --profile --tag functional spec/functional
237
245
 
@@ -264,6 +272,7 @@ jobs:
264
272
  KAFKA_DELETE_TOPIC_ENABLE: true
265
273
  steps:
266
274
  - checkout
275
+ - run: sudo apt-get update && sudo apt-get install -y cmake # For installing snappy
267
276
  - run: bundle install --path vendor/bundle
268
277
  - run: bundle exec rspec --profile --tag functional spec/functional
269
278
 
@@ -296,6 +305,73 @@ jobs:
296
305
  KAFKA_DELETE_TOPIC_ENABLE: true
297
306
  steps:
298
307
  - checkout
308
+ - run: sudo apt-get update && sudo apt-get install -y cmake # For installing snappy
309
+ - run: bundle install --path vendor/bundle
310
+ - run: bundle exec rspec --profile --tag functional spec/functional
311
+
312
+ kafka-2.6:
313
+ docker:
314
+ - image: circleci/ruby:2.5.1-node
315
+ environment:
316
+ LOG_LEVEL: DEBUG
317
+ - image: wurstmeister/zookeeper
318
+ - image: wurstmeister/kafka:2.13-2.6.0
319
+ environment:
320
+ KAFKA_ADVERTISED_HOST_NAME: localhost
321
+ KAFKA_ADVERTISED_PORT: 9092
322
+ KAFKA_PORT: 9092
323
+ KAFKA_ZOOKEEPER_CONNECT: localhost:2181
324
+ KAFKA_DELETE_TOPIC_ENABLE: true
325
+ - image: wurstmeister/kafka:2.13-2.6.0
326
+ environment:
327
+ KAFKA_ADVERTISED_HOST_NAME: localhost
328
+ KAFKA_ADVERTISED_PORT: 9093
329
+ KAFKA_PORT: 9093
330
+ KAFKA_ZOOKEEPER_CONNECT: localhost:2181
331
+ KAFKA_DELETE_TOPIC_ENABLE: true
332
+ - image: wurstmeister/kafka:2.13-2.6.0
333
+ environment:
334
+ KAFKA_ADVERTISED_HOST_NAME: localhost
335
+ KAFKA_ADVERTISED_PORT: 9094
336
+ KAFKA_PORT: 9094
337
+ KAFKA_ZOOKEEPER_CONNECT: localhost:2181
338
+ KAFKA_DELETE_TOPIC_ENABLE: true
339
+ steps:
340
+ - checkout
341
+ - run: sudo apt-get update && sudo apt-get install -y cmake # For installing snappy
342
+ - run: bundle install --path vendor/bundle
343
+ - run: bundle exec rspec --profile --tag functional spec/functional
344
+
345
+ kafka-2.7:
346
+ docker:
347
+ - image: circleci/ruby:2.5.1-node
348
+ environment:
349
+ LOG_LEVEL: DEBUG
350
+ - image: wurstmeister/zookeeper
351
+ - image: wurstmeister/kafka:2.13-2.7.0
352
+ environment:
353
+ KAFKA_ADVERTISED_HOST_NAME: localhost
354
+ KAFKA_ADVERTISED_PORT: 9092
355
+ KAFKA_PORT: 9092
356
+ KAFKA_ZOOKEEPER_CONNECT: localhost:2181
357
+ KAFKA_DELETE_TOPIC_ENABLE: true
358
+ - image: wurstmeister/kafka:2.13-2.7.0
359
+ environment:
360
+ KAFKA_ADVERTISED_HOST_NAME: localhost
361
+ KAFKA_ADVERTISED_PORT: 9093
362
+ KAFKA_PORT: 9093
363
+ KAFKA_ZOOKEEPER_CONNECT: localhost:2181
364
+ KAFKA_DELETE_TOPIC_ENABLE: true
365
+ - image: wurstmeister/kafka:2.13-2.7.0
366
+ environment:
367
+ KAFKA_ADVERTISED_HOST_NAME: localhost
368
+ KAFKA_ADVERTISED_PORT: 9094
369
+ KAFKA_PORT: 9094
370
+ KAFKA_ZOOKEEPER_CONNECT: localhost:2181
371
+ KAFKA_DELETE_TOPIC_ENABLE: true
372
+ steps:
373
+ - checkout
374
+ - run: sudo apt-get update && sudo apt-get install -y cmake # For installing snappy
299
375
  - run: bundle install --path vendor/bundle
300
376
  - run: bundle exec rspec --profile --tag functional spec/functional
301
377
 
@@ -313,3 +389,5 @@ workflows:
313
389
  - kafka-2.3
314
390
  - kafka-2.4
315
391
  - kafka-2.5
392
+ - kafka-2.6
393
+ - kafka-2.7
data/CHANGELOG.md CHANGED
@@ -4,6 +4,15 @@ Changes and additions to the library will be listed here.
4
4
 
5
5
  ## Unreleased
6
6
 
7
+ ## 1.4.0
8
+
9
+ - Refresh a stale cluster's metadata if necessary on `Kafka::Client#deliver_message` (#901).
10
+ - Fix `Kafka::TransactionManager#send_offsets_to_txn` (#866).
11
+ - Add support for `murmur2` based partitioning.
12
+ - Add `resolve_seed_brokers` option to support seed brokers' hostname with multiple addresses (#877).
13
+ - Handle SyncGroup responses with a non-zero error and no assignments (#896).
14
+ - Add support for non-identical topic subscriptions within the same consumer group (#525 / #764).
15
+
7
16
  ## 1.3.0
8
17
 
9
18
  - Support custom assignment strategy (#846).
data/README.md CHANGED
@@ -129,6 +129,16 @@ Or install it yourself as:
129
129
  <td>Limited support</td>
130
130
  <td>Limited support</td>
131
131
  </tr>
132
+ <tr>
133
+ <th>Kafka 2.6</th>
134
+ <td>Limited support</td>
135
+ <td>Limited support</td>
136
+ </tr>
137
+ <tr>
138
+ <th>Kafka 2.7</th>
139
+ <td>Limited support</td>
140
+ <td>Limited support</td>
141
+ </tr>
132
142
  </table>
133
143
 
134
144
  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.
@@ -144,6 +154,8 @@ This library is targeting Kafka 0.9 with the v0.4.x series and Kafka 0.10 with t
144
154
  - **Kafka 2.3:** Everything that works with Kafka 2.2 should still work, but so far no features specific to Kafka 2.3 have been added.
145
155
  - **Kafka 2.4:** Everything that works with Kafka 2.3 should still work, but so far no features specific to Kafka 2.4 have been added.
146
156
  - **Kafka 2.5:** Everything that works with Kafka 2.4 should still work, but so far no features specific to Kafka 2.5 have been added.
157
+ - **Kafka 2.6:** Everything that works with Kafka 2.5 should still work, but so far no features specific to Kafka 2.6 have been added.
158
+ - **Kafka 2.7:** Everything that works with Kafka 2.6 should still work, but so far no features specific to Kafka 2.7 have been added.
147
159
 
148
160
  This library requires Ruby 2.1 or higher.
149
161
 
@@ -164,6 +176,12 @@ require "kafka"
164
176
  kafka = Kafka.new(["kafka1:9092", "kafka2:9092"], client_id: "my-application")
165
177
  ```
166
178
 
179
+ You can also use a hostname with seed brokers' IP addresses:
180
+
181
+ ```ruby
182
+ kafka = Kafka.new("seed-brokers:9092", client_id: "my-application", resolve_seed_brokers: true)
183
+ ```
184
+
167
185
  ### Producing Messages to Kafka
168
186
 
169
187
  The simplest way to write a message to a Kafka topic is to call `#deliver_message`:
@@ -370,6 +388,16 @@ partitioner = -> (partition_count, message) { ... }
370
388
  Kafka.new(partitioner: partitioner, ...)
371
389
  ```
372
390
 
391
+ ##### Supported partitioning schemes
392
+
393
+ In order for semantic partitioning to work a `partition_key` must map to the same partition number every time. The general approach, and the one used by this library, is to hash the key and mod it by the number of partitions. There are many different algorithms that can be used to calculate a hash. By default `crc32` is used. `murmur2` is also supported for compatibility with Java based Kafka producers.
394
+
395
+ To use `murmur2` hashing pass it as an argument to `Partitioner`. For example:
396
+
397
+ ```ruby
398
+ Kafka.new(partitioner: Kafka::Partitioner.new(hash_function: :murmur2))
399
+ ```
400
+
373
401
  #### Buffering and Error Handling
374
402
 
375
403
  The producer is designed for resilience in the face of temporary network errors, Kafka broker failovers, and other issues that prevent the client from writing messages to the destination topics. It does this by employing local, in-memory buffers. Only when messages are acknowledged by a Kafka broker will they be removed from the buffer.
@@ -212,31 +212,45 @@ module Kafka
212
212
  @logger.push_tags(@producer.to_s)
213
213
  @logger.info "Starting async producer in the background..."
214
214
 
215
+ do_loop
216
+ rescue Exception => e
217
+ @logger.error "Unexpected Kafka error #{e.class}: #{e.message}\n#{e.backtrace.join("\n")}"
218
+ @logger.error "Async producer crashed!"
219
+ ensure
220
+ @producer.shutdown
221
+ @logger.pop_tags
222
+ end
223
+
224
+ private
225
+
226
+ def do_loop
215
227
  loop do
216
- operation, payload = @queue.pop
217
-
218
- case operation
219
- when :produce
220
- produce(payload[0], **payload[1])
221
- deliver_messages if threshold_reached?
222
- when :deliver_messages
223
- deliver_messages
224
- when :shutdown
225
- begin
226
- # Deliver any pending messages first.
227
- @producer.deliver_messages
228
- rescue Error => e
229
- @logger.error("Failed to deliver messages during shutdown: #{e.message}")
230
-
231
- @instrumenter.instrument("drop_messages.async_producer", {
232
- message_count: @producer.buffer_size + @queue.size,
233
- })
228
+ begin
229
+ operation, payload = @queue.pop
230
+
231
+ case operation
232
+ when :produce
233
+ produce(payload[0], **payload[1])
234
+ deliver_messages if threshold_reached?
235
+ when :deliver_messages
236
+ deliver_messages
237
+ when :shutdown
238
+ begin
239
+ # Deliver any pending messages first.
240
+ @producer.deliver_messages
241
+ rescue Error => e
242
+ @logger.error("Failed to deliver messages during shutdown: #{e.message}")
243
+
244
+ @instrumenter.instrument("drop_messages.async_producer", {
245
+ message_count: @producer.buffer_size + @queue.size,
246
+ })
247
+ end
248
+
249
+ # Stop the run loop.
250
+ break
251
+ else
252
+ raise "Unknown operation #{operation.inspect}"
234
253
  end
235
-
236
- # Stop the run loop.
237
- break
238
- else
239
- raise "Unknown operation #{operation.inspect}"
240
254
  end
241
255
  end
242
256
  rescue Kafka::Error => e
@@ -245,16 +259,8 @@ module Kafka
245
259
 
246
260
  sleep 10
247
261
  retry
248
- rescue Exception => e
249
- @logger.error "Unexpected Kafka error #{e.class}: #{e.message}\n#{e.backtrace.join("\n")}"
250
- @logger.error "Async producer crashed!"
251
- ensure
252
- @producer.shutdown
253
- @logger.pop_tags
254
262
  end
255
263
 
256
- private
257
-
258
264
  def produce(value, **kwargs)
259
265
  retries = 0
260
266
  begin
data/lib/kafka/client.rb CHANGED
@@ -1,3 +1,4 @@
1
+ # coding: utf-8
1
2
  # frozen_string_literal: true
2
3
 
3
4
  require "kafka/ssl_context"
@@ -38,8 +39,8 @@ module Kafka
38
39
  # @param ssl_ca_cert [String, Array<String>, nil] a PEM encoded CA cert, or an Array of
39
40
  # PEM encoded CA certs, to use with an SSL connection.
40
41
  #
41
- # @param ssl_ca_cert_file_path [String, nil] a path on the filesystem to a PEM encoded CA cert
42
- # to use with an SSL connection.
42
+ # @param ssl_ca_cert_file_path [String, Array<String>, nil] a path on the filesystem, or an
43
+ # Array of paths, to PEM encoded CA cert(s) to use with an SSL connection.
43
44
  #
44
45
  # @param ssl_client_cert [String, nil] a PEM encoded client cert to use with an
45
46
  # SSL connection. Must be used in combination with ssl_client_cert_key.
@@ -74,16 +75,22 @@ module Kafka
74
75
  # the SSL certificate and the signing chain of the certificate have the correct domains
75
76
  # based on the CA certificate
76
77
  #
78
+ # @param resolve_seed_brokers [Boolean] whether to resolve each hostname of the seed brokers.
79
+ # If a broker is resolved to multiple IP addresses, the client tries to connect to each
80
+ # of the addresses until it can connect.
81
+ #
77
82
  # @return [Client]
78
83
  def initialize(seed_brokers:, client_id: "ruby-kafka", logger: nil, connect_timeout: nil, socket_timeout: nil,
79
84
  ssl_ca_cert_file_path: nil, ssl_ca_cert: nil, ssl_client_cert: nil, ssl_client_cert_key: nil,
80
85
  ssl_client_cert_key_password: nil, ssl_client_cert_chain: nil, sasl_gssapi_principal: nil,
81
86
  sasl_gssapi_keytab: nil, sasl_plain_authzid: '', sasl_plain_username: nil, sasl_plain_password: nil,
82
87
  sasl_scram_username: nil, sasl_scram_password: nil, sasl_scram_mechanism: nil,
83
- sasl_over_ssl: true, ssl_ca_certs_from_system: false, partitioner: nil, sasl_oauth_token_provider: nil, ssl_verify_hostname: true)
88
+ sasl_over_ssl: true, ssl_ca_certs_from_system: false, partitioner: nil, sasl_oauth_token_provider: nil, ssl_verify_hostname: true,
89
+ resolve_seed_brokers: false)
84
90
  @logger = TaggedLogger.new(logger)
85
91
  @instrumenter = Instrumenter.new(client_id: client_id)
86
92
  @seed_brokers = normalize_seed_brokers(seed_brokers)
93
+ @resolve_seed_brokers = resolve_seed_brokers
87
94
 
88
95
  ssl_context = SslContext.build(
89
96
  ca_cert_file_path: ssl_ca_cert_file_path,
@@ -204,6 +211,8 @@ module Kafka
204
211
  attempt = 1
205
212
 
206
213
  begin
214
+ @cluster.refresh_metadata_if_necessary!
215
+
207
216
  operation.execute
208
217
 
209
218
  unless buffer.empty?
@@ -809,6 +818,7 @@ module Kafka
809
818
  seed_brokers: @seed_brokers,
810
819
  broker_pool: broker_pool,
811
820
  logger: @logger,
821
+ resolve_seed_brokers: @resolve_seed_brokers,
812
822
  )
813
823
  end
814
824
 
data/lib/kafka/cluster.rb CHANGED
@@ -1,6 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require "kafka/broker_pool"
4
+ require "resolv"
4
5
  require "set"
5
6
 
6
7
  module Kafka
@@ -18,7 +19,8 @@ module Kafka
18
19
  # @param seed_brokers [Array<URI>]
19
20
  # @param broker_pool [Kafka::BrokerPool]
20
21
  # @param logger [Logger]
21
- def initialize(seed_brokers:, broker_pool:, logger:)
22
+ # @param resolve_seed_brokers [Boolean] See {Kafka::Client#initialize}
23
+ def initialize(seed_brokers:, broker_pool:, logger:, resolve_seed_brokers: false)
22
24
  if seed_brokers.empty?
23
25
  raise ArgumentError, "At least one seed broker must be configured"
24
26
  end
@@ -26,6 +28,7 @@ module Kafka
26
28
  @logger = TaggedLogger.new(logger)
27
29
  @seed_brokers = seed_brokers
28
30
  @broker_pool = broker_pool
31
+ @resolve_seed_brokers = resolve_seed_brokers
29
32
  @cluster_info = nil
30
33
  @stale = true
31
34
 
@@ -117,7 +120,7 @@ module Kafka
117
120
 
118
121
  # Finds the broker acting as the coordinator of the given group.
119
122
  #
120
- # @param group_id: [String]
123
+ # @param group_id [String]
121
124
  # @return [Broker] the broker that's currently coordinator.
122
125
  def get_group_coordinator(group_id:)
123
126
  @logger.debug "Getting group coordinator for `#{group_id}`"
@@ -127,7 +130,7 @@ module Kafka
127
130
 
128
131
  # Finds the broker acting as the coordinator of the given transaction.
129
132
  #
130
- # @param transactional_id: [String]
133
+ # @param transactional_id [String]
131
134
  # @return [Broker] the broker that's currently coordinator.
132
135
  def get_transaction_coordinator(transactional_id:)
133
136
  @logger.debug "Getting transaction coordinator for `#{transactional_id}`"
@@ -418,32 +421,35 @@ module Kafka
418
421
  # @return [Protocol::MetadataResponse] the cluster metadata.
419
422
  def fetch_cluster_info
420
423
  errors = []
421
-
422
424
  @seed_brokers.shuffle.each do |node|
423
- @logger.info "Fetching cluster metadata from #{node}"
424
-
425
- begin
426
- broker = @broker_pool.connect(node.hostname, node.port)
427
- cluster_info = broker.fetch_metadata(topics: @target_topics)
428
-
429
- if cluster_info.brokers.empty?
430
- @logger.error "No brokers in cluster"
431
- else
432
- @logger.info "Discovered cluster metadata; nodes: #{cluster_info.brokers.join(', ')}"
433
-
434
- @stale = false
435
-
436
- return cluster_info
425
+ (@resolve_seed_brokers ? Resolv.getaddresses(node.hostname).shuffle : [node.hostname]).each do |hostname_or_ip|
426
+ node_info = node.to_s
427
+ node_info << " (#{hostname_or_ip})" if node.hostname != hostname_or_ip
428
+ @logger.info "Fetching cluster metadata from #{node_info}"
429
+
430
+ begin
431
+ broker = @broker_pool.connect(hostname_or_ip, node.port)
432
+ cluster_info = broker.fetch_metadata(topics: @target_topics)
433
+
434
+ if cluster_info.brokers.empty?
435
+ @logger.error "No brokers in cluster"
436
+ else
437
+ @logger.info "Discovered cluster metadata; nodes: #{cluster_info.brokers.join(', ')}"
438
+
439
+ @stale = false
440
+
441
+ return cluster_info
442
+ end
443
+ rescue Error => e
444
+ @logger.error "Failed to fetch metadata from #{node_info}: #{e}"
445
+ errors << [node_info, e]
446
+ ensure
447
+ broker.disconnect unless broker.nil?
437
448
  end
438
- rescue Error => e
439
- @logger.error "Failed to fetch metadata from #{node}: #{e}"
440
- errors << [node, e]
441
- ensure
442
- broker.disconnect unless broker.nil?
443
449
  end
444
450
  end
445
451
 
446
- error_description = errors.map {|node, exception| "- #{node}: #{exception}" }.join("\n")
452
+ error_description = errors.map {|node_info, exception| "- #{node_info}: #{exception}" }.join("\n")
447
453
 
448
454
  raise ConnectionError, "Could not connect to any of the seed brokers:\n#{error_description}"
449
455
  end
@@ -189,9 +189,14 @@ module Kafka
189
189
  if group_leader?
190
190
  @logger.info "Chosen as leader of group `#{@group_id}`"
191
191
 
192
+ topics = Set.new
193
+ @members.each do |_member, metadata|
194
+ metadata.topics.each { |t| topics.add(t) }
195
+ end
196
+
192
197
  group_assignment = @assignor.assign(
193
198
  members: @members,
194
- topics: @topics,
199
+ topics: topics,
195
200
  )
196
201
  end
197
202
 
@@ -0,0 +1,15 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "zlib"
4
+
5
+ module Kafka
6
+ class Crc32Hash
7
+
8
+ # crc32 is supported natively
9
+ def load; end
10
+
11
+ def hash(value)
12
+ Zlib.crc32(value)
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,22 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "kafka/crc32_hash"
4
+ require "kafka/murmur2_hash"
5
+
6
+ module Kafka
7
+ module Digest
8
+ FUNCTIONS_BY_NAME = {
9
+ :crc32 => Crc32Hash.new,
10
+ :murmur2 => Murmur2Hash.new
11
+ }.freeze
12
+
13
+ def self.find_digest(name)
14
+ digest = FUNCTIONS_BY_NAME.fetch(name) do
15
+ raise LoadError, "Unknown hash function #{name}"
16
+ end
17
+
18
+ digest.load
19
+ digest
20
+ end
21
+ end
22
+ end
@@ -0,0 +1,17 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Kafka
4
+ class Murmur2Hash
5
+ SEED = [0x9747b28c].pack('L')
6
+
7
+ def load
8
+ require 'digest/murmurhash'
9
+ rescue LoadError
10
+ raise LoadError, "using murmur2 hashing requires adding a dependency on the `digest-murmurhash` gem to your Gemfile."
11
+ end
12
+
13
+ def hash(value)
14
+ ::Digest::MurmurHash2.rawdigest(value, SEED) & 0x7fffffff
15
+ end
16
+ end
17
+ end
@@ -1,11 +1,16 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require "zlib"
3
+ require "kafka/digest"
4
4
 
5
5
  module Kafka
6
6
 
7
7
  # Assigns partitions to messages.
8
8
  class Partitioner
9
+ # @param hash_function [Symbol, nil] the algorithm used to compute a messages
10
+ # destination partition. Default is :crc32
11
+ def initialize(hash_function: nil)
12
+ @digest = Digest.find_digest(hash_function || :crc32)
13
+ end
9
14
 
10
15
  # Assigns a partition number based on a partition key. If no explicit
11
16
  # partition key is provided, the message key will be used instead.
@@ -28,7 +33,7 @@ module Kafka
28
33
  if key.nil?
29
34
  rand(partition_count)
30
35
  else
31
- Zlib.crc32(key) % partition_count
36
+ @digest.hash(key) % partition_count
32
37
  end
33
38
  end
34
39
  end
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Kafka
2
4
  module Protocol
3
5
  class AddOffsetsToTxnResponse
@@ -126,7 +126,7 @@ module Kafka
126
126
  # Writes an integer under varints serializing to the IO object.
127
127
  # https://developers.google.com/protocol-buffers/docs/encoding#varints
128
128
  #
129
- # @param string [Integer]
129
+ # @param int [Integer]
130
130
  # @return [nil]
131
131
  def write_varint(int)
132
132
  int = int << 1
@@ -77,7 +77,7 @@ module Kafka
77
77
  record_batch_encoder.write_int8(MAGIC_BYTE)
78
78
 
79
79
  body = encode_record_batch_body
80
- crc = Digest::CRC32c.checksum(body)
80
+ crc = ::Digest::CRC32c.checksum(body)
81
81
 
82
82
  record_batch_encoder.write_int32(crc)
83
83
  record_batch_encoder.write(body)
@@ -213,7 +213,7 @@ module Kafka
213
213
  end
214
214
 
215
215
  def mark_control_record
216
- if in_transaction && is_control_batch
216
+ if is_control_batch
217
217
  record = @records.first
218
218
  record.is_control_record = true unless record.nil?
219
219
  end
@@ -13,9 +13,12 @@ module Kafka
13
13
  end
14
14
 
15
15
  def self.decode(decoder)
16
+ error_code = decoder.int16
17
+ member_assignment_bytes = decoder.bytes
18
+
16
19
  new(
17
- error_code: decoder.int16,
18
- member_assignment: MemberAssignment.decode(Decoder.from_string(decoder.bytes)),
20
+ error_code: error_code,
21
+ member_assignment: member_assignment_bytes ? MemberAssignment.decode(Decoder.from_string(member_assignment_bytes)) : nil
19
22
  )
20
23
  end
21
24
  end
@@ -1,17 +1,46 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Kafka
2
4
  module Protocol
3
5
  class TxnOffsetCommitResponse
6
+ class PartitionError
7
+ attr_reader :partition, :error_code
8
+
9
+ def initialize(partition:, error_code:)
10
+ @partition = partition
11
+ @error_code = error_code
12
+ end
13
+ end
14
+
15
+ class TopicPartitionsError
16
+ attr_reader :topic, :partitions
17
+
18
+ def initialize(topic:, partitions:)
19
+ @topic = topic
20
+ @partitions = partitions
21
+ end
22
+ end
4
23
 
5
- attr_reader :error_code
24
+ attr_reader :errors
6
25
 
7
- def initialize(error_code:)
8
- @error_code = error_code
26
+ def initialize(errors:)
27
+ @errors = errors
9
28
  end
10
29
 
11
30
  def self.decode(decoder)
12
31
  _throttle_time_ms = decoder.int32
13
- error_code = decoder.int16
14
- new(error_code: error_code)
32
+ errors = decoder.array do
33
+ TopicPartitionsError.new(
34
+ topic: decoder.string,
35
+ partitions: decoder.array do
36
+ PartitionError.new(
37
+ partition: decoder.int32,
38
+ error_code: decoder.int16
39
+ )
40
+ end
41
+ )
42
+ end
43
+ new(errors: errors)
15
44
  end
16
45
  end
17
46
  end
@@ -1,9 +1,9 @@
1
- # frozen_string_literal: true
2
-
3
1
  module Kafka
4
2
 
5
- # A consumer group partition assignment strategy that assigns partitions to
6
- # consumers in a round-robin fashion.
3
+ # A round robin assignment strategy inpired on the
4
+ # original java client round robin assignor. It's capable
5
+ # of handling identical as well as different topic subscriptions
6
+ # accross the same consumer group.
7
7
  class RoundRobinAssignmentStrategy
8
8
  def protocol_name
9
9
  "roundrobin"
@@ -19,13 +19,34 @@ module Kafka
19
19
  # @return [Hash<String, Array<Kafka::ConsumerGroup::Assignor::Partition>] a hash
20
20
  # mapping member ids to partitions.
21
21
  def call(cluster:, members:, partitions:)
22
- member_ids = members.keys
23
22
  partitions_per_member = Hash.new {|h, k| h[k] = [] }
24
- partitions.each_with_index do |partition, index|
25
- partitions_per_member[member_ids[index % member_ids.count]] << partition
23
+ relevant_partitions = valid_sorted_partitions(members, partitions)
24
+ members_ids = members.keys
25
+ iterator = (0...members.size).cycle
26
+ idx = iterator.next
27
+
28
+ relevant_partitions.each do |partition|
29
+ topic = partition.topic
30
+
31
+ while !members[members_ids[idx]].topics.include?(topic)
32
+ idx = iterator.next
33
+ end
34
+
35
+ partitions_per_member[members_ids[idx]] << partition
36
+ idx = iterator.next
26
37
  end
27
38
 
28
39
  partitions_per_member
29
40
  end
41
+
42
+ def valid_sorted_partitions(members, partitions)
43
+ subscribed_topics = members.map do |id, metadata|
44
+ metadata && metadata.topics
45
+ end.flatten.compact
46
+
47
+ partitions
48
+ .select { |partition| subscribed_topics.include?(partition.topic) }
49
+ .sort_by { |partition| partition.topic }
50
+ end
30
51
  end
31
52
  end
@@ -47,8 +47,8 @@ module Kafka
47
47
  Array(ca_cert).each do |cert|
48
48
  store.add_cert(OpenSSL::X509::Certificate.new(cert))
49
49
  end
50
- if ca_cert_file_path
51
- store.add_file(ca_cert_file_path)
50
+ Array(ca_cert_file_path).each do |cert_file_path|
51
+ store.add_file(cert_file_path)
52
52
  end
53
53
  if ca_certs_from_system
54
54
  store.set_default_paths
@@ -233,14 +233,23 @@ module Kafka
233
233
  )
234
234
  Protocol.handle_error(add_response.error_code)
235
235
 
236
- send_response = transaction_coordinator.txn_offset_commit(
236
+ send_response = group_coordinator(group_id: group_id).txn_offset_commit(
237
237
  transactional_id: @transactional_id,
238
238
  group_id: group_id,
239
239
  producer_id: @producer_id,
240
240
  producer_epoch: @producer_epoch,
241
241
  offsets: offsets
242
242
  )
243
- Protocol.handle_error(send_response.error_code)
243
+ send_response.errors.each do |tp|
244
+ tp.partitions.each do |partition|
245
+ Protocol.handle_error(partition.error_code)
246
+ end
247
+ end
248
+
249
+ nil
250
+ rescue
251
+ @transaction_state.transition_to!(TransactionStateMachine::ERROR)
252
+ raise
244
253
  end
245
254
 
246
255
  def in_transaction?
@@ -283,6 +292,12 @@ module Kafka
283
292
  )
284
293
  end
285
294
 
295
+ def group_coordinator(group_id:)
296
+ @cluster.get_group_coordinator(
297
+ group_id: group_id
298
+ )
299
+ end
300
+
286
301
  def complete_transaction
287
302
  @transaction_state.transition_to!(TransactionStateMachine::READY)
288
303
  @transaction_partitions = {}
data/lib/kafka/version.rb CHANGED
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Kafka
4
- VERSION = "1.3.0"
4
+ VERSION = "1.4.0"
5
5
  end
data/ruby-kafka.gemspec CHANGED
@@ -33,6 +33,7 @@ Gem::Specification.new do |spec|
33
33
  spec.add_development_dependency "rake", "~> 10.0"
34
34
  spec.add_development_dependency "rspec"
35
35
  spec.add_development_dependency "pry"
36
+ spec.add_development_dependency "digest-murmurhash"
36
37
  spec.add_development_dependency "dotenv"
37
38
  spec.add_development_dependency "docker-api"
38
39
  spec.add_development_dependency "rspec-benchmark"
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: ruby-kafka
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.3.0
4
+ version: 1.4.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Daniel Schierbeck
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2020-10-14 00:00:00.000000000 Z
11
+ date: 2021-08-25 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: digest-crc
@@ -80,6 +80,20 @@ dependencies:
80
80
  - - ">="
81
81
  - !ruby/object:Gem::Version
82
82
  version: '0'
83
+ - !ruby/object:Gem::Dependency
84
+ name: digest-murmurhash
85
+ requirement: !ruby/object:Gem::Requirement
86
+ requirements:
87
+ - - ">="
88
+ - !ruby/object:Gem::Version
89
+ version: '0'
90
+ type: :development
91
+ prerelease: false
92
+ version_requirements: !ruby/object:Gem::Requirement
93
+ requirements:
94
+ - - ">="
95
+ - !ruby/object:Gem::Version
96
+ version: '0'
83
97
  - !ruby/object:Gem::Dependency
84
98
  name: dotenv
85
99
  requirement: !ruby/object:Gem::Requirement
@@ -377,7 +391,9 @@ files:
377
391
  - lib/kafka/consumer.rb
378
392
  - lib/kafka/consumer_group.rb
379
393
  - lib/kafka/consumer_group/assignor.rb
394
+ - lib/kafka/crc32_hash.rb
380
395
  - lib/kafka/datadog.rb
396
+ - lib/kafka/digest.rb
381
397
  - lib/kafka/fetch_operation.rb
382
398
  - lib/kafka/fetched_batch.rb
383
399
  - lib/kafka/fetched_batch_generator.rb
@@ -390,6 +406,7 @@ files:
390
406
  - lib/kafka/interceptors.rb
391
407
  - lib/kafka/lz4_codec.rb
392
408
  - lib/kafka/message_buffer.rb
409
+ - lib/kafka/murmur2_hash.rb
393
410
  - lib/kafka/offset_manager.rb
394
411
  - lib/kafka/partitioner.rb
395
412
  - lib/kafka/pause.rb