Check GitHub releases at most every 6 hours and retry transient failures (release query, checksum or binary download) with exponential backoff from 1 minute up to 3 hours. Forced updates bypass the throttle.
5.9 KiB
AutoUpdate 6-Hour Interval and Exponential Backoff Implementation Plan
For agentic workers: REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (
- [ ]) syntax for tracking.
Goal: Throttle agent/edge auto-update checks to once every 6 hours, and apply exponential backoff on failure (doubling up to a maximum of 3 hours) to prevent exhausting GitHub API rate limits.
Architecture: Add check interval and exponential backoff state tracking to edgeupdater.Service protected by a mutex, with clock injection (NowFunc) for deterministic testing. Expose CanCheck(options UpdateOptions) bool and integrate it into both CheckAndUpdate and edgeheartbeat.TryAutoUpdate so unthrottled checks and noisy logs are eliminated during periodic heartbeat loops.
Tech Stack: Go (1.25), standard library (sync, time, net/http, log/slog).
Global Constraints
- Normal update check interval: 6 hours (
6 * time.Hour). - Initial backoff on failure: 1 minute (
1 * time.Minute), doubling on each consecutive failure. - Maximum retry backoff: 3 hours (
3 * time.Hour). - Success resets failure count and backoff, and sets next check to
now + 6h. - Force check (
UpdateOptions.Force = true) must bypass backoff and interval immediately. - TDD required: write failing tests first, watch them fail, implement minimal code, verify pass.
Task 1: Add Interval and Exponential Backoff to edgeupdater.Service
Files:
- Modify:
internal/apps/edge/updater/service.go - Test:
internal/apps/edge/updater/service_test.go
Interfaces:
-
Consumes:
UpdateOptions{Channel, TagName, Force} -
Produces:
(s *Service) CanCheck(options UpdateOptions) bool,(s *Service) CheckAndUpdate(...) errorwith throttling and exponential backoff -
Step 1: Write the failing tests in
service_test.go
Add tests covering:
-
TestCheckAndUpdateIntervalAndBackoff:- Initial state allows check.
- Successful check sets next check time 6 hours later. Calls before 6 hours return false on
CanCheckand are skipped inCheckAndUpdate. - After 6 hours, check is allowed again.
- Failed check (e.g. mock server returns 403) schedules next check 1 minute later.
- Second failed check schedules next check 2 minutes later.
- Subsequent failures double up to 3 hours (capped at 3 hours).
- Force check (
Force: true) always succeeds in running regardless of cooldown/backoff. - Success after failure resets backoff to initial and schedules next check in 6 hours.
-
Step 2: Run test to verify it fails
Run: go test -v ./internal/apps/edge/updater -run TestCheckAndUpdateIntervalAndBackoff
Expected: FAIL (fields or methods missing or behavior not implemented).
- Step 3: Write minimal implementation in
service.go
- Define constants:
const (
DefaultCheckInterval = 6 * time.Hour
DefaultInitialBackoff = 1 * time.Minute
DefaultMaxBackoff = 3 * time.Hour
)
- Extend
Config:
type Config struct {
LocalVersion string
AssetPrefix string
LogLabel string
CheckInterval time.Duration
InitialBackoff time.Duration
MaxBackoff time.Duration
NowFunc func() time.Time
}
- Extend
Servicestruct with:
type Service struct {
httpClient *http.Client
lastCheckKey string
localVersion string
assetPrefix string
logLabel string
checkInterval time.Duration
initialBackoff time.Duration
maxBackoff time.Duration
nowFunc func() time.Time
mu sync.Mutex
nextCheckTime time.Time
currentBackoff time.Duration
consecutiveFailures int
}
- Implement
CanCheck(options UpdateOptions) bool:
func (s *Service) CanCheck(options UpdateOptions) bool {
if options.Force {
return true
}
s.mu.Lock()
defer s.mu.Unlock()
if s.nextCheckTime.IsZero() {
return true
}
now := s.now()
return !now.Before(s.nextCheckTime)
}
- In
CheckAndUpdate:
-
Check
CanCheck(options). If false, returnnil. -
On
getReleaseerror: calculate next backoff (initial backoff on 1st failure, double previous backoff capped at maxBackoff on subsequent), incrementconsecutiveFailures, setnextCheckTime = now.Add(currentBackoff), return error. -
On
getReleasesuccess: resetconsecutiveFailures = 0, resetcurrentBackoff = initialBackoff, setnextCheckTime = now.Add(checkInterval). -
Step 4: Run test to verify it passes
Run: go test -v ./internal/apps/edge/updater/...
Expected: PASS.
Task 2: Integrate CanCheck in edgeheartbeat.TryAutoUpdate
Files:
- Modify:
internal/apps/edge/heartbeat/autoupdate.go - Test:
internal/apps/edge/heartbeat/autoupdate_test.go
Interfaces:
-
Consumes:
(s *Service) CanCheck(options UpdateOptions) bool -
Produces:
TryAutoUpdate(ctx context.Context, updater *edgeupdater.Service, settings *AutoUpdateSettings, logLabel string) -
Step 1: Write the failing test in
autoupdate_test.go
Test that TryAutoUpdate checks updater.CanCheck(opts) and skips both log output and CheckAndUpdate call when throttled.
- Step 2: Run test to verify it fails
Run: go test -v ./internal/apps/edge/heartbeat/...
Expected: FAIL.
- Step 3: Update
autoupdate.go
Update TryAutoUpdate to evaluate updater.CanCheck(opts) before logging checking for updates and executing CheckAndUpdate.
- Step 4: Run test to verify it passes
Run: go test -v ./internal/apps/edge/heartbeat/...
Expected: PASS.
Task 3: Full Verification of Edge and Agent Packages
Files:
-
Test: All tests in
internal/apps/edge/...andinternal/apps/agent/... -
Step 1: Run comprehensive tests
Run:
go test -v ./internal/apps/edge/... ./internal/apps/agent/...
Expected: PASS with zero failures or race conditions.