Backend Go: - Remplacement complet des anciennes migrations par la base V1 alignée sur ORIGIN. - Durcissement global du parsing JSON (BindAndValidateJSON + RespondWithAppError). - Sécurisation de config.go, CORS, statuts de santé et monitoring. - Implémentation des transactions P0 (RBAC, duplication de playlists, social toggles). - Ajout d’un job worker structuré (emails, analytics, thumbnails) + tests associés. - Nouvelle doc backend : AUDIT_CONFIG, BACKEND_CONFIG, AUTH_PASSWORD_RESET, JOB_WORKER_*. Chat server (Rust): - Refonte du pipeline JWT + sécurité, audit et rate limiting avancé. - Implémentation complète du cycle de message (read receipts, delivered, edit/delete, typing). - Nettoyage des panics, gestion d’erreurs robuste, logs structurés. - Migrations chat alignées sur le schéma UUID et nouvelles features. Stream server (Rust): - Refonte du moteur de streaming (encoding pipeline + HLS) et des modules core. - Transactions P0 pour les jobs et segments, garanties d’atomicité. - Documentation détaillée de la pipeline (AUDIT_STREAM_*, DESIGN_STREAM_PIPELINE, TRANSACTIONS_P0_IMPLEMENTATION). Documentation & audits: - TRIAGE.md et AUDIT_STABILITY.md à jour avec l’état réel des 3 services. - Cartographie complète des migrations et des transactions (DB_MIGRATIONS_*, DB_TRANSACTION_PLAN, AUDIT_DB_TRANSACTIONS, TRANSACTION_TESTS_PHASE3). - Scripts de reset et de cleanup pour la lab DB et la V1. Ce commit fige l’ensemble du travail de stabilisation P0 (UUID, backend, chat et stream) avant les phases suivantes (Coherence Guardian, WS hardening, etc.).
1400 lines
47 KiB
Markdown
1400 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
|
|
|