222 lines
4.9 KiB
Go
222 lines
4.9 KiB
Go
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
|
||
//}
|
||
//}
|