< Summary

Line coverage
85%
Covered lines: 73
Uncovered lines: 12
Coverable lines: 85
Total lines: 276
Line coverage: 85.8%
Branch coverage
92%
Covered branches: 13
Total branches: 14
Branch coverage: 92.8%
Method coverage

Feature is only available for sponsors

Upgrade to PRO version

Metrics

MethodBranch coverage Crap Score Cyclomatic complexity Line coverage
File 1: .cctor()100%210%
File 2: .ctor(...)100%11100%
File 2: RunAsync()100%11100%
File 2: Compensate(...)100%44100%
File 2: TryDelete(...)100%22100%
File 2: DiscardTemporaryFiles(...)100%2266.66%
File 2: TryRemoveIfEmpty(...)75%4469.23%
File 2: BuildValidated(...)100%11100%
File 2: Validate(...)100%22100%

File(s)

/home/runner/work/anichron/anichron/src/.artifacts/obj/Anichron.Worker/debug/Microsoft.Extensions.Logging.Generators/Microsoft.Extensions.Logging.Generators.LoggerMessageGenerator/LoggerMessage.g.cs

File '/home/runner/work/anichron/anichron/src/.artifacts/obj/Anichron.Worker/debug/Microsoft.Extensions.Logging.Generators/Microsoft.Extensions.Logging.Generators.LoggerMessageGenerator/LoggerMessage.g.cs' does not exist (any more).

/home/runner/work/anichron/anichron/src/Anichron.Worker/Ingestion/Pipeline/IngestionPipelineRunner.cs

#LineLine coverage
 1using Anichron.Worker.Ingestion.Proxy;
 2using Anichron.Worker.Settings;
 3using Microsoft.Extensions.Options;
 4using System.IO.Abstractions;
 5
 6namespace Anichron.Worker.Ingestion.Pipeline;
 7
 8internal interface IIngestionPipelineRunner
 9{
 10    Task RunAsync(IngestionContext context, CancellationToken ct);
 11}
 12
 2713internal sealed partial class IngestionPipelineRunner(
 2714    IEnumerable<IIngestionMiddleware> middlewares,
 2715    IProxyDirectoryStrategy proxyDirectoryStrategy,
 2716    IFileSystem fileSystem,
 2717    IOptions<WorkerSettings> settings,
 2718    ILogger<IngestionPipelineRunner> logger) : IIngestionPipelineRunner
 19{
 2820    private readonly IngestionDelegate pipeline = BuildValidated([.. middlewares], logger);
 21
 22    public async Task RunAsync(IngestionContext context, CancellationToken ct)
 2523    {
 24        try
 2525        {
 2526            await pipeline(context, ct);
 1227        }
 1328        catch (Exception ex) when (!IngestionShutdown.IsInProgress(ex, ct))
 1029        {
 30            // The pipeline generates proxies before it writes the asset row, so a failure part-way
 31            // through leaves files on disk that no row will ever reference. This is the one place
 32            // that sees every file written across every step, so cleanup belongs here.
 1033            Compensate(context);
 1034            throw;
 35        }
 1236    }
 37
 38    private void Compensate(IngestionContext context)
 1039    {
 1040        var proxyRoot = settings.Value.ProxyPath;
 1041        var proxies = context.ProxyFiles;
 42
 5043        foreach (var proxy in proxies)
 1044            TryDelete(fileSystem.Path.Combine(proxyRoot, proxy.ProxyPath));
 45
 46        // Asking the strategy rather than deriving the directory from the proxy paths keeps the
 47        // path rule in one place, and covers the attempt that failed in its first generator: it
 48        // registered no proxy at all, but the step had already created the directory.
 1049        if (context.ContentHash is { } contentHash)
 950        {
 951            var directory = fileSystem.Path.Combine(
 952                proxyRoot, proxyDirectoryStrategy.GetDirectory(context.Config.Id, contentHash));
 953            DiscardTemporaryFiles(directory);
 954            TryRemoveIfEmpty(directory);
 955        }
 56
 1057        Log.Compensated(logger, proxies.Count, context.Item.RelativePath);
 1058    }
 59
 60    // Deletion runs while the original exception is in flight, so a file that cannot be deleted is
 61    // reported and skipped: one locked file must not strand the others or replace the real error.
 62    private void TryDelete(string absolutePath)
 1263    {
 64        try
 1265        {
 1266            if (fileSystem.File.Exists(absolutePath))
 1267                fileSystem.File.Delete(absolutePath);
 1068        }
 269        catch (Exception ex) when (ex is IOException or UnauthorizedAccessException)
 270        {
 271            Log.DeleteFailed(logger, absolutePath, ex);
 272        }
 1273    }
 74
 75    private void DiscardTemporaryFiles(string directory)
 976    {
 77        try
 978        {
 979            var temporaryFiles = fileSystem.Directory.EnumerateFiles(
 980                directory, ProxyStagingWriter.TemporaryFileSearchPattern);
 3181            foreach (var temporaryPath in temporaryFiles)
 282                TryDelete(temporaryPath);
 983        }
 084        catch (Exception ex) when (ex is IOException or UnauthorizedAccessException)
 085        {
 086            Log.DeleteFailed(logger, directory, ex);
 087        }
 988    }
 89
 90    // Only when empty: the directory is keyed by storage config and content hash, so a duplicate
 91    // asset can share it until the unique index in #159 lands, and a recursive delete would
 92    // destroy that sibling's proxies.
 93    private void TryRemoveIfEmpty(string directory)
 994    {
 95        try
 996        {
 997            if (fileSystem.Directory.Exists(directory)
 998                && !fileSystem.Directory.EnumerateFileSystemEntries(directory).Any())
 699            {
 6100                fileSystem.Directory.Delete(directory);
 6101            }
 9102        }
 0103        catch (Exception ex) when (ex is IOException or UnauthorizedAccessException)
 0104        {
 0105            Log.DeleteFailed(logger, directory, ex);
 0106        }
 9107    }
 108
 109    private static IngestionDelegate BuildValidated(
 110        IReadOnlyList<IIngestionMiddleware> middlewares, ILogger logger)
 28111    {
 28112        Validate(middlewares);
 27113        var ordered = middlewares.OrderBy(m => m.Order).ToArray();
 27114        return IngestionPipelineBuilder.Build(ordered, logger);
 27115    }
 116
 117    private static void Validate(IReadOnlyList<IIngestionMiddleware> middlewares)
 28118    {
 28119        var duplicates = middlewares
 28120            .GroupBy(m => m.Order)
 28121            .Where(g => g.Count() > 1)
 28122            .Select(g => g.Key)
 28123            .ToList();
 28124        if (duplicates.Count > 0)
 1125        {
 1126            throw new InvalidOperationException(
 1127                $"Duplicate middleware orders: {string.Join(", ", duplicates)}");
 128        }
 27129    }
 130
 131    private static partial class Log
 132    {
 133        [LoggerMessage(Level = LogLevel.Information,
 134            Message = "Cleaned up {Count} proxy file(s) after a failed ingestion of {RelativePath}.")]
 135        public static partial void Compensated(ILogger logger, int count, string relativePath);
 136
 137        [LoggerMessage(Level = LogLevel.Warning, Message = "Could not delete {Path} during cleanup.")]
 138        public static partial void DeleteFailed(ILogger logger, string path, Exception ex);
 139    }
 140}