alicorn 0.0.10 → 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
data/Gemfile CHANGED
@@ -8,4 +8,5 @@ group :development do
8
8
  gem "bundler", "~> 1.1.3"
9
9
  gem "jeweler", "~> 1.8.3"
10
10
  gem "mocha", "~> 0.11.4"
11
+ gem "simplecov", "~> 0.6.4", :require => false, :platforms => :ruby_19
11
12
  end
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.10"
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-21"
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
- opts.on('-h', '--help', 'Display this screen' ) do
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/log_parser'
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
- # Use these two variables to control the profiler
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
- @alp = Alicorn::LogParser.new(@log_file)
66
- @alp.parse
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
@@ -1,3 +1,4 @@
1
1
  module Alicorn; end
2
2
 
3
3
  require 'alicorn/scaler'
4
+ require 'alicorn/version'
@@ -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
@@ -27,6 +27,7 @@ module Alicorn
27
27
  samples << @sample_hash # store the old sample
28
28
  end
29
29
  end
30
+ samples
30
31
  end
31
32
  end
32
33
  end
@@ -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
@@ -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
- self.min_workers = options[:min_workers] || 1
12
- self.max_workers = options[:max_workers]
13
- self.target_ratio = options[:target_ratio] || 1.3
14
- self.buffer = options[:buffer] || 2
15
- self.raindrops_url = options[:url] || "http://127.0.0.1/_raindrops"
16
- self.delay = options[:delay] || 1
17
- self.sample_count = options[:sample_count] || 30
18
- self.app_name = options[:app_name] || "unicorn"
19
- self.dry_run = options[:dry_run]
20
- log_path = options[:log_path] || "/dev/null"
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(1) # Make sure unicorn doesn't discard repeated signals
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 connections.avg > worker_count and data[:queued].avg > 1
60
- logger.debug "danger, will robinson! scaling up fast!"
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
- def find_master_pid(unicorns)
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,3 @@
1
+ module Alicorn
2
+ VERSION = "0.1.0"
3
+ 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
@@ -1,3 +1,8 @@
1
+ unless RUBY_VERSION.match(/1\.8/)
2
+ require 'simplecov'
3
+ SimpleCov.start
4
+ end
5
+
1
6
  require 'rubygems'
2
7
  require 'bundler'
3
8
  begin
@@ -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(method)
9
- self.class.class_eval { public method }
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!" do
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're above the target" do
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're below the target" do
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 several TTIN" do
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.10
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: &70103116675180 !ruby/object:Gem::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: *70103116675180
24
+ version_requirements: *70298610808540
25
25
  - !ruby/object:Gem::Dependency
26
26
  name: shoulda
27
- requirement: &70103116673500 !ruby/object:Gem::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: *70103116673500
35
+ version_requirements: *70298610808060
36
36
  - !ruby/object:Gem::Dependency
37
37
  name: rdoc
38
- requirement: &70103116672420 !ruby/object:Gem::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: *70103116672420
46
+ version_requirements: *70298610807580
47
47
  - !ruby/object:Gem::Dependency
48
48
  name: bundler
49
- requirement: &70103116671500 !ruby/object:Gem::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: *70103116671500
57
+ version_requirements: *70298610807100
58
58
  - !ruby/object:Gem::Dependency
59
59
  name: jeweler
60
- requirement: &70103116670920 !ruby/object:Gem::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: *70103116670920
68
+ version_requirements: *70298610806620
69
69
  - !ruby/object:Gem::Dependency
70
70
  name: mocha
71
- requirement: &70103116670380 !ruby/object:Gem::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: *70103116670380
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: -225078698717891469
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