racecar 2.11.0 → 3.0.0.alpha.1
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 +4 -4
- data/.github/CODEOWNERS +4 -0
- data/.github/workflows/ci.yml +14 -5
- data/.github/workflows/publish.yml +21 -7
- data/.rspec +1 -0
- data/CHANGELOG.md +6 -0
- data/Gemfile +1 -3
- data/Gemfile.lock +24 -12
- data/README.md +21 -5
- data/Rakefile +1 -3
- data/docker-compose.yml +3 -5
- data/lib/racecar/async_partition_processor.rb +142 -0
- data/lib/racecar/cli.rb +1 -2
- data/lib/racecar/config.rb +12 -0
- data/lib/racecar/consumer.rb +5 -5
- data/lib/racecar/consumer_set.rb +48 -22
- data/lib/racecar/partition_processor.rb +217 -0
- data/lib/racecar/pause.rb +16 -0
- data/lib/racecar/producer.rb +1 -0
- data/lib/racecar/rebalance_listener.rb +12 -2
- data/lib/racecar/runner.rb +110 -209
- data/lib/racecar/version.rb +1 -1
- data/lib/racecar.rb +4 -4
- data/racecar.gemspec +2 -2
- metadata +9 -10
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 8d48006d6ad7a9f1cb57902edbd8a2785f3e05c629a71c3345238a0841abeda6
|
|
4
|
+
data.tar.gz: 06b557c5757e1c4bda2d1d6cebd2e679ab27053bc02f58636911f8df8d67f217
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 9e6cd591d16f776a84140105fd70eb565c70c6001ac03c5f61ea0ef45b40a8fd29e656a8da4b445894c87d9bb6002376364d8de896af481847222e4585b37374
|
|
7
|
+
data.tar.gz: 6f1b2afeabde54e49e61ac8242b5a97daf3425f64bbe74bd3d0b69259a67d53a3113224273882779f6e12fc4bf97d1851169640c32a8ed56dcae6ce0b14a00f8
|
data/.github/CODEOWNERS
ADDED
data/.github/workflows/ci.yml
CHANGED
|
@@ -2,21 +2,22 @@ name: CI
|
|
|
2
2
|
|
|
3
3
|
on:
|
|
4
4
|
pull_request:
|
|
5
|
-
branches: ["
|
|
5
|
+
branches: ["main"]
|
|
6
6
|
push:
|
|
7
|
-
branches: ["
|
|
7
|
+
branches: ["main"]
|
|
8
8
|
|
|
9
9
|
jobs:
|
|
10
10
|
unit-specs:
|
|
11
11
|
runs-on: ubuntu-latest
|
|
12
12
|
|
|
13
13
|
strategy:
|
|
14
|
+
fail-fast: false
|
|
14
15
|
matrix:
|
|
15
16
|
ruby-version:
|
|
16
|
-
- "3.0"
|
|
17
17
|
- "3.1"
|
|
18
18
|
- "3.2"
|
|
19
19
|
- "3.3"
|
|
20
|
+
- "3.4"
|
|
20
21
|
|
|
21
22
|
steps:
|
|
22
23
|
- uses: actions/checkout@v4
|
|
@@ -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
43
|
- uses: actions/checkout@v4
|
|
35
44
|
- name: Set up Ruby
|
|
36
45
|
uses: ruby/setup-ruby@v1
|
|
37
46
|
with:
|
|
38
|
-
ruby-version:
|
|
47
|
+
ruby-version: ${{ matrix.ruby-version }}
|
|
39
48
|
bundler-cache: true
|
|
40
49
|
- name: Bring up docker-compose stack
|
|
41
|
-
run: docker
|
|
50
|
+
run: docker compose up -d
|
|
42
51
|
- name: Build and test with RSpec
|
|
43
52
|
env:
|
|
44
53
|
RACECAR_BROKERS: localhost:9092
|
|
@@ -1,12 +1,26 @@
|
|
|
1
|
-
name: Publish
|
|
1
|
+
name: Publish to RubyGems.org
|
|
2
2
|
|
|
3
3
|
on:
|
|
4
4
|
push:
|
|
5
|
-
|
|
5
|
+
branches: main
|
|
6
|
+
paths: lib/racecar/version.rb
|
|
7
|
+
workflow_dispatch:
|
|
6
8
|
|
|
7
9
|
jobs:
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
10
|
+
publish:
|
|
11
|
+
runs-on: ubuntu-latest
|
|
12
|
+
environment: rubygems-publish
|
|
13
|
+
if: github.repository_owner == 'zendesk'
|
|
14
|
+
permissions:
|
|
15
|
+
id-token: write
|
|
16
|
+
contents: write
|
|
17
|
+
steps:
|
|
18
|
+
- uses: actions/checkout@v4
|
|
19
|
+
- name: Set up Ruby
|
|
20
|
+
uses: ruby/setup-ruby@v1
|
|
21
|
+
with:
|
|
22
|
+
bundler-cache: false
|
|
23
|
+
ruby-version: "3.4"
|
|
24
|
+
- name: Install dependencies
|
|
25
|
+
run: bundle install
|
|
26
|
+
- uses: rubygems/release-gem@v1
|
data/.rspec
CHANGED
data/CHANGELOG.md
CHANGED
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
|
-
|
|
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 (
|
|
4
|
+
racecar (3.0.0.alpha.1)
|
|
5
5
|
king_konf (~> 1.0.0)
|
|
6
|
-
rdkafka (
|
|
6
|
+
rdkafka (>= 0.15.0)
|
|
7
7
|
|
|
8
8
|
GEM
|
|
9
9
|
remote: https://rubygems.org/
|
|
10
10
|
specs:
|
|
11
|
-
activesupport (
|
|
12
|
-
|
|
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
|
-
|
|
16
|
-
|
|
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.
|
|
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)
|
|
32
|
+
drb (2.2.1)
|
|
22
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
39
|
mini_portile2 (2.8.5)
|
|
28
|
-
minitest (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.
|
|
48
|
+
rdkafka (0.21.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 (~>
|
|
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.
|
|
84
|
+
2.6.2
|
data/README.md
CHANGED
|
@@ -418,7 +418,7 @@ Racecar supports [Datadog](https://www.datadoghq.com/) monitoring integration. I
|
|
|
418
418
|
- `datadog_namespace` – The namespace to use for Datadog metrics.
|
|
419
419
|
- `datadog_tags` – Tags that should always be set on Datadog metrics.
|
|
420
420
|
|
|
421
|
-
Furthermore, there's a [standard Datadog dashboard configuration file](https://raw.githubusercontent.com/zendesk/racecar/
|
|
421
|
+
Furthermore, there's a [standard Datadog dashboard configuration file](https://raw.githubusercontent.com/zendesk/racecar/main/extra/datadog-dashboard.json) that you can import to get started with a Racecar dashboard for all of your consumers.
|
|
422
422
|
|
|
423
423
|
#### Consumers Without Rails
|
|
424
424
|
|
|
@@ -716,19 +716,35 @@ apt-get update && apt-get install -y libzstd-dev
|
|
|
716
716
|
|
|
717
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.
|
|
718
718
|
|
|
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
|
|
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`.
|
|
720
720
|
|
|
721
721
|
### Running RSpec within Docker
|
|
722
722
|
|
|
723
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:
|
|
724
724
|
|
|
725
725
|
- Uncomment the `tests` service from the docker-compose.yml
|
|
726
|
-
- Bring up the stack with `docker
|
|
727
|
-
- Execute the entire suite with `docker
|
|
728
|
-
- Execute a single spec or directory with `docker
|
|
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`
|
|
729
729
|
|
|
730
730
|
Please note - your code directory is mounted as a volume, so you can make code changes without needing to rebuild
|
|
731
731
|
|
|
732
|
+
### Releasing a new version
|
|
733
|
+
|
|
734
|
+
A new version is published to RubyGems.org every time a change to `version.rb` is pushed to the `main` branch.
|
|
735
|
+
In short, follow these steps:
|
|
736
|
+
1. Update `version.rb`,
|
|
737
|
+
2. run `bundle lock` to update `Gemfile.lock`,
|
|
738
|
+
3. merge this change into `main`, and
|
|
739
|
+
4. look at [the action](https://github.com/zendesk/racecar/actions/workflows/publish.yml) for output.
|
|
740
|
+
|
|
741
|
+
To create a pre-release from a non-main branch:
|
|
742
|
+
1. change the version in `version.rb` to something like `2.13.0.pre.1` or `3.0.0.beta.2`,
|
|
743
|
+
2. push this change to your branch,
|
|
744
|
+
3. go to [Actions → “Publish to RubyGems.org” on GitHub](https://github.com/zendesk/racecar/actions/workflows/publish.yml),
|
|
745
|
+
4. click the “Run workflow” button,
|
|
746
|
+
5. pick your branch from a dropdown.
|
|
747
|
+
|
|
732
748
|
## Contributing
|
|
733
749
|
|
|
734
750
|
Bug reports and pull requests are welcome on [GitHub](https://github.com/zendesk/racecar). Feel free to [join our Slack team](https://ruby-kafka-slack.herokuapp.com/) and ask how best to contribute!
|
data/Rakefile
CHANGED
|
@@ -1,11 +1,9 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
require "bundler/setup"
|
|
3
4
|
require "bundler/gem_tasks"
|
|
4
5
|
require "rspec/core/rake_task"
|
|
5
6
|
|
|
6
|
-
# Pushing to rubygems is handled by a github workflow
|
|
7
|
-
ENV["gem_push"] = "false"
|
|
8
|
-
|
|
9
7
|
RSpec::Core::RakeTask.new(:spec)
|
|
10
8
|
|
|
11
9
|
task :default => :spec
|
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:
|
|
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:
|
|
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
|
|
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
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'racecar/pause'
|
|
4
|
+
require 'concurrent-ruby'
|
|
5
|
+
|
|
6
|
+
module Racecar
|
|
7
|
+
class AsyncPartitionProcessor
|
|
8
|
+
attr_reader :thread
|
|
9
|
+
|
|
10
|
+
THREAD_KEY_IDENTIFIER = 'racecar_topic_partition_identifier'.freeze
|
|
11
|
+
|
|
12
|
+
def self.thread_key(topic, partition)
|
|
13
|
+
"#{topic}/#{partition}"
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def initialize(topic:, partition:, logger:, config:, consumer:, consumer_class:, instrumenter:, rdkafka_consumer:)
|
|
17
|
+
@topic = topic
|
|
18
|
+
@partition = partition
|
|
19
|
+
@logger = logger
|
|
20
|
+
@config = config
|
|
21
|
+
@consumer = consumer
|
|
22
|
+
@consumer_class = consumer_class
|
|
23
|
+
@instrumenter = instrumenter
|
|
24
|
+
@rdkafka_consumer = rdkafka_consumer
|
|
25
|
+
@backpressure_paused = Concurrent::AtomicBoolean.new
|
|
26
|
+
@tpl = build_tpl(topic, partition)
|
|
27
|
+
setup_async_processing
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def process(message)
|
|
31
|
+
push(message)
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def process_batch(messages)
|
|
35
|
+
push(messages)
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def rebalance!
|
|
39
|
+
processor.rebalance!
|
|
40
|
+
@queue << nil
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def shut_down!
|
|
44
|
+
processor.shut_down!
|
|
45
|
+
@queue << nil
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def rebalancing_or_shutting_down?
|
|
49
|
+
processor.rebalancing_or_shutting_down?
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
def resume_paused_partition
|
|
53
|
+
processor.resume_paused_partition
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
private
|
|
57
|
+
|
|
58
|
+
attr_reader :backpressure_paused, :instrumenter, :consumer_class, :consumer, :queue, :config, :processor, :logger
|
|
59
|
+
|
|
60
|
+
def setup_async_processing
|
|
61
|
+
@processor = PartitionProcessor.new(
|
|
62
|
+
config: config,
|
|
63
|
+
logger: logger,
|
|
64
|
+
instrumenter: instrumenter,
|
|
65
|
+
consumer_class_instance: consumer_class.new,
|
|
66
|
+
consumer: consumer,
|
|
67
|
+
topic: @topic,
|
|
68
|
+
partition: @partition,
|
|
69
|
+
pause: Pause.new_from_config(config),
|
|
70
|
+
rdkafka_consumer: @rdkafka_consumer,
|
|
71
|
+
)
|
|
72
|
+
@queue = Queue.new
|
|
73
|
+
@thread = nil
|
|
74
|
+
|
|
75
|
+
use_process_batch = consumer_class.method_defined?(:process_batch)
|
|
76
|
+
|
|
77
|
+
if use_process_batch
|
|
78
|
+
spawn_thread do |msgs|
|
|
79
|
+
processor.process_batch(msgs)
|
|
80
|
+
end
|
|
81
|
+
else
|
|
82
|
+
spawn_thread do |msgs|
|
|
83
|
+
msgs.each do |msg|
|
|
84
|
+
processor.process(msg)
|
|
85
|
+
end
|
|
86
|
+
end
|
|
87
|
+
end
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
def spawn_thread(&block)
|
|
91
|
+
@thread = Thread.new do
|
|
92
|
+
Thread.current.name = "Racecar thread for #{thread_key}"
|
|
93
|
+
Thread.current[AsyncPartitionProcessor::THREAD_KEY_IDENTIFIER] = thread_key
|
|
94
|
+
main_processing_loop(block)
|
|
95
|
+
end
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
def push(messages)
|
|
99
|
+
@queue << Array(messages)
|
|
100
|
+
maybe_apply_backpressure
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
def maybe_apply_backpressure
|
|
104
|
+
if @backpressure_paused.false? && @queue.size >= config.multithreaded_processing_max_queue_size
|
|
105
|
+
@backpressure_paused.make_true
|
|
106
|
+
@rdkafka_consumer.pause(@tpl)
|
|
107
|
+
logger.debug "Paused partition #{@topic}/#{@partition}: queue reached capacity (#{@queue.size}/#{config.multithreaded_processing_max_queue_size})"
|
|
108
|
+
end
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
def maybe_resume_the_partition
|
|
112
|
+
if @backpressure_paused.true? && @queue.size < config.multithreaded_processing_resume_threshold * config.multithreaded_processing_max_queue_size
|
|
113
|
+
@backpressure_paused.make_false
|
|
114
|
+
@rdkafka_consumer.resume(@tpl)
|
|
115
|
+
end
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
def build_tpl(topic, partition)
|
|
119
|
+
Rdkafka::Consumer::TopicPartitionList.new.tap do |tpl|
|
|
120
|
+
tpl.add_topic_and_partitions_with_offsets(topic, partition => -1001)
|
|
121
|
+
end
|
|
122
|
+
end
|
|
123
|
+
|
|
124
|
+
def thread_key
|
|
125
|
+
self.class.thread_key(@topic, @partition)
|
|
126
|
+
end
|
|
127
|
+
|
|
128
|
+
def main_processing_loop(block)
|
|
129
|
+
loop do
|
|
130
|
+
msgs = @queue.pop
|
|
131
|
+
break if msgs.nil?
|
|
132
|
+
|
|
133
|
+
maybe_resume_the_partition
|
|
134
|
+
block.call(msgs)
|
|
135
|
+
rescue => e
|
|
136
|
+
logger.error "Error in processing thread for #{thread_key}: #{e.class} - #{e.full_message}. backtrace: #{e.backtrace&.first(10)&.join("\n")}"
|
|
137
|
+
end
|
|
138
|
+
ensure
|
|
139
|
+
@processor.teardown
|
|
140
|
+
end
|
|
141
|
+
end
|
|
142
|
+
end
|
data/lib/racecar/cli.rb
CHANGED
data/lib/racecar/config.rb
CHANGED
|
@@ -194,6 +194,18 @@ module Racecar
|
|
|
194
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
195
|
string :multi_subscription_strategy, allowed_values: %w(round-robin exhaust-topic), default: "exhaust-topic"
|
|
196
196
|
|
|
197
|
+
desc "Whether multithreaded processing is enabled"
|
|
198
|
+
boolean :multithreaded_processing_enabled, default: false
|
|
199
|
+
|
|
200
|
+
desc "Max size of the queue of messages waiting to be processed when multithreaded processing is enabled"
|
|
201
|
+
integer :multithreaded_processing_max_queue_size, default: 1000
|
|
202
|
+
|
|
203
|
+
desc "Timeout in seconds for the main thread to wait for a processing thread to finish when shutting down the consumer with multithreaded processing enabled"
|
|
204
|
+
integer :multithreaded_processing_shutdown_timeout, default: 300
|
|
205
|
+
|
|
206
|
+
desc "Multi threaded queue resume threshold as a percentage of `multithreaded_processing_max_queue_size`. Defaults to 0.5, meaning that the consumer will attempt to resume a paused partition when the queue size drops below 50% of the max queue size."
|
|
207
|
+
float :multithreaded_processing_resume_threshold, default: 0.5
|
|
208
|
+
|
|
197
209
|
# The error handler must be set directly on the object.
|
|
198
210
|
attr_reader :error_handler
|
|
199
211
|
|
data/lib/racecar/consumer.rb
CHANGED
|
@@ -68,16 +68,16 @@ module Racecar
|
|
|
68
68
|
@instrumenter.instrument('deliver_messages', instrumentation_payload) do
|
|
69
69
|
@delivery_handles.each do |handle|
|
|
70
70
|
begin
|
|
71
|
-
# rdkafka-ruby checks
|
|
72
|
-
# successfully delivered, up to max_wait_timeout seconds
|
|
73
|
-
# Rdkafka::AbstractHandle::WaitTimeoutError. librdkafka will
|
|
74
|
-
# 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"
|
|
75
75
|
# (message.timeout.ms) is exceeded. Phrased differently, rdkafka-ruby's
|
|
76
76
|
# WaitTimeoutError is just informative.
|
|
77
77
|
# The raising can be avoided if max_wait_timeout below is greater than
|
|
78
78
|
# config.message_timeout, but config is not available here (without
|
|
79
79
|
# changing the interface).
|
|
80
|
-
handle.wait(max_wait_timeout: 60
|
|
80
|
+
handle.wait(max_wait_timeout: 60)
|
|
81
81
|
rescue Rdkafka::AbstractHandle::WaitTimeoutError => e
|
|
82
82
|
partition = MessageDeliveryError.partition_from_delivery_handle(handle)
|
|
83
83
|
# ideally we could use the logger passed to the Runner, but it is not
|
data/lib/racecar/consumer_set.rb
CHANGED
|
@@ -1,21 +1,24 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
require "racecar/delivery_callback"
|
|
4
|
+
|
|
3
5
|
module Racecar
|
|
4
6
|
class ConsumerSet
|
|
5
7
|
MAX_POLL_TRIES = 10
|
|
6
8
|
|
|
7
|
-
def initialize(config, logger, instrumenter = NullInstrumenter)
|
|
9
|
+
def initialize(config, logger, partition_processors, instrumenter = NullInstrumenter)
|
|
8
10
|
@config, @logger = config, logger
|
|
9
11
|
@instrumenter = instrumenter
|
|
12
|
+
@partition_processors = partition_processors
|
|
10
13
|
raise ArgumentError, "Subscriptions must not be empty when subscribing" if @config.subscriptions.empty?
|
|
11
14
|
|
|
12
15
|
@consumers = []
|
|
13
16
|
@consumer_id_iterator = (0...@config.subscriptions.size).cycle
|
|
17
|
+
@producer_mutex = Mutex.new
|
|
14
18
|
|
|
15
19
|
@previous_retries = 0
|
|
16
20
|
|
|
17
21
|
@last_poll_read_nil_message = false
|
|
18
|
-
@paused_tpls = Hash.new { |h, k| h[k] = {} }
|
|
19
22
|
end
|
|
20
23
|
|
|
21
24
|
def poll(max_wait_time_ms = @config.max_wait_time_ms)
|
|
@@ -48,8 +51,9 @@ module Racecar
|
|
|
48
51
|
messages
|
|
49
52
|
end
|
|
50
53
|
|
|
51
|
-
def store_offset(message)
|
|
52
|
-
current
|
|
54
|
+
def store_offset(message, raw_consumer = nil)
|
|
55
|
+
consumer = raw_consumer || current
|
|
56
|
+
consumer.store_offset(message)
|
|
53
57
|
rescue Rdkafka::RdkafkaError => e
|
|
54
58
|
if e.code == :state # -172
|
|
55
59
|
@logger.warn "Attempted to store_offset, but we're not subscribed to it: #{ErroneousStateError.new(e)}"
|
|
@@ -66,13 +70,28 @@ module Racecar
|
|
|
66
70
|
|
|
67
71
|
def close
|
|
68
72
|
each_subscribed(&:close)
|
|
69
|
-
|
|
73
|
+
reset_producer!
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
def producer
|
|
77
|
+
@producer_mutex.synchronize do
|
|
78
|
+
@producer ||= Rdkafka::Config.new(producer_config).producer.tap do |p|
|
|
79
|
+
p.delivery_callback = Racecar::DeliveryCallback.new(instrumenter: @instrumenter)
|
|
80
|
+
end
|
|
81
|
+
end
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
def reset_producer!
|
|
85
|
+
@producer_mutex.synchronize do
|
|
86
|
+
@producer&.close
|
|
87
|
+
@producer = nil
|
|
88
|
+
end
|
|
70
89
|
end
|
|
71
90
|
|
|
72
91
|
def current
|
|
73
92
|
@consumers[@consumer_id_iterator.peek] ||= begin
|
|
74
93
|
consumer_config = Rdkafka::Config.new(rdkafka_config(current_subscription))
|
|
75
|
-
listener = RebalanceListener.new(@config
|
|
94
|
+
listener = RebalanceListener.new(@config, @instrumenter, @partition_processors)
|
|
76
95
|
consumer_config.consumer_rebalance_listener = listener
|
|
77
96
|
consumer = consumer_config.consumer
|
|
78
97
|
listener.rdkafka_consumer = consumer
|
|
@@ -86,44 +105,38 @@ module Racecar
|
|
|
86
105
|
|
|
87
106
|
def each_subscribed
|
|
88
107
|
if block_given?
|
|
89
|
-
@consumers.each { |c| yield c }
|
|
108
|
+
@consumers.compact.each { |c| yield c }
|
|
90
109
|
else
|
|
91
|
-
@consumers.each
|
|
110
|
+
@consumers.compact.each
|
|
92
111
|
end
|
|
93
112
|
end
|
|
94
113
|
|
|
95
|
-
def pause(topic, partition, offset)
|
|
114
|
+
def pause(topic, partition, offset = nil)
|
|
96
115
|
consumer, filtered_tpl = find_consumer_by(topic, partition)
|
|
97
|
-
|
|
116
|
+
unless consumer
|
|
98
117
|
@logger.info "Attempted to pause #{topic}/#{partition}, but we're not subscribed to it"
|
|
99
118
|
return
|
|
100
119
|
end
|
|
101
120
|
|
|
102
121
|
consumer.pause(filtered_tpl)
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
122
|
+
if offset
|
|
123
|
+
fake_msg = OpenStruct.new(topic: topic, partition: partition, offset: offset)
|
|
124
|
+
consumer.seek(fake_msg)
|
|
125
|
+
end
|
|
107
126
|
end
|
|
108
127
|
|
|
109
128
|
def resume(topic, partition)
|
|
110
129
|
consumer, filtered_tpl = find_consumer_by(topic, partition)
|
|
111
130
|
|
|
112
|
-
|
|
113
|
-
consumer, filtered_tpl = @paused_tpls[topic][partition]
|
|
114
|
-
end
|
|
115
|
-
|
|
116
|
-
if !consumer
|
|
131
|
+
unless consumer
|
|
117
132
|
@logger.info "Attempted to resume #{topic}/#{partition}, but we're not subscribed to it"
|
|
118
133
|
return
|
|
119
134
|
end
|
|
120
135
|
|
|
121
136
|
consumer.resume(filtered_tpl)
|
|
122
|
-
@paused_tpls[topic].delete(partition)
|
|
123
|
-
@paused_tpls.delete(topic) if @paused_tpls[topic].empty?
|
|
124
137
|
end
|
|
125
138
|
|
|
126
|
-
|
|
139
|
+
alias :each :each_subscribed
|
|
127
140
|
|
|
128
141
|
# Subscribe to all topics eagerly, even if there's still messages elsewhere. Usually
|
|
129
142
|
# that's not needed and Kafka might rebalance if topics are not polled frequently
|
|
@@ -269,5 +282,18 @@ module Racecar
|
|
|
269
282
|
r = limit_ms - ((Time.now - started_at_time)*1000).round
|
|
270
283
|
r <= 0 ? 0 : r
|
|
271
284
|
end
|
|
285
|
+
|
|
286
|
+
def producer_config
|
|
287
|
+
cfg = {
|
|
288
|
+
"bootstrap.servers" => @config.brokers.join(","),
|
|
289
|
+
"client.id" => @config.client_id,
|
|
290
|
+
"statistics.interval.ms" => @config.statistics_interval_ms,
|
|
291
|
+
"message.timeout.ms" => @config.message_timeout * 1000,
|
|
292
|
+
"partitioner" => @config.partitioner.to_s,
|
|
293
|
+
}
|
|
294
|
+
cfg["compression.codec"] = @config.producer_compression_codec.to_s unless @config.producer_compression_codec.nil?
|
|
295
|
+
cfg.merge!(@config.rdkafka_producer)
|
|
296
|
+
cfg
|
|
297
|
+
end
|
|
272
298
|
end
|
|
273
299
|
end
|