First-attempt commit3a5c6e184only 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 ofbec75f143(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 in3a5c6e184and remain in place — this commit does not modify .gitignore. Refs: AUDIT_REPORT.md §11
19 KiB
🔍 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
- Résumé Exécutif
- Backend Go
- Stream Server (Rust)
- Chat Server (Rust)
- Table Récapitulative
- 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
- Marketplace : Commandes partiellement créées (items sans order, licenses sans order)
- Playlists : Duplication incomplète, collaborateurs sans playlist
- Social : Compteurs de likes/comments désynchronisés
- Stream : Segments orphelins sans job, jobs sans segments
- 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 :
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 :
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 :
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 :
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 :
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 :
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 :
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 :
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 :
// 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 :
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 :
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 :
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é :
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 :
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 :
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 :
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 :
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 :
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 :
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 :
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 :
// 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 :
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 :
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 :
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
-
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
-
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
-
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
-
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
-
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 :
// 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)
// 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
- Toute erreur dans la transaction → rollback automatique
- Wrapper des erreurs avec contexte :
fmt.Errorf("OperationName: %w", err) - Pas d'écritures "post-transaction" qui pourraient réintroduire de l'incohérence
- Logs structurés au niveau transaction, pas dans chaque sous-étape
8. PROCHAINES ÉTAPES
- ✅ Phase 1 : Audit — COMPLÉTÉ (ce document)
- ⏳ Phase 2 : Design — Créer
docs/DB_TRANSACTION_PLAN.mdavec plan d'implémentation - ⏳ Phase 3 : Implémentation — Corriger les P0 identifiés
- ⏳ Phase 4 : Tests — Tests ciblés pour vérifier rollback en cas d'erreur
- ⏳ Phase 5 : Documentation — Mettre à jour
TRIAGE.mdetAUDIT_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)