rails_autoscale_agent 0.9.0.beta.2 → 0.10.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 998351ab2e804fc443d63825d3a71255d6539f225ef791a4819391d735d29e51
4
- data.tar.gz: 83df02c312d3fd2a1504c782b699c6ac5e26444145ff885ecff808ebf2a9cfc3
3
+ metadata.gz: ed5e9567b63e205a9477e80cb3b1cb5d0d198f60288957c8560eb988f0ab5322
4
+ data.tar.gz: 0ff58213c0347a71218c668fbcea5f3bae8e6e24701b7c9ce3317d84dfce8ef7
5
5
  SHA512:
6
- metadata.gz: 6a34893873a4fc6b926e93d89002b3527f8adce615f97d1694753995df89666cfea3c37f55d435011edbe3e6c39928f835d7594c79cd310feb2267bfc1bbb1fa
7
- data.tar.gz: 00b6943d706c93e1d93dc01259d6982618083b0d7359b212eb0187c26edac6f979a11c0c7a387671d60f80a6eb1ef13e74f5182b9f92b814dc01cd9d0f8c5b6b
6
+ metadata.gz: 3da81efface83a93ac9c3676b7edbcaf431911cfe428c054a39203663048bc7a320ebd6881606d7b18e563c145d8fcfc1cd6d3acf945bff82525195796f1be96
7
+ data.tar.gz: a4012e4ab5876cddb28bfea34a2b71d6239c34dc5b167cb54616e5635a67d36915245c2b96bc536256ee7109640db6a035d0b5b99bfa20fab76a88900aba6f3f
@@ -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,109 @@
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.1...master)
5
+
6
+ _There are no currently unreleased changes._
7
+
8
+ ## [0.10.1](https://github.com/adamlogic/rails_autoscale_agent/compare/v0.9.1...v0.10.1) - 2021-01-03
9
+
10
+ ### Added
11
+
12
+ - Add support for [long-running jobs](https://railsautoscale.com/docs/long-running-jobs/) in Sidekiq and Delayed Job.
13
+ - Handle x-request-start measured in seconds (instead of milliseconds) to support nginx buildpack ([cd092f3](https://github.com/adamlogic/rails_autoscale_agent/commit/cd092f38718abf5ffaea866bcae7831d4c910ffd))
14
+ - Override worker adapter config via env var ([75dd06b](https://github.com/adamlogic/rails_autoscale_agent/commit/75dd06b2a7ff4eeab829eec24d503dc067c8fe32))
15
+
16
+ ### Changed
17
+
18
+ - Require Ruby 2.5 or newer. ([b033050](https://github.com/adamlogic/rails_autoscale_agent/commit/b033050b7f9d4d7f1e50dbd780cf0e1822249268))
19
+ - Only report worker metrics from web.1 to avoid redundant data. ([d5d5fa8](https://github.com/adamlogic/rails_autoscale_agent/commit/d5d5fa87fb4d7d046832a64edde9ed0c3a6ec75f))
20
+ - Don't collect worker metrics for an unreasonable number of queues. ([a9358af](https://github.com/adamlogic/rails_autoscale_agent/commit/a9358af74a29a941d1f1d60a0222077dafd5ce08))
21
+
22
+ ### Fixed
23
+
24
+ - Avoid holding onto database connections (DJ & Que only). ([3919ca5](https://github.com/adamlogic/rails_autoscale_agent/commit/3919ca54420cafa82abf9f8cd251569f9637482b))
25
+ - Better error handling for worker adapters. ([190786e](https://github.com/adamlogic/rails_autoscale_agent/commit/190786e4a910d41e394a3129aac1d23b594dbd9b))
26
+ - Don't collect metrics of the reporter isn't running. Avoids memory bloat. ([247c322](https://github.com/adamlogic/rails_autoscale_agent/commit/247c322cffc625a8c6b2395080a048ffb94e7f3b))
27
+
28
+ ## [0.10.0](https://github.com/adamlogic/rails_autoscale_agent/compare/v0.9.1...v0.10.0) - 2021-01-03 [YANKED]
29
+
30
+ _I released the wrong branch 🤦‍♂️_
31
+
32
+ ## [0.9.1](https://github.com/adamlogic/rails_autoscale_agent/compare/v0.9.0...v0.9.1) - 2020-07-29
33
+
34
+ ### Fixed
35
+
36
+ - Fix a bug in error handling. ([3018542](https://github.com/adamlogic/rails_autoscale_agent/commit/3018542cd046fc4e1bd6e7da86e72a6aa2d50a8f))
37
+ - Remove unintentional Rails dependency.
38
+
39
+ ## [0.9.0](https://github.com/adamlogic/rails_autoscale_agent/compare/v0.8.3...v0.9.0) - 2020-07-12
40
+
41
+ ### Added
42
+
43
+ - Add support for Resque workers.
44
+ - Add dev mode for working on the agent gem itself. ([47e3fca](https://github.com/adamlogic/rails_autoscale_agent/commit/47e3fca5b788f48567a345d9cab3a26b9cd87693))
45
+ - Report agent exceptions to Rails Autoscale.
46
+
47
+ ### Changed
48
+
49
+ - Adjust queue time metric to exclude time waiting for large request bodies. ([#25](https://github.com/adamlogic/rails_autoscale_agent/pull/25))
50
+ - Que and DJ jobs without a queue name will be included in the "default" queue metrics.
51
+
52
+ ### Fixed
53
+
54
+ - Multiple fixes to the Delayed Job SQL query.
55
+
56
+ ## [0.8.3](https://github.com/adamlogic/rails_autoscale_agent/compare/v0.8.2...v0.8.3) - 2020-05-26
57
+
58
+ ### Fixed
59
+
60
+ - Ignored failed job in Delayed Job adapter. ([fa72fc2](https://github.com/adamlogic/rails_autoscale_agent/compare/v0.8.2...v0.8.3))
61
+
62
+ ## [0.8.2](https://github.com/adamlogic/rails_autoscale_agent/compare/v0.8.1...v0.8.2) - 2020-05-22
63
+
64
+ ### Fixed
65
+
66
+ - 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))
67
+
68
+ ## [0.8.1](https://github.com/adamlogic/rails_autoscale_agent/compare/v0.8.0...v0.8.1) - 2020-05-04
69
+
70
+ ### Fixed
71
+
72
+ - Ignore failed jobs in Que adapter. ([#18](https://github.com/adamlogic/rails_autoscale_agent/pull/18))
73
+
74
+ ## [0.8.0](https://github.com/adamlogic/rails_autoscale_agent/compare/v0.7.0...v0.8.0) - 2020-03-21
75
+
76
+ ### Added
77
+
78
+ - Add support for Delayed Job ([#14](https://github.com/adamlogic/rails_autoscale_agent/pull/14))
79
+ - Add support for Que ([#15](https://github.com/adamlogic/rails_autoscale_agent/pull/15))
80
+
81
+ ## [0.7.0](https://github.com/adamlogic/rails_autoscale_agent/compare/v0.6.3...v0.7.0) - 2019-12-04
82
+
83
+ ### Added
84
+
85
+ - Make worker adapters configurable. ([012d937](https://github.com/adamlogic/rails_autoscale_agent/commit/012d9379296763f5e42df95f05b066fe82ab0051))
86
+
87
+ ## [0.6.3](https://github.com/adamlogic/rails_autoscale_agent/compare/v0.6.2...v0.6.3) - 2019-06-25
88
+
89
+ ### Fixed
90
+
91
+ - Fix issues with logging.
92
+
93
+ ## [0.6.2](https://github.com/adamlogic/rails_autoscale_agent/compare/v0.6.1...v0.6.2) - 2019-06-22
94
+
95
+ ### Fixed
96
+
97
+ - Fix issues with logging.
98
+
99
+ ## [0.6.1](https://github.com/adamlogic/rails_autoscale_agent/compare/v0.6.0...v0.6.1) - 2019-05-06
100
+
101
+ ### Fixed
102
+
103
+ - Don't assume Sidekiq is present.
104
+
105
+ ## [0.6.0](https://github.com/adamlogic/rails_autoscale_agent/compare/v0.4.1...v0.6.0) - 2019-05-03
106
+
107
+ ### Added
108
+
109
+ - 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
@@ -22,17 +22,7 @@ The agent will only communicate with Rails Autoscale if a `RAILS_AUTOSCALE_URL`
22
22
 
23
23
  ## Non-Rails Rack apps
24
24
 
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
- ```
25
+ 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
26
 
37
27
  ## What data is collected?
38
28
 
@@ -47,6 +37,32 @@ The middleware agent runs in its own thread so your web requests are not impacte
47
37
 
48
38
  Rails Autoscale aggregates and stores this information to power the autoscaling algorithm and dashboard visualizations.
49
39
 
40
+ ## Configuration
41
+
42
+ 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:
43
+
44
+ - `RAILS_AUTOSCALE_DEBUG` - Enables debug logging. See more in the [logging](#logging) section below.
45
+ - `RAILS_AUTOSCALE_WORKER_ADAPTER` - Overrides the available worker adapters. See more in the [worker adapters](#worker_adapters) section below.
46
+ - `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.
47
+
48
+ ## Worker adapters
49
+
50
+ 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.
51
+
52
+ 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:
53
+
54
+ ```
55
+ heroku config:add RAILS_AUTOSCALE_WORKER_ADAPTER=sidekiq
56
+ ```
57
+
58
+ You can also disable collection of worker metrics altogether:
59
+
60
+ ```
61
+ heroku config:add RAILS_AUTOSCALE_WORKER_ADAPTER=""
62
+ ```
63
+
64
+ It's also possible to write a custom worker adapter. See [these docs](https://railsautoscale.com/docs/custom-worker-adapter/) for details.
65
+
50
66
  ## Troubleshooting
51
67
 
52
68
  Once installed, you should see something like this in your development log:
@@ -59,17 +75,39 @@ In production, run `heroku logs -t | grep RailsAutoscale`, and you should see so
59
75
 
60
76
  If you don't see either of these, try running `bundle` again and restarting your Rails application.
61
77
 
62
- You can see more detailed (debug) logging by setting the `RAILS_AUTOSCALE_DEBUG` env var on your Heroku app:
78
+ You can see more detailed (debug) logging by setting `RAILS_AUTOSCALE_DEBUG` on your Heroku app:
63
79
 
64
80
  ```
65
81
  heroku config:add RAILS_AUTOSCALE_DEBUG=true
66
82
  ```
67
83
 
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.
84
+ See more in the [logging](#logging) section below.
70
85
 
71
86
  Reach out to help@railsautoscale.com if you run into any other problems.
72
87
 
88
+ ## Logging
89
+
90
+ The Rails logger is used by default.
91
+ If you wish to use a different logger you can set it on the configuration object:
92
+
93
+ ```ruby
94
+ # config/initializers/rails_autoscale_agent.rb
95
+ RailsAutoscaleAgent::Config.instance.logger = MyLogger.new
96
+ ```
97
+
98
+ 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:
99
+
100
+ ```
101
+ heroku config:add RAILS_AUTOSCALE_DEBUG=true
102
+ ```
103
+
104
+ If you find the gem too chatty even without this, you can quiet it down further:
105
+
106
+ ```ruby
107
+ # config/initializers/rails_autoscale_agent.rb
108
+ RailsAutoscaleAgent::Config.instance.quiet = true
109
+ ```
110
+
73
111
  ## Development
74
112
 
75
113
  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.2"
4
+ VERSION = "0.10.1"
5
5
  end
@@ -8,38 +8,67 @@ module RailsAutoscaleAgent
8
8
  include RailsAutoscaleAgent::Logger
9
9
  include Singleton
10
10
 
11
- class << self
12
- attr_accessor :queues
13
- end
14
-
15
- def initialize
16
- # Track the known queues so we can continue reporting on queues that don't
17
- # currently have enqueued jobs.
18
- self.class.queues = Set.new
19
-
20
- install if enabled?
21
- end
11
+ attr_writer :queues
22
12
 
23
13
  def enabled?
24
- 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
25
20
  end
26
21
 
27
22
  def collect!(store)
28
23
  log_msg = String.new
29
- t = Time.now
24
+ t = Time.now.utc
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
30
42
 
31
- # Ignore failed jobs (they skew latency measurement due to the original run_at)
32
- sql = 'SELECT queue, min(run_at) FROM delayed_jobs WHERE attempts = 0 GROUP BY queue'
33
- run_at_by_queue = Hash[ActiveRecord::Base.connection.select_rows(sql)]
34
- 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
35
56
 
36
57
  queues.each do |queue|
37
- next if queue.nil? || queue.empty?
38
58
  run_at = run_at_by_queue[queue]
39
- 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)
40
61
  latency_ms = run_at ? ((t - run_at)*1000).ceil : 0
62
+ latency_ms = 0 if latency_ms < 0
63
+
41
64
  store.push latency_ms, t, queue
42
- 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
43
72
  end
44
73
 
45
74
  logger.debug log_msg unless log_msg.empty?
@@ -47,19 +76,21 @@ module RailsAutoscaleAgent
47
76
 
48
77
  private
49
78
 
50
- def install
51
- plugin = Class.new(Delayed::Plugin) do
52
- 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
53
86
 
54
- callbacks do |lifecycle|
55
- lifecycle.before(:enqueue) do |job, &block|
56
- queue = job.queue || 'default'
57
- WorkerAdapters::DelayedJob.queues.add queue
58
- end
59
- end
60
- end
87
+ def track_long_running_jobs?
88
+ Config.instance.track_long_running_jobs
89
+ end
61
90
 
62
- 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) }
63
94
  end
64
95
  end
65
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,40 +8,64 @@ module RailsAutoscaleAgent
9
8
  include RailsAutoscaleAgent::Logger
10
9
  include Singleton
11
10
 
12
- DEFAULT_QUEUES = ['default']
11
+ attr_writer :queues
13
12
 
14
- class << self
15
- attr_accessor :queues
16
- end
17
-
18
- def initialize
19
- 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'])
20
19
  end
21
20
 
22
21
  def enabled?
23
- defined? ::Que
22
+ if defined?(::Que)
23
+ logger.info "Que enabled (#{::ActiveRecord::Base.default_timezone})"
24
+ true
25
+ end
24
26
  end
25
27
 
26
28
  def collect!(store)
27
29
  log_msg = String.new
28
- 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
39
+
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
29
47
 
30
- # Ignore failed jobs (they skew latency measurement due to the original run_at)
31
- sql = 'SELECT queue, min(run_at) FROM que_jobs WHERE error_count = 0 GROUP BY queue'
32
- run_at_by_queue = Hash[ActiveRecord::Base.connection.select_rows(sql)]
33
- self.class.queues |= run_at_by_queue.keys
48
+ self.queues |= run_at_by_queue.keys
34
49
 
35
- self.class.queues.each do |queue|
36
- next if queue.nil? || queue.empty?
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
55
+
40
56
  store.push latency_ms, t, queue
41
57
  log_msg << "que.#{queue}=#{latency_ms} "
42
58
  end
43
59
 
44
60
  logger.debug log_msg unless log_msg.empty?
45
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
46
69
  end
47
70
  end
48
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.2
4
+ version: 0.10.1
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-10 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-03 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,23 +53,23 @@ 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
213
60
  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
- rubygems_version: 3.0.3
225
- signing_key:
71
+ rubygems_version: 3.1.4
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.