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.).
617 lines
19 KiB
Markdown
617 lines
19 KiB
Markdown
# 🔍 AUDIT DES TRANSACTIONS DB — PROJET VEZA
|
|
|
|
**Date** : 2025-01-27
|
|
**Objectif** : Identifier toutes les opérations multi-étapes non transactionnelles qui peuvent laisser la DB dans un état incohérent
|
|
**Phase** : Hardening — Élimination des risques d'incohérence de données
|
|
|
|
---
|
|
|
|
## 📋 TABLE DES MATIÈRES
|
|
|
|
1. [Résumé Exécutif](#1-résumé-exécutif)
|
|
2. [Backend Go](#2-backend-go)
|
|
3. [Stream Server (Rust)](#3-stream-server-rust)
|
|
4. [Chat Server (Rust)](#4-chat-server-rust)
|
|
5. [Table Récapitulative](#5-table-récapitulative)
|
|
6. [Liste P0 Prioritaire](#6-liste-p0-prioritaire)
|
|
|
|
---
|
|
|
|
## 1. RÉSUMÉ EXÉCUTIF
|
|
|
|
### Statistiques Globales
|
|
|
|
- **Total opérations multi-étapes identifiées** : 18
|
|
- **Opérations transactionnelles** : 8 (44%)
|
|
- **Opérations non transactionnelles** : 10 (56%)
|
|
- **P0 (Critique)** : 5 opérations
|
|
- **P1 (Important)** : 5 opérations
|
|
|
|
### Risques Principaux
|
|
|
|
1. **Marketplace** : Commandes partiellement créées (items sans order, licenses sans order)
|
|
2. **Playlists** : Duplication incomplète, collaborateurs sans playlist
|
|
3. **Social** : Compteurs de likes/comments désynchronisés
|
|
4. **Stream** : Segments orphelins sans job, jobs sans segments
|
|
5. **RBAC** : Assignations de rôles partiellement appliquées
|
|
|
|
---
|
|
|
|
## 2. BACKEND GO
|
|
|
|
### 2.1 Marketplace Service
|
|
|
|
#### ✅ **CreateOrder** — **TRANSACTIONNEL**
|
|
|
|
**Localisation** : `internal/core/marketplace/service.go:136-215`
|
|
|
|
**Flow actuel** :
|
|
```go
|
|
s.db.Transaction(func(tx *gorm.DB) error {
|
|
1. Valider produits + calculer total
|
|
2. CREATE order (PENDING)
|
|
3. UPDATE order (COMPLETED) + PaymentIntent
|
|
4. CREATE order_items (pour chaque produit)
|
|
5. CREATE licenses (pour chaque track)
|
|
})
|
|
```
|
|
|
|
**État** : ✅ **Transactionnel** — Toutes les écritures sont dans une transaction GORM
|
|
|
|
**Risques** : Aucun — En cas d'erreur, rollback complet
|
|
|
|
---
|
|
|
|
#### ✅ **CreateProduct** — **TRANSACTIONNEL**
|
|
|
|
**Localisation** : `internal/core/marketplace/service.go:69-99`
|
|
|
|
**Flow actuel** :
|
|
```go
|
|
s.db.Transaction(func(tx *gorm.DB) error {
|
|
1. Valider track existence + ownership
|
|
2. CREATE product
|
|
})
|
|
```
|
|
|
|
**État** : ✅ **Transactionnel** — Validation + création dans une transaction
|
|
|
|
**Risques** : Aucun
|
|
|
|
---
|
|
|
|
### 2.2 Playlist Services
|
|
|
|
#### ✅ **AddTrack** — **TRANSACTIONNEL**
|
|
|
|
**Localisation** : `internal/repositories/playlist_track_repository.go:41-124`
|
|
|
|
**Flow actuel** :
|
|
```go
|
|
r.db.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
|
|
1. CREATE playlist_track
|
|
2. UPDATE playlists.track_count (+1)
|
|
})
|
|
```
|
|
|
|
**État** : ✅ **Transactionnel** — Création + mise à jour du compteur dans une transaction
|
|
|
|
**Risques** : Aucun
|
|
|
|
---
|
|
|
|
#### ✅ **RemoveTrack** — **TRANSACTIONNEL**
|
|
|
|
**Localisation** : `internal/repositories/playlist_track_repository.go:127-162`
|
|
|
|
**Flow actuel** :
|
|
```go
|
|
r.db.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
|
|
1. DELETE playlist_track
|
|
2. UPDATE playlist_tracks.position (décalage)
|
|
3. UPDATE playlists.track_count (-1)
|
|
})
|
|
```
|
|
|
|
**État** : ✅ **Transactionnel** — Suppression + décalage positions + compteur dans une transaction
|
|
|
|
**Risques** : Aucun
|
|
|
|
---
|
|
|
|
#### ✅ **ReorderTracks** — **TRANSACTIONNEL**
|
|
|
|
**Localisation** : `internal/repositories/playlist_track_repository.go:165-198`
|
|
|
|
**Flow actuel** :
|
|
```go
|
|
r.db.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
|
|
1. UPDATE playlist_tracks.position (pour chaque track)
|
|
})
|
|
```
|
|
|
|
**État** : ✅ **Transactionnel** — Toutes les mises à jour de positions dans une transaction
|
|
|
|
**Risques** : Aucun
|
|
|
|
---
|
|
|
|
#### ❌ **DuplicatePlaylist** — **NON TRANSACTIONNEL** — **P0**
|
|
|
|
**Localisation** : `internal/services/playlist_duplicate_service.go:41-131`
|
|
|
|
**Flow actuel** :
|
|
```go
|
|
1. GET original playlist
|
|
2. CREATE new playlist (via CreatePlaylist)
|
|
3. FOR each track:
|
|
4. AddTrackToPlaylist (chaque appel est transactionnel, mais pas l'ensemble)
|
|
```
|
|
|
|
**État** : ❌ **NON Transactionnel** — La duplication complète n'est pas dans une transaction
|
|
|
|
**Risques concrets** :
|
|
- Si crash après création de la playlist mais avant fin de l'ajout des tracks → **Playlist vide créée**
|
|
- Si crash au milieu de l'ajout des tracks → **Playlist partiellement dupliquée** (certains tracks manquants)
|
|
- Si `AddTrackToPlaylist` échoue pour un track, on continue avec les autres (ligne 117) → **Playlist incomplète**
|
|
|
|
**Impact métier** : **ÉLEVÉ** — Playlists dupliquées incomplètes, confusion utilisateur
|
|
|
|
**Recommandation** : Wrapper toute la duplication dans une transaction :
|
|
```go
|
|
return s.playlistService.db.Transaction(func(tx *gorm.DB) error {
|
|
// Créer playlist
|
|
// Ajouter tous les tracks
|
|
// Si erreur → rollback complet
|
|
})
|
|
```
|
|
|
|
---
|
|
|
|
#### ❌ **AddCollaborator** — **NON TRANSACTIONNEL** — **P1**
|
|
|
|
**Localisation** : `internal/services/playlist_service.go:611-665`
|
|
|
|
**Flow actuel** :
|
|
```go
|
|
1. GET playlist (vérification ownership)
|
|
2. GET user (vérification existence)
|
|
3. CREATE playlist_collaborator (via repository)
|
|
```
|
|
|
|
**État** : ❌ **NON Transactionnel** — Vérifications + création séparées
|
|
|
|
**Risques concrets** :
|
|
- Si crash entre vérification et création → **Pas de collaborateur créé** (acceptable, mais incohérent si d'autres opérations dépendent)
|
|
- Si playlist supprimée entre vérification et création → **Collaborateur créé pour playlist inexistante** (contrainte FK devrait bloquer, mais pas garanti)
|
|
|
|
**Impact métier** : **MOYEN** — Risque faible mais possible
|
|
|
|
**Recommandation** : Wrapper dans une transaction si on veut garantir l'atomicité des vérifications + création
|
|
|
|
---
|
|
|
|
### 2.3 Social Services
|
|
|
|
#### ❌ **ToggleLike** — **NON TRANSACTIONNEL** — **P1**
|
|
|
|
**Localisation** : `internal/core/social/service.go:131-167`
|
|
|
|
**Flow actuel** :
|
|
```go
|
|
// Cas 1: Unlike
|
|
1. DELETE like
|
|
2. UPDATE post.like_count (-1) // ⚠️ Pas dans la même transaction
|
|
|
|
// Cas 2: Like
|
|
1. CREATE like
|
|
2. UPDATE post.like_count (+1) // ⚠️ Pas dans la même transaction
|
|
```
|
|
|
|
**État** : ❌ **NON Transactionnel** — Create/Delete + Update compteur séparés
|
|
|
|
**Risques concrets** :
|
|
- Si crash après DELETE like mais avant UPDATE compteur → **Like supprimé mais compteur non décrémenté** → **Compteur désynchronisé**
|
|
- Si crash après CREATE like mais avant UPDATE compteur → **Like créé mais compteur non incrémenté** → **Compteur désynchronisé**
|
|
|
|
**Impact métier** : **MOYEN** — Compteurs désynchronisés, mais données principales (like) cohérentes
|
|
|
|
**Recommandation** : Wrapper dans une transaction :
|
|
```go
|
|
return s.db.Transaction(func(tx *gorm.DB) error {
|
|
// DELETE ou CREATE like
|
|
// UPDATE post.like_count
|
|
})
|
|
```
|
|
|
|
---
|
|
|
|
#### ❌ **AddComment** — **NON TRANSACTIONNEL** — **P1**
|
|
|
|
**Localisation** : `internal/core/social/service.go:169-188`
|
|
|
|
**Flow actuel** :
|
|
```go
|
|
1. CREATE comment
|
|
2. UPDATE post.comment_count (+1) // ⚠️ Pas dans la même transaction
|
|
```
|
|
|
|
**État** : ❌ **NON Transactionnel** — Création commentaire + mise à jour compteur séparés
|
|
|
|
**Risques concrets** :
|
|
- Si crash après CREATE comment mais avant UPDATE compteur → **Commentaire créé mais compteur non incrémenté** → **Compteur désynchronisé**
|
|
|
|
**Impact métier** : **MOYEN** — Compteurs désynchronisés, mais commentaire créé
|
|
|
|
**Recommandation** : Wrapper dans une transaction
|
|
|
|
---
|
|
|
|
### 2.4 RBAC Services
|
|
|
|
#### ❌ **AssignRoleToUser (RBACService)** — **NON TRANSACTIONNEL** — **P0**
|
|
|
|
**Localisation** : `internal/services/rbac_service.go:168-210`
|
|
|
|
**Flow actuel** :
|
|
```go
|
|
1. SELECT COUNT(*) FROM users WHERE id = $1 // Vérification existence
|
|
2. SELECT COUNT(*) FROM roles WHERE id = $1 // Vérification existence
|
|
3. SELECT COUNT(*) FROM user_roles WHERE ... // Vérification doublon
|
|
4. INSERT INTO user_roles ... // Assignation
|
|
```
|
|
|
|
**État** : ❌ **NON Transactionnel** — 4 queries séparées, pas de transaction
|
|
|
|
**Risques concrets** :
|
|
- Si crash entre vérifications et INSERT → **Pas d'assignation créée** (acceptable)
|
|
- Si user/role supprimé entre vérification et INSERT → **Assignation créée pour user/role inexistant** (contrainte FK devrait bloquer, mais pas garanti si suppression soft)
|
|
- Si race condition : 2 requêtes simultanées peuvent toutes deux passer les vérifications et créer 2 assignations → **Doublon** (contrainte UNIQUE devrait bloquer, mais erreur non gérée proprement)
|
|
|
|
**Impact métier** : **ÉLEVÉ** — Assignations de rôles incohérentes, sécurité compromise
|
|
|
|
**Recommandation** : Wrapper dans une transaction avec isolation level approprié :
|
|
```go
|
|
return s.db.Transaction(func(tx *gorm.DB) error {
|
|
// Vérifications + INSERT dans la même transaction
|
|
})
|
|
```
|
|
|
|
---
|
|
|
|
#### ❌ **AssignRoleToUser (RoleService)** — **NON TRANSACTIONNEL** — **P1**
|
|
|
|
**Localisation** : `internal/services/role_service.go:86-99`
|
|
|
|
**Flow actuel** :
|
|
```go
|
|
1. CREATE user_role
|
|
```
|
|
|
|
**État** : ❌ **NON Transactionnel** — Simple CREATE, mais devrait vérifier existence user/role avant
|
|
|
|
**Risques concrets** :
|
|
- Si user/role n'existe pas → **Erreur FK** (gérée par DB, mais pas de validation préalable)
|
|
- Pas de vérification de doublon avant création
|
|
|
|
**Impact métier** : **MOYEN** — Erreurs DB non gérées proprement
|
|
|
|
**Recommandation** : Ajouter vérifications + wrapper dans transaction
|
|
|
|
---
|
|
|
|
### 2.5 HLS Queue Service
|
|
|
|
#### ✅ **CreateJob** — **TRANSACTIONNEL**
|
|
|
|
**Localisation** : `internal/services/hls_queue_service.go:77`
|
|
|
|
**Flow actuel** :
|
|
```go
|
|
s.db.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
|
|
// Création job + initialisation
|
|
})
|
|
```
|
|
|
|
**État** : ✅ **Transactionnel**
|
|
|
|
**Risques** : Aucun
|
|
|
|
---
|
|
|
|
### 2.6 Refresh Token Service
|
|
|
|
#### ✅ **RotateToken** — **TRANSACTIONNEL**
|
|
|
|
**Localisation** : `internal/services/refresh_token_service.go:70`
|
|
|
|
**Flow actuel** :
|
|
```go
|
|
s.db.Transaction(func(tx *gorm.DB) error {
|
|
// Invalider ancien token + créer nouveau
|
|
})
|
|
```
|
|
|
|
**État** : ✅ **Transactionnel**
|
|
|
|
**Risques** : Aucun
|
|
|
|
---
|
|
|
|
## 3. STREAM SERVER (RUST)
|
|
|
|
### 3.1 Segment Tracker
|
|
|
|
#### ❌ **persist_segment** — **NON TRANSACTIONNEL** — **P0**
|
|
|
|
**Localisation** : `src/core/processing/segment_tracker.rs:82-106`
|
|
|
|
**Flow actuel** :
|
|
```rust
|
|
async fn persist_segment(&self, segment: &SegmentInfo) -> Result<(), AppError> {
|
|
1. INSERT INTO stream_segments (...) // Insert segment
|
|
2. self.update_current_duration().await?; // UPDATE stream_jobs.updated_at
|
|
}
|
|
```
|
|
|
|
**État** : ❌ **NON Transactionnel** — INSERT segment + UPDATE job séparés
|
|
|
|
**Risques concrets** :
|
|
- Si crash après INSERT segment mais avant UPDATE job → **Segment créé mais job non mis à jour** → **Segments orphelins**
|
|
- Si crash après UPDATE job mais avant INSERT segment → **Job mis à jour mais segment non créé** → **Incohérence durée**
|
|
|
|
**Impact métier** : **ÉLEVÉ** — Segments HLS orphelins, jobs avec métadonnées incorrectes, streaming cassé
|
|
|
|
**Recommandation** : Utiliser une transaction SQLx :
|
|
```rust
|
|
let mut tx = self.db.begin().await?;
|
|
sqlx::query!("INSERT INTO stream_segments ...").execute(&mut *tx).await?;
|
|
sqlx::query!("UPDATE stream_jobs ...").execute(&mut *tx).await?;
|
|
tx.commit().await?;
|
|
```
|
|
|
|
---
|
|
|
|
#### ❌ **EncodingPool (insert_segments_from_playlist)** — **NON TRANSACTIONNEL** — **P1**
|
|
|
|
**Localisation** : `src/core/encoding_pool.rs:300-349`
|
|
|
|
**Flow actuel** :
|
|
```rust
|
|
for line in lines {
|
|
if segment_path.exists() {
|
|
sqlx::query!("INSERT INTO stream_segments ...")
|
|
.execute(&self.db_pool) // ⚠️ Pas de transaction
|
|
.await?;
|
|
segment_index += 1;
|
|
}
|
|
}
|
|
```
|
|
|
|
**État** : ❌ **NON Transactionnel** — Insertions de segments multiples sans transaction
|
|
|
|
**Risques concrets** :
|
|
- Si crash au milieu de la boucle → **Segments partiellement insérés** → **Playlist HLS incomplète**
|
|
|
|
**Impact métier** : **MOYEN** — Playlist HLS partiellement générée
|
|
|
|
**Recommandation** : Wrapper toutes les insertions dans une transaction :
|
|
```rust
|
|
let mut tx = self.db_pool.begin().await?;
|
|
for segment in segments {
|
|
sqlx::query!("INSERT ...").execute(&mut *tx).await?;
|
|
}
|
|
tx.commit().await?;
|
|
```
|
|
|
|
---
|
|
|
|
### 3.2 Stream Jobs
|
|
|
|
#### ❌ **Job Creation + Segment Persistence** — **NON TRANSACTIONNEL** — **P0**
|
|
|
|
**Localisation** : `src/core/processing/processor.rs` + `segment_tracker.rs`
|
|
|
|
**Flow actuel** :
|
|
```rust
|
|
// Dans processor.rs
|
|
1. CREATE stream_job (status: processing)
|
|
2. Spawn FFmpeg
|
|
3. Segments détectés → persist_segment() (appelé plusieurs fois)
|
|
4. UPDATE stream_job (status: completed)
|
|
```
|
|
|
|
**État** : ❌ **NON Transactionnel** — Job créé, puis segments persistés individuellement, puis job mis à jour
|
|
|
|
**Risques concrets** :
|
|
- Si crash après création job mais avant segments → **Job créé sans segments** → **Job orphelin**
|
|
- Si crash pendant persistance segments → **Segments partiellement créés** → **Job incomplet**
|
|
- Si crash après segments mais avant UPDATE job → **Segments créés mais job non finalisé** → **Job bloqué en "processing"**
|
|
|
|
**Impact métier** : **ÉLEVÉ** — Jobs de transcodage incomplets, streaming cassé
|
|
|
|
**Recommandation** :
|
|
- **Option 1** : Persister segments en batch à la fin (déjà fait dans `persist_all()`, mais pas utilisé systématiquement)
|
|
- **Option 2** : Utiliser un pattern "two-phase" : job créé en "pending", segments persistés en batch, puis job finalisé en "completed" dans une transaction
|
|
|
|
---
|
|
|
|
## 4. CHAT SERVER (RUST)
|
|
|
|
### 4.1 Message Operations
|
|
|
|
#### ✅ **send_room_message** — **TRANSACTIONNEL**
|
|
|
|
**Localisation** : `src/hub/channels.rs:301-388`
|
|
|
|
**Flow actuel** :
|
|
```rust
|
|
let mut tx = hub.db.begin().await?;
|
|
1. Vérifier membership
|
|
2. INSERT INTO messages
|
|
3. UPDATE messages.thread_count (si parent)
|
|
4. process_mentions() (INSERT mentions)
|
|
tx.commit().await?;
|
|
```
|
|
|
|
**État** : ✅ **Transactionnel** — Toutes les écritures dans une transaction SQLx
|
|
|
|
**Risques** : Aucun
|
|
|
|
---
|
|
|
|
#### ✅ **send_dm_message** — **TRANSACTIONNEL**
|
|
|
|
**Localisation** : `src/hub/direct_messages.rs:278-336`
|
|
|
|
**Flow actuel** :
|
|
```rust
|
|
let mut tx = hub.db.begin().await?;
|
|
1. INSERT INTO messages
|
|
2. UPDATE messages.thread_count (si parent)
|
|
3. process_dm_mentions()
|
|
4. UPDATE dm_conversations.updated_at
|
|
tx.commit().await?;
|
|
```
|
|
|
|
**État** : ✅ **Transactionnel** — Toutes les écritures dans une transaction
|
|
|
|
**Risques** : Aucun
|
|
|
|
---
|
|
|
|
#### ✅ **process_batch (OptimizedPersistence)** — **TRANSACTIONNEL**
|
|
|
|
**Localisation** : `src/optimized_persistence.rs:663-699`
|
|
|
|
**Flow actuel** :
|
|
```rust
|
|
let mut tx = self.pg_pool.begin().await?;
|
|
for message in &messages {
|
|
sqlx::query("INSERT INTO messages ...").execute(&mut *tx).await?;
|
|
}
|
|
tx.commit().await?;
|
|
```
|
|
|
|
**État** : ✅ **Transactionnel** — Toutes les insertions en batch dans une transaction
|
|
|
|
**Risques** : Aucun
|
|
|
|
---
|
|
|
|
## 5. TABLE RÉCAPITULATIVE
|
|
|
|
| Service | Opération | État | Priorité | Risque en cas de crash |
|
|
|---------|-----------|------|----------|------------------------|
|
|
| **Backend Go** |
|
|
| Marketplace | `CreateOrder` | ✅ Transactionnel | - | Aucun |
|
|
| Marketplace | `CreateProduct` | ✅ Transactionnel | - | Aucun |
|
|
| Playlist | `AddTrack` | ✅ Transactionnel | - | Aucun |
|
|
| Playlist | `RemoveTrack` | ✅ Transactionnel | - | Aucun |
|
|
| Playlist | `ReorderTracks` | ✅ Transactionnel | - | Aucun |
|
|
| Playlist | `DuplicatePlaylist` | ❌ **NON** | **P0** | Playlist vide ou incomplète |
|
|
| Playlist | `AddCollaborator` | ❌ **NON** | P1 | Collaborateur sans playlist |
|
|
| Social | `ToggleLike` | ❌ **NON** | P1 | Compteur désynchronisé |
|
|
| Social | `AddComment` | ❌ **NON** | P1 | Compteur désynchronisé |
|
|
| RBAC | `AssignRoleToUser` (RBACService) | ❌ **NON** | **P0** | Assignation incohérente |
|
|
| RBAC | `AssignRoleToUser` (RoleService) | ❌ **NON** | P1 | Erreurs non gérées |
|
|
| HLS | `CreateJob` | ✅ Transactionnel | - | Aucun |
|
|
| Auth | `RotateToken` | ✅ Transactionnel | - | Aucun |
|
|
| **Stream Server** |
|
|
| SegmentTracker | `persist_segment` | ❌ **NON** | **P0** | Segments orphelins |
|
|
| EncodingPool | `insert_segments_from_playlist` | ❌ **NON** | P1 | Playlist HLS incomplète |
|
|
| Processor | Job + Segments | ❌ **NON** | **P0** | Jobs incomplets |
|
|
| **Chat Server** |
|
|
| Channels | `send_room_message` | ✅ Transactionnel | - | Aucun |
|
|
| DirectMessages | `send_dm_message` | ✅ Transactionnel | - | Aucun |
|
|
| Persistence | `process_batch` | ✅ Transactionnel | - | Aucun |
|
|
|
|
---
|
|
|
|
## 6. LISTE P0 PRIORITAIRE
|
|
|
|
### 🔴 P0 — Must-Fix avant déploiement
|
|
|
|
1. **`PlaylistDuplicateService.DuplicatePlaylist`** (Backend Go)
|
|
- **Risque** : Playlists dupliquées incomplètes
|
|
- **Impact** : Confusion utilisateur, données corrompues
|
|
- **Fix** : Wrapper création playlist + ajout tracks dans une transaction
|
|
|
|
2. **`RBACService.AssignRoleToUser`** (Backend Go)
|
|
- **Risque** : Assignations de rôles incohérentes, sécurité compromise
|
|
- **Impact** : Permissions incorrectes, accès non autorisés
|
|
- **Fix** : Wrapper toutes les vérifications + INSERT dans une transaction
|
|
|
|
3. **`SegmentTracker.persist_segment`** (Stream Server)
|
|
- **Risque** : Segments HLS orphelins, jobs avec métadonnées incorrectes
|
|
- **Impact** : Streaming cassé, playlists HLS incomplètes
|
|
- **Fix** : Utiliser transaction SQLx pour INSERT segment + UPDATE job
|
|
|
|
4. **`StreamProcessor` (Job + Segments)** (Stream Server)
|
|
- **Risque** : Jobs de transcodage incomplets, segments partiellement créés
|
|
- **Impact** : Streaming cassé, jobs bloqués
|
|
- **Fix** : Pattern "two-phase" ou persistance batch à la fin
|
|
|
|
5. **`SocialService.ToggleLike` / `AddComment`** (Backend Go) — **P0 si compteurs critiques**
|
|
- **Risque** : Compteurs désynchronisés
|
|
- **Impact** : Métriques incorrectes (si critiques pour business)
|
|
- **Fix** : Wrapper dans transaction
|
|
|
|
---
|
|
|
|
## 7. RECOMMANDATIONS GÉNÉRALES
|
|
|
|
### Pattern Transactionnel Standard (Backend Go)
|
|
|
|
Créer un helper dans `internal/database/` ou utiliser directement GORM :
|
|
|
|
```go
|
|
// Pattern recommandé
|
|
func (s *Service) OperationMultiSteps(ctx context.Context, ...) error {
|
|
return s.db.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
|
|
// 1. Validations
|
|
// 2. Écritures multiples
|
|
// 3. Retour erreur si problème → rollback automatique
|
|
return nil
|
|
})
|
|
}
|
|
```
|
|
|
|
### Pattern Transactionnel Standard (Rust - SQLx)
|
|
|
|
```rust
|
|
// Pattern recommandé
|
|
async fn operation_multi_steps(&self, ...) -> Result<()> {
|
|
let mut tx = self.db.begin().await?;
|
|
|
|
sqlx::query!("INSERT ...").execute(&mut *tx).await?;
|
|
sqlx::query!("UPDATE ...").execute(&mut *tx).await?;
|
|
|
|
tx.commit().await?;
|
|
Ok(())
|
|
}
|
|
```
|
|
|
|
### Règles de Gestion d'Erreur
|
|
|
|
1. **Toute erreur dans la transaction → rollback automatique**
|
|
2. **Wrapper des erreurs avec contexte** : `fmt.Errorf("OperationName: %w", err)`
|
|
3. **Pas d'écritures "post-transaction"** qui pourraient réintroduire de l'incohérence
|
|
4. **Logs structurés au niveau transaction**, pas dans chaque sous-étape
|
|
|
|
---
|
|
|
|
## 8. PROCHAINES ÉTAPES
|
|
|
|
1. ✅ **Phase 1 : Audit** — **COMPLÉTÉ** (ce document)
|
|
2. ⏳ **Phase 2 : Design** — Créer `docs/DB_TRANSACTION_PLAN.md` avec plan d'implémentation
|
|
3. ⏳ **Phase 3 : Implémentation** — Corriger les P0 identifiés
|
|
4. ⏳ **Phase 4 : Tests** — Tests ciblés pour vérifier rollback en cas d'erreur
|
|
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** : ✅ Audit complet — En attente feu vert pour Phase 2 (Design)
|
|
|