carlos / platform Public

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

Plain git — no account needed to clone.

Download

Download this file

Region HA contracts for the claim and moves sessions

Branch region-ha-contracts, 2026-10-07. Three contracts that the claim session (L13) and the moves session (L15, L17 in 2026-10-06-moves-region-ha-conflicts.md) asked region HA for. All three are dormant: no production path calls them yet.

1. Origin-qualified object references (claim L13)

Package internal/hostfiles.

The manifest format is now version 2 (hostfiles.ManifestVersion = 2). Entry.Object is the object's full key from the bucket root:

instances/<host>/<inc>/files/<gen>/objects/<16-digit seq>-<64 hex sha256>

The publisher writes it under its own prefix. Readers fetch exactly that key and never join it to their own prefix. Version 1 held a key relative to the manifest's prefix. The field's meaning changed, so the version was bumped. Any manifest that is not version 2 (including 1) is refused by Load, Open, Restore and CopyManifest with "manifest <prefix> is version N; this binary reads 2". No version 1 manifest exists in production, so nothing needs migrating.

Signatures:

func Restore(ctx context.Context, s release.Store, prefix, dir string, origins []string) (Manifest, RestoreStats, error)
func ReadPlatform(ctx context.Context, s release.Store, prefix string, m Manifest, name string, origins []string) ([]byte, error)
func LoadWake(ctx context.Context, s release.Store, prefix string, m Manifest, origins []string) (WakeState, bool, error)
func CopyManifest(ctx context.Context, s release.Store, fromPrefix, toPrefix string, expectSeq int64, origins []string) (Manifest, error)
var ErrOrigin = errors.New("hostfiles: object origin not admitted")
var ErrManifestExists = errors.New("hostfiles: the target generation already has a manifest")
var ErrSourceMoved = errors.New("hostfiles: the source manifest is not at the expected seq")

prefix must be a clean relative bucket key ending in /. Restore, ReadPlatform (and so LoadWake), Open and CopyManifest refuse any other prefix before reading anything.

origins is the set of generation prefixes that the caller's pin admits. nil or empty means the manifest's own prefix only. The set is used exactly as passed: a caller that passes a set must include the manifest's own prefix if it wants objects published there. Each origin must be a clean relative bucket key ending in /. If one is not, the call fails before anything is read.

What Restore guarantees: it validates every path and every published entry before it touches dir. If any entry fails, Restore writes and removes nothing. An entry's object must:

  • be a clean bucket key: relative, no .., no ., no //, no NUL;
  • be exactly <origin>objects/<%016d Seq>-<SHA256>, using that entry's own Seq and SHA256, where origin is one of the admitted origins. A key nested deeper under an origin, a key under another host's or another generation's prefix, a relative key in the version 1 style, an absolute key, or a wrong seq or sha is refused. If the key is well formed but its origin is not admitted, the error wraps ErrOrigin;
  • hash and size-check as before (checkEntry, then fetch).

ReadPlatform and LoadWake apply the same rules to platform items.

CopyManifest reads fromPrefix's manifest and validates every entry against origins (nil means fromPrefix alone). A source built by an earlier copy references earlier generations, so its caller passes the whole chain. CopyManifest then writes the same manifest (entries and Seq), with the same qualified references, to toPrefix's manifest. The write is create-only. No objects are copied.

expectSeq is the source revision the caller's pin admitted. When it is not 0 and the source is at any other Seq, the call returns ErrSourceMoved and writes nothing, on a first attempt or a retry. 0 copies whatever the source holds.

Errors:

  • an absent source is os.ErrNotExist, never an empty copy;
  • a source not at a non-zero expectSeq is ErrSourceMoved;
  • a target already holding exactly the manifest this call would write is success, so a retry is safe;
  • any other target is ErrManifestExists, and the target is left as it was;
  • a source equal to the target is refused.

Retry-safety requires a source that cannot commit between attempts: the source generation must be fenced or frozen. The claim's admission provides this. Against a live source, a retry can find the target holding an older revision. It then reports ErrManifestExists, or ErrSourceMoved when expectSeq is set. It never reports success for a revision the caller did not admit.

The new generation's Open(toPrefix) continues the sequence from the copy. An unchanged file keeps its old-generation reference. A changed file becomes a new object under toPrefix.

For the claim's live set: the objects a manifest keeps alive are exactly the Object keys of its entries and platform items that are neither deleted nor skipped. Those keys may lie under earlier generations' prefixes.

2. Files publish entry point (moves L17)

Moves asked for files.Publish(ctx, store, location, generation, dir, envPath) (manifestVersion string, err error). The closest stable call is:

func Publish(ctx context.Context, s release.Store, prefix, dir string, skip func(rel string, isDir bool) bool) (int64, SyncStats, error)
  • prefix is the generation's files prefix, <location>files/<generation>/. The caller builds it.
  • It runs one Open, one SyncDir and one Commit, and returns the committed manifest sequence number. That number is the manifest version.
  • On success, every regular file in dir that skip does not exclude is in the manifest. A file larger than MaxFileBytes is recorded as skipped and listed in SyncStats.TooLarge. Anything that is not a regular file is left out and listed in SyncStats.NotRegular.
  • Publish is an install-time contract: it commits a complete and consistent image or nothing. It returns an error and commits nothing when any file could not be read (SyncStats.Unreadable), was replaced between the look and the open (SyncStats.Retry), or changed while it was read (SyncStats.Torn). The caller stops writers and calls Publish again. Objects uploaded by a refused attempt are referenced by no manifest.
  • A prefix that already has a manifest is continued, not replaced. A repeat with nothing changed returns the same sequence number. If another writer's manifest appears between Open and Commit, the error is ErrSuperseded.
  • The env file is not part of this call. A caller that wants platform items in the same revision uses Open, StagePlatform, SyncDir and Commit directly.

3. Eligible-boxes read (moves L15)

New package internal/boxchoice. Console's unexported eligibleBoxes and chooseBox are now thin adapters over it, and their behaviour is unchanged. The predicate and choice tests moved to the new package. Console keeps tests that check its snapshot conversion carries every field.

const Capability = "region-ha-1"
type Headroom struct{ MemFreeMiB, DiskFreeMiB int64; Awake int }
type Box struct {
	Box, BoxID, EIP, Region, Placement string
	HibernationConfigured, ServesSharedPool, AcceptsInbound *bool
	SecretsKeyFP string
	Capabilities []string
	Draining     bool
	Headroom     *Headroom
	Fresh        bool // not on the wire: the caller's liveness verdict
}
type Snapshot struct{ Boxes []Box; Complete bool }
type RosterBox struct{ Placement string; Cloud boxid.Cloud }
type Candidate struct{ BoxID, Region string; MemFreeMiB, DiskFreeMiB int64; Awake int }
type Load struct{ Hosts int; Apps map[string]int }

func Eligible(snap Snapshot, roster map[string]RosterBox, placement, awsRegion, homeName, expectedFP string, lost map[string]bool) []Candidate
func Choose(host string, cands []Candidate, running map[string]Load, app string) (Candidate, bool)

Box uses the edges file's JSON tags, so an edges file unmarshals straight into it. The caller sets Fresh from the box's status snapshot. It sets Snapshot.Complete only when every listed edges file was read and every box's liveness is known.

What Eligible guarantees:

  • It returns nothing when the snapshot is incomplete, expectedFP is empty or awsRegion is empty.
  • It judges each box on that box's own evidence. Two partial boxes never combine into one eligible box.
  • Identity and AWS region come from the roster. The edges file supplies liveness and configuration.
  • The result is sorted by box id.

What Choose guarantees: it prefers a box that does not run app (it never refuses on that), then the fewest hosts, then the greatest SHA-256 of host + "\x00" + boxID, then the smallest box id. The answer does not depend on the order of the candidates. It returns false only when there are no candidates.

Moves' door can call Eligible then Choose in place of its "lexically first box" fallback.