Skip to content

Busy rejection reports a tool as running while its request only waits for a stalled Editor main thread #2770

Closed

Description

@hatayama

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 compile was rejected with 'compile' was not executed because Unity is busy running 'execute-dynamic-code' (running for 353s), with secondsSinceLastMainThreadTick ≈ 345.
  • The commands were not run in parallel by the caller: the earlier execute-dynamic-code CLI had already given up before compile was 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 1

Step 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 secondsSinceLastMainThreadTick is 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

Activity

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

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions