Skip to content

feat: add active ballooning reclaim controller#160

Open
sjmiller609 wants to merge 20 commits intomainfrom
codex/active-ballooning
Open

feat: add active ballooning reclaim controller#160
sjmiller609 wants to merge 20 commits intomainfrom
codex/active-ballooning

Conversation

@sjmiller609
Copy link
Collaborator

@sjmiller609 sjmiller609 commented Mar 19, 2026

Summary

  • add a host-side active ballooning controller in lib/guestmemory with pressure sampling, proportional reclaim, protected floors, and manual reclaim holds
  • expose POST /resources/memory/reclaim, wire the controller through the API startup path, and document/configure the new hypervisor.memory.active_ballooning settings
  • extend the existing guest-memory integration tests for Cloud Hypervisor, QEMU, Firecracker, and VZ to validate runtime balloon targets and manual reclaim flows

Validation

  • go test ./lib/guestmemory -count=1
  • go test ./cmd/api/api -run 'TestReclaimMemory_' -count=1
  • make test-guestmemory-vz
  • make test-guestmemory-linux on deft-kernel-dev from /home/sjmiller609/codex-active-ballooning-plan/hypeman

Note

High Risk
Introduces a new background control loop that actively adjusts VM balloon targets and extends the core hypervisor.Hypervisor interface across all backends, which can affect VM stability and host memory behavior if misconfigured or buggy.

Overview
Adds an active guest-memory ballooning controller (lib/guestmemory) that samples host pressure (Linux /proc + PSI, macOS vm_stat/memory_pressure), computes reclaim targets with hysteresis and protected floors, and applies per-VM balloon target changes with rate limits plus metrics/tracing/logging.

Exposes manual reclaim via POST /resources/memory/reclaim (new API handler + tests), wires a GuestMemoryController through DI and API startup, and adds config surface hypervisor.memory.active_ballooning with defaults, validation, and example YAML updates.

Extends hypervisor backends (Cloud Hypervisor, QEMU, Firecracker, VZ/vz-shim) with runtime balloon Get/SetTargetGuestMemoryBytes, adds socket-based cache keys and Linux PID resolution to stabilize runtime control, and updates guest-memory integration tests/Makefile targets for better determinism and CI robustness.

Written by Cursor Bugbot for commit 400c9c7. This will update automatically on new commits. Configure here.

@github-actions
Copy link

github-actions bot commented Mar 19, 2026

✱ Stainless preview builds

This PR will update the hypeman SDKs with the following commit message.

feat: add active ballooning reclaim controller

Edit this comment to update it. It will appear in the SDK's changelogs.

hypeman-openapi studio · code · diff

Your SDK build had at least one "note" diagnostic, but this did not represent a regression.
generate ✅

hypeman-go studio · code · diff

Your SDK build had at least one "note" diagnostic, but this did not represent a regression.
generate ✅build ✅lint ✅test ✅

go get github.com/stainless-sdks/hypeman-go@5867b4281ee90384143beaf03bf6b20c9f223304
hypeman-typescript studio · code · diff

Your SDK build had at least one "note" diagnostic, but this did not represent a regression.
generate ✅build ✅lint ✅test ✅

npm install https://pkg.stainless.com/s/hypeman-typescript/1e1d26d452af59813e173c58ef22febe2c2b930f/dist.tar.gz

This comment is auto-generated by GitHub Actions and is automatically kept up to date as you push.
If you push custom code to the preview branch, re-run this workflow to update the comment.
Last updated: 2026-03-21 00:34:55 UTC

@sjmiller609 sjmiller609 marked this pull request as ready for review March 20, 2026 14:12
@sjmiller609
Copy link
Collaborator Author

Validated the feature end to end on deft-kernel-dev with fresh uncached runs.

What I checked:

  • Booted real VMs under Cloud Hypervisor, QEMU, and Firecracker on deft
  • Verified the guest-side helper came up far enough for runtime memory control to work
  • Verified Hypeman could read each VM's current runtime memory target over the real hypervisor control interface
  • Verified host-triggered reclaim changed the balloon target, respected the protected floor, and returned the VM to its normal target after clearing reclaim
  • Reran the host-side controller/API checks for proactive reclaim and pressure-state behavior on deft

The main issue I had to fix was that the manual Linux path could reuse a stale non-Linux embedded guest-agent artifact after syncing from this laptop, which caused the guest to shut down early with exec format error. I also tightened Linux runtime PID handling around the manual guest-memory validation path so the tests follow the live VMM process more reliably.

Copy link

@cursor cursor bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cursor Bugbot has reviewed your changes and found 4 potential issues.

Fix All in Cursor

Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.

protected_floor_percent: 50
protected_floor_min_bytes: 512MB
min_adjustment_bytes: 64MB
per_vm_max_step_bytes: 256MB
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Config defaults and example YAML use incompatible byte units

Medium Severity

The example YAML files use 512MB, 64MB, and 256MB while the Go code defaults use raw byte strings "536870912", "67108864", "268435456" (which are 512/64/256 MiB respectively). The c2h5oh/datasize library treats MB as SI megabytes (1,000,000 bytes), not mebibytes (1,048,576 bytes). Anyone copying the example config would get ~4.9% less memory for each threshold than the Go defaults intend, causing subtle behavioral differences between default and YAML-configured deployments.

Additional Locations (2)
Fix in Cursor Fix in Web

} else {
appliedTarget = candidate.currentTargetGuestBytes - minInt64(-delta, c.config.PerVMMaxStepBytes)
}
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Step-size clamping uses stale delta after adjustment suppression

Medium Severity

After appliedTarget is reset to candidate.currentTargetGuestBytes by the min-adjustment or cooldown checks, the subsequent step-size clamping block still uses the original delta (computed from plannedTarget). When appliedTarget == candidate.currentTargetGuestBytes, the step-size branch condition appliedTarget != candidate.currentTargetGuestBytes is false, so this is currently benign, but the logic flow is fragile — if the cooldown or min-adjustment resets appliedTarget to anything other than currentTargetGuestBytes, the step-size clamp would use a stale delta.

Fix in Cursor Fix in Web

parts := strings.Fields(line)
for i := 0; i < len(parts); i++ {
if parts[i] == "of" && i+1 < len(parts) {
n, err := strconv.ParseInt(strings.TrimSuffix(parts[i+1], " bytes)"), 10, 64)
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Darwin vm_stat page size parsing fails on real output

Medium Severity

The parseDarwinVMStatOutput page-size parser uses strings.TrimSuffix(parts[i+1], " bytes)") but since parts comes from strings.Fields, it never contains spaces. The actual token for a line like "(page size of 16384 bytes)" will be "16384" followed by "bytes)" as separate fields. Trimming " bytes)" (with a leading space) from "16384" is a no-op, so the parse succeeds coincidentally, but the suffix removal is dead code. On systems where the token format differs slightly, the fallback to 4096 could silently produce wrong available-memory values.

Fix in Cursor Fix in Web

}
return HostPressureStatePressure
default:
if availablePercent <= highWatermark || sample.Stressed {
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pressure hysteresis uses <= where < matches docs

Low Severity

In nextPressureState, the healthy→pressure transition triggers when availablePercent <= highWatermark. This means available memory exactly at the high watermark (e.g., 10%) enters pressure state. However, the pressure→healthy exit condition uses >= lowWatermark. The asymmetry means available memory at exactly the high watermark threshold enters pressure, which may cause unnecessary pressure transitions on hosts hovering near the boundary, contradicting the hysteresis intent of avoiding flapping at thresholds.

Fix in Cursor Fix in Web

sjmiller609 and others added 2 commits March 21, 2026 00:27
- Change example YAML byte-size values from MB (decimal SI, 10^6) to
  MiB (binary, 2^20) so they match the Go default config which uses raw
  binary byte counts (e.g. 536870912 = 512 MiB).
- Remove dead strings.TrimSuffix call in parseDarwinVMStatOutput; the
  " bytes)" suffix is never present after strings.Fields splits on
  whitespace.

Addresses remaining Cursor Bugbot review findings on PR #160.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
The c2h5oh/datasize library does not support MiB (binary IEC) suffixes.
Use raw byte counts (e.g. 536870912 = 512*1024*1024) to match the Go
default config and avoid the ~5% discrepancy from using MB (decimal SI).

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
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.

1 participant