Skip to content

fix: prevent execute-dynamic-code execution slot leaks after Undo/reload#1761

Merged
hatayama merged 2 commits into
feature/dynamic-code-slot-recoveryfrom
fix/ode-scheduler-semaphore-leak-on-dispose
Jul 13, 2026
Merged

fix: prevent execute-dynamic-code execution slot leaks after Undo/reload#1761
hatayama merged 2 commits into
feature/dynamic-code-slot-recoveryfrom
fix/ode-scheduler-semaphore-leak-on-dispose

Conversation

@hatayama

@hatayama hatayama commented Jul 13, 2026

Copy link
Copy Markdown
Owner

Summary

Refs: Obsidian follow-up フォローアップ / まとめブランチ6 PR 6-1 (ODE execution slot stick)

  • Root cause: CommandRunner set _isRunning before Undo.GetCurrentGroup / SetCurrentGroupName. Undo can throw UnityException right after domain reload/startup; the exception escaped outside try/finally, so EndExecution never cleared the flag. Later requests hit the same ERROR_MESSAGE_EXECUTION_IN_PROGRESS string used for scheduler busy (vibe looked like a stuck SemaphoreSlim, but the Roslyn worker was idle).
  • Mirror fix: EndExecution now clears _isRunning / disposes the CTS in finally even if Undo.CollapseUndoOperations throws.
  • Scheduler hardening (same concern): yield-path setup failures after Wait(0) release the semaphore; resource dispose throws no longer skip Release.

Evidence / notes

  • Vibe execute_code_exception records exception_type=UnityException with 0–3ms; message text is not stored in vibe.
  • Likely recurring trigger: tool entry switches to the main thread, then ConfigureAwait(false) can resume off-main-thread before Undo. Follow-up candidate: keep the Undo / invoke path on the main thread.
  • Stub factory path is not the explanation for dynamic_executor_created ×2 (production factory never returns null).

Known limitations / follow-ups (not in this PR)

  • ReleaseExecutionSlot: if ClearExecutionState / CTS Dispose threw, DisposeResourcesIfRequested would be skipped (neither throws today).
  • Runner busy and scheduler busy still share one error string; consider distinct message or vibe op for runner busy on the umbrella follow-up list (runner busy should be unreachable when the scheduler serializes correctly).

Test plan

  • dotnet test tests/DynamicCodeExecutionScheduler.UnitTests (scheduler + CommandRunnerExecutionSlot; no private-field reflection)
  • uloop compile (0 errors / 0 warnings)
  • After merge to umbrella: real Editor repro — domain reload then uloop execute-dynamic-code must not stick on "already in progress" until Unity quit

CommandRunner marked itself running before Undo APIs that can throw during
startup/reload, so EndExecution never cleared the flag and later requests
kept returning the shared "already in progress" error. Clear the slot even
when undo collapse throws, and harden the scheduler so yield-setup failures
or dispose throws cannot skip semaphore Release.

Co-authored-by: Cursor <cursoragent@cursor.com>
@coderabbitai

coderabbitai Bot commented Jul 13, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Warning

Review limit reached

@hatayama, you've reached your PR review limit, so we couldn't start this review.

Next review available in: 2 minutes

Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available.
You're only billed for reviews past your plan's rate limits ($0.25/file).

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews.

How do review limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: b3c22106-1513-4979-8736-664ba4209444

📥 Commits

Reviewing files that changed from the base of the PR and between 2557536 and 316def7.

📒 Files selected for processing (1)
  • tests/DynamicCodeExecutionScheduler.UnitTests/DynamicCodeExecutionSchedulerTests.cs
📝 Walkthrough

Walkthrough

CommandRunner now delegates execution and undo lifecycle management to dedicated components. DynamicCodeExecutionScheduler centralizes slot cleanup and semaphore release, including exception paths. New hooks and unit tests cover undo failures, concurrent execution, setup failures, and disposal failures.

Changes

Execution Safety

Layer / File(s) Summary
CommandRunner execution slot and undo lifecycle
Packages/src/Editor/FirstPartyTools/ExecuteDynamicCode/Execution/CommandRunner.cs, Packages/src/Editor/FirstPartyTools/ExecuteDynamicCode/Execution/CommandRunnerExecutionSlot.cs, Packages/src/Editor/FirstPartyTools/ExecuteDynamicCode/Execution/CommandRunnerUndoHooks.cs, tests/DynamicCodeExecutionScheduler.UnitTests/CommandRunnerExecutionSlotTests.cs
CommandRunner uses a dedicated execution slot for running state, cancellation, and undo-group cleanup, with injectable undo hooks and failure-path tests.
Scheduler slot release and acquisition failure handling
Packages/src/Editor/FirstPartyTools/ExecuteDynamicCode/Execution/DynamicCodeExecutionScheduler.cs, Packages/src/Editor/FirstPartyTools/ExecuteDynamicCode/Execution/DynamicCodeExecutionSchedulerHooks.cs, tests/DynamicCodeExecutionScheduler.UnitTests/DynamicCodeExecutionSchedulerTests.cs
Scheduler cleanup now releases the semaphore through a centralized helper, including setup and resource-disposal exceptions, with corresponding hooks and tests.

Estimated code review effort: 4 (Complex) | ~45 minutes

Sequence Diagram(s)

sequenceDiagram
  participant Caller
  participant DynamicCodeExecutionScheduler
  participant ExecutionSemaphore
  participant DeferredResources
  Caller->>DynamicCodeExecutionScheduler: TryRunIfIdleAsync
  DynamicCodeExecutionScheduler->>ExecutionSemaphore: Acquire slot
  DynamicCodeExecutionScheduler->>DynamicCodeExecutionScheduler: Setup execution state
  DynamicCodeExecutionScheduler->>DeferredResources: Dispose resources during cleanup
  DeferredResources-->>DynamicCodeExecutionScheduler: Complete or throw
  DynamicCodeExecutionScheduler->>ExecutionSemaphore: Release slot
Loading

Possibly related PRs

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Title check ✅ Passed The title clearly summarizes the main fix: preventing execute-dynamic-code slot leaks after Undo or reload.
Description check ✅ Passed The description is directly about the execution-slot leak fix and related scheduler hardening.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/ode-scheduler-semaphore-leak-on-dispose

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.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 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.

Inline comments:
In
`@Packages/src/Editor/FirstPartyTools/ExecuteDynamicCode/Execution/DynamicCodeExecutionScheduler.cs`:
- Around line 143-146: Update the setup-failure catch in the dynamic code
execution scheduler to call the existing ReleaseExecutionSlot helper instead of
directly invoking _executionSemaphore.Release(). Preserve rethrowing the
original exception so failed setup both releases the slot and triggers any
deferred resource disposal and shutdown completion.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: c61adc54-6f75-49ae-92ea-cf9f4be66abc

📥 Commits

Reviewing files that changed from the base of the PR and between 247cb0c and 2557536.

⛔ Files ignored due to path filters (3)
  • Packages/src/Editor/FirstPartyTools/ExecuteDynamicCode/Execution/CommandRunnerExecutionSlot.cs.meta is excluded by none and included by none
  • Packages/src/Editor/FirstPartyTools/ExecuteDynamicCode/Execution/CommandRunnerUndoHooks.cs.meta is excluded by none and included by none
  • tests/DynamicCodeExecutionScheduler.UnitTests/DynamicCodeExecutionScheduler.UnitTests.csproj is excluded by none and included by none
📒 Files selected for processing (7)
  • Packages/src/Editor/FirstPartyTools/ExecuteDynamicCode/Execution/CommandRunner.cs
  • Packages/src/Editor/FirstPartyTools/ExecuteDynamicCode/Execution/CommandRunnerExecutionSlot.cs
  • Packages/src/Editor/FirstPartyTools/ExecuteDynamicCode/Execution/CommandRunnerUndoHooks.cs
  • Packages/src/Editor/FirstPartyTools/ExecuteDynamicCode/Execution/DynamicCodeExecutionScheduler.cs
  • Packages/src/Editor/FirstPartyTools/ExecuteDynamicCode/Execution/DynamicCodeExecutionSchedulerHooks.cs
  • tests/DynamicCodeExecutionScheduler.UnitTests/CommandRunnerExecutionSlotTests.cs
  • tests/DynamicCodeExecutionScheduler.UnitTests/DynamicCodeExecutionSchedulerTests.cs

Comment on lines +143 to +146
catch
{
_executionSemaphore.Release();
throw;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

Run deferred disposal before releasing a failed setup slot.

If Dispose() races after Wait(0), it returns because the semaphore is held; ThrowIfDisposed() then reaches this catch, which releases the semaphore but never calls DisposeResourcesIfRequested(). Resources and shutdown completion remain stranded.

Reuse ReleaseExecutionSlot here so setup failures also complete deferred cleanup.

Proposed fix
                 catch
                 {
-                    _executionSemaphore.Release();
+                    ReleaseExecutionSlot(true, executionCancellationTokenSource);
                     throw;
                 }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
catch
{
_executionSemaphore.Release();
throw;
catch
{
ReleaseExecutionSlot(true, executionCancellationTokenSource);
throw;
🤖 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/Execution/DynamicCodeExecutionScheduler.cs`
around lines 143 - 146, Update the setup-failure catch in the dynamic code
execution scheduler to call the existing ReleaseExecutionSlot helper instead of
directly invoking _executionSemaphore.Release(). Preserve rethrowing the
original exception so failed setup both releases the slot and triggers any
deferred resource disposal and shutdown completion.

Workspace rules forbid unapproved reflection; prove yield-path release via a
follow-up foreground enter hook, and keep dispose-throw coverage on call
counts plus the post-dispose ObjectDisposedException path.

Co-authored-by: Cursor <cursoragent@cursor.com>
@hatayama
hatayama merged commit 4c648b5 into feature/dynamic-code-slot-recovery Jul 13, 2026
2 checks passed
@hatayama
hatayama deleted the fix/ode-scheduler-semaphore-leak-on-dispose branch July 13, 2026 15:38
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant