Skip to content

refactor: simplify run lifecycle and wrap dispatch#280

Merged
mwiebe merged 2 commits into
OpenJobDescription:mainfrom
mwiebe:fix-wrap-actions-followup
Jul 24, 2026
Merged

refactor: simplify run lifecycle and wrap dispatch#280
mwiebe merged 2 commits into
OpenJobDescription:mainfrom
mwiebe:fix-wrap-actions-followup

Conversation

@mwiebe

@mwiebe mwiebe commented Jul 24, 2026

Copy link
Copy Markdown
Contributor

What was the problem/requirement? (What/Why)

This is a follow-up to #277.

At a high level, openjd run follows a checklist:

  1. Read and validate the job.
  2. Enter the job's environments.
  3. Run the selected steps and tasks.
  4. Exit every environment in reverse order.
  5. Report whether the run succeeded.

Most of that checklist had grown into one 779-line execute() function. It mixed
validation, session setup, environment tracking, task execution, interruption
handling, cleanup, and reporting. That made an important question unnecessarily
hard to answer: "If something fails here, will every environment still be exited?"

Review also found a performance issue in wrapped actions. To work around a Rust
borrowing conflict, the sessions crate cloned the complete wrapper environment
before running a wrap hook. An environment can contain large embedded script
files, so a worker could copy megabytes of data for every wrapped task even though
the hook only needs the environment name, symbol table, and let bindings.

What was the solution? (How)

The run command is now organized around the checklist above:

  • run/mod.rs defines a RunContext that owns the session, entered-environment
    ledger, interruption state, failure state, task count, and timing state.
  • run/execution.rs separates preparation, preflight, session creation,
    environment lifecycle, step/task execution, adaptive chunking, and reporting
    into focused private functions.
  • Environment entry, task execution, and LIFO environment exit are centralized
    in RunContext methods. Step environments are unwound before a task error is
    returned, and the outer execution path always unwinds remaining environments.
  • Preflight now happens before session creation, so invalid input does not create
    a temporary session directory or print session-start messages.
  • An observed SIGINT/SIGTERM consistently marks the command as failed, including
    an interruption received between environment actions.
  • --task-param and --tasks now report an error when the selected step has no
    parameterSpace, instead of silently ignoring the supplied task values.
  • Reported run duration is captured before filesystem cleanup, matching the
    previous timing behavior.

For wrapped actions, WrapEnvironmentScope copies only the three small pieces of
wrapper state needed after the environment-stack borrow is released. Embedded
files and the rest of the environment are no longer cloned for each wrapped task.
Environment exit also takes ownership of the environment already being removed
from the session instead of cloning it first.

The change also consolidates filename-to-document-type detection and reuses the
CLI's shared timestamp formatter.

What is the impact of this change?

The main impact is maintainability and auditability: the run lifecycle is split
into named phases, while one context owns the state needed for reliable cleanup.
This makes failure and interruption paths easier to follow and reduces the chance
that a future early return skips environment teardown.

Wrapped task execution avoids copying potentially large embedded files on every
task. This is most relevant to worker-agent library users that run many wrapped
tasks in one session.

There are a few intentional user-visible corrections:

  • Interrupted runs exit unsuccessfully and print one interruption message.
  • Invalid step/task selections fail before session creation.
  • Explicit task parameters are rejected for steps without a parameter space.
  • Run duration excludes session-directory cleanup time.

No public Rust API was added or changed.

How was this change tested?

  • cargo fmt --all -- --check
  • cargo clippy --all-features --all-targets --workspace -- -D warnings
  • Focused CLI and sessions tests covering interruption state, explicit task
    validation, failed-task environment cleanup, and wrapped-action setup failures
  • cargo test --workspace --exclude openjd-sessions
  • cargo test -p openjd-sessions -- --skip test_with_password_nonexistent_user_returns_logon_failure --skip test_tempdir_windows_nonvalid_principal_raises_error

Yes, the unit and integration tests were run. The full unfiltered
cargo test --workspace run passed all affected tests but failed two existing
Windows account tests on this machine with LogonFailure.

Was this change documented?

Yes. The CLI architecture, run pipeline, parameter parsing, logging, and sessions
runtime specifications were updated to match the implementation. Stale references
to the old monolithic run.rs flow and obsolete helper APIs were also corrected.

No public API documentation changes were required because this does not change a
crate's public API.

Is this a breaking change?

No. This is an internal refactor and performance improvement with correctness
fixes to CLI error and interruption handling.

Does this change impact security?

No impact.


By submitting this pull request, I confirm that you can use, modify, copy, and redistribute this contribution, under the terms of your choice.

@mwiebe
mwiebe requested a review from a team as a code owner July 24, 2026 17:27
@mwiebe
mwiebe force-pushed the fix-wrap-actions-followup branch from d99cc34 to 2cac5b5 Compare July 24, 2026 17:28
Signed-off-by: Mark <399551+mwiebe@users.noreply.github.com>
@mwiebe
mwiebe force-pushed the fix-wrap-actions-followup branch from 2cac5b5 to fe674ec Compare July 24, 2026 17:41
crowecawcaw
crowecawcaw previously approved these changes Jul 24, 2026
Comment thread specs/cli/run.md
Comment on lines +142 to +143
the outer execution phase always unwinds the remaining environments. Thus a
`Session::run_task()` error cannot bypass environment cleanup.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think (?) this case is missing a test.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, this was missing, and in the process of fixing it I identified that the Windows implementation was missing some parts. Addressed in a fresh commit.

The Windows branch of the run command's signal handler only awaited
ctrl_c(). A process spawned with CREATE_NEW_PROCESS_GROUP — as worker
tooling typically does — cannot receive Ctrl+C, and CTRL_BREAK_EVENT is
the only console event another process can deliver to the CLI's group
alone, so the CLI was uninterruptible in that scenario. The handler now
listens for both Ctrl+C and Ctrl+Break on Windows.

Add an end-to-end test for the RESULTS-phase rule "treat any observed
interruption as a failed run" (specs/cli/run.md), which previously had
only a unit test of the RunContext flag mechanics. The test runs the
CLI with a long-sleeping task, delivers a real SIGINT (Unix) or
CTRL_BREAK_EVENT (Windows) once the task signals readiness, and asserts
the task is canceled, "Interruption signal received." is printed, the
session is reported failed, and the exit code is 1.

Signed-off-by: Mark <399551+mwiebe@users.noreply.github.com>
@mwiebe
mwiebe enabled auto-merge (squash) July 24, 2026 22:26
@mwiebe
mwiebe merged commit 7aaec0e into OpenJobDescription:main Jul 24, 2026
22 checks passed
@github-actions github-actions Bot mentioned this pull request Jul 24, 2026
@mwiebe
mwiebe deleted the fix-wrap-actions-followup branch July 24, 2026 23:52
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants