save-your-dosh 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
data/Gemfile ADDED
@@ -0,0 +1,2 @@
1
+ gem 'heroku-api'
2
+ gem 'json'
@@ -0,0 +1,13 @@
1
+ GEM
2
+ specs:
3
+ excon (0.16.10)
4
+ heroku-api (0.3.7)
5
+ excon (~> 0.16.10)
6
+ json (1.7.5)
7
+
8
+ PLATFORMS
9
+ ruby
10
+
11
+ DEPENDENCIES
12
+ heroku-api
13
+ json
@@ -0,0 +1,59 @@
1
+ # Save Your Dosh
2
+
3
+ This is a little gem for [heroku](http://heroku.com) that automatically scales
4
+ dynos in your heroku based app.
5
+
6
+ ## Prerequisites
7
+
8
+ You obviously have to be on `heroku` and `rails`. You also need the New Relic RPM add-on
9
+ switched on. It doesn't matter whether you have a free or a pro account on new relic.
10
+
11
+ ## Installation
12
+
13
+ Hook it up as a rubygem in your `Gemfile`
14
+
15
+ ```ruby
16
+ gem 'save-your-dosh'
17
+ ```
18
+
19
+ Make sure you have the following `ENV` vars in your heroku config
20
+
21
+ ```
22
+ » heroku config
23
+ === doshmosh Config Vars
24
+ .....
25
+ HEROKU_API_KEY: your-heroku-api-key
26
+ NEW_RELIC_API_KEY: your-new-relic-api-key
27
+ NEW_RELIC_APP_NAME: your-app-name-on-new-relic
28
+ NEW_RELIC_ID: your-account-id-on-new-relic
29
+ ......
30
+ ```
31
+
32
+ Once you've done with those, add the `rake save:your:dosh` task in heroku's scheduler
33
+ and set the minimal timeout of `10 mins`. (don't make it less than 6 mins, otherwise
34
+ new relic will kick your ass)
35
+
36
+ ## Configuration
37
+
38
+ You can mangle with the settings by creating a file like that in your rails app `config/save-your-dosh.yaml`
39
+
40
+ ```yml
41
+ dynos:
42
+ min: 1
43
+ max: 5
44
+ threshold: 50 # % of system busyness when we kick in/out a dyno
45
+ ```
46
+
47
+
48
+ ## How It Works
49
+
50
+ It's pretty simple, every time you kick the `rake save:your:dosh` task via cron or scheduler,
51
+ it will make a request to the new relic servers for the data on your application business. If
52
+ it goes over the threshold, it will increase the amount of dynos until reaches the max number
53
+ from the config. Otherwise it will try to switch dynos off until reaches the minimal amount.
54
+
55
+
56
+ ## License
57
+
58
+ All the code in this package released under the terms of the MIT license
59
+ Copyright (C) 2012 Nikolay Nemshilov
data/init.rb ADDED
@@ -0,0 +1 @@
1
+ require 'save_your_dosh'
@@ -0,0 +1,4 @@
1
+ #
2
+ # Just a dummy for `gem 'save-your-dosh'`
3
+ #
4
+ require File.join(File.dirname(__FILE__),'save_your_dosh.rb')
@@ -0,0 +1,28 @@
1
+ #
2
+ # The namespace
3
+ #
4
+ module SaveYourDosh
5
+ extend self
6
+
7
+ VERSION = '0.0.0'
8
+
9
+ autoload :Config, 'save_your_dosh/config.rb'
10
+ autoload :Mangler, 'save_your_dosh/mangler.rb'
11
+ autoload :NewRelic, 'save_your_dosh/new_relic.rb'
12
+
13
+
14
+ def config
15
+ @config ||= Config.new
16
+ end
17
+
18
+ def mangle!
19
+ mangler ||= Mangler.new
20
+ mangler.mangle_dynos!
21
+ mangler.mangle_workers!
22
+ end
23
+
24
+ Dir[File.join(File.dirname(__FILE__),'tasks/*.rake')].each { |ext|
25
+ load ext
26
+ } if defined?(Rake)
27
+
28
+ end
@@ -0,0 +1,13 @@
1
+ interval: 5 # minutes
2
+
3
+ dynos:
4
+ min: 1
5
+ max: 5
6
+ threshold: 50 # % of system busyness when we kick in/out a dyno
7
+
8
+ workers:
9
+ min: 0
10
+ max: 5
11
+ jobs: 20 # jobs per a worker
12
+
13
+ notify: some@email.com
@@ -0,0 +1,36 @@
1
+ #
2
+ # The config object
3
+ #
4
+ require 'yaml'
5
+
6
+ class SaveYourDosh::Config
7
+ DEFAULTS = File.dirname(__FILE__) + "/../save_your_dosh.yml"
8
+ KEYS = %w{ new_relic heroku dynos workers notify interval }
9
+
10
+ KEYS.each{ |key| attr_accessor key }
11
+
12
+ def initialize
13
+ @new_relic = {
14
+ 'acc_id' => ENV['NEW_RELIC_ID'],
15
+ 'app_id' => ENV['NEW_RELIC_APP_NAME'],
16
+ 'api_key' => ENV['NEW_RELIC_API_KEY']
17
+ }
18
+
19
+ @heroku = {
20
+ 'app_id' => ENV['APP_NAME'],
21
+ 'api_key' => ENV['HEROKU_API_KEY']
22
+ }
23
+
24
+ read DEFAULTS
25
+ end
26
+
27
+ def read(file)
28
+ config = YAML.load_file(file)
29
+
30
+ KEYS.each do |key|
31
+ instance_variable_set "@#{key}", config[key] if config.has_key?(key)
32
+ end
33
+ end
34
+
35
+ end
36
+
@@ -0,0 +1,47 @@
1
+ #
2
+ # Mangles with the actual heroku settings
3
+ #
4
+ require 'heroku-api'
5
+
6
+ class SaveYourDosh::Mangler
7
+
8
+ def initialize
9
+ @config = SaveYourDosh.config
10
+ @heroku = Heroku::API.new(api_key: @config.heroku['api_key'])
11
+ end
12
+
13
+ def mangle_dynos!
14
+ mangle! :dynos do |qty|
15
+ load = SaveYourDosh::NewRelic.get_dynos_load
16
+ qty + (load > @config.dynos['threshold'] ? 1 : -1)
17
+ end
18
+ end
19
+
20
+ def mangle_workers!
21
+ mangle! :workers do |qty|
22
+ qty
23
+ end
24
+ end
25
+
26
+ private
27
+
28
+ # a little wrapper to avoid problems with setting/getting
29
+ # a wrong thing
30
+ def mangle!(what, &block)
31
+ qty = @heroku.get_app(@config.heroku['app_id']).body[what.to_s]
32
+ new_qty = yield(qty)
33
+
34
+ min_qty = @config.send(what)['min']
35
+ max_qty = @config.send(what)['max']
36
+
37
+ new_qty = min_qty if new_qty < min_qty
38
+ new_qty = max_qty if new_qty > max_qty
39
+
40
+ if new_qty != qty
41
+ Rails.logger.info "SaveYourDosh: mangling with the #{what}, #{qty} -> #{new_qty}" if defined? Rails
42
+
43
+ @heroku.send("put_#{what}", @config.heroku['app_id'], new_qty)
44
+ end
45
+ end
46
+
47
+ end
@@ -0,0 +1,36 @@
1
+ #
2
+ # A little proxy for the new-relic API
3
+ #
4
+ require 'json'
5
+
6
+ class SaveYourDosh::NewRelic
7
+
8
+ DYNOS_LOAD_CMD = %Q{
9
+ curl --silent -H "x-api-key: %{api_key}" \
10
+ -d "metrics[]=Instance/Busy" \
11
+ -d "field=busy_percent" \
12
+ -d "begin=%{begin_time}" \
13
+ -d "end=%{end_time}" \
14
+ -d "summary=1" \
15
+ -d "app=%{app_id}" \
16
+ https://api.newrelic.com/api/v1/accounts/%{acc_id}/metrics/data.json
17
+ }.strip
18
+
19
+ def self.get_dynos_load
20
+ conf = SaveYourDosh.config
21
+
22
+ data = DYNOS_LOAD_CMD % {
23
+ app_id: conf.new_relic['app_id'],
24
+ acc_id: conf.new_relic['acc_id'],
25
+ api_key: conf.new_relic['api_key'],
26
+ begin_time: Time.now - conf.interval * 60,
27
+ end_time: Time.now
28
+ }
29
+
30
+ JSON.parse(`#{data}`)[0]["busy_percent"]
31
+
32
+ rescue JSON::ParserError, NoMethodError
33
+ return nil
34
+ end
35
+
36
+ end
@@ -0,0 +1,9 @@
1
+
2
+ namespace :save do
3
+ namespace :your do
4
+ desc "Runs the SaveYourDosh.mangle!"
5
+ task :dosh do
6
+ SaveYourDosh.mangle!
7
+ end
8
+ end
9
+ end
@@ -0,0 +1,43 @@
1
+ require 'spec_helper'
2
+
3
+ describe SaveYourDosh::Config do
4
+ describe "defaults" do
5
+ before do
6
+ ENV['NEW_RELIC_ID'] = 'acc-id'
7
+ ENV['NEW_RELIC_APP_NAME'] = '12345'
8
+ ENV['NEW_RELIC_API_KEY'] = 'api-key'
9
+
10
+ ENV['APP_NAME'] = 'app-name'
11
+ ENV['HEROKU_API_KEY'] = 'heroku-api-key'
12
+
13
+ @config = SaveYourDosh::Config.new
14
+ end
15
+
16
+ it "should have default dynos data" do
17
+ @config.dynos.should == {
18
+ "min" => 1, "max" => 5, "threshold" => 50
19
+ }
20
+ end
21
+
22
+ it "should have default workders data" do
23
+ @config.workers.should == {
24
+ "min" => 0, "max" => 5, "jobs" => 20
25
+ }
26
+ end
27
+
28
+ it "should read the new_relic settings from the ENV hash" do
29
+ @config.new_relic.should == {
30
+ 'acc_id' => ENV['NEW_RELIC_ID'],
31
+ 'app_id' => ENV['NEW_RELIC_APP_NAME'],
32
+ 'api_key' => ENV['NEW_RELIC_API_KEY']
33
+ }
34
+ end
35
+
36
+ it "should assign the heroku credentials from the ENV data" do
37
+ @config.heroku.should == {
38
+ 'app_id' => ENV['APP_NAME'],
39
+ 'api_key' => ENV['HEROKU_API_KEY']
40
+ }
41
+ end
42
+ end
43
+ end
@@ -0,0 +1,70 @@
1
+ require 'spec_helper'
2
+
3
+ describe SaveYourDosh::Mangler do
4
+ before do
5
+ @config = SaveYourDosh.config
6
+ @config.heroku = {
7
+ 'app_id' => 'my-app',
8
+ 'login' => 'login',
9
+ 'password' => 'my-password'
10
+ }
11
+
12
+ @heroku = Heroku::API.new(api_key: 'boo hoo')
13
+
14
+ Heroku::API.should_receive(:new).
15
+ with(api_key: @config.heroku['api_key']).
16
+ and_return(@heroku)
17
+
18
+ @mangler = SaveYourDosh::Mangler.new
19
+ end
20
+
21
+ describe ".mangle_dynos!" do
22
+ before do
23
+ @heroku.should_receive(:get_app).
24
+ with(@config.heroku['app_id']).
25
+ and_return(Struct.new(:body).new({'dynos' => 2, 'workers' => 1}))
26
+
27
+ SaveYourDosh::NewRelic.should_receive(:get_dynos_load).
28
+ and_return(40)
29
+ end
30
+
31
+ it "should try to add dynos if we're getting over the threshold" do
32
+ @config.dynos['threshold'] = 30
33
+
34
+ @heroku.should_receive(:put_dynos).
35
+ with(@config.heroku['app_id'], 3)
36
+
37
+ @mangler.mangle_dynos!
38
+ end
39
+
40
+ it "should try to remove dynos if we're below the threshold" do
41
+ @config.dynos['threshold'] = 50
42
+
43
+ @heroku.should_receive(:put_dynos).
44
+ with(@config.heroku['app_id'], 1)
45
+
46
+ @mangler.mangle_dynos!
47
+ end
48
+
49
+ it "should not go below minimal dynos amount" do
50
+ @config.dynos['threshold'] = 50
51
+ @config.dynos['min'] = 2
52
+ @config.dynos['max'] = 2
53
+
54
+ @heroku.should_not_receive(:put_dynos)
55
+
56
+ @mangler.mangle_dynos!
57
+ end
58
+
59
+ it "should not go above dynos amount" do
60
+ @config.dynos['threshold'] = 30
61
+ @config.dynos['min'] = 1
62
+ @config.dynos['max'] = 2
63
+
64
+ @heroku.should_not_receive(:put_dynos)
65
+
66
+ @mangler.mangle_dynos!
67
+ end
68
+
69
+ end
70
+ end
@@ -0,0 +1,50 @@
1
+ require 'spec_helper'
2
+
3
+ describe SaveYourDosh::NewRelic do
4
+ before do
5
+ @config = SaveYourDosh.config
6
+ @config.new_relic = {
7
+ 'acc_id' => 'acc-id',
8
+ 'app_id' => 'app-name',
9
+ 'api_key' => 'api-key'
10
+ }
11
+ end
12
+
13
+ describe ".get_dynos_load" do
14
+ it "should make a request to the server for the data" do
15
+ SaveYourDosh::NewRelic.should_receive(:`).
16
+
17
+ with(%Q{
18
+ curl --silent -H "x-api-key: #{@config.new_relic['api_key']}" \
19
+ -d "metrics[]=Instance/Busy" \
20
+ -d "field=busy_percent" \
21
+ -d "begin=#{Time.now - @config.interval * 60}" \
22
+ -d "end=#{Time.now}" \
23
+ -d "summary=1" \
24
+ -d "app=#{@config.new_relic['app_id']}" \
25
+ https://api.newrelic.com/api/v1/accounts/#{@config.new_relic['acc_id']}/metrics/data.json
26
+ }.strip).
27
+
28
+ and_return %Q{
29
+ [{"name":"Instance/Busy","app":"doshmosh","agent_id":513715,"busy_percent":2.299166620165731}]
30
+ }
31
+
32
+ SaveYourDosh::NewRelic.get_dynos_load.should == 2.299166620165731
33
+ end
34
+
35
+ it "should return nil if the server returns not JSON" do
36
+ SaveYourDosh::NewRelic.should_receive(:`).
37
+ and_return("Fuck you buddy")
38
+
39
+ SaveYourDosh::NewRelic.get_dynos_load.should == nil
40
+ end
41
+
42
+ it "should return nil if the server returns wrong JSON" do
43
+ SaveYourDosh::NewRelic.should_receive(:`).
44
+ and_return('{"fuck": "you buddy"}')
45
+
46
+ SaveYourDosh::NewRelic.get_dynos_load.should == nil
47
+ end
48
+ end
49
+
50
+ end
@@ -0,0 +1,27 @@
1
+ require 'rspec'
2
+ require 'heroku-api'
3
+
4
+ require File.dirname(__FILE__) + "/../lib/save_your_dosh.rb"
5
+
6
+ # mocking the Kernel::` calls in the new-relic API proxy
7
+ class SaveYourDosh::NewRelic
8
+ def self.`(str)
9
+ ''
10
+ end
11
+ end
12
+
13
+ # disabling the Horoku::Client real interactions
14
+ class Heroku::API
15
+ def initialize(*args)
16
+ end
17
+
18
+ def get_app(name)
19
+ Struct.new(:body).new({'dynos' => 1, 'workers' => 1})
20
+ end
21
+
22
+ def put_dynos(name, qty)
23
+ end
24
+
25
+ def put_workers(name, qty)
26
+ end
27
+ end
metadata ADDED
@@ -0,0 +1,60 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: save-your-dosh
3
+ version: !ruby/object:Gem::Version
4
+ version: 1.0.0
5
+ prerelease:
6
+ platform: ruby
7
+ authors:
8
+ - Nikolay Nemshilov
9
+ autorequire:
10
+ bindir: bin
11
+ cert_chain: []
12
+ date: 2012-12-20 00:00:00.000000000 Z
13
+ dependencies: []
14
+ description: This gem can automatically scale the dynos amount on your heroku app
15
+ depending on your system busyness
16
+ email: nemshilov@gmail.com
17
+ executables: []
18
+ extensions: []
19
+ extra_rdoc_files: []
20
+ files:
21
+ - lib/save-your-dosh.rb
22
+ - lib/save_your_dosh/config.rb
23
+ - lib/save_your_dosh/mangler.rb
24
+ - lib/save_your_dosh/new_relic.rb
25
+ - lib/save_your_dosh.rb
26
+ - lib/save_your_dosh.yml
27
+ - lib/tasks/save_your_dosh.rake
28
+ - spec/lib/save_your_dosh/config_spec.rb
29
+ - spec/lib/save_your_dosh/mangler_spec.rb
30
+ - spec/lib/save_your_dosh/new_relic_spec.rb
31
+ - spec/spec_helper.rb
32
+ - README.md
33
+ - init.rb
34
+ - Gemfile
35
+ - Gemfile.lock
36
+ homepage: http://github.com/MadRabbit/save-your-dosh
37
+ licenses: []
38
+ post_install_message:
39
+ rdoc_options: []
40
+ require_paths:
41
+ - lib
42
+ required_ruby_version: !ruby/object:Gem::Requirement
43
+ none: false
44
+ requirements:
45
+ - - ! '>='
46
+ - !ruby/object:Gem::Version
47
+ version: '0'
48
+ required_rubygems_version: !ruby/object:Gem::Requirement
49
+ none: false
50
+ requirements:
51
+ - - ! '>='
52
+ - !ruby/object:Gem::Version
53
+ version: '0'
54
+ requirements: []
55
+ rubyforge_project:
56
+ rubygems_version: 1.8.23
57
+ signing_key:
58
+ specification_version: 3
59
+ summary: Heroku dynos auto-scaling thing
60
+ test_files: []