Skip to content
This repository was archived by the owner on Dec 1, 2022. It is now read-only.

Commit c7c2dbb

Browse files
committed
Improve robustness of Throughput calculation, 100% flow coverage and refactoring improvements
#10 #11
1 parent 4860600 commit c7c2dbb

16 files changed

+671
-328
lines changed

.flowconfig

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,3 +9,4 @@
99
[libs]
1010

1111
[options]
12+
suppress_comment= \\(.\\|\n\\)*\\$FlowIgnore

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
"mocha": "--require resources/mocha-bootload src/**/__tests__/**/*.js"
1616
},
1717
"scripts": {
18+
"localtest": "babel-node ./scripts/localtest.js",
1819
"test": "npm run lint && npm run check",
1920
"lint": "eslint src",
2021
"check": "flow check",

scripts/localtest.js

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
/* @flow */
2+
/* eslint-disable */
3+
// $FlowIgnore
4+
import babelPolyfill from 'babel-polyfill';
5+
/* eslint-enable */
6+
import config from '../src/Config';
7+
// $FlowIgnore
8+
import dotenv from 'dotenv';
9+
import DynamoDB from '../src/DynamoDB';
10+
import CloudWatch from '../src/CloudWatch';
11+
import CapacityCalculator from '../src/CapacityCalculator';
12+
import { log } from '../src/Global';
13+
14+
// Load environment variables
15+
dotenv.config({path: 'config.env'});
16+
17+
async function localtestAsync() {
18+
let tableName = 'Contacts';
19+
20+
log('Getting table description', tableName);
21+
let db = new DynamoDB(config.connection.dynamoDB);
22+
let tableDescription = await db.describeTableAsync({TableName: tableName});
23+
24+
log('Getting table consumed capacity description', tableName);
25+
let cw = new CloudWatch(config.connection.cloudWatch);
26+
let cc = new CapacityCalculator(cw);
27+
let consumedCapacity = await cc.describeTableConsumedCapacityAsync(tableDescription.Table);
28+
29+
log(JSON.stringify({
30+
consumedCapacity
31+
}, null, 2));
32+
}
33+
34+
localtestAsync();

scripts/start.js

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,12 @@
1+
/* eslint-disable no-console */
2+
13
try {
24
var lambda = require('../dist/index.js');
35

46
process.chdir('./dist');
57

68
var context = {
7-
succeed: function(data) {
9+
succeed: data => {
810
try {
911
if (data) {
1012
console.log(JSON.stringify(data));
@@ -14,7 +16,7 @@ try {
1416
console.error(e);
1517
}
1618
},
17-
fail: function(e) {
19+
fail: e => {
1820
console.log(e.stack);
1921
console.error(e);
2022
}

src/CapacityCalculator.js

Lines changed: 142 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,142 @@
1+
/* @flow */
2+
import { json, stats, warning, invariant } from './Global';
3+
import CloudWatch from './CloudWatch';
4+
import type {
5+
TableDescription, GetMetricStatisticsResponse, Dimension,
6+
TableConsumedCapacityDescription } from './FlowTypes';
7+
8+
export default class CapacityCalculator {
9+
_cw: CloudWatch;
10+
11+
constructor(cloudWatch: CloudWatch) {
12+
invariant(typeof cloudWatch !== 'undefined', 'Parameter \'cloudWatch\' is not set');
13+
this._cw = cloudWatch;
14+
}
15+
16+
async describeTableConsumedCapacityAsync(params: TableDescription)
17+
: Promise<TableConsumedCapacityDescription> {
18+
let sw = stats
19+
.timer('DynamoDB.describeTableConsumedCapacityAsync')
20+
.start();
21+
22+
try {
23+
invariant(typeof params !== 'undefined', 'Parameter \'params\' is not set');
24+
25+
// Make all the requests concurrently
26+
let tableRead = this.getConsumedCapacityAsync(true, params.TableName, null);
27+
let tableWrite = this.getConsumedCapacityAsync(false, params.TableName, null);
28+
29+
let gsiReads = (params.GlobalSecondaryIndexes || [])
30+
.map(gsi => this.getConsumedCapacityAsync(true, params.TableName, gsi.IndexName));
31+
32+
let gsiWrites = (params.GlobalSecondaryIndexes || [])
33+
.map(gsi => this.getConsumedCapacityAsync(false, params.TableName, gsi.IndexName));
34+
35+
// Await on the results
36+
let tableConsumedRead = await tableRead;
37+
let tableConsumedWrite = await tableWrite;
38+
let gsiConsumedReads = await Promise.all(gsiReads);
39+
let gsiConsumedWrites = await Promise.all(gsiWrites);
40+
41+
// Format results
42+
let gsis = gsiConsumedReads.map((read, i) => {
43+
let write = gsiConsumedWrites[i];
44+
return {
45+
// $FlowIgnore: The indexName is not null in this case
46+
IndexName: read.globalSecondaryIndexName,
47+
ConsumedThroughput: {
48+
ReadCapacityUnits: read.value,
49+
WriteCapacityUnits: write.value
50+
}
51+
};
52+
});
53+
54+
return {
55+
TableName: params.TableName,
56+
ConsumedThroughput: {
57+
ReadCapacityUnits: tableConsumedRead.value,
58+
WriteCapacityUnits: tableConsumedWrite.value
59+
},
60+
GlobalSecondaryIndexes: gsis
61+
};
62+
} catch (ex) {
63+
warning(JSON.stringify({
64+
class: 'CapacityCalculator',
65+
function: 'describeTableConsumedCapacityAsync',
66+
params,
67+
}, null, json.padding));
68+
throw ex;
69+
} finally {
70+
sw.end();
71+
}
72+
}
73+
74+
async getConsumedCapacityAsync(
75+
isRead: boolean, tableName: string, globalSecondaryIndexName: ?string) {
76+
try {
77+
invariant(typeof isRead !== 'undefined', 'Parameter \'isRead\' is not set');
78+
invariant(typeof tableName !== 'undefined', 'Parameter \'tableName\' is not set');
79+
invariant(typeof globalSecondaryIndexName !== 'undefined',
80+
'Parameter \'globalSecondaryIndexName\' is not set');
81+
82+
// These values determine how many minutes worth of metrics
83+
let durationMinutes = 5;
84+
let periodMinutes = 1;
85+
86+
let EndTime = new Date();
87+
let StartTime = new Date();
88+
StartTime.setTime(EndTime - (60000 * durationMinutes));
89+
let MetricName = isRead ? 'ConsumedReadCapacityUnits' : 'ConsumedWriteCapacityUnits';
90+
let Dimensions = this.getDimensions(tableName, globalSecondaryIndexName);
91+
let params = {
92+
Namespace: 'AWS/DynamoDB',
93+
MetricName,
94+
Dimensions,
95+
StartTime,
96+
EndTime,
97+
Period: (periodMinutes * 60),
98+
Statistics: [ 'Average' ],
99+
Unit: 'Count'
100+
};
101+
102+
let statistics = await this._cw.getMetricStatisticsAsync(params);
103+
let value = this.getProjectedValue(statistics);
104+
return {
105+
tableName,
106+
globalSecondaryIndexName,
107+
value
108+
};
109+
} catch (ex) {
110+
warning(JSON.stringify({
111+
class: 'CapacityCalculator',
112+
function: 'getConsumedCapacityAsync',
113+
isRead, tableName, globalSecondaryIndexName,
114+
}, null, json.padding));
115+
throw ex;
116+
}
117+
}
118+
119+
getProjectedValue(data: GetMetricStatisticsResponse) {
120+
if (data.Datapoints.length === 0) {
121+
return 0;
122+
}
123+
124+
// Default algorithm for projecting a good value for the current ConsumedThroughput is:
125+
// 1. Query 5 average readings each spanning a minute
126+
// 2. Select the Max value from those 5 readings
127+
let averages = data.Datapoints.map(dp => dp.Average);
128+
let value = Math.max(...averages);
129+
return value;
130+
}
131+
132+
getDimensions(tableName: string, globalSecondaryIndexName: ?string): Dimension[] {
133+
if (globalSecondaryIndexName) {
134+
return [
135+
{ Name: 'TableName', Value: tableName},
136+
{ Name: 'GlobalSecondaryIndex', Value: globalSecondaryIndexName}
137+
];
138+
}
139+
140+
return [ { Name: 'TableName', Value: tableName} ];
141+
}
142+
}

src/CloudWatch.js

Lines changed: 12 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,23 @@
1+
/* @flow */
12
import AWS from 'aws-sdk-promise';
2-
import {
3-
json,
4-
stats,
5-
warning,
6-
invariant } from '../src/Global';
3+
import { json, stats, warning, invariant } from './Global';
4+
import type {
5+
CloudWatchOptions,
6+
GetMetricStatisticsRequest,
7+
GetMetricStatisticsResponse,
8+
} from './FlowTypes';
79

810
export default class CloudWatch {
9-
constructor(cloudWatchOptions) {
11+
_cw: AWS.CloudWatch;
12+
13+
constructor(cloudWatchOptions: CloudWatchOptions) {
1014
invariant(typeof cloudWatchOptions !== 'undefined',
1115
'Parameter \'cloudWatchOptions\' is not set');
1216
this._cw = new AWS.CloudWatch(cloudWatchOptions);
1317
}
1418

15-
async getMetricStatisticsAsync(params) {
19+
async getMetricStatisticsAsync(params: GetMetricStatisticsRequest)
20+
: Promise<GetMetricStatisticsResponse> {
1621
let sw = stats.timer('CloudWatch.getMetricStatisticsAsync').start();
1722
try {
1823
invariant(typeof params !== 'undefined',

0 commit comments

Comments
 (0)