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 +4 -4
- data/.circleci/config.yml +78 -0
- data/CHANGELOG.md +9 -0
- data/README.md +28 -0
- data/lib/kafka/async_producer.rb +37 -31
- data/lib/kafka/client.rb +13 -3
- data/lib/kafka/cluster.rb +30 -24
- data/lib/kafka/consumer_group.rb +6 -1
- data/lib/kafka/crc32_hash.rb +15 -0
- data/lib/kafka/digest.rb +22 -0
- data/lib/kafka/murmur2_hash.rb +17 -0
- data/lib/kafka/partitioner.rb +7 -2
- data/lib/kafka/protocol/add_offsets_to_txn_response.rb +2 -0
- data/lib/kafka/protocol/encoder.rb +1 -1
- data/lib/kafka/protocol/record_batch.rb +2 -2
- data/lib/kafka/protocol/sync_group_response.rb +5 -2
- data/lib/kafka/protocol/txn_offset_commit_response.rb +34 -5
- data/lib/kafka/round_robin_assignment_strategy.rb +28 -7
- data/lib/kafka/ssl_context.rb +2 -2
- data/lib/kafka/transaction_manager.rb +17 -2
- data/lib/kafka/version.rb +1 -1
- data/ruby-kafka.gemspec +1 -0
- metadata +19 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: fd754956b907d18ac2a17b3d8f5bc374a8851dea21826277b8941f163403161d
|
4
|
+
data.tar.gz: 7a1b74d4c3d3f8cfb0772bd6521a8a5c726bc1efc5ade83f814802ce08fb0f24
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
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.
|
data/lib/kafka/async_producer.rb
CHANGED
@@ -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
|
-
|
217
|
-
|
218
|
-
|
219
|
-
|
220
|
-
produce
|
221
|
-
|
222
|
-
|
223
|
-
deliver_messages
|
224
|
-
|
225
|
-
|
226
|
-
|
227
|
-
|
228
|
-
|
229
|
-
|
230
|
-
|
231
|
-
|
232
|
-
|
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
|
42
|
-
#
|
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
|
-
|
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
|
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
|
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
|
-
@
|
424
|
-
|
425
|
-
|
426
|
-
|
427
|
-
|
428
|
-
|
429
|
-
|
430
|
-
|
431
|
-
|
432
|
-
|
433
|
-
|
434
|
-
|
435
|
-
|
436
|
-
|
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 {|
|
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
|
data/lib/kafka/consumer_group.rb
CHANGED
@@ -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:
|
199
|
+
topics: topics,
|
195
200
|
)
|
196
201
|
end
|
197
202
|
|
data/lib/kafka/digest.rb
ADDED
@@ -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
|
data/lib/kafka/partitioner.rb
CHANGED
@@ -1,11 +1,16 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
-
require "
|
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
|
-
|
36
|
+
@digest.hash(key) % partition_count
|
32
37
|
end
|
33
38
|
end
|
34
39
|
end
|
@@ -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
|
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
|
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:
|
18
|
-
member_assignment: MemberAssignment.decode(Decoder.from_string(
|
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 :
|
24
|
+
attr_reader :errors
|
6
25
|
|
7
|
-
def initialize(
|
8
|
-
@
|
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
|
-
|
14
|
-
|
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
|
6
|
-
#
|
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
|
-
|
25
|
-
|
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
|
data/lib/kafka/ssl_context.rb
CHANGED
@@ -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
|
-
|
51
|
-
store.add_file(
|
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 =
|
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
|
-
|
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
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.
|
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:
|
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
|