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 "os"
5 "path/filepath"
6
7 "github.com/carlosframework/platform/internal/safefile"
8)
9
10// adoptPassLockFile is the lock every root adoption pass holds for its whole
11// run. It sits in the data dir beside the restart marks, so the single
12// filesystem the sandboxed agent and the root passes both write is the single
13// filesystem their coordination lives on.
14const adoptPassLockFile = "adopt-pass.lock"
15
16func adoptPassLockPath(dd string) string { return filepath.Join(dd, adoptPassLockFile) }
17
18// lockAdoptPass takes an exclusive flock for the whole of one RunAdopt, in
19// EVERY mode.
20//
21// Four units exec `carlos adopt`: carlos-adopt.service (hourly),
22// carlos-stage.service (the agent relaying a stage it is sandboxed out of),
23// carlos-restart.service and carlos-config-sync.service — and systemd will
24// start them in the same second without a thought. Until the staged-version
25// sweep existed that was tolerable: every write a pass performs is a
26// temp-file-then-rename or a symlink swap, so two passes doing the same work
27// converge on the same answer.
28//
29// os.RemoveAll is the first operation in a pass that is not idempotent
30// against a concurrent one. The window is small and real: the sweep reads the
31// live links, a stage pass repoints one at a version the sweep has just
32// decided is unpinned, and the sweep deletes the tree that link now names.
33// Rename-to-a-trash-dir does not close it either, because PointLive stats the
34// target and THEN symlinks — a rename between those two leaves a dangling
35// link and a unit that will not start. Serialising the passes does close it,
36// so that is what this is.
37//
38// The lock is a file of its own rather than a flock on something the pass
39// also writes: a flock lives on an inode, and every state file a pass owns is
40// replaced by rename, which would leave the winner holding a lock on a file
41// that is no longer the state (lockRestartMarks makes the same choice for the
42// same reason).
43//
44// Blocking, like lockRestartMarks: a relayed stage that waits out an hourly
45// pass is doing the right thing, and a stage is already a request the agent
46// re-raises on its next loop.
47func lockAdoptPass(dd string) (unlock func(), err error) {
48 return lockExclusive(adoptPassLockPath(dd))
49}
50
51// lockExclusive opens (creating) path and takes an exclusive advisory lock on
52// it, returning the release. Shared by lockAdoptPass and lockRestartMarks so
53// the two locks cannot drift in how they are taken; the ORDER between them is
54// fixed by their callers, adopt-pass first, and there is no third holder.
55func lockExclusive(path string) (unlock func(), err error) {
56 f, err := safefile.OpenFile(path, os.O_CREATE|os.O_RDWR, 0o644)
57 if err != nil {
58 return nil, err
59 }
60 if err := lockFileEx(f); err != nil {
61 f.Close()
62 return nil, err
63 }
64 return func() {
65 unlockFile(f)
66 f.Close()
67 }, nil
68}
69