carlos / platform Public

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

Plain git — no account needed to clone.

Download

Download this file

1# Region HA contracts for the claim and moves sessions
2
3Branch `region-ha-contracts`, 2026-10-07. Three contracts that the claim
4session (L13) and the moves session (L15, L17 in
5`2026-10-06-moves-region-ha-conflicts.md`) asked region HA for. All three
6are dormant: no production path calls them yet.
7
8## 1. Origin-qualified object references (claim L13)
9
10Package `internal/hostfiles`.
11
12The manifest format is now version 2 (`hostfiles.ManifestVersion = 2`).
13`Entry.Object` is the object's full key from the bucket root:
14
15 instances/<host>/<inc>/files/<gen>/objects/<16-digit seq>-<64 hex sha256>
16
17The publisher writes it under its own prefix. Readers fetch exactly that
18key and never join it to their own prefix. Version 1 held a key relative
19to the manifest's prefix. The field's meaning changed, so the version was
20bumped. Any manifest that is not version 2 (including 1) is refused by
21`Load`, `Open`, `Restore` and `CopyManifest` with "manifest <prefix> is
22version N; this binary reads 2". No version 1 manifest exists in
23production, so nothing needs migrating.
24
25Signatures:
26
27```go
28func Restore(ctx context.Context, s release.Store, prefix, dir string, origins []string) (Manifest, RestoreStats, error)
29func ReadPlatform(ctx context.Context, s release.Store, prefix string, m Manifest, name string, origins []string) ([]byte, error)
30func LoadWake(ctx context.Context, s release.Store, prefix string, m Manifest, origins []string) (WakeState, bool, error)
31func CopyManifest(ctx context.Context, s release.Store, fromPrefix, toPrefix string, expectSeq int64, origins []string) (Manifest, error)
32var ErrOrigin = errors.New("hostfiles: object origin not admitted")
33var ErrManifestExists = errors.New("hostfiles: the target generation already has a manifest")
34var ErrSourceMoved = errors.New("hostfiles: the source manifest is not at the expected seq")
35```
36
37`prefix` must be a clean relative bucket key ending in `/`. Restore,
38ReadPlatform (and so LoadWake), Open and CopyManifest refuse any other
39prefix before reading anything.
40
41`origins` is the set of generation prefixes that the caller's pin admits.
42nil or empty means the manifest's own prefix only. The set is used exactly
43as passed: a caller that passes a set must include the manifest's own
44prefix if it wants objects published there. Each origin must be a clean
45relative bucket key ending in `/`. If one is not, the call fails before
46anything is read.
47
48What Restore guarantees: it validates every path and every published
49entry before it touches `dir`. If any entry fails, Restore writes and
50removes nothing. An entry's object must:
51
52- be a clean bucket key: relative, no `..`, no `.`, no `//`, no NUL;
53- be exactly `<origin>objects/<%016d Seq>-<SHA256>`, using that entry's
54 own Seq and SHA256, where origin is one of the admitted origins. A key
55 nested deeper under an origin, a key under another host's or another
56 generation's prefix, a relative key in the version 1 style, an absolute
57 key, or a wrong seq or sha is refused. If the key is well formed but its
58 origin is not admitted, the error wraps `ErrOrigin`;
59- hash and size-check as before (`checkEntry`, then `fetch`).
60
61ReadPlatform and LoadWake apply the same rules to platform items.
62
63CopyManifest reads `fromPrefix`'s manifest and validates every entry
64against `origins` (nil means `fromPrefix` alone). A source built by an
65earlier copy references earlier generations, so its caller passes the
66whole chain. CopyManifest then writes the same manifest (entries and Seq),
67with the same qualified references, to `toPrefix`'s manifest. The write is
68create-only. No objects are copied.
69
70`expectSeq` is the source revision the caller's pin admitted. When it is
71not 0 and the source is at any other Seq, the call returns
72`ErrSourceMoved` and writes nothing, on a first attempt or a retry. 0
73copies whatever the source holds.
74
75Errors:
76
77- an absent source is `os.ErrNotExist`, never an empty copy;
78- a source not at a non-zero `expectSeq` is `ErrSourceMoved`;
79- a target already holding exactly the manifest this call would write is
80 success, so a retry is safe;
81- any other target is `ErrManifestExists`, and the target is left as it
82 was;
83- a source equal to the target is refused.
84
85Retry-safety requires a source that cannot commit between attempts: the
86source generation must be fenced or frozen. The claim's admission provides
87this. Against a live source, a retry can find the target holding an older
88revision. It then reports `ErrManifestExists`, or `ErrSourceMoved` when
89`expectSeq` is set. It never reports success for a revision the caller
90did not admit.
91
92The new generation's `Open(toPrefix)` continues the sequence from the
93copy. An unchanged file keeps its old-generation reference. A changed file
94becomes a new object under `toPrefix`.
95
96For the claim's live set: the objects a manifest keeps alive are exactly
97the `Object` keys of its entries and platform items that are neither
98deleted nor skipped. Those keys may lie under earlier generations'
99prefixes.
100
101## 2. Files publish entry point (moves L17)
102
103Moves asked for `files.Publish(ctx, store, location, generation, dir,
104envPath) (manifestVersion string, err error)`. The closest stable call is:
105
106```go
107func Publish(ctx context.Context, s release.Store, prefix, dir string, skip func(rel string, isDir bool) bool) (int64, SyncStats, error)
108```
109
110- `prefix` is the generation's files prefix, `<location>files/<generation>/`.
111 The caller builds it.
112- It runs one `Open`, one `SyncDir` and one `Commit`, and returns the
113 committed manifest sequence number. That number is the manifest version.
114- On success, every regular file in `dir` that `skip` does not exclude is
115 in the manifest. A file larger than `MaxFileBytes` is recorded as
116 skipped and listed in `SyncStats.TooLarge`. Anything that is not a
117 regular file is left out and listed in `SyncStats.NotRegular`.
118- Publish is an install-time contract: it commits a complete and
119 consistent image or nothing. It returns an error and commits nothing
120 when any file could not be read (`SyncStats.Unreadable`), was replaced
121 between the look and the open (`SyncStats.Retry`), or changed while it
122 was read (`SyncStats.Torn`). The caller stops writers and calls Publish
123 again. Objects uploaded by a refused attempt are referenced by no
124 manifest.
125- A prefix that already has a manifest is continued, not replaced. A
126 repeat with nothing changed returns the same sequence number. If another
127 writer's manifest appears between Open and Commit, the error is
128 `ErrSuperseded`.
129- The env file is not part of this call. A caller that wants platform
130 items in the same revision uses `Open`, `StagePlatform`, `SyncDir` and
131 `Commit` directly.
132
133## 3. Eligible-boxes read (moves L15)
134
135New package `internal/boxchoice`. Console's unexported `eligibleBoxes` and
136`chooseBox` are now thin adapters over it, and their behaviour is
137unchanged. The predicate and choice tests moved to the new package.
138Console keeps tests that check its snapshot conversion carries every
139field.
140
141```go
142const Capability = "region-ha-1"
143type Headroom struct{ MemFreeMiB, DiskFreeMiB int64; Awake int }
144type Box struct {
145 Box, BoxID, EIP, Region, Placement string
146 HibernationConfigured, ServesSharedPool, AcceptsInbound *bool
147 SecretsKeyFP string
148 Capabilities []string
149 Draining bool
150 Headroom *Headroom
151 Fresh bool // not on the wire: the caller's liveness verdict
152}
153type Snapshot struct{ Boxes []Box; Complete bool }
154type RosterBox struct{ Placement string; Cloud boxid.Cloud }
155type Candidate struct{ BoxID, Region string; MemFreeMiB, DiskFreeMiB int64; Awake int }
156type Load struct{ Hosts int; Apps map[string]int }
157
158func Eligible(snap Snapshot, roster map[string]RosterBox, placement, awsRegion, homeName, expectedFP string, lost map[string]bool) []Candidate
159func Choose(host string, cands []Candidate, running map[string]Load, app string) (Candidate, bool)
160```
161
162`Box` uses the edges file's JSON tags, so an edges file unmarshals
163straight into it. The caller sets `Fresh` from the box's status snapshot.
164It sets `Snapshot.Complete` only when every listed edges file was read
165and every box's liveness is known.
166
167What Eligible guarantees:
168
169- It returns nothing when the snapshot is incomplete, `expectedFP` is
170 empty or `awsRegion` is empty.
171- It judges each box on that box's own evidence. Two partial boxes never
172 combine into one eligible box.
173- Identity and AWS region come from the roster. The edges file supplies
174 liveness and configuration.
175- The result is sorted by box id.
176
177What Choose guarantees: it prefers a box that does not run `app` (it never
178refuses on that), then the fewest hosts, then the greatest SHA-256 of
179`host + "\x00" + boxID`, then the smallest box id. The answer does not
180depend on the order of the candidates. It returns false only when there
181are no candidates.
182
183Moves' door can call `Eligible` then `Choose` in place of its "lexically
184first box" fallback.
185