carlos / platform Public

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

Plain git — no account needed to clone.

Download

Download this file

1package moves
2
3import (
4 "bytes"
5 "encoding/json"
6 "errors"
7 "fmt"
8 "io/fs"
9 "os"
10 "path/filepath"
11 "strings"
12 "sync"
13 "time"
14)
15
16// journalVersion is the only journal format this code reads or writes. A
17// file with any other version is refused, never reinterpreted.
18const journalVersion = 1
19
20const journalFile = "journal.json"
21
22// Journal is the box-local record of how far one move operation has got on
23// this box (§9.1, §9.2). A step that is Done is never redone after a
24// restart; the value a step was marked with is whatever that step needs to
25// resume (a digest, an id, a timestamp). Values are internal state, not
26// operator copy.
27type Journal struct {
28 V int `json:"v"`
29 Host string `json:"host"`
30 Op string `json:"op"`
31 Phase string `json:"phase,omitempty"`
32 Steps map[string]string `json:"steps,omitempty"`
33 Attempt int `json:"attempt,omitempty"`
34 UpdatedAt time.Time `json:"updated_at"`
35
36 // changes are what this value changed since it was read or last
37 // committed (Mark, Unmark), in order, and loadedPhase and loadedAttempt
38 // the Phase and Attempt it was read with: what Commit applies.
39 changes []journalChange
40 loadedPhase string
41 loadedAttempt int
42}
43
44// journalChange is one Mark (del false) or Unmark of a step.
45type journalChange struct {
46 step, value string
47 del bool
48}
49
50// A move's journal has more than one writer on a box: the pass that runs the
51// phase, the probe owners and attempts (Plan 6 fix round 1, ruling 3). Every
52// read-modify-write of one journal goes through UpdateJournal, which holds
53// that journal's lock, the one lock every writer in this process shares
54// (journalLock), across the read, the change and the write (fix round 2,
55// ruling 2). There is no plain write: a writer that read the journal earlier
56// commits its own changes (Commit), which UpdateJournal applies to the
57// journal as it is then, so nothing the writer read and did not change is
58// ever written back over another writer's.
59var journalLocks sync.Map // the journal's directory -> *sync.Mutex
60
61// journalLock is the lock of the journal of (host, op) under dataDir.
62func journalLock(dataDir, host, op string) *sync.Mutex {
63 l, _ := journalLocks.LoadOrStore(filepath.Clean(JournalDir(dataDir, host, op)), &sync.Mutex{})
64 return l.(*sync.Mutex)
65}
66
67// UpdateJournal reads the journal of (host, op), calls fn with it and writes
68// it, all under the journal's lock. It makes the journal when there is none.
69// An error from fn writes nothing and is returned. fn must not call Commit.
70func UpdateJournal(dataDir, host, op string, fn func(*Journal) error) error {
71 if err := journalNames(host, op); err != nil {
72 return err
73 }
74 l := journalLock(dataDir, host, op)
75 l.Lock()
76 defer l.Unlock()
77 j, err := ReadJournal(dataDir, host, op)
78 if err != nil {
79 return err
80 }
81 if err := fn(j); err != nil {
82 return err
83 }
84 return j.write(dataDir)
85}
86
87// Commit makes j's own changes durable: through UpdateJournal, on the
88// journal as it is on disk now, it applies what j changed since it was read
89// or last committed (its Marks and Unmarks in order, and Phase and Attempt
90// if j changed them) and nothing else, and j becomes the journal as written.
91// A step another writer advanced or deleted after j was read stays as that
92// writer left it unless j itself marks it.
93func (j *Journal) Commit(dataDir string) error {
94 var now *Journal
95 err := UpdateJournal(dataDir, j.Host, j.Op, func(d *Journal) error {
96 for _, c := range j.changes {
97 if c.del {
98 delete(d.Steps, c.step)
99 } else {
100 d.Steps[c.step] = c.value
101 }
102 }
103 if j.Phase != j.loadedPhase {
104 d.Phase = j.Phase
105 }
106 if j.Attempt != j.loadedAttempt {
107 d.Attempt = j.Attempt
108 }
109 now = d
110 return nil
111 })
112 if err != nil {
113 return err
114 }
115 j.V, j.Phase, j.Attempt, j.UpdatedAt = now.V, now.Phase, now.Attempt, now.UpdatedAt
116 j.Steps = make(map[string]string, len(now.Steps))
117 for k, v := range now.Steps {
118 j.Steps[k] = v
119 }
120 j.changes, j.loadedPhase, j.loadedAttempt = nil, j.Phase, j.Attempt
121 return nil
122}
123
124// JournalDir is where one operation's journal lives on the box:
125// <dataDir>/.moves/<host>/<op>/.
126func JournalDir(dataDir, host, op string) string {
127 return filepath.Join(dataDir, ".moves", host, op)
128}
129
130// journalName refuses a host or operation id that is empty or could leave
131// its directory. Both come from console-issued records, but the journal
132// path is built from them, so they are checked here as well.
133func journalName(kind, s string) error {
134 if s == "" || s == "." || s == ".." || strings.ContainsAny(s, "/\\\x00") {
135 return fmt.Errorf("moves journal: unusable %s %q", kind, s)
136 }
137 return nil
138}
139
140func journalNames(host, op string) error {
141 if err := journalName("host", host); err != nil {
142 return err
143 }
144 return journalName("op", op)
145}
146
147// JournalFile is the journal's file for (host, op):
148// <JournalDir>/journal.json. Root reads it only through an anchored,
149// no-symlink open of a regular file (internal/host's held release), never
150// by name with ReadJournal.
151func JournalFile(dataDir, host, op string) string {
152 return filepath.Join(JournalDir(dataDir, host, op), journalFile)
153}
154
155// ReadJournal loads the journal for (host, op). A journal that does not
156// exist yet is an empty one. A file that exists but cannot be trusted
157// (unreadable, not valid JSON, an unknown version, or naming a different
158// host or op) is an error and never an empty journal: starting over could
159// redo a step that already ran.
160func ReadJournal(dataDir, host, op string) (*Journal, error) {
161 if err := journalNames(host, op); err != nil {
162 return nil, err
163 }
164 b, err := os.ReadFile(JournalFile(dataDir, host, op))
165 if errors.Is(err, fs.ErrNotExist) {
166 return &Journal{V: journalVersion, Host: host, Op: op, Steps: map[string]string{}}, nil
167 }
168 if err != nil {
169 return nil, fmt.Errorf("moves journal %s/%s: %w", host, op, err)
170 }
171 return DecodeJournal(b, host, op)
172}
173
174// DecodeJournal is ReadJournal's judgement of a journal's bytes b, read by
175// the caller: one JSON object of this version naming exactly (host, op).
176func DecodeJournal(b []byte, host, op string) (*Journal, error) {
177 if err := journalNames(host, op); err != nil {
178 return nil, err
179 }
180 var j Journal
181 dec := json.NewDecoder(bytes.NewReader(b))
182 if err := dec.Decode(&j); err != nil {
183 return nil, fmt.Errorf("moves journal %s/%s is corrupt: %w", host, op, err)
184 }
185 if dec.More() {
186 return nil, fmt.Errorf("moves journal %s/%s is corrupt: trailing data", host, op)
187 }
188 if j.V != journalVersion {
189 return nil, fmt.Errorf("moves journal %s/%s: unknown version %d", host, op, j.V)
190 }
191 if j.Host != host || j.Op != op {
192 return nil, fmt.Errorf("moves journal %s/%s names %s/%s", host, op, j.Host, j.Op)
193 }
194 if j.Steps == nil {
195 j.Steps = map[string]string{}
196 }
197 j.loadedPhase, j.loadedAttempt = j.Phase, j.Attempt
198 return &j, nil
199}
200
201// write stamps UpdatedAt and saves the journal whole: a temp file in the
202// same directory, fsync, rename over the old file, then fsync of the
203// directory, so a crash leaves either the old journal or the new one. Only
204// UpdateJournal calls it, with the journal's lock held since its read
205// (TestJournalHasNoPlainWrite).
206func (j *Journal) write(dataDir string) error {
207 if err := journalNames(j.Host, j.Op); err != nil {
208 return err
209 }
210 if j.V == 0 {
211 j.V = journalVersion
212 }
213 if j.V != journalVersion {
214 return fmt.Errorf("moves journal %s/%s: unknown version %d", j.Host, j.Op, j.V)
215 }
216 j.UpdatedAt = time.Now().UTC()
217 b, err := json.Marshal(j)
218 if err != nil {
219 return err
220 }
221 dir := JournalDir(dataDir, j.Host, j.Op)
222 created, err := missingDirs(dir)
223 if err != nil {
224 return err
225 }
226 if err := os.MkdirAll(dir, 0o700); err != nil {
227 return err
228 }
229 // A directory this call created is only durable once its parent's
230 // entry is, so sync the parent of each one, or a crash could lose the
231 // whole chain with the journal inside it.
232 for _, c := range created {
233 if err := syncDir(filepath.Dir(c)); err != nil {
234 return err
235 }
236 }
237 tmp, err := os.CreateTemp(dir, journalFile+".tmp-*")
238 if err != nil {
239 return err
240 }
241 tmpName := tmp.Name()
242 fail := func(err error) error {
243 tmp.Close()
244 os.Remove(tmpName)
245 return err
246 }
247 if _, err := tmp.Write(b); err != nil {
248 return fail(err)
249 }
250 if err := tmp.Sync(); err != nil {
251 return fail(err)
252 }
253 if err := tmp.Close(); err != nil {
254 os.Remove(tmpName)
255 return err
256 }
257 if err := os.Rename(tmpName, filepath.Join(dir, journalFile)); err != nil {
258 os.Remove(tmpName)
259 return err
260 }
261 return syncDir(dir)
262}
263
264// missingDirs lists the directories at and above dir that do not exist yet,
265// deepest first: the ones MkdirAll is about to create.
266func missingDirs(dir string) ([]string, error) {
267 var out []string
268 for p := filepath.Clean(dir); ; p = filepath.Dir(p) {
269 _, err := os.Lstat(p)
270 if err == nil {
271 return out, nil
272 }
273 if !errors.Is(err, fs.ErrNotExist) {
274 return nil, err
275 }
276 out = append(out, p)
277 if filepath.Dir(p) == p {
278 return out, nil
279 }
280 }
281}
282
283func syncDir(dir string) error {
284 d, err := os.Open(dir)
285 if err != nil {
286 return err
287 }
288 defer d.Close()
289 return d.Sync()
290}
291
292// Done reports whether step has been marked.
293func (j *Journal) Done(step string) bool {
294 _, ok := j.Steps[step]
295 return ok
296}
297
298// Mark records step as done with value. It changes the in-memory journal
299// only; Commit makes it durable.
300func (j *Journal) Mark(step, value string) {
301 if j.Steps == nil {
302 j.Steps = map[string]string{}
303 }
304 j.Steps[step] = value
305 j.changes = append(j.changes, journalChange{step: step, value: value})
306}
307
308// Unmark removes step. It changes the in-memory journal only; Commit makes
309// it durable.
310func (j *Journal) Unmark(step string) {
311 delete(j.Steps, step)
312 j.changes = append(j.changes, journalChange{step: step, del: true})
313}
314