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.
This commit is contained in:
ayi21sui
2026-05-07 03:58:01 +00:00
committed by root
parent 1f393fc37f
commit 855c269fb8
4 changed files with 21 additions and 4 deletions
+5
View File
@@ -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 {
+8 -1
View File
@@ -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())
+6 -2
View File
@@ -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()
+2 -1
View File
@@ -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) {