mirror of
https://github.com/docker/docker-agent.git
synced 2026-11-06 15:07:58 +00:00
Thread context through sqlite/session/RAG/memory DB setup so constructors and migrations no longer mint fresh roots. Fake proxy cleanup and askpass connection handling now use context providers derived from their startup context. LocalRuntime stores a context provider for tool-change refresh callbacks. The remaining suppressions are intentionally detached event hooks/MCP supervisor callbacks or compatibility fallbacks.
95 lines
3.0 KiB
Go
95 lines
3.0 KiB
Go
package sqliteutil
|
|
|
|
import (
|
|
"context"
|
|
"database/sql"
|
|
"errors"
|
|
"fmt"
|
|
"log/slog"
|
|
"os"
|
|
"path/filepath"
|
|
|
|
"modernc.org/sqlite"
|
|
sqlite3 "modernc.org/sqlite/lib"
|
|
)
|
|
|
|
// OpenDB opens a SQLite database with recommended pragmas for concurrency and foreign key support.
|
|
// It configures the connection pool for serialized writes (MaxOpenConns=1).
|
|
func OpenDB(ctx context.Context, path string) (*sql.DB, error) {
|
|
dir := filepath.Dir(path)
|
|
if err := os.MkdirAll(dir, 0o700); err != nil {
|
|
return nil, fmt.Errorf("cannot create database directory %q: %w", dir, err)
|
|
}
|
|
|
|
// Add query parameters for better concurrency handling and data integrity
|
|
// _pragma=busy_timeout(5000): Wait up to 5 seconds if database is locked
|
|
// _pragma=journal_mode(WAL): Enable Write-Ahead Logging for better concurrent access
|
|
// _pragma=foreign_keys(1): Enable foreign key constraints (critical for ON DELETE CASCADE)
|
|
dsn := path + "?_pragma=busy_timeout(5000)&_pragma=journal_mode(WAL)&_pragma=foreign_keys(1)"
|
|
|
|
db, err := sql.Open("sqlite", dsn)
|
|
if err != nil {
|
|
if IsCantOpenError(err) {
|
|
return nil, DiagnoseDBOpenError(path, err)
|
|
}
|
|
return nil, err
|
|
}
|
|
|
|
// Configure connection pool to serialize writes (SQLite limitation)
|
|
// This prevents "database is locked" errors from concurrent writes
|
|
db.SetMaxOpenConns(1)
|
|
db.SetMaxIdleConns(1)
|
|
db.SetConnMaxLifetime(0)
|
|
|
|
// Verify connection works (this will trigger file creation/open)
|
|
if err := db.PingContext(ctx); err != nil {
|
|
db.Close()
|
|
if IsCantOpenError(err) {
|
|
return nil, DiagnoseDBOpenError(path, err)
|
|
}
|
|
return nil, err
|
|
}
|
|
|
|
return db, nil
|
|
}
|
|
|
|
// IsCantOpenError checks if the error is a SQLite CANTOPEN error (code 14).
|
|
func IsCantOpenError(err error) bool {
|
|
if sqliteErr, ok := errors.AsType[*sqlite.Error](err); ok {
|
|
return sqliteErr.Code() == sqlite3.SQLITE_CANTOPEN
|
|
}
|
|
return false
|
|
}
|
|
|
|
// DiagnoseDBOpenError provides a more helpful error message when SQLite
|
|
// fails to open/create a database file.
|
|
func DiagnoseDBOpenError(path string, originalErr error) error {
|
|
dir := filepath.Dir(path)
|
|
|
|
info, err := os.Stat(dir)
|
|
if err != nil {
|
|
if os.IsNotExist(err) {
|
|
return fmt.Errorf("cannot create database at %q: directory %q does not exist", path, dir)
|
|
}
|
|
return fmt.Errorf("cannot create database at %q: %w", path, err)
|
|
}
|
|
|
|
if !info.IsDir() {
|
|
return fmt.Errorf("cannot create database at %q: %q is not a directory", path, dir)
|
|
}
|
|
|
|
return fmt.Errorf("cannot create database at %q: permission denied or file cannot be created in %q (original error: %w)", path, dir, originalErr)
|
|
}
|
|
|
|
// CheckpointAndClose runs a final WAL checkpoint and closes the database.
|
|
// The TRUNCATE checkpoint folds the -wal file back into the main database
|
|
// so it isn't left behind on disk after shutdown. A checkpoint failure is
|
|
// logged but does not prevent the close.
|
|
func CheckpointAndClose(ctx context.Context, db *sql.DB) error {
|
|
ctx = context.WithoutCancel(ctx)
|
|
if _, err := db.ExecContext(ctx, "PRAGMA wal_checkpoint(TRUNCATE)"); err != nil {
|
|
slog.WarnContext(ctx, "Failed to checkpoint WAL before close", "error", err)
|
|
}
|
|
return db.Close()
|
|
}
|