diff --git a/src/Http/Wolverine.Http.Tests/Internal/Generated/WolverineHandlers/GET_maybe.cs b/src/Http/Wolverine.Http.Tests/Internal/Generated/WolverineHandlers/GET_maybe.cs new file mode 100644 index 000000000..64e1dd6cf --- /dev/null +++ b/src/Http/Wolverine.Http.Tests/Internal/Generated/WolverineHandlers/GET_maybe.cs @@ -0,0 +1,52 @@ +// +#pragma warning disable +using Microsoft.AspNetCore.Routing; +using System; +using System.Linq; +using Wolverine.Http; +using Wolverine.Runtime; + +namespace Internal.Generated.WolverineHandlers +{ + // START: GET_maybe + [global::System.CodeDom.Compiler.GeneratedCode("JasperFx", "1.0.0")] + public sealed class GET_maybe : Wolverine.Http.HttpHandler + { + private readonly Wolverine.Http.WolverineHttpOptions _wolverineHttpOptions; + private readonly Wolverine.Runtime.IWolverineRuntime _wolverineRuntime; + + public GET_maybe(Wolverine.Http.WolverineHttpOptions wolverineHttpOptions, Wolverine.Runtime.IWolverineRuntime wolverineRuntime) : base(wolverineHttpOptions) + { + _wolverineHttpOptions = wolverineHttpOptions; + _wolverineRuntime = wolverineRuntime; + } + + + + public override async System.Threading.Tasks.Task Handle(Microsoft.AspNetCore.Http.HttpContext httpContext) + { + + // Tenant Id detection + // 1. Tenant Id is query string value 'tenant' + var tenantId = await TryDetectTenantId(httpContext); + var messageContext = new Wolverine.Runtime.MessageContext(_wolverineRuntime); + messageContext.TenantId = tenantId; + Wolverine.Http.Runtime.RequestIdMiddleware.Apply(httpContext, messageContext); + + // The actual HTTP request handler execution + var result_of_MaybeTenanted = Wolverine.Http.Tests.MultiTenancy.TenantedEndpoints.MaybeTenanted(messageContext); + + await WriteString(httpContext, result_of_MaybeTenanted); + + // Have to flush outgoing messages just in case Marten did nothing because of https://github.com/JasperFx/wolverine/issues/536 + await messageContext.FlushOutgoingMessagesAsync().ConfigureAwait(false); + + } + + } + + // END: GET_maybe + + +} + diff --git a/src/Http/Wolverine.Http.Tests/Internal/Generated/WolverineHandlers/GET_nottenanted.cs b/src/Http/Wolverine.Http.Tests/Internal/Generated/WolverineHandlers/GET_nottenanted.cs new file mode 100644 index 000000000..c7b555104 --- /dev/null +++ b/src/Http/Wolverine.Http.Tests/Internal/Generated/WolverineHandlers/GET_nottenanted.cs @@ -0,0 +1,38 @@ +// +#pragma warning disable +using Microsoft.AspNetCore.Routing; +using System; +using System.Linq; +using Wolverine.Http; + +namespace Internal.Generated.WolverineHandlers +{ + // START: GET_nottenanted + [global::System.CodeDom.Compiler.GeneratedCode("JasperFx", "1.0.0")] + public sealed class GET_nottenanted : Wolverine.Http.HttpHandler + { + private readonly Wolverine.Http.WolverineHttpOptions _wolverineHttpOptions; + + public GET_nottenanted(Wolverine.Http.WolverineHttpOptions wolverineHttpOptions) : base(wolverineHttpOptions) + { + _wolverineHttpOptions = wolverineHttpOptions; + } + + + + public override async System.Threading.Tasks.Task Handle(Microsoft.AspNetCore.Http.HttpContext httpContext) + { + + // The actual HTTP request handler execution + var result_of_NoTenantNoProblem = Wolverine.Http.Tests.MultiTenancy.TenantedEndpoints.NoTenantNoProblem(); + + await WriteString(httpContext, result_of_NoTenantNoProblem); + } + + } + + // END: GET_nottenanted + + +} + diff --git a/src/Http/Wolverine.Http.Tests/Internal/Generated/WolverineHandlers/GET_tenant.cs b/src/Http/Wolverine.Http.Tests/Internal/Generated/WolverineHandlers/GET_tenant.cs new file mode 100644 index 000000000..a7bdf1eff --- /dev/null +++ b/src/Http/Wolverine.Http.Tests/Internal/Generated/WolverineHandlers/GET_tenant.cs @@ -0,0 +1,53 @@ +// +#pragma warning disable +using Microsoft.AspNetCore.Routing; +using System; +using System.Linq; +using Wolverine.Http; +using Wolverine.Runtime; + +namespace Internal.Generated.WolverineHandlers +{ + // START: GET_tenant + [global::System.CodeDom.Compiler.GeneratedCode("JasperFx", "1.0.0")] + public sealed class GET_tenant : Wolverine.Http.HttpHandler + { + private readonly Wolverine.Http.WolverineHttpOptions _wolverineHttpOptions; + private readonly Wolverine.Runtime.IWolverineRuntime _wolverineRuntime; + + public GET_tenant(Wolverine.Http.WolverineHttpOptions wolverineHttpOptions, Wolverine.Runtime.IWolverineRuntime wolverineRuntime) : base(wolverineHttpOptions) + { + _wolverineHttpOptions = wolverineHttpOptions; + _wolverineRuntime = wolverineRuntime; + } + + + + public override async System.Threading.Tasks.Task Handle(Microsoft.AspNetCore.Http.HttpContext httpContext) + { + + // Tenant Id detection + // 1. Tenant Id is request header 'tenant' + var tenantId = await TryDetectTenantId(httpContext); + var tenantIdentifier = new JasperFx.MultiTenancy.TenantId(tenantId); + var messageContext = new Wolverine.Runtime.MessageContext(_wolverineRuntime); + messageContext.TenantId = tenantId; + Wolverine.Http.Runtime.RequestIdMiddleware.Apply(httpContext, messageContext); + + // The actual HTTP request handler execution + var result_of_GetTenantIdFromWhatever = Wolverine.Http.Tests.MultiTenancy.TenantedEndpoints.GetTenantIdFromWhatever(messageContext, httpContext, tenantIdentifier); + + await WriteString(httpContext, result_of_GetTenantIdFromWhatever); + + // Have to flush outgoing messages just in case Marten did nothing because of https://github.com/JasperFx/wolverine/issues/536 + await messageContext.FlushOutgoingMessagesAsync().ConfigureAwait(false); + + } + + } + + // END: GET_tenant + + +} + diff --git a/src/Http/Wolverine.Http.Tests/Internal/Generated/WolverineHandlers/GET_tenant_both_tenant.cs b/src/Http/Wolverine.Http.Tests/Internal/Generated/WolverineHandlers/GET_tenant_both_tenant.cs new file mode 100644 index 000000000..36f756b92 --- /dev/null +++ b/src/Http/Wolverine.Http.Tests/Internal/Generated/WolverineHandlers/GET_tenant_both_tenant.cs @@ -0,0 +1,59 @@ +// +#pragma warning disable +using Microsoft.AspNetCore.Routing; +using System; +using System.Linq; +using Wolverine.Http; +using Wolverine.Runtime; + +namespace Internal.Generated.WolverineHandlers +{ + // START: GET_tenant_both_tenant + [global::System.CodeDom.Compiler.GeneratedCode("JasperFx", "1.0.0")] + public sealed class GET_tenant_both_tenant : Wolverine.Http.HttpHandler + { + private readonly Wolverine.Http.WolverineHttpOptions _wolverineHttpOptions; + private readonly Wolverine.Runtime.IWolverineRuntime _wolverineRuntime; + + public GET_tenant_both_tenant(Wolverine.Http.WolverineHttpOptions wolverineHttpOptions, Wolverine.Runtime.IWolverineRuntime wolverineRuntime) : base(wolverineHttpOptions) + { + _wolverineHttpOptions = wolverineHttpOptions; + _wolverineRuntime = wolverineRuntime; + } + + + + public override async System.Threading.Tasks.Task Handle(Microsoft.AspNetCore.Http.HttpContext httpContext) + { + + // Tenant Id detection + // 1. Tenant Id is route argument named 'tenant' + var tenantId = await TryDetectTenantId(httpContext); + if (string.IsNullOrEmpty(tenantId)) + { + await WriteTenantIdNotFound(httpContext); + return; + } + + var tenantIdentifier = new JasperFx.MultiTenancy.TenantId(tenantId); + var messageContext = new Wolverine.Runtime.MessageContext(_wolverineRuntime); + messageContext.TenantId = tenantId; + Wolverine.Http.Runtime.RequestIdMiddleware.Apply(httpContext, messageContext); + + // The actual HTTP request handler execution + var result_of_GetTenantWithArgs1 = Wolverine.Http.Tests.MultiTenancy.TenantedEndpoints.GetTenantWithArgs1(messageContext, messageContext, tenantIdentifier); + + await WriteString(httpContext, result_of_GetTenantWithArgs1); + + // Have to flush outgoing messages just in case Marten did nothing because of https://github.com/JasperFx/wolverine/issues/536 + await messageContext.FlushOutgoingMessagesAsync().ConfigureAwait(false); + + } + + } + + // END: GET_tenant_both_tenant + + +} + diff --git a/src/Http/Wolverine.Http.Tests/Internal/Generated/WolverineHandlers/GET_tenant_bus_tenant.cs b/src/Http/Wolverine.Http.Tests/Internal/Generated/WolverineHandlers/GET_tenant_bus_tenant.cs new file mode 100644 index 000000000..8b4b1e6d9 --- /dev/null +++ b/src/Http/Wolverine.Http.Tests/Internal/Generated/WolverineHandlers/GET_tenant_bus_tenant.cs @@ -0,0 +1,59 @@ +// +#pragma warning disable +using Microsoft.AspNetCore.Routing; +using System; +using System.Linq; +using Wolverine.Http; +using Wolverine.Runtime; + +namespace Internal.Generated.WolverineHandlers +{ + // START: GET_tenant_bus_tenant + [global::System.CodeDom.Compiler.GeneratedCode("JasperFx", "1.0.0")] + public sealed class GET_tenant_bus_tenant : Wolverine.Http.HttpHandler + { + private readonly Wolverine.Http.WolverineHttpOptions _wolverineHttpOptions; + private readonly Wolverine.Runtime.IWolverineRuntime _wolverineRuntime; + + public GET_tenant_bus_tenant(Wolverine.Http.WolverineHttpOptions wolverineHttpOptions, Wolverine.Runtime.IWolverineRuntime wolverineRuntime) : base(wolverineHttpOptions) + { + _wolverineHttpOptions = wolverineHttpOptions; + _wolverineRuntime = wolverineRuntime; + } + + + + public override async System.Threading.Tasks.Task Handle(Microsoft.AspNetCore.Http.HttpContext httpContext) + { + + // Tenant Id detection + // 1. Tenant Id is route argument named 'tenant' + var tenantId = await TryDetectTenantId(httpContext); + if (string.IsNullOrEmpty(tenantId)) + { + await WriteTenantIdNotFound(httpContext); + return; + } + + var tenantIdentifier = new JasperFx.MultiTenancy.TenantId(tenantId); + var messageContext = new Wolverine.Runtime.MessageContext(_wolverineRuntime); + messageContext.TenantId = tenantId; + Wolverine.Http.Runtime.RequestIdMiddleware.Apply(httpContext, messageContext); + + // The actual HTTP request handler execution + var result_of_GetTenantWithArgs1 = Wolverine.Http.Tests.MultiTenancy.TenantedEndpoints.GetTenantWithArgs1(messageContext, tenantIdentifier); + + await WriteString(httpContext, result_of_GetTenantWithArgs1); + + // Have to flush outgoing messages just in case Marten did nothing because of https://github.com/JasperFx/wolverine/issues/536 + await messageContext.FlushOutgoingMessagesAsync().ConfigureAwait(false); + + } + + } + + // END: GET_tenant_bus_tenant + + +} + diff --git a/src/Http/Wolverine.Http.Tests/Internal/Generated/WolverineHandlers/GET_tenant_context_tenant.cs b/src/Http/Wolverine.Http.Tests/Internal/Generated/WolverineHandlers/GET_tenant_context_tenant.cs new file mode 100644 index 000000000..3f99fc9c9 --- /dev/null +++ b/src/Http/Wolverine.Http.Tests/Internal/Generated/WolverineHandlers/GET_tenant_context_tenant.cs @@ -0,0 +1,59 @@ +// +#pragma warning disable +using Microsoft.AspNetCore.Routing; +using System; +using System.Linq; +using Wolverine.Http; +using Wolverine.Runtime; + +namespace Internal.Generated.WolverineHandlers +{ + // START: GET_tenant_context_tenant + [global::System.CodeDom.Compiler.GeneratedCode("JasperFx", "1.0.0")] + public sealed class GET_tenant_context_tenant : Wolverine.Http.HttpHandler + { + private readonly Wolverine.Http.WolverineHttpOptions _wolverineHttpOptions; + private readonly Wolverine.Runtime.IWolverineRuntime _wolverineRuntime; + + public GET_tenant_context_tenant(Wolverine.Http.WolverineHttpOptions wolverineHttpOptions, Wolverine.Runtime.IWolverineRuntime wolverineRuntime) : base(wolverineHttpOptions) + { + _wolverineHttpOptions = wolverineHttpOptions; + _wolverineRuntime = wolverineRuntime; + } + + + + public override async System.Threading.Tasks.Task Handle(Microsoft.AspNetCore.Http.HttpContext httpContext) + { + + // Tenant Id detection + // 1. Tenant Id is route argument named 'tenant' + var tenantId = await TryDetectTenantId(httpContext); + if (string.IsNullOrEmpty(tenantId)) + { + await WriteTenantIdNotFound(httpContext); + return; + } + + var tenantIdentifier = new JasperFx.MultiTenancy.TenantId(tenantId); + var messageContext = new Wolverine.Runtime.MessageContext(_wolverineRuntime); + messageContext.TenantId = tenantId; + Wolverine.Http.Runtime.RequestIdMiddleware.Apply(httpContext, messageContext); + + // The actual HTTP request handler execution + var result_of_GetTenantWithArgs1 = Wolverine.Http.Tests.MultiTenancy.TenantedEndpoints.GetTenantWithArgs1(messageContext, tenantIdentifier); + + await WriteString(httpContext, result_of_GetTenantWithArgs1); + + // Have to flush outgoing messages just in case Marten did nothing because of https://github.com/JasperFx/wolverine/issues/536 + await messageContext.FlushOutgoingMessagesAsync().ConfigureAwait(false); + + } + + } + + // END: GET_tenant_context_tenant + + +} + diff --git a/src/Http/Wolverine.Http.Tests/Internal/Generated/WolverineHandlers/GET_tenant_route_tenant.cs b/src/Http/Wolverine.Http.Tests/Internal/Generated/WolverineHandlers/GET_tenant_route_tenant.cs new file mode 100644 index 000000000..4b9c766cd --- /dev/null +++ b/src/Http/Wolverine.Http.Tests/Internal/Generated/WolverineHandlers/GET_tenant_route_tenant.cs @@ -0,0 +1,54 @@ +// +#pragma warning disable +using Microsoft.AspNetCore.Routing; +using System; +using System.Linq; +using Wolverine.Http; +using Wolverine.Runtime; + +namespace Internal.Generated.WolverineHandlers +{ + // START: GET_tenant_route_tenant + [global::System.CodeDom.Compiler.GeneratedCode("JasperFx", "1.0.0")] + public sealed class GET_tenant_route_tenant : Wolverine.Http.HttpHandler + { + private readonly Wolverine.Http.WolverineHttpOptions _wolverineHttpOptions; + private readonly Wolverine.Runtime.IWolverineRuntime _wolverineRuntime; + + public GET_tenant_route_tenant(Wolverine.Http.WolverineHttpOptions wolverineHttpOptions, Wolverine.Runtime.IWolverineRuntime wolverineRuntime) : base(wolverineHttpOptions) + { + _wolverineHttpOptions = wolverineHttpOptions; + _wolverineRuntime = wolverineRuntime; + } + + + + public override async System.Threading.Tasks.Task Handle(Microsoft.AspNetCore.Http.HttpContext httpContext) + { + + // Tenant Id detection + // 1. Tenant Id is query string value 't' + // 2. Wolverine.Http.Runtime.MultiTenancy.FallbackDefault + var tenantId = await TryDetectTenantId(httpContext); + var tenantIdentifier = new JasperFx.MultiTenancy.TenantId(tenantId); + var messageContext = new Wolverine.Runtime.MessageContext(_wolverineRuntime); + messageContext.TenantId = tenantId; + Wolverine.Http.Runtime.RequestIdMiddleware.Apply(httpContext, messageContext); + + // The actual HTTP request handler execution + var result_of_GetTenantIdFromRoute = Wolverine.Http.Tests.MultiTenancy.TenantedEndpoints.GetTenantIdFromRoute(messageContext, tenantIdentifier); + + await WriteString(httpContext, result_of_GetTenantIdFromRoute); + + // Have to flush outgoing messages just in case Marten did nothing because of https://github.com/JasperFx/wolverine/issues/536 + await messageContext.FlushOutgoingMessagesAsync().ConfigureAwait(false); + + } + + } + + // END: GET_tenant_route_tenant + + +} + diff --git a/src/Http/Wolverine.Http.Tests/Internal/Generated/WolverineHandlers/GET_todo_id.cs b/src/Http/Wolverine.Http.Tests/Internal/Generated/WolverineHandlers/GET_todo_id.cs new file mode 100644 index 000000000..db0ae92cf --- /dev/null +++ b/src/Http/Wolverine.Http.Tests/Internal/Generated/WolverineHandlers/GET_todo_id.cs @@ -0,0 +1,69 @@ +// +#pragma warning disable +using Microsoft.AspNetCore.Routing; +using System; +using System.Linq; +using Wolverine.Http; +using Wolverine.Marten.Publishing; +using Wolverine.Runtime; + +namespace Internal.Generated.WolverineHandlers +{ + // START: GET_todo_id + [global::System.CodeDom.Compiler.GeneratedCode("JasperFx", "1.0.0")] + public sealed class GET_todo_id : Wolverine.Http.HttpHandler + { + private readonly Wolverine.Http.WolverineHttpOptions _wolverineHttpOptions; + private readonly Wolverine.Marten.Publishing.OutboxedSessionFactory _outboxedSessionFactory; + private readonly Wolverine.Runtime.IWolverineRuntime _wolverineRuntime; + + public GET_todo_id(Wolverine.Http.WolverineHttpOptions wolverineHttpOptions, Wolverine.Marten.Publishing.OutboxedSessionFactory outboxedSessionFactory, Wolverine.Runtime.IWolverineRuntime wolverineRuntime) : base(wolverineHttpOptions) + { + _wolverineHttpOptions = wolverineHttpOptions; + _outboxedSessionFactory = outboxedSessionFactory; + _wolverineRuntime = wolverineRuntime; + } + + + + public override async System.Threading.Tasks.Task Handle(Microsoft.AspNetCore.Http.HttpContext httpContext) + { + + // Tenant Id detection + // 1. Tenant Id is first sub domain name of the request host + // 2. Tenant Id is request header 'tenant' + // 3. Tenant Id is query string value 'tenant' + var tenantId = await TryDetectTenantId(httpContext); + if (string.IsNullOrEmpty(tenantId)) + { + await WriteTenantIdNotFound(httpContext); + return; + } + + var tenantIdentifier = new JasperFx.MultiTenancy.TenantId(tenantId); + var messageContext = new Wolverine.Runtime.MessageContext(_wolverineRuntime); + messageContext.TenantId = tenantId; + // Building the Marten session using the detected tenant id + await using var documentSession = _outboxedSessionFactory.OpenSession(messageContext, tenantId); + var id = (string?)httpContext.GetRouteValue("id"); + if(id == null) + { + httpContext.Response.StatusCode = 404; + return; + } + + + // The actual HTTP request handler execution + var tenantTodo_response = await Wolverine.Http.Tests.MultiTenancy.TenantedEndpoints.Get(id, ((Marten.IQuerySession)documentSession), tenantIdentifier).ConfigureAwait(false); + + // Writing the response body to JSON because this was the first 'return variable' in the method signature + await WriteJsonAsync(httpContext, tenantTodo_response); + } + + } + + // END: GET_todo_id + + +} + diff --git a/src/Http/Wolverine.Http.Tests/Internal/Generated/WolverineHandlers/POST_api_tenants_tenant_counters_id_inc.cs b/src/Http/Wolverine.Http.Tests/Internal/Generated/WolverineHandlers/POST_api_tenants_tenant_counters_id_inc.cs new file mode 100644 index 000000000..7b179dc7b --- /dev/null +++ b/src/Http/Wolverine.Http.Tests/Internal/Generated/WolverineHandlers/POST_api_tenants_tenant_counters_id_inc.cs @@ -0,0 +1,87 @@ +// +#pragma warning disable +using Microsoft.AspNetCore.Routing; +using System; +using System.Linq; +using Wolverine.Http; +using Wolverine.Marten.Publishing; +using Wolverine.Runtime; + +namespace Internal.Generated.WolverineHandlers +{ + // START: POST_api_tenants_tenant_counters_id_inc + [global::System.CodeDom.Compiler.GeneratedCode("JasperFx", "1.0.0")] + public sealed class POST_api_tenants_tenant_counters_id_inc : Wolverine.Http.HttpHandler + { + private readonly Wolverine.Http.WolverineHttpOptions _wolverineHttpOptions; + private readonly Wolverine.Runtime.IWolverineRuntime _wolverineRuntime; + private readonly Wolverine.Marten.Publishing.OutboxedSessionFactory _outboxedSessionFactory; + + public POST_api_tenants_tenant_counters_id_inc(Wolverine.Http.WolverineHttpOptions wolverineHttpOptions, Wolverine.Runtime.IWolverineRuntime wolverineRuntime, Wolverine.Marten.Publishing.OutboxedSessionFactory outboxedSessionFactory) : base(wolverineHttpOptions) + { + _wolverineHttpOptions = wolverineHttpOptions; + _wolverineRuntime = wolverineRuntime; + _outboxedSessionFactory = outboxedSessionFactory; + } + + + + public override async System.Threading.Tasks.Task Handle(Microsoft.AspNetCore.Http.HttpContext httpContext) + { + var messageContext = new Wolverine.Runtime.MessageContext(_wolverineRuntime); + // Building the Marten session + await using var documentSession = _outboxedSessionFactory.OpenSession(messageContext); + string Id_rawValue = (string?)httpContext.GetRouteValue("Id"); + System.Guid Id = default; + + if (Id_rawValue != null && System.Guid.TryParse(Id_rawValue, System.Globalization.CultureInfo.InvariantCulture, out Id)) + { + + } + + else + { + httpContext.Response.StatusCode = 404; + return; + } + + + // Try to load the existing saga document + var counter_Id = await documentSession.LoadAsync(Id, httpContext.RequestAborted).ConfigureAwait(false); + // 404 if this required object is null + if (counter_Id == null) + { + httpContext.Response.StatusCode = 404; + return; + } + + + // The actual HTTP request handler execution + (var result, var martenOp) = Wolverine.Http.Tests.Bugs.CounterEndpoint.Increment(counter_Id); + + if (martenOp != null) + { + + // Placed by Wolverine's ISideEffect policy + martenOp.Execute(documentSession); + + } + + + // Save all pending changes to this Marten session + await documentSession.SaveChangesAsync(httpContext.RequestAborted).ConfigureAwait(false); + + + // Have to flush outgoing messages just in case Marten did nothing because of https://github.com/JasperFx/wolverine/issues/536 + await messageContext.FlushOutgoingMessagesAsync().ConfigureAwait(false); + + await result.ExecuteAsync(httpContext).ConfigureAwait(false); + } + + } + + // END: POST_api_tenants_tenant_counters_id_inc + + +} + diff --git a/src/Http/Wolverine.Http.Tests/Internal/Generated/WolverineHandlers/POST_api_tenants_tenant_counters_id_inc2.cs b/src/Http/Wolverine.Http.Tests/Internal/Generated/WolverineHandlers/POST_api_tenants_tenant_counters_id_inc2.cs new file mode 100644 index 000000000..3d5eb8ff8 --- /dev/null +++ b/src/Http/Wolverine.Http.Tests/Internal/Generated/WolverineHandlers/POST_api_tenants_tenant_counters_id_inc2.cs @@ -0,0 +1,88 @@ +// +#pragma warning disable +using Microsoft.AspNetCore.Routing; +using System; +using System.Linq; +using Wolverine.Http; +using Wolverine.Marten.Publishing; +using Wolverine.Runtime; + +namespace Internal.Generated.WolverineHandlers +{ + // START: POST_api_tenants_tenant_counters_id_inc2 + [global::System.CodeDom.Compiler.GeneratedCode("JasperFx", "1.0.0")] + public sealed class POST_api_tenants_tenant_counters_id_inc2 : Wolverine.Http.HttpHandler + { + private readonly Wolverine.Http.WolverineHttpOptions _wolverineHttpOptions; + private readonly Wolverine.Runtime.IWolverineRuntime _wolverineRuntime; + private readonly Wolverine.Marten.Publishing.OutboxedSessionFactory _outboxedSessionFactory; + + public POST_api_tenants_tenant_counters_id_inc2(Wolverine.Http.WolverineHttpOptions wolverineHttpOptions, Wolverine.Runtime.IWolverineRuntime wolverineRuntime, Wolverine.Marten.Publishing.OutboxedSessionFactory outboxedSessionFactory) : base(wolverineHttpOptions) + { + _wolverineHttpOptions = wolverineHttpOptions; + _wolverineRuntime = wolverineRuntime; + _outboxedSessionFactory = outboxedSessionFactory; + } + + + + public override async System.Threading.Tasks.Task Handle(Microsoft.AspNetCore.Http.HttpContext httpContext) + { + var messageContext = new Wolverine.Runtime.MessageContext(_wolverineRuntime); + // Building the Marten session + await using var documentSession = _outboxedSessionFactory.OpenSession(messageContext); + string Id_rawValue = (string?)httpContext.GetRouteValue("Id"); + System.Guid Id = default; + + if (Id_rawValue != null && System.Guid.TryParse(Id_rawValue, System.Globalization.CultureInfo.InvariantCulture, out Id)) + { + + } + + else + { + httpContext.Response.StatusCode = 404; + return; + } + + + // Try to load the existing saga document + var counter_Id = await documentSession.LoadAsync(Id, httpContext.RequestAborted).ConfigureAwait(false); + // 404 if this required object is null + if (counter_Id == null) + { + httpContext.Response.StatusCode = 404; + return; + } + + + // The actual HTTP request handler execution + var martenOp = Wolverine.Http.Tests.Bugs.CounterEndpoint.Increment2(counter_Id); + + if (martenOp != null) + { + + // Placed by Wolverine's ISideEffect policy + martenOp.Execute(documentSession); + + } + + + // Save all pending changes to this Marten session + await documentSession.SaveChangesAsync(httpContext.RequestAborted).ConfigureAwait(false); + + + // Have to flush outgoing messages just in case Marten did nothing because of https://github.com/JasperFx/wolverine/issues/536 + await messageContext.FlushOutgoingMessagesAsync().ConfigureAwait(false); + + // Wolverine automatically sets the status code to 204 for empty responses + if (httpContext.Response is { HasStarted: false, StatusCode: 200 }) httpContext.Response.StatusCode = 204; + } + + } + + // END: POST_api_tenants_tenant_counters_id_inc2 + + +} + diff --git a/src/Http/Wolverine.Http.Tests/Internal/Generated/WolverineHandlers/POST_tenant_tenant_formdata.cs b/src/Http/Wolverine.Http.Tests/Internal/Generated/WolverineHandlers/POST_tenant_tenant_formdata.cs new file mode 100644 index 000000000..a9681ff9a --- /dev/null +++ b/src/Http/Wolverine.Http.Tests/Internal/Generated/WolverineHandlers/POST_tenant_tenant_formdata.cs @@ -0,0 +1,50 @@ +// +#pragma warning disable +using Microsoft.AspNetCore.Routing; +using System; +using System.Linq; +using Wolverine.Http; + +namespace Internal.Generated.WolverineHandlers +{ + // START: POST_tenant_tenant_formdata + [global::System.CodeDom.Compiler.GeneratedCode("JasperFx", "1.0.0")] + public sealed class POST_tenant_tenant_formdata : Wolverine.Http.HttpHandler + { + private readonly Wolverine.Http.WolverineHttpOptions _wolverineHttpOptions; + + public POST_tenant_tenant_formdata(Wolverine.Http.WolverineHttpOptions wolverineHttpOptions) : base(wolverineHttpOptions) + { + _wolverineHttpOptions = wolverineHttpOptions; + } + + + + public override async System.Threading.Tasks.Task Handle(Microsoft.AspNetCore.Http.HttpContext httpContext) + { + + // Tenant Id detection + // 1. Tenant Id is route argument named 'tenant' + var tenantId = await TryDetectTenantId(httpContext); + if (string.IsNullOrEmpty(tenantId)) + { + await WriteTenantIdNotFound(httpContext); + return; + } + + var tenantIdentifier = new JasperFx.MultiTenancy.TenantId(tenantId); + var value = httpContext.Request.Form["value"]; + + // The actual HTTP request handler execution + var result_of_GetTenantIdWithFormData = Wolverine.Http.Tests.MultiTenancy.TenantedEndpoints.GetTenantIdWithFormData(value, tenantIdentifier); + + await WriteString(httpContext, result_of_GetTenantIdWithFormData); + } + + } + + // END: POST_tenant_tenant_formdata + + +} + diff --git a/src/Http/Wolverine.Http.Tests/Internal/Generated/WolverineHandlers/POST_todo_create.cs b/src/Http/Wolverine.Http.Tests/Internal/Generated/WolverineHandlers/POST_todo_create.cs new file mode 100644 index 000000000..01f4e49d1 --- /dev/null +++ b/src/Http/Wolverine.Http.Tests/Internal/Generated/WolverineHandlers/POST_todo_create.cs @@ -0,0 +1,81 @@ +// +#pragma warning disable +using Microsoft.AspNetCore.Routing; +using System; +using System.Linq; +using Wolverine.Http; +using Wolverine.Marten.Publishing; +using Wolverine.Runtime; + +namespace Internal.Generated.WolverineHandlers +{ + // START: POST_todo_create + [global::System.CodeDom.Compiler.GeneratedCode("JasperFx", "1.0.0")] + public sealed class POST_todo_create : Wolverine.Http.HttpHandler + { + private readonly Wolverine.Http.WolverineHttpOptions _wolverineHttpOptions; + private readonly Wolverine.Marten.Publishing.OutboxedSessionFactory _outboxedSessionFactory; + private readonly Wolverine.Runtime.IWolverineRuntime _wolverineRuntime; + + public POST_todo_create(Wolverine.Http.WolverineHttpOptions wolverineHttpOptions, Wolverine.Marten.Publishing.OutboxedSessionFactory outboxedSessionFactory, Wolverine.Runtime.IWolverineRuntime wolverineRuntime) : base(wolverineHttpOptions) + { + _wolverineHttpOptions = wolverineHttpOptions; + _outboxedSessionFactory = outboxedSessionFactory; + _wolverineRuntime = wolverineRuntime; + } + + + + public override async System.Threading.Tasks.Task Handle(Microsoft.AspNetCore.Http.HttpContext httpContext) + { + + // Tenant Id detection + // 1. Tenant Id is first sub domain name of the request host + // 2. Tenant Id is request header 'tenant' + // 3. Tenant Id is query string value 'tenant' + var tenantId = await TryDetectTenantId(httpContext); + if (string.IsNullOrEmpty(tenantId)) + { + await WriteTenantIdNotFound(httpContext); + return; + } + + var messageContext = new Wolverine.Runtime.MessageContext(_wolverineRuntime); + messageContext.TenantId = tenantId; + // Building the Marten session using the detected tenant id + await using var documentSession = _outboxedSessionFactory.OpenSession(messageContext, tenantId); + var tenantIdentifier = new JasperFx.MultiTenancy.TenantId(tenantId); + // Reading the request body via JSON deserialization + var (command, jsonContinue) = await ReadJsonAsync(httpContext); + if (jsonContinue == Wolverine.HandlerContinuation.Stop) return; + + // The actual HTTP request handler execution + var martenOp = Wolverine.Http.Tests.MultiTenancy.TenantedEndpoints.Create(command, tenantIdentifier); + + if (martenOp != null) + { + + // Placed by Wolverine's ISideEffect policy + martenOp.Execute(documentSession); + + } + + + // Save all pending changes to this Marten session + await documentSession.SaveChangesAsync(httpContext.RequestAborted).ConfigureAwait(false); + + + // Have to flush outgoing messages just in case Marten did nothing because of https://github.com/JasperFx/wolverine/issues/536 + await messageContext.FlushOutgoingMessagesAsync().ConfigureAwait(false); + + // Wolverine automatically sets the status code to 204 for empty responses + if (httpContext.Response is { HasStarted: false, StatusCode: 200 }) httpContext.Response.StatusCode = 204; + } + + } + + // END: POST_todo_create + + +} + diff --git a/src/Persistence/LoadTesting/LoadTesting.csproj b/src/Persistence/LoadTesting/LoadTesting.csproj new file mode 100644 index 000000000..7b1e349f7 --- /dev/null +++ b/src/Persistence/LoadTesting/LoadTesting.csproj @@ -0,0 +1,20 @@ + + + + Exe + enable + enable + + + + + + + + + + Servers.cs + + + + diff --git a/src/Persistence/LoadTesting/Program.cs b/src/Persistence/LoadTesting/Program.cs new file mode 100644 index 000000000..bf1abdee0 --- /dev/null +++ b/src/Persistence/LoadTesting/Program.cs @@ -0,0 +1,66 @@ +// See https://aka.ms/new-console-template for more information + +using IntegrationTests; +using JasperFx; +using JasperFx.Events.Daemon; +using JasperFx.Events.Projections; +using JasperFx.Resources; +using LoadTesting; +using LoadTesting.Trips; +using Marten; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; +using Wolverine; +using Wolverine.Marten; +using Wolverine.RabbitMQ; +using Wolverine.Transports; + +return await Host.CreateDefaultBuilder() + .UseWolverine(opts => + { + opts.Policies.UseDurableLocalQueues(); + + opts.Services.AddMarten(m => + { + m.Connection(Servers.PostgresConnectionString); + m.DatabaseSchemaName = "load_testing"; + + m.DisableNpgsqlLogging = true; + + m.Schema.For(); + + m.Projections.Add(ProjectionLifecycle.Async); + m.Projections.Add(ProjectionLifecycle.Async); + m.Projections.Add(ProjectionLifecycle.Async); + }).AddAsyncDaemon(DaemonMode.Solo).IntegrateWithWolverine(); + + opts.ServiceName = "TripPublisher"; + + opts.Durability.Mode = DurabilityMode.Solo; + opts.ApplicationAssembly = typeof(Program).Assembly; + opts.EnableAutomaticFailureAcks = false; + + opts.Policies.AutoApplyTransactions(); + + opts.Services.AddSingleton(); + + // Force it to use Rabbit MQ + opts.Policies.DisableConventionalLocalRouting(); + opts.UseRabbitMq().UseConventionalRouting().AutoProvision(); + + opts.Policies.UseDurableInboxOnAllListeners(); + opts.Policies.UseDurableOutboxOnAllSendingEndpoints(); + + opts.LocalQueueFor().UseDurableInbox(); + + //opts.Services.AddHostedService(); + opts.Services.AddResourceSetupOnStartup(); + + opts.Policies.AllLocalQueues(listener => + { + listener.UseDurableInbox(); + listener.Sequential(); + }); + + + }).RunJasperFxCommands(args); \ No newline at end of file diff --git a/src/Persistence/LoadTesting/Publisher.cs b/src/Persistence/LoadTesting/Publisher.cs new file mode 100644 index 000000000..1715ad2ad --- /dev/null +++ b/src/Persistence/LoadTesting/Publisher.cs @@ -0,0 +1,100 @@ +using ImTools; +using LoadTesting.Trips; +using Microsoft.Extensions.Hosting; +using Microsoft.Extensions.Logging; +using Wolverine.Runtime; + +namespace LoadTesting; + +public class KickOffPublishing : IHostedService +{ + private readonly Publisher _publisher; + private readonly IWolverineRuntime _runtime; + private readonly ILogger _logger; + + public KickOffPublishing(Publisher publisher, IWolverineRuntime runtime, ILogger logger) + { + _publisher = publisher; + _runtime = runtime; + _logger = logger; + } + + public async Task StartAsync(CancellationToken cancellationToken) + { + var bus = new MessageBus(_runtime); + var messages = _publisher.InitialMessages(); + foreach (var message in messages) + { + + await bus.PublishAsync(message); + _logger.LogInformation("Published initial message {Message}", message); + } + } + + public Task StopAsync(CancellationToken cancellationToken) + { + return Task.CompletedTask; + } +} + +public class Publisher +{ + private ImHashMap _streams = ImHashMap.Empty; + + public Publisher() + { + var streams = TripStream.RandomStreams(10); + foreach (var stream in streams) + { + _streams = _streams.AddOrUpdate(stream.Id, stream); + } + } + + public IEnumerable InitialMessages() + { + foreach (var entry in _streams.Enumerate()) + { + if (entry.Value.TryCheckoutCommand(out var command)) + { + yield return command; + } + } + } + + public IEnumerable NextMessages(Guid id) + { + if (_streams.TryFind(id, out var stream)) + { + if (stream.TryCheckoutCommand(out var message)) + { + yield return message; + } + + if (stream.IsFinishedPublishing()) + { + _streams = _streams = _streams.Remove(id); + } + } + + while (_streams.Count() < 10) + { + stream = new TripStream(); + _streams = _streams.AddOrUpdate(stream.Id, stream); + + if (stream.TryCheckoutCommand(out var message)) + { + yield return message; + } + } + } +} + +public static class ContinueTripHandler +{ + public static IEnumerable Handle(ContinueTrip message, Publisher publisher) + { + Thread.Sleep(250); + + return publisher.NextMessages(message.TripId); + } +} \ No newline at end of file diff --git a/src/Persistence/LoadTesting/Repairs/ConductRepairsHandler.cs b/src/Persistence/LoadTesting/Repairs/ConductRepairsHandler.cs new file mode 100644 index 000000000..bf597d065 --- /dev/null +++ b/src/Persistence/LoadTesting/Repairs/ConductRepairsHandler.cs @@ -0,0 +1,10 @@ +using LoadTesting.Trips; + +public static class ConductRepairsHandler +{ + public static async Task HandleAsync(ConductRepairs message) + { + await Task.Delay(Random.Shared.Next(0, 2000)); + return new RepairsCompleted(message.TripId); + } +} \ No newline at end of file diff --git a/src/Persistence/LoadTesting/Repairs/RepairRequestedHandler.cs b/src/Persistence/LoadTesting/Repairs/RepairRequestedHandler.cs new file mode 100644 index 000000000..2b3edc671 --- /dev/null +++ b/src/Persistence/LoadTesting/Repairs/RepairRequestedHandler.cs @@ -0,0 +1,28 @@ +using LoadTesting.Trips; +using Wolverine; +using Wolverine.ErrorHandling; +using Wolverine.Runtime.Handlers; + +public static class RepairRequestedHandler +{ + public static void Configure(HandlerChain chain) + { + chain.OnAnyException().MoveToErrorQueue(); + } + + public static void Before(RepairRequested requested) + { + // Chaos monkey + if (Random.Shared.NextDouble() < .05) + { + throw new RepairShopTooBusyException(requested.State + " is just too busy"); + } + } + + // Just splitting them + public static object Handle(RepairRequested requested) + { + var localQueue = new Uri($"local://{requested.State.ToLowerInvariant()}"); + return new ConductRepairs(requested.TripId).ToDestination(localQueue); + } +} \ No newline at end of file diff --git a/src/Persistence/LoadTesting/Repairs/RepairWork.cs b/src/Persistence/LoadTesting/Repairs/RepairWork.cs new file mode 100644 index 000000000..7876bb8c9 --- /dev/null +++ b/src/Persistence/LoadTesting/Repairs/RepairWork.cs @@ -0,0 +1,6 @@ +public class RepairWork +{ + public Guid Id { get; set; } + public Guid TripId { get; set; } + public string State { get; set; } +} \ No newline at end of file diff --git a/src/Persistence/LoadTesting/SeedCommand.cs b/src/Persistence/LoadTesting/SeedCommand.cs new file mode 100644 index 000000000..9ec8d9737 --- /dev/null +++ b/src/Persistence/LoadTesting/SeedCommand.cs @@ -0,0 +1,73 @@ +using JasperFx.CommandLine; +using LoadTesting.Trips; +using Wolverine; +using Wolverine.Runtime; +using Wolverine.Tracking; + +namespace LoadTesting; + +// local://loadtesting.trips.starttrip/ + +[Description("Build lots of data in the inbox")] +public class SeedCommand : JasperFxAsyncCommand +{ + public override async Task Execute(NetCoreInput input) + { + using var host = input.BuildHost(); + + var runtime = host.GetRuntime(); + + await runtime.Storage.Admin.MigrateAsync(); + + for (int i = 0; i < 50000; i++) + { + var streams = TripStream.RandomStreams(100); + var envelopes = new List(); + + if (streams[0].TryCheckoutCommand(out var message1)) + { + var envelope = new Envelope + { + Message = message1, + Destination = new Uri("rabbitmq://queue/LoadTesting.Trips.ContinueTrip"), + Serializer = runtime.Options.DefaultSerializer, + ContentType = "application/json", + Status = EnvelopeStatus.Incoming, + OwnerId = 0 + }; + + envelopes.Add(envelope); + + + } + + foreach (var tripStream in streams.Skip(1)) + { + if (tripStream.TryCheckoutCommand(out var message)) + { + var envelope = new Envelope + { + Message = message, + Destination = new Uri("rabbitmq://queue/LoadTesting.Trips.StartTrip"), + Serializer = runtime.Options.DefaultSerializer, + ContentType = "application/json", + Status = EnvelopeStatus.Handled + }; + + envelopes.Add(envelope); + + + } + } + + await runtime.Storage.Inbox.StoreIncomingAsync(envelopes); + + if (i % 1000 == 0) + { + Console.WriteLine("Published " + i); + } + } + + return true; + } +} \ No newline at end of file diff --git a/src/Persistence/LoadTesting/Trips/Commands.cs b/src/Persistence/LoadTesting/Trips/Commands.cs new file mode 100644 index 000000000..374c2439a --- /dev/null +++ b/src/Persistence/LoadTesting/Trips/Commands.cs @@ -0,0 +1,18 @@ + +namespace LoadTesting.Trips; + +public record StartTrip(Guid TripId, int StartDay, string State); + + +public record RecordTravel(Guid TripId, Traveled Event); +public record AbortTrip(Guid TripId); + +public record RecordBreakdown(Guid TripId, bool IsCritical); + +public record MarkVacationOver(Guid TripId); + +public record Arrive(Guid TripId, int Day, string State); + +public record Depart(Guid TripId, int Day, string State); + +public record EndTrip(Guid TripId, int Day, string State); \ No newline at end of file diff --git a/src/Persistence/LoadTesting/Trips/ContinueTrip.cs b/src/Persistence/LoadTesting/Trips/ContinueTrip.cs new file mode 100644 index 000000000..c37c72256 --- /dev/null +++ b/src/Persistence/LoadTesting/Trips/ContinueTrip.cs @@ -0,0 +1,3 @@ +namespace LoadTesting.Trips; + +public record ContinueTrip(Guid TripId); \ No newline at end of file diff --git a/src/Persistence/LoadTesting/Trips/Day.cs b/src/Persistence/LoadTesting/Trips/Day.cs new file mode 100644 index 000000000..4df2c1353 --- /dev/null +++ b/src/Persistence/LoadTesting/Trips/Day.cs @@ -0,0 +1,107 @@ +using JasperFx.Events; +using Marten.Events.Projections; + +namespace LoadTesting.Trips; + +public class Day +{ + public long Version { get; set; } + + public int Id { get; set; } + + // how many trips started on this day? + public int Started { get; set; } + + // how many trips ended on this day? + public int Ended { get; set; } + + public int Stops { get; set; } + + // how many miles did the active trips + // drive in which direction on this day? + public double North { get; set; } + public double East { get; set; } + public double West { get; set; } + public double South { get; set; } +} + +public class DayProjection: MultiStreamProjection +{ + public DayProjection() + { + // Tell the projection how to group the events + // by Day document + Identity(x => x.Day); + + // This just lets the projection work independently + // on each Movement child of the Travel event + // as if it were its own event + FanOut(x => x.Movements); + + // You can also access Event data + FanOut(x => x.Data.Stops); + + ProjectionName = "Day"; + + // Opt into 2nd level caching of up to 100 + // most recently encountered aggregates as a + // performance optimization + CacheLimitPerTenant = 1000; + + // With large event stores of relatively small + // event objects, moving this number up from the + // default can greatly improve throughput and especially + // improve projection rebuild times + Options.BatchSize = 5000; + } + + public void Apply(Day day, TripStarted e) => day.Started++; + public void Apply(Day day, TripEnded e) => day.Ended++; + + public void Apply(Day day, Movement e) + { + switch (e.Direction) + { + case Direction.East: + day.East += e.Distance; + break; + case Direction.North: + day.North += e.Distance; + break; + case Direction.South: + day.South += e.Distance; + break; + case Direction.West: + day.West += e.Distance; + break; + + default: + throw new ArgumentOutOfRangeException(); + } + } + + public void Apply(Day day, Stop e) => day.Stops++; +} + +public class Distance +{ + public Guid Id { get; set; } + + public double Total { get; set; } + + public int Day { get; set; } +} + +public class DistanceProjection: EventProjection +{ + public DistanceProjection() + { + ProjectionName = "Distance"; + } + + // Create a new Distance document based on a Travel event + public Distance Create(Traveled travel, IEvent e) + { + return new Distance {Id = e.Id, Day = travel.Day, Total = travel.TotalDistance()}; + } +} diff --git a/src/Persistence/LoadTesting/Trips/Events.cs b/src/Persistence/LoadTesting/Trips/Events.cs new file mode 100644 index 000000000..814d2a0db --- /dev/null +++ b/src/Persistence/LoadTesting/Trips/Events.cs @@ -0,0 +1,77 @@ +namespace LoadTesting.Trips; + + + +public class FailingEvent +{ + public static bool SerializationFails = false; + + public FailingEvent() + { + if (SerializationFails) throw new DivideByZeroException("Boom!"); + } +} + +public record TripAborted; +public record BrokeDown(bool IsCritical); +public record VacationOver; + +public record Arrival(int Day, string State); + +public record Departure(int Day, string State); + +public enum Direction +{ + North, + South, + East, + West +} + +public interface IDayEvent +{ + int Day { get; } +} + +public record Movement(Direction Direction, double Distance); + +public record Stop(TimeOnly Time, string State, int Duration); + +public class Traveled : IDayEvent +{ + public static Traveled Random(int day) + { + var travel = new Traveled {Day = day,}; + + var random = System.Random.Shared; + var numberOfMovements = random.Next(1, 20); + for (var i = 0; i < numberOfMovements; i++) + { + var movement = new Movement(TripStream.RandomDirection(), random.Next(500, 3000) / 100); + + travel.Movements.Add(movement); + } + + var numberOfStops = random.Next(1, 10); + for (var i = 0; i < numberOfStops; i++) + { + travel.Stops.Add(new Stop(TripStream.RandomTime(), TripStream.RandomState(), random.Next(10, 30))); + } + + return travel; + } + + public int Day { get; set; } + + public IList Movements { get; set; } = new List(); + public List Stops { get; set; } = new(); + + public double TotalDistance() + { + return Movements.Sum(x => x.Distance); + } +} + +public record TripEnded(int Day, string State) : IDayEvent; + +public record TripStarted(int Day, string State) : IDayEvent; \ No newline at end of file diff --git a/src/Persistence/LoadTesting/Trips/Exceptions.cs b/src/Persistence/LoadTesting/Trips/Exceptions.cs new file mode 100644 index 000000000..5868d42c7 --- /dev/null +++ b/src/Persistence/LoadTesting/Trips/Exceptions.cs @@ -0,0 +1,43 @@ +namespace LoadTesting.Trips; + +public class TransientException : Exception +{ + public TransientException(string? message) : base(message) + { + } +} + +public class OtherTransientException : Exception +{ + public OtherTransientException(string? message) : base(message) + { + } +} + +public class RepairShopTooBusyException : Exception +{ + public RepairShopTooBusyException(string? message) : base(message) + { + } +} + +public class TripServiceTooBusyException : Exception +{ + public TripServiceTooBusyException(string? message) : base(message) + { + } +} + +public class TrackingUnavailableException : Exception +{ + public TrackingUnavailableException(string? message) : base(message) + { + } +} + +public class DatabaseIsTiredException : Exception +{ + public DatabaseIsTiredException(string? message) : base(message) + { + } +} \ No newline at end of file diff --git a/src/Persistence/LoadTesting/Trips/RepairMessages.cs b/src/Persistence/LoadTesting/Trips/RepairMessages.cs new file mode 100644 index 000000000..638a38fa1 --- /dev/null +++ b/src/Persistence/LoadTesting/Trips/RepairMessages.cs @@ -0,0 +1,7 @@ +namespace LoadTesting.Trips; + +public record RepairRequested(Guid TripId, string State); +public record ConductRepairs(Guid TripId); +public record RepairsCompleted(Guid TripId); + +public record TripResumed; \ No newline at end of file diff --git a/src/Persistence/LoadTesting/Trips/StartTripHandler.cs b/src/Persistence/LoadTesting/Trips/StartTripHandler.cs new file mode 100644 index 000000000..86cd73255 --- /dev/null +++ b/src/Persistence/LoadTesting/Trips/StartTripHandler.cs @@ -0,0 +1,31 @@ +using Microsoft.Extensions.Logging; +using Wolverine.ErrorHandling; +using Wolverine.Marten; +using Wolverine.Runtime.Handlers; + +namespace LoadTesting.Trips; + +public static class StartTripHandler +{ + public static void Configure(HandlerChain chain) + { + chain.OnAnyException().MoveToErrorQueue(); + } + + public static void Before() + { + // Chaos monkey + if (Random.Shared.Next() < .05) + { + throw new TripServiceTooBusyException("Just feeling tired at " + DateTime.Now); + } + } + + public static IStartStream Handle(StartTrip command, ILogger logger) + { + Thread.Sleep(250); + + logger.LogInformation("Starting a new trip {Id}", command.TripId); + return MartenOps.StartStream(command.TripId, new TripStarted(command.StartDay, command.State)); + } +} \ No newline at end of file diff --git a/src/Persistence/LoadTesting/Trips/Trip.cs b/src/Persistence/LoadTesting/Trips/Trip.cs new file mode 100644 index 000000000..eecf2f90c --- /dev/null +++ b/src/Persistence/LoadTesting/Trips/Trip.cs @@ -0,0 +1,43 @@ +namespace LoadTesting.Trips; + +public class Activity +{ + public Guid Id { get; set; } +} + +public class Trip : Activity +{ + public int EndedOn { get; set; } + + public double Traveled { get; set; } + + public string State { get; set; } + + public bool Active { get; set; } + + public int StartedOn { get; set; } + public bool WaitingRepairs { get; set; } + + protected bool Equals(Trip other) + { + return Id.Equals(other.Id) && EndedOn == other.EndedOn && Traveled.Equals(other.Traveled) && State == other.State && Active == other.Active && StartedOn == other.StartedOn; + } + + public override bool Equals(object obj) + { + if (ReferenceEquals(null, obj)) return false; + if (ReferenceEquals(this, obj)) return true; + if (obj.GetType() != this.GetType()) return false; + return Equals((Trip) obj); + } + + public override int GetHashCode() + { + return HashCode.Combine(Id, EndedOn, Traveled, State, Active, StartedOn); + } + + public override string ToString() + { + return $"{nameof(Id)}: {Id}, {nameof(EndedOn)}: {EndedOn}, {nameof(Traveled)}: {Traveled}, {nameof(State)}: {State}, {nameof(Active)}: {Active}, {nameof(StartedOn)}: {StartedOn}"; + } +} \ No newline at end of file diff --git a/src/Persistence/LoadTesting/Trips/TripMessageHandler.cs b/src/Persistence/LoadTesting/Trips/TripMessageHandler.cs new file mode 100644 index 000000000..a96f1273c --- /dev/null +++ b/src/Persistence/LoadTesting/Trips/TripMessageHandler.cs @@ -0,0 +1,78 @@ +using JasperFx.Core; +using Wolverine; +using Wolverine.ErrorHandling; +using Wolverine.Marten; +using Wolverine.Runtime.Handlers; + +namespace LoadTesting.Trips; + +[AggregateHandler] +public static class TripMessageHandler +{ + public static void Configure(HandlerChain chain) + { + chain.OnException() + .RetryWithCooldown(50.Milliseconds(), 100.Milliseconds(), 250.Milliseconds()) + .Then.MoveToErrorQueue(); + + chain.OnException() + .Requeue(3).Then.MoveToErrorQueue(); + + chain.OnAnyException().MoveToErrorQueue(); + } + + public static void Before() + { + // Chaos monkey + var next = Random.Shared.NextDouble(); + if (next < .01) + { + throw new TripServiceTooBusyException("Just feeling tired at " + DateTime.Now); + } + + if (next < .02) + { + throw new TrackingUnavailableException("Tracking is down at " + DateTime.Now); + } + + if (next < .03) + { + throw new DatabaseIsTiredException("The database wants a break at " + DateTime.Now); + } + + if (next < .04) + { + throw new TransientException("Slow down, you move too fast."); + } + + if (next < .05) + { + throw new OtherTransientException("Slow down, you move too fast."); + } + } + + public static Traveled Handle(RecordTravel message, Trip trip) + { + return message.Event; + } + + public static TripAborted Handle(AbortTrip command, Trip trip) => new(); + + public static (BrokeDown, OutgoingMessages) Handle(RecordBreakdown command, Trip trip) + { + var e = new BrokeDown(command.IsCritical); + return command.IsCritical + ? (e, [new RepairRequested(command.TripId, trip.State)]) + : (e, []); + } + + public static VacationOver Handle(MarkVacationOver command, Trip trip) => new(); + + public static Arrival Handle(Arrive command, Trip trip) => new(command.Day, command.State); + + public static Departure Handle(Depart command, Trip trip) => new(command.Day, command.State); + + public static TripEnded Handle(EndTrip command, Trip trip) => new(command.Day, command.State); + + public static TripResumed Handle(RepairsCompleted e, Trip trip) => new(); +} \ No newline at end of file diff --git a/src/Persistence/LoadTesting/Trips/TripProjection.cs b/src/Persistence/LoadTesting/Trips/TripProjection.cs new file mode 100644 index 000000000..2b70e2595 --- /dev/null +++ b/src/Persistence/LoadTesting/Trips/TripProjection.cs @@ -0,0 +1,48 @@ +using JasperFx.Events; +using Marten; +using Marten.Events.Aggregation; + +namespace LoadTesting.Trips; + +public class TripProjection: SingleStreamProjection +{ + // These methods can be either public, internal, or private but there's + // a small performance gain to making them public + public void Apply(Arrival e, Trip trip) => trip.State = e.State; + public void Apply(Traveled e, Trip trip) => trip.Traveled += e.TotalDistance(); + + public void Apply(Departure e, Trip trip) + { + trip.Active = true; + trip.WaitingRepairs = false; + trip.State = e.State; + } + + public void Apply(TripEnded e, Trip trip) + { + trip.Active = false; + trip.EndedOn = e.Day; + trip.State = e.State; + } + + public Trip Create(TripStarted started) + { + return new Trip { StartedOn = started.Day, Active = true, State = started.State}; + } + + public void Apply(BrokeDown e, Trip trip) + { + trip.WaitingRepairs = true; + } + + public void Apply(TripResumed e, Trip trip) => trip.WaitingRepairs = false; + public override ValueTask RaiseSideEffects(IDocumentOperations operations, IEventSlice slice) + { + if (slice.Snapshot is { WaitingRepairs: false }) + { + slice.PublishMessage(new ContinueTrip(slice.Snapshot.Id)); + } + + return new ValueTask(); + } +} \ No newline at end of file diff --git a/src/Persistence/LoadTesting/Trips/TripStream.cs b/src/Persistence/LoadTesting/Trips/TripStream.cs new file mode 100644 index 000000000..d36e97155 --- /dev/null +++ b/src/Persistence/LoadTesting/Trips/TripStream.cs @@ -0,0 +1,120 @@ +using JasperFx.Core; + +namespace LoadTesting.Trips; + +public class TripStream +{ + public static List RandomStreams(int number) + { + var list = new List(); + for (var i = 0; i < number; i++) + { + var stream = new TripStream(); + list.Add(stream); + } + + return list; + } + + public static readonly string[] States = new string[] {"Texas", "Arkansas", "Missouri", "Kansas", "Oklahoma", "Connecticut", "New Jersey", "New York" }; + + public static string RandomState() + { + var index = Random.Shared.Next(0, States.Length - 1); + return States[index]; + } + + public static Direction RandomDirection() + { + var index = Random.Shared.Next(0, 3); + switch (index) + { + case 0: + return Direction.East; + case 1: + return Direction.North; + case 2: + return Direction.South; + default: + return Direction.West; + } + } + + public static TimeOnly RandomTime() + { + var hour = Random.Shared.Next(0, 24); + return new TimeOnly(hour, 0, 0); + } + + public Guid Id = CombGuidIdGeneration.NewGuid(); + + public readonly Queue Messages = new(); + + public TripStream() + { + var random = Random.Shared; + var startDay = random.Next(1, 100); + + Messages.Enqueue(new StartTrip(Id, startDay, RandomState())); + + var state = RandomState(); + + Messages.Enqueue(new Depart(Id, startDay, state)); + + var duration = random.Next(1, 20); + + var randomNumber = random.NextDouble(); + for (var i = 0; i < duration; i++) + { + var day = startDay + i; + + var travel = new RecordTravel(Id, Traveled.Random(day)); + Messages.Enqueue(travel); + + if (i > 0 && randomNumber > .3) + { + var departure = new Depart(Id, day, state); + + Messages.Enqueue(departure); + + state = RandomState(); + + var arrival = new Arrive(Id, i, state); + Messages.Enqueue(arrival); + } + + if (randomNumber < .05) + { + Messages.Enqueue(new RecordBreakdown(Id, true)); + } + else if (randomNumber < .08) + { + Messages.Enqueue(new RecordBreakdown(Id, false)); + } + } + + if (randomNumber > .5) + { + Messages.Enqueue(new EndTrip(Id, startDay + duration, state)); + } + else if (randomNumber > .9) + { + Messages.Enqueue(new AbortTrip(Id)); + } + + } + + public bool IsFinishedPublishing() + { + return !Messages.Any(); + } + + public bool TryCheckoutCommand(out object command) + { + return (Messages.TryDequeue(out command)); + } + + + public string TenantId { get; set; } + +} \ No newline at end of file diff --git a/src/Persistence/MartenTests/handler_actions_with_implied_marten_operations.cs b/src/Persistence/MartenTests/handler_actions_with_implied_marten_operations.cs index 608a848ac..d55906485 100644 --- a/src/Persistence/MartenTests/handler_actions_with_implied_marten_operations.cs +++ b/src/Persistence/MartenTests/handler_actions_with_implied_marten_operations.cs @@ -24,6 +24,7 @@ public async Task InitializeAsync() .AddMarten(Servers.PostgresConnectionString) .IntegrateWithWolverine(); + opts.Policies.UseDurableLocalQueues(); opts.Policies.AutoApplyTransactions(); }).StartAsync(); @@ -69,7 +70,7 @@ await Should.ThrowAsync(() => } - + [Fact] public async Task update_document_happy_path() { @@ -101,6 +102,17 @@ public async Task delete_document() var doc = await session.LoadAsync("Max"); doc.ShouldBeNull(); } + + [Fact] // used this to checkout inbox behavior + public async Task delete_document_through_send() + { + await _host.SendMessageAndWaitAsync(new InsertMartenDocument("Max")); + await _host.SendMessageAndWaitAsync(new DeleteMartenDocument("Max")); + + using var session = _store.LightweightSession(); + var doc = await session.LoadAsync("Max"); + doc.ShouldBeNull(); + } [Fact] public async Task delete_document_by_int_id() diff --git a/src/Persistence/Wolverine.Marten/FlushOutgoingMessagesOnCommit.cs b/src/Persistence/Wolverine.Marten/FlushOutgoingMessagesOnCommit.cs index cb5b274a6..16d7236a2 100644 --- a/src/Persistence/Wolverine.Marten/FlushOutgoingMessagesOnCommit.cs +++ b/src/Persistence/Wolverine.Marten/FlushOutgoingMessagesOnCommit.cs @@ -31,7 +31,8 @@ public override Task BeforeSaveChangesAsync(IDocumentSession session, Cancellati { if (_context.Envelope.WasPersistedInInbox) { - session.QueueSqlCommand($"update {_messageStore.IncomingFullName} set {DatabaseConstants.Status} = '{EnvelopeStatus.Handled}' where id = ?", _context.Envelope.Id); + var keepUntil = DateTimeOffset.UtcNow.Add(_context.Runtime.Options.Durability.KeepAfterMessageHandling); + session.QueueSqlCommand($"update {_messageStore.IncomingFullName} set {DatabaseConstants.Status} = '{EnvelopeStatus.Handled}', {DatabaseConstants.KeepUntil} = ? where id = ?", keepUntil, _context.Envelope.Id); _context.Envelope.Status = EnvelopeStatus.Handled; } diff --git a/src/Persistence/Wolverine.Postgresql/PostgresqlMessageStore.cs b/src/Persistence/Wolverine.Postgresql/PostgresqlMessageStore.cs index b5c304f38..ec3872062 100644 --- a/src/Persistence/Wolverine.Postgresql/PostgresqlMessageStore.cs +++ b/src/Persistence/Wolverine.Postgresql/PostgresqlMessageStore.cs @@ -527,4 +527,39 @@ protected override void writeMessageIdArrayQueryList(DbCommandBuilder builder, G builder.AppendParameter(messageIds); builder.Append(')'); } + + public override async Task DeleteAllHandledAsync() + { + await using var conn = CreateConnection(); + await conn.OpenAsync(CancellationToken.None); + + var deleted = 1; + + var sql = $@" + WITH todo AS ( + SELECT id + FROM {_settings.SchemaName}.{DatabaseConstants.IncomingTable} + WHERE status = '{EnvelopeStatus.Handled}' + ORDER BY id + LIMIT 10000 + FOR UPDATE SKIP LOCKED + ) + DELETE FROM {_settings.SchemaName}.{DatabaseConstants.IncomingTable} w + USING todo + WHERE w.id = todo.id; +"; + + try + { + while (deleted > 0) + { + deleted = await conn.CreateCommand(sql).ExecuteNonQueryAsync(); + await Task.Delay(10.Milliseconds()); + } + } + finally + { + await conn.CloseAsync(); + } + } } \ No newline at end of file diff --git a/src/Persistence/Wolverine.RDBMS/DurabilityAgent.cs b/src/Persistence/Wolverine.RDBMS/DurabilityAgent.cs index 88760759f..16bd6f1b1 100644 --- a/src/Persistence/Wolverine.RDBMS/DurabilityAgent.cs +++ b/src/Persistence/Wolverine.RDBMS/DurabilityAgent.cs @@ -160,12 +160,14 @@ private bool isTimeToPruneNodeEventRecords() private IDatabaseOperation[] buildOperationBatch() { + var incomingTable = new DbObjectName(_database.SchemaName, DatabaseConstants.IncomingTable); + var now = DateTimeOffset.UtcNow; List ops = [ new CheckRecoverableIncomingMessagesOperation(_database, _runtime.Endpoints, _settings, _logger), new CheckRecoverableOutgoingMessagesOperation(_database, _runtime, _logger), new DeleteExpiredEnvelopesOperation( - new DbObjectName(_database.SchemaName, DatabaseConstants.IncomingTable), DateTimeOffset.UtcNow), + incomingTable, now), new MoveReplayableErrorMessagesToIncomingOperation(_database) ]; @@ -176,12 +178,12 @@ private IDatabaseOperation[] buildOperationBatch() if (_runtime.Options.Durability.OutboxStaleTime.HasValue) { - ops.Add(new BumpStaleOutgoingEnvelopesOperation(new DbObjectName(_database.SchemaName, DatabaseConstants.OutgoingTable), _runtime.Options.Durability, DateTimeOffset.UtcNow)); + ops.Add(new BumpStaleOutgoingEnvelopesOperation(new DbObjectName(_database.SchemaName, DatabaseConstants.OutgoingTable), _runtime.Options.Durability, now)); } if (_runtime.Options.Durability.InboxStaleTime.HasValue) { - ops.Add(new BumpStaleIncomingEnvelopesOperation(new DbObjectName(_database.SchemaName, DatabaseConstants.IncomingTable), _runtime.Options.Durability, DateTimeOffset.UtcNow)); + ops.Add(new BumpStaleIncomingEnvelopesOperation(incomingTable, _runtime.Options.Durability, now)); } return ops.ToArray(); diff --git a/src/Persistence/Wolverine.RDBMS/MessageDatabase.Admin.cs b/src/Persistence/Wolverine.RDBMS/MessageDatabase.Admin.cs index e69af1d26..243f5f57a 100644 --- a/src/Persistence/Wolverine.RDBMS/MessageDatabase.Admin.cs +++ b/src/Persistence/Wolverine.RDBMS/MessageDatabase.Admin.cs @@ -1,5 +1,6 @@ using System.Data.Common; using JasperFx.Core; +using JasperFx.Core.Reflection; using Weasel.Core; using Wolverine.Logging; using Wolverine.Persistence.Durability; @@ -14,6 +15,11 @@ public abstract partial class MessageDatabase { public abstract Task FetchCountsAsync(); + public virtual Task DeleteAllHandledAsync() + { + throw new NotSupportedException($"This function is not (yet) supported by {GetType().FullNameInCode()}"); + } + public async Task ClearAllAsync() { await using var conn = await DataSource.OpenConnectionAsync(_cancellation); diff --git a/src/Persistence/Wolverine.RavenDb/Internals/RavenDbMessageStore.Admin.cs b/src/Persistence/Wolverine.RavenDb/Internals/RavenDbMessageStore.Admin.cs index e299db2b2..a0fdb2eac 100644 --- a/src/Persistence/Wolverine.RavenDb/Internals/RavenDbMessageStore.Admin.cs +++ b/src/Persistence/Wolverine.RavenDb/Internals/RavenDbMessageStore.Admin.cs @@ -10,6 +10,11 @@ namespace Wolverine.RavenDb.Internals; public partial class RavenDbMessageStore : IMessageStoreAdmin { + public Task DeleteAllHandledAsync() + { + throw new NotSupportedException("This function is not yet supported by RavenDb"); + } + public async Task ClearAllAsync() { await _store.DeleteAllAsync(); diff --git a/src/Wolverine/Persistence/ClearHandledCommand.cs b/src/Wolverine/Persistence/ClearHandledCommand.cs new file mode 100644 index 000000000..e312f0cb6 --- /dev/null +++ b/src/Wolverine/Persistence/ClearHandledCommand.cs @@ -0,0 +1,30 @@ +using JasperFx.CommandLine; +using Spectre.Console; +using Wolverine.Tracking; + +namespace Wolverine.Persistence; + +[Description("Clear out all peristed inbox messages marked as `Handled`", Name = "clear-handled")] +public class ClearHandledCommand : JasperFxAsyncCommand +{ + public override async Task Execute(NetCoreInput input) + { + using var host = input.BuildHost(); + var runtime = host.GetRuntime(); + var stores = await runtime.Stores.FindAllAsync(); + + foreach (var store in stores) + { + await store.Admin.MigrateAsync(); + + Console.WriteLine("Starting to clear handled inbox messages in " + store.Uri); + + await store.Admin.DeleteAllHandledAsync(); + Console.WriteLine("Finished clearing handled inbox messages in " + store.Uri); + } + + AnsiConsole.MarkupLine("[green]Finished![/]"); + + return true; + } +} \ No newline at end of file diff --git a/src/Wolverine/Persistence/Durability/IMessageStoreAdmin.cs b/src/Wolverine/Persistence/Durability/IMessageStoreAdmin.cs index c62dfc5f6..c6371e555 100644 --- a/src/Wolverine/Persistence/Durability/IMessageStoreAdmin.cs +++ b/src/Wolverine/Persistence/Durability/IMessageStoreAdmin.cs @@ -4,6 +4,8 @@ namespace Wolverine.Persistence.Durability; public interface IMessageStoreAdmin { + Task DeleteAllHandledAsync(); + /// /// Clears out all persisted envelopes /// diff --git a/src/Wolverine/Persistence/Durability/MultiTenantedMessageStore.cs b/src/Wolverine/Persistence/Durability/MultiTenantedMessageStore.cs index 42a202071..bc827bbfa 100644 --- a/src/Wolverine/Persistence/Durability/MultiTenantedMessageStore.cs +++ b/src/Wolverine/Persistence/Durability/MultiTenantedMessageStore.cs @@ -368,6 +368,11 @@ public IAgent StartScheduledJobs(IWolverineRuntime runtime) Source.AllActive().Select(x => x.StartScheduledJobs(runtime))); } + Task IMessageStoreAdmin.DeleteAllHandledAsync() + { + return executeOnAllAsync(d => d.Admin.DeleteAllHandledAsync()); + } + Task IMessageStoreAdmin.ClearAllAsync() { return executeOnAllAsync(d => d.Admin.ClearAllAsync()); diff --git a/src/Wolverine/Persistence/Durability/NullMessageStore.cs b/src/Wolverine/Persistence/Durability/NullMessageStore.cs index 192527193..4df27582a 100644 --- a/src/Wolverine/Persistence/Durability/NullMessageStore.cs +++ b/src/Wolverine/Persistence/Durability/NullMessageStore.cs @@ -197,6 +197,11 @@ public Task FetchCountsAsync() return Task.FromResult(new PersistedCounts()); } + public Task DeleteAllHandledAsync() + { + return Task.CompletedTask; + } + public Task ClearAllAsync() { return Task.CompletedTask; diff --git a/src/Wolverine/Persistence/StorageCommand.cs b/src/Wolverine/Persistence/StorageCommand.cs index 51644a12b..e78689201 100644 --- a/src/Wolverine/Persistence/StorageCommand.cs +++ b/src/Wolverine/Persistence/StorageCommand.cs @@ -91,4 +91,4 @@ public override async Task Execute(StorageInput input) return true; } -} +} \ No newline at end of file diff --git a/src/Wolverine/Transports/ListeningAgent.cs b/src/Wolverine/Transports/ListeningAgent.cs index 6a956e698..c6a9c7b92 100644 --- a/src/Wolverine/Transports/ListeningAgent.cs +++ b/src/Wolverine/Transports/ListeningAgent.cs @@ -227,33 +227,49 @@ public async ValueTask PauseAsync(TimeSpan pauseTime) _restarter = new Restarter(this, pauseTime); } + private readonly SemaphoreSlim _semaphore = new SemaphoreSlim(0, 1); + public async ValueTask MarkAsTooBusyAndStopReceivingAsync() { if (Status != ListeningStatus.Accepting || Listener == null) { return; } - - using var activity = WolverineTracing.ActivitySource.StartActivity(WolverineTracing.PausingListener); - activity?.SetTag(WolverineTracing.EndpointAddress, Listener.Address); - activity?.SetTag(WolverineTracing.StopReason, WolverineTracing.TooBusy); - try + await _semaphore.WaitAsync(); + if (Status != ListeningStatus.Accepting || Listener == null) { - await Listener.StopAsync(); - await Listener.DisposeAsync(); + _semaphore.Release(); + return; } - catch (Exception e) + + try { - _logger.LogError(e, "Unable to cleanly stop the listener for {Uri}", Uri); - } + using var activity = WolverineTracing.ActivitySource.StartActivity(WolverineTracing.PausingListener); + activity?.SetTag(WolverineTracing.EndpointAddress, Listener.Address); + activity?.SetTag(WolverineTracing.StopReason, WolverineTracing.TooBusy); + + try + { + await Listener.StopAsync(); + await Listener.DisposeAsync(); + } + catch (Exception e) + { + _logger.LogError(e, "Unable to cleanly stop the listener for {Uri}", Uri); + } - Listener = null; + Listener = null; - Status = ListeningStatus.TooBusy; - _runtime.Tracker.Publish(new ListenerState(Uri, Endpoint.EndpointName, Status)); + Status = ListeningStatus.TooBusy; + _runtime.Tracker.Publish(new ListenerState(Uri, Endpoint.EndpointName, Status)); - _logger.LogInformation("Marked listener at {Uri} as too busy and stopped receiving", Uri); + _logger.LogInformation("Marked listener at {Uri} as too busy and stopped receiving", Uri); + } + finally + { + _semaphore.Release(); + } } private async ValueTask buildReceiverAsync() diff --git a/src/Wolverine/WolverineSystemPart.cs b/src/Wolverine/WolverineSystemPart.cs index 2a1c5dda1..e29a30136 100644 --- a/src/Wolverine/WolverineSystemPart.cs +++ b/src/Wolverine/WolverineSystemPart.cs @@ -202,7 +202,7 @@ public override async ValueTask> FindResources( if (!_runtime.Options.ExternalTransportsAreStubbed) { - foreach (var transport in _runtime.Options.Transports) + foreach (var transport in _runtime.Options.Transports.ToArray()) { if (transport.TryBuildStatefulResource(_runtime, out var resource)) { diff --git a/wolverine.sln b/wolverine.sln index be24428a8..736176458 100644 --- a/wolverine.sln +++ b/wolverine.sln @@ -305,6 +305,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "BackLogService", "DomainEve EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "DeepMiddlewareUsage", "src\Http\DeepMiddlewareUsage\DeepMiddlewareUsage.csproj", "{1D14EDEB-FCC6-400C-B74E-7DBD8199A151}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "LoadTesting", "src\Persistence\LoadTesting\LoadTesting.csproj", "{CA5FB523-D71D-4EF3-97B8-00CCDC05C00D}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -1707,6 +1709,18 @@ Global {1D14EDEB-FCC6-400C-B74E-7DBD8199A151}.Release|x64.Build.0 = Release|Any CPU {1D14EDEB-FCC6-400C-B74E-7DBD8199A151}.Release|x86.ActiveCfg = Release|Any CPU {1D14EDEB-FCC6-400C-B74E-7DBD8199A151}.Release|x86.Build.0 = Release|Any CPU + {CA5FB523-D71D-4EF3-97B8-00CCDC05C00D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {CA5FB523-D71D-4EF3-97B8-00CCDC05C00D}.Debug|Any CPU.Build.0 = Debug|Any CPU + {CA5FB523-D71D-4EF3-97B8-00CCDC05C00D}.Debug|x64.ActiveCfg = Debug|Any CPU + {CA5FB523-D71D-4EF3-97B8-00CCDC05C00D}.Debug|x64.Build.0 = Debug|Any CPU + {CA5FB523-D71D-4EF3-97B8-00CCDC05C00D}.Debug|x86.ActiveCfg = Debug|Any CPU + {CA5FB523-D71D-4EF3-97B8-00CCDC05C00D}.Debug|x86.Build.0 = Debug|Any CPU + {CA5FB523-D71D-4EF3-97B8-00CCDC05C00D}.Release|Any CPU.ActiveCfg = Release|Any CPU + {CA5FB523-D71D-4EF3-97B8-00CCDC05C00D}.Release|Any CPU.Build.0 = Release|Any CPU + {CA5FB523-D71D-4EF3-97B8-00CCDC05C00D}.Release|x64.ActiveCfg = Release|Any CPU + {CA5FB523-D71D-4EF3-97B8-00CCDC05C00D}.Release|x64.Build.0 = Release|Any CPU + {CA5FB523-D71D-4EF3-97B8-00CCDC05C00D}.Release|x86.ActiveCfg = Release|Any CPU + {CA5FB523-D71D-4EF3-97B8-00CCDC05C00D}.Release|x86.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -1848,5 +1862,6 @@ Global {B0C50461-1C9C-4867-8023-94AA6755257E} = {D953D733-D154-4DF2-B2B9-30BF942E1B6B} {912B3B0A-7CFB-4838-992F-DA5DBF70308C} = {B0C50461-1C9C-4867-8023-94AA6755257E} {1D14EDEB-FCC6-400C-B74E-7DBD8199A151} = {4B0BC1E5-17F9-4DD0-AC93-DDC522E1BE3C} + {CA5FB523-D71D-4EF3-97B8-00CCDC05C00D} = {7A9E0EAE-9ABF-40F6-9DB9-8FB1243F4210} EndGlobalSection EndGlobal