From 6fc805491ed55ffaeb17404ee294d5a212f67581 Mon Sep 17 00:00:00 2001 From: benya Date: Thu, 4 Jun 2026 16:45:36 +0300 Subject: [PATCH] Prevent SQLite busy errors --- server/internal/store/sqlite.go | 14 ++++++++++ server/internal/store/sqlite_test.go | 39 ++++++++++++++++++++++++++++ 2 files changed, 53 insertions(+) diff --git a/server/internal/store/sqlite.go b/server/internal/store/sqlite.go index 24f9986..20caa7f 100644 --- a/server/internal/store/sqlite.go +++ b/server/internal/store/sqlite.go @@ -54,6 +54,20 @@ func OpenSQLite(ctx context.Context, path string) (*Store, error) { return nil, err } + // SQLite permits one writer at a time. Keep access serialized inside this + // process and wait briefly for locks held during startup or maintenance. + db.SetMaxOpenConns(1) + db.SetMaxIdleConns(1) + for _, stmt := range []string{ + `PRAGMA busy_timeout = 5000`, + `PRAGMA journal_mode = WAL`, + } { + if _, err := db.ExecContext(ctx, stmt); err != nil { + _ = db.Close() + return nil, err + } + } + s := &Store{db: db} if err := s.Migrate(ctx); err != nil { _ = db.Close() diff --git a/server/internal/store/sqlite_test.go b/server/internal/store/sqlite_test.go index 9790d19..7e937ab 100644 --- a/server/internal/store/sqlite_test.go +++ b/server/internal/store/sqlite_test.go @@ -4,6 +4,7 @@ import ( "context" "database/sql" "path/filepath" + "sync" "testing" _ "modernc.org/sqlite" @@ -63,3 +64,41 @@ VALUES ('rs_legacy', 'dev_legacy'); t.Fatalf("got target %q, want ssh", sessions[0].Target) } } + +func TestSQLiteConcurrentAccessDoesNotReturnBusy(t *testing.T) { + ctx := context.Background() + st, err := OpenSQLite(ctx, filepath.Join(t.TempDir(), "concurrent.db")) + if err != nil { + t.Fatal(err) + } + defer st.Close() + + enrolled, err := st.EnrollDevice(ctx, "router", "OpenWrt") + if err != nil { + t.Fatal(err) + } + + var wg sync.WaitGroup + errs := make(chan error, 80) + for range 40 { + wg.Add(2) + go func() { + defer wg.Done() + _, _, err := st.ListRemoteSessions(ctx, enrolled.DeviceID, RemoteSessionListOptions{}) + errs <- err + }() + go func() { + defer wg.Done() + _, err := st.SaveHeartbeat(ctx, enrolled.DeviceID, []byte(`{"hostname":"router"}`), []byte(`{}`)) + errs <- err + }() + } + wg.Wait() + close(errs) + + for err := range errs { + if err != nil { + t.Fatal(err) + } + } +}