Files
qb/recovery/recovery.go

240 lines
5.7 KiB
Go
Raw Permalink Normal View History

2026-06-13 22:43:17 +00:00
package recovery
import (
"errors"
2026-06-14 23:12:03 +03:00
"fmt"
2026-06-13 22:43:17 +00:00
"os"
"path/filepath"
"strconv"
"strings"
"gordenko.dev/dima/qb"
"gordenko.dev/dima/qb/freelist"
"gordenko.dev/dima/qb/storage"
2026-06-14 07:57:01 +03:00
"gordenko.dev/dima/qb/worker"
2026-06-13 22:43:17 +00:00
)
// Result зберігає результати для обох типів файлів
type FileVersion struct {
Name string
Version int
}
// FindMaxSnapshotFiles шукає файли з розширеннями .snapshot_XXX та .wal_XXX
// і повертає структуру з максимальними номерами.
func findFileLatestVersion(dir string, extPrefix string) (FileVersion, error) {
var x FileVersion
if !strings.HasPrefix(extPrefix, ".") {
extPrefix = "." + extPrefix
}
entries, err := os.ReadDir(dir)
if err != nil {
return x, err
}
for _, entry := range entries {
if entry.IsDir() {
continue
}
name := entry.Name()
ext := filepath.Ext(name)
if strings.HasPrefix(ext, extPrefix) {
version, err := strconv.Atoi(strings.TrimPrefix(ext, extPrefix))
if err == nil {
if version > x.Version || x.Name == "" {
x.Name = name
x.Version = version
}
}
}
}
return x, nil
}
var (
ErrLostSnapshot = errors.New("lost snapshot")
)
type RecoveryFiles struct {
Snapshot string
WAL string
SnapshotNumber int
}
func resolveRecoveryFiles(dir string) (_ RecoveryFiles, err error) {
wal, err := findFileLatestVersion(dir, "wal_")
if err != nil {
return
}
snapshot, err := findFileLatestVersion(dir, "snapshot_")
if err != nil {
return
}
if snapshot.Name != "" {
if wal.Name != "" {
if snapshot.Version == wal.Version {
return RecoveryFiles{
Snapshot: snapshot.Name,
WAL: wal.Name,
SnapshotNumber: snapshot.Version,
}, nil
} else if snapshot.Version > wal.Version {
return RecoveryFiles{
Snapshot: snapshot.Name,
SnapshotNumber: snapshot.Version,
}, nil
} else {
// WAL version > snapshot version
err = ErrLostSnapshot
return
}
} else {
return RecoveryFiles{
Snapshot: snapshot.Name,
SnapshotNumber: snapshot.Version,
}, nil
}
} else {
if wal.Name != "" {
if wal.Version == 0 {
return RecoveryFiles{
WAL: wal.Name,
}, nil
} else {
err = ErrLostSnapshot
return
}
} else {
// no files - ok, 1st start of database
return
}
}
}
type RecoveryReport struct {
SnapshotNumber int
WAL string
2026-06-14 23:12:03 +03:00
ReplayMetrics map[uint32]*storage.ReplayMetric
2026-06-13 22:43:17 +00:00
IndexFreeList *freelist.FreeList
DataFreeList *freelist.FreeList
}
func Recovery(dir string, databaseName string) (_ RecoveryReport, err error) {
2026-06-14 07:57:01 +03:00
recoveryFiles, err := resolveRecoveryFiles(dir)
2026-06-13 22:43:17 +00:00
if err != nil {
return
}
2026-06-14 23:12:03 +03:00
fmt.Printf("recovery files: %#v\n", recoveryFiles)
2026-06-13 22:43:17 +00:00
var (
2026-06-14 07:57:01 +03:00
frozenIndexPageCount int
frozenDataPageCount int
freeIndexPages []uint32
freeDataPages []uint32
metrics map[uint32]*storage.ReplayMetric
2026-06-13 22:43:17 +00:00
)
2026-06-14 07:57:01 +03:00
if recoveryFiles.Snapshot != "" {
var snapshot worker.ReadSnapshotOut
2026-06-14 23:12:03 +03:00
snapshot, err = worker.ReadSnapshot(filepath.Join(dir, recoveryFiles.Snapshot))
2026-06-13 22:43:17 +00:00
if err != nil {
return
}
2026-06-14 23:12:03 +03:00
fmt.Printf("snapshot: %#v\n", snapshot)
2026-06-14 07:57:01 +03:00
frozenIndexPageCount = snapshot.FrozenIndexPagesCount
frozenDataPageCount = snapshot.FrozenDataPagesCount
freeIndexPages = snapshot.IndexPageNumbers
freeDataPages = snapshot.DataPageNumbers
metrics = snapshot.Metrics
2026-06-14 23:12:03 +03:00
} else {
metrics = make(map[uint32]*storage.ReplayMetric)
2026-06-13 22:43:17 +00:00
}
2026-06-14 07:57:01 +03:00
if recoveryFiles.WAL != "" {
2026-06-13 22:43:17 +00:00
var (
2026-06-14 23:12:03 +03:00
wal *os.File
2026-06-13 22:43:17 +00:00
walReader *storage.WALReader
walReplayer *storage.WALReplayer
)
2026-06-14 23:12:03 +03:00
wal, err = os.Open(filepath.Join(dir, recoveryFiles.WAL))
if err != nil {
return
}
defer wal.Close()
walReader, err = storage.NewWALReader(wal)
2026-06-13 22:43:17 +00:00
if err != nil {
return
}
walReplayer, err = storage.NewWALReplayer(storage.WALReplayerOptions{
WALReader: walReader,
2026-06-14 07:57:01 +03:00
Metrics: metrics,
2026-06-13 22:43:17 +00:00
FreeIndexPages: freeIndexPages,
FreeDataPages: freeDataPages,
})
if err != nil {
return
}
2026-06-19 07:25:46 +03:00
//fmt.Printf("before replay\n")
2026-06-13 22:43:17 +00:00
err = walReplayer.Replay()
if err != nil {
return
}
2026-06-19 07:25:46 +03:00
//fmt.Printf("after replay\n")
2026-06-13 22:43:17 +00:00
// перезаписую сторінки із останнього комміта
var dataFile *os.File
2026-06-14 23:12:03 +03:00
dataFile, err = os.OpenFile(qb.GetDataFilePath(dir, databaseName), os.O_CREATE|os.O_WRONLY, 0666)
2026-06-13 22:43:17 +00:00
if err != nil {
return
}
2026-06-14 07:57:01 +03:00
err = storage.WriteDataPages(dataFile, walReplayer.DataPagesToRewrite())
2026-06-13 22:43:17 +00:00
if err != nil {
return
}
err = dataFile.Close()
if err != nil {
return
}
var indexFile *os.File
2026-06-14 23:12:03 +03:00
indexFile, err = os.OpenFile(qb.GetIndexFilePath(dir, databaseName), os.O_CREATE|os.O_WRONLY, 0666)
2026-06-13 22:43:17 +00:00
if err != nil {
return
}
2026-06-14 07:57:01 +03:00
err = storage.WriteIndexPages(indexFile, walReplayer.IndexPagesToRewrite())
2026-06-13 22:43:17 +00:00
if err != nil {
return
}
err = indexFile.Close()
if err != nil {
return
}
2026-06-14 07:57:01 +03:00
freeIndexPages = walReplayer.FreeIndexPages()
freeDataPages = walReplayer.FreeDataPages()
2026-06-13 22:43:17 +00:00
}
indexFreeList, err := freelist.New(freelist.Options{
2026-06-14 07:57:01 +03:00
PageSize: 2048,
BaseFilePath: qb.GetIndexFreeListFilePath(dir, databaseName),
DeltaFilePath: qb.GetIndexFreeListDeltaFilePath(dir, databaseName),
FrozenPageCount: frozenIndexPageCount,
2026-06-13 22:43:17 +00:00
})
2026-06-14 07:57:01 +03:00
indexFreeList.AddPageNumbers(freeIndexPages)
2026-06-13 22:43:17 +00:00
dataFreeList, err := freelist.New(freelist.Options{
2026-06-14 07:57:01 +03:00
PageSize: 2048,
BaseFilePath: qb.GetDataFreeListFilePath(dir, databaseName),
DeltaFilePath: qb.GetDataFreeListDeltaFilePath(dir, databaseName),
FrozenPageCount: frozenDataPageCount,
2026-06-13 22:43:17 +00:00
})
2026-06-14 07:57:01 +03:00
dataFreeList.AddPageNumbers(freeDataPages)
2026-06-13 22:43:17 +00:00
return RecoveryReport{
2026-06-14 07:57:01 +03:00
SnapshotNumber: recoveryFiles.SnapshotNumber,
WAL: recoveryFiles.WAL,
2026-06-14 23:12:03 +03:00
ReplayMetrics: metrics,
2026-06-13 22:43:17 +00:00
IndexFreeList: indexFreeList,
DataFreeList: dataFreeList,
}, nil
}