racecar 2.9.0 → 2.12.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: b394d31edcb9c83e562811ba748b6c6915388d0227a4e7425aa9aa13f64e3890
4
- data.tar.gz: 1dfcd7046b6d932f716246d0685589c1635af115a4e126067c9775b13fe05464
3
+ metadata.gz: a4e6b0c4d52637bd8c6bb2c5e3d69013889053973195a5d73ac5d5645047f985
4
+ data.tar.gz: 23425d552932665cafa03cf245d3d4ef879e053e9e9c60d1eb172455f405f3f3
5
5
  SHA512:
6
- metadata.gz: 59043be21e411e680c11815b583236556413a539f5aa9508a460eefe82cee6199f56a30b9fa07a3f206886d5dc811b210cea6130813a7073a68fe6612343ca0d
7
- data.tar.gz: d6692b9bb7cdc27efe5272a10fa1d9061920084a7b517699e5133f5e31e9fd50415e72ee40550e07e5a2fed028aeabb741422fef47372c7a53e0a96bdfc1b090
6
+ metadata.gz: 62d52be41cb3821a5d6534fea43deea8d02dd8a8f03d3449249be0fcdcbf5c842cbb2be8aac7065b97fb515b16370ab93f114ab44dd85dce6a22091d9ac04de4
7
+ data.tar.gz: 006efc7649ddb4257a80e1f2400a62e9e9cba0fa5573d3947f47293d4d77b9b701d66ebf6d465cf91fbf209771e5699ddc9cd7df238ecddf7f54ffbab4eb0c15
@@ -11,17 +11,18 @@ jobs:
11
11
  runs-on: ubuntu-latest
12
12
 
13
13
  strategy:
14
+ fail-fast: false
14
15
  matrix:
15
16
  ruby-version:
16
- - "2.7"
17
- - "3.0"
18
17
  - "3.1"
19
18
  - "3.2"
19
+ - "3.3"
20
+ - "3.4"
20
21
 
21
22
  steps:
22
- - uses: zendesk/checkout@v3
23
+ - uses: actions/checkout@v4
23
24
  - name: Set up Ruby
24
- uses: zendesk/setup-ruby@v1
25
+ uses: ruby/setup-ruby@v1
25
26
  with:
26
27
  ruby-version: ${{ matrix.ruby-version }}
27
28
  bundler-cache: true
@@ -30,15 +31,23 @@ jobs:
30
31
 
31
32
  integration-specs:
32
33
  runs-on: ubuntu-latest
34
+ strategy:
35
+ fail-fast: false
36
+ matrix:
37
+ ruby-version:
38
+ - "3.1"
39
+ - "3.2"
40
+ - "3.3"
41
+ - "3.4"
33
42
  steps:
34
- - uses: zendesk/checkout@v3
43
+ - uses: actions/checkout@v4
35
44
  - name: Set up Ruby
36
- uses: zendesk/setup-ruby@v1
45
+ uses: ruby/setup-ruby@v1
37
46
  with:
38
- ruby-version: "2.7"
47
+ ruby-version: ${{ matrix.ruby-version }}
39
48
  bundler-cache: true
40
49
  - name: Bring up docker-compose stack
41
- run: docker-compose up -d
50
+ run: docker compose up -d
42
51
  - name: Build and test with RSpec
43
52
  env:
44
53
  RACECAR_BROKERS: localhost:9092
data/.rspec CHANGED
@@ -1,3 +1,4 @@
1
1
  --format documentation
2
2
  --require spec_helper
3
3
  --color
4
+ --order random
data/CHANGELOG.md CHANGED
@@ -2,7 +2,31 @@
2
2
 
3
3
  ## Unreleased
4
4
 
5
- * Test with Ruby 3.2
5
+ ## 2.12.0
6
+
7
+ * Add tests against Ruby 3.4
8
+ * Drop support for Ruby 3.0
9
+ * Allow rdkafka gem versions newer than 0.15.0
10
+
11
+ ## 2.11.0
12
+
13
+ * Allow the liveness probe command to skip loading config files
14
+ * Add unix domain socket support for Datadog StatsD metrics
15
+ * Bump minimum rdkafka gem version to 0.15.0
16
+ * Bump minimum Ruby version to 3.0
17
+ * Configurable strategy for consuming multiple topics
18
+
19
+ ## 2.10.0
20
+
21
+ * Bump rdkafka gem version to 0.13.0
22
+ * Support cooperative-sticky
23
+ * Instrument produce delivery errors
24
+ * Resolve Rails 7.1 logger incompatibility
25
+ * Don't load rails env for liveness probe
26
+ * Fix config load for liveness probe
27
+ * Send exceptions to `process_batch` instrumenter
28
+ * Docker test fixes
29
+ * Test in CI with Ruby 3.2
6
30
 
7
31
  ## v2.9.0, v2.9.0.beta1
8
32
 
@@ -12,7 +36,7 @@
12
36
  * Add built-in liveness probe for Kubernetes deployments.
13
37
 
14
38
  ## v2.8.2
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))
39
+ * Handles ErroneousStateError, in previous versions the consumer would do several unnecessary group leave/joins. The log level is also changed to WARN instead of ERROR. ([#295](https://github.com/zendesk/racecar/pull/295))
16
40
 
17
41
  ## v2.8.1
18
42
  * Adds new ErroneousStateError to racecar in order to give more information on this new possible exception.
data/Dockerfile CHANGED
@@ -1,4 +1,4 @@
1
- FROM circleci/ruby:2.7.2
1
+ FROM cimg/ruby:2.7.8
2
2
 
3
3
  RUN sudo apt-get update
4
4
  RUN sudo apt-get install docker
data/Gemfile CHANGED
@@ -5,6 +5,4 @@ source 'https://rubygems.org'
5
5
  # Specify your gem's dependencies in racecar.gemspec
6
6
  gemspec
7
7
 
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'
8
+ gem 'activesupport', '~> 7.2.0'
data/Gemfile.lock CHANGED
@@ -1,31 +1,43 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- racecar (2.9.0.beta1)
4
+ racecar (2.12.0)
5
5
  king_konf (~> 1.0.0)
6
- rdkafka (~> 0.12.0)
6
+ rdkafka (>= 0.15.0)
7
7
 
8
8
  GEM
9
9
  remote: https://rubygems.org/
10
10
  specs:
11
- activesupport (6.1.7.3)
12
- concurrent-ruby (~> 1.0, >= 1.0.2)
11
+ activesupport (7.2.2.1)
12
+ base64
13
+ benchmark (>= 0.3)
14
+ bigdecimal
15
+ concurrent-ruby (~> 1.0, >= 1.3.1)
16
+ connection_pool (>= 2.2.5)
17
+ drb
13
18
  i18n (>= 1.6, < 2)
19
+ logger (>= 1.4.2)
14
20
  minitest (>= 5.1)
15
- tzinfo (~> 2.0)
16
- zeitwerk (~> 2.3)
21
+ securerandom (>= 0.3)
22
+ tzinfo (~> 2.0, >= 2.0.5)
23
+ base64 (0.2.0)
24
+ benchmark (0.4.0)
25
+ bigdecimal (3.1.9)
17
26
  byebug (11.1.3)
18
27
  coderay (1.1.3)
19
- concurrent-ruby (1.2.2)
28
+ concurrent-ruby (1.3.4)
29
+ connection_pool (2.4.1)
20
30
  diff-lcs (1.5.0)
21
31
  dogstatsd-ruby (5.5.0)
22
- ffi (1.15.5)
32
+ drb (2.2.1)
33
+ ffi (1.16.3)
23
34
  i18n (1.12.0)
24
35
  concurrent-ruby (~> 1.0)
25
36
  king_konf (1.0.1)
37
+ logger (1.6.4)
26
38
  method_source (1.0.0)
27
- mini_portile2 (2.8.1)
28
- minitest (5.18.0)
39
+ mini_portile2 (2.8.5)
40
+ minitest (5.25.4)
29
41
  pry (0.14.2)
30
42
  coderay (~> 1.1)
31
43
  method_source (~> 1.0)
@@ -33,7 +45,7 @@ GEM
33
45
  byebug (~> 11.0)
34
46
  pry (>= 0.13, < 0.15)
35
47
  rake (13.0.6)
36
- rdkafka (0.12.0)
48
+ rdkafka (0.18.0)
37
49
  ffi (~> 1.15)
38
50
  mini_portile2 (~> 2.6)
39
51
  rake (> 12)
@@ -50,16 +62,16 @@ GEM
50
62
  diff-lcs (>= 1.2.0, < 2.0)
51
63
  rspec-support (~> 3.12.0)
52
64
  rspec-support (3.12.0)
65
+ securerandom (0.4.1)
53
66
  timecop (0.9.6)
54
67
  tzinfo (2.0.6)
55
68
  concurrent-ruby (~> 1.0)
56
- zeitwerk (2.6.7)
57
69
 
58
70
  PLATFORMS
59
71
  ruby
60
72
 
61
73
  DEPENDENCIES
62
- activesupport (~> 6.1.0)
74
+ activesupport (~> 7.2.0)
63
75
  bundler (>= 1.13, < 3)
64
76
  dogstatsd-ruby (>= 4.0.0, < 6.0.0)
65
77
  pry-byebug
@@ -69,4 +81,4 @@ DEPENDENCIES
69
81
  timecop
70
82
 
71
83
  BUNDLED WITH
72
- 2.4.9
84
+ 2.6.2
data/README.md CHANGED
@@ -414,6 +414,7 @@ Racecar supports [Datadog](https://www.datadoghq.com/) monitoring integration. I
414
414
  - `datadog_enabled` – Whether Datadog monitoring is enabled (defaults to `false`).
415
415
  - `datadog_host` – The host running the Datadog agent.
416
416
  - `datadog_port` – The port of the Datadog agent.
417
+ - `datadog_socket_path` – The unix domain socket of the Datadog agent (when set takes precedence over host/port).
417
418
  - `datadog_namespace` – The namespace to use for Datadog metrics.
418
419
  - `datadog_tags` – Tags that should always be set on Datadog metrics.
419
420
 
@@ -476,6 +477,8 @@ With Foreman, you can easily run these processes locally by executing `foreman r
476
477
 
477
478
  If you run your applications in Kubernetes, use the following [Deployment](https://kubernetes.io/docs/concepts/workloads/controllers/deployment/) spec as a starting point:
478
479
 
480
+ ##### Recreate Strategy
481
+
479
482
  ```yaml
480
483
  apiVersion: apps/v1
481
484
  kind: Deployment
@@ -483,8 +486,8 @@ metadata:
483
486
  name: my-racecar-deployment
484
487
  labels:
485
488
  app: my-racecar
486
- spec:
487
- replicas: 3 # <-- this will give us three consumers in the group.
489
+ spec
490
+ replicas: 4 # <-- this is a good value if you have a multliple of 4 partitions
488
491
  selector:
489
492
  matchLabels:
490
493
  app: my-racecar
@@ -506,24 +509,47 @@ spec:
506
509
  value: 5
507
510
  ```
508
511
 
509
- The important part is the `strategy.type` value, which tells Kubernetes how to upgrade from one version of your Deployment to another. Many services use so-called _rolling updates_, where some but not all containers are replaced with the new version. This is done so that, if the new version doesn't work, the old version is still there to serve most of the requests. For Kafka consumers, this doesn't work well. The reason is that every time a consumer joins or leaves a group, every other consumer in the group needs to stop and synchronize the list of partitions assigned to each group member. So if the group is updated in a rolling fashion, this synchronization would occur over and over again, causing undesirable double-processing of messages as consumers would start only to be synchronized shortly after.
512
+ This configuration uses the recreate strategy which completely terminates all consumers before starting new ones.
513
+ It's simple and easy to understand but can result in significant 'downtime' where no messages are processed.
514
+
515
+ ##### Rolling Updates and 'sticky-cooperative' Assignment
510
516
 
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.
517
+ 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.
518
+ This can be combined with a restricted rolling update to minimize processing downtime.
519
+
520
+ Add to your Racecar config:
521
+ ```ruby
522
+ Racecar.configure do |c|
523
+ c.partition_assignment_strategy = "cooperative-sticky"
524
+ end
525
+ ```
526
+
527
+ Replace the Kubernetes deployment strategy with:
528
+ ```yaml
529
+ strategy:
530
+ type: RollingUpdate
531
+ rollingUpdate:
532
+ maxSurge: 0 # <- Never boot an excess consumer
533
+ maxUnavailable: 1 # <- The deploy 'rolls' one consumer at a time
534
+ ```
535
+
536
+ These two configurations should be deployed together.
537
+
538
+ While `maxSurge` should always be 0, `maxUnavailable` can be increased to reduce deployment times in exchange for longer pauses in message processing.
512
539
 
513
540
  #### Liveness Probe
514
541
 
515
542
  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
543
 
517
544
  To use this feature:
518
- - set the `liveness_probe_enabled` config option to true.
519
- - configure your Kubernetes deployment to run `$ racecarctl liveness_probe`
545
+ 1. Set the `liveness_probe_enabled` config option to true.
546
+ 2. Configure your Kubernetes deployment liveness probe to run this command `$ racecarctl liveness_probe`
520
547
 
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).
548
+ When enabled (see config) Racecar will touch the file at the specified path each time it finishes polling Kafka and processing the messages in the batch (if any).
523
549
 
524
550
  The modified time of this file can be observed to determine when the consumer last exhibited 'liveness'.
525
551
 
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`.
552
+ Running `racecarctl liveness_probe` will return a successful exit status if the last 'liveness' event happened within an acceptable time, which you can set as `liveness_probe_max_interval`.
527
553
 
528
554
  `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
555
 
@@ -531,9 +557,15 @@ On receiving `SIGTERM`, Racecar will gracefully shut down and delete this file,
531
557
 
532
558
  You may wish to tolerate more than one failed probe run to accommodate for environmental variance and clock changes.
533
559
 
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.
560
+ The [Configuration section](https://github.com/zendesk/racecar#configuration) for the various ways the liveness probe can be configured. (We recommend environment variables).
561
+
562
+ ##### Slow racecar.rb / racecar.yml? Skip config files!
535
563
 
536
- Here is an example Kubernetes liveness probe configuration:
564
+ If your config files need to do something expensive, such as load Rails, you can enable `RACECAR_LIVENESS_PROBE_SKIP_CONFIG_FILES`. The liveness probe command will then skip loading your configuration and execute quickly.
565
+
566
+ Most other configuration values can be set via the environment, we recommend you do this for liveness probe settings.
567
+
568
+ ##### Example Kubernetes Configuration
537
569
 
538
570
  ```yaml
539
571
  apiVersion: apps/v1
@@ -549,8 +581,15 @@ spec:
549
581
  - SomeConsumer
550
582
 
551
583
  env:
584
+ # Skip config loading to run fast, only the following values are needed
585
+ - name: RACECAR_LIVENESS_PROBE_SKIP_CONFIG_FILES
586
+ value: "true"
552
587
  - name: RACECAR_LIVENESS_PROBE_ENABLED
553
588
  value: "true"
589
+ - name: RACECAR_LIVENESS_PROBE_FILE_PATH
590
+ value: "/tmp/racecar-liveness"
591
+ - name: RACECAR_LIVENESS_PROBE_MAX_INTERVAL
592
+ value: "5"
554
593
 
555
594
  livenessProbe:
556
595
  exec:
@@ -558,8 +597,8 @@ spec:
558
597
  - racecarctl
559
598
  - liveness_probe
560
599
 
561
- # Allow up to 10 consecutive failures before terminating Pod:
562
- failureThreshold: 10
600
+ # Allow up to 3 consecutive failures before terminating Pod:
601
+ failureThreshold: 3
563
602
 
564
603
  # Wait 30 seconds before starting the probes:
565
604
  initialDelaySeconds: 30
@@ -663,13 +702,13 @@ In order to introspect the configuration of a consumer process, send it the `SIG
663
702
 
664
703
  ### Upgrading from v1 to v2
665
704
 
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.
705
+ 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.
667
706
 
668
707
  ### Compression
669
708
 
670
709
  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
710
 
672
- ```
711
+ ```
673
712
  apt-get update && apt-get install -y libzstd-dev
674
713
  ```
675
714
 
@@ -677,16 +716,16 @@ apt-get update && apt-get install -y libzstd-dev
677
716
 
678
717
  After checking out the repo, run `bin/setup` to install dependencies. Then, run `rspec` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment.
679
718
 
680
- The integration tests run against a Kafka instance that is not automatically started from within `rspec`. You can set one up using the provided `docker-compose.yml` by running `docker-compose up`.
719
+ The integration tests run against a Kafka instance that is not automatically started from within `rspec`. You can set one up using the provided `docker-compose.yml` by running `docker compose up`.
681
720
 
682
721
  ### Running RSpec within Docker
683
722
 
684
- 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.2 image. This could easily be extended with more Dockerfiles to cover different Ruby versions if desired. In order to run the specs via Docker:
723
+ 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:
685
724
 
686
725
  - Uncomment the `tests` service from the docker-compose.yml
687
- - Bring up the stack with `docker-compose up -d`
688
- - Execute the entire suite with `docker-compose run --rm tests rspec`
689
- - Execute a single spec or directory with `docker-compose run --rm tests rspec spec/integration/consumer_spec.rb`
726
+ - Bring up the stack with `docker compose up -d`
727
+ - Execute the entire suite with `docker compose run --rm tests bundle exec rspec`
728
+ - Execute a single spec or directory with `docker compose run --rm tests bundle exec rspec spec/integration/consumer_spec.rb`
690
729
 
691
730
  Please note - your code directory is mounted as a volume, so you can make code changes without needing to rebuild
692
731
 
data/docker-compose.yml CHANGED
@@ -1,8 +1,6 @@
1
- version: '2.1'
2
-
3
1
  services:
4
2
  zookeeper:
5
- image: confluentinc/cp-zookeeper:5.5.1
3
+ image: confluentinc/cp-zookeeper:7.8.1
6
4
  ports:
7
5
  - "2181:2181"
8
6
  environment:
@@ -13,7 +11,7 @@ services:
13
11
  test: echo ruok | nc 127.0.0.1 2181 | grep imok
14
12
 
15
13
  broker:
16
- image: confluentinc/cp-kafka:5.5.1
14
+ image: confluentinc/cp-kafka:7.8.1
17
15
  depends_on:
18
16
  - zookeeper
19
17
  ports:
@@ -57,7 +55,7 @@ services:
57
55
  # RACECAR_BROKERS: broker:29092
58
56
  # DOCKER_SUDO: 'true'
59
57
  # # When bringing up the stack, we just let the container exit. For running the
60
- # # specs, we'll use commands like `docker-compose run tests rspec`
58
+ # # specs, we'll use commands like `docker compose run tests rspec`
61
59
  # command: ["echo", "ready"]
62
60
  # volumes:
63
61
  # # The line below allows us to run docker commands from the container itself
data/lib/racecar/cli.rb CHANGED
@@ -157,10 +157,11 @@ module Racecar
157
157
  require_relative './datadog'
158
158
 
159
159
  Datadog.configure do |datadog|
160
- datadog.host = config.datadog_host unless config.datadog_host.nil?
161
- datadog.port = config.datadog_port unless config.datadog_port.nil?
162
- datadog.namespace = config.datadog_namespace unless config.datadog_namespace.nil?
163
- datadog.tags = config.datadog_tags unless config.datadog_tags.nil?
160
+ datadog.host = config.datadog_host unless config.datadog_host.nil?
161
+ datadog.port = config.datadog_port unless config.datadog_port.nil?
162
+ datadog.socket_path = config.datadog_socket_path unless config.datadog_socket_path.nil?
163
+ datadog.namespace = config.datadog_namespace unless config.datadog_namespace.nil?
164
+ datadog.tags = config.datadog_tags unless config.datadog_tags.nil?
164
165
  end
165
166
  end
166
167
  end
@@ -29,6 +29,9 @@ module Racecar
29
29
  desc "The minimum number of messages in the local consumer queue"
30
30
  integer :min_message_queue_size, default: 2000
31
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
+
32
35
  desc "Kafka consumer configuration options, separated with '=' -- https://github.com/edenhill/librdkafka/blob/master/CONFIGURATION.md"
33
36
  list :consumer, default: []
34
37
 
@@ -155,6 +158,9 @@ module Racecar
155
158
  desc "The port of the Datadog agent"
156
159
  integer :datadog_port
157
160
 
161
+ desc "The unix domain socket of the Datadog agent (when set takes precedence over host/port)"
162
+ string :datadog_socket_path
163
+
158
164
  desc "The namespace to use for Datadog metrics"
159
165
  string :datadog_namespace
160
166
 
@@ -182,6 +188,12 @@ module Racecar
182
188
  desc "Used only by the liveness probe: Max time (in seconds) between liveness events before the process is considered not healthy"
183
189
  integer :liveness_probe_max_interval, default: 5
184
190
 
191
+ desc "Allows the liveness probe command to skip loading config files. When enabled, configure liveness probe values via environmental variables. Defaults still apply. Only applies to the liveness probe command."
192
+ boolean :liveness_probe_skip_config_files, default: false
193
+
194
+ desc "Strategy for switching topics when there are multiple subscriptions. `exhaust-topic` will only switch when the consumer poll returns no messages. `round-robin` will switch after each poll regardless.\nWarning: `round-robin` will be the default in Racecar 3.x"
195
+ string :multi_subscription_strategy, allowed_values: %w(round-robin exhaust-topic), default: "exhaust-topic"
196
+
185
197
  # The error handler must be set directly on the object.
186
198
  attr_reader :error_handler
187
199
 
@@ -296,10 +308,6 @@ module Racecar
296
308
  )
297
309
  end
298
310
 
299
- def rebalance_listener
300
- RebalanceListener.new(self)
301
- end
302
-
303
311
  private
304
312
 
305
313
  def rdkafka_security_config
@@ -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)
@@ -64,16 +68,16 @@ module Racecar
64
68
  @instrumenter.instrument('deliver_messages', instrumentation_payload) do
65
69
  @delivery_handles.each do |handle|
66
70
  begin
67
- # rdkafka-ruby checks every wait_timeout seconds if the message was
68
- # successfully delivered, up to max_wait_timeout seconds before raising
69
- # Rdkafka::AbstractHandle::WaitTimeoutError. librdkafka will (re)try to
70
- # deliver all messages in the background, until "config.message_timeout"
71
+ # rdkafka-ruby checks with exponential backoff starting at 0 seconds wait
72
+ # if the message was successfully delivered, up to max_wait_timeout seconds
73
+ # before raising Rdkafka::AbstractHandle::WaitTimeoutError. librdkafka will
74
+ # (re)try to deliver all messages in the background, until "config.message_timeout"
71
75
  # (message.timeout.ms) is exceeded. Phrased differently, rdkafka-ruby's
72
76
  # WaitTimeoutError is just informative.
73
77
  # The raising can be avoided if max_wait_timeout below is greater than
74
78
  # config.message_timeout, but config is not available here (without
75
79
  # changing the interface).
76
- handle.wait(max_wait_timeout: 60, wait_timeout: 0.1)
80
+ handle.wait(max_wait_timeout: 60)
77
81
  rescue Rdkafka::AbstractHandle::WaitTimeoutError => e
78
82
  partition = MessageDeliveryError.partition_from_delivery_handle(handle)
79
83
  # ideally we could use the logger passed to the Runner, but it is not
@@ -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,14 +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))
73
- consumer_config.consumer_rebalance_listener = @config.rebalance_listener
74
-
75
+ listener = RebalanceListener.new(@config.consumer_class, @instrumenter)
76
+ consumer_config.consumer_rebalance_listener = listener
75
77
  consumer = consumer_config.consumer
78
+ listener.rdkafka_consumer = consumer
79
+
76
80
  @instrumenter.instrument('join_group') do
77
81
  consumer.subscribe current_subscription.topic
78
82
  end
@@ -98,16 +102,25 @@ module Racecar
98
102
  consumer.pause(filtered_tpl)
99
103
  fake_msg = OpenStruct.new(topic: topic, partition: partition, offset: offset)
100
104
  consumer.seek(fake_msg)
105
+
106
+ @paused_tpls[topic][partition] = [consumer, filtered_tpl]
101
107
  end
102
108
 
103
109
  def resume(topic, partition)
104
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
+
105
116
  if !consumer
106
117
  @logger.info "Attempted to resume #{topic}/#{partition}, but we're not subscribed to it"
107
118
  return
108
119
  end
109
120
 
110
121
  consumer.resume(filtered_tpl)
122
+ @paused_tpls[topic].delete(partition)
123
+ @paused_tpls.delete(topic) if @paused_tpls[topic].empty?
111
124
  end
112
125
 
113
126
  alias :each :each_subscribed
@@ -162,6 +175,7 @@ module Racecar
162
175
 
163
176
  # polls a message for the current consumer, handling any API edge cases.
164
177
  def poll_current_consumer(max_wait_time_ms)
178
+ @last_poll_read_nil_message = false
165
179
  msg = current.poll(max_wait_time_ms)
166
180
  rescue Rdkafka::RdkafkaError => e
167
181
  case e.code
@@ -199,9 +213,14 @@ module Racecar
199
213
  end
200
214
 
201
215
  def maybe_select_next_consumer
202
- return unless @last_poll_read_nil_message
203
- @last_poll_read_nil_message = false
204
- select_next_consumer
216
+ case @config.multi_subscription_strategy
217
+ when "round-robin"
218
+ select_next_consumer
219
+ else # "exhaust-topic"
220
+ if @last_poll_read_nil_message
221
+ select_next_consumer
222
+ end
223
+ end
205
224
  end
206
225
 
207
226
  def select_next_consumer
@@ -218,6 +237,11 @@ module Racecar
218
237
  def rdkafka_config(subscription)
219
238
  # https://github.com/edenhill/librdkafka/blob/master/CONFIGURATION.md
220
239
  config = {
240
+ # Manually store offset after messages have been processed successfully
241
+ # to avoid marking failed messages as committed. The call just updates
242
+ # a value within librdkafka and is asynchronously written to proper
243
+ # storage through auto commits.
244
+ "enable.auto.offset.store" => false,
221
245
  "auto.commit.interval.ms" => @config.offset_commit_interval * 1000,
222
246
  "auto.offset.reset" => subscription.start_from_beginning ? "earliest" : "largest",
223
247
  "bootstrap.servers" => @config.brokers.join(","),
@@ -233,7 +257,8 @@ module Racecar
233
257
  "queued.min.messages" => @config.min_message_queue_size,
234
258
  "session.timeout.ms" => @config.session_timeout * 1000,
235
259
  "socket.timeout.ms" => @config.socket_timeout * 1000,
236
- "statistics.interval.ms" => @config.statistics_interval_ms
260
+ "statistics.interval.ms" => @config.statistics_interval_ms,
261
+ "partition.assignment.strategy" => @config.partition_assignment_strategy,
237
262
  }
238
263
  config.merge! @config.rdkafka_consumer
239
264
  config.merge! subscription.additional_config
data/lib/racecar/ctl.rb CHANGED
@@ -36,8 +36,14 @@ module Racecar
36
36
  require "racecar/liveness_probe"
37
37
  parse_options!(args)
38
38
 
39
- if ENV["RAILS_ENV"]
40
- Racecar.config.load_file("config/racecar.yml", ENV["RAILS_ENV"])
39
+ unless config.liveness_probe_skip_config_files
40
+ if File.exist?("config/racecar.rb")
41
+ require "./config/racecar"
42
+ end
43
+
44
+ if ENV["RAILS_ENV"] && File.exist?("config/racecar.yml")
45
+ Racecar.config.load_file("config/racecar.yml", ENV["RAILS_ENV"])
46
+ end
41
47
  end
42
48
 
43
49
  Racecar.config.liveness_probe.check_liveness_within_interval!
@@ -136,5 +142,9 @@ module Racecar
136
142
 
137
143
  parser.parse!(args)
138
144
  end
145
+
146
+ def config
147
+ Racecar.config
148
+ end
139
149
  end
140
150
  end
@@ -19,7 +19,11 @@ module Racecar
19
19
  end
20
20
 
21
21
  def statsd
22
- @statsd ||= ::Datadog::Statsd.new(host, port, namespace: namespace, tags: tags)
22
+ @statsd ||= if socket_path
23
+ ::Datadog::Statsd.new(socket_path: socket_path, namespace: namespace, tags: tags)
24
+ else
25
+ ::Datadog::Statsd.new(host, port, namespace: namespace, tags: tags)
26
+ end
23
27
  end
24
28
 
25
29
  def statsd=(statsd)
@@ -45,6 +49,15 @@ module Racecar
45
49
  clear
46
50
  end
47
51
 
52
+ def socket_path
53
+ @socket_path
54
+ end
55
+
56
+ def socket_path=(socket_path)
57
+ @socket_path = socket_path
58
+ clear
59
+ end
60
+
48
61
  def namespace
49
62
  @namespace ||= STATSD_NAMESPACE
50
63
  end
@@ -40,6 +40,7 @@ module Racecar
40
40
  "client.id" => config.client_id,
41
41
  "statistics.interval.ms" => config.statistics_interval_ms,
42
42
  "message.timeout.ms" => config.message_timeout * 1000,
43
+ "partitioner" => config.partitioner.to_s
43
44
  }
44
45
  producer_config["compression.codec"] = config.producer_compression_codec.to_s unless config.producer_compression_codec.nil?
45
46
  producer_config.merge!(config.rdkafka_producer)
@@ -26,7 +26,11 @@ module Racecar
26
26
  console = ActiveSupport::Logger.new($stdout)
27
27
  console.formatter = Rails.logger.formatter
28
28
  console.level = Rails.logger.level
29
- Rails.logger.extend(ActiveSupport::Logger.broadcast(console))
29
+ if ::Rails::VERSION::STRING < "7.1"
30
+ Rails.logger.extend(ActiveSupport::Logger.broadcast(console))
31
+ else
32
+ Rails.logger = ActiveSupport::BroadcastLogger.new(Rails.logger, console)
33
+ end
30
34
  end
31
35
  end
32
36
  end
@@ -1,22 +1,58 @@
1
1
  module Racecar
2
2
  class RebalanceListener
3
- def initialize(config)
4
- @config = config
5
- @consumer_class = config.consumer_class
3
+ def initialize(consumer_class, instrumenter)
4
+ @consumer_class = consumer_class
5
+ @instrumenter = instrumenter
6
+ @rdkafka_consumer = nil
6
7
  end
7
8
 
8
- attr_reader :config, :consumer_class
9
+ attr_writer :rdkafka_consumer
9
10
 
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
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)
14
34
  end
15
35
 
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
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
20
56
  end
21
57
  end
22
58
  end
@@ -96,7 +96,7 @@ module Racecar
96
96
  end
97
97
  ensure
98
98
  producer.close
99
- Racecar::Datadog.close if Object.const_defined?("Racecar::Datadog")
99
+ Racecar::Datadog.close if config.datadog_enabled
100
100
  @instrumenter.instrument("shut_down", instrumentation_payload || {})
101
101
  end
102
102
 
@@ -131,11 +131,6 @@ module Racecar
131
131
 
132
132
  def consumer
133
133
  @consumer ||= begin
134
- # Manually store offset after messages have been processed successfully
135
- # to avoid marking failed messages as committed. The call just updates
136
- # a value within librdkafka and is asynchronously written to proper
137
- # storage through auto commits.
138
- config.consumer << "enable.auto.offset.store=false"
139
134
  ConsumerSet.new(config, logger, @instrumenter)
140
135
  end
141
136
  end
@@ -213,21 +208,21 @@ module Racecar
213
208
  }
214
209
 
215
210
  @instrumenter.instrument("start_process_batch", instrumentation_payload)
216
- @instrumenter.instrument("process_batch", instrumentation_payload) do
217
- with_pause(first.topic, first.partition, first.offset..last.offset) do |pause|
218
- begin
211
+ with_pause(first.topic, first.partition, first.offset..last.offset) do |pause|
212
+ begin
213
+ @instrumenter.instrument("process_batch", instrumentation_payload) do
219
214
  racecar_messages = messages.map do |message|
220
215
  Racecar::Message.new(message, retries_count: pause.pauses_count)
221
216
  end
222
217
  processor.process_batch(racecar_messages)
223
218
  processor.deliver!
224
219
  consumer.store_offset(messages.last)
225
- rescue => e
226
- instrumentation_payload[:unrecoverable_delivery_error] = reset_producer_on_unrecoverable_delivery_errors(e)
227
- instrumentation_payload[:retries_count] = pause.pauses_count
228
- config.error_handler.call(e, instrumentation_payload)
229
- raise e
230
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
231
226
  end
232
227
  end
233
228
  end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Racecar
4
- VERSION = "2.9.0"
4
+ VERSION = "2.12.0"
5
5
  end
data/racecar.gemspec CHANGED
@@ -20,10 +20,10 @@ Gem::Specification.new do |spec|
20
20
  spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
21
21
  spec.require_paths = ["lib"]
22
22
 
23
- spec.required_ruby_version = '>= 2.6'
23
+ spec.required_ruby_version = '>= 3.1'
24
24
 
25
25
  spec.add_runtime_dependency "king_konf", "~> 1.0.0"
26
- spec.add_runtime_dependency "rdkafka", "~> 0.12.0"
26
+ spec.add_runtime_dependency "rdkafka", ">= 0.15.0"
27
27
 
28
28
  spec.add_development_dependency "bundler", [">= 1.13", "< 3"]
29
29
  spec.add_development_dependency "pry-byebug"
metadata CHANGED
@@ -1,15 +1,15 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: racecar
3
3
  version: !ruby/object:Gem::Version
4
- version: 2.9.0
4
+ version: 2.12.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Daniel Schierbeck
8
8
  - Benjamin Quorning
9
- autorequire:
9
+ autorequire:
10
10
  bindir: exe
11
11
  cert_chain: []
12
- date: 2023-09-25 00:00:00.000000000 Z
12
+ date: 2025-02-27 00:00:00.000000000 Z
13
13
  dependencies:
14
14
  - !ruby/object:Gem::Dependency
15
15
  name: king_konf
@@ -29,16 +29,16 @@ dependencies:
29
29
  name: rdkafka
30
30
  requirement: !ruby/object:Gem::Requirement
31
31
  requirements:
32
- - - "~>"
32
+ - - ">="
33
33
  - !ruby/object:Gem::Version
34
- version: 0.12.0
34
+ version: 0.15.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.12.0
41
+ version: 0.15.0
42
42
  - !ruby/object:Gem::Dependency
43
43
  name: bundler
44
44
  requirement: !ruby/object:Gem::Requirement
@@ -149,7 +149,7 @@ dependencies:
149
149
  - - ">="
150
150
  - !ruby/object:Gem::Version
151
151
  version: '0'
152
- description:
152
+ description:
153
153
  email:
154
154
  - dschierbeck@zendesk.com
155
155
  - bquorning@zendesk.com
@@ -214,7 +214,7 @@ homepage: https://github.com/zendesk/racecar
214
214
  licenses:
215
215
  - Apache License Version 2.0
216
216
  metadata: {}
217
- post_install_message:
217
+ post_install_message:
218
218
  rdoc_options: []
219
219
  require_paths:
220
220
  - lib
@@ -222,15 +222,15 @@ required_ruby_version: !ruby/object:Gem::Requirement
222
222
  requirements:
223
223
  - - ">="
224
224
  - !ruby/object:Gem::Version
225
- version: '2.6'
225
+ version: '3.1'
226
226
  required_rubygems_version: !ruby/object:Gem::Requirement
227
227
  requirements:
228
228
  - - ">="
229
229
  - !ruby/object:Gem::Version
230
230
  version: '0'
231
231
  requirements: []
232
- rubygems_version: 3.0.3.1
233
- signing_key:
232
+ rubygems_version: 3.5.22
233
+ signing_key:
234
234
  specification_version: 4
235
235
  summary: A framework for running Kafka consumers
236
236
  test_files: []