internal/dispatcher/agent : la base est fermée sous les workers, panic intermittent en test #47

Open
opened 2026-08-31 13:07:34 +00:00 by nicolas.boufideline · 1 comment

Symptôme

go test ./... échoue de façon intermittente sur internal/dispatcher/agent :

panic: send on closed channel

Reproduit sur main (10c9bc2), go1.24.11, darwin/arm64 : environ une passe complète sur
deux
. Le paquet seul (go test -count=12 ./internal/dispatcher/agent/) passe toujours — c'est
l'exécution des paquets en parallèle qui le fait sortir.

Trace

panic: send on closed channel

github.com/dgraph-io/badger/v4.(*DB).sendToWriteCh
	badger/v4@v4.8.0/db.go:904
github.com/dgraph-io/badger/v4.(*Txn).commitAndSend
	badger/v4@v4.8.0/txn.go:593
github.com/dgraph-io/badger/v4.(*DB).Update
	badger/v4@v4.8.0/txn.go:820
git.g3e.fr/syonad/two/pkg/db/kv.AddInDB
	pkg/db/kv/addInDB.go:8
git.g3e.fr/syonad/two/internal/state.Set
	internal/state/state.go:67
git.g3e.fr/syonad/two/internal/dispatcher/agent.(*Dispatcher).Dispatch.func1
	internal/dispatcher/agent/dispatcher.go:48
git.g3e.fr/syonad/two/pkg/worker.(*Queue).Start.func1
	pkg/worker/queue.go:45

Le canal fermé est celui de Badger, pas celui de Queue. queue.go:45 n'est que la frame
où la tâche s'exécute.

Cause

internal/dispatcher/agent/helpers_test.go :

db := kv.InitDB(kv.Config{Path: t.TempDir()}, false)
t.Cleanup(func() { db.Close() })
q := worker.New(100)
q.Start(2)

La queue est démarrée mais jamais arrêtée. À la fin du test, db.Close() s'exécute alors
que les deux goroutines worker peuvent encore être en train d'exécuter une tâche dispatchée ;
celle-ci appelle state.Set → kv.AddInDB → écriture Badger sur une base fermée.

C'est exactement l'inversion de l'invariant documenté au CLAUDE.md — serveurs HTTP → drainage
des workers → db.Close()
— mais commise dans le harnais de test.

Effet de bord secondaire : les goroutines worker fuient d'un test à l'autre dans ce paquet,
ce qui explique que le défaut ne sorte que sous la charge d'une passe complète.

Ce qui n'est pas touché

La production est correcte. cmd/agent/main.go respecte l'ordre : shutdown() arrête les
serveurs HTTP, appelle q.Stop(), et db.Close() n'est atteint qu'au retour du drainage
(main.go:45). Le défaut est confiné au test.

Il n'y a donc pas de course à l'arrêt de l'agent — seulement une suite de tests qui échoue une
fois sur deux, ce qui est suffisamment pénible pour être corrigé : une CI rouge par
intermittence finit par ne plus être lue.

Correction proposée

Arrêter la queue avant de fermer la base dans newTestDispatcher :

t.Cleanup(func() {
	q.Stop()
	db.Close()
})

Queue.Stop rejette les nouvelles tâches puis attend les tâches en vol (wg.Wait()), ce qui
garantit qu'aucune écriture n'est en cours quand Badger se ferme. Il faut donc construire la
queue avant d'enregistrer le cleanup.

À faire avant #46, qui touchera ce paquet.

## Symptôme `go test ./...` échoue de façon intermittente sur `internal/dispatcher/agent` : ``` panic: send on closed channel ``` Reproduit sur `main` (10c9bc2), go1.24.11, darwin/arm64 : environ **une passe complète sur deux**. Le paquet seul (`go test -count=12 ./internal/dispatcher/agent/`) passe toujours — c'est l'exécution des paquets en parallèle qui le fait sortir. ## Trace ``` panic: send on closed channel github.com/dgraph-io/badger/v4.(*DB).sendToWriteCh badger/v4@v4.8.0/db.go:904 github.com/dgraph-io/badger/v4.(*Txn).commitAndSend badger/v4@v4.8.0/txn.go:593 github.com/dgraph-io/badger/v4.(*DB).Update badger/v4@v4.8.0/txn.go:820 git.g3e.fr/syonad/two/pkg/db/kv.AddInDB pkg/db/kv/addInDB.go:8 git.g3e.fr/syonad/two/internal/state.Set internal/state/state.go:67 git.g3e.fr/syonad/two/internal/dispatcher/agent.(*Dispatcher).Dispatch.func1 internal/dispatcher/agent/dispatcher.go:48 git.g3e.fr/syonad/two/pkg/worker.(*Queue).Start.func1 pkg/worker/queue.go:45 ``` Le canal fermé est **celui de Badger**, pas celui de `Queue`. `queue.go:45` n'est que la frame où la tâche s'exécute. ## Cause `internal/dispatcher/agent/helpers_test.go` : ```go db := kv.InitDB(kv.Config{Path: t.TempDir()}, false) t.Cleanup(func() { db.Close() }) q := worker.New(100) q.Start(2) ``` La queue est démarrée mais **jamais arrêtée**. À la fin du test, `db.Close()` s'exécute alors que les deux goroutines worker peuvent encore être en train d'exécuter une tâche dispatchée ; celle-ci appelle `state.Set` → `kv.AddInDB` → écriture Badger sur une base fermée. C'est exactement l'inversion de l'invariant documenté au CLAUDE.md — *serveurs HTTP → drainage des workers → `db.Close()`* — mais commise dans le harnais de test. Effet de bord secondaire : les goroutines worker fuient d'un test à l'autre dans ce paquet, ce qui explique que le défaut ne sorte que sous la charge d'une passe complète. ## Ce qui n'est pas touché **La production est correcte.** `cmd/agent/main.go` respecte l'ordre : `shutdown()` arrête les serveurs HTTP, appelle `q.Stop()`, et `db.Close()` n'est atteint qu'au retour du drainage (`main.go:45`). Le défaut est confiné au test. Il n'y a donc pas de course à l'arrêt de l'agent — seulement une suite de tests qui échoue une fois sur deux, ce qui est suffisamment pénible pour être corrigé : une CI rouge par intermittence finit par ne plus être lue. ## Correction proposée Arrêter la queue avant de fermer la base dans `newTestDispatcher` : ```go t.Cleanup(func() { q.Stop() db.Close() }) ``` `Queue.Stop` rejette les nouvelles tâches puis attend les tâches en vol (`wg.Wait()`), ce qui garantit qu'aucune écriture n'est en cours quand Badger se ferme. Il faut donc construire la queue avant d'enregistrer le cleanup. À faire avant #46, qui touchera ce paquet.
Author
Owner

Toujours présent le 2026-10-04 — et aussi sur le paquet lancé seul

Constaté en vérifiant E2 de #50 : une passe de go test ./... a échoué sur
internal/dispatcher/agent, avec exactement la trace du ticket (panic: send on closed channel
dans badger.(*DB).sendToWriteCh, depuis state.Set appelé par Dispatcher.Dispatch.func1).

Reproduit ensuite sur main (01c67bd), sans aucun changement, go1.25.14, darwin/arm64 :

for i in 1 2 3 4 5 6; do go test -count=1 ./internal/dispatcher/agent/; done

→ 1 échec sur 6. Contrairement à ce que dit le ticket, le paquet lancé seul le fait donc
sortir aussi : le parallélisme de la suite complète augmente la fréquence, il n'est pas nécessaire.
Le passage à go 1.25 (#46) n'a rien changé au défaut.

Rien de neuf sur la cause : la queue démarrée par helpers_test.go n'est jamais arrêtée avant
db.Close().

## Toujours présent le 2026-10-04 — et aussi sur le paquet lancé seul Constaté en vérifiant E2 de #50 : une passe de `go test ./...` a échoué sur `internal/dispatcher/agent`, avec **exactement la trace du ticket** (`panic: send on closed channel` dans `badger.(*DB).sendToWriteCh`, depuis `state.Set` appelé par `Dispatcher.Dispatch.func1`). Reproduit ensuite sur `main` (`01c67bd`), sans aucun changement, go1.25.14, darwin/arm64 : ``` for i in 1 2 3 4 5 6; do go test -count=1 ./internal/dispatcher/agent/; done ``` → **1 échec sur 6**. Contrairement à ce que dit le ticket, le paquet **lancé seul** le fait donc sortir aussi : le parallélisme de la suite complète augmente la fréquence, il n'est pas nécessaire. Le passage à go 1.25 (#46) n'a rien changé au défaut. Rien de neuf sur la cause : la queue démarrée par `helpers_test.go` n'est jamais arrêtée avant `db.Close()`.
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
syonad/two#47
No description provided.