rails_autoscale_agent 0.8.3 → 0.9.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: 3924fc840320be8327c183e1cb54055d8516d2fefee1cffbfa7092da39b05985
4
- data.tar.gz: abd81e6c4c2dc3a65be7c426dda8742ec92b7c29c47a71eeb2566731436ce4e6
3
+ metadata.gz: cd7ffb8cd27de3a23d5e76bd9a9c7768ce1fd397389291285022523fb398ee45
4
+ data.tar.gz: d8cf3fa7ab2994ef196742fe872170c069dc6f14bc0490b595ff175e48ce59e6
5
5
  SHA512:
6
- metadata.gz: 4867868c9e9e707d9e4afe46f20d19f600a87b26aabf5a96f4ad8ff91698d00b08f8a0566ef20fe8f3f1c036c9220b1034a92c70280a4cf29a6d5b5a728b4105
7
- data.tar.gz: e2fbbc4afa90c0c636dddb3172d1164108e984f4c550739df552cd185a8a51d7255ff354debc658ecfa893fb4f87f53e6c298bfe4dddf2aa35615f7636e64420
6
+ metadata.gz: '0976e83784dd6a622b18a67491e727cbe9f0fb1a54e1974f50b61f441f642a296b5a4e3349e2f4d292acc35c844765527c16e097cf7ac914a7224b68e2ae8bd6'
7
+ data.tar.gz: 2de7d8a61d44fd408cfbab88b1be37327bd5836da8c125f4c61f80ac7503c0a44c5ee5f893919eb5487e4a34a817102a68aabd76699d34d6ce3086680b8aceb7
@@ -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
  },
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
@@ -24,16 +24,6 @@ The agent will only communicate with Rails Autoscale if a `RAILS_AUTOSCALE_URL`
24
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
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
- ```
36
-
37
27
  ## What data is collected?
38
28
 
39
29
  The middleware agent runs in its own thread so your web requests are not impacted. The following data is submitted periodically to the Rails Autoscale API:
@@ -59,22 +49,44 @@ In production, run `heroku logs -t | grep RailsAutoscale`, and you should see so
59
49
 
60
50
  If you don't see either of these, try running `bundle` again and restarting your Rails application.
61
51
 
62
- You can see more detailed (debug) logging by setting the `RAILS_AUTOSCALE_DEBUG` env var on your Heroku app:
52
+ You can see more detailed (debug) logging by setting `RAILS_AUTOSCALE_DEBUG` on your Heroku app:
63
53
 
64
54
  ```
65
55
  heroku config:add RAILS_AUTOSCALE_DEBUG=true
66
56
  ```
67
57
 
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.
58
+ See more in the [logging](#logging) section below.
70
59
 
71
60
  Reach out to help@railsautoscale.com if you run into any other problems.
72
61
 
62
+ ## Logging
63
+
64
+ The Rails logger is used by default.
65
+ If you wish to use a different logger you can set it on the configuration object:
66
+
67
+ ```ruby
68
+ # config/initializers/rails_autoscale_agent.rb
69
+ RailsAutoscaleAgent::Config.instance.logger = MyLogger.new
70
+ ```
71
+
72
+ 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:
73
+
74
+ ```
75
+ heroku config:add RAILS_AUTOSCALE_DEBUG=true
76
+ ```
77
+
78
+ If you find the gem too chatty even without this, you can quiet it down further:
79
+
80
+ ```ruby
81
+ # config/initializers/rails_autoscale_agent.rb
82
+ RailsAutoscaleAgent::Config.instance.quiet = true
83
+ ```
84
+
73
85
  ## Development
74
86
 
75
87
  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.
76
88
 
77
- To install this gem onto your local machine, run `bundle exec rake install`. To release a new version, update the version number in `version.rb`, and then run `bundle exec rake release`, which will create a git tag for the version, push git commits and tags, and push the `.gem` file to [rubygems.org](https://rubygems.org).
89
+ To install this gem onto your local machine, run `bundle exec rake install`. To release a new version, update the version number in `version.rb`, commit it, and then run `bundle exec rake release`, which will create a git tag for the version, push git commits and tags, and push the `.gem` file to [rubygems.org](https://rubygems.org).
78
90
 
79
91
  ## Contributing
80
92
 
@@ -11,8 +11,8 @@ module RailsAutoscaleAgent
11
11
 
12
12
  SUCCESS = 'success'
13
13
 
14
- def initialize(api_url_base)
15
- @api_url_base = api_url_base
14
+ def initialize(config)
15
+ @config = config
16
16
  end
17
17
 
18
18
  def report_metrics!(report_params, timings_csv)
@@ -24,6 +24,10 @@ module RailsAutoscaleAgent
24
24
  post_json '/registrations', registration: registration_params
25
25
  end
26
26
 
27
+ def report_exception!(ex)
28
+ post_json '/exceptions', message: ex.inspect, backtrace: ex.backtrace.join("\n")
29
+ end
30
+
27
31
  private
28
32
 
29
33
  def post_json(path, data)
@@ -37,9 +41,14 @@ module RailsAutoscaleAgent
37
41
  end
38
42
 
39
43
  def post_raw(options)
40
- uri = URI.parse("#{@api_url_base}#{options.fetch(:path)}")
44
+ uri = URI.parse("#{@config.api_base_url}#{options.fetch(:path)}")
41
45
  ssl = uri.scheme == 'https'
42
46
 
47
+ if @config.dev_mode
48
+ logger.debug "[DEV_MODE] Skipping request to #{uri}"
49
+ return SuccessResponse.new('{}')
50
+ end
51
+
43
52
  response = Net::HTTP.start(uri.host, uri.port, use_ssl: ssl) do |http|
44
53
  request = Net::HTTP::Post.new(uri.request_uri, options[:headers] || {})
45
54
  request.body = options.fetch(:body)
@@ -7,26 +7,30 @@ module RailsAutoscaleAgent
7
7
  include Singleton
8
8
 
9
9
  attr_accessor :report_interval, :logger, :api_base_url, :max_request_size,
10
- :dyno, :pid, :addon_name, :worker_adapters
10
+ :dyno, :pid, :addon_name, :worker_adapters, :dev_mode, :debug, :quiet
11
11
 
12
12
  def initialize
13
13
  require 'rails_autoscale_agent/worker_adapters/sidekiq'
14
14
  require 'rails_autoscale_agent/worker_adapters/delayed_job'
15
15
  require 'rails_autoscale_agent/worker_adapters/que'
16
+ require 'rails_autoscale_agent/worker_adapters/resque'
16
17
  @worker_adapters = [
17
18
  WorkerAdapters::Sidekiq.instance,
18
19
  WorkerAdapters::DelayedJob.instance,
19
20
  WorkerAdapters::Que.instance,
21
+ WorkerAdapters::Resque.instance,
20
22
  ]
21
23
 
22
24
  # Allow the add-on name to be configured - needed for testing
23
25
  @addon_name = ENV['RAILS_AUTOSCALE_ADDON'] || 'RAILS_AUTOSCALE'
24
26
  @api_base_url = ENV["#{@addon_name}_URL"]
27
+ @dev_mode = ENV['RAILS_AUTOSCALE_DEV'] == 'true'
28
+ @debug = dev_mode? || ENV['RAILS_AUTOSCALE_DEBUG'] == 'true'
25
29
  @pid = Process.pid
26
30
  @max_request_size = 100_000 # ignore request payloads over 100k since they skew the queue times
27
- @report_interval = 60 # this default will be overwritten during Reporter#register!
31
+ @report_interval = 10 # this default will be overwritten during Reporter#register!
28
32
  @logger ||= defined?(Rails) ? Rails.logger : ::Logger.new(STDOUT)
29
- @dyno = ENV['DYNO']
33
+ @dyno = dev_mode? ? 'dev.1' : ENV['DYNO']
30
34
  end
31
35
 
32
36
  def to_s
@@ -37,5 +41,8 @@ module RailsAutoscaleAgent
37
41
  @max_request_size
38
42
  end
39
43
 
44
+ alias_method :dev_mode?, :dev_mode
45
+ alias_method :debug?, :debug
46
+ alias_method :quiet?, :quiet
40
47
  end
41
48
  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'
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
@@ -1,9 +1,11 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module RailsAutoscaleAgent
4
- class Measurement < Struct.new(:time, :value, :queue_name)
5
- def initialize(time, value, queue_name = nil)
6
- super time.utc, value.to_i, queue_name
4
+ class Measurement < Struct.new(:time, :value, :queue_name, :metric)
5
+ # No queue_name is assumed to be a web request measurement
6
+ # Metrics: qt = queue time (default), qd = queue depth (needed for Resque support)
7
+ def initialize(time, value, queue_name = nil, metric = nil)
8
+ super time.utc, value.to_i, queue_name, metric
7
9
  end
8
10
  end
9
11
  end
@@ -3,7 +3,7 @@
3
3
  require 'rails_autoscale_agent/version'
4
4
 
5
5
  module RailsAutoscaleAgent
6
- class Registration < Struct.new(:config)
6
+ class Registration < Struct.new(:config, :worker_adapters)
7
7
 
8
8
  def to_params
9
9
  {
@@ -12,6 +12,8 @@ module RailsAutoscaleAgent
12
12
  ruby_version: RUBY_VERSION,
13
13
  rails_version: defined?(Rails) && Rails.version,
14
14
  gem_version: VERSION,
15
+ # example: { worker_adapters: 'Sidekiq,Que' }
16
+ worker_adapters: worker_adapters.map { |o| o.class.name.split('::').last }.join(','),
15
17
  }
16
18
  end
17
19
  end
@@ -19,14 +19,12 @@ module RailsAutoscaleAgent
19
19
  def to_csv
20
20
  String.new.tap do |result|
21
21
  @measurements.each do |measurement|
22
- result << measurement.time.to_i.to_s
23
- result << ','
24
- result << measurement.value.to_s
25
-
26
- if measurement.queue_name
27
- result << ','
28
- result << measurement.queue_name
29
- end
22
+ result << [
23
+ measurement.time.to_i,
24
+ measurement.value,
25
+ measurement.queue_name,
26
+ measurement.metric,
27
+ ].join(',')
30
28
 
31
29
  result << "\n"
32
30
  end
@@ -21,14 +21,14 @@ module RailsAutoscaleAgent
21
21
  @started = true
22
22
  @worker_adapters = config.worker_adapters.select(&:enabled?)
23
23
 
24
- if !config.api_base_url
24
+ if !config.api_base_url && !config.dev_mode?
25
25
  logger.info "Reporter not started: #{config.addon_name}_URL is not set"
26
26
  return
27
27
  end
28
28
 
29
29
  Thread.new do
30
30
  loop do
31
- register!(config) unless @registered
31
+ register!(config, @worker_adapters) unless @registered
32
32
 
33
33
  # Stagger reporting to spread out reports from many processes
34
34
  multiplier = 1 - (rand / 4) # between 0.75 and 1.0
@@ -41,7 +41,7 @@ module RailsAutoscaleAgent
41
41
  # Exceptions in threads other than the main thread will fail silently
42
42
  # https://ruby-doc.org/core-2.2.0/Thread.html#class-Thread-label-Exception+handling
43
43
  logger.error "Reporter error: #{ex.inspect}"
44
- logger.error ex.backtrace.join("\n")
44
+ AutoscaleApi.new(config.api_base_url).report_exception!(ex)
45
45
  end
46
46
  end
47
47
  end
@@ -58,7 +58,7 @@ module RailsAutoscaleAgent
58
58
  logger.info "Reporting #{report.measurements.size} measurements"
59
59
 
60
60
  params = report.to_params(config)
61
- result = AutoscaleApi.new(config.api_base_url).report_metrics!(params, report.to_csv)
61
+ result = AutoscaleApi.new(config).report_metrics!(params, report.to_csv)
62
62
 
63
63
  case result
64
64
  when AutoscaleApi::SuccessResponse
@@ -71,16 +71,16 @@ module RailsAutoscaleAgent
71
71
  end
72
72
  end
73
73
 
74
- def register!(config)
75
- params = Registration.new(config).to_params
76
- result = AutoscaleApi.new(config.api_base_url).register_reporter!(params)
74
+ def register!(config, worker_adapters)
75
+ params = Registration.new(config, worker_adapters).to_params
76
+ result = AutoscaleApi.new(config).register_reporter!(params)
77
77
 
78
78
  case result
79
79
  when AutoscaleApi::SuccessResponse
80
80
  @registered = true
81
81
  config.report_interval = result.data['report_interval'] if result.data['report_interval']
82
82
  config.max_request_size = result.data['max_request_size'] if result.data['max_request_size']
83
- worker_adapters_msg = @worker_adapters.map { |a| a.class.name }.join(', ')
83
+ worker_adapters_msg = worker_adapters.map { |a| a.class.name }.join(', ')
84
84
  logger.info "Reporter starting, will report every #{config.report_interval} seconds or so. Worker adapters: [#{worker_adapters_msg}]"
85
85
  when AutoscaleApi::FailureResponse
86
86
  logger.error "Reporter failed to register: #{result.failure_message}"
@@ -4,17 +4,17 @@ module RailsAutoscaleAgent
4
4
  class Request
5
5
  include Logger
6
6
 
7
- attr_reader :id, :entered_queue_at, :path, :method, :size
8
-
9
7
  def initialize(env, config)
10
8
  @config = config
11
9
  @id = env['HTTP_X_REQUEST_ID']
12
- @path = env['PATH_INFO']
13
- @method = env['REQUEST_METHOD'].downcase
14
10
  @size = env['rack.input'].respond_to?(:size) ? env['rack.input'].size : 0
11
+ @request_body_wait = env['puma.request_body_wait'].to_i
15
12
 
16
- if unix_millis = env['HTTP_X_REQUEST_START']
17
- @entered_queue_at = Time.at(unix_millis.to_f / 1000)
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?
16
+ # In dev mode, fake a queue time of 0-1000ms
17
+ Time.now - rand + @request_body_wait
18
18
  end
19
19
  end
20
20
 
@@ -23,12 +23,17 @@ module RailsAutoscaleAgent
23
23
  end
24
24
 
25
25
  def queue_time
26
- if entered_queue_at
27
- queue_time = ((Time.now - entered_queue_at) * 1000).to_i
28
- queue_time = 0 if queue_time < 0
29
- logger.debug "Collected queue_time=#{queue_time}ms request_id=#{id} request_size=#{size}"
26
+ if @entered_queue_at
27
+ queue_time = ((Time.now - @entered_queue_at) * 1000).to_i
28
+
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
32
+
33
+ logger.debug "Request queue_time=#{queue_time}ms body_wait=#{@request_body_wait}ms request_id=#{@id} size=#{@size}"
30
34
 
31
- queue_time
35
+ # Safeguard against negative queue times (should not happen in practice)
36
+ queue_time > 0 ? queue_time : 0
32
37
  end
33
38
  end
34
39
  end
@@ -15,8 +15,8 @@ module RailsAutoscaleAgent
15
15
  @measurements = []
16
16
  end
17
17
 
18
- def push(value, time = Time.now, queue_name = nil)
19
- @measurements << Measurement.new(time, value, queue_name)
18
+ def push(value, time = Time.now, queue_name = nil, metric = nil)
19
+ @measurements << Measurement.new(time, value, queue_name, metric)
20
20
  end
21
21
 
22
22
  def pop_report
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module RailsAutoscaleAgent
4
- VERSION = "0.8.3"
4
+ VERSION = "0.9.0"
5
5
  end
@@ -2,63 +2,56 @@
2
2
 
3
3
  require 'rails_autoscale_agent/logger'
4
4
 
5
- module WorkerAdapters
6
- class DelayedJob
7
- include RailsAutoscaleAgent::Logger
8
- include Singleton
9
-
10
- class << self
11
- attr_accessor :queues
12
- end
13
-
14
- def initialize
15
- # Track the known queues so we can continue reporting on queues that don't
16
- # currently have enqueued jobs.
17
- self.class.queues = Set.new
18
-
19
- install if enabled?
20
- end
21
-
22
- def enabled?
23
- defined? ::Delayed
24
- end
25
-
26
- def collect!(store)
27
- log_msg = String.new('DelayedJob latency ')
28
- t = Time.now
29
-
30
- # Ignore failed jobs (they skew latency measurement due to the original run_at)
31
- sql = 'SELECT queue, min(run_at) FROM delayed_jobs WHERE attempts = 0 GROUP BY queue'
32
- run_at_by_queue = Hash[ActiveRecord::Base.connection.select_rows(sql)]
33
- queues = self.class.queues | run_at_by_queue.keys
34
-
35
- queues.each do |queue|
36
- next if queue.nil? || queue.empty?
37
- run_at = run_at_by_queue[queue]
38
- run_at = Time.parse(run_at) if run_at.is_a?(String)
39
- latency_ms = run_at ? ((t - run_at)*1000).ceil : 0
40
- store.push latency_ms, t, queue
41
- log_msg << "#{queue}=#{latency_ms} "
5
+ module RailsAutoscaleAgent
6
+ module WorkerAdapters
7
+ class DelayedJob
8
+ include RailsAutoscaleAgent::Logger
9
+ include Singleton
10
+
11
+ attr_writer :queues
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'])
42
19
  end
43
20
 
44
- logger.debug log_msg
45
- end
46
-
47
- private
48
-
49
- def install
50
- plugin = Class.new(Delayed::Plugin) do
51
- require 'delayed_job'
52
-
53
- callbacks do |lifecycle|
54
- lifecycle.before(:enqueue) do |job, &block|
55
- queue = job.queue || 'default'
56
- WorkerAdapters::DelayedJob.queues.add queue
57
- end
21
+ def enabled?
22
+ if defined?(::Delayed::Job) && defined?(::Delayed::Backend::ActiveRecord)
23
+ logger.info "DelayedJob enabled (#{::ActiveRecord::Base.default_timezone})"
24
+ true
58
25
  end
59
26
  end
60
27
 
61
- Delayed::Worker.plugins << plugin
28
+ def collect!(store)
29
+ log_msg = String.new
30
+ t = Time.now.utc
31
+ sql = <<~SQL
32
+ SELECT COALESCE(queue, 'default'), min(run_at)
33
+ FROM delayed_jobs
34
+ WHERE locked_at IS NULL
35
+ AND failed_at IS NULL
36
+ GROUP BY queue
37
+ SQL
38
+
39
+ run_at_by_queue = Hash[ActiveRecord::Base.connection.select_rows(sql)]
40
+ self.queues |= run_at_by_queue.keys
41
+
42
+ queues.each do |queue|
43
+ run_at = run_at_by_queue[queue]
44
+ # DateTime.parse assumes a UTC string
45
+ run_at = DateTime.parse(run_at) if run_at.is_a?(String)
46
+ latency_ms = run_at ? ((t - run_at)*1000).ceil : 0
47
+ latency_ms = 0 if latency_ms < 0
48
+
49
+ store.push latency_ms, t, queue
50
+ log_msg << "dj.#{queue}=#{latency_ms} "
51
+ end
52
+
53
+ logger.debug log_msg unless log_msg.empty?
54
+ end
62
55
  end
63
56
  end
64
57
  end
@@ -1,46 +1,57 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require 'rails_autoscale_agent/logger'
4
- require 'time'
5
4
 
6
- module WorkerAdapters
7
- class Que
8
- include RailsAutoscaleAgent::Logger
9
- include Singleton
5
+ module RailsAutoscaleAgent
6
+ module WorkerAdapters
7
+ class Que
8
+ include RailsAutoscaleAgent::Logger
9
+ include Singleton
10
10
 
11
- DEFAULT_QUEUES = ['default']
11
+ attr_writer :queues
12
12
 
13
- class << self
14
- attr_accessor :queues
15
- end
16
-
17
- def initialize
18
- self.class.queues = DEFAULT_QUEUES
19
- end
20
-
21
- def enabled?
22
- defined? ::Que
23
- end
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
24
20
 
25
- def collect!(store)
26
- log_msg = String.new('Que latency ')
27
- t = Time.now
28
-
29
- # Ignore failed jobs (they skew latency measurement due to the original run_at)
30
- sql = 'SELECT queue, min(run_at) FROM que_jobs WHERE error_count = 0 GROUP BY queue'
31
- run_at_by_queue = Hash[ActiveRecord::Base.connection.select_rows(sql)]
32
- self.class.queues |= run_at_by_queue.keys
33
-
34
- self.class.queues.each do |queue|
35
- next if queue.nil? || queue.empty?
36
- run_at = run_at_by_queue[queue]
37
- run_at = Time.parse(run_at) if run_at.is_a?(String)
38
- latency_ms = run_at ? ((t - run_at)*1000).ceil : 0
39
- store.push latency_ms, t, queue
40
- log_msg << "#{queue}=#{latency_ms} "
21
+ def enabled?
22
+ if defined?(::Que)
23
+ logger.info "Que enabled (#{::ActiveRecord::Base.default_timezone})"
24
+ true
25
+ end
41
26
  end
42
27
 
43
- logger.debug log_msg
28
+ def collect!(store)
29
+ log_msg = String.new
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[ActiveRecord::Base.connection.select_rows(sql)]
41
+ self.queues |= run_at_by_queue.keys
42
+
43
+ queues.each do |queue|
44
+ run_at = run_at_by_queue[queue]
45
+ run_at = DateTime.parse(run_at) if run_at.is_a?(String)
46
+ latency_ms = run_at ? ((t - run_at)*1000).ceil : 0
47
+ latency_ms = 0 if latency_ms < 0
48
+
49
+ store.push latency_ms, t, queue
50
+ log_msg << "que.#{queue}=#{latency_ms} "
51
+ end
52
+
53
+ logger.debug log_msg unless log_msg.empty?
54
+ end
44
55
  end
45
56
  end
46
57
  end
@@ -0,0 +1,43 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'rails_autoscale_agent/logger'
4
+
5
+ module RailsAutoscaleAgent
6
+ module WorkerAdapters
7
+ class Resque
8
+ include RailsAutoscaleAgent::Logger
9
+ include Singleton
10
+
11
+ attr_writer :queues
12
+
13
+ def queues
14
+ @queues ||= ['default']
15
+ end
16
+
17
+ def enabled?
18
+ require 'resque'
19
+ logger.info "Resque enabled"
20
+ true
21
+ rescue LoadError
22
+ false
23
+ end
24
+
25
+ def collect!(store)
26
+ log_msg = String.new
27
+
28
+ # Ensure we continue to collect metrics for known queue names, even when nothing is
29
+ # enqueued at the time. Without this, it will appears that the agent is no longer reporting.
30
+ self.queues |= ::Resque.queues
31
+
32
+ queues.each do |queue|
33
+ next if queue.nil? || queue.empty?
34
+ depth = ::Resque.size(queue)
35
+ store.push depth, Time.now, queue, :qd
36
+ log_msg << "resque-qd.#{queue}=#{depth} "
37
+ end
38
+
39
+ logger.debug log_msg
40
+ end
41
+ end
42
+ end
43
+ end
@@ -2,28 +2,49 @@
2
2
 
3
3
  require 'rails_autoscale_agent/logger'
4
4
 
5
- module WorkerAdapters
6
- class Sidekiq
7
- include RailsAutoscaleAgent::Logger
8
- include Singleton
9
-
10
- def enabled?
11
- require 'sidekiq/api'
12
- true
13
- rescue LoadError
14
- false
15
- end
5
+ module RailsAutoscaleAgent
6
+ module WorkerAdapters
7
+ class Sidekiq
8
+ include RailsAutoscaleAgent::Logger
9
+ include Singleton
10
+
11
+ attr_writer :known_queue_names
16
12
 
17
- def collect!(store)
18
- log_msg = String.new('Sidekiq latency ')
13
+ def known_queue_names
14
+ @known_queue_names ||= ['default']
15
+ end
19
16
 
20
- ::Sidekiq::Queue.all.each do |queue|
21
- latency_ms = (queue.latency * 1000).ceil
22
- store.push latency_ms, Time.now, queue.name
23
- log_msg << "#{queue.name}=#{latency_ms} "
17
+ def enabled?
18
+ require 'sidekiq/api'
19
+ logger.info "Sidekiq enabled"
20
+ true
21
+ rescue LoadError
22
+ false
24
23
  end
25
24
 
26
- logger.debug log_msg
25
+ def collect!(store)
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
30
+
31
+ # 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|
34
+ queues_by_name[queue_name] ||= ::Sidekiq::Queue.new(queue_name)
35
+ end
36
+ self.known_queue_names = queues_by_name.keys
37
+
38
+ queues_by_name.each do |queue_name, queue|
39
+ latency_ms = (queue.latency * 1000).ceil
40
+ depth = queue.size
41
+ store.push latency_ms, Time.now, queue.name, :qt
42
+ store.push depth, Time.now, queue.name, :qd
43
+ log_msg << "sidekiq-qt.#{queue.name}=#{latency_ms} sidekiq-qd.#{queue.name}=#{depth} "
44
+ end
45
+
46
+ logger.debug log_msg
47
+ end
27
48
  end
28
49
  end
29
50
  end
@@ -15,16 +15,4 @@ Gem::Specification.new do |spec|
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.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"
30
18
  end
metadata CHANGED
@@ -1,169 +1,15 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: rails_autoscale_agent
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.8.3
4
+ version: 0.9.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Adam McCrea
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2020-05-26 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'
11
+ date: 2020-07-13 00:00:00.000000000 Z
12
+ dependencies: []
167
13
  description:
168
14
  email:
169
15
  - adam@adamlogic.com
@@ -198,6 +44,7 @@ files:
198
44
  - lib/rails_autoscale_agent/version.rb
199
45
  - lib/rails_autoscale_agent/worker_adapters/delayed_job.rb
200
46
  - lib/rails_autoscale_agent/worker_adapters/que.rb
47
+ - lib/rails_autoscale_agent/worker_adapters/resque.rb
201
48
  - lib/rails_autoscale_agent/worker_adapters/sidekiq.rb
202
49
  - log/.gitkeep
203
50
  - rails_autoscale_agent.gemspec
@@ -220,7 +67,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
220
67
  - !ruby/object:Gem::Version
221
68
  version: '0'
222
69
  requirements: []
223
- rubygems_version: 3.0.3
70
+ rubygems_version: 3.1.4
224
71
  signing_key:
225
72
  specification_version: 4
226
73
  summary: This gem works with the Rails Autoscale Heroku add-on to automatically scale