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