chore(db): use shorter read transactions and periodic checkpoint for smaller WAL (#10027)

Also make sure our journal size limit is in effect when checkpointing.
This commit is contained in:
Jakob Borg
2025-04-02 13:19:34 -07:00
committed by GitHub
parent 4096a35b86
commit 82a0dd8eaa
5 changed files with 53 additions and 24 deletions

View File

@@ -20,6 +20,7 @@ type DB struct {
sql *sqlx.DB
localDeviceIdx int64
updateLock sync.Mutex
updatePoints int
statementsMut sync.RWMutex
statements map[string]*sqlx.Stmt

View File

@@ -36,10 +36,6 @@ func Open(path string) (*DB, error) {
// https://www.sqlite.org/pragma.html#pragma_optimize
return nil, wrap(err, "PRAGMA optimize")
}
if _, err := sqlDB.Exec(`PRAGMA journal_size_limit = 67108864`); err != nil {
// https://www.powersync.com/blog/sqlite-optimizations-for-ultra-high-performance
return nil, wrap(err, "PRAGMA journal_size_limit")
}
return openCommon(sqlDB)
}

View File

@@ -86,10 +86,16 @@ func (s *Service) periodic(ctx context.Context) error {
return wrap(err)
}
_, _ = s.sdb.sql.ExecContext(ctx, `ANALYZE`)
_, _ = s.sdb.sql.ExecContext(ctx, `PRAGMA optimize`)
_, _ = s.sdb.sql.ExecContext(ctx, `PRAGMA incremental_vacuum`)
_, _ = s.sdb.sql.ExecContext(ctx, `PRAGMA wal_checkpoint(TRUNCATE)`)
conn, err := s.sdb.sql.Conn(ctx)
if err != nil {
return wrap(err)
}
defer conn.Close()
_, _ = conn.ExecContext(ctx, `ANALYZE`)
_, _ = conn.ExecContext(ctx, `PRAGMA optimize`)
_, _ = conn.ExecContext(ctx, `PRAGMA incremental_vacuum`)
_, _ = conn.ExecContext(ctx, `PRAGMA journal_size_limit = 67108864`)
_, _ = conn.ExecContext(ctx, `PRAGMA wal_checkpoint(TRUNCATE)`)
return nil
}

View File

@@ -23,6 +23,13 @@ import (
"google.golang.org/protobuf/proto"
)
const (
// Arbitrarily chosen values for checkpoint frequency....
updatePointsPerFile = 100
updatePointsPerBlock = 1
updatePointsThreshold = 250_000
)
func (s *DB) Update(folder string, device protocol.DeviceID, fs []protocol.FileInfo) error {
s.updateLock.Lock()
defer s.updateLock.Unlock()
@@ -143,7 +150,12 @@ func (s *DB) Update(folder string, device protocol.DeviceID, fs []protocol.FileI
}
}
return wrap(tx.Commit())
if err := tx.Commit(); err != nil {
return wrap(err)
}
s.periodicCheckpointLocked(fs)
return nil
}
func (s *DB) DropFolder(folder string) error {
@@ -554,3 +566,30 @@ func (e fileRow) Compare(other fileRow) int {
return 0
}
}
func (s *DB) periodicCheckpointLocked(fs []protocol.FileInfo) {
// Induce periodic checkpoints. We add points for each file and block,
// and checkpoint when we've written more than a threshold of points.
// This ensures we do not go too long without a checkpoint, while also
// not doing it incessantly for every update.
s.updatePoints += updatePointsPerFile * len(fs)
for _, f := range fs {
s.updatePoints += len(f.Blocks) * updatePointsPerBlock
}
if s.updatePoints > updatePointsThreshold {
l.Debugln("checkpoint at", s.updatePoints)
conn, err := s.sql.Conn(context.Background())
if err != nil {
l.Debugln("conn:", err)
return
}
defer conn.Close()
if _, err := conn.ExecContext(context.Background(), `PRAGMA journal_size_limit = 67108864`); err != nil {
l.Debugln("PRAGMA journal_size_limit(RESTART):", err)
}
if _, err := conn.ExecContext(context.Background(), `PRAGMA wal_checkpoint(RESTART)`); err != nil {
l.Debugln("PRAGMA wal_checkpoint(RESTART):", err)
}
s.updatePoints = 0
}
}

View File

@@ -295,7 +295,6 @@ func (s *indexHandler) sendIndexTo(ctx context.Context) error {
var f protocol.FileInfo
previousWasDelete := false
t0 := time.Now()
for fi, err := range itererr.Zip(s.sdb.AllLocalFilesBySequence(s.folder, protocol.LocalDeviceID, s.localPrevSequence+1, 5000)) {
if err != nil {
return err
@@ -304,15 +303,7 @@ func (s *indexHandler) sendIndexTo(ctx context.Context) error {
// Even if the batch is full, we allow a last delete to slip in, we do this by making sure that
// the batch ends with a non-delete, or that the last item in the batch is already a delete
if batch.Full() && (!fi.IsDeleted() || previousWasDelete) {
if err := batch.Flush(); err != nil {
return err
}
if time.Since(t0) > 5*time.Second {
// minor hack -- avoid very long running read transactions
// during index transmission, to help prevent excessive
// growth of database WAL file
break
}
break
}
if fi.SequenceNo() < s.localPrevSequence+1 {
@@ -348,11 +339,7 @@ func (s *indexHandler) sendIndexTo(ctx context.Context) error {
batch.Append(f)
}
if err := batch.Flush(); err != nil {
return err
}
return nil
return batch.Flush()
}
func (s *indexHandler) receive(fs []protocol.FileInfo, update bool, op string, prevSequence, lastSequence int64) error {