Skip to content

feat(cli): really stop a job that uipath server is running [PC-4873] - #1842

Draft
robert-ursu wants to merge 1 commit into
feat/async-job-dispatch-and-result-pushfrom
feat/python-job-cancellation
Draft

robert-ursu wants to merge 1 commit into
feat/async-job-dispatch-and-result-pushfrom
feat/python-job-cancellation

Conversation

@robert-ursu

@robert-ursu robert-ursu commented Aug 4, 2026 •

Copy link
Copy Markdown
Collaborator

Draft. Stacked on #1835 (base feat/async-job-dispatch-and-result-push), so this diff is only the stop work. Re-cut on main @ 7bd1425: the queue, registry and result-push layer from the earlier version are gone because RunJob spans the job over uipath-ipc. What carries over is JobControl / run_job_loop and the stop sequence.

Paired with hdens#7868, which makes the handler send the stop and act on the answer, and hdens#7708, which kills and restarts the server when the answer is "not stopped". PC-4873

Why

uipath server could not stop a job:

  • IPC StopJob was a no-op (fix(cli): report a job's real exit code and stop claiming stops [PC-4873] #1835 makes it return false in the meantime).
  • HTTP had no stop route at all.
  • Cancelling the awaiting task was worse than doing nothing. When the handler's IPC connection drops, uipath-ipc cancels every in-flight handler (uipath_ipc/client/connection.py, _teardown). await asyncio.to_thread(...) raised, the lock was released and env/cwd were restored while the job thread kept running. The next job then ran concurrently in a mutated process.

How a job is stopped

run, debug and eval each drive their own event loop on the worker thread, so a job is an event loop. They now call run_job_loop instead of asyncio.run. It publishes the loop and its root task to the JobControl carried on a ContextVar. Outside the server (uipath run on a terminal) it is asyncio.run unchanged.

stop_job(job_key, resume_version, force) is shared by IPC StopJob and the new POST /jobs/{key}/stop (200 {"stopped": bool}). It works in steps:

  1. Cancel the root task. The runtime unwinds cooperatively, and UiPathRuntimeContext.__exit__ still writes the result. Wait 30 s (5 s with forceStop).
  2. Cancel every task on the job's loop. This gives up on a clean cleanup. Wait 10 s (5 s with forceStop).
  3. Answer False if the job is still running. It is stuck in a call that cannot be interrupted, and only ending the process stops it. Whether to do that is the handler's call: the process and the execution instance belong to it.

Cases that answer True:

  • A queued job (waiting for the lock) is dropped before it runs.
  • An unknown job, or a live run with a different resume version, is not running. Answering False there would invite the handler to kill a resumed run for a stop aimed at the previous one.

Within 60 s, which is the handler's StopJobTimeout.

The lock waits for the thread

The job core now returns or raises only once the worker thread has exited. Cancelling the caller, for example on a dropped connection, stops the job. The CancelledError is re-raised only after that, so the lock, env and cwd are never handed on while the job still runs.

The job-scope teardown (#1890) also completes before the env is restored. That removes the xfail in test_server_job_core.py.

Outcomes

How the job ended ExitCode Error
Stopped on request 143 Job stopped on request
Its own code let a CancelledError escape 1 (unexpected) Job cancelled itself
Otherwise unchanged from #1835 unchanged

A stopped job's result document still says Faulted/ERROR_CancelledError. That comes from uipath-runtime and is a later PR. The handler reports Stopped for an Orchestrator stop regardless of what the runtime wrote.

Worth a careful look

  • The worker is a plain run_in_executor future, not a task. A task re-raises the job's SystemExit into the server loop. The job core has always turned SystemExit into an exit code.
  • A job waiting on its own to_thread work (sync LangGraph nodes, sync HTTP clients) runs its finally at once. But the job's asyncio.Runner does not close until that call returns. run_job_loop withdraws the loop before the runner closes, so the sweep in step 2 cannot cancel the runner's wait on those threads, which would leak them. Such a job answers False if the call outlives the grace.
  • JobControl.cancel() delivers at most once. A stop followed by a force stop is ordinary, and a second delivery would land inside the cleanup that writes output.json.

Testing

  • uv run pytest tests/cli: 1519 passed, 1 skipped. The suite was run three times to check for flakiness.
  • ruff check, ruff format --check and mypy are clean.
  • New tests are in tests/cli/test_server_cancellation.py (16). They use click commands shaped like the real ones and cover:
    • a running job stops, its cleanup runs and the lock is freed;
    • a stop that arrives before the job has a loop is applied when it gets one;
    • a repeated or forced stop does not abort a slow cleanup;
    • a job waiting on a nested thread stops once that call returns;
    • an uninterruptible job answers False and keeps the lock until its thread exits;
    • a self-inflicted cancel is a fault;
    • a queued job is dropped;
    • a stop for another resume version leaves the live run alone;
    • a cancelled caller stops the job and waits for its thread, keeping the lock and the job's env meanwhile;
    • IPC StopJob stops a running RunJob;
    • the HTTP stop route stops a running /start and validates its body.
  • Version 2.14.26 → 2.14.27.

Jira

PC-4873

🤖 Generated with Claude Code

https://claude.ai/code/session_01CFMKm7zHGcptS49z4cnnad

@github-actions github-actions Bot added test:uipath-langchain Triggers tests in the uipath-langchain-python repository test:uipath-integrations labels Aug 4, 2026
@robert-ursu
robert-ursu force-pushed the feat/python-job-cancellation branch 2 times, most recently from ffcf810 to bca74f9 Compare August 4, 2026 11:09
@robert-ursu
robert-ursu force-pushed the feat/async-job-dispatch-and-result-push branch from 114a204 to 71c81e9 Compare August 4, 2026 11:13
@robert-ursu
robert-ursu force-pushed the feat/python-job-cancellation branch from bca74f9 to b05d195 Compare August 4, 2026 11:18
@robert-ursu robert-ursu changed the title feat(cli): really stop a job that is already executing feat(cli): really stop a job that is already executing [PC-4873] Aug 4, 2026
@robert-ursu
robert-ursu force-pushed the feat/python-job-cancellation branch from b05d195 to 1df44eb Compare August 4, 2026 16:00
@robert-ursu
robert-ursu force-pushed the feat/async-job-dispatch-and-result-push branch from 6c8af15 to e509941 Compare August 19, 2026 19:13
@robert-ursu
robert-ursu force-pushed the feat/python-job-cancellation branch from 1df44eb to e14f4b2 Compare August 19, 2026 19:13
@github-actions

Copy link
Copy Markdown

🚨 Heads up: uipath-langchain cross-tests are FAILING 🚨

Your changes may break the uipath-langchain-python integration.

⚠️ These checks are NOT enforced by branch protection rules. Please review the failures before merging.

🔍 Inspect the failed run →

@robert-ursu
robert-ursu force-pushed the feat/async-job-dispatch-and-result-push branch from e509941 to db4653c Compare September 24, 2026 15:12
run/debug/eval drive their own event loop on the server's worker thread. They
now publish it through run_job_loop to a JobControl, so a stop cancels the
job's root task and the runtime unwinds cooperatively, still writing its
result.

stop_job is shared by IPC StopJob and the new POST /jobs/{key}/stop. It
cancels the root task, waits a grace period, cancels every task on the job's
loop, and answers False if the job is still running, because it is blocked
in a call that only ending the process can interrupt. forceStop shortens the
waits. A queued job is dropped before it runs. A stop that targets another
resume version, or an unknown job, answers True: that run is not running.

The job core no longer hands on the lock, env or cwd while the job thread
still runs. A cancelled caller (a dropped IPC connection, a shutdown) stops
the job and re-raises only once the thread has exited, and the job scope's
teardown completes before the env is restored.

A stopped job ends with exit code 143 and "Job stopped on request"; a
CancelledError the job raised on its own is reported as an unexpected
failure.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CFMKm7zHGcptS49z4cnnad
@robert-ursu
robert-ursu force-pushed the feat/python-job-cancellation branch from e14f4b2 to 50e0cc7 Compare September 24, 2026 15:26
@robert-ursu robert-ursu changed the title feat(cli): really stop a job that is already executing [PC-4873] feat(cli): really stop a job that uipath server is running [PC-4873] Sep 24, 2026

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

test:uipath-integrations test:uipath-langchain Triggers tests in the uipath-langchain-python repository test:uipath-runtime

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant