rails_autoscale_agent 0.9.0 → 0.11.0

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: cd7ffb8cd27de3a23d5e76bd9a9c7768ce1fd397389291285022523fb398ee45
4
- data.tar.gz: d8cf3fa7ab2994ef196742fe872170c069dc6f14bc0490b595ff175e48ce59e6
3
+ metadata.gz: f313cab3eefd93584c9d2484698d836ad19467b2e03cd26623003edcd1c64ad5
4
+ data.tar.gz: d8b33799b6dc0c2e92b6fbe770de41ac7e104a8d887965cf35538ea9ab6b8acb
5
5
  SHA512:
6
- metadata.gz: '0976e83784dd6a622b18a67491e727cbe9f0fb1a54e1974f50b61f441f642a296b5a4e3349e2f4d292acc35c844765527c16e097cf7ac914a7224b68e2ae8bd6'
7
- data.tar.gz: 2de7d8a61d44fd408cfbab88b1be37327bd5836da8c125f4c61f80ac7503c0a44c5ee5f893919eb5487e4a34a817102a68aabd76699d34d6ce3086680b8aceb7
6
+ metadata.gz: 3208e51e66e87a9c5e076322c04f7f3594295d11ad2e5c16cdea5600372e02b70e50e15138788fe5d59a7d6854a8c59c908db5cdb28878386989c7e8fb2d9049
7
+ data.tar.gz: 1b771752c5fde1c3cd534a4ef2aca0e5917d3343d10e97cfb5fdb64a89f72cd5343e4662af281c214a35459cb77759f7c19b5110fd8bed979b54ffeff97a1de0
data/CHANGELOG.md ADDED
@@ -0,0 +1,121 @@
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.11.0...master)
5
+
6
+ No currently unreleased changes.
7
+
8
+ ## [0.11.0](https://github.com/adamlogic/rails_autoscale_agent/compare/v0.10.2...v0.11.0)
9
+
10
+ ### Added
11
+
12
+ - Add `RAILS_AUTOSCALE_MAX_QUEUES` config option. ([28738a5](https://github.com/adamlogic/rails_autoscale_agent/commit/28738a5dc4cd6b0a46e77459d6f98e6b33072da9))
13
+
14
+ ## [0.10.2](https://github.com/adamlogic/rails_autoscale_agent/compare/v0.10.1...v0.10.2) - 2021-01-12
15
+
16
+ ### Changed
17
+
18
+ - Loosen Ruby constraint to allow Ruby 3. ([#36](https://github.com/adamlogic/rails_autoscale_agent/pull/36))
19
+
20
+ ## [0.10.1](https://github.com/adamlogic/rails_autoscale_agent/compare/v0.9.1...v0.10.1) - 2021-01-03
21
+
22
+ ### Added
23
+
24
+ - Add support for [long-running jobs](https://railsautoscale.com/docs/long-running-jobs/) in Sidekiq and Delayed Job.
25
+ - Handle x-request-start measured in seconds (instead of milliseconds) to support nginx buildpack ([cd092f3](https://github.com/adamlogic/rails_autoscale_agent/commit/cd092f38718abf5ffaea866bcae7831d4c910ffd))
26
+ - Override worker adapter config via env var ([75dd06b](https://github.com/adamlogic/rails_autoscale_agent/commit/75dd06b2a7ff4eeab829eec24d503dc067c8fe32))
27
+
28
+ ### Changed
29
+
30
+ - Require Ruby 2.5 or newer. ([b033050](https://github.com/adamlogic/rails_autoscale_agent/commit/b033050b7f9d4d7f1e50dbd780cf0e1822249268))
31
+ - Only report worker metrics from web.1 to avoid redundant data. ([d5d5fa8](https://github.com/adamlogic/rails_autoscale_agent/commit/d5d5fa87fb4d7d046832a64edde9ed0c3a6ec75f))
32
+ - Don't collect worker metrics for an unreasonable number of queues. ([a9358af](https://github.com/adamlogic/rails_autoscale_agent/commit/a9358af74a29a941d1f1d60a0222077dafd5ce08))
33
+
34
+ ### Fixed
35
+
36
+ - Avoid holding onto database connections (DJ & Que only). ([3919ca5](https://github.com/adamlogic/rails_autoscale_agent/commit/3919ca54420cafa82abf9f8cd251569f9637482b))
37
+ - Better error handling for worker adapters. ([190786e](https://github.com/adamlogic/rails_autoscale_agent/commit/190786e4a910d41e394a3129aac1d23b594dbd9b))
38
+ - Don't collect metrics of the reporter isn't running. Avoids memory bloat. ([247c322](https://github.com/adamlogic/rails_autoscale_agent/commit/247c322cffc625a8c6b2395080a048ffb94e7f3b))
39
+
40
+ ## [0.10.0](https://github.com/adamlogic/rails_autoscale_agent/compare/v0.9.1...v0.10.0) - 2021-01-03 [YANKED]
41
+
42
+ _I released the wrong branch 🤦‍♂️_
43
+
44
+ ## [0.9.1](https://github.com/adamlogic/rails_autoscale_agent/compare/v0.9.0...v0.9.1) - 2020-07-29
45
+
46
+ ### Fixed
47
+
48
+ - Fix a bug in error handling. ([3018542](https://github.com/adamlogic/rails_autoscale_agent/commit/3018542cd046fc4e1bd6e7da86e72a6aa2d50a8f))
49
+ - Remove unintentional Rails dependency.
50
+
51
+ ## [0.9.0](https://github.com/adamlogic/rails_autoscale_agent/compare/v0.8.3...v0.9.0) - 2020-07-12
52
+
53
+ ### Added
54
+
55
+ - Add support for Resque workers.
56
+ - Add dev mode for working on the agent gem itself. ([47e3fca](https://github.com/adamlogic/rails_autoscale_agent/commit/47e3fca5b788f48567a345d9cab3a26b9cd87693))
57
+ - Report agent exceptions to Rails Autoscale.
58
+
59
+ ### Changed
60
+
61
+ - Adjust queue time metric to exclude time waiting for large request bodies. ([#25](https://github.com/adamlogic/rails_autoscale_agent/pull/25))
62
+ - Que and DJ jobs without a queue name will be included in the "default" queue metrics.
63
+
64
+ ### Fixed
65
+
66
+ - Multiple fixes to the Delayed Job SQL query.
67
+
68
+ ## [0.8.3](https://github.com/adamlogic/rails_autoscale_agent/compare/v0.8.2...v0.8.3) - 2020-05-26
69
+
70
+ ### Fixed
71
+
72
+ - Ignored failed job in Delayed Job adapter. ([fa72fc2](https://github.com/adamlogic/rails_autoscale_agent/compare/v0.8.2...v0.8.3))
73
+
74
+ ## [0.8.2](https://github.com/adamlogic/rails_autoscale_agent/compare/v0.8.1...v0.8.2) - 2020-05-22
75
+
76
+ ### Fixed
77
+
78
+ - 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))
79
+
80
+ ## [0.8.1](https://github.com/adamlogic/rails_autoscale_agent/compare/v0.8.0...v0.8.1) - 2020-05-04
81
+
82
+ ### Fixed
83
+
84
+ - Ignore failed jobs in Que adapter. ([#18](https://github.com/adamlogic/rails_autoscale_agent/pull/18))
85
+
86
+ ## [0.8.0](https://github.com/adamlogic/rails_autoscale_agent/compare/v0.7.0...v0.8.0) - 2020-03-21
87
+
88
+ ### Added
89
+
90
+ - Add support for Delayed Job ([#14](https://github.com/adamlogic/rails_autoscale_agent/pull/14))
91
+ - Add support for Que ([#15](https://github.com/adamlogic/rails_autoscale_agent/pull/15))
92
+
93
+ ## [0.7.0](https://github.com/adamlogic/rails_autoscale_agent/compare/v0.6.3...v0.7.0) - 2019-12-04
94
+
95
+ ### Added
96
+
97
+ - Make worker adapters configurable. ([012d937](https://github.com/adamlogic/rails_autoscale_agent/commit/012d9379296763f5e42df95f05b066fe82ab0051))
98
+
99
+ ## [0.6.3](https://github.com/adamlogic/rails_autoscale_agent/compare/v0.6.2...v0.6.3) - 2019-06-25
100
+
101
+ ### Fixed
102
+
103
+ - Fix issues with logging.
104
+
105
+ ## [0.6.2](https://github.com/adamlogic/rails_autoscale_agent/compare/v0.6.1...v0.6.2) - 2019-06-22
106
+
107
+ ### Fixed
108
+
109
+ - Fix issues with logging.
110
+
111
+ ## [0.6.1](https://github.com/adamlogic/rails_autoscale_agent/compare/v0.6.0...v0.6.1) - 2019-05-06
112
+
113
+ ### Fixed
114
+
115
+ - Don't assume Sidekiq is present.
116
+
117
+ ## [0.6.0](https://github.com/adamlogic/rails_autoscale_agent/compare/v0.4.1...v0.6.0) - 2019-05-03
118
+
119
+ ### Added
120
+
121
+ - Add support for autoscaling Sidekiq.
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,7 +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
+ 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.
26
27
 
27
28
  ## What data is collected?
28
29
 
@@ -37,6 +38,33 @@ The middleware agent runs in its own thread so your web requests are not impacte
37
38
 
38
39
  Rails Autoscale aggregates and stores this information to power the autoscaling algorithm and dashboard visualizations.
39
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
+ - `RAILS_AUTOSCALE_MAX_QUEUES` - Worker metrics will only report up to 50 queues by default. If you have more than 50 queues, you'll need to configure this settings or reduce your number of queues.
49
+
50
+ ## Worker adapters
51
+
52
+ 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.
53
+
54
+ 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:
55
+
56
+ ```
57
+ heroku config:add RAILS_AUTOSCALE_WORKER_ADAPTER=sidekiq
58
+ ```
59
+
60
+ You can also disable collection of worker metrics altogether:
61
+
62
+ ```
63
+ heroku config:add RAILS_AUTOSCALE_WORKER_ADAPTER=""
64
+ ```
65
+
66
+ It's also possible to write a custom worker adapter. See [these docs](https://railsautoscale.com/docs/custom-worker-adapter/) for details.
67
+
40
68
  ## Troubleshooting
41
69
 
42
70
  Once installed, you should see something like this in your development log:
@@ -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,29 +4,27 @@ 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, :debug, :quiet
12
+ :dyno, :addon_name, :worker_adapters, :dev_mode, :debug, :quiet,
13
+ :track_long_running_jobs, :max_queues,
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
25
  @debug = dev_mode? || ENV['RAILS_AUTOSCALE_DEBUG'] == 'true'
29
- @pid = Process.pid
26
+ @track_long_running_jobs = ENV['RAILS_AUTOSCALE_LONG_JOBS'] == 'true'
27
+ @max_queues = ENV.fetch('RAILS_AUTOSCALE_MAX_QUEUES', 50).to_i
30
28
  @max_request_size = 100_000 # ignore request payloads over 100k since they skew the queue times
31
29
  @report_interval = 10 # this default will be overwritten during Reporter#register!
32
30
  @logger ||= defined?(Rails) ? Rails.logger : ::Logger.new(STDOUT)
@@ -34,7 +32,7 @@ module RailsAutoscaleAgent
34
32
  end
35
33
 
36
34
  def to_s
37
- "#{@dyno}##{@pid}"
35
+ "#{@dyno}##{Process.pid}"
38
36
  end
39
37
 
40
38
  def ignore_large_requests?
@@ -44,5 +42,16 @@ module RailsAutoscaleAgent
44
42
  alias_method :dev_mode?, :dev_mode
45
43
  alias_method :debug?, :debug
46
44
  alias_method :quiet?, :quiet
45
+
46
+ private
47
+
48
+ def prepare_worker_adapters
49
+ adapter_names = (ENV['RAILS_AUTOSCALE_WORKER_ADAPTER'] || DEFAULT_WORKER_ADAPTERS).split(',')
50
+ adapter_names.map do |adapter_name|
51
+ require "rails_autoscale_agent/worker_adapters/#{adapter_name}"
52
+ adapter_constant_name = adapter_name.capitalize.gsub(/(?:_)(.)/i) { $1.upcase }
53
+ WorkerAdapters.const_get(adapter_constant_name).instance
54
+ end
55
+ end
47
56
  end
48
57
  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
 
@@ -6,8 +6,6 @@ require 'rails_autoscale_agent/autoscale_api'
6
6
  require 'rails_autoscale_agent/time_rounder'
7
7
  require 'rails_autoscale_agent/registration'
8
8
 
9
- # Reporter wakes up every minute to send metrics to the RailsAutoscale API
10
-
11
9
  module RailsAutoscaleAgent
12
10
  class Reporter
13
11
  include Singleton
@@ -20,6 +18,7 @@ module RailsAutoscaleAgent
20
18
  def start!(config, store)
21
19
  @started = true
22
20
  @worker_adapters = config.worker_adapters.select(&:enabled?)
21
+ @dyno_num = config.dyno.to_s.split('.').last.to_i
23
22
 
24
23
  if !config.api_base_url && !config.dev_mode?
25
24
  logger.info "Reporter not started: #{config.addon_name}_URL is not set"
@@ -34,15 +33,14 @@ module RailsAutoscaleAgent
34
33
  multiplier = 1 - (rand / 4) # between 0.75 and 1.0
35
34
  sleep config.report_interval * multiplier
36
35
 
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)
36
+ # It's redundant to report worker metrics from every web dyno, so only report from web.1
37
+ if @dyno_num == 1
38
+ @worker_adapters.map do |adapter|
39
+ report_exceptions(config) { adapter.collect!(store) }
40
+ end
45
41
  end
42
+
43
+ report_exceptions(config) { report!(config, store) }
46
44
  end
47
45
  end
48
46
  end
@@ -51,23 +49,21 @@ module RailsAutoscaleAgent
51
49
  @started
52
50
  end
53
51
 
52
+ private
53
+
54
54
  def report!(config, store)
55
55
  report = store.pop_report
56
56
 
57
- if report.measurements.any?
58
- logger.info "Reporting #{report.measurements.size} measurements"
57
+ logger.info "Reporting #{report.measurements.size} measurements"
59
58
 
60
- params = report.to_params(config)
61
- result = AutoscaleApi.new(config).report_metrics!(params, report.to_csv)
59
+ params = report.to_params(config)
60
+ result = AutoscaleApi.new(config).report_metrics!(params, report.to_csv)
62
61
 
63
- case result
64
- when AutoscaleApi::SuccessResponse
65
- logger.debug "Reported successfully"
66
- when AutoscaleApi::FailureResponse
67
- logger.error "Reporter failed: #{result.failure_message}"
68
- end
69
- else
70
- logger.debug "Reporter has nothing to report"
62
+ case result
63
+ when AutoscaleApi::SuccessResponse
64
+ logger.debug "Reported successfully"
65
+ when AutoscaleApi::FailureResponse
66
+ logger.error "Reporter failed: #{result.failure_message}"
71
67
  end
72
68
  end
73
69
 
@@ -86,5 +82,20 @@ module RailsAutoscaleAgent
86
82
  logger.error "Reporter failed to register: #{result.failure_message}"
87
83
  end
88
84
  end
85
+
86
+ def report_exceptions(config)
87
+ begin
88
+ yield
89
+ rescue => ex
90
+ # Exceptions in threads other than the main thread will fail silently
91
+ # https://ruby-doc.org/core-2.2.0/Thread.html#class-Thread-label-Exception+handling
92
+ logger.error "Reporter error: #{ex.inspect}"
93
+ AutoscaleApi.new(config).report_exception!(ex)
94
+ end
95
+ rescue => ex
96
+ # An exception was encountered while trying to report the original exception.
97
+ # Swallow the error so the reporter continues to report.
98
+ logger.error "Exception reporting error: #{ex.inspect}"
99
+ end
89
100
  end
90
101
  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
@@ -13,13 +13,19 @@ module RailsAutoscaleAgent
13
13
 
14
14
  def initialize
15
15
  @measurements = []
16
+ @last_pop = Time.now
16
17
  end
17
18
 
18
19
  def push(value, time = Time.now, queue_name = nil, metric = nil)
20
+ # If it's been two minutes since clearing out the store, stop collecting measurements.
21
+ # There could be an issue with the reporter, and continuing to collect will consume linear memory.
22
+ return if @last_pop && @last_pop < Time.now - 120
23
+
19
24
  @measurements << Measurement.new(time, value, queue_name, metric)
20
25
  end
21
26
 
22
27
  def pop_report
28
+ @last_pop = Time.now
23
29
  report = Report.new
24
30
 
25
31
  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"
4
+ VERSION = "0.11.0"
5
5
  end
@@ -10,17 +10,11 @@ module RailsAutoscaleAgent
10
10
 
11
11
  attr_writer :queues
12
12
 
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'])
19
- end
20
-
21
13
  def enabled?
22
14
  if defined?(::Delayed::Job) && defined?(::Delayed::Backend::ActiveRecord)
23
- logger.info "DelayedJob enabled (#{::ActiveRecord::Base.default_timezone})"
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
24
18
  true
25
19
  end
26
20
  end
@@ -36,8 +30,29 @@ module RailsAutoscaleAgent
36
30
  GROUP BY queue
37
31
  SQL
38
32
 
39
- run_at_by_queue = Hash[ActiveRecord::Base.connection.select_rows(sql)]
40
- self.queues |= run_at_by_queue.keys
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 > Config.instance.max_queues
37
+ logger.warn "Skipping DelayedJob metrics - #{run_at_by_queue.size} queues exceeds the #{Config.instance.max_queues} queue limit"
38
+ return
39
+ end
40
+
41
+ self.queues = queues | run_at_by_queue.keys
42
+
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
41
56
 
42
57
  queues.each do |queue|
43
58
  run_at = run_at_by_queue[queue]
@@ -47,11 +62,36 @@ module RailsAutoscaleAgent
47
62
  latency_ms = 0 if latency_ms < 0
48
63
 
49
64
  store.push latency_ms, t, queue
50
- 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
51
72
  end
52
73
 
53
74
  logger.debug log_msg unless log_msg.empty?
54
75
  end
76
+
77
+ private
78
+
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
86
+
87
+ def track_long_running_jobs?
88
+ Config.instance.track_long_running_jobs
89
+ end
90
+
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) }
94
+ end
55
95
  end
56
96
  end
57
97
  end
@@ -37,7 +37,14 @@ module RailsAutoscaleAgent
37
37
  GROUP BY 1
38
38
  SQL
39
39
 
40
- run_at_by_queue = Hash[ActiveRecord::Base.connection.select_rows(sql)]
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 > Config.instance.max_queues
44
+ logger.warn "Skipping Que metrics - #{run_at_by_queue.size} queues exceeds the #{Config.instance.max_queues} queue limit"
45
+ return
46
+ end
47
+
41
48
  self.queues |= run_at_by_queue.keys
42
49
 
43
50
  queues.each do |queue|
@@ -52,6 +59,13 @@ module RailsAutoscaleAgent
52
59
 
53
60
  logger.debug log_msg unless log_msg.empty?
54
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
55
69
  end
56
70
  end
57
71
  end
@@ -24,10 +24,17 @@ module RailsAutoscaleAgent
24
24
 
25
25
  def collect!(store)
26
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 > Config.instance.max_queues
31
+ logger.warn "Skipping Resque metrics - #{current_queues.size} queues exceeds the #{Config.instance.max_queues} queue limit"
32
+ return
33
+ end
27
34
 
28
35
  # Ensure we continue to collect metrics for known queue names, even when nothing is
29
36
  # enqueued at the time. Without this, it will appears that the agent is no longer reporting.
30
- self.queues |= ::Resque.queues
37
+ self.queues |= current_queues
31
38
 
32
39
  queues.each do |queue|
33
40
  next if queue.nil? || queue.empty?
@@ -8,15 +8,15 @@ module RailsAutoscaleAgent
8
8
  include RailsAutoscaleAgent::Logger
9
9
  include Singleton
10
10
 
11
- attr_writer :known_queue_names
12
-
13
- def known_queue_names
14
- @known_queue_names ||= ['default']
15
- end
11
+ attr_writer :queues
16
12
 
17
13
  def enabled?
18
14
  require 'sidekiq/api'
19
- logger.info "Sidekiq enabled"
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
+
20
20
  true
21
21
  rescue LoadError
22
22
  false
@@ -28,23 +28,53 @@ module RailsAutoscaleAgent
28
28
  obj[queue.name] = queue
29
29
  end
30
30
 
31
+ # Don't collect worker metrics if there are unreasonable number of queues
32
+ if queues_by_name.size > Config.instance.max_queues
33
+ logger.warn "Skipping Sidekiq metrics - #{queues_by_name.size} queues exceeds the #{Config.instance.max_queues} queue limit"
34
+ return
35
+ end
36
+
31
37
  # Ensure we continue to collect metrics for known queue names, even when nothing is
32
- # enqueued at the time. Without this, it will appears that the agent is no longer reporting.
33
- known_queue_names.each do |queue_name|
38
+ # enqueued at the time. Without this, it will appear that the agent is no longer reporting.
39
+ queues.each do |queue_name|
34
40
  queues_by_name[queue_name] ||= ::Sidekiq::Queue.new(queue_name)
35
41
  end
36
- self.known_queue_names = queues_by_name.keys
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
37
50
 
38
51
  queues_by_name.each do |queue_name, queue|
39
52
  latency_ms = (queue.latency * 1000).ceil
40
53
  depth = queue.size
54
+
41
55
  store.push latency_ms, Time.now, queue.name, :qt
42
56
  store.push depth, Time.now, queue.name, :qd
43
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
44
64
  end
45
65
 
46
66
  logger.debug log_msg
47
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
48
78
  end
49
79
  end
50
80
  end
@@ -9,10 +9,20 @@ Gem::Specification.new do |spec|
9
9
  spec.authors = ["Adam McCrea"]
10
10
  spec.email = ["adam@adamlogic.com"]
11
11
 
12
- spec.summary = "This gem works with the Rails Autoscale Heroku add-on to automatically scale your web dynos."
13
- spec.homepage = "https://github.com/adamlogic/rails_autoscale_agent"
12
+ spec.summary = "This gem works with the Rails Autoscale Heroku add-on to automatically scale your web and worker dynos."
13
+ spec.homepage = "https://railsautoscale.com"
14
14
  spec.license = "MIT"
15
15
 
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
+
19
+ spec.required_ruby_version = '>= 2.5.0'
20
+
21
+ spec.metadata = {
22
+ "homepage_uri" => "https://railsautoscale.com",
23
+ "bug_tracker_uri" => "https://github.com/adamlogic/rails_autoscale_agent/issues",
24
+ "documentation_uri" => "https://railsautoscale.com/docs",
25
+ "changelog_uri" => "https://github.com/adamlogic/rails_autoscale_agent/blob/master/CHANGELOG.md",
26
+ "source_code_uri" => "https://github.com/adamlogic/rails_autoscale_agent",
27
+ }
18
28
  end
metadata CHANGED
@@ -1,16 +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
4
+ version: 0.11.0
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-07-13 00:00:00.000000000 Z
11
+ date: 2022-01-05 00:00:00.000000000 Z
12
12
  dependencies: []
13
- description:
13
+ description:
14
14
  email:
15
15
  - adam@adamlogic.com
16
16
  executables: []
@@ -19,9 +19,9 @@ extra_rdoc_files: []
19
19
  files:
20
20
  - ".gitignore"
21
21
  - ".rspec"
22
- - ".ruby-version"
23
22
  - ".travis.yml"
24
23
  - ".vscode/tasks.json"
24
+ - CHANGELOG.md
25
25
  - Gemfile
26
26
  - LICENSE.txt
27
27
  - README.md
@@ -48,11 +48,16 @@ files:
48
48
  - lib/rails_autoscale_agent/worker_adapters/sidekiq.rb
49
49
  - log/.gitkeep
50
50
  - rails_autoscale_agent.gemspec
51
- homepage: https://github.com/adamlogic/rails_autoscale_agent
51
+ homepage: https://railsautoscale.com
52
52
  licenses:
53
53
  - MIT
54
- metadata: {}
55
- post_install_message:
54
+ metadata:
55
+ homepage_uri: https://railsautoscale.com
56
+ bug_tracker_uri: https://github.com/adamlogic/rails_autoscale_agent/issues
57
+ documentation_uri: https://railsautoscale.com/docs
58
+ changelog_uri: https://github.com/adamlogic/rails_autoscale_agent/blob/master/CHANGELOG.md
59
+ source_code_uri: https://github.com/adamlogic/rails_autoscale_agent
60
+ post_install_message:
56
61
  rdoc_options: []
57
62
  require_paths:
58
63
  - lib
@@ -60,16 +65,16 @@ required_ruby_version: !ruby/object:Gem::Requirement
60
65
  requirements:
61
66
  - - ">="
62
67
  - !ruby/object:Gem::Version
63
- version: '0'
68
+ version: 2.5.0
64
69
  required_rubygems_version: !ruby/object:Gem::Requirement
65
70
  requirements:
66
71
  - - ">="
67
72
  - !ruby/object:Gem::Version
68
73
  version: '0'
69
74
  requirements: []
70
- rubygems_version: 3.1.4
71
- signing_key:
75
+ rubygems_version: 3.2.15
76
+ signing_key:
72
77
  specification_version: 4
73
78
  summary: This gem works with the Rails Autoscale Heroku add-on to automatically scale
74
- your web dynos.
79
+ your web and worker dynos.
75
80
  test_files: []
data/.ruby-version DELETED
@@ -1 +0,0 @@
1
- 2.6.5