Skip to content

Commit

Permalink
Add todo load test
Browse files Browse the repository at this point in the history
  • Loading branch information
bartelink committed Dec 11, 2018
1 parent 4edc86c commit 4b4390f
Show file tree
Hide file tree
Showing 25 changed files with 224 additions and 149 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
20 changes: 12 additions & 8 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,9 +46,11 @@ 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("{F2A71F9B-5D33-465A-A702-920D77279786}") = "TodoBackend", "samples\TodoBackend\TodoBackend.fsproj", "{EC2EC658-3D85-44F3-AD2F-52AFCAFF8871}"
Project("{6EC3EE1D-3C4E-46DD-8F32-0CC8E7565705}") = "TodoBackend", "samples\TodoBackend\TodoBackend.fsproj", "{EC2EC658-3D85-44F3-AD2F-52AFCAFF8871}"
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Samples.Todo", "Samples.Todo", "{CEFE6686-FD35-445D-975A-D622BF009B0B}"
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
Expand Down Expand Up @@ -117,13 +119,15 @@ Global
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}
{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} = {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}
{EC2EC658-3D85-44F3-AD2F-52AFCAFF8871} = {CEFE6686-FD35-445D-975A-D622BF009B0B}
{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
13 changes: 7 additions & 6 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 @@ -87,16 +88,16 @@ Run, including running the tests that assume you've got a local EventStore and p

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

- being a starting point for users to see how one might consume the libraries.
- 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 rough, but not official guidance as to things that are valid to do in an application consuming Equinox components.
- 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/TodoBackend -S es # run against eventstore, omit `es` to use in-memory store, or see PROVISIONING EVENTSTORE, below
& 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
Expand Down Expand Up @@ -137,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
9 changes: 6 additions & 3 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 All @@ -39,6 +41,7 @@
<PackageReference Include="FSharp.Core" Version="4.0.0.1" Condition=" '$(TargetFramework)' == 'net461' " />
<PackageReference Include="FSharp.Core" Version="4.3.4" Condition=" '$(TargetFramework)' == 'netcoreapp2.1' " />
<PackageReference Include="MathNet.Numerics" Version="4.6.0" />
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="2.1.1" />
<PackageReference Include="Serilog.Sinks.Console" Version="3.1.1" />
<PackageReference Include="Serilog.Sinks.Seq" Version="4.0.0" />
<PackageReference Include="System.Reactive" Version="4.0.0" />
Expand All @@ -47,7 +50,7 @@
<!-- 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 -->
<Target Name="CopyProjectReferencesToPackage" DependsOnTargets="ResolveReferences">
<ItemGroup>
<BuildOutputInPackage Include="@(ReferenceCopyLocalPaths -> WithMetadataValue('ReferenceSourceTarget', 'ProjectReference'))" />
<BuildOutputInPackage Include="@(ReferenceCopyLocalPaths -&gt; WithMetadataValue('ReferenceSourceTarget', 'ProjectReference'))" />
</ItemGroup>
</Target>

Expand Down
18 changes: 15 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 All @@ -177,6 +180,15 @@ module HttpHelpers =
request.Headers.Add(name, value)
request

///// Appends supplied MediaType to request accept header
//let inline withAcceptMediaType (mediaType : string) (request : HttpRequestMessage) =
// request.Headers.Accept.Add(new Headers.MediaTypeWithQualityHeaderValue(mediaType))
// request

///// Includes Accept: application/json in headers
//let inline withAcceptJson (request : HttpRequestMessage) =
// request |> withAcceptMediaType "application/json"

type HttpContent with
member c.ReadAsString() = async {
match c with
Expand Down Expand Up @@ -301,6 +313,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"); First; 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
49 changes: 44 additions & 5 deletions cli/Equinox.Cli/Tests.fs
Original file line number Diff line number Diff line change
@@ -1,11 +1,31 @@
module Equinox.Cli.Tests

open Domain
open Infrastructure
open Microsoft.Extensions.DependencyInjection
open System
open System.Net.Http
open System.Text

type Test = Favorite | SaveForLater
type Test = 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 =
StringBuilder.Build (fun x ->
while x.Length < len do
let req = len - x.Length
x.Append(if req >= seed.Length then seed else seed.Substring(0, req)) |> ignore)
let private guard = obj()
let private rnd = Random()
let rlipsum rlen =
let actualSize = lock guard (fun () -> rnd.Next(1,rlen))
lipsum actualSize
type TodoClient.Todo with
static member Create(size) : TodoClient.Todo =
{ id = 0; url = null; order = 0; title = rlipsum size; completed = false }
type TodoBackend.Events.Todo with
static member Create(size) : TodoBackend.Events.Todo =
{ id = 0; order = 0; title = rlipsum size; completed = false }

let executeLocal (container: ServiceProvider) test: ClientId -> Async<unit> =
match test with
Expand All @@ -29,12 +49,21 @@ let executeLocal (container: ServiceProvider) test: ClientId -> Async<unit> =
let resolveSkus _hasSku = async {
return [|for x in current -> x.skuId|] }
return! service.Remove(clientId, resolveSkus) }

| Todo size ->
let service = container.GetRequiredService<TodoBackend.Service>()
fun clientId -> async {
let! items = service.List(clientId)
if Seq.length items > 1000 then
do! service.Execute(clientId, TodoBackend.Command.Clear)
else
let! _ = service.Create(clientId,TodoBackend.Events.Todo.Create size)
return ()}

let executeRemote (client: HttpClient) test =
match test with
| Favorite ->
fun clientId ->
let session = Clients.Session(client, clientId)
let session = StoreClient.Session(client, clientId)
let client = session.Favorites
async {
let sku = Guid.NewGuid() |> SkuId
Expand All @@ -43,7 +72,7 @@ let executeRemote (client: HttpClient) test =
if items |> Array.exists (fun x -> x.skuId = sku) |> not then invalidOp "Added item not found" }
| SaveForLater ->
fun clientId ->
let session = Clients.Session(client, clientId)
let session = StoreClient.Session(client, clientId)
let client = session.Saves
async {
let skus = [| Guid.NewGuid() |> SkuId; Guid.NewGuid() |> SkuId; Guid.NewGuid() |> SkuId |]
Expand All @@ -54,4 +83,14 @@ let executeRemote (client: HttpClient) test =
if skus |> Array.forall (fun sku -> items |> Array.exists (fun x -> x.skuId = sku)) |> not then invalidOp "Added item not found"
else
let! current = client.List
return! client.Remove [|for x in current -> x.skuId|] }
return! client.Remove [|for x in current -> x.skuId|] }
| Todo size ->
fun clientId -> async {
let session = TodoClient.Session(client, clientId)
let client = session.Todos
let! items = client.List()
if Seq.length items > 1000 then
do! client.Clear()
else
let! _ = client.Add(TodoClient.Todo.Create size)
return () }

0 comments on commit 4b4390f

Please sign in to comment.