# 🎯 PLAN D'IMPLÉMENTATION TRANSACTIONNELLE — PROJET VEZA **Date** : 2025-01-27 **Objectif** : Plan d'action complet pour rendre toutes les opĂ©rations critiques transactionnelles **Phase** : Design — PrĂȘt pour implĂ©mentation **RĂ©fĂ©rences** : `AUDIT_DB_TRANSACTIONS.md`, `AUDIT_STABILITY.md`, `TRIAGE.md` --- ## 📋 TABLE DES MATIÈRES 1. [RĂ©sumĂ© ExĂ©cutif](#1-rĂ©sumĂ©-exĂ©cutif) 2. [Inventaire des OpĂ©rations Critiques](#2-inventaire-des-opĂ©rations-critiques) 3. [Patterns Transactionnels RecommandĂ©s](#3-patterns-transactionnels-recommandĂ©s) 4. [Design DĂ©taillĂ© par Domaine (P0)](#4-design-dĂ©taillĂ©-par-domaine-p0) 5. [Plan d'ImplĂ©mentation par Phases](#5-plan-dimplĂ©mentation-par-phases) 6. [StratĂ©gie de Tests](#6-stratĂ©gie-de-tests) 7. [Checklist de Validation](#7-checklist-de-validation) --- ## 1. RÉSUMÉ EXÉCUTIF ### Pourquoi ce plan est critique Le projet Veza gĂšre des opĂ©rations multi-Ă©tapes critiques qui, en cas d'Ă©chec partiel, peuvent laisser la base de donnĂ©es dans un Ă©tat incohĂ©rent : - **Marketplace** : Commandes partiellement créées (items sans order, licenses sans order) - **Playlists** : Duplications incomplĂštes, collaborateurs sans playlist valide - **Social** : Compteurs de likes/comments dĂ©synchronisĂ©s - **Stream** : Segments HLS orphelins, jobs de transcodage incomplets - **RBAC** : Assignations de rĂŽles partiellement appliquĂ©es, compromettant la sĂ©curitĂ© **Impact mĂ©tier** : DonnĂ©es corrompues, confusion utilisateur, streaming cassĂ©, risques de sĂ©curitĂ©. ### Domaines concernĂ©s | Domaine | Service | Langage | OpĂ©rations P0 | OpĂ©rations P1 | |---------|---------|---------|---------------|---------------| | **Marketplace** | `MarketplaceService` | Go | 0 | 0 | | **Playlists** | `PlaylistService`, `PlaylistDuplicateService` | Go | 1 | 1 | | **Social** | `SocialService` | Go | 0 | 2 | | **RBAC** | `RBACService`, `RoleService` | Go | 1 | 1 | | **Stream** | `SegmentTracker`, `StreamProcessor`, `EncodingPool` | Rust | 2 | 1 | | **Chat** | `Channels`, `DirectMessages` | Rust | 0 | 0 | **Total** : **5 opĂ©rations P0**, **5 opĂ©rations P1** ### Objectif final **100% des opĂ©rations P0 transactionnelles** avant dĂ©ploiement en production. **État actuel** : 8/18 opĂ©rations transactionnelles (44%) **État cible** : 18/18 opĂ©rations transactionnelles (100%) --- ## 2. INVENTAIRE DES OPÉRATIONS CRITIQUES ### 2.1 OpĂ©rations P0 (Critique — Must-Fix) #### 1. `PlaylistDuplicateService.DuplicatePlaylist` - **Service** : Backend Go (`internal/services/playlist_duplicate_service.go:41-131`) - **Fichiers concernĂ©s** : - `internal/services/playlist_duplicate_service.go` - `internal/services/playlist_service.go` (CreatePlaylist) - `internal/repositories/playlist_track_repository.go` (AddTrack) - **Risque actuel** : - Playlist créée mais tracks non ajoutĂ©s → **Playlist vide** - Crash au milieu de l'ajout des tracks → **Playlist partiellement dupliquĂ©e** - Erreur sur un track → **Playlist incomplĂšte** (ligne 117 continue avec les autres) - **Statut** : ❌ **Non transactionnelle** - **Impact mĂ©tier** : **ÉLEVÉ** — Confusion utilisateur, donnĂ©es corrompues #### 2. `RBACService.AssignRoleToUser` - **Service** : Backend Go (`internal/services/rbac_service.go:168-210`) - **Fichiers concernĂ©s** : - `internal/services/rbac_service.go` - **Risque actuel** : - 4 queries sĂ©parĂ©es (vĂ©rifications + INSERT) → **Race condition possible** - User/role supprimĂ© entre vĂ©rification et INSERT → **Assignation incohĂ©rente** - Pas de gestion propre des doublons → **Erreurs DB non gĂ©rĂ©es** - **Statut** : ❌ **Non transactionnelle** - **Impact mĂ©tier** : **ÉLEVÉ** — SĂ©curitĂ© compromise, permissions incorrectes #### 3. `SegmentTracker.persist_segment` - **Service** : Stream Server Rust (`src/core/processing/segment_tracker.rs:82-106`) - **Fichiers concernĂ©s** : - `src/core/processing/segment_tracker.rs` - **Risque actuel** : - INSERT segment + UPDATE job sĂ©parĂ©s → **Segments orphelins** si crash aprĂšs INSERT - UPDATE job + INSERT segment sĂ©parĂ©s → **IncohĂ©rence durĂ©e** si crash aprĂšs UPDATE - **Statut** : ❌ **Non transactionnelle** - **Impact mĂ©tier** : **ÉLEVÉ** — Streaming cassĂ©, playlists HLS incomplĂštes #### 4. `StreamProcessor` (Job Creation + Segment Persistence) - **Service** : Stream Server Rust (`src/core/processing/processor.rs`) - **Fichiers concernĂ©s** : - `src/core/processing/processor.rs` - `src/core/processing/segment_tracker.rs` - **Risque actuel** : - Job créé → Segments persistĂ©s individuellement → Job finalisĂ© - Crash aprĂšs crĂ©ation job → **Job orphelin sans segments** - Crash pendant persistance → **Segments partiellement créés** - Crash aprĂšs segments → **Job bloquĂ© en "processing"** - **Statut** : ❌ **Non transactionnelle** - **Impact mĂ©tier** : **ÉLEVÉ** — Jobs de transcodage incomplets, streaming cassĂ© #### 5. `SocialService.ToggleLike` / `AddComment` (si compteurs critiques) - **Service** : Backend Go (`internal/core/social/service.go:131-188`) - **Fichiers concernĂ©s** : - `internal/core/social/service.go` - **Risque actuel** : - CREATE/DELETE like + UPDATE compteur sĂ©parĂ©s → **Compteur dĂ©synchronisĂ©** - CREATE comment + UPDATE compteur sĂ©parĂ©s → **Compteur dĂ©synchronisĂ©** - **Statut** : ❌ **Non transactionnelle** - **Impact mĂ©tier** : **MOYEN → ÉLEVÉ** (si compteurs critiques pour business) ### 2.2 OpĂ©rations P1 (Important — Production-grade) #### 6. `PlaylistService.AddCollaborator` - **Service** : Backend Go (`internal/services/playlist_service.go:611-665`) - **Risque** : Collaborateur créé pour playlist supprimĂ©e entre vĂ©rification et crĂ©ation - **Statut** : ❌ **Non transactionnelle** #### 7. `RoleService.AssignRoleToUser` - **Service** : Backend Go (`internal/services/role_service.go:86-99`) - **Risque** : Pas de vĂ©rifications prĂ©alables, erreurs FK non gĂ©rĂ©es proprement - **Statut** : ❌ **Non transactionnelle** #### 8. `EncodingPool.insert_segments_from_playlist` - **Service** : Stream Server Rust (`src/core/encoding_pool.rs:300-349`) - **Risque** : Segments partiellement insĂ©rĂ©s si crash au milieu de la boucle - **Statut** : ❌ **Non transactionnelle** ### 2.3 Tableau de SynthĂšse | Domaine | OpĂ©ration | Langage | Transactionnelle ? | PrioritĂ© | Commentaire | |---------|-----------|---------|-------------------|----------|-------------| | **Marketplace** | `CreateOrder` | Go | ✅ Oui | - | DĂ©jĂ  transactionnel | | **Marketplace** | `CreateProduct` | Go | ✅ Oui | - | DĂ©jĂ  transactionnel | | **Playlists** | `AddTrack` | Go | ✅ Oui | - | DĂ©jĂ  transactionnel | | **Playlists** | `RemoveTrack` | Go | ✅ Oui | - | DĂ©jĂ  transactionnel | | **Playlists** | `ReorderTracks` | Go | ✅ Oui | - | DĂ©jĂ  transactionnel | | **Playlists** | `DuplicatePlaylist` | Go | ❌ Non | **P0** | Playlist vide/incomplĂšte | | **Playlists** | `AddCollaborator` | Go | ❌ Non | P1 | Collaborateur sans playlist | | **Social** | `ToggleLike` | Go | ❌ Non | P1/P0 | Compteur dĂ©synchronisĂ© | | **Social** | `AddComment` | Go | ❌ Non | P1/P0 | Compteur dĂ©synchronisĂ© | | **RBAC** | `AssignRoleToUser` (RBACService) | Go | ❌ Non | **P0** | SĂ©curitĂ© compromise | | **RBAC** | `AssignRoleToUser` (RoleService) | Go | ❌ Non | P1 | Erreurs non gĂ©rĂ©es | | **HLS** | `CreateJob` | Go | ✅ Oui | - | DĂ©jĂ  transactionnel | | **Auth** | `RotateToken` | Go | ✅ Oui | - | DĂ©jĂ  transactionnel | | **Stream** | `persist_segment` | Rust | ❌ Non | **P0** | Segments orphelins | | **Stream** | `insert_segments_from_playlist` | Rust | ❌ Non | P1 | Playlist HLS incomplĂšte | | **Stream** | Job + Segments | Rust | ❌ Non | **P0** | Jobs incomplets | | **Chat** | `send_room_message` | Rust | ✅ Oui | - | DĂ©jĂ  transactionnel | | **Chat** | `send_dm_message` | Rust | ✅ Oui | - | DĂ©jĂ  transactionnel | --- ## 3. PATTERNS TRANSACTIONNELS RECOMMANDÉS ### 3.1 Backend Go (GORM) #### Pattern Standard ```go // Pattern recommandĂ© pour toutes les opĂ©rations multi-Ă©tapes func (s *Service) OperationMultiSteps(ctx context.Context, params ...) error { return s.db.WithContext(ctx).Transaction(func(tx *gorm.DB) error { // 1. VALIDATIONS (lectures uniquement, pas d'Ă©critures) if err := s.validateInput(ctx, tx, params); err != nil { return fmt.Errorf("validation failed: %w", err) } // 2. ÉCRITURES MULTIPLES (toutes dans la transaction) if err := tx.Create(&entity1).Error; err != nil { return fmt.Errorf("failed to create entity1: %w", err) } if err := tx.Create(&entity2).Error; err != nil { return fmt.Errorf("failed to create entity2: %w", err) } if err := tx.Model(&entity3).Update("field", value).Error; err != nil { return fmt.Errorf("failed to update entity3: %w", err) } // 3. LOGS STRUCTURÉS (optionnel, mais recommandĂ©) s.logger.Info("OperationMultiSteps completed", zap.String("entity1_id", entity1.ID.String()), zap.String("entity2_id", entity2.ID.String()), ) // 4. RETOUR nil = commit automatique // RETOUR erreur = rollback automatique return nil }) } ``` #### RĂšgles Strictes 1. **Pas d'Ă©critures post-transaction** : Toutes les Ă©critures DB doivent ĂȘtre dans la transaction 2. **Chaque chemin d'erreur → rollback** : Retourner une erreur dans la closure = rollback automatique 3. **Wrapper les erreurs avec contexte** : `fmt.Errorf("OperationName: step description: %w", err)` 4. **Context propagation** : Toujours utiliser `WithContext(ctx)` pour annulation et timeouts 5. **Pas de side effects externes** : Pas d'appels API, pas d'Ă©criture fichiers dans la transaction #### Exemple Concret : DuplicatePlaylist ```go // AVANT (non transactionnel) func (s *PlaylistDuplicateService) DuplicatePlaylist(ctx context.Context, playlistID uuid.UUID, newName string) (*models.Playlist, error) { original, err := s.playlistService.GetPlaylist(ctx, playlistID) if err != nil { return nil, err } newPlaylist, err := s.playlistService.CreatePlaylist(ctx, ...) // Transaction interne if err != nil { return nil, err } for _, track := range original.Tracks { if err := s.playlistService.AddTrackToPlaylist(ctx, newPlaylist.ID, track.ID); err != nil { // ⚠ Continue avec les autres tracks → Playlist incomplĂšte continue } } return newPlaylist, nil } // APRÈS (transactionnel) func (s *PlaylistDuplicateService) DuplicatePlaylist(ctx context.Context, playlistID uuid.UUID, newName string) (*models.Playlist, error) { var newPlaylist *models.Playlist err := s.playlistService.db.WithContext(ctx).Transaction(func(tx *gorm.DB) error { // 1. VALIDATION : RĂ©cupĂ©rer playlist originale var original models.Playlist if err := tx.Preload("Tracks").First(&original, "id = ?", playlistID).Error; err != nil { return fmt.Errorf("failed to load original playlist: %w", err) } // 2. CRÉATION : Nouvelle playlist newPlaylist = &models.Playlist{ Name: newName, UserID: original.UserID, Description: original.Description, // ... autres champs } if err := tx.Create(newPlaylist).Error; err != nil { return fmt.Errorf("failed to create duplicate playlist: %w", err) } // 3. DUPLICATION : Tous les tracks dans la mĂȘme transaction for i, track := range original.Tracks { playlistTrack := models.PlaylistTrack{ PlaylistID: newPlaylist.ID, TrackID: track.ID, Position: i + 1, } if err := tx.Create(&playlistTrack).Error; err != nil { return fmt.Errorf("failed to add track %s to duplicate: %w", track.ID, err) } } // 4. MISE À JOUR : Compteur de tracks if err := tx.Model(newPlaylist).Update("track_count", len(original.Tracks)).Error; err != nil { return fmt.Errorf("failed to update track_count: %w", err) } // 5. LOG s.logger.Info("Playlist duplicated", zap.String("original_id", playlistID.String()), zap.String("new_id", newPlaylist.ID.String()), zap.Int("tracks_count", len(original.Tracks)), ) return nil // Commit automatique }) if err != nil { return nil, err // Rollback automatique si erreur } return newPlaylist, nil } ``` ### 3.2 Rust (SQLx) #### Pattern Standard ```rust // Pattern recommandĂ© pour toutes les opĂ©rations multi-Ă©tapes async fn operation_multi_steps( &self, pool: &PgPool, params: &Params, ) -> Result { // 1. DÉBUT TRANSACTION let mut tx = pool.begin().await .map_err(|e| AppError::DatabaseError { message: "failed to begin transaction".to_string(), source: e.into(), })?; // 2. VALIDATIONS (lectures uniquement) let entity = sqlx::query_as!( Entity, "SELECT * FROM entities WHERE id = $1", params.id ) .fetch_optional(&mut *tx) .await .map_err(|e| AppError::DatabaseError { message: format!("failed to validate entity {}", params.id), source: e.into(), })?; if entity.is_none() { return Err(AppError::NotFound { resource: "entity", id: params.id.to_string(), }); } // 3. ÉCRITURES MULTIPLES (toutes dans la transaction) sqlx::query!( "INSERT INTO table1 (field1, field2) VALUES ($1, $2)", params.value1, params.value2 ) .execute(&mut *tx) .await .map_err(|e| AppError::DatabaseError { message: "failed to insert into table1".to_string(), source: e.into(), })?; sqlx::query!( "UPDATE table2 SET field = $1 WHERE id = $2", params.value3, params.id ) .execute(&mut *tx) .await .map_err(|e| AppError::DatabaseError { message: "failed to update table2".to_string(), source: e.into(), })?; // 4. COMMIT (si tout OK) tx.commit().await .map_err(|e| AppError::DatabaseError { message: "failed to commit transaction".to_string(), source: e.into(), })?; // 5. LOG tracing::info!( entity_id = %params.id, "operation_multi_steps completed" ); Ok(Output { ... }) // NOTE : Si une erreur est retournĂ©e avant commit(), // la transaction est automatiquement rollback Ă  la fin du scope } ``` #### RĂšgles Strictes 1. **Pas d'Ă©critures post-transaction** : Toutes les Ă©critures DB doivent ĂȘtre dans la transaction 2. **Chaque erreur → rollback** : Si une erreur est retournĂ©e avant `tx.commit()`, la transaction est rollback automatiquement 3. **Wrapper les erreurs avec contexte** : `AppError::DatabaseError { message, source }` 4. **Utiliser `&mut *tx`** : Passer `&mut *tx` aux queries, pas `&tx` 5. **Pas de side effects externes** : Pas d'appels API, pas d'Ă©criture fichiers dans la transaction #### Exemple Concret : persist_segment ```rust // AVANT (non transactionnel) async fn persist_segment(&self, segment: &SegmentInfo) -> Result<(), AppError> { // INSERT segment sqlx::query!( "INSERT INTO stream_segments (job_id, segment_path, duration, ...) VALUES ($1, $2, $3, ...)", segment.job_id, segment.path, segment.duration, // ... ) .execute(&self.db_pool) // ⚠ Pas de transaction .await?; // UPDATE job self.update_current_duration().await?; // ⚠ Pas dans la mĂȘme transaction Ok(()) } // APRÈS (transactionnel) async fn persist_segment(&self, segment: &SegmentInfo) -> Result<(), AppError> { let mut tx = self.db_pool.begin().await .map_err(|e| AppError::DatabaseError { message: "failed to begin transaction for segment persistence".to_string(), source: e.into(), })?; // 1. INSERT segment sqlx::query!( "INSERT INTO stream_segments (job_id, segment_path, duration, sequence_number, ...) VALUES ($1, $2, $3, $4, ...)", segment.job_id, segment.path.to_string(), segment.duration.as_secs_f64(), segment.sequence_number, // ... ) .execute(&mut *tx) .await .map_err(|e| AppError::DatabaseError { message: format!("failed to insert segment {} for job {}", segment.path.display(), segment.job_id), source: e.into(), })?; // 2. UPDATE job (durĂ©e actuelle) let current_duration = self.calculate_current_duration(segment.job_id).await?; sqlx::query!( "UPDATE stream_jobs SET current_duration = $1, updated_at = NOW() WHERE id = $2", current_duration.as_secs_f64(), segment.job_id ) .execute(&mut *tx) .await .map_err(|e| AppError::DatabaseError { message: format!("failed to update job {} duration", segment.job_id), source: e.into(), })?; // 3. COMMIT tx.commit().await .map_err(|e| AppError::DatabaseError { message: "failed to commit segment persistence transaction".to_string(), source: e.into(), })?; tracing::debug!( job_id = %segment.job_id, segment_path = %segment.path.display(), "Segment persisted successfully" ); Ok(()) } ``` --- ## 4. DESIGN DÉTAILLÉ PAR DOMAINE (P0) ### 4.1 Marketplace #### OpĂ©rations ConcernĂ©es - ✅ `CreateOrder` — **DĂ©jĂ  transactionnel** (pas de modification nĂ©cessaire) - ✅ `CreateProduct` — **DĂ©jĂ  transactionnel** (pas de modification nĂ©cessaire) #### SchĂ©ma Transactionnel Cible **Aucune modification nĂ©cessaire** — Les opĂ©rations marketplace sont dĂ©jĂ  transactionnelles. #### RĂšgles d'Invariants 1. **Jamais d'item sans order** : Tous les `order_items` sont créés dans la mĂȘme transaction que l'`order` 2. **Jamais de licence sans order** : Toutes les `licenses` sont créées dans la mĂȘme transaction que l'`order` 3. **Order toujours dans un Ă©tat cohĂ©rent** : `PENDING` → `COMPLETED` dans la mĂȘme transaction --- ### 4.2 Playlists / Collaborations #### OpĂ©rations ConcernĂ©es - ❌ `DuplicatePlaylist` — **P0** — À rendre transactionnel - ❌ `AddCollaborator` — **P1** — À rendre transactionnel #### SchĂ©ma Transactionnel Cible : DuplicatePlaylist **Flux transactionnel** : ``` 1. DÉBUT TRANSACTION ↓ 2. VALIDATION : Charger playlist originale + tracks (SELECT avec Preload) ├─ Si playlist n'existe pas → ROLLBACK + erreur NotFound └─ Si erreur DB → ROLLBACK + erreur DatabaseError ↓ 3. CRÉATION : Nouvelle playlist (INSERT INTO playlists) ├─ Si erreur DB → ROLLBACK + erreur DatabaseError └─ Si contrainte violĂ©e → ROLLBACK + erreur ValidationError ↓ 4. DUPLICATION : Pour chaque track de l'originale ├─ INSERT INTO playlist_tracks (playlist_id, track_id, position) ├─ Si erreur sur un track → ROLLBACK complet (tous les tracks annulĂ©s) └─ Si tous les tracks OK → Continue ↓ 5. MISE À JOUR : Compteur de tracks (UPDATE playlists.track_count) ├─ Si erreur DB → ROLLBACK + erreur DatabaseError └─ Si OK → Continue ↓ 6. COMMIT ↓ 7. RETOUR : Nouvelle playlist avec tous les tracks ``` **Erreurs possibles** : | Étape | Erreur Possible | Action | |-------|----------------|--------| | 2 | Playlist n'existe pas | Rollback + `NotFound` | | 2 | Erreur DB (timeout, connection) | Rollback + `DatabaseError` | | 3 | Contrainte violĂ©e (nom dupliquĂ©) | Rollback + `ValidationError` | | 4 | Track n'existe plus | Rollback + `NotFound` (tous les tracks annulĂ©s) | | 4 | Contrainte FK violĂ©e | Rollback + `ValidationError` | | 5 | Erreur DB | Rollback + `DatabaseError` | **Invariants garantis** : - ✅ **Jamais de playlist vide créée** : Si ajout des tracks Ă©choue, la playlist est rollback - ✅ **Jamais de playlist partiellement dupliquĂ©e** : Tous les tracks ou aucun - ✅ **Compteur toujours cohĂ©rent** : `track_count` = nombre rĂ©el de tracks dans `playlist_tracks` #### SchĂ©ma Transactionnel Cible : AddCollaborator **Flux transactionnel** : ``` 1. DÉBUT TRANSACTION ↓ 2. VALIDATION : VĂ©rifier existence playlist (SELECT playlists WHERE id = $1) ├─ Si playlist n'existe pas → ROLLBACK + erreur NotFound └─ Si erreur DB → ROLLBACK + erreur DatabaseError ↓ 3. VALIDATION : VĂ©rifier existence user (SELECT users WHERE id = $1) ├─ Si user n'existe pas → ROLLBACK + erreur NotFound └─ Si erreur DB → ROLLBACK + erreur DatabaseError ↓ 4. VALIDATION : VĂ©rifier doublon (SELECT playlist_collaborators WHERE ...) ├─ Si doublon existe → ROLLBACK + erreur ValidationError └─ Si OK → Continue ↓ 5. CRÉATION : Collaborateur (INSERT INTO playlist_collaborators) ├─ Si erreur DB → ROLLBACK + erreur DatabaseError └─ Si OK → Continue ↓ 6. COMMIT ↓ 7. RETOUR : Collaborateur créé ``` **Invariants garantis** : - ✅ **Jamais de collaborateur sans playlist valide** : VĂ©rification dans la transaction - ✅ **Jamais de collaborateur sans user valide** : VĂ©rification dans la transaction - ✅ **Jamais de doublon** : VĂ©rification dans la transaction --- ### 4.3 Social (Likes/Comments) #### OpĂ©rations ConcernĂ©es - ❌ `ToggleLike` — **P1/P0** — À rendre transactionnel - ❌ `AddComment` — **P1/P0** — À rendre transactionnel #### SchĂ©ma Transactionnel Cible : ToggleLike **Flux transactionnel** : ``` 1. DÉBUT TRANSACTION ↓ 2. VÉRIFICATION : Like existe dĂ©jĂ  ? (SELECT likes WHERE user_id = $1 AND post_id = $2) ├─ Si like existe → Mode UNLIKE │ ├─ DELETE FROM likes WHERE ... │ ├─ UPDATE posts SET like_count = like_count - 1 WHERE id = $2 │ └─ Si erreur → ROLLBACK └─ Si like n'existe pas → Mode LIKE ├─ INSERT INTO likes (user_id, post_id, ...) ├─ UPDATE posts SET like_count = like_count + 1 WHERE id = $2 └─ Si erreur → ROLLBACK ↓ 3. COMMIT ↓ 4. RETOUR : État final (liked/unliked) ``` **Erreurs possibles** : | Étape | Erreur Possible | Action | |-------|----------------|--------| | 2 | Post n'existe pas | Rollback + `NotFound` | | 2 | Erreur DB (timeout) | Rollback + `DatabaseError` | | 2 | Race condition (2 likes simultanĂ©s) | Rollback + `ConflictError` (contrainte UNIQUE) | **Invariants garantis** : - ✅ **Compteur toujours synchronisĂ©** : `like_count` = nombre rĂ©el de likes dans `likes` - ✅ **Pas de like sans post** : VĂ©rification FK dans la transaction - ✅ **Pas de unlike si pas de like** : VĂ©rification dans la transaction #### SchĂ©ma Transactionnel Cible : AddComment **Flux transactionnel** : ``` 1. DÉBUT TRANSACTION ↓ 2. VALIDATION : Post existe ? (SELECT posts WHERE id = $1) ├─ Si post n'existe pas → ROLLBACK + erreur NotFound └─ Si OK → Continue ↓ 3. CRÉATION : Commentaire (INSERT INTO comments) ├─ Si erreur DB → ROLLBACK + erreur DatabaseError └─ Si OK → Continue ↓ 4. MISE À JOUR : Compteur (UPDATE posts SET comment_count = comment_count + 1) ├─ Si erreur DB → ROLLBACK + erreur DatabaseError └─ Si OK → Continue ↓ 5. COMMIT ↓ 6. RETOUR : Commentaire créé ``` **Invariants garantis** : - ✅ **Compteur toujours synchronisĂ©** : `comment_count` = nombre rĂ©el de comments dans `comments` - ✅ **Jamais de commentaire sans post** : VĂ©rification FK dans la transaction --- ### 4.4 Stream Jobs / Segments #### OpĂ©rations ConcernĂ©es - ❌ `SegmentTracker.persist_segment` — **P0** — À rendre transactionnel - ❌ `StreamProcessor` (Job + Segments) — **P0** — À rendre transactionnel - ❌ `EncodingPool.insert_segments_from_playlist` — **P1** — À rendre transactionnel #### SchĂ©ma Transactionnel Cible : persist_segment **Flux transactionnel** : ``` 1. DÉBUT TRANSACTION ↓ 2. VALIDATION : Job existe et est en "processing" ? (SELECT stream_jobs WHERE id = $1) ├─ Si job n'existe pas → ROLLBACK + erreur NotFound ├─ Si job n'est pas en "processing" → ROLLBACK + erreur InvalidState └─ Si OK → Continue ↓ 3. INSERTION : Segment (INSERT INTO stream_segments) ├─ Si erreur DB → ROLLBACK + erreur DatabaseError ├─ Si contrainte violĂ©e (doublon) → ROLLBACK + erreur ValidationError └─ Si OK → Continue ↓ 4. CALCUL : DurĂ©e actuelle (SUM(duration) FROM stream_segments WHERE job_id = $1) ├─ Si erreur DB → ROLLBACK + erreur DatabaseError └─ Si OK → Continue ↓ 5. MISE À JOUR : Job (UPDATE stream_jobs SET current_duration = $1, updated_at = NOW()) ├─ Si erreur DB → ROLLBACK + erreur DatabaseError └─ Si OK → Continue ↓ 6. COMMIT ↓ 7. RETOUR : Segment persistĂ© ``` **Erreurs possibles** : | Étape | Erreur Possible | Action | |-------|----------------|--------| | 2 | Job n'existe pas | Rollback + `NotFound` | | 2 | Job en Ă©tat invalide (completed, failed) | Rollback + `InvalidState` | | 3 | Segment dĂ©jĂ  existant (sequence_number dupliquĂ©) | Rollback + `ValidationError` | | 4 | Erreur DB (timeout) | Rollback + `DatabaseError` | | 5 | Erreur DB | Rollback + `DatabaseError` | **Invariants garantis** : - ✅ **Jamais de segment sans job valide** : VĂ©rification dans la transaction - ✅ **Job toujours Ă  jour** : `current_duration` = somme rĂ©elle des segments - ✅ **Pas de segments orphelins** : Si job supprimĂ©, segments supprimĂ©s (CASCADE) #### SchĂ©ma Transactionnel Cible : StreamProcessor (Job + Segments) **ProblĂšme actuel** : Job créé, puis segments persistĂ©s individuellement, puis job finalisĂ©. **Solution recommandĂ©e** : **Pattern "Two-Phase"** **Phase 1 : CrĂ©ation Job (PENDING)** ``` 1. DÉBUT TRANSACTION ↓ 2. CRÉATION : Job en Ă©tat "pending" (INSERT INTO stream_jobs, status = 'pending') ├─ Si erreur DB → ROLLBACK + erreur DatabaseError └─ Si OK → Continue ↓ 3. COMMIT ↓ 4. RETOUR : Job créé (status = 'pending') ``` **Phase 2 : Traitement FFmpeg (hors transaction)** ``` 1. Spawn FFmpeg process 2. DĂ©tecter segments (via FFmpegMonitor) 3. Persister segments (via persist_segment, chaque segment dans sa propre transaction) ``` **Phase 3 : Finalisation Job (COMPLETED)** ``` 1. DÉBUT TRANSACTION ↓ 2. VALIDATION : VĂ©rifier que tous les segments sont persistĂ©s ├─ SELECT COUNT(*) FROM stream_segments WHERE job_id = $1 ├─ Si aucun segment → ROLLBACK + erreur InvalidState └─ Si OK → Continue ↓ 3. CALCUL : DurĂ©e totale (SUM(duration) FROM stream_segments WHERE job_id = $1) ├─ Si erreur DB → ROLLBACK + erreur DatabaseError └─ Si OK → Continue ↓ 4. MISE À JOUR : Job (UPDATE stream_jobs SET status = 'completed', total_duration = $1, updated_at = NOW()) ├─ Si erreur DB → ROLLBACK + erreur DatabaseError └─ Si OK → Continue ↓ 5. COMMIT ↓ 6. RETOUR : Job finalisĂ© ``` **Alternative (plus simple) : Pattern "Batch Persistence"** Si on veut Ă©viter le pattern two-phase, on peut utiliser `persist_all()` Ă  la fin : ``` 1. DÉBUT TRANSACTION ↓ 2. CRÉATION : Job en Ă©tat "processing" (INSERT INTO stream_jobs) ↓ 3. COMMIT (job créé) ↓ 4. TRAITEMENT FFmpeg (hors transaction) ├─ DĂ©tecter segments (via FFmpegMonitor) ├─ Stocker segments en mĂ©moire (SegmentTracker) └─ Ne PAS persister immĂ©diatement ↓ 5. DÉBUT TRANSACTION (batch) ↓ 6. INSERTION : Tous les segments en batch (INSERT INTO stream_segments ... VALUES (...), (...), (...)) ├─ Si erreur DB → ROLLBACK + erreur DatabaseError └─ Si OK → Continue ↓ 7. CALCUL : DurĂ©e totale (SUM(duration)) ↓ 8. MISE À JOUR : Job (UPDATE stream_jobs SET status = 'completed', total_duration = $1) ├─ Si erreur DB → ROLLBACK + erreur DatabaseError └─ Si OK → Continue ↓ 9. COMMIT ↓ 10. RETOUR : Job finalisĂ© ``` **Recommandation** : **Pattern "Batch Persistence"** (plus simple, moins de transactions) **Invariants garantis** : - ✅ **Jamais de job sans segments** : Validation avant finalisation - ✅ **Job toujours Ă  jour** : `total_duration` = somme rĂ©elle des segments - ✅ **Pas de segments orphelins** : Tous les segments créés dans la mĂȘme transaction que la finalisation #### SchĂ©ma Transactionnel Cible : insert_segments_from_playlist **Flux transactionnel** : ``` 1. DÉBUT TRANSACTION ↓ 2. VALIDATION : Job existe ? (SELECT stream_jobs WHERE id = $1) ├─ Si job n'existe pas → ROLLBACK + erreur NotFound └─ Si OK → Continue ↓ 3. INSERTION : Tous les segments en batch ├─ Pour chaque segment dans la playlist : │ ├─ INSERT INTO stream_segments (job_id, segment_path, sequence_number, ...) │ ├─ Si erreur sur un segment → ROLLBACK complet (tous les segments annulĂ©s) │ └─ Si OK → Continue └─ Si tous les segments OK → Continue ↓ 4. CALCUL : DurĂ©e totale (SUM(duration) FROM stream_segments WHERE job_id = $1) ↓ 5. MISE À JOUR : Job (UPDATE stream_jobs SET total_duration = $1, updated_at = NOW()) ↓ 6. COMMIT ↓ 7. RETOUR : Segments insĂ©rĂ©s ``` **Invariants garantis** : - ✅ **Playlist HLS complĂšte ou vide** : Tous les segments ou aucun - ✅ **Job toujours Ă  jour** : `total_duration` = somme rĂ©elle des segments --- ### 4.5 RBAC / Permissions #### OpĂ©rations ConcernĂ©es - ❌ `RBACService.AssignRoleToUser` — **P0** — À rendre transactionnel - ❌ `RoleService.AssignRoleToUser` — **P1** — À rendre transactionnel #### SchĂ©ma Transactionnel Cible : RBACService.AssignRoleToUser **Flux transactionnel** : ``` 1. DÉBUT TRANSACTION ↓ 2. VALIDATION : User existe ? (SELECT users WHERE id = $1 FOR UPDATE) ├─ Si user n'existe pas → ROLLBACK + erreur NotFound ├─ FOR UPDATE : Verrouille la ligne pour Ă©viter race condition └─ Si OK → Continue ↓ 3. VALIDATION : Role existe ? (SELECT roles WHERE id = $1 FOR UPDATE) ├─ Si role n'existe pas → ROLLBACK + erreur NotFound ├─ FOR UPDATE : Verrouille la ligne pour Ă©viter race condition └─ Si OK → Continue ↓ 4. VALIDATION : Doublon ? (SELECT user_roles WHERE user_id = $1 AND role_id = $2) ├─ Si doublon existe → ROLLBACK + erreur ValidationError └─ Si OK → Continue ↓ 5. INSERTION : Assignation (INSERT INTO user_roles (user_id, role_id, ...)) ├─ Si erreur DB → ROLLBACK + erreur DatabaseError ├─ Si contrainte UNIQUE violĂ©e → ROLLBACK + erreur ValidationError (race condition dĂ©tectĂ©e) └─ Si OK → Continue ↓ 6. COMMIT ↓ 7. RETOUR : Assignation créée ``` **Erreurs possibles** : | Étape | Erreur Possible | Action | |-------|----------------|--------| | 2 | User n'existe pas | Rollback + `NotFound` | | 2 | User supprimĂ© entre vĂ©rification et INSERT | Rollback (FK constraint) | | 3 | Role n'existe pas | Rollback + `NotFound` | | 3 | Role supprimĂ© entre vĂ©rification et INSERT | Rollback (FK constraint) | | 4 | Doublon dĂ©tectĂ© | Rollback + `ValidationError` | | 5 | Race condition (2 assignations simultanĂ©es) | Rollback + `ValidationError` (contrainte UNIQUE) | **Invariants garantis** : - ✅ **Jamais d'assignation sans user valide** : VĂ©rification + FK dans la transaction - ✅ **Jamais d'assignation sans role valide** : VĂ©rification + FK dans la transaction - ✅ **Jamais de doublon** : VĂ©rification + contrainte UNIQUE dans la transaction - ✅ **Pas de race condition** : `FOR UPDATE` + contrainte UNIQUE #### SchĂ©ma Transactionnel Cible : RoleService.AssignRoleToUser **Flux transactionnel** : ``` 1. DÉBUT TRANSACTION ↓ 2. VALIDATION : User existe ? (SELECT users WHERE id = $1) ├─ Si user n'existe pas → ROLLBACK + erreur NotFound └─ Si OK → Continue ↓ 3. VALIDATION : Role existe ? (SELECT roles WHERE id = $1) ├─ Si role n'existe pas → ROLLBACK + erreur NotFound └─ Si OK → Continue ↓ 4. VALIDATION : Doublon ? (SELECT user_roles WHERE user_id = $1 AND role_id = $2) ├─ Si doublon existe → ROLLBACK + erreur ValidationError └─ Si OK → Continue ↓ 5. INSERTION : Assignation (INSERT INTO user_roles) ├─ Si erreur DB → ROLLBACK + erreur DatabaseError └─ Si OK → Continue ↓ 6. COMMIT ↓ 7. RETOUR : Assignation créée ``` **Invariants garantis** : - ✅ **MĂȘme garanties que RBACService** : VĂ©rifications + FK + contrainte UNIQUE --- ## 5. PLAN D'IMPLÉMENTATION PAR PHASES ### Phase 1 — P0 Backend Go **Objectif** : Rendre transactionnelles toutes les opĂ©rations P0 du backend Go. **DurĂ©e estimĂ©e** : 4-6 heures **OpĂ©rations Ă  traiter** : 1. ✅ `PlaylistDuplicateService.DuplicatePlaylist` 2. ✅ `RBACService.AssignRoleToUser` 3. ⚠ `SocialService.ToggleLike` (si compteurs critiques) 4. ⚠ `SocialService.AddComment` (si compteurs critiques) **Ordre recommandĂ©** : 1. **RBAC** (sĂ©curitĂ© critique) → `RBACService.AssignRoleToUser` 2. **Playlists** (impact utilisateur Ă©levĂ©) → `PlaylistDuplicateService.DuplicatePlaylist` 3. **Social** (si compteurs critiques) → `SocialService.ToggleLike`, `AddComment` **Fichiers principaux** : - `internal/services/rbac_service.go` (lignes 168-210) - `internal/services/playlist_duplicate_service.go` (lignes 41-131) - `internal/core/social/service.go` (lignes 131-188) **Risques et points d'attention** : - ⚠ **RBAC** : Utiliser `FOR UPDATE` pour Ă©viter race conditions - ⚠ **Playlists** : VĂ©rifier que `CreatePlaylist` peut ĂȘtre appelĂ© avec un `*gorm.DB` transactionnel (sinon, refactoriser) - ⚠ **Social** : VĂ©rifier si les compteurs sont critiques pour le business (si non, garder en P1) **CritĂšres de "done"** : - [ ] Toutes les opĂ©rations P0 Backend Go sont transactionnelles - [ ] Tests unitaires passent (simulation d'erreur au milieu de la transaction) - [ ] Tests d'intĂ©gration passent (vĂ©rification rollback en cas d'erreur) - [ ] Aucune rĂ©gression sur les fonctionnalitĂ©s existantes --- ### Phase 2 — P0 Rust Stream **Objectif** : Rendre transactionnelles toutes les opĂ©rations P0 du Stream Server. **DurĂ©e estimĂ©e** : 6-8 heures **OpĂ©rations Ă  traiter** : 1. ✅ `SegmentTracker.persist_segment` 2. ✅ `StreamProcessor` (Job + Segments) — Pattern "Batch Persistence" **Ordre recommandĂ©** : 1. **SegmentTracker** (base) → `persist_segment` 2. **StreamProcessor** (orchestration) → Pattern "Batch Persistence" **Fichiers principaux** : - `src/core/processing/segment_tracker.rs` (lignes 82-106) - `src/core/processing/processor.rs` (lignes 238-243, `finalize()`) - `src/core/processing/callbacks.rs` (si nĂ©cessaire pour batch persistence) **Risques et points d'attention** : - ⚠ **SegmentTracker** : VĂ©rifier que `update_current_duration()` peut ĂȘtre intĂ©grĂ© dans la transaction - ⚠ **StreamProcessor** : DĂ©cider entre pattern "Two-Phase" ou "Batch Persistence" (recommandation : Batch) - ⚠ **Performance** : Batch persistence peut ĂȘtre plus lent si beaucoup de segments (optimiser avec `INSERT ... VALUES (...), (...), (...)`) **CritĂšres de "done"** : - [ ] Toutes les opĂ©rations P0 Stream Server sont transactionnelles - [ ] Tests unitaires passent (simulation d'erreur au milieu de la transaction) - [ ] Tests d'intĂ©gration passent (vĂ©rification rollback en cas d'erreur) - [ ] Aucune rĂ©gression sur le streaming (tester avec un fichier audio rĂ©el) --- ### Phase 3 — P1 Backend Go **Objectif** : Rendre transactionnelles toutes les opĂ©rations P1 du backend Go. **DurĂ©e estimĂ©e** : 2-3 heures **OpĂ©rations Ă  traiter** : 1. ✅ `PlaylistService.AddCollaborator` 2. ✅ `RoleService.AssignRoleToUser` **Fichiers principaux** : - `internal/services/playlist_service.go` (lignes 611-665) - `internal/services/role_service.go` (lignes 86-99) **Risques et points d'attention** : - ⚠ **AddCollaborator** : Risque faible, mais bonne pratique de rendre transactionnel - ⚠ **RoleService** : S'assurer que les vĂ©rifications sont bien faites avant INSERT **CritĂšres de "done"** : - [ ] Toutes les opĂ©rations P1 Backend Go sont transactionnelles - [ ] Tests unitaires passent - [ ] Aucune rĂ©gression --- ### Phase 4 — P1 Rust Stream **Objectif** : Rendre transactionnelle l'opĂ©ration P1 du Stream Server. **DurĂ©e estimĂ©e** : 2-3 heures **OpĂ©rations Ă  traiter** : 1. ✅ `EncodingPool.insert_segments_from_playlist` **Fichiers principaux** : - `src/core/encoding_pool.rs` (lignes 300-349) **Risques et points d'attention** : - ⚠ **Performance** : Utiliser batch INSERT pour Ă©viter trop de queries **CritĂšres de "done"** : - [ ] OpĂ©ration P1 Stream Server est transactionnelle - [ ] Tests unitaires passent - [ ] Aucune rĂ©gression --- ### Phase 5 — Tests et Validation **Objectif** : Valider que toutes les transactions fonctionnent correctement. **DurĂ©e estimĂ©e** : 4-6 heures **Actions** : 1. Écrire tests unitaires pour chaque opĂ©ration transactionnelle 2. Écrire tests d'intĂ©gration pour vĂ©rifier rollback 3. Tests de charge (optionnel, si nĂ©cessaire) 4. Documentation mise Ă  jour **CritĂšres de "done"** : - [ ] Tous les tests passent - [ ] Documentation mise Ă  jour (`TRIAGE.md`, `AUDIT_STABILITY.md`) - [ ] Checklist de validation complĂ©tĂ©e --- ## 6. STRATÉGIE DE TESTS ### 6.1 Tests Unitaires #### Backend Go **Pattern de test recommandĂ©** : ```go func TestPlaylistDuplicateService_DuplicatePlaylist_TransactionRollback(t *testing.T) { // Setup : DB de test, mock data db := setupTestDB(t) service := NewPlaylistDuplicateService(db, ...) // Test : Simuler erreur au milieu de la transaction originalPlaylist := createTestPlaylist(t, db, 5) // 5 tracks // Mock : Faire Ă©chouer l'ajout du 3Ăšme track // (en injectant une erreur dans AddTrack) // Action _, err := service.DuplicatePlaylist(ctx, originalPlaylist.ID, "Duplicate") // Assert : Erreur retournĂ©e assert.Error(t, err) // Assert : Aucune playlist créée (rollback complet) var count int64 db.Model(&models.Playlist{}).Where("name = ?", "Duplicate").Count(&count) assert.Equal(t, int64(0), count, "Playlist should not be created on error") // Assert : Aucun track créé (rollback complet) db.Model(&models.PlaylistTrack{}).Where("playlist_id = ?", ...).Count(&count) assert.Equal(t, int64(0), count, "Tracks should not be created on error") } ``` **Tests Ă  Ă©crire pour chaque opĂ©ration** : 1. ✅ **SuccĂšs** : Transaction complĂšte, toutes les Ă©critures OK 2. ✅ **Rollback sur erreur** : Erreur au milieu → rollback complet 3. ✅ **Validation** : Erreur de validation → rollback (pas d'Ă©critures partielles) 4. ✅ **Race condition** : 2 requĂȘtes simultanĂ©es → une seule rĂ©ussit (si applicable) #### Rust (SQLx) **Pattern de test recommandĂ©** : ```rust #[tokio::test] async fn test_persist_segment_transaction_rollback() { // Setup : DB de test, mock data let pool = setup_test_db().await; let tracker = SegmentTracker::new(pool.clone()); // Test : Simuler erreur au milieu de la transaction let job = create_test_job(&pool, "processing").await; let segment = SegmentInfo { job_id: job.id, path: PathBuf::from("/test/segment.ts"), // ... }; // Mock : Faire Ă©chouer l'UPDATE job (en injectant une erreur) // (ex: supprimer le job avant l'UPDATE) // Action let result = tracker.persist_segment(&segment).await; // Assert : Erreur retournĂ©e assert!(result.is_err()); // Assert : Aucun segment créé (rollback complet) let count: i64 = sqlx::query_scalar!( "SELECT COUNT(*) FROM stream_segments WHERE job_id = $1", job.id ) .fetch_one(&pool) .await .unwrap(); assert_eq!(count, 0, "Segment should not be created on error"); } ``` **Tests Ă  Ă©crire pour chaque opĂ©ration** : 1. ✅ **SuccĂšs** : Transaction complĂšte, toutes les Ă©critures OK 2. ✅ **Rollback sur erreur** : Erreur au milieu → rollback complet 3. ✅ **Validation** : Erreur de validation → rollback 4. ✅ **Race condition** : 2 requĂȘtes simultanĂ©es → une seule rĂ©ussit (si applicable) ### 6.2 Tests d'IntĂ©gration #### Simulation d'Erreur "au Milieu" d'une Transaction **Backend Go** : ```go func TestPlaylistDuplicateService_DuplicatePlaylist_Integration(t *testing.T) { db := setupIntegrationDB(t) // CrĂ©er une playlist avec 10 tracks original := createTestPlaylist(t, db, 10) // Injecter une erreur DB au 5Ăšme track (en utilisant un hook GORM) db.Callback().Create().Before("gorm:create").Register("inject_error", func(db *gorm.DB) { // Compter les tracks créés var count int64 db.Model(&models.PlaylistTrack{}).Count(&count) if count == 4 { // 5Ăšme track db.AddError(errors.New("simulated DB error")) } }) service := NewPlaylistDuplicateService(db, ...) _, err := service.DuplicatePlaylist(ctx, original.ID, "Duplicate") assert.Error(t, err) // VĂ©rifier rollback : Aucune playlist créée var playlistCount int64 db.Model(&models.Playlist{}).Where("name = ?", "Duplicate").Count(&playlistCount) assert.Equal(t, int64(0), playlistCount) // VĂ©rifier rollback : Aucun track créé var trackCount int64 db.Model(&models.PlaylistTrack{}).Where("playlist_id = ?", ...).Count(&trackCount) assert.Equal(t, int64(0), trackCount) } ``` **Rust (SQLx)** : ```rust #[tokio::test] async fn test_persist_segment_integration_rollback() { let pool = setup_integration_db().await; let tracker = SegmentTracker::new(pool.clone()); let job = create_test_job(&pool, "processing").await; // Injecter une erreur : Supprimer le job avant l'UPDATE sqlx::query!("DELETE FROM stream_jobs WHERE id = $1", job.id) .execute(&pool) .await .unwrap(); let segment = SegmentInfo { ... }; let result = tracker.persist_segment(&segment).await; assert!(result.is_err()); // VĂ©rifier rollback : Aucun segment créé let count: i64 = sqlx::query_scalar!( "SELECT COUNT(*) FROM stream_segments WHERE job_id = $1", job.id ) .fetch_one(&pool) .await .unwrap(); assert_eq!(count, 0); } ``` ### 6.3 VĂ©rification dans la DB aprĂšs Rollback **Backend Go** : ```go func TestRBACService_AssignRoleToUser_RollbackVerification(t *testing.T) { db := setupTestDB(t) service := NewRBACService(db, ...) user := createTestUser(t, db) role := createTestRole(t, db) // Simuler erreur : Supprimer le role avant l'INSERT db.Delete(&role) err := service.AssignRoleToUser(ctx, user.ID, role.ID) assert.Error(t, err) // VĂ©rifier rollback : Aucune assignation créée var count int64 db.Model(&models.UserRole{}). Where("user_id = ? AND role_id = ?", user.ID, role.ID). Count(&count) assert.Equal(t, int64(0), count, "UserRole should not be created on error") } ``` **Rust (SQLx)** : ```rust #[tokio::test] async fn test_assign_role_rollback_verification() { let pool = setup_test_db().await; let service = RBACService::new(pool.clone()); let user = create_test_user(&pool).await; let role = create_test_role(&pool).await; // Simuler erreur : Supprimer le role avant l'INSERT sqlx::query!("DELETE FROM roles WHERE id = $1", role.id) .execute(&pool) .await .unwrap(); let result = service.assign_role_to_user(user.id, role.id).await; assert!(result.is_err()); // VĂ©rifier rollback : Aucune assignation créée let count: i64 = sqlx::query_scalar!( "SELECT COUNT(*) FROM user_roles WHERE user_id = $1 AND role_id = $2", user.id, role.id ) .fetch_one(&pool) .await .unwrap(); assert_eq!(count, 0); } ``` ### 6.4 IntĂ©gration aux Suites de Tests Existantes **Backend Go** : - Ajouter les tests dans les fichiers `*_test.go` existants - Utiliser `internal/database/test_helpers.go` pour setup DB de test - Utiliser `testify/assert` pour les assertions **Rust** : - Ajouter les tests dans les fichiers `*_test.rs` ou `tests/` existants - Utiliser `sqlx::test` ou containers Docker pour DB de test - Utiliser `assert!` et `assert_eq!` pour les assertions --- ## 7. CHECKLIST DE VALIDATION ### 7.1 Couverture du Plan - [x] Toutes les opĂ©rations P0 sont couvertes par le plan - [x] `PlaylistDuplicateService.DuplicatePlaylist` - [x] `RBACService.AssignRoleToUser` - [x] `SegmentTracker.persist_segment` - [x] `StreamProcessor` (Job + Segments) - [x] `SocialService.ToggleLike` / `AddComment` (si critiques) - [x] Toutes les opĂ©rations P1 sont couvertes par le plan - [x] `PlaylistService.AddCollaborator` - [x] `RoleService.AssignRoleToUser` - [x] `EncodingPool.insert_segments_from_playlist` ### 7.2 Design par OpĂ©ration - [x] Pour chaque opĂ©ration P0 : pattern transactionnel dĂ©fini - [x] `DuplicatePlaylist` : Transaction complĂšte (playlist + tracks + compteur) - [x] `AssignRoleToUser` (RBACService) : Transaction avec `FOR UPDATE` + vĂ©rifications - [x] `persist_segment` : Transaction (INSERT segment + UPDATE job) - [x] `StreamProcessor` : Pattern "Batch Persistence" (job + segments batch) - [x] Pour chaque opĂ©ration P1 : pattern transactionnel dĂ©fini - [x] `AddCollaborator` : Transaction avec vĂ©rifications - [x] `AssignRoleToUser` (RoleService) : Transaction avec vĂ©rifications - [x] `insert_segments_from_playlist` : Transaction batch ### 7.3 Phases d'ImplĂ©mentation - [x] Phases d'implĂ©mentation claires et ordonnĂ©es - [x] Phase 1 : P0 Backend Go (4-6h) - [x] Phase 2 : P0 Rust Stream (6-8h) - [x] Phase 3 : P1 Backend Go (2-3h) - [x] Phase 4 : P1 Rust Stream (2-3h) - [x] Phase 5 : Tests et Validation (4-6h) - [x] Pour chaque phase : objectifs, fichiers, risques, critĂšres de "done" ### 7.4 StratĂ©gie de Tests - [x] StratĂ©gie de test documentĂ©e - [x] Tests unitaires (succĂšs, rollback, validation, race condition) - [x] Tests d'intĂ©gration (simulation d'erreur, vĂ©rification rollback) - [x] IntĂ©gration aux suites de tests existantes ### 7.5 Points Critiques - [x] Aucun point critique laissĂ© en flou - [x] Patterns transactionnels dĂ©finis (Go + Rust) - [x] RĂšgles d'invariants documentĂ©es - [x] Erreurs possibles identifiĂ©es - [x] Risques et points d'attention documentĂ©s --- ## 8. PROCHAINES ÉTAPES 1. ✅ **Phase 1 : Audit** — **COMPLÉTÉ** (`AUDIT_DB_TRANSACTIONS.md`) 2. ✅ **Phase 2 : Design** — **COMPLÉTÉ** (ce document) 3. ⏳ **Phase 3 : ImplĂ©mentation** — PrĂȘt Ă  commencer - Commencer par Phase 1 (P0 Backend Go) - Suivre l'ordre recommandĂ© (RBAC → Playlists → Social) 4. ⏳ **Phase 4 : Tests** — AprĂšs chaque phase d'implĂ©mentation 5. ⏳ **Phase 5 : Documentation** — Mettre Ă  jour `TRIAGE.md` et `AUDIT_STABILITY.md` --- **Date de crĂ©ation** : 2025-01-27 **DerniĂšre mise Ă  jour** : 2025-01-27 **Statut** : ✅ Design complet — PrĂȘt pour implĂ©mentation **RĂ©fĂ©rences** : - `docs/AUDIT_DB_TRANSACTIONS.md` — Audit dĂ©taillĂ© des opĂ©rations - `docs/AUDIT_STABILITY.md` — Audit de stabilitĂ© global - `TRIAGE.md` — État fonctionnel des features