| 1 | package host |
| 2 | |
| 3 | import ( |
| 4 | "errors" |
| 5 | "fmt" |
| 6 | "log" |
| 7 | "os" |
| 8 | "time" |
| 9 | |
| 10 | "github.com/carlosframework/platform/internal/controlname" |
| 11 | ) |
| 12 | |
| 13 | // The held-stage lock (final review finding 1): one flock per host, |
| 14 | // <DataDir>/.moves/held-<host>.lock, taken through movesDir (O_NOFOLLOW |
| 15 | // below a data dir root trusts, a regular file only), so root and the |
| 16 | // sandboxed agent lock the same file whichever created it. |
| 17 | // |
| 18 | // A held publication (release.publishHeld, run through heldStageAuthority's |
| 19 | // guard, by root's relay or by the agent on a writable box) holds it across |
| 20 | // reading the live link, binding, the swap, the re-hash and any rollback. |
| 21 | // T7's reading (release.VerifyHeldLive, from heldReleaseReady) takes it to |
| 22 | // read the link and hash the bytes it names. So T7 never sees a link that a |
| 23 | // failing publication is about to take back, and never hashes bytes the |
| 24 | // publication has not finished vouching for. |
| 25 | // |
| 26 | // Lock order: the apply flock > floor.lock > the registry's write lock > |
| 27 | // this one. The publication takes it inside WithHoldsFrozen; T7 takes it |
| 28 | // holding nothing. Its holder takes no other lock and makes no network |
| 29 | // call (D3's admit, which may read the bucket, runs before it on both |
| 30 | // sides), so it never inverts the order and never holds the registry's |
| 31 | // freeze for long. |
| 32 | // |
| 33 | // The file is removed when the stage finishes (region HA M-K25, #236), by |
| 34 | // the holder, while it still holds the flock (unlockRemoving). Main's |
| 35 | // empty-installation census refuses any file naming the host being |
| 36 | // installed, so a lock left behind would refuse every later from-empty |
| 37 | // install of that host on this box. A waiter that opened the removed file |
| 38 | // re-checks the name after its flock and starts again on the new one |
| 39 | // (lockNamed), so two holders never exist at once. A holder killed |
| 40 | // mid-stage never removes it: the file stays, the census refuses that host |
| 41 | // (region HA's CensusTree(".moves") fallback refuses only that host), and |
| 42 | // the next stage of the host locks the same file and then removes it. |
| 43 | |
| 44 | // heldLockWait bounds the wait for a held-stage lock. The publication waits |
| 45 | // for it inside the registry's freeze, after a claim read bounded by |
| 46 | // heldStageFrozenReadTimeout, so the two stay well under the registry's |
| 47 | // 5 s busy_timeout. Its holders keep it for a hash or two of one binary. |
| 48 | // A variable so a test can make contention time out quickly. |
| 49 | var heldLockWait = time.Second |
| 50 | |
| 51 | // errHeldLockBusy is the held-stage lock still held after heldLockWait: |
| 52 | // a publication, or a reading, is under way. Never a reason to go ahead |
| 53 | // without it. |
| 54 | var errHeldLockBusy = errors.New("another process holds this host's held-stage lock") |
| 55 | |
| 56 | // heldStageLockFile is host's lock file in .moves. |
| 57 | func heldStageLockFile(host string) string { return "held-" + host + ".lock" } |
| 58 | |
| 59 | // withHeldStageLock runs fn holding host's held-stage lock, making .moves |
| 60 | // when it is missing, and removes the lock file before releasing it, |
| 61 | // whatever fn returns. An error opening or locking is returned without |
| 62 | // running fn. A removal that fails is logged and leaves the file as a crash |
| 63 | // would; fn's result stands. Off Linux it always refuses (openMovesDir |
| 64 | // does), which fails T7 and the publication closed; production boxes are |
| 65 | // Linux. |
| 66 | func withHeldStageLock(dataDir, host string, fn func() error) error { |
| 67 | if !controlname.ValidHostShape(host) { |
| 68 | return fmt.Errorf("held-stage lock: malformed host %q", host) |
| 69 | } |
| 70 | d, err := openMovesDir(dataDir, false) |
| 71 | if errors.Is(err, os.ErrNotExist) { |
| 72 | d, err = openMovesDir(dataDir, true) |
| 73 | } |
| 74 | if err != nil { |
| 75 | return err |
| 76 | } |
| 77 | defer d.Close() |
| 78 | if err := d.lockNamed(heldStageLockFile(host), heldLockWait, errHeldLockBusy); err != nil { |
| 79 | return err |
| 80 | } |
| 81 | err = fn() |
| 82 | if rerr := d.unlockRemoving(); rerr != nil { |
| 83 | log.Printf("moves: %s: held-stage lock: %v", host, rerr) |
| 84 | } |
| 85 | return err |
| 86 | } |
| 87 | |