save-your-dosh 1.0.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/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: []