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>
2 files changed,
+56
−25
internal/cgroup/cgroup_test.go+8 −0internal/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") |
| } |