Skip to content

Commit

Permalink
server: fixed next snapshot time computation (#1418)
Browse files Browse the repository at this point in the history
Fixes #1405

Moved logic to SchedulingPolicy, added unit tests.
  • Loading branch information
jkowalski committed Oct 19, 2021
1 parent d5c619f commit 789a739
Show file tree
Hide file tree
Showing 3 changed files with 206 additions and 21 deletions.
25 changes: 4 additions & 21 deletions internal/server/source_manager.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
}

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
}
t, ok := s.pol.NextSnapshotTime(s.lastCompleteSnapshot.StartTime, clock.Now())
if !ok {
return nil
}

return nextSnapshotTime
return &t
}

func (s *sourceManager) refreshStatus(ctx context.Context) {
Expand Down
40 changes: 40 additions & 0 deletions snapshot/policy/scheduling_policy.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
162 changes: 162 additions & 0 deletions snapshot/policy/scheduling_policy_test.go
Original file line number Diff line number Diff line change
@@ -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)
}
}

0 comments on commit 789a739

Please sign in to comment.