9.2 KiB
Plan — Unificar Deletes con Workflows (RoutingSlip)
Contexto
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.
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.
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.
Objetivos
- Un único camino de borrado por aggregate (Track / PlayList), basado en RoutingSlip con actividades granulares.
- El handler de comando hace lo mínimo: validar ownership/reglas, cambiar estado a
Removing, y publicar el evento de borrado (outbox). - El workflow ejecuta pasos ordenados, reutilizables y reiniciables individualmente.
- Reutilizar
RemoveFileFromBucketActivity(ya existente) para cada archivo a borrar, en lugar de lógica ad-hoc dentro de una activity monolítica. - Eliminar
RemoveFileEvent,RemoveFileConsumery su registration si no tienen otros usos.
No objetivos (por ahora):
- No introducir observabilidad extra (tracing/telemetría) fuera de lo ya existente.
Problemas del enfoque actual
PlayLists
- El handler implementa orquestación (decide qué archivos borrar y cómo) → acoplamiento alto.
- Publica múltiples
RemoveFileEventsueltos, sin agruparlos en un proceso único. - No hay log de RoutingSlip que permita seguir el progreso/estado.
Tracks ← nuevo problema identificado
DeleteTrackActivityes monolítica: carga entidad, borra pictures, borra audios, elimina de DB, todo en un soloExecute.- No reutiliza
RemoveFileFromBucketActivityya existente. - Si falla a mitad, no hay granularidad de reintento (no se sabe qué paso falló).
- El RoutingSlip tiene un único nodo → no es realmente un "slip" con pasos.
Diseño propuesto
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 aRemovingy persiste. (O una por aggregate si la lógica difiere.)DeleteTrackFromDbActivity— elimina el Track de EF (cascade elimina relaciones).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)
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).
Estructura del RoutingSlip para PlayList
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)
Handler de comando (ambos aggregates)
// DeletePlayListCommandHandler — ANTES
await PublishRemoveFileEventsAsync(...); // 4 eventos sueltos
database.PlayLists.Remove(playlist);
await database.SaveChangesAsync();
// DESPUÉS
await eventBus.PublishAsync(new DeletePlayListEvent(command.PlayListId, command.UserId));
// Nada más. El workflow se encarga de todo.
// DeleteTrackCommandHandler — ANTES
// (ya publicaba DeleteTrackEvent, se mantiene igual)
await eventBus.PublishAsync(new DeleteTrackEvent(command.TrackId, command.UserId));
// La diferencia es que ahora el slip tiene pasos granulares, no una activity monolítica.
Reglas de negocio a preservar
- Ownership se valida en el handler del comando (igual que ahora), antes de publicar el evento.
- PlayList: solo borrar original/sizes si no son presets.
- PlayList: el original está bajo
uploads/{userId}/...(cambio reciente). - Track: respetar estados de procesamiento — si audio está
Pending/Processing, el handler ya bloquea el delete antes de llegar al workflow. - EF cascade elimina
PlayListHasTrackal borrar la PlayList → no hace falta activity explícita para ello.
Idempotencia / reintentos
- RoutingSlip + reintentos de endpoint ya configurados en MassTransit.
- Operaciones S3: idempotentes por diseño. Si el key no existe → log
InformationyCompleted()(no throw). - DB: si la entidad ya no existe cuando llega
DeleteTrackFromDbActivity/DeletePlayListFromDbActivity, el workflow se consideraCompleted(misma lógica que ahora). - Estado
Removing: permite identificar entidades en proceso de borrado si el sistema se reinicia a mitad del slip.
Plan de implementación (paso a paso)
1. Inventario
- Confirmar todos los usos de
RemoveFileEvent/RemoveFileConsumeren la solución. - Si no tienen otros usos fuera de playlist delete → marcarlos para eliminación en el paso 7.
2. Refactorizar Track delete ← nuevo
- Crear
MarkTrackAsRemovingActivity(execute-activity): carga Track, setea estadoRemoving, persiste. - Crear
DeleteTrackFromDbActivity(execute-activity): carga Track, llamaRemove(), persiste. - Actualizar
DeleteTrackRoutingSlipBuilder:- Carga el Track para calcular el plan (keys condicionales según
IsPicturesProcessed,IsAudioProcessed). - Encadena:
MarkTrackAsRemoving→ N ×RemoveFileFromBucket→DeleteTrackFromDb.
- Carga el Track para calcular el plan (keys condicionales según
- Eliminar
DeleteTrackActivitymonolítica (ya no se usa). - Ajustar registration DI de MassTransit (quitar activity vieja, registrar las nuevas).
3. Crear PlayList delete: evento + consumer + builder
- Añadir
DeletePlayListEvent(PlayListId, UserId)enApplication/Events. - Crear
DeletePlayListConsumeren Infrastructure: recibe el evento y ejecuta el builder. - Crear
DeletePlayListRoutingSlipBuilder:- Carga la PlayList para calcular el plan (keys condicionales por presets, ruta de original).
- Encadena:
MarkPlayListAsRemoving→ N ×RemoveFileFromBucket→DeletePlayListFromDb.
4. Crear actividades de PlayList
MarkPlayListAsRemovingActivity(execute-activity): carga PlayList, seteaRemoving, persiste.DeletePlayListFromDbActivity(execute-activity): carga PlayList, llamaRemove(), persiste (cascade).
5. Actualizar RemoveFileFromBucketActivity para NotFound
- Si el SDK lanza excepción por key inexistente → capturar, log
Information("Key {Key} not found, skipping"), retornarCompleted(). - Si es otro error → log
Error+ throw (reintento normal).
6. Cambiar DeletePlayListCommandHandler
- Reemplazar
PublishRemoveFileEventsAsyncporeventBus.PublishAsync(new DeletePlayListEvent(...)). - No publicar más
RemoveFileEventpara playlist delete.
7. Ajustar DI / registration MassTransit
- Registrar:
DeletePlayListConsumer,MarkPlayListAsRemovingActivity,DeletePlayListFromDbActivity. - Registrar:
MarkTrackAsRemovingActivity,DeleteTrackFromDbActivity. - Desregistrar:
DeleteTrackActivity(monolítica), y si procedeRemoveFileConsumer.
8. Eliminar código muerto
DeleteTrackActivitymonolítica.RemoveFileEventyRemoveFileConsumer(si no tienen otros usos).- Endpoints y registrations asociados.
9. Build y verificación
dotnet build -c Releasesin warnings de referencias muertas.- Smoke test manual: borrar un Track y una PlayList, verificar logs de cada activity en orden.