carlos / platform Public

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

Plain git — no account needed to clone.

Download

Download this file

1package host
2
3import (
4 "context"
5 "errors"
6 "sort"
7 "sync"
8
9 "github.com/carlosframework/platform/internal/hostbox"
10 "github.com/carlosframework/platform/internal/hostguard"
11)
12
13type claimProducer struct {
14 cancel context.CancelFunc
15 joined chan struct{}
16 err error
17}
18type completeClaimProducer func(context.Context, hostguard.FenceTarget, string, hostbox.ProducerCompletion) (hostbox.ProducerCompletion, error)
19
20type ClaimProducers struct {
21 closeWriters func(context.Context, hostguard.FenceTarget) error
22 completed hostbox.ProducerCompletion
23 complete completeClaimProducer
24 mu sync.Mutex
25 target hostguard.FenceTarget
26 processStart string
27 fence string
28 drained bool
29 // writersStopped closes admission to every producer but the retained
30 // ones (StopWriters); the full drain still cancels those.
31 writersStopped bool
32 workers map[string]*claimProducer
33 Record func(context.Context, hostguard.FenceTarget, string) error
34}
35
36func NewClaimProducers(target hostguard.FenceTarget, start string, record func(context.Context, hostguard.FenceTarget, string) error, complete ...completeClaimProducer) (*ClaimProducers, error) {
37 if target.Host == "" || target.ControllerID == "" || start == "" || record == nil || len(complete) > 1 || (len(complete) == 1 && complete[0] == nil) {
38 return nil, ErrClaimStopUnavailable
39 }
40 p := &ClaimProducers{target: target, processStart: start, Record: record, workers: map[string]*claimProducer{}}
41 if len(complete) == 1 {
42 p.complete = complete[0]
43 }
44 return p, nil
45}
46func (p *ClaimProducers) Launch(ctx context.Context, target hostguard.FenceTarget, id string, fn func(context.Context) error) error {
47 if p == nil || fn == nil || !stopComponent(id) {
48 return ErrClaimStopUnavailable
49 }
50 p.mu.Lock()
51 defer p.mu.Unlock()
52 if p.fence != "" || !hostbox.SameLaunchOwner(p.target, target) {
53 return ErrClaimStopIdentity
54 }
55 if p.writersStopped && !claimRetainedProducer(id) {
56 return ErrClaimWritersStopped
57 }
58 if err := p.compact(ctx); err != nil {
59 return err
60 }
61 if _, ok := p.workers[id]; ok {
62 return ErrClaimStopIdentity
63 }
64 if err := p.Record(ctx, target, id); err != nil {
65 return err
66 }
67 child, cancel := context.WithCancel(ctx)
68 w := &claimProducer{cancel: cancel, joined: make(chan struct{})}
69 p.workers[id] = w
70 go func() { p.finish(w, fn(child)) }()
71 return nil
72}
73func (p *ClaimProducers) DrainClaim(ctx context.Context, r StopRequest, fence string) (DrainAcknowledgement, error) {
74 if p == nil || fence == "" {
75 return DrainAcknowledgement{}, ErrClaimStopUnavailable
76 }
77 p.mu.Lock()
78 if p.target.Host != r.Host || p.target.Incarnation != r.Incarnation || p.target.Stamp != r.InstallationStamp || p.target.Session != r.WriterSession || p.target.Box != r.Box || p.target.Enrollment != r.Enrollment || p.target.BootID != r.BootID || p.target.ControllerID != r.ControllerID || (p.fence != "" && p.fence != fence && !p.drained) {
79 p.mu.Unlock()
80 return DrainAcknowledgement{}, ErrClaimStopIdentity
81 }
82 p.fence, p.drained = fence, false
83 if p.closeWriters != nil {
84 if err := p.closeWriters(ctx, p.target); err != nil {
85 p.mu.Unlock()
86 return DrainAcknowledgement{}, err
87 }
88 }
89 workers := map[string]*claimProducer{}
90 for id, w := range p.workers {
91 workers[id] = w
92 w.cancel()
93 }
94 p.mu.Unlock()
95 for _, w := range workers {
96 select {
97 case <-ctx.Done():
98 return DrainAcknowledgement{}, ctx.Err()
99 case <-w.joined:
100 }
101 if w.err != nil && !errors.Is(w.err, context.Canceled) {
102 return DrainAcknowledgement{}, w.err
103 }
104 }
105 p.mu.Lock()
106 defer p.mu.Unlock()
107 if err := p.compact(ctx); err != nil {
108 return DrainAcknowledgement{}, err
109 }
110 ids := make([]string, 0, len(p.workers))
111 for id := range p.workers {
112 ids = append(ids, id)
113 }
114 p.drained = true
115 return DrainClaimAcknowledgement(fence, r.ControllerID, p.processStart, ids, p.completed), nil
116}
117
118func (p *ClaimProducers) Register(ctx context.Context, target hostguard.FenceTarget, id string) (context.Context, func(error), error) {
119 if p == nil || !stopComponent(id) {
120 return nil, nil, ErrClaimStopUnavailable
121 }
122 p.mu.Lock()
123 defer p.mu.Unlock()
124 if p.fence != "" || !hostbox.SameLaunchOwner(p.target, target) {
125 return nil, nil, ErrClaimStopIdentity
126 }
127 if p.writersStopped && !claimRetainedProducer(id) {
128 return nil, nil, ErrClaimWritersStopped
129 }
130 if err := p.compact(ctx); err != nil {
131 return nil, nil, err
132 }
133 if _, ok := p.workers[id]; ok {
134 return nil, nil, ErrClaimStopIdentity
135 }
136 if err := p.Record(ctx, target, id); err != nil {
137 return nil, nil, err
138 }
139 child, cancel := context.WithCancel(ctx)
140 w := &claimProducer{cancel: cancel, joined: make(chan struct{})}
141 p.workers[id] = w
142 var once sync.Once
143 finish := func(err error) { once.Do(func() { p.finish(w, err) }) }
144 return child, finish, nil
145}
146
147// StopWriters is the writers-only drain (claim Plan 11, R2): it closes
148// admission to every producer but the retained ones, cancels the rest (the
149// role launch's producers and the schedule runner's timer deliveries) and
150// joins them. The replicator, the generation publisher and the files
151// publisher keep running; DrainClaim, root's full drain, still ends them.
152// It returns the ids it joined, sorted. Repeating it is safe.
153func (p *ClaimProducers) StopWriters(ctx context.Context, target hostguard.FenceTarget) ([]string, error) {
154 if p == nil || ctx == nil {
155 return nil, ErrClaimStopUnavailable
156 }
157 p.mu.Lock()
158 if p.fence != "" || !hostbox.SameLaunchOwner(p.target, target) {
159 p.mu.Unlock()
160 return nil, ErrClaimStopIdentity
161 }
162 p.writersStopped = true
163 if p.closeWriters != nil {
164 if err := p.closeWriters(ctx, p.target); err != nil {
165 p.mu.Unlock()
166 return nil, err
167 }
168 }
169 workers := map[string]*claimProducer{}
170 for id, w := range p.workers {
171 if !claimRetainedProducer(id) {
172 workers[id] = w
173 w.cancel()
174 }
175 }
176 p.mu.Unlock()
177 ids := make([]string, 0, len(workers))
178 for id, w := range workers {
179 select {
180 case <-ctx.Done():
181 return nil, ctx.Err()
182 case <-w.joined:
183 }
184 if w.err != nil && !errors.Is(w.err, context.Canceled) {
185 return nil, w.err
186 }
187 ids = append(ids, id)
188 }
189 p.mu.Lock()
190 defer p.mu.Unlock()
191 if err := p.compact(ctx); err != nil {
192 return nil, err
193 }
194 sort.Strings(ids)
195 return ids, nil
196}
197
198// Running reports the producers that have not finished, sorted.
199func (p *ClaimProducers) Running() []string {
200 p.mu.Lock()
201 defer p.mu.Unlock()
202 ids := make([]string, 0, len(p.workers))
203 for id, w := range p.workers {
204 select {
205 case <-w.joined:
206 default:
207 ids = append(ids, id)
208 }
209 }
210 sort.Strings(ids)
211 return ids
212}
213
214type ClaimProducerSet struct {
215 CloseWriters func(context.Context, hostguard.FenceTarget) error
216 mu sync.Mutex
217 items []*ClaimProducers
218}
219
220func (s *ClaimProducerSet) Add(p *ClaimProducers) error {
221 if p == nil {
222 return ErrClaimStopUnavailable
223 }
224 s.mu.Lock()
225 defer s.mu.Unlock()
226 for _, old := range s.items {
227 if hostbox.SameLaunchOwner(old.target, p.target) {
228 return ErrClaimStopIdentity
229 }
230 }
231 s.items = append(s.items, p)
232 return nil
233}
234func (s *ClaimProducerSet) For(t hostguard.FenceTarget) (*ClaimProducers, error) {
235 s.mu.Lock()
236 defer s.mu.Unlock()
237 for _, p := range s.items {
238 if hostbox.SameLaunchOwner(p.target, t) {
239 return p, nil
240 }
241 }
242 return nil, ErrClaimStopUnavailable
243}
244func (s *ClaimProducerSet) DrainClaim(ctx context.Context, r StopRequest, fence string) (DrainAcknowledgement, error) {
245 t := hostguard.FenceTarget{Host: r.Host, Incarnation: r.Incarnation, Stamp: r.InstallationStamp, Session: r.WriterSession, Box: r.Box, Enrollment: r.Enrollment, BootID: r.BootID, ControllerID: r.ControllerID}
246 p, err := s.For(t)
247 if err != nil {
248 return DrainAcknowledgement{}, err
249 }
250 return p.DrainClaim(ctx, r, fence)
251}
252
253func (s *ClaimProducerSet) Ensure(target hostguard.FenceTarget, start string, record func(context.Context, hostguard.FenceTarget, string) error, complete ...completeClaimProducer) (*ClaimProducers, error) {
254 s.mu.Lock()
255 defer s.mu.Unlock()
256 for _, old := range s.items {
257 if hostbox.SameLaunchOwner(old.target, target) {
258 if old.processStart != start {
259 return nil, ErrClaimStopIdentity
260 }
261 return old, nil
262 }
263 }
264 p, err := NewClaimProducers(target, start, record, complete...)
265 if err != nil {
266 return nil, err
267 }
268 p.closeWriters = s.CloseWriters
269 s.items = append(s.items, p)
270 return p, nil
271}
272
273func (p *ClaimProducers) finish(w *claimProducer, err error) {
274 p.mu.Lock()
275 defer p.mu.Unlock()
276 w.err = err
277 w.cancel()
278 close(w.joined)
279 ctx, cancel := context.WithTimeout(context.Background(), hostbox.RequestLimit)
280 defer cancel()
281 // A failed durable write retains the joined worker. Admission and drain
282 // retry it before accepting more work or acknowledging completion.
283 _ = p.compact(ctx)
284}
285
286// compact runs under mu so the journal and the memory summary advance together.
287func (p *ClaimProducers) compact(ctx context.Context) error {
288 if p.complete == nil {
289 return nil
290 }
291 for id, w := range p.workers {
292 select {
293 case <-w.joined:
294 default:
295 continue
296 }
297 if w.err != nil && !errors.Is(w.err, context.Canceled) {
298 return w.err
299 }
300 next, err := p.complete(ctx, p.target, id, p.completed)
301 if err != nil {
302 return err
303 }
304 if !next.Valid() || next.Count != p.completed.Count+1 {
305 return ErrClaimStopIdentity
306 }
307 p.completed = next
308 delete(p.workers, id)
309 }
310 return nil
311}
312