rails-autoscale-core 1.6.0 → 1.7.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 +4 -4
- metadata +20 -26
- data/Gemfile +0 -9
- data/Rakefile +0 -11
- data/judoscale-ruby.gemspec +0 -27
- data/lib/judoscale/adapter_api.rb +0 -75
- data/lib/judoscale/config.rb +0 -121
- data/lib/judoscale/job_metrics_collector/active_record_helper.rb +0 -56
- data/lib/judoscale/job_metrics_collector.rb +0 -104
- data/lib/judoscale/logger.rb +0 -45
- data/lib/judoscale/metric.rb +0 -11
- data/lib/judoscale/metrics_collector.rb +0 -13
- data/lib/judoscale/metrics_store.rb +0 -41
- data/lib/judoscale/report.rb +0 -30
- data/lib/judoscale/reporter.rb +0 -102
- data/lib/judoscale/request_metrics.rb +0 -61
- data/lib/judoscale/request_middleware.rb +0 -44
- data/lib/judoscale/version.rb +0 -5
- data/lib/judoscale/web_metrics_collector.rb +0 -12
- data/lib/judoscale-ruby.rb +0 -40
- data/rails-autoscale-core.gemspec +0 -27
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 4ab6318749147db00558b2d8a6f43a408dbfdc1984f27b67626649b451670a6f
|
4
|
+
data.tar.gz: 7d01e0e0a381efb7877449d970805fdd594c0a5c50b2adb5cca741082961474e
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: bd8223438b2ba92be8565545c68af0adcfce0aaa58adaa632ab796009f251875a60bf8006526b1ecd0595db8937fc10f3f8bb5919852aa5560bba65bb92cd946
|
7
|
+
data.tar.gz: 808c2e0be260e5040c55eea07caa4a0206981593c48fd4a10e63bff7d60984a13d83098ea94f931e683bfe3b9abe27a25c6e00628a89ab7ffabcb617178c29e8
|
metadata
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: rails-autoscale-core
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 1.
|
4
|
+
version: 1.7.1
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Adam McCrea
|
@@ -10,8 +10,22 @@ authors:
|
|
10
10
|
autorequire:
|
11
11
|
bindir: bin
|
12
12
|
cert_chain: []
|
13
|
-
date: 2024-
|
14
|
-
dependencies:
|
13
|
+
date: 2024-07-08 00:00:00.000000000 Z
|
14
|
+
dependencies:
|
15
|
+
- !ruby/object:Gem::Dependency
|
16
|
+
name: judoscale-ruby
|
17
|
+
requirement: !ruby/object:Gem::Requirement
|
18
|
+
requirements:
|
19
|
+
- - '='
|
20
|
+
- !ruby/object:Gem::Version
|
21
|
+
version: 1.7.1
|
22
|
+
type: :runtime
|
23
|
+
prerelease: false
|
24
|
+
version_requirements: !ruby/object:Gem::Requirement
|
25
|
+
requirements:
|
26
|
+
- - '='
|
27
|
+
- !ruby/object:Gem::Version
|
28
|
+
version: 1.7.1
|
15
29
|
description:
|
16
30
|
email:
|
17
31
|
- hello@judoscale.com
|
@@ -19,26 +33,7 @@ executables: []
|
|
19
33
|
extensions: []
|
20
34
|
extra_rdoc_files: []
|
21
35
|
files:
|
22
|
-
- Gemfile
|
23
|
-
- Rakefile
|
24
|
-
- judoscale-ruby.gemspec
|
25
|
-
- lib/judoscale-ruby.rb
|
26
|
-
- lib/judoscale/adapter_api.rb
|
27
|
-
- lib/judoscale/config.rb
|
28
|
-
- lib/judoscale/job_metrics_collector.rb
|
29
|
-
- lib/judoscale/job_metrics_collector/active_record_helper.rb
|
30
|
-
- lib/judoscale/logger.rb
|
31
|
-
- lib/judoscale/metric.rb
|
32
|
-
- lib/judoscale/metrics_collector.rb
|
33
|
-
- lib/judoscale/metrics_store.rb
|
34
|
-
- lib/judoscale/report.rb
|
35
|
-
- lib/judoscale/reporter.rb
|
36
|
-
- lib/judoscale/request_metrics.rb
|
37
|
-
- lib/judoscale/request_middleware.rb
|
38
|
-
- lib/judoscale/version.rb
|
39
|
-
- lib/judoscale/web_metrics_collector.rb
|
40
36
|
- lib/rails-autoscale-core.rb
|
41
|
-
- rails-autoscale-core.gemspec
|
42
37
|
homepage: https://judoscale.com
|
43
38
|
licenses:
|
44
39
|
- MIT
|
@@ -56,16 +51,15 @@ required_ruby_version: !ruby/object:Gem::Requirement
|
|
56
51
|
requirements:
|
57
52
|
- - ">="
|
58
53
|
- !ruby/object:Gem::Version
|
59
|
-
version:
|
54
|
+
version: '0'
|
60
55
|
required_rubygems_version: !ruby/object:Gem::Requirement
|
61
56
|
requirements:
|
62
57
|
- - ">="
|
63
58
|
- !ruby/object:Gem::Version
|
64
59
|
version: '0'
|
65
60
|
requirements: []
|
66
|
-
rubygems_version: 3.
|
61
|
+
rubygems_version: 3.5.11
|
67
62
|
signing_key:
|
68
63
|
specification_version: 4
|
69
|
-
summary:
|
70
|
-
web and worker dynos.
|
64
|
+
summary: Autoscaling for Ruby.
|
71
65
|
test_files: []
|
data/Gemfile
DELETED
data/Rakefile
DELETED
data/judoscale-ruby.gemspec
DELETED
@@ -1,27 +0,0 @@
|
|
1
|
-
lib = File.expand_path("../lib", __FILE__)
|
2
|
-
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
|
3
|
-
require "judoscale/version"
|
4
|
-
|
5
|
-
Gem::Specification.new do |spec|
|
6
|
-
spec.name = "judoscale-ruby"
|
7
|
-
spec.version = Judoscale::VERSION
|
8
|
-
spec.authors = ["Adam McCrea", "Carlos Antonio da Silva", "Jon Sullivan"]
|
9
|
-
spec.email = ["hello@judoscale.com"]
|
10
|
-
|
11
|
-
spec.summary = "This gem works with the Judoscale Heroku add-on to automatically scale your web and worker dynos."
|
12
|
-
spec.homepage = "https://judoscale.com"
|
13
|
-
spec.license = "MIT"
|
14
|
-
|
15
|
-
spec.metadata = {
|
16
|
-
"homepage_uri" => "https://judoscale.com",
|
17
|
-
"bug_tracker_uri" => "https://github.com/judoscale/judoscale-ruby/issues",
|
18
|
-
"documentation_uri" => "https://judoscale.com/docs",
|
19
|
-
"changelog_uri" => "https://github.com/judoscale/judoscale-ruby/blob/main/CHANGELOG.md",
|
20
|
-
"source_code_uri" => "https://github.com/judoscale/judoscale-ruby"
|
21
|
-
}
|
22
|
-
|
23
|
-
spec.files = `git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(test|spec|features)/}) }
|
24
|
-
spec.require_paths = ["lib"]
|
25
|
-
|
26
|
-
spec.required_ruby_version = ">= 2.6.0"
|
27
|
-
end
|
@@ -1,75 +0,0 @@
|
|
1
|
-
# frozen_string_literal: true
|
2
|
-
|
3
|
-
require "net/http"
|
4
|
-
require "uri"
|
5
|
-
require "json"
|
6
|
-
require "judoscale/logger"
|
7
|
-
|
8
|
-
module Judoscale
|
9
|
-
class AdapterApi
|
10
|
-
include Logger
|
11
|
-
|
12
|
-
SUCCESS = "success"
|
13
|
-
TRANSIENT_ERRORS = [
|
14
|
-
Errno::ECONNREFUSED,
|
15
|
-
Errno::ECONNRESET,
|
16
|
-
Net::OpenTimeout,
|
17
|
-
Net::ReadTimeout,
|
18
|
-
OpenSSL::SSL::SSLError
|
19
|
-
]
|
20
|
-
|
21
|
-
def initialize(config)
|
22
|
-
@config = config
|
23
|
-
end
|
24
|
-
|
25
|
-
def report_metrics(report_json)
|
26
|
-
post_json "/v3/reports", report_json
|
27
|
-
end
|
28
|
-
|
29
|
-
private
|
30
|
-
|
31
|
-
def post_json(path, data)
|
32
|
-
headers = {"Content-Type" => "application/json"}
|
33
|
-
post_raw path: path, body: JSON.dump(data), headers: headers
|
34
|
-
end
|
35
|
-
|
36
|
-
def post_raw(options)
|
37
|
-
attempts ||= 1
|
38
|
-
uri = URI.parse("#{@config.api_base_url}#{options.fetch(:path)}")
|
39
|
-
ssl = uri.scheme == "https"
|
40
|
-
|
41
|
-
response = Net::HTTP.start(uri.host, uri.port, use_ssl: ssl) do |http|
|
42
|
-
request = Net::HTTP::Post.new(uri.request_uri, options[:headers] || {})
|
43
|
-
request.body = options.fetch(:body)
|
44
|
-
|
45
|
-
logger.debug "Posting #{request.body.size} bytes to #{uri}"
|
46
|
-
http.request(request)
|
47
|
-
end
|
48
|
-
|
49
|
-
case response.code.to_i
|
50
|
-
when 200...300 then SuccessResponse.new(response.body)
|
51
|
-
else FailureResponse.new([response.code, response.message].join(" - "))
|
52
|
-
end
|
53
|
-
rescue *TRANSIENT_ERRORS => ex
|
54
|
-
if attempts < 3
|
55
|
-
# TCP timeouts happen sometimes, but they can usually be successfully retried in a moment
|
56
|
-
sleep 0.01
|
57
|
-
attempts += 1
|
58
|
-
retry
|
59
|
-
else
|
60
|
-
FailureResponse.new("Could not connect to #{uri.host}: #{ex.inspect}")
|
61
|
-
end
|
62
|
-
end
|
63
|
-
|
64
|
-
class SuccessResponse < Struct.new(:body)
|
65
|
-
def data
|
66
|
-
JSON.parse(body)
|
67
|
-
rescue TypeError
|
68
|
-
{}
|
69
|
-
end
|
70
|
-
end
|
71
|
-
|
72
|
-
class FailureResponse < Struct.new(:failure_message)
|
73
|
-
end
|
74
|
-
end
|
75
|
-
end
|
data/lib/judoscale/config.rb
DELETED
@@ -1,121 +0,0 @@
|
|
1
|
-
# frozen_string_literal: true
|
2
|
-
|
3
|
-
require "singleton"
|
4
|
-
require "logger"
|
5
|
-
|
6
|
-
module Judoscale
|
7
|
-
class Config
|
8
|
-
class RuntimeContainer < String
|
9
|
-
# Since Heroku exposes ordinal dyno 'numbers', we can tell if the current
|
10
|
-
# instance is redundant (and thus skip collecting some metrics sometimes)
|
11
|
-
# We don't have a means of determining that on Render though — so every
|
12
|
-
# instance must be considered non-redundant
|
13
|
-
def redundant_instance?
|
14
|
-
instance_number = split(".")[1].to_i
|
15
|
-
instance_number > 1
|
16
|
-
end
|
17
|
-
end
|
18
|
-
|
19
|
-
class JobAdapterConfig
|
20
|
-
UUID_REGEXP = /[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}/
|
21
|
-
DEFAULT_QUEUE_FILTER = ->(queue_name) { !UUID_REGEXP.match?(queue_name) }
|
22
|
-
|
23
|
-
attr_accessor :identifier, :enabled, :max_queues, :queues, :queue_filter, :track_busy_jobs
|
24
|
-
|
25
|
-
def initialize(identifier)
|
26
|
-
@identifier = identifier
|
27
|
-
reset
|
28
|
-
end
|
29
|
-
|
30
|
-
def reset
|
31
|
-
@enabled = true
|
32
|
-
@queues = []
|
33
|
-
@queue_filter = DEFAULT_QUEUE_FILTER
|
34
|
-
|
35
|
-
# Support for deprecated legacy env var configs.
|
36
|
-
@max_queues = (ENV["RAILS_AUTOSCALE_MAX_QUEUES"] || 20).to_i
|
37
|
-
@track_busy_jobs = ENV["RAILS_AUTOSCALE_LONG_JOBS"] == "true"
|
38
|
-
end
|
39
|
-
|
40
|
-
def as_json
|
41
|
-
{
|
42
|
-
identifier => {
|
43
|
-
max_queues: max_queues,
|
44
|
-
queues: queues,
|
45
|
-
queue_filter: queue_filter != DEFAULT_QUEUE_FILTER,
|
46
|
-
track_busy_jobs: track_busy_jobs
|
47
|
-
}
|
48
|
-
}
|
49
|
-
end
|
50
|
-
end
|
51
|
-
|
52
|
-
include Singleton
|
53
|
-
|
54
|
-
@adapter_configs = []
|
55
|
-
class << self
|
56
|
-
attr_reader :adapter_configs
|
57
|
-
end
|
58
|
-
|
59
|
-
def self.expose_adapter_config(config_instance)
|
60
|
-
adapter_configs << config_instance
|
61
|
-
|
62
|
-
define_method(config_instance.identifier) do
|
63
|
-
config_instance
|
64
|
-
end
|
65
|
-
end
|
66
|
-
|
67
|
-
attr_accessor :api_base_url, :report_interval_seconds,
|
68
|
-
:max_request_size_bytes, :logger, :log_tag, :current_runtime_container
|
69
|
-
attr_reader :log_level
|
70
|
-
|
71
|
-
def initialize
|
72
|
-
reset
|
73
|
-
end
|
74
|
-
|
75
|
-
def reset
|
76
|
-
@api_base_url = ENV["JUDOSCALE_URL"] || ENV["RAILS_AUTOSCALE_URL"]
|
77
|
-
@log_tag = "Judoscale"
|
78
|
-
@max_request_size_bytes = 100_000 # ignore request payloads over 100k since they skew the queue times
|
79
|
-
@report_interval_seconds = 10
|
80
|
-
|
81
|
-
self.log_level = ENV["JUDOSCALE_LOG_LEVEL"] || ENV["RAILS_AUTOSCALE_LOG_LEVEL"]
|
82
|
-
@logger = ::Logger.new($stdout)
|
83
|
-
|
84
|
-
self.class.adapter_configs.each(&:reset)
|
85
|
-
|
86
|
-
if ENV["RENDER_INSTANCE_ID"]
|
87
|
-
instance = ENV["RENDER_INSTANCE_ID"].delete_prefix(ENV["RENDER_SERVICE_ID"]).delete_prefix("-")
|
88
|
-
@current_runtime_container = RuntimeContainer.new instance
|
89
|
-
# Allow a custom API base URL to be set for Render (for testing)
|
90
|
-
@api_base_url ||= "https://adapter.judoscale.com/api"
|
91
|
-
@api_base_url += "/#{ENV["RENDER_SERVICE_ID"]}"
|
92
|
-
elsif ENV["DYNO"]
|
93
|
-
@current_runtime_container = RuntimeContainer.new ENV["DYNO"]
|
94
|
-
elsif (metadata_uri = ENV["ECS_CONTAINER_METADATA_URI"])
|
95
|
-
@current_runtime_container = RuntimeContainer.new(metadata_uri.split("/").last)
|
96
|
-
else
|
97
|
-
# unsupported platform? Don't want to leave @current_runtime_container nil though
|
98
|
-
@current_runtime_container = RuntimeContainer.new("")
|
99
|
-
end
|
100
|
-
end
|
101
|
-
|
102
|
-
def log_level=(new_level)
|
103
|
-
@log_level = new_level ? ::Logger::Severity.const_get(new_level.to_s.upcase) : nil
|
104
|
-
end
|
105
|
-
|
106
|
-
def as_json
|
107
|
-
adapter_configs_json = self.class.adapter_configs.reduce({}) { |hash, config| hash.merge!(config.as_json) }
|
108
|
-
|
109
|
-
{
|
110
|
-
log_level: log_level,
|
111
|
-
logger: logger.class.name,
|
112
|
-
report_interval_seconds: report_interval_seconds,
|
113
|
-
max_request_size_bytes: max_request_size_bytes
|
114
|
-
}.merge!(adapter_configs_json)
|
115
|
-
end
|
116
|
-
|
117
|
-
def ignore_large_requests?
|
118
|
-
@max_request_size_bytes
|
119
|
-
end
|
120
|
-
end
|
121
|
-
end
|
@@ -1,56 +0,0 @@
|
|
1
|
-
# frozen_string_literal: true
|
2
|
-
|
3
|
-
module Judoscale
|
4
|
-
class JobMetricsCollector
|
5
|
-
module ActiveRecordHelper
|
6
|
-
# Cleanup any whitespace characters (including new lines) from the SQL for simpler logging.
|
7
|
-
# Reference: ActiveSupport's `squish!` method. https://api.rubyonrails.org/classes/String.html#method-i-squish
|
8
|
-
def self.cleanse_sql(sql)
|
9
|
-
sql = sql.dup
|
10
|
-
sql.gsub!(/[[:space:]]+/, " ")
|
11
|
-
sql.strip!
|
12
|
-
sql
|
13
|
-
end
|
14
|
-
|
15
|
-
# This will respect a multiple-database setup, unlike the `table_exists?` method.
|
16
|
-
def self.table_exists_for_model?(model)
|
17
|
-
model.connection.schema_cache.data_source_exists?(model.table_name)
|
18
|
-
rescue ActiveRecord::NoDatabaseError
|
19
|
-
false
|
20
|
-
end
|
21
|
-
|
22
|
-
def self.table_exists?(table_name)
|
23
|
-
::ActiveRecord::Base.connection.table_exists?(table_name)
|
24
|
-
rescue ActiveRecord::NoDatabaseError
|
25
|
-
false
|
26
|
-
end
|
27
|
-
|
28
|
-
private
|
29
|
-
|
30
|
-
def run_silently(&block)
|
31
|
-
if Config.instance.log_level && ::ActiveRecord::Base.logger.respond_to?(:silence)
|
32
|
-
::ActiveRecord::Base.logger.silence(Config.instance.log_level) { yield }
|
33
|
-
else
|
34
|
-
yield
|
35
|
-
end
|
36
|
-
end
|
37
|
-
|
38
|
-
def select_rows_silently(sql)
|
39
|
-
run_silently { select_rows_tagged(sql) }
|
40
|
-
end
|
41
|
-
|
42
|
-
def select_rows_tagged(sql)
|
43
|
-
if ActiveRecord::Base.logger.respond_to?(:tagged)
|
44
|
-
ActiveRecord::Base.logger.tagged(Config.instance.log_tag) { select_rows(sql) }
|
45
|
-
else
|
46
|
-
select_rows(sql)
|
47
|
-
end
|
48
|
-
end
|
49
|
-
|
50
|
-
def select_rows(sql)
|
51
|
-
# This ensures the agent doesn't hold onto a DB connection any longer than necessary
|
52
|
-
ActiveRecord::Base.connection_pool.with_connection { |c| c.select_rows(sql) }
|
53
|
-
end
|
54
|
-
end
|
55
|
-
end
|
56
|
-
end
|
@@ -1,104 +0,0 @@
|
|
1
|
-
# frozen_string_literal: true
|
2
|
-
|
3
|
-
require "set"
|
4
|
-
require "judoscale/metrics_collector"
|
5
|
-
require "judoscale/logger"
|
6
|
-
|
7
|
-
module Judoscale
|
8
|
-
class JobMetricsCollector < MetricsCollector
|
9
|
-
include Judoscale::Logger
|
10
|
-
|
11
|
-
def self.collect?(config)
|
12
|
-
super && !config.current_runtime_container.redundant_instance? && adapter_config.enabled
|
13
|
-
end
|
14
|
-
|
15
|
-
def self.adapter_name
|
16
|
-
@_adapter_name ||= adapter_identifier.to_s.capitalize.gsub(/(?:_)(.)/i) { $1.upcase }
|
17
|
-
end
|
18
|
-
|
19
|
-
def self.adapter_identifier
|
20
|
-
adapter_config.identifier
|
21
|
-
end
|
22
|
-
|
23
|
-
def self.adapter_config
|
24
|
-
raise "Implement `self.adapter_config` in individual job metrics collectors."
|
25
|
-
end
|
26
|
-
|
27
|
-
def initialize
|
28
|
-
super
|
29
|
-
|
30
|
-
log_msg = +"#{self.class.adapter_name} enabled"
|
31
|
-
log_msg << " with busy job tracking support" if track_busy_jobs?
|
32
|
-
logger.info log_msg
|
33
|
-
end
|
34
|
-
|
35
|
-
# Track the known queues so we can continue reporting on queues that don't
|
36
|
-
# have enqueued jobs at the time of reporting.
|
37
|
-
def queues
|
38
|
-
@queues ||= Set.new([])
|
39
|
-
end
|
40
|
-
|
41
|
-
def queues=(new_queues)
|
42
|
-
@queues = filter_queues(new_queues)
|
43
|
-
end
|
44
|
-
|
45
|
-
def clear_queues
|
46
|
-
@queues = nil
|
47
|
-
end
|
48
|
-
|
49
|
-
private
|
50
|
-
|
51
|
-
def adapter_config
|
52
|
-
self.class.adapter_config
|
53
|
-
end
|
54
|
-
|
55
|
-
def filter_queues(queues)
|
56
|
-
configured_queues = adapter_config.queues
|
57
|
-
|
58
|
-
if configured_queues.empty?
|
59
|
-
configured_filter = adapter_config.queue_filter
|
60
|
-
|
61
|
-
if configured_filter.respond_to?(:call)
|
62
|
-
queues = queues.select { |queue| configured_filter.call(queue) }
|
63
|
-
end
|
64
|
-
else
|
65
|
-
queues = configured_queues
|
66
|
-
end
|
67
|
-
|
68
|
-
queues = filter_max_queues(queues)
|
69
|
-
|
70
|
-
Set.new(queues)
|
71
|
-
end
|
72
|
-
|
73
|
-
# Collect up to the configured `max_queues`, skipping the rest.
|
74
|
-
# We sort queues by name length before making the cut-off, as a simple heuristic to keep the shorter ones
|
75
|
-
# and possibly ignore the longer ones, which are more likely to be dynamically generated for example.
|
76
|
-
def filter_max_queues(queues_to_collect)
|
77
|
-
queues_size = queues_to_collect.size
|
78
|
-
max_queues = adapter_config.max_queues
|
79
|
-
|
80
|
-
if queues_size > max_queues
|
81
|
-
logger.warn "#{self.class.adapter_name} metrics reporting only #{max_queues} queues max, skipping the rest (#{queues_size - max_queues})"
|
82
|
-
queues_to_collect.sort_by(&:length).first(max_queues)
|
83
|
-
else
|
84
|
-
queues_to_collect
|
85
|
-
end
|
86
|
-
end
|
87
|
-
|
88
|
-
# Sample log line for each collection, assuming `sidekiq` as the adapter identifier:
|
89
|
-
# `sidekiq-qt.default=10ms sidekiq-qd.default=3 sidekiq-busy.default=1`
|
90
|
-
def log_collection(metrics)
|
91
|
-
return if metrics.empty?
|
92
|
-
|
93
|
-
identifier = self.class.adapter_identifier
|
94
|
-
messages = metrics.map { |metric|
|
95
|
-
"#{identifier}-#{metric.identifier}.#{metric.queue_name}=#{metric.value}#{"ms" if metric.identifier == :qt}"
|
96
|
-
}
|
97
|
-
logger.debug messages.join(" ")
|
98
|
-
end
|
99
|
-
|
100
|
-
def track_busy_jobs?
|
101
|
-
adapter_config.track_busy_jobs
|
102
|
-
end
|
103
|
-
end
|
104
|
-
end
|
data/lib/judoscale/logger.rb
DELETED
@@ -1,45 +0,0 @@
|
|
1
|
-
# frozen_string_literal: true
|
2
|
-
|
3
|
-
require "judoscale/config"
|
4
|
-
require "logger"
|
5
|
-
|
6
|
-
module Judoscale
|
7
|
-
module Logger
|
8
|
-
def logger
|
9
|
-
if @logger && @logger.log_level == Config.instance.log_level
|
10
|
-
@logger
|
11
|
-
else
|
12
|
-
@logger = LoggerProxy.new(Config.instance.logger, Config.instance.log_level)
|
13
|
-
end
|
14
|
-
end
|
15
|
-
end
|
16
|
-
|
17
|
-
class LoggerProxy < Struct.new(:logger, :log_level)
|
18
|
-
%w[ERROR WARN INFO DEBUG FATAL].each do |severity_name|
|
19
|
-
severity_level = ::Logger::Severity.const_get(severity_name)
|
20
|
-
|
21
|
-
define_method(severity_name.downcase) do |*messages|
|
22
|
-
if log_level.nil?
|
23
|
-
logger.public_send(severity_name.downcase) { tag(messages) }
|
24
|
-
elsif severity_level >= log_level
|
25
|
-
if severity_level >= logger.level
|
26
|
-
logger.public_send(severity_name.downcase) { tag(messages) }
|
27
|
-
else
|
28
|
-
# Our logger proxy is configured with a lower severity level than the underlying logger,
|
29
|
-
# so send this message using the underlying logger severity instead of the actual severity.
|
30
|
-
logger_severity_name = ::Logger::SEV_LABEL[logger.level].downcase
|
31
|
-
logger.public_send(logger_severity_name) { tag(messages, tag_level: severity_name) }
|
32
|
-
end
|
33
|
-
end
|
34
|
-
end
|
35
|
-
end
|
36
|
-
|
37
|
-
private
|
38
|
-
|
39
|
-
def tag(msgs, tag_level: nil)
|
40
|
-
tag = +"[#{Config.instance.log_tag}]"
|
41
|
-
tag << " [#{tag_level}]" if tag_level
|
42
|
-
msgs.map { |msg| "#{tag} #{msg}" }.join("\n")
|
43
|
-
end
|
44
|
-
end
|
45
|
-
end
|
data/lib/judoscale/metric.rb
DELETED
@@ -1,11 +0,0 @@
|
|
1
|
-
# frozen_string_literal: true
|
2
|
-
|
3
|
-
module Judoscale
|
4
|
-
class Metric < Struct.new(:identifier, :value, :time, :queue_name)
|
5
|
-
# No queue_name is assumed to be a web request metric
|
6
|
-
# Metrics: qt = queue time (default), qd = queue depth, busy
|
7
|
-
def initialize(identifier, value, time, queue_name = nil)
|
8
|
-
super(identifier, value.to_i, time.utc, queue_name)
|
9
|
-
end
|
10
|
-
end
|
11
|
-
end
|
@@ -1,41 +0,0 @@
|
|
1
|
-
# frozen_string_literal: true
|
2
|
-
|
3
|
-
require "singleton"
|
4
|
-
require "judoscale/metric"
|
5
|
-
require "judoscale/report"
|
6
|
-
|
7
|
-
module Judoscale
|
8
|
-
class MetricsStore
|
9
|
-
include Singleton
|
10
|
-
|
11
|
-
attr_reader :metrics, :flushed_at
|
12
|
-
|
13
|
-
def initialize
|
14
|
-
@metrics = []
|
15
|
-
@flushed_at = Time.now
|
16
|
-
end
|
17
|
-
|
18
|
-
def push(identifier, value, time = Time.now, queue_name = nil)
|
19
|
-
# If it's been two minutes since clearing out the store, stop collecting metrics.
|
20
|
-
# There could be an issue with the reporter, and continuing to collect will consume linear memory.
|
21
|
-
return if @flushed_at && @flushed_at < Time.now - 120
|
22
|
-
|
23
|
-
@metrics << Metric.new(identifier, value, time, queue_name)
|
24
|
-
end
|
25
|
-
|
26
|
-
def flush
|
27
|
-
@flushed_at = Time.now
|
28
|
-
flushed_metrics = []
|
29
|
-
|
30
|
-
while (metric = @metrics.shift)
|
31
|
-
flushed_metrics << metric
|
32
|
-
end
|
33
|
-
|
34
|
-
flushed_metrics
|
35
|
-
end
|
36
|
-
|
37
|
-
def clear
|
38
|
-
@metrics.clear
|
39
|
-
end
|
40
|
-
end
|
41
|
-
end
|
data/lib/judoscale/report.rb
DELETED
@@ -1,30 +0,0 @@
|
|
1
|
-
# frozen_string_literal: true
|
2
|
-
|
3
|
-
module Judoscale
|
4
|
-
class Report
|
5
|
-
attr_reader :adapters, :config, :metrics
|
6
|
-
|
7
|
-
def initialize(adapters, config, metrics = [])
|
8
|
-
@adapters = adapters
|
9
|
-
@config = config
|
10
|
-
@metrics = metrics
|
11
|
-
end
|
12
|
-
|
13
|
-
def as_json
|
14
|
-
{
|
15
|
-
container: config.current_runtime_container,
|
16
|
-
pid: Process.pid,
|
17
|
-
config: config.as_json,
|
18
|
-
adapters: adapters.reduce({}) { |hash, adapter| hash.merge!(adapter.as_json) },
|
19
|
-
metrics: metrics.map { |metric|
|
20
|
-
[
|
21
|
-
metric.time.to_i,
|
22
|
-
metric.value,
|
23
|
-
metric.identifier,
|
24
|
-
metric.queue_name
|
25
|
-
]
|
26
|
-
}
|
27
|
-
}
|
28
|
-
end
|
29
|
-
end
|
30
|
-
end
|
data/lib/judoscale/reporter.rb
DELETED
@@ -1,102 +0,0 @@
|
|
1
|
-
# frozen_string_literal: true
|
2
|
-
|
3
|
-
require "singleton"
|
4
|
-
require "judoscale/config"
|
5
|
-
require "judoscale/logger"
|
6
|
-
require "judoscale/adapter_api"
|
7
|
-
require "judoscale/job_metrics_collector"
|
8
|
-
require "judoscale/web_metrics_collector"
|
9
|
-
|
10
|
-
module Judoscale
|
11
|
-
class Reporter
|
12
|
-
include Singleton
|
13
|
-
include Logger
|
14
|
-
|
15
|
-
def self.start(config = Config.instance, adapters = Judoscale.adapters)
|
16
|
-
instance.start!(config, adapters) unless instance.started?
|
17
|
-
end
|
18
|
-
|
19
|
-
def start!(config, adapters)
|
20
|
-
@pid = Process.pid
|
21
|
-
|
22
|
-
if config.api_base_url.nil? || config.api_base_url.strip.empty?
|
23
|
-
logger.debug "Set api_base_url to enable metrics reporting"
|
24
|
-
return
|
25
|
-
end
|
26
|
-
|
27
|
-
enabled_adapters, skipped_adapters = adapters.partition { |adapter|
|
28
|
-
adapter.metrics_collector&.collect?(config)
|
29
|
-
}
|
30
|
-
metrics_collectors_classes = enabled_adapters.map(&:metrics_collector)
|
31
|
-
adapters_msg = enabled_adapters.map(&:identifier).concat(
|
32
|
-
skipped_adapters.map { |adapter| "#{adapter.identifier}[skipped]" }
|
33
|
-
).join(", ")
|
34
|
-
|
35
|
-
if metrics_collectors_classes.empty?
|
36
|
-
logger.debug "No metrics need to be collected (adapters: #{adapters_msg})"
|
37
|
-
return
|
38
|
-
end
|
39
|
-
|
40
|
-
logger.info "Reporter starting, will report every ~#{config.report_interval_seconds} seconds (adapters: #{adapters_msg})"
|
41
|
-
|
42
|
-
metrics_collectors = metrics_collectors_classes.map(&:new)
|
43
|
-
|
44
|
-
run_loop(config, metrics_collectors)
|
45
|
-
end
|
46
|
-
|
47
|
-
def run_loop(config, metrics_collectors)
|
48
|
-
@_thread = Thread.new do
|
49
|
-
loop do
|
50
|
-
run_metrics_collection(config, metrics_collectors)
|
51
|
-
|
52
|
-
# Stagger reporting to spread out reports from many processes
|
53
|
-
multiplier = 1 - (rand / 4) # between 0.75 and 1.0
|
54
|
-
sleep config.report_interval_seconds * multiplier
|
55
|
-
end
|
56
|
-
end
|
57
|
-
end
|
58
|
-
|
59
|
-
def run_metrics_collection(config, metrics_collectors)
|
60
|
-
metrics = metrics_collectors.flat_map do |metric_collector|
|
61
|
-
log_exceptions { metric_collector.collect } || []
|
62
|
-
end
|
63
|
-
|
64
|
-
log_exceptions { report(config, metrics) }
|
65
|
-
end
|
66
|
-
|
67
|
-
def started?
|
68
|
-
@pid == Process.pid
|
69
|
-
end
|
70
|
-
|
71
|
-
def stop!
|
72
|
-
@_thread&.terminate
|
73
|
-
@_thread = nil
|
74
|
-
@pid = nil
|
75
|
-
end
|
76
|
-
|
77
|
-
private
|
78
|
-
|
79
|
-
def report(config, metrics)
|
80
|
-
report = Report.new(Judoscale.adapters, config, metrics)
|
81
|
-
logger.info "Reporting #{report.metrics.size} metrics"
|
82
|
-
result = AdapterApi.new(config).report_metrics(report.as_json)
|
83
|
-
|
84
|
-
case result
|
85
|
-
when AdapterApi::SuccessResponse
|
86
|
-
logger.debug "Reported successfully"
|
87
|
-
when AdapterApi::FailureResponse
|
88
|
-
logger.error "Reporter failed: #{result.failure_message}"
|
89
|
-
end
|
90
|
-
end
|
91
|
-
|
92
|
-
def log_exceptions
|
93
|
-
yield
|
94
|
-
rescue => ex
|
95
|
-
# Log the exception but swallow it to keep the thread running and processing reports.
|
96
|
-
# Note: Exceptions in threads other than the main thread will fail silently and terminate it.
|
97
|
-
# https://ruby-doc.org/core-3.1.0/Thread.html#class-Thread-label-Exception+handling
|
98
|
-
logger.error "Reporter error: #{ex.inspect}", *ex.backtrace
|
99
|
-
nil
|
100
|
-
end
|
101
|
-
end
|
102
|
-
end
|
@@ -1,61 +0,0 @@
|
|
1
|
-
# frozen_string_literal: true
|
2
|
-
|
3
|
-
module Judoscale
|
4
|
-
class RequestMetrics
|
5
|
-
MILLISECONDS_CUTOFF = Time.new(2000, 1, 1).to_i * 1000
|
6
|
-
MICROSECONDS_CUTOFF = MILLISECONDS_CUTOFF * 1000
|
7
|
-
NANOSECONDS_CUTOFF = MICROSECONDS_CUTOFF * 1000
|
8
|
-
|
9
|
-
attr_reader :request_id, :size, :network_time
|
10
|
-
|
11
|
-
def initialize(env, config = Config.instance)
|
12
|
-
@config = config
|
13
|
-
@request_id = env["HTTP_X_REQUEST_ID"]
|
14
|
-
@size = env["rack.input"].respond_to?(:size) ? env["rack.input"].size : 0
|
15
|
-
@network_time = env["puma.request_body_wait"].to_i
|
16
|
-
@request_start_header = env["HTTP_X_REQUEST_START"]
|
17
|
-
end
|
18
|
-
|
19
|
-
def ignore?
|
20
|
-
@config.ignore_large_requests? && @size > @config.max_request_size_bytes
|
21
|
-
end
|
22
|
-
|
23
|
-
def started_at
|
24
|
-
if @request_start_header
|
25
|
-
# There are several variants of this header. We handle these:
|
26
|
-
# - whole milliseconds (Heroku)
|
27
|
-
# - whole microseconds (???)
|
28
|
-
# - whole nanoseconds (Render)
|
29
|
-
# - fractional seconds (NGINX)
|
30
|
-
# - preceeding "t=" (NGINX)
|
31
|
-
value = @request_start_header.gsub(/[^0-9.]/, "").to_f
|
32
|
-
|
33
|
-
# `value` could be seconds, milliseconds, microseconds or nanoseconds.
|
34
|
-
# We use some arbitrary cutoffs to determine which one it is.
|
35
|
-
|
36
|
-
if value > NANOSECONDS_CUTOFF
|
37
|
-
Time.at(value / 1_000_000_000.0)
|
38
|
-
elsif value > MICROSECONDS_CUTOFF
|
39
|
-
Time.at(value / 1_000_000.0)
|
40
|
-
elsif value > MILLISECONDS_CUTOFF
|
41
|
-
Time.at(value / 1000.0)
|
42
|
-
else
|
43
|
-
Time.at(value)
|
44
|
-
end
|
45
|
-
end
|
46
|
-
end
|
47
|
-
|
48
|
-
def queue_time(now = Time.now)
|
49
|
-
return if started_at.nil?
|
50
|
-
|
51
|
-
queue_time = ((now - started_at) * 1000).to_i
|
52
|
-
|
53
|
-
# Subtract the time Puma spent waiting on the request body, i.e. the network time. It's irrelevant to
|
54
|
-
# capacity-related queue time. Without this, slow clients and large request payloads will skew queue time.
|
55
|
-
queue_time -= network_time
|
56
|
-
|
57
|
-
# Safeguard against negative queue times (should not happen in practice)
|
58
|
-
(queue_time > 0) ? queue_time : 0
|
59
|
-
end
|
60
|
-
end
|
61
|
-
end
|
@@ -1,44 +0,0 @@
|
|
1
|
-
# frozen_string_literal: true
|
2
|
-
|
3
|
-
require "judoscale/metrics_store"
|
4
|
-
require "judoscale/reporter"
|
5
|
-
require "judoscale/logger"
|
6
|
-
require "judoscale/request_metrics"
|
7
|
-
|
8
|
-
module Judoscale
|
9
|
-
class RequestMiddleware
|
10
|
-
include Logger
|
11
|
-
|
12
|
-
def initialize(app)
|
13
|
-
@app = app
|
14
|
-
end
|
15
|
-
|
16
|
-
def call(env)
|
17
|
-
request_metrics = RequestMetrics.new(env)
|
18
|
-
|
19
|
-
unless request_metrics.ignore?
|
20
|
-
queue_time = request_metrics.queue_time
|
21
|
-
network_time = request_metrics.network_time
|
22
|
-
end
|
23
|
-
|
24
|
-
Reporter.start
|
25
|
-
|
26
|
-
if queue_time
|
27
|
-
store = MetricsStore.instance
|
28
|
-
|
29
|
-
# NOTE: Expose queue time to the app
|
30
|
-
env["judoscale.queue_time"] = queue_time
|
31
|
-
store.push :qt, queue_time
|
32
|
-
|
33
|
-
unless network_time.zero?
|
34
|
-
env["judoscale.network_time"] = network_time
|
35
|
-
store.push :nt, network_time
|
36
|
-
end
|
37
|
-
|
38
|
-
logger.debug "Request queue_time=#{queue_time}ms network_time=#{network_time}ms request_id=#{request_metrics.request_id} size=#{request_metrics.size}"
|
39
|
-
end
|
40
|
-
|
41
|
-
@app.call(env)
|
42
|
-
end
|
43
|
-
end
|
44
|
-
end
|
data/lib/judoscale/version.rb
DELETED
data/lib/judoscale-ruby.rb
DELETED
@@ -1,40 +0,0 @@
|
|
1
|
-
# frozen_string_literal: true
|
2
|
-
|
3
|
-
require "judoscale/config"
|
4
|
-
require "judoscale/version"
|
5
|
-
|
6
|
-
module Judoscale
|
7
|
-
# Allows configuring Judoscale through a block, usually defined during application initialization.
|
8
|
-
#
|
9
|
-
# Example:
|
10
|
-
#
|
11
|
-
# Judoscale.configure do |config|
|
12
|
-
# config.logger = MyLogger.new
|
13
|
-
# end
|
14
|
-
def self.configure
|
15
|
-
yield Config.instance
|
16
|
-
end
|
17
|
-
|
18
|
-
@adapters = []
|
19
|
-
class << self
|
20
|
-
attr_reader :adapters
|
21
|
-
end
|
22
|
-
|
23
|
-
class Adapter < Struct.new(:identifier, :adapter_info, :metrics_collector)
|
24
|
-
def as_json
|
25
|
-
{identifier => adapter_info}
|
26
|
-
end
|
27
|
-
end
|
28
|
-
|
29
|
-
def self.add_adapter(identifier, adapter_info, metrics_collector: nil, expose_config: nil)
|
30
|
-
Config.expose_adapter_config(expose_config) if expose_config
|
31
|
-
@adapters << Adapter.new(identifier, adapter_info, metrics_collector)
|
32
|
-
end
|
33
|
-
|
34
|
-
add_adapter :"judoscale-ruby", {
|
35
|
-
adapter_version: VERSION,
|
36
|
-
language_version: RUBY_VERSION
|
37
|
-
}
|
38
|
-
end
|
39
|
-
|
40
|
-
RailsAutoscale = Judoscale
|
@@ -1,27 +0,0 @@
|
|
1
|
-
lib = File.expand_path("../lib", __FILE__)
|
2
|
-
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
|
3
|
-
require "judoscale/version"
|
4
|
-
|
5
|
-
Gem::Specification.new do |spec|
|
6
|
-
spec.name = "rails-autoscale-core"
|
7
|
-
spec.version = Judoscale::VERSION
|
8
|
-
spec.authors = ["Adam McCrea", "Carlos Antonio da Silva", "Jon Sullivan"]
|
9
|
-
spec.email = ["hello@judoscale.com"]
|
10
|
-
|
11
|
-
spec.summary = "This gem works with the Judoscale Heroku add-on to automatically scale your web and worker dynos."
|
12
|
-
spec.homepage = "https://judoscale.com"
|
13
|
-
spec.license = "MIT"
|
14
|
-
|
15
|
-
spec.metadata = {
|
16
|
-
"homepage_uri" => "https://judoscale.com",
|
17
|
-
"bug_tracker_uri" => "https://github.com/judoscale/judoscale-ruby/issues",
|
18
|
-
"documentation_uri" => "https://judoscale.com/docs",
|
19
|
-
"changelog_uri" => "https://github.com/judoscale/judoscale-ruby/blob/main/CHANGELOG.md",
|
20
|
-
"source_code_uri" => "https://github.com/judoscale/judoscale-ruby"
|
21
|
-
}
|
22
|
-
|
23
|
-
spec.files = `git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(test|spec|features)/}) }
|
24
|
-
spec.require_paths = ["lib"]
|
25
|
-
|
26
|
-
spec.required_ruby_version = ">= 2.6.0"
|
27
|
-
end
|