rails-autoscale-core 1.3.0 → 1.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 0a26698eaf5edca75ec2e8fd7bf1b5035f9b697c1a94fd81a062a9dfec80c636
4
- data.tar.gz: 30476ecf0afa5774c3773c2b2abbd7c258ef1256fe3e0d086d44a6cca07b69ff
3
+ metadata.gz: 9577515e0476c286fa4733675f8814cfbdbbff2f1cfa8260ada7a7d58ad1e6d2
4
+ data.tar.gz: dbefa4da91b6369b437404c884b1720e4789621b0ed0fc1a3eb6d09f32fef955
5
5
  SHA512:
6
- metadata.gz: ae8ddf51539fee0296c875b579dc7a01014fe2001393ea63c2c81da56e1ff008b154d51b60e0d86a42767908d030000d2441019a74380c48386742cb2e4f0e18
7
- data.tar.gz: 5e75546db2c28f54781d03b9c791731408e45f21a5055cddfe03fac7625131f194a4873fb50bcc5bca194754e035b07b1e6c686bbd0b0d932e0419ca65ae511e
6
+ metadata.gz: a8338c11578ac27afd667a42b5cee2469ce7011007fefed9573dad3617d60f2b79c6d4c7b6347a38ba9ca739bda122647e105dd1b4b31f47127cdb06e64a5a7a
7
+ data.tar.gz: e05183812d63c61837229b76ce119df860316e2dc44d5817720c71b778852e65ab0db3e9f28a21b0f33ac6c40eae64a08e9e1de8803c894e504581e67973a320
data/Gemfile.lock CHANGED
@@ -1,7 +1,7 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- judoscale-ruby (1.3.0)
4
+ judoscale-ruby (1.4.0)
5
5
 
6
6
  GEM
7
7
  remote: https://rubygems.org/
@@ -5,8 +5,8 @@ require "judoscale/version"
5
5
  Gem::Specification.new do |spec|
6
6
  spec.name = "judoscale-ruby"
7
7
  spec.version = Judoscale::VERSION
8
- spec.authors = ["Adam McCrea", "Carlos Antonio da Silva"]
9
- spec.email = ["adam@adamlogic.com"]
8
+ spec.authors = ["Adam McCrea", "Carlos Antonio da Silva", "Jon Sullivan"]
9
+ spec.email = ["hello@judoscale.com"]
10
10
 
11
11
  spec.summary = "This gem works with the Judoscale Heroku add-on to automatically scale your web and worker dynos."
12
12
  spec.homepage = "https://judoscale.com"
@@ -10,6 +10,13 @@ module Judoscale
10
10
  include Logger
11
11
 
12
12
  SUCCESS = "success"
13
+ TRANSIENT_ERRORS = [
14
+ Errno::ECONNREFUSED,
15
+ Errno::ECONNRESET,
16
+ Net::OpenTimeout,
17
+ Net::ReadTimeout,
18
+ OpenSSL::SSL::SSLError
19
+ ]
13
20
 
14
21
  def initialize(config)
15
22
  @config = config
@@ -43,14 +50,14 @@ module Judoscale
43
50
  when 200...300 then SuccessResponse.new(response.body)
44
51
  else FailureResponse.new([response.code, response.message].join(" - "))
45
52
  end
46
- rescue Net::OpenTimeout
53
+ rescue *TRANSIENT_ERRORS => ex
47
54
  if attempts < 3
48
55
  # TCP timeouts happen sometimes, but they can usually be successfully retried in a moment
49
56
  sleep 0.01
50
57
  attempts += 1
51
58
  retry
52
59
  else
53
- FailureResponse.new("Timeout while obtaining TCP connection to #{uri.host}")
60
+ FailureResponse.new("Could not connect to #{uri.host}: #{ex.inspect}")
54
61
  end
55
62
  end
56
63
 
@@ -5,16 +5,35 @@ require "logger"
5
5
 
6
6
  module Judoscale
7
7
  class Config
8
- class Dyno
9
- attr_reader :name, :num
10
-
11
- def initialize(dyno_string)
12
- @name, @num = dyno_string.to_s.split(".")
13
- @num = @num.to_i
8
+ class RuntimeContainer
9
+ # E.g.:
10
+ # (Heroku) => "worker_fast", "3"
11
+ # (Render) => "srv-cfa1es5a49987h4vcvfg", "5497f74465-m5wwr", "web" (or "worker", "pserv", "cron", "static")
12
+ def initialize(service_name = nil, instance = nil, service_type = nil)
13
+ @service_name = service_name
14
+ @instance = instance
15
+ @service_type = service_type
14
16
  end
15
17
 
16
18
  def to_s
17
- "#{name}.#{num}"
19
+ # heroku: 'worker_fast.5'
20
+ # render: 'srv-cfa1es5a49987h4vcvfg.5497f74465-m5wwr'
21
+ "#{@service_name}.#{@instance}"
22
+ end
23
+
24
+ def web?
25
+ # NOTE: Heroku isolates 'web' as the required _name_ for its web process
26
+ # type, Render exposes the actual service type more explicitly
27
+ @service_name == "web" || @service_type == "web"
28
+ end
29
+
30
+ # Since Heroku exposes ordinal dyno 'numbers', we can tell if the current
31
+ # instance is redundant (and thus skip collecting some metrics sometimes)
32
+ # We don't have a means of determining that on Render though — so every
33
+ # instance must be considered non-redundant
34
+ def redundant_instance?
35
+ instance_is_number = Integer(@instance, exception: false)
36
+ instance_is_number && instance_is_number != 1
18
37
  end
19
38
  end
20
39
 
@@ -66,28 +85,36 @@ module Judoscale
66
85
  end
67
86
  end
68
87
 
69
- attr_accessor :api_base_url, :report_interval_seconds, :max_request_size_bytes, :logger, :log_tag
70
- attr_reader :dyno, :log_level
88
+ attr_accessor :api_base_url, :report_interval_seconds,
89
+ :max_request_size_bytes, :logger, :log_tag, :current_runtime_container
90
+ attr_reader :log_level
71
91
 
72
92
  def initialize
73
93
  reset
74
94
  end
75
95
 
76
96
  def reset
77
- # Allow the API URL to be configured - needed for testing.
78
97
  @api_base_url = ENV["JUDOSCALE_URL"] || ENV["RAILS_AUTOSCALE_URL"]
79
98
  @log_tag = "Judoscale"
80
- self.dyno = ENV["DYNO"]
81
99
  @max_request_size_bytes = 100_000 # ignore request payloads over 100k since they skew the queue times
82
100
  @report_interval_seconds = 10
101
+
83
102
  self.log_level = ENV["JUDOSCALE_LOG_LEVEL"] || ENV["RAILS_AUTOSCALE_LOG_LEVEL"]
84
103
  @logger = ::Logger.new($stdout)
85
104
 
86
105
  self.class.adapter_configs.each(&:reset)
87
- end
88
106
 
89
- def dyno=(dyno_string)
90
- @dyno = Dyno.new(dyno_string)
107
+ if ENV["RENDER_INSTANCE_ID"]
108
+ instance = ENV["RENDER_INSTANCE_ID"].delete_prefix(ENV["RENDER_SERVICE_ID"]).delete_prefix("-")
109
+ @current_runtime_container = RuntimeContainer.new ENV["RENDER_SERVICE_ID"], instance, ENV["RENDER_SERVICE_TYPE"]
110
+ @api_base_url ||= "https://adapter.judoscale.com/api/#{ENV["RENDER_SERVICE_ID"]}"
111
+ elsif ENV["DYNO"]
112
+ service_name, instance = ENV["DYNO"].split "."
113
+ @current_runtime_container = RuntimeContainer.new service_name, instance
114
+ else
115
+ # unsupported platform? Don't want to leave @current_runtime_container nil though
116
+ @current_runtime_container = RuntimeContainer.new
117
+ end
91
118
  end
92
119
 
93
120
  def log_level=(new_level)
@@ -105,10 +132,6 @@ module Judoscale
105
132
  }.merge!(adapter_configs_json)
106
133
  end
107
134
 
108
- def to_s
109
- "#{@dyno}##{Process.pid}"
110
- end
111
-
112
135
  def ignore_large_requests?
113
136
  @max_request_size_bytes
114
137
  end
@@ -8,9 +8,8 @@ module Judoscale
8
8
  class JobMetricsCollector < MetricsCollector
9
9
  include Judoscale::Logger
10
10
 
11
- # It's redundant to report these metrics from every dyno, so only report from the first one.
12
11
  def self.collect?(config)
13
- config.dyno.num == 1 && adapter_config.enabled
12
+ !config.current_runtime_container.redundant_instance? && adapter_config.enabled
14
13
  end
15
14
 
16
15
  def self.adapter_name
@@ -12,7 +12,7 @@ module Judoscale
12
12
 
13
13
  def as_json
14
14
  {
15
- dyno: config.dyno,
15
+ container: config.current_runtime_container,
16
16
  pid: Process.pid,
17
17
  config: config.as_json,
18
18
  adapters: adapters.reduce({}) { |hash, adapter| hash.merge!(adapter.as_json) },
@@ -31,7 +31,7 @@ module Judoscale
31
31
  metrics_collectors_classes.compact!
32
32
 
33
33
  if metrics_collectors_classes.empty?
34
- logger.info "Reporter not started: no metrics need to be collected on this dyno"
34
+ logger.info "Reporter not started: no metrics need to be collected in this process"
35
35
  return
36
36
  end
37
37
 
@@ -2,6 +2,10 @@
2
2
 
3
3
  module Judoscale
4
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
+
5
9
  attr_reader :request_id, :size, :network_time
6
10
 
7
11
  def initialize(env, config = Config.instance)
@@ -20,15 +24,23 @@ module Judoscale
20
24
  if @request_start_header
21
25
  # There are several variants of this header. We handle these:
22
26
  # - whole milliseconds (Heroku)
27
+ # - whole microseconds (???)
23
28
  # - whole nanoseconds (Render)
24
29
  # - fractional seconds (NGINX)
25
30
  # - preceeding "t=" (NGINX)
26
31
  value = @request_start_header.gsub(/[^0-9.]/, "").to_f
27
32
 
28
- case value
29
- when 0..100_000_000_000 then Time.at(value)
30
- when 100_000_000_000..100_000_000_000_000 then Time.at(value / 1000.0)
31
- else Time.at(value / 1_000_000.0)
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)
32
44
  end
33
45
  end
34
46
  end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Judoscale
4
- VERSION = "1.3.0"
4
+ VERSION = "1.4.0"
5
5
  end
@@ -5,8 +5,10 @@ require "judoscale/metrics_store"
5
5
 
6
6
  module Judoscale
7
7
  class WebMetricsCollector < MetricsCollector
8
+ # NOTE: We collect metrics on all running web processes since they
9
+ # all receive and handle requests independently
8
10
  def self.collect?(config)
9
- config.dyno.name == "web"
11
+ config.current_runtime_container.web?
10
12
  end
11
13
 
12
14
  def collect
@@ -5,8 +5,8 @@ require "judoscale/version"
5
5
  Gem::Specification.new do |spec|
6
6
  spec.name = "rails-autoscale-core"
7
7
  spec.version = Judoscale::VERSION
8
- spec.authors = ["Adam McCrea", "Carlos Antonio da Silva"]
9
- spec.email = ["adam@adamlogic.com"]
8
+ spec.authors = ["Adam McCrea", "Carlos Antonio da Silva", "Jon Sullivan"]
9
+ spec.email = ["hello@judoscale.com"]
10
10
 
11
11
  spec.summary = "This gem works with the Judoscale Heroku add-on to automatically scale your web and worker dynos."
12
12
  spec.homepage = "https://judoscale.com"
metadata CHANGED
@@ -1,19 +1,20 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: rails-autoscale-core
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.3.0
4
+ version: 1.4.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Adam McCrea
8
8
  - Carlos Antonio da Silva
9
+ - Jon Sullivan
9
10
  autorequire:
10
11
  bindir: bin
11
12
  cert_chain: []
12
- date: 2023-02-09 00:00:00.000000000 Z
13
+ date: 2023-04-21 00:00:00.000000000 Z
13
14
  dependencies: []
14
15
  description:
15
16
  email:
16
- - adam@adamlogic.com
17
+ - hello@judoscale.com
17
18
  executables: []
18
19
  extensions: []
19
20
  extra_rdoc_files: []