This commit is contained in:
2026-06-13 22:43:17 +00:00
parent 1d1fdb1f0b
commit 685c48f94f
28 changed files with 2492 additions and 2657 deletions

221
recovery/recovery.go Normal file
View File

@@ -0,0 +1,221 @@
package recovery
import (
"errors"
"os"
"path/filepath"
"strconv"
"strings"
"gordenko.dev/dima/qb"
"gordenko.dev/dima/qb/freelist"
"gordenko.dev/dima/qb/storage"
)
// 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 {
//Metrics []
SnapshotNumber int
WAL string
IndexFreeList *freelist.FreeList
DataFreeList *freelist.FreeList
}
func Recovery(dir string, databaseName string) (_ RecoveryReport, err error) {
files, err := resolveRecoveryFiles(dir)
if err != nil {
return
}
var (
freeIndexPages []uint32
freeDataPages []uint32
)
if files.Snapshot != "" {
var out storage.ReadSnapshotOut
out, err = storage.ReadSnapshot(files.Snapshot)
if err != nil {
return
}
//
freeIndexPages = out.IndexPageNumbers
freeDataPages = out.DataPageNumbers
}
if files.WAL != "" {
var (
walReader *storage.WALReader
walReplayer *storage.WALReplayer
)
walReader, err = storage.NewWALReader(storage.WALReaderOptions{
//FileName: JoinWALFileName(s.dir, files.WAL),
BufferSize: 8 * 1024 * 1024,
})
if err != nil {
return
}
walReplayer, err = storage.NewWALReplayer(storage.WALReplayerOptions{
WALReader: walReader,
Metrics: snapshot.Metrics,
FreeIndexPages: freeIndexPages,
FreeDataPages: freeDataPages,
})
if err != nil {
return
}
err = walReplayer.Replay()
if err != nil {
return
}
// перезаписую сторінки із останнього комміта
var dataFile *os.File
dataFile, err = os.OpenFile(qb.GetDataFilePath(dir, databaseName), os.O_WRONLY, 0666)
if err != nil {
return
}
err = storage.WriteDataPages(dataFile, walReplayer.DataPages())
if err != nil {
return
}
err = dataFile.Close()
if err != nil {
return
}
var indexFile *os.File
indexFile, err = os.OpenFile(qb.GetIndexFilePath(dir, databaseName), os.O_WRONLY, 0666)
if err != nil {
return
}
err = storage.WriteIndexPages(indexFile, walReplayer.IndexPages())
if err != nil {
return
}
err = indexFile.Close()
if err != nil {
return
}
}
indexFreeList, err := freelist.New(freelist.Options{
PageSize: 2048,
BaseFilePath: filepath.Join(dir, databaseName+".free_index"),
DeltaFilePath: filepath.Join(dir, databaseName+".free_index_delta"),
})
// FIX free pages sync
dataFreeList, err := freelist.New(freelist.Options{
PageSize: 2048,
BaseFilePath: filepath.Join(dir, databaseName+".free_data"),
DeltaFilePath: filepath.Join(dir, databaseName+".free_data_delta"),
})
return RecoveryReport{
SnapshotNumber: files.SnapshotNumber,
WAL: files.WAL,
IndexFreeList: indexFreeList,
DataFreeList: dataFreeList,
}, nil
}
//func (s *Database) relayMetricsToMetrics(replayMetrics map[uint32]*ReplayMetric) {
//for metricID, x := range replayMetrics {
//s.metrics[metricID] = x.ToMetric() FIX
//}
//}