Skip to content

feat(hibernate): release the DHCP lease pre-snapshot; renew-nudge stalled wakes in-window#39

Merged
CMGS merged 8 commits into
mainfrom
feat/hibernate-dhcp-release
Jul 19, 2026
Merged

feat(hibernate): release the DHCP lease pre-snapshot; renew-nudge stalled wakes in-window#39
CMGS merged 8 commits into
mainfrom
feat/hibernate-dhcp-release

Conversation

@tonicmuroq

@tonicmuroq tonicmuroq commented Jul 18, 2026

Copy link
Copy Markdown
Contributor

The failure class this kills

Hibernate freezes the guest with its DHCP lease still bound, and never tells cocoon-net — the server-side lease (24h) keeps squatting under the old MAC. Wake restores onto a fresh NIC with a new MAC, so the resumed Windows REQUESTs its cached IP and cocoon-net must NAK it (leased to another client — the VM's own previous MAC). From there:

  • 97%: win11 re-DISCOVERs within 1s, wake succeeds with a new IP — invisible, which is why this survived in production (measured on one node over a week: 66 NAKs, 64 recovered).
  • ~3%: win11 wedges — logs Dhcp-Client event 1002, then goes permanently silent on APIPA (no re-DISCOVER, no documented 5-min background retry; violates both RFC 2131 and Microsoft's own docs; two multilingual research sweeps found no public report or fix). The 45s lease wait times out into lifecycle=failed.

Four production machines hit this in one morning (updater's short-cycle wake sweeps roll the 3% dice constantly); all were revived by a single manual ipconfig /renew, which is what motivated the design.

What this PR does (two things, CH+Windows-gated, best-effort)

Mirrors VMware Tools' default power scripts — release on suspend, renew on resume (@%SYSTEMROOT%\system32\ipconfig /release / /renew, shipped as defaults for decades):

  1. hibernate(): ipconfig /release before the NetResize(0) NIC drop. Server lease frees instantly (also ends the 24h zombie-lease pool depletion), and the snapshot freezes a guest with no cached lease → restored clones DISCOVER cleanly; the NAK precondition is gone. rollbackHibernateNIC renews after re-adding the NIC since the lease is already released. A sick guest (dead agent) still hibernates — release failure is logged and skipped.
  2. waitForFreshIP(): one-shot ipconfig /renew nudge at 30s (tunable) still lease-less, inside the unchanged 45s budget. The deadline stays the external contract: the wake either completes in time or fails for real — no post-deadline recovery machinery. The nudge unsticks the APIPA wedge and covers every pre-existing :hibernate snapshot that still carries a cached lease (each gets one more risky wake during the transition). Guarded three ways: never once a lease is observed, never for Linux guests (markReadyAfterIP shares this wait), never more than once.

Observability

  • cocoon_vk_hibernate_total gains phase=dhcp_release
  • new cocoon_vk_wake_renew_nudge_total{result} — should trend to zero as the fleet's snapshots roll over to post-release generation
  • docs/metrics.md updated

Tests

8 cases pin the behavior: release exactly once before NIC drop; release failure doesn't block hibernate; Linux never gets ipconfig; rollback renews; nudge fires exactly once when lease-less; nudge never fires when a lease exists (even with a 1ns nudge window); nudge never fires for Linux. Full suite green.

Verification plan

Canary on one node: hibernate a test VM (expect RELEASE in cocoon-net journal + lease gone from leases.json), wake it (first wake of a legacy snapshot may still NAK→self-heal; second-generation wakes should be NAK-free DISCOVERs).

@tonicmuroq
tonicmuroq force-pushed the feat/hibernate-dhcp-release branch 4 times, most recently from 2c76ae2 to fb69a13 Compare July 18, 2026 03:28
@tonicmuroq tonicmuroq changed the title feat(hibernate): release DHCP pre-snapshot, renew-nudge stalled wakes, self-heal failed lifecycle feat(hibernate): release the DHCP lease pre-snapshot; renew-nudge stalled wakes in-window Jul 18, 2026
…lled wakes in-window

Hibernate snapshots freeze the guest with its DHCP lease still bound while
the server-side lease (24h) keeps squatting under the old MAC. The restored
clone re-DHCPs on a fresh NIC/MAC, so any wake within the lease lifetime
REQUESTs the cached IP and the server must NAK it ("leased to another
client" — the VM's own previous MAC). win11's DHCP client then wedges on
APIPA ~3% of the time (event 1002, then no re-DISCOVER, no background
retry — measured fleet-wide: 66 NAKs/64 recovered/2 wedged in one week)
and the 45s lease wait times out into lifecycle=failed.

Two changes, both CH+Windows-gated and best-effort, mirroring VMware
Tools' default power scripts (release on suspend, renew on resume):

- hibernate: exec `ipconfig /release` over the agent channel before the
  NetResize(0) NIC drop. Frees the server lease immediately (no 24h zombie
  pool slots) and freezes a guest with no cached lease, so restored clones
  DISCOVER instead of triggering the NAK path. rollbackHibernateNIC renews
  after re-adding the NIC since the lease was already released.
- waitForFreshIP: one-shot `ipconfig /renew` nudge at 15s (tunable) still
  lease-less, inside the same 45s budget — the wake either completes in
  time or fails for real; the deadline stays the external contract.
  Unsticks the APIPA wedge (4/4 in incident response) and covers
  pre-existing snapshots that still carry cached leases. Never fires once
  a lease is observed, never fires for Linux guests.

New metrics: hibernate_total gains phase=dhcp_release; new
wake_renew_nudge_total{result}. docs/metrics.md updated.
@tonicmuroq
tonicmuroq force-pushed the feat/hibernate-dhcp-release branch from fb69a13 to 7d254cf Compare July 18, 2026 03:35
CMGS added 7 commits July 19, 2026 12:23
The release-then-drop sequence compensated every later failure through
rollbackHibernateNIC, except the drop itself failing: that branch returned
with the guest lease-less and its NIC up, and Windows never re-DISCOVERs
after an explicit release. Renew before surfacing the error; extracting
dropNICForHibernate keeps the branch flat.
…ten comments

waitForFreshIP re-derived the guest OS through a lock and map lookup that
could race pod tracking mid-wait; both callers already hold the pod, so
take it as the parameter and drop isWindowsGuest. Fold the nudge tests
onto newDropNICWakeFixture and trim multi-line comments to their one
load-bearing constraint.
…IP waiter to its VMID

An exec error on the release does not prove the guest skipped it — a vsock
timeout can land after the command ran — so gating the compensation renew
on a confirmed release could leave a live VM lease-less. Renew regardless;
renewing a still-bound adapter is benign.

waitForFreshIP resolved the VM by pod key each round, so a same-name pod
recreate mid-wait handed the waiter the successor VM: the renew nudge and
the caller's refresh/notify all sit outside the VMID lifecycle guard. The
waiter now carries the VMID it was armed for and bails on mismatch.
… the lease write-back by VMID

The drop can fail precisely because ctx died, and the compensation ran on
that same dead context — exec.CommandContext then refuses it outright.
Detach it with context.WithoutCancel, bounded by the exec's own timeout;
the fake runtime now honors ctx like the real one so the case is testable.

resolveVMIP looked the lease up for v's MAC but setVMIP wrote it back by
pod key unconditionally, so a same-name recreate landing inside the lookup
put the old MAC's IP on the successor VM with nothing to ever correct it.
setVMIP now writes only when the tracked VMID still matches, and the
lease is returned only after the write lands.
SnapshotSave, push and remove can fail precisely because ctx died, and
rollbackHibernateNIC then ran its NetResize(1) and renew on that dead
context, leaving an online VM NIC-less. The whole rollback now runs under
one cancel-detached, bounded lifetime; the fake runtime's NetResize honors
ctx like the real CommandContext so the case stays pinned.
Rooted in context.Background, lifecycle goroutines outlived SIGTERM
whenever Close was bypassed; buildProvider already holds the signal
context, so derive from it and let cancellation propagate.
…-NIC comments

The unconditional-renew rationale lived twice (godoc and call site); it now
lives once at the call it explains. newLeaseParser replaces the leases.json
boilerplate duplicated across two tests. The swap test's nudge delay and the
cancelled-ctx test's netResizeErr never influenced their runs.
@CMGS
CMGS merged commit 0d6bb37 into main Jul 19, 2026
2 checks passed
@CMGS
CMGS deleted the feat/hibernate-dhcp-release branch July 19, 2026 06:58
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants