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 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