racecar 2.9.0.beta1 → 2.10.0.beta1
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/CHANGELOG.md +13 -1
- data/Dockerfile +1 -1
- data/Gemfile +3 -3
- data/Gemfile.lock +37 -32
- data/README.md +41 -13
- data/lib/racecar/cli.rb +7 -1
- data/lib/racecar/config.rb +6 -0
- data/lib/racecar/consumer.rb +4 -0
- data/lib/racecar/consumer_set.rb +24 -2
- data/lib/racecar/ctl.rb +8 -2
- data/lib/racecar/daemon.rb +2 -2
- data/lib/racecar/datadog.rb +22 -1
- data/lib/racecar/delivery_callback.rb +27 -0
- data/lib/racecar/parallel_runner.rb +4 -0
- data/lib/racecar/producer.rb +18 -8
- data/lib/racecar/rebalance_listener.rb +58 -0
- data/lib/racecar/runner.rb +11 -25
- data/lib/racecar/version.rb +1 -1
- data/lib/racecar.rb +6 -2
- data/racecar.gemspec +3 -3
- metadata +9 -13
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: e0057ec2861db783b83e34a63c0a09a0be476ed6cb57b760dea3e2cc5ef57d1f
|
4
|
+
data.tar.gz: 22dac9ed21af6f9abfd50f22d9dc86032a68af556e1be77bff2f59cbf505ccf2
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: f3baeec4de02a68b55e4a302d2b713de67d0245564aae57cbca363aec8b34633f8dd2220d44c1e212347857d7fde35217644dae8afdbde06103e11a52394997d
|
7
|
+
data.tar.gz: 66a2168ca6f7a4ec7a6d63e8cf767397502c02487d87445b59e4d7fbdd0269abee3279019e36ab1f343e246d4c58146e951ad521b1bc881a4cb60b3f35076b6e
|
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
|
data/CHANGELOG.md
CHANGED
@@ -1,6 +1,18 @@
|
|
1
1
|
# Changelog
|
2
2
|
|
3
|
-
##
|
3
|
+
## Unreleased
|
4
|
+
|
5
|
+
## 2.10.0.beta1
|
6
|
+
|
7
|
+
* Bump rdkafka gem version to 0.13.0
|
8
|
+
* Support cooperative-sticky
|
9
|
+
* Instrument produce delivery errors
|
10
|
+
* Fix config load for liveness probe
|
11
|
+
* Send exceptions to `process_batch` instrumenter
|
12
|
+
* Docker test fixes
|
13
|
+
* Test with Ruby 3.2
|
14
|
+
|
15
|
+
## v2.9.0, v2.9.0.beta1
|
4
16
|
|
5
17
|
* Add `partitioner` producer config option to allow changing the strategy to
|
6
18
|
determine which topic partition a message is written to when racecar
|
data/Dockerfile
CHANGED
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.9.0
|
4
|
+
racecar (2.9.0)
|
5
5
|
king_konf (~> 1.0.0)
|
6
|
-
rdkafka (~> 0.
|
6
|
+
rdkafka (~> 0.13.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
25
|
king_konf (1.0.1)
|
24
26
|
method_source (1.0.0)
|
25
27
|
mini_portile2 (2.8.1)
|
26
|
-
minitest (5.
|
27
|
-
pry (0.
|
28
|
+
minitest (5.18.0)
|
29
|
+
pry (0.14.2)
|
28
30
|
coderay (~> 1.1)
|
29
31
|
method_source (~> 1.0)
|
30
|
-
|
31
|
-
|
32
|
+
pry-byebug (3.10.1)
|
33
|
+
byebug (~> 11.0)
|
34
|
+
pry (>= 0.13, < 0.15)
|
35
|
+
rake (13.0.6)
|
36
|
+
rdkafka (0.13.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
@@ -249,7 +249,8 @@ You can set message headers by passing a `headers:` option with a Hash of header
|
|
249
249
|
### Standalone Producer
|
250
250
|
|
251
251
|
Racecar provides a standalone producer to publish messages to Kafka directly from your Rails application:
|
252
|
-
|
252
|
+
|
253
|
+
```ruby
|
253
254
|
# app/controllers/comments_controller.rb
|
254
255
|
class CommentsController < ApplicationController
|
255
256
|
def create
|
@@ -263,7 +264,8 @@ end
|
|
263
264
|
```
|
264
265
|
|
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:
|
266
|
-
|
267
|
+
|
268
|
+
```ruby
|
267
269
|
# app/controllers/comments_controller.rb
|
268
270
|
class CommentsController < ApplicationController
|
269
271
|
def show
|
@@ -284,9 +286,9 @@ end
|
|
284
286
|
```
|
285
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.
|
286
288
|
|
287
|
-
A third method is to produce messages first (without delivering the messages to Kafka yet), and deliver them synchronously later
|
289
|
+
A third method is to produce messages first (without delivering the messages to Kafka yet), and deliver them synchronously later:
|
288
290
|
|
289
|
-
```
|
291
|
+
```ruby
|
290
292
|
# app/controllers/comments_controller.rb
|
291
293
|
class CommentsController < ApplicationController
|
292
294
|
def create
|
@@ -415,7 +417,7 @@ Racecar supports [Datadog](https://www.datadoghq.com/) monitoring integration. I
|
|
415
417
|
- `datadog_namespace` – The namespace to use for Datadog metrics.
|
416
418
|
- `datadog_tags` – Tags that should always be set on Datadog metrics.
|
417
419
|
|
418
|
-
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.
|
419
421
|
|
420
422
|
#### Consumers Without Rails
|
421
423
|
|
@@ -474,6 +476,8 @@ With Foreman, you can easily run these processes locally by executing `foreman r
|
|
474
476
|
|
475
477
|
If you run your applications in Kubernetes, use the following [Deployment](https://kubernetes.io/docs/concepts/workloads/controllers/deployment/) spec as a starting point:
|
476
478
|
|
479
|
+
##### Recreate Strategy
|
480
|
+
|
477
481
|
```yaml
|
478
482
|
apiVersion: apps/v1
|
479
483
|
kind: Deployment
|
@@ -481,8 +485,8 @@ metadata:
|
|
481
485
|
name: my-racecar-deployment
|
482
486
|
labels:
|
483
487
|
app: my-racecar
|
484
|
-
spec
|
485
|
-
replicas:
|
488
|
+
spec
|
489
|
+
replicas: 4 # <-- this is a good value if you have a multliple of 4 partitions
|
486
490
|
selector:
|
487
491
|
matchLabels:
|
488
492
|
app: my-racecar
|
@@ -504,9 +508,33 @@ spec:
|
|
504
508
|
value: 5
|
505
509
|
```
|
506
510
|
|
507
|
-
|
511
|
+
This configuration uses the recreate strategy which completely terminates all consumers before starting new ones.
|
512
|
+
It's simple and easy to understand but can result in significant 'downtime' where no messages are processed.
|
513
|
+
|
514
|
+
##### Rolling Updates and 'sticky-cooperative' Assignment
|
515
|
+
|
516
|
+
A newer alternative is to use the consumer's "cooperative-sticky" assignment strategy which allows healthy consumers to keep processing their partitions while others are terminated.
|
517
|
+
This can be combined with a restricted rolling update to minimize processing downtime.
|
518
|
+
|
519
|
+
Add to your Racecar config:
|
520
|
+
```ruby
|
521
|
+
Racecar.configure do |c|
|
522
|
+
c.partition_assignment_strategy = "cooperative-sticky"
|
523
|
+
end
|
524
|
+
```
|
525
|
+
|
526
|
+
Replace the Kubernetes deployment strategy with:
|
527
|
+
```yaml
|
528
|
+
strategy:
|
529
|
+
type: RollingUpdate
|
530
|
+
rollingUpdate:
|
531
|
+
maxSurge: 0 # <- Never boot an excess consumer
|
532
|
+
maxUnavailable: 1 # <- The deploy 'rolls' one consumer at a time
|
533
|
+
```
|
534
|
+
|
535
|
+
These two configurations should be deployed together.
|
508
536
|
|
509
|
-
|
537
|
+
While `maxSurge` should always be 0, `maxUnavailable` can be increased to reduce deployment times in exchange for longer pauses in message processing.
|
510
538
|
|
511
539
|
#### Liveness Probe
|
512
540
|
|
@@ -661,7 +689,7 @@ In order to introspect the configuration of a consumer process, send it the `SIG
|
|
661
689
|
|
662
690
|
### Upgrading from v1 to v2
|
663
691
|
|
664
|
-
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.
|
692
|
+
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.
|
665
693
|
|
666
694
|
### Compression
|
667
695
|
|
@@ -679,12 +707,12 @@ The integration tests run against a Kafka instance that is not automatically sta
|
|
679
707
|
|
680
708
|
### Running RSpec within Docker
|
681
709
|
|
682
|
-
There can be behavioural inconsistencies between running the specs on your machine, and in the CI pipeline. Due to this, there is now a Dockerfile included in the project, which is based on the CircleCI ruby 2.7.
|
710
|
+
There can be behavioural inconsistencies between running the specs on your machine, and in the CI pipeline. Due to this, there is now a Dockerfile included in the project, which is based on the CircleCI ruby 2.7.8 image. This could easily be extended with more Dockerfiles to cover different Ruby versions if desired. In order to run the specs via Docker:
|
683
711
|
|
684
712
|
- Uncomment the `tests` service from the docker-compose.yml
|
685
713
|
- Bring up the stack with `docker-compose up -d`
|
686
|
-
- Execute the entire suite with `docker-compose run --rm tests rspec`
|
687
|
-
- Execute a single spec or directory with `docker-compose run --rm tests rspec spec/integration/consumer_spec.rb`
|
714
|
+
- Execute the entire suite with `docker-compose run --rm tests bundle exec rspec`
|
715
|
+
- Execute a single spec or directory with `docker-compose run --rm tests bundle exec rspec spec/integration/consumer_spec.rb`
|
688
716
|
|
689
717
|
Please note - your code directory is mounted as a volume, so you can make code changes without needing to rebuild
|
690
718
|
|
data/lib/racecar/cli.rb
CHANGED
@@ -19,6 +19,7 @@ module Racecar
|
|
19
19
|
@parser = build_parser
|
20
20
|
@parser.parse!(args)
|
21
21
|
@consumer_name = args.first or raise Racecar::Error, "no consumer specified"
|
22
|
+
@runner = nil
|
22
23
|
end
|
23
24
|
|
24
25
|
def run
|
@@ -65,10 +66,15 @@ module Racecar
|
|
65
66
|
end
|
66
67
|
|
67
68
|
processor = consumer_class.new
|
68
|
-
Racecar.
|
69
|
+
@runner = Racecar.runner(processor)
|
70
|
+
@runner.run
|
69
71
|
nil
|
70
72
|
end
|
71
73
|
|
74
|
+
def stop
|
75
|
+
@runner.stop
|
76
|
+
end
|
77
|
+
|
72
78
|
private
|
73
79
|
|
74
80
|
attr_reader :consumer_name
|
data/lib/racecar/config.rb
CHANGED
@@ -6,6 +6,7 @@ require "king_konf"
|
|
6
6
|
|
7
7
|
require "racecar/liveness_probe"
|
8
8
|
require "racecar/instrumenter"
|
9
|
+
require "racecar/rebalance_listener"
|
9
10
|
|
10
11
|
module Racecar
|
11
12
|
class Config < KingKonf::Config
|
@@ -28,6 +29,9 @@ module Racecar
|
|
28
29
|
desc "The minimum number of messages in the local consumer queue"
|
29
30
|
integer :min_message_queue_size, default: 2000
|
30
31
|
|
32
|
+
desc "Which partition assignment strategy to use, range, roundrobin or cooperative-sticky. -- https://github.com/edenhill/librdkafka/blob/master/CONFIGURATION.md"
|
33
|
+
string :partition_assignment_strategy, default: "range,roundrobin"
|
34
|
+
|
31
35
|
desc "Kafka consumer configuration options, separated with '=' -- https://github.com/edenhill/librdkafka/blob/master/CONFIGURATION.md"
|
32
36
|
list :consumer, default: []
|
33
37
|
|
@@ -227,6 +231,7 @@ module Racecar
|
|
227
231
|
end
|
228
232
|
|
229
233
|
def load_consumer_class(consumer_class)
|
234
|
+
self.consumer_class = consumer_class
|
230
235
|
self.group_id = consumer_class.group_id || self.group_id
|
231
236
|
|
232
237
|
self.group_id ||= [
|
@@ -243,6 +248,7 @@ module Racecar
|
|
243
248
|
self.fetch_messages = consumer_class.fetch_messages || self.fetch_messages
|
244
249
|
self.pidfile ||= "#{group_id}.pid"
|
245
250
|
end
|
251
|
+
attr_accessor :consumer_class
|
246
252
|
|
247
253
|
def on_error(&handler)
|
248
254
|
@error_handler = handler
|
data/lib/racecar/consumer.rb
CHANGED
@@ -37,6 +37,10 @@ module Racecar
|
|
37
37
|
subscriptions << Subscription.new(topic, start_from_beginning, max_bytes_per_partition, additional_config)
|
38
38
|
end
|
39
39
|
end
|
40
|
+
|
41
|
+
# Rebalance hooks for subclasses to override
|
42
|
+
def on_partitions_assigned(rebalance_event); end
|
43
|
+
def on_partitions_revoked(rebalance_event); end
|
40
44
|
end
|
41
45
|
|
42
46
|
def configure(producer:, consumer:, instrumenter: NullInstrumenter, config: Racecar.config)
|
data/lib/racecar/consumer_set.rb
CHANGED
@@ -15,6 +15,7 @@ module Racecar
|
|
15
15
|
@previous_retries = 0
|
16
16
|
|
17
17
|
@last_poll_read_nil_message = false
|
18
|
+
@paused_tpls = Hash.new { |h, k| h[k] = {} }
|
18
19
|
end
|
19
20
|
|
20
21
|
def poll(max_wait_time_ms = @config.max_wait_time_ms)
|
@@ -65,11 +66,17 @@ module Racecar
|
|
65
66
|
|
66
67
|
def close
|
67
68
|
each_subscribed(&:close)
|
69
|
+
@paused_tpls.clear
|
68
70
|
end
|
69
71
|
|
70
72
|
def current
|
71
73
|
@consumers[@consumer_id_iterator.peek] ||= begin
|
72
|
-
|
74
|
+
consumer_config = Rdkafka::Config.new(rdkafka_config(current_subscription))
|
75
|
+
listener = RebalanceListener.new(@config.consumer_class, @instrumenter)
|
76
|
+
consumer_config.consumer_rebalance_listener = listener
|
77
|
+
consumer = consumer_config.consumer
|
78
|
+
listener.rdkafka_consumer = consumer
|
79
|
+
|
73
80
|
@instrumenter.instrument('join_group') do
|
74
81
|
consumer.subscribe current_subscription.topic
|
75
82
|
end
|
@@ -95,16 +102,25 @@ module Racecar
|
|
95
102
|
consumer.pause(filtered_tpl)
|
96
103
|
fake_msg = OpenStruct.new(topic: topic, partition: partition, offset: offset)
|
97
104
|
consumer.seek(fake_msg)
|
105
|
+
|
106
|
+
@paused_tpls[topic][partition] = [consumer, filtered_tpl]
|
98
107
|
end
|
99
108
|
|
100
109
|
def resume(topic, partition)
|
101
110
|
consumer, filtered_tpl = find_consumer_by(topic, partition)
|
111
|
+
|
112
|
+
if !consumer && @paused_tpls[topic][partition]
|
113
|
+
consumer, filtered_tpl = @paused_tpls[topic][partition]
|
114
|
+
end
|
115
|
+
|
102
116
|
if !consumer
|
103
117
|
@logger.info "Attempted to resume #{topic}/#{partition}, but we're not subscribed to it"
|
104
118
|
return
|
105
119
|
end
|
106
120
|
|
107
121
|
consumer.resume(filtered_tpl)
|
122
|
+
@paused_tpls[topic].delete(partition)
|
123
|
+
@paused_tpls.delete(topic) if @paused_tpls[topic].empty?
|
108
124
|
end
|
109
125
|
|
110
126
|
alias :each :each_subscribed
|
@@ -215,6 +231,11 @@ module Racecar
|
|
215
231
|
def rdkafka_config(subscription)
|
216
232
|
# https://github.com/edenhill/librdkafka/blob/master/CONFIGURATION.md
|
217
233
|
config = {
|
234
|
+
# Manually store offset after messages have been processed successfully
|
235
|
+
# to avoid marking failed messages as committed. The call just updates
|
236
|
+
# a value within librdkafka and is asynchronously written to proper
|
237
|
+
# storage through auto commits.
|
238
|
+
"enable.auto.offset.store" => false,
|
218
239
|
"auto.commit.interval.ms" => @config.offset_commit_interval * 1000,
|
219
240
|
"auto.offset.reset" => subscription.start_from_beginning ? "earliest" : "largest",
|
220
241
|
"bootstrap.servers" => @config.brokers.join(","),
|
@@ -230,7 +251,8 @@ module Racecar
|
|
230
251
|
"queued.min.messages" => @config.min_message_queue_size,
|
231
252
|
"session.timeout.ms" => @config.session_timeout * 1000,
|
232
253
|
"socket.timeout.ms" => @config.socket_timeout * 1000,
|
233
|
-
"statistics.interval.ms" => @config.statistics_interval_ms
|
254
|
+
"statistics.interval.ms" => @config.statistics_interval_ms,
|
255
|
+
"partition.assignment.strategy" => @config.partition_assignment_strategy,
|
234
256
|
}
|
235
257
|
config.merge! @config.rdkafka_consumer
|
236
258
|
config.merge! subscription.additional_config
|
data/lib/racecar/ctl.rb
CHANGED
@@ -36,8 +36,10 @@ module Racecar
|
|
36
36
|
require "racecar/liveness_probe"
|
37
37
|
parse_options!(args)
|
38
38
|
|
39
|
-
|
40
|
-
|
39
|
+
RailsConfigFileLoader.load! unless config.without_rails?
|
40
|
+
|
41
|
+
if File.exist?("config/racecar.rb")
|
42
|
+
require "./config/racecar"
|
41
43
|
end
|
42
44
|
|
43
45
|
Racecar.config.liveness_probe.check_liveness_within_interval!
|
@@ -136,5 +138,9 @@ module Racecar
|
|
136
138
|
|
137
139
|
parser.parse!(args)
|
138
140
|
end
|
141
|
+
|
142
|
+
def config
|
143
|
+
Racecar.config
|
144
|
+
end
|
139
145
|
end
|
140
146
|
end
|
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
|
|
@@ -244,7 +248,15 @@ module Racecar
|
|
244
248
|
# Number of messages ACK'd for the topic.
|
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
|
+
|
248
260
|
def produce_async(event)
|
249
261
|
client = event.payload.fetch(:client_id)
|
250
262
|
topic = event.payload.fetch(:topic)
|
@@ -256,6 +268,10 @@ module Racecar
|
|
256
268
|
topic: topic,
|
257
269
|
}
|
258
270
|
|
271
|
+
if event.payload.key?(:exception)
|
272
|
+
increment("producer.produce.errors", tags: tags)
|
273
|
+
end
|
274
|
+
|
259
275
|
# This gets us the write rate.
|
260
276
|
increment("producer.produce.messages", tags: tags.merge(topic: topic))
|
261
277
|
|
@@ -279,6 +295,11 @@ module Racecar
|
|
279
295
|
topic: topic,
|
280
296
|
}
|
281
297
|
|
298
|
+
if event.payload.key?(:exception)
|
299
|
+
increment("producer.produce.errors", tags: tags)
|
300
|
+
end
|
301
|
+
|
302
|
+
|
282
303
|
# This gets us the write rate.
|
283
304
|
increment("producer.produce.messages", tags: tags.merge(topic: topic))
|
284
305
|
|
@@ -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/producer.rb
CHANGED
@@ -1,6 +1,7 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
3
|
require "racecar/message_delivery_error"
|
4
|
+
require "racecar/delivery_callback"
|
4
5
|
|
5
6
|
at_exit do
|
6
7
|
Racecar::Producer.shutdown!
|
@@ -42,19 +43,24 @@ module Racecar
|
|
42
43
|
}
|
43
44
|
producer_config["compression.codec"] = config.producer_compression_codec.to_s unless config.producer_compression_codec.nil?
|
44
45
|
producer_config.merge!(config.rdkafka_producer)
|
45
|
-
Rdkafka::Config.new(producer_config).producer
|
46
|
+
Rdkafka::Config.new(producer_config).producer.tap do |producer|
|
47
|
+
producer.delivery_callback = DeliveryCallback.new(instrumenter: @instrumenter)
|
48
|
+
end
|
46
49
|
end
|
47
50
|
end
|
48
51
|
end
|
49
52
|
|
50
|
-
|
51
|
-
# fire and forget - you won't get any guarantees or feedback from
|
53
|
+
# fire and forget - you won't get any guarantees or feedback from
|
52
54
|
# Racecar on the status of the message and it won't halt execution
|
53
55
|
# of the rest of your code.
|
54
56
|
def produce_async(value:, topic:, **options)
|
55
57
|
with_instrumentation(action: "produce_async", value: value, topic: topic, **options) do
|
56
|
-
|
57
|
-
|
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
|
58
64
|
end
|
59
65
|
|
60
66
|
nil
|
@@ -63,8 +69,12 @@ module Racecar
|
|
63
69
|
# synchronous message production - will wait until the delivery handle succeeds, fails or times out.
|
64
70
|
def produce_sync(value:, topic:, **options)
|
65
71
|
with_instrumentation(action: "produce_sync", value: value, topic: topic, **options) do
|
66
|
-
|
67
|
-
|
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
|
68
78
|
end
|
69
79
|
|
70
80
|
nil
|
@@ -126,4 +136,4 @@ module Racecar
|
|
126
136
|
end
|
127
137
|
end
|
128
138
|
end
|
129
|
-
end
|
139
|
+
end
|
@@ -0,0 +1,58 @@
|
|
1
|
+
module Racecar
|
2
|
+
class RebalanceListener
|
3
|
+
def initialize(consumer_class, instrumenter)
|
4
|
+
@consumer_class = consumer_class
|
5
|
+
@instrumenter = instrumenter
|
6
|
+
@rdkafka_consumer = nil
|
7
|
+
end
|
8
|
+
|
9
|
+
attr_writer :rdkafka_consumer
|
10
|
+
|
11
|
+
attr_reader :consumer_class, :instrumenter, :rdkafka_consumer
|
12
|
+
private :consumer_class, :instrumenter, :rdkafka_consumer
|
13
|
+
|
14
|
+
def on_partitions_assigned(rdkafka_topic_partition_list)
|
15
|
+
event = Event.new(rdkafka_consumer: rdkafka_consumer, rdkafka_topic_partition_list: rdkafka_topic_partition_list)
|
16
|
+
|
17
|
+
instrument("partitions_assigned", partitions: event.partition_numbers) do
|
18
|
+
consumer_class.on_partitions_assigned(event)
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
22
|
+
def on_partitions_revoked(rdkafka_topic_partition_list)
|
23
|
+
event = Event.new(rdkafka_consumer: rdkafka_consumer, rdkafka_topic_partition_list: rdkafka_topic_partition_list)
|
24
|
+
|
25
|
+
instrument("partitions_revoked", partitions: event.partition_numbers) do
|
26
|
+
consumer_class.on_partitions_revoked(event)
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
private
|
31
|
+
|
32
|
+
def instrument(event, payload, &block)
|
33
|
+
instrumenter.instrument(event, payload, &block)
|
34
|
+
end
|
35
|
+
|
36
|
+
class Event
|
37
|
+
def initialize(rdkafka_topic_partition_list:, rdkafka_consumer:)
|
38
|
+
@__rdkafka_topic_partition_list = rdkafka_topic_partition_list
|
39
|
+
@__rdkafka_consumer = rdkafka_consumer
|
40
|
+
end
|
41
|
+
|
42
|
+
def topic_name
|
43
|
+
__rdkafka_topic_partition_list.to_h.keys.first
|
44
|
+
end
|
45
|
+
|
46
|
+
def partition_numbers
|
47
|
+
__rdkafka_topic_partition_list.to_h.values.flatten.map(&:partition)
|
48
|
+
end
|
49
|
+
|
50
|
+
def empty?
|
51
|
+
__rdkafka_topic_partition_list.empty?
|
52
|
+
end
|
53
|
+
|
54
|
+
# API private and not guaranteed stable
|
55
|
+
attr_reader :__rdkafka_topic_partition_list, :__rdkafka_consumer
|
56
|
+
end
|
57
|
+
end
|
58
|
+
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
|
@@ -95,7 +96,7 @@ module Racecar
|
|
95
96
|
end
|
96
97
|
ensure
|
97
98
|
producer.close
|
98
|
-
Racecar::Datadog.close if
|
99
|
+
Racecar::Datadog.close if config.datadog_enabled
|
99
100
|
@instrumenter.instrument("shut_down", instrumentation_payload || {})
|
100
101
|
end
|
101
102
|
|
@@ -130,18 +131,13 @@ module Racecar
|
|
130
131
|
|
131
132
|
def consumer
|
132
133
|
@consumer ||= begin
|
133
|
-
# Manually store offset after messages have been processed successfully
|
134
|
-
# to avoid marking failed messages as committed. The call just updates
|
135
|
-
# a value within librdkafka and is asynchronously written to proper
|
136
|
-
# storage through auto commits.
|
137
|
-
config.consumer << "enable.auto.offset.store=false"
|
138
134
|
ConsumerSet.new(config, logger, @instrumenter)
|
139
135
|
end
|
140
136
|
end
|
141
137
|
|
142
138
|
def producer
|
143
139
|
@producer ||= Rdkafka::Config.new(producer_config).producer.tap do |producer|
|
144
|
-
producer.delivery_callback =
|
140
|
+
producer.delivery_callback = Racecar::DeliveryCallback.new(instrumenter: @instrumenter)
|
145
141
|
end
|
146
142
|
end
|
147
143
|
|
@@ -160,16 +156,6 @@ module Racecar
|
|
160
156
|
producer_config
|
161
157
|
end
|
162
158
|
|
163
|
-
def delivery_callback
|
164
|
-
->(delivery_report) do
|
165
|
-
payload = {
|
166
|
-
offset: delivery_report.offset,
|
167
|
-
partition: delivery_report.partition
|
168
|
-
}
|
169
|
-
@instrumenter.instrument("acknowledged_message", payload)
|
170
|
-
end
|
171
|
-
end
|
172
|
-
|
173
159
|
def install_signal_handlers
|
174
160
|
# Stop the consumer on SIGINT, SIGQUIT or SIGTERM.
|
175
161
|
trap("QUIT") { stop }
|
@@ -222,21 +208,21 @@ module Racecar
|
|
222
208
|
}
|
223
209
|
|
224
210
|
@instrumenter.instrument("start_process_batch", instrumentation_payload)
|
225
|
-
|
226
|
-
|
227
|
-
|
211
|
+
with_pause(first.topic, first.partition, first.offset..last.offset) do |pause|
|
212
|
+
begin
|
213
|
+
@instrumenter.instrument("process_batch", instrumentation_payload) do
|
228
214
|
racecar_messages = messages.map do |message|
|
229
215
|
Racecar::Message.new(message, retries_count: pause.pauses_count)
|
230
216
|
end
|
231
217
|
processor.process_batch(racecar_messages)
|
232
218
|
processor.deliver!
|
233
219
|
consumer.store_offset(messages.last)
|
234
|
-
rescue => e
|
235
|
-
instrumentation_payload[:unrecoverable_delivery_error] = reset_producer_on_unrecoverable_delivery_errors(e)
|
236
|
-
instrumentation_payload[:retries_count] = pause.pauses_count
|
237
|
-
config.error_handler.call(e, instrumentation_payload)
|
238
|
-
raise e
|
239
220
|
end
|
221
|
+
rescue => e
|
222
|
+
instrumentation_payload[:unrecoverable_delivery_error] = reset_producer_on_unrecoverable_delivery_errors(e)
|
223
|
+
instrumentation_payload[:retries_count] = pause.pauses_count
|
224
|
+
config.error_handler.call(e, instrumentation_payload)
|
225
|
+
raise e
|
240
226
|
end
|
241
227
|
end
|
242
228
|
end
|
data/lib/racecar/version.rb
CHANGED
data/lib/racecar.rb
CHANGED
@@ -66,12 +66,16 @@ module Racecar
|
|
66
66
|
end
|
67
67
|
|
68
68
|
def self.run(processor)
|
69
|
+
runner(processor).run
|
70
|
+
end
|
71
|
+
|
72
|
+
def self.runner(processor)
|
69
73
|
runner = Runner.new(processor, config: config, logger: logger, instrumenter: config.instrumenter)
|
70
74
|
|
71
75
|
if config.parallel_workers && config.parallel_workers > 1
|
72
|
-
ParallelRunner.new(runner: runner, config: config, logger: logger)
|
76
|
+
ParallelRunner.new(runner: runner, config: config, logger: logger)
|
73
77
|
else
|
74
|
-
runner
|
78
|
+
runner
|
75
79
|
end
|
76
80
|
end
|
77
81
|
end
|
data/racecar.gemspec
CHANGED
@@ -23,13 +23,13 @@ Gem::Specification.new do |spec|
|
|
23
23
|
spec.required_ruby_version = '>= 2.6'
|
24
24
|
|
25
25
|
spec.add_runtime_dependency "king_konf", "~> 1.0.0"
|
26
|
-
spec.add_runtime_dependency "rdkafka", "~> 0.
|
26
|
+
spec.add_runtime_dependency "rdkafka", "~> 0.13.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.10.0.beta1
|
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: 2023-
|
12
|
+
date: 2023-10-09 00:00:00.000000000 Z
|
13
13
|
dependencies:
|
14
14
|
- !ruby/object:Gem::Dependency
|
15
15
|
name: king_konf
|
@@ -31,14 +31,14 @@ dependencies:
|
|
31
31
|
requirements:
|
32
32
|
- - "~>"
|
33
33
|
- !ruby/object:Gem::Version
|
34
|
-
version: 0.
|
34
|
+
version: 0.13.0
|
35
35
|
type: :runtime
|
36
36
|
prerelease: false
|
37
37
|
version_requirements: !ruby/object:Gem::Requirement
|
38
38
|
requirements:
|
39
39
|
- - "~>"
|
40
40
|
- !ruby/object:Gem::Version
|
41
|
-
version: 0.
|
41
|
+
version: 0.13.0
|
42
42
|
- !ruby/object:Gem::Dependency
|
43
43
|
name: bundler
|
44
44
|
requirement: !ruby/object:Gem::Requirement
|
@@ -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
|
@@ -200,6 +194,7 @@ 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
|
@@ -211,6 +206,7 @@ files:
|
|
211
206
|
- lib/racecar/pause.rb
|
212
207
|
- lib/racecar/producer.rb
|
213
208
|
- lib/racecar/rails_config_file_loader.rb
|
209
|
+
- lib/racecar/rebalance_listener.rb
|
214
210
|
- lib/racecar/runner.rb
|
215
211
|
- lib/racecar/version.rb
|
216
212
|
- racecar.gemspec
|