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.
@@ -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
@@ -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! open: true
57
- puts "Graph saved to #{path}"
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
@@ -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
@@ -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
@@ -1,4 +1 @@
1
1
  require_relative '../config/environment/test'
2
-
3
- RSpec.configure do |config|
4
- end
@@ -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