Deduplicate MassTransit routing slips, activities and consumers

This commit is contained in:
2026-09-24 12:25:55 +02:00
parent 0cba32f776
commit 405df082cc
76 changed files with 922 additions and 1933 deletions
@@ -1,10 +1,9 @@
using MassTransit;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Logging;
using Musify.Application.Contracts;
using Musify.Domain.Entities;
using Musify.Domain.ValueObjects;
using Musify.Infrastructure.MassTransit.Arguments;
using Musify.Infrastructure.MassTransit.RoutingSlip;
namespace Musify.Infrastructure.MassTransit.Activities.Audio;
@@ -17,26 +16,15 @@ internal class GenerateAudioWorkflowPathsActivity(
public async Task<ExecutionResult> Execute(ExecuteContext<GenerateAudioWorkflowPathsArguments> executeContext)
{
Track track;
try
{
var getTrack = await database.Tracks.SingleOrDefaultAsync(p => p.Id == executeContext.Arguments.TrackId, executeContext.CancellationToken);
track = getTrack ?? throw new InvalidOperationException($"Track with id {executeContext.Arguments.TrackId} not found.");
}
catch (Exception exception)
{
logger.LogError(exception, "Failed to retrieve track for TrackId {TrackId}",
executeContext.Arguments.TrackId);
throw;
}
var arguments = executeContext.Arguments;
var track = await TrackAudioStatus.LoadAsync(database, arguments.TrackId, executeContext.CancellationToken);
try
{
track.Audio.TranscodeStatus = ProcessingStatus.Processing;
var folderName = Guid.NewGuid().ToString();
var sourceFileName = Path.GetFileName(executeContext.Arguments.SourceKey);
var workingDirectory = Path.Combine(executeContext.Arguments.TemporaryRootDirectory, folderName);
var sourceFilePath = Path.Combine(workingDirectory, sourceFileName);
var workingDirectory = Path.Combine(arguments.TemporaryRootDirectory, Guid.NewGuid().ToString());
var sourceFilePath = Path.Combine(workingDirectory, Path.GetFileName(arguments.SourceKey));
Directory.CreateDirectory(workingDirectory);
await database.SaveChangesAsync(executeContext.CancellationToken);
@@ -50,22 +38,9 @@ internal class GenerateAudioWorkflowPathsActivity(
[RoutingSlipVariableNames.Audio.TranscodedDirectory] = workingDirectory
});
}
catch (Exception exception)
catch
{
logger.LogError(exception, "Failed to generate audio workflow paths for {SourceKey}",
executeContext.Arguments.SourceKey);
try
{
track.Audio.TranscodeStatus = ProcessingStatus.Failed;
await database.SaveChangesAsync(executeContext.CancellationToken);
}
catch (Exception dbException)
{
logger.LogError(dbException, "Failed to update ProcessingStatus to Failed for track {TrackId}",
executeContext.Arguments.TrackId);
}
await TrackAudioStatus.MarkFailedAsync(database, arguments.TrackId, executeContext.CancellationToken);
throw;
}
}
@@ -0,0 +1,24 @@
using Microsoft.EntityFrameworkCore;
using Musify.Application.Contracts;
using Musify.Domain.Entities;
using Musify.Domain.ValueObjects;
namespace Musify.Infrastructure.MassTransit.Activities.Audio;
/// <summary>Shared bookkeeping of the audio processing activities.</summary>
internal static class TrackAudioStatus
{
public static async Task<Track> LoadAsync(IDatabase database, Guid trackId, CancellationToken cancellationToken) =>
await database.Tracks.SingleOrDefaultAsync(track => track.Id == trackId, cancellationToken)
?? throw new InvalidOperationException($"Track with id {trackId} not found");
public static async Task MarkFailedAsync(IDatabase database, Guid trackId, CancellationToken cancellationToken)
{
var track = await database.Tracks.SingleOrDefaultAsync(t => t.Id == trackId, cancellationToken);
if (track == null)
return;
track.Audio.TranscodeStatus = ProcessingStatus.Failed;
await database.SaveChangesAsync(cancellationToken);
}
}
@@ -1,11 +1,9 @@
using MassTransit;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Logging;
using Musify.Application.Contracts;
using Musify.Domain.Entities;
using Musify.Domain.ValueObjects;
using Musify.Infrastructure.MassTransit.Arguments;
using Musify.Infrastructure.MassTransit.Logs;
using Musify.Infrastructure.MassTransit.RoutingSlip;
namespace Musify.Infrastructure.MassTransit.Activities.Audio;
@@ -19,22 +17,13 @@ internal class TranscodeAudioActivity(
public async Task<ExecutionResult> Execute(ExecuteContext<TranscodeAudioArguments> executeContext)
{
var sourceFilePath = executeContext.GetVariable<string>(executeContext.Arguments.SourceFilePathVariable);
ArgumentNullException.ThrowIfNull(sourceFilePath, nameof(sourceFilePath));
var workingDirectory = executeContext.GetVariable<string>(executeContext.Arguments.WorkingDirectoryVariable);
ArgumentNullException.ThrowIfNull(workingDirectory, nameof(workingDirectory));
var arguments = executeContext.Arguments;
var sourceFilePath = executeContext.GetVariable<string>(arguments.SourceFilePathVariable);
ArgumentNullException.ThrowIfNull(sourceFilePath);
var workingDirectory = executeContext.GetVariable<string>(arguments.WorkingDirectoryVariable);
ArgumentNullException.ThrowIfNull(workingDirectory);
Track track;
try
{
var getTrack = await database.Tracks.SingleOrDefaultAsync(t => t.Id == executeContext.Arguments.TrackId, executeContext.CancellationToken);
track = getTrack ?? throw new InvalidOperationException($"Track with Id {executeContext.Arguments.TrackId} not found");
}
catch (Exception exception)
{
logger.LogError(exception, "Failed to retrieve track with Id {TrackId} from database", executeContext.Arguments.TrackId);
throw;
}
await TrackAudioStatus.LoadAsync(database, arguments.TrackId, executeContext.CancellationToken);
try
{
@@ -42,9 +31,7 @@ internal class TranscodeAudioActivity(
await using (var fileStream = File.OpenRead(sourceFilePath))
{
transcodeResult = await audioTranscoder.TranscodeToAudioFileAsync(
fileStream,
workingDirectory,
executeContext.CancellationToken);
fileStream, workingDirectory, executeContext.CancellationToken);
}
if (transcodeResult.StatusCode != 0)
@@ -52,51 +39,27 @@ internal class TranscodeAudioActivity(
File.Delete(sourceFilePath);
var log = new TranscodeAudioLog(workingDirectory);
return executeContext.CompletedWithVariables(log, new Dictionary<string, object>
return executeContext.CompletedWithVariables(new TranscodeAudioLog(workingDirectory), new Dictionary<string, object>
{
[RoutingSlipVariableNames.Audio.DurationSeconds] = transcodeResult.Duration.TotalSeconds
});
}
catch (Exception exception)
catch
{
logger.LogError(exception, "Failed to transcode {SourceFilePath}", sourceFilePath);
try
{
track.Audio.TranscodeStatus = ProcessingStatus.Failed;
await database.SaveChangesAsync(executeContext.CancellationToken);
}
catch (Exception dbException)
{
logger.LogError(dbException, "Failed to update ProcessingStatus to Failed for track {TrackId}",
executeContext.Arguments.TrackId);
}
await TrackAudioStatus.MarkFailedAsync(database, arguments.TrackId, executeContext.CancellationToken);
throw;
}
}
public Task<CompensationResult> Compensate(CompensateContext<TranscodeAudioLog> compensateContext)
{
try
{
if (Directory.Exists(compensateContext.Log.WorkingDirectory))
{
Directory.Delete(compensateContext.Log.WorkingDirectory, recursive: true);
}
else
{
logger.LogWarning("Working directory {WorkingDirectory} not found during compensation",
compensateContext.Log.WorkingDirectory);
}
var workingDirectory = compensateContext.Log.WorkingDirectory;
return Task.FromResult(compensateContext.Compensated());
}
catch (Exception exception)
{
logger.LogError(exception, "Failed to compensate transcoded audio directory {WorkingDirectory}", compensateContext.Log.WorkingDirectory);
return Task.FromResult(compensateContext.Failed(exception));
}
if (Directory.Exists(workingDirectory))
Directory.Delete(workingDirectory, recursive: true);
else
logger.LogWarning("Working directory {WorkingDirectory} not found during compensation", workingDirectory);
return Task.FromResult(compensateContext.Compensated());
}
}
@@ -1,9 +1,7 @@
using MassTransit;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Logging;
using Musify.Application.Contracts;
using Musify.Application.Events;
using Musify.Domain.Entities;
using Musify.Domain.ValueObjects;
using Musify.Infrastructure.MassTransit.Arguments;
using Musify.Infrastructure.MassTransit.Logs;
@@ -20,92 +18,49 @@ internal class UpdateTrackAudioActivity(
public async Task<ExecutionResult> Execute(ExecuteContext<UpdateTrackAudioArguments> executeContext)
{
Track track;
try
{
var getTrack = await database.Tracks.SingleOrDefaultAsync(t => t.Id == executeContext.Arguments.TrackId, executeContext.CancellationToken);
track = getTrack ?? throw new InvalidOperationException($"Track with id {executeContext.Arguments.TrackId} not found");
}
catch (Exception exception)
{
logger.LogError(exception, "Failed to retrieve track {TrackId}",
executeContext.Arguments.TrackId);
throw;
}
var audioFolderKey = executeContext.Arguments.AudioFolderKey;
logger.LogDebug("Resolved audio folder key: {AudioFolderKey}", audioFolderKey);
if (string.IsNullOrWhiteSpace(audioFolderKey))
{
throw new InvalidOperationException($"Audio folder key is empty for track {track.Id}");
}
var arguments = executeContext.Arguments;
var track = await TrackAudioStatus.LoadAsync(database, arguments.TrackId, executeContext.CancellationToken);
try
{
var durationSeconds = executeContext.GetVariable<double>(executeContext.Arguments.DurationSecondsVariable)
?? throw new InvalidOperationException($"Missing routing slip variable {executeContext.Arguments.DurationSecondsVariable}");
if (string.IsNullOrWhiteSpace(arguments.AudioFolderKey))
throw new InvalidOperationException($"Audio folder key is empty for track {track.Id}");
var log = new UpdateTrackAudioLog(
track.Id,
track.Audio.FolderName,
track.DurationSeconds);
var durationSeconds = executeContext.GetVariable<double>(arguments.DurationSecondsVariable)
?? throw new InvalidOperationException($"Missing routing slip variable {arguments.DurationSecondsVariable}");
track.Audio.FolderName = Path.GetFileName(audioFolderKey);
var log = new UpdateTrackAudioLog(track.Id, track.Audio.FolderName, track.DurationSeconds);
track.Audio.FolderName = Path.GetFileName(arguments.AudioFolderKey);
track.DurationSeconds = durationSeconds;
track.Audio.TranscodeStatus = ProcessingStatus.Completed;
await database.SaveChangesAsync(executeContext.CancellationToken);
logger.LogInformation("Updated track {TrackId} audio", executeContext.Arguments.TrackId);
logger.LogInformation("Updated track {TrackId} audio", arguments.TrackId);
await publishEndpoint.Publish(
new TrackAudioProcessed(executeContext.Arguments.TrackId),
executeContext.CancellationToken);
await publishEndpoint.Publish(new TrackAudioProcessed(arguments.TrackId), executeContext.CancellationToken);
return executeContext.Completed(log);
}
catch (Exception exception)
catch
{
logger.LogError(exception, "Failed to update track {TrackId} audio",
executeContext.Arguments.TrackId);
try
{
track.Audio.TranscodeStatus = ProcessingStatus.Failed;
await database.SaveChangesAsync(executeContext.CancellationToken);
}
catch (Exception dbException)
{
logger.LogError(dbException, "Failed to update ProcessingStatus to Failed for track {TrackId}",
executeContext.Arguments.TrackId);
}
await TrackAudioStatus.MarkFailedAsync(database, arguments.TrackId, executeContext.CancellationToken);
throw;
}
}
public async Task<CompensationResult> Compensate(CompensateContext<UpdateTrackAudioLog> compensateContext)
{
try
{
var track = await database.Tracks.SingleOrDefaultAsync(t => t.Id == compensateContext.Log.TrackId, compensateContext.CancellationToken);
if (track == null)
throw new InvalidOperationException($"Track with id {compensateContext.Log.TrackId} not found");
var log = compensateContext.Log;
track.Audio.FolderName = compensateContext.Log.PreviousAudioFolderName;
track.DurationSeconds = compensateContext.Log.PreviousDurationSeconds;
track.Audio.TranscodeStatus = ProcessingStatus.Failed;
await database.SaveChangesAsync(compensateContext.CancellationToken);
var track = await TrackAudioStatus.LoadAsync(database, log.TrackId, compensateContext.CancellationToken);
track.Audio.FolderName = log.PreviousAudioFolderName;
track.DurationSeconds = log.PreviousDurationSeconds;
track.Audio.TranscodeStatus = ProcessingStatus.Failed;
await database.SaveChangesAsync(compensateContext.CancellationToken);
logger.LogInformation("Compensated track {TrackId} audio", compensateContext.Log.TrackId);
logger.LogInformation("Compensated track {TrackId} audio", log.TrackId);
return compensateContext.Compensated();
}
catch (Exception exception)
{
logger.LogError(exception, "Failed to compensate track {TrackId} audio",
compensateContext.Log.TrackId);
return compensateContext.Failed(exception);
}
return compensateContext.Compensated();
}
}