1401 lines
47 KiB
Markdown
1401 lines
47 KiB
Markdown
|
|
# 🎯 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<Output, AppError> {
|
||
|
|
// 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
|
||
|
|
|