20
20
* IN THE SOFTWARE.
21
21
*/
22
22
23
- import { SimpleDB } from "aws-sdk"
23
+ import { AWSError , SimpleDB } from "aws-sdk"
24
24
import { castArray , toPairs } from "lodash/fp"
25
+ import { operation , OperationOptions } from "retry"
25
26
26
27
import { IsotopeDictionary } from "../format"
27
28
@@ -34,6 +35,7 @@ import { IsotopeDictionary } from "../format"
34
35
*/
35
36
export interface IsotopeClientOptions {
36
37
consistent ?: boolean /* Whether to use consistent reads */
38
+ retry ?: OperationOptions /* Retry strategy options */
37
39
}
38
40
39
41
/**
@@ -52,15 +54,57 @@ export interface IsotopeClientItemList {
52
54
next ?: string /* Pagination token */
53
55
}
54
56
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
+
55
90
/* ----------------------------------------------------------------------------
56
91
* Values
57
92
* ------------------------------------------------------------------------- */
58
93
59
94
/**
60
95
* 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
61
99
*/
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
+ }
64
108
}
65
109
66
110
/* ----------------------------------------------------------------------------
@@ -117,6 +161,11 @@ export function mapAttributesToDictionary(
117
161
*/
118
162
export class IsotopeClient {
119
163
164
+ /**
165
+ * Isotope client options
166
+ */
167
+ protected options : Required < IsotopeClientOptions >
168
+
120
169
/**
121
170
* SimpleDB instance
122
171
*/
@@ -130,8 +179,9 @@ export class IsotopeClient {
130
179
*/
131
180
public constructor (
132
181
protected domain : string ,
133
- protected options : IsotopeClientOptions = defaultOptions
182
+ options ? : IsotopeClientOptions
134
183
) {
184
+ this . options = { ...defaultOptions , ...options }
135
185
this . simpledb = new SimpleDB ( { apiVersion : "2009-04-15" } )
136
186
}
137
187
@@ -141,9 +191,10 @@ export class IsotopeClient {
141
191
* @return Promise resolving with no result
142
192
*/
143
193
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 )
147
198
}
148
199
149
200
/**
@@ -152,9 +203,10 @@ export class IsotopeClient {
152
203
* @return Promise resolving with no result
153
204
*/
154
205
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 )
158
210
}
159
211
160
212
/**
@@ -168,12 +220,13 @@ export class IsotopeClient {
168
220
public async get (
169
221
id : string , names ?: string [ ]
170
222
) : 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 )
177
230
178
231
/* Item not found */
179
232
if ( ! Attributes )
@@ -197,11 +250,12 @@ export class IsotopeClient {
197
250
public async put (
198
251
id : string , attrs : IsotopeDictionary
199
252
) : 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 )
205
259
}
206
260
207
261
/**
@@ -215,14 +269,15 @@ export class IsotopeClient {
215
269
public async delete (
216
270
id : string , names ?: string [ ]
217
271
) : 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 )
226
281
}
227
282
228
283
/**
@@ -236,11 +291,12 @@ export class IsotopeClient {
236
291
public async select (
237
292
expr : string , next ?: string
238
293
) : 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 )
244
300
245
301
/* No items found */
246
302
if ( ! Items )
0 commit comments