fix(db): transactions wait for the write lock instead of failing (FLAW-23)
MindSift CI & Docker Release / Build, Test in Container & Push (push) Successful in 58s

A load test with 16 concurrent writers lost 12 of 480 writes with
SQLITE_BUSY: transactions that read before writing cannot be helped by
busy_timeout once another connection writes. _txlock=immediate makes
every transaction take the write lock at BEGIN, so it queues instead.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
sandrodachandClaude Opus 5.5 committed 2026-10-29 14:51:01 +01:00
1 parent dc44f8250c
commit 792ca946f8
3 files changed
+68 -2

No files matched your search

+62
View File
@@ -0,0 +1,62 @@
package storage
import (
"fmt"
"path/filepath"
"sync"
"testing"
"github.com/sandrodach/mindsift/internal/domain"
)
// Many writers at once on one SQLite file (FLAW-23): transactions that read before they write must
// wait for the write lock instead of failing with SQLITE_BUSY.
func TestConcurrentWritersDoNotFail(t *testing.T) {
store, err := NewStore(filepath.Join(t.TempDir(), "load.db"))
if err != nil {
t.Fatal(err)
}
defer store.Close()
u, _ := store.CreateUser("load@example.com", "hash", "Last", "member")
tl, _ := store.CreateTaskList(u.ID, "", "", "Last", "", "")
var ids []string
for i := 0; i < 20; i++ {
th, _ := store.CreateThoughtWithDetails(u.ID, fmt.Sprintf("Aufgabe %d", i), "normal", "manual", "", tl.ID, "todo", "")
_, _ = store.UpdateThoughtTriage(u.ID, th.ID, "keep", "task", "", "", tl.ID)
ids = append(ids, th.ID)
}
var wg sync.WaitGroup
errs := make(chan error, 400)
for w := 0; w < 16; w++ {
wg.Add(1)
go func(w int) {
defer wg.Done()
for i := 0; i < 15; i++ {
status := "todo"
if (w+i)%2 == 0 {
status = "done"
}
items := []domain.ThoughtReorderItem{{ID: ids[(w+i)%len(ids)], SortOrder: i, KanbanStatus: &status}}
if err := store.ReorderTasks(tl.ID, items); err != nil {
errs <- fmt.Errorf("reorder: %w", err)
}
if _, err := store.CreateThought(u.ID, fmt.Sprintf("Gedanke %d-%d", w, i), "normal", "manual", ""); err != nil {
errs <- fmt.Errorf("create: %w", err)
}
}
}(w)
}
wg.Wait()
close(errs)
failed := 0
for err := range errs {
if failed < 3 {
t.Error(err)
}
failed++
}
if failed > 0 {
t.Fatalf("%d of %d writes failed", failed, 16*15*2)
}
}