Repository navigation
fix: A command that reached Unity after a busy retry no longer reports the earlier busy answer when its connection drops or times out - #3196
Conversation
…usy answer After a busy answer, the retry that reached Unity can still end in a dropped connection or a timeout. Today the retry loop reports the earlier busy answer instead, so compile never asks Unity for its status even though the compile is running. These tests pin the attempt's own error and outcome, keep the busy answer for an attempt that never reached Unity, and keep a caller's cancellation winning in both cases.
…nswer A busy answer means Unity ran nothing, so it was a fair diagnosis for any later transport error back when the retry window and the connection deadline ended together. Each attempt now has its own deadline, and a retry that reached Unity may already be running: replacing its dropped connection or timeout with the busy answer stopped compile from asking Unity for its status and reported a running command as not executed. The busy answer now wins only over an attempt that never reached Unity, and a caller's cancellation still wins over both.
|
No actionable comments were generated in the recent review. 🎉 ℹ️ Recent review info⚙️ Run configuration
📒 Files selected for processing (2)
Included review availability: This review used your included allowance. Your plan provides up to 4 included reviews per hour; 0 remain after this review. 📝 WalkthroughWalkthroughThe connection retry flow now returns the current attempt’s outcome and error when a non-RPC failure occurs after dispatch. Tests cover undispatched failures, dispatched failures, and caller cancellation. ChangesConnection retry outcomes
Priority: ➖ Normal Estimated code review effort: 2 (Simple) | ~12 minutes Change: Bug fix Merge Risk: ⚪ Minimal · up to The retry outcome change is ready to merge after normal checks; no specific user-facing regression remains identified. Security Architecture ReviewSecurity architecture risk: 🔵 Low · up to The change corrects misleading failure reporting without adding a new command or permission boundary. Existing recovery preserves request identity and limits resends. Some uncertainty remains because the editor-side guarantees that prevent resending a still-live compile were not independently verified. Retained concerns Security review detailsSecurity Blast Radius
Trust Boundaries and Controls
Resilience and Maintainability Implications
🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
…y tests The final response timeout and unanswered request tests let the stand-in close the connection 150 ms after the client's deadline. On a slow runner the client can fall that far behind, and the close then arrives first as a disconnect. Reading until the client hangs up makes the client's own deadline the only thing that can end the wait.
An accepted request can also end with the heartbeat reporting a stalled main thread. That error is not an RPC answer either, so it used to be replaced by the earlier busy answer too. The new test pins its own error and outcome and the main-thread-stall focus handling a first attempt gets. The busy attempt in these tests now carries a distinct timing, so returning its outcome instead of the current one fails in every case.
The rule keeps the error of an attempt that reached Unity whatever its kind, not only disconnects, timeouts, and the editor-unresponsive error. A final response that fails to decode, for one, reaches the same branch. A table case with a plain error pins that the rule does not depend on the kind of error, and that a cancelled caller still gets the cancellation.
3ee9b85
into
feature/hot-reload-large-project-feedback
Summary
uloop compilesent while another tool is running no longer exits 1 withUNITY_SERVER_BUSYwhile the compile is in fact running in Unity: it goes on to ask Unity for the compile status and returns the real result.User Impact
uloop compilesent while another tool was running got a busy answer, retried, and reached Unity once the tool finished. Unity then ran the compile, but the compile took longer than the 2-second final response wait (or the domain reload dropped the connection). The CLI then printedUNITY_SERVER_BUSY("'compile' was not executed because Unity is busy running …") and exited 1. It did not ask Unity for the compile status, and it did not reach the compile resend added in fix: Send the compile again when Unity lost the request or rejected it as already compiling #3192.Cause
finishNonRetryableConnectionAttemptreturned the earlier busy answer whenever the current attempt ended in an error that was not an RPC answer. It did not check whether that attempt's request had reached Unity. The rule dates from when the retry window and the connection deadline ended at the same moment. Each attempt now has its own deadline, so that reasoning no longer holds. A busy answer still means Unity ran nothing, but only for the attempt it answered.Changes
finishNonRetryableConnectionAttemptreturns the earlier busy answer only when the current attempt's request never reached Unity (a failed connect or write). An attempt that reached Unity (RequestDispatched) keeps its own error and outcome and takes the same path as a first attempt, including the Unity focus handling.finishUndispatchedRetryProbe) is unchanged.Input space
Previous attempt × current attempt × caller cancellation. When the current attempt succeeds, gets a busy answer, or is an undispatched transient dial failure, it never reaches this branch, so nothing changes for those cases.
…PrefersBusyOverAnUndispatchedTransportErrorctx.Err()…KeepsADispatchedFailureAfterBusy(dropped after the accept),…SurfacesADroppedConnectionAfterBusy…KeepsADispatchedFailureAfterBusy(dropped before the accept)…KeepsADispatchedFailureAfterBusy(final response timed out),…SurfacesAFinalResponseTimeoutAfterBusy…SurfacesAnUnansweredRequestAfterBusy…KeepsAnEditorUnresponsiveErrorAfterBusy…KeepsADispatchedFailureAfterBusy(failed another way after the accept)ctx.Err()…KeepsADispatchedFailureAfterBusy…SurfacesDispatchedFailureAfterBusyNot used as axes: the command (this function does not look at it; the caller differences are listed below),
responseTimeout(it only affects the focus reason after the branch, which is unchanged), and how long ago the busy answer came (never looked at).Behaviour change by caller
In general, an attempt that reached Unity after a busy answer now gets the same recovery as a first attempt, including that recovery's limits. A caller that waits for the result of a request Unity never registered now waits, instead of ending at once with the busy answer.
Each caller below was checked by reading its code. Only compile was also exercised on a device.
shouldWaitForCompileStatusand the compile status polling (and the fix: Send the compile again when Unity lost the request or rejected it as already compiling #3192 resend), instead of exiting 1 withUNITY_SERVER_BUSY.freshCompileAttemptOptions{}, so the fix: Send the compile again when Unity lost the request or rejected it as already compiling #3192 resend never applies (canResendCompileis false). Its own busy retry (sendCompileWithBusyRetry) resends only on a busy answer, and no longer sees one for a request that reached Unity.shouldWaitForCompileStatus), instead of exiting 1 withUNITY_SERVER_BUSY.TimeoutSeconds+ 60 seconds (600 + 60 = 660 seconds by default) and exits 1 withRUN_TESTS_WAIT_TIMEOUT. Before, it exited 1 at once withUNITY_SERVER_BUSY. A first attempt that drops the same way already ends like this today.UNITY_SERVER_BUSY.TimeoutSeconds(180 seconds by default) and exits 1 withCONTROL_PLAY_MODE_WAIT_TIMEOUT. Before, it exited 1 at once withUNITY_SERVER_BUSY. A first attempt that drops the same way already ends like this today.--wait-for-domain-reload): a dispatched drop waits for the domain reload before it reports the failure. Exit stays 1.list,sync,enable-pause-point): stderr reports how the attempt that reached Unity ended, instead ofUNITY_SERVER_BUSY. That isUNITY_DISCONNECTED_AFTER_ACCEPT,UNITY_DISCONNECTED_AFTER_DISPATCH,UNITY_RESPONSE_TIMEOUT_AFTER_ACCEPT, the classified timeout, or the classified form of any other error the attempt ended with. Exit stays 1.UNITY_EDITOR_UNRESPONSIVE("Unity accepted the request, but the Editor main thread stopped responding.") instead ofUNITY_SERVER_BUSY, and the main-thread-stall focus handling brings Unity to the front. Exit stays 1. The compile paths never get here: Unity sends its first heartbeat 10 seconds after the accept, and their 2-second response timeout has ended the wait by then.Verification
CI for Go (
build-cli) does not run on pull requests to this integration branch, so everything below was run locally on macOS (darwin-arm64). On this PR, Complexity Report and File Length Report ran and passed.New and changed tests
TestFinishNonRetryableConnectionAttemptKeepsADispatchedFailureAfterBusy): dropped after the accept / dropped before the accept / final response timed out after the accept / failed another way after the accept (a plain error standing for one that is neither a disconnect nor a timeout, such as a final response that fails to decode). Each case checks the attempt's own error and outcome, and that a cancelled caller still getscontext.Canceled.TestFinishNonRetryableConnectionAttemptKeepsAnEditorUnresponsiveErrorAfterBusy): an editor-unresponsive error from an accepted attempt after a busy answer. It runs with a real focus controller whose process lookup finds no Unity. It checks the attempt's own error and outcome, and that the lookup ran once, which is the main-thread-stall focus handling a first attempt gets.…PrefersBusyOverAnUndispatchedTransportError. It pinned the dispatched shape that this PR fixes.…SurfacesDispatchedFailureAfterBusytest and skip on Windows for the same reason; the unit tests cover the branch on every platform.…SurfacesADroppedConnectionAfterBusy: transport disconnect, dispatched + accepted,shouldWaitForCompileStatustrue.…SurfacesAFinalResponseTimeoutAfterBusy: 50 ms response timeout,IsFinalResponseTimeoutErrortrue, accepted,shouldWaitForCompileStatustrue.…SurfacesAnUnansweredRequestAfterBusy: no ack, response timeout, dispatched but not accepted.Red / Green
…KeepsADispatchedFailureAfterBusyand the three new retry tests failed, all because the busy answer came back. The three cancellation subtests,…PrefersBusyOverAnUndispatchedTransportError, and the existing…SurfacesDispatchedFailureAfterBusypassed.Mutations (4 of 4 caught)
RequestDispatchedcheck (the code before this PR)…KeepsAnEditorUnresponsiveErrorAfterBusy, and the three new retry tests…PrefersBusyOverAnUndispatchedTransportError…KeepsAnEditorUnresponsiveErrorAfterBusy,…SurfacesADroppedConnectionAfterBusy,…SurfacesAFinalResponseTimeoutAfterBusySuites and checks
go test ./... -count=1incli/project-runner: 938 passed, 1 failed. The failure isTestSendWithTransientConnectionRetryAbortsOnRefusedConnect, which cannot bind its Unix socket in the sandbox (bind: operation not permitted). On the base commit the same run gives 921 passed and the same 1 failure. The difference of 17 is the new tests and subtests.-race -count=20, rerun after the review changes: pass.scripts/check-go-cli.sh:cli/commontests, because theipcendpointtest cannot create its directory under the system temp directory in the sandbox.cli/commonis not touched by this PR; its fmt, vet, and lint passed before the stop.cli/dispatcher,cli/release-automation, andcli/project-runner. fmt diff, vet, andgolangci-lintreport 0 issues (cli/project-runneragain after the review changes). Tests pass except the one Unix-socket test above.go-winresdownload. Built darwin-arm64 directly with the same flags.scripts/check-code-complexity.shwith fail-on-exceeded): cyclop 0 issues in all four modules. No C# finding above 15.scripts/check-file-length.shwith fail-on-exceeded): no file over 500 SLOC.coverage-report --mode report):finishNonRetryableConnectionAttemptis 100% covered.ipcendpointtests fail in the sandbox, as above.docsandPackages/srcmarkdown for the busy wording found nothing.Real Editor check (this branch's Unity project, dev binary,
ULOOP_DEBUG=1)Each run started
uloop execute-dynamic-coderunningThread.Sleep(1500)in the background and sentuloop compileabout 0.1 s later. A one-line comment in an EditMode test source made the compile and domain reload real. Times are seconds after the Editor loggedexecute_dynamic_code_start.UNITY_SERVER_BUSYat +4.17. The compile finished successfully in Unity at +10.2.Success: trueSuccess: trueNot verified
Not covered
compilerecognizes a request that reached Unity but was never registered, and sends it again (fix: Send the compile again when Unity lost the request or rejected it as already compiling #3192). control-play-mode, run-tests, and the pause-point recovery compile (which runs without the fix: Send the compile again when Unity lost the request or rejected it as already compiling #3192 resend) wait until their timeout instead, whether the request was a first attempt or came after a busy answer. Fixing that belongs in a separate PR.Notes