Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 7 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -269,25 +269,26 @@ let doesntExistCondition = compile <@ fun t -> NOT_EXISTS t.Value @>
let existsCondition = compile <@ fun t -> EXISTS t.Value @>
let key = TableKey.Combined(hashKey, rangeKey)

let transaction = Transaction()
let transaction = table.CreateTransaction()

transaction.Check(table, key, doesntExistCondition)
transaction.Put(table, item2, None)
transaction.Put(table, item3, Some existsCondition)
transaction.Delete (table ,table.Template.ExtractKey item5, None)
transaction.Delete(table, table.Template.ExtractKey item5, None)

do! transaction.TransactWriteItems()
```

Failed preconditions (or `TransactWrite.Check`s) are signalled as per the underlying API: via a `TransactionCanceledException`.
Use `TransactWriteItemsRequest.TransactionCanceledConditionalCheckFailed` to trap such conditions:
Failed preconditions (or `Check`s) are signalled as per the underlying API: via a `TransactionCanceledException`.
Use `Transaction.TransactionCanceledConditionalCheckFailed` to trap such conditions:

```fsharp
try do! transaction.TransactWriteItems()
return Some result
with TransactWriteItemsRequest.TransactionCanceledConditionalCheckFailed -> return None
with Transaction.TransactionCanceledConditionalCheckFailed -> return None
```

See [`TransactWriteItems tests`](./tests/FSharp.AWS.DynamoDB.Tests/SimpleTableOperationTests.fs#130) for more details and examples.
See [`TransactWriteItems tests`](./tests/FSharp.AWS.DynamoDB.Tests/SimpleTableOperationTests.fs#156) for more details and examples.

It generally costs [double or more the Write Capacity Units charges compared to using precondition expressions](https://zaccharles.medium.com/calculating-a-dynamodb-items-size-and-consumed-capacity-d1728942eb7c)
on individual operations.
Expand Down
3 changes: 3 additions & 0 deletions RELEASE_NOTES.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
### 0.12.2-beta
* (breaking) Revised multi-table transaction API (thanks @bartelink)

### 0.12.1-beta
* Added support for `defaultArg` in update expressions on the same attribute, allowing SET if_not_exists semantics (eg { record with OptionalValue = Some (defaultArg record.OptionalValue "Default") })
* Allow empty strings in non-key attributes (thanks @purkhusid)
Expand Down
52 changes: 18 additions & 34 deletions src/FSharp.AWS.DynamoDB/TableContext.fs
Original file line number Diff line number Diff line change
Expand Up @@ -1318,22 +1318,17 @@ type TableContext<'TRecord>
else
t.VerifyTableAsync()

member t.Transaction() =
match metricsCollector with
| Some metricsCollector -> Transaction(metricsCollector = metricsCollector)
| None -> Transaction()
/// <summary>Creates a new `Transaction`, using the DynamoDB client and metricsCollector configured for this `TableContext`</summary>
member _.CreateTransaction() =
Transaction(client, ?metricsCollector = metricsCollector)

/// <summary>
/// Represents a transactional set of operations to be applied atomically to a arbitrary number of DynamoDB tables.
/// </summary>
/// <param name="client">DynamoDB client instance</param>
/// <param name="metricsCollector">Function to receive request metrics.</param>
and Transaction(?metricsCollector: (RequestMetrics -> unit)) =
and Transaction(client: IAmazonDynamoDB, ?metricsCollector: (RequestMetrics -> unit)) =
let transactionItems = ResizeArray<TransactWriteItem>()
let mutable (dynamoDbClient: IAmazonDynamoDB) = null

let setClient client =
if dynamoDbClient = null then
dynamoDbClient <- client

let reportMetrics collector (tableName: string) (operation: Operation) (consumedCapacity: ConsumedCapacity list) (itemCount: int) =
collector
Expand All @@ -1353,35 +1348,30 @@ and Transaction(?metricsCollector: (RequestMetrics -> unit)) =
/// <param name="tableContext">Table context to operate on.</param>
/// <param name="item">Item to be put.</param>
/// <param name="precondition">Optional precondition expression.</param>
member this.Put<'TRecord>
member _.Put<'TRecord>
(
tableContext: TableContext<'TRecord>,
item: 'TRecord,
?precondition: ConditionExpression<'TRecord>
) : Transaction =
setClient tableContext.Client
) =
let req = Put(TableName = tableContext.TableName, Item = tableContext.Template.ToAttributeValues item)
precondition
|> Option.iter (fun cond ->
let writer = AttributeWriter(req.ExpressionAttributeNames, req.ExpressionAttributeValues)
req.ConditionExpression <- cond.Conditional.Write writer)
transactionItems.Add(TransactWriteItem(Put = req))
this

/// <summary>
/// Adds a ConditionCheck operation to the transaction.
/// </summary>
/// <param name="tableContext">Table context to operate on.</param>
/// <param name="key">Key of item to check.</param>
/// <param name="condition">Condition to check.</param>
member this.Check(tableContext: TableContext<'TRecord>, key: TableKey, condition: ConditionExpression<'TRecord>) : Transaction =
setClient tableContext.Client

member _.Check(tableContext: TableContext<'TRecord>, key: TableKey, condition: ConditionExpression<'TRecord>) =
let req = ConditionCheck(TableName = tableContext.TableName, Key = tableContext.Template.ToAttributeValues key)
let writer = AttributeWriter(req.ExpressionAttributeNames, req.ExpressionAttributeValues)
req.ConditionExpression <- condition.Conditional.Write writer
transactionItems.Add(TransactWriteItem(ConditionCheck = req))
this

/// <summary>
/// Adds an Update operation to the transaction.
Expand All @@ -1390,44 +1380,38 @@ and Transaction(?metricsCollector: (RequestMetrics -> unit)) =
/// <param name="key">Key of item to update.</param>
/// <param name="updater">Update expression.</param>
/// <param name="precondition">Optional precondition expression.</param>
member this.Update
member _.Update
(
tableContext: TableContext<'TRecord>,
key: TableKey,
updater: UpdateExpression<'TRecord>,
?precondition: ConditionExpression<'TRecord>

) : Transaction =
setClient tableContext.Client

) =
let req = Update(TableName = tableContext.TableName, Key = tableContext.Template.ToAttributeValues key)
let writer = AttributeWriter(req.ExpressionAttributeNames, req.ExpressionAttributeValues)
req.UpdateExpression <- updater.UpdateOps.Write(writer)
precondition |> Option.iter (fun cond -> req.ConditionExpression <- cond.Conditional.Write writer)
transactionItems.Add(TransactWriteItem(Update = req))
this

/// <summary>
/// Adds a Delete operation to the transaction.
/// </summary>
/// <param name="tableContext">Table context to operate on.</param>
/// <param name="key">Key of item to delete.</param>
/// <param name="precondition">Optional precondition expression.</param>
member this.Delete
member _.Delete
(
tableContext: TableContext<'TRecord>,
key: TableKey,
precondition: option<ConditionExpression<'TRecord>>
) : Transaction =
setClient tableContext.Client

?precondition: ConditionExpression<'TRecord>
) =
let req = Delete(TableName = tableContext.TableName, Key = tableContext.Template.ToAttributeValues key)
precondition
|> Option.iter (fun cond ->
let writer = AttributeWriter(req.ExpressionAttributeNames, req.ExpressionAttributeValues)
req.ConditionExpression <- cond.Conditional.Write writer)
transactionItems.Add(TransactWriteItem(Delete = req))
this

/// <summary>
/// Atomically applies a set of 1-100 operations to the table.<br/>
Expand All @@ -1436,13 +1420,13 @@ and Transaction(?metricsCollector: (RequestMetrics -> unit)) =
/// </summary>
/// <param name="clientRequestToken">The <c>ClientRequestToken</c> to supply as an idempotency key (10 minute window).</param>
member _.TransactWriteItems(?clientRequestToken) : Async<unit> = async {
if (Seq.length transactionItems) = 0 || (Seq.length transactionItems) > 100 then
if transactionItems.Count = 0 || transactionItems.Count > 100 then
raise
<| System.ArgumentOutOfRangeException(nameof transactionItems, "must be between 1 and 100 items.")
let req = TransactWriteItemsRequest(ReturnConsumedCapacity = returnConsumedCapacity, TransactItems = (ResizeArray transactionItems))
let req = TransactWriteItemsRequest(ReturnConsumedCapacity = returnConsumedCapacity, TransactItems = transactionItems)
clientRequestToken |> Option.iter (fun x -> req.ClientRequestToken <- x)
let! ct = Async.CancellationToken
let! response = dynamoDbClient.TransactWriteItemsAsync(req, ct) |> Async.AwaitTaskCorrect
let! response = client.TransactWriteItemsAsync(req, ct) |> Async.AwaitTaskCorrect
maybeReport
|> Option.iter (fun r ->
response.ConsumedCapacity
Expand Down Expand Up @@ -2150,8 +2134,8 @@ module Scripting =
let spec = Throughput.Provisioned provisionedThroughput
t.UpdateTableIfRequiredAsync(spec) |> Async.Ignore |> Async.RunSynchronously

/// Helpers for working with <c>TransactWriteItemsRequest</c>
module TransactWriteItemsRequest =
/// Helpers for working with <c>Transaction</c>
module Transaction =
/// <summary>Exception filter to identify whether a <c>TransactWriteItems</c> call has failed due to
/// one or more of the supplied <c>precondition</c> checks failing.</summary>
let (|TransactionCanceledConditionalCheckFailed|_|): exn -> unit option =
Expand Down
15 changes: 7 additions & 8 deletions tests/FSharp.AWS.DynamoDB.Tests/MetricsCollectorTests.fs
Original file line number Diff line number Diff line change
Expand Up @@ -106,10 +106,9 @@ type Tests(fixture: TableFixture) =
collector.Clear()

let item = mkItem (guid ()) (guid ()) 0
do!
Transaction(collector.Collect)
.Put(sut, item, compile <@ fun t -> NOT_EXISTS t.RangeKey @>)
.TransactWriteItems()
let transaction = Transaction(sut.Client, collector.Collect)
transaction.Put(sut, item, compile <@ fun t -> NOT_EXISTS t.RangeKey @>)
do! transaction.TransactWriteItems()

test
<@
Expand All @@ -131,14 +130,14 @@ type Tests(fixture: TableFixture) =
let sut = rawTable.WithMetricsCollector(collector.Collect)

let item = mkItem (guid ()) (guid ()) 0
let transaction = rawTable.CreateTransaction()
transaction.Put(sut, item, compile <@ fun t -> EXISTS t.RangeKey @>)
let mutable failed = false
try
do!
// The check will fail, which triggers a throw from the underlying AWS SDK; there's no way to extract the consumption info in that case
Transaction()
.Put(sut, item, compile <@ fun t -> EXISTS t.RangeKey @>)
.TransactWriteItems()
with TransactWriteItemsRequest.TransactionCanceledConditionalCheckFailed ->
transaction.TransactWriteItems()
with Transaction.TransactionCanceledConditionalCheckFailed ->
failed <- true
true =! failed
[] =! collector.Metrics
Expand Down
53 changes: 24 additions & 29 deletions tests/FSharp.AWS.DynamoDB.Tests/SimpleTableOperationTests.fs
Original file line number Diff line number Diff line change
Expand Up @@ -182,10 +182,9 @@ type ``TransactWriteItems tests``(table1: TableFixture, table2: TableFixture) =
[<Fact>]
let ``Minimal happy path`` () = async {
let item = mkItem ()
do!
Transaction()
.Put(table1, item, doesntExistConditionTable1)
.TransactWriteItems()
let transaction = table1.CreateTransaction()
transaction.Put(table1, item, doesntExistConditionTable1)
do! transaction.TransactWriteItems()

let! itemFound = table1.ContainsKeyAsync(table1.Template.ExtractKey item)
true =! itemFound
Expand All @@ -196,11 +195,10 @@ type ``TransactWriteItems tests``(table1: TableFixture, table2: TableFixture) =
let item = mkItem ()
let compatibleItem = mkCompatibleItem ()

do!
Transaction()
.Put(table1, item, doesntExistConditionTable1)
.Put(table2, compatibleItem, doesntExistConditionTable2)
.TransactWriteItems()
let transaction = table1.CreateTransaction()
transaction.Put(table1, item, doesntExistConditionTable1)
transaction.Put(table2, compatibleItem, doesntExistConditionTable2)
do! transaction.TransactWriteItems()

let! itemFound = table1.ContainsKeyAsync(table1.Template.ExtractKey item)
true =! itemFound
Expand All @@ -213,13 +211,12 @@ type ``TransactWriteItems tests``(table1: TableFixture, table2: TableFixture) =
let ``Minimal Canceled path`` () = async {
let item = mkItem ()

let transaction = table1.CreateTransaction()
transaction.Put(table1, item, existsConditionTable1)
let mutable failed = false
try
do!
Transaction()
.Put(table1, item, existsConditionTable1)
.TransactWriteItems()
with TransactWriteItemsRequest.TransactionCanceledConditionalCheckFailed ->
do! transaction.TransactWriteItems()
with Transaction.TransactionCanceledConditionalCheckFailed ->
failed <- true

true =! failed
Expand All @@ -233,18 +230,17 @@ type ``TransactWriteItems tests``(table1: TableFixture, table2: TableFixture) =
let item, item2 = mkItem (), mkItem ()
let! key = table1.PutItemAsync item

let transaction =
if shouldFail then
Transaction().Check(table1, key, doesntExistConditionTable1)
else
Transaction()
.Check(table1, key, existsConditionTable1)
.Put(table1, item2)
let transaction = table1.CreateTransaction()
if shouldFail then
transaction.Check(table1, key, doesntExistConditionTable1)
else
transaction.Check(table1, key, existsConditionTable1)
transaction.Put(table1, item2)

let mutable failed = false
try
do! transaction.TransactWriteItems()
with TransactWriteItemsRequest.TransactionCanceledConditionalCheckFailed ->
with Transaction.TransactionCanceledConditionalCheckFailed ->
failed <- true

failed =! shouldFail
Expand All @@ -257,14 +253,14 @@ type ``TransactWriteItems tests``(table1: TableFixture, table2: TableFixture) =
let ``All paths`` shouldFail = async {
let item, item2, item3, item4, item5, item6, item7 = mkItem (), mkItem (), mkItem (), mkItem (), mkItem (), mkItem (), mkItem ()
let! key = table1.PutItemAsync item
let transaction = Transaction()
let transaction = table1.CreateTransaction()

let requests =
[ transaction.Update(table1, key, compileUpdateTable1 <@ fun t -> { t with Value = 42 } @>, existsConditionTable1)
transaction.Put(table1, item2)
transaction.Put(table1, item3, doesntExistConditionTable1)
transaction.Delete(table1, table1.Template.ExtractKey item4, Some doesntExistConditionTable1)
transaction.Delete(table1, table1.Template.ExtractKey item5, None)
transaction.Delete(table1, table1.Template.ExtractKey item4, doesntExistConditionTable1)
transaction.Delete(table1, table1.Template.ExtractKey item5)
transaction.Check(
table1,
table1.Template.ExtractKey item6,
Expand All @@ -281,7 +277,7 @@ type ``TransactWriteItems tests``(table1: TableFixture, table2: TableFixture) =
let mutable failed = false
try
do! transaction.TransactWriteItems()
with TransactWriteItemsRequest.TransactionCanceledConditionalCheckFailed ->
with Transaction.TransactionCanceledConditionalCheckFailed ->
failed <- true
failed =! shouldFail

Expand Down Expand Up @@ -310,13 +306,12 @@ type ``TransactWriteItems tests``(table1: TableFixture, table2: TableFixture) =

[<Fact>]
let ``Empty request list is rejected with AORE`` () =
shouldBeRejectedWithArgumentOutOfRangeException (Transaction())
shouldBeRejectedWithArgumentOutOfRangeException (Transaction(table1.Client))
|> Async.RunSynchronously
|> ignore

[<Fact>]
let ``Over 100 writes are rejected with AORE`` () =
let Transaction = Transaction()
let Transaction = Transaction(table1.Client)
for _x in 1..101 do
Transaction.Put(table1, mkItem ()) |> ignore

Expand Down