racecar 2.8.2 → 2.9.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.github/workflows/ci.yml +12 -8
- data/.github/workflows/publish.yml +12 -0
- data/CHANGELOG.md +11 -2
- data/Gemfile +3 -3
- data/Gemfile.lock +37 -32
- data/README.md +146 -3
- data/Rakefile +3 -0
- data/examples/batch_consumer.rb +2 -2
- data/lib/racecar/cli.rb +13 -1
- data/lib/racecar/config.rb +53 -0
- data/lib/racecar/consumer_set.rb +5 -2
- data/lib/racecar/ctl.rb +11 -0
- data/lib/racecar/daemon.rb +2 -2
- data/lib/racecar/datadog.rb +65 -0
- data/lib/racecar/delivery_callback.rb +27 -0
- data/lib/racecar/instrumenter.rb +2 -9
- data/lib/racecar/liveness_probe.rb +78 -0
- data/lib/racecar/parallel_runner.rb +4 -0
- data/lib/racecar/producer.rb +139 -0
- data/lib/racecar/rebalance_listener.rb +22 -0
- data/lib/racecar/runner.rb +7 -11
- data/lib/racecar/version.rb +1 -1
- data/lib/racecar.rb +28 -10
- data/racecar.gemspec +2 -2
- metadata +11 -13
- data/.circleci/config.yml +0 -56
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: b394d31edcb9c83e562811ba748b6c6915388d0227a4e7425aa9aa13f64e3890
|
4
|
+
data.tar.gz: 1dfcd7046b6d932f716246d0685589c1635af115a4e126067c9775b13fe05464
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 59043be21e411e680c11815b583236556413a539f5aa9508a460eefe82cee6199f56a30b9fa07a3f206886d5dc811b210cea6130813a7073a68fe6612343ca0d
|
7
|
+
data.tar.gz: d6692b9bb7cdc27efe5272a10fa1d9061920084a7b517699e5133f5e31e9fd50415e72ee40550e07e5a2fed028aeabb741422fef47372c7a53e0a96bdfc1b090
|
data/.github/workflows/ci.yml
CHANGED
@@ -12,30 +12,34 @@ jobs:
|
|
12
12
|
|
13
13
|
strategy:
|
14
14
|
matrix:
|
15
|
-
ruby-version:
|
15
|
+
ruby-version:
|
16
|
+
- "2.7"
|
17
|
+
- "3.0"
|
18
|
+
- "3.1"
|
19
|
+
- "3.2"
|
16
20
|
|
17
21
|
steps:
|
18
|
-
- uses: zendesk/checkout@
|
22
|
+
- uses: zendesk/checkout@v3
|
19
23
|
- name: Set up Ruby
|
20
|
-
uses: zendesk/setup-ruby@v1
|
24
|
+
uses: zendesk/setup-ruby@v1
|
21
25
|
with:
|
22
26
|
ruby-version: ${{ matrix.ruby-version }}
|
23
27
|
bundler-cache: true
|
24
28
|
- name: Build and test with RSpec
|
25
|
-
run: bundle exec rspec --
|
29
|
+
run: bundle exec rspec --exclude-pattern='spec/integration/*_spec.rb'
|
26
30
|
|
27
31
|
integration-specs:
|
28
32
|
runs-on: ubuntu-latest
|
29
33
|
steps:
|
30
|
-
- uses: zendesk/checkout@
|
34
|
+
- uses: zendesk/checkout@v3
|
31
35
|
- name: Set up Ruby
|
32
|
-
uses: zendesk/setup-ruby@v1
|
36
|
+
uses: zendesk/setup-ruby@v1
|
33
37
|
with:
|
34
|
-
ruby-version: 2.7
|
38
|
+
ruby-version: "2.7"
|
35
39
|
bundler-cache: true
|
36
40
|
- name: Bring up docker-compose stack
|
37
41
|
run: docker-compose up -d
|
38
42
|
- name: Build and test with RSpec
|
39
43
|
env:
|
40
44
|
RACECAR_BROKERS: localhost:9092
|
41
|
-
run: timeout --kill-after 180 150 bundle exec rspec
|
45
|
+
run: timeout --kill-after 180 150 bundle exec rspec spec/integration/*_spec.rb
|
@@ -0,0 +1,12 @@
|
|
1
|
+
name: Publish Gem
|
2
|
+
|
3
|
+
on:
|
4
|
+
push:
|
5
|
+
tags: v*
|
6
|
+
|
7
|
+
jobs:
|
8
|
+
call-workflow:
|
9
|
+
uses: zendesk/gw/.github/workflows/ruby-gem-publication.yml@main
|
10
|
+
secrets:
|
11
|
+
RUBY_GEMS_API_KEY: ${{ secrets.RUBY_GEMS_API_KEY }}
|
12
|
+
RUBY_GEMS_TOTP_DEVICE: ${{ secrets.RUBY_GEMS_TOTP_DEVICE }}
|
data/CHANGELOG.md
CHANGED
@@ -2,14 +2,23 @@
|
|
2
2
|
|
3
3
|
## Unreleased
|
4
4
|
|
5
|
+
* Test with Ruby 3.2
|
6
|
+
|
7
|
+
## v2.9.0, v2.9.0.beta1
|
8
|
+
|
9
|
+
* Add `partitioner` producer config option to allow changing the strategy to
|
10
|
+
determine which topic partition a message is written to when racecar
|
11
|
+
produces a kafka message
|
12
|
+
* Add built-in liveness probe for Kubernetes deployments.
|
13
|
+
|
5
14
|
## v2.8.2
|
6
|
-
* Handles ErroneousStateError, in previous versions the consumer would do several unecessary group leave/joins. The log level is also changed to WARN instead of ERROR. ([#
|
15
|
+
* Handles ErroneousStateError, in previous versions the consumer would do several unecessary group leave/joins. The log level is also changed to WARN instead of ERROR. ([#295](https://github.com/zendesk/racecar/pull/295))
|
7
16
|
|
8
17
|
## v2.8.1
|
9
18
|
* Adds new ErroneousStateError to racecar in order to give more information on this new possible exception.
|
10
19
|
|
11
20
|
## v2.8.0
|
12
|
-
* Update librdkafka version from 1.8.2 to 1.9.0 by upgrading from rdkafka 0.10.0 to 0.12.0. ([#
|
21
|
+
* Update librdkafka version from 1.8.2 to 1.9.0 by upgrading from rdkafka 0.10.0 to 0.12.0. ([#293](https://github.com/zendesk/racecar/pull/293))
|
13
22
|
|
14
23
|
## v2.7.0
|
15
24
|
|
data/Gemfile
CHANGED
@@ -5,6 +5,6 @@ source 'https://rubygems.org'
|
|
5
5
|
# Specify your gem's dependencies in racecar.gemspec
|
6
6
|
gemspec
|
7
7
|
|
8
|
-
# We actually support version
|
9
|
-
# on Ruby 2.
|
10
|
-
gem 'activesupport', '
|
8
|
+
# We actually support version 7.x (see gemspec); this extra restriction is added just for running the test suite also
|
9
|
+
# on Ruby 2.6, which activesupport 7.0 does not support.
|
10
|
+
gem 'activesupport', '~> 6.1.0'
|
data/Gemfile.lock
CHANGED
@@ -1,67 +1,72 @@
|
|
1
1
|
PATH
|
2
2
|
remote: .
|
3
3
|
specs:
|
4
|
-
racecar (2.
|
4
|
+
racecar (2.9.0.beta1)
|
5
5
|
king_konf (~> 1.0.0)
|
6
6
|
rdkafka (~> 0.12.0)
|
7
7
|
|
8
8
|
GEM
|
9
9
|
remote: https://rubygems.org/
|
10
10
|
specs:
|
11
|
-
activesupport (
|
11
|
+
activesupport (6.1.7.3)
|
12
12
|
concurrent-ruby (~> 1.0, >= 1.0.2)
|
13
|
-
i18n (>=
|
14
|
-
minitest (
|
15
|
-
tzinfo (~>
|
13
|
+
i18n (>= 1.6, < 2)
|
14
|
+
minitest (>= 5.1)
|
15
|
+
tzinfo (~> 2.0)
|
16
|
+
zeitwerk (~> 2.3)
|
17
|
+
byebug (11.1.3)
|
16
18
|
coderay (1.1.3)
|
17
|
-
concurrent-ruby (1.
|
18
|
-
diff-lcs (1.
|
19
|
-
dogstatsd-ruby (5.
|
19
|
+
concurrent-ruby (1.2.2)
|
20
|
+
diff-lcs (1.5.0)
|
21
|
+
dogstatsd-ruby (5.5.0)
|
20
22
|
ffi (1.15.5)
|
21
|
-
i18n (1.
|
23
|
+
i18n (1.12.0)
|
22
24
|
concurrent-ruby (~> 1.0)
|
23
|
-
king_konf (1.0.
|
25
|
+
king_konf (1.0.1)
|
24
26
|
method_source (1.0.0)
|
25
|
-
mini_portile2 (2.8.
|
26
|
-
minitest (5.
|
27
|
-
pry (0.
|
27
|
+
mini_portile2 (2.8.1)
|
28
|
+
minitest (5.18.0)
|
29
|
+
pry (0.14.2)
|
28
30
|
coderay (~> 1.1)
|
29
31
|
method_source (~> 1.0)
|
30
|
-
|
32
|
+
pry-byebug (3.10.1)
|
33
|
+
byebug (~> 11.0)
|
34
|
+
pry (>= 0.13, < 0.15)
|
35
|
+
rake (13.0.6)
|
31
36
|
rdkafka (0.12.0)
|
32
37
|
ffi (~> 1.15)
|
33
38
|
mini_portile2 (~> 2.6)
|
34
39
|
rake (> 12)
|
35
|
-
rspec (3.
|
36
|
-
rspec-core (~> 3.
|
37
|
-
rspec-expectations (~> 3.
|
38
|
-
rspec-mocks (~> 3.
|
39
|
-
rspec-core (3.
|
40
|
-
rspec-support (~> 3.
|
41
|
-
rspec-expectations (3.
|
40
|
+
rspec (3.12.0)
|
41
|
+
rspec-core (~> 3.12.0)
|
42
|
+
rspec-expectations (~> 3.12.0)
|
43
|
+
rspec-mocks (~> 3.12.0)
|
44
|
+
rspec-core (3.12.1)
|
45
|
+
rspec-support (~> 3.12.0)
|
46
|
+
rspec-expectations (3.12.2)
|
42
47
|
diff-lcs (>= 1.2.0, < 2.0)
|
43
|
-
rspec-support (~> 3.
|
44
|
-
rspec-mocks (3.
|
48
|
+
rspec-support (~> 3.12.0)
|
49
|
+
rspec-mocks (3.12.4)
|
45
50
|
diff-lcs (>= 1.2.0, < 2.0)
|
46
|
-
rspec-support (~> 3.
|
47
|
-
rspec-support (3.
|
48
|
-
|
49
|
-
|
50
|
-
|
51
|
-
|
51
|
+
rspec-support (~> 3.12.0)
|
52
|
+
rspec-support (3.12.0)
|
53
|
+
timecop (0.9.6)
|
54
|
+
tzinfo (2.0.6)
|
55
|
+
concurrent-ruby (~> 1.0)
|
56
|
+
zeitwerk (2.6.7)
|
52
57
|
|
53
58
|
PLATFORMS
|
54
59
|
ruby
|
55
60
|
|
56
61
|
DEPENDENCIES
|
57
|
-
activesupport (
|
62
|
+
activesupport (~> 6.1.0)
|
58
63
|
bundler (>= 1.13, < 3)
|
59
64
|
dogstatsd-ruby (>= 4.0.0, < 6.0.0)
|
60
|
-
pry
|
65
|
+
pry-byebug
|
61
66
|
racecar!
|
62
67
|
rake (> 10.0)
|
63
68
|
rspec (~> 3.0)
|
64
69
|
timecop
|
65
70
|
|
66
71
|
BUNDLED WITH
|
67
|
-
2.
|
72
|
+
2.4.9
|
data/README.md
CHANGED
@@ -20,6 +20,7 @@ The framework is based on [rdkafka-ruby](https://github.com/appsignal/rdkafka-ru
|
|
20
20
|
8. [Logging](#logging)
|
21
21
|
9. [Operations](#operations)
|
22
22
|
10. [Upgrading from v1 to v2](#upgrading-from-v1-to-v2)
|
23
|
+
11. [Compression](#compression)
|
23
24
|
3. [Development](#development)
|
24
25
|
4. [Contributing](#contributing)
|
25
26
|
5. [Support and Discussion](#support-and-discussion)
|
@@ -245,6 +246,71 @@ The `deliver!` method can be used to block until the broker received all queued
|
|
245
246
|
|
246
247
|
You can set message headers by passing a `headers:` option with a Hash of headers.
|
247
248
|
|
249
|
+
### Standalone Producer
|
250
|
+
|
251
|
+
Racecar provides a standalone producer to publish messages to Kafka directly from your Rails application:
|
252
|
+
|
253
|
+
```ruby
|
254
|
+
# app/controllers/comments_controller.rb
|
255
|
+
class CommentsController < ApplicationController
|
256
|
+
def create
|
257
|
+
@comment = Comment.create!(params)
|
258
|
+
|
259
|
+
# This will publish a JSON representation of the comment to the `comments` topic
|
260
|
+
# in Kafka. Make sure to create the topic first, or this may fail.
|
261
|
+
Racecar.produce_sync(value:comment.to_json, topic: "comments")
|
262
|
+
end
|
263
|
+
end
|
264
|
+
```
|
265
|
+
|
266
|
+
The above example will block the server process until the message has been delivered. If you want deliveries to happen in the background in order to free up your server processes more quickly, call #deliver_async instead:
|
267
|
+
|
268
|
+
```ruby
|
269
|
+
# app/controllers/comments_controller.rb
|
270
|
+
class CommentsController < ApplicationController
|
271
|
+
def show
|
272
|
+
@comment = Comment.find(params[:id])
|
273
|
+
|
274
|
+
event = {
|
275
|
+
name: "comment_viewed",
|
276
|
+
data: {
|
277
|
+
comment_id: @comment.id,
|
278
|
+
user_id: current_user.id
|
279
|
+
}
|
280
|
+
}
|
281
|
+
|
282
|
+
# By delivering messages asynchronously you free up your server processes faster.
|
283
|
+
Racecar.produce_async(value: event.to_json, topic: "activity")
|
284
|
+
end
|
285
|
+
end
|
286
|
+
```
|
287
|
+
In addition to improving response time, delivering messages asynchronously also protects your application against Kafka availability issues -- if messages cannot be delivered, they'll be buffered for later and retried automatically.
|
288
|
+
|
289
|
+
A third method is to produce messages first (without delivering the messages to Kafka yet), and deliver them synchronously later:
|
290
|
+
|
291
|
+
```ruby
|
292
|
+
# app/controllers/comments_controller.rb
|
293
|
+
class CommentsController < ApplicationController
|
294
|
+
def create
|
295
|
+
@comment = Comment.create!(params)
|
296
|
+
|
297
|
+
event = {
|
298
|
+
name: "comment_created",
|
299
|
+
data: {
|
300
|
+
comment_id: @comment.id
|
301
|
+
user_id: current_user.id
|
302
|
+
}
|
303
|
+
}
|
304
|
+
|
305
|
+
# This will queue the two messages in the internal buffer and block server process until they are delivered.
|
306
|
+
Racecar.wait_for_delivery do
|
307
|
+
Racecar.produce_async(comment.to_json, topic: "comments")
|
308
|
+
Racecar.produce_async(event.to_json, topic: "activity")
|
309
|
+
end
|
310
|
+
end
|
311
|
+
end
|
312
|
+
```
|
313
|
+
|
248
314
|
### Configuration
|
249
315
|
|
250
316
|
Racecar provides a flexible way to configure your consumer in a way that feels at home in a Rails application. If you haven't already, run `bundle exec rails generate racecar:install` in order to generate a config file. You'll get a separate section for each Rails environment, with the common configuration values in a shared `common` section.
|
@@ -338,6 +404,7 @@ Racecar has support for using SASL to authenticate clients using either the GSSA
|
|
338
404
|
|
339
405
|
These settings are related to consumers that _produce messages to Kafka_.
|
340
406
|
|
407
|
+
- `partitioner` – The strategy used to determine which topic partition a message is written to when Racecar produces a value to Kafka. The codec needs to be one of `consistent`, `consistent_random` `murmur2` `murmur2_random` `fnv1a` `fnv1a_random` either as a Symbol or a String, defaults to `consistent_random`
|
341
408
|
- `producer_compression_codec` – If defined, Racecar will compress messages before writing them to Kafka. The codec needs to be one of `gzip`, `lz4`, or `snappy`, either as a Symbol or a String.
|
342
409
|
|
343
410
|
#### Datadog monitoring
|
@@ -350,7 +417,7 @@ Racecar supports [Datadog](https://www.datadoghq.com/) monitoring integration. I
|
|
350
417
|
- `datadog_namespace` – The namespace to use for Datadog metrics.
|
351
418
|
- `datadog_tags` – Tags that should always be set on Datadog metrics.
|
352
419
|
|
353
|
-
Furthermore, there's a [standard Datadog dashboard
|
420
|
+
Furthermore, there's a [standard Datadog dashboard configuration file](https://raw.githubusercontent.com/zendesk/racecar/master/extra/datadog-dashboard.json) that you can import to get started with a Racecar dashboard for all of your consumers.
|
354
421
|
|
355
422
|
#### Consumers Without Rails
|
356
423
|
|
@@ -443,6 +510,64 @@ The important part is the `strategy.type` value, which tells Kubernetes how to u
|
|
443
510
|
|
444
511
|
Instead, the `Recreate` update strategy should be used. It completely tears down the existing containers before starting all of the new containers simultaneously, allowing for a single synchronization stage and a much faster, more stable deployment update.
|
445
512
|
|
513
|
+
#### Liveness Probe
|
514
|
+
|
515
|
+
Racecar comes with a built-in liveness probe, primarily for use with Kubernetes, but useful for any deployment environment where you can periodically run a process to check the health of your consumer.
|
516
|
+
|
517
|
+
To use this feature:
|
518
|
+
- set the `liveness_probe_enabled` config option to true.
|
519
|
+
- configure your Kubernetes deployment to run `$ racecarctl liveness_probe`
|
520
|
+
|
521
|
+
|
522
|
+
When enabled (see config) Racecar will touch the file at `liveness_probe_file_path` each time it finishes polling Kafka and processing the messages in the batch (if any).
|
523
|
+
|
524
|
+
The modified time of this file can be observed to determine when the consumer last exhibited 'liveness'.
|
525
|
+
|
526
|
+
Running `racecarctl liveness_probe` will return a successful exit status if the last 'liveness' event happened within an acceptable time, `liveness_probe_max_interval`.
|
527
|
+
|
528
|
+
`liveness_probe_max_interval` should be long enough to account for both the Kafka polling time of `max_wait_time` and the processing time of a full message batch.
|
529
|
+
|
530
|
+
On receiving `SIGTERM`, Racecar will gracefully shut down and delete this file, causing the probe to fail immediately after exit.
|
531
|
+
|
532
|
+
You may wish to tolerate more than one failed probe run to accommodate for environmental variance and clock changes.
|
533
|
+
|
534
|
+
See the [Configuration section](https://github.com/zendesk/racecar#configuration) for the various ways the liveness probe can be configured, environment variables being one option.
|
535
|
+
|
536
|
+
Here is an example Kubernetes liveness probe configuration:
|
537
|
+
|
538
|
+
```yaml
|
539
|
+
apiVersion: apps/v1
|
540
|
+
kind: Deployment
|
541
|
+
spec:
|
542
|
+
template:
|
543
|
+
spec:
|
544
|
+
containers:
|
545
|
+
- name: consumer
|
546
|
+
|
547
|
+
args:
|
548
|
+
- racecar
|
549
|
+
- SomeConsumer
|
550
|
+
|
551
|
+
env:
|
552
|
+
- name: RACECAR_LIVENESS_PROBE_ENABLED
|
553
|
+
value: "true"
|
554
|
+
|
555
|
+
livenessProbe:
|
556
|
+
exec:
|
557
|
+
command:
|
558
|
+
- racecarctl
|
559
|
+
- liveness_probe
|
560
|
+
|
561
|
+
# Allow up to 10 consecutive failures before terminating Pod:
|
562
|
+
failureThreshold: 10
|
563
|
+
|
564
|
+
# Wait 30 seconds before starting the probes:
|
565
|
+
initialDelaySeconds: 30
|
566
|
+
|
567
|
+
# Perform the check every 10 seconds:
|
568
|
+
periodSeconds: 10
|
569
|
+
```
|
570
|
+
|
446
571
|
#### Deploying to Heroku
|
447
572
|
|
448
573
|
If you run your applications in Heroku and/or use the Heroku Kafka add-on, you application will be provided with 4 ENV variables that allow connecting to the cluster: `KAFKA_URL`, `KAFKA_TRUSTED_CERT`, `KAFKA_CLIENT_CERT`, and `KAFKA_CLIENT_CERT_KEY`.
|
@@ -479,7 +604,7 @@ Again, the recommended approach is to manage the processes using process manager
|
|
479
604
|
|
480
605
|
### Handling errors
|
481
606
|
|
482
|
-
When processing messages from a Kafka topic, your code may encounter an error and raise an exception. The cause is typically one of two things:
|
607
|
+
#### When processing messages from a Kafka topic, your code may encounter an error and raise an exception. The cause is typically one of two things:
|
483
608
|
|
484
609
|
1. The message being processed is somehow malformed or doesn't conform with the assumptions made by the processing code.
|
485
610
|
2. You're using some external resource such as a database or a network API that is temporarily unavailable.
|
@@ -514,6 +639,16 @@ end
|
|
514
639
|
|
515
640
|
It is highly recommended that you set up an error handler. Please note that the `info` object contains different keys and values depending on whether you are using `process` or `process_batch`. See the `instrumentation_payload` object in the `process` and `process_batch` methods in the `Runner` class for the complete list.
|
516
641
|
|
642
|
+
#### Errors related to Compression
|
643
|
+
|
644
|
+
A sample error might look like this:
|
645
|
+
|
646
|
+
```
|
647
|
+
E, [2022-10-09T11:28:29.976548 #15] ERROR -- : (try 5/10): Error for topic subscription #<struct Racecar::Consumer::Subscription topic="support.entity_incremental.views.view_ticket_ids", start_from_beginning=false, max_bytes_per_partition=104857, additional_config={}>: Local: Not implemented (not_implemented)
|
648
|
+
```
|
649
|
+
|
650
|
+
Please see [Compression](#compression)
|
651
|
+
|
517
652
|
### Logging
|
518
653
|
|
519
654
|
By default, Racecar will log to `STDOUT`. If you're using Rails, your application code will use whatever logger you've configured there.
|
@@ -528,7 +663,15 @@ In order to introspect the configuration of a consumer process, send it the `SIG
|
|
528
663
|
|
529
664
|
### Upgrading from v1 to v2
|
530
665
|
|
531
|
-
In order to safely upgrade from Racecar v1 to v2, you need to completely shut down your consumer group before starting it up again with the v2 Racecar dependency. In general, you should avoid rolling deploys for consumers groups, so it is likely the case that this will just work for you, but it's a good idea to check first.
|
666
|
+
In order to safely upgrade from Racecar v1 to v2, you need to completely shut down your consumer group before starting it up again with the v2 Racecar dependency. In general, you should avoid rolling deploys for consumers groups, so it is likely the case that this will just work for you, but it's a good idea to check first.
|
667
|
+
|
668
|
+
### Compression
|
669
|
+
|
670
|
+
Racecar v2 requires a C library (zlib) to compress the messages before producing to the topic. If not already installed on you consumer docker container, please install using following command in Dockerfile of consumer
|
671
|
+
|
672
|
+
```
|
673
|
+
apt-get update && apt-get install -y libzstd-dev
|
674
|
+
```
|
532
675
|
|
533
676
|
## Development
|
534
677
|
|
data/Rakefile
CHANGED
data/examples/batch_consumer.rb
CHANGED
data/lib/racecar/cli.rb
CHANGED
@@ -5,6 +5,7 @@ require "logger"
|
|
5
5
|
require "fileutils"
|
6
6
|
require "racecar/rails_config_file_loader"
|
7
7
|
require "racecar/daemon"
|
8
|
+
require "racecar/liveness_probe"
|
8
9
|
|
9
10
|
module Racecar
|
10
11
|
class Cli
|
@@ -18,6 +19,7 @@ module Racecar
|
|
18
19
|
@parser = build_parser
|
19
20
|
@parser.parse!(args)
|
20
21
|
@consumer_name = args.first or raise Racecar::Error, "no consumer specified"
|
22
|
+
@runner = nil
|
21
23
|
end
|
22
24
|
|
23
25
|
def run
|
@@ -58,11 +60,21 @@ module Racecar
|
|
58
60
|
$stderr.puts "=> Ctrl-C to shutdown consumer"
|
59
61
|
end
|
60
62
|
|
63
|
+
if config.liveness_probe_enabled
|
64
|
+
$stderr.puts "=> Liveness probe enabled"
|
65
|
+
config.install_liveness_probe
|
66
|
+
end
|
67
|
+
|
61
68
|
processor = consumer_class.new
|
62
|
-
Racecar.
|
69
|
+
@runner = Racecar.runner(processor)
|
70
|
+
@runner.run
|
63
71
|
nil
|
64
72
|
end
|
65
73
|
|
74
|
+
def stop
|
75
|
+
@runner.stop
|
76
|
+
end
|
77
|
+
|
66
78
|
private
|
67
79
|
|
68
80
|
attr_reader :consumer_name
|
data/lib/racecar/config.rb
CHANGED
@@ -1,7 +1,13 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
+
require "tmpdir"
|
4
|
+
|
3
5
|
require "king_konf"
|
4
6
|
|
7
|
+
require "racecar/liveness_probe"
|
8
|
+
require "racecar/instrumenter"
|
9
|
+
require "racecar/rebalance_listener"
|
10
|
+
|
5
11
|
module Racecar
|
6
12
|
class Config < KingKonf::Config
|
7
13
|
env_prefix :racecar
|
@@ -77,6 +83,9 @@ module Racecar
|
|
77
83
|
desc "The log level for the Racecar logs"
|
78
84
|
string :log_level, default: "info"
|
79
85
|
|
86
|
+
desc "The strategy used to determine which topic partition a message is written to when Racecar produces a value to Kafka; defaults to `consistent_random`"
|
87
|
+
symbol :partitioner, allowed_values: %i{consistent consistent_random murmur2 murmur2_random fnv1a fnv1a_random}, default: :consistent_random
|
88
|
+
|
80
89
|
desc "Protocol used to communicate with brokers"
|
81
90
|
symbol :security_protocol, allowed_values: %i{plaintext ssl sasl_plaintext sasl_ssl}
|
82
91
|
|
@@ -164,6 +173,15 @@ module Racecar
|
|
164
173
|
for backward compatibility, however this can be quite memory intensive"
|
165
174
|
integer :statistics_interval, default: 1
|
166
175
|
|
176
|
+
desc "Whether to enable liveness probe behavior (touch the file)"
|
177
|
+
boolean :liveness_probe_enabled, default: false
|
178
|
+
|
179
|
+
desc "Path to a file Racecar will touch to show liveness"
|
180
|
+
string :liveness_probe_file_path, default: "#{Dir.tmpdir}/racecar-liveness"
|
181
|
+
|
182
|
+
desc "Used only by the liveness probe: Max time (in seconds) between liveness events before the process is considered not healthy"
|
183
|
+
integer :liveness_probe_max_interval, default: 5
|
184
|
+
|
167
185
|
# The error handler must be set directly on the object.
|
168
186
|
attr_reader :error_handler
|
169
187
|
|
@@ -210,6 +228,7 @@ module Racecar
|
|
210
228
|
end
|
211
229
|
|
212
230
|
def load_consumer_class(consumer_class)
|
231
|
+
self.consumer_class = consumer_class
|
213
232
|
self.group_id = consumer_class.group_id || self.group_id
|
214
233
|
|
215
234
|
self.group_id ||= [
|
@@ -226,6 +245,7 @@ module Racecar
|
|
226
245
|
self.fetch_messages = consumer_class.fetch_messages || self.fetch_messages
|
227
246
|
self.pidfile ||= "#{group_id}.pid"
|
228
247
|
end
|
248
|
+
attr_accessor :consumer_class
|
229
249
|
|
230
250
|
def on_error(&handler)
|
231
251
|
@error_handler = handler
|
@@ -247,6 +267,39 @@ module Racecar
|
|
247
267
|
producer_config
|
248
268
|
end
|
249
269
|
|
270
|
+
def instrumenter
|
271
|
+
@instrumenter ||= begin
|
272
|
+
default_payload = { client_id: client_id, group_id: group_id }
|
273
|
+
|
274
|
+
if defined?(ActiveSupport::Notifications)
|
275
|
+
# ActiveSupport needs `concurrent-ruby` but doesn't `require` it.
|
276
|
+
require 'concurrent/utility/monotonic_time'
|
277
|
+
Instrumenter.new(backend: ActiveSupport::Notifications, default_payload: default_payload)
|
278
|
+
else
|
279
|
+
logger.warn "ActiveSupport::Notifications not available, instrumentation is disabled"
|
280
|
+
NullInstrumenter
|
281
|
+
end
|
282
|
+
end
|
283
|
+
end
|
284
|
+
attr_writer :instrumenter
|
285
|
+
|
286
|
+
def install_liveness_probe
|
287
|
+
liveness_probe.tap(&:install)
|
288
|
+
end
|
289
|
+
|
290
|
+
def liveness_probe
|
291
|
+
require "active_support/notifications"
|
292
|
+
@liveness_probe ||= LivenessProbe.new(
|
293
|
+
ActiveSupport::Notifications,
|
294
|
+
liveness_probe_file_path,
|
295
|
+
liveness_probe_max_interval
|
296
|
+
)
|
297
|
+
end
|
298
|
+
|
299
|
+
def rebalance_listener
|
300
|
+
RebalanceListener.new(self)
|
301
|
+
end
|
302
|
+
|
250
303
|
private
|
251
304
|
|
252
305
|
def rdkafka_security_config
|
data/lib/racecar/consumer_set.rb
CHANGED
@@ -69,7 +69,10 @@ module Racecar
|
|
69
69
|
|
70
70
|
def current
|
71
71
|
@consumers[@consumer_id_iterator.peek] ||= begin
|
72
|
-
|
72
|
+
consumer_config = Rdkafka::Config.new(rdkafka_config(current_subscription))
|
73
|
+
consumer_config.consumer_rebalance_listener = @config.rebalance_listener
|
74
|
+
|
75
|
+
consumer = consumer_config.consumer
|
73
76
|
@instrumenter.instrument('join_group') do
|
74
77
|
consumer.subscribe current_subscription.topic
|
75
78
|
end
|
@@ -140,7 +143,7 @@ module Racecar
|
|
140
143
|
@logger.debug "No time remains for polling messages. Will try on next call."
|
141
144
|
return nil
|
142
145
|
elsif wait_ms >= remain_ms
|
143
|
-
@logger.
|
146
|
+
@logger.warn "Only #{remain_ms}ms left, but want to wait for #{wait_ms}ms before poll. Will retry on next call."
|
144
147
|
@previous_retries = try
|
145
148
|
return nil
|
146
149
|
elsif wait_ms > 0
|
data/lib/racecar/ctl.rb
CHANGED
@@ -32,6 +32,17 @@ module Racecar
|
|
32
32
|
@command = command
|
33
33
|
end
|
34
34
|
|
35
|
+
def liveness_probe(args)
|
36
|
+
require "racecar/liveness_probe"
|
37
|
+
parse_options!(args)
|
38
|
+
|
39
|
+
if ENV["RAILS_ENV"]
|
40
|
+
Racecar.config.load_file("config/racecar.yml", ENV["RAILS_ENV"])
|
41
|
+
end
|
42
|
+
|
43
|
+
Racecar.config.liveness_probe.check_liveness_within_interval!
|
44
|
+
end
|
45
|
+
|
35
46
|
def status(args)
|
36
47
|
parse_options!(args)
|
37
48
|
|
data/lib/racecar/daemon.rb
CHANGED
@@ -54,7 +54,7 @@ module Racecar
|
|
54
54
|
end
|
55
55
|
|
56
56
|
def pid
|
57
|
-
if File.
|
57
|
+
if File.exist?(pidfile)
|
58
58
|
File.read(pidfile).to_i
|
59
59
|
else
|
60
60
|
nil
|
@@ -89,7 +89,7 @@ module Racecar
|
|
89
89
|
end
|
90
90
|
|
91
91
|
at_exit do
|
92
|
-
File.delete(pidfile) if File.
|
92
|
+
File.delete(pidfile) if File.exist?(pidfile)
|
93
93
|
end
|
94
94
|
rescue Errno::EEXIST
|
95
95
|
check_pid
|
data/lib/racecar/datadog.rb
CHANGED
@@ -211,6 +211,10 @@ module Racecar
|
|
211
211
|
topic: topic,
|
212
212
|
}
|
213
213
|
|
214
|
+
if event.payload.key?(:exception)
|
215
|
+
increment("producer.produce.errors", tags: tags)
|
216
|
+
end
|
217
|
+
|
214
218
|
# This gets us the write rate.
|
215
219
|
increment("producer.produce.messages", tags: tags.merge(topic: topic))
|
216
220
|
|
@@ -245,6 +249,67 @@ module Racecar
|
|
245
249
|
increment("producer.ack.messages", tags: tags)
|
246
250
|
end
|
247
251
|
|
252
|
+
def produce_delivery_error(event)
|
253
|
+
tags = {
|
254
|
+
client: event.payload.fetch(:client_id),
|
255
|
+
}
|
256
|
+
|
257
|
+
increment("producer.produce.delivery.errors", tags: tags)
|
258
|
+
end
|
259
|
+
|
260
|
+
def produce_async(event)
|
261
|
+
client = event.payload.fetch(:client_id)
|
262
|
+
topic = event.payload.fetch(:topic)
|
263
|
+
message_size = event.payload.fetch(:message_size)
|
264
|
+
buffer_size = event.payload.fetch(:buffer_size)
|
265
|
+
|
266
|
+
tags = {
|
267
|
+
client: client,
|
268
|
+
topic: topic,
|
269
|
+
}
|
270
|
+
|
271
|
+
if event.payload.key?(:exception)
|
272
|
+
increment("producer.produce.errors", tags: tags)
|
273
|
+
end
|
274
|
+
|
275
|
+
# This gets us the write rate.
|
276
|
+
increment("producer.produce.messages", tags: tags.merge(topic: topic))
|
277
|
+
|
278
|
+
# Information about typical/average/95p message size.
|
279
|
+
histogram("producer.produce.message_size", message_size, tags: tags.merge(topic: topic))
|
280
|
+
|
281
|
+
# Aggregate message size.
|
282
|
+
count("producer.produce.message_size.sum", message_size, tags: tags.merge(topic: topic))
|
283
|
+
|
284
|
+
# This gets us the avg/max buffer size per producer.
|
285
|
+
histogram("producer.buffer.size", buffer_size, tags: tags)
|
286
|
+
end
|
287
|
+
|
288
|
+
def produce_sync(event)
|
289
|
+
client = event.payload.fetch(:client_id)
|
290
|
+
topic = event.payload.fetch(:topic)
|
291
|
+
message_size = event.payload.fetch(:message_size)
|
292
|
+
|
293
|
+
tags = {
|
294
|
+
client: client,
|
295
|
+
topic: topic,
|
296
|
+
}
|
297
|
+
|
298
|
+
if event.payload.key?(:exception)
|
299
|
+
increment("producer.produce.errors", tags: tags)
|
300
|
+
end
|
301
|
+
|
302
|
+
|
303
|
+
# This gets us the write rate.
|
304
|
+
increment("producer.produce.messages", tags: tags.merge(topic: topic))
|
305
|
+
|
306
|
+
# Information about typical/average/95p message size.
|
307
|
+
histogram("producer.produce.message_size", message_size, tags: tags.merge(topic: topic))
|
308
|
+
|
309
|
+
# Aggregate message size.
|
310
|
+
count("producer.produce.message_size.sum", message_size, tags: tags.merge(topic: topic))
|
311
|
+
end
|
312
|
+
|
248
313
|
attach_to "racecar"
|
249
314
|
end
|
250
315
|
end
|
@@ -0,0 +1,27 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Racecar
|
4
|
+
class DeliveryCallback
|
5
|
+
attr_reader :instrumenter
|
6
|
+
|
7
|
+
def initialize(instrumenter:)
|
8
|
+
@instrumenter = instrumenter
|
9
|
+
end
|
10
|
+
|
11
|
+
def call(delivery_report)
|
12
|
+
if delivery_report.error.to_i.zero?
|
13
|
+
payload = {
|
14
|
+
offset: delivery_report.offset,
|
15
|
+
partition: delivery_report.partition
|
16
|
+
}
|
17
|
+
instrumenter.instrument("acknowledged_message", payload)
|
18
|
+
else
|
19
|
+
payload = {
|
20
|
+
partition: delivery_report.partition,
|
21
|
+
exception: delivery_report.error
|
22
|
+
}
|
23
|
+
instrumenter.instrument("produce_delivery_error", payload)
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
data/lib/racecar/instrumenter.rb
CHANGED
@@ -9,16 +9,9 @@ module Racecar
|
|
9
9
|
NAMESPACE = "racecar"
|
10
10
|
attr_reader :backend
|
11
11
|
|
12
|
-
def initialize(default_payload
|
12
|
+
def initialize(backend:, default_payload: {})
|
13
|
+
@backend = backend
|
13
14
|
@default_payload = default_payload
|
14
|
-
|
15
|
-
@backend = if defined?(ActiveSupport::Notifications)
|
16
|
-
# ActiveSupport needs `concurrent-ruby` but doesn't `require` it.
|
17
|
-
require 'concurrent/utility/monotonic_time'
|
18
|
-
ActiveSupport::Notifications
|
19
|
-
else
|
20
|
-
NullInstrumenter
|
21
|
-
end
|
22
15
|
end
|
23
16
|
|
24
17
|
def instrument(event_name, payload = {}, &block)
|
@@ -0,0 +1,78 @@
|
|
1
|
+
require "fileutils"
|
2
|
+
|
3
|
+
module Racecar
|
4
|
+
class LivenessProbe
|
5
|
+
def initialize(message_bus, file_path, max_interval)
|
6
|
+
@message_bus = message_bus
|
7
|
+
@file_path = file_path
|
8
|
+
@max_interval = max_interval
|
9
|
+
@subscribers = []
|
10
|
+
end
|
11
|
+
|
12
|
+
attr_reader :message_bus, :file_path, :max_interval, :subscribers
|
13
|
+
private :message_bus, :file_path, :max_interval, :subscribers
|
14
|
+
|
15
|
+
def check_liveness_within_interval!
|
16
|
+
unless liveness_event_within_interval?
|
17
|
+
$stderr.puts "Racecar healthcheck failed: No liveness within interval #{max_interval}s. Last liveness at #{last_liveness_event_at}, #{elapsed_since_liveness_event} seconds ago."
|
18
|
+
Process.exit(1)
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
22
|
+
def liveness_event_within_interval?
|
23
|
+
elapsed_since_liveness_event < max_interval
|
24
|
+
rescue Errno::ENOENT
|
25
|
+
$stderr.puts "Racecar healthcheck failed: Liveness file not found `#{file_path}`"
|
26
|
+
Process.exit(1)
|
27
|
+
end
|
28
|
+
|
29
|
+
def install
|
30
|
+
unless file_path && file_writeable?
|
31
|
+
raise(
|
32
|
+
"Liveness probe configuration error: `liveness_probe_file_path` must be set to a writable file path.\n" \
|
33
|
+
" Set `RACECAR_LIVENESS_PROBE_FILE_PATH` and `RACECAR_LIVENESS_MAX_INTERVAL` environment variables."
|
34
|
+
)
|
35
|
+
end
|
36
|
+
|
37
|
+
subscribers << message_bus.subscribe("start_main_loop.racecar") do
|
38
|
+
touch_liveness_file
|
39
|
+
end
|
40
|
+
|
41
|
+
subscribers = message_bus.subscribe("shut_down.racecar") do
|
42
|
+
delete_liveness_file
|
43
|
+
end
|
44
|
+
|
45
|
+
nil
|
46
|
+
end
|
47
|
+
|
48
|
+
def uninstall
|
49
|
+
subscribers.each { |s| message_bus.unsubscribe(s) }
|
50
|
+
end
|
51
|
+
|
52
|
+
private
|
53
|
+
|
54
|
+
def elapsed_since_liveness_event
|
55
|
+
Time.now - last_liveness_event_at
|
56
|
+
end
|
57
|
+
|
58
|
+
def last_liveness_event_at
|
59
|
+
File.mtime(file_path)
|
60
|
+
end
|
61
|
+
|
62
|
+
def touch_liveness_file
|
63
|
+
FileUtils.touch(file_path)
|
64
|
+
end
|
65
|
+
|
66
|
+
def delete_liveness_file
|
67
|
+
FileUtils.rm_rf(file_path)
|
68
|
+
end
|
69
|
+
|
70
|
+
def file_writeable?
|
71
|
+
File.write(file_path, "")
|
72
|
+
File.unlink(file_path)
|
73
|
+
true
|
74
|
+
rescue
|
75
|
+
false
|
76
|
+
end
|
77
|
+
end
|
78
|
+
end
|
@@ -0,0 +1,139 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "racecar/message_delivery_error"
|
4
|
+
require "racecar/delivery_callback"
|
5
|
+
|
6
|
+
at_exit do
|
7
|
+
Racecar::Producer.shutdown!
|
8
|
+
end
|
9
|
+
|
10
|
+
module Racecar
|
11
|
+
class Producer
|
12
|
+
|
13
|
+
@@mutex = Mutex.new
|
14
|
+
|
15
|
+
class << self
|
16
|
+
def shutdown!
|
17
|
+
@@mutex.synchronize do
|
18
|
+
if !@internal_producer.nil?
|
19
|
+
@internal_producer.close
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
def initialize(config: nil, logger: nil, instrumenter: NullInstrumenter)
|
26
|
+
@config = config
|
27
|
+
@logger = logger
|
28
|
+
@delivery_handles = []
|
29
|
+
@instrumenter = instrumenter
|
30
|
+
@batching = false
|
31
|
+
@internal_producer = init_internal_producer(config)
|
32
|
+
end
|
33
|
+
|
34
|
+
def init_internal_producer(config)
|
35
|
+
@@mutex.synchronize do
|
36
|
+
@@init_internal_producer ||= begin
|
37
|
+
# https://github.com/edenhill/librdkafka/blob/master/CONFIGURATION.md
|
38
|
+
producer_config = {
|
39
|
+
"bootstrap.servers" => config.brokers.join(","),
|
40
|
+
"client.id" => config.client_id,
|
41
|
+
"statistics.interval.ms" => config.statistics_interval_ms,
|
42
|
+
"message.timeout.ms" => config.message_timeout * 1000,
|
43
|
+
}
|
44
|
+
producer_config["compression.codec"] = config.producer_compression_codec.to_s unless config.producer_compression_codec.nil?
|
45
|
+
producer_config.merge!(config.rdkafka_producer)
|
46
|
+
Rdkafka::Config.new(producer_config).producer.tap do |producer|
|
47
|
+
producer.delivery_callback = DeliveryCallback.new(instrumenter: @instrumenter)
|
48
|
+
end
|
49
|
+
end
|
50
|
+
end
|
51
|
+
end
|
52
|
+
|
53
|
+
# fire and forget - you won't get any guarantees or feedback from
|
54
|
+
# Racecar on the status of the message and it won't halt execution
|
55
|
+
# of the rest of your code.
|
56
|
+
def produce_async(value:, topic:, **options)
|
57
|
+
with_instrumentation(action: "produce_async", value: value, topic: topic, **options) do
|
58
|
+
begin
|
59
|
+
handle = internal_producer.produce(payload: value, topic: topic, **options)
|
60
|
+
@delivery_handles << handle if @batching
|
61
|
+
rescue Rdkafka::RdkafkaError => e
|
62
|
+
raise MessageDeliveryError.new(e, handle)
|
63
|
+
end
|
64
|
+
end
|
65
|
+
|
66
|
+
nil
|
67
|
+
end
|
68
|
+
|
69
|
+
# synchronous message production - will wait until the delivery handle succeeds, fails or times out.
|
70
|
+
def produce_sync(value:, topic:, **options)
|
71
|
+
with_instrumentation(action: "produce_sync", value: value, topic: topic, **options) do
|
72
|
+
begin
|
73
|
+
handle = internal_producer.produce(payload: value, topic: topic, **options)
|
74
|
+
deliver_with_error_handling(handle)
|
75
|
+
rescue Rdkafka::RdkafkaError => e
|
76
|
+
raise MessageDeliveryError.new(e, handle)
|
77
|
+
end
|
78
|
+
end
|
79
|
+
|
80
|
+
nil
|
81
|
+
end
|
82
|
+
|
83
|
+
# Blocks until all messages that have been asynchronously produced in the block have been delivered.
|
84
|
+
# Usage:
|
85
|
+
# messages = [
|
86
|
+
# {value: "message1", topic: "topic1"},
|
87
|
+
# {value: "message2", topic: "topic1"},
|
88
|
+
# {value: "message3", topic: "topic2"}
|
89
|
+
# ]
|
90
|
+
# Racecar.wait_for_delivery {
|
91
|
+
# messages.each do |msg|
|
92
|
+
# Racecar.produce_async(value: msg[:value], topic: msg[:topic])
|
93
|
+
# end
|
94
|
+
# }
|
95
|
+
def wait_for_delivery
|
96
|
+
@batching = true
|
97
|
+
@delivery_handles.clear
|
98
|
+
yield
|
99
|
+
@delivery_handles.each do |handle|
|
100
|
+
deliver_with_error_handling(handle)
|
101
|
+
end
|
102
|
+
ensure
|
103
|
+
@delivery_handles.clear
|
104
|
+
@batching = false
|
105
|
+
|
106
|
+
nil
|
107
|
+
end
|
108
|
+
|
109
|
+
private
|
110
|
+
|
111
|
+
attr_reader :internal_producer
|
112
|
+
|
113
|
+
def deliver_with_error_handling(handle)
|
114
|
+
handle.wait
|
115
|
+
rescue Rdkafka::AbstractHandle::WaitTimeoutError => e
|
116
|
+
partition = MessageDeliveryError.partition_from_delivery_handle(handle)
|
117
|
+
@logger.warn "Still trying to deliver message to (partition #{partition})... (will try up to Racecar.config.message_timeout)"
|
118
|
+
retry
|
119
|
+
rescue Rdkafka::RdkafkaError => e
|
120
|
+
raise MessageDeliveryError.new(e, handle)
|
121
|
+
end
|
122
|
+
|
123
|
+
def with_instrumentation(action:, value:, topic:, **options)
|
124
|
+
message_size = value.respond_to?(:bytesize) ? value.bytesize : 0
|
125
|
+
instrumentation_payload = {
|
126
|
+
value: value,
|
127
|
+
topic: topic,
|
128
|
+
message_size: message_size,
|
129
|
+
buffer_size: @delivery_handles.size,
|
130
|
+
key: options.fetch(:key, nil),
|
131
|
+
partition: options.fetch(:partition, nil),
|
132
|
+
partition_key: options.fetch(:partition_key, nil)
|
133
|
+
}
|
134
|
+
@instrumenter.instrument(action, instrumentation_payload) do
|
135
|
+
yield
|
136
|
+
end
|
137
|
+
end
|
138
|
+
end
|
139
|
+
end
|
@@ -0,0 +1,22 @@
|
|
1
|
+
module Racecar
|
2
|
+
class RebalanceListener
|
3
|
+
def initialize(config)
|
4
|
+
@config = config
|
5
|
+
@consumer_class = config.consumer_class
|
6
|
+
end
|
7
|
+
|
8
|
+
attr_reader :config, :consumer_class
|
9
|
+
|
10
|
+
def on_partitions_assigned(_consumer, topic_partition_list)
|
11
|
+
consumer_class.respond_to?(:on_partitions_assigned) &&
|
12
|
+
consumer_class.on_partitions_assigned(topic_partition_list.to_h)
|
13
|
+
rescue
|
14
|
+
end
|
15
|
+
|
16
|
+
def on_partitions_revoked(_consumer, topic_partition_list)
|
17
|
+
consumer_class.respond_to?(:on_partitions_revoked) &&
|
18
|
+
consumer_class.on_partitions_revoked(topic_partition_list.to_h)
|
19
|
+
rescue
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
data/lib/racecar/runner.rb
CHANGED
@@ -5,6 +5,7 @@ require "racecar/pause"
|
|
5
5
|
require "racecar/message"
|
6
6
|
require "racecar/message_delivery_error"
|
7
7
|
require "racecar/erroneous_state_error"
|
8
|
+
require "racecar/delivery_callback"
|
8
9
|
|
9
10
|
module Racecar
|
10
11
|
class Runner
|
@@ -67,6 +68,8 @@ module Racecar
|
|
67
68
|
loop do
|
68
69
|
break if @stop_requested
|
69
70
|
resume_paused_partitions
|
71
|
+
|
72
|
+
@instrumenter.instrument("start_main_loop", instrumentation_payload)
|
70
73
|
@instrumenter.instrument("main_loop", instrumentation_payload) do
|
71
74
|
case process_method
|
72
75
|
when :batch then
|
@@ -94,6 +97,7 @@ module Racecar
|
|
94
97
|
ensure
|
95
98
|
producer.close
|
96
99
|
Racecar::Datadog.close if Object.const_defined?("Racecar::Datadog")
|
100
|
+
@instrumenter.instrument("shut_down", instrumentation_payload || {})
|
97
101
|
end
|
98
102
|
|
99
103
|
def stop
|
@@ -138,7 +142,7 @@ module Racecar
|
|
138
142
|
|
139
143
|
def producer
|
140
144
|
@producer ||= Rdkafka::Config.new(producer_config).producer.tap do |producer|
|
141
|
-
producer.delivery_callback =
|
145
|
+
producer.delivery_callback = Racecar::DeliveryCallback.new(instrumenter: @instrumenter)
|
142
146
|
end
|
143
147
|
end
|
144
148
|
|
@@ -149,22 +153,14 @@ module Racecar
|
|
149
153
|
"client.id" => config.client_id,
|
150
154
|
"statistics.interval.ms" => config.statistics_interval_ms,
|
151
155
|
"message.timeout.ms" => config.message_timeout * 1000,
|
156
|
+
"partitioner" => config.partitioner.to_s,
|
152
157
|
}
|
158
|
+
|
153
159
|
producer_config["compression.codec"] = config.producer_compression_codec.to_s unless config.producer_compression_codec.nil?
|
154
160
|
producer_config.merge!(config.rdkafka_producer)
|
155
161
|
producer_config
|
156
162
|
end
|
157
163
|
|
158
|
-
def delivery_callback
|
159
|
-
->(delivery_report) do
|
160
|
-
payload = {
|
161
|
-
offset: delivery_report.offset,
|
162
|
-
partition: delivery_report.partition
|
163
|
-
}
|
164
|
-
@instrumenter.instrument("acknowledged_message", payload)
|
165
|
-
end
|
166
|
-
end
|
167
|
-
|
168
164
|
def install_signal_handlers
|
169
165
|
# Stop the consumer on SIGINT, SIGQUIT or SIGTERM.
|
170
166
|
trap("QUIT") { stop }
|
data/lib/racecar/version.rb
CHANGED
data/lib/racecar.rb
CHANGED
@@ -8,6 +8,7 @@ require "racecar/consumer"
|
|
8
8
|
require "racecar/consumer_set"
|
9
9
|
require "racecar/runner"
|
10
10
|
require "racecar/parallel_runner"
|
11
|
+
require "racecar/producer"
|
11
12
|
require "racecar/config"
|
12
13
|
require "racecar/version"
|
13
14
|
require "ensure_hash_compact"
|
@@ -39,25 +40,42 @@ module Racecar
|
|
39
40
|
config.logger = logger
|
40
41
|
end
|
41
42
|
|
42
|
-
def self.
|
43
|
-
|
44
|
-
|
43
|
+
def self.produce_async(value:, topic:, **options)
|
44
|
+
producer.produce_async(value: value, topic: topic, **options)
|
45
|
+
end
|
45
46
|
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
|
47
|
+
def self.produce_sync(value:, topic:, **options)
|
48
|
+
producer.produce_sync(value: value, topic: topic, **options)
|
49
|
+
end
|
50
|
+
|
51
|
+
def self.wait_for_delivery(&block)
|
52
|
+
producer.wait_for_delivery(&block)
|
53
|
+
end
|
54
|
+
|
55
|
+
def self.producer
|
56
|
+
Thread.current[:racecar_producer] ||= begin
|
57
|
+
if config.datadog_enabled
|
58
|
+
require "racecar/datadog"
|
50
59
|
end
|
60
|
+
Racecar::Producer.new(config: config, logger: logger, instrumenter: instrumenter)
|
51
61
|
end
|
52
62
|
end
|
53
63
|
|
64
|
+
def self.instrumenter
|
65
|
+
config.instrumenter
|
66
|
+
end
|
67
|
+
|
54
68
|
def self.run(processor)
|
55
|
-
runner
|
69
|
+
runner(processor).run
|
70
|
+
end
|
71
|
+
|
72
|
+
def self.runner(processor)
|
73
|
+
runner = Runner.new(processor, config: config, logger: logger, instrumenter: config.instrumenter)
|
56
74
|
|
57
75
|
if config.parallel_workers && config.parallel_workers > 1
|
58
|
-
ParallelRunner.new(runner: runner, config: config, logger: logger)
|
76
|
+
ParallelRunner.new(runner: runner, config: config, logger: logger)
|
59
77
|
else
|
60
|
-
runner
|
78
|
+
runner
|
61
79
|
end
|
62
80
|
end
|
63
81
|
end
|
data/racecar.gemspec
CHANGED
@@ -26,10 +26,10 @@ Gem::Specification.new do |spec|
|
|
26
26
|
spec.add_runtime_dependency "rdkafka", "~> 0.12.0"
|
27
27
|
|
28
28
|
spec.add_development_dependency "bundler", [">= 1.13", "< 3"]
|
29
|
-
spec.add_development_dependency "pry"
|
29
|
+
spec.add_development_dependency "pry-byebug"
|
30
30
|
spec.add_development_dependency "rake", "> 10.0"
|
31
31
|
spec.add_development_dependency "rspec", "~> 3.0"
|
32
32
|
spec.add_development_dependency "timecop"
|
33
33
|
spec.add_development_dependency "dogstatsd-ruby", ">= 4.0.0", "< 6.0.0"
|
34
|
-
spec.add_development_dependency "activesupport"
|
34
|
+
spec.add_development_dependency "activesupport"
|
35
35
|
end
|
metadata
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: racecar
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 2.
|
4
|
+
version: 2.9.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Daniel Schierbeck
|
@@ -9,7 +9,7 @@ authors:
|
|
9
9
|
autorequire:
|
10
10
|
bindir: exe
|
11
11
|
cert_chain: []
|
12
|
-
date:
|
12
|
+
date: 2023-09-25 00:00:00.000000000 Z
|
13
13
|
dependencies:
|
14
14
|
- !ruby/object:Gem::Dependency
|
15
15
|
name: king_konf
|
@@ -60,7 +60,7 @@ dependencies:
|
|
60
60
|
- !ruby/object:Gem::Version
|
61
61
|
version: '3'
|
62
62
|
- !ruby/object:Gem::Dependency
|
63
|
-
name: pry
|
63
|
+
name: pry-byebug
|
64
64
|
requirement: !ruby/object:Gem::Requirement
|
65
65
|
requirements:
|
66
66
|
- - ">="
|
@@ -141,20 +141,14 @@ dependencies:
|
|
141
141
|
requirements:
|
142
142
|
- - ">="
|
143
143
|
- !ruby/object:Gem::Version
|
144
|
-
version: '
|
145
|
-
- - "<"
|
146
|
-
- !ruby/object:Gem::Version
|
147
|
-
version: '6.1'
|
144
|
+
version: '0'
|
148
145
|
type: :development
|
149
146
|
prerelease: false
|
150
147
|
version_requirements: !ruby/object:Gem::Requirement
|
151
148
|
requirements:
|
152
149
|
- - ">="
|
153
150
|
- !ruby/object:Gem::Version
|
154
|
-
version: '
|
155
|
-
- - "<"
|
156
|
-
- !ruby/object:Gem::Version
|
157
|
-
version: '6.1'
|
151
|
+
version: '0'
|
158
152
|
description:
|
159
153
|
email:
|
160
154
|
- dschierbeck@zendesk.com
|
@@ -165,9 +159,9 @@ executables:
|
|
165
159
|
extensions: []
|
166
160
|
extra_rdoc_files: []
|
167
161
|
files:
|
168
|
-
- ".circleci/config.yml"
|
169
162
|
- ".github/dependabot.yml"
|
170
163
|
- ".github/workflows/ci.yml"
|
164
|
+
- ".github/workflows/publish.yml"
|
171
165
|
- ".gitignore"
|
172
166
|
- ".rspec"
|
173
167
|
- CHANGELOG.md
|
@@ -200,15 +194,19 @@ files:
|
|
200
194
|
- lib/racecar/ctl.rb
|
201
195
|
- lib/racecar/daemon.rb
|
202
196
|
- lib/racecar/datadog.rb
|
197
|
+
- lib/racecar/delivery_callback.rb
|
203
198
|
- lib/racecar/erroneous_state_error.rb
|
204
199
|
- lib/racecar/heroku.rb
|
205
200
|
- lib/racecar/instrumenter.rb
|
201
|
+
- lib/racecar/liveness_probe.rb
|
206
202
|
- lib/racecar/message.rb
|
207
203
|
- lib/racecar/message_delivery_error.rb
|
208
204
|
- lib/racecar/null_instrumenter.rb
|
209
205
|
- lib/racecar/parallel_runner.rb
|
210
206
|
- lib/racecar/pause.rb
|
207
|
+
- lib/racecar/producer.rb
|
211
208
|
- lib/racecar/rails_config_file_loader.rb
|
209
|
+
- lib/racecar/rebalance_listener.rb
|
212
210
|
- lib/racecar/runner.rb
|
213
211
|
- lib/racecar/version.rb
|
214
212
|
- racecar.gemspec
|
@@ -231,7 +229,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
231
229
|
- !ruby/object:Gem::Version
|
232
230
|
version: '0'
|
233
231
|
requirements: []
|
234
|
-
rubygems_version: 3.0.3
|
232
|
+
rubygems_version: 3.0.3.1
|
235
233
|
signing_key:
|
236
234
|
specification_version: 4
|
237
235
|
summary: A framework for running Kafka consumers
|
data/.circleci/config.yml
DELETED
@@ -1,56 +0,0 @@
|
|
1
|
-
version: 2.1
|
2
|
-
orbs:
|
3
|
-
ruby: circleci/ruby@0.1.2
|
4
|
-
|
5
|
-
jobs:
|
6
|
-
build:
|
7
|
-
docker:
|
8
|
-
- image: circleci/ruby:2.6.3-stretch-node
|
9
|
-
executor: ruby/default
|
10
|
-
steps:
|
11
|
-
- checkout
|
12
|
-
- run:
|
13
|
-
name: Which bundler?
|
14
|
-
command: bundle -v
|
15
|
-
- ruby/bundle-install
|
16
|
-
- run: bundle exec rspec --exclude-pattern='spec/integration/*_spec.rb'
|
17
|
-
integration-tests:
|
18
|
-
docker:
|
19
|
-
- image: circleci/ruby:2.6.3-stretch-node
|
20
|
-
- image: wurstmeister/zookeeper
|
21
|
-
- image: wurstmeister/kafka:2.11-2.0.0
|
22
|
-
environment:
|
23
|
-
KAFKA_ADVERTISED_HOST_NAME: localhost
|
24
|
-
KAFKA_ADVERTISED_PORT: 9092
|
25
|
-
KAFKA_PORT: 9092
|
26
|
-
KAFKA_ZOOKEEPER_CONNECT: localhost:2181
|
27
|
-
KAFKA_DELETE_TOPIC_ENABLE: true
|
28
|
-
- image: wurstmeister/kafka:2.11-2.0.0
|
29
|
-
environment:
|
30
|
-
KAFKA_ADVERTISED_HOST_NAME: localhost
|
31
|
-
KAFKA_ADVERTISED_PORT: 9093
|
32
|
-
KAFKA_PORT: 9093
|
33
|
-
KAFKA_ZOOKEEPER_CONNECT: localhost:2181
|
34
|
-
KAFKA_DELETE_TOPIC_ENABLE: true
|
35
|
-
- image: wurstmeister/kafka:2.11-2.0.0
|
36
|
-
environment:
|
37
|
-
KAFKA_ADVERTISED_HOST_NAME: localhost
|
38
|
-
KAFKA_ADVERTISED_PORT: 9094
|
39
|
-
KAFKA_PORT: 9094
|
40
|
-
KAFKA_ZOOKEEPER_CONNECT: localhost:2181
|
41
|
-
KAFKA_DELETE_TOPIC_ENABLE: true
|
42
|
-
executor: ruby/default
|
43
|
-
steps:
|
44
|
-
- checkout
|
45
|
-
- run:
|
46
|
-
name: Which bundler?
|
47
|
-
command: bundle -v
|
48
|
-
- ruby/bundle-install
|
49
|
-
- run: bundle exec rspec --pattern='spec/integration/*_spec.rb'
|
50
|
-
|
51
|
-
workflows:
|
52
|
-
version: 2
|
53
|
-
test:
|
54
|
-
jobs:
|
55
|
-
- build
|
56
|
-
- integration-tests
|