Skip to content

K2GO-386 feat(diskguard): app-side free-space guard that stops the box before ENOSPC - #544

Open
luisguzman-adfa wants to merge 4 commits into
mainfrom
feat/K2GO-386-disk-guard
Open

K2GO-386 feat(diskguard): app-side free-space guard that stops the box before ENOSPC#544
luisguzman-adfa wants to merge 4 commits into
mainfrom
feat/K2GO-386-disk-guard

Conversation

@luisguzman-adfa

Copy link
Copy Markdown
Collaborator

What

Adds an Android-side guard that watches free space while the box is up and, on a
critical reading, tears the box down and reclaims the runaway log — so a service
that busy-loops into /var/log can no longer fill the device to ENOSPC.

  • diskguard/domain/DiskGuardPolicy — pure rule (JVM-tested): free space below a
    1.5 GiB floor → CRITICAL; null/negative → UNKNOWN (fail-safe, never acts on a bad
    read). The floor sits below StorageGuard's 2 GiB op-floor, so a normal op keeps
    ≥2 GiB and only a headroom-less runaway trips the guard.
  • diskguard/DiskGuard — on CRITICAL: set desired=DOWN via the reconciler, reap the
    box, reclaim the runaway log, notify the user.
  • env/EnvironmentProcess.reapBox — kill the box proot + reap its services by
    /proc cmdline (same technique as the existing reapEnvironmentHttpFront).
  • WatchdogService — a single poller runs DiskGuard.check every 25 s for the life
    of a protected (box-up) session; started once per session, stopped on destroy.
  • strings_untranslated.xml — the guard's user alert (pending l10n).
  • src/debug — an exported DebugDiskGuardReceiver to trigger the guard from adb for
    device-verify; never ships in release.

Why

The disk-fill happens exactly when the box proot dies (a relaunch/restore orphans a
service that busy-loops), so the in-box healer is already dead — only an
outside-the-rootfs actor can stop it. Two points that shaped the design, both
device-proven:

  • desired=DOWN before the reap. A reap alone is undone: the ADFA-5343 reconciler
    relaunches the box in ~3 s and the runaway resumes. The guard first sets the one
    persisted intent the server toggle already owns (setUserWantsOn(false)) so the
    reconciler will not relaunch, then reaps for immediacy. No new "forced-down" flag —
    one source for "should the box be up"; the user re-enables the server after freeing
    space.
  • Single surface = free disk space, so the guard catches any runaway service,
    not just php-fpm.

Verification

Device-verified end to end (OnePlus HD1901, Android 15 / API 35): the guard set
desired=DOWN, reaped the box, the reconciler then held intent=NOOP (box did not
relaunch), the runaway .log was truncated and the space reclaimed, and the alert
posted. DiskGuardPolicy unit tests green.

Coordination

Barrier 2 of K2GO-386. Barrier 1 (stop php-fpm running idle in the rootfs build) ships
on fix/K2GO-386-php-fpm-idle-off.

The disk-fill happens when the box proot dies (a relaunch/restore orphans a
service that busy-loops); the in-box healer dies with it. An Android-side guard,
independent of the box, is the only thing that can act — device-proven: only a
full teardown (force-stop) stops the orphan; an in-box restart is unreachable and
an in-proot kill does not reach it.

- diskguard/domain/DiskGuardPolicy: pure rule. CRITICAL below a 1.5 GiB floor,
  set below StorageGuard's 2 GiB op-floor so only a headroom-less runaway trips
  it (legit ops keep >= 2 GiB). JVM unit-tested.
- diskguard/DiskGuard: probe (StorageProbe) -> policy -> on CRITICAL reap the box,
  truncate the runaway log to reclaim the space, and warn the user (notification).
- EnvironmentProcess.reapBox: full box teardown (kill the proot + reap the
  daemonised services by cmdline, same uid + SELinux domain) = the force-stop
  equivalent from inside the app.
- WatchdogService: a 25 s poller runs the guard for the life of a protected
  (box-up) session; plus a DEBUG-only ACTION_DISK_GUARD_TEST hook to force a check
  with an injected floor (device-verify without filling ~58 GB).
- strings_untranslated.xml: the guard's user alert, pending localization.

Covers ANY runaway via the one common surface (free space), not just php-fpm.
…g files

Code-review pass-2 fixes on the disk-guard reclaim step:
- reclaimRunawayLog now recurses /var/log subdirectories, so a runaway
  writing to e.g. nginx/error.log is reclaimed, not just top-level files.
- Only *.log files over 1 GiB are candidates (was any file over 256 MB),
  so a legitimate large file is never truncated, including when the DEBUG
  device-verify hook fires on a healthy box.
…ot relaunched

Device testing (HD1901, 2026-09-04) proved a reap alone is undone: the
ADFA-5343 reconciler relaunches the box proot within ~3s, so the runaway
resumes. The guard now first sets the persisted user-intent lever to off
(ServerLifecycleReconciler.setUserWantsOn(false)) so desired=DOWN and the
reconciler will not relaunch, THEN reaps for immediacy (the reconciler's own
graceful pdsm stop is ~40s, too slow while the disk fills). Reuses the one
existing lever the server toggle owns rather than adding a second 'forced
down' flag; the user re-enables the server after freeing space.
…g receiver

The device-verify trigger lived on WatchdogService (exported=false), which adb
am cannot reliably reach. Move it to DebugDiskGuardReceiver in src/debug
(exported, reachable via am broadcast), mirroring DebugDeliveryReceiver, and
drop the DEBUG action + hook from WatchdogService. The trigger never ships in
release (src/debug source set) and no longer carries debug code in the
production service.
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