carlos / platform Public

zdd-readiness: cgroup recreation tests prove freshness without relying on inode numbers

The Task 19 tests proved a recreated cgroup by a changed inode. ext4 and xfs reuse an inode straight after rmdir+mkdir, so they failed on disk and passed on tmpfs. They now plant a sentinel file in the killed directory and assert it is gone (or still there for cgroups that must be kept). The kill marks are also cleared when a test root is made, because a stale mark from an earlier test could name a reused inode in a later test's fresh directory.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
claude agent, operated by paul@keymail.dev 13dffeab1d5ca25181b3cfd13efa73ffe7c27f6e parent b0ab08f
2 files changed, +56 −25
  • internal/cgroup/cgroup_test.go +8 −0
  • internal/cgroup/recreate_linux_test.go +48 −25

CI passed — run details

CI log
=== gate ===
go: downloading github.com/aws/aws-sdk-go-v2 v1.44.0
go: downloading github.com/aws/aws-sdk-go-v2/service/sns v1.43.0
go: downloading github.com/aws/aws-sdk-go-v2/config v1.32.6
go: downloading github.com/aws/aws-sdk-go-v2/service/s3 v1.105.0
go: downloading modernc.org/sqlite v1.55.0
go: downloading github.com/aws/aws-sdk-go-v2/service/acm v1.45.0
go: downloading github.com/aws/aws-sdk-go-v2/service/cloudwatch v1.68.0
go: downloading github.com/aws/aws-sdk-go-v2/service/ec2 v1.324.0
go: downloading github.com/aws/aws-sdk-go-v2/service/iam v1.58.2
go: downloading github.com/aws/aws-sdk-go-v2/service/resourcegroupstaggingapi v1.37.0
go: downloading github.com/aws/aws-sdk-go-v2/service/route53 v1.64.1
go: downloading github.com/aws/aws-sdk-go-v2/service/sts v1.45.6
go: downloading github.com/aws/smithy-go v1.28.1
go: downloading github.com/superfly/ltx v0.5.1
go: downloading github.com/benbjohnson/litestream v0.5.14
go: downloading golang.org/x/sys v0.46.0
go: downloading golang.org/x/crypto v0.52.0
go: downloading github.com/oschwald/maxminddb-golang v1.13.1
go: downloading github.com/aws/aws-sdk-go-v2/credentials v1.19.6
go: downloading github.com/pion/logging v0.2.4
go: downloading github.com/pion/stun/v3 v3.0.1
go: downloading github.com/pion/turn/v4 v4.1.4
go: downloading golang.org/x/net v0.55.0
go: downloading github.com/klauspost/compress v1.18.0
go: downloading github.com/aws/aws-sdk-go-v2/service/sesv2 v1.66.6
go: downloading github.com/aws/aws-sdk-go-v2/service/costexplorer v1.67.5
go: downloading github.com/pion/randutil v0.1.0
go: downloading github.com/pion/transport/v4 v4.0.1
go: downloading github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.16
go: downloading github.com/aws/aws-sdk-go-v2/internal/ini v1.8.4
go: downloading github.com/aws/aws-sdk-go-v2/service/signin v1.0.4
go: downloading github.com/aws/aws-sdk-go-v2/service/ssooidc v1.35.12
go: downloading github.com/aws/aws-sdk-go-v2/service/sso v1.30.8
go: downloading github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.40
go: downloading github.com/aws/aws-sdk-go-v2/internal/v4a v1.4.38
go: downloading github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.19
go: downloading github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.40
go: downloading github.com/pion/dtls/v3 v3.0.7
go: downloading github.com/pion/transport/v3 v3.0.8
go: downloading github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.40
go: downloading github.com/wlynxg/anet v0.0.5
go: downloading github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.14
go: downloading github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.19.31
go: downloading github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.9.23
go: downloading github.com/pierrec/lz4/v4 v4.1.22
go: downloading github.com/prometheus/client_golang v1.17.0
go: downloading golang.org/x/sync v0.21.0
go: downloading github.com/aws/aws-sdk-go-v2/feature/s3/manager v1.20.18
go: downloading github.com/mattn/go-isatty v0.0.20
go: downloading github.com/lmittmann/tint v1.1.3
go: downloading github.com/cespare/xxhash/v2 v2.3.0
go: downloading github.com/beorn7/perks v1.0.1
go: downloading github.com/prometheus/client_model v0.5.0
go: downloading github.com/prometheus/common v0.45.0
go: downloading google.golang.org/protobuf v1.36.10
go: downloading github.com/prometheus/procfs v0.12.0
go: downloading github.com/matttproud/golang_protobuf_extensions/v2 v2.0.0
go: downloading golang.org/x/text v0.37.0
go: downloading modernc.org/libc v1.74.1
go: downloading modernc.org/memory v1.11.0
go: downloading modernc.org/mathutil v1.7.1
go: downloading github.com/dustin/go-humanize v1.0.1
go: downloading github.com/google/uuid v1.6.0
go: downloading github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec
cd launchctx && go vet ./... && go test ./... -count=1
ok  	amadan.net/carlos/platform.git/launchctx	0.012s
?   	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	332.013s
?   	github.com/carlosframework/platform/cmd/changelog	[no test files]
ok  	github.com/carlosframework/platform/cmd/console	4.032s
ok  	github.com/carlosframework/platform/cmd/moveprobe	92.267s
ok  	github.com/carlosframework/platform/internal/activator	214.287s
ok  	github.com/carlosframework/platform/internal/agentsock	0.018s
ok  	github.com/carlosframework/platform/internal/alerting	0.004s
ok  	github.com/carlosframework/platform/internal/analytics	0.019s
ok  	github.com/carlosframework/platform/internal/appconfig	2.349s
ok  	github.com/carlosframework/platform/internal/apperr	0.102s
ok  	github.com/carlosframework/platform/internal/applog	0.210s
ok  	github.com/carlosframework/platform/internal/awsinv	0.007s
ok  	github.com/carlosframework/platform/internal/boxchoice	0.009s
ok  	github.com/carlosframework/platform/internal/boxid	0.578s
?   	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.018s
ok  	github.com/carlosframework/platform/internal/caps	0.018s
ok  	github.com/carlosframework/platform/internal/certwarm	0.002s
ok  	github.com/carlosframework/platform/internal/cgroup	0.205s
ok  	github.com/carlosframework/platform/internal/changelog	0.270s
ok  	github.com/carlosframework/platform/internal/clamd	0.066s
?   	github.com/carlosframework/platform/internal/cliflag	[no test files]
ok  	github.com/carlosframework/platform/internal/console	359.056s
?   	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	6.513s
ok  	github.com/carlosframework/platform/internal/dbdoor/dbdoortest	0.010s
ok  	github.com/carlosframework/platform/internal/dbverify	0.006s
ok  	github.com/carlosframework/platform/internal/dnsrec	0.003s
ok  	github.com/carlosframework/platform/internal/docsite	0.006s
ok  	github.com/carlosframework/platform/internal/domains	2.021s
ok  	github.com/carlosframework/platform/internal/doorkey	0.006s
ok  	github.com/carlosframework/platform/internal/economics	0.005s
ok  	github.com/carlosframework/platform/internal/edge	25.899s
?   	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.141s
ok  	github.com/carlosframework/platform/internal/gate	1.655s
ok  	github.com/carlosframework/platform/internal/genselect	0.020s
ok  	github.com/carlosframework/platform/internal/geo	0.015s
ok  	github.com/carlosframework/platform/internal/geoip	0.039s
ok  	github.com/carlosframework/platform/internal/host	53.127s
ok  	github.com/carlosframework/platform/internal/hostactors	0.545s
ok  	github.com/carlosframework/platform/internal/hostbox	1.318s
ok  	github.com/carlosframework/platform/internal/hostclaim	0.256s
ok  	github.com/carlosframework/platform/internal/hostevidence	0.006s
ok  	github.com/carlosframework/platform/internal/hostexec	0.318s
ok  	github.com/carlosframework/platform/internal/hostfence	0.031s
ok  	github.com/carlosframework/platform/internal/hostfiles	4.148s
ok  	github.com/carlosframework/platform/internal/hostfloor	0.010s
ok  	github.com/carlosframework/platform/internal/hostguard	0.129s
ok  	github.com/carlosframework/platform/internal/hostops	1.346s
?   	github.com/carlosframework/platform/internal/hostops/fencerelease	[no test files]
ok  	github.com/carlosframework/platform/internal/hostops/internal/fencerelease	0.005s
ok  	github.com/carlosframework/platform/internal/hostquarantine	0.002s
ok  	github.com/carlosframework/platform/internal/hostresolve	0.131s
ok  	github.com/carlosframework/platform/internal/hostwrite	0.002s
ok  	github.com/carlosframework/platform/internal/infraview	0.003s
ok  	github.com/carlosframework/platform/internal/instancectl	1.704s
ok  	github.com/carlosframework/platform/internal/instanceexec	2.693s
ok  	github.com/carlosframework/platform/internal/instancefs	0.004s
ok  	github.com/carlosframework/platform/internal/instancekey	0.002s
ok  	github.com/carlosframework/platform/internal/instanceupdates	0.008s
ok  	github.com/carlosframework/platform/internal/layout	1.362s
ok  	github.com/carlosframework/platform/internal/lease	9.437s
ok  	github.com/carlosframework/platform/internal/lease/leasetest	0.006s
ok  	github.com/carlosframework/platform/internal/ledger	0.017s
ok  	github.com/carlosframework/platform/internal/mail	0.153s
ok  	github.com/carlosframework/platform/internal/maintenance	0.088s
ok  	github.com/carlosframework/platform/internal/maintenanceexec	5.540s
ok  	github.com/carlosframework/platform/internal/maintenanceguard	0.066s
ok  	github.com/carlosframework/platform/internal/metrics	0.005s
ok  	github.com/carlosframework/platform/internal/moves	21.157s
ok  	github.com/carlosframework/platform/internal/moves/archive	0.746s
ok  	github.com/carlosframework/platform/internal/objstore	0.162s
ok  	github.com/carlosframework/platform/internal/ops	0.154s
ok  	github.com/carlosframework/platform/internal/park	0.179s
ok  	github.com/carlosframework/platform/internal/privatefile	0.003s
ok  	github.com/carlosframework/platform/internal/provision	3.184s
ok  	github.com/carlosframework/platform/internal/publicrelease	0.009s
ok  	github.com/carlosframework/platform/internal/registry	3.730s
ok  	github.com/carlosframework/platform/internal/relay	7.230s
ok  	github.com/carlosframework/platform/internal/release	19.411s
ok  	github.com/carlosframework/platform/internal/rollout	3.262s
ok  	github.com/carlosframework/platform/internal/rolloutwords	0.004s
ok  	github.com/carlosframework/platform/internal/rootaws	0.007s
ok  	github.com/carlosframework/platform/internal/s3compat	0.005s
ok  	github.com/carlosframework/platform/internal/safefile	0.005s
ok  	github.com/carlosframework/platform/internal/schedule	2.243s
ok  	github.com/carlosframework/platform/internal/sim/hostlife	2.444s
ok  	github.com/carlosframework/platform/internal/sim/litestreamfacts	4.083s
ok  	github.com/carlosframework/platform/internal/status	0.037s
ok  	github.com/carlosframework/platform/internal/steering	0.006s
ok  	github.com/carlosframework/platform/internal/stopevidence	0.130s
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.003s
ok  	github.com/carlosframework/platform/internal/updatewords	0.002s
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.551s
=== cross ===
--- shard 0 of 5 ---
cross-compiling darwin/arm64
go: downloading github.com/ncruces/go-strftime v1.0.0
--- shard 1 of 5 ---
cross-compiling darwin/amd64
go: downloading github.com/ncruces/go-strftime v1.0.0
--- shard 2 of 5 ---
cross-compiling linux/arm64
--- shard 3 of 5 ---
cross-compiling windows/amd64
go: downloading github.com/ncruces/go-strftime v1.0.0
--- shard 4 of 5 ---
cross-compiling windows/arm64
go: downloading github.com/ncruces/go-strftime v1.0.0
=== minio-contract ===
minio contract: arm64, minio RELEASE.2025-09-07T16-13-09Z, mc RELEASE.2025-08-13T08-35-41Z
  minio: downloading https://github.com/minio/minio/releases/download/RELEASE.2025-09-07T16-13-09Z/minio.linux-arm64.RELEASE.2025-09-07T16-13-09Z
  minio: downloaded, digest verified
  mc: downloading https://github.com/minio/mc/releases/download/RELEASE.2025-08-13T08-35-41Z/mc.linux-arm64.RELEASE.2025-08-13T08-35-41Z
  mc: downloaded, 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.023s
=== RUN   TestT4StorageProbeMinio
2026/10/12 10:48:57 moves: T4: STORE: list: operation error S3: ListObjectsV2, https response error StatusCode: 403, RequestID: 18DDC28D116649A0, 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.01s)
PASS
ok  	github.com/carlosframework/platform/internal/host	0.021s
minio contract: PASS
diff --git a/internal/cgroup/cgroup_test.go b/internal/cgroup/cgroup_test.go
index 9a8bd0521..bd3706473 100644
--- a/internal/cgroup/cgroup_test.go
+++ b/internal/cgroup/cgroup_test.go
@@ -53,8 +53,16 @@ func TestParseSelfCgroup(t *testing.T) {
// control files systemd chowns to the unit's user when Delegate= is set.
// Everything the dance does is plain file I/O, so a temp dir is a faithful
// fixture — the same seam listCarlosUnits uses in internal/host.
+// forgetKills clears the process-wide kill marks (Linux builds set it). The
+// marks are keyed by (device, inode), so a mark an earlier test left would
+// name a fresh test directory on a filesystem that reuses inodes (ext4,
+// xfs), and the start would remove a cgroup that never took a kill.
+// cgroupfs does not reuse cgroup inode ids; a temp dir can.
+var forgetKills = func() {}
+
func newDelegatedRoot(t *testing.T) string {
t.Helper()
+ forgetKills()
root := t.TempDir()
for _, f := range []string{"cgroup.procs", "cgroup.subtree_control"} {
if err := os.WriteFile(filepath.Join(root, f), nil, 0o644); err != nil {
diff --git a/internal/cgroup/recreate_linux_test.go b/internal/cgroup/recreate_linux_test.go
index 6962bb7cb..598cd9b63 100644
--- a/internal/cgroup/recreate_linux_test.go
+++ b/internal/cgroup/recreate_linux_test.go
@@ -17,7 +17,7 @@ import (
// amadan #272: once cgroup.kill has been written to a cgroup, every later
// clone3(CLONE_INTO_CGROUP) into that same cgroup is SIGKILLed at once. The
// fixture has no kernel behind it, so these tests pin the bookkeeping: a
-// cgroup that took a kill is removed and made fresh (a new inode, its limit
+// cgroup that took a kill is removed and made fresh (its sentinel gone, its limit
// files written again) before the next spawn into its path, and a removal
// that fails refuses the spawn.
@@ -57,20 +57,39 @@ func fakeCgroupRmdir(t *testing.T, fail *error) *[]string {
// temp dir can never meet one.
func forgetKillsAfter(t *testing.T) {
t.Helper()
- t.Cleanup(func() {
+ t.Cleanup(forgetKills)
+}
+
+func init() {
+ forgetKills = func() {
killedCgroups.Lock()
clear(killedCgroups.ids)
killedCgroups.Unlock()
- })
+ }
}
-func inodeOf(t *testing.T, path string) uint64 {
+// sentinelName is a file production code never creates in a cgroup. A test
+// plants it in a directory to learn later whether that very directory was
+// removed and made again (the sentinel is gone) or left alone (it is still
+// there). Freshness is proved this way, not by a changed inode number:
+// ext4 and xfs hand a directory's inode straight back after rmdir+mkdir,
+// so an inode comparison passes or fails by filesystem. Real cgroupfs does
+// not reuse cgroup inode ids, which is why the production marks can key on
+// identity.
+const sentinelName = "test-sentinel"
+
+func plantSentinel(t *testing.T, dir string) {
t.Helper()
- var st unix.Stat_t
- if err := unix.Lstat(path, &st); err != nil {
- t.Fatalf("stat %s: %v", path, err)
+ writeFixture(t, filepath.Join(dir, sentinelName), "planted")
+}
+
+func hasSentinel(t *testing.T, dir string) bool {
+ t.Helper()
+ _, err := os.Lstat(filepath.Join(dir, sentinelName))
+ if err != nil && !errors.Is(err, os.ErrNotExist) {
+ t.Fatalf("stat sentinel in %s: %v", dir, err)
}
- return st.Ino
+ return err == nil
}
// killFixture gives dir the files a cleanup handle reads, empty, and writes
@@ -121,8 +140,8 @@ func TestAKilledHostCgroupIsRecreatedBeforeTheNextStart(t *testing.T) {
if len(*removed) != 0 {
t.Fatalf("starts that followed no kill removed %v", *removed)
}
- before := inodeOf(t, dir)
killFixture(t, m, host, "", dir)
+ plantSentinel(t, dir)
// A limit file left behind would say nothing about the recreation;
// scribble on it so only a fresh write can restore it.
writeFixture(t, filepath.Join(dir, "memory.max"), "max")
@@ -135,8 +154,8 @@ func TestAKilledHostCgroupIsRecreatedBeforeTheNextStart(t *testing.T) {
if len(*removed) != 1 || (*removed)[0] != dir {
t.Fatalf("removed %v, want only %s", *removed, dir)
}
- if after := inodeOf(t, dir); after == before {
- t.Fatalf("the respawn reused the killed cgroup (inode %d)", before)
+ if hasSentinel(t, dir) {
+ t.Fatal("the respawn reused the killed cgroup (the sentinel planted in it survived)")
}
for file, want := range map[string]string{
"memory.max": "536870912",
@@ -175,8 +194,8 @@ func TestAKilledCgroupThatCannotBeRemovedRefusesTheNextStart(t *testing.T) {
if _, err := m.Start(host, TierDefault, buildTrue); err != nil {
t.Fatal(err)
}
- before := inodeOf(t, dir)
killFixture(t, m, host, "", dir)
+ plantSentinel(t, dir)
built := 0
cmd, err := m.Start(host, TierDefault, func() *exec.Cmd { built++; return buildTrue() })
if !errors.Is(err, ErrKilledCgroupKept) || !errors.Is(err, unix.EBUSY) || cmd != nil {
@@ -185,7 +204,7 @@ func TestAKilledCgroupThatCannotBeRemovedRefusesTheNextStart(t *testing.T) {
if built != 0 || len(*calls) != 1 {
t.Fatalf("the refused start built %d command(s) and started %d time(s) in all, want 0 and 1", built, len(*calls))
}
- if inodeOf(t, dir) != before {
+ if !hasSentinel(t, dir) {
t.Fatal("the refused start replaced the cgroup")
}
// Still refused while it will not go: the mark outlives the failure.
@@ -196,7 +215,7 @@ func TestAKilledCgroupThatCannotBeRemovedRefusesTheNextStart(t *testing.T) {
if _, err := m.Start(host, TierDefault, buildTrue); err != nil {
t.Fatal(err)
}
- if len(*removed) != 1 || inodeOf(t, dir) == before {
+ if len(*removed) != 1 || hasSentinel(t, dir) {
t.Fatalf("removed %v; the start after the removal succeeded must be in a fresh cgroup", *removed)
}
}
@@ -215,8 +234,9 @@ func TestAKilledRoleSidecarIsRecreatedUnderTheSameParent(t *testing.T) {
if _, err := m.StartChild(host, child, TierDefault, Limit{}, buildTrue); err != nil {
t.Fatal(err)
}
- parentInode, childInode := inodeOf(t, parent), inodeOf(t, dir)
killFixture(t, m, host, child, dir)
+ plantSentinel(t, dir)
+ plantSentinel(t, parent)
writeFixture(t, filepath.Join(parent, "memory.max"), "1073741824") // a raise the respawn must not undo
writeFixture(t, filepath.Join(parent, "cgroup.events"), "populated 1\n")
if _, err := m.StartChild(host, child, TierDefault, Limit{}, buildTrue); err != nil {
@@ -225,10 +245,10 @@ func TestAKilledRoleSidecarIsRecreatedUnderTheSameParent(t *testing.T) {
if len(*removed) != 1 || (*removed)[0] != dir {
t.Fatalf("removed %v, want only %s", *removed, dir)
}
- if inodeOf(t, dir) == childInode {
+ if hasSentinel(t, dir) {
t.Fatal("the sidecar respawn reused the killed role child")
}
- if inodeOf(t, parent) != parentInode {
+ if !hasSentinel(t, parent) {
t.Fatal("the sidecar respawn replaced the parent")
}
if got := readFixture(t, filepath.Join(parent, "memory.max")); got != "1073741824" {
@@ -256,8 +276,10 @@ func TestAKilledParentIsRecreatedWithItsChildren(t *testing.T) {
if _, err := m.StartChild(host, "g4", TierDefault, TierDefault, buildTrue); err != nil {
t.Fatal(err)
}
- parentInode := inodeOf(t, parent)
killFixture(t, m, host, "", parent)
+ plantSentinel(t, parent)
+ plantSentinel(t, filepath.Join(parent, sidecar))
+ plantSentinel(t, filepath.Join(parent, "g4"))
// The fixture's parent would lose its controller list with it; the
// kernel lists them on every new cgroup, so the fake rmdir's recreation
// needs it back.
@@ -282,7 +304,7 @@ func TestAKilledParentIsRecreatedWithItsChildren(t *testing.T) {
if len(*removed) != 3 || !want[(*removed)[0]] || !want[(*removed)[1]] || (*removed)[2] != parent {
t.Fatalf("removed %v, want both children then %s", *removed, parent)
}
- if inodeOf(t, parent) == parentInode {
+ if hasSentinel(t, parent) {
t.Fatal("the respawn reused the killed parent")
}
if _, err := os.Stat(filepath.Join(parent, "g4")); !errors.Is(err, os.ErrNotExist) {
@@ -451,8 +473,9 @@ func TestARetainingSetupMakesEmptyLeftoversFresh(t *testing.T) {
// child a KillSidecar left.
live := retainedTree(t, root, "live.example.com", "populated 1\n", "", map[string]string{"g3": "populated 1\n", "role-sidecar": "populated 0\n"})
claim := retainedTree(t, root, "claim-0123abcd.local", "populated 0\n", "", nil)
- before := map[string]uint64{empty: inodeOf(t, empty), busy: inodeOf(t, busy), live: inodeOf(t, live),
- filepath.Join(live, "g3"): inodeOf(t, filepath.Join(live, "g3")), filepath.Join(live, "role-sidecar"): inodeOf(t, filepath.Join(live, "role-sidecar"))}
+ for _, d := range []string{empty, busy, live, filepath.Join(live, "g3"), filepath.Join(live, "role-sidecar")} {
+ plantSentinel(t, d)
+ }
m, err := SetupAtRetaining(root, t.Logf)
if err != nil {
t.Fatal(err)
@@ -463,7 +486,7 @@ func TestARetainingSetupMakesEmptyLeftoversFresh(t *testing.T) {
if _, err := m.Start("worker.example.com", TierDefault, buildTrue); err != nil {
t.Fatal(err)
}
- if inodeOf(t, empty) == before[empty] {
+ if hasSentinel(t, empty) {
t.Fatal("the first start reused an empty retained host cgroup")
}
if got := readFixture(t, filepath.Join(empty, "memory.max")); got != "536870912" {
@@ -473,17 +496,17 @@ func TestARetainingSetupMakesEmptyLeftoversFresh(t *testing.T) {
if _, err := m.Start("busy.example.com", TierDefault, buildTrue); err != nil {
t.Fatal(err)
}
- if inodeOf(t, busy) != before[busy] {
+ if !hasSentinel(t, busy) {
t.Fatal("a retained cgroup holding a process was replaced")
}
if _, err := m.StartChild("live.example.com", RoleChild("sidecar"), TierDefault, Limit{}, buildTrue); err != nil {
t.Fatal(err)
}
- if inodeOf(t, filepath.Join(live, "role-sidecar")) == before[filepath.Join(live, "role-sidecar")] {
+ if hasSentinel(t, filepath.Join(live, "role-sidecar")) {
t.Fatal("the sidecar respawn reused an empty retained role child")
}
- if inodeOf(t, live) != before[live] || inodeOf(t, filepath.Join(live, "g3")) != before[filepath.Join(live, "g3")] {
+ if !hasSentinel(t, live) || !hasSentinel(t, filepath.Join(live, "g3")) {
t.Fatal("a populated parent or its live generation was replaced")
}