Skip to content

Commit 36a2ccf

Browse files
dilyevskyclaude
andcommitted
[apiserver] drop litestream recovery artifacts from kine DB
The lost_and_found tables created by sqlite3 .recover (via litestream-recover) were baked into the litestream replica and restored on every pod restart, bloating the DB (~97MB of dead data). Drop these tables in enableAutoVacuum before VACUUM so the space is reclaimed. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent 8abb52b commit 36a2ccf

File tree

2 files changed

+120
-0
lines changed

2 files changed

+120
-0
lines changed

pkg/apiserver/storage.go

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,29 @@ func enableAutoVacuum(path string) error {
6464
if _, err := db.Exec("PRAGMA auto_vacuum = INCREMENTAL"); err != nil {
6565
return fmt.Errorf("setting auto_vacuum: %w", err)
6666
}
67+
68+
// Drop litestream recovery artifacts (lost_and_found tables from sqlite3 .recover).
69+
rows, err := db.Query("SELECT name FROM sqlite_master WHERE type='table' AND name LIKE 'lost_and_found%'")
70+
if err != nil {
71+
return fmt.Errorf("querying lost_and_found tables: %w", err)
72+
}
73+
var toDrop []string
74+
for rows.Next() {
75+
var name string
76+
if err := rows.Scan(&name); err != nil {
77+
rows.Close()
78+
return fmt.Errorf("scanning table name: %w", err)
79+
}
80+
toDrop = append(toDrop, name)
81+
}
82+
rows.Close()
83+
for _, name := range toDrop {
84+
slog.Info("Dropping litestream recovery artifact", "table", name)
85+
if _, err := db.Exec("DROP TABLE IF EXISTS " + name); err != nil {
86+
return fmt.Errorf("dropping table %s: %w", name, err)
87+
}
88+
}
89+
6790
// VACUUM is required to convert the database to the new auto_vacuum mode.
6891
if _, err := db.Exec("VACUUM"); err != nil {
6992
return fmt.Errorf("vacuuming database: %w", err)

pkg/apiserver/storage_test.go

Lines changed: 97 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,97 @@
1+
package apiserver
2+
3+
import (
4+
"database/sql"
5+
"os"
6+
"path/filepath"
7+
"testing"
8+
9+
_ "github.com/mattn/go-sqlite3"
10+
)
11+
12+
func createBloatedDB(t *testing.T, path string) int64 {
13+
t.Helper()
14+
db, err := sql.Open("sqlite3", path)
15+
if err != nil {
16+
t.Fatal(err)
17+
}
18+
defer db.Close()
19+
20+
if _, err := db.Exec("CREATE TABLE t(k TEXT, v BLOB)"); err != nil {
21+
t.Fatal(err)
22+
}
23+
for i := 0; i < 1000; i++ {
24+
if _, err := db.Exec("INSERT INTO t VALUES (?, randomblob(4096))", i); err != nil {
25+
t.Fatal(err)
26+
}
27+
}
28+
if _, err := db.Exec("DELETE FROM t WHERE k != '0'"); err != nil {
29+
t.Fatal(err)
30+
}
31+
32+
// Simulate litestream recovery artifacts (sqlite3 .recover output).
33+
if _, err := db.Exec("CREATE TABLE lost_and_found(rootpgno INT, pgno INT, nfield INT, id INT, c0, c1, c2, c3)"); err != nil {
34+
t.Fatal(err)
35+
}
36+
if _, err := db.Exec("CREATE TABLE lost_and_found_0(rootpgno INT, pgno INT, nfield INT, id INT, c0)"); err != nil {
37+
t.Fatal(err)
38+
}
39+
for i := 0; i < 500; i++ {
40+
if _, err := db.Exec("INSERT INTO lost_and_found VALUES (?, ?, 4, ?, randomblob(1024), randomblob(1024), randomblob(1024), randomblob(1024))", i, i, i); err != nil {
41+
t.Fatal(err)
42+
}
43+
if _, err := db.Exec("INSERT INTO lost_and_found_0 VALUES (?, ?, 1, ?, randomblob(2048))", i, i, i); err != nil {
44+
t.Fatal(err)
45+
}
46+
}
47+
48+
fi, err := os.Stat(path)
49+
if err != nil {
50+
t.Fatal(err)
51+
}
52+
return fi.Size()
53+
}
54+
55+
func TestEnableAutoVacuum(t *testing.T) {
56+
dbPath := filepath.Join(t.TempDir(), "test.db")
57+
before := createBloatedDB(t, dbPath)
58+
t.Logf("before: %d bytes", before)
59+
60+
if err := enableAutoVacuum(dbPath); err != nil {
61+
t.Fatalf("enableAutoVacuum: %v", err)
62+
}
63+
64+
db, err := sql.Open("sqlite3", dbPath)
65+
if err != nil {
66+
t.Fatal(err)
67+
}
68+
defer db.Close()
69+
70+
var mode int
71+
if err := db.QueryRow("PRAGMA auto_vacuum").Scan(&mode); err != nil {
72+
t.Fatal(err)
73+
}
74+
if mode != 2 {
75+
t.Errorf("auto_vacuum = %d, want 2", mode)
76+
}
77+
78+
// Verify lost_and_found tables were dropped.
79+
var lostCount int
80+
if err := db.QueryRow("SELECT COUNT(*) FROM sqlite_master WHERE type='table' AND name LIKE 'lost_and_found%'").Scan(&lostCount); err != nil {
81+
t.Fatal(err)
82+
}
83+
if lostCount != 0 {
84+
t.Errorf("lost_and_found tables remain: %d", lostCount)
85+
}
86+
87+
fi, err := os.Stat(dbPath)
88+
if err != nil {
89+
t.Fatal(err)
90+
}
91+
after := fi.Size()
92+
t.Logf("after: %d bytes", after)
93+
94+
if after >= before {
95+
t.Errorf("DB did not shrink: before=%d after=%d", before, after)
96+
}
97+
}

0 commit comments

Comments
 (0)