alicorn 0.0.6 → 0.0.7
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 +4 -0
- data/VERSION +1 -1
- data/alicorn.gemspec +5 -2
- data/bin/alicorn +3 -1
- data/bin/alicorn_profiler +14 -4
- data/lib/alicorn/dataset.rb +1 -1
- data/lib/alicorn/log_parser.rb +14 -18
- data/lib/alicorn/scaler.rb +52 -30
- data/test/helper.rb +1 -1
- data/test/test_alicorn.rb +80 -2
- metadata +24 -13
data/Gemfile
CHANGED
data/Gemfile.lock
CHANGED
@@ -9,6 +9,9 @@ GEM
|
|
9
9
|
rake
|
10
10
|
rdoc
|
11
11
|
json (1.7.2)
|
12
|
+
metaclass (0.0.1)
|
13
|
+
mocha (0.11.4)
|
14
|
+
metaclass (~> 0.0.1)
|
12
15
|
rake (0.9.2.2)
|
13
16
|
rdoc (3.12)
|
14
17
|
json (~> 1.4)
|
@@ -25,5 +28,6 @@ DEPENDENCIES
|
|
25
28
|
bundler (~> 1.1.3)
|
26
29
|
curb (~> 0.8.0)
|
27
30
|
jeweler (~> 1.8.3)
|
31
|
+
mocha (~> 0.11.4)
|
28
32
|
rdoc (~> 3.12)
|
29
33
|
shoulda (~> 3.0.1)
|
data/VERSION
CHANGED
@@ -1 +1 @@
|
|
1
|
-
0.0.
|
1
|
+
0.0.7
|
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.0.7"
|
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-21"
|
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"]
|
@@ -50,12 +50,14 @@ Gem::Specification.new do |s|
|
|
50
50
|
s.add_development_dependency(%q<rdoc>, ["~> 3.12"])
|
51
51
|
s.add_development_dependency(%q<bundler>, ["~> 1.1.3"])
|
52
52
|
s.add_development_dependency(%q<jeweler>, ["~> 1.8.3"])
|
53
|
+
s.add_development_dependency(%q<mocha>, ["~> 0.11.4"])
|
53
54
|
else
|
54
55
|
s.add_dependency(%q<curb>, ["~> 0.8.0"])
|
55
56
|
s.add_dependency(%q<shoulda>, ["~> 3.0.1"])
|
56
57
|
s.add_dependency(%q<rdoc>, ["~> 3.12"])
|
57
58
|
s.add_dependency(%q<bundler>, ["~> 1.1.3"])
|
58
59
|
s.add_dependency(%q<jeweler>, ["~> 1.8.3"])
|
60
|
+
s.add_dependency(%q<mocha>, ["~> 0.11.4"])
|
59
61
|
end
|
60
62
|
else
|
61
63
|
s.add_dependency(%q<curb>, ["~> 0.8.0"])
|
@@ -63,6 +65,7 @@ Gem::Specification.new do |s|
|
|
63
65
|
s.add_dependency(%q<rdoc>, ["~> 3.12"])
|
64
66
|
s.add_dependency(%q<bundler>, ["~> 1.1.3"])
|
65
67
|
s.add_dependency(%q<jeweler>, ["~> 1.8.3"])
|
68
|
+
s.add_dependency(%q<mocha>, ["~> 0.11.4"])
|
66
69
|
end
|
67
70
|
end
|
68
71
|
|
data/bin/alicorn
CHANGED
@@ -18,7 +18,7 @@ data sets. \n\nUsage: alicorn [options]"
|
|
18
18
|
options[:min_workers] = v
|
19
19
|
end
|
20
20
|
|
21
|
-
opts.on("--max-workers
|
21
|
+
opts.on("--max-workers N", Integer, "the maximum number of workers to scale up to") do |v|
|
22
22
|
options[:max_workers] = v
|
23
23
|
end
|
24
24
|
|
@@ -55,4 +55,6 @@ data sets. \n\nUsage: alicorn [options]"
|
|
55
55
|
end
|
56
56
|
|
57
57
|
end.parse!
|
58
|
+
raise OptionParser::MissingArgument.new("--max-workers is a mandatory argument") unless options[:max_workers]
|
59
|
+
|
58
60
|
Alicorn::Scaler.new(options).scale!
|
data/bin/alicorn_profiler
CHANGED
@@ -14,7 +14,12 @@ to collect statistics about your application load. Then pull that log file
|
|
14
14
|
and feed it into this profiler. Experiment with the options until you find
|
15
15
|
settings that work well for you. Be aware that occasional 'Overloaded!'
|
16
16
|
situations are not necessarily bad; they just result in requests getting
|
17
|
-
queued.
|
17
|
+
queued. Note that this profiler is inaccurate in two situations. First, it
|
18
|
+
cannot reliably detect a restart (which will reset the worker count, typically
|
19
|
+
to max). Second, if the production system is currently running with alicorn in
|
20
|
+
dry-run mode, it will not be producing accurate queued counts, which alicorn
|
21
|
+
uses to trigger panic responses (which produce fast scale-ups).
|
22
|
+
\n\nUsage: alicorn [options]"
|
18
23
|
opts.on( '-h', '--help', 'Display this screen' ) do
|
19
24
|
puts opts
|
20
25
|
exit
|
@@ -36,6 +41,10 @@ queued. \n\nUsage: alicorn [options]"
|
|
36
41
|
options[:target_ratio] = v
|
37
42
|
end
|
38
43
|
|
44
|
+
opts.on("-v", "--[no-]verbose", "turn on to write full sample data on overloads") do
|
45
|
+
options[:verbose] = true
|
46
|
+
end
|
47
|
+
|
39
48
|
opts.on("--log-path PATH", String, "location of the alicorn log file to read") do |v|
|
40
49
|
options[:log_path] = v
|
41
50
|
end
|
@@ -63,8 +72,9 @@ p "Testing #{@alp.samples.count} samples"
|
|
63
72
|
@alp.samples.each do |sample|
|
64
73
|
if sample[:active].max > @worker_count
|
65
74
|
p "Overloaded! Ran #{@worker_count} and got #{sample[:active].max} active"
|
75
|
+
p sample if options[:verbose]
|
66
76
|
end
|
67
|
-
sig = scaler.send(:auto_scale, sample, @worker_count)
|
68
|
-
@worker_count +=
|
69
|
-
@worker_count -=
|
77
|
+
sig, count = scaler.send(:auto_scale, sample, @worker_count)
|
78
|
+
@worker_count += count if sig == "TTIN"
|
79
|
+
@worker_count -= count if sig == "TTOU"
|
70
80
|
end
|
data/lib/alicorn/dataset.rb
CHANGED
data/lib/alicorn/log_parser.rb
CHANGED
@@ -1,34 +1,30 @@
|
|
1
1
|
module Alicorn
|
2
2
|
class LogParser
|
3
|
-
attr_accessor :filename, :samples
|
3
|
+
attr_accessor :filename, :samples
|
4
4
|
|
5
5
|
def initialize(file = "alicorn.log")
|
6
6
|
self.filename = file
|
7
|
-
self.samples
|
7
|
+
self.samples = []
|
8
8
|
end
|
9
9
|
|
10
10
|
def parse
|
11
11
|
f = File.open(filename)
|
12
12
|
f.each do |line|
|
13
|
-
|
13
|
+
if line.match(/Sampling/)
|
14
14
|
@sample_hash = {} # this will reset every sample
|
15
|
-
elsif line.match(/
|
15
|
+
elsif line.match(/calling:\[(.+)\]/)
|
16
16
|
data = $1.split(", ").map(&:to_i)
|
17
|
-
calling << data
|
18
|
-
|
19
|
-
elsif line.match(/"calling avg:([\d]+)"/)
|
20
|
-
data = $1.to_i
|
21
|
-
calling_avg << data
|
22
|
-
@sample_hash[:calling_avg] = data
|
23
|
-
elsif line.match(/"active:\[(.+)\]"/)
|
17
|
+
@sample_hash[:calling] = (DataSet.new << data).flatten
|
18
|
+
elsif line.match(/writing:\[(.+)\]/)
|
24
19
|
data = $1.split(", ").map(&:to_i)
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
20
|
+
@sample_hash[:writing] = (DataSet.new << data).flatten
|
21
|
+
elsif line.match(/active:\[(.+)\]/)
|
22
|
+
data = $1.split(", ").map(&:to_i)
|
23
|
+
@sample_hash[:active] = (DataSet.new << data).flatten
|
24
|
+
elsif line.match(/queued:\[(.+)\]/)
|
25
|
+
data = $1.split(", ").map(&:to_i)
|
26
|
+
@sample_hash[:queued] = (DataSet.new << data).flatten
|
27
|
+
samples << @sample_hash # store the old sample
|
32
28
|
end
|
33
29
|
end
|
34
30
|
end
|
data/lib/alicorn/scaler.rb
CHANGED
@@ -5,14 +5,15 @@ require 'alicorn/dataset'
|
|
5
5
|
module Alicorn
|
6
6
|
class Scaler
|
7
7
|
attr_accessor :min_workers, :max_workers, :target_ratio, :buffer,
|
8
|
-
:raindrops_url, :delay, :sample_count, :app_name, :dry_run,
|
9
|
-
|
8
|
+
:raindrops_url, :delay, :sample_count, :app_name, :dry_run, :logger
|
9
|
+
|
10
|
+
def initialize(options = {})
|
11
|
+
raise ArgumentError.new("You must pass a :max_workers option") unless options[:max_workers]
|
10
12
|
|
11
|
-
def initialize(options)
|
12
13
|
self.min_workers = options[:min_workers] || 1
|
13
14
|
self.max_workers = options[:max_workers]
|
14
15
|
self.target_ratio = options[:target_ratio] || 1.3
|
15
|
-
self.buffer = options[:buffer] ||
|
16
|
+
self.buffer = options[:buffer] || 2
|
16
17
|
self.raindrops_url = options[:url] || "http://127.0.0.1/_raindrops"
|
17
18
|
self.delay = options[:delay] || 1
|
18
19
|
self.sample_count = options[:sample_count] || 30
|
@@ -25,18 +26,25 @@ module Alicorn
|
|
25
26
|
end
|
26
27
|
|
27
28
|
def scale!
|
28
|
-
master_pid = find_master_pid
|
29
|
-
worker_count = find_worker_count
|
30
29
|
data = collect_data
|
30
|
+
unicorns = find_unicorns
|
31
|
+
master_pid = find_master_pid(unicorns)
|
32
|
+
worker_count = find_worker_count(unicorns)
|
31
33
|
|
32
|
-
sig = auto_scale(data, worker_count)
|
33
|
-
|
34
|
+
sig, number = auto_scale(data, worker_count)
|
35
|
+
if sig and !dry_run
|
36
|
+
number.times do
|
37
|
+
send_signal(master_pid, sig)
|
38
|
+
sleep(1) # Make sure unicorn doesn't discard repeated signals
|
39
|
+
end
|
40
|
+
end
|
34
41
|
end
|
35
42
|
|
36
43
|
protected
|
37
44
|
|
38
45
|
def auto_scale(data, worker_count)
|
39
46
|
return nil if data[:active].empty?
|
47
|
+
|
40
48
|
# Calculate target
|
41
49
|
target = data[:active].max * target_ratio + buffer
|
42
50
|
|
@@ -46,23 +54,42 @@ module Alicorn
|
|
46
54
|
target = target.ceil
|
47
55
|
|
48
56
|
logger.debug "target calculated at: #{target}, worker count at #{worker_count}"
|
49
|
-
if
|
50
|
-
logger.debug "scaling up!"
|
51
|
-
return "TTIN"
|
57
|
+
if data[:active].avg > worker_count and data[:queued].avg > 1
|
58
|
+
logger.debug "danger, will robinson! scaling up fast!"
|
59
|
+
return "TTIN", target - worker_count
|
60
|
+
elsif target > worker_count
|
61
|
+
logger.debug "scaling up!"
|
62
|
+
return "TTIN", 1
|
52
63
|
elsif target < worker_count
|
53
|
-
logger.debug "scaling down!"
|
54
|
-
return "TTOU"
|
64
|
+
logger.debug "scaling down!"
|
65
|
+
return "TTOU", 1
|
66
|
+
elsif target == worker_count
|
67
|
+
logger.debug "just right!"
|
68
|
+
return nil, 0
|
69
|
+
end
|
70
|
+
end
|
71
|
+
|
72
|
+
def find_master_pid(unicorns)
|
73
|
+
master_lines = unicorns.select { |line| line.match /master/ }
|
74
|
+
if master_lines.size > 1
|
75
|
+
abort "Too many unicorn master processes detected. You may be restarting, or have an app name collision: #{master_lines}"
|
76
|
+
elsif master_lines.first.match /\(old\)/
|
77
|
+
abort "Old master process detected. You may be restarting: #{master_lines.first}"
|
78
|
+
else
|
79
|
+
master_lines.first.split.first
|
55
80
|
end
|
56
81
|
end
|
57
82
|
|
58
|
-
def
|
83
|
+
def find_worker_count(unicorns)
|
84
|
+
unicorns.select { |line| line.match /worker\[[\d]\]/ }.count
|
59
85
|
end
|
60
86
|
|
61
|
-
def
|
62
|
-
|
87
|
+
def find_unicorns
|
88
|
+
ptable = %x(ps ax).split("\n")
|
89
|
+
unicorns = ptable.select { |line| line.match(/unicorn/) && line.match(/#{Regexp.escape(app_name)}/) }
|
90
|
+
unicorns.map(&:strip)
|
63
91
|
end
|
64
92
|
|
65
|
-
private
|
66
93
|
|
67
94
|
def collect_data
|
68
95
|
logger.debug "Sampling #{sample_count} times"
|
@@ -75,30 +102,25 @@ module Alicorn
|
|
75
102
|
queued << $1.to_i if raindrops.detect { |line| line.match(/queued: ([0-9]+)/) }
|
76
103
|
sleep(delay)
|
77
104
|
end
|
105
|
+
|
78
106
|
logger.debug "Collected:"
|
79
107
|
logger.debug "calling:#{calling}"
|
80
|
-
logger.debug "calling avg:#{calling.avg}"
|
81
|
-
logger.debug "calling stddev:#{calling.stddev}"
|
82
108
|
logger.debug "writing:#{writing}"
|
83
|
-
logger.debug "writing avg:#{writing.avg}"
|
84
|
-
logger.debug "writing stddev:#{writing.stddev}"
|
85
109
|
logger.debug "active:#{active}"
|
86
|
-
logger.debug "active avg:#{active.avg}"
|
87
|
-
logger.debug "active stddev:#{active.stddev}"
|
88
110
|
logger.debug "queued:#{queued}"
|
89
|
-
|
90
|
-
logger.debug "queued stddev:#{queued.stddev}"
|
111
|
+
|
91
112
|
{:calling => calling, :writing => writing, :active => active, :queued => queued}
|
92
113
|
end
|
93
114
|
|
115
|
+
def send_signal(master_pid, sig)
|
116
|
+
Process.kill(sig, master_pid)
|
117
|
+
end
|
118
|
+
|
119
|
+
private
|
120
|
+
|
94
121
|
def get_raindrops(url)
|
95
122
|
Curl::Easy.http_get(url).body_str.split("\n")
|
96
123
|
end
|
97
124
|
|
98
|
-
def send_signal(sig)
|
99
|
-
return false if dry_run
|
100
|
-
return sig
|
101
|
-
# Process.kill(sig, master_pid)
|
102
|
-
end
|
103
125
|
end
|
104
126
|
end
|
data/test/helper.rb
CHANGED
@@ -9,11 +9,11 @@ rescue Bundler::BundlerError => e
|
|
9
9
|
end
|
10
10
|
require 'test/unit'
|
11
11
|
require 'shoulda'
|
12
|
+
require 'mocha'
|
12
13
|
|
13
14
|
$LOAD_PATH.unshift(File.join(File.dirname(__FILE__), '..', 'lib'))
|
14
15
|
$LOAD_PATH.unshift(File.dirname(__FILE__))
|
15
16
|
require 'alicorn'
|
16
|
-
require 'script/al'
|
17
17
|
|
18
18
|
class Test::Unit::TestCase
|
19
19
|
end
|
data/test/test_alicorn.rb
CHANGED
@@ -2,8 +2,86 @@ require 'helper'
|
|
2
2
|
|
3
3
|
class TestScaler < Test::Unit::TestCase
|
4
4
|
|
5
|
-
|
6
|
-
|
5
|
+
def setup
|
6
|
+
@scaler = Alicorn::Scaler.new(:delay => 0)
|
7
|
+
class << @scaler
|
8
|
+
def publicize(method)
|
9
|
+
self.class.class_eval { public method }
|
10
|
+
end
|
11
|
+
end
|
12
|
+
end
|
13
|
+
|
14
|
+
context "#auto_scale" do
|
15
|
+
setup do
|
16
|
+
@scaler.publicize :auto_scale
|
17
|
+
@worker_count = 10
|
18
|
+
@scaler.min_workers = 1
|
19
|
+
@scaler.max_workers = 25
|
20
|
+
@scaler.buffer = 2
|
21
|
+
@scaler.target_ratio = 1.3
|
22
|
+
@data = { :active => DataSet.new,
|
23
|
+
:queued => DataSet.new }
|
24
|
+
@data[:queued] << 0
|
25
|
+
end
|
26
|
+
|
27
|
+
context "when we're above the target" do
|
28
|
+
setup do
|
29
|
+
@data[:active] << 3 << 4 << 5
|
30
|
+
end
|
31
|
+
|
32
|
+
should "return 1 TTOU" do
|
33
|
+
assert_equal ["TTOU", 1], @scaler.auto_scale(@data, @worker_count)
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
context "when we're below the target" do
|
38
|
+
setup do
|
39
|
+
@data[:active] << 2 << 10
|
40
|
+
end
|
41
|
+
|
42
|
+
should "return 1 TTIN" do
|
43
|
+
assert_equal ["TTIN", 1], @scaler.auto_scale(@data, @worker_count)
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
47
|
+
context "when we need to scale up fast" do
|
48
|
+
setup do
|
49
|
+
@data[:queued] << 12
|
50
|
+
@data[:active] << 12
|
51
|
+
end
|
52
|
+
|
53
|
+
should "return several TTIN" do
|
54
|
+
assert_equal ["TTIN", 8], @scaler.auto_scale(@data, @worker_count)
|
55
|
+
end
|
56
|
+
end
|
57
|
+
|
58
|
+
context "when we don't need to scale at all" do
|
59
|
+
setup do
|
60
|
+
@data[:active] << 6
|
61
|
+
end
|
62
|
+
|
63
|
+
should "return several TTIN" do
|
64
|
+
assert_equal [nil, 0], @scaler.auto_scale(@data, @worker_count)
|
65
|
+
end
|
66
|
+
end
|
67
|
+
|
68
|
+
end
|
69
|
+
|
70
|
+
context "#collect_data" do
|
71
|
+
setup do
|
72
|
+
@scaler.publicize :collect_data
|
73
|
+
raindrops = "calling: 3\nwriting: 1\n/tmp/cart.socket active: 4\n/tmp/cart.socket queued: 0\n"
|
74
|
+
Curl::Easy.stubs(:http_get).returns(stub(:body_str => raindrops))
|
75
|
+
end
|
76
|
+
|
77
|
+
should "return the correct data" do
|
78
|
+
data = @scaler.collect_data
|
79
|
+
expected_data = { :calling => [3]*30,
|
80
|
+
:writing => [1]*30,
|
81
|
+
:active => [4]*30,
|
82
|
+
:queued => [0]*30 }
|
83
|
+
assert_equal expected_data, data
|
84
|
+
end
|
7
85
|
end
|
8
86
|
|
9
87
|
end
|
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.0.7
|
5
5
|
prerelease:
|
6
6
|
platform: ruby
|
7
7
|
authors:
|
@@ -9,11 +9,11 @@ authors:
|
|
9
9
|
autorequire:
|
10
10
|
bindir: bin
|
11
11
|
cert_chain: []
|
12
|
-
date: 2012-05-
|
12
|
+
date: 2012-05-21 00:00:00.000000000 Z
|
13
13
|
dependencies:
|
14
14
|
- !ruby/object:Gem::Dependency
|
15
15
|
name: curb
|
16
|
-
requirement: &
|
16
|
+
requirement: &70323769373580 !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: *70323769373580
|
25
25
|
- !ruby/object:Gem::Dependency
|
26
26
|
name: shoulda
|
27
|
-
requirement: &
|
27
|
+
requirement: &70323769373060 !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: *70323769373060
|
36
36
|
- !ruby/object:Gem::Dependency
|
37
37
|
name: rdoc
|
38
|
-
requirement: &
|
38
|
+
requirement: &70323769372560 !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: *70323769372560
|
47
47
|
- !ruby/object:Gem::Dependency
|
48
48
|
name: bundler
|
49
|
-
requirement: &
|
49
|
+
requirement: &70323769371920 !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: *70323769371920
|
58
58
|
- !ruby/object:Gem::Dependency
|
59
59
|
name: jeweler
|
60
|
-
requirement: &
|
60
|
+
requirement: &70323769371300 !ruby/object:Gem::Requirement
|
61
61
|
none: false
|
62
62
|
requirements:
|
63
63
|
- - ~>
|
@@ -65,7 +65,18 @@ dependencies:
|
|
65
65
|
version: 1.8.3
|
66
66
|
type: :development
|
67
67
|
prerelease: false
|
68
|
-
version_requirements: *
|
68
|
+
version_requirements: *70323769371300
|
69
|
+
- !ruby/object:Gem::Dependency
|
70
|
+
name: mocha
|
71
|
+
requirement: &70323769370440 !ruby/object:Gem::Requirement
|
72
|
+
none: false
|
73
|
+
requirements:
|
74
|
+
- - ~>
|
75
|
+
- !ruby/object:Gem::Version
|
76
|
+
version: 0.11.4
|
77
|
+
type: :development
|
78
|
+
prerelease: false
|
79
|
+
version_requirements: *70323769370440
|
69
80
|
description: Highly configurable dumb auto-scaler for managing unicorn web servers
|
70
81
|
email: somers.ben@gmail.com
|
71
82
|
executables:
|
@@ -107,7 +118,7 @@ required_ruby_version: !ruby/object:Gem::Requirement
|
|
107
118
|
version: '0'
|
108
119
|
segments:
|
109
120
|
- 0
|
110
|
-
hash:
|
121
|
+
hash: 136029821414189245
|
111
122
|
required_rubygems_version: !ruby/object:Gem::Requirement
|
112
123
|
none: false
|
113
124
|
requirements:
|