Update delete workflows
This commit is contained in:
+32
-3
@@ -97,8 +97,7 @@ Archivos clave:
|
||||
- Se añadieron overloads de rutas para construir keys con `userId`.
|
||||
- Los handlers de request presigned URL ahora generan keys usando el prefijo con `userId`.
|
||||
- Los eventos de workflows (sourceKey) se publican con la key prefijada.
|
||||
- El borrado de playlist borra la key original prefijada.
|
||||
- El borrado de track (`DeleteTrackActivity`) borra originales usando el `userId` presente en `DeleteTrackArguments`.
|
||||
- El borrado de playlist y track usan keys prefijadas dentro de sus workflows de delete.
|
||||
|
||||
Archivos:
|
||||
- `Application/Configuration/PlayListConfiguration.cs`
|
||||
@@ -109,7 +108,7 @@ Archivos:
|
||||
- `Application/PlayLists/Handlers/UpdatePlayListCommandHandler.cs`
|
||||
- `Application/Tracks/Handler/CreateTrackCommandHandler.cs`
|
||||
- `Application/PlayLists/Handlers/DeletePlayListCommandHandler.cs`
|
||||
- `Infrastructure/MassTransit/Activities/Tracks/DeleteTrackActivity.cs`
|
||||
|
||||
|
||||
---
|
||||
|
||||
@@ -163,3 +162,33 @@ Archivo:
|
||||
## 10) Estado final
|
||||
|
||||
- `dotnet build MusifyBackend.slnx -c Release` ✅
|
||||
|
||||
---
|
||||
|
||||
## 11) Deletes unificados con Workflows (RoutingSlip)
|
||||
|
||||
**Objetivo**: homogeneizar deletes de Track/PlayList usando Courier RoutingSlip con actividades granulares e idempotentes.
|
||||
|
||||
### Cambios
|
||||
- Se añadió `LifeCycleStatus` (`Active`/`Removing`) a `Track` y `PlayList` y se generó migración.
|
||||
- Track delete:
|
||||
- Se eliminó la activity monolítica y el slip ahora encadena: marcar como removing → borrar N ficheros (condicional) → borrar en DB.
|
||||
- PlayList delete:
|
||||
- Se añadió `DeletePlayListEvent` + consumer + routing slip builder + activities para marcar como removing, borrar pictures (si no son preset) y borrar en DB.
|
||||
- Se endureció la idempotencia de borrado en S3: si una key no existe, se loguea y el paso se considera completado.
|
||||
- Se eliminaron `RemoveFileEvent`/`RemoveFileConsumer` por quedar sin usos.
|
||||
|
||||
Archivos clave:
|
||||
- `Domain/Entities/LifeCycleStatus.cs`
|
||||
- `Domain/Entities/Track.cs`
|
||||
- `Domain/Entities/PlayList.cs`
|
||||
- `Application/Events/DeletePlayListEvent.cs`
|
||||
- `Application/PlayLists/Handlers/DeletePlayListCommandHandler.cs`
|
||||
- `Application/Tracks/Handler/DeleteTrackCommandHandler.cs`
|
||||
- `Infrastructure/MassTransit/Consumers/DeleteTrackConsumer.cs`
|
||||
- `Infrastructure/MassTransit/Consumers/DeletePlayListConsumer.cs`
|
||||
- `Infrastructure/MassTransit/RoutingSlip/Builders/DeleteTrackRoutingSlipBuilder.cs`
|
||||
- `Infrastructure/MassTransit/RoutingSlip/Builders/DeletePlayListRoutingSlipBuilder.cs`
|
||||
- `Infrastructure/MassTransit/Activities/Files/RemoveFileFromBucketActivity.cs`
|
||||
- `Infrastructure/MassTransit/DependencyInjection/MassTransitDependencyInjection.Registration.cs`
|
||||
- `Infrastructure/Persistence/Migrations/20260514161518_AddLifeCycleStatus.cs`
|
||||
|
||||
@@ -4,9 +4,9 @@
|
||||
|
||||
Actualmente hay dos problemas de diseño en los deletes:
|
||||
|
||||
**PlayLists:** el borrado dispara múltiples `RemoveFileEvent` sueltos desde `DeletePlayListCommandHandler` (4 llamadas separadas para original/small/medium/large) y luego elimina la entidad en DB. Sin workflow, sin trazabilidad.
|
||||
**PlayLists:** el borrado disparaba múltiples `RemoveFileEvent` sueltos desde `DeletePlayListCommandHandler` (4 llamadas separadas para original/small/medium/large) y luego eliminaba la entidad en DB. Sin workflow, sin trazabilidad.
|
||||
|
||||
**Tracks:** ya existe un workflow (`DeleteTrackEvent` → consumer → RoutingSlip → `DeleteTrackActivity`), pero `DeleteTrackActivity` hace **demasiado en un único paso**: carga la entidad, borra todos los archivos de S3 (pictures + audios), y elimina de DB. Esto rompe el principio de actividades granulares y reutilizables, hace que el slip no sea "seguible" paso a paso, y dificulta los reintentos parciales.
|
||||
**Tracks:** existía un workflow (`DeleteTrackEvent` → consumer → RoutingSlip → `DeleteTrackActivity`), pero `DeleteTrackActivity` hacía **demasiado en un único paso**: carga la entidad, borra todos los archivos de S3 (pictures + audios), y elimina de DB. Esto rompe el principio de actividades granulares y reutilizables, hace que el slip no sea "seguible" paso a paso, y dificulta los reintentos parciales.
|
||||
|
||||
La idea es **homogeneizar y corregir ambos**: que los deletes de Track y PlayList sigan un RoutingSlip con actividades pequeñas, ordenadas, repetibles e independientes.
|
||||
|
||||
@@ -42,30 +42,54 @@ No objetivos (por ahora):
|
||||
|
||||
## Diseño propuesto
|
||||
|
||||
## Implementación final (nombres reales)
|
||||
|
||||
Este plan se implementó con los siguientes nombres finales (para facilitar traceo/debug):
|
||||
|
||||
### Colas (consumers)
|
||||
- `delete-track` (`DeleteTrackConsumer`)
|
||||
- `delete-playList` (`DeletePlayListConsumer`)
|
||||
|
||||
### Execute endpoints (activities)
|
||||
MassTransit crea las colas de execute como `<endpointName>_execute`.
|
||||
|
||||
- `remove-file-from-bucket` → cola `remove-file-from-bucket_execute`
|
||||
- `mark-track-as-removing` → cola `mark-track-as-removing_execute`
|
||||
- `delete-track-from-db` → cola `delete-track-from-db_execute`
|
||||
- `mark-playList-as-removing` → cola `mark-playList-as-removing_execute`
|
||||
- `delete-playList-from-db` → cola `delete-playList-from-db_execute`
|
||||
|
||||
### Estado de borrado (DB)
|
||||
- Se introdujo `LifeCycleStatus` en `Track` y `PlayList` con valores `Active` y `Removing`.
|
||||
- El handler marca `Removing` antes de publicar el evento (y persiste con `SaveChanges`).
|
||||
|
||||
> Nota: se eliminó el enfoque de “estado Removing” implícito en `ProcessingStatus` para deletes. El tracking de delete queda en `LifeCycleStatus`.
|
||||
|
||||
### Actividades compartidas / reutilizables
|
||||
|
||||
Ya existe (y se mantiene):
|
||||
- `RemoveFileFromBucketActivity` — borra un único objeto de S3. Se usará N veces en cada slip.
|
||||
|
||||
Se añaden actividades de DB compartibles por aggregate:
|
||||
- `MarkAsRemovingActivity<TArgs>` — cambia estado de la entidad a `Removing` y persiste. (O una por aggregate si la lógica difiere.)
|
||||
Se añaden actividades de DB por aggregate:
|
||||
- `MarkTrackAsRemovingActivity` — cambia estado `LifeCycleStatus` a `Removing` y persiste.
|
||||
- `DeleteTrackFromDbActivity` — elimina el Track de EF (cascade elimina relaciones).
|
||||
- `MarkPlayListAsRemovingActivity` — cambia estado `LifeCycleStatus` a `Removing` y persiste.
|
||||
- `DeletePlayListFromDbActivity` — elimina la PlayList de EF (cascade elimina relaciones).
|
||||
|
||||
### Estructura del RoutingSlip para Track
|
||||
|
||||
```
|
||||
DeleteTrackEvent
|
||||
└─► DeleteTrackConsumer
|
||||
└─► DeleteTrackRoutingSlipBuilder
|
||||
├─ MarkTrackAsRemovingActivity (cambia estado → Removing, persiste)
|
||||
├─ RemoveFileFromBucketActivity (original picture)
|
||||
├─ RemoveFileFromBucketActivity (small picture) ← solo si IsPicturesProcessed
|
||||
├─ RemoveFileFromBucketActivity (medium picture) ← solo si IsPicturesProcessed
|
||||
├─ RemoveFileFromBucketActivity (large picture) ← solo si IsPicturesProcessed
|
||||
├─ RemoveFileFromBucketActivity (original audio)
|
||||
├─ RemoveFileFromBucketActivity (processed audio) ← solo si IsAudioProcessed
|
||||
└─ DeleteTrackFromDbActivity (Remove() + SaveChanges)
|
||||
└─► DeleteTrackConsumer (queue: `delete-track`)
|
||||
└─► DeleteTrackRoutingSlipBuilder.BuildAsync(...)
|
||||
├─ MarkTrackAsRemovingActivity (endpoint: `mark-track-as-removing`)
|
||||
├─ RemoveFileFromBucketActivity (endpoint: `remove-file-from-bucket`) — original picture
|
||||
├─ RemoveFileFromBucketActivity (endpoint: `remove-file-from-bucket`) — small (si IsPicturesProcessed)
|
||||
├─ RemoveFileFromBucketActivity (endpoint: `remove-file-from-bucket`) — medium (si IsPicturesProcessed)
|
||||
├─ RemoveFileFromBucketActivity (endpoint: `remove-file-from-bucket`) — large (si IsPicturesProcessed)
|
||||
├─ RemoveFileFromBucketActivity (endpoint: `remove-file-from-bucket`) — original audio
|
||||
├─ RemoveFileFromBucketActivity (endpoint: `remove-file-from-bucket`) — processed audio (si IsAudioProcessed)
|
||||
└─ DeleteTrackFromDbActivity (endpoint: `delete-track-from-db`)
|
||||
```
|
||||
|
||||
> El builder carga la entidad para construir el plan (keys condicionales) antes de arrancar el slip. Las actividades de S3 son idempotentes (NotFound → log Information + continue).
|
||||
@@ -74,14 +98,14 @@ DeleteTrackEvent
|
||||
|
||||
```
|
||||
DeletePlayListEvent
|
||||
└─► DeletePlayListConsumer
|
||||
└─► DeletePlayListRoutingSlipBuilder
|
||||
├─ MarkPlayListAsRemovingActivity (cambia estado → Removing, persiste)
|
||||
├─ RemoveFileFromBucketActivity (original — uploads/{userId}/...) ← solo si no es preset
|
||||
├─ RemoveFileFromBucketActivity (small) ← solo si no es preset
|
||||
├─ RemoveFileFromBucketActivity (medium) ← solo si no es preset
|
||||
├─ RemoveFileFromBucketActivity (large) ← solo si no es preset
|
||||
└─ DeletePlayListFromDbActivity (Remove() + SaveChanges, cascade elimina PlayListHasTrack)
|
||||
└─► DeletePlayListConsumer (queue: `delete-playList`)
|
||||
└─► DeletePlayListRoutingSlipBuilder.BuildAsync(...)
|
||||
├─ MarkPlayListAsRemovingActivity (endpoint: `mark-playList-as-removing`)
|
||||
├─ RemoveFileFromBucketActivity (endpoint: `remove-file-from-bucket`) — original (uploads/{userId}/...) ← solo si no es preset
|
||||
├─ RemoveFileFromBucketActivity (endpoint: `remove-file-from-bucket`) — small ← solo si no es preset
|
||||
├─ RemoveFileFromBucketActivity (endpoint: `remove-file-from-bucket`) — medium ← solo si no es preset
|
||||
├─ RemoveFileFromBucketActivity (endpoint: `remove-file-from-bucket`) — large ← solo si no es preset
|
||||
└─ DeletePlayListFromDbActivity (endpoint: `delete-playList-from-db`)
|
||||
```
|
||||
|
||||
### Handler de comando (ambos aggregates)
|
||||
@@ -93,14 +117,20 @@ database.PlayLists.Remove(playlist);
|
||||
await database.SaveChangesAsync();
|
||||
|
||||
// DESPUÉS
|
||||
playlist.LifeCycleStatus = LifeCycleStatus.Removing;
|
||||
await eventBus.PublishAsync(new DeletePlayListEvent(command.PlayListId, command.UserId));
|
||||
// Nada más. El workflow se encarga de todo.
|
||||
await database.SaveChangesAsync();
|
||||
// El workflow se encarga de borrar ficheros y eliminar en DB.
|
||||
```
|
||||
|
||||
```csharp
|
||||
// DeleteTrackCommandHandler — ANTES
|
||||
// (ya publicaba DeleteTrackEvent, se mantiene igual)
|
||||
|
||||
// DESPUÉS
|
||||
track.LifeCycleStatus = LifeCycleStatus.Removing;
|
||||
await eventBus.PublishAsync(new DeleteTrackEvent(command.TrackId, command.UserId));
|
||||
await database.SaveChangesAsync();
|
||||
|
||||
// La diferencia es que ahora el slip tiene pasos granulares, no una activity monolítica.
|
||||
```
|
||||
@@ -120,7 +150,7 @@ await eventBus.PublishAsync(new DeleteTrackEvent(command.TrackId, command.UserId
|
||||
## Idempotencia / reintentos
|
||||
|
||||
- RoutingSlip + reintentos de endpoint ya configurados en MassTransit.
|
||||
- Operaciones S3: idempotentes por diseño. Si el key no existe → log `Information` y `Completed()` (no throw).
|
||||
- Operaciones S3: idempotentes por diseño. Si el key no existe (`NoSuchKey`/`NotFound`) → log `Information` y `Completed()` (no throw).
|
||||
- DB: si la entidad ya no existe cuando llega `DeleteTrackFromDbActivity` / `DeletePlayListFromDbActivity`, el workflow se considera `Completed` (misma lógica que ahora).
|
||||
- Estado `Removing`: permite identificar entidades en proceso de borrado si el sistema se reinicia a mitad del slip.
|
||||
|
||||
|
||||
Reference in New Issue
Block a user