@salto-io/persistent-pool 0.3.40-main.5ef2e84db → 0.3.40-publishOnlyDist.57b77fb79
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.
- package/package.json +7 -8
- package/dist/test/lib/dynamodb/dynamodb_repo.e2e.test.d.ts +0 -2
- package/dist/test/lib/dynamodb/dynamodb_repo.e2e.test.d.ts.map +0 -1
- package/dist/test/lib/dynamodb/dynamodb_repo.e2e.test.js +0 -98
- package/dist/test/lib/dynamodb/dynamodb_repo.e2e.test.js.map +0 -1
- package/dist/test/lib/dynamodb/dynamodb_repo.test.d.ts +0 -2
- package/dist/test/lib/dynamodb/dynamodb_repo.test.d.ts.map +0 -1
- package/dist/test/lib/dynamodb/dynamodb_repo.test.js +0 -533
- package/dist/test/lib/dynamodb/dynamodb_repo.test.js.map +0 -1
- package/dist/test/lib/dynamodb/environment/index.d.ts +0 -9
- package/dist/test/lib/dynamodb/environment/index.d.ts.map +0 -1
- package/dist/test/lib/dynamodb/environment/index.js +0 -70
- package/dist/test/lib/dynamodb/environment/index.js.map +0 -1
- package/dist/test/lib/dynamodb/environment/types.d.ts +0 -7
- package/dist/test/lib/dynamodb/environment/types.d.ts.map +0 -1
- package/dist/test/lib/dynamodb/environment/types.js +0 -3
- package/dist/test/lib/dynamodb/environment/types.js.map +0 -1
- package/dist/test/lib/dynamodb/utils.d.ts +0 -12
- package/dist/test/lib/dynamodb/utils.d.ts.map +0 -1
- package/dist/test/lib/dynamodb/utils.js +0 -53
- package/dist/test/lib/dynamodb/utils.js.map +0 -1
- package/dist/test/lib/renewed_lease.test.d.ts +0 -2
- package/dist/test/lib/renewed_lease.test.d.ts.map +0 -1
- package/dist/test/lib/renewed_lease.test.js +0 -134
- package/dist/test/lib/renewed_lease.test.js.map +0 -1
- package/dist/test/mock_repo.d.ts +0 -8
- package/dist/test/mock_repo.d.ts.map +0 -1
- package/dist/test/mock_repo.js +0 -22
- package/dist/test/mock_repo.js.map +0 -1
- package/dist/test/types.d.ts +0 -7
- package/dist/test/types.d.ts.map +0 -1
- package/dist/test/types.js +0 -9
- package/dist/test/types.js.map +0 -1
- package/dist/test/utils/async_to_array.d.ts +0 -3
- package/dist/test/utils/async_to_array.d.ts.map +0 -1
- package/dist/test/utils/async_to_array.js +0 -32
- package/dist/test/utils/async_to_array.js.map +0 -1
- package/dist/test/utils/promise_series.d.ts +0 -3
- package/dist/test/utils/promise_series.d.ts.map +0 -1
- package/dist/test/utils/promise_series.js +0 -28
- package/dist/test/utils/promise_series.js.map +0 -1
- package/dist/test/utils/random_string.d.ts +0 -6
- package/dist/test/utils/random_string.d.ts.map +0 -1
- package/dist/test/utils/random_string.js +0 -22
- package/dist/test/utils/random_string.js.map +0 -1
- package/dist/test/utils/repeat.d.ts +0 -3
- package/dist/test/utils/repeat.d.ts.map +0 -1
- package/dist/test/utils/repeat.js +0 -20
- package/dist/test/utils/repeat.js.map +0 -1
- package/dist/test/utils/timing.d.ts +0 -10
- package/dist/test/utils/timing.d.ts.map +0 -1
- package/dist/test/utils/timing.js +0 -39
- package/dist/test/utils/timing.js.map +0 -1
- package/dist/tsconfig.tsbuildinfo +0 -14427
- package/src/index.ts +0 -21
- package/src/lib/dynamodb/dynamodb_repo.ts +0 -487
- package/src/lib/dynamodb/utils.ts +0 -168
- package/src/lib/renewed_lease.ts +0 -83
- package/src/types.ts +0 -106
package/src/index.ts
DELETED
|
@@ -1,21 +0,0 @@
|
|
|
1
|
-
/*
|
|
2
|
-
* Copyright 2023 Salto Labs Ltd.
|
|
3
|
-
*
|
|
4
|
-
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
5
|
-
* you may not use this file except in compliance with
|
|
6
|
-
* the License. You may obtain a copy of the License at
|
|
7
|
-
*
|
|
8
|
-
* http://www.apache.org/licenses/LICENSE-2.0
|
|
9
|
-
*
|
|
10
|
-
* Unless required by applicable law or agreed to in writing, software
|
|
11
|
-
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
12
|
-
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
13
|
-
* See the License for the specific language governing permissions and
|
|
14
|
-
* limitations under the License.
|
|
15
|
-
*/
|
|
16
|
-
export {
|
|
17
|
-
repo as dynamoDbRepo,
|
|
18
|
-
DynamoRepoOpts as DynamoDbRepoOpts,
|
|
19
|
-
} from './lib/dynamodb/dynamodb_repo'
|
|
20
|
-
export * from './types'
|
|
21
|
-
export { default as RenewedLease, RenewedLeaseOpts } from './lib/renewed_lease'
|
|
@@ -1,487 +0,0 @@
|
|
|
1
|
-
/*
|
|
2
|
-
* Copyright 2023 Salto Labs Ltd.
|
|
3
|
-
*
|
|
4
|
-
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
5
|
-
* you may not use this file except in compliance with
|
|
6
|
-
* the License. You may obtain a copy of the License at
|
|
7
|
-
*
|
|
8
|
-
* http://www.apache.org/licenses/LICENSE-2.0
|
|
9
|
-
*
|
|
10
|
-
* Unless required by applicable law or agreed to in writing, software
|
|
11
|
-
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
12
|
-
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
13
|
-
* See the License for the specific language governing permissions and
|
|
14
|
-
* limitations under the License.
|
|
15
|
-
*/
|
|
16
|
-
import {
|
|
17
|
-
BillingMode,
|
|
18
|
-
CreateTableCommandInput,
|
|
19
|
-
DynamoDBClient,
|
|
20
|
-
DynamoDBClientConfig,
|
|
21
|
-
} from '@aws-sdk/client-dynamodb'
|
|
22
|
-
import {
|
|
23
|
-
DeleteCommand,
|
|
24
|
-
DynamoDBDocumentClient,
|
|
25
|
-
GetCommand,
|
|
26
|
-
PutCommand,
|
|
27
|
-
QueryCommand,
|
|
28
|
-
QueryCommandInput,
|
|
29
|
-
UpdateCommand,
|
|
30
|
-
UpdateCommandInput,
|
|
31
|
-
} from '@aws-sdk/lib-dynamodb'
|
|
32
|
-
|
|
33
|
-
import { functions, retry as retryUtil } from '@salto-io/lowerdash'
|
|
34
|
-
import { v4 as uuidv4 } from 'uuid'
|
|
35
|
-
import {
|
|
36
|
-
InstanceId, Pool, Repo, LeaseWithStatus, Lease,
|
|
37
|
-
InstanceNotFoundError, InstanceNotLeasedError, InstanceIdAlreadyRegistered, RetryStrategy,
|
|
38
|
-
RepoOpts,
|
|
39
|
-
LeaseUpdateOpts,
|
|
40
|
-
DEFAULT_LEASE_UPDATE_OPTS,
|
|
41
|
-
} from '../../types'
|
|
42
|
-
import { Key, dbUtils, dbDocUtils, BatchDeleteOpts, QueryOpts } from './utils'
|
|
43
|
-
|
|
44
|
-
const { defaultOpts } = functions
|
|
45
|
-
const { withRetry, retryStrategies, RetryError } = retryUtil
|
|
46
|
-
|
|
47
|
-
type ItemList = Key[]
|
|
48
|
-
|
|
49
|
-
export type DynamoRepoPartialOpts = {
|
|
50
|
-
tableName: string
|
|
51
|
-
listBatchSize: number | undefined
|
|
52
|
-
billingMode: BillingMode
|
|
53
|
-
optimisticLockMaxRetries: number
|
|
54
|
-
leaseRandomizationRange: number | undefined
|
|
55
|
-
batchDelete: BatchDeleteOpts
|
|
56
|
-
query: QueryOpts
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
export type DynamoDbInstances = {
|
|
60
|
-
db: DynamoDBClient
|
|
61
|
-
dbDoc: DynamoDBDocumentClient
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
export type DynamoInstancesOrConfig = DynamoDbInstances | {
|
|
65
|
-
serviceOpts: DynamoDBClientConfig
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
export type DynamoRepoRequiredOpts = RepoOpts & DynamoInstancesOrConfig
|
|
69
|
-
|
|
70
|
-
export type DynamoRepoOpts = DynamoRepoPartialOpts & DynamoRepoRequiredOpts
|
|
71
|
-
|
|
72
|
-
// taken from: https://www.typescriptlang.org/docs/handbook/advanced-types.html#type-inference-in-conditional-types
|
|
73
|
-
type Unpacked<T> =
|
|
74
|
-
T extends (infer U)[] ? U :
|
|
75
|
-
T extends (...args: unknown[]) => infer U ? U :
|
|
76
|
-
T extends PromiseLike<infer U> ? U :
|
|
77
|
-
T;
|
|
78
|
-
|
|
79
|
-
const createTableParams = (
|
|
80
|
-
{ tableName, billingMode }: Pick<DynamoRepoOpts, 'tableName' | 'billingMode'>,
|
|
81
|
-
): CreateTableCommandInput & { TableName: string } => ({
|
|
82
|
-
KeySchema: [
|
|
83
|
-
{ AttributeName: 'type', KeyType: 'HASH' },
|
|
84
|
-
{ AttributeName: 'id', KeyType: 'RANGE' },
|
|
85
|
-
],
|
|
86
|
-
AttributeDefinitions: [
|
|
87
|
-
{ AttributeName: 'type', AttributeType: 'S' },
|
|
88
|
-
{ AttributeName: 'id', AttributeType: 'S' },
|
|
89
|
-
{ AttributeName: 'leaseExpiresBy', AttributeType: 'S' },
|
|
90
|
-
],
|
|
91
|
-
LocalSecondaryIndexes: [
|
|
92
|
-
{
|
|
93
|
-
IndexName: 'leaseExpiresBy',
|
|
94
|
-
KeySchema: [
|
|
95
|
-
{ AttributeName: 'type', KeyType: 'HASH' },
|
|
96
|
-
{ AttributeName: 'leaseExpiresBy', KeyType: 'RANGE' },
|
|
97
|
-
],
|
|
98
|
-
Projection: {
|
|
99
|
-
ProjectionType: 'INCLUDE',
|
|
100
|
-
NonKeyAttributes: ['version'],
|
|
101
|
-
},
|
|
102
|
-
},
|
|
103
|
-
],
|
|
104
|
-
BillingMode: billingMode,
|
|
105
|
-
TableName: tableName,
|
|
106
|
-
})
|
|
107
|
-
|
|
108
|
-
type LeaseDoc<T> = {
|
|
109
|
-
id: string
|
|
110
|
-
value: T
|
|
111
|
-
version: number
|
|
112
|
-
leaseExpiresBy: string
|
|
113
|
-
leasingClientId: string
|
|
114
|
-
suspensionReason?: string
|
|
115
|
-
}
|
|
116
|
-
|
|
117
|
-
export const dynamoDbInstances = (opts: DynamoInstancesOrConfig): DynamoDbInstances => {
|
|
118
|
-
if ('serviceOpts' in opts) {
|
|
119
|
-
const db = new DynamoDBClient(opts.serviceOpts)
|
|
120
|
-
const dbDoc = DynamoDBDocumentClient.from(db)
|
|
121
|
-
return { db, dbDoc }
|
|
122
|
-
}
|
|
123
|
-
return { db: opts.db, dbDoc: opts.dbDoc }
|
|
124
|
-
}
|
|
125
|
-
|
|
126
|
-
const NOT_LEASED = (new Date(0)).toISOString()
|
|
127
|
-
|
|
128
|
-
export const repo = defaultOpts.withRequired<
|
|
129
|
-
DynamoRepoPartialOpts,
|
|
130
|
-
DynamoRepoRequiredOpts,
|
|
131
|
-
Promise<Repo>
|
|
132
|
-
>(async (opts: DynamoRepoOpts): Promise<Repo> => {
|
|
133
|
-
const { db, dbDoc } = dynamoDbInstances(opts)
|
|
134
|
-
const utils = dbUtils(db)
|
|
135
|
-
const docUtils = dbDocUtils(dbDoc)
|
|
136
|
-
|
|
137
|
-
const { clientId } = opts
|
|
138
|
-
|
|
139
|
-
const withOptimisticLock = async <TResult>(
|
|
140
|
-
f: () => Promise<TResult>,
|
|
141
|
-
retryNumber = 0,
|
|
142
|
-
): Promise<TResult> => {
|
|
143
|
-
try {
|
|
144
|
-
return await f()
|
|
145
|
-
} catch (e) {
|
|
146
|
-
if (e.toString().includes('ConditionalCheckFailedException')) {
|
|
147
|
-
if (retryNumber === opts.optimisticLockMaxRetries) {
|
|
148
|
-
throw e
|
|
149
|
-
}
|
|
150
|
-
return withOptimisticLock(f, retryNumber + 1)
|
|
151
|
-
}
|
|
152
|
-
throw e
|
|
153
|
-
}
|
|
154
|
-
}
|
|
155
|
-
|
|
156
|
-
const pool = <T>(typeName: string): Pool<T> => {
|
|
157
|
-
const queryInput = (
|
|
158
|
-
partialQueryInput: Partial<QueryCommandInput> = {},
|
|
159
|
-
): QueryCommandInput => ({
|
|
160
|
-
TableName: opts.tableName,
|
|
161
|
-
ConsistentRead: true,
|
|
162
|
-
Select: 'ALL_ATTRIBUTES',
|
|
163
|
-
KeyConditionExpression: '#type = :type',
|
|
164
|
-
ExpressionAttributeValues: {
|
|
165
|
-
':type': typeName,
|
|
166
|
-
},
|
|
167
|
-
ExpressionAttributeNames: {
|
|
168
|
-
'#type': 'type',
|
|
169
|
-
},
|
|
170
|
-
Limit: opts.listBatchSize,
|
|
171
|
-
...partialQueryInput,
|
|
172
|
-
})
|
|
173
|
-
|
|
174
|
-
const lease = (
|
|
175
|
-
returnTimeout: number,
|
|
176
|
-
retryStrategy?: () => RetryStrategy,
|
|
177
|
-
): Promise<Lease<T> | null> => withOptimisticLock(async (): Promise<Lease<T> | null> => {
|
|
178
|
-
const now = Date.now()
|
|
179
|
-
const retry = retryStrategy ?? retryStrategies.none()
|
|
180
|
-
|
|
181
|
-
const makeQuery = async (): Promise<ItemList | undefined> => {
|
|
182
|
-
const queryResults = await dbDoc.send(new QueryCommand({
|
|
183
|
-
TableName: opts.tableName,
|
|
184
|
-
IndexName: 'leaseExpiresBy',
|
|
185
|
-
ConsistentRead: true,
|
|
186
|
-
Select: 'ALL_PROJECTED_ATTRIBUTES',
|
|
187
|
-
KeyConditionExpression: '#type = :type and leaseExpiresBy <= :now',
|
|
188
|
-
ExpressionAttributeNames: {
|
|
189
|
-
'#type': 'type',
|
|
190
|
-
},
|
|
191
|
-
ExpressionAttributeValues: {
|
|
192
|
-
':type': typeName,
|
|
193
|
-
':now': (new Date(now)).toISOString(),
|
|
194
|
-
},
|
|
195
|
-
Limit: opts.leaseRandomizationRange,
|
|
196
|
-
}))
|
|
197
|
-
|
|
198
|
-
const maybeItems = queryResults.Items
|
|
199
|
-
const hasItems = maybeItems !== undefined && maybeItems.length !== 0
|
|
200
|
-
return hasItems ? maybeItems as ItemList : undefined
|
|
201
|
-
}
|
|
202
|
-
|
|
203
|
-
let items: ItemList
|
|
204
|
-
try {
|
|
205
|
-
items = await withRetry(makeQuery, {
|
|
206
|
-
strategy: retry,
|
|
207
|
-
description: 'waiting for available lease',
|
|
208
|
-
}) as ItemList
|
|
209
|
-
} catch (e) {
|
|
210
|
-
if (e instanceof RetryError && retryStrategy === undefined) {
|
|
211
|
-
return null
|
|
212
|
-
}
|
|
213
|
-
throw e
|
|
214
|
-
}
|
|
215
|
-
|
|
216
|
-
const itemIndex = Math.floor(Math.random() * items.length)
|
|
217
|
-
const { id, version } = items[itemIndex] as Pick<LeaseDoc<T>, 'id' | 'version'>
|
|
218
|
-
|
|
219
|
-
const updateResult = await dbDoc.send(new UpdateCommand({
|
|
220
|
-
TableName: opts.tableName,
|
|
221
|
-
Key: {
|
|
222
|
-
type: typeName,
|
|
223
|
-
id,
|
|
224
|
-
},
|
|
225
|
-
UpdateExpression: 'set leaseExpiresBy = :leaseExpiresBy, leasingClientId = :leasingClientId, #version = :newVersion',
|
|
226
|
-
ConditionExpression: '#version = :currentVersion',
|
|
227
|
-
ExpressionAttributeNames: {
|
|
228
|
-
'#version': 'version',
|
|
229
|
-
},
|
|
230
|
-
ExpressionAttributeValues: {
|
|
231
|
-
':leasingClientId': clientId,
|
|
232
|
-
':currentVersion': version,
|
|
233
|
-
':newVersion': version + 1,
|
|
234
|
-
':leaseExpiresBy': new Date(now + returnTimeout).toISOString(),
|
|
235
|
-
},
|
|
236
|
-
ReturnValues: 'ALL_NEW',
|
|
237
|
-
}))
|
|
238
|
-
|
|
239
|
-
return { id, value: (updateResult.Attributes as LeaseDoc<T>).value }
|
|
240
|
-
})
|
|
241
|
-
|
|
242
|
-
const updateLease = (
|
|
243
|
-
id: InstanceId,
|
|
244
|
-
updates: Pick<UpdateCommandInput, 'UpdateExpression' | 'ExpressionAttributeValues'>,
|
|
245
|
-
updateOpts?: Partial<LeaseUpdateOpts> & { validateLeased: boolean },
|
|
246
|
-
): Promise<void> => {
|
|
247
|
-
const { validateClientId, validateLeased } = {
|
|
248
|
-
...DEFAULT_LEASE_UPDATE_OPTS,
|
|
249
|
-
...updateOpts,
|
|
250
|
-
}
|
|
251
|
-
|
|
252
|
-
return withOptimisticLock(async (): Promise<void> => {
|
|
253
|
-
const item = (await dbDoc.send(new GetCommand({
|
|
254
|
-
TableName: opts.tableName,
|
|
255
|
-
Key: {
|
|
256
|
-
type: typeName,
|
|
257
|
-
id,
|
|
258
|
-
},
|
|
259
|
-
}))).Item as LeaseDoc<T> | undefined
|
|
260
|
-
|
|
261
|
-
if (item === undefined) {
|
|
262
|
-
throw new InstanceNotFoundError({ id, typeName })
|
|
263
|
-
}
|
|
264
|
-
|
|
265
|
-
if (
|
|
266
|
-
(validateLeased && item.leaseExpiresBy === NOT_LEASED)
|
|
267
|
-
|| (validateClientId && item.leasingClientId !== clientId)
|
|
268
|
-
) {
|
|
269
|
-
throw new InstanceNotLeasedError({ id, typeName, clientId })
|
|
270
|
-
}
|
|
271
|
-
|
|
272
|
-
const { version } = item
|
|
273
|
-
|
|
274
|
-
await dbDoc.send(new UpdateCommand({
|
|
275
|
-
TableName: opts.tableName,
|
|
276
|
-
Key: {
|
|
277
|
-
type: typeName,
|
|
278
|
-
id,
|
|
279
|
-
},
|
|
280
|
-
UpdateExpression: updates.UpdateExpression,
|
|
281
|
-
ConditionExpression: '#version = :currentVersion',
|
|
282
|
-
ExpressionAttributeNames: {
|
|
283
|
-
'#version': 'version',
|
|
284
|
-
},
|
|
285
|
-
ExpressionAttributeValues: {
|
|
286
|
-
':currentVersion': version,
|
|
287
|
-
':newVersion': version + 1,
|
|
288
|
-
...updates.ExpressionAttributeValues,
|
|
289
|
-
},
|
|
290
|
-
}))
|
|
291
|
-
})
|
|
292
|
-
}
|
|
293
|
-
|
|
294
|
-
return {
|
|
295
|
-
async register(value: T, id: InstanceId = uuidv4()): Promise<InstanceId> {
|
|
296
|
-
try {
|
|
297
|
-
await dbDoc.send(new PutCommand({
|
|
298
|
-
TableName: opts.tableName,
|
|
299
|
-
Item: {
|
|
300
|
-
type: typeName,
|
|
301
|
-
id,
|
|
302
|
-
version: 0,
|
|
303
|
-
value,
|
|
304
|
-
leaseExpiresBy: NOT_LEASED,
|
|
305
|
-
},
|
|
306
|
-
ConditionExpression: 'attribute_not_exists(id)',
|
|
307
|
-
}))
|
|
308
|
-
return id
|
|
309
|
-
} catch (e) {
|
|
310
|
-
if (e.toString().includes('ConditionalCheckFailedException')) {
|
|
311
|
-
throw new InstanceIdAlreadyRegistered({ id, typeName })
|
|
312
|
-
}
|
|
313
|
-
throw e
|
|
314
|
-
}
|
|
315
|
-
},
|
|
316
|
-
|
|
317
|
-
async unregister(id: InstanceId): Promise<void> {
|
|
318
|
-
try {
|
|
319
|
-
await dbDoc.send(new DeleteCommand({
|
|
320
|
-
TableName: opts.tableName,
|
|
321
|
-
Key: {
|
|
322
|
-
type: typeName,
|
|
323
|
-
id,
|
|
324
|
-
},
|
|
325
|
-
ConditionExpression: 'attribute_exists(id)',
|
|
326
|
-
}))
|
|
327
|
-
} catch (e) {
|
|
328
|
-
if (e.toString().includes('ConditionalCheckFailedException')) {
|
|
329
|
-
throw new InstanceNotFoundError({ id, typeName })
|
|
330
|
-
}
|
|
331
|
-
throw e
|
|
332
|
-
}
|
|
333
|
-
},
|
|
334
|
-
|
|
335
|
-
async suspend(
|
|
336
|
-
id: InstanceId,
|
|
337
|
-
reason: string,
|
|
338
|
-
timeout: number,
|
|
339
|
-
suspendOpts?: Partial<LeaseUpdateOpts>,
|
|
340
|
-
): Promise<void> {
|
|
341
|
-
const now = Date.now()
|
|
342
|
-
const leaseExpiresBy = new Date(now + timeout).toISOString()
|
|
343
|
-
|
|
344
|
-
return updateLease(
|
|
345
|
-
id,
|
|
346
|
-
{
|
|
347
|
-
UpdateExpression: 'set #version = :newVersion, leaseExpiresBy = :leaseExpiresBy, suspensionReason = :suspensionReason',
|
|
348
|
-
ExpressionAttributeValues: {
|
|
349
|
-
':leaseExpiresBy': leaseExpiresBy,
|
|
350
|
-
':suspensionReason': reason,
|
|
351
|
-
},
|
|
352
|
-
},
|
|
353
|
-
{ ...suspendOpts, validateLeased: false },
|
|
354
|
-
)
|
|
355
|
-
},
|
|
356
|
-
|
|
357
|
-
async lease(returnTimeout: number): Promise<Lease<T> | null> {
|
|
358
|
-
return lease(returnTimeout)
|
|
359
|
-
},
|
|
360
|
-
|
|
361
|
-
async waitForLease(
|
|
362
|
-
returnTimeout: number,
|
|
363
|
-
retryStrategy: () => RetryStrategy,
|
|
364
|
-
): Promise<Lease<T>> {
|
|
365
|
-
return lease(returnTimeout, retryStrategy) as Promise<Lease<T>>
|
|
366
|
-
},
|
|
367
|
-
|
|
368
|
-
updateTimeout(
|
|
369
|
-
id: InstanceId,
|
|
370
|
-
newTimeout: number,
|
|
371
|
-
extendOpts?: Partial<LeaseUpdateOpts>,
|
|
372
|
-
): Promise<void> {
|
|
373
|
-
const now = Date.now()
|
|
374
|
-
const leaseExpiresBy = new Date(now + newTimeout).toISOString()
|
|
375
|
-
return updateLease(
|
|
376
|
-
id,
|
|
377
|
-
{
|
|
378
|
-
UpdateExpression: 'set #version = :newVersion, leaseExpiresBy = :leaseExpiresBy',
|
|
379
|
-
ExpressionAttributeValues: { ':leaseExpiresBy': leaseExpiresBy },
|
|
380
|
-
},
|
|
381
|
-
{ ...extendOpts, validateLeased: true },
|
|
382
|
-
)
|
|
383
|
-
},
|
|
384
|
-
|
|
385
|
-
return(
|
|
386
|
-
id: InstanceId,
|
|
387
|
-
returnOpts?: Partial<LeaseUpdateOpts>,
|
|
388
|
-
): Promise<void> {
|
|
389
|
-
return updateLease(
|
|
390
|
-
id,
|
|
391
|
-
{
|
|
392
|
-
UpdateExpression: 'set #version = :newVersion, leaseExpiresBy = :NOT_LEASED remove leasingClientId, supensionReason',
|
|
393
|
-
ExpressionAttributeValues: { ':NOT_LEASED': NOT_LEASED },
|
|
394
|
-
},
|
|
395
|
-
{ ...returnOpts, validateLeased: true },
|
|
396
|
-
)
|
|
397
|
-
},
|
|
398
|
-
|
|
399
|
-
[Symbol.asyncIterator](): AsyncIterator<LeaseWithStatus<T>> {
|
|
400
|
-
const now = Date.now()
|
|
401
|
-
|
|
402
|
-
const toLeaseWithStatus = (d: LeaseDoc<T>): LeaseWithStatus<T> => {
|
|
403
|
-
const leaseExpiresBy = Date.parse(d.leaseExpiresBy)
|
|
404
|
-
const isLeased = leaseExpiresBy > now
|
|
405
|
-
|
|
406
|
-
const base = {
|
|
407
|
-
id: d.id,
|
|
408
|
-
value: d.value,
|
|
409
|
-
}
|
|
410
|
-
|
|
411
|
-
if (isLeased) {
|
|
412
|
-
const unavailableBase = {
|
|
413
|
-
...base,
|
|
414
|
-
leaseExpiresBy: new Date(leaseExpiresBy),
|
|
415
|
-
clientId: d.leasingClientId,
|
|
416
|
-
}
|
|
417
|
-
return d.suspensionReason !== undefined
|
|
418
|
-
? { status: 'suspended', ...unavailableBase, suspensionReason: d.suspensionReason }
|
|
419
|
-
: { status: 'leased', ...unavailableBase }
|
|
420
|
-
}
|
|
421
|
-
|
|
422
|
-
return { status: 'available', ...base }
|
|
423
|
-
}
|
|
424
|
-
|
|
425
|
-
const iter = docUtils.queryIterator<LeaseDoc<T>>(queryInput(), opts.query)
|
|
426
|
-
|
|
427
|
-
return {
|
|
428
|
-
async next(): Promise<IteratorResult<LeaseWithStatus<T>>> {
|
|
429
|
-
const { done, value } = await iter.next()
|
|
430
|
-
return {
|
|
431
|
-
done,
|
|
432
|
-
value: value && toLeaseWithStatus(value),
|
|
433
|
-
} as IteratorResult<LeaseWithStatus<T>>
|
|
434
|
-
},
|
|
435
|
-
}
|
|
436
|
-
},
|
|
437
|
-
|
|
438
|
-
async clear(): Promise<void> {
|
|
439
|
-
type QueryResult = { id: InstanceId }
|
|
440
|
-
const batchIter = docUtils.queryBatchIterator<QueryResult>(queryInput({
|
|
441
|
-
Select: 'SPECIFIC_ATTRIBUTES',
|
|
442
|
-
ProjectionExpression: 'id',
|
|
443
|
-
}), opts.query)
|
|
444
|
-
|
|
445
|
-
const keysToDelete: Key[] = []
|
|
446
|
-
|
|
447
|
-
let batch: Unpacked<ReturnType<typeof batchIter.next>>
|
|
448
|
-
|
|
449
|
-
const nextBatch = async (): Promise<void> => {
|
|
450
|
-
if (!batch || keysToDelete.length === 0) {
|
|
451
|
-
if (batch?.done) {
|
|
452
|
-
return undefined
|
|
453
|
-
}
|
|
454
|
-
batch = await batchIter.next()
|
|
455
|
-
if (batch.value) {
|
|
456
|
-
keysToDelete.push(
|
|
457
|
-
...batch.value.map(r => ({ type: typeName, id: r.id } as Key))
|
|
458
|
-
)
|
|
459
|
-
}
|
|
460
|
-
return nextBatch()
|
|
461
|
-
}
|
|
462
|
-
|
|
463
|
-
await docUtils.batchDelete(opts.tableName, keysToDelete, opts.batchDelete)
|
|
464
|
-
return nextBatch()
|
|
465
|
-
}
|
|
466
|
-
|
|
467
|
-
return nextBatch()
|
|
468
|
-
},
|
|
469
|
-
}
|
|
470
|
-
}
|
|
471
|
-
|
|
472
|
-
await utils.ensureTableExists(createTableParams(opts))
|
|
473
|
-
|
|
474
|
-
return {
|
|
475
|
-
pool<T>(typeName: string): Promise<Pool<T>> {
|
|
476
|
-
return Promise.resolve(pool<T>(typeName))
|
|
477
|
-
},
|
|
478
|
-
}
|
|
479
|
-
}, {
|
|
480
|
-
tableName: 'persistent_pool',
|
|
481
|
-
listBatchSize: undefined,
|
|
482
|
-
billingMode: 'PAY_PER_REQUEST',
|
|
483
|
-
optimisticLockMaxRetries: 10,
|
|
484
|
-
leaseRandomizationRange: 10,
|
|
485
|
-
batchDelete: { strategy: retryStrategies.exponentialBackoff() },
|
|
486
|
-
query: { strategy: retryStrategies.intervals({ interval: 250, maxRetries: 10 }) },
|
|
487
|
-
})
|
|
@@ -1,168 +0,0 @@
|
|
|
1
|
-
/*
|
|
2
|
-
* Copyright 2023 Salto Labs Ltd.
|
|
3
|
-
*
|
|
4
|
-
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
5
|
-
* you may not use this file except in compliance with
|
|
6
|
-
* the License. You may obtain a copy of the License at
|
|
7
|
-
*
|
|
8
|
-
* http://www.apache.org/licenses/LICENSE-2.0
|
|
9
|
-
*
|
|
10
|
-
* Unless required by applicable law or agreed to in writing, software
|
|
11
|
-
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
12
|
-
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
13
|
-
* See the License for the specific language governing permissions and
|
|
14
|
-
* limitations under the License.
|
|
15
|
-
*/
|
|
16
|
-
import {
|
|
17
|
-
CreateTableCommand,
|
|
18
|
-
CreateTableCommandInput,
|
|
19
|
-
DeleteRequest,
|
|
20
|
-
DescribeTableCommand,
|
|
21
|
-
DynamoDBClient,
|
|
22
|
-
ScalarAttributeType,
|
|
23
|
-
} from '@aws-sdk/client-dynamodb'
|
|
24
|
-
import {
|
|
25
|
-
BatchWriteCommand,
|
|
26
|
-
DynamoDBDocumentClient,
|
|
27
|
-
QueryCommand,
|
|
28
|
-
QueryCommandInput,
|
|
29
|
-
} from '@aws-sdk/lib-dynamodb'
|
|
30
|
-
|
|
31
|
-
import { retry } from '@salto-io/lowerdash'
|
|
32
|
-
|
|
33
|
-
const { withRetry } = retry
|
|
34
|
-
|
|
35
|
-
// eslint-disable-next-line @typescript-eslint/explicit-function-return-type
|
|
36
|
-
export const dbUtils = (db: DynamoDBClient) => {
|
|
37
|
-
const tableStatus = async (tableName: string): Promise<string | undefined> => {
|
|
38
|
-
try {
|
|
39
|
-
const description = await db.send(new DescribeTableCommand({ TableName: tableName }))
|
|
40
|
-
return description && description.Table && description.Table.TableStatus
|
|
41
|
-
} catch (e) {
|
|
42
|
-
if (e.toString().includes('ResourceNotFoundException')) {
|
|
43
|
-
return undefined
|
|
44
|
-
}
|
|
45
|
-
throw e
|
|
46
|
-
}
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
const tableExists = async (
|
|
50
|
-
tableName: string,
|
|
51
|
-
): Promise<boolean> => await tableStatus(tableName) === 'ACTIVE'
|
|
52
|
-
|
|
53
|
-
const ensureTableExists = async (
|
|
54
|
-
tableParams: CreateTableCommandInput & { TableName: string},
|
|
55
|
-
waitOpts?: retry.RetryOpts,
|
|
56
|
-
): Promise<void> => {
|
|
57
|
-
try {
|
|
58
|
-
await db.send(new CreateTableCommand(tableParams))
|
|
59
|
-
} catch (e) {
|
|
60
|
-
if (!e.toString().includes('ResourceInUseException')) {
|
|
61
|
-
throw e
|
|
62
|
-
}
|
|
63
|
-
}
|
|
64
|
-
await withRetry(() => tableExists(tableParams.TableName), waitOpts)
|
|
65
|
-
}
|
|
66
|
-
return {
|
|
67
|
-
tableStatus,
|
|
68
|
-
tableExists,
|
|
69
|
-
ensureTableExists,
|
|
70
|
-
}
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
export type QueryOpts = Omit<retry.RetryOpts, 'description'>
|
|
74
|
-
export type BatchDeleteOpts = Omit<retry.RetryOpts, 'description'>
|
|
75
|
-
|
|
76
|
-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
77
|
-
export type Key = Record<string, any> | undefined
|
|
78
|
-
type AttributeMap = Record<string, ScalarAttributeType>
|
|
79
|
-
|
|
80
|
-
// eslint-disable-next-line @typescript-eslint/explicit-function-return-type
|
|
81
|
-
export const dbDocUtils = (dbDoc: DynamoDBDocumentClient) => {
|
|
82
|
-
const queryBatchIterator = <TReturn extends {} = AttributeMap>(
|
|
83
|
-
input: QueryCommandInput,
|
|
84
|
-
opts?: QueryOpts,
|
|
85
|
-
): AsyncIterator<TReturn[], void> => {
|
|
86
|
-
let done = false
|
|
87
|
-
let items: TReturn[]
|
|
88
|
-
let startKey: Key
|
|
89
|
-
|
|
90
|
-
return {
|
|
91
|
-
next: async () => {
|
|
92
|
-
if (done) {
|
|
93
|
-
return { done: true, value: undefined }
|
|
94
|
-
}
|
|
95
|
-
|
|
96
|
-
await withRetry(async (): Promise<boolean> => {
|
|
97
|
-
const queryResults = await dbDoc.send(new QueryCommand({
|
|
98
|
-
...input,
|
|
99
|
-
ExclusiveStartKey: startKey,
|
|
100
|
-
}))
|
|
101
|
-
|
|
102
|
-
startKey = queryResults.LastEvaluatedKey
|
|
103
|
-
|
|
104
|
-
done = startKey === undefined
|
|
105
|
-
items = queryResults.Items as TReturn[]
|
|
106
|
-
|
|
107
|
-
return done || Boolean(items?.length)
|
|
108
|
-
}, { ...opts, description: 'query' })
|
|
109
|
-
|
|
110
|
-
return (items && items.length !== 0)
|
|
111
|
-
? { done: false, value: items }
|
|
112
|
-
: { done: true, value: undefined }
|
|
113
|
-
},
|
|
114
|
-
}
|
|
115
|
-
}
|
|
116
|
-
|
|
117
|
-
const queryIterator = <TReturn extends {} = AttributeMap>(
|
|
118
|
-
input: QueryCommandInput,
|
|
119
|
-
opts?: QueryOpts,
|
|
120
|
-
): AsyncIterator<TReturn, void> => {
|
|
121
|
-
const batchIter = queryBatchIterator<TReturn>(input, opts)
|
|
122
|
-
let batch: IteratorResult<TReturn[]>
|
|
123
|
-
|
|
124
|
-
return {
|
|
125
|
-
async next(): Promise<IteratorResult<TReturn>> {
|
|
126
|
-
if (!batch || (!batch.done && batch.value.length === 0)) {
|
|
127
|
-
batch = await batchIter.next()
|
|
128
|
-
return this.next()
|
|
129
|
-
}
|
|
130
|
-
|
|
131
|
-
return batch.done
|
|
132
|
-
? { done: true, value: undefined }
|
|
133
|
-
: { done: false, value: batch.value.shift() as TReturn }
|
|
134
|
-
},
|
|
135
|
-
}
|
|
136
|
-
}
|
|
137
|
-
|
|
138
|
-
const MAX_ITEMS_PER_BATCH_WRITE = 25
|
|
139
|
-
|
|
140
|
-
const batchDelete = async (
|
|
141
|
-
tableName: string,
|
|
142
|
-
keysToDelete: Key[],
|
|
143
|
-
opts: BatchDeleteOpts,
|
|
144
|
-
): Promise<void> => {
|
|
145
|
-
await withRetry(async () => {
|
|
146
|
-
const keysToSend = keysToDelete.splice(0, MAX_ITEMS_PER_BATCH_WRITE)
|
|
147
|
-
|
|
148
|
-
const deleteResult = await dbDoc.send(new BatchWriteCommand({
|
|
149
|
-
RequestItems: {
|
|
150
|
-
[tableName]: keysToSend.map(r => ({ DeleteRequest: { Key: r } })),
|
|
151
|
-
},
|
|
152
|
-
}))
|
|
153
|
-
|
|
154
|
-
const unprocessed = (deleteResult.UnprocessedItems?.[tableName] ?? [])
|
|
155
|
-
.map(r => (r.DeleteRequest as DeleteRequest).Key)
|
|
156
|
-
|
|
157
|
-
keysToDelete.push(...unprocessed)
|
|
158
|
-
|
|
159
|
-
return keysToDelete.length === 0
|
|
160
|
-
}, { ...opts, description: 'batchDelete' })
|
|
161
|
-
}
|
|
162
|
-
|
|
163
|
-
return {
|
|
164
|
-
queryBatchIterator,
|
|
165
|
-
queryIterator,
|
|
166
|
-
batchDelete,
|
|
167
|
-
}
|
|
168
|
-
}
|