diff --git a/README.md b/README.md index 5552b0a..2b53d41 100644 --- a/README.md +++ b/README.md @@ -120,7 +120,7 @@ type Counter private (table : TableContext, key : TableKey) = static member Create(client : IAmazonDynamoDB, tableName : string) = async { let table = TableContext(client, tableName) let throughput = ProvisionedThroughput(readCapacityUnits = 10L, writeCapacityUnits = 10L) - do! table.CreateTableIfNotExistsAsync(Throughput.Provisioned throughput) + do! table.VerifyOrCreateTableAsync(Throughput.Provisioned 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 9237554..8f91bda 100644 --- a/RELEASE_NOTES.md +++ b/RELEASE_NOTES.md @@ -5,15 +5,15 @@ * Added `TryGetItemAsync` (same as `GetItemAsync`, but returns `None`, instead of throwing, if an item is not present) * Switched test framework to Xunit, assertions to Unquote, runner to `dotnet test` * Clarified Creation/Verification APIs: - * Obsoleted `TableContext.Create` (replace with `TableContext.Scripting.Initialize`, `TableContext.CreateTableIfNotExistsAsync`, `TableContext.VerifyTableAsync`) + * Obsoleted `TableContext.Create` (replace with `TableContext.Scripting.Initialize`, `TableContext.VerifyOrCreateTableAsync`, `TableContext.VerifyTableAsync`) * Added `TableContext` constructor (replaces `TableContext.Create(verifyTable = false)`) * Added `TableContext.Scripting.Initialize` (replaces `TableContext.Create()`) * Added `TableContext.VerifyTableAsync` overload that only performs verification but never creates a Table - * Added `TableContext.CreateTableIfNotExistsAsync` (replaces `TableContext.VerifyTableAsync(createIfNotExists = true)`) - * Added `TableContext.ProvisionTableAsync` (as per `CreateTableIfNotExistsAsync` but does an `UpdateTableAsync` if `throughput` or `streaming` has changed) - * Added Support for `Throughput.OnDemand` mode (sets `BillingMode` to `PAY_PER_REQUEST` rather than attempting to configure a `ProvisionedThroughput`) - * Added ability to configure DynamoDB streaming (via `Streaming` DU) to `CreateTableIfNotExistsAsync` and `ProvisionTableAsync` - * Removed `TableContext.CreateAsync` (replace with `TableContext.VerifyTableAsync` or `CreateTableIfNotExistsAsync`) + * Added `TableContext.VerifyOrCreateTableAsync` (replaces `TableContext.VerifyTableAsync(createIfNotExists = true)`) + * Added `TableContext.UpdateTableIfRequiredAsync` (conditional `UpdateTableAsync` to establish specified `throughput` or `streaming` only if required) + * Added `Throughput.OnDemand` mode (sets `BillingMode` to `PAY_PER_REQUEST` rather than attempting to configure a `ProvisionedThroughput`) + * Added ability to configure DynamoDB streaming (via `Streaming` DU) to `VerifyOrCreateTableAsync` and `UpdateTableIfRequiredAsync` + * Removed `TableContext.CreateAsync` (replace with `TableContext.VerifyTableAsync` or `VerifyOrCreateTableAsync`) * Replaced `TableKeySchemata.CreateCreateTableRequest` with `ApplyToCreateTableRequest` ### 0.9.3-beta diff --git a/src/FSharp.AWS.DynamoDB/Script.fsx b/src/FSharp.AWS.DynamoDB/Script.fsx index 5ac9325..b08e6a6 100644 --- a/src/FSharp.AWS.DynamoDB/Script.fsx +++ b/src/FSharp.AWS.DynamoDB/Script.fsx @@ -128,7 +128,7 @@ type EasyCounters private (table : TableContext) = // Create the table if necessary. Verifies schema is correct if it has already been created // NOTE the hard coded initial throughput provisioning - arguably this belongs outside of your application logic let throughput = ProvisionedThroughput(readCapacityUnits = 10L, writeCapacityUnits = 10L) - do! table.CreateTableIfNotExistsAsync(Throughput.Provisioned throughput) + do! table.VerifyOrCreateTableAsync(Throughput.Provisioned throughput) return EasyCounters(table) } @@ -138,16 +138,21 @@ type EasyCounters private (table : TableContext) = /// Variant of EasyCounters that splits the provisioning step from the (optional) validation that the table is present type SimpleCounters private (table : TableContext) = - static member Provision(client : IAmazonDynamoDB, tableName : string, readCapacityUnits, writeCapacityUnits) : Async = + static member Provision(client : IAmazonDynamoDB, tableName : string, readCapacityUnits, writeCapacityUnits) : Async = 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 `ProvisionTableAsync` instead of `CreateTableIfNotExistsAsync` to reset it each time we deploy the app let provisionedThroughput = ProvisionedThroughput(readCapacityUnits, writeCapacityUnits) - table.ProvisionTableAsync(Throughput.Provisioned provisionedThroughput) + let throughput = Throughput.Provisioned provisionedThroughput + // normally, RCU/WCU provisioning only happens first time the Table is created and is then considered an external concern + // here we use `UpdateTableIfRequiredAsync` to reset it each time we deploy the app + do! table.VerifyOrCreateTableAsync(throughput) + do! table.UpdateTableIfRequiredAsync(throughput) } - static member ProvisionOnDemand(client : IAmazonDynamoDB, tableName : string) : Async = + static member ProvisionOnDemand(client : IAmazonDynamoDB, tableName : string) : Async = async { let table = TableContext(client, tableName) - table.ProvisionTableAsync(Throughput.OnDemand) + let throughput = Throughput.OnDemand + do! table.VerifyOrCreateTableAsync(throughput) + // as per the Provision, above, we reset to OnDemand, if it got reconfigured since it was originally created + do! table.UpdateTableIfRequiredAsync(throughput) } /// 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 diff --git a/src/FSharp.AWS.DynamoDB/TableContext.fs b/src/FSharp.AWS.DynamoDB/TableContext.fs index 1a03eff..975604e 100644 --- a/src/FSharp.AWS.DynamoDB/TableContext.fs +++ b/src/FSharp.AWS.DynamoDB/TableContext.fs @@ -106,7 +106,8 @@ module internal UpdateTableRequest = Some request | tc, sc, Some customize -> apply tc sc request - customize request + if customize request then Some request + else None let execute (client : IAmazonDynamoDB) request : Async = async { let! ct = Async.CancellationToken @@ -115,16 +116,20 @@ module internal UpdateTableRequest = module internal Provisioning = - let private describe (client : IAmazonDynamoDB, tableName : string) : Async = + let tryDescribe (client : IAmazonDynamoDB, tableName : string) : Async = async { + let! ct = Async.CancellationToken + let! td = client.DescribeTableAsync(tableName, ct) |> Async.AwaitTaskCorrect + return match td.Table with t when t.TableStatus = TableStatus.ACTIVE -> Some t | _ -> None + } + + let private waitForActive (client : IAmazonDynamoDB, tableName : string) : 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 + match! tryDescribe (client, tableName) with + | Some t -> return t + | None -> + do! Async.Sleep 1000 // wait indefinitely if table is in transition state return! wait () - else - return td.Table } wait () @@ -136,7 +141,7 @@ module internal Provisioning = let private checkOrCreate (client, tableName) validateDescription maybeMakeCreateTableRequest : Async = let rec aux retries = async { - match! describe (client, tableName) |> Async.Catch with + match! waitForActive (client, tableName) |> Async.Catch with | Choice1Of2 desc -> validateDescription desc return desc @@ -166,16 +171,16 @@ module internal Provisioning = tableName existingSchema typeof<'TRecord> |> invalidOp - let private run (client, tableName, template) maybeMakeCreateRequest : Async = + let private run (client, tableName, template) maybeMakeCreateRequest : Async = let validate = validateDescription (tableName, template) - checkOrCreate (client, tableName) validate maybeMakeCreateRequest + checkOrCreate (client, tableName) validate maybeMakeCreateRequest |> Async.Ignore - let createOrValidate (client, tableName, template) throughput streaming customize : Async = + let verifyOrCreate (client, tableName, template) throughput streaming customize : Async = let generateCreateRequest () = CreateTableRequest.create (tableName, template) throughput streaming customize run (client, tableName, template) (Some generateCreateRequest) - let validateOnly (client, tableName, template) = - run (client, tableName, template) None |> Async.Ignore + let validateOnly (client, tableName, template) : Async = + run (client, tableName, template) None /// Represents the operation performed on the table, for metrics collection purposes type Operation = GetItem | PutItem | UpdateItem | DeleteItem | BatchGetItems | BatchWriteItems | Scan | Query @@ -420,7 +425,7 @@ type TableContext<'TRecord> internal /// /// Creates a DynamoDB client instance for given F# record and table name.
- /// For creating, provisioning or verification, see CreateTableIfNotExistsAsync and VerifyTableAsync. + /// For creating, provisioning or verification, see VerifyOrCreateTableAsync and VerifyTableAsync. ///
/// DynamoDB client instance. /// Table name to target. @@ -1015,46 +1020,49 @@ type TableContext<'TRecord> internal } - /// - /// Asynchronously verify that the table exists and is compatible with record key schema, or throw.
- /// See also CreateTableIfNotExistsAsync, which performs the same check, but can create or re-provision the Table if required. - ///
- member _.VerifyTableAsync() : Async = - Provisioning.validateOnly (client, tableName, template) - - member internal _.InternalCreateOrValidate(?throughput, ?streaming, ?customize) : Async = - Provisioning.createOrValidate (client, tableName, template) throughput streaming customize - /// /// 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 created, with the specified throughput.
+ /// If the table is not present, it is created, with the specified throughput (and optionally streaming) configuration.
/// See also VerifyTableAsync, which only verifies the Table is present and correct.
- /// See also ProvisionTableAsync, which will adjust throughput and streaming if they are not as specified. + /// See also UpdateTableIfRequiredAsync, which will adjust throughput and streaming if they are not as specified. ///
/// Throughput configuration to use for the table. /// Optional streaming configuration to apply for the table. Default: Disabled.. /// Callback to post-process the CreateTableRequest. - member t.CreateTableIfNotExistsAsync(throughput : Throughput, ?streaming, ?customize) : Async = - t.InternalCreateOrValidate(throughput, ?streaming = streaming, ?customize = customize) |> Async.Ignore + member t.VerifyOrCreateTableAsync(throughput : Throughput, ?streaming, ?customize) : Async = + Provisioning.verifyOrCreate (client, tableName, template) (Some throughput) streaming customize /// - /// 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 and optionally streaming.
- /// If it is present, and the throughput or streaming are not as specified, uses UpdateTableAsync to adjust.
+ /// Asynchronously verify that the table exists and is compatible with record key schema, or throw.
+ /// See also VerifyOrCreateTableAsync, which performs the same check, but can create or re-provision the Table if required. + ///
+ member _.VerifyTableAsync() : Async = + Provisioning.validateOnly (client, tableName, template) + + /// + /// Adjusts the Table's configuration via UpdateTable if the throughput or streaming are not as specified.
+ /// NOTE: The underlying API can throw if a change is currently in progress; see the DynamoDB UpdateTable API documentation.
+ /// NOTE: Throws InvalidOperationException if the table is not yet Active. It is recommended to ensure the Table is prepared via VerifyTableAsync or VerifyOrCreateTableAsync to guard against the potential for this state. ///
/// Throughput configuration to use for the table. Always applied via either CreateTable or UpdateTable. /// Optional streaming configuration to apply for the table. Default (if creating): Disabled. Default: (if existing) do not change. - /// Callback to post-process the CreateTableRequest if desired. - /// Callback to post-process the UpdateTableRequest if desired. When supplied, UpdateTable is inhibited if it returns None. - member t.ProvisionTableAsync(throughput : Throughput, ?streaming, ?customizeCreate, ?customizeUpdate) : Async = async { - let! tableDescription = t.InternalCreateOrValidate(throughput, ?streaming = streaming, ?customize = customizeCreate) - match UpdateTableRequest.createIfRequired tableName tableDescription (Some throughput) streaming customizeUpdate with + /// Callback to post-process the UpdateTableRequest. UpdateTable is inhibited if it returns false and no other configuration requires a change. + /// Current table configuration, if known. Retrieved via DescribeTable if not supplied. + member t.UpdateTableIfRequiredAsync(?throughput : Throughput, ?streaming, ?custom, ?currentTableDescription : TableDescription) : Async = async { + let! tableDescription = async { + match currentTableDescription with + | Some d -> return d + | None -> + match! Provisioning.tryDescribe (client, tableName) with + | Some d -> return d + | None -> return invalidOp "Table is not currently Active. Please use VerifyTableAsync or VerifyOrCreateTableAsync to guard against this state." } + match UpdateTableRequest.createIfRequired tableName tableDescription throughput streaming custom with | None -> () | Some request -> do! UpdateTableRequest.execute client request } /// /// Asynchronously updates the underlying table with supplied configuration.
- /// Underlying processing will throw if none of the options represent a change.
+ /// NOTE: The underlying API can throw if none the options represent a change or a change is in currently progress; see the DynamoDB UpdateTable API documentation. ///
/// Optional Throughput configuration to apply. /// Optional Streaming configuration to apply. @@ -1067,24 +1075,24 @@ type TableContext<'TRecord> internal /// Asynchronously updates the underlying table with supplied provisioned throughput. /// Provisioned throughput to use on table. - [] + [] member t.UpdateProvisionedThroughputAsync(provisionedThroughput : ProvisionedThroughput) : Async = t.UpdateTableAsync(Throughput.Provisioned provisionedThroughput) /// Asynchronously verify that the table exists and is compatible with record key schema. /// Create the table instance now instance if it does not exist. Defaults to false. /// Provisioned throughput for the table if newly created. Defaults to (10,10). - [] + [] member t.VerifyTableAsync(?createIfNotExists : bool, ?provisionedThroughput : ProvisionedThroughput) : Async = if createIfNotExists = Some true then let throughput = match provisionedThroughput with Some p -> p | None -> ProvisionedThroughput(10L, 10L) - t.CreateTableIfNotExistsAsync(Throughput.Provisioned throughput) + t.VerifyOrCreateTableAsync(Throughput.Provisioned throughput) else t.VerifyTableAsync() // Deprecated factory method, to be removed. Replaced with // 1. TableContext<'T> ctor (synchronous) -// 2. CreateTableIfNotExistsAsync OR VerifyTableAsync (explicitly async to signify that verification/creation is a costly and/or privileged operation) +// 2. VerifyOrCreateTableAsync OR VerifyTableAsync (explicitly async to signify that verification/creation is a costly and/or privileged operation) type TableContext internal () = /// @@ -1097,7 +1105,7 @@ type TableContext internal () = /// Provisioned throughput for the table if newly created. Default: 10 RCU, 10 WCU /// Function to receive request metrics. [] static member Create<'TRecord> ( client : IAmazonDynamoDB, tableName : string, ?verifyTable : bool, @@ -1106,7 +1114,7 @@ type TableContext internal () = let context = TableContext<'TRecord>(client, tableName, ?metricsCollector = metricsCollector) if createIfNotExists = Some true then let throughput = match provisionedThroughput with Some p -> p | None -> ProvisionedThroughput(10L, 10L) - do! context.CreateTableIfNotExistsAsync(Throughput.Provisioned throughput) + do! context.VerifyOrCreateTableAsync(Throughput.Provisioned throughput) elif verifyTable <> Some false then do! context.VerifyTableAsync() return context } @@ -1132,7 +1140,7 @@ module Scripting = let context = TableContext<'TRecord>(client, tableName, ?metricsCollector = metricsCollector) match throughput with | None -> context.VerifyTableAsync() |> Async.RunSynchronously - | Some t -> context.CreateTableIfNotExistsAsync(t) |> Async.RunSynchronously + | Some t -> context.VerifyOrCreateTableAsync(t) |> Async.RunSynchronously context type TableContext<'TRecord> with