Files
at-container-registry/pkg/hold/db/hold_db.go
T
Evan JarrettandClaude Opus 5 c44a874090 hold: set busy_timeout on every pooled connection, not just the first
A PRAGMA is per-connection, and database/sql opens one connection per
concurrent caller. hold_db.go and scan_broadcaster.go each set
busy_timeout = 5000 with a one-shot query against the pool, which configures
whichever connection happened to serve it and leaves every other writer at
zero. Probing the live pools showed exactly that: conn 0 at 5000, conns 1 and 2
at 0. That is the database is locked on COMMIT, and the carstore had no
busy_timeout and no WAL on any connection at all.

Opening a database now goes through a connector that runs the PRAGMA on every
connection the pool creates, ported from the appview, which had already solved
this. Applied to the carstore, the shared hold DB, the records index, the event
broadcaster and the scan broadcaster. In-memory databases are left unwrapped,
since libsql gives each connection its own, and the embedded replica path still
skips PRAGMAs because a remote rejects them.

WAL is not a new risk: production was already WAL, because journal mode is a
persistent property of the file and OpenHoldDB has been setting it. This makes
the carstore's own opener agree rather than leaving standalone and test
databases in rollback-journal mode. A database that refuses WAL logs a warning
instead of failing boot, so a network mount cannot stop the hold starting.

NewHoldPDS's file mode opened the same file a second time for the records
index; it now shares the carstore's pool. Two pools on one file buy nothing,
since SQLite's write lock is per database, so the second only adds contenders.
Production already shared a pool and was unaffected by that half.

Note for anyone reading OpenHoldDB: its foreign_keys pragma has the same
one-connection scope, but libsql reports foreign_keys on for every fresh
connection anyway, so it is decorative rather than broken.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01U1Km3N3uUmeGaj7VbaM8PF
2026-09-05 16:14:35 -05:00

86 lines
2.5 KiB
Go

package db
import (
"database/sql"
"fmt"
"io"
"log/slog"
"time"
"github.com/tursodatabase/go-libsql"
)
// LibsqlConfig holds optional libSQL sync settings for embedded replicas.
// When SyncURL is empty, the database operates in local-only mode.
type LibsqlConfig struct {
SyncURL string
AuthToken string
SyncInterval time.Duration
}
// HoldDB wraps the shared *sql.DB and optional connector for lifecycle management.
type HoldDB struct {
DB *sql.DB
connector io.Closer // non-nil only in embedded replica mode
}
// Close closes the database connection and connector (if any).
// The connector must be closed to release file locks.
func (h *HoldDB) Close() error {
var dbErr, connErr error
if h.DB != nil {
dbErr = h.DB.Close()
}
if h.connector != nil {
connErr = h.connector.Close()
}
if dbErr != nil {
return dbErr
}
return connErr
}
// OpenHoldDB initializes the hold's shared database connection.
// Uses libSQL embedded replica when cfg.SyncURL is set, otherwise local-only.
// The caller must call HoldDB.Close() on shutdown.
func OpenHoldDB(path string, cfg LibsqlConfig) (*HoldDB, error) {
var db *sql.DB
var connector io.Closer
if cfg.SyncURL != "" {
// Embedded replica mode: local file + sync to remote
opts := []libsql.Option{
libsql.WithAuthToken(cfg.AuthToken),
}
if cfg.SyncInterval > 0 {
opts = append(opts, libsql.WithSyncInterval(cfg.SyncInterval))
}
conn, err := libsql.NewEmbeddedReplicaConnector(path, cfg.SyncURL, opts...)
if err != nil {
return nil, fmt.Errorf("failed to create libsql embedded replica connector: %w", err)
}
db = sql.OpenDB(conn)
connector = conn
slog.Info("Hold database opened in embedded replica mode", "path", path, "sync_url", cfg.SyncURL)
} else {
// Local-only mode: WAL journal mode plus a busy_timeout on *every*
// pooled connection. In embedded replica mode the remote server manages
// these settings and PRAGMA assignments are rejected as "unsupported
// statement" (observed with Bunny Database), so OpenLocalDB is only for
// the local-only branch.
var err error
db, err = OpenLocalDB(path)
if err != nil {
return nil, fmt.Errorf("failed to open hold database: %w", err)
}
slog.Info("Hold database opened in local-only mode", "path", path)
}
// Foreign keys work in both modes
if _, err := db.Exec("PRAGMA foreign_keys = ON"); err != nil {
return nil, fmt.Errorf("failed to enable foreign keys: %w", err)
}
return &HoldDB{DB: db, connector: connector}, nil
}