rails_autoscale_agent 0.9.0.beta.4 → 0.10.2

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: 70253908c027308ec4d19c9b41eeed033759e6669eace3301925a65a64748447
4
- data.tar.gz: 199e75a27f4708920b47964cbc75e2b705ddce88508c35fd614512bb9bfcfd5e
3
+ metadata.gz: fd7549c359dd5f45dbd5d44113c04413685c0cabc12836c1a88559404104ea8b
4
+ data.tar.gz: b193f06500d2d281e22b2684364e2aad7107dcd1713f8e30d98f2cc3a814a99f
5
5
  SHA512:
6
- metadata.gz: af5043f8402b40c15b5efe0be205e0e4c6107b8d05ea7126ccfa445c28679567c7e0f15e2a01eb2b7e107c95d3802543c48f428c31f70be823953173cd903ae1
7
- data.tar.gz: 1fcfa15d0c76a8f1abf38653890d6b2157fd2e4edfc2ac072eb198897ab09029ea3f1c83ee11620e13a1a07a379695d7913c5bed89ce51e1907451367e61b1df
6
+ metadata.gz: 0ecc593e4d6a404bbeb0d082047c87511e5be55daa77169a4159f0a96c8bc1bd7c5b78e09e712ec90119e0198c615849331de9b8ff1fd48e3f2ecf631e8395a5
7
+ data.tar.gz: b4802270f5a2ac21f5c8fee31c200dcd2fee5b8771591a3b9c9eb21b1a716ec59e56e24e958afbbc8ea5aa49670e0bf62dc64ad8d3670f8cb4ff7126afaf6f4f
@@ -6,7 +6,8 @@
6
6
  {
7
7
  "label": "test: all",
8
8
  "group": "test",
9
- "command": "rspec",
9
+ "command": "bundle",
10
+ "args": ["exec", "rspec"],
10
11
  "runOptions": {
11
12
  "reevaluateOnRerun": false
12
13
  },
@@ -31,8 +32,8 @@
31
32
  {
32
33
  "label": "test: file",
33
34
  "group": "test",
34
- "command": "rspec",
35
- "args": ["${relativeFile}"],
35
+ "command": "bundle",
36
+ "args": ["exec", "rspec", "${relativeFile}"],
36
37
  "runOptions": {
37
38
  "reevaluateOnRerun": false
38
39
  },
@@ -57,8 +58,8 @@
57
58
  {
58
59
  "label": "test: line",
59
60
  "group": "test",
60
- "command": "rspec",
61
- "args": ["${relativeFile}:${lineNumber}"],
61
+ "command": "bundle",
62
+ "args": ["exec", "rspec", "${relativeFile}:${lineNumber}"],
62
63
  "runOptions": {
63
64
  "reevaluateOnRerun": false
64
65
  },
@@ -0,0 +1,115 @@
1
+ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
2
+ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
3
+
4
+ ## [Unreleased](https://github.com/adamlogic/rails_autoscale_agent/compare/v0.10.2...master)
5
+
6
+ No currently unreleased changes.
7
+
8
+ ## [0.10.2](https://github.com/adamlogic/rails_autoscale_agent/compare/v0.10.1...v0.10.2) - 2021-01-12
9
+
10
+ ### Changed
11
+
12
+ - Loosen Ruby constraint to allow Ruby 3. ([#36](https://github.com/adamlogic/rails_autoscale_agent/pull/36))
13
+
14
+ ## [0.10.1](https://github.com/adamlogic/rails_autoscale_agent/compare/v0.9.1...v0.10.1) - 2021-01-03
15
+
16
+ ### Added
17
+
18
+ - Add support for [long-running jobs](https://railsautoscale.com/docs/long-running-jobs/) in Sidekiq and Delayed Job.
19
+ - Handle x-request-start measured in seconds (instead of milliseconds) to support nginx buildpack ([cd092f3](https://github.com/adamlogic/rails_autoscale_agent/commit/cd092f38718abf5ffaea866bcae7831d4c910ffd))
20
+ - Override worker adapter config via env var ([75dd06b](https://github.com/adamlogic/rails_autoscale_agent/commit/75dd06b2a7ff4eeab829eec24d503dc067c8fe32))
21
+
22
+ ### Changed
23
+
24
+ - Require Ruby 2.5 or newer. ([b033050](https://github.com/adamlogic/rails_autoscale_agent/commit/b033050b7f9d4d7f1e50dbd780cf0e1822249268))
25
+ - Only report worker metrics from web.1 to avoid redundant data. ([d5d5fa8](https://github.com/adamlogic/rails_autoscale_agent/commit/d5d5fa87fb4d7d046832a64edde9ed0c3a6ec75f))
26
+ - Don't collect worker metrics for an unreasonable number of queues. ([a9358af](https://github.com/adamlogic/rails_autoscale_agent/commit/a9358af74a29a941d1f1d60a0222077dafd5ce08))
27
+
28
+ ### Fixed
29
+
30
+ - Avoid holding onto database connections (DJ & Que only). ([3919ca5](https://github.com/adamlogic/rails_autoscale_agent/commit/3919ca54420cafa82abf9f8cd251569f9637482b))
31
+ - Better error handling for worker adapters. ([190786e](https://github.com/adamlogic/rails_autoscale_agent/commit/190786e4a910d41e394a3129aac1d23b594dbd9b))
32
+ - Don't collect metrics of the reporter isn't running. Avoids memory bloat. ([247c322](https://github.com/adamlogic/rails_autoscale_agent/commit/247c322cffc625a8c6b2395080a048ffb94e7f3b))
33
+
34
+ ## [0.10.0](https://github.com/adamlogic/rails_autoscale_agent/compare/v0.9.1...v0.10.0) - 2021-01-03 [YANKED]
35
+
36
+ _I released the wrong branch 🤦‍♂️_
37
+
38
+ ## [0.9.1](https://github.com/adamlogic/rails_autoscale_agent/compare/v0.9.0...v0.9.1) - 2020-07-29
39
+
40
+ ### Fixed
41
+
42
+ - Fix a bug in error handling. ([3018542](https://github.com/adamlogic/rails_autoscale_agent/commit/3018542cd046fc4e1bd6e7da86e72a6aa2d50a8f))
43
+ - Remove unintentional Rails dependency.
44
+
45
+ ## [0.9.0](https://github.com/adamlogic/rails_autoscale_agent/compare/v0.8.3...v0.9.0) - 2020-07-12
46
+
47
+ ### Added
48
+
49
+ - Add support for Resque workers.
50
+ - Add dev mode for working on the agent gem itself. ([47e3fca](https://github.com/adamlogic/rails_autoscale_agent/commit/47e3fca5b788f48567a345d9cab3a26b9cd87693))
51
+ - Report agent exceptions to Rails Autoscale.
52
+
53
+ ### Changed
54
+
55
+ - Adjust queue time metric to exclude time waiting for large request bodies. ([#25](https://github.com/adamlogic/rails_autoscale_agent/pull/25))
56
+ - Que and DJ jobs without a queue name will be included in the "default" queue metrics.
57
+
58
+ ### Fixed
59
+
60
+ - Multiple fixes to the Delayed Job SQL query.
61
+
62
+ ## [0.8.3](https://github.com/adamlogic/rails_autoscale_agent/compare/v0.8.2...v0.8.3) - 2020-05-26
63
+
64
+ ### Fixed
65
+
66
+ - Ignored failed job in Delayed Job adapter. ([fa72fc2](https://github.com/adamlogic/rails_autoscale_agent/compare/v0.8.2...v0.8.3))
67
+
68
+ ## [0.8.2](https://github.com/adamlogic/rails_autoscale_agent/compare/v0.8.1...v0.8.2) - 2020-05-22
69
+
70
+ ### Fixed
71
+
72
+ - Ignore worker metrics from unnamed queues (DJ & Que only). These metrics were being lumped with web metrics. ([#21](https://github.com/adamlogic/rails_autoscale_agent/pull/21))
73
+
74
+ ## [0.8.1](https://github.com/adamlogic/rails_autoscale_agent/compare/v0.8.0...v0.8.1) - 2020-05-04
75
+
76
+ ### Fixed
77
+
78
+ - Ignore failed jobs in Que adapter. ([#18](https://github.com/adamlogic/rails_autoscale_agent/pull/18))
79
+
80
+ ## [0.8.0](https://github.com/adamlogic/rails_autoscale_agent/compare/v0.7.0...v0.8.0) - 2020-03-21
81
+
82
+ ### Added
83
+
84
+ - Add support for Delayed Job ([#14](https://github.com/adamlogic/rails_autoscale_agent/pull/14))
85
+ - Add support for Que ([#15](https://github.com/adamlogic/rails_autoscale_agent/pull/15))
86
+
87
+ ## [0.7.0](https://github.com/adamlogic/rails_autoscale_agent/compare/v0.6.3...v0.7.0) - 2019-12-04
88
+
89
+ ### Added
90
+
91
+ - Make worker adapters configurable. ([012d937](https://github.com/adamlogic/rails_autoscale_agent/commit/012d9379296763f5e42df95f05b066fe82ab0051))
92
+
93
+ ## [0.6.3](https://github.com/adamlogic/rails_autoscale_agent/compare/v0.6.2...v0.6.3) - 2019-06-25
94
+
95
+ ### Fixed
96
+
97
+ - Fix issues with logging.
98
+
99
+ ## [0.6.2](https://github.com/adamlogic/rails_autoscale_agent/compare/v0.6.1...v0.6.2) - 2019-06-22
100
+
101
+ ### Fixed
102
+
103
+ - Fix issues with logging.
104
+
105
+ ## [0.6.1](https://github.com/adamlogic/rails_autoscale_agent/compare/v0.6.0...v0.6.1) - 2019-05-06
106
+
107
+ ### Fixed
108
+
109
+ - Don't assume Sidekiq is present.
110
+
111
+ ## [0.6.0](https://github.com/adamlogic/rails_autoscale_agent/compare/v0.4.1...v0.6.0) - 2019-05-03
112
+
113
+ ### Added
114
+
115
+ - Add support for autoscaling Sidekiq.
data/Gemfile CHANGED
@@ -1,4 +1,16 @@
1
1
  source 'https://rubygems.org'
2
2
 
3
- # Specify your gem's dependencies in rails_autoscale_agent.gemspec
4
3
  gemspec
4
+
5
+ gem "rake", ">= 12.3.3"
6
+ gem "rspec", ">= 3.0"
7
+ gem "vcr", ">= 3.0"
8
+ gem "webmock"
9
+ gem "pry-byebug"
10
+ gem "sidekiq", ">= 5.0"
11
+ gem "delayed_job"
12
+ gem "delayed_job_active_record"
13
+ gem "que"
14
+ gem "resque"
15
+ gem "activesupport"
16
+ gem "sqlite3", platforms: :ruby
data/README.md CHANGED
@@ -6,7 +6,8 @@ This gem works together with the [Rails Autoscale](https://railsautoscale.com) H
6
6
 
7
7
  ## Requirements
8
8
 
9
- Tested with Rails versions 3.2 and higher and Ruby versions 1.9.3 and higher.
9
+ - Rack-based app
10
+ - Ruby 2.5 or newer
10
11
 
11
12
  ## Getting Started
12
13
 
@@ -22,17 +23,7 @@ The agent will only communicate with Rails Autoscale if a `RAILS_AUTOSCALE_URL`
22
23
 
23
24
  ## Non-Rails Rack apps
24
25
 
25
- You'll need to insert the `RailsAutoscaleAgent::Middleware` manually. Insert it before `Rack::Runtime` to ensure accuracy of request queue timings.
26
-
27
- ## Changing the logger
28
-
29
- The Rails logger is used by default.
30
- If you wish to use a different logger you can set it on the configuration object:
31
-
32
- ```ruby
33
- # config/initializers/rails_autoscale_agent.rb
34
- RailsAutoscaleAgent::Config.instance.logger = MyLogger.new
35
- ```
26
+ You'll need to `require 'rails_autoscale_agent/middleware'` and insert the `RailsAutoscaleAgent::Middleware` manually. Insert it before `Rack::Runtime` to ensure accuracy of request queue timings.
36
27
 
37
28
  ## What data is collected?
38
29
 
@@ -47,6 +38,32 @@ The middleware agent runs in its own thread so your web requests are not impacte
47
38
 
48
39
  Rails Autoscale aggregates and stores this information to power the autoscaling algorithm and dashboard visualizations.
49
40
 
41
+ ## Configuration
42
+
43
+ Most Rails Autoscale configurations are handled via the settings page on your Rails Autoscale dashboard, but there a few ways you can directly change the behavior of the agent via environment variables:
44
+
45
+ - `RAILS_AUTOSCALE_DEBUG` - Enables debug logging. See more in the [logging](#logging) section below.
46
+ - `RAILS_AUTOSCALE_WORKER_ADAPTER` - Overrides the available worker adapters. See more in the [worker adapters](#worker_adapters) section below.
47
+ - `RAILS_AUTOSCALE_LONG_JOBS` - Enables reporting for active workers. See [Handling Long-Running Background Jobs](https://railsautoscale.com/docs/long-running-jobs/) in the Rails Autoscale docs for more.
48
+
49
+ ## Worker adapters
50
+
51
+ Rails Autoscale supports autoscaling worker dynos. Out of the box, four job backends are supported: Sidekiq, Resque, Delayed Job, and Que. The agent will automatically enable the appropriate worker adapter based on what you have installed in your app.
52
+
53
+ In some scenarios you might want to override this behavior. Let's say you have both Sidekiq and Resque installed 🤷‍♂️, but you only want Rails Autoscale to collect metrics for Sidekiq. Here's how you'd override that:
54
+
55
+ ```
56
+ heroku config:add RAILS_AUTOSCALE_WORKER_ADAPTER=sidekiq
57
+ ```
58
+
59
+ You can also disable collection of worker metrics altogether:
60
+
61
+ ```
62
+ heroku config:add RAILS_AUTOSCALE_WORKER_ADAPTER=""
63
+ ```
64
+
65
+ It's also possible to write a custom worker adapter. See [these docs](https://railsautoscale.com/docs/custom-worker-adapter/) for details.
66
+
50
67
  ## Troubleshooting
51
68
 
52
69
  Once installed, you should see something like this in your development log:
@@ -59,17 +76,39 @@ In production, run `heroku logs -t | grep RailsAutoscale`, and you should see so
59
76
 
60
77
  If you don't see either of these, try running `bundle` again and restarting your Rails application.
61
78
 
62
- You can see more detailed (debug) logging by setting the `RAILS_AUTOSCALE_DEBUG` env var on your Heroku app:
79
+ You can see more detailed (debug) logging by setting `RAILS_AUTOSCALE_DEBUG` on your Heroku app:
63
80
 
64
81
  ```
65
82
  heroku config:add RAILS_AUTOSCALE_DEBUG=true
66
83
  ```
67
84
 
68
- Debug logs are silenced by default because Rails apps default to a DEBUG log level in production,
69
- and these can get very noisy with this gem.
85
+ See more in the [logging](#logging) section below.
70
86
 
71
87
  Reach out to help@railsautoscale.com if you run into any other problems.
72
88
 
89
+ ## Logging
90
+
91
+ The Rails logger is used by default.
92
+ If you wish to use a different logger you can set it on the configuration object:
93
+
94
+ ```ruby
95
+ # config/initializers/rails_autoscale_agent.rb
96
+ RailsAutoscaleAgent::Config.instance.logger = MyLogger.new
97
+ ```
98
+
99
+ Debug logs are silenced by default because Rails apps default to a DEBUG log level in production, and this gem has _very_ chatty debug logs. If you want to see the debug logs, set `RAILS_AUTOSCALE_DEBUG` on your Heroku app:
100
+
101
+ ```
102
+ heroku config:add RAILS_AUTOSCALE_DEBUG=true
103
+ ```
104
+
105
+ If you find the gem too chatty even without this, you can quiet it down further:
106
+
107
+ ```ruby
108
+ # config/initializers/rails_autoscale_agent.rb
109
+ RailsAutoscaleAgent::Config.instance.quiet = true
110
+ ```
111
+
73
112
  ## Development
74
113
 
75
114
  After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake spec` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment.
@@ -59,7 +59,7 @@ module RailsAutoscaleAgent
59
59
 
60
60
  case response.code.to_i
61
61
  when 200...300 then SuccessResponse.new(response.body)
62
- else FailureResponse.new(response.message)
62
+ else FailureResponse.new([response.code, response.message].join(' - '))
63
63
  end
64
64
  end
65
65
 
@@ -4,36 +4,34 @@ require 'singleton'
4
4
 
5
5
  module RailsAutoscaleAgent
6
6
  class Config
7
+ DEFAULT_WORKER_ADAPTERS = 'sidekiq,delayed_job,que,resque'
8
+
7
9
  include Singleton
8
10
 
9
11
  attr_accessor :report_interval, :logger, :api_base_url, :max_request_size,
10
- :dyno, :pid, :addon_name, :worker_adapters, :dev_mode
12
+ :dyno, :addon_name, :worker_adapters, :dev_mode, :debug, :quiet,
13
+ :track_long_running_jobs,
14
+
15
+ # legacy configs, no longer used
16
+ :sidekiq_latency_for_active_jobs, :latency_for_active_jobs
11
17
 
12
18
  def initialize
13
- require 'rails_autoscale_agent/worker_adapters/sidekiq'
14
- require 'rails_autoscale_agent/worker_adapters/delayed_job'
15
- require 'rails_autoscale_agent/worker_adapters/que'
16
- require 'rails_autoscale_agent/worker_adapters/resque'
17
- @worker_adapters = [
18
- WorkerAdapters::Sidekiq.instance,
19
- WorkerAdapters::DelayedJob.instance,
20
- WorkerAdapters::Que.instance,
21
- WorkerAdapters::Resque.instance,
22
- ]
19
+ @worker_adapters = prepare_worker_adapters
23
20
 
24
21
  # Allow the add-on name to be configured - needed for testing
25
22
  @addon_name = ENV['RAILS_AUTOSCALE_ADDON'] || 'RAILS_AUTOSCALE'
26
23
  @api_base_url = ENV["#{@addon_name}_URL"]
27
24
  @dev_mode = ENV['RAILS_AUTOSCALE_DEV'] == 'true'
28
- @pid = Process.pid
25
+ @debug = dev_mode? || ENV['RAILS_AUTOSCALE_DEBUG'] == 'true'
26
+ @track_long_running_jobs = ENV['RAILS_AUTOSCALE_LONG_JOBS'] == 'true'
29
27
  @max_request_size = 100_000 # ignore request payloads over 100k since they skew the queue times
30
28
  @report_interval = 10 # this default will be overwritten during Reporter#register!
31
29
  @logger ||= defined?(Rails) ? Rails.logger : ::Logger.new(STDOUT)
32
- @dyno = @dev_mode ? 'dev.1' : ENV['DYNO']
30
+ @dyno = dev_mode? ? 'dev.1' : ENV['DYNO']
33
31
  end
34
32
 
35
33
  def to_s
36
- "#{@dyno}##{@pid}"
34
+ "#{@dyno}##{Process.pid}"
37
35
  end
38
36
 
39
37
  def ignore_large_requests?
@@ -41,5 +39,18 @@ module RailsAutoscaleAgent
41
39
  end
42
40
 
43
41
  alias_method :dev_mode?, :dev_mode
42
+ alias_method :debug?, :debug
43
+ alias_method :quiet?, :quiet
44
+
45
+ private
46
+
47
+ def prepare_worker_adapters
48
+ adapter_names = (ENV['RAILS_AUTOSCALE_WORKER_ADAPTER'] || DEFAULT_WORKER_ADAPTERS).split(',')
49
+ adapter_names.map do |adapter_name|
50
+ require "rails_autoscale_agent/worker_adapters/#{adapter_name}"
51
+ adapter_constant_name = adapter_name.capitalize.gsub(/(?:_)(.)/i) { $1.upcase }
52
+ WorkerAdapters.const_get(adapter_constant_name).instance
53
+ end
54
+ end
44
55
  end
45
56
  end
@@ -13,26 +13,33 @@ module RailsAutoscaleAgent
13
13
  class LoggerProxy < Struct.new(:logger)
14
14
  TAG = '[RailsAutoscale]'
15
15
 
16
- %w[info warn error].each do |name|
17
- define_method name do |msg|
18
- logger.send name, tag(msg)
19
- end
16
+ def error(msg)
17
+ logger.error tag(msg)
18
+ end
19
+
20
+ def warn(msg)
21
+ logger.warn tag(msg)
22
+ end
23
+
24
+ def info(msg)
25
+ logger.info tag(msg) unless Config.instance.quiet?
20
26
  end
21
27
 
22
28
  def debug(msg)
23
29
  # Silence debug logs by default to avoiding being overly chatty (Rails logger defaults
24
- # to DEBUG level in production).
25
- # This uses a separate logger so that RAILS_AUTOSCALE_DEBUG
26
- # shows debug logs regardless of Rails log level.
27
- debug_logger.debug tag(msg) if ENV['RAILS_AUTOSCALE_DEBUG'] == 'true' || Config.instance.dev_mode?
30
+ # to DEBUG level in production). Setting RAILS_AUTOSCALE_DEBUG=true enables debug logs,
31
+ # even if the underlying logger severity level is INFO.
32
+ if Config.instance.debug?
33
+ if logger.respond_to?(:debug?) && logger.debug?
34
+ logger.debug tag(msg)
35
+ elsif logger.respond_to?(:info?) && logger.info?
36
+ logger.info tag("[DEBUG] #{msg}")
37
+ end
38
+ end
28
39
  end
29
40
 
30
41
  private
31
42
 
32
- def debug_logger
33
- @debug_loggers ||= ::Logger.new(STDOUT)
34
- end
35
-
36
43
  def tag(msg)
37
44
  "#{TAG} #{msg}"
38
45
  end
@@ -8,7 +8,7 @@ module RailsAutoscaleAgent
8
8
  def to_params
9
9
  {
10
10
  dyno: config.dyno,
11
- pid: config.pid,
11
+ pid: Process.pid,
12
12
  ruby_version: RUBY_VERSION,
13
13
  rails_version: defined?(Rails) && Rails.version,
14
14
  gem_version: VERSION,
@@ -12,7 +12,7 @@ module RailsAutoscaleAgent
12
12
  def to_params(config)
13
13
  {
14
14
  dyno: config.dyno,
15
- pid: config.pid,
15
+ pid: Process.pid,
16
16
  }
17
17
  end
18
18
 
@@ -20,6 +20,7 @@ module RailsAutoscaleAgent
20
20
  def start!(config, store)
21
21
  @started = true
22
22
  @worker_adapters = config.worker_adapters.select(&:enabled?)
23
+ @dyno_num = config.dyno.to_s.split('.').last.to_i
23
24
 
24
25
  if !config.api_base_url && !config.dev_mode?
25
26
  logger.info "Reporter not started: #{config.addon_name}_URL is not set"
@@ -34,15 +35,14 @@ module RailsAutoscaleAgent
34
35
  multiplier = 1 - (rand / 4) # between 0.75 and 1.0
35
36
  sleep config.report_interval * multiplier
36
37
 
37
- begin
38
- @worker_adapters.map { |a| a.collect!(store) }
39
- report!(config, store)
40
- rescue => ex
41
- # Exceptions in threads other than the main thread will fail silently
42
- # https://ruby-doc.org/core-2.2.0/Thread.html#class-Thread-label-Exception+handling
43
- logger.error "Reporter error: #{ex.inspect}"
44
- AutoscaleApi.new(config.api_base_url).report_exception!(ex)
38
+ # It's redundant to report worker metrics from every web dyno, so only report from web.1
39
+ if @dyno_num == 1
40
+ @worker_adapters.map do |adapter|
41
+ report_exceptions(config) { adapter.collect!(store) }
42
+ end
45
43
  end
44
+
45
+ report_exceptions(config) { report!(config, store) }
46
46
  end
47
47
  end
48
48
  end
@@ -51,6 +51,8 @@ module RailsAutoscaleAgent
51
51
  @started
52
52
  end
53
53
 
54
+ private
55
+
54
56
  def report!(config, store)
55
57
  report = store.pop_report
56
58
 
@@ -86,5 +88,20 @@ module RailsAutoscaleAgent
86
88
  logger.error "Reporter failed to register: #{result.failure_message}"
87
89
  end
88
90
  end
91
+
92
+ def report_exceptions(config)
93
+ begin
94
+ yield
95
+ rescue => ex
96
+ # Exceptions in threads other than the main thread will fail silently
97
+ # https://ruby-doc.org/core-2.2.0/Thread.html#class-Thread-label-Exception+handling
98
+ logger.error "Reporter error: #{ex.inspect}"
99
+ AutoscaleApi.new(config).report_exception!(ex)
100
+ end
101
+ rescue => ex
102
+ # An exception was encountered while trying to report the original exception.
103
+ # Swallow the error so the reporter continues to report.
104
+ logger.error "Exception reporting error: #{ex.inspect}"
105
+ end
89
106
  end
90
107
  end
@@ -1,5 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require 'rails_autoscale_agent/logger'
4
+
3
5
  module RailsAutoscaleAgent
4
6
  class Request
5
7
  include Logger
@@ -9,32 +11,39 @@ module RailsAutoscaleAgent
9
11
  @id = env['HTTP_X_REQUEST_ID']
10
12
  @size = env['rack.input'].respond_to?(:size) ? env['rack.input'].size : 0
11
13
  @request_body_wait = env['puma.request_body_wait'].to_i
14
+ @request_start_header = env['HTTP_X_REQUEST_START']
15
+ end
16
+
17
+ def ignore?
18
+ @config.ignore_large_requests? && @size > @config.max_request_size
19
+ end
12
20
 
13
- @entered_queue_at = if unix_millis = env['HTTP_X_REQUEST_START']
14
- Time.at(unix_millis.to_f / 1000)
15
- elsif config.dev_mode?
21
+ def started_at
22
+ if @request_start_header
23
+ # Heroku sets the header as an integer, measured in milliseconds.
24
+ # If nginx is involved, it might be in seconds with fractional milliseconds,
25
+ # and it might be preceeded by "t=". We can all cases by removing non-digits
26
+ # and treating as milliseconds.
27
+ Time.at(@request_start_header.gsub(/\D/, '').to_i / 1000.0)
28
+ elsif @config.dev_mode?
16
29
  # In dev mode, fake a queue time of 0-1000ms
17
30
  Time.now - rand + @request_body_wait
18
31
  end
19
32
  end
20
33
 
21
- def ignore?
22
- @config.ignore_large_requests? && @size > @config.max_request_size
23
- end
34
+ def queue_time(now = Time.now)
35
+ return if started_at.nil?
24
36
 
25
- def queue_time
26
- if @entered_queue_at
27
- queue_time = ((Time.now - @entered_queue_at) * 1000).to_i
37
+ queue_time = ((now - started_at) * 1000).to_i
28
38
 
29
- # Subtract the time Puma spent waiting on the request body. It's irrelevant to capacity-related queue time.
30
- # Without this, slow clients and large request payloads will skew queue time.
31
- queue_time -= @request_body_wait
39
+ # Subtract the time Puma spent waiting on the request body. It's irrelevant to capacity-related queue time.
40
+ # Without this, slow clients and large request payloads will skew queue time.
41
+ queue_time -= @request_body_wait
32
42
 
33
- logger.debug "Request queue_time=#{queue_time}ms body_wait=#{@request_body_wait}ms request_id=#{@id} size=#{@size}"
43
+ logger.debug "Request queue_time=#{queue_time}ms body_wait=#{@request_body_wait}ms request_id=#{@id} size=#{@size}"
34
44
 
35
- # Safeguard against negative queue times (should not happen in practice)
36
- queue_time > 0 ? queue_time : 0
37
- end
45
+ # Safeguard against negative queue times (should not happen in practice)
46
+ queue_time > 0 ? queue_time : 0
38
47
  end
39
48
  end
40
49
  end
@@ -16,10 +16,15 @@ module RailsAutoscaleAgent
16
16
  end
17
17
 
18
18
  def push(value, time = Time.now, queue_name = nil, metric = nil)
19
+ # If it's been two minutes since clearing out the store, stop collecting measurements.
20
+ # There could be an issue with the reporter, and continuing to collect will consume linear memory.
21
+ return if @last_pop && @last_pop < Time.now - 120
22
+
19
23
  @measurements << Measurement.new(time, value, queue_name, metric)
20
24
  end
21
25
 
22
26
  def pop_report
27
+ @last_pop = Time.now
23
28
  report = Report.new
24
29
 
25
30
  while measurement = @measurements.shift
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module RailsAutoscaleAgent
4
- VERSION = "0.9.0.beta.4"
4
+ VERSION = "0.10.2"
5
5
  end
@@ -8,42 +8,67 @@ module RailsAutoscaleAgent
8
8
  include RailsAutoscaleAgent::Logger
9
9
  include Singleton
10
10
 
11
- UNNAMED_QUEUE = '[unnamed]'
12
-
13
- class << self
14
- attr_accessor :queues
15
- end
16
-
17
- def initialize
18
- # Track the known queues so we can continue reporting on queues that don't
19
- # currently have enqueued jobs.
20
- self.class.queues = Set.new
21
-
22
- install if enabled?
23
- end
11
+ attr_writer :queues
24
12
 
25
13
  def enabled?
26
- defined? ::Delayed
14
+ if defined?(::Delayed::Job) && defined?(::Delayed::Backend::ActiveRecord)
15
+ log_msg = String.new("DelayedJob enabled (#{::ActiveRecord::Base.default_timezone})")
16
+ log_msg << " with long-running job support" if track_long_running_jobs?
17
+ logger.info log_msg
18
+ true
19
+ end
27
20
  end
28
21
 
29
22
  def collect!(store)
30
23
  log_msg = String.new
31
24
  t = Time.now.utc
32
- db_time = t.strftime('%Y-%m-%d %H:%M:%S')
25
+ sql = <<~SQL
26
+ SELECT COALESCE(queue, 'default'), min(run_at)
27
+ FROM delayed_jobs
28
+ WHERE locked_at IS NULL
29
+ AND failed_at IS NULL
30
+ GROUP BY queue
31
+ SQL
32
+
33
+ run_at_by_queue = Hash[select_rows(sql)]
34
+
35
+ # Don't collect worker metrics if there are unreasonable number of queues
36
+ if run_at_by_queue.size > 50
37
+ logger.debug "Skipping DelayedJob metrics - #{run_at_by_queue.size} queues"
38
+ return
39
+ end
40
+
41
+ self.queues = queues | run_at_by_queue.keys
33
42
 
34
- # Ignore failed jobs (they skew latency measurement due to the original run_at)
35
- sql = "SELECT queue, min(run_at) FROM delayed_jobs WHERE run_at < '#{db_time}' AND locked_at IS NULL AND failed_at IS NULL GROUP BY queue"
36
- run_at_by_queue = Hash[ActiveRecord::Base.connection.select_rows(sql)]
37
- queues = self.class.queues | run_at_by_queue.keys
43
+ if track_long_running_jobs?
44
+ sql = <<~SQL
45
+ SELECT COALESCE(queue, 'default'), count(*)
46
+ FROM delayed_jobs
47
+ WHERE locked_at IS NOT NULL
48
+ AND locked_by IS NOT NULL
49
+ AND failed_at IS NULL
50
+ GROUP BY 1
51
+ SQL
52
+
53
+ busy_count_by_queue = Hash[select_rows(sql)]
54
+ self.queues = queues | busy_count_by_queue.keys
55
+ end
38
56
 
39
57
  queues.each do |queue|
40
58
  run_at = run_at_by_queue[queue]
41
- run_at = Time.parse(run_at) if run_at.is_a?(String)
59
+ # DateTime.parse assumes a UTC string
60
+ run_at = DateTime.parse(run_at) if run_at.is_a?(String)
42
61
  latency_ms = run_at ? ((t - run_at)*1000).ceil : 0
62
+ latency_ms = 0 if latency_ms < 0
43
63
 
44
- queue = UNNAMED_QUEUE if queue.nil? || queue.empty?
45
64
  store.push latency_ms, t, queue
46
- log_msg << "dj.#{queue}=#{latency_ms} "
65
+ log_msg << "dj-qt.#{queue}=#{latency_ms} "
66
+
67
+ if track_long_running_jobs?
68
+ busy_count = busy_count_by_queue[queue] || 0
69
+ store.push busy_count, Time.now, queue, :busy
70
+ log_msg << "dj-busy.#{queue}=#{busy_count} "
71
+ end
47
72
  end
48
73
 
49
74
  logger.debug log_msg unless log_msg.empty?
@@ -51,19 +76,21 @@ module RailsAutoscaleAgent
51
76
 
52
77
  private
53
78
 
54
- def install
55
- plugin = Class.new(Delayed::Plugin) do
56
- require 'delayed_job'
79
+ def queues
80
+ # Track the known queues so we can continue reporting on queues that don't
81
+ # have enqueued jobs at the time of reporting.
82
+ # Assume a "default" queue so we always report *something*, even when nothing
83
+ # is enqueued.
84
+ @queues ||= Set.new(['default'])
85
+ end
57
86
 
58
- callbacks do |lifecycle|
59
- lifecycle.before(:enqueue) do |job, &block|
60
- queue = job.queue || 'default'
61
- WorkerAdapters::DelayedJob.queues.add queue
62
- end
63
- end
64
- end
87
+ def track_long_running_jobs?
88
+ Config.instance.track_long_running_jobs
89
+ end
65
90
 
66
- Delayed::Worker.plugins << plugin
91
+ def select_rows(sql)
92
+ # This ensures the agent doesn't hold onto a DB connection any longer than necessary
93
+ ActiveRecord::Base.connection_pool.with_connection { |c| c.select_rows(sql) }
67
94
  end
68
95
  end
69
96
  end
@@ -1,7 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require 'rails_autoscale_agent/logger'
4
- require 'time'
5
4
 
6
5
  module RailsAutoscaleAgent
7
6
  module WorkerAdapters
@@ -9,42 +8,64 @@ module RailsAutoscaleAgent
9
8
  include RailsAutoscaleAgent::Logger
10
9
  include Singleton
11
10
 
12
- UNNAMED_QUEUE = '[unnamed]'
13
- DEFAULT_QUEUES = ['default']
11
+ attr_writer :queues
14
12
 
15
- class << self
16
- attr_accessor :queues
17
- end
18
-
19
- def initialize
20
- self.class.queues = DEFAULT_QUEUES
13
+ def queues
14
+ # Track the known queues so we can continue reporting on queues that don't
15
+ # have enqueued jobs at the time of reporting.
16
+ # Assume a "default" queue so we always report *something*, even when nothing
17
+ # is enqueued.
18
+ @queues ||= Set.new(['default'])
21
19
  end
22
20
 
23
21
  def enabled?
24
- defined? ::Que
22
+ if defined?(::Que)
23
+ logger.info "Que enabled (#{::ActiveRecord::Base.default_timezone})"
24
+ true
25
+ end
25
26
  end
26
27
 
27
28
  def collect!(store)
28
29
  log_msg = String.new
29
- t = Time.now
30
+ t = Time.now.utc
31
+ sql = <<~SQL
32
+ SELECT queue, min(run_at)
33
+ FROM que_jobs
34
+ WHERE finished_at IS NULL
35
+ AND expired_at IS NULL
36
+ AND error_count = 0
37
+ GROUP BY 1
38
+ SQL
30
39
 
31
- # Ignore failed jobs (they skew latency measurement due to the original run_at)
32
- sql = 'SELECT queue, min(run_at) FROM que_jobs WHERE error_count = 0 GROUP BY queue'
33
- run_at_by_queue = Hash[ActiveRecord::Base.connection.select_rows(sql)]
34
- self.class.queues |= run_at_by_queue.keys
40
+ run_at_by_queue = Hash[select_rows(sql)]
41
+
42
+ # Don't collect worker metrics if there are unreasonable number of queues
43
+ if run_at_by_queue.size > 50
44
+ logger.debug "Skipping Que metrics - #{run_at_by_queue.size} queues"
45
+ return
46
+ end
35
47
 
36
- self.class.queues.each do |queue|
48
+ self.queues |= run_at_by_queue.keys
49
+
50
+ queues.each do |queue|
37
51
  run_at = run_at_by_queue[queue]
38
- run_at = Time.parse(run_at) if run_at.is_a?(String)
52
+ run_at = DateTime.parse(run_at) if run_at.is_a?(String)
39
53
  latency_ms = run_at ? ((t - run_at)*1000).ceil : 0
54
+ latency_ms = 0 if latency_ms < 0
40
55
 
41
- queue = UNNAMED_QUEUE if queue.nil? || queue.empty?
42
56
  store.push latency_ms, t, queue
43
57
  log_msg << "que.#{queue}=#{latency_ms} "
44
58
  end
45
59
 
46
60
  logger.debug log_msg unless log_msg.empty?
47
61
  end
62
+
63
+ private
64
+
65
+ def select_rows(sql)
66
+ # This ensures the agent doesn't hold onto a DB connection any longer than necessary
67
+ ActiveRecord::Base.connection_pool.with_connection { |c| c.select_rows(sql) }
68
+ end
48
69
  end
49
70
  end
50
71
  end
@@ -8,8 +8,15 @@ module RailsAutoscaleAgent
8
8
  include RailsAutoscaleAgent::Logger
9
9
  include Singleton
10
10
 
11
+ attr_writer :queues
12
+
13
+ def queues
14
+ @queues ||= ['default']
15
+ end
16
+
11
17
  def enabled?
12
18
  require 'resque'
19
+ logger.info "Resque enabled"
13
20
  true
14
21
  rescue LoadError
15
22
  false
@@ -17,8 +24,19 @@ module RailsAutoscaleAgent
17
24
 
18
25
  def collect!(store)
19
26
  log_msg = String.new
27
+ current_queues = ::Resque.queues
28
+
29
+ # Don't collect worker metrics if there are unreasonable number of queues
30
+ if current_queues.size > 50
31
+ logger.debug "Skipping Resque metrics - #{current_queues.size} queues"
32
+ return
33
+ end
34
+
35
+ # Ensure we continue to collect metrics for known queue names, even when nothing is
36
+ # enqueued at the time. Without this, it will appears that the agent is no longer reporting.
37
+ self.queues |= current_queues
20
38
 
21
- ::Resque.queues.each do |queue|
39
+ queues.each do |queue|
22
40
  next if queue.nil? || queue.empty?
23
41
  depth = ::Resque.size(queue)
24
42
  store.push depth, Time.now, queue, :qd
@@ -8,8 +8,15 @@ module RailsAutoscaleAgent
8
8
  include RailsAutoscaleAgent::Logger
9
9
  include Singleton
10
10
 
11
+ attr_writer :queues
12
+
11
13
  def enabled?
12
14
  require 'sidekiq/api'
15
+
16
+ log_msg = String.new("Sidekiq enabled")
17
+ log_msg << " with long-running job support" if track_long_running_jobs?
18
+ logger.info log_msg
19
+
13
20
  true
14
21
  rescue LoadError
15
22
  false
@@ -17,17 +24,57 @@ module RailsAutoscaleAgent
17
24
 
18
25
  def collect!(store)
19
26
  log_msg = String.new
27
+ queues_by_name = ::Sidekiq::Queue.all.each_with_object({}) do |queue, obj|
28
+ obj[queue.name] = queue
29
+ end
20
30
 
21
- ::Sidekiq::Queue.all.each do |queue|
31
+ # Don't collect worker metrics if there are unreasonable number of queues
32
+ if queues_by_name.size > 50
33
+ logger.debug "Skipping Sidekiq metrics - #{queues_by_name.size} queues"
34
+ return
35
+ end
36
+
37
+ # Ensure we continue to collect metrics for known queue names, even when nothing is
38
+ # enqueued at the time. Without this, it will appear that the agent is no longer reporting.
39
+ queues.each do |queue_name|
40
+ queues_by_name[queue_name] ||= ::Sidekiq::Queue.new(queue_name)
41
+ end
42
+ self.queues = queues_by_name.keys
43
+
44
+ if track_long_running_jobs?
45
+ busy_counts = Hash.new { |h,k| h[k] = 0}
46
+ ::Sidekiq::Workers.new.each do |pid, tid, work|
47
+ busy_counts[work.dig('payload', 'queue')] += 1
48
+ end
49
+ end
50
+
51
+ queues_by_name.each do |queue_name, queue|
22
52
  latency_ms = (queue.latency * 1000).ceil
23
53
  depth = queue.size
54
+
24
55
  store.push latency_ms, Time.now, queue.name, :qt
25
56
  store.push depth, Time.now, queue.name, :qd
26
57
  log_msg << "sidekiq-qt.#{queue.name}=#{latency_ms} sidekiq-qd.#{queue.name}=#{depth} "
58
+
59
+ if track_long_running_jobs?
60
+ busy_count = busy_counts[queue.name]
61
+ store.push busy_count, Time.now, queue.name, :busy
62
+ log_msg << "sidekiq-busy.#{queue.name}=#{busy_count} "
63
+ end
27
64
  end
28
65
 
29
66
  logger.debug log_msg
30
67
  end
68
+
69
+ private
70
+
71
+ def queues
72
+ @queues ||= ['default']
73
+ end
74
+
75
+ def track_long_running_jobs?
76
+ Config.instance.track_long_running_jobs
77
+ end
31
78
  end
32
79
  end
33
80
  end
@@ -16,15 +16,5 @@ Gem::Specification.new do |spec|
16
16
  spec.files = `git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(test|spec|features)/}) }
17
17
  spec.require_paths = ["lib"]
18
18
 
19
- spec.add_development_dependency "bundler", "~> 2.0"
20
- spec.add_development_dependency "rake", "~> 12.3.3"
21
- spec.add_development_dependency "rspec", "~> 3.0"
22
- spec.add_development_dependency "vcr", "~> 3.0"
23
- spec.add_development_dependency "webmock"
24
- spec.add_development_dependency "pry"
25
- spec.add_development_dependency "pry-byebug"
26
- spec.add_development_dependency "sidekiq", "~> 5.0"
27
- spec.add_development_dependency "delayed_job"
28
- spec.add_development_dependency "que"
29
- spec.add_development_dependency "activesupport"
19
+ spec.required_ruby_version = '>= 2.5'
30
20
  end
metadata CHANGED
@@ -1,170 +1,16 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: rails_autoscale_agent
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.9.0.beta.4
4
+ version: 0.10.2
5
5
  platform: ruby
6
6
  authors:
7
7
  - Adam McCrea
8
- autorequire:
8
+ autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2020-06-17 00:00:00.000000000 Z
12
- dependencies:
13
- - !ruby/object:Gem::Dependency
14
- name: bundler
15
- requirement: !ruby/object:Gem::Requirement
16
- requirements:
17
- - - "~>"
18
- - !ruby/object:Gem::Version
19
- version: '2.0'
20
- type: :development
21
- prerelease: false
22
- version_requirements: !ruby/object:Gem::Requirement
23
- requirements:
24
- - - "~>"
25
- - !ruby/object:Gem::Version
26
- version: '2.0'
27
- - !ruby/object:Gem::Dependency
28
- name: rake
29
- requirement: !ruby/object:Gem::Requirement
30
- requirements:
31
- - - "~>"
32
- - !ruby/object:Gem::Version
33
- version: 12.3.3
34
- type: :development
35
- prerelease: false
36
- version_requirements: !ruby/object:Gem::Requirement
37
- requirements:
38
- - - "~>"
39
- - !ruby/object:Gem::Version
40
- version: 12.3.3
41
- - !ruby/object:Gem::Dependency
42
- name: rspec
43
- requirement: !ruby/object:Gem::Requirement
44
- requirements:
45
- - - "~>"
46
- - !ruby/object:Gem::Version
47
- version: '3.0'
48
- type: :development
49
- prerelease: false
50
- version_requirements: !ruby/object:Gem::Requirement
51
- requirements:
52
- - - "~>"
53
- - !ruby/object:Gem::Version
54
- version: '3.0'
55
- - !ruby/object:Gem::Dependency
56
- name: vcr
57
- requirement: !ruby/object:Gem::Requirement
58
- requirements:
59
- - - "~>"
60
- - !ruby/object:Gem::Version
61
- version: '3.0'
62
- type: :development
63
- prerelease: false
64
- version_requirements: !ruby/object:Gem::Requirement
65
- requirements:
66
- - - "~>"
67
- - !ruby/object:Gem::Version
68
- version: '3.0'
69
- - !ruby/object:Gem::Dependency
70
- name: webmock
71
- requirement: !ruby/object:Gem::Requirement
72
- requirements:
73
- - - ">="
74
- - !ruby/object:Gem::Version
75
- version: '0'
76
- type: :development
77
- prerelease: false
78
- version_requirements: !ruby/object:Gem::Requirement
79
- requirements:
80
- - - ">="
81
- - !ruby/object:Gem::Version
82
- version: '0'
83
- - !ruby/object:Gem::Dependency
84
- name: pry
85
- requirement: !ruby/object:Gem::Requirement
86
- requirements:
87
- - - ">="
88
- - !ruby/object:Gem::Version
89
- version: '0'
90
- type: :development
91
- prerelease: false
92
- version_requirements: !ruby/object:Gem::Requirement
93
- requirements:
94
- - - ">="
95
- - !ruby/object:Gem::Version
96
- version: '0'
97
- - !ruby/object:Gem::Dependency
98
- name: pry-byebug
99
- requirement: !ruby/object:Gem::Requirement
100
- requirements:
101
- - - ">="
102
- - !ruby/object:Gem::Version
103
- version: '0'
104
- type: :development
105
- prerelease: false
106
- version_requirements: !ruby/object:Gem::Requirement
107
- requirements:
108
- - - ">="
109
- - !ruby/object:Gem::Version
110
- version: '0'
111
- - !ruby/object:Gem::Dependency
112
- name: sidekiq
113
- requirement: !ruby/object:Gem::Requirement
114
- requirements:
115
- - - "~>"
116
- - !ruby/object:Gem::Version
117
- version: '5.0'
118
- type: :development
119
- prerelease: false
120
- version_requirements: !ruby/object:Gem::Requirement
121
- requirements:
122
- - - "~>"
123
- - !ruby/object:Gem::Version
124
- version: '5.0'
125
- - !ruby/object:Gem::Dependency
126
- name: delayed_job
127
- requirement: !ruby/object:Gem::Requirement
128
- requirements:
129
- - - ">="
130
- - !ruby/object:Gem::Version
131
- version: '0'
132
- type: :development
133
- prerelease: false
134
- version_requirements: !ruby/object:Gem::Requirement
135
- requirements:
136
- - - ">="
137
- - !ruby/object:Gem::Version
138
- version: '0'
139
- - !ruby/object:Gem::Dependency
140
- name: que
141
- requirement: !ruby/object:Gem::Requirement
142
- requirements:
143
- - - ">="
144
- - !ruby/object:Gem::Version
145
- version: '0'
146
- type: :development
147
- prerelease: false
148
- version_requirements: !ruby/object:Gem::Requirement
149
- requirements:
150
- - - ">="
151
- - !ruby/object:Gem::Version
152
- version: '0'
153
- - !ruby/object:Gem::Dependency
154
- name: activesupport
155
- requirement: !ruby/object:Gem::Requirement
156
- requirements:
157
- - - ">="
158
- - !ruby/object:Gem::Version
159
- version: '0'
160
- type: :development
161
- prerelease: false
162
- version_requirements: !ruby/object:Gem::Requirement
163
- requirements:
164
- - - ">="
165
- - !ruby/object:Gem::Version
166
- version: '0'
167
- description:
11
+ date: 2021-01-12 00:00:00.000000000 Z
12
+ dependencies: []
13
+ description:
168
14
  email:
169
15
  - adam@adamlogic.com
170
16
  executables: []
@@ -176,6 +22,7 @@ files:
176
22
  - ".ruby-version"
177
23
  - ".travis.yml"
178
24
  - ".vscode/tasks.json"
25
+ - CHANGELOG.md
179
26
  - Gemfile
180
27
  - LICENSE.txt
181
28
  - README.md
@@ -206,7 +53,7 @@ homepage: https://github.com/adamlogic/rails_autoscale_agent
206
53
  licenses:
207
54
  - MIT
208
55
  metadata: {}
209
- post_install_message:
56
+ post_install_message:
210
57
  rdoc_options: []
211
58
  require_paths:
212
59
  - lib
@@ -214,15 +61,15 @@ required_ruby_version: !ruby/object:Gem::Requirement
214
61
  requirements:
215
62
  - - ">="
216
63
  - !ruby/object:Gem::Version
217
- version: '0'
64
+ version: '2.5'
218
65
  required_rubygems_version: !ruby/object:Gem::Requirement
219
66
  requirements:
220
- - - ">"
67
+ - - ">="
221
68
  - !ruby/object:Gem::Version
222
- version: 1.3.1
69
+ version: '0'
223
70
  requirements: []
224
71
  rubygems_version: 3.1.4
225
- signing_key:
72
+ signing_key:
226
73
  specification_version: 4
227
74
  summary: This gem works with the Rails Autoscale Heroku add-on to automatically scale
228
75
  your web dynos.