Skip to content

Latest commit

 

History

History
132 lines (95 loc) · 5.16 KB

File metadata and controls

132 lines (95 loc) · 5.16 KB

Process Lifecycle

State flow

The process subsystem follows this flow:

Command/Pipeline -> validated plan -> spawn -> running
    -> terminating/killed -> reaped result

render(plan(...)) stops before spawn. It only formats values for inspection.

Planning

Command validates text boundaries and policy combinations before the adapter creates a native Julia Cmd. The adapter builds one direct command per stage, applies the working directory and environment, and requests a detached process group when the lifecycle policy enables process groups.

The package never turns a command string into shell source. Shell behavior requires an explicit shell executable with a -c argument vector:

Command("/bin/sh", ["-c", "printf '%s\\n' \"\$VALUE\""];
        env=EnvOverlay(["VALUE" => "trusted input"]))

That is explicitly trusted shell execution, not a sandbox boundary.

Spawn

spawn returns immediately with a ProcessHandle. The handle owns the Julia process objects and capture pipes. A lifecycle task waits for completion while reader tasks drain stdout and stderr captures concurrently.

The first event is :started. Its detail holds native process IDs and the command count. A PID may be unavailable if a process exits before the adapter reads it.

Output

Capture readers retain at most the configured number of bytes. Reads into the result buffer are never unbounded.

Policy Behavior
Capture(...; overflow=:fail) Stop retaining after the bound, request termination, return OutputLimitExceeded.
Capture(...; overflow=:truncate) Keep draining, retain only the bound, return the underlying status with truncated=true.
Stream(io) Forward output to the caller's open IO; the result retains nothing.
Inherit() Connect the child to the corresponding parent descriptor.
Null() Connect the child to /dev/null.

In pipelines, internal stage connections are kernel pipes. Public captured stdout comes from the final stage; stderr follows the configured terminal sink.

Normal completion

The lifecycle task observes all native processes, waits for the process or chain, joins the output readers, snapshots lifecycle events, collects resource accounting, and publishes one ProcessResult.

Success means status === Succeeded and result.ok == true. Non-zero exit and signal termination are returned as data by run; use run_checked to throw instead.

Timeout and cancellation

When a timeout expires or cancellation is observed:

  1. The handle records the reason.
  2. The graceful signal goes to the process group, where enabled.
  3. The lifecycle task waits for grace_period.
  4. The final signal goes out if an owned process remains alive.
  5. The handle waits and reaps before publishing the result.

Timeout results use TimedOut with a TimeoutError recording attempted signals. Cancellation results use Cancelled. Cancellation is idempotent: a request after natural completion does not rewrite the published result.

The lifecycle task checks cancellation, timeout, overflow, and liveness on a short polling interval.

Explicit signals

signal(handle, signum) sends immediately and records a :signal event. terminate(handle) requests the graceful lifecycle path. kill(handle) sends signal 9 immediately. Call these only from code that owns the handle.

Process groups

A single command requests a detached process group by default. Linux group signaling uses a negative PID, so timeout or cancellation reaches descendants in the same group, not just the leader.

In pipelines the first stage leads the group and followers are joined into it with setpgid on a best-effort basis. When the kernel refuses (EPERM for session leaders, EACCES after exec), the handle falls back to per-stage group signals and records :process_group with unified=false. Descendant cleanup under fallback is release work with dedicated stress tests (see ../TODOS.md).

Resource accounting

After owned processes are reaped, Linux resource values come from getrusage(RUSAGE_CHILDREN). CPU values are seconds; maximum RSS is converted to bytes. RUSAGE_CHILDREN accumulates reaped children of the Julia process, so long-lived applications observe earlier child work. Per-process accounting is not implemented.

Cleanup

CleanupResult reports whether native process waiting completed and whether owned capture descriptors closed without a recorded error. Explicit wait is the cleanup path; Julia finalizers are not part of the contract.

On cleanup failure, inspect result.cleanup.errors and the lifecycle event trace. Do not silently retry a resource-sensitive operation while a descendant may still be running.

Events

Published event kinds:

  • :started once native processes exist;
  • :process_group with unification outcome for multi-stage pipelines;
  • :signal for an explicit immediate signal request;
  • :terminating when graceful escalation begins;
  • :killed when the final signal is sent;
  • :output_collected and :output_overflow per capture channel, with byte counts;
  • :cleanup_error when lifecycle cleanup records an exception;
  • :completed immediately before the terminal result snapshot.