Skip to content

Commit

Permalink
Add TodoBackend Web Api implementation and load test (#63)
Browse files Browse the repository at this point in the history
* Add TodoBackend web impl
* Add todo load test
  • Loading branch information
bartelink committed Dec 11, 2018
1 parent 8bfd61e commit 6baf3a1
Show file tree
Hide file tree
Showing 23 changed files with 352 additions and 47 deletions.
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ msbuild.*

# Visual Studio 2015+ cache/options directory
.vs/
*.fsproj.user

# ReSharper is a .NET coding add-in
_ReSharper*/
Expand Down
49 changes: 31 additions & 18 deletions Equinox.sln
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ VisualStudioVersion = 15.0.27110.0
MinimumVisualStudioVersion = 10.0.40219.1
Project("{6EC3EE1D-3C4E-46DD-8F32-0CC8E7565705}") = "Equinox", "src\Equinox\Equinox.fsproj", "{54CD058F-5B0A-4564-B732-1F6301E120AC}"
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Samples.Store", "Samples.Store", "{D67D5A5F-2E59-4514-A997-FEBDC467AAF6}"
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Store", "Store", "{D67D5A5F-2E59-4514-A997-FEBDC467AAF6}"
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = ".project", ".project", "{7E3A7020-AE75-4513-B81A-57FD3E0D0D84}"
ProjectSection(SolutionItems) = preProject
Expand All @@ -28,11 +28,11 @@ Project("{6EC3EE1D-3C4E-46DD-8F32-0CC8E7565705}") = "Domain.Tests", "samples\Sto
EndProject
Project("{6EC3EE1D-3C4E-46DD-8F32-0CC8E7565705}") = "Backend", "samples\Store\Backend\Backend.fsproj", "{0D98B603-2EBB-40C2-AFE5-083C92EFB822}"
EndProject
Project("{6EC3EE1D-3C4E-46DD-8F32-0CC8E7565705}") = "Infrastructure", "samples\Store\Infrastructure\Infrastructure.fsproj", "{ACE52D04-2FE3-4FD6-A066-9C81429C3997}"
Project("{6EC3EE1D-3C4E-46DD-8F32-0CC8E7565705}") = "Infrastructure", "samples\Infrastructure\Infrastructure.fsproj", "{ACE52D04-2FE3-4FD6-A066-9C81429C3997}"
EndProject
Project("{6EC3EE1D-3C4E-46DD-8F32-0CC8E7565705}") = "Integration", "samples\Store\Integration\Integration.fsproj", "{0B2D5815-D6A5-4AAC-9B75-D57B165E2A92}"
EndProject
Project("{6EC3EE1D-3C4E-46DD-8F32-0CC8E7565705}") = "Web", "samples\Store\Web\Web.fsproj", "{1B0D4568-96FD-4083-8520-CD537C0B2FF0}"
Project("{6EC3EE1D-3C4E-46DD-8F32-0CC8E7565705}") = "Web", "samples\Web\Web.fsproj", "{1B0D4568-96FD-4083-8520-CD537C0B2FF0}"
EndProject
Project("{6EC3EE1D-3C4E-46DD-8F32-0CC8E7565705}") = "Equinox.EventStore", "src\Equinox.EventStore\Equinox.EventStore.fsproj", "{92B4ACC9-7F30-4727-A4B6-0B6903D0AA08}"
EndProject
Expand All @@ -46,6 +46,12 @@ Project("{6EC3EE1D-3C4E-46DD-8F32-0CC8E7565705}") = "Equinox.Codec", "src\Equino
EndProject
Project("{6EC3EE1D-3C4E-46DD-8F32-0CC8E7565705}") = "Equinox.Cli", "cli\Equinox.Cli\Equinox.Cli.fsproj", "{C8992C1C-6DC5-42CD-A3D7-1C5663433FED}"
EndProject
Project("{6EC3EE1D-3C4E-46DD-8F32-0CC8E7565705}") = "TodoBackend", "samples\TodoBackend\TodoBackend.fsproj", "{EC2EC658-3D85-44F3-AD2F-52AFCAFF8871}"
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Samples", "Samples", "{8F3EB30C-8BA3-4CC0-8361-0EA47C19ABB9}"
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Todo", "Todo", "{8CDE1CC3-8619-44DE-8B4D-4102CE476C35}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Expand All @@ -60,18 +66,26 @@ Global
{37B4A45F-039E-4515-8A84-D242DDE12D22}.Debug|Any CPU.Build.0 = Debug|Any CPU
{37B4A45F-039E-4515-8A84-D242DDE12D22}.Release|Any CPU.ActiveCfg = Release|Any CPU
{37B4A45F-039E-4515-8A84-D242DDE12D22}.Release|Any CPU.Build.0 = Release|Any CPU
{0D98B603-2EBB-40C2-AFE5-083C92EFB822}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{0D98B603-2EBB-40C2-AFE5-083C92EFB822}.Debug|Any CPU.Build.0 = Debug|Any CPU
{0D98B603-2EBB-40C2-AFE5-083C92EFB822}.Release|Any CPU.ActiveCfg = Release|Any CPU
{0D98B603-2EBB-40C2-AFE5-083C92EFB822}.Release|Any CPU.Build.0 = Release|Any CPU
{406A280E-0708-4B12-8443-8FD5660CD271}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{406A280E-0708-4B12-8443-8FD5660CD271}.Debug|Any CPU.Build.0 = Debug|Any CPU
{406A280E-0708-4B12-8443-8FD5660CD271}.Release|Any CPU.ActiveCfg = Release|Any CPU
{406A280E-0708-4B12-8443-8FD5660CD271}.Release|Any CPU.Build.0 = Release|Any CPU
{0D98B603-2EBB-40C2-AFE5-083C92EFB822}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{0D98B603-2EBB-40C2-AFE5-083C92EFB822}.Debug|Any CPU.Build.0 = Debug|Any CPU
{0D98B603-2EBB-40C2-AFE5-083C92EFB822}.Release|Any CPU.ActiveCfg = Release|Any CPU
{0D98B603-2EBB-40C2-AFE5-083C92EFB822}.Release|Any CPU.Build.0 = Release|Any CPU
{ACE52D04-2FE3-4FD6-A066-9C81429C3997}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{ACE52D04-2FE3-4FD6-A066-9C81429C3997}.Debug|Any CPU.Build.0 = Debug|Any CPU
{ACE52D04-2FE3-4FD6-A066-9C81429C3997}.Release|Any CPU.ActiveCfg = Release|Any CPU
{ACE52D04-2FE3-4FD6-A066-9C81429C3997}.Release|Any CPU.Build.0 = Release|Any CPU
{0B2D5815-D6A5-4AAC-9B75-D57B165E2A92}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{0B2D5815-D6A5-4AAC-9B75-D57B165E2A92}.Debug|Any CPU.Build.0 = Debug|Any CPU
{0B2D5815-D6A5-4AAC-9B75-D57B165E2A92}.Release|Any CPU.ActiveCfg = Release|Any CPU
{0B2D5815-D6A5-4AAC-9B75-D57B165E2A92}.Release|Any CPU.Build.0 = Release|Any CPU
{1B0D4568-96FD-4083-8520-CD537C0B2FF0}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{1B0D4568-96FD-4083-8520-CD537C0B2FF0}.Debug|Any CPU.Build.0 = Debug|Any CPU
{1B0D4568-96FD-4083-8520-CD537C0B2FF0}.Release|Any CPU.ActiveCfg = Release|Any CPU
{1B0D4568-96FD-4083-8520-CD537C0B2FF0}.Release|Any CPU.Build.0 = Release|Any CPU
{92B4ACC9-7F30-4727-A4B6-0B6903D0AA08}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{92B4ACC9-7F30-4727-A4B6-0B6903D0AA08}.Debug|Any CPU.Build.0 = Debug|Any CPU
{92B4ACC9-7F30-4727-A4B6-0B6903D0AA08}.Release|Any CPU.ActiveCfg = Release|Any CPU
Expand All @@ -96,25 +110,24 @@ Global
{C8992C1C-6DC5-42CD-A3D7-1C5663433FED}.Debug|Any CPU.Build.0 = Debug|Any CPU
{C8992C1C-6DC5-42CD-A3D7-1C5663433FED}.Release|Any CPU.ActiveCfg = Release|Any CPU
{C8992C1C-6DC5-42CD-A3D7-1C5663433FED}.Release|Any CPU.Build.0 = Release|Any CPU
{1B0D4568-96FD-4083-8520-CD537C0B2FF0}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{1B0D4568-96FD-4083-8520-CD537C0B2FF0}.Debug|Any CPU.Build.0 = Debug|Any CPU
{1B0D4568-96FD-4083-8520-CD537C0B2FF0}.Release|Any CPU.ActiveCfg = Release|Any CPU
{1B0D4568-96FD-4083-8520-CD537C0B2FF0}.Release|Any CPU.Build.0 = Release|Any CPU
{ACE52D04-2FE3-4FD6-A066-9C81429C3997}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{ACE52D04-2FE3-4FD6-A066-9C81429C3997}.Debug|Any CPU.Build.0 = Debug|Any CPU
{ACE52D04-2FE3-4FD6-A066-9C81429C3997}.Release|Any CPU.ActiveCfg = Release|Any CPU
{ACE52D04-2FE3-4FD6-A066-9C81429C3997}.Release|Any CPU.Build.0 = Release|Any CPU
{EC2EC658-3D85-44F3-AD2F-52AFCAFF8871}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{EC2EC658-3D85-44F3-AD2F-52AFCAFF8871}.Debug|Any CPU.Build.0 = Debug|Any CPU
{EC2EC658-3D85-44F3-AD2F-52AFCAFF8871}.Release|Any CPU.ActiveCfg = Release|Any CPU
{EC2EC658-3D85-44F3-AD2F-52AFCAFF8871}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
EndGlobalSection
GlobalSection(NestedProjects) = preSolution
{D67D5A5F-2E59-4514-A997-FEBDC467AAF6} = {8F3EB30C-8BA3-4CC0-8361-0EA47C19ABB9}
{37B4A45F-039E-4515-8A84-D242DDE12D22} = {D67D5A5F-2E59-4514-A997-FEBDC467AAF6}
{0D98B603-2EBB-40C2-AFE5-083C92EFB822} = {D67D5A5F-2E59-4514-A997-FEBDC467AAF6}
{406A280E-0708-4B12-8443-8FD5660CD271} = {D67D5A5F-2E59-4514-A997-FEBDC467AAF6}
{0D98B603-2EBB-40C2-AFE5-083C92EFB822} = {D67D5A5F-2E59-4514-A997-FEBDC467AAF6}
{ACE52D04-2FE3-4FD6-A066-9C81429C3997} = {8F3EB30C-8BA3-4CC0-8361-0EA47C19ABB9}
{0B2D5815-D6A5-4AAC-9B75-D57B165E2A92} = {D67D5A5F-2E59-4514-A997-FEBDC467AAF6}
{1B0D4568-96FD-4083-8520-CD537C0B2FF0} = {D67D5A5F-2E59-4514-A997-FEBDC467AAF6}
{ACE52D04-2FE3-4FD6-A066-9C81429C3997} = {D67D5A5F-2E59-4514-A997-FEBDC467AAF6}
{1B0D4568-96FD-4083-8520-CD537C0B2FF0} = {8F3EB30C-8BA3-4CC0-8361-0EA47C19ABB9}
{EC2EC658-3D85-44F3-AD2F-52AFCAFF8871} = {8CDE1CC3-8619-44DE-8B4D-4102CE476C35}
{8CDE1CC3-8619-44DE-8B4D-4102CE476C35} = {8F3EB30C-8BA3-4CC0-8361-0EA47C19ABB9}
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {177E1E7B-E275-4FC6-AE3C-2C651ECCF71E}
Expand Down
38 changes: 34 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,8 @@ The Equinox components within this repository are delivered as a series of multi
- `Equinox.EventStore` (Nuget: `Equinox.EventStore`, depends on `EventStore.Client[Api.NetCore] >= 4`, `System.Runtime.Caching`, `FSharp.Control.AsyncSeq`, `TypeShape`): Production-strength [EventStore](http://geteventstore.com) Adapter instrumented to the degree necessitated by Jet's production monitoring requirements
- `Equinox.MemoryStore` (Nuget: `Equinox.MemoryStore`): In-memory store for integration testing/performance baselining/providing out-of-the-box zero dependency storage for examples.
- `samples/Store` (in this repo): Example domain types reflecting examples of how one applies Equinox to a diverse set of stream-based models
- `Equinox.Cli` (in this repo): General purpose tool incorporating a scenario runner that facilitates running representative load tests composed of transactions in `samples/Store` against each backend store; this allows perf tuning and measurement in terms of both latency and transaction charge aspects.
- `samples/TodoBackend` (in this repo): Standard https://todobackend.com compliant backend
- `Equinox.Cli` (in this repo): General purpose tool incorporating a scenario runner that facilitates running representative load tests composed of transactions in `samples/Store` and `samples/TodoBackend` against each backend store; this allows perf tuning and measurement in terms of both latency and transaction charge aspects.

# CONTRIBUTING

Expand Down Expand Up @@ -83,9 +84,38 @@ Run, including running the tests that assume you've got a local EventStore and p

./build -se

# SAMPLES

The `samples/` folder contains various examples, with the complementary goals of:

- being a starting point to see how one might consume the libraries.
- acting as [Consumer Driven Contracts](https://martinfowler.com/articles/consumerDrivenContracts.html) to validate and pin API designs.
- providing outline (not official and complete) guidance as to things that are valid to do in an application consuming Equinox components.
- to validate that each specific Storage implementation can fulfill the needs of each of the example Services/Aggregates/Applications. (_unfortunately this concern makes a lot of the DI wiring more complex than a real application should be; it's definitely not a goal for every Equinox app to be able to switch between backends, even though that's very much possible to achieve._)

## [TODOBACKEND, see samples/TodoBackend](/samples/TodoBackend)

The repo contains a vanilla ASP.NET Core 2.1 implemention of [the well-known TodoBackend Spec](https://www.todobackend.com). **NB the implementation is largely dictated by spec; no architectural guidance expressed or implied ;)**. It can be run via:

& dotnet run -f netcoreapp2.1 -p samples/Web -S es # run against eventstore, omit `es` to use in-memory store, or see PROVISIONING EVENTSTORE, below
start https://www.todobackend.com/specs/index.html?https://localhost:5001/todos # for low-level debugging / validation of hosting arrangements
start https://www.todobackend.com/client/index.html?https://localhost:5001/todos # Actual UI
start http://localhost:5341/#/events # see logs triggered by `-S` above in https://getseq.net

## [STORE, see /samples/Store](/samples/Store)

The core sample in this repo is the `Store` sample, which contains code and tests extracted from real implementations (with minor simplifications in some cases).

These facts mean that:

- some of the code may be less than approachable for a beginner (e.g. some of the code is written is in its present form for reasons of efficiency)
- some of the code may not represent official best practice guidance that the authors would necessarily stand over (e.g., the CQRS pattern is not strictly adhered to in all circumstances; some command designs are not completely correct from the point of view of making sense from an idempotency perspective)

While these things can of course be perfected through PRs, this is definitely not top of the TODO list for the purposes of this repo. (We'd be happy to link to other samples, including cleanups / rewrites of these samples written with different testing platforms, web platforms, or DDD/CQRS/ES design flavors).

# BENCHMARKS

A key facility of this repo is beoing able to run load tests, either in process against a nominated store, or via HTTP to a nominated Web app. The following tests are implemented at present:
A key facility of this repo is being able to run load tests, either in process against a nominated store, or via HTTP to a nominated Web app. The following tests are implemented at present:

- `Favorite` - Simulate a very enthusiastic user that favorites things once per Second - triggering an ever-growing state which can only work efficiently if you:
- apply a snapshotting scheme (although being unbounded, it will eventually hit the store's limits - 4MB/event for EventStore, 3MB/document for CosmosDb)
Expand All @@ -108,11 +138,11 @@ At present, .NET Core seems to show comparable perf under normal load, but becom

## run Web benchmark

The CLI can drive the Store/Web ASP.NET Core app. Doing so requires starting a web process with an appropriate store (EventStore in this example, but can be `memory`/omitted etc. as in the other examples)
The CLI can drive the Store and TodoBackend samples in the `samples/Web` ASP.NET Core app. Doing so requires starting a web process with an appropriate store (EventStore in this example, but can be `memory`/omitted etc. as in the other examples)

### in Window 1

& dotnet run -c Release -f netcoreapp2.1 -p samples/Store/Web -- -C -U es
& dotnet run -c Release -f netcoreapp2.1 -p samples/Web -- -C -U es

### in Window 2

Expand Down
6 changes: 4 additions & 2 deletions cli/Equinox.Cli/Equinox.Cli.fsproj
Original file line number Diff line number Diff line change
Expand Up @@ -20,17 +20,19 @@
<Compile Include="Infrastructure\Aggregate.fs" />
<Compile Include="Infrastructure\LoadTestRunner.fs" />
<Compile Include="Infrastructure\LocalLoadTestRunner.fs" />
<Compile Include="Clients.fs" />
<Compile Include="StoreClient.fs" />
<Compile Include="TodoClient.fs" />
<Compile Include="Tests.fs" />
<Compile Include="Program.fs" />
</ItemGroup>

<ItemGroup>
<!-- workaround for not being able to make Backend and Domain as inlined in a complete way https://github.com/nuget/home/issues/3891#issuecomment-377319939 -->
<ProjectReference Include="..\..\samples\TodoBackend\TodoBackend.fsproj" />
<ProjectReference Include="..\..\src\Equinox.MemoryStore\Equinox.MemoryStore.fsproj" />
<ProjectReference Include="..\..\samples\Store\Backend\Backend.fsproj" PrivateAssets="all" />
<ProjectReference Include="..\..\samples\Store\Domain\Domain.fsproj" PrivateAssets="all" />
<ProjectReference Include="..\..\samples\Store\Infrastructure\Infrastructure.fsproj" PrivateAssets="all" />
<ProjectReference Include="..\..\samples\Infrastructure\Infrastructure.fsproj" PrivateAssets="all" />
</ItemGroup>

<ItemGroup>
Expand Down
9 changes: 6 additions & 3 deletions cli/Equinox.Cli/Infrastructure/Infrastructure.fs
Original file line number Diff line number Diff line change
Expand Up @@ -143,7 +143,10 @@ module HttpHelpers =
/// Creates an HTTP POST request.
let inline post () = create () |> withMethod HttpMethod.Post

/// Creates an HTTP DELET request.
/// Creates an HTTP PATCH request.
let inline patch () = create () |> withMethod (HttpMethod "PATCH")

/// Creates an HTTP DELETE request.
let inline delete () = create () |> withMethod HttpMethod.Delete

/// Assigns a path to an HTTP request.
Expand All @@ -168,7 +171,7 @@ module HttpHelpers =
let withJson (serialize : 'Request -> string) (input : 'Request) (request : HttpRequestMessage) =
request |> withJsonString (serialize input)

/// Use Batman Json.Net profile convert the request to a json rendering
/// Use standard Json.Net profile convert the request to a json rendering
let withJsonNet<'Request> (input : 'Request) (request : HttpRequestMessage) =
request |> withJson Newtonsoft.Json.JsonConvert.SerializeObject input

Expand Down Expand Up @@ -301,6 +304,6 @@ module HttpHelpers =
let deserializeExpectedJsonNet<'t> expectedStatusCode (res : HttpResponseMessage) =
res.Interpret(expectedStatusCode, Newtonsoft.Json.JsonConvert.DeserializeObject<'t>)

/// Deserialize body using Batman Json.Net profile - throw with content details if StatusCode is not OK or decoding fails
/// Deserialize body using default Json.Net profile - throw with content details if StatusCode is not OK or decoding fails
let deserializeOkJsonNet<'t> =
deserializeExpectedJsonNet<'t> HttpStatusCode.OK
11 changes: 9 additions & 2 deletions cli/Equinox.Cli/Program.fs
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,8 @@ and [<NoComparison>]WebArguments =
| Endpoint _ -> "Target address. Default: https://localhost:5001"
and [<NoComparison>]
TestArguments =
| [<AltCommandLine("-t"); First; Unique>] Name of Tests.Test
| [<AltCommandLine("-t"); Unique>] Name of Test
| [<AltCommandLine("-s")>] Size of int
| [<AltCommandLine("-C")>] Cached
| [<AltCommandLine("-U")>] Unfolds
| [<AltCommandLine("-f")>] TestsPerSecond of int
Expand All @@ -45,6 +46,7 @@ and [<NoComparison>]
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)."
Expand All @@ -54,6 +56,7 @@ and [<NoComparison>]
| Memory _ -> "target in-process Transient Memory Store (Default if not other target specified)."
| Es _ -> "Run transactions in-process against EventStore."
| Web _ -> "Run transactions against a Web endpoint."
and Test = Favorite | SaveForLater | Todo

let createStoreLog verbose verboseConsole maybeSeqEndpoint =
let c = LoggerConfiguration().Destructure.FSharpTypes()
Expand Down Expand Up @@ -98,7 +101,11 @@ module LoadTest =
| _ | Some (Memory _) ->
log.Warning("Running transactions in-process against Volatile Store with storage options: {options:l}", options)
createStoreLog false, MemoryStore.config () |> Some, None
let test = args.GetResult(Name,Tests.Favorite)
let test =
match args.GetResult(Name,Favorite) with
| Favorite -> Tests.Favorite
| SaveForLater -> Tests.SaveForLater
| Todo -> Tests.Todo (args.GetResult(Size,100))
let runSingleTest : ClientId -> Async<unit> =
match storeConfig, httpClient with
| None, Some client ->
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
module Equinox.Cli.Clients
module Equinox.Cli.StoreClient

open Domain
open Equinox.Cli.Infrastructure
Expand Down

0 comments on commit 6baf3a1

Please sign in to comment.