diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 73671fb7ec..3641c5547c 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -15,7 +15,7 @@ jobs: strategy: matrix: os: [ windows-2022 ] - test-category: [ Default, SqlServer, AzureServiceBus, RabbitMQ, AzureStorageQueues, MSMQ, SQS ] + test-category: [ Default, SqlServer, AzureServiceBus, RabbitMQ, AzureStorageQueues, MSMQ, SQS, Raven35Acceptance ] include: - os: windows-2022 os-name: Windows diff --git a/.gitignore b/.gitignore index 190ac12bc6..4ae2510ff1 100644 --- a/.gitignore +++ b/.gitignore @@ -100,3 +100,6 @@ src/scaffolding.config # Visual Studio Code .vscode + + +.transport diff --git a/src/Persisters.Primary.Includes.props b/src/Persisters.Primary.Includes.props new file mode 100644 index 0000000000..980c909b60 --- /dev/null +++ b/src/Persisters.Primary.Includes.props @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/src/ServiceControl.AcceptanceTesting/DispatchRawMessages.cs b/src/ServiceControl.AcceptanceTesting/DispatchRawMessages.cs index d6f3145f93..810f2824da 100644 --- a/src/ServiceControl.AcceptanceTesting/DispatchRawMessages.cs +++ b/src/ServiceControl.AcceptanceTesting/DispatchRawMessages.cs @@ -51,7 +51,7 @@ public DispatchTask(IDispatchMessages dispatcher, Func oper protected override async Task OnStart(IMessageSession session) { - await before(session).ConfigureAwait(false); + await before(session); var operations = operationFactory(); foreach (var op in operations.UnicastTransportOperations) { @@ -63,9 +63,8 @@ protected override async Task OnStart(IMessageSession session) op.Message.Headers["SC.SessionID"] = scenarioContext.TestRunId.ToString(); } - await dispatchMessages.Dispatch(operations, new TransportTransaction(), new ContextBag()) - .ConfigureAwait(false); - await after(session).ConfigureAwait(false); + await dispatchMessages.Dispatch(operations, new TransportTransaction(), new ContextBag()); + await after(session); } protected override Task OnStop(IMessageSession session) diff --git a/src/ServiceControl.AcceptanceTesting/EndpointTemplates/DefaultServerBase.cs b/src/ServiceControl.AcceptanceTesting/EndpointTemplates/DefaultServerBase.cs index d6098a04dc..4fc9f33368 100644 --- a/src/ServiceControl.AcceptanceTesting/EndpointTemplates/DefaultServerBase.cs +++ b/src/ServiceControl.AcceptanceTesting/EndpointTemplates/DefaultServerBase.cs @@ -51,7 +51,7 @@ public async Task GetConfiguration(RunDescriptor runDescr runDescriptor.OnTestCompleted(_ => endpointTestExecutionConfiguration.Cleanup()); endpointConfiguration.RegisterComponentsAndInheritanceHierarchy(runDescriptor); - await endpointConfiguration.DefinePersistence(runDescriptor, endpointCustomizations).ConfigureAwait(false); + await endpointConfiguration.DefinePersistence(runDescriptor, endpointCustomizations); typeof(ScenarioContext).GetProperty("CurrentEndpoint", BindingFlags.Static | BindingFlags.NonPublic).SetValue(runDescriptor.ScenarioContext, endpointCustomizations.EndpointName); diff --git a/src/ServiceControl.AcceptanceTesting/HttpExtensions.cs b/src/ServiceControl.AcceptanceTesting/HttpExtensions.cs index eb2f6ba421..9972ecb3ce 100644 --- a/src/ServiceControl.AcceptanceTesting/HttpExtensions.cs +++ b/src/ServiceControl.AcceptanceTesting/HttpExtensions.cs @@ -64,7 +64,7 @@ public static async Task> TryGetMany(this IAcceptanceTestInfras condition = _ => true; } - var response = await provider.GetInternal>(url).ConfigureAwait(false); + var response = await provider.GetInternal>(url); if (response == null || !response.Any(m => condition(m))) { @@ -83,13 +83,13 @@ public static async Task Patch(this IAcceptanceTestInfrastruc var json = JsonConvert.SerializeObject(payload, provider.SerializerSettings); var httpClient = provider.HttpClient; - var response = await httpClient.PatchAsync(url, new StringContent(json, null, "application/json")).ConfigureAwait(false); + var response = await httpClient.PatchAsync(url, new StringContent(json, null, "application/json")); Console.WriteLine($"PATCH - {url} - {(int)response.StatusCode}"); if (!response.IsSuccessStatusCode) { - var body = await response.Content.ReadAsStringAsync().ConfigureAwait(false); + var body = await response.Content.ReadAsStringAsync(); throw new InvalidOperationException($"Call failed: {(int)response.StatusCode} - {response.ReasonPhrase} - {body}"); } @@ -103,7 +103,7 @@ public static async Task> TryGet(this IAcceptanceTestInfrastr condition = _ => true; } - var response = await provider.GetInternal(url).ConfigureAwait(false); + var response = await provider.GetInternal(url); if (response == null || !condition(response)) { @@ -115,9 +115,9 @@ public static async Task> TryGet(this IAcceptanceTestInfrastr public static async Task> TryGet(this IAcceptanceTestInfrastructureProvider provider, string url, Func> condition) where T : class { - var response = await provider.GetInternal(url).ConfigureAwait(false); + var response = await provider.GetInternal(url); - if (response == null || !await condition(response).ConfigureAwait(false)) + if (response == null || !await condition(response)) { return SingleResult.Empty; } @@ -162,7 +162,7 @@ public static async Task Get(this IAcceptanceTestInfrastructureP } var httpClient = provider.HttpClient; - var response = await httpClient.GetAsync(url).ConfigureAwait(false); + var response = await httpClient.GetAsync(url); Console.WriteLine($"{response.RequestMessage.Method} - {url} - {(int)response.StatusCode}"); @@ -178,7 +178,7 @@ public static async Task Post(this IAcceptanceTestInfrastructureProvider prov var json = JsonConvert.SerializeObject(payload, provider.SerializerSettings); var httpClient = provider.HttpClient; - var response = await httpClient.PostAsync(url, new StringContent(json, null, "application/json")).ConfigureAwait(false); + var response = await httpClient.PostAsync(url, new StringContent(json, null, "application/json")); Console.WriteLine($"{response.RequestMessage.Method} - {url} - {(int)response.StatusCode}"); @@ -194,7 +194,7 @@ public static async Task Post(this IAcceptanceTestInfrastructureProvider prov if (!response.IsSuccessStatusCode) { - var body = await response.Content.ReadAsStringAsync().ConfigureAwait(false); + var body = await response.Content.ReadAsStringAsync(); throw new InvalidOperationException($"Call failed: {(int)response.StatusCode} - {response.ReasonPhrase} - {body}"); } } @@ -238,7 +238,7 @@ public static async Task DownloadData(this IAcceptanceTestInfrastructure static async Task GetInternal(this IAcceptanceTestInfrastructureProvider provider, string url) where T : class { - var response = await provider.GetRaw(url).ConfigureAwait(false); + var response = await provider.GetRaw(url); //for now if (response.StatusCode == HttpStatusCode.NotFound || response.StatusCode == HttpStatusCode.ServiceUnavailable) @@ -254,7 +254,7 @@ static async Task GetInternal(this IAcceptanceTestInfrastructureProvider p throw new InvalidOperationException($"Call failed: {(int)response.StatusCode} - {response.ReasonPhrase} {Environment.NewLine} {content}"); } - var body = await response.Content.ReadAsStringAsync().ConfigureAwait(false); + var body = await response.Content.ReadAsStringAsync(); LogRequest(); return JsonConvert.DeserializeObject(body, provider.SerializerSettings); diff --git a/src/ServiceControl.AcceptanceTesting/InfrastructureConfig/ConfigureEndpointLearningTransport.cs b/src/ServiceControl.AcceptanceTesting/InfrastructureConfig/ConfigureEndpointLearningTransport.cs index e3cacdb532..9c174981d0 100644 --- a/src/ServiceControl.AcceptanceTesting/InfrastructureConfig/ConfigureEndpointLearningTransport.cs +++ b/src/ServiceControl.AcceptanceTesting/InfrastructureConfig/ConfigureEndpointLearningTransport.cs @@ -9,6 +9,14 @@ public class ConfigureEndpointLearningTransport : ITransportIntegration { + public ConfigureEndpointLearningTransport() + { + var relativePath = Path.Combine(TestContext.CurrentContext.TestDirectory, @"..", "..", "..", ".transport"); + ConnectionString = Path.GetFullPath(relativePath); + } + + public string ConnectionString { get; set; } + public Task Configure(string endpointName, EndpointConfiguration configuration, RunSettings settings, PublisherMetadata publisherMetadata) { Directory.CreateDirectory(ConnectionString); @@ -24,7 +32,13 @@ public Task Cleanup() { if (Directory.Exists(ConnectionString)) { - Directory.Delete(ConnectionString, true); + try + { + Directory.Delete(ConnectionString, true); + } + catch (DirectoryNotFoundException) + { + } } return Task.FromResult(0); @@ -32,6 +46,5 @@ public Task Cleanup() public string Name => "Learning"; public string TypeName => $"{typeof(LearningTransportCustomization).AssemblyQualifiedName}"; - public string ConnectionString { get; set; } = Path.Combine(TestContext.CurrentContext.TestDirectory, @"..\..\..\.transport"); } } \ No newline at end of file diff --git a/src/ServiceControl.AcceptanceTesting/NServiceBusAcceptanceTest.cs b/src/ServiceControl.AcceptanceTesting/NServiceBusAcceptanceTest.cs index 62901bba3e..09acd6124c 100644 --- a/src/ServiceControl.AcceptanceTesting/NServiceBusAcceptanceTest.cs +++ b/src/ServiceControl.AcceptanceTesting/NServiceBusAcceptanceTest.cs @@ -10,6 +10,7 @@ /// Base class for all the NSB test that sets up our conventions /// [TestFixture] + [FixtureLifeCycle(LifeCycle.InstancePerTestCase)] public abstract partial class NServiceBusAcceptanceTest { [SetUp] diff --git a/src/ServiceControl.AcceptanceTesting/ScenarioWithEndpointBehaviorExtensions.cs b/src/ServiceControl.AcceptanceTesting/ScenarioWithEndpointBehaviorExtensions.cs index 38b166c70e..b14ecad05f 100644 --- a/src/ServiceControl.AcceptanceTesting/ScenarioWithEndpointBehaviorExtensions.cs +++ b/src/ServiceControl.AcceptanceTesting/ScenarioWithEndpointBehaviorExtensions.cs @@ -109,13 +109,13 @@ public override Task ComponentsStarted(CancellationToken cancellationToken = def { while (!tokenSource.IsCancellationRequested) { - if (await isDone(scenarioContext).ConfigureAwait(false)) + if (await isDone(scenarioContext)) { setDone(); return; } - await Task.Delay(100, tokenSource.Token).ConfigureAwait(false); + await Task.Delay(100, tokenSource.Token); } } catch (Exception e) @@ -136,7 +136,7 @@ public override async Task Stop() tokenSource.Cancel(); try { - await checkTask.ConfigureAwait(false); + await checkTask; } catch (OperationCanceledException) { diff --git a/src/ServiceControl.AcceptanceTesting/Sequence.cs b/src/ServiceControl.AcceptanceTesting/Sequence.cs index f4599815e4..37c366fea6 100644 --- a/src/ServiceControl.AcceptanceTesting/Sequence.cs +++ b/src/ServiceControl.AcceptanceTesting/Sequence.cs @@ -18,7 +18,7 @@ public Sequence Do(string step, Func handler) { steps.Add(async context => { - await handler(context).ConfigureAwait(false); + await handler(context); return true; }); stepNames.Add(step); @@ -39,7 +39,7 @@ public async Task Continue(TContext context) } var step = steps[currentStep]; - var advance = await step(context).ConfigureAwait(false); + var advance = await step(context); if (advance) { var nextStep = currentStep + 1; diff --git a/src/ServiceControl.AcceptanceTests.RavenDB/AcceptanceTestStorageConfiguration.cs b/src/ServiceControl.AcceptanceTests.RavenDB/AcceptanceTestStorageConfiguration.cs new file mode 100644 index 0000000000..c0ec329d4b --- /dev/null +++ b/src/ServiceControl.AcceptanceTests.RavenDB/AcceptanceTestStorageConfiguration.cs @@ -0,0 +1,50 @@ +namespace ServiceControl.AcceptanceTests +{ + using System.Collections.Generic; + using System.Linq; + using System.Net.NetworkInformation; + using System.Threading.Tasks; + using Persistence.RavenDb; + + class AcceptanceTestStorageConfiguration + { + public string PersistenceType { get; protected set; } + + public Task> CustomizeSettings() + { + return Task.FromResult>(new Dictionary + { + { "RavenDB35/RunInMemory", bool.TrueString}, + { "DatabaseMaintenancePort", FindAvailablePort(33334).ToString()}, + { "HostName", "localhost" } + }); + } + + public Task Configure() + { + PersistenceType = typeof(RavenDbPersistenceConfiguration).AssemblyQualifiedName; + + return Task.CompletedTask; + } + + static int FindAvailablePort(int startPort) + { + var activeTcpListeners = IPGlobalProperties + .GetIPGlobalProperties() + .GetActiveTcpListeners(); + + for (var port = startPort; port < startPort + 1024; port++) + { + var portCopy = port; + if (activeTcpListeners.All(endPoint => endPoint.Port != portCopy)) + { + return port; + } + } + + return startPort; + } + + public Task Cleanup() => Task.CompletedTask; + } +} diff --git a/src/ServiceControl.AcceptanceTests/App.config b/src/ServiceControl.AcceptanceTests.RavenDB/App.config similarity index 100% rename from src/ServiceControl.AcceptanceTests/App.config rename to src/ServiceControl.AcceptanceTests.RavenDB/App.config diff --git a/src/ServiceControl.AcceptanceTests.RavenDB/Legacy/AuditRetention.cs b/src/ServiceControl.AcceptanceTests.RavenDB/Legacy/AuditRetention.cs new file mode 100644 index 0000000000..84979bf317 --- /dev/null +++ b/src/ServiceControl.AcceptanceTests.RavenDB/Legacy/AuditRetention.cs @@ -0,0 +1,114 @@ +namespace ServiceControl.MultiInstance.AcceptanceTests.SagaAudit +{ + using System; + using System.Linq; + using System.Threading.Tasks; + using AcceptanceTesting; + using EventLog; + using Microsoft.Extensions.DependencyInjection; + using NServiceBus; + using NServiceBus.AcceptanceTesting; + using NServiceBus.CustomChecks; + using NUnit.Framework; + using Raven.Client; + using ServiceBus.Management.Infrastructure.Settings; + using ServiceControl.AcceptanceTests; + using ServiceControl.AcceptanceTests.TestSupport.EndpointTemplates; + using ServiceControl.Persistence; + using ServiceControl.Persistence.RavenDb.SagaAudit; + using ServiceControl.SagaAudit; + + + class AuditRetention : AcceptanceTest + { + [Test] + public async Task + Check_fails_if_no_audit_retention_is_configured() + { + ////Ensure custom checks are enabled + SetSettings = settings => + { + settings.DisableHealthChecks = false; + settings.AuditRetentionPeriod = TimeSpan.FromSeconds(5); + }; + + //Override the configuration of the check in the container in order to make it run more frequently for testing purposes. + CustomizeHostBuilder = hostBuilder => hostBuilder.ConfigureServices((ctx, services) => + services.AddTransient(provider => new AuditRetentionCustomCheck(provider.GetRequiredService(), provider.GetRequiredService(), TimeSpan.FromSeconds(10)))); + + SingleResult customCheckEventEntry = default; + bool sagaAudiDataInMainInstanceIsAvailableForQuery = false; + + await Define() + .WithEndpoint(b => b.When((bus, c) => bus.SendLocal(new MessageInitiatingSaga { Id = "Id" }))) + .Done(async c => + { + if (!c.SagaId.HasValue) + { + return false; + } + + if (sagaAudiDataInMainInstanceIsAvailableForQuery == false) + { + var sagaData = + await this.TryGet($"/api/sagas/{c.SagaId}"); + sagaAudiDataInMainInstanceIsAvailableForQuery = sagaData.HasResult; + return false; + } + + customCheckEventEntry = await this.TryGetSingle("/api/eventlogitems/", + e => e.EventType == "CustomCheckFailed" && e.Description.StartsWith("Saga Audit Data Retention")); + + return customCheckEventEntry; + }) + .Run(); + + Assert.IsTrue(customCheckEventEntry.Item.RelatedTo.Any(item => item == "/customcheck/Saga Audit Data Retention"), "Event log entry should be related to the Saga Audit Data Retention"); + Assert.IsTrue(customCheckEventEntry.Item.RelatedTo.Any(item => item.StartsWith("/endpoint/Particular.ServiceControl")), "Event log entry should be related to the ServiceControl endpoint"); + } + + public class SagaEndpoint : EndpointConfigurationBuilder + { + public SagaEndpoint() + { + EndpointSetup(c => + { + c.AuditSagaStateChanges(Settings.DEFAULT_SERVICE_NAME); + }); + } + + public class MySaga : Saga, IAmStartedByMessages + { + public MyContext TestContext { get; set; } + + public Task Handle(MessageInitiatingSaga message, IMessageHandlerContext context) + { + TestContext.SagaId = Data.Id; + return Task.CompletedTask; + } + + protected override void ConfigureHowToFindSaga(SagaPropertyMapper mapper) + { + mapper.ConfigureMapping(msg => msg.Id).ToSaga(saga => saga.MessageId); + } + } + + public class MySagaData : ContainSagaData + { + public string MessageId { get; set; } + } + } + + + public class MessageInitiatingSaga : ICommand + { + public string Id { get; set; } + } + + + public class MyContext : ScenarioContext + { + public Guid? SagaId { get; set; } + } + } +} \ No newline at end of file diff --git a/src/ServiceControl.AcceptanceTests/Legacy/When_processed_message_is_still_available.cs b/src/ServiceControl.AcceptanceTests.RavenDB/Legacy/When_processed_message_is_still_available.cs similarity index 80% rename from src/ServiceControl.AcceptanceTests/Legacy/When_processed_message_is_still_available.cs rename to src/ServiceControl.AcceptanceTests.RavenDB/Legacy/When_processed_message_is_still_available.cs index 709810ab15..51db54d943 100644 --- a/src/ServiceControl.AcceptanceTests/Legacy/When_processed_message_is_still_available.cs +++ b/src/ServiceControl.AcceptanceTests.RavenDB/Legacy/When_processed_message_is_still_available.cs @@ -1,4 +1,4 @@ -namespace ServiceControl.AcceptanceTests.Legacy +namespace ServiceControl.AcceptanceTests.RavenDB.Legacy { using System; using System.Collections.Generic; @@ -8,9 +8,9 @@ using AcceptanceTesting; using AcceptanceTests; using CompositeViews.Messages; - using Infrastructure.RavenDB; using MessageAuditing; using Microsoft.Extensions.DependencyInjection; + using Microsoft.Extensions.Hosting; using NServiceBus; using NServiceBus.AcceptanceTesting; using NUnit.Framework; @@ -28,7 +28,7 @@ public async Task Should_be_accessible_via_the_rest_api() CustomizeHostBuilder = hostBuilder => hostBuilder.ConfigureServices((hostBuilderContext, services) - => services.AddSingleton(new CreateMessageDataMigration(messageId))); + => services.AddHostedService(sp => new CreateMessageDataMigration(messageId, sp.GetRequiredService()))); MessagesView auditedMessage = null; byte[] body = null; @@ -58,13 +58,18 @@ public async Task Should_be_accessible_via_the_rest_api() Assert.AreEqual("Some Content", bodyAsString); } - class CreateMessageDataMigration : IDataMigration + class CreateMessageDataMigration : IHostedService { readonly string messageId; + readonly IDocumentStore store; - public CreateMessageDataMigration(string messageId) => this.messageId = messageId; + public CreateMessageDataMigration(string messageId, IDocumentStore store) + { + this.messageId = messageId; + this.store = store; + } - public async Task Migrate(IDocumentStore store) + public async Task StartAsync(CancellationToken cancellationToken) { using (var documentSession = store.OpenAsyncSession()) { @@ -84,13 +89,14 @@ public async Task Migrate(IDocumentStore store) ProcessedAt = DateTime.UtcNow }; - await documentSession.StoreAsync(processedMessage); - await documentSession.SaveChangesAsync(); + await documentSession.StoreAsync(processedMessage, cancellationToken); + await documentSession.SaveChangesAsync(cancellationToken); } SpinWait.SpinUntil(() => store.DatabaseCommands.GetStatistics().StaleIndexes.Length == 0, TimeSpan.FromSeconds(10)); - } + + public Task StopAsync(CancellationToken cancellationToken) => Task.CompletedTask; } public class Endpoint : EndpointConfigurationBuilder diff --git a/src/ServiceControl.AcceptanceTests/Recoverability/MessageFailures/FailedErrorsController.cs b/src/ServiceControl.AcceptanceTests.RavenDB/Recoverability/MessageFailures/FailedErrorsController.cs similarity index 95% rename from src/ServiceControl.AcceptanceTests/Recoverability/MessageFailures/FailedErrorsController.cs rename to src/ServiceControl.AcceptanceTests.RavenDB/Recoverability/MessageFailures/FailedErrorsController.cs index 4885e33c32..d266f8d60b 100644 --- a/src/ServiceControl.AcceptanceTests/Recoverability/MessageFailures/FailedErrorsController.cs +++ b/src/ServiceControl.AcceptanceTests.RavenDB/Recoverability/MessageFailures/FailedErrorsController.cs @@ -1,4 +1,4 @@ -namespace ServiceControl.AcceptanceTests.Recoverability.MessageFailures +namespace ServiceControl.AcceptanceTests.RavenDB.Recoverability.MessageFailures { using System; using System.Net; @@ -6,11 +6,11 @@ using System.Threading; using System.Threading.Tasks; using System.Web.Http; + using Infrastructure.RavenDB.Expiration; using Infrastructure.WebApi; using Operations; using Raven.Client; using Raven.Client.Embedded; - using Infrastructure.RavenDB.Expiration; public class FailedErrorsCountReponse { @@ -40,7 +40,7 @@ public async Task GetFailedErrorsCount() { Count = count }) - .WithEtag(stats); + .WithEtag(stats.IndexEtag); } } diff --git a/src/ServiceControl.AcceptanceTests/Recoverability/MessageFailures/FailedMessageRetriesController.cs b/src/ServiceControl.AcceptanceTests.RavenDB/Recoverability/MessageFailures/FailedMessageRetriesController.cs similarity index 85% rename from src/ServiceControl.AcceptanceTests/Recoverability/MessageFailures/FailedMessageRetriesController.cs rename to src/ServiceControl.AcceptanceTests.RavenDB/Recoverability/MessageFailures/FailedMessageRetriesController.cs index 9b149f5dd1..e2ac53e9b2 100644 --- a/src/ServiceControl.AcceptanceTests/Recoverability/MessageFailures/FailedMessageRetriesController.cs +++ b/src/ServiceControl.AcceptanceTests.RavenDB/Recoverability/MessageFailures/FailedMessageRetriesController.cs @@ -1,4 +1,4 @@ -namespace ServiceControl.AcceptanceTests.Recoverability.MessageFailures +namespace ServiceControl.AcceptanceTests.RavenDB.Recoverability.MessageFailures { using System.Net; using System.Net.Http; @@ -26,13 +26,13 @@ public async Task GetFailedMessageRetriesCount() { using (var session = store.OpenAsyncSession()) { - await session.Query().Statistics(out var stats).ToListAsync().ConfigureAwait(false); + await session.Query().Statistics(out var stats).ToListAsync(); return Request.CreateResponse(HttpStatusCode.OK, new FailedMessageRetriesCountReponse { Count = stats.TotalResults }) - .WithEtag(stats); + .WithEtag(stats.IndexEtag); } } diff --git a/src/ServiceControl.AcceptanceTests/Recoverability/MessageFailures/When_a_failed_message_is_retried.cs b/src/ServiceControl.AcceptanceTests.RavenDB/Recoverability/MessageFailures/When_a_failed_message_is_retried.cs similarity index 96% rename from src/ServiceControl.AcceptanceTests/Recoverability/MessageFailures/When_a_failed_message_is_retried.cs rename to src/ServiceControl.AcceptanceTests.RavenDB/Recoverability/MessageFailures/When_a_failed_message_is_retried.cs index 7a52dad9d2..5a7a9415d5 100644 --- a/src/ServiceControl.AcceptanceTests/Recoverability/MessageFailures/When_a_failed_message_is_retried.cs +++ b/src/ServiceControl.AcceptanceTests.RavenDB/Recoverability/MessageFailures/When_a_failed_message_is_retried.cs @@ -1,4 +1,4 @@ -namespace ServiceControl.AcceptanceTests.Recoverability.MessageFailures +namespace ServiceControl.AcceptanceTests.RavenDB.Recoverability.MessageFailures { using System; using System.Collections.Generic; @@ -6,6 +6,7 @@ using System.Threading.Tasks; using AcceptanceTesting; using Infrastructure; + using Microsoft.Extensions.DependencyInjection; using NServiceBus; using NServiceBus.AcceptanceTesting; using NServiceBus.Features; @@ -18,6 +19,15 @@ class When_a_failed_message_is_retried : AcceptanceTest { + public When_a_failed_message_is_retried() + { + CustomizeHostBuilder = builder => builder.ConfigureServices((hostContext, services) => + { + services.AddScoped(); + services.AddScoped(); + }); + } + [Test] public async Task Should_remove_failedmessageretries_when_retrying_groups() { diff --git a/src/ServiceControl.AcceptanceTests/Recoverability/MessageFailures/When_a_message_fails_to_import.cs b/src/ServiceControl.AcceptanceTests.RavenDB/Recoverability/MessageFailures/When_a_message_fails_to_import.cs similarity index 95% rename from src/ServiceControl.AcceptanceTests/Recoverability/MessageFailures/When_a_message_fails_to_import.cs rename to src/ServiceControl.AcceptanceTests.RavenDB/Recoverability/MessageFailures/When_a_message_fails_to_import.cs index e6bfc688f2..180e9f2813 100644 --- a/src/ServiceControl.AcceptanceTests/Recoverability/MessageFailures/When_a_message_fails_to_import.cs +++ b/src/ServiceControl.AcceptanceTests.RavenDB/Recoverability/MessageFailures/When_a_message_fails_to_import.cs @@ -1,4 +1,4 @@ -namespace ServiceControl.AcceptanceTests.Recoverability.MessageFailures +namespace ServiceControl.AcceptanceTests.RavenDB.Recoverability.MessageFailures { using System; using System.Threading.Tasks; @@ -6,6 +6,8 @@ using Contracts.MessageFailures; using Infrastructure; using Infrastructure.DomainEvents; + using Microsoft.Extensions.DependencyInjection; + using Microsoft.Extensions.Hosting; using NServiceBus; using NServiceBus.AcceptanceTesting; using NServiceBus.AcceptanceTesting.Customization; @@ -40,6 +42,8 @@ public async Task It_can_be_reimported() settings.ErrorLogQueue = Conventions.EndpointNamingConvention(typeof(ErrorLogSpy)); }; + CustomizeHostBuilder = builder => builder.ConfigureServices(services => services.AddScoped()); + var runResult = await Define() .WithEndpoint(b => b.When((bus, c) => bus.Send(new MyMessage()))) .WithEndpoint(b => b.DoNotFailOnErrorMessages()) diff --git a/src/ServiceControl.AcceptanceTests/ServiceControl.AcceptanceTests.csproj b/src/ServiceControl.AcceptanceTests.RavenDB/ServiceControl.AcceptanceTests.RavenDB.csproj similarity index 83% rename from src/ServiceControl.AcceptanceTests/ServiceControl.AcceptanceTests.csproj rename to src/ServiceControl.AcceptanceTests.RavenDB/ServiceControl.AcceptanceTests.RavenDB.csproj index 48f2fd6b08..be29578e9e 100644 --- a/src/ServiceControl.AcceptanceTests/ServiceControl.AcceptanceTests.csproj +++ b/src/ServiceControl.AcceptanceTests.RavenDB/ServiceControl.AcceptanceTests.RavenDB.csproj @@ -7,12 +7,10 @@ - - - + @@ -20,10 +18,14 @@ - + - \ No newline at end of file + + + + + diff --git a/src/ServiceControl.AcceptanceTests/ServiceControl.runsettings b/src/ServiceControl.AcceptanceTests.RavenDB/ServiceControl.runsettings similarity index 100% rename from src/ServiceControl.AcceptanceTests/ServiceControl.runsettings rename to src/ServiceControl.AcceptanceTests.RavenDB/ServiceControl.runsettings diff --git a/src/ServiceControl.AcceptanceTests.RavenDB/TestsFilter.cs b/src/ServiceControl.AcceptanceTests.RavenDB/TestsFilter.cs new file mode 100644 index 0000000000..b207ccee13 --- /dev/null +++ b/src/ServiceControl.AcceptanceTests.RavenDB/TestsFilter.cs @@ -0,0 +1,2 @@ +// This project alone tends to take ~13 minutes on GitHub Actions runner +[assembly: IncludeInTestCategory("Raven35Acceptance")] \ No newline at end of file diff --git a/src/ServiceControl.AcceptanceTests/Monitoring/CustomChecks/When_critical_storage_threshold_reached.cs b/src/ServiceControl.AcceptanceTests/Monitoring/CustomChecks/When_critical_storage_threshold_reached.cs index 5f718cfaf4..77c357fb30 100644 --- a/src/ServiceControl.AcceptanceTests/Monitoring/CustomChecks/When_critical_storage_threshold_reached.cs +++ b/src/ServiceControl.AcceptanceTests/Monitoring/CustomChecks/When_critical_storage_threshold_reached.cs @@ -2,32 +2,53 @@ { using System; using System.Linq; + using System.Threading; using System.Threading.Tasks; using AcceptanceTesting; using MessageFailures.Api; + using Microsoft.Extensions.DependencyInjection; + using Microsoft.Extensions.Hosting; using NServiceBus; using NServiceBus.AcceptanceTesting; using NUnit.Framework; + using Persistence; using ServiceBus.Management.Infrastructure.Settings; using TestSupport.EndpointTemplates; [TestFixture] class When_critical_storage_threshold_reached : AcceptanceTest { - [SetUp] - public void SetupIngestion() => + public void SetupIngestion() + { SetSettings = s => { s.TimeToRestartErrorIngestionAfterFailure = TimeSpan.FromSeconds(1); s.DisableHealthChecks = false; - s.MinimumStorageLeftRequiredForIngestion = 0; }; + } + class PersisterSettingsRetriever : IHostedService + { + static PersistenceSettings _persistenceSettings; + + public PersisterSettingsRetriever(PersistenceSettings persistenceSettings, int value) + { + _persistenceSettings = persistenceSettings; + SetMinimumStorageLeftForIngestion(value); + } + + public Task StartAsync(CancellationToken cancellationToken) => Task.CompletedTask; + public Task StopAsync(CancellationToken cancellationToken) => Task.CompletedTask; + + public static void SetMinimumStorageLeftForIngestion(int value) => _persistenceSettings.PersisterSpecificSettings["MinimumStorageLeftRequiredForIngestion"] = value.ToString(); + } [Test] public async Task Should_stop_ingestion() { + CustomizeHostBuilder = hostBuilder => hostBuilder.ConfigureServices(services => services.AddHostedService(b => new PersisterSettingsRetriever(b.GetRequiredService(), 0))); + await Define() .WithEndpoint(b => b .When(context => @@ -35,7 +56,7 @@ await Define() return context.Logs.ToArray().Any(i => i.Message.StartsWith("Ensure started. Infrastructure started")); }, (_, __) => { - Settings.MinimumStorageLeftRequiredForIngestion = 100; + PersisterSettingsRetriever.SetMinimumStorageLeftForIngestion(100); return Task.CompletedTask; }) .When(context => @@ -46,13 +67,14 @@ await Define() }, (bus, c) => bus.SendLocal(new MyMessage()) ) .DoNotFailOnErrorMessages()) - .Done(async c => await this.TryGetSingle("/api/errors") == false) + //.Done(async c => await this.TryGetSingle("/api/errors") == false) .Run(); } [Test] public async Task Should_stop_ingestion_and_resume_when_more_space_is_available() { + CustomizeHostBuilder = hostBuilder => hostBuilder.ConfigureServices(services => services.AddHostedService(b => new PersisterSettingsRetriever(b.GetRequiredService(), 0))); var ingestionShutdown = false; await Define() @@ -64,7 +86,7 @@ await Define() "Ensure started. Infrastructure started")); }, (session, context) => { - Settings.MinimumStorageLeftRequiredForIngestion = 100; + PersisterSettingsRetriever.SetMinimumStorageLeftForIngestion(100); return Task.CompletedTask; }) .When(context => @@ -81,7 +103,7 @@ await Define() }) .When(c => ingestionShutdown, (session, context) => { - Settings.MinimumStorageLeftRequiredForIngestion = 0; + PersisterSettingsRetriever.SetMinimumStorageLeftForIngestion(0); return Task.CompletedTask; }) .DoNotFailOnErrorMessages()) diff --git a/src/ServiceControl.AcceptanceTests/Monitoring/CustomChecks/When_email_notifications_are_enabled.cs b/src/ServiceControl.AcceptanceTests/Monitoring/CustomChecks/When_email_notifications_are_enabled.cs index c5d027e83e..2b673fc322 100644 --- a/src/ServiceControl.AcceptanceTests/Monitoring/CustomChecks/When_email_notifications_are_enabled.cs +++ b/src/ServiceControl.AcceptanceTests/Monitoring/CustomChecks/When_email_notifications_are_enabled.cs @@ -1,18 +1,20 @@ namespace ServiceControl.AcceptanceTests.Monitoring.CustomChecks { - using System; - using System.IO; - using System.Linq; - using System.Threading.Tasks; - using Infrastructure.RavenDB; using Microsoft.Extensions.DependencyInjection; + using Microsoft.Extensions.Hosting; using Notifications; using NServiceBus; using NServiceBus.AcceptanceTesting; using NServiceBus.CustomChecks; using NUnit.Framework; - using Raven.Client; using ServiceBus.Management.Infrastructure.Settings; + using ServiceControl.Persistence; + using ServiceControl.Persistence.RavenDb.Editing; + using System; + using System.IO; + using System.Linq; + using System.Threading; + using System.Threading.Tasks; using TestSupport.EndpointTemplates; [TestFixture] @@ -32,8 +34,7 @@ public async Task Should_send_custom_check_status_change_emails() }; CustomizeHostBuilder = hostBuilder => - hostBuilder.ConfigureServices((hostBuilderContext, services) => - services.AddSingleton()); + hostBuilder.ConfigureServices(services => services.AddHostedService()); await Define(c => { @@ -58,31 +59,31 @@ await Define(c => Assert.AreEqual("Subject: [Particular.ServiceControl] health check failed", emailText[6]); } - class CreateNotificationsDataMigration : IDataMigration + class SetupNotificationSettings : IHostedService { - public Task Migrate(IDocumentStore store) + IErrorMessageDataStore errorMessageDataStore; + + public SetupNotificationSettings(IErrorMessageDataStore errorMessageDataStore) { - using (var session = store.OpenSession()) - { - var notificationsSettings = new NotificationsSettings - { - Id = NotificationsSettings.SingleDocumentId, - Email = new EmailNotifications - { - Enabled = true, - From = "YouServiceControl@particular.net", - To = "WhoeverMightBeConcerned@particular.net", - } - }; - - session.Store(notificationsSettings); - - session.SaveChanges(); - } + this.errorMessageDataStore = errorMessageDataStore; + } - return Task.CompletedTask; + public async Task StartAsync(CancellationToken cancellationToken) + { + var notificationsManager = await errorMessageDataStore.CreateNotificationsManager(); + + var settings = await notificationsManager.LoadSettings(); + settings.Email = new EmailNotifications + { + Enabled = true, + From = "YouServiceControl@particular.net", + To = "WhoeverMightBeConcerned@particular.net", + }; + await notificationsManager.SaveChanges(); } + + public Task StopAsync(CancellationToken cancellationToken) => Task.CompletedTask; } public class MyContext : ScenarioContext diff --git a/src/ServiceControl.AcceptanceTests/Monitoring/InternalCustomChecks/When_a_critical_error_is_triggered.cs b/src/ServiceControl.AcceptanceTests/Monitoring/InternalCustomChecks/When_a_critical_error_is_triggered.cs index 0e6ee741bb..4c97cd1571 100644 --- a/src/ServiceControl.AcceptanceTests/Monitoring/InternalCustomChecks/When_a_critical_error_is_triggered.cs +++ b/src/ServiceControl.AcceptanceTests/Monitoring/InternalCustomChecks/When_a_critical_error_is_triggered.cs @@ -1,48 +1,42 @@ namespace ServiceControl.AcceptanceTests.Monitoring.InternalCustomChecks { using System; - using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; using AcceptanceTesting; using Contracts.CustomChecks; using EventLog; + using Microsoft.Extensions.DependencyInjection; using NServiceBus.AcceptanceTesting; - using NServiceBus.CustomChecks; using NUnit.Framework; - using Operations; - using ServiceControl.CustomChecks; + using ServiceControl.Operations; [TestFixture] - class When_a_critical_error_is_triggered : AcceptanceTest { - static Type[] allServiceControlTypes = typeof(InternalCustomChecks).Assembly.GetTypes(); - static IEnumerable allTypesExcludingBuiltInCustomChecks = allServiceControlTypes.Where(t => !t.GetInterfaces().Contains(typeof(ICustomCheck))); - static Type[] customChecksUnderTest = { typeof(CriticalErrorCustomCheck) }; - static IEnumerable typesToScan = allTypesExcludingBuiltInCustomChecks.Concat(customChecksUnderTest); - [Test] public async Task Service_control_is_not_killed_and_error_is_reported_via_custom_check() { + CustomizeHostBuilder = builder => builder.ConfigureServices((hostContext, services) => + { + services.AddScoped(); + services.AddTransient(_ => new CriticalErrorCustomCheck(TimeSpan.FromSeconds(1))); // Overrides existing registration to have an increased test interval + }); + SetSettings = settings => { settings.DisableHealthChecks = false; }; EventLogItem entry = null; - var criticalErrorTriggered = false; - await Define() .Done(async c => { - if (!criticalErrorTriggered) - { - await this.Post("/api/criticalerror/trigger?message=Failed"); - criticalErrorTriggered = true; - return false; - } - + await this.Post("/api/criticalerror/trigger?message=Failed"); + return true; + }) + .Done(async c => + { var result = await this.TryGetSingle("/api/eventlogitems/", e => e.EventType == nameof(CustomCheckFailed) && e.Description.Contains("ServiceControl Primary Instance")); entry = result; return result; diff --git a/src/ServiceControl.AcceptanceTests/Monitoring/When_a_failed_message_from_unmonitored_endpoint_is_imported.cs b/src/ServiceControl.AcceptanceTests/Monitoring/When_a_failed_message_from_unmonitored_endpoint_is_imported.cs index 8fb75d4ddd..ed41fb4d24 100644 --- a/src/ServiceControl.AcceptanceTests/Monitoring/When_a_failed_message_from_unmonitored_endpoint_is_imported.cs +++ b/src/ServiceControl.AcceptanceTests/Monitoring/When_a_failed_message_from_unmonitored_endpoint_is_imported.cs @@ -4,6 +4,7 @@ using System.Linq; using System.Threading.Tasks; using AcceptanceTesting; + using Microsoft.Extensions.DependencyInjection; using NServiceBus; using NServiceBus.AcceptanceTesting; using NServiceBus.Settings; @@ -38,6 +39,8 @@ public async Task It_is_detected() [Test] public async Task It_is_persisted() { + CustomizeHostBuilder = builder => builder.ConfigureServices((hostContext, services) => services.AddScoped()); + var endpointName = Conventions.EndpointNamingConvention(typeof(Receiver)); KnownEndpoint endpoint = default; diff --git a/src/ServiceControl.AcceptanceTests/Monitoring/When_an_endpoint_with_heartbeat_plugin_starts_up.cs b/src/ServiceControl.AcceptanceTests/Monitoring/When_an_endpoint_with_heartbeat_plugin_starts_up.cs index 6f0bc88691..257ed67318 100644 --- a/src/ServiceControl.AcceptanceTests/Monitoring/When_an_endpoint_with_heartbeat_plugin_starts_up.cs +++ b/src/ServiceControl.AcceptanceTests/Monitoring/When_an_endpoint_with_heartbeat_plugin_starts_up.cs @@ -2,6 +2,7 @@ { using System.Threading.Tasks; using AcceptanceTesting; + using Microsoft.Extensions.DependencyInjection; using NServiceBus; using NServiceBus.AcceptanceTesting; using NUnit.Framework; @@ -37,6 +38,8 @@ await Define() [Test] public async Task Should_be_persisted() { + CustomizeHostBuilder = builder => builder.ConfigureServices((hostContext, services) => services.AddScoped()); + var endpointName = Conventions.EndpointNamingConvention(typeof(StartingEndpoint)); KnownEndpoint endpoint = default; diff --git a/src/ServiceControl.AcceptanceTests/Recoverability/ExternalIntegration/ExternalIntegrationAcceptanceTest.cs b/src/ServiceControl.AcceptanceTests/Recoverability/ExternalIntegration/ExternalIntegrationAcceptanceTest.cs index c523a954fe..76b0cd95ba 100644 --- a/src/ServiceControl.AcceptanceTests/Recoverability/ExternalIntegration/ExternalIntegrationAcceptanceTest.cs +++ b/src/ServiceControl.AcceptanceTests/Recoverability/ExternalIntegration/ExternalIntegrationAcceptanceTest.cs @@ -54,7 +54,7 @@ OutgoingMessage CreateTransportMessage(string messageId) {"NServiceBus.ProcessingEndpoint", nameof(ErrorSender)}, {Headers.TimeSent, DateTimeExtensions.ToWireFormattedString(date)}, {Headers.EnclosedMessageTypes, typeof(AMessage).AssemblyQualifiedName} - }, new byte[0]); + }, Array.Empty()); return msg; } } diff --git a/src/ServiceControl.AcceptanceTests/Recoverability/ExternalIntegration/When_a_group_is_archived.cs b/src/ServiceControl.AcceptanceTests/Recoverability/ExternalIntegration/When_a_group_is_archived.cs index 3f528eeaf8..2b42a72d7c 100644 --- a/src/ServiceControl.AcceptanceTests/Recoverability/ExternalIntegration/When_a_group_is_archived.cs +++ b/src/ServiceControl.AcceptanceTests/Recoverability/ExternalIntegration/When_a_group_is_archived.cs @@ -52,7 +52,7 @@ public async Task Should_publish_notification() ctx => Task.FromResult(ctx.ExternalProcessorSubscribed)) .Do("WaitUntilGroupContainsBothMessages", async ctx => { - var failedMessages = await this.TryGetMany($"/api/recoverability/groups/{ctx.GroupId}/errors").ConfigureAwait(false); + var failedMessages = await this.TryGetMany($"/api/recoverability/groups/{ctx.GroupId}/errors"); return failedMessages && failedMessages.Items.Count == 1; }) .Do("Archive", async ctx => { await this.Post($"/api/recoverability/groups/{ctx.GroupId}/errors/archive"); }) diff --git a/src/ServiceControl.AcceptanceTests/Recoverability/ExternalIntegration/When_encountered_an_error.cs b/src/ServiceControl.AcceptanceTests/Recoverability/ExternalIntegration/When_encountered_an_error.cs index 6cbabd9439..d7ae31bddd 100644 --- a/src/ServiceControl.AcceptanceTests/Recoverability/ExternalIntegration/When_encountered_an_error.cs +++ b/src/ServiceControl.AcceptanceTests/Recoverability/ExternalIntegration/When_encountered_an_error.cs @@ -12,7 +12,6 @@ namespace ServiceControl.AcceptanceTests.Recoverability.ExternalIntegration using NServiceBus; using NServiceBus.AcceptanceTesting; using NUnit.Framework; - using Raven.Client; using ServiceBus.Management.Infrastructure.Settings; using ServiceControl.Operations; using TestSupport.EndpointTemplates; @@ -89,7 +88,7 @@ public object CreateDispatchContext(IDomainEvent @event) return null; } - public Task> PublishEventsForOwnContexts(IEnumerable allContexts, IAsyncDocumentSession session) + public Task> PublishEventsForOwnContexts(IEnumerable allContexts) { if (!failed) { diff --git a/src/ServiceControl.AcceptanceTests/Recoverability/Groups/When_a_group_is_archived.cs b/src/ServiceControl.AcceptanceTests/Recoverability/Groups/When_a_group_is_archived.cs index 99511e7924..11698f24c5 100644 --- a/src/ServiceControl.AcceptanceTests/Recoverability/Groups/When_a_group_is_archived.cs +++ b/src/ServiceControl.AcceptanceTests/Recoverability/Groups/When_a_group_is_archived.cs @@ -20,10 +20,8 @@ public async Task All_messages_in_group_should_get_archived() await Define() .WithEndpoint(b => b.When(async bus => { - await bus.SendLocal(m => m.MessageNumber = 1) - .ConfigureAwait(false); - await bus.SendLocal(m => m.MessageNumber = 2) - .ConfigureAwait(false); + await bus.SendLocal(m => m.MessageNumber = 1); + await bus.SendLocal(m => m.MessageNumber = 2); }).DoNotFailOnErrorMessages()) .Do("WaitUntilGrouped", async ctx => { @@ -44,7 +42,7 @@ await bus.SendLocal(m => m.MessageNumber = 2) }) .Do("WaitUntilGroupContainsBothMessages", async ctx => { - var failedMessages = await this.TryGetMany($"/api/recoverability/groups/{ctx.GroupId}/errors").ConfigureAwait(false); + var failedMessages = await this.TryGetMany($"/api/recoverability/groups/{ctx.GroupId}/errors"); return failedMessages && failedMessages.Items.Count == 2; }) .Do("Archive", async ctx => { await this.Post($"/api/recoverability/groups/{ctx.GroupId}/errors/archive"); }) @@ -68,10 +66,8 @@ public async Task All_archived_messages_should_be_grouped() await Define() .WithEndpoint(b => b.When(async bus => { - await bus.SendLocal(m => m.MessageNumber = 1) - .ConfigureAwait(false); - await bus.SendLocal(m => m.MessageNumber = 2) - .ConfigureAwait(false); + await bus.SendLocal(m => m.MessageNumber = 1); + await bus.SendLocal(m => m.MessageNumber = 2); }).DoNotFailOnErrorMessages()) .Do("WaitUntilGrouped", async ctx => { @@ -92,7 +88,7 @@ await bus.SendLocal(m => m.MessageNumber = 2) }) .Do("WaitUntilGroupContainsBothMessages", async ctx => { - var failedMessages = await this.TryGetMany($"/api/recoverability/groups/{ctx.GroupId}/errors").ConfigureAwait(false); + var failedMessages = await this.TryGetMany($"/api/recoverability/groups/{ctx.GroupId}/errors"); return failedMessages && failedMessages.Items.Count == 2; }) .Do("Archive", async ctx => { await this.Post($"/api/recoverability/groups/{ctx.GroupId}/errors/archive"); }) @@ -108,7 +104,7 @@ await bus.SendLocal(m => m.MessageNumber = 2) }) .Do("WaitUntilArchiveGroupContainsBothMessages", async ctx => { - var failedMessages = await this.TryGetMany("/api/errors/groups/").ConfigureAwait(false); + var failedMessages = await this.TryGetMany("/api/errors/groups/"); return failedMessages && failedMessages.Items.Count == 1 && failedMessages.Items[0].Count == 2; }) .Done(ctx => true) //Done when sequence is finished @@ -121,10 +117,8 @@ public async Task Archived_messages_group_info_should_be_accessible() await Define() .WithEndpoint(b => b.When(async bus => { - await bus.SendLocal(m => m.MessageNumber = 1) - .ConfigureAwait(false); - await bus.SendLocal(m => m.MessageNumber = 2) - .ConfigureAwait(false); + await bus.SendLocal(m => m.MessageNumber = 1); + await bus.SendLocal(m => m.MessageNumber = 2); }).DoNotFailOnErrorMessages()) .Do("WaitUntilGrouped", async ctx => { @@ -145,7 +139,7 @@ await bus.SendLocal(m => m.MessageNumber = 2) }) .Do("WaitUntilGroupContainsBothMessages", async ctx => { - var failedMessages = await this.TryGetMany($"/api/recoverability/groups/{ctx.GroupId}/errors").ConfigureAwait(false); + var failedMessages = await this.TryGetMany($"/api/recoverability/groups/{ctx.GroupId}/errors"); return failedMessages && failedMessages.Items.Count == 2; }) .Do("Archive", async ctx => { await this.Post($"/api/recoverability/groups/{ctx.GroupId}/errors/archive"); }) @@ -161,7 +155,7 @@ await bus.SendLocal(m => m.MessageNumber = 2) }) .Do("WaitUntilArchiveGroupContainsBothMessages", async ctx => { - var failedMessages = await this.TryGet($"/api/archive/groups/id/{ctx.GroupId}").ConfigureAwait(false); + var failedMessages = await this.TryGet($"/api/archive/groups/id/{ctx.GroupId}"); return failedMessages && failedMessages.Item.Count == 2; }) .Done(ctx => true) //Done when sequence is finished @@ -174,10 +168,8 @@ public async Task Only_unresolved_issues_should_be_archived() await Define() .WithEndpoint(b => b.When(async bus => { - await bus.SendLocal(m => m.MessageNumber = 1) - .ConfigureAwait(false); - await bus.SendLocal(m => m.MessageNumber = 2) - .ConfigureAwait(false); + await bus.SendLocal(m => m.MessageNumber = 1); + await bus.SendLocal(m => m.MessageNumber = 2); }).DoNotFailOnErrorMessages()) .Do("WaitUntilGrouped", async ctx => { diff --git a/src/ServiceControl.AcceptanceTests/Recoverability/Groups/When_a_group_is_retried.cs b/src/ServiceControl.AcceptanceTests/Recoverability/Groups/When_a_group_is_retried.cs index 469a314ef7..ca81226cd4 100644 --- a/src/ServiceControl.AcceptanceTests/Recoverability/Groups/When_a_group_is_retried.cs +++ b/src/ServiceControl.AcceptanceTests/Recoverability/Groups/When_a_group_is_retried.cs @@ -23,10 +23,8 @@ public async Task Only_unresolved_issues_should_be_retried() await Define() .WithEndpoint(b => b.When(async bus => { - await bus.SendLocal(m => m.MessageNumber = 1) - .ConfigureAwait(false); - await bus.SendLocal(m => m.MessageNumber = 2) - .ConfigureAwait(false); + await bus.SendLocal(m => m.MessageNumber = 1); + await bus.SendLocal(m => m.MessageNumber = 2); }).DoNotFailOnErrorMessages()) .Done(async c => { diff --git a/src/ServiceControl.AcceptanceTests/Recoverability/Groups/When_messages_have_failed.cs b/src/ServiceControl.AcceptanceTests/Recoverability/Groups/When_messages_have_failed.cs index 942a6818f1..0e02817e91 100644 --- a/src/ServiceControl.AcceptanceTests/Recoverability/Groups/When_messages_have_failed.cs +++ b/src/ServiceControl.AcceptanceTests/Recoverability/Groups/When_messages_have_failed.cs @@ -30,10 +30,8 @@ public async Task Should_be_grouped() var context = await Define() .WithEndpoint(b => b.When(async bus => { - await bus.SendLocal(new MyMessageA()) - .ConfigureAwait(false); - await bus.SendLocal(new MyMessageB()) - .ConfigureAwait(false); + await bus.SendLocal(new MyMessageA()); + await bus.SendLocal(new MyMessageB()); }).DoNotFailOnErrorMessages()) .Done(async c => { diff --git a/src/ServiceControl.AcceptanceTests/Recoverability/Groups/When_two_similar_messages_have_failed.cs b/src/ServiceControl.AcceptanceTests/Recoverability/Groups/When_two_similar_messages_have_failed.cs index 90b93d0b47..338ef04193 100644 --- a/src/ServiceControl.AcceptanceTests/Recoverability/Groups/When_two_similar_messages_have_failed.cs +++ b/src/ServiceControl.AcceptanceTests/Recoverability/Groups/When_two_similar_messages_have_failed.cs @@ -27,12 +27,9 @@ public async Task They_should_be_grouped_together() await Define() .WithEndpoint(b => b.When(async bus => { - await bus.SendLocal(m => m.IsFirst = true) - .ConfigureAwait(false); - await Task.Delay(TimeSpan.FromSeconds(1)) - .ConfigureAwait(false); - await bus.SendLocal(m => m.IsFirst = false) - .ConfigureAwait(false); + await bus.SendLocal(m => m.IsFirst = true); + await Task.Delay(TimeSpan.FromSeconds(1)); + await bus.SendLocal(m => m.IsFirst = false); }).DoNotFailOnErrorMessages()) .Done(async c => { diff --git a/src/ServiceControl.AcceptanceTests/Recoverability/MessageFailures/When_a_message_has_failed.cs b/src/ServiceControl.AcceptanceTests/Recoverability/MessageFailures/When_a_message_has_failed.cs index 371d1221c0..ab51e13ae2 100644 --- a/src/ServiceControl.AcceptanceTests/Recoverability/MessageFailures/When_a_message_has_failed.cs +++ b/src/ServiceControl.AcceptanceTests/Recoverability/MessageFailures/When_a_message_has_failed.cs @@ -75,12 +75,21 @@ await Define() .WithEndpoint(b => b.When(bus => bus.SendLocal(myMessage)).DoNotFailOnErrorMessages()) .Done(async c => { + if (c.MessageId == null) + { + // Message hasn't failed yet + return false; + } + result = await this.GetRaw("/api/messages/" + c.MessageId + "/body"); return result.IsSuccessStatusCode; }) .Run(); - Assert.AreEqual($"{{\"Content\":\"{myMessage.Content}\"}}", await result.Content.ReadAsStringAsync()); + var stringResult = await result.Content.ReadAsStringAsync(); + var expectedResult = $"{{\"Content\":\"{myMessage.Content}\"}}"; + + Assert.AreEqual(expectedResult, stringResult); } [Test] @@ -97,6 +106,12 @@ await Define() .WithEndpoint(b => b.When(bus => bus.SendLocal(myMessage)).DoNotFailOnErrorMessages()) .Done(async c => { + if (c.MessageId == null) + { + // Message hasn't failed yet + return false; + } + result = await this.GetRaw("/api/messages/" + c.MessageId + "/body"); return result.IsSuccessStatusCode; }) diff --git a/src/ServiceControl.AcceptanceTests/Recoverability/MessageFailures/When_a_retry_fails_to_be_sent.cs b/src/ServiceControl.AcceptanceTests/Recoverability/MessageFailures/When_a_retry_fails_to_be_sent.cs index 768f817bbe..425bce9038 100644 --- a/src/ServiceControl.AcceptanceTests/Recoverability/MessageFailures/When_a_retry_fails_to_be_sent.cs +++ b/src/ServiceControl.AcceptanceTests/Recoverability/MessageFailures/When_a_retry_fails_to_be_sent.cs @@ -12,9 +12,9 @@ using NServiceBus.Transport; using NUnit.Framework; using Operations.BodyStorage; - using Raven.Client; using ServiceControl.MessageFailures; using ServiceControl.MessageFailures.Api; + using ServiceControl.Persistence; using ServiceControl.Recoverability; using TestSupport; using TestSupport.EndpointTemplates; @@ -26,7 +26,7 @@ public async Task SubsequentBatchesShouldBeProcessed() { FailedMessage decomissionedFailure = null, successfullyRetried = null; - CustomConfiguration = config => config.RegisterComponents(components => components.ConfigureComponent(b => new FakeReturnToSender(b.Build(), b.Build(), b.Build()), DependencyLifecycle.SingleInstance)); + CustomConfiguration = config => config.RegisterComponents(components => components.ConfigureComponent(b => new FakeReturnToSender(b.Build(), b.Build(), b.Build()), DependencyLifecycle.SingleInstance)); await Define() .WithEndpoint(b => b.DoNotFailOnErrorMessages() @@ -148,7 +148,7 @@ public class MessageThatWillFail : ICommand public class FakeReturnToSender : ReturnToSender { - public FakeReturnToSender(IBodyStorage bodyStorage, IDocumentStore documentStore, MyContext myContext) : base(bodyStorage, documentStore) + public FakeReturnToSender(IBodyStorage bodyStorage, IErrorMessageDataStore errorMessageStore, MyContext myContext) : base(bodyStorage, errorMessageStore) { this.myContext = myContext; } diff --git a/src/ServiceControl.AcceptanceTests/Recoverability/MessageFailures/When_a_retry_for_a_empty_body_message_is_successful.cs b/src/ServiceControl.AcceptanceTests/Recoverability/MessageFailures/When_a_retry_for_a_empty_body_message_is_successful.cs index eb97ef54d4..ad199364d2 100644 --- a/src/ServiceControl.AcceptanceTests/Recoverability/MessageFailures/When_a_retry_for_a_empty_body_message_is_successful.cs +++ b/src/ServiceControl.AcceptanceTests/Recoverability/MessageFailures/When_a_retry_for_a_empty_body_message_is_successful.cs @@ -61,7 +61,7 @@ async Task IssueRetry(MyContext c, Func retryAction) if (!c.RetryIssued) { c.RetryIssued = true; - await retryAction().ConfigureAwait(false); + await retryAction(); } } diff --git a/src/ServiceControl.AcceptanceTests/Recoverability/MessageFailures/When_a_retry_for_a_failed_message_is_successful.cs b/src/ServiceControl.AcceptanceTests/Recoverability/MessageFailures/When_a_retry_for_a_failed_message_is_successful.cs index 7294623104..4d5f84d0e8 100644 --- a/src/ServiceControl.AcceptanceTests/Recoverability/MessageFailures/When_a_retry_for_a_failed_message_is_successful.cs +++ b/src/ServiceControl.AcceptanceTests/Recoverability/MessageFailures/When_a_retry_for_a_failed_message_is_successful.cs @@ -185,7 +185,7 @@ async Task IssueRetry(MyContext c, Func retryAction) { c.RetryIssued = true; - await retryAction().ConfigureAwait(false); + await retryAction(); } } diff --git a/src/ServiceControl.AcceptanceTests/SagaAudit/When_multiple_messages_are_emitted_by_a_saga.cs b/src/ServiceControl.AcceptanceTests/SagaAudit/When_multiple_messages_are_emitted_by_a_saga.cs index 7f2fb46159..904767f5d7 100644 --- a/src/ServiceControl.AcceptanceTests/SagaAudit/When_multiple_messages_are_emitted_by_a_saga.cs +++ b/src/ServiceControl.AcceptanceTests/SagaAudit/When_multiple_messages_are_emitted_by_a_saga.cs @@ -81,14 +81,10 @@ public MySaga(MyContext testContext) public async Task Handle(MessageInitiatingSaga message, IMessageHandlerContext context) { - await context.Reply(new MessageReplyBySaga()) - .ConfigureAwait(false); - await ReplyToOriginator(context, new MessageReplyToOriginatorBySaga()) - .ConfigureAwait(false); - await context.SendLocal(new MessageSentBySaga()) - .ConfigureAwait(false); - await context.Publish(new MessagePublishedBySaga()) - .ConfigureAwait(false); + await context.Reply(new MessageReplyBySaga()); + await ReplyToOriginator(context, new MessageReplyToOriginatorBySaga()); + await context.SendLocal(new MessageSentBySaga()); + await context.Publish(new MessagePublishedBySaga()); testContext.SagaId = Data.Id; } diff --git a/src/ServiceControl.AcceptanceTests/AcceptanceTest.cs b/src/ServiceControl.AcceptanceTests/TestSupport/AcceptanceTest.cs similarity index 90% rename from src/ServiceControl.AcceptanceTests/AcceptanceTest.cs rename to src/ServiceControl.AcceptanceTests/TestSupport/AcceptanceTest.cs index cb7b9aa301..30d7dfbfbd 100644 --- a/src/ServiceControl.AcceptanceTests/AcceptanceTest.cs +++ b/src/ServiceControl.AcceptanceTests/TestSupport/AcceptanceTest.cs @@ -40,13 +40,13 @@ protected AcceptanceTest() public string Port => serviceControlRunnerBehavior.Port; [OneTimeSetUp] - public void OneTimeSetup() + public static void OneTimeSetup() { Scenario.GetLoggerFactory = ctx => new StaticLoggerFactory(ctx); } [SetUp] - public Task Setup() + public async Task Setup() { SetSettings = _ => { }; CustomConfiguration = _ => { }; @@ -68,23 +68,22 @@ public Task Setup() TransportIntegration = new ConfigureEndpointLearningTransport(); - DataStoreConfiguration = new DataStoreConfiguration - { - DataStoreTypeName = "RavenDB35" - }; + StorageConfiguration = new AcceptanceTestStorageConfiguration(); - serviceControlRunnerBehavior = new ServiceControlComponentBehavior(TransportIntegration, DataStoreConfiguration, s => SetSettings(s), s => CustomConfiguration(s), hb => CustomizeHostBuilder(hb)); + await StorageConfiguration.Configure(); - return Task.CompletedTask; + serviceControlRunnerBehavior = new ServiceControlComponentBehavior(TransportIntegration, StorageConfiguration, s => SetSettings(s), s => CustomConfiguration(s), hb => CustomizeHostBuilder(hb)); } [TearDown] - public void Teardown() + public Task Teardown() { TransportIntegration = null; Trace.Flush(); Trace.Close(); Trace.Listeners.Remove(textWriterTraceListener); + + return StorageConfiguration.Cleanup(); } #pragma warning disable IDE0060 // Remove unused parameter @@ -118,7 +117,7 @@ protected void ExecuteWhen(Func execute, Func action, protected Action SetSettings = _ => { }; protected Action CustomizeHostBuilder = _ => { }; protected ITransportIntegration TransportIntegration; - protected DataStoreConfiguration DataStoreConfiguration; + protected AcceptanceTestStorageConfiguration StorageConfiguration; ServiceControlComponentBehavior serviceControlRunnerBehavior; TextWriterTraceListener textWriterTraceListener; diff --git a/src/ServiceControl.AcceptanceTests/ContextLoggerExtensions.cs b/src/ServiceControl.AcceptanceTests/TestSupport/ContextLoggerExtensions.cs similarity index 100% rename from src/ServiceControl.AcceptanceTests/ContextLoggerExtensions.cs rename to src/ServiceControl.AcceptanceTests/TestSupport/ContextLoggerExtensions.cs diff --git a/src/ServiceControl.AcceptanceTests/TestSupport/ReportSuccessfulRetryToServiceControl.cs b/src/ServiceControl.AcceptanceTests/TestSupport/ReportSuccessfulRetryToServiceControl.cs index 9c3119bb62..2f105fc85b 100644 --- a/src/ServiceControl.AcceptanceTests/TestSupport/ReportSuccessfulRetryToServiceControl.cs +++ b/src/ServiceControl.AcceptanceTests/TestSupport/ReportSuccessfulRetryToServiceControl.cs @@ -11,7 +11,7 @@ class ReportSuccessfulRetryToServiceControl : IBehavior next) { - await next(context).ConfigureAwait(false); + await next(context); if (context.MessageHeaders.TryGetValue("ServiceControl.Retry.UniqueMessageId", out var messageId)) { @@ -21,7 +21,7 @@ public async Task Invoke(IIncomingPhysicalMessageContext context, Func setSettings, Action customConfiguration, Action hostBuilderCustomization) + public ServiceControlComponentBehavior(ITransportIntegration transportToUse, AcceptanceTestStorageConfiguration persistenceToUse, Action setSettings, Action customConfiguration, Action hostBuilderCustomization) { this.customConfiguration = customConfiguration; - this.dataStoreToUse = dataStoreToUse; + this.persistenceToUse = persistenceToUse; this.hostBuilderCustomization = hostBuilderCustomization; this.setSettings = setSettings; transportIntegration = transportToUse; @@ -31,13 +31,13 @@ public ServiceControlComponentBehavior(ITransportIntegration transportToUse, Dat public async Task CreateRunner(RunDescriptor run) { - runner = new ServiceControlComponentRunner(transportIntegration, dataStoreToUse, setSettings, customConfiguration, hostBuilderCustomization); - await runner.Initialize(run).ConfigureAwait(false); + runner = new ServiceControlComponentRunner(transportIntegration, persistenceToUse, setSettings, customConfiguration, hostBuilderCustomization); + await runner.Initialize(run); return runner; } ITransportIntegration transportIntegration; - DataStoreConfiguration dataStoreToUse; + AcceptanceTestStorageConfiguration persistenceToUse; Action setSettings; Action customConfiguration; Action hostBuilderCustomization; diff --git a/src/ServiceControl.AcceptanceTests/TestSupport/ServiceControlComponentRunner.cs b/src/ServiceControl.AcceptanceTests/TestSupport/ServiceControlComponentRunner.cs index 37d3d31940..97795026d1 100644 --- a/src/ServiceControl.AcceptanceTests/TestSupport/ServiceControlComponentRunner.cs +++ b/src/ServiceControl.AcceptanceTests/TestSupport/ServiceControlComponentRunner.cs @@ -1,4 +1,4 @@ -namespace ServiceControl.AcceptanceTests.TestSupport +namespace ServiceControl.AcceptanceTests.TestSupport { using System; using System.Configuration; @@ -21,18 +21,15 @@ namespace ServiceControl.AcceptanceTests.TestSupport using NServiceBus.AcceptanceTesting.Support; using NServiceBus.Configuration.AdvancedExtensibility; using Particular.ServiceControl; - using Recoverability.MessageFailures; using ServiceBus.Management.Infrastructure.OWIN; using ServiceBus.Management.Infrastructure.Settings; - using ServiceControl.AcceptanceTests.Monitoring; - using ServiceControl.AcceptanceTests.Monitoring.InternalCustomChecks; class ServiceControlComponentRunner : ComponentRunner, IAcceptanceTestInfrastructureProvider { - public ServiceControlComponentRunner(ITransportIntegration transportToUse, DataStoreConfiguration dataStoreToUse, Action setSettings, Action customConfiguration, Action hostBuilderCustomization) + public ServiceControlComponentRunner(ITransportIntegration transportToUse, AcceptanceTestStorageConfiguration persistenceToUse, Action setSettings, Action customConfiguration, Action hostBuilderCustomization) { this.transportToUse = transportToUse; - this.dataStoreToUse = dataStoreToUse; + this.persistenceToUse = persistenceToUse; this.customConfiguration = customConfiguration; this.hostBuilderCustomization = hostBuilderCustomization; this.setSettings = setSettings; @@ -69,28 +66,32 @@ static int FindAvailablePort(int startPort) return startPort; } + async Task InitializeServiceControl(ScenarioContext context) { var instancePort = FindAvailablePort(33333); var maintenancePort = FindAvailablePort(instancePort + 1); ConfigurationManager.AppSettings.Set("ServiceControl/TransportType", transportToUse.TypeName); - ConfigurationManager.AppSettings.Set("ServiceControl/SqlStorageConnectionString", dataStoreToUse.ConnectionString); - var settings = new Settings(instanceName) + // TODO: ⬇️ This is required for the PERSISTER implementation to actually retrieve the DBPath settings are persister isn't using this type-safe settings class + var dbPath = Path.Combine(Path.GetTempPath(), Path.GetRandomFileName()); + ConfigurationManager.AppSettings.Set("ServiceControl/DBPath", dbPath); // TODO: Tests should not use static ConfigurationManager.AppSettings + ConfigurationManager.AppSettings.Set("ServiceControl/DatabaseMaintenancePort", "33434"); + ConfigurationManager.AppSettings.Set("ServiceControl/ExposeRavenDB", "False"); + // TODO: ⬆️ + + var settings = new Settings(instanceName, transportToUse.TypeName, persistenceToUse.PersistenceType) { - DataStoreType = (DataStoreType)Enum.Parse(typeof(DataStoreType), dataStoreToUse.DataStoreTypeName), Port = instancePort, DatabaseMaintenancePort = maintenancePort, - DbPath = Path.Combine(Path.GetTempPath(), Path.GetRandomFileName()), + DbPath = dbPath, ForwardErrorMessages = false, - TransportType = transportToUse.TypeName, TransportConnectionString = transportToUse.ConnectionString, ProcessRetryBatchesFrequency = TimeSpan.FromSeconds(2), TimeToRestartErrorIngestionAfterFailure = TimeSpan.FromSeconds(2), MaximumConcurrencyLevel = 2, HttpDefaultConnectionLimit = int.MaxValue, - RunInMemory = true, DisableHealthChecks = true, ExposeApi = true, MessageFilter = messageContext => @@ -128,6 +129,21 @@ async Task InitializeServiceControl(ScenarioContext context) setSettings(settings); Settings = settings; + + var persisterSpecificSettings = await persistenceToUse.CustomizeSettings(); + + foreach (var persisterSpecificSetting in persisterSpecificSettings) + { + ConfigurationManager.AppSettings.Set($"ServiceControl/{persisterSpecificSetting.Key}", persisterSpecificSetting.Value); + } + + using (new DiagnosticTimer($"Creating infrastructure for {instanceName}")) + { + + var setupBootstrapper = new SetupBootstrapper(settings); + await setupBootstrapper.Run(null); + } + var configuration = new EndpointConfiguration(instanceName); configuration.GetSettings().Set("SC.ScenarioContext", context); @@ -169,20 +185,15 @@ async Task InitializeServiceControl(ScenarioContext context) HttpClientFactory = HttpClientFactory }; + // Do not register additional test controllers or hosted services here. Instead, in the test that needs them, use (for example): + // CustomizeHostBuilder = builder => builder.ConfigureServices((hostContext, services) => services.AddHostedService()); bootstrapper.HostBuilder - .ConfigureLogging((c, b) => b.AddScenarioContextLogging()) - .ConfigureServices(serviceCollection => - { - serviceCollection.AddScoped(); - serviceCollection.AddScoped(); - serviceCollection.AddScoped(); - serviceCollection.AddScoped(); - }); + .ConfigureLogging((c, b) => b.AddScenarioContextLogging()); hostBuilderCustomization(bootstrapper.HostBuilder); host = bootstrapper.HostBuilder.Build(); - await host.StartAsync().ConfigureAwait(false); + await host.StartAsync(); DomainEvents = host.Services.GetService(); } @@ -190,7 +201,7 @@ async Task InitializeServiceControl(ScenarioContext context) { var app = new AppBuilder(); var startup = new Startup(host.Services, bootstrapper.ApiAssemblies); - startup.Configuration(app, typeof(FailedErrorsController).Assembly); + startup.Configuration(app, typeof(AcceptanceTest).Assembly); var appFunc = app.Build(); Handler = new OwinHttpMessageHandler(appFunc) @@ -209,7 +220,7 @@ public override async Task Stop() { using (new DiagnosticTimer($"Test TearDown for {instanceName}")) { - await host.StopAsync().ConfigureAwait(false); + await host.StopAsync(); HttpClient.Dispose(); Handler.Dispose(); DirectoryDeleter.Delete(Settings.DbPath); @@ -222,6 +233,10 @@ public override async Task Stop() HttpClient HttpClientFactory() { + if (Handler == null) + { + throw new InvalidOperationException("Handler field not yet initialized"); // TODO: This method is invoked before `Initialize` completes which is strange and should be looked into as that seems like a race condition + } var httpClient = new HttpClient(Handler); httpClient.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json")); return httpClient; @@ -230,7 +245,7 @@ HttpClient HttpClientFactory() IHost host; Bootstrapper bootstrapper; ITransportIntegration transportToUse; - DataStoreConfiguration dataStoreToUse; + AcceptanceTestStorageConfiguration persistenceToUse; Action setSettings; Action customConfiguration; Action hostBuilderCustomization; diff --git a/src/ServiceControl.Audit.AcceptanceTests.RavenDB/.editorconfig b/src/ServiceControl.Audit.AcceptanceTests.RavenDB/.editorconfig new file mode 100644 index 0000000000..5f68a610b3 --- /dev/null +++ b/src/ServiceControl.Audit.AcceptanceTests.RavenDB/.editorconfig @@ -0,0 +1,4 @@ +[*.cs] + +# Justification: Test project +dotnet_diagnostic.CA2007.severity = none diff --git a/src/ServiceControl.Audit.AcceptanceTests.RavenDB/AcceptanceTestStorageConfiguration.cs b/src/ServiceControl.Audit.AcceptanceTests.RavenDB/AcceptanceTestStorageConfiguration.cs index 7c7a6ee94a..0423bbf4ed 100644 --- a/src/ServiceControl.Audit.AcceptanceTests.RavenDB/AcceptanceTestStorageConfiguration.cs +++ b/src/ServiceControl.Audit.AcceptanceTests.RavenDB/AcceptanceTestStorageConfiguration.cs @@ -6,7 +6,7 @@ using System.Threading.Tasks; using ServiceControl.Audit.Persistence.RavenDb; - partial class AcceptanceTestStorageConfiguration + class AcceptanceTestStorageConfiguration { public string PersistenceType { get; protected set; } diff --git a/src/ServiceControl.Audit.AcceptanceTests.RavenDB5/.editorconfig b/src/ServiceControl.Audit.AcceptanceTests.RavenDB5/.editorconfig new file mode 100644 index 0000000000..5f68a610b3 --- /dev/null +++ b/src/ServiceControl.Audit.AcceptanceTests.RavenDB5/.editorconfig @@ -0,0 +1,4 @@ +[*.cs] + +# Justification: Test project +dotnet_diagnostic.CA2007.severity = none diff --git a/src/ServiceControl.Audit.AcceptanceTests.RavenDB5/AcceptanceTestStorageConfiguration.cs b/src/ServiceControl.Audit.AcceptanceTests.RavenDB5/AcceptanceTestStorageConfiguration.cs index 88b1e1982c..d841cc37ee 100644 --- a/src/ServiceControl.Audit.AcceptanceTests.RavenDB5/AcceptanceTestStorageConfiguration.cs +++ b/src/ServiceControl.Audit.AcceptanceTests.RavenDB5/AcceptanceTestStorageConfiguration.cs @@ -6,7 +6,7 @@ using ServiceControl.Audit.Persistence.RavenDb; using ServiceControl.Audit.Persistence.Tests; - partial class AcceptanceTestStorageConfiguration + class AcceptanceTestStorageConfiguration { public string PersistenceType { get; protected set; } diff --git a/src/ServiceControl.Audit.AcceptanceTests/AcceptanceTest.cs b/src/ServiceControl.Audit.AcceptanceTests/AcceptanceTest.cs index 00814d774f..703ad657c7 100644 --- a/src/ServiceControl.Audit.AcceptanceTests/AcceptanceTest.cs +++ b/src/ServiceControl.Audit.AcceptanceTests/AcceptanceTest.cs @@ -36,7 +36,7 @@ protected AcceptanceTest() public string Port => serviceControlRunnerBehavior.Port; [OneTimeSetUp] - public void OneTimeSetup() + public static void OneTimeSetup() { Scenario.GetLoggerFactory = ctx => new StaticLoggerFactory(ctx); } diff --git a/src/ServiceControl.Audit.AcceptanceTests/AcceptanceTestStorageConfiguration.cs b/src/ServiceControl.Audit.AcceptanceTests/AcceptanceTestStorageConfiguration.cs index bee3e6873d..817d9b92e6 100644 --- a/src/ServiceControl.Audit.AcceptanceTests/AcceptanceTestStorageConfiguration.cs +++ b/src/ServiceControl.Audit.AcceptanceTests/AcceptanceTestStorageConfiguration.cs @@ -4,7 +4,7 @@ using System.Threading.Tasks; using ServiceControl.Audit.Persistence.InMemory; - partial class AcceptanceTestStorageConfiguration + class AcceptanceTestStorageConfiguration { public string PersistenceType { get; protected set; } diff --git a/src/ServiceControl.Audit.AcceptanceTests/Auditing/When_messages_are_marked_as_system_messages.cs b/src/ServiceControl.Audit.AcceptanceTests/Auditing/When_messages_are_marked_as_system_messages.cs index 629e3ffcd2..81c621156f 100644 --- a/src/ServiceControl.Audit.AcceptanceTests/Auditing/When_messages_are_marked_as_system_messages.cs +++ b/src/ServiceControl.Audit.AcceptanceTests/Auditing/When_messages_are_marked_as_system_messages.cs @@ -80,6 +80,9 @@ await Define(ctx => return false; } + // Wait for other parts of the system to work + await Task.Delay(250); + var result = await this.TryGet>("/api/messages"); List messages = result; if (!result) diff --git a/src/ServiceControl.Audit.AcceptanceTests/TestSupport/ServiceControlComponentBehavior.cs b/src/ServiceControl.Audit.AcceptanceTests/TestSupport/ServiceControlComponentBehavior.cs index 7705789898..bd7a439d0c 100644 --- a/src/ServiceControl.Audit.AcceptanceTests/TestSupport/ServiceControlComponentBehavior.cs +++ b/src/ServiceControl.Audit.AcceptanceTests/TestSupport/ServiceControlComponentBehavior.cs @@ -29,7 +29,7 @@ public ServiceControlComponentBehavior(ITransportIntegration transportToUse, Acc public async Task CreateRunner(RunDescriptor run) { runner = new ServiceControlComponentRunner(transportIntegration, persistenceToUse, setSettings, customConfiguration, setStorageConfiguration); - await runner.Initialize(run).ConfigureAwait(false); + await runner.Initialize(run); return runner; } diff --git a/src/ServiceControl.Audit.AcceptanceTests/TestSupport/ServiceControlComponentRunner.cs b/src/ServiceControl.Audit.AcceptanceTests/TestSupport/ServiceControlComponentRunner.cs index c85430d39c..46168ad670 100644 --- a/src/ServiceControl.Audit.AcceptanceTests/TestSupport/ServiceControlComponentRunner.cs +++ b/src/ServiceControl.Audit.AcceptanceTests/TestSupport/ServiceControlComponentRunner.cs @@ -188,7 +188,7 @@ async Task InitializeServiceControl(ScenarioContext context) s.AddTransient(); }); - host = await bootstrapper.HostBuilder.StartAsync().ConfigureAwait(false); + host = await bootstrapper.HostBuilder.StartAsync(); ServiceProvider = host.Services; } @@ -215,7 +215,7 @@ public override async Task Stop() { using (new DiagnosticTimer($"Test TearDown for {instanceName}")) { - await host.StopAsync().ConfigureAwait(false); + await host.StopAsync(); HttpClient.Dispose(); handler.Dispose(); } diff --git a/src/ServiceControl.Audit.Persistence.InMemory/.editorconfig b/src/ServiceControl.Audit.Persistence.InMemory/.editorconfig new file mode 100644 index 0000000000..ff993b49bb --- /dev/null +++ b/src/ServiceControl.Audit.Persistence.InMemory/.editorconfig @@ -0,0 +1,4 @@ +[*.cs] + +# Justification: ServiceControl app has no synchronization context +dotnet_diagnostic.CA2007.severity = none diff --git a/src/ServiceControl.Audit.Persistence.InMemory/InMemoryAttachmentsBodyStorage.cs b/src/ServiceControl.Audit.Persistence.InMemory/InMemoryAttachmentsBodyStorage.cs index 90c84fcbcd..90d3a1de55 100644 --- a/src/ServiceControl.Audit.Persistence.InMemory/InMemoryAttachmentsBodyStorage.cs +++ b/src/ServiceControl.Audit.Persistence.InMemory/InMemoryAttachmentsBodyStorage.cs @@ -60,7 +60,7 @@ public async Task TryFetch(string bodyId) ContentType = messageBody.ContentType, BodySize = messageBody.BodySize, Etag = string.Empty - }).ConfigureAwait(false); + }); } class MessageBody diff --git a/src/ServiceControl.Audit.Persistence.InMemory/InMemoryAuditDataStore.cs b/src/ServiceControl.Audit.Persistence.InMemory/InMemoryAuditDataStore.cs index b06b49a634..45464afd61 100644 --- a/src/ServiceControl.Audit.Persistence.InMemory/InMemoryAuditDataStore.cs +++ b/src/ServiceControl.Audit.Persistence.InMemory/InMemoryAuditDataStore.cs @@ -35,10 +35,10 @@ public async Task> QuerySagaHistoryById(Guid input) if (sagaHistory == null) { - return await Task.FromResult(QueryResult.Empty()).ConfigureAwait(false); + return await Task.FromResult(QueryResult.Empty()); } - return await Task.FromResult(new QueryResult(sagaHistory, new QueryStatsInfo(string.Empty, 1))).ConfigureAwait(false); + return await Task.FromResult(new QueryResult(sagaHistory, new QueryStatsInfo(string.Empty, 1))); } public async Task>> GetMessages(bool includeSystemMessages, PagingInfo pagingInfo, SortInfo sortInfo) @@ -47,7 +47,7 @@ public async Task>> GetMessages(bool includeSyst .Where(w => !w.IsSystemMessage || includeSystemMessages) .ToList(); - return await Task.FromResult(new QueryResult>(matched, new QueryStatsInfo(string.Empty, matched.Count))).ConfigureAwait(false); + return await Task.FromResult(new QueryResult>(matched, new QueryStatsInfo(string.Empty, matched.Count))); } public async Task>> QueryMessages(string keyword, PagingInfo pagingInfo, SortInfo sortInfo) @@ -57,7 +57,7 @@ public async Task>> QueryMessages(string keyword var matched = messageViews .Where(w => messages.Contains(w.MessageId)) .ToList(); - return await Task.FromResult(new QueryResult>(matched, new QueryStatsInfo(string.Empty, matched.Count()))).ConfigureAwait(false); + return await Task.FromResult(new QueryResult>(matched, new QueryStatsInfo(string.Empty, matched.Count()))); } public async Task>> QueryMessagesByReceivingEndpointAndKeyword(string endpoint, string keyword, PagingInfo pagingInfo, SortInfo sortInfo) @@ -65,28 +65,28 @@ public async Task>> QueryMessagesByReceivingEndp var messages = GetMessageIdsMatchingQuery(keyword); var matched = messageViews.Where(w => w.ReceivingEndpoint.Name == endpoint && messages.Contains(w.MessageId)).ToList(); - return await Task.FromResult(new QueryResult>(matched, new QueryStatsInfo(string.Empty, matched.Count))).ConfigureAwait(false); + return await Task.FromResult(new QueryResult>(matched, new QueryStatsInfo(string.Empty, matched.Count))); } public async Task>> QueryMessagesByReceivingEndpoint(bool includeSystemMessages, string endpointName, PagingInfo pagingInfo, SortInfo sortInfo) { var matched = messageViews.Where(w => w.ReceivingEndpoint.Name == endpointName).ToList(); - return await Task.FromResult(new QueryResult>(matched, new QueryStatsInfo(string.Empty, matched.Count))).ConfigureAwait(false); + return await Task.FromResult(new QueryResult>(matched, new QueryStatsInfo(string.Empty, matched.Count))); } public async Task>> QueryMessagesByConversationId(string conversationId, PagingInfo pagingInfo, SortInfo sortInfo) { var matched = messageViews.Where(w => w.ConversationId == conversationId).ToList(); - return await Task.FromResult(new QueryResult>(matched, new QueryStatsInfo(string.Empty, matched.Count))).ConfigureAwait(false); + return await Task.FromResult(new QueryResult>(matched, new QueryStatsInfo(string.Empty, matched.Count))); } public async Task GetMessageBody(string messageId) { - var result = await GetMessageBodyFromMetadata(messageId).ConfigureAwait(false); + var result = await GetMessageBodyFromMetadata(messageId); if (!result.Found) { - var fromAttachments = await GetMessageBodyFromAttachments(messageId).ConfigureAwait(false); + var fromAttachments = await GetMessageBodyFromAttachments(messageId); if (fromAttachments.Found) { return fromAttachments; @@ -123,7 +123,7 @@ IList GetMessageIdsMatchingQuery(string keyword) } async Task GetMessageBodyFromAttachments(string messageId) { - var fromBodyStorage = await bodyStorage.TryFetch(messageId).ConfigureAwait(false); + var fromBodyStorage = await bodyStorage.TryFetch(messageId); if (fromBodyStorage.HasResult) { @@ -181,7 +181,7 @@ public async Task>> QueryKnownEndpoints() }) .ToList(); - return await Task.FromResult(new QueryResult>(knownEndpointsView, new QueryStatsInfo(string.Empty, knownEndpointsView.Count))).ConfigureAwait(false); + return await Task.FromResult(new QueryResult>(knownEndpointsView, new QueryStatsInfo(string.Empty, knownEndpointsView.Count))); } public Task>> QueryAuditCounts(string endpointName) diff --git a/src/ServiceControl.Audit.Persistence.InMemory/InMemoryAuditIngestionUnitOfWork.cs b/src/ServiceControl.Audit.Persistence.InMemory/InMemoryAuditIngestionUnitOfWork.cs index e71e8dfa24..0d2f060a76 100644 --- a/src/ServiceControl.Audit.Persistence.InMemory/InMemoryAuditIngestionUnitOfWork.cs +++ b/src/ServiceControl.Audit.Persistence.InMemory/InMemoryAuditIngestionUnitOfWork.cs @@ -31,9 +31,9 @@ public async Task RecordProcessedMessage(ProcessedMessage processedMessage, byte { if (body != null) { - await bodyStorageEnricher.StoreAuditMessageBody(body, processedMessage).ConfigureAwait(false); + await bodyStorageEnricher.StoreAuditMessageBody(body, processedMessage); } - await dataStore.SaveProcessedMessage(processedMessage).ConfigureAwait(false); + await dataStore.SaveProcessedMessage(processedMessage); } public Task RecordSagaSnapshot(SagaSnapshot sagaSnapshot) diff --git a/src/ServiceControl.Audit.Persistence.InMemory/InMemoryFailedAuditStorage.cs b/src/ServiceControl.Audit.Persistence.InMemory/InMemoryFailedAuditStorage.cs index 979f9b855f..3cc60944b5 100644 --- a/src/ServiceControl.Audit.Persistence.InMemory/InMemoryFailedAuditStorage.cs +++ b/src/ServiceControl.Audit.Persistence.InMemory/InMemoryFailedAuditStorage.cs @@ -15,7 +15,7 @@ public async Task ProcessFailedMessages(Func { return Task.CompletedTask; }, cancellationToken).ConfigureAwait(false); + await onMessage(transportMessage, (_) => { return Task.CompletedTask; }, cancellationToken); } dataStore.failedAuditImports.Clear(); diff --git a/src/ServiceControl.Audit.Persistence.RavenDb/.editorconfig b/src/ServiceControl.Audit.Persistence.RavenDb/.editorconfig new file mode 100644 index 0000000000..ff993b49bb --- /dev/null +++ b/src/ServiceControl.Audit.Persistence.RavenDb/.editorconfig @@ -0,0 +1,4 @@ +[*.cs] + +# Justification: ServiceControl app has no synchronization context +dotnet_diagnostic.CA2007.severity = none diff --git a/src/ServiceControl.Audit.Persistence.RavenDb/AsyncTimer.cs b/src/ServiceControl.Audit.Persistence.RavenDb/AsyncTimer.cs index 31a2da6d59..a9723f8cc1 100644 --- a/src/ServiceControl.Audit.Persistence.RavenDb/AsyncTimer.cs +++ b/src/ServiceControl.Audit.Persistence.RavenDb/AsyncTimer.cs @@ -20,20 +20,20 @@ void Start(Func> callback, Time { try { - await Task.Delay(due, token).ConfigureAwait(false); + await Task.Delay(due, token); while (!token.IsCancellationRequested) { try { - var result = await callback(token).ConfigureAwait(false); + var result = await callback(token); if (result == TimerJobExecutionResult.DoNotContinueExecuting) { tokenSource.Cancel(); } else if (result == TimerJobExecutionResult.ScheduleNextExecution) { - await Task.Delay(interval, token).ConfigureAwait(false); + await Task.Delay(interval, token); } //Otherwise execute immediately diff --git a/src/ServiceControl.Audit.Persistence.RavenDb/Migrations/MigrateKnownEndpoints.cs b/src/ServiceControl.Audit.Persistence.RavenDb/Migrations/MigrateKnownEndpoints.cs index 899ff0d8b7..07f2eac765 100644 --- a/src/ServiceControl.Audit.Persistence.RavenDb/Migrations/MigrateKnownEndpoints.cs +++ b/src/ServiceControl.Audit.Persistence.RavenDb/Migrations/MigrateKnownEndpoints.cs @@ -18,7 +18,7 @@ public MigrateKnownEndpoints(IDocumentStore documentStore) } public async Task Migrate(int pageSize = 1024, CancellationToken cancellationToken = default) { - var knownEndpointsIndex = await documentStore.AsyncDatabaseCommands.GetIndexAsync("EndpointsIndex", cancellationToken).ConfigureAwait(false); + var knownEndpointsIndex = await documentStore.AsyncDatabaseCommands.GetIndexAsync("EndpointsIndex", cancellationToken); if (knownEndpointsIndex == null) { Logger.Debug("EndpointsIndex migration already completed."); @@ -26,7 +26,7 @@ public async Task Migrate(int pageSize = 1024, CancellationToken cancellationTok return; } - var dbStatistics = await documentStore.AsyncDatabaseCommands.GetStatisticsAsync(cancellationToken).ConfigureAwait(false); + var dbStatistics = await documentStore.AsyncDatabaseCommands.GetStatisticsAsync(cancellationToken); var indexStats = dbStatistics.Indexes.First(index => index.Name == knownEndpointsIndex.Name); if (indexStats.Priority == IndexingPriority.Disabled) { @@ -34,7 +34,7 @@ public async Task Migrate(int pageSize = 1024, CancellationToken cancellationTok // This should only happen the second time the migration is attempted. // The index is disabled so the data should have been migrated. We can now delete the index. - await documentStore.AsyncDatabaseCommands.DeleteIndexAsync(knownEndpointsIndex.Name, cancellationToken).ConfigureAwait(false); + await documentStore.AsyncDatabaseCommands.DeleteIndexAsync(knownEndpointsIndex.Name, cancellationToken); return; } @@ -46,8 +46,7 @@ public async Task Migrate(int pageSize = 1024, CancellationToken cancellationTok var endpointsFromIndex = await session.Query(knownEndpointsIndex.Name, true) .Skip(previouslyDone) .Take(pageSize) - .ToListAsync(cancellationToken) - .ConfigureAwait(false); + .ToListAsync(cancellationToken); if (endpointsFromIndex.Count == 0) { @@ -77,7 +76,7 @@ public async Task Migrate(int pageSize = 1024, CancellationToken cancellationTok } Logger.Debug($"Migrating {endpointsFromIndex.Count} entries."); - await bulkInsert.DisposeAsync().ConfigureAwait(false); + await bulkInsert.DisposeAsync(); } } } @@ -85,7 +84,7 @@ public async Task Migrate(int pageSize = 1024, CancellationToken cancellationTok Logger.Debug("EndpointsIndex entries migrated. Disabling EndpointsIndex."); // Disable the index so it can be safely deleted in the next migration run - await documentStore.AsyncDatabaseCommands.SetIndexPriorityAsync(knownEndpointsIndex.Name, IndexingPriority.Disabled, cancellationToken).ConfigureAwait(false); + await documentStore.AsyncDatabaseCommands.SetIndexPriorityAsync(knownEndpointsIndex.Name, IndexingPriority.Disabled, cancellationToken); } static readonly ILog Logger = LogManager.GetLogger(typeof(MigrateKnownEndpoints)); diff --git a/src/ServiceControl.Audit.Persistence.RavenDb/RavenAttachmentsBodyStorage.cs b/src/ServiceControl.Audit.Persistence.RavenDb/RavenAttachmentsBodyStorage.cs index 53a26a63db..2ecf0f64dc 100644 --- a/src/ServiceControl.Audit.Persistence.RavenDb/RavenAttachmentsBodyStorage.cs +++ b/src/ServiceControl.Audit.Persistence.RavenDb/RavenAttachmentsBodyStorage.cs @@ -29,7 +29,7 @@ public async Task TryFetch(string bodyId) { //We want to continue using attachments for now #pragma warning disable 618 - var attachment = await documentStore.AsyncDatabaseCommands.GetAttachmentAsync($"messagebodies/{bodyId}").ConfigureAwait(false); + var attachment = await documentStore.AsyncDatabaseCommands.GetAttachmentAsync($"messagebodies/{bodyId}"); #pragma warning restore 618 return attachment == null diff --git a/src/ServiceControl.Audit.Persistence.RavenDb/RavenBootstrapper.cs b/src/ServiceControl.Audit.Persistence.RavenDb/RavenBootstrapper.cs index 32dac738ab..91de906633 100644 --- a/src/ServiceControl.Audit.Persistence.RavenDb/RavenBootstrapper.cs +++ b/src/ServiceControl.Audit.Persistence.RavenDb/RavenBootstrapper.cs @@ -3,12 +3,10 @@ using System; using System.ComponentModel.Composition.Hosting; using System.IO; - using System.Reflection; using System.Runtime.Serialization; using NServiceBus.Logging; using Raven.Client.Embedded; using ServiceControl.Audit.Infrastructure.Migration; - using ServiceControl.SagaAudit; class RavenBootstrapper { @@ -134,10 +132,5 @@ static string ReadAllTextWithoutLocking(string path) static readonly ILog Logger = LogManager.GetLogger(typeof(RavenBootstrapper)); static readonly SerializationBinder MigratedTypeAwareBinder = new MigratedTypeAwareBinder(); - - public static Assembly[] IndexAssemblies = - { - typeof(RavenBootstrapper).Assembly, typeof(SagaDetailsIndex).Assembly - }; } } \ No newline at end of file diff --git a/src/ServiceControl.Audit.Persistence.RavenDb/RavenDbAuditDataStore.cs b/src/ServiceControl.Audit.Persistence.RavenDb/RavenDbAuditDataStore.cs index da0c3e823e..96f596cb8e 100644 --- a/src/ServiceControl.Audit.Persistence.RavenDb/RavenDbAuditDataStore.cs +++ b/src/ServiceControl.Audit.Persistence.RavenDb/RavenDbAuditDataStore.cs @@ -31,8 +31,7 @@ public async Task> QuerySagaHistoryById(Guid input) var sagaHistory = await session.Query() .Statistics(out var stats) - .SingleOrDefaultAsync(x => x.SagaId == input) - .ConfigureAwait(false); + .SingleOrDefaultAsync(x => x.SagaId == input); if (sagaHistory == null) { @@ -53,8 +52,7 @@ public async Task>> GetMessages(bool includeSyst .Sort(sortInfo) .Paging(pagingInfo) .TransformWith() - .ToListAsync() - .ConfigureAwait(false); + .ToListAsync(); return new QueryResult>(results, stats.ToQueryStatsInfo()); } @@ -70,8 +68,7 @@ public async Task>> QueryMessages(string searchP .Sort(sortInfo) .Paging(pagingInfo) .TransformWith() - .ToListAsync() - .ConfigureAwait(false); + .ToListAsync(); return new QueryResult>(results, stats.ToQueryStatsInfo()); } @@ -88,8 +85,7 @@ public async Task>> QueryMessagesByReceivingEndp .Sort(sortInfo) .Paging(pagingInfo) .TransformWith() - .ToListAsync() - .ConfigureAwait(false); + .ToListAsync(); return new QueryResult>(results, stats.ToQueryStatsInfo()); } @@ -106,8 +102,7 @@ public async Task>> QueryMessagesByReceivingEndp .Sort(sortInfo) .Paging(pagingInfo) .TransformWith() - .ToListAsync() - .ConfigureAwait(false); + .ToListAsync(); return new QueryResult>(results, stats.ToQueryStatsInfo()); } @@ -123,8 +118,7 @@ public async Task>> QueryMessagesByConversationI .Sort(sortInfo) .Paging(pagingInfo) .TransformWith() - .ToListAsync() - .ConfigureAwait(false); + .ToListAsync(); return new QueryResult>(results, stats.ToQueryStatsInfo()); } @@ -132,11 +126,11 @@ public async Task>> QueryMessagesByConversationI public async Task GetMessageBody(string messageId) { - var fromIndex = await GetMessageBodyFromIndex(messageId).ConfigureAwait(false); + var fromIndex = await GetMessageBodyFromIndex(messageId); if (!fromIndex.Found) { - var fromAttachments = await GetMessageBodyFromAttachments(messageId).ConfigureAwait(false); + var fromAttachments = await GetMessageBodyFromAttachments(messageId); if (fromAttachments.Found) { return fromAttachments; @@ -153,8 +147,7 @@ async Task GetMessageBodyFromIndex(string messageId) var message = await session.Query() .Statistics(out var stats) .TransformWith() - .FirstOrDefaultAsync(f => f.MessageId == messageId) - .ConfigureAwait(false); + .FirstOrDefaultAsync(f => f.MessageId == messageId); if (message == null) { @@ -179,7 +172,7 @@ async Task GetMessageBodyFromAttachments(string messageId) { //We want to continue using attachments for now #pragma warning disable 618 - var attachment = await documentStore.AsyncDatabaseCommands.GetAttachmentAsync($"messagebodies/{messageId}").ConfigureAwait(false); + var attachment = await documentStore.AsyncDatabaseCommands.GetAttachmentAsync($"messagebodies/{messageId}"); #pragma warning restore 618 if (attachment == null) @@ -199,8 +192,7 @@ public async Task>> QueryKnownEndpoints() { using (var session = documentStore.OpenAsyncSession()) { - var endpoints = await session.Advanced.LoadStartingWithAsync(KnownEndpoint.CollectionName, pageSize: 1024) - .ConfigureAwait(false); + var endpoints = await session.Advanced.LoadStartingWithAsync(KnownEndpoint.CollectionName, pageSize: 1024); var knownEndpoints = endpoints .Select(x => new KnownEndpointsView @@ -233,8 +225,7 @@ public async Task>> QueryAuditCounts(string endpoi .Where(m => m.ReceivingEndpointName == endpointName) .OrderBy(m => m.ProcessedAt) .TransformWith() - .FirstOrDefaultAsync() - .ConfigureAwait(false); + .FirstOrDefaultAsync(); if (oldestMsg != null) { @@ -252,8 +243,7 @@ public async Task>> QueryAuditCounts(string endpoi .Statistics(out var stats) .Where(m => m.ReceivingEndpointName == endpointName && !m.IsSystemMessage && m.ProcessedAt >= date && m.ProcessedAt < nextDate) .Take(0) - .ToListAsync() - .ConfigureAwait(false); + .ToListAsync(); if (stats.TotalResults > 0) { diff --git a/src/ServiceControl.Audit.Persistence.RavenDb/RavenDbFailedAuditStorage.cs b/src/ServiceControl.Audit.Persistence.RavenDb/RavenDbFailedAuditStorage.cs index bcbab90882..ba015ac6c7 100644 --- a/src/ServiceControl.Audit.Persistence.RavenDb/RavenDbFailedAuditStorage.cs +++ b/src/ServiceControl.Audit.Persistence.RavenDb/RavenDbFailedAuditStorage.cs @@ -17,11 +17,9 @@ public async Task Store(dynamic failure) { using (var session = store.OpenAsyncSession()) { - await session.StoreAsync(failure) - .ConfigureAwait(false); + await session.StoreAsync(failure); - await session.SaveChangesAsync() - .ConfigureAwait(false); + await session.SaveChangesAsync(); } } @@ -29,8 +27,8 @@ public async Task SaveFailedAuditImport(FailedAuditImport message) { using (var session = store.OpenAsyncSession()) { - await session.StoreAsync(message).ConfigureAwait(false); - await session.SaveChangesAsync().ConfigureAwait(false); + await session.StoreAsync(message); + await session.SaveChangesAsync(); } } @@ -42,14 +40,14 @@ public async Task ProcessFailedMessages( { var query = session.Query(); using (var stream = await session.Advanced.StreamAsync(query, cancellationToken) - .ConfigureAwait(false)) + ) { while (!cancellationToken.IsCancellationRequested && - await stream.MoveNextAsync().ConfigureAwait(false)) + await stream.MoveNextAsync()) { FailedTransportMessage transportMessage = stream.Current.Document.Message; - await onMessage(transportMessage, (token) => store.AsyncDatabaseCommands.DeleteAsync(stream.Current.Key, null, token), cancellationToken).ConfigureAwait(false); + await onMessage(transportMessage, (token) => store.AsyncDatabaseCommands.DeleteAsync(stream.Current.Key, null, token), cancellationToken); } } } diff --git a/src/ServiceControl.Audit.Persistence.RavenDb/RavenDbInstaller.cs b/src/ServiceControl.Audit.Persistence.RavenDb/RavenDbInstaller.cs index a6f05ac15c..919f98cfb0 100644 --- a/src/ServiceControl.Audit.Persistence.RavenDb/RavenDbInstaller.cs +++ b/src/ServiceControl.Audit.Persistence.RavenDb/RavenDbInstaller.cs @@ -24,15 +24,13 @@ public async Task Install(CancellationToken cancellationToken) Logger.Info("Index creation started"); var indexProvider = ravenStartup.CreateIndexProvider(); - await IndexCreation.CreateIndexesAsync(indexProvider, documentStore) - .ConfigureAwait(false); + await IndexCreation.CreateIndexesAsync(indexProvider, documentStore); Logger.Info("Index creation complete"); Logger.Info("Data migrations starting"); var endpointMigrations = new MigrateKnownEndpoints(documentStore); - await endpointMigrations.Migrate(cancellationToken: cancellationToken) - .ConfigureAwait(false); + await endpointMigrations.Migrate(cancellationToken: cancellationToken); Logger.Info("Data migrations complete"); } diff --git a/src/ServiceControl.Audit.Persistence.RavenDb/RavenDbPersistenceConfiguration.cs b/src/ServiceControl.Audit.Persistence.RavenDb/RavenDbPersistenceConfiguration.cs index b830d7d847..432306a3fc 100644 --- a/src/ServiceControl.Audit.Persistence.RavenDb/RavenDbPersistenceConfiguration.cs +++ b/src/ServiceControl.Audit.Persistence.RavenDb/RavenDbPersistenceConfiguration.cs @@ -26,11 +26,7 @@ public IPersistence Create(PersistenceSettings settings) RavenBootstrapper.Configure(documentStore, settings); var ravenStartup = new RavenStartup(); - - foreach (var indexAssembly in RavenBootstrapper.IndexAssemblies) - { - ravenStartup.AddIndexAssembly(indexAssembly); - } + ravenStartup.AddIndexAssembly(typeof(RavenBootstrapper).Assembly); return new RavenDbPersistence(settings, documentStore, ravenStartup); } diff --git a/src/ServiceControl.Audit.Persistence.RavenDb/RavenDbPersistenceLifecycle.cs b/src/ServiceControl.Audit.Persistence.RavenDb/RavenDbPersistenceLifecycle.cs index cb09e8680f..9ad02fb1a2 100644 --- a/src/ServiceControl.Audit.Persistence.RavenDb/RavenDbPersistenceLifecycle.cs +++ b/src/ServiceControl.Audit.Persistence.RavenDb/RavenDbPersistenceLifecycle.cs @@ -24,8 +24,7 @@ public async Task Start(CancellationToken cancellationToken) Logger.Info("Index creation started"); var indexProvider = ravenStartup.CreateIndexProvider(); - await IndexCreation.CreateIndexesAsync(indexProvider, documentStore) - .ConfigureAwait(false); + await IndexCreation.CreateIndexesAsync(indexProvider, documentStore); Logger.Info("Index creation complete"); } diff --git a/src/ServiceControl.Audit.Persistence.RavenDb/UnitOfWork/RavenDbAuditIngestionUnitOfWork.cs b/src/ServiceControl.Audit.Persistence.RavenDb/UnitOfWork/RavenDbAuditIngestionUnitOfWork.cs index 4289c1522d..fdd7c88376 100644 --- a/src/ServiceControl.Audit.Persistence.RavenDb/UnitOfWork/RavenDbAuditIngestionUnitOfWork.cs +++ b/src/ServiceControl.Audit.Persistence.RavenDb/UnitOfWork/RavenDbAuditIngestionUnitOfWork.cs @@ -23,12 +23,10 @@ public async Task RecordProcessedMessage(ProcessedMessage processedMessage, byte { if (body != null) { - await bodyStorageEnricher.StoreAuditMessageBody(body, processedMessage) - .ConfigureAwait(false); + await bodyStorageEnricher.StoreAuditMessageBody(body, processedMessage); } - await bulkInsert.StoreAsync(processedMessage) - .ConfigureAwait(false); + await bulkInsert.StoreAsync(processedMessage); } public Task RecordSagaSnapshot(SagaSnapshot sagaSnapshot) @@ -38,6 +36,6 @@ public Task RecordKnownEndpoint(KnownEndpoint knownEndpoint) => bulkInsert.StoreAsync(knownEndpoint); public async ValueTask DisposeAsync() - => await bulkInsert.DisposeAsync().ConfigureAwait(false); + => await bulkInsert.DisposeAsync(); } } \ No newline at end of file diff --git a/src/ServiceControl.Audit.Persistence.RavenDb5/.editorconfig b/src/ServiceControl.Audit.Persistence.RavenDb5/.editorconfig new file mode 100644 index 0000000000..ff993b49bb --- /dev/null +++ b/src/ServiceControl.Audit.Persistence.RavenDb5/.editorconfig @@ -0,0 +1,4 @@ +[*.cs] + +# Justification: ServiceControl app has no synchronization context +dotnet_diagnostic.CA2007.severity = none diff --git a/src/ServiceControl.Audit.Persistence.RavenDb5/CustomChecks/CheckRavenDBIndexLag.cs b/src/ServiceControl.Audit.Persistence.RavenDb5/CustomChecks/CheckRavenDBIndexLag.cs index a62a5472ef..ebe810fa74 100644 --- a/src/ServiceControl.Audit.Persistence.RavenDb5/CustomChecks/CheckRavenDBIndexLag.cs +++ b/src/ServiceControl.Audit.Persistence.RavenDb5/CustomChecks/CheckRavenDBIndexLag.cs @@ -22,8 +22,7 @@ public CheckRavenDBIndexLag(IRavenDbDocumentStoreProvider documentStoreProvider) public override async Task PerformCheck() { var store = documentStoreProvider.GetDocumentStore(); - var statistics = await store.Maintenance.SendAsync(new GetIndexesStatisticsOperation()) - .ConfigureAwait(false); + var statistics = await store.Maintenance.SendAsync(new GetIndexesStatisticsOperation()); var indexes = statistics.OrderBy(x => x.Name).ToArray(); CreateDiagnosticsLogEntry(indexes); diff --git a/src/ServiceControl.Audit.Persistence.RavenDb5/DatabaseSetup.cs b/src/ServiceControl.Audit.Persistence.RavenDb5/DatabaseSetup.cs index c7e7e3270b..214db78f2d 100644 --- a/src/ServiceControl.Audit.Persistence.RavenDb5/DatabaseSetup.cs +++ b/src/ServiceControl.Audit.Persistence.RavenDb5/DatabaseSetup.cs @@ -26,16 +26,14 @@ public async Task Execute(IDocumentStore documentStore, CancellationToken cancel { try { - await documentStore.Maintenance.ForDatabase(configuration.Name).SendAsync(new GetStatisticsOperation(), cancellationToken) - .ConfigureAwait(false); + await documentStore.Maintenance.ForDatabase(configuration.Name).SendAsync(new GetStatisticsOperation(), cancellationToken); } catch (DatabaseDoesNotExistException) { try { await documentStore.Maintenance.Server - .SendAsync(new CreateDatabaseOperation(new DatabaseRecord(configuration.Name)), cancellationToken) - .ConfigureAwait(false); + .SendAsync(new CreateDatabaseOperation(new DatabaseRecord(configuration.Name)), cancellationToken); } catch (ConcurrencyException) { @@ -51,18 +49,16 @@ await documentStore.Maintenance.Server if (configuration.EnableFullTextSearch) { indexList.Add(new MessagesViewIndexWithFullTextSearch()); - await documentStore.Maintenance.SendAsync(new DeleteIndexOperation("MessagesViewIndex"), cancellationToken) - .ConfigureAwait(false); + await documentStore.Maintenance.SendAsync(new DeleteIndexOperation("MessagesViewIndex"), cancellationToken); } else { indexList.Add(new MessagesViewIndex()); await documentStore.Maintenance - .SendAsync(new DeleteIndexOperation("MessagesViewIndexWithFullTextSearch"), cancellationToken) - .ConfigureAwait(false); + .SendAsync(new DeleteIndexOperation("MessagesViewIndexWithFullTextSearch"), cancellationToken); } - await IndexCreation.CreateIndexesAsync(indexList, documentStore, null, null, cancellationToken).ConfigureAwait(false); + await IndexCreation.CreateIndexesAsync(indexList, documentStore, null, null, cancellationToken); var expirationConfig = new ExpirationConfiguration { @@ -70,8 +66,7 @@ await documentStore.Maintenance DeleteFrequencyInSec = configuration.ExpirationProcessTimerInSeconds }; - await documentStore.Maintenance.SendAsync(new ConfigureExpirationOperation(expirationConfig), cancellationToken) - .ConfigureAwait(false); + await documentStore.Maintenance.SendAsync(new ConfigureExpirationOperation(expirationConfig), cancellationToken); } readonly DatabaseConfiguration configuration; diff --git a/src/ServiceControl.Audit.Persistence.RavenDb5/EmbeddedDatabase.cs b/src/ServiceControl.Audit.Persistence.RavenDb5/EmbeddedDatabase.cs index badd8723c7..a98d0dfadd 100644 --- a/src/ServiceControl.Audit.Persistence.RavenDb5/EmbeddedDatabase.cs +++ b/src/ServiceControl.Audit.Persistence.RavenDb5/EmbeddedDatabase.cs @@ -100,13 +100,13 @@ void Start(ServerOptions serverOptions) { try { - await Task.Delay(delayBetweenRestarts, shutdownTokenSource.Token).ConfigureAwait(false); + await Task.Delay(delayBetweenRestarts, shutdownTokenSource.Token); if (restartRequired) { logger.Info("Restarting RavenDB server process"); - await EmbeddedServer.Instance.RestartServerAsync().ConfigureAwait(false); + await EmbeddedServer.Instance.RestartServerAsync(); restartRequired = false; logger.Info("RavenDB server process restarted successfully."); @@ -139,10 +139,10 @@ public async Task Connect(CancellationToken cancellationToken) dbOptions.Conventions.FindClrType += configuration.FindClrType; } - var store = await EmbeddedServer.Instance.GetDocumentStoreAsync(dbOptions, cancellationToken).ConfigureAwait(false); + var store = await EmbeddedServer.Instance.GetDocumentStoreAsync(dbOptions, cancellationToken); var databaseSetup = new DatabaseSetup(configuration); - await databaseSetup.Execute(store, cancellationToken).ConfigureAwait(false); + await databaseSetup.Execute(store, cancellationToken); return store; } diff --git a/src/ServiceControl.Audit.Persistence.RavenDb5/RavenAttachmentsBodyStorage.cs b/src/ServiceControl.Audit.Persistence.RavenDb5/RavenAttachmentsBodyStorage.cs index 1743071d29..c5b98e63cb 100644 --- a/src/ServiceControl.Audit.Persistence.RavenDb5/RavenAttachmentsBodyStorage.cs +++ b/src/ServiceControl.Audit.Persistence.RavenDb5/RavenAttachmentsBodyStorage.cs @@ -29,8 +29,7 @@ public async Task TryFetch(string bodyId) { using (var session = sessionProvider.OpenSession()) { - var result = await session.Advanced.Attachments.GetAsync($"message/{bodyId}", "body") - .ConfigureAwait(false); + var result = await session.Advanced.Attachments.GetAsync($"message/{bodyId}", "body"); if (result == null) { diff --git a/src/ServiceControl.Audit.Persistence.RavenDb5/RavenDb5Installer.cs b/src/ServiceControl.Audit.Persistence.RavenDb5/RavenDb5Installer.cs index 0ff0d58596..2b34bc6c4e 100644 --- a/src/ServiceControl.Audit.Persistence.RavenDb5/RavenDb5Installer.cs +++ b/src/ServiceControl.Audit.Persistence.RavenDb5/RavenDb5Installer.cs @@ -12,8 +12,8 @@ public RavenDb5Installer(IRavenDbPersistenceLifecycle lifecycle) public async Task Install(CancellationToken cancellationToken) { - await lifecycle.Start(cancellationToken).ConfigureAwait(false); - await lifecycle.Stop(cancellationToken).ConfigureAwait(false); + await lifecycle.Start(cancellationToken); + await lifecycle.Stop(cancellationToken); } readonly IRavenDbPersistenceLifecycle lifecycle; diff --git a/src/ServiceControl.Audit.Persistence.RavenDb5/RavenDbAuditDataStore.cs b/src/ServiceControl.Audit.Persistence.RavenDb5/RavenDbAuditDataStore.cs index 3ba57f5446..2a873002d6 100644 --- a/src/ServiceControl.Audit.Persistence.RavenDb5/RavenDbAuditDataStore.cs +++ b/src/ServiceControl.Audit.Persistence.RavenDb5/RavenDbAuditDataStore.cs @@ -31,8 +31,7 @@ public async Task> QuerySagaHistoryById(Guid input) var sagaHistory = await session.Query() .Statistics(out var stats) - .SingleOrDefaultAsync(x => x.SagaId == input) - .ConfigureAwait(false); + .SingleOrDefaultAsync(x => x.SagaId == input); if (sagaHistory == null) { @@ -53,8 +52,7 @@ public async Task>> GetMessages(bool includeSyst .Sort(sortInfo) .Paging(pagingInfo) .ToMessagesView() - .ToListAsync() - .ConfigureAwait(false); + .ToListAsync(); return new QueryResult>(results, stats.ToQueryStatsInfo()); } @@ -70,8 +68,7 @@ public async Task>> QueryMessages(string searchP .Sort(sortInfo) .Paging(pagingInfo) .ToMessagesView() - .ToListAsync() - .ConfigureAwait(false); + .ToListAsync(); return new QueryResult>(results, stats.ToQueryStatsInfo()); } @@ -88,8 +85,7 @@ public async Task>> QueryMessagesByReceivingEndp .Sort(sortInfo) .Paging(pagingInfo) .ToMessagesView() - .ToListAsync() - .ConfigureAwait(false); + .ToListAsync(); return new QueryResult>(results, stats.ToQueryStatsInfo()); } @@ -106,8 +102,7 @@ public async Task>> QueryMessagesByReceivingEndp .Sort(sortInfo) .Paging(pagingInfo) .ToMessagesView() - .ToListAsync() - .ConfigureAwait(false); + .ToListAsync(); return new QueryResult>(results, stats.ToQueryStatsInfo()); } @@ -123,8 +118,7 @@ public async Task>> QueryMessagesByConversationI .Sort(sortInfo) .Paging(pagingInfo) .ToMessagesView() - .ToListAsync() - .ConfigureAwait(false); + .ToListAsync(); return new QueryResult>(results, stats.ToQueryStatsInfo()); } @@ -134,7 +128,7 @@ public async Task GetMessageBody(string messageId) { using (var session = sessionProvider.OpenSession()) { - var result = await session.Advanced.Attachments.GetAsync(messageId, "body").ConfigureAwait(false); + var result = await session.Advanced.Attachments.GetAsync(messageId, "body"); if (result == null) { @@ -154,8 +148,7 @@ public async Task>> QueryKnownEndpoints() { using (var session = sessionProvider.OpenSession()) { - var endpoints = await session.Advanced.LoadStartingWithAsync(KnownEndpoint.CollectionName, pageSize: 1024) - .ConfigureAwait(false); + var endpoints = await session.Advanced.LoadStartingWithAsync(KnownEndpoint.CollectionName, pageSize: 1024); var knownEndpoints = endpoints .Select(x => new KnownEndpointsView @@ -189,8 +182,7 @@ public async Task>> QueryAuditCounts(string endpoi var oldestMsg = await session.Query(indexName) .Where(m => m.ReceivingEndpointName == endpointName) .OrderBy(m => m.ProcessedAt) - .FirstOrDefaultAsync() - .ConfigureAwait(false); + .FirstOrDefaultAsync(); if (oldestMsg != null) { @@ -208,8 +200,7 @@ public async Task>> QueryAuditCounts(string endpoi .Statistics(out var stats) .Where(m => m.ReceivingEndpointName == endpointName && !m.IsSystemMessage && m.ProcessedAt >= date && m.ProcessedAt < nextDate) .Take(0) - .ToArrayAsync() - .ConfigureAwait(false); + .ToArrayAsync(); if (stats.LongTotalResults > 0) { diff --git a/src/ServiceControl.Audit.Persistence.RavenDb5/RavenDbEmbeddedPersistenceLifecycle.cs b/src/ServiceControl.Audit.Persistence.RavenDb5/RavenDbEmbeddedPersistenceLifecycle.cs index 1682a2d043..fdb4e74f9c 100644 --- a/src/ServiceControl.Audit.Persistence.RavenDb5/RavenDbEmbeddedPersistenceLifecycle.cs +++ b/src/ServiceControl.Audit.Persistence.RavenDb5/RavenDbEmbeddedPersistenceLifecycle.cs @@ -26,7 +26,7 @@ public async Task Start(CancellationToken cancellationToken) { database = EmbeddedDatabase.Start(databaseConfiguration); - documentStore = await database.Connect(cancellationToken).ConfigureAwait(false); + documentStore = await database.Connect(cancellationToken); } public Task Stop(CancellationToken cancellationToken) diff --git a/src/ServiceControl.Audit.Persistence.RavenDb5/RavenDbFailedAuditStorage.cs b/src/ServiceControl.Audit.Persistence.RavenDb5/RavenDbFailedAuditStorage.cs index 025e304b50..02065af000 100644 --- a/src/ServiceControl.Audit.Persistence.RavenDb5/RavenDbFailedAuditStorage.cs +++ b/src/ServiceControl.Audit.Persistence.RavenDb5/RavenDbFailedAuditStorage.cs @@ -21,11 +21,9 @@ public async Task Store(dynamic failure) { using (var session = sessionProvider.OpenSession()) { - await session.StoreAsync(failure) - .ConfigureAwait(false); + await session.StoreAsync(failure); - await session.SaveChangesAsync() - .ConfigureAwait(false); + await session.SaveChangesAsync(); } } @@ -33,8 +31,8 @@ public async Task SaveFailedAuditImport(FailedAuditImport message) { using (var session = sessionProvider.OpenSession()) { - await session.StoreAsync(message).ConfigureAwait(false); - await session.SaveChangesAsync().ConfigureAwait(false); + await session.StoreAsync(message); + await session.SaveChangesAsync(); } } @@ -49,10 +47,9 @@ public async Task ProcessFailedMessages( IAsyncEnumerator> stream = default; try { - stream = await session.Advanced.StreamAsync(query, cancellationToken) - .ConfigureAwait(false); + stream = await session.Advanced.StreamAsync(query, cancellationToken); while (!cancellationToken.IsCancellationRequested && - await stream.MoveNextAsync().ConfigureAwait(false)) + await stream.MoveNextAsync()) { FailedTransportMessage transportMessage = stream.Current.Document.Message; var localSession = session; @@ -62,16 +59,16 @@ await onMessage(transportMessage, (token) => localSession.Advanced.Defer( new DeleteCommandData(stream.Current.Id, stream.Current.ChangeVector)); return Task.CompletedTask; - }, cancellationToken).ConfigureAwait(false); + }, cancellationToken); } - await session.SaveChangesAsync(cancellationToken).ConfigureAwait(false); + await session.SaveChangesAsync(cancellationToken); } finally { if (stream != null) { - await stream.DisposeAsync().ConfigureAwait(false); + await stream.DisposeAsync(); } } } @@ -82,8 +79,7 @@ public async Task GetFailedAuditsCount() using (var session = sessionProvider.OpenSession()) { return await session.Query() - .CountAsync() - .ConfigureAwait(false); + .CountAsync(); } } diff --git a/src/ServiceControl.Audit.Persistence.RavenDb5/UnitOfWork/RavenDbAuditIngestionUnitOfWork.cs b/src/ServiceControl.Audit.Persistence.RavenDb5/UnitOfWork/RavenDbAuditIngestionUnitOfWork.cs index 39f7382a01..7e2c625224 100644 --- a/src/ServiceControl.Audit.Persistence.RavenDb5/UnitOfWork/RavenDbAuditIngestionUnitOfWork.cs +++ b/src/ServiceControl.Audit.Persistence.RavenDb5/UnitOfWork/RavenDbAuditIngestionUnitOfWork.cs @@ -39,7 +39,7 @@ public async Task RecordProcessedMessage(ProcessedMessage processedMessage, byte processedMessage.MessageMetadata["ContentLength"] = 0; } - await bulkInsert.StoreAsync(processedMessage, GetExpirationMetadata()).ConfigureAwait(false); + await bulkInsert.StoreAsync(processedMessage, GetExpirationMetadata()); if (body != null) { @@ -50,7 +50,7 @@ public async Task RecordProcessedMessage(ProcessedMessage processedMessage, byte contentType = "text/xml"; } - await bodyStorage.Store(processedMessage.Id, contentType, body.Length, stream).ConfigureAwait(false); + await bodyStorage.Store(processedMessage.Id, contentType, body.Length, stream); } } } @@ -70,6 +70,6 @@ public Task RecordKnownEndpoint(KnownEndpoint knownEndpoint) => bulkInsert.StoreAsync(knownEndpoint, GetExpirationMetadata()); public async ValueTask DisposeAsync() - => await bulkInsert.DisposeAsync().ConfigureAwait(false); + => await bulkInsert.DisposeAsync(); } } \ No newline at end of file diff --git a/src/ServiceControl.Audit.Persistence.Tests.RavenDB/.editorconfig b/src/ServiceControl.Audit.Persistence.Tests.RavenDB/.editorconfig new file mode 100644 index 0000000000..5f68a610b3 --- /dev/null +++ b/src/ServiceControl.Audit.Persistence.Tests.RavenDB/.editorconfig @@ -0,0 +1,4 @@ +[*.cs] + +# Justification: Test project +dotnet_diagnostic.CA2007.severity = none diff --git a/src/ServiceControl.Audit.Persistence.Tests.RavenDB/EndpointMigrationTests.cs b/src/ServiceControl.Audit.Persistence.Tests.RavenDB/EndpointMigrationTests.cs index c8208917eb..d7ff87509f 100644 --- a/src/ServiceControl.Audit.Persistence.Tests.RavenDB/EndpointMigrationTests.cs +++ b/src/ServiceControl.Audit.Persistence.Tests.RavenDB/EndpointMigrationTests.cs @@ -33,18 +33,18 @@ await session.StoreAsync(new ProcessedMessage { "SendingEndpoint", new EndpointDetails { Host = "SendHost", HostId = sendHostId, Name = "SendHostName" } }, { "ReceivingEndpoint", new EndpointDetails { Host = "ReceivingHost", HostId = receiverHostId, Name = "ReceivingHostName" } }, } - }).ConfigureAwait(false); + }); - await session.SaveChangesAsync().ConfigureAwait(false); + await session.SaveChangesAsync(); } configuration.DocumentStore.WaitForIndexing(); var migrator = new MigrateKnownEndpoints(configuration.DocumentStore); - await migrator.Migrate().ConfigureAwait(false); + await migrator.Migrate(); - var dbStatistics = await configuration.DocumentStore.AsyncDatabaseCommands.GetStatisticsAsync().ConfigureAwait(false); + var dbStatistics = await configuration.DocumentStore.AsyncDatabaseCommands.GetStatisticsAsync(); var indexStats = dbStatistics.Indexes.First(index => index.Name == "EndpointsIndex"); Assert.AreEqual(IndexingPriority.Disabled, indexStats.Priority); } @@ -69,19 +69,19 @@ await session.StoreAsync(new ProcessedMessage { "SendingEndpoint", new EndpointDetails { Host = "SendHost", HostId = sendHostId, Name = "SendHostName" } }, { "ReceivingEndpoint", new EndpointDetails { Host = "ReceivingHost", HostId = receiverHostId, Name = "ReceivingHostName" } }, } - }).ConfigureAwait(false); + }); - await session.SaveChangesAsync().ConfigureAwait(false); + await session.SaveChangesAsync(); } configuration.DocumentStore.WaitForIndexing(); var migrator = new MigrateKnownEndpoints(configuration.DocumentStore); - await migrator.Migrate().ConfigureAwait(false); - await migrator.Migrate().ConfigureAwait(false); + await migrator.Migrate(); + await migrator.Migrate(); - var knownEndpointsIndex = await configuration.DocumentStore.AsyncDatabaseCommands.GetIndexAsync("EndpointsIndex").ConfigureAwait(false); + var knownEndpointsIndex = await configuration.DocumentStore.AsyncDatabaseCommands.GetIndexAsync("EndpointsIndex"); Assert.IsNull(knownEndpointsIndex); } @@ -105,21 +105,21 @@ await session.StoreAsync(new ProcessedMessage { "SendingEndpoint", new EndpointDetails { Host = "SendHost", HostId = sendHostId, Name = "SendHostName" } }, { "ReceivingEndpoint", new EndpointDetails { Host = "ReceivingHost", HostId = receiverHostId, Name = "ReceivingHostName" } }, } - }).ConfigureAwait(false); + }); - await session.SaveChangesAsync().ConfigureAwait(false); + await session.SaveChangesAsync(); } configuration.DocumentStore.WaitForIndexing(); var migrator = new MigrateKnownEndpoints(configuration.DocumentStore); - await migrator.Migrate().ConfigureAwait(false); + await migrator.Migrate(); using (var session = configuration.DocumentStore.OpenAsyncSession()) { - var loadedSenderEndpoint = await session.LoadAsync(KnownEndpoint.MakeDocumentId("SendHostName", sendHostId)).ConfigureAwait(false); - var loadedReceiverEndpoint = await session.LoadAsync(KnownEndpoint.MakeDocumentId("ReceivingHostName", receiverHostId)).ConfigureAwait(false); + var loadedSenderEndpoint = await session.LoadAsync(KnownEndpoint.MakeDocumentId("SendHostName", sendHostId)); + var loadedReceiverEndpoint = await session.LoadAsync(KnownEndpoint.MakeDocumentId("ReceivingHostName", receiverHostId)); Assert.NotNull(loadedReceiverEndpoint); Assert.NotNull(loadedSenderEndpoint); @@ -146,30 +146,30 @@ await session.StoreAsync(new ProcessedMessage { "SendingEndpoint", new EndpointDetails { Host = "SendHost", HostId = sendHostId, Name = "SendHostName" } }, { "ReceivingEndpoint", new EndpointDetails { Host = "ReceivingHost", HostId = receiverHostId, Name = "ReceivingHostName" } }, } - }).ConfigureAwait(false); + }); - await session.SaveChangesAsync().ConfigureAwait(false); + await session.SaveChangesAsync(); } configuration.DocumentStore.WaitForIndexing(); var migrator = new MigrateKnownEndpoints(configuration.DocumentStore); - await migrator.Migrate().ConfigureAwait(false); + await migrator.Migrate(); configuration.DocumentStore.ExecuteIndex(new EndpointsIndex()); configuration.DocumentStore.WaitForIndexing(); - await migrator.Migrate().ConfigureAwait(false); - await migrator.Migrate().ConfigureAwait(false); - await migrator.Migrate().ConfigureAwait(false); - await migrator.Migrate().ConfigureAwait(false); - await migrator.Migrate().ConfigureAwait(false); + await migrator.Migrate(); + await migrator.Migrate(); + await migrator.Migrate(); + await migrator.Migrate(); + await migrator.Migrate(); using (var session = configuration.DocumentStore.OpenAsyncSession()) { - var loadedSenderEndpoint = await session.LoadAsync(KnownEndpoint.MakeDocumentId("SendHostName", sendHostId)).ConfigureAwait(false); - var loadedReceiverEndpoint = await session.LoadAsync(KnownEndpoint.MakeDocumentId("ReceivingHostName", receiverHostId)).ConfigureAwait(false); + var loadedSenderEndpoint = await session.LoadAsync(KnownEndpoint.MakeDocumentId("SendHostName", sendHostId)); + var loadedReceiverEndpoint = await session.LoadAsync(KnownEndpoint.MakeDocumentId("ReceivingHostName", receiverHostId)); Assert.NotNull(loadedReceiverEndpoint); Assert.NotNull(loadedSenderEndpoint); @@ -196,21 +196,21 @@ await session.StoreAsync(new ProcessedMessage { "SendingEndpoint", new EndpointDetails { Host = "SendHost", HostId = sendHostId, Name = "SendHostName" } }, { "ReceivingEndpoint", new EndpointDetails { Host = "ReceivingHost", HostId = receiverHostId, Name = "ReceivingHostName" } }, } - }).ConfigureAwait(false); + }); - await session.SaveChangesAsync().ConfigureAwait(false); + await session.SaveChangesAsync(); } configuration.DocumentStore.WaitForIndexing(); var migrator = new MigrateKnownEndpoints(configuration.DocumentStore); - await migrator.Migrate(pageSize: 1).ConfigureAwait(false); + await migrator.Migrate(pageSize: 1); using (var session = configuration.DocumentStore.OpenAsyncSession()) { - var loadedSenderEndpoint = await session.LoadAsync(KnownEndpoint.MakeDocumentId("SendHostName", sendHostId)).ConfigureAwait(false); - var loadedReceiverEndpoint = await session.LoadAsync(KnownEndpoint.MakeDocumentId("ReceivingHostName", receiverHostId)).ConfigureAwait(false); + var loadedSenderEndpoint = await session.LoadAsync(KnownEndpoint.MakeDocumentId("SendHostName", sendHostId)); + var loadedReceiverEndpoint = await session.LoadAsync(KnownEndpoint.MakeDocumentId("ReceivingHostName", receiverHostId)); Assert.NotNull(loadedReceiverEndpoint); Assert.NotNull(loadedSenderEndpoint); @@ -235,20 +235,20 @@ await session.StoreAsync(new ProcessedMessage { "SendingEndpoint", new EndpointDetails { Host = "SendHost", HostId = sendHostId, Name = "SendHostName" } }, { "ReceivingEndpoint", new EndpointDetails { Host = "ReceivingHost", HostId = receiverHostId, Name = "ReceivingHostName" } }, } - }).ConfigureAwait(false); + }); - await session.SaveChangesAsync().ConfigureAwait(false); + await session.SaveChangesAsync(); } configuration.DocumentStore.WaitForIndexing(); var migrator = new MigrateKnownEndpoints(configuration.DocumentStore); - await migrator.Migrate(pageSize: 1).ConfigureAwait(false); + await migrator.Migrate(pageSize: 1); using (var session = configuration.DocumentStore.OpenAsyncSession()) { - var loadedSenderEndpoint = await session.LoadAsync(KnownEndpoint.MakeDocumentId("SendHostName", sendHostId)).ConfigureAwait(false); + var loadedSenderEndpoint = await session.LoadAsync(KnownEndpoint.MakeDocumentId("SendHostName", sendHostId)); Assert.Null(loadedSenderEndpoint); } diff --git a/src/ServiceControl.Audit.Persistence.Tests.RavenDB/PersistenceTestsConfiguration.cs b/src/ServiceControl.Audit.Persistence.Tests.RavenDB/PersistenceTestsConfiguration.cs index 754ce7c31a..a13c7871f4 100644 --- a/src/ServiceControl.Audit.Persistence.Tests.RavenDB/PersistenceTestsConfiguration.cs +++ b/src/ServiceControl.Audit.Persistence.Tests.RavenDB/PersistenceTestsConfiguration.cs @@ -16,6 +16,7 @@ partial class PersistenceTestsConfiguration public IFailedAuditStorage FailedAuditStorage { get; protected set; } public IBodyStorage BodyStorage { get; protected set; } public IAuditIngestionUnitOfWorkFactory AuditIngestionUnitOfWorkFactory { get; protected set; } + ServiceProvider serviceProvider; public async Task Configure(Action setSettings) { @@ -35,7 +36,7 @@ public async Task Configure(Action setSettings) await persistenceLifecycle.Start(); - var serviceProvider = serviceCollection.BuildServiceProvider(); + serviceProvider = serviceCollection.BuildServiceProvider(); AuditDataStore = serviceProvider.GetRequiredService(); FailedAuditStorage = serviceProvider.GetRequiredService(); @@ -50,9 +51,11 @@ public Task CompleteDBOperation() return Task.CompletedTask; } - public Task Cleanup() + public async Task Cleanup() { - return persistenceLifecycle?.Stop(); + await persistenceLifecycle.Stop(); + await serviceProvider.DisposeAsync(); + } IPersistenceLifecycle persistenceLifecycle; diff --git a/src/ServiceControl.Audit.Persistence.Tests.RavenDb5/.editorconfig b/src/ServiceControl.Audit.Persistence.Tests.RavenDb5/.editorconfig new file mode 100644 index 0000000000..5f68a610b3 --- /dev/null +++ b/src/ServiceControl.Audit.Persistence.Tests.RavenDb5/.editorconfig @@ -0,0 +1,4 @@ +[*.cs] + +# Justification: Test project +dotnet_diagnostic.CA2007.severity = none diff --git a/src/ServiceControl.Audit.Persistence.Tests.RavenDb5/RetentionTests.cs b/src/ServiceControl.Audit.Persistence.Tests.RavenDb5/RetentionTests.cs index fae044f6ef..648ffd4f14 100644 --- a/src/ServiceControl.Audit.Persistence.Tests.RavenDb5/RetentionTests.cs +++ b/src/ServiceControl.Audit.Persistence.Tests.RavenDb5/RetentionTests.cs @@ -30,15 +30,13 @@ public async Task AuditMessageRetention() await IngestProcessedMessagesAudits( message - ).ConfigureAwait(false); + ); - var queryResultBeforeExpiration = await DataStore.QueryMessages("MyMessageId", new PagingInfo(), new SortInfo("Id", "asc")) - .ConfigureAwait(false); + var queryResultBeforeExpiration = await DataStore.QueryMessages("MyMessageId", new PagingInfo(), new SortInfo("Id", "asc")); - await Task.Delay(4000).ConfigureAwait(false); + await Task.Delay(4000); - var queryResultAfterExpiration = await DataStore.QueryMessages("MyMessageId", new PagingInfo(), new SortInfo("Id", "asc")) - .ConfigureAwait(false); + var queryResultAfterExpiration = await DataStore.QueryMessages("MyMessageId", new PagingInfo(), new SortInfo("Id", "asc")); Assert.That(queryResultBeforeExpiration.Results.Count, Is.EqualTo(1)); Assert.That(queryResultBeforeExpiration.Results[0].MessageId, Is.EqualTo("MyMessageId")); @@ -60,15 +58,13 @@ public async Task KnownEndpointRetention() await IngestKnownEndpoints( knownEndpoint - ).ConfigureAwait(false); + ); - var queryResultBeforeExpiration = await DataStore.QueryKnownEndpoints() - .ConfigureAwait(false); + var queryResultBeforeExpiration = await DataStore.QueryKnownEndpoints(); - await Task.Delay(4000).ConfigureAwait(false); + await Task.Delay(4000); - var queryResultAfterExpiration = await DataStore.QueryKnownEndpoints() - .ConfigureAwait(false); + var queryResultAfterExpiration = await DataStore.QueryKnownEndpoints(); Assert.That(queryResultBeforeExpiration.Results.Count, Is.EqualTo(1)); Assert.That(queryResultBeforeExpiration.Results[0].EndpointDetails.Name, Is.EqualTo("Wazowsky")); @@ -90,7 +86,7 @@ await IngestSagaAudits( var queryResultBeforeExpiration = await DataStore.QuerySagaHistoryById(sagaId); - await Task.Delay(4000).ConfigureAwait(false); + await Task.Delay(4000); var queryResultAfterExpiration = await DataStore.QuerySagaHistoryById(sagaId); @@ -140,11 +136,10 @@ async Task IngestProcessedMessagesAudits(params ProcessedMessage[] processedMess var unitOfWork = StartAuditUnitOfWork(processedMessages.Length); foreach (var processedMessage in processedMessages) { - await unitOfWork.RecordProcessedMessage(processedMessage) - .ConfigureAwait(false); + await unitOfWork.RecordProcessedMessage(processedMessage); } - await unitOfWork.DisposeAsync().ConfigureAwait(false); - await configuration.CompleteDBOperation().ConfigureAwait(false); + await unitOfWork.DisposeAsync(); + await configuration.CompleteDBOperation(); } async Task IngestKnownEndpoints(params KnownEndpoint[] knownEndpoints) @@ -152,11 +147,10 @@ async Task IngestKnownEndpoints(params KnownEndpoint[] knownEndpoints) var unitOfWork = StartAuditUnitOfWork(knownEndpoints.Length); foreach (var knownEndpoint in knownEndpoints) { - await unitOfWork.RecordKnownEndpoint(knownEndpoint) - .ConfigureAwait(false); + await unitOfWork.RecordKnownEndpoint(knownEndpoint); } - await unitOfWork.DisposeAsync().ConfigureAwait(false); - await configuration.CompleteDBOperation().ConfigureAwait(false); + await unitOfWork.DisposeAsync(); + await configuration.CompleteDBOperation(); } async Task IngestSagaAudits(params SagaSnapshot[] snapshots) @@ -164,9 +158,9 @@ async Task IngestSagaAudits(params SagaSnapshot[] snapshots) var unitOfWork = StartAuditUnitOfWork(snapshots.Length); foreach (var snapshot in snapshots) { - await unitOfWork.RecordSagaSnapshot(snapshot).ConfigureAwait(false); + await unitOfWork.RecordSagaSnapshot(snapshot); } - await unitOfWork.DisposeAsync().ConfigureAwait(false); + await unitOfWork.DisposeAsync(); await configuration.CompleteDBOperation(); } } diff --git a/src/ServiceControl.Audit.Persistence.Tests.RavenDb5/SharedEmbeddedServer.cs b/src/ServiceControl.Audit.Persistence.Tests.RavenDb5/SharedEmbeddedServer.cs index cf673b48a4..3a0b52ca0a 100644 --- a/src/ServiceControl.Audit.Persistence.Tests.RavenDb5/SharedEmbeddedServer.cs +++ b/src/ServiceControl.Audit.Persistence.Tests.RavenDb5/SharedEmbeddedServer.cs @@ -13,7 +13,7 @@ class SharedEmbeddedServer { public static async Task GetInstance(CancellationToken cancellationToken = default) { - await semaphoreSlim.WaitAsync(cancellationToken).ConfigureAwait(false); + await semaphoreSlim.WaitAsync(cancellationToken); try { @@ -34,7 +34,7 @@ public static async Task GetInstance(CancellationToken cancell { try { - using (await embeddedDatabase.Connect(cancellationToken).ConfigureAwait(false)) + using (await embeddedDatabase.Connect(cancellationToken)) { //no-op } @@ -43,7 +43,7 @@ public static async Task GetInstance(CancellationToken cancell } catch (Exception) { - await Task.Delay(500, cancellationToken).ConfigureAwait(false); + await Task.Delay(500, cancellationToken); } } } @@ -55,7 +55,7 @@ public static async Task GetInstance(CancellationToken cancell public static async Task Stop(CancellationToken cancellationToken = default) { - await semaphoreSlim.WaitAsync(cancellationToken).ConfigureAwait(false); + await semaphoreSlim.WaitAsync(cancellationToken); try { embeddedDatabase?.Dispose(); diff --git a/src/ServiceControl.Audit.Persistence.Tests/AuditTests.cs b/src/ServiceControl.Audit.Persistence.Tests/AuditTests.cs index 66ac39f27e..54b3a2037d 100644 --- a/src/ServiceControl.Audit.Persistence.Tests/AuditTests.cs +++ b/src/ServiceControl.Audit.Persistence.Tests/AuditTests.cs @@ -30,8 +30,7 @@ await IngestProcessedMessagesAudits( message ); - var queryResult = await DataStore.QueryMessages("MyMessageId", new PagingInfo(), new SortInfo("Id", "asc")) - ; + var queryResult = await DataStore.QueryMessages("MyMessageId", new PagingInfo(), new SortInfo("Id", "asc")); Assert.That(queryResult.Results.Count, Is.EqualTo(1)); Assert.That(queryResult.Results[0].MessageId, Is.EqualTo("MyMessageId")); @@ -88,13 +87,13 @@ public async Task Can_roundtrip_message_body() new Random().NextBytes(body); var processedMessage = MakeMessage(); - await unitOfWork.RecordProcessedMessage(processedMessage, body).ConfigureAwait(false); + await unitOfWork.RecordProcessedMessage(processedMessage, body); - await unitOfWork.DisposeAsync().ConfigureAwait(false); + await unitOfWork.DisposeAsync(); var bodyId = GetBodyId(processedMessage); - var retrievedMessage = await DataStore.GetMessageBody(bodyId).ConfigureAwait(false); + var retrievedMessage = await DataStore.GetMessageBody(bodyId); Assert.That(retrievedMessage, Is.Not.Null); Assert.That(retrievedMessage.Found, Is.True); @@ -106,8 +105,7 @@ public async Task Can_roundtrip_message_body() Assert.That(retrievedMessage.ContentType, Is.EqualTo(expectedContentType)); var resultBody = new byte[body.Length]; - var readBytes = await retrievedMessage.StreamContent.ReadAsync(resultBody, 0, body.Length) - .ConfigureAwait(false); + var readBytes = await retrievedMessage.StreamContent.ReadAsync(resultBody, 0, body.Length); Assert.That(readBytes, Is.EqualTo(body.Length)); Assert.That(resultBody, Is.EqualTo(body)); } @@ -121,13 +119,13 @@ public async Task Does_respect_max_message_body() new Random().NextBytes(body); var processedMessage = MakeMessage(); - await unitOfWork.RecordProcessedMessage(processedMessage, body).ConfigureAwait(false); + await unitOfWork.RecordProcessedMessage(processedMessage, body); - await unitOfWork.DisposeAsync().ConfigureAwait(false); + await unitOfWork.DisposeAsync(); var bodyId = GetBodyId(processedMessage); - var retrievedMessage = await DataStore.GetMessageBody(bodyId).ConfigureAwait(false); + var retrievedMessage = await DataStore.GetMessageBody(bodyId); Assert.That(retrievedMessage, Is.Not.Null); Assert.That(retrievedMessage.Found, Is.True); @@ -145,10 +143,10 @@ public async Task Deduplicates_messages_in_same_batch() var processedMessage = MakeMessage(messageId: messageId, processingEndpoint: processingEndpoint, processingStarted: processingStarted); var duplicatedMessage = MakeMessage(messageId: messageId, processingEndpoint: processingEndpoint, processingStarted: processingStarted); - await unitOfWork.RecordProcessedMessage(processedMessage).ConfigureAwait(false); - await unitOfWork.RecordProcessedMessage(duplicatedMessage).ConfigureAwait(false); + await unitOfWork.RecordProcessedMessage(processedMessage); + await unitOfWork.RecordProcessedMessage(duplicatedMessage); - await unitOfWork.DisposeAsync().ConfigureAwait(false); + await unitOfWork.DisposeAsync(); await configuration.CompleteDBOperation(); @@ -166,13 +164,13 @@ public async Task Deduplicates_messages_in_different_batches() var processedMessage = MakeMessage(messageId: messageId, processingEndpoint: processingEndpoint, processingStarted: processingStarted); var unitOfWork1 = AuditIngestionUnitOfWorkFactory.StartNew(1); - await unitOfWork1.RecordProcessedMessage(processedMessage).ConfigureAwait(false); - await unitOfWork1.DisposeAsync().ConfigureAwait(false); + await unitOfWork1.RecordProcessedMessage(processedMessage); + await unitOfWork1.DisposeAsync(); var duplicatedMessage = MakeMessage(messageId: messageId, processingEndpoint: processingEndpoint, processingStarted: processingStarted); var unitOfWork2 = AuditIngestionUnitOfWorkFactory.StartNew(1); - await unitOfWork2.RecordProcessedMessage(duplicatedMessage).ConfigureAwait(false); - await unitOfWork2.DisposeAsync().ConfigureAwait(false); + await unitOfWork2.RecordProcessedMessage(duplicatedMessage); + await unitOfWork2.DisposeAsync(); await configuration.CompleteDBOperation(); @@ -192,10 +190,10 @@ public async Task Does_not_deduplicate_with_different_processing_started_header( var processedMessage = MakeMessage(messageId: messageId, processingEndpoint: processingEndpoint, processingStarted: processingStarted); var duplicatedMessage = MakeMessage(messageId: messageId, processingEndpoint: processingEndpoint, processingStarted: duplicatedProcessingStarted); - await unitOfWork.RecordProcessedMessage(processedMessage).ConfigureAwait(false); - await unitOfWork.RecordProcessedMessage(duplicatedMessage).ConfigureAwait(false); + await unitOfWork.RecordProcessedMessage(processedMessage); + await unitOfWork.RecordProcessedMessage(duplicatedMessage); - await unitOfWork.DisposeAsync().ConfigureAwait(false); + await unitOfWork.DisposeAsync(); await configuration.CompleteDBOperation(); diff --git a/src/ServiceControl.Audit.Persistence.Tests/FailedAuditStorageTests.cs b/src/ServiceControl.Audit.Persistence.Tests/FailedAuditStorageTests.cs index 9cc550fe8d..0cfff7e77b 100644 --- a/src/ServiceControl.Audit.Persistence.Tests/FailedAuditStorageTests.cs +++ b/src/ServiceControl.Audit.Persistence.Tests/FailedAuditStorageTests.cs @@ -31,8 +31,7 @@ public async Task Should_be_able_to_process_failures() var succeeded = 0; await FailedAuditStorage.ProcessFailedMessages(async (transportMessage, markComplete, token) => { - await markComplete(token) - ; + await markComplete(token); succeeded++; }, CancellationToken.None); diff --git a/src/ServiceControl.Audit.Persistence.Tests/KnownEndpointsTests.cs b/src/ServiceControl.Audit.Persistence.Tests/KnownEndpointsTests.cs index 20a23c4589..f9418783c6 100644 --- a/src/ServiceControl.Audit.Persistence.Tests/KnownEndpointsTests.cs +++ b/src/ServiceControl.Audit.Persistence.Tests/KnownEndpointsTests.cs @@ -56,8 +56,7 @@ async Task IngestKnownEndpoints(params KnownEndpoint[] knownEndpoints) var unitOfWork = StartAuditUnitOfWork(knownEndpoints.Length); foreach (var knownEndpoint in knownEndpoints) { - await unitOfWork.RecordKnownEndpoint(knownEndpoint) - ; + await unitOfWork.RecordKnownEndpoint(knownEndpoint); } await unitOfWork.DisposeAsync(); await configuration.CompleteDBOperation(); diff --git a/src/ServiceControl.Audit.Persistence.Tests/PersistenceTestFixture.cs b/src/ServiceControl.Audit.Persistence.Tests/PersistenceTestFixture.cs index 157edea263..cf358d9a31 100644 --- a/src/ServiceControl.Audit.Persistence.Tests/PersistenceTestFixture.cs +++ b/src/ServiceControl.Audit.Persistence.Tests/PersistenceTestFixture.cs @@ -9,7 +9,7 @@ using UnitOfWork; [TestFixture] - class PersistenceTestFixture + abstract class PersistenceTestFixture { public Action SetSettings = _ => { }; diff --git a/src/ServiceControl.Audit.Persistence.Tests/SagaHistoryTests.cs b/src/ServiceControl.Audit.Persistence.Tests/SagaHistoryTests.cs index cecf065041..957f98d3af 100644 --- a/src/ServiceControl.Audit.Persistence.Tests/SagaHistoryTests.cs +++ b/src/ServiceControl.Audit.Persistence.Tests/SagaHistoryTests.cs @@ -21,8 +21,7 @@ await IngestSagaAudits( } ); - var queryResult = await DataStore.QuerySagaHistoryById(sagaId) - ; + var queryResult = await DataStore.QuerySagaHistoryById(sagaId); Assert.That(queryResult.Results.SagaId, Is.EqualTo(sagaId)); Assert.That(queryResult.Results.SagaType, Is.EqualTo("MySagaType")); @@ -62,8 +61,7 @@ async Task IngestSagaAudits(params SagaSnapshot[] snapshots) var unitOfWork = StartAuditUnitOfWork(snapshots.Length); foreach (var snapshot in snapshots) { - await unitOfWork.RecordSagaSnapshot(snapshot) - ; + await unitOfWork.RecordSagaSnapshot(snapshot); } await unitOfWork.DisposeAsync(); await configuration.CompleteDBOperation(); diff --git a/src/ServiceControl.Audit.Persistence/.editorconfig b/src/ServiceControl.Audit.Persistence/.editorconfig new file mode 100644 index 0000000000..ff993b49bb --- /dev/null +++ b/src/ServiceControl.Audit.Persistence/.editorconfig @@ -0,0 +1,4 @@ +[*.cs] + +# Justification: ServiceControl app has no synchronization context +dotnet_diagnostic.CA2007.severity = none diff --git a/src/ServiceControl.Audit.Persistence/BodyStorageEnricher.cs b/src/ServiceControl.Audit.Persistence/BodyStorageEnricher.cs index ad530ea5e1..ccf5f313dd 100644 --- a/src/ServiceControl.Audit.Persistence/BodyStorageEnricher.cs +++ b/src/ServiceControl.Audit.Persistence/BodyStorageEnricher.cs @@ -28,8 +28,7 @@ public async ValueTask StoreAuditMessageBody(byte[] body, ProcessedMessage proce var contentType = GetContentType(processedMessage.Headers, "text/xml"); processedMessage.MessageMetadata.Add("ContentType", contentType); - var stored = await TryStoreBody(body, processedMessage, bodySize, contentType) - .ConfigureAwait(false); + var stored = await TryStoreBody(body, processedMessage, bodySize, contentType); if (!stored) { processedMessage.MessageMetadata.Add("BodyNotStored", true); @@ -84,8 +83,7 @@ async ValueTask TryStoreBody(byte[] body, ProcessedMessage processedMessag if (useBodyStore) { - await StoreBodyInBodyStorage(body, bodyId, contentType, bodySize) - .ConfigureAwait(false); + await StoreBodyInBodyStorage(body, bodyId, contentType, bodySize); storedInBodyStorage = true; } } @@ -98,8 +96,7 @@ async Task StoreBodyInBodyStorage(byte[] body, string bodyId, string contentType { using (var bodyStream = Manager.GetStream(bodyId, body, 0, bodySize)) { - await bodyStorage.Store(bodyId, contentType, bodySize, bodyStream) - .ConfigureAwait(false); + await bodyStorage.Store(bodyId, contentType, bodySize, bodyStream); } } diff --git a/src/ServiceControl.Audit.UnitTests/ApprovalFiles/APIApprovals.PublicClr.approved.txt b/src/ServiceControl.Audit.UnitTests/ApprovalFiles/APIApprovals.PublicClr.approved.txt index 63487b2fd9..cfdaa5ad1b 100644 --- a/src/ServiceControl.Audit.UnitTests/ApprovalFiles/APIApprovals.PublicClr.approved.txt +++ b/src/ServiceControl.Audit.UnitTests/ApprovalFiles/APIApprovals.PublicClr.approved.txt @@ -4,11 +4,9 @@ [assembly: System.Runtime.CompilerServices.InternalsVisibleTo("ServiceControl.Audit.Persistence.InMemory")] [assembly: System.Runtime.CompilerServices.InternalsVisibleTo("ServiceControl.Audit.Persistence.RavenDb")] [assembly: System.Runtime.CompilerServices.InternalsVisibleTo("ServiceControl.Audit.Persistence.RavenDb5")] -[assembly: System.Runtime.CompilerServices.InternalsVisibleTo("ServiceControl.Audit.Persistence.SqlServer")] [assembly: System.Runtime.CompilerServices.InternalsVisibleTo("ServiceControl.Audit.Persistence.Tests")] [assembly: System.Runtime.CompilerServices.InternalsVisibleTo("ServiceControl.Audit.Persistence.Tests.RavenDB")] [assembly: System.Runtime.CompilerServices.InternalsVisibleTo("ServiceControl.Audit.Persistence.Tests.RavenDb5")] -[assembly: System.Runtime.CompilerServices.InternalsVisibleTo("ServiceControl.Audit.Persistence.Tests.SqlServer")] [assembly: System.Runtime.CompilerServices.InternalsVisibleTo("ServiceControl.Audit.UnitTests")] [assembly: System.Runtime.CompilerServices.InternalsVisibleTo("ServiceControl.Loadtests.Reporter")] [assembly: System.Runtime.CompilerServices.InternalsVisibleTo("ServiceControl.MultiInstance.AcceptanceTests")] diff --git a/src/ServiceControl.Audit/.editorconfig b/src/ServiceControl.Audit/.editorconfig new file mode 100644 index 0000000000..ff993b49bb --- /dev/null +++ b/src/ServiceControl.Audit/.editorconfig @@ -0,0 +1,4 @@ +[*.cs] + +# Justification: ServiceControl app has no synchronization context +dotnet_diagnostic.CA2007.severity = none diff --git a/src/ServiceControl.Audit/Auditing/AuditIngestion.cs b/src/ServiceControl.Audit/Auditing/AuditIngestion.cs index d0552dd9a0..2b9253a18b 100644 --- a/src/ServiceControl.Audit/Auditing/AuditIngestion.cs +++ b/src/ServiceControl.Audit/Auditing/AuditIngestion.cs @@ -63,9 +63,9 @@ public AuditIngestion( public async Task StopAsync(CancellationToken _) { - await watchdog.Stop().ConfigureAwait(false); + await watchdog.Stop(); channel.Writer.Complete(); - await ingestionWorker.ConfigureAwait(false); + await ingestionWorker; } FailedAuditImport FailedMessageFactory(FailedTransportMessage msg) @@ -87,7 +87,7 @@ async Task EnsureStarted(CancellationToken cancellationToken = default) try { logger.Debug("Ensure started. Start/stop semaphore acquiring"); - await startStopSemaphore.WaitAsync(cancellationToken).ConfigureAwait(false); + await startStopSemaphore.WaitAsync(cancellationToken); logger.Debug("Ensure started. Start/stop semaphore acquired"); if (!unitOfWorkFactory.CanIngestMore()) @@ -97,7 +97,7 @@ async Task EnsureStarted(CancellationToken cancellationToken = default) var stoppable = queueIngestor; queueIngestor = null; logger.Info("Shutting down due to failed persistence health check. Infrastructure shut down commencing"); - await stoppable.Stop().ConfigureAwait(false); + await stoppable.Stop(); logger.Info("Shutting down due to failed persistence health check. Infrastructure shut down completed"); } return; @@ -116,14 +116,13 @@ async Task EnsureStarted(CancellationToken cancellationToken = default) transportSettings, OnMessage, errorHandlingPolicy.OnError, - OnCriticalError).ConfigureAwait(false); + OnCriticalError); - dispatcher = await transportCustomization.InitializeDispatcher(inputEndpoint, transportSettings).ConfigureAwait(false); + dispatcher = await transportCustomization.InitializeDispatcher(inputEndpoint, transportSettings); - await auditIngestor.VerifyCanReachForwardingAddress(dispatcher).ConfigureAwait(false); + await auditIngestor.VerifyCanReachForwardingAddress(dispatcher); - await queueIngestor.Start() - .ConfigureAwait(false); + await queueIngestor.Start(); logger.Info("Ensure started. Infrastructure started"); } @@ -131,7 +130,7 @@ await queueIngestor.Start() { if (queueIngestor != null) { - await queueIngestor.Stop().ConfigureAwait(false); + await queueIngestor.Stop(); } queueIngestor = null; // Setting to null so that it doesn't exit when it retries in line 185 @@ -151,7 +150,7 @@ async Task EnsureStopped(CancellationToken cancellationToken = default) try { logger.Info("Shutting down. Start/stop semaphore acquiring"); - await startStopSemaphore.WaitAsync(cancellationToken).ConfigureAwait(false); + await startStopSemaphore.WaitAsync(cancellationToken); logger.Info("Shutting down. Start/stop semaphore acquired"); if (queueIngestor == null) @@ -162,7 +161,7 @@ async Task EnsureStopped(CancellationToken cancellationToken = default) var stoppable = queueIngestor; queueIngestor = null; logger.Info("Shutting down. Infrastructure shut down commencing"); - await stoppable.Stop().ConfigureAwait(false); + await stoppable.Stop(); logger.Info("Shutting down. Infrastructure shut down completed"); } finally @@ -185,15 +184,15 @@ async Task OnMessage(MessageContext messageContext) receivedMeter.Mark(); - await channel.Writer.WriteAsync(messageContext).ConfigureAwait(false); - await taskCompletionSource.Task.ConfigureAwait(false); + await channel.Writer.WriteAsync(messageContext); + await taskCompletionSource.Task; } async Task Loop() { var contexts = new List(settings.MaximumConcurrencyLevel); - while (await channel.Reader.WaitToReadAsync().ConfigureAwait(false)) + while (await channel.Reader.WaitToReadAsync()) { // will only enter here if there is something to read. try @@ -207,7 +206,7 @@ async Task Loop() batchSizeMeter.Mark(contexts.Count); using (batchDurationMeter.Measure()) { - await auditIngestor.Ingest(contexts, dispatcher).ConfigureAwait(false); + await auditIngestor.Ingest(contexts, dispatcher); } } catch (OperationCanceledException) diff --git a/src/ServiceControl.Audit/Auditing/AuditIngestionFaultPolicy.cs b/src/ServiceControl.Audit/Auditing/AuditIngestionFaultPolicy.cs index 3ea7530ad6..7e36ff9115 100644 --- a/src/ServiceControl.Audit/Auditing/AuditIngestionFaultPolicy.cs +++ b/src/ServiceControl.Audit/Auditing/AuditIngestionFaultPolicy.cs @@ -35,8 +35,7 @@ public async Task OnError(ErrorContext errorContext) return ErrorHandleResult.RetryRequired; } - await StoreFailedMessageDocument(errorContext) - .ConfigureAwait(false); + await StoreFailedMessageDocument(errorContext); return ErrorHandleResult.Handled; } @@ -56,8 +55,7 @@ async Task Handle(Exception exception, dynamic failure) { try { - await DoLogging(exception, failure) - .ConfigureAwait(false); + await DoLogging(exception, failure); } finally { @@ -71,7 +69,7 @@ async Task DoLogging(Exception exception, dynamic failure) failure.Id = id; // Write to storage - await failedAuditStorage.Store(failure).ConfigureAwait(false); + await failedAuditStorage.Store(failure); // Write to Log Path var filePath = Path.Combine(logPath, failure.Id + ".txt"); diff --git a/src/ServiceControl.Audit/Auditing/AuditIngestor.cs b/src/ServiceControl.Audit/Auditing/AuditIngestor.cs index 54ac82e5a1..2ffd36b8fa 100644 --- a/src/ServiceControl.Audit/Auditing/AuditIngestor.cs +++ b/src/ServiceControl.Audit/Auditing/AuditIngestor.cs @@ -58,7 +58,7 @@ public async Task Ingest(List contexts, IDispatchMessages dispat log.Debug($"Ingesting {contexts.Count} message contexts"); } - var stored = await auditPersister.Persist(contexts, dispatcher).ConfigureAwait(false); + var stored = await auditPersister.Persist(contexts, dispatcher); try { @@ -68,8 +68,7 @@ public async Task Ingest(List contexts, IDispatchMessages dispat { log.Debug($"Forwarding {contexts.Count} messages"); } - await Forward(stored, settings.AuditLogQueue, dispatcher) - .ConfigureAwait(false); + await Forward(stored, settings.AuditLogQueue, dispatcher); if (log.IsDebugEnabled) { log.Debug("Forwarded messages"); @@ -146,8 +145,7 @@ public async Task VerifyCanReachForwardingAddress(IDispatchMessages dispatcher) ) ); - await dispatcher.Dispatch(transportOperations, new TransportTransaction(), new ContextBag()) - .ConfigureAwait(false); + await dispatcher.Dispatch(transportOperations, new TransportTransaction(), new ContextBag()); } catch (Exception e) { diff --git a/src/ServiceControl.Audit/Auditing/AuditPersister.cs b/src/ServiceControl.Audit/Auditing/AuditPersister.cs index 899d1a847d..98a9f74e6c 100644 --- a/src/ServiceControl.Audit/Auditing/AuditPersister.cs +++ b/src/ServiceControl.Audit/Auditing/AuditPersister.cs @@ -60,7 +60,7 @@ public async Task> Persist(List co inserts.Add(ProcessMessage(context, dispatcher)); } - await Task.WhenAll(inserts).ConfigureAwait(false); + await Task.WhenAll(inserts); var knownEndpoints = new Dictionary(); @@ -85,7 +85,7 @@ public async Task> Persist(List co using (auditBulkInsertDurationMeter.Measure()) { - await unitOfWork.RecordProcessedMessage(processedMessage, context.Body).ConfigureAwait(false); + await unitOfWork.RecordProcessedMessage(processedMessage, context.Body); } storedContexts.Add(context); @@ -100,7 +100,7 @@ public async Task> Persist(List co using (sagaAuditBulkInsertDurationMeter.Measure()) { - await unitOfWork.RecordSagaSnapshot(sagaSnapshot).ConfigureAwait(false); + await unitOfWork.RecordSagaSnapshot(sagaSnapshot); } storedContexts.Add(context); @@ -115,7 +115,7 @@ public async Task> Persist(List co Logger.Debug($"Adding known endpoint '{endpoint.Name}' for bulk storage"); } - await unitOfWork.RecordKnownEndpoint(endpoint).ConfigureAwait(false); + await unitOfWork.RecordKnownEndpoint(endpoint); } } catch (Exception e) @@ -142,7 +142,7 @@ public async Task> Persist(List co // this can throw even though dispose is never supposed to throw using (bulkInsertCommitDurationMeter.Measure()) { - await unitOfWork.DisposeAsync().ConfigureAwait(false); + await unitOfWork.DisposeAsync(); } } catch (Exception e) @@ -199,7 +199,7 @@ async Task ProcessMessage(MessageContext context, IDispatchMessages dispatcher) } else { - await ProcessAuditMessage(context, dispatcher).ConfigureAwait(false); + await ProcessAuditMessage(context, dispatcher); } } @@ -263,13 +263,12 @@ async Task ProcessAuditMessage(MessageContext context, IDispatchMessages dispatc } foreach (var commandToEmit in commandsToEmit) { - await messageSession.Send(commandToEmit) - .ConfigureAwait(false); + await messageSession.Send(commandToEmit); } await dispatcher.Dispatch(new TransportOperations(messagesToEmit.ToArray()), new TransportTransaction(), //Do not hook into the incoming transaction - new ContextBag()).ConfigureAwait(false); + new ContextBag()); if (Logger.IsDebugEnabled) { diff --git a/src/ServiceControl.Audit/Auditing/FailedAuditImportCustomCheck.cs b/src/ServiceControl.Audit/Auditing/FailedAuditImportCustomCheck.cs index 52275e2274..6515afd8bf 100644 --- a/src/ServiceControl.Audit/Auditing/FailedAuditImportCustomCheck.cs +++ b/src/ServiceControl.Audit/Auditing/FailedAuditImportCustomCheck.cs @@ -16,7 +16,7 @@ public FailedAuditImportCustomCheck(IFailedAuditStorage store) public override async Task PerformCheck() { - var count = await store.GetFailedAuditsCount().ConfigureAwait(false); + var count = await store.GetFailedAuditsCount(); if (count > 0) { Logger.Warn(message); diff --git a/src/ServiceControl.Audit/Auditing/GetBodyByIdApi.cs b/src/ServiceControl.Audit/Auditing/GetBodyByIdApi.cs index a5882ffa6e..f22f7acae8 100644 --- a/src/ServiceControl.Audit/Auditing/GetBodyByIdApi.cs +++ b/src/ServiceControl.Audit/Auditing/GetBodyByIdApi.cs @@ -16,7 +16,7 @@ public async Task Execute(HttpRequestMessage request, strin { messageId = messageId?.Replace("/", @"\"); - var result = await dataStore.GetMessageBody(messageId).ConfigureAwait(false); + var result = await dataStore.GetMessageBody(messageId); if (result.Found == false) { diff --git a/src/ServiceControl.Audit/Auditing/ImportFailedAudits.cs b/src/ServiceControl.Audit/Auditing/ImportFailedAudits.cs index d33545d852..57489204bc 100644 --- a/src/ServiceControl.Audit/Auditing/ImportFailedAudits.cs +++ b/src/ServiceControl.Audit/Auditing/ImportFailedAudits.cs @@ -28,9 +28,9 @@ public ImportFailedAudits( public async Task Run(CancellationToken cancellationToken = default) { - var dispatcher = await transportCustomization.InitializeDispatcher("ImportFailedAudits", transportSettings).ConfigureAwait(false); + var dispatcher = await transportCustomization.InitializeDispatcher("ImportFailedAudits", transportSettings); - await auditIngestor.VerifyCanReachForwardingAddress(dispatcher).ConfigureAwait(false); + await auditIngestor.VerifyCanReachForwardingAddress(dispatcher); var succeeded = 0; var failed = 0; @@ -44,12 +44,11 @@ await failedAuditStore.ProcessFailedMessages( var taskCompletionSource = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); messageContext.SetTaskCompletionSource(taskCompletionSource); - await auditIngestor.Ingest(new List { messageContext }, dispatcher).ConfigureAwait(false); + await auditIngestor.Ingest(new List { messageContext }, dispatcher); - await taskCompletionSource.Task.ConfigureAwait(false); + await taskCompletionSource.Task; - await markComplete(token) - .ConfigureAwait(false); + await markComplete(token); succeeded++; if (Logger.IsDebugEnabled) { @@ -66,7 +65,7 @@ await markComplete(token) failed++; } - }, cancellationToken).ConfigureAwait(false); + }, cancellationToken); Logger.Info($"Done re-importing failed audits. Successfully re-imported {succeeded} messages. Failed re-importing {failed} messages."); diff --git a/src/ServiceControl.Audit/Auditing/MessagesView/ApiBase.cs b/src/ServiceControl.Audit/Auditing/MessagesView/ApiBase.cs index e2664fa05a..e387833a94 100644 --- a/src/ServiceControl.Audit/Auditing/MessagesView/ApiBase.cs +++ b/src/ServiceControl.Audit/Auditing/MessagesView/ApiBase.cs @@ -24,7 +24,7 @@ public async Task Execute(ApiController controller, TIn inp { var currentRequest = controller.Request; - var queryResult = await Query(currentRequest, input).ConfigureAwait(false); + var queryResult = await Query(currentRequest, input); return Negotiator.FromQueryResult(currentRequest, queryResult); } diff --git a/src/ServiceControl.Audit/Auditing/MessagesView/GetAllMessagesApi.cs b/src/ServiceControl.Audit/Auditing/MessagesView/GetAllMessagesApi.cs index 26b8004b32..b4476c141e 100644 --- a/src/ServiceControl.Audit/Auditing/MessagesView/GetAllMessagesApi.cs +++ b/src/ServiceControl.Audit/Auditing/MessagesView/GetAllMessagesApi.cs @@ -17,7 +17,7 @@ protected override async Task>> Query(HttpReques var pagingInfo = request.GetPagingInfo(); var sortInfo = request.GetSortInfo(); var includeSystemMessages = request.GetIncludeSystemMessages(); - return await DataStore.GetMessages(includeSystemMessages, pagingInfo, sortInfo).ConfigureAwait(false); + return await DataStore.GetMessages(includeSystemMessages, pagingInfo, sortInfo); } } } \ No newline at end of file diff --git a/src/ServiceControl.Audit/Auditing/MessagesView/GetAllMessagesForEndpointApi.cs b/src/ServiceControl.Audit/Auditing/MessagesView/GetAllMessagesForEndpointApi.cs index 0d586c5cd1..57eef66606 100644 --- a/src/ServiceControl.Audit/Auditing/MessagesView/GetAllMessagesForEndpointApi.cs +++ b/src/ServiceControl.Audit/Auditing/MessagesView/GetAllMessagesForEndpointApi.cs @@ -17,7 +17,7 @@ protected override async Task>> Query(HttpReques var pagingInfo = request.GetPagingInfo(); var sortInfo = request.GetSortInfo(); var includeSystemMessages = request.GetIncludeSystemMessages(); - return await DataStore.QueryMessagesByReceivingEndpoint(includeSystemMessages, input, pagingInfo, sortInfo).ConfigureAwait(false); + return await DataStore.QueryMessagesByReceivingEndpoint(includeSystemMessages, input, pagingInfo, sortInfo); } } } \ No newline at end of file diff --git a/src/ServiceControl.Audit/Auditing/MessagesView/GetAuditCountsForEndpointApi.cs b/src/ServiceControl.Audit/Auditing/MessagesView/GetAuditCountsForEndpointApi.cs index 3b5f18aa0a..67bd2b8a26 100644 --- a/src/ServiceControl.Audit/Auditing/MessagesView/GetAuditCountsForEndpointApi.cs +++ b/src/ServiceControl.Audit/Auditing/MessagesView/GetAuditCountsForEndpointApi.cs @@ -13,7 +13,7 @@ public GetAuditCountsForEndpointApi(IAuditDataStore dataStore) : base(dataStore) protected override async Task>> Query(HttpRequestMessage request, string endpointName) { - return await DataStore.QueryAuditCounts(endpointName).ConfigureAwait(false); + return await DataStore.QueryAuditCounts(endpointName); } } } \ No newline at end of file diff --git a/src/ServiceControl.Audit/Auditing/MessagesView/MessagesByConversationApi.cs b/src/ServiceControl.Audit/Auditing/MessagesView/MessagesByConversationApi.cs index a352c5be62..0186795608 100644 --- a/src/ServiceControl.Audit/Auditing/MessagesView/MessagesByConversationApi.cs +++ b/src/ServiceControl.Audit/Auditing/MessagesView/MessagesByConversationApi.cs @@ -16,7 +16,7 @@ protected override async Task>> Query(HttpReques { var pagingInfo = request.GetPagingInfo(); var sortInfo = request.GetSortInfo(); - return await DataStore.QueryMessagesByConversationId(conversationId, pagingInfo, sortInfo).ConfigureAwait(false); + return await DataStore.QueryMessagesByConversationId(conversationId, pagingInfo, sortInfo); } } } \ No newline at end of file diff --git a/src/ServiceControl.Audit/Auditing/MessagesView/SearchApi.cs b/src/ServiceControl.Audit/Auditing/MessagesView/SearchApi.cs index cae83d654a..36b8692c4a 100644 --- a/src/ServiceControl.Audit/Auditing/MessagesView/SearchApi.cs +++ b/src/ServiceControl.Audit/Auditing/MessagesView/SearchApi.cs @@ -16,7 +16,7 @@ protected override async Task>> Query(HttpReques { var pagingInfo = request.GetPagingInfo(); var sortInfo = request.GetSortInfo(); - return await DataStore.QueryMessages(input, pagingInfo, sortInfo).ConfigureAwait(false); + return await DataStore.QueryMessages(input, pagingInfo, sortInfo); } } } \ No newline at end of file diff --git a/src/ServiceControl.Audit/Auditing/MessagesView/SearchEndpointApi.cs b/src/ServiceControl.Audit/Auditing/MessagesView/SearchEndpointApi.cs index 1c94025156..7f94750e5c 100644 --- a/src/ServiceControl.Audit/Auditing/MessagesView/SearchEndpointApi.cs +++ b/src/ServiceControl.Audit/Auditing/MessagesView/SearchEndpointApi.cs @@ -16,7 +16,7 @@ protected override async Task>> Query(HttpReques { var pagingInfo = request.GetPagingInfo(); var sortInfo = request.GetSortInfo(); - return await DataStore.QueryMessagesByReceivingEndpointAndKeyword(input.Endpoint, input.Keyword, pagingInfo, sortInfo).ConfigureAwait(false); + return await DataStore.QueryMessagesByReceivingEndpointAndKeyword(input.Endpoint, input.Keyword, pagingInfo, sortInfo); } public class Input diff --git a/src/ServiceControl.Audit/Infrastructure/Extensions/WaitHandleExtensions.cs b/src/ServiceControl.Audit/Infrastructure/Extensions/WaitHandleExtensions.cs index 59cd83eb55..c382aaf498 100644 --- a/src/ServiceControl.Audit/Infrastructure/Extensions/WaitHandleExtensions.cs +++ b/src/ServiceControl.Audit/Infrastructure/Extensions/WaitHandleExtensions.cs @@ -22,7 +22,7 @@ public static async Task WaitOneAsync(this WaitHandle handle, int millisec tokenRegistration = cancellationToken.Register( state => ((TaskCompletionSource)state).TrySetCanceled(), tcs); - return await tcs.Task.ConfigureAwait(false); + return await tcs.Task; } finally { diff --git a/src/ServiceControl.Audit/Infrastructure/Hosting/Commands/CommandRunner.cs b/src/ServiceControl.Audit/Infrastructure/Hosting/Commands/CommandRunner.cs index 414bfbdf2f..b6798c1334 100644 --- a/src/ServiceControl.Audit/Infrastructure/Hosting/Commands/CommandRunner.cs +++ b/src/ServiceControl.Audit/Infrastructure/Hosting/Commands/CommandRunner.cs @@ -16,7 +16,7 @@ public async Task Execute(HostArguments args, Settings.Settings settings) foreach (var commandType in commands) { var command = (AbstractCommand)Activator.CreateInstance(commandType); - await command.Execute(args, settings).ConfigureAwait(false); + await command.Execute(args, settings); } } diff --git a/src/ServiceControl.Audit/Infrastructure/Hosting/Commands/ImportFailedAuditsCommand.cs b/src/ServiceControl.Audit/Infrastructure/Hosting/Commands/ImportFailedAuditsCommand.cs index 894614afd0..d9e36d431d 100644 --- a/src/ServiceControl.Audit/Infrastructure/Hosting/Commands/ImportFailedAuditsCommand.cs +++ b/src/ServiceControl.Audit/Infrastructure/Hosting/Commands/ImportFailedAuditsCommand.cs @@ -28,7 +28,7 @@ public override async Task Execute(HostArguments args, Settings settings) var host = bootstrapper.HostBuilder.Build(); - await host.StartAsync(tokenSource.Token).ConfigureAwait(false); + await host.StartAsync(tokenSource.Token); var importer = host.Services.GetRequiredService(); @@ -36,7 +36,7 @@ public override async Task Execute(HostArguments args, Settings settings) try { - await importer.Run(tokenSource.Token).ConfigureAwait(false); + await importer.Run(tokenSource.Token); } catch (OperationCanceledException) { @@ -44,7 +44,7 @@ public override async Task Execute(HostArguments args, Settings settings) } finally { - await host.StopAsync(CancellationToken.None).ConfigureAwait(false); + await host.StopAsync(CancellationToken.None); } } } diff --git a/src/ServiceControl.Audit/Infrastructure/Hosting/Commands/SetupCommand.cs b/src/ServiceControl.Audit/Infrastructure/Hosting/Commands/SetupCommand.cs index f56763931f..bbb15b870f 100644 --- a/src/ServiceControl.Audit/Infrastructure/Hosting/Commands/SetupCommand.cs +++ b/src/ServiceControl.Audit/Infrastructure/Hosting/Commands/SetupCommand.cs @@ -9,8 +9,7 @@ public override async Task Execute(HostArguments args, Settings settings) { settings.SkipQueueCreation = args.SkipQueueCreation; - await new SetupBootstrapper(settings).Run(args.Username) - .ConfigureAwait(false); + await new SetupBootstrapper(settings).Run(args.Username); } } } \ No newline at end of file diff --git a/src/ServiceControl.Audit/Infrastructure/MaintenanceBootstrapper.cs b/src/ServiceControl.Audit/Infrastructure/MaintenanceBootstrapper.cs index c16959bac9..032f54de5b 100644 --- a/src/ServiceControl.Audit/Infrastructure/MaintenanceBootstrapper.cs +++ b/src/ServiceControl.Audit/Infrastructure/MaintenanceBootstrapper.cs @@ -22,19 +22,17 @@ public static async Task Run(HostArguments args, Settings.Settings settings) { hostBuilder.UseWindowsService(); - await hostBuilder.Build().RunAsync().ConfigureAwait(false); + await hostBuilder.Build().RunAsync(); } else { hostBuilder.UseConsoleLifetime(); - await Console.Out.WriteLineAsync("Running in Maintenance Mode - Press CTRL+C to exit") - .ConfigureAwait(false); + await Console.Out.WriteLineAsync("Running in Maintenance Mode - Press CTRL+C to exit"); - await hostBuilder.Build().RunAsync().ConfigureAwait(false); + await hostBuilder.Build().RunAsync(); - await Console.Out.WriteLineAsync("Disposing persister (this might take a while)...") - .ConfigureAwait(false); + await Console.Out.WriteLineAsync("Disposing persister (this might take a while)..."); } } } diff --git a/src/ServiceControl.Audit/Infrastructure/OWIN/ApiLogger.cs b/src/ServiceControl.Audit/Infrastructure/OWIN/ApiLogger.cs index 0d3cf5cb11..6db950ef8e 100644 --- a/src/ServiceControl.Audit/Infrastructure/OWIN/ApiLogger.cs +++ b/src/ServiceControl.Audit/Infrastructure/OWIN/ApiLogger.cs @@ -24,7 +24,7 @@ static async Task LogAllIncomingCalls(LogApiCalls middleware, IOwinContext conte { log.DebugFormat("Begin {0}: {1}", context.Request.Method, context.Request.Uri.ToString()); - await middleware.Next.Invoke(context).ConfigureAwait(false); + await middleware.Next.Invoke(context); log.DebugFormat("End {0} ({1}): {2}", context.Request.Method, context.Response.StatusCode, context.Request.Uri.ToString()); } diff --git a/src/ServiceControl.Audit/Infrastructure/SetupBootstrapper.cs b/src/ServiceControl.Audit/Infrastructure/SetupBootstrapper.cs index 7d95fd80e9..a430f1b9ef 100644 --- a/src/ServiceControl.Audit/Infrastructure/SetupBootstrapper.cs +++ b/src/ServiceControl.Audit/Infrastructure/SetupBootstrapper.cs @@ -41,7 +41,7 @@ public async Task Run(string username) additionalQueues.Add(settings.AuditLogQueue); } - await transportCustomization.ProvisionQueues(username, transportSettings, additionalQueues).ConfigureAwait(false); + await transportCustomization.ProvisionQueues(username, transportSettings, additionalQueues); } } @@ -52,8 +52,7 @@ public async Task Run(string username) var persistence = persistenceConfiguration.Create(persistenceSettings); var installer = persistence.CreateInstaller(); - await installer.Install() - .ConfigureAwait(false); + await installer.Install(); } bool ValidateLicense(Settings.Settings settings) diff --git a/src/ServiceControl.Audit/Infrastructure/WebApi/CachingHttpHandler.cs b/src/ServiceControl.Audit/Infrastructure/WebApi/CachingHttpHandler.cs index d63d818c63..32f483bac1 100644 --- a/src/ServiceControl.Audit/Infrastructure/WebApi/CachingHttpHandler.cs +++ b/src/ServiceControl.Audit/Infrastructure/WebApi/CachingHttpHandler.cs @@ -8,7 +8,7 @@ class CachingHttpHandler : DelegatingHandler { protected override async Task SendAsync(HttpRequestMessage request, CancellationToken cancellationToken = default) { - var response = await base.SendAsync(request, cancellationToken).ConfigureAwait(false); + var response = await base.SendAsync(request, cancellationToken); if (!response.Headers.Contains("Cache-Control")) { diff --git a/src/ServiceControl.Audit/Infrastructure/WebApi/CompressionEncodingHttpHandler.cs b/src/ServiceControl.Audit/Infrastructure/WebApi/CompressionEncodingHttpHandler.cs index b0a9fa66c1..775b1cbbac 100644 --- a/src/ServiceControl.Audit/Infrastructure/WebApi/CompressionEncodingHttpHandler.cs +++ b/src/ServiceControl.Audit/Infrastructure/WebApi/CompressionEncodingHttpHandler.cs @@ -13,7 +13,7 @@ class CompressionEncodingHttpHandler : DelegatingHandler { protected override async Task SendAsync(HttpRequestMessage request, CancellationToken cancellationToken = default) { - var response = await base.SendAsync(request, cancellationToken).ConfigureAwait(false); + var response = await base.SendAsync(request, cancellationToken); response.Headers.TransferEncodingChunked = true; @@ -114,7 +114,7 @@ protected override async Task SerializeToStreamAsync(Stream stream, TransportCon try { - await originalContent.CopyToAsync(compressedStream).ConfigureAwait(false); + await originalContent.CopyToAsync(compressedStream); } finally { diff --git a/src/ServiceControl.Audit/Infrastructure/WebApi/NotModifiedStatusHttpHandler.cs b/src/ServiceControl.Audit/Infrastructure/WebApi/NotModifiedStatusHttpHandler.cs index af921a8a81..791d4580b1 100644 --- a/src/ServiceControl.Audit/Infrastructure/WebApi/NotModifiedStatusHttpHandler.cs +++ b/src/ServiceControl.Audit/Infrastructure/WebApi/NotModifiedStatusHttpHandler.cs @@ -11,7 +11,7 @@ class NotModifiedStatusHttpHandler : DelegatingHandler { protected override async Task SendAsync(HttpRequestMessage request, CancellationToken cancellationToken = default) { - var response = await base.SendAsync(request, cancellationToken).ConfigureAwait(false); + var response = await base.SendAsync(request, cancellationToken); if (!response.IsSuccessStatusCode) { diff --git a/src/ServiceControl.Audit/Infrastructure/WebApi/XParticularVersionHttpHandler.cs b/src/ServiceControl.Audit/Infrastructure/WebApi/XParticularVersionHttpHandler.cs index ac1bc83706..063dddd351 100644 --- a/src/ServiceControl.Audit/Infrastructure/WebApi/XParticularVersionHttpHandler.cs +++ b/src/ServiceControl.Audit/Infrastructure/WebApi/XParticularVersionHttpHandler.cs @@ -14,7 +14,7 @@ static XParticularVersionHttpHandler() protected override async Task SendAsync(HttpRequestMessage request, CancellationToken cancellationToken = default) { - var response = await base.SendAsync(request, cancellationToken).ConfigureAwait(false); + var response = await base.SendAsync(request, cancellationToken); response.Headers.Add("X-Particular-Version", FileVersion); diff --git a/src/ServiceControl.Audit/InternalsVisibleTo.cs b/src/ServiceControl.Audit/InternalsVisibleTo.cs index cf53135123..8978054572 100644 --- a/src/ServiceControl.Audit/InternalsVisibleTo.cs +++ b/src/ServiceControl.Audit/InternalsVisibleTo.cs @@ -8,9 +8,7 @@ [assembly: InternalsVisibleTo("ServiceControl.Loadtests.Reporter")] [assembly: InternalsVisibleTo("ServiceControl.Audit.Persistence.RavenDb")] [assembly: InternalsVisibleTo("ServiceControl.Audit.Persistence.RavenDb5")] -[assembly: InternalsVisibleTo("ServiceControl.Audit.Persistence.SqlServer")] [assembly: InternalsVisibleTo("ServiceControl.Audit.Persistence.InMemory")] [assembly: InternalsVisibleTo("ServiceControl.Audit.Persistence.Tests")] [assembly: InternalsVisibleTo("ServiceControl.Audit.Persistence.Tests.RavenDB")] [assembly: InternalsVisibleTo("ServiceControl.Audit.Persistence.Tests.RavenDb5")] -[assembly: InternalsVisibleTo("ServiceControl.Audit.Persistence.Tests.SqlServer")] diff --git a/src/ServiceControl.Audit/Monitoring/KnownEndpoints/GetKnownEndpointsApi.cs b/src/ServiceControl.Audit/Monitoring/KnownEndpoints/GetKnownEndpointsApi.cs index 3797cd23dd..68eb175263 100644 --- a/src/ServiceControl.Audit/Monitoring/KnownEndpoints/GetKnownEndpointsApi.cs +++ b/src/ServiceControl.Audit/Monitoring/KnownEndpoints/GetKnownEndpointsApi.cs @@ -14,7 +14,7 @@ public GetKnownEndpointsApi(IAuditDataStore dataStore) : base(dataStore) protected override async Task>> Query(HttpRequestMessage request) { - return await DataStore.QueryKnownEndpoints().ConfigureAwait(false); + return await DataStore.QueryKnownEndpoints(); } } } \ No newline at end of file diff --git a/src/ServiceControl.Audit/Program.cs b/src/ServiceControl.Audit/Program.cs index 76bc7cab6c..a77a9c3ccd 100644 --- a/src/ServiceControl.Audit/Program.cs +++ b/src/ServiceControl.Audit/Program.cs @@ -34,8 +34,7 @@ static async Task Main(string[] args) settings = Settings.FromConfiguration(arguments.ServiceName); - await new CommandRunner(arguments.Commands).Execute(arguments, settings) - .ConfigureAwait(false); + await new CommandRunner(arguments.Commands).Execute(arguments, settings); } static void LogException(Exception ex) diff --git a/src/ServiceControl.Audit/SagaAudit/GetSagaByIdApi.cs b/src/ServiceControl.Audit/SagaAudit/GetSagaByIdApi.cs index 7e8ca4d75d..abd34d0873 100644 --- a/src/ServiceControl.Audit/SagaAudit/GetSagaByIdApi.cs +++ b/src/ServiceControl.Audit/SagaAudit/GetSagaByIdApi.cs @@ -15,7 +15,7 @@ public GetSagaByIdApi(IAuditDataStore dataStore) : base(dataStore) protected override async Task> Query(HttpRequestMessage request, Guid input) { - return await DataStore.QuerySagaHistoryById(input).ConfigureAwait(false); + return await DataStore.QuerySagaHistoryById(input); } } } \ No newline at end of file diff --git a/src/ServiceControl.Config.Tests/.editorconfig b/src/ServiceControl.Config.Tests/.editorconfig new file mode 100644 index 0000000000..5f68a610b3 --- /dev/null +++ b/src/ServiceControl.Config.Tests/.editorconfig @@ -0,0 +1,4 @@ +[*.cs] + +# Justification: Test project +dotnet_diagnostic.CA2007.severity = none diff --git a/src/ServiceControl.Config.Tests/AutoAttachmentModuleTests.cs b/src/ServiceControl.Config.Tests/AutoAttachmentModuleTests.cs index e1c6c1726e..91c02b7181 100644 --- a/src/ServiceControl.Config.Tests/AutoAttachmentModuleTests.cs +++ b/src/ServiceControl.Config.Tests/AutoAttachmentModuleTests.cs @@ -10,10 +10,10 @@ public class AutoAttachmentModuleTests { [OneTimeSetUp] - public void SetUp() => AssemblySource.Instance.Add(typeof(AutoAttachmentModuleTests).Assembly); + public static void SetUp() => AssemblySource.Instance.Add(typeof(AutoAttachmentModuleTests).Assembly); [OneTimeTearDown] - public void TearDown() => AssemblySource.Instance.Remove(typeof(AutoAttachmentModuleTests).Assembly); + public static void TearDown() => AssemblySource.Instance.Remove(typeof(AutoAttachmentModuleTests).Assembly); [Test] public void AttachmentsAreAttached() diff --git a/src/ServiceControl.Config.Tests/UpgradeControlTests.cs b/src/ServiceControl.Config.Tests/UpgradeControlTests.cs index d78936b8e2..9658759987 100644 --- a/src/ServiceControl.Config.Tests/UpgradeControlTests.cs +++ b/src/ServiceControl.Config.Tests/UpgradeControlTests.cs @@ -14,7 +14,7 @@ public async Task GetUpgradeInfoForTargetVersionSameMajor() { var current = new Version("4.13.3"); - var releaseDetails = await VersionCheckerHelper.GetLatestRelease(current.ToString()).ConfigureAwait(false); + var releaseDetails = await VersionCheckerHelper.GetLatestRelease(current.ToString()); Assert.IsNotNull(releaseDetails, "Failed to get a release details"); Assert.That(releaseDetails.Version, Is.GreaterThanOrEqualTo(current), "Got a lower version than current"); diff --git a/src/ServiceControl.Config/Framework/Modules/InstallerModule.cs b/src/ServiceControl.Config/Framework/Modules/InstallerModule.cs index 735356cf62..6be8fbee67 100644 --- a/src/ServiceControl.Config/Framework/Modules/InstallerModule.cs +++ b/src/ServiceControl.Config/Framework/Modules/InstallerModule.cs @@ -151,7 +151,7 @@ internal async Task Update(ServiceControlBaseService instance, bool try { instance.ReportCard = new ReportCard(); - await instance.ValidateChanges().ConfigureAwait(false); + await instance.ValidateChanges(); if (instance.ReportCard.HasErrors) { instance.ReportCard.Status = Status.FailedValidation; diff --git a/src/ServiceControl.Config/UI/Shell/ShellViewModel.cs b/src/ServiceControl.Config/UI/Shell/ShellViewModel.cs index 20a388a682..2ef87d8cc6 100644 --- a/src/ServiceControl.Config/UI/Shell/ShellViewModel.cs +++ b/src/ServiceControl.Config/UI/Shell/ShellViewModel.cs @@ -146,7 +146,7 @@ async Task CheckForUpdates() // get the json version file from https://s3.us-east-1.amazonaws.com/platformupdate.particular.net/servicecontrol.txt var shortAppVersion = AppVersion.Split('-').First(); - var availableUpgradeRelease = await VersionCheckerHelper.GetLatestRelease(shortAppVersion).ConfigureAwait(false); + var availableUpgradeRelease = await VersionCheckerHelper.GetLatestRelease(shortAppVersion); if (availableUpgradeRelease.Version.ToString() == shortAppVersion) { diff --git a/src/ServiceControl.Config/UI/Shell/VersionCheckerHelper.cs b/src/ServiceControl.Config/UI/Shell/VersionCheckerHelper.cs index f282622dca..2681d58b27 100644 --- a/src/ServiceControl.Config/UI/Shell/VersionCheckerHelper.cs +++ b/src/ServiceControl.Config/UI/Shell/VersionCheckerHelper.cs @@ -14,7 +14,7 @@ public static class VersionCheckerHelper public static async Task GetLatestRelease(string currentVersion) { var current = new Version(currentVersion); - List releases = await GetVersionInformation().ConfigureAwait(false); + List releases = await GetVersionInformation(); if (releases != null) { @@ -34,7 +34,7 @@ static async Task> GetVersionInformation() { try { - var json = await httpClient.GetStringAsync("https://s3.us-east-1.amazonaws.com/platformupdate.particular.net/servicecontrol.txt").ConfigureAwait(false); + var json = await httpClient.GetStringAsync("https://s3.us-east-1.amazonaws.com/platformupdate.particular.net/servicecontrol.txt"); return JsonConvert.DeserializeObject>(json); } diff --git a/src/ServiceControl.Configuration/ConfigFileSettingsReader.cs b/src/ServiceControl.Configuration/ConfigFileSettingsReader.cs index bbe7260da2..f0a9da3ad4 100644 --- a/src/ServiceControl.Configuration/ConfigFileSettingsReader.cs +++ b/src/ServiceControl.Configuration/ConfigFileSettingsReader.cs @@ -24,9 +24,11 @@ public static bool TryRead(string root, string name, out T value) { var fullKey = $"{root}/{name}"; - if (ConfigurationManager.AppSettings[fullKey] != null) + var appSettingValue = ConfigurationManager.AppSettings[fullKey]; + if (appSettingValue != null) { - value = (T)Convert.ChangeType(ConfigurationManager.AppSettings[fullKey], typeof(T)); + appSettingValue = Environment.ExpandEnvironmentVariables(appSettingValue); // TODO: Just added this to have expansing on appsettings to not have hardcoded "temp" paths which are different for everyone. + value = (T)Convert.ChangeType(appSettingValue, typeof(T)); return true; } diff --git a/src/ServiceControl.Configuration/InternalsVisibleTo.cs b/src/ServiceControl.Configuration/InternalsVisibleTo.cs index 980a012cb9..a5ba5a760b 100644 --- a/src/ServiceControl.Configuration/InternalsVisibleTo.cs +++ b/src/ServiceControl.Configuration/InternalsVisibleTo.cs @@ -8,4 +8,3 @@ [assembly: InternalsVisibleTo("ServiceControl.MultiInstance.AcceptanceTests")] [assembly: InternalsVisibleTo("ServiceControl.Persistence.InMemory")] [assembly: InternalsVisibleTo("ServiceControl.Persistence.RavenDb")] -[assembly: InternalsVisibleTo("ServiceControl.Persistence.SqlServer")] diff --git a/src/ServiceControl/Infrastructure/DomainEvents/DomainEvents.cs b/src/ServiceControl.DomainEvents/DomainEvents.cs similarity index 95% rename from src/ServiceControl/Infrastructure/DomainEvents/DomainEvents.cs rename to src/ServiceControl.DomainEvents/DomainEvents.cs index 5fd577c4a0..6df5c980c1 100644 --- a/src/ServiceControl/Infrastructure/DomainEvents/DomainEvents.cs +++ b/src/ServiceControl.DomainEvents/DomainEvents.cs @@ -4,7 +4,7 @@ using System.Threading.Tasks; using Microsoft.Extensions.DependencyInjection; - class DomainEvents : IDomainEvents + public class DomainEvents : IDomainEvents { readonly IServiceProvider serviceProvider; public DomainEvents(IServiceProvider serviceProvider) => this.serviceProvider = serviceProvider; diff --git a/src/ServiceControl/Infrastructure/DomainEvents/DomainEventsServiceCollectionExtensions.cs b/src/ServiceControl.DomainEvents/DomainEventsServiceCollectionExtensions.cs similarity index 95% rename from src/ServiceControl/Infrastructure/DomainEvents/DomainEventsServiceCollectionExtensions.cs rename to src/ServiceControl.DomainEvents/DomainEventsServiceCollectionExtensions.cs index 5515b09bd9..bc4cd41259 100644 --- a/src/ServiceControl/Infrastructure/DomainEvents/DomainEventsServiceCollectionExtensions.cs +++ b/src/ServiceControl.DomainEvents/DomainEventsServiceCollectionExtensions.cs @@ -4,7 +4,7 @@ using System.Linq; using Microsoft.Extensions.DependencyInjection; - static class DomainEventsServiceCollectionExtensions + public static class DomainEventsServiceCollectionExtensions { /// /// Registers provided type as event handler for all its implemented IDomainHandler interfaces as a transient component diff --git a/src/ServiceControl/Infrastructure/DomainEvents/IDomainEvent.cs b/src/ServiceControl.DomainEvents/Interfaces/IDomainEvent.cs similarity index 81% rename from src/ServiceControl/Infrastructure/DomainEvents/IDomainEvent.cs rename to src/ServiceControl.DomainEvents/Interfaces/IDomainEvent.cs index a0006b74a7..692541d852 100644 --- a/src/ServiceControl/Infrastructure/DomainEvents/IDomainEvent.cs +++ b/src/ServiceControl.DomainEvents/Interfaces/IDomainEvent.cs @@ -3,7 +3,7 @@ /// /// A marker interface for domain events. /// - interface IDomainEvent + public interface IDomainEvent { } } \ No newline at end of file diff --git a/src/ServiceControl/Infrastructure/DomainEvents/IDomainEvents.cs b/src/ServiceControl.DomainEvents/Interfaces/IDomainEvents.cs similarity index 82% rename from src/ServiceControl/Infrastructure/DomainEvents/IDomainEvents.cs rename to src/ServiceControl.DomainEvents/Interfaces/IDomainEvents.cs index a8798bdfeb..7e7ea4cf3b 100644 --- a/src/ServiceControl/Infrastructure/DomainEvents/IDomainEvents.cs +++ b/src/ServiceControl.DomainEvents/Interfaces/IDomainEvents.cs @@ -2,7 +2,7 @@ { using System.Threading.Tasks; - interface IDomainEvents + public interface IDomainEvents { Task Raise(T domainEvent) where T : IDomainEvent; } diff --git a/src/ServiceControl/Infrastructure/DomainEvents/IDomainHandler.cs b/src/ServiceControl.DomainEvents/Interfaces/IDomainHandler.cs similarity index 68% rename from src/ServiceControl/Infrastructure/DomainEvents/IDomainHandler.cs rename to src/ServiceControl.DomainEvents/Interfaces/IDomainHandler.cs index 1ebb721f1f..f7d49d97ea 100644 --- a/src/ServiceControl/Infrastructure/DomainEvents/IDomainHandler.cs +++ b/src/ServiceControl.DomainEvents/Interfaces/IDomainHandler.cs @@ -2,7 +2,7 @@ namespace ServiceControl.Infrastructure.DomainEvents { using System.Threading.Tasks; - interface IDomainHandler where T : IDomainEvent + public interface IDomainHandler where T : IDomainEvent { Task Handle(T domainEvent); } diff --git a/src/ServiceControl/Infrastructure/SignalR/IUserInterfaceEvent.cs b/src/ServiceControl.DomainEvents/Interfaces/IUserInterfaceEvent.cs similarity index 100% rename from src/ServiceControl/Infrastructure/SignalR/IUserInterfaceEvent.cs rename to src/ServiceControl.DomainEvents/Interfaces/IUserInterfaceEvent.cs diff --git a/src/ServiceControl/Recoverability/Archiving/Messages/ArchiveOperationBatchCompleted.cs b/src/ServiceControl.DomainEvents/Recoverability/ArchiveOperationBatchCompleted.cs similarity index 100% rename from src/ServiceControl/Recoverability/Archiving/Messages/ArchiveOperationBatchCompleted.cs rename to src/ServiceControl.DomainEvents/Recoverability/ArchiveOperationBatchCompleted.cs diff --git a/src/ServiceControl/Recoverability/Archiving/Messages/ArchiveOperationCompleted.cs b/src/ServiceControl.DomainEvents/Recoverability/ArchiveOperationCompleted.cs similarity index 100% rename from src/ServiceControl/Recoverability/Archiving/Messages/ArchiveOperationCompleted.cs rename to src/ServiceControl.DomainEvents/Recoverability/ArchiveOperationCompleted.cs diff --git a/src/ServiceControl/Recoverability/Archiving/Messages/ArchiveOperationFinalizing.cs b/src/ServiceControl.DomainEvents/Recoverability/ArchiveOperationFinalizing.cs similarity index 100% rename from src/ServiceControl/Recoverability/Archiving/Messages/ArchiveOperationFinalizing.cs rename to src/ServiceControl.DomainEvents/Recoverability/ArchiveOperationFinalizing.cs diff --git a/src/ServiceControl/Recoverability/Archiving/Messages/ArchiveOperationStarting.cs b/src/ServiceControl.DomainEvents/Recoverability/ArchiveOperationStarting.cs similarity index 100% rename from src/ServiceControl/Recoverability/Archiving/Messages/ArchiveOperationStarting.cs rename to src/ServiceControl.DomainEvents/Recoverability/ArchiveOperationStarting.cs diff --git a/src/ServiceControl/Recoverability/Archiving/ArchiveProgress.cs b/src/ServiceControl.DomainEvents/Recoverability/ArchiveProgress.cs similarity index 100% rename from src/ServiceControl/Recoverability/Archiving/ArchiveProgress.cs rename to src/ServiceControl.DomainEvents/Recoverability/ArchiveProgress.cs diff --git a/src/ServiceControl/Recoverability/Archiving/ArchiveType.cs b/src/ServiceControl.DomainEvents/Recoverability/ArchiveType.cs similarity index 100% rename from src/ServiceControl/Recoverability/Archiving/ArchiveType.cs rename to src/ServiceControl.DomainEvents/Recoverability/ArchiveType.cs diff --git a/src/ServiceControl/Recoverability/Archiving/Messages/FailedMessageGroupArchived.cs b/src/ServiceControl.DomainEvents/Recoverability/FailedMessageGroupArchived.cs similarity index 100% rename from src/ServiceControl/Recoverability/Archiving/Messages/FailedMessageGroupArchived.cs rename to src/ServiceControl.DomainEvents/Recoverability/FailedMessageGroupArchived.cs diff --git a/src/ServiceControl/Recoverability/Archiving/Messages/FailedMessageGroupBatchUnarchived.cs b/src/ServiceControl.DomainEvents/Recoverability/FailedMessageGroupBatchUnarchived.cs similarity index 100% rename from src/ServiceControl/Recoverability/Archiving/Messages/FailedMessageGroupBatchUnarchived.cs rename to src/ServiceControl.DomainEvents/Recoverability/FailedMessageGroupBatchUnarchived.cs diff --git a/src/ServiceControl/Recoverability/Archiving/Messages/FailedMessageGroupUnarchived.cs b/src/ServiceControl.DomainEvents/Recoverability/FailedMessageGroupUnarchived.cs similarity index 100% rename from src/ServiceControl/Recoverability/Archiving/Messages/FailedMessageGroupUnarchived.cs rename to src/ServiceControl.DomainEvents/Recoverability/FailedMessageGroupUnarchived.cs diff --git a/src/ServiceControl/Recoverability/Archiving/Messages/UnarchiveOperationBatchCompleted.cs b/src/ServiceControl.DomainEvents/Recoverability/UnarchiveOperationBatchCompleted.cs similarity index 100% rename from src/ServiceControl/Recoverability/Archiving/Messages/UnarchiveOperationBatchCompleted.cs rename to src/ServiceControl.DomainEvents/Recoverability/UnarchiveOperationBatchCompleted.cs diff --git a/src/ServiceControl/Recoverability/Archiving/Messages/UnarchiveOperationCompleted.cs b/src/ServiceControl.DomainEvents/Recoverability/UnarchiveOperationCompleted.cs similarity index 100% rename from src/ServiceControl/Recoverability/Archiving/Messages/UnarchiveOperationCompleted.cs rename to src/ServiceControl.DomainEvents/Recoverability/UnarchiveOperationCompleted.cs diff --git a/src/ServiceControl/Recoverability/Archiving/Messages/UnarchiveOperationFinalizing.cs b/src/ServiceControl.DomainEvents/Recoverability/UnarchiveOperationFinalizing.cs similarity index 100% rename from src/ServiceControl/Recoverability/Archiving/Messages/UnarchiveOperationFinalizing.cs rename to src/ServiceControl.DomainEvents/Recoverability/UnarchiveOperationFinalizing.cs diff --git a/src/ServiceControl/Recoverability/Archiving/Messages/UnarchiveOperationStarting.cs b/src/ServiceControl.DomainEvents/Recoverability/UnarchiveOperationStarting.cs similarity index 100% rename from src/ServiceControl/Recoverability/Archiving/Messages/UnarchiveOperationStarting.cs rename to src/ServiceControl.DomainEvents/Recoverability/UnarchiveOperationStarting.cs diff --git a/src/ServiceControl/Recoverability/Archiving/UnarchiveProgress.cs b/src/ServiceControl.DomainEvents/Recoverability/UnarchiveProgress.cs similarity index 100% rename from src/ServiceControl/Recoverability/Archiving/UnarchiveProgress.cs rename to src/ServiceControl.DomainEvents/Recoverability/UnarchiveProgress.cs diff --git a/src/ServiceControl.DomainEvents/ServiceControl.DomainEvents.csproj b/src/ServiceControl.DomainEvents/ServiceControl.DomainEvents.csproj new file mode 100644 index 0000000000..b338efabda --- /dev/null +++ b/src/ServiceControl.DomainEvents/ServiceControl.DomainEvents.csproj @@ -0,0 +1,11 @@ + + + + net472 + + + + + + + diff --git a/src/ServiceControl/Infrastructure/BackgroundTasks/AsyncTimer.cs b/src/ServiceControl.Infrastructure/AsyncTimer.cs similarity index 100% rename from src/ServiceControl/Infrastructure/BackgroundTasks/AsyncTimer.cs rename to src/ServiceControl.Infrastructure/AsyncTimer.cs diff --git a/src/ServiceControl.Infrastructure/ServiceControl.Infrastructure.csproj b/src/ServiceControl.Infrastructure/ServiceControl.Infrastructure.csproj index ef64205cee..c81f250251 100644 --- a/src/ServiceControl.Infrastructure/ServiceControl.Infrastructure.csproj +++ b/src/ServiceControl.Infrastructure/ServiceControl.Infrastructure.csproj @@ -5,6 +5,7 @@ + diff --git a/src/ServiceControl.LoadTests.AuditGenerator/LoadGenerator.cs b/src/ServiceControl.LoadTests.AuditGenerator/LoadGenerator.cs index 7e067eed5c..7b4151376b 100644 --- a/src/ServiceControl.LoadTests.AuditGenerator/LoadGenerator.cs +++ b/src/ServiceControl.LoadTests.AuditGenerator/LoadGenerator.cs @@ -17,7 +17,7 @@ public LoadGenerator(string destination, Func serviceControlRunnerBehavior.Handler; [OneTimeSetUp] - public void OneTimeSetup() + public static void OneTimeSetup() { Scenario.GetLoggerFactory = ctx => new StaticLoggerFactory(ctx); } diff --git a/src/ServiceControl.Monitoring.AcceptanceTests/PerformanceTests.cs b/src/ServiceControl.Monitoring.AcceptanceTests/PerformanceTests.cs index 971758e805..c895d0546f 100644 --- a/src/ServiceControl.Monitoring.AcceptanceTests/PerformanceTests.cs +++ b/src/ServiceControl.Monitoring.AcceptanceTests/PerformanceTests.cs @@ -101,7 +101,7 @@ public async Task GetMonitoredEndpointsQueryTest(int numberOfEndpoints, int numb } source.Cancel(); - await Task.WhenAll(reporters).ConfigureAwait(false); + await Task.WhenAll(reporters); var reportFinalHistogram = MergeHistograms(reporters); @@ -156,11 +156,11 @@ public async Task GetMonitoredSingleEndpointQueryTest(int numberOfInstances, int var elapsed = Stopwatch.GetTimestamp() - start; histogram.RecordValue(elapsed); - await Task.Delay(queryEveryInMilliseconds).ConfigureAwait(false); + await Task.Delay(queryEveryInMilliseconds); } source.Cancel(); - await Task.WhenAll(reporters).ConfigureAwait(false); + await Task.WhenAll(reporters); var reportFinalHistogram = MergeHistograms(reporters); @@ -197,7 +197,7 @@ static Task StartReporter(int sendReportEvery, int numberOfEntrie var elapsed = Stopwatch.GetTimestamp() - start; histogram.RecordValue(elapsed); - await Task.Delay(sendReportEvery).ConfigureAwait(false); + await Task.Delay(sendReportEvery); } return histogram; diff --git a/src/ServiceControl.Monitoring.AcceptanceTests/TestSupport/ServiceControlComponentBehavior.cs b/src/ServiceControl.Monitoring.AcceptanceTests/TestSupport/ServiceControlComponentBehavior.cs index bde051ae3c..076d37c08f 100644 --- a/src/ServiceControl.Monitoring.AcceptanceTests/TestSupport/ServiceControlComponentBehavior.cs +++ b/src/ServiceControl.Monitoring.AcceptanceTests/TestSupport/ServiceControlComponentBehavior.cs @@ -27,7 +27,7 @@ public ServiceControlComponentBehavior(ITransportIntegration transportToUse, Act public async Task CreateRunner(RunDescriptor run) { runner = new ServiceControlComponentRunner(transportIntegration, setSettings, customConfiguration); - await runner.Initialize(run).ConfigureAwait(false); + await runner.Initialize(run); return runner; } diff --git a/src/ServiceControl.Monitoring/Bootstrapper.cs b/src/ServiceControl.Monitoring/Bootstrapper.cs index 97a2964265..2f1ded27e9 100644 --- a/src/ServiceControl.Monitoring/Bootstrapper.cs +++ b/src/ServiceControl.Monitoring/Bootstrapper.cs @@ -187,7 +187,7 @@ public override async Task Invoke(IIncomingLogicalMessageContext context, Func SendAsync(HttpRequestMessage request, CancellationToken cancellationToken = default) { - var response = await base.SendAsync(request, cancellationToken).ConfigureAwait(false); + var response = await base.SendAsync(request, cancellationToken); if (!response.Content.Headers.Contains("Expires")) { diff --git a/src/ServiceControl.Monitoring/Infrastructure/WebApi/XParticularVersionHttpHandler.cs b/src/ServiceControl.Monitoring/Infrastructure/WebApi/XParticularVersionHttpHandler.cs index 68da595a12..250a6d12f4 100644 --- a/src/ServiceControl.Monitoring/Infrastructure/WebApi/XParticularVersionHttpHandler.cs +++ b/src/ServiceControl.Monitoring/Infrastructure/WebApi/XParticularVersionHttpHandler.cs @@ -14,7 +14,7 @@ static XParticularVersionHttpHandler() protected override async Task SendAsync(HttpRequestMessage request, CancellationToken cancellationToken = default) { - var response = await base.SendAsync(request, cancellationToken).ConfigureAwait(false); + var response = await base.SendAsync(request, cancellationToken); response.Headers.Add("X-Particular-Version", FileVersion); diff --git a/src/ServiceControl.Monitoring/Program.cs b/src/ServiceControl.Monitoring/Program.cs index e73d339dbd..675dc3134b 100644 --- a/src/ServiceControl.Monitoring/Program.cs +++ b/src/ServiceControl.Monitoring/Program.cs @@ -24,8 +24,7 @@ static async Task Main(string[] args) LoggingConfigurator.Configure(settings, !runAsWindowsService); await new CommandRunner(arguments.Commands) - .Run(settings) - .ConfigureAwait(false); + .Run(settings); } static void LoadSettings(HostArguments args) diff --git a/src/ServiceControl.MultiInstance.AcceptanceTests/AcceptanceTest.cs b/src/ServiceControl.MultiInstance.AcceptanceTests/AcceptanceTest.cs index 9624e03dae..c83cffbe63 100644 --- a/src/ServiceControl.MultiInstance.AcceptanceTests/AcceptanceTest.cs +++ b/src/ServiceControl.MultiInstance.AcceptanceTests/AcceptanceTest.cs @@ -37,7 +37,7 @@ protected AcceptanceTest() public Dictionary SettingsPerInstance => serviceControlRunnerBehavior.SettingsPerInstance; [OneTimeSetUp] - public void OneTimeSetup() + public static void OneTimeSetup() { Scenario.GetLoggerFactory = ctx => new StaticLoggerFactory(ctx); } diff --git a/src/ServiceControl.MultiInstance.AcceptanceTests/Monitoring/When_a_message_is_imported_twice.cs b/src/ServiceControl.MultiInstance.AcceptanceTests/Monitoring/When_a_message_is_imported_twice.cs index 3b48f1a150..5805763ecd 100644 --- a/src/ServiceControl.MultiInstance.AcceptanceTests/Monitoring/When_a_message_is_imported_twice.cs +++ b/src/ServiceControl.MultiInstance.AcceptanceTests/Monitoring/When_a_message_is_imported_twice.cs @@ -64,8 +64,8 @@ class DuplicateAuditsBehavior : Behavior { public override async Task Invoke(IAuditContext context, Func next) { - await next().ConfigureAwait(false); - await next().ConfigureAwait(false); + await next(); + await next(); } } diff --git a/src/ServiceControl.MultiInstance.AcceptanceTests/Recoverability/When_a_message_retry_audit_from_old_sc_is_sent_to_audit_instance.cs b/src/ServiceControl.MultiInstance.AcceptanceTests/Recoverability/When_a_message_retry_audit_from_old_sc_is_sent_to_audit_instance.cs index 6ebe88a082..34f4d29da9 100644 --- a/src/ServiceControl.MultiInstance.AcceptanceTests/Recoverability/When_a_message_retry_audit_from_old_sc_is_sent_to_audit_instance.cs +++ b/src/ServiceControl.MultiInstance.AcceptanceTests/Recoverability/When_a_message_retry_audit_from_old_sc_is_sent_to_audit_instance.cs @@ -25,6 +25,9 @@ await Define() .WithEndpoint(b => b.When(session => session.SendLocal(new MyMessage())).DoNotFailOnErrorMessages()) .Done(async c => { + // Give the other parts of the system a chance to work + await Task.Delay(500); + var result = await GetFailedMessage(c); failure = result; if (!result) diff --git a/src/ServiceControl.MultiInstance.AcceptanceTests/SagaAudit/When_sending_saga_audit_to_main_instance.cs b/src/ServiceControl.MultiInstance.AcceptanceTests/SagaAudit/When_sending_saga_audit_to_main_instance.cs index 2faf69dcee..b82b347f40 100644 --- a/src/ServiceControl.MultiInstance.AcceptanceTests/SagaAudit/When_sending_saga_audit_to_main_instance.cs +++ b/src/ServiceControl.MultiInstance.AcceptanceTests/SagaAudit/When_sending_saga_audit_to_main_instance.cs @@ -5,12 +5,9 @@ using System.Linq; using System.Threading.Tasks; using AcceptanceTesting; - using EventLog; using NServiceBus; using NServiceBus.AcceptanceTesting; using NUnit.Framework; - using Raven.Client; - using ServiceBus.Management.Infrastructure.Settings; using ServiceControl.SagaAudit; using TestSupport; using TestSupport.EndpointTemplates; @@ -83,56 +80,6 @@ public async Task Saga_history_can_be_fetched_from_main_instance() Assert.AreEqual("Send", sagaStateChange.InitiatingMessage.Intent); } - [Test] - public async Task - Check_fails_if_no_audit_retention_is_configured() - { - //Ensure custom checks are enabled - CustomServiceControlSettings = settings => - { - settings.DisableHealthChecks = false; - }; - - //Override the configuration of the check in the container in order to make it run more frequently for testing purposes. - CustomEndpointConfiguration = config => - { - config.RegisterComponents(registration => - { - registration.ConfigureComponent((builder) => new AuditRetentionCustomCheck(builder.Build(), builder.Build(), TimeSpan.FromSeconds(10)), DependencyLifecycle.SingleInstance); - }); - }; - - SingleResult customCheckEventEntry = default; - bool sagaAudiDataInMainInstanceIsAvailableForQuery = false; - - await Define() - .WithEndpoint(b => b.When((bus, c) => bus.SendLocal(new MessageInitiatingSaga { Id = "Id" }))) - .Done(async c => - { - if (!c.SagaId.HasValue) - { - return false; - } - - if (sagaAudiDataInMainInstanceIsAvailableForQuery == false) - { - var sagaData = - await this.TryGet($"/api/sagas/{c.SagaId}", instanceName: ServiceControlInstanceName); - sagaAudiDataInMainInstanceIsAvailableForQuery = sagaData.HasResult; - return false; - } - - customCheckEventEntry = await this.TryGetSingle("/api/eventlogitems/", - e => e.EventType == "CustomCheckFailed" && e.Description.StartsWith("Saga Audit Data Retention")); - - return customCheckEventEntry; - }) - .Run(); - - Assert.IsTrue(customCheckEventEntry.Item.RelatedTo.Any(item => item == "/customcheck/Saga Audit Data Retention"), "Event log entry should be related to the Saga Audit Data Retention"); - Assert.IsTrue(customCheckEventEntry.Item.RelatedTo.Any(item => item.StartsWith("/endpoint/Particular.ServiceControl")), "Event log entry should be related to the ServiceControl endpoint"); - } - public class SagaEndpoint : EndpointConfigurationBuilder { public SagaEndpoint() diff --git a/src/ServiceControl.MultiInstance.AcceptanceTests/ServiceControl.MultiInstance.AcceptanceTests.csproj b/src/ServiceControl.MultiInstance.AcceptanceTests/ServiceControl.MultiInstance.AcceptanceTests.csproj index 4369c07f9f..3fe2bed049 100644 --- a/src/ServiceControl.MultiInstance.AcceptanceTests/ServiceControl.MultiInstance.AcceptanceTests.csproj +++ b/src/ServiceControl.MultiInstance.AcceptanceTests/ServiceControl.MultiInstance.AcceptanceTests.csproj @@ -10,9 +10,7 @@ - - diff --git a/src/ServiceControl.MultiInstance.AcceptanceTests/TestSupport/ServiceControlComponentBehavior.cs b/src/ServiceControl.MultiInstance.AcceptanceTests/TestSupport/ServiceControlComponentBehavior.cs index 95a0987a50..814b2d236f 100644 --- a/src/ServiceControl.MultiInstance.AcceptanceTests/TestSupport/ServiceControlComponentBehavior.cs +++ b/src/ServiceControl.MultiInstance.AcceptanceTests/TestSupport/ServiceControlComponentBehavior.cs @@ -29,7 +29,7 @@ public ServiceControlComponentBehavior(ITransportIntegration transportToUse, Dat public async Task CreateRunner(RunDescriptor run) { runner = new ServiceControlComponentRunner(transportIntegration, dataStoreConfiguration, customEndpointConfiguration, customAuditEndpointConfiguration, customServiceControlSettings, customServiceControlAuditSettings); - await runner.Initialize(run).ConfigureAwait(false); + await runner.Initialize(run); return runner; } diff --git a/src/ServiceControl.MultiInstance.AcceptanceTests/TestSupport/ServiceControlComponentRunner.cs b/src/ServiceControl.MultiInstance.AcceptanceTests/TestSupport/ServiceControlComponentRunner.cs index dd89cc79ce..993bb7da20 100644 --- a/src/ServiceControl.MultiInstance.AcceptanceTests/TestSupport/ServiceControlComponentRunner.cs +++ b/src/ServiceControl.MultiInstance.AcceptanceTests/TestSupport/ServiceControlComponentRunner.cs @@ -52,8 +52,8 @@ public async Task Initialize(RunDescriptor run) var mainInstanceDbPort = FindAvailablePort(mainInstancePort + 1); var auditInstancePort = FindAvailablePort(mainInstanceDbPort + 1); - await InitializeServiceControlAudit(run.ScenarioContext, auditInstancePort).ConfigureAwait(false); - await InitializeServiceControl(run.ScenarioContext, mainInstancePort, mainInstanceDbPort, auditInstancePort).ConfigureAwait(false); + await InitializeServiceControlAudit(run.ScenarioContext, auditInstancePort); + await InitializeServiceControl(run.ScenarioContext, mainInstancePort, mainInstanceDbPort, auditInstancePort); } static int FindAvailablePort(int startPort) @@ -80,14 +80,16 @@ async Task InitializeServiceControl(ScenarioContext context, int instancePort, i typeof(ScenarioContext).GetProperty("CurrentEndpoint", BindingFlags.Static | BindingFlags.NonPublic)?.SetValue(context, instanceName); ConfigurationManager.AppSettings.Set("ServiceControl/TransportType", transportToUse.TypeName); - ConfigurationManager.AppSettings.Set("ServiceControl/SqlStorageConnectionString", dataStoreConfiguration.ConnectionString); - var settings = new Settings(instanceName) + var settings = new Settings(instanceName, transportToUse.TypeName, dataStoreConfiguration.DataStoreTypeName) { - DataStoreType = (DataStoreType)Enum.Parse(typeof(DataStoreType), dataStoreConfiguration.DataStoreTypeName), Port = instancePort, - DatabaseMaintenancePort = maintenancePort, - DbPath = Path.Combine(Path.GetTempPath(), Path.GetRandomFileName()), + PersisterSpecificSettings = new Dictionary() + { + { "HostName", "localhost" }, + { "RavenDB35/RunInMemory", "true" }, + { "DatabaseMaintenancePort", maintenancePort.ToString() } + }, ForwardErrorMessages = false, TransportType = transportToUse.TypeName, TransportConnectionString = transportToUse.ConnectionString, @@ -95,7 +97,6 @@ async Task InitializeServiceControl(ScenarioContext context, int instancePort, i TimeToRestartErrorIngestionAfterFailure = TimeSpan.FromSeconds(2), MaximumConcurrencyLevel = 2, HttpDefaultConnectionLimit = int.MaxValue, - RunInMemory = true, DisableHealthChecks = true, ExposeApi = false, RemoteInstances = new[] @@ -155,7 +156,10 @@ async Task InitializeServiceControl(ScenarioContext context, int instancePort, i // This is a hack to ensure ServiceControl picks the correct type for the messages that come from plugins otherwise we pick the type from the plugins assembly and that is not the type we want, we need to pick the type from ServiceControl assembly. // This is needed because we no longer use the AppDomain separation. - configuration.RegisterComponents(r => { configuration.GetSettings().Set("SC.ConfigureComponent", r); }); + configuration.RegisterComponents(r => + { + configuration.GetSettings().Set("SC.ConfigureComponent", r); + }); configuration.RegisterComponents(r => { @@ -190,7 +194,7 @@ async Task InitializeServiceControl(ScenarioContext context, int instancePort, i }; host = bootstrapper.HostBuilder.Build(); - await host.StartAsync().ConfigureAwait(false); + await host.StartAsync(); hosts[instanceName] = host; } @@ -263,8 +267,9 @@ async Task InitializeServiceControlAudit(ScenarioContext context, int instancePo var excludedAssemblies = new[] { - Path.GetFileName(typeof(Settings).Assembly.CodeBase), - typeof(ServiceControlComponentRunner).Assembly.GetName().Name + Path.GetFileName(typeof(Settings).Assembly.CodeBase), // ServiceControl.exe + "ServiceControl.Persistence.RavenDb.dll", + typeof(ServiceControlComponentRunner).Assembly.GetName().Name // This project }; customServiceControlAuditSettings(settings); @@ -327,7 +332,7 @@ async Task InitializeServiceControlAudit(ScenarioContext context, int instancePo host = bootstrapper.HostBuilder.Build(); - await host.StartAsync().ConfigureAwait(false); + await host.StartAsync(); hosts[instanceName] = host; } @@ -363,7 +368,7 @@ public override async Task Stop() { if (hosts.ContainsKey(instanceName)) { - await hosts[instanceName].StopAsync().ConfigureAwait(false); + await hosts[instanceName].StopAsync(); } HttpClients[instanceName].Dispose(); handlers[instanceName].Dispose(); diff --git a/src/ServiceControl.Persistence.InMemory/.editorconfig b/src/ServiceControl.Persistence.InMemory/.editorconfig new file mode 100644 index 0000000000..ff993b49bb --- /dev/null +++ b/src/ServiceControl.Persistence.InMemory/.editorconfig @@ -0,0 +1,4 @@ +[*.cs] + +# Justification: ServiceControl app has no synchronization context +dotnet_diagnostic.CA2007.severity = none diff --git a/src/ServiceControl.Persistence.InMemory/InMemoryCustomCheckDataStore.cs b/src/ServiceControl.Persistence.InMemory/InMemoryCustomCheckDataStore.cs index 5fc08a7379..4f31cc56e9 100644 --- a/src/ServiceControl.Persistence.InMemory/InMemoryCustomCheckDataStore.cs +++ b/src/ServiceControl.Persistence.InMemory/InMemoryCustomCheckDataStore.cs @@ -47,7 +47,7 @@ public Task>> GetStats(PagingInfo paging, string }) .ToList(); - var stats = new QueryStatsInfo("", storage.Count); + var stats = new QueryStatsInfo("", storage.Count, false); return Task.FromResult(new QueryResult>(result, stats)); } @@ -62,6 +62,8 @@ public Task DeleteCustomCheck(Guid id) return Task.CompletedTask; } + public Task GetNumberOfFailedChecks() => Task.FromResult(storage.Count(x => x.Value.HasFailed)); + Dictionary storage = new Dictionary(); } } \ No newline at end of file diff --git a/src/ServiceControl.Persistence.InMemory/InMemoryPersistenceConfiguration.cs b/src/ServiceControl.Persistence.InMemory/InMemoryPersistenceConfiguration.cs index 162eec6145..a30f58f5a4 100644 --- a/src/ServiceControl.Persistence.InMemory/InMemoryPersistenceConfiguration.cs +++ b/src/ServiceControl.Persistence.InMemory/InMemoryPersistenceConfiguration.cs @@ -1,5 +1,6 @@ namespace ServiceControl.Persistence.InMemory { + using System.Collections.Generic; using Microsoft.Extensions.DependencyInjection; using ServiceControl.Persistence.UnitOfWork; @@ -11,5 +12,9 @@ public void ConfigureServices(IServiceCollection serviceCollection) serviceCollection.AddSingleton(); serviceCollection.AddPartialUnitOfWorkFactory(); } + + public string Name { get; } + public IEnumerable ConfigurationKeys { get; } + public IPersistence Create(PersistenceSettings settings) => throw new System.NotImplementedException(); } } diff --git a/src/ServiceControl.Persistence.InMemory/InMemorySagaAuditDataStore.cs b/src/ServiceControl.Persistence.InMemory/InMemorySagaAuditDataStore.cs new file mode 100644 index 0000000000..17f7421604 --- /dev/null +++ b/src/ServiceControl.Persistence.InMemory/InMemorySagaAuditDataStore.cs @@ -0,0 +1,56 @@ +namespace ServiceControl.Persistence.InMemory +{ + using System; + using System.Collections.Concurrent; + using System.Linq; + using System.Threading.Tasks; + using ServiceControl.Persistence.Infrastructure; + using ServiceControl.SagaAudit; + + class InMemorySagaAuditDataStore : ISagaAuditDataStore + { + ConcurrentDictionary snapshotStorage = new ConcurrentDictionary(); + + public Task> GetSagaById(Guid sagaId) + { + var snapshots = snapshotStorage.Values.Where(snapshot => snapshot.SagaId == sagaId); + + var results = from result in snapshots + group result by result.SagaId into g + let first = g.First() + select new SagaHistory + { + Id = first.SagaId, + SagaId = first.SagaId, + SagaType = first.SagaType, + Changes = (from doc in g + select new SagaStateChange + { + Endpoint = doc.Endpoint, + FinishTime = doc.FinishTime, + InitiatingMessage = doc.InitiatingMessage, + OutgoingMessages = doc.OutgoingMessages, + StartTime = doc.StartTime, + StateAfterChange = doc.StateAfterChange, + Status = doc.Status + }) + .OrderByDescending(x => x.FinishTime) + .ToList() + }; + + var sagaHistory = results.FirstOrDefault(); + if (sagaHistory == null) + { + return Task.FromResult(QueryResult.Empty()); + } + + return Task.FromResult(new QueryResult(sagaHistory, new QueryStatsInfo())); + } + + public Task StoreSnapshot(SagaSnapshot sagaSnapshot) + { + snapshotStorage[sagaSnapshot.Id] = sagaSnapshot; + return Task.CompletedTask; + } + } +} diff --git a/src/ServiceControl.Persistence.InMemory/NoOpFailureReclassifier.cs b/src/ServiceControl.Persistence.InMemory/NoOpFailureReclassifier.cs new file mode 100644 index 0000000000..195f55f834 --- /dev/null +++ b/src/ServiceControl.Persistence.InMemory/NoOpFailureReclassifier.cs @@ -0,0 +1,9 @@ +namespace ServiceControl.Persistence.InMemory +{ + using System.Threading.Tasks; + + class NoOpFailureReclassifier : IReclassifyFailedMessages + { + public Task ReclassifyFailedMessages(bool force) => Task.FromResult(0); + } +} diff --git a/src/ServiceControl.Persistence.InMemory/ServiceControl.Persistence.InMemory.csproj b/src/ServiceControl.Persistence.InMemory/ServiceControl.Persistence.InMemory.csproj index e92ba1b6af..be9d9f606f 100644 --- a/src/ServiceControl.Persistence.InMemory/ServiceControl.Persistence.InMemory.csproj +++ b/src/ServiceControl.Persistence.InMemory/ServiceControl.Persistence.InMemory.csproj @@ -8,4 +8,8 @@ + + + + \ No newline at end of file diff --git a/src/ServiceControl.Persistence.InMemory/UnitOfWork/InMemoryIngestionUnitOfWork.cs b/src/ServiceControl.Persistence.InMemory/UnitOfWork/InMemoryIngestionUnitOfWork.cs index 95c10d4f7c..432db928bb 100644 --- a/src/ServiceControl.Persistence.InMemory/UnitOfWork/InMemoryIngestionUnitOfWork.cs +++ b/src/ServiceControl.Persistence.InMemory/UnitOfWork/InMemoryIngestionUnitOfWork.cs @@ -19,8 +19,7 @@ public override async Task Complete() { foreach (var endpoint in knownEndpoints) { - await dataStore.CreateIfNotExists(endpoint.EndpointDetails) - .ConfigureAwait(false); + await dataStore.CreateIfNotExists(endpoint.EndpointDetails); } } diff --git a/src/ServiceControl.Persistence.RavenDb/.editorconfig b/src/ServiceControl.Persistence.RavenDb/.editorconfig new file mode 100644 index 0000000000..ff993b49bb --- /dev/null +++ b/src/ServiceControl.Persistence.RavenDb/.editorconfig @@ -0,0 +1,4 @@ +[*.cs] + +# Justification: ServiceControl app has no synchronization context +dotnet_diagnostic.CA2007.severity = none diff --git a/src/ServiceControl.Persistence.RavenDb/BulkRetry/FailedMessages_UniqueMessageIdAndTimeOfFailures.cs b/src/ServiceControl.Persistence.RavenDb/BulkRetry/FailedMessages_UniqueMessageIdAndTimeOfFailures.cs new file mode 100644 index 0000000000..ba15cb1158 --- /dev/null +++ b/src/ServiceControl.Persistence.RavenDb/BulkRetry/FailedMessages_UniqueMessageIdAndTimeOfFailures.cs @@ -0,0 +1,27 @@ +namespace ServiceControl.Recoverability +{ + using System; + using System.Linq; + using MessageFailures; + using Raven.Client.Indexes; + + class FailedMessages_UniqueMessageIdAndTimeOfFailures : AbstractTransformerCreationTask + { + public FailedMessages_UniqueMessageIdAndTimeOfFailures() + { + TransformResults = failedMessages => from failedMessage in failedMessages + select new + { + failedMessage.UniqueMessageId, + LatestTimeOfFailure = failedMessage.ProcessingAttempts.Max(x => x.FailureDetails.TimeOfFailure) + }; + } + + public struct Result + { + public string UniqueMessageId { get; set; } + + public DateTime LatestTimeOfFailure { get; set; } + } + } +} \ No newline at end of file diff --git a/src/ServiceControl.Persistence.RavenDb/Chunker.cs b/src/ServiceControl.Persistence.RavenDb/Chunker.cs index a1a3c64dd7..bae4cb0da8 100644 --- a/src/ServiceControl.Persistence.RavenDb/Chunker.cs +++ b/src/ServiceControl.Persistence.RavenDb/Chunker.cs @@ -3,7 +3,7 @@ using System; using System.Threading; - public static class Chunker + static class Chunker { public static int ExecuteInChunks(int total, Func action, T1 t1, T2 t2, CancellationToken cancellationToken = default) { diff --git a/src/ServiceControl/Operations/CheckFreeDiskSpace.cs b/src/ServiceControl.Persistence.RavenDb/CustomChecks/CheckFreeDiskSpace.cs similarity index 52% rename from src/ServiceControl/Operations/CheckFreeDiskSpace.cs rename to src/ServiceControl.Persistence.RavenDb/CustomChecks/CheckFreeDiskSpace.cs index 1d2167304a..da9de3e68d 100644 --- a/src/ServiceControl/Operations/CheckFreeDiskSpace.cs +++ b/src/ServiceControl.Persistence.RavenDb/CustomChecks/CheckFreeDiskSpace.cs @@ -5,14 +5,16 @@ using System.Threading.Tasks; using NServiceBus.CustomChecks; using NServiceBus.Logging; - using ServiceBus.Management.Infrastructure.Settings; + using Persistence; + using Persistence.RavenDb; class CheckFreeDiskSpace : CustomCheck { - public CheckFreeDiskSpace(Settings settings) : base("ServiceControl database", "Storage space", TimeSpan.FromMinutes(5)) + public CheckFreeDiskSpace(PersistenceSettings settings) : base("ServiceControl database", "Storage space", TimeSpan.FromMinutes(5)) { - dataPath = settings.DbPath; - percentageThreshold = settings.DataSpaceRemainingThreshold / 100m; + dataPath = settings.PersisterSpecificSettings[RavenDbPersistenceConfiguration.DbPathKey]; + percentageThreshold = GetDataSpaceRemainingThreshold(settings) / 100m; + Logger.Debug($"Check ServiceControl data drive space remaining custom check starting. Threshold {percentageThreshold:P0}"); } @@ -41,8 +43,37 @@ public override Task PerformCheck() : CheckResult.Failed($"{percentRemaining:P0} disk space remaining on data drive '{dataDriveInfo.VolumeLabel} ({dataDriveInfo.RootDirectory})' on '{Environment.MachineName}'."); } + int GetDataSpaceRemainingThreshold(PersistenceSettings settings) + { + var threshold = DataSpaceRemainingThresholdDefault; + + if (settings.PersisterSpecificSettings.TryGetValue(RavenDbPersistenceConfiguration.DataSpaceRemainingThresholdKey, out var thresholdValue)) + { + threshold = int.Parse(thresholdValue); + } + string message; + + if (threshold < 0) + { + message = $"{RavenDbPersistenceConfiguration.DataSpaceRemainingThresholdKey} is invalid, minimum value is 0."; + Logger.Fatal(message); + throw new Exception(message); + } + + if (threshold > 100) + { + message = $"{RavenDbPersistenceConfiguration.DataSpaceRemainingThresholdKey} is invalid, maximum value is 100."; + Logger.Fatal(message); + throw new Exception(message); + } + + return threshold; + } + readonly string dataPath; - decimal percentageThreshold; + readonly decimal percentageThreshold; + + const int DataSpaceRemainingThresholdDefault = 20; static readonly ILog Logger = LogManager.GetLogger(typeof(CheckFreeDiskSpace)); } } \ No newline at end of file diff --git a/src/ServiceControl/Operations/CheckMinimumStorageRequiredForIngestion.cs b/src/ServiceControl.Persistence.RavenDb/CustomChecks/CheckMinimumStorageRequiredForIngestion.cs similarity index 58% rename from src/ServiceControl/Operations/CheckMinimumStorageRequiredForIngestion.cs rename to src/ServiceControl.Persistence.RavenDb/CustomChecks/CheckMinimumStorageRequiredForIngestion.cs index 2505e65aa9..c87cf2066b 100644 --- a/src/ServiceControl/Operations/CheckMinimumStorageRequiredForIngestion.cs +++ b/src/ServiceControl.Persistence.RavenDb/CustomChecks/CheckMinimumStorageRequiredForIngestion.cs @@ -5,23 +5,24 @@ using System.Threading.Tasks; using NServiceBus.CustomChecks; using NServiceBus.Logging; - using ServiceBus.Management.Infrastructure.Settings; + using Persistence.RavenDb; using ServiceControl.Persistence; class CheckMinimumStorageRequiredForIngestion : CustomCheck { - public CheckMinimumStorageRequiredForIngestion(MinimumRequiredStorageState stateHolder, Settings settings) + public CheckMinimumStorageRequiredForIngestion(MinimumRequiredStorageState stateHolder, PersistenceSettings settings) : base("Message Ingestion Process", "ServiceControl Health", TimeSpan.FromSeconds(5)) { this.stateHolder = stateHolder; this.settings = settings; + + dataPathRoot = Path.GetPathRoot(settings.PersisterSpecificSettings[RavenDbPersistenceConfiguration.DbPathKey]); } public override Task PerformCheck() { - var percentageThreshold = settings.MinimumStorageLeftRequiredForIngestion / 100m; + percentageThreshold = GetMinimumStorageLeftRequiredForIngestion(settings) / 100m; - var dataPathRoot = Path.GetPathRoot(settings.DbPath); if (dataPathRoot == null) { stateHolder.CanIngestMore = true; @@ -47,14 +48,47 @@ public override Task PerformCheck() return successResult; } - var message = $"Error message ingestion stopped! {percentRemaining:P0} disk space remaining on data drive '{dataDriveInfo.VolumeLabel} ({dataDriveInfo.RootDirectory})' on '{Environment.MachineName}'. This is less than {percentageThreshold}% - the minimal required space configured. The threshold can be set using the {nameof(Settings.MinimumStorageLeftRequiredForIngestion)} configuration setting."; + var message = $"Error message ingestion stopped! {percentRemaining:P0} disk space remaining on data drive '{dataDriveInfo.VolumeLabel} ({dataDriveInfo.RootDirectory})' on '{Environment.MachineName}'. This is less than {percentageThreshold}% - the minimal required space configured. The threshold can be set using the {RavenBootstrapper.MinimumStorageLeftRequiredForIngestionKey} configuration setting."; Logger.Warn(message); stateHolder.CanIngestMore = false; return CheckResult.Failed(message); } + int GetMinimumStorageLeftRequiredForIngestion(PersistenceSettings settings) + { + int threshold = MinimumStorageLeftRequiredForIngestionDefault; + + if (settings.PersisterSpecificSettings.TryGetValue(RavenBootstrapper.MinimumStorageLeftRequiredForIngestionKey, out var thresholdValue)) + { + threshold = int.Parse(thresholdValue); + } + + string message; + if (threshold < 0) + { + message = $"{RavenBootstrapper.MinimumStorageLeftRequiredForIngestionKey} is invalid, minimum value is 0."; + Logger.Fatal(message); + throw new Exception(message); + } + + if (threshold > 100) + { + message = $"{RavenBootstrapper.MinimumStorageLeftRequiredForIngestionKey} is invalid, maximum value is 100."; + Logger.Fatal(message); + throw new Exception(message); + } + + return threshold; + } + + const int MinimumStorageLeftRequiredForIngestionDefault = 5; + readonly MinimumRequiredStorageState stateHolder; - readonly Settings settings; + readonly PersistenceSettings settings; + readonly string dataPathRoot; + + decimal percentageThreshold; + static Task successResult = Task.FromResult(CheckResult.Pass); static readonly ILog Logger = LogManager.GetLogger(typeof(CheckMinimumStorageRequiredForIngestion)); } diff --git a/src/ServiceControl/Operations/CheckRavenDBIndexErrors.cs b/src/ServiceControl.Persistence.RavenDb/CustomChecks/CheckRavenDBIndexErrors.cs similarity index 85% rename from src/ServiceControl/Operations/CheckRavenDBIndexErrors.cs rename to src/ServiceControl.Persistence.RavenDb/CustomChecks/CheckRavenDBIndexErrors.cs index 83f0e39c05..27f038687d 100644 --- a/src/ServiceControl/Operations/CheckRavenDBIndexErrors.cs +++ b/src/ServiceControl.Persistence.RavenDb/CustomChecks/CheckRavenDBIndexErrors.cs @@ -13,12 +13,12 @@ class CheckRavenDBIndexErrors : CustomCheck public CheckRavenDBIndexErrors(IDocumentStore store) : base("Error Database Index Errors", "ServiceControl Health", TimeSpan.FromMinutes(5)) { - _store = store; + this.store = store; } public override Task PerformCheck() { - var statistics = _store.DatabaseCommands.GetStatistics(); + var statistics = store.DatabaseCommands.GetStatistics(); var indexes = statistics.Indexes.OrderBy(x => x.Name).ToArray(); if (statistics.Errors.Length == 0) @@ -37,11 +37,12 @@ public override Task PerformCheck() text.AppendLine().AppendLine("See: https://docs.particular.net/search?q=servicecontrol+troubleshooting"); var message = text.ToString(); - _log.Error(message); + Logger.Error(message); return CheckResult.Failed(message); } - static ILog _log = LogManager.GetLogger(); - IDocumentStore _store; + static ILog Logger = LogManager.GetLogger(); + + IDocumentStore store; } } diff --git a/src/ServiceControl/Operations/CheckRavenDBIndexLag.cs b/src/ServiceControl.Persistence.RavenDb/CustomChecks/CheckRavenDBIndexLag.cs similarity index 84% rename from src/ServiceControl/Operations/CheckRavenDBIndexLag.cs rename to src/ServiceControl.Persistence.RavenDb/CustomChecks/CheckRavenDBIndexLag.cs index 03ed388545..ebec2d73e6 100644 --- a/src/ServiceControl/Operations/CheckRavenDBIndexLag.cs +++ b/src/ServiceControl.Persistence.RavenDb/CustomChecks/CheckRavenDBIndexLag.cs @@ -6,22 +6,25 @@ using System.Threading.Tasks; using NServiceBus.CustomChecks; using NServiceBus.Logging; + using Persistence; + using Persistence.RavenDb; using Raven.Client; using Raven.Abstractions.Data; - using ServiceBus.Management.Infrastructure.Settings; + using Raven.Abstractions.Extensions; + using CustomCheck = NServiceBus.CustomChecks.CustomCheck; class CheckRavenDBIndexLag : CustomCheck { - public CheckRavenDBIndexLag(IDocumentStore store, LoggingSettings settings) + public CheckRavenDBIndexLag(IDocumentStore store, PersistenceSettings settings) : base("Error Database Index Lag", "ServiceControl Health", TimeSpan.FromMinutes(5)) { - _store = store; - LogPath = settings?.LogPath; + this.store = store; + this.settings = settings; } public override Task PerformCheck() { - var statistics = _store.DatabaseCommands.GetStatistics(); + var statistics = store.DatabaseCommands.GetStatistics(); var indexes = statistics.Indexes.OrderBy(x => x.Name).ToArray(); CreateDiagnosticsLogEntry(statistics, indexes); @@ -30,7 +33,9 @@ public override Task PerformCheck() if (indexCountWithTooMuchLag > 0) { - return CheckResult.Failed($"At least one index significantly stale. Please run maintenance mode if this custom check persists to ensure index(es) can recover. See log file in `{LogPath}` for more details. Visit https://docs.particular.net/search?q=servicecontrol+troubleshooting for more information."); + var logPath = settings.PersisterSpecificSettings.GetOrDefault(RavenDbPersistenceConfiguration.LogPathKey); + + return CheckResult.Failed($"At least one index significantly stale. Please run maintenance mode if this custom check persists to ensure index(es) can recover. See log file in `{logPath}` for more details. Visit https://docs.particular.net/search?q=servicecontrol+troubleshooting for more information."); } return CheckResult.Pass; @@ -84,7 +89,8 @@ static void CreateDiagnosticsLogEntry(DatabaseStatistics statistics, IndexStats[ const int IndexLagThresholdWarning = 10000; const int IndexLagThresholdError = 100000; static ILog _log = LogManager.GetLogger(); - IDocumentStore _store; - string LogPath; + + IDocumentStore store; + PersistenceSettings settings; } } \ No newline at end of file diff --git a/src/ServiceControl/Operations/FailedAuditImportCustomCheck.cs b/src/ServiceControl.Persistence.RavenDb/CustomChecks/FailedAuditImportCustomCheck.cs similarity index 93% rename from src/ServiceControl/Operations/FailedAuditImportCustomCheck.cs rename to src/ServiceControl.Persistence.RavenDb/CustomChecks/FailedAuditImportCustomCheck.cs index d41c950c1a..bada097bf3 100644 --- a/src/ServiceControl/Operations/FailedAuditImportCustomCheck.cs +++ b/src/ServiceControl.Persistence.RavenDb/CustomChecks/FailedAuditImportCustomCheck.cs @@ -20,9 +20,9 @@ public override async Task PerformCheck() { var query = session.Query(); using (var ie = await session.Advanced.StreamAsync(query) - .ConfigureAwait(false)) + ) { - if (await ie.MoveNextAsync().ConfigureAwait(false)) + if (await ie.MoveNextAsync()) { Logger.Warn(message); return CheckResult.Failed(message); diff --git a/src/ServiceControl.Persistence.RavenDb/DocumentIdGenerators/FailedMessageIdGenerator.cs b/src/ServiceControl.Persistence.RavenDb/DocumentIdGenerators/FailedMessageIdGenerator.cs new file mode 100644 index 0000000000..06f242dfd4 --- /dev/null +++ b/src/ServiceControl.Persistence.RavenDb/DocumentIdGenerators/FailedMessageIdGenerator.cs @@ -0,0 +1,14 @@ +static class FailedMessageIdGenerator +{ + public const string CollectionName = "FailedMessages"; + + //[Obsolete("Use Guid.Parse")] TODO: As these are all guids... we don't need these generators.. Unless these are MessageIdentifiers as these can have any string value + public static string MakeDocumentId(string messageUniqueId) + { + Guard.Assert(!HasPrefix(messageUniqueId), $"value has {CollectionName}/ prefix"); // TODO: Could potentially be removed when all tests are green but no harm as its only included on Debug builds + return $"{CollectionName}/{messageUniqueId}"; + } + + public static string GetMessageIdFromDocumentId(string failedMessageDocumentId) => failedMessageDocumentId.Substring(CollectionName.Length + 1); + static bool HasPrefix(string value) => value.StartsWith(CollectionName + "/"); +} \ No newline at end of file diff --git a/src/ServiceControl.Persistence.RavenDb/DocumentIdGenerators/MessageBodyIdGenerator.cs b/src/ServiceControl.Persistence.RavenDb/DocumentIdGenerators/MessageBodyIdGenerator.cs new file mode 100644 index 0000000000..3c693e749a --- /dev/null +++ b/src/ServiceControl.Persistence.RavenDb/DocumentIdGenerators/MessageBodyIdGenerator.cs @@ -0,0 +1,12 @@ +static class MessageBodyIdGenerator +{ + const string CollectionName = "messagebodies"; + + public static string MakeDocumentId(string messageUniqueId) + { + Guard.Assert(!HasPrefix(messageUniqueId), $"value has {CollectionName}/ prefix"); // TODO: Could potentially be removed when all tests are green but no harm as its only included on Debug builds + return $"{CollectionName}/{messageUniqueId}"; + } + + static bool HasPrefix(string value) => value.StartsWith(CollectionName + "/"); +} \ No newline at end of file diff --git a/src/ServiceControl.Persistence.RavenDb/Editing/EditFailedMessageManager.cs b/src/ServiceControl.Persistence.RavenDb/Editing/EditFailedMessageManager.cs new file mode 100644 index 0000000000..3664c36fb8 --- /dev/null +++ b/src/ServiceControl.Persistence.RavenDb/Editing/EditFailedMessageManager.cs @@ -0,0 +1,54 @@ +namespace ServiceControl.Persistence.RavenDb +{ + using System; + using System.Threading.Tasks; + using Raven.Abstractions.Data; + using Raven.Client; + using ServiceControl.MessageFailures; + using ServiceControl.Persistence.Recoverability.Editing; + + class EditFailedMessageManager : AbstractSessionManager, IEditFailedMessagesManager + { + readonly IAsyncDocumentSession session; + FailedMessage failedMessage; + + public EditFailedMessageManager(IAsyncDocumentSession session) + : base(session) + { + this.session = session; + } + + public async Task GetFailedMessage(string failedMessageId) + { + failedMessage = await session.LoadAsync(FailedMessageIdGenerator.MakeDocumentId(failedMessageId)); + return failedMessage; + } + + public async Task GetCurrentEditingMessageId(string failedMessageId) + { + var edit = await session.LoadAsync(FailedMessageEdit.MakeDocumentId(failedMessageId)); + return edit?.EditId; + } + + public Task SetCurrentEditingMessageId(string editingMessageId) + { + if (failedMessage == null) + { + throw new InvalidOperationException("No failed message loaded"); + } + return session.StoreAsync(new FailedMessageEdit + { + Id = FailedMessageEdit.MakeDocumentId(failedMessage.UniqueMessageId), + FailedMessageId = failedMessage.Id, + EditId = editingMessageId + }, Etag.Empty); + } + + public Task SetFailedMessageAsResolved() + { + // Instance is tracked by the document session + failedMessage.Status = FailedMessageStatus.Resolved; + return Task.CompletedTask; + } + } +} diff --git a/src/ServiceControl/Recoverability/Editing/FailedMessageEdit.cs b/src/ServiceControl.Persistence.RavenDb/Editing/FailedMessageEdit.cs similarity index 85% rename from src/ServiceControl/Recoverability/Editing/FailedMessageEdit.cs rename to src/ServiceControl.Persistence.RavenDb/Editing/FailedMessageEdit.cs index 33f85bec75..2bc0e7a1e0 100644 --- a/src/ServiceControl/Recoverability/Editing/FailedMessageEdit.cs +++ b/src/ServiceControl.Persistence.RavenDb/Editing/FailedMessageEdit.cs @@ -1,4 +1,4 @@ -namespace ServiceControl.Recoverability.Editing +namespace ServiceControl.Persistence.Recoverability.Editing { class FailedMessageEdit { diff --git a/src/ServiceControl.Persistence.RavenDb/Editing/NotificationsManager.cs b/src/ServiceControl.Persistence.RavenDb/Editing/NotificationsManager.cs new file mode 100644 index 0000000000..0a4177e6b9 --- /dev/null +++ b/src/ServiceControl.Persistence.RavenDb/Editing/NotificationsManager.cs @@ -0,0 +1,38 @@ +namespace ServiceControl.Persistence.RavenDb.Editing +{ + using System; + using System.Threading.Tasks; + using Notifications; + using Raven.Client; + + class NotificationsManager : AbstractSessionManager, INotificationsManager + { + static readonly TimeSpan CacheTimeoutDefault = TimeSpan.FromMinutes(5); // TODO: Copied value from SendEmailNotificationHandler.cacheTimeout, Raven requires this to be at least 1 second + + public NotificationsManager(IAsyncDocumentSession session) : base(session) + { + } + + public async Task LoadSettings(TimeSpan? cacheTimeout = null) + { + + using (Session.Advanced.DocumentStore.AggressivelyCacheFor(cacheTimeout ?? CacheTimeoutDefault)) + { + var settings = await Session + .LoadAsync(NotificationsSettings.SingleDocumentId); + + if (settings == null) + { + settings = new NotificationsSettings + { + Id = NotificationsSettings.SingleDocumentId + }; + + await Session.StoreAsync(settings); + } + + return settings; + } + } + } +} \ No newline at end of file diff --git a/src/ServiceControl/Infrastructure/EndpointDetailsParser.cs b/src/ServiceControl.Persistence.RavenDb/EndpointDetailsParser.cs similarity index 100% rename from src/ServiceControl/Infrastructure/EndpointDetailsParser.cs rename to src/ServiceControl.Persistence.RavenDb/EndpointDetailsParser.cs diff --git a/src/ServiceControl.Persistence.RavenDb/ErrorMessagesDataStore.cs b/src/ServiceControl.Persistence.RavenDb/ErrorMessagesDataStore.cs new file mode 100644 index 0000000000..8eb216e327 --- /dev/null +++ b/src/ServiceControl.Persistence.RavenDb/ErrorMessagesDataStore.cs @@ -0,0 +1,740 @@ +namespace ServiceControl.Persistence.RavenDb +{ + using System; + using System.Collections.Generic; + using System.Globalization; + using System.Linq; + using System.Text; + using System.Threading.Tasks; + using CompositeViews.Messages; + using Editing; + using NServiceBus.Logging; + using Raven.Abstractions.Data; + using Raven.Abstractions.Extensions; + using Raven.Client; + using Raven.Client.Linq; + using Raven.Json.Linq; + using ServiceControl.EventLog; + using ServiceControl.MessageFailures; + using ServiceControl.MessageFailures.Api; + using ServiceControl.Operations; + using ServiceControl.Persistence.Infrastructure; + using ServiceControl.Recoverability; + + class ErrorMessagesDataStore : IErrorMessageDataStore + { + readonly IDocumentStore documentStore; + + public ErrorMessagesDataStore(IDocumentStore documentStore) + { + this.documentStore = documentStore; + + } + + public async Task>> GetAllMessages( + PagingInfo pagingInfo, + SortInfo sortInfo, + bool includeSystemMessages + ) + { + using (var session = documentStore.OpenAsyncSession()) + { + var results = await session.Query() + .IncludeSystemMessagesWhere(includeSystemMessages) + .Statistics(out var stats) + .Sort(sortInfo) + .Paging(pagingInfo) + .TransformWith() + .ToListAsync(); + + return new QueryResult>(results, stats.ToQueryStatsInfo()); + } + } + + public async Task>> GetAllMessagesForEndpoint( + string endpointName, + PagingInfo pagingInfo, + SortInfo sortInfo, + bool includeSystemMessages + ) + { + using (var session = documentStore.OpenAsyncSession()) + { + var results = await session.Query() + .IncludeSystemMessagesWhere(includeSystemMessages) + .Where(m => m.ReceivingEndpointName == endpointName) + .Statistics(out var stats) + .Sort(sortInfo) + .Paging(pagingInfo) + .TransformWith() + .ToListAsync(); + + return new QueryResult>(results, stats.ToQueryStatsInfo()); + } + } + + public async Task>> SearchEndpointMessages( + string endpointName, + string searchKeyword, + PagingInfo pagingInfo, + SortInfo sortInfo + ) + { + using (var session = documentStore.OpenAsyncSession()) + { + var results = await session.Query() + .Statistics(out var stats) + .Search(x => x.Query, searchKeyword) + .Where(m => m.ReceivingEndpointName == endpointName) + .Sort(sortInfo) + .Paging(pagingInfo) + .TransformWith() + .ToListAsync(); + + return new QueryResult>(results, stats.ToQueryStatsInfo()); + } + } + + public async Task>> GetAllMessagesByConversation( + string conversationId, + PagingInfo pagingInfo, + SortInfo sortInfo, + bool includeSystemMessages + ) + { + using (var session = documentStore.OpenAsyncSession()) + { + var results = await session.Query() + .Statistics(out var stats) + .Where(m => m.ConversationId == conversationId) + .Sort(sortInfo) + .Paging(pagingInfo) + .TransformWith() + .ToListAsync(); + + return new QueryResult>(results, stats.ToQueryStatsInfo()); + } + } + + public async Task>> GetAllMessagesForSearch( + string searchTerms, + PagingInfo pagingInfo, + SortInfo sortInfo + ) + { + using (var session = documentStore.OpenAsyncSession()) + { + var results = await session.Query() + .Statistics(out var stats) + .Search(x => x.Query, searchTerms) + .Sort(sortInfo) + .Paging(pagingInfo) + .TransformWith() + .ToListAsync(); + + return new QueryResult>(results, stats.ToQueryStatsInfo()); + } + } + + public async Task>> GetAllMessagesForEndpoint( + string searchTerms, + string receivingEndpointName, + PagingInfo pagingInfo, + SortInfo sortInfo + ) + { + using (var session = documentStore.OpenAsyncSession()) + { + var results = await session.Query() + .Statistics(out var stats) + .Search(x => x.Query, searchTerms) + .Where(m => m.ReceivingEndpointName == receivingEndpointName) + .Sort(sortInfo) + .Paging(pagingInfo) + .TransformWith() + .ToListAsync(); + + return new QueryResult>(results, stats.ToQueryStatsInfo()); + } + } + + public async Task FailedMessageFetch(string failedMessageId) + { + using (var session = documentStore.OpenAsyncSession()) + { + return await session.LoadAsync(new Guid(failedMessageId)); + } + } + + public async Task FailedMessageMarkAsArchived(string failedMessageId) + { + using (var session = documentStore.OpenAsyncSession()) + { + var failedMessage = await session.LoadAsync(new Guid(failedMessageId)); + + if (failedMessage.Status != FailedMessageStatus.Archived) + { + failedMessage.Status = FailedMessageStatus.Archived; + } + + await session.SaveChangesAsync(); + } + } + + public async Task FailedMessagesFetch(Guid[] ids) + { + using (var session = documentStore.OpenAsyncSession()) + { + var results = await session.LoadAsync(ids.Cast()); + return results.Where(x => x != null).ToArray(); + } + } + + public async Task StoreFailedErrorImport(FailedErrorImport failure) + { + using (var session = documentStore.OpenAsyncSession()) + { + await session.StoreAsync(failure); + + await session.SaveChangesAsync(); + } + } + + public Task CreateEditFailedMessageManager() + { + var session = documentStore.OpenAsyncSession(); + var manager = new EditFailedMessageManager(session); + return Task.FromResult((IEditFailedMessagesManager)manager); + } + + public async Task> GetFailureGroupView(string groupId, string status, string modified) + { + using (var session = documentStore.OpenAsyncSession()) + { + var document = await session.Advanced + .AsyncDocumentQuery() + .Statistics(out var stats) + .WhereEquals(group => group.Id, groupId) + .FilterByStatusWhere(status) + .FilterByLastModifiedRange(modified) + .FirstOrDefaultAsync(); // TODO: Was previously a to list with a linq to object FirstOrDefault, not sure if this works; + + return new QueryResult(document, stats.ToQueryStatsInfo()); + } + } + + public async Task> GetFailureGroupsByClassifier(string classifier) + { + using (var session = documentStore.OpenAsyncSession()) + { + var groups = session + .Query() + .Where(v => v.Type == classifier); + + var results = await groups + .OrderByDescending(x => x.Last) + .Take(200) // only show 200 groups + .ToListAsync(); + + return results; + } + } + + public async Task>> ErrorGet( + string status, + string modified, + string queueAddress, + PagingInfo pagingInfo, + SortInfo sortInfo + ) + { + using (var session = documentStore.OpenAsyncSession()) + { + var results = await session.Advanced + .AsyncDocumentQuery() + .Statistics(out var stats) + .FilterByStatusWhere(status) + .FilterByLastModifiedRange(modified) + .FilterByQueueAddress(queueAddress) + .Sort(sortInfo) + .Paging(pagingInfo) + .SetResultTransformer(new FailedMessageViewTransformer().TransformerName) + .SelectFields() + .ToListAsync(); + + return new QueryResult>(results, stats.ToQueryStatsInfo()); + } + } + + public async Task ErrorsHead( + string status, + string modified, + string queueAddress + ) + { + using (var session = documentStore.OpenAsyncSession()) + { + var stats = await session.Advanced + .AsyncDocumentQuery() + .FilterByStatusWhere(status) + .FilterByLastModifiedRange(modified) + .FilterByQueueAddress(queueAddress) + .QueryResultAsync(); + + return stats.ToQueryStatsInfo(); + } + } + + public async Task>> ErrorsByEndpointName( + string status, + string endpointName, + string modified, + PagingInfo pagingInfo, + SortInfo sortInfo + ) + { + using (var session = documentStore.OpenAsyncSession()) + { + var results = await session.Advanced + .AsyncDocumentQuery() + .Statistics(out var stats) + .FilterByStatusWhere(status) + .AndAlso() + .WhereEquals("ReceivingEndpointName", endpointName) + .FilterByLastModifiedRange(modified) + .Sort(sortInfo) + .Paging(pagingInfo) + .SetResultTransformer(new FailedMessageViewTransformer().TransformerName) + .SelectFields() + .ToListAsync(); + + return new QueryResult>(results, stats.ToQueryStatsInfo()); + } + } + + public async Task> ErrorsSummary() + { + using (var session = documentStore.OpenAsyncSession()) + { + var facetResults = await session.Query() + .ToFacetsAsync(new List + { + new Facet + { + Name = "Name", + DisplayName = "Endpoints" + }, + new Facet + { + Name = "Host", + DisplayName = "Hosts" + }, + new Facet + { + Name = "MessageType", + DisplayName = "Message types" + } + }); + + var results = facetResults + .Results + .ToDictionary( + x => x.Key, + x => (object)x.Value + ); + + Guard.Assert(false, "TODO: Check how to convert dictionary item VALUES, currently return object which must be typed"); + + return results; + } + } + + public async Task ErrorBy(Guid failedMessageId) + { + using (var session = documentStore.OpenAsyncSession()) + { + var message = await session.LoadAsync(failedMessageId); + return message; + } + } + + public async Task ErrorBy(string failedMessageId) + { + using (var session = documentStore.OpenAsyncSession()) + { + var message = await session.LoadAsync(FailedMessageIdGenerator.MakeDocumentId(failedMessageId)); + return message; + } + } + + public Task CreateNotificationsManager() + { + var session = documentStore.OpenAsyncSession(); + var manager = new NotificationsManager(session); + + return Task.FromResult(manager); + } + + public async Task ErrorLastBy(Guid failedMessageId) + { + using (var session = documentStore.OpenAsyncSession()) + { + var message = await session.LoadAsync(failedMessageId); + if (message == null) + { + return null; + } + var result = Map(message, session); + return result; + } + } + + static FailedMessageView Map(FailedMessage message, IAsyncDocumentSession session) + { + var processingAttempt = message.ProcessingAttempts.Last(); + + var metadata = processingAttempt.MessageMetadata; + var failureDetails = processingAttempt.FailureDetails; + var wasEdited = message.ProcessingAttempts.Last().Headers.ContainsKey("ServiceControl.EditOf"); + + var failedMsgView = new FailedMessageView + { + Id = message.UniqueMessageId, + MessageType = metadata.GetAsStringOrNull("MessageType"), + IsSystemMessage = metadata.GetOrDefault("IsSystemMessage"), + TimeSent = metadata.GetAsNullableDatetime("TimeSent"), + MessageId = metadata.GetAsStringOrNull("MessageId"), + Exception = failureDetails.Exception, + QueueAddress = failureDetails.AddressOfFailingEndpoint, + NumberOfProcessingAttempts = message.ProcessingAttempts.Count, + Status = message.Status, + TimeOfFailure = failureDetails.TimeOfFailure, + LastModified = session.Advanced.GetMetadataFor(message)["Last-Modified"].Value(), + Edited = wasEdited, + EditOf = wasEdited ? message.ProcessingAttempts.Last().Headers["ServiceControl.EditOf"] : "" + }; + + try + { + failedMsgView.SendingEndpoint = metadata.GetOrDefault("SendingEndpoint"); + } + catch (Exception ex) + { + Logger.Warn($"Unable to parse SendingEndpoint from metadata for messageId {message.UniqueMessageId}", ex); + failedMsgView.SendingEndpoint = EndpointDetailsParser.SendingEndpoint(processingAttempt.Headers); + } + + try + { + failedMsgView.ReceivingEndpoint = metadata.GetOrDefault("ReceivingEndpoint"); + } + catch (Exception ex) + { + Logger.Warn($"Unable to parse ReceivingEndpoint from metadata for messageId {message.UniqueMessageId}", ex); + failedMsgView.ReceivingEndpoint = EndpointDetailsParser.ReceivingEndpoint(processingAttempt.Headers); + } + + return failedMsgView; + } + + + public async Task EditComment(string groupId, string comment) + { + using (var session = documentStore.OpenAsyncSession()) + { + var groupComment = + await session.LoadAsync(GroupComment.MakeId(groupId)) + ?? new GroupComment { Id = GroupComment.MakeId(groupId) }; + + groupComment.Comment = comment; + + await session.StoreAsync(groupComment); + await session.SaveChangesAsync(); + } + } + + public async Task DeleteComment(string groupId) + { + using (var session = documentStore.OpenAsyncSession()) + { + session.Delete(GroupComment.MakeId(groupId)); + await session.SaveChangesAsync(); + } + } + + public async Task>> GetGroupErrors( + string groupId, + string status, + string modified, + SortInfo sortInfo, + PagingInfo pagingInfo + ) + { + using (var session = documentStore.OpenAsyncSession()) + { + var results = await session.Advanced + .AsyncDocumentQuery() + .Statistics(out var stats) + .WhereEquals(view => view.FailureGroupId, groupId) + .FilterByStatusWhere(status) + .FilterByLastModifiedRange(modified) + .Sort(sortInfo) + .Paging(pagingInfo) + .SetResultTransformer(FailedMessageViewTransformer.Name) + .SelectFields() + .ToListAsync(); + + return results.ToQueryResult(stats); + } + } + + public async Task GetGroupErrorsCount(string groupId, string status, string modified) + { + using (var session = documentStore.OpenAsyncSession()) + { + var queryResult = await session.Advanced + .AsyncDocumentQuery() + .WhereEquals(view => view.FailureGroupId, groupId) + .FilterByStatusWhere(status) + .FilterByLastModifiedRange(modified) + .QueryResultAsync(); + + return queryResult.ToQueryStatsInfo(); + } + } + + public async Task>> GetGroup(string groupId, string status, string modified) + { + using (var session = documentStore.OpenAsyncSession()) + { + var queryResult = await session.Advanced + .AsyncDocumentQuery() + .Statistics(out var stats) + .WhereEquals(group => group.Id, groupId) + .FilterByStatusWhere(status) + .FilterByLastModifiedRange(modified) + .ToListAsync(); + + return queryResult.ToQueryResult(stats); + } + } + + public async Task MarkMessageAsResolved(string failedMessageId) + { + using (var session = documentStore.OpenAsyncSession()) + { + session.Advanced.UseOptimisticConcurrency = true; + + var failedMessage = await session.LoadAsync(new Guid(failedMessageId)); + + if (failedMessage == null) + { + return false; + } + + failedMessage.Status = FailedMessageStatus.Resolved; + + await session.SaveChangesAsync(); + + return true; + } + } + + public async Task ProcessPendingRetries(DateTime periodFrom, DateTime periodTo, string queueAddress, Func processCallback) + { + using (var session = documentStore.OpenAsyncSession()) + { + var prequery = session.Advanced + .AsyncDocumentQuery() + .WhereEquals("Status", (int)FailedMessageStatus.RetryIssued) + .AndAlso() + .WhereBetweenOrEqual("LastModified", periodFrom.Ticks, periodTo.Ticks); + + if (!string.IsNullOrWhiteSpace(queueAddress)) + { + prequery = prequery.AndAlso() + .WhereEquals(options => options.QueueAddress, queueAddress); + } + + var query = prequery + .SetResultTransformer(new FailedMessageViewTransformer().TransformerName) + .SelectFields(); + + using (var ie = await session.Advanced.StreamAsync(query)) + { + while (await ie.MoveNextAsync()) + { + await processCallback(ie.Current.Document.Id); + } + } + } + } + + class DocumentPatchResult + { + public string Document { get; set; } + } + + public async Task<(string[] ids, int count)> UnArchiveMessagesByRange(DateTime from, DateTime to, DateTime cutOff) + { + var options = new BulkOperationOptions + { + AllowStale = true + }; + + var result = await documentStore.AsyncDatabaseCommands.UpdateByIndexAsync( + new FailedMessageViewIndex().IndexName, + new IndexQuery + { + Query = string.Format(CultureInfo.InvariantCulture, "LastModified:[{0} TO {1}] AND Status:{2}", from.Ticks, to.Ticks, (int)FailedMessageStatus.Archived), + Cutoff = cutOff + }, new ScriptedPatchRequest + { + Script = @" +if(this.Status === archivedStatus) { + this.Status = unresolvedStatus; +} +", + Values = + { + {"archivedStatus", (int)FailedMessageStatus.Archived}, + {"unresolvedStatus", (int)FailedMessageStatus.Unresolved} + } + }, options); + + var patchedDocumentIds = (await result.WaitForCompletionAsync()) + .JsonDeserialization(); + + return ( + patchedDocumentIds.Select(x => FailedMessageIdGenerator.GetMessageIdFromDocumentId(x.Document)).ToArray(), + patchedDocumentIds.Length + ); + } + + public async Task<(string[] ids, int count)> UnArchiveMessages(IEnumerable failedMessageIds) + { + FailedMessage[] failedMessages; + + using (var session = documentStore.OpenAsyncSession()) + { + session.Advanced.UseOptimisticConcurrency = true; + + var documentIds = failedMessageIds.Select(FailedMessageIdGenerator.MakeDocumentId); + + failedMessages = await session.LoadAsync(documentIds); + + foreach (var failedMessage in failedMessages) + { + if (failedMessage.Status == FailedMessageStatus.Archived) + { + failedMessage.Status = FailedMessageStatus.Unresolved; + } + } + + await session.SaveChangesAsync(); + } + + return ( + failedMessages.Select(x => x.UniqueMessageId).ToArray(), + failedMessages.Length + ); + } + + public async Task RevertRetry(string messageUniqueId) + { + using (var session = documentStore.OpenAsyncSession()) + { + var failedMessage = await session + .LoadAsync(FailedMessageIdGenerator.MakeDocumentId(messageUniqueId)); + if (failedMessage != null) + { + failedMessage.Status = FailedMessageStatus.Unresolved; + } + + var failedMessageRetry = await session + .LoadAsync(FailedMessageRetry.MakeDocumentId(messageUniqueId)); + if (failedMessageRetry != null) + { + session.Delete(failedMessageRetry); + } + + await session.SaveChangesAsync(); + } + } + + public Task RemoveFailedMessageRetryDocument(string uniqueMessageId) + { + return documentStore.AsyncDatabaseCommands.DeleteAsync(FailedMessageRetry.MakeDocumentId(uniqueMessageId), null); + } + + public async Task GetRetryPendingMessages(DateTime from, DateTime to, string queueAddress) // TODO: Could we use IAsyncEnumerable here as this is an unbounded query? + { + var ids = new List(); + + using (var session = documentStore.OpenAsyncSession()) + { + var query = session.Advanced + .AsyncDocumentQuery() + .WhereEquals("Status", (int)FailedMessageStatus.RetryIssued) + .AndAlso() + .WhereBetweenOrEqual(options => options.LastModified, from.Ticks, to.Ticks) + .AndAlso() + .WhereEquals(o => o.QueueAddress, queueAddress) + .SetResultTransformer(FailedMessageViewTransformer.Name) + .SelectFields(new[] { "Id" }); + + using (var ie = await session.Advanced.StreamAsync(query)) + { + while (await ie.MoveNextAsync()) + { + ids.Add(ie.Current.Document.Id); + } + } + } + + return ids.ToArray(); // TODO: Currently returning array as all other API's return arrays and not IEnumerable + } + + // TODO: How is this different than what RavenAttachmentBodyStorage.TryFetch is doing? Is this implemented twice? + public async Task FetchFromFailedMessage(string uniqueMessageId) + { + string documentId = FailedMessageIdGenerator.MakeDocumentId(uniqueMessageId); + var results = await documentStore.AsyncDatabaseCommands.GetAsync(new[] { documentId }, null, + transformer: MessagesBodyTransformer.Name); + + string resultBody = ((results.Results?.SingleOrDefault()?["$values"] as RavenJArray)?.SingleOrDefault() as RavenJObject) + ?.ToObject()?.Body; + + if (resultBody != null) + { + return Encoding.UTF8.GetBytes(resultBody); + } + + return null; + } + + public async Task StoreEventLogItem(EventLogItem logItem) + { + using (var session = documentStore.OpenAsyncSession()) + { + await session.StoreAsync(logItem); + await session.SaveChangesAsync(); + } + } + + public async Task StoreFailedMessages(params FailedMessage[] failedMessages) + { + using (var session = documentStore.OpenAsyncSession()) + { + foreach (var message in failedMessages) + { + await session.StoreAsync(message); + } + + await session.SaveChangesAsync(); + } + } + + static readonly ILog Logger = LogManager.GetLogger(); + } +} diff --git a/src/ServiceControl.Persistence.RavenDb/EventLogDataStore.cs b/src/ServiceControl.Persistence.RavenDb/EventLogDataStore.cs new file mode 100644 index 0000000000..6fbafb64f6 --- /dev/null +++ b/src/ServiceControl.Persistence.RavenDb/EventLogDataStore.cs @@ -0,0 +1,41 @@ +namespace ServiceControl.Persistence.RavenDb +{ + using System.Collections.Generic; + using System.Threading.Tasks; + using EventLog; + using Persistence.Infrastructure; + using Raven.Client; + using Raven.Client.Linq; + + class EventLogDataStore : IEventLogDataStore + { + readonly IDocumentStore documentStore; + + public EventLogDataStore(IDocumentStore documentStore) + { + this.documentStore = documentStore; + } + + public async Task Add(EventLogItem logItem) + { + using (var session = documentStore.OpenAsyncSession()) + { + await session.StoreAsync(logItem); + await session.SaveChangesAsync(); + } + } + + public async Task<(IList, int, string)> GetEventLogItems(PagingInfo pagingInfo) + { + using (var session = documentStore.OpenAsyncSession()) + { + var results = await session.Query().Statistics(out var stats) + .OrderByDescending(p => p.RaisedAt) + .Paging(pagingInfo) + .ToListAsync(); + + return (results, stats.TotalResults, stats.ResultEtag.ToString()); + } + } + } +} diff --git a/src/ServiceControl/Infrastructure/RavenDB/Expiration/AuditMessageCleaner.cs b/src/ServiceControl.Persistence.RavenDb/Expiration/AuditMessageCleaner.cs similarity index 100% rename from src/ServiceControl/Infrastructure/RavenDB/Expiration/AuditMessageCleaner.cs rename to src/ServiceControl.Persistence.RavenDb/Expiration/AuditMessageCleaner.cs diff --git a/src/ServiceControl/Infrastructure/RavenDB/Expiration/ErrorMessageCleaner.cs b/src/ServiceControl.Persistence.RavenDb/Expiration/ErrorMessageCleaner.cs similarity index 98% rename from src/ServiceControl/Infrastructure/RavenDB/Expiration/ErrorMessageCleaner.cs rename to src/ServiceControl.Persistence.RavenDb/Expiration/ErrorMessageCleaner.cs index 81e7c672df..8e91475081 100644 --- a/src/ServiceControl/Infrastructure/RavenDB/Expiration/ErrorMessageCleaner.cs +++ b/src/ServiceControl.Persistence.RavenDb/Expiration/ErrorMessageCleaner.cs @@ -63,7 +63,7 @@ public static void Clean(int deletionBatchSize, DocumentDatabase database, DateT return; } - var failedMessageRetryId = FailedMessageRetry.MakeDocumentId(FailedMessage.GetMessageIdFromDocumentId(id)); + var failedMessageRetryId = FailedMessageRetry.MakeDocumentId(FailedMessageIdGenerator.GetMessageIdFromDocumentId(id)); state.failedRetryItems.Add(new DeleteCommandData { Key = failedMessageRetryId diff --git a/src/ServiceControl/Infrastructure/RavenDB/Expiration/EventLogItemsCleaner.cs b/src/ServiceControl.Persistence.RavenDb/Expiration/EventLogItemsCleaner.cs similarity index 100% rename from src/ServiceControl/Infrastructure/RavenDB/Expiration/EventLogItemsCleaner.cs rename to src/ServiceControl.Persistence.RavenDb/Expiration/EventLogItemsCleaner.cs diff --git a/src/ServiceControl/Infrastructure/RavenDB/Expiration/ExpiredDocumentsCleaner.cs b/src/ServiceControl.Persistence.RavenDb/Expiration/ExpiredDocumentsCleaner.cs similarity index 81% rename from src/ServiceControl/Infrastructure/RavenDB/Expiration/ExpiredDocumentsCleaner.cs rename to src/ServiceControl.Persistence.RavenDb/Expiration/ExpiredDocumentsCleaner.cs index 59384ef3cd..ea0b50f3e9 100644 --- a/src/ServiceControl/Infrastructure/RavenDB/Expiration/ExpiredDocumentsCleaner.cs +++ b/src/ServiceControl.Persistence.RavenDb/Expiration/ExpiredDocumentsCleaner.cs @@ -8,13 +8,13 @@ using Raven.Abstractions; using Raven.Database; using SagaAudit; - using ServiceBus.Management.Infrastructure.Settings; + using ServiceControl.Persistence; class ExpiredDocumentsCleaner { - public static Task RunCleanup(int deletionBatchSize, DocumentDatabase database, Settings settings, CancellationToken cancellationToken = default) + public static Task RunCleanup(int deletionBatchSize, DocumentDatabase database, PersistenceSettings settings, CancellationToken cancellationToken = default) { - var threshold = SystemTime.UtcNow.Add(-settings.ErrorRetentionPeriod); + var threshold = SystemTime.UtcNow.Add(settings.ErrorRetentionPeriod); if (logger.IsDebugEnabled) { @@ -22,7 +22,7 @@ public static Task RunCleanup(int deletionBatchSize, Do } ErrorMessageCleaner.Clean(deletionBatchSize, database, threshold, cancellationToken); - threshold = SystemTime.UtcNow.Add(-settings.EventsRetentionPeriod); + threshold = SystemTime.UtcNow.Add(settings.EventsRetentionPeriod); if (logger.IsDebugEnabled) { @@ -32,7 +32,7 @@ public static Task RunCleanup(int deletionBatchSize, Do if (settings.AuditRetentionPeriod.HasValue) { - threshold = SystemTime.UtcNow.Add(-settings.AuditRetentionPeriod.Value); + threshold = SystemTime.UtcNow.Add(settings.AuditRetentionPeriod.Value); if (logger.IsDebugEnabled) { diff --git a/src/ServiceControl.Persistence.RavenDb/Expiration/ExpiredDocumentsCleanerBundle.cs b/src/ServiceControl.Persistence.RavenDb/Expiration/ExpiredDocumentsCleanerBundle.cs new file mode 100644 index 0000000000..cb0ccb016a --- /dev/null +++ b/src/ServiceControl.Persistence.RavenDb/Expiration/ExpiredDocumentsCleanerBundle.cs @@ -0,0 +1,133 @@ +namespace ServiceControl.Infrastructure.RavenDB.Expiration +{ + using System; + using System.ComponentModel.Composition; + using System.Threading.Tasks; + using BackgroundTasks; + using NServiceBus.Logging; + using Persistence.RavenDb; + using Raven.Database; + using Raven.Database.Plugins; + + [InheritedExport(typeof(IStartupTask))] + [ExportMetadata("Bundle", "customDocumentExpiration")] + class ExpiredDocumentsCleanerBundle : IStartupTask, IDisposable + { + // TODO: Ensure that the timers are started when the persister starts! + + public void Dispose() + { + lock (this) + { + if (timer == null) + { + return; + } + + var stopTask = timer.Stop(); + var delayTask = Task.Delay(TimeSpan.FromSeconds(30)); + var composite = Task.WhenAny(stopTask, delayTask); + + var finishedTask = composite.GetAwaiter().GetResult(); + if (finishedTask == delayTask) + { + logger.Error("Cleanup process did not finish on time. Forcing shutdown."); + } + else + { + logger.Info("Expired documents cleanup process stopped."); + } + + timer = null; + } + } + + public void Execute(DocumentDatabase database) + { + var persistenceSettings = RavenBootstrapper.Settings; + + if (ExpirationProcessTimerInSeconds == 0) + { + return; + } + + var due = TimeSpan.FromSeconds(ExpirationProcessTimerInSeconds); + var deletionBatchSize = ExpirationProcessBatchSize; + + logger.Info($"Running deletion of expired documents every {ExpirationProcessTimerInSeconds} seconds"); + logger.Info($"Deletion batch size set to {deletionBatchSize}"); + logger.Info($"Retention period for errors is {persistenceSettings.ErrorRetentionPeriod}"); + + var auditRetention = persistenceSettings.AuditRetentionPeriod; + + if (auditRetention.HasValue) + { + logger.InfoFormat("Retention period for audits and saga history is {0}", persistenceSettings.AuditRetentionPeriod); + } + + timer = new TimerJob( + token => ExpiredDocumentsCleaner.RunCleanup(deletionBatchSize, database, persistenceSettings, token), due, due, e => { logger.Error("Error when trying to find expired documents", e); }); + } + + int ExpirationProcessTimerInSeconds + { + get + { + var expirationProcessTimerInSeconds = ExpirationProcessTimerInSecondsDefault; + + if (RavenBootstrapper.Settings.PersisterSpecificSettings.TryGetValue(RavenBootstrapper.ExpirationProcessTimerInSecondsKey, out var expirationProcessTimerInSecondsString)) + { + expirationProcessTimerInSeconds = int.Parse(expirationProcessTimerInSecondsString); + } + + if (expirationProcessTimerInSeconds < 0) + { + logger.Error($"ExpirationProcessTimerInSeconds cannot be negative. Defaulting to {ExpirationProcessTimerInSecondsDefault}"); + return ExpirationProcessTimerInSecondsDefault; + } + + if (expirationProcessTimerInSeconds > TimeSpan.FromHours(3).TotalSeconds) + { + logger.Error($"ExpirationProcessTimerInSeconds cannot be larger than {TimeSpan.FromHours(3).TotalSeconds}. Defaulting to {ExpirationProcessTimerInSecondsDefault}"); + return ExpirationProcessTimerInSecondsDefault; + } + + return expirationProcessTimerInSeconds; + } + } + + public int ExpirationProcessBatchSize + { + get + { + var expirationProcessBatchSize = ExpirationProcessBatchSizeDefault; + + if (RavenBootstrapper.Settings.PersisterSpecificSettings.TryGetValue(RavenBootstrapper.ExpirationProcessBatchSizeKey, out var expirationProcessBatchSizeString)) + { + expirationProcessBatchSize = int.Parse(expirationProcessBatchSizeString); + } + if (expirationProcessBatchSize < 1) + { + logger.Error($"ExpirationProcessBatchSize cannot be less than 1. Defaulting to {ExpirationProcessBatchSizeDefault}"); + return ExpirationProcessBatchSizeDefault; + } + + if (expirationProcessBatchSize < ExpirationProcessBatchSizeMinimum) + { + logger.Error($"ExpirationProcessBatchSize cannot be less than {ExpirationProcessBatchSizeMinimum}. Defaulting to {ExpirationProcessBatchSizeDefault}"); + return ExpirationProcessBatchSizeDefault; + } + + return expirationProcessBatchSize; + } + } + + const int ExpirationProcessTimerInSecondsDefault = 600; + const int ExpirationProcessBatchSizeDefault = 65512; + const int ExpirationProcessBatchSizeMinimum = 10240; + + + ILog logger = LogManager.GetLogger(typeof(ExpiredDocumentsCleanerBundle)); + TimerJob timer; + } +} \ No newline at end of file diff --git a/src/ServiceControl/Infrastructure/RavenDB/Expiration/ExpiryErrorMessageIndex.cs b/src/ServiceControl.Persistence.RavenDb/Expiration/ExpiryErrorMessageIndex.cs similarity index 89% rename from src/ServiceControl/Infrastructure/RavenDB/Expiration/ExpiryErrorMessageIndex.cs rename to src/ServiceControl.Persistence.RavenDb/Expiration/ExpiryErrorMessageIndex.cs index fb949d6ba3..39f18df6c8 100644 --- a/src/ServiceControl/Infrastructure/RavenDB/Expiration/ExpiryErrorMessageIndex.cs +++ b/src/ServiceControl.Persistence.RavenDb/Expiration/ExpiryErrorMessageIndex.cs @@ -5,7 +5,7 @@ namespace ServiceControl.Infrastructure.RavenDB.Expiration using MessageFailures; using Raven.Client.Indexes; - public class ExpiryErrorMessageIndex : AbstractIndexCreationTask + class ExpiryErrorMessageIndex : AbstractIndexCreationTask { public ExpiryErrorMessageIndex() { diff --git a/src/ServiceControl/Infrastructure/RavenDB/Expiration/ExpiryEventLogItemsIndex.cs b/src/ServiceControl.Persistence.RavenDb/Expiration/ExpiryEventLogItemsIndex.cs similarity index 86% rename from src/ServiceControl/Infrastructure/RavenDB/Expiration/ExpiryEventLogItemsIndex.cs rename to src/ServiceControl.Persistence.RavenDb/Expiration/ExpiryEventLogItemsIndex.cs index 343efeeb41..13ebc15cfe 100644 --- a/src/ServiceControl/Infrastructure/RavenDB/Expiration/ExpiryEventLogItemsIndex.cs +++ b/src/ServiceControl.Persistence.RavenDb/Expiration/ExpiryEventLogItemsIndex.cs @@ -5,7 +5,7 @@ namespace ServiceControl.Infrastructure.RavenDB.Expiration using EventLog; using Raven.Client.Indexes; - public class ExpiryEventLogItemsIndex : AbstractIndexCreationTask + class ExpiryEventLogItemsIndex : AbstractIndexCreationTask { public ExpiryEventLogItemsIndex() { diff --git a/src/ServiceControl/Infrastructure/RavenDB/Expiration/ExpiryProcessedMessageIndex.cs b/src/ServiceControl.Persistence.RavenDb/Expiration/ExpiryProcessedMessageIndex.cs similarity index 84% rename from src/ServiceControl/Infrastructure/RavenDB/Expiration/ExpiryProcessedMessageIndex.cs rename to src/ServiceControl.Persistence.RavenDb/Expiration/ExpiryProcessedMessageIndex.cs index c0b443dec5..3d6e24725b 100644 --- a/src/ServiceControl/Infrastructure/RavenDB/Expiration/ExpiryProcessedMessageIndex.cs +++ b/src/ServiceControl.Persistence.RavenDb/Expiration/ExpiryProcessedMessageIndex.cs @@ -4,7 +4,7 @@ namespace ServiceControl.Infrastructure.RavenDB.Expiration using MessageAuditing; using Raven.Client.Indexes; - public class ExpiryProcessedMessageIndex : AbstractIndexCreationTask + class ExpiryProcessedMessageIndex : AbstractIndexCreationTask { public ExpiryProcessedMessageIndex() { diff --git a/src/ServiceControl.Persistence.RavenDb/Extensions.cs b/src/ServiceControl.Persistence.RavenDb/Extensions.cs index 7b1c971fe3..5f2184db8f 100644 --- a/src/ServiceControl.Persistence.RavenDb/Extensions.cs +++ b/src/ServiceControl.Persistence.RavenDb/Extensions.cs @@ -6,7 +6,7 @@ using Raven.Database; using Raven.Json.Linq; - public static class Extensions + static class Extensions { public static void Query(this DocumentDatabase db, string index, IndexQuery query, Action onItem, TState state, CancellationToken cancellationToken = default) { diff --git a/src/ServiceControl/Infrastructure/DictionaryExtensions.cs b/src/ServiceControl.Persistence.RavenDb/Extensions/DictionaryExtensions.cs similarity index 100% rename from src/ServiceControl/Infrastructure/DictionaryExtensions.cs rename to src/ServiceControl.Persistence.RavenDb/Extensions/DictionaryExtensions.cs diff --git a/src/ServiceControl.Persistence.RavenDb/Extensions/QueryResultConvert.cs b/src/ServiceControl.Persistence.RavenDb/Extensions/QueryResultConvert.cs new file mode 100644 index 0000000000..9d47302837 --- /dev/null +++ b/src/ServiceControl.Persistence.RavenDb/Extensions/QueryResultConvert.cs @@ -0,0 +1,15 @@ +namespace ServiceControl.Persistence.RavenDb +{ + using System.Collections.Generic; + using Persistence.Infrastructure; + using Raven.Client; + + static class QueryResultConvert + { + public static QueryResult> ToQueryResult(this IList result, RavenQueryStatistics stats) + where T : class + { + return new QueryResult>(result, stats.ToQueryStatsInfo()); + } + } +} \ No newline at end of file diff --git a/src/ServiceControl.Persistence.RavenDb/ExternalIntegrationRequestsDataStore.cs b/src/ServiceControl.Persistence.RavenDb/ExternalIntegrationRequestsDataStore.cs new file mode 100644 index 0000000000..10288ff84e --- /dev/null +++ b/src/ServiceControl.Persistence.RavenDb/ExternalIntegrationRequestsDataStore.cs @@ -0,0 +1,219 @@ +namespace ServiceControl.Persistence.RavenDb +{ + using System; + using System.Collections.Generic; + using System.Linq; + using System.Reactive.Linq; + using System.Threading; + using System.Threading.Tasks; + using ExternalIntegrations; + using Microsoft.Extensions.Hosting; + using NServiceBus; + using NServiceBus.Logging; + using Raven.Abstractions.Data; + using Raven.Client; + using Raven.Client.Linq; + using ServiceBus.Management.Infrastructure.Extensions; + + class ExternalIntegrationRequestsDataStore + : IExternalIntegrationRequestsDataStore + , IHostedService + , IAsyncDisposable + { + public ExternalIntegrationRequestsDataStore(PersistenceSettings settings, IDocumentStore documentStore, CriticalError criticalError) + { + this.settings = settings; + this.documentStore = documentStore; + + circuitBreaker = new RepeatedFailuresOverTimeCircuitBreaker( + "EventDispatcher", + TimeSpan.FromMinutes(5), // TODO: Shouldn't be magic value but coming from settings + ex => criticalError.Raise("Repeated failures when dispatching external integration events.", ex), + TimeSpan.FromSeconds(20) // TODO: Shouldn't be magic value but coming from settings + ); + } + + const string KeyPrefix = "ExternalIntegrationDispatchRequests"; + + public async Task StoreDispatchRequest(IEnumerable dispatchRequests) + { + using (var session = documentStore.OpenAsyncSession()) + { + foreach (var dispatchRequest in dispatchRequests) + { + if (dispatchRequest.Id != null) + { + throw new ArgumentException("Items cannot have their Id property set"); + } + + dispatchRequest.Id = KeyPrefix + "/" + Guid.NewGuid(); // TODO: Key is generated to persistence + await session.StoreAsync(dispatchRequest); + } + + await session.SaveChangesAsync(); + } + } + + public void Subscribe(Func callback) + { + if (this.callback != null) + { + throw new InvalidOperationException("Subscription already exists."); + } + + this.callback = callback ?? throw new ArgumentNullException(nameof(callback)); + + StartDispatcher(); + } + + void StartDispatcher() + { + task = StartDispatcherTask(tokenSource.Token); + } + + async Task StartDispatcherTask(CancellationToken cancellationToken) + { + try + { + await DispatchEvents(cancellationToken); + do + { + try + { + await signal.WaitHandle.WaitOneAsync(cancellationToken); + signal.Reset(); + } + catch (OperationCanceledException) + { + break; + } + + await DispatchEvents(cancellationToken); + } + while (!cancellationToken.IsCancellationRequested); + } + catch (OperationCanceledException) + { + // ignore + } + catch (Exception ex) + { + Logger.Error("An exception occurred when dispatching external integration events", ex); + await circuitBreaker.Failure(ex); + + if (!tokenSource.IsCancellationRequested) + { + StartDispatcher(); + } + } + } + + async Task DispatchEvents(CancellationToken cancellationToken) + { + bool more; + + do + { + more = await TryDispatchEventBatch(); + + circuitBreaker.Success(); + + if (more && !cancellationToken.IsCancellationRequested) + { + //if there is more events to dispatch we sleep for a bit and then we go again + await Task.Delay(1000, CancellationToken.None); + } + } + while (!cancellationToken.IsCancellationRequested && more); + } + + async Task TryDispatchEventBatch() + { + using (var session = documentStore.OpenAsyncSession()) + { + var awaitingDispatching = await session + .Query() + .Statistics(out var stats) + .Take(settings.ExternalIntegrationsDispatchingBatchSize) + .ToListAsync(); + + if (awaitingDispatching.Count == 0) + { + // If the index hasn't caught up, try again + return stats.IndexEtag.CompareTo(latestEtag) < 0; + } + + var allContexts = awaitingDispatching.Select(r => r.DispatchContext).ToArray(); + if (Logger.IsDebugEnabled) + { + Logger.Debug($"Dispatching {allContexts.Length} events."); + } + + await callback(allContexts); + + foreach (var dispatchedEvent in awaitingDispatching) + { + session.Delete(dispatchedEvent); + } + + await session.SaveChangesAsync(); + } + + return true; + } + + public Task StartAsync(CancellationToken cancellationToken) + { + subscription = documentStore + .Changes() + .ForDocumentsStartingWith(KeyPrefix) + .Where(c => c.Type == DocumentChangeTypes.Put) + .Subscribe(d => + { + latestEtag = Etag.Max(d.Etag, latestEtag); + signal.Set(); + }); + + return Task.CompletedTask; + } + + public async Task StopAsync(CancellationToken cancellationToken) + { + await DisposeAsync(); + } + + public async ValueTask DisposeAsync() + { + if (isDisposed) + { + return; + } + + isDisposed = true; + subscription?.Dispose(); + tokenSource?.Cancel(); + + if (task != null) + { + await task; + } + + tokenSource?.Dispose(); + circuitBreaker?.Dispose(); + } + + readonly PersistenceSettings settings; + readonly IDocumentStore documentStore; + readonly CancellationTokenSource tokenSource = new CancellationTokenSource(); + readonly RepeatedFailuresOverTimeCircuitBreaker circuitBreaker; + + IDisposable subscription; + Etag latestEtag = Etag.Empty; + Task task; + ManualResetEventSlim signal = new ManualResetEventSlim(); + Func callback; + bool isDisposed; + + static ILog Logger = LogManager.GetLogger(typeof(ExternalIntegrationRequestsDataStore)); + } +} \ No newline at end of file diff --git a/src/ServiceControl.Persistence.RavenDb/FailedErrorImportDataStore.cs b/src/ServiceControl.Persistence.RavenDb/FailedErrorImportDataStore.cs new file mode 100644 index 0000000000..3d96697bd1 --- /dev/null +++ b/src/ServiceControl.Persistence.RavenDb/FailedErrorImportDataStore.cs @@ -0,0 +1,79 @@ +namespace ServiceControl.Persistence.RavenDb +{ + using System; + using System.Threading; + using System.Threading.Tasks; + using NServiceBus.Logging; + using Raven.Client; + using ServiceControl.Operations; + + class FailedErrorImportDataStore : IFailedErrorImportDataStore + { + readonly IDocumentStore store; + + static readonly ILog Logger = LogManager.GetLogger(typeof(FailedErrorImportDataStore)); + + public FailedErrorImportDataStore(IDocumentStore store) + { + this.store = store; + } + + public async Task ProcessFailedErrorImports(Func processMessage, CancellationToken cancellationToken) + { + var succeeded = 0; + var failed = 0; + using (var session = store.OpenAsyncSession()) + { + var query = session.Query(); + using (var stream = await session.Advanced.StreamAsync(query, cancellationToken) + ) + { + while (!cancellationToken.IsCancellationRequested && await stream.MoveNextAsync()) + { + var transportMessage = stream.Current.Document.Message; + try + { + await processMessage(transportMessage); + + await store.AsyncDatabaseCommands.DeleteAsync(stream.Current.Key, null, cancellationToken); + succeeded++; + + if (Logger.IsDebugEnabled) + { + Logger.Debug($"Successfully re-imported failed error message {transportMessage.Id}."); + } + } + catch (OperationCanceledException) + { + // no-op + } + catch (Exception e) + { + Logger.Error($"Error while attempting to re-import failed error message {transportMessage.Id}.", e); + failed++; + } + } + } + } + + Logger.Info($"Done re-importing failed errors. Successfully re-imported {succeeded} messages. Failed re-importing {failed} messages."); + + if (failed > 0) + { + Logger.Warn($"{failed} messages could not be re-imported. This could indicate a problem with the data. Contact Particular support if you need help with recovering the messages."); + } + } + + public async Task QueryContainsFailedImports() + { + using (var session = store.OpenAsyncSession()) + { + var query = session.Query(); + using (var ie = await session.Advanced.StreamAsync(query)) + { + return await ie.MoveNextAsync(); + } + } + } + } +} \ No newline at end of file diff --git a/src/ServiceControl.Persistence.RavenDb/FailedMessageViewIndexNotifications.cs b/src/ServiceControl.Persistence.RavenDb/FailedMessageViewIndexNotifications.cs new file mode 100644 index 0000000000..0d0bf814e5 --- /dev/null +++ b/src/ServiceControl.Persistence.RavenDb/FailedMessageViewIndexNotifications.cs @@ -0,0 +1,110 @@ +namespace ServiceControl.MessageFailures +{ + using System; + using System.Threading; + using System.Threading.Tasks; + using Api; + using Microsoft.Extensions.Hosting; + using NServiceBus.Logging; + using Persistence; + using Raven.Client; + + class FailedMessageViewIndexNotifications // TODO: Must be registered as single and as hosted service + : IFailedMessageViewIndexNotifications + , IDisposable + , IHostedService + { + public FailedMessageViewIndexNotifications(IDocumentStore store) + { + this.store = store; + } + + void OnNext() + { + try + { + UpdatedCount().GetAwaiter().GetResult(); + } + catch (Exception ex) + { + logging.WarnFormat("Failed to emit MessageFailuresUpdated - {0}", ex); + } + } + + async Task UpdatedCount() + { + using (var session = store.OpenAsyncSession()) + { + var failedUnresolvedMessageCount = await session + .Query() + .CountAsync(p => p.Status == FailedMessageStatus.Unresolved); + + var failedArchivedMessageCount = await session + .Query() + .CountAsync(p => p.Status == FailedMessageStatus.Archived); + + if (lastUnresolvedCount == failedUnresolvedMessageCount && lastArchivedCount == failedArchivedMessageCount) + { + return; + } + + lastUnresolvedCount = failedUnresolvedMessageCount; + lastArchivedCount = failedArchivedMessageCount; + + if (subscriber != null) + { + await subscriber(new FailedMessageTotals + { + ArchivedTotal = failedArchivedMessageCount, + UnresolvedTotal = failedUnresolvedMessageCount + }); + } + } + } + + public IDisposable Subscribe(Func callback) + { + if (callback is null) + { + throw new ArgumentNullException(nameof(callback)); + } + + if (!(subscriber is null)) + { + throw new InvalidOperationException("Already a subscriber, only a single subscriber supported"); + } + + subscriber = callback; + return this; + } + + public void Dispose() + { + subscriber = null; + subscription?.Dispose(); + } + + public Task StartAsync(CancellationToken cancellationToken) + { + subscription = store + .Changes() + .ForIndex(new FailedMessageViewIndex().IndexName) + .Subscribe(d => OnNext()); + return Task.CompletedTask; + } + + public Task StopAsync(CancellationToken cancellationToken) + { + Dispose(); + return Task.CompletedTask; + } + + readonly IDocumentStore store; + readonly ILog logging = LogManager.GetLogger(typeof(FailedMessageViewIndexNotifications)); + + Func subscriber; + IDisposable subscription; + int lastUnresolvedCount; + int lastArchivedCount; + } +} \ No newline at end of file diff --git a/src/ServiceControl/Recoverability/Grouping/Groupers/Reclassifier.cs b/src/ServiceControl.Persistence.RavenDb/FailedMessages/FailedMessageReclassifier.cs similarity index 81% rename from src/ServiceControl/Recoverability/Grouping/Groupers/Reclassifier.cs rename to src/ServiceControl.Persistence.RavenDb/FailedMessages/FailedMessageReclassifier.cs index 0c6466877c..3da00aee79 100644 --- a/src/ServiceControl/Recoverability/Grouping/Groupers/Reclassifier.cs +++ b/src/ServiceControl.Persistence.RavenDb/FailedMessages/FailedMessageReclassifier.cs @@ -1,26 +1,35 @@ -namespace ServiceControl.Recoverability +namespace ServiceControl.Persistence.RavenDb { using System; using System.Collections.Generic; using System.Linq; using System.Threading; using System.Threading.Tasks; - using Infrastructure; - using MessageFailures; - using MessageFailures.Api; using Microsoft.Extensions.Hosting; using NServiceBus.Logging; using Raven.Abstractions.Data; using Raven.Abstractions.Exceptions; using Raven.Client; using Raven.Json.Linq; + using ServiceControl.MessageFailures; + using ServiceControl.MessageFailures.Api; + using ServiceControl.Persistence.Infrastructure; + using ServiceControl.Recoverability; - public class Reclassifier + class FailedMessageReclassifier : IReclassifyFailedMessages { - internal Reclassifier(IHostApplicationLifetime applicationLifetime) - => applicationLifetime?.ApplicationStopping.Register(() => { abort = true; }); + readonly IDocumentStore store; + readonly IEnumerable classifiers; - internal async Task ReclassifyFailedMessages(IDocumentStore store, bool force, IEnumerable classifiers) + public FailedMessageReclassifier(IDocumentStore store, IHostApplicationLifetime applicationLifetime, IEnumerable classifiers) + { + this.store = store; + this.classifiers = classifiers; + + applicationLifetime?.ApplicationStopping.Register(() => { abort = true; }); + } + + public async Task ReclassifyFailedMessages(bool force) { logger.Info("Reclassification of failures started."); @@ -33,8 +42,7 @@ internal async Task ReclassifyFailedMessages(IDocumentStore store, bool for if (!force) { - settings = await session.LoadAsync(ReclassifyErrorSettings.IdentifierCase) - .ConfigureAwait(false); + settings = await session.LoadAsync(ReclassifyErrorSettings.IdentifierCase); if (settings != null && settings.ReclassificationDone) { @@ -49,9 +57,9 @@ internal async Task ReclassifyFailedMessages(IDocumentStore store, bool for var totalMessagesReclassified = 0; using (var stream = await session.Advanced.StreamAsync(query.As()) - .ConfigureAwait(false)) + ) { - while (!abort && await stream.MoveNextAsync().ConfigureAwait(false)) + while (!abort && await stream.MoveNextAsync()) { currentBatch.Add(Tuple.Create(stream.Current.Document.Id, new ClassifiableMessageDetails(stream.Current.Document))); @@ -79,8 +87,8 @@ internal async Task ReclassifyFailedMessages(IDocumentStore store, bool for } settings.ReclassificationDone = true; - await session.StoreAsync(settings).ConfigureAwait(false); - await session.SaveChangesAsync().ConfigureAwait(false); + await session.StoreAsync(settings); + await session.SaveChangesAsync(); return failedMessagesReclassified; } @@ -139,9 +147,8 @@ int ReclassifyBatch(IDocumentStore store, IEnumerable(); + readonly ILog logger = LogManager.GetLogger(); const int BatchSize = 1000; + bool abort; } -} \ No newline at end of file +} diff --git a/src/ServiceControl.Persistence.RavenDb/Guard.cs b/src/ServiceControl.Persistence.RavenDb/Guard.cs new file mode 100644 index 0000000000..a74daf1fd9 --- /dev/null +++ b/src/ServiceControl.Persistence.RavenDb/Guard.cs @@ -0,0 +1,20 @@ +using System; +using System.Diagnostics; + +static class Guard +{ + /// + /// Checks for a condition; if the condition is false, raise exception with message + /// + public static void Assert(bool condition, string message) + { + if (!condition) + { + if (Debugger.IsAttached) + { + Debugger.Break(); + } + throw new InvalidOperationException(message); + } + } +} \ No newline at end of file diff --git a/src/ServiceControl/Recoverability/Grouping/Raven/ArchivedGroupsViewIndex.cs b/src/ServiceControl.Persistence.RavenDb/Indexes/ArchivedGroupsViewIndex.cs similarity index 94% rename from src/ServiceControl/Recoverability/Grouping/Raven/ArchivedGroupsViewIndex.cs rename to src/ServiceControl.Persistence.RavenDb/Indexes/ArchivedGroupsViewIndex.cs index d20333de9c..6de54efd63 100644 --- a/src/ServiceControl/Recoverability/Grouping/Raven/ArchivedGroupsViewIndex.cs +++ b/src/ServiceControl.Persistence.RavenDb/Indexes/ArchivedGroupsViewIndex.cs @@ -4,7 +4,7 @@ namespace ServiceControl.Recoverability using MessageFailures; using Raven.Client.Indexes; - public class ArchivedGroupsViewIndex : AbstractIndexCreationTask + class ArchivedGroupsViewIndex : AbstractIndexCreationTask { public ArchivedGroupsViewIndex() { diff --git a/src/ServiceControl.Persistence.RavenDb/Indexes/CustomChecksIndex.cs b/src/ServiceControl.Persistence.RavenDb/Indexes/CustomChecksIndex.cs index 963a4fa465..83a96221c4 100644 --- a/src/ServiceControl.Persistence.RavenDb/Indexes/CustomChecksIndex.cs +++ b/src/ServiceControl.Persistence.RavenDb/Indexes/CustomChecksIndex.cs @@ -4,7 +4,7 @@ using Raven.Client.Indexes; using ServiceControl.Contracts.CustomChecks; - public class CustomChecksIndex : AbstractIndexCreationTask + class CustomChecksIndex : AbstractIndexCreationTask { public CustomChecksIndex() { diff --git a/src/ServiceControl.Persistence.RavenDb/Indexes/ExpirySagaAuditIndex.cs b/src/ServiceControl.Persistence.RavenDb/Indexes/ExpirySagaAuditIndex.cs index 0586841112..fb6e83706c 100644 --- a/src/ServiceControl.Persistence.RavenDb/Indexes/ExpirySagaAuditIndex.cs +++ b/src/ServiceControl.Persistence.RavenDb/Indexes/ExpirySagaAuditIndex.cs @@ -4,7 +4,7 @@ namespace ServiceControl.SagaAudit using System.Linq; using Raven.Client.Indexes; - public class ExpirySagaAuditIndex : AbstractMultiMapIndexCreationTask + class ExpirySagaAuditIndex : AbstractMultiMapIndexCreationTask { public ExpirySagaAuditIndex() { diff --git a/src/ServiceControl/Operations/FailedErrorImportIndex.cs b/src/ServiceControl.Persistence.RavenDb/Indexes/FailedErrorImportIndex.cs similarity index 84% rename from src/ServiceControl/Operations/FailedErrorImportIndex.cs rename to src/ServiceControl.Persistence.RavenDb/Indexes/FailedErrorImportIndex.cs index 4fcf117a60..0816d9ca4e 100644 --- a/src/ServiceControl/Operations/FailedErrorImportIndex.cs +++ b/src/ServiceControl.Persistence.RavenDb/Indexes/FailedErrorImportIndex.cs @@ -3,7 +3,7 @@ using System.Linq; using Raven.Client.Indexes; - public class FailedErrorImportIndex : AbstractIndexCreationTask + class FailedErrorImportIndex : AbstractIndexCreationTask { public FailedErrorImportIndex() { diff --git a/src/ServiceControl/MessageFailures/Api/FailedMessageFacetsIndex.cs b/src/ServiceControl.Persistence.RavenDb/Indexes/FailedMessageFacetsIndex.cs similarity index 92% rename from src/ServiceControl/MessageFailures/Api/FailedMessageFacetsIndex.cs rename to src/ServiceControl.Persistence.RavenDb/Indexes/FailedMessageFacetsIndex.cs index 409311f245..6ecba0affc 100644 --- a/src/ServiceControl/MessageFailures/Api/FailedMessageFacetsIndex.cs +++ b/src/ServiceControl.Persistence.RavenDb/Indexes/FailedMessageFacetsIndex.cs @@ -5,7 +5,7 @@ using Raven.Client.Indexes; using ServiceControl.Operations; - public class FailedMessageFacetsIndex : AbstractIndexCreationTask + class FailedMessageFacetsIndex : AbstractIndexCreationTask { public FailedMessageFacetsIndex() { diff --git a/src/ServiceControl.Persistence.RavenDb/Indexes/FailedMessageRetries_ByBatch.cs b/src/ServiceControl.Persistence.RavenDb/Indexes/FailedMessageRetries_ByBatch.cs index c3752dde7d..2ff6a5d4ba 100644 --- a/src/ServiceControl.Persistence.RavenDb/Indexes/FailedMessageRetries_ByBatch.cs +++ b/src/ServiceControl.Persistence.RavenDb/Indexes/FailedMessageRetries_ByBatch.cs @@ -4,7 +4,7 @@ namespace ServiceControl.Persistence using Raven.Client.Indexes; using ServiceControl.Recoverability; - public class FailedMessageRetries_ByBatch : AbstractIndexCreationTask + class FailedMessageRetries_ByBatch : AbstractIndexCreationTask { public FailedMessageRetries_ByBatch() { diff --git a/src/ServiceControl/MessageFailures/Api/FailedMessageViewIndex.cs b/src/ServiceControl.Persistence.RavenDb/Indexes/FailedMessageViewIndex.cs similarity index 95% rename from src/ServiceControl/MessageFailures/Api/FailedMessageViewIndex.cs rename to src/ServiceControl.Persistence.RavenDb/Indexes/FailedMessageViewIndex.cs index b3aa55a0ca..11e3514eec 100644 --- a/src/ServiceControl/MessageFailures/Api/FailedMessageViewIndex.cs +++ b/src/ServiceControl.Persistence.RavenDb/Indexes/FailedMessageViewIndex.cs @@ -5,7 +5,7 @@ namespace ServiceControl.MessageFailures.Api using Raven.Client.Indexes; using ServiceControl.Operations; - public class FailedMessageViewIndex : AbstractIndexCreationTask + class FailedMessageViewIndex : AbstractIndexCreationTask { public FailedMessageViewIndex() { diff --git a/src/ServiceControl/Recoverability/Grouping/Raven/FailedMessages_ByGroup.cs b/src/ServiceControl.Persistence.RavenDb/Indexes/FailedMessages_ByGroup.cs similarity index 92% rename from src/ServiceControl/Recoverability/Grouping/Raven/FailedMessages_ByGroup.cs rename to src/ServiceControl.Persistence.RavenDb/Indexes/FailedMessages_ByGroup.cs index 7c6f872210..d7b96da4d2 100644 --- a/src/ServiceControl/Recoverability/Grouping/Raven/FailedMessages_ByGroup.cs +++ b/src/ServiceControl.Persistence.RavenDb/Indexes/FailedMessages_ByGroup.cs @@ -6,7 +6,7 @@ namespace ServiceControl.Recoverability using Raven.Abstractions.Indexing; using Raven.Client.Indexes; - public class FailedMessages_ByGroup : AbstractIndexCreationTask + class FailedMessages_ByGroup : AbstractIndexCreationTask { public FailedMessages_ByGroup() { diff --git a/src/ServiceControl/Recoverability/Grouping/Raven/FailureGroupsViewIndex.cs b/src/ServiceControl.Persistence.RavenDb/Indexes/FailureGroupsViewIndex.cs similarity index 94% rename from src/ServiceControl/Recoverability/Grouping/Raven/FailureGroupsViewIndex.cs rename to src/ServiceControl.Persistence.RavenDb/Indexes/FailureGroupsViewIndex.cs index 1f172b5f19..47f2402d7c 100644 --- a/src/ServiceControl/Recoverability/Grouping/Raven/FailureGroupsViewIndex.cs +++ b/src/ServiceControl.Persistence.RavenDb/Indexes/FailureGroupsViewIndex.cs @@ -4,7 +4,7 @@ namespace ServiceControl.Recoverability using MessageFailures; using Raven.Client.Indexes; - public class FailureGroupsViewIndex : AbstractIndexCreationTask + class FailureGroupsViewIndex : AbstractIndexCreationTask { public FailureGroupsViewIndex() { diff --git a/src/ServiceControl/Recoverability/Grouping/Raven/GroupCommentIndex.cs b/src/ServiceControl.Persistence.RavenDb/Indexes/GroupCommentIndex.cs similarity index 81% rename from src/ServiceControl/Recoverability/Grouping/Raven/GroupCommentIndex.cs rename to src/ServiceControl.Persistence.RavenDb/Indexes/GroupCommentIndex.cs index bce49c6a8a..e5b86901c6 100644 --- a/src/ServiceControl/Recoverability/Grouping/Raven/GroupCommentIndex.cs +++ b/src/ServiceControl.Persistence.RavenDb/Indexes/GroupCommentIndex.cs @@ -4,7 +4,7 @@ namespace ServiceControl.Recoverability using MessageFailures; using Raven.Client.Indexes; - public class GroupCommentIndex : AbstractIndexCreationTask + class GroupCommentIndex : AbstractIndexCreationTask { public GroupCommentIndex() { diff --git a/src/ServiceControl/Operations/ImportFailedAudits.cs b/src/ServiceControl.Persistence.RavenDb/Indexes/ImportFailedAudits.cs similarity index 84% rename from src/ServiceControl/Operations/ImportFailedAudits.cs rename to src/ServiceControl.Persistence.RavenDb/Indexes/ImportFailedAudits.cs index a0bdeb8e65..10b8970b81 100644 --- a/src/ServiceControl/Operations/ImportFailedAudits.cs +++ b/src/ServiceControl.Persistence.RavenDb/Indexes/ImportFailedAudits.cs @@ -3,7 +3,7 @@ namespace ServiceControl.Operations using System.Linq; using Raven.Client.Indexes; - public class FailedAuditImportIndex : AbstractIndexCreationTask + class FailedAuditImportIndex : AbstractIndexCreationTask { public FailedAuditImportIndex() { diff --git a/src/ServiceControl.Persistence.RavenDb/Indexes/KnownEndpointIndex.cs b/src/ServiceControl.Persistence.RavenDb/Indexes/KnownEndpointIndex.cs index 55cfa985cc..297724d957 100644 --- a/src/ServiceControl.Persistence.RavenDb/Indexes/KnownEndpointIndex.cs +++ b/src/ServiceControl.Persistence.RavenDb/Indexes/KnownEndpointIndex.cs @@ -3,7 +3,7 @@ namespace ServiceControl.Persistence using System.Linq; using Raven.Client.Indexes; - public class KnownEndpointIndex : AbstractIndexCreationTask + class KnownEndpointIndex : AbstractIndexCreationTask { public KnownEndpointIndex() { diff --git a/src/ServiceControl.Persistence.RavenDb/Indexes/MessagesViewIndex.cs b/src/ServiceControl.Persistence.RavenDb/Indexes/MessagesViewIndex.cs index 3f3fa0e75d..3b3acd9476 100644 --- a/src/ServiceControl.Persistence.RavenDb/Indexes/MessagesViewIndex.cs +++ b/src/ServiceControl.Persistence.RavenDb/Indexes/MessagesViewIndex.cs @@ -9,7 +9,7 @@ namespace ServiceControl.Persistence using ServiceControl.MessageFailures; using ServiceControl.Operations; - public class MessagesViewIndex : AbstractMultiMapIndexCreationTask + class MessagesViewIndex : AbstractMultiMapIndexCreationTask { public MessagesViewIndex() { diff --git a/src/ServiceControl/MessageFailures/Api/QueueAddressIndex.cs b/src/ServiceControl.Persistence.RavenDb/Indexes/QueueAddressIndex.cs similarity index 91% rename from src/ServiceControl/MessageFailures/Api/QueueAddressIndex.cs rename to src/ServiceControl.Persistence.RavenDb/Indexes/QueueAddressIndex.cs index 4352589105..5a31449809 100644 --- a/src/ServiceControl/MessageFailures/Api/QueueAddressIndex.cs +++ b/src/ServiceControl.Persistence.RavenDb/Indexes/QueueAddressIndex.cs @@ -3,7 +3,7 @@ namespace ServiceControl.MessageFailures.Api using System.Linq; using Raven.Client.Indexes; - public class QueueAddressIndex : AbstractIndexCreationTask + class QueueAddressIndex : AbstractIndexCreationTask { public QueueAddressIndex() { diff --git a/src/ServiceControl.Persistence.RavenDb/Indexes/RetryBatches_ByStatusAndSession.cs b/src/ServiceControl.Persistence.RavenDb/Indexes/RetryBatches_ByStatusAndSession.cs index 940a2ea535..70ad6da482 100644 --- a/src/ServiceControl.Persistence.RavenDb/Indexes/RetryBatches_ByStatusAndSession.cs +++ b/src/ServiceControl.Persistence.RavenDb/Indexes/RetryBatches_ByStatusAndSession.cs @@ -3,7 +3,7 @@ namespace ServiceControl.Persistence using System.Linq; using Raven.Client.Indexes; - public class RetryBatches_ByStatusAndSession : AbstractIndexCreationTask + class RetryBatches_ByStatusAndSession : AbstractIndexCreationTask { public RetryBatches_ByStatusAndSession() { diff --git a/src/ServiceControl.Persistence.RavenDb/Indexes/RetryBatches_ByStatus_ReduceInitialBatchSize.cs b/src/ServiceControl.Persistence.RavenDb/Indexes/RetryBatches_ByStatus_ReduceInitialBatchSize.cs index d78e428d61..c20490177b 100644 --- a/src/ServiceControl.Persistence.RavenDb/Indexes/RetryBatches_ByStatus_ReduceInitialBatchSize.cs +++ b/src/ServiceControl.Persistence.RavenDb/Indexes/RetryBatches_ByStatus_ReduceInitialBatchSize.cs @@ -3,7 +3,7 @@ namespace ServiceControl.Persistence using System.Linq; using Raven.Client.Indexes; - public class RetryBatches_ByStatus_ReduceInitialBatchSize : AbstractIndexCreationTask + class RetryBatches_ByStatus_ReduceInitialBatchSize : AbstractIndexCreationTask { public RetryBatches_ByStatus_ReduceInitialBatchSize() { diff --git a/src/ServiceControl.Persistence.RavenDb/Indexes/SagaDetailsIndex.cs b/src/ServiceControl.Persistence.RavenDb/Indexes/SagaDetailsIndex.cs index 82005b13ac..36ecd37a53 100644 --- a/src/ServiceControl.Persistence.RavenDb/Indexes/SagaDetailsIndex.cs +++ b/src/ServiceControl.Persistence.RavenDb/Indexes/SagaDetailsIndex.cs @@ -3,7 +3,7 @@ namespace ServiceControl.SagaAudit using System.Linq; using Raven.Client.Indexes; - public class SagaDetailsIndex : AbstractMultiMapIndexCreationTask + class SagaDetailsIndex : AbstractMultiMapIndexCreationTask { public SagaDetailsIndex() { diff --git a/src/ServiceControl.Persistence.RavenDb/Indexes/SagaHistoryCleaner.cs b/src/ServiceControl.Persistence.RavenDb/Indexes/SagaHistoryCleaner.cs index e4e4686789..e0b6782807 100644 --- a/src/ServiceControl.Persistence.RavenDb/Indexes/SagaHistoryCleaner.cs +++ b/src/ServiceControl.Persistence.RavenDb/Indexes/SagaHistoryCleaner.cs @@ -13,7 +13,7 @@ using Raven.Database; using ServiceControl.Infrastructure.RavenDB; - public static class SagaHistoryCleaner + static class SagaHistoryCleaner { public static void Clean(int deletionBatchSize, DocumentDatabase database, DateTime expiryThreshold, CancellationToken cancellationToken = default) { diff --git a/src/ServiceControl/Infrastructure/RavenDB/MigratedTypeAwareBinder.cs b/src/ServiceControl.Persistence.RavenDb/Infrastructure/MigratedTypeAwareBinder.cs similarity index 100% rename from src/ServiceControl/Infrastructure/RavenDB/MigratedTypeAwareBinder.cs rename to src/ServiceControl.Persistence.RavenDb/Infrastructure/MigratedTypeAwareBinder.cs diff --git a/src/ServiceControl/Infrastructure/RavenDB/IDataMigration.cs b/src/ServiceControl.Persistence.RavenDb/Infrastructure/Migrations/IDataMigration.cs similarity index 82% rename from src/ServiceControl/Infrastructure/RavenDB/IDataMigration.cs rename to src/ServiceControl.Persistence.RavenDb/Infrastructure/Migrations/IDataMigration.cs index 3a48d51adc..006e2e7c8f 100644 --- a/src/ServiceControl/Infrastructure/RavenDB/IDataMigration.cs +++ b/src/ServiceControl.Persistence.RavenDb/Infrastructure/Migrations/IDataMigration.cs @@ -3,7 +3,7 @@ using System.Threading.Tasks; using Raven.Client; - public interface IDataMigration + interface IDataMigration { Task Migrate(IDocumentStore store); } diff --git a/src/ServiceControl/Infrastructure/RavenDB/PurgeKnownEndpointsWithTemporaryIdsThatAreDuplicateDataMigration.cs b/src/ServiceControl.Persistence.RavenDb/Infrastructure/Migrations/PurgeKnownEndpointsWithTemporaryIdsThatAreDuplicateDataMigration.cs similarity index 100% rename from src/ServiceControl/Infrastructure/RavenDB/PurgeKnownEndpointsWithTemporaryIdsThatAreDuplicateDataMigration.cs rename to src/ServiceControl.Persistence.RavenDb/Infrastructure/Migrations/PurgeKnownEndpointsWithTemporaryIdsThatAreDuplicateDataMigration.cs diff --git a/src/ServiceControl/Infrastructure/RavenDB/Subscriptions/LegacyAddress.cs b/src/ServiceControl.Persistence.RavenDb/Infrastructure/Subscriptions/LegacyAddress.cs similarity index 100% rename from src/ServiceControl/Infrastructure/RavenDB/Subscriptions/LegacyAddress.cs rename to src/ServiceControl.Persistence.RavenDb/Infrastructure/Subscriptions/LegacyAddress.cs diff --git a/src/ServiceControl/Infrastructure/RavenDB/Subscriptions/MessageTypeConverter.cs b/src/ServiceControl.Persistence.RavenDb/Infrastructure/Subscriptions/MessageTypeConverter.cs similarity index 100% rename from src/ServiceControl/Infrastructure/RavenDB/Subscriptions/MessageTypeConverter.cs rename to src/ServiceControl.Persistence.RavenDb/Infrastructure/Subscriptions/MessageTypeConverter.cs diff --git a/src/ServiceControl/Infrastructure/RavenDB/Subscriptions/SubscriptionPersister.cs b/src/ServiceControl.Persistence.RavenDb/Infrastructure/Subscriptions/RavenDbSubscriptionStorage.cs similarity index 84% rename from src/ServiceControl/Infrastructure/RavenDB/Subscriptions/SubscriptionPersister.cs rename to src/ServiceControl.Persistence.RavenDb/Infrastructure/Subscriptions/RavenDbSubscriptionStorage.cs index 6e92a4b9e0..a64bd817c3 100644 --- a/src/ServiceControl/Infrastructure/RavenDB/Subscriptions/SubscriptionPersister.cs +++ b/src/ServiceControl.Persistence.RavenDb/Infrastructure/Subscriptions/RavenDbSubscriptionStorage.cs @@ -14,15 +14,17 @@ using NServiceBus.Unicast.Subscriptions; using NServiceBus.Unicast.Subscriptions.MessageDrivenSubscriptions; using Raven.Client; + using ServiceControl.Persistence; + using ServiceControl.Persistence.RavenDb.Infrastructure; - class SubscriptionPersister : ISubscriptionStorage, IPrimableSubscriptionStorage + class RavenDbSubscriptionStorage : IServiceControlSubscriptionStorage { - public SubscriptionPersister(IDocumentStore store, ReadOnlySettings settings) : + public RavenDbSubscriptionStorage(IDocumentStore store, ReadOnlySettings settings) : this(store, settings.EndpointName(), settings.LocalAddress(), settings.GetAvailableTypes().Implementing().Select(e => new MessageType(e)).ToArray()) { } - public SubscriptionPersister(IDocumentStore store, string endpointName, string localAddress, MessageType[] locallyHandledEventTypes) + public RavenDbSubscriptionStorage(IDocumentStore store, string endpointName, string localAddress, MessageType[] locallyHandledEventTypes) { this.store = store; localClient = new SubscriptionClient @@ -37,14 +39,13 @@ public SubscriptionPersister(IDocumentStore store, string endpointName, string l SetSubscriptions(new Subscriptions()).GetAwaiter().GetResult(); } - public async Task Prime() + public async Task Initialize() { using (var session = store.OpenAsyncSession()) { - var primeSubscriptions = await LoadSubscriptions(session).ConfigureAwait(false) ?? await MigrateSubscriptions(session, localClient).ConfigureAwait(false); + var primeSubscriptions = await LoadSubscriptions(session) ?? await MigrateSubscriptions(session, localClient); - await SetSubscriptions(primeSubscriptions) - .ConfigureAwait(false); + await SetSubscriptions(primeSubscriptions); } } @@ -57,11 +58,11 @@ public async Task Subscribe(Subscriber subscriber, MessageType messageType, Cont try { - await subscriptionsLock.WaitAsync().ConfigureAwait(false); + await subscriptionsLock.WaitAsync(); if (AddOrUpdateSubscription(messageType, subscriber)) { - await SaveSubscriptions().ConfigureAwait(false); + await SaveSubscriptions(); } } finally @@ -74,7 +75,7 @@ public async Task Unsubscribe(Subscriber subscriber, MessageType messageType, Co { try { - await subscriptionsLock.WaitAsync().ConfigureAwait(false); + await subscriptionsLock.WaitAsync(); var needsSave = false; if (subscriptions.All.TryGetValue(FormatId(messageType), out var subscription)) @@ -88,7 +89,7 @@ public async Task Unsubscribe(Subscriber subscriber, MessageType messageType, Co if (needsSave) { - await SaveSubscriptions().ConfigureAwait(false); + await SaveSubscriptions(); } } finally @@ -149,10 +150,9 @@ async Task SaveSubscriptions() { using (var session = store.OpenAsyncSession()) { - await session.StoreAsync(subscriptions, Subscriptions.SingleDocumentId) - .ConfigureAwait(false); + await session.StoreAsync(subscriptions, Subscriptions.SingleDocumentId); UpdateLookup(); - await session.SaveChangesAsync().ConfigureAwait(false); + await session.SaveChangesAsync(); } } @@ -191,8 +191,7 @@ async Task SetSubscriptions(Subscriptions newSubscriptions) { try { - await subscriptionsLock.WaitAsync() - .ConfigureAwait(false); + await subscriptionsLock.WaitAsync(); subscriptions = newSubscriptions; UpdateLookup(); @@ -213,20 +212,19 @@ static async Task MigrateSubscriptions(IAsyncDocumentSession sess var subscriptions = new Subscriptions(); using (var stream = await session.Advanced.StreamAsync("Subscriptions") - .ConfigureAwait(false)) + ) { - while (await stream.MoveNextAsync().ConfigureAwait(false)) + while (await stream.MoveNextAsync()) { var existingSubscription = stream.Current.Document; existingSubscription.Subscribers.Remove(localClient); subscriptions.All.Add(existingSubscription.Id.Replace("Subscriptions/", string.Empty), existingSubscription); - await session.Advanced.DocumentStore.AsyncDatabaseCommands.DeleteAsync(stream.Current.Key, null) - .ConfigureAwait(false); + await session.Advanced.DocumentStore.AsyncDatabaseCommands.DeleteAsync(stream.Current.Key, null); } } - await session.StoreAsync(subscriptions, Subscriptions.SingleDocumentId).ConfigureAwait(false); - await session.SaveChangesAsync().ConfigureAwait(false); + await session.StoreAsync(subscriptions, Subscriptions.SingleDocumentId); + await session.SaveChangesAsync(); return subscriptions; } @@ -238,7 +236,7 @@ await session.Advanced.DocumentStore.AsyncDatabaseCommands.DeleteAsync(stream.Cu SemaphoreSlim subscriptionsLock = new SemaphoreSlim(1); - static ILog logger = LogManager.GetLogger(); + static ILog logger = LogManager.GetLogger(); } class Subscriptions diff --git a/src/ServiceControl/Infrastructure/RavenDB/Subscriptions/Subscription.cs b/src/ServiceControl.Persistence.RavenDb/Infrastructure/Subscriptions/Subscription.cs similarity index 100% rename from src/ServiceControl/Infrastructure/RavenDB/Subscriptions/Subscription.cs rename to src/ServiceControl.Persistence.RavenDb/Infrastructure/Subscriptions/Subscription.cs diff --git a/src/ServiceControl/Infrastructure/RavenDB/Subscriptions/SubscriptionClient.cs b/src/ServiceControl.Persistence.RavenDb/Infrastructure/Subscriptions/SubscriptionClient.cs similarity index 100% rename from src/ServiceControl/Infrastructure/RavenDB/Subscriptions/SubscriptionClient.cs rename to src/ServiceControl.Persistence.RavenDb/Infrastructure/Subscriptions/SubscriptionClient.cs diff --git a/src/ServiceControl/Infrastructure/RavenDB/Subscriptions/SubscriptionsLegacyAddressConverter.cs b/src/ServiceControl.Persistence.RavenDb/Infrastructure/Subscriptions/SubscriptionsLegacyAddressConverter.cs similarity index 100% rename from src/ServiceControl/Infrastructure/RavenDB/Subscriptions/SubscriptionsLegacyAddressConverter.cs rename to src/ServiceControl.Persistence.RavenDb/Infrastructure/Subscriptions/SubscriptionsLegacyAddressConverter.cs diff --git a/src/ServiceControl.Persistence.RavenDb/Infrastructure/TypeExtensions.cs b/src/ServiceControl.Persistence.RavenDb/Infrastructure/TypeExtensions.cs new file mode 100644 index 0000000000..20c25c6d90 --- /dev/null +++ b/src/ServiceControl.Persistence.RavenDb/Infrastructure/TypeExtensions.cs @@ -0,0 +1,16 @@ +namespace ServiceControl.Persistence.RavenDb.Infrastructure +{ + using System; + using System.Collections.Generic; + using System.Linq; + + static class TypeExtensions + { + public static IEnumerable Implementing(this IEnumerable types) + { + return from type in types + where typeof(T).IsAssignableFrom(type) && !type.IsAbstract && !type.IsInterface + select type; + } + } +} \ No newline at end of file diff --git a/src/ServiceControl.Persistence.RavenDb/InternalsVisibleTo.cs b/src/ServiceControl.Persistence.RavenDb/InternalsVisibleTo.cs index 92194e8183..f923d470ad 100644 --- a/src/ServiceControl.Persistence.RavenDb/InternalsVisibleTo.cs +++ b/src/ServiceControl.Persistence.RavenDb/InternalsVisibleTo.cs @@ -1,3 +1,6 @@ using System.Runtime.CompilerServices; [assembly: InternalsVisibleTo("ServiceControl.UnitTests")] +[assembly: InternalsVisibleTo("ServiceControl.PersistenceTests")] +[assembly: InternalsVisibleTo("ServiceControl.Persistence.Tests.RavenDb")] +[assembly: InternalsVisibleTo("ServiceControl.AcceptanceTests.RavenDB")] diff --git a/src/ServiceControl.Persistence.RavenDb/MessageRedirects/MessageRedirectsDataStore.cs b/src/ServiceControl.Persistence.RavenDb/MessageRedirects/MessageRedirectsDataStore.cs new file mode 100644 index 0000000000..842342cb8d --- /dev/null +++ b/src/ServiceControl.Persistence.RavenDb/MessageRedirects/MessageRedirectsDataStore.cs @@ -0,0 +1,46 @@ +namespace ServiceControl.Persistence.RavenDb.MessageRedirects +{ + using System; + using System.Threading.Tasks; + using Raven.Client; + using ServiceControl.Persistence.MessageRedirects; + + class MessageRedirectsDataStore : IMessageRedirectsDataStore + { + readonly IDocumentStore store; + + public MessageRedirectsDataStore(IDocumentStore store) + { + this.store = store; + } + + public async Task GetOrCreate() + { + using (var session = store.OpenAsyncSession()) + { + var redirects = await session.LoadAsync(DefaultId); + + if (redirects != null) + { + redirects.ETag = session.Advanced.GetEtagFor(redirects); + redirects.LastModified = session.Advanced.GetMetadataFor(redirects).Value("Last-Modified"); + + return redirects; + } + + return new MessageRedirectsCollection(); + } + } + + public async Task Save(MessageRedirectsCollection redirects) + { + using (var session = store.OpenAsyncSession()) + { + await session.StoreAsync(redirects, redirects.ETag, DefaultId); + await session.SaveChangesAsync(); + } + } + + const string DefaultId = "messageredirects"; + } +} diff --git a/src/ServiceControl.Persistence.RavenDb/MetadataExtensions.cs b/src/ServiceControl.Persistence.RavenDb/MetadataExtensions.cs new file mode 100644 index 0000000000..ddfb3ce644 --- /dev/null +++ b/src/ServiceControl.Persistence.RavenDb/MetadataExtensions.cs @@ -0,0 +1,40 @@ +namespace ServiceControl +{ + using System; + using System.Collections.Generic; + + static class MetadataExtensions + { + public static T GetOrDefault(this IDictionary metadata, string key) + { + if (metadata.TryGetValue(key, out var foundValue)) + { + return (T)foundValue; + } + + return default; + } + + public static string GetAsStringOrNull(this IDictionary metadata, string key) + { + if (metadata.TryGetValue(key, out var foundValue)) + { + return foundValue?.ToString(); + } + + return null; + } + + public static DateTime? GetAsNullableDatetime(this IDictionary metadata, string key) + { + var datetimeAsString = metadata.GetAsStringOrNull(key); + + if (datetimeAsString != null) + { + return DateTime.Parse(datetimeAsString); + } + + return null; + } + } +} \ No newline at end of file diff --git a/src/ServiceControl.Persistence.RavenDb/QueueAddressStore.cs b/src/ServiceControl.Persistence.RavenDb/QueueAddressStore.cs new file mode 100644 index 0000000000..c951bb94d7 --- /dev/null +++ b/src/ServiceControl.Persistence.RavenDb/QueueAddressStore.cs @@ -0,0 +1,53 @@ +namespace ServiceControl.Persistence.RavenDb +{ + using System.Collections.Generic; + using System.Linq; + using System.Threading.Tasks; + using Raven.Client; + using Raven.Client.Linq; + using ServiceControl.MessageFailures; + using ServiceControl.MessageFailures.Api; + using ServiceControl.Persistence.Infrastructure; + + class QueueAddressStore : IQueueAddressStore + { + readonly IDocumentStore documentStore; + + public QueueAddressStore(IDocumentStore documentStore) + { + this.documentStore = documentStore; + } + + public async Task>> GetAddresses(PagingInfo pagingInfo) + { + using (var session = documentStore.OpenAsyncSession()) + { + var addresses = await session + .Query() + .Statistics(out var stats) + .Paging(pagingInfo) + .ToListAsync(); + + var result = new QueryResult>(addresses, stats.ToQueryStatsInfo()); + return result; + } + } + + public async Task>> GetAddressesBySearchTerm(string search, PagingInfo pagingInfo) + { + using (var session = documentStore.OpenAsyncSession()) + { + var failedMessageQueues = await session + .Query() + .Statistics(out var stats) + .Where(q => q.PhysicalAddress.StartsWith(search)) + .OrderBy(q => q.PhysicalAddress) + .Paging(pagingInfo) + .ToListAsync(); + + var result = new QueryResult>(failedMessageQueues, stats.ToQueryStatsInfo()); + return result; + } + } + } +} \ No newline at end of file diff --git a/src/ServiceControl.Persistence.RavenDb/RavenAttachmentsBodyStorage.cs b/src/ServiceControl.Persistence.RavenDb/RavenAttachmentsBodyStorage.cs new file mode 100644 index 0000000000..8fdc936d17 --- /dev/null +++ b/src/ServiceControl.Persistence.RavenDb/RavenAttachmentsBodyStorage.cs @@ -0,0 +1,112 @@ +namespace ServiceControl.Operations.BodyStorage.RavenAttachments +{ + using System; + using System.IO; + using System.Threading.Tasks; + using Raven.Client; + using Raven.Json.Linq; + using ServiceControl.CompositeViews.Messages; + using ServiceControl.Persistence; + + // For Raven5, look at how the Audit instance is implementing this, as Attachments won't exist + // and there will be no need for a fallback method on a new persistence + class RavenAttachmentsBodyStorage : IBodyStorage + { + readonly IDocumentStore documentStore; + + public RavenAttachmentsBodyStorage(IDocumentStore documentStore) + { + this.documentStore = documentStore; + } + + public Task Store(string bodyId, string contentType, int bodySize, Stream bodyStream) + { + var id = MessageBodyIdGenerator.MakeDocumentId(bodyId); + //We want to continue using attachments for now +#pragma warning disable 618 + return documentStore.AsyncDatabaseCommands.PutAttachmentAsync(id, null, bodyStream, + new RavenJObject +#pragma warning restore 618 + { + {"ContentType", contentType}, + {"ContentLength", bodySize} + }); + } + + // The RavenDB5 implementation, like the Audit instance, will not use Attachments + public async Task TryFetch(string bodyId) + { + bodyId = bodyId?.Replace("/", @"\"); + + // First try loading from index + + using (var session = documentStore.OpenAsyncSession()) + { + var message = await session.Query() + .Statistics(out var stats) + .TransformWith() + .FirstOrDefaultAsync(f => f.MessageId == bodyId); + + if (message != null) + { + if (message.Body == null && message.BodyNotStored) + { + return new MessageBodyStreamResult { HasResult = false }; + } + + if (message.Body != null) + { + var stream = new MemoryStream(); + var writer = new StreamWriter(stream); + // ReSharper disable once MethodHasAsyncOverload, operations are MemoryStream so do not require async overloads + writer.Write(message.Body); + // ReSharper disable once MethodHasAsyncOverload, operations are MemoryStream so do not require async overloads + writer.Flush(); + stream.Position = 0; + + return new MessageBodyStreamResult + { + HasResult = true, + Stream = stream, + ContentType = message.ContentType, + BodySize = message.BodySize, + Etag = stats.IndexEtag + }; + } + else if (message.BodySize == 0) + { + return new MessageBodyStreamResult + { + HasResult = true, + Stream = new MemoryStream(Array.Empty()), + ContentType = message.ContentType, + BodySize = message.BodySize, + Etag = stats.IndexEtag + }; + } + } + } + + var id = MessageBodyIdGenerator.MakeDocumentId(bodyId); + + //We want to continue using attachments for now +#pragma warning disable 618 + var attachment = await documentStore.AsyncDatabaseCommands.GetAttachmentAsync(id); +#pragma warning restore 618 + + if (attachment != null) + { + return new MessageBodyStreamResult + { + HasResult = true, + Stream = (MemoryStream)attachment.Data(), + ContentType = attachment.Metadata["ContentType"].Value(), + BodySize = attachment.Metadata["ContentLength"].Value(), + Etag = attachment.Etag + }; + } + + return null; + } + } +} \ No newline at end of file diff --git a/src/ServiceControl.Persistence.RavenDb/RavenBootstrapper.cs b/src/ServiceControl.Persistence.RavenDb/RavenBootstrapper.cs new file mode 100644 index 0000000000..ea8058d164 --- /dev/null +++ b/src/ServiceControl.Persistence.RavenDb/RavenBootstrapper.cs @@ -0,0 +1,160 @@ +namespace ServiceControl.Persistence.RavenDb +{ + using System; + using System.ComponentModel.Composition.Hosting; + using System.IO; + using System.Runtime.Serialization; + using NServiceBus.Logging; + using Raven.Abstractions.Data; + using Raven.Client.Document; + using Raven.Client.Embedded; + using ServiceControl.Infrastructure.RavenDB; + using ServiceControl.Infrastructure.RavenDB.Subscriptions; + + static class RavenBootstrapper + { + public const string DatabasePathKey = "DbPath"; + public const string HostNameKey = "HostName"; + public const string DatabaseMaintenancePortKey = "DatabaseMaintenancePort"; + public const string ExposeRavenDBKey = "ExposeRavenDB"; + public const string ExpirationProcessTimerInSecondsKey = "ExpirationProcessTimerInSeconds"; + public const string ExpirationProcessBatchSizeKey = "ExpirationProcessBatchSize"; + public const string RunCleanupBundleKey = "RavenDB35/RunCleanupBundle"; + public const string RunInMemoryKey = "RavenDB35/RunInMemory"; + public const string MinimumStorageLeftRequiredForIngestionKey = "MinimumStorageLeftRequiredForIngestion"; + + + public static PersistenceSettings Settings { get; private set; } + + public static void Configure(EmbeddableDocumentStore documentStore, PersistenceSettings settings) + { + Settings = settings; + + var runInMemory = false; + if (settings.PersisterSpecificSettings.TryGetValue(RunInMemoryKey, out var runInMemoryString)) + { + runInMemory = bool.Parse(runInMemoryString); + } + + if (runInMemory) + { + documentStore.RunInMemory = true; + } + else + { + if (!settings.PersisterSpecificSettings.TryGetValue(DatabasePathKey, out string dbPath)) + { + throw new InvalidOperationException($"{DatabasePathKey} is mandatory"); + } + + Directory.CreateDirectory(dbPath); + + documentStore.DataDirectory = dbPath; + documentStore.Configuration.CompiledIndexCacheDirectory = dbPath; + documentStore.Listeners.RegisterListener(new SubscriptionsLegacyAddressConverter()); + } + + var exposeRavenDB = false; + if (settings.PersisterSpecificSettings.TryGetValue(ExposeRavenDBKey, out var exposeRavenDBString)) + { + exposeRavenDB = bool.Parse(exposeRavenDBString); + } + + documentStore.UseEmbeddedHttpServer = settings.MaintenanceMode || exposeRavenDB; + documentStore.EnlistInDistributedTransactions = false; + + var localRavenLicense = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "RavenLicense.xml"); + if (File.Exists(localRavenLicense)) + { + Logger.InfoFormat("Loading RavenDB license found from {0}", localRavenLicense); + documentStore.Configuration.Settings["Raven/License"] = ReadAllTextWithoutLocking(localRavenLicense); + } + else + { + Logger.InfoFormat("Loading Embedded RavenDB license"); + documentStore.Configuration.Settings["Raven/License"] = ReadLicense(); + } + + //This is affects only remote access to the database in maintenance mode and enables access without authentication + documentStore.Configuration.Settings["Raven/AnonymousAccess"] = "Admin"; + documentStore.Configuration.Settings["Raven/Licensing/AllowAdminAnonymousAccessForCommercialUse"] = "true"; + + var runCleanupBundle = true; + if (settings.PersisterSpecificSettings.TryGetValue(RunCleanupBundleKey, out var runCleanupBundleString)) + { + runCleanupBundle = bool.Parse(runCleanupBundleString); + } + + if (runCleanupBundle) + { + documentStore.Configuration.Settings.Add("Raven/ActiveBundles", "CustomDocumentExpiration"); + } + + documentStore.Configuration.DisableClusterDiscovery = true; + documentStore.Configuration.ResetIndexOnUncleanShutdown = true; + + if (!settings.PersisterSpecificSettings.TryGetValue(DatabaseMaintenancePortKey, out var databaseMaintenancePort)) + { + throw new Exception($"{DatabaseMaintenancePortKey} is mandatory."); + } + + documentStore.Configuration.Port = int.Parse(databaseMaintenancePort); + + if (!settings.PersisterSpecificSettings.TryGetValue(HostNameKey, out var hostName)) + { + throw new Exception($"{HostNameKey} is mandatory."); + } + + documentStore.Configuration.HostName = hostName == "*" || hostName == "+" + ? "localhost" + : hostName; + documentStore.Conventions.SaveEnumsAsIntegers = true; + documentStore.Conventions.CustomizeJsonSerializer = serializer => serializer.Binder = MigratedTypeAwareBinder; + documentStore.Configuration.Catalog.Catalogs.Add(new AssemblyCatalog(typeof(RavenBootstrapper).Assembly)); + documentStore.Conventions.FindClrType = (id, doc, metadata) => + { + var clrtype = metadata.Value(Constants.RavenClrType); + + // The CLR type cannot be assumed to be always there + if (clrtype == null) + { + return null; + } + + if (clrtype.EndsWith(".Subscription, NServiceBus.Core")) + { + clrtype = ReflectionUtil.GetFullNameWithoutVersionInformation(typeof(Subscription)); + } + else if (clrtype.EndsWith(".Subscription, NServiceBus.RavenDB")) + { + clrtype = ReflectionUtil.GetFullNameWithoutVersionInformation(typeof(Subscription)); + } + + return clrtype; + }; + } + + public static string ReadLicense() + { + using (var resourceStream = typeof(RavenBootstrapper).Assembly.GetManifestResourceStream("ServiceControl.Persistence.RavenDb.RavenLicense.xml")) + using (var reader = new StreamReader(resourceStream)) + { + return reader.ReadToEnd(); + } + } + + static string ReadAllTextWithoutLocking(string path) + { + using (var fileStream = new FileStream(path, FileMode.Open, FileAccess.Read, FileShare.ReadWrite)) + using (var textReader = new StreamReader(fileStream)) + { + return textReader.ReadToEnd(); + } + } + + static SerializationBinder MigratedTypeAwareBinder = new MigratedTypeAwareBinder(); + + static readonly ILog Logger = LogManager.GetLogger(typeof(RavenBootstrapper)); + + } +} diff --git a/src/ServiceControl.Persistence.RavenDb/RavenDbCustomCheckDataStore.cs b/src/ServiceControl.Persistence.RavenDb/RavenDbCustomCheckDataStore.cs index 53ac919fba..483e3b3f01 100644 --- a/src/ServiceControl.Persistence.RavenDb/RavenDbCustomCheckDataStore.cs +++ b/src/ServiceControl.Persistence.RavenDb/RavenDbCustomCheckDataStore.cs @@ -4,11 +4,11 @@ using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; - using Infrastructure; using Raven.Client; using Raven.Client.Linq; using ServiceControl.Contracts.CustomChecks; using ServiceControl.Persistence; + using ServiceControl.Persistence.Infrastructure; class RavenDbCustomCheckDataStore : ICustomChecksDataStore { @@ -24,8 +24,7 @@ public async Task UpdateCustomCheckStatus(CustomCheckDetail de using (var session = store.OpenAsyncSession()) { - var customCheck = await session.LoadAsync(id) - .ConfigureAwait(false); + var customCheck = await session.LoadAsync(id); if (customCheck == null || (customCheck.Status == Status.Fail && !detail.HasFailed) || @@ -48,10 +47,8 @@ public async Task UpdateCustomCheckStatus(CustomCheckDetail de customCheck.ReportedAt = detail.ReportedAt; customCheck.FailureReason = detail.FailureReason; customCheck.OriginatingEndpoint = detail.OriginatingEndpoint; - await session.StoreAsync(customCheck) - .ConfigureAwait(false); - await session.SaveChangesAsync() - .ConfigureAwait(false); + await session.StoreAsync(customCheck); + await session.SaveChangesAsync(); } return status; @@ -68,17 +65,25 @@ public async Task>> GetStats(PagingInfo paging, s var results = await query .Paging(paging) - .ToListAsync() - .ConfigureAwait(false); + .ToListAsync(); - return new QueryResult>(results, new QueryStatsInfo(stats.IndexEtag, stats.TotalResults)); + return new QueryResult>(results, new QueryStatsInfo(stats.IndexEtag, stats.TotalResults, stats.IsStale)); } } public async Task DeleteCustomCheck(Guid id) { - await store.AsyncDatabaseCommands.DeleteAsync(store.Conventions.DefaultFindFullDocumentKeyFromNonStringIdentifier(id, typeof(CustomCheck), false), null) - .ConfigureAwait(false); + await store.AsyncDatabaseCommands.DeleteAsync(store.Conventions.DefaultFindFullDocumentKeyFromNonStringIdentifier(id, typeof(CustomCheck), false), null); + } + + public async Task GetNumberOfFailedChecks() + { + using (var session = store.OpenAsyncSession()) + { + var failedCustomCheckCount = await session.Query().CountAsync(p => p.Status == Status.Fail); + + return failedCustomCheckCount; + } } static IRavenQueryable AddStatusFilter(IRavenQueryable query, string status) diff --git a/src/ServiceControl.Persistence.RavenDb/RavenDbInstaller.cs b/src/ServiceControl.Persistence.RavenDb/RavenDbInstaller.cs new file mode 100644 index 0000000000..7a4c4562ee --- /dev/null +++ b/src/ServiceControl.Persistence.RavenDb/RavenDbInstaller.cs @@ -0,0 +1,48 @@ +namespace ServiceControl.Persistence.RavenDb +{ + using System.Threading; + using System.Threading.Tasks; + using NServiceBus.Logging; + using Raven.Client.Embedded; + + class RavenDbInstaller : IPersistenceInstaller + { + public RavenDbInstaller(EmbeddableDocumentStore documentStore, RavenStartup ravenStartup) + { + this.documentStore = documentStore; + this.ravenStartup = ravenStartup; + } + + public async Task Install(CancellationToken cancellationToken = default) + { + Logger.Info("Database initialization starting"); + documentStore.Initialize(); + Logger.Info("Database initialization complete"); + + await ravenStartup.CreateIndexesAsync(documentStore); + + Logger.Info("Data migrations starting **TODO NOT IMPLEMENTED YET**"); + + // TODO: Figure out migrations + // This was copied from audit code: + ////var endpointMigrations = new MigrateKnownEndpoints(documentStore); + ////await endpointMigrations.Migrate(cancellationToken: cancellationToken) + //// ; + // While this was copied from EmbeddedRavenDbHostedService in main ServiceControl project, but list was injected + ////foreach (var migration in dataMigrations) + ////{ + //// await migration.Migrate(documentStore) + //// ; + ////} + + Logger.Info("Data migrations complete"); + + } + + readonly EmbeddableDocumentStore documentStore; + readonly RavenStartup ravenStartup; + + static readonly ILog Logger = LogManager.GetLogger(typeof(RavenDbPersistenceLifecycle)); + + } +} diff --git a/src/ServiceControl.Persistence.RavenDb/RavenDbMonitoringDataStore.cs b/src/ServiceControl.Persistence.RavenDb/RavenDbMonitoringDataStore.cs index dfe7188922..f04a89718b 100644 --- a/src/ServiceControl.Persistence.RavenDb/RavenDbMonitoringDataStore.cs +++ b/src/ServiceControl.Persistence.RavenDb/RavenDbMonitoringDataStore.cs @@ -21,8 +21,7 @@ public async Task CreateIfNotExists(EndpointDetails endpoint) using (var session = store.OpenAsyncSession()) { - var knownEndpoint = await session.LoadAsync(id) - .ConfigureAwait(false); + var knownEndpoint = await session.LoadAsync(id); if (knownEndpoint != null) { @@ -37,10 +36,9 @@ public async Task CreateIfNotExists(EndpointDetails endpoint) Monitored = false }; - await session.StoreAsync(knownEndpoint).ConfigureAwait(false); + await session.StoreAsync(knownEndpoint); - await session.SaveChangesAsync() - .ConfigureAwait(false); + await session.SaveChangesAsync(); } } @@ -50,8 +48,7 @@ public async Task CreateOrUpdate(EndpointDetails endpoint, IEndpointInstanceMoni using (var session = store.OpenAsyncSession()) { - var knownEndpoint = await session.LoadAsync(id) - .ConfigureAwait(false); + var knownEndpoint = await session.LoadAsync(id); if (knownEndpoint == null) { @@ -63,15 +60,14 @@ public async Task CreateOrUpdate(EndpointDetails endpoint, IEndpointInstanceMoni Monitored = true }; - await session.StoreAsync(knownEndpoint).ConfigureAwait(false); + await session.StoreAsync(knownEndpoint); } else { knownEndpoint.Monitored = endpointInstanceMonitoring.IsMonitored(id); } - await session.SaveChangesAsync() - .ConfigureAwait(false); + await session.SaveChangesAsync(); } } @@ -81,15 +77,13 @@ public async Task UpdateEndpointMonitoring(EndpointDetails endpoint, bool isMoni using (var session = store.OpenAsyncSession()) { - var knownEndpoint = await session.LoadAsync(id) - .ConfigureAwait(false); + var knownEndpoint = await session.LoadAsync(id); if (knownEndpoint != null) { knownEndpoint.Monitored = isMonitored; - await session.SaveChangesAsync() - .ConfigureAwait(false); + await session.SaveChangesAsync(); } } } @@ -99,9 +93,9 @@ public async Task WarmupMonitoringFromPersistence(IEndpointInstanceMonitoring en using (var session = store.OpenAsyncSession()) { using (var endpointsEnumerator = await session.Advanced.StreamAsync(session.Query()) - .ConfigureAwait(false)) + ) { - while (await endpointsEnumerator.MoveNextAsync().ConfigureAwait(false)) + while (await endpointsEnumerator.MoveNextAsync()) { var endpoint = endpointsEnumerator.Current.Document; @@ -116,7 +110,7 @@ public async Task Delete(Guid endpointId) using (var session = store.OpenAsyncSession()) { session.Delete(endpointId); - await session.SaveChangesAsync().ConfigureAwait(false); + await session.SaveChangesAsync(); } } public async Task> GetAllKnownEndpoints() @@ -124,7 +118,7 @@ public async Task> GetAllKnownEndpoints() using (var session = store.OpenAsyncSession()) { var knownEndpoints = await session.Query() - .ToListAsync().ConfigureAwait(false); + .ToListAsync(); return knownEndpoints.ToArray(); } diff --git a/src/ServiceControl.Persistence.RavenDb/RavenDbPersistence.cs b/src/ServiceControl.Persistence.RavenDb/RavenDbPersistence.cs new file mode 100644 index 0000000000..008e4f748c --- /dev/null +++ b/src/ServiceControl.Persistence.RavenDb/RavenDbPersistence.cs @@ -0,0 +1,105 @@ +namespace ServiceControl.Persistence.RavenDb +{ + using MessageRedirects; + using Microsoft.Extensions.DependencyInjection; + using Persistence.Recoverability; + using Raven.Client; + using Raven.Client.Embedded; + using Recoverability; + using ServiceControl.CustomChecks; + using ServiceControl.Infrastructure.RavenDB.Subscriptions; + using ServiceControl.MessageFailures; + using ServiceControl.Operations; + using ServiceControl.Operations.BodyStorage; + using ServiceControl.Operations.BodyStorage.RavenAttachments; + using ServiceControl.Persistence.MessageRedirects; + using ServiceControl.Persistence.RavenDb.SagaAudit; + using ServiceControl.Persistence.UnitOfWork; + using ServiceControl.Recoverability; + + class RavenDbPersistence : IPersistence + { + public RavenDbPersistence(PersistenceSettings settings, EmbeddableDocumentStore documentStore, RavenStartup ravenStartup) + { + this.settings = settings; + this.documentStore = documentStore; + this.ravenStartup = ravenStartup; + } + + public void Configure(IServiceCollection serviceCollection) + { + serviceCollection.AddSingleton(settings); + serviceCollection.AddSingleton(documentStore); + + serviceCollection.AddSingleton(); + serviceCollection.AddSingleton(); + serviceCollection.AddSingleton(); + serviceCollection.AddUnitOfWorkFactory(); + serviceCollection.AddSingleton(); + serviceCollection.AddSingleton(); + + serviceCollection.AddSingleton(); + serviceCollection.AddSingleton(p => p.GetRequiredService()); + serviceCollection.AddHostedService(p => p.GetRequiredService()); + + serviceCollection.AddSingleton(); + serviceCollection.AddSingleton(p => p.GetRequiredService()); + serviceCollection.AddHostedService(p => p.GetRequiredService()); + + serviceCollection.AddCustomCheck(); + serviceCollection.AddCustomCheck(); + serviceCollection.AddCustomCheck(); + serviceCollection.AddCustomCheck(); + serviceCollection.AddCustomCheck(); + serviceCollection.AddCustomCheck(); + + serviceCollection.AddSingleton(); + + serviceCollection.AddSingleton(); + serviceCollection.AddSingleton(); + serviceCollection.AddSingleton(); + serviceCollection.AddSingleton(); + serviceCollection.AddSingleton(); + serviceCollection.AddSingleton(); + serviceCollection.AddSingleton(); + serviceCollection.AddSingleton(); + serviceCollection.AddSingleton(); + serviceCollection.AddSingleton(); + serviceCollection.AddSingleton(); + serviceCollection.AddSingleton(); + serviceCollection.AddSingleton(); + serviceCollection.AddSingleton(); + serviceCollection.AddSingleton(); + serviceCollection.AddSingleton(); + } + + public IPersistenceLifecycle CreateLifecycle() + { + return new RavenDbPersistenceLifecycle(ravenStartup, documentStore); + } + + // TODO: Make sure this stuff from PersistenceHostBuilderExtensions is accounted for here + + //var documentStore = new EmbeddableDocumentStore(); + //RavenBootstrapper.Configure(documentStore, settings); + + //hostBuilder.ConfigureServices(serviceCollection => + //{ + // serviceCollection.AddSingleton(documentStore); + // serviceCollection.AddHostedService(); + // serviceCollection.AddCustomCheck(); + // serviceCollection.AddCustomCheck(); + + // serviceCollection.AddServiceControlPersistence(settings.DataStoreType); + //}); + + public IPersistenceInstaller CreateInstaller() + { + return new RavenDbInstaller(documentStore, ravenStartup); + } + + readonly RavenStartup ravenStartup; + readonly PersistenceSettings settings; + readonly EmbeddableDocumentStore documentStore; + } +} diff --git a/src/ServiceControl.Persistence.RavenDb/RavenDbPersistenceConfiguration.cs b/src/ServiceControl.Persistence.RavenDb/RavenDbPersistenceConfiguration.cs index 2d8c6f1a14..c457809fa3 100644 --- a/src/ServiceControl.Persistence.RavenDb/RavenDbPersistenceConfiguration.cs +++ b/src/ServiceControl.Persistence.RavenDb/RavenDbPersistenceConfiguration.cs @@ -1,16 +1,41 @@ namespace ServiceControl.Persistence.RavenDb { - using Microsoft.Extensions.DependencyInjection; - using ServiceControl.Persistence.UnitOfWork; + using System.Collections.Generic; + using Raven.Client.Embedded; - public class RavenDbPersistenceConfiguration : IPersistenceConfiguration + class RavenDbPersistenceConfiguration : IPersistenceConfiguration { - public void ConfigureServices(IServiceCollection serviceCollection) + //TODO: figure out what can be strongly typed + public const string LogPathKey = "LogPath"; + public const string DbPathKey = "DbPath"; + public const string DataSpaceRemainingThresholdKey = "DataSpaceRemainingThreshold"; + public const string MinimumStorageLeftRequiredForIngestionKey = "MinimumStorageLeftRequiredForIngestion"; + public const string AuditRetentionPeriodKey = "AuditRetentionPeriod"; + + + public string Name => "RavenDB35"; + + public IEnumerable ConfigurationKeys => new[]{ + RavenBootstrapper.DatabasePathKey, + RavenBootstrapper.HostNameKey, + RavenBootstrapper.DatabaseMaintenancePortKey, + RavenBootstrapper.ExposeRavenDBKey, + RavenBootstrapper.ExpirationProcessTimerInSecondsKey, + RavenBootstrapper.ExpirationProcessBatchSizeKey, + RavenBootstrapper.RunCleanupBundleKey, + RavenBootstrapper.RunInMemoryKey, + RavenBootstrapper.MinimumStorageLeftRequiredForIngestionKey, + DataSpaceRemainingThresholdKey + }; + + public IPersistence Create(PersistenceSettings settings) { - serviceCollection.AddSingleton(); - serviceCollection.AddSingleton(); - serviceCollection.AddUnitOfWorkFactory(); - serviceCollection.AddSingleton(); + var documentStore = new EmbeddableDocumentStore(); + RavenBootstrapper.Configure(documentStore, settings); + + var ravenStartup = new RavenStartup(); + + return new RavenDbPersistence(settings, documentStore, ravenStartup); } } } diff --git a/src/ServiceControl.Persistence.RavenDb/RavenDbPersistenceLifecycle.cs b/src/ServiceControl.Persistence.RavenDb/RavenDbPersistenceLifecycle.cs new file mode 100644 index 0000000000..66c4b3af3a --- /dev/null +++ b/src/ServiceControl.Persistence.RavenDb/RavenDbPersistenceLifecycle.cs @@ -0,0 +1,62 @@ +namespace ServiceControl.Persistence.RavenDb +{ + using System; + using System.Threading; + using System.Threading.Tasks; + using NServiceBus.Logging; + using Raven.Client; + using Raven.Client.Embedded; + using ServiceControl.Persistence; + + class RavenDbPersistenceLifecycle : IPersistenceLifecycle + { + public RavenDbPersistenceLifecycle(RavenStartup ravenStartup, EmbeddableDocumentStore documentStore) + { + this.ravenStartup = ravenStartup; + this.documentStore = documentStore; + } + + public async Task Start(CancellationToken cancellationToken) + { + Logger.Info("Database initialization starting"); + documentStore.Initialize(); + Logger.Info("Database initialization complete"); + + await ravenStartup.CreateIndexesAsync(documentStore); + + Logger.Info("Testing indexes"); + await TestAllIndexesAndResetIfException(documentStore); + } + + public Task Stop(CancellationToken cancellationToken) + { + documentStore.Dispose(); + return Task.CompletedTask; + } + + static async Task TestAllIndexesAndResetIfException(IDocumentStore store) + { + foreach (var index in store.DatabaseCommands.GetStatistics().Indexes) + { + try + { + using (var session = store.OpenAsyncSession()) + { + await session.Advanced.AsyncDocumentQuery(index.Name).Take(1).ToListAsync(); + } + } + catch (Exception ex) + { + Logger.Warn($"When trying to fetch 1 document from index {index.Name} the following exception was thrown: {ex}"); + Logger.Warn($"Attempting to reset errored index: [{index.Name}] priority: {index.Priority} is valid: {index.IsInvalidIndex} indexing attempts: {index.IndexingAttempts}, failed indexing attempts:{index.IndexingErrors}"); + store.DatabaseCommands.ResetIndex(index.Name); + } + } + } + + readonly RavenStartup ravenStartup; + readonly EmbeddableDocumentStore documentStore; + + static readonly ILog Logger = LogManager.GetLogger(typeof(RavenDbPersistenceLifecycle)); + } +} \ No newline at end of file diff --git a/src/ServiceControl/Infrastructure/RavenDB/RavenLicense.xml b/src/ServiceControl.Persistence.RavenDb/RavenLicense.xml similarity index 100% rename from src/ServiceControl/Infrastructure/RavenDB/RavenLicense.xml rename to src/ServiceControl.Persistence.RavenDb/RavenLicense.xml diff --git a/src/ServiceControl.Persistence.RavenDb/RavenQueryExtensions.cs b/src/ServiceControl.Persistence.RavenDb/RavenQueryExtensions.cs index 698e4777bf..94c4897bb2 100644 --- a/src/ServiceControl.Persistence.RavenDb/RavenQueryExtensions.cs +++ b/src/ServiceControl.Persistence.RavenDb/RavenQueryExtensions.cs @@ -12,17 +12,79 @@ namespace ServiceControl.Persistence using ServiceControl.MessageFailures; using ServiceControl.Persistence.Infrastructure; - public static class RavenQueryExtensions + static class RavenQueryExtensions { - public static IAsyncDocumentQuery Paging(this IAsyncDocumentQuery source, HttpRequestMessage request) + public static IRavenQueryable IncludeSystemMessagesWhere( + this IRavenQueryable source, bool includeSystemMessages) { - var maxResultsPerPage = request.GetQueryStringValue("per_page", 50); + if (!includeSystemMessages) + { + return source.Where(m => !m.IsSystemMessage); + } + + return source; + } + + public static IRavenQueryable Paging(this IRavenQueryable source, PagingInfo pagingInfo) + => source.Skip(pagingInfo.Offset).Take(pagingInfo.PageSize); + + public static IRavenQueryable Sort(this IRavenQueryable source, SortInfo sortInfo) + where TSource : MessagesViewIndex.SortAndFilterOptions + { + Expression> keySelector; + switch (sortInfo.Sort) + { + case "id": + case "message_id": + keySelector = m => m.MessageId; + break; + + case "message_type": + keySelector = m => m.MessageType; + break; + + case "critical_time": + keySelector = m => m.CriticalTime; + break; + + case "delivery_time": + keySelector = m => m.DeliveryTime; + break; + + case "processing_time": + keySelector = m => m.ProcessingTime; + break; + + case "processed_at": + keySelector = m => m.ProcessedAt; + break; + + case "status": + keySelector = m => m.Status; + break; + + default: + keySelector = m => m.TimeSent; + break; + } + + if (sortInfo.Direction == "asc") + { + return source.OrderBy(keySelector); + } + + return source.OrderByDescending(keySelector); + } + + public static IAsyncDocumentQuery Paging(this IAsyncDocumentQuery source, PagingInfo pagingInfo) + { + var maxResultsPerPage = pagingInfo.PageSize; if (maxResultsPerPage < 1) { maxResultsPerPage = 50; } - var page = request.GetQueryStringValue("page", 1); + var page = pagingInfo.Page; if (page < 1) { page = 1; @@ -34,11 +96,11 @@ public static IAsyncDocumentQuery Paging(this IAsyncDocumentQu .Take(maxResultsPerPage); } - public static IAsyncDocumentQuery Sort(this IAsyncDocumentQuery source, HttpRequestMessage request) + public static IAsyncDocumentQuery Sort(this IAsyncDocumentQuery source, SortInfo sortInfo) { var descending = true; - var direction = request.GetQueryStringValue("direction", "desc"); + var direction = sortInfo.Direction; if (direction == "asc") { descending = false; @@ -46,7 +108,7 @@ public static IAsyncDocumentQuery Sort(this IAsyncDocumentQuer string keySelector; - var sort = request.GetQueryStringValue("sort", "time_sent"); + var sort = sortInfo.Sort; if (!AsyncDocumentQuerySortOptions.Contains(sort)) { sort = "time_sent"; @@ -84,9 +146,8 @@ public static IAsyncDocumentQuery Sort(this IAsyncDocumentQuer } - public static IAsyncDocumentQuery FilterByStatusWhere(this IAsyncDocumentQuery source, HttpRequestMessage request) + public static IAsyncDocumentQuery FilterByStatusWhere(this IAsyncDocumentQuery source, string status) { - var status = request.GetQueryStringValue("status"); if (status == null) { return source; @@ -146,10 +207,8 @@ public static IAsyncDocumentQuery FilterByStatusWhere(this IAsyncDocumentQ } - public static IAsyncDocumentQuery FilterByLastModifiedRange(this IAsyncDocumentQuery source, HttpRequestMessage request) + public static IAsyncDocumentQuery FilterByLastModifiedRange(this IAsyncDocumentQuery source, string modified) { - var modified = request.GetQueryStringValue("modified"); - if (modified == null) { return source; @@ -177,9 +236,8 @@ public static IAsyncDocumentQuery FilterByLastModifiedRange(this IAsyncDoc return source; } - public static IAsyncDocumentQuery FilterByQueueAddress(this IAsyncDocumentQuery source, HttpRequestMessage request) + public static IAsyncDocumentQuery FilterByQueueAddress(this IAsyncDocumentQuery source, string queueAddress) { - var queueAddress = request.GetQueryStringValue("queueaddress"); if (string.IsNullOrWhiteSpace(queueAddress)) { return source; @@ -219,12 +277,6 @@ public static IOrderedQueryable Paging(this IOrderedQueryable< .Take(maxResultsPerPage); } - public static IRavenQueryable Paging(this IRavenQueryable source, PagingInfo paging) - { - return source.Skip(paging.Offset) - .Take(paging.PageSize); - } - public static IRavenQueryable Paging(this IRavenQueryable source, HttpRequestMessage request) { var maxResultsPerPage = request.GetQueryStringValue("per_page", 50); diff --git a/src/ServiceControl.Persistence.RavenDb/RavenQueryStatisticsExtensions.cs b/src/ServiceControl.Persistence.RavenDb/RavenQueryStatisticsExtensions.cs index 137bec9ea9..9be4c7b5f4 100644 --- a/src/ServiceControl.Persistence.RavenDb/RavenQueryStatisticsExtensions.cs +++ b/src/ServiceControl.Persistence.RavenDb/RavenQueryStatisticsExtensions.cs @@ -3,11 +3,15 @@ using Raven.Client; using ServiceControl.Persistence.Infrastructure; - public static class RavenQueryStatisticsExtensions + static class RavenQueryStatisticsExtensions { public static QueryStatsInfo ToQueryStatsInfo(this RavenQueryStatistics stats) { - return new QueryStatsInfo(stats.IndexEtag, stats.TotalResults); + return new QueryStatsInfo(stats.IndexEtag, stats.TotalResults, stats.IsStale); + } + public static QueryStatsInfo ToQueryStatsInfo(this Raven.Abstractions.Data.QueryResult queryResult) + { + return new QueryStatsInfo(queryResult.IndexEtag, queryResult.TotalResults, queryResult.IsStale); } } } \ No newline at end of file diff --git a/src/ServiceControl.Persistence.RavenDb/RavenStartup.cs b/src/ServiceControl.Persistence.RavenDb/RavenStartup.cs new file mode 100644 index 0000000000..7e4c22131c --- /dev/null +++ b/src/ServiceControl.Persistence.RavenDb/RavenStartup.cs @@ -0,0 +1,19 @@ +namespace ServiceControl.Persistence.RavenDb +{ + using System.Threading.Tasks; + using NServiceBus.Logging; + using Raven.Client; + using Raven.Client.Indexes; + + class RavenStartup + { + public async Task CreateIndexesAsync(IDocumentStore documentStore) + { + Logger.Info("Index creation started"); + await IndexCreation.CreateIndexesAsync(typeof(RavenBootstrapper).Assembly, documentStore); + Logger.Info("Index creation complete"); + } + + static readonly ILog Logger = LogManager.GetLogger(); + } +} \ No newline at end of file diff --git a/src/ServiceControl/Recoverability/Archiving/Raven/ArchiveBatch.cs b/src/ServiceControl.Persistence.RavenDb/Recoverability/Archiving/ArchiveBatch.cs similarity index 100% rename from src/ServiceControl/Recoverability/Archiving/Raven/ArchiveBatch.cs rename to src/ServiceControl.Persistence.RavenDb/Recoverability/Archiving/ArchiveBatch.cs diff --git a/src/ServiceControl/Recoverability/Archiving/ArchiveDocumentManager.cs b/src/ServiceControl.Persistence.RavenDb/Recoverability/Archiving/ArchiveDocumentManager.cs similarity index 86% rename from src/ServiceControl/Recoverability/Archiving/ArchiveDocumentManager.cs rename to src/ServiceControl.Persistence.RavenDb/Recoverability/Archiving/ArchiveDocumentManager.cs index e9512ba54f..b0ca31afc3 100644 --- a/src/ServiceControl/Recoverability/Archiving/ArchiveDocumentManager.cs +++ b/src/ServiceControl.Persistence.RavenDb/Recoverability/Archiving/ArchiveDocumentManager.cs @@ -32,7 +32,7 @@ public async Task CreateArchiveOperation(IAsyncDocumentSession CurrentBatch = 0 }; - await session.StoreAsync(operation).ConfigureAwait(false); + await session.StoreAsync(operation); var documentCount = 0; var indexQuery = session.Query(new FailedMessages_ByGroup().IndexName); @@ -42,7 +42,7 @@ public async Task CreateArchiveOperation(IAsyncDocumentSession .Where(failure => failure.Status == FailedMessageStatus.Unresolved) .Select(document => document.Id); - var docs = await StreamResults(session, docQuery).ConfigureAwait(false); + var docs = await StreamResults(session, docQuery); var batches = docs .GroupBy(d => documentCount++ / batchSize); @@ -55,7 +55,7 @@ public async Task CreateArchiveOperation(IAsyncDocumentSession DocumentIds = batch.ToList() }; - await session.StoreAsync(archiveBatch).ConfigureAwait(false); + await session.StoreAsync(archiveBatch); } return operation; @@ -64,9 +64,9 @@ public async Task CreateArchiveOperation(IAsyncDocumentSession async Task> StreamResults(IAsyncDocumentSession session, IQueryable query) { var results = new List(); - using (var enumerator = await session.Advanced.StreamAsync(query).ConfigureAwait(false)) + using (var enumerator = await session.Advanced.StreamAsync(query)) { - while (await enumerator.MoveNextAsync().ConfigureAwait(false)) + while (await enumerator.MoveNextAsync()) { results.Add(enumerator.Current.Document); } @@ -83,8 +83,7 @@ public Task GetArchiveBatch(IAsyncDocumentSession session, string public async Task GetGroupDetails(IAsyncDocumentSession session, string groupId) { var group = await session.Query() - .FirstOrDefaultAsync(x => x.Id == groupId) - .ConfigureAwait(false); + .FirstOrDefaultAsync(x => x.Id == groupId); return new GroupDetails { @@ -99,10 +98,8 @@ public async Task ArchiveMessageGroupBatch(IAsyncDocumentSession session, Archiv if (patchCommands != null) { - await session.Advanced.DocumentStore.AsyncDatabaseCommands.BatchAsync(patchCommands) - .ConfigureAwait(false); - await session.Advanced.DocumentStore.AsyncDatabaseCommands.DeleteAsync(batch.Id, null) - .ConfigureAwait(false); + await session.Advanced.DocumentStore.AsyncDatabaseCommands.BatchAsync(patchCommands); + await session.Advanced.DocumentStore.AsyncDatabaseCommands.DeleteAsync(batch.Id, null); } } @@ -119,7 +116,7 @@ public async Task WaitForIndexUpdateOfArchiveOperation(IDocumentStore stor try { - await docQuery.AnyAsync().ConfigureAwait(false); + await docQuery.AnyAsync(); return true; } @@ -139,10 +136,8 @@ public async Task RemoveArchiveOperation(IDocumentStore store, ArchiveOperation { using (var session = store.OpenAsyncSession()) { - await session.Advanced.DocumentStore.AsyncDatabaseCommands.DeleteAsync(archiveOperation.Id, null) - .ConfigureAwait(false); - await session.SaveChangesAsync() - .ConfigureAwait(false); + await session.Advanced.DocumentStore.AsyncDatabaseCommands.DeleteAsync(archiveOperation.Id, null); + await session.SaveChangesAsync(); logger.Info($"Removing ArchiveOperation {archiveOperation.Id} completed"); } diff --git a/src/ServiceControl/Recoverability/Archiving/Raven/ArchiveOperation.cs b/src/ServiceControl.Persistence.RavenDb/Recoverability/Archiving/ArchiveOperation.cs similarity index 100% rename from src/ServiceControl/Recoverability/Archiving/Raven/ArchiveOperation.cs rename to src/ServiceControl.Persistence.RavenDb/Recoverability/Archiving/ArchiveOperation.cs diff --git a/src/ServiceControl.Persistence.RavenDb/Recoverability/Archiving/ArchiveOperationExtensions.cs b/src/ServiceControl.Persistence.RavenDb/Recoverability/Archiving/ArchiveOperationExtensions.cs new file mode 100644 index 0000000000..8ec9667654 --- /dev/null +++ b/src/ServiceControl.Persistence.RavenDb/Recoverability/Archiving/ArchiveOperationExtensions.cs @@ -0,0 +1,39 @@ +namespace ServiceControl.Persistence.RavenDb.Recoverability +{ + using ServiceControl.Recoverability; + + static class ArchiveOperationExtensions + { + public static ArchiveOperation ToArchiveOperation(this InMemoryArchive a) + { + return new ArchiveOperation + { + ArchiveType = a.ArchiveType, + GroupName = a.GroupName, + Id = ArchiveOperation.MakeId(a.RequestId, a.ArchiveType), + NumberOfMessagesArchived = a.NumberOfMessagesArchived, + RequestId = a.RequestId, + Started = a.Started, + TotalNumberOfMessages = a.TotalNumberOfMessages, + NumberOfBatches = a.NumberOfBatches, + CurrentBatch = a.CurrentBatch + }; + } + + public static UnarchiveOperation ToUnarchiveOperation(this InMemoryUnarchive u) + { + return new UnarchiveOperation + { + ArchiveType = u.ArchiveType, + GroupName = u.GroupName, + Id = UnarchiveOperation.MakeId(u.RequestId, u.ArchiveType), + NumberOfMessagesUnarchived = u.NumberOfMessagesUnarchived, + RequestId = u.RequestId, + Started = u.Started, + TotalNumberOfMessages = u.TotalNumberOfMessages, + NumberOfBatches = u.NumberOfBatches, + CurrentBatch = u.CurrentBatch + }; + } + } +} diff --git a/src/ServiceControl/Recoverability/Archiving/ArchivingManager.cs b/src/ServiceControl.Persistence.RavenDb/Recoverability/Archiving/ArchivingManager.cs similarity index 100% rename from src/ServiceControl/Recoverability/Archiving/ArchivingManager.cs rename to src/ServiceControl.Persistence.RavenDb/Recoverability/Archiving/ArchivingManager.cs diff --git a/src/ServiceControl.Persistence.RavenDb/Recoverability/Archiving/MessageArchiver.cs b/src/ServiceControl.Persistence.RavenDb/Recoverability/Archiving/MessageArchiver.cs new file mode 100644 index 0000000000..085c64e7c5 --- /dev/null +++ b/src/ServiceControl.Persistence.RavenDb/Recoverability/Archiving/MessageArchiver.cs @@ -0,0 +1,242 @@ +namespace ServiceControl.Persistence.RavenDb.Recoverability +{ + using System; + using System.Collections.Generic; + using System.Linq; + using System.Threading.Tasks; + using NServiceBus.Logging; + using Raven.Client; + using ServiceControl.Infrastructure.DomainEvents; + using ServiceControl.Persistence.Recoverability; + using ServiceControl.Recoverability; + + class MessageArchiver : IArchiveMessages + { + public MessageArchiver(IDocumentStore store, OperationsManager operationsManager, IDomainEvents domainEvents) + { + this.store = store; + this.domainEvents = domainEvents; + this.operationsManager = operationsManager; + + archiveDocumentManager = new ArchiveDocumentManager(); + archivingManager = new ArchivingManager(domainEvents, operationsManager); + + unarchiveDocumentManager = new UnarchiveDocumentManager(); + unarchivingManager = new UnarchivingManager(domainEvents, operationsManager); + } + + public async Task ArchiveAllInGroup(string groupId) + { + logger.Info($"Archiving of {groupId} started"); + ArchiveOperation archiveOperation; + + using (var session = store.OpenAsyncSession()) + { + session.Advanced.UseOptimisticConcurrency = true; // Ensure 2 messages don't split the same operation into batches at once + + archiveOperation = await archiveDocumentManager.LoadArchiveOperation(session, groupId, ArchiveType.FailureGroup); + + if (archiveOperation == null) + { + var groupDetails = await archiveDocumentManager.GetGroupDetails(session, groupId); + if (groupDetails.NumberOfMessagesInGroup == 0) + { + logger.Warn($"No messages to archive in group {groupId}"); + return; + } + + logger.Info($"Splitting group {groupId} into batches"); + archiveOperation = await archiveDocumentManager.CreateArchiveOperation(session, groupId, ArchiveType.FailureGroup, groupDetails.NumberOfMessagesInGroup, groupDetails.GroupName, batchSize); + await session.SaveChangesAsync(); + + logger.Info($"Group {groupId} has been split into {archiveOperation.NumberOfBatches} batches"); + } + } + + await archivingManager.StartArchiving(archiveOperation); + + while (archiveOperation.CurrentBatch < archiveOperation.NumberOfBatches) + { + using (var batchSession = store.OpenAsyncSession()) + { + var nextBatch = await archiveDocumentManager.GetArchiveBatch(batchSession, archiveOperation.Id, archiveOperation.CurrentBatch); + if (nextBatch == null) + { + // We're only here in the case where Raven indexes are stale + logger.Warn($"Attempting to archive a batch ({archiveOperation.Id}/{archiveOperation.CurrentBatch}) which appears to already have been archived."); + } + else + { + logger.Info($"Archiving {nextBatch.DocumentIds.Count} messages from group {groupId} starting"); + } + + await archiveDocumentManager.ArchiveMessageGroupBatch(batchSession, nextBatch); + + await archivingManager.BatchArchived(archiveOperation.RequestId, archiveOperation.ArchiveType, nextBatch?.DocumentIds.Count ?? 0); + + archiveOperation = archivingManager.GetStatusForArchiveOperation(archiveOperation.RequestId, archiveOperation.ArchiveType).ToArchiveOperation(); + + await archiveDocumentManager.UpdateArchiveOperation(batchSession, archiveOperation); + + await batchSession.SaveChangesAsync(); + + if (nextBatch != null) + { + await domainEvents.Raise(new FailedMessageGroupBatchArchived + { + // Remove `FailedMessages/` prefix and publish pure GUIDs without Raven collection name + FailedMessagesIds = nextBatch.DocumentIds.Select(id => id.Replace("FailedMessages/", "")).ToArray() + }); + } + + if (nextBatch != null) + { + logger.Info($"Archiving of {nextBatch.DocumentIds.Count} messages from group {groupId} completed"); + } + } + } + + logger.Info($"Archiving of group {groupId} is complete. Waiting for index updates."); + await archivingManager.ArchiveOperationFinalizing(archiveOperation.RequestId, archiveOperation.ArchiveType); + if (!await archiveDocumentManager.WaitForIndexUpdateOfArchiveOperation(store, archiveOperation.RequestId, TimeSpan.FromMinutes(5)) + ) + { + logger.Warn($"Archiving group {groupId} completed but index not updated."); + } + + await archivingManager.ArchiveOperationCompleted(archiveOperation.RequestId, archiveOperation.ArchiveType); + await archiveDocumentManager.RemoveArchiveOperation(store, archiveOperation); + + await domainEvents.Raise(new FailedMessageGroupArchived + { + GroupId = groupId, + GroupName = archiveOperation.GroupName, + MessagesCount = archiveOperation.TotalNumberOfMessages, + }); + + logger.Info($"Archiving of group {groupId} completed"); + } + + public async Task UnarchiveAllInGroup(string groupId) + { + logger.Info($"Unarchiving of {groupId} started"); + UnarchiveOperation unarchiveOperation; + + using (var session = store.OpenAsyncSession()) + { + session.Advanced.UseOptimisticConcurrency = true; // Ensure 2 messages don't split the same operation into batches at once + + unarchiveOperation = await unarchiveDocumentManager.LoadUnarchiveOperation(session, groupId, ArchiveType.FailureGroup); + + if (unarchiveOperation == null) + { + var groupDetails = await unarchiveDocumentManager.GetGroupDetails(session, groupId); + if (groupDetails.NumberOfMessagesInGroup == 0) + { + logger.Warn($"No messages to unarchive in group {groupId}"); + + return; + } + + logger.Info($"Splitting group {groupId} into batches"); + unarchiveOperation = await unarchiveDocumentManager.CreateUnarchiveOperation(session, groupId, ArchiveType.FailureGroup, groupDetails.NumberOfMessagesInGroup, groupDetails.GroupName, batchSize); + await session.SaveChangesAsync(); + + logger.Info($"Group {groupId} has been split into {unarchiveOperation.NumberOfBatches} batches"); + } + } + + await unarchivingManager.StartUnarchiving(unarchiveOperation); + + while (unarchiveOperation.CurrentBatch < unarchiveOperation.NumberOfBatches) + { + using (var batchSession = store.OpenAsyncSession()) + { + var nextBatch = await unarchiveDocumentManager.GetUnarchiveBatch(batchSession, unarchiveOperation.Id, unarchiveOperation.CurrentBatch); + if (nextBatch == null) + { + // We're only here in the case where Raven indexes are stale + logger.Warn($"Attempting to unarchive a batch ({unarchiveOperation.Id}/{unarchiveOperation.CurrentBatch}) which appears to already have been archived."); + } + else + { + logger.Info($"Unarchiving {nextBatch.DocumentIds.Count} messages from group {groupId} starting"); + } + + await unarchiveDocumentManager.UnarchiveMessageGroupBatch(batchSession, nextBatch); + + await unarchivingManager.BatchUnarchived(unarchiveOperation.RequestId, unarchiveOperation.ArchiveType, nextBatch?.DocumentIds.Count ?? 0); + + unarchiveOperation = unarchivingManager.GetStatusForUnarchiveOperation(unarchiveOperation.RequestId, unarchiveOperation.ArchiveType).ToUnarchiveOperation(); + + await unarchiveDocumentManager.UpdateUnarchiveOperation(batchSession, unarchiveOperation); + + await batchSession.SaveChangesAsync(); + + if (nextBatch != null) + { + await domainEvents.Raise(new FailedMessageGroupBatchUnarchived + { + // Remove `FailedMessages/` prefix and publish pure GUIDs without Raven collection name + FailedMessagesIds = nextBatch.DocumentIds.Select(id => id.Replace("FailedMessages/", "")).ToArray() + }); + } + + if (nextBatch != null) + { + logger.Info($"Unarchiving of {nextBatch.DocumentIds.Count} messages from group {groupId} completed"); + } + } + } + + logger.Info($"Unarchiving of group {groupId} is complete. Waiting for index updates."); + await unarchivingManager.UnarchiveOperationFinalizing(unarchiveOperation.RequestId, unarchiveOperation.ArchiveType); + if (!await unarchiveDocumentManager.WaitForIndexUpdateOfUnarchiveOperation(store, unarchiveOperation.RequestId, TimeSpan.FromMinutes(5)) + ) + { + logger.Warn($"Unarchiving group {groupId} completed but index not updated."); + } + + logger.Info($"Unarchiving of group {groupId} completed"); + await unarchivingManager.UnarchiveOperationCompleted(unarchiveOperation.RequestId, unarchiveOperation.ArchiveType); + await unarchiveDocumentManager.RemoveUnarchiveOperation(store, unarchiveOperation); + + await domainEvents.Raise(new FailedMessageGroupUnarchived + { + GroupId = groupId, + GroupName = unarchiveOperation.GroupName, + MessagesCount = unarchiveOperation.TotalNumberOfMessages, + }); + } + + public bool IsOperationInProgressFor(string groupId, ArchiveType archiveType) + { + return operationsManager.IsOperationInProgressFor(groupId, archiveType); + } + + public bool IsArchiveInProgressFor(string groupId) + => archivingManager.IsArchiveInProgressFor(groupId); + + public void DismissArchiveOperation(string groupId, ArchiveType archiveType) + => archivingManager.DismissArchiveOperation(groupId, archiveType); + + public Task StartArchiving(string groupId, ArchiveType archiveType) + => archivingManager.StartArchiving(groupId, archiveType); + + public Task StartUnarchiving(string groupId, ArchiveType archiveType) + => unarchivingManager.StartUnarchiving(groupId, archiveType); + + public IEnumerable GetArchivalOperations() + => archivingManager.GetArchivalOperations(); + + readonly IDocumentStore store; + readonly OperationsManager operationsManager; + readonly IDomainEvents domainEvents; + readonly ArchiveDocumentManager archiveDocumentManager; + readonly ArchivingManager archivingManager; + readonly UnarchiveDocumentManager unarchiveDocumentManager; + readonly UnarchivingManager unarchivingManager; + static readonly ILog logger = LogManager.GetLogger(); + const int batchSize = 1000; + } +} diff --git a/src/ServiceControl/Recoverability/Archiving/Raven/UnarchiveBatch.cs b/src/ServiceControl.Persistence.RavenDb/Recoverability/Archiving/UnarchiveBatch.cs similarity index 100% rename from src/ServiceControl/Recoverability/Archiving/Raven/UnarchiveBatch.cs rename to src/ServiceControl.Persistence.RavenDb/Recoverability/Archiving/UnarchiveBatch.cs diff --git a/src/ServiceControl/Recoverability/Archiving/UnarchiveDocumentManager.cs b/src/ServiceControl.Persistence.RavenDb/Recoverability/Archiving/UnarchiveDocumentManager.cs similarity index 85% rename from src/ServiceControl/Recoverability/Archiving/UnarchiveDocumentManager.cs rename to src/ServiceControl.Persistence.RavenDb/Recoverability/Archiving/UnarchiveDocumentManager.cs index c0dd0f87d7..2ee549bf30 100644 --- a/src/ServiceControl/Recoverability/Archiving/UnarchiveDocumentManager.cs +++ b/src/ServiceControl.Persistence.RavenDb/Recoverability/Archiving/UnarchiveDocumentManager.cs @@ -31,7 +31,7 @@ public async Task CreateUnarchiveOperation(IAsyncDocumentSes CurrentBatch = 0 }; - await session.StoreAsync(operation).ConfigureAwait(false); + await session.StoreAsync(operation); var documentCount = 0; var indexQuery = session.Query(new FailedMessages_ByGroup().IndexName); @@ -41,7 +41,7 @@ public async Task CreateUnarchiveOperation(IAsyncDocumentSes .Where(failure => failure.Status == FailedMessageStatus.Archived) .Select(document => document.Id); - var docs = await StreamResults(session, docQuery).ConfigureAwait(false); + var docs = await StreamResults(session, docQuery); var batches = docs .GroupBy(d => documentCount++ / batchSize); @@ -54,7 +54,7 @@ public async Task CreateUnarchiveOperation(IAsyncDocumentSes DocumentIds = batch.ToList() }; - await session.StoreAsync(unarchiveBatch).ConfigureAwait(false); + await session.StoreAsync(unarchiveBatch); } return operation; @@ -63,9 +63,9 @@ public async Task CreateUnarchiveOperation(IAsyncDocumentSes async Task> StreamResults(IAsyncDocumentSession session, IQueryable query) { var results = new List(); - using (var enumerator = await session.Advanced.StreamAsync(query).ConfigureAwait(false)) + using (var enumerator = await session.Advanced.StreamAsync(query)) { - while (await enumerator.MoveNextAsync().ConfigureAwait(false)) + while (await enumerator.MoveNextAsync()) { results.Add(enumerator.Current.Document); } @@ -82,8 +82,7 @@ public Task GetUnarchiveBatch(IAsyncDocumentSession session, str public async Task GetGroupDetails(IAsyncDocumentSession session, string groupId) { var group = await session.Query() - .FirstOrDefaultAsync(x => x.Id == groupId) - .ConfigureAwait(false); + .FirstOrDefaultAsync(x => x.Id == groupId); return new GroupDetails { @@ -98,10 +97,8 @@ public async Task UnarchiveMessageGroupBatch(IAsyncDocumentSession session, Unar if (patchCommands != null) { - await session.Advanced.DocumentStore.AsyncDatabaseCommands.BatchAsync(patchCommands) - .ConfigureAwait(false); - await session.Advanced.DocumentStore.AsyncDatabaseCommands.DeleteAsync(batch.Id, null) - .ConfigureAwait(false); + await session.Advanced.DocumentStore.AsyncDatabaseCommands.BatchAsync(patchCommands); + await session.Advanced.DocumentStore.AsyncDatabaseCommands.DeleteAsync(batch.Id, null); } } @@ -118,7 +115,7 @@ public async Task WaitForIndexUpdateOfUnarchiveOperation(IDocumentStore st try { - await docQuery.AnyAsync().ConfigureAwait(false); + await docQuery.AnyAsync(); return true; } @@ -138,10 +135,8 @@ public async Task RemoveUnarchiveOperation(IDocumentStore store, UnarchiveOperat { using (var session = store.OpenAsyncSession()) { - await session.Advanced.DocumentStore.AsyncDatabaseCommands.DeleteAsync(unarchiveOperation.Id, null) - .ConfigureAwait(false); - await session.SaveChangesAsync() - .ConfigureAwait(false); + await session.Advanced.DocumentStore.AsyncDatabaseCommands.DeleteAsync(unarchiveOperation.Id, null); + await session.SaveChangesAsync(); } } diff --git a/src/ServiceControl/Recoverability/Archiving/Raven/UnarchiveOperation.cs b/src/ServiceControl.Persistence.RavenDb/Recoverability/Archiving/UnarchiveOperation.cs similarity index 100% rename from src/ServiceControl/Recoverability/Archiving/Raven/UnarchiveOperation.cs rename to src/ServiceControl.Persistence.RavenDb/Recoverability/Archiving/UnarchiveOperation.cs diff --git a/src/ServiceControl/Recoverability/Archiving/UnarchivingManager.cs b/src/ServiceControl.Persistence.RavenDb/Recoverability/Archiving/UnarchivingManager.cs similarity index 100% rename from src/ServiceControl/Recoverability/Archiving/UnarchivingManager.cs rename to src/ServiceControl.Persistence.RavenDb/Recoverability/Archiving/UnarchivingManager.cs diff --git a/src/ServiceControl.Persistence.RavenDb/Recoverability/GroupsDataStore.cs b/src/ServiceControl.Persistence.RavenDb/Recoverability/GroupsDataStore.cs new file mode 100644 index 0000000000..2df22233aa --- /dev/null +++ b/src/ServiceControl.Persistence.RavenDb/Recoverability/GroupsDataStore.cs @@ -0,0 +1,60 @@ +namespace ServiceControl.Persistence.RavenDb.Recoverability +{ + using System.Collections.Generic; + using System.Linq; + using System.Threading; + using System.Threading.Tasks; + using Raven.Client; + using Raven.Client.Linq; + using ServiceControl.MessageFailures; + using ServiceControl.Recoverability; + + class GroupsDataStore : IGroupsDataStore + { + readonly IDocumentStore store; + + public GroupsDataStore(IDocumentStore store) + { + this.store = store; + } + + public async Task> GetFailureGroupsByClassifier(string classifier, string classifierFilter) + { + using (var session = store.OpenAsyncSession()) + { + var query = Queryable.Where(session.Query(), v => v.Type == classifier); + + if (!string.IsNullOrWhiteSpace(classifierFilter)) + { + query = query.Where(v => v.Title == classifierFilter); + } + + var groups = await query.OrderByDescending(x => x.Last) + .Take(200) + .ToListAsync(); + + var commentIds = groups.Select(x => GroupComment.MakeId(x.Id)).ToArray(); + var comments = await session.Query().Where(x => x.Id.In(commentIds)) + .ToListAsync(CancellationToken.None); + + foreach (var group in groups) + { + group.Comment = comments.FirstOrDefault(x => x.Id == GroupComment.MakeId(group.Id))?.Comment; + } + + return groups; + } + } + + public async Task GetCurrentForwardingBatch() + { + using (var session = store.OpenAsyncSession()) + { + var nowForwarding = await session.Include(r => r.RetryBatchId) + .LoadAsync(RetryBatchNowForwarding.Id); + + return nowForwarding == null ? null : await session.LoadAsync(nowForwarding.RetryBatchId); + } + } + } +} diff --git a/src/ServiceControl.Persistence.RavenDb/Recoverability/RetryHistoryDataStore.cs b/src/ServiceControl.Persistence.RavenDb/Recoverability/RetryHistoryDataStore.cs new file mode 100644 index 0000000000..e25305d18e --- /dev/null +++ b/src/ServiceControl.Persistence.RavenDb/Recoverability/RetryHistoryDataStore.cs @@ -0,0 +1,87 @@ +namespace ServiceControl.Persistence.RavenDb.Recoverability +{ + using System; + using System.Threading.Tasks; + using Raven.Client; + using ServiceControl.Recoverability; + + class RetryHistoryDataStore : IRetryHistoryDataStore + { + public RetryHistoryDataStore(IDocumentStore documentStore) + { + this.documentStore = documentStore; + } + + public async Task GetRetryHistory() + { + using (var session = documentStore.OpenAsyncSession()) + { + var id = RetryHistory.MakeId(); + var retryHistory = await session.LoadAsync(id); + + retryHistory = retryHistory ?? RetryHistory.CreateNew(); + + return retryHistory; + } + } + + public async Task RecordRetryOperationCompleted(string requestId, RetryType retryType, DateTime startTime, DateTime completionTime, + string originator, string classifier, bool messageFailed, int numberOfMessagesProcessed, DateTime lastProcessed, int retryHistoryDepth) + { + using (var session = documentStore.OpenAsyncSession()) + { + var retryHistory = await session.LoadAsync(RetryHistory.MakeId()) ?? + RetryHistory.CreateNew(); + + retryHistory.AddToUnacknowledged(new UnacknowledgedRetryOperation + { + RequestId = requestId, + RetryType = retryType, + StartTime = startTime, + CompletionTime = completionTime, + Originator = originator, + Classifier = classifier, + Failed = messageFailed, + NumberOfMessagesProcessed = numberOfMessagesProcessed, + Last = lastProcessed + }); + + retryHistory.AddToHistory(new HistoricRetryOperation + { + RequestId = requestId, + RetryType = retryType, + StartTime = startTime, + CompletionTime = completionTime, + Originator = originator, + Failed = messageFailed, + NumberOfMessagesProcessed = numberOfMessagesProcessed + }, retryHistoryDepth); + + await session.StoreAsync(retryHistory); + await session.SaveChangesAsync(); + } + } + + public async Task AcknowledgeRetryGroup(string groupId) + { + using (var session = documentStore.OpenAsyncSession()) + { + var retryHistory = await session.LoadAsync(RetryHistory.MakeId()); + if (retryHistory != null) + { + if (retryHistory.Acknowledge(groupId, RetryType.FailureGroup)) + { + await session.StoreAsync(retryHistory); + await session.SaveChangesAsync(); + + return true; + } + } + } + + return false; + } + + readonly IDocumentStore documentStore; + } +} diff --git a/src/ServiceControl.Persistence.RavenDb/RepeatedFailuresOverTimeCircuitBreaker.cs b/src/ServiceControl.Persistence.RavenDb/RepeatedFailuresOverTimeCircuitBreaker.cs new file mode 100644 index 0000000000..09cb5b0700 --- /dev/null +++ b/src/ServiceControl.Persistence.RavenDb/RepeatedFailuresOverTimeCircuitBreaker.cs @@ -0,0 +1,74 @@ +namespace NServiceBus +{ + using System; + using System.Threading; + using System.Threading.Tasks; + using Logging; + + class RepeatedFailuresOverTimeCircuitBreaker : IDisposable + { + public RepeatedFailuresOverTimeCircuitBreaker(string name, TimeSpan timeToWaitBeforeTriggering, Action triggerAction, TimeSpan delayAfterFailure) + { + this.delayAfterFailure = delayAfterFailure; + this.name = name; + this.triggerAction = triggerAction; + this.timeToWaitBeforeTriggering = timeToWaitBeforeTriggering; + + timer = new Timer(CircuitBreakerTriggered); + } + + public void Dispose() + { + //Injected + } + + public void Success() + { + var oldValue = Interlocked.Exchange(ref failureCount, 0); + + if (oldValue == 0) + { + return; + } + + timer.Change(System.Threading.Timeout.Infinite, System.Threading.Timeout.Infinite); + Logger.InfoFormat("The circuit breaker for {0} is now disarmed", name); + } + + public Task Failure(Exception exception) + { + lastException = exception; + var newValue = Interlocked.Increment(ref failureCount); + + if (newValue == 1) + { + timer.Change(timeToWaitBeforeTriggering, NoPeriodicTriggering); + Logger.WarnFormat("The circuit breaker for {0} is now in the armed state", name); + } + + return Task.Delay(delayAfterFailure); + } + + void CircuitBreakerTriggered(object state) + { + if (Interlocked.Read(ref failureCount) > 0) + { + Logger.WarnFormat("The circuit breaker for {0} will now be triggered", name); + triggerAction(lastException); + } + } + + readonly TimeSpan delayAfterFailure; + + long failureCount; + Exception lastException; + + string name; + Timer timer; + TimeSpan timeToWaitBeforeTriggering; + Action triggerAction; + + static TimeSpan NoPeriodicTriggering = TimeSpan.FromMilliseconds(-1); + static ILog Logger = LogManager.GetLogger(); + } +} \ No newline at end of file diff --git a/src/ServiceControl.Persistence.RavenDb/RetryBatchesDataStore.cs b/src/ServiceControl.Persistence.RavenDb/RetryBatchesDataStore.cs new file mode 100644 index 0000000000..c3c125de4f --- /dev/null +++ b/src/ServiceControl.Persistence.RavenDb/RetryBatchesDataStore.cs @@ -0,0 +1,96 @@ +namespace ServiceControl.Persistence.RavenDb +{ + using System; + using System.Collections.Generic; + using System.Threading.Tasks; + using MessageFailures; + using NServiceBus.Logging; + using Raven.Abstractions.Commands; + using Raven.Abstractions.Data; + using Raven.Abstractions.Exceptions; + using Raven.Client; + using ServiceControl.Recoverability; + + class RetryBatchesDataStore : IRetryBatchesDataStore + { + readonly IDocumentStore documentStore; + + static ILog Log = LogManager.GetLogger(typeof(RetryBatchesDataStore)); + + public RetryBatchesDataStore(IDocumentStore documentStore) + { + this.documentStore = documentStore; + } + + public Task CreateRetryBatchesManager() + { + var session = documentStore.OpenAsyncSession(); + var manager = new RetryBatchesManager(session); + + return Task.FromResult(manager); + } + + public async Task RecordFailedStagingAttempt(IReadOnlyCollection messages, + IReadOnlyDictionary failedMessageRetriesById, Exception e, + int maxStagingAttempts, string stagingId) + { + var commands = new ICommandData[messages.Count]; + var commandIndex = 0; + foreach (var failedMessage in messages) + { + var failedMessageRetry = failedMessageRetriesById[failedMessage.Id]; + + Log.Warn($"Attempt {1} of {maxStagingAttempts} to stage a retry message {failedMessage.UniqueMessageId} failed", e); + + commands[commandIndex] = new PatchCommandData + { + Patches = new[] + { + new PatchRequest + { + Type = PatchCommandType.Set, Name = "StageAttempts", Value = 1 + } + }, + Key = failedMessageRetry.Id + }; + + commandIndex++; + } + + + try + { + await documentStore.AsyncDatabaseCommands.BatchAsync(commands); + } + catch (ConcurrencyException) + { + Log.DebugFormat( + "Ignoring concurrency exception while incrementing staging attempt count for {0}", + stagingId); + } + } + + public async Task IncrementAttemptCounter(FailedMessageRetry message) + { + try + { + await documentStore.AsyncDatabaseCommands.PatchAsync(message.Id, + new[] + { + new PatchRequest + { + Type = PatchCommandType.Set, + Name = "StageAttempts", + Value = message.StageAttempts + 1 + } + }); + } + catch (ConcurrencyException) + { + Log.DebugFormat("Ignoring concurrency exception while incrementing staging attempt count for {0}", message.FailedMessageId); + } + } + + public Task DeleteFailedMessageRetry(string uniqueMessageId) => documentStore.AsyncDatabaseCommands.DeleteAsync(FailedMessageRetry.MakeDocumentId(uniqueMessageId), null); + } +} \ No newline at end of file diff --git a/src/ServiceControl.Persistence.RavenDb/RetryBatchesManager.cs b/src/ServiceControl.Persistence.RavenDb/RetryBatchesManager.cs new file mode 100644 index 0000000000..15edba340e --- /dev/null +++ b/src/ServiceControl.Persistence.RavenDb/RetryBatchesManager.cs @@ -0,0 +1,58 @@ +namespace ServiceControl.Persistence.RavenDb +{ + using System; + using System.Collections.Generic; + using System.Threading; + using System.Threading.Tasks; + using MessageFailures; + using Persistence.MessageRedirects; + using Raven.Client; + using ServiceControl.Recoverability; + + class RetryBatchesManager : AbstractSessionManager, IRetryBatchesManager + { + public RetryBatchesManager(IAsyncDocumentSession session) : base(session) + { + } + + public void Delete(RetryBatch retryBatch) => Session.Delete(retryBatch); + + public void Delete(RetryBatchNowForwarding forwardingBatch) => Session.Delete(forwardingBatch); + + public Task GetFailedMessageRetries(IList stagingBatchFailureRetries) => Session.LoadAsync(stagingBatchFailureRetries); + + public void Evict(FailedMessageRetry failedMessageRetry) => Session.Advanced.Evict(failedMessageRetry); + + public Task GetFailedMessages(Dictionary.KeyCollection keys) => Session.LoadAsync(keys); + + public async Task GetRetryBatchNowForwarding() => + await Session.Include(r => r.RetryBatchId) + .LoadAsync(RetryBatchNowForwarding.Id); + + public async Task GetRetryBatch(string retryBatchId, CancellationToken cancellationToken) => + await Session.LoadAsync(retryBatchId, cancellationToken); + + public async Task GetStagingBatch() => + await Session.Query() + .Customize(q => q.Include(b => b.FailureRetries)) + .FirstOrDefaultAsync(b => b.Status == RetryBatchStatus.Staging); + + public async Task Store(RetryBatchNowForwarding retryBatchNowForwarding) => + await Session.StoreAsync(retryBatchNowForwarding, RetryBatchNowForwarding.Id); + + public async Task GetOrCreateMessageRedirectsCollection() + { + var redirects = await Session.LoadAsync(MessageRedirectsCollection.DefaultId); + + if (redirects != null) + { + redirects.ETag = Session.Advanced.GetEtagFor(redirects); + redirects.LastModified = Session.Advanced.GetMetadataFor(redirects).Value("Last-Modified"); + + return redirects; + } + + return new MessageRedirectsCollection(); + } + } +} \ No newline at end of file diff --git a/src/ServiceControl.Persistence.RavenDb/RetryDocumentDataStore.cs b/src/ServiceControl.Persistence.RavenDb/RetryDocumentDataStore.cs new file mode 100644 index 0000000000..582dde65a6 --- /dev/null +++ b/src/ServiceControl.Persistence.RavenDb/RetryDocumentDataStore.cs @@ -0,0 +1,303 @@ +namespace ServiceControl.Persistence.RavenDb +{ + using System; + using System.Collections.Generic; + using System.Linq; + using System.Linq.Expressions; + using System.Threading.Tasks; + using MessageFailures; + using NServiceBus.Logging; + using Persistence.Infrastructure; + using Raven.Abstractions.Commands; + using Raven.Abstractions.Data; + using Raven.Abstractions.Exceptions; + using Raven.Client; + using Raven.Client.Indexes; + using Raven.Client.Linq; + using Raven.Json.Linq; + using ServiceControl.MessageFailures.Api; + using ServiceControl.Recoverability; + + class RetryDocumentDataStore : IRetryDocumentDataStore + { + readonly IDocumentStore store; + + public RetryDocumentDataStore(IDocumentStore store) + { + this.store = store; + } + + public async Task StageRetryByUniqueMessageIds(string batchDocumentId, string requestId, RetryType retryType, string[] messageIds, + DateTime startTime, + DateTime? last = null, string originator = null, string batchName = null, string classifier = null) + { + var commands = new ICommandData[messageIds.Length]; + + for (var i = 0; i < messageIds.Length; i++) + { + commands[i] = CreateFailedMessageRetryDocument(batchDocumentId, messageIds[i]); + } + + await store.AsyncDatabaseCommands.BatchAsync(commands); + } + + public async Task MoveBatchToStaging(string batchDocumentId) + { + try + { + await store.AsyncDatabaseCommands.PatchAsync(batchDocumentId, + new[] + { + new PatchRequest + { + Type = PatchCommandType.Set, + Name = "Status", + Value = (int)RetryBatchStatus.Staging, + PrevVal = (int)RetryBatchStatus.MarkingDocuments + } + }); + } + catch (ConcurrencyException) + { + log.DebugFormat("Ignoring concurrency exception while moving batch to staging {0}", batchDocumentId); + } + } + + public async Task CreateBatchDocument(string retrySessionId, string requestId, RetryType retryType, string[] failedMessageRetryIds, + string originator, + DateTime startTime, DateTime? last = null, string batchName = null, string classifier = null) + { + var batchDocumentId = RetryBatch.MakeDocumentId(Guid.NewGuid().ToString()); + using (var session = store.OpenAsyncSession()) + { + await session.StoreAsync(new RetryBatch + { + Id = batchDocumentId, + Context = batchName, + RequestId = requestId, + RetryType = retryType, + Originator = originator, + Classifier = classifier, + StartTime = startTime, + Last = last, + InitialBatchSize = failedMessageRetryIds.Length, + RetrySessionId = retrySessionId, + FailureRetries = failedMessageRetryIds, + Status = RetryBatchStatus.MarkingDocuments + }); + await session.SaveChangesAsync(); + } + + return batchDocumentId; + } + + public async Task>> QueryOrphanedBatches(string retrySessionId, DateTime cutoff) + { + using (var session = store.OpenAsyncSession()) + { + var orphanedBatches = await session.Query() + .Customize(c => c.BeforeQueryExecution(index => index.Cutoff = cutoff)) + .Where(b => b.Status == RetryBatchStatus.MarkingDocuments && b.RetrySessionId != retrySessionId) + .Statistics(out var stats) + .ToListAsync(); + + return orphanedBatches.ToQueryResult(stats); + } + } + + public async Task> QueryAvailableBatches() + { + using (var session = store.OpenAsyncSession()) + { + var results = await session.Query() + .Where(b => b.HasStagingBatches || b.HasForwardingBatches) + .ToListAsync(); + return results; + } + } + + static ICommandData CreateFailedMessageRetryDocument(string batchDocumentId, string messageId) + { + return new PatchCommandData + { + Patches = PatchRequestsEmpty, + PatchesIfMissing = new[] + { + new PatchRequest + { + Name = "FailedMessageId", + Type = PatchCommandType.Set, + Value = FailedMessageIdGenerator.MakeDocumentId(messageId) + }, + new PatchRequest + { + Name = "RetryBatchId", + Type = PatchCommandType.Set, + Value = batchDocumentId + } + }, + Key = FailedMessageRetry.MakeDocumentId(messageId), + Metadata = DefaultMetadata + }; + } + + static RavenJObject DefaultMetadata = RavenJObject.Parse($@" + {{ + ""Raven-Entity-Name"": ""{FailedMessageRetry.CollectionName}"", + ""Raven-Clr-Type"": ""{typeof(FailedMessageRetry).AssemblyQualifiedName}"" + }}"); + + static PatchRequest[] PatchRequestsEmpty = Array.Empty(); + + static ILog log = LogManager.GetLogger(typeof(RetryDocumentDataStore)); + + + + public async Task GetBatchesForAll(DateTime cutoff, Func callback) + { + // StartRetryForIndex("All", RetryType.All, DateTime.UtcNow, originator: "all messages"); + //public void StartRetryForIndex(string requestId, RetryType retryType, DateTime startTime, Expression> filter = null, string originator = null, string classifier = null) + //StartRetryForIndex(endpoint, RetryType.AllForEndpoint, DateTime.UtcNow, m => m.ReceivingEndpointName == endpoint, $"all messages for endpoint {endpoint}"); + + var x = new IndexBasedBulkRetryRequest( + "All", + RetryType.All, + originator: "all messages", + classifier: null, + cutoff, + filter: null + ); + + using (var session = store.OpenAsyncSession()) + using (var stream = await x.GetDocuments(session)) + { + while (await stream.MoveNextAsync()) + { + var current = stream.Current.Document; + await callback(current.UniqueMessageId, current.LatestTimeOfFailure); + } + } + } + + public async Task GetBatchesForEndpoint(DateTime cutoff, string endpoint, Func callback) + { + //ForIndex + //StartRetryForIndex(endpoint, RetryType.AllForEndpoint, DateTime.UtcNow, m => m.ReceivingEndpointName == endpoint, $"all messages for endpoint {endpoint}"); + + var x = new IndexBasedBulkRetryRequest( + endpoint, + RetryType.AllForEndpoint, + originator: $"all messages for endpoint {endpoint}", + classifier: null, + cutoff, + filter: m => m.ReceivingEndpointName == endpoint + ); + + using (var session = store.OpenAsyncSession()) + using (var stream = await x.GetDocuments(session)) + { + while (await stream.MoveNextAsync()) + { + var current = stream.Current.Document; + await callback(current.UniqueMessageId, current.LatestTimeOfFailure); + } + } + } + + public async Task GetBatchesForFailedQueueAddress(DateTime cutoff, string failedQueueAddress, FailedMessageStatus status, Func callback) + { + //ForIndex + //StartRetryForIndex(failedQueueAddress, RetryType.ByQueueAddress, DateTime.UtcNow, m => m.QueueAddress == failedQueueAddress && m.Status == status, ); + + var x = new IndexBasedBulkRetryRequest( + failedQueueAddress, + RetryType.AllForEndpoint, + originator: $"all messages for failed queue address '{failedQueueAddress}'", + classifier: null, + cutoff, + filter: m => m.QueueAddress == failedQueueAddress && m.Status == status + ); + + using (var session = store.OpenAsyncSession()) + using (var stream = await x.GetDocuments(session)) + { + while (await stream.MoveNextAsync()) + { + var current = stream.Current.Document; + await callback(current.UniqueMessageId, current.LatestTimeOfFailure); + } + } + } + + public async Task GetBatchesForFailureGroup(string groupId, string groupTitle, string groupType, DateTime cutoff, Func callback) + { + //retries.StartRetryForIndex(message.GroupId, RetryType.FailureGroup, started, x => x.FailureGroupId == message.GroupId, originator, group?.Type); + + var x = new IndexBasedBulkRetryRequest( + groupId, + RetryType.AllForEndpoint, + originator: groupTitle, + classifier: groupType, + cutoff, + filter: m => m.FailureGroupId == groupId + ); + + using (var session = store.OpenAsyncSession()) + using (var stream = await x.GetDocuments(session)) + { + while (await stream.MoveNextAsync()) + { + var current = stream.Current.Document; + await callback(current.UniqueMessageId, current.LatestTimeOfFailure); + } + } + } + + public async Task QueryFailureGroupViewOnGroupId(string groupId) + { + using (var session = store.OpenAsyncSession()) + { + var group = await session.Query() + .FirstOrDefaultAsync(x => x.Id == groupId); + return group; + } + } + + class IndexBasedBulkRetryRequest + where TIndex : AbstractIndexCreationTask, new() + where TType : IHaveStatus + { + public IndexBasedBulkRetryRequest(string requestId, RetryType retryType, string originator, string classifier, DateTime startTime, Expression> filter) + { + RequestId = requestId; + RetryType = retryType; + Originator = originator; + this.filter = filter; + StartTime = startTime; + Classifier = classifier; + } + + public string RequestId { get; set; } + public RetryType RetryType { get; set; } + public string Originator { get; set; } + public string Classifier { get; set; } + public DateTime StartTime { get; set; } + + public Task>> GetDocuments(IAsyncDocumentSession session) + { + var query = session.Query(); + + query = query.Where(d => d.Status == FailedMessageStatus.Unresolved); + + if (filter != null) + { + query = query.Where(filter); + } + + return session.Advanced.StreamAsync(query.TransformWith()); + } + + readonly Expression> filter; + } + } +} \ No newline at end of file diff --git a/src/ServiceControl/SagaAudit/AuditRetentionCustomCheck.cs b/src/ServiceControl.Persistence.RavenDb/SagaAudit/AuditRetentionCustomCheck.cs similarity index 70% rename from src/ServiceControl/SagaAudit/AuditRetentionCustomCheck.cs rename to src/ServiceControl.Persistence.RavenDb/SagaAudit/AuditRetentionCustomCheck.cs index 8c816919d6..6204aa696d 100644 --- a/src/ServiceControl/SagaAudit/AuditRetentionCustomCheck.cs +++ b/src/ServiceControl.Persistence.RavenDb/SagaAudit/AuditRetentionCustomCheck.cs @@ -1,22 +1,26 @@ -namespace ServiceControl.SagaAudit + +namespace ServiceControl.Persistence.RavenDb.SagaAudit { using System; using System.Threading.Tasks; using NServiceBus.CustomChecks; + using Persistence; + using Persistence.RavenDb; using Raven.Client; - using ServiceBus.Management.Infrastructure.Settings; + using ServiceControl.SagaAudit; + // This custom check stays in the Raven3.5 persister becuase the Raven5 persister will not store audit data class AuditRetentionCustomCheck : CustomCheck { readonly IDocumentStore _documentStore; readonly bool _auditRetentionPeriodIsSet; - public AuditRetentionCustomCheck(IDocumentStore documentStore, Settings settings, TimeSpan? repeatAfter = null) + public AuditRetentionCustomCheck(IDocumentStore documentStore, PersistenceSettings settings, TimeSpan? repeatAfter = null) : base("Saga Audit Data Retention", "ServiceControl Health", repeatAfter.HasValue ? repeatAfter : TimeSpan.FromHours(1)) { _documentStore = documentStore; - _auditRetentionPeriodIsSet = settings.AuditRetentionPeriod.HasValue; + _auditRetentionPeriodIsSet = settings.PersisterSpecificSettings.ContainsKey(RavenDbPersistenceConfiguration.AuditRetentionPeriodKey); } public override async Task PerformCheck() @@ -26,7 +30,7 @@ public override async Task PerformCheck() return CheckResult.Pass; } - if (await DetectSagaAuditData().ConfigureAwait(false)) + if (await DetectSagaAuditData()) { return CheckResult.Failed("Saga snapshot data detected without an audit retention period configured. If saga audit data is allowed to accumulate, it can result in degraded performance. Visit https://docs.particular.net/search?q=servicecontrol+troubleshooting for more information."); } @@ -38,7 +42,7 @@ async Task DetectSagaAuditData() { using (var session = _documentStore.OpenAsyncSession()) { - return await session.Query().AnyAsync().ConfigureAwait(false); + return await session.Query().AnyAsync(); } } } diff --git a/src/ServiceControl.Persistence.RavenDb/SagaAudit/SagaAuditDataStore.cs b/src/ServiceControl.Persistence.RavenDb/SagaAudit/SagaAuditDataStore.cs new file mode 100644 index 0000000000..69af8b1da6 --- /dev/null +++ b/src/ServiceControl.Persistence.RavenDb/SagaAudit/SagaAuditDataStore.cs @@ -0,0 +1,45 @@ +namespace ServiceControl.Persistence.RavenDb +{ + using System; + using System.Threading.Tasks; + using Raven.Client; + using ServiceControl.Persistence.Infrastructure; + using ServiceControl.SagaAudit; + + class SagaAuditDataStore : ISagaAuditDataStore + { + public SagaAuditDataStore(IDocumentStore store) + { + this.store = store; + } + + public async Task StoreSnapshot(SagaSnapshot sagaSnapshot) + { + using (var session = store.OpenAsyncSession()) + { + await session.StoreAsync(sagaSnapshot); + await session.SaveChangesAsync(); + } + } + + public async Task> GetSagaById(Guid sagaId) + { + using (var session = store.OpenAsyncSession()) + { + var sagaHistory = await + session.Query() + .Statistics(out var stats) + .SingleOrDefaultAsync(x => x.SagaId == sagaId); + + if (sagaHistory == null) + { + return QueryResult.Empty(); + } + + return new QueryResult(sagaHistory, new QueryStatsInfo(stats.IndexEtag, stats.TotalResults, stats.IsStale)); + } + } + + readonly IDocumentStore store; + } +} diff --git a/src/ServiceControl/SagaAudit/SagaListIndex.cs b/src/ServiceControl.Persistence.RavenDb/SagaAudit/SagaListIndex.cs similarity index 94% rename from src/ServiceControl/SagaAudit/SagaListIndex.cs rename to src/ServiceControl.Persistence.RavenDb/SagaAudit/SagaListIndex.cs index 8ce2c52090..73961a8b79 100644 --- a/src/ServiceControl/SagaAudit/SagaListIndex.cs +++ b/src/ServiceControl.Persistence.RavenDb/SagaAudit/SagaListIndex.cs @@ -4,7 +4,7 @@ namespace ServiceControl.SagaAudit using System.Linq; using Raven.Client.Indexes; - public class SagaListIndex : AbstractMultiMapIndexCreationTask + class SagaListIndex : AbstractMultiMapIndexCreationTask { public SagaListIndex() { diff --git a/src/ServiceControl.Persistence.RavenDb/ServiceControl.Persistence.RavenDb.csproj b/src/ServiceControl.Persistence.RavenDb/ServiceControl.Persistence.RavenDb.csproj index b2226055b7..fc725447dd 100644 --- a/src/ServiceControl.Persistence.RavenDb/ServiceControl.Persistence.RavenDb.csproj +++ b/src/ServiceControl.Persistence.RavenDb/ServiceControl.Persistence.RavenDb.csproj @@ -4,16 +4,32 @@ net472 + + + + + + + + + + + + - + + + + + \ No newline at end of file diff --git a/src/ServiceControl.Persistence.RavenDb/Transactions/RavenTransactionalDataStore.cs b/src/ServiceControl.Persistence.RavenDb/Transactions/RavenTransactionalDataStore.cs new file mode 100644 index 0000000000..caa69201b3 --- /dev/null +++ b/src/ServiceControl.Persistence.RavenDb/Transactions/RavenTransactionalDataStore.cs @@ -0,0 +1,14 @@ +namespace ServiceControl.Persistence.RavenDb +{ + using System.Threading.Tasks; + using Raven.Client; + + abstract class AbstractSessionManager : IDataSessionManager + { + protected IAsyncDocumentSession Session { get; } + + protected AbstractSessionManager(IAsyncDocumentSession session) => Session = session; + public Task SaveChanges() => Session.SaveChangesAsync(); + public void Dispose() => Session.Dispose(); + } +} \ No newline at end of file diff --git a/src/ServiceControl/MessageFailures/Api/FailedMessageViewTransformer.cs b/src/ServiceControl.Persistence.RavenDb/Transformers/FailedMessageViewTransformer.cs similarity index 96% rename from src/ServiceControl/MessageFailures/Api/FailedMessageViewTransformer.cs rename to src/ServiceControl.Persistence.RavenDb/Transformers/FailedMessageViewTransformer.cs index eedb9c8dfa..157a7ce15e 100644 --- a/src/ServiceControl/MessageFailures/Api/FailedMessageViewTransformer.cs +++ b/src/ServiceControl.Persistence.RavenDb/Transformers/FailedMessageViewTransformer.cs @@ -4,7 +4,7 @@ using System.Linq; using Raven.Client.Indexes; - public class FailedMessageViewTransformer : AbstractTransformerCreationTask + class FailedMessageViewTransformer : AbstractTransformerCreationTask { public FailedMessageViewTransformer() { diff --git a/src/ServiceControl/CompositeViews/Messages/MessagesBodyTransformer.cs b/src/ServiceControl.Persistence.RavenDb/Transformers/MessagesBodyTransformer.cs similarity index 95% rename from src/ServiceControl/CompositeViews/Messages/MessagesBodyTransformer.cs rename to src/ServiceControl.Persistence.RavenDb/Transformers/MessagesBodyTransformer.cs index 9043575070..18a53ae86b 100644 --- a/src/ServiceControl/CompositeViews/Messages/MessagesBodyTransformer.cs +++ b/src/ServiceControl.Persistence.RavenDb/Transformers/MessagesBodyTransformer.cs @@ -5,7 +5,7 @@ namespace ServiceControl.CompositeViews.Messages using Raven.Client.Indexes; using ServiceControl.MessageFailures; - public class MessagesBodyTransformer : AbstractTransformerCreationTask + class MessagesBodyTransformer : AbstractTransformerCreationTask { public MessagesBodyTransformer() { diff --git a/src/ServiceControl/CompositeViews/Messages/MessagesViewTransformer.cs b/src/ServiceControl.Persistence.RavenDb/Transformers/MessagesViewTransformer.cs similarity index 97% rename from src/ServiceControl/CompositeViews/Messages/MessagesViewTransformer.cs rename to src/ServiceControl.Persistence.RavenDb/Transformers/MessagesViewTransformer.cs index c9fc5ad706..98047d961b 100644 --- a/src/ServiceControl/CompositeViews/Messages/MessagesViewTransformer.cs +++ b/src/ServiceControl.Persistence.RavenDb/Transformers/MessagesViewTransformer.cs @@ -7,7 +7,7 @@ namespace ServiceControl.CompositeViews.Messages using Raven.Client.Indexes; using ServiceControl.Persistence; - public class MessagesViewTransformer : AbstractTransformerCreationTask + class MessagesViewTransformer : AbstractTransformerCreationTask { public MessagesViewTransformer() { diff --git a/src/ServiceControl.Persistence.RavenDb/UnitOfWork/RavenDbRecoverabilityIngestionUnitOfWork.cs b/src/ServiceControl.Persistence.RavenDb/UnitOfWork/RavenDbRecoverabilityIngestionUnitOfWork.cs index 95ab57abb2..6ec853c166 100644 --- a/src/ServiceControl.Persistence.RavenDb/UnitOfWork/RavenDbRecoverabilityIngestionUnitOfWork.cs +++ b/src/ServiceControl.Persistence.RavenDb/UnitOfWork/RavenDbRecoverabilityIngestionUnitOfWork.cs @@ -33,7 +33,7 @@ public Task RecordFailedProcessingAttempt( public Task RecordSuccessfulRetry(string retriedMessageUniqueId) { - var failedMessageDocumentId = FailedMessage.MakeDocumentId(retriedMessageUniqueId); + var failedMessageDocumentId = FailedMessageIdGenerator.MakeDocumentId(retriedMessageUniqueId); var failedMessageRetryDocumentId = FailedMessageRetry.MakeDocumentId(retriedMessageUniqueId); parentUnitOfWork.AddCommand(new PatchCommandData @@ -52,7 +52,7 @@ public Task RecordSuccessfulRetry(string retriedMessageUniqueId) ICommandData CreateFailedMessagesPatchCommand(string uniqueMessageId, FailedMessage.ProcessingAttempt processingAttempt, List groups) { - var documentId = FailedMessage.MakeDocumentId(uniqueMessageId); + var documentId = FailedMessageIdGenerator.MakeDocumentId(uniqueMessageId); var serializedGroups = RavenJToken.FromObject(groups); var serializedAttempt = RavenJToken.FromObject(processingAttempt, Serializer); @@ -125,7 +125,7 @@ static RavenDbRecoverabilityIngestionUnitOfWork() FailedMessageMetadata = RavenJObject.Parse($@" {{ - ""Raven-Entity-Name"": ""{FailedMessage.CollectionName}"", + ""Raven-Entity-Name"": ""{FailedMessageIdGenerator.CollectionName}"", ""Raven-Clr-Type"": ""{typeof(FailedMessage).AssemblyQualifiedName}"" }}"); } diff --git a/src/ServiceControl.Persistence.RavenDb/WaitHandleExtensions.cs b/src/ServiceControl.Persistence.RavenDb/WaitHandleExtensions.cs new file mode 100644 index 0000000000..5535209353 --- /dev/null +++ b/src/ServiceControl.Persistence.RavenDb/WaitHandleExtensions.cs @@ -0,0 +1,44 @@ +namespace ServiceBus.Management.Infrastructure.Extensions +{ + using System; + using System.Threading; + using System.Threading.Tasks; + + static class WaitHandleExtensions + { + public static async Task WaitOneAsync(this WaitHandle handle, int millisecondsTimeout, CancellationToken cancellationToken = default) + { + RegisteredWaitHandle registeredHandle = null; + var tokenRegistration = default(CancellationTokenRegistration); + try + { + var tcs = new TaskCompletionSource(); + registeredHandle = ThreadPool.RegisterWaitForSingleObject( + handle, + (state, timedOut) => ((TaskCompletionSource)state).TrySetResult(!timedOut), + tcs, + millisecondsTimeout, + true); + tokenRegistration = cancellationToken.Register( + state => ((TaskCompletionSource)state).TrySetCanceled(), + tcs); + return await tcs.Task; + } + finally + { + registeredHandle?.Unregister(null); + tokenRegistration.Dispose(); + } + } + + public static Task WaitOneAsync(this WaitHandle handle, TimeSpan timeout, CancellationToken cancellationToken = default) + { + return handle.WaitOneAsync((int)timeout.TotalMilliseconds, cancellationToken); + } + + public static Task WaitOneAsync(this WaitHandle handle, CancellationToken cancellationToken = default) + { + return handle.WaitOneAsync(Timeout.Infinite, cancellationToken); + } + } +} \ No newline at end of file diff --git a/src/ServiceControl.Persistence.RavenDb/persistence.manifest b/src/ServiceControl.Persistence.RavenDb/persistence.manifest new file mode 100644 index 0000000000..4b3fab078a --- /dev/null +++ b/src/ServiceControl.Persistence.RavenDb/persistence.manifest @@ -0,0 +1,35 @@ +{ + "Version": "1.0.0", + "Name": "RavenDB35", + "DisplayName": "RavenDB 3.5", + "Description": "RavenDB 3.5 ServiceControl persister", + "TypeName": "ServiceControl.Persistence.RavenDb.RavenDbPersistenceConfiguration, ServiceControl.Persistence.RavenDb", + "Settings": [ + { + "Name": "Raven/ESENT/MaxPageSize", + "DefaultValue": "4096" + }, + { + "Name": "ServiceControl/RavenDB35/RunCleanupBundle", + "DefaultValue": "true" + }, + { + "Name": "ServiceControl/DBPath", + "Mandatory": true + }, + { + "Name": "ServiceControl/HostName", + "Mandatory": true + }, + { + "Name": "ServiceControl/DatabaseMaintenancePort", + "Mandatory": true + } + ], + "SettingsWithPathsToCleanup": [ + "Raven/IndexStoragePath", + "Raven/CompiledIndexCacheDirectory", + "Raven/Esent/LogsPath", + "ServiceControl/DBPath" + ] +} diff --git a/src/ServiceControl.Persistence.SqlServer/ServiceControl.Persistence.SqlServer.csproj b/src/ServiceControl.Persistence.SqlServer/ServiceControl.Persistence.SqlServer.csproj deleted file mode 100644 index 38cfcad190..0000000000 --- a/src/ServiceControl.Persistence.SqlServer/ServiceControl.Persistence.SqlServer.csproj +++ /dev/null @@ -1,20 +0,0 @@ - - - - net472 - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/src/ServiceControl.Persistence.SqlServer/SqlDbConnectionManager.cs b/src/ServiceControl.Persistence.SqlServer/SqlDbConnectionManager.cs deleted file mode 100644 index e80d978099..0000000000 --- a/src/ServiceControl.Persistence.SqlServer/SqlDbConnectionManager.cs +++ /dev/null @@ -1,39 +0,0 @@ -namespace ServiceControl.Persistence.SqlServer -{ - using System; - using System.Data; - using System.Data.SqlClient; - using System.Threading.Tasks; - using ServiceControl.Persistence.Infrastructure; - - class SqlDbConnectionManager - { - public string ConnectionString { get; } - - public SqlDbConnectionManager(string connectionString) => ConnectionString = connectionString; - } - - static class SqlDbConnectionManagerExtensions - { - public static async Task Perform(this SqlDbConnectionManager manager, Func action) - { - using (var connection = new SqlConnection(manager.ConnectionString)) - { - await connection.OpenAsync().ConfigureAwait(false); - await action(connection).ConfigureAwait(false); - } - } - - public static async Task> PagedQuery(this SqlDbConnectionManager manager, Func>> action) where T : class - { - using (var connection = new SqlConnection(manager.ConnectionString)) - { - await connection.OpenAsync().ConfigureAwait(false); - - var result = await action(connection).ConfigureAwait(false); - - return result; - } - } - } -} \ No newline at end of file diff --git a/src/ServiceControl.Persistence.SqlServer/SqlDbCustomCheckDataStore.cs b/src/ServiceControl.Persistence.SqlServer/SqlDbCustomCheckDataStore.cs deleted file mode 100644 index 8435c19192..0000000000 --- a/src/ServiceControl.Persistence.SqlServer/SqlDbCustomCheckDataStore.cs +++ /dev/null @@ -1,128 +0,0 @@ -namespace ServiceControl.Persistence.SqlServer -{ - using System; - using System.Collections.Generic; - using System.Threading.Tasks; - using Dapper; - using Infrastructure; - using ServiceControl.Contracts.CustomChecks; - using ServiceControl.Operations; - using ServiceControl.Persistence; - - class SqlDbCustomCheckDataStore : ICustomChecksDataStore - { - public SqlDbCustomCheckDataStore(SqlDbConnectionManager connectionManager) - { - this.connectionManager = connectionManager; - } - - public async Task UpdateCustomCheckStatus(CustomCheckDetail detail) - { - var status = CheckStateChange.Unchanged; - var id = detail.GetDeterministicId(); - - await connectionManager.Perform(async connection => - { - var customCheck = await connection.QueryFirstOrDefaultAsync( - "SELECT * FROM [CustomChecks] WHERE [Id] = @Id", - new { Id = id }).ConfigureAwait(false); - - if (customCheck == null || - ((int)customCheck.Status == (int)Status.Fail && !detail.HasFailed) || - ((int)customCheck.Status == (int)Status.Pass && detail.HasFailed)) - { - status = CheckStateChange.Changed; - } - - await connection.ExecuteAsync( - @"IF EXISTS(SELECT * FROM [CustomChecks] WHERE Id = @Id) - UPDATE [CustomChecks] SET - [CustomCheckId] = @CustomCheckId, - [Category] = @Category, - [Status] = @Status, - [ReportedAt] = @ReportedAt, - [FailureReason] = @FailureReason, - [OriginatingEndpointName] = @OriginatingEndpointName, - [OriginatingEndpointHost] = @OriginatingEndpointHost, - [OriginatingEndpointHostId] = @OriginatingEndpointHostId - WHERE [Id] = @Id - ELSE - INSERT INTO [CustomChecks](Id, CustomCheckId, Category, Status, ReportedAt, FailureReason, OriginatingEndpointName, OriginatingEndpointHost, OriginatingEndpointHostId) - VALUES(@Id, @CustomCheckId, @Category, @Status, @ReportedAt, @FailureReason, @OriginatingEndpointName, @OriginatingEndpointHost, @OriginatingEndpointHostId)", - new - { - Id = id, - detail.CustomCheckId, - detail.Category, - detail.ReportedAt, - detail.FailureReason, - Status = detail.HasFailed ? Status.Fail : Status.Pass, - OriginatingEndpointName = detail.OriginatingEndpoint.Name, - OriginatingEndpointHostId = detail.OriginatingEndpoint.HostId, - OriginatingEndpointHost = detail.OriginatingEndpoint.Host - }).ConfigureAwait(false); - }).ConfigureAwait(false); - - return status; - } - - public async Task>> GetStats(PagingInfo paging, string status = null) - { - var checks = new List(); - var totalCount = 0; - _ = Enum.TryParse(status, true, out var checkStatus); - - return await connectionManager.PagedQuery(async connection => - { - var filter = @" WHERE [Status] = @Status "; - var query = @"SELECT * FROM [CustomChecks] " + filter + - @"ORDER BY ID - OFFSET @Offset ROWS - FETCH NEXT @Next ROWS ONLY;"; - - var countQuery = @"SELECT COUNT(Id) FROM [CustomChecks] " + filter; - - using (var multi = await connection.QueryMultipleAsync(query + countQuery, new { paging.Offset, paging.Next, Status = checkStatus }).ConfigureAwait(false)) - { - var rows = await multi.ReadAsync().ConfigureAwait(false); - foreach (dynamic row in rows) - { - checks.Add(new CustomCheck - { - Id = row.Id, - CustomCheckId = row.CustomCheckId, - Status = (Status)row.Status, - Category = row.Category, - FailureReason = row.FailureReason, - ReportedAt = row.ReportedAt, - OriginatingEndpoint = new EndpointDetails - { - Name = row.OriginatingEndpointName, - HostId = row.OriginatingEndpointHostId, - Host = row.OriginatingEndpointHost - } - }); - } - totalCount = multi.ReadFirst(); - } - - return new QueryResult>(checks, new QueryStatsInfo(null, totalCount)); - }).ConfigureAwait(false); - } - - public async Task DeleteCustomCheck(Guid id) - { - await connectionManager.Perform(async connection => - { - await connection.ExecuteAsync( - @"DELETE FROM [dbo].[CustomChecks] WHERE [Id] = @Id", - new - { - Id = id - }).ConfigureAwait(false); - }).ConfigureAwait(false); - } - - readonly SqlDbConnectionManager connectionManager; - } -} \ No newline at end of file diff --git a/src/ServiceControl.Persistence.SqlServer/SqlDbMonitoringDataStore.cs b/src/ServiceControl.Persistence.SqlServer/SqlDbMonitoringDataStore.cs deleted file mode 100644 index a228c5ef0c..0000000000 --- a/src/ServiceControl.Persistence.SqlServer/SqlDbMonitoringDataStore.cs +++ /dev/null @@ -1,136 +0,0 @@ -namespace ServiceControl.Persistence.SqlServer -{ - using System; - using System.Collections.Generic; - using System.Threading.Tasks; - using Dapper; - using ServiceControl.Operations; - using ServiceControl.Persistence; - - class SqlDbMonitoringDataStore : IMonitoringDataStore - { - readonly SqlDbConnectionManager connectionManager; - - public SqlDbMonitoringDataStore(SqlDbConnectionManager connectionManager) - { - this.connectionManager = connectionManager; - } - - public async Task CreateIfNotExists(EndpointDetails endpoint) - { - await connectionManager.Perform(async connection => - { - await connection.ExecuteAsync( - @"IF NOT EXISTS(SELECT * FROM [KnownEndpoints] WHERE Id = @Id) - BEGIN - INSERT INTO [KnownEndpoints](Id, HostId, Host, HostDisplayName, Monitored) - VALUES(@Id, @HostId, @Host, @HostDisplayName, @Monitored) - END", - new - { - Id = endpoint.GetDeterministicId(), - endpoint.HostId, - endpoint.Host, - HostDisplayName = endpoint.Name, - Monitored = false - }).ConfigureAwait(false); - }).ConfigureAwait(false); - } - - public async Task CreateOrUpdate(EndpointDetails endpoint, IEndpointInstanceMonitoring endpointInstanceMonitoring) - { - var id = endpoint.GetDeterministicId(); - - await connectionManager.Perform(async connection => - { - await connection.ExecuteAsync( - @"IF EXISTS(SELECT * FROM [KnownEndpoints] WHERE Id = @Id) - UPDATE [KnownEndpoints] SET [Monitored] = @Monitored WHERE [Id] = @Id - ELSE - INSERT INTO [KnownEndpoints](Id, HostId, Host, HostDisplayName, Monitored) - VALUES(@Id, @HostId, @Host, @HostDisplayName, 1)", - new - { - Id = id, - endpoint.HostId, - endpoint.Host, - HostDisplayName = endpoint.Name, - Monitored = endpointInstanceMonitoring.IsMonitored(id) - }).ConfigureAwait(false); - }).ConfigureAwait(false); - } - - public async Task UpdateEndpointMonitoring(EndpointDetails endpoint, bool isMonitored) - { - await connectionManager.Perform(async connection => - { - await connection.ExecuteAsync( - @"UPDATE [KnownEndpoints] SET [Monitored] = @Monitored WHERE [Id] = @Id", - new - { - Id = endpoint.GetDeterministicId(), - Monitored = isMonitored - }).ConfigureAwait(false); - }).ConfigureAwait(false); - } - - public async Task WarmupMonitoringFromPersistence(IEndpointInstanceMonitoring endpointInstanceMonitoring) - { - await connectionManager.Perform(async connection => - { - var rows = await connection.QueryAsync("SELECT * FROM [KnownEndpoints]").ConfigureAwait(false); - - foreach (dynamic row in rows) - { - endpointInstanceMonitoring.DetectEndpointFromPersistentStore(new EndpointDetails - { - HostId = row.HostId, - Host = row.Host, - Name = row.HostDisplayName - }, row.Monitored); - } - }).ConfigureAwait(false); - } - - public async Task Delete(Guid endpointId) - { - await connectionManager.Perform(async connection => - { - await connection.ExecuteAsync( - @"DELETE FROM [dbo].[KnownEndpoints] WHERE [Id] = @Id", - new - { - Id = endpointId - }).ConfigureAwait(false); - }).ConfigureAwait(false); - } - - public async Task> GetAllKnownEndpoints() - { - var endpoints = new List(); - - await connectionManager.Perform(async connection => - { - var rows = await connection.QueryAsync("SELECT * FROM [KnownEndpoints]").ConfigureAwait(false); - - foreach (var row in rows) - { - endpoints.Add(new KnownEndpoint - { - EndpointDetails = new EndpointDetails - { - Host = row.Host, - HostId = row.HostId, - Name = row.HostDisplayName - }, - HostDisplayName = row.HostDisplayName, - Id = row.Id, - Monitored = row.Monitored - }); - } - }).ConfigureAwait(false); - - return endpoints; - } - } -} \ No newline at end of file diff --git a/src/ServiceControl.Persistence.SqlServer/SqlDbPersistenceConfiguration.cs b/src/ServiceControl.Persistence.SqlServer/SqlDbPersistenceConfiguration.cs deleted file mode 100644 index 350ce630cf..0000000000 --- a/src/ServiceControl.Persistence.SqlServer/SqlDbPersistenceConfiguration.cs +++ /dev/null @@ -1,18 +0,0 @@ -namespace ServiceControl.Persistence.SqlServer -{ - using Microsoft.Extensions.DependencyInjection; - using ServiceBus.Management.Infrastructure.Settings; - using ServiceControl.Persistence.UnitOfWork; - - public class SqlDbPersistenceConfiguration : IPersistenceConfiguration - { - public void ConfigureServices(IServiceCollection serviceCollection) - { - serviceCollection.AddSingleton(sp => - new SqlDbConnectionManager(SettingsReader.Read("SqlStorageConnectionString"))); - serviceCollection.AddSingleton(); - serviceCollection.AddSingleton(); - serviceCollection.AddPartialUnitOfWorkFactory(); - } - } -} diff --git a/src/ServiceControl.Persistence.SqlServer/UnitOfWork/SqlIngestionUnitOfWork.cs b/src/ServiceControl.Persistence.SqlServer/UnitOfWork/SqlIngestionUnitOfWork.cs deleted file mode 100644 index 87f2424fcb..0000000000 --- a/src/ServiceControl.Persistence.SqlServer/UnitOfWork/SqlIngestionUnitOfWork.cs +++ /dev/null @@ -1,34 +0,0 @@ -namespace ServiceControl.Persistence.SqlServer -{ - using System.Data.SqlClient; - using System.Threading.Tasks; - using ServiceControl.Persistence.UnitOfWork; - - class SqlIngestionUnitOfWork : IngestionUnitOfWorkBase - { - readonly SqlConnection connection; - readonly SqlTransaction transaction; - - public SqlIngestionUnitOfWork(SqlConnection connection, SqlTransaction transaction) - { - this.connection = connection; - this.transaction = transaction; - Monitoring = new SqlMonitoringIngestionUnitOfWork(connection, transaction); - } - - public override Task Complete() - { - transaction.Commit(); - return Task.CompletedTask; - } - - protected override void Dispose(bool disposing) - { - if (disposing) - { - transaction?.Dispose(); - connection?.Dispose(); - } - } - } -} \ No newline at end of file diff --git a/src/ServiceControl.Persistence.SqlServer/UnitOfWork/SqlIngestionUnitOfWorkFactory.cs b/src/ServiceControl.Persistence.SqlServer/UnitOfWork/SqlIngestionUnitOfWorkFactory.cs deleted file mode 100644 index b05718d9ef..0000000000 --- a/src/ServiceControl.Persistence.SqlServer/UnitOfWork/SqlIngestionUnitOfWorkFactory.cs +++ /dev/null @@ -1,27 +0,0 @@ -namespace ServiceControl.Persistence.SqlServer -{ - using System.Data.SqlClient; - using System.Threading.Tasks; - using ServiceControl.Persistence.UnitOfWork; - - class SqlIngestionUnitOfWorkFactory : IIngestionUnitOfWorkFactory - { - readonly string connectionString; - - public SqlIngestionUnitOfWorkFactory(SqlDbConnectionManager connectionManager) - => connectionString = connectionManager.ConnectionString; - - public async ValueTask StartNew() - { - var connection = new SqlConnection(connectionString); - - await connection.OpenAsync().ConfigureAwait(false); - - var transaction = connection.BeginTransaction(); - - return new SqlIngestionUnitOfWork(connection, transaction); - } - - public bool CanIngestMore() => true; - } -} \ No newline at end of file diff --git a/src/ServiceControl.Persistence.SqlServer/UnitOfWork/SqlMonitoringIngestionUnitOfWork.cs b/src/ServiceControl.Persistence.SqlServer/UnitOfWork/SqlMonitoringIngestionUnitOfWork.cs deleted file mode 100644 index 77d78ffa14..0000000000 --- a/src/ServiceControl.Persistence.SqlServer/UnitOfWork/SqlMonitoringIngestionUnitOfWork.cs +++ /dev/null @@ -1,35 +0,0 @@ -namespace ServiceControl.Persistence.SqlServer -{ - using System.Data.SqlClient; - using System.Threading.Tasks; - using Dapper; - using ServiceControl.Persistence.UnitOfWork; - - class SqlMonitoringIngestionUnitOfWork : IMonitoringIngestionUnitOfWork - { - readonly SqlConnection connection; - readonly SqlTransaction transaction; - - public SqlMonitoringIngestionUnitOfWork(SqlConnection connection, SqlTransaction transaction) - { - this.connection = connection; - this.transaction = transaction; - } - - public Task RecordKnownEndpoint(KnownEndpoint knownEndpoint) - => connection.ExecuteAsync( - @"IF NOT EXISTS(SELECT * FROM [KnownEndpoints] WHERE Id = @Id) - BEGIN - INSERT INTO [KnownEndpoints](Id, HostId, Host, HostDisplayName, Monitored) - VALUES(@Id, @HostId, @Host, @HostDisplayName, @Monitored) - END", - new - { - knownEndpoint.Id, - knownEndpoint.EndpointDetails.HostId, - knownEndpoint.EndpointDetails.Host, - HostDisplayName = knownEndpoint.EndpointDetails.Name, - knownEndpoint.Monitored - }, transaction); - } -} \ No newline at end of file diff --git a/src/ServiceControl.Persistence.Tests.RavenDb/.editorconfig b/src/ServiceControl.Persistence.Tests.RavenDb/.editorconfig new file mode 100644 index 0000000000..5f68a610b3 --- /dev/null +++ b/src/ServiceControl.Persistence.Tests.RavenDb/.editorconfig @@ -0,0 +1,4 @@ +[*.cs] + +# Justification: Test project +dotnet_diagnostic.CA2007.severity = none diff --git a/src/ServiceControl.Persistence.Tests.RavenDb/API/APIApprovals.cs b/src/ServiceControl.Persistence.Tests.RavenDb/API/APIApprovals.cs new file mode 100644 index 0000000000..33c836cf72 --- /dev/null +++ b/src/ServiceControl.Persistence.Tests.RavenDb/API/APIApprovals.cs @@ -0,0 +1,73 @@ +namespace ServiceControl.UnitTests.API +{ + using System; + using System.Collections.Generic; + using System.Linq; + using System.Reflection; + using NServiceBus.CustomChecks; + using NUnit.Framework; + using Particular.Approvals; + using Persistence; + using Persistence.RavenDb; + using ServiceBus.Management.Infrastructure.Settings; + + [TestFixture] + class APIApprovals + { + [Test] + public void CustomCheckDetails() + { + // HINT: Custom checks are documented on the docs site and Id and Category are published in integration events + // If any changes have been made to custom checks, this may break customer integration subscribers. + Approver.Verify( + string.Join(Environment.NewLine, + from check in GetCustomChecks() + orderby check.Category, check.Id + select $"{check.Category}: {check.Id}" + ) + ); + } + + static IEnumerable GetCustomChecks() + { + var serviceControlTypes = typeof(RavenDbPersistenceConfiguration).Assembly + .GetTypes() + .Where(t => t.IsAbstract == false); + + var customCheckTypes = serviceControlTypes.Where(t => typeof(ICustomCheck).IsAssignableFrom(t)); + + var objects = new List() + { + new Settings(), + new PersistenceSettings(TimeSpan.Zero, TimeSpan.Zero, TimeSpan.Zero, 1, false) + { + PersisterSpecificSettings = { [RavenDbPersistenceConfiguration.DbPathKey] = "c:/" } + } + }; + + object MapParam(ParameterInfo pi) + { + foreach (var obj in objects) + { + if (obj.GetType() == pi.ParameterType) + { + return obj; + } + } + + return null; + } + + + foreach (var customCheckType in customCheckTypes) + { + var constructor = customCheckType.GetConstructors().Single(); + var constructorParameters = constructor.GetParameters() + .Select(MapParam) + .ToArray(); + var instance = (ICustomCheck)constructor.Invoke(constructorParameters); + yield return instance; + } + } + } +} \ No newline at end of file diff --git a/src/ServiceControl.Persistence.Tests.RavenDb/ApprovalFiles/APIApprovals.CustomCheckDetails.approved.txt b/src/ServiceControl.Persistence.Tests.RavenDb/ApprovalFiles/APIApprovals.CustomCheckDetails.approved.txt new file mode 100644 index 0000000000..3f79bf4f12 --- /dev/null +++ b/src/ServiceControl.Persistence.Tests.RavenDb/ApprovalFiles/APIApprovals.CustomCheckDetails.approved.txt @@ -0,0 +1,6 @@ +ServiceControl Health: Audit Message Ingestion +ServiceControl Health: Error Database Index Errors +ServiceControl Health: Error Database Index Lag +ServiceControl Health: Message Ingestion Process +ServiceControl Health: Saga Audit Data Retention +Storage space: ServiceControl database \ No newline at end of file diff --git a/src/ServiceControl.UnitTests/ApprovalFiles/RavenPersistedTypes.Verify.approved.txt b/src/ServiceControl.Persistence.Tests.RavenDb/ApprovalFiles/RavenPersistedTypes.Verify.approved.txt similarity index 59% rename from src/ServiceControl.UnitTests/ApprovalFiles/RavenPersistedTypes.Verify.approved.txt rename to src/ServiceControl.Persistence.Tests.RavenDb/ApprovalFiles/RavenPersistedTypes.Verify.approved.txt index 752bd1d2dc..2024580e53 100644 --- a/src/ServiceControl.UnitTests/ApprovalFiles/RavenPersistedTypes.Verify.approved.txt +++ b/src/ServiceControl.Persistence.Tests.RavenDb/ApprovalFiles/RavenPersistedTypes.Verify.approved.txt @@ -1,17 +1,17 @@ -ServiceControl.SagaAudit.SagaListIndex+Result, ServiceControl, Version=4.0.0.0, Culture=neutral, PublicKeyToken=null +ServiceControl.Contracts.CustomChecks.CustomCheck, ServiceControl.Persistence, Version=4.0.0.0, Culture=neutral, PublicKeyToken=null +ServiceControl.EventLog.EventLogItem, ServiceControl.Persistence, Version=4.0.0.0, Culture=neutral, PublicKeyToken=null +ServiceControl.MessageAuditing.ProcessedMessage, ServiceControl.Persistence, Version=4.0.0.0, Culture=neutral, PublicKeyToken=null ServiceControl.MessageFailures.FailedMessage, ServiceControl.Persistence, Version=4.0.0.0, Culture=neutral, PublicKeyToken=null -ServiceControl.Recoverability.FailureGroupView, ServiceControl, Version=4.0.0.0, Culture=neutral, PublicKeyToken=null -ServiceControl.Recoverability.FailureGroupMessageView, ServiceControl, Version=4.0.0.0, Culture=neutral, PublicKeyToken=null ServiceControl.MessageFailures.GroupComment, ServiceControl.Persistence, Version=4.0.0.0, Culture=neutral, PublicKeyToken=null -ServiceControl.MessageFailures.QueueAddress, ServiceControl, Version=4.0.0.0, Culture=neutral, PublicKeyToken=null -ServiceControl.Operations.FailedErrorImport, ServiceControl, Version=4.0.0.0, Culture=neutral, PublicKeyToken=null -ServiceControl.Operations.FailedAuditImport, ServiceControl, Version=4.0.0.0, Culture=neutral, PublicKeyToken=null -ServiceControl.EventLog.EventLogItem, ServiceControl, Version=4.0.0.0, Culture=neutral, PublicKeyToken=null -ServiceControl.MessageAuditing.ProcessedMessage, ServiceControl.Persistence, Version=4.0.0.0, Culture=neutral, PublicKeyToken=null -ServiceControl.SagaAudit.SagaHistory, ServiceControl.Audit.Persistence.SagaAudit, Version=4.0.0.0, Culture=neutral, PublicKeyToken=null -ServiceControl.Contracts.CustomChecks.CustomCheck, ServiceControl.Persistence, Version=4.0.0.0, Culture=neutral, PublicKeyToken=null -ServiceControl.Recoverability.FailedMessageRetry, ServiceControl.Persistence, Version=4.0.0.0, Culture=neutral, PublicKeyToken=null +ServiceControl.MessageFailures.QueueAddress, ServiceControl.Persistence, Version=4.0.0.0, Culture=neutral, PublicKeyToken=null +ServiceControl.Operations.FailedAuditImport, ServiceControl.Persistence, Version=4.0.0.0, Culture=neutral, PublicKeyToken=null +ServiceControl.Operations.FailedErrorImport, ServiceControl.Persistence, Version=4.0.0.0, Culture=neutral, PublicKeyToken=null ServiceControl.Persistence.KnownEndpoint, ServiceControl.Persistence, Version=4.0.0.0, Culture=neutral, PublicKeyToken=null ServiceControl.Persistence.MessagesViewIndex+SortAndFilterOptions, ServiceControl.Persistence.RavenDb, Version=4.0.0.0, Culture=neutral, PublicKeyToken=null ServiceControl.Persistence.RetryBatch, ServiceControl.Persistence, Version=4.0.0.0, Culture=neutral, PublicKeyToken=null -ServiceControl.Persistence.RetryBatchGroup, ServiceControl.Persistence.RavenDb, Version=4.0.0.0, Culture=neutral, PublicKeyToken=null \ No newline at end of file +ServiceControl.Persistence.RetryBatchGroup, ServiceControl.Persistence, Version=4.0.0.0, Culture=neutral, PublicKeyToken=null +ServiceControl.Recoverability.FailedMessageRetry, ServiceControl.Persistence, Version=4.0.0.0, Culture=neutral, PublicKeyToken=null +ServiceControl.Recoverability.FailureGroupMessageView, ServiceControl.Persistence, Version=4.0.0.0, Culture=neutral, PublicKeyToken=null +ServiceControl.Recoverability.FailureGroupView, ServiceControl.Persistence, Version=4.0.0.0, Culture=neutral, PublicKeyToken=null +ServiceControl.SagaAudit.SagaHistory, ServiceControl.Audit.Persistence.SagaAudit, Version=4.0.0.0, Culture=neutral, PublicKeyToken=null +ServiceControl.SagaAudit.SagaListIndex+Result, ServiceControl.Persistence.RavenDb, Version=4.0.0.0, Culture=neutral, PublicKeyToken=null \ No newline at end of file diff --git a/src/ServiceControl.UnitTests/ApprovalFiles/SagaDetailsIndexTests.RunMapReduce.approved.txt b/src/ServiceControl.Persistence.Tests.RavenDb/ApprovalFiles/SagaDetailsIndexTests.RunMapReduce.approved.txt similarity index 100% rename from src/ServiceControl.UnitTests/ApprovalFiles/SagaDetailsIndexTests.RunMapReduce.approved.txt rename to src/ServiceControl.Persistence.Tests.RavenDb/ApprovalFiles/SagaDetailsIndexTests.RunMapReduce.approved.txt diff --git a/src/ServiceControl.UnitTests/ApprovalFiles/SagaListIndexTests.RunMapReduce.approved.txt b/src/ServiceControl.Persistence.Tests.RavenDb/ApprovalFiles/SagaListIndexTests.RunMapReduce.approved.txt similarity index 100% rename from src/ServiceControl.UnitTests/ApprovalFiles/SagaListIndexTests.RunMapReduce.approved.txt rename to src/ServiceControl.Persistence.Tests.RavenDb/ApprovalFiles/SagaListIndexTests.RunMapReduce.approved.txt diff --git a/src/ServiceControl.Persistence.Tests.RavenDb/Archiving/ArchiveGroupTests.cs b/src/ServiceControl.Persistence.Tests.RavenDb/Archiving/ArchiveGroupTests.cs new file mode 100644 index 0000000000..41b1cb33c1 --- /dev/null +++ b/src/ServiceControl.Persistence.Tests.RavenDb/Archiving/ArchiveGroupTests.cs @@ -0,0 +1,115 @@ +namespace ServiceControl.UnitTests.Archiving +{ + using System; + using System.Threading.Tasks; + using Microsoft.Extensions.DependencyInjection; + using Microsoft.Extensions.Hosting; + using NServiceBus.Testing; + using NUnit.Framework; + using Raven.Client; + using ServiceControl.PersistenceTests; + using ServiceControl.Recoverability; + + [TestFixture] + class ArchiveGroupTests : PersistenceTestBase + { + IDocumentStore DocumentStore => GetRequiredService(); + + protected override IHostBuilder CreateHostBuilder() => base.CreateHostBuilder().ConfigureServices(services => + { + services.AddSingleton(); + services.AddSingleton(); + }); + + [Test] + public async Task ArchiveGroup_skips_over_empty_batches_but_still_completes() + { + // Arrange + var groupId = "TestGroup"; + var previousArchiveBatchId = ArchiveBatch.MakeId(groupId, ArchiveType.FailureGroup, 1); + + using (var session = DocumentStore.OpenAsyncSession()) + { + var previousArchiveBatch = new ArchiveBatch { Id = previousArchiveBatchId }; + await session.StoreAsync(previousArchiveBatch); + + var previousArchiveOperation = new ArchiveOperation + { + Id = ArchiveOperation.MakeId(groupId, ArchiveType.FailureGroup), + RequestId = groupId, + ArchiveType = ArchiveType.FailureGroup, + TotalNumberOfMessages = 2, + NumberOfMessagesArchived = 0, + Started = DateTime.Now, + GroupName = "Test Group", + NumberOfBatches = 3, + CurrentBatch = 0 + }; + await session.StoreAsync(previousArchiveOperation); + + await session.SaveChangesAsync(); + } + + var handler = GetRequiredService(); // See this.CreateHostBuilder + + var context = new TestableMessageHandlerContext(); + var message = new ArchiveAllInGroup { GroupId = groupId }; + + // Act + await handler.Handle(message, context); + + // Assert + using (var session = DocumentStore.OpenSession()) + { + var loadedBatch = session.Load(previousArchiveBatchId); + Assert.IsNull(loadedBatch); + } + } + + [Test] + public async Task ArchiveGroup_GetGroupDetails_doesnt_fail_with_invalid_groupId() + { + // Arrange + var failureGroupsViewIndex = new FailureGroupsViewIndex(); + await failureGroupsViewIndex.ExecuteAsync(DocumentStore); + + var groupId = "TestGroup"; + var previousArchiveBatchId = ArchiveBatch.MakeId(groupId, ArchiveType.FailureGroup, 1); + + using (var session = DocumentStore.OpenAsyncSession()) + { + var previousArchiveBatch = new ArchiveBatch { Id = previousArchiveBatchId }; + await session.StoreAsync(previousArchiveBatch); + + var previousArchiveOperation = new ArchiveOperation + { + Id = ArchiveOperation.MakeId(groupId, ArchiveType.FailureGroup), + RequestId = groupId, + ArchiveType = ArchiveType.FailureGroup, + TotalNumberOfMessages = 2, + NumberOfMessagesArchived = 0, + Started = DateTime.Now, + GroupName = "Test Group", + NumberOfBatches = 3, + CurrentBatch = 0 + }; + await session.StoreAsync(previousArchiveOperation); + + await session.SaveChangesAsync(); + } + + var handler = GetRequiredService(); // See this.CreateHostBuilder + + var context = new TestableMessageHandlerContext(); + var message = new ArchiveAllInGroup { GroupId = groupId + "Invalid" }; + + // Act + // Assert + Assert.DoesNotThrowAsync(async () => + { + // Act + await handler.Handle(message, context); + }); + } + } +} \ No newline at end of file diff --git a/src/ServiceControl.Persistence.Tests.RavenDb/BodyStorage/RavenAttachmentsBodyStorageTests.cs b/src/ServiceControl.Persistence.Tests.RavenDb/BodyStorage/RavenAttachmentsBodyStorageTests.cs new file mode 100644 index 0000000000..f477920e26 --- /dev/null +++ b/src/ServiceControl.Persistence.Tests.RavenDb/BodyStorage/RavenAttachmentsBodyStorageTests.cs @@ -0,0 +1,32 @@ +namespace ServiceControl.UnitTests.BodyStorage +{ + using System; + using System.IO; + using System.Threading.Tasks; + using NUnit.Framework; + using PersistenceTests; + + [TestFixture] + sealed class RavenAttachmentsBodyStorageTests : PersistenceTestBase + { + [Test] + public async Task Attachments_with_ids_that_contain_backslash_should_be_readable() + { + var messageId = "3f0240a7-9b2e-4e2a-ab39-6114932adad1\\2055783"; + var contentType = "NotImportant"; + var body = BitConverter.GetBytes(0xDEADBEEF); + + await BodyStorage.Store(messageId, contentType, body.Length, new MemoryStream(body)); + + var retrieved = await BodyStorage.TryFetch(messageId); + Assert.IsNotNull(retrieved); + Assert.True(retrieved.HasResult); + Assert.AreEqual(contentType, retrieved.ContentType); + + var buffer = new byte[retrieved.BodySize]; + retrieved.Stream.Read(buffer, 0, retrieved.BodySize); + + Assert.AreEqual(body, buffer); + } + } +} \ No newline at end of file diff --git a/src/ServiceControl.UnitTests/CompositeViews/FailedMessagesTests.cs b/src/ServiceControl.Persistence.Tests.RavenDb/CompositeViews/FailedMessagesTests.cs similarity index 100% rename from src/ServiceControl.UnitTests/CompositeViews/FailedMessagesTests.cs rename to src/ServiceControl.Persistence.Tests.RavenDb/CompositeViews/FailedMessagesTests.cs diff --git a/src/ServiceControl.UnitTests/CompositeViews/MessagesViewTests.cs b/src/ServiceControl.Persistence.Tests.RavenDb/CompositeViews/MessagesViewTests.cs similarity index 100% rename from src/ServiceControl.UnitTests/CompositeViews/MessagesViewTests.cs rename to src/ServiceControl.Persistence.Tests.RavenDb/CompositeViews/MessagesViewTests.cs diff --git a/src/ServiceControl.UnitTests/Expiration/ChunkerTests.cs b/src/ServiceControl.Persistence.Tests.RavenDb/Expiration/ChunkerTests.cs similarity index 100% rename from src/ServiceControl.UnitTests/Expiration/ChunkerTests.cs rename to src/ServiceControl.Persistence.Tests.RavenDb/Expiration/ChunkerTests.cs diff --git a/src/ServiceControl.Persistence.Tests.RavenDb/Expiration/ProcessedMessageExpirationTests.cs b/src/ServiceControl.Persistence.Tests.RavenDb/Expiration/ProcessedMessageExpirationTests.cs new file mode 100644 index 0000000000..c62acc37c9 --- /dev/null +++ b/src/ServiceControl.Persistence.Tests.RavenDb/Expiration/ProcessedMessageExpirationTests.cs @@ -0,0 +1,256 @@ +namespace ServiceControl.UnitTests.Expiration +{ + using System; + using System.Collections.Generic; + using System.Diagnostics; + using System.IO; + using System.Linq; + using System.Threading; + using System.Threading.Tasks; + using MessageAuditing; + using MessageFailures; + using NUnit.Framework; + using PersistenceTests; + using Raven.Client; + using Raven.Client.Embedded; + using ServiceControl.Infrastructure.RavenDB.Expiration; + using ServiceControl.Operations.BodyStorage.RavenAttachments; + + sealed class ProcessedMessageExpirationTests : PersistenceTestBase + { + IDocumentStore DocumentStore => GetRequiredService(); // TODO: Should primary use abstractions and only have native code for triggering the cleanup + + [Test] + public void Old_documents_are_being_expired() + { + var expiredDate = DateTime.UtcNow.AddDays(-3); + var thresholdDate = DateTime.UtcNow.AddDays(-2); + var processedMessage = new ProcessedMessage + { + Id = "1", + ProcessedAt = expiredDate + }; + + using (var session = DocumentStore.OpenSession()) + { + session.Store(processedMessage); + session.SaveChanges(); + } + + RunExpiry(thresholdDate); + + using (var session = DocumentStore.OpenSession()) + { + Assert.IsEmpty(session.Query()); + } + } + + [Test] + public void Many_documents_are_being_expired() + { + var expiredDate = DateTime.UtcNow.AddDays(-3); + var thresholdDate = DateTime.UtcNow.AddDays(-2); + var recentDate = DateTime.UtcNow.AddDays(-1); + var expiredMessages = BuildExpiredMessaged(expiredDate).ToList(); + using (var session = DocumentStore.OpenSession()) + { + foreach (var message in expiredMessages) + { + session.Store(message); + } + + session.SaveChanges(); + } + + using (var session = DocumentStore.OpenSession()) + { + var recentMessage = new ProcessedMessage + { + Id = "recentMessageId", + ProcessedAt = recentDate + }; + session.Store(recentMessage); + session.SaveChanges(); + } + + RunExpiry(thresholdDate); + foreach (dynamic message in expiredMessages) + { + using (var session = DocumentStore.OpenSession()) + { + Assert.Null(session.Load(message.Id)); + } + } + + using (var session = DocumentStore.OpenSession()) + { + Assert.AreEqual(1, session.Query().Count()); + } + } + + IEnumerable BuildExpiredMessaged(DateTime dateTime) + { + for (var i = 0; i < DocTestRange; i++) + { + yield return new ProcessedMessage + { + Id = Guid.NewGuid().ToString(), + ProcessedAt = dateTime + }; + } + } + + void RunExpiry(DateTime expiryThreshold) + { + var documentStore = DocumentStore; + var embeddableDocumentStore = (EmbeddableDocumentStore)documentStore; + + new ExpiryProcessedMessageIndex().Execute(documentStore); + documentStore.WaitForIndexing(); + AuditMessageCleaner.Clean(DocTestRange, embeddableDocumentStore.DocumentDatabase, expiryThreshold); + documentStore.WaitForIndexing(); + } + + [Test] + public void Only_processed_messages_are_being_expired() + { + var expiredDate = DateTime.UtcNow.AddDays(-3); + var thresholdDate = DateTime.UtcNow.AddDays(-2); + var recentDate = DateTime.UtcNow.AddDays(-1); + var expiredMessage = new ProcessedMessage + { + Id = "1", + ProcessedAt = expiredDate + }; + + using (var session = DocumentStore.OpenSession()) + { + session.Store(expiredMessage); + session.SaveChanges(); + } + + var recentMessage = new ProcessedMessage + { + Id = "2", + ProcessedAt = recentDate + }; + using (var session = DocumentStore.OpenSession()) + { + session.Store(recentMessage); + session.SaveChanges(); + } + + RunExpiry(thresholdDate); + + using (var session = DocumentStore.OpenSession()) + { + Assert.Null(session.Load(expiredMessage.Id)); + Assert.NotNull(session.Load(recentMessage.Id)); + } + } + + [Test] + public async Task Stored_bodies_are_being_removed_when_message_expires() + { + var expiredDate = DateTime.UtcNow.AddDays(-3); + var thresholdDate = DateTime.UtcNow.AddDays(-2); + // Store expired message with associated body + var messageId = "21"; + + var processedMessage = new ProcessedMessage + { + Id = "1", + ProcessedAt = expiredDate, + MessageMetadata = new Dictionary + { + { + "MessageId", messageId + } + } + }; + + using (var session = DocumentStore.OpenSession()) + { + session.Store(processedMessage); + session.SaveChanges(); + } + + var body = new byte[] + { + 1, + 2, + 3, + 4, + 5 + }; + + var bodyStorage = new RavenAttachmentsBodyStorage(DocumentStore); + using (var stream = new MemoryStream(body)) + { + await bodyStorage.Store(messageId, "binary", 5, stream); + } + + RunExpiry(thresholdDate); + + // Verify message expired + using (var session = DocumentStore.OpenSession()) + { + Assert.Null(session.Load(processedMessage.Id)); + } + + var result = await bodyStorage.TryFetch(messageId); + // Verify body expired + Assert.Null(result, "Audit document body should be deleted"); + } + + [Test] + public void Recent_processed_messages_are_not_being_expired() + { + var thresholdDate = DateTime.UtcNow.AddDays(-2); + var recentDate = DateTime.UtcNow.AddDays(-1); + var message = new ProcessedMessage + { + Id = "1", + ProcessedAt = recentDate + }; + using (var session = DocumentStore.OpenSession()) + { + session.Store(message); + session.SaveChanges(); + } + + RunExpiry(thresholdDate); + using (var session = DocumentStore.OpenSession()) + { + Assert.AreEqual(1, session.Query().Count()); + } + } + + [Test] + public void Errors_are_not_being_expired() + { + var failedMsg = new FailedMessage + { + Id = "1" + }; + + using (var session = DocumentStore.OpenSession()) + { + session.Store(failedMsg); + session.SaveChanges(); + + Debug.WriteLine(session.Advanced.GetMetadataFor(failedMsg)["Last-Modified"]); + } + + Thread.Sleep(100); + RunExpiry(DateTime.UtcNow); + + using (var session = DocumentStore.OpenSession()) + { + Assert.NotNull(session.Load(failedMsg.Id)); + } + } + + const int DocTestRange = 999; + } +} \ No newline at end of file diff --git a/src/ServiceControl.UnitTests/Expiration/RavenLastModifiedScope.cs b/src/ServiceControl.Persistence.Tests.RavenDb/Expiration/RavenLastModifiedScope.cs similarity index 100% rename from src/ServiceControl.UnitTests/Expiration/RavenLastModifiedScope.cs rename to src/ServiceControl.Persistence.Tests.RavenDb/Expiration/RavenLastModifiedScope.cs diff --git a/src/ServiceControl.UnitTests/Expiration/SagaAuditExpirationTests.cs b/src/ServiceControl.Persistence.Tests.RavenDb/Expiration/SagaAuditExpirationTests.cs similarity index 100% rename from src/ServiceControl.UnitTests/Expiration/SagaAuditExpirationTests.cs rename to src/ServiceControl.Persistence.Tests.RavenDb/Expiration/SagaAuditExpirationTests.cs diff --git a/src/ServiceControl.UnitTests/Infrastructure/RavenDB/Indexes/InMemoryStoreBuilder.cs b/src/ServiceControl.Persistence.Tests.RavenDb/Infrastructure/InMemoryStoreBuilder.cs similarity index 100% rename from src/ServiceControl.UnitTests/Infrastructure/RavenDB/Indexes/InMemoryStoreBuilder.cs rename to src/ServiceControl.Persistence.Tests.RavenDb/Infrastructure/InMemoryStoreBuilder.cs diff --git a/src/ServiceControl.Persistence.Tests.RavenDb/Infrastructure/MessageExtensions.cs b/src/ServiceControl.Persistence.Tests.RavenDb/Infrastructure/MessageExtensions.cs new file mode 100644 index 0000000000..ee278f421e --- /dev/null +++ b/src/ServiceControl.Persistence.Tests.RavenDb/Infrastructure/MessageExtensions.cs @@ -0,0 +1,14 @@ +using ServiceControl.MessageAuditing; + +public static class MessageExtensions +{ + public static void MakeSystemMessage(this ProcessedMessage message, bool isSystem = true) + { + message.MessageMetadata["IsSystemMessage"] = isSystem; + } + + public static void SetMessageId(this ProcessedMessage message, string messageId) + { + message.MessageMetadata["MessageId"] = messageId; + } +} \ No newline at end of file diff --git a/src/ServiceControl.PersistenceTests/RavenIndexAwaiter.cs b/src/ServiceControl.Persistence.Tests.RavenDb/Infrastructure/RavenIndexAwaiter.cs similarity index 100% rename from src/ServiceControl.PersistenceTests/RavenIndexAwaiter.cs rename to src/ServiceControl.Persistence.Tests.RavenDb/Infrastructure/RavenIndexAwaiter.cs diff --git a/src/ServiceControl.UnitTests/Infrastructure/RavenDB/MigrationTests.cs b/src/ServiceControl.Persistence.Tests.RavenDb/MigrationTests.cs similarity index 100% rename from src/ServiceControl.UnitTests/Infrastructure/RavenDB/MigrationTests.cs rename to src/ServiceControl.Persistence.Tests.RavenDb/MigrationTests.cs diff --git a/src/ServiceControl.UnitTests/Operations/FailedAuditImportCustomCheckTests.cs b/src/ServiceControl.Persistence.Tests.RavenDb/Operations/FailedAuditImportCustomCheckTests.cs similarity index 100% rename from src/ServiceControl.UnitTests/Operations/FailedAuditImportCustomCheckTests.cs rename to src/ServiceControl.Persistence.Tests.RavenDb/Operations/FailedAuditImportCustomCheckTests.cs diff --git a/src/ServiceControl.Persistence.Tests.RavenDb/Operations/FailedErrorImportCustomCheckTests.cs b/src/ServiceControl.Persistence.Tests.RavenDb/Operations/FailedErrorImportCustomCheckTests.cs new file mode 100644 index 0000000000..0cde2d8569 --- /dev/null +++ b/src/ServiceControl.Persistence.Tests.RavenDb/Operations/FailedErrorImportCustomCheckTests.cs @@ -0,0 +1,55 @@ +namespace ServiceControl.UnitTests.Operations +{ + using System.Threading.Tasks; + using Microsoft.Extensions.DependencyInjection; + using Microsoft.Extensions.Hosting; + using NServiceBus.CustomChecks; + using NUnit.Framework; + using PersistenceTests; + using Raven.Client; + using ServiceControl.Operations; + + [TestFixture] + class FailedErrorImportCustomCheckTests : PersistenceTestBase + { + IDocumentStore DocumentStore => GetRequiredService(); + + protected override IHostBuilder CreateHostBuilder() => base.CreateHostBuilder().ConfigureServices(services => + { + services.AddSingleton(); + }); + + [Test] + public async Task Pass_if_no_failed_imports() + { + await DocumentStore.ExecuteIndexAsync(new FailedErrorImportIndex()); + + var customCheck = GetRequiredService(); + + var result = await customCheck.PerformCheck(); + + Assert.AreEqual(CheckResult.Pass, result); + } + + [Test] + public async Task Fail_if_failed_imports() + { + await DocumentStore.ExecuteIndexAsync(new FailedErrorImportIndex()); + + using (var session = DocumentStore.OpenAsyncSession()) + { + await session.StoreAsync(new FailedErrorImport()); + await session.SaveChangesAsync(); + } + + DocumentStore.WaitForIndexing(); + + var customCheck = GetRequiredService(); + + var result = await customCheck.PerformCheck(); + + Assert.IsTrue(result.HasFailed); + StringAssert.StartsWith("One or more error messages have failed to import properly into ServiceControl and have been stored in the ServiceControl database.", result.FailureReason); + } + } +} \ No newline at end of file diff --git a/src/ServiceControl.UnitTests/Infrastructure/PreventReindexing.cs b/src/ServiceControl.Persistence.Tests.RavenDb/PreventReindexing.cs similarity index 100% rename from src/ServiceControl.UnitTests/Infrastructure/PreventReindexing.cs rename to src/ServiceControl.Persistence.Tests.RavenDb/PreventReindexing.cs diff --git a/src/ServiceControl.UnitTests/Infrastructure/RavenDB/RavenBootstrapperTests.cs b/src/ServiceControl.Persistence.Tests.RavenDb/RavenBootstrapperTests.cs similarity index 85% rename from src/ServiceControl.UnitTests/Infrastructure/RavenDB/RavenBootstrapperTests.cs rename to src/ServiceControl.Persistence.Tests.RavenDb/RavenBootstrapperTests.cs index 13337ecc75..cca0b65feb 100644 --- a/src/ServiceControl.UnitTests/Infrastructure/RavenDB/RavenBootstrapperTests.cs +++ b/src/ServiceControl.Persistence.Tests.RavenDb/RavenBootstrapperTests.cs @@ -1,5 +1,5 @@ using NUnit.Framework; -using ServiceControl.Infrastructure.RavenDB; +using ServiceControl.Persistence.RavenDb; [TestFixture] class RavenBootstrapperTests diff --git a/src/ServiceControl.UnitTests/RavenPersistedTypes.cs b/src/ServiceControl.Persistence.Tests.RavenDb/RavenPersistedTypes.cs similarity index 92% rename from src/ServiceControl.UnitTests/RavenPersistedTypes.cs rename to src/ServiceControl.Persistence.Tests.RavenDb/RavenPersistedTypes.cs index e54d05c183..34c7d260a1 100644 --- a/src/ServiceControl.UnitTests/RavenPersistedTypes.cs +++ b/src/ServiceControl.Persistence.Tests.RavenDb/RavenPersistedTypes.cs @@ -20,7 +20,7 @@ public void Verify() .SelectMany(indexType => indexType.BaseType?.GenericTypeArguments) .Distinct(); - var documentTypeNames = string.Join(Environment.NewLine, documentTypes.Select(t => t.AssemblyQualifiedName)); + var documentTypeNames = string.Join(Environment.NewLine, documentTypes.Select(t => t.AssemblyQualifiedName).OrderBy(x => x)); Approver.Verify(documentTypeNames); } diff --git a/src/ServiceControl.UnitTests/Recoverability/FailedMessageRetryTests.cs b/src/ServiceControl.Persistence.Tests.RavenDb/Recoverability/FailedMessageRetryTests.cs similarity index 77% rename from src/ServiceControl.UnitTests/Recoverability/FailedMessageRetryTests.cs rename to src/ServiceControl.Persistence.Tests.RavenDb/Recoverability/FailedMessageRetryTests.cs index 16ae67b893..8019c78618 100644 --- a/src/ServiceControl.UnitTests/Recoverability/FailedMessageRetryTests.cs +++ b/src/ServiceControl.Persistence.Tests.RavenDb/Recoverability/FailedMessageRetryTests.cs @@ -12,9 +12,9 @@ public class FailedMessageRetryTests public void Should_Extract_Correct_FailedMessageRetryId_From_FailedMessageId() { var messageId = Guid.NewGuid().ToString(); - var failedMessageId = FailedMessage.MakeDocumentId(messageId); + var failedMessageId = FailedMessageIdGenerator.MakeDocumentId(messageId); - var extractedFailedMessageRetryId = FailedMessageRetry.MakeDocumentId(FailedMessage.GetMessageIdFromDocumentId(failedMessageId)); + var extractedFailedMessageRetryId = FailedMessageRetry.MakeDocumentId(FailedMessageIdGenerator.GetMessageIdFromDocumentId(failedMessageId)); Assert.AreEqual(FailedMessageRetry.MakeDocumentId(messageId), extractedFailedMessageRetryId); } diff --git a/src/ServiceControl.UnitTests/Recoverability/RetryConfirmationProcessorTests.cs b/src/ServiceControl.Persistence.Tests.RavenDb/Recoverability/RetryConfirmationProcessorTests.cs similarity index 72% rename from src/ServiceControl.UnitTests/Recoverability/RetryConfirmationProcessorTests.cs rename to src/ServiceControl.Persistence.Tests.RavenDb/Recoverability/RetryConfirmationProcessorTests.cs index 3907c3e24c..d40e64937f 100644 --- a/src/ServiceControl.UnitTests/Recoverability/RetryConfirmationProcessorTests.cs +++ b/src/ServiceControl.Persistence.Tests.RavenDb/Recoverability/RetryConfirmationProcessorTests.cs @@ -5,50 +5,41 @@ using System.Threading; using System.Threading.Tasks; using Contracts.MessageFailures; + using MessageFailures; using MessageFailures.Handlers; using NServiceBus.Extensibility; using NServiceBus.Testing; using NServiceBus.Transport; using NUnit.Framework; - using Operations; - using Raven.Client; - using Raven.Tests.Helpers; - using MessageFailures; - using Persistence.RavenDb; using ServiceControl.Operations; - using ServiceControl.Recoverability; + using PersistenceTests; - public class RetryConfirmationProcessorTests : RavenTestBase + // TODO: Moved by Ramon to RavenDB specific tests, has a lot of RavenDB dependencies + class RetryConfirmationProcessorTests : PersistenceTestBase { - IDocumentStore Store { get; set; } RetryConfirmationProcessor Processor { get; set; } LegacyMessageFailureResolvedHandler Handler { get; set; } [SetUp] - public async Task Setup() + public new async Task Setup() { - Store = NewDocumentStore(); - var domainEvents = new FakeDomainEvents(); Processor = new RetryConfirmationProcessor(domainEvents); - var retryDocumentManager = new RetryDocumentManager(new FakeApplicationLifetime(), Store, new RetryingManager(domainEvents)); - Handler = new LegacyMessageFailureResolvedHandler(Store, domainEvents, retryDocumentManager); + Handler = new LegacyMessageFailureResolvedHandler(ErrorMessageDataStore, domainEvents); - using (var session = Store.OpenAsyncSession()) - { - var failedMessage = new FailedMessage + await ErrorMessageDataStore.StoreFailedMessages( + new FailedMessage { - Id = FailedMessage.MakeDocumentId(MessageId), + Id = MessageId, Status = FailedMessageStatus.Unresolved - }; + } + ); - await session.StoreAsync(failedMessage); - await session.SaveChangesAsync(); - } + // TODO: Strange... I just commented these lines and the tests are still green.... - var retryDocumentCommands = RetryDocumentManager.CreateFailedMessageRetryDocument(Guid.NewGuid().ToString(), MessageId); - await Store.AsyncDatabaseCommands.BatchAsync(new[] { retryDocumentCommands }); + //var retryDocumentCommands = RetryDocumentDataStore.CreateFailedMessageRetryDocument(Guid.NewGuid().ToString(), MessageId); + //await GetRequiredService().AsyncDatabaseCommands.BatchAsync(new[] { retryDocumentCommands }); } [Test] @@ -60,7 +51,7 @@ public async Task Should_handle_multiple_retry_confirmations_in_the_error_ingest CreateRetryAcknowledgementMessage() }; - var unitOfWork = new RavenDbIngestionUnitOfWork(Store); + var unitOfWork = await UnitOfWorkFactory.StartNew(); await Processor.Process(messageContexts, unitOfWork); Assert.DoesNotThrowAsync(() => unitOfWork.Complete()); @@ -83,7 +74,7 @@ public async Task Should_handle_retry_confirmation_followed_by_legacy_command() CreateRetryAcknowledgementMessage() }; - var unitOfWork = new RavenDbIngestionUnitOfWork(Store); + var unitOfWork = await UnitOfWorkFactory.StartNew(); await Processor.Process(messageContexts, unitOfWork); await unitOfWork.Complete(); @@ -101,7 +92,7 @@ public async Task Should_handle_legacy_retry_confirmation_command_followed_by_ne CreateRetryAcknowledgementMessage() }; - var unitOfWork = new RavenDbIngestionUnitOfWork(Store); + var unitOfWork = await UnitOfWorkFactory.StartNew(); await Processor.Process(messageContexts, unitOfWork); Assert.DoesNotThrowAsync(() => unitOfWork.Complete()); } @@ -125,7 +116,7 @@ static MessageContext CreateRetryAcknowledgementMessage() var messageContext = new MessageContext( MessageId, headers, - new byte[0], + Array.Empty(), new TransportTransaction(), new CancellationTokenSource(), new ContextBag()); @@ -133,6 +124,5 @@ static MessageContext CreateRetryAcknowledgementMessage() } const string MessageId = "83C73A86-A45E-4FDF-8C95-E292526166F5"; - } } \ No newline at end of file diff --git a/src/ServiceControl.UnitTests/Recoverability/ReturnToSenderDequeuerTests.cs b/src/ServiceControl.Persistence.Tests.RavenDb/Recoverability/ReturnToSenderDequeuerTests.cs similarity index 77% rename from src/ServiceControl.UnitTests/Recoverability/ReturnToSenderDequeuerTests.cs rename to src/ServiceControl.Persistence.Tests.RavenDb/Recoverability/ReturnToSenderDequeuerTests.cs index 6b4103821a..181e16cf58 100644 --- a/src/ServiceControl.UnitTests/Recoverability/ReturnToSenderDequeuerTests.cs +++ b/src/ServiceControl.Persistence.Tests.RavenDb/Recoverability/ReturnToSenderDequeuerTests.cs @@ -8,15 +8,19 @@ using System.Threading; using System.Threading.Tasks; using MessageFailures; + using Microsoft.Extensions.DependencyInjection; + using Microsoft.Extensions.Hosting; using NServiceBus.Extensibility; using NServiceBus.Transport; using NUnit.Framework; + using Raven.Client; using ServiceControl.CompositeViews.Messages; using ServiceControl.Operations.BodyStorage; + using ServiceControl.PersistenceTests; using ServiceControl.Recoverability; [TestFixture] - public class ReturnToSenderDequeuerTests + class ReturnToSenderDequeuerTests : PersistenceTestBase { MessageContext CreateMessage(string id, Dictionary headers) { @@ -30,6 +34,8 @@ MessageContext CreateMessage(string id, Dictionary headers) ); } + protected override IHostBuilder CreateHostBuilder() => base.CreateHostBuilder().ConfigureServices(services => services.AddSingleton()); + [Test] public async Task It_removes_staging_id_header() { @@ -42,8 +48,7 @@ public async Task It_removes_staging_id_header() }; var message = CreateMessage(Guid.NewGuid().ToString(), headers); - await new ReturnToSender(new FakeBodyStorage(), null).HandleMessage(message, sender, "error") - .ConfigureAwait(false); + await new ReturnToSender(new FakeBodyStorage(), null).HandleMessage(message, sender, "error"); Assert.IsFalse(sender.Message.Headers.ContainsKey("ServiceControl.Retry.StagingId")); } @@ -61,8 +66,7 @@ public async Task It_fetches_the_body_from_storage_if_provided() }; var message = CreateMessage(Guid.NewGuid().ToString(), headers); - await new ReturnToSender(new FakeBodyStorage(), null).HandleMessage(message, sender, "error") - .ConfigureAwait(false); + await new ReturnToSender(new FakeBodyStorage(), null).HandleMessage(message, sender, "error"); Assert.AreEqual("MessageBodyId", Encoding.UTF8.GetString(sender.Message.Body)); } @@ -82,39 +86,35 @@ public async Task It_fetches_the_body_from_index_if_provided() }; var message = CreateMessage(Guid.NewGuid().ToString(), headers); - using (var documentStore = InMemoryStoreBuilder.GetInMemoryStore()) + var documentStore = GetRequiredService(); + + var failedMessage = new FailedMessage { - using (var session = documentStore.OpenAsyncSession()) + Id = FailedMessageIdGenerator.MakeDocumentId("MessageBodyId"), + ProcessingAttempts = new List { - await session.StoreAsync(new FailedMessage + new FailedMessage.ProcessingAttempt { - Id = FailedMessage.MakeDocumentId("MessageBodyId"), - ProcessingAttempts = new List - { - new FailedMessage.ProcessingAttempt - { - MessageId = "MessageBodyId", - MessageMetadata = new Dictionary - { - { "Body", "MessageBodyId" } - } - } - } - }); - - await session.SaveChangesAsync(); + MessageId = "MessageBodyId", + MessageMetadata = { { "Body", "MessageBodyId" } } + } } + }; - var transformer = new MessagesBodyTransformer(); - await transformer.ExecuteAsync(documentStore); + await ErrorMessageDataStore.StoreFailedMessages(failedMessage); - documentStore.WaitForIndexing(); + var transformer = new MessagesBodyTransformer(); + await transformer.ExecuteAsync(documentStore); - await new ReturnToSender(null, documentStore).HandleMessage(message, sender, "error") - .ConfigureAwait(false); + await CompleteDatabaseOperation(); - Assert.AreEqual("MessageBodyId", Encoding.UTF8.GetString(sender.Message.Body)); - } + var instance = GetRequiredService(); // See this.CreateHostBuilder + + // Acts + await instance.HandleMessage(message, sender, "error"); + + // Assert + Assert.AreEqual("MessageBodyId", Encoding.UTF8.GetString(sender.Message.Body)); } [Test] @@ -130,8 +130,7 @@ public async Task It_uses_retry_to_if_provided() }; var message = CreateMessage(Guid.NewGuid().ToString(), headers); - await new ReturnToSender(new FakeBodyStorage(), null).HandleMessage(message, sender, "error") - .ConfigureAwait(false); + await new ReturnToSender(new FakeBodyStorage(), null).HandleMessage(message, sender, "error"); Assert.AreEqual("Proxy", sender.Destination); Assert.AreEqual("TargetEndpoint", sender.Message.Headers["ServiceControl.TargetEndpointAddress"]); @@ -149,8 +148,7 @@ public async Task It_sends_directly_to_target_if_retry_to_is_not_provided() }; var message = CreateMessage(Guid.NewGuid().ToString(), headers); - await new ReturnToSender(new FakeBodyStorage(), null).HandleMessage(message, sender, "error") - .ConfigureAwait(false); + await new ReturnToSender(new FakeBodyStorage(), null).HandleMessage(message, sender, "error"); Assert.AreEqual("TargetEndpoint", sender.Destination); Assert.IsFalse(sender.Message.Headers.ContainsKey("ServiceControl.TargetEndpointAddress")); @@ -171,8 +169,7 @@ public async Task It_restores_body_id_and_target_addres_after_failure() try { - await new ReturnToSender(new FakeBodyStorage(), null).HandleMessage(message, sender, "error") - .ConfigureAwait(false); + await new ReturnToSender(new FakeBodyStorage(), null).HandleMessage(message, sender, "error"); } catch (Exception) { @@ -213,10 +210,10 @@ public Task Store(string bodyId, string contentType, int bodySize, Stream bodySt throw new NotImplementedException(); } - public Task TryFetch(string bodyId) + public Task TryFetch(string bodyId) { var stream = new MemoryStream(Encoding.UTF8.GetBytes(bodyId)); //Echo back the body ID. - return Task.FromResult(new StreamResult + return Task.FromResult(new MessageBodyStreamResult { HasResult = true, Stream = stream diff --git a/src/ServiceControl.UnitTests/SagaAudit/SagaDetailsIndexTests.cs b/src/ServiceControl.Persistence.Tests.RavenDb/SagaAudit/SagaDetailsIndexTests.cs similarity index 100% rename from src/ServiceControl.UnitTests/SagaAudit/SagaDetailsIndexTests.cs rename to src/ServiceControl.Persistence.Tests.RavenDb/SagaAudit/SagaDetailsIndexTests.cs diff --git a/src/ServiceControl.UnitTests/SagaAudit/SagaListIndexTests.cs b/src/ServiceControl.Persistence.Tests.RavenDb/SagaAudit/SagaListIndexTests.cs similarity index 100% rename from src/ServiceControl.UnitTests/SagaAudit/SagaListIndexTests.cs rename to src/ServiceControl.Persistence.Tests.RavenDb/SagaAudit/SagaListIndexTests.cs diff --git a/src/ServiceControl.PersistenceTests/ServiceControl.PersistenceTests.csproj b/src/ServiceControl.Persistence.Tests.RavenDb/ServiceControl.Persistence.Tests.RavenDb.csproj similarity index 67% rename from src/ServiceControl.PersistenceTests/ServiceControl.PersistenceTests.csproj rename to src/ServiceControl.Persistence.Tests.RavenDb/ServiceControl.Persistence.Tests.RavenDb.csproj index eb111dbfa7..15bcfda682 100644 --- a/src/ServiceControl.PersistenceTests/ServiceControl.PersistenceTests.csproj +++ b/src/ServiceControl.Persistence.Tests.RavenDb/ServiceControl.Persistence.Tests.RavenDb.csproj @@ -1,4 +1,4 @@ - + net472 @@ -6,10 +6,11 @@ - - - + + + + @@ -19,6 +20,13 @@ + + + + + + + diff --git a/src/ServiceControl.PersistenceTests/ServiceControl.runsettings b/src/ServiceControl.Persistence.Tests.RavenDb/ServiceControl.runsettings similarity index 100% rename from src/ServiceControl.PersistenceTests/ServiceControl.runsettings rename to src/ServiceControl.Persistence.Tests.RavenDb/ServiceControl.runsettings diff --git a/src/ServiceControl.UnitTests/Infrastructure/RavenDB/SubscriptionPersisterTests.cs b/src/ServiceControl.Persistence.Tests.RavenDb/SubscriptionPersisterTests.cs similarity index 89% rename from src/ServiceControl.UnitTests/Infrastructure/RavenDB/SubscriptionPersisterTests.cs rename to src/ServiceControl.Persistence.Tests.RavenDb/SubscriptionPersisterTests.cs index 9902f8ebf1..297076bfb6 100644 --- a/src/ServiceControl.UnitTests/Infrastructure/RavenDB/SubscriptionPersisterTests.cs +++ b/src/ServiceControl.Persistence.Tests.RavenDb/SubscriptionPersisterTests.cs @@ -16,7 +16,7 @@ public class SubscriptionPersisterTests [Test] public async Task ShouldReturnSubscriptionsForOlderVersionsOfSameMessageType() { - var subscriptionPersister = new SubscriptionPersister(documentStore, "NServiceBus.Routing.EndpointName", "TestEndpoint", new MessageType[0]); + var subscriptionPersister = new RavenDbSubscriptionStorage(documentStore, "NServiceBus.Routing.EndpointName", "TestEndpoint", new MessageType[0]); var v1MessageType = new MessageType(typeof(SampleMessageType).FullName, new Version(1, 0, 0)); var v2MessageType = new MessageType(typeof(SampleMessageType).FullName, new Version(2, 0, 0)); @@ -34,7 +34,7 @@ public async Task ShouldReturnSubscriptionsForOlderVersionsOfSameMessageType() [Test] public async Task ShouldReturnSubscriptionsForNewerVersionsOfSameMessageType() { - var subscriptionPersister = new SubscriptionPersister(documentStore, "NServiceBus.Routing.EndpointName", "TestEndpoint", new MessageType[0]); + var subscriptionPersister = new RavenDbSubscriptionStorage(documentStore, "NServiceBus.Routing.EndpointName", "TestEndpoint", new MessageType[0]); var v1MessageType = new MessageType(typeof(SampleMessageType).FullName, new Version(1, 0, 0)); var v2MessageType = new MessageType(typeof(SampleMessageType).FullName, new Version(2, 0, 0)); diff --git a/src/ServiceControl.Persistence.Tests.RavenDb/TestPersistenceImpl.cs b/src/ServiceControl.Persistence.Tests.RavenDb/TestPersistenceImpl.cs new file mode 100644 index 0000000000..c803a50d9f --- /dev/null +++ b/src/ServiceControl.Persistence.Tests.RavenDb/TestPersistenceImpl.cs @@ -0,0 +1,105 @@ +namespace ServiceControl.PersistenceTests +{ + using System; + using System.Diagnostics; + using System.Runtime.InteropServices; + using System.Threading; + using System.Threading.Tasks; + using Microsoft.Extensions.DependencyInjection; + using Microsoft.Extensions.Hosting; + using NUnit.Framework; + using Persistence; + using Raven.Client; + using ServiceBus.Management.Infrastructure.Settings; + using ServiceControl.Persistence.RavenDb; + + sealed class TestPersistenceImpl : TestPersistence + { + IDocumentStore documentStore; + + public const string DatabaseMaintenancePort = "55554"; + + static PersistenceSettings CreateSettings() + { + var retentionPeriod = TimeSpan.FromMinutes(1); + var settings = new PersistenceSettings(retentionPeriod, retentionPeriod, retentionPeriod, 100, false) + { + PersisterSpecificSettings = + { + [RavenBootstrapper.RunInMemoryKey] = bool.TrueString, + [RavenBootstrapper.HostNameKey] = "localhost", + [RavenBootstrapper.DatabaseMaintenancePortKey] = DatabaseMaintenancePort + } + }; + + if (Debugger.IsAttached) + { + Console.WriteLine("If you get 'Access is denied' exception while debugging, comment out this line or create a URLACL reservervation:"); + Console.WriteLine("> netsh http add urlacl http://+:55554/ user=Everyone"); + settings.PersisterSpecificSettings[RavenBootstrapper.ExposeRavenDBKey] = bool.TrueString; + } + + return settings; + } + + public override void Configure(IServiceCollection services) + { + var config = PersistenceConfigurationFactory.LoadPersistenceConfiguration(DataStoreConfig.RavenDB35PersistenceTypeFullyQualifiedName); + var settings = CreateSettings(); + + var instance = config.Create(settings); + PersistenceHostBuilderExtensions.CreatePersisterLifecyle(services, instance); + + + services.AddHostedService(p => new Wrapper(this, p.GetRequiredService())); + } + + public override Task CompleteDatabaseOperation() + { + Assert.IsNotNull(documentStore); + documentStore.WaitForIndexing(); + return Task.CompletedTask; + } + + class Wrapper : IHostedService + { + public Wrapper(TestPersistenceImpl instance, IDocumentStore store) + { + instance.documentStore = store; + } + + public Task StartAsync(CancellationToken cancellationToken) => Task.CompletedTask; + public Task StopAsync(CancellationToken cancellationToken) => Task.CompletedTask; + } + + public override void BlockToInspectDatabase() + { + if (!Debugger.IsAttached) + { + return; + } + + var url = $"http://localhost:{DatabaseMaintenancePort}/studio/index.html#databases/documents?&database=%3Csystem%3E"; + + if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) + { + url = url.Replace("&", "^&"); + Process.Start(new ProcessStartInfo(url) { UseShellExecute = true }); + } + else if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux)) + { + Process.Start("xdg-open", url); + } + else if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX)) + { + Process.Start("open", url); + } + + while (true) + { + Thread.Sleep(5000); + Trace.Write("Waiting for debugger pause"); + } + } + } +} \ No newline at end of file diff --git a/src/ServiceControl.Persistence.Tests.RavenDb/Unarchiving/UnarchiveGroupTests.cs b/src/ServiceControl.Persistence.Tests.RavenDb/Unarchiving/UnarchiveGroupTests.cs new file mode 100644 index 0000000000..34e47ffeab --- /dev/null +++ b/src/ServiceControl.Persistence.Tests.RavenDb/Unarchiving/UnarchiveGroupTests.cs @@ -0,0 +1,115 @@ +namespace ServiceControl.UnitTests.Archiving +{ + using System; + using System.Threading.Tasks; + using Microsoft.Extensions.DependencyInjection; + using Microsoft.Extensions.Hosting; + using NServiceBus.Testing; + using NUnit.Framework; + using PersistenceTests; + using Raven.Client; + using ServiceControl.Recoverability; + + [TestFixture] + class UnarchiveGroupTests : PersistenceTestBase + { + IDocumentStore DocumentStore => GetRequiredService(); + + protected override IHostBuilder CreateHostBuilder() => base.CreateHostBuilder().ConfigureServices(services => + { + services.AddSingleton(); + services.AddSingleton(); + }); + + [Test] + public async Task UnarchiveGroup_skips_over_empty_batches_but_still_completes() + { + // Arrange + var groupId = "TestGroup"; + var previousUnarchiveBatchId = UnarchiveBatch.MakeId(groupId, ArchiveType.FailureGroup, 1); + + using (var session = DocumentStore.OpenAsyncSession()) + { + var previousUnarchiveBatch = new UnarchiveBatch { Id = previousUnarchiveBatchId }; + await session.StoreAsync(previousUnarchiveBatch); + + var previousUnarchiveOperation = new UnarchiveOperation + { + Id = UnarchiveOperation.MakeId(groupId, ArchiveType.FailureGroup), + RequestId = groupId, + ArchiveType = ArchiveType.FailureGroup, + TotalNumberOfMessages = 2, + NumberOfMessagesUnarchived = 2, + Started = DateTime.Now, + GroupName = "Test Group", + NumberOfBatches = 3, + CurrentBatch = 0 + }; + await session.StoreAsync(previousUnarchiveOperation); + + await session.SaveChangesAsync(); + } + + var handler = GetRequiredService(); // See this.CreateHostBuilder + + var context = new TestableMessageHandlerContext(); + var message = new UnarchiveAllInGroup { GroupId = groupId }; + + // Act + await handler.Handle(message, context); + + // Assert + using (var session = DocumentStore.OpenSession()) + { + var loadedBatch = session.Load(previousUnarchiveBatchId); + Assert.IsNull(loadedBatch); + } + } + + [Test] + public async Task UnarchiveGroup_GetGroupDetails_doesnt_fail_with_invalid_groupId() + { + // Arrange + var failureGroupsViewIndex = new ArchivedGroupsViewIndex(); + await failureGroupsViewIndex.ExecuteAsync(DocumentStore); + + var groupId = "TestGroup"; + var previousUnarchiveBatchId = UnarchiveBatch.MakeId(groupId, ArchiveType.FailureGroup, 1); + + using (var session = DocumentStore.OpenAsyncSession()) + { + var previousUnarchiveBatch = new UnarchiveBatch { Id = previousUnarchiveBatchId }; + await session.StoreAsync(previousUnarchiveBatch); + + var previousUnarchiveOperation = new UnarchiveOperation + { + Id = UnarchiveOperation.MakeId(groupId, ArchiveType.FailureGroup), + RequestId = groupId, + ArchiveType = ArchiveType.FailureGroup, + TotalNumberOfMessages = 2, + NumberOfMessagesUnarchived = 0, + Started = DateTime.Now, + GroupName = "Test Group", + NumberOfBatches = 3, + CurrentBatch = 0 + }; + await session.StoreAsync(previousUnarchiveOperation); + + await session.SaveChangesAsync(); + } + + var handler = GetRequiredService(); // See this.CreateHostBuilder + + var context = new TestableMessageHandlerContext(); + var message = new UnarchiveAllInGroup { GroupId = groupId + "Invalid" }; + + // Act + // Assert + Assert.DoesNotThrowAsync(async () => + { + // Act + await handler.Handle(message, context); + }); + } + } +} \ No newline at end of file diff --git a/src/ServiceControl.PersistenceTests/app.config b/src/ServiceControl.Persistence.Tests.RavenDb/app.config similarity index 100% rename from src/ServiceControl.PersistenceTests/app.config rename to src/ServiceControl.Persistence.Tests.RavenDb/app.config diff --git a/src/ServiceControl.Persistence.Tests/.editorconfig b/src/ServiceControl.Persistence.Tests/.editorconfig new file mode 100644 index 0000000000..5f68a610b3 --- /dev/null +++ b/src/ServiceControl.Persistence.Tests/.editorconfig @@ -0,0 +1,4 @@ +[*.cs] + +# Justification: Test project +dotnet_diagnostic.CA2007.severity = none diff --git a/src/ServiceControl.Persistence.Tests/BaseHostTest.cs b/src/ServiceControl.Persistence.Tests/BaseHostTest.cs new file mode 100644 index 0000000000..208ca8a844 --- /dev/null +++ b/src/ServiceControl.Persistence.Tests/BaseHostTest.cs @@ -0,0 +1,28 @@ +using Microsoft.Extensions.Hosting; +using Microsoft.Extensions.Logging; +using NUnit.Framework; +using System.Threading.Tasks; +using Microsoft.Extensions.DependencyInjection; + +[Parallelizable(ParallelScope.All)] +[FixtureLifeCycle(LifeCycle.InstancePerTestCase)] +public abstract class BaseHostTest +{ + readonly IHost testHost; + + protected BaseHostTest() => testHost = CreateHostBuilder().Build(); + + [SetUp] + public async Task SetUp() => await testHost.StartAsync(); + + [TearDown] + public async Task TearDown() => await testHost.StopAsync(); + + protected T GetRequiredService() => testHost.Services.GetRequiredService(); + + protected virtual IHostBuilder CreateHostBuilder() => Host.CreateDefaultBuilder() + .ConfigureLogging((hostingContext, logging) => + { + logging.AddConsole(); + }); +} \ No newline at end of file diff --git a/src/ServiceControl.Persistence.Tests/ControllerDependencies.cs b/src/ServiceControl.Persistence.Tests/ControllerDependencies.cs new file mode 100644 index 0000000000..c9229bec63 --- /dev/null +++ b/src/ServiceControl.Persistence.Tests/ControllerDependencies.cs @@ -0,0 +1,72 @@ +namespace ServiceControl.Persistence.Tests +{ + using System; + using System.Collections.Generic; + using System.Linq; + using System.Net.Http; + using System.Reflection; + using System.Web.Http.Controllers; + using Microsoft.Extensions.DependencyInjection; + using Microsoft.Extensions.Hosting; + using NServiceBus; + using NUnit.Framework; + using Particular.ServiceControl; + using ServiceBus.Management.Infrastructure.Settings; + using ServiceControl.Infrastructure.DomainEvents; + using ServiceControl.Infrastructure.WebApi; + using ServiceControl.PersistenceTests; + + [TestFixture] + class ControllerDependencies + { + /// + /// This test makes sure that each persistence has registered all the required services to + /// instantiate each of the WebAPI controllers present in the ServiceControl app. + /// + [Test] + public void EnsurePersistenceProvidesAllControllerDependencies() + { + // Arrange + var testPersistence = new TestPersistenceImpl(); + + var assembly = Assembly.GetAssembly(typeof(WebApiHostBuilderExtensions)); + var controllerTypes = assembly.DefinedTypes + .Where(type => typeof(IHttpController).IsAssignableFrom(type) && + type.Name.EndsWith("Controller", StringComparison.Ordinal)) + .ToArray(); + + var hostBuilder = new HostBuilder() + .ConfigureServices(serviceCollection => + { + serviceCollection.AddSingleton>(() => new HttpClient()); + serviceCollection.AddSingleton(); + serviceCollection.AddSingleton(new LoggingSettings("test")); + + testPersistence.Configure(serviceCollection); + }) + .UseNServiceBus(_ => + { + var config = new EndpointConfiguration("test"); + config.UseTransport(); + return config; + }) + .UseServiceControlComponents(new Settings(), ServiceControlMainInstance.Components); + + // Act + var host = hostBuilder + .UseWebApi(new List { assembly }, string.Empty, false) + .Build(); + + // Assert + Assert.That(host, Is.Not.Null); + + // Make sure the list isn't suddenly empty + Assert.That(controllerTypes.Length, Is.GreaterThan(10)); + foreach (var controllerType in controllerTypes) + { + Console.WriteLine($"Getting service {controllerType.FullName}"); + Assert.That(host.Services.GetService(controllerType), Is.Not.Null); + } + } + } +} \ No newline at end of file diff --git a/src/ServiceControl.PersistenceTests/CustomChecksDataStoreTests.cs b/src/ServiceControl.Persistence.Tests/CustomChecksDataStoreTests.cs similarity index 60% rename from src/ServiceControl.PersistenceTests/CustomChecksDataStoreTests.cs rename to src/ServiceControl.Persistence.Tests/CustomChecksDataStoreTests.cs index 8096f48b1a..d35058f7e7 100644 --- a/src/ServiceControl.PersistenceTests/CustomChecksDataStoreTests.cs +++ b/src/ServiceControl.Persistence.Tests/CustomChecksDataStoreTests.cs @@ -1,33 +1,16 @@ -namespace ServiceControl.Persistence.Tests +namespace ServiceControl.PersistenceTests { using System; using System.Linq; using System.Threading.Tasks; using Contracts.CustomChecks; - using Infrastructure; using NUnit.Framework; using ServiceControl.Operations; + using ServiceControl.Persistence; + using ServiceControl.Persistence.Infrastructure; - [TestFixtureSource(typeof(PersistenceTestCollection))] - class CustomChecksDataStoreTests + class CustomChecksDataStoreTests : PersistenceTestBase { - public CustomChecksDataStoreTests(PersistenceDataStoreFixture fixture) - { - this.fixture = fixture; - } - - [SetUp] - public async Task Setup() - { - await fixture.SetupDataStore().ConfigureAwait(false); - } - - [TearDown] - public async Task Cleanup() - { - await fixture.CleanupDB().ConfigureAwait(false); - } - [Test] public async Task CustomChecks_load_from_data_store() { @@ -45,10 +28,10 @@ public async Task CustomChecks_load_from_data_store() }, }; - var status = await fixture.CustomCheckDataStore.UpdateCustomCheckStatus(checkDetails).ConfigureAwait(false); + var status = await CustomChecks.UpdateCustomCheckStatus(checkDetails); - await fixture.CompleteDBOperation().ConfigureAwait(false); - var stats = await fixture.CustomCheckDataStore.GetStats(new PagingInfo()).ConfigureAwait(false); + await CompleteDatabaseOperation(); + var stats = await CustomChecks.GetStats(new PagingInfo()); Assert.AreEqual(1, stats.Results.Count); Assert.AreEqual(Status.Fail, stats.Results[0].Status); @@ -72,10 +55,10 @@ public async Task Storing_failed_custom_checks_returns_unchanged() }, }; - var statusInitial = await fixture.CustomCheckDataStore.UpdateCustomCheckStatus(checkDetails).ConfigureAwait(false); - var statusUpdate = await fixture.CustomCheckDataStore.UpdateCustomCheckStatus(checkDetails).ConfigureAwait(false); + var statusInitial = await CustomChecks.UpdateCustomCheckStatus(checkDetails); + var statusUpdate = await CustomChecks.UpdateCustomCheckStatus(checkDetails); - await fixture.CompleteDBOperation().ConfigureAwait(false); + await CompleteDatabaseOperation(); Assert.AreEqual(CheckStateChange.Changed, statusInitial); Assert.AreEqual(CheckStateChange.Unchanged, statusUpdate); @@ -98,11 +81,11 @@ public async Task Retrieving_custom_checks_by_status() }, }; - var _ = await fixture.CustomCheckDataStore.UpdateCustomCheckStatus(checkDetails).ConfigureAwait(false); + var _ = await CustomChecks.UpdateCustomCheckStatus(checkDetails); - await fixture.CompleteDBOperation().ConfigureAwait(false); + await CompleteDatabaseOperation(); - var stats = await fixture.CustomCheckDataStore.GetStats(new PagingInfo(), "pass").ConfigureAwait(false); + var stats = await CustomChecks.GetStats(new PagingInfo(), "pass"); Assert.AreEqual(1, stats.Results.Count); } @@ -125,20 +108,18 @@ public async Task Should_delete_custom_checks() var checkId = checkDetails.GetDeterministicId(); - var _ = await fixture.CustomCheckDataStore.UpdateCustomCheckStatus(checkDetails).ConfigureAwait(false); + var _ = await CustomChecks.UpdateCustomCheckStatus(checkDetails); - await fixture.CompleteDBOperation().ConfigureAwait(false); + await CompleteDatabaseOperation(); - await fixture.CustomCheckDataStore.DeleteCustomCheck(checkId).ConfigureAwait(false); + await CustomChecks.DeleteCustomCheck(checkId); - await fixture.CompleteDBOperation().ConfigureAwait(false); + await CompleteDatabaseOperation(); - var storedChecks = await fixture.CustomCheckDataStore.GetStats(new PagingInfo()).ConfigureAwait(false); + var storedChecks = await CustomChecks.GetStats(new PagingInfo()); var check = storedChecks.Results.Where(c => c.Id == checkId).ToList(); Assert.AreEqual(0, check.Count); } - - readonly PersistenceDataStoreFixture fixture; } } \ No newline at end of file diff --git a/src/ServiceControl.Persistence.Tests/FakeDomainEvents.cs b/src/ServiceControl.Persistence.Tests/FakeDomainEvents.cs new file mode 100644 index 0000000000..3eaff65bbd --- /dev/null +++ b/src/ServiceControl.Persistence.Tests/FakeDomainEvents.cs @@ -0,0 +1,30 @@ +namespace ServiceControl.PersistenceTests +{ + using System.Collections.Generic; + using System.Threading.Tasks; + using Newtonsoft.Json; + using Newtonsoft.Json.Converters; + using NUnit.Framework; + using ServiceControl.Infrastructure.DomainEvents; + + class FakeDomainEvents : IDomainEvents + { + public List RaisedEvents { get; } = new List(); + + public Task Raise(T domainEvent) where T : IDomainEvent + { + RaisedEvents.Add(domainEvent); + TestContext.WriteLine($"Raised DomainEvent {typeof(T).Name}:"); + TestContext.WriteLine(JsonConvert.SerializeObject(domainEvent, jsonSettings)); + return Task.FromResult(0); + } + + static FakeDomainEvents() + { + jsonSettings = new JsonSerializerSettings { Formatting = Formatting.Indented }; + jsonSettings.Converters.Add(new StringEnumConverter()); + } + + static readonly JsonSerializerSettings jsonSettings; + } +} \ No newline at end of file diff --git a/src/ServiceControl.Persistence.Tests/MagicStringTypeTests.cs b/src/ServiceControl.Persistence.Tests/MagicStringTypeTests.cs new file mode 100644 index 0000000000..25ecee97b3 --- /dev/null +++ b/src/ServiceControl.Persistence.Tests/MagicStringTypeTests.cs @@ -0,0 +1,21 @@ +namespace ServiceControl.PersistenceTests +{ + using System.Threading.Tasks; + using NUnit.Framework; + using ServiceBus.Management.Infrastructure.Settings; + using ServiceControl.Persistence.RavenDb; + + class MagicStringTypeTests + { + [Test] + public Task Verify_ravendb_persistence_type_string() + { + var typeNamespace = DataStoreConfig.RavenDB35PersistenceTypeFullyQualifiedName.Split(',')[1].Trim(); + var typeFullName = DataStoreConfig.RavenDB35PersistenceTypeFullyQualifiedName.Split(',')[0].Trim(); + var type = typeof(RavenDbPersistenceConfiguration); + Assert.AreEqual(type.Namespace, typeNamespace); + Assert.AreEqual(type.FullName, typeFullName); + return Task.CompletedTask; + } + } +} \ No newline at end of file diff --git a/src/ServiceControl.PersistenceTests/MonitoringDataStoreTests.cs b/src/ServiceControl.Persistence.Tests/MonitoringDataStoreTests.cs similarity index 51% rename from src/ServiceControl.PersistenceTests/MonitoringDataStoreTests.cs rename to src/ServiceControl.Persistence.Tests/MonitoringDataStoreTests.cs index a4d5824714..df1e3e8c15 100644 --- a/src/ServiceControl.PersistenceTests/MonitoringDataStoreTests.cs +++ b/src/ServiceControl.Persistence.Tests/MonitoringDataStoreTests.cs @@ -1,4 +1,4 @@ -namespace ServiceControl.Persistence.Tests +namespace ServiceControl.PersistenceTests { using System; using System.Linq; @@ -6,38 +6,19 @@ using NUnit.Framework; using ServiceControl.Monitoring; using ServiceControl.Operations; + using ServiceControl.Persistence; - [TestFixtureSource(typeof(PersistenceTestCollection))] - class MonitoringDataStoreTests + class MonitoringDataStoreTests : PersistenceTestBase { - PersistenceDataStoreFixture persistenceDataStoreFixture; - - public MonitoringDataStoreTests(PersistenceDataStoreFixture persistenceDataStoreFixture) - { - this.persistenceDataStoreFixture = persistenceDataStoreFixture; - } - - [SetUp] - public async Task Setup() - { - await persistenceDataStoreFixture.SetupDataStore().ConfigureAwait(false); - } - - [TearDown] - public async Task Cleanup() - { - await persistenceDataStoreFixture.CleanupDB().ConfigureAwait(false); - } - [Test] public async Task Endpoints_load_from_dataStore_into_monitor() { var endpointInstanceMonitoring = new EndpointInstanceMonitoring(new FakeDomainEvents()); var endpoint1 = new EndpointDetails() { HostId = Guid.NewGuid(), Host = "Host1", Name = "Name1" }; - await persistenceDataStoreFixture.MonitoringDataStore.CreateIfNotExists(endpoint1).ConfigureAwait(false); + await MonitoringDataStore.CreateIfNotExists(endpoint1); - await persistenceDataStoreFixture.CompleteDBOperation().ConfigureAwait(false); - await persistenceDataStoreFixture.MonitoringDataStore.WarmupMonitoringFromPersistence(endpointInstanceMonitoring).ConfigureAwait(false); + await CompleteDatabaseOperation(); + await MonitoringDataStore.WarmupMonitoringFromPersistence(endpointInstanceMonitoring); Assert.AreEqual(1, endpointInstanceMonitoring.GetKnownEndpoints().Count(w => w.HostDisplayName == endpoint1.Host)); } @@ -47,11 +28,11 @@ public async Task Endpoints_added_more_than_once_are_treated_as_same_endpoint() { var endpointInstanceMonitoring = new EndpointInstanceMonitoring(new FakeDomainEvents()); var endpoint1 = new EndpointDetails() { HostId = Guid.NewGuid(), Host = "Host1", Name = "Name1" }; - await persistenceDataStoreFixture.MonitoringDataStore.CreateIfNotExists(endpoint1).ConfigureAwait(false); - await persistenceDataStoreFixture.MonitoringDataStore.CreateIfNotExists(endpoint1).ConfigureAwait(false); + await MonitoringDataStore.CreateIfNotExists(endpoint1); + await MonitoringDataStore.CreateIfNotExists(endpoint1); - await persistenceDataStoreFixture.CompleteDBOperation().ConfigureAwait(false); - await persistenceDataStoreFixture.MonitoringDataStore.WarmupMonitoringFromPersistence(endpointInstanceMonitoring).ConfigureAwait(false); + await CompleteDatabaseOperation(); + await MonitoringDataStore.WarmupMonitoringFromPersistence(endpointInstanceMonitoring); Assert.AreEqual(1, endpointInstanceMonitoring.GetKnownEndpoints().Count(w => w.HostDisplayName == endpoint1.Host)); } @@ -61,11 +42,11 @@ public async Task Updating_existing_endpoint_does_not_create_new_ones() { var endpointInstanceMonitoring = new EndpointInstanceMonitoring(new FakeDomainEvents()); var endpoint1 = new EndpointDetails() { HostId = Guid.NewGuid(), Host = "Host1", Name = "Name1" }; - await persistenceDataStoreFixture.MonitoringDataStore.CreateIfNotExists(endpoint1).ConfigureAwait(false); - await persistenceDataStoreFixture.MonitoringDataStore.CreateOrUpdate(endpoint1, endpointInstanceMonitoring).ConfigureAwait(false); + await MonitoringDataStore.CreateIfNotExists(endpoint1); + await MonitoringDataStore.CreateOrUpdate(endpoint1, endpointInstanceMonitoring); - await persistenceDataStoreFixture.CompleteDBOperation().ConfigureAwait(false); - await persistenceDataStoreFixture.MonitoringDataStore.WarmupMonitoringFromPersistence(endpointInstanceMonitoring).ConfigureAwait(false); + await CompleteDatabaseOperation(); + await MonitoringDataStore.WarmupMonitoringFromPersistence(endpointInstanceMonitoring); Assert.AreEqual(1, endpointInstanceMonitoring.GetKnownEndpoints().Count(w => w.HostDisplayName == endpoint1.Host)); } @@ -76,11 +57,11 @@ public async Task Endpoint_is_created_if_doesnt_exist() var endpointInstanceMonitoring = new EndpointInstanceMonitoring(new FakeDomainEvents()); var endpoint1 = new EndpointDetails() { HostId = Guid.NewGuid(), Host = "Host1", Name = "Name1" }; var endpoint2 = new EndpointDetails() { HostId = Guid.NewGuid(), Host = "Host2", Name = "Name2" }; - await persistenceDataStoreFixture.MonitoringDataStore.CreateIfNotExists(endpoint1).ConfigureAwait(false); - await persistenceDataStoreFixture.MonitoringDataStore.CreateIfNotExists(endpoint2).ConfigureAwait(false); + await MonitoringDataStore.CreateIfNotExists(endpoint1); + await MonitoringDataStore.CreateIfNotExists(endpoint2); - await persistenceDataStoreFixture.CompleteDBOperation().ConfigureAwait(false); - await persistenceDataStoreFixture.MonitoringDataStore.WarmupMonitoringFromPersistence(endpointInstanceMonitoring).ConfigureAwait(false); + await CompleteDatabaseOperation(); + await MonitoringDataStore.WarmupMonitoringFromPersistence(endpointInstanceMonitoring); Assert.AreEqual(2, endpointInstanceMonitoring.GetKnownEndpoints().Count(w => w.HostDisplayName == endpoint1.Host || w.HostDisplayName == endpoint2.Host)); } @@ -91,11 +72,11 @@ public async Task Endpoint_is_created_if_doesnt_exist_on_update() var endpointInstanceMonitoring = new EndpointInstanceMonitoring(new FakeDomainEvents()); var endpoint1 = new EndpointDetails() { HostId = Guid.NewGuid(), Host = "Host1", Name = "Name1" }; var endpoint2 = new EndpointDetails() { HostId = Guid.NewGuid(), Host = "Host2", Name = "Name2" }; - await persistenceDataStoreFixture.MonitoringDataStore.CreateIfNotExists(endpoint1).ConfigureAwait(false); - await persistenceDataStoreFixture.MonitoringDataStore.CreateOrUpdate(endpoint2, endpointInstanceMonitoring).ConfigureAwait(false); + await MonitoringDataStore.CreateIfNotExists(endpoint1); + await MonitoringDataStore.CreateOrUpdate(endpoint2, endpointInstanceMonitoring); - await persistenceDataStoreFixture.CompleteDBOperation().ConfigureAwait(false); - await persistenceDataStoreFixture.MonitoringDataStore.WarmupMonitoringFromPersistence(endpointInstanceMonitoring).ConfigureAwait(false); + await CompleteDatabaseOperation(); + await MonitoringDataStore.WarmupMonitoringFromPersistence(endpointInstanceMonitoring); Assert.AreEqual(2, endpointInstanceMonitoring.GetKnownEndpoints().Count(w => w.HostDisplayName == endpoint1.Host || w.HostDisplayName == endpoint2.Host)); } @@ -105,17 +86,17 @@ public async Task Endpoint_is_updated() { var endpointInstanceMonitoring = new EndpointInstanceMonitoring(new FakeDomainEvents()); var endpoint1 = new EndpointDetails() { HostId = Guid.NewGuid(), Host = "Host1", Name = "Name1" }; - await persistenceDataStoreFixture.MonitoringDataStore.CreateIfNotExists(endpoint1).ConfigureAwait(false); + await MonitoringDataStore.CreateIfNotExists(endpoint1); - await persistenceDataStoreFixture.CompleteDBOperation().ConfigureAwait(false); - await persistenceDataStoreFixture.MonitoringDataStore.WarmupMonitoringFromPersistence(endpointInstanceMonitoring).ConfigureAwait(false); + await CompleteDatabaseOperation(); + await MonitoringDataStore.WarmupMonitoringFromPersistence(endpointInstanceMonitoring); Assert.IsFalse(endpointInstanceMonitoring.IsMonitored(endpointInstanceMonitoring.GetEndpoints()[0].Id)); - await persistenceDataStoreFixture.MonitoringDataStore.UpdateEndpointMonitoring(endpoint1, true).ConfigureAwait(false); + await MonitoringDataStore.UpdateEndpointMonitoring(endpoint1, true); endpointInstanceMonitoring = new EndpointInstanceMonitoring(new FakeDomainEvents()); - await persistenceDataStoreFixture.CompleteDBOperation().ConfigureAwait(false); - await persistenceDataStoreFixture.MonitoringDataStore.WarmupMonitoringFromPersistence(endpointInstanceMonitoring).ConfigureAwait(false); + await CompleteDatabaseOperation(); + await MonitoringDataStore.WarmupMonitoringFromPersistence(endpointInstanceMonitoring); Assert.IsTrue(endpointInstanceMonitoring.IsMonitored(endpointInstanceMonitoring.GetEndpoints()[0].Id)); } @@ -125,18 +106,18 @@ public async Task Endpoint_is_deleted() { var endpointInstanceMonitoring = new EndpointInstanceMonitoring(new FakeDomainEvents()); var endpoint1 = new EndpointDetails() { HostId = Guid.NewGuid(), Host = "Host1", Name = "Name1" }; - await persistenceDataStoreFixture.MonitoringDataStore.CreateIfNotExists(endpoint1).ConfigureAwait(false); + await MonitoringDataStore.CreateIfNotExists(endpoint1); - await persistenceDataStoreFixture.CompleteDBOperation().ConfigureAwait(false); - await persistenceDataStoreFixture.MonitoringDataStore.WarmupMonitoringFromPersistence(endpointInstanceMonitoring).ConfigureAwait(false); + await CompleteDatabaseOperation(); + await MonitoringDataStore.WarmupMonitoringFromPersistence(endpointInstanceMonitoring); Assert.AreEqual(1, endpointInstanceMonitoring.GetKnownEndpoints().Count(w => w.HostDisplayName == endpoint1.Host)); - await persistenceDataStoreFixture.MonitoringDataStore.Delete(endpointInstanceMonitoring.GetEndpoints()[0].Id).ConfigureAwait(false); + await MonitoringDataStore.Delete(endpointInstanceMonitoring.GetEndpoints()[0].Id); endpointInstanceMonitoring = new EndpointInstanceMonitoring(new FakeDomainEvents()); - await persistenceDataStoreFixture.CompleteDBOperation().ConfigureAwait(false); - await persistenceDataStoreFixture.MonitoringDataStore.WarmupMonitoringFromPersistence(endpointInstanceMonitoring).ConfigureAwait(false); + await CompleteDatabaseOperation(); + await MonitoringDataStore.WarmupMonitoringFromPersistence(endpointInstanceMonitoring); Assert.AreEqual(0, endpointInstanceMonitoring.GetKnownEndpoints().Count(w => w.HostDisplayName == endpoint1.Host)); } @@ -151,17 +132,16 @@ public async Task Unit_of_work_detects_endpoint() knownEndpoint.Id = knownEndpoint.EndpointDetails.GetDeterministicId(); - using (var unitOfWork = - await persistenceDataStoreFixture.UnitOfWorkFactory.StartNew().ConfigureAwait(false)) + using (var unitOfWork = await UnitOfWorkFactory.StartNew()) { - await unitOfWork.Monitoring.RecordKnownEndpoint(knownEndpoint).ConfigureAwait(false); + await unitOfWork.Monitoring.RecordKnownEndpoint(knownEndpoint); - await unitOfWork.Complete().ConfigureAwait(false); + await unitOfWork.Complete(); } - await persistenceDataStoreFixture.CompleteDBOperation().ConfigureAwait(false); + await CompleteDatabaseOperation(); - var knownEndpoints = await persistenceDataStoreFixture.MonitoringDataStore.GetAllKnownEndpoints().ConfigureAwait(false); + var knownEndpoints = await MonitoringDataStore.GetAllKnownEndpoints(); Assert.AreEqual(1, knownEndpoints.Count); var fromStorage = knownEndpoints[0]; diff --git a/src/ServiceControl.Persistence.Tests/PersistenceTestBase.cs b/src/ServiceControl.Persistence.Tests/PersistenceTestBase.cs new file mode 100644 index 0000000000..bb9bdd0f50 --- /dev/null +++ b/src/ServiceControl.Persistence.Tests/PersistenceTestBase.cs @@ -0,0 +1,57 @@ +namespace ServiceControl.PersistenceTests +{ + using System.Diagnostics; + using System.Threading.Tasks; + using Infrastructure.DomainEvents; + using Microsoft.Extensions.DependencyInjection; + using Microsoft.Extensions.Hosting; + using NServiceBus; + using NUnit.Framework; + using Persistence; + using Persistence.MessageRedirects; + using ServiceControl.Operations.BodyStorage; + using ServiceControl.Persistence.Recoverability; + using ServiceControl.Persistence.UnitOfWork; + + abstract class PersistenceTestBase : BaseHostTest + { + TestPersistence testPersistence; + + protected override IHostBuilder CreateHostBuilder() + { + return base.CreateHostBuilder().ConfigureServices(services => + { + services.AddSingleton(); + services.AddSingleton(new CriticalError(null)); + testPersistence = new TestPersistenceImpl(); + testPersistence.Configure(services); + }); + } + + [SetUp] + public virtual Task Setup() + { + return CompleteDatabaseOperation(); + } + + protected Task CompleteDatabaseOperation() + { + return testPersistence.CompleteDatabaseOperation(); + } + + // TODO: Evaluate if BlockToInspectDatabase() concept should stay in codebase long-term + [Conditional("DEBUG")] + protected void BlockToInspectDatabase() => testPersistence.BlockToInspectDatabase(); + + protected IErrorMessageDataStore ErrorStore => GetRequiredService(); + protected IRetryDocumentDataStore RetryStore => GetRequiredService(); + protected IBodyStorage BodyStorage => GetRequiredService(); + protected IRetryBatchesDataStore RetryBatchesStore => GetRequiredService(); + protected IErrorMessageDataStore ErrorMessageDataStore => GetRequiredService(); + protected IMessageRedirectsDataStore MessageRedirectsDataStore => GetRequiredService(); + protected IMonitoringDataStore MonitoringDataStore => GetRequiredService(); + protected IIngestionUnitOfWorkFactory UnitOfWorkFactory => GetRequiredService(); + protected ICustomChecksDataStore CustomChecks => GetRequiredService(); + protected IArchiveMessages ArchiveMessages => GetRequiredService(); + } +} \ No newline at end of file diff --git a/src/ServiceControl.UnitTests/Recoverability/EditMessageTests.cs b/src/ServiceControl.Persistence.Tests/Recoverability/EditMessageTests.cs similarity index 51% rename from src/ServiceControl.UnitTests/Recoverability/EditMessageTests.cs rename to src/ServiceControl.Persistence.Tests/Recoverability/EditMessageTests.cs index 097a421edf..09f1079969 100644 --- a/src/ServiceControl.UnitTests/Recoverability/EditMessageTests.cs +++ b/src/ServiceControl.Persistence.Tests/Recoverability/EditMessageTests.cs @@ -7,44 +7,42 @@ using System.Threading.Tasks; using Contracts.Operations; using MessageFailures; - using MessageRedirects; + using Microsoft.Extensions.DependencyInjection; + using Microsoft.Extensions.Hosting; using NServiceBus.Extensibility; using NServiceBus.Testing; using NServiceBus.Transport; using NUnit.Framework; - using Raven.Client; - using Raven.Tests.Helpers; + using ServiceControl.Persistence.MessageRedirects; + using ServiceControl.PersistenceTests; using ServiceControl.Recoverability; using ServiceControl.Recoverability.Editing; - [TestFixture] - public class EditMessageTests : RavenTestBase + sealed class EditMessageTests : PersistenceTestBase { - IDocumentStore Store { get; set; } - EditHandler Handler { get; set; } - TestableUnicastDispatcher Dispatcher { get; set; } + EditHandler handler; + readonly TestableUnicastDispatcher dispatcher = new TestableUnicastDispatcher(); - [SetUp] - public void Setup() - { - Store = NewDocumentStore(); - Dispatcher = new TestableUnicastDispatcher(); - Handler = new EditHandler(Store, Dispatcher); - } + protected override IHostBuilder CreateHostBuilder() => base + .CreateHostBuilder() + .ConfigureServices(services => services + .AddSingleton(dispatcher) + .AddTransient() + ); - [TearDown] - public void Teardown() + [SetUp] + public new void Setup() { - Store.Dispose(); + handler = GetRequiredService(); } [Test] public async Task Should_discard_edit_when_failed_message_not_exists() { var message = CreateEditMessage("some-id"); - await Handler.Handle(message, new TestableMessageHandlerContext()); + await handler.Handle(message, new TestableMessageHandlerContext()); - Assert.IsEmpty(Dispatcher.DispatchedMessages); + Assert.IsEmpty(dispatcher.DispatchedMessages); } [Test] @@ -54,21 +52,20 @@ public async Task Should_discard_edit_when_failed_message_not_exists() public async Task Should_discard_edit_if_edited_message_not_unresolved(FailedMessageStatus status) { var failedMessageId = Guid.NewGuid().ToString("D"); - await CreateFailedMessage(failedMessageId, status); + await CreateAndStoreFailedMessage(failedMessageId, status); var message = CreateEditMessage(failedMessageId); - await Handler.Handle(message, new TestableMessageHandlerContext()); + await handler.Handle(message, new TestableMessageHandlerContext()); - using (var session = Store.OpenAsyncSession()) - { - var failedMessage = await session.LoadAsync(FailedMessage.MakeDocumentId(failedMessageId)); - var editOperation = await session.LoadAsync(failedMessageId); + var failedMessage = await ErrorMessageDataStore.FailedMessageFetch(failedMessageId); - Assert.AreEqual(status, failedMessage.Status); - Assert.IsNull(editOperation); - } + var editFailedMessagesManager = await ErrorMessageDataStore.CreateEditFailedMessageManager(); + var editOperation = await editFailedMessagesManager.GetCurrentEditingMessageId(failedMessageId); - Assert.IsEmpty(Dispatcher.DispatchedMessages); + Assert.AreEqual(status, failedMessage.Status); + Assert.IsNull(editOperation); + + Assert.IsEmpty(dispatcher.DispatchedMessages); } [Test] @@ -77,60 +74,62 @@ public async Task Should_discard_edit_when_different_edit_already_exists() var failedMessageId = Guid.NewGuid().ToString(); var previousEdit = Guid.NewGuid().ToString(); - await CreateFailedMessage(failedMessageId); + _ = await CreateAndStoreFailedMessage(failedMessageId); - using (var session = Store.OpenAsyncSession()) + using (var editFailedMessagesManager = await ErrorMessageDataStore.CreateEditFailedMessageManager()) { - await session.StoreAsync(new FailedMessageEdit - { - Id = FailedMessageEdit.MakeDocumentId(failedMessageId), - FailedMessageId = failedMessageId, - EditId = previousEdit - }); - - await session.SaveChangesAsync(); + _ = await editFailedMessagesManager.GetFailedMessage(failedMessageId); + await editFailedMessagesManager.SetCurrentEditingMessageId(previousEdit); + await editFailedMessagesManager.SaveChanges(); } var message = CreateEditMessage(failedMessageId); - await Handler.Handle(message, new TestableMessageHandlerContext()); - using (var session = Store.OpenAsyncSession()) + // Act + await handler.Handle(message, new TestableMessageHandlerContext()); + + BlockToInspectDatabase(); + + using (var editFailedMessagesManagerAssert = await ErrorMessageDataStore.CreateEditFailedMessageManager()) { - var failedMessage = await session.LoadAsync(FailedMessage.MakeDocumentId(failedMessageId)); - var editOperation = await session.LoadAsync(FailedMessageEdit.MakeDocumentId(failedMessageId)); + var failedMessage = await editFailedMessagesManagerAssert.GetFailedMessage(failedMessageId); + var editId = await editFailedMessagesManagerAssert.GetCurrentEditingMessageId(failedMessageId); + Assert.AreEqual(previousEdit, editId); Assert.AreEqual(FailedMessageStatus.Unresolved, failedMessage.Status); - Assert.AreEqual(previousEdit, editOperation.EditId); } - Assert.IsEmpty(Dispatcher.DispatchedMessages); + Assert.IsEmpty(dispatcher.DispatchedMessages); } [Test] public async Task Should_dispatch_edited_message_when_first_edit() { - var failedMessage = await CreateFailedMessage(); + var failedMessage = await CreateAndStoreFailedMessage(); var newBodyContent = Encoding.UTF8.GetBytes("new body content"); var newHeaders = new Dictionary { { "someKey", "someValue" } }; var message = CreateEditMessage(failedMessage.UniqueMessageId, newBodyContent, newHeaders); var handlerContent = new TestableMessageHandlerContext(); - await Handler.Handle(message, handlerContent); + await handler.Handle(message, handlerContent); - var dispatchedMessage = Dispatcher.DispatchedMessages.Single(); + var dispatchedMessage = dispatcher.DispatchedMessages.Single(); Assert.AreEqual( failedMessage.ProcessingAttempts.Last().FailureDetails.AddressOfFailingEndpoint, dispatchedMessage.Item1.Destination); Assert.AreEqual(newBodyContent, dispatchedMessage.Item1.Message.Body); Assert.AreEqual("someValue", dispatchedMessage.Item1.Message.Headers["someKey"]); - using (var session = Store.OpenAsyncSession()) + + using (var x = await ErrorMessageDataStore.CreateEditFailedMessageManager()) { - failedMessage = await session.LoadAsync(failedMessage.Id); - var editOperation = await session.LoadAsync(FailedMessageEdit.MakeDocumentId(failedMessage.UniqueMessageId)); + var failedMessage2 = await x.GetFailedMessage(failedMessage.UniqueMessageId); + Assert.IsNotNull(failedMessage2, "Edited failed message"); - Assert.AreEqual(FailedMessageStatus.Resolved, failedMessage.Status); - Assert.AreEqual(handlerContent.MessageId, editOperation.EditId); + var editId = await x.GetCurrentEditingMessageId(failedMessage2.UniqueMessageId); + + Assert.AreEqual(FailedMessageStatus.Resolved, failedMessage2.Status, "Failed message status"); + Assert.AreEqual(handlerContent.MessageId, editId, "MessageId"); } } @@ -138,63 +137,60 @@ public async Task Should_dispatch_edited_message_when_first_edit() public async Task Should_dispatch_edited_message_when_retrying() { var failedMessageId = Guid.NewGuid().ToString(); - await CreateFailedMessage(failedMessageId); + await CreateAndStoreFailedMessage(failedMessageId); var handlerContext = new TestableMessageHandlerContext(); var message = CreateEditMessage(failedMessageId); - await Handler.Handle(message, handlerContext); - await Handler.Handle(message, handlerContext); + await handler.Handle(message, handlerContext); + await handler.Handle(message, handlerContext); - Assert.AreEqual(2, Dispatcher.DispatchedMessages.Count); + Assert.AreEqual(2, dispatcher.DispatchedMessages.Count, "Dispatched message count"); } [Test] public async Task Should_dispatch_message_using_incoming_transaction() { - var failedMessage = await CreateFailedMessage(); + var failedMessage = await CreateAndStoreFailedMessage(); var message = CreateEditMessage(failedMessage.UniqueMessageId); var handlerContent = new TestableMessageHandlerContext(); var transportTransaction = new TransportTransaction(); handlerContent.Extensions.Set(transportTransaction); - await Handler.Handle(message, handlerContent); + await handler.Handle(message, handlerContent); - Assert.AreSame(Dispatcher.DispatchedMessages.Single().Item2, transportTransaction); + Assert.AreSame(dispatcher.DispatchedMessages.Single().Item2, transportTransaction); } [Test] public async Task Should_route_to_redirect_route_if_exists() { const string redirectAddress = "a different destination"; - var failedMessage = await CreateFailedMessage(); + var failedMessage = await CreateAndStoreFailedMessage(); var message = CreateEditMessage(failedMessage.UniqueMessageId); - using (var session = Store.OpenAsyncSession()) + var redirects = await MessageRedirectsDataStore.GetOrCreate(); + redirects.Redirects.Add(new MessageRedirect { - var redirects = await MessageRedirectsCollection.GetOrCreate(session); - redirects.Redirects.Add(new MessageRedirect - { - FromPhysicalAddress = failedMessage.ProcessingAttempts.Last().FailureDetails.AddressOfFailingEndpoint, - ToPhysicalAddress = redirectAddress - }); - await redirects.Save(session); - } + FromPhysicalAddress = failedMessage.ProcessingAttempts.Last().FailureDetails.AddressOfFailingEndpoint, + ToPhysicalAddress = redirectAddress + }); + await MessageRedirectsDataStore.Save(redirects); - await Handler.Handle(message, new TestableInvokeHandlerContext()); + await handler.Handle(message, new TestableInvokeHandlerContext()); - var sentMessage = Dispatcher.DispatchedMessages.Single().Item1; + var sentMessage = dispatcher.DispatchedMessages.Single().Item1; Assert.AreEqual(redirectAddress, sentMessage.Destination); } [Test] public async Task Should_mark_edited_message_with_edit_information() { - var messageFailure = await CreateFailedMessage(); + var messageFailure = await CreateAndStoreFailedMessage(); var message = CreateEditMessage(messageFailure.UniqueMessageId); - await Handler.Handle(message, new TestableInvokeHandlerContext()); + await handler.Handle(message, new TestableInvokeHandlerContext()); - var sentMessage = Dispatcher.DispatchedMessages.Single(); + var sentMessage = dispatcher.DispatchedMessages.Single(); Assert.AreEqual( messageFailure.Id, "FailedMessages/" + sentMessage.Item1.Message.Headers["ServiceControl.EditOf"]); @@ -203,12 +199,12 @@ public async Task Should_mark_edited_message_with_edit_information() [Test] public async Task Should_assign_edited_message_new_message_id() { - var messageFailure = await CreateFailedMessage(); + var messageFailure = await CreateAndStoreFailedMessage(); var message = CreateEditMessage(messageFailure.UniqueMessageId); - await Handler.Handle(message, new TestableInvokeHandlerContext()); + await handler.Handle(message, new TestableInvokeHandlerContext()); - var sentMessage = Dispatcher.DispatchedMessages.Single(); + var sentMessage = dispatcher.DispatchedMessages.Single(); Assert.AreNotEqual( messageFailure.ProcessingAttempts.Last().MessageId, sentMessage.Item1.Message.MessageId); @@ -224,17 +220,16 @@ static EditAndSend CreateEditMessage(string failedMessageId, byte[] newBodyConte }; } - async Task CreateFailedMessage(string failedMessageId = null, FailedMessageStatus status = FailedMessageStatus.Unresolved) + async Task CreateAndStoreFailedMessage(string failedMessageId = null, FailedMessageStatus status = FailedMessageStatus.Unresolved) { failedMessageId = failedMessageId ?? Guid.NewGuid().ToString(); - using (var session = Store.OpenAsyncSession()) + + var failedMessage = new FailedMessage { - var failedMessage = new FailedMessage - { - UniqueMessageId = failedMessageId, - Id = FailedMessage.MakeDocumentId(failedMessageId), - Status = status, - ProcessingAttempts = new List + UniqueMessageId = failedMessageId, + Id = FailedMessageIdGenerator.MakeDocumentId(failedMessageId), + Status = status, + ProcessingAttempts = new List { new FailedMessage.ProcessingAttempt { @@ -245,17 +240,15 @@ async Task CreateFailedMessage(string failedMessageId = null, Fai } } } - }; - await session.StoreAsync(failedMessage); - await session.SaveChangesAsync(); - return failedMessage; - } + }; + await ErrorMessageDataStore.StoreFailedMessages(new[] { failedMessage }); + return failedMessage; } } - public class TestableUnicastDispatcher : IDispatchMessages + public sealed class TestableUnicastDispatcher : IDispatchMessages { - public List<(UnicastTransportOperation, TransportTransaction, ContextBag)> DispatchedMessages { get; set; } = new List<(UnicastTransportOperation, TransportTransaction, ContextBag)>(); + public List<(UnicastTransportOperation, TransportTransaction, ContextBag)> DispatchedMessages { get; } = new List<(UnicastTransportOperation, TransportTransaction, ContextBag)>(); public Task Dispatch(TransportOperations outgoingMessages, TransportTransaction transaction, ContextBag context) { diff --git a/src/ServiceControl.Persistence.Tests/RetryStateTests.cs b/src/ServiceControl.Persistence.Tests/RetryStateTests.cs new file mode 100644 index 0000000000..7d2d1f91b3 --- /dev/null +++ b/src/ServiceControl.Persistence.Tests/RetryStateTests.cs @@ -0,0 +1,307 @@ +namespace ServiceControl.PersistenceTests +{ + using System; + using System.Collections.Generic; + using System.Linq; + using System.Threading; + using System.Threading.Tasks; + using Microsoft.Extensions.Hosting; + using NServiceBus.Extensibility; + using NServiceBus.Transport; + using NUnit.Framework; + using ServiceBus.Management.Infrastructure.Settings; + using ServiceControl.Contracts.Operations; + using ServiceControl.Infrastructure.BackgroundTasks; + using ServiceControl.Infrastructure.DomainEvents; + using ServiceControl.MessageFailures; + using ServiceControl.Persistence; + using ServiceControl.Recoverability; + + [NonParallelizable] + class RetryStateTests : PersistenceTestBase + { + [Test] + public async Task When_a_group_is_processed_it_is_set_to_the_Preparing_state() + { + var domainEvents = new FakeDomainEvents(); + var retryManager = new RetryingManager(domainEvents); + + await CreateAFailedMessageAndMarkAsPartOfRetryBatch(retryManager, "Test-group", true, 1); + var status = retryManager.GetStatusForRetryOperation("Test-group", RetryType.FailureGroup); + + Assert.AreEqual(RetryState.Preparing, status.RetryState); + } + + [Test] + public async Task When_a_group_is_prepared_and_SC_is_started_the_group_is_marked_as_failed() + { + var domainEvents = new FakeDomainEvents(); + var retryManager = new RetryingManager(domainEvents); + + await CreateAFailedMessageAndMarkAsPartOfRetryBatch(retryManager, "Test-group", false, 1); + + var documentManager = new CustomRetryDocumentManager(false, RetryStore, retryManager); + + var orphanage = new RecoverabilityComponent.AdoptOrphanBatchesFromPreviousSessionHostedService(documentManager, new AsyncTimer()); + await orphanage.AdoptOrphanedBatchesAsync(); + await CompleteDatabaseOperation(); + + var status = retryManager.GetStatusForRetryOperation("Test-group", RetryType.FailureGroup); + Assert.True(status.Failed); + } + + [Test] + public async Task When_a_group_is_prepared_with_three_batches_and_SC_is_restarted_while_the_first_group_is_being_forwarded_then_the_count_still_matches() + { + var domainEvents = new FakeDomainEvents(); + var retryManager = new RetryingManager(domainEvents); + + await CreateAFailedMessageAndMarkAsPartOfRetryBatch(retryManager, "Test-group", true, 2001); + + var sender = new TestSender(); + var processor = new RetryProcessor(RetryBatchesStore, domainEvents, new TestReturnToSenderDequeuer(new ReturnToSender(BodyStorage, ErrorStore), ErrorStore, domainEvents, "TestEndpoint"), retryManager); + + // Needs index RetryBatches_ByStatus_ReduceInitialBatchSize + await CompleteDatabaseOperation(); + + await processor.ProcessBatches(sender); // mark ready + + // Simulate SC restart + retryManager = new RetryingManager(domainEvents); + + var documentManager = new CustomRetryDocumentManager(false, RetryStore, retryManager); + + await documentManager.RebuildRetryOperationState(); + + processor = new RetryProcessor(RetryBatchesStore, domainEvents, new TestReturnToSenderDequeuer(new ReturnToSender(BodyStorage, ErrorStore), ErrorStore, domainEvents, "TestEndpoint"), retryManager); + + await processor.ProcessBatches(sender); + + var status = retryManager.GetStatusForRetryOperation("Test-group", RetryType.FailureGroup); + Assert.AreEqual(2001, status.TotalNumberOfMessages); + } + + [Test] + public async Task When_a_group_is_forwarded_the_status_is_Completed() + { + var domainEvents = new FakeDomainEvents(); + var retryManager = new RetryingManager(domainEvents); + + await CreateAFailedMessageAndMarkAsPartOfRetryBatch(retryManager, "Test-group", true, 1); + + var sender = new TestSender(); + + var returnToSender = new TestReturnToSenderDequeuer(new ReturnToSender(BodyStorage, ErrorStore), ErrorStore, domainEvents, "TestEndpoint"); + var processor = new RetryProcessor(RetryBatchesStore, domainEvents, returnToSender, retryManager); + + await processor.ProcessBatches(sender); // mark ready + await processor.ProcessBatches(sender); + + var status = retryManager.GetStatusForRetryOperation("Test-group", RetryType.FailureGroup); + Assert.AreEqual(RetryState.Completed, status.RetryState); + } + + [Test] + public async Task When_there_is_one_poison_message_it_is_removed_from_batch_and_the_status_is_Complete() + { + var domainEvents = new FakeDomainEvents(); + var retryManager = new RetryingManager(domainEvents); + + await CreateAFailedMessageAndMarkAsPartOfRetryBatch(retryManager, "Test-group", true, "A", "B", "C"); + + var sender = new TestSender + { + Callback = operation => + { + //Always fails staging message B + if (operation.Message.MessageId == "FailedMessages/B") + { + throw new Exception("Simulated"); + } + } + }; + + var returnToSender = new TestReturnToSenderDequeuer(new ReturnToSender(BodyStorage, ErrorStore), ErrorStore, domainEvents, "TestEndpoint"); + var processor = new RetryProcessor(RetryBatchesStore, domainEvents, returnToSender, retryManager); + + bool c; + do + { + try + { + c = await processor.ProcessBatches(sender); + } + catch (Exception) + { + //Continue trying until there is no exception -> poison message is removed from the batch + c = true; + } + } + while (c); + + var status = retryManager.GetStatusForRetryOperation("Test-group", RetryType.FailureGroup); + + Assert.AreEqual(RetryState.Completed, status.RetryState); + Assert.AreEqual(3, status.NumberOfMessagesPrepared); + Assert.AreEqual(2, status.NumberOfMessagesForwarded); + Assert.AreEqual(1, status.NumberOfMessagesSkipped); + } + + [Test] + public async Task When_a_group_has_one_batch_out_of_two_forwarded_the_status_is_Forwarding() + { + var domainEvents = new FakeDomainEvents(); + var retryManager = new RetryingManager(domainEvents); + + await CreateAFailedMessageAndMarkAsPartOfRetryBatch(retryManager, "Test-group", true, 1001); + + var returnToSender = new ReturnToSender(BodyStorage, ErrorStore); + + var sender = new TestSender(); + + var processor = new RetryProcessor(RetryBatchesStore, domainEvents, new TestReturnToSenderDequeuer(returnToSender, ErrorStore, domainEvents, "TestEndpoint"), retryManager); + + await CompleteDatabaseOperation(); + + await processor.ProcessBatches(sender); // mark ready + await processor.ProcessBatches(sender); + + var status = retryManager.GetStatusForRetryOperation("Test-group", RetryType.FailureGroup); + Assert.AreEqual(RetryState.Forwarding, status.RetryState); + } + + Task CreateAFailedMessageAndMarkAsPartOfRetryBatch(RetryingManager retryManager, string groupId, bool progressToStaged, int numberOfMessages) + { + return CreateAFailedMessageAndMarkAsPartOfRetryBatch(retryManager, groupId, progressToStaged, Enumerable.Range(0, numberOfMessages).Select(i => Guid.NewGuid().ToString()).ToArray()); + } + + async Task CreateAFailedMessageAndMarkAsPartOfRetryBatch(RetryingManager retryManager, string groupId, bool progressToStaged, params string[] messageIds) + { + var messages = messageIds.Select(id => new FailedMessage + { + Id = FailedMessageIdGenerator.MakeDocumentId(id), + UniqueMessageId = id, + FailureGroups = new List + { + new FailedMessage.FailureGroup + { + Id = groupId, + Title = groupId, + Type = groupId + } + }, + Status = FailedMessageStatus.Unresolved, + ProcessingAttempts = new List + { + new FailedMessage.ProcessingAttempt + { + AttemptedAt = DateTime.UtcNow, + MessageMetadata = new Dictionary(), + FailureDetails = new FailureDetails(), + Headers = new Dictionary() + } + } + }).ToArray(); + + await ErrorStore.StoreFailedMessages(messages); + + // Needs index FailedMessages_ByGroup + // Needs index FailedMessages_UniqueMessageIdAndTimeOfFailures + await CompleteDatabaseOperation(); + + var documentManager = new CustomRetryDocumentManager(progressToStaged, RetryStore, retryManager); + var gateway = new CustomRetriesGateway(progressToStaged, RetryStore, retryManager); + + // TODO: groupType appears to be the same as classifier, which was null in the previous StartRetryForIndex call - make sure that's true + gateway.EnqueueRetryForFailureGroup(new RetriesGateway.RetryForFailureGroup(groupId, "Test-Context", groupType: null, DateTime.UtcNow)); + + await CompleteDatabaseOperation(); + + await gateway.ProcessNextBulkRetry(); + + // Wait for indexes to catch up + await CompleteDatabaseOperation(); + } + + class CustomRetriesGateway : RetriesGateway + { + public CustomRetriesGateway(bool progressToStaged, IRetryDocumentDataStore store, RetryingManager retryManager) + : base(store, retryManager) + { + this.progressToStaged = progressToStaged; + } + + protected override Task MoveBatchToStaging(string batchDocumentId) + { + if (progressToStaged) + { + return base.MoveBatchToStaging(batchDocumentId); + } + + return Task.FromResult(0); + } + + bool progressToStaged; + } + + class CustomRetryDocumentManager : RetryDocumentManager + { + public CustomRetryDocumentManager(bool progressToStaged, IRetryDocumentDataStore retryStore, RetryingManager retryManager) + : base(new FakeApplicationLifetime(), retryStore, retryManager) + { + RetrySessionId = Guid.NewGuid().ToString(); + this.progressToStaged = progressToStaged; + } + + public override Task MoveBatchToStaging(string batchDocumentId) + { + if (progressToStaged) + { + return base.MoveBatchToStaging(batchDocumentId); + } + + return Task.FromResult(0); + } + + bool progressToStaged; + } + + + class FakeApplicationLifetime : IHostApplicationLifetime + { + public void StopApplication() => throw new NotImplementedException(); + + public CancellationToken ApplicationStarted { get; } = new CancellationToken(); + public CancellationToken ApplicationStopping { get; } = new CancellationToken(); + public CancellationToken ApplicationStopped { get; } = new CancellationToken(); + } + + class TestReturnToSenderDequeuer : ReturnToSenderDequeuer + { + public TestReturnToSenderDequeuer(ReturnToSender returnToSender, IErrorMessageDataStore store, IDomainEvents domainEvents, string endpointName) + : base(returnToSender, store, domainEvents, null, new Settings(endpointName)) + { + } + + public override Task Run(string forwardingBatchId, Predicate filter, int? expectedMessageCount, CancellationToken cancellationToken = default) + { + return Task.FromResult(0); + } + } + + public class TestSender : IDispatchMessages + { + public Action Callback { get; set; } = m => { }; + + public Task Dispatch(TransportOperations outgoingMessages, TransportTransaction transaction, ContextBag context) + { + foreach (var operation in outgoingMessages.UnicastTransportOperations) + { + Callback(operation); + } + + return Task.FromResult(0); + } + } + } +} diff --git a/src/ServiceControl.Persistence.Tests/TestPersistence.cs b/src/ServiceControl.Persistence.Tests/TestPersistence.cs new file mode 100644 index 0000000000..7bd9e6694b --- /dev/null +++ b/src/ServiceControl.Persistence.Tests/TestPersistence.cs @@ -0,0 +1,18 @@ +namespace ServiceControl.PersistenceTests +{ + using System.Diagnostics; + using System.Threading.Tasks; + using Microsoft.Extensions.DependencyInjection; + + abstract class TestPersistence + { + public abstract void Configure(IServiceCollection services); + + public abstract Task CompleteDatabaseOperation(); + + public override string ToString() => GetType().Name; + + [Conditional("DEBUG")] + public virtual void BlockToInspectDatabase() { } + } +} \ No newline at end of file diff --git a/src/ServiceControl.AcceptanceTests/TestsFilter.cs b/src/ServiceControl.Persistence.Tests/TestsFilter.cs similarity index 100% rename from src/ServiceControl.AcceptanceTests/TestsFilter.cs rename to src/ServiceControl.Persistence.Tests/TestsFilter.cs diff --git a/src/ServiceControl.Persistence/.editorconfig b/src/ServiceControl.Persistence/.editorconfig new file mode 100644 index 0000000000..ff993b49bb --- /dev/null +++ b/src/ServiceControl.Persistence/.editorconfig @@ -0,0 +1,4 @@ +[*.cs] + +# Justification: ServiceControl app has no synchronization context +dotnet_diagnostic.CA2007.severity = none diff --git a/src/ServiceControl/Notifications/EmailNotifications.cs b/src/ServiceControl.Persistence/EmailNotifications.cs similarity index 100% rename from src/ServiceControl/Notifications/EmailNotifications.cs rename to src/ServiceControl.Persistence/EmailNotifications.cs diff --git a/src/ServiceControl/EventLog/EventLogItem.cs b/src/ServiceControl.Persistence/EventLog/EventLogItem.cs similarity index 100% rename from src/ServiceControl/EventLog/EventLogItem.cs rename to src/ServiceControl.Persistence/EventLog/EventLogItem.cs diff --git a/src/ServiceControl/EventLog/Severity.cs b/src/ServiceControl.Persistence/EventLog/Severity.cs similarity index 100% rename from src/ServiceControl/EventLog/Severity.cs rename to src/ServiceControl.Persistence/EventLog/Severity.cs diff --git a/src/ServiceControl.Persistence/ExternalIntegrations/ExternalIntegrationDispatchRequest.cs b/src/ServiceControl.Persistence/ExternalIntegrations/ExternalIntegrationDispatchRequest.cs new file mode 100644 index 0000000000..fc7ce898e9 --- /dev/null +++ b/src/ServiceControl.Persistence/ExternalIntegrations/ExternalIntegrationDispatchRequest.cs @@ -0,0 +1,8 @@ +namespace ServiceControl.ExternalIntegrations +{ + public class ExternalIntegrationDispatchRequest + { + public string Id { get; set; } + public object DispatchContext; // TODO: This is of type object, do we want the persister API to explicitly do something with this? Maybe instead of object use the specific types? Alternatively already have SC do serialization on this field so the storage engine does not need to? + } +} \ No newline at end of file diff --git a/src/ServiceControl/Operations/FailedAuditImport.cs b/src/ServiceControl.Persistence/FailedAuditImport.cs similarity index 100% rename from src/ServiceControl/Operations/FailedAuditImport.cs rename to src/ServiceControl.Persistence/FailedAuditImport.cs diff --git a/src/ServiceControl/Operations/FailedErrorImport.cs b/src/ServiceControl.Persistence/FailedErrorImport.cs similarity index 100% rename from src/ServiceControl/Operations/FailedErrorImport.cs rename to src/ServiceControl.Persistence/FailedErrorImport.cs diff --git a/src/ServiceControl.Persistence/FailedMessage.cs b/src/ServiceControl.Persistence/FailedMessage.cs index 073d75ea62..2671888f3c 100644 --- a/src/ServiceControl.Persistence/FailedMessage.cs +++ b/src/ServiceControl.Persistence/FailedMessage.cs @@ -21,17 +21,6 @@ public FailedMessage() public FailedMessageStatus Status { get; set; } - public static string MakeDocumentId(string messageUniqueId) - { - return $"{CollectionName}/{messageUniqueId}"; - } - - public static string GetMessageIdFromDocumentId(string failedMessageDocumentId) - { - return failedMessageDocumentId.Substring(CollectionName.Length + 1); - } - - public const string CollectionName = "FailedMessages"; public class ProcessingAttempt { diff --git a/src/ServiceControl.Persistence/FailedMessageTotals.cs b/src/ServiceControl.Persistence/FailedMessageTotals.cs new file mode 100644 index 0000000000..46608d21cb --- /dev/null +++ b/src/ServiceControl.Persistence/FailedMessageTotals.cs @@ -0,0 +1,8 @@ +namespace ServiceControl.Persistence +{ + public class FailedMessageTotals + { + public int UnresolvedTotal; + public int ArchivedTotal; + } +} \ No newline at end of file diff --git a/src/ServiceControl/MessageFailures/Api/FailedMessageView.cs b/src/ServiceControl.Persistence/FailedMessageView.cs similarity index 100% rename from src/ServiceControl/MessageFailures/Api/FailedMessageView.cs rename to src/ServiceControl.Persistence/FailedMessageView.cs diff --git a/src/ServiceControl/Operations/FailedTransportMessage.cs b/src/ServiceControl.Persistence/FailedTransportMessage.cs similarity index 100% rename from src/ServiceControl/Operations/FailedTransportMessage.cs rename to src/ServiceControl.Persistence/FailedTransportMessage.cs diff --git a/src/ServiceControl/Recoverability/Grouping/Raven/FailureGroupMessageView.cs b/src/ServiceControl.Persistence/FailureGroupMessageView.cs similarity index 100% rename from src/ServiceControl/Recoverability/Grouping/Raven/FailureGroupMessageView.cs rename to src/ServiceControl.Persistence/FailureGroupMessageView.cs diff --git a/src/ServiceControl/Recoverability/Grouping/Raven/FailureGroupView.cs b/src/ServiceControl.Persistence/FailureGroupView.cs similarity index 100% rename from src/ServiceControl/Recoverability/Grouping/Raven/FailureGroupView.cs rename to src/ServiceControl.Persistence/FailureGroupView.cs diff --git a/src/ServiceControl/Recoverability/GroupOperation.cs b/src/ServiceControl.Persistence/GroupOperation.cs similarity index 100% rename from src/ServiceControl/Recoverability/GroupOperation.cs rename to src/ServiceControl.Persistence/GroupOperation.cs diff --git a/src/ServiceControl/Recoverability/Retrying/History/HistoricRetryOperation.cs b/src/ServiceControl.Persistence/History/HistoricRetryOperation.cs similarity index 100% rename from src/ServiceControl/Recoverability/Retrying/History/HistoricRetryOperation.cs rename to src/ServiceControl.Persistence/History/HistoricRetryOperation.cs diff --git a/src/ServiceControl/Recoverability/Retrying/History/UnacknowledgedRetryOperation.cs b/src/ServiceControl.Persistence/History/UnacknowledgedRetryOperation.cs similarity index 100% rename from src/ServiceControl/Recoverability/Retrying/History/UnacknowledgedRetryOperation.cs rename to src/ServiceControl.Persistence/History/UnacknowledgedRetryOperation.cs diff --git a/src/ServiceControl/Operations/BodyStorage/IBodyStorage.cs b/src/ServiceControl.Persistence/IBodyStorage.cs similarity index 60% rename from src/ServiceControl/Operations/BodyStorage/IBodyStorage.cs rename to src/ServiceControl.Persistence/IBodyStorage.cs index 8c6fa7829f..9c688e1c88 100644 --- a/src/ServiceControl/Operations/BodyStorage/IBodyStorage.cs +++ b/src/ServiceControl.Persistence/IBodyStorage.cs @@ -3,16 +3,16 @@ using System.IO; using System.Threading.Tasks; - interface IBodyStorage + public interface IBodyStorage { Task Store(string bodyId, string contentType, int bodySize, Stream bodyStream); - Task TryFetch(string bodyId); + Task TryFetch(string bodyId); } - class StreamResult + public class MessageBodyStreamResult { public bool HasResult; - public Stream Stream; + public MemoryStream Stream; // Intentional, other streams could require a context public string ContentType; public int BodySize; public string Etag; diff --git a/src/ServiceControl.Persistence/ICustomChecksDataStore.cs b/src/ServiceControl.Persistence/ICustomChecksDataStore.cs index f5223413f8..187cf472cf 100644 --- a/src/ServiceControl.Persistence/ICustomChecksDataStore.cs +++ b/src/ServiceControl.Persistence/ICustomChecksDataStore.cs @@ -12,5 +12,6 @@ public interface ICustomChecksDataStore Task>> GetStats(PagingInfo paging, string status = null); Task DeleteCustomCheck(Guid id); + Task GetNumberOfFailedChecks(); } } \ No newline at end of file diff --git a/src/ServiceControl.Persistence/IDataSessionManager.cs b/src/ServiceControl.Persistence/IDataSessionManager.cs new file mode 100644 index 0000000000..fa6973ce03 --- /dev/null +++ b/src/ServiceControl.Persistence/IDataSessionManager.cs @@ -0,0 +1,10 @@ +namespace ServiceControl.Persistence +{ + using System; + using System.Threading.Tasks; + + public interface IDataSessionManager : IDisposable + { + Task SaveChanges(); + } +} diff --git a/src/ServiceControl.Persistence/IEditFailedMessagesManager.cs b/src/ServiceControl.Persistence/IEditFailedMessagesManager.cs new file mode 100644 index 0000000000..4e58d8ed05 --- /dev/null +++ b/src/ServiceControl.Persistence/IEditFailedMessagesManager.cs @@ -0,0 +1,13 @@ +namespace ServiceControl.Persistence +{ + using System.Threading.Tasks; + using ServiceControl.MessageFailures; + + public interface IEditFailedMessagesManager : IDataSessionManager + { + Task GetFailedMessage(string failedMessageId); + Task GetCurrentEditingMessageId(string failedMessageId); + Task SetCurrentEditingMessageId(string editingMessageId); + Task SetFailedMessageAsResolved(); + } +} diff --git a/src/ServiceControl.Persistence/IErrorMessageDatastore.cs b/src/ServiceControl.Persistence/IErrorMessageDatastore.cs new file mode 100644 index 0000000000..d204753742 --- /dev/null +++ b/src/ServiceControl.Persistence/IErrorMessageDatastore.cs @@ -0,0 +1,82 @@ +namespace ServiceControl.Persistence +{ + using System; + using System.Collections.Generic; + using System.Threading.Tasks; + using CompositeViews.Messages; + using Infrastructure; + using MessageFailures.Api; + using ServiceControl.EventLog; + using ServiceControl.MessageFailures; + using ServiceControl.Operations; + using ServiceControl.Recoverability; + + public interface IErrorMessageDataStore + { + Task>> GetAllMessages(PagingInfo pagingInfo, SortInfo sortInfo, bool includeSystemMessages); + Task>> GetAllMessagesForEndpoint(string endpointName, PagingInfo pagingInfo, SortInfo sortInfo, bool includeSystemMessages); + Task>> GetAllMessagesByConversation(string conversationId, PagingInfo pagingInfo, SortInfo sortInfo, bool includeSystemMessages); + Task>> GetAllMessagesForSearch(string searchTerms, PagingInfo pagingInfo, SortInfo sortInfo); + Task>> GetAllMessagesForEndpoint(string searchTerms, string receivingEndpointName, PagingInfo pagingInfo, SortInfo sortInfo); + Task FailedMessageFetch(string failedMessageId); + Task FailedMessageMarkAsArchived(string failedMessageId); + Task FailedMessagesFetch(Guid[] ids); + Task StoreFailedErrorImport(FailedErrorImport failure); + Task CreateEditFailedMessageManager(); + Task> GetFailureGroupView(string groupId, string status, string modified); + Task> GetFailureGroupsByClassifier(string classifier); + + // GetAllErrorsController + Task>> ErrorGet(string status, string modified, string queueAddress, PagingInfo pagingInfo, SortInfo sortInfo); + Task ErrorsHead(string status, string modified, string queueAddress); + Task>> ErrorsByEndpointName(string status, string endpointName, string modified, PagingInfo pagingInfo, SortInfo sortInfo); + Task> ErrorsSummary(); // TODO: Must not be object + + // GetErrorByIdController + Task ErrorBy(Guid failedMessageId); + Task ErrorLastBy(Guid failedMessageId); + + //EditFailedMessagesController + Task ErrorBy(string failedMessageId); + + //NotificationsController + Task CreateNotificationsManager(); + + // FailureGroupsController + Task EditComment(string groupId, string comment); + Task DeleteComment(string groupId); + // Task GetAllGroups([FromUri] string classifierFilter = null, string classifier = "Exception Type and Stack Trace"); TODO: Analyze what to do with the `GroupFetcher` dependency + Task>> GetGroupErrors(string groupId, string status, string modified, SortInfo sortInfo, PagingInfo pagingInfo); + Task GetGroupErrorsCount(string groupId, string status, string modified); + + Task>> GetGroup(string groupId, string status, string modified); + + // LegacyMessageFailureResolvedHandler + Task MarkMessageAsResolved(string failedMessageId); + + // MessageFailureResolvedHandler + Task ProcessPendingRetries(DateTime periodFrom, DateTime periodTo, string queueAddress, Func processCallback); // TODO: Passing a callback is there to not change behavior of original implementation. + + // UnArchiveMessagesByRangeHandler + Task<(string[] ids, int count)> UnArchiveMessagesByRange(DateTime from, DateTime to, DateTime cutOff); + + // UnArchiveMessagesHandler + Task<(string[] ids, int count)> UnArchiveMessages(IEnumerable failedMessageIds); + + // ReturnToSenderDequeuer.CaptureIfMessageSendingFails + Task RevertRetry(string messageUniqueId); + Task RemoveFailedMessageRetryDocument(string uniqueMessageId); + Task GetRetryPendingMessages(DateTime from, DateTime to, string queueAddress); + + // ReturnToSender.FetchFromFailedMessage + Task FetchFromFailedMessage(string uniqueMessageId); + + // AuditEventLogWriter + Task StoreEventLogItem(EventLogItem logItem); + + Task>> SearchEndpointMessages(string endpointName, string searchKeyword, PagingInfo pagingInfo, SortInfo sortInfo); + + // TODO: So far only used in a persistence test: RetryStateTests + Task StoreFailedMessages(params FailedMessage[] failedMessages); + } +} \ No newline at end of file diff --git a/src/ServiceControl.Persistence/IEventLogDataStore.cs b/src/ServiceControl.Persistence/IEventLogDataStore.cs new file mode 100644 index 0000000000..b9725cac47 --- /dev/null +++ b/src/ServiceControl.Persistence/IEventLogDataStore.cs @@ -0,0 +1,13 @@ +namespace ServiceControl.Persistence +{ + using System.Collections.Generic; + using System.Threading.Tasks; + using EventLog; + using Infrastructure; + + public interface IEventLogDataStore + { + Task Add(EventLogItem logItem); + Task<(IList items, int total, string version)> GetEventLogItems(PagingInfo pagingInfo); + } +} \ No newline at end of file diff --git a/src/ServiceControl.Persistence/IExternalIntegrationRequestsDataStore.cs b/src/ServiceControl.Persistence/IExternalIntegrationRequestsDataStore.cs new file mode 100644 index 0000000000..d1da154989 --- /dev/null +++ b/src/ServiceControl.Persistence/IExternalIntegrationRequestsDataStore.cs @@ -0,0 +1,15 @@ +namespace ServiceControl.Persistence +{ + using System; + using System.Collections.Generic; + using System.Threading; + using System.Threading.Tasks; + using ServiceControl.ExternalIntegrations; + + public interface IExternalIntegrationRequestsDataStore + { + void Subscribe(Func callback); + Task StoreDispatchRequest(IEnumerable dispatchRequests); + Task StopAsync(CancellationToken cancellationToken); + } +} \ No newline at end of file diff --git a/src/ServiceControl.Persistence/IFailedErrorImportDataStore.cs b/src/ServiceControl.Persistence/IFailedErrorImportDataStore.cs new file mode 100644 index 0000000000..54464bffcf --- /dev/null +++ b/src/ServiceControl.Persistence/IFailedErrorImportDataStore.cs @@ -0,0 +1,13 @@ +namespace ServiceControl.Persistence +{ + using System; + using System.Threading; + using System.Threading.Tasks; + using ServiceControl.Operations; + + public interface IFailedErrorImportDataStore + { + Task ProcessFailedErrorImports(Func processMessage, CancellationToken cancellationToken); + Task QueryContainsFailedImports(); + } +} \ No newline at end of file diff --git a/src/ServiceControl.Persistence/IFailedMessageViewIndexNotifications.cs b/src/ServiceControl.Persistence/IFailedMessageViewIndexNotifications.cs new file mode 100644 index 0000000000..7825c40884 --- /dev/null +++ b/src/ServiceControl.Persistence/IFailedMessageViewIndexNotifications.cs @@ -0,0 +1,10 @@ +namespace ServiceControl.Persistence +{ + using System; + using System.Threading.Tasks; + + public interface IFailedMessageViewIndexNotifications + { + IDisposable Subscribe(Func callback); + } +} \ No newline at end of file diff --git a/src/ServiceControl.Persistence/IGroupsDataStore.cs b/src/ServiceControl.Persistence/IGroupsDataStore.cs new file mode 100644 index 0000000000..7f1fa1b02c --- /dev/null +++ b/src/ServiceControl.Persistence/IGroupsDataStore.cs @@ -0,0 +1,12 @@ +namespace ServiceControl.Persistence +{ + using System.Collections.Generic; + using System.Threading.Tasks; + using ServiceControl.Recoverability; + + public interface IGroupsDataStore + { + Task> GetFailureGroupsByClassifier(string classifier, string classifierFilter); + Task GetCurrentForwardingBatch(); + } +} diff --git a/src/ServiceControl.Persistence/INotificationsManager.cs b/src/ServiceControl.Persistence/INotificationsManager.cs new file mode 100644 index 0000000000..6bdb21dd5b --- /dev/null +++ b/src/ServiceControl.Persistence/INotificationsManager.cs @@ -0,0 +1,11 @@ +namespace ServiceControl.Persistence +{ + using System; + using System.Threading.Tasks; + using Notifications; + + public interface INotificationsManager : IDataSessionManager + { + Task LoadSettings(TimeSpan? cacheTimeout = null); + } +} \ No newline at end of file diff --git a/src/ServiceControl.Persistence/IPersistence.cs b/src/ServiceControl.Persistence/IPersistence.cs new file mode 100644 index 0000000000..2a4bef4cdd --- /dev/null +++ b/src/ServiceControl.Persistence/IPersistence.cs @@ -0,0 +1,11 @@ +namespace ServiceControl.Persistence +{ + using Microsoft.Extensions.DependencyInjection; + + public interface IPersistence + { + void Configure(IServiceCollection serviceCollection); + IPersistenceInstaller CreateInstaller(); + IPersistenceLifecycle CreateLifecycle(); + } +} diff --git a/src/ServiceControl.Persistence/IPersistenceConfiguration.cs b/src/ServiceControl.Persistence/IPersistenceConfiguration.cs index dfbf8ac53e..e7794196c8 100644 --- a/src/ServiceControl.Persistence/IPersistenceConfiguration.cs +++ b/src/ServiceControl.Persistence/IPersistenceConfiguration.cs @@ -1,77 +1,11 @@ namespace ServiceControl.Persistence { - using System.Data.SqlClient; - using System.Threading.Tasks; - using Microsoft.Extensions.DependencyInjection; + using System.Collections.Generic; public interface IPersistenceConfiguration { - void ConfigureServices(IServiceCollection serviceCollection); - } - - //NOTE ideally once we only have one type of persistence (ie use ravendb or sql for all) then these could be refactored - public static class SetupSqlPersistence - { - public static async Task SetupMonitoring(string connectionString) - { - using (var connection = new SqlConnection(connectionString)) - { - var catalog = new SqlConnectionStringBuilder(connectionString).InitialCatalog; - - var createCommand = $@" - IF NOT EXISTS ( - SELECT * - FROM {catalog}.sys.objects - WHERE object_id = OBJECT_ID(N'KnownEndpoints') AND type in (N'U') - ) - BEGIN - CREATE TABLE [dbo].[KnownEndpoints]( - [Id] [uniqueidentifier] NOT NULL, - [HostId] [uniqueidentifier] NOT NULL, - [Host] [nvarchar](300) NULL, - [HostDisplayName] [nvarchar](300) NULL, - [Monitored] [bit] NOT NULL - ) ON [PRIMARY] - END"; - - connection.Open(); - - var cmd = new SqlCommand(createCommand, connection); - await cmd.ExecuteNonQueryAsync().ConfigureAwait(false); - } - } - - public static async Task SetupCustomChecks(string connectionString) - { - using (var connection = new SqlConnection(connectionString)) - { - var catalog = new SqlConnectionStringBuilder(connectionString).InitialCatalog; - - var createCommand = $@" - IF NOT EXISTS ( - SELECT * - FROM {catalog}.sys.objects - WHERE object_id = OBJECT_ID(N'CustomChecks') AND type in (N'U') - ) - BEGIN - CREATE TABLE [dbo].[CustomChecks]( - [Id] [uniqueidentifier] NOT NULL, - [CustomCheckId] nvarchar(300) NOT NULL, - [Category] nvarchar(300) NULL, - [Status] int NOT NULL, - [ReportedAt] datetime NOT NULL, - [FailureReason] nvarchar(300) NULL, - [OriginatingEndpointName] nvarchar(300) NOT NULL, - [OriginatingEndpointHostId] [uniqueidentifier] NOT NULL, - [OriginatingEndpointHost] nvarchar(300) NOT NULL - ) ON [PRIMARY] - END"; - - connection.Open(); - - var cmd = new SqlCommand(createCommand, connection); - await cmd.ExecuteNonQueryAsync().ConfigureAwait(false); - } - } + string Name { get; } + IEnumerable ConfigurationKeys { get; } + IPersistence Create(PersistenceSettings settings); } } \ No newline at end of file diff --git a/src/ServiceControl.Persistence/IPersistenceInstaller.cs b/src/ServiceControl.Persistence/IPersistenceInstaller.cs new file mode 100644 index 0000000000..dc5f4b28d9 --- /dev/null +++ b/src/ServiceControl.Persistence/IPersistenceInstaller.cs @@ -0,0 +1,10 @@ +namespace ServiceControl.Persistence +{ + using System.Threading; + using System.Threading.Tasks; + + public interface IPersistenceInstaller + { + Task Install(CancellationToken cancellationToken = default); + } +} diff --git a/src/ServiceControl.Persistence/IPersistenceLifecycle.cs b/src/ServiceControl.Persistence/IPersistenceLifecycle.cs new file mode 100644 index 0000000000..ff56494956 --- /dev/null +++ b/src/ServiceControl.Persistence/IPersistenceLifecycle.cs @@ -0,0 +1,11 @@ +namespace ServiceControl.Persistence +{ + using System.Threading; + using System.Threading.Tasks; + + public interface IPersistenceLifecycle // TODO: We don't need this, implementations should just implement IHostedService + { + Task Start(CancellationToken cancellationToken = default); + Task Stop(CancellationToken cancellationToken = default); + } +} diff --git a/src/ServiceControl.Persistence/IQueueAddressStore.cs b/src/ServiceControl.Persistence/IQueueAddressStore.cs new file mode 100644 index 0000000000..c1f12cf6d1 --- /dev/null +++ b/src/ServiceControl.Persistence/IQueueAddressStore.cs @@ -0,0 +1,13 @@ +namespace ServiceControl.Persistence +{ + using System.Collections.Generic; + using System.Threading.Tasks; + using ServiceControl.MessageFailures; + using ServiceControl.Persistence.Infrastructure; + + public interface IQueueAddressStore + { + Task>> GetAddresses(PagingInfo pagingInfo); + Task>> GetAddressesBySearchTerm(string search, PagingInfo pagingInfo); + } +} \ No newline at end of file diff --git a/src/ServiceControl.Persistence/IRetryBatchesDataStore.cs b/src/ServiceControl.Persistence/IRetryBatchesDataStore.cs new file mode 100644 index 0000000000..d24308558a --- /dev/null +++ b/src/ServiceControl.Persistence/IRetryBatchesDataStore.cs @@ -0,0 +1,20 @@ +namespace ServiceControl.Persistence +{ + using System; + using System.Collections.Generic; + using System.Threading.Tasks; + using MessageFailures; + using ServiceControl.Recoverability; + + public interface IRetryBatchesDataStore + { + Task CreateRetryBatchesManager(); + + Task RecordFailedStagingAttempt(IReadOnlyCollection messages, + IReadOnlyDictionary failedMessageRetriesById, Exception e, + int maxStagingAttempts, string stagingId); + + Task IncrementAttemptCounter(FailedMessageRetry failedMessageRetry); + Task DeleteFailedMessageRetry(string makeDocumentId); + } +} \ No newline at end of file diff --git a/src/ServiceControl.Persistence/IRetryBatchesManager.cs b/src/ServiceControl.Persistence/IRetryBatchesManager.cs new file mode 100644 index 0000000000..ccd6c1e613 --- /dev/null +++ b/src/ServiceControl.Persistence/IRetryBatchesManager.cs @@ -0,0 +1,23 @@ +namespace ServiceControl.Persistence +{ + using System.Collections.Generic; + using System.Threading; + using System.Threading.Tasks; + using MessageFailures; + using MessageRedirects; + using ServiceControl.Recoverability; + + public interface IRetryBatchesManager : IDataSessionManager + { + void Delete(RetryBatch retryBatch); + void Delete(RetryBatchNowForwarding forwardingBatch); + Task GetFailedMessageRetries(IList stagingBatchFailureRetries); + void Evict(FailedMessageRetry failedMessageRetry); + Task GetFailedMessages(Dictionary.KeyCollection keys); + Task GetRetryBatchNowForwarding(); + Task GetRetryBatch(string retryBatchId, CancellationToken cancellationToken); + Task GetStagingBatch(); + Task Store(RetryBatchNowForwarding retryBatchNowForwarding); + Task GetOrCreateMessageRedirectsCollection(); + } +} \ No newline at end of file diff --git a/src/ServiceControl.Persistence/IRetryDocumentDataStore.cs b/src/ServiceControl.Persistence/IRetryDocumentDataStore.cs new file mode 100644 index 0000000000..3562304783 --- /dev/null +++ b/src/ServiceControl.Persistence/IRetryDocumentDataStore.cs @@ -0,0 +1,34 @@ +namespace ServiceControl.Persistence +{ + using System.Threading.Tasks; + using System; + using System.Collections.Generic; + using Infrastructure; + using ServiceControl.MessageFailures; + using ServiceControl.Recoverability; + + public interface IRetryDocumentDataStore + { + Task StageRetryByUniqueMessageIds(string batchDocumentId, string requestId, RetryType retryType, string[] messageIds, + DateTime startTime, DateTime? last = null, string originator = null, string batchName = null, + string classifier = null); + + Task MoveBatchToStaging(string batchDocumentId); + + Task CreateBatchDocument(string retrySessionId, string requestId, RetryType retryType, + string[] failedMessageRetryIds, string originator, DateTime startTime, DateTime? last = null, + string batchName = null, string classifier = null); + + Task>> QueryOrphanedBatches(string retrySessionId, DateTime cutoff); + Task> QueryAvailableBatches(); + + // RetriesGateway + Task GetBatchesForAll(DateTime cutoff, Func callback); + Task GetBatchesForEndpoint(DateTime cutoff, string endpoint, Func callback); + Task GetBatchesForFailedQueueAddress(DateTime cutoff, string failedQueueAddresspoint, FailedMessageStatus status, Func callback); + Task GetBatchesForFailureGroup(string groupId, string groupTitle, string groupType, DateTime cutoff, Func callback); + + // RetryAllInGroupHandler + Task QueryFailureGroupViewOnGroupId(string groupId); + } +} \ No newline at end of file diff --git a/src/ServiceControl.Persistence/IRetryHistoryDataStore.cs b/src/ServiceControl.Persistence/IRetryHistoryDataStore.cs new file mode 100644 index 0000000000..ec3d2c4cb7 --- /dev/null +++ b/src/ServiceControl.Persistence/IRetryHistoryDataStore.cs @@ -0,0 +1,14 @@ +namespace ServiceControl.Persistence +{ + using System; + using System.Threading.Tasks; + using ServiceControl.Recoverability; + + public interface IRetryHistoryDataStore + { + Task GetRetryHistory(); + Task RecordRetryOperationCompleted(string requestId, RetryType retryType, DateTime startTime, DateTime completionTime, + string originator, string classifier, bool messageFailed, int numberOfMessagesProcessed, DateTime lastProcessed, int retryHistoryDepth); + Task AcknowledgeRetryGroup(string groupId); + } +} diff --git a/src/ServiceControl.Persistence/IServiceControlSubscriptionStorage.cs b/src/ServiceControl.Persistence/IServiceControlSubscriptionStorage.cs new file mode 100644 index 0000000000..09e57a38fc --- /dev/null +++ b/src/ServiceControl.Persistence/IServiceControlSubscriptionStorage.cs @@ -0,0 +1,10 @@ +namespace ServiceControl.Persistence +{ + using System.Threading.Tasks; + using NServiceBus.Unicast.Subscriptions.MessageDrivenSubscriptions; + + public interface IServiceControlSubscriptionStorage : ISubscriptionStorage + { + Task Initialize(); + } +} \ No newline at end of file diff --git a/src/ServiceControl.Persistence/Infrastructure/PagingInfo.cs b/src/ServiceControl.Persistence/Infrastructure/PagingInfo.cs index 65b51e98c9..5f6fe7746e 100644 --- a/src/ServiceControl.Persistence/Infrastructure/PagingInfo.cs +++ b/src/ServiceControl.Persistence/Infrastructure/PagingInfo.cs @@ -1,7 +1,5 @@ namespace ServiceControl.Persistence.Infrastructure { - using System.Net.Http; - public class PagingInfo { public const int DefaultPageSize = 50; @@ -19,24 +17,4 @@ public PagingInfo(int page = 1, int pageSize = DefaultPageSize) Offset = (Page - 1) * Next; } } - - public static class PagingInfoExtension - { - public static PagingInfo GetPagingInfo(this HttpRequestMessage request) - { - var maxResultsPerPage = request.GetQueryStringValue("per_page", PagingInfo.DefaultPageSize); - if (maxResultsPerPage < 1) - { - maxResultsPerPage = PagingInfo.DefaultPageSize; - } - - var page = request.GetQueryStringValue("page", 1); - if (page < 1) - { - page = 1; - } - - return new PagingInfo(page, maxResultsPerPage); - } - } } \ No newline at end of file diff --git a/src/ServiceControl.Persistence/Infrastructure/QueryStatsInfo.cs b/src/ServiceControl.Persistence/Infrastructure/QueryStatsInfo.cs index 1837959612..18d14e63d4 100644 --- a/src/ServiceControl.Persistence/Infrastructure/QueryStatsInfo.cs +++ b/src/ServiceControl.Persistence/Infrastructure/QueryStatsInfo.cs @@ -5,14 +5,17 @@ public struct QueryStatsInfo public readonly string ETag; public readonly int TotalCount; public readonly int HighestTotalCountOfAllTheInstances; + public readonly bool IsStale; - public QueryStatsInfo(string eTag, int totalCount, int? highestTotalCountOfAllTheInstances = null) + public QueryStatsInfo(string eTag, int totalCount, bool isStale, int? highestTotalCountOfAllTheInstances = null) { ETag = eTag; TotalCount = totalCount; + IsStale = isStale; + HighestTotalCountOfAllTheInstances = highestTotalCountOfAllTheInstances ?? totalCount; } - public static readonly QueryStatsInfo Zero = new QueryStatsInfo(string.Empty, 0); + public static readonly QueryStatsInfo Zero = new QueryStatsInfo(string.Empty, 0, false); } } \ No newline at end of file diff --git a/src/ServiceControl.Persistence/Infrastructure/SortInfo.cs b/src/ServiceControl.Persistence/Infrastructure/SortInfo.cs index d4e6d8a128..b2ef7e6287 100644 --- a/src/ServiceControl.Persistence/Infrastructure/SortInfo.cs +++ b/src/ServiceControl.Persistence/Infrastructure/SortInfo.cs @@ -1,8 +1,5 @@ namespace ServiceControl.Persistence.Infrastructure { - using System.Collections.Generic; - using System.Net.Http; - public class SortInfo { public string Direction { get; } @@ -14,37 +11,4 @@ public SortInfo(string sort, string direction) Direction = direction; } } - - public static class SortInfoExtension - { - public static SortInfo GetSortInfo(this HttpRequestMessage request, string defaultSortDirection = "desc") - { - var direction = request.GetQueryStringValue("direction", defaultSortDirection); - if (direction != "asc" && direction != "desc") - { - direction = defaultSortDirection; - } - - var sort = request.GetQueryStringValue("sort", "time_sent"); - if (!AllowableSortOptions.Contains(sort)) - { - sort = "time_sent"; - } - - return new SortInfo(sort, direction); - } - - static HashSet AllowableSortOptions = new HashSet - { - "processed_at", - "id", - "message_type", - "time_sent", - "critical_time", - "delivery_time", - "processing_time", - "status", - "message_id" - }; - } } \ No newline at end of file diff --git a/src/ServiceControl.Persistence/InternalCustomChecksServiceCollectionExtensions.cs b/src/ServiceControl.Persistence/InternalCustomChecksServiceCollectionExtensions.cs new file mode 100644 index 0000000000..4fdfa0243f --- /dev/null +++ b/src/ServiceControl.Persistence/InternalCustomChecksServiceCollectionExtensions.cs @@ -0,0 +1,15 @@ +namespace ServiceControl.CustomChecks +{ + using Microsoft.Extensions.DependencyInjection; + using NServiceBus.CustomChecks; + + public static class InternalCustomChecksServiceCollectionExtensions + { + public static void AddCustomCheck(this IServiceCollection serviceCollection) + where T : class, ICustomCheck + { + serviceCollection.AddTransient(); // Allows for T to have different instance registered for testing + serviceCollection.AddTransient(b => b.GetService()); + } + } +} \ No newline at end of file diff --git a/src/ServiceControl.Persistence/MessageRedirects/IMessageRedirectsDataStore.cs b/src/ServiceControl.Persistence/MessageRedirects/IMessageRedirectsDataStore.cs new file mode 100644 index 0000000000..03e51dcb76 --- /dev/null +++ b/src/ServiceControl.Persistence/MessageRedirects/IMessageRedirectsDataStore.cs @@ -0,0 +1,10 @@ +namespace ServiceControl.Persistence.MessageRedirects +{ + using System.Threading.Tasks; + + public interface IMessageRedirectsDataStore + { + Task GetOrCreate(); + Task Save(MessageRedirectsCollection redirects); + } +} diff --git a/src/ServiceControl/MessageRedirects/MessageRedirect.cs b/src/ServiceControl.Persistence/MessageRedirects/MessageRedirect.cs similarity index 83% rename from src/ServiceControl/MessageRedirects/MessageRedirect.cs rename to src/ServiceControl.Persistence/MessageRedirects/MessageRedirect.cs index df378392ed..c06ceaf7c1 100644 --- a/src/ServiceControl/MessageRedirects/MessageRedirect.cs +++ b/src/ServiceControl.Persistence/MessageRedirects/MessageRedirect.cs @@ -1,10 +1,10 @@ -namespace ServiceControl.MessageRedirects +namespace ServiceControl.Persistence.MessageRedirects { using System; using System.Collections.Concurrent; using Infrastructure; - class MessageRedirect + public class MessageRedirect { public Guid MessageRedirectId => idCache.GetOrAdd(FromPhysicalAddress, DeterministicGuid.MakeId); diff --git a/src/ServiceControl.Persistence/MessageRedirects/MessageRedirectsCollection.cs b/src/ServiceControl.Persistence/MessageRedirects/MessageRedirectsCollection.cs new file mode 100644 index 0000000000..6d37492071 --- /dev/null +++ b/src/ServiceControl.Persistence/MessageRedirects/MessageRedirectsCollection.cs @@ -0,0 +1,21 @@ +namespace ServiceControl.Persistence.MessageRedirects +{ + using System; + using System.Collections.Generic; + using System.Linq; + + [System.Diagnostics.CodeAnalysis.SuppressMessage("Naming", "CA1711:Identifiers should not have incorrect suffix", Justification = "Legacy stored data")] + public class MessageRedirectsCollection + { + public string ETag { get; set; } + + public DateTime LastModified { get; set; } + + public MessageRedirect this[string from] => Redirects.SingleOrDefault(r => r.FromPhysicalAddress == from); + public MessageRedirect this[Guid id] => Redirects.SingleOrDefault(r => r.MessageRedirectId == id); + + public List Redirects { get; set; } = new List(); + + public const string DefaultId = "messageredirects"; + } +} diff --git a/src/ServiceControl/CompositeViews/Messages/MessagesView.cs b/src/ServiceControl.Persistence/MessagesView.cs similarity index 100% rename from src/ServiceControl/CompositeViews/Messages/MessagesView.cs rename to src/ServiceControl.Persistence/MessagesView.cs diff --git a/src/ServiceControl/Notifications/NotificationsSettings.cs b/src/ServiceControl.Persistence/NotificationsSettings.cs similarity index 100% rename from src/ServiceControl/Notifications/NotificationsSettings.cs rename to src/ServiceControl.Persistence/NotificationsSettings.cs diff --git a/src/ServiceControl.Persistence/PersistenceManifestLibrary.cs b/src/ServiceControl.Persistence/PersistenceManifestLibrary.cs new file mode 100644 index 0000000000..07e322718f --- /dev/null +++ b/src/ServiceControl.Persistence/PersistenceManifestLibrary.cs @@ -0,0 +1,102 @@ +namespace ServiceControl.Persistence +{ + using System; + using System.Collections.Generic; + using System.IO; + using System.Linq; + using System.Text.Json; + using NServiceBus.Logging; + + public class PersistenceManifest + { + public string Version { get; set; } + public string Name { get; set; } + public string DisplayName { get; set; } + public string Description { get; set; } + public string TypeName { get; set; } + + internal bool IsMatch(string persistenceType) => + string.Compare(TypeName, persistenceType, true) == 0 + || string.Compare(Name, persistenceType, true) == 0; + } + + public static class PersistenceManifestLibrary + { + public static List PersistenceManifests { get; set; } + + static bool initialized; + static void Initialize() + { + if (PersistenceManifests == null) + { + PersistenceManifests = new List(); + } + + if (!initialized) + { + initialized = true; + var assemblyLocation = GetAssemblyDirectory(); + try + { + PersistenceManifests.AddRange( + Directory.EnumerateFiles(assemblyLocation, "persistence.manifest", SearchOption.AllDirectories) + .Select(manifest => JsonSerializer.Deserialize(File.ReadAllText(manifest))) + ); + + PersistenceManifests.ForEach(m => logger.Info($"Found persistence manifest for {m.DisplayName}")); + } + catch (Exception ex) + { + logger.Warn($"Failed to load persistence manifests from {assemblyLocation}", ex); + } + } + } + + static string GetAssemblyDirectory() + { + var assemblyLocation = typeof(PersistenceManifestLibrary).Assembly.Location; + return Path.GetDirectoryName(assemblyLocation); + } + + public static string Find(string persistenceType) + { + if (persistenceType == null) + { + throw new Exception("No persistenceType has been configured. Either provide a Type or Name in the PersistenceType setting."); + } + + Initialize(); + + var persistenceManifestDefinition = PersistenceManifests.FirstOrDefault(w => w.IsMatch(persistenceType)); + + if (persistenceManifestDefinition != null) + { + return persistenceManifestDefinition.TypeName; + } + + return persistenceType; + } + + public static string GetPersistenceFolder(string persistenceType) + { + if (persistenceType == null) + { + throw new Exception("No persistenceType has been configured. Either provide a Type or Name in the PersistenceType setting."); + } + + Initialize(); + + var persistenceManifestDefinition = PersistenceManifests.FirstOrDefault(w => w.IsMatch(persistenceType)); + + if (persistenceManifestDefinition != null) + { + return persistenceManifestDefinition.Name.Split('.').FirstOrDefault(); + } + + return null; + } + + static ILog logger = LogManager.GetLogger(typeof(PersistenceManifestLibrary)); + } +} + diff --git a/src/ServiceControl.Persistence/PersistenceSettings.cs b/src/ServiceControl.Persistence/PersistenceSettings.cs new file mode 100644 index 0000000000..f037c991e3 --- /dev/null +++ b/src/ServiceControl.Persistence/PersistenceSettings.cs @@ -0,0 +1,31 @@ +namespace ServiceControl.Persistence +{ + using System; + using System.Collections.Generic; + + public class PersistenceSettings + { + public PersistenceSettings( + TimeSpan errorRetentionPeriod, + TimeSpan eventsRetentionPeriod, + TimeSpan? auditRetentionPeriod, + int externalIntegrationsDispatchingBatchSize, + bool maintenanceMode + ) + { + ErrorRetentionPeriod = errorRetentionPeriod; + EventsRetentionPeriod = eventsRetentionPeriod; + AuditRetentionPeriod = auditRetentionPeriod; + MaintenanceMode = maintenanceMode; + ExternalIntegrationsDispatchingBatchSize = externalIntegrationsDispatchingBatchSize; + } + + public IDictionary PersisterSpecificSettings { get; } = new Dictionary(); + + public bool MaintenanceMode { get; } + public TimeSpan ErrorRetentionPeriod { get; } + public TimeSpan EventsRetentionPeriod { get; } + public TimeSpan? AuditRetentionPeriod { get; } + public int ExternalIntegrationsDispatchingBatchSize { get; } + } +} \ No newline at end of file diff --git a/src/ServiceControl/MessageFailures/QueueAddress.cs b/src/ServiceControl.Persistence/QueueAddress.cs similarity index 100% rename from src/ServiceControl/MessageFailures/QueueAddress.cs rename to src/ServiceControl.Persistence/QueueAddress.cs diff --git a/src/ServiceControl/Recoverability/Archiving/ArchiveState.cs b/src/ServiceControl.Persistence/Recoverability/Archiving/ArchiveState.cs similarity index 100% rename from src/ServiceControl/Recoverability/Archiving/ArchiveState.cs rename to src/ServiceControl.Persistence/Recoverability/Archiving/ArchiveState.cs diff --git a/src/ServiceControl.Persistence/Recoverability/Archiving/IArchiveMessages.cs b/src/ServiceControl.Persistence/Recoverability/Archiving/IArchiveMessages.cs new file mode 100644 index 0000000000..a95f03faf1 --- /dev/null +++ b/src/ServiceControl.Persistence/Recoverability/Archiving/IArchiveMessages.cs @@ -0,0 +1,25 @@ +namespace ServiceControl.Persistence.Recoverability +{ + using System.Collections.Generic; + using System.Threading.Tasks; + using ServiceControl.Recoverability; + + /// + /// Implementers of this interface are expected to emit domain events as well + /// + public interface IArchiveMessages + { + Task ArchiveAllInGroup(string groupId); + Task UnarchiveAllInGroup(string groupId); + + bool IsOperationInProgressFor(string groupId, ArchiveType archiveType); + + bool IsArchiveInProgressFor(string groupId); + void DismissArchiveOperation(string groupId, ArchiveType archiveType); + + Task StartArchiving(string groupId, ArchiveType archiveType); + Task StartUnarchiving(string groupId, ArchiveType archiveType); + + IEnumerable GetArchivalOperations(); + } +} diff --git a/src/ServiceControl/Recoverability/Archiving/InMemoryArchive.cs b/src/ServiceControl.Persistence/Recoverability/Archiving/InMemoryArchive.cs similarity index 80% rename from src/ServiceControl/Recoverability/Archiving/InMemoryArchive.cs rename to src/ServiceControl.Persistence/Recoverability/Archiving/InMemoryArchive.cs index 7e7aed2305..0a20be335a 100644 --- a/src/ServiceControl/Recoverability/Archiving/InMemoryArchive.cs +++ b/src/ServiceControl.Persistence/Recoverability/Archiving/InMemoryArchive.cs @@ -4,7 +4,7 @@ using System.Threading.Tasks; using Infrastructure.DomainEvents; - class InMemoryArchive // in memory + public class InMemoryArchive // in memory { public InMemoryArchive(string requestId, ArchiveType archiveType, IDomainEvents domainEvents) { @@ -40,7 +40,7 @@ public ArchiveProgress GetProgress() return new ArchiveProgress(roundedPercentage, TotalNumberOfMessages, NumberOfMessagesArchived, remaining); } - internal Task Start() + public Task Start() { ArchiveState = ArchiveState.ArchiveStarted; CompletionTime = null; @@ -54,7 +54,7 @@ internal Task Start() }); } - internal Task BatchArchived(int numberOfMessagesArchivedInBatch) + public Task BatchArchived(int numberOfMessagesArchivedInBatch) { ArchiveState = ArchiveState.ArchiveProgressing; NumberOfMessagesArchived += numberOfMessagesArchivedInBatch; @@ -71,7 +71,7 @@ internal Task BatchArchived(int numberOfMessagesArchivedInBatch) }); } - internal Task FinalizeArchive() + public Task FinalizeArchive() { ArchiveState = ArchiveState.ArchiveFinalizing; NumberOfMessagesArchived = TotalNumberOfMessages; @@ -87,7 +87,7 @@ internal Task FinalizeArchive() }); } - internal Task Complete() + public Task Complete() { ArchiveState = ArchiveState.ArchiveCompleted; NumberOfMessagesArchived = TotalNumberOfMessages; @@ -106,23 +106,7 @@ internal Task Complete() }); } - internal ArchiveOperation ToArchiveOperation() - { - return new ArchiveOperation - { - ArchiveType = ArchiveType, - GroupName = GroupName, - Id = ArchiveOperation.MakeId(RequestId, ArchiveType), - NumberOfMessagesArchived = NumberOfMessagesArchived, - RequestId = RequestId, - Started = Started, - TotalNumberOfMessages = TotalNumberOfMessages, - NumberOfBatches = NumberOfBatches, - CurrentBatch = CurrentBatch - }; - } - - internal bool NeedsAcknowledgement() + public bool NeedsAcknowledgement() { return ArchiveState == ArchiveState.ArchiveCompleted; } diff --git a/src/ServiceControl/Recoverability/Archiving/InMemoryUnarchive.cs b/src/ServiceControl.Persistence/Recoverability/Archiving/InMemoryUnarchive.cs similarity index 81% rename from src/ServiceControl/Recoverability/Archiving/InMemoryUnarchive.cs rename to src/ServiceControl.Persistence/Recoverability/Archiving/InMemoryUnarchive.cs index f5a69317fb..7d11c13030 100644 --- a/src/ServiceControl/Recoverability/Archiving/InMemoryUnarchive.cs +++ b/src/ServiceControl.Persistence/Recoverability/Archiving/InMemoryUnarchive.cs @@ -4,7 +4,7 @@ using System.Threading.Tasks; using Infrastructure.DomainEvents; - class InMemoryUnarchive // in memory + public class InMemoryUnarchive // in memory { public InMemoryUnarchive(string requestId, ArchiveType archiveType, IDomainEvents domainEvents) { @@ -40,7 +40,7 @@ public UnarchiveProgress GetProgress() return new UnarchiveProgress(roundedPercentage, TotalNumberOfMessages, NumberOfMessagesUnarchived, remaining); } - internal Task Start() + public Task Start() { ArchiveState = ArchiveState.ArchiveStarted; CompletionTime = null; @@ -54,7 +54,7 @@ internal Task Start() }); } - internal Task BatchUnarchived(int numberOfMessagesUnarchivedInBatch) + public Task BatchUnarchived(int numberOfMessagesUnarchivedInBatch) { ArchiveState = ArchiveState.ArchiveProgressing; NumberOfMessagesUnarchived += numberOfMessagesUnarchivedInBatch; @@ -71,7 +71,7 @@ internal Task BatchUnarchived(int numberOfMessagesUnarchivedInBatch) }); } - internal Task FinalizeUnarchive() + public Task FinalizeUnarchive() { ArchiveState = ArchiveState.ArchiveFinalizing; NumberOfMessagesUnarchived = TotalNumberOfMessages; @@ -87,7 +87,7 @@ internal Task FinalizeUnarchive() }); } - internal Task Complete() + public Task Complete() { ArchiveState = ArchiveState.ArchiveCompleted; NumberOfMessagesUnarchived = TotalNumberOfMessages; @@ -106,22 +106,6 @@ internal Task Complete() }); } - internal UnarchiveOperation ToUnarchiveOperation() - { - return new UnarchiveOperation - { - ArchiveType = ArchiveType, - GroupName = GroupName, - Id = UnarchiveOperation.MakeId(RequestId, ArchiveType), - NumberOfMessagesUnarchived = NumberOfMessagesUnarchived, - RequestId = RequestId, - Started = Started, - TotalNumberOfMessages = TotalNumberOfMessages, - NumberOfBatches = NumberOfBatches, - CurrentBatch = CurrentBatch - }; - } - internal bool NeedsAcknowledgement() { return ArchiveState == ArchiveState.ArchiveCompleted; diff --git a/src/ServiceControl/Recoverability/Archiving/OperationsManager.cs b/src/ServiceControl.Persistence/Recoverability/Archiving/OperationsManager.cs similarity index 96% rename from src/ServiceControl/Recoverability/Archiving/OperationsManager.cs rename to src/ServiceControl.Persistence/Recoverability/Archiving/OperationsManager.cs index b867d8c77a..2badfacad6 100644 --- a/src/ServiceControl/Recoverability/Archiving/OperationsManager.cs +++ b/src/ServiceControl.Persistence/Recoverability/Archiving/OperationsManager.cs @@ -2,7 +2,7 @@ namespace ServiceControl.Recoverability { using System.Collections.Generic; - class OperationsManager + public class OperationsManager { public bool IsOperationInProgressFor(string requestId, ArchiveType archiveType) { diff --git a/src/ServiceControl/Recoverability/Grouping/Groupers/ClassifiableMessageDetails.cs b/src/ServiceControl.Persistence/Recoverability/ClassifiableMessageDetails.cs similarity index 100% rename from src/ServiceControl/Recoverability/Grouping/Groupers/ClassifiableMessageDetails.cs rename to src/ServiceControl.Persistence/Recoverability/ClassifiableMessageDetails.cs diff --git a/src/ServiceControl/Recoverability/Grouping/Groupers/IFailureClassifier.cs b/src/ServiceControl.Persistence/Recoverability/IFailureClassifier.cs similarity index 100% rename from src/ServiceControl/Recoverability/Grouping/Groupers/IFailureClassifier.cs rename to src/ServiceControl.Persistence/Recoverability/IFailureClassifier.cs diff --git a/src/ServiceControl.Persistence/Recoverability/IReclassifyFailedMessages.cs b/src/ServiceControl.Persistence/Recoverability/IReclassifyFailedMessages.cs new file mode 100644 index 0000000000..7366b4837d --- /dev/null +++ b/src/ServiceControl.Persistence/Recoverability/IReclassifyFailedMessages.cs @@ -0,0 +1,14 @@ +namespace ServiceControl.Persistence +{ + using System.Threading.Tasks; + + /// + /// Only RavenDB 3.5 should need a working implementation of this interface because reclassification is only valid + /// for older data stored in previous versions of ServiceControl. Newer instances that start with other persistence + /// should never need to do this, and can be implemented as a no-op. + /// + public interface IReclassifyFailedMessages + { + Task ReclassifyFailedMessages(bool force); + } +} diff --git a/src/ServiceControl/Recoverability/OperationProgressCalculator.cs b/src/ServiceControl.Persistence/Recoverability/OperationProgressCalculator.cs similarity index 100% rename from src/ServiceControl/Recoverability/OperationProgressCalculator.cs rename to src/ServiceControl.Persistence/Recoverability/OperationProgressCalculator.cs diff --git a/src/ServiceControl/Recoverability/Grouping/Raven/ReclassifyErrorSettings.cs b/src/ServiceControl.Persistence/Recoverability/ReclassifyErrorSettings.cs similarity index 100% rename from src/ServiceControl/Recoverability/Grouping/Raven/ReclassifyErrorSettings.cs rename to src/ServiceControl.Persistence/Recoverability/ReclassifyErrorSettings.cs diff --git a/src/ServiceControl/Recoverability/Retrying/RetryState.cs b/src/ServiceControl.Persistence/Recoverability/RetryState.cs similarity index 100% rename from src/ServiceControl/Recoverability/Retrying/RetryState.cs rename to src/ServiceControl.Persistence/Recoverability/RetryState.cs diff --git a/src/ServiceControl.Persistence.RavenDb/RetryBatchGroup.cs b/src/ServiceControl.Persistence/RetryBatchGroup.cs similarity index 65% rename from src/ServiceControl.Persistence.RavenDb/RetryBatchGroup.cs rename to src/ServiceControl.Persistence/RetryBatchGroup.cs index 857ef41524..8504bbb7d9 100644 --- a/src/ServiceControl.Persistence.RavenDb/RetryBatchGroup.cs +++ b/src/ServiceControl.Persistence/RetryBatchGroup.cs @@ -1,13 +1,13 @@ namespace ServiceControl.Persistence { using System; - using Raven.Imports.Newtonsoft.Json; public class RetryBatchGroup { public string RequestId { get; set; } - [JsonProperty(NullValueHandling = NullValueHandling.Ignore)] //default to RetryType.Unknown for backwards compatability + // [Raven.Imports.Newtonsoft.Json.JsonProperty(NullValueHandling = NullValueHandling.Ignore)] //default to RetryType.Unknown for backwards compatability + // TODO: Need to fix ethe JsonProperty, maybe RavenDB has a method to specify metatdata or use a mapper/transformation public RetryType RetryType { get; set; } public bool HasStagingBatches { get; set; } diff --git a/src/ServiceControl/Recoverability/Retrying/History/RetryHistory.cs b/src/ServiceControl.Persistence/RetryHistory.cs similarity index 100% rename from src/ServiceControl/Recoverability/Retrying/History/RetryHistory.cs rename to src/ServiceControl.Persistence/RetryHistory.cs diff --git a/src/ServiceControl.Persistence/SagaAudit/ISagaAuditDataStore.cs b/src/ServiceControl.Persistence/SagaAudit/ISagaAuditDataStore.cs new file mode 100644 index 0000000000..7756340b3d --- /dev/null +++ b/src/ServiceControl.Persistence/SagaAudit/ISagaAuditDataStore.cs @@ -0,0 +1,13 @@ +namespace ServiceControl.Persistence +{ + using System; + using System.Threading.Tasks; + using ServiceControl.Persistence.Infrastructure; + using ServiceControl.SagaAudit; + + public interface ISagaAuditDataStore + { + Task StoreSnapshot(SagaSnapshot sagaSnapshot); + Task> GetSagaById(Guid sagaId); + } +} \ No newline at end of file diff --git a/src/ServiceControl.Persistence/ServiceControl.Persistence.csproj b/src/ServiceControl.Persistence/ServiceControl.Persistence.csproj index b2e7bfe8be..fd6857877f 100644 --- a/src/ServiceControl.Persistence/ServiceControl.Persistence.csproj +++ b/src/ServiceControl.Persistence/ServiceControl.Persistence.csproj @@ -2,8 +2,8 @@ net472 - - + + @@ -11,9 +11,11 @@ - - + + + - + + \ No newline at end of file diff --git a/src/ServiceControl.Persistence/UnitOfWork/FallbackIngestionUnitOfWorkFactory.cs b/src/ServiceControl.Persistence/UnitOfWork/FallbackIngestionUnitOfWorkFactory.cs index 636d985e35..a0c6c30bd4 100644 --- a/src/ServiceControl.Persistence/UnitOfWork/FallbackIngestionUnitOfWorkFactory.cs +++ b/src/ServiceControl.Persistence/UnitOfWork/FallbackIngestionUnitOfWorkFactory.cs @@ -15,8 +15,8 @@ public FallbackIngestionUnitOfWorkFactory(IIngestionUnitOfWorkFactory primary, I public async ValueTask StartNew() { - var primaryUnitOfWork = await primary.StartNew().ConfigureAwait(false); - var secondaryUnitOfWork = await secondary.StartNew().ConfigureAwait(false); + var primaryUnitOfWork = await primary.StartNew(); + var secondaryUnitOfWork = await secondary.StartNew(); return new FallbackIngestionUnitOfWork( primaryUnitOfWork, diff --git a/src/ServiceControl.PersistenceTests/FakeDomainEvents.cs b/src/ServiceControl.PersistenceTests/FakeDomainEvents.cs deleted file mode 100644 index 88c6635dd7..0000000000 --- a/src/ServiceControl.PersistenceTests/FakeDomainEvents.cs +++ /dev/null @@ -1,17 +0,0 @@ -namespace ServiceControl.Persistence.Tests -{ - using System.Collections.Generic; - using System.Threading.Tasks; - using ServiceControl.Infrastructure.DomainEvents; - - class FakeDomainEvents : IDomainEvents - { - public List RaisedEvents { get; } = new List(); - - public Task Raise(T domainEvent) where T : IDomainEvent - { - RaisedEvents.Add(domainEvent); - return Task.FromResult(0); - } - } -} \ No newline at end of file diff --git a/src/ServiceControl.PersistenceTests/MagicStringTypeTests.cs b/src/ServiceControl.PersistenceTests/MagicStringTypeTests.cs deleted file mode 100644 index c43043c3e4..0000000000 --- a/src/ServiceControl.PersistenceTests/MagicStringTypeTests.cs +++ /dev/null @@ -1,45 +0,0 @@ -namespace ServiceControl.Persistence.Tests -{ - using System.Threading.Tasks; - using NUnit.Framework; - using ServiceBus.Management.Infrastructure.Settings; - using ServiceControl.Persistence.InMemory; - using ServiceControl.Persistence.RavenDb; - using ServiceControl.Persistence.SqlServer; - - class MagicStringTypeTests - { - [Test] - public Task Verify_inmemory_persistence_type_string() - { - var typeNamespace = DataStoreConfig.InMemoryPersistenceTypeFullyQualifiedName.Split(',')[1].Trim(); - var typeFullName = DataStoreConfig.InMemoryPersistenceTypeFullyQualifiedName.Split(',')[0].Trim(); - var type = typeof(InMemoryPersistenceConfiguration); - Assert.AreEqual(type.Namespace, typeNamespace); - Assert.AreEqual(type.FullName, typeFullName); - return Task.CompletedTask; - } - - [Test] - public Task Verify_sqlserver_persistence_type_string() - { - var typeNamespace = DataStoreConfig.SqlServerPersistenceTypeFullyQualifiedName.Split(',')[1].Trim(); - var typeFullName = DataStoreConfig.SqlServerPersistenceTypeFullyQualifiedName.Split(',')[0].Trim(); - var type = typeof(SqlDbPersistenceConfiguration); - Assert.AreEqual(type.Namespace, typeNamespace); - Assert.AreEqual(type.FullName, typeFullName); - return Task.CompletedTask; - } - - [Test] - public Task Verify_ravendb_persistence_type_string() - { - var typeNamespace = DataStoreConfig.RavenDB35PersistenceTypeFullyQualifiedName.Split(',')[1].Trim(); - var typeFullName = DataStoreConfig.RavenDB35PersistenceTypeFullyQualifiedName.Split(',')[0].Trim(); - var type = typeof(RavenDbPersistenceConfiguration); - Assert.AreEqual(type.Namespace, typeNamespace); - Assert.AreEqual(type.FullName, typeFullName); - return Task.CompletedTask; - } - } -} diff --git a/src/ServiceControl.PersistenceTests/PersistenceDataStoreFixture.cs b/src/ServiceControl.PersistenceTests/PersistenceDataStoreFixture.cs deleted file mode 100644 index a6910907c0..0000000000 --- a/src/ServiceControl.PersistenceTests/PersistenceDataStoreFixture.cs +++ /dev/null @@ -1,19 +0,0 @@ -namespace ServiceControl.Persistence.Tests -{ - using System.Threading.Tasks; - using Persistence; - using ServiceControl.Persistence.UnitOfWork; - - abstract class PersistenceDataStoreFixture - { - public ICustomChecksDataStore CustomCheckDataStore { get; protected set; } - public IMonitoringDataStore MonitoringDataStore { get; protected set; } - public IIngestionUnitOfWorkFactory UnitOfWorkFactory { get; protected set; } - - public abstract Task SetupDataStore(); - - public virtual Task CompleteDBOperation() => Task.CompletedTask; - - public virtual Task CleanupDB() => Task.CompletedTask; - } -} \ No newline at end of file diff --git a/src/ServiceControl.PersistenceTests/PersistenceTestCollection.cs b/src/ServiceControl.PersistenceTests/PersistenceTestCollection.cs deleted file mode 100644 index 5d5b35ebaf..0000000000 --- a/src/ServiceControl.PersistenceTests/PersistenceTestCollection.cs +++ /dev/null @@ -1,22 +0,0 @@ -namespace ServiceControl.Persistence.Tests -{ - using System.Collections; - using PersistenceTests; - using ServiceBus.Management.Infrastructure.Settings; - - public class PersistenceTestCollection : IEnumerable - { - public IEnumerator GetEnumerator() - { - yield return new InMemory(); - yield return new RavenDb(); - - var sqlConnectionString = SettingsReader.Read("ServiceControl", "SqlStorageConnectionString", ""); - - if (!string.IsNullOrEmpty(sqlConnectionString)) - { - yield return new SqlDb(sqlConnectionString); - } - } - } -} \ No newline at end of file diff --git a/src/ServiceControl.PersistenceTests/Persistences/InMemory.cs b/src/ServiceControl.PersistenceTests/Persistences/InMemory.cs deleted file mode 100644 index 41dd606d6d..0000000000 --- a/src/ServiceControl.PersistenceTests/Persistences/InMemory.cs +++ /dev/null @@ -1,34 +0,0 @@ -namespace ServiceControl.PersistenceTests -{ - using System.Threading.Tasks; - using Microsoft.Extensions.DependencyInjection; - using Persistence; - using Persistence.Tests; - using Raven.Client.Embedded; - using ServiceBus.Management.Infrastructure.Settings; - using ServiceControl.Persistence.UnitOfWork; - - class InMemory : PersistenceDataStoreFixture - { - public override async Task SetupDataStore() - { - var serviceCollection = new ServiceCollection(); - fallback = await serviceCollection.AddInitializedDocumentStore().ConfigureAwait(false); - serviceCollection.AddServiceControlPersistence(DataStoreType.InMemory); - var serviceProvider = serviceCollection.BuildServiceProvider(); - MonitoringDataStore = serviceProvider.GetRequiredService(); - CustomCheckDataStore = serviceProvider.GetRequiredService(); - UnitOfWorkFactory = serviceProvider.GetRequiredService(); - } - - public override Task CleanupDB() - { - fallback?.Dispose(); - return base.CleanupDB(); - } - - public override string ToString() => "In-Memory"; - - EmbeddableDocumentStore fallback; - } -} \ No newline at end of file diff --git a/src/ServiceControl.PersistenceTests/Persistences/RavenDb.cs b/src/ServiceControl.PersistenceTests/Persistences/RavenDb.cs deleted file mode 100644 index 2499d1d4da..0000000000 --- a/src/ServiceControl.PersistenceTests/Persistences/RavenDb.cs +++ /dev/null @@ -1,41 +0,0 @@ -namespace ServiceControl.PersistenceTests -{ - using System.Threading.Tasks; - using Microsoft.Extensions.DependencyInjection; - using Persistence; - using Persistence.Tests; - using Raven.Client.Embedded; - using ServiceBus.Management.Infrastructure.Settings; - using ServiceControl.Persistence.UnitOfWork; - - class RavenDb : PersistenceDataStoreFixture - { - public override async Task SetupDataStore() - { - var serviceCollection = new ServiceCollection(); - - documentStore = await serviceCollection.AddInitializedDocumentStore().ConfigureAwait(false); - serviceCollection.AddServiceControlPersistence(DataStoreType.RavenDB35); - var serviceProvider = serviceCollection.BuildServiceProvider(); - MonitoringDataStore = serviceProvider.GetRequiredService(); - CustomCheckDataStore = serviceProvider.GetRequiredService(); - UnitOfWorkFactory = serviceProvider.GetRequiredService(); - } - - public override Task CompleteDBOperation() - { - documentStore.WaitForIndexing(); - return base.CompleteDBOperation(); - } - - public override Task CleanupDB() - { - documentStore?.Dispose(); - return base.CleanupDB(); - } - - public override string ToString() => "RavenDB35"; - - EmbeddableDocumentStore documentStore; - } -} \ No newline at end of file diff --git a/src/ServiceControl.PersistenceTests/Persistences/RavenDbExtensions.cs b/src/ServiceControl.PersistenceTests/Persistences/RavenDbExtensions.cs deleted file mode 100644 index 79042dec76..0000000000 --- a/src/ServiceControl.PersistenceTests/Persistences/RavenDbExtensions.cs +++ /dev/null @@ -1,42 +0,0 @@ -namespace ServiceControl.PersistenceTests -{ - using System.ComponentModel.Composition.Hosting; - using System.Linq; - using System.Reflection; - using System.Threading.Tasks; - using Infrastructure.RavenDB; - using Microsoft.Extensions.DependencyInjection; - using Raven.Client; - using Raven.Client.Embedded; - using Raven.Client.Indexes; - using ServiceBus.Management.Infrastructure.Settings; - using ServiceControl.Persistence; - - static class RavenDbExtensions - { - public static async Task AddInitializedDocumentStore(this IServiceCollection serviceCollection) - { - var settings = new Settings - { - RunInMemory = true - }; - var documentStore = new EmbeddableDocumentStore(); - RavenBootstrapper.Configure(documentStore, settings); - documentStore.Initialize(); - - ExportProvider CreateIndexProvider(System.Collections.Generic.List indexAssemblies) => - new CompositionContainer( - new AggregateCatalog( - from indexAssembly in indexAssemblies select new AssemblyCatalog(indexAssembly) - ) - ); - - var indexProvider = CreateIndexProvider(new System.Collections.Generic.List { typeof(RavenBootstrapper).Assembly, typeof(CustomChecksIndex).Assembly }); - await IndexCreation.CreateIndexesAsync(indexProvider, documentStore) - .ConfigureAwait(false); - - serviceCollection.AddSingleton(documentStore); - return documentStore; - } - } -} \ No newline at end of file diff --git a/src/ServiceControl.PersistenceTests/Persistences/SqlDb.cs b/src/ServiceControl.PersistenceTests/Persistences/SqlDb.cs deleted file mode 100644 index fa55d959ff..0000000000 --- a/src/ServiceControl.PersistenceTests/Persistences/SqlDb.cs +++ /dev/null @@ -1,58 +0,0 @@ -namespace ServiceControl.PersistenceTests -{ - using System.Configuration; - using System.Data.SqlClient; - using System.Threading.Tasks; - using Dapper; - using Microsoft.Extensions.DependencyInjection; - using Persistence; - using Persistence.Tests; - using Raven.Client.Embedded; - using ServiceBus.Management.Infrastructure.Settings; - using ServiceControl.Persistence.UnitOfWork; - - class SqlDb : PersistenceDataStoreFixture - { - public SqlDb(string sqlDbConnectionString) - { - this.sqlDbConnectionString = sqlDbConnectionString; - } - - public override async Task SetupDataStore() - { - await SetupSqlPersistence.SetupMonitoring(sqlDbConnectionString).ConfigureAwait(false); - await SetupSqlPersistence.SetupCustomChecks(sqlDbConnectionString).ConfigureAwait(false); - - ConfigurationManager.AppSettings.Set("ServiceControl/SqlStorageConnectionString", sqlDbConnectionString); - - var serviceCollection = new ServiceCollection(); - fallback = await serviceCollection.AddInitializedDocumentStore().ConfigureAwait(false); - serviceCollection.AddSingleton(new Settings() /*{ SqlStorageConnectionString = sqlDbConnectionString }*/); - serviceCollection.AddServiceControlPersistence(DataStoreType.SqlDb); - var serviceProvider = serviceCollection.BuildServiceProvider(); - MonitoringDataStore = serviceProvider.GetRequiredService(); - CustomCheckDataStore = serviceProvider.GetRequiredService(); - UnitOfWorkFactory = serviceProvider.GetRequiredService(); - } - - public override async Task CleanupDB() - { - using (var connection = new SqlConnection(sqlDbConnectionString)) - { - var dropConstraints = "EXEC sp_msforeachtable 'ALTER TABLE ? NOCHECK CONSTRAINT all'"; - var dropTables = "EXEC sp_msforeachtable 'DROP TABLE ?'"; - - await connection.OpenAsync().ConfigureAwait(false); - await connection.ExecuteAsync(dropConstraints).ConfigureAwait(false); - await connection.ExecuteAsync(dropTables).ConfigureAwait(false); - } - - fallback?.Dispose(); - } - - public override string ToString() => "Sql"; - - string sqlDbConnectionString; - EmbeddableDocumentStore fallback; - } -} \ No newline at end of file diff --git a/src/ServiceControl.PersistenceTests/TestsFilter.cs b/src/ServiceControl.PersistenceTests/TestsFilter.cs deleted file mode 100644 index cbb64ab1fc..0000000000 --- a/src/ServiceControl.PersistenceTests/TestsFilter.cs +++ /dev/null @@ -1 +0,0 @@ -[assembly: IncludeInDefaultTests()] \ No newline at end of file diff --git a/src/ServiceControl.Transports.ASB/.editorconfig b/src/ServiceControl.Transports.ASB/.editorconfig new file mode 100644 index 0000000000..ff993b49bb --- /dev/null +++ b/src/ServiceControl.Transports.ASB/.editorconfig @@ -0,0 +1,4 @@ +[*.cs] + +# Justification: ServiceControl app has no synchronization context +dotnet_diagnostic.CA2007.severity = none diff --git a/src/ServiceControl.Transports.ASB/QueueLengthProvider.cs b/src/ServiceControl.Transports.ASB/QueueLengthProvider.cs index 5636bb7ec9..fe8074344d 100644 --- a/src/ServiceControl.Transports.ASB/QueueLengthProvider.cs +++ b/src/ServiceControl.Transports.ASB/QueueLengthProvider.cs @@ -57,7 +57,7 @@ public Task Start() { Logger.DebugFormat("Querying namespace manager: {0}", namespaceManager.Address); - var queues = await namespaceManager.GetQueuesAsync().ConfigureAwait(false); + var queues = await namespaceManager.GetQueuesAsync(); var lookup = queues.ToLookup(x => x.Path, StringComparer.InvariantCultureIgnoreCase); Logger.DebugFormat("Retrieved details of {0} queues", lookup.Count); @@ -65,7 +65,7 @@ public Task Start() UpdateQueueLengthStore(lookup); Logger.Debug("Waiting for next interval"); - await Task.Delay(QueryDelayInterval, token).ConfigureAwait(false); + await Task.Delay(QueryDelayInterval, token); } catch (OperationCanceledException) { diff --git a/src/ServiceControl.Transports.ASBS/.editorconfig b/src/ServiceControl.Transports.ASBS/.editorconfig new file mode 100644 index 0000000000..ff993b49bb --- /dev/null +++ b/src/ServiceControl.Transports.ASBS/.editorconfig @@ -0,0 +1,4 @@ +[*.cs] + +# Justification: ServiceControl app has no synchronization context +dotnet_diagnostic.CA2007.severity = none diff --git a/src/ServiceControl.Transports.ASBS/DeadLetterQueueCheck.cs b/src/ServiceControl.Transports.ASBS/DeadLetterQueueCheck.cs index 7b6d773bec..2ac6910d8c 100644 --- a/src/ServiceControl.Transports.ASBS/DeadLetterQueueCheck.cs +++ b/src/ServiceControl.Transports.ASBS/DeadLetterQueueCheck.cs @@ -27,7 +27,7 @@ public override async Task PerformCheck() Logger.Debug("Checking Dead Letter Queue length"); var managementClient = new ServiceBusAdministrationClient(connectionString); - var queueRuntimeInfo = await managementClient.GetQueueRuntimePropertiesAsync(stagingQueue).ConfigureAwait(false); + var queueRuntimeInfo = await managementClient.GetQueueRuntimePropertiesAsync(stagingQueue); var deadLetterMessageCount = queueRuntimeInfo.Value.DeadLetterMessageCount; if (deadLetterMessageCount > 0) diff --git a/src/ServiceControl.Transports.ASBS/QueueLengthProvider.cs b/src/ServiceControl.Transports.ASBS/QueueLengthProvider.cs index ac5c79586b..483e4c5c97 100644 --- a/src/ServiceControl.Transports.ASBS/QueueLengthProvider.cs +++ b/src/ServiceControl.Transports.ASBS/QueueLengthProvider.cs @@ -50,11 +50,11 @@ public Task Start() try { Logger.Debug("Waiting for next interval"); - await Task.Delay(queryDelayInterval, token).ConfigureAwait(false); + await Task.Delay(queryDelayInterval, token); Logger.DebugFormat("Querying management client."); - var queueRuntimeInfos = await GetQueueList(token).ConfigureAwait(false); + var queueRuntimeInfos = await GetQueueList(token); Logger.DebugFormat("Retrieved details of {0} queues", queueRuntimeInfos.Count); @@ -82,7 +82,7 @@ async Task> GetQueueList(Can var enumerator = queuesRuntimeProperties.GetAsyncEnumerator(cancellationToken); try { - while (await enumerator.MoveNextAsync().ConfigureAwait(false)) + while (await enumerator.MoveNextAsync()) { var queueRuntimeProperties = enumerator.Current; queuePathToRuntimeInfo.Add(queueRuntimeProperties.Name, queueRuntimeProperties); @@ -90,7 +90,7 @@ async Task> GetQueueList(Can } finally { - await enumerator.DisposeAsync().ConfigureAwait(false); + await enumerator.DisposeAsync(); } return queuePathToRuntimeInfo; @@ -129,7 +129,7 @@ void UpdateQueueLength(KeyValuePair monitoredEndpoint, IReadOnly public async Task Stop() { stop.Cancel(); - await poller.ConfigureAwait(false); + await poller; } ConcurrentDictionary endpointQueueMappings = new ConcurrentDictionary(); diff --git a/src/ServiceControl.Transports.ASQ/.editorconfig b/src/ServiceControl.Transports.ASQ/.editorconfig new file mode 100644 index 0000000000..ff993b49bb --- /dev/null +++ b/src/ServiceControl.Transports.ASQ/.editorconfig @@ -0,0 +1,4 @@ +[*.cs] + +# Justification: ServiceControl app has no synchronization context +dotnet_diagnostic.CA2007.severity = none diff --git a/src/ServiceControl.Transports.ASQ/QueueLengthProvider.cs b/src/ServiceControl.Transports.ASQ/QueueLengthProvider.cs index 1030a3e407..e08d7e96bb 100644 --- a/src/ServiceControl.Transports.ASQ/QueueLengthProvider.cs +++ b/src/ServiceControl.Transports.ASQ/QueueLengthProvider.cs @@ -43,11 +43,11 @@ public Task Start() { try { - await FetchQueueSizes(token).ConfigureAwait(false); + await FetchQueueSizes(token); UpdateQueueLengthStore(); - await Task.Delay(QueryDelayInterval, token).ConfigureAwait(false); + await Task.Delay(QueryDelayInterval, token); } catch (OperationCanceledException) { @@ -78,7 +78,7 @@ async Task FetchLength(QueueLengthValue queueLength, CancellationToken cancellat { var queueReference = queueLength.QueueReference; - QueueProperties properties = await queueReference.GetPropertiesAsync(cancellationToken).ConfigureAwait(false); + QueueProperties properties = await queueReference.GetPropertiesAsync(cancellationToken); queueLength.Length = properties.ApproximateMessagesCount; diff --git a/src/ServiceControl.Transports.Learning/.editorconfig b/src/ServiceControl.Transports.Learning/.editorconfig new file mode 100644 index 0000000000..ff993b49bb --- /dev/null +++ b/src/ServiceControl.Transports.Learning/.editorconfig @@ -0,0 +1,4 @@ +[*.cs] + +# Justification: ServiceControl app has no synchronization context +dotnet_diagnostic.CA2007.severity = none diff --git a/src/ServiceControl.Transports.Learning/QueueLengthProvider.cs b/src/ServiceControl.Transports.Learning/QueueLengthProvider.cs index 9f83d3d957..a3d0b95799 100644 --- a/src/ServiceControl.Transports.Learning/QueueLengthProvider.cs +++ b/src/ServiceControl.Transports.Learning/QueueLengthProvider.cs @@ -34,7 +34,7 @@ public Task Start() var queueLengths = GetQueueLengths(); UpdateStore(queueLengths); - await Task.Delay(QueryDelayInterval, cancel.Token).ConfigureAwait(false); + await Task.Delay(QueryDelayInterval, cancel.Token); } catch (OperationCanceledException) { diff --git a/src/ServiceControl.Transports.Msmq/.editorconfig b/src/ServiceControl.Transports.Msmq/.editorconfig new file mode 100644 index 0000000000..ff993b49bb --- /dev/null +++ b/src/ServiceControl.Transports.Msmq/.editorconfig @@ -0,0 +1,4 @@ +[*.cs] + +# Justification: ServiceControl app has no synchronization context +dotnet_diagnostic.CA2007.severity = none diff --git a/src/ServiceControl.Transports.RabbitMQ/.editorconfig b/src/ServiceControl.Transports.RabbitMQ/.editorconfig new file mode 100644 index 0000000000..ff993b49bb --- /dev/null +++ b/src/ServiceControl.Transports.RabbitMQ/.editorconfig @@ -0,0 +1,4 @@ +[*.cs] + +# Justification: ServiceControl app has no synchronization context +dotnet_diagnostic.CA2007.severity = none diff --git a/src/ServiceControl.Transports.RabbitMQ/QueueLengthProvider.cs b/src/ServiceControl.Transports.RabbitMQ/QueueLengthProvider.cs index a1f866fef1..ce8c863522 100644 --- a/src/ServiceControl.Transports.RabbitMQ/QueueLengthProvider.cs +++ b/src/ServiceControl.Transports.RabbitMQ/QueueLengthProvider.cs @@ -41,11 +41,11 @@ public Task Start() { try { - await FetchQueueLengths(token).ConfigureAwait(false); + await FetchQueueLengths(token); UpdateQueueLengths(); - await Task.Delay(QueryDelayInterval, token).ConfigureAwait(false); + await Task.Delay(QueryDelayInterval, token); } catch (OperationCanceledException) { @@ -101,7 +101,7 @@ await queryExecutor.Execute(m => { Logger.Warn($"Error querying queue length for {queueName}", e); } - }, cancellationToken).ConfigureAwait(false); + }, cancellationToken); } } @@ -109,7 +109,7 @@ public async Task Stop() { stoppedTokenSource.Cancel(); - await poller.ConfigureAwait(false); + await poller; queryExecutor.Dispose(); } @@ -165,7 +165,7 @@ public async Task Execute(Action action, CancellationToken cancellationT //Connection implements reconnection logic while (connection.IsOpen == false) { - await Task.Delay(ReconnectionDelay, cancellationToken).ConfigureAwait(false); + await Task.Delay(ReconnectionDelay, cancellationToken); } if (model == null || model.IsClosed) diff --git a/src/ServiceControl.Transports.SQS.Tests/TransportTestsConfiguration.cs b/src/ServiceControl.Transports.SQS.Tests/TransportTestsConfiguration.cs index 60dd2fda59..d852c48f6b 100644 --- a/src/ServiceControl.Transports.SQS.Tests/TransportTestsConfiguration.cs +++ b/src/ServiceControl.Transports.SQS.Tests/TransportTestsConfiguration.cs @@ -18,7 +18,7 @@ public Task Configure() if (string.IsNullOrEmpty(ConnectionString)) { - throw new Exception($"Environment variable {ConnectionStringKey} is required for SQL transport tests to run"); + throw new Exception($"Environment variable {ConnectionStringKey} is required for SQS transport tests to run"); } return Task.CompletedTask; diff --git a/src/ServiceControl.Transports.SQS/.editorconfig b/src/ServiceControl.Transports.SQS/.editorconfig new file mode 100644 index 0000000000..ff993b49bb --- /dev/null +++ b/src/ServiceControl.Transports.SQS/.editorconfig @@ -0,0 +1,4 @@ +[*.cs] + +# Justification: ServiceControl app has no synchronization context +dotnet_diagnostic.CA2007.severity = none diff --git a/src/ServiceControl.Transports.SQS/QueueAttributesRequestCache.cs b/src/ServiceControl.Transports.SQS/QueueAttributesRequestCache.cs index 37bc6c2c20..74eb64d82a 100644 --- a/src/ServiceControl.Transports.SQS/QueueAttributesRequestCache.cs +++ b/src/ServiceControl.Transports.SQS/QueueAttributesRequestCache.cs @@ -21,7 +21,7 @@ public async Task GetQueueAttributesRequest(string qu return attReq; } - var queueUrl = await GetQueueUrl(queueName, cancellationToken).ConfigureAwait(false); + var queueUrl = await GetQueueUrl(queueName, cancellationToken); attReq = new GetQueueAttributesRequest { QueueUrl = queueUrl }; attReq.AttributeNames.Add("ApproximateNumberOfMessages"); @@ -33,8 +33,7 @@ public async Task GetQueueAttributesRequest(string qu async Task GetQueueUrl(string queueName, CancellationToken cancellationToken) { - var response = await sqsClient.GetQueueUrlAsync(queueName, cancellationToken) - .ConfigureAwait(false); + var response = await sqsClient.GetQueueUrlAsync(queueName, cancellationToken); return response.QueueUrl; } diff --git a/src/ServiceControl.Transports.SQS/QueueLengthProvider.cs b/src/ServiceControl.Transports.SQS/QueueLengthProvider.cs index 8aea2aa4b1..13f01dc7f6 100644 --- a/src/ServiceControl.Transports.SQS/QueueLengthProvider.cs +++ b/src/ServiceControl.Transports.SQS/QueueLengthProvider.cs @@ -66,11 +66,11 @@ public Task Start() { try { - await FetchQueueSizes(cache, client, token).ConfigureAwait(false); + await FetchQueueSizes(cache, client, token); UpdateQueueLengthStore(); - await Task.Delay(QueryDelayInterval, token).ConfigureAwait(false); + await Task.Delay(QueryDelayInterval, token); } catch (OperationCanceledException) { @@ -119,8 +119,8 @@ async Task FetchLength(string queue, IAmazonSQS client, QueueAttributesRequestCa { try { - var attReq = await cache.GetQueueAttributesRequest(queue, cancellationToken).ConfigureAwait(false); - var response = await client.GetQueueAttributesAsync(attReq, cancellationToken).ConfigureAwait(false); + var attReq = await cache.GetQueueAttributesRequest(queue, cancellationToken); + var response = await client.GetQueueAttributesAsync(attReq, cancellationToken); sizes[queue] = response.ApproximateNumberOfMessages; } catch (OperationCanceledException) diff --git a/src/ServiceControl.Transports.SqlServer/.editorconfig b/src/ServiceControl.Transports.SqlServer/.editorconfig new file mode 100644 index 0000000000..ff993b49bb --- /dev/null +++ b/src/ServiceControl.Transports.SqlServer/.editorconfig @@ -0,0 +1,4 @@ +[*.cs] + +# Justification: ServiceControl app has no synchronization context +dotnet_diagnostic.CA2007.severity = none diff --git a/src/ServiceControl.Transports.SqlServer/QueueLengthProvider.cs b/src/ServiceControl.Transports.SqlServer/QueueLengthProvider.cs index 4c06f194e1..8fb6a9e62b 100644 --- a/src/ServiceControl.Transports.SqlServer/QueueLengthProvider.cs +++ b/src/ServiceControl.Transports.SqlServer/QueueLengthProvider.cs @@ -50,9 +50,9 @@ public Task Start() { try { - await Task.Delay(QueryDelayInterval, token).ConfigureAwait(false); + await Task.Delay(QueryDelayInterval, token); - await QueryTableSizes(token).ConfigureAwait(false); + await QueryTableSizes(token); UpdateQueueLengthStore(); } @@ -110,11 +110,11 @@ async Task QueryTableSizes(CancellationToken cancellationToken) using (var connection = new SqlConnection(connectionString)) { - await connection.OpenAsync(cancellationToken).ConfigureAwait(false); + await connection.OpenAsync(cancellationToken); foreach (var chunk in chunks) { - await UpdateChunk(connection, chunk, cancellationToken).ConfigureAwait(false); + await UpdateChunk(connection, chunk, cancellationToken); } } } @@ -125,11 +125,11 @@ async Task UpdateChunk(SqlConnection connection, KeyValuePair[] c using (var command = new SqlCommand(query, connection)) { - using (var reader = await command.ExecuteReaderAsync(cancellationToken).ConfigureAwait(false)) + using (var reader = await command.ExecuteReaderAsync(cancellationToken)) { foreach (var chunkPair in chunk) { - await reader.ReadAsync(cancellationToken).ConfigureAwait(false); + await reader.ReadAsync(cancellationToken); var queueLength = reader.GetInt32(0); @@ -142,7 +142,7 @@ async Task UpdateChunk(SqlConnection connection, KeyValuePair[] c tableSizes.TryUpdate(chunkPair.Key, queueLength, chunkPair.Value); } - await reader.NextResultAsync(cancellationToken).ConfigureAwait(false); + await reader.NextResultAsync(cancellationToken); } } } diff --git a/src/ServiceControl.Transports.Tests/TransportTestFixture.cs b/src/ServiceControl.Transports.Tests/TransportTestFixture.cs index d7953f8a9a..bb1b901738 100644 --- a/src/ServiceControl.Transports.Tests/TransportTestFixture.cs +++ b/src/ServiceControl.Transports.Tests/TransportTestFixture.cs @@ -19,7 +19,7 @@ class TransportTestFixture { [OneTimeSetUp] - public void OneTimeSetup() + public static void OneTimeSetup() { Scenario.GetLoggerFactory = ctx => new StaticLoggerFactory(ctx); } diff --git a/src/ServiceControl.Transports/.editorconfig b/src/ServiceControl.Transports/.editorconfig new file mode 100644 index 0000000000..ff993b49bb --- /dev/null +++ b/src/ServiceControl.Transports/.editorconfig @@ -0,0 +1,4 @@ +[*.cs] + +# Justification: ServiceControl app has no synchronization context +dotnet_diagnostic.CA2007.severity = none diff --git a/src/ServiceControl.Transports/TransportCustomization.cs b/src/ServiceControl.Transports/TransportCustomization.cs index 7a091faa0e..fa36689377 100644 --- a/src/ServiceControl.Transports/TransportCustomization.cs +++ b/src/ServiceControl.Transports/TransportCustomization.cs @@ -61,7 +61,7 @@ public async Task InitializeDispatcher(string name, Transport CustomizeRawSendOnlyEndpoint(config, transportSettings); - return await RawEndpoint.Create(config).ConfigureAwait(false); + return await RawEndpoint.Create(config); } public async Task InitializeQueueIngestor( @@ -81,7 +81,7 @@ public async Task InitializeQueueIngestor( CustomizeForQueueIngestion(config, transportSettings); - var startableRaw = await RawEndpoint.Create(config).ConfigureAwait(false); + var startableRaw = await RawEndpoint.Create(config); return new QueueIngestor(startableRaw); } @@ -120,7 +120,7 @@ class QueueIngestor : IQueueIngestor public async Task Start() { - stoppableRaw = await startableRaw.Start().ConfigureAwait(false); + stoppableRaw = await startableRaw.Start(); } public Task Stop() diff --git a/src/ServiceControl.UnitTests/API/APIApprovals.cs b/src/ServiceControl.UnitTests/API/APIApprovals.cs index bb4af38000..72888d0e97 100644 --- a/src/ServiceControl.UnitTests/API/APIApprovals.cs +++ b/src/ServiceControl.UnitTests/API/APIApprovals.cs @@ -6,9 +6,11 @@ using System.Net.Http; using System.Reflection; using System.Text; + using System.Text.Json; using System.Web.Http.Controllers; using System.Web.Http.Hosting; using System.Web.Http.Routing; + using Newtonsoft.Json.Linq; using NServiceBus.CustomChecks; using NUnit.Framework; using Particular.Approvals; @@ -17,6 +19,7 @@ using PublicApiGenerator; using ServiceBus.Management.Infrastructure.Settings; using ServiceControl.Infrastructure.WebApi; + using ServiceControl.Persistence; using ServiceControlInstaller.Engine.Instances; [TestFixture] @@ -28,7 +31,8 @@ public void RootPathValue() var request = new HttpRequestMessage(HttpMethod.Get, "http://localhost"); request.Properties.Add(HttpPropertyKeys.RequestContextKey, new HttpRequestContext { VirtualPathRoot = "/" }); - var controller = new RootController(new ActiveLicense { IsValid = true }, new LoggingSettings("testEndpoint"), new Settings(), httpClientFactory: null) + var persistenceSettings = new PersistenceSettings(TimeSpan.Zero, TimeSpan.Zero, TimeSpan.Zero, 1, false); + var controller = new RootController(new ActiveLicense { IsValid = true }, new LoggingSettings("testEndpoint"), new Settings(), persistenceSettings, httpClientFactory: null) { Url = new UrlHelper(request) }; @@ -130,7 +134,7 @@ public void TransportNames() Approver.Verify(publicTransportNames); } - [Test] + [Test, Ignore("TODO: Deal with this once persister settings are properly managed")] // public void PlatformSampleSettings() { //HINT: Particular.PlatformSample includes a parameterized version of the ServiceControl.exe.config file. @@ -151,7 +155,7 @@ string RemoveDataStoreSettings(string json) var result = string.Empty; - var dataStoreSettings = new[] { nameof(Settings.DataStoreType) }; + var dataStoreSettings = new[] { nameof(Settings.PersistenceType) }; foreach (var settingLine in settingsLines) { diff --git a/src/ServiceControl.UnitTests/ApprovalFiles/APIApprovals.CustomCheckDetails.approved.txt b/src/ServiceControl.UnitTests/ApprovalFiles/APIApprovals.CustomCheckDetails.approved.txt index 8d1f80beb4..bcb0c0f49c 100644 --- a/src/ServiceControl.UnitTests/ApprovalFiles/APIApprovals.CustomCheckDetails.approved.txt +++ b/src/ServiceControl.UnitTests/ApprovalFiles/APIApprovals.CustomCheckDetails.approved.txt @@ -1,10 +1,4 @@ Health: ServiceControl Primary Instance Health: ServiceControl Remotes -ServiceControl Health: Audit Message Ingestion -ServiceControl Health: Error Database Index Errors -ServiceControl Health: Error Database Index Lag ServiceControl Health: Error Message Ingestion -ServiceControl Health: Error Message Ingestion Process -ServiceControl Health: Message Ingestion Process -ServiceControl Health: Saga Audit Data Retention -Storage space: ServiceControl database \ No newline at end of file +ServiceControl Health: Error Message Ingestion Process \ No newline at end of file diff --git a/src/ServiceControl.UnitTests/ApprovalFiles/APIApprovals.HttpApiRoutes.approved.txt b/src/ServiceControl.UnitTests/ApprovalFiles/APIApprovals.HttpApiRoutes.approved.txt index 88decfa8ea..ea1023f850 100644 --- a/src/ServiceControl.UnitTests/ApprovalFiles/APIApprovals.HttpApiRoutes.approved.txt +++ b/src/ServiceControl.UnitTests/ApprovalFiles/APIApprovals.HttpApiRoutes.approved.txt @@ -16,7 +16,7 @@ GET /endpoints/{endpoint}/messages/search => ServiceControl.CompositeViews.Messa GET /endpoints/{endpoint}/messages/search/{keyword} => ServiceControl.CompositeViews.Messages.GetMessagesController:SearchByKeyword(String endpoint, String keyword) DELETE /endpoints/{endpointId} => ServiceControl.Monitoring.EndpointsMonitoringController:DeleteEndpoint(Guid endpointId) PATCH /endpoints/{endpointId} => ServiceControl.Monitoring.EndpointsMonitoringController:Foo(Guid endpointId, EndpointUpdateModel data) -GET /endpoints/{endpointname}/errors => ServiceControl.MessageFailures.Api.GetAllErrorsController:ErrorsByEndpointName(String endpointName) +GET /endpoints/{endpointname}/errors => ServiceControl.MessageFailures.Api.GetAllErrorsController:ErrorsByEndpointName() GET /endpoints/known => ServiceControl.Monitoring.EndpointsMonitoringController:KnownEndpoints() GET /errors => ServiceControl.MessageFailures.Api.GetAllErrorsController:ErrorsGet() HEAD /errors => ServiceControl.MessageFailures.Api.GetAllErrorsController:ErrorsHead() @@ -53,7 +53,7 @@ POST /pendingretries/queues/retry => ServiceControl.MessageFailures.Api.PendingR PATCH /pendingretries/resolve => ServiceControl.MessageFailures.Api.ResolveMessagesController:ResolveBy(ResolveRequest request) POST /pendingretries/retry => ServiceControl.MessageFailures.Api.PendingRetryMessagesController:RetryBy(List ids) GET /recoverability/classifiers => ServiceControl.Recoverability.API.FailureGroupsController:GetSupportedClassifiers() -GET /recoverability/groups/{classifier?} => ServiceControl.Recoverability.API.FailureGroupsController:GetAllGroups(String classifierFilter, String classifier) +GET /recoverability/groups/{classifier?} => ServiceControl.Recoverability.API.FailureGroupsController:GetAllGroups(String classifier) DELETE /recoverability/groups/{groupid}/comment => ServiceControl.Recoverability.API.FailureGroupsController:DeleteComment(String groupId) POST /recoverability/groups/{groupid}/comment => ServiceControl.Recoverability.API.FailureGroupsController:EditComment(String groupId, String comment) GET /recoverability/groups/{groupId}/errors => ServiceControl.Recoverability.API.FailureGroupsController:GetGroupErrors(String groupId) diff --git a/src/ServiceControl.UnitTests/Archiving/ArchiveGroupTests.cs b/src/ServiceControl.UnitTests/Archiving/ArchiveGroupTests.cs deleted file mode 100644 index 659073b77f..0000000000 --- a/src/ServiceControl.UnitTests/Archiving/ArchiveGroupTests.cs +++ /dev/null @@ -1,132 +0,0 @@ -namespace ServiceControl.UnitTests.Archiving -{ - using System; - using System.Threading.Tasks; - using NServiceBus.Testing; - using NUnit.Framework; - using ServiceControl.Infrastructure.DomainEvents; - using ServiceControl.Recoverability; - - [TestFixture] - public class ArchiveGroupTests - { - [Test] - public async Task ArchiveGroup_skips_over_empty_batches_but_still_completes() - { - // Arrange - using (var documentStore = InMemoryStoreBuilder.GetInMemoryStore()) - { - var groupId = "TestGroup"; - var previousArchiveBatchId = ArchiveBatch.MakeId(groupId, ArchiveType.FailureGroup, 1); - - using (var session = documentStore.OpenAsyncSession()) - { - var previousArchiveBatch = new ArchiveBatch { Id = previousArchiveBatchId }; - await session.StoreAsync(previousArchiveBatch) - .ConfigureAwait(false); - - var previousArchiveOperation = new ArchiveOperation - { - Id = ArchiveOperation.MakeId(groupId, ArchiveType.FailureGroup), - RequestId = groupId, - ArchiveType = ArchiveType.FailureGroup, - TotalNumberOfMessages = 2, - NumberOfMessagesArchived = 0, - Started = DateTime.Now, - GroupName = "Test Group", - NumberOfBatches = 3, - CurrentBatch = 0 - }; - await session.StoreAsync(previousArchiveOperation) - .ConfigureAwait(false); - - await session.SaveChangesAsync() - .ConfigureAwait(false); - } - - var domainEvents = new FakeDomainEvents(); - var handler = new ArchiveAllInGroupHandler(documentStore, - new FakeDomainEvents(), - new ArchiveDocumentManager(), - new ArchivingManager(domainEvents, new OperationsManager()), - new RetryingManager(domainEvents)); - - var context = new TestableMessageHandlerContext(); - var message = new ArchiveAllInGroup { GroupId = groupId }; - - // Act - await handler.Handle(message, context) - .ConfigureAwait(false); - - // Assert - using (var session = documentStore.OpenSession()) - { - var loadedBatch = session.Load(previousArchiveBatchId); - Assert.IsNull(loadedBatch); - } - } - } - - [Test] - public async Task ArchiveGroup_GetGroupDetails_doesnt_fail_with_invalid_groupId() - { - // Arrange - using (var documentStore = InMemoryStoreBuilder.GetInMemoryStore()) - { - var failureGroupsViewIndex = new FailureGroupsViewIndex(); - await failureGroupsViewIndex.ExecuteAsync(documentStore); - - var groupId = "TestGroup"; - var previousArchiveBatchId = ArchiveBatch.MakeId(groupId, ArchiveType.FailureGroup, 1); - - using (var session = documentStore.OpenAsyncSession()) - { - var previousArchiveBatch = new ArchiveBatch { Id = previousArchiveBatchId }; - await session.StoreAsync(previousArchiveBatch) - .ConfigureAwait(false); - - var previousArchiveOperation = new ArchiveOperation - { - Id = ArchiveOperation.MakeId(groupId, ArchiveType.FailureGroup), - RequestId = groupId, - ArchiveType = ArchiveType.FailureGroup, - TotalNumberOfMessages = 2, - NumberOfMessagesArchived = 0, - Started = DateTime.Now, - GroupName = "Test Group", - NumberOfBatches = 3, - CurrentBatch = 0 - }; - await session.StoreAsync(previousArchiveOperation) - .ConfigureAwait(false); - - await session.SaveChangesAsync() - .ConfigureAwait(false); - } - - var domainEvents = new FakeDomainEvents(); - var handler = new ArchiveAllInGroupHandler(documentStore, - new FakeDomainEvents(), - new ArchiveDocumentManager(), - new ArchivingManager(domainEvents, new OperationsManager()), - new RetryingManager(domainEvents)); - - var context = new TestableMessageHandlerContext(); - var message = new ArchiveAllInGroup { GroupId = groupId + "Invalid" }; - - // Act - // Assert - Assert.DoesNotThrowAsync(async () => - { - // Act - await handler.Handle(message, context).ConfigureAwait(false); - }); - } - } - - class FakeDomainEvents : IDomainEvents - { - public Task Raise(T domainEvent) where T : IDomainEvent => Task.CompletedTask; - } - } -} \ No newline at end of file diff --git a/src/ServiceControl.UnitTests/BodyStorage/BodyStorageEnricherTests.cs b/src/ServiceControl.UnitTests/BodyStorage/BodyStorageEnricherTests.cs index 10798efb6a..884913d329 100644 --- a/src/ServiceControl.UnitTests/BodyStorage/BodyStorageEnricherTests.cs +++ b/src/ServiceControl.UnitTests/BodyStorage/BodyStorageEnricherTests.cs @@ -129,7 +129,7 @@ public Task Store(string bodyId, string contentType, int bodySize, Stream bodySt return Task.CompletedTask; } - public Task TryFetch(string bodyId) + public Task TryFetch(string bodyId) { throw new NotImplementedException(); } diff --git a/src/ServiceControl.UnitTests/BodyStorage/RavenAttachmentsBodyStorageTests.cs b/src/ServiceControl.UnitTests/BodyStorage/RavenAttachmentsBodyStorageTests.cs deleted file mode 100644 index 889e6fa99c..0000000000 --- a/src/ServiceControl.UnitTests/BodyStorage/RavenAttachmentsBodyStorageTests.cs +++ /dev/null @@ -1,35 +0,0 @@ -namespace ServiceControl.UnitTests.BodyStorage -{ - using System.IO; - using System.Threading.Tasks; - using NUnit.Framework; - using ServiceControl.Operations.BodyStorage.RavenAttachments; - - [TestFixture] - public class RavenAttachmentsBodyStorageTests - { - [Test] - public async Task Attachments_with_ids_that_contain_backslash_should_be_readable() - { - using (var store = InMemoryStoreBuilder.GetInMemoryStore()) - { - var bodyStore = new RavenAttachmentsBodyStorage(store); - - var messageId = "messagebodies/3f0240a7-9b2e-4e2a-ab39-6114932adad1\\2055783"; - var contentType = "NotImportant"; - var body = new byte[] { 1, 2, 3 }; - - await bodyStore.Store(messageId, contentType, body.Length, new MemoryStream(body)); - - var retrieved = await bodyStore.TryFetch(messageId); - Assert.True(retrieved.HasResult); - Assert.AreEqual(contentType, retrieved.ContentType); - - var buffer = new byte[retrieved.BodySize]; - retrieved.Stream.Read(buffer, 0, retrieved.BodySize); - - Assert.AreEqual(body, buffer); - } - } - } -} \ No newline at end of file diff --git a/src/ServiceControl.UnitTests/Expiration/ProcessedMessageExpirationTests.cs b/src/ServiceControl.UnitTests/Expiration/ProcessedMessageExpirationTests.cs deleted file mode 100644 index 41e52b1ca4..0000000000 --- a/src/ServiceControl.UnitTests/Expiration/ProcessedMessageExpirationTests.cs +++ /dev/null @@ -1,267 +0,0 @@ -namespace ServiceControl.UnitTests.Expiration -{ - using System; - using System.Collections.Generic; - using System.Diagnostics; - using System.IO; - using System.Linq; - using System.Threading; - using System.Threading.Tasks; - using MessageAuditing; - using MessageFailures; - using NUnit.Framework; - using Raven.Client.Embedded; - using ServiceControl.Infrastructure.RavenDB.Expiration; - using ServiceControl.Operations.BodyStorage.RavenAttachments; - - [TestFixture] - public class ProcessedMessageExpirationTests - { - [Test] - public void Old_documents_are_being_expired() - { - using (var documentStore = InMemoryStoreBuilder.GetInMemoryStore()) - { - var expiredDate = DateTime.UtcNow.AddDays(-3); - var thresholdDate = DateTime.UtcNow.AddDays(-2); - var processedMessage = new ProcessedMessage - { - Id = "1", - ProcessedAt = expiredDate - }; - - using (var session = documentStore.OpenSession()) - { - session.Store(processedMessage); - session.SaveChanges(); - } - - RunExpiry(documentStore, thresholdDate); - - using (var session = documentStore.OpenSession()) - { - Assert.IsEmpty(session.Query()); - } - } - } - - [Test] - public void Many_documents_are_being_expired() - { - using (var documentStore = InMemoryStoreBuilder.GetInMemoryStore()) - { - var expiredDate = DateTime.UtcNow.AddDays(-3); - var thresholdDate = DateTime.UtcNow.AddDays(-2); - var recentDate = DateTime.UtcNow.AddDays(-1); - var expiredMessages = BuildExpiredMessaged(expiredDate).ToList(); - using (var session = documentStore.OpenSession()) - { - foreach (var message in expiredMessages) - { - session.Store(message); - } - - session.SaveChanges(); - } - - using (var session = documentStore.OpenSession()) - { - var recentMessage = new ProcessedMessage - { - Id = "recentMessageId", - ProcessedAt = recentDate - }; - session.Store(recentMessage); - session.SaveChanges(); - } - - RunExpiry(documentStore, thresholdDate); - foreach (dynamic message in expiredMessages) - { - using (var session = documentStore.OpenSession()) - { - Assert.Null(session.Load(message.Id)); - } - } - - using (var session = documentStore.OpenSession()) - { - Assert.AreEqual(1, session.Query().Count()); - } - } - } - - IEnumerable BuildExpiredMessaged(DateTime dateTime) - { - for (var i = 0; i < doctestrange; i++) - { - yield return new ProcessedMessage - { - Id = Guid.NewGuid().ToString(), - ProcessedAt = dateTime - }; - } - } - - static void RunExpiry(EmbeddableDocumentStore documentStore, DateTime expiryThreshold) - { - new ExpiryProcessedMessageIndex().Execute(documentStore); - documentStore.WaitForIndexing(); - AuditMessageCleaner.Clean(doctestrange, documentStore.DocumentDatabase, expiryThreshold); - documentStore.WaitForIndexing(); - } - - [Test] - public void Only_processed_messages_are_being_expired() - { - using (var documentStore = InMemoryStoreBuilder.GetInMemoryStore()) - { - var expiredDate = DateTime.UtcNow.AddDays(-3); - var thresholdDate = DateTime.UtcNow.AddDays(-2); - var recentDate = DateTime.UtcNow.AddDays(-1); - var expiredMessage = new ProcessedMessage - { - Id = "1", - ProcessedAt = expiredDate - }; - - using (var session = documentStore.OpenSession()) - { - session.Store(expiredMessage); - session.SaveChanges(); - } - - var recentMessage = new ProcessedMessage - { - Id = "2", - ProcessedAt = recentDate - }; - using (var session = documentStore.OpenSession()) - { - session.Store(recentMessage); - session.SaveChanges(); - } - - RunExpiry(documentStore, thresholdDate); - - using (var session = documentStore.OpenSession()) - { - Assert.Null(session.Load(expiredMessage.Id)); - Assert.NotNull(session.Load(recentMessage.Id)); - } - } - } - - [Test] - public async Task Stored_bodies_are_being_removed_when_message_expires() - { - using (var documentStore = InMemoryStoreBuilder.GetInMemoryStore()) - { - var expiredDate = DateTime.UtcNow.AddDays(-3); - var thresholdDate = DateTime.UtcNow.AddDays(-2); - // Store expired message with associated body - var messageId = "21"; - - var processedMessage = new ProcessedMessage - { - Id = "1", - ProcessedAt = expiredDate, - MessageMetadata = new Dictionary - { - { - "MessageId", messageId - } - } - }; - - using (var session = documentStore.OpenSession()) - { - session.Store(processedMessage); - session.SaveChanges(); - } - - var body = new byte[] - { - 1, - 2, - 3, - 4, - 5 - }; - - var bodyStorage = new RavenAttachmentsBodyStorage(documentStore); - using (var stream = new MemoryStream(body)) - { - await bodyStorage.Store(messageId, "binary", 5, stream); - } - - RunExpiry(documentStore, thresholdDate); - - // Verify message expired - using (var session = documentStore.OpenSession()) - { - Assert.Null(session.Load(processedMessage.Id)); - } - - // Verify body expired - Assert.False((await bodyStorage.TryFetch(messageId)).HasResult, "Audit document body should be deleted"); - } - } - - [Test] - public void Recent_processed_messages_are_not_being_expired() - { - using (var documentStore = InMemoryStoreBuilder.GetInMemoryStore()) - { - var thresholdDate = DateTime.UtcNow.AddDays(-2); - var recentDate = DateTime.UtcNow.AddDays(-1); - var message = new ProcessedMessage - { - Id = "1", - ProcessedAt = recentDate - }; - using (var session = documentStore.OpenSession()) - { - session.Store(message); - session.SaveChanges(); - } - - RunExpiry(documentStore, thresholdDate); - using (var session = documentStore.OpenSession()) - { - Assert.AreEqual(1, session.Query().Count()); - } - } - } - - [Test] - public void Errors_are_not_being_expired() - { - using (var documentStore = InMemoryStoreBuilder.GetInMemoryStore()) - { - var failedMsg = new FailedMessage - { - Id = "1" - }; - - using (var session = documentStore.OpenSession()) - { - session.Store(failedMsg); - session.SaveChanges(); - - Debug.WriteLine(session.Advanced.GetMetadataFor(failedMsg)["Last-Modified"]); - } - - Thread.Sleep(100); - RunExpiry(documentStore, DateTime.UtcNow); - - using (var session = documentStore.OpenSession()) - { - Assert.NotNull(session.Load(failedMsg.Id)); - } - } - } - - static int doctestrange = 999; - } -} \ No newline at end of file diff --git a/src/ServiceControl.UnitTests/Fakes/FakeApplicationLifetime.cs b/src/ServiceControl.UnitTests/Fakes/FakeApplicationLifetime.cs new file mode 100644 index 0000000000..9cd3772c1f --- /dev/null +++ b/src/ServiceControl.UnitTests/Fakes/FakeApplicationLifetime.cs @@ -0,0 +1,15 @@ +namespace ServiceControl.UnitTests +{ + using System; + using System.Threading; + using Microsoft.Extensions.Hosting; + + class FakeApplicationLifetime : IHostApplicationLifetime + { + public void StopApplication() => throw new NotImplementedException(); + + public CancellationToken ApplicationStarted { get; } = new CancellationToken(); + public CancellationToken ApplicationStopping { get; } = new CancellationToken(); + public CancellationToken ApplicationStopped { get; } = new CancellationToken(); + } +} diff --git a/src/ServiceControl.UnitTests/Infrastructure/WebApiHostBuilderExtensionsTests.cs b/src/ServiceControl.UnitTests/Infrastructure/WebApiHostBuilderExtensionsTests.cs deleted file mode 100644 index ea09af05b6..0000000000 --- a/src/ServiceControl.UnitTests/Infrastructure/WebApiHostBuilderExtensionsTests.cs +++ /dev/null @@ -1,67 +0,0 @@ -namespace ServiceControl.UnitTests.Infrastructure -{ - using System; - using System.Collections.Generic; - using System.Linq; - using System.Net.Http; - using System.Reflection; - using System.Web.Http.Controllers; - using Microsoft.Extensions.DependencyInjection; - using Microsoft.Extensions.Hosting; - using NServiceBus; - using NUnit.Framework; - using Particular.ServiceControl; - using Raven.Client; - using ServiceBus.Management.Infrastructure.Settings; - using ServiceControl.Infrastructure.DomainEvents; - using ServiceControl.Infrastructure.WebApi; - using ServiceControl.Persistence; - - [TestFixture] - class WebApiHostBuilderExtensionsTests - { - static IHostBuilder PrepareHostBuilder() => new HostBuilder() - .ConfigureServices(serviceCollection => - { - serviceCollection.AddSingleton(_ => InMemoryStoreBuilder.GetInMemoryStore()); - serviceCollection.AddSingleton>(() => new HttpClient()); - serviceCollection.AddSingleton(); - serviceCollection.AddServiceControlPersistence(DataStoreType.InMemory); - serviceCollection.AddSingleton(new LoggingSettings("test")); - }) - .UseNServiceBus(_ => - { - var config = new EndpointConfiguration("test"); - config.UseTransport(); - return config; - }) - .UseServiceControlComponents( - new ServiceBus.Management.Infrastructure.Settings.Settings(), - ServiceControlMainInstance.Components); - - [Test] - public void UseWebApi_RegistersAllControllers() - { - // Arrange - var assembly = Assembly.GetAssembly(typeof(WebApiHostBuilderExtensions)); - var controllerTypes = assembly.DefinedTypes - .Where(type => typeof(IHttpController).IsAssignableFrom(type) && - type.Name.EndsWith("Controller", StringComparison.Ordinal)); - - var hostBuilder = PrepareHostBuilder(); - - // Act - var host = hostBuilder - .UseWebApi(new List { assembly }, string.Empty, false) - .Build(); - - // Assert - Assert.That(host, Is.Not.Null); - - foreach (var controllerType in controllerTypes) - { - Assert.That(host.Services.GetService(controllerType), Is.Not.Null); - } - } - } -} \ No newline at end of file diff --git a/src/ServiceControl.UnitTests/Operations/FailedErrorImportCustomCheckTests.cs b/src/ServiceControl.UnitTests/Operations/FailedErrorImportCustomCheckTests.cs deleted file mode 100644 index 43799ac6ba..0000000000 --- a/src/ServiceControl.UnitTests/Operations/FailedErrorImportCustomCheckTests.cs +++ /dev/null @@ -1,50 +0,0 @@ -namespace ServiceControl.UnitTests.Operations -{ - using System.Threading.Tasks; - using NServiceBus.CustomChecks; - using NUnit.Framework; - using ServiceControl.Operations; - - [TestFixture] - public class FailedErrorImportCustomCheckTests - { - [Test] - public async Task Pass_if_no_failed_imports() - { - using (var store = InMemoryStoreBuilder.GetInMemoryStore()) - { - store.ExecuteIndex(new FailedErrorImportIndex()); - - var customCheck = new FailedErrorImportCustomCheck(store); - - var result = await customCheck.PerformCheck(); - - Assert.AreEqual(CheckResult.Pass, result); - } - } - - [Test] - public async Task Fail_if_failed_imports() - { - using (var store = InMemoryStoreBuilder.GetInMemoryStore()) - { - store.ExecuteIndex(new FailedErrorImportIndex()); - - using (var session = store.OpenAsyncSession()) - { - await session.StoreAsync(new FailedErrorImport()); - await session.SaveChangesAsync(); - } - - store.WaitForIndexing(); - - var customCheck = new FailedErrorImportCustomCheck(store); - - var result = await customCheck.PerformCheck(); - - Assert.IsTrue(result.HasFailed); - StringAssert.StartsWith("One or more error messages have failed to import properly into ServiceControl and have been stored in the ServiceControl database.", result.FailureReason); - } - } - } -} \ No newline at end of file diff --git a/src/ServiceControl.UnitTests/RavenIndexAwaiter.cs b/src/ServiceControl.UnitTests/RavenIndexAwaiter.cs deleted file mode 100644 index 26ec4d1a83..0000000000 --- a/src/ServiceControl.UnitTests/RavenIndexAwaiter.cs +++ /dev/null @@ -1,18 +0,0 @@ -using System; -using System.Threading; -using NUnit.Framework; -using Raven.Client; - -public static class RavenIndexAwaiter -{ - public static void WaitForIndexing(this IDocumentStore store) - { - store.WaitForIndexing(10); - } - - static void WaitForIndexing(this IDocumentStore store, int secondsToWait) - { - var databaseCommands = store.DatabaseCommands; - Assert.True(SpinWait.SpinUntil(() => databaseCommands.GetStatistics().StaleIndexes.Length == 0, TimeSpan.FromSeconds(secondsToWait))); - } -} \ No newline at end of file diff --git a/src/ServiceControl.UnitTests/Recoverability/Retry_State_Tests.cs b/src/ServiceControl.UnitTests/Recoverability/Retry_State_Tests.cs deleted file mode 100644 index 60a761dfdc..0000000000 --- a/src/ServiceControl.UnitTests/Recoverability/Retry_State_Tests.cs +++ /dev/null @@ -1,347 +0,0 @@ -namespace ServiceControl.UnitTests.Recoverability -{ - using System; - using System.Collections.Generic; - using System.Linq; - using System.Threading; - using System.Threading.Tasks; - using Contracts.Operations; - using MessageFailures; - using Microsoft.Extensions.Hosting; - using NServiceBus.Extensibility; - using NServiceBus.Transport; - using NUnit.Framework; - using Operations; - using Raven.Client; - using ServiceBus.Management.Infrastructure.Settings; - using ServiceControl.Infrastructure.BackgroundTasks; - using ServiceControl.Infrastructure.DomainEvents; - using ServiceControl.Operations.BodyStorage.RavenAttachments; - using ServiceControl.Persistence; - using ServiceControl.Recoverability; - - [TestFixture] - public class Retry_State_Tests - { - [Test] - public async Task When_a_group_is_processed_it_is_set_to_the_Preparing_state() - { - var domainEvents = new FakeDomainEvents(); - var retryManager = new RetryingManager(domainEvents); - - using (var documentStore = InMemoryStoreBuilder.GetInMemoryStore()) - { - await CreateAFailedMessageAndMarkAsPartOfRetryBatch(documentStore, retryManager, "Test-group", true, 1); - var status = retryManager.GetStatusForRetryOperation("Test-group", RetryType.FailureGroup); - - Assert.AreEqual(RetryState.Preparing, status.RetryState); - } - } - - [Test] - public async Task When_a_group_is_prepared_and_SC_is_started_the_group_is_marked_as_failed() - { - var domainEvents = new FakeDomainEvents(); - var retryManager = new RetryingManager(domainEvents); - - using (var documentStore = InMemoryStoreBuilder.GetInMemoryStore()) - { - await CreateAFailedMessageAndMarkAsPartOfRetryBatch(documentStore, retryManager, "Test-group", false, 1); - - new RetryBatches_ByStatusAndSession().Execute(documentStore); - new FailedMessageRetries_ByBatch().Execute(documentStore); - - documentStore.WaitForIndexing(); - - var documentManager = new CustomRetryDocumentManager(false, documentStore, retryManager); - - var orphanage = new RecoverabilityComponent.AdoptOrphanBatchesFromPreviousSessionHostedService(documentManager, documentStore, new AsyncTimer()); - await orphanage.AdoptOrphanedBatchesAsync(); - - var status = retryManager.GetStatusForRetryOperation("Test-group", RetryType.FailureGroup); - Assert.True(status.Failed); - } - } - - [Test] - public async Task When_a_group_is_prepared_with_three_batches_and_SC_is_restarted_while_the_first_group_is_being_forwarded_then_the_count_still_matches() - { - var domainEvents = new FakeDomainEvents(); - var retryManager = new RetryingManager(domainEvents); - - using (var documentStore = InMemoryStoreBuilder.GetInMemoryStore()) - { - await CreateAFailedMessageAndMarkAsPartOfRetryBatch(documentStore, retryManager, "Test-group", true, 2001); - - new RetryBatches_ByStatus_ReduceInitialBatchSize().Execute(documentStore); - - var sender = new TestSender(); - - var bodyStorage = new RavenAttachmentsBodyStorage(documentStore); - - var processor = new RetryProcessor(documentStore, domainEvents, new TestReturnToSenderDequeuer(new ReturnToSender(bodyStorage, documentStore), documentStore, domainEvents, "TestEndpoint"), retryManager); - - documentStore.WaitForIndexing(); - - using (var session = documentStore.OpenAsyncSession()) - { - await processor.ProcessBatches(session, sender); // mark ready - await session.SaveChangesAsync(); - - - // Simulate SC restart - retryManager = new RetryingManager(domainEvents); - - var documentManager = new CustomRetryDocumentManager(false, documentStore, retryManager); - - await documentManager.RebuildRetryOperationState(session); - - processor = new RetryProcessor(documentStore, domainEvents, new TestReturnToSenderDequeuer(new ReturnToSender(bodyStorage, documentStore), documentStore, domainEvents, "TestEndpoint"), retryManager); - - await processor.ProcessBatches(session, sender); - await session.SaveChangesAsync(); - } - - var status = retryManager.GetStatusForRetryOperation("Test-group", RetryType.FailureGroup); - Assert.AreEqual(2001, status.TotalNumberOfMessages); - } - } - - [Test] - public async Task When_a_group_is_forwarded_the_status_is_Completed() - { - var domainEvents = new FakeDomainEvents(); - var retryManager = new RetryingManager(domainEvents); - - using (var documentStore = InMemoryStoreBuilder.GetInMemoryStore()) - { - await CreateAFailedMessageAndMarkAsPartOfRetryBatch(documentStore, retryManager, "Test-group", true, 1); - - var sender = new TestSender(); - - var bodyStorage = new RavenAttachmentsBodyStorage(documentStore); - - var returnToSender = new TestReturnToSenderDequeuer(new ReturnToSender(bodyStorage, documentStore), documentStore, domainEvents, "TestEndpoint"); - var processor = new RetryProcessor(documentStore, domainEvents, returnToSender, retryManager); - - using (var session = documentStore.OpenAsyncSession()) - { - await processor.ProcessBatches(session, sender); // mark ready - await session.SaveChangesAsync(); - - await processor.ProcessBatches(session, sender); - await session.SaveChangesAsync(); - } - - var status = retryManager.GetStatusForRetryOperation("Test-group", RetryType.FailureGroup); - Assert.AreEqual(RetryState.Completed, status.RetryState); - } - } - - [Test] - public async Task When_there_is_one_poison_message_it_is_removed_from_batch_and_the_status_is_Complete() - { - var domainEvents = new FakeDomainEvents(); - var retryManager = new RetryingManager(domainEvents); - - using (var documentStore = InMemoryStoreBuilder.GetInMemoryStore()) - { - await CreateAFailedMessageAndMarkAsPartOfRetryBatch(documentStore, retryManager, "Test-group", true, "A", "B", "C"); - - var sender = new TestSender - { - Callback = operation => - { - //Always fails staging message B - if (operation.Message.MessageId == "FailedMessages/B") - { - throw new Exception("Simulated"); - } - } - }; - - var bodyStorage = new RavenAttachmentsBodyStorage(documentStore); - - var returnToSender = new TestReturnToSenderDequeuer(new ReturnToSender(bodyStorage, documentStore), documentStore, domainEvents, "TestEndpoint"); - var processor = new RetryProcessor(documentStore, domainEvents, returnToSender, retryManager); - - bool c; - do - { - try - { - using (var session = documentStore.OpenAsyncSession()) - { - c = await processor.ProcessBatches(session, sender); - await session.SaveChangesAsync(); - } - } - catch (Exception) - { - //Continue trying until there is no exception -> poison message is removed from the batch - c = true; - } - } - while (c); - - var status = retryManager.GetStatusForRetryOperation("Test-group", RetryType.FailureGroup); - - Assert.AreEqual(RetryState.Completed, status.RetryState); - Assert.AreEqual(3, status.NumberOfMessagesPrepared); - Assert.AreEqual(2, status.NumberOfMessagesForwarded); - Assert.AreEqual(1, status.NumberOfMessagesSkipped); - } - } - - [Test] - public async Task When_a_group_has_one_batch_out_of_two_forwarded_the_status_is_Forwarding() - { - var domainEvents = new FakeDomainEvents(); - var retryManager = new RetryingManager(domainEvents); - - using (var documentStore = InMemoryStoreBuilder.GetInMemoryStore()) - { - await CreateAFailedMessageAndMarkAsPartOfRetryBatch(documentStore, retryManager, "Test-group", true, 1001); - - var bodyStorage = new RavenAttachmentsBodyStorage(documentStore); - - var returnToSender = new ReturnToSender(bodyStorage, documentStore); - - var sender = new TestSender(); - - var processor = new RetryProcessor(documentStore, domainEvents, new TestReturnToSenderDequeuer(returnToSender, documentStore, domainEvents, "TestEndpoint"), retryManager); - - documentStore.WaitForIndexing(); - - using (var session = documentStore.OpenAsyncSession()) - { - await processor.ProcessBatches(session, sender); // mark ready - await session.SaveChangesAsync(); - - await processor.ProcessBatches(session, sender); - await session.SaveChangesAsync(); - } - - var status = retryManager.GetStatusForRetryOperation("Test-group", RetryType.FailureGroup); - Assert.AreEqual(RetryState.Forwarding, status.RetryState); - } - } - - Task CreateAFailedMessageAndMarkAsPartOfRetryBatch(IDocumentStore documentStore, RetryingManager retryManager, string groupId, bool progressToStaged, int numberOfMessages) - { - return CreateAFailedMessageAndMarkAsPartOfRetryBatch(documentStore, retryManager, groupId, progressToStaged, Enumerable.Range(0, numberOfMessages).Select(i => Guid.NewGuid().ToString()).ToArray()); - } - - async Task CreateAFailedMessageAndMarkAsPartOfRetryBatch(IDocumentStore documentStore, RetryingManager retryManager, string groupId, bool progressToStaged, params string[] messageIds) - { - var messages = messageIds.Select(id => new FailedMessage - { - Id = FailedMessage.MakeDocumentId(id), - UniqueMessageId = id, - FailureGroups = new List - { - new FailedMessage.FailureGroup - { - Id = groupId, - Title = groupId, - Type = groupId - } - }, - Status = FailedMessageStatus.Unresolved, - ProcessingAttempts = new List - { - new FailedMessage.ProcessingAttempt - { - AttemptedAt = DateTime.UtcNow, - MessageMetadata = new Dictionary(), - FailureDetails = new FailureDetails(), - Headers = new Dictionary() - } - } - }); - - using (var session = documentStore.OpenAsyncSession()) - { - foreach (var message in messages) - { - await session.StoreAsync(message); - } - - await session.SaveChangesAsync(); - } - - await new FailedMessages_ByGroup().ExecuteAsync(documentStore); - await new FailedMessages_UniqueMessageIdAndTimeOfFailures().ExecuteAsync(documentStore); - - documentStore.WaitForIndexing(); - - var documentManager = new CustomRetryDocumentManager(progressToStaged, documentStore, retryManager); - var gateway = new RetriesGateway(documentStore, documentManager, retryManager); - - gateway.StartRetryForIndex("Test-group", RetryType.FailureGroup, DateTime.UtcNow, x => x.FailureGroupId == "Test-group", "Test-Context"); - - documentStore.WaitForIndexing(); - - await gateway.ProcessNextBulkRetry(); - } - } - - class FakeApplicationLifetime : IHostApplicationLifetime - { - public void StopApplication() => throw new NotImplementedException(); - - public CancellationToken ApplicationStarted { get; } = new CancellationToken(); - public CancellationToken ApplicationStopping { get; } = new CancellationToken(); - public CancellationToken ApplicationStopped { get; } = new CancellationToken(); - } - - class CustomRetryDocumentManager : RetryDocumentManager - { - public CustomRetryDocumentManager(bool progressToStaged, IDocumentStore documentStore, RetryingManager retryManager) - : base(new FakeApplicationLifetime(), documentStore, retryManager) - { - RetrySessionId = Guid.NewGuid().ToString(); - this.progressToStaged = progressToStaged; - } - - public override Task MoveBatchToStaging(string batchDocumentId) - { - if (progressToStaged) - { - return base.MoveBatchToStaging(batchDocumentId); - } - - return Task.FromResult(0); - } - - bool progressToStaged; - } - - class TestReturnToSenderDequeuer : ReturnToSenderDequeuer - { - public TestReturnToSenderDequeuer(ReturnToSender returnToSender, IDocumentStore store, IDomainEvents domainEvents, string endpointName) - : base(returnToSender, store, domainEvents, null, new Settings(endpointName)) - { - } - - public override Task Run(string forwardingBatchId, Predicate filter, int? expectedMessageCount, CancellationToken cancellationToken = default) - { - return Task.FromResult(0); - } - } - - public class TestSender : IDispatchMessages - { - public Action Callback { get; set; } = m => { }; - - public Task Dispatch(TransportOperations outgoingMessages, TransportTransaction transaction, ContextBag context) - { - foreach (var operation in outgoingMessages.UnicastTransportOperations) - { - Callback(operation); - } - - return Task.FromResult(0); - } - } -} \ No newline at end of file diff --git a/src/ServiceControl.UnitTests/ScatterGather/MessageView_ScatterGatherTest.cs b/src/ServiceControl.UnitTests/ScatterGather/MessageView_ScatterGatherTest.cs index 0991cabd2f..b7bad34726 100644 --- a/src/ServiceControl.UnitTests/ScatterGather/MessageView_ScatterGatherTest.cs +++ b/src/ServiceControl.UnitTests/ScatterGather/MessageView_ScatterGatherTest.cs @@ -6,7 +6,6 @@ using System.Net.Http; using System.Threading.Tasks; using NUnit.Framework; - using Raven.Client; using ServiceBus.Management.Infrastructure.Settings; using ServiceControl.CompositeViews.Messages; using ServiceControl.Persistence.Infrastructure; @@ -40,7 +39,7 @@ QueryResult> GetPage(IEnumerable source, strin return new QueryResult>( pageOfResults, - new QueryStatsInfo(etag, allResults.Count)) + new QueryStatsInfo(etag, allResults.Count, isStale: false)) { InstanceId = instanceId }; @@ -69,9 +68,9 @@ protected IEnumerable RemoteData() protected const string RemoteETag = nameof(RemoteETag); protected const int PageSize = 50; - class TestApi : ScatterGatherApiMessageView + class TestApi : ScatterGatherApiMessageView { - public TestApi(IDocumentStore documentStore, Settings settings, Func httpClientFactory) : base(documentStore, settings, httpClientFactory) + public TestApi(object documentStore, Settings settings, Func httpClientFactory) : base(documentStore, settings, httpClientFactory) { } diff --git a/src/ServiceControl.UnitTests/ServiceControl.UnitTests.csproj b/src/ServiceControl.UnitTests/ServiceControl.UnitTests.csproj index 5ed967818c..921fc51d38 100644 --- a/src/ServiceControl.UnitTests/ServiceControl.UnitTests.csproj +++ b/src/ServiceControl.UnitTests/ServiceControl.UnitTests.csproj @@ -8,8 +8,6 @@ - - @@ -24,7 +22,6 @@ - diff --git a/src/ServiceControl.UnitTests/Unarchiving/UnarchiveGroupTests.cs b/src/ServiceControl.UnitTests/Unarchiving/UnarchiveGroupTests.cs deleted file mode 100644 index 05a41f78fe..0000000000 --- a/src/ServiceControl.UnitTests/Unarchiving/UnarchiveGroupTests.cs +++ /dev/null @@ -1,132 +0,0 @@ -namespace ServiceControl.UnitTests.Archiving -{ - using System; - using System.Threading.Tasks; - using NServiceBus.Testing; - using NUnit.Framework; - using ServiceControl.Infrastructure.DomainEvents; - using ServiceControl.Recoverability; - - [TestFixture] - public class UnarchiveGroupTests - { - [Test] - public async Task UnarchiveGroup_skips_over_empty_batches_but_still_completes() - { - // Arrange - using (var documentStore = InMemoryStoreBuilder.GetInMemoryStore()) - { - var groupId = "TestGroup"; - var previousUnarchiveBatchId = UnarchiveBatch.MakeId(groupId, ArchiveType.FailureGroup, 1); - - using (var session = documentStore.OpenAsyncSession()) - { - var previousUnarchiveBatch = new UnarchiveBatch { Id = previousUnarchiveBatchId }; - await session.StoreAsync(previousUnarchiveBatch) - .ConfigureAwait(false); - - var previousUnarchiveOperation = new UnarchiveOperation - { - Id = UnarchiveOperation.MakeId(groupId, ArchiveType.FailureGroup), - RequestId = groupId, - ArchiveType = ArchiveType.FailureGroup, - TotalNumberOfMessages = 2, - NumberOfMessagesUnarchived = 2, - Started = DateTime.Now, - GroupName = "Test Group", - NumberOfBatches = 3, - CurrentBatch = 0 - }; - await session.StoreAsync(previousUnarchiveOperation) - .ConfigureAwait(false); - - await session.SaveChangesAsync() - .ConfigureAwait(false); - } - - var domainEvents = new FakeDomainEvents(); - var handler = new UnarchiveAllInGroupHandler(documentStore, - new FakeDomainEvents(), - new UnarchiveDocumentManager(), - new UnarchivingManager(domainEvents, new OperationsManager()), - new RetryingManager(domainEvents)); - - var context = new TestableMessageHandlerContext(); - var message = new UnarchiveAllInGroup { GroupId = groupId }; - - // Act - await handler.Handle(message, context) - .ConfigureAwait(false); - - // Assert - using (var session = documentStore.OpenSession()) - { - var loadedBatch = session.Load(previousUnarchiveBatchId); - Assert.IsNull(loadedBatch); - } - } - } - - [Test] - public async Task UnarchiveGroup_GetGroupDetails_doesnt_fail_with_invalid_groupId() - { - // Arrange - using (var documentStore = InMemoryStoreBuilder.GetInMemoryStore()) - { - var failureGroupsViewIndex = new ArchivedGroupsViewIndex(); - await failureGroupsViewIndex.ExecuteAsync(documentStore); - - var groupId = "TestGroup"; - var previousUnarchiveBatchId = UnarchiveBatch.MakeId(groupId, ArchiveType.FailureGroup, 1); - - using (var session = documentStore.OpenAsyncSession()) - { - var previousUnarchiveBatch = new UnarchiveBatch { Id = previousUnarchiveBatchId }; - await session.StoreAsync(previousUnarchiveBatch) - .ConfigureAwait(false); - - var previousUnarchiveOperation = new UnarchiveOperation - { - Id = UnarchiveOperation.MakeId(groupId, ArchiveType.FailureGroup), - RequestId = groupId, - ArchiveType = ArchiveType.FailureGroup, - TotalNumberOfMessages = 2, - NumberOfMessagesUnarchived = 0, - Started = DateTime.Now, - GroupName = "Test Group", - NumberOfBatches = 3, - CurrentBatch = 0 - }; - await session.StoreAsync(previousUnarchiveOperation) - .ConfigureAwait(false); - - await session.SaveChangesAsync() - .ConfigureAwait(false); - } - - var domainEvents = new FakeDomainEvents(); - var handler = new UnarchiveAllInGroupHandler(documentStore, - new FakeDomainEvents(), - new UnarchiveDocumentManager(), - new UnarchivingManager(domainEvents, new OperationsManager()), - new RetryingManager(domainEvents)); - - var context = new TestableMessageHandlerContext(); - var message = new UnarchiveAllInGroup { GroupId = groupId + "Invalid" }; - - // Act - // Assert - Assert.DoesNotThrowAsync(async () => - { - // Act - await handler.Handle(message, context).ConfigureAwait(false); - }); - } - } - - class FakeDomainEvents : IDomainEvents - { - public Task Raise(T domainEvent) where T : IDomainEvent => Task.CompletedTask; - } - } -} \ No newline at end of file diff --git a/src/ServiceControl.sln b/src/ServiceControl.sln index 16571df0d3..d580a47f88 100644 --- a/src/ServiceControl.sln +++ b/src/ServiceControl.sln @@ -6,8 +6,6 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ServiceControl.UnitTests", EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ServiceControl", "ServiceControl\ServiceControl.csproj", "{4C6B71B2-74EF-4B9E-88FF-C56532727C6D}" EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ServiceControl.AcceptanceTests", "ServiceControl.AcceptanceTests\ServiceControl.AcceptanceTests.csproj", "{A1E60657-039F-4BAB-B031-2515F86B0C3D}" -EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Installer", "Installer", "{55C388DD-2B39-4C2F-AEBD-AFD3444815F1}" EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ServiceControlInstaller.Engine", "ServiceControlInstaller.Engine\ServiceControlInstaller.Engine.csproj", "{E2F41605-F664-4EC6-9D64-1F142CC3B7B2}" @@ -51,6 +49,7 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Directory.Build.targets = Directory.Build.targets Directory.Packages.props = Directory.Packages.props Persisters.Audit.Includes.props = Persisters.Audit.Includes.props + Persisters.Primary.Includes.props = Persisters.Primary.Includes.props Transports.Includes.props = Transports.Includes.props EndProjectSection EndProject @@ -114,12 +113,8 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Persistence", "Persistence" EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ServiceControl.Persistence.InMemory", "ServiceControl.Persistence.InMemory\ServiceControl.Persistence.InMemory.csproj", "{C536A96A-D4B7-4793-890E-5A29ED7144E5}" EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ServiceControl.Persistence.SqlServer", "ServiceControl.Persistence.SqlServer\ServiceControl.Persistence.SqlServer.csproj", "{2A91F751-8432-424C-8D45-7E3BFC4CDBAD}" -EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ServiceControl.Persistence.RavenDb", "ServiceControl.Persistence.RavenDb\ServiceControl.Persistence.RavenDb.csproj", "{07F15F50-0A81-4872-9C7D-34DE88599B9F}" EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ServiceControl.PersistenceTests", "ServiceControl.PersistenceTests\ServiceControl.PersistenceTests.csproj", "{30038795-181B-488E-8E99-CFF2009FC6D7}" -EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Persistence", "Persistence", "{BD162BC6-705F-45B4-A6B5-C138DC966C1D}" EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ServiceControl.Audit.Persistence.RavenDb", "ServiceControl.Audit.Persistence.RavenDb\ServiceControl.Audit.Persistence.RavenDb.csproj", "{59B2735E-9B70-4277-8E8C-C8CCB40A097A}" @@ -180,290 +175,860 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "TestHelper", "TestHelper\Te EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ServiceControl.Infrastructure", "ServiceControl.Infrastructure\ServiceControl.Infrastructure.csproj", "{F488851D-C635-49C9-8CD7-E9B90C756565}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ServiceControl.Infrastructure.Tests", "ServiceControl.Infrastructure.Tests\ServiceControl.Infrastructure.Tests.csproj", "{80008096-F1F9-43C3-8CE6-E79A68F8676E}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ServiceControl.Infrastructure.Tests", "ServiceControl.Infrastructure.Tests\ServiceControl.Infrastructure.Tests.csproj", "{80008096-F1F9-43C3-8CE6-E79A68F8676E}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ServiceControl.DomainEvents", "ServiceControl.DomainEvents\ServiceControl.DomainEvents.csproj", "{B93B8B7E-BEAF-4645-AA5C-2A1CADEC3D4B}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ServiceControl.Persistence.Tests.RavenDb", "ServiceControl.Persistence.Tests.RavenDb\ServiceControl.Persistence.Tests.RavenDb.csproj", "{F2BD40E4-A077-429A-8A22-1C80AFC240E8}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ServiceControl.AcceptanceTests.RavenDB", "ServiceControl.AcceptanceTests.RavenDB\ServiceControl.AcceptanceTests.RavenDB.csproj", "{33D5D084-FABB-4F65-A046-1C6626DF9AFB}" EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU + Debug|x64 = Debug|x64 + Debug|x86 = Debug|x86 Release|Any CPU = Release|Any CPU + Release|x64 = Release|x64 + Release|x86 = Release|x86 EndGlobalSection GlobalSection(ProjectConfigurationPlatforms) = postSolution {4F65FAE1-4C51-4BEF-956E-97C6A96807F9}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {4F65FAE1-4C51-4BEF-956E-97C6A96807F9}.Debug|Any CPU.Build.0 = Debug|Any CPU + {4F65FAE1-4C51-4BEF-956E-97C6A96807F9}.Debug|x64.ActiveCfg = Debug|Any CPU + {4F65FAE1-4C51-4BEF-956E-97C6A96807F9}.Debug|x64.Build.0 = Debug|Any CPU + {4F65FAE1-4C51-4BEF-956E-97C6A96807F9}.Debug|x86.ActiveCfg = Debug|Any CPU + {4F65FAE1-4C51-4BEF-956E-97C6A96807F9}.Debug|x86.Build.0 = Debug|Any CPU {4F65FAE1-4C51-4BEF-956E-97C6A96807F9}.Release|Any CPU.ActiveCfg = Release|Any CPU {4F65FAE1-4C51-4BEF-956E-97C6A96807F9}.Release|Any CPU.Build.0 = Release|Any CPU + {4F65FAE1-4C51-4BEF-956E-97C6A96807F9}.Release|x64.ActiveCfg = Release|Any CPU + {4F65FAE1-4C51-4BEF-956E-97C6A96807F9}.Release|x64.Build.0 = Release|Any CPU + {4F65FAE1-4C51-4BEF-956E-97C6A96807F9}.Release|x86.ActiveCfg = Release|Any CPU + {4F65FAE1-4C51-4BEF-956E-97C6A96807F9}.Release|x86.Build.0 = Release|Any CPU {4C6B71B2-74EF-4B9E-88FF-C56532727C6D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {4C6B71B2-74EF-4B9E-88FF-C56532727C6D}.Debug|Any CPU.Build.0 = Debug|Any CPU + {4C6B71B2-74EF-4B9E-88FF-C56532727C6D}.Debug|x64.ActiveCfg = Debug|Any CPU + {4C6B71B2-74EF-4B9E-88FF-C56532727C6D}.Debug|x64.Build.0 = Debug|Any CPU + {4C6B71B2-74EF-4B9E-88FF-C56532727C6D}.Debug|x86.ActiveCfg = Debug|Any CPU + {4C6B71B2-74EF-4B9E-88FF-C56532727C6D}.Debug|x86.Build.0 = Debug|Any CPU {4C6B71B2-74EF-4B9E-88FF-C56532727C6D}.Release|Any CPU.ActiveCfg = Release|Any CPU {4C6B71B2-74EF-4B9E-88FF-C56532727C6D}.Release|Any CPU.Build.0 = Release|Any CPU - {A1E60657-039F-4BAB-B031-2515F86B0C3D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {A1E60657-039F-4BAB-B031-2515F86B0C3D}.Debug|Any CPU.Build.0 = Debug|Any CPU - {A1E60657-039F-4BAB-B031-2515F86B0C3D}.Release|Any CPU.ActiveCfg = Release|Any CPU - {A1E60657-039F-4BAB-B031-2515F86B0C3D}.Release|Any CPU.Build.0 = Release|Any CPU + {4C6B71B2-74EF-4B9E-88FF-C56532727C6D}.Release|x64.ActiveCfg = Release|Any CPU + {4C6B71B2-74EF-4B9E-88FF-C56532727C6D}.Release|x64.Build.0 = Release|Any CPU + {4C6B71B2-74EF-4B9E-88FF-C56532727C6D}.Release|x86.ActiveCfg = Release|Any CPU + {4C6B71B2-74EF-4B9E-88FF-C56532727C6D}.Release|x86.Build.0 = Release|Any CPU {E2F41605-F664-4EC6-9D64-1F142CC3B7B2}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {E2F41605-F664-4EC6-9D64-1F142CC3B7B2}.Debug|Any CPU.Build.0 = Debug|Any CPU + {E2F41605-F664-4EC6-9D64-1F142CC3B7B2}.Debug|x64.ActiveCfg = Debug|Any CPU + {E2F41605-F664-4EC6-9D64-1F142CC3B7B2}.Debug|x64.Build.0 = Debug|Any CPU + {E2F41605-F664-4EC6-9D64-1F142CC3B7B2}.Debug|x86.ActiveCfg = Debug|Any CPU + {E2F41605-F664-4EC6-9D64-1F142CC3B7B2}.Debug|x86.Build.0 = Debug|Any CPU {E2F41605-F664-4EC6-9D64-1F142CC3B7B2}.Release|Any CPU.ActiveCfg = Release|Any CPU {E2F41605-F664-4EC6-9D64-1F142CC3B7B2}.Release|Any CPU.Build.0 = Release|Any CPU + {E2F41605-F664-4EC6-9D64-1F142CC3B7B2}.Release|x64.ActiveCfg = Release|Any CPU + {E2F41605-F664-4EC6-9D64-1F142CC3B7B2}.Release|x64.Build.0 = Release|Any CPU + {E2F41605-F664-4EC6-9D64-1F142CC3B7B2}.Release|x86.ActiveCfg = Release|Any CPU + {E2F41605-F664-4EC6-9D64-1F142CC3B7B2}.Release|x86.Build.0 = Release|Any CPU {FA9439BF-37B4-4F27-B7E9-BE3605E66BE7}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {FA9439BF-37B4-4F27-B7E9-BE3605E66BE7}.Debug|Any CPU.Build.0 = Debug|Any CPU + {FA9439BF-37B4-4F27-B7E9-BE3605E66BE7}.Debug|x64.ActiveCfg = Debug|Any CPU + {FA9439BF-37B4-4F27-B7E9-BE3605E66BE7}.Debug|x64.Build.0 = Debug|Any CPU + {FA9439BF-37B4-4F27-B7E9-BE3605E66BE7}.Debug|x86.ActiveCfg = Debug|Any CPU + {FA9439BF-37B4-4F27-B7E9-BE3605E66BE7}.Debug|x86.Build.0 = Debug|Any CPU {FA9439BF-37B4-4F27-B7E9-BE3605E66BE7}.Release|Any CPU.ActiveCfg = Release|Any CPU {FA9439BF-37B4-4F27-B7E9-BE3605E66BE7}.Release|Any CPU.Build.0 = Release|Any CPU + {FA9439BF-37B4-4F27-B7E9-BE3605E66BE7}.Release|x64.ActiveCfg = Release|Any CPU + {FA9439BF-37B4-4F27-B7E9-BE3605E66BE7}.Release|x64.Build.0 = Release|Any CPU + {FA9439BF-37B4-4F27-B7E9-BE3605E66BE7}.Release|x86.ActiveCfg = Release|Any CPU + {FA9439BF-37B4-4F27-B7E9-BE3605E66BE7}.Release|x86.Build.0 = Release|Any CPU {2343779D-26D4-4F9D-A42F-32E146632EF0}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {2343779D-26D4-4F9D-A42F-32E146632EF0}.Debug|Any CPU.Build.0 = Debug|Any CPU + {2343779D-26D4-4F9D-A42F-32E146632EF0}.Debug|x64.ActiveCfg = Debug|Any CPU + {2343779D-26D4-4F9D-A42F-32E146632EF0}.Debug|x64.Build.0 = Debug|Any CPU + {2343779D-26D4-4F9D-A42F-32E146632EF0}.Debug|x86.ActiveCfg = Debug|Any CPU + {2343779D-26D4-4F9D-A42F-32E146632EF0}.Debug|x86.Build.0 = Debug|Any CPU {2343779D-26D4-4F9D-A42F-32E146632EF0}.Release|Any CPU.ActiveCfg = Release|Any CPU {2343779D-26D4-4F9D-A42F-32E146632EF0}.Release|Any CPU.Build.0 = Release|Any CPU + {2343779D-26D4-4F9D-A42F-32E146632EF0}.Release|x64.ActiveCfg = Release|Any CPU + {2343779D-26D4-4F9D-A42F-32E146632EF0}.Release|x64.Build.0 = Release|Any CPU + {2343779D-26D4-4F9D-A42F-32E146632EF0}.Release|x86.ActiveCfg = Release|Any CPU + {2343779D-26D4-4F9D-A42F-32E146632EF0}.Release|x86.Build.0 = Release|Any CPU {15F811B7-314C-4E9E-B5E9-35596045BBC4}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {15F811B7-314C-4E9E-B5E9-35596045BBC4}.Debug|Any CPU.Build.0 = Debug|Any CPU + {15F811B7-314C-4E9E-B5E9-35596045BBC4}.Debug|x64.ActiveCfg = Debug|Any CPU + {15F811B7-314C-4E9E-B5E9-35596045BBC4}.Debug|x64.Build.0 = Debug|Any CPU + {15F811B7-314C-4E9E-B5E9-35596045BBC4}.Debug|x86.ActiveCfg = Debug|Any CPU + {15F811B7-314C-4E9E-B5E9-35596045BBC4}.Debug|x86.Build.0 = Debug|Any CPU {15F811B7-314C-4E9E-B5E9-35596045BBC4}.Release|Any CPU.ActiveCfg = Release|Any CPU {15F811B7-314C-4E9E-B5E9-35596045BBC4}.Release|Any CPU.Build.0 = Release|Any CPU + {15F811B7-314C-4E9E-B5E9-35596045BBC4}.Release|x64.ActiveCfg = Release|Any CPU + {15F811B7-314C-4E9E-B5E9-35596045BBC4}.Release|x64.Build.0 = Release|Any CPU + {15F811B7-314C-4E9E-B5E9-35596045BBC4}.Release|x86.ActiveCfg = Release|Any CPU + {15F811B7-314C-4E9E-B5E9-35596045BBC4}.Release|x86.Build.0 = Release|Any CPU {C0EEF6D1-5DF7-4A26-9964-6376F465B085}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {C0EEF6D1-5DF7-4A26-9964-6376F465B085}.Debug|Any CPU.Build.0 = Debug|Any CPU + {C0EEF6D1-5DF7-4A26-9964-6376F465B085}.Debug|x64.ActiveCfg = Debug|Any CPU + {C0EEF6D1-5DF7-4A26-9964-6376F465B085}.Debug|x64.Build.0 = Debug|Any CPU + {C0EEF6D1-5DF7-4A26-9964-6376F465B085}.Debug|x86.ActiveCfg = Debug|Any CPU + {C0EEF6D1-5DF7-4A26-9964-6376F465B085}.Debug|x86.Build.0 = Debug|Any CPU {C0EEF6D1-5DF7-4A26-9964-6376F465B085}.Release|Any CPU.ActiveCfg = Release|Any CPU {C0EEF6D1-5DF7-4A26-9964-6376F465B085}.Release|Any CPU.Build.0 = Release|Any CPU + {C0EEF6D1-5DF7-4A26-9964-6376F465B085}.Release|x64.ActiveCfg = Release|Any CPU + {C0EEF6D1-5DF7-4A26-9964-6376F465B085}.Release|x64.Build.0 = Release|Any CPU + {C0EEF6D1-5DF7-4A26-9964-6376F465B085}.Release|x86.ActiveCfg = Release|Any CPU + {C0EEF6D1-5DF7-4A26-9964-6376F465B085}.Release|x86.Build.0 = Release|Any CPU {F6473A7A-01B7-4A9A-B30C-4D200BFFFE4D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {F6473A7A-01B7-4A9A-B30C-4D200BFFFE4D}.Debug|x64.ActiveCfg = Debug|Any CPU + {F6473A7A-01B7-4A9A-B30C-4D200BFFFE4D}.Debug|x64.Build.0 = Debug|Any CPU + {F6473A7A-01B7-4A9A-B30C-4D200BFFFE4D}.Debug|x86.ActiveCfg = Debug|Any CPU + {F6473A7A-01B7-4A9A-B30C-4D200BFFFE4D}.Debug|x86.Build.0 = Debug|Any CPU {F6473A7A-01B7-4A9A-B30C-4D200BFFFE4D}.Release|Any CPU.ActiveCfg = Release|Any CPU + {F6473A7A-01B7-4A9A-B30C-4D200BFFFE4D}.Release|x64.ActiveCfg = Release|Any CPU + {F6473A7A-01B7-4A9A-B30C-4D200BFFFE4D}.Release|x64.Build.0 = Release|Any CPU + {F6473A7A-01B7-4A9A-B30C-4D200BFFFE4D}.Release|x86.ActiveCfg = Release|Any CPU + {F6473A7A-01B7-4A9A-B30C-4D200BFFFE4D}.Release|x86.Build.0 = Release|Any CPU {C7EC4072-F74C-410D-B0C3-4C05B7C9CFB7}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {C7EC4072-F74C-410D-B0C3-4C05B7C9CFB7}.Debug|Any CPU.Build.0 = Debug|Any CPU + {C7EC4072-F74C-410D-B0C3-4C05B7C9CFB7}.Debug|x64.ActiveCfg = Debug|Any CPU + {C7EC4072-F74C-410D-B0C3-4C05B7C9CFB7}.Debug|x64.Build.0 = Debug|Any CPU + {C7EC4072-F74C-410D-B0C3-4C05B7C9CFB7}.Debug|x86.ActiveCfg = Debug|Any CPU + {C7EC4072-F74C-410D-B0C3-4C05B7C9CFB7}.Debug|x86.Build.0 = Debug|Any CPU {C7EC4072-F74C-410D-B0C3-4C05B7C9CFB7}.Release|Any CPU.ActiveCfg = Release|Any CPU {C7EC4072-F74C-410D-B0C3-4C05B7C9CFB7}.Release|Any CPU.Build.0 = Release|Any CPU + {C7EC4072-F74C-410D-B0C3-4C05B7C9CFB7}.Release|x64.ActiveCfg = Release|Any CPU + {C7EC4072-F74C-410D-B0C3-4C05B7C9CFB7}.Release|x64.Build.0 = Release|Any CPU + {C7EC4072-F74C-410D-B0C3-4C05B7C9CFB7}.Release|x86.ActiveCfg = Release|Any CPU + {C7EC4072-F74C-410D-B0C3-4C05B7C9CFB7}.Release|x86.Build.0 = Release|Any CPU {20284C68-6381-4AE9-832C-F094E996D913}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {20284C68-6381-4AE9-832C-F094E996D913}.Debug|Any CPU.Build.0 = Debug|Any CPU + {20284C68-6381-4AE9-832C-F094E996D913}.Debug|x64.ActiveCfg = Debug|Any CPU + {20284C68-6381-4AE9-832C-F094E996D913}.Debug|x64.Build.0 = Debug|Any CPU + {20284C68-6381-4AE9-832C-F094E996D913}.Debug|x86.ActiveCfg = Debug|Any CPU + {20284C68-6381-4AE9-832C-F094E996D913}.Debug|x86.Build.0 = Debug|Any CPU {20284C68-6381-4AE9-832C-F094E996D913}.Release|Any CPU.ActiveCfg = Release|Any CPU {20284C68-6381-4AE9-832C-F094E996D913}.Release|Any CPU.Build.0 = Release|Any CPU + {20284C68-6381-4AE9-832C-F094E996D913}.Release|x64.ActiveCfg = Release|Any CPU + {20284C68-6381-4AE9-832C-F094E996D913}.Release|x64.Build.0 = Release|Any CPU + {20284C68-6381-4AE9-832C-F094E996D913}.Release|x86.ActiveCfg = Release|Any CPU + {20284C68-6381-4AE9-832C-F094E996D913}.Release|x86.Build.0 = Release|Any CPU {AECE0265-F6E3-49A7-9F45-8847D359F05D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {AECE0265-F6E3-49A7-9F45-8847D359F05D}.Debug|Any CPU.Build.0 = Debug|Any CPU + {AECE0265-F6E3-49A7-9F45-8847D359F05D}.Debug|x64.ActiveCfg = Debug|Any CPU + {AECE0265-F6E3-49A7-9F45-8847D359F05D}.Debug|x64.Build.0 = Debug|Any CPU + {AECE0265-F6E3-49A7-9F45-8847D359F05D}.Debug|x86.ActiveCfg = Debug|Any CPU + {AECE0265-F6E3-49A7-9F45-8847D359F05D}.Debug|x86.Build.0 = Debug|Any CPU {AECE0265-F6E3-49A7-9F45-8847D359F05D}.Release|Any CPU.ActiveCfg = Release|Any CPU {AECE0265-F6E3-49A7-9F45-8847D359F05D}.Release|Any CPU.Build.0 = Release|Any CPU + {AECE0265-F6E3-49A7-9F45-8847D359F05D}.Release|x64.ActiveCfg = Release|Any CPU + {AECE0265-F6E3-49A7-9F45-8847D359F05D}.Release|x64.Build.0 = Release|Any CPU + {AECE0265-F6E3-49A7-9F45-8847D359F05D}.Release|x86.ActiveCfg = Release|Any CPU + {AECE0265-F6E3-49A7-9F45-8847D359F05D}.Release|x86.Build.0 = Release|Any CPU {0A6BF34E-B7C3-4D3D-908F-BA74DAC4743F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {0A6BF34E-B7C3-4D3D-908F-BA74DAC4743F}.Debug|Any CPU.Build.0 = Debug|Any CPU + {0A6BF34E-B7C3-4D3D-908F-BA74DAC4743F}.Debug|x64.ActiveCfg = Debug|Any CPU + {0A6BF34E-B7C3-4D3D-908F-BA74DAC4743F}.Debug|x64.Build.0 = Debug|Any CPU + {0A6BF34E-B7C3-4D3D-908F-BA74DAC4743F}.Debug|x86.ActiveCfg = Debug|Any CPU + {0A6BF34E-B7C3-4D3D-908F-BA74DAC4743F}.Debug|x86.Build.0 = Debug|Any CPU {0A6BF34E-B7C3-4D3D-908F-BA74DAC4743F}.Release|Any CPU.ActiveCfg = Release|Any CPU {0A6BF34E-B7C3-4D3D-908F-BA74DAC4743F}.Release|Any CPU.Build.0 = Release|Any CPU + {0A6BF34E-B7C3-4D3D-908F-BA74DAC4743F}.Release|x64.ActiveCfg = Release|Any CPU + {0A6BF34E-B7C3-4D3D-908F-BA74DAC4743F}.Release|x64.Build.0 = Release|Any CPU + {0A6BF34E-B7C3-4D3D-908F-BA74DAC4743F}.Release|x86.ActiveCfg = Release|Any CPU + {0A6BF34E-B7C3-4D3D-908F-BA74DAC4743F}.Release|x86.Build.0 = Release|Any CPU {114E51C5-8EB1-44C6-AC5A-78C8EF6C7D61}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {114E51C5-8EB1-44C6-AC5A-78C8EF6C7D61}.Debug|Any CPU.Build.0 = Debug|Any CPU + {114E51C5-8EB1-44C6-AC5A-78C8EF6C7D61}.Debug|x64.ActiveCfg = Debug|Any CPU + {114E51C5-8EB1-44C6-AC5A-78C8EF6C7D61}.Debug|x64.Build.0 = Debug|Any CPU + {114E51C5-8EB1-44C6-AC5A-78C8EF6C7D61}.Debug|x86.ActiveCfg = Debug|Any CPU + {114E51C5-8EB1-44C6-AC5A-78C8EF6C7D61}.Debug|x86.Build.0 = Debug|Any CPU {114E51C5-8EB1-44C6-AC5A-78C8EF6C7D61}.Release|Any CPU.ActiveCfg = Release|Any CPU {114E51C5-8EB1-44C6-AC5A-78C8EF6C7D61}.Release|Any CPU.Build.0 = Release|Any CPU + {114E51C5-8EB1-44C6-AC5A-78C8EF6C7D61}.Release|x64.ActiveCfg = Release|Any CPU + {114E51C5-8EB1-44C6-AC5A-78C8EF6C7D61}.Release|x64.Build.0 = Release|Any CPU + {114E51C5-8EB1-44C6-AC5A-78C8EF6C7D61}.Release|x86.ActiveCfg = Release|Any CPU + {114E51C5-8EB1-44C6-AC5A-78C8EF6C7D61}.Release|x86.Build.0 = Release|Any CPU {691074CB-67F0-45CE-B3DE-AC9A94AF29B4}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {691074CB-67F0-45CE-B3DE-AC9A94AF29B4}.Debug|Any CPU.Build.0 = Debug|Any CPU + {691074CB-67F0-45CE-B3DE-AC9A94AF29B4}.Debug|x64.ActiveCfg = Debug|Any CPU + {691074CB-67F0-45CE-B3DE-AC9A94AF29B4}.Debug|x64.Build.0 = Debug|Any CPU + {691074CB-67F0-45CE-B3DE-AC9A94AF29B4}.Debug|x86.ActiveCfg = Debug|Any CPU + {691074CB-67F0-45CE-B3DE-AC9A94AF29B4}.Debug|x86.Build.0 = Debug|Any CPU {691074CB-67F0-45CE-B3DE-AC9A94AF29B4}.Release|Any CPU.ActiveCfg = Release|Any CPU {691074CB-67F0-45CE-B3DE-AC9A94AF29B4}.Release|Any CPU.Build.0 = Release|Any CPU + {691074CB-67F0-45CE-B3DE-AC9A94AF29B4}.Release|x64.ActiveCfg = Release|Any CPU + {691074CB-67F0-45CE-B3DE-AC9A94AF29B4}.Release|x64.Build.0 = Release|Any CPU + {691074CB-67F0-45CE-B3DE-AC9A94AF29B4}.Release|x86.ActiveCfg = Release|Any CPU + {691074CB-67F0-45CE-B3DE-AC9A94AF29B4}.Release|x86.Build.0 = Release|Any CPU {46F65684-6439-4199-806E-9EA4CA0AC31C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {46F65684-6439-4199-806E-9EA4CA0AC31C}.Debug|Any CPU.Build.0 = Debug|Any CPU + {46F65684-6439-4199-806E-9EA4CA0AC31C}.Debug|x64.ActiveCfg = Debug|Any CPU + {46F65684-6439-4199-806E-9EA4CA0AC31C}.Debug|x64.Build.0 = Debug|Any CPU + {46F65684-6439-4199-806E-9EA4CA0AC31C}.Debug|x86.ActiveCfg = Debug|Any CPU + {46F65684-6439-4199-806E-9EA4CA0AC31C}.Debug|x86.Build.0 = Debug|Any CPU {46F65684-6439-4199-806E-9EA4CA0AC31C}.Release|Any CPU.ActiveCfg = Release|Any CPU {46F65684-6439-4199-806E-9EA4CA0AC31C}.Release|Any CPU.Build.0 = Release|Any CPU + {46F65684-6439-4199-806E-9EA4CA0AC31C}.Release|x64.ActiveCfg = Release|Any CPU + {46F65684-6439-4199-806E-9EA4CA0AC31C}.Release|x64.Build.0 = Release|Any CPU + {46F65684-6439-4199-806E-9EA4CA0AC31C}.Release|x86.ActiveCfg = Release|Any CPU + {46F65684-6439-4199-806E-9EA4CA0AC31C}.Release|x86.Build.0 = Release|Any CPU {EDA26C67-405A-4EC7-A3E6-EBAB7278E70A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {EDA26C67-405A-4EC7-A3E6-EBAB7278E70A}.Debug|Any CPU.Build.0 = Debug|Any CPU + {EDA26C67-405A-4EC7-A3E6-EBAB7278E70A}.Debug|x64.ActiveCfg = Debug|Any CPU + {EDA26C67-405A-4EC7-A3E6-EBAB7278E70A}.Debug|x64.Build.0 = Debug|Any CPU + {EDA26C67-405A-4EC7-A3E6-EBAB7278E70A}.Debug|x86.ActiveCfg = Debug|Any CPU + {EDA26C67-405A-4EC7-A3E6-EBAB7278E70A}.Debug|x86.Build.0 = Debug|Any CPU {EDA26C67-405A-4EC7-A3E6-EBAB7278E70A}.Release|Any CPU.ActiveCfg = Release|Any CPU {EDA26C67-405A-4EC7-A3E6-EBAB7278E70A}.Release|Any CPU.Build.0 = Release|Any CPU + {EDA26C67-405A-4EC7-A3E6-EBAB7278E70A}.Release|x64.ActiveCfg = Release|Any CPU + {EDA26C67-405A-4EC7-A3E6-EBAB7278E70A}.Release|x64.Build.0 = Release|Any CPU + {EDA26C67-405A-4EC7-A3E6-EBAB7278E70A}.Release|x86.ActiveCfg = Release|Any CPU + {EDA26C67-405A-4EC7-A3E6-EBAB7278E70A}.Release|x86.Build.0 = Release|Any CPU {24C5A540-EE02-4519-B000-EDA470759C5B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {24C5A540-EE02-4519-B000-EDA470759C5B}.Debug|Any CPU.Build.0 = Debug|Any CPU + {24C5A540-EE02-4519-B000-EDA470759C5B}.Debug|x64.ActiveCfg = Debug|Any CPU + {24C5A540-EE02-4519-B000-EDA470759C5B}.Debug|x64.Build.0 = Debug|Any CPU + {24C5A540-EE02-4519-B000-EDA470759C5B}.Debug|x86.ActiveCfg = Debug|Any CPU + {24C5A540-EE02-4519-B000-EDA470759C5B}.Debug|x86.Build.0 = Debug|Any CPU {24C5A540-EE02-4519-B000-EDA470759C5B}.Release|Any CPU.ActiveCfg = Release|Any CPU {24C5A540-EE02-4519-B000-EDA470759C5B}.Release|Any CPU.Build.0 = Release|Any CPU + {24C5A540-EE02-4519-B000-EDA470759C5B}.Release|x64.ActiveCfg = Release|Any CPU + {24C5A540-EE02-4519-B000-EDA470759C5B}.Release|x64.Build.0 = Release|Any CPU + {24C5A540-EE02-4519-B000-EDA470759C5B}.Release|x86.ActiveCfg = Release|Any CPU + {24C5A540-EE02-4519-B000-EDA470759C5B}.Release|x86.Build.0 = Release|Any CPU {3D9E3A53-164F-4430-A6A4-D35551F5DC88}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {3D9E3A53-164F-4430-A6A4-D35551F5DC88}.Debug|Any CPU.Build.0 = Debug|Any CPU + {3D9E3A53-164F-4430-A6A4-D35551F5DC88}.Debug|x64.ActiveCfg = Debug|Any CPU + {3D9E3A53-164F-4430-A6A4-D35551F5DC88}.Debug|x64.Build.0 = Debug|Any CPU + {3D9E3A53-164F-4430-A6A4-D35551F5DC88}.Debug|x86.ActiveCfg = Debug|Any CPU + {3D9E3A53-164F-4430-A6A4-D35551F5DC88}.Debug|x86.Build.0 = Debug|Any CPU {3D9E3A53-164F-4430-A6A4-D35551F5DC88}.Release|Any CPU.ActiveCfg = Release|Any CPU {3D9E3A53-164F-4430-A6A4-D35551F5DC88}.Release|Any CPU.Build.0 = Release|Any CPU + {3D9E3A53-164F-4430-A6A4-D35551F5DC88}.Release|x64.ActiveCfg = Release|Any CPU + {3D9E3A53-164F-4430-A6A4-D35551F5DC88}.Release|x64.Build.0 = Release|Any CPU + {3D9E3A53-164F-4430-A6A4-D35551F5DC88}.Release|x86.ActiveCfg = Release|Any CPU + {3D9E3A53-164F-4430-A6A4-D35551F5DC88}.Release|x86.Build.0 = Release|Any CPU {EFB42D88-E935-4601-AC62-62E68F81B3C3}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {EFB42D88-E935-4601-AC62-62E68F81B3C3}.Debug|Any CPU.Build.0 = Debug|Any CPU + {EFB42D88-E935-4601-AC62-62E68F81B3C3}.Debug|x64.ActiveCfg = Debug|Any CPU + {EFB42D88-E935-4601-AC62-62E68F81B3C3}.Debug|x64.Build.0 = Debug|Any CPU + {EFB42D88-E935-4601-AC62-62E68F81B3C3}.Debug|x86.ActiveCfg = Debug|Any CPU + {EFB42D88-E935-4601-AC62-62E68F81B3C3}.Debug|x86.Build.0 = Debug|Any CPU {EFB42D88-E935-4601-AC62-62E68F81B3C3}.Release|Any CPU.ActiveCfg = Release|Any CPU {EFB42D88-E935-4601-AC62-62E68F81B3C3}.Release|Any CPU.Build.0 = Release|Any CPU + {EFB42D88-E935-4601-AC62-62E68F81B3C3}.Release|x64.ActiveCfg = Release|Any CPU + {EFB42D88-E935-4601-AC62-62E68F81B3C3}.Release|x64.Build.0 = Release|Any CPU + {EFB42D88-E935-4601-AC62-62E68F81B3C3}.Release|x86.ActiveCfg = Release|Any CPU + {EFB42D88-E935-4601-AC62-62E68F81B3C3}.Release|x86.Build.0 = Release|Any CPU {E1F20A07-DA6E-460C-ADC8-87BD1D1D8300}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {E1F20A07-DA6E-460C-ADC8-87BD1D1D8300}.Debug|Any CPU.Build.0 = Debug|Any CPU + {E1F20A07-DA6E-460C-ADC8-87BD1D1D8300}.Debug|x64.ActiveCfg = Debug|Any CPU + {E1F20A07-DA6E-460C-ADC8-87BD1D1D8300}.Debug|x64.Build.0 = Debug|Any CPU + {E1F20A07-DA6E-460C-ADC8-87BD1D1D8300}.Debug|x86.ActiveCfg = Debug|Any CPU + {E1F20A07-DA6E-460C-ADC8-87BD1D1D8300}.Debug|x86.Build.0 = Debug|Any CPU {E1F20A07-DA6E-460C-ADC8-87BD1D1D8300}.Release|Any CPU.ActiveCfg = Release|Any CPU {E1F20A07-DA6E-460C-ADC8-87BD1D1D8300}.Release|Any CPU.Build.0 = Release|Any CPU + {E1F20A07-DA6E-460C-ADC8-87BD1D1D8300}.Release|x64.ActiveCfg = Release|Any CPU + {E1F20A07-DA6E-460C-ADC8-87BD1D1D8300}.Release|x64.Build.0 = Release|Any CPU + {E1F20A07-DA6E-460C-ADC8-87BD1D1D8300}.Release|x86.ActiveCfg = Release|Any CPU + {E1F20A07-DA6E-460C-ADC8-87BD1D1D8300}.Release|x86.Build.0 = Release|Any CPU {D3BA37DC-FB3C-4ACC-9A4D-4CBBB7BF6A99}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {D3BA37DC-FB3C-4ACC-9A4D-4CBBB7BF6A99}.Debug|Any CPU.Build.0 = Debug|Any CPU + {D3BA37DC-FB3C-4ACC-9A4D-4CBBB7BF6A99}.Debug|x64.ActiveCfg = Debug|Any CPU + {D3BA37DC-FB3C-4ACC-9A4D-4CBBB7BF6A99}.Debug|x64.Build.0 = Debug|Any CPU + {D3BA37DC-FB3C-4ACC-9A4D-4CBBB7BF6A99}.Debug|x86.ActiveCfg = Debug|Any CPU + {D3BA37DC-FB3C-4ACC-9A4D-4CBBB7BF6A99}.Debug|x86.Build.0 = Debug|Any CPU {D3BA37DC-FB3C-4ACC-9A4D-4CBBB7BF6A99}.Release|Any CPU.ActiveCfg = Release|Any CPU {D3BA37DC-FB3C-4ACC-9A4D-4CBBB7BF6A99}.Release|Any CPU.Build.0 = Release|Any CPU + {D3BA37DC-FB3C-4ACC-9A4D-4CBBB7BF6A99}.Release|x64.ActiveCfg = Release|Any CPU + {D3BA37DC-FB3C-4ACC-9A4D-4CBBB7BF6A99}.Release|x64.Build.0 = Release|Any CPU + {D3BA37DC-FB3C-4ACC-9A4D-4CBBB7BF6A99}.Release|x86.ActiveCfg = Release|Any CPU + {D3BA37DC-FB3C-4ACC-9A4D-4CBBB7BF6A99}.Release|x86.Build.0 = Release|Any CPU {DEDC8155-F9EF-4A8D-9476-B44610AABCA1}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {DEDC8155-F9EF-4A8D-9476-B44610AABCA1}.Debug|Any CPU.Build.0 = Debug|Any CPU + {DEDC8155-F9EF-4A8D-9476-B44610AABCA1}.Debug|x64.ActiveCfg = Debug|Any CPU + {DEDC8155-F9EF-4A8D-9476-B44610AABCA1}.Debug|x64.Build.0 = Debug|Any CPU + {DEDC8155-F9EF-4A8D-9476-B44610AABCA1}.Debug|x86.ActiveCfg = Debug|Any CPU + {DEDC8155-F9EF-4A8D-9476-B44610AABCA1}.Debug|x86.Build.0 = Debug|Any CPU {DEDC8155-F9EF-4A8D-9476-B44610AABCA1}.Release|Any CPU.ActiveCfg = Release|Any CPU {DEDC8155-F9EF-4A8D-9476-B44610AABCA1}.Release|Any CPU.Build.0 = Release|Any CPU + {DEDC8155-F9EF-4A8D-9476-B44610AABCA1}.Release|x64.ActiveCfg = Release|Any CPU + {DEDC8155-F9EF-4A8D-9476-B44610AABCA1}.Release|x64.Build.0 = Release|Any CPU + {DEDC8155-F9EF-4A8D-9476-B44610AABCA1}.Release|x86.ActiveCfg = Release|Any CPU + {DEDC8155-F9EF-4A8D-9476-B44610AABCA1}.Release|x86.Build.0 = Release|Any CPU {97A45FD5-CDBC-4BFC-AECB-6C09D8DE5A26}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {97A45FD5-CDBC-4BFC-AECB-6C09D8DE5A26}.Debug|Any CPU.Build.0 = Debug|Any CPU + {97A45FD5-CDBC-4BFC-AECB-6C09D8DE5A26}.Debug|x64.ActiveCfg = Debug|Any CPU + {97A45FD5-CDBC-4BFC-AECB-6C09D8DE5A26}.Debug|x64.Build.0 = Debug|Any CPU + {97A45FD5-CDBC-4BFC-AECB-6C09D8DE5A26}.Debug|x86.ActiveCfg = Debug|Any CPU + {97A45FD5-CDBC-4BFC-AECB-6C09D8DE5A26}.Debug|x86.Build.0 = Debug|Any CPU {97A45FD5-CDBC-4BFC-AECB-6C09D8DE5A26}.Release|Any CPU.ActiveCfg = Release|Any CPU {97A45FD5-CDBC-4BFC-AECB-6C09D8DE5A26}.Release|Any CPU.Build.0 = Release|Any CPU + {97A45FD5-CDBC-4BFC-AECB-6C09D8DE5A26}.Release|x64.ActiveCfg = Release|Any CPU + {97A45FD5-CDBC-4BFC-AECB-6C09D8DE5A26}.Release|x64.Build.0 = Release|Any CPU + {97A45FD5-CDBC-4BFC-AECB-6C09D8DE5A26}.Release|x86.ActiveCfg = Release|Any CPU + {97A45FD5-CDBC-4BFC-AECB-6C09D8DE5A26}.Release|x86.Build.0 = Release|Any CPU {F8511FED-7CA1-459C-8451-FDDFD721F2ED}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {F8511FED-7CA1-459C-8451-FDDFD721F2ED}.Debug|Any CPU.Build.0 = Debug|Any CPU + {F8511FED-7CA1-459C-8451-FDDFD721F2ED}.Debug|x64.ActiveCfg = Debug|Any CPU + {F8511FED-7CA1-459C-8451-FDDFD721F2ED}.Debug|x64.Build.0 = Debug|Any CPU + {F8511FED-7CA1-459C-8451-FDDFD721F2ED}.Debug|x86.ActiveCfg = Debug|Any CPU + {F8511FED-7CA1-459C-8451-FDDFD721F2ED}.Debug|x86.Build.0 = Debug|Any CPU {F8511FED-7CA1-459C-8451-FDDFD721F2ED}.Release|Any CPU.ActiveCfg = Release|Any CPU {F8511FED-7CA1-459C-8451-FDDFD721F2ED}.Release|Any CPU.Build.0 = Release|Any CPU + {F8511FED-7CA1-459C-8451-FDDFD721F2ED}.Release|x64.ActiveCfg = Release|Any CPU + {F8511FED-7CA1-459C-8451-FDDFD721F2ED}.Release|x64.Build.0 = Release|Any CPU + {F8511FED-7CA1-459C-8451-FDDFD721F2ED}.Release|x86.ActiveCfg = Release|Any CPU + {F8511FED-7CA1-459C-8451-FDDFD721F2ED}.Release|x86.Build.0 = Release|Any CPU {943F6EFD-5054-48F4-AC34-2235FD5B79F0}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {943F6EFD-5054-48F4-AC34-2235FD5B79F0}.Debug|Any CPU.Build.0 = Debug|Any CPU + {943F6EFD-5054-48F4-AC34-2235FD5B79F0}.Debug|x64.ActiveCfg = Debug|Any CPU + {943F6EFD-5054-48F4-AC34-2235FD5B79F0}.Debug|x64.Build.0 = Debug|Any CPU + {943F6EFD-5054-48F4-AC34-2235FD5B79F0}.Debug|x86.ActiveCfg = Debug|Any CPU + {943F6EFD-5054-48F4-AC34-2235FD5B79F0}.Debug|x86.Build.0 = Debug|Any CPU {943F6EFD-5054-48F4-AC34-2235FD5B79F0}.Release|Any CPU.ActiveCfg = Release|Any CPU {943F6EFD-5054-48F4-AC34-2235FD5B79F0}.Release|Any CPU.Build.0 = Release|Any CPU + {943F6EFD-5054-48F4-AC34-2235FD5B79F0}.Release|x64.ActiveCfg = Release|Any CPU + {943F6EFD-5054-48F4-AC34-2235FD5B79F0}.Release|x64.Build.0 = Release|Any CPU + {943F6EFD-5054-48F4-AC34-2235FD5B79F0}.Release|x86.ActiveCfg = Release|Any CPU + {943F6EFD-5054-48F4-AC34-2235FD5B79F0}.Release|x86.Build.0 = Release|Any CPU {A70BB055-CE03-4C2E-8527-AC59B0EF3192}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {A70BB055-CE03-4C2E-8527-AC59B0EF3192}.Debug|Any CPU.Build.0 = Debug|Any CPU + {A70BB055-CE03-4C2E-8527-AC59B0EF3192}.Debug|x64.ActiveCfg = Debug|Any CPU + {A70BB055-CE03-4C2E-8527-AC59B0EF3192}.Debug|x64.Build.0 = Debug|Any CPU + {A70BB055-CE03-4C2E-8527-AC59B0EF3192}.Debug|x86.ActiveCfg = Debug|Any CPU + {A70BB055-CE03-4C2E-8527-AC59B0EF3192}.Debug|x86.Build.0 = Debug|Any CPU {A70BB055-CE03-4C2E-8527-AC59B0EF3192}.Release|Any CPU.ActiveCfg = Release|Any CPU {A70BB055-CE03-4C2E-8527-AC59B0EF3192}.Release|Any CPU.Build.0 = Release|Any CPU + {A70BB055-CE03-4C2E-8527-AC59B0EF3192}.Release|x64.ActiveCfg = Release|Any CPU + {A70BB055-CE03-4C2E-8527-AC59B0EF3192}.Release|x64.Build.0 = Release|Any CPU + {A70BB055-CE03-4C2E-8527-AC59B0EF3192}.Release|x86.ActiveCfg = Release|Any CPU + {A70BB055-CE03-4C2E-8527-AC59B0EF3192}.Release|x86.Build.0 = Release|Any CPU {C9020438-FCBF-4323-88A2-2901B831D358}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {C9020438-FCBF-4323-88A2-2901B831D358}.Debug|Any CPU.Build.0 = Debug|Any CPU + {C9020438-FCBF-4323-88A2-2901B831D358}.Debug|x64.ActiveCfg = Debug|Any CPU + {C9020438-FCBF-4323-88A2-2901B831D358}.Debug|x64.Build.0 = Debug|Any CPU + {C9020438-FCBF-4323-88A2-2901B831D358}.Debug|x86.ActiveCfg = Debug|Any CPU + {C9020438-FCBF-4323-88A2-2901B831D358}.Debug|x86.Build.0 = Debug|Any CPU {C9020438-FCBF-4323-88A2-2901B831D358}.Release|Any CPU.ActiveCfg = Release|Any CPU {C9020438-FCBF-4323-88A2-2901B831D358}.Release|Any CPU.Build.0 = Release|Any CPU + {C9020438-FCBF-4323-88A2-2901B831D358}.Release|x64.ActiveCfg = Release|Any CPU + {C9020438-FCBF-4323-88A2-2901B831D358}.Release|x64.Build.0 = Release|Any CPU + {C9020438-FCBF-4323-88A2-2901B831D358}.Release|x86.ActiveCfg = Release|Any CPU + {C9020438-FCBF-4323-88A2-2901B831D358}.Release|x86.Build.0 = Release|Any CPU {BA42A516-AC01-4B4E-9177-B62AB2CFC367}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {BA42A516-AC01-4B4E-9177-B62AB2CFC367}.Debug|Any CPU.Build.0 = Debug|Any CPU + {BA42A516-AC01-4B4E-9177-B62AB2CFC367}.Debug|x64.ActiveCfg = Debug|Any CPU + {BA42A516-AC01-4B4E-9177-B62AB2CFC367}.Debug|x64.Build.0 = Debug|Any CPU + {BA42A516-AC01-4B4E-9177-B62AB2CFC367}.Debug|x86.ActiveCfg = Debug|Any CPU + {BA42A516-AC01-4B4E-9177-B62AB2CFC367}.Debug|x86.Build.0 = Debug|Any CPU {BA42A516-AC01-4B4E-9177-B62AB2CFC367}.Release|Any CPU.ActiveCfg = Release|Any CPU {BA42A516-AC01-4B4E-9177-B62AB2CFC367}.Release|Any CPU.Build.0 = Release|Any CPU + {BA42A516-AC01-4B4E-9177-B62AB2CFC367}.Release|x64.ActiveCfg = Release|Any CPU + {BA42A516-AC01-4B4E-9177-B62AB2CFC367}.Release|x64.Build.0 = Release|Any CPU + {BA42A516-AC01-4B4E-9177-B62AB2CFC367}.Release|x86.ActiveCfg = Release|Any CPU + {BA42A516-AC01-4B4E-9177-B62AB2CFC367}.Release|x86.Build.0 = Release|Any CPU {27C1CD4C-524D-4B28-9931-02A0D79F856B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {27C1CD4C-524D-4B28-9931-02A0D79F856B}.Debug|Any CPU.Build.0 = Debug|Any CPU + {27C1CD4C-524D-4B28-9931-02A0D79F856B}.Debug|x64.ActiveCfg = Debug|Any CPU + {27C1CD4C-524D-4B28-9931-02A0D79F856B}.Debug|x64.Build.0 = Debug|Any CPU + {27C1CD4C-524D-4B28-9931-02A0D79F856B}.Debug|x86.ActiveCfg = Debug|Any CPU + {27C1CD4C-524D-4B28-9931-02A0D79F856B}.Debug|x86.Build.0 = Debug|Any CPU {27C1CD4C-524D-4B28-9931-02A0D79F856B}.Release|Any CPU.ActiveCfg = Release|Any CPU {27C1CD4C-524D-4B28-9931-02A0D79F856B}.Release|Any CPU.Build.0 = Release|Any CPU + {27C1CD4C-524D-4B28-9931-02A0D79F856B}.Release|x64.ActiveCfg = Release|Any CPU + {27C1CD4C-524D-4B28-9931-02A0D79F856B}.Release|x64.Build.0 = Release|Any CPU + {27C1CD4C-524D-4B28-9931-02A0D79F856B}.Release|x86.ActiveCfg = Release|Any CPU + {27C1CD4C-524D-4B28-9931-02A0D79F856B}.Release|x86.Build.0 = Release|Any CPU {0E78A541-18AF-4B50-8F05-23CA25BBB793}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {0E78A541-18AF-4B50-8F05-23CA25BBB793}.Debug|Any CPU.Build.0 = Debug|Any CPU + {0E78A541-18AF-4B50-8F05-23CA25BBB793}.Debug|x64.ActiveCfg = Debug|Any CPU + {0E78A541-18AF-4B50-8F05-23CA25BBB793}.Debug|x64.Build.0 = Debug|Any CPU + {0E78A541-18AF-4B50-8F05-23CA25BBB793}.Debug|x86.ActiveCfg = Debug|Any CPU + {0E78A541-18AF-4B50-8F05-23CA25BBB793}.Debug|x86.Build.0 = Debug|Any CPU {0E78A541-18AF-4B50-8F05-23CA25BBB793}.Release|Any CPU.ActiveCfg = Release|Any CPU {0E78A541-18AF-4B50-8F05-23CA25BBB793}.Release|Any CPU.Build.0 = Release|Any CPU + {0E78A541-18AF-4B50-8F05-23CA25BBB793}.Release|x64.ActiveCfg = Release|Any CPU + {0E78A541-18AF-4B50-8F05-23CA25BBB793}.Release|x64.Build.0 = Release|Any CPU + {0E78A541-18AF-4B50-8F05-23CA25BBB793}.Release|x86.ActiveCfg = Release|Any CPU + {0E78A541-18AF-4B50-8F05-23CA25BBB793}.Release|x86.Build.0 = Release|Any CPU {1A3C57ED-8948-4B30-8C3B-F4E00C7236D6}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {1A3C57ED-8948-4B30-8C3B-F4E00C7236D6}.Debug|Any CPU.Build.0 = Debug|Any CPU + {1A3C57ED-8948-4B30-8C3B-F4E00C7236D6}.Debug|x64.ActiveCfg = Debug|Any CPU + {1A3C57ED-8948-4B30-8C3B-F4E00C7236D6}.Debug|x64.Build.0 = Debug|Any CPU + {1A3C57ED-8948-4B30-8C3B-F4E00C7236D6}.Debug|x86.ActiveCfg = Debug|Any CPU + {1A3C57ED-8948-4B30-8C3B-F4E00C7236D6}.Debug|x86.Build.0 = Debug|Any CPU {1A3C57ED-8948-4B30-8C3B-F4E00C7236D6}.Release|Any CPU.ActiveCfg = Release|Any CPU {1A3C57ED-8948-4B30-8C3B-F4E00C7236D6}.Release|Any CPU.Build.0 = Release|Any CPU + {1A3C57ED-8948-4B30-8C3B-F4E00C7236D6}.Release|x64.ActiveCfg = Release|Any CPU + {1A3C57ED-8948-4B30-8C3B-F4E00C7236D6}.Release|x64.Build.0 = Release|Any CPU + {1A3C57ED-8948-4B30-8C3B-F4E00C7236D6}.Release|x86.ActiveCfg = Release|Any CPU + {1A3C57ED-8948-4B30-8C3B-F4E00C7236D6}.Release|x86.Build.0 = Release|Any CPU {134DD03F-F7EA-47D3-B7A9-D5514C5D75EE}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {134DD03F-F7EA-47D3-B7A9-D5514C5D75EE}.Debug|Any CPU.Build.0 = Debug|Any CPU + {134DD03F-F7EA-47D3-B7A9-D5514C5D75EE}.Debug|x64.ActiveCfg = Debug|Any CPU + {134DD03F-F7EA-47D3-B7A9-D5514C5D75EE}.Debug|x64.Build.0 = Debug|Any CPU + {134DD03F-F7EA-47D3-B7A9-D5514C5D75EE}.Debug|x86.ActiveCfg = Debug|Any CPU + {134DD03F-F7EA-47D3-B7A9-D5514C5D75EE}.Debug|x86.Build.0 = Debug|Any CPU {134DD03F-F7EA-47D3-B7A9-D5514C5D75EE}.Release|Any CPU.ActiveCfg = Release|Any CPU {134DD03F-F7EA-47D3-B7A9-D5514C5D75EE}.Release|Any CPU.Build.0 = Release|Any CPU + {134DD03F-F7EA-47D3-B7A9-D5514C5D75EE}.Release|x64.ActiveCfg = Release|Any CPU + {134DD03F-F7EA-47D3-B7A9-D5514C5D75EE}.Release|x64.Build.0 = Release|Any CPU + {134DD03F-F7EA-47D3-B7A9-D5514C5D75EE}.Release|x86.ActiveCfg = Release|Any CPU + {134DD03F-F7EA-47D3-B7A9-D5514C5D75EE}.Release|x86.Build.0 = Release|Any CPU {25FD3DDB-B847-490C-8834-C48F828B8B6F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {25FD3DDB-B847-490C-8834-C48F828B8B6F}.Debug|Any CPU.Build.0 = Debug|Any CPU + {25FD3DDB-B847-490C-8834-C48F828B8B6F}.Debug|x64.ActiveCfg = Debug|Any CPU + {25FD3DDB-B847-490C-8834-C48F828B8B6F}.Debug|x64.Build.0 = Debug|Any CPU + {25FD3DDB-B847-490C-8834-C48F828B8B6F}.Debug|x86.ActiveCfg = Debug|Any CPU + {25FD3DDB-B847-490C-8834-C48F828B8B6F}.Debug|x86.Build.0 = Debug|Any CPU {25FD3DDB-B847-490C-8834-C48F828B8B6F}.Release|Any CPU.ActiveCfg = Release|Any CPU {25FD3DDB-B847-490C-8834-C48F828B8B6F}.Release|Any CPU.Build.0 = Release|Any CPU + {25FD3DDB-B847-490C-8834-C48F828B8B6F}.Release|x64.ActiveCfg = Release|Any CPU + {25FD3DDB-B847-490C-8834-C48F828B8B6F}.Release|x64.Build.0 = Release|Any CPU + {25FD3DDB-B847-490C-8834-C48F828B8B6F}.Release|x86.ActiveCfg = Release|Any CPU + {25FD3DDB-B847-490C-8834-C48F828B8B6F}.Release|x86.Build.0 = Release|Any CPU {9CA13A10-1F6F-495A-9623-AC62201FC0BD}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {9CA13A10-1F6F-495A-9623-AC62201FC0BD}.Debug|Any CPU.Build.0 = Debug|Any CPU + {9CA13A10-1F6F-495A-9623-AC62201FC0BD}.Debug|x64.ActiveCfg = Debug|Any CPU + {9CA13A10-1F6F-495A-9623-AC62201FC0BD}.Debug|x64.Build.0 = Debug|Any CPU + {9CA13A10-1F6F-495A-9623-AC62201FC0BD}.Debug|x86.ActiveCfg = Debug|Any CPU + {9CA13A10-1F6F-495A-9623-AC62201FC0BD}.Debug|x86.Build.0 = Debug|Any CPU {9CA13A10-1F6F-495A-9623-AC62201FC0BD}.Release|Any CPU.ActiveCfg = Release|Any CPU {9CA13A10-1F6F-495A-9623-AC62201FC0BD}.Release|Any CPU.Build.0 = Release|Any CPU + {9CA13A10-1F6F-495A-9623-AC62201FC0BD}.Release|x64.ActiveCfg = Release|Any CPU + {9CA13A10-1F6F-495A-9623-AC62201FC0BD}.Release|x64.Build.0 = Release|Any CPU + {9CA13A10-1F6F-495A-9623-AC62201FC0BD}.Release|x86.ActiveCfg = Release|Any CPU + {9CA13A10-1F6F-495A-9623-AC62201FC0BD}.Release|x86.Build.0 = Release|Any CPU {FD1B9998-4B0C-4109-A3BC-0748F829F852}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {FD1B9998-4B0C-4109-A3BC-0748F829F852}.Debug|Any CPU.Build.0 = Debug|Any CPU + {FD1B9998-4B0C-4109-A3BC-0748F829F852}.Debug|x64.ActiveCfg = Debug|Any CPU + {FD1B9998-4B0C-4109-A3BC-0748F829F852}.Debug|x64.Build.0 = Debug|Any CPU + {FD1B9998-4B0C-4109-A3BC-0748F829F852}.Debug|x86.ActiveCfg = Debug|Any CPU + {FD1B9998-4B0C-4109-A3BC-0748F829F852}.Debug|x86.Build.0 = Debug|Any CPU {FD1B9998-4B0C-4109-A3BC-0748F829F852}.Release|Any CPU.ActiveCfg = Release|Any CPU {FD1B9998-4B0C-4109-A3BC-0748F829F852}.Release|Any CPU.Build.0 = Release|Any CPU + {FD1B9998-4B0C-4109-A3BC-0748F829F852}.Release|x64.ActiveCfg = Release|Any CPU + {FD1B9998-4B0C-4109-A3BC-0748F829F852}.Release|x64.Build.0 = Release|Any CPU + {FD1B9998-4B0C-4109-A3BC-0748F829F852}.Release|x86.ActiveCfg = Release|Any CPU + {FD1B9998-4B0C-4109-A3BC-0748F829F852}.Release|x86.Build.0 = Release|Any CPU {56422C80-6A26-46B4-AF5C-84AF08BAB1D1}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {56422C80-6A26-46B4-AF5C-84AF08BAB1D1}.Debug|Any CPU.Build.0 = Debug|Any CPU + {56422C80-6A26-46B4-AF5C-84AF08BAB1D1}.Debug|x64.ActiveCfg = Debug|Any CPU + {56422C80-6A26-46B4-AF5C-84AF08BAB1D1}.Debug|x64.Build.0 = Debug|Any CPU + {56422C80-6A26-46B4-AF5C-84AF08BAB1D1}.Debug|x86.ActiveCfg = Debug|Any CPU + {56422C80-6A26-46B4-AF5C-84AF08BAB1D1}.Debug|x86.Build.0 = Debug|Any CPU {56422C80-6A26-46B4-AF5C-84AF08BAB1D1}.Release|Any CPU.ActiveCfg = Release|Any CPU {56422C80-6A26-46B4-AF5C-84AF08BAB1D1}.Release|Any CPU.Build.0 = Release|Any CPU + {56422C80-6A26-46B4-AF5C-84AF08BAB1D1}.Release|x64.ActiveCfg = Release|Any CPU + {56422C80-6A26-46B4-AF5C-84AF08BAB1D1}.Release|x64.Build.0 = Release|Any CPU + {56422C80-6A26-46B4-AF5C-84AF08BAB1D1}.Release|x86.ActiveCfg = Release|Any CPU + {56422C80-6A26-46B4-AF5C-84AF08BAB1D1}.Release|x86.Build.0 = Release|Any CPU {C536A96A-D4B7-4793-890E-5A29ED7144E5}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {C536A96A-D4B7-4793-890E-5A29ED7144E5}.Debug|Any CPU.Build.0 = Debug|Any CPU + {C536A96A-D4B7-4793-890E-5A29ED7144E5}.Debug|x64.ActiveCfg = Debug|Any CPU + {C536A96A-D4B7-4793-890E-5A29ED7144E5}.Debug|x64.Build.0 = Debug|Any CPU + {C536A96A-D4B7-4793-890E-5A29ED7144E5}.Debug|x86.ActiveCfg = Debug|Any CPU + {C536A96A-D4B7-4793-890E-5A29ED7144E5}.Debug|x86.Build.0 = Debug|Any CPU {C536A96A-D4B7-4793-890E-5A29ED7144E5}.Release|Any CPU.ActiveCfg = Release|Any CPU {C536A96A-D4B7-4793-890E-5A29ED7144E5}.Release|Any CPU.Build.0 = Release|Any CPU - {2A91F751-8432-424C-8D45-7E3BFC4CDBAD}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {2A91F751-8432-424C-8D45-7E3BFC4CDBAD}.Debug|Any CPU.Build.0 = Debug|Any CPU - {2A91F751-8432-424C-8D45-7E3BFC4CDBAD}.Release|Any CPU.ActiveCfg = Release|Any CPU - {2A91F751-8432-424C-8D45-7E3BFC4CDBAD}.Release|Any CPU.Build.0 = Release|Any CPU + {C536A96A-D4B7-4793-890E-5A29ED7144E5}.Release|x64.ActiveCfg = Release|Any CPU + {C536A96A-D4B7-4793-890E-5A29ED7144E5}.Release|x64.Build.0 = Release|Any CPU + {C536A96A-D4B7-4793-890E-5A29ED7144E5}.Release|x86.ActiveCfg = Release|Any CPU + {C536A96A-D4B7-4793-890E-5A29ED7144E5}.Release|x86.Build.0 = Release|Any CPU {07F15F50-0A81-4872-9C7D-34DE88599B9F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {07F15F50-0A81-4872-9C7D-34DE88599B9F}.Debug|Any CPU.Build.0 = Debug|Any CPU + {07F15F50-0A81-4872-9C7D-34DE88599B9F}.Debug|x64.ActiveCfg = Debug|Any CPU + {07F15F50-0A81-4872-9C7D-34DE88599B9F}.Debug|x64.Build.0 = Debug|Any CPU + {07F15F50-0A81-4872-9C7D-34DE88599B9F}.Debug|x86.ActiveCfg = Debug|Any CPU + {07F15F50-0A81-4872-9C7D-34DE88599B9F}.Debug|x86.Build.0 = Debug|Any CPU {07F15F50-0A81-4872-9C7D-34DE88599B9F}.Release|Any CPU.ActiveCfg = Release|Any CPU {07F15F50-0A81-4872-9C7D-34DE88599B9F}.Release|Any CPU.Build.0 = Release|Any CPU - {30038795-181B-488E-8E99-CFF2009FC6D7}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {30038795-181B-488E-8E99-CFF2009FC6D7}.Debug|Any CPU.Build.0 = Debug|Any CPU - {30038795-181B-488E-8E99-CFF2009FC6D7}.Release|Any CPU.ActiveCfg = Release|Any CPU - {30038795-181B-488E-8E99-CFF2009FC6D7}.Release|Any CPU.Build.0 = Release|Any CPU + {07F15F50-0A81-4872-9C7D-34DE88599B9F}.Release|x64.ActiveCfg = Release|Any CPU + {07F15F50-0A81-4872-9C7D-34DE88599B9F}.Release|x64.Build.0 = Release|Any CPU + {07F15F50-0A81-4872-9C7D-34DE88599B9F}.Release|x86.ActiveCfg = Release|Any CPU + {07F15F50-0A81-4872-9C7D-34DE88599B9F}.Release|x86.Build.0 = Release|Any CPU {59B2735E-9B70-4277-8E8C-C8CCB40A097A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {59B2735E-9B70-4277-8E8C-C8CCB40A097A}.Debug|Any CPU.Build.0 = Debug|Any CPU + {59B2735E-9B70-4277-8E8C-C8CCB40A097A}.Debug|x64.ActiveCfg = Debug|Any CPU + {59B2735E-9B70-4277-8E8C-C8CCB40A097A}.Debug|x64.Build.0 = Debug|Any CPU + {59B2735E-9B70-4277-8E8C-C8CCB40A097A}.Debug|x86.ActiveCfg = Debug|Any CPU + {59B2735E-9B70-4277-8E8C-C8CCB40A097A}.Debug|x86.Build.0 = Debug|Any CPU {59B2735E-9B70-4277-8E8C-C8CCB40A097A}.Release|Any CPU.ActiveCfg = Release|Any CPU {59B2735E-9B70-4277-8E8C-C8CCB40A097A}.Release|Any CPU.Build.0 = Release|Any CPU + {59B2735E-9B70-4277-8E8C-C8CCB40A097A}.Release|x64.ActiveCfg = Release|Any CPU + {59B2735E-9B70-4277-8E8C-C8CCB40A097A}.Release|x64.Build.0 = Release|Any CPU + {59B2735E-9B70-4277-8E8C-C8CCB40A097A}.Release|x86.ActiveCfg = Release|Any CPU + {59B2735E-9B70-4277-8E8C-C8CCB40A097A}.Release|x86.Build.0 = Release|Any CPU {C228C9DD-4B2E-4A86-9F4E-09C4A8C2AD61}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {C228C9DD-4B2E-4A86-9F4E-09C4A8C2AD61}.Debug|Any CPU.Build.0 = Debug|Any CPU + {C228C9DD-4B2E-4A86-9F4E-09C4A8C2AD61}.Debug|x64.ActiveCfg = Debug|Any CPU + {C228C9DD-4B2E-4A86-9F4E-09C4A8C2AD61}.Debug|x64.Build.0 = Debug|Any CPU + {C228C9DD-4B2E-4A86-9F4E-09C4A8C2AD61}.Debug|x86.ActiveCfg = Debug|Any CPU + {C228C9DD-4B2E-4A86-9F4E-09C4A8C2AD61}.Debug|x86.Build.0 = Debug|Any CPU {C228C9DD-4B2E-4A86-9F4E-09C4A8C2AD61}.Release|Any CPU.ActiveCfg = Release|Any CPU {C228C9DD-4B2E-4A86-9F4E-09C4A8C2AD61}.Release|Any CPU.Build.0 = Release|Any CPU + {C228C9DD-4B2E-4A86-9F4E-09C4A8C2AD61}.Release|x64.ActiveCfg = Release|Any CPU + {C228C9DD-4B2E-4A86-9F4E-09C4A8C2AD61}.Release|x64.Build.0 = Release|Any CPU + {C228C9DD-4B2E-4A86-9F4E-09C4A8C2AD61}.Release|x86.ActiveCfg = Release|Any CPU + {C228C9DD-4B2E-4A86-9F4E-09C4A8C2AD61}.Release|x86.Build.0 = Release|Any CPU {D0BD5CD8-4928-46CE-B926-8BD3D2D28F20}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {D0BD5CD8-4928-46CE-B926-8BD3D2D28F20}.Debug|Any CPU.Build.0 = Debug|Any CPU + {D0BD5CD8-4928-46CE-B926-8BD3D2D28F20}.Debug|x64.ActiveCfg = Debug|Any CPU + {D0BD5CD8-4928-46CE-B926-8BD3D2D28F20}.Debug|x64.Build.0 = Debug|Any CPU + {D0BD5CD8-4928-46CE-B926-8BD3D2D28F20}.Debug|x86.ActiveCfg = Debug|Any CPU + {D0BD5CD8-4928-46CE-B926-8BD3D2D28F20}.Debug|x86.Build.0 = Debug|Any CPU {D0BD5CD8-4928-46CE-B926-8BD3D2D28F20}.Release|Any CPU.ActiveCfg = Release|Any CPU {D0BD5CD8-4928-46CE-B926-8BD3D2D28F20}.Release|Any CPU.Build.0 = Release|Any CPU + {D0BD5CD8-4928-46CE-B926-8BD3D2D28F20}.Release|x64.ActiveCfg = Release|Any CPU + {D0BD5CD8-4928-46CE-B926-8BD3D2D28F20}.Release|x64.Build.0 = Release|Any CPU + {D0BD5CD8-4928-46CE-B926-8BD3D2D28F20}.Release|x86.ActiveCfg = Release|Any CPU + {D0BD5CD8-4928-46CE-B926-8BD3D2D28F20}.Release|x86.Build.0 = Release|Any CPU {04359CC6-170E-4AB7-8291-FA58AAA9C441}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {04359CC6-170E-4AB7-8291-FA58AAA9C441}.Debug|Any CPU.Build.0 = Debug|Any CPU + {04359CC6-170E-4AB7-8291-FA58AAA9C441}.Debug|x64.ActiveCfg = Debug|Any CPU + {04359CC6-170E-4AB7-8291-FA58AAA9C441}.Debug|x64.Build.0 = Debug|Any CPU + {04359CC6-170E-4AB7-8291-FA58AAA9C441}.Debug|x86.ActiveCfg = Debug|Any CPU + {04359CC6-170E-4AB7-8291-FA58AAA9C441}.Debug|x86.Build.0 = Debug|Any CPU {04359CC6-170E-4AB7-8291-FA58AAA9C441}.Release|Any CPU.ActiveCfg = Release|Any CPU {04359CC6-170E-4AB7-8291-FA58AAA9C441}.Release|Any CPU.Build.0 = Release|Any CPU + {04359CC6-170E-4AB7-8291-FA58AAA9C441}.Release|x64.ActiveCfg = Release|Any CPU + {04359CC6-170E-4AB7-8291-FA58AAA9C441}.Release|x64.Build.0 = Release|Any CPU + {04359CC6-170E-4AB7-8291-FA58AAA9C441}.Release|x86.ActiveCfg = Release|Any CPU + {04359CC6-170E-4AB7-8291-FA58AAA9C441}.Release|x86.Build.0 = Release|Any CPU {7274653C-5E5E-4AA7-A565-5B89F488E1D0}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {7274653C-5E5E-4AA7-A565-5B89F488E1D0}.Debug|Any CPU.Build.0 = Debug|Any CPU + {7274653C-5E5E-4AA7-A565-5B89F488E1D0}.Debug|x64.ActiveCfg = Debug|Any CPU + {7274653C-5E5E-4AA7-A565-5B89F488E1D0}.Debug|x64.Build.0 = Debug|Any CPU + {7274653C-5E5E-4AA7-A565-5B89F488E1D0}.Debug|x86.ActiveCfg = Debug|Any CPU + {7274653C-5E5E-4AA7-A565-5B89F488E1D0}.Debug|x86.Build.0 = Debug|Any CPU {7274653C-5E5E-4AA7-A565-5B89F488E1D0}.Release|Any CPU.ActiveCfg = Release|Any CPU {7274653C-5E5E-4AA7-A565-5B89F488E1D0}.Release|Any CPU.Build.0 = Release|Any CPU + {7274653C-5E5E-4AA7-A565-5B89F488E1D0}.Release|x64.ActiveCfg = Release|Any CPU + {7274653C-5E5E-4AA7-A565-5B89F488E1D0}.Release|x64.Build.0 = Release|Any CPU + {7274653C-5E5E-4AA7-A565-5B89F488E1D0}.Release|x86.ActiveCfg = Release|Any CPU + {7274653C-5E5E-4AA7-A565-5B89F488E1D0}.Release|x86.Build.0 = Release|Any CPU {A67C7407-BB65-4C81-9DE8-65D69A8E97C5}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {A67C7407-BB65-4C81-9DE8-65D69A8E97C5}.Debug|Any CPU.Build.0 = Debug|Any CPU + {A67C7407-BB65-4C81-9DE8-65D69A8E97C5}.Debug|x64.ActiveCfg = Debug|Any CPU + {A67C7407-BB65-4C81-9DE8-65D69A8E97C5}.Debug|x64.Build.0 = Debug|Any CPU + {A67C7407-BB65-4C81-9DE8-65D69A8E97C5}.Debug|x86.ActiveCfg = Debug|Any CPU + {A67C7407-BB65-4C81-9DE8-65D69A8E97C5}.Debug|x86.Build.0 = Debug|Any CPU {A67C7407-BB65-4C81-9DE8-65D69A8E97C5}.Release|Any CPU.ActiveCfg = Release|Any CPU {A67C7407-BB65-4C81-9DE8-65D69A8E97C5}.Release|Any CPU.Build.0 = Release|Any CPU + {A67C7407-BB65-4C81-9DE8-65D69A8E97C5}.Release|x64.ActiveCfg = Release|Any CPU + {A67C7407-BB65-4C81-9DE8-65D69A8E97C5}.Release|x64.Build.0 = Release|Any CPU + {A67C7407-BB65-4C81-9DE8-65D69A8E97C5}.Release|x86.ActiveCfg = Release|Any CPU + {A67C7407-BB65-4C81-9DE8-65D69A8E97C5}.Release|x86.Build.0 = Release|Any CPU {7FD0C9BD-BF75-45FA-AD03-AE5F887A9CA8}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {7FD0C9BD-BF75-45FA-AD03-AE5F887A9CA8}.Debug|Any CPU.Build.0 = Debug|Any CPU + {7FD0C9BD-BF75-45FA-AD03-AE5F887A9CA8}.Debug|x64.ActiveCfg = Debug|Any CPU + {7FD0C9BD-BF75-45FA-AD03-AE5F887A9CA8}.Debug|x64.Build.0 = Debug|Any CPU + {7FD0C9BD-BF75-45FA-AD03-AE5F887A9CA8}.Debug|x86.ActiveCfg = Debug|Any CPU + {7FD0C9BD-BF75-45FA-AD03-AE5F887A9CA8}.Debug|x86.Build.0 = Debug|Any CPU {7FD0C9BD-BF75-45FA-AD03-AE5F887A9CA8}.Release|Any CPU.ActiveCfg = Release|Any CPU {7FD0C9BD-BF75-45FA-AD03-AE5F887A9CA8}.Release|Any CPU.Build.0 = Release|Any CPU + {7FD0C9BD-BF75-45FA-AD03-AE5F887A9CA8}.Release|x64.ActiveCfg = Release|Any CPU + {7FD0C9BD-BF75-45FA-AD03-AE5F887A9CA8}.Release|x64.Build.0 = Release|Any CPU + {7FD0C9BD-BF75-45FA-AD03-AE5F887A9CA8}.Release|x86.ActiveCfg = Release|Any CPU + {7FD0C9BD-BF75-45FA-AD03-AE5F887A9CA8}.Release|x86.Build.0 = Release|Any CPU {62F493D6-2282-4830-940D-906F8B85D3D7}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {62F493D6-2282-4830-940D-906F8B85D3D7}.Debug|Any CPU.Build.0 = Debug|Any CPU + {62F493D6-2282-4830-940D-906F8B85D3D7}.Debug|x64.ActiveCfg = Debug|Any CPU + {62F493D6-2282-4830-940D-906F8B85D3D7}.Debug|x64.Build.0 = Debug|Any CPU + {62F493D6-2282-4830-940D-906F8B85D3D7}.Debug|x86.ActiveCfg = Debug|Any CPU + {62F493D6-2282-4830-940D-906F8B85D3D7}.Debug|x86.Build.0 = Debug|Any CPU {62F493D6-2282-4830-940D-906F8B85D3D7}.Release|Any CPU.ActiveCfg = Release|Any CPU {62F493D6-2282-4830-940D-906F8B85D3D7}.Release|Any CPU.Build.0 = Release|Any CPU + {62F493D6-2282-4830-940D-906F8B85D3D7}.Release|x64.ActiveCfg = Release|Any CPU + {62F493D6-2282-4830-940D-906F8B85D3D7}.Release|x64.Build.0 = Release|Any CPU + {62F493D6-2282-4830-940D-906F8B85D3D7}.Release|x86.ActiveCfg = Release|Any CPU + {62F493D6-2282-4830-940D-906F8B85D3D7}.Release|x86.Build.0 = Release|Any CPU {41D7051A-013E-42EC-87A2-FF30F564CDB6}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {41D7051A-013E-42EC-87A2-FF30F564CDB6}.Debug|Any CPU.Build.0 = Debug|Any CPU + {41D7051A-013E-42EC-87A2-FF30F564CDB6}.Debug|x64.ActiveCfg = Debug|Any CPU + {41D7051A-013E-42EC-87A2-FF30F564CDB6}.Debug|x64.Build.0 = Debug|Any CPU + {41D7051A-013E-42EC-87A2-FF30F564CDB6}.Debug|x86.ActiveCfg = Debug|Any CPU + {41D7051A-013E-42EC-87A2-FF30F564CDB6}.Debug|x86.Build.0 = Debug|Any CPU {41D7051A-013E-42EC-87A2-FF30F564CDB6}.Release|Any CPU.ActiveCfg = Release|Any CPU {41D7051A-013E-42EC-87A2-FF30F564CDB6}.Release|Any CPU.Build.0 = Release|Any CPU + {41D7051A-013E-42EC-87A2-FF30F564CDB6}.Release|x64.ActiveCfg = Release|Any CPU + {41D7051A-013E-42EC-87A2-FF30F564CDB6}.Release|x64.Build.0 = Release|Any CPU + {41D7051A-013E-42EC-87A2-FF30F564CDB6}.Release|x86.ActiveCfg = Release|Any CPU + {41D7051A-013E-42EC-87A2-FF30F564CDB6}.Release|x86.Build.0 = Release|Any CPU {CD8495ED-3C98-41FC-8643-61B35B50C413}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {CD8495ED-3C98-41FC-8643-61B35B50C413}.Debug|Any CPU.Build.0 = Debug|Any CPU + {CD8495ED-3C98-41FC-8643-61B35B50C413}.Debug|x64.ActiveCfg = Debug|Any CPU + {CD8495ED-3C98-41FC-8643-61B35B50C413}.Debug|x64.Build.0 = Debug|Any CPU + {CD8495ED-3C98-41FC-8643-61B35B50C413}.Debug|x86.ActiveCfg = Debug|Any CPU + {CD8495ED-3C98-41FC-8643-61B35B50C413}.Debug|x86.Build.0 = Debug|Any CPU {CD8495ED-3C98-41FC-8643-61B35B50C413}.Release|Any CPU.ActiveCfg = Release|Any CPU {CD8495ED-3C98-41FC-8643-61B35B50C413}.Release|Any CPU.Build.0 = Release|Any CPU + {CD8495ED-3C98-41FC-8643-61B35B50C413}.Release|x64.ActiveCfg = Release|Any CPU + {CD8495ED-3C98-41FC-8643-61B35B50C413}.Release|x64.Build.0 = Release|Any CPU + {CD8495ED-3C98-41FC-8643-61B35B50C413}.Release|x86.ActiveCfg = Release|Any CPU + {CD8495ED-3C98-41FC-8643-61B35B50C413}.Release|x86.Build.0 = Release|Any CPU {1C648E04-F9E3-433F-86D9-E99344275613}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {1C648E04-F9E3-433F-86D9-E99344275613}.Debug|x64.ActiveCfg = Debug|Any CPU + {1C648E04-F9E3-433F-86D9-E99344275613}.Debug|x64.Build.0 = Debug|Any CPU + {1C648E04-F9E3-433F-86D9-E99344275613}.Debug|x86.ActiveCfg = Debug|Any CPU + {1C648E04-F9E3-433F-86D9-E99344275613}.Debug|x86.Build.0 = Debug|Any CPU {1C648E04-F9E3-433F-86D9-E99344275613}.Release|Any CPU.ActiveCfg = Release|Any CPU + {1C648E04-F9E3-433F-86D9-E99344275613}.Release|x64.ActiveCfg = Release|Any CPU + {1C648E04-F9E3-433F-86D9-E99344275613}.Release|x64.Build.0 = Release|Any CPU + {1C648E04-F9E3-433F-86D9-E99344275613}.Release|x86.ActiveCfg = Release|Any CPU + {1C648E04-F9E3-433F-86D9-E99344275613}.Release|x86.Build.0 = Release|Any CPU {31CF8050-9A87-4569-A750-3171B2A892C0}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {31CF8050-9A87-4569-A750-3171B2A892C0}.Debug|Any CPU.Build.0 = Debug|Any CPU + {31CF8050-9A87-4569-A750-3171B2A892C0}.Debug|x64.ActiveCfg = Debug|Any CPU + {31CF8050-9A87-4569-A750-3171B2A892C0}.Debug|x64.Build.0 = Debug|Any CPU + {31CF8050-9A87-4569-A750-3171B2A892C0}.Debug|x86.ActiveCfg = Debug|Any CPU + {31CF8050-9A87-4569-A750-3171B2A892C0}.Debug|x86.Build.0 = Debug|Any CPU {31CF8050-9A87-4569-A750-3171B2A892C0}.Release|Any CPU.ActiveCfg = Release|Any CPU {31CF8050-9A87-4569-A750-3171B2A892C0}.Release|Any CPU.Build.0 = Release|Any CPU + {31CF8050-9A87-4569-A750-3171B2A892C0}.Release|x64.ActiveCfg = Release|Any CPU + {31CF8050-9A87-4569-A750-3171B2A892C0}.Release|x64.Build.0 = Release|Any CPU + {31CF8050-9A87-4569-A750-3171B2A892C0}.Release|x86.ActiveCfg = Release|Any CPU + {31CF8050-9A87-4569-A750-3171B2A892C0}.Release|x86.Build.0 = Release|Any CPU {03074EEB-1851-4C55-BDB9-E5F527DF2FF6}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {03074EEB-1851-4C55-BDB9-E5F527DF2FF6}.Debug|Any CPU.Build.0 = Debug|Any CPU + {03074EEB-1851-4C55-BDB9-E5F527DF2FF6}.Debug|x64.ActiveCfg = Debug|Any CPU + {03074EEB-1851-4C55-BDB9-E5F527DF2FF6}.Debug|x64.Build.0 = Debug|Any CPU + {03074EEB-1851-4C55-BDB9-E5F527DF2FF6}.Debug|x86.ActiveCfg = Debug|Any CPU + {03074EEB-1851-4C55-BDB9-E5F527DF2FF6}.Debug|x86.Build.0 = Debug|Any CPU {03074EEB-1851-4C55-BDB9-E5F527DF2FF6}.Release|Any CPU.ActiveCfg = Release|Any CPU {03074EEB-1851-4C55-BDB9-E5F527DF2FF6}.Release|Any CPU.Build.0 = Release|Any CPU + {03074EEB-1851-4C55-BDB9-E5F527DF2FF6}.Release|x64.ActiveCfg = Release|Any CPU + {03074EEB-1851-4C55-BDB9-E5F527DF2FF6}.Release|x64.Build.0 = Release|Any CPU + {03074EEB-1851-4C55-BDB9-E5F527DF2FF6}.Release|x86.ActiveCfg = Release|Any CPU + {03074EEB-1851-4C55-BDB9-E5F527DF2FF6}.Release|x86.Build.0 = Release|Any CPU {BBF5F832-2D4A-4363-985A-8A9E4695EEC3}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {BBF5F832-2D4A-4363-985A-8A9E4695EEC3}.Debug|Any CPU.Build.0 = Debug|Any CPU + {BBF5F832-2D4A-4363-985A-8A9E4695EEC3}.Debug|x64.ActiveCfg = Debug|Any CPU + {BBF5F832-2D4A-4363-985A-8A9E4695EEC3}.Debug|x64.Build.0 = Debug|Any CPU + {BBF5F832-2D4A-4363-985A-8A9E4695EEC3}.Debug|x86.ActiveCfg = Debug|Any CPU + {BBF5F832-2D4A-4363-985A-8A9E4695EEC3}.Debug|x86.Build.0 = Debug|Any CPU {BBF5F832-2D4A-4363-985A-8A9E4695EEC3}.Release|Any CPU.ActiveCfg = Release|Any CPU {BBF5F832-2D4A-4363-985A-8A9E4695EEC3}.Release|Any CPU.Build.0 = Release|Any CPU + {BBF5F832-2D4A-4363-985A-8A9E4695EEC3}.Release|x64.ActiveCfg = Release|Any CPU + {BBF5F832-2D4A-4363-985A-8A9E4695EEC3}.Release|x64.Build.0 = Release|Any CPU + {BBF5F832-2D4A-4363-985A-8A9E4695EEC3}.Release|x86.ActiveCfg = Release|Any CPU + {BBF5F832-2D4A-4363-985A-8A9E4695EEC3}.Release|x86.Build.0 = Release|Any CPU {9911DDA3-35DE-4059-B790-D42F568F13C5}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {9911DDA3-35DE-4059-B790-D42F568F13C5}.Debug|Any CPU.Build.0 = Debug|Any CPU + {9911DDA3-35DE-4059-B790-D42F568F13C5}.Debug|x64.ActiveCfg = Debug|Any CPU + {9911DDA3-35DE-4059-B790-D42F568F13C5}.Debug|x64.Build.0 = Debug|Any CPU + {9911DDA3-35DE-4059-B790-D42F568F13C5}.Debug|x86.ActiveCfg = Debug|Any CPU + {9911DDA3-35DE-4059-B790-D42F568F13C5}.Debug|x86.Build.0 = Debug|Any CPU {9911DDA3-35DE-4059-B790-D42F568F13C5}.Release|Any CPU.ActiveCfg = Release|Any CPU {9911DDA3-35DE-4059-B790-D42F568F13C5}.Release|Any CPU.Build.0 = Release|Any CPU + {9911DDA3-35DE-4059-B790-D42F568F13C5}.Release|x64.ActiveCfg = Release|Any CPU + {9911DDA3-35DE-4059-B790-D42F568F13C5}.Release|x64.Build.0 = Release|Any CPU + {9911DDA3-35DE-4059-B790-D42F568F13C5}.Release|x86.ActiveCfg = Release|Any CPU + {9911DDA3-35DE-4059-B790-D42F568F13C5}.Release|x86.Build.0 = Release|Any CPU {32B4384E-0669-44BA-A862-BEABB6102A3B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {32B4384E-0669-44BA-A862-BEABB6102A3B}.Debug|Any CPU.Build.0 = Debug|Any CPU + {32B4384E-0669-44BA-A862-BEABB6102A3B}.Debug|x64.ActiveCfg = Debug|Any CPU + {32B4384E-0669-44BA-A862-BEABB6102A3B}.Debug|x64.Build.0 = Debug|Any CPU + {32B4384E-0669-44BA-A862-BEABB6102A3B}.Debug|x86.ActiveCfg = Debug|Any CPU + {32B4384E-0669-44BA-A862-BEABB6102A3B}.Debug|x86.Build.0 = Debug|Any CPU {32B4384E-0669-44BA-A862-BEABB6102A3B}.Release|Any CPU.ActiveCfg = Release|Any CPU {32B4384E-0669-44BA-A862-BEABB6102A3B}.Release|Any CPU.Build.0 = Release|Any CPU + {32B4384E-0669-44BA-A862-BEABB6102A3B}.Release|x64.ActiveCfg = Release|Any CPU + {32B4384E-0669-44BA-A862-BEABB6102A3B}.Release|x64.Build.0 = Release|Any CPU + {32B4384E-0669-44BA-A862-BEABB6102A3B}.Release|x86.ActiveCfg = Release|Any CPU + {32B4384E-0669-44BA-A862-BEABB6102A3B}.Release|x86.Build.0 = Release|Any CPU {114D6F8B-B105-4C22-86BA-D83732D05941}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {114D6F8B-B105-4C22-86BA-D83732D05941}.Debug|Any CPU.Build.0 = Debug|Any CPU + {114D6F8B-B105-4C22-86BA-D83732D05941}.Debug|x64.ActiveCfg = Debug|Any CPU + {114D6F8B-B105-4C22-86BA-D83732D05941}.Debug|x64.Build.0 = Debug|Any CPU + {114D6F8B-B105-4C22-86BA-D83732D05941}.Debug|x86.ActiveCfg = Debug|Any CPU + {114D6F8B-B105-4C22-86BA-D83732D05941}.Debug|x86.Build.0 = Debug|Any CPU {114D6F8B-B105-4C22-86BA-D83732D05941}.Release|Any CPU.ActiveCfg = Release|Any CPU {114D6F8B-B105-4C22-86BA-D83732D05941}.Release|Any CPU.Build.0 = Release|Any CPU + {114D6F8B-B105-4C22-86BA-D83732D05941}.Release|x64.ActiveCfg = Release|Any CPU + {114D6F8B-B105-4C22-86BA-D83732D05941}.Release|x64.Build.0 = Release|Any CPU + {114D6F8B-B105-4C22-86BA-D83732D05941}.Release|x86.ActiveCfg = Release|Any CPU + {114D6F8B-B105-4C22-86BA-D83732D05941}.Release|x86.Build.0 = Release|Any CPU {36D2F2F5-A85F-41F3-807C-78793E9633A3}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {36D2F2F5-A85F-41F3-807C-78793E9633A3}.Debug|Any CPU.Build.0 = Debug|Any CPU + {36D2F2F5-A85F-41F3-807C-78793E9633A3}.Debug|x64.ActiveCfg = Debug|Any CPU + {36D2F2F5-A85F-41F3-807C-78793E9633A3}.Debug|x64.Build.0 = Debug|Any CPU + {36D2F2F5-A85F-41F3-807C-78793E9633A3}.Debug|x86.ActiveCfg = Debug|Any CPU + {36D2F2F5-A85F-41F3-807C-78793E9633A3}.Debug|x86.Build.0 = Debug|Any CPU {36D2F2F5-A85F-41F3-807C-78793E9633A3}.Release|Any CPU.ActiveCfg = Release|Any CPU {36D2F2F5-A85F-41F3-807C-78793E9633A3}.Release|Any CPU.Build.0 = Release|Any CPU + {36D2F2F5-A85F-41F3-807C-78793E9633A3}.Release|x64.ActiveCfg = Release|Any CPU + {36D2F2F5-A85F-41F3-807C-78793E9633A3}.Release|x64.Build.0 = Release|Any CPU + {36D2F2F5-A85F-41F3-807C-78793E9633A3}.Release|x86.ActiveCfg = Release|Any CPU + {36D2F2F5-A85F-41F3-807C-78793E9633A3}.Release|x86.Build.0 = Release|Any CPU {1A747A4A-E6D1-4117-9510-ADBA9BD1189D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {1A747A4A-E6D1-4117-9510-ADBA9BD1189D}.Debug|Any CPU.Build.0 = Debug|Any CPU + {1A747A4A-E6D1-4117-9510-ADBA9BD1189D}.Debug|x64.ActiveCfg = Debug|Any CPU + {1A747A4A-E6D1-4117-9510-ADBA9BD1189D}.Debug|x64.Build.0 = Debug|Any CPU + {1A747A4A-E6D1-4117-9510-ADBA9BD1189D}.Debug|x86.ActiveCfg = Debug|Any CPU + {1A747A4A-E6D1-4117-9510-ADBA9BD1189D}.Debug|x86.Build.0 = Debug|Any CPU {1A747A4A-E6D1-4117-9510-ADBA9BD1189D}.Release|Any CPU.ActiveCfg = Release|Any CPU {1A747A4A-E6D1-4117-9510-ADBA9BD1189D}.Release|Any CPU.Build.0 = Release|Any CPU + {1A747A4A-E6D1-4117-9510-ADBA9BD1189D}.Release|x64.ActiveCfg = Release|Any CPU + {1A747A4A-E6D1-4117-9510-ADBA9BD1189D}.Release|x64.Build.0 = Release|Any CPU + {1A747A4A-E6D1-4117-9510-ADBA9BD1189D}.Release|x86.ActiveCfg = Release|Any CPU + {1A747A4A-E6D1-4117-9510-ADBA9BD1189D}.Release|x86.Build.0 = Release|Any CPU {CF806E34-5551-41F2-ABAD-313943C4A943}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {CF806E34-5551-41F2-ABAD-313943C4A943}.Debug|Any CPU.Build.0 = Debug|Any CPU + {CF806E34-5551-41F2-ABAD-313943C4A943}.Debug|x64.ActiveCfg = Debug|Any CPU + {CF806E34-5551-41F2-ABAD-313943C4A943}.Debug|x64.Build.0 = Debug|Any CPU + {CF806E34-5551-41F2-ABAD-313943C4A943}.Debug|x86.ActiveCfg = Debug|Any CPU + {CF806E34-5551-41F2-ABAD-313943C4A943}.Debug|x86.Build.0 = Debug|Any CPU {CF806E34-5551-41F2-ABAD-313943C4A943}.Release|Any CPU.ActiveCfg = Release|Any CPU {CF806E34-5551-41F2-ABAD-313943C4A943}.Release|Any CPU.Build.0 = Release|Any CPU + {CF806E34-5551-41F2-ABAD-313943C4A943}.Release|x64.ActiveCfg = Release|Any CPU + {CF806E34-5551-41F2-ABAD-313943C4A943}.Release|x64.Build.0 = Release|Any CPU + {CF806E34-5551-41F2-ABAD-313943C4A943}.Release|x86.ActiveCfg = Release|Any CPU + {CF806E34-5551-41F2-ABAD-313943C4A943}.Release|x86.Build.0 = Release|Any CPU {D1860861-9F1F-4F93-BA65-85A96C355737}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {D1860861-9F1F-4F93-BA65-85A96C355737}.Debug|Any CPU.Build.0 = Debug|Any CPU + {D1860861-9F1F-4F93-BA65-85A96C355737}.Debug|x64.ActiveCfg = Debug|Any CPU + {D1860861-9F1F-4F93-BA65-85A96C355737}.Debug|x64.Build.0 = Debug|Any CPU + {D1860861-9F1F-4F93-BA65-85A96C355737}.Debug|x86.ActiveCfg = Debug|Any CPU + {D1860861-9F1F-4F93-BA65-85A96C355737}.Debug|x86.Build.0 = Debug|Any CPU {D1860861-9F1F-4F93-BA65-85A96C355737}.Release|Any CPU.ActiveCfg = Release|Any CPU {D1860861-9F1F-4F93-BA65-85A96C355737}.Release|Any CPU.Build.0 = Release|Any CPU + {D1860861-9F1F-4F93-BA65-85A96C355737}.Release|x64.ActiveCfg = Release|Any CPU + {D1860861-9F1F-4F93-BA65-85A96C355737}.Release|x64.Build.0 = Release|Any CPU + {D1860861-9F1F-4F93-BA65-85A96C355737}.Release|x86.ActiveCfg = Release|Any CPU + {D1860861-9F1F-4F93-BA65-85A96C355737}.Release|x86.Build.0 = Release|Any CPU {E706924A-27D8-43FC-973A-3B98BC50DE2B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {E706924A-27D8-43FC-973A-3B98BC50DE2B}.Debug|Any CPU.Build.0 = Debug|Any CPU + {E706924A-27D8-43FC-973A-3B98BC50DE2B}.Debug|x64.ActiveCfg = Debug|Any CPU + {E706924A-27D8-43FC-973A-3B98BC50DE2B}.Debug|x64.Build.0 = Debug|Any CPU + {E706924A-27D8-43FC-973A-3B98BC50DE2B}.Debug|x86.ActiveCfg = Debug|Any CPU + {E706924A-27D8-43FC-973A-3B98BC50DE2B}.Debug|x86.Build.0 = Debug|Any CPU {E706924A-27D8-43FC-973A-3B98BC50DE2B}.Release|Any CPU.ActiveCfg = Release|Any CPU {E706924A-27D8-43FC-973A-3B98BC50DE2B}.Release|Any CPU.Build.0 = Release|Any CPU + {E706924A-27D8-43FC-973A-3B98BC50DE2B}.Release|x64.ActiveCfg = Release|Any CPU + {E706924A-27D8-43FC-973A-3B98BC50DE2B}.Release|x64.Build.0 = Release|Any CPU + {E706924A-27D8-43FC-973A-3B98BC50DE2B}.Release|x86.ActiveCfg = Release|Any CPU + {E706924A-27D8-43FC-973A-3B98BC50DE2B}.Release|x86.Build.0 = Release|Any CPU {3972A724-04DB-41EE-90FB-3B469CEA26E3}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {3972A724-04DB-41EE-90FB-3B469CEA26E3}.Debug|Any CPU.Build.0 = Debug|Any CPU + {3972A724-04DB-41EE-90FB-3B469CEA26E3}.Debug|x64.ActiveCfg = Debug|Any CPU + {3972A724-04DB-41EE-90FB-3B469CEA26E3}.Debug|x64.Build.0 = Debug|Any CPU + {3972A724-04DB-41EE-90FB-3B469CEA26E3}.Debug|x86.ActiveCfg = Debug|Any CPU + {3972A724-04DB-41EE-90FB-3B469CEA26E3}.Debug|x86.Build.0 = Debug|Any CPU {3972A724-04DB-41EE-90FB-3B469CEA26E3}.Release|Any CPU.ActiveCfg = Release|Any CPU {3972A724-04DB-41EE-90FB-3B469CEA26E3}.Release|Any CPU.Build.0 = Release|Any CPU + {3972A724-04DB-41EE-90FB-3B469CEA26E3}.Release|x64.ActiveCfg = Release|Any CPU + {3972A724-04DB-41EE-90FB-3B469CEA26E3}.Release|x64.Build.0 = Release|Any CPU + {3972A724-04DB-41EE-90FB-3B469CEA26E3}.Release|x86.ActiveCfg = Release|Any CPU + {3972A724-04DB-41EE-90FB-3B469CEA26E3}.Release|x86.Build.0 = Release|Any CPU {CDC2131C-4B59-43D1-BC4A-02B5B35EC0BB}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {CDC2131C-4B59-43D1-BC4A-02B5B35EC0BB}.Debug|Any CPU.Build.0 = Debug|Any CPU + {CDC2131C-4B59-43D1-BC4A-02B5B35EC0BB}.Debug|x64.ActiveCfg = Debug|Any CPU + {CDC2131C-4B59-43D1-BC4A-02B5B35EC0BB}.Debug|x64.Build.0 = Debug|Any CPU + {CDC2131C-4B59-43D1-BC4A-02B5B35EC0BB}.Debug|x86.ActiveCfg = Debug|Any CPU + {CDC2131C-4B59-43D1-BC4A-02B5B35EC0BB}.Debug|x86.Build.0 = Debug|Any CPU {CDC2131C-4B59-43D1-BC4A-02B5B35EC0BB}.Release|Any CPU.ActiveCfg = Release|Any CPU {CDC2131C-4B59-43D1-BC4A-02B5B35EC0BB}.Release|Any CPU.Build.0 = Release|Any CPU + {CDC2131C-4B59-43D1-BC4A-02B5B35EC0BB}.Release|x64.ActiveCfg = Release|Any CPU + {CDC2131C-4B59-43D1-BC4A-02B5B35EC0BB}.Release|x64.Build.0 = Release|Any CPU + {CDC2131C-4B59-43D1-BC4A-02B5B35EC0BB}.Release|x86.ActiveCfg = Release|Any CPU + {CDC2131C-4B59-43D1-BC4A-02B5B35EC0BB}.Release|x86.Build.0 = Release|Any CPU {24D35FEE-787D-46D4-B072-157D4ED8DFBC}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {24D35FEE-787D-46D4-B072-157D4ED8DFBC}.Debug|Any CPU.Build.0 = Debug|Any CPU + {24D35FEE-787D-46D4-B072-157D4ED8DFBC}.Debug|x64.ActiveCfg = Debug|Any CPU + {24D35FEE-787D-46D4-B072-157D4ED8DFBC}.Debug|x64.Build.0 = Debug|Any CPU + {24D35FEE-787D-46D4-B072-157D4ED8DFBC}.Debug|x86.ActiveCfg = Debug|Any CPU + {24D35FEE-787D-46D4-B072-157D4ED8DFBC}.Debug|x86.Build.0 = Debug|Any CPU {24D35FEE-787D-46D4-B072-157D4ED8DFBC}.Release|Any CPU.ActiveCfg = Release|Any CPU {24D35FEE-787D-46D4-B072-157D4ED8DFBC}.Release|Any CPU.Build.0 = Release|Any CPU + {24D35FEE-787D-46D4-B072-157D4ED8DFBC}.Release|x64.ActiveCfg = Release|Any CPU + {24D35FEE-787D-46D4-B072-157D4ED8DFBC}.Release|x64.Build.0 = Release|Any CPU + {24D35FEE-787D-46D4-B072-157D4ED8DFBC}.Release|x86.ActiveCfg = Release|Any CPU + {24D35FEE-787D-46D4-B072-157D4ED8DFBC}.Release|x86.Build.0 = Release|Any CPU {B75EEFBA-FAE9-4DEE-9227-7ECF3C7B5F6A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {B75EEFBA-FAE9-4DEE-9227-7ECF3C7B5F6A}.Debug|Any CPU.Build.0 = Debug|Any CPU + {B75EEFBA-FAE9-4DEE-9227-7ECF3C7B5F6A}.Debug|x64.ActiveCfg = Debug|Any CPU + {B75EEFBA-FAE9-4DEE-9227-7ECF3C7B5F6A}.Debug|x64.Build.0 = Debug|Any CPU + {B75EEFBA-FAE9-4DEE-9227-7ECF3C7B5F6A}.Debug|x86.ActiveCfg = Debug|Any CPU + {B75EEFBA-FAE9-4DEE-9227-7ECF3C7B5F6A}.Debug|x86.Build.0 = Debug|Any CPU {B75EEFBA-FAE9-4DEE-9227-7ECF3C7B5F6A}.Release|Any CPU.ActiveCfg = Release|Any CPU {B75EEFBA-FAE9-4DEE-9227-7ECF3C7B5F6A}.Release|Any CPU.Build.0 = Release|Any CPU + {B75EEFBA-FAE9-4DEE-9227-7ECF3C7B5F6A}.Release|x64.ActiveCfg = Release|Any CPU + {B75EEFBA-FAE9-4DEE-9227-7ECF3C7B5F6A}.Release|x64.Build.0 = Release|Any CPU + {B75EEFBA-FAE9-4DEE-9227-7ECF3C7B5F6A}.Release|x86.ActiveCfg = Release|Any CPU + {B75EEFBA-FAE9-4DEE-9227-7ECF3C7B5F6A}.Release|x86.Build.0 = Release|Any CPU {AD692C6B-D99B-4EAF-B473-BA68AA151ED2}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {AD692C6B-D99B-4EAF-B473-BA68AA151ED2}.Debug|Any CPU.Build.0 = Debug|Any CPU + {AD692C6B-D99B-4EAF-B473-BA68AA151ED2}.Debug|x64.ActiveCfg = Debug|Any CPU + {AD692C6B-D99B-4EAF-B473-BA68AA151ED2}.Debug|x64.Build.0 = Debug|Any CPU + {AD692C6B-D99B-4EAF-B473-BA68AA151ED2}.Debug|x86.ActiveCfg = Debug|Any CPU + {AD692C6B-D99B-4EAF-B473-BA68AA151ED2}.Debug|x86.Build.0 = Debug|Any CPU {AD692C6B-D99B-4EAF-B473-BA68AA151ED2}.Release|Any CPU.ActiveCfg = Release|Any CPU {AD692C6B-D99B-4EAF-B473-BA68AA151ED2}.Release|Any CPU.Build.0 = Release|Any CPU + {AD692C6B-D99B-4EAF-B473-BA68AA151ED2}.Release|x64.ActiveCfg = Release|Any CPU + {AD692C6B-D99B-4EAF-B473-BA68AA151ED2}.Release|x64.Build.0 = Release|Any CPU + {AD692C6B-D99B-4EAF-B473-BA68AA151ED2}.Release|x86.ActiveCfg = Release|Any CPU + {AD692C6B-D99B-4EAF-B473-BA68AA151ED2}.Release|x86.Build.0 = Release|Any CPU {F488851D-C635-49C9-8CD7-E9B90C756565}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {F488851D-C635-49C9-8CD7-E9B90C756565}.Debug|Any CPU.Build.0 = Debug|Any CPU + {F488851D-C635-49C9-8CD7-E9B90C756565}.Debug|x64.ActiveCfg = Debug|Any CPU + {F488851D-C635-49C9-8CD7-E9B90C756565}.Debug|x64.Build.0 = Debug|Any CPU + {F488851D-C635-49C9-8CD7-E9B90C756565}.Debug|x86.ActiveCfg = Debug|Any CPU + {F488851D-C635-49C9-8CD7-E9B90C756565}.Debug|x86.Build.0 = Debug|Any CPU {F488851D-C635-49C9-8CD7-E9B90C756565}.Release|Any CPU.ActiveCfg = Release|Any CPU {F488851D-C635-49C9-8CD7-E9B90C756565}.Release|Any CPU.Build.0 = Release|Any CPU + {F488851D-C635-49C9-8CD7-E9B90C756565}.Release|x64.ActiveCfg = Release|Any CPU + {F488851D-C635-49C9-8CD7-E9B90C756565}.Release|x64.Build.0 = Release|Any CPU + {F488851D-C635-49C9-8CD7-E9B90C756565}.Release|x86.ActiveCfg = Release|Any CPU + {F488851D-C635-49C9-8CD7-E9B90C756565}.Release|x86.Build.0 = Release|Any CPU {80008096-F1F9-43C3-8CE6-E79A68F8676E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {80008096-F1F9-43C3-8CE6-E79A68F8676E}.Debug|Any CPU.Build.0 = Debug|Any CPU + {80008096-F1F9-43C3-8CE6-E79A68F8676E}.Debug|x64.ActiveCfg = Debug|Any CPU + {80008096-F1F9-43C3-8CE6-E79A68F8676E}.Debug|x64.Build.0 = Debug|Any CPU + {80008096-F1F9-43C3-8CE6-E79A68F8676E}.Debug|x86.ActiveCfg = Debug|Any CPU + {80008096-F1F9-43C3-8CE6-E79A68F8676E}.Debug|x86.Build.0 = Debug|Any CPU {80008096-F1F9-43C3-8CE6-E79A68F8676E}.Release|Any CPU.ActiveCfg = Release|Any CPU {80008096-F1F9-43C3-8CE6-E79A68F8676E}.Release|Any CPU.Build.0 = Release|Any CPU + {80008096-F1F9-43C3-8CE6-E79A68F8676E}.Release|x64.ActiveCfg = Release|Any CPU + {80008096-F1F9-43C3-8CE6-E79A68F8676E}.Release|x64.Build.0 = Release|Any CPU + {80008096-F1F9-43C3-8CE6-E79A68F8676E}.Release|x86.ActiveCfg = Release|Any CPU + {80008096-F1F9-43C3-8CE6-E79A68F8676E}.Release|x86.Build.0 = Release|Any CPU + {B93B8B7E-BEAF-4645-AA5C-2A1CADEC3D4B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {B93B8B7E-BEAF-4645-AA5C-2A1CADEC3D4B}.Debug|Any CPU.Build.0 = Debug|Any CPU + {B93B8B7E-BEAF-4645-AA5C-2A1CADEC3D4B}.Debug|x64.ActiveCfg = Debug|Any CPU + {B93B8B7E-BEAF-4645-AA5C-2A1CADEC3D4B}.Debug|x64.Build.0 = Debug|Any CPU + {B93B8B7E-BEAF-4645-AA5C-2A1CADEC3D4B}.Debug|x86.ActiveCfg = Debug|Any CPU + {B93B8B7E-BEAF-4645-AA5C-2A1CADEC3D4B}.Debug|x86.Build.0 = Debug|Any CPU + {B93B8B7E-BEAF-4645-AA5C-2A1CADEC3D4B}.Release|Any CPU.ActiveCfg = Release|Any CPU + {B93B8B7E-BEAF-4645-AA5C-2A1CADEC3D4B}.Release|Any CPU.Build.0 = Release|Any CPU + {B93B8B7E-BEAF-4645-AA5C-2A1CADEC3D4B}.Release|x64.ActiveCfg = Release|Any CPU + {B93B8B7E-BEAF-4645-AA5C-2A1CADEC3D4B}.Release|x64.Build.0 = Release|Any CPU + {B93B8B7E-BEAF-4645-AA5C-2A1CADEC3D4B}.Release|x86.ActiveCfg = Release|Any CPU + {B93B8B7E-BEAF-4645-AA5C-2A1CADEC3D4B}.Release|x86.Build.0 = Release|Any CPU + {F2BD40E4-A077-429A-8A22-1C80AFC240E8}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {F2BD40E4-A077-429A-8A22-1C80AFC240E8}.Debug|Any CPU.Build.0 = Debug|Any CPU + {F2BD40E4-A077-429A-8A22-1C80AFC240E8}.Debug|x64.ActiveCfg = Debug|Any CPU + {F2BD40E4-A077-429A-8A22-1C80AFC240E8}.Debug|x64.Build.0 = Debug|Any CPU + {F2BD40E4-A077-429A-8A22-1C80AFC240E8}.Debug|x86.ActiveCfg = Debug|Any CPU + {F2BD40E4-A077-429A-8A22-1C80AFC240E8}.Debug|x86.Build.0 = Debug|Any CPU + {F2BD40E4-A077-429A-8A22-1C80AFC240E8}.Release|Any CPU.ActiveCfg = Release|Any CPU + {F2BD40E4-A077-429A-8A22-1C80AFC240E8}.Release|Any CPU.Build.0 = Release|Any CPU + {F2BD40E4-A077-429A-8A22-1C80AFC240E8}.Release|x64.ActiveCfg = Release|Any CPU + {F2BD40E4-A077-429A-8A22-1C80AFC240E8}.Release|x64.Build.0 = Release|Any CPU + {F2BD40E4-A077-429A-8A22-1C80AFC240E8}.Release|x86.ActiveCfg = Release|Any CPU + {F2BD40E4-A077-429A-8A22-1C80AFC240E8}.Release|x86.Build.0 = Release|Any CPU + {33D5D084-FABB-4F65-A046-1C6626DF9AFB}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {33D5D084-FABB-4F65-A046-1C6626DF9AFB}.Debug|Any CPU.Build.0 = Debug|Any CPU + {33D5D084-FABB-4F65-A046-1C6626DF9AFB}.Debug|x64.ActiveCfg = Debug|Any CPU + {33D5D084-FABB-4F65-A046-1C6626DF9AFB}.Debug|x64.Build.0 = Debug|Any CPU + {33D5D084-FABB-4F65-A046-1C6626DF9AFB}.Debug|x86.ActiveCfg = Debug|Any CPU + {33D5D084-FABB-4F65-A046-1C6626DF9AFB}.Debug|x86.Build.0 = Debug|Any CPU + {33D5D084-FABB-4F65-A046-1C6626DF9AFB}.Release|Any CPU.ActiveCfg = Release|Any CPU + {33D5D084-FABB-4F65-A046-1C6626DF9AFB}.Release|Any CPU.Build.0 = Release|Any CPU + {33D5D084-FABB-4F65-A046-1C6626DF9AFB}.Release|x64.ActiveCfg = Release|Any CPU + {33D5D084-FABB-4F65-A046-1C6626DF9AFB}.Release|x64.Build.0 = Release|Any CPU + {33D5D084-FABB-4F65-A046-1C6626DF9AFB}.Release|x86.ActiveCfg = Release|Any CPU + {33D5D084-FABB-4F65-A046-1C6626DF9AFB}.Release|x86.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -471,7 +1036,6 @@ Global GlobalSection(NestedProjects) = preSolution {4F65FAE1-4C51-4BEF-956E-97C6A96807F9} = {350F72AB-142D-4AAD-9EF1-1A83DC991D87} {4C6B71B2-74EF-4B9E-88FF-C56532727C6D} = {CD6E567B-C17B-4C90-87CA-F6CB941EC307} - {A1E60657-039F-4BAB-B031-2515F86B0C3D} = {350F72AB-142D-4AAD-9EF1-1A83DC991D87} {E2F41605-F664-4EC6-9D64-1F142CC3B7B2} = {55C388DD-2B39-4C2F-AEBD-AFD3444815F1} {FA9439BF-37B4-4F27-B7E9-BE3605E66BE7} = {55C388DD-2B39-4C2F-AEBD-AFD3444815F1} {2343779D-26D4-4F9D-A42F-32E146632EF0} = {55C388DD-2B39-4C2F-AEBD-AFD3444815F1} @@ -515,9 +1079,7 @@ Global {927A078A-E271-4878-A153-86D71AE510E2} = {55C388DD-2B39-4C2F-AEBD-AFD3444815F1} {9B52418E-BF18-4D25-BE17-4B56D3FB1154} = {CD6E567B-C17B-4C90-87CA-F6CB941EC307} {C536A96A-D4B7-4793-890E-5A29ED7144E5} = {9B52418E-BF18-4D25-BE17-4B56D3FB1154} - {2A91F751-8432-424C-8D45-7E3BFC4CDBAD} = {9B52418E-BF18-4D25-BE17-4B56D3FB1154} {07F15F50-0A81-4872-9C7D-34DE88599B9F} = {9B52418E-BF18-4D25-BE17-4B56D3FB1154} - {30038795-181B-488E-8E99-CFF2009FC6D7} = {350F72AB-142D-4AAD-9EF1-1A83DC991D87} {BD162BC6-705F-45B4-A6B5-C138DC966C1D} = {E2249BAA-D9E9-4369-9C70-0E21C69A3E56} {59B2735E-9B70-4277-8E8C-C8CCB40A097A} = {BD162BC6-705F-45B4-A6B5-C138DC966C1D} {C228C9DD-4B2E-4A86-9F4E-09C4A8C2AD61} = {BD162BC6-705F-45B4-A6B5-C138DC966C1D} @@ -547,6 +1109,9 @@ Global {B75EEFBA-FAE9-4DEE-9227-7ECF3C7B5F6A} = {9AF9D3C7-E859-451B-BA4D-B954D289213A} {F488851D-C635-49C9-8CD7-E9B90C756565} = {9AF9D3C7-E859-451B-BA4D-B954D289213A} {80008096-F1F9-43C3-8CE6-E79A68F8676E} = {927A078A-E271-4878-A153-86D71AE510E2} + {B93B8B7E-BEAF-4645-AA5C-2A1CADEC3D4B} = {9AF9D3C7-E859-451B-BA4D-B954D289213A} + {F2BD40E4-A077-429A-8A22-1C80AFC240E8} = {350F72AB-142D-4AAD-9EF1-1A83DC991D87} + {33D5D084-FABB-4F65-A046-1C6626DF9AFB} = {350F72AB-142D-4AAD-9EF1-1A83DC991D87} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {3B9E5B72-F580-465A-A22C-2D2148AF4EB4} diff --git a/src/ServiceControl/.editorconfig b/src/ServiceControl/.editorconfig new file mode 100644 index 0000000000..ff993b49bb --- /dev/null +++ b/src/ServiceControl/.editorconfig @@ -0,0 +1,4 @@ +[*.cs] + +# Justification: ServiceControl app has no synchronization context +dotnet_diagnostic.CA2007.severity = none diff --git a/src/ServiceControl/App.config b/src/ServiceControl/App.config index 8635e3820c..fada510b70 100644 --- a/src/ServiceControl/App.config +++ b/src/ServiceControl/App.config @@ -5,54 +5,71 @@ These settings are only here so that we can debug ServiceControl while developin --> - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/ServiceControl/Bootstrapper.cs b/src/ServiceControl/Bootstrapper.cs index 6972279d67..29573fcdba 100644 --- a/src/ServiceControl/Bootstrapper.cs +++ b/src/ServiceControl/Bootstrapper.cs @@ -70,6 +70,9 @@ public Bootstrapper(Settings settings, EndpointConfiguration configuration, Logg void CreateHost() { + var persistenceConfiguration = PersistenceConfigurationFactory.LoadPersistenceConfiguration(settings.PersistenceType); + var persistenceSettings = persistenceConfiguration.BuildPersistenceSettings(settings); + RecordStartup(loggingSettings, configuration); if (!string.IsNullOrWhiteSpace(settings.LicenseFileText)) @@ -90,6 +93,11 @@ void CreateHost() HostBuilder = new HostBuilder(); HostBuilder + .UseDefaultServiceProvider(c => // TODO: Remove when done testing + { + c.ValidateOnBuild = false; + c.ValidateScopes = false; + }) .ConfigureLogging(builder => { builder.ClearProviders(); @@ -112,7 +120,7 @@ void CreateHost() services.AddSingleton(sp => HttpClientFactory); }) .UseLicenseCheck() - .SetupPersistence(settings) + .SetupPersistence(persistenceSettings, persistenceConfiguration) .UseMetrics(settings.PrintMetrics) .UseNServiceBus(context => { @@ -125,8 +133,7 @@ void CreateHost() .UseEmailNotifications() .UseAsyncTimer() .If(!settings.DisableHealthChecks, b => b.UseInternalCustomChecks()) - .UseServiceControlComponents(settings, ServiceControlMainInstance.Components) - ; + .UseServiceControlComponents(settings, ServiceControlMainInstance.Components); } TransportSettings MapSettings(Settings settings) @@ -149,6 +156,10 @@ long DataSize() try { var info = new FileInfo(datafilePath); + if (!info.Exists) + { + return -1; + } return info.Length; } catch @@ -174,16 +185,21 @@ long FolderSize() static long DirSize(DirectoryInfo d) { long size = 0; - FileInfo[] fis = d.GetFiles(); - foreach (FileInfo fi in fis) - { - size += fi.Length; - } - DirectoryInfo[] dis = d.GetDirectories(); - foreach (DirectoryInfo di in dis) + if (d.Exists) { - size += DirSize(di); + FileInfo[] fis = d.GetFiles(); + foreach (FileInfo fi in fis) + { + size += fi.Length; + } + + DirectoryInfo[] dis = d.GetDirectories(); + foreach (DirectoryInfo di in dis) + { + size += DirSize(di); + } } + return size; } @@ -227,7 +243,7 @@ Audit Retention Period (optional): {settings.AuditRetentionPeriod} settings.NotificationsFilter, settings.RemoteInstances, settings.RetryHistoryDepth, - settings.RunInMemory, + settings.PersisterSpecificSettings, settings.SkipQueueCreation, settings.EnableFullTextSearchOnBodies, settings.TransportType, diff --git a/src/ServiceControl/CompositeViews/AuditCounts/GetAuditCountsForEndpointApi.cs b/src/ServiceControl/CompositeViews/AuditCounts/GetAuditCountsForEndpointApi.cs index 0e55c0ec50..231a56b6ea 100644 --- a/src/ServiceControl/CompositeViews/AuditCounts/GetAuditCountsForEndpointApi.cs +++ b/src/ServiceControl/CompositeViews/AuditCounts/GetAuditCountsForEndpointApi.cs @@ -5,17 +5,17 @@ using System.Linq; using System.Net.Http; using System.Threading.Tasks; - using Raven.Client; using ServiceBus.Management.Infrastructure.Settings; using ServiceControl.CompositeViews.Messages; + using ServiceControl.Persistence; using ServiceControl.Persistence.Infrastructure; - class GetAuditCountsForEndpointApi : ScatterGatherApi> + class GetAuditCountsForEndpointApi : ScatterGatherApi> { static readonly IList empty = new List().AsReadOnly(); - public GetAuditCountsForEndpointApi(IDocumentStore documentStore, Settings settings, Func httpClientFactory) - : base(documentStore, settings, httpClientFactory) + public GetAuditCountsForEndpointApi(IErrorMessageDataStore dataStore, Settings settings, Func httpClientFactory) + : base(dataStore, settings, httpClientFactory) { } diff --git a/src/ServiceControl/CompositeViews/Messages/GetAllMessagesApi.cs b/src/ServiceControl/CompositeViews/Messages/GetAllMessagesApi.cs index 4993de2863..ba3200421b 100644 --- a/src/ServiceControl/CompositeViews/Messages/GetAllMessagesApi.cs +++ b/src/ServiceControl/CompositeViews/Messages/GetAllMessagesApi.cs @@ -4,32 +4,23 @@ namespace ServiceControl.CompositeViews.Messages using System.Collections.Generic; using System.Net.Http; using System.Threading.Tasks; - using Raven.Client; using ServiceBus.Management.Infrastructure.Settings; using ServiceControl.Persistence; using ServiceControl.Persistence.Infrastructure; - class GetAllMessagesApi : ScatterGatherApiMessageView + class GetAllMessagesApi : ScatterGatherApiMessageView { - public GetAllMessagesApi(IDocumentStore documentStore, Settings settings, Func httpClientFactory) : base(documentStore, settings, httpClientFactory) + public GetAllMessagesApi(IErrorMessageDataStore dataStore, Settings settings, Func httpClientFactory) : base(dataStore, settings, httpClientFactory) { } - protected override async Task>> LocalQuery(HttpRequestMessage request, NoInput input) + protected override Task>> LocalQuery(HttpRequestMessage request, NoInput input) { - using (var session = Store.OpenAsyncSession()) - { - var results = await session.Query() - .IncludeSystemMessagesWhere(request) - .Statistics(out var stats) - .Sort(request) - .Paging(request) - .TransformWith() - .ToListAsync() - .ConfigureAwait(false); + var pagingInfo = request.GetPagingInfo(); + var sortInfo = request.GetSortInfo(); + var includeSystemMessages = request.GetIncludeSystemMessages(); - return new QueryResult>(results, stats.ToQueryStatsInfo()); - } + return DataStore.GetAllMessages(pagingInfo, sortInfo, includeSystemMessages); } } } \ No newline at end of file diff --git a/src/ServiceControl/CompositeViews/Messages/GetAllMessagesForEndpointApi.cs b/src/ServiceControl/CompositeViews/Messages/GetAllMessagesForEndpointApi.cs index f3ca9f1814..bfd76cf7f7 100644 --- a/src/ServiceControl/CompositeViews/Messages/GetAllMessagesForEndpointApi.cs +++ b/src/ServiceControl/CompositeViews/Messages/GetAllMessagesForEndpointApi.cs @@ -4,34 +4,23 @@ namespace ServiceControl.CompositeViews.Messages using System.Collections.Generic; using System.Net.Http; using System.Threading.Tasks; - using Raven.Client; - using Raven.Client.Linq; using ServiceBus.Management.Infrastructure.Settings; using ServiceControl.Persistence; using ServiceControl.Persistence.Infrastructure; - class GetAllMessagesForEndpointApi : ScatterGatherApiMessageView + class GetAllMessagesForEndpointApi : ScatterGatherApiMessageView { - public GetAllMessagesForEndpointApi(IDocumentStore documentStore, Settings settings, Func httpClientFactory) : base(documentStore, settings, httpClientFactory) + public GetAllMessagesForEndpointApi(IErrorMessageDataStore dataStore, Settings settings, Func httpClientFactory) : base(dataStore, settings, httpClientFactory) { } - protected override async Task>> LocalQuery(HttpRequestMessage request, string input) + protected override Task>> LocalQuery(HttpRequestMessage request, string endpointName) { - using (var session = Store.OpenAsyncSession()) - { - var results = await session.Query() - .IncludeSystemMessagesWhere(request) - .Where(m => m.ReceivingEndpointName == input) - .Statistics(out var stats) - .Sort(request) - .Paging(request) - .TransformWith() - .ToListAsync() - .ConfigureAwait(false); + var pagingInfo = request.GetPagingInfo(); + var sortInfo = request.GetSortInfo(); + var includeSystemMessages = request.GetIncludeSystemMessages(); - return new QueryResult>(results, stats.ToQueryStatsInfo()); - } + return DataStore.GetAllMessagesForEndpoint(endpointName, pagingInfo, sortInfo, includeSystemMessages); } } } \ No newline at end of file diff --git a/src/ServiceControl/CompositeViews/Messages/MessagesByConversationApi.cs b/src/ServiceControl/CompositeViews/Messages/MessagesByConversationApi.cs index 26ca0ac04b..7129b0a973 100644 --- a/src/ServiceControl/CompositeViews/Messages/MessagesByConversationApi.cs +++ b/src/ServiceControl/CompositeViews/Messages/MessagesByConversationApi.cs @@ -4,33 +4,23 @@ namespace ServiceControl.CompositeViews.Messages using System.Collections.Generic; using System.Net.Http; using System.Threading.Tasks; - using Raven.Client; - using Raven.Client.Linq; using ServiceBus.Management.Infrastructure.Settings; using ServiceControl.Persistence; using ServiceControl.Persistence.Infrastructure; - class MessagesByConversationApi : ScatterGatherApiMessageView + class MessagesByConversationApi : ScatterGatherApiMessageView { - public MessagesByConversationApi(IDocumentStore documentStore, Settings settings, Func httpClientFactory) : base(documentStore, settings, httpClientFactory) + public MessagesByConversationApi(IErrorMessageDataStore dataStore, Settings settings, Func httpClientFactory) : base(dataStore, settings, httpClientFactory) { } - protected override async Task>> LocalQuery(HttpRequestMessage request, string input) + protected override Task>> LocalQuery(HttpRequestMessage request, string conversationId) { - using (var session = Store.OpenAsyncSession()) - { - var results = await session.Query() - .Statistics(out var stats) - .Where(m => m.ConversationId == input) - .Sort(request) - .Paging(request) - .TransformWith() - .ToListAsync() - .ConfigureAwait(false); + var pagingInfo = request.GetPagingInfo(); + var sortInfo = request.GetSortInfo(); + var includeSystemMessages = request.GetIncludeSystemMessages(); - return new QueryResult>(results, stats.ToQueryStatsInfo()); - } + return DataStore.GetAllMessagesByConversation(conversationId, pagingInfo, sortInfo, includeSystemMessages); } } } \ No newline at end of file diff --git a/src/ServiceControl/CompositeViews/Messages/RoutedApi.cs b/src/ServiceControl/CompositeViews/Messages/RoutedApi.cs index d1cc3b9908..75f57012e9 100644 --- a/src/ServiceControl/CompositeViews/Messages/RoutedApi.cs +++ b/src/ServiceControl/CompositeViews/Messages/RoutedApi.cs @@ -56,7 +56,7 @@ async Task RemoteCall(HttpRequestMessage currentRequest, st currentRequest.Headers.Host = remoteUri.Authority; //switch the host header to the new instance host - var rawResponse = await httpClient.SendAsync(currentRequest).ConfigureAwait(false); + var rawResponse = await httpClient.SendAsync(currentRequest); return rawResponse; } diff --git a/src/ServiceControl/CompositeViews/Messages/ScatterGatherApi.cs b/src/ServiceControl/CompositeViews/Messages/ScatterGatherApi.cs index 74a20f55c8..28055cff13 100644 --- a/src/ServiceControl/CompositeViews/Messages/ScatterGatherApi.cs +++ b/src/ServiceControl/CompositeViews/Messages/ScatterGatherApi.cs @@ -12,7 +12,6 @@ namespace ServiceControl.CompositeViews.Messages using Infrastructure.WebApi; using Newtonsoft.Json; using NServiceBus.Logging; - using Raven.Client; using ServiceBus.Management.Infrastructure.Settings; using ServiceControl.Persistence.Infrastructure; @@ -26,19 +25,19 @@ abstract class ScatterGatherApiBase protected static JsonSerializer jsonSerializer = JsonSerializer.Create(JsonNetSerializerSettings.CreateDefault()); } - abstract class ScatterGatherApi : ScatterGatherApiBase, IApi + abstract class ScatterGatherApi : ScatterGatherApiBase, IApi where TOut : class { - protected ScatterGatherApi(IDocumentStore documentStore, Settings settings, Func httpClientFactory) + protected ScatterGatherApi(TDataStore store, Settings settings, Func httpClientFactory) { - Store = documentStore; + DataStore = store; Settings = settings; HttpClientFactory = httpClientFactory; } - protected IDocumentStore Store { get; } - protected Settings Settings { get; } - protected Func HttpClientFactory { get; } + protected TDataStore DataStore { get; } + Settings Settings { get; } + Func HttpClientFactory { get; } public async Task Execute(ApiController controller, TIn input) { @@ -60,14 +59,15 @@ public async Task Execute(ApiController controller, TIn inp tasks.Add(RemoteCall(currentRequest, remote.ApiAsUri, InstanceIdGenerator.FromApiUrl(remote.ApiUri))); } - var response = AggregateResults(currentRequest, await Task.WhenAll(tasks).ConfigureAwait(false)); + var results = await Task.WhenAll(tasks); + var response = AggregateResults(currentRequest, results); return Negotiator.FromQueryResult(currentRequest, response); } async Task> LocalCall(HttpRequestMessage request, TIn input, string instanceId) { - var result = await LocalQuery(request, input).ConfigureAwait(false); + var result = await LocalQuery(request, input); result.InstanceId = instanceId; return result; } @@ -93,13 +93,14 @@ protected virtual QueryStatsInfo AggregateStats(IEnumerable> r return new QueryStatsInfo( string.Join("", infos.OrderBy(x => x.ETag).Select(x => x.ETag)), infos.Sum(x => x.TotalCount), + isStale: infos.Any(x => x.IsStale), infos.Max(x => x.HighestTotalCountOfAllTheInstances) ); } async Task> RemoteCall(HttpRequestMessage currentRequest, Uri remoteUri, string instanceId) { - var fetched = await FetchAndParse(currentRequest, remoteUri).ConfigureAwait(false); + var fetched = await FetchAndParse(currentRequest, remoteUri); fetched.InstanceId = instanceId; return fetched; } @@ -111,15 +112,14 @@ async Task> FetchAndParse(HttpRequestMessage currentRequest, U try { // Assuming SendAsync returns uncompressed response and the AutomaticDecompression is enabled on the http client. - var rawResponse = await httpClient.SendAsync(new HttpRequestMessage(HttpMethod.Get, instanceUri)) - .ConfigureAwait(false); + var rawResponse = await httpClient.SendAsync(new HttpRequestMessage(HttpMethod.Get, instanceUri)); // special case - queried by conversation ID and nothing was found if (rawResponse.StatusCode == HttpStatusCode.NotFound) { return QueryResult.Empty(); } - return await ParseResult(rawResponse).ConfigureAwait(false); + return await ParseResult(rawResponse); } catch (HttpRequestException httpRequestException) { @@ -141,7 +141,7 @@ void DisableRemoteInstance(Uri remoteUri) static async Task> ParseResult(HttpResponseMessage responseMessage) { - using (var responseStream = await responseMessage.Content.ReadAsStreamAsync().ConfigureAwait(false)) + using (var responseStream = await responseMessage.Content.ReadAsStreamAsync()) using (var jsonReader = new JsonTextReader(new StreamReader(responseStream))) { var remoteResults = jsonSerializer.Deserialize(jsonReader); @@ -152,23 +152,28 @@ static async Task> ParseResult(HttpResponseMessage responseMes totalCount = int.Parse(totalCounts.ElementAt(0)); } - string etag = null; - if (responseMessage.Headers.TryGetValues("ETag", out var etags)) + string etag = responseMessage.Headers.ETag?.Tag; + if (etag != null) { - etag = etags.ElementAt(0); + // Strip quotes from Etag, checking for " which isn't really needed as Etag always has quotes but not 100% certain. + // Later the value is joined into a new Etag when the results are aggregated and returned + if (etag.StartsWith("\"")) + { + etag = etag.Substring(1, etag.Length - 2); + } } - return new QueryResult(remoteResults, new QueryStatsInfo(etag, totalCount)); + return new QueryResult(remoteResults, new QueryStatsInfo(etag, totalCount, isStale: false)); } } - static ILog logger = LogManager.GetLogger(typeof(ScatterGatherApi)); + static readonly ILog logger = LogManager.GetLogger(typeof(ScatterGatherApi)); } - abstract class ScatterGatherApiNoInput : ScatterGatherApi + abstract class ScatterGatherApiNoInput : ScatterGatherApi where TOut : class { - protected ScatterGatherApiNoInput(IDocumentStore documentStore, Settings settings, Func httpClientFactory) : base(documentStore, settings, httpClientFactory) + protected ScatterGatherApiNoInput(TStore store, Settings settings, Func httpClientFactory) : base(store, settings, httpClientFactory) { } diff --git a/src/ServiceControl/CompositeViews/Messages/ScatterGatherApiMessageView.cs b/src/ServiceControl/CompositeViews/Messages/ScatterGatherApiMessageView.cs index 54e0d3ccf2..4b7f1ecc79 100644 --- a/src/ServiceControl/CompositeViews/Messages/ScatterGatherApiMessageView.cs +++ b/src/ServiceControl/CompositeViews/Messages/ScatterGatherApiMessageView.cs @@ -4,13 +4,12 @@ namespace ServiceControl.CompositeViews.Messages using System.Collections.Generic; using System.Linq; using System.Net.Http; - using Raven.Client; using ServiceBus.Management.Infrastructure.Settings; using ServiceControl.Persistence.Infrastructure; - abstract class ScatterGatherApiMessageView : ScatterGatherApi> + abstract class ScatterGatherApiMessageView : ScatterGatherApi> { - protected ScatterGatherApiMessageView(IDocumentStore documentStore, Settings settings, Func httpClientFactory) : base(documentStore, settings, httpClientFactory) + protected ScatterGatherApiMessageView(TDataStore dataStore, Settings settings, Func httpClientFactory) : base(dataStore, settings, httpClientFactory) { } diff --git a/src/ServiceControl/CompositeViews/Messages/SearchApi.cs b/src/ServiceControl/CompositeViews/Messages/SearchApi.cs index 1fa496faac..f57a8f4887 100644 --- a/src/ServiceControl/CompositeViews/Messages/SearchApi.cs +++ b/src/ServiceControl/CompositeViews/Messages/SearchApi.cs @@ -4,32 +4,21 @@ namespace ServiceControl.CompositeViews.Messages using System.Collections.Generic; using System.Net.Http; using System.Threading.Tasks; - using Raven.Client; using ServiceBus.Management.Infrastructure.Settings; using ServiceControl.Persistence; using ServiceControl.Persistence.Infrastructure; - class SearchApi : ScatterGatherApiMessageView + class SearchApi : ScatterGatherApiMessageView { - public SearchApi(IDocumentStore documentStore, Settings settings, Func httpClientFactory) : base(documentStore, settings, httpClientFactory) + public SearchApi(IErrorMessageDataStore dataStore, Settings settings, Func httpClientFactory) : base(dataStore, settings, httpClientFactory) { } - protected override async Task>> LocalQuery(HttpRequestMessage request, string input) + protected override Task>> LocalQuery(HttpRequestMessage request, string input) { - using (var session = Store.OpenAsyncSession()) - { - var results = await session.Query() - .Statistics(out var stats) - .Search(x => x.Query, input) - .Sort(request) - .Paging(request) - .TransformWith() - .ToListAsync() - .ConfigureAwait(false); - - return new QueryResult>(results, stats.ToQueryStatsInfo()); - } + var pagingInfo = request.GetPagingInfo(); + var sortInfo = request.GetSortInfo(); + return DataStore.GetAllMessagesForSearch(input, pagingInfo, sortInfo); } } } \ No newline at end of file diff --git a/src/ServiceControl/CompositeViews/Messages/SearchEndpointApi.cs b/src/ServiceControl/CompositeViews/Messages/SearchEndpointApi.cs index 452bbbd47a..5154687bce 100644 --- a/src/ServiceControl/CompositeViews/Messages/SearchEndpointApi.cs +++ b/src/ServiceControl/CompositeViews/Messages/SearchEndpointApi.cs @@ -4,34 +4,19 @@ namespace ServiceControl.CompositeViews.Messages using System.Collections.Generic; using System.Net.Http; using System.Threading.Tasks; - using Raven.Client; - using Raven.Client.Linq; using ServiceBus.Management.Infrastructure.Settings; using ServiceControl.Persistence; using ServiceControl.Persistence.Infrastructure; - class SearchEndpointApi : ScatterGatherApiMessageView + class SearchEndpointApi : ScatterGatherApiMessageView { - public SearchEndpointApi(IDocumentStore documentStore, Settings settings, Func httpClientFactory) : base(documentStore, settings, httpClientFactory) + public SearchEndpointApi(IErrorMessageDataStore dataStore, Settings settings, Func httpClientFactory) : base(dataStore, settings, httpClientFactory) { } - protected override async Task>> LocalQuery(HttpRequestMessage request, Input input) + protected override Task>> LocalQuery(HttpRequestMessage request, Input input) { - using (var session = Store.OpenAsyncSession()) - { - var results = await session.Query() - .Statistics(out var stats) - .Search(x => x.Query, input.Keyword) - .Where(m => m.ReceivingEndpointName == input.Endpoint) - .Sort(request) - .Paging(request) - .TransformWith() - .ToListAsync() - .ConfigureAwait(false); - - return new QueryResult>(results, stats.ToQueryStatsInfo()); - } + return DataStore.SearchEndpointMessages(input.Endpoint, input.Keyword, request.GetPagingInfo(), request.GetSortInfo()); } public class Input diff --git a/src/ServiceControl/Connection/ConnectionController.cs b/src/ServiceControl/Connection/ConnectionController.cs index 26f968e5a9..0c1da0ec04 100644 --- a/src/ServiceControl/Connection/ConnectionController.cs +++ b/src/ServiceControl/Connection/ConnectionController.cs @@ -15,7 +15,7 @@ class ConnectionController : ApiController [HttpGet] public async Task GetConnectionDetails() { - var connectionDetails = await connectionBuilder.BuildPlatformConnection().ConfigureAwait(false); + var connectionDetails = await connectionBuilder.BuildPlatformConnection(); return Json( new diff --git a/src/ServiceControl/Connection/PlatformConnectionBuilder.cs b/src/ServiceControl/Connection/PlatformConnectionBuilder.cs index f0a6527282..eda437446f 100644 --- a/src/ServiceControl/Connection/PlatformConnectionBuilder.cs +++ b/src/ServiceControl/Connection/PlatformConnectionBuilder.cs @@ -18,7 +18,7 @@ public async Task BuildPlatformConnection() await Task.WhenAll( from provider in platformConnectionProviders select provider.ProvideConnectionDetails(connectionDetails) - ).ConfigureAwait(false); + ); return connectionDetails; } diff --git a/src/ServiceControl/Connection/RemotePlatformConnectionDetailsProvider.cs b/src/ServiceControl/Connection/RemotePlatformConnectionDetailsProvider.cs index 5a0cc2316e..0281287c7f 100644 --- a/src/ServiceControl/Connection/RemotePlatformConnectionDetailsProvider.cs +++ b/src/ServiceControl/Connection/RemotePlatformConnectionDetailsProvider.cs @@ -33,8 +33,7 @@ async Task UpdateFromRemote(RemoteInstanceSetting remote, PlatformConnectionDeta var client = httpClientFactory(); try { - var result = await client.GetStringAsync(remoteConnectionUri) - .ConfigureAwait(false); + var result = await client.GetStringAsync(remoteConnectionUri); var dictionary = JsonConvert.DeserializeObject>(result); if (dictionary == null) { diff --git a/src/ServiceControl/Contracts/MessageFailures/FailedMessagesUnArchived.cs b/src/ServiceControl/Contracts/MessageFailures/FailedMessagesUnArchived.cs index 03d74f42f0..e1d16c987e 100644 --- a/src/ServiceControl/Contracts/MessageFailures/FailedMessagesUnArchived.cs +++ b/src/ServiceControl/Contracts/MessageFailures/FailedMessagesUnArchived.cs @@ -5,6 +5,6 @@ public class FailedMessagesUnArchived : IDomainEvent { public int MessagesCount { get; set; } - public string[] DocumentIds { get; set; } + public string[] DocumentIds { get; set; } //REFACTOR: The name suggest that this leaks persister specific info into the property name and/or value } } \ No newline at end of file diff --git a/src/ServiceControl/Contracts/MessageFailures/MessageFailuresUpdated.cs b/src/ServiceControl/Contracts/MessageFailures/MessageFailuresUpdated.cs index 823c1aa238..5fac13982d 100644 --- a/src/ServiceControl/Contracts/MessageFailures/MessageFailuresUpdated.cs +++ b/src/ServiceControl/Contracts/MessageFailures/MessageFailuresUpdated.cs @@ -11,7 +11,9 @@ public MessageFailuresUpdated() RaisedAt = DateTime.UtcNow; } - public int Total { get; set; } + [Obsolete] + public int Total => UnresolvedTotal; // Left here for backwards compatibility, to be removed eventually. + public DateTime RaisedAt { get; set; } public int ArchivedTotal { get; set; } public int UnresolvedTotal { get; set; } diff --git a/src/ServiceControl/CustomChecks/CustomCheckFailedPublisher.cs b/src/ServiceControl/CustomChecks/CustomCheckFailedPublisher.cs index 5dceb6a8ae..ed104b7760 100644 --- a/src/ServiceControl/CustomChecks/CustomCheckFailedPublisher.cs +++ b/src/ServiceControl/CustomChecks/CustomCheckFailedPublisher.cs @@ -6,7 +6,6 @@ namespace ServiceControl.CustomChecks using System.Threading.Tasks; using Contracts.CustomChecks; using ExternalIntegrations; - using Raven.Client; class CustomCheckFailedPublisher : EventPublisher { @@ -24,7 +23,7 @@ protected override DispatchContext CreateDispatchRequest(CustomCheckFailed @even }; } - protected override Task> PublishEvents(IEnumerable contexts, IAsyncDocumentSession session) + protected override Task> PublishEvents(IEnumerable contexts) { return Task.FromResult(contexts.Select(r => (object)new Contracts.CustomCheckFailed { diff --git a/src/ServiceControl/CustomChecks/CustomCheckNotifications.cs b/src/ServiceControl/CustomChecks/CustomCheckNotifications.cs deleted file mode 100644 index df1246a540..0000000000 --- a/src/ServiceControl/CustomChecks/CustomCheckNotifications.cs +++ /dev/null @@ -1,66 +0,0 @@ -namespace ServiceControl.CustomChecks -{ - using System; - using System.Threading.Tasks; - using Infrastructure.DomainEvents; - using NServiceBus.Logging; - using Raven.Abstractions.Data; - using Raven.Client; - using ServiceControl.Contracts.CustomChecks; - using ServiceControl.Persistence; - - class CustomCheckNotifications : IObserver - { - public CustomCheckNotifications(IDocumentStore store, IDomainEvents domainEvents) - { - this.store = store; - this.domainEvents = domainEvents; - } - - public void OnNext(IndexChangeNotification value) - { - try - { - UpdateCount().GetAwaiter().GetResult(); - } - catch (Exception ex) - { - logging.WarnFormat("Failed to emit CustomCheckUpdated - {0}", ex); - } - } - - public void OnError(Exception error) - { - //Ignore - } - - public void OnCompleted() - { - //Ignore - } - - async Task UpdateCount() - { - using (var session = store.OpenAsyncSession()) - { - var failedCustomCheckCount = await session.Query().CountAsync(p => p.Status == Status.Fail) - .ConfigureAwait(false); - if (lastCount == failedCustomCheckCount) - { - return; - } - - lastCount = failedCustomCheckCount; - await domainEvents.Raise(new CustomChecksUpdated - { - Failed = lastCount - }).ConfigureAwait(false); - } - } - - IDomainEvents domainEvents; - IDocumentStore store; - int lastCount; - ILog logging = LogManager.GetLogger(typeof(CustomCheckNotifications)); - } -} \ No newline at end of file diff --git a/src/ServiceControl/CustomChecks/CustomCheckResultProcessor.cs b/src/ServiceControl/CustomChecks/CustomCheckResultProcessor.cs index 27f4905c6b..3f3f8c9217 100644 --- a/src/ServiceControl/CustomChecks/CustomCheckResultProcessor.cs +++ b/src/ServiceControl/CustomChecks/CustomCheckResultProcessor.cs @@ -20,8 +20,21 @@ public async Task ProcessResult(CustomCheckDetail checkDetail) { try { - var statusChange = await store.UpdateCustomCheckStatus(checkDetail).ConfigureAwait(false); - await RaiseEvents(statusChange, checkDetail).ConfigureAwait(false); + var statusChange = await store.UpdateCustomCheckStatus(checkDetail); + await RaiseEvents(statusChange, checkDetail); + + var numberOfFailedChecks = await store.GetNumberOfFailedChecks(); + + if (lastCount == numberOfFailedChecks) + { + return; + } + lastCount = numberOfFailedChecks; + + await domainEvents.Raise(new CustomChecksUpdated + { + Failed = numberOfFailedChecks + }); } catch (Exception ex) { @@ -45,7 +58,7 @@ await domainEvents.Raise(new CustomCheckFailed FailedAt = detail.ReportedAt, FailureReason = detail.FailureReason, OriginatingEndpoint = detail.OriginatingEndpoint - }).ConfigureAwait(false); + }); } else { @@ -56,7 +69,7 @@ await domainEvents.Raise(new CustomCheckSucceeded Category = detail.Category, SucceededAt = detail.ReportedAt, OriginatingEndpoint = detail.OriginatingEndpoint - }).ConfigureAwait(false); + }); } } } @@ -64,6 +77,8 @@ await domainEvents.Raise(new CustomCheckSucceeded readonly IDomainEvents domainEvents; readonly ICustomChecksDataStore store; + int lastCount; + static ILog Logger = LogManager.GetLogger(); } } \ No newline at end of file diff --git a/src/ServiceControl/CustomChecks/CustomCheckSucceededPublisher.cs b/src/ServiceControl/CustomChecks/CustomCheckSucceededPublisher.cs index 8527ea6fd9..6ea417b2ef 100644 --- a/src/ServiceControl/CustomChecks/CustomCheckSucceededPublisher.cs +++ b/src/ServiceControl/CustomChecks/CustomCheckSucceededPublisher.cs @@ -6,7 +6,6 @@ namespace ServiceControl.CustomChecks using System.Threading.Tasks; using Contracts.CustomChecks; using ExternalIntegrations; - using Raven.Client; class CustomCheckSucceededPublisher : EventPublisher { @@ -23,7 +22,7 @@ protected override DispatchContext CreateDispatchRequest(CustomCheckSucceeded @e }; } - protected override Task> PublishEvents(IEnumerable contexts, IAsyncDocumentSession session) + protected override Task> PublishEvents(IEnumerable contexts) { return Task.FromResult(contexts.Select(r => (object)new Contracts.CustomCheckSucceeded { diff --git a/src/ServiceControl/CustomChecks/CustomChecksComponent.cs b/src/ServiceControl/CustomChecks/CustomChecksComponent.cs index 799e3a99fc..837d6694b2 100644 --- a/src/ServiceControl/CustomChecks/CustomChecksComponent.cs +++ b/src/ServiceControl/CustomChecks/CustomChecksComponent.cs @@ -13,7 +13,6 @@ public override void Configure(Settings settings, IHostBuilder hostBuilder) { hostBuilder.ConfigureServices((ctx, serviceCollection) => { - serviceCollection.AddHostedService(); serviceCollection.AddIntegrationEventPublisher(); serviceCollection.AddIntegrationEventPublisher(); serviceCollection.AddEventLogMapping(); @@ -23,15 +22,5 @@ public override void Configure(Settings settings, IHostBuilder hostBuilder) serviceCollection.AddSingleton(); }); } - - public override void Setup(Settings settings, IComponentInstallationContext context) - { - // TODO: Move this in the persister project - if (settings.DataStoreType == DataStoreType.SqlDb) - { - var connectionString = SettingsReader.Read("SqlStorageConnectionString"); - context.RegisterInstallationTask(() => Persistence.SetupSqlPersistence.SetupCustomChecks(connectionString)); - } - } } } \ No newline at end of file diff --git a/src/ServiceControl/CustomChecks/CustomChecksHostedService.cs b/src/ServiceControl/CustomChecks/CustomChecksHostedService.cs deleted file mode 100644 index c681469833..0000000000 --- a/src/ServiceControl/CustomChecks/CustomChecksHostedService.cs +++ /dev/null @@ -1,35 +0,0 @@ -namespace ServiceControl.CustomChecks -{ - using System; - using System.Threading; - using System.Threading.Tasks; - using Infrastructure.DomainEvents; - using Microsoft.Extensions.Hosting; - using Raven.Client; - using ServiceControl.Persistence; - - class CustomChecksHostedService : IHostedService - { - IDocumentStore store; - CustomCheckNotifications notifications; - IDisposable subscription; - - public CustomChecksHostedService(IDocumentStore store, IDomainEvents domainEvents) - { - this.store = store; - notifications = new CustomCheckNotifications(store, domainEvents); - } - - public Task StartAsync(CancellationToken cancellationToken) - { - subscription = store.Changes().ForIndex(new CustomChecksIndex().IndexName).Subscribe(notifications); - return Task.FromResult(0); - } - - public Task StopAsync(CancellationToken cancellationToken) - { - subscription.Dispose(); - return Task.FromResult(0); - } - } -} \ No newline at end of file diff --git a/src/ServiceControl/CustomChecks/DeleteCustomCheckHandler.cs b/src/ServiceControl/CustomChecks/DeleteCustomCheckHandler.cs index abb7e3459a..95d2d973e6 100644 --- a/src/ServiceControl/CustomChecks/DeleteCustomCheckHandler.cs +++ b/src/ServiceControl/CustomChecks/DeleteCustomCheckHandler.cs @@ -15,11 +15,9 @@ public DeleteCustomCheckHandler(ICustomChecksDataStore customChecksDataStore, ID public async Task Handle(DeleteCustomCheck message, IMessageHandlerContext context) { - await customChecksDataStore.DeleteCustomCheck(message.Id) - .ConfigureAwait(false); + await customChecksDataStore.DeleteCustomCheck(message.Id); - await domainEvents.Raise(new CustomCheckDeleted { Id = message.Id }) - .ConfigureAwait(false); + await domainEvents.Raise(new CustomCheckDeleted { Id = message.Id }); } ICustomChecksDataStore customChecksDataStore; diff --git a/src/ServiceControl/Operations/FailedErrorImportCustomCheck.cs b/src/ServiceControl/CustomChecks/FailedErrorImportCustomCheck.cs similarity index 58% rename from src/ServiceControl/Operations/FailedErrorImportCustomCheck.cs rename to src/ServiceControl/CustomChecks/FailedErrorImportCustomCheck.cs index dba7f4de50..264343d9d5 100644 --- a/src/ServiceControl/Operations/FailedErrorImportCustomCheck.cs +++ b/src/ServiceControl/CustomChecks/FailedErrorImportCustomCheck.cs @@ -4,11 +4,11 @@ using System.Threading.Tasks; using NServiceBus.CustomChecks; using NServiceBus.Logging; - using Raven.Client; + using Persistence; class FailedErrorImportCustomCheck : CustomCheck { - public FailedErrorImportCustomCheck(IDocumentStore store) + public FailedErrorImportCustomCheck(IFailedErrorImportDataStore store) : base("Error Message Ingestion", "ServiceControl Health", TimeSpan.FromHours(1)) { this.store = store; @@ -16,25 +16,20 @@ public FailedErrorImportCustomCheck(IDocumentStore store) public override async Task PerformCheck() { - using (var session = store.OpenAsyncSession()) + var hasFailedImports = await store.QueryContainsFailedImports(); + + if (hasFailedImports) { - var query = session.Query(); - using (var ie = await session.Advanced.StreamAsync(query).ConfigureAwait(false)) - { - if (await ie.MoveNextAsync().ConfigureAwait(false)) - { - Logger.Warn(message); - return CheckResult.Failed(message); - } - } + Logger.Warn(Message); + return CheckResult.Failed(Message); } return CheckResult.Pass; } - readonly IDocumentStore store; + readonly IFailedErrorImportDataStore store; - const string message = @"One or more error messages have failed to import properly into ServiceControl and have been stored in the ServiceControl database. + const string Message = @"One or more error messages have failed to import properly into ServiceControl and have been stored in the ServiceControl database. The import of these messages could have failed for a number of reasons and ServiceControl is not able to automatically reimport them. For guidance on how to resolve this see https://docs.particular.net/servicecontrol/import-failed-messages"; static readonly ILog Logger = LogManager.GetLogger(typeof(FailedErrorImportCustomCheck)); diff --git a/src/ServiceControl/CustomChecks/InternalCustomChecks/InternalCustomCheckManager.cs b/src/ServiceControl/CustomChecks/InternalCustomChecks/InternalCustomCheckManager.cs index 72792779b9..2ee2349a19 100644 --- a/src/ServiceControl/CustomChecks/InternalCustomChecks/InternalCustomCheckManager.cs +++ b/src/ServiceControl/CustomChecks/InternalCustomChecks/InternalCustomCheckManager.cs @@ -38,8 +38,7 @@ async Task Run(CancellationToken cancellationToken) CheckResult result = default; try { - result = await check.PerformCheck() - .ConfigureAwait(false); + result = await check.PerformCheck(); } catch (OperationCanceledException) { @@ -61,7 +60,7 @@ async Task Run(CancellationToken cancellationToken) FailureReason = result.FailureReason }; - await checkResultProcessor.ProcessResult(detail).ConfigureAwait(false); + await checkResultProcessor.ProcessResult(detail); return check.Interval.HasValue ? TimerJobExecutionResult.ScheduleNextExecution diff --git a/src/ServiceControl/CustomChecks/InternalCustomChecks/InternalCustomChecks.cs b/src/ServiceControl/CustomChecks/InternalCustomChecks/InternalCustomChecks.cs index f2c1f573b2..d261bdbf1d 100644 --- a/src/ServiceControl/CustomChecks/InternalCustomChecks/InternalCustomChecks.cs +++ b/src/ServiceControl/CustomChecks/InternalCustomChecks/InternalCustomChecks.cs @@ -17,9 +17,7 @@ public static IHostBuilder UseInternalCustomChecks(this IHostBuilder hostBuilder { collection.AddCustomCheck(); collection.AddCustomCheck(); - collection.AddCustomCheck(); - collection.AddCustomCheck(); - collection.AddCustomCheck(); + collection.AddHostedService(provider => new InternalCustomChecksHostedService( provider.GetServices().ToList(), diff --git a/src/ServiceControl/CustomChecks/InternalCustomChecks/InternalCustomChecksHostedService.cs b/src/ServiceControl/CustomChecks/InternalCustomChecks/InternalCustomChecksHostedService.cs index c8d39583b8..c268f3b750 100644 --- a/src/ServiceControl/CustomChecks/InternalCustomChecks/InternalCustomChecksHostedService.cs +++ b/src/ServiceControl/CustomChecks/InternalCustomChecks/InternalCustomChecksHostedService.cs @@ -47,8 +47,7 @@ public async Task StopAsync(CancellationToken cancellationToken) { if (managers.Any()) { - await Task.WhenAll(managers.Select(m => m.Stop()).ToArray()) - .ConfigureAwait(false); + await Task.WhenAll(managers.Select(m => m.Stop()).ToArray()); } } diff --git a/src/ServiceControl/CustomChecks/InternalCustomChecks/InternalCustomChecksServiceCollectionExtensions.cs b/src/ServiceControl/CustomChecks/InternalCustomChecks/InternalCustomChecksServiceCollectionExtensions.cs deleted file mode 100644 index 04effd9d24..0000000000 --- a/src/ServiceControl/CustomChecks/InternalCustomChecks/InternalCustomChecksServiceCollectionExtensions.cs +++ /dev/null @@ -1,15 +0,0 @@ - -namespace ServiceControl.CustomChecks -{ - using Microsoft.Extensions.DependencyInjection; - using NServiceBus.CustomChecks; - - static class InternalCustomChecksServiceCollectionExtensions - { - public static void AddCustomCheck(this IServiceCollection serviceCollection) - where T : class, ICustomCheck - { - serviceCollection.AddTransient(); - } - } -} \ No newline at end of file diff --git a/src/ServiceControl/CustomChecks/ReportCustomCheckResultHandler.cs b/src/ServiceControl/CustomChecks/ReportCustomCheckResultHandler.cs index 30888a36a5..122b49ac74 100644 --- a/src/ServiceControl/CustomChecks/ReportCustomCheckResultHandler.cs +++ b/src/ServiceControl/CustomChecks/ReportCustomCheckResultHandler.cs @@ -46,7 +46,7 @@ public async Task Handle(ReportCustomCheckResult message, IMessageHandlerContext FailureReason = message.FailureReason }; - await checkResultProcessor.ProcessResult(checkDetails).ConfigureAwait(false); + await checkResultProcessor.ProcessResult(checkDetails); } readonly CustomCheckResultProcessor checkResultProcessor; diff --git a/src/ServiceControl/CustomChecks/Web/CustomCheckController.cs b/src/ServiceControl/CustomChecks/Web/CustomCheckController.cs index 25622cad0b..3bc26ab616 100644 --- a/src/ServiceControl/CustomChecks/Web/CustomCheckController.cs +++ b/src/ServiceControl/CustomChecks/Web/CustomCheckController.cs @@ -24,7 +24,7 @@ public CustomCheckController(ICustomChecksDataStore customChecksDataStore, IMess public async Task CustomChecks(string status = null) { var paging = Request.GetPagingInfo(); - var stats = await customChecksDataStore.GetStats(paging, status).ConfigureAwait(false); + var stats = await customChecksDataStore.GetStats(paging, status); return Negotiator .FromModel(Request, stats.Results) .WithPagingLinksAndTotalCount(stats.QueryStats.TotalCount, Request) @@ -35,7 +35,7 @@ public async Task CustomChecks(string status = null) [HttpDelete] public async Task Delete(Guid id) { - await messageSession.SendLocal(new DeleteCustomCheck { Id = id }).ConfigureAwait(false); + await messageSession.SendLocal(new DeleteCustomCheck { Id = id }); return StatusCode(HttpStatusCode.Accepted); } diff --git a/src/ServiceControl/EventLog/AuditEventLogWriter.cs b/src/ServiceControl/EventLog/AuditEventLogWriter.cs index 81a492c2b3..c41cb4f4d3 100644 --- a/src/ServiceControl/EventLog/AuditEventLogWriter.cs +++ b/src/ServiceControl/EventLog/AuditEventLogWriter.cs @@ -4,7 +4,7 @@ using Contracts.EventLog; using Infrastructure.DomainEvents; using Infrastructure.SignalR; - using Raven.Client; + using ServiceControl.Persistence; /// /// Only for events that have been defined (under EventLog\Definitions), a logentry item will @@ -12,10 +12,10 @@ /// class AuditEventLogWriter : IDomainHandler { - public AuditEventLogWriter(GlobalEventHandler broadcaster, IDocumentStore store, EventLogMappings mappings) + public AuditEventLogWriter(GlobalEventHandler broadcaster, IErrorMessageDataStore dataStore, EventLogMappings mappings) { this.broadcaster = broadcaster; - this.store = store; + this.dataStore = dataStore; this.mappings = mappings; } @@ -28,13 +28,7 @@ public async Task Handle(IDomainEvent message) var logItem = mappings.ApplyMapping(message); - using (var session = store.OpenAsyncSession()) - { - await session.StoreAsync(logItem) - .ConfigureAwait(false); - await session.SaveChangesAsync() - .ConfigureAwait(false); - } + await dataStore.StoreEventLogItem(logItem); await broadcaster.Broadcast(new EventLogItemAdded { @@ -47,11 +41,11 @@ await broadcaster.Broadcast(new EventLogItemAdded // The reason is because this data is not useful for end users, so for now we just empty it. // At the moment too much data is being populated in this field, and this has significant down sides to the amount of data we are sending down to ServicePulse (it actually crashes it). RelatedTo = emptyArray - }).ConfigureAwait(false); + }); } readonly GlobalEventHandler broadcaster; - readonly IDocumentStore store; + readonly IErrorMessageDataStore dataStore; readonly EventLogMappings mappings; static string[] emptyArray = new string[0]; } diff --git a/src/ServiceControl/EventLog/EventLogApiController.cs b/src/ServiceControl/EventLog/EventLogApiController.cs index dd30873e95..ca45e95057 100644 --- a/src/ServiceControl/EventLog/EventLogApiController.cs +++ b/src/ServiceControl/EventLog/EventLogApiController.cs @@ -1,37 +1,33 @@ namespace ServiceControl.EventLog { - using System.Linq; using System.Net.Http; using System.Threading.Tasks; using System.Web.Http; using Infrastructure.WebApi; - using Raven.Client; + using Persistence.Infrastructure; using ServiceControl.Persistence; class EventLogApiController : ApiController { - public EventLogApiController(IDocumentStore documentStore) + public EventLogApiController(IEventLogDataStore eventLogDataStore) { - this.documentStore = documentStore; + this.eventLogDataStore = eventLogDataStore; } [Route("eventlogitems")] [HttpGet] public async Task Items() { - using (var session = documentStore.OpenAsyncSession()) - { - var results = await session.Query().Statistics(out var stats).OrderByDescending(p => p.RaisedAt) - .Paging(Request) - .ToListAsync() - .ConfigureAwait(false); + var pagingInfo = Request.GetPagingInfo(); - return Negotiator.FromModel(Request, results) - .WithPagingLinksAndTotalCount(stats.TotalResults, Request) - .WithEtag(stats); - } + var (results, totalCount, version) = await eventLogDataStore.GetEventLogItems(pagingInfo); + + + return Negotiator.FromModel(Request, results) + .WithPagingLinksAndTotalCount(totalCount, Request) + .WithEtag(version); } - readonly IDocumentStore documentStore; + readonly IEventLogDataStore eventLogDataStore; } } \ No newline at end of file diff --git a/src/ServiceControl/ExternalIntegrations/EventDispatcherHostedService.cs b/src/ServiceControl/ExternalIntegrations/EventDispatcherHostedService.cs index 116718e62c..564e61dcb1 100644 --- a/src/ServiceControl/ExternalIntegrations/EventDispatcherHostedService.cs +++ b/src/ServiceControl/ExternalIntegrations/EventDispatcherHostedService.cs @@ -2,26 +2,24 @@ { using System; using System.Collections.Generic; - using System.Linq; - using System.Reactive.Linq; using System.Threading; using System.Threading.Tasks; using Infrastructure.DomainEvents; using Microsoft.Extensions.Hosting; using NServiceBus; using NServiceBus.Logging; - using Raven.Abstractions.Data; - using Raven.Client; - using ServiceBus.Management.Infrastructure.Extensions; - using ServiceBus.Management.Infrastructure.Settings; + using Persistence; class EventDispatcherHostedService : IHostedService { - public EventDispatcherHostedService(IDocumentStore store, IDomainEvents domainEvents, CriticalError criticalError, Settings settings, IEnumerable eventPublishers, IMessageSession messageSession) + public EventDispatcherHostedService( + IExternalIntegrationRequestsDataStore store, + IDomainEvents domainEvents, + IEnumerable eventPublishers, + IMessageSession messageSession + ) { this.store = store; - this.criticalError = criticalError; - this.settings = settings; this.eventPublishers = eventPublishers; this.domainEvents = domainEvents; this.messageSession = messageSession; @@ -29,189 +27,63 @@ public EventDispatcherHostedService(IDocumentStore store, IDomainEvents domainEv public Task StartAsync(CancellationToken cancellationToken) { - subscription = store.Changes().ForDocumentsStartingWith("ExternalIntegrationDispatchRequests").Where(c => c.Type == DocumentChangeTypes.Put).Subscribe(OnNext); + store.Subscribe(TryDispatchEventBatch); - tokenSource = new CancellationTokenSource(); - circuitBreaker = new RepeatedFailuresOverTimeCircuitBreaker("EventDispatcher", - TimeSpan.FromMinutes(5), - ex => criticalError.Raise("Repeated failures when dispatching external integration events.", ex), - TimeSpan.FromSeconds(20)); - - StartDispatcher(); return Task.FromResult(0); } - void OnNext(DocumentChangeNotification documentChangeNotification) - { - latestEtag = Etag.Max(documentChangeNotification.Etag, latestEtag); - signal.Set(); - } - - void StartDispatcher() - { - task = StartDispatcherTask(); - } - - async Task StartDispatcherTask() + async Task TryDispatchEventBatch(object[] allContexts) { - try + var eventsToBePublished = new List(); + foreach (var publisher in eventPublishers) { - await DispatchEvents(tokenSource.Token).ConfigureAwait(false); - do - { - try - { - await signal.WaitHandle.WaitOneAsync(tokenSource.Token).ConfigureAwait(false); - signal.Reset(); - } - catch (OperationCanceledException) - { - break; - } - - await DispatchEvents(tokenSource.Token).ConfigureAwait(false); - } - while (!tokenSource.IsCancellationRequested); - } - catch (OperationCanceledException) - { - // ignore + var events = await publisher.PublishEventsForOwnContexts(allContexts); + eventsToBePublished.AddRange(events); } - catch (Exception ex) - { - Logger.Error("An exception occurred when dispatching external integration events", ex); - await circuitBreaker.Failure(ex).ConfigureAwait(false); - if (!tokenSource.IsCancellationRequested) - { - StartDispatcher(); - } - } - } - - async Task DispatchEvents(CancellationToken cancellationToken) - { - bool more; - - do + foreach (var eventToBePublished in eventsToBePublished) { - more = await TryDispatchEventBatch() - .ConfigureAwait(false); - - circuitBreaker.Success(); - - if (more && !cancellationToken.IsCancellationRequested) - { - //if there is more events to dispatch we sleep for a bit and then we go again - await Task.Delay(1000, CancellationToken.None).ConfigureAwait(false); - } - } - while (!cancellationToken.IsCancellationRequested && more); - } - - async Task TryDispatchEventBatch() - { - using (var session = store.OpenAsyncSession()) - { - var awaitingDispatching = await session - .Query() - .Statistics(out var stats) - .Take(settings.ExternalIntegrationsDispatchingBatchSize) - .ToListAsync() - .ConfigureAwait(false); - - if (awaitingDispatching.Count == 0) - { - // If the index hasn't caught up, try again - return stats.IndexEtag.CompareTo(latestEtag) < 0; - } - - var allContexts = awaitingDispatching.Select(r => r.DispatchContext).ToArray(); if (Logger.IsDebugEnabled) { - Logger.Debug($"Dispatching {allContexts.Length} events."); + Logger.Debug("Publishing external event on the bus."); } - var eventsToBePublished = new List(); - foreach (var publisher in eventPublishers) + try { - var events = await publisher.PublishEventsForOwnContexts(allContexts, session) - .ConfigureAwait(false); - eventsToBePublished.AddRange(events); + await messageSession.Publish(eventToBePublished); } - - foreach (var eventToBePublished in eventsToBePublished) + catch (Exception e) { - if (Logger.IsDebugEnabled) - { - Logger.Debug("Publishing external event on the bus."); - } + Logger.Error("Failed dispatching external integration event.", e); + var m = new ExternalIntegrationEventFailedToBePublished + { + EventType = eventToBePublished.GetType() + }; try { - await messageSession.Publish(eventToBePublished) - .ConfigureAwait(false); + m.Reason = e.GetBaseException().Message; } - catch (Exception e) + catch (Exception) { - Logger.Error("Failed dispatching external integration event.", e); - - var m = new ExternalIntegrationEventFailedToBePublished - { - EventType = eventToBePublished.GetType() - }; - try - { - m.Reason = e.GetBaseException().Message; - } - catch (Exception) - { - m.Reason = "Failed to retrieve reason!"; - } - - await domainEvents.Raise(m) - .ConfigureAwait(false); + m.Reason = "Failed to retrieve reason!"; } - } - foreach (var dispatchedEvent in awaitingDispatching) - { - session.Delete(dispatchedEvent); + await domainEvents.Raise(m); } - - await session.SaveChangesAsync() - .ConfigureAwait(false); } - - return true; } - public async Task StopAsync(CancellationToken cancellationToken) + public Task StopAsync(CancellationToken cancellationToken) { - subscription.Dispose(); - tokenSource.Cancel(); - - if (task != null) - { - await task.ConfigureAwait(false); - } - - tokenSource.Dispose(); - circuitBreaker.Dispose(); + return store.StopAsync(cancellationToken); } IMessageSession messageSession; - CriticalError criticalError; IEnumerable eventPublishers; - Settings settings; - ManualResetEventSlim signal = new ManualResetEventSlim(); - IDocumentStore store; + IExternalIntegrationRequestsDataStore store; IDomainEvents domainEvents; - RepeatedFailuresOverTimeCircuitBreaker circuitBreaker; - IDisposable subscription; - Task task; - CancellationTokenSource tokenSource; - Etag latestEtag = Etag.Empty; + static ILog Logger = LogManager.GetLogger(typeof(EventDispatcherHostedService)); } } \ No newline at end of file diff --git a/src/ServiceControl/ExternalIntegrations/EventPublisher.cs b/src/ServiceControl/ExternalIntegrations/EventPublisher.cs index 877fb21a20..61198b7ee1 100644 --- a/src/ServiceControl/ExternalIntegrations/EventPublisher.cs +++ b/src/ServiceControl/ExternalIntegrations/EventPublisher.cs @@ -4,7 +4,6 @@ namespace ServiceControl.ExternalIntegrations using System.Linq; using System.Threading.Tasks; using Infrastructure.DomainEvents; - using Raven.Client; abstract class EventPublisher : IEventPublisher where TEvent : IDomainEvent @@ -19,13 +18,13 @@ public object CreateDispatchContext(IDomainEvent @event) return CreateDispatchRequest((TEvent)@event); } - public Task> PublishEventsForOwnContexts(IEnumerable allContexts, IAsyncDocumentSession session) + public Task> PublishEventsForOwnContexts(IEnumerable allContexts) { - return PublishEvents(allContexts.OfType(), session); + return PublishEvents(allContexts.OfType()); } protected abstract TDispatchContext CreateDispatchRequest(TEvent @event); - protected abstract Task> PublishEvents(IEnumerable contexts, IAsyncDocumentSession session); + protected abstract Task> PublishEvents(IEnumerable contexts); } } \ No newline at end of file diff --git a/src/ServiceControl/ExternalIntegrations/ExternalIntegrationDispatchRequest.cs b/src/ServiceControl/ExternalIntegrations/ExternalIntegrationDispatchRequest.cs deleted file mode 100644 index 2b64bcff52..0000000000 --- a/src/ServiceControl/ExternalIntegrations/ExternalIntegrationDispatchRequest.cs +++ /dev/null @@ -1,8 +0,0 @@ -namespace ServiceControl.ExternalIntegrations -{ - class ExternalIntegrationDispatchRequest - { - public string Id { get; set; } - public object DispatchContext; - } -} \ No newline at end of file diff --git a/src/ServiceControl/ExternalIntegrations/IEventPublisher.cs b/src/ServiceControl/ExternalIntegrations/IEventPublisher.cs index 2dac44bed7..cd189af159 100644 --- a/src/ServiceControl/ExternalIntegrations/IEventPublisher.cs +++ b/src/ServiceControl/ExternalIntegrations/IEventPublisher.cs @@ -3,13 +3,12 @@ namespace ServiceControl.ExternalIntegrations using System.Collections.Generic; using System.Threading.Tasks; using Infrastructure.DomainEvents; - using Raven.Client; interface IEventPublisher { bool Handles(IDomainEvent @event); object CreateDispatchContext(IDomainEvent @event); - Task> PublishEventsForOwnContexts(IEnumerable allContexts, IAsyncDocumentSession session); + Task> PublishEventsForOwnContexts(IEnumerable allContexts); } } \ No newline at end of file diff --git a/src/ServiceControl/ExternalIntegrations/IntegrationEventWriter.cs b/src/ServiceControl/ExternalIntegrations/IntegrationEventWriter.cs index afee37cf82..768c0442e9 100644 --- a/src/ServiceControl/ExternalIntegrations/IntegrationEventWriter.cs +++ b/src/ServiceControl/ExternalIntegrations/IntegrationEventWriter.cs @@ -1,16 +1,15 @@ namespace ServiceControl.ExternalIntegrations { - using System; using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; using Infrastructure.DomainEvents; using NServiceBus.Logging; - using Raven.Client; + using ServiceControl.Persistence; class IntegrationEventWriter : IDomainHandler { - public IntegrationEventWriter(IDocumentStore store, IEnumerable eventPublishers) + public IntegrationEventWriter(IExternalIntegrationRequestsDataStore store, IEnumerable eventPublishers) { this.store = store; this.eventPublishers = eventPublishers; @@ -28,31 +27,21 @@ public async Task Handle(IDomainEvent message) return; } - using (var session = store.OpenAsyncSession()) + if (Logger.IsDebugEnabled) { - foreach (var dispatchContext in dispatchContexts) - { - if (Logger.IsDebugEnabled) - { - Logger.Debug("Storing dispatch request."); - } - - var dispatchRequest = new ExternalIntegrationDispatchRequest - { - Id = $"ExternalIntegrationDispatchRequests/{Guid.NewGuid()}", - DispatchContext = dispatchContext - }; - - await session.StoreAsync(dispatchRequest) - .ConfigureAwait(false); - } - - await session.SaveChangesAsync() - .ConfigureAwait(false); + Logger.Debug("Storing dispatch requests"); } + + var dispatchRequests = dispatchContexts.Select(dispatchContext => new ExternalIntegrationDispatchRequest + { + DispatchContext = dispatchContext + }).ToList(); + + + await store.StoreDispatchRequest(dispatchRequests); } - readonly IDocumentStore store; + readonly IExternalIntegrationRequestsDataStore store; readonly IEnumerable eventPublishers; static readonly ILog Logger = LogManager.GetLogger(typeof(IntegrationEventWriter)); diff --git a/src/ServiceControl/Hosting/Commands/CommandRunner.cs b/src/ServiceControl/Hosting/Commands/CommandRunner.cs index 25ef137cc1..7b46fbeaa9 100644 --- a/src/ServiceControl/Hosting/Commands/CommandRunner.cs +++ b/src/ServiceControl/Hosting/Commands/CommandRunner.cs @@ -18,7 +18,7 @@ public async Task Execute(HostArguments args, Settings settings) foreach (var commandType in commands) { var command = (AbstractCommand)Activator.CreateInstance(commandType); - await command.Execute(args, settings).ConfigureAwait(false); + await command.Execute(args, settings); } } diff --git a/src/ServiceControl/Hosting/Commands/ImportFailedErrorsCommand.cs b/src/ServiceControl/Hosting/Commands/ImportFailedErrorsCommand.cs index be9b8a3469..53ba63a40c 100644 --- a/src/ServiceControl/Hosting/Commands/ImportFailedErrorsCommand.cs +++ b/src/ServiceControl/Hosting/Commands/ImportFailedErrorsCommand.cs @@ -28,7 +28,7 @@ public override async Task Execute(HostArguments args, Settings settings) var loggingSettings = new LoggingSettings(settings.ServiceName, LogLevel.Info, LogLevel.Info); var bootstrapper = new Bootstrapper(settings, busConfiguration, loggingSettings); var host = bootstrapper.HostBuilder.Build(); - await host.StartAsync(CancellationToken.None).ConfigureAwait(false); + await host.StartAsync(CancellationToken.None); var importFailedErrors = host.Services.GetRequiredService(); @@ -36,7 +36,7 @@ public override async Task Execute(HostArguments args, Settings settings) try { - await importFailedErrors.Run(tokenSource.Token).ConfigureAwait(false); + await importFailedErrors.Run(tokenSource.Token); } catch (OperationCanceledException) { @@ -44,7 +44,7 @@ public override async Task Execute(HostArguments args, Settings settings) } finally { - await host.StopAsync(CancellationToken.None).ConfigureAwait(false); + await host.StopAsync(CancellationToken.None); } } } diff --git a/src/ServiceControl/Hosting/Commands/SetupCommand.cs b/src/ServiceControl/Hosting/Commands/SetupCommand.cs index c46857cc43..c0b3c484a3 100644 --- a/src/ServiceControl/Hosting/Commands/SetupCommand.cs +++ b/src/ServiceControl/Hosting/Commands/SetupCommand.cs @@ -9,8 +9,7 @@ class SetupCommand : AbstractCommand public override async Task Execute(HostArguments args, Settings settings) { settings.SkipQueueCreation = args.SkipQueueCreation; - await new SetupBootstrapper(settings).Run(args.Username) - .ConfigureAwait(false); + await new SetupBootstrapper(settings).Run(args.Username); } } } \ No newline at end of file diff --git a/src/ServiceControl/IComponentInstallationContext.cs b/src/ServiceControl/IComponentInstallationContext.cs index 0dc33001c2..28e3c8bc48 100644 --- a/src/ServiceControl/IComponentInstallationContext.cs +++ b/src/ServiceControl/IComponentInstallationContext.cs @@ -1,13 +1,11 @@ namespace Particular.ServiceControl { using System; - using System.Reflection; using System.Threading.Tasks; interface IComponentInstallationContext { void CreateQueue(string queueName); - void AddIndexAssembly(Assembly assembly); void RegisterInstallationTask(Func setupTask); } } \ No newline at end of file diff --git a/src/ServiceControl/Infrastructure/CriticalErrorCustomCheck.cs b/src/ServiceControl/Infrastructure/CriticalErrorCustomCheck.cs index 8c67effb2c..11c80f341e 100644 --- a/src/ServiceControl/Infrastructure/CriticalErrorCustomCheck.cs +++ b/src/ServiceControl/Infrastructure/CriticalErrorCustomCheck.cs @@ -14,6 +14,11 @@ public CriticalErrorCustomCheck() { } + internal CriticalErrorCustomCheck(TimeSpan interval) + : base("ServiceControl Primary Instance", "Health", interval) + { + } + public override Task PerformCheck() { var lastFailure = recentFailure; diff --git a/src/ServiceControl/Infrastructure/Extensions/CollectionExtensions.cs b/src/ServiceControl/Infrastructure/Extensions/CollectionExtensions.cs new file mode 100644 index 0000000000..b3cc7348ab --- /dev/null +++ b/src/ServiceControl/Infrastructure/Extensions/CollectionExtensions.cs @@ -0,0 +1,15 @@ +namespace ServiceControl.Infrastructure +{ + using System.Collections.Generic; + + static class CollectionExtensions + { + public static void AddRange(this IList list, params T[] items) + { + foreach (var item in items) + { + list.Add(item); + } + } + } +} \ No newline at end of file diff --git a/src/ServiceControl/Infrastructure/Extensions/DictionaryExtensions.cs b/src/ServiceControl/Infrastructure/Extensions/DictionaryExtensions.cs new file mode 100644 index 0000000000..7aabb6ea83 --- /dev/null +++ b/src/ServiceControl/Infrastructure/Extensions/DictionaryExtensions.cs @@ -0,0 +1,16 @@ +namespace ServiceControl.Infrastructure +{ + using System; + using System.Collections.Generic; + + static class DictionaryExtensions + { + public static void CheckIfKeyExists(string key, IReadOnlyDictionary headers, Action actionToInvokeWhenKeyIsFound) + { + if (headers.TryGetValue(key, out var value)) + { + actionToInvokeWhenKeyIsFound(value); + } + } + } +} \ No newline at end of file diff --git a/src/ServiceControl/Infrastructure/Extensions/HttpRequestMessageExtensions.cs b/src/ServiceControl/Infrastructure/Extensions/HttpRequestMessageExtensions.cs new file mode 100644 index 0000000000..fc4822f1da --- /dev/null +++ b/src/ServiceControl/Infrastructure/Extensions/HttpRequestMessageExtensions.cs @@ -0,0 +1,32 @@ +namespace ServiceControl.Persistence.Infrastructure +{ + using System.Net.Http; + + public static class HttpRequestMessageExtensions + { + public static string GetStatus(this HttpRequestMessage request) + { + return request.GetQueryStringValue("status"); + } + + public static string GetModified(this HttpRequestMessage request) + { + return request.GetQueryStringValue("modified"); + } + + public static string GetEndpointName(this HttpRequestMessage request) + { + return request.GetQueryStringValue("endpointName"); + } + + public static string GetQueueAddress(this HttpRequestMessage request) + { + return request.GetQueryStringValue("queueAddress"); + } + + public static string GetClassifierFilter(this HttpRequestMessage request) + { + return request.GetQueryStringValue("classifierFilter"); + } + } +} \ No newline at end of file diff --git a/src/ServiceControl/Infrastructure/Extensions/IncludeSystemMessageExtension.cs b/src/ServiceControl/Infrastructure/Extensions/IncludeSystemMessageExtension.cs new file mode 100644 index 0000000000..a60689bc84 --- /dev/null +++ b/src/ServiceControl/Infrastructure/Extensions/IncludeSystemMessageExtension.cs @@ -0,0 +1,14 @@ +namespace ServiceControl.Persistence.Infrastructure +{ + using System.Net.Http; + + public static class IncludeSystemMessageExtension + { + public static bool GetIncludeSystemMessages(this HttpRequestMessage request) + { + var includeSystemMessages = request.GetQueryStringValue("include_system_messages", false); + + return includeSystemMessages; + } + } +} \ No newline at end of file diff --git a/src/ServiceControl/Infrastructure/Extensions/PagingInfoExtension.cs b/src/ServiceControl/Infrastructure/Extensions/PagingInfoExtension.cs new file mode 100644 index 0000000000..aed23c0cfb --- /dev/null +++ b/src/ServiceControl/Infrastructure/Extensions/PagingInfoExtension.cs @@ -0,0 +1,24 @@ +namespace ServiceControl.Persistence.Infrastructure +{ + using System.Net.Http; + + public static class PagingInfoExtension + { + public static PagingInfo GetPagingInfo(this HttpRequestMessage request) + { + var maxResultsPerPage = request.GetQueryStringValue("per_page", PagingInfo.DefaultPageSize); + if (maxResultsPerPage < 1) + { + maxResultsPerPage = PagingInfo.DefaultPageSize; + } + + var page = request.GetQueryStringValue("page", 1); + if (page < 1) + { + page = 1; + } + + return new PagingInfo(page, maxResultsPerPage); + } + } +} \ No newline at end of file diff --git a/src/ServiceControl.Persistence/Infrastructure/QueryStringExtension.cs b/src/ServiceControl/Infrastructure/Extensions/QueryStringExtension.cs similarity index 82% rename from src/ServiceControl.Persistence/Infrastructure/QueryStringExtension.cs rename to src/ServiceControl/Infrastructure/Extensions/QueryStringExtension.cs index d17589fef3..4a86226d55 100644 --- a/src/ServiceControl.Persistence/Infrastructure/QueryStringExtension.cs +++ b/src/ServiceControl/Infrastructure/Extensions/QueryStringExtension.cs @@ -8,6 +8,19 @@ static class QueryStringExtension { public static T GetQueryStringValue(this HttpRequestMessage request, string key, T defaultValue = default) + { + var queryStringDictionary = GetCachedQueryStringDictionary(request); + + queryStringDictionary.TryGetValue(key, out var value); + if (string.IsNullOrEmpty(value)) + { + return defaultValue; + } + + return (T)Convert.ChangeType(value, typeof(T)); + } + + static Dictionary GetCachedQueryStringDictionary(HttpRequestMessage request) { Dictionary queryStringDictionary; if (!request.Properties.TryGetValue("QueryStringAsDictionary", out var dictionaryAsObject)) @@ -20,13 +33,7 @@ public static T GetQueryStringValue(this HttpRequestMessage request, string k queryStringDictionary = (Dictionary)dictionaryAsObject; } - queryStringDictionary.TryGetValue(key, out var value); - if (string.IsNullOrEmpty(value)) - { - return defaultValue; - } - - return (T)Convert.ChangeType(value, typeof(T)); + return queryStringDictionary; } } } \ No newline at end of file diff --git a/src/ServiceControl/Infrastructure/Extensions/SortInfoExtension.cs b/src/ServiceControl/Infrastructure/Extensions/SortInfoExtension.cs new file mode 100644 index 0000000000..05eca7e1f0 --- /dev/null +++ b/src/ServiceControl/Infrastructure/Extensions/SortInfoExtension.cs @@ -0,0 +1,38 @@ +namespace ServiceControl.Persistence.Infrastructure +{ + using System.Collections.Generic; + using System.Net.Http; + + public static class SortInfoExtension + { + public static SortInfo GetSortInfo(this HttpRequestMessage request, string defaultSortDirection = "desc") + { + var direction = request.GetQueryStringValue("direction", defaultSortDirection); + if (direction != "asc" && direction != "desc") + { + direction = defaultSortDirection; + } + + var sort = request.GetQueryStringValue("sort", "time_sent"); + if (!AllowableSortOptions.Contains(sort)) + { + sort = "time_sent"; + } + + return new SortInfo(sort, direction); + } + + static HashSet AllowableSortOptions = new HashSet + { + "processed_at", + "id", + "message_type", + "time_sent", + "critical_time", + "delivery_time", + "processing_time", + "status", + "message_id" + }; + } +} \ No newline at end of file diff --git a/src/ServiceControl/Infrastructure/TypeExtensions.cs b/src/ServiceControl/Infrastructure/Extensions/TypeExtensions.cs similarity index 100% rename from src/ServiceControl/Infrastructure/TypeExtensions.cs rename to src/ServiceControl/Infrastructure/Extensions/TypeExtensions.cs diff --git a/src/ServiceControl/Infrastructure/Extensions/WaitHandleExtensions.cs b/src/ServiceControl/Infrastructure/Extensions/WaitHandleExtensions.cs index f2563a9772..5535209353 100644 --- a/src/ServiceControl/Infrastructure/Extensions/WaitHandleExtensions.cs +++ b/src/ServiceControl/Infrastructure/Extensions/WaitHandleExtensions.cs @@ -22,7 +22,7 @@ public static async Task WaitOneAsync(this WaitHandle handle, int millisec tokenRegistration = cancellationToken.Register( state => ((TaskCompletionSource)state).TrySetCanceled(), tcs); - return await tcs.Task.ConfigureAwait(false); + return await tcs.Task; } finally { diff --git a/src/ServiceControl/Infrastructure/Metrics/MetricsReporterHostedService.cs b/src/ServiceControl/Infrastructure/Metrics/MetricsReporterHostedService.cs index 59b4891f18..96e1822cf4 100644 --- a/src/ServiceControl/Infrastructure/Metrics/MetricsReporterHostedService.cs +++ b/src/ServiceControl/Infrastructure/Metrics/MetricsReporterHostedService.cs @@ -28,7 +28,7 @@ public async Task StopAsync(CancellationToken cancellationToken) { try { - await reporter.Stop().ConfigureAwait(false); + await reporter.Stop(); } catch (OperationCanceledException) { diff --git a/src/ServiceControl/Infrastructure/NServiceBusFactory.cs b/src/ServiceControl/Infrastructure/NServiceBusFactory.cs index 605bac8597..cb1b540b46 100644 --- a/src/ServiceControl/Infrastructure/NServiceBusFactory.cs +++ b/src/ServiceControl/Infrastructure/NServiceBusFactory.cs @@ -4,7 +4,7 @@ namespace ServiceBus.Management.Infrastructure using NServiceBus; using NServiceBus.Configuration.AdvancedExtensibility; using ServiceControl.ExternalIntegrations; - using ServiceControl.Infrastructure.RavenDB.Subscriptions; + using ServiceControl.Infrastructure.Subscriptions; using ServiceControl.Notifications.Email; using ServiceControl.Operations; using ServiceControl.Transports; @@ -40,7 +40,7 @@ public static void Configure(Settings.Settings settings, TransportCustomization recoverability.CustomPolicy(SendEmailNotificationHandler.RecoverabilityPolicy); - configuration.UsePersistence(); + configuration.UsePersistence(); configuration.UseSerialization(); configuration.LimitMessageProcessingConcurrencyTo(settings.MaximumConcurrencyLevel); diff --git a/src/ServiceControl/Infrastructure/OWIN/ApiLogger.cs b/src/ServiceControl/Infrastructure/OWIN/ApiLogger.cs index fdb1c59052..9fe05fc9a4 100644 --- a/src/ServiceControl/Infrastructure/OWIN/ApiLogger.cs +++ b/src/ServiceControl/Infrastructure/OWIN/ApiLogger.cs @@ -24,7 +24,7 @@ static async Task LogAllIncomingCalls(LogApiCalls middleware, IOwinContext conte { log.DebugFormat("Begin {0}: {1}", context.Request.Method, context.Request.Uri.ToString()); - await middleware.Next.Invoke(context).ConfigureAwait(false); + await middleware.Next.Invoke(context); log.DebugFormat("End {0} ({1}): {2}", context.Request.Method, context.Response.StatusCode, context.Request.Uri.ToString()); } diff --git a/src/ServiceControl/Infrastructure/RavenDB/EmbeddedRavenDbHostedService.cs b/src/ServiceControl/Infrastructure/RavenDB/EmbeddedRavenDbHostedService.cs deleted file mode 100644 index 80f7d843f0..0000000000 --- a/src/ServiceControl/Infrastructure/RavenDB/EmbeddedRavenDbHostedService.cs +++ /dev/null @@ -1,85 +0,0 @@ -namespace ServiceControl.Infrastructure.RavenDB -{ - using System; - using System.Collections.Generic; - using System.ComponentModel.Composition.Hosting; - using System.Linq; - using System.Reflection; - using System.Threading; - using System.Threading.Tasks; - using Microsoft.Extensions.Hosting; - using NServiceBus.Logging; - using Particular.ServiceControl; - using Raven.Client; - using Raven.Client.Indexes; - - class EmbeddedRavenDbHostedService : IHostedService - { - readonly IDocumentStore documentStore; - readonly IEnumerable dataMigrations; - readonly ComponentInstallationContext installationContext; - static readonly ILog Log = LogManager.GetLogger(typeof(EmbeddedRavenDbHostedService)); - public EmbeddedRavenDbHostedService(IDocumentStore documentStore, IEnumerable dataMigrations, ComponentInstallationContext installationContext) - { - this.documentStore = documentStore; - this.dataMigrations = dataMigrations; - this.installationContext = installationContext; - } - - public Task StartAsync(CancellationToken cancellationToken) => SetupDatabase(); - - public async Task SetupDatabase() - { - Log.Info("Initializing RavenDB instance"); - documentStore.Initialize(); - - Log.Info("Creating indexes if not present."); - var indexProvider = CreateIndexProvider(installationContext.IndexAssemblies); - await IndexCreation.CreateIndexesAsync(indexProvider, documentStore) - .ConfigureAwait(false); - - Log.Info("Testing indexes"); - await TestAllIndexesAndResetIfException(documentStore).ConfigureAwait(false); - - Log.Info("Executing data migrations"); - foreach (var migration in dataMigrations) - { - await migration.Migrate(documentStore) - .ConfigureAwait(false); - } - } - - public Task StopAsync(CancellationToken cancellationToken) - { - documentStore.Dispose(); - return Task.CompletedTask; - } - - ExportProvider CreateIndexProvider(List indexAssemblies) => - new CompositionContainer( - new AggregateCatalog( - from indexAssembly in indexAssemblies select new AssemblyCatalog(indexAssembly) - ) - ); - - static async Task TestAllIndexesAndResetIfException(IDocumentStore store) - { - foreach (var index in store.DatabaseCommands.GetStatistics().Indexes) - { - try - { - using (var session = store.OpenAsyncSession()) - { - await session.Advanced.AsyncDocumentQuery(index.Name).Take(1).ToListAsync().ConfigureAwait(false); - } - } - catch (Exception ex) - { - Log.Warn($"When trying to fetch 1 document from index {index.Name} the following exception was thrown: {ex}"); - Log.Warn($"Attempting to reset errored index: [{index.Name}] priority: {index.Priority} is valid: {index.IsInvalidIndex} indexing attempts: {index.IndexingAttempts}, failed indexing attempts:{index.IndexingErrors}"); - store.DatabaseCommands.ResetIndex(index.Name); - } - } - } - } -} \ No newline at end of file diff --git a/src/ServiceControl/Infrastructure/RavenDB/Expiration/ExpiredDocumentsCleanerBundle.cs b/src/ServiceControl/Infrastructure/RavenDB/Expiration/ExpiredDocumentsCleanerBundle.cs deleted file mode 100644 index 310cce09c1..0000000000 --- a/src/ServiceControl/Infrastructure/RavenDB/Expiration/ExpiredDocumentsCleanerBundle.cs +++ /dev/null @@ -1,72 +0,0 @@ -namespace ServiceControl.Infrastructure.RavenDB.Expiration -{ - using System; - using System.ComponentModel.Composition; - using System.Threading.Tasks; - using BackgroundTasks; - using NServiceBus.Logging; - using Raven.Database; - using Raven.Database.Plugins; - - [InheritedExport(typeof(IStartupTask))] - [ExportMetadata("Bundle", "customDocumentExpiration")] - public class ExpiredDocumentsCleanerBundle : IStartupTask, IDisposable - { - public void Dispose() - { - lock (this) - { - if (timer == null) - { - return; - } - - var stopTask = timer.Stop(); - var delayTask = Task.Delay(TimeSpan.FromSeconds(30)); - var composite = Task.WhenAny(stopTask, delayTask); - - var finishedTask = composite.GetAwaiter().GetResult(); - if (finishedTask == delayTask) - { - logger.Error("Cleanup process did not finish on time. Forcing shutdown."); - } - else - { - logger.Info("Expired documents cleanup process stopped."); - } - - timer = null; - } - } - - public void Execute(DocumentDatabase database) - { - var deleteFrequencyInSeconds = RavenBootstrapper.Settings.ExpirationProcessTimerInSeconds; - - if (deleteFrequencyInSeconds == 0) - { - return; - } - - var due = TimeSpan.FromSeconds(deleteFrequencyInSeconds); - var deletionBatchSize = RavenBootstrapper.Settings.ExpirationProcessBatchSize; - - logger.Info($"Running deletion of expired documents every {deleteFrequencyInSeconds} seconds"); - logger.Info($"Deletion batch size set to {deletionBatchSize}"); - logger.Info($"Retention period for errors is {RavenBootstrapper.Settings.ErrorRetentionPeriod}"); - - var auditRetention = RavenBootstrapper.Settings.AuditRetentionPeriod; - - if (auditRetention.HasValue) - { - logger.InfoFormat("Retention period for audits and saga history is {0}", RavenBootstrapper.Settings.AuditRetentionPeriod); - } - - timer = new TimerJob( - token => ExpiredDocumentsCleaner.RunCleanup(deletionBatchSize, database, RavenBootstrapper.Settings, token), due, due, e => { logger.Error("Error when trying to find expired documents", e); }); - } - - ILog logger = LogManager.GetLogger(typeof(ExpiredDocumentsCleanerBundle)); - TimerJob timer; - } -} \ No newline at end of file diff --git a/src/ServiceControl/Infrastructure/RavenDB/RavenBootstrapper.cs b/src/ServiceControl/Infrastructure/RavenDB/RavenBootstrapper.cs deleted file mode 100644 index dc0f19b673..0000000000 --- a/src/ServiceControl/Infrastructure/RavenDB/RavenBootstrapper.cs +++ /dev/null @@ -1,107 +0,0 @@ -namespace ServiceControl.Infrastructure.RavenDB -{ - using System; - using System.ComponentModel.Composition.Hosting; - using System.IO; - using System.Runtime.Serialization; - using NServiceBus.Logging; - using Particular.Licensing; - using Raven.Abstractions.Data; - using Raven.Client.Document; - using Raven.Client.Embedded; - using ServiceBus.Management.Infrastructure.Settings; - using Subscriptions; - - class RavenBootstrapper - { - public static Settings Settings { get; set; } - - public static string ReadLicense() - { - using (var resourceStream = typeof(RavenBootstrapper).Assembly.GetManifestResourceStream("ServiceControl.Infrastructure.RavenDB.RavenLicense.xml")) - using (var reader = new StreamReader(resourceStream)) - { - return reader.ReadToEnd(); - } - } - - public static void Configure(EmbeddableDocumentStore documentStore, Settings settings, bool maintenanceMode = false) - { - Settings = settings; - - Directory.CreateDirectory(settings.DbPath); - - documentStore.Listeners.RegisterListener(new SubscriptionsLegacyAddressConverter()); - - if (settings.RunInMemory) - { - documentStore.RunInMemory = true; - } - else - { - documentStore.DataDirectory = settings.DbPath; - documentStore.Configuration.CompiledIndexCacheDirectory = settings.DbPath; - } - - documentStore.UseEmbeddedHttpServer = maintenanceMode || settings.ExposeRavenDB; - documentStore.EnlistInDistributedTransactions = false; - - var localRavenLicense = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "RavenLicense.xml"); - if (File.Exists(localRavenLicense)) - { - Logger.InfoFormat("Loading RavenDB license found from {0}", localRavenLicense); - documentStore.Configuration.Settings["Raven/License"] = NonBlockingReader.ReadAllTextWithoutLocking(localRavenLicense); - } - else - { - Logger.InfoFormat("Loading Embedded RavenDB license"); - documentStore.Configuration.Settings["Raven/License"] = ReadLicense(); - } - - //This is affects only remote access to the database in maintenace mode and enables access without authentication - documentStore.Configuration.Settings["Raven/AnonymousAccess"] = "Admin"; - documentStore.Configuration.Settings["Raven/Licensing/AllowAdminAnonymousAccessForCommercialUse"] = "true"; - - if (Settings.RunCleanupBundle) - { - documentStore.Configuration.Settings.Add("Raven/ActiveBundles", "CustomDocumentExpiration"); - } - - documentStore.Configuration.DisableClusterDiscovery = true; - documentStore.Configuration.ResetIndexOnUncleanShutdown = true; - documentStore.Configuration.Port = settings.DatabaseMaintenancePort; - documentStore.Configuration.HostName = settings.Hostname == "*" || settings.Hostname == "+" - ? "localhost" - : settings.Hostname; - documentStore.Conventions.SaveEnumsAsIntegers = true; - documentStore.Conventions.CustomizeJsonSerializer = serializer => serializer.Binder = MigratedTypeAwareBinder; - - documentStore.Configuration.Catalog.Catalogs.Add(new AssemblyCatalog(typeof(RavenBootstrapper).Assembly)); - documentStore.Conventions.FindClrType = (id, doc, metadata) => - { - var clrtype = metadata.Value(Constants.RavenClrType); - - // The CLR type cannot be assumed to be always there - if (clrtype == null) - { - return null; - } - - if (clrtype.EndsWith(".Subscription, NServiceBus.Core")) - { - clrtype = ReflectionUtil.GetFullNameWithoutVersionInformation(typeof(Subscription)); - } - else if (clrtype.EndsWith(".Subscription, NServiceBus.RavenDB")) - { - clrtype = ReflectionUtil.GetFullNameWithoutVersionInformation(typeof(Subscription)); - } - - return clrtype; - }; - } - - static readonly ILog Logger = LogManager.GetLogger(typeof(RavenBootstrapper)); - - static SerializationBinder MigratedTypeAwareBinder = new MigratedTypeAwareBinder(); - } -} \ No newline at end of file diff --git a/src/ServiceControl/Infrastructure/RavenDB/Subscriptions/IPrimableSubscriptionStorage.cs b/src/ServiceControl/Infrastructure/RavenDB/Subscriptions/IPrimableSubscriptionStorage.cs deleted file mode 100644 index ccafc56db3..0000000000 --- a/src/ServiceControl/Infrastructure/RavenDB/Subscriptions/IPrimableSubscriptionStorage.cs +++ /dev/null @@ -1,9 +0,0 @@ -namespace ServiceControl.Infrastructure.RavenDB.Subscriptions -{ - using System.Threading.Tasks; - - interface IPrimableSubscriptionStorage - { - Task Prime(); - } -} \ No newline at end of file diff --git a/src/ServiceControl/Infrastructure/Settings/DataStoreType.cs b/src/ServiceControl/Infrastructure/Settings/DataStoreType.cs index 2759cf44f8..4ec2d20dd7 100644 --- a/src/ServiceControl/Infrastructure/Settings/DataStoreType.cs +++ b/src/ServiceControl/Infrastructure/Settings/DataStoreType.cs @@ -3,14 +3,12 @@ public enum DataStoreType { InMemory = 1, - RavenDB35 = 2, - SqlDb = 3 + RavenDB35 = 2 } public static class DataStoreConfig { public static string InMemoryPersistenceTypeFullyQualifiedName = "ServiceControl.Persistence.InMemory.InMemoryPersistenceConfiguration, ServiceControl.Persistence.InMemory"; - public static string SqlServerPersistenceTypeFullyQualifiedName = "ServiceControl.Persistence.SqlServer.SqlDbPersistenceConfiguration, ServiceControl.Persistence.SqlServer"; public static string RavenDB35PersistenceTypeFullyQualifiedName = "ServiceControl.Persistence.RavenDb.RavenDbPersistenceConfiguration, ServiceControl.Persistence.RavenDb"; } } \ No newline at end of file diff --git a/src/ServiceControl/Infrastructure/Settings/Settings.cs b/src/ServiceControl/Infrastructure/Settings/Settings.cs index 6d8bf0c2af..ca48c5e0be 100644 --- a/src/ServiceControl/Infrastructure/Settings/Settings.cs +++ b/src/ServiceControl/Infrastructure/Settings/Settings.cs @@ -14,7 +14,7 @@ namespace ServiceBus.Management.Infrastructure.Settings public class Settings { - public Settings(string serviceName = null) + public Settings(string serviceName = null, string transportType = null, string persisterType = null) { ServiceName = serviceName; @@ -32,13 +32,14 @@ public Settings(string serviceName = null) TryLoadLicenseFromConfig(); TransportConnectionString = GetConnectionString(); - TransportType = SettingsReader.Read("TransportType"); + TransportType = transportType ?? SettingsReader.Read("TransportType"); + PersistenceType = persisterType ?? SettingsReader.Read("PersistenceType"); AuditRetentionPeriod = GetAuditRetentionPeriod(); ForwardErrorMessages = GetForwardErrorMessages(); ErrorRetentionPeriod = GetErrorRetentionPeriod(); EventsRetentionPeriod = GetEventRetentionPeriod(); Port = SettingsReader.Read("Port", 33333); - DatabaseMaintenancePort = SettingsReader.Read("DatabaseMaintenancePort", 33334); + DatabaseMaintenancePort = SettingsReader.Read("DatabaseMaintenancePort", 33334); // TODO: Should not be in Core but in the persister implementation ProcessRetryBatchesFrequency = TimeSpan.FromSeconds(30); MaximumConcurrencyLevel = SettingsReader.Read("MaximumConcurrencyLevel", 10); RetryHistoryDepth = SettingsReader.Read("RetryHistoryDepth", 10); @@ -47,12 +48,10 @@ public Settings(string serviceName = null) NotificationsFilter = SettingsReader.Read("NotificationsFilter"); RemoteInstances = GetRemoteInstances().ToArray(); DataSpaceRemainingThreshold = GetDataSpaceRemainingThreshold(); - MinimumStorageLeftRequiredForIngestion = GetMinimumStorageLeftRequiredForIngestion(); DbPath = GetDbPath(); TimeToRestartErrorIngestionAfterFailure = GetTimeToRestartErrorIngestionAfterFailure(); DisableExternalIntegrationsPublishing = SettingsReader.Read("DisableExternalIntegrationsPublishing", false); EnableFullTextSearchOnBodies = SettingsReader.Read("EnableFullTextSearchOnBodies", true); - DataStoreType = GetDataStoreType(); } public string NotificationsFilter { get; set; } @@ -62,9 +61,6 @@ public Settings(string serviceName = null) //HINT: acceptance tests only public Func MessageFilter { get; set; } - //HINT: acceptance tests only - public bool RunInMemory { get; set; } - //HINT: acceptance tests only public string EmailDropFolder { get; set; } @@ -102,11 +98,13 @@ public string RootUrl public string StagingQueue => $"{ServiceName}.staging"; public int Port { get; set; } - public int DatabaseMaintenancePort { get; set; } + public int DatabaseMaintenancePort { get; set; } // TODO: Should not be in Core but in the persister implementation public string LicenseFileText { get; set; } - public bool ExposeRavenDB => SettingsReader.Read("ExposeRavenDB"); + public Dictionary PersisterSpecificSettings { get; set; } = new Dictionary(); + + public bool ExposeRavenDB => SettingsReader.Read("ExposeRavenDB"); // TODO: Should not be in Core but in the persister implementation public bool PrintMetrics => SettingsReader.Read("PrintMetrics"); public string Hostname => SettingsReader.Read("Hostname", "localhost"); public string VirtualDirectory => SettingsReader.Read("VirtualDirectory", string.Empty); @@ -128,7 +126,8 @@ public TimeSpan HeartbeatGracePeriod } public string TransportType { get; set; } - public string DbPath { get; set; } + public string PersistenceType { get; private set; } + public string DbPath { get; set; } // TODO: Should not be in Core but in the persister implementation public string ErrorLogQueue { get; set; } public string ErrorQueue { get; set; } @@ -137,52 +136,11 @@ public TimeSpan HeartbeatGracePeriod public bool IngestErrorMessages { get; set; } = true; public bool RunRetryProcessor { get; set; } = true; - public int ExpirationProcessTimerInSeconds - { - get - { - if (expirationProcessTimerInSeconds < 0) - { - logger.Error($"ExpirationProcessTimerInSeconds cannot be negative. Defaulting to {ExpirationProcessTimerInSecondsDefault}"); - return ExpirationProcessTimerInSecondsDefault; - } - - if (ValidateConfiguration && expirationProcessTimerInSeconds > TimeSpan.FromHours(3).TotalSeconds) - { - logger.Error($"ExpirationProcessTimerInSeconds cannot be larger than {TimeSpan.FromHours(3).TotalSeconds}. Defaulting to {ExpirationProcessTimerInSecondsDefault}"); - return ExpirationProcessTimerInSecondsDefault; - } - - return expirationProcessTimerInSeconds; - } - } - - public TimeSpan? AuditRetentionPeriod { get; } + public TimeSpan? AuditRetentionPeriod { get; set; } public TimeSpan ErrorRetentionPeriod { get; } public TimeSpan EventsRetentionPeriod { get; } - - public int ExpirationProcessBatchSize - { - get - { - if (expirationProcessBatchSize < 1) - { - logger.Error($"ExpirationProcessBatchSize cannot be less than 1. Defaulting to {ExpirationProcessBatchSizeDefault}"); - return ExpirationProcessBatchSizeDefault; - } - - if (ValidateConfiguration && expirationProcessBatchSize < ExpirationProcessBatchSizeMinimum) - { - logger.Error($"ExpirationProcessBatchSize cannot be less than {ExpirationProcessBatchSizeMinimum}. Defaulting to {ExpirationProcessBatchSizeDefault}"); - return ExpirationProcessBatchSizeDefault; - } - - return expirationProcessBatchSize; - } - } - public string ServiceName { get; } public int HttpDefaultConnectionLimit { get; set; } @@ -196,7 +154,6 @@ public int ExpirationProcessBatchSize public RemoteInstanceSetting[] RemoteInstances { get; set; } public int DataSpaceRemainingThreshold { get; set; } - public int MinimumStorageLeftRequiredForIngestion { get; set; } public bool EnableFullTextSearchOnBodies { get; set; } @@ -204,8 +161,6 @@ public int ExpirationProcessBatchSize public bool ExposeApi { get; set; } = true; - public DataStoreType DataStoreType { get; set; } = DataStoreType.RavenDB35; - public TransportCustomization LoadTransportCustomization() { try @@ -499,49 +454,15 @@ int GetDataSpaceRemainingThreshold() return threshold; } - int GetMinimumStorageLeftRequiredForIngestion() - { - string message; - var threshold = SettingsReader.Read("MinimumStorageLeftRequiredForIngestion", MinimumStorageLeftRequiredForIngestionDefault); - if (threshold < 0) - { - message = $"{nameof(MinimumStorageLeftRequiredForIngestion)} is invalid, minimum value is 0."; - logger.Fatal(message); - throw new Exception(message); - } - - if (threshold > 100) - { - message = $"{nameof(MinimumStorageLeftRequiredForIngestion)} is invalid, maximum value is 100."; - logger.Fatal(message); - throw new Exception(message); - } - - return threshold; - } - - DataStoreType GetDataStoreType() - { - var value = SettingsReader.Read("DataStoreType", "RavenDB35"); - - return (DataStoreType)Enum.Parse(typeof(DataStoreType), value); - } - void TryLoadLicenseFromConfig() { LicenseFileText = SettingsReader.Read("LicenseText"); } ILog logger = LogManager.GetLogger(typeof(Settings)); - int expirationProcessBatchSize = SettingsReader.Read("ExpirationProcessBatchSize", ExpirationProcessBatchSizeDefault); - int expirationProcessTimerInSeconds = SettingsReader.Read("ExpirationProcessTimerInSeconds", ExpirationProcessTimerInSecondsDefault); public const string DEFAULT_SERVICE_NAME = "Particular.ServiceControl"; public const string Disabled = "!disable"; - const int ExpirationProcessTimerInSecondsDefault = 600; - const int ExpirationProcessBatchSizeDefault = 65512; - const int ExpirationProcessBatchSizeMinimum = 10240; const int DataSpaceRemainingThresholdDefault = 20; - const int MinimumStorageLeftRequiredForIngestionDefault = 5; } } \ No newline at end of file diff --git a/src/ServiceControl/Infrastructure/SignalR/MessageStreamerConnection.cs b/src/ServiceControl/Infrastructure/SignalR/MessageStreamerConnection.cs index 59ba3f08f7..9c320b2d6f 100644 --- a/src/ServiceControl/Infrastructure/SignalR/MessageStreamerConnection.cs +++ b/src/ServiceControl/Infrastructure/SignalR/MessageStreamerConnection.cs @@ -44,7 +44,7 @@ protected override async Task OnReceived(IRequest request, string connectionId, var transportOperation = new TransportOperation(message, new UnicastAddressTag(localAddress)); var transportOperations = new TransportOperations(transportOperation); - await sender.Dispatch(transportOperations, transportTransaction, contextBag).ConfigureAwait(false); + await sender.Dispatch(transportOperations, transportTransaction, contextBag); } catch (Exception ex) { diff --git a/src/ServiceControl/Infrastructure/SignalR/ServicePulseNotifier.cs b/src/ServiceControl/Infrastructure/SignalR/ServicePulseNotifier.cs index c2cc28bd15..f05b23e58f 100644 --- a/src/ServiceControl/Infrastructure/SignalR/ServicePulseNotifier.cs +++ b/src/ServiceControl/Infrastructure/SignalR/ServicePulseNotifier.cs @@ -17,8 +17,7 @@ public async Task Handle(IDomainEvent domainEvent) { if (domainEvent is IUserInterfaceEvent userInteraceEvent) { - await broadcaster.Broadcast(userInteraceEvent) - .ConfigureAwait(false); + await broadcaster.Broadcast(userInteraceEvent); } } diff --git a/src/ServiceControl/Infrastructure/RavenDB/Subscriptions/CachedRavenDBPersistence.cs b/src/ServiceControl/Infrastructure/Subscriptions/ServiceControlSubscriptionPersistence.cs similarity index 55% rename from src/ServiceControl/Infrastructure/RavenDB/Subscriptions/CachedRavenDBPersistence.cs rename to src/ServiceControl/Infrastructure/Subscriptions/ServiceControlSubscriptionPersistence.cs index 8780925b43..f7539ef7a0 100644 --- a/src/ServiceControl/Infrastructure/RavenDB/Subscriptions/CachedRavenDBPersistence.cs +++ b/src/ServiceControl/Infrastructure/Subscriptions/ServiceControlSubscriptionPersistence.cs @@ -1,12 +1,12 @@ -namespace ServiceControl.Infrastructure.RavenDB.Subscriptions +namespace ServiceControl.Infrastructure.Subscriptions { using NServiceBus; using NServiceBus.Features; using NServiceBus.Persistence; - class CachedRavenDBPersistence : PersistenceDefinition + class ServiceControlSubscriptionPersistence : PersistenceDefinition { - public CachedRavenDBPersistence() + public ServiceControlSubscriptionPersistence() { Supports(s => s.EnableFeatureByDefault()); } diff --git a/src/ServiceControl/Infrastructure/RavenDB/Subscriptions/SubscriptionStorage.cs b/src/ServiceControl/Infrastructure/Subscriptions/SubscriptionStorage.cs similarity index 53% rename from src/ServiceControl/Infrastructure/RavenDB/Subscriptions/SubscriptionStorage.cs rename to src/ServiceControl/Infrastructure/Subscriptions/SubscriptionStorage.cs index 6ebfe2c4c5..cd481e436d 100644 --- a/src/ServiceControl/Infrastructure/RavenDB/Subscriptions/SubscriptionStorage.cs +++ b/src/ServiceControl/Infrastructure/Subscriptions/SubscriptionStorage.cs @@ -1,42 +1,35 @@ -namespace ServiceControl.Infrastructure.RavenDB.Subscriptions +namespace ServiceControl.Infrastructure.Subscriptions { using System.Threading.Tasks; using NServiceBus; using NServiceBus.Features; - using NServiceBus.Settings; using NServiceBus.Transport; + using ServiceControl.Persistence; class SubscriptionStorage : Feature { SubscriptionStorage() { - Prerequisite(c => c.Settings.Get().OutboundRoutingPolicy.Publishes == OutboundRoutingType.Unicast || IsMigrationModeEnabled(c.Settings), "The transport supports native pub sub"); - } - - static bool IsMigrationModeEnabled(ReadOnlySettings settings) - { - return settings.TryGet("NServiceBus.Subscriptions.EnableMigrationMode", out bool enabled) && enabled; + Prerequisite(c => c.Settings.Get().OutboundRoutingPolicy.Publishes == OutboundRoutingType.Unicast, "The transport does not support native pub sub"); } protected override void Setup(FeatureConfigurationContext context) { - context.Container.ConfigureComponent(DependencyLifecycle.SingleInstance); - context.Container.ConfigureComponent(DependencyLifecycle.SingleInstance); context.RegisterStartupTask(b => b.Build()); } class PrimeSubscriptions : FeatureStartupTask { - public IPrimableSubscriptionStorage persister; + public IServiceControlSubscriptionStorage persister; - public PrimeSubscriptions(IPrimableSubscriptionStorage persister) + public PrimeSubscriptions(IServiceControlSubscriptionStorage persister) { this.persister = persister; } protected override Task OnStart(IMessageSession session) { - return persister?.Prime() ?? Task.FromResult(0); + return persister?.Initialize() ?? Task.FromResult(0); } protected override Task OnStop(IMessageSession session) diff --git a/src/ServiceControl/Infrastructure/WebApi/CachingHttpHandler.cs b/src/ServiceControl/Infrastructure/WebApi/CachingHttpHandler.cs index 3f829bdd74..cd40b34eeb 100644 --- a/src/ServiceControl/Infrastructure/WebApi/CachingHttpHandler.cs +++ b/src/ServiceControl/Infrastructure/WebApi/CachingHttpHandler.cs @@ -8,7 +8,7 @@ class CachingHttpHandler : DelegatingHandler { protected override async Task SendAsync(HttpRequestMessage request, CancellationToken cancellationToken = default) { - var response = await base.SendAsync(request, cancellationToken).ConfigureAwait(false); + var response = await base.SendAsync(request, cancellationToken); if (!response.Headers.Contains("Cache-Control")) { diff --git a/src/ServiceControl/Infrastructure/WebApi/CompressionEncodingHttpHandler.cs b/src/ServiceControl/Infrastructure/WebApi/CompressionEncodingHttpHandler.cs index 1320d650c7..ba8048998b 100644 --- a/src/ServiceControl/Infrastructure/WebApi/CompressionEncodingHttpHandler.cs +++ b/src/ServiceControl/Infrastructure/WebApi/CompressionEncodingHttpHandler.cs @@ -13,7 +13,7 @@ class CompressionEncodingHttpHandler : DelegatingHandler { protected override async Task SendAsync(HttpRequestMessage request, CancellationToken cancellationToken = default) { - var response = await base.SendAsync(request, cancellationToken).ConfigureAwait(false); + var response = await base.SendAsync(request, cancellationToken); if (response.StatusCode != HttpStatusCode.OK) { @@ -119,7 +119,7 @@ protected override async Task SerializeToStreamAsync(Stream stream, TransportCon try { - await originalContent.CopyToAsync(compressedStream).ConfigureAwait(false); + await originalContent.CopyToAsync(compressedStream); } finally { diff --git a/src/ServiceControl/Infrastructure/WebApi/Cors.cs b/src/ServiceControl/Infrastructure/WebApi/Cors.cs index eb3c6e5cd3..256b7576b1 100644 --- a/src/ServiceControl/Infrastructure/WebApi/Cors.cs +++ b/src/ServiceControl/Infrastructure/WebApi/Cors.cs @@ -3,7 +3,7 @@ using System.Threading.Tasks; using System.Web.Cors; using Microsoft.Owin.Cors; - using Raven.Abstractions.Extensions; + using ServiceControl.Infrastructure; class Cors { diff --git a/src/ServiceControl/Infrastructure/WebApi/Negotiator.cs b/src/ServiceControl/Infrastructure/WebApi/Negotiator.cs index 20858a9e8a..91af251fb6 100644 --- a/src/ServiceControl/Infrastructure/WebApi/Negotiator.cs +++ b/src/ServiceControl/Infrastructure/WebApi/Negotiator.cs @@ -8,8 +8,7 @@ namespace ServiceControl.Infrastructure.WebApi using System.Net.Http; using System.Net.Http.Headers; using System.Text; - using Raven.Abstractions.Data; - using Raven.Client; + using ServiceControl.Persistence.Infrastructure; using QueryResult = Persistence.Infrastructure.QueryResult; static class Negotiator @@ -146,11 +145,10 @@ static void AddLink(ICollection links, int page, string rel, string uriP links.Add($"<{uriPath + query}>; rel=\"{rel}\""); } - public static HttpResponseMessage WithEtag(this HttpResponseMessage response, RavenQueryStatistics stats) + public static HttpResponseMessage WithEtag(this HttpResponseMessage response, string etag) { - var etag = stats.IndexEtag; - - return response.WithEtag(etag); + response.Headers.ETag = new EntityTagHeaderValue($"\"{etag}\""); + return response; } public static HttpResponseMessage WithDeterministicEtag(this HttpResponseMessage response, string data) @@ -161,13 +159,22 @@ public static HttpResponseMessage WithDeterministicEtag(this HttpResponseMessage } var guid = DeterministicGuid.MakeId(data); - return response.WithEtag(Etag.Parse(guid.ToString())); + return response.WithEtag(guid.ToString()); } - public static HttpResponseMessage WithEtag(this HttpResponseMessage response, Etag etag) + public static HttpResponseMessage FromQueryStatsInfo(HttpRequestMessage request, QueryStatsInfo queryStatsInfo) { - response.Headers.ETag = new EntityTagHeaderValue($"\"{etag}\""); - return response; + var response = request.CreateResponse(HttpStatusCode.OK); + return WithTotalCount(response, queryStatsInfo.TotalCount) + .WithEtag(queryStatsInfo.ETag); + } + + public static HttpResponseMessage FromQueryResult(HttpRequestMessage request, QueryResult queryResult) + where T : class + { + return FromModel(request, queryResult.Results) + .WithPagingLinksAndTotalCount(queryResult.QueryStats.TotalCount, request) + .WithEtag(queryResult.QueryStats.ETag); } static HttpResponseMessage WithHeader(this HttpResponseMessage response, string header, string value) diff --git a/src/ServiceControl/Infrastructure/WebApi/NotModifiedStatusHttpHandler.cs b/src/ServiceControl/Infrastructure/WebApi/NotModifiedStatusHttpHandler.cs index 56a18abdfc..b4147437a6 100644 --- a/src/ServiceControl/Infrastructure/WebApi/NotModifiedStatusHttpHandler.cs +++ b/src/ServiceControl/Infrastructure/WebApi/NotModifiedStatusHttpHandler.cs @@ -11,7 +11,7 @@ class NotModifiedStatusHttpHandler : DelegatingHandler { protected override async Task SendAsync(HttpRequestMessage request, CancellationToken cancellationToken = default) { - var response = await base.SendAsync(request, cancellationToken).ConfigureAwait(false); + var response = await base.SendAsync(request, cancellationToken); if (!response.IsSuccessStatusCode) { diff --git a/src/ServiceControl/Infrastructure/WebApi/RootController.cs b/src/ServiceControl/Infrastructure/WebApi/RootController.cs index cf3f1edd84..0356938396 100644 --- a/src/ServiceControl/Infrastructure/WebApi/RootController.cs +++ b/src/ServiceControl/Infrastructure/WebApi/RootController.cs @@ -10,13 +10,15 @@ using Newtonsoft.Json; using Newtonsoft.Json.Linq; using Particular.ServiceControl.Licensing; + using Persistence; using ServiceBus.Management.Infrastructure.Settings; class RootController : ApiController { - public RootController(ActiveLicense license, LoggingSettings loggingSettings, Settings settings, Func httpClientFactory) + public RootController(ActiveLicense license, LoggingSettings loggingSettings, Settings settings, PersistenceSettings persistenceSettings, Func httpClientFactory) { this.settings = settings; + this.persistenceSettings = persistenceSettings; this.license = license; this.loggingSettings = loggingSettings; this.httpClientFactory = httpClientFactory; @@ -83,10 +85,9 @@ public OkNegotiatedContentResult Config() PerformanceTunning = new { settings.HttpDefaultConnectionLimit, - settings.ExternalIntegrationsDispatchingBatchSize, - settings.ExpirationProcessBatchSize, - settings.ExpirationProcessTimerInSeconds + settings.ExternalIntegrationsDispatchingBatchSize }, + PersistenceSettings = persistenceSettings, Transport = new { settings.TransportType, @@ -119,14 +120,14 @@ public async Task RemoteConfig() try { - var response = await httpClient.GetAsync(uri).ConfigureAwait(false); + var response = await httpClient.GetAsync(uri); if (response.Headers.TryGetValues("X-Particular-Version", out var values)) { version = values.FirstOrDefault() ?? "Missing"; } - using (var stream = await response.Content.ReadAsStreamAsync().ConfigureAwait(false)) + using (var stream = await response.Content.ReadAsStreamAsync()) using (var reader = new StreamReader(stream)) using (var jsonReader = new JsonTextReader(reader)) { @@ -148,7 +149,7 @@ public async Task RemoteConfig() }) .ToArray(); - var results = await Task.WhenAll(tasks).ConfigureAwait(false); + var results = await Task.WhenAll(tasks); return Negotiator.FromModel(Request, results); } @@ -156,6 +157,7 @@ public async Task RemoteConfig() readonly LoggingSettings loggingSettings; readonly ActiveLicense license; readonly Settings settings; + readonly PersistenceSettings persistenceSettings; readonly Func httpClientFactory; static readonly JsonSerializer jsonSerializer = JsonSerializer.Create(JsonNetSerializerSettings.CreateDefault()); diff --git a/src/ServiceControl/Infrastructure/WebApi/XParticularVersionHttpHandler.cs b/src/ServiceControl/Infrastructure/WebApi/XParticularVersionHttpHandler.cs index 8c5544df4e..cb929b050c 100644 --- a/src/ServiceControl/Infrastructure/WebApi/XParticularVersionHttpHandler.cs +++ b/src/ServiceControl/Infrastructure/WebApi/XParticularVersionHttpHandler.cs @@ -14,7 +14,7 @@ static XParticularVersionHttpHandler() protected override async Task SendAsync(HttpRequestMessage request, CancellationToken cancellationToken = default) { - var response = await base.SendAsync(request, cancellationToken).ConfigureAwait(false); + var response = await base.SendAsync(request, cancellationToken); response.Headers.Add("X-Particular-Version", FileVersion); diff --git a/src/ServiceControl/InternalsVisibleTo.cs b/src/ServiceControl/InternalsVisibleTo.cs index a6b395b064..a897334d2c 100644 --- a/src/ServiceControl/InternalsVisibleTo.cs +++ b/src/ServiceControl/InternalsVisibleTo.cs @@ -2,9 +2,10 @@ [assembly: InternalsVisibleTo("ServiceControl.UnitTests")] [assembly: InternalsVisibleTo("ServiceControl.AcceptanceTests")] +[assembly: InternalsVisibleTo("ServiceControl.AcceptanceTests.RavenDB")] [assembly: InternalsVisibleTo("ServiceControl.AcceptanceTesting")] [assembly: InternalsVisibleTo("ServiceControl.PersistenceTests")] +[assembly: InternalsVisibleTo("ServiceControl.Persistence.Tests.RavenDB")] [assembly: InternalsVisibleTo("ServiceControl.MultiInstance.AcceptanceTests")] [assembly: InternalsVisibleTo("ServiceControl.Persistence.InMemory")] [assembly: InternalsVisibleTo("ServiceControl.Persistence.RavenDb")] -[assembly: InternalsVisibleTo("ServiceControl.Persistence.SqlServer")] diff --git a/src/ServiceControl/MaintenanceBootstrapper.cs b/src/ServiceControl/MaintenanceBootstrapper.cs index c00726591a..805539ea9b 100644 --- a/src/ServiceControl/MaintenanceBootstrapper.cs +++ b/src/ServiceControl/MaintenanceBootstrapper.cs @@ -2,41 +2,35 @@ namespace Particular.ServiceControl { using System; using System.Threading.Tasks; - using global::ServiceControl.Infrastructure.RavenDB; + using global::ServiceControl.Persistence; using Hosting; - using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Hosting; - using Raven.Client.Embedded; using ServiceBus.Management.Infrastructure.Settings; static class MaintenanceBootstrapper { public static async Task Run(HostArguments args, Settings settings) { - var hostBuilder = new HostBuilder() - .ConfigureServices(services => - { - var documentStore = new EmbeddableDocumentStore(); - - RavenBootstrapper.Configure(documentStore, settings, true); + var persistenceConfiguration = PersistenceConfigurationFactory.LoadPersistenceConfiguration(settings.PersistenceType); + var persistenceSettings = persistenceConfiguration.BuildPersistenceSettings(settings, maintenanceMode: true); - services.AddHostedService(sp => new EmbeddedRavenDbHostedService(documentStore, new IDataMigration[0], new ComponentInstallationContext())); - }); + var hostBuilder = new HostBuilder() + .SetupPersistence(persistenceSettings, persistenceConfiguration); if (args.RunAsWindowsService) { hostBuilder.UseWindowsService(); - await hostBuilder.Build().RunAsync().ConfigureAwait(false); + await hostBuilder.Build().RunAsync(); } else { - await Console.Out.WriteLineAsync($"RavenDB is now accepting requests on {settings.DatabaseMaintenanceUrl}").ConfigureAwait(false); - await Console.Out.WriteLineAsync("RavenDB Maintenance Mode - Press CTRL+C to exit").ConfigureAwait(false); + await Console.Out.WriteLineAsync($"RavenDB is now accepting requests on {settings.DatabaseMaintenanceUrl}"); + await Console.Out.WriteLineAsync("RavenDB Maintenance Mode - Press CTRL+C to exit"); hostBuilder.UseConsoleLifetime(); - await hostBuilder.Build().RunAsync().ConfigureAwait(false); + await hostBuilder.Build().RunAsync(); } } } diff --git a/src/ServiceControl/MessageFailures/Api/ArchiveMessagesController.cs b/src/ServiceControl/MessageFailures/Api/ArchiveMessagesController.cs index b8711d234c..9619c776bc 100644 --- a/src/ServiceControl/MessageFailures/Api/ArchiveMessagesController.cs +++ b/src/ServiceControl/MessageFailures/Api/ArchiveMessagesController.cs @@ -1,4 +1,4 @@ -namespace ServiceControl.MessageFailures.Api +namespace ServiceControl.MessageFailures.Api { using System.Collections.Generic; using System.Linq; @@ -8,17 +8,16 @@ using System.Web.Http; using InternalMessages; using NServiceBus; - using Raven.Client; using Infrastructure.WebApi; - using Recoverability; using ServiceControl.Persistence; + using ServiceControl.Persistence.Infrastructure; class ArchiveMessagesController : ApiController { - public ArchiveMessagesController(IMessageSession session, IDocumentStore store) + public ArchiveMessagesController(IMessageSession session, IErrorMessageDataStore dataStore) { messageSession = session; - this.store = store; + this.dataStore = dataStore; } [Route("errors/archive")] @@ -35,7 +34,7 @@ public async Task ArchiveBatch(List messageIds) { var request = new ArchiveMessage { FailedMessageId = id }; - await messageSession.SendLocal(request).ConfigureAwait(false); + await messageSession.SendLocal(request); } return Request.CreateResponse(HttpStatusCode.Accepted); @@ -45,18 +44,10 @@ public async Task ArchiveBatch(List messageIds) [HttpGet] public async Task GetArchiveMessageGroups(string classifier = "Exception Type and Stack Trace") { - using (var session = store.OpenAsyncSession()) - { - var groups = session.Query().Where(v => v.Type == classifier); - - var results = await groups.OrderByDescending(x => x.Last) - .Take(200) // only show 200 groups - .ToListAsync() - .ConfigureAwait(false); + var results = await dataStore.GetFailureGroupsByClassifier(classifier); - return Negotiator.FromModel(Request, results) - .WithDeterministicEtag(EtagHelper.CalculateEtag(results)); - } + return Negotiator.FromModel(Request, results) + .WithDeterministicEtag(EtagHelper.CalculateEtag(results)); } [Route("errors/{messageid}/archive")] @@ -69,7 +60,7 @@ public async Task Archive(string messageId) return Request.CreateResponse(HttpStatusCode.BadRequest); } - await messageSession.SendLocal(m => { m.FailedMessageId = messageId; }).ConfigureAwait(false); + await messageSession.SendLocal(m => { m.FailedMessageId = messageId; }); return Request.CreateResponse(HttpStatusCode.Accepted); } @@ -78,24 +69,17 @@ public async Task Archive(string messageId) [HttpGet] public async Task GetGroup(string groupId) { - using (var session = store.OpenAsyncSession()) - { - var queryResult = await session.Advanced - .AsyncDocumentQuery() - .Statistics(out var stats) - .WhereEquals(group => group.Id, groupId) - .FilterByStatusWhere(Request) - .FilterByLastModifiedRange(Request) - .ToListAsync() - .ConfigureAwait(false); + string status = Request.GetStatus(); + string modified = Request.GetModified(); - return Negotiator - .FromModel(Request, queryResult.FirstOrDefault()) - .WithEtag(stats); - } + var result = await dataStore.GetFailureGroupView(groupId, status, modified); + + return Negotiator + .FromModel(Request, result.Results) + .WithEtag(result.QueryStats.ETag); } - readonly IDocumentStore store; + readonly IErrorMessageDataStore dataStore; readonly IMessageSession messageSession; } } \ No newline at end of file diff --git a/src/ServiceControl/MessageFailures/Api/EditFailedMessagesController.cs b/src/ServiceControl/MessageFailures/Api/EditFailedMessagesController.cs index e74c7b2075..3451c84ea8 100644 --- a/src/ServiceControl/MessageFailures/Api/EditFailedMessagesController.cs +++ b/src/ServiceControl/MessageFailures/Api/EditFailedMessagesController.cs @@ -10,16 +10,16 @@ using System.Web.Http.Results; using NServiceBus; using NServiceBus.Logging; - using Raven.Client; + using Persistence; using Recoverability; using ServiceBus.Management.Infrastructure.Settings; class EditFailedMessagesController : ApiController { - public EditFailedMessagesController(Settings settings, IDocumentStore documentStore, IMessageSession messageSession) + public EditFailedMessagesController(Settings settings, IErrorMessageDataStore store, IMessageSession messageSession) { this.messageSession = messageSession; - this.documentStore = documentStore; + this.store = store; this.settings = settings; } @@ -42,12 +42,7 @@ public async Task Edit(string failedMessageId, EditMessageMode return StatusCode(HttpStatusCode.BadRequest); } - FailedMessage failedMessage; - - using (var session = documentStore.OpenAsyncSession()) - { - failedMessage = await session.LoadAsync(FailedMessage.MakeDocumentId(failedMessageId)).ConfigureAwait(false); - } + var failedMessage = await store.ErrorBy(failedMessageId); if (failedMessage == null) { @@ -84,7 +79,7 @@ await messageSession.SendLocal(new EditAndSend FailedMessageId = failedMessageId, NewBody = base64String, NewHeaders = edit.MessageHeaders.ToDictionary(kvp => kvp.Key, kvp => kvp.Value) - }).ConfigureAwait(false); + }); return StatusCode(HttpStatusCode.Accepted); } @@ -138,7 +133,7 @@ EditConfigurationModel GetEditConfiguration() } Settings settings; - IDocumentStore documentStore; + IErrorMessageDataStore store; IMessageSession messageSession; static ILog logging = LogManager.GetLogger(typeof(EditFailedMessagesController)); diff --git a/src/ServiceControl/MessageFailures/Api/GetAllErrorsController.cs b/src/ServiceControl/MessageFailures/Api/GetAllErrorsController.cs index 7a6201a5e3..bac72b1743 100644 --- a/src/ServiceControl/MessageFailures/Api/GetAllErrorsController.cs +++ b/src/ServiceControl/MessageFailures/Api/GetAllErrorsController.cs @@ -1,128 +1,90 @@ namespace ServiceControl.MessageFailures.Api { - using System.Collections.Generic; - using System.Net; using System.Net.Http; using System.Threading.Tasks; using System.Web.Http; using Infrastructure.WebApi; - using Raven.Abstractions.Data; - using Raven.Client; + using Persistence.Infrastructure; using ServiceControl.Persistence; class GetAllErrorsController : ApiController { - public GetAllErrorsController(IDocumentStore documentStore) + public GetAllErrorsController(IErrorMessageDataStore dataStore) { - this.documentStore = documentStore; + this.dataStore = dataStore; } [Route("errors")] [HttpGet] public async Task ErrorsGet() { - using (var session = documentStore.OpenAsyncSession()) - { - var results = await session.Advanced - .AsyncDocumentQuery() - .Statistics(out var stats) - .FilterByStatusWhere(Request) - .FilterByLastModifiedRange(Request) - .FilterByQueueAddress(Request) - .Sort(Request) - .Paging(Request) - .SetResultTransformer(new FailedMessageViewTransformer().TransformerName) - .SelectFields() - .ToListAsync() - .ConfigureAwait(false); - - return Negotiator - .FromModel(Request, results) - .WithPagingLinksAndTotalCount(stats.TotalResults, Request) - .WithEtag(stats); - } + string status = Request.GetStatus(); + string modified = Request.GetModified(); + string queueAddress = Request.GetQueueAddress(); + + var sortInfo = Request.GetSortInfo(); + var pagingInfo = Request.GetPagingInfo(); + + var results = await dataStore.ErrorGet( + status: status, + modified: modified, + queueAddress: queueAddress, + pagingInfo, + sortInfo + ); + + return Negotiator.FromQueryResult(Request, results); } [Route("errors")] [HttpHead] public async Task ErrorsHead() { - using (var session = documentStore.OpenAsyncSession()) - { - var queryResult = await session.Advanced - .AsyncDocumentQuery() - .FilterByStatusWhere(Request) - .FilterByLastModifiedRange(Request) - .FilterByQueueAddress(Request) - .QueryResultAsync() - .ConfigureAwait(false); - - var response = Request.CreateResponse(HttpStatusCode.OK); - - return response - .WithTotalCount(queryResult.TotalResults) - .WithEtag(queryResult.IndexEtag); - } + string status = Request.GetStatus(); + string modified = Request.GetModified(); + string queueAddress = Request.GetQueueAddress(); + + var queryResult = await dataStore.ErrorsHead( + status: status, + modified: modified, + queueAddress: queueAddress + ); + + + return Negotiator.FromQueryStatsInfo(Request, queryResult); } [Route("endpoints/{endpointname}/errors")] [HttpGet] - public async Task ErrorsByEndpointName(string endpointName) + public async Task ErrorsByEndpointName() { - using (var session = documentStore.OpenAsyncSession()) - { - var results = await session.Advanced - .AsyncDocumentQuery() - .Statistics(out var stats) - .FilterByStatusWhere(Request) - .AndAlso() - .WhereEquals("ReceivingEndpointName", endpointName) - .FilterByLastModifiedRange(Request) - .Sort(Request) - .Paging(Request) - .SetResultTransformer(new FailedMessageViewTransformer().TransformerName) - .SelectFields() - .ToListAsync() - .ConfigureAwait(false); - - return Negotiator - .FromModel(Request, results) - .WithPagingLinksAndTotalCount(stats.TotalResults, Request) - .WithEtag(stats); - } + string status = Request.GetStatus(); + string modified = Request.GetModified(); + string endpointName = Request.GetEndpointName(); + + var sortInfo = Request.GetSortInfo(); + var pagingInfo = Request.GetPagingInfo(); + + var results = await dataStore.ErrorsByEndpointName( + status: status, + endpointName: endpointName, + modified: modified, + pagingInfo, + sortInfo + ); + + return Negotiator.FromQueryResult(Request, results); } [Route("errors/summary")] [HttpGet] public async Task ErrorsSummary() { - using (var session = documentStore.OpenAsyncSession()) - { - var facetResults = await session.Query() - .ToFacetsAsync(new List - { - new Facet - { - Name = "Name", - DisplayName = "Endpoints" - }, - new Facet - { - Name = "Host", - DisplayName = "Hosts" - }, - new Facet - { - Name = "MessageType", - DisplayName = "Message types" - } - }) - .ConfigureAwait(false); - - return Negotiator.FromModel(Request, facetResults.Results); - } + var results = await dataStore.ErrorsSummary(); + + return Negotiator.FromModel(Request, results); } - readonly IDocumentStore documentStore; + readonly IErrorMessageDataStore dataStore; } } \ No newline at end of file diff --git a/src/ServiceControl/MessageFailures/Api/GetErrorByIdController.cs b/src/ServiceControl/MessageFailures/Api/GetErrorByIdController.cs index 71b6c823c7..07c546dbfb 100644 --- a/src/ServiceControl/MessageFailures/Api/GetErrorByIdController.cs +++ b/src/ServiceControl/MessageFailures/Api/GetErrorByIdController.cs @@ -1,108 +1,49 @@ namespace ServiceControl.MessageFailures.Api { using System; - using System.Linq; using System.Net; using System.Net.Http; using System.Threading.Tasks; using System.Web.Http; using Infrastructure.WebApi; - using NServiceBus.Logging; - using Raven.Client; - using ServiceControl.Operations; + using Persistence; class GetErrorByIdController : ApiController { - public GetErrorByIdController(IDocumentStore documentStore) + public GetErrorByIdController(IErrorMessageDataStore dataStore) { - this.documentStore = documentStore; + this.dataStore = dataStore; } [Route("errors/{failedmessageid:guid}")] [HttpGet] public async Task ErrorBy(Guid failedMessageId) { - using (var session = documentStore.OpenAsyncSession()) - { - var message = await session.LoadAsync(failedMessageId).ConfigureAwait(false); - - if (message == null) - { - return Request.CreateResponse(HttpStatusCode.NotFound); - } + var result = await dataStore.ErrorBy(failedMessageId); - return Negotiator.FromModel(Request, message); + if (result == null) + { + return Request.CreateResponse(HttpStatusCode.NotFound); } + + return Negotiator.FromModel(Request, result); } [Route("errors/last/{failedmessageid:guid}")] [HttpGet] public async Task ErrorLastBy(Guid failedMessageId) { - using (var session = documentStore.OpenAsyncSession()) - { - var message = await session.LoadAsync(failedMessageId).ConfigureAwait(false); - - if (message == null) - { - return Request.CreateResponse(HttpStatusCode.NotFound); - } + var result = await dataStore.ErrorLastBy(failedMessageId); - var result = Map(message, session); - - return Negotiator.FromModel(Request, result); - } - } - - static FailedMessageView Map(FailedMessage message, IAsyncDocumentSession session) - { - var processingAttempt = message.ProcessingAttempts.Last(); - - var metadata = processingAttempt.MessageMetadata; - var failureDetails = processingAttempt.FailureDetails; - var wasEdited = message.ProcessingAttempts.Last().Headers.ContainsKey("ServiceControl.EditOf"); - - var failedMsgView = new FailedMessageView - { - Id = message.UniqueMessageId, - MessageType = metadata.GetAsStringOrNull("MessageType"), - IsSystemMessage = metadata.GetOrDefault("IsSystemMessage"), - TimeSent = metadata.GetAsNullableDatetime("TimeSent"), - MessageId = metadata.GetAsStringOrNull("MessageId"), - Exception = failureDetails.Exception, - QueueAddress = failureDetails.AddressOfFailingEndpoint, - NumberOfProcessingAttempts = message.ProcessingAttempts.Count, - Status = message.Status, - TimeOfFailure = failureDetails.TimeOfFailure, - LastModified = session.Advanced.GetMetadataFor(message)["Last-Modified"].Value(), - Edited = wasEdited, - EditOf = wasEdited ? message.ProcessingAttempts.Last().Headers["ServiceControl.EditOf"] : "" - }; - - try - { - failedMsgView.SendingEndpoint = metadata.GetOrDefault("SendingEndpoint"); - } - catch (Exception ex) + if (result == null) { - Logger.Warn($"Unable to parse SendingEndpoint from metadata for messageId {message.UniqueMessageId}", ex); - failedMsgView.SendingEndpoint = EndpointDetailsParser.SendingEndpoint(processingAttempt.Headers); + return Request.CreateResponse(HttpStatusCode.NotFound); } - try - { - failedMsgView.ReceivingEndpoint = metadata.GetOrDefault("ReceivingEndpoint"); - } - catch (Exception ex) - { - Logger.Warn($"Unable to parse ReceivingEndpoint from metadata for messageId {message.UniqueMessageId}", ex); - failedMsgView.ReceivingEndpoint = EndpointDetailsParser.ReceivingEndpoint(processingAttempt.Headers); - } - return failedMsgView; + return Negotiator.FromModel(Request, result); } - readonly IDocumentStore documentStore; - static readonly ILog Logger = LogManager.GetLogger(typeof(GetErrorByIdController)); + readonly IErrorMessageDataStore dataStore; } } \ No newline at end of file diff --git a/src/ServiceControl/MessageFailures/Api/PendingRetryMessagesController.cs b/src/ServiceControl/MessageFailures/Api/PendingRetryMessagesController.cs index 378c7767af..0bf9b51cf1 100644 --- a/src/ServiceControl/MessageFailures/Api/PendingRetryMessagesController.cs +++ b/src/ServiceControl/MessageFailures/Api/PendingRetryMessagesController.cs @@ -28,8 +28,7 @@ public async Task RetryBy(List ids) return StatusCode(HttpStatusCode.BadRequest); } - await messageSession.SendLocal(m => m.MessageUniqueIds = ids.ToArray()) - .ConfigureAwait(false); + await messageSession.SendLocal(m => m.MessageUniqueIds = ids.ToArray()); return StatusCode(HttpStatusCode.Accepted); } @@ -60,7 +59,7 @@ await messageSession.SendLocal(m => m.QueueAddress = request.queueaddress; m.PeriodFrom = from; m.PeriodTo = to; - }).ConfigureAwait(false); + }); return Request.CreateResponse(HttpStatusCode.Accepted); } diff --git a/src/ServiceControl/MessageFailures/Api/QueueAddressController.cs b/src/ServiceControl/MessageFailures/Api/QueueAddressController.cs index c64388559b..22d2b7af72 100644 --- a/src/ServiceControl/MessageFailures/Api/QueueAddressController.cs +++ b/src/ServiceControl/MessageFailures/Api/QueueAddressController.cs @@ -1,68 +1,45 @@ namespace ServiceControl.MessageFailures.Api { - using System.Linq; using System.Net; using System.Net.Http; using System.Threading.Tasks; using System.Web.Http; using Infrastructure.WebApi; - using Raven.Client; + using Persistence.Infrastructure; using ServiceControl.Persistence; class QueueAddressController : ApiController { - public QueueAddressController(IDocumentStore documentStore) + public QueueAddressController(IQueueAddressStore dataStore) { - this.documentStore = documentStore; + this.dataStore = dataStore; } [Route("errors/queues/addresses")] [HttpGet] public async Task GetAddresses() { - using (var session = documentStore.OpenAsyncSession()) - { - var addresses = await session - .Query() - .Statistics(out var stats) - .Paging(Request) - .ToListAsync() - .ConfigureAwait(false); + var pagingInfo = Request.GetPagingInfo(); + var result = await dataStore.GetAddresses(pagingInfo); - return Negotiator - .FromModel(Request, addresses) - .WithPagingLinksAndTotalCount(stats.TotalResults, Request) - .WithEtag(stats); - } + return Negotiator.FromQueryResult(Request, result); } [Route("errors/queues/addresses/search/{search}")] [HttpGet] public async Task GetAddressesBySearchTerm(string search = null) { - using (var session = documentStore.OpenAsyncSession()) + if (string.IsNullOrWhiteSpace(search)) { - if (string.IsNullOrWhiteSpace(search)) - { - return Request.CreateResponse(HttpStatusCode.BadRequest); - } + return Request.CreateResponse(HttpStatusCode.BadRequest); + } - var failedMessageQueues = - await session.Query() - .Statistics(out var stats) - .Where(q => q.PhysicalAddress.StartsWith(search)) - .OrderBy(q => q.PhysicalAddress) - .Paging(Request) - .ToListAsync() - .ConfigureAwait(false); + var pagingInfo = Request.GetPagingInfo(); + var result = await dataStore.GetAddressesBySearchTerm(search, pagingInfo); - return Negotiator - .FromModel(Request, failedMessageQueues) - .WithPagingLinksAndTotalCount(stats.TotalResults, Request) - .WithEtag(stats); - } + return Negotiator.FromQueryResult(Request, result); } - readonly IDocumentStore documentStore; + readonly IQueueAddressStore dataStore; } } \ No newline at end of file diff --git a/src/ServiceControl/MessageFailures/Api/ResolveMessagesController.cs b/src/ServiceControl/MessageFailures/Api/ResolveMessagesController.cs index 8d6b85ff3e..5a19e93ca5 100644 --- a/src/ServiceControl/MessageFailures/Api/ResolveMessagesController.cs +++ b/src/ServiceControl/MessageFailures/Api/ResolveMessagesController.cs @@ -31,8 +31,7 @@ public async Task ResolveBy(ResolveRequest request) foreach (var id in request.uniquemessageids) { - await messageSession.SendLocal(new MarkPendingRetryAsResolved { FailedMessageId = id }) - .ConfigureAwait(false); + await messageSession.SendLocal(new MarkPendingRetryAsResolved { FailedMessageId = id }); } return Request.CreateResponse(HttpStatusCode.Accepted); @@ -54,7 +53,7 @@ await messageSession.SendLocal(m => { m.PeriodFrom = from; m.PeriodTo = to; - }).ConfigureAwait(false); + }); return Request.CreateResponse(HttpStatusCode.Accepted); } @@ -85,7 +84,7 @@ await messageSession.SendLocal(m => m.QueueAddress = request.queueaddress; m.PeriodFrom = from; m.PeriodTo = to; - }).ConfigureAwait(false); + }); return Request.CreateResponse(HttpStatusCode.Accepted); } diff --git a/src/ServiceControl/MessageFailures/Api/RetryMessages.cs b/src/ServiceControl/MessageFailures/Api/RetryMessages.cs index 5611b97c2d..3182d1a4d1 100644 --- a/src/ServiceControl/MessageFailures/Api/RetryMessages.cs +++ b/src/ServiceControl/MessageFailures/Api/RetryMessages.cs @@ -27,7 +27,7 @@ public async Task RetryMessageBy(string failedMessageId) return Request.CreateResponse(HttpStatusCode.BadRequest); } - return await retryMessagesApi.Execute(this, failedMessageId).ConfigureAwait(false); + return await retryMessagesApi.Execute(this, failedMessageId); } [Route("errors/retry")] @@ -39,8 +39,7 @@ public async Task RetryAllBy(List messageIds) return Request.CreateResponse(HttpStatusCode.BadRequest); } - await messageSession.SendLocal(m => m.MessageUniqueIds = messageIds.ToArray()) - .ConfigureAwait(false); + await messageSession.SendLocal(m => m.MessageUniqueIds = messageIds.ToArray()); return Request.CreateResponse(HttpStatusCode.Accepted); } @@ -58,7 +57,7 @@ await messageSession.SendLocal(m => { m.QueueAddress = queueAddress; m.Status = FailedMessageStatus.Unresolved; - }).ConfigureAwait(false); + }); return Request.CreateResponse(HttpStatusCode.Accepted); } @@ -67,8 +66,7 @@ await messageSession.SendLocal(m => [HttpPost] public async Task RetryAll() { - await messageSession.SendLocal(new RequestRetryAll()) - .ConfigureAwait(false); + await messageSession.SendLocal(new RequestRetryAll()); return Request.CreateResponse(HttpStatusCode.Accepted); } @@ -77,8 +75,7 @@ await messageSession.SendLocal(new RequestRetryAll()) [HttpPost] public async Task RetryAllByEndpoint(string endpointName) { - await messageSession.SendLocal(new RequestRetryAll { Endpoint = endpointName }) - .ConfigureAwait(false); + await messageSession.SendLocal(new RequestRetryAll { Endpoint = endpointName }); return Request.CreateResponse(HttpStatusCode.Accepted); } diff --git a/src/ServiceControl/MessageFailures/Api/RetryMessagesApi.cs b/src/ServiceControl/MessageFailures/Api/RetryMessagesApi.cs index 0d7ccb81ea..7ac0011852 100644 --- a/src/ServiceControl/MessageFailures/Api/RetryMessagesApi.cs +++ b/src/ServiceControl/MessageFailures/Api/RetryMessagesApi.cs @@ -20,8 +20,7 @@ public RetryMessagesApi(IMessageSession messageSession, Settings settings, Func< protected override async Task LocalQuery(HttpRequestMessage request, string input, string instanceId) { - await messageSession.SendLocal(m => { m.FailedMessageId = input; }) - .ConfigureAwait(false); + await messageSession.SendLocal(m => { m.FailedMessageId = input; }); return request.CreateResponse(HttpStatusCode.Accepted); } diff --git a/src/ServiceControl/MessageFailures/Api/UnArchiveMessagesController.cs b/src/ServiceControl/MessageFailures/Api/UnArchiveMessagesController.cs index 87b0c26383..e525e03de6 100644 --- a/src/ServiceControl/MessageFailures/Api/UnArchiveMessagesController.cs +++ b/src/ServiceControl/MessageFailures/Api/UnArchiveMessagesController.cs @@ -29,7 +29,7 @@ public async Task Unarchive(List ids) var request = new UnArchiveMessages { FailedMessageIds = ids }; - await messageSession.SendLocal(request).ConfigureAwait(false); + await messageSession.SendLocal(request); return StatusCode(HttpStatusCode.Accepted); } @@ -55,7 +55,7 @@ await messageSession.SendLocal(new UnArchiveMessagesByRange From = fromDateTime, To = toDateTime, CutOff = DateTime.UtcNow - }).ConfigureAwait(false); + }); return StatusCode(HttpStatusCode.Accepted); } diff --git a/src/ServiceControl/MessageFailures/FailedMessageViewIndexNotifications.cs b/src/ServiceControl/MessageFailures/FailedMessageViewIndexNotifications.cs deleted file mode 100644 index 67879a3063..0000000000 --- a/src/ServiceControl/MessageFailures/FailedMessageViewIndexNotifications.cs +++ /dev/null @@ -1,74 +0,0 @@ -namespace ServiceControl.MessageFailures -{ - using System; - using System.Threading.Tasks; - using Api; - using Contracts.MessageFailures; - using Infrastructure.DomainEvents; - using NServiceBus.Logging; - using Raven.Abstractions.Data; - using Raven.Client; - - class FailedMessageViewIndexNotifications : IObserver - { - public FailedMessageViewIndexNotifications(IDocumentStore store, IDomainEvents domainEvents) - { - this.store = store; - this.domainEvents = domainEvents; - } - - public void OnNext(IndexChangeNotification value) - { - try - { - UpdatedCount().GetAwaiter().GetResult(); - } - catch (Exception ex) - { - logging.WarnFormat("Failed to emit MessageFailuresUpdated - {0}", ex); - } - } - - public void OnError(Exception error) - { - //Ignore - } - - public void OnCompleted() - { - //Ignore - } - - async Task UpdatedCount() - { - using (var session = store.OpenAsyncSession()) - { - var failedUnresolvedMessageCount = await session.Query().CountAsync(p => p.Status == FailedMessageStatus.Unresolved) - .ConfigureAwait(false); - var failedArchivedMessageCount = await session.Query().CountAsync(p => p.Status == FailedMessageStatus.Archived) - .ConfigureAwait(false); - - if (lastUnresolvedCount == failedUnresolvedMessageCount && lastArchivedCount == failedArchivedMessageCount) - { - return; - } - - lastUnresolvedCount = failedUnresolvedMessageCount; - lastArchivedCount = failedArchivedMessageCount; - - await domainEvents.Raise(new MessageFailuresUpdated - { - Total = failedUnresolvedMessageCount, // Left here for backwards compatibility, to be removed eventually. - UnresolvedTotal = failedUnresolvedMessageCount, - ArchivedTotal = failedArchivedMessageCount - }).ConfigureAwait(false); - } - } - - IDocumentStore store; - IDomainEvents domainEvents; - int lastUnresolvedCount; - int lastArchivedCount; - ILog logging = LogManager.GetLogger(typeof(FailedMessageViewIndexNotifications)); - } -} \ No newline at end of file diff --git a/src/ServiceControl/MessageFailures/Handlers/ArchiveMessageHandler.cs b/src/ServiceControl/MessageFailures/Handlers/ArchiveMessageHandler.cs index 7e36425b44..f0f5bc746d 100644 --- a/src/ServiceControl/MessageFailures/Handlers/ArchiveMessageHandler.cs +++ b/src/ServiceControl/MessageFailures/Handlers/ArchiveMessageHandler.cs @@ -1,48 +1,38 @@ namespace ServiceControl.MessageFailures.Handlers { - using System; using System.Threading.Tasks; using Contracts.MessageFailures; using Infrastructure.DomainEvents; using InternalMessages; using NServiceBus; - using Raven.Client; + using ServiceControl.Persistence; class ArchiveMessageHandler : IHandleMessages { - public ArchiveMessageHandler(IDocumentStore store, IDomainEvents domainEvents) + public ArchiveMessageHandler(IErrorMessageDataStore dataStore, IDomainEvents domainEvents) { - this.store = store; + this.dataStore = dataStore; this.domainEvents = domainEvents; } public async Task Handle(ArchiveMessage message, IMessageHandlerContext context) { - using (var session = store.OpenAsyncSession()) - { - var failedMessage = await session.LoadAsync(new Guid(message.FailedMessageId)) - .ConfigureAwait(false); + var failedMessageId = message.FailedMessageId; - if (failedMessage == null) - { - return; - } + var failedMessage = await dataStore.FailedMessageFetch(failedMessageId); - if (failedMessage.Status != FailedMessageStatus.Archived) + if (failedMessage.Status != FailedMessageStatus.Archived) + { + await domainEvents.Raise(new FailedMessageArchived { - failedMessage.Status = FailedMessageStatus.Archived; - - await domainEvents.Raise(new FailedMessageArchived - { - FailedMessageId = message.FailedMessageId - }).ConfigureAwait(false); - } + FailedMessageId = failedMessageId + }); - await session.SaveChangesAsync().ConfigureAwait(false); + await dataStore.FailedMessageMarkAsArchived(failedMessageId); } } - IDocumentStore store; + IErrorMessageDataStore dataStore; IDomainEvents domainEvents; } } \ No newline at end of file diff --git a/src/ServiceControl/MessageFailures/Handlers/LegacyMessageFailureResolvedHandler.cs b/src/ServiceControl/MessageFailures/Handlers/LegacyMessageFailureResolvedHandler.cs index 348e8710c4..1cf465463f 100644 --- a/src/ServiceControl/MessageFailures/Handlers/LegacyMessageFailureResolvedHandler.cs +++ b/src/ServiceControl/MessageFailures/Handlers/LegacyMessageFailureResolvedHandler.cs @@ -1,13 +1,11 @@ namespace ServiceControl.MessageFailures.Handlers { - using System; using System.Linq; using System.Threading.Tasks; using Contracts.MessageFailures; using Infrastructure.DomainEvents; using NServiceBus; - using Raven.Client; - using Recoverability; + using Persistence; /// /// This class handles legacy messages that mark a failed message as successfully retried. For further details go to message definitions. @@ -16,41 +14,40 @@ class LegacyMessageFailureResolvedHandler : IHandleMessages, IHandleMessages { - public LegacyMessageFailureResolvedHandler(IDocumentStore store, IDomainEvents domainEvents, RetryDocumentManager retryDocumentManager) + public LegacyMessageFailureResolvedHandler(IErrorMessageDataStore store, IDomainEvents domainEvents) { this.store = store; this.domainEvents = domainEvents; - this.retryDocumentManager = retryDocumentManager; } public async Task Handle(MarkMessageFailureResolvedByRetry message, IMessageHandlerContext context) { - await MarkAsResolvedByRetry(message.FailedMessageId, message.AlternativeFailedMessageIds) - .ConfigureAwait(false); + await MarkAsResolvedByRetry(message.FailedMessageId, message.AlternativeFailedMessageIds); await domainEvents.Raise(new MessageFailureResolvedByRetry { AlternativeFailedMessageIds = message.AlternativeFailedMessageIds, FailedMessageId = message.FailedMessageId - }).ConfigureAwait(false); + }); } // This is only needed because we might get this from legacy not yet converted instances public async Task Handle(MessageFailureResolvedByRetry message, IMessageHandlerContext context) { - await MarkAsResolvedByRetry(message.FailedMessageId, message.AlternativeFailedMessageIds) - .ConfigureAwait(false); + await MarkAsResolvedByRetry(message.FailedMessageId, message.AlternativeFailedMessageIds); await domainEvents.Raise(new MessageFailureResolvedByRetry { AlternativeFailedMessageIds = message.AlternativeFailedMessageIds, FailedMessageId = message.FailedMessageId - }).ConfigureAwait(false); + }); } async Task MarkAsResolvedByRetry(string primaryId, string[] messageAlternativeFailedMessageIds) { - await retryDocumentManager.RemoveFailedMessageRetryDocument(primaryId).ConfigureAwait(false); - if (await MarkMessageAsResolved(primaryId) - .ConfigureAwait(false)) + await store.RemoveFailedMessageRetryDocument(primaryId); + + var primaryUpdated = await store.MarkMessageAsResolved(primaryId); + + if (primaryUpdated) { return; } @@ -62,40 +59,18 @@ async Task MarkAsResolvedByRetry(string primaryId, string[] messageAlternativeFa foreach (var alternative in messageAlternativeFailedMessageIds.Where(x => x != primaryId)) { - await retryDocumentManager.RemoveFailedMessageRetryDocument(alternative).ConfigureAwait(false); - if (await MarkMessageAsResolved(alternative) - .ConfigureAwait(false)) - { - return; - } - } - } + await store.RemoveFailedMessageRetryDocument(alternative); - async Task MarkMessageAsResolved(string failedMessageId) - { + var alternativeUpdated = await store.MarkMessageAsResolved(alternative); - using (var session = store.OpenAsyncSession()) - { - session.Advanced.UseOptimisticConcurrency = true; - - var failedMessage = await session.LoadAsync(new Guid(failedMessageId)) - .ConfigureAwait(false); - - if (failedMessage == null) + if (alternativeUpdated) { - return false; + return; } - - failedMessage.Status = FailedMessageStatus.Resolved; - - await session.SaveChangesAsync().ConfigureAwait(false); - - return true; } } - IDocumentStore store; - IDomainEvents domainEvents; - RetryDocumentManager retryDocumentManager; + readonly IErrorMessageDataStore store; + readonly IDomainEvents domainEvents; } } \ No newline at end of file diff --git a/src/ServiceControl/MessageFailures/Handlers/MessageFailureResolvedHandler.cs b/src/ServiceControl/MessageFailures/Handlers/MessageFailureResolvedHandler.cs index 68231b6834..7c92c831c3 100644 --- a/src/ServiceControl/MessageFailures/Handlers/MessageFailureResolvedHandler.cs +++ b/src/ServiceControl/MessageFailures/Handlers/MessageFailureResolvedHandler.cs @@ -1,93 +1,53 @@ namespace ServiceControl.MessageFailures.Handlers { - using System; using System.Threading.Tasks; - using Api; using Contracts.MessageFailures; using Infrastructure.DomainEvents; using InternalMessages; using NServiceBus; - using Raven.Client; + using Persistence; class MessageFailureResolvedHandler : IHandleMessages, IHandleMessages { - public MessageFailureResolvedHandler(IDocumentStore store, IDomainEvents domainEvents) + public MessageFailureResolvedHandler(IErrorMessageDataStore dataStore, IDomainEvents domainEvents) { - this.store = store; + this.dataStore = dataStore; this.domainEvents = domainEvents; } - public async Task Handle(MarkPendingRetriesAsResolved message, IMessageHandlerContext context) + public Task Handle(MarkPendingRetriesAsResolved message, IMessageHandlerContext context) { - using (var session = store.OpenAsyncSession()) + Task ProcessCallback(string id) { - var prequery = session.Advanced - .AsyncDocumentQuery() - .WhereEquals("Status", (int)FailedMessageStatus.RetryIssued) - .AndAlso() - .WhereBetweenOrEqual("LastModified", message.PeriodFrom.Ticks, message.PeriodTo.Ticks); - - if (!string.IsNullOrWhiteSpace(message.QueueAddress)) - { - prequery = prequery.AndAlso() - .WhereEquals(options => options.QueueAddress, message.QueueAddress); - } - - var query = prequery - .SetResultTransformer(new FailedMessageViewTransformer().TransformerName) - .SelectFields(); - - using (var ie = await session.Advanced.StreamAsync(query).ConfigureAwait(false)) - { - while (await ie.MoveNextAsync().ConfigureAwait(false)) - { - var sendOptions = new SendOptions(); - sendOptions.RouteToThisEndpoint(); - // In AzureServiceBus transport there is a limit of 100 messages being sent in a single transaction - // These do not need to be transactionally consistent so we can dispatch the messages immediately - sendOptions.RequireImmediateDispatch(); - await context.Send(m => m.FailedMessageId = ie.Current.Document.Id, sendOptions) - .ConfigureAwait(false); - } - } + var sendOptions = new SendOptions(); + sendOptions.RouteToThisEndpoint(); + // In AzureServiceBus transport there is a limit of 100 messages being sent in a single transaction + // These do not need to be transactionally consistent so we can dispatch the messages immediately + sendOptions.RequireImmediateDispatch(); + return context.Send(m => m.FailedMessageId = id, sendOptions); } + + return dataStore.ProcessPendingRetries( + message.PeriodFrom, + message.PeriodTo, + message.QueueAddress, + ProcessCallback + ); } public async Task Handle(MarkPendingRetryAsResolved message, IMessageHandlerContext context) { - await MarkMessageAsResolved(message.FailedMessageId) - .ConfigureAwait(false); + _ = await dataStore.MarkMessageAsResolved(message.FailedMessageId); await domainEvents.Raise(new MessageFailureResolvedManually { FailedMessageId = message.FailedMessageId - }).ConfigureAwait(false); - } - - async Task MarkMessageAsResolved(string failedMessageId) - { - - using (var session = store.OpenAsyncSession()) - { - session.Advanced.UseOptimisticConcurrency = true; - - var failedMessage = await session.LoadAsync(new Guid(failedMessageId)) - .ConfigureAwait(false); - - if (failedMessage == null) - { - return; - } - - failedMessage.Status = FailedMessageStatus.Resolved; - - await session.SaveChangesAsync().ConfigureAwait(false); - } + }); } - IDocumentStore store; + IErrorMessageDataStore dataStore; IDomainEvents domainEvents; } } \ No newline at end of file diff --git a/src/ServiceControl/MessageFailures/Handlers/UnArchiveMessagesByRangeHandler.cs b/src/ServiceControl/MessageFailures/Handlers/UnArchiveMessagesByRangeHandler.cs index f6c2e70d88..7c98893fb7 100644 --- a/src/ServiceControl/MessageFailures/Handlers/UnArchiveMessagesByRangeHandler.cs +++ b/src/ServiceControl/MessageFailures/Handlers/UnArchiveMessagesByRangeHandler.cs @@ -1,67 +1,40 @@ namespace ServiceControl.MessageFailures.Handlers { - using System.Globalization; + using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; - using Api; using Contracts.MessageFailures; using Infrastructure.DomainEvents; using InternalMessages; using NServiceBus; - using Raven.Abstractions.Data; - using Raven.Abstractions.Extensions; - using Raven.Client; + using Persistence; class UnArchiveMessagesByRangeHandler : IHandleMessages { - public UnArchiveMessagesByRangeHandler(IDocumentStore store, IDomainEvents domainEvents) + public UnArchiveMessagesByRangeHandler(IErrorMessageDataStore dataStore, IDomainEvents domainEvents) { - this.store = store; + this.dataStore = dataStore; this.domainEvents = domainEvents; } public async Task Handle(UnArchiveMessagesByRange message, IMessageHandlerContext context) { - var options = new BulkOperationOptions - { - AllowStale = true - }; - var result = await store.AsyncDatabaseCommands.UpdateByIndexAsync( - new FailedMessageViewIndex().IndexName, - new IndexQuery - { - Query = string.Format(CultureInfo.InvariantCulture, "LastModified:[{0} TO {1}] AND Status:{2}", message.From.Ticks, message.To.Ticks, (int)FailedMessageStatus.Archived), - Cutoff = message.CutOff - }, new ScriptedPatchRequest - { - Script = @" -if(this.Status === archivedStatus) { - this.Status = unresolvedStatus; -} -", - Values = - { - {"archivedStatus", (int)FailedMessageStatus.Archived}, - {"unresolvedStatus", (int)FailedMessageStatus.Unresolved} - } - }, options).ConfigureAwait(false); - - var patchedDocumentIds = (await result.WaitForCompletionAsync().ConfigureAwait(false)) - .JsonDeserialization(); + var (ids, count) = await dataStore.UnArchiveMessagesByRange( + message.From, + message.To, + message.CutOff + ); await domainEvents.Raise(new FailedMessagesUnArchived { - DocumentIds = patchedDocumentIds.Select(x => FailedMessage.GetMessageIdFromDocumentId(x.Document)).ToArray(), - MessagesCount = patchedDocumentIds.Length - }).ConfigureAwait(false); + DocumentIds = ids, + MessagesCount = count + }); + } - IDocumentStore store; + IErrorMessageDataStore dataStore; IDomainEvents domainEvents; - class DocumentPatchResult - { - public string Document { get; set; } - } } } \ No newline at end of file diff --git a/src/ServiceControl/MessageFailures/Handlers/UnArchiveMessagesHandler.cs b/src/ServiceControl/MessageFailures/Handlers/UnArchiveMessagesHandler.cs index 554b69dede..f805940d40 100644 --- a/src/ServiceControl/MessageFailures/Handlers/UnArchiveMessagesHandler.cs +++ b/src/ServiceControl/MessageFailures/Handlers/UnArchiveMessagesHandler.cs @@ -6,11 +6,11 @@ using Infrastructure.DomainEvents; using InternalMessages; using NServiceBus; - using Raven.Client; + using Persistence; class UnArchiveMessagesHandler : IHandleMessages { - public UnArchiveMessagesHandler(IDocumentStore store, IDomainEvents domainEvents) + public UnArchiveMessagesHandler(IErrorMessageDataStore store, IDomainEvents domainEvents) { this.store = store; this.domainEvents = domainEvents; @@ -18,35 +18,16 @@ public UnArchiveMessagesHandler(IDocumentStore store, IDomainEvents domainEvents public async Task Handle(UnArchiveMessages messages, IMessageHandlerContext context) { - FailedMessage[] failedMessages; - - using (var session = store.OpenAsyncSession()) - { - session.Advanced.UseOptimisticConcurrency = true; - - failedMessages = await session.LoadAsync(messages.FailedMessageIds.Select(FailedMessage.MakeDocumentId)) - .ConfigureAwait(false); - - foreach (var failedMessage in failedMessages) - { - if (failedMessage.Status == FailedMessageStatus.Archived) - { - failedMessage.Status = FailedMessageStatus.Unresolved; - } - } - - await session.SaveChangesAsync() - .ConfigureAwait(false); - } + var (ids, count) = await store.UnArchiveMessages(messages.FailedMessageIds); await domainEvents.Raise(new FailedMessagesUnArchived { - DocumentIds = failedMessages.Select(x => x.UniqueMessageId).ToArray(), - MessagesCount = failedMessages.Length - }).ConfigureAwait(false); + DocumentIds = ids, + MessagesCount = count + }); } - IDocumentStore store; + IErrorMessageDataStore store; IDomainEvents domainEvents; } } \ No newline at end of file diff --git a/src/ServiceControl/MessageFailures/InternalMessages/ReclassificationOfErrorMessageComplete.cs b/src/ServiceControl/MessageFailures/InternalMessages/ReclassificationOfErrorMessageComplete.cs index 69b92a5b17..428c67c01c 100644 --- a/src/ServiceControl/MessageFailures/InternalMessages/ReclassificationOfErrorMessageComplete.cs +++ b/src/ServiceControl/MessageFailures/InternalMessages/ReclassificationOfErrorMessageComplete.cs @@ -2,6 +2,7 @@ { using Infrastructure.DomainEvents; + // TODO: Only used by Raven3.5 storage engine public class ReclassificationOfErrorMessageComplete : IDomainEvent { public int NumberofMessageReclassified { get; set; } diff --git a/src/ServiceControl/MessageFailures/InternalMessages/ReclassifyErrors.cs b/src/ServiceControl/MessageFailures/InternalMessages/ReclassifyErrors.cs index b7e8843db9..27cf4d71e2 100644 --- a/src/ServiceControl/MessageFailures/InternalMessages/ReclassifyErrors.cs +++ b/src/ServiceControl/MessageFailures/InternalMessages/ReclassifyErrors.cs @@ -1,7 +1,9 @@ namespace ServiceControl.MessageFailures.InternalMessages { + using System; using NServiceBus; + [Obsolete("Only used by legacy RavenDB35 storage engine")] // TODO: how to deal with this domain event class ReclassifyErrors : ICommand { public bool Force { get; set; } diff --git a/src/ServiceControl/MessageFailures/InternalMessages/UnArchiveMessages.cs b/src/ServiceControl/MessageFailures/InternalMessages/UnArchiveMessages.cs index c4e01da278..588160ad8d 100644 --- a/src/ServiceControl/MessageFailures/InternalMessages/UnArchiveMessages.cs +++ b/src/ServiceControl/MessageFailures/InternalMessages/UnArchiveMessages.cs @@ -5,6 +5,6 @@ class UnArchiveMessages : ICommand { - public List FailedMessageIds { get; set; } + public List FailedMessageIds { get; set; } // TODO: } } \ No newline at end of file diff --git a/src/ServiceControl/MessageRedirects/Api/MessageRedirectsCollectionExtensions.cs b/src/ServiceControl/MessageRedirects/Api/MessageRedirectsCollectionExtensions.cs index dc76c68a8b..b6c74b6ddb 100644 --- a/src/ServiceControl/MessageRedirects/Api/MessageRedirectsCollectionExtensions.cs +++ b/src/ServiceControl/MessageRedirects/Api/MessageRedirectsCollectionExtensions.cs @@ -4,6 +4,7 @@ namespace ServiceControl.MessageRedirects.Api using System.Collections.Generic; using System.Linq; using System.Net.Http; + using ServiceControl.Persistence.MessageRedirects; static class MessageRedirectsCollectionExtensions { diff --git a/src/ServiceControl/MessageRedirects/Api/MessageRedirectsController.cs b/src/ServiceControl/MessageRedirects/Api/MessageRedirectsController.cs index 3b0871e8e6..3a3143abfb 100644 --- a/src/ServiceControl/MessageRedirects/Api/MessageRedirectsController.cs +++ b/src/ServiceControl/MessageRedirects/Api/MessageRedirectsController.cs @@ -12,13 +12,13 @@ using Infrastructure.WebApi; using MessageFailures.InternalMessages; using NServiceBus; - using Raven.Client; + using ServiceControl.Persistence.MessageRedirects; class MessageRedirectsController : ApiController { - public MessageRedirectsController(IMessageSession messageSession, IDocumentStore documentStore, IDomainEvents domainEvents) + public MessageRedirectsController(IMessageSession messageSession, IMessageRedirectsDataStore redirectsStore, IDomainEvents domainEvents) { - this.documentStore = documentStore; + this.redirectsStore = redirectsStore; this.domainEvents = domainEvents; this.messageSession = messageSession; } @@ -39,37 +39,34 @@ public async Task NewRedirects(MessageRedirectRequest reque LastModifiedTicks = DateTime.UtcNow.Ticks }; - using (var session = documentStore.OpenAsyncSession()) - { - var collection = await MessageRedirectsCollection.GetOrCreate(session).ConfigureAwait(false); + var collection = await redirectsStore.GetOrCreate(); - var existing = collection[messageRedirect.MessageRedirectId]; + var existing = collection[messageRedirect.MessageRedirectId]; - if (existing != null) - { - return existing.ToPhysicalAddress == messageRedirect.ToPhysicalAddress - ? Negotiator.FromModel(Request, messageRedirect, HttpStatusCode.Created) - : Negotiator.FromModel(Request, existing, HttpStatusCode.Conflict).WithReasonPhrase("Duplicate"); - } + if (existing != null) + { + return existing.ToPhysicalAddress == messageRedirect.ToPhysicalAddress + ? Negotiator.FromModel(Request, messageRedirect, HttpStatusCode.Created) + : Negotiator.FromModel(Request, existing, HttpStatusCode.Conflict).WithReasonPhrase("Duplicate"); + } - var dependents = collection.Redirects.Where(r => r.ToPhysicalAddress == request.fromphysicaladdress).ToList(); + var dependents = collection.Redirects.Where(r => r.ToPhysicalAddress == request.fromphysicaladdress).ToList(); - if (dependents.Any()) - { - return Negotiator.FromModel(Request, dependents, HttpStatusCode.Conflict).WithReasonPhrase("Dependents"); - } + if (dependents.Any()) + { + return Negotiator.FromModel(Request, dependents, HttpStatusCode.Conflict).WithReasonPhrase("Dependents"); + } - collection.Redirects.Add(messageRedirect); + collection.Redirects.Add(messageRedirect); - await collection.Save(session).ConfigureAwait(false); - } + await redirectsStore.Save(collection); await domainEvents.Raise(new MessageRedirectCreated { MessageRedirectId = messageRedirect.MessageRedirectId, FromPhysicalAddress = messageRedirect.FromPhysicalAddress, ToPhysicalAddress = messageRedirect.ToPhysicalAddress - }).ConfigureAwait(false); + }); if (request.retryexisting) { @@ -78,7 +75,7 @@ await messageSession.SendLocal(new RetryPendingMessages QueueAddress = messageRedirect.FromPhysicalAddress, PeriodFrom = DateTime.MinValue, PeriodTo = DateTime.UtcNow - }).ConfigureAwait(false); + }); } return Request.CreateResponse(HttpStatusCode.Created); @@ -93,69 +90,62 @@ public async Task UpdateRedirect(Guid messageRedirectId, Me return Request.CreateResponse(HttpStatusCode.BadRequest); } - using (var session = documentStore.OpenAsyncSession()) - { - var redirects = await MessageRedirectsCollection.GetOrCreate(session).ConfigureAwait(false); + var redirects = await redirectsStore.GetOrCreate(); - var messageRedirect = redirects[messageRedirectId]; + var messageRedirect = redirects[messageRedirectId]; - if (messageRedirect == null) - { - return Request.CreateResponse(HttpStatusCode.NotFound); - } + if (messageRedirect == null) + { + return Request.CreateResponse(HttpStatusCode.NotFound); + } - var toMessageRedirectId = DeterministicGuid.MakeId(request.tophysicaladdress); + var toMessageRedirectId = DeterministicGuid.MakeId(request.tophysicaladdress); - if (redirects[toMessageRedirectId] != null) - { - return Request.CreateResponse(HttpStatusCode.Conflict); - } + if (redirects[toMessageRedirectId] != null) + { + return Request.CreateResponse(HttpStatusCode.Conflict); + } - var messageRedirectChanged = new MessageRedirectChanged - { - MessageRedirectId = messageRedirectId, - PreviousToPhysicalAddress = messageRedirect.ToPhysicalAddress, - FromPhysicalAddress = messageRedirect.FromPhysicalAddress, - ToPhysicalAddress = messageRedirect.ToPhysicalAddress = request.tophysicaladdress - }; + var messageRedirectChanged = new MessageRedirectChanged + { + MessageRedirectId = messageRedirectId, + PreviousToPhysicalAddress = messageRedirect.ToPhysicalAddress, + FromPhysicalAddress = messageRedirect.FromPhysicalAddress, + ToPhysicalAddress = messageRedirect.ToPhysicalAddress = request.tophysicaladdress + }; - messageRedirect.LastModifiedTicks = DateTime.UtcNow.Ticks; + messageRedirect.LastModifiedTicks = DateTime.UtcNow.Ticks; - await redirects.Save(session).ConfigureAwait(false); + await redirectsStore.Save(redirects); - await domainEvents.Raise(messageRedirectChanged) - .ConfigureAwait(false); + await domainEvents.Raise(messageRedirectChanged); - return Request.CreateResponse(HttpStatusCode.NoContent); - } + return Request.CreateResponse(HttpStatusCode.NoContent); } [Route("redirects/{messageredirectid:guid}")] [HttpDelete] public async Task DeleteRedirect(Guid messageRedirectId) { - using (var session = documentStore.OpenAsyncSession()) - { - var redirects = await MessageRedirectsCollection.GetOrCreate(session).ConfigureAwait(false); + var redirects = await redirectsStore.GetOrCreate(); - var messageRedirect = redirects[messageRedirectId]; + var messageRedirect = redirects[messageRedirectId]; - if (messageRedirect == null) - { - return Request.CreateResponse(HttpStatusCode.NoContent); - } + if (messageRedirect == null) + { + return Request.CreateResponse(HttpStatusCode.NoContent); + } - redirects.Redirects.Remove(messageRedirect); + redirects.Redirects.Remove(messageRedirect); - await redirects.Save(session).ConfigureAwait(false); + await redirectsStore.Save(redirects); - await domainEvents.Raise(new MessageRedirectRemoved - { - MessageRedirectId = messageRedirectId, - FromPhysicalAddress = messageRedirect.FromPhysicalAddress, - ToPhysicalAddress = messageRedirect.ToPhysicalAddress - }).ConfigureAwait(false); - } + await domainEvents.Raise(new MessageRedirectRemoved + { + MessageRedirectId = messageRedirectId, + FromPhysicalAddress = messageRedirect.FromPhysicalAddress, + ToPhysicalAddress = messageRedirect.ToPhysicalAddress + }); return Request.CreateResponse(HttpStatusCode.NoContent); } @@ -164,45 +154,39 @@ await domainEvents.Raise(new MessageRedirectRemoved [HttpHead] public async Task CountRedirects() { - using (var session = documentStore.OpenAsyncSession()) - { - var redirects = await MessageRedirectsCollection.GetOrCreate(session).ConfigureAwait(false); + var redirects = await redirectsStore.GetOrCreate(); - return Request.CreateResponse(HttpStatusCode.OK) - .WithEtag(redirects.ETag) - .WithTotalCount(redirects.Redirects.Count); - } + return Request.CreateResponse(HttpStatusCode.OK) + .WithEtag(redirects.ETag) + .WithTotalCount(redirects.Redirects.Count); } [Route("redirects")] [HttpGet] public async Task Redirects() { - using (var session = documentStore.OpenAsyncSession()) - { - var redirects = await MessageRedirectsCollection.GetOrCreate(session).ConfigureAwait(false); - - var queryResult = redirects - .Sort(Request) - .Paging(Request) - .Select(r => new - { - r.MessageRedirectId, - r.FromPhysicalAddress, - r.ToPhysicalAddress, - LastModified = new DateTime(r.LastModifiedTicks) - }); - - return Negotiator - .FromModel(Request, queryResult) - .WithEtag(redirects.ETag) - .WithPagingLinksAndTotalCount(redirects.Redirects.Count, Request); - } + var redirects = await redirectsStore.GetOrCreate(); + + var queryResult = redirects + .Sort(Request) + .Paging(Request) + .Select(r => new + { + r.MessageRedirectId, + r.FromPhysicalAddress, + r.ToPhysicalAddress, + LastModified = new DateTime(r.LastModifiedTicks) + }); + + return Negotiator + .FromModel(Request, queryResult) + .WithEtag(redirects.ETag) + .WithPagingLinksAndTotalCount(redirects.Redirects.Count, Request); } readonly IDomainEvents domainEvents; - readonly IDocumentStore documentStore; - IMessageSession messageSession; + readonly IMessageRedirectsDataStore redirectsStore; + readonly IMessageSession messageSession; public class MessageRedirectRequest { diff --git a/src/ServiceControl/MessageRedirects/MessageRedirectsCollection.cs b/src/ServiceControl/MessageRedirects/MessageRedirectsCollection.cs deleted file mode 100644 index 82b3229661..0000000000 --- a/src/ServiceControl/MessageRedirects/MessageRedirectsCollection.cs +++ /dev/null @@ -1,46 +0,0 @@ -namespace ServiceControl.MessageRedirects -{ - using System; - using System.Collections.Generic; - using System.Linq; - using System.Threading.Tasks; - using Raven.Abstractions.Data; - using Raven.Client; - - class MessageRedirectsCollection - { - public string Id { get; set; } = DefaultId; - - public Etag ETag { get; set; } - - public DateTime LastModified { get; set; } - - public MessageRedirect this[string from] => Redirects.SingleOrDefault(r => r.FromPhysicalAddress == from); - public MessageRedirect this[Guid id] => Redirects.SingleOrDefault(r => r.MessageRedirectId == id); - - public List Redirects { get; set; } = new List(); - - public async Task Save(IAsyncDocumentSession session) - { - await session.StoreAsync(this).ConfigureAwait(false); - await session.SaveChangesAsync().ConfigureAwait(false); - } - - public static async Task GetOrCreate(IAsyncDocumentSession session) - { - var redirects = await session.LoadAsync(DefaultId).ConfigureAwait(false); - - if (redirects != null) - { - redirects.ETag = session.Advanced.GetEtagFor(redirects); - redirects.LastModified = session.Advanced.GetMetadataFor(redirects).Value("Last-Modified"); - - return redirects; - } - - return new MessageRedirectsCollection(); - } - - const string DefaultId = "messageredirects"; - } -} \ No newline at end of file diff --git a/src/ServiceControl/Monitoring/EndpointInstanceMonitor.cs b/src/ServiceControl/Monitoring/EndpointInstanceMonitor.cs index 466df8ae44..2941c43b9b 100644 --- a/src/ServiceControl/Monitoring/EndpointInstanceMonitor.cs +++ b/src/ServiceControl/Monitoring/EndpointInstanceMonitor.cs @@ -23,15 +23,13 @@ public EndpointInstanceMonitor(EndpointInstanceId endpointInstanceId, bool monit public async Task EnableMonitoring() { - await domainEvents.Raise(new MonitoringEnabledForEndpoint { Endpoint = Convert(Id) }) - .ConfigureAwait(false); + await domainEvents.Raise(new MonitoringEnabledForEndpoint { Endpoint = Convert(Id) }); Monitored = true; } public async Task DisableMonitoring() { - await domainEvents.Raise(new MonitoringDisabledForEndpoint { Endpoint = Convert(Id) }) - .ConfigureAwait(false); + await domainEvents.Raise(new MonitoringDisabledForEndpoint { Endpoint = Convert(Id) }); Monitored = false; } @@ -39,8 +37,7 @@ public async Task UpdateStatus(HeartbeatStatus newStatus, DateTime? latestTimest { if (newStatus != status) { - await RaiseStateChangeEvents(newStatus, latestTimestamp) - .ConfigureAwait(false); + await RaiseStateChangeEvents(newStatus, latestTimestamp); } lastSeen = latestTimestamp; @@ -60,7 +57,7 @@ await domainEvents.Raise(new HeartbeatingEndpointDetected { Endpoint = Convert(Id), DetectedAt = latestTimestamp ?? DateTime.UtcNow - }).ConfigureAwait(false); + }); } else if (status == HeartbeatStatus.Dead && Monitored) { @@ -68,7 +65,7 @@ await domainEvents.Raise(new EndpointHeartbeatRestored { Endpoint = Convert(Id), RestoredAt = latestTimestamp ?? DateTime.UtcNow - }).ConfigureAwait(false); + }); } } else if (newStatus == HeartbeatStatus.Dead && Monitored) @@ -78,7 +75,7 @@ await domainEvents.Raise(new EndpointFailedToHeartbeat Endpoint = Convert(Id), DetectedAt = DateTime.UtcNow, LastReceivedAt = latestTimestamp ?? DateTime.MinValue - }).ConfigureAwait(false); + }); } } diff --git a/src/ServiceControl/Monitoring/EndpointInstanceMonitoring.cs b/src/ServiceControl/Monitoring/EndpointInstanceMonitoring.cs index 6a1c1e1217..d74ba45356 100644 --- a/src/ServiceControl/Monitoring/EndpointInstanceMonitoring.cs +++ b/src/ServiceControl/Monitoring/EndpointInstanceMonitoring.cs @@ -28,13 +28,12 @@ public async Task CheckEndpoints(DateTime threshold) var endpointInstanceId = entry.Key; var monitor = endpoints.GetOrAdd(endpointInstanceId.UniqueId, id => new EndpointInstanceMonitor(endpointInstanceId, true, domainEvents)); - await monitor.UpdateStatus(recordedHeartbeat.Status, recordedHeartbeat.Timestamp) - .ConfigureAwait(false); + await monitor.UpdateStatus(recordedHeartbeat.Status, recordedHeartbeat.Timestamp); } var stats = GetStats(); - await Update(stats).ConfigureAwait(false); + await Update(stats); } public bool IsNewInstance(EndpointDetails newEndpointDetails) @@ -53,7 +52,7 @@ await domainEvents.Raise(new EndpointDetected { DetectedAt = DateTime.UtcNow, Endpoint = newEndpointDetails - }).ConfigureAwait(false); + }); } } @@ -66,7 +65,7 @@ await domainEvents.Raise(new EndpointStarted { EndpointDetails = newEndpointDetails, StartedAt = startedAt - }).ConfigureAwait(false); + }); } public void DetectEndpointFromPersistentStore(EndpointDetails endpointDetails, bool monitored) @@ -86,7 +85,7 @@ await domainEvents.Raise(new HeartbeatsUpdated Active = stats.Active, Failing = stats.Failing, RaisedAt = DateTime.UtcNow - }).ConfigureAwait(false); + }); previousStats = stats; } } diff --git a/src/ServiceControl/Monitoring/HeartbeatMonitoringComponent.cs b/src/ServiceControl/Monitoring/HeartbeatMonitoringComponent.cs index fe41afb8bc..5bfa500045 100644 --- a/src/ServiceControl/Monitoring/HeartbeatMonitoringComponent.cs +++ b/src/ServiceControl/Monitoring/HeartbeatMonitoringComponent.cs @@ -5,7 +5,6 @@ using EventLog; using ExternalIntegrations; using Infrastructure.DomainEvents; - using Infrastructure.RavenDB; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Hosting; using Particular.ServiceControl; @@ -19,7 +18,6 @@ public override void Configure(Settings settings, IHostBuilder hostBuilder) { hostBuilder.ConfigureServices(collection => { - collection.AddSingleton(); collection.AddHostedService(); collection.AddSingleton(); @@ -39,15 +37,5 @@ public override void Configure(Settings settings, IHostBuilder hostBuilder) collection.AddPlatformConnectionProvider(); }); } - - public override void Setup(Settings settings, IComponentInstallationContext context) - { - // TODO: Move this in the persister project - if (settings.DataStoreType == DataStoreType.SqlDb) - { - var connectionString = SettingsReader.Read("SqlStorageConnectionString"); - context.RegisterInstallationTask(() => SetupSqlPersistence.SetupMonitoring(connectionString)); - } - } } } \ No newline at end of file diff --git a/src/ServiceControl/Monitoring/HeartbeatMonitoringHostedService.cs b/src/ServiceControl/Monitoring/HeartbeatMonitoringHostedService.cs index f062728689..be11f2f486 100644 --- a/src/ServiceControl/Monitoring/HeartbeatMonitoringHostedService.cs +++ b/src/ServiceControl/Monitoring/HeartbeatMonitoringHostedService.cs @@ -20,7 +20,7 @@ public HeartbeatMonitoringHostedService(IEndpointInstanceMonitoring monitor, IMo } public async Task StartAsync(CancellationToken cancellationToken) { - await persistence.WarmupMonitoringFromPersistence(monitor).ConfigureAwait(false); + await persistence.WarmupMonitoringFromPersistence(monitor); timer = scheduler.Schedule(_ => CheckEndpoints(), TimeSpan.Zero, TimeSpan.FromSeconds(5), e => { log.Error("Exception occurred when monitoring endpoint instances", e); }); } @@ -28,7 +28,7 @@ public async Task StopAsync(CancellationToken cancellationToken) { try { - await timer.Stop().ConfigureAwait(false); + await timer.Stop(); } catch (OperationCanceledException) { @@ -44,7 +44,7 @@ async Task CheckEndpoints() log.Debug($"Monitoring Endpoint Instances. Inactivity Threshold = {inactivityThreshold}"); } - await monitor.CheckEndpoints(inactivityThreshold).ConfigureAwait(false); + await monitor.CheckEndpoints(inactivityThreshold); return TimerJobExecutionResult.ScheduleNextExecution; } diff --git a/src/ServiceControl/Monitoring/HeartbeatRestoredPublisher.cs b/src/ServiceControl/Monitoring/HeartbeatRestoredPublisher.cs index 2b2fddb25d..7dfae2050e 100644 --- a/src/ServiceControl/Monitoring/HeartbeatRestoredPublisher.cs +++ b/src/ServiceControl/Monitoring/HeartbeatRestoredPublisher.cs @@ -7,7 +7,6 @@ namespace ServiceControl.Monitoring using Contracts; using Contracts.HeartbeatMonitoring; using ExternalIntegrations; - using Raven.Client; class HeartbeatRestoredPublisher : EventPublisher { @@ -22,7 +21,7 @@ protected override DispatchContext CreateDispatchRequest(EndpointHeartbeatRestor }; } - protected override Task> PublishEvents(IEnumerable contexts, IAsyncDocumentSession session) + protected override Task> PublishEvents(IEnumerable contexts) { return Task.FromResult(contexts.Select(r => (object)new HeartbeatRestored { diff --git a/src/ServiceControl/Monitoring/HeartbeatStoppedPublisher.cs b/src/ServiceControl/Monitoring/HeartbeatStoppedPublisher.cs index e1df5c67ca..b3cc02365b 100644 --- a/src/ServiceControl/Monitoring/HeartbeatStoppedPublisher.cs +++ b/src/ServiceControl/Monitoring/HeartbeatStoppedPublisher.cs @@ -7,7 +7,6 @@ namespace ServiceControl.Monitoring using Contracts; using Contracts.HeartbeatMonitoring; using ExternalIntegrations; - using Raven.Client; class HeartbeatStoppedPublisher : EventPublisher { @@ -23,7 +22,7 @@ protected override DispatchContext CreateDispatchRequest(EndpointFailedToHeartbe }; } - protected override Task> PublishEvents(IEnumerable contexts, IAsyncDocumentSession session) + protected override Task> PublishEvents(IEnumerable contexts) { return Task.FromResult(contexts.Select(r => (object)new HeartbeatStopped { diff --git a/src/ServiceControl/Monitoring/Web/EndpointsMonitoringController.cs b/src/ServiceControl/Monitoring/Web/EndpointsMonitoringController.cs index 8e287e5ffe..3d5abaa910 100644 --- a/src/ServiceControl/Monitoring/Web/EndpointsMonitoringController.cs +++ b/src/ServiceControl/Monitoring/Web/EndpointsMonitoringController.cs @@ -58,7 +58,7 @@ public async Task DeleteEndpoint(Guid endpointId) return StatusCode(HttpStatusCode.NotFound); } - await monitoringDataStore.Delete(endpointId).ConfigureAwait(false); + await monitoringDataStore.Delete(endpointId); endpointInstanceMonitoring.RemoveEndpoint(endpointId); return StatusCode(HttpStatusCode.NoContent); @@ -66,7 +66,7 @@ public async Task DeleteEndpoint(Guid endpointId) [Route("endpoints/known")] [HttpGet] - public Task KnownEndpoints() => getKnownEndpointsApi.Execute(this, endpointInstanceMonitoring); + public Task KnownEndpoints() => getKnownEndpointsApi.Execute(this); [Route("endpoints/{endpointId}")] @@ -75,13 +75,11 @@ public async Task Foo(Guid endpointId, EndpointUpdateModel dat { if (data.MonitorHeartbeat) { - await endpointInstanceMonitoring.EnableMonitoring(endpointId) - .ConfigureAwait(false); + await endpointInstanceMonitoring.EnableMonitoring(endpointId); } else { - await endpointInstanceMonitoring.DisableMonitoring(endpointId) - .ConfigureAwait(false); + await endpointInstanceMonitoring.DisableMonitoring(endpointId); } return StatusCode(HttpStatusCode.Accepted); diff --git a/src/ServiceControl/Monitoring/Web/GetKnownEndpointsApi.cs b/src/ServiceControl/Monitoring/Web/GetKnownEndpointsApi.cs index 739f5e306c..e815b7af9a 100644 --- a/src/ServiceControl/Monitoring/Web/GetKnownEndpointsApi.cs +++ b/src/ServiceControl/Monitoring/Web/GetKnownEndpointsApi.cs @@ -6,23 +6,24 @@ using System.Net.Http; using System.Threading.Tasks; using CompositeViews.Messages; - using Raven.Client; using ServiceBus.Management.Infrastructure.Settings; using ServiceControl.Persistence; using ServiceControl.Persistence.Infrastructure; - class GetKnownEndpointsApi : ScatterGatherApi> + class GetKnownEndpointsApi : ScatterGatherApiNoInput> { - public GetKnownEndpointsApi(IDocumentStore documentStore, Settings settings, Func httpClientFactory) : base(documentStore, settings, httpClientFactory) + public GetKnownEndpointsApi(IEndpointInstanceMonitoring store, Settings settings, Func httpClientFactory) : base(store, settings, httpClientFactory) { } - protected override Task>> LocalQuery(HttpRequestMessage request, IEndpointInstanceMonitoring input) + protected override Task>> LocalQuery(HttpRequestMessage request) { + var knownEndpoints = DataStore.GetKnownEndpoints(); + return Task.FromResult( new QueryResult>( - input.GetKnownEndpoints(), - new QueryStatsInfo(string.Empty, input.GetKnownEndpoints().Count) + knownEndpoints, + new QueryStatsInfo(string.Empty, knownEndpoints.Count, isStale: false) ) ); } diff --git a/src/ServiceControl/Notifications/Api/NotificationsController.cs b/src/ServiceControl/Notifications/Api/NotificationsController.cs index ed15a81f9d..d74769537d 100644 --- a/src/ServiceControl/Notifications/Api/NotificationsController.cs +++ b/src/ServiceControl/Notifications/Api/NotificationsController.cs @@ -7,12 +7,12 @@ using System.Web.Http; using Email; using Infrastructure.WebApi; - using Raven.Client; + using Persistence; using ServiceBus.Management.Infrastructure.Settings; class NotificationsController : ApiController { - public NotificationsController(IDocumentStore store, Settings settings) + public NotificationsController(IErrorMessageDataStore store, Settings settings) { this.store = store; instanceName = settings.ServiceName; @@ -22,9 +22,9 @@ public NotificationsController(IDocumentStore store, Settings settings) [HttpGet] public async Task GetEmailNotificationsSettings(HttpRequestMessage request) { - using (var session = store.OpenAsyncSession()) + using (var manager = await store.CreateNotificationsManager()) { - var settings = await LoadSettings(session).ConfigureAwait(false); + var settings = await manager.LoadSettings(); return Negotiator.FromModel(request, settings.Email); } @@ -34,13 +34,13 @@ public async Task GetEmailNotificationsSettings(HttpRequest [HttpPost] public async Task ToggleEmailNotifications(ToggleEmailNotifications request) { - using (var session = store.OpenAsyncSession()) + using (var manager = await store.CreateNotificationsManager()) { - var settings = await LoadSettings(session).ConfigureAwait(false); + var settings = await manager.LoadSettings(); settings.Email.Enabled = request.Enabled; - await session.SaveChangesAsync().ConfigureAwait(false); + await manager.SaveChanges(); } return new HttpResponseMessage(HttpStatusCode.OK); @@ -50,9 +50,9 @@ public async Task ToggleEmailNotifications(ToggleEmailNotif [HttpPost] public async Task UpdateSettings(UpdateEmailNotificationsSettingsRequest request) { - using (var session = store.OpenAsyncSession()) + using (var manager = await store.CreateNotificationsManager()) { - var settings = await LoadSettings(session).ConfigureAwait(false); + var settings = await manager.LoadSettings(); var emailSettings = settings.Email; @@ -66,7 +66,7 @@ public async Task UpdateSettings(UpdateEmailNotificationsSe emailSettings.From = request.From; emailSettings.To = request.To; - await session.SaveChangesAsync().ConfigureAwait(false); + await manager.SaveChanges(); return new HttpResponseMessage(HttpStatusCode.OK); } @@ -76,17 +76,16 @@ public async Task UpdateSettings(UpdateEmailNotificationsSe [HttpPost] public async Task SendTestEmail() { - using (var session = store.OpenAsyncSession()) + using (var manager = await store.CreateNotificationsManager()) { - var settings = await LoadSettings(session).ConfigureAwait(false); + var settings = await manager.LoadSettings(); try { await EmailSender.Send( settings.Email, $"[{instanceName}] health check notification check successful", - $"[{instanceName}] health check notification check successful.") - .ConfigureAwait(false); + $"[{instanceName}] health check notification check successful."); } catch (Exception e) { @@ -101,24 +100,7 @@ await EmailSender.Send( } } - static async Task LoadSettings(IAsyncDocumentSession session) - { - var settings = await session.LoadAsync(NotificationsSettings.SingleDocumentId).ConfigureAwait(false); - - if (settings == null) - { - settings = new NotificationsSettings - { - Id = NotificationsSettings.SingleDocumentId - }; - - await session.StoreAsync(settings).ConfigureAwait(false); - } - - return settings; - } - - readonly IDocumentStore store; + readonly IErrorMessageDataStore store; readonly string instanceName; } } diff --git a/src/ServiceControl/Notifications/Email/EmailSender.cs b/src/ServiceControl/Notifications/Email/EmailSender.cs index 18572df626..5201183b6e 100644 --- a/src/ServiceControl/Notifications/Email/EmailSender.cs +++ b/src/ServiceControl/Notifications/Email/EmailSender.cs @@ -15,7 +15,7 @@ public static async Task Send(EmailNotifications settings, string subject, strin using (var client = CreateSmtpClient(settings, emailDropFolder)) using (var mailMessage = new MailMessage(settings.From, settings.To, subject, body)) { - await client.SendMailAsync(mailMessage).ConfigureAwait(false); + await client.SendMailAsync(mailMessage); } } catch (Exception e) diff --git a/src/ServiceControl/Notifications/Email/SendEmailNotificationHandler.cs b/src/ServiceControl/Notifications/Email/SendEmailNotificationHandler.cs index 65647dd65c..3344bed831 100644 --- a/src/ServiceControl/Notifications/Email/SendEmailNotificationHandler.cs +++ b/src/ServiceControl/Notifications/Email/SendEmailNotificationHandler.cs @@ -5,15 +5,15 @@ using NServiceBus; using NServiceBus.Logging; using NServiceBus.Transport; - using Raven.Client; + using Persistence; using ServiceBus.Management.Infrastructure.Settings; public class SendEmailNotificationHandler : IHandleMessages { - readonly IDocumentStore store; + readonly IErrorMessageDataStore store; readonly EmailThrottlingState throttlingState; - public SendEmailNotificationHandler(IDocumentStore store, Settings settings, EmailThrottlingState throttlingState) + public SendEmailNotificationHandler(IErrorMessageDataStore store, Settings settings, EmailThrottlingState throttlingState) { this.store = store; this.throttlingState = throttlingState; @@ -25,14 +25,9 @@ public async Task Handle(SendEmailNotification message, IMessageHandlerContext c { NotificationsSettings notifications; - using (var session = store.OpenAsyncSession()) + using (var manager = await store.CreateNotificationsManager()) { - using (session.Advanced.DocumentStore.AggressivelyCacheFor(cacheTimeout)) - { - notifications = await session - .LoadAsync(NotificationsSettings.SingleDocumentId) - .ConfigureAwait(false); - } + notifications = await manager.LoadSettings(cacheTimeout); } if (notifications == null || !notifications.Email.Enabled) @@ -55,7 +50,7 @@ public async Task Handle(SendEmailNotification message, IMessageHandlerContext c return; } - hasSemaphore = await throttlingState.Semaphore.WaitAsync(spinDelay, cancellationToken).ConfigureAwait(false); + hasSemaphore = await throttlingState.Semaphore.WaitAsync(spinDelay, cancellationToken); } if (context.MessageId == throttlingState.RetriedMessageId) @@ -64,8 +59,7 @@ public async Task Handle(SendEmailNotification message, IMessageHandlerContext c "\n\nWARNING: Your SMTP server was temporarily unavailable. Make sure to check ServicePulse for a full list of health check notifications."; } - await EmailSender.Send(notifications.Email, message.Subject, message.Body, emailDropFolder) - .ConfigureAwait(false); + await EmailSender.Send(notifications.Email, message.Subject, message.Body, emailDropFolder); } catch (Exception e) when (!(e is OperationCanceledException)) { @@ -73,7 +67,7 @@ await EmailSender.Send(notifications.Email, message.Subject, message.Body, email { throttlingState.ThrottlingOn(); - await Task.Delay(throttlingDelay, cancellationToken).ConfigureAwait(false); + await Task.Delay(throttlingDelay, cancellationToken); throttlingState.ThrottlingOff(); diff --git a/src/ServiceControl/Operations/BodyStorage/BodyStorageEnricher.cs b/src/ServiceControl/Operations/BodyStorage/BodyStorageEnricher.cs index cfd47cefa7..1bef32efe0 100644 --- a/src/ServiceControl/Operations/BodyStorage/BodyStorageEnricher.cs +++ b/src/ServiceControl/Operations/BodyStorage/BodyStorageEnricher.cs @@ -29,8 +29,7 @@ public async ValueTask StoreErrorMessageBody(byte[] body, ProcessingAttempt proc var contentType = GetContentType(processingAttempt.Headers, "text/xml"); processingAttempt.MessageMetadata.Add("ContentType", contentType); - await StoreBody(body, processingAttempt, bodySize, contentType) - .ConfigureAwait(false); + await StoreBody(body, processingAttempt, bodySize, contentType); } static string GetContentType(IReadOnlyDictionary headers, string defaultContentType) @@ -77,8 +76,7 @@ async ValueTask StoreBody(byte[] body, ProcessingAttempt processingAttempt, int if (useBodyStore) { - await StoreBodyInBodyStorage(body, bodyId, contentType, bodySize) - .ConfigureAwait(false); + await StoreBodyInBodyStorage(body, bodyId, contentType, bodySize); } processingAttempt.MessageMetadata.Add("BodyUrl", bodyUrl); @@ -88,8 +86,7 @@ async Task StoreBodyInBodyStorage(byte[] body, string bodyId, string contentType { using (var bodyStream = Memory.Manager.GetStream(bodyId, body, 0, bodySize)) { - await bodyStorage.Store(bodyId, contentType, bodySize, bodyStream) - .ConfigureAwait(false); + await bodyStorage.Store(bodyId, contentType, bodySize, bodyStream); } } diff --git a/src/ServiceControl/Operations/BodyStorage/RavenAttachments/GetBodyByIdApi.cs b/src/ServiceControl/Operations/BodyStorage/RavenAttachments/GetBodyByIdApi.cs index d1d2e1f5fe..9cac0f659e 100644 --- a/src/ServiceControl/Operations/BodyStorage/RavenAttachments/GetBodyByIdApi.cs +++ b/src/ServiceControl/Operations/BodyStorage/RavenAttachments/GetBodyByIdApi.cs @@ -6,15 +6,12 @@ using System.Net.Http.Headers; using System.Threading.Tasks; using CompositeViews.Messages; - using Raven.Client; using ServiceBus.Management.Infrastructure.Settings; - using ServiceControl.Persistence; class GetBodyByIdApi : RoutedApi { - public GetBodyByIdApi(IDocumentStore documentStore, IBodyStorage bodyStorage, Settings settings, Func httpClientFactory) + public GetBodyByIdApi(IBodyStorage bodyStorage, Settings settings, Func httpClientFactory) { - this.documentStore = documentStore; this.bodyStorage = bodyStorage; Settings = settings; HttpClientFactory = httpClientFactory; @@ -23,78 +20,29 @@ public GetBodyByIdApi(IDocumentStore documentStore, IBodyStorage bodyStorage, Se protected override async Task LocalQuery(HttpRequestMessage request, string input, string instanceId) { var messageId = input; - messageId = messageId?.Replace("/", @"\"); - var indexResponse = await TryFetchFromIndex(request, messageId).ConfigureAwait(false); - // when fetching result from index is successful go ahead - if (indexResponse.IsSuccessStatusCode) - { - return indexResponse; - } - - // try to fetch from body - var bodyStorageResponse = await TryFetchFromStorage(request, messageId).ConfigureAwait(false); - // if found return, if not the result from the index takes precedence to be backward compatible - return bodyStorageResponse.IsSuccessStatusCode ? bodyStorageResponse : indexResponse; - } - async Task TryFetchFromStorage(HttpRequestMessage request, string messageId) - { - var result = await bodyStorage.TryFetch(messageId).ConfigureAwait(false); + var result = await bodyStorage.TryFetch(messageId); - if (result.HasResult) + if (result == null) { - var response = request.CreateResponse(HttpStatusCode.OK); - var content = new StreamContent(result.Stream); - MediaTypeHeaderValue.TryParse(result.ContentType, out var parsedContentType); - content.Headers.ContentType = parsedContentType ?? new MediaTypeHeaderValue("text/*"); - content.Headers.ContentLength = result.BodySize; - response.Headers.ETag = new EntityTagHeaderValue($"\"{result.Etag}\""); - response.Content = content; - return response; + return request.CreateResponse(HttpStatusCode.NotFound); } - return request.CreateResponse(HttpStatusCode.NotFound); - } - - async Task TryFetchFromIndex(HttpRequestMessage request, string messageId) - { - using (var session = documentStore.OpenAsyncSession()) + if (!result.HasResult) { - var message = await session.Query() - .Statistics(out var stats) - .TransformWith() - .FirstOrDefaultAsync(f => f.MessageId == messageId) - .ConfigureAwait(false); - - if (message == null) - { - return request.CreateResponse(HttpStatusCode.NotFound); - } - - if (message.BodyNotStored && message.Body == null) - { - return request.CreateResponse(HttpStatusCode.NoContent); - } - - if (message.Body == null) - { - return request.CreateResponse(HttpStatusCode.NotFound); - } - - var response = request.CreateResponse(HttpStatusCode.OK); - var content = new StringContent(message.Body); - - MediaTypeHeaderValue.TryParse(message.ContentType, out var parsedContentType); - content.Headers.ContentType = parsedContentType ?? new MediaTypeHeaderValue("text/*"); - - content.Headers.ContentLength = message.BodySize; - response.Headers.ETag = new EntityTagHeaderValue($"\"{stats.IndexEtag}\""); - response.Content = content; - return response; + return request.CreateResponse(HttpStatusCode.NoContent); } + + var response = request.CreateResponse(HttpStatusCode.OK); + var content = new StreamContent(result.Stream); + MediaTypeHeaderValue.TryParse(result.ContentType, out var parsedContentType); + content.Headers.ContentType = parsedContentType ?? new MediaTypeHeaderValue("text/*"); + content.Headers.ContentLength = result.BodySize; + response.Headers.ETag = new EntityTagHeaderValue($"\"{result.Etag}\""); + response.Content = content; + return response; } readonly IBodyStorage bodyStorage; - readonly IDocumentStore documentStore; } } diff --git a/src/ServiceControl/Operations/BodyStorage/RavenAttachments/RavenAttachmentsBodyStorage.cs b/src/ServiceControl/Operations/BodyStorage/RavenAttachments/RavenAttachmentsBodyStorage.cs deleted file mode 100644 index ff2a0fde4b..0000000000 --- a/src/ServiceControl/Operations/BodyStorage/RavenAttachments/RavenAttachmentsBodyStorage.cs +++ /dev/null @@ -1,53 +0,0 @@ -namespace ServiceControl.Operations.BodyStorage.RavenAttachments -{ - using System.IO; - using System.Threading.Tasks; - using Raven.Client; - using Raven.Json.Linq; - - class RavenAttachmentsBodyStorage : IBodyStorage - { - public RavenAttachmentsBodyStorage(IDocumentStore documentStore) - { - this.documentStore = documentStore; - } - - IDocumentStore documentStore; - - public Task Store(string bodyId, string contentType, int bodySize, Stream bodyStream) - { - //We want to continue using attachments for now -#pragma warning disable 618 - return documentStore.AsyncDatabaseCommands.PutAttachmentAsync($"messagebodies/{bodyId}", null, bodyStream, - new RavenJObject -#pragma warning restore 618 - { - {"ContentType", contentType}, - {"ContentLength", bodySize} - }); - } - - public async Task TryFetch(string bodyId) - { - //We want to continue using attachments for now -#pragma warning disable 618 - var attachment = await documentStore.AsyncDatabaseCommands.GetAttachmentAsync($"messagebodies/{bodyId}").ConfigureAwait(false); -#pragma warning restore 618 - - return attachment == null - ? new StreamResult - { - HasResult = false, - Stream = null - } - : new StreamResult - { - HasResult = true, - Stream = attachment.Data(), - ContentType = attachment.Metadata["ContentType"].Value(), - BodySize = attachment.Metadata["ContentLength"].Value(), - Etag = attachment.Etag - }; - } - } -} \ No newline at end of file diff --git a/src/ServiceControl/Operations/CheckRemotes.cs b/src/ServiceControl/Operations/CheckRemotes.cs index 6bbe1d155e..6ce9603b9f 100644 --- a/src/ServiceControl/Operations/CheckRemotes.cs +++ b/src/ServiceControl/Operations/CheckRemotes.cs @@ -34,7 +34,7 @@ public override async Task PerformCheck() try { - await Task.WhenAll(remoteQueryTasks).ConfigureAwait(false); + await Task.WhenAll(remoteQueryTasks); return CheckResult.Pass; } catch (Exception) @@ -45,7 +45,7 @@ public override async Task PerformCheck() { try { - await task.ConfigureAwait(false); + await task; } catch (TimeoutException e) { @@ -67,7 +67,7 @@ static async Task CheckSuccessStatusCode(HttpClient client, RemoteInstanceSettin { try { - var response = await client.GetAsync(remoteSettings.ApiUri, cancellationToken).ConfigureAwait(false); + var response = await client.GetAsync(remoteSettings.ApiUri, cancellationToken); response.EnsureSuccessStatusCode(); remoteSettings.TemporarilyUnavailable = false; } diff --git a/src/ServiceControl/Operations/ErrorIngestion.cs b/src/ServiceControl/Operations/ErrorIngestion.cs index 62fc68ed6c..5b32b281c5 100644 --- a/src/ServiceControl/Operations/ErrorIngestion.cs +++ b/src/ServiceControl/Operations/ErrorIngestion.cs @@ -8,12 +8,11 @@ using System.Threading.Tasks; using Infrastructure.Metrics; using Microsoft.Extensions.Hosting; - using NServiceBus; using NServiceBus.Logging; using NServiceBus.Transport; - using Raven.Client; using ServiceBus.Management.Infrastructure.Settings; using ServiceControl.Infrastructure; + using ServiceControl.Persistence; using ServiceControl.Persistence.UnitOfWork; using ServiceControl.Transports; @@ -27,7 +26,7 @@ public ErrorIngestion( TransportCustomization transportCustomization, TransportSettings transportSettings, Metrics metrics, - IDocumentStore documentStore, + IErrorMessageDataStore dataStore, LoggingSettings loggingSettings, ErrorIngestionCustomCheck.State ingestionState, ErrorIngestor ingestor, @@ -54,7 +53,7 @@ public ErrorIngestion( FullMode = BoundedChannelFullMode.Wait }); - errorHandlingPolicy = new ErrorIngestionFaultPolicy(documentStore, loggingSettings, OnCriticalError); + errorHandlingPolicy = new ErrorIngestionFaultPolicy(dataStore, loggingSettings, OnCriticalError); watchdog = new Watchdog("failed message ingestion", EnsureStarted, EnsureStopped, ingestionState.ReportError, ingestionState.Clear, settings.TimeToRestartErrorIngestionAfterFailure, log); @@ -65,16 +64,16 @@ public ErrorIngestion( public async Task StopAsync(CancellationToken _) { - await watchdog.Stop().ConfigureAwait(false); + await watchdog.Stop(); channel.Writer.Complete(); - await ingestionWorker.ConfigureAwait(false); + await ingestionWorker; } async Task Loop() { var contexts = new List(settings.MaximumConcurrencyLevel); - while (await channel.Reader.WaitToReadAsync().ConfigureAwait(false)) + while (await channel.Reader.WaitToReadAsync()) { // will only enter here if there is something to read. try @@ -88,7 +87,7 @@ async Task Loop() batchSizeMeter.Mark(contexts.Count); using (batchDurationMeter.Measure()) { - await ingestor.Ingest(contexts, dispatcher).ConfigureAwait(false); + await ingestor.Ingest(contexts, dispatcher); } } catch (OperationCanceledException) @@ -121,7 +120,7 @@ async Task EnsureStarted(CancellationToken cancellationToken = default) { try { - await startStopSemaphore.WaitAsync(cancellationToken).ConfigureAwait(false); + await startStopSemaphore.WaitAsync(cancellationToken); if (!unitOfWorkFactory.CanIngestMore()) { @@ -129,7 +128,7 @@ async Task EnsureStarted(CancellationToken cancellationToken = default) { var stoppable = queueIngestor; queueIngestor = null; - await stoppable.Stop().ConfigureAwait(false); + await stoppable.Stop(); logger.Info("Shutting down due to failed persistence health check. Infrastructure shut down completed"); } return; @@ -145,17 +144,16 @@ async Task EnsureStarted(CancellationToken cancellationToken = default) transportSettings, OnMessage, errorHandlingPolicy.OnError, - OnCriticalError).ConfigureAwait(false); + OnCriticalError); - dispatcher = await transportCustomization.InitializeDispatcher(errorQueue, transportSettings).ConfigureAwait(false); + dispatcher = await transportCustomization.InitializeDispatcher(errorQueue, transportSettings); if (settings.ForwardErrorMessages) { - await ingestor.VerifyCanReachForwardingAddress(dispatcher).ConfigureAwait(false); + await ingestor.VerifyCanReachForwardingAddress(dispatcher); } - await queueIngestor.Start() - .ConfigureAwait(false); + await queueIngestor.Start(); logger.Info("Ensure started. Infrastructure started"); } @@ -163,7 +161,7 @@ await queueIngestor.Start() { if (queueIngestor != null) { - await queueIngestor.Stop().ConfigureAwait(false); + await queueIngestor.Stop(); } queueIngestor = null; // Setting to null so that it doesn't exit when it retries in line 134 @@ -188,8 +186,8 @@ async Task OnMessage(MessageContext messageContext) receivedMeter.Mark(); - await channel.Writer.WriteAsync(messageContext).ConfigureAwait(false); - await taskCompletionSource.Task.ConfigureAwait(false); + await channel.Writer.WriteAsync(messageContext); + await taskCompletionSource.Task; } Task OnCriticalError(string failure, Exception exception) @@ -202,7 +200,7 @@ async Task EnsureStopped(CancellationToken cancellationToken = default) { try { - await startStopSemaphore.WaitAsync(cancellationToken).ConfigureAwait(false); + await startStopSemaphore.WaitAsync(cancellationToken); if (queueIngestor == null) { @@ -210,7 +208,7 @@ async Task EnsureStopped(CancellationToken cancellationToken = default) } var stoppable = queueIngestor; queueIngestor = null; - await stoppable.Stop().ConfigureAwait(false); + await stoppable.Stop(); } finally { diff --git a/src/ServiceControl/Operations/ErrorIngestionFaultPolicy.cs b/src/ServiceControl/Operations/ErrorIngestionFaultPolicy.cs index 773b953617..42b3d89ac9 100644 --- a/src/ServiceControl/Operations/ErrorIngestionFaultPolicy.cs +++ b/src/ServiceControl/Operations/ErrorIngestionFaultPolicy.cs @@ -5,21 +5,21 @@ using System.IO; using System.Threading.Tasks; using NServiceBus.Transport; - using Raven.Client; using ServiceBus.Management.Infrastructure.Installers; using ServiceBus.Management.Infrastructure.Settings; + using ServiceControl.Persistence; class ErrorIngestionFaultPolicy { - IDocumentStore store; + IErrorMessageDataStore store; string logPath; ImportFailureCircuitBreaker failureCircuitBreaker; - public ErrorIngestionFaultPolicy(IDocumentStore store, LoggingSettings loggingSettings, Func onCriticalError) + public ErrorIngestionFaultPolicy(IErrorMessageDataStore store, LoggingSettings loggingSettings, Func onCriticalError) { this.store = store; - logPath = Path.Combine(loggingSettings.LogPath, @"FailedImports\Error"); + logPath = Path.Combine(loggingSettings.LogPath, "FailedImports", "Error"); failureCircuitBreaker = new ImportFailureCircuitBreaker(onCriticalError); @@ -34,8 +34,7 @@ public async Task OnError(ErrorContext errorContext) return ErrorHandleResult.RetryRequired; } - await Handle(errorContext) - .ConfigureAwait(false); + await Handle(errorContext); return ErrorHandleResult.Handled; } @@ -58,8 +57,7 @@ async Task Handle(Exception exception, FailedErrorImport failure) { try { - await DoLogging(exception, failure) - .ConfigureAwait(false); + await DoLogging(exception, failure); } finally { @@ -69,19 +67,10 @@ await DoLogging(exception, failure) async Task DoLogging(Exception exception, FailedErrorImport failure) { - var id = Guid.NewGuid(); + failure.Id = Guid.NewGuid(); - // Write to Raven - using (var session = store.OpenAsyncSession()) - { - failure.Id = id; - - await session.StoreAsync(failure) - .ConfigureAwait(false); - - await session.SaveChangesAsync() - .ConfigureAwait(false); - } + // Write to data store + await store.StoreFailedErrorImport(failure); // Write to Log Path var filePath = Path.Combine(logPath, failure.Id + ".txt"); diff --git a/src/ServiceControl/Operations/ErrorIngestor.cs b/src/ServiceControl/Operations/ErrorIngestor.cs index c7baf87317..8222e081f4 100644 --- a/src/ServiceControl/Operations/ErrorIngestor.cs +++ b/src/ServiceControl/Operations/ErrorIngestor.cs @@ -65,8 +65,7 @@ public async Task Ingest(List contexts, IDispatchMessages dispat } - var storedFailed = await PersistFailedMessages(failedMessages, retriedMessages) - .ConfigureAwait(false); + var storedFailed = await PersistFailedMessages(failedMessages, retriedMessages); try { @@ -80,7 +79,7 @@ public async Task Ingest(List contexts, IDispatchMessages dispat announcerTasks.Add(retryConfirmationProcessor.Announce(context)); } - await Task.WhenAll(announcerTasks).ConfigureAwait(false); + await Task.WhenAll(announcerTasks); if (settings.ForwardErrorMessages) { @@ -88,7 +87,7 @@ public async Task Ingest(List contexts, IDispatchMessages dispat { log.Debug($"Forwarding {contexts.Count} messages"); } - await Forward(storedFailed, dispatcher).ConfigureAwait(false); + await Forward(storedFailed, dispatcher); if (log.IsDebugEnabled) { log.Debug("Forwarded messages"); @@ -123,17 +122,14 @@ async Task> PersistFailedMessages(List> Process(List co tasks.Add(ProcessMessage(context, unitOfWork)); } - await Task.WhenAll(tasks).ConfigureAwait(false); + await Task.WhenAll(tasks); var knownEndpoints = new Dictionary(); foreach (var context in contexts) @@ -63,7 +63,7 @@ public async Task> Process(List co Logger.Debug($"Adding known endpoint '{endpoint.EndpointDetails.Name}' for bulk storage"); } - await unitOfWork.Monitoring.RecordKnownEndpoint(endpoint).ConfigureAwait(false); + await unitOfWork.Monitoring.RecordKnownEndpoint(endpoint); } return storedContexts; @@ -106,37 +106,25 @@ async Task ProcessMessage(MessageContext context, IIngestionUnitOfWork unitOfWor if (Logger.IsDebugEnabled) { - Logger.Debug($"Ingesting error message {context.MessageId} (original message id: {(isOriginalMessageId ? messageId : string.Empty)})"); + Logger.DebugFormat("Ingesting error message {0} (original message id: {1})", context.MessageId, isOriginalMessageId ? messageId : string.Empty); } try { - var metadata = new Dictionary - { - ["MessageId"] = messageId, - ["MessageIntent"] = context.Headers.MessageIntent() - }; - - var enricherContext = new ErrorEnricherContext(context.Headers, metadata); - foreach (var enricher in enrichers) - { - enricher.Enrich(enricherContext); - } + var (metadata, enricherContext) = ExecuteEnrichRoutinesAndCreateMetaData(context, messageId); var failureDetails = failedMessageFactory.ParseFailureDetails(context.Headers); var processingAttempt = failedMessageFactory.CreateProcessingAttempt( context.Headers, - new Dictionary(metadata), + new Dictionary(metadata), // TODO: metadata is already a dictionary, it this really needed? failureDetails); - await bodyStorageEnricher.StoreErrorMessageBody(context.Body, processingAttempt) - .ConfigureAwait(false); + await bodyStorageEnricher.StoreErrorMessageBody(context.Body, processingAttempt); var groups = failedMessageFactory.GetGroups((string)metadata["MessageType"], failureDetails, processingAttempt); - await unitOfWork.Recoverability.RecordFailedProcessingAttempt(context.Headers.UniqueId(), processingAttempt, groups) - .ConfigureAwait(false); + await unitOfWork.Recoverability.RecordFailedProcessingAttempt(context.Headers.UniqueId(), processingAttempt, groups); context.Extensions.Set(failureDetails); context.Extensions.Set(enricherContext.NewEndpoints); @@ -145,13 +133,29 @@ await unitOfWork.Recoverability.RecordFailedProcessingAttempt(context.Headers.Un { if (Logger.IsWarnEnabled) { - Logger.Warn($"Processing of message '{context.MessageId}' failed.", e); + Logger.WarnFormat("Processing of message '{0}' failed.\n{1}", context.MessageId, e); } context.GetTaskCompletionSource().TrySetException(e); } } + (Dictionary metadata, ErrorEnricherContext enricherContext) ExecuteEnrichRoutinesAndCreateMetaData(MessageContext context, string messageId) + { + var metadata = new Dictionary + { + ["MessageId"] = messageId, + ["MessageIntent"] = context.Headers.MessageIntent() + }; + var enricherContext = new ErrorEnricherContext(context.Headers, metadata); + foreach (var enricher in enrichers) + { + enricher.Enrich(enricherContext); + } + + return (metadata, enricherContext); + } + static void RecordKnownEndpoints(EndpointDetails observedEndpoint, Dictionary observedEndpoints) { var uniqueEndpointId = $"{observedEndpoint.Name}{observedEndpoint.HostId}"; @@ -170,8 +174,8 @@ static void RecordKnownEndpoints(EndpointDetails observedEndpoint, Dictionary(); } } \ No newline at end of file diff --git a/src/ServiceControl/Operations/ImportFailedErrors.cs b/src/ServiceControl/Operations/ImportFailedErrors.cs index 88b432a671..61c854147a 100644 --- a/src/ServiceControl/Operations/ImportFailedErrors.cs +++ b/src/ServiceControl/Operations/ImportFailedErrors.cs @@ -1,21 +1,18 @@ namespace ServiceControl.Operations { - using System; using System.Collections.Generic; using System.Threading; using System.Threading.Tasks; using NServiceBus.Extensibility; - using NServiceBus.Logging; - using NServiceBus.Raw; using NServiceBus.Transport; - using Raven.Client; + using Persistence; using ServiceBus.Management.Infrastructure.Settings; using ServiceControl.Transports; class ImportFailedErrors { public ImportFailedErrors( - IDocumentStore store, + IFailedErrorImportDataStore store, ErrorIngestor errorIngestor, Settings settings, TransportCustomization transportCustomization, @@ -30,65 +27,27 @@ public ImportFailedErrors( public async Task Run(CancellationToken cancellationToken = default) { - var dispatcher = await transportCustomization.InitializeDispatcher("ImportFailedErrors", transportSettings).ConfigureAwait(false); + var dispatcher = await transportCustomization.InitializeDispatcher("ImportFailedErrors", transportSettings); if (settings.ForwardErrorMessages) { - await errorIngestor.VerifyCanReachForwardingAddress(dispatcher).ConfigureAwait(false); + await errorIngestor.VerifyCanReachForwardingAddress(dispatcher); } - var succeeded = 0; - var failed = 0; - using (var session = store.OpenAsyncSession()) + await store.ProcessFailedErrorImports(async transportMessage => { - var query = session.Query(); - using (var stream = await session.Advanced.StreamAsync(query, cancellationToken) - .ConfigureAwait(false)) - { - while (!cancellationToken.IsCancellationRequested && await stream.MoveNextAsync().ConfigureAwait(false)) - { - var transportMessage = stream.Current.Document.Message; - try - { - var messageContext = new MessageContext(transportMessage.Id, transportMessage.Headers, transportMessage.Body, EmptyTransaction, EmptyTokenSource, EmptyContextBag); - var taskCompletionSource = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); - messageContext.SetTaskCompletionSource(taskCompletionSource); - - await errorIngestor.Ingest(new List { messageContext }, dispatcher).ConfigureAwait(false); - - await taskCompletionSource.Task.ConfigureAwait(false); - - await store.AsyncDatabaseCommands.DeleteAsync(stream.Current.Key, null, cancellationToken) - .ConfigureAwait(false); - succeeded++; - - if (Logger.IsDebugEnabled) - { - Logger.Debug($"Successfully re-imported failed error message {transportMessage.Id}."); - } - } - catch (OperationCanceledException) - { - // no-op - } - catch (Exception e) - { - Logger.Error($"Error while attempting to re-import failed error message {transportMessage.Id}.", e); - failed++; - } - } - } - } - - Logger.Info($"Done re-importing failed errors. Successfully re-imported {succeeded} messages. Failed re-importing {failed} messages."); - - if (failed > 0) - { - Logger.Warn($"{failed} messages could not be re-imported. This could indicate a problem with the data. Contact Particular support if you need help with recovering the messages."); - } + var messageContext = new MessageContext(transportMessage.Id, transportMessage.Headers, + transportMessage.Body, EmptyTransaction, EmptyTokenSource, EmptyContextBag); + var taskCompletionSource = + new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); + messageContext.SetTaskCompletionSource(taskCompletionSource); + + await errorIngestor.Ingest(new List { messageContext }, dispatcher); + await taskCompletionSource.Task; + }, cancellationToken); } - readonly IDocumentStore store; + readonly IFailedErrorImportDataStore store; readonly ErrorIngestor errorIngestor; readonly Settings settings; readonly TransportCustomization transportCustomization; @@ -96,6 +55,5 @@ await store.AsyncDatabaseCommands.DeleteAsync(stream.Current.Key, null, cancella static readonly TransportTransaction EmptyTransaction = new TransportTransaction(); static readonly CancellationTokenSource EmptyTokenSource = new CancellationTokenSource(); static readonly ContextBag EmptyContextBag = new ContextBag(); - static readonly ILog Logger = LogManager.GetLogger(typeof(ImportFailedErrors)); } } \ No newline at end of file diff --git a/src/ServiceControl/Operations/RetryConfirmationProcessor.cs b/src/ServiceControl/Operations/RetryConfirmationProcessor.cs index 079f98972c..43243faff8 100644 --- a/src/ServiceControl/Operations/RetryConfirmationProcessor.cs +++ b/src/ServiceControl/Operations/RetryConfirmationProcessor.cs @@ -22,8 +22,7 @@ public async Task Process(List contexts, IIngestionUnitOfWork un foreach (var context in contexts) { var retriedMessageUniqueId = context.Headers[RetryUniqueMessageIdHeader]; - await unitOfWork.Recoverability.RecordSuccessfulRetry(retriedMessageUniqueId) - .ConfigureAwait(false); + await unitOfWork.Recoverability.RecordSuccessfulRetry(retriedMessageUniqueId); } } diff --git a/src/ServiceControl/Persistence/PersistenceConfigurationFactory.cs b/src/ServiceControl/Persistence/PersistenceConfigurationFactory.cs new file mode 100644 index 0000000000..ef50d5995a --- /dev/null +++ b/src/ServiceControl/Persistence/PersistenceConfigurationFactory.cs @@ -0,0 +1,55 @@ +namespace ServiceControl.Persistence +{ + using System; + using ServiceBus.Management.Infrastructure.Settings; + + // Added recently by David + + static class PersistenceConfigurationFactory + { + public static IPersistenceConfiguration LoadPersistenceConfiguration(string persistenceType) + { + try + { + var foundPersistenceType = PersistenceManifestLibrary.Find(persistenceType); + + var customizationType = Type.GetType(foundPersistenceType, true); + + return (IPersistenceConfiguration)Activator.CreateInstance(customizationType); + } + catch (Exception e) + { + throw new Exception($"Could not load persistence customization type {persistenceType}.", e); + } + } + + public static PersistenceSettings BuildPersistenceSettings(this IPersistenceConfiguration persistenceConfiguration, Settings settings, bool maintenanceMode = false) + { + // TODO: Audit instance passed settings.AuditRetentionPeriod, settings.EnableFullTextSearchOnBodies, settings.MaxBodySizeToStore - are those needed? + // And then remove settings parameter if not needed (but it probably is for something) + var persistenceSettings = new PersistenceSettings( + settings.ErrorRetentionPeriod, + settings.EventsRetentionPeriod, + settings.AuditRetentionPeriod, + settings.ExternalIntegrationsDispatchingBatchSize, + maintenanceMode + ); + + foreach (var keyPair in settings.PersisterSpecificSettings) + { + persistenceSettings.PersisterSpecificSettings[keyPair.Key] = keyPair.Value; + } + + foreach (var key in persistenceConfiguration.ConfigurationKeys) + { + var value = SettingsReader.Read("ServiceControl", key, null); + if (!string.IsNullOrWhiteSpace(value)) + { + persistenceSettings.PersisterSpecificSettings[key] = value; + } + } + + return persistenceSettings; + } + } +} \ No newline at end of file diff --git a/src/ServiceControl/Persistence/PersistenceHostBuilderExtensions.cs b/src/ServiceControl/Persistence/PersistenceHostBuilderExtensions.cs index b0f7a4b994..0c8942d57a 100644 --- a/src/ServiceControl/Persistence/PersistenceHostBuilderExtensions.cs +++ b/src/ServiceControl/Persistence/PersistenceHostBuilderExtensions.cs @@ -2,30 +2,27 @@ namespace ServiceControl.Persistence { using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Hosting; - using Raven.Client; - using Raven.Client.Embedded; - using ServiceBus.Management.Infrastructure.Settings; - using ServiceControl.CustomChecks; - using ServiceControl.Infrastructure.RavenDB; static class PersistenceHostBuilderExtensions { - public static IHostBuilder SetupPersistence(this IHostBuilder hostBuilder, Settings settings) + public static IHostBuilder SetupPersistence(this IHostBuilder hostBuilder, PersistenceSettings persistenceSettings, IPersistenceConfiguration persistenceConfiguration) { - var documentStore = new EmbeddableDocumentStore(); - RavenBootstrapper.Configure(documentStore, settings); + var persistence = persistenceConfiguration.Create(persistenceSettings); hostBuilder.ConfigureServices(serviceCollection => { - serviceCollection.AddSingleton(documentStore); - serviceCollection.AddHostedService(); - serviceCollection.AddCustomCheck(); - serviceCollection.AddCustomCheck(); - - serviceCollection.AddServiceControlPersistence(settings.DataStoreType); + CreatePersisterLifecyle(serviceCollection, persistence); }); return hostBuilder; } + + public static void CreatePersisterLifecyle(IServiceCollection serviceCollection, IPersistence persistence) + { + var lifecycle = persistence.CreateLifecycle(); + // lifecycle needs to be started before any other hosted service + serviceCollection.AddHostedService(_ => new PersistenceLifecycleHostedService(lifecycle)); + persistence.Configure(serviceCollection); + } } } \ No newline at end of file diff --git a/src/ServiceControl/Persistence/PersistenceLifecycleHostedService.cs b/src/ServiceControl/Persistence/PersistenceLifecycleHostedService.cs new file mode 100644 index 0000000000..7e92cececc --- /dev/null +++ b/src/ServiceControl/Persistence/PersistenceLifecycleHostedService.cs @@ -0,0 +1,20 @@ +namespace ServiceControl.Persistence +{ + using System.Threading; + using System.Threading.Tasks; + using Microsoft.Extensions.Hosting; + + class PersistenceLifecycleHostedService : IHostedService + { + public PersistenceLifecycleHostedService(IPersistenceLifecycle lifecycle) + { + this.lifecycle = lifecycle; + } + + public Task StartAsync(CancellationToken cancellationToken) => lifecycle.Start(cancellationToken); + + public Task StopAsync(CancellationToken cancellationToken) => lifecycle.Stop(cancellationToken); + + readonly IPersistenceLifecycle lifecycle; + } +} diff --git a/src/ServiceControl/Persistence/PersistenceServiceCollectionExtensions.cs b/src/ServiceControl/Persistence/PersistenceServiceCollectionExtensions.cs deleted file mode 100644 index 3598c6a2b6..0000000000 --- a/src/ServiceControl/Persistence/PersistenceServiceCollectionExtensions.cs +++ /dev/null @@ -1,40 +0,0 @@ -namespace ServiceControl.Persistence -{ - using System; - using Microsoft.Extensions.DependencyInjection; - using ServiceBus.Management.Infrastructure.Settings; - - static class PersistenceServiceCollectionExtensions - { - public static void AddServiceControlPersistence(this IServiceCollection serviceCollection, DataStoreType dataStoreType) - { - try - { - var persistenceCreationInfo = GetPersistenceCreationInfo(dataStoreType); - var persistenceConfig = (IPersistenceConfiguration)Activator.CreateInstance(persistenceCreationInfo); - persistenceConfig.ConfigureServices(serviceCollection); - - } - catch (Exception e) - { - throw new Exception($"Could not load persistence customization for {dataStoreType}.", e); - } - } - - static Type GetPersistenceCreationInfo(DataStoreType dataStoreType) - { - switch (dataStoreType) - { - case DataStoreType.InMemory: - return Type.GetType(DataStoreConfig.InMemoryPersistenceTypeFullyQualifiedName, true); - case DataStoreType.RavenDB35: - return Type.GetType(DataStoreConfig.RavenDB35PersistenceTypeFullyQualifiedName, true); - case DataStoreType.SqlDb: - return Type.GetType(DataStoreConfig.SqlServerPersistenceTypeFullyQualifiedName, true); - default: - return default; - } - } - - } -} \ No newline at end of file diff --git a/src/ServiceControl/Program.cs b/src/ServiceControl/Program.cs index 532f600a6f..842f26ae66 100644 --- a/src/ServiceControl/Program.cs +++ b/src/ServiceControl/Program.cs @@ -5,6 +5,7 @@ using System.Reflection; using System.Threading.Tasks; using Commands; + using global::ServiceControl.Persistence; using global::ServiceControl.Transports; using Hosting; using NServiceBus.Logging; @@ -32,8 +33,7 @@ static async Task Main(string[] args) settings = new Settings(arguments.ServiceName); - await new CommandRunner(arguments.Commands).Execute(arguments, settings) - .ConfigureAwait(false); + await new CommandRunner(arguments.Commands).Execute(arguments, settings); } static void LogException(Exception ex) { @@ -57,6 +57,13 @@ static Assembly ResolveAssembly(string name) var subFolderPath = Path.Combine(appDirectory, "Transports", transportFolder); assembly = TryLoadTypeFromSubdirectory(subFolderPath, requestingName); } + + var persistenceFolder = PersistenceManifestLibrary.GetPersistenceFolder(settings.PersistenceType); + if (assembly == null && persistenceFolder != null) + { + var subFolderPath = Path.Combine(appDirectory, "Persisters", persistenceFolder); + assembly = TryLoadTypeFromSubdirectory(subFolderPath, requestingName); + } } return assembly; diff --git a/src/ServiceControl/Recoverability/API/FailureGroupsArchiveController.cs b/src/ServiceControl/Recoverability/API/FailureGroupsArchiveController.cs index 266fa7dfd1..2ebcc39fd1 100644 --- a/src/ServiceControl/Recoverability/API/FailureGroupsArchiveController.cs +++ b/src/ServiceControl/Recoverability/API/FailureGroupsArchiveController.cs @@ -5,13 +5,14 @@ using System.Threading.Tasks; using System.Web.Http; using NServiceBus; + using ServiceControl.Persistence.Recoverability; class FailureGroupsArchiveController : ApiController { - public FailureGroupsArchiveController(IMessageSession bus, ArchivingManager archivingManager) + public FailureGroupsArchiveController(IMessageSession bus, IArchiveMessages archiver) { this.bus = bus; - this.archivingManager = archivingManager; + this.archiver = archiver; } @@ -24,18 +25,17 @@ public async Task ArchiveGroupErrors(string groupId) return Request.CreateErrorResponse(HttpStatusCode.BadRequest, "missing groupId"); } - if (!archivingManager.IsOperationInProgressFor(groupId, ArchiveType.FailureGroup)) + if (!archiver.IsOperationInProgressFor(groupId, ArchiveType.FailureGroup)) { - await archivingManager.StartArchiving(groupId, ArchiveType.FailureGroup) - .ConfigureAwait(false); + await archiver.StartArchiving(groupId, ArchiveType.FailureGroup); - await bus.SendLocal(m => { m.GroupId = groupId; }).ConfigureAwait(false); + await bus.SendLocal(m => { m.GroupId = groupId; }); } return Request.CreateResponse(HttpStatusCode.Accepted); } readonly IMessageSession bus; - readonly ArchivingManager archivingManager; + readonly IArchiveMessages archiver; } } \ No newline at end of file diff --git a/src/ServiceControl/Recoverability/API/FailureGroupsController.cs b/src/ServiceControl/Recoverability/API/FailureGroupsController.cs index 2365d003b4..5eb8a99ea1 100644 --- a/src/ServiceControl/Recoverability/API/FailureGroupsController.cs +++ b/src/ServiceControl/Recoverability/API/FailureGroupsController.cs @@ -1,5 +1,6 @@ namespace ServiceControl.Recoverability.API { + using System; using System.Collections.Generic; using System.Linq; using System.Net; @@ -7,11 +8,9 @@ using System.Threading.Tasks; using System.Web.Http; using Infrastructure.WebApi; - using MessageFailures; - using MessageFailures.Api; using MessageFailures.InternalMessages; using NServiceBus; - using Raven.Client; + using Persistence.Infrastructure; using ServiceControl.Persistence; class FailureGroupsController : ApiController @@ -19,14 +18,16 @@ class FailureGroupsController : ApiController public FailureGroupsController( IEnumerable classifiers, IMessageSession bus, - IDocumentStore store, - GroupFetcher groupFetcher + GroupFetcher groupFetcher, + IErrorMessageDataStore dataStore, + IRetryHistoryDataStore retryStore ) { this.classifiers = classifiers; this.bus = bus; - this.store = store; this.groupFetcher = groupFetcher; + this.dataStore = dataStore; + this.retryStore = retryStore; } [Route("recoverability/classifiers")] @@ -42,6 +43,7 @@ public HttpResponseMessage GetSupportedClassifiers() .WithTotalCount(result.Length); } + [Obsolete("Only used by legacy RavenDB35 storage engine")] // TODO: how to deal with this domain event [Route("recoverability/groups/reclassify")] [HttpPost] public async Task ReclassifyErrors() @@ -49,7 +51,7 @@ public async Task ReclassifyErrors() await bus.SendLocal(new ReclassifyErrors { Force = true - }).ConfigureAwait(false); + }); return Content(HttpStatusCode.Accepted, string.Empty); } @@ -58,18 +60,7 @@ await bus.SendLocal(new ReclassifyErrors [HttpPost] public async Task EditComment(string groupId, string comment) { - using (var session = store.OpenAsyncSession()) - { - var groupComment = - await session.LoadAsync(GroupComment.MakeId(groupId)).ConfigureAwait(false) - ?? new GroupComment { Id = GroupComment.MakeId(groupId) }; - - groupComment.Comment = comment; - - await session.StoreAsync(groupComment).ConfigureAwait(false); - - await session.SaveChangesAsync().ConfigureAwait(false); - } + await dataStore.EditComment(groupId, comment); return Content(HttpStatusCode.Accepted, string.Empty); } @@ -78,56 +69,41 @@ await session.LoadAsync(GroupComment.MakeId(groupId)).ConfigureAwa [HttpDelete] public async Task DeleteComment(string groupId) { - using (var session = store.OpenAsyncSession()) - { - session.Delete(GroupComment.MakeId(groupId)); - await session.SaveChangesAsync().ConfigureAwait(false); - } + await dataStore.DeleteComment(groupId); return Content(HttpStatusCode.Accepted, string.Empty); } [Route("recoverability/groups/{classifier?}")] [HttpGet] - public async Task GetAllGroups([FromUri] string classifierFilter = null, string classifier = "Exception Type and Stack Trace") + public async Task GetAllGroups(string classifier = "Exception Type and Stack Trace") { + string classifierFilter = Request.GetClassifierFilter(); + if (classifierFilter == "undefined") { classifierFilter = null; } - using (var session = store.OpenAsyncSession()) - { - var results = await groupFetcher.GetGroups(session, classifier, classifierFilter).ConfigureAwait(false); + var results = await groupFetcher.GetGroups(classifier, classifierFilter); // TODO: Analyze what to do with the GroupFetcher dependency - return Negotiator.FromModel(Request, results) - .WithDeterministicEtag(EtagHelper.CalculateEtag(results)); - } + return Negotiator.FromModel(Request, results) + .WithDeterministicEtag(EtagHelper.CalculateEtag(results)); } [Route("recoverability/groups/{groupId}/errors")] [HttpGet] public async Task GetGroupErrors(string groupId) { - using (var session = store.OpenAsyncSession()) - { - var results = await session.Advanced - .AsyncDocumentQuery() - .Statistics(out var stats) - .WhereEquals(view => view.FailureGroupId, groupId) - .FilterByStatusWhere(Request) - .FilterByLastModifiedRange(Request) - .Sort(Request) - .Paging(Request) - .SetResultTransformer(FailedMessageViewTransformer.Name) - .SelectFields() - .ToListAsync() - .ConfigureAwait(false); - - return Negotiator.FromModel(Request, results) - .WithPagingLinksAndTotalCount(stats.TotalResults, Request) - .WithEtag(stats); - } + string status = Request.GetStatus(); + string modified = Request.GetModified(); + + var sortInfo = Request.GetSortInfo(); + var pagingInfo = Request.GetPagingInfo(); + + var results = await dataStore.GetGroupErrors(groupId, status, modified, sortInfo, pagingInfo); + + return Negotiator.FromQueryResult(Request, results); } @@ -135,63 +111,44 @@ public async Task GetGroupErrors(string groupId) [HttpHead] public async Task GetGroupErrorsCount(string groupId) { - using (var session = store.OpenAsyncSession()) - { - var queryResult = await session.Advanced - .AsyncDocumentQuery() - .WhereEquals(view => view.FailureGroupId, groupId) - .FilterByStatusWhere(Request) - .FilterByLastModifiedRange(Request) - .QueryResultAsync() - .ConfigureAwait(false); - - var response = Request.CreateResponse(HttpStatusCode.OK); - - return response - .WithTotalCount(queryResult.TotalResults) - .WithEtag(queryResult.IndexEtag); - } + string status = Request.GetStatus(); + string modified = Request.GetModified(); + + var results = await dataStore.GetGroupErrorsCount(groupId, status, modified); + + return Negotiator.FromQueryStatsInfo(Request, results); } [Route("recoverability/history")] [HttpGet] public async Task GetRetryHistory() { - using (var session = store.OpenAsyncSession()) - { - var retryHistory = await session.LoadAsync(RetryHistory.MakeId()).ConfigureAwait(false) - ?? RetryHistory.CreateNew(); + var retryHistory = await retryStore.GetRetryHistory(); - return Negotiator - .FromModel(Request, retryHistory) - .WithDeterministicEtag(retryHistory.GetHistoryOperationsUniqueIdentifier()); - } + return Negotiator + .FromModel(Request, retryHistory) + .WithDeterministicEtag(retryHistory.GetHistoryOperationsUniqueIdentifier()); } [Route("recoverability/groups/id/{groupId}")] [HttpGet] public async Task GetGroup(string groupId) { - using (var session = store.OpenAsyncSession()) - { - var queryResult = await session.Advanced - .AsyncDocumentQuery() - .Statistics(out var stats) - .WhereEquals(group => group.Id, groupId) - .FilterByStatusWhere(Request) - .FilterByLastModifiedRange(Request) - .ToListAsync() - .ConfigureAwait(false); - - return Negotiator - .FromModel(Request, queryResult.FirstOrDefault()) - .WithEtag(stats); - } + string status = Request.GetStatus(); + string modified = Request.GetModified(); + + // TODO: Migrated as previous behavior but can be optimized as http api will return at most 1 item + var result = await dataStore.GetGroup(groupId, status, modified); + + return Negotiator + .FromModel(Request, result.Results.FirstOrDefault()) + .WithEtag(result.QueryStats.ETag); } readonly IEnumerable classifiers; readonly IMessageSession bus; - readonly IDocumentStore store; readonly GroupFetcher groupFetcher; + readonly IErrorMessageDataStore dataStore; + readonly IRetryHistoryDataStore retryStore; } } \ No newline at end of file diff --git a/src/ServiceControl/Recoverability/API/FailureGroupsRetryController.cs b/src/ServiceControl/Recoverability/API/FailureGroupsRetryController.cs index 5e022bedc8..c45a9cf8db 100644 --- a/src/ServiceControl/Recoverability/API/FailureGroupsRetryController.cs +++ b/src/ServiceControl/Recoverability/API/FailureGroupsRetryController.cs @@ -30,14 +30,13 @@ public async Task ArchiveGroupErrors(string groupId) if (!retryingManager.IsOperationInProgressFor(groupId, RetryType.FailureGroup)) { - await retryingManager.Wait(groupId, RetryType.FailureGroup, started) - .ConfigureAwait(false); + await retryingManager.Wait(groupId, RetryType.FailureGroup, started); await bus.SendLocal(new RetryAllInGroup { GroupId = groupId, Started = started - }).ConfigureAwait(false); + }); } return Request.CreateResponse(HttpStatusCode.Accepted); diff --git a/src/ServiceControl/Recoverability/API/FailureGroupsUnarchiveController.cs b/src/ServiceControl/Recoverability/API/FailureGroupsUnarchiveController.cs index b7e914e66e..af6cf0bae4 100644 --- a/src/ServiceControl/Recoverability/API/FailureGroupsUnarchiveController.cs +++ b/src/ServiceControl/Recoverability/API/FailureGroupsUnarchiveController.cs @@ -5,13 +5,14 @@ using System.Threading.Tasks; using System.Web.Http; using NServiceBus; + using ServiceControl.Persistence.Recoverability; class FailureGroupsUnarchiveController : ApiController { - public FailureGroupsUnarchiveController(IMessageSession bus, UnarchivingManager unarchivingManager) + public FailureGroupsUnarchiveController(IMessageSession bus, IArchiveMessages archiver) { this.bus = bus; - this.unarchivingManager = unarchivingManager; + this.archiver = archiver; } @@ -24,18 +25,17 @@ public async Task UnarchiveGroupErrors(string groupId) return Request.CreateErrorResponse(HttpStatusCode.BadRequest, "missing groupId"); } - if (!unarchivingManager.IsOperationInProgressFor(groupId, ArchiveType.FailureGroup)) + if (!archiver.IsOperationInProgressFor(groupId, ArchiveType.FailureGroup)) { - await unarchivingManager.StartUnarchiving(groupId, ArchiveType.FailureGroup) - .ConfigureAwait(false); + await archiver.StartUnarchiving(groupId, ArchiveType.FailureGroup); - await bus.SendLocal(m => { m.GroupId = groupId; }).ConfigureAwait(false); + await bus.SendLocal(m => { m.GroupId = groupId; }); } return Request.CreateResponse(HttpStatusCode.Accepted); } readonly IMessageSession bus; - readonly UnarchivingManager unarchivingManager; + readonly IArchiveMessages archiver; } } \ No newline at end of file diff --git a/src/ServiceControl/Recoverability/API/GroupFetcher.cs b/src/ServiceControl/Recoverability/API/GroupFetcher.cs index cc7ab1a56f..29354690c8 100644 --- a/src/ServiceControl/Recoverability/API/GroupFetcher.cs +++ b/src/ServiceControl/Recoverability/API/GroupFetcher.cs @@ -2,59 +2,44 @@ { using System.Collections.Generic; using System.Linq; - using System.Threading; using System.Threading.Tasks; - using MessageFailures; - using Raven.Client; - using Raven.Client.Linq; using ServiceControl.Persistence; + using ServiceControl.Persistence.Recoverability; class GroupFetcher { - public GroupFetcher(RetryingManager retryingManager, ArchivingManager archivingManager) + public GroupFetcher(IGroupsDataStore store, IRetryHistoryDataStore retryStore, RetryingManager retryingManager, IArchiveMessages archiver) { + this.store = store; + this.retryStore = retryStore; this.retryingManager = retryingManager; - this.archivingManager = archivingManager; + this.archiver = archiver; } - public async Task GetGroups(IAsyncDocumentSession session, string classifier, string classifierFilter) + public async Task GetGroups(string classifier, string classifierFilter) { - var dbGroups = await GetDBGroups(session, classifier, classifierFilter).ConfigureAwait(false); - - await GetComments(session, dbGroups).ConfigureAwait(false); - - var retryHistory = await session.LoadAsync(RetryHistory.MakeId()).ConfigureAwait(false) ?? RetryHistory.CreateNew(); + var dbGroups = await store.GetFailureGroupsByClassifier(classifier, classifierFilter); + var retryHistory = await retryStore.GetRetryHistory(); var unacknowledgedRetries = retryHistory.GetUnacknowledgedByClassifier(classifier); var openRetryAcknowledgements = MapAcksToOpenGroups(dbGroups, unacknowledgedRetries); var closedRetryAcknowledgements = unacknowledgedRetries.Except(openRetryAcknowledgements).ToArray(); var closedGroups = MapClosedGroups(classifier, closedRetryAcknowledgements); - closedGroups = closedGroups.Union(MapClosedGroups(classifier, archivingManager.GetArchivalOperations().Where(archiveOp => archiveOp.NeedsAcknowledgement()))); + closedGroups = closedGroups.Union(MapClosedGroups(classifier, archiver.GetArchivalOperations().Where(archiveOp => archiveOp.NeedsAcknowledgement()))); var openGroups = MapOpenGroups(dbGroups, retryHistory, openRetryAcknowledgements).ToList(); - openGroups = MapOpenGroups(openGroups, archivingManager.GetArchivalOperations()).ToList(); + openGroups = MapOpenGroups(openGroups, archiver.GetArchivalOperations()).ToList(); openGroups = openGroups.Where(group => !closedGroups.Any(closedGroup => closedGroup.Id == group.Id)).ToList(); - MakeSureForwardingBatchIsIncludedAsOpen(classifier, await GetCurrentForwardingBatch(session).ConfigureAwait(false), openGroups); + var currentForwardingBatch = await store.GetCurrentForwardingBatch(); + MakeSureForwardingBatchIsIncludedAsOpen(classifier, currentForwardingBatch, openGroups); var groups = openGroups.Union(closedGroups); return groups.OrderByDescending(g => g.Last).ToArray(); } - async Task GetComments(IAsyncDocumentSession session, IList dbGroups) - { - var commentIds = dbGroups.Select(x => GroupComment.MakeId(x.Id)).ToArray(); - var comments = await session.Query().Where(x => x.Id.In(commentIds)) - .ToListAsync(CancellationToken.None).ConfigureAwait(false); - - foreach (var group in dbGroups) - { - group.Comment = comments.FirstOrDefault(x => x.Id == GroupComment.MakeId(group.Id))?.Comment; - } - } - void MakeSureForwardingBatchIsIncludedAsOpen(string classifier, RetryBatch forwardingBatch, List open) { if (forwardingBatch == null || forwardingBatch.Classifier != classifier) @@ -78,20 +63,6 @@ join unack in acks on g.Id equals unack.RequestId select unack).ToArray(); } - static Task> GetDBGroups(IAsyncDocumentSession session, string classifier, string classifierFilter) - { - var groups = Queryable.Where(session.Query(), v => v.Type == classifier); - - if (!string.IsNullOrWhiteSpace(classifierFilter)) - { - groups = groups.Where(v => v.Title == classifierFilter); - } - - return groups.OrderByDescending(x => x.Last) - .Take(200) - .ToListAsync(); - } - static bool IsCurrentForwardingOperationIncluded(List open, RetryBatch forwardingBatch) { return open.Any(x => x.Id == forwardingBatch.RequestId && x.Type == forwardingBatch.Classifier && forwardingBatch.RetryType == RetryType.FailureGroup); @@ -117,15 +88,6 @@ static GroupOperation MapOpenForForwardingOperation(string classifier, RetryBatc }; } - static async Task GetCurrentForwardingBatch(IAsyncDocumentSession session) - { - var nowForwarding = await session.Include(r => r.RetryBatchId) - .LoadAsync(RetryBatchNowForwarding.Id) - .ConfigureAwait(false); - - return nowForwarding == null ? null : await session.LoadAsync(nowForwarding.RetryBatchId).ConfigureAwait(false); - } - static IEnumerable MapClosedGroups(string classifier, UnacknowledgedRetryOperation[] standaloneUnacknowledgements) { return standaloneUnacknowledgements.Select(standalone => @@ -226,7 +188,9 @@ static HistoricRetryOperation GetLatestHistoricOperation(RetryHistory history, s .FirstOrDefault(); } + readonly IGroupsDataStore store; + readonly IRetryHistoryDataStore retryStore; readonly RetryingManager retryingManager; - readonly ArchivingManager archivingManager; + readonly IArchiveMessages archiver; } } \ No newline at end of file diff --git a/src/ServiceControl/Recoverability/API/UnacknowledgedGroupsController.cs b/src/ServiceControl/Recoverability/API/UnacknowledgedGroupsController.cs index 7ec6b9113e..dc01ebdf2e 100644 --- a/src/ServiceControl/Recoverability/API/UnacknowledgedGroupsController.cs +++ b/src/ServiceControl/Recoverability/API/UnacknowledgedGroupsController.cs @@ -4,46 +4,39 @@ using System.Net.Http; using System.Threading.Tasks; using System.Web.Http; - using Raven.Client; using ServiceControl.Persistence; + using ServiceControl.Persistence.Recoverability; class UnacknowledgedGroupsController : ApiController { - public UnacknowledgedGroupsController(ArchivingManager archivingManager, IDocumentStore store) + public UnacknowledgedGroupsController(IRetryHistoryDataStore retryStore, IArchiveMessages archiver) { - this.archivingManager = archivingManager; - this.store = store; + this.retryStore = retryStore; + this.archiver = archiver; } [Route("recoverability/unacknowledgedgroups/{groupId}")] [HttpDelete] public async Task AcknowledgeOperation(string groupId) { - if (archivingManager.IsArchiveInProgressFor(groupId)) + if (archiver.IsArchiveInProgressFor(groupId)) { - archivingManager.DismissArchiveOperation(groupId, ArchiveType.FailureGroup); + archiver.DismissArchiveOperation(groupId, ArchiveType.FailureGroup); return Request.CreateResponse(HttpStatusCode.OK); } - using (var session = store.OpenAsyncSession()) - { - var retryHistory = await session.LoadAsync(RetryHistory.MakeId()).ConfigureAwait(false); - if (retryHistory != null) - { - if (retryHistory.Acknowledge(groupId, RetryType.FailureGroup)) - { - await session.StoreAsync(retryHistory).ConfigureAwait(false); - await session.SaveChangesAsync().ConfigureAwait(false); + var success = await retryStore.AcknowledgeRetryGroup(groupId); - return Request.CreateResponse(HttpStatusCode.OK); - } - } + if (success) + { + return Request.CreateResponse(HttpStatusCode.OK); } + return Request.CreateResponse(HttpStatusCode.NotFound); } - readonly ArchivingManager archivingManager; - readonly IDocumentStore store; + readonly IRetryHistoryDataStore retryStore; + readonly IArchiveMessages archiver; } } \ No newline at end of file diff --git a/src/ServiceControl/Recoverability/Archiving/ArchiveAllInGroupHandler.cs b/src/ServiceControl/Recoverability/Archiving/ArchiveAllInGroupHandler.cs index 361887ac5d..c695e220c1 100644 --- a/src/ServiceControl/Recoverability/Archiving/ArchiveAllInGroupHandler.cs +++ b/src/ServiceControl/Recoverability/Archiving/ArchiveAllInGroupHandler.cs @@ -1,141 +1,32 @@ namespace ServiceControl.Recoverability { - using System; - using System.Linq; using System.Threading.Tasks; - using Infrastructure.DomainEvents; using NServiceBus; using NServiceBus.Logging; - using Raven.Client; + using ServiceControl.Persistence.Recoverability; class ArchiveAllInGroupHandler : IHandleMessages { - public ArchiveAllInGroupHandler(IDocumentStore store, IDomainEvents domainEvents, ArchiveDocumentManager documentManager, ArchivingManager archiveOperationManager, RetryingManager retryingManager) + public ArchiveAllInGroupHandler(IArchiveMessages archiver, RetryingManager retryingManager) { - this.store = store; - this.documentManager = documentManager; - this.archiveOperationManager = archiveOperationManager; + this.archiver = archiver; this.retryingManager = retryingManager; - this.domainEvents = domainEvents; } public async Task Handle(ArchiveAllInGroup message, IMessageHandlerContext context) { if (retryingManager.IsRetryInProgressFor(message.GroupId)) { - logger.Warn($"Attempt to archive a group ({message.GroupId}) which is currently in the process of being retried"); + Log.Warn($"Attempt to archive a group ({message.GroupId}) which is currently in the process of being retried"); return; } - logger.Info($"Archiving of {message.GroupId} started"); - ArchiveOperation archiveOperation; - - using (var session = store.OpenAsyncSession()) - { - session.Advanced.UseOptimisticConcurrency = true; // Ensure 2 messages don't split the same operation into batches at once - - archiveOperation = await documentManager.LoadArchiveOperation(session, message.GroupId, ArchiveType.FailureGroup) - .ConfigureAwait(false); - - if (archiveOperation == null) - { - var groupDetails = await documentManager.GetGroupDetails(session, message.GroupId).ConfigureAwait(false); - if (groupDetails.NumberOfMessagesInGroup == 0) - { - logger.Warn($"No messages to archive in group {message.GroupId}"); - - return; - } - - logger.Info($"Splitting group {message.GroupId} into batches"); - archiveOperation = await documentManager.CreateArchiveOperation(session, message.GroupId, ArchiveType.FailureGroup, groupDetails.NumberOfMessagesInGroup, groupDetails.GroupName, batchSize) - .ConfigureAwait(false); - await session.SaveChangesAsync() - .ConfigureAwait(false); - - logger.Info($"Group {message.GroupId} has been split into {archiveOperation.NumberOfBatches} batches"); - } - } - - await archiveOperationManager.StartArchiving(archiveOperation) - .ConfigureAwait(false); - - while (archiveOperation.CurrentBatch < archiveOperation.NumberOfBatches) - { - using (var batchSession = store.OpenAsyncSession()) - { - var nextBatch = await documentManager.GetArchiveBatch(batchSession, archiveOperation.Id, archiveOperation.CurrentBatch) - .ConfigureAwait(false); - if (nextBatch == null) - { - // We're only here in the case where Raven indexes are stale - logger.Warn($"Attempting to archive a batch ({archiveOperation.Id}/{archiveOperation.CurrentBatch}) which appears to already have been archived."); - } - else - { - logger.Info($"Archiving {nextBatch.DocumentIds.Count} messages from group {message.GroupId} starting"); - } - - await documentManager.ArchiveMessageGroupBatch(batchSession, nextBatch) - .ConfigureAwait(false); - - await archiveOperationManager.BatchArchived(archiveOperation.RequestId, archiveOperation.ArchiveType, nextBatch?.DocumentIds.Count ?? 0) - .ConfigureAwait(false); - - archiveOperation = archiveOperationManager.GetStatusForArchiveOperation(archiveOperation.RequestId, archiveOperation.ArchiveType).ToArchiveOperation(); - - await documentManager.UpdateArchiveOperation(batchSession, archiveOperation) - .ConfigureAwait(false); - - await batchSession.SaveChangesAsync() - .ConfigureAwait(false); - - if (nextBatch != null) - { - await domainEvents.Raise(new FailedMessageGroupBatchArchived - { - // Remove `FailedMessages/` prefix and publish pure GUIDs without Raven collection name - FailedMessagesIds = nextBatch.DocumentIds.Select(id => id.Replace("FailedMessages/", "")).ToArray() - }).ConfigureAwait(false); - } - - if (nextBatch != null) - { - logger.Info($"Archiving of {nextBatch.DocumentIds.Count} messages from group {message.GroupId} completed"); - } - } - } - - logger.Info($"Archiving of group {message.GroupId} is complete. Waiting for index updates."); - await archiveOperationManager.ArchiveOperationFinalizing(archiveOperation.RequestId, archiveOperation.ArchiveType) - .ConfigureAwait(false); - if (!await documentManager.WaitForIndexUpdateOfArchiveOperation(store, archiveOperation.RequestId, TimeSpan.FromMinutes(5)) - .ConfigureAwait(false)) - { - logger.Warn($"Archiving group {message.GroupId} completed but index not updated."); - } - - await archiveOperationManager.ArchiveOperationCompleted(archiveOperation.RequestId, archiveOperation.ArchiveType) - .ConfigureAwait(false); - await documentManager.RemoveArchiveOperation(store, archiveOperation).ConfigureAwait(false); - - await domainEvents.Raise(new FailedMessageGroupArchived - { - GroupId = message.GroupId, - GroupName = archiveOperation.GroupName, - MessagesCount = archiveOperation.TotalNumberOfMessages, - }).ConfigureAwait(false); - - logger.Info($"Archiving of group {message.GroupId} completed"); + await archiver.ArchiveAllInGroup(message.GroupId); } - IDocumentStore store; - IDomainEvents domainEvents; - ArchiveDocumentManager documentManager; - ArchivingManager archiveOperationManager; - RetryingManager retryingManager; - const int batchSize = 1000; + readonly IArchiveMessages archiver; + readonly RetryingManager retryingManager; - static ILog logger = LogManager.GetLogger(); + static ILog Log = LogManager.GetLogger(); } } diff --git a/src/ServiceControl/Recoverability/Archiving/UnArchiveAllInGroupHandler.cs b/src/ServiceControl/Recoverability/Archiving/UnArchiveAllInGroupHandler.cs index 909e98d5cf..44eeebd731 100644 --- a/src/ServiceControl/Recoverability/Archiving/UnArchiveAllInGroupHandler.cs +++ b/src/ServiceControl/Recoverability/Archiving/UnArchiveAllInGroupHandler.cs @@ -1,22 +1,16 @@ namespace ServiceControl.Recoverability { - using System; - using System.Linq; using System.Threading.Tasks; - using Infrastructure.DomainEvents; using NServiceBus; using NServiceBus.Logging; - using Raven.Client; + using ServiceControl.Persistence.Recoverability; class UnarchiveAllInGroupHandler : IHandleMessages { - public UnarchiveAllInGroupHandler(IDocumentStore store, IDomainEvents domainEvents, UnarchiveDocumentManager documentManager, UnarchivingManager unarchiveOperationManager, RetryingManager retryingManager) + public UnarchiveAllInGroupHandler(IArchiveMessages archiver, RetryingManager retryingManager) { - this.store = store; - this.documentManager = documentManager; - this.unarchiveOperationManager = unarchiveOperationManager; + this.archiver = archiver; this.retryingManager = retryingManager; - this.domainEvents = domainEvents; } public async Task Handle(UnarchiveAllInGroup message, IMessageHandlerContext context) @@ -27,114 +21,12 @@ public async Task Handle(UnarchiveAllInGroup message, IMessageHandlerContext con return; } - logger.Info($"Unarchiving of {message.GroupId} started"); - UnarchiveOperation unarchiveOperation; - - using (var session = store.OpenAsyncSession()) - { - session.Advanced.UseOptimisticConcurrency = true; // Ensure 2 messages don't split the same operation into batches at once - - unarchiveOperation = await documentManager.LoadUnarchiveOperation(session, message.GroupId, ArchiveType.FailureGroup) - .ConfigureAwait(false); - - if (unarchiveOperation == null) - { - var groupDetails = await documentManager.GetGroupDetails(session, message.GroupId).ConfigureAwait(false); - if (groupDetails.NumberOfMessagesInGroup == 0) - { - logger.Warn($"No messages to unarchive in group {message.GroupId}"); - - return; - } - - logger.Info($"Splitting group {message.GroupId} into batches"); - unarchiveOperation = await documentManager.CreateUnarchiveOperation(session, message.GroupId, ArchiveType.FailureGroup, groupDetails.NumberOfMessagesInGroup, groupDetails.GroupName, batchSize) - .ConfigureAwait(false); - await session.SaveChangesAsync() - .ConfigureAwait(false); - - logger.Info($"Group {message.GroupId} has been split into {unarchiveOperation.NumberOfBatches} batches"); - } - } - - await unarchiveOperationManager.StartUnarchiving(unarchiveOperation) - .ConfigureAwait(false); - - while (unarchiveOperation.CurrentBatch < unarchiveOperation.NumberOfBatches) - { - using (var batchSession = store.OpenAsyncSession()) - { - var nextBatch = await documentManager.GetUnarchiveBatch(batchSession, unarchiveOperation.Id, unarchiveOperation.CurrentBatch) - .ConfigureAwait(false); - if (nextBatch == null) - { - // We're only here in the case where Raven indexes are stale - logger.Warn($"Attempting to unarchive a batch ({unarchiveOperation.Id}/{unarchiveOperation.CurrentBatch}) which appears to already have been archived."); - } - else - { - logger.Info($"Unarchiving {nextBatch.DocumentIds.Count} messages from group {message.GroupId} starting"); - } - - await documentManager.UnarchiveMessageGroupBatch(batchSession, nextBatch) - .ConfigureAwait(false); - - await unarchiveOperationManager.BatchUnarchived(unarchiveOperation.RequestId, unarchiveOperation.ArchiveType, nextBatch?.DocumentIds.Count ?? 0) - .ConfigureAwait(false); - - unarchiveOperation = unarchiveOperationManager.GetStatusForUnarchiveOperation(unarchiveOperation.RequestId, unarchiveOperation.ArchiveType).ToUnarchiveOperation(); - - await documentManager.UpdateUnarchiveOperation(batchSession, unarchiveOperation) - .ConfigureAwait(false); - - await batchSession.SaveChangesAsync() - .ConfigureAwait(false); - - if (nextBatch != null) - { - await domainEvents.Raise(new FailedMessageGroupBatchUnarchived - { - // Remove `FailedMessages/` prefix and publish pure GUIDs without Raven collection name - FailedMessagesIds = nextBatch.DocumentIds.Select(id => id.Replace("FailedMessages/", "")).ToArray() - }).ConfigureAwait(false); - } - - if (nextBatch != null) - { - logger.Info($"Unarchiving of {nextBatch.DocumentIds.Count} messages from group {message.GroupId} completed"); - } - } - } - - logger.Info($"Unarchiving of group {message.GroupId} is complete. Waiting for index updates."); - await unarchiveOperationManager.UnarchiveOperationFinalizing(unarchiveOperation.RequestId, unarchiveOperation.ArchiveType) - .ConfigureAwait(false); - if (!await documentManager.WaitForIndexUpdateOfUnarchiveOperation(store, unarchiveOperation.RequestId, TimeSpan.FromMinutes(5)) - .ConfigureAwait(false)) - { - logger.Warn($"Unarchiving group {message.GroupId} completed but index not updated."); - } - - logger.Info($"Unarchiving of group {message.GroupId} completed"); - await unarchiveOperationManager.UnarchiveOperationCompleted(unarchiveOperation.RequestId, unarchiveOperation.ArchiveType) - .ConfigureAwait(false); - await documentManager.RemoveUnarchiveOperation(store, unarchiveOperation).ConfigureAwait(false); - - await domainEvents.Raise(new FailedMessageGroupUnarchived - { - GroupId = message.GroupId, - GroupName = unarchiveOperation.GroupName, - MessagesCount = unarchiveOperation.TotalNumberOfMessages, - }).ConfigureAwait(false); + await archiver.UnarchiveAllInGroup(message.GroupId); } - IDocumentStore store; - IDomainEvents domainEvents; - UnarchiveDocumentManager documentManager; - UnarchivingManager unarchiveOperationManager; + IArchiveMessages archiver; RetryingManager retryingManager; - const int batchSize = 1000; static ILog logger = LogManager.GetLogger(); } -} +} \ No newline at end of file diff --git a/src/ServiceControl/Recoverability/Editing/EditHandler.cs b/src/ServiceControl/Recoverability/Editing/EditHandler.cs index bd8745169b..1e725bfc0e 100644 --- a/src/ServiceControl/Recoverability/Editing/EditHandler.cs +++ b/src/ServiceControl/Recoverability/Editing/EditHandler.cs @@ -4,21 +4,21 @@ using System.Linq; using System.Threading.Tasks; using MessageFailures; - using MessageRedirects; using NServiceBus; using NServiceBus.Extensibility; using NServiceBus.Logging; using NServiceBus.Routing; using NServiceBus.Support; using NServiceBus.Transport; - using Raven.Abstractions.Data; - using Raven.Client; + using ServiceControl.Persistence; + using ServiceControl.Persistence.MessageRedirects; class EditHandler : IHandleMessages { - public EditHandler(IDocumentStore store, IDispatchMessages dispatcher) + public EditHandler(IErrorMessageDataStore store, IMessageRedirectsDataStore redirectsStore, IDispatchMessages dispatcher) { this.store = store; + this.redirectsStore = redirectsStore; this.dispatcher = dispatcher; corruptedReplyToHeaderStrategy = new CorruptedReplyToHeaderStrategy(RuntimeEnvironment.MachineName); } @@ -26,11 +26,9 @@ public EditHandler(IDocumentStore store, IDispatchMessages dispatcher) public async Task Handle(EditAndSend message, IMessageHandlerContext context) { FailedMessage failedMessage; - MessageRedirectsCollection redirects; - using (var session = store.OpenAsyncSession()) + using (var session = await store.CreateEditFailedMessageManager()) { - failedMessage = await session.LoadAsync(FailedMessage.MakeDocumentId(message.FailedMessageId)) - .ConfigureAwait(false); + failedMessage = await session.GetFailedMessage(message.FailedMessageId); if (failedMessage == null) { @@ -38,9 +36,8 @@ public async Task Handle(EditAndSend message, IMessageHandlerContext context) return; } - var edit = await session.LoadAsync(FailedMessageEdit.MakeDocumentId(message.FailedMessageId)) - .ConfigureAwait(false); - if (edit == null) + var editId = await session.GetCurrentEditingMessageId(message.FailedMessageId); + if (editId == null) { if (failedMessage.Status != FailedMessageStatus.Unresolved) { @@ -49,36 +46,30 @@ public async Task Handle(EditAndSend message, IMessageHandlerContext context) } // create a retries document to prevent concurrent edits - await session.StoreAsync(new FailedMessageEdit - { - Id = FailedMessageEdit.MakeDocumentId(message.FailedMessageId), - FailedMessageId = message.FailedMessageId, - EditId = context.MessageId - }, Etag.Empty).ConfigureAwait(false); + await session.SetCurrentEditingMessageId(context.MessageId); } - else if (edit.EditId != context.MessageId) + else if (editId != context.MessageId) { - log.WarnFormat($"Discarding edit & retry request because the failure ({FailedMessage.MakeDocumentId(message.FailedMessageId)}) has already been edited by {FailedMessageEdit.MakeDocumentId(message.FailedMessageId)}"); + log.WarnFormat($"Discarding edit & retry request because the failed message id {message.FailedMessageId} has already been edited by Message ID {editId}"); return; } // the original failure is marked as resolved as any failures of the edited message are treated as a new message failure. - failedMessage.Status = FailedMessageStatus.Resolved; + await session.SetFailedMessageAsResolved(); - redirects = await MessageRedirectsCollection.GetOrCreate(session) - .ConfigureAwait(false); - await session.SaveChangesAsync().ConfigureAwait(false); + await session.SaveChanges(); } + var redirects = await redirectsStore.GetOrCreate(); + var attempt = failedMessage.ProcessingAttempts.Last(); var outgoingMessage = BuildMessage(message); // mark the new message with a link to the original message id outgoingMessage.Headers.Add("ServiceControl.EditOf", message.FailedMessageId); var address = ApplyRedirect(attempt.FailureDetails.AddressOfFailingEndpoint, redirects); - await DispatchEditedMessage(outgoingMessage, address, context) - .ConfigureAwait(false); + await DispatchEditedMessage(outgoingMessage, address, context); } OutgoingMessage BuildMessage(EditAndSend message) @@ -115,9 +106,10 @@ Task DispatchEditedMessage(OutgoingMessage editedMessage, string address, IMessa new ContextBag()); } - CorruptedReplyToHeaderStrategy corruptedReplyToHeaderStrategy; - IDocumentStore store; - IDispatchMessages dispatcher; - static ILog log = LogManager.GetLogger(); + readonly CorruptedReplyToHeaderStrategy corruptedReplyToHeaderStrategy; + readonly IErrorMessageDataStore store; + readonly IMessageRedirectsDataStore redirectsStore; + readonly IDispatchMessages dispatcher; + static readonly ILog log = LogManager.GetLogger(); } } \ No newline at end of file diff --git a/src/ServiceControl/Recoverability/ExternalIntegration/FailedMessageArchivedPublisher.cs b/src/ServiceControl/Recoverability/ExternalIntegration/FailedMessageArchivedPublisher.cs index 8737b69fec..de0bc0eb8c 100644 --- a/src/ServiceControl/Recoverability/ExternalIntegration/FailedMessageArchivedPublisher.cs +++ b/src/ServiceControl/Recoverability/ExternalIntegration/FailedMessageArchivedPublisher.cs @@ -5,7 +5,6 @@ namespace ServiceControl.Recoverability.ExternalIntegration using System.Threading.Tasks; using Contracts.MessageFailures; using ExternalIntegrations; - using Raven.Client; class FailedMessageArchivedPublisher : EventPublisher { @@ -17,7 +16,7 @@ protected override DispatchContext CreateDispatchRequest(FailedMessageArchived @ }; } - protected override Task> PublishEvents(IEnumerable contexts, IAsyncDocumentSession session) + protected override Task> PublishEvents(IEnumerable contexts) { return Task.FromResult(contexts.Select(r => (object)new Contracts.FailedMessagesArchived { diff --git a/src/ServiceControl/Recoverability/ExternalIntegration/FailedMessageGroupArchivedPublisher.cs b/src/ServiceControl/Recoverability/ExternalIntegration/FailedMessageGroupArchivedPublisher.cs index 12675b9ef9..51d3eb10ac 100644 --- a/src/ServiceControl/Recoverability/ExternalIntegration/FailedMessageGroupArchivedPublisher.cs +++ b/src/ServiceControl/Recoverability/ExternalIntegration/FailedMessageGroupArchivedPublisher.cs @@ -4,7 +4,6 @@ namespace ServiceControl.Recoverability.ExternalIntegration using System.Linq; using System.Threading.Tasks; using ExternalIntegrations; - using Raven.Client; using Recoverability; class FailedMessageGroupBatchArchivedPublisher : EventPublisher @@ -17,7 +16,7 @@ protected override DispatchContext CreateDispatchRequest(FailedMessageGroupBatch }; } - protected override Task> PublishEvents(IEnumerable contexts, IAsyncDocumentSession session) + protected override Task> PublishEvents(IEnumerable contexts) { return Task.FromResult(contexts.Select(r => (object)new Contracts.FailedMessagesArchived { diff --git a/src/ServiceControl/Recoverability/ExternalIntegration/FailedMessageGroupBatchUnarchivedPublisher.cs b/src/ServiceControl/Recoverability/ExternalIntegration/FailedMessageGroupBatchUnarchivedPublisher.cs index a240f123fb..1a94a8ef53 100644 --- a/src/ServiceControl/Recoverability/ExternalIntegration/FailedMessageGroupBatchUnarchivedPublisher.cs +++ b/src/ServiceControl/Recoverability/ExternalIntegration/FailedMessageGroupBatchUnarchivedPublisher.cs @@ -4,7 +4,6 @@ namespace ServiceControl.Recoverability.ExternalIntegration using System.Linq; using System.Threading.Tasks; using ExternalIntegrations; - using Raven.Client; using Recoverability; class FailedMessageGroupBatchUnarchivedPublisher : EventPublisher @@ -17,7 +16,7 @@ protected override DispatchContext CreateDispatchRequest(FailedMessageGroupBatch }; } - protected override Task> PublishEvents(IEnumerable contexts, IAsyncDocumentSession session) + protected override Task> PublishEvents(IEnumerable contexts) { return Task.FromResult(contexts.Select(r => (object)new Contracts.FailedMessagesUnArchived() { diff --git a/src/ServiceControl/Recoverability/ExternalIntegration/FailedMessagesUnarchivedPublisher.cs b/src/ServiceControl/Recoverability/ExternalIntegration/FailedMessagesUnarchivedPublisher.cs index 092cf739fc..e2520d64b2 100644 --- a/src/ServiceControl/Recoverability/ExternalIntegration/FailedMessagesUnarchivedPublisher.cs +++ b/src/ServiceControl/Recoverability/ExternalIntegration/FailedMessagesUnarchivedPublisher.cs @@ -5,7 +5,6 @@ namespace ServiceControl.Recoverability.ExternalIntegration using System.Threading.Tasks; using Contracts.MessageFailures; using ExternalIntegrations; - using Raven.Client; class FailedMessagesUnarchivedPublisher : EventPublisher { @@ -18,7 +17,7 @@ protected override DispatchContext CreateDispatchRequest(FailedMessagesUnArchive }; } - protected override Task> PublishEvents(IEnumerable contexts, IAsyncDocumentSession session) + protected override Task> PublishEvents(IEnumerable contexts) { return Task.FromResult(contexts.Select(r => (object)new Contracts.FailedMessagesUnArchived { diff --git a/src/ServiceControl/Recoverability/ExternalIntegration/MessageFailedPublisher.cs b/src/ServiceControl/Recoverability/ExternalIntegration/MessageFailedPublisher.cs index da073ddf64..f0db52c942 100644 --- a/src/ServiceControl/Recoverability/ExternalIntegration/MessageFailedPublisher.cs +++ b/src/ServiceControl/Recoverability/ExternalIntegration/MessageFailedPublisher.cs @@ -6,11 +6,17 @@ namespace ServiceControl.Recoverability.ExternalIntegration using System.Threading.Tasks; using Contracts.MessageFailures; using ExternalIntegrations; - using MessageFailures; - using Raven.Client; + using ServiceControl.Persistence; class MessageFailedPublisher : EventPublisher { + readonly IErrorMessageDataStore dataStore; + + public MessageFailedPublisher(IErrorMessageDataStore dataStore) + { + this.dataStore = dataStore; + } + protected override DispatchContext CreateDispatchRequest(MessageFailed @event) { return new DispatchContext @@ -19,23 +25,11 @@ protected override DispatchContext CreateDispatchRequest(MessageFailed @event) }; } - protected override async Task> PublishEvents(IEnumerable contexts, IAsyncDocumentSession session) + protected override async Task> PublishEvents(IEnumerable contexts) { - var documentIds = contexts.Select(x => x.FailedMessageId).Cast().ToArray(); - var failedMessageData = await session.LoadAsync(documentIds) - .ConfigureAwait(false); - - var failedMessages = new List(failedMessageData.Length); - foreach (var entity in failedMessageData) - { - if (entity != null) - { - session.Advanced.Evict(entity); - failedMessages.Add(entity.ToEvent()); - } - } - - return failedMessages; + var ids = contexts.Select(x => x.FailedMessageId).ToArray(); + var results = await dataStore.FailedMessagesFetch(ids); + return results.Select(x => x.ToEvent()); } public class DispatchContext diff --git a/src/ServiceControl/Recoverability/ExternalIntegration/MessageFailureResolvedByRetryPublisher.cs b/src/ServiceControl/Recoverability/ExternalIntegration/MessageFailureResolvedByRetryPublisher.cs index a9fd5a1476..85dd3b26a2 100644 --- a/src/ServiceControl/Recoverability/ExternalIntegration/MessageFailureResolvedByRetryPublisher.cs +++ b/src/ServiceControl/Recoverability/ExternalIntegration/MessageFailureResolvedByRetryPublisher.cs @@ -5,7 +5,6 @@ namespace ServiceControl.Recoverability.ExternalIntegration using System.Threading.Tasks; using Contracts.MessageFailures; using ExternalIntegrations; - using Raven.Client; class MessageFailureResolvedByRetryPublisher : EventPublisher { @@ -17,7 +16,7 @@ protected override DispatchContext CreateDispatchRequest(MessageFailureResolvedB }; } - protected override Task> PublishEvents(IEnumerable contexts, IAsyncDocumentSession session) + protected override Task> PublishEvents(IEnumerable contexts) { return Task.FromResult(contexts.Select(r => (object)new Contracts.MessageFailureResolvedByRetry { diff --git a/src/ServiceControl/Recoverability/ExternalIntegration/MessageFailureResolvedManuallyPublisher.cs b/src/ServiceControl/Recoverability/ExternalIntegration/MessageFailureResolvedManuallyPublisher.cs index 30cf06413c..3f014c37c6 100644 --- a/src/ServiceControl/Recoverability/ExternalIntegration/MessageFailureResolvedManuallyPublisher.cs +++ b/src/ServiceControl/Recoverability/ExternalIntegration/MessageFailureResolvedManuallyPublisher.cs @@ -5,7 +5,6 @@ namespace ServiceControl.Recoverability.ExternalIntegration using System.Threading.Tasks; using Contracts.MessageFailures; using ExternalIntegrations; - using Raven.Client; class MessageFailureResolvedManuallyPublisher : EventPublisher { @@ -17,7 +16,7 @@ protected override DispatchContext CreateDispatchRequest(MessageFailureResolvedM }; } - protected override Task> PublishEvents(IEnumerable contexts, IAsyncDocumentSession session) + protected override Task> PublishEvents(IEnumerable contexts) { return Task.FromResult(contexts.Select(r => (object)new Contracts.MessageFailureResolvedManually { diff --git a/src/ServiceControl/Recoverability/Grouping/Groupers/ReclassifyErrorsHandler.cs b/src/ServiceControl/Recoverability/Grouping/Groupers/ReclassifyErrorsHandler.cs index 11ba5c6dc9..6c618595f0 100644 --- a/src/ServiceControl/Recoverability/Grouping/Groupers/ReclassifyErrorsHandler.cs +++ b/src/ServiceControl/Recoverability/Grouping/Groupers/ReclassifyErrorsHandler.cs @@ -1,23 +1,20 @@ namespace ServiceControl.Recoverability { - using System.Collections.Generic; + using System; using System.Threading; using System.Threading.Tasks; using Infrastructure.DomainEvents; using MessageFailures.InternalMessages; - using Microsoft.Extensions.Hosting; using NServiceBus; - using Raven.Client; + using ServiceControl.Persistence; + [Obsolete("Only used by legacy RavenDB35 storage engine")] // TODO: how to deal with this domain event class ReclassifyErrorsHandler : IHandleMessages { - public ReclassifyErrorsHandler(IDocumentStore store, IDomainEvents domainEvents, IHostApplicationLifetime lifetime, IEnumerable classifiers) + public ReclassifyErrorsHandler(IReclassifyFailedMessages reclassifier, IDomainEvents domainEvents) { - this.store = store; - this.classifiers = classifiers; + this.reclassifier = reclassifier; this.domainEvents = domainEvents; - - reclassifier = new Reclassifier(lifetime); } public async Task Handle(ReclassifyErrors message, IMessageHandlerContext context) @@ -30,15 +27,14 @@ public async Task Handle(ReclassifyErrors message, IMessageHandlerContext contex try { - var failedMessagesReclassified = await reclassifier.ReclassifyFailedMessages(store, message.Force, classifiers) - .ConfigureAwait(false); + var failedMessagesReclassified = await reclassifier.ReclassifyFailedMessages(message.Force); if (failedMessagesReclassified > 0) { await domainEvents.Raise(new ReclassificationOfErrorMessageComplete { NumberofMessageReclassified = failedMessagesReclassified - }).ConfigureAwait(false); + }); } } finally @@ -48,9 +44,7 @@ await domainEvents.Raise(new ReclassificationOfErrorMessageComplete } IDomainEvents domainEvents; - IDocumentStore store; - IEnumerable classifiers; - Reclassifier reclassifier; + IReclassifyFailedMessages reclassifier; static int executing; } } \ No newline at end of file diff --git a/src/ServiceControl/Recoverability/RecoverabilityComponent.cs b/src/ServiceControl/Recoverability/RecoverabilityComponent.cs index 1a1d29f907..bdc92a0e82 100644 --- a/src/ServiceControl/Recoverability/RecoverabilityComponent.cs +++ b/src/ServiceControl/Recoverability/RecoverabilityComponent.cs @@ -10,21 +10,16 @@ using ExternalIntegrations; using Infrastructure.BackgroundTasks; using Infrastructure.DomainEvents; - using Infrastructure.RavenDB; - using MessageFailures; - using MessageFailures.Api; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Hosting; using NServiceBus.Logging; - using NServiceBus.Raw; using NServiceBus.Transport; using Operations; using Operations.BodyStorage; - using Operations.BodyStorage.RavenAttachments; using Particular.ServiceControl; - using Raven.Client; using Retrying; using ServiceBus.Management.Infrastructure.Settings; + using ServiceControl.Contracts.MessageFailures; using ServiceControl.Persistence; using ServiceControl.Transports; @@ -37,12 +32,7 @@ public override void Configure(Settings settings, IHostBuilder hostBuilder) collection.AddPlatformConnectionProvider(); //Archiving - collection.AddSingleton(); - collection.AddSingleton(); - collection.AddSingleton(); - collection.AddSingleton(); - collection.AddSingleton(); //Grouping collection.AddSingleton(); @@ -84,11 +74,9 @@ public override void Configure(Settings settings, IHostBuilder hostBuilder) } //Failed messages - collection.AddSingleton(); collection.AddHostedService(); //Body storage - collection.AddSingleton(); collection.AddSingleton(); //Health checks @@ -136,33 +124,42 @@ public override void Setup(Settings settings, IComponentInstallationContext cont { context.CreateQueue(settings.ErrorLogQueue); } - - context.AddIndexAssembly(typeof(RavenBootstrapper).Assembly); - context.AddIndexAssembly(typeof(CustomChecksIndex).Assembly); } class FailedMessageNotificationsHostedService : IHostedService { - public FailedMessageNotificationsHostedService(FailedMessageViewIndexNotifications notifications, IDocumentStore store) + public FailedMessageNotificationsHostedService( + IDomainEvents domainEvents, + IFailedMessageViewIndexNotifications store + ) { - this.notifications = notifications; + this.domainEvents = domainEvents; this.store = store; } public Task StartAsync(CancellationToken cancellationToken) { - subscription = store.Changes().ForIndex(new FailedMessageViewIndex().IndexName).Subscribe(notifications); + subscription = store.Subscribe(Callback); return Task.FromResult(true); } + Task Callback(FailedMessageTotals message) + { + return domainEvents.Raise(new MessageFailuresUpdated + { + UnresolvedTotal = message.UnresolvedTotal, + ArchivedTotal = message.UnresolvedTotal + }); + } + public Task StopAsync(CancellationToken cancellationToken) { subscription.Dispose(); return Task.FromResult(true); } - FailedMessageViewIndexNotifications notifications; - IDocumentStore store; + readonly IDomainEvents domainEvents; + IFailedMessageViewIndexNotifications store; IDisposable subscription; } @@ -191,7 +188,7 @@ public Task StopAsync(CancellationToken cancellationToken) async Task ProcessRequestedBulkRetryOperations() { - var processedRequests = await retries.ProcessNextBulkRetry().ConfigureAwait(false); + var processedRequests = await retries.ProcessNextBulkRetry(); return processedRequests ? TimerJobExecutionResult.ExecuteImmediately : TimerJobExecutionResult.ScheduleNextExecution; } @@ -204,18 +201,14 @@ async Task ProcessRequestedBulkRetryOperations() class RebuildRetryGroupStatusesHostedService : IHostedService { - public RebuildRetryGroupStatusesHostedService(RetryDocumentManager retryDocumentManager, IDocumentStore store) + public RebuildRetryGroupStatusesHostedService(RetryDocumentManager retryDocumentManager) { - this.store = store; this.retryDocumentManager = retryDocumentManager; } - public async Task StartAsync(CancellationToken cancellationToken) + public Task StartAsync(CancellationToken cancellationToken) { - using (var storageSession = store.OpenAsyncSession()) - { - await retryDocumentManager.RebuildRetryOperationState(storageSession).ConfigureAwait(false); - } + return retryDocumentManager.RebuildRetryOperationState(); } public Task StopAsync(CancellationToken cancellationToken) @@ -223,35 +216,30 @@ public Task StopAsync(CancellationToken cancellationToken) return Task.CompletedTask; } - RetryDocumentManager retryDocumentManager; - IDocumentStore store; + readonly RetryDocumentManager retryDocumentManager; } internal class AdoptOrphanBatchesFromPreviousSessionHostedService : IHostedService { - public AdoptOrphanBatchesFromPreviousSessionHostedService(RetryDocumentManager retryDocumentManager, IDocumentStore store, IAsyncTimer scheduler) + public AdoptOrphanBatchesFromPreviousSessionHostedService(RetryDocumentManager retryDocumentManager, IAsyncTimer scheduler) { this.retryDocumentManager = retryDocumentManager; - this.store = store; this.scheduler = scheduler; startTime = DateTime.UtcNow; } internal async Task AdoptOrphanedBatchesAsync() { - bool hasMoreWorkToDo; - using (var session = store.OpenAsyncSession()) - { - hasMoreWorkToDo = await retryDocumentManager.AdoptOrphanedBatches(session, startTime).ConfigureAwait(false); - } + var hasMoreWorkToDo = await retryDocumentManager.AdoptOrphanedBatches(startTime); return hasMoreWorkToDo; } + public Task StartAsync(CancellationToken cancellationToken) { timer = scheduler.Schedule(async _ => { - var hasMoreWork = await AdoptOrphanedBatchesAsync().ConfigureAwait(false); + var hasMoreWork = await AdoptOrphanedBatchesAsync(); return hasMoreWork ? TimerJobExecutionResult.ScheduleNextExecution : TimerJobExecutionResult.DoNotContinueExecuting; }, TimeSpan.Zero, TimeSpan.FromMinutes(2), e => { log.Error("Unhandled exception while trying to adopt orphaned batches", e); }); return Task.CompletedTask; @@ -263,17 +251,15 @@ public Task StopAsync(CancellationToken cancellationToken) } TimerJob timer; - DateTime startTime; - IDocumentStore store; + readonly DateTime startTime; readonly IAsyncTimer scheduler; - RetryDocumentManager retryDocumentManager; - static ILog log = LogManager.GetLogger(); + readonly RetryDocumentManager retryDocumentManager; + static readonly ILog log = LogManager.GetLogger(); } class ProcessRetryBatchesHostedService : IHostedService { public ProcessRetryBatchesHostedService( - IDocumentStore store, RetryProcessor processor, Settings settings, IAsyncTimer scheduler, @@ -281,7 +267,6 @@ public ProcessRetryBatchesHostedService( TransportSettings transportSettings) { this.processor = processor; - this.store = store; this.settings = settings; this.scheduler = scheduler; this.transportCustomization = transportCustomization; @@ -290,9 +275,9 @@ public ProcessRetryBatchesHostedService( public async Task StartAsync(CancellationToken cancellationToken) { - dispatcher = await transportCustomization.InitializeDispatcher("RetryProcessor", transportSettings).ConfigureAwait(false); + dispatcher = await transportCustomization.InitializeDispatcher("RetryProcessor", transportSettings); - timer = scheduler.Schedule(t => Process(t), TimeSpan.Zero, settings.ProcessRetryBatchesFrequency, e => { log.Error("Unhandled exception while processing retry batches", e); }); + timer = scheduler.Schedule(Process, TimeSpan.Zero, settings.ProcessRetryBatchesFrequency, e => { log.Error("Unhandled exception while processing retry batches", e); }); } public Task StopAsync(CancellationToken cancellationToken) @@ -302,12 +287,8 @@ public Task StopAsync(CancellationToken cancellationToken) async Task Process(CancellationToken cancellationToken) { - using (var session = store.OpenAsyncSession()) - { - var batchesProcessed = await processor.ProcessBatches(session, dispatcher, cancellationToken).ConfigureAwait(false); - await session.SaveChangesAsync(CancellationToken.None).ConfigureAwait(false); - return batchesProcessed ? TimerJobExecutionResult.ExecuteImmediately : TimerJobExecutionResult.ScheduleNextExecution; - } + var batchesProcessed = await processor.ProcessBatches(dispatcher, cancellationToken); + return batchesProcessed ? TimerJobExecutionResult.ExecuteImmediately : TimerJobExecutionResult.ScheduleNextExecution; } readonly Settings settings; @@ -316,7 +297,6 @@ async Task Process(CancellationToken cancellationToken) readonly TransportSettings transportSettings; TimerJob timer; - IDocumentStore store; RetryProcessor processor; static ILog log = LogManager.GetLogger(typeof(ProcessRetryBatchesHostedService)); IDispatchMessages dispatcher; diff --git a/src/ServiceControl/Recoverability/Retrying/FailedMessageRetryCleaner.cs b/src/ServiceControl/Recoverability/Retrying/FailedMessageRetryCleaner.cs index a43be0e7db..ff5531359b 100644 --- a/src/ServiceControl/Recoverability/Retrying/FailedMessageRetryCleaner.cs +++ b/src/ServiceControl/Recoverability/Retrying/FailedMessageRetryCleaner.cs @@ -3,21 +3,22 @@ namespace ServiceControl.Recoverability using System.Threading.Tasks; using Contracts.MessageFailures; using Infrastructure.DomainEvents; + using Persistence; class FailedMessageRetryCleaner : IDomainHandler { - readonly RetryDocumentManager retryDocumentManager; + readonly IErrorMessageDataStore dataStore; - public FailedMessageRetryCleaner(RetryDocumentManager retryDocumentManager) + public FailedMessageRetryCleaner(IErrorMessageDataStore dataStore) { - this.retryDocumentManager = retryDocumentManager; + this.dataStore = dataStore; } public Task Handle(MessageFailed message) { if (message.RepeatedFailure) { - return retryDocumentManager.RemoveFailedMessageRetryDocument(message.FailedMessageId); + return dataStore.RemoveFailedMessageRetryDocument(message.FailedMessageId); } return Task.FromResult(0); diff --git a/src/ServiceControl/Recoverability/Retrying/Handlers/PendingRetriesHandler.cs b/src/ServiceControl/Recoverability/Retrying/Handlers/PendingRetriesHandler.cs index fe1725db45..8cff935258 100644 --- a/src/ServiceControl/Recoverability/Retrying/Handlers/PendingRetriesHandler.cs +++ b/src/ServiceControl/Recoverability/Retrying/Handlers/PendingRetriesHandler.cs @@ -2,66 +2,43 @@ namespace ServiceControl.Recoverability { using System.Collections.Generic; using System.Threading.Tasks; - using MessageFailures; - using MessageFailures.Api; using MessageFailures.InternalMessages; using NServiceBus; - using Raven.Client; + using Persistence; class PendingRetriesHandler : IHandleMessages, IHandleMessages { - public PendingRetriesHandler(IDocumentStore store, RetryDocumentManager manager) + public PendingRetriesHandler(IErrorMessageDataStore dataStore) { - this.store = store; - this.manager = manager; + this.dataStore = dataStore; } public async Task Handle(RetryPendingMessages message, IMessageHandlerContext context) { var messageIds = new List(); - using (var session = store.OpenAsyncSession()) - { - var query = session.Advanced - .AsyncDocumentQuery() - .WhereEquals("Status", (int)FailedMessageStatus.RetryIssued) - .AndAlso() - .WhereBetweenOrEqual(options => options.LastModified, message.PeriodFrom.Ticks, message.PeriodTo.Ticks) - .AndAlso() - .WhereEquals(o => o.QueueAddress, message.QueueAddress) - .SetResultTransformer(FailedMessageViewTransformer.Name) - .SelectFields(fields); + var ids = await dataStore.GetRetryPendingMessages(message.PeriodFrom, message.PeriodTo, message.QueueAddress); - using (var ie = await session.Advanced.StreamAsync(query).ConfigureAwait(false)) - { - while (await ie.MoveNextAsync().ConfigureAwait(false)) - { - await manager.RemoveFailedMessageRetryDocument(ie.Current.Document.Id) - .ConfigureAwait(false); - messageIds.Add(ie.Current.Document.Id); - } - } + foreach (var id in ids) + { + await dataStore.RemoveFailedMessageRetryDocument(id); + messageIds.Add(id); } - await context.SendLocal(new RetryMessagesById { MessageUniqueIds = messageIds.ToArray() }) - .ConfigureAwait(false); + await context.SendLocal(new RetryMessagesById { MessageUniqueIds = messageIds.ToArray() }); } public async Task Handle(RetryPendingMessagesById message, IMessageHandlerContext context) { foreach (var messageUniqueId in message.MessageUniqueIds) { - await manager.RemoveFailedMessageRetryDocument(messageUniqueId) - .ConfigureAwait(false); + await dataStore.RemoveFailedMessageRetryDocument(messageUniqueId); } - await context.SendLocal(m => m.MessageUniqueIds = message.MessageUniqueIds) - .ConfigureAwait(false); + await context.SendLocal(m => m.MessageUniqueIds = message.MessageUniqueIds); } - readonly IDocumentStore store; - readonly RetryDocumentManager manager; - static string[] fields = { "Id" }; + readonly IErrorMessageDataStore dataStore; } } \ No newline at end of file diff --git a/src/ServiceControl/Recoverability/Retrying/Handlers/RetriesHandler.cs b/src/ServiceControl/Recoverability/Retrying/Handlers/RetriesHandler.cs index 847135fb42..b7989d917a 100644 --- a/src/ServiceControl/Recoverability/Retrying/Handlers/RetriesHandler.cs +++ b/src/ServiceControl/Recoverability/Retrying/Handlers/RetriesHandler.cs @@ -1,10 +1,7 @@ namespace ServiceControl.Recoverability { - using System; using System.Threading.Tasks; using Contracts.MessageFailures; - using MessageFailures; - using MessageFailures.Api; using MessageFailures.InternalMessages; using NServiceBus; using ServiceControl.Persistence; @@ -16,13 +13,14 @@ class RetriesHandler : IHandleMessages, IHandleMessages { readonly RetriesGateway retries; - readonly RetryDocumentManager retryDocumentManager; + readonly IErrorMessageDataStore dataStore; - public RetriesHandler(RetriesGateway retries, RetryDocumentManager retryDocumentManager) + public RetriesHandler(RetriesGateway retries, IErrorMessageDataStore dataStore) { this.retries = retries; - this.retryDocumentManager = retryDocumentManager; + this.dataStore = dataStore; } + /// /// For handling leftover messages. MessageFailed are no longer published on the bus and the code is moved to /// . @@ -31,7 +29,7 @@ public Task Handle(MessageFailed message, IMessageHandlerContext context) { if (message.RepeatedFailure) { - return retryDocumentManager.RemoveFailedMessageRetryDocument(message.FailedMessageId); + return dataStore.RemoveFailedMessageRetryDocument(message.FailedMessageId); } return Task.FromResult(0); @@ -41,11 +39,11 @@ public Task Handle(RequestRetryAll message, IMessageHandlerContext context) { if (!string.IsNullOrWhiteSpace(message.Endpoint)) { - retries.StartRetryForIndex(message.Endpoint, RetryType.AllForEndpoint, DateTime.UtcNow, m => m.ReceivingEndpointName == message.Endpoint, "all messages for endpoint " + message.Endpoint); + retries.StartRetryForEndpoint(message.Endpoint); } else { - retries.StartRetryForIndex("All", RetryType.All, DateTime.UtcNow, originator: "all messages"); + retries.StartRetryForAllMessages(); } return Task.FromResult(0); @@ -65,7 +63,7 @@ public Task Handle(RetryMessagesByQueueAddress message, IMessageHandlerContext c { var failedQueueAddress = message.QueueAddress; - retries.StartRetryForIndex(failedQueueAddress, RetryType.ByQueueAddress, DateTime.UtcNow, m => m.QueueAddress == failedQueueAddress && m.Status == message.Status, $"all messages for failed queue address '{message.QueueAddress}'"); + retries.StartRetryForFailedQueueAddress(failedQueueAddress, message.Status); return Task.FromResult(0); } diff --git a/src/ServiceControl/Recoverability/Retrying/Handlers/RetryAllInGroupHandler.cs b/src/ServiceControl/Recoverability/Retrying/Handlers/RetryAllInGroupHandler.cs index 593ffdab2f..4e753fff00 100644 --- a/src/ServiceControl/Recoverability/Retrying/Handlers/RetryAllInGroupHandler.cs +++ b/src/ServiceControl/Recoverability/Retrying/Handlers/RetryAllInGroupHandler.cs @@ -4,8 +4,8 @@ namespace ServiceControl.Recoverability using System.Threading.Tasks; using NServiceBus; using NServiceBus.Logging; - using Raven.Client; using ServiceControl.Persistence; + using ServiceControl.Persistence.Recoverability; class RetryAllInGroupHandler : IHandleMessages { @@ -13,24 +13,18 @@ public async Task Handle(RetryAllInGroup message, IMessageHandlerContext context { if (retries == null) { - log.Warn($"Attempt to retry a group ({message.GroupId}) when retries are disabled"); + Log.Warn($"Attempt to retry a group ({message.GroupId}) when retries are disabled"); return; } - if (archivingManager.IsArchiveInProgressFor(message.GroupId)) + if (archiver.IsArchiveInProgressFor(message.GroupId)) { - log.Warn($"Attempt to retry a group ({message.GroupId}) which is currently in the process of being archived"); + Log.Warn($"Attempt to retry a group ({message.GroupId}) which is currently in the process of being archived"); return; } - FailureGroupView group; - using (var session = store.OpenAsyncSession()) - { - group = await session.Query() - .FirstOrDefaultAsync(x => x.Id == message.GroupId) - .ConfigureAwait(false); - } + var group = await dataStore.QueryFailureGroupViewOnGroupId(message.GroupId); string originator = null; if (group?.Title != null) @@ -39,23 +33,28 @@ public async Task Handle(RetryAllInGroup message, IMessageHandlerContext context } var started = message.Started ?? DateTime.UtcNow; - await retryingManager.Wait(message.GroupId, RetryType.FailureGroup, started, originator, group?.Type, group?.Last) - .ConfigureAwait(false); - retries.StartRetryForIndex(message.GroupId, RetryType.FailureGroup, started, x => x.FailureGroupId == message.GroupId, originator, group?.Type); + await retryingManager.Wait(message.GroupId, RetryType.FailureGroup, started, originator, group?.Type, group?.Last); + + retries.EnqueueRetryForFailureGroup(new RetriesGateway.RetryForFailureGroup( + message.GroupId, + originator, + group?.Type, + started + )); } - public RetryAllInGroupHandler(RetriesGateway retries, IDocumentStore store, RetryingManager retryingManager, ArchivingManager archivingManager) + public RetryAllInGroupHandler(RetriesGateway retries, RetryingManager retryingManager, IArchiveMessages archiver, IRetryDocumentDataStore dataStore) { this.retries = retries; - this.store = store; this.retryingManager = retryingManager; - this.archivingManager = archivingManager; + this.archiver = archiver; + this.dataStore = dataStore; } readonly RetriesGateway retries; - readonly IDocumentStore store; readonly RetryingManager retryingManager; - readonly ArchivingManager archivingManager; - static ILog log = LogManager.GetLogger(typeof(RetryAllInGroupHandler)); + readonly IArchiveMessages archiver; + readonly IRetryDocumentDataStore dataStore; + static readonly ILog Log = LogManager.GetLogger(typeof(RetryAllInGroupHandler)); } } \ No newline at end of file diff --git a/src/ServiceControl/Recoverability/Retrying/History/StoreHistoryHandler.cs b/src/ServiceControl/Recoverability/Retrying/History/StoreHistoryHandler.cs index ce5aad5bf5..c1939f9ed7 100644 --- a/src/ServiceControl/Recoverability/Retrying/History/StoreHistoryHandler.cs +++ b/src/ServiceControl/Recoverability/Retrying/History/StoreHistoryHandler.cs @@ -2,56 +2,33 @@ { using System.Threading.Tasks; using Infrastructure.DomainEvents; - using Raven.Client; using ServiceBus.Management.Infrastructure.Settings; + using ServiceControl.Persistence; public class StoreHistoryHandler : IDomainHandler { - public StoreHistoryHandler(IDocumentStore store, Settings settings) + public StoreHistoryHandler(IRetryHistoryDataStore store, Settings settings) { this.store = store; this.settings = settings; } - public async Task Handle(RetryOperationCompleted message) + public Task Handle(RetryOperationCompleted message) { - using (var session = store.OpenAsyncSession()) - { - var retryHistory = await session.LoadAsync(RetryHistory.MakeId()).ConfigureAwait(false) ?? - RetryHistory.CreateNew(); - - retryHistory.AddToUnacknowledged(new UnacknowledgedRetryOperation - { - RequestId = message.RequestId, - RetryType = message.RetryType, - StartTime = message.StartTime, - CompletionTime = message.CompletionTime, - Originator = message.Originator, - Classifier = message.Classifier, - Failed = message.Failed, - NumberOfMessagesProcessed = message.NumberOfMessagesProcessed, - Last = message.Last - }); - - retryHistory.AddToHistory(new HistoricRetryOperation - { - RequestId = message.RequestId, - RetryType = message.RetryType, - StartTime = message.StartTime, - CompletionTime = message.CompletionTime, - Originator = message.Originator, - Failed = message.Failed, - NumberOfMessagesProcessed = message.NumberOfMessagesProcessed - }, settings.RetryHistoryDepth); - - await session.StoreAsync(retryHistory) - .ConfigureAwait(false); - await session.SaveChangesAsync() - .ConfigureAwait(false); - } + return store.RecordRetryOperationCompleted( + message.RequestId, + message.RetryType, + message.StartTime, + message.CompletionTime, + message.Originator, + message.Classifier, + message.Failed, + message.NumberOfMessagesProcessed, + message.Last, + settings.RetryHistoryDepth); } - readonly IDocumentStore store; + readonly IRetryHistoryDataStore store; readonly Settings settings; } } \ No newline at end of file diff --git a/src/ServiceControl/Recoverability/Retrying/InMemoryRetry.cs b/src/ServiceControl/Recoverability/Retrying/InMemoryRetry.cs index d5414a0ed3..dac3a2f874 100644 --- a/src/ServiceControl/Recoverability/Retrying/InMemoryRetry.cs +++ b/src/ServiceControl/Recoverability/Retrying/InMemoryRetry.cs @@ -132,9 +132,9 @@ await domainEvents.Raise(new RetryMessagesForwarded Progress = GetProgress(), IsFailed = Failed, StartTime = Started - }).ConfigureAwait(false); + }); - await CheckForCompletion().ConfigureAwait(false); + await CheckForCompletion(); } public Task Skip(int numberOfMessagesSkipped) @@ -165,7 +165,7 @@ await domainEvents.Raise(new RetryOperationCompleted NumberOfMessagesProcessed = NumberOfMessagesForwarded, Last = Last ?? DateTime.MaxValue, Classifier = Classifier - }).ConfigureAwait(false); + }); if (retryType == RetryType.FailureGroup) { @@ -174,7 +174,7 @@ await domainEvents.Raise(new MessagesSubmittedForRetry FailedMessageIds = new string[0], NumberOfFailedMessages = NumberOfMessagesForwarded, Context = Originator - }).ConfigureAwait(false); + }); } Log.Info($"Retry operation {RequestId} completed. {NumberOfMessagesSkipped} messages skipped, {NumberOfMessagesForwarded} forwarded. Total {TotalNumberOfMessages}."); diff --git a/src/ServiceControl/Recoverability/Retrying/Infrastructure/ReturnToSender.cs b/src/ServiceControl/Recoverability/Retrying/Infrastructure/ReturnToSender.cs index bd14dbf270..f133ee9961 100644 --- a/src/ServiceControl/Recoverability/Retrying/Infrastructure/ReturnToSender.cs +++ b/src/ServiceControl/Recoverability/Retrying/Infrastructure/ReturnToSender.cs @@ -1,24 +1,19 @@ namespace ServiceControl.Recoverability { + using System; using System.Collections.Generic; - using System.IO; - using System.Linq; - using System.Text; using System.Threading.Tasks; - using CompositeViews.Messages; - using MessageFailures; using NServiceBus.Logging; using NServiceBus.Routing; using NServiceBus.Transport; using Operations.BodyStorage; - using Raven.Client; - using Raven.Json.Linq; + using ServiceControl.Persistence; class ReturnToSender { - public ReturnToSender(IBodyStorage bodyStorage, IDocumentStore documentStore) + public ReturnToSender(IBodyStorage bodyStorage, IErrorMessageDataStore errorMessageStore) { - this.documentStore = documentStore; + this.errorMessageStore = errorMessageStore; this.bodyStorage = bodyStorage; } @@ -40,13 +35,11 @@ public virtual async Task HandleMessage(MessageContext message, IDispatchMessage { if (outgoingHeaders.Remove("ServiceControl.Retry.BodyOnFailedMessage")) { - body = await FetchFromFailedMessage(outgoingHeaders, messageId, attemptMessageId) - .ConfigureAwait(false); + body = await FetchFromFailedMessage(outgoingHeaders, messageId, attemptMessageId); } else { - body = await FetchFromBodyStore(attemptMessageId, messageId) - .ConfigureAwait(false); + body = await FetchFromBodyStore(attemptMessageId, messageId); } outgoingHeaders.Remove("ServiceControl.Retry.Attempt.MessageId"); @@ -79,8 +72,7 @@ public virtual async Task HandleMessage(MessageContext message, IDispatchMessage var transportOp = new TransportOperation(outgoingMessage, new UnicastAddressTag(retryTo)); - await sender.Dispatch(new TransportOperations(transportOp), message.TransportTransaction, message.Extensions) - .ConfigureAwait(false); + await sender.Dispatch(new TransportOperations(transportOp), message.TransportTransaction, message.Extensions); if (Log.IsDebugEnabled) { @@ -90,45 +82,42 @@ await sender.Dispatch(new TransportOperations(transportOp), message.TransportTra async Task FetchFromFailedMessage(Dictionary outgoingHeaders, string messageId, string attemptMessageId) { - byte[] body = null; - string documentId = FailedMessage.MakeDocumentId(outgoingHeaders["ServiceControl.Retry.UniqueMessageId"]); - var results = await documentStore.AsyncDatabaseCommands.GetAsync(new[] { documentId }, null, - transformer: MessagesBodyTransformer.Name).ConfigureAwait(false); - - string resultBody = ((results.Results?.SingleOrDefault()?["$values"] as RavenJArray)?.SingleOrDefault() as RavenJObject) - ?.ToObject()?.Body; + var uniqueMessageId = outgoingHeaders["ServiceControl.Retry.UniqueMessageId"]; + byte[] body = await errorMessageStore.FetchFromFailedMessage(uniqueMessageId); - if (resultBody != null) + // TODO: Weird that none of these logged parameters are actually used in the attempt to load the thing + if (body == null) { - body = Encoding.UTF8.GetBytes(resultBody); - - if (Log.IsDebugEnabled) - { - Log.DebugFormat("{0}: Body size: {1} bytes retrieved from index", messageId, body.LongLength); - } + Log.WarnFormat("{0}: Message Body not found on index for attempt Id {1}", messageId, attemptMessageId); } - else + else if (Log.IsDebugEnabled) { - Log.WarnFormat("{0}: Message Body not found on index for attempt Id {1}", messageId, attemptMessageId); + Log.DebugFormat("{0}: Body size: {1} bytes retrieved from index", messageId, body.LongLength); } + return body; } async Task FetchFromBodyStore(string attemptMessageId, string messageId) { byte[] body = null; - var result = await bodyStorage.TryFetch(attemptMessageId) - .ConfigureAwait(false); + var result = await bodyStorage.TryFetch(attemptMessageId); + + if (result == null) + { + throw new InvalidOperationException("IBodyStorage.TryFetch result cannot be null"); + } + if (result.HasResult) { - using (result.Stream) + using (result.Stream) // Not strictly required for MemoryStream but might be different behavior in future .NET versions { // Unfortunately we can't use the buffer manager here yet because core doesn't allow to set the length property so usage of GetBuffer is not possible // furthermore call ToArray would neglect many of the benefits of the recyclable stream // RavenDB always returns a memory stream so there is no need to pretend we need to do buffered reads since the memory is anyway fully allocated already // this assumption might change when the database is upgraded but right now this is the most memory efficient way to do things // https://github.com/microsoft/Microsoft.IO.RecyclableMemoryStream#getbuffer-and-toarray - body = ((MemoryStream)result.Stream).ToArray(); + body = result.Stream.ToArray(); } if (Log.IsDebugEnabled) @@ -138,15 +127,14 @@ async Task FetchFromBodyStore(string attemptMessageId, string messageId) } else { - Log.WarnFormat("{0}: Message Body not found in attachment store for attempt Id {1}", messageId, - attemptMessageId); + Log.WarnFormat("{0}: Message Body not found in attachment store for attempt Id {1}", messageId, attemptMessageId); } return body; } - static readonly byte[] EmptyBody = new byte[0]; + static readonly byte[] EmptyBody = Array.Empty(); readonly IBodyStorage bodyStorage; static readonly ILog Log = LogManager.GetLogger(typeof(ReturnToSender)); - readonly IDocumentStore documentStore; + readonly IErrorMessageDataStore errorMessageStore; } } diff --git a/src/ServiceControl/Recoverability/Retrying/Infrastructure/ReturnToSenderDequeuer.cs b/src/ServiceControl/Recoverability/Retrying/Infrastructure/ReturnToSenderDequeuer.cs index 4b90831a9e..cbe808fa39 100644 --- a/src/ServiceControl/Recoverability/Retrying/Infrastructure/ReturnToSenderDequeuer.cs +++ b/src/ServiceControl/Recoverability/Retrying/Infrastructure/ReturnToSenderDequeuer.cs @@ -4,20 +4,18 @@ namespace ServiceControl.Recoverability using System.Threading; using System.Threading.Tasks; using Infrastructure.DomainEvents; - using MessageFailures; using Microsoft.Extensions.Hosting; using NServiceBus; using NServiceBus.Logging; using NServiceBus.Raw; using NServiceBus.Routing; using NServiceBus.Transport; - using Raven.Client; using ServiceBus.Management.Infrastructure.Settings; using ServiceControl.Persistence; class ReturnToSenderDequeuer : IHostedService { - public ReturnToSenderDequeuer(ReturnToSender returnToSender, IDocumentStore store, IDomainEvents domainEvents, RawEndpointFactory rawEndpointFactory, Settings settings) + public ReturnToSenderDequeuer(ReturnToSender returnToSender, IErrorMessageDataStore dataStore, IDomainEvents domainEvents, RawEndpointFactory rawEndpointFactory, Settings settings) { InputAddress = settings.StagingQueue; this.returnToSender = returnToSender; @@ -32,7 +30,7 @@ public ReturnToSenderDequeuer(ReturnToSender returnToSender, IDocumentStore stor return config; }; - faultManager = new CaptureIfMessageSendingFails(store, domainEvents, IncrementCounterOrProlongTimer); + faultManager = new CaptureIfMessageSendingFails(dataStore, domainEvents, IncrementCounterOrProlongTimer); timer = new Timer(state => StopInternal().GetAwaiter().GetResult()); } @@ -55,7 +53,7 @@ async Task Handle(MessageContext message, IDispatchMessages sender) if (shouldProcess(message)) { - await returnToSender.HandleMessage(message, sender, errorQueueTransportAddress).ConfigureAwait(false); + await returnToSender.HandleMessage(message, sender, errorQueueTransportAddress); IncrementCounterOrProlongTimer(); } else @@ -121,11 +119,11 @@ public virtual async Task Run(string forwardingBatchId, Predicate(); registration = cancellationToken.Register(() => _ = Task.Run(() => syncEvent.TrySetResult(true), CancellationToken.None)); - var startable = await RawEndpoint.Create(config).ConfigureAwait(false); + var startable = await RawEndpoint.Create(config); errorQueueTransportAddress = GetErrorQueueTransportAddress(startable); - processor = await startable.Start().ConfigureAwait(false); + processor = await startable.Start(); Log.Info($"Forwarder for batch {forwardingBatchId} started receiving messages from {processor.TransportAddress}."); @@ -146,16 +144,16 @@ public virtual async Task Run(string forwardingBatchId, Predicate stopCompletionSource.TrySetResult(true), CancellationToken.None).ConfigureAwait(false); + await Task.Run(() => stopCompletionSource.TrySetResult(true), CancellationToken.None); } if (endedPrematurely || cancellationToken.IsCancellationRequested) @@ -185,8 +183,8 @@ async Task StopInternal() Log.Debug("Completing forwarding."); } - await Task.Run(() => syncEvent?.TrySetResult(true)).ConfigureAwait(false); - await (stopCompletionSource?.Task ?? (Task)Task.FromResult(0)).ConfigureAwait(false); + await Task.Run(() => syncEvent?.TrySetResult(true)); + await (stopCompletionSource?.Task ?? (Task)Task.FromResult(0)); if (Log.IsDebugEnabled) { Log.Debug("Forwarding completed."); @@ -210,9 +208,9 @@ async Task StopInternal() class CaptureIfMessageSendingFails : IErrorHandlingPolicy { - public CaptureIfMessageSendingFails(IDocumentStore store, IDomainEvents domainEvents, Action executeOnFailure) + public CaptureIfMessageSendingFails(IErrorMessageDataStore dataStore, IDomainEvents domainEvents, Action executeOnFailure) { - this.store = store; + this.dataStore = dataStore; this.executeOnFailure = executeOnFailure; this.domainEvents = domainEvents; } @@ -226,25 +224,8 @@ public async Task OnError(IErrorHandlingPolicyContext handlin var messageUniqueId = message.Headers["ServiceControl.Retry.UniqueMessageId"]; Log.Warn($"Failed to send '{messageUniqueId}' message to '{destination}' for retry. Attempting to revert message status to unresolved so it can be tried again.", handlingContext.Error.Exception); - using (var session = store.OpenAsyncSession()) - { - var failedMessage = await session.LoadAsync(FailedMessage.MakeDocumentId(messageUniqueId)) - .ConfigureAwait(false); - if (failedMessage != null) - { - failedMessage.Status = FailedMessageStatus.Unresolved; - } - - var failedMessageRetry = await session.LoadAsync(FailedMessageRetry.MakeDocumentId(messageUniqueId)) - .ConfigureAwait(false); - if (failedMessageRetry != null) - { - session.Delete(failedMessageRetry); - } - - await session.SaveChangesAsync() - .ConfigureAwait(false); - } + await dataStore.RevertRetry(messageUniqueId); + string reason; try @@ -261,7 +242,7 @@ await domainEvents.Raise(new MessagesSubmittedForRetryFailed Reason = reason, FailedMessageId = messageUniqueId, Destination = destination - }).ConfigureAwait(false); + }); } catch (Exception ex) { @@ -277,8 +258,8 @@ await domainEvents.Raise(new MessagesSubmittedForRetryFailed } readonly Action executeOnFailure; - IDocumentStore store; - IDomainEvents domainEvents; + readonly IErrorMessageDataStore dataStore; + readonly IDomainEvents domainEvents; static readonly ILog Log = LogManager.GetLogger(typeof(CaptureIfMessageSendingFails)); } diff --git a/src/ServiceControl/Recoverability/Retrying/RetriesGateway.cs b/src/ServiceControl/Recoverability/Retrying/RetriesGateway.cs index d1bad09b3a..245375c71f 100644 --- a/src/ServiceControl/Recoverability/Retrying/RetriesGateway.cs +++ b/src/ServiceControl/Recoverability/Retrying/RetriesGateway.cs @@ -4,169 +4,100 @@ namespace ServiceControl.Recoverability using System.Collections.Concurrent; using System.Collections.Generic; using System.Linq; - using System.Linq.Expressions; using System.Threading.Tasks; using Infrastructure; using MessageFailures; using NServiceBus.Logging; - using Raven.Abstractions.Commands; - using Raven.Abstractions.Data; - using Raven.Client; - using Raven.Client.Indexes; - using Raven.Client.Linq; using ServiceControl.Persistence; class RetriesGateway { - public RetriesGateway(IDocumentStore store, RetryDocumentManager documentManager, RetryingManager operationManager) + public RetriesGateway(IRetryDocumentDataStore store, RetryingManager operationManager) { this.store = store; - retryDocumentManager = documentManager; this.operationManager = operationManager; } - async Task, DateTime>> GetRequestedBatches(IBulkRetryRequest request) - { - var response = new List(); - var currentBatch = new HashSet(StringComparer.OrdinalIgnoreCase); - var latestAttempt = DateTime.MinValue; - - using (var session = store.OpenAsyncSession()) - using (var stream = await request.GetDocuments(session).ConfigureAwait(false)) - { - while (await stream.MoveNextAsync().ConfigureAwait(false)) - { - var current = stream.Current.Document; - currentBatch.Add(current.UniqueMessageId); - - if (currentBatch.Count == BatchSize) - { - response.Add(currentBatch.ToArray()); - - currentBatch.Clear(); - } - - var lastDocumentAttempt = current.LatestTimeOfFailure; - if (lastDocumentAttempt > latestAttempt) - { - latestAttempt = lastDocumentAttempt; - } - } - - if (currentBatch.Count > 0) - { - response.Add(currentBatch.ToArray()); - } - } - - return Tuple.Create(response, latestAttempt); - } - - public void StartRetryForIndex(string requestId, RetryType retryType, DateTime startTime, Expression> filter = null, string originator = null, string classifier = null) - where TIndex : AbstractIndexCreationTask, new() - where TType : IHaveStatus - { - log.Info($"Enqueuing index based bulk retry '{originator}'"); - - var request = new IndexBasedBulkRetryRequest(requestId, retryType, originator, classifier, startTime, filter); - - bulkRequests.Enqueue(request); - } - public async Task StartRetryForSingleMessage(string uniqueMessageId) { - log.Info($"Retrying a single message {uniqueMessageId}"); + Log.Info($"Retrying a single message {uniqueMessageId}"); var requestId = uniqueMessageId; var retryType = RetryType.SingleMessage; var numberOfMessages = 1; - await operationManager.Prepairing(requestId, retryType, numberOfMessages) - .ConfigureAwait(false); - await StageRetryByUniqueMessageIds(requestId, retryType, new[] { uniqueMessageId }, DateTime.UtcNow) - .ConfigureAwait(false); - await operationManager.PreparedBatch(requestId, retryType, numberOfMessages) - .ConfigureAwait(false); + await operationManager.Preparing(requestId, retryType, numberOfMessages); + await StageRetryByUniqueMessageIds(requestId, retryType, new[] { uniqueMessageId }, DateTime.UtcNow); + await operationManager.PreparedBatch(requestId, retryType, numberOfMessages); } public async Task StartRetryForMessageSelection(string[] uniqueMessageIds) { - log.Info($"Retrying a selection of {uniqueMessageIds.Length} messages"); + Log.Info($"Retrying a selection of {uniqueMessageIds.Length} messages"); var requestId = DeterministicGuid.MakeId(string.Join(string.Empty, uniqueMessageIds)).ToString(); var retryType = RetryType.MultipleMessages; var numberOfMessages = uniqueMessageIds.Length; - await operationManager.Prepairing(requestId, retryType, numberOfMessages) - .ConfigureAwait(false); - await StageRetryByUniqueMessageIds(requestId, retryType, uniqueMessageIds, DateTime.UtcNow) - .ConfigureAwait(false); - await operationManager.PreparedBatch(requestId, retryType, numberOfMessages) - .ConfigureAwait(false); + await operationManager.Preparing(requestId, retryType, numberOfMessages); + await StageRetryByUniqueMessageIds(requestId, retryType, uniqueMessageIds, DateTime.UtcNow); + await operationManager.PreparedBatch(requestId, retryType, numberOfMessages); } async Task StageRetryByUniqueMessageIds(string requestId, RetryType retryType, string[] messageIds, DateTime startTime, DateTime? last = null, string originator = null, string batchName = null, string classifier = null) { if (messageIds == null || !messageIds.Any()) { - log.Info($"Batch '{batchName}' contains no messages"); + Log.Info($"Batch '{batchName}' contains no messages"); return; } var failedMessageRetryIds = messageIds.Select(FailedMessageRetry.MakeDocumentId).ToArray(); - var batchDocumentId = await retryDocumentManager.CreateBatchDocument(requestId, retryType, failedMessageRetryIds, originator, startTime, last, batchName, classifier) - .ConfigureAwait(false); - - log.Info($"Created Batch '{batchDocumentId}' with {messageIds.Length} messages for '{batchName}'."); + var batchDocumentId = await store.CreateBatchDocument(RetryDocumentManager.RetrySessionId, requestId, retryType, failedMessageRetryIds, originator, startTime, last, batchName, classifier); - var commands = new ICommandData[messageIds.Length]; - for (var i = 0; i < messageIds.Length; i++) - { - commands[i] = RetryDocumentManager.CreateFailedMessageRetryDocument(batchDocumentId, messageIds[i]); - } + Log.Info($"Created Batch '{batchDocumentId}' with {messageIds.Length} messages for '{batchName}'."); - await store.AsyncDatabaseCommands.BatchAsync(commands) - .ConfigureAwait(false); + await store.StageRetryByUniqueMessageIds(batchDocumentId, requestId, retryType, messageIds, startTime, last, + originator, batchName, classifier); - await retryDocumentManager.MoveBatchToStaging(batchDocumentId).ConfigureAwait(false); + await MoveBatchToStaging(batchDocumentId); - log.Info($"Moved Batch '{batchDocumentId}' to Staging"); + Log.Info($"Moved Batch '{batchDocumentId}' to Staging"); } - internal async Task ProcessNextBulkRetry() + // Needs to be overridable by a test + protected virtual Task MoveBatchToStaging(string batchDocumentId) => store.MoveBatchToStaging(batchDocumentId); + + + public async Task ProcessNextBulkRetry() // Invoked from BulkRetryBatchCreationHostedService in schedule { if (!bulkRequests.TryDequeue(out var request)) { return false; } - await ProcessRequest(request).ConfigureAwait(false); + await ProcessRequest(request); return true; } - async Task ProcessRequest(IBulkRetryRequest request) + async Task ProcessRequest(BulkRetryRequest request) { - var batchesWithLastAttempt = await GetRequestedBatches(request).ConfigureAwait(false); - var batches = batchesWithLastAttempt.Item1; - var latestAttempt = batchesWithLastAttempt.Item2; + var (batches, latestAttempt) = await request.GetRequestedBatches(store); var totalMessages = batches.Sum(b => b.Length); if (!operationManager.IsOperationInProgressFor(request.RequestId, request.RetryType) && totalMessages > 0) { var numberOfMessagesAdded = 0; - await operationManager.Prepairing(request.RequestId, request.RetryType, totalMessages) - .ConfigureAwait(false); + await operationManager.Preparing(request.RequestId, request.RetryType, totalMessages); for (var i = 0; i < batches.Count; i++) { - await StageRetryByUniqueMessageIds(request.RequestId, request.RetryType, batches[i], request.StartTime, latestAttempt, request.Originator, GetBatchName(i + 1, batches.Count, request.Originator), request.Classifier) - .ConfigureAwait(false); + await StageRetryByUniqueMessageIds(request.RequestId, request.RetryType, batches[i], request.StartTime, latestAttempt, request.Originator, GetBatchName(i + 1, batches.Count, request.Originator), request.Classifier); numberOfMessagesAdded += batches[i].Length; - await operationManager.PreparedBatch(request.RequestId, request.RetryType, numberOfMessagesAdded) - .ConfigureAwait(false); + await operationManager.PreparedBatch(request.RequestId, request.RetryType, numberOfMessagesAdded); } } } @@ -181,79 +112,172 @@ static string GetBatchName(int pageNum, int totalPages, string context) return $"'{context}' batch {pageNum} of {totalPages}"; } - IDocumentStore store; - RetryDocumentManager retryDocumentManager; - RetryingManager operationManager; - ConcurrentQueue bulkRequests = new ConcurrentQueue(); - const int BatchSize = 1000; + public void StartRetryForAllMessages() + { + var item = new RetryForAllMessages(); + Log.Info($"Enqueuing index based bulk retry '{item}'"); + bulkRequests.Enqueue(item); + } + + public void StartRetryForEndpoint(string endpoint) + { + var item = new RetryForEndpoint(endpoint); + Log.Info($"Enqueuing index based bulk retry '{item}'"); + bulkRequests.Enqueue(item); + } - static ILog log = LogManager.GetLogger(typeof(RetriesGateway)); + public void StartRetryForFailedQueueAddress(string failedQueueAddress, FailedMessageStatus status) + { + var item = new RetryForFailedQueueAddress(failedQueueAddress, status); + Log.Info($"Enqueuing index based bulk retry '{item}'"); + bulkRequests.Enqueue(item); + } - interface IBulkRetryRequest + public void EnqueueRetryForFailureGroup(RetryForFailureGroup item) { - string RequestId { get; } - RetryType RetryType { get; } - string Originator { get; set; } - string Classifier { get; set; } - DateTime StartTime { get; set; } - Task>> GetDocuments(IAsyncDocumentSession session); + Log.Info($"Enqueuing index based bulk retry '{item}'"); + bulkRequests.Enqueue(item); } - class IndexBasedBulkRetryRequest : IBulkRetryRequest - where TIndex : AbstractIndexCreationTask, new() - where TType : IHaveStatus + readonly IRetryDocumentDataStore store; + readonly RetryingManager operationManager; + readonly ConcurrentQueue bulkRequests = new ConcurrentQueue(); + const int BatchSize = 1000; + + static readonly ILog Log = LogManager.GetLogger(typeof(RetriesGateway)); + + public abstract class BulkRetryRequest { - public IndexBasedBulkRetryRequest(string requestId, RetryType retryType, string originator, string classifier, DateTime startTime, Expression> filter) + public string RequestId { get; } + public RetryType RetryType { get; } + public string Originator { get; } + public string Classifier { get; } + public DateTime StartTime { get; } + + public BulkRetryRequest( + string requestId, + RetryType retryType, + DateTime startTime, + string originator + ) { RequestId = requestId; RetryType = retryType; Originator = originator; - this.filter = filter; StartTime = startTime; - Classifier = classifier; } - public string RequestId { get; set; } - public RetryType RetryType { get; set; } - public string Originator { get; set; } - public string Classifier { get; set; } - public DateTime StartTime { get; set; } + protected abstract Task Invoke(IRetryDocumentDataStore store, Func callback); - public Task>> GetDocuments(IAsyncDocumentSession session) + public async Task, DateTime>> GetRequestedBatches(IRetryDocumentDataStore store) { - var query = session.Query(); + var response = new List(); + var currentBatch = new HashSet(StringComparer.OrdinalIgnoreCase); + var latestAttempt = DateTime.MinValue; + + Task Process(string uniqueMessageId, DateTime latestTimeOfFailure) + { + currentBatch.Add(uniqueMessageId); + + if (currentBatch.Count == BatchSize) + { + response.Add(currentBatch.ToArray()); + + currentBatch.Clear(); + } + + var lastDocumentAttempt = latestTimeOfFailure; + if (lastDocumentAttempt > latestAttempt) + { + latestAttempt = lastDocumentAttempt; + } + + return Task.FromResult(response); + } - query = query.Where(d => d.Status == FailedMessageStatus.Unresolved); + await Invoke(store, Process); - if (filter != null) + if (currentBatch.Count > 0) { - query = query.Where(filter); + response.Add(currentBatch.ToArray()); } - return session.Advanced.StreamAsync(query.TransformWith()); + + return Tuple.Create(response, latestAttempt); + } + } + + class RetryForAllMessages : BulkRetryRequest + { + public RetryForAllMessages() : base(requestId: "All", RetryType.All, DateTime.UtcNow, "all messages") + { + } + + protected override Task Invoke(IRetryDocumentDataStore store, Func callback) + { + return store.GetBatchesForAll(StartTime, callback); + } + } + + class RetryForEndpoint : BulkRetryRequest + { + public string Endpoint { get; } + + public RetryForEndpoint(string endpoint) : base(requestId: endpoint, RetryType.AllForEndpoint, DateTime.UtcNow, originator: $"all messages for endpoint {endpoint}") + { + Endpoint = endpoint; } - Expression> filter; + protected override Task Invoke(IRetryDocumentDataStore store, Func callback) + { + return store.GetBatchesForEndpoint(StartTime, Endpoint, callback); + } } - } - public class FailedMessages_UniqueMessageIdAndTimeOfFailures : AbstractTransformerCreationTask - { - public FailedMessages_UniqueMessageIdAndTimeOfFailures() + public sealed class RetryForFailureGroup : BulkRetryRequest { - TransformResults = failedMessages => from failedMessage in failedMessages - select new - { - failedMessage.UniqueMessageId, - LatestTimeOfFailure = failedMessage.ProcessingAttempts.Max(x => x.FailureDetails.TimeOfFailure) - }; + public string GroupId { get; } + public string GroupTitle { get; } + public string GroupType { get; } + + public RetryForFailureGroup(string groupId, string groupTitle, string groupType, DateTime started) : base(requestId: groupId, RetryType.FailureGroup, started, originator: groupTitle) + { + GroupId = groupId; + GroupType = groupType; + GroupTitle = groupTitle; + } + + protected override Task Invoke(IRetryDocumentDataStore store, Func callback) + { + return store.GetBatchesForFailureGroup( + groupId: GroupId, + groupTitle: GroupTitle, + groupType: GroupType, + cutoff: StartTime, + callback + ); + } } - public struct Result + class RetryForFailedQueueAddress : BulkRetryRequest { - public string UniqueMessageId { get; set; } + public string FailedQueueAddress { get; } + public FailedMessageStatus Status { get; } - public DateTime LatestTimeOfFailure { get; set; } + + public RetryForFailedQueueAddress( + string failedQueueAddress, + FailedMessageStatus status + ) : base(requestId: failedQueueAddress, RetryType.ByQueueAddress, DateTime.UtcNow, originator: $"all messages for failed queue address '{failedQueueAddress}'") + { + FailedQueueAddress = failedQueueAddress; + Status = status; + } + + protected override Task Invoke(IRetryDocumentDataStore store, Func callback) + { + return store.GetBatchesForFailedQueueAddress(StartTime, FailedQueueAddress, Status, callback); + } } } } \ No newline at end of file diff --git a/src/ServiceControl/Recoverability/Retrying/RetryDocumentManager.cs b/src/ServiceControl/Recoverability/Retrying/RetryDocumentManager.cs index 6ffd3529f8..ff798af01b 100644 --- a/src/ServiceControl/Recoverability/Retrying/RetryDocumentManager.cs +++ b/src/ServiceControl/Recoverability/Retrying/RetryDocumentManager.cs @@ -3,123 +3,33 @@ namespace ServiceControl.Recoverability using System; using System.Linq; using System.Threading.Tasks; - using MessageFailures; using Microsoft.Extensions.Hosting; using NServiceBus.Logging; - using Raven.Abstractions.Commands; - using Raven.Abstractions.Data; - using Raven.Abstractions.Exceptions; - using Raven.Client; - using Raven.Client.Linq; - using Raven.Json.Linq; using ServiceControl.Persistence; class RetryDocumentManager { - public RetryDocumentManager(IHostApplicationLifetime applicationLifetime, IDocumentStore store, RetryingManager operationManager) + public RetryDocumentManager(IHostApplicationLifetime applicationLifetime, IRetryDocumentDataStore store, RetryingManager operationManager) { this.store = store; applicationLifetime?.ApplicationStopping.Register(() => { abort = true; }); this.operationManager = operationManager; } - public async Task CreateBatchDocument(string requestId, RetryType retryType, string[] failedMessageRetryIds, string originator, DateTime startTime, DateTime? last = null, string batchName = null, string classifier = null) + public async Task AdoptOrphanedBatches(DateTime cutoff) { - var batchDocumentId = RetryBatch.MakeDocumentId(Guid.NewGuid().ToString()); - using (var session = store.OpenAsyncSession()) - { - await session.StoreAsync(new RetryBatch - { - Id = batchDocumentId, - Context = batchName, - RequestId = requestId, - RetryType = retryType, - Originator = originator, - Classifier = classifier, - StartTime = startTime, - Last = last, - InitialBatchSize = failedMessageRetryIds.Length, - RetrySessionId = RetrySessionId, - FailureRetries = failedMessageRetryIds, - Status = RetryBatchStatus.MarkingDocuments - }).ConfigureAwait(false); - await session.SaveChangesAsync().ConfigureAwait(false); - } - - return batchDocumentId; - } + var orphanedBatches = await store.QueryOrphanedBatches(RetrySessionId, cutoff); - public static ICommandData CreateFailedMessageRetryDocument(string batchDocumentId, string messageId) - { - return new PatchCommandData - { - Patches = patchRequestsEmpty, - PatchesIfMissing = new[] - { - new PatchRequest - { - Name = "FailedMessageId", - Type = PatchCommandType.Set, - Value = FailedMessage.MakeDocumentId(messageId) - }, - new PatchRequest - { - Name = "RetryBatchId", - Type = PatchCommandType.Set, - Value = batchDocumentId - } - }, - Key = FailedMessageRetry.MakeDocumentId(messageId), - Metadata = defaultMetadata - }; - } - - public virtual async Task MoveBatchToStaging(string batchDocumentId) - { - try - { - await store.AsyncDatabaseCommands.PatchAsync(batchDocumentId, - new[] - { - new PatchRequest - { - Type = PatchCommandType.Set, - Name = "Status", - Value = (int)RetryBatchStatus.Staging, - PrevVal = (int)RetryBatchStatus.MarkingDocuments - } - }).ConfigureAwait(false); - } - catch (ConcurrencyException) - { - log.DebugFormat("Ignoring concurrency exception while moving batch to staging {0}", batchDocumentId); - } - } - - public Task RemoveFailedMessageRetryDocument(string uniqueMessageId) - { - return store.AsyncDatabaseCommands.DeleteAsync(FailedMessageRetry.MakeDocumentId(uniqueMessageId), null); - } - - internal async Task AdoptOrphanedBatches(IAsyncDocumentSession session, DateTime cutoff) - { - var orphanedBatches = await session.Query() - .Customize(c => c.BeforeQueryExecution(index => index.Cutoff = cutoff)) - .Where(b => b.Status == RetryBatchStatus.MarkingDocuments && b.RetrySessionId != RetrySessionId) - .Statistics(out var stats) - .ToListAsync() - .ConfigureAwait(false); - - log.Info($"Found {orphanedBatches.Count} orphaned retry batches from previous sessions."); + log.Info($"Found {orphanedBatches.Results.Count} orphaned retry batches from previous sessions."); // let's leave Task.Run for now due to sync sends - await Task.WhenAll(orphanedBatches.Select(b => Task.Run(async () => + await Task.WhenAll(orphanedBatches.Results.Select(b => Task.Run(async () => { log.Info($"Adopting retry batch {b.Id} with {b.FailureRetries.Count} messages."); - await MoveBatchToStaging(b.Id).ConfigureAwait(false); - }))).ConfigureAwait(false); + await MoveBatchToStaging(b.Id); + }))); - foreach (var batch in orphanedBatches) + foreach (var batch in orphanedBatches.Results) { if (batch.RetryType != RetryType.MultipleMessages) { @@ -132,15 +42,17 @@ await Task.WhenAll(orphanedBatches.Select(b => Task.Run(async () => return false; } - return stats.IsStale || orphanedBatches.Any(); + return orphanedBatches.QueryStats.IsStale || orphanedBatches.Results.Any(); } - internal async Task RebuildRetryOperationState(IAsyncDocumentSession session) + public virtual Task MoveBatchToStaging(string batchDocumentId) { - var stagingBatchGroups = await session.Query() - .Where(b => b.HasStagingBatches || b.HasForwardingBatches) - .ToListAsync() - .ConfigureAwait(false); + return store.MoveBatchToStaging(batchDocumentId); + } + + public async Task RebuildRetryOperationState() + { + var stagingBatchGroups = await store.QueryAvailableBatches(); foreach (var group in stagingBatchGroups) { @@ -151,25 +63,16 @@ internal async Task RebuildRetryOperationState(IAsyncDocumentSession session) log.DebugFormat("Rebuilt retry operation status for {0}/{1}. Aggregated batchsize: {2}", group.RetryType, group.RequestId, group.InitialBatchSize); } - await operationManager.PreparedAdoptedBatch(group.RequestId, group.RetryType, group.InitialBatchSize, group.InitialBatchSize, group.Originator, group.Classifier, group.StartTime, group.Last) - .ConfigureAwait(false); + await operationManager.PreparedAdoptedBatch(group.RequestId, group.RetryType, group.InitialBatchSize, group.InitialBatchSize, group.Originator, group.Classifier, group.StartTime, group.Last); } } } - RetryingManager operationManager; - IDocumentStore store; + readonly RetryingManager operationManager; + readonly IRetryDocumentDataStore store; bool abort; - protected static string RetrySessionId = Guid.NewGuid().ToString(); - - static RavenJObject defaultMetadata = RavenJObject.Parse($@" - {{ - ""Raven-Entity-Name"": ""{FailedMessageRetry.CollectionName}"", - ""Raven-Clr-Type"": ""{typeof(FailedMessageRetry).AssemblyQualifiedName}"" - }}"); - - static PatchRequest[] patchRequestsEmpty = new PatchRequest[0]; + public static string RetrySessionId = Guid.NewGuid().ToString(); // TODO: Uplift this into DI? - static ILog log = LogManager.GetLogger(typeof(RetryDocumentManager)); + static readonly ILog log = LogManager.GetLogger(typeof(RetryDocumentManager)); } } \ No newline at end of file diff --git a/src/ServiceControl/Recoverability/Retrying/RetryProcessor.cs b/src/ServiceControl/Recoverability/Retrying/RetryProcessor.cs index a192ab5cf6..84a7fbe6df 100644 --- a/src/ServiceControl/Recoverability/Retrying/RetryProcessor.cs +++ b/src/ServiceControl/Recoverability/Retrying/RetryProcessor.cs @@ -7,21 +7,17 @@ namespace ServiceControl.Recoverability using System.Threading.Tasks; using Infrastructure.DomainEvents; using MessageFailures; - using MessageRedirects; using NServiceBus.Extensibility; using NServiceBus.Logging; using NServiceBus.Routing; using NServiceBus.Support; using NServiceBus.Transport; - using Raven.Abstractions.Commands; - using Raven.Abstractions.Data; - using Raven.Abstractions.Exceptions; - using Raven.Client; + using Persistence.MessageRedirects; using ServiceControl.Persistence; class RetryProcessor { - public RetryProcessor(IDocumentStore store, IDomainEvents domainEvents, ReturnToSenderDequeuer returnToSender, RetryingManager retryingManager) + public RetryProcessor(IRetryBatchesDataStore store, IDomainEvents domainEvents, ReturnToSenderDequeuer returnToSender, RetryingManager retryingManager) { this.store = store; this.returnToSender = returnToSender; @@ -35,12 +31,19 @@ Task Enqueue(IDispatchMessages sender, TransportOperations outgoingMessages) return sender.Dispatch(outgoingMessages, new TransportTransaction(), new ContextBag()); } - public async Task ProcessBatches(IAsyncDocumentSession session, IDispatchMessages sender, CancellationToken cancellationToken = default) + public async Task ProcessBatches(IDispatchMessages sender, CancellationToken cancellationToken = default) { - return await ForwardCurrentBatch(session, cancellationToken).ConfigureAwait(false) || await MoveStagedBatchesToForwardingBatch(session, sender).ConfigureAwait(false); + using (var manager = await store.CreateRetryBatchesManager()) + { + var result = await ForwardCurrentBatch(manager, cancellationToken) || await MoveStagedBatchesToForwardingBatch(manager, sender); + + await manager.SaveChanges(); + + return result; + } } - async Task MoveStagedBatchesToForwardingBatch(IAsyncDocumentSession session, IDispatchMessages sender) + async Task MoveStagedBatchesToForwardingBatch(IRetryBatchesManager manager, IDispatchMessages sender) { try { @@ -51,28 +54,23 @@ async Task MoveStagedBatchesToForwardingBatch(IAsyncDocumentSession sessio isRecoveringFromPrematureShutdown = false; - var stagingBatch = await session.Query() - .Customize(q => q.Include(b => b.FailureRetries)) - .FirstOrDefaultAsync(b => b.Status == RetryBatchStatus.Staging) - .ConfigureAwait(false); + var stagingBatch = await manager.GetStagingBatch(); if (stagingBatch != null) { Log.Info($"Staging batch {stagingBatch.Id}."); - redirects = await MessageRedirectsCollection.GetOrCreate(session).ConfigureAwait(false); - var stagedMessages = await Stage(stagingBatch, session, sender).ConfigureAwait(false); + redirects = await manager.GetOrCreateMessageRedirectsCollection(); + var stagedMessages = await Stage(stagingBatch, manager, sender); var skippedMessages = stagingBatch.InitialBatchSize - stagedMessages; - await retryingManager.Skip(stagingBatch.RequestId, stagingBatch.RetryType, skippedMessages) - .ConfigureAwait(false); + await retryingManager.Skip(stagingBatch.RequestId, stagingBatch.RetryType, skippedMessages); if (stagedMessages > 0) { Log.Info($"Batch {stagingBatch.Id} with {stagedMessages} messages staged and {skippedMessages} skipped ready to be forwarded."); - await session.StoreAsync(new RetryBatchNowForwarding + await manager.Store(new RetryBatchNowForwarding { RetryBatchId = stagingBatch.Id - }, RetryBatchNowForwarding.Id) - .ConfigureAwait(false); + }); } return true; @@ -87,16 +85,14 @@ await session.StoreAsync(new RetryBatchNowForwarding } } - async Task ForwardCurrentBatch(IAsyncDocumentSession session, CancellationToken cancellationToken) + async Task ForwardCurrentBatch(IRetryBatchesManager manager, CancellationToken cancellationToken) { if (Log.IsDebugEnabled) { Log.Debug("Looking for batch to forward."); } - var nowForwarding = await session.Include(r => r.RetryBatchId) - .LoadAsync(RetryBatchNowForwarding.Id) - .ConfigureAwait(false); + var nowForwarding = await manager.GetRetryBatchNowForwarding(); if (nowForwarding != null) { @@ -105,7 +101,7 @@ async Task ForwardCurrentBatch(IAsyncDocumentSession session, Cancellation Log.Debug($"Loading batch {nowForwarding.RetryBatchId} for forwarding."); } - var forwardingBatch = await session.LoadAsync(nowForwarding.RetryBatchId, cancellationToken).ConfigureAwait(false); + var forwardingBatch = await manager.GetRetryBatch(nowForwarding.RetryBatchId, cancellationToken); if (forwardingBatch != null) { @@ -114,8 +110,7 @@ async Task ForwardCurrentBatch(IAsyncDocumentSession session, Cancellation Log.Info($"Forwarding batch {forwardingBatch.Id}."); } - await Forward(forwardingBatch, session, cancellationToken) - .ConfigureAwait(false); + await Forward(forwardingBatch, manager, cancellationToken); if (Log.IsDebugEnabled) { @@ -132,7 +127,7 @@ await Forward(forwardingBatch, session, cancellationToken) Log.Debug("Removing forwarding document."); } - session.Delete(nowForwarding); + manager.Delete(nowForwarding); return true; } @@ -140,20 +135,17 @@ await Forward(forwardingBatch, session, cancellationToken) return false; } - async Task Forward(RetryBatch forwardingBatch, IAsyncDocumentSession session, CancellationToken cancellationToken) + async Task Forward(RetryBatch forwardingBatch, IRetryBatchesManager manager, CancellationToken cancellationToken) { var messageCount = forwardingBatch.FailureRetries.Count; - await retryingManager.Forwarding(forwardingBatch.RequestId, forwardingBatch.RetryType) - .ConfigureAwait(false); + await retryingManager.Forwarding(forwardingBatch.RequestId, forwardingBatch.RetryType); if (isRecoveringFromPrematureShutdown) { Log.Warn($"Recovering from premature shutdown. Starting forwarder for batch {forwardingBatch.Id} in timeout mode."); - await returnToSender.Run(forwardingBatch.Id, IsPartOfStagedBatch(forwardingBatch.StagingId), null, cancellationToken) - .ConfigureAwait(false); - await retryingManager.ForwardedBatch(forwardingBatch.RequestId, forwardingBatch.RetryType, forwardingBatch.InitialBatchSize) - .ConfigureAwait(false); + await returnToSender.Run(forwardingBatch.Id, IsPartOfStagedBatch(forwardingBatch.StagingId), null, cancellationToken); + await retryingManager.ForwardedBatch(forwardingBatch.RequestId, forwardingBatch.RetryType, forwardingBatch.InitialBatchSize); } else { @@ -164,15 +156,13 @@ await retryingManager.ForwardedBatch(forwardingBatch.RequestId, forwardingBatch. else { Log.Info($"Starting forwarder for batch {forwardingBatch.Id} with {messageCount} messages in counting mode."); - await returnToSender.Run(forwardingBatch.Id, IsPartOfStagedBatch(forwardingBatch.StagingId), messageCount, cancellationToken) - .ConfigureAwait(false); + await returnToSender.Run(forwardingBatch.Id, IsPartOfStagedBatch(forwardingBatch.StagingId), messageCount, cancellationToken); } - await retryingManager.ForwardedBatch(forwardingBatch.RequestId, forwardingBatch.RetryType, messageCount) - .ConfigureAwait(false); + await retryingManager.ForwardedBatch(forwardingBatch.RequestId, forwardingBatch.RetryType, messageCount); } - session.Delete(forwardingBatch); + manager.Delete(forwardingBatch); Log.Info($"Done forwarding batch {forwardingBatch.Id}."); } @@ -186,11 +176,11 @@ static Predicate IsPartOfStagedBatch(string stagingId) }; } - async Task Stage(RetryBatch stagingBatch, IAsyncDocumentSession session, IDispatchMessages sender) + async Task Stage(RetryBatch stagingBatch, IRetryBatchesManager manager, IDispatchMessages sender) { var stagingId = Guid.NewGuid().ToString(); - var failedMessageRetryDocs = await session.LoadAsync(stagingBatch.FailureRetries).ConfigureAwait(false); + var failedMessageRetryDocs = await manager.GetFailedMessageRetries(stagingBatch.FailureRetries); var failedMessageRetriesById = failedMessageRetryDocs .Where(r => r != null && r.RetryBatchId == stagingBatch.Id) @@ -201,18 +191,18 @@ async Task Stage(RetryBatch stagingBatch, IAsyncDocumentSession session, ID { if (failedMessageRetry != null) { - session.Advanced.Evict(failedMessageRetry); + manager.Evict(failedMessageRetry); } } if (failedMessageRetriesById.Count == 0) { Log.Info($"Retry batch {stagingBatch.Id} cancelled as all matching unresolved messages are already marked for retry as part of another batch."); - session.Delete(stagingBatch); + manager.Delete(stagingBatch); return 0; } - var failedMessagesDocs = await session.LoadAsync(failedMessageRetriesById.Keys).ConfigureAwait(false); + var failedMessagesDocs = await manager.GetFailedMessages(failedMessageRetriesById.Keys); var messages = failedMessagesDocs.Where(m => m != null).ToArray(); Log.Info($"Staging {messages.Length} messages for retry batch {stagingBatch.Id} with staging attempt Id {stagingId}."); @@ -233,7 +223,7 @@ async Task Stage(RetryBatch stagingBatch, IAsyncDocumentSession session, ID failedMessage.Status = FailedMessageStatus.RetryIssued; } - await TryDispatch(sender, transportOperations, messages, failedMessageRetriesById, stagingId, previousAttemptFailed).ConfigureAwait(false); + await TryDispatch(sender, transportOperations, messages, failedMessageRetriesById, stagingId, previousAttemptFailed); if (stagingBatch.RetryType != RetryType.FailureGroup) //FailureGroup published on completion of entire group { @@ -243,7 +233,7 @@ await domainEvents.Raise(new MessagesSubmittedForRetry FailedMessageIds = failedIds, NumberOfFailedMessages = failedIds.Length, Context = stagingBatch.Context - }).ConfigureAwait(false); + }); } var msgLookup = messages.ToLookup(x => x.Id); @@ -255,7 +245,10 @@ await domainEvents.Raise(new MessagesSubmittedForRetry return messages.Length; } - Task TryDispatch(IDispatchMessages sender, TransportOperation[] transportOperations, IReadOnlyCollection messages, IReadOnlyDictionary failedMessageRetriesById, string stagingId, bool previousAttemptFailed) + Task TryDispatch(IDispatchMessages sender, + TransportOperation[] transportOperations, IReadOnlyCollection messages, + IReadOnlyDictionary failedMessageRetriesById, string stagingId, + bool previousAttemptFailed) { return previousAttemptFailed ? ConcurrentDispatchToTransport(sender, transportOperations, failedMessageRetriesById) : BatchDispatchToTransport(sender, transportOperations, messages, failedMessageRetriesById, stagingId); @@ -271,47 +264,17 @@ Task ConcurrentDispatchToTransport(IDispatchMessages sender, IReadOnlyCollection return Task.WhenAll(tasks); } - async Task BatchDispatchToTransport(IDispatchMessages sender, TransportOperation[] transportOperations, IReadOnlyCollection messages, IReadOnlyDictionary failedMessageRetriesById, string stagingId) + async Task BatchDispatchToTransport(IDispatchMessages sender, + TransportOperation[] transportOperations, IReadOnlyCollection messages, + IReadOnlyDictionary failedMessageRetriesById, string stagingId) { try { - await Enqueue(sender, new TransportOperations(transportOperations)).ConfigureAwait(false); + await Enqueue(sender, new TransportOperations(transportOperations)); } catch (Exception e) { - var commands = new ICommandData[messages.Count]; - var commandIndex = 0; - foreach (var failedMessage in messages) - { - var failedMessageRetry = failedMessageRetriesById[failedMessage.Id]; - - Log.Warn($"Attempt {1} of {MaxStagingAttempts} to stage a retry message {failedMessage.UniqueMessageId} failed", e); - - commands[commandIndex] = new PatchCommandData - { - Patches = new[] - { - new PatchRequest - { - Type = PatchCommandType.Set, - Name = "StageAttempts", - Value = 1 - } - }, - Key = failedMessageRetry.Id - }; - - commandIndex++; - } - - try - { - await store.AsyncDatabaseCommands.BatchAsync(commands).ConfigureAwait(false); - } - catch (ConcurrencyException) - { - Log.DebugFormat("Ignoring concurrency exception while incrementing staging attempt count for {0}", stagingId); - } + await store.RecordFailedStagingAttempt(messages, failedMessageRetriesById, e, MaxStagingAttempts, stagingId); throw new RetryStagingException(e); } @@ -321,7 +284,7 @@ async Task TryStageMessage(IDispatchMessages sender, TransportOperation transpor { try { - await Enqueue(sender, new TransportOperations(transportOperation)).ConfigureAwait(false); + await Enqueue(sender, new TransportOperations(transportOperation)); } catch (Exception e) { @@ -332,48 +295,24 @@ async Task TryStageMessage(IDispatchMessages sender, TransportOperation transpor { Log.Warn($"Attempt {incrementedAttempts} of {MaxStagingAttempts} to stage a retry message {uniqueMessageId} failed", e); - await IncrementAttemptCounter(failedMessageRetry) - .ConfigureAwait(false); + await store.IncrementAttemptCounter(failedMessageRetry); } else { Log.Error($"Retry message {uniqueMessageId} reached its staging retry limit ({MaxStagingAttempts}) and is going to be removed from the batch.", e); - await store.AsyncDatabaseCommands.DeleteAsync(FailedMessageRetry.MakeDocumentId(uniqueMessageId), null) - .ConfigureAwait(false); + await store.DeleteFailedMessageRetry(uniqueMessageId); await domainEvents.Raise(new MessageFailedInStaging { UniqueMessageId = uniqueMessageId - }).ConfigureAwait(false); + }); } throw new RetryStagingException(e); } } - async Task IncrementAttemptCounter(FailedMessageRetry message) - { - try - { - await store.AsyncDatabaseCommands.PatchAsync(message.Id, - new[] - { - new PatchRequest - { - Type = PatchCommandType.Set, - Name = "StageAttempts", - Value = message.StageAttempts + 1 - } - }).ConfigureAwait(false); - } - catch (ConcurrencyException) - { - Log.DebugFormat("Ignoring concurrency exception while incrementing staging attempt count for {0}", message.FailedMessageId); - } - } - - TransportOperation ToTransportOperation(FailedMessage message, string stagingId) { var attempt = message.ProcessingAttempts.Last(); @@ -404,8 +343,8 @@ TransportOperation ToTransportOperation(FailedMessage message, string stagingId) return new TransportOperation(transportMessage, new UnicastAddressTag(returnToSender.InputAddress)); } - IDocumentStore store; IDomainEvents domainEvents; + IRetryBatchesDataStore store; ReturnToSenderDequeuer returnToSender; RetryingManager retryingManager; MessageRedirectsCollection redirects; diff --git a/src/ServiceControl/Recoverability/Retrying/RetryingManager.cs b/src/ServiceControl/Recoverability/Retrying/RetryingManager.cs index fb5d5aa956..89f6251b99 100644 --- a/src/ServiceControl/Recoverability/Retrying/RetryingManager.cs +++ b/src/ServiceControl/Recoverability/Retrying/RetryingManager.cs @@ -41,7 +41,7 @@ public bool IsRetryInProgressFor(string requestId) return retryOperations.Values.Any(o => o.RequestId == requestId && o.IsInProgress()); } - public async Task Prepairing(string requestId, RetryType retryType, int totalNumberOfMessages) + public async Task Preparing(string requestId, RetryType retryType, int totalNumberOfMessages) { if (requestId == null) //legacy support for batches created before operations were introduced { @@ -50,8 +50,7 @@ public async Task Prepairing(string requestId, RetryType retryType, int totalNum var summary = GetOrCreate(retryType, requestId); - await summary.Prepare(totalNumberOfMessages) - .ConfigureAwait(false); + await summary.Prepare(totalNumberOfMessages); } public async Task PreparedAdoptedBatch(string requestId, RetryType retryType, int numberOfMessagesPrepared, int totalNumberOfMessages, string originator, string classifier, DateTime startTime, DateTime last) @@ -63,8 +62,8 @@ public async Task PreparedAdoptedBatch(string requestId, RetryType retryType, in var summary = GetOrCreate(retryType, requestId); - await summary.Prepare(totalNumberOfMessages).ConfigureAwait(false); - await summary.PrepareAdoptedBatch(numberOfMessagesPrepared, originator, classifier, startTime, last).ConfigureAwait(false); + await summary.Prepare(totalNumberOfMessages); + await summary.PrepareAdoptedBatch(numberOfMessagesPrepared, originator, classifier, startTime, last); } public async Task PreparedBatch(string requestId, RetryType retryType, int numberOfMessagesPrepared) @@ -76,7 +75,7 @@ public async Task PreparedBatch(string requestId, RetryType retryType, int numbe var summary = GetOrCreate(retryType, requestId); - await summary.PrepareBatch(numberOfMessagesPrepared).ConfigureAwait(false); + await summary.PrepareBatch(numberOfMessagesPrepared); } public async Task Forwarding(string requestId, RetryType retryType) @@ -88,7 +87,7 @@ public async Task Forwarding(string requestId, RetryType retryType) var summary = GetOrCreate(retryType, requestId); - await summary.Forwarding().ConfigureAwait(false); + await summary.Forwarding(); } public async Task ForwardedBatch(string requestId, RetryType retryType, int numberOfMessagesForwarded) @@ -100,8 +99,7 @@ public async Task ForwardedBatch(string requestId, RetryType retryType, int numb var summary = GetOrCreate(retryType, requestId); - await summary.BatchForwarded(numberOfMessagesForwarded) - .ConfigureAwait(false); + await summary.BatchForwarded(numberOfMessagesForwarded); } public void Fail(RetryType retryType, string requestId) @@ -124,8 +122,7 @@ public async Task Skip(string requestId, RetryType retryType, int numberOfMessag } var summary = GetOrCreate(retryType, requestId); - await summary.Skip(numberOfMessagesSkipped) - .ConfigureAwait(false); + await summary.Skip(numberOfMessagesSkipped); } InMemoryRetry GetOrCreate(RetryType retryType, string requestId) diff --git a/src/ServiceControl/SagaAudit/GetSagaByIdApi.cs b/src/ServiceControl/SagaAudit/GetSagaByIdApi.cs index e773d4e9c9..7fc3e52e8f 100644 --- a/src/ServiceControl/SagaAudit/GetSagaByIdApi.cs +++ b/src/ServiceControl/SagaAudit/GetSagaByIdApi.cs @@ -5,33 +5,20 @@ namespace ServiceControl.SagaAudit using System.Net.Http; using System.Threading.Tasks; using CompositeViews.Messages; - using Raven.Client; using ServiceBus.Management.Infrastructure.Settings; + using ServiceControl.Persistence; using ServiceControl.Persistence.Infrastructure; - class GetSagaByIdApi : ScatterGatherApi + class GetSagaByIdApi : ScatterGatherApi { - public GetSagaByIdApi(IDocumentStore documentStore, Settings settings, Func httpClientFactory) : base(documentStore, settings, httpClientFactory) + public GetSagaByIdApi(ISagaAuditDataStore store, Settings settings, Func httpClientFactory) : base(store, settings, httpClientFactory) { } protected override async Task> LocalQuery(HttpRequestMessage request, Guid input) { - using (var session = Store.OpenAsyncSession()) - { - var sagaHistory = await - session.Query() - .Statistics(out var stats) - .SingleOrDefaultAsync(x => x.SagaId == input) - .ConfigureAwait(false); - - if (sagaHistory == null) - { - return QueryResult.Empty(); - } - - return new QueryResult(sagaHistory, new QueryStatsInfo(stats.IndexEtag, stats.TotalResults)); - } + var result = await DataStore.GetSagaById(input); + return result; } protected override SagaHistory ProcessResults(HttpRequestMessage request, QueryResult[] results) diff --git a/src/ServiceControl/SagaAudit/SagaAuditComponent.cs b/src/ServiceControl/SagaAudit/SagaAuditComponent.cs index f3ce9ce13e..d47027dc51 100644 --- a/src/ServiceControl/SagaAudit/SagaAuditComponent.cs +++ b/src/ServiceControl/SagaAudit/SagaAuditComponent.cs @@ -1,6 +1,5 @@ namespace ServiceControl.SagaAudit { - using CustomChecks; using Microsoft.Extensions.Hosting; using Particular.ServiceControl; using ServiceBus.Management.Infrastructure.Settings; @@ -9,12 +8,11 @@ class SagaAuditComponent : ServiceControlComponent { public override void Configure(Settings settings, IHostBuilder hostBuilder) { - hostBuilder.ConfigureServices(collection => - { - collection.AddCustomCheck(); - }); + // TODO: If this component doesn't do anything, should it even exist? } - public override void Setup(Settings settings, IComponentInstallationContext context) => context.AddIndexAssembly(typeof(SagaDetailsIndex).Assembly); + public override void Setup(Settings settings, IComponentInstallationContext context) + { + } } } \ No newline at end of file diff --git a/src/ServiceControl/SagaAudit/SagaUpdatedHandler.cs b/src/ServiceControl/SagaAudit/SagaUpdatedHandler.cs index 1ef9179162..9b592618a6 100644 --- a/src/ServiceControl/SagaAudit/SagaUpdatedHandler.cs +++ b/src/ServiceControl/SagaAudit/SagaUpdatedHandler.cs @@ -3,26 +3,22 @@ using System.Threading.Tasks; using EndpointPlugin.Messages.SagaState; using NServiceBus; - using Raven.Client; + using ServiceControl.Persistence; class SagaUpdatedHandler : IHandleMessages { - public SagaUpdatedHandler(IDocumentStore store) + public SagaUpdatedHandler(ISagaAuditDataStore store) { this.store = store; } - public async Task Handle(SagaUpdatedMessage message, IMessageHandlerContext context) + public Task Handle(SagaUpdatedMessage message, IMessageHandlerContext context) { var sagaSnapshot = SagaSnapshotFactory.Create(message); - using (var session = store.OpenAsyncSession()) - { - await session.StoreAsync(sagaSnapshot).ConfigureAwait(false); - await session.SaveChangesAsync().ConfigureAwait(false); - } + return store.StoreSnapshot(sagaSnapshot); } - readonly IDocumentStore store; + readonly ISagaAuditDataStore store; } } \ No newline at end of file diff --git a/src/ServiceControl/ServiceControl.csproj b/src/ServiceControl/ServiceControl.csproj index 197fa21f8b..a9be8ce4b8 100644 --- a/src/ServiceControl/ServiceControl.csproj +++ b/src/ServiceControl/ServiceControl.csproj @@ -8,8 +8,15 @@ + + + + + + + @@ -19,7 +26,6 @@ - @@ -45,11 +51,6 @@ - - - - - diff --git a/src/ServiceControl/SetupBootstrapper.cs b/src/ServiceControl/SetupBootstrapper.cs index ed1570496d..d5bdaeadef 100644 --- a/src/ServiceControl/SetupBootstrapper.cs +++ b/src/ServiceControl/SetupBootstrapper.cs @@ -1,12 +1,10 @@ namespace Particular.ServiceControl { using System.Threading.Tasks; - using global::ServiceControl.Infrastructure.RavenDB; using global::ServiceControl.LicenseManagement; + using global::ServiceControl.Persistence; using global::ServiceControl.Transports; using NServiceBus.Logging; - using Raven.Client.Embedded; - using ServiceBus.Management.Infrastructure; using ServiceBus.Management.Infrastructure.Installers; using ServiceBus.Management.Infrastructure.Settings; @@ -34,18 +32,16 @@ public async Task Run(string username) foreach (var installationTask in componentSetupContext.InstallationTasks) { - await installationTask().ConfigureAwait(false); + await installationTask(); } - if (!settings.RunInMemory) //RunInMemory is used in acceptance tests - { - using (var documentStore = new EmbeddableDocumentStore()) - { - RavenBootstrapper.Configure(documentStore, settings); - var service = new EmbeddedRavenDbHostedService(documentStore, new IDataMigration[0], componentSetupContext); - await service.SetupDatabase().ConfigureAwait(false); - } - } + var persistenceConfiguration = PersistenceConfigurationFactory.LoadPersistenceConfiguration(settings.PersistenceType); + var persistenceSettings = persistenceConfiguration.BuildPersistenceSettings(settings); + var persistence = persistenceConfiguration.Create(persistenceSettings); + var installer = persistence.CreateInstaller(); + + await installer.Install(); + EventSourceCreator.Create(); if (settings.SkipQueueCreation) @@ -60,7 +56,7 @@ public async Task Run(string username) await transportCustomization.ProvisionQueues( username, transportSettings, - componentSetupContext.Queues).ConfigureAwait(false); + componentSetupContext.Queues); } } diff --git a/src/ServiceControlInstaller.Packaging.UnitTests/PrimaryDeploymentPackageTests.cs b/src/ServiceControlInstaller.Packaging.UnitTests/PrimaryDeploymentPackageTests.cs index 4e1c46a343..89fc21e73f 100644 --- a/src/ServiceControlInstaller.Packaging.UnitTests/PrimaryDeploymentPackageTests.cs +++ b/src/ServiceControlInstaller.Packaging.UnitTests/PrimaryDeploymentPackageTests.cs @@ -1,5 +1,6 @@ namespace Tests { + using System.IO; using System.Linq; using NUnit.Framework; @@ -14,7 +15,8 @@ public PrimaryDeploymentPackageTests() [Test] public void Should_bundle_ravendb35_persister() { - FileAssert.Exists($"{deploymentPackage.Directory.FullName}/ServiceControl/ServiceControl.Persistence.RavenDb.dll", "RavenDB 3.5 persister should be bundled"); + var persistenceAssemblyPath = Path.Combine(deploymentPackage.Directory.FullName, "Persisters", "RavenDB35", "ServiceControl.Persistence.RavenDb.dll"); + FileAssert.Exists(persistenceAssemblyPath, "RavenDB 3.5 persister should be bundled"); } readonly DeploymentPackage deploymentPackage; diff --git a/src/TestHelper/IncludeInTestCategoryAttribute.cs b/src/TestHelper/IncludeInTestCategoryAttribute.cs new file mode 100644 index 0000000000..66179a1ad7 --- /dev/null +++ b/src/TestHelper/IncludeInTestCategoryAttribute.cs @@ -0,0 +1,9 @@ +public class IncludeInTestCategoryAttribute : IncludeInTestsAttribute +{ + public IncludeInTestCategoryAttribute(string filter) + { + Filter = filter; + } + + protected override string Filter { get; } +} diff --git a/src/TestHelper/IncludeInTestsAttribute.cs b/src/TestHelper/IncludeInTestsAttribute.cs index 5b4a693c94..eeea538267 100644 --- a/src/TestHelper/IncludeInTestsAttribute.cs +++ b/src/TestHelper/IncludeInTestsAttribute.cs @@ -1,5 +1,4 @@ using System; -using System.Linq; using NUnit.Framework; using NUnit.Framework.Interfaces; using NUnit.Framework.Internal;