refactor: Init & Cleanup System

pull/76/head
Robert Dailey 2 years ago
parent 9a736426ce
commit 17bff83a2a

@ -0,0 +1,47 @@
using FluentAssertions;
using NSubstitute;
using NUnit.Framework;
using Recyclarr.Command;
using Recyclarr.Command.Initialization;
using TestLibrary.AutoFixture;
namespace Recyclarr.Tests.Command.Initialization;
[TestFixture]
[Parallelizable(ParallelScope.All)]
public class ServiceInitializationAndCleanupTest
{
[Test, AutoMockData]
public async Task Cleanup_happens_when_exception_occurs_in_action(
IServiceCommand cmd,
IServiceCleaner cleaner)
{
var sut = new ServiceInitializationAndCleanup(
Enumerable.Empty<IServiceInitializer>().OrderBy(_ => 1),
new[] {cleaner}.OrderBy(_ => 1));
var act = () => sut.Execute(cmd, () => throw new NullReferenceException());
await act.Should().ThrowAsync<NullReferenceException>();
cleaner.Received().Cleanup();
}
[Test, AutoMockData]
public async Task Cleanup_happens_when_exception_occurs_in_init(
IServiceCommand cmd,
IServiceInitializer init,
IServiceCleaner cleaner)
{
var sut = new ServiceInitializationAndCleanup(
new[] {init}.OrderBy(_ => 1),
new[] {cleaner}.OrderBy(_ => 1));
init.WhenForAnyArgs(x => x.Initialize(default!))
.Do(_ => throw new NullReferenceException());
var act = () => sut.Execute(cmd, () => Task.CompletedTask);
await act.Should().ThrowAsync<NullReferenceException>();
cleaner.Received().Cleanup();
}
}

@ -1,6 +0,0 @@
namespace Recyclarr.Command.Helpers;
public interface IServiceInitialization
{
void Initialize(IServiceCommand cmd);
}

@ -0,0 +1,6 @@
namespace Recyclarr.Command.Initialization;
public interface IServiceCleaner
{
void Cleanup();
}

@ -0,0 +1,6 @@
namespace Recyclarr.Command.Initialization;
public interface IServiceInitializationAndCleanup
{
Task Execute(IServiceCommand cmd, Func<Task> logic);
}

@ -0,0 +1,6 @@
namespace Recyclarr.Command.Initialization;
public interface IServiceInitializer
{
void Initialize(IServiceCommand cmd);
}

@ -0,0 +1,26 @@
using Autofac;
using Autofac.Extras.Ordering;
namespace Recyclarr.Command.Initialization;
public class InitializationAutofacModule : Module
{
protected override void Load(ContainerBuilder builder)
{
base.Load(builder);
builder.RegisterType<ServiceInitializationAndCleanup>().As<IServiceInitializationAndCleanup>();
// Initialization Services
builder.RegisterTypes(
typeof(ServiceInitializer),
typeof(ServicePreInitializer))
.As<IServiceInitializer>()
.OrderByRegistration();
// Cleanup Services
builder.RegisterTypes(
typeof(OldLogFileCleaner))
.As<IServiceCleaner>()
.OrderByRegistration();
}
}

@ -0,0 +1,16 @@
namespace Recyclarr.Command.Initialization;
internal class OldLogFileCleaner : IServiceCleaner
{
private readonly ILogJanitor _janitor;
public OldLogFileCleaner(ILogJanitor janitor)
{
_janitor = janitor;
}
public void Cleanup()
{
_janitor.DeleteOldestLogFiles(20);
}
}

@ -0,0 +1,31 @@
using MoreLinq.Extensions;
namespace Recyclarr.Command.Initialization;
public class ServiceInitializationAndCleanup : IServiceInitializationAndCleanup
{
private readonly IOrderedEnumerable<IServiceInitializer> _initializers;
private readonly IOrderedEnumerable<IServiceCleaner> _cleaners;
public ServiceInitializationAndCleanup(
IOrderedEnumerable<IServiceInitializer> initializers,
IOrderedEnumerable<IServiceCleaner> cleaners)
{
_initializers = initializers;
_cleaners = cleaners;
}
public async Task Execute(IServiceCommand cmd, Func<Task> logic)
{
try
{
_initializers.ForEach(x => x.Initialize(cmd));
await logic();
}
finally
{
_cleaners.ForEach(x => x.Cleanup());
}
}
}

@ -9,9 +9,9 @@ using TrashLib.Config.Settings;
using TrashLib.Extensions;
using TrashLib.Repo;
namespace Recyclarr.Command.Helpers;
namespace Recyclarr.Command.Initialization;
internal class ServiceInitialization : IServiceInitialization
internal class ServiceInitializer : IServiceInitializer
{
private readonly ILogger _log;
private readonly LoggingLevelSwitch _loggingLevelSwitch;
@ -19,7 +19,7 @@ internal class ServiceInitialization : IServiceInitialization
private readonly ISettingsProvider _settingsProvider;
private readonly IRepoUpdater _repoUpdater;
public ServiceInitialization(
public ServiceInitializer(
ILogger log,
LoggingLevelSwitch loggingLevelSwitch,
ISettingsPersister settingsPersister,

@ -0,0 +1,49 @@
using System.Text;
using CliFx.Exceptions;
using Recyclarr.Migration;
namespace Recyclarr.Command.Initialization;
internal class ServicePreInitializer : IServiceInitializer
{
private readonly IMigrationExecutor _migration;
public ServicePreInitializer(IMigrationExecutor migration)
{
_migration = migration;
}
public void Initialize(IServiceCommand cmd)
{
// Migrations are performed before we process command line arguments because we cannot instantiate any service
// objects via the DI container before migration logic is performed. This is due to the fact that migration
// steps may alter important files and directories which those services may depend on.
PerformMigrations();
}
private void PerformMigrations()
{
try
{
_migration.PerformAllMigrationSteps();
}
catch (MigrationException e)
{
var msg = new StringBuilder();
msg.AppendLine("Fatal exception during migration step. Details are below.\n");
msg.AppendLine($"Step That Failed: {e.OperationDescription}");
msg.AppendLine($"Failure Reason: {e.OriginalException.Message}");
if (e.Remediation.Any())
{
msg.AppendLine("\nPossible remediation steps:");
foreach (var remedy in e.Remediation)
{
msg.AppendLine($" - {remedy}");
}
}
throw new CommandException(msg.ToString());
}
}
}

@ -1,7 +1,7 @@
using CliFx.Attributes;
using JetBrains.Annotations;
using Recyclarr.Command.Initialization;
using Recyclarr.Command.Services;
using Recyclarr.Migration;
namespace Recyclarr.Command;
@ -16,8 +16,8 @@ internal class RadarrCommand : ServiceCommand, IRadarrCommand
public override string Name => "Radarr";
public RadarrCommand(IMigrationExecutor migration, Lazy<RadarrService> service)
: base(migration)
public RadarrCommand(IServiceInitializationAndCleanup init, Lazy<RadarrService> service)
: base(init)
{
_service = service;
}

@ -1,17 +1,14 @@
using System.Text;
using CliFx;
using CliFx.Attributes;
using CliFx.Exceptions;
using CliFx.Infrastructure;
using JetBrains.Annotations;
using Recyclarr.Migration;
using Recyclarr.Command.Initialization;
namespace Recyclarr.Command;
public abstract class ServiceCommand : ICommand, IServiceCommand
{
private readonly IMigrationExecutor _migration;
private readonly ILogJanitor _logJanitor;
private readonly IServiceInitializationAndCleanup _init;
[CommandOption("preview", 'p', Description =
"Only display the processed markdown results without making any API calls.")]
@ -30,53 +27,13 @@ public abstract class ServiceCommand : ICommand, IServiceCommand
public abstract string CacheStoragePath { get; }
public abstract string Name { get; }
protected ServiceCommand(IMigrationExecutor migration, ILogJanitor logJanitor)
protected ServiceCommand(IServiceInitializationAndCleanup init)
{
_migration = migration;
_logJanitor = logJanitor;
_init = init;
}
public async ValueTask ExecuteAsync(IConsole console)
{
// Stuff that needs to happen pre-service-initialization goes here
// Migrations are performed before we process command line arguments because we cannot instantiate any service
// objects via the DI container before migration logic is performed. This is due to the fact that migration
// steps may alter important files and directories which those services may depend on.
PerformMigrations();
// Initialize command services and execute business logic (system environment changes should be done by this
// point)
await Process();
_logJanitor.DeleteOldestLogFiles(20);
}
private void PerformMigrations()
{
try
{
_migration.PerformAllMigrationSteps();
}
catch (MigrationException e)
{
var msg = new StringBuilder();
msg.AppendLine("Fatal exception during migration step. Details are below.\n");
msg.AppendLine($"Step That Failed: {e.OperationDescription}");
msg.AppendLine($"Failure Reason: {e.OriginalException.Message}");
if (e.Remediation.Any())
{
msg.AppendLine("\nPossible remediation steps:");
foreach (var remedy in e.Remediation)
{
msg.AppendLine($" - {remedy}");
}
}
throw new CommandException(msg.ToString());
}
}
=> await _init.Execute(this, Process);
protected abstract Task Process();
}

@ -1,5 +1,4 @@
using Recyclarr.Command.Helpers;
using Recyclarr.Config;
using Recyclarr.Config;
using Serilog;
using TrashLib.Extensions;
using TrashLib.Radarr.Config;
@ -17,11 +16,9 @@ public class RadarrService : ServiceBase<IRadarrCommand>
public RadarrService(
ILogger log,
IServiceInitialization serviceInitialization,
IConfigurationLoader<RadarrConfiguration> configLoader,
Func<IRadarrQualityDefinitionUpdater> qualityUpdaterFactory,
Func<ICustomFormatUpdater> customFormatUpdaterFactory)
: base(log, serviceInitialization)
{
_log = log;
_configLoader = configLoader;

@ -1,7 +1,6 @@
using CliFx.Exceptions;
using System.Text;
using CliFx.Exceptions;
using Flurl.Http;
using Recyclarr.Command.Helpers;
using Serilog;
using TrashLib.Extensions;
using YamlDotNet.Core;
@ -12,44 +11,31 @@ namespace Recyclarr.Command.Services;
/// </summary>
public abstract class ServiceBase<T> where T : IServiceCommand
{
private readonly ILogger _log;
private readonly IServiceInitialization _serviceInitialization;
protected ServiceBase(ILogger log, IServiceInitialization serviceInitialization)
{
_log = log;
_serviceInitialization = serviceInitialization;
}
public async Task Execute(T cmd)
{
try
{
_serviceInitialization.Initialize(cmd);
await Process(cmd);
}
catch (YamlException e)
{
var message = e.InnerException is not null ? e.InnerException.Message : e.Message;
_log.Error("Found Unrecognized YAML Property: {ErrorMsg}", message);
_log.Error("Please remove the property quoted in the above message from your YAML file");
throw new CommandException("Exiting due to invalid configuration");
var msg = new StringBuilder();
msg.AppendLine($"Found Unrecognized YAML Property: {message}");
msg.AppendLine("Please remove the property quoted in the above message from your YAML file");
msg.AppendLine("Exiting due to invalid configuration");
throw new CommandException(msg.ToString());
}
catch (FlurlHttpException e)
{
_log.Error("HTTP error while communicating with {ServiceName}: {Msg}", cmd.Name,
e.SanitizedExceptionMessage());
ExitDueToFailure();
throw new CommandException(
$"HTTP error while communicating with {cmd.Name}: {e.SanitizedExceptionMessage()}");
}
catch (Exception e) when (e is not CommandException)
{
_log.Error(e, "Unrecoverable Exception");
ExitDueToFailure();
throw new CommandException(e.ToString());
}
}
protected abstract Task Process(T cmd);
private static void ExitDueToFailure()
=> throw new CommandException("Exiting due to previous exception");
}

@ -1,5 +1,4 @@
using CliFx.Exceptions;
using Recyclarr.Command.Helpers;
using Recyclarr.Config;
using Serilog;
using TrashLib.Extensions;
@ -20,12 +19,10 @@ public class SonarrService : ServiceBase<ISonarrCommand>
public SonarrService(
ILogger log,
IServiceInitialization serviceInitialization,
IConfigurationLoader<SonarrConfiguration> configLoader,
Func<IReleaseProfileUpdater> profileUpdaterFactory,
Func<ISonarrQualityDefinitionUpdater> qualityUpdaterFactory,
IReleaseProfileLister lister)
: base(log, serviceInitialization)
{
_log = log;
_configLoader = configLoader;

@ -1,7 +1,7 @@
using CliFx.Attributes;
using JetBrains.Annotations;
using Recyclarr.Command.Initialization;
using Recyclarr.Command.Services;
using Recyclarr.Migration;
namespace Recyclarr.Command;
@ -27,8 +27,8 @@ internal class SonarrCommand : ServiceCommand, ISonarrCommand
public override string Name => "Sonarr";
public SonarrCommand(IMigrationExecutor migration, Lazy<SonarrService> service)
: base(migration)
public SonarrCommand(IServiceInitializationAndCleanup init, Lazy<SonarrService> service)
: base(init)
{
_service = service;
}

@ -7,6 +7,7 @@ using CliFx;
using CliFx.Infrastructure;
using Common;
using Recyclarr.Command.Helpers;
using Recyclarr.Command.Initialization;
using Recyclarr.Command.Services;
using Recyclarr.Config;
using Recyclarr.Migration;
@ -63,7 +64,7 @@ public static class CompositionRoot
{
builder.RegisterType<SonarrService>();
builder.RegisterType<RadarrService>();
builder.RegisterType<ServiceInitialization>().As<IServiceInitialization>();
builder.RegisterType<ServiceInitializer>().As<IServiceInitializer>();
// Register all types deriving from CliFx's ICommand. These are all of our supported subcommands.
builder.RegisterAssemblyTypes(Assembly.GetExecutingAssembly())
@ -98,13 +99,13 @@ public static class CompositionRoot
ConfigurationRegistrations(builder);
CommandRegistrations(builder);
SetupLogging(builder);
builder.RegisterModule<SonarrAutofacModule>();
builder.RegisterModule<RadarrAutofacModule>();
builder.RegisterModule<VersionControlAutofacModule>();
builder.RegisterModule<MigrationAutofacModule>();
builder.RegisterModule<InitializationAutofacModule>();
builder.Register(_ => AutoMapperConfig.Setup()).SingleInstance();

Loading…
Cancel
Save