From 9801d0ae40536285eada0dc95f801eff9b643872 Mon Sep 17 00:00:00 2001 From: dawn Date: Thu, 22 Oct 2026 02:32:51 +0300 Subject: [PATCH] spindle/db: keep idle db connections open so bursts don't churn them Signed-off-by: dawn --- spindle/db/db.go | 4 +++ spindle/db/fd_leak_test.go | 64 ++++++++++++++++++++++++++++++++++++++ 2 files changed, 68 insertions(+) create mode 100644 spindle/db/fd_leak_test.go diff --git a/spindle/db/db.go b/spindle/db/db.go index 12643f4cf..453122c55 100644 --- a/spindle/db/db.go +++ b/spindle/db/db.go @@ -45,6 +45,8 @@ func queryDIDs(q DBTX, query string, args ...any) ([]syntax.DID, error) { return out, rows.Err() } +const maxIdleConns = 64 + func Make(ctx context.Context, dbPath string) (*DB, error) { logger := log.FromContext(ctx) logger = log.SubLogger(logger, "db") @@ -53,6 +55,8 @@ func Make(ctx context.Context, dbPath string) (*DB, error) { if err != nil { return nil, err } + // viewers that cancel mid-query would churn fds, so keep the idle conns around + db.SetMaxIdleConns(maxIdleConns) conn, err := db.Conn(ctx) if err != nil { diff --git a/spindle/db/fd_leak_test.go b/spindle/db/fd_leak_test.go new file mode 100644 index 000000000..078ee30cb --- /dev/null +++ b/spindle/db/fd_leak_test.go @@ -0,0 +1,64 @@ +package db + +import ( + "context" + "os" + "path/filepath" + "strings" + "sync" + "testing" + "time" +) + +func fdsOnFile(t *testing.T, path string) int { + t.Helper() + entries, err := os.ReadDir("/proc/self/fd") + if err != nil { + t.Skip("no /proc/self/fd here") + } + n := 0 + for _, e := range entries { + if target, err := os.Readlink(filepath.Join("/proc/self/fd", e.Name())); err == nil && target == path { + n++ + } + } + return n +} + +func TestClosedConnectionsReleaseTheirDescriptors(t *testing.T) { + ctx := context.Background() + path := filepath.Join(t.TempDir(), "spindle.db") + database, err := Make(ctx, path) + if err != nil { + t.Fatal(err) + } + t.Cleanup(func() { database.Close() }) + + var wg sync.WaitGroup + for i := range 30 { + wg.Add(1) + go func() { + defer wg.Done() + queryCtx, cancel := context.WithTimeout(ctx, 20*time.Millisecond) + defer cancel() + if i%2 == 0 { + var n int + _ = database.QueryRowContext(queryCtx, ` + with recursive c(x) as (select 1 union all select x + 1 from c where x < 5000000) + select count(*) from c`).Scan(&n) + return + } + rows, err := database.QueryContext(queryCtx, `select name from sqlite_master`) + if err == nil { + rows.Close() + } + }() + } + wg.Wait() + time.Sleep(200 * time.Millisecond) + + open := database.Stats().OpenConnections + if fds := fdsOnFile(t, strings.TrimSpace(path)); fds > open+1 { + t.Fatalf("%d descriptors on the db file with %d open connections, closed connections keep theirs", fds, open) + } +} -- 2.54.0