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 "encoding/json"
6 "errors"
7 "fmt"
8 "log"
9 "os"
10 "path/filepath"
11 "strings"
12
13 "github.com/carlosframework/platform/internal/moves"
14)
15
16// The moves-owned local retirement (ruling H5(b), the pattern of Plan 2's
17// extraction staging): artifacts the claim's inventory does not list, so
18// park.Local cannot retire them, renamed into the park engine's local
19// layout under a journalled plan, and an op's own journal directory parked
20// after them. The source's finalization (movesfinal.go) uses it; Plan 4's
21// rollback reuses it.
22
23func journalComponent(s string) bool {
24 return s != "" && s != "." && s != ".." && !strings.ContainsAny(s, "/\\\x00")
25}
26
27// localArtifactPark is one moves-owned local retirement (ruling H5(b), the
28// pattern of Plan 2's extraction staging): artifacts the claim's inventory
29// does not list, so park.Local cannot retire them, renamed into the park
30// engine's local layout, .parked/<incarnation>/<host>/<retirement>/artifacts/,
31// keeping their paths. Plan 4's rollback reuses it.
32type localArtifactPark struct {
33 DataDir, Incarnation, Host, Op string
34 // Paths are the candidates, relative to DataDir. Absent ones are recorded
35 // as absent and never touched.
36 Paths []string
37 // Step is the journal step that records the retirement id once every
38 // rename is durable; Step+"-plan" records the plan before the first one.
39 Step string
40 NewID func() string
41 // Fault is the test seam, after the plan and after each rename.
42 Fault func(point string) error
43 // Owns, when set, is asked about each candidate present when the plan
44 // is made, with its identity: false records it in the plan as not this
45 // park's (Unowned), so it is logged and left where it is, then and on
46 // every replay. The rollback's cleaning passes its ownership evidence
47 // here (the final review's finding 1); nil owns every candidate.
48 Owns func(path, id string) (bool, error)
49}
50
51// localParkPlan is what is journalled before the first rename: the
52// retirement id and every candidate's identity ("dev:ino"; "" when absent).
53type localParkPlan struct {
54 Retirement string `json:"retirement"`
55 Incarnation string `json:"incarnation"`
56 Items []localParkItem `json:"items"`
57}
58
59type localParkItem struct {
60 Path string `json:"path"`
61 ID string `json:"id,omitempty"`
62 // Unowned is a candidate that was present when the plan was made but
63 // that the park has no record of creating (localArtifactPark.Owns): ID
64 // is empty, and it is never moved.
65 Unowned bool `json:"unowned,omitempty"`
66}
67
68// localParkReceipt sits beside artifacts/ in the retirement, so a later
69// reader can find a moves-owned retirement by its op (D6, I57).
70type localParkReceipt struct {
71 V int `json:"v"`
72 Op string `json:"op"`
73 Host string `json:"host"`
74 Incarnation string `json:"incarnation"`
75 Retirement string `json:"retirement"`
76 Paths []string `json:"paths"`
77}
78
79const localParkReceiptName = "moves-retirement.json"
80
81// retireSync is the fsync every moves-owned retirement makes durable with
82// (syncPath); a variable so a test can see which directories were synced.
83var retireSync = syncPath
84
85// parkedEventDir and parkedArtifactsDir are the engine's local layout
86// (park.Local's parkLocal): one directory per retirement event.
87func parkedEventDir(dataDir, incarnation, host, retirement string) string {
88 return filepath.Join(dataDir, ".parked", incarnation, host, retirement)
89}
90
91func parkedArtifactsDir(dataDir, incarnation, host, retirement string) string {
92 return filepath.Join(parkedEventDir(dataDir, incarnation, host, retirement), "artifacts")
93}
94
95func journalledLocalPlan(j *moves.Journal, step string) (localParkPlan, error) {
96 var plan localParkPlan
97 raw, ok := j.Steps[step+"-plan"]
98 if !ok {
99 return plan, fmt.Errorf("the journal has no %s plan", step)
100 }
101 if err := json.Unmarshal([]byte(raw), &plan); err != nil {
102 return plan, fmt.Errorf("the journal's %s plan: %w", step, err)
103 }
104 if !journalComponent(plan.Retirement) || !journalComponent(plan.Incarnation) {
105 return plan, fmt.Errorf("the journal's %s plan names an unusable retirement", step)
106 }
107 return plan, nil
108}
109
110// artifactPathOK is a relative path under the data directory that is not
111// the guard's or the park's own.
112func artifactPathOK(p string) bool {
113 if p == "" || filepath.IsAbs(p) || filepath.Clean(p) != p || strings.ContainsAny(p, "\\\x00") {
114 return false
115 }
116 for _, c := range strings.Split(p, "/") {
117 if !journalComponent(c) {
118 return false
119 }
120 }
121 first, _, _ := strings.Cut(p, "/")
122 return first != ".parked" && first != "hostguard"
123}
124
125// parkLocalArtifacts runs one moves-owned local retirement and returns its
126// retirement id. In order: the plan (a fresh retirement id and each
127// candidate's identity) is journalled before anything moves; the retirement's
128// directories and its receipt are made; each recorded artifact is renamed to
129// the same path under artifacts/ and its source's parent and every
130// destination directory up to the data directory synced; then Step records
131// the retirement id. A replay resumes from the journal:
132// - an artifact already at its destination with its identity is synced as
133// a fresh rename is (a stop between the rename and its syncs leaves the
134// rename undurable, fix round 1), and is then done;
135// - one still at its live path with its identity is renamed;
136// - one recorded absent, or unowned (present, but with no record that
137// this park's op created it), or whose live path now holds something
138// with another identity, is not this park's: it is left where it is,
139// never moved or removed;
140// - a destination that holds anything else is an error, and nothing more
141// is moved.
142func parkLocalArtifacts(ctx context.Context, j *moves.Journal, mark func(step, value string) error, p localArtifactPark) (string, error) {
143 if r, ok := j.Steps[p.Step]; ok {
144 return r, nil
145 }
146 if p.DataDir == "" || !journalComponent(p.Incarnation) || !journalComponent(p.Host) || p.Op == "" || p.Step == "" || p.NewID == nil {
147 return "", errors.New("a local park names its data directory, incarnation, host, op and step")
148 }
149 for _, path := range p.Paths {
150 if !artifactPathOK(path) {
151 return "", fmt.Errorf("a local park cannot retire %q", path)
152 }
153 }
154 fault := func(point string) error {
155 if p.Fault != nil {
156 return p.Fault(point)
157 }
158 return nil
159 }
160 plan, err := journalledLocalPlan(j, p.Step)
161 if err != nil {
162 if _, planned := j.Steps[p.Step+"-plan"]; planned {
163 return "", err
164 }
165 if plan, err = p.plan(); err != nil {
166 return "", err
167 }
168 body, err := json.Marshal(plan)
169 if err != nil {
170 return "", err
171 }
172 if err := mark(p.Step+"-plan", string(body)); err != nil {
173 return "", err
174 }
175 }
176 if plan.Incarnation != p.Incarnation || len(plan.Items) != len(p.Paths) {
177 return "", fmt.Errorf("the journal's %s plan is not this park's", p.Step)
178 }
179 for i, it := range plan.Items {
180 if it.Path != p.Paths[i] {
181 return "", fmt.Errorf("the journal's %s plan is not this park's", p.Step)
182 }
183 }
184 dest := parkedArtifactsDir(p.DataDir, p.Incarnation, p.Host, plan.Retirement)
185 if err := mkdirSynced(p.DataDir, dest); err != nil {
186 return "", err
187 }
188 receipt, err := json.Marshal(localParkReceipt{V: 1, Op: p.Op, Host: p.Host, Incarnation: p.Incarnation, Retirement: plan.Retirement, Paths: p.Paths})
189 if err != nil {
190 return "", err
191 }
192 event := parkedEventDir(p.DataDir, p.Incarnation, p.Host, plan.Retirement)
193 if err := writeFileAtomic(filepath.Join(event, localParkReceiptName), receipt, 0o640); err != nil {
194 return "", err
195 }
196 for _, it := range plan.Items {
197 if err := ctx.Err(); err != nil {
198 return "", err
199 }
200 src, dst := filepath.Join(p.DataDir, it.Path), filepath.Join(dest, it.Path)
201 if it.Unowned {
202 log.Printf("moves: %s: %s has no record that %s created it; it is left where it is", p.Host, it.Path, p.Op)
203 continue
204 }
205 if it.ID == "" {
206 if _, err := os.Lstat(src); err == nil {
207 log.Printf("moves: %s: %s appeared after the park recorded it absent; it is not this park's and is left where it is", p.Host, it.Path)
208 }
209 continue
210 }
211 if done, err := sameIdentity(dst, it.ID); err != nil {
212 return "", err
213 } else if done {
214 if err := syncRename(p.DataDir, src, dst); err != nil {
215 return "", err
216 }
217 if err := fault("after-rename:" + it.Path); err != nil {
218 return "", err
219 }
220 continue
221 }
222 if _, err := os.Lstat(dst); err == nil {
223 return "", fmt.Errorf("%s holds what this park did not put there", dst)
224 } else if !errors.Is(err, os.ErrNotExist) {
225 return "", err
226 }
227 ours, err := sameIdentity(src, it.ID)
228 if err != nil {
229 return "", err
230 }
231 if !ours {
232 log.Printf("moves: %s: %s is no longer the artifact the park recorded; what is there is left where it is", p.Host, it.Path)
233 continue
234 }
235 if err := mkdirSynced(p.DataDir, filepath.Dir(dst)); err != nil {
236 return "", err
237 }
238 if err := os.Rename(src, dst); err != nil {
239 return "", fmt.Errorf("park %s: %w", it.Path, err)
240 }
241 if err := fault("after-rename-unsynced:" + it.Path); err != nil {
242 return "", err
243 }
244 if err := syncRename(p.DataDir, src, dst); err != nil {
245 return "", err
246 }
247 if err := fault("after-rename:" + it.Path); err != nil {
248 return "", err
249 }
250 }
251 if err := retireSync(dest); err != nil {
252 return "", err
253 }
254 if err := mark(p.Step, plan.Retirement); err != nil {
255 return "", err
256 }
257 log.Printf("moves: %s: the local artifacts of %s are parked in %s", p.Host, p.Op, event)
258 return plan.Retirement, nil
259}
260
261// plan records a fresh retirement id and each candidate's identity, or, for
262// a present candidate Owns refuses, that it is not this park's. The
263// retirement's directory must not exist yet: the id is this park's alone.
264func (p localArtifactPark) plan() (localParkPlan, error) {
265 plan := localParkPlan{Retirement: p.NewID(), Incarnation: p.Incarnation}
266 if !journalComponent(plan.Retirement) {
267 return plan, errors.New("no retirement id")
268 }
269 if _, err := os.Lstat(parkedEventDir(p.DataDir, p.Incarnation, p.Host, plan.Retirement)); err == nil {
270 return plan, fmt.Errorf("retirement %s already exists", plan.Retirement)
271 } else if !errors.Is(err, os.ErrNotExist) {
272 return plan, err
273 }
274 for _, path := range p.Paths {
275 id, err := pathIdentity(filepath.Join(p.DataDir, path))
276 if errors.Is(err, os.ErrNotExist) {
277 id, err = "", nil
278 }
279 if err != nil {
280 return plan, err
281 }
282 if id != "" && p.Owns != nil {
283 owned, err := p.Owns(path, id)
284 if err != nil {
285 return plan, fmt.Errorf("the ownership of %s: %w", path, err)
286 }
287 if !owned {
288 plan.Items = append(plan.Items, localParkItem{Path: path, Unowned: true})
289 continue
290 }
291 }
292 plan.Items = append(plan.Items, localParkItem{Path: path, ID: id})
293 }
294 return plan, nil
295}
296
297// mkdirSynced makes dir (under root) and syncs it and every directory above
298// it up to root. Every call syncs the whole chain, not only the directories
299// it made: a replay after a stop between MkdirAll and the syncs finds the
300// directories there and must still make their entries durable.
301func mkdirSynced(root, dir string) error {
302 if err := os.MkdirAll(dir, 0o770); err != nil {
303 return err
304 }
305 return syncChain(root, dir)
306}
307
308// syncChain syncs dir and every directory above it up to and including root.
309// dir must be root or under it.
310func syncChain(root, dir string) error {
311 root, dir = filepath.Clean(root), filepath.Clean(dir)
312 if rel, err := filepath.Rel(root, dir); err != nil || rel == ".." || strings.HasPrefix(rel, "../") {
313 return fmt.Errorf("%s is not under %s", dir, root)
314 }
315 for d := dir; ; d = filepath.Dir(d) {
316 if err := retireSync(d); err != nil {
317 return err
318 }
319 if d == root || d == filepath.Dir(d) {
320 return nil
321 }
322 }
323}
324
325// syncRename makes a rename from src to dst durable: the source's parent,
326// and every destination directory up to root.
327func syncRename(root, src, dst string) error {
328 if err := retireSync(filepath.Dir(src)); err != nil {
329 return err
330 }
331 return syncChain(root, filepath.Dir(dst))
332}
333
334// parkOpJournal renames the op's journal directory into the retirement's
335// artifacts, at .moves/<host>/<op>. The directory is the op's own (its name
336// is the op id). Absent with the destination there is a replay after the
337// rename, which is synced again and done; absent with no destination is
338// nothing to park; a destination already there beside the journal is
339// refused.
340func parkOpJournal(dataDir, incarnation, host, op, retirement string) error {
341 src := moves.JournalDir(dataDir, host, op)
342 dst := filepath.Join(parkedArtifactsDir(dataDir, incarnation, host, retirement), ".moves", host, op)
343 if _, err := os.Lstat(src); errors.Is(err, os.ErrNotExist) {
344 if _, err := os.Lstat(dst); err == nil {
345 return syncRename(dataDir, src, dst)
346 }
347 return nil
348 } else if err != nil {
349 return err
350 }
351 if _, err := os.Lstat(dst); err == nil {
352 return fmt.Errorf("%s already holds a journal", dst)
353 } else if !errors.Is(err, os.ErrNotExist) {
354 return err
355 }
356 if err := mkdirSynced(dataDir, filepath.Dir(dst)); err != nil {
357 return err
358 }
359 if err := os.Rename(src, dst); err != nil {
360 return err
361 }
362 return syncRename(dataDir, src, dst)
363}
364