From 855c269fb8c1ca2a3226df73148042bbe5189e93 Mon Sep 17 00:00:00 2001 From: ayi21sui Date: Thu, 7 May 2026 03:58:01 +0000 Subject: [PATCH] fix: SQLite connection leak causing unbounded memory growth SaveStats returned tx.Create().Error directly without assigning to the local err variable, so the deferred closure always saw err==nil and called Commit() on a failed transaction. This left the underlying go-sqlite3 connection unreturned to the pool. With StatsJob firing every 10s, leaked connections accumulated ~150 KB each, reaching 400+ MB after ~33 hours. Fixes: - stats.go: assign Create result to err so defer can Rollback on failure - backup.go: defer-close backupDb to prevent pool leak on early return - migration/main.go: defer-close migration db - db.go: add ConnMaxIdleTime(5m), lower MaxIdleConns to 2, set _cache_size=-200 to reduce per-connection memory from ~2 MB to ~200 KB Measured: RSS dropped from 419 MB to 66 MB, db file descriptors from 4484 to 6, with zero growth over 3-minute observation window. --- cmd/migration/main.go | 5 +++++ database/backup.go | 9 ++++++++- database/db.go | 8 ++++++-- service/stats.go | 3 ++- 4 files changed, 21 insertions(+), 4 deletions(-) diff --git a/cmd/migration/main.go b/cmd/migration/main.go index b841342..3e70ca2 100644 --- a/cmd/migration/main.go +++ b/cmd/migration/main.go @@ -25,6 +25,11 @@ func MigrateDb() { log.Fatal(err) return } + defer func() { + if sqlDB, e := db.DB(); e == nil { + _ = sqlDB.Close() + } + }() tx := db.Begin() defer func() { if err == nil { diff --git a/database/backup.go b/database/backup.go index 69bb43d..61a6db6 100644 --- a/database/backup.go +++ b/database/backup.go @@ -42,6 +42,11 @@ func GetDb(exclude string) ([]byte, error) { if err != nil { return nil, err } + defer func() { + if sqlDB, e := backupDb.DB(); e == nil { + _ = sqlDB.Close() + } + }() defer os.Remove(dbPath) err = backupDb.AutoMigrate( @@ -218,7 +223,9 @@ func ImportDB(file multipart.File) error { return common.NewErrorf("Error checking db: %v", err) } newDb_db, _ := newDb.DB() - newDb_db.Close() + if newDb_db != nil { + newDb_db.Close() + } // Backup the current database for fallback fallbackPath := fmt.Sprintf("%s.backup", config.GetDBPath()) diff --git a/database/db.go b/database/db.go index b302775..1af09ed 100644 --- a/database/db.go +++ b/database/db.go @@ -55,7 +55,10 @@ func OpenDB(dbPath string) error { if strings.Contains(dbPath, "?") { sep = "&" } - dsn := dbPath + sep + "_busy_timeout=10000&_journal_mode=WAL" + // _cache_size=-200 caps each connection's page cache at ~200 KiB + // (default is ~2 MiB), reducing memory amplification if a connection + // escapes the pool. + dsn := dbPath + sep + "_busy_timeout=10000&_journal_mode=WAL&_cache_size=-200" db, err = gorm.Open(sqlite.Open(dsn), c) if err != nil { return err @@ -66,8 +69,9 @@ func OpenDB(dbPath string) error { return err } sqlDB.SetMaxOpenConns(25) - sqlDB.SetMaxIdleConns(5) + sqlDB.SetMaxIdleConns(2) sqlDB.SetConnMaxLifetime(time.Hour) + sqlDB.SetConnMaxIdleTime(5 * time.Minute) if config.IsDebug() { db = db.Debug() diff --git a/service/stats.go b/service/stats.go index 35cdfc7..6234b97 100644 --- a/service/stats.go +++ b/service/stats.go @@ -83,7 +83,8 @@ func (s *StatsService) SaveStats(enableTraffic bool) error { if !enableTraffic { return nil } - return tx.Create(&stats).Error + err = tx.Create(&stats).Error + return err } func (s *StatsService) GetStats(resource string, tag string, limit int) ([]model.Stats, error) {