carlos / platform Public

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

Plain git — no account needed to clone.

Download

Download this file

1package activator
2
3import (
4 "context"
5 "errors"
6 "os/exec"
7 "time"
8
9 "github.com/carlosframework/platform/internal/hostbox"
10 "github.com/carlosframework/platform/internal/hostclaim"
11 "github.com/carlosframework/platform/internal/hostwrite"
12 "github.com/carlosframework/platform/internal/registry"
13 "github.com/carlosframework/platform/internal/release"
14)
15
16type ServingRetries struct {
17 BeforeReserve func(context.Context, registry.Route, hostclaim.Observed) error
18 Claims release.Store
19 Box, Enrollment, BootID, ControllerID string
20 Now func() time.Time
21 NewID func() string
22 Inputs func(context.Context, registry.Route) (string, error)
23 StopAttempt func(context.Context, hostclaim.Observed, hostclaim.WakeRetryAttempt) error
24 ExactHealth func(context.Context, *exec.Cmd, hostclaim.WakeRetryAttempt) error
25}
26
27func (d ServingRetries) valid() bool {
28 return d.Claims != nil && d.Box != "" && d.Enrollment != "" && d.BootID != "" && d.ControllerID != "" && d.Now != nil && d.NewID != nil && d.Inputs != nil && d.StopAttempt != nil && d.ExactHealth != nil
29}
30
31func (d ServingRetries) attempt(inputs string, session uint64) hostclaim.WakeRetryAttempt {
32 return hostclaim.WakeRetryAttempt{ID: d.NewID(), Box: d.Box, Enrollment: d.Enrollment, BootID: d.BootID, ControllerID: d.ControllerID, Session: session, InvocationID: d.NewID(), LaunchInputs: inputs}
33}
34
35func retryOf(o hostclaim.Observed) *hostclaim.WakeRetry {
36 if o.Claim == nil || o.Claim.Holder == nil {
37 return nil
38 }
39 return o.Claim.Holder.WakeRetry
40}
41
42func (d ServingRetries) settle(ctx context.Context, expected hostclaim.Observed, writes ...func(hostclaim.Observed, hostclaim.Observed) error) error {
43 r := retryOf(expected)
44 if r == nil || r.State != "attempting" || r.Attempt == nil {
45 return nil
46 }
47 o, err := hostclaim.Read(ctx, d.Claims, expected.Host)
48 if err != nil {
49 return err
50 }
51 current := retryOf(o)
52 if o.Claim == nil || o.Claim.Holder == nil || o.Claim.Holder.Incarnation != expected.Claim.Holder.Incarnation || current == nil || current.EpisodeID != r.EpisodeID || current.Attempt == nil || *current.Attempt != *r.Attempt {
53 return hostbox.ErrChanged
54 }
55 if err := d.StopAttempt(ctx, o, *r.Attempt); err != nil {
56 return err
57 }
58 // A stop can change bookkeeping. Never use its earlier ETag for settlement.
59 o, err = hostclaim.Read(ctx, d.Claims, expected.Host)
60 if err != nil {
61 return err
62 }
63 written, err := hostclaim.Update(ctx, d.Claims, o, hostclaim.FailWakeRetryAttempt(hostclaim.WakeRetryResult{HolderIncarnation: expected.Claim.Holder.Incarnation, EpisodeID: r.EpisodeID, Attempt: *r.Attempt, Now: d.Now()}))
64 if err == nil {
65 for _, write := range writes {
66 if write != nil {
67 if err := write(o, written); err != nil {
68 return err
69 }
70 }
71 }
72 }
73 return err
74}
75
76func (d ServingRetries) Wrap(base *ClaimRuntime) (*ClaimRuntime, error) {
77 if !d.valid() || base == nil || base.Admit == nil || base.Prepare == nil || base.Prerequisites == nil || base.FailedAdmission == nil {
78 return nil, hostwrite.ErrUnavailable
79 }
80 out := *base
81 out.Prerequisites = func(ctx context.Context, rt registry.Route, o hostclaim.Observed, p StartPolicy) error {
82 if o.Claim != nil && o.Claim.Op.Kind == hostclaim.KindServing && !p.Probe {
83 if r := retryOf(o); r != nil {
84 if r.State == "attempting" {
85 if err := d.settle(ctx, o); err != nil {
86 return err
87 }
88 return hostbox.ErrChanged
89 }
90 if r.State != "open" || d.Now().Before(r.NotBefore) {
91 return hostclaim.ErrWrongState
92 }
93 }
94 }
95 if o.Claim != nil && o.Claim.Op.Kind == hostclaim.KindServing && !p.Probe && d.BeforeReserve != nil {
96 if err := d.BeforeReserve(ctx, rt, o); err != nil {
97 return err
98 }
99 }
100 return base.Prerequisites(ctx, rt, o, p)
101 }
102 out.Admit = func(ctx context.Context, rt registry.Route, o hostclaim.Observed, p StartPolicy) (result hostclaim.Observed, resultErr error) {
103 if o.Claim == nil || o.Claim.Op.Kind != hostclaim.KindServing || p.Probe {
104 return base.Admit(ctx, rt, o, p)
105 }
106 cleanupObservation := o
107 defer func() {
108 if resultErr != nil {
109 recovery, cancel := CleanupContext(ctx, 10*time.Second)
110 defer cancel()
111 resultErr = errors.Join(resultErr, base.FailedAdmission(recovery, cleanupObservation, resultErr))
112 }
113 }()
114 if d.BeforeReserve != nil {
115 if err := d.BeforeReserve(ctx, rt, o); err != nil {
116 return hostclaim.Observed{}, err
117 }
118 }
119 inputs, err := d.Inputs(ctx, rt)
120 if err != nil || inputs == "" {
121 return hostclaim.Observed{}, errors.Join(err, hostwrite.ErrUnavailable)
122 }
123 episode := d.NewID()
124 if r := retryOf(o); r != nil {
125 episode = r.EpisodeID
126 }
127 attempt := d.attempt(inputs, 0)
128 if d.BeforeReserve != nil {
129 if err := d.BeforeReserve(ctx, rt, o); err != nil {
130 return hostclaim.Observed{}, err
131 }
132 }
133 reserved, err := hostclaim.Update(ctx, d.Claims, o, hostclaim.ReserveWakeRetryAttempt(hostclaim.WakeRetryReservation{HolderIncarnation: o.Claim.Holder.Incarnation, EpisodeID: episode, Attempt: attempt, Now: d.Now()}))
134 if err != nil {
135 return hostclaim.Observed{}, err
136 }
137 cleanupObservation = reserved
138 admitted, err := base.Admit(ctx, rt, reserved, p)
139 if err == nil && admitted.Claim != nil && admitted.Claim.Holder != nil {
140 admitted, err = hostclaim.Update(ctx, d.Claims, admitted, hostclaim.BindWakeRetrySession(o.Claim.Holder.Incarnation, episode, attempt.ID, admitted.Claim.Holder.Session))
141 } else if err == nil {
142 err = hostbox.ErrChanged
143 }
144 if err != nil {
145 recovery, cancel := CleanupContext(ctx, 10*time.Second)
146 defer cancel()
147 // An ambiguous admission or bind is settled using the stored attempt, never refunded.
148 fresh, readErr := hostclaim.Read(recovery, d.Claims, o.Host)
149 if readErr == nil && fresh.Claim != nil && fresh.Claim.Holder != nil && fresh.Claim.Holder.Incarnation == o.Claim.Holder.Incarnation {
150 r := retryOf(fresh)
151 if r != nil && r.EpisodeID == episode && r.Attempt != nil && r.Attempt.ID == attempt.ID {
152 readErr = d.settle(recovery, fresh)
153 } else {
154 readErr = hostbox.ErrChanged
155 }
156 }
157 return hostclaim.Observed{}, errors.Join(err, readErr)
158 }
159 return admitted, nil
160 }
161 out.FailedAdmission = func(ctx context.Context, o hostclaim.Observed, cause error) error {
162 err := base.FailedAdmission(ctx, o, cause)
163 fresh, readErr := hostclaim.Read(ctx, d.Claims, o.Host)
164 if readErr != nil {
165 return errors.Join(err, readErr)
166 }
167 if o.Claim == nil || o.Claim.Holder == nil || fresh.Claim == nil || fresh.Claim.Holder == nil || o.Claim.Holder.Incarnation != fresh.Claim.Holder.Incarnation {
168 return errors.Join(err, hostbox.ErrChanged)
169 }
170 expected, current := retryOf(o), retryOf(fresh)
171 if expected == nil || current == nil || expected.EpisodeID != current.EpisodeID {
172 return err
173 }
174 if current.Attempt != nil && (!sameAdmission(fresh, o) || current.Attempt.Box != d.Box || current.Attempt.Enrollment != d.Enrollment || current.Attempt.BootID != d.BootID || current.Attempt.ControllerID != d.ControllerID) {
175 return errors.Join(err, hostbox.ErrChanged)
176 }
177 return errors.Join(err, d.settle(ctx, fresh))
178 }
179 out.Prepare = func(ctx context.Context, rt registry.Route, o hostclaim.Observed, p StartPolicy) (ClaimStart, error) {
180 s, err := base.Prepare(ctx, rt, o, p)
181 if err != nil || p.Probe || o.Claim == nil || o.Claim.Op.Kind != hostclaim.KindServing {
182 return s, err
183 }
184 r := retryOf(o)
185 if r == nil || r.Attempt == nil || r.Attempt.Session == 0 || r.Attempt.Session != o.Claim.Holder.Session {
186 return ClaimStart{}, hostbox.ErrChanged
187 }
188 episode, inc := r.EpisodeID, o.Claim.Holder.Incarnation
189 attempt := *r.Attempt
190 s.CurrentAttempt = func() hostclaim.WakeRetryAttempt { return attempt }
191 s.Reserve = func(ctx context.Context, n int) error {
192 if err := ctx.Err(); err != nil {
193 return err
194 }
195 fresh, err := hostclaim.Read(ctx, d.Claims, o.Host)
196 if err != nil {
197 return err
198 }
199 if !sameAdmission(fresh, o) {
200 return hostbox.ErrChanged
201 }
202 if n == 0 {
203 rr := retryOf(fresh)
204 if rr == nil || rr.EpisodeID != episode || rr.Attempt == nil || *rr.Attempt != attempt {
205 return hostbox.ErrChanged
206 }
207 return nil
208 }
209 inputs, err := d.Inputs(ctx, rt)
210 if err != nil {
211 return err
212 }
213 if inputs != attempt.LaunchInputs {
214 return hostbox.ErrChanged
215 }
216 next := d.attempt(inputs, o.Claim.Holder.Session)
217 written, err := hostclaim.Update(ctx, d.Claims, fresh, hostclaim.ReserveWakeRetryAttempt(hostclaim.WakeRetryReservation{HolderIncarnation: inc, EpisodeID: episode, Attempt: next, Now: d.Now()}))
218 if err == nil {
219 attempt = next
220 if s.RetryClaimWrite != nil {
221 return s.RetryClaimWrite(fresh, written)
222 }
223 }
224 return err
225 }
226 s.Failed = func(ctx context.Context, _ int) error {
227 recovery, cancel := CleanupContext(ctx, 10*time.Second)
228 defer cancel()
229 fresh, err := hostclaim.Read(recovery, d.Claims, o.Host)
230 if err != nil {
231 return err
232 }
233 rr := retryOf(fresh)
234 if rr == nil || rr.EpisodeID != episode || rr.Attempt == nil || *rr.Attempt != attempt {
235 return hostbox.ErrChanged
236 }
237 return d.settle(recovery, fresh, s.RetryClaimWrite)
238 }
239 s.Healthy = func(ctx context.Context, _ int, cmd *exec.Cmd) error {
240 if cmd == nil || cmd.Process == nil {
241 return hostbox.ErrChanged
242 }
243 if err := d.ExactHealth(ctx, cmd, attempt); err != nil {
244 return err
245 }
246 inputs, err := d.Inputs(ctx, rt)
247 if err != nil {
248 return err
249 }
250 if inputs != attempt.LaunchInputs {
251 return hostbox.ErrChanged
252 }
253 fresh, err := hostclaim.Read(ctx, d.Claims, o.Host)
254 if err != nil {
255 return err
256 }
257 if !sameAdmission(fresh, o) {
258 return hostbox.ErrChanged
259 }
260 _, err = hostclaim.Update(ctx, d.Claims, fresh, hostclaim.ClearWakeRetryHealthy(hostclaim.WakeRetryHealth{WakeRetryResult: hostclaim.WakeRetryResult{HolderIncarnation: inc, EpisodeID: episode, Attempt: attempt, Now: d.Now()}, ReadinessPassed: true}))
261 return err
262 }
263 return s, nil
264 }
265 return &out, nil
266}
267
268// SettleAbandoned is the seed rerun's settlement boundary. Its caller holds the
269// host exclusion. It spends no new attempt and remains usable below disk floor.
270func (d ServingRetries) SettleAbandoned(ctx context.Context, expected hostclaim.Observed) (hostclaim.Observed, error) {
271 if !d.valid() || ctx == nil || expected.Absent || expected.Claim == nil || expected.Claim.Holder == nil || expected.Claim.Op.Kind != hostclaim.KindServing || expected.Claim.Op.Exec != d.Box {
272 return hostclaim.Observed{}, hostwrite.ErrUnavailable
273 }
274 fresh, err := hostclaim.Read(ctx, d.Claims, expected.Host)
275 if err != nil {
276 return hostclaim.Observed{}, err
277 }
278 if fresh.Absent || fresh.ETag != expected.ETag || !hostclaim.SameWriterIdentity(fresh, expected) {
279 return hostclaim.Observed{}, hostbox.ErrChanged
280 }
281 if err := d.settle(ctx, fresh); err != nil {
282 return hostclaim.Observed{}, err
283 }
284 return hostclaim.Read(ctx, d.Claims, expected.Host)
285}
286