bug(bilan) : « database is locked » lors de la proposition des comptes standards (proposeStarterAccounts) #258

Closed
opened 2026-07-06 00:29:15 +00:00 by maximus · 1 comment
Owner

Symptôme

Erreur « database is locked » lors du script de proposition/migration de la hiérarchie de comptes standard (Compte chèque, CELI, REER, Compte non-enregistré) dans le module Bilan.

Piste (à confirmer via /analyze)

proposeStarterAccounts (src/services/balance.service.ts:703) est wrappé dans withTransaction (l.708), mais proposeStarterAccountsInTransaction (l.713) émet AUSSI await db.execute("BEGIN") (l.720). À vérifier : BEGIN imbriqué ou transaction non refermée → connexion strandée du pool sqlx = verrou zombie. Même classe de bug que le fix v0.10.1 (sérialisation withTransaction, jamais de db.execute("BEGIN") brut concurrent). Vérifier aussi le second site l.1507/1524.

Reproduction (à préciser)

Déclencher la proposition de comptes standards (StarterAccountsModal). Scénario exact à confirmer : profil vide, après un abandon, ou enchaînement rapide ?

Fichiers

  • src/services/balance.service.tsproposeStarterAccounts / proposeStarterAccountsInTransaction (l.703-720+)
  • src/components/balance/StarterAccountsModal.tsx
  • src/components/balance/StarterAccountsModal.test.tsx (régression obligatoire)

Note

type:bug → test de régression requis. /analyze recommandé avant fix pour confirmer la cause racine.

## Symptôme Erreur « database is locked » lors du script de proposition/migration de la hiérarchie de comptes standard (Compte chèque, CELI, REER, Compte non-enregistré) dans le module Bilan. ## Piste (à confirmer via /analyze) `proposeStarterAccounts` (`src/services/balance.service.ts:703`) est wrappé dans `withTransaction` (l.708), mais `proposeStarterAccountsInTransaction` (l.713) émet AUSSI `await db.execute("BEGIN")` (l.720). À vérifier : BEGIN imbriqué ou transaction non refermée → connexion strandée du pool sqlx = verrou zombie. Même classe de bug que le fix v0.10.1 (sérialisation `withTransaction`, jamais de `db.execute("BEGIN")` brut concurrent). Vérifier aussi le second site l.1507/1524. ## Reproduction (à préciser) Déclencher la proposition de comptes standards (StarterAccountsModal). Scénario exact à confirmer : profil vide, après un abandon, ou enchaînement rapide ? ## Fichiers - `src/services/balance.service.ts` — `proposeStarterAccounts` / `proposeStarterAccountsInTransaction` (l.703-720+) - `src/components/balance/StarterAccountsModal.tsx` - `src/components/balance/StarterAccountsModal.test.tsx` (régression obligatoire) ## Note type:bug → test de régression requis. `/analyze` recommandé avant fix pour confirmer la cause racine.
maximus added the
status:triage
type:bug
source:human
labels 2026-07-06 00:29:15 +00:00
Author
Owner

Fermé — déjà corrigé, pas de bug résiduel. (Investigation code sur main @ v0.12.0.)

La piste « BEGIN imbriqué » est une mauvaise lecture. withTransaction (src/services/db.ts) n'émet aucun BEGIN — il tient le verrou FIFO et pose le flag inTransaction=true pendant le callback (docstring : « fn is responsible for issuing BEGIN/COMMIT »). Le db.execute("BEGIN") de proposeStarterAccountsInTransaction (balance.service.ts:720) est donc le seul BEGIN, pas un second.

Preuve par cohérence : upsertSnapshotLines (:1507→1524) et saveSnapshotAtomic (:1595→1610) utilisent exactement le même pattern withTransaction((db) => …InTransaction(db)) avec BEGIN/COMMIT dans le callback. Si ce pattern double-BEGIN-ait, chaque sauvegarde de snapshot deadlockerait — ce n'est pas le cas. Le proxy db.ts bypasse d'ailleurs le verrou quand inTransaction===true, donc pas de self-deadlock (couvert par db.test.ts:89-114).

Le vrai « database is locked » (stranding de connexion sur le pool sqlx multi-connexions) a été fermé en v0.10.1 (commit 8cb7e53, sérialisation via withTransaction) — présent dans v0.11.0 et v0.12.0 (git tag --contains 8cb7e53). Avant le fix (≤ v0.10.0), getDb() renvoyait le handle brut du pool → BEGIN et COMMIT pouvaient frapper des connexions différentes → transaction strandée = verrou zombie. Le pattern actuel est correct : ne pas rendre withTransaction réentrant ni retirer le BEGIN interne.

L'absence de repro en 0.12.0 est cohérente : le code tourné a le fix, et il n'y a aucun nesting résiduel.

Point latent (hors scope, non bloquant) : le garde inTransaction est un booléen global, pas un compteur → une requête simple dispatchée par du code non lié pendant un withTransaction en cours lirait true et bypasserait le verrou. Pas un reproducteur pour ce chemin (profil vide = quasi aucune activité DB concurrente pendant la transaction), mais à traiter dans une issue dédiée si on veut blinder la sérialisation.

**Fermé — déjà corrigé, pas de bug résiduel.** (Investigation code sur `main` @ v0.12.0.) **La piste « BEGIN imbriqué » est une mauvaise lecture.** `withTransaction` (`src/services/db.ts`) n'émet **aucun** `BEGIN` — il tient le verrou FIFO et pose le flag `inTransaction=true` pendant le callback (docstring : « fn is responsible for issuing BEGIN/COMMIT »). Le `db.execute("BEGIN")` de `proposeStarterAccountsInTransaction` (`balance.service.ts:720`) est donc le **seul** BEGIN, pas un second. Preuve par cohérence : `upsertSnapshotLines` (`:1507→1524`) et `saveSnapshotAtomic` (`:1595→1610`) utilisent **exactement** le même pattern `withTransaction((db) => …InTransaction(db))` avec BEGIN/COMMIT dans le callback. Si ce pattern double-BEGIN-ait, **chaque sauvegarde de snapshot deadlockerait** — ce n'est pas le cas. Le proxy `db.ts` bypasse d'ailleurs le verrou quand `inTransaction===true`, donc pas de self-deadlock (couvert par `db.test.ts:89-114`). **Le vrai « database is locked » (stranding de connexion sur le pool sqlx multi-connexions) a été fermé en v0.10.1** (commit `8cb7e53`, sérialisation via `withTransaction`) — présent dans `v0.11.0` et `v0.12.0` (`git tag --contains 8cb7e53`). Avant le fix (≤ v0.10.0), `getDb()` renvoyait le handle brut du pool → BEGIN et COMMIT pouvaient frapper des connexions différentes → transaction strandée = verrou zombie. Le pattern actuel est correct : **ne pas** rendre `withTransaction` réentrant ni retirer le BEGIN interne. L'absence de repro en 0.12.0 est cohérente : le code tourné a le fix, et il n'y a aucun nesting résiduel. **Point latent (hors scope, non bloquant)** : le garde `inTransaction` est un booléen global, pas un compteur → une requête simple dispatchée par du code non lié *pendant* un `withTransaction` en cours lirait `true` et bypasserait le verrou. Pas un reproducteur pour ce chemin (profil vide = quasi aucune activité DB concurrente pendant la transaction), mais à traiter dans une issue dédiée si on veut blinder la sérialisation.
Sign in to join this conversation.
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference: maximus/Simpl-Resultat#258
No description provided.