carlos / platform Public

settings-4 task 5b: fix round 1: the recorded build's tier; a missing capability is never "may have run"

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
claude agent, operated by paul@keymail.dev facb27709b86c9a5f16b6654fd41dcbf714cce52 parent a9fe69f
3 files changed, +255 −40
  • internal/activator/backend_exec.go +68 −33
  • internal/activator/containment.go +4 −3
  • internal/activator/identity_test.go +183 −4
diff --git a/internal/activator/backend_exec.go b/internal/activator/backend_exec.go
index cd4aa9b93..c6b381240 100644
--- a/internal/activator/backend_exec.go
+++ b/internal/activator/backend_exec.go
@@ -364,7 +364,7 @@ func (b *ExecBackend) tier(rt registry.Route) cgroup.Limit {
// for the host in between. With Strict nil (a box with no moves wiring)
// none of this runs and no lock is taken: the spawn is main's exactly.
func (b *ExecBackend) spawnInCgroup(rt registry.Route, build func() *exec.Cmd) (*exec.Cmd, func(), error) {
- return b.spawnInCgroupGated(rt, build, nil)
+ return b.spawnInCgroupGated(rt, cgroup.Limit{}, build, nil)
}
// spawnInCgroupGated is spawnInCgroup with a before-start hook (final review
@@ -372,9 +372,16 @@ func (b *ExecBackend) spawnInCgroup(rt registry.Route, build func() *exec.Cmd) (
// (StartGated), or, strict, after the strict group is prepared. A refusal
// starts nothing and is cleaned up as a failed start is. A nil before is
// spawnInCgroup exactly.
-func (b *ExecBackend) spawnInCgroupGated(rt registry.Route, build func() *exec.Cmd, before func() error) (*exec.Cmd, func(), error) {
+//
+// tier is the artifact's own (a recorded or staged build's, ruling
+// R-TIER); zero is b.tier(rt), the live link's, which is every caller but
+// a staged spawn.
+func (b *ExecBackend) spawnInCgroupGated(rt registry.Route, tier cgroup.Limit, build func() *exec.Cmd, before func() error) (*exec.Cmd, func(), error) {
+ if tier == (cgroup.Limit{}) {
+ tier = b.tier(rt)
+ }
if b.Strict == nil {
- cmd, err := b.Cgroups.StartGated(rt.Host, b.tier(rt), build, before)
+ cmd, err := b.Cgroups.StartGated(rt.Host, tier, build, before)
return cmd, func() {}, err
}
strict := b.Strict(rt.Host)
@@ -393,14 +400,14 @@ func (b *ExecBackend) spawnInCgroupGated(rt registry.Route, build func() *exec.C
return nil, nil, fmt.Errorf("%s: %w", rt.Host, cause)
}
if !strict {
- cmd, err := b.Cgroups.StartGated(rt.Host, b.tier(rt), build, before)
+ cmd, err := b.Cgroups.StartGated(rt.Host, tier, build, before)
g.mu.Unlock()
return cmd, func() {}, err
}
o := &strictOwner{}
g.owner = o
g.mu.Unlock()
- sp, err := b.prepareStrictProc(rt.Host, b.tier(rt))
+ sp, err := b.prepareStrictProc(rt.Host, tier)
if err != nil {
b.disown(rt.Host, o, false)
return nil, nil, fmt.Errorf("%s: %w: %v", rt.Host, ErrStrictRequired, err)
@@ -780,30 +787,33 @@ func (b *ExecBackend) Seed(ctx context.Context, rt registry.Route) error {
p := b.pathsFor(rt)
switch p.layout {
case LayoutNested:
- return b.seedUnit(ctx, rt, nil)
+ return b.seedUnit(ctx, rt, nil, cgroup.Limit{})
case LayoutFenced, LayoutConflict:
return fmt.Errorf("%s: refusing to seed: %s", rt.Host, p.reason)
}
- return b.seedChild(ctx, rt, b.liveLink(rt.Host))
+ return b.seedChild(ctx, rt, b.liveLink(rt.Host), cgroup.Limit{})
}
// SeedExe is Seed on the identity a wake recorded (settings spec §4.2):
// id.StagedPath is checked against id.Digest first, and a copy that
// changed seeds nothing (never the live link instead). A flat or foreign
// row execs that file by path. A nested row asks root to run the seed unit
-// on it, which root validates and binds in place of the live link.
+// on it, which root validates and binds in place of the live link. The
+// seed runs at id's own tier (GenTier, ruling R-TIER), never the tier of
+// whatever the live link names by then.
func (b *ExecBackend) SeedExe(ctx context.Context, rt registry.Route, id release.StagedIdentity) error {
if err := release.VerifyStaged(id); err != nil {
return fmt.Errorf("%s: not seeded with %s: %w", rt.Host, id.Version, err)
}
+ tier := b.GenTier(rt, id.Version)
p := b.pathsFor(rt)
switch p.layout {
case LayoutNested:
- return b.seedUnit(ctx, rt, stagedExeOf(id))
+ return b.seedUnit(ctx, rt, stagedExeOf(id), tier)
case LayoutFenced, LayoutConflict:
return fmt.Errorf("%s: refusing to seed: %s", rt.Host, p.reason)
}
- return b.seedChild(ctx, rt, id.StagedPath)
+ return b.seedChild(ctx, rt, id.StagedPath, tier)
}
// seedArgv parses rt.Seed's JSON argv array and appends the --db flag, the
@@ -837,7 +847,8 @@ func removeSeedDB(dbPath string) {
// seedChild is Seed's flat body. exe is the file the seed execs: the live
// link (Seed), which is materialized first if it is missing, or a staged
// path SeedExe verified, which is exec'd by path whatever the link names.
-func (b *ExecBackend) seedChild(ctx context.Context, rt registry.Route, exe string) error {
+// tier is that file's own; zero is the live tier (b.tier).
+func (b *ExecBackend) seedChild(ctx context.Context, rt registry.Route, exe string, tier cgroup.Limit) error {
host := rt.Host
paths := b.pathsFor(rt)
argv, err := seedArgv(rt, paths.dbPath)
@@ -883,7 +894,7 @@ func (b *ExecBackend) seedChild(ctx context.Context, rt registry.Route, exe stri
if perr != nil {
return perr
}
- cmd, settle, err := b.spawnInCgroupAt(rt, child, false, func() *exec.Cmd {
+ cmd, settle, err := b.spawnInCgroupAtTier(rt, child, false, tier, func() *exec.Cmd {
cmd := exec.CommandContext(ctx, exe, argv...)
// The control-channel vars are agent-owned in BOTH directions here too
// (Start's discipline, see withoutVars), and here they are always
@@ -916,7 +927,7 @@ func (b *ExecBackend) seedChild(ctx context.Context, rt registry.Route, exe stri
// wedged seed would hold the lease until the wake's own 5m budget expired.
cmd.WaitDelay = seedDrainGrace
return cmd
- })
+ }, nil)
if err == nil {
err = cmd.Wait()
settle()
@@ -1017,11 +1028,15 @@ func (c *capBuffer) String() string {
// wake recorded (SeedExe): root validates it and binds the seed unit's app
// mount to it, so the live link need not exist and a repoint of it
// changes nothing. A control channel that cannot name it refuses the seed.
-func (b *ExecBackend) seedUnit(ctx context.Context, rt registry.Route, exe *instancectl.StagedExe) error {
+// tier is that build's own (ruling R-TIER); zero is the live tier.
+func (b *ExecBackend) seedUnit(ctx context.Context, rt registry.Route, exe *instancectl.StagedExe, tier cgroup.Limit) error {
host := rt.Host
if err := b.nestedGate(rt); err != nil {
return err
}
+ if tier == (cgroup.Limit{}) {
+ tier = b.tier(rt)
+ }
var xc StagedExeControl
if exe == nil {
if err := b.ensureLiveLink(ctx, host); err != nil {
@@ -1067,9 +1082,9 @@ func (b *ExecBackend) seedUnit(ctx context.Context, rt registry.Route, exe *inst
sctx, cancel := context.WithTimeout(ctx, instancectl.DeadlineSeed)
var res instancectl.SeedResponse
if exe != nil {
- res, err = xc.SeedExe(sctx, host, b.tier(rt).MemoryMax, *exe)
+ res, err = xc.SeedExe(sctx, host, tier.MemoryMax, *exe)
} else {
- res, err = b.Control.Seed(sctx, host, b.tier(rt).MemoryMax)
+ res, err = b.Control.Seed(sctx, host, tier.MemoryMax)
}
cancel()
if errors.Is(err, instancectl.ErrStagedExe) {
@@ -1266,11 +1281,11 @@ func (b *ExecBackend) Start(ctx context.Context, rt registry.Route) (Proc, strin
p := b.pathsFor(rt)
switch p.layout {
case LayoutNested:
- return b.startUnitAt(ctx, rt, rt.Socket, 0, nil)
+ return b.startUnitAt(ctx, rt, rt.Socket, 0, nil, cgroup.Limit{})
case LayoutFenced, LayoutConflict:
return nil, "", fmt.Errorf("%s: refusing to wake: %s", rt.Host, p.reason)
}
- return b.startChildAt(ctx, rt, rt.Socket, 0, "")
+ return b.startChildAt(ctx, rt, rt.Socket, 0, "", cgroup.Limit{})
}
// StartGen is Start for generation n (zero-downtime spec §7.1). perGen false
@@ -1286,7 +1301,7 @@ func (b *ExecBackend) Start(ctx context.Context, rt registry.Route) (Proc, strin
// whose counter started at 1 like this one's). A sibling generation's path
// is never touched.
func (b *ExecBackend) StartGen(ctx context.Context, rt registry.Route, n uint64, perGen bool) (Proc, Gen, error) {
- return b.startGenFrom(ctx, rt, n, perGen, nil)
+ return b.startGenFrom(ctx, rt, n, perGen, nil, cgroup.Limit{})
}
// StartGenExe is StartGen on the identity a wake recorded (settings spec
@@ -1296,22 +1311,30 @@ func (b *ExecBackend) StartGen(ctx context.Context, rt registry.Route, n uint64,
// foreign row execs the file by path. A nested row asks root to bind it in
// place of the live link: the original run unit while GenContainment is
// off (generation 0), generation n's unit while it is on. The handle names
-// id's build, path, digest and demand, so a sidecar can bind the same file.
+// id's build, path, digest, demand and tier, so a sidecar can bind the same
+// file.
+//
+// The tier is id's own (GenTier, as StartStaged takes it; ruling R-TIER),
+// never b.tier(rt): that reads whatever the live link names when it is
+// asked, and root may have repointed the link since the record, which
+// would run A's bytes under B's limits.
func (b *ExecBackend) StartGenExe(ctx context.Context, rt registry.Route, n uint64, perGen bool, id release.StagedIdentity) (Proc, Gen, error) {
if err := release.VerifyStaged(id); err != nil {
return nil, Gen{}, fmt.Errorf("%s: not started on %s: %w", rt.Host, id.Version, err)
}
- proc, g, err := b.startGenFrom(ctx, rt, n, perGen, &id)
+ tier := b.GenTier(rt, id.Version)
+ proc, g, err := b.startGenFrom(ctx, rt, n, perGen, &id, tier)
if err != nil {
return proc, g, err
}
- g.Build, g.StagedPath, g.Digest, g.Demand = id.Version, id.StagedPath, id.Digest, id.Demand
+ g.Build, g.StagedPath, g.Digest, g.Demand, g.Tier = id.Version, id.StagedPath, id.Digest, id.Demand, tier
return proc, g, nil
}
// startGenFrom is StartGen's body. A nil id is the live link (StartGen); a
-// non-nil one is the verified file StartGenExe names.
-func (b *ExecBackend) startGenFrom(ctx context.Context, rt registry.Route, n uint64, perGen bool, id *release.StagedIdentity) (Proc, Gen, error) {
+// non-nil one is the verified file StartGenExe names. tier is that file's
+// own; zero is the live tier (b.tier).
+func (b *ExecBackend) startGenFrom(ctx context.Context, rt registry.Route, n uint64, perGen bool, id *release.StagedIdentity, tier cgroup.Limit) (Proc, Gen, error) {
var (
flatExe string
unitExe *instancectl.StagedExe
@@ -1337,7 +1360,7 @@ func (b *ExecBackend) startGenFrom(ctx context.Context, rt registry.Route, n uin
switch p.layout {
case LayoutNested:
// prepareAt removes the stale file at sock for the nested layout.
- proc, ver, err = b.startUnitAt(ctx, rt, sock, n, unitExe)
+ proc, ver, err = b.startUnitAt(ctx, rt, sock, n, unitExe, tier)
case LayoutFenced, LayoutConflict:
return nil, Gen{}, fmt.Errorf("%s: refusing to wake: %s", rt.Host, p.reason)
default:
@@ -1353,7 +1376,7 @@ func (b *ExecBackend) startGenFrom(ctx context.Context, rt registry.Route, n uin
return nil, Gen{}, fmt.Errorf("%s: generation socket: %w", rt.Host, err)
}
}
- proc, ver, err = b.startChildAt(ctx, rt, sock, n, flatExe)
+ proc, ver, err = b.startChildAt(ctx, rt, sock, n, flatExe, tier)
}
g := Gen{N: n, Socket: sock, Control: sock + ".control"}
if err != nil {
@@ -1399,8 +1422,9 @@ func clearOwnGenSocket(sock string) error {
// (StartGenExe). spawnChildFrom leaves a staged spawn's cache purge to the
// swap's switch, but a wake's staged spawn is the host's only process, so
// its purge happens here, at the same moment, before the health wait.
-func (b *ExecBackend) startChildAt(ctx context.Context, rt registry.Route, sock string, n uint64, exe string) (Proc, string, error) {
- p, spawnVer, err := b.spawnChildFrom(ctx, rt, sock, n, exe, cgroup.Limit{})
+// tier is exe's own; zero is the live tier (b.tier).
+func (b *ExecBackend) startChildAt(ctx context.Context, rt registry.Route, sock string, n uint64, exe string, tier cgroup.Limit) (Proc, string, error) {
+ p, spawnVer, err := b.spawnChildFrom(ctx, rt, sock, n, exe, tier)
if err != nil {
return p, "", err
}
@@ -1797,8 +1821,18 @@ func (b *ExecBackend) spawnUnitFrom(ctx context.Context, rt registry.Route, sock
inv, err := b.startRole(ctx, rt, instancectl.RoleRun, gen, exe, tier)
// Past this call root may have started the unit: every failure below
// but ErrStagedExe (root's refusal before it laid out or started
- // anything) is marked startUncertain, since the build may have run
- // before it failed or exited (final review wave 2, N4).
+ // anything) and errNoStagedExeControl (root was never asked: the
+ // channel cannot name the file) is marked startUncertain, since the
+ // build may have run before it failed or exited (final review wave 2,
+ // N4).
+ if errors.Is(err, errNoStagedExeControl) {
+ // Nothing was sent to root. Proven not run, so never a failure
+ // record. A generation's own launch file goes as after a refusal.
+ if gen != 0 {
+ _ = os.Remove(filepath.Join(p.dataDir, instancectl.GenLaunchFileName(gen)))
+ }
+ return fail(fmt.Errorf("%s: start unit: %w", rt.Host, err))
+ }
if errors.Is(err, instancectl.ErrStagedExe) {
// Root refused the staged build before it laid out or started
// anything, so there is nothing to kill and nothing unproven. The
@@ -1853,9 +1887,10 @@ func (b *ExecBackend) spawnUnitFrom(ctx context.Context, rt registry.Route, sock
// than exec'd directly. The agent has no privilege to enter the tenant's
// namespace, only the control service does. It is spawnUnitFrom, then the
// health wait. exe is nil for the live link, or the staged file a wake
-// recorded (StartGenExe), which root binds in place of the link.
-func (b *ExecBackend) startUnitAt(ctx context.Context, rt registry.Route, sock string, n uint64, exe *instancectl.StagedExe) (Proc, string, error) {
- p, spawnVer, err := b.spawnUnitFrom(ctx, rt, sock, n, exe, cgroup.Limit{})
+// recorded (StartGenExe), which root binds in place of the link. tier is
+// exe's own; zero is the live tier (b.tier).
+func (b *ExecBackend) startUnitAt(ctx context.Context, rt registry.Route, sock string, n uint64, exe *instancectl.StagedExe, tier cgroup.Limit) (Proc, string, error) {
+ p, spawnVer, err := b.spawnUnitFrom(ctx, rt, sock, n, exe, tier)
if err != nil {
return p, "", err
}
diff --git a/internal/activator/containment.go b/internal/activator/containment.go
index b948de49f..12d07f089 100644
--- a/internal/activator/containment.go
+++ b/internal/activator/containment.go
@@ -98,8 +98,9 @@ func (b *ExecBackend) spawnInCgroupAt(rt registry.Route, child string, ownTier b
// spawnInCgroupAtTier is spawnInCgroup for a placed child, with the
// artifact's tier named: the generation child's own limit, and the parent's
// floor for StartChild (which never lowers a parent a swap has raised). Zero
-// tier is b.tier(rt). With no child it IS spawnInCgroup, so a flag-off spawn
-// is byte for byte today's.
+// tier is b.tier(rt). With no child it IS spawnInCgroup at that tier, so a
+// flag-off spawn with a zero tier is byte for byte today's, and a recorded
+// build's (ruling R-TIER) caps the host cgroup at its own tier.
//
// A placed spawn is never strict: strict containment of a generation layout
// is the claim path's (PrepareStrictParent, plan 5). It still honours the
@@ -115,7 +116,7 @@ func (b *ExecBackend) spawnInCgroupAt(rt registry.Route, child string, ownTier b
// own error. Nil is the spawn exactly as it was.
func (b *ExecBackend) spawnInCgroupAtTier(rt registry.Route, child string, ownTier bool, tier cgroup.Limit, build func() *exec.Cmd, before func() error) (*exec.Cmd, func(), error) {
if child == "" {
- return b.spawnInCgroupGated(rt, build, before)
+ return b.spawnInCgroupGated(rt, tier, build, before)
}
start := func() (*exec.Cmd, func(), error) {
t := tier
diff --git a/internal/activator/identity_test.go b/internal/activator/identity_test.go
index 1b07a5f95..661a96eb1 100644
--- a/internal/activator/identity_test.go
+++ b/internal/activator/identity_test.go
@@ -154,7 +154,8 @@ func TestStartGenExeFlatExecsTheRecordedFile(t *testing.T) {
if got := waitForFile(t, marker, 2*time.Second); got != a {
t.Fatalf("execd %q, want the recorded %q", got, a)
}
- want := Gen{N: 3, Socket: rt.Socket, Control: rt.Socket + ".control", Build: "vA", StagedPath: a, Digest: id.Digest}
+ // No TierOf here, so the build's tier is GenTier's fallback, the default.
+ want := Gen{N: 3, Socket: rt.Socket, Control: rt.Socket + ".control", Build: "vA", StagedPath: a, Digest: id.Digest, Tier: cgroup.TierDefault}
if g != want {
t.Fatalf("handle = %+v, want %+v", g, want)
}
@@ -294,23 +295,26 @@ func exeLine(verb, host, what string, x instancectl.StagedExe) string {
return fmt.Sprintf("%s %s %s %s %s %s", verb, host, what, x.Path, x.Version, x.Digest)
}
-func (c identityControl) StartExe(_ context.Context, host string, role instancectl.Role, _ int64, x instancectl.StagedExe) (string, error) {
+func (c identityControl) StartExe(_ context.Context, host string, role instancectl.Role, max int64, x instancectl.StagedExe) (string, error) {
c.rec(exeLine("startexe", host, string(role), x))
+ c.rec(fmt.Sprintf("tier startexe %d", max))
if c.onStart != nil {
c.onStart()
}
return c.inv, c.startErr
}
-func (c identityControl) SeedExe(_ context.Context, host string, _ int64, x instancectl.StagedExe) (instancectl.SeedResponse, error) {
+func (c identityControl) SeedExe(_ context.Context, host string, max int64, x instancectl.StagedExe) (instancectl.SeedResponse, error) {
c.rec(exeLine("seedexe", host, "seed", x))
+ c.rec(fmt.Sprintf("tier seedexe %d", max))
return c.seed, nil
}
func (c identityControl) StartGen(_ context.Context, host string, n uint64, _ int64) (string, error) {
c.rec(fmt.Sprintf("startgen %s %d", host, n))
return "", errors.New("test: the live-link generation start was used")
}
-func (c identityControl) StartGenExe(_ context.Context, host string, n uint64, _ int64, x instancectl.StagedExe) (string, error) {
+func (c identityControl) StartGenExe(_ context.Context, host string, n uint64, max int64, x instancectl.StagedExe) (string, error) {
c.rec(exeLine("startgenexe", host, fmt.Sprint(n), x))
+ c.rec(fmt.Sprintf("tier startgenexe %d", max))
if c.onStart != nil {
c.onStart()
}
@@ -445,3 +449,178 @@ func TestStartRoleRefusesAnExeTheControlCannotName(t *testing.T) {
t.Fatalf("root was asked:\n%s", ctl.joined())
}
}
+
+// tierFollowsTheLink is production's tier shape for a test backend: b.Tier
+// reads the manifest of whatever the live link names when it is asked, and
+// TierOf reads one named version's. vA declares children (1 GiB); every
+// other build is the default (512 MiB).
+func tierFollowsTheLink(b *ExecBackend, vA string) {
+ b.TierOf = func(_ registry.Route, v string) cgroup.Limit {
+ if v == vA {
+ return cgroup.TierChildren
+ }
+ return cgroup.TierDefault
+ }
+ b.Tier = func(rt registry.Route) cgroup.Limit {
+ v, _ := b.LiveVersion(rt.Host)
+ return b.TierOf(rt, v)
+ }
+}
+
+const childrenMax = "1073741824" // cgroup.TierChildren.MemoryMax
+
+// Ruling R-TIER: the recorded build runs under its own tier, not the tier
+// of whatever root repointed the live link to after the record. A declares
+// children (1 GiB), B the default (512 MiB), and the link names B by the
+// time each spawn and seed runs. Flat, the limit lands on the host cgroup
+// (flag off) or generation n's child (flag on); nested, it is the memory_max
+// root is sent. The handle carries A's tier.
+func TestStartGenExeAndSeedExeUseTheRecordedBuildsTier(t *testing.T) {
+ flat := func(t *testing.T, gens bool) (*ExecBackend, registry.Route, release.StagedIdentity, string) {
+ const host = "tier.example.com"
+ b, parent := delegatedBackend(t, host)
+ b.RunDirOverride = shortRunDir(t)
+ b.GenContainment = gens
+ tierFollowsTheLink(b, "vA")
+ marker := filepath.Join(t.TempDir(), "argv0")
+ stageVersion(t, b, host, "vA", servingBody(t, marker))
+ rt := flatGenRoute(b, host)
+ id, err := b.LiveIdentity(context.Background(), rt)
+ if err != nil {
+ t.Fatal(err)
+ }
+ stageVersion(t, b, host, "vB", servingBody(t, marker))
+ if got := b.tier(rt); got != cgroup.TierDefault {
+ t.Fatalf("fixture: the live tier is %v, want B's default", got)
+ }
+ return b, rt, id, parent
+ }
+ readMax := func(t *testing.T, path string) string {
+ t.Helper()
+ got, err := os.ReadFile(path)
+ if err != nil {
+ t.Fatal(err)
+ }
+ return string(got)
+ }
+ t.Run("flat, generations off", func(t *testing.T) {
+ requireUnixSockets(t)
+ b, rt, id, parent := flat(t, false)
+ proc, g, err := b.StartGenExe(context.Background(), rt, 3, false, id)
+ if err != nil {
+ t.Fatalf("StartGenExe: %v", err)
+ }
+ defer proc.Stop(context.Background()) //nolint:errcheck
+ if got := readMax(t, filepath.Join(parent, "memory.max")); got != childrenMax || g.Tier != cgroup.TierChildren {
+ t.Fatalf("host cgroup memory.max = %s, handle tier %v; want A's 1 GiB", got, g.Tier)
+ }
+ })
+ t.Run("flat, generations on", func(t *testing.T) {
+ requireUnixSockets(t)
+ b, rt, id, parent := flat(t, true)
+ proc, g, err := b.StartGenExe(context.Background(), rt, 9, true, id)
+ if err != nil {
+ t.Fatalf("StartGenExe: %v", err)
+ }
+ defer proc.Stop(context.Background()) //nolint:errcheck
+ if got := readMax(t, filepath.Join(parent, "g9", "memory.max")); got != childrenMax || g.Tier != cgroup.TierChildren {
+ t.Fatalf("g9 memory.max = %s, handle tier %v; want A's 1 GiB", got, g.Tier)
+ }
+ })
+ t.Run("flat seed", func(t *testing.T) {
+ b, rt, id, parent := flat(t, false)
+ rt.Seed = `["seed"]`
+ // A's script execs the fake app, which exits 2 with no --socket:
+ // the seed's verdict is not this test's, its cgroup limit is.
+ _ = b.SeedExe(context.Background(), rt, id)
+ if got := readMax(t, filepath.Join(parent, "memory.max")); got != childrenMax {
+ t.Fatalf("host cgroup memory.max after the seed = %s, want A's 1 GiB", got)
+ }
+ })
+ for _, tc := range []struct {
+ name string
+ gens bool
+ want string
+ }{
+ {"nested, generation 0", false, "tier startexe " + childrenMax},
+ {"nested, generation n", true, "tier startgenexe " + childrenMax},
+ } {
+ t.Run(tc.name, func(t *testing.T) {
+ ctl := &fakeControl{inv: "inv1"}
+ unit := &fakeUnit{}
+ b, rt := nestedBackend(t, ctl, unit)
+ b.Control = identityControl{ctl}
+ b.GenContainment = tc.gens
+ tierFollowsTheLink(b, "vA")
+ stageVersion(t, b, rt.Host, "vA", "exit 0\n")
+ id, err := b.LiveIdentity(context.Background(), rt)
+ if err != nil {
+ t.Fatal(err)
+ }
+ stageVersion(t, b, rt.Host, "vB", "exit 0\n")
+ sock := rt.Socket
+ if tc.gens {
+ sock = nestedGenSocketIn(b.pathsFor(rt).runDir, 6)
+ }
+ var stopServing func()
+ ctl.onStart = func() { stopServing = serveOK(t, sock) }
+ defer func() {
+ if stopServing != nil {
+ stopServing()
+ }
+ }()
+ unit.set("active", "inv1", true, true)
+ p, g, err := b.StartGenExe(context.Background(), rt, 6, tc.gens, id)
+ if err != nil {
+ t.Fatalf("StartGenExe: %v", err)
+ }
+ if !strings.Contains(ctl.joined(), tc.want) || g.Tier != cgroup.TierChildren {
+ t.Fatalf("root was asked:\n%s\nwant %q; handle tier %v", ctl.joined(), tc.want, g.Tier)
+ }
+ unit.set("inactive", "inv1", false, false)
+ waitFor(t, "the unit to exit", p.Exited)
+ })
+ }
+ t.Run("nested seed", func(t *testing.T) {
+ ctl := &fakeControl{seed: instancectl.SeedResponse{InvocationID: "s1", Result: "success", Status: "0", Code: "exited"}}
+ b, rt := nestedBackend(t, ctl, &fakeUnit{})
+ b.Control = identityControl{ctl}
+ tierFollowsTheLink(b, "vA")
+ rt.Seed = `["seed"]`
+ stageVersion(t, b, rt.Host, "vA", "exit 0\n")
+ id, err := b.LiveIdentity(context.Background(), rt)
+ if err != nil {
+ t.Fatal(err)
+ }
+ stageVersion(t, b, rt.Host, "vB", "exit 0\n")
+ if err := b.SeedExe(context.Background(), rt, id); err != nil {
+ t.Fatalf("SeedExe: %v", err)
+ }
+ if want := "tier seedexe " + childrenMax; !strings.Contains(ctl.joined(), want) {
+ t.Fatalf("root was asked:\n%s\nwant %q", ctl.joined(), want)
+ }
+ })
+}
+
+// A control channel that cannot name the recorded file refuses a nested
+// generation-0 start before root is asked anything. That is proven not to
+// have run: never start-uncertain, so it cannot become a failure record.
+func TestAMissingStagedExeCapabilityIsNeverMayHaveRun(t *testing.T) {
+ ctl := &fakeControl{inv: "inv1"}
+ b, rt := nestedBackend(t, ctl, &fakeUnit{})
+ stageVersion(t, b, rt.Host, "vA", "exit 0\n")
+ id, err := b.LiveIdentity(context.Background(), rt)
+ if err != nil {
+ t.Fatal(err)
+ }
+ proc, _, err := b.StartGenExe(context.Background(), rt, 1, false, id)
+ if !errors.Is(err, errNoStagedExeControl) {
+ t.Fatalf("StartGenExe = %v, want errNoStagedExeControl", err)
+ }
+ if startMayHaveRun(proc, err) {
+ t.Fatalf("a start root was never asked for reads as may-have-run: %v", err)
+ }
+ if ctl.joined() != "" {
+ t.Fatalf("root was asked:\n%s", ctl.joined())
+ }
+}