racecar 2.0.0 → 2.10.0.beta2

Sign up to get free protection for your applications and to get access to all the features.
Files changed (46) hide show
  1. checksums.yaml +4 -4
  2. data/.github/dependabot.yml +17 -0
  3. data/.github/workflows/ci.yml +46 -0
  4. data/.github/workflows/publish.yml +12 -0
  5. data/.gitignore +1 -2
  6. data/CHANGELOG.md +83 -1
  7. data/Dockerfile +9 -0
  8. data/Gemfile +6 -0
  9. data/Gemfile.lock +72 -0
  10. data/README.md +303 -82
  11. data/Rakefile +5 -0
  12. data/docker-compose.yml +65 -0
  13. data/examples/batch_consumer.rb +4 -2
  14. data/examples/cat_consumer.rb +2 -0
  15. data/examples/producing_consumer.rb +2 -0
  16. data/exe/racecar +37 -14
  17. data/extra/datadog-dashboard.json +1 -0
  18. data/lib/ensure_hash_compact.rb +2 -0
  19. data/lib/generators/racecar/consumer_generator.rb +2 -0
  20. data/lib/generators/racecar/install_generator.rb +2 -0
  21. data/lib/racecar/cli.rb +26 -21
  22. data/lib/racecar/config.rb +80 -4
  23. data/lib/racecar/consumer.rb +51 -6
  24. data/lib/racecar/consumer_set.rb +113 -44
  25. data/lib/racecar/ctl.rb +31 -3
  26. data/lib/racecar/daemon.rb +4 -2
  27. data/lib/racecar/datadog.rb +83 -3
  28. data/lib/racecar/delivery_callback.rb +27 -0
  29. data/lib/racecar/erroneous_state_error.rb +34 -0
  30. data/lib/racecar/heroku.rb +49 -0
  31. data/lib/racecar/instrumenter.rb +4 -7
  32. data/lib/racecar/liveness_probe.rb +78 -0
  33. data/lib/racecar/message.rb +6 -1
  34. data/lib/racecar/message_delivery_error.rb +112 -0
  35. data/lib/racecar/null_instrumenter.rb +2 -0
  36. data/lib/racecar/parallel_runner.rb +110 -0
  37. data/lib/racecar/pause.rb +8 -4
  38. data/lib/racecar/producer.rb +139 -0
  39. data/lib/racecar/rails_config_file_loader.rb +7 -1
  40. data/lib/racecar/rebalance_listener.rb +58 -0
  41. data/lib/racecar/runner.rb +79 -37
  42. data/lib/racecar/version.rb +3 -1
  43. data/lib/racecar.rb +36 -8
  44. data/racecar.gemspec +7 -4
  45. metadata +47 -25
  46. data/.github/workflows/rspec.yml +0 -24
@@ -1,6 +1,11 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require "rdkafka"
2
4
  require "racecar/pause"
3
5
  require "racecar/message"
6
+ require "racecar/message_delivery_error"
7
+ require "racecar/erroneous_state_error"
8
+ require "racecar/delivery_callback"
4
9
 
5
10
  module Racecar
6
11
  class Runner
@@ -51,6 +56,7 @@ module Racecar
51
56
  producer: producer,
52
57
  consumer: consumer,
53
58
  instrumenter: @instrumenter,
59
+ config: @config,
54
60
  )
55
61
 
56
62
  instrumentation_payload = {
@@ -62,6 +68,8 @@ module Racecar
62
68
  loop do
63
69
  break if @stop_requested
64
70
  resume_paused_partitions
71
+
72
+ @instrumenter.instrument("start_main_loop", instrumentation_payload)
65
73
  @instrumenter.instrument("main_loop", instrumentation_payload) do
66
74
  case process_method
67
75
  when :batch then
@@ -77,12 +85,19 @@ module Racecar
77
85
  end
78
86
 
79
87
  logger.info "Gracefully shutting down"
80
- processor.deliver!
81
- processor.teardown
82
- consumer.commit
83
- @instrumenter.instrument('leave_group') do
84
- consumer.close
88
+ begin
89
+ processor.deliver!
90
+ processor.teardown
91
+ consumer.commit
92
+ ensure
93
+ @instrumenter.instrument('leave_group') do
94
+ consumer.close
95
+ end
85
96
  end
97
+ ensure
98
+ producer.close
99
+ Racecar::Datadog.close if config.datadog_enabled
100
+ @instrumenter.instrument("shut_down", instrumentation_payload || {})
86
101
  end
87
102
 
88
103
  def stop
@@ -96,28 +111,33 @@ module Racecar
96
111
  def process_method
97
112
  @process_method ||= begin
98
113
  case
99
- when processor.respond_to?(:process_batch) then :batch
100
- when processor.respond_to?(:process) then :single
114
+ when processor.respond_to?(:process_batch)
115
+ if processor.method(:process_batch).arity != 1
116
+ raise Racecar::Error, "Invalid method signature for `process_batch`. The method must take exactly 1 argument."
117
+ end
118
+
119
+ :batch
120
+ when processor.respond_to?(:process)
121
+ if processor.method(:process).arity != 1
122
+ raise Racecar::Error, "Invalid method signature for `process`. The method must take exactly 1 argument."
123
+ end
124
+
125
+ :single
101
126
  else
102
- raise NotImplementedError, "Consumer class must implement process or process_batch method"
127
+ raise NotImplementedError, "Consumer class `#{processor.class}` must implement a `process` or `process_batch` method"
103
128
  end
104
129
  end
105
130
  end
106
131
 
107
132
  def consumer
108
133
  @consumer ||= begin
109
- # Manually store offset after messages have been processed successfully
110
- # to avoid marking failed messages as committed. The call just updates
111
- # a value within librdkafka and is asynchronously written to proper
112
- # storage through auto commits.
113
- config.consumer << "enable.auto.offset.store=false"
114
134
  ConsumerSet.new(config, logger, @instrumenter)
115
135
  end
116
136
  end
117
137
 
118
138
  def producer
119
139
  @producer ||= Rdkafka::Config.new(producer_config).producer.tap do |producer|
120
- producer.delivery_callback = delivery_callback
140
+ producer.delivery_callback = Racecar::DeliveryCallback.new(instrumenter: @instrumenter)
121
141
  end
122
142
  end
123
143
 
@@ -126,23 +146,16 @@ module Racecar
126
146
  producer_config = {
127
147
  "bootstrap.servers" => config.brokers.join(","),
128
148
  "client.id" => config.client_id,
129
- "statistics.interval.ms" => 1000,
149
+ "statistics.interval.ms" => config.statistics_interval_ms,
150
+ "message.timeout.ms" => config.message_timeout * 1000,
151
+ "partitioner" => config.partitioner.to_s,
130
152
  }
153
+
131
154
  producer_config["compression.codec"] = config.producer_compression_codec.to_s unless config.producer_compression_codec.nil?
132
155
  producer_config.merge!(config.rdkafka_producer)
133
156
  producer_config
134
157
  end
135
158
 
136
- def delivery_callback
137
- ->(delivery_report) do
138
- payload = {
139
- offset: delivery_report.offset,
140
- partition: delivery_report.partition
141
- }
142
- @instrumenter.instrument("acknowledged_message", payload)
143
- end
144
- end
145
-
146
159
  def install_signal_handlers
147
160
  # Stop the consumer on SIGINT, SIGQUIT or SIGTERM.
148
161
  trap("QUIT") { stop }
@@ -166,14 +179,16 @@ module Racecar
166
179
  }
167
180
 
168
181
  @instrumenter.instrument("start_process_message", instrumentation_payload)
169
- with_pause(message.topic, message.partition, message.offset..message.offset) do
182
+ with_pause(message.topic, message.partition, message.offset..message.offset) do |pause|
170
183
  begin
171
184
  @instrumenter.instrument("process_message", instrumentation_payload) do
172
- processor.process(Racecar::Message.new(message))
185
+ processor.process(Racecar::Message.new(message, retries_count: pause.pauses_count))
173
186
  processor.deliver!
174
187
  consumer.store_offset(message)
175
188
  end
176
189
  rescue => e
190
+ instrumentation_payload[:unrecoverable_delivery_error] = reset_producer_on_unrecoverable_delivery_errors(e)
191
+ instrumentation_payload[:retries_count] = pause.pauses_count
177
192
  config.error_handler.call(e, instrumentation_payload)
178
193
  raise e
179
194
  end
@@ -193,32 +208,59 @@ module Racecar
193
208
  }
194
209
 
195
210
  @instrumenter.instrument("start_process_batch", instrumentation_payload)
196
- @instrumenter.instrument("process_batch", instrumentation_payload) do
197
- with_pause(first.topic, first.partition, first.offset..last.offset) do
198
- begin
199
- processor.process_batch(messages.map {|message| Racecar::Message.new(message) })
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)
216
+ end
217
+ processor.process_batch(racecar_messages)
200
218
  processor.deliver!
201
219
  consumer.store_offset(messages.last)
202
- rescue => e
203
- config.error_handler.call(e, instrumentation_payload)
204
- raise e
205
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
206
226
  end
207
227
  end
208
228
  end
209
229
 
230
+ # librdkafka will continue to try to deliver already queued messages, even if ruby-rdkafka
231
+ # raised before that. This method detects any unrecoverable errors and resets the producer
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
+
210
252
  def with_pause(topic, partition, offsets)
211
- return yield if config.pause_timeout == 0
253
+ pause = pauses[topic][partition]
254
+ return yield pause if config.pause_timeout == 0
212
255
 
213
256
  begin
214
- yield
257
+ yield pause
215
258
  # We've successfully processed a batch from the partition, so we can clear the pause.
216
259
  pauses[topic][partition].reset!
217
260
  rescue => e
218
261
  desc = "#{topic}/#{partition}"
219
262
  logger.error "Failed to process #{desc} at #{offsets}: #{e}"
220
263
 
221
- pause = pauses[topic][partition]
222
264
  logger.warn "Pausing partition #{desc} for #{pause.backoff_interval} seconds"
223
265
  consumer.pause(topic, partition, offsets.first)
224
266
  pause.pause!
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Racecar
2
- VERSION = "2.0.0"
4
+ VERSION = "2.10.0.beta2"
3
5
  end
data/lib/racecar.rb CHANGED
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require "logger"
2
4
 
3
5
  require "racecar/instrumenter"
@@ -5,7 +7,10 @@ require "racecar/null_instrumenter"
5
7
  require "racecar/consumer"
6
8
  require "racecar/consumer_set"
7
9
  require "racecar/runner"
10
+ require "racecar/parallel_runner"
11
+ require "racecar/producer"
8
12
  require "racecar/config"
13
+ require "racecar/version"
9
14
  require "ensure_hash_compact"
10
15
 
11
16
  module Racecar
@@ -35,19 +40,42 @@ module Racecar
35
40
  config.logger = logger
36
41
  end
37
42
 
38
- def self.instrumenter
39
- @instrumenter ||= begin
40
- default_payload = { client_id: config.client_id, group_id: config.group_id }
43
+ def self.produce_async(value:, topic:, **options)
44
+ producer.produce_async(value: value, topic: topic, **options)
45
+ end
41
46
 
42
- Instrumenter.new(default_payload).tap do |instrumenter|
43
- if instrumenter.backend == NullInstrumenter
44
- logger.warn "ActiveSupport::Notifications not available, instrumentation is disabled"
45
- end
47
+ def self.produce_sync(value:, topic:, **options)
48
+ producer.produce_sync(value: value, topic: topic, **options)
49
+ end
50
+
51
+ def self.wait_for_delivery(&block)
52
+ producer.wait_for_delivery(&block)
53
+ end
54
+
55
+ def self.producer
56
+ Thread.current[:racecar_producer] ||= begin
57
+ if config.datadog_enabled
58
+ require "racecar/datadog"
46
59
  end
60
+ Racecar::Producer.new(config: config, logger: logger, instrumenter: instrumenter)
47
61
  end
48
62
  end
49
63
 
64
+ def self.instrumenter
65
+ config.instrumenter
66
+ end
67
+
50
68
  def self.run(processor)
51
- Runner.new(processor, config: config, logger: logger, instrumenter: instrumenter).run
69
+ runner(processor).run
70
+ end
71
+
72
+ def self.runner(processor)
73
+ runner = Runner.new(processor, config: config, logger: logger, instrumenter: config.instrumenter)
74
+
75
+ if config.parallel_workers && config.parallel_workers > 1
76
+ ParallelRunner.new(runner: runner, config: config, logger: logger)
77
+ else
78
+ runner
79
+ end
52
80
  end
53
81
  end
data/racecar.gemspec CHANGED
@@ -20,13 +20,16 @@ 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.add_runtime_dependency "king_konf", "~> 0.3.7"
24
- spec.add_runtime_dependency "rdkafka", "~> 0.8.0.beta.1"
23
+ spec.required_ruby_version = '>= 2.6'
24
+
25
+ spec.add_runtime_dependency "king_konf", "~> 1.0.0"
26
+ spec.add_runtime_dependency "rdkafka", "~> 0.13.0"
25
27
 
26
28
  spec.add_development_dependency "bundler", [">= 1.13", "< 3"]
29
+ spec.add_development_dependency "pry-byebug"
27
30
  spec.add_development_dependency "rake", "> 10.0"
28
31
  spec.add_development_dependency "rspec", "~> 3.0"
29
32
  spec.add_development_dependency "timecop"
30
- spec.add_development_dependency "dogstatsd-ruby", ">= 4.0.0", "< 5.0.0"
31
- spec.add_development_dependency "activesupport", ">= 4.0", "< 6.1"
33
+ spec.add_development_dependency "dogstatsd-ruby", ">= 4.0.0", "< 6.0.0"
34
+ spec.add_development_dependency "activesupport"
32
35
  end
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.0.0
4
+ version: 2.10.0.beta2
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: 2020-08-24 00:00:00.000000000 Z
12
+ date: 2023-10-18 00:00:00.000000000 Z
13
13
  dependencies:
14
14
  - !ruby/object:Gem::Dependency
15
15
  name: king_konf
@@ -17,28 +17,28 @@ dependencies:
17
17
  requirements:
18
18
  - - "~>"
19
19
  - !ruby/object:Gem::Version
20
- version: 0.3.7
20
+ version: 1.0.0
21
21
  type: :runtime
22
22
  prerelease: false
23
23
  version_requirements: !ruby/object:Gem::Requirement
24
24
  requirements:
25
25
  - - "~>"
26
26
  - !ruby/object:Gem::Version
27
- version: 0.3.7
27
+ version: 1.0.0
28
28
  - !ruby/object:Gem::Dependency
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.8.0.beta.1
34
+ version: 0.13.0
35
35
  type: :runtime
36
36
  prerelease: false
37
37
  version_requirements: !ruby/object:Gem::Requirement
38
38
  requirements:
39
39
  - - "~>"
40
40
  - !ruby/object:Gem::Version
41
- version: 0.8.0.beta.1
41
+ version: 0.13.0
42
42
  - !ruby/object:Gem::Dependency
43
43
  name: bundler
44
44
  requirement: !ruby/object:Gem::Requirement
@@ -59,6 +59,20 @@ dependencies:
59
59
  - - "<"
60
60
  - !ruby/object:Gem::Version
61
61
  version: '3'
62
+ - !ruby/object:Gem::Dependency
63
+ name: pry-byebug
64
+ requirement: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - ">="
67
+ - !ruby/object:Gem::Version
68
+ version: '0'
69
+ type: :development
70
+ prerelease: false
71
+ version_requirements: !ruby/object:Gem::Requirement
72
+ requirements:
73
+ - - ">="
74
+ - !ruby/object:Gem::Version
75
+ version: '0'
62
76
  - !ruby/object:Gem::Dependency
63
77
  name: rake
64
78
  requirement: !ruby/object:Gem::Requirement
@@ -110,7 +124,7 @@ dependencies:
110
124
  version: 4.0.0
111
125
  - - "<"
112
126
  - !ruby/object:Gem::Version
113
- version: 5.0.0
127
+ version: 6.0.0
114
128
  type: :development
115
129
  prerelease: false
116
130
  version_requirements: !ruby/object:Gem::Requirement
@@ -120,28 +134,22 @@ dependencies:
120
134
  version: 4.0.0
121
135
  - - "<"
122
136
  - !ruby/object:Gem::Version
123
- version: 5.0.0
137
+ version: 6.0.0
124
138
  - !ruby/object:Gem::Dependency
125
139
  name: activesupport
126
140
  requirement: !ruby/object:Gem::Requirement
127
141
  requirements:
128
142
  - - ">="
129
143
  - !ruby/object:Gem::Version
130
- version: '4.0'
131
- - - "<"
132
- - !ruby/object:Gem::Version
133
- version: '6.1'
144
+ version: '0'
134
145
  type: :development
135
146
  prerelease: false
136
147
  version_requirements: !ruby/object:Gem::Requirement
137
148
  requirements:
138
149
  - - ">="
139
150
  - !ruby/object:Gem::Version
140
- version: '4.0'
141
- - - "<"
142
- - !ruby/object:Gem::Version
143
- version: '6.1'
144
- description:
151
+ version: '0'
152
+ description:
145
153
  email:
146
154
  - dschierbeck@zendesk.com
147
155
  - bquorning@zendesk.com
@@ -151,22 +159,28 @@ executables:
151
159
  extensions: []
152
160
  extra_rdoc_files: []
153
161
  files:
154
- - ".github/workflows/rspec.yml"
162
+ - ".github/dependabot.yml"
163
+ - ".github/workflows/ci.yml"
164
+ - ".github/workflows/publish.yml"
155
165
  - ".gitignore"
156
166
  - ".rspec"
157
167
  - CHANGELOG.md
168
+ - Dockerfile
158
169
  - Gemfile
170
+ - Gemfile.lock
159
171
  - LICENSE.txt
160
172
  - Procfile
161
173
  - README.md
162
174
  - Rakefile
163
175
  - bin/console
164
176
  - bin/setup
177
+ - docker-compose.yml
165
178
  - examples/batch_consumer.rb
166
179
  - examples/cat_consumer.rb
167
180
  - examples/producing_consumer.rb
168
181
  - exe/racecar
169
182
  - exe/racecarctl
183
+ - extra/datadog-dashboard.json
170
184
  - lib/ensure_hash_compact.rb
171
185
  - lib/generators/racecar/consumer_generator.rb
172
186
  - lib/generators/racecar/install_generator.rb
@@ -180,11 +194,19 @@ files:
180
194
  - lib/racecar/ctl.rb
181
195
  - lib/racecar/daemon.rb
182
196
  - lib/racecar/datadog.rb
197
+ - lib/racecar/delivery_callback.rb
198
+ - lib/racecar/erroneous_state_error.rb
199
+ - lib/racecar/heroku.rb
183
200
  - lib/racecar/instrumenter.rb
201
+ - lib/racecar/liveness_probe.rb
184
202
  - lib/racecar/message.rb
203
+ - lib/racecar/message_delivery_error.rb
185
204
  - lib/racecar/null_instrumenter.rb
205
+ - lib/racecar/parallel_runner.rb
186
206
  - lib/racecar/pause.rb
207
+ - lib/racecar/producer.rb
187
208
  - lib/racecar/rails_config_file_loader.rb
209
+ - lib/racecar/rebalance_listener.rb
188
210
  - lib/racecar/runner.rb
189
211
  - lib/racecar/version.rb
190
212
  - racecar.gemspec
@@ -192,7 +214,7 @@ homepage: https://github.com/zendesk/racecar
192
214
  licenses:
193
215
  - Apache License Version 2.0
194
216
  metadata: {}
195
- post_install_message:
217
+ post_install_message:
196
218
  rdoc_options: []
197
219
  require_paths:
198
220
  - lib
@@ -200,15 +222,15 @@ required_ruby_version: !ruby/object:Gem::Requirement
200
222
  requirements:
201
223
  - - ">="
202
224
  - !ruby/object:Gem::Version
203
- version: '0'
225
+ version: '2.6'
204
226
  required_rubygems_version: !ruby/object:Gem::Requirement
205
227
  requirements:
206
- - - ">="
228
+ - - ">"
207
229
  - !ruby/object:Gem::Version
208
- version: '0'
230
+ version: 1.3.1
209
231
  requirements: []
210
- rubygems_version: 3.1.2
211
- signing_key:
232
+ rubygems_version: 3.0.3.1
233
+ signing_key:
212
234
  specification_version: 4
213
235
  summary: A framework for running Kafka consumers
214
236
  test_files: []
@@ -1,24 +0,0 @@
1
- name: Execute Specs
2
-
3
- on: [push]
4
-
5
- jobs:
6
- rspec:
7
-
8
- runs-on: ubuntu-latest
9
-
10
- strategy:
11
- matrix:
12
- ruby-version: ["2.5.x", "2.6.x"]
13
-
14
- steps:
15
- - uses: actions/checkout@v1
16
- - name: Set up Ruby 2.6
17
- uses: actions/setup-ruby@v1
18
- with:
19
- ruby-version: ${{ matrix.ruby-version }}
20
- - name: Build and test with RSpec
21
- run: |
22
- gem install bundler --no-document
23
- bundle install --jobs 4 --retry 3
24
- bundle exec rspec --format documentation --require spec_helper --color