alicorn 0.0.4 → 0.0.5
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/VERSION +1 -1
- data/alicorn.gemspec +5 -3
- data/bin/alicorn +23 -19
- data/bin/alicorn_profiler +70 -0
- data/lib/alicorn/dataset.rb +3 -0
- data/lib/alicorn/log_parser.rb +36 -0
- data/lib/alicorn/scaler.rb +87 -70
- data/test/helper.rb +2 -1
- data/test/test_alicorn.rb +3 -1
- metadata +16 -13
data/VERSION
CHANGED
@@ -1 +1 @@
|
|
1
|
-
0.0.
|
1
|
+
0.0.5
|
data/alicorn.gemspec
CHANGED
@@ -5,14 +5,14 @@
|
|
5
5
|
|
6
6
|
Gem::Specification.new do |s|
|
7
7
|
s.name = "alicorn"
|
8
|
-
s.version = "0.0.
|
8
|
+
s.version = "0.0.5"
|
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-15"
|
13
13
|
s.description = "Highly configurable dumb auto-scaler for managing unicorn web servers"
|
14
14
|
s.email = "somers.ben@gmail.com"
|
15
|
-
s.executables = ["alicorn"]
|
15
|
+
s.executables = ["alicorn", "alicorn_profiler"]
|
16
16
|
s.extra_rdoc_files = [
|
17
17
|
"LICENSE.txt",
|
18
18
|
"README.rdoc"
|
@@ -27,8 +27,10 @@ Gem::Specification.new do |s|
|
|
27
27
|
"VERSION",
|
28
28
|
"alicorn.gemspec",
|
29
29
|
"bin/alicorn",
|
30
|
+
"bin/alicorn_profiler",
|
30
31
|
"lib/alicorn.rb",
|
31
32
|
"lib/alicorn/dataset.rb",
|
33
|
+
"lib/alicorn/log_parser.rb",
|
32
34
|
"lib/alicorn/scaler.rb",
|
33
35
|
"test/helper.rb",
|
34
36
|
"test/test_alicorn.rb"
|
data/bin/alicorn
CHANGED
@@ -6,48 +6,52 @@ require 'alicorn'
|
|
6
6
|
options = {}
|
7
7
|
OptionParser.new do |opts|
|
8
8
|
opts.banner = "Alicorn is a tool for auto-scaling unicorn worker counts.
|
9
|
-
|
10
|
-
|
11
|
-
|
9
|
+
It relies on unicorn's built-in Raindrops middleware (so make sure you have
|
10
|
+
enabled it). It computes rough load scores based on separately averaged
|
11
|
+
data sets. \n\nUsage: alicorn [options]"
|
12
12
|
opts.on( '-h', '--help', 'Display this screen' ) do
|
13
13
|
puts opts
|
14
14
|
exit
|
15
15
|
end
|
16
16
|
|
17
|
-
opts.on("--min-workers N", Integer, "the minimum number of workers to scale down to") do |v|
|
17
|
+
opts.on("--min-workers [N]", Integer, "the minimum number of workers to scale down to") do |v|
|
18
18
|
options[:min_workers] = v
|
19
19
|
end
|
20
20
|
|
21
|
-
opts.on("--max-workers N", Integer, "the maximum number of workers to scale up to") do |v|
|
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
|
|
25
|
-
opts.on("--
|
26
|
-
options[:
|
25
|
+
opts.on("--buffer [N]", Integer, "the number of extra workers to keep for safety's sake") do |v|
|
26
|
+
options[:buffer] = v
|
27
27
|
end
|
28
28
|
|
29
|
-
opts.on("--
|
30
|
-
options[:
|
29
|
+
opts.on("--target-ratio [F]", Float, "the desired ratio of workers to busy workers") do |v|
|
30
|
+
options[:target_ratio] = v
|
31
31
|
end
|
32
32
|
|
33
|
-
opts.on("--
|
34
|
-
options[:
|
33
|
+
opts.on("--url [URL]", String, "raindrops URL to check, defaults to 127.0.0.1/_raindrops") do |v|
|
34
|
+
options[:url] = v
|
35
35
|
end
|
36
36
|
|
37
|
-
opts.on("--
|
38
|
-
options[:
|
37
|
+
opts.on("--sample-count [N]", Integer, "number of data points to check before scaling") do |v|
|
38
|
+
options[:sample_count] = v
|
39
39
|
end
|
40
40
|
|
41
|
-
opts.on("--
|
42
|
-
options[:
|
41
|
+
opts.on("--delay [F]", Float, "delay between checks") do |v|
|
42
|
+
options[:delay] = v
|
43
43
|
end
|
44
44
|
|
45
|
-
opts.on("--
|
46
|
-
options[:
|
45
|
+
opts.on("--log-path [PATH]", String, "path to write log file - leave blank for no logging") do |v|
|
46
|
+
options[:log_path] = v
|
47
47
|
end
|
48
48
|
|
49
|
-
opts.on("
|
50
|
-
options[:
|
49
|
+
opts.on("-v", "--[no-]verbose", "turn on to write profiling info") do
|
50
|
+
options[:verbose] = true
|
51
|
+
end
|
52
|
+
|
53
|
+
opts.on("-d", "--[no-]dry-run", "turn on to disable actual scaling") do
|
54
|
+
options[:dry_run] = true
|
51
55
|
end
|
52
56
|
|
53
57
|
end.parse!
|
@@ -0,0 +1,70 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
$:.push File.join(File.dirname(__FILE__),'..','lib')
|
3
|
+
|
4
|
+
require 'optparse'
|
5
|
+
require 'alicorn'
|
6
|
+
require 'alicorn/log_parser'
|
7
|
+
|
8
|
+
|
9
|
+
options = {}
|
10
|
+
OptionParser.new do |opts|
|
11
|
+
opts.banner = "Alicorn Profiler is a profiling tool to use for determining
|
12
|
+
optimal settings to run alicorn at. First, run the scaler in dry-run mode
|
13
|
+
to collect statistics about your application load. Then pull that log file
|
14
|
+
and feed it into this profiler. Experiment with the options until you find
|
15
|
+
settings that work well for you. Be aware that occasional 'Overloaded!'
|
16
|
+
situations are not necessarily bad; they just result in requests getting
|
17
|
+
queued. \n\nUsage: alicorn [options]"
|
18
|
+
opts.on( '-h', '--help', 'Display this screen' ) do
|
19
|
+
puts opts
|
20
|
+
exit
|
21
|
+
end
|
22
|
+
|
23
|
+
opts.on("--min-workers N", Integer, "the minimum number of workers to scale down to") do |v|
|
24
|
+
options[:min_workers] = v
|
25
|
+
end
|
26
|
+
|
27
|
+
opts.on("--max-workers N", Integer, "the maximum number of workers to scale up to") do |v|
|
28
|
+
options[:max_workers] = v
|
29
|
+
end
|
30
|
+
|
31
|
+
opts.on("--buffer N", Integer, "the number of extra workers to keep for safety's sake") do |v|
|
32
|
+
options[:buffer] = v
|
33
|
+
end
|
34
|
+
|
35
|
+
opts.on("--target-ratio F", Float, "the desired ratio of workers to busy workers") do |v|
|
36
|
+
options[:target_ratio] = v
|
37
|
+
end
|
38
|
+
|
39
|
+
opts.on("--log-path PATH", String, "location of the alicorn log file to read") do |v|
|
40
|
+
options[:log_path] = v
|
41
|
+
end
|
42
|
+
end.parse!
|
43
|
+
|
44
|
+
# Use these two variables to control the profiler
|
45
|
+
# The log file is the output of the sampling-mode alicorn
|
46
|
+
# The options are a stripped-down option set, including only
|
47
|
+
# the options that matter to the scaling algorithm.
|
48
|
+
@log_file = options[:log_path]
|
49
|
+
@options = { :min_workers => options[:min_workers],
|
50
|
+
:max_workers => options[:max_workers],
|
51
|
+
:target_ratio => options[:target_ration],
|
52
|
+
:buffer => options[:buffer],
|
53
|
+
:debug => true,
|
54
|
+
:dry_run => true}
|
55
|
+
|
56
|
+
@alp = Alicorn::LogParser.new(@log_file)
|
57
|
+
@alp.parse
|
58
|
+
|
59
|
+
scaler = Alicorn::Scaler.new(@options)
|
60
|
+
@worker_count = @options[:max_workers]
|
61
|
+
|
62
|
+
p "Testing #{@alp.samples.count} samples"
|
63
|
+
@alp.samples.each do |sample|
|
64
|
+
if sample[:active].max > @worker_count
|
65
|
+
p "Overloaded! Ran #{@worker_count} and got #{sample[:active].max} active"
|
66
|
+
end
|
67
|
+
sig = scaler.send(:auto_scale, sample, @worker_count)
|
68
|
+
@worker_count += 1 if sig == "TTIN"
|
69
|
+
@worker_count -= 1 if sig == "TTOU"
|
70
|
+
end
|
data/lib/alicorn/dataset.rb
CHANGED
@@ -1,14 +1,17 @@
|
|
1
1
|
class DataSet < Array
|
2
2
|
def avg
|
3
|
+
return nil if empty?
|
3
4
|
inject(:+) / size.to_i
|
4
5
|
end
|
5
6
|
|
6
7
|
def variance
|
8
|
+
return nil if empty?
|
7
9
|
sum=self.inject(0){|acc,i|acc +(i-avg)**2}
|
8
10
|
return(1/self.length.to_f*sum)
|
9
11
|
end
|
10
12
|
|
11
13
|
def stddev
|
14
|
+
return nil if empty?
|
12
15
|
Math.sqrt(self.variance)
|
13
16
|
end
|
14
17
|
end
|
@@ -0,0 +1,36 @@
|
|
1
|
+
module Alicorn
|
2
|
+
class LogParser
|
3
|
+
attr_accessor :filename, :samples, :calling, :active, :queued, :calling_avg, :active_avg, :queued_avg
|
4
|
+
|
5
|
+
def initialize(file = "alicorn.log")
|
6
|
+
self.filename = file
|
7
|
+
self.samples, self.calling, self.active, self.queued, self.calling_avg, self.active_avg, self.queued_avg = [], [], [], [], [], [], []
|
8
|
+
end
|
9
|
+
|
10
|
+
def parse
|
11
|
+
f = File.open(filename)
|
12
|
+
f.each do |line|
|
13
|
+
if line.match(/^"Sampling/)
|
14
|
+
@sample_hash = {} # this will reset every sample
|
15
|
+
elsif line.match(/^"calling:\[(.+)\]"/)
|
16
|
+
data = $1.split(", ").map(&:to_i)
|
17
|
+
calling << data
|
18
|
+
@sample_hash[:calling] = data
|
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:\[(.+)\]"/)
|
24
|
+
data = $1.split(", ").map(&:to_i)
|
25
|
+
active << data
|
26
|
+
@sample_hash[:active] = data
|
27
|
+
elsif line.match(/^"active avg:([\d]+)"/)
|
28
|
+
data = $1.to_i
|
29
|
+
active_avg << data
|
30
|
+
@sample_hash[:active_avg] = data
|
31
|
+
samples << @sample_hash
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
data/lib/alicorn/scaler.rb
CHANGED
@@ -1,86 +1,103 @@
|
|
1
1
|
require 'curl'
|
2
|
+
require 'logger'
|
2
3
|
require 'alicorn/dataset'
|
3
4
|
|
4
|
-
|
5
|
-
|
6
|
-
:
|
7
|
-
|
8
|
-
|
5
|
+
module Alicorn
|
6
|
+
class Scaler
|
7
|
+
attr_accessor :min_workers, :max_workers, :target_ratio, :buffer,
|
8
|
+
:raindrops_url, :delay, :sample_count, :app_name, :dry_run,
|
9
|
+
:master_pid, :worker_count, :logger
|
9
10
|
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
self.master_pid = find_master_pid(app_name)
|
22
|
-
self.worker_count = find_worker_count(app_name)
|
23
|
-
self.calling = DataSet.new
|
24
|
-
self.writing = DataSet.new
|
25
|
-
self.active = DataSet.new
|
26
|
-
self.queued = DataSet.new
|
27
|
-
end
|
11
|
+
def initialize(options)
|
12
|
+
self.min_workers = options[:min_workers] || 1
|
13
|
+
self.max_workers = options[:max_workers]
|
14
|
+
self.target_ratio = options[:target_ratio] || 1.3
|
15
|
+
self.buffer = options[:buffer] || 0
|
16
|
+
self.raindrops_url = options[:url] || "http://127.0.0.1/_raindrops"
|
17
|
+
self.delay = options[:delay] || 1
|
18
|
+
self.sample_count = options[:sample_count] || 30
|
19
|
+
self.app_name = options[:app_name] || "unicorn"
|
20
|
+
self.dry_run = options[:dry_run]
|
21
|
+
log_path = options[:log_path] || "/dev/null"
|
28
22
|
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
end
|
23
|
+
self.logger = Logger.new(log_path)
|
24
|
+
logger.level = options[:verbose] ? Logger::DEBUG : Logger::WARN
|
25
|
+
end
|
33
26
|
|
34
|
-
|
27
|
+
def scale!
|
28
|
+
master_pid = find_master_pid
|
29
|
+
worker_count = find_worker_count
|
30
|
+
data = collect_data
|
35
31
|
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
# return false if calling.all? { |sample| sample == 0 }
|
40
|
-
end
|
32
|
+
sig = auto_scale(data, worker_count)
|
33
|
+
send_signal(sig) if sig
|
34
|
+
end
|
41
35
|
|
42
|
-
|
43
|
-
# return false if worker_count <= min_workers
|
44
|
-
end
|
36
|
+
protected
|
45
37
|
|
46
|
-
|
38
|
+
def auto_scale(data, worker_count)
|
39
|
+
# Calculate target
|
40
|
+
target = data[:active].max * target_ratio + buffer
|
47
41
|
|
48
|
-
|
49
|
-
|
42
|
+
# Check hard thresholds
|
43
|
+
target = max_workers if max_workers and target > max_workers
|
44
|
+
target = min_workers if target < min_workers
|
45
|
+
target = target.ceil
|
50
46
|
|
51
|
-
|
52
|
-
|
47
|
+
logger.debug "target calculated at: #{target}, worker count at #{worker_count}"
|
48
|
+
if target > worker_count
|
49
|
+
logger.debug "scaling up!" unless dry_run
|
50
|
+
return "TTIN"
|
51
|
+
elsif target < worker_count
|
52
|
+
logger.debug "scaling down!" unless dry_run
|
53
|
+
return "TTOU"
|
54
|
+
end
|
55
|
+
end
|
53
56
|
|
54
|
-
|
55
|
-
p "Sampling #{sample_count} times"
|
56
|
-
sample_count.times do
|
57
|
-
raindrops = get_raindrops(raindrops_url)
|
58
|
-
calling << $1.to_i if raindrops.detect { |line| line.match(/calling: ([0-9]+)/) }
|
59
|
-
writing << $1.to_i if raindrops.detect { |line| line.match(/writing: ([0-9]+)/) }
|
60
|
-
active << $1.to_i if raindrops.detect { |line| line.match(/active: ([0-9]+)/) }
|
61
|
-
queued << $1.to_i if raindrops.detect { |line| line.match(/queued: ([0-9]+)/) }
|
62
|
-
sleep(delay)
|
57
|
+
def find_master_pid
|
63
58
|
end
|
64
|
-
p "Collected:"
|
65
|
-
p "calling:#{calling}"
|
66
|
-
p "calling avg:#{calling.avg}"
|
67
|
-
p "calling stddev:#{calling.stddev}"
|
68
|
-
p "writing:#{writing}"
|
69
|
-
p "writing avg:#{writing.avg}"
|
70
|
-
p "writing stddev:#{writing.stddev}"
|
71
|
-
p "active:#{active}"
|
72
|
-
p "active avg:#{active.avg}"
|
73
|
-
p "active stddev:#{active.stddev}"
|
74
|
-
p "queued:#{queued}"
|
75
|
-
p "queued avg:#{queued.avg}"
|
76
|
-
p "queued stddev:#{queued.stddev}"
|
77
|
-
end
|
78
59
|
|
79
|
-
|
80
|
-
|
81
|
-
|
82
|
-
|
83
|
-
|
84
|
-
|
60
|
+
def find_worker_count
|
61
|
+
14
|
62
|
+
end
|
63
|
+
|
64
|
+
private
|
65
|
+
|
66
|
+
def collect_data
|
67
|
+
logger.debug "Sampling #{sample_count} times at #{Time.now}"
|
68
|
+
calling, writing, active, queued = DataSet.new, DataSet.new, DataSet.new, DataSet.new
|
69
|
+
sample_count.times do
|
70
|
+
raindrops = get_raindrops(raindrops_url)
|
71
|
+
calling << $1.to_i if raindrops.detect { |line| line.match(/calling: ([0-9]+)/) }
|
72
|
+
writing << $1.to_i if raindrops.detect { |line| line.match(/writing: ([0-9]+)/) }
|
73
|
+
active << $1.to_i if raindrops.detect { |line| line.match(/active: ([0-9]+)/) }
|
74
|
+
queued << $1.to_i if raindrops.detect { |line| line.match(/queued: ([0-9]+)/) }
|
75
|
+
sleep(delay)
|
76
|
+
end
|
77
|
+
logger.debug "Collected:"
|
78
|
+
logger.debug "calling:#{calling}"
|
79
|
+
logger.debug "calling avg:#{calling.avg}"
|
80
|
+
logger.debug "calling stddev:#{calling.stddev}"
|
81
|
+
logger.debug "writing:#{writing}"
|
82
|
+
logger.debug "writing avg:#{writing.avg}"
|
83
|
+
logger.debug "writing stddev:#{writing.stddev}"
|
84
|
+
logger.debug "active:#{active}"
|
85
|
+
logger.debug "active avg:#{active.avg}"
|
86
|
+
logger.debug "active stddev:#{active.stddev}"
|
87
|
+
logger.debug "queued:#{queued}"
|
88
|
+
logger.debug "queued avg:#{queued.avg}"
|
89
|
+
logger.debug "queued stddev:#{queued.stddev}"
|
90
|
+
{:calling => calling, :writing => writing, :active => active, :queued => queued}
|
91
|
+
end
|
92
|
+
|
93
|
+
def get_raindrops(url)
|
94
|
+
Curl::Easy.http_get(url).body_str.split("\n")
|
95
|
+
end
|
96
|
+
|
97
|
+
def send_signal(sig)
|
98
|
+
return false if dry_run
|
99
|
+
return sig
|
100
|
+
# Process.kill(sig, master_pid)
|
101
|
+
end
|
85
102
|
end
|
86
103
|
end
|
data/test/helper.rb
CHANGED
data/test/test_alicorn.rb
CHANGED
@@ -1,7 +1,9 @@
|
|
1
1
|
require 'helper'
|
2
2
|
|
3
|
-
class
|
3
|
+
class TestScaler < Test::Unit::TestCase
|
4
|
+
|
4
5
|
should "probably rename this file and start testing for real" do
|
5
6
|
flunk "hey buddy, you should probably rename this file and start testing for real"
|
6
7
|
end
|
8
|
+
|
7
9
|
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.5
|
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-15 00:00:00.000000000 Z
|
13
13
|
dependencies:
|
14
14
|
- !ruby/object:Gem::Dependency
|
15
15
|
name: curb
|
16
|
-
requirement: &
|
16
|
+
requirement: &70095244082940 !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: *70095244082940
|
25
25
|
- !ruby/object:Gem::Dependency
|
26
26
|
name: shoulda
|
27
|
-
requirement: &
|
27
|
+
requirement: &70095244082440 !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: *70095244082440
|
36
36
|
- !ruby/object:Gem::Dependency
|
37
37
|
name: rdoc
|
38
|
-
requirement: &
|
38
|
+
requirement: &70095244081720 !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: *70095244081720
|
47
47
|
- !ruby/object:Gem::Dependency
|
48
48
|
name: bundler
|
49
|
-
requirement: &
|
49
|
+
requirement: &70095244081000 !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: *70095244081000
|
58
58
|
- !ruby/object:Gem::Dependency
|
59
59
|
name: jeweler
|
60
|
-
requirement: &
|
60
|
+
requirement: &70095244080240 !ruby/object:Gem::Requirement
|
61
61
|
none: false
|
62
62
|
requirements:
|
63
63
|
- - ~>
|
@@ -65,11 +65,12 @@ dependencies:
|
|
65
65
|
version: 1.8.3
|
66
66
|
type: :development
|
67
67
|
prerelease: false
|
68
|
-
version_requirements: *
|
68
|
+
version_requirements: *70095244080240
|
69
69
|
description: Highly configurable dumb auto-scaler for managing unicorn web servers
|
70
70
|
email: somers.ben@gmail.com
|
71
71
|
executables:
|
72
72
|
- alicorn
|
73
|
+
- alicorn_profiler
|
73
74
|
extensions: []
|
74
75
|
extra_rdoc_files:
|
75
76
|
- LICENSE.txt
|
@@ -84,8 +85,10 @@ files:
|
|
84
85
|
- VERSION
|
85
86
|
- alicorn.gemspec
|
86
87
|
- bin/alicorn
|
88
|
+
- bin/alicorn_profiler
|
87
89
|
- lib/alicorn.rb
|
88
90
|
- lib/alicorn/dataset.rb
|
91
|
+
- lib/alicorn/log_parser.rb
|
89
92
|
- lib/alicorn/scaler.rb
|
90
93
|
- test/helper.rb
|
91
94
|
- test/test_alicorn.rb
|
@@ -104,7 +107,7 @@ required_ruby_version: !ruby/object:Gem::Requirement
|
|
104
107
|
version: '0'
|
105
108
|
segments:
|
106
109
|
- 0
|
107
|
-
hash:
|
110
|
+
hash: -2775814352740060929
|
108
111
|
required_rubygems_version: !ruby/object:Gem::Requirement
|
109
112
|
none: false
|
110
113
|
requirements:
|