From ec779b14c71afb8c06e5a770e9e56f35b295cf4a Mon Sep 17 00:00:00 2001 From: Jarek Kowalski Date: Thu, 17 May 2018 20:31:37 -0700 Subject: [PATCH] refactored and cleaned up policy management, added retention tags to snapshot list --- cli/command_policy_set.go | 5 +- cli/command_snapshot_expire.go | 188 +++++---------------------------- cli/command_snapshot_list.go | 92 ++++++++-------- snapshot/files_policy.go | 68 ++++++++++++ snapshot/manifest.go | 36 ++++++- snapshot/policy.go | 111 +------------------ snapshot/retention_policy.go | 150 ++++++++++++++++++++++++++ snapshot/scheduling_policy.go | 16 +++ 8 files changed, 349 insertions(+), 317 deletions(-) create mode 100644 snapshot/files_policy.go create mode 100644 snapshot/retention_policy.go create mode 100644 snapshot/scheduling_policy.go diff --git a/cli/command_policy_set.go b/cli/command_policy_set.go index ae7cd950d..c275a87b9 100644 --- a/cli/command_policy_set.go +++ b/cli/command_policy_set.go @@ -95,6 +95,7 @@ func setPolicyFromFlags(target snapshot.SourceInfo, p *snapshot.Policy) error { // It's not really a list, just optional boolean. for _, inherit := range *policySetInherit { p.NoParent = !inherit + break } if *policySetClearExclude { @@ -108,8 +109,10 @@ func setPolicyFromFlags(target snapshot.SourceInfo, p *snapshot.Policy) error { p.FilesPolicy.Include = addRemoveDedupeAndSort(p.FilesPolicy.Include, *policySetAddInclude, *policySetRemoveInclude) } + // It's not really a list, just optional value. for _, freq := range *policySetFrequency { - p.SchedulingPolicy.Frequency = freq + p.SchedulingPolicy.MaxFrequency = &freq + break } return nil diff --git a/cli/command_snapshot_expire.go b/cli/command_snapshot_expire.go index b041798cb..eb1e963e7 100644 --- a/cli/command_snapshot_expire.go +++ b/cli/command_snapshot_expire.go @@ -4,9 +4,7 @@ "context" "fmt" "os" - "sort" "strings" - "time" "github.com/rs/zerolog/log" @@ -24,121 +22,6 @@ snapshotExpireDelete = snapshotExpireCommand.Flag("delete", "Whether to actually delete snapshots").Default("no").String() ) -type cutoffTimes struct { - annual time.Time - monthly time.Time - daily time.Time - hourly time.Time - weekly time.Time -} - -func yearsAgo(base time.Time, n int) time.Time { - return base.AddDate(-n, 0, 0) -} - -func monthsAgo(base time.Time, n int) time.Time { - return base.AddDate(0, -n, 0) -} - -func daysAgo(base time.Time, n int) time.Time { - return base.AddDate(0, 0, -n) -} - -func weeksAgo(base time.Time, n int) time.Time { - return base.AddDate(0, 0, -n*7) -} - -func hoursAgo(base time.Time, n int) time.Time { - return base.Add(time.Duration(-n) * time.Hour) -} - -func expireSnapshotsForSingleSource(snapshots []*snapshot.Manifest, src snapshot.SourceInfo, pol *snapshot.Policy, snapshotNames []string) []string { - var toDelete []string - - now := time.Now() - maxTime := now.Add(365 * 24 * time.Hour) - - cutoffTime := func(setting *int, add func(time.Time, int) time.Time) time.Time { - if setting != nil { - return add(now, *setting) - } - - return maxTime - } - - cutoff := cutoffTimes{ - annual: cutoffTime(pol.RetentionPolicy.KeepAnnual, yearsAgo), - monthly: cutoffTime(pol.RetentionPolicy.KeepMonthly, monthsAgo), - daily: cutoffTime(pol.RetentionPolicy.KeepDaily, daysAgo), - hourly: cutoffTime(pol.RetentionPolicy.KeepHourly, hoursAgo), - weekly: cutoffTime(pol.RetentionPolicy.KeepHourly, weeksAgo), - } - - fmt.Printf("\nProcessing %v\n", src) - ids := make(map[string]bool) - idCounters := make(map[string]int) - - for i, s := range snapshots { - keep := getReasonsToKeep(i, s, cutoff, pol, ids, idCounters) - - tm := s.StartTime.Local().Format("2006-01-02 15:04:05 MST") - if len(keep) > 0 { - fmt.Printf(" keeping %v (%v) %v\n", tm, s.ID, strings.Join(keep, ",")) - } else { - fmt.Printf(" deleting %v (%v)\n", tm, s.ID) - toDelete = append(toDelete, s.ID) - } - } - - return toDelete -} - -func getReasonsToKeep(i int, s *snapshot.Manifest, cutoff cutoffTimes, pol *snapshot.Policy, ids map[string]bool, idCounters map[string]int) []string { - if s.IncompleteReason != "" { - return nil - } - - var keepReasons []string - var zeroTime time.Time - - yyyy, wk := s.StartTime.ISOWeek() - - cases := []struct { - cutoffTime time.Time - timePeriodID string - timePeriodType string - max *int - }{ - {zeroTime, fmt.Sprintf("%v", i), "latest", pol.RetentionPolicy.KeepLatest}, - {cutoff.annual, s.StartTime.Format("2006"), "annual", pol.RetentionPolicy.KeepAnnual}, - {cutoff.monthly, s.StartTime.Format("2006-01"), "monthly", pol.RetentionPolicy.KeepMonthly}, - {cutoff.weekly, fmt.Sprintf("%04v-%02v", yyyy, wk), "weekly", pol.RetentionPolicy.KeepWeekly}, - {cutoff.daily, s.StartTime.Format("2006-01-02"), "daily", pol.RetentionPolicy.KeepDaily}, - {cutoff.hourly, s.StartTime.Format("2006-01-02 15"), "hourly", pol.RetentionPolicy.KeepHourly}, - } - - for _, c := range cases { - if c.max == nil { - continue - } - if s.StartTime.Before(c.cutoffTime) { - continue - } - - if _, exists := ids[c.timePeriodID]; exists { - continue - } - - if idCounters[c.timePeriodType] < *c.max { - ids[c.timePeriodID] = true - idCounters[c.timePeriodType]++ - keepReasons = append(keepReasons, c.timePeriodType) - } - } - - return keepReasons -} - func getSnapshotNamesToExpire(mgr *snapshot.Manager) ([]string, error) { if !*snapshotExpireAll && len(*snapshotExpirePaths) == 0 { return nil, fmt.Errorf("Must specify paths to expire or --all") @@ -173,56 +56,41 @@ func getSnapshotNamesToExpire(mgr *snapshot.Manager) ([]string, error) { } func expireSnapshots(pmgr *snapshot.PolicyManager, snapshots []*snapshot.Manifest, names []string) ([]string, error) { - var lastSource snapshot.SourceInfo - var pendingSnapshots []*snapshot.Manifest - var pendingNames []string var toDelete []string - - flush := func() error { - if len(pendingSnapshots) > 0 { - src := pendingSnapshots[0].Source - pol, err := pmgr.GetEffectivePolicy(src) - if err != nil { - return err - } - td := expireSnapshotsForSingleSource(pendingSnapshots, src, pol, pendingNames) - if len(td) == 0 { - fmt.Fprintf(os.Stderr, "Nothing to delete for %q.\n", src) - } else { - log.Printf("would delete %v out of %v snapshots for %q", len(td), len(pendingSnapshots), src) - toDelete = append(toDelete, td...) - } + for _, snapshotGroup := range snapshot.GroupBySource(snapshots) { + td, err := expireSnapshotsForSingleSource(pmgr, snapshotGroup) + if err != nil { + return nil, err } - pendingSnapshots = nil - pendingNames = nil - return nil + toDelete = append(toDelete, td...) } + return toDelete, nil +} - sort.Slice(snapshots, func(i, j int) bool { - s1, s2 := snapshots[i].Source, snapshots[j].Source - - if s1.String() != s2.String() { - return s1.String() < s2.String() - } - - return snapshots[i].StartTime.Before(snapshots[j].StartTime) - }) - - for i, s := range snapshots { - if s.Source != lastSource { - lastSource = s.Source - if err := flush(); err != nil { - return nil, err - } - } - - pendingSnapshots = append(pendingSnapshots, s) - pendingNames = append(pendingNames, names[i]) - } - if err := flush(); err != nil { +func expireSnapshotsForSingleSource(pmgr *snapshot.PolicyManager, snapshots []*snapshot.Manifest) ([]string, error) { + src := snapshots[0].Source + pol, err := pmgr.GetEffectivePolicy(src) + if err != nil { return nil, err } + pol.RetentionPolicy.ComputeRetentionReasons(snapshots) + + var toDelete []string + for _, s := range snapshots { + if len(s.RetentionReasons) == 0 { + log.Printf(" deleting %v", s.StartTime) + toDelete = append(toDelete, s.ID) + } else { + log.Printf(" keeping %v reasons: [%v]", s.StartTime, strings.Join(s.RetentionReasons, ",")) + } + } + if len(toDelete) == 0 { + fmt.Fprintf(os.Stderr, "Nothing to delete for %q.\n", src) + } else { + fmt.Printf("Would delete %v/%v snapshots for %v\n", len(toDelete), len(snapshots), src) + } + return toDelete, nil } diff --git a/cli/command_snapshot_list.go b/cli/command_snapshot_list.go index 6ad5e524f..bca5b7da2 100644 --- a/cli/command_snapshot_list.go +++ b/cli/command_snapshot_list.go @@ -4,7 +4,6 @@ "context" "fmt" "path/filepath" - "sort" "strings" "github.com/rs/zerolog/log" @@ -80,20 +79,35 @@ func runBackupsCommand(ctx context.Context, rep *repo.Repository) error { return err } - sort.Sort(manifestSorter(manifests)) - outputManifests(manifests, relPath) + polMgr := snapshot.NewPolicyManager(rep) + + outputManifestGroups(manifests, relPath, polMgr) return nil } -func outputManifests(manifests []*snapshot.Manifest, relPath string) { - var lastSource snapshot.SourceInfo +func outputManifestGroups(manifests []*snapshot.Manifest, relPath string, polMgr *snapshot.PolicyManager) { + separator := "" + for _, snapshotGroup := range snapshot.GroupBySource(manifests) { + src := snapshotGroup[0].Source + fmt.Printf("%v%v\n", separator, src) + separator = "\n" + + pol, err := polMgr.GetEffectivePolicy(src) + if err != nil { + log.Warn().Msgf("unable to determine effective policy for %v", src) + } else { + pol.RetentionPolicy.ComputeRetentionReasons(snapshotGroup) + } + outputManifestFromSingleSource(snapshotGroup, relPath) + } +} + +func outputManifestFromSingleSource(manifests []*snapshot.Manifest, relPath string) { var count int var lastTotalFileSize int64 - separator := "" - - for _, m := range manifests { + for _, m := range snapshot.SortByTime(manifests, false) { maybeIncomplete := "" if m.IncompleteReason != "" { if !*snapshotListIncludeIncomplete { @@ -102,32 +116,28 @@ func outputManifests(manifests []*snapshot.Manifest, relPath string) { maybeIncomplete = " " + m.IncompleteReason } - if m.Source != lastSource { - fmt.Printf("%v%v\n", separator, m.Source) - separator = "\n" - lastSource = m.Source - count = 0 - lastTotalFileSize = m.Stats.TotalFileSize + if count > *maxResultsPerPath { + return } - if count < *maxResultsPerPath { - fmt.Printf( - " %v %v%v %v %v%v\n", - m.StartTime.Format("2006-01-02 15:04:05 MST"), - m.RootObjectID, - relPath, - units.BytesStringBase10(m.Stats.TotalFileSize), - deltaBytes(m.Stats.TotalFileSize-lastTotalFileSize), - maybeIncomplete, - ) - if *snapshotListShowItemID { - fmt.Printf(" metadata: %v\n", m.ID) - } - if *snapshotListShowHashCache { - fmt.Printf(" hashcache: %v\n", m.HashCacheID) - } - count++ + fmt.Printf( + " %v %v%v %v %v %v %v\n", + m.StartTime.Format("2006-01-02 15:04:05 MST"), + m.RootObjectID, + relPath, + units.BytesStringBase10(m.Stats.TotalFileSize), + retentionReasonString(m.RetentionReasons), + deltaBytes(m.Stats.TotalFileSize-lastTotalFileSize), + maybeIncomplete, + ) + + if *snapshotListShowItemID { + fmt.Printf(" metadata: %v\n", m.ID) } + if *snapshotListShowHashCache { + fmt.Printf(" hashcache: %v\n", m.HashCacheID) + } + count++ if m.IncompleteReason == "" || !*snapshotListIncludeIncomplete { lastTotalFileSize = m.Stats.TotalFileSize @@ -135,19 +145,6 @@ func outputManifests(manifests []*snapshot.Manifest, relPath string) { } } -type manifestSorter []*snapshot.Manifest - -func (b manifestSorter) Len() int { return len(b) } -func (b manifestSorter) Less(i, j int) bool { - if c := strings.Compare(b[i].Source.String(), b[j].Source.String()); c != 0 { - return c < 0 - } - - return b[i].StartTime.UnixNano() < b[j].StartTime.UnixNano() -} - -func (b manifestSorter) Swap(i, j int) { b[i], b[j] = b[j], b[i] } - func deltaBytes(b int64) string { if b > 0 { return "(+" + units.BytesStringBase10(b) + ")" @@ -156,6 +153,13 @@ func deltaBytes(b int64) string { return "" } +func retentionReasonString(s []string) string { + if len(s) == 0 { + return "-" + } + return strings.Join(s, ",") +} + func init() { snapshotListCommand.Action(repositoryAction(runBackupsCommand)) } diff --git a/snapshot/files_policy.go b/snapshot/files_policy.go new file mode 100644 index 000000000..579ea074a --- /dev/null +++ b/snapshot/files_policy.go @@ -0,0 +1,68 @@ +package snapshot + +import ( + "path/filepath" + + "github.com/kopia/kopia/fs" + "github.com/rs/zerolog/log" +) + +// FilesPolicy describes files to be uploaded when taking snapshots +type FilesPolicy struct { + Include []string `json:"include,omitempty"` + Exclude []string `json:"exclude,omitempty"` + MaxSize *int `json:"maxSize,omitempty"` +} + +// ShouldInclude determines whether given filesystem entry should be included based on the policy. +func (p *FilesPolicy) ShouldInclude(e *fs.EntryMetadata) bool { + if len(p.Include) > 0 && !fileNameMatchesAnyPattern(e, p.Include) { + return false + } + + if len(p.Exclude) > 0 && fileNameMatchesAnyPattern(e, p.Include) { + return false + } + + if p.MaxSize != nil && e.Type == fs.EntryTypeFile && e.FileSize > int64(*p.MaxSize) { + return false + } + + return true +} + +func fileNameMatches(fname string, pattern string) bool { + ok, err := filepath.Match(pattern, fname) + if err != nil { + log.Printf("warning: %v, assuming %q does not match the pattern", err, fname) + return false + } + + return ok +} + +func fileNameMatchesAnyPattern(e *fs.EntryMetadata, patterns []string) bool { + for _, i := range patterns { + if fileNameMatches(e.Name, i) { + return true + } + } + + return false +} + +func mergeFilesPolicy(dst, src *FilesPolicy) { + if dst.MaxSize == nil { + dst.MaxSize = src.MaxSize + } + + if len(dst.Include) == 0 { + dst.Include = src.Include + } + + if len(dst.Exclude) == 0 { + dst.Exclude = src.Exclude + } +} + +var defaultFilesPolicy = &FilesPolicy{} diff --git a/snapshot/manifest.go b/snapshot/manifest.go index 446d7bb2f..905c9c8eb 100644 --- a/snapshot/manifest.go +++ b/snapshot/manifest.go @@ -1,6 +1,7 @@ package snapshot import ( + "sort" "time" "github.com/kopia/kopia/object" @@ -18,8 +19,37 @@ type Manifest struct { RootObjectID object.ID `json:"root"` HashCacheID object.ID `json:"hashCache"` HashCacheCutoffTime time.Time `json:"hashCacheCutoff"` + Stats Stats `json:"stats"` + IncompleteReason string `json:"incomplete,omitempty"` - Stats Stats `json:"stats"` - - IncompleteReason string `json:"incomplete,omitempty"` + RetentionReasons []string `json:"-"` +} + +// GroupBySource returns a slice of slices, such that each result item contains manifests from a single source. +func GroupBySource(manifests []*Manifest) [][]*Manifest { + resultMap := map[SourceInfo][]*Manifest{} + for _, m := range manifests { + resultMap[m.Source] = append(resultMap[m.Source], m) + } + + var result [][]*Manifest + for _, v := range resultMap { + result = append(result, v) + } + + sort.Slice(result, func(i, j int) bool { + return result[i][0].Source.String() < result[j][0].Source.String() + }) + + return result +} + +// SortByTime returns a slice of manifests sorted by start time. +func SortByTime(manifests []*Manifest, reverse bool) []*Manifest { + result := append([]*Manifest(nil), manifests...) + sort.Slice(result, func(i, j int) bool { + return result[i].StartTime.After(result[j].StartTime) == reverse + }) + + return result } diff --git a/snapshot/policy.go b/snapshot/policy.go index 36bd14292..7e2cfeb59 100644 --- a/snapshot/policy.go +++ b/snapshot/policy.go @@ -4,77 +4,13 @@ "bytes" "encoding/json" "errors" - "path/filepath" - "time" "github.com/rs/zerolog/log" - - "github.com/kopia/kopia/fs" ) // ErrPolicyNotFound is returned when the policy is not found. var ErrPolicyNotFound = errors.New("policy not found") -// RetentionPolicy describes snapshot retention policy. -type RetentionPolicy struct { - KeepLatest *int `json:"keepLatest,omitempty"` - KeepHourly *int `json:"keepHourly,omitempty"` - KeepDaily *int `json:"keepDaily,omitempty"` - KeepWeekly *int `json:"keepWeekly,omitempty"` - KeepMonthly *int `json:"keepMonthly,omitempty"` - KeepAnnual *int `json:"keepAnnual,omitempty"` -} - -var defaultRetentionPolicy = &RetentionPolicy{ - KeepLatest: intPtr(1), - KeepHourly: intPtr(48), - KeepDaily: intPtr(7), - KeepWeekly: intPtr(4), - KeepMonthly: intPtr(4), - KeepAnnual: intPtr(0), -} - -// FilesPolicy describes files to be uploaded when taking snapshots -type FilesPolicy struct { - Include []string `json:"include,omitempty"` - Exclude []string `json:"exclude,omitempty"` - MaxSize *int `json:"maxSize,omitempty"` -} - -// SchedulingPolicy describes policy for scheduling snapshots. -type SchedulingPolicy struct { - Frequency time.Duration `json:"frequency"` -} - -// ShouldInclude determines whether given filesystem entry should be included based on the policy. -func (p *FilesPolicy) ShouldInclude(e *fs.EntryMetadata) bool { - if len(p.Include) > 0 && !fileNameMatchesAnyPattern(e, p.Include) { - return false - } - - if len(p.Exclude) > 0 && fileNameMatchesAnyPattern(e, p.Include) { - return false - } - - if p.MaxSize != nil && e.Type == fs.EntryTypeFile && e.FileSize > int64(*p.MaxSize) { - return false - } - - return true -} - -func fileNameMatchesAnyPattern(e *fs.EntryMetadata, patterns []string) bool { - for _, i := range patterns { - if fileNameMatches(e.Name, i) { - return true - } - } - - return false -} - -var defaultFilesPolicy = &FilesPolicy{} - // Policy describes snapshot policy for a single source. type Policy struct { Labels map[string]string `json:"-"` @@ -95,16 +31,6 @@ func (p *Policy) String() string { return buf.String() } -func fileNameMatches(fname string, pattern string) bool { - ok, err := filepath.Match(pattern, fname) - if err != nil { - log.Printf("warning: %v, assuming %q does not match the pattern", err, fname) - return false - } - - return ok -} - // MergePolicies computes the policy by applying the specified list of policies in order. func MergePolicies(policies []*Policy) *Policy { var merged Policy @@ -116,50 +42,17 @@ func MergePolicies(policies []*Policy) *Policy { mergeRetentionPolicy(&merged.RetentionPolicy, &p.RetentionPolicy) mergeFilesPolicy(&merged.FilesPolicy, &p.FilesPolicy) + mergeSchedulingPolicy(&merged.SchedulingPolicy, &p.SchedulingPolicy) } // Merge default expiration policy. mergeRetentionPolicy(&merged.RetentionPolicy, defaultRetentionPolicy) mergeFilesPolicy(&merged.FilesPolicy, defaultFilesPolicy) + mergeSchedulingPolicy(&merged.SchedulingPolicy, defaultSchedulingPolicy) return &merged } -func mergeRetentionPolicy(dst, src *RetentionPolicy) { - if dst.KeepLatest == nil { - dst.KeepLatest = src.KeepLatest - } - if dst.KeepHourly == nil { - dst.KeepHourly = src.KeepHourly - } - if dst.KeepDaily == nil { - dst.KeepDaily = src.KeepDaily - } - if dst.KeepWeekly == nil { - dst.KeepWeekly = src.KeepWeekly - } - if dst.KeepMonthly == nil { - dst.KeepMonthly = src.KeepMonthly - } - if dst.KeepAnnual == nil { - dst.KeepAnnual = src.KeepAnnual - } -} - -func mergeFilesPolicy(dst, src *FilesPolicy) { - if dst.MaxSize == nil { - dst.MaxSize = src.MaxSize - } - - if len(dst.Include) == 0 { - dst.Include = src.Include - } - - if len(dst.Exclude) == 0 { - dst.Exclude = src.Exclude - } -} - func intPtr(n int) *int { return &n } diff --git a/snapshot/retention_policy.go b/snapshot/retention_policy.go new file mode 100644 index 000000000..8c94aab6e --- /dev/null +++ b/snapshot/retention_policy.go @@ -0,0 +1,150 @@ +package snapshot + +import ( + "fmt" + "time" +) + +// RetentionPolicy describes snapshot retention policy. +type RetentionPolicy struct { + KeepLatest *int `json:"keepLatest,omitempty"` + KeepHourly *int `json:"keepHourly,omitempty"` + KeepDaily *int `json:"keepDaily,omitempty"` + KeepWeekly *int `json:"keepWeekly,omitempty"` + KeepMonthly *int `json:"keepMonthly,omitempty"` + KeepAnnual *int `json:"keepAnnual,omitempty"` +} + +// ComputeRetentionReasons computes the reasons why each snapshot is retained, based on +// the settings in retention policy and stores them in RetentionReason field. +func (r *RetentionPolicy) ComputeRetentionReasons(manifests []*Manifest) { + now := time.Now() + maxTime := now.Add(365 * 24 * time.Hour) + + cutoffTime := func(setting *int, add func(time.Time, int) time.Time) time.Time { + if setting != nil { + return add(now, *setting) + } + + return maxTime + } + + cutoff := cutoffTimes{ + annual: cutoffTime(r.KeepAnnual, yearsAgo), + monthly: cutoffTime(r.KeepMonthly, monthsAgo), + daily: cutoffTime(r.KeepDaily, daysAgo), + hourly: cutoffTime(r.KeepHourly, hoursAgo), + weekly: cutoffTime(r.KeepHourly, weeksAgo), + } + + ids := make(map[string]bool) + idCounters := make(map[string]int) + + for i, s := range SortByTime(manifests, true) { + s.RetentionReasons = r.getRetentionReasons(i, s, cutoff, ids, idCounters) + } +} + +func (r *RetentionPolicy) getRetentionReasons(i int, s *Manifest, cutoff cutoffTimes, ids map[string]bool, idCounters map[string]int) []string { + if s.IncompleteReason != "" { + return nil + } + + var keepReasons []string + var zeroTime time.Time + + yyyy, wk := s.StartTime.ISOWeek() + + cases := []struct { + cutoffTime time.Time + timePeriodID string + timePeriodType string + max *int + }{ + {zeroTime, fmt.Sprintf("%v", i), "latest", r.KeepLatest}, + {cutoff.annual, s.StartTime.Format("2006"), "annual", r.KeepAnnual}, + {cutoff.monthly, s.StartTime.Format("2006-01"), "monthly", r.KeepMonthly}, + {cutoff.weekly, fmt.Sprintf("%04v-%02v", yyyy, wk), "weekly", r.KeepWeekly}, + {cutoff.daily, s.StartTime.Format("2006-01-02"), "daily", r.KeepDaily}, + {cutoff.hourly, s.StartTime.Format("2006-01-02 15"), "hourly", r.KeepHourly}, + } + + for _, c := range cases { + if c.max == nil { + continue + } + if s.StartTime.Before(c.cutoffTime) { + continue + } + + if _, exists := ids[c.timePeriodID]; exists { + continue + } + + if idCounters[c.timePeriodType] < *c.max { + ids[c.timePeriodID] = true + idCounters[c.timePeriodType]++ + keepReasons = append(keepReasons, c.timePeriodType) + } + } + + return keepReasons +} + +type cutoffTimes struct { + annual time.Time + monthly time.Time + daily time.Time + hourly time.Time + weekly time.Time +} + +func yearsAgo(base time.Time, n int) time.Time { + return base.AddDate(-n, 0, 0) +} + +func monthsAgo(base time.Time, n int) time.Time { + return base.AddDate(0, -n, 0) +} + +func daysAgo(base time.Time, n int) time.Time { + return base.AddDate(0, 0, -n) +} + +func weeksAgo(base time.Time, n int) time.Time { + return base.AddDate(0, 0, -n*7) +} + +func hoursAgo(base time.Time, n int) time.Time { + return base.Add(time.Duration(-n) * time.Hour) +} + +var defaultRetentionPolicy = &RetentionPolicy{ + KeepLatest: intPtr(1), + KeepHourly: intPtr(48), + KeepDaily: intPtr(7), + KeepWeekly: intPtr(4), + KeepMonthly: intPtr(4), + KeepAnnual: intPtr(0), +} + +func mergeRetentionPolicy(dst, src *RetentionPolicy) { + if dst.KeepLatest == nil { + dst.KeepLatest = src.KeepLatest + } + if dst.KeepHourly == nil { + dst.KeepHourly = src.KeepHourly + } + if dst.KeepDaily == nil { + dst.KeepDaily = src.KeepDaily + } + if dst.KeepWeekly == nil { + dst.KeepWeekly = src.KeepWeekly + } + if dst.KeepMonthly == nil { + dst.KeepMonthly = src.KeepMonthly + } + if dst.KeepAnnual == nil { + dst.KeepAnnual = src.KeepAnnual + } +} diff --git a/snapshot/scheduling_policy.go b/snapshot/scheduling_policy.go new file mode 100644 index 000000000..7542790f4 --- /dev/null +++ b/snapshot/scheduling_policy.go @@ -0,0 +1,16 @@ +package snapshot + +import "time" + +// SchedulingPolicy describes policy for scheduling snapshots. +type SchedulingPolicy struct { + MaxFrequency *time.Duration `json:"frequency"` +} + +func mergeSchedulingPolicy(dst, src *SchedulingPolicy) { + if dst.MaxFrequency == nil { + dst.MaxFrequency = src.MaxFrequency + } +} + +var defaultSchedulingPolicy = &SchedulingPolicy{}