Skip to content

review round and refactor - #28

Merged
CMGS merged 24 commits into
mainfrom
review/whole-repo-2026-09-21
Sep 21, 2026
Merged

CMGS merged 24 commits into
mainfrom
review/whole-repo-2026-09-21

Conversation

@CMGS

@CMGS CMGS commented Sep 20, 2026 •

Copy link
Copy Markdown
Contributor

Whole-repo review round, 2026-09-21. Every production file read in full; the DHCP/ENI flow lens and a test lens run by reader agents, every finding adjudicated against the source; Linux-gated paths verified in a linux/arm64 container.

Commits

  • fdb1a5f review: path.Base, strings.CutLast and time.RFC3339 for three renderings
  • 98c1882 test: drop the shortfall test that never reached provision.go
  • bd82bc1 fix: delete an orphan ENI on a detached context and stop the create loop on cancel
  • a075c30 fix: teardown keeps the detach errors a cut propagation wait would drop
  • bdfbc69 fix: bound the wait on a canceled subprocess's pipe
  • 43813dc fix: order a RELEASE against the REQUEST it raced by receive time
  • 68a70da fix: name the lease-file outcome the daemon starts from
  • efe4a1e fix: reject a --dns entry that is not an IPv4 address
  • 616e237 docs: --dns takes IPv4 entries, lease-file startup outcomes, an overtaken RELEASE
  • 990c3de cut: one nic0 alias update for assign and teardown
  • 8daca2d fix: an ENI attached before a cut propagation wait stays in the result and in pool.json
  • 16964f5 fix: a failed init merges the attached ENIs into the existing pool state
  • 50ed1d3 fix: the daemon re-applies the GKE guest-agent route fix on every start
  • 99794e4 fix: teardown refuses while the daemon holds the pidfile
  • e12661f fix: the daemon keeps serving when a secondary NIC is missing
  • 3a5a9de review: drop two err shadows and clean the pidfile path
  • bf5eddd test: the cut-wait ENI test cancels after the attach instead of racing a deadline
  • 0f1371a review: name the local logger in the two single-call warn sites
  • a1ca4ae review: the gauge's help string already carries the secondary NIC godoc
  • ad7469c fix: the bridge and the CNI veths follow the primary NIC MTU
  • 306cd6f docs: the conflist mtu follows the primary NIC and invalidates old FC snapshots
  • 4656ebb docs: a snapshot after an MTU change needs the source VM recreated first
  • a944b1e docs: simplify README navigation and section order
  • aebaa6e fix: init without --pool-size gets its own 140 default, not adopt's 253

Findings

# where claim outcome
1 platform/volcengine/eni.go the orphan-ENI delete on the attach-failure path reused the canceled context, so a SIGTERM during init leaked a detached ENI that teardown never sees; a canceled attach was also swallowed into continue and reported as a provisioning shortfall fixed: the delete runs on a detached, bounded context; the create loop returns ctx.Err(); TestEnsureENIsDeletesTheOrphanWhenTheAttachIsCanceled
2 dhcp/release.go handleRelease validated the packet outside lifecycleMu and freed whatever the MAC held at lock time, so a RELEASE/REQUEST pair from one guest could free the lease the REQUEST had just ACKed; a first fix compared handler-start time to grant-commit time, which Codex showed still misorders under goroutine scheduling fixed: the server owns the receive loop and stamps every packet at ReadFrom (keeping server4's broadcast reply for an unspecified source); the stamp becomes the lease's grant time, orders a RELEASE against the REQUEST by receive time, and a REQUEST older than the current grant is ignored so the grant never moves backwards; explicit-stamp tests plus a loop test over loopback UDP
3 platform/volcengine/teardown.go the propagation-wait arm returned only the context error, dropping the detach failures accumulated so far fixed: errors.Join; TestTeardownKeepsTheDetachErrorsACutWaitWouldDrop
4 platform/subprocess.go no WaitDelay, so a canceled gcloud/ve call blocked on a grandchild holding the pipe fixed: WaitDelay 2 s; TestRunSubprocessReturnsWhenAGrandchildHoldsThePipe
5 dhcp/server.go one warning covered both the benign first start and a lost lease table fixed: missing file at Info, any other load error at Error
6 cmd/utils.go a --dns typo was persisted silently and VMs got one resolver fixed: entries must be IPv4 or init/adopt fail
7 node/node.go one detached secondary ENI failed node.Setup, so the daemon crash-looped and every VM on the node lost DHCP, not only the ones behind the missing NIC fixed (#23): usableSecondaryNICs sets up the NICs that are present, warns per missing NIC and publishes cocoon_net_secondary_nics{state=expected|present}; only an all-missing set still fails startup; TestUsableSecondaryNICsKeepsThePresentOnes
8 node/bridge_linux.go, node/node.go cni0 and every veth defaulted to 1500 while the GCE VPC MTU is 1460, so a guest could emit frames the egress NIC cannot carry; the nodes only worked because every runbook runs cocoon-check --fix first, which installs the mangle FORWARD TCPMSS --clamp-mss-to-pmtu rule (cocoon #135, the July GCP incident) fixed (#24, scoped to cocoon-net by the owner): Setup reads the primary NIC MTU, creates or resets cni0 to it and writes mtu into the conflist; cocoon syncs the TAP to the veth and the hypervisor advertises the TAP MTU to the guest, so no DHCP option is needed; TestCNIConflistCarriesThePrimaryNICMTU, TestLinkMTUReadsTheLoopback, TestSetupBridgeFollowsTheMTU (Linux, under CAP_NET_ADMIN); the docs state that a changed node MTU invalidates Firecracker snapshots captured at the old value, since clone and restore reject a target network MTU that differs from the snapshot's, and that the source VM must be recreated after the daemon restart before the snapshot is captured again (a running VM keeps its old veth and TAP)
9 platform/volcengine/eni.go, provision.go, cmd/init.go a cancel during the attach-propagation wait dropped the ENI just attached from the result, ProvisionNetwork discarded the result on error and the seed pool.json kept an empty eniIDs, so the next teardown fell back to deleting every non-primary ENI on the instance fixed: the ENI is recorded before the wait, the attached ids ride with the error, init persists them before failing; TestEnsureENIsKeepsAnAttachedENIWhenTheWaitIsCut (now cancel-driven: the ve stub marks the Attach call, the test cancels after it and fails only when the ENI is neither in the result nor deleted); Codex then showed the first cut of this fix rewrote a working node's pool.json from the seed on a retried init, so the attached ids are merged into the loaded state (mergeENIIDs, TestMergeENIIDsKeepsRecordedAndAddsNew) and every post-attach error carries them (closes #25)
10 cmd/daemon.go a GKE node reboot re-installed the guest-agent route policy while state.json survived, so the daemon came back without the route fix and VM egress stayed broken until a manual adopt fixed (#26): on GKE the daemon calls gke.Adopt with the persisted node name, subnet, gateway and primary NIC before node.Setup; the fix is idempotent, so a healthy node sees no change
11 cmd/teardown.go teardown deleted the pool state and leases while the daemon still served them fixed (#27): teardown checks the pidfile after the dry-run branch and returns "stop it first or pass --force"; --force skips the check; TestTeardownForceFlag, TestCheckExistingPIDRefusesALiveDaemon
12 platform/volcengine/eni.go reusable-ENI subnet reuse of any attached non-primary ENI documented contract (docs/volcengine.md), no change
13 cmd/utils.go, cmd/init.go, cmd/adopt.go both subcommands bound --pool-size to one package variable and pflag writes each default into it at registration, so building the root command (init then adopt) left 253 in place and a bare GKE init provisioned 253 addresses while its help said 140 (since c25d54c; raised by the owner's independent Codex pass) fixed: the flag uses the FlagSet's own storage and runInit/runAdopt read it; TestInitAndAdoptKeepTheirOwnPoolSizeDefaults builds the real root command and failed on the previous head with init pool-size = 253, <nil>, want 140

Docs: --dns row and help string, the lease-file startup outcomes, and the overtaken-RELEASE rule in docs/dhcp.md; the cocoon_net_secondary_nics metric and the --force flag in docs/configuration.md; the missing-ENI troubleshooting row in docs/volcengine.md; the daemon's route re-apply in docs/gke.md; teardown's refusal while the daemon runs in docs/dhcp.md; the conflist mtu rule and the snapshot re-capture note in docs/architecture.md and docs/gke.md.

Cut-list (owner decided 2026-09-21)

candidate est. status
platform/volcengine/utils.go sleepCtx → commonk8s.SleepCtx −12 rejected on inspection: cocoon-common/k8s imports client-go, controller-runtime, clientcmd and record, and this daemon has no k8s.io dependency today; twelve lines do not buy that
platform/gke/gcloud.go + teardown.go: one updateNic0Aliases helper for the two identical network-interfaces update --aliases invocations −6 applied (cut: commit, −4 net after the helper)
dhcp/lease.go add() other-MAC eviction branch + the request.go log arm −8 verified unreachable through the handlers (isLeasedToOther ignores expired leases but the pool slot stays used until the sweep, which deletes the lease and frees the slot in one locked transaction), yet TestLeaseStore_AddOtherMACSameIP and …Expired pin it as the store's own invariant with a stated reason (the sweep must never reclaim the new holder's IP); a cut would have to delete those tests, so it stays

LOC (same counting on both ends)

  • cocoon-net origin/main=3cc2885: prod=3855 test=1491 comments=142 blanks=541 effective=3172
  • cocoon-net HEAD=aebaa6e: prod=4032 test=1992 comments=142 blanks=553 effective=3337

Per commit (prod adds/dels · test adds/dels): review 7/9; test drop 0/27; orphan ENI 12/10 · 38/0; teardown 1/1 · 26/0; WaitDelay 4/0 · 26/0; RELEASE ordering (incl. the receive loop) see the commit stat; lease-file log 7/3; --dns 19/3 · 26/0; docs 1/1; cut 13/17; ENI result 13/6 · 34/0; ENI merge 18/5 · 10/0; route re-apply 6/0; teardown refusal 10/3 · 31/1; missing NIC 49/15 · 19/0; lint follow-up 3/3; cancel-driven test 0/0 · 22/8; logger names 4/2; godoc drop 0/1; MTU 53/24 · 67/0; pool-size default 11/4 · 14/0. Production comment lines end where they started (142).

Gates

  • GOWORK=off make fmt-check, GOWORK=off make lint on GOOS=darwin and GOOS=linux: 0 issues.
  • asl ./... on both GOOS: 0 findings.
  • go test -race -count=1 ./...: green (seven packages).
  • linux/arm64 golang:1.27 container on 306cd6f and again on aebaa6e with --cap-add=NET_ADMIN: go build ./... && go vet ./... && go test -race -count=1 ./... green (seven packages); TestSetupBridgeFollowsTheMTU runs there and was mutation-checked (without the LinkSetMTU branch: cni0 mtu = 1500 after setupBridge(1460)).
  • Every fix commit carries a regression test that was mutation-checked: reverting the fix makes the test fail for the stated reason.
  • Fix, test and docs commits add zero comment lines.

Codex

Round 1 on the committed diff raised the RELEASE-ordering residual (handler-start vs receive time) and three timing-dependent tests; round 2 raised the lost broadcast reply for an unspecified source, a backwards grant on a reordered REQUEST pair, and weak ordering tests. All were folded into their commits. Round 3 on dde9b7a: "No blockers: this diff converges." An independent re-read then raised the WaitDelay success-path regression, an import shadow and a coverage gap, all folded in; round 4 on 616e237: "No blockers: this diff converges." Round 5 on the ENI-persistence fix raised the seed-overwrite regression and the partial-ids gap on later errors; round 6 on 16964f5: "No blockers: this diff converges." Round 7 on bf5eddd (the #23/#26/#27 fixes, the lint follow-up and the cancel-driven test): "No blockers: this diff converges." Round 8 on a1ca4ae (the two review commits): "No blockers: this diff converges." Round 9 on 306cd6f (the MTU fix): "No blockers: this diff converges." Round 10 on 4656ebb (the re-capture wording): "No blockers: this diff converges." Round 11 on aebaa6e (the pool-size default): "No blockers: this diff converges."

Follow-ups

Closes #23, #24, #25, #26, #27.

CMGS added 11 commits September 21, 2026 02:34
The GCE zone and subnetwork URLs end in the segment path.Base returns,
the region is the zone without its last dash suffix, and the status
timestamp is UTC so RFC3339 prints the same string as the literal layout.
It rebuilt the fixture and recomputed ipsPerENI minus the count itself,
so no change to the shortfall arithmetic in ProvisionNetwork could fail it.
…oop on cancel

The attach-failure cleanup ran DeleteNetworkInterface on the same context
whose cancellation had killed the attach, so a SIGTERM during init left an
unattached ENI that teardown's attached-only listing never finds. Both
orphan deletes now share the detached, time-bounded path the create arm
already used, and a canceled attach returns ctx.Err() instead of reporting
the shortfall as "no secondary IPs assigned".
CommandContext kills gcloud or ve, but CombinedOutput waited for the pipe
to reach EOF; a helper process that inherited it kept init hanging after
the operator's signal.
handleRelease validated the packet outside lifecycleMu and then freed
whatever the MAC held at lock time, so a RELEASE/REQUEST pair from one
guest could free the lease the REQUEST had just ACKed. The server now
stamps every packet when it is read from the socket, records that stamp
as the lease's grant time, validates the RELEASE under the lock, and
drops a RELEASE received before the REQUEST that holds the lease; one
received after it is honored even when the two overlapped in flight.
A missing file is the normal first start; an unreadable one was logged with
the same 'starting fresh' warning and every later REQUEST then failed to
persist, with nothing pointing at the file.
init and adopt persisted the raw list and the daemon silently dropped the
entry it could not parse, so a typo cost the VMs a resolver with no error.
…aken RELEASE

The --dns help string now names the IPv4 requirement the flag enforces,
so the binary and the configuration page agree.
Both sites built the same six-argument gcloud network-interfaces update
and differed only in the error text, which stays at the call sites.
…t and in pool.json

ensureENIs appended to its result only after the attach-propagation
sleep, so a cancel inside that window returned a result without the ENI
it had just attached, ProvisionNetwork discarded the result on error, and
the seed pool.json kept an empty eniIDs; the next teardown then fell back
to deleting every non-primary ENI on the instance. The ENI is recorded
before the wait, ProvisionNetwork returns the attached ids with its error,
and init persists them before it fails.
Persisting the partial result rewrote pool.json from the fresh seed even
when a working state existed, so a retried init that hit an ENI create
failure erased the node's IPs, NICs and gateway. The attached ids are now
merged into the loaded state (or the seed on a first run), and
ProvisionNetwork carries them with every error raised after the attach.
Only init and adopt removed the guest agent's local alias route and
installed the boot cron, so after a restart the cron raced the guest
agent's own programming and the route could come back. The daemon now
runs the GKE adopt step before node setup; Volcengine has nothing to do,
so its platform is not constructed.
Removing the cloud allocation under a live DHCP server left every later
lease unroutable with nothing telling the operator; teardown now checks
the pidfile the way the daemon does and stops unless --force is passed.
node.Setup failed on the first pool NIC without a link, so one ENI
detached out of band crash-looped the daemon and took every VM on the
node offline. Setup now uses the NICs that are present, warns per missing
one, fails only when none is present, and reports expected versus present
through cocoon_net_secondary_nics so the degraded state is visible.
On GCE the VPC MTU is 1460 while cni0 and every veth defaulted to 1500, so
guests could emit frames the egress NIC cannot carry and the node relied on
the doctor's TCPMSS clamp. Setup now reads the primary NIC MTU, creates or
resets cni0 to it and writes it into the conflist; cocoon syncs the TAP to the
veth and the hypervisor advertises the TAP MTU to the guest.
@CMGS CMGS changed the title Whole-repo review round 2026-09-21 review round and refactor Sep 21, 2026
Both subcommands bound --pool-size to one package variable, and pflag writes
the default into that variable at registration, so building the root command
left the last registration's 253 in place and a bare init provisioned a GKE
pool of 253 addresses while its help said 140. Each FlagSet now owns its
storage and the RunE reads it.
@CMGS
CMGS merged commit a789d55 into main Sep 21, 2026
2 checks passed
@CMGS
CMGS deleted the review/whole-repo-2026-09-21 branch September 21, 2026 06:05
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

1 participant