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.
@@ -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