Repository navigation
Busy rejection reports a tool as running while its request only waits for a stalled Editor main thread #2770
Description
Summary
A request holds the single-flight execution slot while it waits for a stalled Editor main thread. Every other tool is then rejected with UNITY_SERVER_BUSY naming the waiting tool as the one running, although its work has not started yet (or has already finished and only its last main-thread continuation is pending). Agents read this as "my snippet is still running" and wait, retry, or restart Unity for the wrong reason.
The main-thread stall itself cannot be avoided (a synchronous asset refresh or script compile blocks every tool). What is wrong is how the situation is reported.
Observed
- A snippet called
UnityEditor.PackageManager.Client.Resolve(). The resolve triggered a synchronous asset refresh with script compilation that kept the main thread busy for about ten minutes. - During that time
uloop compilewas rejected with'compile' was not executed because Unity is busy running 'execute-dynamic-code' (running for 353s), withsecondsSinceLastMainThreadTick≈ 345. - The commands were not run in parallel by the caller: the earlier
execute-dynamic-codeCLI had already given up beforecompilewas sent.
Reproduction
With the Editor focused:
# 1. Schedule a 90 s main-thread stall right after the snippet returns
uloop execute-dynamic-code --code 'UnityEditor.EditorApplication.delayCall += () => System.Threading.Thread.Sleep(90000); return "scheduled";'
# 2. Immediately send another request; it enters the slot and waits for the main thread
uloop execute-dynamic-code --code 'return 1;' &
# 3. About 10 s later
uloop get-logs --max-count 1Step 3 returns UNITY_SERVER_BUSY ("busy running 'execute-dynamic-code' (running for 20s)").
With temporary VibeLogger instrumentation around the slot and the response write (ULOOP_DEBUG), the timeline was:
| Time | Event |
|---|---|
| +0.0 s | return 1; request enters the execution slot |
| +10 s … +20 s | get-logs retried once per second, each rejected as busy by execute-dynamic-code |
| +89.8 s | execute_dynamic_code_start for return 1; (the snippet only now starts) |
| +89.8 s | snippet completes in 15 ms, slot released (heldMs 89780), response written a few ms later |
So the "running" tool had not started at all while it was reported as running. The response write itself was not delayed.
Why it happens
UnityCliLoopToolExecutionService.ExecuteToolAsync enters the slot in ToolExecutionSession.Begin and only then awaits MainThreadSwitcher.SwitchToMainThread. execute-dynamic-code also switches back to the main thread after the snippet (ApplyPauseStateAsync), so a stall can hold the slot either before the tool starts or after it finishes.
Proposed direction
- Track the phase of the request holding the slot (waiting for the main thread / executing / finishing on the main thread) and include it in the busy error data.
- In the CLI, when the holder is waiting for the main thread and
secondsSinceLastMainThreadTickis large, say that the Editor main thread has been stalled for N seconds (e.g. import or compilation) and that no tool can run until it resumes, instead of saying the tool is running. - The added field is ignored by older CLIs, so no protocol version bump should be needed.
Taking the slot only after reaching the main thread was considered and rejected: every request arriving during a stall would then wait and race for the slot when the Editor resumes, which defeats rejecting overlapping requests immediately.
Also observed
The get-logs CLI that received the final busy rejection did not exit until the main thread resumed, about 69 s after the server had written that rejection. No server-side request was logged in between, so what the CLI was waiting for is unknown.
Related
- chore: BUSY rejection message should describe single-flight behavior instead of forbidding parallel runs #2671 (wording of the busy rejection message)
- feat: include the running tool's elapsed time in single-flight busy rejections #2323 (elapsed seconds in busy rejections)
- fix: Keep execute-dynamic-code waiting through self-induced main-thread stalls #1940 (execute-dynamic-code waiting through self-induced main-thread stalls)
Activity