package recovery import ( "errors" "os" "path/filepath" "strconv" "strings" "gordenko.dev/dima/qb" "gordenko.dev/dima/qb/freelist" "gordenko.dev/dima/qb/storage" "gordenko.dev/dima/qb/worker" ) // 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 IndexFreeList *freelist.FreeList DataFreeList *freelist.FreeList } func Recovery(dir string, databaseName string) (_ RecoveryReport, err error) { recoveryFiles, err := resolveRecoveryFiles(dir) if err != nil { return } var ( frozenIndexPageCount int frozenDataPageCount int freeIndexPages []uint32 freeDataPages []uint32 metrics map[uint32]*storage.ReplayMetric ) if recoveryFiles.Snapshot != "" { var snapshot worker.ReadSnapshotOut snapshot, err = worker.ReadSnapshot(recoveryFiles.Snapshot) if err != nil { return } // frozenIndexPageCount = snapshot.FrozenIndexPagesCount frozenDataPageCount = snapshot.FrozenDataPagesCount freeIndexPages = snapshot.IndexPageNumbers freeDataPages = snapshot.DataPageNumbers metrics = snapshot.Metrics } if recoveryFiles.WAL != "" { var ( walReader *storage.WALReader walReplayer *storage.WALReplayer ) walReader, err = storage.NewWALReader(storage.WALReaderOptions{ FileName: recoveryFiles.WAL, BufferSize: 8 * 1024 * 1024, }) if err != nil { return } walReplayer, err = storage.NewWALReplayer(storage.WALReplayerOptions{ WALReader: walReader, Metrics: 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.DataPagesToRewrite()) 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.IndexPagesToRewrite()) if err != nil { return } err = indexFile.Close() if err != nil { return } freeIndexPages = walReplayer.FreeIndexPages() freeDataPages = walReplayer.FreeDataPages() } indexFreeList, err := freelist.New(freelist.Options{ PageSize: 2048, BaseFilePath: qb.GetIndexFreeListFilePath(dir, databaseName), DeltaFilePath: qb.GetIndexFreeListDeltaFilePath(dir, databaseName), FrozenPageCount: frozenIndexPageCount, }) indexFreeList.AddPageNumbers(freeIndexPages) dataFreeList, err := freelist.New(freelist.Options{ PageSize: 2048, BaseFilePath: qb.GetDataFreeListFilePath(dir, databaseName), DeltaFilePath: qb.GetDataFreeListDeltaFilePath(dir, databaseName), FrozenPageCount: frozenDataPageCount, }) dataFreeList.AddPageNumbers(freeDataPages) return RecoveryReport{ SnapshotNumber: recoveryFiles.SnapshotNumber, WAL: recoveryFiles.WAL, IndexFreeList: indexFreeList, DataFreeList: dataFreeList, }, nil }