Class: OurEelHacks::Autoscaler
- Inherits:
-
Object
- Object
- OurEelHacks::Autoscaler
- Defined in:
- lib/our-eel-hacks/autoscaler.rb
Defined Under Namespace
Classes: Limit, LowerLimit, UpperLimit
Constant Summary collapse
- MILLIS_PER_DAY =
24 * 60 * 60 * 1000
- API_CALLS_PER_SCALE =
2
Instance Attribute Summary collapse
-
#app_name ⇒ Object
Returns the value of attribute app_name.
-
#dynos ⇒ Object
readonly
Returns the value of attribute dynos.
-
#entered_soft ⇒ Object
readonly
Returns the value of attribute entered_soft.
-
#heroku_api_key ⇒ Object
Returns the value of attribute heroku_api_key.
-
#heroku_rate_limit ⇒ Object
Returns the value of attribute heroku_rate_limit.
-
#heroku_rate_limit_margin ⇒ Object
Returns the value of attribute heroku_rate_limit_margin.
-
#last_reading ⇒ Object
readonly
Returns the value of attribute last_reading.
-
#last_scaled ⇒ Object
readonly
Returns the value of attribute last_scaled.
-
#logger ⇒ Object
Returns the value of attribute logger.
-
#lower_limits ⇒ Object
Returns the value of attribute lower_limits.
-
#max_dynos ⇒ Object
Returns the value of attribute max_dynos.
-
#millis_til_next_scale ⇒ Object
readonly
Returns the value of attribute millis_til_next_scale.
-
#min_dynos ⇒ Object
Returns the value of attribute min_dynos.
-
#ps_type ⇒ Object
Returns the value of attribute ps_type.
-
#scaling_frequency ⇒ Object
Returns the value of attribute scaling_frequency.
-
#soft_duration ⇒ Object
Returns the value of attribute soft_duration.
-
#soft_side ⇒ Object
readonly
Returns the value of attribute soft_side.
-
#upper_limits ⇒ Object
Returns the value of attribute upper_limits.
Class Method Summary collapse
- .configure(flavor = :web, &block) ⇒ Object
- .get_instance(flavor) ⇒ Object
- .instance_for(flavor = :web) ⇒ Object
Instance Method Summary collapse
- #check_settings ⇒ Object
- #clear_dyno_info ⇒ Object
- #configure(flavor = nil) {|_self| ... } ⇒ Object
- #dyno_info ⇒ Object
- #dynos_stable? ⇒ Boolean
- #elapsed(start, finish) ⇒ Object
- #heroku ⇒ Object
-
#initialize ⇒ Autoscaler
constructor
A new instance of Autoscaler.
- #scale(metric_hash) ⇒ Object
- #set_dynos(count, moment) ⇒ Object
- #soft_limit(metric, moment) ⇒ Object
- #target_scale(metric, moment) ⇒ Object
- #update_dynos(new_value, moment) ⇒ Object
- #update_scaling_delay(starting_wait) ⇒ Object
Constructor Details
#initialize ⇒ Autoscaler
Returns a new instance of Autoscaler.
67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 |
# File 'lib/our-eel-hacks/autoscaler.rb', line 67 def initialize() @dynos = nil @soft_side = nil @memoed_dyno_info = nil @last_scaled = Time.at(0) @entered_soft = Time.at(0) @app_name = nil @ps_type = nil @heroku_api_key = nil @min_dynos = 1 @max_dynos = 10 @lower_limits = LowerLimit.new(5, 1) @upper_limits = UpperLimit.new(30, 50) @soft_duration = 10000 @scaling_frequency = 5000 @heroku_rate_limit = 80_000 @heroku_rate_limit_margin = 0.1 @millis_til_next_scale = nil @logger = NullLogger.new end |
Instance Attribute Details
#app_name ⇒ Object
Returns the value of attribute app_name.
121 122 123 |
# File 'lib/our-eel-hacks/autoscaler.rb', line 121 def app_name @app_name end |
#dynos ⇒ Object (readonly)
Returns the value of attribute dynos.
123 124 125 |
# File 'lib/our-eel-hacks/autoscaler.rb', line 123 def dynos @dynos end |
#entered_soft ⇒ Object (readonly)
Returns the value of attribute entered_soft.
123 124 125 |
# File 'lib/our-eel-hacks/autoscaler.rb', line 123 def entered_soft @entered_soft end |
#heroku_api_key ⇒ Object
Returns the value of attribute heroku_api_key.
121 122 123 |
# File 'lib/our-eel-hacks/autoscaler.rb', line 121 def heroku_api_key @heroku_api_key end |
#heroku_rate_limit ⇒ Object
Returns the value of attribute heroku_rate_limit.
121 122 123 |
# File 'lib/our-eel-hacks/autoscaler.rb', line 121 def heroku_rate_limit @heroku_rate_limit end |
#heroku_rate_limit_margin ⇒ Object
Returns the value of attribute heroku_rate_limit_margin.
121 122 123 |
# File 'lib/our-eel-hacks/autoscaler.rb', line 121 def heroku_rate_limit_margin @heroku_rate_limit_margin end |
#last_reading ⇒ Object (readonly)
Returns the value of attribute last_reading.
123 124 125 |
# File 'lib/our-eel-hacks/autoscaler.rb', line 123 def last_reading @last_reading end |
#last_scaled ⇒ Object (readonly)
Returns the value of attribute last_scaled.
123 124 125 |
# File 'lib/our-eel-hacks/autoscaler.rb', line 123 def last_scaled @last_scaled end |
#logger ⇒ Object
Returns the value of attribute logger.
121 122 123 |
# File 'lib/our-eel-hacks/autoscaler.rb', line 121 def logger @logger end |
#lower_limits ⇒ Object
Returns the value of attribute lower_limits.
121 122 123 |
# File 'lib/our-eel-hacks/autoscaler.rb', line 121 def lower_limits @lower_limits end |
#max_dynos ⇒ Object
Returns the value of attribute max_dynos.
121 122 123 |
# File 'lib/our-eel-hacks/autoscaler.rb', line 121 def max_dynos @max_dynos end |
#millis_til_next_scale ⇒ Object (readonly)
Returns the value of attribute millis_til_next_scale.
123 124 125 |
# File 'lib/our-eel-hacks/autoscaler.rb', line 123 def millis_til_next_scale @millis_til_next_scale end |
#min_dynos ⇒ Object
Returns the value of attribute min_dynos.
121 122 123 |
# File 'lib/our-eel-hacks/autoscaler.rb', line 121 def min_dynos @min_dynos end |
#ps_type ⇒ Object
Returns the value of attribute ps_type.
121 122 123 |
# File 'lib/our-eel-hacks/autoscaler.rb', line 121 def ps_type @ps_type end |
#scaling_frequency ⇒ Object
Returns the value of attribute scaling_frequency.
121 122 123 |
# File 'lib/our-eel-hacks/autoscaler.rb', line 121 def scaling_frequency @scaling_frequency end |
#soft_duration ⇒ Object
Returns the value of attribute soft_duration.
121 122 123 |
# File 'lib/our-eel-hacks/autoscaler.rb', line 121 def soft_duration @soft_duration end |
#soft_side ⇒ Object (readonly)
Returns the value of attribute soft_side.
123 124 125 |
# File 'lib/our-eel-hacks/autoscaler.rb', line 123 def soft_side @soft_side end |
#upper_limits ⇒ Object
Returns the value of attribute upper_limits.
121 122 123 |
# File 'lib/our-eel-hacks/autoscaler.rb', line 121 def upper_limits @upper_limits end |
Class Method Details
.configure(flavor = :web, &block) ⇒ Object
19 20 21 |
# File 'lib/our-eel-hacks/autoscaler.rb', line 19 def configure(flavor = :web, &block) get_instance(flavor).configure(flavor, &block) end |
.get_instance(flavor) ⇒ Object
13 14 15 16 17 |
# File 'lib/our-eel-hacks/autoscaler.rb', line 13 def get_instance(flavor) flavor = flavor.to_sym @instances ||= Hash.new{ |h,k| h[k] = self.new } return @instances[flavor] end |
.instance_for(flavor = :web) ⇒ Object
23 24 25 26 27 |
# File 'lib/our-eel-hacks/autoscaler.rb', line 23 def instance_for(flavor = :web) instance = get_instance(flavor) instance.check_settings return instance end |
Instance Method Details
#check_settings ⇒ Object
105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 |
# File 'lib/our-eel-hacks/autoscaler.rb', line 105 def check_settings errors = [] errors << "No heroku api key set" if @heroku_api_key.nil? errors << "No app name set" if @app_name.nil? errors << "No process type set" if @ps_type.nil? if (MILLIS_PER_DAY / @heroku_rate_limit) * (1.0 - @heroku_rate_limit_margin) * API_CALLS_PER_SCALE > @scaling_frequency errors << "Scaling frequency will lock up Heroku" end unless errors.empty? logger.warn{ "Problems configuring Autoscaler: #{errors.inspect}" } raise "OurEelHacks::Autoscaler, configuration problem: " + errors.join(", ") end end |
#clear_dyno_info ⇒ Object
215 216 217 |
# File 'lib/our-eel-hacks/autoscaler.rb', line 215 def clear_dyno_info @memoed_dyno_info = nil end |
#configure(flavor = nil) {|_self| ... } ⇒ Object
95 96 97 98 99 100 101 102 |
# File 'lib/our-eel-hacks/autoscaler.rb', line 95 def configure(flavor = nil) yield self check_settings logger.info{ "Autoscaler configured for #{flavor || "{{unknown flavor}}"}"} update_dynos(dyno_info.count, Time.now) update_scaling_delay(0) end |
#dyno_info ⇒ Object
219 220 221 222 223 224 225 226 227 |
# File 'lib/our-eel-hacks/autoscaler.rb', line 219 def dyno_info return @memoed_dyno_info ||= begin regexp = /^#{ps_type}[.].*/ heroku.ps(app_name).find_all do |dyno| dyno["process"] =~ regexp end end end |
#dynos_stable? ⇒ Boolean
229 230 231 232 233 |
# File 'lib/our-eel-hacks/autoscaler.rb', line 229 def dynos_stable? return dyno_info.all? do |dyno| dyno["state"] == "up" end end |
#elapsed(start, finish) ⇒ Object
125 126 127 128 129 130 131 |
# File 'lib/our-eel-hacks/autoscaler.rb', line 125 def elapsed(start, finish) seconds = finish.to_i - start.to_i micros = finish.usec - start.usec diff = seconds * 1000 + micros / 1000 logger.debug{ "Elapsed: #{start.to_s}:#{finish.to_s} : #{diff}ms" } return diff end |
#heroku ⇒ Object
235 236 237 |
# File 'lib/our-eel-hacks/autoscaler.rb', line 235 def heroku @heroku ||= HerokuClient.new(logger, heroku_api_key) end |
#scale(metric_hash) ⇒ Object
134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 |
# File 'lib/our-eel-hacks/autoscaler.rb', line 134 def scale(metric_hash) logger.debug{ "Scaling request for #{@ps_type}: metrics are: #{metric_hash.inspect}" } #TODO: multi-metric scaling logic metric = metric_hash.to_a.last.last #Yeah, this is awful moment = Time.now if elapsed(last_scaled, moment) < millis_til_next_scale logger.debug{ "Not scaling: elapsed #{elapsed(last_scaled, moment)} less than computed #{millis_til_next_scale}" } return end clear_dyno_info starting_wait = millis_til_next_scale update_dynos(dyno_info.count, moment) target_dynos = target_scale(metric, moment) target_dynos = [[target_dynos, max_dynos].min, min_dynos].max logger.debug{ "Target dynos at: #{min_dynos}/#{target_dynos}/#{max_dynos} (vs. current: #{@dynos})" } set_dynos(target_dynos, moment) update_scaling_delay(starting_wait) rescue => ex logger.warn{ "Problem scaling: #{ex.inspect} \t#{ex.backtrace.join("\t\n")}" } end |
#set_dynos(count, moment) ⇒ Object
239 240 241 242 243 244 245 246 247 248 249 250 251 252 |
# File 'lib/our-eel-hacks/autoscaler.rb', line 239 def set_dynos(count,moment) if count == dynos logger.debug{ "Not scaling: #{count} ?= #{dynos}" } return end if not (stable = dynos_stable?) logger.debug{ "Not scaling: dynos not stable (iow: not all #{ps_type} dynos are up)" } return end logger.info{ "Scaling from #{dynos} to #{count} dynos for #{ps_type}" } heroku.ps_scale(app_name, ps_type, count) update_dynos(count, moment) end |
#soft_limit(metric, moment) ⇒ Object
174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 |
# File 'lib/our-eel-hacks/autoscaler.rb', line 174 def soft_limit(metric, moment) hit_limit = [lower_limits, upper_limits].find{|lim| lim.includes? metric} if soft_side == hit_limit if elapsed(entered_soft, moment) > soft_duration entered_soft = moment case hit_limit when upper_limits return +1 when lower_limits return -1 else return 0 end else return 0 end else @entered_soft = moment end @soft_side = hit_limit return 0 end |
#target_scale(metric, moment) ⇒ Object
163 164 165 166 167 168 169 170 171 172 |
# File 'lib/our-eel-hacks/autoscaler.rb', line 163 def target_scale(metric, moment) if lower_limits > metric return dynos - 1 elsif upper_limits < metric return dynos + 1 elsif result = (dynos + soft_limit(metric, moment)) return result end end |
#update_dynos(new_value, moment) ⇒ Object
207 208 209 210 211 212 213 |
# File 'lib/our-eel-hacks/autoscaler.rb', line 207 def update_dynos(new_value, moment) if new_value != dynos @entered_soft = moment end @dynos = new_value @last_scaled = moment end |
#update_scaling_delay(starting_wait) ⇒ Object
199 200 201 202 203 204 205 |
# File 'lib/our-eel-hacks/autoscaler.rb', line 199 def update_scaling_delay(starting_wait) @millis_til_next_scale = scaling_frequency * @dynos if starting_wait > millis_til_next_scale logger.debug{ "Adjusting scaling delay for cadence between #{@millis_til_next_scale.inspect} and #{starting_wait.inspect}" } @millis_til_next_scale += rand(starting_wait - @millis_til_next_scale) end end |