dynamo-autoscale 0.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.
Files changed (52) hide show
  1. data/.gitignore +4 -0
  2. data/Gemfile +13 -0
  3. data/Gemfile.lock +58 -0
  4. data/LICENSE +21 -0
  5. data/README.md +400 -0
  6. data/Rakefile +9 -0
  7. data/aws.sample.yml +16 -0
  8. data/bin/dynamo-autoscale +131 -0
  9. data/config/environment/common.rb +114 -0
  10. data/config/environment/console.rb +2 -0
  11. data/config/environment/test.rb +3 -0
  12. data/config/logger.yml +11 -0
  13. data/config/services/aws.rb +20 -0
  14. data/config/services/logger.rb +35 -0
  15. data/data/.gitkeep +0 -0
  16. data/dynamo-autoscale.gemspec +29 -0
  17. data/lib/dynamo-autoscale/actioner.rb +265 -0
  18. data/lib/dynamo-autoscale/cw_poller.rb +49 -0
  19. data/lib/dynamo-autoscale/dispatcher.rb +39 -0
  20. data/lib/dynamo-autoscale/dynamo_actioner.rb +59 -0
  21. data/lib/dynamo-autoscale/ext/active_support/duration.rb +7 -0
  22. data/lib/dynamo-autoscale/local_actioner.rb +39 -0
  23. data/lib/dynamo-autoscale/local_data_poll.rb +51 -0
  24. data/lib/dynamo-autoscale/logger.rb +15 -0
  25. data/lib/dynamo-autoscale/metrics.rb +192 -0
  26. data/lib/dynamo-autoscale/poller.rb +41 -0
  27. data/lib/dynamo-autoscale/pretty_formatter.rb +27 -0
  28. data/lib/dynamo-autoscale/rule.rb +180 -0
  29. data/lib/dynamo-autoscale/rule_set.rb +69 -0
  30. data/lib/dynamo-autoscale/table_tracker.rb +329 -0
  31. data/lib/dynamo-autoscale/unit_cost.rb +41 -0
  32. data/lib/dynamo-autoscale/version.rb +3 -0
  33. data/lib/dynamo-autoscale.rb +1 -0
  34. data/rlib/dynamodb_graph.r +15 -0
  35. data/rlib/dynamodb_scatterplot.r +13 -0
  36. data/rulesets/default.rb +5 -0
  37. data/rulesets/erroneous.rb +1 -0
  38. data/rulesets/gradual_tail.rb +11 -0
  39. data/rulesets/none.rb +0 -0
  40. data/script/console +3 -0
  41. data/script/historic_data +46 -0
  42. data/script/hourly_wastage +40 -0
  43. data/script/monitor +55 -0
  44. data/script/simulator +40 -0
  45. data/script/test +52 -0
  46. data/script/validate_ruleset +20 -0
  47. data/spec/actioner_spec.rb +244 -0
  48. data/spec/rule_set_spec.rb +89 -0
  49. data/spec/rule_spec.rb +491 -0
  50. data/spec/spec_helper.rb +4 -0
  51. data/spec/table_tracker_spec.rb +256 -0
  52. metadata +178 -0
data/.gitignore ADDED
@@ -0,0 +1,4 @@
1
+ config/aws.yml
2
+ aws.yml
3
+ data/
4
+ pkg/
data/Gemfile ADDED
@@ -0,0 +1,13 @@
1
+ source 'https://rubygems.org'
2
+
3
+ gemspec
4
+
5
+ group :development do
6
+ gem 'pry'
7
+ gem 'ripl'
8
+ gem 'timecop'
9
+ end
10
+
11
+ group :test do
12
+ gem 'rspec'
13
+ end
data/Gemfile.lock ADDED
@@ -0,0 +1,58 @@
1
+ PATH
2
+ remote: .
3
+ specs:
4
+ dynamo-autoscale (0.1)
5
+ aws-sdk
6
+ colored
7
+ rbtree
8
+ ruby-prof
9
+
10
+ GEM
11
+ remote: https://rubygems.org/
12
+ specs:
13
+ activesupport (3.2.13)
14
+ i18n (= 0.6.1)
15
+ multi_json (~> 1.0)
16
+ aws-sdk (1.11.2)
17
+ json (~> 1.4)
18
+ nokogiri (< 1.6.0)
19
+ uuidtools (~> 2.1)
20
+ bond (0.4.3)
21
+ coderay (1.0.9)
22
+ colored (1.2)
23
+ diff-lcs (1.2.4)
24
+ i18n (0.6.1)
25
+ json (1.8.0)
26
+ method_source (0.8.1)
27
+ multi_json (1.7.6)
28
+ nokogiri (1.5.10)
29
+ pry (0.9.12.2)
30
+ coderay (~> 1.0.5)
31
+ method_source (~> 0.8)
32
+ slop (~> 3.4)
33
+ rbtree (0.4.1)
34
+ ripl (0.7.0)
35
+ bond (~> 0.4.2)
36
+ rspec (2.13.0)
37
+ rspec-core (~> 2.13.0)
38
+ rspec-expectations (~> 2.13.0)
39
+ rspec-mocks (~> 2.13.0)
40
+ rspec-core (2.13.1)
41
+ rspec-expectations (2.13.0)
42
+ diff-lcs (>= 1.1.3, < 2.0)
43
+ rspec-mocks (2.13.1)
44
+ ruby-prof (0.13.0)
45
+ slop (3.4.5)
46
+ timecop (0.6.1)
47
+ uuidtools (2.1.4)
48
+
49
+ PLATFORMS
50
+ ruby
51
+
52
+ DEPENDENCIES
53
+ activesupport
54
+ dynamo-autoscale!
55
+ pry
56
+ ripl
57
+ rspec
58
+ timecop
data/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ The MIT License (MIT)
2
+
3
+ Copyright (c) 2013 InvisibleHand Software Ltd
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in
13
+ all copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
21
+ THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,400 @@
1
+ # DynamoDB Autoscaling
2
+
3
+ **IMPORTANT**: It's highly recommended that you read this README before
4
+ continuing. This project, if used incorrectly, has a lot of potential to cost
5
+ you huge amounts of money. Proceeding with caution is paramount, as we cannot be
6
+ held responsible for misuse that leads to excessive cost on your part.
7
+
8
+ There are tools and flags in place that will allow you to dry-run the project
9
+ before actually allowing it to change your provisioned throughputs and it is
10
+ highly recommended that you first try running the project as a dry-run and
11
+ inspecting the log output to make sure it is doing what you expect.
12
+
13
+ It is also worth noting that this project is very much in its infancy.
14
+
15
+ You have been warned.
16
+
17
+ ## Rules of the game
18
+
19
+ Welcome to the delightful mini-game that is DynamoDB provisioned throughputs.
20
+ Here are the rules of the game:
21
+
22
+ - In a single API call, you can only change your throughput by up to 100% in
23
+ either direction. In other words, you can decrease as much as you want but
24
+ you can only increase to up to double what the current throughput is.
25
+
26
+ - You may scale up as many times per day as you like, however you may only
27
+ scale down 4 times per day per table. (If you scale both reads and writes
28
+ down in the same request, that only counts as 1 downscale used)
29
+
30
+ - Scaling is not an instantaneous event. It can take up to 5 minutes for a
31
+ table's throughput to be updated.
32
+
33
+ - Small spikes over your threshold are tolerated but the exact amount of time
34
+ they are tolerated for seems to vary.
35
+
36
+ This project aims to take all of this into consideration and automatically scale
37
+ your throughputs to enable you to deal with spikes and save money where
38
+ possible.
39
+
40
+ # Configuration
41
+
42
+ This library requires AWS keys that have access to both CloudWatch and DynamoDB,
43
+ for retriving data and sending scaling requests. Using IAM, create a new user, and
44
+ assign the 'CloudWatch Read Only Access' policy template. In addition, you will
45
+ need to use the Policy Generator to add at least the following Amazon DynamoDB actions:
46
+
47
+ - "dynamodb:DescribeTable"
48
+ - "dynamodb:ListTables"
49
+ - "dynamodb:UpdateTable"
50
+
51
+ The ARN for the custom policy can be specified as '\*' to allow access to all tables,
52
+ or alternatively you can refer to the IAM documentation to limit access to specific
53
+ tables only.
54
+
55
+ The project will look for a YAML file in the following locations on start up:
56
+
57
+ - ./aws.yml
58
+ - ENV['AWS_CONFIG']
59
+
60
+ If it doesn't find an AWS YAML config in any of those locations, the process
61
+ prints an error and exits.
62
+
63
+ **A sample config can be found in the project root directory.**
64
+
65
+ # Usage
66
+
67
+ First of all, you'll need to install this project as a gem:
68
+
69
+ $ gem install dynamo-autoscale
70
+
71
+ This will give you access to the `dynamo-autoscale` executable. For some
72
+ internal documentation on the executable, you can run:
73
+
74
+ $ dynamo-autoscale -h
75
+
76
+ This should tell you what flags you can set and what arguments the command
77
+ expects.
78
+
79
+ ## Logging
80
+
81
+ By default, not a whole lot will be logged at first. If you want to be sure that
82
+ the gem is working and doing things, you can run with the `DEBUG` environment
83
+ variable set to `true`:
84
+
85
+ $ DEBUG=true dynamo-autoscale <args...>
86
+
87
+ Also, if you want pretty coloured logging, you can set the `PRETTY_LOG`
88
+ environment variable to `true`:
89
+
90
+ $ PRETTY_LOG=true DEBUG=true dynamo-autoscale <args...>
91
+
92
+ ## Rulesets
93
+
94
+ One of the first things you'll notice upon looking into the `--help` on the
95
+ executable is that it's looking for a "rule set". What on earth is a rule set?
96
+
97
+ A rule set is the primary user input for dynamo-autoscale. It is a DSL for
98
+ specifying when to increase and decrease your provisioned throughputs. Here is a
99
+ very basic rule set:
100
+
101
+ ``` ruby
102
+ reads last: 1, greater_than: "90%", scale: { on: :consumed, by: 2 }
103
+ writes last: 1, greater_than: "90%", scale: { on: :consumed, by: 2 }
104
+
105
+ reads for: 2.hours, less_than: "50%", min: 2, scale: { on: :consumed, by: 2 }
106
+ writes for: 2.hours, less_than: "50%", min: 2, scale: { on: :consumed, by: 2 }
107
+ ```
108
+
109
+ You would put this ruleset in a file and then pass that file in as the first
110
+ argument to `dynamo-autoscale` on the command line.
111
+
112
+ The first two rules are designed to deal with spikes. They are saying that if
113
+ the consumed capacity units is greater than 90% of the provisioned throughput
114
+ for a single data point, scale the provisioned throughput up by the last
115
+ consumed units multiplied by two.
116
+
117
+ For example, if we had a provisioned reads of 100 and a consumed units of
118
+ 95 comes through, that will trigger that rule and the table will be scaled up to
119
+ have a provisioned reads of 190.
120
+
121
+ The last two rules are controlling downscaling. Because downscaling can only
122
+ happen 4 times per day per table, the rules are far less aggressive. Those rules
123
+ are saying: if the consumed capacity is less than 50% of the provisioned for a
124
+ whole two hours, with a minimum of 2 data points, scale the provisioned
125
+ throughput to the consumed units multiplied by 2.
126
+
127
+ ### The :last and :for options
128
+
129
+ These options declare how many points or what time range you want to examine.
130
+ They're aliases of each other and if you specify both, one will be ignored. If
131
+ you don't specify a `:min` or `:max` option, they will just get as many points
132
+ as they can and evaluate the rest of the rule even if they don't get a full 2
133
+ hours of data, or a full 6 points of data. This only affects the start of the
134
+ process's lifetime, eventually it will have enough data to always get the full
135
+ range of points you're asking for.
136
+
137
+ ### The :min and :max options
138
+
139
+ If you're not keen on asking for 2 hours of data and not receiving the full
140
+ range before evaluating the rest of the rule, you can specify a minimum or
141
+ maximum number of points to evaluate. Currently, this only supports a numeric
142
+ value. So you can ask for at least 20 points to be present like so:
143
+
144
+ ``` ruby
145
+ reads for: 2.hours, less_than: "50%", min: 20, scale: { on: :consumed, by: 2 }
146
+ ```
147
+
148
+ ### The :greater_than and :less_than options
149
+
150
+ You must specify at least one of these options for the rule to actually validate
151
+ without throwing an error. Having neither makes no sense.
152
+
153
+ You can specify either an absolute value or a percentage specified as a string.
154
+ The percentage will calculate the percentage consumed against the amount
155
+ provisioned.
156
+
157
+ Examples:
158
+
159
+ ``` ruby
160
+ reads for: 2.hours, less_than: 10, scale: { on: :consumed, by: 2 }
161
+
162
+ reads for: 2, less_than: "20%", scale: { on: :consumed, by: 2 }
163
+ ```
164
+
165
+ ### The :scale option
166
+
167
+ The `:scale` option is a way of doing a simple change to the provisioned
168
+ throughput without having to specify repetitive stuff in a block. `:scale`
169
+ expects to be a hash and it expects to have two keys in the hash: `:on` and
170
+ `:by`.
171
+
172
+ `:on` specifies what part of the metric you want to scale on. It can either by
173
+ `:provisioned` or `:consumed`. In most cases, `:consumed` makes a lot more sense
174
+ than `:provisioned`.
175
+
176
+ `:by` specifies the scale factor. If you want to double the provisioned capacity
177
+ when a rule triggers, you would write something like this:
178
+
179
+ ``` ruby
180
+ reads for: 2.hours, less_than: "30%", scale: { on: :provisioned, by: 0.5 }
181
+ ```
182
+
183
+ And that would half the provisioned throughput for reads if the consumed is
184
+ less than 30% of the provisioned for 2 hours.
185
+
186
+ ### Passing a block
187
+
188
+ If you want to do something a little bit more complicated with your rules, you
189
+ can pass a block to them. The block will get passed three things: the table the
190
+ rule was triggered for, the rule object that triggered and the actioner for that
191
+ table.
192
+
193
+ An actioner is an abstraction of communication with Dynamo and it allows
194
+ communication to be faked if you want to do a dry run. It exposes a very simple
195
+ interface. Here's an example:
196
+
197
+ ``` ruby
198
+ writes for: 2.hours, greater_than: 200 do |table, rule, actioner|
199
+ actioner.set(:writes, 300)
200
+ end
201
+ ```
202
+
203
+ This rule will set the provisioned write throughput to 300 if the consumed
204
+ writes are greater than 200 for 2 hours. The actioner handles a tonne of things
205
+ under the hood, such as making sure you don't scale up more than you're allowed
206
+ to in a single call and making sure you don't try to change a table when it's in
207
+ the updating state.
208
+
209
+ It also handles the grouping of downscales, which we will talk about in a later
210
+ section of the README.
211
+
212
+ The `table` argument is a `TableTracker` object. For a run down of what
213
+ information is available to you I advise checking out the source code in
214
+ `lib/dynamo-autoscale/table_tracker.rb`.
215
+
216
+ ### The :count option
217
+
218
+ The `:count` option allows you to specify that a rule must be triggered a set
219
+ number of times in a row before its action is executed.
220
+
221
+ Example:
222
+
223
+ ``` ruby
224
+ writes for: 10.minutes, greater_than: "90%", count: 3, scale: { on: :consumed, by: 1.5 }
225
+ ```
226
+
227
+ This says that is writes are greater than 90% for 10 minutes three checks in a
228
+ row, scale by the amount consumed multiplied by 1.5. A new check will only
229
+ happen when the table receives new data from cloud watch, which means that the
230
+ 10 minute windows could potentially overlap.
231
+
232
+ ## Downscale grouping
233
+
234
+ You can downscale reads or writes individually and this will cost you one of
235
+ your four downscales for the current day. Or, you can downscale reads and writes
236
+ at the same time and this also costs you one of your four. (Reference:
237
+ http://docs.aws.amazon.com/amazondynamodb/latest/developerguide/Limits.html)
238
+
239
+ Because of this, the actioner can handle the grouping up of downscales. Let's
240
+ say you passed in the following options in at the command line:
241
+
242
+ $ dynamo-autoscale some/ruleset.rb some_table --group-downscales --flush-after 300
243
+
244
+ What this is saying is that if a write downscale came in, the actioner wouldn't
245
+ fire it off immediately. It would wait 300 seconds, or 5 minutes, to see if a
246
+ corresponding read downscale was triggered and would run them both at the same
247
+ time. If no corresponding read came in, after 5 minutes the pending write
248
+ downscale would get "flushed" and applied without a read downscale.
249
+
250
+ This technique helps to save downscales on tables that may have unpredictable
251
+ consumption. You may need to tweak the `--flush-after` value to match your own
252
+ situation. By default, there is no `--flush-after` and downscales will wait
253
+ indefinitely, this may not be desirable.
254
+
255
+ ## Signaling
256
+
257
+ The `dynamo-autoscale` process responds to the SIGUSR1 and SIGUSR2 signals. What
258
+ we've done may be a dramatic bastardisation of what signals are intended for or
259
+ how they work, but here's what each does.
260
+
261
+ ### USR1
262
+
263
+ If you send SIGUSR1 to the process as it's running, the process will dump all of
264
+ the data it has collected on all of the tables it is collecting for into CSV
265
+ files in the directory it was run in.
266
+
267
+ Example:
268
+
269
+ $ dynamo-autoscale some/ruleset.rb some_table
270
+ # Runs as PID 1234. Wait for some time to pass...
271
+ $ kill -USR1 1234
272
+ $ cat some_table.csv
273
+
274
+ The CSV is in the following format:
275
+
276
+ time,provisioned_reads,provisioned_writes,consumed_reads,consumed_writes
277
+ 2013-07-02T10:48:00Z,800.0,600.0,390.93666666666667,30.54
278
+ 2013-07-02T10:49:00Z,800.0,600.0,390.93666666666667,30.54
279
+ 2013-07-02T10:53:00Z,800.0,600.0,386.4533333333333,95.26666666666667
280
+ 2013-07-02T10:54:00Z,800.0,600.0,386.4533333333333,95.26666666666667
281
+ 2013-07-02T10:58:00Z,800.0,600.0,110.275,25.406666666666666
282
+ 2013-07-02T10:59:00Z,800.0,600.0,246.12,54.92
283
+
284
+ ### USR2
285
+
286
+ If you send SIGUSR2 to the process as it's running, the process will take all of
287
+ the data it has on all of its tables and generate a graph for each table using R
288
+ (see the Graphs section below). This is handy for visualising what the process
289
+ is doing, especially after doing a few hours of a `--dry-run`.
290
+
291
+ # Developers / Tooling
292
+
293
+ Everything below this part of the README is intended for people that want to
294
+ work on the dynamo-autoscale codebase or use the internal tools that we use for
295
+ testing new rulesets.
296
+
297
+ ## Technical details
298
+
299
+ The code has a set number of moving parts that are globally available and must
300
+ implement certain interfaces (for exact details, you would need to study the
301
+ code):
302
+
303
+ - `DynamoAutoscale.poller`: This component is responsible for pulling data
304
+ from a data source (CloudWatch or Local at the moment) and piping it into
305
+ the next stage in the pipeline.
306
+
307
+ - `DynamoAutoscale.dispatcher`: The dispatcher takes data from the poller and
308
+ populates a hash table of `TableTracker` objects, as well as checking to see
309
+ if any of the tables have triggered any rules.
310
+
311
+ - `DynamoAutoscale.rules`: The ruleset contains an array of `Rule` objects
312
+ inside a hash table keyed by table name. The ruleset initializer takes a
313
+ file path as an argument, or a block, either of these needs to contain a set
314
+ of rules (examples can be found in the `rulesets/` directory).
315
+
316
+ - `DynamoAutoscale.actioners`: The actioners are what perform provision scaling.
317
+ Locally this is faked, in production it makes API calls to DynamoDB.
318
+
319
+ - `DynamoAutoscale.tables`: This is a hash table of `TableTracker` objects,
320
+ keyed on the table name.
321
+
322
+ All of these components are globally available because most of them need access
323
+ to each other and it was a pain to pass instances of them around to everybody
324
+ that needed them.
325
+
326
+ They're also completely swappable. As long as they implement the right methods
327
+ you can get your data from anywhere, dispatch your data to anywhere and send
328
+ your actions to whatever you want. The defaults all work on local data gathered
329
+ with the `script/historic_data` executable.
330
+
331
+ ## Testing rules locally
332
+
333
+ If you want to test rules on your local machine without having to query
334
+ CloudWatch or hit DynamoDB, there are tools that facilitate that nicely.
335
+
336
+ The first thing you would need to do is gather some historic data. There's a
337
+ script called `script/historic_data` that you can run to gather data on a
338
+ specific table and store it into the `data/` directory in a format that all of
339
+ the other scripts are familiar with.
340
+
341
+ Next there are a couple of things you can do.
342
+
343
+ ### Running a test
344
+
345
+ You can run a big batch of data all in one go with the `script/test` script.
346
+ This script can be invoked like this:
347
+
348
+ $ script/test rulesets/default.rb table_name
349
+
350
+ Substituting `table_name` with the name of a table that exists in your DynamoDB.
351
+ This will run through all of the data for that table in time order, logging
352
+ along the way and triggering rules from the rule set if any were defined.
353
+
354
+ At the end, it shows you a report on the amount of wasted, used and lost units.
355
+
356
+ #### Graphs
357
+
358
+ If you felt so inclined, you could add the `--graph` flag to the above command
359
+ and the script will generate a graph for you at the end. This will shell out to
360
+ an R process to generate the graph, so you will need to ensure that you have R
361
+ installed on your system with the `ggplot2` and `reshape` packages installed.
362
+
363
+ Personally, I use a Mac and I attempted to install R through Homebrew but had
364
+ troubles with compiling packages. I had far more success when I installed R
365
+ straight from the R website, http://cran.r-project.org/bin/macosx/, and used
366
+ their GUI R.app to install the packages.
367
+
368
+ None of this is required to run the `dynamo-autoscale` executable in production.
369
+
370
+ ### Simulating data coming in
371
+
372
+ There's a script called `script/simulator` that allows you to step through data
373
+ as it arrives. It takes the exact same arguments as the `script/test` script but
374
+ instead of running all the way through the data and generating a report,
375
+ `script/simulate` will pause after each round of new data and drop you into a
376
+ REPL. This is very handy for debugging tricky situations with your rules or the
377
+ codebase.
378
+
379
+ The simulator does not hit CloudWatch or DynamoDB at any point.
380
+
381
+ ## Contributing
382
+
383
+ Report Issues/Feature requests on
384
+ [GitHub Issues](https://github.com/invisiblehand/dynamo-autoscale/issues).
385
+
386
+ #### Note on Patches/Pull Requests
387
+
388
+ * Fork the project.
389
+ * Make your feature addition or bug fix.
390
+ * Add tests for it. This is important so we don't break it in a
391
+ future version unintentionally.
392
+ * Commit, do not modify the rakefile, version, or history.
393
+ (if you want to have your own version, that is fine but bump version in a commit by itself so it can be ignored when we pull)
394
+ * Send a pull request. Bonus points for topic branches.
395
+
396
+ ### Copyright
397
+
398
+ Copyright (c) 2013 InvisibleHand Software Ltd. See
399
+ [LICENSE](https://github.com/invisiblehand/dynamo-autoscale/blob/master/LICENSE)
400
+ for details.
data/Rakefile ADDED
@@ -0,0 +1,9 @@
1
+ require 'rspec/core/rake_task'
2
+ require 'bundler/gem_tasks'
3
+
4
+ task :default => [:test]
5
+
6
+ desc "Run all tests"
7
+ RSpec::Core::RakeTask.new(:test) do |t|
8
+ t.rspec_opts = '-cfs'
9
+ end
data/aws.sample.yml ADDED
@@ -0,0 +1,16 @@
1
+ default: &default
2
+ :access_key_id: your_id
3
+ :secret_access_key: your_key
4
+
5
+ development:
6
+ <<: *default
7
+ :dynamo_db_endpoint: dynamodb.us-east-1.amazonaws.com
8
+
9
+ test:
10
+ <<: *default
11
+ :dynamo_db_endpoint: localhost
12
+ :dynamo_db_port: 4568
13
+
14
+ production:
15
+ <<: *default
16
+ :dynamo_db_endpoint: dynamodb.us-east-1.amazonaws.com
@@ -0,0 +1,131 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require 'pp'
4
+ require 'optparse'
5
+ require 'active_support/all'
6
+
7
+ # Force this script into production mode as it's the only thing that will
8
+ # actually hit DynamoDB in the entire project.
9
+ ENV['RACK_ENV'] = "production"
10
+
11
+ actioner_opts = {}
12
+ general_opts = {}
13
+
14
+ OptionParser.new do |opts|
15
+ opts.banner = "Usage: dynamo-autoscale ruleset_path table_name [more table names] [options]"
16
+
17
+ doc = 'Makes read and write downscales happen at the same time to save ' +
18
+ 'downscales per day.'
19
+
20
+ opts.on('-g', '--group-downscales', doc) do
21
+ actioner_opts[:group_downscales] = true
22
+ end
23
+
24
+ doc = 'Only works in conjunction with --group-downscales. Sets a maximum ' +
25
+ 'amount of time for an operation to be pending before it gets applied to Dynamo'
26
+
27
+ opts.on('--flush-after SECONDS', Integer, doc) do |seconds|
28
+ actioner_opts[:flush_after] = seconds.to_i.seconds
29
+ end
30
+
31
+ doc = 'Stops dynamo-autoscale from talking to DynamoDB. Instead, it just ' +
32
+ 'tracks the changes it would have made locally.'
33
+
34
+ opts.on('--dry-run', doc) do
35
+ general_opts[:dry_run] = true
36
+ end
37
+
38
+ doc = "Sets a minimum value for throughputs to be set to. " +
39
+ "Defaults to 10."
40
+
41
+ opts.on('--minimum-throughput VALUE', Float, doc) do |value|
42
+ if value < 1.0
43
+ STDERR.puts "Cannot set minimum throughput to less than 1."
44
+ exit 1
45
+ end
46
+
47
+ general_opts[:minimum_throughput] = value
48
+ end
49
+
50
+ doc = "Sets a maximum value for throughputs to be set to. " +
51
+ "Defaults to 20,000."
52
+
53
+ opts.on('--maximum-throughput VALUE', Float, doc) do |value|
54
+ general_opts[:maximum_throughput] = value
55
+ end
56
+
57
+ opts.on( '-h', '--help', 'Display this screen' ) do
58
+ puts opts
59
+ exit
60
+ end
61
+ end.parse!
62
+
63
+ ruleset = ARGV.shift
64
+ tables = ARGV
65
+
66
+ if tables.empty? or ruleset.nil?
67
+ STDERR.puts "Usage: dynamo-autoscale ruleset table_name [another_table_name ... ]"
68
+ exit 1
69
+ end
70
+
71
+ if actioner_opts[:flush_after] and actioner_opts[:group_downscales].nil?
72
+ STDERR.puts "Cannot specify a flush_after value with setting --group-downscales."
73
+ exit 1
74
+ end
75
+
76
+ require_relative '../config/environment/common'
77
+ include DynamoAutoscale
78
+ extend DynamoAutoscale
79
+
80
+ dynamo = AWS::DynamoDB.new
81
+ tables.select! do |table_name|
82
+ if dynamo.tables[table_name].exists?
83
+ true
84
+ else
85
+ logger.error "Table #{table_name} does not exist inside your DynamoDB."
86
+ false
87
+ end
88
+ end
89
+
90
+ if tables.empty?
91
+ STDERR.puts "No valid tables specified."
92
+ exit 1
93
+ end
94
+
95
+ poller_opts = { tables: tables }
96
+
97
+ if general_opts[:dry_run]
98
+ poller_opts[:filters] = LocalActioner.faux_provisioning_filters
99
+ end
100
+
101
+ DynamoAutoscale.rules = RuleSet.new(ruleset)
102
+ DynamoAutoscale.dispatcher = Dispatcher.new
103
+ DynamoAutoscale.poller = CWPoller.new(poller_opts)
104
+ DynamoAutoscale.actioner_class = general_opts[:dry_run] ? LocalActioner : DynamoActioner
105
+ DynamoAutoscale.actioner_opts = actioner_opts
106
+
107
+ if general_opts[:minimum_throughput]
108
+ Actioner.minimum_throughput = general_opts[:minimum_throughput]
109
+ end
110
+
111
+ if general_opts[:maximum_throughput]
112
+ Actioner.maximum_throughput = general_opts[:maximum_throughput]
113
+ end
114
+
115
+ Signal.trap("USR1") do
116
+ logger.info "[signal] Caught SIGUSR1. Dumping CSV for all tables in #{Dir.pwd}"
117
+
118
+ DynamoAutoscale.tables.each do |name, table|
119
+ table.to_csv! path: File.join(Dir.pwd, "#{table.name}.csv")
120
+ end
121
+ end
122
+
123
+ Signal.trap("USR2") do
124
+ logger.info "[signal] Caught SIGUSR2. Dumping graphs for all tables in #{Dir.pwd}"
125
+
126
+ DynamoAutoscale.tables.each do |name, table|
127
+ table.graph! path: File.join(Dir.pwd, "#{table.name}.png")
128
+ end
129
+ end
130
+
131
+ DynamoAutoscale.poller.run