diff --git a/internal/scrobble/worker.go b/internal/scrobble/worker.go new file mode 100644 index 00000000..89298602 --- /dev/null +++ b/internal/scrobble/worker.go @@ -0,0 +1,30 @@ +package scrobble + +import ( + "time" +) + +// backoffSchedule maps the failure count (1-indexed: 1 = first failure +// just happened) to the delay before the next attempt. Per spec §M4a: +// 1m → 5m → 30m → 2h → 6h, then give up. +var backoffSchedule = []time.Duration{ + 1 * time.Minute, + 5 * time.Minute, + 30 * time.Minute, + 2 * time.Hour, + 6 * time.Hour, +} + +const maxAttempts = 5 + +// backoffDelay returns the delay before the next attempt given the value +// of the `attempts` column AFTER the failure has been recorded (1-indexed: +// attempts=1 means the first failure just happened, the next retry should +// wait 1 minute). Returns (_, false) when attempts > maxAttempts (the +// caller should mark the row failed). +func backoffDelay(attempts int) (time.Duration, bool) { + if attempts < 1 || attempts > maxAttempts { + return 0, false + } + return backoffSchedule[attempts-1], true +} diff --git a/internal/scrobble/worker_test.go b/internal/scrobble/worker_test.go new file mode 100644 index 00000000..ed720782 --- /dev/null +++ b/internal/scrobble/worker_test.go @@ -0,0 +1,31 @@ +package scrobble + +import ( + "testing" + "time" +) + +func TestBackoffDelay(t *testing.T) { + cases := []struct { + attempts int + want time.Duration + ok bool + }{ + {1, 1 * time.Minute, true}, + {2, 5 * time.Minute, true}, + {3, 30 * time.Minute, true}, + {4, 2 * time.Hour, true}, + {5, 6 * time.Hour, true}, + {6, 0, false}, + {99, 0, false}, + } + for _, c := range cases { + got, ok := backoffDelay(c.attempts) + if ok != c.ok { + t.Errorf("backoffDelay(%d) ok = %v, want %v", c.attempts, ok, c.ok) + } + if got != c.want { + t.Errorf("backoffDelay(%d) = %v, want %v", c.attempts, got, c.want) + } + } +}