oficina / calendar Public

Clone
git clone https://amadan.net/oficina/calendar

Plain git — no account needed to clone.

Download

Download this file

1package calendar
2
3import (
4 "context"
5 "errors"
6 "net/http"
7 "sync"
8 "time"
9
10 "amadan.net/rastrillo/rastrillo/carlos"
11)
12
13const scheduledWorkName = "calendar-next-due"
14
15type scheduledWork struct {
16 mu sync.Mutex
17 lifeMu sync.Mutex
18 started, closed bool
19 finishOnce sync.Once
20 ctx context.Context
21 cancel context.CancelFunc
22 done chan struct{}
23 wake chan struct{}
24 query func(context.Context) (time.Time, error)
25 put func(context.Context, string, time.Time, string) error
26 remove func(context.Context, string) error
27 capability func(context.Context) error
28 armed func(context.Context) (remoteSchedule, error)
29 now func() time.Time
30 report func(error)
31 development bool
32 known bool
33 registered time.Time
34 registeredAt time.Time
35 rawDue time.Time
36 observed time.Time
37 force bool
38 allowRestore bool
39 failures int
40 notBefore time.Time
41 checked time.Time
42 retryDelay time.Duration
43 reconcileEvery time.Duration
44}
45
46func newScheduledWork(query func(context.Context) (time.Time, error), development bool, report func(error)) *scheduledWork {
47 ctx, cancel := context.WithCancel(context.Background())
48 return &scheduledWork{ctx: ctx, cancel: cancel, done: make(chan struct{}), wake: make(chan struct{}, 1), query: query,
49 put: carlos.ScheduleAt, remove: carlos.ScheduleCancel, capability: scheduleCapability, armed: scheduleArmed, now: time.Now,
50 report: report, development: development, retryDelay: time.Second, reconcileEvery: time.Minute, allowRestore: true}
51}
52
53func (s *scheduledWork) start() error {
54 s.lifeMu.Lock()
55 defer s.lifeMu.Unlock()
56 if s.closed {
57 return context.Canceled
58 }
59 if s.started {
60 return nil
61 }
62 s.started = true
63 if _, err := s.sync(true); err != nil && permanentScheduleError(err) {
64 s.cancel()
65 s.finishOnce.Do(func() { close(s.done) })
66 return err
67 }
68 go s.retry()
69 return nil
70}
71
72func (s *scheduledWork) close() {
73 s.cancel()
74 s.lifeMu.Lock()
75 s.closed = true
76 if !s.started {
77 s.finishOnce.Do(func() { close(s.done) })
78 }
79 s.lifeMu.Unlock()
80 <-s.done
81 s.mu.Lock()
82 s.mu.Unlock()
83}
84
85func (s *scheduledWork) request() {
86 select {
87 case s.wake <- struct{}{}:
88 default:
89 }
90}
91
92func (s *scheduledWork) sync(force bool) (time.Time, error) {
93 due, err := s.synchronize(force, false)
94 if err != nil {
95 s.request()
96 }
97 return due, err
98}
99
100func (s *scheduledWork) finishDrain(start time.Time, progress int, drainErr error) error {
101 s.mu.Lock()
102 s.allowRestore = false
103 ctx, cancel := context.WithTimeout(s.ctx, 2*time.Second)
104 due, queryErr := s.query(ctx)
105 cancel()
106 if queryErr != nil {
107 due = s.rawDue
108 }
109 failed := drainErr != nil || queryErr != nil || (progress == 0 && !due.IsZero() && !due.After(start))
110 if !failed {
111 s.failures = 0
112 s.notBefore = time.Time{}
113 } else {
114 s.failures = min(s.failures+1, 5)
115 delay := min(time.Minute<<(s.failures-1), 15*time.Minute)
116 if !due.IsZero() {
117 delay = max(delay, min(max(s.now().Sub(due)/4, time.Minute), 15*time.Minute))
118 }
119 s.notBefore = s.now().Add(delay)
120 }
121 s.mu.Unlock()
122 _, err := s.sync(true)
123 if queryErr != nil {
124 return queryErr
125 }
126 return err
127}
128
129func (s *scheduledWork) synchronize(force, inspect bool) (due time.Time, err error) {
130 s.mu.Lock()
131 defer s.mu.Unlock()
132 ctx, cancel := context.WithTimeout(s.ctx, 4*time.Second)
133 defer cancel()
134 return s.synchronizeLocked(ctx, force, inspect)
135}
136
137func (s *scheduledWork) synchronizeLocked(ctx context.Context, force, inspect bool) (due time.Time, err error) {
138 if err := ctx.Err(); err != nil {
139 return s.observed, err
140 }
141 s.force = s.force || force
142 attempted := false
143 defer func() {
144 if err != nil {
145 s.force = s.force || attempted
146 if s.report != nil {
147 s.report(err)
148 }
149 }
150 }()
151 queryCtx, cancel := context.WithTimeout(ctx, 2*time.Second)
152 due, err = s.query(queryCtx)
153 cancel()
154 if err != nil {
155 return s.observed, err
156 }
157 s.rawDue = due
158 if !s.known && s.allowRestore {
159 if restored, restoreErr := s.restore(ctx, due); restoreErr != nil {
160 return due, restoreErr
161 } else if restored {
162 return s.observed, nil
163 }
164 }
165 if !s.notBefore.IsZero() && (due.IsZero() || due.Before(s.notBefore)) {
166 due = s.notBefore
167 }
168 s.observed = due
169 if s.known && !s.force && due.Equal(s.registered) {
170 if !inspect && s.now().Sub(s.checked) < s.reconcileEvery {
171 return due, nil
172 }
173 controlCtx, cancel := context.WithTimeout(ctx, 2*time.Second)
174 var armed remoteSchedule
175 armed, err = s.armed(controlCtx)
176 cancel()
177 if errors.Is(err, carlos.ErrNotOnCarlos) && s.development {
178 err = nil
179 armed = remoteSchedule{NextAt: s.registeredAt.Unix(), DueAt: s.registeredAt.Unix(), Path: "/jobs/reminders", Kind: "once"}
180 }
181 if err != nil {
182 return due, err
183 }
184 s.checked = s.now()
185 if due.IsZero() || s.matches(armed) {
186 return due, nil
187 }
188 s.force = true
189 }
190 capabilityCtx, cancel := context.WithTimeout(ctx, 2*time.Second)
191 err = s.capability(capabilityCtx)
192 cancel()
193 if errors.Is(err, carlos.ErrNotOnCarlos) && s.development {
194 err = nil
195 } else if err != nil {
196 return due, err
197 }
198 controlCtx, cancel := context.WithTimeout(ctx, 2*time.Second)
199 defer cancel()
200 attempted = true
201 if due.IsZero() {
202 err = s.remove(controlCtx, scheduledWorkName)
203 } else {
204 at := due
205 now := s.now()
206 if at.Before(now) || at.Unix() <= 0 {
207 at = now
208 }
209 if latest := now.Add(carlos.MaxAhead - time.Minute); at.After(latest) {
210 at = latest
211 }
212 err = s.put(controlCtx, scheduledWorkName, at, "/jobs/reminders")
213 if err == nil {
214 s.registeredAt = time.Unix(at.Unix(), 0)
215 }
216 }
217 if errors.Is(err, carlos.ErrNotOnCarlos) && s.development {
218 err = nil
219 }
220 if err == nil {
221 s.known, s.force = true, false
222 s.registered = due
223 s.checked = s.now()
224 }
225 return due, err
226}
227
228func (s *scheduledWork) retry() {
229 defer s.finishOnce.Do(func() { close(s.done) })
230 timer := time.NewTimer(s.reconcileEvery)
231 defer timer.Stop()
232 retrying := false
233 delay := s.retryDelay
234 for {
235 inspect := false
236 select {
237 case <-s.ctx.Done():
238 return
239 case <-s.wake:
240 if retrying {
241 continue
242 }
243 case <-timer.C:
244 inspect = !retrying
245 }
246 _, err := s.synchronize(false, inspect)
247 if err != nil {
248 retrying = true
249 timer.Reset(delay)
250 delay = min(delay*2, 30*time.Second)
251 } else {
252 retrying = false
253 delay = s.retryDelay
254 timer.Reset(s.reconcileEvery)
255 }
256 }
257}
258
259func (s *scheduledWork) middleware(next http.Handler) http.Handler {
260 return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
261 defer s.request()
262 next.ServeHTTP(w, r)
263 })
264}
265