@@ -150,6 +150,31 @@ public extension AWSDynamoDBCompositePrimaryKeyTable {
150
150
throw DynamoDBTableError . batchErrorsReturned ( errorCount: errorCount, messageMap: errorMap)
151
151
}
152
152
153
+ private func writeTransactionItems< AttributesType, ItemType> (
154
+ _ entries: [ WriteEntry < AttributesType , ItemType > ] , constraints: [ TransactionConstraintEntry < AttributesType , ItemType > ] ) async throws
155
+ {
156
+ // if there are no items, there is nothing to update
157
+ guard entries. count > 0 else {
158
+ return
159
+ }
160
+
161
+ let entryStatements = try entries. map { entry -> DynamoDBClientTypes . ParameterizedStatement in
162
+ let statement = try self . entryToStatement ( entry)
163
+
164
+ return DynamoDBClientTypes . ParameterizedStatement ( statement: statement)
165
+ }
166
+
167
+ let requiredItemsStatements = try constraints. map { entry -> DynamoDBClientTypes . ParameterizedStatement in
168
+ let statement = try self . entryToStatement ( entry)
169
+
170
+ return DynamoDBClientTypes . ParameterizedStatement ( statement: statement)
171
+ }
172
+
173
+ let transactionInput = ExecuteTransactionInput ( transactStatements: entryStatements + requiredItemsStatements)
174
+
175
+ _ = try await dynamodb. executeTransaction ( input: transactionInput)
176
+ }
177
+
153
178
private func writeTransactionItems(
154
179
_ entries: [ some PolymorphicWriteEntry ] , constraints: [ some PolymorphicTransactionConstraintEntry ] ) async throws
155
180
{
@@ -179,6 +204,18 @@ public extension AWSDynamoDBCompositePrimaryKeyTable {
179
204
_ = try await dynamodb. executeTransaction ( input: transactionInput)
180
205
}
181
206
207
+ func transactWrite( _ entries: [ WriteEntry < some Any , some Any > ] ) async throws {
208
+ try await self . transactWrite ( entries, constraints: [ ] ,
209
+ retriesRemaining: self . retryConfiguration. numRetries)
210
+ }
211
+
212
+ func transactWrite< AttributesType, ItemType> ( _ entries: [ WriteEntry < AttributesType , ItemType > ] ,
213
+ constraints: [ TransactionConstraintEntry < AttributesType , ItemType > ] ) async throws
214
+ {
215
+ try await self . transactWrite ( entries, constraints: constraints,
216
+ retriesRemaining: self . retryConfiguration. numRetries)
217
+ }
218
+
182
219
func polymorphicTransactWrite( _ entries: [ some PolymorphicWriteEntry ] ) async throws {
183
220
let noConstraints : [ EmptyPolymorphicTransactionConstraintEntry ] = [ ]
184
221
return try await self . polymorphicTransactWrite ( entries, constraints: noConstraints,
@@ -192,6 +229,113 @@ public extension AWSDynamoDBCompositePrimaryKeyTable {
192
229
retriesRemaining: self . retryConfiguration. numRetries)
193
230
}
194
231
232
+ private func transactWrite< AttributesType, ItemType> (
233
+ _ entries: [ WriteEntry < AttributesType , ItemType > ] , constraints: [ TransactionConstraintEntry < AttributesType , ItemType > ] ,
234
+ retriesRemaining: Int ) async throws
235
+ {
236
+ let entryCount = entries. count + constraints. count
237
+
238
+ if entryCount > AWSDynamoDBLimits . maximumUpdatesPerTransactionStatement {
239
+ throw DynamoDBTableError . transactionSizeExceeded ( attemptedSize: entryCount,
240
+ maximumSize: AWSDynamoDBLimits . maximumUpdatesPerTransactionStatement)
241
+ }
242
+
243
+ let result : Swift . Result < Void , DynamoDBTableError >
244
+ do {
245
+ try await self . writeTransactionItems ( entries, constraints: constraints)
246
+
247
+ result = . success( ( ) )
248
+ } catch let exception as TransactionCanceledException {
249
+ guard let cancellationReasons = exception. properties. cancellationReasons else {
250
+ throw DynamoDBTableError . transactionCanceled ( reasons: [ ] )
251
+ }
252
+
253
+ let keys = entries. map ( \. compositePrimaryKey) + constraints. map ( \. compositePrimaryKey)
254
+
255
+ var isTransactionConflict = false
256
+ let reasons = try zip ( cancellationReasons, keys) . compactMap { cancellationReason, entryKey -> DynamoDBTableError ? in
257
+ let key : StandardCompositePrimaryKey ?
258
+ if let item = cancellationReason. item {
259
+ key = try DynamoDBDecoder ( ) . decode ( . m( item) )
260
+ } else {
261
+ key = nil
262
+ }
263
+
264
+ let partitionKey = key? . partitionKey ?? entryKey. partitionKey
265
+ let sortKey = key? . sortKey ?? entryKey. sortKey
266
+
267
+ // https://docs.aws.amazon.com/amazondynamodb/latest/APIReference/API_ExecuteTransaction.html
268
+ switch cancellationReason. code {
269
+ case " None " :
270
+ return nil
271
+ case " ConditionalCheckFailed " :
272
+ return DynamoDBTableError . transactionConditionalCheckFailed ( partitionKey: partitionKey,
273
+ sortKey: sortKey,
274
+ message: cancellationReason. message)
275
+ case " DuplicateItem " :
276
+ return DynamoDBTableError . duplicateItem ( partitionKey: partitionKey, sortKey: sortKey,
277
+ message: cancellationReason. message)
278
+ case " ItemCollectionSizeLimitExceeded " :
279
+ return DynamoDBTableError . transactionSizeExceeded ( attemptedSize: entryCount,
280
+ maximumSize: AWSDynamoDBLimits . maximumUpdatesPerTransactionStatement)
281
+ case " TransactionConflict " :
282
+ isTransactionConflict = true
283
+
284
+ return DynamoDBTableError . transactionConflict ( message: cancellationReason. message)
285
+ case " ProvisionedThroughputExceeded " :
286
+ return DynamoDBTableError . transactionProvisionedThroughputExceeded ( message: cancellationReason. message)
287
+ case " ThrottlingError " :
288
+ return DynamoDBTableError . transactionThrottling ( message: cancellationReason. message)
289
+ case " ValidationError " :
290
+ return DynamoDBTableError . transactionValidation ( partitionKey: partitionKey, sortKey: sortKey,
291
+ message: cancellationReason. message)
292
+ default :
293
+ return DynamoDBTableError . transactionUnknown ( code: cancellationReason. code, partitionKey: partitionKey,
294
+ sortKey: sortKey, message: cancellationReason. message)
295
+ }
296
+ }
297
+
298
+ if isTransactionConflict, retriesRemaining > 0 {
299
+ return try await retryTransactWrite ( entries, constraints: constraints, retriesRemaining: retriesRemaining)
300
+ }
301
+
302
+ result = . failure( DynamoDBTableError . transactionCanceled ( reasons: reasons) )
303
+ } catch let exception as TransactionConflictException {
304
+ if retriesRemaining > 0 {
305
+ return try await retryTransactWrite ( entries, constraints: constraints, retriesRemaining: retriesRemaining)
306
+ }
307
+
308
+ let reason = DynamoDBTableError . transactionConflict ( message: exception. message)
309
+
310
+ result = . failure( DynamoDBTableError . transactionCanceled ( reasons: [ reason] ) )
311
+ }
312
+
313
+ let retryCount = self . retryConfiguration. numRetries - retriesRemaining
314
+ self . tableMetrics. transactWriteRetryCountRecorder? . record ( retryCount)
315
+
316
+ switch result {
317
+ case . success:
318
+ return
319
+ case let . failure( failure) :
320
+ throw failure
321
+ }
322
+ }
323
+
324
+ private func retryTransactWrite< AttributesType, ItemType> (
325
+ _ entries: [ WriteEntry < AttributesType , ItemType > ] , constraints: [ TransactionConstraintEntry < AttributesType , ItemType > ] ,
326
+ retriesRemaining: Int ) async throws
327
+ {
328
+ // determine the required interval
329
+ let retryInterval = Int ( self . retryConfiguration. getRetryInterval ( retriesRemaining: retriesRemaining) )
330
+
331
+ logger. warning (
332
+ " Transaction retried due to conflict. Remaining retries: \( retriesRemaining) . Retrying in \( retryInterval) ms. " )
333
+ try await Task . sleep ( nanoseconds: UInt64 ( retryInterval) * millisecondsToNanoSeconds)
334
+
335
+ logger. trace ( " Reattempting request due to remaining retries: \( retryInterval) " )
336
+ return try await self . transactWrite ( entries, constraints: constraints, retriesRemaining: retriesRemaining - 1 )
337
+ }
338
+
195
339
private func polymorphicTransactWrite(
196
340
_ entries: [ some PolymorphicWriteEntry ] , constraints: [ some PolymorphicTransactionConstraintEntry ] ,
197
341
retriesRemaining: Int ) async throws
@@ -259,13 +403,13 @@ public extension AWSDynamoDBCompositePrimaryKeyTable {
259
403
}
260
404
261
405
if isTransactionConflict, retriesRemaining > 0 {
262
- return try await retryTransactWrite ( entries, constraints: constraints, retriesRemaining: retriesRemaining)
406
+ return try await retryPolymorphicTransactWrite ( entries, constraints: constraints, retriesRemaining: retriesRemaining)
263
407
}
264
408
265
409
result = . failure( DynamoDBTableError . transactionCanceled ( reasons: reasons) )
266
410
} catch let exception as TransactionConflictException {
267
411
if retriesRemaining > 0 {
268
- return try await retryTransactWrite ( entries, constraints: constraints, retriesRemaining: retriesRemaining)
412
+ return try await retryPolymorphicTransactWrite ( entries, constraints: constraints, retriesRemaining: retriesRemaining)
269
413
}
270
414
271
415
let reason = DynamoDBTableError . transactionConflict ( message: exception. message)
@@ -284,7 +428,7 @@ public extension AWSDynamoDBCompositePrimaryKeyTable {
284
428
}
285
429
}
286
430
287
- private func retryTransactWrite (
431
+ private func retryPolymorphicTransactWrite (
288
432
_ entries: [ some PolymorphicWriteEntry ] , constraints: [ some PolymorphicTransactionConstraintEntry ] ,
289
433
retriesRemaining: Int ) async throws
290
434
{
0 commit comments