zrepl/pruning/keep_grid.go
Christian Schwarz 3a4e841c73 [#292] pruning: grid: add all snapshots that do not match the regex to the rule's destroy list
Before this patch, multiple grids with disjoint regexes would result in
no snapshots being destroyed at all.

fixes #292
2020-09-02 22:45:44 +02:00

134 lines
3.3 KiB
Go

package pruning
import (
"fmt"
"regexp"
"sort"
"time"
"github.com/pkg/errors"
"github.com/zrepl/zrepl/config"
"github.com/zrepl/zrepl/pruning/retentiongrid"
)
// KeepGrid fits snapshots that match a given regex into a retentiongrid.Grid,
// uses the most recent snapshot among those that match the regex as 'now',
// and deletes all snapshots that do not fit the grid specification.
type KeepGrid struct {
retentionGrid *retentiongrid.Grid
re *regexp.Regexp
}
func NewKeepGrid(in *config.PruneGrid) (p *KeepGrid, err error) {
if in.Regex == "" {
return nil, fmt.Errorf("Regex must not be empty")
}
re, err := regexp.Compile(in.Regex)
if err != nil {
return nil, errors.Wrap(err, "Regex is invalid")
}
return newKeepGrid(re, in.Grid)
}
func MustNewKeepGrid(regex, gridspec string) *KeepGrid {
ris, err := config.ParseRetentionIntervalSpec(gridspec)
if err != nil {
panic(err)
}
re := regexp.MustCompile(regex)
grid, err := newKeepGrid(re, ris)
if err != nil {
panic(err)
}
return grid
}
func newKeepGrid(re *regexp.Regexp, configIntervals []config.RetentionInterval) (*KeepGrid, error) {
if re == nil {
panic("re must not be nil")
}
if len(configIntervals) == 0 {
return nil, errors.New("retention grid must specify at least one interval")
}
intervals := make([]retentiongrid.Interval, len(configIntervals))
for i := range configIntervals {
intervals[i] = &configIntervals[i]
}
// Assert intervals are of increasing length (not necessarily required, but indicates config mistake)
lastDuration := time.Duration(0)
for i := range intervals {
if intervals[i].Length() < lastDuration {
// If all intervals before were keep=all, this is ok
allPrevKeepCountAll := true
for j := i - 1; allPrevKeepCountAll && j >= 0; j-- {
allPrevKeepCountAll = intervals[j].KeepCount() == config.RetentionGridKeepCountAll
}
if allPrevKeepCountAll {
goto isMonotonicIncrease
}
return nil, errors.New("retention grid interval length must be monotonically increasing")
}
isMonotonicIncrease:
lastDuration = intervals[i].Length()
}
return &KeepGrid{
retentionGrid: retentiongrid.NewGrid(intervals),
re: re,
}, nil
}
type retentionGridAdaptor struct {
Snapshot
}
func (a retentionGridAdaptor) LessThan(b retentiongrid.Entry) bool {
return a.Date().Before(b.Date())
}
// Prune filters snapshots with the retention grid.
func (p *KeepGrid) KeepRule(snaps []Snapshot) (destroyList []Snapshot) {
matching, notMatching := partitionSnapList(snaps, func(snapshot Snapshot) bool {
return p.re.MatchString(snapshot.Name())
})
// snaps that don't match the regex are not kept by this rule
destroyList = append(destroyList, notMatching...)
if len(matching) == 0 {
return destroyList
}
// Build adaptors for retention grid
adaptors := make([]retentiongrid.Entry, 0)
for i := range matching {
adaptors = append(adaptors, retentionGridAdaptor{matching[i]})
}
// determine 'now' edge
sort.SliceStable(adaptors, func(i, j int) bool {
return adaptors[i].LessThan(adaptors[j])
})
now := adaptors[len(adaptors)-1].Date()
// Evaluate retention grid
_, removea := p.retentionGrid.FitEntries(now, adaptors)
// Revert adaptors
for i := range removea {
destroyList = append(destroyList, removea[i].(retentionGridAdaptor).Snapshot)
}
return destroyList
}