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-14 23:12:03 +03:00
|
|
|
|
fmt.Printf("before replay\n")
|
2026-06-13 22:43:17 +00:00
|
|
|
|
err = walReplayer.Replay()
|
|
|
|
|
|
if err != nil {
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
2026-06-14 23:12:03 +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
|
|
|
|
|
|
}
|