dynamo-autoscale 0.3.6 → 0.4.1
Sign up to get free protection for your applications and to get access to all the features.
- 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/script/random_test
ADDED
@@ -0,0 +1,68 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
|
3
|
+
# This script will locally test the tables and options you have specified in
|
4
|
+
# your config passed in as ARGV[0].
|
5
|
+
#
|
6
|
+
# You will first need to have obtained historic data on the tables in your
|
7
|
+
# config file. To do this, run:
|
8
|
+
#
|
9
|
+
# $ script/historic_data path/to/config.yml
|
10
|
+
#
|
11
|
+
# This script does not change any throughputs on DynamoDB whatsoever. The
|
12
|
+
# historic script data will hit CloudWatch fairly hard to get its data, though.
|
13
|
+
|
14
|
+
require_relative '../config/environment/common'
|
15
|
+
require 'timecop'
|
16
|
+
|
17
|
+
OptionParser.new do |opts|
|
18
|
+
opts.on("--graph") do
|
19
|
+
GRAPH = true
|
20
|
+
end
|
21
|
+
end.parse!
|
22
|
+
|
23
|
+
if ARGV[0]
|
24
|
+
DynamoAutoscale.setup_from_config(ARGV[0], dry_run: true)
|
25
|
+
elsif ARGV[0].nil?
|
26
|
+
STDERR.puts "Usage: script/test path/to/config.yml"
|
27
|
+
|
28
|
+
exit 1
|
29
|
+
elsif ARGV[0] and !File.exists?(ARGV[0])
|
30
|
+
STDERR.puts "Usage: script/test path/to/config.yml"
|
31
|
+
STDERR.puts "Error: The path you specified is to a file that does not exist."
|
32
|
+
|
33
|
+
exit 1
|
34
|
+
end
|
35
|
+
|
36
|
+
# Uncomment this and the below RubyProf lines if you want profiling information.
|
37
|
+
# RubyProf.start
|
38
|
+
|
39
|
+
DynamoAutoscale.poller_class = DynamoAutoscale::RandomDataGenerator
|
40
|
+
DynamoAutoscale.poller_opts = {
|
41
|
+
num_points: 100,
|
42
|
+
start_time: Time.now,
|
43
|
+
provisioned_reads: 600,
|
44
|
+
provisioned_writes: 600,
|
45
|
+
}.merge(DynamoAutoscale.poller_opts)
|
46
|
+
|
47
|
+
begin
|
48
|
+
DynamoAutoscale.poller.run { |table_name, time| Timecop.travel(time) }
|
49
|
+
rescue Interrupt
|
50
|
+
Ripl.start binding: binding
|
51
|
+
end
|
52
|
+
|
53
|
+
# Uncomment these and the above RubyProf line if you want profiling information.
|
54
|
+
# printer = RubyProf::FlatPrinter.new(RubyProf.stop)
|
55
|
+
# printer.print(STDOUT, min_percent: 2)
|
56
|
+
|
57
|
+
# Uncomment this if you want to drop into a REPL at the end of the test.
|
58
|
+
# Ripl.start binding: binding
|
59
|
+
|
60
|
+
DynamoAutoscale.tables.each do |_, table|
|
61
|
+
table.report! metric: :cost
|
62
|
+
|
63
|
+
if Kernel.const_defined?(:GRAPH) and GRAPH
|
64
|
+
if path = table.graph!(open: true)
|
65
|
+
puts "Graph saved to #{path}"
|
66
|
+
end
|
67
|
+
end
|
68
|
+
end
|
data/script/test
CHANGED
@@ -52,8 +52,9 @@ end
|
|
52
52
|
DynamoAutoscale.tables.each do |_, table|
|
53
53
|
table.report!
|
54
54
|
|
55
|
-
if GRAPH
|
56
|
-
path = table.graph!
|
57
|
-
|
55
|
+
if Kernel.const_defined?(:GRAPH) and GRAPH
|
56
|
+
if path = table.graph!(open: true)
|
57
|
+
puts "Graph saved to #{path}"
|
58
|
+
end
|
58
59
|
end
|
59
60
|
end
|
data/spec/config_spec.rb
ADDED
@@ -0,0 +1,72 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
describe 'configuration' do
|
4
|
+
it "should crash with no AWS region specified" do
|
5
|
+
expect do
|
6
|
+
DynamoAutoscale.setup_from_config(TEST_CONFIG_PATH, aws: { })
|
7
|
+
end.to raise_error DynamoAutoscale::Error::InvalidConfigurationError
|
8
|
+
end
|
9
|
+
|
10
|
+
it "should warn when using a non standard AWS region" do
|
11
|
+
log = catch_logs do
|
12
|
+
DynamoAutoscale.setup_from_config(TEST_CONFIG_PATH, {
|
13
|
+
logger: nil,
|
14
|
+
aws: { region: "wut" }
|
15
|
+
})
|
16
|
+
end
|
17
|
+
|
18
|
+
bool = log[:warn].any? { |m| m.include?("wut") and m.include?("region") }
|
19
|
+
bool.should be_true
|
20
|
+
end
|
21
|
+
|
22
|
+
it "should set log level to debug when ENV['DEBUG'] is set" do
|
23
|
+
with_env("DEBUG" => "true") do
|
24
|
+
DynamoAutoscale.setup_from_config(TEST_CONFIG_PATH)
|
25
|
+
DynamoAutoscale.logger.level.should == ::Logger::DEBUG
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
it "should set ENV['DEBUG'] log level even when no logger config is present" do
|
30
|
+
with_env("DEBUG" => "true") do
|
31
|
+
DynamoAutoscale.setup_from_config(TEST_CONFIG_PATH, logger: nil)
|
32
|
+
DynamoAutoscale.logger.level.should == ::Logger::DEBUG
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
it "should set log level to fatal when ENV['SILENT'] is set" do
|
37
|
+
with_env("SILENT" => "true") do
|
38
|
+
DynamoAutoscale.setup_from_config(TEST_CONFIG_PATH)
|
39
|
+
DynamoAutoscale.logger.level.should == ::Logger::FATAL
|
40
|
+
end
|
41
|
+
end
|
42
|
+
|
43
|
+
it "should set log level to info by default" do
|
44
|
+
with_env({}) do
|
45
|
+
DynamoAutoscale.setup_from_config(TEST_CONFIG_PATH)
|
46
|
+
DynamoAutoscale.logger.level.should == ::Logger::INFO
|
47
|
+
end
|
48
|
+
end
|
49
|
+
|
50
|
+
it "DEBUG should take precedence over SILENT" do
|
51
|
+
with_env("SILENT" => "true", "DEBUG" => "true") do
|
52
|
+
DynamoAutoscale.setup_from_config(TEST_CONFIG_PATH)
|
53
|
+
DynamoAutoscale.logger.level.should == ::Logger::DEBUG
|
54
|
+
end
|
55
|
+
end
|
56
|
+
|
57
|
+
it "should use a standard formatter when pretty is not specified" do
|
58
|
+
DynamoAutoscale.setup_from_config(TEST_CONFIG_PATH, {
|
59
|
+
logger: {}
|
60
|
+
})
|
61
|
+
|
62
|
+
DynamoAutoscale.logger.formatter.should be_a DynamoAutoscale::StandardFormatter
|
63
|
+
end
|
64
|
+
|
65
|
+
it "should use a pretty formatter when pretty is specified" do
|
66
|
+
DynamoAutoscale.setup_from_config(TEST_CONFIG_PATH, {
|
67
|
+
logger: { style: "pretty" }
|
68
|
+
})
|
69
|
+
|
70
|
+
DynamoAutoscale.logger.formatter.should be_a DynamoAutoscale::PrettyFormatter
|
71
|
+
end
|
72
|
+
end
|
@@ -0,0 +1,56 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
describe DynamoAutoscale::Dispatcher do
|
4
|
+
let(:table_name) { "example_table" }
|
5
|
+
let(:table) { DynamoAutoscale.tables[table_name] }
|
6
|
+
let(:poller_class) { DynamoAutoscale::FakePoller }
|
7
|
+
let(:poller_opts) { { data: poller_data, tables: [table_name] } }
|
8
|
+
|
9
|
+
before do
|
10
|
+
DynamoAutoscale.poller_class = poller_class
|
11
|
+
DynamoAutoscale.poller_opts = poller_opts
|
12
|
+
end
|
13
|
+
|
14
|
+
describe "simple dispatching" do
|
15
|
+
let(:time1) { Time.now }
|
16
|
+
let(:time2) { time1 + 15.minutes }
|
17
|
+
|
18
|
+
let :poller_data do
|
19
|
+
{
|
20
|
+
consumed_reads: {
|
21
|
+
time1 => 10,
|
22
|
+
time2 => 20,
|
23
|
+
}
|
24
|
+
}
|
25
|
+
end
|
26
|
+
|
27
|
+
it "should correctly dispatch data" do
|
28
|
+
DynamoAutoscale.poller.run
|
29
|
+
|
30
|
+
table.data[time1][:consumed_reads].should == 10
|
31
|
+
table.data[time2][:consumed_reads].should == 20
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
describe "dispatching out of order data" do
|
36
|
+
let(:time1) { Time.now }
|
37
|
+
let(:time2) { time1 + 15.minutes }
|
38
|
+
|
39
|
+
let :poller_data do
|
40
|
+
{
|
41
|
+
consumed_reads: {
|
42
|
+
time2 => 20,
|
43
|
+
time1 => 10,
|
44
|
+
}
|
45
|
+
}
|
46
|
+
end
|
47
|
+
|
48
|
+
it "should correctly dispatch data" do
|
49
|
+
DynamoAutoscale.poller.run
|
50
|
+
|
51
|
+
table.data[time1][:consumed_reads].should == 10
|
52
|
+
table.data[time2][:consumed_reads].should == 20
|
53
|
+
table.data.keys.should == [time1, time2]
|
54
|
+
end
|
55
|
+
end
|
56
|
+
end
|
@@ -0,0 +1,15 @@
|
|
1
|
+
module DynamoAutoscale
|
2
|
+
module Helpers
|
3
|
+
module EnvironmentHelper
|
4
|
+
def with_env env, &block
|
5
|
+
old_env = ENV.respond_to?(:to_h) ? ENV.to_h.dup : ENV.to_hash.dup
|
6
|
+
ENV.clear
|
7
|
+
env.each { |h, k| ENV[h] = k }
|
8
|
+
block.call
|
9
|
+
ENV.clear
|
10
|
+
old_env.each { |h, k| ENV[h] = k }
|
11
|
+
nil
|
12
|
+
end
|
13
|
+
end
|
14
|
+
end
|
15
|
+
end
|
@@ -0,0 +1,20 @@
|
|
1
|
+
module DynamoAutoscale
|
2
|
+
module Helpers
|
3
|
+
module LoggerHelper
|
4
|
+
def catch_logs &block
|
5
|
+
old_logger = DynamoAutoscale.logger
|
6
|
+
old_formatter = old_logger.formatter
|
7
|
+
old_level = old_logger.level
|
8
|
+
collector = LogCollector.new(STDOUT)
|
9
|
+
collector.formatter = old_formatter
|
10
|
+
collector.level = old_level
|
11
|
+
DynamoAutoscale.logger = collector
|
12
|
+
|
13
|
+
block.call
|
14
|
+
|
15
|
+
DynamoAutoscale.logger = old_logger
|
16
|
+
collector.messages
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
data/spec/poller_spec.rb
ADDED
@@ -0,0 +1,39 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
describe DynamoAutoscale::Poller do
|
4
|
+
let(:table_name) { "example_table" }
|
5
|
+
let(:table) { DynamoAutoscale.tables[table_name] }
|
6
|
+
let(:poller_class) { DynamoAutoscale::FakePoller }
|
7
|
+
let(:poller_opts) { { data: poller_data, tables: [table_name] } }
|
8
|
+
|
9
|
+
before do
|
10
|
+
DynamoAutoscale.poller_class = poller_class
|
11
|
+
DynamoAutoscale.poller_opts = poller_opts
|
12
|
+
end
|
13
|
+
|
14
|
+
describe "simple dispatching" do
|
15
|
+
let(:time1) { Time.now }
|
16
|
+
let(:time2) { time1 + 15.minutes }
|
17
|
+
|
18
|
+
let :poller_data do
|
19
|
+
{
|
20
|
+
consumed_reads: {
|
21
|
+
time1 => 10,
|
22
|
+
time2 => 20,
|
23
|
+
}
|
24
|
+
}
|
25
|
+
end
|
26
|
+
|
27
|
+
it "should correctly dispatch data" do
|
28
|
+
DynamoAutoscale.dispatcher.should_receive(:dispatch).with(
|
29
|
+
table, time1, { consumed_reads: 10 }
|
30
|
+
).once
|
31
|
+
|
32
|
+
DynamoAutoscale.dispatcher.should_receive(:dispatch).with(
|
33
|
+
table, time2, { consumed_reads: 20 }
|
34
|
+
).once
|
35
|
+
|
36
|
+
DynamoAutoscale.poller.run
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
data/spec/spec_helper.rb
CHANGED
data/spec/table_tracker_spec.rb
CHANGED
@@ -51,6 +51,115 @@ describe DynamoAutoscale::TableTracker do
|
|
51
51
|
it { should == table_name }
|
52
52
|
end
|
53
53
|
|
54
|
+
describe "#earliest_data_time" do
|
55
|
+
subject { table.earliest_data_time }
|
56
|
+
it { should == table.data.keys.first }
|
57
|
+
end
|
58
|
+
|
59
|
+
describe "#total_read_units" do
|
60
|
+
subject { table.total_read_units }
|
61
|
+
it { should == 1900 }
|
62
|
+
end
|
63
|
+
|
64
|
+
describe "#total_write_units" do
|
65
|
+
subject { table.total_write_units }
|
66
|
+
it { should == 2600 }
|
67
|
+
end
|
68
|
+
|
69
|
+
describe "#lost_read_units" do
|
70
|
+
subject { table.lost_read_units }
|
71
|
+
it { should == 0 }
|
72
|
+
end
|
73
|
+
|
74
|
+
describe "#lost_write_units" do
|
75
|
+
subject { table.lost_write_units }
|
76
|
+
it { should == 0 }
|
77
|
+
end
|
78
|
+
|
79
|
+
describe "#wasted_read_units" do
|
80
|
+
subject { table.wasted_read_units }
|
81
|
+
it { should == 1820 }
|
82
|
+
end
|
83
|
+
|
84
|
+
describe "#wasted_write_units" do
|
85
|
+
subject { table.wasted_write_units }
|
86
|
+
it { should == 2480 }
|
87
|
+
end
|
88
|
+
|
89
|
+
context 'AWS region us-east' do
|
90
|
+
before { AWS.config(region: 'us-east-1') }
|
91
|
+
|
92
|
+
describe "#total_read_cost" do
|
93
|
+
subject { table.total_read_cost }
|
94
|
+
it { should be_a Float }
|
95
|
+
it { should be >= 0 }
|
96
|
+
end
|
97
|
+
|
98
|
+
describe "#total_write_cost" do
|
99
|
+
subject { table.total_write_cost }
|
100
|
+
it { should be_a Float }
|
101
|
+
it { should be >= 0 }
|
102
|
+
end
|
103
|
+
|
104
|
+
describe "#total_read_cost" do
|
105
|
+
subject { table.total_read_cost }
|
106
|
+
it { should be_a Float }
|
107
|
+
it { should be >= 0 }
|
108
|
+
end
|
109
|
+
|
110
|
+
describe "#lost_write_cost" do
|
111
|
+
subject { table.lost_write_cost }
|
112
|
+
it { should be_a Float }
|
113
|
+
it { should be >= 0 }
|
114
|
+
end
|
115
|
+
|
116
|
+
describe "#lost_read_cost" do
|
117
|
+
subject { table.lost_read_cost }
|
118
|
+
it { should be_a Float }
|
119
|
+
it { should be >= 0 }
|
120
|
+
end
|
121
|
+
|
122
|
+
describe "#wasted_read_cost" do
|
123
|
+
subject { table.wasted_read_cost }
|
124
|
+
it { should be_a Float }
|
125
|
+
it { should be >= 0 }
|
126
|
+
end
|
127
|
+
|
128
|
+
describe "#wasted_write_cost" do
|
129
|
+
subject { table.wasted_write_cost }
|
130
|
+
it { should be_a Float }
|
131
|
+
it { should be >= 0 }
|
132
|
+
end
|
133
|
+
end
|
134
|
+
|
135
|
+
describe "#lost_write_percent" do
|
136
|
+
subject { table.lost_write_percent }
|
137
|
+
it { should be_a Float }
|
138
|
+
it { should be >= 0 }
|
139
|
+
it { should be <= 100 }
|
140
|
+
end
|
141
|
+
|
142
|
+
describe "#lost_read_percent" do
|
143
|
+
subject { table.lost_read_percent }
|
144
|
+
it { should be_a Float }
|
145
|
+
it { should be >= 0 }
|
146
|
+
it { should be <= 100 }
|
147
|
+
end
|
148
|
+
|
149
|
+
describe "#wasted_read_percent" do
|
150
|
+
subject { table.wasted_read_percent }
|
151
|
+
it { should be_a Float }
|
152
|
+
it { should be >= 0 }
|
153
|
+
it { should be <= 100 }
|
154
|
+
end
|
155
|
+
|
156
|
+
describe "#wasted_write_percent" do
|
157
|
+
subject { table.wasted_write_percent }
|
158
|
+
it { should be_a Float }
|
159
|
+
it { should be >= 0 }
|
160
|
+
it { should be <= 100 }
|
161
|
+
end
|
162
|
+
|
54
163
|
describe "#last 3.seconds, :consumed_reads" do
|
55
164
|
subject { table.last 3.seconds, :consumed_reads }
|
56
165
|
it { should == [20.0] }
|
@@ -76,12 +185,66 @@ describe DynamoAutoscale::TableTracker do
|
|
76
185
|
it { should == 800.0 }
|
77
186
|
end
|
78
187
|
|
188
|
+
describe "#last_provisioned_for :writes, at: 3.hours.ago" do
|
189
|
+
subject { table.last_provisioned_for :writes, at: 3.hours.ago }
|
190
|
+
it { should == nil }
|
191
|
+
end
|
192
|
+
|
193
|
+
describe "#last_consumed_for :reads" do
|
194
|
+
subject { table.last_consumed_for :reads }
|
195
|
+
it { should == 20.0 }
|
196
|
+
end
|
197
|
+
|
198
|
+
describe "#last_consumed_for :writes, at: now" do
|
199
|
+
subject { table.last_consumed_for :writes, at: now }
|
200
|
+
it { should == 30.0 }
|
201
|
+
end
|
202
|
+
|
203
|
+
describe "#last_consumed_for :writes, at: 3.minutes.ago" do
|
204
|
+
subject { table.last_consumed_for :writes, at: 3.minutes.ago }
|
205
|
+
it { should == 30.0 }
|
206
|
+
end
|
207
|
+
|
208
|
+
describe "#last_consumed_for :writes, at: 3.hours.ago" do
|
209
|
+
subject { table.last_consumed_for :writes, at: 3.hours.ago }
|
210
|
+
it { should == nil }
|
211
|
+
end
|
212
|
+
|
79
213
|
describe "#all_times" do
|
80
214
|
subject { table.all_times }
|
81
215
|
its(:length) { should == 4 }
|
82
216
|
|
83
217
|
specify("is ordered") { subject.should == subject.sort }
|
84
218
|
end
|
219
|
+
|
220
|
+
describe "#to_csv!" do
|
221
|
+
let(:tempfile) { Tempfile.new(table_name) }
|
222
|
+
subject { File.readlines(table.to_csv!(path: tempfile.path)) }
|
223
|
+
its(:count) { should == 5 }
|
224
|
+
after { tempfile.unlink }
|
225
|
+
end
|
226
|
+
|
227
|
+
describe "#report!" do
|
228
|
+
context "metric: :units:" do
|
229
|
+
it "should not error" do
|
230
|
+
table.report! metric: :units
|
231
|
+
end
|
232
|
+
end
|
233
|
+
|
234
|
+
context "metric: :cost" do
|
235
|
+
it "should not error" do
|
236
|
+
table.report! metric: :cost
|
237
|
+
end
|
238
|
+
end
|
239
|
+
|
240
|
+
context "metric: :bananas" do
|
241
|
+
it "should error" do
|
242
|
+
expect do
|
243
|
+
table.report! metric: :bananas
|
244
|
+
end.to raise_error ArgumentError
|
245
|
+
end
|
246
|
+
end
|
247
|
+
end
|
85
248
|
end
|
86
249
|
|
87
250
|
describe 'clearing data' do
|