Skip to main content

STDERR_DIAGNOSTIC_DEADLINE

Constant STDERR_DIAGNOSTIC_DEADLINE 

Source
pub const STDERR_DIAGNOSTIC_DEADLINE: Duration;
Expand description

Total wall-clock ALL diagnostic writes in ONE PROCESS may spend waiting for a reader that is not draining. Hermit forks, so read the fork note below before treating this as the whole exit path’s budget.

⚠️ WITHOUT A CEILING THIS LOOP NEVER ENDS, AND IT RUNS WHILE HERMIT IS ALREADY FAILING. EAGAIN -> poll(POLLOUT, 1s) -> retry is correct for a reader that is SLOW and unbounded for a reader that is STOPPED. Measured 2026-08-26 on a 4096-byte pipe filled to 3900 with O_NONBLOCK set, where nothing ever reads:

  before RetryingStderr (eprintln!)   EXITED rc=101 immediately
  RetryingStderr, no ceiling          STILL RUNNING after 25s, no exit

The first is wrong loudly; the second does not return at all, on the path that reports why hermit is stopping. A supervisor that hangs while reporting an error is harder to diagnose than one that dies reporting it badly.

⚠️ THIS IS A TOTAL FOR THE WHOLE PROCESS, NOT A BUDGET PER write() CALL, AND THE DIFFERENCE IS THE ENTIRE GUARANTEE. An earlier version started the clock inside fn write, so every call got the full allowance. Nothing writes to stderr exactly once on the exit path:

  hermit-cli/src/bin/hermit/main.rs      the failure class, the head of the
                                         error chain, then ONE PER CAUSE in
                                         `for cause in chain` -- 2 + chain length
  hermit-cli/src/bin/hermit/tracing.rs   .with_writer(|| RetryingStderr) is the
                                         writer for the WHOLE subscriber: one
                                         write per log event
  detcore/src/tool_global.rs             a multi-part `write!`, which `write_fmt`
                                         splits into several `write` calls

With a per-call budget the process-level cost was N times this number, N is bounded by the error chain rather than by anything here, and every caller’s let _ = swallowed the overrun silently. A --log info run measured at 138 lines would have been minutes, not seconds.

⚠️ THE VALUE IS DERIVED FROM THE BOUND THAT ENCLOSES IT, NOT CHOSEN. Diagnostics on the exit path run inside RUN_TIMEOUT_UNWIND_GRACE (10s, hermit-cli/src/lib.rs): the window between a --timeout expiring and the SIGALRM fallback calling _exit. Overrun there does not merely delay the report, it loses it – the fallback fires mid-sentence and additionally emits HERMIT_RUN_TIMEOUT_FALLBACK, which is supposed to mean the teardown wedged. So the arithmetic is:

⚠️ IT USED TO APPLY ONCE PER PROCESS, BECAUSE HERMIT FORKS. Container::run forks the container init, which got its own COPY of the process-wide clock and spent its own full deadline; both write diagnostics to the same fd 2 on the way out. Measured 2026-08-26 against a stopped reader on a real guest, which is the case where both processes report:

  deadline 5000ms   ->  10.03s observed   two processes, one deadline each
  deadline 2500ms   ->   5.02s observed   the same two, halved
  deadline 2500ms   ->   2.52s observed   missing guest: only ONE process reports

⚠️ AND THE MULTIPLIER WAS NOT ALWAYS TWO, WHICH IS WHY IT IS NO LONGER A CONSTANT. hermit run --verify runs the guest twice and forks once per run, so it is the outer hermit plus TWO container inits – three writers, and siblings rather than a chain. From the call structure, so it needs no sampling:

  run.rs  fn verify()      Run1 -> self.run_verify(..)   Run2 -> self.run_verify(..)
          fn run_verify()  -> with_container(..) forks a container init

At three, 3 x 2500ms x 2 = 15s against a 10s grace: the stated split does not hold. Correcting the constant to three would force the deadline to ~1666ms and leave a hardcoded count for the next differently-forking path to falsify.

So the ORIGIN is shared across the invocation instead (see STDERR_ORIGIN_ADDR): every hermit process measures from the same start, the invocation spends ONE deadline however many processes write, and the count stops being an input. The arithmetic no longer contains it:

  RUN_TIMEOUT_UNWIND_GRACE                    10s     the enclosing bound
  diagnostics may have half of it              5s     leaving 5s for the unwind
  this deadline, per INVOCATION             2500ms    comfortably inside the 5s

Half is a split, not a measurement, and is stated as one. What is ALSO measured is that the previous arrangement could not fit: at 5s per CALL, the same real-guest fixture took 15.04s – three blocked writes – against a 10s grace, so the inner bound exceeded its outer bound before any unwind work happened at all. hermit-cli asserts this relationship in a test, so raising either side without the other fails by name.

On expiry the write returns WouldBlock, writeln! gives up, the caller’s let _ = drops that line, and hermit exits — the pre-existing contract for a diagnostic that cannot be delivered.