fix: shared Roslyn worker ReadLine no longer blocks Editor shutdown#1747
Conversation
Keep worker write→read conversations serialized with an async SemaphoreSlim gate, while shutdown kills the process under a short state lock without waiting on that gate. Async protocol reads observe abandoned ReadLine tasks and kill the worker on timeout so pipes unblock. Co-authored-by: Cursor <cursoragent@cursor.com>
The CI workflow references this csproj; *.csproj is gitignored so it must be force-added like the other pure unit test projects. Co-authored-by: Cursor <cursoragent@cursor.com>
|
No actionable comments were generated in the recent review. 🎉 ℹ️ Recent review info⚙️ Run configurationConfiguration used: Path: .coderabbit.yaml Review profile: CHILL Plan: Pro Run ID: 📒 Files selected for processing (1)
🚧 Files skipped from review as they are similar to previous changes (1)
📝 WalkthroughWalkthroughThe shared Roslyn compiler worker now uses asynchronous compilation and protocol reading, separate state locking and compile serialization, coordinated shutdown handling, expanded unit tests, and CI execution of the shared worker test project. ChangesAsync Roslyn worker
Estimated code review effort: 4 (Complex) | ~45 minutes Sequence Diagram(s)sequenceDiagram
participant RoslynCompilerBackend
participant SharedRoslynCompilerWorkerHost
participant SharedRoslynCompilerWorkerSessionCoordination
participant SharedRoslynCompilerWorkerLineReader
RoslynCompilerBackend->>SharedRoslynCompilerWorkerHost: TryCompileAsync
SharedRoslynCompilerWorkerHost->>SharedRoslynCompilerWorkerSessionCoordination: Serialize compilation
SharedRoslynCompilerWorkerHost->>SharedRoslynCompilerWorkerLineReader: Read protocol response asynchronously
SharedRoslynCompilerWorkerLineReader-->>SharedRoslynCompilerWorkerHost: Diagnostics and compiler messages
SharedRoslynCompilerWorkerHost-->>RoslynCompilerBackend: CompilerMessage[]
Possibly related PRs
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 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 |
There was a problem hiding this comment.
🧹 Nitpick comments (3)
Packages/src/Editor/FirstPartyTools/ExecuteDynamicCode/DynamicCompilation/SharedRoslynCompilerWorkerHost.cs (1)
344-395: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low valueConsider extracting the repeated force-shutdown call.
ServiceValue.ExecuteWithStateLock(ServiceValue.ShutdownProcessLocked)appears in several failure paths (empty header, malformed header, missing end marker, retry loop, cancellation). A small private helper (e.g.,ForceShutdownWorker()) would centralize the intent and reduce duplication.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@Packages/src/Editor/FirstPartyTools/ExecuteDynamicCode/DynamicCompilation/SharedRoslynCompilerWorkerHost.cs` around lines 344 - 395, Extract the repeated ServiceValue.ExecuteWithStateLock(ServiceValue.ShutdownProcessLocked) operation into a private helper such as ForceShutdownWorker(). Replace the direct calls in ReadWorkerResponseAsync and the other retry, cancellation, and failure paths with this helper, preserving the existing shutdown behavior.tests/SharedRoslynCompilerWorker.UnitTests/SharedRoslynCompilerWorkerLineReaderTests.cs (2)
89-102: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick winConsider adding a timeout/cancellation test for
ReadDiagnosticLinesAsync.Only the happy path (end marker arrives) is tested. The upstream contract returns
nullwhenReadLineAsynctimes out mid-aggregation, and throwsOperationCanceledExceptionon cancellation — neither path is covered.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@tests/SharedRoslynCompilerWorker.UnitTests/SharedRoslynCompilerWorkerLineReaderTests.cs` around lines 89 - 102, Add tests for SharedRoslynCompilerWorkerLineReader.ReadDiagnosticLinesAsync covering both failure paths: verify it returns null when ReadLineAsync times out during diagnostic aggregation, and verify it propagates OperationCanceledException when cancellation is requested. Keep the existing end-marker test unchanged and use short, deterministic timeout/cancellation setup.
52-65: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low valueConsider making this test
async Taskand awaitingAssert.ThrowsAsync.The test is
voidandAssert.ThrowsAsyncis not awaited. It works here because the pre-canceled token causesThrowIfCancellationRequestedto throw synchronously, but it's inconsistent with the other async tests and fragile if the upstream contract changes to an async path.♻️ Proposed refactor
- public void ReadLineAsync_WhenCanceled_ShouldThrowOperationCanceledException() + public async Task ReadLineAsync_WhenCanceled_ShouldThrowOperationCanceledException() { // Verifies cooperative cancel surfaces as OperationCanceledException before ReadLine starts. using CancellationTokenSource cancellationTokenSource = new(); cancellationTokenSource.Cancel(); using StringReader reader = new(string.Empty); - Assert.ThrowsAsync<OperationCanceledException>( + await Assert.ThrowsAsync<OperationCanceledException>( async () => await SharedRoslynCompilerWorkerLineReader.ReadLineAsync( reader, cancellationTokenSource.Token, timeoutMilliseconds: 1000)); }🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@tests/SharedRoslynCompilerWorker.UnitTests/SharedRoslynCompilerWorkerLineReaderTests.cs` around lines 52 - 65, Update ReadLineAsync_WhenCanceled_ShouldThrowOperationCanceledException to return Task and await Assert.ThrowsAsync, preserving the pre-canceled token setup and existing assertion behavior.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Nitpick comments:
In
`@Packages/src/Editor/FirstPartyTools/ExecuteDynamicCode/DynamicCompilation/SharedRoslynCompilerWorkerHost.cs`:
- Around line 344-395: Extract the repeated
ServiceValue.ExecuteWithStateLock(ServiceValue.ShutdownProcessLocked) operation
into a private helper such as ForceShutdownWorker(). Replace the direct calls in
ReadWorkerResponseAsync and the other retry, cancellation, and failure paths
with this helper, preserving the existing shutdown behavior.
In
`@tests/SharedRoslynCompilerWorker.UnitTests/SharedRoslynCompilerWorkerLineReaderTests.cs`:
- Around line 89-102: Add tests for
SharedRoslynCompilerWorkerLineReader.ReadDiagnosticLinesAsync covering both
failure paths: verify it returns null when ReadLineAsync times out during
diagnostic aggregation, and verify it propagates OperationCanceledException when
cancellation is requested. Keep the existing end-marker test unchanged and use
short, deterministic timeout/cancellation setup.
- Around line 52-65: Update
ReadLineAsync_WhenCanceled_ShouldThrowOperationCanceledException to return Task
and await Assert.ThrowsAsync, preserving the pre-canceled token setup and
existing assertion behavior.
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Pro
Run ID: 234d03b9-b2a2-4939-a167-13eaa263ac86
⛔ Files ignored due to path filters (3)
Packages/src/Editor/FirstPartyTools/ExecuteDynamicCode/DynamicCompilation/SharedRoslynCompilerWorkerLineReader.cs.metais excluded by none and included by nonePackages/src/Editor/FirstPartyTools/ExecuteDynamicCode/DynamicCompilation/SharedRoslynCompilerWorkerSessionCoordination.cs.metais excluded by none and included by nonetests/SharedRoslynCompilerWorker.UnitTests/SharedRoslynCompilerWorker.UnitTests.csprojis excluded by none and included by none
📒 Files selected for processing (9)
.github/workflows/unity-compile-check-and-test-runner.yml.github/workflows/unity-editmode-tests.ymlPackages/src/Editor/FirstPartyTools/ExecuteDynamicCode/DynamicCompilation/RoslynCompilerBackend.csPackages/src/Editor/FirstPartyTools/ExecuteDynamicCode/DynamicCompilation/SharedRoslynCompilerWorkerHost.csPackages/src/Editor/FirstPartyTools/ExecuteDynamicCode/DynamicCompilation/SharedRoslynCompilerWorkerLineReader.csPackages/src/Editor/FirstPartyTools/ExecuteDynamicCode/DynamicCompilation/SharedRoslynCompilerWorkerProtocol.csPackages/src/Editor/FirstPartyTools/ExecuteDynamicCode/DynamicCompilation/SharedRoslynCompilerWorkerSession.csPackages/src/Editor/FirstPartyTools/ExecuteDynamicCode/DynamicCompilation/SharedRoslynCompilerWorkerSessionCoordination.cstests/SharedRoslynCompilerWorker.UnitTests/SharedRoslynCompilerWorkerLineReaderTests.cs
Review (Fable) — conditional LGTM, 1 required fixFull 1044-line diff reviewed against the approved Option A conditions, plus local verification ( Design conditions — all satisfied
Required fix: compile request send lost its retry protectionPreviously Preferred shape: keep the prepare lock to liveness check + reader capture only, and perform the send inside the Umbrella notes to record (no code change required)
Local
|
Moving stdin WriteLine/Flush back into the try restores the old path where a dead worker's broken pipe becomes a retryable communication failure instead of escaping to the tool caller. Co-authored-by: Cursor <cursoragent@cursor.com>
|
Addressed the required fix in 7f05782:
Umbrella notes updated in
Also re-ran |
|
LGTM. Verified commit 7f05782: the compile request send is back inside the retryable try ( |
cbe2662
into
feature/dynamic-code-cancellation-hardening
Summary
User Impact
ReadLineheld the session lock, so shutdown (reload/quit) could stall the Editor for the full response timeoutLock boundary (before / after)
Changes
SharedRoslynCompilerWorkerLineReader(timeout/cancel + observe abandonedReadLine)SharedRoslynCompilerWorkerSessionCoordination(async compile gate + short state lock + shutdown bypass)TryCompile→TryCompileAsync; host kills worker on empty/invalid/timeout responsesOut of scope (umbrella note)
SharedRoslynCompilerWorkerAssemblyBuilderstill uses syncWaitForExitfor worker DLL builds; call sites can run on the Unity main thread during compile/prewarmVerification
dotnet test tests/SharedRoslynCompilerWorker.UnitTests/...(8 passed)ExecuteDynamicCode.Editorwith no CS errors for these filesuloop compilehung after domain reload in this environment (IPC warmup hit disposedDynamicCodeExecutionScheduler); CI compile check is the authoritative follow-upTest plan
SharedRoslynCompilerWorkerHostTestsstill pass