carlos / platform Public

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

Plain git — no account needed to clone.

Download

Download this file

1package release
2
3import (
4 "context"
5 "errors"
6 "fmt"
7 "path/filepath"
8 "time"
9
10 "github.com/carlosframework/platform/internal/controlname"
11 "github.com/carlosframework/platform/internal/registry"
12)
13
14// The carry fence (settings plan 4, ruling R-CARRY-FENCE; Astra round 2
15// finding 2). Root publishes its request record for a host in
16// /opt/carlos/consumed/<host> (WriteConsumed); before a graceful move seals
17// the build state, the agent carries that record into it (CarryRequest).
18// Root's pass decides from a registry snapshot taken before the pass, so
19// without a fence a pass already running when a cut begins could publish a
20// resolution or a failed completion after the carry read the file: the cut
21// seals a state without it, and the destination resolves the request again.
22//
23// The two serialize on one cross-process lock per host, flock on
24// ConsumedLockPath. Root takes it, re-reads the route and publishes only
25// when the route is in no move hold (publishGate), then releases. The cut
26// takes it only once its move hold is effective (the row's status is a hold:
27// registry.IsHold), carries, then releases. So a root publication lands
28// before the carry and is carried, or sees the hold and is not made at all
29// (nothing is installed for that route on that pass). Neither side waits on
30// the other for more than one record write or one build-state write.
31
32// consumedFenceWait bounds a wait for the carry fence, on either side. A
33// var so tests can shrink it.
34var consumedFenceWait = 30 * time.Second
35
36// ErrMoveHeld is root's refusal to publish a request record for a route a
37// move holds (publishGate): nothing is written and nothing installed for it
38// on that pass.
39var ErrMoveHeld = errors.New("the route is held by a move, so root publishes no request record for it")
40
41// ConsumedLockPath is host's carry-fence lock file, beside its consumed file.
42// The leading dot is no host's shape, so it never reads as a consumed file.
43// The caller validates host (controlname.ValidHostShape).
44func ConsumedLockPath(optDir, host string) string {
45 return filepath.Join(optDir, "consumed", "."+host+".lock")
46}
47
48// publishGate is root's re-check inside the carry fence: nil when root may
49// publish a request record for rt, the route as the registry holds it now.
50// A row a move holds (any of registry.HoldStatuses: the source moving, a
51// rollback's restoring or releasing, a landing) or a row gone from this box
52// is refused with ErrMoveHeld. The update simulation calls it too.
53func publishGate(rt *registry.Route) error {
54 if rt == nil {
55 return fmt.Errorf("%w: the row is gone from this box", ErrMoveHeld)
56 }
57 if registry.IsHold(rt.Status) {
58 return fmt.Errorf("%w: its status is %q", ErrMoveHeld, rt.Status)
59 }
60 return nil
61}
62
63// beforeConsumedWrite, when set (tests only), runs inside root's fenced
64// publication: at "fence" before the lock is taken, and at "write" with the
65// lock held and the re-check passed, immediately before WriteConsumed.
66var beforeConsumedWrite func(host, at string)
67
68// publishConsumed is root's publication of c under the carry fence: the
69// lock, the route re-read and re-checked (publishGate), then WriteConsumed.
70// route reads the registry row now. A refusal (ErrMoveHeld), a fence that
71// could not be taken, or a row that does not read writes nothing.
72func publishConsumed(ctx context.Context, optDir string, route func(host string) (*registry.Route, error), c Consumed) error {
73 if !controlname.ValidHostShape(c.Host) {
74 return fmt.Errorf("consumed: invalid host %q", c.Host)
75 }
76 if hook := beforeConsumedWrite; hook != nil {
77 hook(c.Host, "fence")
78 }
79 unlock, err := lockConsumedPublish(ctx, optDir, c.Host)
80 if err != nil {
81 return fmt.Errorf("the carry fence: %w", err)
82 }
83 defer unlock()
84 if route == nil {
85 return errors.New("no registry to re-check the route under the carry fence")
86 }
87 rt, err := route(c.Host)
88 if err != nil {
89 return fmt.Errorf("re-read the route under the carry fence: %w", err)
90 }
91 if err := publishGate(rt); err != nil {
92 return err
93 }
94 if hook := beforeConsumedWrite; hook != nil {
95 hook(c.Host, "write")
96 }
97 return WriteConsumed(optDir, c)
98}
99
100// FenceCarry is the cut's side of the carry fence for host, taken once the
101// move hold on its row is effective and held across the carry: unlock
102// releases it. A host whose lock file does not exist (root has never
103// published under the fence for it, or the consumed directory does not
104// exist) is carried without the lock: any later publication takes the lock
105// first and then sees the hold. A lock file that does not open, or a fence
106// not taken within its bound, is an error, and the cut does not carry.
107func FenceCarry(ctx context.Context, optDir, host string) (unlock func(), err error) {
108 if !controlname.ValidHostShape(host) {
109 return nil, fmt.Errorf("consumed: invalid host %q", host)
110 }
111 return lockConsumedCarry(ctx, optDir, host)
112}
113