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 +2 -0
- data/Gemfile.lock +13 -0
- data/README.md +59 -0
- data/init.rb +1 -0
- data/lib/save-your-dosh.rb +4 -0
- data/lib/save_your_dosh.rb +28 -0
- data/lib/save_your_dosh.yml +13 -0
- data/lib/save_your_dosh/config.rb +36 -0
- data/lib/save_your_dosh/mangler.rb +47 -0
- data/lib/save_your_dosh/new_relic.rb +36 -0
- data/lib/tasks/save_your_dosh.rake +9 -0
- data/spec/lib/save_your_dosh/config_spec.rb +43 -0
- data/spec/lib/save_your_dosh/mangler_spec.rb +70 -0
- data/spec/lib/save_your_dosh/new_relic_spec.rb +50 -0
- data/spec/spec_helper.rb +27 -0
- metadata +60 -0
data/Gemfile
ADDED
data/Gemfile.lock
ADDED
data/README.md
ADDED
@@ -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,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,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,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
|
data/spec/spec_helper.rb
ADDED
@@ -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: []
|