package main import "time" type goalStatus string const ( active goalStatus = "active" paused goalStatus = "paused" blocked goalStatus = "blocked" ) type goal struct { Objective string `json:"objective"` Status goalStatus `json:"status"` CreatedAt int64 `json:"createdAt"` UpdatedAt int64 `json:"updatedAt"` } func sameGoal(a, b *goal) bool { return a != nil && b != nil && a.Objective == b.Objective && a.CreatedAt == b.CreatedAt } func sameState(a, b *goal) bool { return sameGoal(a, b) && a.Status == b.Status && a.UpdatedAt == b.UpdatedAt } type watch struct { Goal *goal `json:"goal,omitempty"` Turn turn `json:"turn"` Armed bool `json:"armed"` Known bool `json:"known"` Pending bool `json:"pending,omitempty"` Hold *goal `json:"hold,omitempty"` Source *intentLog `json:"source,omitempty"` Failures int `json:"failures"` Due time.Time `json:"due"` revision uint64 echo *goal seed *goal } func (w *watch) stop() { w.Armed = false w.Due = time.Time{} } func (w *watch) update(g *goal) { w.revision++ if w.Hold != nil { if !sameGoal(w.Hold, g) { w.Hold = nil } else if g.Status == active && g.Status == w.Hold.Status && !w.Pending && w.echo == nil { w.Hold = nil if g.Status == active { w.Armed = true } } else { if g.Status == w.Hold.Status { w.Goal = g } w.stop() return } } previous := w.Goal same := sameGoal(previous, g) if !same { w.stop() w.Known = false w.Failures = 0 } else if w.Armed || previous.Status == active { w.Known = true } w.Goal = g if g == nil { w.stop() return } switch g.Status { case active: w.Known = true if !same || previous.Status != active { w.Due = time.Time{} w.Armed = true } else if w.Turn.Status == "inProgress" { w.Armed = true } case blocked: if !same && w.Pending { w.Hold = g } default: w.stop() w.Hold = g } } func (w *watch) notice(g *goal) { if w.seed != nil { w.seed = nil return } if sameState(w.echo, g) { w.echo = nil return } if w.Hold != nil && sameGoal(w.Hold, g) { w.revision++ if g.Status == blocked || g.Status == active { return } w.Goal, w.Hold = g, g w.stop() return } if g != nil && g.Status == active && sameGoal(w.Goal, g) { w.revision++ return } w.update(g) } func (w *watch) observe(t turn) { if t.ID == "" { return } w.revision++ fresh := t.ID != w.Turn.ID if w.Known && (fresh || w.Turn.Status == "inProgress") && t.Status == "failed" && t.Error != nil && t.Error.Info.retryable() { w.Armed = true } if fresh { w.Due = time.Time{} if w.Goal != nil && t.Status == "inProgress" && w.Goal.Status == active { w.Armed = true } } w.Turn = t switch t.Status { case "completed": w.Due = time.Time{} w.Failures = 0 case "interrupted": w.stop() if w.Pending && w.Goal != nil { desired := *w.Goal desired.Status = paused w.Goal, w.Hold = &desired, &desired } case "failed": if t.Error == nil || !t.Error.Info.retryable() { w.stop() } } if w.Hold != nil { w.stop() } } func (w *watch) eligible() bool { return w.Hold == nil && w.Armed && w.Goal != nil && w.Goal.Status == blocked && w.Turn.Status == "failed" && w.Turn.Error != nil && w.Turn.Error.Info.retryable() } func (w *watch) schedule(now time.Time, base, cap time.Duration, jitter func(time.Duration) time.Duration) bool { if !w.eligible() || !w.Due.IsZero() { return false } w.Failures++ w.Due = now.Add(min(cap, jitter(backoff(base, cap, w.Failures)))) return true } func backoff(base, cap time.Duration, failures int) time.Duration { delay := base for i := 1; i < failures && delay < cap; i++ { if delay > cap/2 { return cap } delay *= 2 } return min(delay, cap) }