From 1617cb1f293dfda06c850fa3e2f49465b13979b6 Mon Sep 17 00:00:00 2001 From: John Simons Date: Mon, 28 Sep 2026 11:15:28 +1000 Subject: [PATCH 1/2] Remove InMemory persistence from ServiceControl.Audit The InMemory persistence was serving as a lightweight option for tests and local development, but it masked persistence-specific behaviors (e.g. ETag versioning, maintenance mode) and required test exclusions to paper over the differences. All audit acceptance and persistence tests now run exclusively against RavenDB. This also brings it inline with what is in the primary instance. --- README.md | 2 +- src/ProjectReferences.Persisters.Audit.props | 1 - .../AcceptanceTestStorageConfiguration.cs | 17 +- ...ntrol.Audit.AcceptanceTests.RavenDB.csproj | 9 +- .../AcceptanceTest.cs | 2 +- .../AcceptanceTestStorageConfiguration.cs | 23 -- .../MaintenanceModeTests.cs | 8 +- ...erviceControl.Audit.AcceptanceTests.csproj | 31 --- .../IAcceptanceTestStorageConfiguration.cs | 17 ++ .../ServiceControlComponentBehavior.cs | 2 +- .../ServiceControlComponentRunner.cs | 4 +- .../.editorconfig | 4 - .../InMemoryAttachmentsBodyStorage.cs | 73 ------ .../InMemoryAuditDataStore.cs | 248 ------------------ .../InMemoryAuditIngestionUnitOfWork.cs | 32 --- ...InMemoryAuditIngestionUnitOfWorkFactory.cs | 20 -- .../InMemoryFailedAuditStorage.cs | 46 ---- .../InMemoryPersistence.cs | 25 -- .../InMemoryPersistenceConfiguration.cs | 15 -- .../MessagesViewFactory.cs | 61 ----- ...eControl.Audit.Persistence.InMemory.csproj | 20 -- .../persistence.manifest | 7 - .../PersistenceTestsConfiguration.cs | 2 +- ...rol.Audit.Persistence.Tests.RavenDB.csproj | 3 +- .../IPersistenceTestsConfiguration.cs | 29 ++ .../InMemory/BodyVersionTests.cs | 43 --- .../InMemory/PersistenceTestsConfiguration.cs | 70 ----- .../PersistenceTestFixture.cs | 4 + ...viceControl.Audit.Persistence.Tests.csproj | 32 --- .../DevelopmentPersistenceLocations.cs | 1 - .../ServiceControl.Audit.Persistence.csproj | 2 +- .../API/APIApprovals.cs | 2 +- ...rovals.PlatformSampleSettings.approved.txt | 2 +- .../PersistenceManifestLibraryTests.cs | 21 +- .../Infrastructure/When_instance_is_setup.cs | 31 ++- .../ServiceControl.Audit.UnitTests.csproj | 1 - .../Verification/CustomChecksTest.cs | 2 +- src/ServiceControl.Audit/App.config | 3 +- .../ServiceControl.Audit.csproj | 1 - ...ntrol.MultiInstance.AcceptanceTests.csproj | 9 +- .../ServiceControlComponentRunner.cs | 8 +- .../SharedRavenStorageConfiguration.cs | 79 ++++++ .../ServiceControl.Persistence.RavenDB.csproj | 1 + src/ServiceControl.slnx | 3 - ...expected_config_file.InMemory.approved.txt | 14 - 45 files changed, 210 insertions(+), 820 deletions(-) delete mode 100644 src/ServiceControl.Audit.AcceptanceTests/AcceptanceTestStorageConfiguration.cs rename src/{ServiceControl.Audit.UnitTests/Infrastructure => ServiceControl.Audit.AcceptanceTests}/MaintenanceModeTests.cs (73%) delete mode 100644 src/ServiceControl.Audit.AcceptanceTests/ServiceControl.Audit.AcceptanceTests.csproj create mode 100644 src/ServiceControl.Audit.AcceptanceTests/TestSupport/IAcceptanceTestStorageConfiguration.cs delete mode 100644 src/ServiceControl.Audit.Persistence.InMemory/.editorconfig delete mode 100644 src/ServiceControl.Audit.Persistence.InMemory/InMemoryAttachmentsBodyStorage.cs delete mode 100644 src/ServiceControl.Audit.Persistence.InMemory/InMemoryAuditDataStore.cs delete mode 100644 src/ServiceControl.Audit.Persistence.InMemory/InMemoryAuditIngestionUnitOfWork.cs delete mode 100644 src/ServiceControl.Audit.Persistence.InMemory/InMemoryAuditIngestionUnitOfWorkFactory.cs delete mode 100644 src/ServiceControl.Audit.Persistence.InMemory/InMemoryFailedAuditStorage.cs delete mode 100644 src/ServiceControl.Audit.Persistence.InMemory/InMemoryPersistence.cs delete mode 100644 src/ServiceControl.Audit.Persistence.InMemory/InMemoryPersistenceConfiguration.cs delete mode 100644 src/ServiceControl.Audit.Persistence.InMemory/MessagesViewFactory.cs delete mode 100644 src/ServiceControl.Audit.Persistence.InMemory/ServiceControl.Audit.Persistence.InMemory.csproj delete mode 100644 src/ServiceControl.Audit.Persistence.InMemory/persistence.manifest create mode 100644 src/ServiceControl.Audit.Persistence.Tests/IPersistenceTestsConfiguration.cs delete mode 100644 src/ServiceControl.Audit.Persistence.Tests/InMemory/BodyVersionTests.cs delete mode 100644 src/ServiceControl.Audit.Persistence.Tests/InMemory/PersistenceTestsConfiguration.cs delete mode 100644 src/ServiceControl.Audit.Persistence.Tests/ServiceControl.Audit.Persistence.Tests.csproj rename src/{ServiceControl.Audit.Persistence.Tests => ServiceControl.Audit.UnitTests/Infrastructure}/PersistenceManifestLibraryTests.cs (86%) create mode 100644 src/ServiceControl.MultiInstance.AcceptanceTests/TestSupport/SharedRavenStorageConfiguration.cs delete mode 100644 src/ServiceControlInstaller.Engine.UnitTests/ApprovalFiles/InstallationTests.Audit_install_should_write_expected_config_file.InMemory.approved.txt diff --git a/README.md b/README.md index dbdd477cb5..f2e4c32b88 100644 --- a/README.md +++ b/README.md @@ -11,7 +11,7 @@ ServiceControl, ServiceControl.Audit, and ServiceControl.Monitoring can be run/d - Edit the `app.config` file of the instance type that needs to be run/debugged to select which transport and persistence to use. - The configuration file contains commented settings for each supported transport and persistence. It also provides some guidance on additional required settings for specific persisters. - ServiceControl works with a RavenDB persistence - - ServiceControl.Audit can work with RavenDB or an InMemory persistence + - ServiceControl.Audit works with a RavenDB persistence - Run or debug the project as usual A video demo showing how to set it up is available on the Particular YouTube channel: diff --git a/src/ProjectReferences.Persisters.Audit.props b/src/ProjectReferences.Persisters.Audit.props index 0a9c4d0dcb..e729ddae3d 100644 --- a/src/ProjectReferences.Persisters.Audit.props +++ b/src/ProjectReferences.Persisters.Audit.props @@ -1,7 +1,6 @@ - diff --git a/src/ServiceControl.Audit.AcceptanceTests.RavenDB/AcceptanceTestStorageConfiguration.cs b/src/ServiceControl.Audit.AcceptanceTests.RavenDB/AcceptanceTestStorageConfiguration.cs index 734ec5ba45..2a34743384 100644 --- a/src/ServiceControl.Audit.AcceptanceTests.RavenDB/AcceptanceTestStorageConfiguration.cs +++ b/src/ServiceControl.Audit.AcceptanceTests.RavenDB/AcceptanceTestStorageConfiguration.cs @@ -1,16 +1,17 @@ -namespace ServiceControl.Audit.AcceptanceTests +namespace ServiceControl.Audit.AcceptanceTests.RavenDB { using System; using System.Collections.Generic; using System.Threading; using System.Threading.Tasks; using Nito.Disposables; + using ServiceControl.Audit.AcceptanceTests.TestSupport; using ServiceControl.Audit.Persistence.RavenDB; using ServiceControl.Audit.Persistence.Tests; using ServiceControl.RavenDB; using TestHelper; - public class AcceptanceTestStorageConfiguration + public class AcceptanceTestStorageConfiguration : IAcceptanceTestStorageConfiguration { public string PersistenceType { get; } = "RavenDB"; @@ -18,10 +19,10 @@ public class AcceptanceTestStorageConfiguration string databaseName; static readonly SemaphoreSlim databaseLifecycleLock = new SemaphoreSlim(1, 1); - public async Task> CustomizeSettings() + public async Task> CustomizeSettings(CancellationToken cancellationToken = default) { databaseName = Guid.NewGuid().ToString("n"); - databaseInstance = await SharedEmbeddedServer.GetInstance(); + databaseInstance = await SharedEmbeddedServer.GetInstance(cancellationToken); return new Dictionary { @@ -30,21 +31,21 @@ public async Task> CustomizeSettings() }; } - public async Task Cleanup() + public async Task Cleanup(CancellationToken cancellationToken = default) { if (databaseInstance == null) { return; } - using var _ = await UseDatabaseLifecycleLock(); - await databaseInstance.DeleteDatabase(databaseName); + using var _ = await UseDatabaseLifecycleLock(cancellationToken); + await databaseInstance.DeleteDatabase(databaseName, cancellationToken); } /// /// The shared server cannot perform database lifecycle operations in parallel, take this lock when you /// need to do one of these operations in a test. /// - public static async Task UseDatabaseLifecycleLock(CancellationToken cancellationToken = default) + public async Task UseDatabaseLifecycleLock(CancellationToken cancellationToken = default) { await databaseLifecycleLock.WaitAsync(cancellationToken); return Disposable.Create(() => databaseLifecycleLock.Release()); diff --git a/src/ServiceControl.Audit.AcceptanceTests.RavenDB/ServiceControl.Audit.AcceptanceTests.RavenDB.csproj b/src/ServiceControl.Audit.AcceptanceTests.RavenDB/ServiceControl.Audit.AcceptanceTests.RavenDB.csproj index a22d55be05..ea2e5b6579 100644 --- a/src/ServiceControl.Audit.AcceptanceTests.RavenDB/ServiceControl.Audit.AcceptanceTests.RavenDB.csproj +++ b/src/ServiceControl.Audit.AcceptanceTests.RavenDB/ServiceControl.Audit.AcceptanceTests.RavenDB.csproj @@ -5,6 +5,10 @@ Raven + + + + @@ -24,9 +28,12 @@ - + + + + diff --git a/src/ServiceControl.Audit.AcceptanceTests/AcceptanceTest.cs b/src/ServiceControl.Audit.AcceptanceTests/AcceptanceTest.cs index 133e7e805a..dafe95f451 100644 --- a/src/ServiceControl.Audit.AcceptanceTests/AcceptanceTest.cs +++ b/src/ServiceControl.Audit.AcceptanceTests/AcceptanceTest.cs @@ -72,7 +72,7 @@ public Task Teardown() protected Action> SetStorageConfiguration = _ => { }; protected Action CustomizeHostBuilder = _ => { }; protected ITransportIntegration TransportIntegration; - protected AcceptanceTestStorageConfiguration StorageConfiguration; + protected IAcceptanceTestStorageConfiguration StorageConfiguration; ServiceControlComponentBehavior serviceControlRunnerBehavior; TextWriterTraceListener textWriterTraceListener; diff --git a/src/ServiceControl.Audit.AcceptanceTests/AcceptanceTestStorageConfiguration.cs b/src/ServiceControl.Audit.AcceptanceTests/AcceptanceTestStorageConfiguration.cs deleted file mode 100644 index 41f816339d..0000000000 --- a/src/ServiceControl.Audit.AcceptanceTests/AcceptanceTestStorageConfiguration.cs +++ /dev/null @@ -1,23 +0,0 @@ -namespace ServiceControl.Audit.AcceptanceTests -{ - using System; - using System.Collections.Generic; - using System.Threading; - using System.Threading.Tasks; - - public class AcceptanceTestStorageConfiguration - { - public string PersistenceType { get; } = "InMemory"; - - public Task> CustomizeSettings() => Task.FromResult>(new Dictionary()); - - public Task Cleanup() => Task.CompletedTask; - - public static Task UseDatabaseLifecycleLock() => Task.FromResult(new EmptyDisposable()); - - class EmptyDisposable : IDisposable - { - public void Dispose() { } - } - } -} \ No newline at end of file diff --git a/src/ServiceControl.Audit.UnitTests/Infrastructure/MaintenanceModeTests.cs b/src/ServiceControl.Audit.AcceptanceTests/MaintenanceModeTests.cs similarity index 73% rename from src/ServiceControl.Audit.UnitTests/Infrastructure/MaintenanceModeTests.cs rename to src/ServiceControl.Audit.AcceptanceTests/MaintenanceModeTests.cs index e98b6e1543..85e4ea25fd 100644 --- a/src/ServiceControl.Audit.UnitTests/Infrastructure/MaintenanceModeTests.cs +++ b/src/ServiceControl.Audit.AcceptanceTests/MaintenanceModeTests.cs @@ -1,4 +1,4 @@ -namespace ServiceControl.Audit.UnitTests.Infrastructure +namespace ServiceControl.Audit.AcceptanceTests { using System; using System.Runtime.Loader; @@ -8,12 +8,12 @@ namespace ServiceControl.Audit.UnitTests.Infrastructure using Audit.Infrastructure.Settings; using NUnit.Framework; - class MaintenanceModeTests + class MaintenanceModeTests : AcceptanceTest { [Test] public void Should_refuse_unsupported_persister_before_starting_host() { - var settings = new Settings(persisterType: "InMemory") + var settings = new Settings(persisterType: StorageConfiguration.PersistenceType) { AssemblyLoadContextResolver = static _ => AssemblyLoadContext.Default }; @@ -21,7 +21,7 @@ public void Should_refuse_unsupported_persister_before_starting_host() var exception = Assert.ThrowsAsync(async () => await new MaintenanceModeCommand().Execute(new HostArguments([]), settings)); - Assert.That(exception.Message, Does.Contain("Maintenance mode is not supported").And.Contain("InMemory")); + Assert.That(exception.Message, Does.Contain("Maintenance mode is not supported").And.Contain(StorageConfiguration.PersistenceType)); } } } diff --git a/src/ServiceControl.Audit.AcceptanceTests/ServiceControl.Audit.AcceptanceTests.csproj b/src/ServiceControl.Audit.AcceptanceTests/ServiceControl.Audit.AcceptanceTests.csproj deleted file mode 100644 index a36762973f..0000000000 --- a/src/ServiceControl.Audit.AcceptanceTests/ServiceControl.Audit.AcceptanceTests.csproj +++ /dev/null @@ -1,31 +0,0 @@ - - - - net10.0 - Raven - - - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/src/ServiceControl.Audit.AcceptanceTests/TestSupport/IAcceptanceTestStorageConfiguration.cs b/src/ServiceControl.Audit.AcceptanceTests/TestSupport/IAcceptanceTestStorageConfiguration.cs new file mode 100644 index 0000000000..ad8c43e24b --- /dev/null +++ b/src/ServiceControl.Audit.AcceptanceTests/TestSupport/IAcceptanceTestStorageConfiguration.cs @@ -0,0 +1,17 @@ +namespace ServiceControl.Audit.AcceptanceTests.TestSupport; + +using System; +using System.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; + +public interface IAcceptanceTestStorageConfiguration +{ + string PersistenceType { get; } + + Task> CustomizeSettings(CancellationToken cancellationToken = default); + + Task Cleanup(CancellationToken cancellationToken = default); + + Task UseDatabaseLifecycleLock(CancellationToken cancellationToken = default); +} diff --git a/src/ServiceControl.Audit.AcceptanceTests/TestSupport/ServiceControlComponentBehavior.cs b/src/ServiceControl.Audit.AcceptanceTests/TestSupport/ServiceControlComponentBehavior.cs index b95cdd3d62..5165260070 100644 --- a/src/ServiceControl.Audit.AcceptanceTests/TestSupport/ServiceControlComponentBehavior.cs +++ b/src/ServiceControl.Audit.AcceptanceTests/TestSupport/ServiceControlComponentBehavior.cs @@ -13,7 +13,7 @@ namespace ServiceControl.Audit.AcceptanceTests.TestSupport class ServiceControlComponentBehavior( ITransportIntegration transportToUse, - AcceptanceTestStorageConfiguration persistenceToUse, + IAcceptanceTestStorageConfiguration persistenceToUse, Action setSettings, Action customConfiguration, Action> setStorageConfiguration, diff --git a/src/ServiceControl.Audit.AcceptanceTests/TestSupport/ServiceControlComponentRunner.cs b/src/ServiceControl.Audit.AcceptanceTests/TestSupport/ServiceControlComponentRunner.cs index 61ba9459cb..81d73b1386 100644 --- a/src/ServiceControl.Audit.AcceptanceTests/TestSupport/ServiceControlComponentRunner.cs +++ b/src/ServiceControl.Audit.AcceptanceTests/TestSupport/ServiceControlComponentRunner.cs @@ -29,7 +29,7 @@ namespace ServiceControl.Audit.AcceptanceTests.TestSupport public class ServiceControlComponentRunner( ITransportIntegration transportToUse, - AcceptanceTestStorageConfiguration persistenceToUse, + IAcceptanceTestStorageConfiguration persistenceToUse, Action setSettings, Action customConfiguration, Action> setStorageConfiguration, @@ -49,7 +49,7 @@ async Task InitializeServiceControl(ScenarioContext context) // 1. ConfigurationManager.AppSettings is process-global and not thread-safe // 2. The embedded RavenDB server does not support concurrent database create/delete/index operations // The test scenario execution (after this method returns) still runs in parallel. - using var _ = await AcceptanceTestStorageConfiguration.UseDatabaseLifecycleLock(); + using var _ = await persistenceToUse.UseDatabaseLifecycleLock(); await InitializeServiceControlCore(context); } diff --git a/src/ServiceControl.Audit.Persistence.InMemory/.editorconfig b/src/ServiceControl.Audit.Persistence.InMemory/.editorconfig deleted file mode 100644 index ff993b49bb..0000000000 --- a/src/ServiceControl.Audit.Persistence.InMemory/.editorconfig +++ /dev/null @@ -1,4 +0,0 @@ -[*.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 deleted file mode 100644 index c25dc228cc..0000000000 --- a/src/ServiceControl.Audit.Persistence.InMemory/InMemoryAttachmentsBodyStorage.cs +++ /dev/null @@ -1,73 +0,0 @@ -namespace ServiceControl.Audit.Persistence.InMemory -{ - using System; - using System.Collections.Generic; - using System.IO; - using System.Linq; - using System.Threading; - using System.Threading.Tasks; - using ServiceControl.Audit.Auditing.BodyStorage; - using ServiceControl.Audit.Persistence; - using ServiceControl.Infrastructure; - - class InMemoryAttachmentsBodyStorage : IBodyStorage - { - List messageBodies; - - public InMemoryAttachmentsBodyStorage() - { - messageBodies = []; - } - - public Task Store(string bodyId, string contentType, int bodySize, Stream bodyStream, CancellationToken cancellationToken = default) - { - var messageBody = messageBodies.FirstOrDefault(w => w.BodyId == bodyId); - - var needToAdd = false; - if (messageBody == null) - { - messageBody = new MessageBody() { BodyId = bodyId }; - needToAdd = true; - } - - messageBody.BodySize = bodySize; - - using (var reader = new BinaryReader(bodyStream)) - { - messageBody.Content = reader.ReadBytes(bodySize); - } - - messageBody.ContentType = contentType; - messageBody.Version = DataVersion.FromToken(Guid.NewGuid().ToString("N")); - - if (needToAdd) - { - messageBodies.Add(messageBody); - } - - return Task.CompletedTask; - } - - public Task TryFetch(string bodyId, CancellationToken cancellationToken = default) - { - var messageBody = messageBodies.FirstOrDefault(w => w.BodyId == bodyId); - - return Task.FromResult(messageBody == null - ? MessageBodyView.NotFound() - : MessageBodyView.FromStream( - new MemoryStream(messageBody.Content), - messageBody.ContentType, - messageBody.BodySize, - messageBody.Version)); - } - - class MessageBody - { - public string BodyId { get; set; } - public string ContentType { get; set; } - public int BodySize { get; set; } - public byte[] Content { get; set; } - public DataVersion Version { get; set; } - } - } -} \ No newline at end of file diff --git a/src/ServiceControl.Audit.Persistence.InMemory/InMemoryAuditDataStore.cs b/src/ServiceControl.Audit.Persistence.InMemory/InMemoryAuditDataStore.cs deleted file mode 100644 index 5d34f109fc..0000000000 --- a/src/ServiceControl.Audit.Persistence.InMemory/InMemoryAuditDataStore.cs +++ /dev/null @@ -1,248 +0,0 @@ -namespace ServiceControl.Audit.Persistence.InMemory -{ - using System; - using System.Collections.Generic; - using System.Linq; - using System.Threading; - using System.Threading.Tasks; - using ServiceControl.Audit.Auditing; - using ServiceControl.Audit.Auditing.BodyStorage; - using ServiceControl.Audit.Auditing.MessagesView; - using ServiceControl.Audit.Infrastructure; - using ServiceControl.Audit.Persistence; - using ServiceControl.Infrastructure; - using ServiceControl.SagaAudit; - - class InMemoryAuditDataStore : IAuditMessagesViewDataStore, ISagaHistoryDataStore - { - IBodyStorage bodyStorage; - public List failedAuditImports; - - public InMemoryAuditDataStore(IBodyStorage bodyStorage) - { - this.bodyStorage = bodyStorage; - sagaHistories = []; - messageViews = []; - processedMessages = []; - failedAuditImports = []; - } - - public async Task> QuerySagaHistoryById(Guid input, CancellationToken cancellationToken = default) - { - var sagaHistory = sagaHistories.FirstOrDefault(w => w.SagaId == input); - - if (sagaHistory == null) - { - return await Task.FromResult(QueryResult.Empty()); - } - - return await Task.FromResult(new QueryResult(sagaHistory, new QueryStatsInfo(DataVersion.None, 1))); - } - - public async Task>> GetMessages(bool includeSystemMessages, PagingInfo pagingInfo, SortInfo sortInfo, DateTimeRange timeSentRange, CancellationToken cancellationToken = default) - { - var matched = messageViews - .Where(w => (!w.IsSystemMessage || includeSystemMessages) && - (timeSentRange == null || !timeSentRange.From.HasValue || w.TimeSent >= timeSentRange.From.Value) && - (timeSentRange == null || !timeSentRange.To.HasValue || w.TimeSent <= timeSentRange.To.Value)) - .ToList(); - - return await Task.FromResult(new QueryResult>(matched, new QueryStatsInfo(DataVersion.None, matched.Count))); - } - - public async Task>> QueryMessages(string keyword, PagingInfo pagingInfo, SortInfo sortInfo, DateTimeRange timeSentRange, CancellationToken cancellationToken = default) - { - var messages = GetMessageIdsMatchingQuery(keyword); - - var matched = messageViews - .Where(w => messages.Contains(w.MessageId) && - (timeSentRange == null || !timeSentRange.From.HasValue || w.TimeSent >= timeSentRange.From.Value) && - (timeSentRange == null || !timeSentRange.To.HasValue || w.TimeSent <= timeSentRange.To.Value)) - .ToList(); - return await Task.FromResult(new QueryResult>(matched, new QueryStatsInfo(DataVersion.None, matched.Count()))); - } - - public async Task>> QueryMessagesByReceivingEndpointAndKeyword(string endpoint, string keyword, PagingInfo pagingInfo, SortInfo sortInfo, DateTimeRange timeSentRange, CancellationToken cancellationToken = default) - { - var messages = GetMessageIdsMatchingQuery(keyword); - - var matched = messageViews.Where(w => w.ReceivingEndpoint.Name == endpoint && messages.Contains(w.MessageId) && - (timeSentRange == null || !timeSentRange.From.HasValue || w.TimeSent >= timeSentRange.From.Value) && - (timeSentRange == null || !timeSentRange.To.HasValue || w.TimeSent <= timeSentRange.To.Value)) - .ToList(); - return await Task.FromResult(new QueryResult>(matched, new QueryStatsInfo(DataVersion.None, matched.Count))); - } - - public async Task>> QueryMessagesByReceivingEndpoint(bool includeSystemMessages, string endpointName, PagingInfo pagingInfo, SortInfo sortInfo, DateTimeRange timeSentRange, CancellationToken cancellationToken = default) - { - var matched = messageViews.Where(w => w.ReceivingEndpoint.Name == endpointName && - (timeSentRange == null || !timeSentRange.From.HasValue || w.TimeSent >= timeSentRange.From.Value) && - (timeSentRange == null || !timeSentRange.To.HasValue || w.TimeSent <= timeSentRange.To.Value)) - .ToList(); - return await Task.FromResult(new QueryResult>(matched, new QueryStatsInfo(DataVersion.None, matched.Count))); - } - - public async Task>> QueryMessagesByConversationId(string conversationId, PagingInfo pagingInfo, SortInfo sortInfo, CancellationToken cancellationToken = default) - { - var matched = messageViews.Where(w => w.ConversationId == conversationId).ToList(); - return await Task.FromResult(new QueryResult>(matched, new QueryStatsInfo(DataVersion.None, matched.Count))); - } - - public async Task GetMessageBody(string messageId, CancellationToken cancellationToken = default) - { - var result = await GetMessageBodyFromMetadata(messageId, cancellationToken); - - if (!result.Found) - { - var fromAttachments = await GetMessageBodyFromAttachments(messageId, cancellationToken); - if (fromAttachments.Found) - { - return fromAttachments; - } - } - - return result; - } - - IList GetMessageIdsMatchingQuery(string keyword) - { - return processedMessages - .Where(pm => - { - if ((pm.MessageMetadata["MessageId"] as string) == keyword) - { - return true; - } - - if (TryGet(pm.MessageMetadata, "MessageType") is string messageType && messageType.Contains(keyword)) - { - return true; - } - - if (pm.Headers.Values.Contains(keyword)) - { - return true; - } - - return pm.MessageMetadata.ContainsKey("Body") && (pm.MessageMetadata["Body"] as string).Contains(keyword); - }) - .Select(pm => pm.MessageMetadata["MessageId"] as string) - .ToList(); - } - - async Task GetMessageBodyFromAttachments(string messageId, CancellationToken cancellationToken) - { - return await bodyStorage.TryFetch(messageId, cancellationToken); - } - - Task GetMessageBodyFromMetadata(string messageId, CancellationToken cancellationToken) - { - var message = processedMessages.FirstOrDefault(pm => (pm.MessageMetadata["MessageId"] as string) == messageId); - - if (message == null) - { - return Task.FromResult(MessageBodyView.NotFound()); - } - - var body = !string.IsNullOrEmpty(message.Body) ? message.Body : TryGet(message.MessageMetadata, "Body") as string; - var bodySize = (int?)TryGet(message.MessageMetadata, "ContentLength") ?? 0; - var contentType = TryGet(message.MessageMetadata, "ContentType") as string; - var bodyNotStored = message.MessageMetadata.ContainsKey("BodyNotStored") && (bool)message.MessageMetadata["BodyNotStored"]; - - if (bodyNotStored && body == null) - { - return Task.FromResult(MessageBodyView.NoContent()); - } - - if (body == null) - { - return Task.FromResult(MessageBodyView.NotFound()); - } - - return Task.FromResult(MessageBodyView.FromString(body, contentType, bodySize, DataVersion.FromToken(messageId))); - } - - public Task>> QueryAuditCounts(string endpointName, CancellationToken cancellationToken = default) - { - var hasSent = messageViews.Any(m => m.SendingEndpoint?.Name == endpointName); - var results = messageViews - .Where(m => m.ReceivingEndpoint.Name == endpointName && !m.IsSystemMessage) - .GroupBy(m => m.ProcessedAt.ToUniversalTime().Date) - .Select(g => new AuditCount - { - UtcDate = g.Key, - Count = g.LongCount() - }) - .OrderBy(r => r.UtcDate) - .ToList(); - - return results.Count == 0 && hasSent - ? Task.FromResult(new QueryResult>([new AuditCount - { - UtcDate = messageViews.First().ProcessedAt.ToUniversalTime().Date, - Count = 0 - }], QueryStatsInfo.Zero)) - : Task.FromResult(new QueryResult>(results, QueryStatsInfo.Zero)); - } - - public Task SaveProcessedMessage(ProcessedMessage processedMessage, CancellationToken cancellationToken = default) - { - processedMessage.Id ??= processedMessage.GetProcessingId(); - if (processedMessages.Any(pm => pm.Id == processedMessage.Id)) - { - return Task.CompletedTask; - } - - processedMessages.Add(processedMessage); - messageViews.Add(MessagesViewFactory.Create(processedMessage)); - - return Task.CompletedTask; - } - - public Task SaveSagaSnapshot(SagaSnapshot sagaSnapshot, CancellationToken cancellationToken = default) - { - var sagaHistory = sagaHistories.SingleOrDefault(sh => sh.SagaId == sagaSnapshot.SagaId); - - if (sagaHistory == null) - { - sagaHistory = new SagaHistory - { - Id = sagaSnapshot.SagaId, - SagaId = sagaSnapshot.SagaId, - SagaType = sagaSnapshot.SagaType, - }; - - sagaHistories.Add(sagaHistory); - } - - sagaHistory.Changes.Add(new SagaStateChange - { - StartTime = sagaSnapshot.StartTime, - FinishTime = sagaSnapshot.FinishTime, - Status = sagaSnapshot.Status, - StateAfterChange = sagaSnapshot.StateAfterChange, - InitiatingMessage = sagaSnapshot.InitiatingMessage, - OutgoingMessages = sagaSnapshot.OutgoingMessages, - Endpoint = sagaSnapshot.Endpoint - }); - - return Task.CompletedTask; - } - - object TryGet(Dictionary metadata, string key) - { - if (metadata.TryGetValue(key, out var value)) - { - return value; - } - - return null; - } - - public Task Setup(CancellationToken cancellationToken = default) => Task.CompletedTask; - - List messageViews; - List processedMessages; - List sagaHistories; - } -} \ No newline at end of file diff --git a/src/ServiceControl.Audit.Persistence.InMemory/InMemoryAuditIngestionUnitOfWork.cs b/src/ServiceControl.Audit.Persistence.InMemory/InMemoryAuditIngestionUnitOfWork.cs deleted file mode 100644 index 649b916220..0000000000 --- a/src/ServiceControl.Audit.Persistence.InMemory/InMemoryAuditIngestionUnitOfWork.cs +++ /dev/null @@ -1,32 +0,0 @@ -namespace ServiceControl.Audit.Persistence.InMemory -{ - using System; - using System.Threading; - using System.Threading.Tasks; - using Auditing.BodyStorage; - using ServiceControl.Audit.Auditing; - using ServiceControl.Audit.Persistence.UnitOfWork; - using ServiceControl.SagaAudit; - - class InMemoryAuditIngestionUnitOfWork( - InMemoryAuditDataStore dataStore, - BodyStorageEnricher bodyStorageEnricher) - : IAuditIngestionUnitOfWork - { - public ValueTask DisposeAsync() => ValueTask.CompletedTask; - - // InMemory writes are immediate, so Complete has nothing to commit. - public Task Complete(CancellationToken cancellationToken = default) => Task.CompletedTask; - - public async Task RecordProcessedMessage(ProcessedMessage processedMessage, ReadOnlyMemory body, CancellationToken cancellationToken = default) - { - if (!body.IsEmpty) - { - await bodyStorageEnricher.StoreAuditMessageBody(body, processedMessage, cancellationToken); - } - await dataStore.SaveProcessedMessage(processedMessage, cancellationToken); - } - - public Task RecordSagaSnapshot(SagaSnapshot sagaSnapshot, CancellationToken cancellationToken = default) => dataStore.SaveSagaSnapshot(sagaSnapshot, cancellationToken); - } -} \ No newline at end of file diff --git a/src/ServiceControl.Audit.Persistence.InMemory/InMemoryAuditIngestionUnitOfWorkFactory.cs b/src/ServiceControl.Audit.Persistence.InMemory/InMemoryAuditIngestionUnitOfWorkFactory.cs deleted file mode 100644 index d0f0ddf664..0000000000 --- a/src/ServiceControl.Audit.Persistence.InMemory/InMemoryAuditIngestionUnitOfWorkFactory.cs +++ /dev/null @@ -1,20 +0,0 @@ -namespace ServiceControl.Audit.Persistence.InMemory -{ - using System.Threading; - using System.Threading.Tasks; - using ServiceControl.Audit.Auditing.BodyStorage; - using ServiceControl.Audit.Persistence.UnitOfWork; - - class InMemoryAuditIngestionUnitOfWorkFactory(InMemoryAuditDataStore dataStore, BodyStorageEnricher bodyStorageEnricher) : IAuditIngestionUnitOfWorkFactory - { - public ValueTask StartNew(int batchSize, CancellationToken cancellationToken = default) - { - //The batchSize argument is ignored: the in-memory storage implementation doesn't support batching. - return new ValueTask(new InMemoryAuditIngestionUnitOfWork(dataStore, bodyStorageEnricher)); - } - - public bool CanIngestMore() => true; - - public bool SupportsConcurrentBatches => false; - } -} \ No newline at end of file diff --git a/src/ServiceControl.Audit.Persistence.InMemory/InMemoryFailedAuditStorage.cs b/src/ServiceControl.Audit.Persistence.InMemory/InMemoryFailedAuditStorage.cs deleted file mode 100644 index e6dbf35e02..0000000000 --- a/src/ServiceControl.Audit.Persistence.InMemory/InMemoryFailedAuditStorage.cs +++ /dev/null @@ -1,46 +0,0 @@ -namespace ServiceControl.Audit.Persistence.InMemory -{ - using System; - using System.Threading; - using System.Threading.Tasks; - using ServiceControl.Audit.Auditing; - - class InMemoryFailedAuditStorage(InMemoryAuditDataStore dataStore) : IFailedAuditStorage - { - public async Task ProcessFailedMessages(Func, CancellationToken, Task> onMessage, CancellationToken cancellationToken = default) - { - cancellationToken.ThrowIfCancellationRequested(); - - foreach (var failedMessage in dataStore.failedAuditImports) - { - cancellationToken.ThrowIfCancellationRequested(); - FailedTransportMessage transportMessage = failedMessage.Message; - - await onMessage(transportMessage, _ => Task.CompletedTask, cancellationToken); - } - - dataStore.failedAuditImports.Clear(); - } - - public Task SaveFailedAuditImport(FailedAuditImport message, CancellationToken cancellationToken = default) - { - cancellationToken.ThrowIfCancellationRequested(); - var existing = message.Id == null ? -1 : dataStore.failedAuditImports.FindIndex(x => x.Id == message.Id); - if (existing >= 0) - { - dataStore.failedAuditImports[existing] = message; - } - else - { - dataStore.failedAuditImports.Add(message); - } - return Task.CompletedTask; - } - - public Task GetFailedAuditsCount(CancellationToken cancellationToken = default) - { - cancellationToken.ThrowIfCancellationRequested(); - return Task.FromResult(dataStore.failedAuditImports.Count); - } - } -} diff --git a/src/ServiceControl.Audit.Persistence.InMemory/InMemoryPersistence.cs b/src/ServiceControl.Audit.Persistence.InMemory/InMemoryPersistence.cs deleted file mode 100644 index b0a487ef3e..0000000000 --- a/src/ServiceControl.Audit.Persistence.InMemory/InMemoryPersistence.cs +++ /dev/null @@ -1,25 +0,0 @@ -namespace ServiceControl.Audit.Persistence.InMemory -{ - using Auditing.BodyStorage; - using Microsoft.Extensions.DependencyInjection; - using UnitOfWork; - - public class InMemoryPersistence(PersistenceSettings persistenceSettings) : IPersistence - { - public void AddPersistence(IServiceCollection services) - { - services.AddSingleton(persistenceSettings); - services.AddSingleton(); - services.AddSingleton(sp => sp.GetRequiredService()); - services.AddSingleton(sp => sp.GetRequiredService()); - services.AddSingleton(); - services.AddSingleton(); - services.AddSingleton(); - services.AddSingleton(); - } - - public void AddInstaller(IServiceCollection services) - { - } - } -} \ No newline at end of file diff --git a/src/ServiceControl.Audit.Persistence.InMemory/InMemoryPersistenceConfiguration.cs b/src/ServiceControl.Audit.Persistence.InMemory/InMemoryPersistenceConfiguration.cs deleted file mode 100644 index 016adef5ec..0000000000 --- a/src/ServiceControl.Audit.Persistence.InMemory/InMemoryPersistenceConfiguration.cs +++ /dev/null @@ -1,15 +0,0 @@ -namespace ServiceControl.Audit.Persistence.InMemory -{ - using System.Collections.Generic; - - public class InMemoryPersistenceConfiguration : IPersistenceConfiguration - { - public bool SupportsMaintenanceMode => false; - - public string Name => "InMemory"; - - public IEnumerable ConfigurationKeys => new string[0]; - - public IPersistence Create(PersistenceSettings settings) => new InMemoryPersistence(settings); - } -} diff --git a/src/ServiceControl.Audit.Persistence.InMemory/MessagesViewFactory.cs b/src/ServiceControl.Audit.Persistence.InMemory/MessagesViewFactory.cs deleted file mode 100644 index 66a66249a2..0000000000 --- a/src/ServiceControl.Audit.Persistence.InMemory/MessagesViewFactory.cs +++ /dev/null @@ -1,61 +0,0 @@ -namespace ServiceControl.Audit.Persistence.InMemory -{ - using System; - using System.Collections.Generic; - using System.Linq; - using System.Reflection; - using Auditing; - using Auditing.MessagesView; - using ServiceControl.Audit.Monitoring; - - static class MessagesViewFactory - { - static ILookup writeableMessageViewProperties - = typeof(MessagesView).GetProperties(BindingFlags.Instance | BindingFlags.Public) - .Where(p => p.CanWrite) - .ToLookup(p => p.Name, StringComparer.InvariantCultureIgnoreCase); - - static ILookup propertyMapping - = new Dictionary - { - // Metadata key, Property name - { "ContentLength", "BodySize" } - }.ToLookup( - x => x.Key, - x => x.Value, - StringComparer.InvariantCultureIgnoreCase); - - public static MessagesView Create(ProcessedMessage message) - { - var result = new MessagesView - { - Id = message.UniqueMessageId, - ProcessedAt = message.ProcessedAt, - Status = MessageStatus.Successful, - Headers = message.Headers.Select(header => new KeyValuePair(header.Key, header.Value)) - }; - - foreach (var metadata in message.MessageMetadata) - { - foreach (var property in writeableMessageViewProperties[metadata.Key]) - { - property.SetValue(result, metadata.Value); - } - foreach (var mappedName in propertyMapping[metadata.Key]) - { - foreach (var property in writeableMessageViewProperties[mappedName]) - { - property.SetValue(result, metadata.Value); - } - } - } - - if (message.MessageMetadata.TryGetValue("IsRetried", out var isRetried) && (bool)isRetried) - { - result.Status = MessageStatus.ResolvedSuccessfully; - } - - return result; - } - } -} \ No newline at end of file diff --git a/src/ServiceControl.Audit.Persistence.InMemory/ServiceControl.Audit.Persistence.InMemory.csproj b/src/ServiceControl.Audit.Persistence.InMemory/ServiceControl.Audit.Persistence.InMemory.csproj deleted file mode 100644 index 8184641fa3..0000000000 --- a/src/ServiceControl.Audit.Persistence.InMemory/ServiceControl.Audit.Persistence.InMemory.csproj +++ /dev/null @@ -1,20 +0,0 @@ - - - - net10.0 - true - true - - - - - - - - - - - - - - \ No newline at end of file diff --git a/src/ServiceControl.Audit.Persistence.InMemory/persistence.manifest b/src/ServiceControl.Audit.Persistence.InMemory/persistence.manifest deleted file mode 100644 index 2dce91234b..0000000000 --- a/src/ServiceControl.Audit.Persistence.InMemory/persistence.manifest +++ /dev/null @@ -1,7 +0,0 @@ -{ - "Name": "InMemory", - "DisplayName": "In-memory", - "Description": "InMemory ServiceControl Audit persister", - "AssemblyName": "ServiceControl.Audit.Persistence.InMemory", - "TypeName": "ServiceControl.Audit.Persistence.InMemory.InMemoryPersistenceConfiguration, ServiceControl.Audit.Persistence.InMemory" -} \ No newline at end of file diff --git a/src/ServiceControl.Audit.Persistence.Tests.RavenDB/PersistenceTestsConfiguration.cs b/src/ServiceControl.Audit.Persistence.Tests.RavenDB/PersistenceTestsConfiguration.cs index 4d699d2bd9..cc4a3808ed 100644 --- a/src/ServiceControl.Audit.Persistence.Tests.RavenDB/PersistenceTestsConfiguration.cs +++ b/src/ServiceControl.Audit.Persistence.Tests.RavenDB/PersistenceTestsConfiguration.cs @@ -14,7 +14,7 @@ using ServiceControl.Audit.Persistence.RavenDB; using UnitOfWork; - class PersistenceTestsConfiguration + class PersistenceTestsConfiguration : IPersistenceTestsConfiguration { public IAuditMessagesViewDataStore MessagesViewStore { get; private set; } diff --git a/src/ServiceControl.Audit.Persistence.Tests.RavenDB/ServiceControl.Audit.Persistence.Tests.RavenDB.csproj b/src/ServiceControl.Audit.Persistence.Tests.RavenDB/ServiceControl.Audit.Persistence.Tests.RavenDB.csproj index 31ca62448b..60be6df82f 100644 --- a/src/ServiceControl.Audit.Persistence.Tests.RavenDB/ServiceControl.Audit.Persistence.Tests.RavenDB.csproj +++ b/src/ServiceControl.Audit.Persistence.Tests.RavenDB/ServiceControl.Audit.Persistence.Tests.RavenDB.csproj @@ -26,8 +26,7 @@ - - + diff --git a/src/ServiceControl.Audit.Persistence.Tests/IPersistenceTestsConfiguration.cs b/src/ServiceControl.Audit.Persistence.Tests/IPersistenceTestsConfiguration.cs new file mode 100644 index 0000000000..9ad8eff88f --- /dev/null +++ b/src/ServiceControl.Audit.Persistence.Tests/IPersistenceTestsConfiguration.cs @@ -0,0 +1,29 @@ +namespace ServiceControl.Audit.Persistence.Tests; + +using System; +using System.Threading.Tasks; +using Auditing.BodyStorage; +using UnitOfWork; + +interface IPersistenceTestsConfiguration +{ + string Name { get; } + + IAuditMessagesViewDataStore MessagesViewStore { get; } + + ISagaHistoryDataStore SagaHistoryStore { get; } + + IFailedAuditStorage FailedAuditStorage { get; } + + IBodyStorage BodyStorage { get; } + + IAuditIngestionUnitOfWorkFactory AuditIngestionUnitOfWorkFactory { get; } + + IServiceProvider ServiceProvider { get; } + + Task Configure(Action setSettings); + + Task CompleteDBOperation(); + + Task Cleanup(); +} diff --git a/src/ServiceControl.Audit.Persistence.Tests/InMemory/BodyVersionTests.cs b/src/ServiceControl.Audit.Persistence.Tests/InMemory/BodyVersionTests.cs deleted file mode 100644 index 34dfef456a..0000000000 --- a/src/ServiceControl.Audit.Persistence.Tests/InMemory/BodyVersionTests.cs +++ /dev/null @@ -1,43 +0,0 @@ -namespace ServiceControl.Audit.Persistence.Tests -{ - using System; - using System.IO; - using System.Threading.Tasks; - using NUnit.Framework; - using ServiceControl.Audit.Infrastructure; - - [TestFixture] - class BodyVersionTests : PersistenceTestFixture - { - [Test] - public async Task In_memory_list_queries_remain_unversioned() - { - var result = await MessagesViewStore.GetMessages(false, new PagingInfo(), new SortInfo("message_id", "asc")); - - Assert.That(result.QueryStats.Version.HasValue, Is.False); - } - - [Test] - public async Task Replacing_a_body_changes_its_validator_without_changing_reads() - { - var bodyId = Guid.NewGuid().ToString(); - await BodyStorage.Store(bodyId, "text/plain", 1, new MemoryStream([1])); - var first = await BodyStorage.TryFetch(bodyId); - var repeated = await BodyStorage.TryFetch(bodyId); - - await BodyStorage.Store(bodyId, "text/plain", 1, new MemoryStream([2])); - var replaced = await BodyStorage.TryFetch(bodyId); - - using (Assert.EnterMultipleScope()) - { - Assert.That(first.Version.HasValue, Is.True); - Assert.That(repeated.Version, Is.EqualTo(first.Version)); - Assert.That(replaced.Version, Is.Not.EqualTo(first.Version)); - } - - first.StreamContent.Dispose(); - repeated.StreamContent.Dispose(); - replaced.StreamContent.Dispose(); - } - } -} diff --git a/src/ServiceControl.Audit.Persistence.Tests/InMemory/PersistenceTestsConfiguration.cs b/src/ServiceControl.Audit.Persistence.Tests/InMemory/PersistenceTestsConfiguration.cs deleted file mode 100644 index f1a1a5fde4..0000000000 --- a/src/ServiceControl.Audit.Persistence.Tests/InMemory/PersistenceTestsConfiguration.cs +++ /dev/null @@ -1,70 +0,0 @@ -namespace ServiceControl.Audit.Persistence.Tests -{ - using System; - using System.Threading.Tasks; - using Auditing.BodyStorage; - using Microsoft.Extensions.DependencyInjection; - using Microsoft.Extensions.Hosting; - using NServiceBus.CustomChecks; - using ServiceControl.Audit.Persistence.InMemory; - using UnitOfWork; - - class PersistenceTestsConfiguration - { - public IAuditMessagesViewDataStore MessagesViewStore { get; private set; } - - public ISagaHistoryDataStore SagaHistoryStore { get; private set; } - - public IFailedAuditStorage FailedAuditStorage { get; private set; } - - public IBodyStorage BodyStorage { get; private set; } - - public IAuditIngestionUnitOfWorkFactory AuditIngestionUnitOfWorkFactory { get; private set; } - - public IServiceProvider ServiceProvider => host.Services; - - public string Name => "InMemory"; - - public async Task Configure(Action setSettings) - { - var config = new InMemoryPersistenceConfiguration(); - var hostBuilder = Host.CreateApplicationBuilder(); - var settings = new PersistenceSettings(TimeSpan.FromHours(1), true, 100000); - - setSettings(settings); - - var persistence = config.Create(settings); - persistence.AddPersistence(hostBuilder.Services); - persistence.AddInstaller(hostBuilder.Services); - - var assembly = typeof(InMemoryPersistenceConfiguration).Assembly; - - foreach (var type in assembly.DefinedTypes) - { - if (type.IsAssignableTo(typeof(ICustomCheck))) - { - hostBuilder.Services.AddTransient(typeof(ICustomCheck), type); - } - } - - host = hostBuilder.Build(); - await host.StartAsync(); - - MessagesViewStore = host.Services.GetRequiredService(); - SagaHistoryStore = host.Services.GetRequiredService(); - FailedAuditStorage = host.Services.GetRequiredService(); - BodyStorage = host.Services.GetService(); - AuditIngestionUnitOfWorkFactory = host.Services.GetRequiredService(); - } - - public Task CompleteDBOperation() => Task.CompletedTask; - - public async Task Cleanup() - { - await host.StopAsync(); - host.Dispose(); - } - - IHost host; - } -} \ No newline at end of file diff --git a/src/ServiceControl.Audit.Persistence.Tests/PersistenceTestFixture.cs b/src/ServiceControl.Audit.Persistence.Tests/PersistenceTestFixture.cs index a7cdbd7b32..31e108881d 100644 --- a/src/ServiceControl.Audit.Persistence.Tests/PersistenceTestFixture.cs +++ b/src/ServiceControl.Audit.Persistence.Tests/PersistenceTestFixture.cs @@ -17,6 +17,10 @@ abstract class PersistenceTestFixture public virtual Task Setup() { configuration = new PersistenceTestsConfiguration(); + if (configuration is not IPersistenceTestsConfiguration) + { + throw new Exception($"{nameof(PersistenceTestsConfiguration)} must implement {nameof(IPersistenceTestsConfiguration)}"); + } testCancellationTokenSource = Debugger.IsAttached ? new CancellationTokenSource() : new CancellationTokenSource(TestTimeout); diff --git a/src/ServiceControl.Audit.Persistence.Tests/ServiceControl.Audit.Persistence.Tests.csproj b/src/ServiceControl.Audit.Persistence.Tests/ServiceControl.Audit.Persistence.Tests.csproj deleted file mode 100644 index 219f89cb81..0000000000 --- a/src/ServiceControl.Audit.Persistence.Tests/ServiceControl.Audit.Persistence.Tests.csproj +++ /dev/null @@ -1,32 +0,0 @@ - - - - net10.0 - Raven - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/src/ServiceControl.Audit.Persistence/DevelopmentPersistenceLocations.cs b/src/ServiceControl.Audit.Persistence/DevelopmentPersistenceLocations.cs index ffa49e2575..5c533739b7 100644 --- a/src/ServiceControl.Audit.Persistence/DevelopmentPersistenceLocations.cs +++ b/src/ServiceControl.Audit.Persistence/DevelopmentPersistenceLocations.cs @@ -17,7 +17,6 @@ static DevelopmentPersistenceLocations() if (!string.IsNullOrWhiteSpace(srcFolder) && srcFolder.EndsWith("src")) { - ManifestFiles.Add(BuildManifestPath(srcFolder, "ServiceControl.Audit.Persistence.InMemory")); ManifestFiles.Add(BuildManifestPath(srcFolder, "ServiceControl.Audit.Persistence.RavenDB")); } } diff --git a/src/ServiceControl.Audit.Persistence/ServiceControl.Audit.Persistence.csproj b/src/ServiceControl.Audit.Persistence/ServiceControl.Audit.Persistence.csproj index 9c01837062..8e54463350 100644 --- a/src/ServiceControl.Audit.Persistence/ServiceControl.Audit.Persistence.csproj +++ b/src/ServiceControl.Audit.Persistence/ServiceControl.Audit.Persistence.csproj @@ -15,7 +15,7 @@ - + \ No newline at end of file diff --git a/src/ServiceControl.Audit.UnitTests/API/APIApprovals.cs b/src/ServiceControl.Audit.UnitTests/API/APIApprovals.cs index 78e39fe0eb..94e2302234 100644 --- a/src/ServiceControl.Audit.UnitTests/API/APIApprovals.cs +++ b/src/ServiceControl.Audit.UnitTests/API/APIApprovals.cs @@ -172,6 +172,6 @@ public void PlatformSampleSettings() Approver.Verify(settings); } - static Settings CreateTestSettings() => new("LearningTransport", "InMemory"); + static Settings CreateTestSettings() => new("LearningTransport", "RavenDB"); } } \ No newline at end of file diff --git a/src/ServiceControl.Audit.UnitTests/ApprovalFiles/APIApprovals.PlatformSampleSettings.approved.txt b/src/ServiceControl.Audit.UnitTests/ApprovalFiles/APIApprovals.PlatformSampleSettings.approved.txt index c1066673e7..44ef85404e 100644 --- a/src/ServiceControl.Audit.UnitTests/ApprovalFiles/APIApprovals.PlatformSampleSettings.approved.txt +++ b/src/ServiceControl.Audit.UnitTests/ApprovalFiles/APIApprovals.PlatformSampleSettings.approved.txt @@ -50,7 +50,7 @@ "Hostname": "localhost", "VirtualDirectory": "", "TransportType": "LearningTransport", - "PersistenceType": "InMemory", + "PersistenceType": "RavenDB", "AuditQueue": "audit", "ForwardAuditMessages": false, "IngestAuditMessages": true, diff --git a/src/ServiceControl.Audit.Persistence.Tests/PersistenceManifestLibraryTests.cs b/src/ServiceControl.Audit.UnitTests/Infrastructure/PersistenceManifestLibraryTests.cs similarity index 86% rename from src/ServiceControl.Audit.Persistence.Tests/PersistenceManifestLibraryTests.cs rename to src/ServiceControl.Audit.UnitTests/Infrastructure/PersistenceManifestLibraryTests.cs index f68bc70c67..19b5559ece 100644 --- a/src/ServiceControl.Audit.Persistence.Tests/PersistenceManifestLibraryTests.cs +++ b/src/ServiceControl.Audit.UnitTests/Infrastructure/PersistenceManifestLibraryTests.cs @@ -1,4 +1,4 @@ -namespace ServiceControl.Audit.Persistence.Tests +namespace ServiceControl.Audit.UnitTests.Infrastructure { using System.Collections.Generic; using System.IO; @@ -95,27 +95,16 @@ public class PersistenceManifestLoadingTests } """; - const string InMemory = """ - { - "Name": "InMemory", - "DisplayName": "In-memory", - "Description": "InMemory ServiceControl Audit persister", - "AssemblyName": "ServiceControl.Audit.Persistence.InMemory", - "TypeName": "ServiceControl.Audit.Persistence.InMemory.InMemoryPersistenceConfiguration, ServiceControl.Audit.Persistence.InMemory" - } - """; - [Test] public void Legacy_manifest_without_an_assembly_does_not_hide_the_persisters_after_it() { var manifests = LoadFrom(new() { ["RavenDB"] = RavenDB, - ["RavenDB35"] = RavenDB35, - ["InMemory"] = InMemory + ["RavenDB35"] = RavenDB35 }); - Assert.That(manifests.Select(m => m.Name), Is.EquivalentTo(["RavenDB", "RavenDB35", "InMemory"])); + Assert.That(manifests.Select(m => m.Name), Is.EquivalentTo(["RavenDB", "RavenDB35"])); } [Test] @@ -124,10 +113,10 @@ public void Unreadable_manifest_does_not_hide_the_persisters_after_it() var manifests = LoadFrom(new() { ["Corrupt"] = "{ this is not json", - ["InMemory"] = InMemory + ["RavenDB"] = RavenDB }); - Assert.That(manifests.Select(m => m.Name), Is.EqualTo(["InMemory"])); + Assert.That(manifests.Select(m => m.Name), Is.EqualTo(["RavenDB"])); } static List LoadFrom(Dictionary persisters) diff --git a/src/ServiceControl.Audit.UnitTests/Infrastructure/When_instance_is_setup.cs b/src/ServiceControl.Audit.UnitTests/Infrastructure/When_instance_is_setup.cs index 4c2cc1d874..7deb11df5b 100644 --- a/src/ServiceControl.Audit.UnitTests/Infrastructure/When_instance_is_setup.cs +++ b/src/ServiceControl.Audit.UnitTests/Infrastructure/When_instance_is_setup.cs @@ -13,6 +13,7 @@ using NServiceBus; using NServiceBus.Transport; using NUnit.Framework; + using Persistence; using Transports; class When_instance_is_setup @@ -33,9 +34,17 @@ public async Task Should_provision_queues() TransportManifestLibrary.TransportManifests.Add(manifest); + PersistenceManifestLibrary.PersistenceManifests.Add(new PersistenceManifest + { + Name = "FakePersistence", + Location = AppContext.BaseDirectory, + AssemblyName = Assembly.GetExecutingAssembly().GetName().Name, + TypeName = typeof(FakePersistenceConfiguration).AssemblyQualifiedName + }); + var instanceInputQueueName = "SomeInstanceQueue"; - var settings = new Settings("FakeTransport", "InMemory") + var settings = new Settings("FakeTransport", "FakePersistence") { InstanceName = instanceInputQueueName, ForwardAuditMessages = true, @@ -92,4 +101,24 @@ public Task CreateTransportInfrastructure(string name, throw new NotImplementedException(); public string ToTransportQualifiedQueueName(string queueName) => queueName; } + + class FakePersistenceConfiguration : IPersistenceConfiguration + { + public bool SupportsMaintenanceMode => false; + + public string Name => "FakePersistence"; + + public IEnumerable ConfigurationKeys => []; + + public IPersistence Create(PersistenceSettings settings) => new FakePersistence(); + } + + class FakePersistence : IPersistence + { + public void AddPersistence(IServiceCollection services) => throw new NotImplementedException(); + + public void AddInstaller(IServiceCollection services) + { + } + } } \ No newline at end of file diff --git a/src/ServiceControl.Audit.UnitTests/ServiceControl.Audit.UnitTests.csproj b/src/ServiceControl.Audit.UnitTests/ServiceControl.Audit.UnitTests.csproj index c6334d7c62..9fcab2fd61 100644 --- a/src/ServiceControl.Audit.UnitTests/ServiceControl.Audit.UnitTests.csproj +++ b/src/ServiceControl.Audit.UnitTests/ServiceControl.Audit.UnitTests.csproj @@ -7,7 +7,6 @@ - diff --git a/src/ServiceControl.Audit.UnitTests/Verification/CustomChecksTest.cs b/src/ServiceControl.Audit.UnitTests/Verification/CustomChecksTest.cs index 18e824d544..030afe14b4 100644 --- a/src/ServiceControl.Audit.UnitTests/Verification/CustomChecksTest.cs +++ b/src/ServiceControl.Audit.UnitTests/Verification/CustomChecksTest.cs @@ -19,7 +19,7 @@ class CustomChecksTest [Test] public void VerifyCustomChecks() { - var settings = (object)new Settings("LearningTransport", "InMemory"); + var settings = (object)new Settings("LearningTransport", "RavenDB"); var discovered = from type in typeof(Settings).Assembly.GetTypes() diff --git a/src/ServiceControl.Audit/App.config b/src/ServiceControl.Audit/App.config index 6e4cc74869..2e6b60036a 100644 --- a/src/ServiceControl.Audit/App.config +++ b/src/ServiceControl.Audit/App.config @@ -19,8 +19,7 @@ These settings are only here so that we can debug ServiceControl while developin - - + diff --git a/src/ServiceControl.Audit/ServiceControl.Audit.csproj b/src/ServiceControl.Audit/ServiceControl.Audit.csproj index d8fc99ea76..9df69e711f 100644 --- a/src/ServiceControl.Audit/ServiceControl.Audit.csproj +++ b/src/ServiceControl.Audit/ServiceControl.Audit.csproj @@ -57,7 +57,6 @@ - diff --git a/src/ServiceControl.MultiInstance.AcceptanceTests/ServiceControl.MultiInstance.AcceptanceTests.csproj b/src/ServiceControl.MultiInstance.AcceptanceTests/ServiceControl.MultiInstance.AcceptanceTests.csproj index cbaba829ae..54a16ec686 100644 --- a/src/ServiceControl.MultiInstance.AcceptanceTests/ServiceControl.MultiInstance.AcceptanceTests.csproj +++ b/src/ServiceControl.MultiInstance.AcceptanceTests/ServiceControl.MultiInstance.AcceptanceTests.csproj @@ -7,10 +7,10 @@ - + - + @@ -27,4 +27,9 @@ + + + + + \ No newline at end of file diff --git a/src/ServiceControl.MultiInstance.AcceptanceTests/TestSupport/ServiceControlComponentRunner.cs b/src/ServiceControl.MultiInstance.AcceptanceTests/TestSupport/ServiceControlComponentRunner.cs index c8015c8846..3d0afbbf82 100644 --- a/src/ServiceControl.MultiInstance.AcceptanceTests/TestSupport/ServiceControlComponentRunner.cs +++ b/src/ServiceControl.MultiInstance.AcceptanceTests/TestSupport/ServiceControlComponentRunner.cs @@ -8,7 +8,6 @@ namespace ServiceControl.MultiInstance.AcceptanceTests.TestSupport using System.Threading; using System.Threading.Tasks; using AcceptanceTesting; - using Audit.AcceptanceTests; using Microsoft.AspNetCore.TestHost; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Hosting; @@ -50,13 +49,14 @@ public ServiceControlComponentRunner(ITransportIntegration transportToUse, public async Task Initialize(RunDescriptor run) { SettingsPerInstance.Clear(); + storage = new SharedRavenStorageConfiguration(); // The way we are setting up things here means we assume there is only one remote instance. Should we move away from this approach // parts of the logic in this class would have to be augmented to dynamically spin up multiple audit instances based on some configuration // currently we don't need that so YAGNI. auditInstanceComponentRunner = new AuditInstanceTestsSupport.ServiceControlComponentRunner( transportToUse, - new AcceptanceTestStorageConfiguration(), auditSettings => + storage, auditSettings => { auditSettings.ServiceControlQueueAddress = PrimaryInstanceSettings.DEFAULT_INSTANCE_NAME; customServiceControlAuditSettings(auditSettings); @@ -77,7 +77,7 @@ public async Task Initialize(RunDescriptor run) primaryInstanceComponentRunner = new PrimaryInstanceTestsSupport.ServiceControlComponentRunner( transportToUse, - new ServiceControl.AcceptanceTests.RavenDB.AcceptanceTestStorageConfiguration(), primarySettings => + storage, primarySettings => { primarySettings.RemoteInstances = [auditInstance]; customServiceControlSettings(primarySettings); @@ -120,6 +120,7 @@ public override async Task Stop(CancellationToken cancellationToken = default) { await auditInstanceComponentRunner.Stop(cancellationToken); await primaryInstanceComponentRunner.Stop(cancellationToken); + await storage.Cleanup(cancellationToken); } ITransportIntegration transportToUse; @@ -129,6 +130,7 @@ public override async Task Stop(CancellationToken cancellationToken = default) Action primaryHostBuilderCustomization; Action auditHostBuilderCustomization; Action customServiceControlSettings; + SharedRavenStorageConfiguration storage; Audit.AcceptanceTests.TestSupport.ServiceControlComponentRunner auditInstanceComponentRunner; PrimaryInstanceTestsSupport.ServiceControlComponentRunner primaryInstanceComponentRunner; } diff --git a/src/ServiceControl.MultiInstance.AcceptanceTests/TestSupport/SharedRavenStorageConfiguration.cs b/src/ServiceControl.MultiInstance.AcceptanceTests/TestSupport/SharedRavenStorageConfiguration.cs new file mode 100644 index 0000000000..0b599318fb --- /dev/null +++ b/src/ServiceControl.MultiInstance.AcceptanceTests/TestSupport/SharedRavenStorageConfiguration.cs @@ -0,0 +1,79 @@ +namespace ServiceControl.MultiInstance.AcceptanceTests.TestSupport +{ + using System; + using System.Collections.Generic; + using System.Reactive.Disposables; + using System.Threading; + using System.Threading.Tasks; + using ServiceControl.Persistence.RavenDB; + using ServiceControl.Persistence.Tests; + using ServiceControl.RavenDB; + using TestHelper; + using AuditRavenPersistenceConfiguration = ServiceControl.Audit.Persistence.RavenDB.RavenPersistenceConfiguration; + using AuditStorageConfiguration = ServiceControl.Audit.AcceptanceTests.TestSupport.IAcceptanceTestStorageConfiguration; + using PrimaryInstanceSettings = ServiceBus.Management.Infrastructure.Settings.Settings; + using PrimaryStorageConfiguration = ServiceControl.AcceptanceTests.TestSupport.IAcceptanceTestStorageConfiguration; + + // RavenDB runs one embedded server per process, so the primary and the audit instance keep their databases on the same one + class SharedRavenStorageConfiguration : PrimaryStorageConfiguration, AuditStorageConfiguration + { + // Both persisters ship a manifest named RavenDB, and only one of them survives the copy into this project's output folder + string PrimaryStorageConfiguration.PersistenceType => TypeNameOf(typeof(RavenPersistenceConfiguration)); + + string AuditStorageConfiguration.PersistenceType => TypeNameOf(typeof(AuditRavenPersistenceConfiguration)); + + async Task PrimaryStorageConfiguration.CustomizeSettings(PrimaryInstanceSettings settings, CancellationToken cancellationToken) + { + var databaseName = Guid.NewGuid().ToString("n"); + databaseInstance = await SharedEmbeddedServer.GetInstance(cancellationToken); + databaseNames.Add(databaseName); + databaseNames.Add($"{databaseName}-throughput"); + + settings.PersisterSpecificSettings = new RavenPersisterSettings + { + ErrorRetentionPeriod = TimeSpan.FromDays(10), + ConnectionString = databaseInstance.ServerUrl, + DatabaseName = databaseName, + ThroughputDatabaseName = $"{databaseName}-throughput" + }; + } + + async Task> AuditStorageConfiguration.CustomizeSettings(CancellationToken cancellationToken) + { + var databaseName = Guid.NewGuid().ToString("n"); + databaseInstance = await SharedEmbeddedServer.GetInstance(cancellationToken); + databaseNames.Add(databaseName); + + return new Dictionary + { + { AuditRavenPersistenceConfiguration.ConnectionStringKey, databaseInstance.ServerUrl }, + { AuditRavenPersistenceConfiguration.DatabaseNameKey, databaseName } + }; + } + + public async Task Cleanup(CancellationToken cancellationToken = default) + { + if (databaseInstance == null) + { + return; + } + using var _ = await UseDatabaseLifecycleLock(cancellationToken); + foreach (var databaseName in databaseNames) + { + await databaseInstance.DeleteDatabase(databaseName, cancellationToken); + } + } + + public async Task UseDatabaseLifecycleLock(CancellationToken cancellationToken = default) + { + await databaseLifecycleLock.WaitAsync(cancellationToken); + return Disposable.Create(() => databaseLifecycleLock.Release()); + } + + static string TypeNameOf(Type type) => $"{type.FullName}, {type.Assembly.GetName().Name}"; + + static readonly SemaphoreSlim databaseLifecycleLock = new SemaphoreSlim(1, 1); + readonly List databaseNames = []; + EmbeddedDatabase databaseInstance; + } +} diff --git a/src/ServiceControl.Persistence.RavenDB/ServiceControl.Persistence.RavenDB.csproj b/src/ServiceControl.Persistence.RavenDB/ServiceControl.Persistence.RavenDB.csproj index e121d7e979..64ee49dea8 100644 --- a/src/ServiceControl.Persistence.RavenDB/ServiceControl.Persistence.RavenDB.csproj +++ b/src/ServiceControl.Persistence.RavenDB/ServiceControl.Persistence.RavenDB.csproj @@ -46,6 +46,7 @@ + diff --git a/src/ServiceControl.slnx b/src/ServiceControl.slnx index 622050f094..5abc0cecec 100644 --- a/src/ServiceControl.slnx +++ b/src/ServiceControl.slnx @@ -18,16 +18,13 @@ - - - diff --git a/src/ServiceControlInstaller.Engine.UnitTests/ApprovalFiles/InstallationTests.Audit_install_should_write_expected_config_file.InMemory.approved.txt b/src/ServiceControlInstaller.Engine.UnitTests/ApprovalFiles/InstallationTests.Audit_install_should_write_expected_config_file.InMemory.approved.txt deleted file mode 100644 index 4e46215a5c..0000000000 --- a/src/ServiceControlInstaller.Engine.UnitTests/ApprovalFiles/InstallationTests.Audit_install_should_write_expected_config_file.InMemory.approved.txt +++ /dev/null @@ -1,14 +0,0 @@ - - - - - - - - - - - - - \ No newline at end of file From bf9d61191ef8bcbd5c8f3b6ca52cf1f2ed8c299c Mon Sep 17 00:00:00 2001 From: John Simons Date: Mon, 28 Sep 2026 15:09:40 +1000 Subject: [PATCH 2/2] Remove app.config-based saga audit plugin setup from multi-instance tests The SetUp/TearDown methods were manipulating ConfigurationManager to configure the SagaAudit plugin queue, which is no longer needed now that configuration is handled differently. --- ...en_sending_saga_audit_to_audit_instance.cs | 23 ------------------- 1 file changed, 23 deletions(-) diff --git a/src/ServiceControl.MultiInstance.AcceptanceTests/SagaAudit/When_sending_saga_audit_to_audit_instance.cs b/src/ServiceControl.MultiInstance.AcceptanceTests/SagaAudit/When_sending_saga_audit_to_audit_instance.cs index 631cb52741..5dbbd1f3e2 100644 --- a/src/ServiceControl.MultiInstance.AcceptanceTests/SagaAudit/When_sending_saga_audit_to_audit_instance.cs +++ b/src/ServiceControl.MultiInstance.AcceptanceTests/SagaAudit/When_sending_saga_audit_to_audit_instance.cs @@ -1,7 +1,6 @@ namespace ServiceControl.MultiInstance.AcceptanceTests.SagaAudit { using System; - using System.Configuration; using System.Linq; using System.Threading.Tasks; using AcceptanceTesting; @@ -15,28 +14,6 @@ class When_sending_saga_audit_to_audit_instance : AcceptanceTest { - [SetUp] - public void ConfigSetup() - { - // To configure the SagaAudit plugin - var config = ConfigurationManager.OpenExeConfiguration(ConfigurationUserLevel.None); - var appSettings = (AppSettingsSection)config.GetSection("appSettings"); - appSettings.Settings.Add("ServiceControl/Queue", ServiceControlInstanceName); - config.Save(); - ConfigurationManager.RefreshSection("appSettings"); - } - - [TearDown] - public void ConfigTeardown() - { - // Cleanup the saga audit plugin configuration to not leak into other tests - var config = ConfigurationManager.OpenExeConfiguration(ConfigurationUserLevel.None); - var appSettings = (AppSettingsSection)config.GetSection("appSettings"); - appSettings.Settings.Remove("ServiceControl/Queue"); - config.Save(); - ConfigurationManager.RefreshSection("appSettings"); - } - [Test] public async Task Saga_history_can_be_fetched_from_main_instance() {