package db import ( "context" "path/filepath" "testing" "time" ) // regression for the 2026-09-02 executor-lost incident: a writer blocked past // the busy timeout by a competing connection must retry and apply exactly // once, not surface "database is locked" and kill the executor session func TestAppendOutboxRowRetriesHeldWriteLock(t *testing.T) { ctx := context.Background() dbPath := filepath.Join(t.TempDir(), "mill.db") d, err := Make(ctx, dbPath) if err != nil { t.Fatalf("Make: %v", err) } t.Cleanup(func() { d.Close() }) if err := d.SetOutboxEpoch("epoch-1"); err != nil { t.Fatalf("SetOutboxEpoch: %v", err) } other, err := Make(ctx, dbPath) if err != nil { t.Fatalf("Make second: %v", err) } t.Cleanup(func() { other.Close() }) conn, err := other.Conn(ctx) if err != nil { t.Fatalf("Conn: %v", err) } defer conn.Close() if _, err := conn.ExecContext(ctx, "BEGIN IMMEDIATE"); err != nil { t.Fatalf("BEGIN IMMEDIATE: %v", err) } done := make(chan struct{}) go func() { defer close(done) // hold the write lock past the 5s busy timeout so the first attempt // fails and retrySQLite must recover time.Sleep(5500 * time.Millisecond) _, _ = conn.ExecContext(ctx, "ROLLBACK") }() start := time.Now() seqno, err := d.AppendOutboxRow([]byte("payload"), false) if err != nil { t.Fatalf("AppendOutboxRow: %v", err) } if elapsed := time.Since(start); elapsed < 5*time.Second { t.Fatalf("AppendOutboxRow returned in %v without contending", elapsed) } if seqno != 1 { t.Fatalf("seqno = %d, want 1", seqno) } epoch, next, err := d.GetOutboxState() if err != nil { t.Fatalf("GetOutboxState: %v", err) } if epoch != "epoch-1" || next != 2 { t.Fatalf("outbox state = (%q, %d), want (epoch-1, 2): seqno must advance exactly once across retries", epoch, next) } rows, err := d.ListOutboxRows() if err != nil { t.Fatalf("ListOutboxRows: %v", err) } if len(rows) != 1 { t.Fatalf("outbox rows = %d, want 1", len(rows)) } <-done }