racecar 2.12.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 +2 -2
- data/.github/workflows/publish.yml +21 -7
- data/Gemfile.lock +2 -2
- data/README.md +17 -1
- data/Rakefile +1 -3
- 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_set.rb +48 -22
- data/lib/racecar/partition_processor.rb +217 -0
- data/lib/racecar/pause.rb +16 -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
- metadata +6 -7
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
|
@@ -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/Gemfile.lock
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
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
6
|
rdkafka (>= 0.15.0)
|
|
7
7
|
|
|
@@ -45,7 +45,7 @@ GEM
|
|
|
45
45
|
byebug (~> 11.0)
|
|
46
46
|
pry (>= 0.13, < 0.15)
|
|
47
47
|
rake (13.0.6)
|
|
48
|
-
rdkafka (0.
|
|
48
|
+
rdkafka (0.21.0)
|
|
49
49
|
ffi (~> 1.15)
|
|
50
50
|
mini_portile2 (~> 2.6)
|
|
51
51
|
rake (> 12)
|
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
|
|
|
@@ -729,6 +729,22 @@ There can be behavioural inconsistencies between running the specs on your machi
|
|
|
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
|
|
@@ -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_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
|
|
@@ -0,0 +1,217 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "rdkafka"
|
|
4
|
+
require "racecar/pause"
|
|
5
|
+
require "racecar/delivery_callback"
|
|
6
|
+
|
|
7
|
+
module Racecar
|
|
8
|
+
class PartitionProcessor
|
|
9
|
+
attr_reader :consumer_class_instance, :config, :logger, :instrumenter, :consumer, :topic, :partition, :pause
|
|
10
|
+
attr_accessor :rebalancing, :shutting_down
|
|
11
|
+
|
|
12
|
+
def initialize(config:, logger:, instrumenter:, consumer_class_instance:, consumer:, topic:, partition:, pause:, rdkafka_consumer: nil)
|
|
13
|
+
@config = config
|
|
14
|
+
@logger = logger
|
|
15
|
+
@instrumenter = instrumenter
|
|
16
|
+
@consumer_class_instance = consumer_class_instance
|
|
17
|
+
@pause = pause
|
|
18
|
+
@topic = topic
|
|
19
|
+
@partition = partition
|
|
20
|
+
@consumer = consumer
|
|
21
|
+
@rdkafka_consumer = rdkafka_consumer
|
|
22
|
+
|
|
23
|
+
if config.multithreaded_processing_enabled
|
|
24
|
+
consumer_class_instance.configure(
|
|
25
|
+
producer: consumer.producer,
|
|
26
|
+
consumer: @consumer,
|
|
27
|
+
instrumenter: @instrumenter,
|
|
28
|
+
config: @config,
|
|
29
|
+
)
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
@sleep_mutex = Mutex.new
|
|
33
|
+
@sleep_cv = ConditionVariable.new
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def process(message)
|
|
37
|
+
payload = {
|
|
38
|
+
consumer_class: consumer_class_instance.class.to_s,
|
|
39
|
+
topic: message.topic,
|
|
40
|
+
partition: message.partition,
|
|
41
|
+
offset: message.offset,
|
|
42
|
+
create_time: message.timestamp,
|
|
43
|
+
key: message.key,
|
|
44
|
+
value: message.payload,
|
|
45
|
+
headers: message.headers,
|
|
46
|
+
}
|
|
47
|
+
@instrumenter.instrument("start_process_message", payload)
|
|
48
|
+
|
|
49
|
+
with_error_handling(message, payload) do |pause|
|
|
50
|
+
@instrumenter.instrument("process_message", payload) do
|
|
51
|
+
if @config.multithreaded_processing_enabled && consumer_class_instance.instance_variable_get(:@producer)&.closed?
|
|
52
|
+
reconfigure_consumer_class_instance!
|
|
53
|
+
end
|
|
54
|
+
consumer_class_instance.process(Racecar::Message.new(message, retries_count: pause.pauses_count))
|
|
55
|
+
consumer_class_instance.deliver!
|
|
56
|
+
consumer.store_offset(message, @rdkafka_consumer) unless rebalancing
|
|
57
|
+
end
|
|
58
|
+
end
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
def process_batch(messages)
|
|
62
|
+
first, last = messages.first, messages.last
|
|
63
|
+
payload = {
|
|
64
|
+
consumer_class: consumer_class_instance.class.to_s,
|
|
65
|
+
topic: first.topic,
|
|
66
|
+
partition: first.partition,
|
|
67
|
+
first_offset: first.offset,
|
|
68
|
+
last_offset: last.offset,
|
|
69
|
+
last_create_time: last.timestamp,
|
|
70
|
+
message_count: messages.size,
|
|
71
|
+
}
|
|
72
|
+
@instrumenter.instrument("start_process_batch", payload)
|
|
73
|
+
|
|
74
|
+
with_error_handling(messages, payload) do |pause|
|
|
75
|
+
@instrumenter.instrument("process_batch", payload) do
|
|
76
|
+
racecar_messages = messages.map do |message|
|
|
77
|
+
Racecar::Message.new(message, retries_count: pause.pauses_count)
|
|
78
|
+
end
|
|
79
|
+
if @config.multithreaded_processing_enabled && consumer_class_instance.instance_variable_get(:@producer)&.closed?
|
|
80
|
+
reconfigure_consumer_class_instance!
|
|
81
|
+
end
|
|
82
|
+
consumer_class_instance.process_batch(racecar_messages)
|
|
83
|
+
consumer_class_instance.deliver!
|
|
84
|
+
consumer.store_offset(messages.last, @rdkafka_consumer) unless rebalancing
|
|
85
|
+
end
|
|
86
|
+
end
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
def teardown
|
|
90
|
+
consumer_class_instance.deliver! unless rebalancing
|
|
91
|
+
ensure
|
|
92
|
+
consumer_class_instance.teardown
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
def resume_paused_partition
|
|
96
|
+
return if config.pause_timeout == 0 || !pause.paused?
|
|
97
|
+
|
|
98
|
+
@instrumenter.instrument("pause_status", {
|
|
99
|
+
topic: topic,
|
|
100
|
+
partition: partition,
|
|
101
|
+
duration: pause.pause_duration,
|
|
102
|
+
consumer_class: consumer_class_instance.class.to_s,
|
|
103
|
+
})
|
|
104
|
+
|
|
105
|
+
if pause.paused? && pause.expired?
|
|
106
|
+
logger.info "Automatically resuming partition #{topic}/#{partition}, pause timeout expired"
|
|
107
|
+
consumer.resume(topic, partition)
|
|
108
|
+
pause.resume!
|
|
109
|
+
end
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
def rebalance!
|
|
113
|
+
@rebalancing = true
|
|
114
|
+
@sleep_mutex.synchronize { @sleep_cv.signal }
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
def shut_down!
|
|
118
|
+
@shutting_down = true
|
|
119
|
+
@sleep_mutex.synchronize { @sleep_cv.signal }
|
|
120
|
+
resume_paused_partition
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
def rebalancing_or_shutting_down?
|
|
124
|
+
rebalancing || shutting_down
|
|
125
|
+
end
|
|
126
|
+
|
|
127
|
+
private
|
|
128
|
+
|
|
129
|
+
def with_error_handling(messages, payload)
|
|
130
|
+
if config.multithreaded_processing_enabled
|
|
131
|
+
with_multi_threaded_error_handling(messages, payload) { |pause| yield(pause) }
|
|
132
|
+
else
|
|
133
|
+
with_single_threaded_error_handling(messages, payload) { |pause| yield(pause) }
|
|
134
|
+
end
|
|
135
|
+
end
|
|
136
|
+
|
|
137
|
+
def with_multi_threaded_error_handling(messages, payload)
|
|
138
|
+
loop do
|
|
139
|
+
begin
|
|
140
|
+
yield(pause)
|
|
141
|
+
pause.reset!
|
|
142
|
+
break
|
|
143
|
+
rescue => e
|
|
144
|
+
if rebalancing
|
|
145
|
+
Thread.exit
|
|
146
|
+
elsif !shutting_down
|
|
147
|
+
handle_processing_error(e, payload, pause: pause)
|
|
148
|
+
pause.pause!
|
|
149
|
+
unless config.pause_timeout <= 0
|
|
150
|
+
@sleep_mutex.synchronize do
|
|
151
|
+
next if rebalancing || shutting_down
|
|
152
|
+
@sleep_cv.wait(@sleep_mutex, pause.backoff_interval)
|
|
153
|
+
end
|
|
154
|
+
end
|
|
155
|
+
Thread.exit if rebalancing
|
|
156
|
+
break if shutting_down || config.pause_timeout <= 0
|
|
157
|
+
else
|
|
158
|
+
handle_processing_error(e, payload, pause: pause)
|
|
159
|
+
break
|
|
160
|
+
end
|
|
161
|
+
end
|
|
162
|
+
end
|
|
163
|
+
end
|
|
164
|
+
|
|
165
|
+
def with_single_threaded_error_handling(messages, payload)
|
|
166
|
+
offsets = messages.is_a?(Array) ? messages.first.offset..messages.last.offset : messages.offset..messages.offset
|
|
167
|
+
with_pause(offsets) do
|
|
168
|
+
yield(pause)
|
|
169
|
+
rescue => e
|
|
170
|
+
handle_processing_error(e, payload, pause: pause)
|
|
171
|
+
raise e
|
|
172
|
+
end
|
|
173
|
+
end
|
|
174
|
+
|
|
175
|
+
def with_pause(offsets)
|
|
176
|
+
return yield if config.pause_timeout == 0
|
|
177
|
+
|
|
178
|
+
begin
|
|
179
|
+
yield
|
|
180
|
+
pause.reset!
|
|
181
|
+
rescue => e
|
|
182
|
+
desc = "#{topic}/#{partition}"
|
|
183
|
+
logger.error "Failed to process #{desc} at #{offsets}: #{e}"
|
|
184
|
+
logger.warn "Pausing partition #{desc} for #{pause.backoff_interval} seconds"
|
|
185
|
+
consumer.pause(topic, partition, offsets.first)
|
|
186
|
+
pause.pause!
|
|
187
|
+
end
|
|
188
|
+
end
|
|
189
|
+
|
|
190
|
+
def handle_processing_error(error, payload, pause:)
|
|
191
|
+
if error.is_a?(Racecar::MessageDeliveryError) && error.code == :msg_timed_out
|
|
192
|
+
logger.error error.to_s
|
|
193
|
+
logger.error "Racecar will reset the producer to force a new broker connection."
|
|
194
|
+
reset_producer!
|
|
195
|
+
payload[:unrecoverable_delivery_error] = true
|
|
196
|
+
else
|
|
197
|
+
payload[:unrecoverable_delivery_error] = false
|
|
198
|
+
end
|
|
199
|
+
payload[:retries_count] = pause.pauses_count
|
|
200
|
+
config.error_handler.call(error, payload)
|
|
201
|
+
end
|
|
202
|
+
|
|
203
|
+
def reset_producer!
|
|
204
|
+
consumer.reset_producer!
|
|
205
|
+
reconfigure_consumer_class_instance!
|
|
206
|
+
end
|
|
207
|
+
|
|
208
|
+
def reconfigure_consumer_class_instance!
|
|
209
|
+
consumer_class_instance.configure(
|
|
210
|
+
producer: consumer.producer,
|
|
211
|
+
consumer: consumer,
|
|
212
|
+
instrumenter: @instrumenter,
|
|
213
|
+
config: @config,
|
|
214
|
+
)
|
|
215
|
+
end
|
|
216
|
+
end
|
|
217
|
+
end
|
data/lib/racecar/pause.rb
CHANGED
|
@@ -4,6 +4,22 @@ module Racecar
|
|
|
4
4
|
class Pause
|
|
5
5
|
attr_reader :pauses_count
|
|
6
6
|
|
|
7
|
+
def self.new_from_config(config)
|
|
8
|
+
timeout = if config.pause_timeout == -1 || config.pause_timeout == 0
|
|
9
|
+
nil
|
|
10
|
+
elsif config.pause_timeout > 0
|
|
11
|
+
config.pause_timeout
|
|
12
|
+
else
|
|
13
|
+
raise ArgumentError, "Invalid value for pause_timeout: must be integer greater or equal -1"
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
new(
|
|
17
|
+
timeout: timeout,
|
|
18
|
+
max_timeout: config.max_pause_timeout,
|
|
19
|
+
exponential_backoff: config.pause_with_exponential_backoff
|
|
20
|
+
)
|
|
21
|
+
end
|
|
22
|
+
|
|
7
23
|
def initialize(timeout: nil, max_timeout: nil, exponential_backoff: false)
|
|
8
24
|
@started_at = nil
|
|
9
25
|
@pauses_count = 0
|
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
module Racecar
|
|
2
2
|
class RebalanceListener
|
|
3
|
-
def initialize(
|
|
4
|
-
@consumer_class = consumer_class
|
|
3
|
+
def initialize(config, instrumenter, partition_processors)
|
|
4
|
+
@consumer_class = config.consumer_class
|
|
5
|
+
@config = config
|
|
5
6
|
@instrumenter = instrumenter
|
|
7
|
+
@partition_processors = partition_processors
|
|
6
8
|
@rdkafka_consumer = nil
|
|
7
9
|
end
|
|
8
10
|
|
|
@@ -24,6 +26,14 @@ module Racecar
|
|
|
24
26
|
|
|
25
27
|
instrument("partitions_revoked", partitions: event.partition_numbers) do
|
|
26
28
|
consumer_class.on_partitions_revoked(event)
|
|
29
|
+
rdkafka_topic_partition_list.to_h.each do |topic, partitions_metadata|
|
|
30
|
+
partitions_metadata.flatten.map(&:partition).each do |partition|
|
|
31
|
+
key = Runner.topic_partition_key(topic, partition)
|
|
32
|
+
processor = @partition_processors[key]
|
|
33
|
+
processor&.rebalance!
|
|
34
|
+
@partition_processors.delete(key)
|
|
35
|
+
end
|
|
36
|
+
end
|
|
27
37
|
end
|
|
28
38
|
end
|
|
29
39
|
|
data/lib/racecar/runner.rb
CHANGED
|
@@ -6,156 +6,120 @@ require "racecar/message"
|
|
|
6
6
|
require "racecar/message_delivery_error"
|
|
7
7
|
require "racecar/erroneous_state_error"
|
|
8
8
|
require "racecar/delivery_callback"
|
|
9
|
+
require "racecar/partition_processor"
|
|
10
|
+
require "racecar/async_partition_processor"
|
|
9
11
|
|
|
10
12
|
module Racecar
|
|
11
13
|
class Runner
|
|
12
|
-
attr_reader :
|
|
14
|
+
attr_reader :consumer_class, :config, :logger, :partition_processors
|
|
13
15
|
|
|
14
|
-
def initialize(
|
|
15
|
-
@
|
|
16
|
+
def initialize(consumer_class, config:, logger:, instrumenter: NullInstrumenter)
|
|
17
|
+
@consumer_class, @config, @logger = consumer_class, config, logger
|
|
16
18
|
@instrumenter = instrumenter
|
|
17
19
|
@stop_requested = false
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
if
|
|
21
|
-
Rdkafka::Config.statistics_callback =
|
|
20
|
+
@partition_processors = Concurrent::Hash.new
|
|
21
|
+
@consumer_class_instance = consumer_class.new
|
|
22
|
+
if @consumer_class_instance.respond_to?(:statistics_callback) && Rdkafka::Config.statistics_callback.nil?
|
|
23
|
+
Rdkafka::Config.statistics_callback = @consumer_class_instance.method(:statistics_callback).to_proc
|
|
22
24
|
end
|
|
23
|
-
|
|
24
|
-
setup_pauses
|
|
25
|
+
Rdkafka::Config.logger = logger
|
|
25
26
|
end
|
|
26
27
|
|
|
27
|
-
def
|
|
28
|
-
|
|
29
|
-
nil
|
|
30
|
-
elsif config.pause_timeout == 0
|
|
31
|
-
# no op, handled elsewhere
|
|
32
|
-
elsif config.pause_timeout > 0
|
|
33
|
-
config.pause_timeout
|
|
34
|
-
else
|
|
35
|
-
raise ArgumentError, "Invalid value for pause_timeout: must be integer greater or equal -1"
|
|
36
|
-
end
|
|
37
|
-
|
|
38
|
-
@pauses = Hash.new {|h, k|
|
|
39
|
-
h[k] = Hash.new {|h2, k2|
|
|
40
|
-
h2[k2] = ::Racecar::Pause.new(
|
|
41
|
-
timeout: timeout,
|
|
42
|
-
max_timeout: config.max_pause_timeout,
|
|
43
|
-
exponential_backoff: config.pause_with_exponential_backoff
|
|
44
|
-
)
|
|
45
|
-
}
|
|
46
|
-
}
|
|
28
|
+
def self.topic_partition_key(topic, partition)
|
|
29
|
+
"#{topic}/#{partition}"
|
|
47
30
|
end
|
|
48
31
|
|
|
49
32
|
def run
|
|
50
33
|
install_signal_handlers
|
|
51
34
|
@stop_requested = false
|
|
52
35
|
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
36
|
+
unless config.multithreaded_processing_enabled
|
|
37
|
+
@consumer_class_instance.configure(
|
|
38
|
+
producer: consumer.producer,
|
|
39
|
+
consumer: consumer,
|
|
40
|
+
instrumenter: @instrumenter,
|
|
41
|
+
config: config,
|
|
42
|
+
)
|
|
43
|
+
end
|
|
61
44
|
|
|
62
|
-
|
|
63
|
-
consumer_class:
|
|
45
|
+
loop_payload = {
|
|
46
|
+
consumer_class: consumer_class.to_s,
|
|
64
47
|
consumer_set: consumer
|
|
65
48
|
}
|
|
66
|
-
|
|
67
49
|
# Main loop
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
50
|
+
begin
|
|
51
|
+
loop do
|
|
52
|
+
break if @stop_requested
|
|
53
|
+
|
|
54
|
+
@instrumenter.instrument("start_main_loop", loop_payload)
|
|
55
|
+
@instrumenter.instrument("main_loop", loop_payload) do
|
|
56
|
+
resume_all_paused_partitions unless config.multithreaded_processing_enabled
|
|
57
|
+
|
|
58
|
+
case process_method
|
|
59
|
+
when :batch then
|
|
60
|
+
msg_per_part = consumer.batch_poll(config.max_wait_time_ms).group_by(&:partition)
|
|
61
|
+
msg_per_part.each_value do |messages_per_partition|
|
|
62
|
+
processor = assign_and_get_processor(messages_per_partition)
|
|
63
|
+
processor&.process_batch(messages_per_partition) unless processor&.rebalancing_or_shutting_down?
|
|
64
|
+
end
|
|
65
|
+
when :single then
|
|
66
|
+
message = consumer.poll(config.max_wait_time_ms)
|
|
67
|
+
if message
|
|
68
|
+
processor = assign_and_get_processor(message)
|
|
69
|
+
processor&.process(message) unless processor&.rebalancing_or_shutting_down?
|
|
70
|
+
end
|
|
79
71
|
end
|
|
80
|
-
when :single then
|
|
81
|
-
message = consumer.poll(config.max_wait_time_ms)
|
|
82
|
-
process(message) if message
|
|
83
72
|
end
|
|
84
73
|
end
|
|
74
|
+
ensure
|
|
75
|
+
logger.info "Gracefully shutting down"
|
|
76
|
+
shutdown_processors_and_wait
|
|
77
|
+
consumer.commit
|
|
85
78
|
end
|
|
86
|
-
|
|
87
|
-
logger.info "Gracefully shutting down"
|
|
79
|
+
ensure
|
|
88
80
|
begin
|
|
89
|
-
processor.deliver!
|
|
90
|
-
processor.teardown
|
|
91
|
-
consumer.commit
|
|
92
|
-
ensure
|
|
93
81
|
@instrumenter.instrument('leave_group') do
|
|
94
82
|
consumer.close
|
|
95
83
|
end
|
|
84
|
+
ensure
|
|
85
|
+
Racecar::Datadog.close if config.datadog_enabled
|
|
86
|
+
@instrumenter.instrument("shut_down", loop_payload || {})
|
|
96
87
|
end
|
|
97
|
-
ensure
|
|
98
|
-
producer.close
|
|
99
|
-
Racecar::Datadog.close if config.datadog_enabled
|
|
100
|
-
@instrumenter.instrument("shut_down", instrumentation_payload || {})
|
|
101
88
|
end
|
|
102
89
|
|
|
103
90
|
def stop
|
|
104
91
|
@stop_requested = true
|
|
105
92
|
end
|
|
106
93
|
|
|
107
|
-
|
|
94
|
+
def consumer
|
|
95
|
+
@consumer ||= begin
|
|
96
|
+
ConsumerSet.new(config, logger, @partition_processors, @instrumenter)
|
|
97
|
+
end
|
|
98
|
+
end
|
|
108
99
|
|
|
109
|
-
|
|
100
|
+
private
|
|
110
101
|
|
|
111
102
|
def process_method
|
|
112
103
|
@process_method ||= begin
|
|
113
104
|
case
|
|
114
|
-
when
|
|
115
|
-
if
|
|
105
|
+
when consumer_class.method_defined?(:process_batch)
|
|
106
|
+
if consumer_class.instance_method(:process_batch).arity != 1
|
|
116
107
|
raise Racecar::Error, "Invalid method signature for `process_batch`. The method must take exactly 1 argument."
|
|
117
108
|
end
|
|
118
109
|
|
|
119
110
|
:batch
|
|
120
|
-
when
|
|
121
|
-
if
|
|
111
|
+
when consumer_class.method_defined?(:process)
|
|
112
|
+
if consumer_class.instance_method(:process).arity != 1
|
|
122
113
|
raise Racecar::Error, "Invalid method signature for `process`. The method must take exactly 1 argument."
|
|
123
114
|
end
|
|
124
115
|
|
|
125
116
|
:single
|
|
126
117
|
else
|
|
127
|
-
raise NotImplementedError, "Consumer class `#{
|
|
118
|
+
raise NotImplementedError, "Consumer class `#{consumer_class}` must implement a `process` or `process_batch` method"
|
|
128
119
|
end
|
|
129
120
|
end
|
|
130
121
|
end
|
|
131
122
|
|
|
132
|
-
def consumer
|
|
133
|
-
@consumer ||= begin
|
|
134
|
-
ConsumerSet.new(config, logger, @instrumenter)
|
|
135
|
-
end
|
|
136
|
-
end
|
|
137
|
-
|
|
138
|
-
def producer
|
|
139
|
-
@producer ||= Rdkafka::Config.new(producer_config).producer.tap do |producer|
|
|
140
|
-
producer.delivery_callback = Racecar::DeliveryCallback.new(instrumenter: @instrumenter)
|
|
141
|
-
end
|
|
142
|
-
end
|
|
143
|
-
|
|
144
|
-
def producer_config
|
|
145
|
-
# https://github.com/edenhill/librdkafka/blob/master/CONFIGURATION.md
|
|
146
|
-
producer_config = {
|
|
147
|
-
"bootstrap.servers" => config.brokers.join(","),
|
|
148
|
-
"client.id" => config.client_id,
|
|
149
|
-
"statistics.interval.ms" => config.statistics_interval_ms,
|
|
150
|
-
"message.timeout.ms" => config.message_timeout * 1000,
|
|
151
|
-
"partitioner" => config.partitioner.to_s,
|
|
152
|
-
}
|
|
153
|
-
|
|
154
|
-
producer_config["compression.codec"] = config.producer_compression_codec.to_s unless config.producer_compression_codec.nil?
|
|
155
|
-
producer_config.merge!(config.rdkafka_producer)
|
|
156
|
-
producer_config
|
|
157
|
-
end
|
|
158
|
-
|
|
159
123
|
def install_signal_handlers
|
|
160
124
|
# Stop the consumer on SIGINT, SIGQUIT or SIGTERM.
|
|
161
125
|
trap("QUIT") { stop }
|
|
@@ -166,128 +130,65 @@ module Racecar
|
|
|
166
130
|
trap("USR1") { $stderr.puts config.inspect }
|
|
167
131
|
end
|
|
168
132
|
|
|
169
|
-
def
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
instrumentation_payload[:retries_count] = pause.pauses_count
|
|
192
|
-
config.error_handler.call(e, instrumentation_payload)
|
|
193
|
-
raise e
|
|
194
|
-
end
|
|
133
|
+
def assign_and_get_processor(messages)
|
|
134
|
+
topic = messages.is_a?(Array) ? messages.first.topic : messages.topic
|
|
135
|
+
partition = messages.is_a?(Array) ? messages.first.partition : messages.partition
|
|
136
|
+
key = Runner.topic_partition_key(topic, partition)
|
|
137
|
+
return partition_processors[key] if partition_processors[key]
|
|
138
|
+
|
|
139
|
+
processor = if config.multithreaded_processing_enabled
|
|
140
|
+
AsyncPartitionProcessor.new(
|
|
141
|
+
**common_processor_params,
|
|
142
|
+
consumer_class: consumer_class,
|
|
143
|
+
topic: topic,
|
|
144
|
+
partition: partition,
|
|
145
|
+
rdkafka_consumer: consumer.current,
|
|
146
|
+
)
|
|
147
|
+
else
|
|
148
|
+
PartitionProcessor.new(
|
|
149
|
+
**common_processor_params,
|
|
150
|
+
consumer_class_instance: @consumer_class_instance,
|
|
151
|
+
topic: topic,
|
|
152
|
+
partition: partition,
|
|
153
|
+
pause: Pause.new_from_config(config),
|
|
154
|
+
)
|
|
195
155
|
end
|
|
156
|
+
partition_processors[key] = processor
|
|
196
157
|
end
|
|
197
158
|
|
|
198
|
-
def
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
}
|
|
209
|
-
|
|
210
|
-
@instrumenter.instrument("start_process_batch", instrumentation_payload)
|
|
211
|
-
with_pause(first.topic, first.partition, first.offset..last.offset) do |pause|
|
|
212
|
-
begin
|
|
213
|
-
@instrumenter.instrument("process_batch", instrumentation_payload) do
|
|
214
|
-
racecar_messages = messages.map do |message|
|
|
215
|
-
Racecar::Message.new(message, retries_count: pause.pauses_count)
|
|
159
|
+
def shutdown_processors_and_wait
|
|
160
|
+
if config.multithreaded_processing_enabled
|
|
161
|
+
processors_snapshot = partition_processors.values
|
|
162
|
+
processors_snapshot.each { |processor| processor.shut_down! if processor }
|
|
163
|
+
processors_snapshot.each do |processor|
|
|
164
|
+
if processor.respond_to?(:thread)
|
|
165
|
+
begin
|
|
166
|
+
processor.thread.join(config.multithreaded_processing_shutdown_timeout)
|
|
167
|
+
rescue => e
|
|
168
|
+
logger.error "Error while waiting for processor thread to finish: #{e}"
|
|
216
169
|
end
|
|
217
|
-
processor.process_batch(racecar_messages)
|
|
218
|
-
processor.deliver!
|
|
219
|
-
consumer.store_offset(messages.last)
|
|
220
170
|
end
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
171
|
+
end
|
|
172
|
+
else
|
|
173
|
+
begin
|
|
174
|
+
@consumer_class_instance.deliver!
|
|
175
|
+
ensure
|
|
176
|
+
@consumer_class_instance.teardown
|
|
226
177
|
end
|
|
227
178
|
end
|
|
228
179
|
end
|
|
229
180
|
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
# as a last ditch effort.
|
|
233
|
-
# The function returns true if there were unrecoverable errors, or false otherwise.
|
|
234
|
-
def reset_producer_on_unrecoverable_delivery_errors(error)
|
|
235
|
-
return false unless error.is_a?(Racecar::MessageDeliveryError)
|
|
236
|
-
return false unless error.code == :msg_timed_out # -192
|
|
237
|
-
|
|
238
|
-
logger.error error.to_s
|
|
239
|
-
logger.error "Racecar will reset the producer to force a new broker connection."
|
|
240
|
-
@producer.close
|
|
241
|
-
@producer = nil
|
|
242
|
-
processor.configure(
|
|
243
|
-
producer: producer,
|
|
244
|
-
consumer: consumer,
|
|
245
|
-
instrumenter: @instrumenter,
|
|
246
|
-
config: @config,
|
|
247
|
-
)
|
|
248
|
-
|
|
249
|
-
true
|
|
250
|
-
end
|
|
251
|
-
|
|
252
|
-
def with_pause(topic, partition, offsets)
|
|
253
|
-
pause = pauses[topic][partition]
|
|
254
|
-
return yield pause if config.pause_timeout == 0
|
|
255
|
-
|
|
256
|
-
begin
|
|
257
|
-
yield pause
|
|
258
|
-
# We've successfully processed a batch from the partition, so we can clear the pause.
|
|
259
|
-
pauses[topic][partition].reset!
|
|
260
|
-
rescue => e
|
|
261
|
-
desc = "#{topic}/#{partition}"
|
|
262
|
-
logger.error "Failed to process #{desc} at #{offsets}: #{e}"
|
|
263
|
-
|
|
264
|
-
logger.warn "Pausing partition #{desc} for #{pause.backoff_interval} seconds"
|
|
265
|
-
consumer.pause(topic, partition, offsets.first)
|
|
266
|
-
pause.pause!
|
|
267
|
-
end
|
|
181
|
+
def resume_all_paused_partitions
|
|
182
|
+
partition_processors.values.reject(&:rebalancing_or_shutting_down?).each(&:resume_paused_partition)
|
|
268
183
|
end
|
|
269
184
|
|
|
270
|
-
def
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
partition: partition,
|
|
278
|
-
duration: pause.pause_duration,
|
|
279
|
-
consumer_class: processor.class.to_s,
|
|
280
|
-
}
|
|
281
|
-
@instrumenter.instrument("pause_status", instrumentation_payload)
|
|
282
|
-
|
|
283
|
-
if pause.paused? && pause.expired?
|
|
284
|
-
logger.info "Automatically resuming partition #{topic}/#{partition}, pause timeout expired"
|
|
285
|
-
consumer.resume(topic, partition)
|
|
286
|
-
pause.resume!
|
|
287
|
-
# TODO: # During re-balancing we might have lost the paused partition. Check if partition is still in group before seek. ?
|
|
288
|
-
end
|
|
289
|
-
end
|
|
290
|
-
end
|
|
185
|
+
def common_processor_params
|
|
186
|
+
{
|
|
187
|
+
config: config,
|
|
188
|
+
logger: logger,
|
|
189
|
+
instrumenter: @instrumenter,
|
|
190
|
+
consumer: consumer,
|
|
191
|
+
}
|
|
291
192
|
end
|
|
292
193
|
end
|
|
293
194
|
end
|
data/lib/racecar/version.rb
CHANGED
data/lib/racecar.rb
CHANGED
|
@@ -65,12 +65,12 @@ module Racecar
|
|
|
65
65
|
config.instrumenter
|
|
66
66
|
end
|
|
67
67
|
|
|
68
|
-
def self.run(
|
|
69
|
-
runner(
|
|
68
|
+
def self.run(consumer_class)
|
|
69
|
+
runner(consumer_class).run
|
|
70
70
|
end
|
|
71
71
|
|
|
72
|
-
def self.runner(
|
|
73
|
-
runner = Runner.new(
|
|
72
|
+
def self.runner(consumer_class)
|
|
73
|
+
runner = Runner.new(consumer_class, config: config, logger: logger, instrumenter: config.instrumenter)
|
|
74
74
|
|
|
75
75
|
if config.parallel_workers && config.parallel_workers > 1
|
|
76
76
|
ParallelRunner.new(runner: runner, config: config, logger: logger)
|
metadata
CHANGED
|
@@ -1,15 +1,14 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: racecar
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version:
|
|
4
|
+
version: 3.0.0.alpha.1
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Daniel Schierbeck
|
|
8
8
|
- Benjamin Quorning
|
|
9
|
-
autorequire:
|
|
10
9
|
bindir: exe
|
|
11
10
|
cert_chain: []
|
|
12
|
-
date:
|
|
11
|
+
date: 1980-01-02 00:00:00.000000000 Z
|
|
13
12
|
dependencies:
|
|
14
13
|
- !ruby/object:Gem::Dependency
|
|
15
14
|
name: king_konf
|
|
@@ -149,7 +148,6 @@ dependencies:
|
|
|
149
148
|
- - ">="
|
|
150
149
|
- !ruby/object:Gem::Version
|
|
151
150
|
version: '0'
|
|
152
|
-
description:
|
|
153
151
|
email:
|
|
154
152
|
- dschierbeck@zendesk.com
|
|
155
153
|
- bquorning@zendesk.com
|
|
@@ -159,6 +157,7 @@ executables:
|
|
|
159
157
|
extensions: []
|
|
160
158
|
extra_rdoc_files: []
|
|
161
159
|
files:
|
|
160
|
+
- ".github/CODEOWNERS"
|
|
162
161
|
- ".github/dependabot.yml"
|
|
163
162
|
- ".github/workflows/ci.yml"
|
|
164
163
|
- ".github/workflows/publish.yml"
|
|
@@ -187,6 +186,7 @@ files:
|
|
|
187
186
|
- lib/generators/templates/consumer.rb.erb
|
|
188
187
|
- lib/generators/templates/racecar.yml.erb
|
|
189
188
|
- lib/racecar.rb
|
|
189
|
+
- lib/racecar/async_partition_processor.rb
|
|
190
190
|
- lib/racecar/cli.rb
|
|
191
191
|
- lib/racecar/config.rb
|
|
192
192
|
- lib/racecar/consumer.rb
|
|
@@ -203,6 +203,7 @@ files:
|
|
|
203
203
|
- lib/racecar/message_delivery_error.rb
|
|
204
204
|
- lib/racecar/null_instrumenter.rb
|
|
205
205
|
- lib/racecar/parallel_runner.rb
|
|
206
|
+
- lib/racecar/partition_processor.rb
|
|
206
207
|
- lib/racecar/pause.rb
|
|
207
208
|
- lib/racecar/producer.rb
|
|
208
209
|
- lib/racecar/rails_config_file_loader.rb
|
|
@@ -214,7 +215,6 @@ homepage: https://github.com/zendesk/racecar
|
|
|
214
215
|
licenses:
|
|
215
216
|
- Apache License Version 2.0
|
|
216
217
|
metadata: {}
|
|
217
|
-
post_install_message:
|
|
218
218
|
rdoc_options: []
|
|
219
219
|
require_paths:
|
|
220
220
|
- lib
|
|
@@ -229,8 +229,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
|
229
229
|
- !ruby/object:Gem::Version
|
|
230
230
|
version: '0'
|
|
231
231
|
requirements: []
|
|
232
|
-
rubygems_version: 3.
|
|
233
|
-
signing_key:
|
|
232
|
+
rubygems_version: 3.6.9
|
|
234
233
|
specification_version: 4
|
|
235
234
|
summary: A framework for running Kafka consumers
|
|
236
235
|
test_files: []
|