Skip to content

Commit

Permalink
refactor(Tool)!: run->loadTest, config->initSql (#445)
Browse files Browse the repository at this point in the history
  • Loading branch information
bartelink authored Feb 21, 2024
1 parent 79e546a commit f298a56
Show file tree
Hide file tree
Showing 11 changed files with 363 additions and 367 deletions.
2 changes: 1 addition & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ The `Unreleased` section name is replaced by the expected version of next releas
- `MessageDb`: Implements a [message-db](http://docs.eventide-project.org/user-guide/message-db/) storage backend [#339](https://github.com/jet/equinox/pull/339) with OpenTelemetry tracing and snapshotting support [#348](https://github.com/jet/equinox/pull/348) :pray: [@nordfjord](https://github.com/nordfjord)
- `eqx init --indexUnfolds cosmos`: enables querying uncompressed unfolds (see `shouldCompress`) [#434](https://github.com/jet/equinox/pull/434)
- `eqx query cosmos`: Queries based on uncompressed unfolds (see `eqx init -U`) [#434](https://github.com/jet/equinox/pull/434)
- `eqx query -m raw -o FILEPATH cosmos`: Allows capture of raw JSON to a file [#444](https://github.com/jet/equinox/pull/444)
- `eqx query -o FILEPATH cosmos`: Allows capture of raw JSON to a file [#444](https://github.com/jet/equinox/pull/444)
- `eqx dump`: `-s` flag is now optional
- `eqx stats`: `-A` flag to request all stats (equivalent to requesting `-ESD`) [#424](https://github.com/jet/equinox/pull/424)

Expand Down
62 changes: 31 additions & 31 deletions README.md

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion samples/Infrastructure/Infrastructure.fsproj
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@
<ItemGroup>
<PackageReference Include="FSharp.Core" Version="6.0.7" ExcludeAssets="contentfiles" />

<PackageReference Include="Argu" Version="6.1.4" />
<PackageReference Include="Argu" Version="6.2.1" />
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="6.0.0" />
</ItemGroup>

Expand Down
4 changes: 3 additions & 1 deletion samples/Store/Domain/Infrastructure.fs
Original file line number Diff line number Diff line change
Expand Up @@ -86,7 +86,9 @@ module CartId = let toString (value: CartId): string = Guid.toStringN %value
/// ClientId strongly typed id; represented internally as a Guid; not used for storage so rendering is not significant
type ClientId = Guid<clientId>
and [<Measure>] clientId
module ClientId = let toString (value: ClientId): string = Guid.toStringN %value
module ClientId =
let gen (): ClientId = Guid.gen () |> UMX.tag
let toString (value: ClientId): string = Guid.toStringN %value

/// InventoryItemId strongly typed id
type InventoryItemId = Guid<inventoryItemId>
Expand Down
6 changes: 3 additions & 3 deletions samples/Web/Program.fs
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ let createWebHostBuilder (args, parsed) : IWebHostBuilder =
[<EntryPoint>]
let main argv =
try printfn "Running at pid %d" (System.Diagnostics.Process.GetCurrentProcess().Id)
let p = Arguments.parse argv
let p = Arguments.Parse argv
// Replace logger chain with https://github.com/serilog/serilog-aspnetcore
let c =
LoggerConfiguration()
Expand All @@ -39,5 +39,5 @@ let main argv =
createWebHostBuilder(argv, p).Build().Run()
0
finally Log.CloseAndFlush()
with :? ArguParseException as e -> eprintfn "%s" e.Message; 1
| e -> eprintfn "EXCEPTION: %O" e; 1
with :? ArguParseException as e -> eprintfn $"%s{e.Message}"; 1
| e -> eprintfn $"EXCEPTION: %O{e}"; 1
5 changes: 1 addition & 4 deletions samples/Web/Startup.fs
Original file line number Diff line number Diff line change
Expand Up @@ -36,10 +36,7 @@ type Arguments =
| MySql _ -> "specify storage in MySql (--help for options)."
| Postgres _ -> "specify storage in Postgres (--help for options)."
| Memory _ -> "specify In-Memory Volatile Store (Default store)."
module Arguments =
let parse argv =
let programName = System.Reflection.Assembly.GetEntryAssembly().GetName().Name
ArgumentParser.Create<Arguments>(programName = programName).ParseCommandLine(argv)
static member Parse argv = ArgumentParser.Create<Arguments>().ParseCommandLine(argv)

type App = class end

Expand Down
2 changes: 1 addition & 1 deletion tools/Equinox.Tool/Infrastructure/Infrastructure.fs
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
[<AutoOpen>]
module internal Equinox.Tool.Infrastructure.Prelude
module Equinox.Tool.Infrastructure

open Equinox.Tools.TestHarness.HttpHelpers
open System
Expand Down
488 changes: 168 additions & 320 deletions tools/Equinox.Tool/Program.fs

Large diffs are not rendered by default.

1 change: 0 additions & 1 deletion tools/Equinox.Tool/StoreClient.fs
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
module Equinox.Tool.StoreClient

open Equinox.Tool.Infrastructure
open System
open System.Net
open System.Net.Http
Expand Down
155 changes: 153 additions & 2 deletions tools/Equinox.Tool/Tests.fs
Original file line number Diff line number Diff line change
@@ -1,13 +1,14 @@
module Equinox.Tool.Tests

open Domain
open Equinox.Tool.Infrastructure
open Microsoft.Extensions.DependencyInjection
open Serilog
open System
open System.Net.Http
open System.Text
open System.Threading

type Test = Favorite | SaveForLater | Todo of size: int
type TestKind = Favorite | SaveForLater | Todo of size: int

let [<Literal>] seed = "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Donec a diam lectus. Sed sit amet ipsum mauris. Maecenas congue ligula ac quam viverra nec consectetur ante hendrerit. Donec et mollis dolor. Praesent et diam eget libero egestas mattis sit amet vitae augue. Nam tincidunt congue enim, ut porta lorem lacinia consectetur! "
let lipsum len =
Expand Down Expand Up @@ -93,3 +94,153 @@ let executeRemote (client: HttpClient) test =
else
let! _ = client.Add(TodoClient.Todo.Create size)
return () }

module LoadTest =

open Argu
open Samples.Infrastructure

type [<NoComparison; NoEquality; RequireSubcommand>] LoadTestParameters =
| [<AltCommandLine "-t"; Unique>] Name of Test
| [<AltCommandLine "-s">] Size of int
| [<AltCommandLine "-C">] Cached
| [<AltCommandLine "-U">] Unfolds
| [<AltCommandLine "-f">] TestsPerSecond of int
| [<AltCommandLine "-d">] DurationM of float
| [<AltCommandLine "-e">] ErrorCutoff of int64
| [<AltCommandLine "-i">] ReportIntervalS of int
| [<CliPrefix(CliPrefix.None); Last>] Cosmos of ParseResults<Store.Cosmos.Parameters>
| [<CliPrefix(CliPrefix.None); Last>] Dynamo of ParseResults<Store.Dynamo.Parameters>
| [<CliPrefix(CliPrefix.None); Last>] Es of ParseResults<Store.EventStore.Parameters>
| [<CliPrefix(CliPrefix.None); Last>] Memory of ParseResults<Store.MemoryStore.Parameters>
| [<CliPrefix(CliPrefix.None); Last; AltCommandLine "ms">] MsSql of ParseResults<Store.Sql.Ms.Parameters>
| [<CliPrefix(CliPrefix.None); Last; AltCommandLine "my">] MySql of ParseResults<Store.Sql.My.Parameters>
| [<CliPrefix(CliPrefix.None); Last; AltCommandLine "pg">] Postgres of ParseResults<Store.Sql.Pg.Parameters>
| [<CliPrefix(CliPrefix.None); Last>] Mdb of ParseResults<Store.MessageDb.Parameters>
| [<CliPrefix(CliPrefix.None); Last>] Web of ParseResults<WebParameters>
interface IArgParserTemplate with
member a.Usage = a |> function
| Name _ -> "specify which test to run. (default: Favorite)."
| Size _ -> "For `-t Todo`: specify random title length max size to use (default 100)."
| Cached -> "employ a 50MB cache, wire in to Stream configuration."
| Unfolds -> "employ a store-appropriate Rolling Snapshots and/or Unfolding strategy."
| TestsPerSecond _ -> "specify a target number of requests per second (default: 1000)."
| DurationM _ -> "specify a run duration in minutes (default: 30)."
| ErrorCutoff _ -> "specify an error cutoff; test ends when exceeded (default: 10000)."
| ReportIntervalS _ -> "specify reporting intervals in seconds (default: 10)."
| Es _ -> "Run transactions in-process against EventStore."
| Cosmos _ -> "Run transactions in-process against CosmosDB."
| Dynamo _ -> "Run transactions in-process against DynamoDB."
| Memory _ -> "target in-process Transient Memory Store (Default if not other target specified)."
| MsSql _ -> "Run transactions in-process against Sql Server."
| MySql _ -> "Run transactions in-process against MySql."
| Postgres _ -> "Run transactions in-process against Postgres."
| Mdb _ -> "Run transactions in-process against MessageDB."
| Web _ -> "Run transactions against a Web endpoint."
and [<NoComparison>] WebParameters =
| [<AltCommandLine "-u">] Endpoint of string
interface IArgParserTemplate with
member a.Usage = a |> function
| Endpoint _ -> "Target address. Default: https://localhost:5001"
and Test = Favorite | SaveForLater | Todo
and TestArguments(p : ParseResults<LoadTestParameters>) =
member val Options = p.GetResults Cached @ p.GetResults Unfolds
member x.Cache = x.Options |> List.exists (function Cached -> true | _ -> false)
member x.Unfolds = x.Options |> List.exists (function Unfolds -> true | _ -> false)
member val Test = p.GetResult(Name, Test.Favorite)
member val ErrorCutoff = p.GetResult(ErrorCutoff, 10000L)
member val TestsPerSecond = p.GetResult(TestsPerSecond, 1000)
member val Duration = p.GetResult(DurationM, 30.) |> TimeSpan.FromMinutes
member x.ReportingIntervals = match p.GetResults(ReportIntervalS) with
| [] -> TimeSpan.FromSeconds 10.|> Seq.singleton
| intervals -> seq { for i in intervals -> TimeSpan.FromSeconds(float i) }
|> fun intervals -> [| yield x.Duration; yield! intervals |]
member x.ConfigureStore(log : ILogger, createStoreLog) =
let cache = if x.Cache then Equinox.Cache("dummy", sizeMb = 50) |> Some else None
match p.GetSubCommand() with
| Cosmos p -> let storeLog = createStoreLog <| p.Contains Store.Cosmos.Parameters.StoreVerbose
log.Information("Running transactions in-process against CosmosDB with storage options: {options:l}", x.Options)
storeLog, Store.Cosmos.config log (cache, x.Unfolds) (Store.Cosmos.Arguments p)
| Dynamo p -> let storeLog = createStoreLog <| p.Contains Store.Dynamo.Parameters.StoreVerbose
log.Information("Running transactions in-process against DynamoDB with storage options: {options:l}", x.Options)
storeLog, Store.Dynamo.config log (cache, x.Unfolds) (Store.Dynamo.Arguments p)
| Es p -> let storeLog = createStoreLog <| p.Contains Store.EventStore.Parameters.StoreVerbose
log.Information("Running transactions in-process against EventStore with storage options: {options:l}", x.Options)
storeLog, Store.EventStore.config log (cache, x.Unfolds) p
| MsSql p -> let storeLog = createStoreLog false
log.Information("Running transactions in-process against MsSql with storage options: {options:l}", x.Options)
storeLog, Store.Sql.Ms.config log (cache, x.Unfolds) p
| MySql p -> let storeLog = createStoreLog false
log.Information("Running transactions in-process against MySql with storage options: {options:l}", x.Options)
storeLog, Store.Sql.My.config log (cache, x.Unfolds) p
| Postgres p -> let storeLog = createStoreLog false
log.Information("Running transactions in-process against Postgres with storage options: {options:l}", x.Options)
storeLog, Store.Sql.Pg.config log (cache, x.Unfolds) p
| Mdb p -> let storeLog = createStoreLog false
log.Information("Running transactions in-process against MessageDb with storage options: {options:l}", x.Options)
storeLog, Store.MessageDb.config log cache p
| Memory _ -> log.Warning("Running transactions in-process against Volatile Store with storage options: {options:l}", x.Options)
createStoreLog false, Store.MemoryStore.config ()
| x -> p.Raise $"unexpected subcommand %A{x}"
member _.TestKind = match p.GetResult(Name, Favorite) with
| Favorite -> TestKind.Favorite
| SaveForLater -> TestKind.SaveForLater
| Todo -> TestKind.Todo (p.GetResult(Size, 100))

let private runLoadTest log testsPerSecond duration errorCutoff reportingIntervals (clients : ClientId[]) runSingleTest =
let mutable idx = -1L
let selectClient () =
let clientIndex = Interlocked.Increment(&idx) |> int
clients[clientIndex % clients.Length]
let selectClient = async { return async { return selectClient() } }
Equinox.Tools.TestHarness.Local.runLoadTest log reportingIntervals testsPerSecond errorCutoff duration selectClient runSingleTest
let private decorateWithLogger (domainLog : ILogger, verbose) (run: 't -> Async<unit>) =
let execute clientId =
if not verbose then run clientId
else async {
domainLog.Information("Executing for client {sessionId}", clientId)
try return! run clientId
with e -> domainLog.Warning(e, "Test threw an exception"); e.Reraise () }
execute
let private createResultLog fileName = LoggerConfiguration().WriteTo.File(fileName).CreateLogger()
let run (log : ILogger) (createStoreLog, verbose, verboseConsole) (resetStats, dumpStats) reportFilename (p : ParseResults<LoadTestParameters>) =
let a = TestArguments p
let storeLog, storeConfig, httpClient: ILogger * Store.Config option * HttpClient option =
match p.GetSubCommand() with
| Web p ->
let uri = p.GetResult(WebParameters.Endpoint,"https://localhost:5001") |> Uri
log.Information("Running web test targeting: {url}", uri)
createStoreLog false, None, new HttpClient(BaseAddress = uri) |> Some
| _ ->
let storeLog, storeConfig = a.ConfigureStore(log, createStoreLog)
storeLog, Some storeConfig, None
let test, duration = a.TestKind, a.Duration
let runSingleTest : ClientId -> Async<unit> =
match storeConfig, httpClient with
| None, Some client ->
let execForClient = executeRemote client test
decorateWithLogger (log, verbose) execForClient
| Some storeConfig, _ ->
let services = ServiceCollection()
Services.register(services, storeConfig, storeLog)
let container = services.BuildServiceProvider()
let execForClient = executeLocal container test
decorateWithLogger (log, verbose) execForClient
| None, None -> invalidOp "impossible None, None"
let clients = Array.init (a.TestsPerSecond * 2) (fun _ -> ClientId.gen())

let renderedIds = clients |> Seq.map ClientId.toString |> if verboseConsole then id else Seq.truncate 5
log.ForContext((if verboseConsole then "clientIds" else "clientIdsExcerpt"),renderedIds)
.Information("Running {test} for {duration} @ {tps} hits/s across {clients} clients; Max errors: {errorCutOff}, reporting intervals: {ri}, report file: {report}",
test, a.Duration, a.TestsPerSecond, clients.Length, a.ErrorCutoff, a.ReportingIntervals, reportFilename)

resetStats ()

let results = runLoadTest log a.TestsPerSecond (duration.Add(TimeSpan.FromSeconds 5.)) a.ErrorCutoff a.ReportingIntervals clients runSingleTest |> Async.RunSynchronously

let resultFile = createResultLog reportFilename
for r in results do
resultFile.Information("Aggregate: {aggregate}", r)
log.Information("Run completed; Current memory allocation: {bytes:n2} MiB", (GC.GetTotalMemory(true) |> float) / 1024./1024.)

storeConfig |> Option.iter (dumpStats log)
3 changes: 1 addition & 2 deletions tools/Equinox.Tool/TodoClient.fs
Original file line number Diff line number Diff line change
@@ -1,9 +1,8 @@
module Equinox.Tool.TodoClient

open Equinox.Tool.Infrastructure
open System
open System.Net
open System.Net.Http
open System

type Todo = { id: int; url: string; order: int; title: string; completed: bool }

Expand Down

0 comments on commit f298a56

Please sign in to comment.