alicorn 0.0.10 → 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/Gemfile +1 -0
- data/Gemfile.lock +6 -0
- data/Rakefile +3 -0
- data/alicorn.gemspec +10 -3
- data/bin/alicorn +7 -1
- data/bin/alicorn_profiler +4 -29
- data/lib/alicorn.rb +1 -0
- data/lib/alicorn/dataset.rb +0 -11
- data/lib/alicorn/log_parser.rb +1 -0
- data/lib/alicorn/profiler.rb +38 -0
- data/lib/alicorn/scaler.rb +56 -40
- data/lib/alicorn/version.rb +3 -0
- data/test/fixtures/sample.alicorn.log +14 -0
- data/test/helper.rb +5 -0
- data/test/test_log_parser.rb +28 -0
- data/test/test_profiler.rb +28 -0
- data/test/test_scaler.rb +114 -15
- metadata +30 -15
- data/VERSION +0 -1
data/Gemfile
CHANGED
data/Gemfile.lock
CHANGED
@@ -12,6 +12,7 @@ GEM
|
|
12
12
|
metaclass (0.0.1)
|
13
13
|
mocha (0.11.4)
|
14
14
|
metaclass (~> 0.0.1)
|
15
|
+
multi_json (1.3.4)
|
15
16
|
rake (0.9.2.2)
|
16
17
|
rdoc (3.12)
|
17
18
|
json (~> 1.4)
|
@@ -20,6 +21,10 @@ GEM
|
|
20
21
|
shoulda-matchers (~> 1.0.0)
|
21
22
|
shoulda-context (1.0.0)
|
22
23
|
shoulda-matchers (1.0.0)
|
24
|
+
simplecov (0.6.4)
|
25
|
+
multi_json (~> 1.0)
|
26
|
+
simplecov-html (~> 0.5.3)
|
27
|
+
simplecov-html (0.5.3)
|
23
28
|
|
24
29
|
PLATFORMS
|
25
30
|
ruby
|
@@ -31,3 +36,4 @@ DEPENDENCIES
|
|
31
36
|
mocha (~> 0.11.4)
|
32
37
|
rdoc (~> 3.12)
|
33
38
|
shoulda (~> 3.0.1)
|
39
|
+
simplecov (~> 0.6.4)
|
data/Rakefile
CHANGED
@@ -1,4 +1,5 @@
|
|
1
1
|
# encoding: utf-8
|
2
|
+
$:.push File.join(File.dirname(__FILE__),'lib')
|
2
3
|
|
3
4
|
require 'rubygems'
|
4
5
|
require 'bundler'
|
@@ -11,10 +12,12 @@ rescue Bundler::BundlerError => e
|
|
11
12
|
end
|
12
13
|
require 'rake'
|
13
14
|
|
15
|
+
require 'alicorn'
|
14
16
|
require 'jeweler'
|
15
17
|
Jeweler::Tasks.new do |gem|
|
16
18
|
# gem is a Gem::Specification... see http://docs.rubygems.org/read/chapter/20 for more options
|
17
19
|
gem.name = "alicorn"
|
20
|
+
gem.version = Alicorn::VERSION
|
18
21
|
gem.homepage = "http://github.com/bensomers/alicorn"
|
19
22
|
gem.license = "MIT"
|
20
23
|
gem.summary = %Q{Standalone auto-scaler for unicorn}
|
data/alicorn.gemspec
CHANGED
@@ -5,11 +5,11 @@
|
|
5
5
|
|
6
6
|
Gem::Specification.new do |s|
|
7
7
|
s.name = "alicorn"
|
8
|
-
s.version = "0.0
|
8
|
+
s.version = "0.1.0"
|
9
9
|
|
10
10
|
s.required_rubygems_version = Gem::Requirement.new(">= 0") if s.respond_to? :required_rubygems_version=
|
11
11
|
s.authors = ["Ben Somers"]
|
12
|
-
s.date = "2012-05-
|
12
|
+
s.date = "2012-05-22"
|
13
13
|
s.description = "Highly configurable dumb auto-scaler for managing unicorn web servers"
|
14
14
|
s.email = "somers.ben@gmail.com"
|
15
15
|
s.executables = ["alicorn", "alicorn_profiler"]
|
@@ -24,15 +24,19 @@ Gem::Specification.new do |s|
|
|
24
24
|
"LICENSE.txt",
|
25
25
|
"README.rdoc",
|
26
26
|
"Rakefile",
|
27
|
-
"VERSION",
|
28
27
|
"alicorn.gemspec",
|
29
28
|
"bin/alicorn",
|
30
29
|
"bin/alicorn_profiler",
|
31
30
|
"lib/alicorn.rb",
|
32
31
|
"lib/alicorn/dataset.rb",
|
33
32
|
"lib/alicorn/log_parser.rb",
|
33
|
+
"lib/alicorn/profiler.rb",
|
34
34
|
"lib/alicorn/scaler.rb",
|
35
|
+
"lib/alicorn/version.rb",
|
36
|
+
"test/fixtures/sample.alicorn.log",
|
35
37
|
"test/helper.rb",
|
38
|
+
"test/test_log_parser.rb",
|
39
|
+
"test/test_profiler.rb",
|
36
40
|
"test/test_scaler.rb"
|
37
41
|
]
|
38
42
|
s.homepage = "http://github.com/bensomers/alicorn"
|
@@ -51,6 +55,7 @@ Gem::Specification.new do |s|
|
|
51
55
|
s.add_development_dependency(%q<bundler>, ["~> 1.1.3"])
|
52
56
|
s.add_development_dependency(%q<jeweler>, ["~> 1.8.3"])
|
53
57
|
s.add_development_dependency(%q<mocha>, ["~> 0.11.4"])
|
58
|
+
s.add_development_dependency(%q<simplecov>, ["~> 0.6.4"])
|
54
59
|
else
|
55
60
|
s.add_dependency(%q<curb>, ["~> 0.8.0"])
|
56
61
|
s.add_dependency(%q<shoulda>, ["~> 3.0.1"])
|
@@ -58,6 +63,7 @@ Gem::Specification.new do |s|
|
|
58
63
|
s.add_dependency(%q<bundler>, ["~> 1.1.3"])
|
59
64
|
s.add_dependency(%q<jeweler>, ["~> 1.8.3"])
|
60
65
|
s.add_dependency(%q<mocha>, ["~> 0.11.4"])
|
66
|
+
s.add_dependency(%q<simplecov>, ["~> 0.6.4"])
|
61
67
|
end
|
62
68
|
else
|
63
69
|
s.add_dependency(%q<curb>, ["~> 0.8.0"])
|
@@ -66,6 +72,7 @@ Gem::Specification.new do |s|
|
|
66
72
|
s.add_dependency(%q<bundler>, ["~> 1.1.3"])
|
67
73
|
s.add_dependency(%q<jeweler>, ["~> 1.8.3"])
|
68
74
|
s.add_dependency(%q<mocha>, ["~> 0.11.4"])
|
75
|
+
s.add_dependency(%q<simplecov>, ["~> 0.6.4"])
|
69
76
|
end
|
70
77
|
end
|
71
78
|
|
data/bin/alicorn
CHANGED
@@ -9,11 +9,17 @@ OptionParser.new do |opts|
|
|
9
9
|
It relies on unicorn's built-in Raindrops middleware (so make sure you have
|
10
10
|
enabled it). It computes rough load scores based on separately averaged
|
11
11
|
data sets. \n\nUsage: alicorn [options]"
|
12
|
-
|
12
|
+
|
13
|
+
opts.on('-h', '--help', 'Display this screen') do
|
13
14
|
puts opts
|
14
15
|
exit
|
15
16
|
end
|
16
17
|
|
18
|
+
opts.on('--version', "version number") do
|
19
|
+
puts Alicorn::VERSION
|
20
|
+
exit
|
21
|
+
end
|
22
|
+
|
17
23
|
opts.on("--min-workers [N]", Integer, "the minimum number of workers to scale down to") do |v|
|
18
24
|
options[:min_workers] = v
|
19
25
|
end
|
data/bin/alicorn_profiler
CHANGED
@@ -3,7 +3,7 @@ $:.push File.join(File.dirname(__FILE__),'..','lib')
|
|
3
3
|
|
4
4
|
require 'optparse'
|
5
5
|
require 'alicorn'
|
6
|
-
require 'alicorn/
|
6
|
+
require 'alicorn/profiler'
|
7
7
|
|
8
8
|
|
9
9
|
options = {}
|
@@ -50,32 +50,7 @@ uses to trigger panic responses (which produce fast scale-ups).
|
|
50
50
|
end
|
51
51
|
end.parse!
|
52
52
|
|
53
|
-
|
54
|
-
# The log file is the output of the sampling-mode alicorn
|
55
|
-
# The options are a stripped-down option set, including only
|
56
|
-
# the options that matter to the scaling algorithm.
|
57
|
-
@log_file = options[:log_path]
|
58
|
-
@options = { :min_workers => options[:min_workers],
|
59
|
-
:max_workers => options[:max_workers],
|
60
|
-
:target_ratio => options[:target_ration],
|
61
|
-
:buffer => options[:buffer],
|
62
|
-
:debug => true,
|
63
|
-
:dry_run => true}
|
53
|
+
options.merge!(:debug => true, :dry_run => true)
|
64
54
|
|
65
|
-
|
66
|
-
|
67
|
-
|
68
|
-
scaler = Alicorn::Scaler.new(@options)
|
69
|
-
@worker_count = @options[:max_workers]
|
70
|
-
|
71
|
-
p "Testing #{@alp.samples.count} samples"
|
72
|
-
@alp.samples.each do |sample|
|
73
|
-
connections = sample[:active].zip(sample[:queued]).map { |e| e.inject(:+) }
|
74
|
-
if connections.max > @worker_count
|
75
|
-
p "Overloaded! Ran #{@worker_count} and got #{connections.max} active + queued"
|
76
|
-
p sample if options[:verbose]
|
77
|
-
end
|
78
|
-
sig, count = scaler.send(:auto_scale, sample, @worker_count)
|
79
|
-
@worker_count += count if sig == "TTIN"
|
80
|
-
@worker_count -= count if sig == "TTOU"
|
81
|
-
end
|
55
|
+
profiler = Alicorn::Profiler.new(options)
|
56
|
+
profiler.profile
|
data/lib/alicorn.rb
CHANGED
data/lib/alicorn/dataset.rb
CHANGED
@@ -3,15 +3,4 @@ class DataSet < Array
|
|
3
3
|
return 0 if empty?
|
4
4
|
inject(:+) / size.to_i
|
5
5
|
end
|
6
|
-
|
7
|
-
def variance
|
8
|
-
return nil if empty?
|
9
|
-
sum=self.inject(0){|acc,i|acc +(i-avg)**2}
|
10
|
-
return(1/self.length.to_f*sum)
|
11
|
-
end
|
12
|
-
|
13
|
-
def stddev
|
14
|
-
return nil if empty?
|
15
|
-
Math.sqrt(self.variance)
|
16
|
-
end
|
17
6
|
end
|
data/lib/alicorn/log_parser.rb
CHANGED
@@ -0,0 +1,38 @@
|
|
1
|
+
require 'alicorn/log_parser'
|
2
|
+
|
3
|
+
module Alicorn
|
4
|
+
class Profiler
|
5
|
+
attr_accessor :log_path, :min_workers, :max_workers, :target_ratio, :buffer,
|
6
|
+
:worker_count, :verbose, :out
|
7
|
+
|
8
|
+
def initialize(options = {})
|
9
|
+
@log_path = options[:log_path]
|
10
|
+
@min_workers = options[:min_workers]
|
11
|
+
@max_workers = options[:max_workers]
|
12
|
+
@target_ratio = options[:target_ratio]
|
13
|
+
@buffer = options[:buffer]
|
14
|
+
@verbose = options.delete(:verbose)
|
15
|
+
|
16
|
+
@out = options[:test] ? File.open("/dev/null") : STDOUT
|
17
|
+
@worker_count = @max_workers
|
18
|
+
@scaler = Scaler.new(options.merge(:log_path => nil))
|
19
|
+
@alp = LogParser.new(@log_path)
|
20
|
+
end
|
21
|
+
|
22
|
+
def profile
|
23
|
+
samples = @alp.parse
|
24
|
+
@out.puts "Profiling #{samples.count} samples"
|
25
|
+
|
26
|
+
samples.each do |sample|
|
27
|
+
connections = sample[:active].zip(sample[:queued]).map { |e| e.inject(:+) }
|
28
|
+
if connections.max > @worker_count
|
29
|
+
@out.puts "Overloaded! Ran #{@worker_count} and got #{connections.max} active + queued"
|
30
|
+
@out.puts sample if @verbose
|
31
|
+
end
|
32
|
+
sig, count = @scaler.auto_scale(sample, @worker_count)
|
33
|
+
@worker_count += count if sig == "TTIN"
|
34
|
+
@worker_count -= count if sig == "TTOU"
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
data/lib/alicorn/scaler.rb
CHANGED
@@ -7,27 +7,29 @@ module Alicorn
|
|
7
7
|
attr_accessor :min_workers, :max_workers, :target_ratio, :buffer,
|
8
8
|
:raindrops_url, :delay, :sample_count, :app_name, :dry_run, :logger
|
9
9
|
|
10
|
+
attr_reader :signal_delay
|
11
|
+
|
10
12
|
def initialize(options = {})
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
13
|
+
@min_workers = options[:min_workers] || 1
|
14
|
+
@max_workers = options[:max_workers]
|
15
|
+
@target_ratio = options[:target_ratio] || 1.3
|
16
|
+
@buffer = options[:buffer] || 2
|
17
|
+
@raindrops_url = options[:url] || "http://127.0.0.1/_raindrops"
|
18
|
+
@delay = options[:delay] || 1
|
19
|
+
@sample_count = options[:sample_count] || 30
|
20
|
+
@app_name = options[:app_name] || "unicorn"
|
21
|
+
@dry_run = options[:dry_run]
|
22
|
+
@signal_delay = 1
|
23
|
+
log_path = options[:log_path] || "/dev/null"
|
21
24
|
|
22
25
|
self.logger = Logger.new(log_path)
|
23
26
|
logger.level = options[:verbose] ? Logger::DEBUG : Logger::WARN
|
24
27
|
end
|
25
28
|
|
26
|
-
def scale
|
29
|
+
def scale
|
27
30
|
data = collect_data
|
28
31
|
unicorns = find_unicorns
|
29
32
|
|
30
|
-
abort "Could not find any unicorn processes" if unicorns.empty?
|
31
33
|
master_pid = find_master_pid(unicorns)
|
32
34
|
worker_count = find_worker_count(unicorns)
|
33
35
|
|
@@ -35,13 +37,14 @@ module Alicorn
|
|
35
37
|
if sig and !dry_run
|
36
38
|
number.times do
|
37
39
|
send_signal(master_pid, sig)
|
38
|
-
sleep(
|
40
|
+
sleep(signal_delay) # Make sure unicorn doesn't discard repeated signals
|
39
41
|
end
|
40
42
|
end
|
43
|
+
rescue Exception => e
|
44
|
+
logger.error "exception occurred: #{e.class}\n\n#{e.message}"
|
45
|
+
raise e
|
41
46
|
end
|
42
47
|
|
43
|
-
protected
|
44
|
-
|
45
48
|
def auto_scale(data, worker_count)
|
46
49
|
return nil if data[:active].empty? or data[:queued].empty?
|
47
50
|
connections = data[:active].zip(data[:queued]).map { |e| e.inject(:+) }
|
@@ -56,8 +59,11 @@ module Alicorn
|
|
56
59
|
target = target.ceil
|
57
60
|
|
58
61
|
logger.debug "target calculated at: #{target}, worker count at #{worker_count}"
|
59
|
-
if
|
60
|
-
logger.
|
62
|
+
if target >= max_workers
|
63
|
+
logger.warn "at maximum capacity! cannot scale up"
|
64
|
+
return nil, 0
|
65
|
+
elsif connections.avg > worker_count and data[:queued].avg > 1
|
66
|
+
logger.warn "danger, will robinson! scaling up fast!"
|
61
67
|
return "TTIN", target - worker_count
|
62
68
|
elsif target > worker_count
|
63
69
|
logger.debug "scaling up!"
|
@@ -71,26 +77,7 @@ module Alicorn
|
|
71
77
|
end
|
72
78
|
end
|
73
79
|
|
74
|
-
|
75
|
-
master_lines = unicorns.select { |line| line.match /master/ }
|
76
|
-
if master_lines.size > 1
|
77
|
-
abort "Too many unicorn master processes detected. You may be restarting, or have an app name collision: #{master_lines}"
|
78
|
-
elsif master_lines.first.match /\(old\)/
|
79
|
-
abort "Old master process detected. You may be restarting: #{master_lines.first}"
|
80
|
-
else
|
81
|
-
master_lines.first.split.first.to_i
|
82
|
-
end
|
83
|
-
end
|
84
|
-
|
85
|
-
def find_worker_count(unicorns)
|
86
|
-
unicorns.select { |line| line.match /worker\[[\d]\]/ }.count
|
87
|
-
end
|
88
|
-
|
89
|
-
def find_unicorns
|
90
|
-
ptable = %x(ps ax).split("\n")
|
91
|
-
unicorns = ptable.select { |line| line.match(/unicorn/) && line.match(/#{Regexp.escape(app_name)}/) }
|
92
|
-
unicorns.map(&:strip)
|
93
|
-
end
|
80
|
+
protected
|
94
81
|
|
95
82
|
def collect_data
|
96
83
|
logger.debug "Sampling #{sample_count} times"
|
@@ -113,15 +100,44 @@ module Alicorn
|
|
113
100
|
{:calling => calling, :writing => writing, :active => active, :queued => queued}
|
114
101
|
end
|
115
102
|
|
103
|
+
private
|
104
|
+
|
105
|
+
# Raises errors if the master is busy restarting, or we can't be certain which PID to signal
|
106
|
+
def find_master_pid(unicorns)
|
107
|
+
master_lines = unicorns.select { |line| line.match /master/ }
|
108
|
+
if master_lines.size == 0
|
109
|
+
raise "No unicorn master processes detected. You may still be starting up."
|
110
|
+
elsif master_lines.size > 1
|
111
|
+
raise "Too many unicorn master processes detected. You may be restarting, or have an app name collision: #{master_lines}"
|
112
|
+
elsif master_lines.first.match /\(old\)/
|
113
|
+
raise "Old master process detected. You may be restarting: #{master_lines.first}"
|
114
|
+
else
|
115
|
+
master_lines.first.split.first.to_i
|
116
|
+
end
|
117
|
+
end
|
118
|
+
|
119
|
+
def find_worker_count(unicorns)
|
120
|
+
unicorns.select { |line| line.match /worker\[[\d+]\]/ }.count
|
121
|
+
end
|
122
|
+
|
123
|
+
def find_unicorns
|
124
|
+
ptable = grep_process_list.split("\n")
|
125
|
+
unicorns = ptable.select { |line| line.match(/unicorn/) && line.match(/#{Regexp.escape(app_name)}/) }
|
126
|
+
raise "Could not find any unicorn processes" if unicorns.empty?
|
127
|
+
|
128
|
+
unicorns.map(&:strip)
|
129
|
+
end
|
130
|
+
|
131
|
+
def grep_process_list
|
132
|
+
%x(ps ax)
|
133
|
+
end
|
134
|
+
|
116
135
|
def send_signal(master_pid, sig)
|
117
136
|
Process.kill(sig, master_pid)
|
118
137
|
end
|
119
138
|
|
120
|
-
private
|
121
|
-
|
122
139
|
def get_raindrops(url)
|
123
140
|
Curl::Easy.http_get(url).body_str.split("\n")
|
124
141
|
end
|
125
|
-
|
126
142
|
end
|
127
143
|
end
|
@@ -0,0 +1,14 @@
|
|
1
|
+
D, [2012-05-17T14:45:02.092344 #26889] DEBUG -- : Sampling 3 times
|
2
|
+
D, [2012-05-17T14:45:32.901373 #26889] DEBUG -- : Collected:
|
3
|
+
D, [2012-05-17T14:45:32.901563 #26889] DEBUG -- : calling:[1, 2, 1]
|
4
|
+
D, [2012-05-17T14:45:32.901960 #26889] DEBUG -- : writing:[2, 1, 2]
|
5
|
+
D, [2012-05-17T14:45:32.902297 #26889] DEBUG -- : active:[1, 4, 3]
|
6
|
+
D, [2012-05-17T14:45:32.902606 #26889] DEBUG -- : queued:[0, 2, 1]
|
7
|
+
D, [2012-05-17T14:45:32.902944 #26889] DEBUG -- : target calculated at: 4, worker count at 2
|
8
|
+
D, [2012-05-17T14:50:01.575204 #27727] DEBUG -- : Sampling 5 times
|
9
|
+
D, [2012-05-17T14:50:31.697833 #27727] DEBUG -- : Collected:
|
10
|
+
D, [2012-05-17T14:50:31.698001 #27727] DEBUG -- : calling:[11, 12, 11, 13, 14]
|
11
|
+
D, [2012-05-17T14:50:31.698452 #27727] DEBUG -- : writing:[12, 11, 12, 14, 13]
|
12
|
+
D, [2012-05-17T14:50:31.698792 #27727] DEBUG -- : active:[11, 14, 13, 12, 11]
|
13
|
+
D, [2012-05-17T14:50:31.699107 #27727] DEBUG -- : queued:[10, 12, 11, 11, 12]
|
14
|
+
D, [2012-05-17T14:50:31.699448 #27727] DEBUG -- : target calculated at: 14, worker count at 12
|
data/test/helper.rb
CHANGED
@@ -0,0 +1,28 @@
|
|
1
|
+
require 'helper'
|
2
|
+
require 'alicorn/log_parser'
|
3
|
+
|
4
|
+
class TestLogParser < Test::Unit::TestCase
|
5
|
+
|
6
|
+
def setup
|
7
|
+
@alp = Alicorn::LogParser.new("test/fixtures/sample.alicorn.log")
|
8
|
+
end
|
9
|
+
|
10
|
+
context "#parse" do
|
11
|
+
should "correctly read log file" do
|
12
|
+
expected = [ { :calling => DataSet.new([1,2,1]),
|
13
|
+
:writing => DataSet.new([2,1,2]),
|
14
|
+
:active => DataSet.new([1,4,3]),
|
15
|
+
:queued => DataSet.new([0,2,1]) },
|
16
|
+
{ :calling => DataSet.new([11, 12, 11, 13, 14]),
|
17
|
+
:writing => DataSet.new([12, 11, 12, 14, 13]),
|
18
|
+
:active => DataSet.new([11, 14, 13, 12, 11]),
|
19
|
+
:queued => DataSet.new([10, 12, 11, 11, 12]) }
|
20
|
+
]
|
21
|
+
|
22
|
+
@alp.parse
|
23
|
+
|
24
|
+
assert_equal 2, @alp.samples.count
|
25
|
+
assert_equal expected, @alp.samples
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
@@ -0,0 +1,28 @@
|
|
1
|
+
require 'helper'
|
2
|
+
require 'alicorn/profiler'
|
3
|
+
|
4
|
+
class TestProfiler < Test::Unit::TestCase
|
5
|
+
|
6
|
+
def setup
|
7
|
+
@prof = Alicorn::Profiler.new(:log_path => "test/fixtures/sample.alicorn.log",
|
8
|
+
:min_workers => 2,
|
9
|
+
:max_workers => 10,
|
10
|
+
:target_ratio => 1.3,
|
11
|
+
:buffer => 1,
|
12
|
+
:test => true)
|
13
|
+
@out = StringIO.new
|
14
|
+
@prof.out = @out
|
15
|
+
end
|
16
|
+
|
17
|
+
context "#profile" do
|
18
|
+
should "simulate real-world alicorn runs" do
|
19
|
+
Alicorn::Scaler.any_instance.stubs(:auto_scale).returns(["TTIN", 1], ["TTOU", 3])
|
20
|
+
@out.expects(:puts).with("Profiling 2 samples")
|
21
|
+
@out.expects(:puts).with { |string| string.match /Overloaded! Ran 11 and got 26/ }
|
22
|
+
|
23
|
+
@prof.profile
|
24
|
+
assert_equal 8, @prof.worker_count
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
end
|
data/test/test_scaler.rb
CHANGED
@@ -4,27 +4,112 @@ class TestScaler < Test::Unit::TestCase
|
|
4
4
|
|
5
5
|
def setup
|
6
6
|
@scaler = Alicorn::Scaler.new(:delay => 0)
|
7
|
+
|
8
|
+
# stub out external call
|
9
|
+
raindrops = "calling: 3\nwriting: 1\n/tmp/cart.socket active: 4\n/tmp/cart.socket queued: 0\n"
|
10
|
+
Curl::Easy.stubs(:http_get).returns(stub(:body_str => raindrops))
|
11
|
+
|
12
|
+
# enable us to test private methods, for the complicated ones
|
7
13
|
class << @scaler
|
8
|
-
def publicize(
|
9
|
-
|
14
|
+
def publicize(*methods)
|
15
|
+
methods.each do |meth|
|
16
|
+
self.class.class_eval { public meth }
|
17
|
+
end
|
10
18
|
end
|
11
19
|
end
|
12
20
|
end
|
13
21
|
|
14
|
-
context "#scale
|
22
|
+
context "#scale" do
|
15
23
|
context "when no unicorn processes are running" do
|
24
|
+
setup do
|
25
|
+
@scaler.stubs(:grep_process_list).returns("foo\nbar")
|
26
|
+
end
|
27
|
+
|
28
|
+
should "raise an error" do
|
29
|
+
exception = assert_raise(RuntimeError) do
|
30
|
+
@scaler.scale
|
31
|
+
end
|
32
|
+
assert_equal "Could not find any unicorn processes", exception.message
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
context "when no master processes are running" do
|
37
|
+
setup do
|
38
|
+
plist = "1050 ? Sl 1:06 unicorn_rails worker[0] -c first_unicorn.rb -E staging -D\n
|
39
|
+
1051 ? Sl 1:06 unicorn_rails worker[1] -c other_unicorn.rb -E staging -D\n"
|
40
|
+
@scaler.stubs(:grep_process_list).returns(plist)
|
41
|
+
end
|
42
|
+
|
43
|
+
should "raise an error" do
|
44
|
+
exception = assert_raise(RuntimeError) do
|
45
|
+
@scaler.scale
|
46
|
+
end
|
47
|
+
assert_match /No unicorn master processes/, exception.message
|
48
|
+
end
|
16
49
|
end
|
50
|
+
|
17
51
|
context "when multiple master processes are found" do
|
52
|
+
setup do
|
53
|
+
plist = "1050 ? Sl 1:06 unicorn_rails master -c first_unicorn.rb -E staging -D\n
|
54
|
+
1051 ? Sl 1:06 unicorn_rails master -c other_unicorn.rb -E staging -D\n"
|
55
|
+
@scaler.stubs(:grep_process_list).returns(plist)
|
56
|
+
end
|
57
|
+
|
58
|
+
should "raise an error" do
|
59
|
+
exception = assert_raise(RuntimeError) do
|
60
|
+
@scaler.scale
|
61
|
+
end
|
62
|
+
assert_match /Too many unicorn master processes/, exception.message
|
63
|
+
end
|
18
64
|
end
|
65
|
+
|
19
66
|
context "when an old master process is running" do
|
67
|
+
setup do
|
68
|
+
plist = "1050 ? Sl 1:06 unicorn_rails master (old) -c first_unicorn.rb -E staging -D\n
|
69
|
+
1051 ? Sl 1:06 unicorn_rails worker[0] -c other_unicorn.rb -E staging -D\n"
|
70
|
+
@scaler.stubs(:grep_process_list).returns(plist)
|
71
|
+
end
|
72
|
+
|
73
|
+
should "raise an error" do
|
74
|
+
exception = assert_raise(RuntimeError) do
|
75
|
+
@scaler.scale
|
76
|
+
end
|
77
|
+
assert_match /Old master process detected/, exception.message
|
78
|
+
end
|
20
79
|
end
|
80
|
+
|
21
81
|
context "when things are properly detected" do
|
82
|
+
setup do
|
83
|
+
plist = "1050 ? Sl 1:06 unicorn_rails master -c first_unicorn.rb -E staging -D\n
|
84
|
+
1051 ? Sl 1:06 unicorn_rails master -c other_unicorn.rb -E staging -D\n
|
85
|
+
1052 ? Sl 1:06 unicorn_rails worker[0] -c other_unicorn.rb -E staging -D\n
|
86
|
+
1053 ? Sl 1:06 unicorn_rails worker[1] -c other_unicorn.rb -E staging -D\n
|
87
|
+
1054 ? Sl 1:06 unicorn_rails worker[0] -c first_unicorn.rb -E staging -D\n"
|
88
|
+
@scaler.stubs(:grep_process_list).returns(plist)
|
89
|
+
@scaler.stubs(:auto_scale).returns(["NOTASIGNAL", 2])
|
90
|
+
@scaler.stubs(:signal_delay).returns(0)
|
91
|
+
@scaler.app_name = "first"
|
92
|
+
end
|
93
|
+
|
94
|
+
should "find the correct master and correct worker quantity" do
|
95
|
+
@scaler.publicize(:find_unicorns, :find_master_pid, :find_worker_count)
|
96
|
+
unicorns = @scaler.find_unicorns
|
97
|
+
|
98
|
+
assert_equal 1050, @scaler.find_master_pid(unicorns)
|
99
|
+
assert_equal 1, @scaler.find_worker_count(unicorns)
|
100
|
+
end
|
101
|
+
|
102
|
+
should "send the correct number of signals" do
|
103
|
+
@scaler.expects(:send_signal).with(1050, "NOTASIGNAL").twice
|
104
|
+
@scaler.scale
|
105
|
+
assert true
|
106
|
+
end
|
22
107
|
end
|
23
108
|
end
|
24
109
|
|
110
|
+
# Test the scaling algorithm separately for simplicity's sake
|
25
111
|
context "#auto_scale" do
|
26
112
|
setup do
|
27
|
-
@scaler.publicize :auto_scale
|
28
113
|
@worker_count = 10
|
29
114
|
@scaler.min_workers = 1
|
30
115
|
@scaler.max_workers = 25
|
@@ -35,7 +120,7 @@ class TestScaler < Test::Unit::TestCase
|
|
35
120
|
@data[:queued]
|
36
121
|
end
|
37
122
|
|
38
|
-
context "when we
|
123
|
+
context "when we need to scale down" do
|
39
124
|
setup do
|
40
125
|
@data[:active] << 3 << 4 << 5
|
41
126
|
@data[:queued] << 0 << 0 << 0
|
@@ -44,9 +129,19 @@ class TestScaler < Test::Unit::TestCase
|
|
44
129
|
should "return 1 TTOU" do
|
45
130
|
assert_equal ["TTOU", 1], @scaler.auto_scale(@data, @worker_count)
|
46
131
|
end
|
132
|
+
|
133
|
+
context "but we've hit our minimum workers" do
|
134
|
+
setup do
|
135
|
+
@scaler.min_workers = 10
|
136
|
+
end
|
137
|
+
|
138
|
+
should "return no signal" do
|
139
|
+
assert_equal [nil, 0], @scaler.auto_scale(@data, @worker_count)
|
140
|
+
end
|
141
|
+
end
|
47
142
|
end
|
48
143
|
|
49
|
-
context "when we
|
144
|
+
context "when we need to scale up" do
|
50
145
|
setup do
|
51
146
|
@data[:active] << 2 << 10
|
52
147
|
@data[:queued] << 0 << 0
|
@@ -55,9 +150,19 @@ class TestScaler < Test::Unit::TestCase
|
|
55
150
|
should "return 1 TTIN" do
|
56
151
|
assert_equal ["TTIN", 1], @scaler.auto_scale(@data, @worker_count)
|
57
152
|
end
|
153
|
+
|
154
|
+
context "but we've hit our maximum workers" do
|
155
|
+
setup do
|
156
|
+
@scaler.max_workers = 10
|
157
|
+
end
|
158
|
+
|
159
|
+
should "return no signal" do
|
160
|
+
assert_equal [nil, 0], @scaler.auto_scale(@data, @worker_count)
|
161
|
+
end
|
162
|
+
end
|
58
163
|
end
|
59
164
|
|
60
|
-
context "when we need to scale up fast" do
|
165
|
+
context "when we need to panic and scale up fast" do
|
61
166
|
setup do
|
62
167
|
@data[:queued] << 6
|
63
168
|
@data[:active] << 6
|
@@ -74,7 +179,7 @@ class TestScaler < Test::Unit::TestCase
|
|
74
179
|
@data[:queued] << 0
|
75
180
|
end
|
76
181
|
|
77
|
-
should "return
|
182
|
+
should "return no signal" do
|
78
183
|
assert_equal [nil, 0], @scaler.auto_scale(@data, @worker_count)
|
79
184
|
end
|
80
185
|
end
|
@@ -82,14 +187,8 @@ class TestScaler < Test::Unit::TestCase
|
|
82
187
|
end
|
83
188
|
|
84
189
|
context "#collect_data" do
|
85
|
-
setup do
|
86
|
-
@scaler.publicize :collect_data
|
87
|
-
raindrops = "calling: 3\nwriting: 1\n/tmp/cart.socket active: 4\n/tmp/cart.socket queued: 0\n"
|
88
|
-
Curl::Easy.stubs(:http_get).returns(stub(:body_str => raindrops))
|
89
|
-
end
|
90
|
-
|
91
190
|
should "return the correct data" do
|
92
|
-
data = @scaler.collect_data
|
191
|
+
data = @scaler.send(:collect_data)
|
93
192
|
expected_data = { :calling => [3]*30,
|
94
193
|
:writing => [1]*30,
|
95
194
|
:active => [4]*30,
|
metadata
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: alicorn
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.0
|
4
|
+
version: 0.1.0
|
5
5
|
prerelease:
|
6
6
|
platform: ruby
|
7
7
|
authors:
|
@@ -13,7 +13,7 @@ date: 2012-05-22 00:00:00.000000000 Z
|
|
13
13
|
dependencies:
|
14
14
|
- !ruby/object:Gem::Dependency
|
15
15
|
name: curb
|
16
|
-
requirement: &
|
16
|
+
requirement: &70298610808540 !ruby/object:Gem::Requirement
|
17
17
|
none: false
|
18
18
|
requirements:
|
19
19
|
- - ~>
|
@@ -21,10 +21,10 @@ dependencies:
|
|
21
21
|
version: 0.8.0
|
22
22
|
type: :runtime
|
23
23
|
prerelease: false
|
24
|
-
version_requirements: *
|
24
|
+
version_requirements: *70298610808540
|
25
25
|
- !ruby/object:Gem::Dependency
|
26
26
|
name: shoulda
|
27
|
-
requirement: &
|
27
|
+
requirement: &70298610808060 !ruby/object:Gem::Requirement
|
28
28
|
none: false
|
29
29
|
requirements:
|
30
30
|
- - ~>
|
@@ -32,10 +32,10 @@ dependencies:
|
|
32
32
|
version: 3.0.1
|
33
33
|
type: :development
|
34
34
|
prerelease: false
|
35
|
-
version_requirements: *
|
35
|
+
version_requirements: *70298610808060
|
36
36
|
- !ruby/object:Gem::Dependency
|
37
37
|
name: rdoc
|
38
|
-
requirement: &
|
38
|
+
requirement: &70298610807580 !ruby/object:Gem::Requirement
|
39
39
|
none: false
|
40
40
|
requirements:
|
41
41
|
- - ~>
|
@@ -43,10 +43,10 @@ dependencies:
|
|
43
43
|
version: '3.12'
|
44
44
|
type: :development
|
45
45
|
prerelease: false
|
46
|
-
version_requirements: *
|
46
|
+
version_requirements: *70298610807580
|
47
47
|
- !ruby/object:Gem::Dependency
|
48
48
|
name: bundler
|
49
|
-
requirement: &
|
49
|
+
requirement: &70298610807100 !ruby/object:Gem::Requirement
|
50
50
|
none: false
|
51
51
|
requirements:
|
52
52
|
- - ~>
|
@@ -54,10 +54,10 @@ dependencies:
|
|
54
54
|
version: 1.1.3
|
55
55
|
type: :development
|
56
56
|
prerelease: false
|
57
|
-
version_requirements: *
|
57
|
+
version_requirements: *70298610807100
|
58
58
|
- !ruby/object:Gem::Dependency
|
59
59
|
name: jeweler
|
60
|
-
requirement: &
|
60
|
+
requirement: &70298610806620 !ruby/object:Gem::Requirement
|
61
61
|
none: false
|
62
62
|
requirements:
|
63
63
|
- - ~>
|
@@ -65,10 +65,10 @@ dependencies:
|
|
65
65
|
version: 1.8.3
|
66
66
|
type: :development
|
67
67
|
prerelease: false
|
68
|
-
version_requirements: *
|
68
|
+
version_requirements: *70298610806620
|
69
69
|
- !ruby/object:Gem::Dependency
|
70
70
|
name: mocha
|
71
|
-
requirement: &
|
71
|
+
requirement: &70298610806140 !ruby/object:Gem::Requirement
|
72
72
|
none: false
|
73
73
|
requirements:
|
74
74
|
- - ~>
|
@@ -76,7 +76,18 @@ dependencies:
|
|
76
76
|
version: 0.11.4
|
77
77
|
type: :development
|
78
78
|
prerelease: false
|
79
|
-
version_requirements: *
|
79
|
+
version_requirements: *70298610806140
|
80
|
+
- !ruby/object:Gem::Dependency
|
81
|
+
name: simplecov
|
82
|
+
requirement: &70298610805600 !ruby/object:Gem::Requirement
|
83
|
+
none: false
|
84
|
+
requirements:
|
85
|
+
- - ~>
|
86
|
+
- !ruby/object:Gem::Version
|
87
|
+
version: 0.6.4
|
88
|
+
type: :development
|
89
|
+
prerelease: false
|
90
|
+
version_requirements: *70298610805600
|
80
91
|
description: Highly configurable dumb auto-scaler for managing unicorn web servers
|
81
92
|
email: somers.ben@gmail.com
|
82
93
|
executables:
|
@@ -93,15 +104,19 @@ files:
|
|
93
104
|
- LICENSE.txt
|
94
105
|
- README.rdoc
|
95
106
|
- Rakefile
|
96
|
-
- VERSION
|
97
107
|
- alicorn.gemspec
|
98
108
|
- bin/alicorn
|
99
109
|
- bin/alicorn_profiler
|
100
110
|
- lib/alicorn.rb
|
101
111
|
- lib/alicorn/dataset.rb
|
102
112
|
- lib/alicorn/log_parser.rb
|
113
|
+
- lib/alicorn/profiler.rb
|
103
114
|
- lib/alicorn/scaler.rb
|
115
|
+
- lib/alicorn/version.rb
|
116
|
+
- test/fixtures/sample.alicorn.log
|
104
117
|
- test/helper.rb
|
118
|
+
- test/test_log_parser.rb
|
119
|
+
- test/test_profiler.rb
|
105
120
|
- test/test_scaler.rb
|
106
121
|
homepage: http://github.com/bensomers/alicorn
|
107
122
|
licenses:
|
@@ -118,7 +133,7 @@ required_ruby_version: !ruby/object:Gem::Requirement
|
|
118
133
|
version: '0'
|
119
134
|
segments:
|
120
135
|
- 0
|
121
|
-
hash:
|
136
|
+
hash: 1604332443773651441
|
122
137
|
required_rubygems_version: !ruby/object:Gem::Requirement
|
123
138
|
none: false
|
124
139
|
requirements:
|
data/VERSION
DELETED
@@ -1 +0,0 @@
|
|
1
|
-
0.0.10
|