mirror of
https://github.com/databasus/databasus.git
synced 2026-04-06 00:32:03 +02:00
FEATURE (backups): Trigger backup in scheduled time independently of manual backups
This commit is contained in:
@@ -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"
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user