dynamo-autoscale 0.2.9 → 0.3

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/CHANGELOG CHANGED
@@ -1,3 +1,11 @@
1
+ v0.3
2
+
3
+ - Added backdating on startup. When dynamo-autoscale starts up, it now fetches 6
4
+ hours of data for each table then starts polling.
5
+ - Added a check before actioning that makes sure CloudWatch and DynamoDB agree
6
+ on what the current provisioned throughput is. Fixes a bug where downscales
7
+ were happening too frequently.
8
+
1
9
  v0.2.9
2
10
 
3
11
  - Fixed a problem where scale events weren't being showed in the scale report
data/bin/dynamo-autoscale CHANGED
@@ -33,5 +33,8 @@ unless DynamoAutoscale.config[:dry_run]
33
33
  DynamoAutoscale.actioner_class = DynamoAutoscale::DynamoActioner
34
34
  end
35
35
 
36
- DynamoAutoscale.logger.info "Finished setup. Starting polling loop."
36
+ DynamoAutoscale.logger.info "Finished setup. Backdating..."
37
+ DynamoAutoscale.poller.backdate
38
+
39
+ DynamoAutoscale.logger.info "Starting polling loop..."
37
40
  DynamoAutoscale.poller.run
@@ -245,13 +245,26 @@ module DynamoAutoscale
245
245
  return result
246
246
  end
247
247
 
248
+ def pending_reads?
249
+ !!@pending[:reads]
250
+ end
251
+
252
+ def pending_writes?
253
+ !!@pending[:writes]
254
+ end
255
+
256
+ def clear_pending!
257
+ @pending[:writes] = nil
258
+ @pending[:reads] = nil
259
+ end
260
+
248
261
  def should_flush?
249
262
  if @opts[:group_downscales].nil?
250
263
  logger.info "[flush] Downscales are not being grouped. Should flush."
251
264
  return true
252
265
  end
253
266
 
254
- if @pending[:reads] and @pending[:writes]
267
+ if pending_reads? and pending_writes?
255
268
  logger.info "[flush] Both a read and a write are pending. Should flush."
256
269
  return true
257
270
  end
@@ -3,10 +3,21 @@ module DynamoAutoscale
3
3
  include DynamoAutoscale::Logger
4
4
  INTERVAL = 5.minutes
5
5
 
6
- def poll tables, &block
7
- if tables.nil?
8
- tables = AWS::DynamoDB.new.tables.to_a.map(&:name)
6
+ def backdate
7
+ now = Time.now.utc
8
+
9
+ @tables.each do |table_name|
10
+ table = DynamoAutoscale.tables[table_name]
11
+ dispatch(table, Metrics.all_metrics(table_name, {
12
+ period: 5.minutes,
13
+ start_time: now - 6.hours,
14
+ end_time: now,
15
+ }))
9
16
  end
17
+ end
18
+
19
+ def poll tables, &block
20
+ tables = AWS::DynamoDB.new.tables.to_a.map(&:name) if tables.nil?
10
21
 
11
22
  loop do
12
23
  # Sleep until the next interval occurrs. This calculation ensures that
@@ -24,7 +35,7 @@ module DynamoAutoscale
24
35
  logger.debug "[cw_poller] Beginning CloudWatch poll..."
25
36
  now = Time.now
26
37
 
27
- tables.each do |table|
38
+ tables.each do |table_name|
28
39
  # This code will dispatch a message to the listening table that looks
29
40
  # like this:
30
41
  #
@@ -39,7 +50,7 @@ module DynamoAutoscale
39
50
  #
40
51
  # There may also be :provisioned_reads and :provisioned_writes
41
52
  # depending on how the CloudWatch API feels.
42
- block.call(table, Metrics.all_metrics(table, {
53
+ block.call(table_name, Metrics.all_metrics(table_name, {
43
54
  period: 5.minutes,
44
55
  start_time: now - 20.minutes,
45
56
  end_time: now,
@@ -33,13 +33,18 @@ module DynamoAutoscale
33
33
  block.call(table, time, datum) if block
34
34
 
35
35
  if @last_check[table.name].nil? or @last_check[table.name] < time
36
- if DynamoAutoscale.actioners[table].can_run?
37
- logger.debug "[dispatcher] Checking rules..."
38
- DynamoAutoscale.rules.test(table)
39
- @last_check[table.name] = time
36
+ if time > 20.minutes.ago # Too young to vote!
37
+ if DynamoAutoscale.actioners[table].can_run?
38
+ logger.debug "[dispatcher] Checking rules..."
39
+ DynamoAutoscale.rules.test(table)
40
+ @last_check[table.name] = time
41
+ else
42
+ logger.debug "[dispatcher] Skipped rule check, table is not ready " +
43
+ "to have its throughputs modified."
44
+ end
40
45
  else
41
- logger.debug "[dispatcher] Skipped rule check, table is not ready " +
42
- "to have its throughputs modified."
46
+ logger.debug "[dispatcher] Skipped rule check, data point was from " +
47
+ "more than 20 minutes ago. Too young to vote!"
43
48
  end
44
49
  else
45
50
  logger.debug "[dispatcher] Skipped rule check, already checked for " +
@@ -14,17 +14,57 @@ module DynamoAutoscale
14
14
  :write_capacity_units
15
15
  end
16
16
 
17
- dynamo_scale(aws_throughput_key => value)
17
+ if throughput_synced?
18
+ dynamo_scale(aws_throughput_key => value)
19
+ else
20
+ # If the throughputs were not synced, the likelihood is we made the
21
+ # decision to scale based on false data. Clear it.
22
+ clear_pending!
23
+ false
24
+ end
18
25
  end
19
26
 
20
27
  def scale_both reads, writes
21
- dynamo_scale(read_capacity_units: reads, write_capacity_units: writes)
28
+ if throughput_synced?
29
+ dynamo_scale(read_capacity_units: reads, write_capacity_units: writes)
30
+ else
31
+ # If the throughputs were not synced, the likelihood is we made the
32
+ # decision to scale based on false data. Clear it.
33
+ clear_pending!
34
+ false
35
+ end
22
36
  end
23
37
 
24
38
  def can_run?
25
39
  dynamo.status == :active
26
40
  end
27
41
 
42
+ def throughput_synced?
43
+ time, datum = table.data.last
44
+
45
+ # If we've not gathered any data, we cannot know if our values are synced
46
+ # with Dynamo so we have to assume they are not until we get some data in.
47
+ return false if time.nil? or datum.nil?
48
+
49
+ if dynamo.read_capacity_units != datum[:provisioned_reads]
50
+ logger.error "[actioner] DynamoDB disagrees with CloudWatch on what " +
51
+ "the provisioned reads are. To be on the safe side, operations are " +
52
+ "not being applied."
53
+
54
+ return false
55
+ end
56
+
57
+ if dynamo.write_capacity_units != datum[:provisioned_writes]
58
+ logger.error "[actioner] DynamoDB disagrees with CloudWatch on what " +
59
+ "the provisioned writes are. To be on the safe side, operations are " +
60
+ "not being applied."
61
+
62
+ return false
63
+ end
64
+
65
+ return true
66
+ end
67
+
28
68
  private
29
69
 
30
70
  def dynamo_scale opts
@@ -19,24 +19,27 @@ module DynamoAutoscale
19
19
  def run &block
20
20
  poll(tables) do |table_name, data|
21
21
  logger.debug "[poller] Got data: #{data}"
22
- table = DynamoAutoscale.tables[table_name]
23
22
 
24
- times = data.inject([]) do |memo, (_, timeseries)|
25
- memo += timeseries.keys
26
- end.sort!.uniq!
23
+ dispatch(DynamoAutoscale.tables[table_name], data, &block)
24
+ end
25
+ end
26
+
27
+ def dispatch table, data, &block
28
+ times = data.inject([]) do |memo, (_, timeseries)|
29
+ memo += timeseries.keys
30
+ end.sort!.uniq!
27
31
 
28
- times.each do |time|
29
- datum = {
30
- provisioned_writes: data[:provisioned_writes][time],
31
- provisioned_reads: data[:provisioned_reads][time],
32
- consumed_writes: data[:consumed_writes][time],
33
- consumed_reads: data[:consumed_reads][time],
34
- }
32
+ times.each do |time|
33
+ datum = {
34
+ provisioned_writes: data[:provisioned_writes][time],
35
+ provisioned_reads: data[:provisioned_reads][time],
36
+ consumed_writes: data[:consumed_writes][time],
37
+ consumed_reads: data[:consumed_reads][time],
38
+ }
35
39
 
36
- filters.each { |filter| filter.call(table, time, datum) }
40
+ filters.each { |filter| filter.call(table, time, datum) }
37
41
 
38
- DynamoAutoscale.dispatcher.dispatch(table, time, datum, &block)
39
- end
42
+ DynamoAutoscale.dispatcher.dispatch(table, time, datum, &block)
40
43
  end
41
44
  end
42
45
  end
@@ -1,3 +1,3 @@
1
1
  module DynamoAutoscale
2
- VERSION = '0.2.9'
2
+ VERSION = '0.3'
3
3
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: dynamo-autoscale
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.2.9
4
+ version: '0.3'
5
5
  prerelease:
6
6
  platform: ruby
7
7
  authors: