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/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() { 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