Raw
  1package userbase
  2
  3import (
  4	"errors"
  5	"time"
  6
  7	"github.com/go-git/go-git/v5/plumbing"
  8
  9	"tug/internal/platform/replica"
 10)
 11
 12const (
 13	pollInterval    = 5 * time.Second
 14	quietPeriod     = 30 * time.Second
 15	pendingCap      = 5 * time.Minute
 16	dailyInterval   = 24 * time.Hour
 17	retryFirst      = 30 * time.Second
 18	retryMax        = 15 * time.Minute
 19	retryJitter     = 0.2
 20	recheckInterval = time.Hour
 21	pruneInterval   = 24 * time.Hour
 22)
 23
 24type action int
 25
 26const (
 27	actWait action = iota
 28	actPlan
 29	actStep
 30	actRecheck
 31)
 32
 33type refusal struct {
 34	kind string
 35	head plumbing.Hash
 36}
 37
 38type schedule struct {
 39	jitter func() float64
 40
 41	isPlanned bool
 42	stepNow   bool
 43	known     bool
 44	committed [32]byte
 45	dailyDue  time.Time
 46
 47	seenSet    bool
 48	seen       [32]byte
 49	quietSince time.Time
 50	dirtySince time.Time
 51
 52	retry     time.Time
 53	backoff   time.Duration
 54	movedOnce bool
 55
 56	refusing  bool
 57	refusalAt refusal
 58	audited   map[refusal]bool
 59
 60	lastPrune time.Time
 61}
 62
 63func newSchedule(jitter func() float64) *schedule {
 64	return &schedule{jitter: jitter, audited: map[refusal]bool{}}
 65}
 66
 67func (s *schedule) pending() bool {
 68	return !s.known || s.seen != s.committed
 69}
 70
 71func (s *schedule) settle(now time.Time) {
 72	switch {
 73	case !s.seenSet || !s.pending():
 74		s.dirtySince = time.Time{}
 75	case s.dirtySince.IsZero():
 76		s.dirtySince = now
 77	}
 78}
 79
 80func (s *schedule) observe(now time.Time, digest [32]byte) {
 81	if !s.seenSet || digest != s.seen {
 82		s.seen, s.seenSet, s.quietSince = digest, true, now
 83	}
 84	s.settle(now)
 85}
 86
 87func (s *schedule) planned(now time.Time, plan replica.StartPlan) {
 88	s.isPlanned = true
 89	s.stepNow = plan.StepNow
 90	s.known = !plan.StepNow && !plan.DailyDue.IsZero()
 91	s.committed = plan.Committed
 92	s.dailyDue = plan.DailyDue
 93	s.clearFailures()
 94	s.settle(now)
 95}
 96
 97func (s *schedule) clearFailures() {
 98	s.retry = time.Time{}
 99	s.backoff = 0
100	s.movedOnce = false
101	s.refusing = false
102}
103
104func (s *schedule) next(now time.Time) action {
105	switch {
106	case now.Before(s.retry):
107		return actWait
108	case s.refusing:
109		return actRecheck
110	case !s.isPlanned:
111		return actPlan
112	case s.stepNow:
113		return actStep
114	case s.seenSet && s.pending() && (now.Sub(s.quietSince) >= quietPeriod || now.Sub(s.dirtySince) >= pendingCap):
115		return actStep
116	case s.known && !now.Before(s.dailyDue):
117		return actStep
118	}
119	return actWait
120}
121
122func (s *schedule) succeeded(now time.Time, current, committed [32]byte, commitTime time.Time) {
123	s.clearFailures()
124	s.stepNow = false
125	s.known = true
126	s.committed = committed
127	s.dailyDue = commitTime.Add(dailyInterval)
128	if !s.seenSet || current != s.seen {
129		s.seen, s.seenSet, s.quietSince = current, true, now
130	}
131	s.settle(now)
132}
133
134func (s *schedule) failed(now time.Time, err error) {
135	if errors.Is(err, replica.ErrRemoteMoved) && !s.movedOnce {
136		s.movedOnce = true
137		s.retry = now
138		return
139	}
140	if !errors.Is(err, replica.ErrRemoteMoved) {
141		s.movedOnce = false
142	}
143	s.backoff = min(max(2*s.backoff, retryFirst), retryMax)
144	s.retry = now.Add(time.Duration(float64(s.backoff) * (1 + retryJitter*s.jitter())))
145}
146
147func (s *schedule) refused(now time.Time, r refusal) bool {
148	s.backoff = 0
149	s.movedOnce = false
150	s.refusing = true
151	s.refusalAt = r
152	s.retry = now.Add(recheckInterval)
153	if s.audited[r] {
154		return false
155	}
156	s.audited[r] = true
157	return true
158}
159
160func (s *schedule) unchanged(now time.Time) {
161	s.backoff = 0
162	s.retry = now.Add(recheckInterval)
163}
164
165func (s *schedule) currentRefusal() (refusal, bool) {
166	return s.refusalAt, s.refusing
167}
168
169func (s *schedule) retryAt() time.Time {
170	return s.retry
171}
172
173func (s *schedule) pruneDue(now time.Time, res replica.Result) bool {
174	if res.Outcome != replica.Pushed || res.MirrorErr != nil {
175		return false
176	}
177	return s.lastPrune.IsZero() || now.Sub(s.lastPrune) >= pruneInterval
178}
179
180func (s *schedule) pruned(now time.Time) {
181	s.lastPrune = now
182}