package db import ( "context" "fmt" "path/filepath" "strings" "sync" "testing" "github.com/bluesky-social/indigo/models" blockformat "github.com/ipfs/go-block-format" "github.com/ipfs/go-cid" ) // isLockErr reports whether err is a SQLite busy/locked error rather than a // logic error. Contention tests must fail on lock errors specifically, so a // typo cannot masquerade as a reproduction. func isLockErr(err error) bool { if err == nil { return false } s := strings.ToLower(err.Error()) return strings.Contains(s, "database is locked") || strings.Contains(s, "database table is locked") || strings.Contains(s, "sqlite_busy") || strings.Contains(s, "busy") } func makeBlocks(t *testing.T, n int, salt string) (cid.Cid, map[cid.Cid]blockformat.Block) { t.Helper() blks := make(map[cid.Cid]blockformat.Block, n) var root cid.Cid for i := range n { b := blockformat.NewBlock(fmt.Appendf(nil, "%s-block-%d-%s", salt, i, strings.Repeat("x", 2048))) blks[b.Cid()] = b if i == 0 { root = b.Cid() } } return root, blks } // writeShards hammers the carstore with concurrent shard writes and reports any // lock error it hits. This is the exact path every hold record write takes. func writeShards(t *testing.T, sqs *SQLiteStore, writers, iters int) error { t.Helper() ctx := context.Background() var wg sync.WaitGroup var mu sync.Mutex var firstErr error for w := range writers { wg.Add(1) go func(w int) { defer wg.Done() for i := range iters { salt := fmt.Sprintf("w%d-i%d", w, i) root, blks := makeBlocks(t, 12, salt) rev := fmt.Sprintf("rev-%03d-%03d", w, i) _, err := sqs.writeNewShard(ctx, root, rev, models.Uid(1), i, blks, nil) if err != nil { mu.Lock() if firstErr == nil { firstErr = err } mu.Unlock() return } } }(w) } wg.Wait() return firstErr } // TestCarstoreConcurrentWritesSharedPool reproduces the production topology: // one *sql.DB pool (OpenHoldDB) shared by the carstore, records index, events // and scan broadcaster. database/sql opens a fresh connection per concurrent // caller, and busy_timeout is per-connection, so any connection beyond the // first commits with busy_timeout=0 and fails immediately under contention. func TestCarstoreConcurrentWritesSharedPool(t *testing.T) { path := filepath.Join(t.TempDir(), "db.sqlite3") h, err := OpenHoldDB(path, LibsqlConfig{}) if err != nil { t.Fatalf("OpenHoldDB: %v", err) } defer h.Close() sqs, err := NewSQLiteStoreWithDB(path, h.DB) if err != nil { t.Fatalf("NewSQLiteStoreWithDB: %v", err) } if err := writeShards(t, sqs, 8, 25); err != nil { if isLockErr(err) { t.Fatalf("shared-pool carstore write hit a lock error: %v", err) } t.Fatalf("shared-pool carstore write failed: %v", err) } } // TestCarstoreConcurrentWritesTwoOpeners covers the topology where a second // subsystem (the scan broadcaster or the event broadcaster, when they are not // handed the shared pool) opens