From 789a739a5bc034ca67ffd43c0deca9db2e46bf0e Mon Sep 17 00:00:00 2001 From: Jarek Kowalski Date: Mon, 18 Oct 2021 22:55:11 -0700 Subject: [PATCH] server: fixed next snapshot time computation (#1418) Fixes #1405 Moved logic to SchedulingPolicy, added unit tests. --- internal/server/source_manager.go | 25 +--- snapshot/policy/scheduling_policy.go | 40 ++++++ snapshot/policy/scheduling_policy_test.go | 162 ++++++++++++++++++++++ 3 files changed, 206 insertions(+), 21 deletions(-) create mode 100644 snapshot/policy/scheduling_policy_test.go diff --git a/internal/server/source_manager.go b/internal/server/source_manager.go index 0e30cc8cd..6ba90b305 100644 --- a/internal/server/source_manager.go +++ b/internal/server/source_manager.go @@ -312,29 +312,12 @@ func (s *sourceManager) snapshotInternal(ctx context.Context, ctrl uitask.Contro } func (s *sourceManager) findClosestNextSnapshotTime() *time.Time { - var nextSnapshotTime *time.Time - - // compute next snapshot time based on interval - if interval := s.pol.IntervalSeconds; interval != 0 { - interval := time.Duration(interval) * time.Second - nt := s.lastSnapshot.StartTime.Add(interval).Truncate(interval) - nextSnapshotTime = &nt + t, ok := s.pol.NextSnapshotTime(s.lastCompleteSnapshot.StartTime, clock.Now()) + if !ok { + return nil } - for _, tod := range s.pol.TimesOfDay { - nowLocalTime := clock.Now().Local() - localSnapshotTime := time.Date(nowLocalTime.Year(), nowLocalTime.Month(), nowLocalTime.Day(), tod.Hour, tod.Minute, 0, 0, time.Local) - - if tod.Hour < nowLocalTime.Hour() || (tod.Hour == nowLocalTime.Hour() && tod.Minute < nowLocalTime.Minute()) { - localSnapshotTime = localSnapshotTime.Add(oneDay) - } - - if nextSnapshotTime == nil || localSnapshotTime.Before(*nextSnapshotTime) { - nextSnapshotTime = &localSnapshotTime - } - } - - return nextSnapshotTime + return &t } func (s *sourceManager) refreshStatus(ctx context.Context) { diff --git a/snapshot/policy/scheduling_policy.go b/snapshot/policy/scheduling_policy.go index d01ad715f..75def143c 100644 --- a/snapshot/policy/scheduling_policy.go +++ b/snapshot/policy/scheduling_policy.go @@ -70,6 +70,46 @@ func (p *SchedulingPolicy) SetInterval(d time.Duration) { p.IntervalSeconds = int64(d.Seconds()) } +// NextSnapshotTime computes next snapshot time given previous +// snapshot time and current wall clock time. +func (p *SchedulingPolicy) NextSnapshotTime(previousSnapshotTime, now time.Time) (time.Time, bool) { + const oneDay = 24 * time.Hour + + var ( + nextSnapshotTime time.Time + ok bool + ) + + // compute next snapshot time based on interval + if interval := p.IntervalSeconds; interval != 0 { + interval := time.Duration(interval) * time.Second + + nt := previousSnapshotTime.Add(interval).Truncate(interval) + nextSnapshotTime = nt + ok = true + + if nextSnapshotTime.Before(now) { + nextSnapshotTime = now + } + } + + for _, tod := range p.TimesOfDay { + nowLocalTime := now.Local() + localSnapshotTime := time.Date(nowLocalTime.Year(), nowLocalTime.Month(), nowLocalTime.Day(), tod.Hour, tod.Minute, 0, 0, time.Local) + + if now.After(localSnapshotTime) { + localSnapshotTime = localSnapshotTime.Add(oneDay) + } + + if !ok || localSnapshotTime.Before(nextSnapshotTime) { + nextSnapshotTime = localSnapshotTime + ok = true + } + } + + return nextSnapshotTime, ok +} + // Merge applies default values from the provided policy. func (p *SchedulingPolicy) Merge(src SchedulingPolicy) { if p.IntervalSeconds == 0 { diff --git a/snapshot/policy/scheduling_policy_test.go b/snapshot/policy/scheduling_policy_test.go new file mode 100644 index 000000000..14c888cc2 --- /dev/null +++ b/snapshot/policy/scheduling_policy_test.go @@ -0,0 +1,162 @@ +package policy_test + +import ( + "testing" + "time" + + "github.com/stretchr/testify/require" + + "github.com/kopia/kopia/snapshot/policy" +) + +func TestNextSnapshotTime(t *testing.T) { + cases := []struct { + pol policy.SchedulingPolicy + now time.Time + previousSnapshotTime time.Time + wantTime time.Time + wantOK bool + }{ + {}, // empty policy, no snapshot + { + // next snapshot is 1 minute after last, which is in the past + pol: policy.SchedulingPolicy{IntervalSeconds: 60}, + now: time.Date(2020, time.January, 1, 12, 3, 0, 0, time.Local), + previousSnapshotTime: time.Date(2020, time.January, 1, 11, 50, 0, 0, time.Local), + wantTime: time.Date(2020, time.January, 1, 12, 3, 0, 0, time.Local), + wantOK: true, + }, + { + pol: policy.SchedulingPolicy{IntervalSeconds: 60}, + now: time.Date(2020, time.January, 1, 11, 50, 30, 0, time.Local), + previousSnapshotTime: time.Date(2020, time.January, 1, 11, 50, 0, 0, time.Local), + wantTime: time.Date(2020, time.January, 1, 11, 51, 0, 0, time.Local), + wantOK: true, + }, + { + pol: policy.SchedulingPolicy{IntervalSeconds: 300}, + now: time.Date(2020, time.January, 1, 11, 50, 30, 0, time.Local), + previousSnapshotTime: time.Date(2020, time.January, 1, 11, 51, 0, 0, time.Local), + wantTime: time.Date(2020, time.January, 1, 11, 55, 0, 0, time.Local), + wantOK: true, + }, + { + // next time after 11:50 truncated to 20 full minutes, which is 12:00 + pol: policy.SchedulingPolicy{IntervalSeconds: 1200}, + now: time.Date(2020, time.January, 1, 11, 50, 30, 0, time.Local), + previousSnapshotTime: time.Date(2020, time.January, 1, 11, 50, 0, 0, time.Local), + wantTime: time.Date(2020, time.January, 1, 12, 0, 0, 0, time.Local), + wantOK: true, + }, + { + // next time after 11:50 truncated to 20 full minutes, which is 12:00 + pol: policy.SchedulingPolicy{IntervalSeconds: 1200}, + now: time.Date(2020, time.January, 1, 11, 50, 30, 0, time.Local), + previousSnapshotTime: time.Date(2020, time.January, 1, 11, 50, 0, 0, time.Local), + wantTime: time.Date(2020, time.January, 1, 12, 0, 0, 0, time.Local), + wantOK: true, + }, + { + pol: policy.SchedulingPolicy{ + TimesOfDay: []policy.TimeOfDay{{11, 55}, {11, 57}}, + }, + now: time.Date(2020, time.January, 1, 11, 50, 30, 0, time.Local), + previousSnapshotTime: time.Date(2020, time.January, 1, 11, 50, 0, 0, time.Local), + wantTime: time.Date(2020, time.January, 1, 11, 55, 0, 0, time.Local), + wantOK: true, + }, + { + pol: policy.SchedulingPolicy{ + TimesOfDay: []policy.TimeOfDay{{11, 55}, {11, 57}}, + }, + now: time.Date(2020, time.January, 1, 11, 55, 30, 0, time.Local), + wantTime: time.Date(2020, time.January, 1, 11, 57, 0, 0, time.Local), + wantOK: true, + }, + { + pol: policy.SchedulingPolicy{ + IntervalSeconds: 300, // every 5 minutes + TimesOfDay: []policy.TimeOfDay{{11, 54}, {11, 57}}, + }, + previousSnapshotTime: time.Date(2020, time.January, 1, 11, 50, 0, 0, time.Local), + now: time.Date(2020, time.January, 1, 11, 53, 0, 0, time.Local), + wantTime: time.Date(2020, time.January, 1, 11, 54, 0, 0, time.Local), + wantOK: true, + }, + { + pol: policy.SchedulingPolicy{ + IntervalSeconds: 300, // every 5 minutes + TimesOfDay: []policy.TimeOfDay{{11, 54}, {11, 57}}, + }, + previousSnapshotTime: time.Date(2020, time.January, 1, 11, 50, 0, 0, time.Local), + now: time.Date(2020, time.January, 1, 11, 54, 0, 0, time.Local), + wantTime: time.Date(2020, time.January, 1, 11, 54, 0, 0, time.Local), + wantOK: true, + }, + { + pol: policy.SchedulingPolicy{ + IntervalSeconds: 300, // every 5 minutes + TimesOfDay: []policy.TimeOfDay{{11, 54}, {11, 57}}, + }, + previousSnapshotTime: time.Date(2020, time.January, 1, 11, 50, 0, 0, time.Local), + now: time.Date(2020, time.January, 1, 11, 54, 1, 0, time.Local), + wantTime: time.Date(2020, time.January, 1, 11, 55, 0, 0, time.Local), + wantOK: true, + }, + { + pol: policy.SchedulingPolicy{ + IntervalSeconds: 300, // every 5 minutes + TimesOfDay: []policy.TimeOfDay{{11, 54}, {11, 57}}, + }, + previousSnapshotTime: time.Date(2020, time.January, 1, 11, 50, 0, 0, time.Local), + now: time.Date(2020, time.January, 1, 11, 55, 0, 0, time.Local), + wantTime: time.Date(2020, time.January, 1, 11, 55, 0, 0, time.Local), + wantOK: true, + }, + { + pol: policy.SchedulingPolicy{ + IntervalSeconds: 300, // every 5 minutes + TimesOfDay: []policy.TimeOfDay{{11, 54}, {11, 57}}, + }, + previousSnapshotTime: time.Date(2020, time.January, 1, 11, 50, 0, 0, time.Local), + now: time.Date(2020, time.January, 1, 11, 55, 1, 0, time.Local), + // interval-based snapshot is overdue + wantTime: time.Date(2020, time.January, 1, 11, 55, 1, 0, time.Local), + wantOK: true, + }, + { + pol: policy.SchedulingPolicy{ + TimesOfDay: []policy.TimeOfDay{{11, 54}, {11, 57}}, + }, + previousSnapshotTime: time.Date(2020, time.January, 1, 11, 50, 0, 0, time.Local), + now: time.Date(2020, time.January, 1, 11, 56, 0, 0, time.Local), + wantTime: time.Date(2020, time.January, 1, 11, 57, 0, 0, time.Local), + wantOK: true, + }, + { + pol: policy.SchedulingPolicy{ + TimesOfDay: []policy.TimeOfDay{{11, 54}, {11, 57}}, + }, + previousSnapshotTime: time.Date(2020, time.January, 1, 11, 50, 0, 0, time.Local), + now: time.Date(2020, time.January, 1, 11, 57, 0, 0, time.Local), + wantTime: time.Date(2020, time.January, 1, 11, 57, 0, 0, time.Local), + wantOK: true, + }, + { + pol: policy.SchedulingPolicy{ + TimesOfDay: []policy.TimeOfDay{{11, 54}, {11, 57}}, + }, + previousSnapshotTime: time.Date(2020, time.January, 1, 11, 50, 0, 0, time.Local), + now: time.Date(2020, time.January, 1, 11, 57, 0, 1, time.Local), + wantTime: time.Date(2020, time.January, 2, 11, 54, 0, 0, time.Local), + wantOK: true, + }, + } + + for _, tc := range cases { + gotTime, gotOK := tc.pol.NextSnapshotTime(tc.previousSnapshotTime, tc.now) + + require.Equal(t, tc.wantTime, gotTime) + require.Equal(t, tc.wantOK, gotOK) + } +}