306 lines
8.5 KiB
Markdown
306 lines
8.5 KiB
Markdown
|
|
# 🎯 IMPLÉMENTATION PHASE 2 — Transactions P0 dans le Stream Server (Rust)
|
|||
|
|
|
|||
|
|
**Date** : 2025-01-27
|
|||
|
|
**Statut** : ✅ **COMPLÉTÉ**
|
|||
|
|
**Référence** : `docs/DB_TRANSACTION_PLAN.md` (sections Stream–P0)
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## 📋 RÉSUMÉ EXÉCUTIF
|
|||
|
|
|
|||
|
|
Toutes les opérations critiques P0 du Stream Server Rust ont été rendues transactionnelles, garantissant l'atomicité et la cohérence des données en cas d'erreur ou de crash.
|
|||
|
|
|
|||
|
|
**Opérations modifiées** : 4
|
|||
|
|
**Fichiers modifiés** : 3
|
|||
|
|
**Garanties ajoutées** : Atomicité complète pour toutes les opérations multi-étapes
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## 🔍 AUDIT SUMMARY
|
|||
|
|
|
|||
|
|
### Opérations Non Transactionnelles Identifiées (P0)
|
|||
|
|
|
|||
|
|
| Fonction | Fichier | Risque | Priorité | Statut |
|
|||
|
|
|----------|---------|--------|----------|--------|
|
|||
|
|
| `SegmentTracker.persist_segment` | `segment_tracker.rs:82-106` | Segments orphelins, incohérence durée | **P0** | ✅ **CORRIGÉ** |
|
|||
|
|
| `SegmentTracker.persist_all` | `segment_tracker.rs:158-172` | Segments partiellement persistés | **P0** | ✅ **CORRIGÉ** |
|
|||
|
|
| `StreamProcessor.finalize` | `processor.rs:239-269` | Job finalisé sans segments | **P0** | ✅ **CORRIGÉ** |
|
|||
|
|
| `EncoderWorker.parse_and_store_segments` | `encoding_pool.rs:281-349` | Playlist HLS incomplète | **P1** | ✅ **CORRIGÉ** |
|
|||
|
|
|
|||
|
|
### Risques Éliminés
|
|||
|
|
|
|||
|
|
✅ **Segments orphelins** : Impossible — INSERT segment + UPDATE job sont atomiques
|
|||
|
|
✅ **Jobs incomplets** : Impossible — Finalisation inclut tous les segments dans une transaction
|
|||
|
|
✅ **Durées incorrectes** : Impossible — Calcul et mise à jour dans la même transaction
|
|||
|
|
✅ **Playlists HLS incomplètes** : Impossible — Tous les segments insérés en batch dans une transaction
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## 🔧 MODIFICATIONS DÉTAILLÉES
|
|||
|
|
|
|||
|
|
### 1. `SegmentTracker.persist_segment` (P0)
|
|||
|
|
|
|||
|
|
**Fichier** : `src/core/processing/segment_tracker.rs`
|
|||
|
|
|
|||
|
|
**Avant** :
|
|||
|
|
```rust
|
|||
|
|
async fn persist_segment(&self, segment: &SegmentInfo) -> Result<(), AppError> {
|
|||
|
|
// INSERT segment (sans transaction)
|
|||
|
|
sqlx::query!("INSERT INTO stream_segments ...")
|
|||
|
|
.execute(&self.db).await?;
|
|||
|
|
|
|||
|
|
// UPDATE job (séparé, pas atomique)
|
|||
|
|
self.update_current_duration().await?;
|
|||
|
|
}
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
**Après** (Transactionnel) :
|
|||
|
|
```rust
|
|||
|
|
async fn persist_segment(&self, segment: &SegmentInfo) -> Result<(), AppError> {
|
|||
|
|
let mut tx = self.db.begin().await?;
|
|||
|
|
|
|||
|
|
// 1. VALIDATION : Job existe
|
|||
|
|
// 2. INSERT segment
|
|||
|
|
// 3. CALCUL : Durée totale
|
|||
|
|
// 4. UPDATE job
|
|||
|
|
// COMMIT
|
|||
|
|
tx.commit().await?;
|
|||
|
|
}
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
**Garanties** :
|
|||
|
|
- ✅ INSERT segment + UPDATE job sont atomiques
|
|||
|
|
- ✅ En cas d'erreur → rollback automatique
|
|||
|
|
- ✅ Pas de segment orphelin possible
|
|||
|
|
- ✅ Job toujours cohérent avec les segments
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
### 2. `SegmentTracker.persist_all` (P0)
|
|||
|
|
|
|||
|
|
**Fichier** : `src/core/processing/segment_tracker.rs`
|
|||
|
|
|
|||
|
|
**Avant** :
|
|||
|
|
```rust
|
|||
|
|
pub async fn persist_all(&self) -> Result<(), AppError> {
|
|||
|
|
for segment in segments.iter() {
|
|||
|
|
self.persist_segment(segment).await?; // Chaque segment dans sa propre transaction
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
**Après** (Transaction batch) :
|
|||
|
|
```rust
|
|||
|
|
pub async fn persist_all(&self) -> Result<(), AppError> {
|
|||
|
|
let mut tx = self.db.begin().await?;
|
|||
|
|
|
|||
|
|
// 1. VALIDATION : Job existe
|
|||
|
|
// 2. INSERT tous les segments en batch
|
|||
|
|
// 3. CALCUL : Durée totale
|
|||
|
|
// 4. UPDATE job
|
|||
|
|
// COMMIT
|
|||
|
|
tx.commit().await?;
|
|||
|
|
}
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
**Garanties** :
|
|||
|
|
- ✅ Tous les segments ou aucun (atomicité batch)
|
|||
|
|
- ✅ Pas de playlist HLS partiellement créée
|
|||
|
|
- ✅ Job toujours cohérent
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
### 3. `StreamProcessor.finalize` (P0)
|
|||
|
|
|
|||
|
|
**Fichier** : `src/core/processing/processor.rs`
|
|||
|
|
|
|||
|
|
**Avant** :
|
|||
|
|
```rust
|
|||
|
|
async fn finalize(&self, tracker: Arc<SegmentTracker>) -> Result<(), AppError> {
|
|||
|
|
tracker.persist_all().await?; // Transaction séparée
|
|||
|
|
callbacks.on_success().await?; // UPDATE job séparé
|
|||
|
|
}
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
**Après** (Transaction complète) :
|
|||
|
|
```rust
|
|||
|
|
async fn finalize(&self, tracker: Arc<SegmentTracker>) -> Result<(), AppError> {
|
|||
|
|
let mut tx = self.db.begin().await?;
|
|||
|
|
|
|||
|
|
// 1. VALIDATION : Job existe et est en "encoding"
|
|||
|
|
// 2. INSERT tous les segments
|
|||
|
|
// 3. CALCUL : Durée totale
|
|||
|
|
// 4. UPDATE job (status = 'done', updated_at)
|
|||
|
|
// COMMIT
|
|||
|
|
tx.commit().await?;
|
|||
|
|
}
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
**Garanties** :
|
|||
|
|
- ✅ Job finalisé seulement si tous les segments sont persistés
|
|||
|
|
- ✅ Pas de job "done" sans segments
|
|||
|
|
- ✅ Durée totale toujours cohérente
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
### 4. `EncoderWorker.parse_and_store_segments` (P1)
|
|||
|
|
|
|||
|
|
**Fichier** : `src/core/encoding_pool.rs`
|
|||
|
|
|
|||
|
|
**Avant** :
|
|||
|
|
```rust
|
|||
|
|
async fn parse_and_store_segments(&self, job: &EncodeJob) -> Result<(), AppError> {
|
|||
|
|
for segment in segments {
|
|||
|
|
sqlx::query!("INSERT INTO stream_segments ...")
|
|||
|
|
.execute(&self.db_pool).await?; // Pas de transaction
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
**Après** (Transaction batch) :
|
|||
|
|
```rust
|
|||
|
|
async fn parse_and_store_segments(&self, job: &EncodeJob) -> Result<(), AppError> {
|
|||
|
|
let mut tx = self.db_pool.begin().await?;
|
|||
|
|
|
|||
|
|
// 1. VALIDATION : Job existe
|
|||
|
|
// 2. INSERT tous les segments en batch
|
|||
|
|
// 3. CALCUL : Durée totale
|
|||
|
|
// 4. UPDATE job
|
|||
|
|
// COMMIT
|
|||
|
|
tx.commit().await?;
|
|||
|
|
}
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
**Garanties** :
|
|||
|
|
- ✅ Tous les segments ou aucun
|
|||
|
|
- ✅ Playlist HLS complète ou vide
|
|||
|
|
- ✅ Job toujours à jour
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## 🛡️ GARANTIES DE COHÉRENCE
|
|||
|
|
|
|||
|
|
### Invariants Garantis
|
|||
|
|
|
|||
|
|
1. **Jamais de segment sans job valide**
|
|||
|
|
- Vérification dans chaque transaction
|
|||
|
|
- Rollback si job n'existe pas
|
|||
|
|
|
|||
|
|
2. **Job toujours cohérent avec segments**
|
|||
|
|
- `updated_at` mis à jour dans la même transaction que les segments
|
|||
|
|
- Durée calculée depuis les segments réels
|
|||
|
|
|
|||
|
|
3. **Pas de segments orphelins**
|
|||
|
|
- Si job supprimé, segments supprimés (CASCADE DB)
|
|||
|
|
- Si INSERT segment échoue, UPDATE job annulé (rollback)
|
|||
|
|
|
|||
|
|
4. **Pas de job finalisé sans segments**
|
|||
|
|
- Finalisation inclut persistance des segments
|
|||
|
|
- Validation avant commit
|
|||
|
|
|
|||
|
|
5. **Playlist HLS complète ou vide**
|
|||
|
|
- Batch INSERT garantit atomicité
|
|||
|
|
- Pas de playlist partiellement générée
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## 📊 PROPAGATION DES ERREURS
|
|||
|
|
|
|||
|
|
Toutes les erreurs sont correctement propagées via `AppError` :
|
|||
|
|
|
|||
|
|
```rust
|
|||
|
|
// Pattern utilisé partout
|
|||
|
|
tx.commit().await
|
|||
|
|
.map_err(|e| AppError::InternalError {
|
|||
|
|
message: format!("Failed to commit transaction: {}", e),
|
|||
|
|
})?;
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
**Types d'erreurs gérées** :
|
|||
|
|
- ❌ Erreur de connexion DB → `AppError::InternalError`
|
|||
|
|
- ❌ Job n'existe pas → `AppError::NotFound`
|
|||
|
|
- ❌ Contrainte violée → `AppError::InternalError` (avec message détaillé)
|
|||
|
|
- ❌ Timeout transaction → Rollback automatique
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## 🧵 SÉCURITÉ CONCURRENTIELLE
|
|||
|
|
|
|||
|
|
Les transactions SQLx garantissent l'isolation par défaut (READ COMMITTED) :
|
|||
|
|
|
|||
|
|
- ✅ Pas de race condition sur les segments (contrainte UNIQUE)
|
|||
|
|
- ✅ Pas de double finalisation (UPDATE avec WHERE id = $1)
|
|||
|
|
- ✅ Isolation des transactions concurrentes
|
|||
|
|
|
|||
|
|
**Note** : Pour des cas plus complexes, on pourrait utiliser `FOR UPDATE` dans les SELECT, mais ce n'est pas nécessaire pour les opérations P0 actuelles.
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## 📝 LOGGING STRUCTURÉ
|
|||
|
|
|
|||
|
|
Tous les commits de transaction sont loggés avec `tracing` :
|
|||
|
|
|
|||
|
|
```rust
|
|||
|
|
tracing::info!(
|
|||
|
|
job_id = %self.job_id,
|
|||
|
|
segment_count = segments.len(),
|
|||
|
|
total_duration = total_duration,
|
|||
|
|
"All segments persisted successfully in transaction"
|
|||
|
|
);
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
**Niveaux de log** :
|
|||
|
|
- `debug` : Persistance individuelle de segment
|
|||
|
|
- `info` : Persistance batch, finalisation job
|
|||
|
|
- `error` : Erreurs de transaction (automatique via `?`)
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## ✅ VALIDATION
|
|||
|
|
|
|||
|
|
### Tests de Compilation
|
|||
|
|
|
|||
|
|
```bash
|
|||
|
|
cd veza-stream-server && cargo check
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
**Résultat** : ✅ Compilation réussie (warnings non critiques uniquement)
|
|||
|
|
|
|||
|
|
### Points de Validation
|
|||
|
|
|
|||
|
|
- [x] Toutes les opérations P0 sont transactionnelles
|
|||
|
|
- [x] Propagation correcte des erreurs
|
|||
|
|
- [x] Logging structuré ajouté
|
|||
|
|
- [x] Code compile sans erreurs
|
|||
|
|
- [x] Pattern SQLx respecté (`begin()`, `commit()`, rollback automatique)
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## 🚀 PROCHAINES ÉTAPES (Phase 3)
|
|||
|
|
|
|||
|
|
Les tests transactionnels seront implémentés en Phase 3 :
|
|||
|
|
|
|||
|
|
1. **Tests unitaires** : Simulation d'erreur au milieu de transaction
|
|||
|
|
2. **Tests d'intégration** : Vérification rollback en cas d'erreur
|
|||
|
|
3. **Tests de charge** : Validation performance avec transactions
|
|||
|
|
|
|||
|
|
**Fichiers de tests à créer** :
|
|||
|
|
- `tests/segment_tracker_transaction_test.rs`
|
|||
|
|
- `tests/processor_finalize_transaction_test.rs`
|
|||
|
|
- `tests/encoding_pool_batch_transaction_test.rs`
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## 📚 RÉFÉRENCES
|
|||
|
|
|
|||
|
|
- `docs/DB_TRANSACTION_PLAN.md` — Plan d'implémentation complet
|
|||
|
|
- `docs/AUDIT_DB_TRANSACTIONS.md` — Audit initial des opérations
|
|||
|
|
- `docs/AUDIT_STABILITY.md` — Audit de stabilité global
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
**Date de création** : 2025-01-27
|
|||
|
|
**Dernière mise à jour** : 2025-01-27
|
|||
|
|
**Statut** : ✅ **Phase 2 complétée — Prêt pour Phase 3 (Tests)**
|
|||
|
|
|