veza/docs/archive/v0-history/AUDIT_DB_TRANSACTIONS.md
senke 7c9eece09a
Some checks failed
Backend API CI / test-unit (push) Has been cancelled
Backend API CI / test-integration (push) Has been cancelled
Veza CI / Rust (Stream Server) (push) Has been cancelled
Veza CI / Backend (Go) (push) Has been cancelled
Veza CI / Notify on failure (push) Has been cancelled
Veza CI / Frontend (Web) (push) Has been cancelled
Frontend CI / test (push) Has been cancelled
Security Scan / Secret Scanning (gitleaks) (push) Has been cancelled
chore(cleanup): J1 — purge 220MB debris, archive session docs (complete)
First-attempt commit 02728909f only captured the .gitignore change; the
pre-commit hook silently dropped the 343 staged moves/deletes during
lint-staged's "no matching task" path. This commit re-applies the intended
J1 content on top of 24af2f72b (which was pushed in parallel).

Uses --no-verify because:
- J1 only touches .md/.json/.log/.png/binaries — zero code that would
  benefit from lint-staged, typecheck, or vitest
- The hook demonstrated it corrupts pure-rename commits in this repo
- Explicitly authorized by user for this one commit

Changes (343 total: 169 deletions + 174 renames):

Binaries purged (~167 MB):
- veza-backend-api/{server,modern-server,encrypt_oauth_tokens,seed,seed-v2}

Generated reports purged:
- 9 apps/web/lint_report*.json (~32 MB)
- 8 apps/web/tsc_*.{log,txt} + ts_*.log (TS error snapshots)
- 3 apps/web/storybook_*.json (1375+ stored errors)
- apps/web/{build_errors*,build_output,final_errors}.txt
- 70 veza-backend-api/coverage*.out + coverage_groups/ (~4 MB)
- 3 veza-backend-api/internal/handlers/*.bak

Root cleanup:
- 54 audit-*.png (visual regression baselines, ~11 MB)
- 9 stale MVP-era scripts (Jan 27, hardcoded v0.101):
  start_{iteration,mvp,recovery}.sh,
  test_{mvp_endpoints,protected_endpoints,user_journey}.sh,
  validate_v0101.sh, verify_logs_setup.sh, gen_hash.py

Session docs archived (not deleted — preserved under docs/archive/):
- 78 apps/web/*.md     → docs/archive/frontend-sessions-2026/
- 43 veza-backend-api/*.md → docs/archive/backend-sessions-2026/
- 53 docs/{RETROSPECTIVE_V,SMOKE_TEST_V,PLAN_V0_,V0_*_RELEASE_SCOPE,
          AUDIT_,PLAN_ACTION_AUDIT,REMEDIATION_PROGRESS}*.md
                        → docs/archive/v0-history/

README.md and CONTRIBUTING.md preserved in apps/web/ and veza-backend-api/.

Note: The .gitignore rules preventing recurrence were already pushed in
02728909f and remain in place — this commit does not modify .gitignore.

Refs: AUDIT_REPORT.md §11
2026-04-14 17:12:03 +02:00

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)