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}