Skip to content

Commit cb44be6

Browse files
committed
Force truncation checkpoint if WAL becomes runaway
A failsafe to force WAL truncation if somehow between normal checkpoints the WAL grows to extreme sizes. Default is around 200 megabytes with a 4k page size.
1 parent ac9ad40 commit cb44be6

File tree

1 file changed

+48
-28
lines changed

1 file changed

+48
-28
lines changed

db.go

+48-28
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ const (
3131
DefaultCheckpointInterval = 1 * time.Minute
3232
DefaultMinCheckpointPageN = 1000
3333
DefaultMaxCheckpointPageN = 10000
34+
DefaultTruncatePageN = 500000
3435
)
3536

3637
// MaxIndex is the maximum possible WAL index.
@@ -83,6 +84,16 @@ type DB struct {
8384
// unbounded if there are always read transactions occurring.
8485
MaxCheckpointPageN int
8586

87+
// Threshold of WAL size, in pages, before a forced truncation checkpoint.
88+
// A forced truncation checkpoint will block new transactions and wait for
89+
// existing transactions to finish before issuing a checkpoint and
90+
// truncating the WAL.
91+
//
92+
// If zero, no truncates are forced. This can cause the WAL to grow
93+
// unbounded if there's a sudden spike of changes between other
94+
// checkpoints.
95+
TruncatePageN int
96+
8697
// Time between automatic checkpoints in the WAL. This is done to allow
8798
// more fine-grained WAL files so that restores can be performed with
8899
// better precision.
@@ -104,6 +115,7 @@ func NewDB(path string) *DB {
104115

105116
MinCheckpointPageN: DefaultMinCheckpointPageN,
106117
MaxCheckpointPageN: DefaultMaxCheckpointPageN,
118+
TruncatePageN: DefaultTruncatePageN,
107119
CheckpointInterval: DefaultCheckpointInterval,
108120
MonitorInterval: DefaultMonitorInterval,
109121
}
@@ -740,7 +752,7 @@ func (db *DB) Sync(ctx context.Context) (err error) {
740752
}
741753

742754
// Synchronize real WAL with current shadow WAL.
743-
newWALSize, err := db.syncWAL(info)
755+
origWALSize, newWALSize, err := db.syncWAL(info)
744756
if err != nil {
745757
return fmt.Errorf("sync wal: %w", err)
746758
}
@@ -749,7 +761,9 @@ func (db *DB) Sync(ctx context.Context) (err error) {
749761
// If WAL size is greater than min threshold, attempt checkpoint.
750762
var checkpoint bool
751763
checkpointMode := CheckpointModePassive
752-
if db.MaxCheckpointPageN > 0 && newWALSize >= calcWALSize(db.pageSize, db.MaxCheckpointPageN) {
764+
if db.TruncatePageN > 0 && origWALSize >= calcWALSize(db.pageSize, db.TruncatePageN) {
765+
checkpoint, checkpointMode = true, CheckpointModeTruncate
766+
} else if db.MaxCheckpointPageN > 0 && newWALSize >= calcWALSize(db.pageSize, db.MaxCheckpointPageN) {
753767
checkpoint, checkpointMode = true, CheckpointModeRestart
754768
} else if newWALSize >= calcWALSize(db.pageSize, db.MinCheckpointPageN) {
755769
checkpoint = true
@@ -908,29 +922,29 @@ type syncInfo struct {
908922
}
909923

910924
// syncWAL copies pending bytes from the real WAL to the shadow WAL.
911-
func (db *DB) syncWAL(info syncInfo) (newSize int64, err error) {
925+
func (db *DB) syncWAL(info syncInfo) (origSize int64, newSize int64, err error) {
912926
// Copy WAL starting from end of shadow WAL. Exit if no new shadow WAL needed.
913-
newSize, err = db.copyToShadowWAL(info.shadowWALPath)
927+
origSize, newSize, err = db.copyToShadowWAL(info.shadowWALPath)
914928
if err != nil {
915-
return newSize, fmt.Errorf("cannot copy to shadow wal: %w", err)
929+
return origSize, newSize, fmt.Errorf("cannot copy to shadow wal: %w", err)
916930
} else if !info.restart {
917-
return newSize, nil // If no restart required, exit.
931+
return origSize, newSize, nil // If no restart required, exit.
918932
}
919933

920934
// Parse index of current shadow WAL file.
921935
dir, base := filepath.Split(info.shadowWALPath)
922936
index, err := ParseWALPath(base)
923937
if err != nil {
924-
return 0, fmt.Errorf("cannot parse shadow wal filename: %s", base)
938+
return 0, 0, fmt.Errorf("cannot parse shadow wal filename: %s", base)
925939
}
926940

927941
// Start a new shadow WAL file with next index.
928942
newShadowWALPath := filepath.Join(dir, FormatWALPath(index+1))
929943
newSize, err = db.initShadowWALFile(newShadowWALPath)
930944
if err != nil {
931-
return 0, fmt.Errorf("cannot init shadow wal file: name=%s err=%w", newShadowWALPath, err)
945+
return 0, 0, fmt.Errorf("cannot init shadow wal file: name=%s err=%w", newShadowWALPath, err)
932946
}
933-
return newSize, nil
947+
return origSize, newSize, nil
934948
}
935949

936950
func (db *DB) initShadowWALFile(filename string) (int64, error) {
@@ -966,58 +980,64 @@ func (db *DB) initShadowWALFile(filename string) (int64, error) {
966980
_ = os.Chown(filename, uid, gid)
967981

968982
// Copy as much shadow WAL as available.
969-
newSize, err := db.copyToShadowWAL(filename)
983+
_, newSize, err := db.copyToShadowWAL(filename)
970984
if err != nil {
971985
return 0, fmt.Errorf("cannot copy to new shadow wal: %w", err)
972986
}
973987
return newSize, nil
974988
}
975989

976-
func (db *DB) copyToShadowWAL(filename string) (newSize int64, err error) {
990+
func (db *DB) copyToShadowWAL(filename string) (origWalSize int64, newSize int64, err error) {
977991
Tracef("%s: copy-shadow: %s", db.path, filename)
978992

979993
r, err := os.Open(db.WALPath())
980994
if err != nil {
981-
return 0, err
995+
return 0, 0, err
982996
}
983997
defer r.Close()
984998

999+
fi, err := r.Stat()
1000+
if err != nil {
1001+
return 0, 0, err
1002+
}
1003+
origWalSize = frameAlign(fi.Size(), db.pageSize)
1004+
9851005
w, err := os.OpenFile(filename, os.O_RDWR, 0666)
9861006
if err != nil {
987-
return 0, err
1007+
return 0, 0, err
9881008
}
9891009
defer w.Close()
9901010

991-
fi, err := w.Stat()
1011+
fi, err = w.Stat()
9921012
if err != nil {
993-
return 0, err
1013+
return 0, 0, err
9941014
}
9951015
origSize := frameAlign(fi.Size(), db.pageSize)
9961016

9971017
// Read shadow WAL header to determine byte order for checksum & salt.
9981018
hdr := make([]byte, WALHeaderSize)
9991019
if _, err := io.ReadFull(w, hdr); err != nil {
1000-
return 0, fmt.Errorf("read header: %w", err)
1020+
return 0, 0, fmt.Errorf("read header: %w", err)
10011021
}
10021022
hsalt0 := binary.BigEndian.Uint32(hdr[16:])
10031023
hsalt1 := binary.BigEndian.Uint32(hdr[20:])
10041024

10051025
bo, err := headerByteOrder(hdr)
10061026
if err != nil {
1007-
return 0, err
1027+
return 0, 0, err
10081028
}
10091029

10101030
// Read previous checksum.
10111031
chksum0, chksum1, err := readLastChecksumFrom(w, db.pageSize)
10121032
if err != nil {
1013-
return 0, fmt.Errorf("last checksum: %w", err)
1033+
return 0, 0, fmt.Errorf("last checksum: %w", err)
10141034
}
10151035

10161036
// Seek to correct position on real wal.
10171037
if _, err := r.Seek(origSize, io.SeekStart); err != nil {
1018-
return 0, fmt.Errorf("real wal seek: %w", err)
1038+
return 0, 0, fmt.Errorf("real wal seek: %w", err)
10191039
} else if _, err := w.Seek(origSize, io.SeekStart); err != nil {
1020-
return 0, fmt.Errorf("shadow wal seek: %w", err)
1040+
return 0, 0, fmt.Errorf("shadow wal seek: %w", err)
10211041
}
10221042

10231043
// Read through WAL from last position to find the page of the last
@@ -1032,7 +1052,7 @@ func (db *DB) copyToShadowWAL(filename string) (newSize int64, err error) {
10321052
Tracef("%s: copy-shadow: break %s @ %d; err=%s", db.path, filename, offset, err)
10331053
break // end of file or partial page
10341054
} else if err != nil {
1035-
return 0, fmt.Errorf("read wal: %w", err)
1055+
return 0, 0, fmt.Errorf("read wal: %w", err)
10361056
}
10371057

10381058
// Read frame salt & compare to header salt. Stop reading on mismatch.
@@ -1063,7 +1083,7 @@ func (db *DB) copyToShadowWAL(filename string) (newSize int64, err error) {
10631083
newDBSize := binary.BigEndian.Uint32(frame[4:])
10641084
if newDBSize != 0 {
10651085
if _, err := buf.WriteTo(w); err != nil {
1066-
return 0, fmt.Errorf("write shadow wal: %w", err)
1086+
return 0, 0, fmt.Errorf("write shadow wal: %w", err)
10671087
}
10681088
buf.Reset()
10691089
lastCommitSize = offset
@@ -1072,15 +1092,15 @@ func (db *DB) copyToShadowWAL(filename string) (newSize int64, err error) {
10721092

10731093
// Sync & close.
10741094
if err := w.Sync(); err != nil {
1075-
return 0, err
1095+
return 0, 0, err
10761096
} else if err := w.Close(); err != nil {
1077-
return 0, err
1097+
return 0, 0, err
10781098
}
10791099

10801100
// Track total number of bytes written to WAL.
10811101
db.totalWALBytesCounter.Add(float64(lastCommitSize - origSize))
10821102

1083-
return lastCommitSize, nil
1103+
return origWalSize, lastCommitSize, nil
10841104
}
10851105

10861106
// ShadowWALReader opens a reader for a shadow WAL file at a given position.
@@ -1249,7 +1269,7 @@ func (db *DB) checkpoint(ctx context.Context, generation, mode string) error {
12491269
}
12501270

12511271
// Copy shadow WAL before checkpoint to copy as much as possible.
1252-
if _, err := db.copyToShadowWAL(shadowWALPath); err != nil {
1272+
if _, _, err := db.copyToShadowWAL(shadowWALPath); err != nil {
12531273
return fmt.Errorf("cannot copy to end of shadow wal before checkpoint: %w", err)
12541274
}
12551275

@@ -1284,7 +1304,7 @@ func (db *DB) checkpoint(ctx context.Context, generation, mode string) error {
12841304
}
12851305

12861306
// Copy the end of the previous WAL before starting a new shadow WAL.
1287-
if _, err := db.copyToShadowWAL(shadowWALPath); err != nil {
1307+
if _, _, err := db.copyToShadowWAL(shadowWALPath); err != nil {
12881308
return fmt.Errorf("cannot copy to end of shadow wal: %w", err)
12891309
}
12901310

@@ -1343,7 +1363,7 @@ func (db *DB) execCheckpoint(mode string) (err error) {
13431363
if err := db.db.QueryRow(rawsql).Scan(&row[0], &row[1], &row[2]); err != nil {
13441364
return err
13451365
}
1346-
Tracef("%s: checkpoint: mode=%v (%d,%d,%d)", db.path, mode, row[0], row[1], row[2])
1366+
log.Printf("%s: checkpoint: mode=%v (%d,%d,%d)", db.path, mode, row[0], row[1], row[2])
13471367

13481368
// Reacquire the read lock immediately after the checkpoint.
13491369
if err := db.acquireReadLock(); err != nil {

0 commit comments

Comments
 (0)