hipstascale 0.1.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.
- data/lib/hipsta_scale/application.rb +96 -0
- data/lib/hipsta_scale/base.rb +38 -0
- data/lib/hipsta_scale/heroku.rb +42 -0
- data/lib/hipsta_scale/new_relic.rb +45 -0
- data/lib/hipsta_scale/process.rb +38 -0
- data/lib/hipsta_scale.rb +41 -0
- data/lib/hipstascale.rb +1 -0
- metadata +52 -0
@@ -0,0 +1,96 @@
|
|
1
|
+
module HipstaScale
|
2
|
+
class Application
|
3
|
+
attr_reader :name
|
4
|
+
|
5
|
+
def initialize(name, &block)
|
6
|
+
@name = name
|
7
|
+
instance_eval(&block) if block_given?
|
8
|
+
end
|
9
|
+
|
10
|
+
# We take what NewRelic returns us and add an extra 20% to it base on the
|
11
|
+
# recommendation of this blog post:
|
12
|
+
# http://engineering.viki.com/2011/08/10/autoscaling-heroku-dynos/
|
13
|
+
def current_load
|
14
|
+
(new_relic.busy_percent + 20.0) / 100
|
15
|
+
end
|
16
|
+
|
17
|
+
def heroku_key(key=nil)
|
18
|
+
@heroku_key = key unless key.nil?
|
19
|
+
@heroku_key || HipstaScale.heroku_key
|
20
|
+
end
|
21
|
+
|
22
|
+
def heroku_ps(name=nil)
|
23
|
+
@heroku_ps = name unless name.nil?
|
24
|
+
@heroku_ps || HipstaScale.heroku_ps
|
25
|
+
end
|
26
|
+
|
27
|
+
def interval(interval=nil)
|
28
|
+
@interval = interval unless interval.nil?
|
29
|
+
@interval || HipstaScale.interval
|
30
|
+
end
|
31
|
+
|
32
|
+
def load_limit(load=nil)
|
33
|
+
@load_limit = load unless load.nil?
|
34
|
+
@load_limit || HipstaScale.load_limit
|
35
|
+
end
|
36
|
+
|
37
|
+
def minimum_processes(minimum=nil)
|
38
|
+
@minimum_processes = minimum unless minimum.nil?
|
39
|
+
@minimum_processes || HipstaScale.minimum_processes
|
40
|
+
end
|
41
|
+
alias_method :minimum_processes, :minimum_process
|
42
|
+
|
43
|
+
def new_relic_id(id=nil)
|
44
|
+
@new_relic_id = id unless id.nil?
|
45
|
+
@new_relic_id
|
46
|
+
end
|
47
|
+
|
48
|
+
def new_relic_key(key=nil)
|
49
|
+
@new_relic_key = key unless key.nil?
|
50
|
+
@new_relic_key || HipstaScale.new_relic_key
|
51
|
+
end
|
52
|
+
|
53
|
+
def processes_running
|
54
|
+
heroku.processes.count
|
55
|
+
end
|
56
|
+
|
57
|
+
def processes_needed
|
58
|
+
processes = ((processes_running * current_load) / load_limit).ceil
|
59
|
+
processes = minimum_processes if processes < minimum_processes
|
60
|
+
processes = maximum_processes if processes > maximum_processes
|
61
|
+
processes
|
62
|
+
end
|
63
|
+
|
64
|
+
def scale(count)
|
65
|
+
heroku
|
66
|
+
end
|
67
|
+
|
68
|
+
def scale!
|
69
|
+
scale(processes_needed)
|
70
|
+
end
|
71
|
+
|
72
|
+
def tick(&block)
|
73
|
+
block.call
|
74
|
+
expire!
|
75
|
+
end
|
76
|
+
|
77
|
+
private
|
78
|
+
|
79
|
+
def expire!
|
80
|
+
heroku.expire!
|
81
|
+
new_relic.expire!
|
82
|
+
end
|
83
|
+
|
84
|
+
def heroku
|
85
|
+
@_heroku ||= Heroku.new(self)
|
86
|
+
end
|
87
|
+
|
88
|
+
def maximum_processes
|
89
|
+
100
|
90
|
+
end
|
91
|
+
|
92
|
+
def new_relic
|
93
|
+
@_new_relic ||= NewRelic.new(self)
|
94
|
+
end
|
95
|
+
end
|
96
|
+
end
|
@@ -0,0 +1,38 @@
|
|
1
|
+
module HipstaScale
|
2
|
+
module Base
|
3
|
+
def app(name, &block)
|
4
|
+
HipstaScale.apps << Application.new(name, &block)
|
5
|
+
end
|
6
|
+
|
7
|
+
def interval(interval=nil)
|
8
|
+
HipstaScale.interval = interval unless interval.nil?
|
9
|
+
HipstaScale.interval
|
10
|
+
end
|
11
|
+
|
12
|
+
def heroku_key(key=nil)
|
13
|
+
HipstaScale.heroku_key = key unless key.nil?
|
14
|
+
HipstaScale.heroku_key
|
15
|
+
end
|
16
|
+
|
17
|
+
def heroku_ps(name=nil)
|
18
|
+
HipstaScale.heroku_ps = name unless name.nil?
|
19
|
+
HipstaScale.heroku_ps
|
20
|
+
end
|
21
|
+
|
22
|
+
def load_limit(load=nil)
|
23
|
+
HipstaScale.load_limit = load unless load.nil?
|
24
|
+
HipstaScale.load_limit
|
25
|
+
end
|
26
|
+
|
27
|
+
def minimum_processes(minimum=nil)
|
28
|
+
HipstaScale.minimum_processes = minimum unless minimum.nil?
|
29
|
+
HipstaScale.minimum_processes
|
30
|
+
end
|
31
|
+
alias_method :minimum_processes, :minimum_process
|
32
|
+
|
33
|
+
def new_relic_key(key=nil)
|
34
|
+
HipstaScale.new_relic_key = key unless key.nil?
|
35
|
+
HipstaScale.new_relic_key
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
@@ -0,0 +1,42 @@
|
|
1
|
+
require 'json'
|
2
|
+
require 'ostruct'
|
3
|
+
require 'rest_client'
|
4
|
+
|
5
|
+
module HipstaScale
|
6
|
+
class Heroku
|
7
|
+
attr_reader :app
|
8
|
+
|
9
|
+
def initialize(app)
|
10
|
+
@app = app
|
11
|
+
end
|
12
|
+
|
13
|
+
def expire!
|
14
|
+
@_cache = nil
|
15
|
+
end
|
16
|
+
|
17
|
+
def fetch
|
18
|
+
return @_cache unless @_cache.nil?
|
19
|
+
json = RestClient.get(url, accept: 'json')
|
20
|
+
response = JSON.parse(json)
|
21
|
+
@_cache = response.map {|ps| OpenStruct.new(ps) }
|
22
|
+
end
|
23
|
+
|
24
|
+
def processes
|
25
|
+
fetch.select {|ps| ps.process.match(app.heroku_ps) }
|
26
|
+
end
|
27
|
+
|
28
|
+
def processes_other
|
29
|
+
fetch - processes
|
30
|
+
end
|
31
|
+
|
32
|
+
def scale_processes(count)
|
33
|
+
RestClient.post("#{url}/scale?type=#{app.heroku_ps}&qty=#{count}", accept: 'json')
|
34
|
+
end
|
35
|
+
|
36
|
+
private
|
37
|
+
|
38
|
+
def url
|
39
|
+
"https://:#{app.heroku_key}@api.heroku.com/apps/#{app.name}/ps"
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
@@ -0,0 +1,45 @@
|
|
1
|
+
require 'json'
|
2
|
+
require 'ostruct'
|
3
|
+
require 'rest_client'
|
4
|
+
|
5
|
+
module HipstaScale
|
6
|
+
class NewRelic
|
7
|
+
BASE_URL = 'https://api.newrelic.com/api/v1'
|
8
|
+
|
9
|
+
attr_reader :app
|
10
|
+
|
11
|
+
def initialize(app)
|
12
|
+
@app = app
|
13
|
+
end
|
14
|
+
|
15
|
+
def busy_percent
|
16
|
+
fetch.busy_percent
|
17
|
+
end
|
18
|
+
|
19
|
+
def expire!
|
20
|
+
@_cache = nil
|
21
|
+
end
|
22
|
+
|
23
|
+
def fetch
|
24
|
+
return @_cache unless @_cache.nil?
|
25
|
+
json = RestClient.get(url, 'x-api-key' => app.new_relic_key)
|
26
|
+
response = JSON.parse(json)
|
27
|
+
@_cache = OpenStruct.new(response.first)
|
28
|
+
end
|
29
|
+
|
30
|
+
private
|
31
|
+
|
32
|
+
def params
|
33
|
+
''.tap do |params|
|
34
|
+
params << 'begin=' << (Time.now - (app.interval * 60)).strftime('%Y-%m-%dT%H:%M:%S%z')
|
35
|
+
params << '&end=' << Time.now.strftime('%Y-%m-%dT%H:%M:%S%z')
|
36
|
+
params << '&field=' << 'busy_percent'
|
37
|
+
params << '&metrics[]=' << 'Instance/Busy'
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
41
|
+
def url
|
42
|
+
"#{BASE_URL}/applications/#{app.new_relic_id}/data.json?#{params}"
|
43
|
+
end
|
44
|
+
end
|
45
|
+
end
|
@@ -0,0 +1,38 @@
|
|
1
|
+
require 'eventmachine'
|
2
|
+
|
3
|
+
module HipstaScale
|
4
|
+
class Process
|
5
|
+
|
6
|
+
def self.run!
|
7
|
+
EventMachine.run do
|
8
|
+
HipstaScale.apps.each do |app|
|
9
|
+
Process.new(app)
|
10
|
+
# Simple way to make sure all of our external calls don't happen all
|
11
|
+
# at once.
|
12
|
+
sleep(10)
|
13
|
+
end
|
14
|
+
end
|
15
|
+
end
|
16
|
+
|
17
|
+
def initialize(app)
|
18
|
+
logger.info "Check usage for #{app.name} every #{app.interval * 60} seconds"
|
19
|
+
EventMachine.add_periodic_timer(app.interval * 60) do
|
20
|
+
app.tick do
|
21
|
+
logger.info "[#{app.name.upcase}] Instance usage: %.2f%%" % (app.current_load * 100.0)
|
22
|
+
logger.info "[#{app.name.upcase}] Amount of processes needed to reach the %.2f%% of target load: #{app.processes_needed}" % (app.load_limit * 100)
|
23
|
+
|
24
|
+
if app.processes_needed != app.processes_running
|
25
|
+
logger.info "[#{app.name.upcase}] Scaling processes from #{app.processes_running} to #{app.processes_needed}"
|
26
|
+
app.scale!
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
private
|
33
|
+
|
34
|
+
def logger
|
35
|
+
HipstaScale.logger
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
data/lib/hipsta_scale.rb
ADDED
@@ -0,0 +1,41 @@
|
|
1
|
+
require 'logger'
|
2
|
+
|
3
|
+
module HipstaScale
|
4
|
+
autoload :Application, 'hipsta_scale/application'
|
5
|
+
autoload :Base, 'hipsta_scale/base'
|
6
|
+
autoload :Heroku, 'hipsta_scale/heroku'
|
7
|
+
autoload :NewRelic, 'hipsta_scale/new_relic'
|
8
|
+
autoload :Process, 'hipsta_scale/process'
|
9
|
+
|
10
|
+
# We store the global configuration information in the HipstaScale
|
11
|
+
# module itself for easy access across the process.
|
12
|
+
class << self
|
13
|
+
attr_accessor :heroku_key, :heroku_ps, :interval, :new_relic_key
|
14
|
+
attr_writer :apps, :load_limit, :logger, :minimum_processes
|
15
|
+
end
|
16
|
+
|
17
|
+
def self.apps
|
18
|
+
@apps ||= []
|
19
|
+
end
|
20
|
+
|
21
|
+
def self.load_limit
|
22
|
+
@load_limit || 0.8
|
23
|
+
end
|
24
|
+
|
25
|
+
def self.logger
|
26
|
+
@logger || Logger.new(STDOUT)
|
27
|
+
end
|
28
|
+
|
29
|
+
def self.minimum_processes
|
30
|
+
@minimum_processes || 1
|
31
|
+
end
|
32
|
+
|
33
|
+
def self.run!
|
34
|
+
puts apps.inspect
|
35
|
+
end
|
36
|
+
|
37
|
+
at_exit { Process.run! }
|
38
|
+
end
|
39
|
+
|
40
|
+
# Add out Base class (which is the DSL) to the main object space.
|
41
|
+
extend HipstaScale::Base
|
data/lib/hipstascale.rb
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
require 'hipsta_scale'
|
metadata
ADDED
@@ -0,0 +1,52 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: hipstascale
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.1.0
|
5
|
+
prerelease:
|
6
|
+
platform: ruby
|
7
|
+
authors:
|
8
|
+
- Jeremy Boles
|
9
|
+
autorequire:
|
10
|
+
bindir: bin
|
11
|
+
cert_chain: []
|
12
|
+
date: 2012-05-30 00:00:00.000000000 Z
|
13
|
+
dependencies: []
|
14
|
+
description: HipstaScale helps create long-running processes that use NewRelic RPM
|
15
|
+
information to scale your Heroku instances before you need it.
|
16
|
+
email: jeremy@jeremyboles.com
|
17
|
+
executables: []
|
18
|
+
extensions: []
|
19
|
+
extra_rdoc_files: []
|
20
|
+
files:
|
21
|
+
- lib/hipsta_scale/application.rb
|
22
|
+
- lib/hipsta_scale/base.rb
|
23
|
+
- lib/hipsta_scale/heroku.rb
|
24
|
+
- lib/hipsta_scale/new_relic.rb
|
25
|
+
- lib/hipsta_scale/process.rb
|
26
|
+
- lib/hipsta_scale.rb
|
27
|
+
- lib/hipstascale.rb
|
28
|
+
homepage: https://github.com/jeremyboles/hipstascale
|
29
|
+
licenses: []
|
30
|
+
post_install_message:
|
31
|
+
rdoc_options: []
|
32
|
+
require_paths:
|
33
|
+
- lib
|
34
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
35
|
+
none: false
|
36
|
+
requirements:
|
37
|
+
- - ! '>='
|
38
|
+
- !ruby/object:Gem::Version
|
39
|
+
version: '0'
|
40
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
41
|
+
none: false
|
42
|
+
requirements:
|
43
|
+
- - ! '>='
|
44
|
+
- !ruby/object:Gem::Version
|
45
|
+
version: '0'
|
46
|
+
requirements: []
|
47
|
+
rubyforge_project:
|
48
|
+
rubygems_version: 1.8.11
|
49
|
+
signing_key:
|
50
|
+
specification_version: 3
|
51
|
+
summary: HipstaScale
|
52
|
+
test_files: []
|