618 lines
19 KiB
Markdown
618 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)
|
||
|
|
|