moves: captured env pins live with their wake (home part 2, 2b r5)
captureEnvPin kept an entry for every host ever selected. Each entry now lives as long as its wake: Materialize holds it under the activator start loop's context, which the loop cancels when the wake ends (completed, terminally failed or abandoned), and the end drops it unless a later capture replaced it. Retries of the same launch run under the same context and still find it. An entry whose wake never reaches Materialize is pruned at the wake's deadline (envPinHold when the policy names none), on every capture and lookup, so nothing outlives its wake or the move. TestCapturedEnvPinLivesWithItsWake: gone after a completed wake and a failed one, found by a retry, pruned past the deadline, and an earlier wake's end keeps a later capture. Census: three probe owner rows re-frozen at main's dispositions, new seal; ruling owed.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
4 files changed,
+216
−21
internal/host/movesenvpin_lifetime_test.go+119 −0internal/host/movesprobeowners.go+88 −13internal/provision/plan14_root_reads_test.go+1 −1internal/provision/testdata/plan14-root-reads-home-part2.tsv+8 −7
CI passed — run details
CI log
=== gate ===
cd launchctx && go vet ./... && go test ./... -count=1
ok amadan.net/carlos/platform.git/launchctx 0.011s
? amadan.net/carlos/platform.git/launchctx/controlname [no test files]
? amadan.net/carlos/platform.git/launchctx/wireid [no test files]
go vet ./...
gofmt -l .: clean
go test ./... -count=1 -timeout 30m
ok github.com/carlosframework/platform 279.429s
? github.com/carlosframework/platform/cmd/changelog [no test files]
ok github.com/carlosframework/platform/cmd/console 2.615s
ok github.com/carlosframework/platform/cmd/moveprobe 92.211s
ok github.com/carlosframework/platform/cmd/zddprobe 0.707s
ok github.com/carlosframework/platform/internal/activator 248.259s
ok github.com/carlosframework/platform/internal/agentsock 0.022s
ok github.com/carlosframework/platform/internal/alerting 0.003s
ok github.com/carlosframework/platform/internal/analytics 0.022s
ok github.com/carlosframework/platform/internal/appconfig 2.408s
ok github.com/carlosframework/platform/internal/apperr 0.072s
ok github.com/carlosframework/platform/internal/applog 0.130s
ok github.com/carlosframework/platform/internal/awsinv 0.008s
ok github.com/carlosframework/platform/internal/boxchoice 0.008s
ok github.com/carlosframework/platform/internal/boxid 0.561s
? github.com/carlosframework/platform/internal/boxid/boxidtest [no test files]
? github.com/carlosframework/platform/internal/boxid/internal/signtest [no test files]
ok github.com/carlosframework/platform/internal/boxvolumes 0.010s
ok github.com/carlosframework/platform/internal/caps 0.011s
ok github.com/carlosframework/platform/internal/certwarm 0.002s
ok github.com/carlosframework/platform/internal/cgroup 0.694s
ok github.com/carlosframework/platform/internal/changelog 0.304s
ok github.com/carlosframework/platform/internal/clamd 0.055s
? github.com/carlosframework/platform/internal/cliflag [no test files]
ok github.com/carlosframework/platform/internal/console 386.620s
? github.com/carlosframework/platform/internal/controlname [no test files]
? github.com/carlosframework/platform/internal/controlsock [no test files]
ok github.com/carlosframework/platform/internal/dbdoor 5.786s
ok github.com/carlosframework/platform/internal/dbdoor/dbdoortest 0.011s
ok github.com/carlosframework/platform/internal/dbverify 0.005s
ok github.com/carlosframework/platform/internal/dnsrec 0.010s
ok github.com/carlosframework/platform/internal/docsite 0.005s
ok github.com/carlosframework/platform/internal/domains 2.339s
ok github.com/carlosframework/platform/internal/doorkey 0.006s
ok github.com/carlosframework/platform/internal/economics 0.004s
ok github.com/carlosframework/platform/internal/edge 26.725s
? github.com/carlosframework/platform/internal/edgeproof [no test files]
ok github.com/carlosframework/platform/internal/edgesfile 0.008s
ok github.com/carlosframework/platform/internal/fleetapi 0.131s
ok github.com/carlosframework/platform/internal/gate 1.657s
ok github.com/carlosframework/platform/internal/genselect 0.015s
ok github.com/carlosframework/platform/internal/geo 0.012s
ok github.com/carlosframework/platform/internal/geoip 0.038s
ok github.com/carlosframework/platform/internal/host 133.584s
ok github.com/carlosframework/platform/internal/hostactors 0.471s
ok github.com/carlosframework/platform/internal/hostbox 2.221s
ok github.com/carlosframework/platform/internal/hostclaim 0.504s
ok github.com/carlosframework/platform/internal/hostevidence 0.015s
ok github.com/carlosframework/platform/internal/hostexec 4.696s
ok github.com/carlosframework/platform/internal/hostfence 0.034s
ok github.com/carlosframework/platform/internal/hostfiles 10.474s
ok github.com/carlosframework/platform/internal/hostfloor 0.015s
ok github.com/carlosframework/platform/internal/hostguard 1.006s
ok github.com/carlosframework/platform/internal/hostops 1.521s
? github.com/carlosframework/platform/internal/hostops/fencerelease [no test files]
ok github.com/carlosframework/platform/internal/hostops/internal/fencerelease 0.013s
ok github.com/carlosframework/platform/internal/hostquarantine 0.015s
ok github.com/carlosframework/platform/internal/hostrelocate 0.434s
ok github.com/carlosframework/platform/internal/hostresolve 0.150s
ok github.com/carlosframework/platform/internal/hostwrite 0.015s
ok github.com/carlosframework/platform/internal/infraview 0.015s
ok github.com/carlosframework/platform/internal/instancectl 4.207s
ok github.com/carlosframework/platform/internal/instanceexec 2.696s
ok github.com/carlosframework/platform/internal/instancefs 0.017s
ok github.com/carlosframework/platform/internal/instancekey 0.015s
ok github.com/carlosframework/platform/internal/instanceupdates 0.022s
ok github.com/carlosframework/platform/internal/layout 1.488s
ok github.com/carlosframework/platform/internal/lease 9.816s
ok github.com/carlosframework/platform/internal/lease/leasetest 0.020s
ok github.com/carlosframework/platform/internal/ledger 0.027s
ok github.com/carlosframework/platform/internal/mail 0.179s
ok github.com/carlosframework/platform/internal/maintenance 0.091s
ok github.com/carlosframework/platform/internal/maintenanceexec 5.489s
ok github.com/carlosframework/platform/internal/maintenanceguard 0.070s
ok github.com/carlosframework/platform/internal/metrics 0.005s
ok github.com/carlosframework/platform/internal/moves 20.926s
ok github.com/carlosframework/platform/internal/moves/archive 0.740s
ok github.com/carlosframework/platform/internal/objstore 0.184s
ok github.com/carlosframework/platform/internal/ops 0.177s
ok github.com/carlosframework/platform/internal/park 0.200s
ok github.com/carlosframework/platform/internal/privatefile 0.003s
ok github.com/carlosframework/platform/internal/provision 14.096s
ok github.com/carlosframework/platform/internal/publicrelease 0.007s
? github.com/carlosframework/platform/internal/regionname [no test files]
ok github.com/carlosframework/platform/internal/registry 4.856s
ok github.com/carlosframework/platform/internal/relay 7.233s
ok github.com/carlosframework/platform/internal/release 19.603s
ok github.com/carlosframework/platform/internal/rollout 3.622s
ok github.com/carlosframework/platform/internal/rolloutwords 0.003s
ok github.com/carlosframework/platform/internal/rootaws 0.006s
ok github.com/carlosframework/platform/internal/rootparticipation 0.006s
ok github.com/carlosframework/platform/internal/rootstate 0.024s
ok github.com/carlosframework/platform/internal/runs 0.569s
ok github.com/carlosframework/platform/internal/s3compat 0.005s
ok github.com/carlosframework/platform/internal/safefile 0.004s
ok github.com/carlosframework/platform/internal/schedule 2.457s
ok github.com/carlosframework/platform/internal/seeds 0.012s
ok github.com/carlosframework/platform/internal/sim/hostlife 8.674s
ok github.com/carlosframework/platform/internal/sim/litestreamfacts 3.624s
ok github.com/carlosframework/platform/internal/status 0.040s
ok github.com/carlosframework/platform/internal/steering 0.005s
ok github.com/carlosframework/platform/internal/stopevidence 0.280s
ok github.com/carlosframework/platform/internal/svcrole 0.002s
ok github.com/carlosframework/platform/internal/telemetry 0.004s
ok github.com/carlosframework/platform/internal/termfence 0.004s
ok github.com/carlosframework/platform/internal/tunnelclient 0.004s
ok github.com/carlosframework/platform/internal/unixdial 0.002s
ok github.com/carlosframework/platform/internal/updatewords 0.004s
ok github.com/carlosframework/platform/internal/usage 0.004s
ok github.com/carlosframework/platform/internal/version 0.001s
ok github.com/carlosframework/platform/internal/vet 1.549s
=== cross ===
--- shard 0 of 5 ---
cross-compiling darwin/arm64
--- shard 1 of 5 ---
cross-compiling darwin/amd64
--- shard 2 of 5 ---
cross-compiling linux/arm64
linux/arm64 carlos binary: 46399650 bytes (ceiling 52000000)
--- shard 3 of 5 ---
cross-compiling windows/amd64
--- shard 4 of 5 ---
cross-compiling windows/arm64
=== minio-contract ===
minio contract: arm64, minio RELEASE.2025-09-07T16-13-09Z, mc RELEASE.2025-08-13T08-35-41Z
minio: cached, digest verified
mc: cached, digest verified
minio healthy on 127.0.0.1:20999
Added `local` successfully.
Bucket created successfully `local/contract-bucket`.
Bucket created successfully `local/denied-bucket`.
Created policy `contract-only` successfully.
Added user `scopeduser` successfully.
Attached Policies: [contract-only]
To User: scopeduser
buckets and scoped user ready
=== RUN TestDeliveredContractMinio
=== RUN TestDeliveredContractMinio/put
=== RUN TestDeliveredContractMinio/get_round-trips_the_body
=== RUN TestDeliveredContractMinio/list_shows_the_key
=== RUN TestDeliveredContractMinio/head_and_list_agree_on_the_version
=== RUN TestDeliveredContractMinio/the_app_cannot_tag_its_own_object
=== RUN TestDeliveredContractMinio/the_app_cannot_tag_its_own_object/not_by_PutObjectTagging
=== RUN TestDeliveredContractMinio/the_app_cannot_tag_its_own_object/and_not_by_x-amz-tagging_on_upload
=== RUN TestDeliveredContractMinio/the_app_cannot_tag_by_any_other_route
=== RUN TestDeliveredContractMinio/the_app_cannot_tag_by_any_other_route/not_by_CreateMultipartUpload
=== RUN TestDeliveredContractMinio/the_app_cannot_tag_by_any_other_route/not_by_CopyObject_with_a_REPLACE_tagging_directive
=== RUN TestDeliveredContractMinio/the_app_cannot_tag_by_any_other_route/but_multipart_itself_is_allowed
=== RUN TestDeliveredContractMinio/the_app_cannot_tag_by_any_other_route/and_copy_itself_is_allowed
=== RUN TestDeliveredContractMinio/the_app_cannot_tag_by_any_other_route/a_POST-policy_forged_tag_with_a_matching_version_is_still_not_a_verdict
=== RUN TestDeliveredContractMinio/the_app_cannot_tag_by_any_other_route/and_an_ordinary_tagged-with-something-else_upload_is_allowed
=== RUN TestDeliveredContractMinio/delete_removes_it
=== RUN TestDeliveredContractMinio/cross-bucket_denial
--- PASS: TestDeliveredContractMinio (0.02s)
--- PASS: TestDeliveredContractMinio/put (0.00s)
--- PASS: TestDeliveredContractMinio/get_round-trips_the_body (0.00s)
--- PASS: TestDeliveredContractMinio/list_shows_the_key (0.00s)
--- PASS: TestDeliveredContractMinio/head_and_list_agree_on_the_version (0.00s)
--- PASS: TestDeliveredContractMinio/the_app_cannot_tag_its_own_object (0.00s)
--- PASS: TestDeliveredContractMinio/the_app_cannot_tag_its_own_object/not_by_PutObjectTagging (0.00s)
--- PASS: TestDeliveredContractMinio/the_app_cannot_tag_its_own_object/and_not_by_x-amz-tagging_on_upload (0.00s)
--- PASS: TestDeliveredContractMinio/the_app_cannot_tag_by_any_other_route (0.01s)
--- PASS: TestDeliveredContractMinio/the_app_cannot_tag_by_any_other_route/not_by_CreateMultipartUpload (0.00s)
--- PASS: TestDeliveredContractMinio/the_app_cannot_tag_by_any_other_route/not_by_CopyObject_with_a_REPLACE_tagging_directive (0.00s)
--- PASS: TestDeliveredContractMinio/the_app_cannot_tag_by_any_other_route/but_multipart_itself_is_allowed (0.00s)
--- PASS: TestDeliveredContractMinio/the_app_cannot_tag_by_any_other_route/and_copy_itself_is_allowed (0.00s)
--- PASS: TestDeliveredContractMinio/the_app_cannot_tag_by_any_other_route/a_POST-policy_forged_tag_with_a_matching_version_is_still_not_a_verdict (0.00s)
--- PASS: TestDeliveredContractMinio/the_app_cannot_tag_by_any_other_route/and_an_ordinary_tagged-with-something-else_upload_is_allowed (0.00s)
--- PASS: TestDeliveredContractMinio/delete_removes_it (0.00s)
--- PASS: TestDeliveredContractMinio/cross-bucket_denial (0.00s)
PASS
ok github.com/carlosframework/platform/internal/objstore 0.021s
=== RUN TestT4StorageProbeMinio
2026/10/15 18:02:24 moves: T4: STORE: list: operation error S3: ListObjectsV2, https response error StatusCode: 403, RequestID: 18DEC5F1CD123C7C, HostID: 7c420053e0fbcd7f41653fdd92ffa2802c903d05303b1aa15adc2e8bb65d183f, api error SignatureDoesNotMatch: The request signature we calculated does not match the signature you provided. Check your key and signing method.
--- PASS: TestT4StorageProbeMinio (0.00s)
PASS
ok github.com/carlosframework/platform/internal/host 0.019s
minio contract: PASS
diff --git a/internal/host/movesenvpin_lifetime_test.go b/internal/host/movesenvpin_lifetime_test.go| new file mode 100644 |
| index 000000000..beeed9481 |
| --- /dev/null |
| +++ b/internal/host/movesenvpin_lifetime_test.go |
| @@ -0,0 +1,119 @@ |
| +package host |
| + |
| +import ( |
| + "context" |
| + "testing" |
| + "time" |
| +) |
| + |
| +// The probe owners' captured env pins live as long as their wake (home part |
| +// 2, Task 2b fix round 5). Select captures a pin (in Prerequisites, and |
| +// again in Admit); every start attempt of the wake materializes under the |
| +// activator start loop's context, which it cancels when the wake ends: |
| +// completed, terminally failed or abandoned. A retry of the same launch |
| +// finds the pin; the end of the wake drops it; a wake that never reaches |
| +// Materialize leaves nothing past its deadline. |
| + |
| +func (f *poFixture) hasEnvPin() bool { |
| + f.owners.mu.Lock() |
| + defer f.owners.mu.Unlock() |
| + _, ok := f.owners.envPins[poHost] |
| + return ok |
| +} |
| + |
| +// envPinGone waits for the wake's end to drop the pin (context.AfterFunc |
| +// runs in its own goroutine). |
| +func (f *poFixture) envPinGone(t *testing.T, why string) { |
| + t.Helper() |
| + for deadline := time.Now().Add(2 * time.Second); time.Now().Before(deadline); time.Sleep(5 * time.Millisecond) { |
| + if !f.hasEnvPin() { |
| + return |
| + } |
| + } |
| + t.Fatalf("%s: the captured pin outlived its wake", why) |
| +} |
| + |
| +func TestCapturedEnvPinLivesWithItsWake(t *testing.T) { |
| + t.Run("a completed wake, with a retry", func(t *testing.T) { |
| + f := newPOFixture(t) |
| + o := f.read() |
| + in, err := f.owners.Select(t.Context(), f.route, o, poProbe) |
| + if err != nil { |
| + t.Fatal(err) |
| + } |
| + admitted := f.admit(o, in) |
| + wake, end := context.WithCancel(t.Context()) |
| + for attempt := 0; attempt < 2; attempt++ { |
| + // the second is a retry of the same launch: it finds the pin |
| + if err := f.owners.Materialize(wake, admitted, in, poWork{action: "spawn", f: f}); err != nil { |
| + t.Fatalf("attempt %d: %v", attempt, err) |
| + } |
| + if !f.hasEnvPin() { |
| + t.Fatalf("attempt %d: the pin is gone while its wake runs", attempt) |
| + } |
| + } |
| + end() // the start loop returns: the wake completed |
| + f.envPinGone(t, "a completed wake") |
| + }) |
| + t.Run("a failed wake", func(t *testing.T) { |
| + f := newPOFixture(t) |
| + o := f.read() |
| + in, err := f.owners.Select(t.Context(), f.route, o, poProbe) |
| + if err != nil { |
| + t.Fatal(err) |
| + } |
| + admitted := f.admit(o, in) |
| + // The held render no longer matches the pins: every attempt fails. |
| + f.renderOverlay = "changed" |
| + wake, end := context.WithCancel(t.Context()) |
| + for attempt := 0; attempt < 2; attempt++ { |
| + if err := f.owners.Materialize(wake, admitted, in, poWork{action: "spawn", f: f}); err == nil { |
| + t.Fatalf("attempt %d materialized changed inputs", attempt) |
| + } |
| + if !f.hasEnvPin() { |
| + t.Fatalf("attempt %d: the pin is gone before the wake ends", attempt) |
| + } |
| + } |
| + end() // the start loop gives up: the wake failed |
| + f.envPinGone(t, "a failed wake") |
| + }) |
| + t.Run("a wake abandoned before Materialize", func(t *testing.T) { |
| + f := newPOFixture(t) |
| + now := f.owners.Now() |
| + policy := poProbe |
| + policy.Deadline = now.Add(time.Minute) |
| + if _, err := f.owners.Select(t.Context(), f.route, f.read(), policy); err != nil { |
| + t.Fatal(err) |
| + } |
| + if !f.hasEnvPin() { |
| + t.Fatal("Select captured no pin") |
| + } |
| + f.owners.Now = func() time.Time { return now.Add(time.Minute) } |
| + if _, ok := f.owners.capturedPin(poHost, nil); ok || f.hasEnvPin() { |
| + t.Fatal("a pin past its wake's deadline is still kept") |
| + } |
| + }) |
| + t.Run("an earlier wake's end keeps a later capture", func(t *testing.T) { |
| + f := newPOFixture(t) |
| + o := f.read() |
| + in, err := f.owners.Select(t.Context(), f.route, o, poProbe) |
| + if err != nil { |
| + t.Fatal(err) |
| + } |
| + admitted := f.admit(o, in) |
| + wake, end := context.WithCancel(t.Context()) |
| + if err := f.owners.Materialize(wake, admitted, in, poWork{action: "spawn", f: f}); err != nil { |
| + t.Fatal(err) |
| + } |
| + // a later wake of the same host captures again before the first |
| + // one's end is observed |
| + if _, err := f.owners.Select(t.Context(), f.route, admitted, poProbe); err != nil { |
| + t.Fatal(err) |
| + } |
| + end() |
| + time.Sleep(50 * time.Millisecond) |
| + if !f.hasEnvPin() { |
| + t.Fatal("the end of an earlier wake dropped a later capture") |
| + } |
| + }) |
| +} |
diff --git a/internal/host/movesprobeowners.go b/internal/host/movesprobeowners.go| index 1fec01d19..e15008be5 100644 |
| --- a/internal/host/movesprobeowners.go |
| +++ b/internal/host/movesprobeowners.go |
| @@ -160,8 +160,10 @@ type movesProbeOwners struct { |
| // envPins are the move-portable pins Select captured, by host, each |
| // bound to the exact launch environment it was computed for |
| // (captureEnvPin): what Materialize checks the same inputs against. |
| - // Memory only, never persisted, never digested by the claim. |
| - envPins map[string]capturedEnvPin |
| + // Memory only, never persisted, never digested by the claim. Each lives |
| + // as long as its wake (holdEnvPin) and never past its deadline. |
| + envPins map[string]capturedEnvPin |
| + envPinSeq uint64 |
| // fences are the admitted spawn selections Materialize ran under, by |
| // host and session: the live fence for the session's marker writes. |
| fences map[string]hostwrite.Work |
| @@ -342,8 +344,20 @@ var probeOnlyVars = map[string]bool{"CARLOS_MOVE_PROBE": true} |
| // capturedEnvPin is a launch environment's move-portable pin as Select |
| // computed it at capture (Launch's report), bound to that environment by |
| -// its fingerprint. |
| -type capturedEnvPin struct{ env, pin string } |
| +// its fingerprint. seq names this capture, so the end of an earlier wake |
| +// never drops a later one; until is the wake's deadline, after which the |
| +// entry is gone whether or not the wake ever reached Materialize; held is |
| +// set once its wake's start loop holds it (holdEnvPin). |
| +type capturedEnvPin struct { |
| + env, pin string |
| + seq uint64 |
| + until time.Time |
| + held bool |
| +} |
| + |
| +// envPinHold bounds a captured pin whose wake names no deadline (the |
| +// committed wake's policy). |
| +const envPinHold = 15 * time.Minute |
| // envFingerprint identifies a launch environment exactly: every entry, in |
| // order. |
| @@ -353,22 +367,50 @@ func envFingerprint(env []string) string { |
| } |
| // captureEnvPin records the pin Select computed for the inputs it returns |
| -// (env is ClaimLaunchInputs.Env), replacing any earlier one for host. |
| -func (m *movesProbeOwners) captureEnvPin(host string, env []string, pin string) { |
| +// (env is ClaimLaunchInputs.Env), replacing any earlier one for host. The |
| +// entry lasts until its wake ends (holdEnvPin) and never past deadline (the |
| +// wake's StartPolicy.Deadline, or envPinHold when it names none): a wake |
| +// abandoned before Materialize leaves nothing behind longer than that. |
| +// Every capture first drops the expired entries. |
| +func (m *movesProbeOwners) captureEnvPin(host string, env []string, pin string, deadline time.Time) { |
| + now := m.now() |
| + if deadline.IsZero() { |
| + deadline = now.Add(envPinHold) |
| + } |
| m.mu.Lock() |
| defer m.mu.Unlock() |
| if m.envPins == nil { |
| m.envPins = map[string]capturedEnvPin{} |
| } |
| - m.envPins[host] = capturedEnvPin{env: envFingerprint(env), pin: pin} |
| + m.pruneEnvPins(now) |
| + m.envPinSeq++ |
| + m.envPins[host] = capturedEnvPin{env: envFingerprint(env), pin: pin, seq: m.envPinSeq, until: deadline} |
| +} |
| + |
| +// now is the owners' clock, the wall clock when none is set. |
| +func (m *movesProbeOwners) now() time.Time { |
| + if m.Now == nil { |
| + return time.Now() |
| + } |
| + return m.Now() |
| +} |
| + |
| +// pruneEnvPins drops the entries past their deadline. m.mu is held. |
| +func (m *movesProbeOwners) pruneEnvPins(now time.Time) { |
| + for host, c := range m.envPins { |
| + if !now.Before(c.until) { |
| + delete(m.envPins, host) |
| + } |
| + } |
| } |
| // capturedPin is the pin Select captured for exactly env on host; false |
| -// when Select captured none for it in this process (Materialize then |
| -// refuses, fail closed). |
| +// when Select captured none for it in this process, or it has expired |
| +// (Materialize then refuses, fail closed). |
| func (m *movesProbeOwners) capturedPin(host string, env []string) (string, bool) { |
| m.mu.Lock() |
| defer m.mu.Unlock() |
| + m.pruneEnvPins(m.now()) |
| c, ok := m.envPins[host] |
| if !ok || c.env != envFingerprint(env) { |
| return "", false |
| @@ -376,6 +418,35 @@ func (m *movesProbeOwners) capturedPin(host string, env []string) (string, bool) |
| return c.pin, true |
| } |
| +// holdEnvPin ties host's captured pin to its wake: ctx is the context the |
| +// activator's start loop passes Materialize, which it cancels when the |
| +// wake ends, completed, failed or abandoned. Every retry of the same |
| +// launch runs under the same context and finds the entry; the end of the |
| +// wake drops it, and never a later capture for the same host (seq). Held |
| +// once per capture. |
| +func (m *movesProbeOwners) holdEnvPin(ctx context.Context, host string) { |
| + m.mu.Lock() |
| + c, ok := m.envPins[host] |
| + if !ok || c.held { |
| + m.mu.Unlock() |
| + return |
| + } |
| + c.held = true |
| + m.envPins[host] = c |
| + m.mu.Unlock() |
| + seq := c.seq |
| + context.AfterFunc(ctx, func() { m.dropEnvPin(host, seq) }) |
| +} |
| + |
| +// dropEnvPin removes host's captured pin if it is still capture seq. |
| +func (m *movesProbeOwners) dropEnvPin(host string, seq uint64) { |
| + m.mu.Lock() |
| + defer m.mu.Unlock() |
| + if c, ok := m.envPins[host]; ok && c.seq == seq { |
| + delete(m.envPins, host) |
| + } |
| +} |
| + |
| // localEnvDigest is the digest of the whole launch environment, every |
| // variable but the probe-only ones (moves.LocalDigest): the recovery's |
| // check against inputs this same box recorded just before the wake (fix |
| @@ -602,7 +673,7 @@ func (m *movesProbeOwners) Select(ctx context.Context, rt registry.Route, o host |
| if p.Probe || p.Prepared != nil || p.Inputs != nil { |
| return ClaimLaunchInputs{}, hostbox.ErrUnsupported |
| } |
| - return m.selectCommitted(ctx, rt, o, v) |
| + return m.selectCommitted(ctx, rt, o, v, p.Deadline) |
| } |
| if !p.Probe { |
| return ClaimLaunchInputs{}, hostbox.ErrUnsupported |
| @@ -673,7 +744,7 @@ func (m *movesProbeOwners) Select(ctx context.Context, rt registry.Route, o host |
| in := ClaimLaunchInputs{Roles: []string{"main"}, Identity: launchIdentity(pins), Baseline: plan, Admission: admission, |
| Executable: l.Executable, ExecutableSHA256: l.ExecutableSHA256, Args: append([]string(nil), l.Args...), Env: probeEnv(l.Env), |
| DBPath: rt.DBPath, Version: l.Version, Staged: l.Staged, Limit: l.Limit, Prepared: prepared, ReadyPath: ready} |
| - m.captureEnvPin(rt.Host, in.Env, report.Digest) |
| + m.captureEnvPin(rt.Host, in.Env, report.Digest, p.Deadline) |
| return in, nil |
| } |
| @@ -788,6 +859,10 @@ func (m *movesProbeOwners) Materialize(ctx context.Context, o hostclaim.Observed |
| if recoveryKind(o) { |
| return m.recovery().Materialize(ctx, o, in, work) |
| } |
| + // The captured pin lives as long as this wake: every start attempt |
| + // materializes under the start loop's context, which ends with the |
| + // wake, completed, failed or abandoned. |
| + m.holdEnvPin(ctx, o.Host) |
| v, err := m.landing(ctx, o) |
| if err != nil { |
| return err |
| @@ -1454,7 +1529,7 @@ func (m *movesProbeOwners) committedAdmission(o hostclaim.Observed, v probeLandi |
| NewGeneration: gen, KeepStamp: h.Stamp > h.Session, Region: m.Region, PredecessorRegion: predecessor, RemoteMaxTXID: plan.MaxTXID}, nil |
| } |
| -func (m *movesProbeOwners) selectCommitted(ctx context.Context, rt registry.Route, o hostclaim.Observed, v probeLanding) (ClaimLaunchInputs, error) { |
| +func (m *movesProbeOwners) selectCommitted(ctx context.Context, rt registry.Route, o hostclaim.Observed, v probeLanding, deadline time.Time) (ClaimLaunchInputs, error) { |
| if err := committedRoute(rt, o.Host, v.op); err != nil { |
| return ClaimLaunchInputs{}, err |
| } |
| @@ -1491,7 +1566,7 @@ func (m *movesProbeOwners) selectCommitted(ctx context.Context, rt registry.Rout |
| in := ClaimLaunchInputs{Roles: []string{"main"}, Identity: id, Baseline: plan, Admission: admission, |
| Executable: l.Executable, ExecutableSHA256: l.ExecutableSHA256, Args: append([]string(nil), l.Args...), Env: withoutProbeVars(l.Env), |
| DBPath: rt.DBPath, Version: l.Version, Staged: l.Staged, Limit: l.Limit} |
| - m.captureEnvPin(rt.Host, in.Env, report.Digest) |
| + m.captureEnvPin(rt.Host, in.Env, report.Digest, deadline) |
| return in, nil |
| } |
diff --git a/internal/provision/plan14_root_reads_test.go b/internal/provision/plan14_root_reads_test.go| index 2943c317c..f45ad18eb 100644 |
| --- a/internal/provision/plan14_root_reads_test.go |
| +++ b/internal/provision/plan14_root_reads_test.go |
| @@ -750,7 +750,7 @@ func TestPlan14RootReadCensus(t *testing.T) { |
| if err = loadRootReadInventory(filepath.Join(root, "internal/provision/testdata/plan14-root-reads-home-part2.tsv"), &inv, true); err != nil { |
| t.Fatal(err) |
| } |
| - seal = "351eba46c02a820d32e087490114f82c1bfdfc1d432d136b8bce271da268ac72" |
| + seal = "4332d54efb67cd04114b6c52f11cdac5c28b554dc56860d3cb7c91167343efc5" |
| errs := append(compareRootReads(got, inv), checkRootReadLegacy(inv, seal)...) |
| errs = append(errs, checkRootReadDeclarations(got, inv)...) |
| if len(errs) > 0 { |
diff --git a/internal/provision/testdata/plan14-root-reads-home-part2.tsv b/internal/provision/testdata/plan14-root-reads-home-part2.tsv| index bc1c9fe82..1e3d5358b 100644 |
| --- a/internal/provision/testdata/plan14-root-reads-home-part2.tsv |
| +++ b/internal/provision/testdata/plan14-root-reads-home-part2.tsv |
| @@ -2,6 +2,7 @@ |
| # Task 1: moves.CompareEnv takes targetHome, and T3 (internal/host checkEnv) passes rec.Move.To == "" for it: a home target must set no CARLOS_REGION and fails region-present when it does. Task 2b: internal/moves owns the move-portable env rule once: a name is left out of the pin only when it is box-owned (AgentOwnedVars, BoxVars) and the instance's own layers did not supply its running value (Pinned); ReportLaunch and PortableDigest make the v2 report and pin (EnvReportVersion, "env2:sha256:"), and LocalDigest the whole env for a check made on the box that recorded it. The composer reports the instance's layer names (activator ExecBackend.LaunchLayers, LaunchEnvironment's composition moved into its own unit), never a name the agent decides (agentOwnedNames). The moves launch source (claimMoveLaunchSource) makes the env report from that one composition and returns it with the launch; the probe owners take its digest as the launch's pin (movesReadLaunch passes it through) and keep it in memory for Materialize, bound to the exact env. No layer names cross into the claim: ClaimServingLaunch, ClaimLaunchInputs and ClaimLaunchDigest are main's. T3 refuses a v1 source report and the probe owners a v1 pin, fail closed. The recovery records recoveryInputs and checks localEnvDigest, the whole local env. |
| # Env digests, by where they are compared. Across boxes, the move-portable v2 pin: the source pin writeSourceReport sr.Env.Digest (host/movesphase1.go) copied to the target report's Inputs.EnvReport (host/movesreport.go) and checked on the target against the launch's own pin, the digest of the env report claimMoveLaunchSource makes from the same composition as the launch's env (host/claimmoveaccess.go, passed through by movesReadLaunch, host/moveswakesource.go): the probe owners' Select and the committed wake's selectCommitted compare it, and Materialize and materializeCommitted check the pin Select captured in memory for exactly that env (host/movesprobeowners.go captureEnvPin); the cut's currentInputs (host/movescut.go), compared on the source with that pin. Same box, the whole local env: the recovery's recoveryInputs (host/movescut.go, recorded through host/movesrecoveryprobe.go:139-146 on the recovering box) checked by the recovery owners' Select and Materialize (host/movesrecoveryowners.go localEnvDigest). Same box, main's full composed env, unchanged: ClaimLaunchDigest (host/claimoperations.go, byte for byte main's; it is root's LaunchSpec hash), compared only with the attempt's own box, boot and controller (host/movesprobeattempts.go, host/relocationattempts.go, host/claimlaunch_authority.go, host/claimwakestop.go, activator/servingretry.go, activator/rolelaunch_linux.go, activator/rolelaunch_root_linux.go); snapshot.Identity.Config (host/claimmoveaccess.go), which the moves owners never read (they set their own identity from the pins). |
| # Re-frozen units, every one at main's disposition (class, task and basis unchanged; kinds and digest refreshed); these rows replace this overlay's earlier, unruled rows. Authority rows, read set unchanged (only source offsets moved): host/claimmoveaccess.go claimMoveLaunchSource (authority C,G,J1; it calls LaunchLayers and reports the env from that composition) and host/movespass.go armPhase1 (authority B2,C; its LaunchEnv hook returns the layer names). Legacy rows: activator ExecBackend.LaunchEnvironment (a wrapper of LaunchLayers) and the new ExecBackend.LaunchLayers (LaunchEnvironment's composition and reads, moved; LaunchEnvironment's disposition); host movesWorker.writeSourceReport (ReportLaunch with the layer names); movesProbeOwners.Select, Materialize, selectCommitted and materializeCommitted (the captured report's pin, v2 only); movesRecoveryOwners.Select and Materialize (localEnvDigest); moveswakesource.go movesReadLaunch (passes the access's report through). No new read and no root decision. Ten frozen legacy declarations are added. |
| +# Fix round 5: the probe owners' captured pins live as long as their wake (holdEnvPin from Materialize, under the start loop's context) and never past the wake's deadline (captureEnvPin, pruneEnvPins); movesProbeOwners.Select, Materialize and selectCommitted re-frozen again, read sets unchanged. |
| @legacy 849ce111c61c3dcedd80595348d0a26c67fb47462d497fba63d52d2c94e1d5d0 internal/activator/backend_exec.go ExecBackend.LaunchEnvironment legacy accepted-legacy accepted-legacy;rev3=J1;basis=root-route whole:Route@139 e8853e15b427b80a701b5e10b565f7d4f0b778e0c8185da63266e2408f6b4113 |
| internal/activator/backend_exec.go ExecBackend.LaunchEnvironment legacy accepted-legacy accepted-legacy;rev3=J1;basis=root-route whole:Route@139 e8853e15b427b80a701b5e10b565f7d4f0b778e0c8185da63266e2408f6b4113 |
| @legacy 1680e7269c40013e72c2f0a39abc13ee75cecadf2b3b8aa3d3a0278c060458ce internal/activator/backend_exec.go ExecBackend.LaunchLayers legacy accepted-legacy accepted-legacy;rev3=J1;basis=root-route registry:Socket@751,whole:Route@179,whole:Route@747 66a14ac6d0a4c2c9333adf45a792407d8cc4fa389416feb31fcf58616fc5b611 |
| @@ -10,14 +11,14 @@ internal/host/claimmoveaccess.go claimMoveLaunchSource authority C,G,J1 root-rou |
| internal/host/movespass.go armPhase1 authority B2,C rev3=census-ruling-seeded-instances;basis=the agent (/opt/carlos/carlos agent, carlos-edge.service, user carlos): main's body with the S1 probe hook refusing a row whose import gate is set; main's B2,C authority retained registry:ImportGate@8279,whole:Route@8266,whole:Route@8408,whole:Route@8757,whole:Route@8898 b24834b54b93a21e8ba0757fa8f61a9a5eee2ea3c8fa85d634ef4ce4aba1c85d |
| @legacy 6080eef9badd1adefd1a5c4e7a6dd3c64999b8cf58eb016adbe4e72ab8efbff2 internal/host/movesphase1.go movesWorker.writeSourceReport legacy accepted-legacy accepted-legacy;rev3=J4;basis=root-input file:Stat@753 585c0c5bb9b8c20d5a5d77dc08e49720268951ec30bbc1175db9c83900c817f2 |
| internal/host/movesphase1.go movesWorker.writeSourceReport legacy accepted-legacy accepted-legacy;rev3=J4;basis=root-input file:Stat@753 585c0c5bb9b8c20d5a5d77dc08e49720268951ec30bbc1175db9c83900c817f2 |
| -@legacy 31374a531ac40f3a026dda50d7a0e34f5dc20cfc9c2efa542179153fb1bd5a6b internal/host/movesprobeowners.go movesProbeOwners.Materialize legacy accepted-legacy accepted-legacy;rev3=merge-ruling-2;basis=Agent landing materialization adds C8 payload establishment and recovery delegation; generic payload rendering remains legacy. registry:DBPath@1041,whole:Route@1214,whole:Route@2099 74025530a72fa4db43de8510ce1757f333e8fc29aeb4a4d233faaa4e1be525bb |
| -internal/host/movesprobeowners.go movesProbeOwners.Materialize legacy accepted-legacy accepted-legacy;rev3=merge-ruling-2;basis=Agent landing materialization adds C8 payload establishment and recovery delegation; generic payload rendering remains legacy. registry:DBPath@1041,whole:Route@1214,whole:Route@2099 74025530a72fa4db43de8510ce1757f333e8fc29aeb4a4d233faaa4e1be525bb |
| -@legacy 21682c4aef12b99459a3f35f076b1514353a7d68de6f4cd17e61ff75037de8e5 internal/host/movesprobeowners.go movesProbeOwners.Select legacy accepted-legacy accepted-legacy;rev3=J1;basis=root-route registry:DBPath@1400,registry:DBPath@3185,registry:Host@3305,whole:Route@206,whole:Route@2292,whole:Route@465,whole:Route@765 caa777dee227adb4efe3e56ce19651c35c777e042dd425fa34a294b87940b0b2 |
| -internal/host/movesprobeowners.go movesProbeOwners.Select legacy accepted-legacy accepted-legacy;rev3=J1;basis=root-route registry:DBPath@1400,registry:DBPath@3185,registry:Host@3305,whole:Route@206,whole:Route@2292,whole:Route@465,whole:Route@765 caa777dee227adb4efe3e56ce19651c35c777e042dd425fa34a294b87940b0b2 |
| +@legacy 771616e04534c6fed0261b9a78e478699bcaf9dae630c33b728f850af6aa054f internal/host/movesprobeowners.go movesProbeOwners.Materialize legacy accepted-legacy accepted-legacy;rev3=merge-ruling-2;basis=Agent landing materialization adds C8 payload establishment and recovery delegation; generic payload rendering remains legacy. registry:DBPath@1248,whole:Route@1421,whole:Route@2306 42c8c9e5f6abe378563a8a56a0eae3343c67af943ce2e416ab98e38df4e28f01 |
| +internal/host/movesprobeowners.go movesProbeOwners.Materialize legacy accepted-legacy accepted-legacy;rev3=merge-ruling-2;basis=Agent landing materialization adds C8 payload establishment and recovery delegation; generic payload rendering remains legacy. registry:DBPath@1248,whole:Route@1421,whole:Route@2306 42c8c9e5f6abe378563a8a56a0eae3343c67af943ce2e416ab98e38df4e28f01 |
| +@legacy 1dab60240392c98afca2cc2717f3746e5b49f4ae54f2013e1177334452931ba8 internal/host/movesprobeowners.go movesProbeOwners.Select legacy accepted-legacy accepted-legacy;rev3=J1;basis=root-route registry:DBPath@1412,registry:DBPath@3197,registry:Host@3317,whole:Route@206,whole:Route@2304,whole:Route@465,whole:Route@777 6f60256f2c39449c7d8749a7e69d85d956b516294c8351e37c7e8526c29b2f38 |
| +internal/host/movesprobeowners.go movesProbeOwners.Select legacy accepted-legacy accepted-legacy;rev3=J1;basis=root-route registry:DBPath@1412,registry:DBPath@3197,registry:Host@3317,whole:Route@206,whole:Route@2304,whole:Route@465,whole:Route@777 6f60256f2c39449c7d8749a7e69d85d956b516294c8351e37c7e8526c29b2f38 |
| @legacy 4974c2d7e20a2ce3851c4ffb0e9a13ed30bc4f6dc9735be24499db02e8ef5226 internal/host/movesprobeowners.go movesProbeOwners.materializeCommitted legacy accepted-legacy accepted-legacy;rev3=merge-ruling-2;basis=Committed landing materialization establishes C8 files before start check; payload delivery remains legacy. registry:DBPath@652,whole:Route@639,whole:Route@735 4d00a204256a26f0d3158ef601c9dd96fee156dfac743b9a47132c3c45b34630 |
| internal/host/movesprobeowners.go movesProbeOwners.materializeCommitted legacy accepted-legacy accepted-legacy;rev3=merge-ruling-2;basis=Committed landing materialization establishes C8 files before start check; payload delivery remains legacy. registry:DBPath@652,whole:Route@639,whole:Route@735 4d00a204256a26f0d3158ef601c9dd96fee156dfac743b9a47132c3c45b34630 |
| -@legacy 1df478369249b1745a665becd8378c7a9d432e5f0c91e69990aeb9a5b3083165 internal/host/movesprobeowners.go movesProbeOwners.selectCommitted legacy accepted-legacy accepted-legacy;rev3=J1;basis=root-route registry:DBPath@1608,registry:Host@1690,whole:Route@176,whole:Route@619 b398f595a9b8e2b295b7c82c4c678345186cbed3e831590ece16dffc491fece2 |
| -internal/host/movesprobeowners.go movesProbeOwners.selectCommitted legacy accepted-legacy accepted-legacy;rev3=J1;basis=root-route registry:DBPath@1608,registry:Host@1690,whole:Route@176,whole:Route@619 b398f595a9b8e2b295b7c82c4c678345186cbed3e831590ece16dffc491fece2 |
| +@legacy b5bfc970af59cd3103c3090cb0c339809e580d3057e22afb371144859c426dcb internal/host/movesprobeowners.go movesProbeOwners.selectCommitted legacy accepted-legacy accepted-legacy;rev3=J1;basis=root-route registry:DBPath@1628,registry:Host@1710,whole:Route@196,whole:Route@639 d98982bb138a7f4c5a0220cc051e4e9b224b2eb058c992c50b43eb110396be82 |
| +internal/host/movesprobeowners.go movesProbeOwners.selectCommitted legacy accepted-legacy accepted-legacy;rev3=J1;basis=root-route registry:DBPath@1628,registry:Host@1710,whole:Route@196,whole:Route@639 d98982bb138a7f4c5a0220cc051e4e9b224b2eb058c992c50b43eb110396be82 |
| @legacy 7ba24c0aa59e9f30cdf08fdd9e7acb056ab11672ae774772df2c7bb3bbb15ac3 internal/host/movesrecoveryowners.go movesRecoveryOwners.Materialize legacy accepted-legacy accepted-legacy;rev3=J1;basis=root-route registry:DBPath@577,whole:Route@1566,whole:Route@750 a87029143a13dc05d5b873b6b4f099515be737f9e0188586fac5fe91719ccd81 |
| internal/host/movesrecoveryowners.go movesRecoveryOwners.Materialize legacy accepted-legacy accepted-legacy;rev3=J1;basis=root-route registry:DBPath@577,whole:Route@1566,whole:Route@750 a87029143a13dc05d5b873b6b4f099515be737f9e0188586fac5fe91719ccd81 |
| @legacy c8ceeedc46203dacdf33f82908b901fe25afea0de4f86401d27bbfb5ab27293c internal/host/movesrecoveryowners.go movesRecoveryOwners.Select legacy accepted-legacy accepted-legacy;rev3=J1;basis=root-route registry:DBPath@1756,registry:DBPath@792,whole:Route@1011,whole:Route@514 746a4c5f4a87e6b5210ae12e986af05ea8bf3849d940a4fec6598000b555cd46 |
| @@ -25,5 +26,5 @@ internal/host/movesrecoveryowners.go movesRecoveryOwners.Select legacy accepted- |
| @legacy 1a77e1c538d55076eb81dbe54e0c110803dbafd56a79990246430473fca9a16d internal/host/moveswakesource.go movesReadLaunch legacy accepted-legacy accepted-legacy;rev3=merge-ruling-2;basis=Moves launch-input adapter selects by host through existing read interface; generic launch payload selection remains legacy. registry:Host@258 780a60ccf1035f2c7d64fdc804a867b64d380b05a51b151155ca6efe70b01767 |
| internal/host/moveswakesource.go movesReadLaunch legacy accepted-legacy accepted-legacy;rev3=merge-ruling-2;basis=Moves launch-input adapter selects by host through existing read interface; generic launch payload selection remains legacy. registry:Host@258 780a60ccf1035f2c7d64fdc804a867b64d380b05a51b151155ca6efe70b01767 |
| @coverage internal/activator 3e4fae49c0669c0524ec2cadcedf5d86de1bc10df1ebe4ad370d99b5712819f1 |
| -@coverage internal/host 500e7528b90e328c2e89ed753844c3325a3f206a6e016fceeac48f5934c390df |
| +@coverage internal/host c23c90cb6dedd6da24099f75d3af31f2e0e2f8ad685df4ad1cd5891aac219360 |
| @coverage internal/moves 51ae19338bf9f92d45cc166f9fb71cdd3280c617892f71bb9ce67b2b00a7ae27 |