racecar 2.0.0 → 2.10.0.beta2

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.
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