Repository navigation
fix(db): KF-002-H-002 deadlock-retry helper + apply to finalize (closes #43) - #65
Merged
Merged
Conversation
#43) Adresse le risque user-visible 500 sur deadlock cross-table documenté dans Pattern 5. Avant ce fix, MariaDB ne détecte pas les deadlocks multi-table avant `innodb_lock_wait_timeout` (50s) → 500 brut. ## Changements ### `crates/kesh-db/src/retry.rs` (nouveau, 378 lignes) Helper de retry centralisé : - `retry_on_deadlock` (ergo wrapper, max 3 tentatives par défaut) - `retry_on_deadlock_with(max_attempts, ...)` (paramétrable) - `retry_with(max_attempts, predicate, ...)` (générique sur tout type d'erreur — utilisé par kesh-api pour wrap AppError) - `is_deadlock_error(&DbError) -> bool` (prédicat exposé pour les callers de kesh-api) Backoff exponentiel : 50 ms × 2^(attempt-1). Détection : MariaDB error code 1213 (`ER_LOCK_DEADLOCK`) via `MySqlDatabaseError::number()`. ### `crates/kesh-api/src/routes/onboarding.rs::finalize` Refactoring : extraction du body en `finalize_inner()` (idempotent sous retry — `INSERT IGNORE`, `create_if_absent_in_tx`, court-circuit `step_completed == 8`). Wrap dans `retry_with` avec prédicat AppError. Préserve toutes les ACs business actuelles (`OnboardingStepAlreadyCompleted`, `Validation`, etc. ne sont pas retryables). ### `docs/MULTI-TENANT-SCOPING-PATTERNS.md` Pattern 5 - Mise à jour du status KF-002-H-002 (resolved 2026-05-03). - Section deny-list de divergent endpoints (vide actuellement). - Pattern d'usage du retry helper (avec contraintes d'idempotence). - CI lint deferred (effort vs valeur — review discipline + Pattern 5 doc couvrent pour l'instant). ## Tests - 5 unit tests dans `retry.rs` (success path, non-deadlock passthrough, max_attempts edge cases, integration real deadlock). - 152/152 kesh-db lib tests verts (release). - 18/18 onboarding e2e tests verts (incluant `full_path_b_flow` qui exercise finalize end-to-end). - clippy `-D warnings` clean, cargo fmt clean. ## Notes - `tokio` ajouté en dependency (default-features=false, features=time) pour `tokio::time::sleep`. Le runtime est déjà fourni par sqlx. - `kesh-db` Cargo.toml mis à jour. - L'integration test `integration_real_deadlock_recovers_via_retry` utilise une stratégie sleep-based (pas de barrier — incompatible avec le retry car la barrière ne survit pas au rappel). Code review adversariale (1 passe Sonnet) à suivre dans le PR. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Code review adversariale Sonnet (1 passe, contexte frais) :
- Verdict GO. 0 CRITICAL. 1 HIGH (test panic sans DATABASE_URL).
- 3 MEDIUM, 5 LOW.
Patches appliqués :
- M-001 doc latence corrigée (350 → 150 ms worst case ; 3 tentatives
produisent 2 sleeps, pas 3 — le 3e essai n'est suivi d'aucun sleep
car le helper return Err si `attempt >= attempts`).
- M-002 cap saturating sur backoff (`MAX_BACKOFF_MS = 30s`) +
`checked_shl` au lieu de `pow` panic-en-debug. Rend l'API safe pour
tout `max_attempts: u32` même si DEFAULT=3 ne peut jamais y atteindre.
- M-003 reword commentaire `tokio` dep (cargo feature unification, pas
"runtime déjà fourni par sqlx" qui est ambigu).
- L-004 doc `is_deadlock_sqlx` mentionne SQLSTATE 40001 vs MariaDB code
1213 (on match strictement le numérique).
- L-005 Pattern 5 doc précise que 1205 (`lock_wait_timeout`) n'est pas
retried — un retry sur 1205 re-déclencherait simplement la même
attente (différent d'un cycle de deadlock détecté instantané).
Defers :
- H-001 test panic sans DATABASE_URL : convention projet (152 tests
font pareil avec `expect("DATABASE_URL required")`). Un changement
isolé ici créerait une incohérence ; à traiter globalement si CI
ajoute un job sans DB.
- L-001 tracing sans request context : caller responsibility (le helper
est générique). Pattern 5 doc note la recommandation de wrapper en
span côté caller.
- L-002 mirror divergence company_invoice_settings : pré-existant, hors
scope KF-002-H-002.
- L-003 integration test non-déterminisme : invariant final
(val_1==1, val_2==1) suffisant. Le retry-counter assertion serait un
détecteur supplémentaire mais pas nécessaire pour la sanité.
Tests : 5/5 retry verts (incluant integration deadlock), clippy
`-D warnings` clean, fmt clean.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Item #3 du prep sprint Epic 8. Adresse le risque user-visible 500 sur deadlock cross-table documenté dans Pattern 5.
Avant : MariaDB ne détecte pas les deadlocks multi-table avant
innodb_lock_wait_timeout(50s) → 500 brut côté user. Mitigation v0.1 : doc-only.Après : helper
retry_withgénérique catcheER_LOCK_DEADLOCK(1213) et rejoue la closure avec backoff exponentiel (50→100ms ; max 3 tentatives, ≈ 150ms latence ajoutée pire cas). Appliqué àfinalize()qui était l'endpoint at-risk identifié.Architecture (option B validée Guy)
Helper repo-level
retry_with(max_attempts, predicate, closure)danscrates/kesh-db/src/retry.rs:retry_on_deadlock: ergo wrapper surDbError, max 3 tentatives.retry_with: générique sur tout type d'erreurE+ prédicat. Utilisé par kesh-api pour wrapAppError.is_deadlock_error(&DbError): prédicat exposé pour les callers.Backoff capé à
MAX_BACKOFF_MS = 30s+checked_shlpour éviter overflow surmax_attemptsexotiques (review M-002).Application à
finalize()Refactoring : extraction du body en
finalize_inner()(idempotent —INSERT IGNORE,create_if_absent_in_tx, court-circuitstep_completed == 8). Wrap dansretry_withavec prédicat AppError ciblant uniquementAppError::Database(DbError::Sqlx(deadlock)).Business errors (
OnboardingStepAlreadyCompleted,Validation) ne sont pas retryables — pass-through immédiat.Code review (1 passe Sonnet adversariale)
DATABASE_URLu64overflow àmax_attempts ≥ 65Verdict reviewer : GO.
Doc updates
docs/MULTI-TENANT-SCOPING-PATTERNS.mdPattern 5 :Test plan
full_path_b_flowqui exercise finalize)-D warningscleancloses #43Suite — prep sprint Epic 8
Items restants après ce PR :
epic-8.md(PR docs(epic-8): create planning artifact + open CR-009/CR-010 #63)kesh-importcrate design🤖 Generated with Claude Code