Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@

public class InMemoryPersistenceConfiguration : IPersistenceConfiguration
{
public bool SupportsMaintenanceMode => false;

public string Name => "InMemory";

public IEnumerable<string> ConfigurationKeys => new string[0];
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,8 @@ public class RavenPersistenceConfiguration : IPersistenceConfiguration
QueryTimeoutInSecondsKey
};

public bool SupportsMaintenanceMode => true;

public string Name => "RavenDB";

public IPersistence Create(PersistenceSettings settings)
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
namespace ServiceControl.Audit.Persistence.Tests
{
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Reflection;
Expand Down Expand Up @@ -70,4 +71,84 @@ public void All_types_defined_in_manifest_files_exist_in_specified_assembly()
Assert.That(count, Is.Not.Zero, "No persistence manifests found.");
}
}

[TestFixture]
public class PersistenceManifestLoadingTests
{
const string RavenDB = """
{
"Name": "RavenDB",
"DisplayName": "RavenDB",
"Description": "RavenDB ServiceControl Audit persister",
"AssemblyName": "ServiceControl.Audit.Persistence.RavenDB",
"TypeName": "ServiceControl.Audit.Persistence.RavenDB.RavenPersistenceConfiguration, ServiceControl.Audit.Persistence.RavenDB"
}
""";

// Ships so that ServiceControl Management can describe pre-v5 instances, and has no assembly left to name
const string RavenDB35 = """
{
"Name": "RavenDB35",
"IsSupported": false,
"DisplayName": "RavenDB 3.5 (Legacy)",
"Description": "RavenDB 3.5 (Legacy) ServiceControl Audit persister"
}
""";

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
});

Assert.That(manifests.Select(m => m.Name), Is.EquivalentTo(["RavenDB", "RavenDB35", "InMemory"]));
}

[Test]
public void Unreadable_manifest_does_not_hide_the_persisters_after_it()
{
var manifests = LoadFrom(new()
{
["Corrupt"] = "{ this is not json",
["InMemory"] = InMemory
});

Assert.That(manifests.Select(m => m.Name), Is.EqualTo(["InMemory"]));
}

static List<PersistenceManifest> LoadFrom(Dictionary<string, string> persisters)
{
var installDirectory = Path.Combine(Path.GetTempPath(), TestContext.CurrentContext.Test.ID);

foreach (var (persister, manifest) in persisters)
{
var persisterDirectory = Path.Combine(installDirectory, "Persisters", persister);
Directory.CreateDirectory(persisterDirectory);
File.WriteAllText(Path.Combine(persisterDirectory, "persistence.manifest"), manifest);
}

try
{
return PersistenceManifestLibrary.LoadManifests(Directory.EnumerateFiles(installDirectory, "persistence.manifest", SearchOption.AllDirectories));
}
finally
{
Directory.Delete(installDirectory, true);
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@

public interface IPersistenceConfiguration
{
bool SupportsMaintenanceMode { get; }

string Name { get; }

IEnumerable<string> ConfigurationKeys { get; }
Expand Down
44 changes: 30 additions & 14 deletions src/ServiceControl.Audit.Persistence/PersistenceManifest.cs
Original file line number Diff line number Diff line change
Expand Up @@ -42,13 +42,7 @@ static PersistenceManifestLibrary()

try
{
foreach (var manifestFile in Directory.EnumerateFiles(assemblyDirectory, "persistence.manifest", SearchOption.AllDirectories))
{
var manifest = JsonSerializer.Deserialize<PersistenceManifest>(File.ReadAllText(manifestFile));
manifest.Location = Path.GetDirectoryName(manifestFile);

PersistenceManifests.Add(manifest);
}
PersistenceManifests.AddRange(LoadManifests(Directory.EnumerateFiles(assemblyDirectory, "persistence.manifest", SearchOption.AllDirectories)));
}
catch (Exception ex)
{
Expand All @@ -57,13 +51,7 @@ static PersistenceManifestLibrary()

try
{
foreach (var manifestFile in DevelopmentPersistenceLocations.ManifestFiles)
{
var manifest = JsonSerializer.Deserialize<PersistenceManifest>(File.ReadAllText(manifestFile));
manifest.Location = Path.GetDirectoryName(manifestFile);

PersistenceManifests.Add(manifest);
}
PersistenceManifests.AddRange(LoadManifests(DevelopmentPersistenceLocations.ManifestFiles));
}
catch (Exception ex)
{
Expand All @@ -73,6 +61,34 @@ static PersistenceManifestLibrary()
PersistenceManifests.ForEach(m => logger.LogInformation("Found persistence manifest for {ManifestDisplayName}", m.DisplayName));
}

// One unreadable manifest must not hide the persisters enumerated after it
internal static List<PersistenceManifest> LoadManifests(IEnumerable<string> manifestFiles)
{
var manifests = new List<PersistenceManifest>();

foreach (var manifestFile in manifestFiles)
{
try
{
manifests.Add(DeserializeManifest(manifestFile));
}
catch (Exception ex)
{
logger.LogWarning(ex, "Failed to load persistence manifest {ManifestFile}", manifestFile);
}
}

return manifests;
}

static PersistenceManifest DeserializeManifest(string manifestFile)
{
var manifest = JsonSerializer.Deserialize<PersistenceManifest>(File.ReadAllText(manifestFile))
?? throw new InvalidDataException($"The persistence manifest '{manifestFile}' is empty or invalid.");
manifest.Location = Path.GetDirectoryName(manifestFile);
return manifest;
}

static string GetAssemblyDirectory()
{
var assemblyLocation = typeof(PersistenceManifestLibrary).Assembly.Location;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,4 +14,8 @@
<PackageReference Include="NServiceBus" />
</ItemGroup>

<ItemGroup>
<InternalsVisibleTo Include="ServiceControl.Audit.Persistence.Tests" />
</ItemGroup>

</Project>
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
namespace ServiceControl.Audit.UnitTests.Infrastructure
{
using System;
using System.Runtime.Loader;
using System.Threading.Tasks;
using Audit.Infrastructure.Hosting;
using Audit.Infrastructure.Hosting.Commands;
using Audit.Infrastructure.Settings;
using NUnit.Framework;

class MaintenanceModeTests
{
[Test]
public void Should_refuse_unsupported_persister_before_starting_host()
{
var settings = new Settings(persisterType: "InMemory")
{
AssemblyLoadContextResolver = static _ => AssemblyLoadContext.Default
};

var exception = Assert.ThrowsAsync<InvalidOperationException>(async () =>
await new MaintenanceModeCommand().Execute(new HostArguments([]), settings));

Assert.That(exception.Message, Does.Contain("Maintenance mode is not supported").And.Contain("InMemory"));
}
}
}
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
namespace ServiceControl.Audit.Infrastructure.Hosting.Commands
{
using System;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Extensions.Hosting;
Expand All @@ -13,6 +14,11 @@ class MaintenanceModeCommand : AbstractCommand
public override async Task Execute(HostArguments args, Settings settings, CancellationToken cancellationToken = default)
{
var persistenceConfiguration = PersistenceConfigurationFactory.LoadPersistenceConfiguration(settings);
if (!persistenceConfiguration.SupportsMaintenanceMode)
{
throw new InvalidOperationException($"Maintenance mode is not supported by the {settings.PersistenceType} persister.");
}

var persistenceSettings = persistenceConfiguration.BuildPersistenceSettings(settings);

persistenceSettings.MaintenanceMode = true;
Expand Down
Loading