Compare commits
1
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
847a930df8 |
@@ -81,12 +81,14 @@ const (
|
|||||||
sqliteHardHeapLimitBytes = 2147483648
|
sqliteHardHeapLimitBytes = 2147483648
|
||||||
)
|
)
|
||||||
|
|
||||||
// Settings that keep the WAL file small (see Checkpoint).
|
// Settings that bound the WAL file's size once no read is open (see
|
||||||
|
// Checkpoint).
|
||||||
const (
|
const (
|
||||||
// sqliteWALSizeLimitBytes (64 MiB) is the WAL file size past which
|
// sqliteWALSizeLimitBytes (64 MiB) is the WAL file size past which
|
||||||
// Checkpoint truncates the file. It is also the journal_size_limit of the
|
// Checkpoint runs a TRUNCATE checkpoint. It is also the
|
||||||
// connection kept for writing: when SQLite starts the WAL again from the
|
// journal_size_limit of the connection kept for writing: when SQLite
|
||||||
// beginning, it cuts a larger WAL file down to this size.
|
// starts the WAL again from the beginning, it cuts a larger WAL file down
|
||||||
|
// to this size.
|
||||||
sqliteWALSizeLimitBytes = 67108864
|
sqliteWALSizeLimitBytes = 67108864
|
||||||
// truncateBusyTimeoutMs is the busy timeout of the TRUNCATE checkpoint:
|
// truncateBusyTimeoutMs is the busy timeout of the TRUNCATE checkpoint:
|
||||||
// the longest it waits for open reads, and so the longest it keeps batch
|
// the longest it waits for open reads, and so the longest it keeps batch
|
||||||
@@ -130,7 +132,8 @@ var (
|
|||||||
// ErrCheckpointIncomplete is returned by Checkpoint when it left part of
|
// ErrCheckpointIncomplete is returned by Checkpoint when it left part of
|
||||||
// the WAL uncopied, or left a WAL file over sqliteWALSizeLimitBytes
|
// the WAL uncopied, or left a WAL file over sqliteWALSizeLimitBytes
|
||||||
// untruncated, because reads still needed the WAL or SQLite reported the
|
// untruncated, because reads still needed the WAL or SQLite reported the
|
||||||
// checkpoint busy. A later checkpoint finishes the work.
|
// checkpoint busy. A later checkpoint that runs when no read is open
|
||||||
|
// finishes the work.
|
||||||
ErrCheckpointIncomplete = errors.New("WAL checkpoint incomplete")
|
ErrCheckpointIncomplete = errors.New("WAL checkpoint incomplete")
|
||||||
|
|
||||||
errInvalidASN = errors.New("invalid ASN")
|
errInvalidASN = errors.New("invalid ASN")
|
||||||
@@ -2282,25 +2285,24 @@ func (d *Database) Analyze(ctx context.Context) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// Checkpoint copies the WAL into the database file and keeps the WAL file
|
// Checkpoint copies the WAL into the database file. It holds the database
|
||||||
// small. It holds the database lock, so batch writes wait while it runs.
|
// lock, so batch writes wait while it runs.
|
||||||
//
|
//
|
||||||
// It runs a PASSIVE checkpoint, which never waits: it copies what no open read
|
// It runs a PASSIVE checkpoint, which never waits: it copies what no open read
|
||||||
// still needs. That alone does not keep the file small: SQLite starts the WAL
|
// still needs, and leaves the file as large as it is. When the file is over
|
||||||
// again from the beginning only once a checkpoint has copied all of it and no
|
// sqliteWALSizeLimitBytes, Checkpoint runs one TRUNCATE checkpoint instead,
|
||||||
// read that began before is still open, which never happens while reads keep
|
// which waits at most truncateBusyTimeoutMs for the open reads to end, then
|
||||||
// overlapping. So when the file is over sqliteWALSizeLimitBytes, Checkpoint
|
// copies all of the WAL and empties the file, or gives up. It is logged with
|
||||||
// runs a TRUNCATE checkpoint instead, which waits for the reads that still need
|
// how long it took and whether it emptied the file.
|
||||||
// the WAL, copies all of it and empties the file. That checkpoint waits at most
|
|
||||||
// truncateBusyTimeoutMs, and it is logged with how long it took.
|
|
||||||
//
|
//
|
||||||
// So while reads overlap, the WAL file stays under sqliteWALSizeLimitBytes
|
// So a file over the limit is emptied by the first Checkpoint that finds no
|
||||||
// plus what batch writes add between two checkpoints, as long as no read stays
|
// read open, at its start or at some moment of its wait. While reads follow
|
||||||
// open longer than that wait. A longer read makes the TRUNCATE checkpoint give
|
// one another with no pause between them, every TRUNCATE checkpoint can give
|
||||||
// up, and the file grows until a later one empties it.
|
// up after its full wait, and the file can keep growing for as long as they
|
||||||
|
// do.
|
||||||
//
|
//
|
||||||
// It returns ErrCheckpointIncomplete when it leaves part of the WAL uncopied,
|
// It returns ErrCheckpointIncomplete when it leaves part of the WAL uncopied,
|
||||||
// or a file over the limit untruncated.
|
// or a file over the limit not emptied.
|
||||||
func (d *Database) Checkpoint(ctx context.Context) error {
|
func (d *Database) Checkpoint(ctx context.Context) error {
|
||||||
d.lock("Checkpoint")
|
d.lock("Checkpoint")
|
||||||
defer d.unlock()
|
defer d.unlock()
|
||||||
|
|||||||
@@ -72,24 +72,11 @@ const (
|
|||||||
// waiting for it, may take while a reader is open: well under the busy
|
// waiting for it, may take while a reader is open: well under the busy
|
||||||
// timeout, which a checkpoint that waits for the reader takes.
|
// timeout, which a checkpoint that waits for the reader takes.
|
||||||
readerCheckpointLimit = time.Second
|
readerCheckpointLimit = time.Second
|
||||||
)
|
// truncateCheckpointLimit is how long a checkpoint of a WAL file over
|
||||||
|
// sqliteWALSizeLimitBytes may take to give up while a reader is open: its
|
||||||
// Parameters of the test of the WAL file's size while reads overlap.
|
// wait for the reader plus a second, well under the busy timeout.
|
||||||
const (
|
truncateCheckpointLimit = truncateBusyTimeoutMs*time.Millisecond +
|
||||||
// walFillStep is how much the test writes between two checkpoints.
|
time.Second
|
||||||
walFillStep = sqliteWALSizeLimitBytes / 16
|
|
||||||
// walFillTotal is how much it writes in all: past sqliteWALSizeLimitBytes,
|
|
||||||
// and on until a WAL file that no checkpoint empties ends over walFillBound.
|
|
||||||
walFillTotal = sqliteWALSizeLimitBytes + 4*walFillStep
|
|
||||||
// walFillBound is the size the WAL file must stay under: the limit, plus
|
|
||||||
// the writes since the last checkpoint, plus one more step for SQLite's
|
|
||||||
// own pages besides the written data.
|
|
||||||
walFillBound = sqliteWALSizeLimitBytes + 2*walFillStep
|
|
||||||
// overlappingReads is how many reads the test keeps open at once.
|
|
||||||
overlappingReads = 2
|
|
||||||
// overlappingReadDuration is how long each read stays open: well under
|
|
||||||
// truncateBusyTimeoutMs, so the TRUNCATE checkpoint can wait for it.
|
|
||||||
overlappingReadDuration = 50 * time.Millisecond
|
|
||||||
)
|
)
|
||||||
|
|
||||||
// Parameters for the prefix-length sample tests.
|
// Parameters for the prefix-length sample tests.
|
||||||
@@ -939,15 +926,14 @@ func TestCheckpointDoesNotWaitForReader(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// TestCheckpointKeepsWALSmallWhileReadsOverlap writes walFillTotal while
|
// TestCheckpointEmptiesWALOnceNoReadIsOpen fills the WAL file past
|
||||||
// overlappingReads reads keep overlapping, so that one is open at every write,
|
// sqliteWALSizeLimitBytes while the test holds a read open. With the read
|
||||||
// and runs Checkpoint after every walFillStep, as the maintainer does between
|
// open, it checks that Checkpoint gives up within truncateCheckpointLimit and
|
||||||
// batch writes. It checks that the WAL file stays under walFillBound and that
|
// reports the checkpoint incomplete, and that a batch write still succeeds.
|
||||||
// the writing connection has its busy timeout back afterwards. With only a
|
// After the test closes the read, it checks that the next Checkpoint brings
|
||||||
// PASSIVE checkpoint, SQLite never started the WAL again from the beginning
|
// the WAL file under the limit and gives the writing connection its busy
|
||||||
// while reads overlapped, and the file grew for as long as they did
|
// timeout back (https://git.eeqj.de/sneak/routewatch/issues/102).
|
||||||
// (https://git.eeqj.de/sneak/routewatch/issues/102).
|
func TestCheckpointEmptiesWALOnceNoReadIsOpen(t *testing.T) {
|
||||||
func TestCheckpointKeepsWALSmallWhileReadsOverlap(t *testing.T) {
|
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
|
|
||||||
db, err := New(&config.Config{StateDir: t.TempDir()}, logger.New())
|
db, err := New(&config.Config{StateDir: t.TempDir()}, logger.New())
|
||||||
@@ -958,58 +944,65 @@ func TestCheckpointKeepsWALSmallWhileReadsOverlap(t *testing.T) {
|
|||||||
|
|
||||||
ctx := t.Context()
|
ctx := t.Context()
|
||||||
|
|
||||||
// The test writes to a table of its own: each write replaces the one
|
reader, err := db.db.Conn(ctx)
|
||||||
// row's data with walFillStep of new random bytes, so the WAL grows by that
|
|
||||||
// much while the database file stays small. SQLite writes no page whose
|
|
||||||
// content stays the same, so the bytes must change. Like batch writes,
|
|
||||||
// the writes use the writing connection between the checkpoints.
|
|
||||||
_, err = db.writer.ExecContext(ctx,
|
|
||||||
"CREATE TABLE wal_fill (data BLOB); INSERT INTO wal_fill VALUES (NULL)")
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("failed to create the table the test writes to: %v", err)
|
t.Fatalf("failed to open the reading connection: %v", err)
|
||||||
|
}
|
||||||
|
defer func() { _ = reader.Close() }()
|
||||||
|
|
||||||
|
// A plain BEGIN starts a transaction that only reads, unlike the BEGIN
|
||||||
|
// IMMEDIATE of _txlock. It keeps the snapshot its first read takes until
|
||||||
|
// it ends, so no checkpoint can copy what is written after that read.
|
||||||
|
_, err = reader.ExecContext(ctx, "BEGIN")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("failed to begin the read transaction: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
stop := make(chan struct{})
|
var routes int
|
||||||
|
|
||||||
var readers sync.WaitGroup
|
err = reader.QueryRowContext(ctx,
|
||||||
|
"SELECT count(*) FROM live_routes_v4").Scan(&routes)
|
||||||
for i := range overlappingReads {
|
if err != nil {
|
||||||
readers.Go(func() {
|
t.Fatalf("failed to read the live routes: %v", err)
|
||||||
// Reader i starts i/overlappingReads of a read after the first,
|
|
||||||
// so that the reads overlap.
|
|
||||||
time.Sleep(time.Duration(i) * overlappingReadDuration /
|
|
||||||
overlappingReads)
|
|
||||||
keepReading(ctx, t, db, stop)
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
defer func() {
|
fillWALPastLimit(ctx, t, db)
|
||||||
close(stop)
|
|
||||||
readers.Wait()
|
|
||||||
}()
|
|
||||||
|
|
||||||
for written := 0; written < walFillTotal; written += walFillStep {
|
started := time.Now()
|
||||||
_, err = db.writer.ExecContext(ctx,
|
|
||||||
"UPDATE wal_fill SET data = randomblob(?)", walFillStep)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("failed to write: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
wal, statErr := os.Stat(db.path + walFileSuffix)
|
err = db.Checkpoint(ctx)
|
||||||
if statErr != nil {
|
if !errors.Is(err, ErrCheckpointIncomplete) {
|
||||||
t.Fatalf("failed to read the WAL file's size: %v", statErr)
|
t.Errorf("Checkpoint = %v with a read open, want %v",
|
||||||
}
|
err, ErrCheckpointIncomplete)
|
||||||
|
}
|
||||||
|
|
||||||
if wal.Size() > walFillBound {
|
took := time.Since(started)
|
||||||
t.Fatalf("WAL file of %d bytes after writing %d with reads "+
|
if took >= truncateCheckpointLimit {
|
||||||
"overlapping, want at most %d", wal.Size(), written+walFillStep,
|
t.Errorf("Checkpoint took %v with a read open, want under %v",
|
||||||
walFillBound)
|
took, truncateCheckpointLimit)
|
||||||
}
|
}
|
||||||
|
|
||||||
err = db.Checkpoint(ctx)
|
err = db.UpsertLiveRouteBatch([]*LiveRoute{
|
||||||
if err != nil && !errors.Is(err, ErrCheckpointIncomplete) {
|
mkV4Route(t, "192.0.2.0/24", lockTestASN, time.Now().UTC()),
|
||||||
t.Fatalf("Checkpoint: %v", err)
|
})
|
||||||
}
|
if err != nil {
|
||||||
|
t.Errorf("batch write after Checkpoint gave up: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err = reader.ExecContext(ctx, "ROLLBACK")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("failed to end the read transaction: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
err = db.Checkpoint(ctx)
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("Checkpoint with no read open: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
size := walFileSize(t, db)
|
||||||
|
if size >= sqliteWALSizeLimitBytes {
|
||||||
|
t.Errorf("WAL file of %d bytes after Checkpoint with no read open, "+
|
||||||
|
"want under %d", size, sqliteWALSizeLimitBytes)
|
||||||
}
|
}
|
||||||
|
|
||||||
busyTimeout := readPragma(ctx, t, db.writer, 0, "busy_timeout")
|
busyTimeout := readPragma(ctx, t, db.writer, 0, "busy_timeout")
|
||||||
@@ -1019,57 +1012,40 @@ func TestCheckpointKeepsWALSmallWhileReadsOverlap(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// keepReading runs one read transaction after another on a connection of its
|
// fillWALPastLimit writes sqliteWALSizeLimitBytes of random bytes to a table
|
||||||
// own, each open for overlappingReadDuration, until stop is closed.
|
// of the test's own, which with SQLite's page and frame headers leaves the WAL
|
||||||
func keepReading(
|
// file of db over the limit, and checks that it does.
|
||||||
ctx context.Context, t *testing.T, db *Database, stop <-chan struct{},
|
func fillWALPastLimit(ctx context.Context, t *testing.T, db *Database) {
|
||||||
) {
|
|
||||||
t.Helper()
|
t.Helper()
|
||||||
|
|
||||||
conn, err := db.db.Conn(ctx)
|
_, err := db.writer.ExecContext(ctx, "CREATE TABLE wal_fill (data BLOB)")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Errorf("failed to open a reading connection: %v", err)
|
t.Fatalf("failed to create the table the test writes to: %v", err)
|
||||||
|
|
||||||
return
|
|
||||||
}
|
}
|
||||||
defer func() { _ = conn.Close() }()
|
|
||||||
|
|
||||||
for {
|
_, err = db.writer.ExecContext(ctx,
|
||||||
select {
|
"INSERT INTO wal_fill VALUES (randomblob(?))", sqliteWALSizeLimitBytes)
|
||||||
case <-stop:
|
if err != nil {
|
||||||
return
|
t.Fatalf("failed to fill the WAL: %v", err)
|
||||||
default:
|
|
||||||
}
|
|
||||||
|
|
||||||
// A plain BEGIN starts a transaction that only reads, unlike the
|
|
||||||
// BEGIN IMMEDIATE of _txlock. It keeps the snapshot its first read
|
|
||||||
// takes until it ends.
|
|
||||||
_, err = conn.ExecContext(ctx, "BEGIN")
|
|
||||||
if err != nil {
|
|
||||||
t.Errorf("failed to begin a read transaction: %v", err)
|
|
||||||
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
var rows int
|
|
||||||
|
|
||||||
err = conn.QueryRowContext(ctx, "SELECT count(*) FROM wal_fill").
|
|
||||||
Scan(&rows)
|
|
||||||
if err != nil {
|
|
||||||
t.Errorf("failed to read: %v", err)
|
|
||||||
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
time.Sleep(overlappingReadDuration)
|
|
||||||
|
|
||||||
_, err = conn.ExecContext(ctx, "ROLLBACK")
|
|
||||||
if err != nil {
|
|
||||||
t.Errorf("failed to end a read transaction: %v", err)
|
|
||||||
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
size := walFileSize(t, db)
|
||||||
|
if size <= sqliteWALSizeLimitBytes {
|
||||||
|
t.Fatalf("WAL file of %d bytes after the test's write, want over %d",
|
||||||
|
size, sqliteWALSizeLimitBytes)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// walFileSize returns the size of the WAL file of db.
|
||||||
|
func walFileSize(t *testing.T, db *Database) int64 {
|
||||||
|
t.Helper()
|
||||||
|
|
||||||
|
wal, err := os.Stat(db.path + walFileSuffix)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("failed to read the WAL file's size: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return wal.Size()
|
||||||
}
|
}
|
||||||
|
|
||||||
// TestGetRandomPrefixesByLengthListsPrefixOnce stores one prefix of each
|
// TestGetRandomPrefixesByLengthListsPrefixOnce stores one prefix of each
|
||||||
|
|||||||
Reference in New Issue
Block a user