Compare commits

..
1 Commits
Author SHA1 Message Date
sneak cdc0d33da7 Log the start-up checkpoint's figures under their own names (closes #117)
check / check (push) Canceled after 0s
Initialize scanned the row of PRAGMA wal_checkpoint(TRUNCATE), which is
the busy flag, the frames in the WAL and the frames copied, into
wal_pages, checkpointed and moved; and a completed TRUNCATE reads 0 for
both counts. The line now logs busy, wal_frames and checkpointed_frames,
plus wal_bytes, the WAL file's size read before the checkpoint, and is a
warning when busy is 1. Tests open a database a second time while the
first stays open, with and without a read open on the first, and check
the line's figures.

Model: opus-5-5
2026-10-20 02:08:51 +00:00
2 changed files with 24 additions and 32 deletions

No files matched your search

+16 -21
View File
@@ -169,20 +169,6 @@ func New(cfg *config.Config, logger *logger.Logger) (*Database, error) {
return nil, fmt.Errorf("failed to create database directory: %w", err) return nil, fmt.Errorf("failed to create database directory: %w", err)
} }
// The size of the WAL file the previous run left, which Initialize logs
// with the start-up checkpoint. It is read before the database is opened,
// because opening it writes a frame to the WAL. There is no WAL file when
// the database is new or the last run closed the database, which deletes
// it.
var walBytes int64
wal, err := os.Stat(dbPath + walFileSuffix)
if err == nil {
walBytes = wal.Size()
} else if !errors.Is(err, os.ErrNotExist) {
logger.Warn("Failed to read the WAL file's size on startup", "error", err)
}
// Per-connection SQLite settings go in the DSN so every pooled connection // Per-connection SQLite settings go in the DSN so every pooled connection
// gets them, not just the one that runs the Initialize pragmas. _txlock= // gets them, not just the one that runs the Initialize pragmas. _txlock=
// immediate makes every transaction take the write lock at BEGIN. Without it // immediate makes every transaction take the write lock at BEGIN. Without it
@@ -243,7 +229,7 @@ func New(cfg *config.Config, logger *logger.Logger) (*Database, error) {
counts: &liveCounts{}, counts: &liveCounts{},
} }
err = database.Initialize(walBytes) err = database.Initialize()
if err != nil { if err != nil {
return nil, fmt.Errorf("failed to initialize database: %w", err) return nil, fmt.Errorf("failed to initialize database: %w", err)
} }
@@ -265,10 +251,8 @@ const (
startupCheckpointIncompleteMessage = "WAL checkpoint on startup incomplete" startupCheckpointIncompleteMessage = "WAL checkpoint on startup incomplete"
) )
// Initialize creates the database schema if it doesn't exist. walBytes is the // Initialize creates the database schema if it doesn't exist.
// size of the WAL file the previous run left, which New reads before it opens func (d *Database) Initialize() error {
// the database.
func (d *Database) Initialize(walBytes int64) error {
// Set SQLite pragmas for performance. Per-connection settings (cache_size, // Set SQLite pragmas for performance. Per-connection settings (cache_size,
// synchronous, busy_timeout, journal_mode) live in the DSN, and New sets the // synchronous, busy_timeout, journal_mode) live in the DSN, and New sets the
// writing connection's cache_size; temp_store is left at its default so // writing connection's cache_size; temp_store is left at its default so
@@ -290,10 +274,21 @@ func (d *Database) Initialize(walBytes int64) error {
// The start-up checkpoint copies the WAL a previous run left into the // The start-up checkpoint copies the WAL a previous run left into the
// database file and empties the WAL file; its frame counts then read 0, // database file and empties the WAL file; its frame counts then read 0,
// so walBytes is logged with them. // so the WAL file's size is read first. There is no WAL file when the
// database is new or the last run closed the database, which deletes it.
var walBytes int64
wal, err := os.Stat(d.path + walFileSuffix)
if err == nil {
walBytes = wal.Size()
} else if !errors.Is(err, os.ErrNotExist) {
d.logger.Warn("Failed to read the WAL file's size on startup",
"error", err)
}
var busy, walFrames, checkpointedFrames int var busy, walFrames, checkpointedFrames int
err := d.writer.QueryRowContext(context.Background(), err = d.writer.QueryRowContext(context.Background(),
"PRAGMA wal_checkpoint(TRUNCATE)", "PRAGMA wal_checkpoint(TRUNCATE)",
).Scan(&busy, &walFrames, &checkpointedFrames) ).Scan(&busy, &walFrames, &checkpointedFrames)
+8 -11
View File
@@ -1057,9 +1057,10 @@ func walFileSize(t *testing.T, db *Database) int64 {
// TestStartupCheckpointLogsItsFigures opens a new database, writes a route, // TestStartupCheckpointLogsItsFigures opens a new database, writes a route,
// and opens the same file again while the first stays open, so that its WAL // and opens the same file again while the first stays open, so that its WAL
// file stays. The first open finds no WAL file: it logs a size of 0 and no // file stays. The first open finds no WAL file: it logs a size of 0 and no
// warning. The second logs the size of the WAL file the first left, and the // warning. The second logs the size of the WAL file it found, at least what
// checkpoint's busy flag and frame counts, each under its own name; the // the first left (opening the file writes to the WAL before the size is
// counts read 0 once the checkpoint has emptied the file. // read), and the checkpoint's busy flag and frame counts, each under its own
// name; the counts read 0 once the checkpoint has emptied the file.
// Initialize logged the busy flag as wal_pages and the counts under each // Initialize logged the busy flag as wal_pages and the counts under each
// other's names (https://git.eeqj.de/sneak/routewatch/issues/117). // other's names (https://git.eeqj.de/sneak/routewatch/issues/117).
func TestStartupCheckpointLogsItsFigures(t *testing.T) { func TestStartupCheckpointLogsItsFigures(t *testing.T) {
@@ -1102,9 +1103,10 @@ func TestStartupCheckpointLogsItsFigures(t *testing.T) {
} }
} }
if line["wal_bytes"] != float64(walBytes) { loggedBytes, _ := line["wal_bytes"].(float64)
t.Errorf("start-up checkpoint logged wal_bytes = %v, want the %d "+ if loggedBytes < float64(walBytes) {
"bytes the first database left", line["wal_bytes"], walBytes) t.Errorf("start-up checkpoint logged wal_bytes = %v, want at least "+
"the %d bytes the first database left", line["wal_bytes"], walBytes)
} }
} }
@@ -1154,11 +1156,6 @@ func TestStartupCheckpointWarnsWhenIncomplete(t *testing.T) {
_, lines := openLogged(t, cfg) _, lines := openLogged(t, cfg)
line := findLine(t, lines, startupCheckpointIncompleteMessage) line := findLine(t, lines, startupCheckpointIncompleteMessage)
if line[slog.LevelKey] != slog.LevelWarn.String() {
t.Errorf("start-up checkpoint logged at level %v, want %s",
line[slog.LevelKey], slog.LevelWarn)
}
if line["busy"] != 1.0 { if line["busy"] != 1.0 {
t.Errorf("start-up checkpoint logged busy = %v, want 1", line["busy"]) t.Errorf("start-up checkpoint logged busy = %v, want 1", line["busy"])
} }