● Fixed. That was a genuinely fatal bug, and it had two compounding causes — #3

Merged
navicore merged 1 commit from fix-file-leak into main 2026-06-05 00:46:17 +00:00
Owner

I found and fixed both.

Root causes

  1. Self-sustaining feedback loop (the fatal one). status() and the compiler
    both read the .pl file, and on a recursively-watched dir those reads emit
    their own filesystem events → re-check → more reads → forever. Measured: 31
    plgc compiles in 4 seconds with zero user activity.
  2. Over-checking. current() re-ran the checker for every completed exercise
    on every event (completed_count + 2 compiles per save). Harmless with the
    old interpreter; catastrophic with a compiler.

Each compile extracts a ~21MB runtime to /tmp/plgc-*; at ~7 compiles/sec
the leak hit 1696 dirs / 36GB.

Fixes (in src/main.rs watch loop)

  • Cursor model — track the exercise you're on; re-check only that one,
    never completed ones.
  • Content-change gate — react only when the current file's (mtime, size)
    actually changed. Crucially this uses metadata (no content read), so the
    check itself can't feed the loop. This is what breaks the cycle.

Verified empirically

┌──────────────────┬──────────────────┬───────┐
│ │ before │ after │
├──────────────────┼──────────────────┼───────┤
│ idle (no saves) │ 31 compiles / 4s │ 0 │
├──────────────────┼──────────────────┼───────┤
│ one real save │ 9 compiles │ 1 │
├──────────────────┼──────────────────┼───────┤
│ leaked /tmp dirs │ 1696 (36GB) │ 0 │
└──────────────────┴──────────────────┴───────┘

Solving an exercise still correctly advances to the next; startup is a
bounded one-time scan. just ci green (clippy+pedantic, 27 exercises,
build).

Also done

  • Reclaimed the 36GB of leaked /tmp/plgc-*.
  • Recorded the constraint in ARCHITECTURE — both guards are load-bearing;
    detect change via metadata, never a content read — so this can't silently
    regress.
  • Filed patch-prolog2 #4 (enhancement): cache the per-run 21MB
    libplg_runtime.a extraction instead of re-extracting each call. I couldn't
    reproduce the leak in isolation (single/concurrent/SIGINT/SIGKILL all clean
    up) — the accumulation was driven by loglings' runaway + disk pressure —
    but caching the runtime is a real efficiency win for any frequent caller
    and would have capped the blast radius.

The loglings-side cause is fully fixed and verified; #4 is hardening on the
compiler side.

I found and fixed both. Root causes 1. Self-sustaining feedback loop (the fatal one). status() and the compiler both read the .pl file, and on a recursively-watched dir those reads emit their own filesystem events → re-check → more reads → forever. Measured: 31 plgc compiles in 4 seconds with zero user activity. 2. Over-checking. current() re-ran the checker for every completed exercise on every event (completed_count + 2 compiles per save). Harmless with the old interpreter; catastrophic with a compiler. Each compile extracts a ~21MB runtime to /tmp/plgc-*; at ~7 compiles/sec the leak hit 1696 dirs / 36GB. Fixes (in src/main.rs watch loop) - Cursor model — track the exercise you're on; re-check only that one, never completed ones. - Content-change gate — react only when the current file's (mtime, size) actually changed. Crucially this uses metadata (no content read), so the check itself can't feed the loop. This is what breaks the cycle. Verified empirically ┌──────────────────┬──────────────────┬───────┐ │ │ before │ after │ ├──────────────────┼──────────────────┼───────┤ │ idle (no saves) │ 31 compiles / 4s │ 0 │ ├──────────────────┼──────────────────┼───────┤ │ one real save │ 9 compiles │ 1 │ ├──────────────────┼──────────────────┼───────┤ │ leaked /tmp dirs │ 1696 (36GB) │ 0 │ └──────────────────┴──────────────────┴───────┘ Solving an exercise still correctly advances to the next; startup is a bounded one-time scan. just ci green (clippy+pedantic, 27 exercises, build). Also done - Reclaimed the 36GB of leaked /tmp/plgc-*. - Recorded the constraint in ARCHITECTURE — both guards are load-bearing; detect change via metadata, never a content read — so this can't silently regress. - Filed patch-prolog2 #4 (enhancement): cache the per-run 21MB libplg_runtime.a extraction instead of re-extracting each call. I couldn't reproduce the leak in isolation (single/concurrent/SIGINT/SIGKILL all clean up) — the accumulation was driven by loglings' runaway + disk pressure — but caching the runtime is a real efficiency win for any frequent caller and would have capped the blast radius. The loglings-side cause is fully fixed and verified; #4 is hardening on the compiler side.
● Fixed. That was a genuinely fatal bug, and it had two compounding causes —
All checks were successful
CI - Linux / CI - Linux x86_64 (pull_request) Successful in 52s
4215556e28
I found and fixed both.

  Root causes

  1. Self-sustaining feedback loop (the fatal one). status() and the compiler
  both read the .pl file, and on a recursively-watched dir those reads emit
  their own filesystem events → re-check → more reads → forever. Measured: 31
  plgc compiles in 4 seconds with zero user activity.
  2. Over-checking. current() re-ran the checker for every completed exercise
  on every event (completed_count + 2 compiles per save). Harmless with the
  old interpreter; catastrophic with a compiler.

  Each compile extracts a ~21MB runtime to /tmp/plgc-*; at ~7 compiles/sec
  the leak hit 1696 dirs / 36GB.

  Fixes (in src/main.rs watch loop)

  - Cursor model — track the exercise you're on; re-check only that one,
  never completed ones.
  - Content-change gate — react only when the current file's (mtime, size)
  actually changed. Crucially this uses metadata (no content read), so the
  check itself can't feed the loop. This is what breaks the cycle.

  Verified empirically

  ┌──────────────────┬──────────────────┬───────┐
  │                  │      before      │ after │
  ├──────────────────┼──────────────────┼───────┤
  │ idle (no saves)  │ 31 compiles / 4s │ 0     │
  ├──────────────────┼──────────────────┼───────┤
  │ one real save    │ 9 compiles       │ 1     │
  ├──────────────────┼──────────────────┼───────┤
  │ leaked /tmp dirs │ 1696 (36GB)      │ 0     │
  └──────────────────┴──────────────────┴───────┘

  Solving an exercise still correctly advances to the next; startup is a
  bounded one-time scan. just ci green (clippy+pedantic, 27 exercises,
  build).

  Also done

  - Reclaimed the 36GB of leaked /tmp/plgc-*.
  - Recorded the constraint in ARCHITECTURE — both guards are load-bearing;
  detect change via metadata, never a content read — so this can't silently
  regress.
  - Filed patch-prolog2 #4 (enhancement): cache the per-run 21MB
  libplg_runtime.a extraction instead of re-extracting each call. I couldn't
  reproduce the leak in isolation (single/concurrent/SIGINT/SIGKILL all clean
  up) — the accumulation was driven by loglings' runaway + disk pressure —
  but caching the runtime is a real efficiency win for any frequent caller
  and would have capped the blast radius.

  The loglings-side cause is fully fixed and verified; #4 is hardening on the
  compiler side.
navicore deleted branch fix-file-leak 2026-06-05 00:46:17 +00:00
Sign in to join this conversation.
No reviewers
No labels
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
navicore/loglings!3
No description provided.