carlos / platform Public

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

Plain git — no account needed to clone.

Download

Download this file

1package hostops
2
3import (
4 "context"
5 "errors"
6 "os"
7 "slices"
8
9 "github.com/carlosframework/platform/internal/hostclaim"
10)
11
12func (c Controller) CensusPurge(ctx context.Context, account, app, id, appID string) error {
13 j, tag, err := ReadPurgeJournal(ctx, c.Store, account, app, id, appID)
14 if err != nil {
15 return err
16 }
17 if j.Phase != "reserved" {
18 return ErrPending
19 }
20 records, err := Records(ctx, c.Store, account, app)
21 if err != nil {
22 return err
23 }
24 for _, r := range records {
25 obs, readErr := hostclaim.Read(ctx, c.Store, r.Holder.Host)
26 if readErr != nil {
27 return readErr
28 }
29 if obs.Absent {
30 return ErrPending
31 }
32 if !matches(r, obs.Claim.Holder) {
33 return ErrIdentity
34 }
35 }
36 hosts, err := ListClaims(ctx, c.Store)
37 if err != nil {
38 return err
39 }
40 for _, host := range hosts {
41 obs, err := hostclaim.Read(ctx, c.Store, host)
42 if err != nil {
43 return err
44 }
45 if obs.Absent || obs.Claim.Holder == nil {
46 continue
47 }
48 h := obs.Claim.Holder
49 if h.Account != account || h.App != app {
50 continue
51 }
52 if obs.Claim.Op.Kind != hostclaim.KindServing || !obs.Claim.Touched.Complete {
53 return ErrPending
54 }
55 if _, _, err = c.Authority.Inventory(ctx, obs); err != nil {
56 return err
57 }
58 r, err := ReadRecord(ctx, c.Store, h.Key)
59 if err != nil {
60 return err
61 }
62 if !matches(r, h) {
63 return ErrIdentity
64 }
65 progress := j.Hosts[host]
66 holder := *h
67 progress.Holder = &holder
68 progress.RecordETag = r.ETag
69 progress.ClaimETag = obs.ETag
70 progress.ClaimGen = obs.Claim.Gen
71 j.Hosts[host] = progress
72 }
73 j.Phase = "adopted"
74 _, err = c.Control.SavePurgeJournal(ctx, j, tag)
75 return err
76}
77func (c Controller) ClaimPurgeHosts(ctx context.Context, account, app, id, appID string) error {
78 j, _, err := ReadPurgeJournal(ctx, c.Store, account, app, id, appID)
79 if err != nil {
80 return err
81 }
82 if j.Phase != "adopted" {
83 return ErrPending
84 }
85 for host, p := range j.Hosts {
86 if p.Holder == nil {
87 continue
88 }
89 obs, err := hostclaim.Read(ctx, c.Store, host)
90 if err != nil {
91 return err
92 }
93 if obs.Absent || obs.Claim.Holder == nil || obs.Claim.Holder.Incarnation != p.Holder.Incarnation {
94 return ErrIdentity
95 }
96 if obs.Claim.Op.Kind == hostclaim.KindPurging && obs.Claim.Op.ID == id {
97 continue
98 }
99 if obs.ETag != p.ClaimETag || obs.Claim.Gen != p.ClaimGen {
100 // A canceled unused hold invalidates its old observation. Re-save
101 // only while this purge still owns the adopted phase and holder.
102 fresh, tag, e := ReadPurgeJournal(ctx, c.Store, account, app, id, appID)
103 if e != nil {
104 return e
105 }
106 if fresh.Phase != "adopted" || obs.Claim.Op.Kind != hostclaim.KindServing || obs.Claim.Holder.Stamp != p.Holder.Stamp {
107 return ErrPending
108 }
109 saved := fresh.Hosts[host]
110 saved.ClaimETag = obs.ETag
111 saved.ClaimGen = obs.Claim.Gen
112 fresh.Hosts[host] = saved
113 _, e = c.Control.SavePurgeJournal(ctx, fresh, tag)
114 return e
115 }
116 authority, ok := c.Authority.(*ProductionAuthority)
117 if !ok {
118 return ErrContract
119 }
120 _, err = authority.StartPurgeClaim(ctx, obs, account, app, appID, id, j.RequestedBy)
121 if err != nil {
122 return err
123 }
124 }
125 fresh, tag, err := ReadPurgeJournal(ctx, c.Store, account, app, id, appID)
126 if err != nil {
127 return err
128 }
129 if fresh.Phase != "adopted" {
130 return ErrPending
131 }
132 fresh.Phase = "claimed"
133 _, err = c.Control.SavePurgeJournal(ctx, fresh, tag)
134 return err
135}
136func (c Controller) PurgeBarrier(ctx context.Context, account, app, id, appID string) error {
137 j, _, err := ReadPurgeJournal(ctx, c.Store, account, app, id, appID)
138 if err != nil {
139 return err
140 }
141 if j.Phase != "claimed" {
142 return ErrPending
143 }
144 for host, p := range j.Hosts {
145 if p.Holder == nil {
146 continue
147 }
148 obs, err := hostclaim.Read(ctx, c.Store, host)
149 if err != nil {
150 return err
151 }
152 if obs.Absent || obs.Claim.Holder == nil || obs.Claim.Holder.Incarnation != p.Holder.Incarnation || obs.Claim.Op.Kind != hostclaim.KindPurging || obs.Claim.Op.ID != id {
153 return ErrPending
154 }
155 if err = c.Authority.FirstPhase(ctx, obs); err != nil {
156 return err
157 }
158 }
159 return nil
160}
161
162// ToParking records history before its chores finish. The old holder must
163// also be gone; FreeAfterPark is the only transition that removes it.
164func purgeHostFreed(claim hostclaim.Claim, p PurgeHost) bool {
165 if p.Holder == nil || (claim.Holder != nil && claim.Holder.Incarnation == p.Holder.Incarnation) {
166 return false
167 }
168 return slices.Contains(claim.RetiredIncarnations, p.Holder.Incarnation) ||
169 (p.FreeFromGen != 0 && claim.Gen > p.FreeFromGen)
170}
171func (c Controller) RetirePurgeHosts(ctx context.Context, account, app, id, appID string) error {
172 j, _, err := ReadPurgeJournal(ctx, c.Store, account, app, id, appID)
173 if err != nil {
174 return err
175 }
176 if j.Phase != "torn-down" {
177 return ErrPending
178 }
179 for host, p := range j.Hosts {
180 if p.Holder == nil || p.Retired {
181 continue
182 }
183 obs, err := hostclaim.Read(ctx, c.Store, host)
184 if err != nil {
185 return err
186 }
187 if obs.Absent {
188 return ErrPending
189 }
190 if purgeHostFreed(*obs.Claim, p) {
191 fresh, tag, err := ReadPurgeJournal(ctx, c.Store, account, app, id, appID)
192 if err != nil {
193 return err
194 }
195 saved := fresh.Hosts[host]
196 saved.Retired = true
197 fresh.Hosts[host] = saved
198 if _, err = c.Control.SavePurgeJournal(ctx, fresh, tag); err != nil {
199 return err
200 }
201 continue
202 }
203 if obs.Claim.Holder == nil || obs.Claim.Holder.Incarnation != p.Holder.Incarnation || obs.Claim.Op.ID != id {
204 return ErrPending
205 }
206 if obs.Claim.Op.Kind == hostclaim.KindParking {
207 if len(obs.Claim.Obligations) != 0 {
208 return ErrPending
209 }
210 fresh, tag, err := ReadPurgeJournal(ctx, c.Store, account, app, id, appID)
211 if err != nil {
212 return err
213 }
214 saved := fresh.Hosts[host]
215 saved.FreeFromGen = obs.Claim.Gen
216 fresh.Hosts[host] = saved
217 if _, err = c.Control.SavePurgeJournal(ctx, fresh, tag); err != nil {
218 return err
219 }
220 if err = c.Free(ctx, host, id); err != nil {
221 return err
222 }
223 return ErrPending
224 }
225 if obs.Claim.Op.Kind != hostclaim.KindPurging {
226 return ErrPending
227 }
228 if obs.Claim.Op.Intent["etag"] == "" {
229 r, err := ReadRecord(ctx, c.Store, p.Holder.Key)
230 if errors.Is(err, os.ErrNotExist) {
231 return ErrPending
232 }
233 if err != nil {
234 return err
235 }
236 if !matches(r, p.Holder) {
237 return ErrIdentity
238 }
239 // Save the exact observed record before the host intent, so a crash
240 // cannot make later replay rediscover an unrelated version.
241 fresh, tag, err := ReadPurgeJournal(ctx, c.Store, account, app, id, appID)
242 if err != nil {
243 return err
244 }
245 saved := fresh.Hosts[host]
246 saved.RecordETag = r.ETag
247 fresh.Hosts[host] = saved
248 if _, err = c.Control.SavePurgeJournal(ctx, fresh, tag); err != nil {
249 return err
250 }
251 if _, err = hostclaim.Update(ctx, c.Store, obs, PurgeRecordIntent(id, r.Holder.Key, r.ETag)); err != nil {
252 return err
253 }
254 }
255 if err = c.Retire(ctx, host, id); err != nil {
256 return err
257 }
258 return ErrPending
259 }
260 fresh, tag, err := ReadPurgeJournal(ctx, c.Store, account, app, id, appID)
261 if err != nil {
262 return err
263 }
264 for _, p := range fresh.Hosts {
265 if p.Holder != nil && !p.Retired {
266 return ErrPending
267 }
268 }
269 _ = tag
270 return nil
271}
272