From cf1c9117d530aace0c04aeac341639be62f19aa0 Mon Sep 17 00:00:00 2001 From: Ruben Bartelink Date: Mon, 28 Mar 2022 14:47:13 +0100 Subject: [PATCH] Readme updates and extended examples --- README.md | 42 +++++++----- src/FSharp.AWS.DynamoDB/Script.fsx | 85 ++++++++++++++++++++++++- src/FSharp.AWS.DynamoDB/TableContext.fs | 6 +- 3 files changed, 114 insertions(+), 19 deletions(-) diff --git a/README.md b/README.md index ab1eb73..67ff6ff 100644 --- a/README.md +++ b/README.md @@ -34,9 +34,11 @@ We can now perform table operations on DynamoDB like so: ```fsharp open Amazon.DynamoDBv2 +open FSharp.AWS.DynamoDB.Scripting // Expose non-Async methods, e.g. PutItem/GetItem let client : IAmazonDynamoDB = ``your DynamoDB client instance`` -let table = TableContext.Create(client, tableName = "workItems", createIfNotExists = true) +let autoCreate = InitializationMode.CreateIfNotExists (ProvisionedThroughput(10, 10)) +let table = TableContext.Initialize(client, tableName = "workItems", mode = autoCreate) let workItem = { ProcessId = 0L ; WorkItemId = 1L ; Name = "Test" ; UUID = guid() ; Dependencies = set ["mscorlib"] ; Started = None } @@ -99,24 +101,34 @@ Update expressions support the following F# value constructors: * `Option.Value` and `Option.get`. * `fst` and `snd` for tuple records. -## Example: Creating an atomic counter +## Example: Representing an atomic counter as an Item in a DynamoDB Table ```fsharp type private CounterEntry = { [] Id : Guid ; Value : int64 } type Counter private (table : TableContext, key : TableKey) = - member _.Value = table.GetItem(key).Value - member _.Incr() = - let updated = table.UpdateItem(key, <@ fun e -> { e with Value = e.Value + 1L } @>) - updated.Value - - static member Create(client : IAmazonDynamoDB, table : string) = - let table = TableContext.Create(client, table, createIfNotExists = true) - let entry = { Id = Guid.NewGuid() ; Value = 0L } - let key = table.PutItem entry - new Counter(table, key) + + member _.Value = async { + let! current = table.GetItemAsync(key) + return current.Value + } + + member _.Incr() = async { + let! updated = table.UpdateItemAsync(key, <@ fun e -> { e with Value = e.Value + 1L } @>) + return updated.Value + } + + static member Create(client : IAmazonDynamoDB, tableName : string) = async { + let table = TableContext(client, tableName) + do! table.InitializeTableAsync( ProvisionedThroughput(100L, 100L)) + let initialEntry = { Id = Guid.NewGuid() ; Value = 0L } + let! key = table.PutItemAsync(initialEntry) + return Counter(table, key) + } ``` +_NOTE: It's advised to split single time initialization/verification of table creation from the application logic, see [`Script.fsx`](src/FSharp.AWS.DynamoDB/Script.fsx#99) for further details_. + ## Projection Expressions Projection expressions can be used to fetch a subset of table attributes, which can be useful when performing large queries: @@ -233,10 +245,10 @@ table.Scan(startedBefore (DateTimeOffset.Now - TimeSpan.FromDays 1.)) A hook is provided so metrics can be published via your preferred Observability provider. For example, using [Prometheus.NET](https://github.com/prometheus-net/prometheus-net): ```fsharp -let dbCounter = Metrics.CreateCounter ("aws_dynamodb_requests_total", "Count of all DynamoDB requests", "table", "operation") +let dbCounter = Prometheus.Metrics.CreateCounter("aws_dynamodb_requests_total", "Count of all DynamoDB requests", "table", "operation") let processMetrics (m : RequestMetrics) = - dbCounter.WithLabels(m.TableName, string m.Operation).Inc () |> ignore -let table = TableContext.Create(client, tableName = "workItems", metricsCollector = processMetrics) + dbCounter.WithLabels(m.TableName, string m.Operation).Inc() +let table = TableContext(client, tableName = "workItems", metricsCollector = processMetrics) ``` If `metricsCollector` is supplied, the requests will include `ReturnConsumedCapacity = ReturnConsumedCapacity.INDEX` diff --git a/src/FSharp.AWS.DynamoDB/Script.fsx b/src/FSharp.AWS.DynamoDB/Script.fsx index 84d6eea..8e7ffcc 100644 --- a/src/FSharp.AWS.DynamoDB/Script.fsx +++ b/src/FSharp.AWS.DynamoDB/Script.fsx @@ -54,8 +54,8 @@ type Test = Bytes : byte[] } - -let table = TableContext.Create(ddb, "test", createIfNotExists = true) +let autoCreate = InitializationMode.CreateIfNotExists (ProvisionedThroughput (100L, 100L)) +let table = TableContext.Initialize(ddb, "test", mode = autoCreate) let value = { HashKey = Guid.NewGuid() ; List = [] ; RangeKey = "2" ; Value = 3.1415926 ; Date = DateTimeOffset.Now + TimeSpan.FromDays 2. ; Value2 = None ; Values = [|{ A = "foo" ; B = System.Reflection.BindingFlags.Instance }|] ; Map = Map.ofList [("A1",1)] ; Set = [set [1L];set [2L]] ; Bytes = [|1uy..10uy|]; String = ref "1a" ; Unions = [A 42; B("42",3)]} @@ -94,3 +94,84 @@ let uexpr2 = table.Template.PrecomputeUpdateExpr <@ fun v r -> { r with Value2 = for i = 1 to 1000 do let _ = table.UpdateItem(key, uexpr2 (Some 42)) () + +(* Expanded version of README sample that illustrates how one can better split Table initialization from application logic *) + +type internal CounterEntry = { [] Id : Guid ; Value : int64 } + +/// Represents a single Item in a Counters Table +type Counter internal (table : TableContext, key : TableKey) = + + static member internal Start(table : TableContext) = async { + let initialEntry = { Id = Guid.NewGuid() ; Value = 0L } + let! key = table.PutItemAsync(initialEntry) + return Counter(table, key) + } + + member _.Value = async { + let! current = table.GetItemAsync(key) + return current.Value + } + + member _.Incr() = async { + let! updated = table.UpdateItemAsync(key, <@ fun (e : CounterEntry) -> { e with Value = e.Value + 1L } @>) + return updated.Value + } + +/// Wrapper that creates/verifies the table only once per call to Create() +/// This does assume that your application will be sufficiently privileged to create tables on the fly +type EasyCounters private (table : TableContext) = + + // We only want to do the initialization bit once per instance of our application + 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)) + return EasyCounters(table) + } + + member _.StartCounter() : Async = + Counter.Start table + +/// 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 = + 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) + + /// 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 + /// However it will never actually create the table + static member CreateWithVerify(client : IAmazonDynamoDB, tableName : string) : Async = async { + let table = TableContext(client, tableName) + // This validates the Table has been created correctly + // (in general this is a good idea, but it is an optional step so it can be skipped, i.e. see Create() below) + do! table.VerifyTableAsync() + return SimpleCounters(table) + } + + /// Assumes the table has been provisioned externally via Provision() + static member Create(client : IAmazonDynamoDB, tableName : string) : SimpleCounters = + // NOTE we are skipping + SimpleCounters(TableContext(client, tableName)) + + member _.StartCounter() : Async = + Counter.Start table + +let e = EasyCounters.Create(ddb, "testing") |> Async.RunSynchronously +let e1 = e.StartCounter() |> Async.RunSynchronously +let e2 = e.StartCounter() |> Async.RunSynchronously +e1.Incr() |> Async.RunSynchronously +e2.Incr() |> Async.RunSynchronously + +SimpleCounters.Provision(ddb, "testing-pre-provisioned", 100L, 100L) |> Async.RunSynchronously +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 + +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 diff --git a/src/FSharp.AWS.DynamoDB/TableContext.fs b/src/FSharp.AWS.DynamoDB/TableContext.fs index 6f4d516..8354819 100644 --- a/src/FSharp.AWS.DynamoDB/TableContext.fs +++ b/src/FSharp.AWS.DynamoDB/TableContext.fs @@ -944,7 +944,9 @@ type TableContext<'TRecord> internal |> invalidOp match mode with | InitializationMode.VerifyOnly | InitializationMode.CreateIfNotExists _ -> () - | InitializationMode.CreateOrUpdateThroughput t -> do! __.UpdateProvisionedThroughputAsync(t) + | InitializationMode.CreateOrUpdateThroughput t -> + // TODO make this not throw when its a null update + do! __.UpdateProvisionedThroughputAsync(t) | Choice2Of2 (:? ResourceNotFoundException) when mode <> InitializationMode.VerifyOnly -> let throughput = @@ -1022,7 +1024,7 @@ type TableContext internal () = /// Table name to target. /// Verify that the table exists and is compatible with supplied record schema. Defaults to true. /// Create the table now if it does not exist. Defaults to false. - /// Provisioned throughput for the table if newly created. + /// Provisioned throughput for the table if newly created. Default: 10 RCU, 10 WCU /// Function to receive request metrics. [