feat(cli): really stop a job that uipath server is running [PC-4873] - #1842
Draft
robert-ursu wants to merge 1 commit into
Draft
robert-ursu wants to merge 1 commit into
robert-ursu wants to merge 1 commit into
Conversation
robert-ursu
force-pushed
the
feat/python-job-cancellation
branch
2 times, most recently
from
August 4, 2026 11:09
ffcf810 to
bca74f9
Compare
robert-ursu
force-pushed
the
feat/async-job-dispatch-and-result-push
branch
from
August 4, 2026 11:13
114a204 to
71c81e9
Compare
robert-ursu
force-pushed
the
feat/python-job-cancellation
branch
from
August 4, 2026 11:18
bca74f9 to
b05d195
Compare
robert-ursu
force-pushed
the
feat/python-job-cancellation
branch
from
August 4, 2026 16:00
b05d195 to
1df44eb
Compare
robert-ursu
force-pushed
the
feat/async-job-dispatch-and-result-push
branch
from
August 19, 2026 19:13
6c8af15 to
e509941
Compare
robert-ursu
force-pushed
the
feat/python-job-cancellation
branch
from
August 19, 2026 19:13
1df44eb to
e14f4b2
Compare
🚨 Heads up:
|
robert-ursu
force-pushed
the
feat/async-job-dispatch-and-result-push
branch
from
September 24, 2026 15:12
e509941 to
db4653c
Compare
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
force-pushed
the
feat/python-job-cancellation
branch
from
September 24, 2026 15:26
e14f4b2 to
50e0cc7
Compare
This branch has not been deployed
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.
Why
uipath servercould not stop a job:StopJobwas 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).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,debugandevaleach drive their own event loop on the worker thread, so a job is an event loop. They now callrun_job_loopinstead ofasyncio.run. It publishes the loop and its root task to theJobControlcarried on aContextVar. Outside the server (uipath runon a terminal) it isasyncio.rununchanged.stop_job(job_key, resume_version, force)is shared by IPCStopJoband the newPOST /jobs/{key}/stop(200 {"stopped": bool}). It works in steps:UiPathRuntimeContext.__exit__still writes the result. Wait 30 s (5 s withforceStop).forceStop).Falseif 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:Falsethere 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
CancelledErroris 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
xfailintest_server_job_core.py.Outcomes
ExitCodeError143Job stopped on requestCancelledErrorescape1(unexpected)Job cancelled itselfA 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
run_in_executorfuture, not a task. A task re-raises the job'sSystemExitinto the server loop. The job core has always turnedSystemExitinto an exit code.to_threadwork (sync LangGraph nodes, sync HTTP clients) runs itsfinallyat once. But the job'sasyncio.Runnerdoes not close until that call returns.run_job_loopwithdraws 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 answersFalseif 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 writesoutput.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 --checkandmypyare clean.tests/cli/test_server_cancellation.py(16). They use click commands shaped like the real ones and cover:Falseand keeps the lock until its thread exits;StopJobstops a runningRunJob;/startand validates its body.Jira
PC-4873
🤖 Generated with Claude Code
https://claude.ai/code/session_01CFMKm7zHGcptS49z4cnnad