Auto vacuum and manage connection

This commit is contained in:
Sung 2025-10-26 13:33:21 -07:00
commit 3ee060def4
6 changed files with 80 additions and 0 deletions

View file

@ -37,6 +37,7 @@ func (a *App) CreateBook(user database.User, name string) (database.Book, error)
uuid, err := helpers.GenUUID()
if err != nil {
tx.Rollback()
return database.Book{}, err
}

View file

@ -55,6 +55,7 @@ func (a *App) CreateNote(user database.User, bookUUID, content string, addedOn *
uuid, err := helpers.GenUUID()
if err != nil {
tx.Rollback()
return database.Note{}, err
}

View file

@ -66,9 +66,11 @@ func (a *App) CreateUser(email, password string, passwordConfirmation string) (d
var count int64
if err := tx.Model(&database.User{}).Where("email = ?", email).Count(&count).Error; err != nil {
tx.Rollback()
return database.User{}, pkgErrors.Wrap(err, "counting user")
}
if count > 0 {
tx.Rollback()
return database.User{}, ErrDuplicateEmail
}

View file

@ -22,10 +22,12 @@ import (
"fmt"
"net/http"
"os"
"time"
"github.com/dnote/dnote/pkg/server/buildinfo"
"github.com/dnote/dnote/pkg/server/config"
"github.com/dnote/dnote/pkg/server/controllers"
"github.com/dnote/dnote/pkg/server/database"
"github.com/dnote/dnote/pkg/server/log"
"github.com/pkg/errors"
)
@ -67,6 +69,12 @@ func startCmd(args []string) {
}
}()
// Start WAL checkpointing to prevent WAL file from growing unbounded.
database.StartWALCheckpointing(app.DB, 5*time.Minute)
// Start periodic VACUUM to reclaim space and defragment database.
database.StartPeriodicVacuum(app.DB, 24*time.Hour)
ctl := controllers.New(&app)
rc := controllers.RouteConfig{
WebRoutes: controllers.NewWebRoutes(&app, ctl),

View file

@ -203,11 +203,13 @@ func (b *Books) update(r *http.Request) (database.Book, error) {
var book database.Book
if err := tx.Where("user_id = ? AND uuid = ?", user.ID, uuid).First(&book).Error; err != nil {
tx.Rollback()
return database.Book{}, pkgErrors.Wrap(err, "finding book")
}
var params updateBookPayload
if err := parseRequestData(r, &params); err != nil {
tx.Rollback()
return database.Book{}, pkgErrors.Wrap(err, "decoding payload")
}
@ -253,11 +255,13 @@ func (b *Books) del(r *http.Request) (database.Book, error) {
var book database.Book
if err := tx.Where("user_id = ? AND uuid = ?", user.ID, uuid).First(&book).Error; err != nil {
tx.Rollback()
return database.Book{}, pkgErrors.Wrap(err, "finding a book")
}
var notes []database.Note
if err := tx.Where("book_uuid = ? AND NOT deleted", uuid).Order("usn ASC").Find(&notes).Error; err != nil {
tx.Rollback()
return database.Book{}, pkgErrors.Wrap(err, "finding notes for the book")
}
@ -270,6 +274,7 @@ func (b *Books) del(r *http.Request) (database.Book, error) {
book, err := b.app.DeleteBook(tx, *user, book)
if err != nil {
tx.Rollback()
return database.Book{}, pkgErrors.Wrap(err, "deleting the book")
}

View file

@ -21,6 +21,7 @@ package database
import (
"os"
"path/filepath"
"time"
"github.com/pkg/errors"
"gorm.io/driver/sqlite"
@ -58,5 +59,67 @@ func Open(dbPath string) *gorm.DB {
panic(errors.Wrap(err, "opening database conection"))
}
// Get underlying *sql.DB to configure connection pool
sqlDB, err := db.DB()
if err != nil {
panic(errors.Wrap(err, "getting underlying database connection"))
}
// Configure connection pool for SQLite with WAL mode
sqlDB.SetMaxOpenConns(25)
sqlDB.SetMaxIdleConns(5)
sqlDB.SetConnMaxLifetime(0) // Doesn't expire.
// Apply performance PRAGMAs
pragmas := []string{
"PRAGMA journal_mode=WAL", // Enable WAL mode for better concurrency
"PRAGMA synchronous=NORMAL", // Balance between safety and speed
"PRAGMA cache_size=-64000", // 64MB cache (negative = KB)
"PRAGMA busy_timeout=5000", // Wait up to 5s for locks
"PRAGMA foreign_keys=ON", // Enforce foreign key constraints
"PRAGMA temp_store=MEMORY", // Store temp tables in memory
}
for _, pragma := range pragmas {
if err := db.Exec(pragma).Error; err != nil {
panic(errors.Wrapf(err, "executing pragma: %s", pragma))
}
}
return db
}
// StartWALCheckpointing starts a background goroutine that periodically
// checkpoints the WAL file to prevent it from growing unbounded
func StartWALCheckpointing(db *gorm.DB, interval time.Duration) {
go func() {
ticker := time.NewTicker(interval)
defer ticker.Stop()
for range ticker.C {
// TRUNCATE mode removes the WAL file after checkpointing
if err := db.Exec("PRAGMA wal_checkpoint(TRUNCATE)").Error; err != nil {
// Log error but don't panic - this is a background maintenance task
// TODO: Use proper logging once available
_ = err
}
}
}()
}
// StartPeriodicVacuum runs full VACUUM on a schedule to reclaim space and defragment.
// WARNING: VACUUM acquires an exclusive lock and blocks all database operations briefly.
func StartPeriodicVacuum(db *gorm.DB, interval time.Duration) {
go func() {
ticker := time.NewTicker(interval)
defer ticker.Stop()
for range ticker.C {
if err := db.Exec("VACUUM").Error; err != nil {
// Log error but don't panic - this is a background maintenance task
// TODO: Use proper logging once available
_ = err
}
}
}()
}