Repository navigation
Make process lifecycle robust: escalating stop, restart backoff, non-fatal registration - #93
Merged
Merged
Conversation
…ing up - Config (#54): PyroLabConfiguration rejects dangling cross-references (service daemon/nameservers, daemon nameservers, autolaunch entries), reporting every problem at once. `config update` and `info` print them readably; `rename` refuses to write a config it would invalidate. - Startup (#54, #56): pyrolabd.main() validates the config before starting anything, publishes the lockfile as soon as the daemon can answer, then autolaunches in a background thread with each entry isolated. A startup failure is written to STARTUP_ERROR_FILE. - Lockfile (#56): written atomically (temp file + os.replace), read tolerantly, and judged stale by whether its PID is running (new utils.pid_is_running, using ctypes on Windows) rather than by a failed connection. - `pyrolab up` waits until the daemon responds (--timeout, default 30s) and otherwise reports why it exited; it refuses to start a second daemon while one is running, even an unresponsive one. - ProcessManager methods share a reentrant lock, now that autolaunch, the checkup timer, and Pyro request threads run concurrently. - Importing pyrolab.api no longer fails on an invalid user config, one with no nameservers, or an unresolvable "public" host; this would otherwise have locked users out of every CLI command. Closes #54 Closes #56 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…gistration - Stop (#50): send the stop message, wait 10s, terminate, wait 5s, kill; forget an entity only once its process is confirmed dead, and remove a killed daemon's nameserver entries on its behalf. Several entities are stopped in parallel, and the waiting happens outside the manager lock. `pyrolab down` gives up after --timeout (default 60s) instead of looping forever. - Restart (#51): crashes are restarted after 5, 15, 60, 300s; after 5 in a row the entity is marked failed until started by hand or reload. 10 minutes of uptime resets the count. Runners report their error to the manager, and `ps` shows it in a new LAST ERROR column. - Registration (#52, #53): per-nameserver and non-fatal, retried every 30s in a background thread so the daemon serves immediately; every nameserver call has a 5s timeout and its proxy is always released. - Meaningful results (#59): launch can wait for the runner to report ready; `pyrolab start` reports running / failed (with the reason) / timeout / unknown name, and no longer spawns a duplicate of a running entity; reload reports whether everything came back; _lock returns False when another client holds the lock. - The background daemon's stdout/stderr go to logs/pyrolabd_output.log rather than the terminal that ran `pyrolab up`. Closes #50 Closes #51 Closes #52 Closes #53 Closes #59 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
test_nameserver_runner_reports_startup_error held a port on 127.0.0.1 and expected a nameserver configured for "localhost" to fail to bind it. On CI runners "localhost" also resolves to ::1, and an IPv6 bind on the same port succeeds, so the nameserver started and run() served forever: every pytest job hung until cancelled. - Use 127.0.0.1 throughout test_registration.py, so held, refused, and silent ports mean the same thing on every machine (the timeout test was also silently being refused over ::1 instead of timing out). - Run that runner in a thread with a bounded join, failing clearly instead of blocking if it ever does start. - pytest's faulthandler_timeout (60s) dumps every thread's traceback when a test runs too long, and CI jobs now have timeout-minutes, so a future hang fails within minutes with a traceback. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…stration On CI runners the default host "localhost" resolved to ::1, while the nameserver the test started listens on 127.0.0.1, so the retry was refused and the test failed on every job. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Makes the process manager reliable when processes hang, crash, or can't reach a nameserver. Closes #50, #51, #52, #53, #59.
Stopping (#50)
terminate(), waits 5 s, then callskill().kill(), it stays in the table andstopreports failure._stop_many), sodownandreloadtake one grace period, not one per entity.pskeeps responding while something is shutting down.pyrolab downnow gives up after--timeout(60 s by default) instead of looping forever.Crash recovery (#51)
pyrolab reloadruns. Its nameserver entries are removed, so clients don't get sent to a dead URI.pshas a new LAST ERROR column. Example:checkup()now runs every second instead of every 30, so crashes are noticed promptly and the 5-second backoff means something.Nameserver registration (#52, #53)
nameserver_proxy(), which sets a 5 s timeout. Pyro's default is no timeout, so a host that silently drops packets could block for minutes.nsvariable that was reused for both a config name and a proxy is gone.host: publicnameservers: these are now resolved to this machine's IP before connecting. Before, the literal hostname "public" was passed to the connection.Results that mean something (#59)
Launching can wait:
launch_*(wait=True)waits for the runner to report ready, or to die, and returns{"status", "error"}.pyrolab startuses that and reports exactly what happened:Daemon 'x' is running.failed to start: <reason>There is no daemon named 'x' in the configuration.It also no longer starts a second process for something that's already running. Before, the first one was silently orphaned. Autolaunch and automatic restarts don't wait.
reloadreturnsFalseunless everything stopped cleanly and every relaunched entity reported ready.LockableDaemon._lockreturnsFalsewhen another client holds the lock.Also
logs/pyrolabd_output.log. Before, on Linux, a crashing child's traceback printed into whichever terminal had runpyrolab up.logs exportskips that file, since it has no timestamps (pyrolab logs exportcrashes on any log entry without a parseable timestamp #71 still covers the general case).start_ns_loophas a new optionalon_readycallback, used by the nameserver runner.Checks run
Suite: 350 passed, 9 skipped. pre-commit passes on every file.
tests/test_lifecycle.py(new): fake runners that behave like real ones, plus a fake clock. It covers:[5, 15, 60, 300], then failed;startand byreload;tests/test_registration.py(new): real nameservers in threads. It covers:It also runs the real
DaemonRunner.run()andNameServerRunner.run()in a thread: serving while a nameserver is unreachable, registering, deregistering on exit, and reporting errors.The timing-sensitive tests passed repeated runs.
End-to-end run with real child processes, from a scratch copy with isolated runtime files:
startreportedfailed to start: RuntimeError: instrument not connected, and the log showed restarts after 5, 15 and 60 s.stopescalated to terminate after the 10 s grace period and reported success.down: took 3 s and left no processes behind.🤖 Generated with Claude Code