Time travel pragma.

This commit is contained in:
Nuno Cruces
2025-12-03 14:51:41 +00:00
parent 7028e3a5b9
commit 15e9087fa8
4 changed files with 128 additions and 39 deletions

View File

@@ -23,6 +23,7 @@ if [[ "$OSTYPE" == "msys" || "$OSTYPE" == "cygwin" ]]; then
MSYS_NO_PATHCONV=1 nmake /f makefile.msc sqlite3.c "OPTS=-DSQLITE_ENABLE_UPDATE_DELETE_LIMIT -DSQLITE_ENABLE_ORDERED_SET_AGGREGATES" MSYS_NO_PATHCONV=1 nmake /f makefile.msc sqlite3.c "OPTS=-DSQLITE_ENABLE_UPDATE_DELETE_LIMIT -DSQLITE_ENABLE_ORDERED_SET_AGGREGATES"
else else
sh configure --enable-update-limit sh configure --enable-update-limit
make verify-source
OPTS=-DSQLITE_ENABLE_ORDERED_SET_AGGREGATES make sqlite3.c OPTS=-DSQLITE_ENABLE_ORDERED_SET_AGGREGATES make sqlite3.c
fi fi
cd ~- cd ~-

View File

@@ -7,6 +7,7 @@ import (
"fmt" "fmt"
"io" "io"
"strconv" "strconv"
"strings"
"sync" "sync"
"time" "time"
@@ -62,8 +63,10 @@ type liteFile struct {
db *liteDB db *liteDB
conn *sqlite3.Conn conn *sqlite3.Conn
pages *pageIndex pages *pageIndex
syncTime time.Time
txid ltx.TXID txid ltx.TXID
pageSize uint32 pageSize uint32
locked bool
} }
func (f *liteFile) Close() error { return nil } func (f *liteFile) Close() error { return nil }
@@ -71,10 +74,11 @@ func (f *liteFile) Close() error { return nil }
func (f *liteFile) ReadAt(p []byte, off int64) (n int, err error) { func (f *liteFile) ReadAt(p []byte, off int64) (n int, err error) {
ctx := f.context() ctx := f.context()
pages, txid := f.pages, f.txid pages, txid := f.pages, f.txid
if pages == nil { if pages == nil && f.syncTime.IsZero() {
pages, txid, err = f.db.pollReplica(ctx) pages, txid, err = f.db.pollReplica(ctx)
} }
if err != nil { if err != nil {
f.db.opts.Logger.Error("poll replica", "error", err)
return 0, err return 0, err
} }
@@ -135,14 +139,25 @@ func (f *liteFile) Size() (size int64, err error) {
func (f *liteFile) Lock(lock vfs.LockLevel) (err error) { func (f *liteFile) Lock(lock vfs.LockLevel) (err error) {
if lock >= vfs.LOCK_RESERVED { if lock >= vfs.LOCK_RESERVED {
// notest // OPEN_READONLY
return sqlite3.IOERR_LOCK return sqlite3.IOERR_LOCK
} }
f.pages, f.txid, err = f.db.pollReplica(f.context()) if f.syncTime.IsZero() {
f.pages, f.txid, err = f.db.pollReplica(f.context())
}
if err != nil {
f.db.opts.Logger.Error("poll replica", "error", err)
} else {
f.locked = true
}
return err return err
} }
func (f *liteFile) Unlock(lock vfs.LockLevel) error { func (f *liteFile) Unlock(lock vfs.LockLevel) error {
f.pages, f.txid = nil, 0 if f.syncTime.IsZero() {
f.pages, f.txid = nil, 0
}
f.locked = false
return nil return nil
} }
@@ -166,7 +181,6 @@ func (f *liteFile) Pragma(name, value string) (string, error) {
case "litestream_txid": case "litestream_txid":
txid := f.txid txid := f.txid
if txid == 0 { if txid == 0 {
// Outside transaction.
f.db.mtx.Lock() f.db.mtx.Lock()
txid = f.db.txids[0] txid = f.db.txids[0]
f.db.mtx.Unlock() f.db.mtx.Unlock()
@@ -179,11 +193,45 @@ func (f *liteFile) Pragma(name, value string) (string, error) {
f.db.mtx.Unlock() f.db.mtx.Unlock()
if lastPoll.IsZero() { if lastPoll.IsZero() {
// Never polled successfully.
return "-1", nil return "-1", nil
} }
lag := time.Since(lastPoll) / time.Second lag := time.Since(lastPoll) / time.Second
return strconv.FormatInt(int64(lag), 10), nil return strconv.FormatInt(int64(lag), 10), nil
case "litestream_time":
if value == "" {
syncTime := f.syncTime
if syncTime.IsZero() {
f.db.mtx.Lock()
syncTime = f.db.lastInfo
f.db.mtx.Unlock()
}
if syncTime.IsZero() {
return "latest", nil
}
return syncTime.Format(time.RFC3339Nano), nil
}
if !f.locked {
return "", sqlite3.MISUSE
}
if strings.EqualFold(value, "latest") {
f.syncTime = time.Time{}
f.pages, f.txid = nil, 0
return "", nil
}
syncTime, err := sqlite3.TimeFormatAuto.Decode(value)
if err != nil {
return "", err
}
err = f.buildIndex(f.context(), syncTime)
if err != nil {
f.db.opts.Logger.Error("build index", "error", err)
}
return "", err
} }
return "", sqlite3.NOTFOUND return "", sqlite3.NOTFOUND
@@ -200,27 +248,53 @@ func (f *liteFile) context() context.Context {
return context.Background() return context.Background()
} }
func (f *liteFile) buildIndex(ctx context.Context, syncTime time.Time) error {
// Build the index from scratch from a Litestream restore plan.
infos, err := litestream.CalcRestorePlan(ctx, f.db.client, 0, syncTime, f.db.opts.Logger)
if err != nil {
if !errors.Is(err, litestream.ErrTxNotAvailable) {
return fmt.Errorf("calc restore plan: %w", err)
}
return nil
}
var txid ltx.TXID
var pages *pageIndex
for _, info := range infos {
pages, err = fetchPageIndex(ctx, pages, f.db.client, info)
if err != nil {
return err
}
txid = max(txid, info.MaxTXID)
}
f.syncTime = syncTime
f.pages = pages
f.txid = txid
return nil
}
type liteDB struct { type liteDB struct {
client litestream.ReplicaClient client litestream.ReplicaClient
opts ReplicaOptions opts ReplicaOptions
cache pageCache cache pageCache
pages *pageIndex // +checklocks:mtx pages *pageIndex // +checklocks:mtx
lastPoll time.Time // +checklocks:mtx lastPoll time.Time // +checklocks:mtx
lastInfo time.Time // +checklocks:mtx
txids levelTXIDs // +checklocks:mtx txids levelTXIDs // +checklocks:mtx
mtx sync.Mutex mtx sync.Mutex
} }
func (f *liteDB) buildIndex(ctx context.Context) error { func (d *liteDB) buildIndex(ctx context.Context) error {
f.mtx.Lock() d.mtx.Lock()
defer f.mtx.Unlock() defer d.mtx.Unlock()
// Skip if we already have an index. // Skip if we already have an index.
if f.pages != nil { if d.pages != nil {
return nil return nil
} }
// Build the index from scratch from a Litestream restore plan. // Build the index from scratch from a Litestream restore plan.
infos, err := litestream.CalcRestorePlan(ctx, f.client, 0, time.Time{}, f.opts.Logger) infos, err := litestream.CalcRestorePlan(ctx, d.client, 0, time.Time{}, d.opts.Logger)
if err != nil { if err != nil {
if !errors.Is(err, litestream.ErrTxNotAvailable) { if !errors.Is(err, litestream.ErrTxNotAvailable) {
return fmt.Errorf("calc restore plan: %w", err) return fmt.Errorf("calc restore plan: %w", err)
@@ -229,47 +303,46 @@ func (f *liteDB) buildIndex(ctx context.Context) error {
} }
for _, info := range infos { for _, info := range infos {
err := f.updateInfo(ctx, info) err := d.updateInfo(ctx, info)
if err != nil { if err != nil {
return err return err
} }
} }
f.lastPoll = time.Now() d.lastPoll = time.Now()
return nil return nil
} }
func (f *liteDB) pollReplica(ctx context.Context) (*pageIndex, ltx.TXID, error) { func (d *liteDB) pollReplica(ctx context.Context) (*pageIndex, ltx.TXID, error) {
f.mtx.Lock() d.mtx.Lock()
defer f.mtx.Unlock() defer d.mtx.Unlock()
// Limit polling interval. // Limit polling interval.
if time.Since(f.lastPoll) < f.opts.PollInterval { if time.Since(d.lastPoll) < d.opts.PollInterval {
return f.pages, f.txids[0], nil return d.pages, d.txids[0], nil
} }
for level := range []int{0, 1, litestream.SnapshotLevel} { for level := range []int{0, 1, litestream.SnapshotLevel} {
if err := f.updateLevel(ctx, level); err != nil { if err := d.updateLevel(ctx, level); err != nil {
f.opts.Logger.Error("cannot poll replica", "error", err)
return nil, 0, err return nil, 0, err
} }
} }
f.lastPoll = time.Now() d.lastPoll = time.Now()
return f.pages, f.txids[0], nil return d.pages, d.txids[0], nil
} }
// +checklocks:f.mtx // +checklocks:d.mtx
func (f *liteDB) updateLevel(ctx context.Context, level int) error { func (d *liteDB) updateLevel(ctx context.Context, level int) error {
var nextTXID ltx.TXID var nextTXID ltx.TXID
// Snapshots must start from scratch, // Snapshots must start from scratch,
// other levels can start from where they were left. // other levels can start from where they were left.
if level != litestream.SnapshotLevel { if level != litestream.SnapshotLevel {
nextTXID = f.txids[level] + 1 nextTXID = d.txids[level] + 1
} }
// Start reading from the next LTX file after the current position. // Start reading from the next LTX file after the current position.
itr, err := f.client.LTXFiles(ctx, level, nextTXID, false) itr, err := d.client.LTXFiles(ctx, level, nextTXID, false)
if err != nil { if err != nil {
return fmt.Errorf("ltx files: %w", err) return fmt.Errorf("ltx files: %w", err)
} }
@@ -280,11 +353,11 @@ func (f *liteDB) updateLevel(ctx context.Context, level int) error {
info := itr.Item() info := itr.Item()
// Skip LTX files already fully loaded into the index. // Skip LTX files already fully loaded into the index.
if info.MaxTXID <= f.txids[level] { if info.MaxTXID <= d.txids[level] {
continue continue
} }
err := f.updateInfo(ctx, info) err := d.updateInfo(ctx, info)
if err != nil { if err != nil {
return err return err
} }
@@ -295,26 +368,41 @@ func (f *liteDB) updateLevel(ctx context.Context, level int) error {
return itr.Close() return itr.Close()
} }
// +checklocks:f.mtx // +checklocks:d.mtx
func (f *liteDB) updateInfo(ctx context.Context, info *ltx.FileInfo) error { func (d *liteDB) updateInfo(ctx context.Context, info *ltx.FileInfo) error {
idx, err := litestream.FetchPageIndex(ctx, f.client, info) pages, err := fetchPageIndex(ctx, d.pages, d.client, info)
if err != nil { if err != nil {
return fmt.Errorf("fetch page index: %w", err) return err
}
// Track the MaxTXID for each level.
maxTXID := &d.txids[info.Level]
*maxTXID = max(*maxTXID, info.MaxTXID)
d.txids[0] = max(d.txids[0], *maxTXID)
if d.lastInfo.Before(info.CreatedAt) {
d.lastInfo = info.CreatedAt
}
d.pages = pages
return nil
}
func fetchPageIndex(
ctx context.Context, pages *pageIndex,
client litestream.ReplicaClient, info *ltx.FileInfo) (*pageIndex, error) {
idx, err := litestream.FetchPageIndex(ctx, client, info)
if err != nil {
return nil, fmt.Errorf("fetch page index: %w", err)
} }
// Replace pages in the index with new pages. // Replace pages in the index with new pages.
for k, v := range idx { for k, v := range idx {
// Patch avoids mutating the index for an unmodified page. // Patch avoids mutating the index for an unmodified page.
f.pages = f.pages.Patch(k, func(node *pageIndex) (ltx.PageIndexElem, bool) { pages = pages.Patch(k, func(node *pageIndex) (ltx.PageIndexElem, bool) {
return v, node == nil || v != node.Value() return v, node == nil || v != node.Value()
}) })
} }
return pages, nil
// Track the MaxTXID for each level.
maxTXID := &f.txids[info.Level]
*maxTXID = max(*maxTXID, info.MaxTXID)
f.txids[0] = max(f.txids[0], *maxTXID)
return nil
} }
// Type aliases; these are a mouthful. // Type aliases; these are a mouthful.

View File

@@ -329,9 +329,9 @@ func vfsFileControlImpl(ctx context.Context, mod api.Module, file File, op _Fcnt
case _FCNTL_PRAGMA: case _FCNTL_PRAGMA:
if file, ok := file.(FilePragma); ok { if file, ok := file.(FilePragma); ok {
var value string
ptr := util.Read32[ptr_t](mod, pArg+1*ptrlen) ptr := util.Read32[ptr_t](mod, pArg+1*ptrlen)
name := util.ReadString(mod, ptr, _MAX_SQL_LENGTH) name := util.ReadString(mod, ptr, _MAX_SQL_LENGTH)
var value string
if ptr := util.Read32[ptr_t](mod, pArg+2*ptrlen); ptr != 0 { if ptr := util.Read32[ptr_t](mod, pArg+2*ptrlen); ptr != 0 {
value = util.ReadString(mod, ptr, _MAX_SQL_LENGTH) value = util.ReadString(mod, ptr, _MAX_SQL_LENGTH)
} }