Skip to content

Commit c975f8a

Browse files
committed
Added fault-tolerance (retryability) to all SimpleDB client requests
1 parent 53c64e4 commit c975f8a

File tree

5 files changed

+111
-36
lines changed

5 files changed

+111
-36
lines changed

CHANGELOG

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,7 @@
1+
isotopes-0.4.0 (2018-08-05)
2+
3+
* Added fault-tolerance (retryability) to all SimpleDB client requests
4+
15
isotopes-0.3.3 (2018-08-02)
26

37
* Added runtime check to Isotope.put for presence of item name

package-lock.json

Lines changed: 12 additions & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "isotopes",
3-
"version": "0.3.3",
3+
"version": "0.4.0",
44
"description": "Serverless and typed object store built on top of AWS SimpleDB",
55
"keywords": [
66
"aws",
@@ -35,6 +35,7 @@
3535
},
3636
"dependencies": {
3737
"lodash": "^4.17.10",
38+
"retry": "^0.12.0",
3839
"squel": "^5.12.2"
3940
},
4041
"devDependencies": {
@@ -43,6 +44,7 @@
4344
"@types/jasmine": "^2.8.6",
4445
"@types/lodash": "^4.14.113",
4546
"@types/node": "^9.6.2",
47+
"@types/retry": "^0.10.2",
4648
"aws-sdk": "^2.279.1",
4749
"aws-sdk-mock": "^2.0.0",
4850
"chance": "^1.0.13",

src/isotopes/client/index.ts

Lines changed: 90 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -20,8 +20,9 @@
2020
* IN THE SOFTWARE.
2121
*/
2222

23-
import { SimpleDB } from "aws-sdk"
23+
import { AWSError, SimpleDB } from "aws-sdk"
2424
import { castArray, toPairs } from "lodash/fp"
25+
import { operation, OperationOptions } from "retry"
2526

2627
import { IsotopeDictionary } from "../format"
2728

@@ -34,6 +35,7 @@ import { IsotopeDictionary } from "../format"
3435
*/
3536
export interface IsotopeClientOptions {
3637
consistent?: boolean /* Whether to use consistent reads */
38+
retry?: OperationOptions /* Retry strategy options */
3739
}
3840

3941
/**
@@ -52,15 +54,57 @@ export interface IsotopeClientItemList {
5254
next?: string /* Pagination token */
5355
}
5456

57+
/* ----------------------------------------------------------------------------
58+
* Functions
59+
* ------------------------------------------------------------------------- */
60+
61+
/**
62+
* Make a function returning a Promise retryable
63+
*
64+
* Only 5xx errors need to be retried as we don't need to retry client errors,
65+
* so fail immediately if the status code is below 500.
66+
*
67+
* @param action - Function returning a Promise
68+
* @param options - Retry strategy options
69+
*
70+
* @return Promise resolving with the original Promise's result
71+
*/
72+
export function retryable<T>(
73+
action: () => Promise<T>,
74+
options: OperationOptions
75+
): Promise<T> {
76+
return new Promise((resolve, reject) => {
77+
const op = operation(options)
78+
op.attempt(async () => {
79+
try {
80+
resolve(await action())
81+
} catch (err) {
82+
const { statusCode } = err as AWSError
83+
if (!statusCode || statusCode < 500 || !op.retry(err))
84+
reject(err)
85+
}
86+
})
87+
})
88+
}
89+
5590
/* ----------------------------------------------------------------------------
5691
* Values
5792
* ------------------------------------------------------------------------- */
5893

5994
/**
6095
* Default client options
96+
*
97+
* We're not using the exponential backoff strategy (as recommended) due to the
98+
* observations made in this article: https://bit.ly/2AJQiNV
6199
*/
62-
const defaultOptions: IsotopeClientOptions = {
63-
consistent: false
100+
const defaultOptions: Required<IsotopeClientOptions> = {
101+
consistent: false,
102+
retry: {
103+
minTimeout: 100,
104+
maxTimeout: 250,
105+
retries: 3,
106+
factor: 1
107+
}
64108
}
65109

66110
/* ----------------------------------------------------------------------------
@@ -117,6 +161,11 @@ export function mapAttributesToDictionary(
117161
*/
118162
export class IsotopeClient {
119163

164+
/**
165+
* Isotope client options
166+
*/
167+
protected options: Required<IsotopeClientOptions>
168+
120169
/**
121170
* SimpleDB instance
122171
*/
@@ -130,8 +179,9 @@ export class IsotopeClient {
130179
*/
131180
public constructor(
132181
protected domain: string,
133-
protected options: IsotopeClientOptions = defaultOptions
182+
options?: IsotopeClientOptions
134183
) {
184+
this.options = { ...defaultOptions, ...options }
135185
this.simpledb = new SimpleDB({ apiVersion: "2009-04-15" })
136186
}
137187

@@ -141,9 +191,10 @@ export class IsotopeClient {
141191
* @return Promise resolving with no result
142192
*/
143193
public async create(): Promise<void> {
144-
await this.simpledb.createDomain({
145-
DomainName: this.domain
146-
}).promise()
194+
await retryable(() =>
195+
this.simpledb.createDomain({
196+
DomainName: this.domain
197+
}).promise(), this.options.retry)
147198
}
148199

149200
/**
@@ -152,9 +203,10 @@ export class IsotopeClient {
152203
* @return Promise resolving with no result
153204
*/
154205
public async destroy(): Promise<void> {
155-
await this.simpledb.deleteDomain({
156-
DomainName: this.domain
157-
}).promise()
206+
await retryable(() =>
207+
this.simpledb.deleteDomain({
208+
DomainName: this.domain
209+
}).promise(), this.options.retry)
158210
}
159211

160212
/**
@@ -168,12 +220,13 @@ export class IsotopeClient {
168220
public async get(
169221
id: string, names?: string[]
170222
): Promise<IsotopeClientItem | undefined> {
171-
const { Attributes } = await this.simpledb.getAttributes({
172-
DomainName: this.domain,
173-
ItemName: id,
174-
AttributeNames: names,
175-
ConsistentRead: this.options.consistent
176-
}).promise()
223+
const { Attributes } = await retryable(() =>
224+
this.simpledb.getAttributes({
225+
DomainName: this.domain,
226+
ItemName: id,
227+
AttributeNames: names,
228+
ConsistentRead: this.options.consistent
229+
}).promise(), this.options.retry)
177230

178231
/* Item not found */
179232
if (!Attributes)
@@ -197,11 +250,12 @@ export class IsotopeClient {
197250
public async put(
198251
id: string, attrs: IsotopeDictionary
199252
): Promise<void> {
200-
await this.simpledb.putAttributes({
201-
DomainName: this.domain,
202-
ItemName: id,
203-
Attributes: mapDictionaryToAttributes(attrs)
204-
}).promise()
253+
await retryable(() =>
254+
this.simpledb.putAttributes({
255+
DomainName: this.domain,
256+
ItemName: id,
257+
Attributes: mapDictionaryToAttributes(attrs)
258+
}).promise(), this.options.retry)
205259
}
206260

207261
/**
@@ -215,14 +269,15 @@ export class IsotopeClient {
215269
public async delete(
216270
id: string, names?: string[]
217271
): Promise<void> {
218-
await this.simpledb.deleteAttributes({
219-
DomainName: this.domain,
220-
ItemName: id,
221-
Attributes: (names || [])
222-
.map<SimpleDB.DeletableAttribute>(name => ({
223-
Name: name
224-
}))
225-
}).promise()
272+
await retryable(() =>
273+
this.simpledb.deleteAttributes({
274+
DomainName: this.domain,
275+
ItemName: id,
276+
Attributes: (names || [])
277+
.map<SimpleDB.DeletableAttribute>(name => ({
278+
Name: name
279+
}))
280+
}).promise(), this.options.retry)
226281
}
227282

228283
/**
@@ -236,11 +291,12 @@ export class IsotopeClient {
236291
public async select(
237292
expr: string, next?: string
238293
): Promise<IsotopeClientItemList> {
239-
const { Items, NextToken } = await this.simpledb.select({
240-
SelectExpression: expr,
241-
NextToken: next,
242-
ConsistentRead: this.options.consistent
243-
}).promise()
294+
const { Items, NextToken } = await retryable(() =>
295+
this.simpledb.select({
296+
SelectExpression: expr,
297+
NextToken: next,
298+
ConsistentRead: this.options.consistent
299+
}).promise(), this.options.retry)
244300

245301
/* No items found */
246302
if (!Items)

tests/tsconfig.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
{
22
"extends": "../tsconfig.json",
33
"compilerOptions": {
4+
"noUnusedLocals": false,
5+
"noUnusedParameters": false,
46
"paths": {
57
"_/*": ["../tests/*"]
68
},

0 commit comments

Comments
 (0)