From 74ffac5126e7d0b5c7adaabb9b69b1012db94269 Mon Sep 17 00:00:00 2001 From: Adam Shannon Date: Fri, 18 Oct 2024 12:13:14 -0500 Subject: [PATCH] provider/pd: fixup schedule checking to find closest allowed check-in time --- internal/provider/pd/client.go | 6 +-- internal/provider/pd/client_test.go | 52 +++++++++++++++--- internal/provider/snooze/snooze.go | 36 +++++++++++-- internal/provider/snooze/snooze_test.go | 70 ++++++++++++++++++++++--- 4 files changed, 142 insertions(+), 22 deletions(-) diff --git a/internal/provider/pd/client.go b/internal/provider/pd/client.go index dd93ed5..b32a4a1 100644 --- a/internal/provider/pd/client.go +++ b/internal/provider/pd/client.go @@ -165,13 +165,13 @@ func (c *client) CheckIn(ctx context.Context, check config.Check) (time.Time, er } } - future := now.Add(wait) - _, wait, err = snooze.Calculate(future, check.Schedule) + // future := now.Add(wait) + _, wait, err = snooze.Calculate(scheduleTime, check.Schedule) if err != nil { return time.Time{}, fmt.Errorf("calculating second snooze: %w", err) } - future = future.Add(wait) + future := scheduleTime.Add(wait) logger.Info().Logf("snoozing incident %s until %v", inc.ID, future.Format(time.RFC3339)) err = c.snoozeIncident(ctx, logger, inc, service, now, future.Sub(now)) diff --git a/internal/provider/pd/client_test.go b/internal/provider/pd/client_test.go index b017417..3c5e545 100644 --- a/internal/provider/pd/client_test.go +++ b/internal/provider/pd/client_test.go @@ -2,6 +2,7 @@ package pd import ( "context" + "fmt" "os" "testing" "time" @@ -105,7 +106,7 @@ func TestClient_RejectedLateCheckIn(t *testing.T) { require.NoError(t, err) }) - nextCheckInExpected, err := pdc.CheckIn(ctx, config.Check{ + conf := config.Check{ Name: t.Name(), Schedule: config.ScheduleConfig{ Weekdays: &config.PartialDay{ @@ -113,15 +114,52 @@ func TestClient_RejectedLateCheckIn(t *testing.T) { Times: []string{ // Never allow the current time to check-in now.Add(-1*time.Hour - 30*time.Minute).Format("15:04"), + // Add a future time that's too far in the future + now.Add(2*time.Hour + 30*time.Minute).Format("15:04"), }, - Tolerance: "1m", + Tolerance: "5m", }, }, - }) - require.ErrorContains(t, err, "check-in is late by 1h29m") + } + nextCheckInExpected, err := pdc.CheckIn(ctx, conf) + require.ErrorContains(t, err, fmt.Sprintf("%s check-in is late by 1h25m", conf.Schedule.Weekdays.Times[0])) require.True(t, nextCheckInExpected.IsZero()) } +func TestClient_CheckInJustBefore(t *testing.T) { + ctx := context.Background() + + pdc := newTestClient(t) + + loc, err := time.LoadLocation("America/New_York") + require.NoError(t, err) + + timeService := stime.NewStaticTimeService() + timeService.Change(time.Date(2024, time.October, 16, 13, 59, 30, 0, loc)) // just before expected check-in + pdc.timeService = timeService + + t.Cleanup(func() { + service, err := pdc.findService(ctx, t.Name()) + require.NoError(t, err) + + err = pdc.deleteService(ctx, service) + require.NoError(t, err) + }) + + nextCheckInExpected, err := pdc.CheckIn(ctx, config.Check{ + Name: t.Name(), + Schedule: config.ScheduleConfig{ + Weekdays: &config.PartialDay{ + Timezone: "America/New_York", + Times: []string{"13:30", "14:00", "14:30"}, + Tolerance: "5m", + }, + }, + }) + require.NoError(t, err) + require.Equal(t, "14:35", nextCheckInExpected.Format("15:04")) +} + func TestClient_CheckInJustAfter(t *testing.T) { ctx := context.Background() @@ -131,7 +169,7 @@ func TestClient_CheckInJustAfter(t *testing.T) { require.NoError(t, err) timeService := stime.NewStaticTimeService() - timeService.Change(time.Date(2024, time.October, 16, 14, 0, 1, 0, loc)) // just after expected check-in + timeService.Change(time.Date(2024, time.October, 16, 14, 1, 1, 0, loc)) // just after expected check-in pdc.timeService = timeService t.Cleanup(func() { @@ -147,11 +185,11 @@ func TestClient_CheckInJustAfter(t *testing.T) { Schedule: config.ScheduleConfig{ Weekdays: &config.PartialDay{ Timezone: "America/New_York", - Times: []string{"14:00"}, + Times: []string{"13:30", "14:00", "14:30"}, Tolerance: "5m", }, }, }) require.NoError(t, err) - require.Equal(t, "14:05", nextCheckInExpected.Format("15:04")) + require.Equal(t, "14:35", nextCheckInExpected.Format("15:04")) } diff --git a/internal/provider/snooze/snooze.go b/internal/provider/snooze/snooze.go index f59915a..495716a 100644 --- a/internal/provider/snooze/snooze.go +++ b/internal/provider/snooze/snooze.go @@ -112,12 +112,38 @@ func Calculate(now time.Time, schedule config.ScheduleConfig) (time.Time, time.D tolerance = t } - // Find the next future hour:minute + // Find the hour:minute (within the tolerance) scheduled check-in which contains the current time. current := now.Format("15:04") - for _, hourminute := range times { - if hourminute.Format("15:04") > current { - hhmm := time.Date(now.Year(), now.Month(), now.Day(), hourminute.Hour(), hourminute.Minute(), 0, 0, now.Location()) - return hhmm, hhmm.Sub(now) + tolerance, nil + + for idx, hourminute := range times { + low := hourminute.Add(-1 * tolerance).Format("15:04") + high := hourminute.Add(tolerance).Format("15:04") + + scheduledCheckIn := time.Date(now.Year(), now.Month(), now.Day(), hourminute.Hour(), hourminute.Minute(), 0, 0, now.Location()) + + // If we're before all scheduled check-ins today + if idx == 0 && current < low { + return scheduledCheckIn, scheduledCheckIn.Sub(now) + tolerance, nil + } + + // The current time must be within our scheduled time +/- the tolerance + if low <= current && high >= current { + // Based on the scheduledCheckIn calculate how long to sleep for + var nextCheckIn time.Time + if len(times) > idx+1 { + nextCheckIn = times[idx+1] // the next time later today + } else { + nextCheckIn = times[0] // tomorrow's first time + nextCheckIn = nextCheckIn.AddDate(0, 0, 1) + } + + next := time.Date(now.Year(), now.Month(), now.Day(), nextCheckIn.Hour(), nextCheckIn.Minute(), 0, 0, now.Location()) + + if nextCheckIn.Day() > 1 { // zero value of time.Time is Jan 1 1970 + next = next.AddDate(0, 0, nextCheckIn.Day()-1) + } + + return scheduledCheckIn, next.Sub(now) + tolerance, nil } } diff --git a/internal/provider/snooze/snooze_test.go b/internal/provider/snooze/snooze_test.go index 6df9ca6..35e3df4 100644 --- a/internal/provider/snooze/snooze_test.go +++ b/internal/provider/snooze/snooze_test.go @@ -107,13 +107,15 @@ func TestSnooze_Weekdays(t *testing.T) { }) t.Run("09:00 tomorrow", func(t *testing.T) { + now = now.Add(5 * time.Minute) + schedule.Weekdays.Times = []string{"09:00", "09:10", "09:20"} clockTime, snooze, err := Calculate(now, schedule) require.NoError(t, err) require.Equal(t, "2024-10-07T09:00:00-04:00", clockTime.Format(time.RFC3339)) - require.Equal(t, "23h42m55s", snooze.String()) + require.Equal(t, "23h37m55s", snooze.String()) require.Equal(t, "2024-10-08T09:05:00-04:00", now.In(nyc).Add(snooze).Format(time.RFC3339)) }) } @@ -123,7 +125,7 @@ func TestSnooze_BankingDays(t *testing.T) { require.NoError(t, err) t.Run("13:00 tomorrow over weekend + holiday", func(t *testing.T) { - now := time.Date(2024, time.October, 11, 13, 22, 5, 0, time.UTC) + now := time.Date(2024, time.October, 11, 13, 26, 5, 0, time.UTC) var schedule config.ScheduleConfig schedule.BankingDays = &config.PartialDay{ @@ -136,22 +138,76 @@ func TestSnooze_BankingDays(t *testing.T) { require.NoError(t, err) require.Equal(t, "2024-10-11T09:00:00-04:00", clockTime.Format(time.RFC3339)) - require.Equal(t, "95h42m55s", snooze.String()) + require.Equal(t, "95h38m55s", snooze.String()) require.Equal(t, "2024-10-15T09:05:00-04:00", now.In(nyc).Add(snooze).Format(time.RFC3339)) }) } -func TestSnooze_JustAfter(t *testing.T) { +func TestSnooze_Close(t *testing.T) { nyc, err := time.LoadLocation("America/New_York") require.NoError(t, err) + t.Run("1s before 14:00 expected check-in", func(t *testing.T) { + now := time.Date(2024, time.October, 17, 13, 59, 59, 0, nyc) + + var schedule config.ScheduleConfig + schedule.BankingDays = &config.PartialDay{ + Timezone: "America/New_York", + Times: []string{"13:30", "14:00", "14:30"}, + Tolerance: "5m", + } + + clockTime, snooze, err := Calculate(now, schedule) + require.NoError(t, err) + + require.Equal(t, "2024-10-17T14:00:00-04:00", clockTime.Format(time.RFC3339)) + require.Equal(t, "35m1s", snooze.String()) + require.Equal(t, "2024-10-17T14:35:00-04:00", now.In(nyc).Add(snooze).Format(time.RFC3339)) + }) + t.Run("1s after 14:00 expected check-in", func(t *testing.T) { now := time.Date(2024, time.October, 17, 14, 0, 1, 0, nyc) var schedule config.ScheduleConfig schedule.BankingDays = &config.PartialDay{ Timezone: "America/New_York", - Times: []string{"14:00"}, + Times: []string{"13:30", "14:00", "14:30"}, + Tolerance: "5m", + } + + clockTime, snooze, err := Calculate(now, schedule) + require.NoError(t, err) + + require.Equal(t, "2024-10-17T14:00:00-04:00", clockTime.Format(time.RFC3339)) + require.Equal(t, "34m59s", snooze.String()) + require.Equal(t, "2024-10-17T14:35:00-04:00", now.In(nyc).Add(snooze).Format(time.RFC3339)) + }) + + t.Run("1m before 14:00 expected check-in", func(t *testing.T) { + now := time.Date(2024, time.October, 17, 13, 59, 0, 0, nyc) + + var schedule config.ScheduleConfig + schedule.BankingDays = &config.PartialDay{ + Timezone: "America/New_York", + Times: []string{"13:30", "14:00", "14:30"}, + Tolerance: "5m", + } + + clockTime, snooze, err := Calculate(now, schedule) + require.NoError(t, err) + + require.Equal(t, "2024-10-17T14:00:00-04:00", clockTime.Format(time.RFC3339)) + require.Equal(t, "36m0s", snooze.String()) + require.Equal(t, "2024-10-17T14:35:00-04:00", now.In(nyc).Add(snooze).Format(time.RFC3339)) + }) + + t.Run("1m after 14:00 expected check-in", func(t *testing.T) { + now := time.Date(2024, time.October, 17, 14, 1, 0, 0, nyc) + + var schedule config.ScheduleConfig + schedule.BankingDays = &config.PartialDay{ + Timezone: "America/New_York", + Times: []string{"13:30", "14:00", "14:30"}, Tolerance: "5m", } @@ -159,7 +215,7 @@ func TestSnooze_JustAfter(t *testing.T) { require.NoError(t, err) require.Equal(t, "2024-10-17T14:00:00-04:00", clockTime.Format(time.RFC3339)) - require.Equal(t, "24h4m59s", snooze.String()) - require.Equal(t, "2024-10-18T14:05:00-04:00", now.In(nyc).Add(snooze).Format(time.RFC3339)) + require.Equal(t, "34m0s", snooze.String()) + require.Equal(t, "2024-10-17T14:35:00-04:00", now.In(nyc).Add(snooze).Format(time.RFC3339)) }) }