186 lines
3.7 KiB
Go
186 lines
3.7 KiB
Go
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)
|
|
}
|