package host import ( "errors" "fmt" "log" "os" "time" "github.com/carlosframework/platform/internal/controlname" ) // The held-stage lock (final review finding 1): one flock per host, // /.moves/held-.lock, taken through movesDir (O_NOFOLLOW // below a data dir root trusts, a regular file only), so root and the // sandboxed agent lock the same file whichever created it. // // A held publication (release.publishHeld, run through heldStageAuthority's // guard, by root's relay or by the agent on a writable box) holds it across // reading the live link, binding, the swap, the re-hash and any rollback. // T7's reading (release.VerifyHeldLive, from heldReleaseReady) takes it to // read the link and hash the bytes it names. So T7 never sees a link that a // failing publication is about to take back, and never hashes bytes the // publication has not finished vouching for. // // Lock order: the apply flock > floor.lock > the registry's write lock > // this one. The publication takes it inside WithHoldsFrozen; T7 takes it // holding nothing. Its holder takes no other lock and makes no network // call (D3's admit, which may read the bucket, runs before it on both // sides), so it never inverts the order and never holds the registry's // freeze for long. // // The file is removed when the stage finishes (region HA M-K25, #236), by // the holder, while it still holds the flock (unlockRemoving). Main's // empty-installation census refuses any file naming the host being // installed, so a lock left behind would refuse every later from-empty // install of that host on this box. A waiter that opened the removed file // re-checks the name after its flock and starts again on the new one // (lockNamed), so two holders never exist at once. A holder killed // mid-stage never removes it: the file stays, the census refuses that host // (region HA's CensusTree(".moves") fallback refuses only that host), and // the next stage of the host locks the same file and then removes it. // heldLockWait bounds the wait for a held-stage lock. The publication waits // for it inside the registry's freeze, after a claim read bounded by // heldStageFrozenReadTimeout, so the two stay well under the registry's // 5 s busy_timeout. Its holders keep it for a hash or two of one binary. // A variable so a test can make contention time out quickly. var heldLockWait = time.Second // errHeldLockBusy is the held-stage lock still held after heldLockWait: // a publication, or a reading, is under way. Never a reason to go ahead // without it. var errHeldLockBusy = errors.New("another process holds this host's held-stage lock") // heldStageLockFile is host's lock file in .moves. func heldStageLockFile(host string) string { return "held-" + host + ".lock" } // withHeldStageLock runs fn holding host's held-stage lock, making .moves // when it is missing, and removes the lock file before releasing it, // whatever fn returns. An error opening or locking is returned without // running fn. A removal that fails is logged and leaves the file as a crash // would; fn's result stands. Off Linux it always refuses (openMovesDir // does), which fails T7 and the publication closed; production boxes are // Linux. func withHeldStageLock(dataDir, host string, fn func() error) error { if !controlname.ValidHostShape(host) { return fmt.Errorf("held-stage lock: malformed host %q", host) } d, err := openMovesDir(dataDir, false) if errors.Is(err, os.ErrNotExist) { d, err = openMovesDir(dataDir, true) } if err != nil { return err } defer d.Close() if err := d.lockNamed(heldStageLockFile(host), heldLockWait, errHeldLockBusy); err != nil { return err } err = fn() if rerr := d.unlockRemoving(); rerr != nil { log.Printf("moves: %s: held-stage lock: %v", host, rerr) } return err }