From 93ee8be0cd97e29f74739a9af537136b987c4518 Mon Sep 17 00:00:00 2001 From: XShine555 Date: Sun, 5 Jul 2026 14:55:40 +0200 Subject: [PATCH] feat(infra): playlist processing saga + generalize fault handling (phase 3) Adds the PlayList picture-processing lifecycle saga (correlated by PlayListId) mirroring the track one, and generalizes the failure plumbing: - Rename the track fault translator to ProcessingSlipFaultConsumer and use a generic Workflow.SubjectId variable + ProcessKind; it now emits typed Track{Picture,Audio}/PlayListPicture ProcessingFailed events for all slips - PlayListProcessingStateMachine (+ EF-persisted PlayListProcessingState, migration): starts on CreatePlayListResourcesEvent, finalizes on PlayListPictureProcessed, and on failure marks the playlist Failed and removes the orphaned final picture (PlayListProcessingFailedConsumer + MarkPlayListAsFailedActivity) - UpdatePlayListPictureActivity publishes PlayListPictureProcessed on success - Picture slip carries SubjectId + PlayListPicture kind + fault subscription Verified end-to-end in Docker (RabbitMQ + migrated Postgres): both saga endpoints, the fault translator and cleanup consumers configure, and an addressing cross-check confirms all 21 activity execute queues the builders address exist (caught + fixed a kebab mismatch on mark-play-list-as-failed). Build: 0 warnings, 0 errors. --- .../Events/PlayListPictureProcessed.cs | 4 + .../Events/PlayListPictureProcessingFailed.cs | 4 + .../Events/PlayListProcessingFailed.cs | 7 + .../Pictures/UpdatePlayListPictureActivity.cs | 7 + .../PlayLists/MarkPlayListAsFailedActivity.cs | 32 + .../MarkPlayListAsFailedArguments.cs | 4 + .../PlayListProcessingFailedConsumer.cs | 36 + ...umer.cs => ProcessingSlipFaultConsumer.cs} | 15 +- ...TransitDependencyInjection.Registration.cs | 4 +- .../MassTransitDependencyInjection.cs | 8 + .../MassTransit/RoutingSlip/ActivityNames.cs | 1 + .../AudioWorkflowRoutingSlipBuilder.cs | 4 +- .../PictureWorkflowRoutingSlipBuilder.cs | 10 +- .../RoutingSlip/RoutingSlipVariableNames.cs | 3 +- .../Sagas/PlayListProcessingState.cs | 19 + .../Sagas/PlayListProcessingStateMachine.cs | 51 ++ .../Persistence/Database.cs | 5 + ...AddPlayListProcessingStateSaga.Designer.cs | 646 ++++++++++++++++++ ...05125356_AddPlayListProcessingStateSaga.cs | 38 ++ .../Migrations/DatabaseModelSnapshot.cs | 27 + 20 files changed, 913 insertions(+), 12 deletions(-) create mode 100644 backend/Musify.Application/Events/PlayListPictureProcessed.cs create mode 100644 backend/Musify.Application/Events/PlayListPictureProcessingFailed.cs create mode 100644 backend/Musify.Application/Events/PlayListProcessingFailed.cs create mode 100644 backend/Musify.Infrastructure/MassTransit/Activities/PlayLists/MarkPlayListAsFailedActivity.cs create mode 100644 backend/Musify.Infrastructure/MassTransit/Arguments/MarkPlayListAsFailedArguments.cs create mode 100644 backend/Musify.Infrastructure/MassTransit/Consumers/PlayListProcessingFailedConsumer.cs rename backend/Musify.Infrastructure/MassTransit/Consumers/{TrackProcessingSlipFaultConsumer.cs => ProcessingSlipFaultConsumer.cs} (64%) create mode 100644 backend/Musify.Infrastructure/MassTransit/Sagas/PlayListProcessingState.cs create mode 100644 backend/Musify.Infrastructure/MassTransit/Sagas/PlayListProcessingStateMachine.cs create mode 100644 backend/Musify.Infrastructure/Persistence/Migrations/20260705125356_AddPlayListProcessingStateSaga.Designer.cs create mode 100644 backend/Musify.Infrastructure/Persistence/Migrations/20260705125356_AddPlayListProcessingStateSaga.cs diff --git a/backend/Musify.Application/Events/PlayListPictureProcessed.cs b/backend/Musify.Application/Events/PlayListPictureProcessed.cs new file mode 100644 index 0000000..74f03f2 --- /dev/null +++ b/backend/Musify.Application/Events/PlayListPictureProcessed.cs @@ -0,0 +1,4 @@ +namespace Musify.Application.Events +{ + public record PlayListPictureProcessed(Guid PlayListId); +} diff --git a/backend/Musify.Application/Events/PlayListPictureProcessingFailed.cs b/backend/Musify.Application/Events/PlayListPictureProcessingFailed.cs new file mode 100644 index 0000000..661932c --- /dev/null +++ b/backend/Musify.Application/Events/PlayListPictureProcessingFailed.cs @@ -0,0 +1,4 @@ +namespace Musify.Application.Events +{ + public record PlayListPictureProcessingFailed(Guid PlayListId); +} diff --git a/backend/Musify.Application/Events/PlayListProcessingFailed.cs b/backend/Musify.Application/Events/PlayListProcessingFailed.cs new file mode 100644 index 0000000..50a5054 --- /dev/null +++ b/backend/Musify.Application/Events/PlayListProcessingFailed.cs @@ -0,0 +1,7 @@ +namespace Musify.Application.Events +{ + public record PlayListProcessingFailed( + Guid PlayListId, + string? Bucket, + string? PictureKey); +} diff --git a/backend/Musify.Infrastructure/MassTransit/Activities/Pictures/UpdatePlayListPictureActivity.cs b/backend/Musify.Infrastructure/MassTransit/Activities/Pictures/UpdatePlayListPictureActivity.cs index 464b105..4d38041 100644 --- a/backend/Musify.Infrastructure/MassTransit/Activities/Pictures/UpdatePlayListPictureActivity.cs +++ b/backend/Musify.Infrastructure/MassTransit/Activities/Pictures/UpdatePlayListPictureActivity.cs @@ -1,6 +1,7 @@ using MassTransit; using Microsoft.Extensions.Logging; using Musify.Application.Contracts; +using Musify.Application.Events; using Musify.Infrastructure.MassTransit.Arguments; using Musify.Infrastructure.MassTransit.Logs; @@ -8,6 +9,7 @@ namespace Musify.Infrastructure.MassTransit.Activities.Pictures { internal class UpdatePlayListPictureActivity( IDatabase database, + IPublishEndpoint publishEndpoint, ILogger logger) : IActivity { @@ -52,6 +54,11 @@ namespace Musify.Infrastructure.MassTransit.Activities.Pictures logger.LogInformation("Updated playlist {PlayListId} pictures", executeContext.Arguments.PlayListId); + + await publishEndpoint.Publish( + new PlayListPictureProcessed(executeContext.Arguments.PlayListId), + executeContext.CancellationToken); + return executeContext.Completed(); } catch (Exception exception) diff --git a/backend/Musify.Infrastructure/MassTransit/Activities/PlayLists/MarkPlayListAsFailedActivity.cs b/backend/Musify.Infrastructure/MassTransit/Activities/PlayLists/MarkPlayListAsFailedActivity.cs new file mode 100644 index 0000000..41f4ddd --- /dev/null +++ b/backend/Musify.Infrastructure/MassTransit/Activities/PlayLists/MarkPlayListAsFailedActivity.cs @@ -0,0 +1,32 @@ +using MassTransit; +using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.Logging; +using Musify.Application.Contracts; +using Musify.Domain.ValueObjects; +using Musify.Infrastructure.MassTransit.Arguments; + +namespace Musify.Infrastructure.MassTransit.Activities.PlayLists +{ + internal class MarkPlayListAsFailedActivity( + IDatabase database, + ILogger logger) + : IExecuteActivity + { + public const string ExecuteEndpointName = "mark-play-list-as-failed"; + + public async Task Execute(ExecuteContext executeContext) + { + var playList = await database.PlayLists + .SingleOrDefaultAsync(p => p.Id == executeContext.Arguments.PlayListId, executeContext.CancellationToken); + + if (playList is not null) + { + playList.LifeCycleStatus = LifeCycleStatus.Failed; + await database.SaveChangesAsync(executeContext.CancellationToken); + logger.LogInformation("Marked playlist {PlayListId} as failed", executeContext.Arguments.PlayListId); + } + + return executeContext.Completed(); + } + } +} diff --git a/backend/Musify.Infrastructure/MassTransit/Arguments/MarkPlayListAsFailedArguments.cs b/backend/Musify.Infrastructure/MassTransit/Arguments/MarkPlayListAsFailedArguments.cs new file mode 100644 index 0000000..386c9ec --- /dev/null +++ b/backend/Musify.Infrastructure/MassTransit/Arguments/MarkPlayListAsFailedArguments.cs @@ -0,0 +1,4 @@ +namespace Musify.Infrastructure.MassTransit.Arguments +{ + public record MarkPlayListAsFailedArguments(Guid PlayListId); +} diff --git a/backend/Musify.Infrastructure/MassTransit/Consumers/PlayListProcessingFailedConsumer.cs b/backend/Musify.Infrastructure/MassTransit/Consumers/PlayListProcessingFailedConsumer.cs new file mode 100644 index 0000000..9f0292d --- /dev/null +++ b/backend/Musify.Infrastructure/MassTransit/Consumers/PlayListProcessingFailedConsumer.cs @@ -0,0 +1,36 @@ +using MassTransit; +using MassTransit.Courier.Contracts; +using Musify.Application.Events; +using Musify.Infrastructure.MassTransit.Activities.Files; +using Musify.Infrastructure.MassTransit.Activities.PlayLists; +using Musify.Infrastructure.MassTransit.Arguments; +using Musify.Infrastructure.MassTransit.RoutingSlip; + +namespace Musify.Infrastructure.MassTransit.Consumers +{ + public class PlayListProcessingFailedConsumer(IBus bus) : IConsumer + { + public async Task Consume(ConsumeContext context) + { + var message = context.Message; + + var builder = new RoutingSlipBuilder(NewId.NextGuid()); + builder.AddSubscription( + EndpointHelper.BuildConsumerUri(RoutingSlipCleanUpConsumer.QueueName), + RoutingSlipEvents.Completed | RoutingSlipEvents.Faulted); + + if (!string.IsNullOrWhiteSpace(message.Bucket) && !string.IsNullOrWhiteSpace(message.PictureKey)) + builder.AddActivity( + ActivityNames.RemovePlayListOriginalPicture, + EndpointHelper.BuildExecuteActivityUri(RemoveFileFromBucketActivity.ExecuteEndpointName), + new RemoveFileFromBucketArguments(message.Bucket, message.PictureKey)); + + builder.AddActivity( + ActivityNames.MarkPlayListAsFailed, + EndpointHelper.BuildExecuteActivityUri(MarkPlayListAsFailedActivity.ExecuteEndpointName), + new MarkPlayListAsFailedArguments(message.PlayListId)); + + await bus.Execute(builder.Build()); + } + } +} diff --git a/backend/Musify.Infrastructure/MassTransit/Consumers/TrackProcessingSlipFaultConsumer.cs b/backend/Musify.Infrastructure/MassTransit/Consumers/ProcessingSlipFaultConsumer.cs similarity index 64% rename from backend/Musify.Infrastructure/MassTransit/Consumers/TrackProcessingSlipFaultConsumer.cs rename to backend/Musify.Infrastructure/MassTransit/Consumers/ProcessingSlipFaultConsumer.cs index edab8bb..fd7e8bf 100644 --- a/backend/Musify.Infrastructure/MassTransit/Consumers/TrackProcessingSlipFaultConsumer.cs +++ b/backend/Musify.Infrastructure/MassTransit/Consumers/ProcessingSlipFaultConsumer.cs @@ -4,9 +4,9 @@ using Musify.Application.Events; namespace Musify.Infrastructure.MassTransit.Consumers { - public class TrackProcessingSlipFaultConsumer : IConsumer + public class ProcessingSlipFaultConsumer : IConsumer { - public const string QueueName = "track-processing-slip-fault"; + public const string QueueName = "processing-slip-fault"; public async Task Consume(ConsumeContext context) { @@ -15,17 +15,20 @@ namespace Musify.Infrastructure.MassTransit.Consumers if (!variables.TryGetValue(RoutingSlipVariableNames.Workflow.ProcessKind, out var kindObject)) return; - if (!variables.TryGetValue(RoutingSlipVariableNames.Workflow.TrackId, out var trackObject) - || !Guid.TryParse(trackObject?.ToString(), out var trackId)) + if (!variables.TryGetValue(RoutingSlipVariableNames.Workflow.SubjectId, out var subjectObject) + || !Guid.TryParse(subjectObject?.ToString(), out var subjectId)) return; switch (kindObject?.ToString()) { case RoutingSlipVariableNames.ProcessKinds.TrackPicture: - await context.Publish(new TrackPictureProcessingFailed(trackId)); + await context.Publish(new TrackPictureProcessingFailed(subjectId)); break; case RoutingSlipVariableNames.ProcessKinds.TrackAudio: - await context.Publish(new TrackAudioProcessingFailed(trackId)); + await context.Publish(new TrackAudioProcessingFailed(subjectId)); + break; + case RoutingSlipVariableNames.ProcessKinds.PlayListPicture: + await context.Publish(new PlayListPictureProcessingFailed(subjectId)); break; } } diff --git a/backend/Musify.Infrastructure/MassTransit/DependencyInjection/MassTransitDependencyInjection.Registration.cs b/backend/Musify.Infrastructure/MassTransit/DependencyInjection/MassTransitDependencyInjection.Registration.cs index 820aadc..ccfdb8b 100644 --- a/backend/Musify.Infrastructure/MassTransit/DependencyInjection/MassTransitDependencyInjection.Registration.cs +++ b/backend/Musify.Infrastructure/MassTransit/DependencyInjection/MassTransitDependencyInjection.Registration.cs @@ -36,8 +36,9 @@ namespace Musify.Infrastructure.MassTransit options.AddConsumer(); options.AddConsumer(); options.AddConsumer(); - options.AddConsumer(); + options.AddConsumer(); options.AddConsumer(); + options.AddConsumer(); options.AddExecuteActivity(); options.AddExecuteActivity(); @@ -49,6 +50,7 @@ namespace Musify.Infrastructure.MassTransit options.AddExecuteActivity(); options.AddExecuteActivity(); options.AddExecuteActivity(); + options.AddExecuteActivity(); options.AddActivity(); options.AddActivity(); diff --git a/backend/Musify.Infrastructure/MassTransit/DependencyInjection/MassTransitDependencyInjection.cs b/backend/Musify.Infrastructure/MassTransit/DependencyInjection/MassTransitDependencyInjection.cs index d9eb52c..2af7530 100644 --- a/backend/Musify.Infrastructure/MassTransit/DependencyInjection/MassTransitDependencyInjection.cs +++ b/backend/Musify.Infrastructure/MassTransit/DependencyInjection/MassTransitDependencyInjection.cs @@ -75,6 +75,14 @@ namespace Musify.Infrastructure.MassTransit repository.UsePostgres(); }); + options.AddSagaStateMachine() + .EntityFrameworkRepository(repository => + { + repository.ConcurrencyMode = ConcurrencyMode.Pessimistic; + repository.ExistingDbContext(); + repository.UsePostgres(); + }); + options.AddConfigureEndpointsCallback((registrationContext, _, endpointConfigurator) => { UseStandardRetry(endpointConfigurator); diff --git a/backend/Musify.Infrastructure/MassTransit/RoutingSlip/ActivityNames.cs b/backend/Musify.Infrastructure/MassTransit/RoutingSlip/ActivityNames.cs index 4c1a912..902f107 100644 --- a/backend/Musify.Infrastructure/MassTransit/RoutingSlip/ActivityNames.cs +++ b/backend/Musify.Infrastructure/MassTransit/RoutingSlip/ActivityNames.cs @@ -6,6 +6,7 @@ namespace Musify.Infrastructure.MassTransit.RoutingSlip internal const string MarkTrackAsFailed = "MarkTrackAsFailed"; internal const string DeleteTrackFromDb = "DeleteTrackFromDb"; internal const string MarkPlayListAsRemoving = "MarkPlayListAsRemoving"; + internal const string MarkPlayListAsFailed = "MarkPlayListAsFailed"; internal const string DeletePlayListFromDb = "DeletePlayListFromDb"; internal const string GenerateAudioWorkflowPaths = "GenerateAudioWorkflowPaths"; diff --git a/backend/Musify.Infrastructure/MassTransit/RoutingSlip/Builders/AudioWorkflowRoutingSlipBuilder.cs b/backend/Musify.Infrastructure/MassTransit/RoutingSlip/Builders/AudioWorkflowRoutingSlipBuilder.cs index 9016fab..41867e9 100644 --- a/backend/Musify.Infrastructure/MassTransit/RoutingSlip/Builders/AudioWorkflowRoutingSlipBuilder.cs +++ b/backend/Musify.Infrastructure/MassTransit/RoutingSlip/Builders/AudioWorkflowRoutingSlipBuilder.cs @@ -60,10 +60,10 @@ namespace Musify.Infrastructure.MassTransit.RoutingSlip.Builders message.TrackId, message.DestinationFolderKey)); - routingSlipBuilder.AddVariable(RoutingSlipVariableNames.Workflow.TrackId, message.TrackId); + routingSlipBuilder.AddVariable(RoutingSlipVariableNames.Workflow.SubjectId, message.TrackId); routingSlipBuilder.AddVariable(RoutingSlipVariableNames.Workflow.ProcessKind, RoutingSlipVariableNames.ProcessKinds.TrackAudio); routingSlipBuilder.AddSubscription( - EndpointHelper.BuildConsumerUri(TrackProcessingSlipFaultConsumer.QueueName), + EndpointHelper.BuildConsumerUri(ProcessingSlipFaultConsumer.QueueName), RoutingSlipEvents.Faulted); return routingSlipBuilder; diff --git a/backend/Musify.Infrastructure/MassTransit/RoutingSlip/Builders/PictureWorkflowRoutingSlipBuilder.cs b/backend/Musify.Infrastructure/MassTransit/RoutingSlip/Builders/PictureWorkflowRoutingSlipBuilder.cs index 731d160..b411ac1 100644 --- a/backend/Musify.Infrastructure/MassTransit/RoutingSlip/Builders/PictureWorkflowRoutingSlipBuilder.cs +++ b/backend/Musify.Infrastructure/MassTransit/RoutingSlip/Builders/PictureWorkflowRoutingSlipBuilder.cs @@ -32,10 +32,10 @@ namespace Musify.Infrastructure.MassTransit.RoutingSlip.Builders RoutingSlipVariableNames.Picture.MediumResizedFilePath, RoutingSlipVariableNames.Picture.LargeResizedFilePath)); - routingSlipBuilder.AddVariable(RoutingSlipVariableNames.Workflow.TrackId, message.TrackId); + routingSlipBuilder.AddVariable(RoutingSlipVariableNames.Workflow.SubjectId, message.TrackId); routingSlipBuilder.AddVariable(RoutingSlipVariableNames.Workflow.ProcessKind, RoutingSlipVariableNames.ProcessKinds.TrackPicture); routingSlipBuilder.AddSubscription( - EndpointHelper.BuildConsumerUri(TrackProcessingSlipFaultConsumer.QueueName), + EndpointHelper.BuildConsumerUri(ProcessingSlipFaultConsumer.QueueName), RoutingSlipEvents.Faulted); return routingSlipBuilder; @@ -61,6 +61,12 @@ namespace Musify.Infrastructure.MassTransit.RoutingSlip.Builders RoutingSlipVariableNames.Picture.MediumResizedFilePath, RoutingSlipVariableNames.Picture.LargeResizedFilePath)); + routingSlipBuilder.AddVariable(RoutingSlipVariableNames.Workflow.SubjectId, message.PlayListId); + routingSlipBuilder.AddVariable(RoutingSlipVariableNames.Workflow.ProcessKind, RoutingSlipVariableNames.ProcessKinds.PlayListPicture); + routingSlipBuilder.AddSubscription( + EndpointHelper.BuildConsumerUri(ProcessingSlipFaultConsumer.QueueName), + RoutingSlipEvents.Faulted); + return routingSlipBuilder; } diff --git a/backend/Musify.Infrastructure/MassTransit/RoutingSlip/RoutingSlipVariableNames.cs b/backend/Musify.Infrastructure/MassTransit/RoutingSlip/RoutingSlipVariableNames.cs index de4fdb6..6549d03 100644 --- a/backend/Musify.Infrastructure/MassTransit/RoutingSlip/RoutingSlipVariableNames.cs +++ b/backend/Musify.Infrastructure/MassTransit/RoutingSlip/RoutingSlipVariableNames.cs @@ -6,7 +6,7 @@ namespace Musify.Infrastructure.MassTransit { internal const string TemporalDirectory = "Workflow.TemporalDirectory"; internal const string CorrelationId = "Workflow.CorrelationId"; - internal const string TrackId = "Workflow.TrackId"; + internal const string SubjectId = "Workflow.SubjectId"; internal const string ProcessKind = "Workflow.ProcessKind"; } @@ -14,6 +14,7 @@ namespace Musify.Infrastructure.MassTransit { internal const string TrackPicture = "TrackPicture"; internal const string TrackAudio = "TrackAudio"; + internal const string PlayListPicture = "PlayListPicture"; } internal static class Audio diff --git a/backend/Musify.Infrastructure/MassTransit/Sagas/PlayListProcessingState.cs b/backend/Musify.Infrastructure/MassTransit/Sagas/PlayListProcessingState.cs new file mode 100644 index 0000000..213112b --- /dev/null +++ b/backend/Musify.Infrastructure/MassTransit/Sagas/PlayListProcessingState.cs @@ -0,0 +1,19 @@ +using MassTransit; + +namespace Musify.Infrastructure.MassTransit.Sagas +{ + public class PlayListProcessingState : SagaStateMachineInstance + { + public Guid CorrelationId { get; set; } + + public string CurrentState { get; set; } = null!; + + public string? Bucket { get; set; } + + public string? PictureKey { get; set; } + + public DateTime CreatedAt { get; set; } + + public DateTime UpdatedAt { get; set; } + } +} diff --git a/backend/Musify.Infrastructure/MassTransit/Sagas/PlayListProcessingStateMachine.cs b/backend/Musify.Infrastructure/MassTransit/Sagas/PlayListProcessingStateMachine.cs new file mode 100644 index 0000000..c6bd0dd --- /dev/null +++ b/backend/Musify.Infrastructure/MassTransit/Sagas/PlayListProcessingStateMachine.cs @@ -0,0 +1,51 @@ +using MassTransit; +using Musify.Application.Events; + +namespace Musify.Infrastructure.MassTransit.Sagas +{ + public sealed class PlayListProcessingStateMachine : MassTransitStateMachine + { + public State Processing { get; private set; } = null!; + public State Failed { get; private set; } = null!; + + public Event ProcessingStarted { get; private set; } = null!; + public Event PictureProcessed { get; private set; } = null!; + public Event PictureFailed { get; private set; } = null!; + + public PlayListProcessingStateMachine() + { + InstanceState(state => state.CurrentState); + + Event(() => ProcessingStarted, config => config.CorrelateById(context => context.Message.PlayListId)); + Event(() => PictureProcessed, config => config.CorrelateById(context => context.Message.PlayListId)); + Event(() => PictureFailed, config => config.CorrelateById(context => context.Message.PlayListId)); + + Initially( + When(ProcessingStarted) + .Then(context => + { + context.Saga.CreatedAt = DateTime.UtcNow; + context.Saga.UpdatedAt = DateTime.UtcNow; + context.Saga.Bucket = context.Message.Bucket; + context.Saga.PictureKey = context.Message.PictureDestinationKey; + }) + .TransitionTo(Processing)); + + During(Processing, + When(PictureProcessed).Finalize(), + When(PictureFailed) + .Then(context => context.Saga.UpdatedAt = DateTime.UtcNow) + .TransitionTo(Failed) + .Publish(context => new PlayListProcessingFailed( + context.Saga.CorrelationId, + context.Saga.Bucket, + context.Saga.PictureKey))); + + During(Failed, + Ignore(PictureProcessed), + Ignore(PictureFailed)); + + SetCompletedWhenFinalized(); + } + } +} diff --git a/backend/Musify.Infrastructure/Persistence/Database.cs b/backend/Musify.Infrastructure/Persistence/Database.cs index b4aa0e9..12947e0 100644 --- a/backend/Musify.Infrastructure/Persistence/Database.cs +++ b/backend/Musify.Infrastructure/Persistence/Database.cs @@ -30,6 +30,11 @@ namespace Musify.Infrastructure.Persistence trackProcessing.HasKey(state => state.CorrelationId); trackProcessing.Property(state => state.CorrelationId).ValueGeneratedNever(); trackProcessing.Property(state => state.CurrentState).HasMaxLength(64); + + var playListProcessing = modelBuilder.Entity(); + playListProcessing.HasKey(state => state.CorrelationId); + playListProcessing.Property(state => state.CorrelationId).ValueGeneratedNever(); + playListProcessing.Property(state => state.CurrentState).HasMaxLength(64); } public DbSet Users => Set(); diff --git a/backend/Musify.Infrastructure/Persistence/Migrations/20260705125356_AddPlayListProcessingStateSaga.Designer.cs b/backend/Musify.Infrastructure/Persistence/Migrations/20260705125356_AddPlayListProcessingStateSaga.Designer.cs new file mode 100644 index 0000000..1a2ac56 --- /dev/null +++ b/backend/Musify.Infrastructure/Persistence/Migrations/20260705125356_AddPlayListProcessingStateSaga.Designer.cs @@ -0,0 +1,646 @@ +// +using System; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using Musify.Infrastructure.Persistence; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; + +#nullable disable + +namespace Musify.Infrastructure.Persistence.Migrations +{ + [DbContext(typeof(Database))] + [Migration("20260705125356_AddPlayListProcessingStateSaga")] + partial class AddPlayListProcessingStateSaga + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "10.0.8") + .HasAnnotation("Relational:MaxIdentifierLength", 63); + + NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); + + modelBuilder.Entity("MassTransit.EntityFrameworkCoreIntegration.InboxState", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("Consumed") + .HasColumnType("timestamp with time zone"); + + b.Property("ConsumerId") + .HasColumnType("uuid"); + + b.Property("Delivered") + .HasColumnType("timestamp with time zone"); + + b.Property("ExpirationTime") + .HasColumnType("timestamp with time zone"); + + b.Property("LastSequenceNumber") + .HasColumnType("bigint"); + + b.Property("LockId") + .HasColumnType("uuid"); + + b.Property("MessageId") + .HasColumnType("uuid"); + + b.Property("ReceiveCount") + .HasColumnType("integer"); + + b.Property("Received") + .HasColumnType("timestamp with time zone"); + + b.Property("RowVersion") + .IsConcurrencyToken() + .ValueGeneratedOnAddOrUpdate() + .HasColumnType("bytea"); + + b.HasKey("Id"); + + b.HasIndex("Delivered"); + + b.ToTable("InboxState"); + }); + + modelBuilder.Entity("MassTransit.EntityFrameworkCoreIntegration.OutboxMessage", b => + { + b.Property("SequenceNumber") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("SequenceNumber")); + + b.Property("Body") + .IsRequired() + .HasColumnType("text"); + + b.Property("ContentType") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("ConversationId") + .HasColumnType("uuid"); + + b.Property("CorrelationId") + .HasColumnType("uuid"); + + b.Property("DestinationAddress") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("EnqueueTime") + .HasColumnType("timestamp with time zone"); + + b.Property("ExpirationTime") + .HasColumnType("timestamp with time zone"); + + b.Property("FaultAddress") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("Headers") + .HasColumnType("text"); + + b.Property("InboxConsumerId") + .HasColumnType("uuid"); + + b.Property("InboxMessageId") + .HasColumnType("uuid"); + + b.Property("InitiatorId") + .HasColumnType("uuid"); + + b.Property("MessageId") + .HasColumnType("uuid"); + + b.Property("MessageType") + .IsRequired() + .HasColumnType("text"); + + b.Property("OutboxId") + .HasColumnType("uuid"); + + b.Property("Properties") + .HasColumnType("text"); + + b.Property("RequestId") + .HasColumnType("uuid"); + + b.Property("ResponseAddress") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("SentTime") + .HasColumnType("timestamp with time zone"); + + b.Property("SourceAddress") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.HasKey("SequenceNumber"); + + b.HasIndex("EnqueueTime"); + + b.HasIndex("ExpirationTime"); + + b.HasIndex("OutboxId", "SequenceNumber") + .IsUnique(); + + b.HasIndex("InboxMessageId", "InboxConsumerId", "SequenceNumber") + .IsUnique(); + + b.ToTable("OutboxMessage"); + }); + + modelBuilder.Entity("MassTransit.EntityFrameworkCoreIntegration.OutboxState", b => + { + b.Property("OutboxId") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Created") + .HasColumnType("timestamp with time zone"); + + b.Property("Delivered") + .HasColumnType("timestamp with time zone"); + + b.Property("LastSequenceNumber") + .HasColumnType("bigint"); + + b.Property("LockId") + .HasColumnType("uuid"); + + b.Property("RowVersion") + .IsConcurrencyToken() + .ValueGeneratedOnAddOrUpdate() + .HasColumnType("bytea"); + + b.HasKey("OutboxId"); + + b.HasIndex("Created"); + + b.ToTable("OutboxState"); + }); + + modelBuilder.Entity("Musify.Domain.Entities.PlayList", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Description") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("LargePictureName") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b.Property("LifeCycleStatus") + .HasColumnType("integer"); + + b.Property("MediumPictureName") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("NormalizedName") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("OriginalPictureName") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b.Property("SmallPictureName") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("UserId") + .HasColumnType("bigint"); + + b.HasKey("Id"); + + b.HasIndex("UserId"); + + b.ToTable("PlayLists"); + }); + + modelBuilder.Entity("Musify.Domain.Entities.PlayListHasTrack", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("AddedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("PlayListId") + .HasColumnType("uuid"); + + b.Property("Position") + .HasColumnType("integer"); + + b.Property("TrackId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("PlayListId"); + + b.HasIndex("TrackId"); + + b.ToTable("PlayListHasTrack"); + }); + + modelBuilder.Entity("Musify.Domain.Entities.Track", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("AudioFolderName") + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b.Property("AudioTranscodeProcessingStatus") + .HasColumnType("integer"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("LargePictureName") + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b.Property("LastRetryAt") + .HasColumnType("timestamp with time zone"); + + b.Property("LifeCycleStatus") + .HasColumnType("integer"); + + b.Property("MediumPictureName") + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b.Property("NormalizedTitle") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("OriginalAudioName") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b.Property("OriginalPictureName") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b.Property("PicturesProcessingStatus") + .HasColumnType("integer"); + + b.Property("RetryCount") + .HasColumnType("integer"); + + b.Property("SmallPictureName") + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b.Property("Title") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.ToTable("Tracks"); + }); + + modelBuilder.Entity("Musify.Domain.Entities.Upload", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Bucket") + .IsRequired() + .HasColumnType("text"); + + b.Property("ContentType") + .IsRequired() + .HasColumnType("text"); + + b.Property("CreatedTime") + .HasColumnType("timestamp with time zone"); + + b.Property("Key") + .IsRequired() + .HasColumnType("text"); + + b.Property("State") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.ToTable("Upload"); + }); + + modelBuilder.Entity("Musify.Domain.Entities.UploadIntent", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Bucket") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("character varying(128)"); + + b.Property("ContentType") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("character varying(128)"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("ExpectedSizeBytes") + .HasColumnType("bigint"); + + b.Property("ExpiresAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Key") + .IsRequired() + .HasMaxLength(512) + .HasColumnType("character varying(512)"); + + b.Property("ObjectName") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("character varying(128)"); + + b.Property("Purpose") + .HasColumnType("integer"); + + b.Property("Status") + .HasColumnType("integer"); + + b.Property("UserId") + .HasColumnType("bigint"); + + b.HasKey("Id"); + + b.HasIndex("UserId"); + + b.ToTable("UploadIntents"); + }); + + modelBuilder.Entity("Musify.Domain.Entities.User", b => + { + b.Property("Id") + .HasColumnType("bigint"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("FirstName") + .HasMaxLength(48) + .HasColumnType("character varying(48)"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(48) + .HasColumnType("character varying(48)"); + + b.Property("NormalizedName") + .IsRequired() + .HasMaxLength(48) + .HasColumnType("character varying(48)"); + + b.Property("ProfilePictureUrl") + .HasMaxLength(2048) + .HasColumnType("character varying(2048)"); + + b.Property("SecondName") + .HasMaxLength(48) + .HasColumnType("character varying(48)"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.ToTable("User"); + }); + + modelBuilder.Entity("Musify.Domain.Entities.UserHasTrack", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("TrackId") + .HasColumnType("uuid"); + + b.Property("UserId") + .HasColumnType("bigint"); + + b.HasKey("Id"); + + b.HasIndex("TrackId"); + + b.HasIndex("UserId"); + + b.ToTable("UserHasTrack"); + }); + + modelBuilder.Entity("Musify.Infrastructure.MassTransit.Sagas.PlayListProcessingState", b => + { + b.Property("CorrelationId") + .HasColumnType("uuid"); + + b.Property("Bucket") + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("CurrentState") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b.Property("PictureKey") + .HasColumnType("text"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.HasKey("CorrelationId"); + + b.ToTable("PlayListProcessingState"); + }); + + modelBuilder.Entity("Musify.Infrastructure.MassTransit.Sagas.TrackProcessingState", b => + { + b.Property("CorrelationId") + .HasColumnType("uuid"); + + b.Property("AudioKey") + .HasColumnType("text"); + + b.Property("AudioProcessed") + .HasColumnType("boolean"); + + b.Property("Bucket") + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("CurrentState") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b.Property("PictureKey") + .HasColumnType("text"); + + b.Property("PictureProcessed") + .HasColumnType("boolean"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.HasKey("CorrelationId"); + + b.ToTable("TrackProcessingState"); + }); + + modelBuilder.Entity("MassTransit.EntityFrameworkCoreIntegration.OutboxMessage", b => + { + b.HasOne("MassTransit.EntityFrameworkCoreIntegration.OutboxState", null) + .WithMany() + .HasForeignKey("OutboxId"); + + b.HasOne("MassTransit.EntityFrameworkCoreIntegration.InboxState", null) + .WithMany() + .HasForeignKey("InboxMessageId", "InboxConsumerId") + .HasPrincipalKey("MessageId", "ConsumerId"); + }); + + modelBuilder.Entity("Musify.Domain.Entities.PlayList", b => + { + b.HasOne("Musify.Domain.Entities.User", "User") + .WithMany("PlayLists") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Musify.Domain.Entities.PlayListHasTrack", b => + { + b.HasOne("Musify.Domain.Entities.PlayList", "PlayList") + .WithMany("PlayListTracks") + .HasForeignKey("PlayListId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Musify.Domain.Entities.Track", "Track") + .WithMany("PlayListTracks") + .HasForeignKey("TrackId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("PlayList"); + + b.Navigation("Track"); + }); + + modelBuilder.Entity("Musify.Domain.Entities.UploadIntent", b => + { + b.HasOne("Musify.Domain.Entities.User", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Musify.Domain.Entities.UserHasTrack", b => + { + b.HasOne("Musify.Domain.Entities.Track", "Track") + .WithMany("UserTracks") + .HasForeignKey("TrackId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Musify.Domain.Entities.User", "User") + .WithMany("UserTracks") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Track"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Musify.Domain.Entities.PlayList", b => + { + b.Navigation("PlayListTracks"); + }); + + modelBuilder.Entity("Musify.Domain.Entities.Track", b => + { + b.Navigation("PlayListTracks"); + + b.Navigation("UserTracks"); + }); + + modelBuilder.Entity("Musify.Domain.Entities.User", b => + { + b.Navigation("PlayLists"); + + b.Navigation("UserTracks"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/backend/Musify.Infrastructure/Persistence/Migrations/20260705125356_AddPlayListProcessingStateSaga.cs b/backend/Musify.Infrastructure/Persistence/Migrations/20260705125356_AddPlayListProcessingStateSaga.cs new file mode 100644 index 0000000..bb964d8 --- /dev/null +++ b/backend/Musify.Infrastructure/Persistence/Migrations/20260705125356_AddPlayListProcessingStateSaga.cs @@ -0,0 +1,38 @@ +using System; +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace Musify.Infrastructure.Persistence.Migrations +{ + /// + public partial class AddPlayListProcessingStateSaga : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.CreateTable( + name: "PlayListProcessingState", + columns: table => new + { + CorrelationId = table.Column(type: "uuid", nullable: false), + CurrentState = table.Column(type: "character varying(64)", maxLength: 64, nullable: false), + Bucket = table.Column(type: "text", nullable: true), + PictureKey = table.Column(type: "text", nullable: true), + CreatedAt = table.Column(type: "timestamp with time zone", nullable: false), + UpdatedAt = table.Column(type: "timestamp with time zone", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_PlayListProcessingState", x => x.CorrelationId); + }); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropTable( + name: "PlayListProcessingState"); + } + } +} diff --git a/backend/Musify.Infrastructure/Persistence/Migrations/DatabaseModelSnapshot.cs b/backend/Musify.Infrastructure/Persistence/Migrations/DatabaseModelSnapshot.cs index 71a9d9f..2bddd69 100644 --- a/backend/Musify.Infrastructure/Persistence/Migrations/DatabaseModelSnapshot.cs +++ b/backend/Musify.Infrastructure/Persistence/Migrations/DatabaseModelSnapshot.cs @@ -484,6 +484,33 @@ namespace Musify.Infrastructure.Persistence.Migrations b.ToTable("UserHasTrack"); }); + modelBuilder.Entity("Musify.Infrastructure.MassTransit.Sagas.PlayListProcessingState", b => + { + b.Property("CorrelationId") + .HasColumnType("uuid"); + + b.Property("Bucket") + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("CurrentState") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b.Property("PictureKey") + .HasColumnType("text"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.HasKey("CorrelationId"); + + b.ToTable("PlayListProcessingState"); + }); + modelBuilder.Entity("Musify.Infrastructure.MassTransit.Sagas.TrackProcessingState", b => { b.Property("CorrelationId")