Skip to content

fix: deliver mid-action cancels through SessionCancelHandle#312

Merged
seant-aws merged 1 commit into
OpenJobDescription:mainlinefrom
seant-aws:phase1-containers
Jul 24, 2026
Merged

fix: deliver mid-action cancels through SessionCancelHandle#312
seant-aws merged 1 commit into
OpenJobDescription:mainlinefrom
seant-aws:phase1-containers

Conversation

@seant-aws

@seant-aws seant-aws commented Jul 15, 2026

Copy link
Copy Markdown
Contributor

Reworked 2026-07-24. The original PR pre-armed a parent CancellationToken and cancel-request channel via set_cancel_parent_token / set_cancel_request_channel — API proposed in openjd-rs#258's original form, which was superseded by Session::cancel_handle() (openjd-rs#260, released in openjd-sessions 0.4.0; #258 was distilled to the terminate_delay capping and merged separately). This rework replaces the fallback machinery with the handle: smaller diff, same fix.

What was the problem?

Session.cancel_action() failed with "session is busy" whenever an action was actually running: the Rust Session::cancel_action needs &mut Session, but a background action thread owns the Session for exactly that window — the primary window in which callers want to cancel. Mid-action cancels (service-initiated cancels and time_limit expiries in Deadline Cloud's worker agent) never reached the job process.

What is the fix?

Obtain a SessionCancelHandle at session construction and route the taken-session case through handle.cancel(time_limit, mark_action_failed). The handle delivers the cancel to the in-flight action without touching the Session, following the action's declared cancelation method on both the direct and cross-user helper paths (openjd-sessions 0.4.0 also carries the min(time_limit, terminate_delay) grace capping from openjd-rs#258).

One semantic note: the handle cannot perform the Running → Canceling session-state transition (it does not own the Session), so the binding updates its state snapshot to Canceling directly — Python-side pollers keep observing the cancel-in-progress phase until the terminal state lands, preserving the contract that openjd-sessions-for-python#331 (merged) relies on.

How was this change tested?

  • cargo check / clippy --all-targets / fmt clean; cargo test (build/link gate) passes against the published crates.io openjd-sessions 0.4.0 — no patch overrides.
  • Full Python suite: 5443 passed; the 4 failures in test/openjd/expr/test_operation_limit.py reproduce identically on pristine mainline in the same environment (pre-existing, unrelated).
  • End-to-end validation with the Deadline Cloud worker agent's differential Python-vs-Rust cancel test suite is the next step and will be reported on this PR.

Comment thread rust-bindings/Cargo.toml Outdated
# openjd-expr = { path = "../../openjd-rs/crates/openjd-expr" }
# openjd-model = { path = "../../openjd-rs/crates/openjd-model" }
# openjd-sessions = { path = "../../openjd-rs/crates/openjd-sessions" }
[patch.crates-io]

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

This uncomments the local-dev [patch.crates-io] overrides, but the comment block directly above (lines 51-53) states these must stay commented out for committed work and CI: "the published wheel is built against the crates.io versions, which is what the Cargo.lock file pins."

With this active, any build outside a machine that has a sibling ~/openjd-rs checkout at ../../openjd-rs/crates/... will fail to resolve the path dependencies — this breaks CI and the published-wheel build. Consistent with that, Cargo.lock now drops the source/checksum for openjd-expr, confirming it is being resolved from the local patch rather than crates.io.

This looks like an accidental commit of a local iteration state. It should be reverted (re-comment the block, and remove the corresponding [patch.crates-io] addition in the workspace Cargo.toml) before merge.

cancel_action previously failed with 'session is busy' whenever an
action was running, because Session::cancel_action needs &mut Session
and a background action thread owns the Session for exactly that
window - the primary window in which callers want to cancel.

openjd-sessions 0.4.0 introduced Session::cancel_handle(), a
thread-safe handle that delivers a cancel to the in-flight action
without touching the Session, following the action's declared
cancelation method on both the direct and cross-user helper paths.
Obtain the handle at construction and route the taken-session case
through it.

The handle cannot perform the Running -> Canceling state transition
(it does not own the Session), so the snapshot is updated to Canceling
directly, keeping the state visible to Python-side pollers until the
terminal state lands.

Signed-off-by: Sean Tang <171081544+seant-aws@users.noreply.github.com>
@seant-aws
seant-aws force-pushed the phase1-containers branch from ea09394 to 493ff96 Compare July 24, 2026 23:27
@seant-aws seant-aws changed the title fix: route mid-action cancel and time_limit through the fallback cancel path fix: deliver mid-action cancels through SessionCancelHandle Jul 24, 2026
// reflect it in the snapshot so Python-side pollers observe the
// cancel-in-progress phase until the terminal state lands.
let mut snap = lock_recover(&self.snapshot);
snap.state = SessionState::Canceling;

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

The snap.state = Canceling write here can be clobbered by (or can clobber) the terminal state that run_action writes concurrently, since the two use independent lock acquisitions with no ordering guarantee.

Consider this interleaving:

  1. cancel_action locks session, sees None (taken), drops the guard.
  2. cancel_handle.cancel(...) returns true.
  3. The background action returns; run_action restores the session and then writes the terminal state (Ready + terminal action_status) to the snapshot.
  4. cancel_action now locks the snapshot and overwrites state = Canceling.

The snapshot is then stuck at Canceling even though the action has finished and the session is restored. A Python poller watching state would never observe the terminal transition and could hang.

To make the Canceling write safe it should be conditional (e.g. only set Canceling if the current snapshot state is still Running) or otherwise coordinated with run_action's restore/update so a terminal state cannot be overwritten.

@seant-aws
seant-aws marked this pull request as ready for review July 24, 2026 23:37
@seant-aws
seant-aws requested a review from a team as a code owner July 24, 2026 23:37

@godobyte godobyte left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Does it make sense to add unit or integ tests as a follow-up?

@seant-aws
seant-aws merged commit 23a73cd into OpenJobDescription:mainline Jul 24, 2026
31 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants