package recovery import ( "errors" "fmt" "os" "path/filepath" "regexp" "sort" "strconv" ) var ( reChanges = regexp.MustCompile(`(\d+)\.wal`) reSnapshot = regexp.MustCompile(`(\d+)\.snapshot`) ) func joinWALFileName(dir string, snapshotNumber int) string { return filepath.Join(dir, fmt.Sprintf("%d.wal", snapshotNumber)) } type RecoveryRecipe struct { Snapshot string Changes []string SnapshotNumber int ToDelete []string CompleteSnapshot bool // флаг - что нужно завершить создание снапшота } type RecoveryAdvisor struct { dir string verifySnapshot func(string) (bool, error) // (fileName) isVerified, error } type RecoveryAdvisorOptions struct { Dir string VerifySnapshot func(string) (bool, error) } func NewRecoveryAdvisor(opt RecoveryAdvisorOptions) (*RecoveryAdvisor, error) { if opt.Dir == "" { return nil, errors.New("Dir option is required") } if opt.VerifySnapshot == nil { return nil, errors.New("VerifySnapshot option is required") } return &RecoveryAdvisor{ dir: opt.Dir, verifySnapshot: opt.VerifySnapshot, }, nil } type SnapshotChangesPair struct { SnapshotFileName string WALFileName string SnapshotNumber int } func (s *RecoveryAdvisor) getSnapshotChangesPairs() (*SnapshotChangesPair, *SnapshotChangesPair, error) { var ( numSet = make(map[int]bool) changesSet = make(map[int]bool) snapshotsSet = make(map[int]bool) pairs []SnapshotChangesPair ) entries, err := os.ReadDir(s.dir) if err != nil { return nil, nil, err } for _, entry := range entries { if entry.Type().IsRegular() { baseName := entry.Name() groups := reChanges.FindStringSubmatch(baseName) if len(groups) == 2 { num, _ := strconv.Atoi(groups[1]) numSet[num] = true changesSet[num] = true } groups = reSnapshot.FindStringSubmatch(baseName) if len(groups) == 2 { num, _ := strconv.Atoi(groups[1]) numSet[num] = true snapshotsSet[num] = true } } } for snapshotNumber := range numSet { var ( snapshotFileName string changesFileName string ) if changesSet[snapshotNumber] { changesFileName = joinWALFileName(s.dir, snapshotNumber) } if snapshotsSet[snapshotNumber] { snapshotFileName = filepath.Join(s.dir, fmt.Sprintf("%d.snapshot", snapshotNumber)) } pairs = append(pairs, SnapshotChangesPair{ WALFileName: changesFileName, SnapshotFileName: snapshotFileName, SnapshotNumber: snapshotNumber, }) } if len(pairs) == 0 { return nil, nil, nil } sort.Slice(pairs, func(i, j int) bool { return pairs[i].SnapshotNumber > pairs[j].SnapshotNumber }) pair := pairs[0] if pair.WALFileName == "" { return nil, nil, fmt.Errorf("has %d.shapshot file, but %d.changes file not found", pair.SnapshotNumber, pair.SnapshotNumber) } if len(pairs) > 1 { prevPair := pairs[1] if prevPair.SnapshotFileName == "" && prevPair.SnapshotNumber != 1 { return &pair, nil, nil } if prevPair.WALFileName == "" && pair.SnapshotFileName == "" { return &pair, nil, nil } return &pair, &prevPair, nil } else { return &pair, nil, nil } } func (s *RecoveryAdvisor) GetRecipe() (*RecoveryRecipe, error) { pair, prevPair, err := s.getSnapshotChangesPairs() if err != nil { return nil, err } if pair == nil { return nil, nil } if pair.SnapshotFileName != "" { isVerified, err := s.verifySnapshot(pair.SnapshotFileName) if err != nil { return nil, fmt.Errorf("verifySnapshot %s: %s", pair.SnapshotFileName, err) } if isVerified { recipe := &RecoveryRecipe{ Snapshot: pair.SnapshotFileName, Changes: []string{ pair.WALFileName, }, SnapshotNumber: pair.SnapshotNumber, } if prevPair != nil { if prevPair.WALFileName != "" { recipe.ToDelete = append(recipe.ToDelete, prevPair.WALFileName) } if prevPair.SnapshotFileName != "" { recipe.ToDelete = append(recipe.ToDelete, prevPair.SnapshotFileName) } } return recipe, nil } if prevPair != nil { return s.tryPrevPair(pair, prevPair) } return nil, fmt.Errorf("%d.shapshot is corrupted", pair.SnapshotNumber) } else { if prevPair != nil { return s.tryPrevPair(pair, prevPair) } else { if pair.SnapshotNumber == 1 { return &RecoveryRecipe{ Changes: []string{ pair.WALFileName, }, SnapshotNumber: pair.SnapshotNumber, }, nil } else { return nil, fmt.Errorf("%d.snapshot not found", pair.SnapshotNumber) } } } } func (s *RecoveryAdvisor) tryPrevPair(pair, prevPair *SnapshotChangesPair) (*RecoveryRecipe, error) { if prevPair.WALFileName == "" { if pair.SnapshotFileName != "" { return nil, fmt.Errorf("%d.shapshot is corrupted and %d.changes not found", pair.SnapshotNumber, prevPair.SnapshotNumber) } else { return nil, fmt.Errorf("%d.changes not found", prevPair.SnapshotNumber) } } if prevPair.SnapshotFileName == "" { if prevPair.SnapshotNumber == 1 { recipe := &RecoveryRecipe{ Changes: []string{ prevPair.WALFileName, pair.WALFileName, }, SnapshotNumber: pair.SnapshotNumber, CompleteSnapshot: true, ToDelete: []string{ prevPair.WALFileName, }, } return recipe, nil } else { if pair.SnapshotFileName != "" { return nil, fmt.Errorf("%d.shapshot is corrupted and %d.snapshot not found", pair.SnapshotNumber, prevPair.SnapshotNumber) } else { return nil, fmt.Errorf("%d.snapshot not found", pair.SnapshotNumber) } } } isVerified, err := s.verifySnapshot(prevPair.SnapshotFileName) if err != nil { return nil, fmt.Errorf("verifySnapshot %s: %s", prevPair.SnapshotFileName, err) } if !isVerified { return nil, fmt.Errorf("%d.shapshot is corrupted", prevPair.SnapshotNumber) } recipe := &RecoveryRecipe{ Snapshot: prevPair.SnapshotFileName, Changes: []string{ prevPair.WALFileName, pair.WALFileName, }, SnapshotNumber: pair.SnapshotNumber, CompleteSnapshot: true, ToDelete: []string{ prevPair.WALFileName, prevPair.SnapshotFileName, }, } return recipe, nil }