racecar 2.9.0.beta1 → 2.10.0.beta1

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 364239e804c99b816c0a37802b605ff4035d2d4c51abc0b161e11609268349ea
4
- data.tar.gz: 4d7bd65a04b8c914640a2edc11f3c7521b2e061db8eb3c215ba29a785f031e12
3
+ metadata.gz: e0057ec2861db783b83e34a63c0a09a0be476ed6cb57b760dea3e2cc5ef57d1f
4
+ data.tar.gz: 22dac9ed21af6f9abfd50f22d9dc86032a68af556e1be77bff2f59cbf505ccf2
5
5
  SHA512:
6
- metadata.gz: c60037cd0dd5477a77a5fddd42003a3d4be67c38fb1c812cfff698cdb8fb7cd62e7e65097b6508c2a19a768daad5139f3eb469e91f0ef3ff3fe9535f9fd86a8a
7
- data.tar.gz: 88d3ffcfa27b6bcb89b454151db353165ab62fa0dd42586c177d05ea6091663227b8d78f14180e69c4937417d0f761fc5666657be1320d44d54eeebcc5d78601
6
+ metadata.gz: f3baeec4de02a68b55e4a302d2b713de67d0245564aae57cbca363aec8b34633f8dd2220d44c1e212347857d7fde35217644dae8afdbde06103e11a52394997d
7
+ data.tar.gz: 66a2168ca6f7a4ec7a6d63e8cf767397502c02487d87445b59e4d7fbdd0269abee3279019e36ab1f343e246d4c58146e951ad521b1bc881a4cb60b3f35076b6e
@@ -12,30 +12,34 @@ jobs:
12
12
 
13
13
  strategy:
14
14
  matrix:
15
- ruby-version: ["2.6", "3.0"]
15
+ ruby-version:
16
+ - "2.7"
17
+ - "3.0"
18
+ - "3.1"
19
+ - "3.2"
16
20
 
17
21
  steps:
18
- - uses: zendesk/checkout@v2
22
+ - uses: zendesk/checkout@v3
19
23
  - name: Set up Ruby
20
- uses: zendesk/setup-ruby@v1.64.1
24
+ uses: zendesk/setup-ruby@v1
21
25
  with:
22
26
  ruby-version: ${{ matrix.ruby-version }}
23
27
  bundler-cache: true
24
28
  - name: Build and test with RSpec
25
- run: bundle exec rspec --format documentation --require spec_helper --color --exclude-pattern='spec/integration/*_spec.rb'
29
+ run: bundle exec rspec --exclude-pattern='spec/integration/*_spec.rb'
26
30
 
27
31
  integration-specs:
28
32
  runs-on: ubuntu-latest
29
33
  steps:
30
- - uses: zendesk/checkout@v2
34
+ - uses: zendesk/checkout@v3
31
35
  - name: Set up Ruby
32
- uses: zendesk/setup-ruby@v1.64.1
36
+ uses: zendesk/setup-ruby@v1
33
37
  with:
34
- ruby-version: 2.7
38
+ ruby-version: "2.7"
35
39
  bundler-cache: true
36
40
  - name: Bring up docker-compose stack
37
41
  run: docker-compose up -d
38
42
  - name: Build and test with RSpec
39
43
  env:
40
44
  RACECAR_BROKERS: localhost:9092
41
- run: timeout --kill-after 180 150 bundle exec rspec --format documentation --require spec_helper --color spec/integration/*_spec.rb
45
+ run: timeout --kill-after 180 150 bundle exec rspec spec/integration/*_spec.rb
data/CHANGELOG.md CHANGED
@@ -1,6 +1,18 @@
1
1
  # Changelog
2
2
 
3
- ## v2.9.0.beta1
3
+ ## Unreleased
4
+
5
+ ## 2.10.0.beta1
6
+
7
+ * Bump rdkafka gem version to 0.13.0
8
+ * Support cooperative-sticky
9
+ * Instrument produce delivery errors
10
+ * Fix config load for liveness probe
11
+ * Send exceptions to `process_batch` instrumenter
12
+ * Docker test fixes
13
+ * Test with Ruby 3.2
14
+
15
+ ## v2.9.0, v2.9.0.beta1
4
16
 
5
17
  * Add `partitioner` producer config option to allow changing the strategy to
6
18
  determine which topic partition a message is written to when racecar
data/Dockerfile CHANGED
@@ -1,4 +1,4 @@
1
- FROM circleci/ruby:2.7.2
1
+ FROM cimg/ruby:2.7.8
2
2
 
3
3
  RUN sudo apt-get update
4
4
  RUN sudo apt-get install docker
data/Gemfile CHANGED
@@ -5,6 +5,6 @@ source 'https://rubygems.org'
5
5
  # Specify your gem's dependencies in racecar.gemspec
6
6
  gemspec
7
7
 
8
- # We actually support version 6.0 (see gemspec); this extra restriction is added just for running the test suite also
9
- # on Ruby 2.4, which activesupport 6.0 no longer supports
10
- gem 'activesupport', '< 6.0'
8
+ # We actually support version 7.x (see gemspec); this extra restriction is added just for running the test suite also
9
+ # on Ruby 2.6, which activesupport 7.0 does not support.
10
+ gem 'activesupport', '~> 6.1.0'
data/Gemfile.lock CHANGED
@@ -1,67 +1,72 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- racecar (2.9.0.beta1)
4
+ racecar (2.9.0)
5
5
  king_konf (~> 1.0.0)
6
- rdkafka (~> 0.12.0)
6
+ rdkafka (~> 0.13.0)
7
7
 
8
8
  GEM
9
9
  remote: https://rubygems.org/
10
10
  specs:
11
- activesupport (5.2.6)
11
+ activesupport (6.1.7.3)
12
12
  concurrent-ruby (~> 1.0, >= 1.0.2)
13
- i18n (>= 0.7, < 2)
14
- minitest (~> 5.1)
15
- tzinfo (~> 1.1)
13
+ i18n (>= 1.6, < 2)
14
+ minitest (>= 5.1)
15
+ tzinfo (~> 2.0)
16
+ zeitwerk (~> 2.3)
17
+ byebug (11.1.3)
16
18
  coderay (1.1.3)
17
- concurrent-ruby (1.1.9)
18
- diff-lcs (1.4.4)
19
- dogstatsd-ruby (5.2.0)
19
+ concurrent-ruby (1.2.2)
20
+ diff-lcs (1.5.0)
21
+ dogstatsd-ruby (5.5.0)
20
22
  ffi (1.15.5)
21
- i18n (1.8.10)
23
+ i18n (1.12.0)
22
24
  concurrent-ruby (~> 1.0)
23
25
  king_konf (1.0.1)
24
26
  method_source (1.0.0)
25
27
  mini_portile2 (2.8.1)
26
- minitest (5.14.4)
27
- pry (0.13.1)
28
+ minitest (5.18.0)
29
+ pry (0.14.2)
28
30
  coderay (~> 1.1)
29
31
  method_source (~> 1.0)
30
- rake (13.0.1)
31
- rdkafka (0.12.0)
32
+ pry-byebug (3.10.1)
33
+ byebug (~> 11.0)
34
+ pry (>= 0.13, < 0.15)
35
+ rake (13.0.6)
36
+ rdkafka (0.13.0)
32
37
  ffi (~> 1.15)
33
38
  mini_portile2 (~> 2.6)
34
39
  rake (> 12)
35
- rspec (3.10.0)
36
- rspec-core (~> 3.10.0)
37
- rspec-expectations (~> 3.10.0)
38
- rspec-mocks (~> 3.10.0)
39
- rspec-core (3.10.1)
40
- rspec-support (~> 3.10.0)
41
- rspec-expectations (3.10.1)
40
+ rspec (3.12.0)
41
+ rspec-core (~> 3.12.0)
42
+ rspec-expectations (~> 3.12.0)
43
+ rspec-mocks (~> 3.12.0)
44
+ rspec-core (3.12.1)
45
+ rspec-support (~> 3.12.0)
46
+ rspec-expectations (3.12.2)
42
47
  diff-lcs (>= 1.2.0, < 2.0)
43
- rspec-support (~> 3.10.0)
44
- rspec-mocks (3.10.2)
48
+ rspec-support (~> 3.12.0)
49
+ rspec-mocks (3.12.4)
45
50
  diff-lcs (>= 1.2.0, < 2.0)
46
- rspec-support (~> 3.10.0)
47
- rspec-support (3.10.2)
48
- thread_safe (0.3.6)
49
- timecop (0.9.2)
50
- tzinfo (1.2.10)
51
- thread_safe (~> 0.1)
51
+ rspec-support (~> 3.12.0)
52
+ rspec-support (3.12.0)
53
+ timecop (0.9.6)
54
+ tzinfo (2.0.6)
55
+ concurrent-ruby (~> 1.0)
56
+ zeitwerk (2.6.7)
52
57
 
53
58
  PLATFORMS
54
59
  ruby
55
60
 
56
61
  DEPENDENCIES
57
- activesupport (< 6.0)
62
+ activesupport (~> 6.1.0)
58
63
  bundler (>= 1.13, < 3)
59
64
  dogstatsd-ruby (>= 4.0.0, < 6.0.0)
60
- pry
65
+ pry-byebug
61
66
  racecar!
62
67
  rake (> 10.0)
63
68
  rspec (~> 3.0)
64
69
  timecop
65
70
 
66
71
  BUNDLED WITH
67
- 2.3.7
72
+ 2.4.9
data/README.md CHANGED
@@ -249,7 +249,8 @@ You can set message headers by passing a `headers:` option with a Hash of header
249
249
  ### Standalone Producer
250
250
 
251
251
  Racecar provides a standalone producer to publish messages to Kafka directly from your Rails application:
252
- ```
252
+
253
+ ```ruby
253
254
  # app/controllers/comments_controller.rb
254
255
  class CommentsController < ApplicationController
255
256
  def create
@@ -263,7 +264,8 @@ end
263
264
  ```
264
265
 
265
266
  The above example will block the server process until the message has been delivered. If you want deliveries to happen in the background in order to free up your server processes more quickly, call #deliver_async instead:
266
- ```
267
+
268
+ ```ruby
267
269
  # app/controllers/comments_controller.rb
268
270
  class CommentsController < ApplicationController
269
271
  def show
@@ -284,9 +286,9 @@ end
284
286
  ```
285
287
  In addition to improving response time, delivering messages asynchronously also protects your application against Kafka availability issues -- if messages cannot be delivered, they'll be buffered for later and retried automatically.
286
288
 
287
- A third method is to produce messages first (without delivering the messages to Kafka yet), and deliver them synchronously later.
289
+ A third method is to produce messages first (without delivering the messages to Kafka yet), and deliver them synchronously later:
288
290
 
289
- ```
291
+ ```ruby
290
292
  # app/controllers/comments_controller.rb
291
293
  class CommentsController < ApplicationController
292
294
  def create
@@ -415,7 +417,7 @@ Racecar supports [Datadog](https://www.datadoghq.com/) monitoring integration. I
415
417
  - `datadog_namespace` – The namespace to use for Datadog metrics.
416
418
  - `datadog_tags` – Tags that should always be set on Datadog metrics.
417
419
 
418
- Furthermore, there's a [standard Datadog dashboard configution file](https://raw.githubusercontent.com/zendesk/racecar/master/extra/datadog-dashboard.json) that you can import to get started with a Racecar dashboard for all of your consumers.
420
+ Furthermore, there's a [standard Datadog dashboard configuration file](https://raw.githubusercontent.com/zendesk/racecar/master/extra/datadog-dashboard.json) that you can import to get started with a Racecar dashboard for all of your consumers.
419
421
 
420
422
  #### Consumers Without Rails
421
423
 
@@ -474,6 +476,8 @@ With Foreman, you can easily run these processes locally by executing `foreman r
474
476
 
475
477
  If you run your applications in Kubernetes, use the following [Deployment](https://kubernetes.io/docs/concepts/workloads/controllers/deployment/) spec as a starting point:
476
478
 
479
+ ##### Recreate Strategy
480
+
477
481
  ```yaml
478
482
  apiVersion: apps/v1
479
483
  kind: Deployment
@@ -481,8 +485,8 @@ metadata:
481
485
  name: my-racecar-deployment
482
486
  labels:
483
487
  app: my-racecar
484
- spec:
485
- replicas: 3 # <-- this will give us three consumers in the group.
488
+ spec
489
+ replicas: 4 # <-- this is a good value if you have a multliple of 4 partitions
486
490
  selector:
487
491
  matchLabels:
488
492
  app: my-racecar
@@ -504,9 +508,33 @@ spec:
504
508
  value: 5
505
509
  ```
506
510
 
507
- The important part is the `strategy.type` value, which tells Kubernetes how to upgrade from one version of your Deployment to another. Many services use so-called _rolling updates_, where some but not all containers are replaced with the new version. This is done so that, if the new version doesn't work, the old version is still there to serve most of the requests. For Kafka consumers, this doesn't work well. The reason is that every time a consumer joins or leaves a group, every other consumer in the group needs to stop and synchronize the list of partitions assigned to each group member. So if the group is updated in a rolling fashion, this synchronization would occur over and over again, causing undesirable double-processing of messages as consumers would start only to be synchronized shortly after.
511
+ This configuration uses the recreate strategy which completely terminates all consumers before starting new ones.
512
+ It's simple and easy to understand but can result in significant 'downtime' where no messages are processed.
513
+
514
+ ##### Rolling Updates and 'sticky-cooperative' Assignment
515
+
516
+ A newer alternative is to use the consumer's "cooperative-sticky" assignment strategy which allows healthy consumers to keep processing their partitions while others are terminated.
517
+ This can be combined with a restricted rolling update to minimize processing downtime.
518
+
519
+ Add to your Racecar config:
520
+ ```ruby
521
+ Racecar.configure do |c|
522
+ c.partition_assignment_strategy = "cooperative-sticky"
523
+ end
524
+ ```
525
+
526
+ Replace the Kubernetes deployment strategy with:
527
+ ```yaml
528
+ strategy:
529
+ type: RollingUpdate
530
+ rollingUpdate:
531
+ maxSurge: 0 # <- Never boot an excess consumer
532
+ maxUnavailable: 1 # <- The deploy 'rolls' one consumer at a time
533
+ ```
534
+
535
+ These two configurations should be deployed together.
508
536
 
509
- Instead, the `Recreate` update strategy should be used. It completely tears down the existing containers before starting all of the new containers simultaneously, allowing for a single synchronization stage and a much faster, more stable deployment update.
537
+ While `maxSurge` should always be 0, `maxUnavailable` can be increased to reduce deployment times in exchange for longer pauses in message processing.
510
538
 
511
539
  #### Liveness Probe
512
540
 
@@ -661,7 +689,7 @@ In order to introspect the configuration of a consumer process, send it the `SIG
661
689
 
662
690
  ### Upgrading from v1 to v2
663
691
 
664
- In order to safely upgrade from Racecar v1 to v2, you need to completely shut down your consumer group before starting it up again with the v2 Racecar dependency. In general, you should avoid rolling deploys for consumers groups, so it is likely the case that this will just work for you, but it's a good idea to check first.
692
+ In order to safely upgrade from Racecar v1 to v2, you need to completely shut down your consumer group before starting it up again with the v2 Racecar dependency.
665
693
 
666
694
  ### Compression
667
695
 
@@ -679,12 +707,12 @@ The integration tests run against a Kafka instance that is not automatically sta
679
707
 
680
708
  ### Running RSpec within Docker
681
709
 
682
- There can be behavioural inconsistencies between running the specs on your machine, and in the CI pipeline. Due to this, there is now a Dockerfile included in the project, which is based on the CircleCI ruby 2.7.2 image. This could easily be extended with more Dockerfiles to cover different Ruby versions if desired. In order to run the specs via Docker:
710
+ 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:
683
711
 
684
712
  - Uncomment the `tests` service from the docker-compose.yml
685
713
  - Bring up the stack with `docker-compose up -d`
686
- - Execute the entire suite with `docker-compose run --rm tests rspec`
687
- - Execute a single spec or directory with `docker-compose run --rm tests rspec spec/integration/consumer_spec.rb`
714
+ - Execute the entire suite with `docker-compose run --rm tests bundle exec rspec`
715
+ - Execute a single spec or directory with `docker-compose run --rm tests bundle exec rspec spec/integration/consumer_spec.rb`
688
716
 
689
717
  Please note - your code directory is mounted as a volume, so you can make code changes without needing to rebuild
690
718
 
data/lib/racecar/cli.rb CHANGED
@@ -19,6 +19,7 @@ module Racecar
19
19
  @parser = build_parser
20
20
  @parser.parse!(args)
21
21
  @consumer_name = args.first or raise Racecar::Error, "no consumer specified"
22
+ @runner = nil
22
23
  end
23
24
 
24
25
  def run
@@ -65,10 +66,15 @@ module Racecar
65
66
  end
66
67
 
67
68
  processor = consumer_class.new
68
- Racecar.run(processor)
69
+ @runner = Racecar.runner(processor)
70
+ @runner.run
69
71
  nil
70
72
  end
71
73
 
74
+ def stop
75
+ @runner.stop
76
+ end
77
+
72
78
  private
73
79
 
74
80
  attr_reader :consumer_name
@@ -6,6 +6,7 @@ require "king_konf"
6
6
 
7
7
  require "racecar/liveness_probe"
8
8
  require "racecar/instrumenter"
9
+ require "racecar/rebalance_listener"
9
10
 
10
11
  module Racecar
11
12
  class Config < KingKonf::Config
@@ -28,6 +29,9 @@ module Racecar
28
29
  desc "The minimum number of messages in the local consumer queue"
29
30
  integer :min_message_queue_size, default: 2000
30
31
 
32
+ desc "Which partition assignment strategy to use, range, roundrobin or cooperative-sticky. -- https://github.com/edenhill/librdkafka/blob/master/CONFIGURATION.md"
33
+ string :partition_assignment_strategy, default: "range,roundrobin"
34
+
31
35
  desc "Kafka consumer configuration options, separated with '=' -- https://github.com/edenhill/librdkafka/blob/master/CONFIGURATION.md"
32
36
  list :consumer, default: []
33
37
 
@@ -227,6 +231,7 @@ module Racecar
227
231
  end
228
232
 
229
233
  def load_consumer_class(consumer_class)
234
+ self.consumer_class = consumer_class
230
235
  self.group_id = consumer_class.group_id || self.group_id
231
236
 
232
237
  self.group_id ||= [
@@ -243,6 +248,7 @@ module Racecar
243
248
  self.fetch_messages = consumer_class.fetch_messages || self.fetch_messages
244
249
  self.pidfile ||= "#{group_id}.pid"
245
250
  end
251
+ attr_accessor :consumer_class
246
252
 
247
253
  def on_error(&handler)
248
254
  @error_handler = handler
@@ -37,6 +37,10 @@ module Racecar
37
37
  subscriptions << Subscription.new(topic, start_from_beginning, max_bytes_per_partition, additional_config)
38
38
  end
39
39
  end
40
+
41
+ # Rebalance hooks for subclasses to override
42
+ def on_partitions_assigned(rebalance_event); end
43
+ def on_partitions_revoked(rebalance_event); end
40
44
  end
41
45
 
42
46
  def configure(producer:, consumer:, instrumenter: NullInstrumenter, config: Racecar.config)
@@ -15,6 +15,7 @@ module Racecar
15
15
  @previous_retries = 0
16
16
 
17
17
  @last_poll_read_nil_message = false
18
+ @paused_tpls = Hash.new { |h, k| h[k] = {} }
18
19
  end
19
20
 
20
21
  def poll(max_wait_time_ms = @config.max_wait_time_ms)
@@ -65,11 +66,17 @@ module Racecar
65
66
 
66
67
  def close
67
68
  each_subscribed(&:close)
69
+ @paused_tpls.clear
68
70
  end
69
71
 
70
72
  def current
71
73
  @consumers[@consumer_id_iterator.peek] ||= begin
72
- consumer = Rdkafka::Config.new(rdkafka_config(current_subscription)).consumer
74
+ consumer_config = Rdkafka::Config.new(rdkafka_config(current_subscription))
75
+ listener = RebalanceListener.new(@config.consumer_class, @instrumenter)
76
+ consumer_config.consumer_rebalance_listener = listener
77
+ consumer = consumer_config.consumer
78
+ listener.rdkafka_consumer = consumer
79
+
73
80
  @instrumenter.instrument('join_group') do
74
81
  consumer.subscribe current_subscription.topic
75
82
  end
@@ -95,16 +102,25 @@ module Racecar
95
102
  consumer.pause(filtered_tpl)
96
103
  fake_msg = OpenStruct.new(topic: topic, partition: partition, offset: offset)
97
104
  consumer.seek(fake_msg)
105
+
106
+ @paused_tpls[topic][partition] = [consumer, filtered_tpl]
98
107
  end
99
108
 
100
109
  def resume(topic, partition)
101
110
  consumer, filtered_tpl = find_consumer_by(topic, partition)
111
+
112
+ if !consumer && @paused_tpls[topic][partition]
113
+ consumer, filtered_tpl = @paused_tpls[topic][partition]
114
+ end
115
+
102
116
  if !consumer
103
117
  @logger.info "Attempted to resume #{topic}/#{partition}, but we're not subscribed to it"
104
118
  return
105
119
  end
106
120
 
107
121
  consumer.resume(filtered_tpl)
122
+ @paused_tpls[topic].delete(partition)
123
+ @paused_tpls.delete(topic) if @paused_tpls[topic].empty?
108
124
  end
109
125
 
110
126
  alias :each :each_subscribed
@@ -215,6 +231,11 @@ module Racecar
215
231
  def rdkafka_config(subscription)
216
232
  # https://github.com/edenhill/librdkafka/blob/master/CONFIGURATION.md
217
233
  config = {
234
+ # Manually store offset after messages have been processed successfully
235
+ # to avoid marking failed messages as committed. The call just updates
236
+ # a value within librdkafka and is asynchronously written to proper
237
+ # storage through auto commits.
238
+ "enable.auto.offset.store" => false,
218
239
  "auto.commit.interval.ms" => @config.offset_commit_interval * 1000,
219
240
  "auto.offset.reset" => subscription.start_from_beginning ? "earliest" : "largest",
220
241
  "bootstrap.servers" => @config.brokers.join(","),
@@ -230,7 +251,8 @@ module Racecar
230
251
  "queued.min.messages" => @config.min_message_queue_size,
231
252
  "session.timeout.ms" => @config.session_timeout * 1000,
232
253
  "socket.timeout.ms" => @config.socket_timeout * 1000,
233
- "statistics.interval.ms" => @config.statistics_interval_ms
254
+ "statistics.interval.ms" => @config.statistics_interval_ms,
255
+ "partition.assignment.strategy" => @config.partition_assignment_strategy,
234
256
  }
235
257
  config.merge! @config.rdkafka_consumer
236
258
  config.merge! subscription.additional_config
data/lib/racecar/ctl.rb CHANGED
@@ -36,8 +36,10 @@ module Racecar
36
36
  require "racecar/liveness_probe"
37
37
  parse_options!(args)
38
38
 
39
- if ENV["RAILS_ENV"]
40
- Racecar.config.load_file("config/racecar.yml", ENV["RAILS_ENV"])
39
+ RailsConfigFileLoader.load! unless config.without_rails?
40
+
41
+ if File.exist?("config/racecar.rb")
42
+ require "./config/racecar"
41
43
  end
42
44
 
43
45
  Racecar.config.liveness_probe.check_liveness_within_interval!
@@ -136,5 +138,9 @@ module Racecar
136
138
 
137
139
  parser.parse!(args)
138
140
  end
141
+
142
+ def config
143
+ Racecar.config
144
+ end
139
145
  end
140
146
  end
@@ -54,7 +54,7 @@ module Racecar
54
54
  end
55
55
 
56
56
  def pid
57
- if File.exists?(pidfile)
57
+ if File.exist?(pidfile)
58
58
  File.read(pidfile).to_i
59
59
  else
60
60
  nil
@@ -89,7 +89,7 @@ module Racecar
89
89
  end
90
90
 
91
91
  at_exit do
92
- File.delete(pidfile) if File.exists?(pidfile)
92
+ File.delete(pidfile) if File.exist?(pidfile)
93
93
  end
94
94
  rescue Errno::EEXIST
95
95
  check_pid
@@ -211,6 +211,10 @@ module Racecar
211
211
  topic: topic,
212
212
  }
213
213
 
214
+ if event.payload.key?(:exception)
215
+ increment("producer.produce.errors", tags: tags)
216
+ end
217
+
214
218
  # This gets us the write rate.
215
219
  increment("producer.produce.messages", tags: tags.merge(topic: topic))
216
220
 
@@ -244,7 +248,15 @@ module Racecar
244
248
  # Number of messages ACK'd for the topic.
245
249
  increment("producer.ack.messages", tags: tags)
246
250
  end
247
-
251
+
252
+ def produce_delivery_error(event)
253
+ tags = {
254
+ client: event.payload.fetch(:client_id),
255
+ }
256
+
257
+ increment("producer.produce.delivery.errors", tags: tags)
258
+ end
259
+
248
260
  def produce_async(event)
249
261
  client = event.payload.fetch(:client_id)
250
262
  topic = event.payload.fetch(:topic)
@@ -256,6 +268,10 @@ module Racecar
256
268
  topic: topic,
257
269
  }
258
270
 
271
+ if event.payload.key?(:exception)
272
+ increment("producer.produce.errors", tags: tags)
273
+ end
274
+
259
275
  # This gets us the write rate.
260
276
  increment("producer.produce.messages", tags: tags.merge(topic: topic))
261
277
 
@@ -279,6 +295,11 @@ module Racecar
279
295
  topic: topic,
280
296
  }
281
297
 
298
+ if event.payload.key?(:exception)
299
+ increment("producer.produce.errors", tags: tags)
300
+ end
301
+
302
+
282
303
  # This gets us the write rate.
283
304
  increment("producer.produce.messages", tags: tags.merge(topic: topic))
284
305
 
@@ -0,0 +1,27 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Racecar
4
+ class DeliveryCallback
5
+ attr_reader :instrumenter
6
+
7
+ def initialize(instrumenter:)
8
+ @instrumenter = instrumenter
9
+ end
10
+
11
+ def call(delivery_report)
12
+ if delivery_report.error.to_i.zero?
13
+ payload = {
14
+ offset: delivery_report.offset,
15
+ partition: delivery_report.partition
16
+ }
17
+ instrumenter.instrument("acknowledged_message", payload)
18
+ else
19
+ payload = {
20
+ partition: delivery_report.partition,
21
+ exception: delivery_report.error
22
+ }
23
+ instrumenter.instrument("produce_delivery_error", payload)
24
+ end
25
+ end
26
+ end
27
+ end
@@ -37,6 +37,10 @@ module Racecar
37
37
  wait_for_exit
38
38
  end
39
39
 
40
+ def stop
41
+ terminate_workers
42
+ end
43
+
40
44
  private
41
45
 
42
46
  attr_accessor :workers
@@ -1,6 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require "racecar/message_delivery_error"
4
+ require "racecar/delivery_callback"
4
5
 
5
6
  at_exit do
6
7
  Racecar::Producer.shutdown!
@@ -42,19 +43,24 @@ module Racecar
42
43
  }
43
44
  producer_config["compression.codec"] = config.producer_compression_codec.to_s unless config.producer_compression_codec.nil?
44
45
  producer_config.merge!(config.rdkafka_producer)
45
- Rdkafka::Config.new(producer_config).producer
46
+ Rdkafka::Config.new(producer_config).producer.tap do |producer|
47
+ producer.delivery_callback = DeliveryCallback.new(instrumenter: @instrumenter)
48
+ end
46
49
  end
47
50
  end
48
51
  end
49
52
 
50
-
51
- # fire and forget - you won't get any guarantees or feedback from
53
+ # fire and forget - you won't get any guarantees or feedback from
52
54
  # Racecar on the status of the message and it won't halt execution
53
55
  # of the rest of your code.
54
56
  def produce_async(value:, topic:, **options)
55
57
  with_instrumentation(action: "produce_async", value: value, topic: topic, **options) do
56
- handle = internal_producer.produce(payload: value, topic: topic, **options)
57
- @delivery_handles << handle if @batching
58
+ begin
59
+ handle = internal_producer.produce(payload: value, topic: topic, **options)
60
+ @delivery_handles << handle if @batching
61
+ rescue Rdkafka::RdkafkaError => e
62
+ raise MessageDeliveryError.new(e, handle)
63
+ end
58
64
  end
59
65
 
60
66
  nil
@@ -63,8 +69,12 @@ module Racecar
63
69
  # synchronous message production - will wait until the delivery handle succeeds, fails or times out.
64
70
  def produce_sync(value:, topic:, **options)
65
71
  with_instrumentation(action: "produce_sync", value: value, topic: topic, **options) do
66
- handle = internal_producer.produce(payload: value, topic: topic, **options)
67
- deliver_with_error_handling(handle)
72
+ begin
73
+ handle = internal_producer.produce(payload: value, topic: topic, **options)
74
+ deliver_with_error_handling(handle)
75
+ rescue Rdkafka::RdkafkaError => e
76
+ raise MessageDeliveryError.new(e, handle)
77
+ end
68
78
  end
69
79
 
70
80
  nil
@@ -126,4 +136,4 @@ module Racecar
126
136
  end
127
137
  end
128
138
  end
129
- end
139
+ end
@@ -0,0 +1,58 @@
1
+ module Racecar
2
+ class RebalanceListener
3
+ def initialize(consumer_class, instrumenter)
4
+ @consumer_class = consumer_class
5
+ @instrumenter = instrumenter
6
+ @rdkafka_consumer = nil
7
+ end
8
+
9
+ attr_writer :rdkafka_consumer
10
+
11
+ attr_reader :consumer_class, :instrumenter, :rdkafka_consumer
12
+ private :consumer_class, :instrumenter, :rdkafka_consumer
13
+
14
+ def on_partitions_assigned(rdkafka_topic_partition_list)
15
+ event = Event.new(rdkafka_consumer: rdkafka_consumer, rdkafka_topic_partition_list: rdkafka_topic_partition_list)
16
+
17
+ instrument("partitions_assigned", partitions: event.partition_numbers) do
18
+ consumer_class.on_partitions_assigned(event)
19
+ end
20
+ end
21
+
22
+ def on_partitions_revoked(rdkafka_topic_partition_list)
23
+ event = Event.new(rdkafka_consumer: rdkafka_consumer, rdkafka_topic_partition_list: rdkafka_topic_partition_list)
24
+
25
+ instrument("partitions_revoked", partitions: event.partition_numbers) do
26
+ consumer_class.on_partitions_revoked(event)
27
+ end
28
+ end
29
+
30
+ private
31
+
32
+ def instrument(event, payload, &block)
33
+ instrumenter.instrument(event, payload, &block)
34
+ end
35
+
36
+ class Event
37
+ def initialize(rdkafka_topic_partition_list:, rdkafka_consumer:)
38
+ @__rdkafka_topic_partition_list = rdkafka_topic_partition_list
39
+ @__rdkafka_consumer = rdkafka_consumer
40
+ end
41
+
42
+ def topic_name
43
+ __rdkafka_topic_partition_list.to_h.keys.first
44
+ end
45
+
46
+ def partition_numbers
47
+ __rdkafka_topic_partition_list.to_h.values.flatten.map(&:partition)
48
+ end
49
+
50
+ def empty?
51
+ __rdkafka_topic_partition_list.empty?
52
+ end
53
+
54
+ # API private and not guaranteed stable
55
+ attr_reader :__rdkafka_topic_partition_list, :__rdkafka_consumer
56
+ end
57
+ end
58
+ end
@@ -5,6 +5,7 @@ require "racecar/pause"
5
5
  require "racecar/message"
6
6
  require "racecar/message_delivery_error"
7
7
  require "racecar/erroneous_state_error"
8
+ require "racecar/delivery_callback"
8
9
 
9
10
  module Racecar
10
11
  class Runner
@@ -95,7 +96,7 @@ module Racecar
95
96
  end
96
97
  ensure
97
98
  producer.close
98
- Racecar::Datadog.close if Object.const_defined?("Racecar::Datadog")
99
+ Racecar::Datadog.close if config.datadog_enabled
99
100
  @instrumenter.instrument("shut_down", instrumentation_payload || {})
100
101
  end
101
102
 
@@ -130,18 +131,13 @@ module Racecar
130
131
 
131
132
  def consumer
132
133
  @consumer ||= begin
133
- # Manually store offset after messages have been processed successfully
134
- # to avoid marking failed messages as committed. The call just updates
135
- # a value within librdkafka and is asynchronously written to proper
136
- # storage through auto commits.
137
- config.consumer << "enable.auto.offset.store=false"
138
134
  ConsumerSet.new(config, logger, @instrumenter)
139
135
  end
140
136
  end
141
137
 
142
138
  def producer
143
139
  @producer ||= Rdkafka::Config.new(producer_config).producer.tap do |producer|
144
- producer.delivery_callback = delivery_callback
140
+ producer.delivery_callback = Racecar::DeliveryCallback.new(instrumenter: @instrumenter)
145
141
  end
146
142
  end
147
143
 
@@ -160,16 +156,6 @@ module Racecar
160
156
  producer_config
161
157
  end
162
158
 
163
- def delivery_callback
164
- ->(delivery_report) do
165
- payload = {
166
- offset: delivery_report.offset,
167
- partition: delivery_report.partition
168
- }
169
- @instrumenter.instrument("acknowledged_message", payload)
170
- end
171
- end
172
-
173
159
  def install_signal_handlers
174
160
  # Stop the consumer on SIGINT, SIGQUIT or SIGTERM.
175
161
  trap("QUIT") { stop }
@@ -222,21 +208,21 @@ module Racecar
222
208
  }
223
209
 
224
210
  @instrumenter.instrument("start_process_batch", instrumentation_payload)
225
- @instrumenter.instrument("process_batch", instrumentation_payload) do
226
- with_pause(first.topic, first.partition, first.offset..last.offset) do |pause|
227
- begin
211
+ with_pause(first.topic, first.partition, first.offset..last.offset) do |pause|
212
+ begin
213
+ @instrumenter.instrument("process_batch", instrumentation_payload) do
228
214
  racecar_messages = messages.map do |message|
229
215
  Racecar::Message.new(message, retries_count: pause.pauses_count)
230
216
  end
231
217
  processor.process_batch(racecar_messages)
232
218
  processor.deliver!
233
219
  consumer.store_offset(messages.last)
234
- rescue => e
235
- instrumentation_payload[:unrecoverable_delivery_error] = reset_producer_on_unrecoverable_delivery_errors(e)
236
- instrumentation_payload[:retries_count] = pause.pauses_count
237
- config.error_handler.call(e, instrumentation_payload)
238
- raise e
239
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
240
226
  end
241
227
  end
242
228
  end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Racecar
4
- VERSION = "2.9.0.beta1"
4
+ VERSION = "2.10.0.beta1"
5
5
  end
data/lib/racecar.rb CHANGED
@@ -66,12 +66,16 @@ module Racecar
66
66
  end
67
67
 
68
68
  def self.run(processor)
69
+ runner(processor).run
70
+ end
71
+
72
+ def self.runner(processor)
69
73
  runner = Runner.new(processor, config: config, logger: logger, instrumenter: config.instrumenter)
70
74
 
71
75
  if config.parallel_workers && config.parallel_workers > 1
72
- ParallelRunner.new(runner: runner, config: config, logger: logger).run
76
+ ParallelRunner.new(runner: runner, config: config, logger: logger)
73
77
  else
74
- runner.run
78
+ runner
75
79
  end
76
80
  end
77
81
  end
data/racecar.gemspec CHANGED
@@ -23,13 +23,13 @@ Gem::Specification.new do |spec|
23
23
  spec.required_ruby_version = '>= 2.6'
24
24
 
25
25
  spec.add_runtime_dependency "king_konf", "~> 1.0.0"
26
- spec.add_runtime_dependency "rdkafka", "~> 0.12.0"
26
+ spec.add_runtime_dependency "rdkafka", "~> 0.13.0"
27
27
 
28
28
  spec.add_development_dependency "bundler", [">= 1.13", "< 3"]
29
- spec.add_development_dependency "pry"
29
+ spec.add_development_dependency "pry-byebug"
30
30
  spec.add_development_dependency "rake", "> 10.0"
31
31
  spec.add_development_dependency "rspec", "~> 3.0"
32
32
  spec.add_development_dependency "timecop"
33
33
  spec.add_development_dependency "dogstatsd-ruby", ">= 4.0.0", "< 6.0.0"
34
- spec.add_development_dependency "activesupport", ">= 4.0", "< 6.1"
34
+ spec.add_development_dependency "activesupport"
35
35
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: racecar
3
3
  version: !ruby/object:Gem::Version
4
- version: 2.9.0.beta1
4
+ version: 2.10.0.beta1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Daniel Schierbeck
@@ -9,7 +9,7 @@ authors:
9
9
  autorequire:
10
10
  bindir: exe
11
11
  cert_chain: []
12
- date: 2023-03-22 00:00:00.000000000 Z
12
+ date: 2023-10-09 00:00:00.000000000 Z
13
13
  dependencies:
14
14
  - !ruby/object:Gem::Dependency
15
15
  name: king_konf
@@ -31,14 +31,14 @@ dependencies:
31
31
  requirements:
32
32
  - - "~>"
33
33
  - !ruby/object:Gem::Version
34
- version: 0.12.0
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.12.0
41
+ version: 0.13.0
42
42
  - !ruby/object:Gem::Dependency
43
43
  name: bundler
44
44
  requirement: !ruby/object:Gem::Requirement
@@ -60,7 +60,7 @@ dependencies:
60
60
  - !ruby/object:Gem::Version
61
61
  version: '3'
62
62
  - !ruby/object:Gem::Dependency
63
- name: pry
63
+ name: pry-byebug
64
64
  requirement: !ruby/object:Gem::Requirement
65
65
  requirements:
66
66
  - - ">="
@@ -141,20 +141,14 @@ dependencies:
141
141
  requirements:
142
142
  - - ">="
143
143
  - !ruby/object:Gem::Version
144
- version: '4.0'
145
- - - "<"
146
- - !ruby/object:Gem::Version
147
- version: '6.1'
144
+ version: '0'
148
145
  type: :development
149
146
  prerelease: false
150
147
  version_requirements: !ruby/object:Gem::Requirement
151
148
  requirements:
152
149
  - - ">="
153
150
  - !ruby/object:Gem::Version
154
- version: '4.0'
155
- - - "<"
156
- - !ruby/object:Gem::Version
157
- version: '6.1'
151
+ version: '0'
158
152
  description:
159
153
  email:
160
154
  - dschierbeck@zendesk.com
@@ -200,6 +194,7 @@ files:
200
194
  - lib/racecar/ctl.rb
201
195
  - lib/racecar/daemon.rb
202
196
  - lib/racecar/datadog.rb
197
+ - lib/racecar/delivery_callback.rb
203
198
  - lib/racecar/erroneous_state_error.rb
204
199
  - lib/racecar/heroku.rb
205
200
  - lib/racecar/instrumenter.rb
@@ -211,6 +206,7 @@ files:
211
206
  - lib/racecar/pause.rb
212
207
  - lib/racecar/producer.rb
213
208
  - lib/racecar/rails_config_file_loader.rb
209
+ - lib/racecar/rebalance_listener.rb
214
210
  - lib/racecar/runner.rb
215
211
  - lib/racecar/version.rb
216
212
  - racecar.gemspec