FEATURE (backups): Trigger backup in scheduled time independently of manual backups

This commit is contained in:
Rostislav Dugin
2025-06-06 21:24:14 +03:00
parent ef4bb81087
commit eea3f7200e
3 changed files with 227 additions and 64 deletions

View File

@@ -195,7 +195,11 @@ func runMigrations(log *slog.Logger) {
log.Info("Running database migrations...")
cmd := exec.Command("goose", "up")
cmd.Env = append(os.Environ(), "GOOSE_DRIVER=postgres", "GOOSE_DBSTRING="+config.GetEnv().DatabaseDsn)
cmd.Env = append(
os.Environ(),
"GOOSE_DRIVER=postgres",
"GOOSE_DBSTRING="+config.GetEnv().DatabaseDsn,
)
// Set the working directory to where migrations are located
cmd.Dir = "./migrations"

View File

@@ -96,6 +96,7 @@ func (i *Interval) shouldTriggerDaily(now, lastBackup time.Time) bool {
}
}
}
// no TimeOfDay: if it's a new calendar day
return !isSameDay(lastBackup, now)
}
@@ -104,35 +105,45 @@ func (i *Interval) shouldTriggerDaily(now, lastBackup time.Time) bool {
func (i *Interval) shouldTriggerWeekly(now, lastBackup time.Time) bool {
if i.Weekday != nil {
targetWd := time.Weekday(*i.Weekday)
// Calculate the target datetime for this week
startOfWeek := getStartOfWeek(now)
// today is target weekday and no backup this week
if now.Weekday() == targetWd && lastBackup.Before(startOfWeek) {
if i.TimeOfDay != nil {
t, err := time.Parse("15:04", *i.TimeOfDay)
if err == nil {
todayT := time.Date(
now.Year(),
now.Month(),
now.Day(),
t.Hour(),
t.Minute(),
0,
0,
now.Location(),
)
return now.After(todayT) || now.Equal(todayT)
}
// Convert Go weekday to days from Monday: Sunday=6, Monday=0, Tuesday=1, ..., Saturday=5
var daysFromMonday int
if targetWd == time.Sunday {
daysFromMonday = 6
} else {
daysFromMonday = int(targetWd) - 1
}
targetThisWeek := startOfWeek.AddDate(0, 0, daysFromMonday)
if i.TimeOfDay != nil {
t, err := time.Parse("15:04", *i.TimeOfDay)
if err == nil {
targetThisWeek = time.Date(
targetThisWeek.Year(),
targetThisWeek.Month(),
targetThisWeek.Day(),
t.Hour(),
t.Minute(),
0,
0,
targetThisWeek.Location(),
)
}
return true
}
// passed this week's slot and missed entirely
targetThisWeek := startOfWeek.AddDate(0, 0, int(targetWd))
if now.After(targetThisWeek) && lastBackup.Before(startOfWeek) {
return true
// If current time is at or after the target time this week
// and no backup has been made at or after the target time, trigger
if now.After(targetThisWeek) || now.Equal(targetThisWeek) {
return lastBackup.Before(targetThisWeek)
}
return false
}
// no Weekday: generic 7-day interval
return now.Sub(lastBackup) >= 7*24*time.Hour
}
@@ -141,32 +152,32 @@ func (i *Interval) shouldTriggerWeekly(now, lastBackup time.Time) bool {
func (i *Interval) shouldTriggerMonthly(now, lastBackup time.Time) bool {
if i.DayOfMonth != nil {
day := *i.DayOfMonth
startOfMonth := getStartOfMonth(now)
// today is target day and no backup this month
if now.Day() == day && lastBackup.Before(startOfMonth) {
if i.TimeOfDay != nil {
t, err := time.Parse("15:04", *i.TimeOfDay)
if err == nil {
todayT := time.Date(
now.Year(),
now.Month(),
now.Day(),
t.Hour(),
t.Minute(),
0,
0,
now.Location(),
)
return now.After(todayT) || now.Equal(todayT)
}
// Calculate the target datetime for this month
targetThisMonth := time.Date(now.Year(), now.Month(), day, 0, 0, 0, 0, now.Location())
if i.TimeOfDay != nil {
t, err := time.Parse("15:04", *i.TimeOfDay)
if err == nil {
targetThisMonth = time.Date(
targetThisMonth.Year(),
targetThisMonth.Month(),
targetThisMonth.Day(),
t.Hour(),
t.Minute(),
0,
0,
targetThisMonth.Location(),
)
}
return true
}
// passed this month's slot and missed entirely
if now.Day() > day && lastBackup.Before(startOfMonth) {
return true
// If current time is at or after the target time this month
// and no backup has been made at or after the target time, trigger
if now.After(targetThisMonth) || now.Equal(targetThisMonth) {
return lastBackup.Before(targetThisMonth)
}
return false
}
// no DayOfMonth: if we're in a new calendar month

View File

@@ -143,28 +143,30 @@ func TestInterval_ShouldTriggerBackup_Weekly(t *testing.T) {
})
t.Run(
"Backup already done this week (Wednesday 15:00): Do not trigger again",
"Backup already done at scheduled time (Wednesday 15:00): Do not trigger again",
func(t *testing.T) {
now := time.Date(2024, 1, 18, 10, 0, 0, 0, time.UTC) // Thursday
lastBackup := time.Date(2024, 1, 17, 15, 0, 0, 0, time.UTC) // Wednesday this week
now := time.Date(2024, 1, 18, 10, 0, 0, 0, time.UTC) // Thursday
// Wednesday this week at scheduled time
lastBackup := time.Date(
2024,
1,
17,
15,
0,
0,
0,
time.UTC,
)
should := interval.ShouldTriggerBackup(now, &lastBackup)
assert.False(t, should)
},
)
t.Run(
"Backup missed yesterday (it's Thursday): Trigger backup immediately",
func(t *testing.T) {
now := time.Date(2024, 1, 18, 10, 0, 0, 0, time.UTC) // Thursday
lastBackup := time.Date(2024, 1, 10, 15, 0, 0, 0, time.UTC) // Previous week
should := interval.ShouldTriggerBackup(now, &lastBackup)
assert.True(t, should)
},
)
t.Run(
"Backup last week: Trigger backup at this week's scheduled time or immediately if already missed",
"Manual backup before scheduled time should not prevent scheduled backup",
func(t *testing.T) {
// Wednesday at scheduled time
now := time.Date(
2024,
1,
@@ -174,12 +176,117 @@ func TestInterval_ShouldTriggerBackup_Weekly(t *testing.T) {
0,
0,
time.UTC,
) // Wednesday at scheduled time
)
// Manual backup same day, before scheduled time
lastBackup := time.Date(
2024,
1,
17,
10,
0,
0,
0,
time.UTC,
)
should := interval.ShouldTriggerBackup(now, &lastBackup)
assert.True(t, should)
},
)
t.Run(
"Manual backup after scheduled time should prevent another backup",
func(t *testing.T) {
now := time.Date(2024, 1, 18, 10, 0, 0, 0, time.UTC) // Thursday
// Manual backup after scheduled time
lastBackup := time.Date(
2024,
1,
17,
16,
0,
0,
0,
time.UTC,
)
should := interval.ShouldTriggerBackup(now, &lastBackup)
assert.False(t, should)
},
)
t.Run(
"Backup missed completely: Trigger backup immediately after scheduled time",
func(t *testing.T) {
// Thursday after missed Wednesday
now := time.Date(
2024,
1,
18,
10,
0,
0,
0,
time.UTC,
)
lastBackup := time.Date(2024, 1, 10, 15, 0, 0, 0, time.UTC) // Previous week
should := interval.ShouldTriggerBackup(now, &lastBackup)
assert.True(t, should)
},
)
t.Run(
"Backup last week: Trigger backup at this week's scheduled time",
func(t *testing.T) {
// Wednesday at scheduled time
now := time.Date(
2024,
1,
17,
15,
0,
0,
0,
time.UTC,
)
lastBackup := time.Date(2024, 1, 10, 15, 0, 0, 0, time.UTC) // Previous week
should := interval.ShouldTriggerBackup(now, &lastBackup)
assert.True(t, should)
},
)
t.Run(
"User's scenario: Weekly Friday 00:00 backup should trigger even after Wednesday manual backup",
func(t *testing.T) {
timeOfDay := "00:00"
weekday := 5 // Friday (0=Sunday, 1=Monday, ..., 5=Friday)
fridayInterval := &Interval{
ID: uuid.New(),
Interval: IntervalWeekly,
TimeOfDay: &timeOfDay,
Weekday: &weekday,
}
// Friday at 00:00 - scheduled backup time
friday := time.Date(2024, 1, 19, 0, 0, 0, 0, time.UTC) // Friday Jan 19, 2024
// Manual backup was done on Wednesday
wednesdayBackup := time.Date(
2024,
1,
17,
21,
0,
0,
0,
time.UTC,
) // Wednesday Jan 17, 2024 at 21:00
should := fridayInterval.ShouldTriggerBackup(friday, &wednesdayBackup)
assert.True(
t,
should,
"Friday scheduled backup should trigger despite Wednesday manual backup",
)
},
)
}
func TestInterval_ShouldTriggerBackup_Monthly(t *testing.T) {
@@ -238,17 +345,58 @@ func TestInterval_ShouldTriggerBackup_Monthly(t *testing.T) {
},
)
t.Run("Backup already performed this month: Do not trigger again", func(t *testing.T) {
t.Run("Backup already performed at scheduled time: Do not trigger again", func(t *testing.T) {
now := time.Date(2024, 1, 15, 10, 0, 0, 0, time.UTC)
lastBackup := time.Date(2024, 1, 10, 8, 0, 0, 0, time.UTC) // This month
lastBackup := time.Date(2024, 1, 10, 8, 0, 0, 0, time.UTC) // This month at scheduled time
should := interval.ShouldTriggerBackup(now, &lastBackup)
assert.False(t, should)
})
t.Run(
"Backup performed last month on schedule: Trigger backup this month at or after scheduled date/time",
"Manual backup before scheduled time should not prevent scheduled backup",
func(t *testing.T) {
now := time.Date(2024, 1, 10, 8, 0, 0, 0, time.UTC) // Scheduled time
// Manual backup earlier this month
lastBackup := time.Date(
2024,
1,
5,
10,
0,
0,
0,
time.UTC,
)
should := interval.ShouldTriggerBackup(now, &lastBackup)
assert.True(t, should)
},
)
t.Run(
"Manual backup after scheduled time should prevent another backup",
func(t *testing.T) {
now := time.Date(2024, 1, 15, 10, 0, 0, 0, time.UTC)
// Manual backup after scheduled time
lastBackup := time.Date(
2024,
1,
10,
9,
0,
0,
0,
time.UTC,
)
should := interval.ShouldTriggerBackup(now, &lastBackup)
assert.False(t, should)
},
)
t.Run(
"Backup performed last month on schedule: Trigger backup this month at scheduled time",
func(t *testing.T) {
now := time.Date(2024, 1, 10, 8, 0, 0, 0, time.UTC)
// Previous month at scheduled time
lastBackup := time.Date(
2023,
12,
@@ -258,7 +406,7 @@ func TestInterval_ShouldTriggerBackup_Monthly(t *testing.T) {
0,
0,
time.UTC,
) // Previous month at scheduled time
)
should := interval.ShouldTriggerBackup(now, &lastBackup)
assert.True(t, should)
},