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.
@@ -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
@@ -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
@@ -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: []