Skip to content

Commit 71664f8

Browse files
authored
fix(kine): remove corrupt WAL artefacts on startup to break boot loop (#148)
1 parent abfc0aa commit 71664f8

5 files changed

Lines changed: 87 additions & 2 deletions

File tree

cmd/kubesolo/main.go

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,7 @@ type kubesolo struct {
5050
localStorageSharedPath string
5151
fullMode bool
5252
disableIPv6 bool
53+
dbWALRepair bool
5354
embedded types.Embedded
5455
}
5556

@@ -78,6 +79,7 @@ func service() (*kubesolo, error) {
7879
localStorageSharedPath: *flags.LocalStorageSharedPath,
7980
fullMode: *flags.Full,
8081
disableIPv6: *flags.DisableIPv6,
82+
dbWALRepair: *flags.DBWALRepair,
8183
}, nil
8284
}
8385

@@ -162,7 +164,7 @@ func (s *kubesolo) run() {
162164
{
163165
name: "kine",
164166
start: func() {
165-
kineService := kine.NewService(ctx, cancel, s.embedded.KineDir, kineReadyCh)
167+
kineService := kine.NewService(ctx, cancel, s.embedded.KineDir, kineReadyCh, s.dbWALRepair)
166168
s.wg.Go(func() {
167169
kineService.Run()
168170
})

internal/config/flags/flags.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,5 +26,6 @@ var (
2626
Debug = Application.Flag("debug", "Enable debug logging. Defaults to false.").Envar("KUBESOLO_DEBUG").Default("false").Bool()
2727
PprofServer = Application.Flag("pprof-server", "Enable pprof server. Defaults to false.").Envar("KUBESOLO_PPROF_SERVER").Default("false").Bool()
2828
Full = Application.Flag("full", "Disable memory-saving overrides and use upstream Kubernetes defaults. Kubesolo still uses NodeSetter in favour of the scheduler. Recommended for CI and developer environments where memory is not constrained. Leave unset for edge deployments.").Envar("KUBESOLO_FULL").Default("false").Bool()
29+
DBWALRepair = Application.Flag("db-wal-repair", "On startup, run an integrity check against the SQLite database and remove WAL artefacts (state.db-wal, state.db-shm) if corruption is detected. Recovers from unclean shutdowns caused by power loss. Defaults to false.").Envar("KUBESOLO_DB_WAL_REPAIR").Default("false").Bool()
2930
DisableIPv6 = Application.Flag("disable-ipv6", "Disable IPv6 support. When set, CoreDNS will not serve ip6.arpa reverse zones and kubelet will register with an explicit IPv4 node address. Defaults to false.").Envar("KUBESOLO_DISABLE_IPV6").Default("false").Bool()
3031
)

pkg/kine/executor.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,8 @@ func (s *service) Run() error {
2525
return err
2626
}
2727

28+
s.repairWALIfCorrupt()
29+
2830
if err := kubesoloservice.RunServiceWithStartupCheck(func() error {
2931
log.Debug().Str("component", "kine").Msg("starting kine server...")
3032
_, err := endpoint.Listen(s.ctx, s.generateKineConfig())

pkg/kine/repair.go

Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
package kine
2+
3+
import (
4+
"context"
5+
"database/sql"
6+
"os"
7+
"path/filepath"
8+
"time"
9+
10+
_ "github.com/mattn/go-sqlite3"
11+
"github.com/rs/zerolog/log"
12+
)
13+
14+
// repairWALIfCorrupt runs a quick integrity check against the kine SQLite
15+
// database. If the check fails (or the DB cannot be opened at all) and
16+
// --db-wal-repair is enabled, the WAL artefacts (state.db-wal, state.db-shm)
17+
// are removed so that SQLite falls back to the last cleanly checkpointed state.
18+
// Without the flag, a fatal log is emitted with instructions for manual recovery
19+
// so that operators are never silently left in a broken boot loop.
20+
func (s *service) repairWALIfCorrupt() {
21+
dbPath := filepath.Join(s.databaseDir, "state.db")
22+
23+
if _, err := os.Stat(dbPath); os.IsNotExist(err) {
24+
return
25+
}
26+
27+
db, err := sql.Open("sqlite3", dbPath)
28+
if err != nil {
29+
s.handleCorruption(dbPath, "cannot open SQLite DB for integrity check: %v", err)
30+
return
31+
}
32+
defer db.Close()
33+
34+
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
35+
defer cancel()
36+
37+
rows, err := db.QueryContext(ctx, "PRAGMA quick_check")
38+
if err != nil {
39+
s.handleCorruption(dbPath, "SQLite quick_check query failed: %v", err)
40+
return
41+
}
42+
defer rows.Close()
43+
44+
if rows.Next() {
45+
var result string
46+
if rows.Scan(&result) == nil && result == "ok" {
47+
log.Debug().Str("component", "kine").Msg("SQLite integrity check passed")
48+
return
49+
}
50+
}
51+
52+
s.handleCorruption(dbPath, "SQLite integrity check failed")
53+
}
54+
55+
func (s *service) handleCorruption(dbPath string, format string, args ...any) {
56+
if s.dbWALRepair {
57+
log.Warn().Str("component", "kine").Msgf(format+", removing WAL artefacts to recover from unclean shutdown", args...)
58+
removeWALArtefacts(dbPath)
59+
return
60+
}
61+
62+
log.Fatal().Str("component", "kine").Msgf(
63+
format+". The SQLite WAL artefacts may be corrupt after an unclean shutdown. "+
64+
"Remove %s-wal and %s-shm manually, or restart with --db-wal-repair to remove them automatically.",
65+
append(args, dbPath, dbPath)...,
66+
)
67+
}
68+
69+
func removeWALArtefacts(dbPath string) {
70+
for _, suffix := range []string{"-wal", "-shm"} {
71+
path := dbPath + suffix
72+
if err := os.Remove(path); err != nil && !os.IsNotExist(err) {
73+
log.Warn().Str("component", "kine").Msgf("failed to remove %s: %v", path, err)
74+
} else if err == nil {
75+
log.Info().Str("component", "kine").Msgf("removed %s", path)
76+
}
77+
}
78+
}

pkg/kine/service.go

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,14 +12,16 @@ type service struct {
1212
kineReady chan struct{}
1313
ctx context.Context
1414
cancel context.CancelFunc
15+
dbWALRepair bool
1516
}
1617

1718
// NewService creates a new kine service
18-
func NewService(ctx context.Context, cancel context.CancelFunc, databaseDir string, kineReady chan struct{}) *service {
19+
func NewService(ctx context.Context, cancel context.CancelFunc, databaseDir string, kineReady chan struct{}, dbWALRepair bool) *service {
1920
return &service{
2021
databaseDir: databaseDir,
2122
kineReady: kineReady,
2223
ctx: ctx,
2324
cancel: cancel,
25+
dbWALRepair: dbWALRepair,
2426
}
2527
}

0 commit comments

Comments
 (0)