carlos / platform Public

Clone
git clone https://amadan.net/carlos/platform

Plain git — no account needed to clone.

Download

Download this file

1package release
2
3import (
4 "context"
5 "errors"
6 "fmt"
7 "os"
8 "sort"
9 "sync"
10 "time"
11)
12
13// ChangeIndexBackstop is the longest a gated reader goes without a successful
14// full read of a scope, however quiet the change index is (R3). It lives here
15// so provision, host, edge and console share one value without import cycles.
16// Callers jitter it by +-10%.
17const ChangeIndexBackstop = 30 * time.Minute
18
19// ChangeIndexPollTimeout bounds one Poll from a caller with a deadline of its
20// own to keep (the agent's fast tick, the edge's health-stamping loops): the
21// wait for another caller's in-flight poll and the conditional GET together.
22// A poll that runs out is "unknown" to its caller, never "unchanged".
23const ChangeIndexPollTimeout = 5 * time.Second
24
25// ChangeWatch follows the change index (ChangeIndexKey) with one conditional
26// GET per Poll: an unchanged index costs a 304 and no body. It is safe for
27// concurrent use.
28//
29// The watch is unusable until its first good poll, and again after any
30// failed one: an access error, a network error, an unknown schema, a
31// malformed body, a store without ConditionalGetter, or a caller's context
32// ending, while waiting for another caller's poll included. "Unknown" is never
33// read as "empty and unchanged"; callers walk as they did before the index
34// existed. The one exception is an absent index, which is the ordinary state
35// before the first bump and reads as usable and empty.
36type ChangeWatch struct {
37 store Store
38
39 // pollSem serializes polls: a one-slot semaphore held across the
40 // network call. A channel rather than a mutex so a waiter behind a hung
41 // poll gives up when its context ends instead of inheriting the hang.
42 pollSem chan struct{}
43
44 mu sync.Mutex // guards the fields below; never held across I/O
45 usable bool
46 etag string // etag of the last good index body ("" when it was absent)
47 entries map[string]uint64 // the last good observation
48}
49
50// NewChangeWatch returns a watch over s. It has not polled yet, so it is
51// unusable.
52func NewChangeWatch(s Store) *ChangeWatch {
53 return &ChangeWatch{store: s, entries: map[string]uint64{}, pollSem: make(chan struct{}, 1)}
54}
55
56// Poll reads the index conditionally and returns, sorted, the scopes whose
57// generation differs from the last good observation: every scope on the
58// first good poll, then those that rose or appeared. On a 304 it returns
59// nil, nil. Any error marks the watch unusable until the next good poll and
60// is returned; that next poll reports every scope that differs from the last
61// good observation, including ones that rose while the watch was unusable.
62//
63// moved is advisory. Several consumers in one process share a watch (a box
64// agent's tick, the record view, the cert mirror), so whichever polls first
65// consumes a moved list and the others see nil. A consumer that must act on
66// movement compares Observed against the map it last acted on instead of
67// relying on Poll's return.
68//
69// A Poll that waits behind another caller's in-flight poll stops waiting when
70// ctx ends and returns ctx's error. That give-up is a failed poll like any
71// other: the watch is unusable until the next good poll, so no gated reader
72// trusts an observation a stalled index GET has frozen. The ETag and entries
73// are kept, so the next good poll of an unchanged index is a 304 again.
74func (w *ChangeWatch) Poll(ctx context.Context) (moved []string, err error) {
75 if err := ctx.Err(); err != nil {
76 return nil, w.fail(err)
77 }
78 select {
79 case w.pollSem <- struct{}{}:
80 case <-ctx.Done():
81 return nil, w.fail(ctx.Err())
82 }
83 defer func() { <-w.pollSem }()
84
85 g, ok := w.store.(ConditionalGetter)
86 if !ok {
87 return nil, w.fail(fmt.Errorf("release: change index unreadable: store has no conditional read: %w", errors.ErrUnsupported))
88 }
89 w.mu.Lock()
90 etag := w.etag
91 w.mu.Unlock()
92
93 body, newETag, changed, err := g.GetIfChanged(ctx, ChangeIndexKey, etag)
94 if errors.Is(err, os.ErrNotExist) {
95 return w.adopt(ChangeIndex{Schema: 1, Entries: map[string]uint64{}}, ""), nil
96 }
97 if err != nil {
98 return nil, w.fail(err)
99 }
100 if !changed {
101 w.mu.Lock()
102 w.usable = true // same bytes as the last good body, which are the last good observation
103 w.mu.Unlock()
104 return nil, nil
105 }
106 idx, _, err := readChangeIndex(ctx, func(context.Context, string) ([]byte, string, error) {
107 return body, newETag, nil
108 })
109 if err != nil {
110 return nil, w.fail(err)
111 }
112 return w.adopt(idx, newETag), nil
113}
114
115func (w *ChangeWatch) fail(err error) error {
116 w.mu.Lock()
117 w.usable = false
118 w.mu.Unlock()
119 return err
120}
121
122// adopt records idx as the last good observation and returns the scopes it
123// moved.
124func (w *ChangeWatch) adopt(idx ChangeIndex, etag string) []string {
125 w.mu.Lock()
126 defer w.mu.Unlock()
127 var moved []string
128 for scope, gen := range idx.Entries {
129 if old, ok := w.entries[scope]; !ok || old != gen {
130 moved = append(moved, scope)
131 }
132 }
133 sort.Strings(moved)
134 w.entries = idx.Entries
135 w.etag = etag
136 w.usable = true
137 return moved
138}
139
140// Usable reports whether the last poll succeeded.
141func (w *ChangeWatch) Usable() bool {
142 w.mu.Lock()
143 defer w.mu.Unlock()
144 return w.usable
145}
146
147// Gen is the last observed generation of scope. A scope the index does not
148// list has generation 0 with ok true; ok is false while the watch is
149// unusable.
150func (w *ChangeWatch) Gen(scope string) (gen uint64, ok bool) {
151 w.mu.Lock()
152 defer w.mu.Unlock()
153 if !w.usable {
154 return 0, false
155 }
156 return w.entries[scope], true
157}
158
159// Observed returns a copy of the last good entries, and false while the watch
160// is unusable. Poll's moved list is advisory when consumers share a watch, so
161// a consumer that must act on movement diffs Observed against its own
162// last-acted map.
163func (w *ChangeWatch) Observed() (map[string]uint64, bool) {
164 w.mu.Lock()
165 defer w.mu.Unlock()
166 if !w.usable {
167 return nil, false
168 }
169 out := make(map[string]uint64, len(w.entries))
170 for k, v := range w.entries {
171 out[k] = v
172 }
173 return out, true
174}
175
176// ReadGate decides, per scope, whether a full read is due: when the watch
177// cannot vouch for the scope, the scope's generation has moved since the last
178// successful read, or the last successful read is maxAge old. It is safe for
179// concurrent use.
180type ReadGate struct {
181 w *ChangeWatch
182 maxAge time.Duration
183
184 mu sync.Mutex
185 marks map[string]gateMark
186}
187
188type gateMark struct {
189 gen uint64
190 at time.Time
191 ok bool
192}
193
194// NewReadGate gates reads on w. A nil w makes every read due. maxAge is the
195// longest a successful read stands in for the next one (use
196// ChangeIndexBackstop, jittered).
197func NewReadGate(w *ChangeWatch, maxAge time.Duration) *ReadGate {
198 return &ReadGate{w: w, maxAge: maxAge, marks: map[string]gateMark{}}
199}
200
201// MaxAge is the longest a successful read stands in for the next one. A
202// caller keeping finer marks of its own under one scope (the agent's canary
203// groups, one per channel) ages them by it too.
204func (g *ReadGate) MaxAge() time.Duration { return g.maxAge }
205
206// Due reports whether scope must be read now and returns the generation to
207// pass to Mark once the read is done. Capture it BEFORE the read: a write that
208// lands during the read then leaves the scope due on the next check instead
209// of being hidden by it. The generation is 0 when the watch is nil or
210// unusable.
211func (g *ReadGate) Due(scope string, now time.Time) (gen uint64, due bool) {
212 if g == nil || g.w == nil {
213 return 0, true
214 }
215 gen, ok := g.w.Gen(scope)
216 if !ok {
217 return 0, true
218 }
219 g.mu.Lock()
220 m, marked := g.marks[scope]
221 g.mu.Unlock()
222 switch {
223 case !marked, !m.ok, m.gen != gen, now.Sub(m.at) >= g.maxAge:
224 return gen, true
225 }
226 return gen, false
227}
228
229// Mark records the outcome of a read of scope that began when Due returned
230// gen. A failed read (ok false) leaves the scope due.
231func (g *ReadGate) Mark(scope string, gen uint64, now time.Time, ok bool) {
232 if g == nil {
233 return
234 }
235 g.mu.Lock()
236 g.marks[scope] = gateMark{gen: gen, at: now, ok: ok}
237 g.mu.Unlock()
238}
239