diff --git a/README.md b/README.md index 67ff6ff..83224a5 100644 --- a/README.md +++ b/README.md @@ -37,8 +37,8 @@ open Amazon.DynamoDBv2 open FSharp.AWS.DynamoDB.Scripting // Expose non-Async methods, e.g. PutItem/GetItem let client : IAmazonDynamoDB = ``your DynamoDB client instance`` -let autoCreate = InitializationMode.CreateIfNotExists (ProvisionedThroughput(10, 10)) -let table = TableContext.Initialize(client, tableName = "workItems", mode = autoCreate) +let throughput = ProvisionedThroughput (readCapacityUnits = 1L, writeCapacityUnits = 10L) +let table = TableContext.Initialize(client, tableName = "workItems", throughput) let workItem = { ProcessId = 0L ; WorkItemId = 1L ; Name = "Test" ; UUID = guid() ; Dependencies = set ["mscorlib"] ; Started = None } @@ -120,7 +120,8 @@ type Counter private (table : TableContext, key : TableKey) = static member Create(client : IAmazonDynamoDB, tableName : string) = async { let table = TableContext(client, tableName) - do! table.InitializeTableAsync( ProvisionedThroughput(100L, 100L)) + let throughput = ProvisionedThroughput(readCapacityUnits = 100L, writeCapacityUnits = 100L) + do! table.InitializeTableAsync throughput let initialEntry = { Id = Guid.NewGuid() ; Value = 0L } let! key = table.PutItemAsync(initialEntry) return Counter(table, key) diff --git a/RELEASE_NOTES.md b/RELEASE_NOTES.md index c83f174..167fd8a 100644 --- a/RELEASE_NOTES.md +++ b/RELEASE_NOTES.md @@ -8,7 +8,8 @@ * Added `TableContext.Scripting.Initialize` (replaces `TableContext.Create()`) * Added `TableContext.VerifyTableAsync` overload that only performs verification but never creates a Table * Added `TableContext.InitializeTableAsync` (replaces `TableContext.VerifyTableAsync(createIfNotExists = true)`) - * Removed `TableContext.CreateAsync` (replace with `TableContext.VerifyTableAsync` or `TableContext.InitializeTableAsync`) + * Added `TableContext.ProvisionTableAsync` (as per `InitializeTableAsync` but does an `UpdateProvisionedThroughputAsync` if throughput has changed) + * Removed `TableContext.CreateAsync` (replace with `TableContext.VerifyTableAsync` or `InitializeTableAsync`) ### 0.9.3-beta * Added `RequestMetrics` record type diff --git a/src/FSharp.AWS.DynamoDB/Script.fsx b/src/FSharp.AWS.DynamoDB/Script.fsx index 8e7ffcc..d985a91 100644 --- a/src/FSharp.AWS.DynamoDB/Script.fsx +++ b/src/FSharp.AWS.DynamoDB/Script.fsx @@ -126,7 +126,8 @@ type EasyCounters private (table : TableContext) = static member Create(client : IAmazonDynamoDB, tableName : string) : Async = async { let table = TableContext(client, tableName) // Create the table if necessary. Verifies schema is correct if it has already been created - do! table.InitializeTableAsync( ProvisionedThroughput(100L, 100L)) + let throughput = ProvisionedThroughput(readCapacityUnits = 100L, writeCapacityUnits = 100L) + do! table.InitializeTableAsync throughput return EasyCounters(table) } @@ -139,8 +140,8 @@ type SimpleCounters private (table : TableContext) = static member Provision(client : IAmazonDynamoDB, tableName : string, readCapacityUnits, writeCapacityUnits) : Async = let table = TableContext(client, tableName) // normally, RCU/WCU provisioning only happens first time the Table is created and is then considered an external concern - // here we use `updateThroughputIfExists = true` to reset it each time we start the app - table.InitializeTableAsync(ProvisionedThroughput (readCapacityUnits, writeCapacityUnits), updateThroughputIfExists = true) + // here we use `ProvisionTableAsync` instead of `InitializeAsync` to reset it each time we start the app + table.ProvisionTableAsync(ProvisionedThroughput (readCapacityUnits, writeCapacityUnits)) /// We only want to do the initialization bit once per instance of our application /// Similar to EasyCounters.Create in that it ensures the table is provisioned correctly @@ -168,10 +169,15 @@ e1.Incr() |> Async.RunSynchronously e2.Incr() |> Async.RunSynchronously SimpleCounters.Provision(ddb, "testing-pre-provisioned", 100L, 100L) |> Async.RunSynchronously +// The consuming code can assume the provisioning has been carried out as part of the deploy +// that allows the creation to be synchronous (and not impede application startup) let s = SimpleCounters.Create(ddb, "testing-pre-provisioned") let s1 = s.StartCounter() |> Async.RunSynchronously // Would throw if Provision has not been carried out s1.Incr() |> Async.RunSynchronously +// Alternately, we can have the app do an extra call (and have some asynchronous initialization work) to check the table is ready let v = SimpleCounters.CreateWithVerify(ddb, "testing-not-present") |> Async.RunSynchronously // Throws, as table not present let v2 = v.StartCounter() |> Async.RunSynchronously v2.Incr() |> Async.RunSynchronously + +// (TOCONSIDER: Illustrate how to use AsyncCacheCell from https://github.com/jet/equinox/blob/master/src/Equinox.Core/AsyncCacheCell.fs to make Verify call lazy) diff --git a/src/FSharp.AWS.DynamoDB/TableContext.fs b/src/FSharp.AWS.DynamoDB/TableContext.fs index a6835d4..a105030 100644 --- a/src/FSharp.AWS.DynamoDB/TableContext.fs +++ b/src/FSharp.AWS.DynamoDB/TableContext.fs @@ -46,16 +46,6 @@ type private LimitType = All | Default | Count of int static member AllOrCount (l : int option) = l |> Option.map Count |> Option.defaultValue All static member DefaultOrCount (l : int option) = l |> Option.map Count |> Option.defaultValue Default -/// Defines Verification and/or Provisioning steps to be applied when initializing and/or validating a TableContext -[] -type InitializationMode = - /// Validate the Table exists and has a compatible schema - | VerifyOnly - /// Perform a Validation step as per VerifyOnly, but Create the Table with the specified Throughput if it was not found - | CreateIfNotExists of provisionedThroughput : ProvisionedThroughput - /// Validate and/or Create as per CreateIfNotExists, but also re-apply the specified Throughput in case it has diverged - | CreateOrUpdateThroughput of provisionedThroughput : ProvisionedThroughput - /// DynamoDB client object for performing table operations in the context of given F# record representations [] type TableContext<'TRecord> internal @@ -915,69 +905,61 @@ type TableContext<'TRecord> internal } - member __.InternalInitializeAsync(mode : InitializationMode) : Async = async { + member internal _.InternalDescribe() : Async = + let rec wait () = async { + let! ct = Async.CancellationToken + let! td = client.DescribeTableAsync(tableName, ct) |> Async.AwaitTaskCorrect + if td.Table.TableStatus = TableStatus.ACTIVE then + do! Async.Sleep 2000 + // wait indefinitely if table is in transition state + return! wait () + else + + return td.Table + } + wait () + + member internal __.CreateOrValidateTableAsync(createThroughput) : Async = let (|Conflict|_|) (e : exn) = match e with | :? AmazonDynamoDBException as e when e.StatusCode = HttpStatusCode.Conflict -> Some() | :? ResourceInUseException -> Some () | _ -> None - let rec verify retries = async { - let! ct = Async.CancellationToken - let! response = - client.DescribeTableAsync(tableName, ct) - |> Async.AwaitTaskCorrect - |> Async.Catch - - match response with - | Choice1Of2 td -> - if td.Table.TableStatus <> TableStatus.ACTIVE then - do! Async.Sleep 2000 - // wait indefinitely if table is in transition state - return! verify retries - else - - let existingSchema = TableKeySchemata.OfTableDescription td.Table + let rec checkOrCreate retries = async { + match! __.InternalDescribe() |> Async.Catch with + | Choice1Of2 desc -> + let existingSchema = TableKeySchemata.OfTableDescription desc if existingSchema <> template.Info.Schemata then sprintf "table '%s' exists with key schema %A, which is incompatible with record '%O'." tableName existingSchema typeof<'TRecord> |> invalidOp - match mode with - | InitializationMode.VerifyOnly | InitializationMode.CreateIfNotExists _ -> () - | InitializationMode.CreateOrUpdateThroughput t -> - let provisioned = td.Table.ProvisionedThroughput - if t.ReadCapacityUnits <> provisioned.ReadCapacityUnits - || t.WriteCapacityUnits <> provisioned.WriteCapacityUnits then - do! __.UpdateProvisionedThroughputAsync(t) - - | Choice2Of2 (:? ResourceNotFoundException) when mode <> InitializationMode.VerifyOnly -> - let throughput = - match mode with - | InitializationMode.VerifyOnly -> failwith "Unexpected" // the when guard should preclude this - | InitializationMode.CreateIfNotExists t | InitializationMode.CreateOrUpdateThroughput t -> t - let ctr = template.Info.Schemata.CreateCreateTableRequest(tableName, throughput) + return desc + + | Choice2Of2 (:? ResourceNotFoundException) when Option.isSome createThroughput -> + let! ct = Async.CancellationToken + let ctr = template.Info.Schemata.CreateCreateTableRequest(tableName, Option.get createThroughput) let! response = client.CreateTableAsync(ctr, ct) |> Async.AwaitTaskCorrect |> Async.Catch match response with - | Choice1Of2 _ -> return! verify retries + | Choice1Of2 _ -> return! checkOrCreate retries | Choice2Of2 Conflict when retries > 0 -> do! Async.Sleep 2000 - return! verify (retries - 1) + return! checkOrCreate (retries - 1) - | Choice2Of2 e -> do! Async.Raise e + | Choice2Of2 e -> return! Async.Raise e | Choice2Of2 Conflict when retries > 0 -> do! Async.Sleep 2000 - return! verify (retries - 1) + return! checkOrCreate (retries - 1) - | Choice2Of2 e -> do! Async.Raise e + | Choice2Of2 e -> return! Async.Raise e } - do! verify 9 // up to 9 retries, i.e. 10 attempts before we let exception propagate - } + checkOrCreate 9 // up to 9 retries, i.e. 10 attempts before we let exception propagate /// /// Asynchronously verify that the table exists and is compatible with record key schema. @@ -986,33 +968,41 @@ type TableContext<'TRecord> internal /// Provisioned throughput for the table if newly created. Defaults to (10,10). [] member __.VerifyTableAsync(?createIfNotExists : bool, ?provisionedThroughput : ProvisionedThroughput) : Async = - let mode = + let throughputIfCreate = if createIfNotExists = Some true then let throughput = match provisionedThroughput with Some p -> p | None -> ProvisionedThroughput(10L, 10L) - InitializationMode.CreateIfNotExists throughput - else InitializationMode.VerifyOnly - __.InternalInitializeAsync(mode) + Some throughput + else None + __.CreateOrValidateTableAsync(throughputIfCreate) |> Async.Ignore /// /// Asynchronously verify that the table exists and is compatible with record key schema, or throw.
/// See also InitializeTableAsync, which performs the same check, but can create or re-provision the Table if required. ///
member __.VerifyTableAsync() : Async = - __.InternalInitializeAsync(InitializationMode.VerifyOnly) + __.CreateOrValidateTableAsync(None) |> Async.Ignore /// /// Asynchronously verifies that the table exists and is compatible with record key schema, throwing if it is incompatible.
/// If the table is not present, it is provisioned, with the specified throughput.
- /// Optionally can reapply the cited throughput level if the Table has already been created.
/// See also VerifyTableAsync, which only verifies the Table is present and correct. ///
/// Provisioned throughput to use for the table. - /// Trigger re-provisioning of the Table's throughput if it was already present. Default: false - member __.InitializeTableAsync(throughput : ProvisionedThroughput, ?updateThroughputIfExists) : Async = - let mode = - if updateThroughputIfExists = Some true then InitializationMode.CreateOrUpdateThroughput - else InitializationMode.CreateIfNotExists - __.InternalInitializeAsync(mode throughput) + member __.InitializeTableAsync(throughput : ProvisionedThroughput) : Async = + __.CreateOrValidateTableAsync(Some throughput) |> Async.Ignore + + /// + /// Asynchronously verifies that the table exists and is compatible with record key schema, throwing if it is incompatible.
+ /// If the table is not present, it is provisioned, with the specified throughput.
+ /// If it is present, and the throughput is not as specified, uses UpdateProvisionedThroughputAsync to update it.
+ ///
+ /// Provisioned throughput to use for the table. + member __.ProvisionTableAsync(throughput : ProvisionedThroughput) : Async = async { + let! desc = __.CreateOrValidateTableAsync(Some throughput) + let provisioned = desc.ProvisionedThroughput + if throughput.ReadCapacityUnits <> provisioned.ReadCapacityUnits + || throughput.WriteCapacityUnits <> provisioned.WriteCapacityUnits then + do! __.UpdateProvisionedThroughputAsync(throughput) } // Deprecated factory method, to be removed. Replaced with // 1. TableContext<'T> ctor (synchronous) @@ -1034,17 +1024,14 @@ type TableContext internal () = static member Create<'TRecord> ( client : IAmazonDynamoDB, tableName : string, ?verifyTable : bool, ?createIfNotExists : bool, ?provisionedThroughput : ProvisionedThroughput, - ?metricsCollector : RequestMetrics -> unit) = + ?metricsCollector : RequestMetrics -> unit) = async { let context = TableContext<'TRecord>(client, tableName, ?metricsCollector = metricsCollector) - let verifyTable, createIfNotExists = verifyTable <> Some false, createIfNotExists = Some true - if verifyTable || createIfNotExists then - let mode = - if createIfNotExists then - let throughput = match provisionedThroughput with Some p -> p | None -> ProvisionedThroughput(10L, 10L) - InitializationMode.CreateIfNotExists throughput - else InitializationMode.VerifyOnly - context.InternalInitializeAsync(mode) |> Async.RunSynchronously - context + if createIfNotExists = Some true then + let throughput = match provisionedThroughput with Some p -> p | None -> ProvisionedThroughput(10L, 10L) + do! context.InitializeTableAsync throughput + elif verifyTable <> Some false then + do! context.VerifyTableAsync() + return context } /// /// Sync-over-Async helpers that can be opted-into when working in scripting scenarios. @@ -1059,15 +1046,15 @@ module Scripting = /// Creates a DynamoDB client instance for the specified F# record type, client and table name. /// DynamoDB client instance. /// Table name to target. - /// Allows one to define auto-creation or re-provisioning rules via . + /// Optional throughput to configure if the Table does not yet exist. /// Function to receive request metrics. static member Initialize<'TRecord> - ( client : IAmazonDynamoDB, tableName : string, - ?mode : InitializationMode, + ( client : IAmazonDynamoDB, tableName : string, ?throughput, ?metricsCollector : RequestMetrics -> unit) : TableContext<'TRecord> = let context = TableContext<'TRecord>(client, tableName, ?metricsCollector = metricsCollector) - let mode = defaultArg mode InitializationMode.VerifyOnly - context.InternalInitializeAsync(mode) |> Async.RunSynchronously + match throughput with + | None -> context.VerifyTableAsync() |> Async.RunSynchronously + | Some t -> context.InitializeTableAsync(t) |> Async.RunSynchronously context type TableContext<'TRecord> with diff --git a/tests/FSharp.AWS.DynamoDB.Tests/Utils.fs b/tests/FSharp.AWS.DynamoDB.Tests/Utils.fs index b4f707f..f152e16 100644 --- a/tests/FSharp.AWS.DynamoDB.Tests/Utils.fs +++ b/tests/FSharp.AWS.DynamoDB.Tests/Utils.fs @@ -39,12 +39,10 @@ module Utils = type TableFixture() = let client = getDynamoDBAccount() let tableName = getRandomTableName() - member __.Client = client - member __.TableName = tableName member __.CreateContextAndTableIfNotExists<'TRecord>() = - let autoCreate = InitializationMode.CreateIfNotExists (ProvisionedThroughput(10L, 10L)) - Scripting.TableContext.Initialize<'TRecord>(__.Client, __.TableName, mode = autoCreate) + let createThroughput = ProvisionedThroughput(10L, 10L) + Scripting.TableContext.Initialize<'TRecord>(client, tableName, createThroughput) interface IDisposable with member __.Dispose() =