refactored and cleaned up policy management, added retention tags to snapshot list

This commit is contained in:
Jarek Kowalski
2018-05-17 20:31:37 -07:00
parent c907580aaf
commit ec779b14c7
8 changed files with 349 additions and 317 deletions

View File

@@ -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

View File

@@ -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
}

View File

@@ -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))
}

68
snapshot/files_policy.go Normal file
View File

@@ -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{}

View File

@@ -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
}

View File

@@ -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
}

View File

@@ -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
}
}

View File

@@ -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{}