dynamo-autoscale 0.3.6 → 0.4.1
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/.gitignore +1 -0
- data/Gemfile +1 -0
- data/Gemfile.lock +13 -8
- data/README.md +5 -1
- data/bin/dynamo-autoscale +30 -19
- data/config/environment/common.rb +73 -18
- data/config/environment/test.rb +25 -2
- data/config/services/aws.rb +18 -2
- data/config/services/logger.rb +20 -21
- data/lib/dynamo-autoscale/actioner.rb +1 -0
- data/lib/dynamo-autoscale/dynamo_actioner.rb +3 -2
- data/lib/dynamo-autoscale/fake_poller.rb +40 -0
- data/lib/dynamo-autoscale/log_collector.rb +18 -0
- data/lib/dynamo-autoscale/logger.rb +5 -1
- data/lib/dynamo-autoscale/metrics.rb +11 -28
- data/lib/dynamo-autoscale/poller.rb +17 -6
- data/lib/dynamo-autoscale/random_data_generator.rb +51 -0
- data/lib/dynamo-autoscale/scale_report.rb +2 -2
- data/lib/dynamo-autoscale/table_tracker.rb +56 -13
- data/lib/dynamo-autoscale/unit_cost.rb +42 -14
- data/lib/dynamo-autoscale/version.rb +1 -1
- data/script/historic_data +1 -1
- data/script/random_test +68 -0
- data/script/test +4 -3
- data/spec/config_spec.rb +72 -0
- data/spec/dispatcher_spec.rb +56 -0
- data/spec/helpers/environment_helper.rb +15 -0
- data/spec/helpers/logger.rb +20 -0
- data/spec/poller_spec.rb +39 -0
- data/spec/spec_helper.rb +0 -3
- data/spec/table_tracker_spec.rb +163 -0
- data/spec/unit_cost_spec.rb +39 -0
- metadata +13 -3
data/.gitignore
CHANGED
data/Gemfile
CHANGED
data/Gemfile.lock
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
PATH
|
2
2
|
remote: .
|
3
3
|
specs:
|
4
|
-
dynamo-autoscale (0.
|
4
|
+
dynamo-autoscale (0.4.1)
|
5
5
|
activesupport
|
6
6
|
aws-sdk
|
7
7
|
colored
|
@@ -12,28 +12,28 @@ PATH
|
|
12
12
|
GEM
|
13
13
|
remote: https://rubygems.org/
|
14
14
|
specs:
|
15
|
-
activesupport (4.0.
|
15
|
+
activesupport (4.0.2)
|
16
16
|
i18n (~> 0.6, >= 0.6.4)
|
17
17
|
minitest (~> 4.2)
|
18
18
|
multi_json (~> 1.3)
|
19
19
|
thread_safe (~> 0.1)
|
20
20
|
tzinfo (~> 0.3.37)
|
21
21
|
atomic (1.1.14)
|
22
|
-
aws-sdk (1.
|
22
|
+
aws-sdk (1.19.0)
|
23
23
|
json (~> 1.4)
|
24
|
-
nokogiri (< 1.6.0)
|
24
|
+
nokogiri (>= 1.4.4, < 1.6.0)
|
25
25
|
uuidtools (~> 2.1)
|
26
26
|
bond (0.4.3)
|
27
27
|
coderay (1.0.9)
|
28
28
|
colored (1.2)
|
29
29
|
diff-lcs (1.2.4)
|
30
|
-
i18n (0.6.
|
31
|
-
json (1.8.
|
30
|
+
i18n (0.6.9)
|
31
|
+
json (1.8.1)
|
32
32
|
mail (2.5.4)
|
33
33
|
mime-types (~> 1.16)
|
34
34
|
treetop (~> 1.4.8)
|
35
35
|
method_source (0.8.1)
|
36
|
-
mime-types (1.
|
36
|
+
mime-types (1.25)
|
37
37
|
minitest (4.7.5)
|
38
38
|
multi_json (1.8.0)
|
39
39
|
nokogiri (1.5.10)
|
@@ -57,6 +57,10 @@ GEM
|
|
57
57
|
diff-lcs (>= 1.1.3, < 2.0)
|
58
58
|
rspec-mocks (2.13.1)
|
59
59
|
ruby-prof (0.13.0)
|
60
|
+
simplecov (0.7.1)
|
61
|
+
multi_json (~> 1.0)
|
62
|
+
simplecov-html (~> 0.7.1)
|
63
|
+
simplecov-html (0.7.1)
|
60
64
|
slop (3.4.5)
|
61
65
|
thread_safe (0.1.3)
|
62
66
|
atomic
|
@@ -64,7 +68,7 @@ GEM
|
|
64
68
|
treetop (1.4.14)
|
65
69
|
polyglot
|
66
70
|
polyglot (>= 0.3.1)
|
67
|
-
tzinfo (0.3.
|
71
|
+
tzinfo (0.3.38)
|
68
72
|
uuidtools (2.1.4)
|
69
73
|
|
70
74
|
PLATFORMS
|
@@ -76,4 +80,5 @@ DEPENDENCIES
|
|
76
80
|
rake
|
77
81
|
ripl
|
78
82
|
rspec
|
83
|
+
simplecov
|
79
84
|
timecop
|
data/README.md
CHANGED
@@ -78,7 +78,11 @@ tables only.
|
|
78
78
|
:aws:
|
79
79
|
:access_key_id: "your_id"
|
80
80
|
:secret_access_key: "your_key"
|
81
|
-
|
81
|
+
|
82
|
+
# Currently, dynamo-autoscale can only operate in one region at a time. If you
|
83
|
+
# want to track tables in multiple regions, you will have to run multiple
|
84
|
+
# instances of dynamo-autoscale.
|
85
|
+
:region: "us-east-1"
|
82
86
|
|
83
87
|
# There are some example rulesets in the rulesets/ directory of this project.
|
84
88
|
:ruleset: "path_to_your_ruleset.rb"
|
data/bin/dynamo-autoscale
CHANGED
@@ -2,6 +2,8 @@
|
|
2
2
|
|
3
3
|
require 'optparse'
|
4
4
|
|
5
|
+
USAGE_PATH = File.join(File.dirname(__FILE__), '..', 'USAGE')
|
6
|
+
|
5
7
|
# This OptionParser block is here to ensure the queries to --version are
|
6
8
|
# actually fast and don't have to go through all of the Ruby requiring lark.
|
7
9
|
begin
|
@@ -13,55 +15,64 @@ begin
|
|
13
15
|
end
|
14
16
|
|
15
17
|
opts.on('--help', 'Shows this documentation.') do
|
16
|
-
puts File.read(
|
18
|
+
puts File.read(USAGE_PATH)
|
17
19
|
exit 0
|
18
20
|
end
|
19
21
|
end.parse!
|
20
22
|
rescue OptionParser::InvalidOption => e
|
21
|
-
STDERR.puts
|
22
|
-
STDERR.puts
|
23
|
-
|
24
|
-
STDERR.puts File.read(File.join(File.dirname(__FILE__), '..', 'USAGE'))
|
23
|
+
STDERR.puts("#{e.message}\n\n#{File.read(USAGE_PATH)}")
|
25
24
|
exit 1
|
26
25
|
end
|
27
26
|
|
28
|
-
|
27
|
+
begin
|
28
|
+
require_relative '../config/environment/common'
|
29
|
+
rescue DynamoAutoscale::Error::InvalidConfigurationError => e
|
30
|
+
STDERR.puts e.message
|
31
|
+
exit 1
|
32
|
+
end
|
29
33
|
|
34
|
+
# Typing out DynamoAutoscale so many times is tedious.
|
35
|
+
DA = DynamoAutoscale
|
30
36
|
|
31
37
|
if ARGV[0]
|
32
|
-
|
38
|
+
begin
|
39
|
+
DA.setup_from_config(ARGV[0])
|
40
|
+
rescue DA::Error::InvalidConfigurationError => e
|
41
|
+
STDERR.puts e.message
|
42
|
+
exit 1
|
43
|
+
end
|
33
44
|
elsif ARGV[0].nil?
|
34
|
-
STDERR.puts File.read(
|
45
|
+
STDERR.puts File.read(USAGE_PATH)
|
35
46
|
|
36
47
|
exit 1
|
37
48
|
elsif ARGV[0] and !File.exists?(ARGV[0])
|
38
49
|
STDERR.puts "Error: The path you specified is to a file that does not exist."
|
39
50
|
STDERR.puts
|
40
|
-
STDERR.puts File.read(
|
51
|
+
STDERR.puts File.read(USAGE_PATH)
|
41
52
|
|
42
53
|
exit 1
|
43
54
|
end
|
44
55
|
|
45
|
-
|
56
|
+
DA.logger.info "Ensuring tables exist in DynamoDB..."
|
46
57
|
dynamo = AWS::DynamoDB.new
|
47
58
|
|
48
|
-
|
59
|
+
DA.poller_opts[:tables].select! do |table_name|
|
49
60
|
if dynamo.tables[table_name].exists?
|
50
61
|
true
|
51
62
|
else
|
52
|
-
|
63
|
+
DA.logger.error "Table #{table_name} does not exist inside your DynamoDB."
|
53
64
|
false
|
54
65
|
end
|
55
66
|
end
|
56
67
|
|
57
|
-
|
68
|
+
DA.poller_class = DA::CWPoller
|
58
69
|
|
59
|
-
unless
|
60
|
-
|
70
|
+
unless DA.config[:dry_run]
|
71
|
+
DA.actioner_class = DA::DynamoActioner
|
61
72
|
end
|
62
73
|
|
63
|
-
|
64
|
-
|
74
|
+
DA.logger.info "Finished setup. Backdating..."
|
75
|
+
DA.poller.backdate
|
65
76
|
|
66
|
-
|
67
|
-
|
77
|
+
DA.logger.info "Starting polling loop..."
|
78
|
+
DA.poller.run
|
@@ -1,4 +1,5 @@
|
|
1
1
|
require 'logger'
|
2
|
+
require 'optparse'
|
2
3
|
require 'fileutils'
|
3
4
|
require 'time'
|
4
5
|
require 'csv'
|
@@ -18,14 +19,32 @@ require_relative '../../lib/dynamo-autoscale/actioner'
|
|
18
19
|
module DynamoAutoscale
|
19
20
|
include DynamoAutoscale::Logger
|
20
21
|
|
21
|
-
|
22
|
+
module Error
|
23
|
+
InvalidConfigurationError = Class.new(StandardError)
|
24
|
+
end
|
22
25
|
|
23
26
|
def self.root
|
24
|
-
File.expand_path(File.join(File.dirname(__FILE__), '..', '..'))
|
27
|
+
@@root ||= File.expand_path(File.join(File.dirname(__FILE__), '..', '..'))
|
28
|
+
end
|
29
|
+
|
30
|
+
def self.root_dir *args
|
31
|
+
File.join(self.root, *args)
|
32
|
+
end
|
33
|
+
|
34
|
+
def self.data_dir *args
|
35
|
+
root_dir 'data', *args
|
36
|
+
end
|
37
|
+
|
38
|
+
def self.config_dir *args
|
39
|
+
root_dir 'config', *args
|
25
40
|
end
|
26
41
|
|
27
|
-
def self.
|
28
|
-
|
42
|
+
def self.rlib_dir *args
|
43
|
+
root_dir 'rlib', *args
|
44
|
+
end
|
45
|
+
|
46
|
+
def self.template_dir *args
|
47
|
+
root_dir 'templates', *args
|
29
48
|
end
|
30
49
|
|
31
50
|
def self.config
|
@@ -36,18 +55,29 @@ module DynamoAutoscale
|
|
36
55
|
@@config = new_config
|
37
56
|
end
|
38
57
|
|
58
|
+
def self.require_in_order *files
|
59
|
+
expand_paths(*files).each { |path| require path }
|
60
|
+
end
|
61
|
+
|
62
|
+
def self.load_in_order *files
|
63
|
+
expand_paths(*files).each { |path| load path }
|
64
|
+
end
|
65
|
+
|
39
66
|
def self.setup_from_config path, overrides = {}
|
40
67
|
logger.debug "[setup] Loading config..."
|
41
68
|
self.config = YAML.load_file(path).merge(overrides)
|
42
69
|
|
43
70
|
if config[:tables].nil? or config[:tables].empty?
|
44
|
-
|
45
|
-
":tables section."
|
46
|
-
|
47
|
-
exit 1
|
71
|
+
raise Error::InvalidConfigurationError.new("You need to specify at " +
|
72
|
+
"least one table in your config's :tables section.")
|
48
73
|
end
|
49
74
|
|
50
|
-
filters = config[:dry_run]
|
75
|
+
filters = if config[:dry_run]
|
76
|
+
DynamoAutoscale::LocalActioner.faux_provisioning_filters
|
77
|
+
else
|
78
|
+
[]
|
79
|
+
end
|
80
|
+
|
51
81
|
if filters.empty?
|
52
82
|
logger.debug "[setup] Not running as a dry run. Hitting production Dynamo."
|
53
83
|
else
|
@@ -89,14 +119,11 @@ module DynamoAutoscale
|
|
89
119
|
DynamoAutoscale.load_services
|
90
120
|
end
|
91
121
|
|
92
|
-
def self.require_all path
|
93
|
-
Dir[File.join(root, path, '*.rb')].each { |file| require file }
|
94
|
-
end
|
95
|
-
|
96
122
|
def self.load_services
|
97
|
-
|
98
|
-
|
99
|
-
|
123
|
+
load_in_order(
|
124
|
+
'config/services/logger.rb',
|
125
|
+
'config/services/*.rb'
|
126
|
+
)
|
100
127
|
end
|
101
128
|
|
102
129
|
def self.dispatcher= new_dispatcher
|
@@ -153,6 +180,10 @@ module DynamoAutoscale
|
|
153
180
|
end
|
154
181
|
end
|
155
182
|
|
183
|
+
def self.actioners= new_actioners
|
184
|
+
@@actioners = new_actioners
|
185
|
+
end
|
186
|
+
|
156
187
|
def self.reset_tables
|
157
188
|
@@tables = Hash.new { |h, k| h[k] = TableTracker.new(k) }
|
158
189
|
end
|
@@ -180,9 +211,33 @@ module DynamoAutoscale
|
|
180
211
|
def self.current_table
|
181
212
|
@@current_table ||= nil
|
182
213
|
end
|
214
|
+
|
215
|
+
private
|
216
|
+
|
217
|
+
# Expands strings given to it as paths relative to the project root
|
218
|
+
def self.expand_paths *files
|
219
|
+
files.inject([]) do |memo, path|
|
220
|
+
full_path = root_dir(path)
|
221
|
+
|
222
|
+
if (paths = Dir.glob(full_path)).length > 0
|
223
|
+
memo += paths.select { |p| File.file?(p) }
|
224
|
+
elsif File.exist?("#{full_path}.rb")
|
225
|
+
memo << "#{full_path}.rb"
|
226
|
+
elsif File.exist?(full_path)
|
227
|
+
memo << full_path
|
228
|
+
else
|
229
|
+
logger.warn "[load] Could not load file #{full_path}"
|
230
|
+
STDERR.puts Kernel.caller
|
231
|
+
exit 1
|
232
|
+
end
|
233
|
+
|
234
|
+
memo
|
235
|
+
end
|
236
|
+
end
|
183
237
|
end
|
184
238
|
|
185
|
-
DynamoAutoscale.
|
186
|
-
|
239
|
+
DynamoAutoscale.require_in_order(
|
240
|
+
'lib/dynamo-autoscale/**.rb',
|
241
|
+
)
|
187
242
|
|
188
243
|
DynamoAutoscale.load_services
|
data/config/environment/test.rb
CHANGED
@@ -1,6 +1,29 @@
|
|
1
1
|
ENV['RACK_ENV'] = "test"
|
2
|
+
|
3
|
+
# SimpleCov seems to get confused when you load a file into your code multiple
|
4
|
+
# times. It will wipe all of its current data about that file when it gets
|
5
|
+
# reloaded, so some of our coverage stats are less than they should be.
|
6
|
+
require 'simplecov'
|
7
|
+
SimpleCov.start
|
8
|
+
|
2
9
|
require_relative 'common'
|
3
10
|
require 'timecop'
|
4
11
|
|
5
|
-
|
6
|
-
DynamoAutoscale.setup_from_config(
|
12
|
+
TEST_CONFIG_PATH = DynamoAutoscale.config_dir('dynamo-autoscale-test.yml')
|
13
|
+
DynamoAutoscale.setup_from_config(TEST_CONFIG_PATH)
|
14
|
+
|
15
|
+
DynamoAutoscale.require_in_order(
|
16
|
+
'spec/helpers/**.rb'
|
17
|
+
)
|
18
|
+
|
19
|
+
RSpec.configure do |config|
|
20
|
+
config.include DynamoAutoscale::Helpers::LoggerHelper
|
21
|
+
config.include DynamoAutoscale::Helpers::EnvironmentHelper
|
22
|
+
|
23
|
+
config.before(:each) do
|
24
|
+
DynamoAutoscale.reset_tables
|
25
|
+
DynamoAutoscale.dispatcher = nil
|
26
|
+
DynamoAutoscale.actioners = nil
|
27
|
+
DynamoAutoscale.poller = nil
|
28
|
+
end
|
29
|
+
end
|
data/config/services/aws.rb
CHANGED
@@ -1,3 +1,19 @@
|
|
1
|
-
if DynamoAutoscale.config[:aws]
|
2
|
-
|
1
|
+
if config = DynamoAutoscale.config[:aws]
|
2
|
+
valid_regions = [
|
3
|
+
"us-east-1", "us-west-1", "us-west-2", "eu-west-1", "ap-southeast-1",
|
4
|
+
"ap-southeast-2", "ap-northeast-1", "sa-east-1",
|
5
|
+
]
|
6
|
+
|
7
|
+
unless config[:region]
|
8
|
+
raise DynamoAutoscale::Error::InvalidConfigurationError.new("You must " +
|
9
|
+
"specify a :region key in the :aws section of your dynamo-autoscale " +
|
10
|
+
"configuration file!")
|
11
|
+
end
|
12
|
+
|
13
|
+
unless valid_regions.include?(config[:region])
|
14
|
+
DynamoAutoscale::Logger.logger.warn "Specified region \"#{config[:region]}\"" +
|
15
|
+
" does not appear in the valid list of regions. Proceed with caution."
|
16
|
+
end
|
17
|
+
|
18
|
+
AWS.config(config)
|
3
19
|
end
|
data/config/services/logger.rb
CHANGED
@@ -1,31 +1,30 @@
|
|
1
|
-
config = DynamoAutoscale.config[:logger]
|
1
|
+
if config = DynamoAutoscale.config[:logger]
|
2
|
+
if config[:sync]
|
3
|
+
STDOUT.sync = true
|
4
|
+
STDERR.sync = true
|
5
|
+
end
|
2
6
|
|
3
|
-
if config[:
|
4
|
-
|
5
|
-
|
6
|
-
end
|
7
|
+
if config[:log_to]
|
8
|
+
STDOUT.reopen(config[:log_to])
|
9
|
+
STDERR.reopen(config[:log_to])
|
10
|
+
end
|
7
11
|
|
8
|
-
|
9
|
-
STDOUT.reopen(config[:log_to])
|
10
|
-
STDERR.reopen(config[:log_to])
|
11
|
-
end
|
12
|
+
DynamoAutoscale::Logger.logger = ::Logger.new(STDOUT)
|
12
13
|
|
13
|
-
|
14
|
+
if config[:style] == "pretty"
|
15
|
+
DynamoAutoscale::Logger.logger.formatter = DynamoAutoscale::PrettyFormatter.new
|
16
|
+
else
|
17
|
+
DynamoAutoscale::Logger.logger.formatter = DynamoAutoscale::StandardFormatter.new
|
18
|
+
end
|
14
19
|
|
15
|
-
if config[:
|
16
|
-
|
17
|
-
|
18
|
-
DynamoAutoscale::Logger.logger.formatter = DynamoAutoscale::StandardFormatter.new
|
20
|
+
if config[:level]
|
21
|
+
DynamoAutoscale::Logger.logger.level = ::Logger.const_get(config[:level])
|
22
|
+
end
|
19
23
|
end
|
20
24
|
|
21
25
|
if ENV['DEBUG']
|
22
26
|
DynamoAutoscale::Logger.logger.level = ::Logger::DEBUG
|
23
|
-
|
24
|
-
|
25
|
-
end
|
26
|
-
|
27
|
-
if ENV['SILENT']
|
27
|
+
AWS.config(logger: DynamoAutoscale::Logger.logger)
|
28
|
+
elsif ENV['SILENT']
|
28
29
|
DynamoAutoscale::Logger.logger.level = ::Logger::FATAL
|
29
30
|
end
|
30
|
-
|
31
|
-
AWS.config(logger: DynamoAutoscale::Logger.logger) if ENV['DEBUG']
|
@@ -86,13 +86,14 @@ module DynamoAutoscale
|
|
86
86
|
aws_description = self.class.describe_table(table)
|
87
87
|
decreases_today = aws_description[:provisioned_throughput][:number_of_decreases_today]
|
88
88
|
|
89
|
-
downscales(
|
89
|
+
downscales(decreases_today)
|
90
90
|
logger.warn "[#{e.class}] #{e.message}"
|
91
91
|
return false
|
92
92
|
end
|
93
93
|
|
94
94
|
def self.describe_table table
|
95
|
-
|
95
|
+
client = AWS::DynamoDB::Client.new(:api_version => '2012-08-10')
|
96
|
+
data = client.describe_table(table_name: table.name)
|
96
97
|
|
97
98
|
data[:table]
|
98
99
|
end
|