Skip to content

Extract server recovery/bind-retry flow into UnityCliLoopServerRecoveryExecutor - #1655

Merged
hatayama merged 2 commits into
v3-betafrom
refactor/hatayama/extract-server-recovery-executor
Jul 9, 2026
Merged

Extract server recovery/bind-retry flow into UnityCliLoopServerRecoveryExecutor#1655
hatayama merged 2 commits into
v3-betafrom
refactor/hatayama/extract-server-recovery-executor

Conversation

@hatayama

@hatayama hatayama commented Jul 9, 2026

Copy link
Copy Markdown
Owner

Refs: R3-8 in the Unity CLI Loop Phase 2/3 refactor ToDo

Summary

  • UnityCliLoopServerControllerService mixed IPC lifecycle orchestration (start/stop/domain-reload handling) with the coalesced recovery/bind-retry algorithm (StartRecoveryIfNeededAsync + TryBindWithWaitAsync, ~150 lines).
  • Extracted both methods into a new internal sealed class UnityCliLoopServerRecoveryExecutor, constructed once in the controller's constructor and wired via a thin one-line delegating wrapper.
  • The only genuinely shared mutable state, _bridgeServer, is bridged with a Func<IUnityCliLoopServerInstance> getBridgeServer / Action<IUnityCliLoopServerInstance> setBridgeServer pair — everything else the executor needs was already an injected interface, so no other bridging was required.
  • IsBackgroundUnityProcess() was promoted from private to internal static (it has zero instance-state dependencies — it only calls the static AssetDatabase.IsAssetImportWorkerProcess()), letting the executor call it directly without duplication or another delegate.
  • cancellationToken parameter renamed to ct in the moved methods per project convention.

Verification

  • Pure-move check: normalized two-way diff (sort -u + comm -23/comm -13) between the pre-move file and the combined post-move files shows only the pre-approved adaptations (ct rename, _bridgeServer → delegate calls, IsBackgroundUnityProcess static-ization, and the new executor's DI wiring/wrapper) — no unrelated logic changes.
  • uloop compile — 0 errors / 0 warnings.
  • uloop run-tests --filter-value 'UnityCliLoopServerControllerRecoveryTests' — 23/23 pass, test file unmodified (public constructor signature of UnityCliLoopServerControllerService preserved, so the existing tests exercise the new code path unchanged).

Test plan

  • uloop compile clean
  • UnityCliLoopServerControllerRecoveryTests 23/23 pass unmodified
  • Normalized diff confirms byte-equal behavior modulo approved adaptations

Review in cubic

…ryExecutor

UnityCliLoopServerControllerService mixed IPC lifecycle orchestration
with the coalesced recovery/bind-retry algorithm. Moving
StartRecoveryIfNeededAsync and TryBindWithWaitAsync into a dedicated
executor improves cohesion: the controller now only owns server
lifecycle state, while the executor owns the bind-retry loop.

The only shared mutable state (_bridgeServer) is bridged with a
Func/Action getter-setter pair rather than passing the whole
controller, keeping the executor's dependencies to injected
interfaces plus the two delegates.
@coderabbitai

coderabbitai Bot commented Jul 9, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

📝 Walkthrough

Walkthrough

The recovery-start logic in UnityCliLoopServerController is extracted into a new UnityCliLoopServerRecoveryExecutor class handling coalesced server binding, retries, session-flag updates, and error logging. The controller now delegates to this executor and exposes IsBackgroundUnityProcess as internal static. A test guard allowlist is also updated.

Changes

Recovery executor extraction

Layer / File(s) Summary
New recovery executor
Packages/src/Editor/Infrastructure/Server/UnityCliLoopServerRecoveryExecutor.cs
Adds a new class implementing background-process/startup-protection skip logic, semaphore-coalesced server rebinding with retries up to 5 seconds, session-flag updates, tool-registry warmup, and socket error logging.
Controller delegation
Packages/src/Editor/Infrastructure/Server/UnityCliLoopServerController.cs
StartRecoveryIfNeededAsync now delegates to a _recoveryExecutor field initialized in the constructor with bridge-server access delegates; IsBackgroundUnityProcess becomes internal static; unused using directives removed.
Test guard allowlist
Assets/Tests/Editor/StaticFacadeStateGuardTests.cs
Adds DynamicCodeMissingReturnRetryPolicy.cs to the async cancellation-token guard's scanned file paths.

Estimated code review effort: 3 (Moderate) | ~25 minutes

Sequence Diagram(s)

sequenceDiagram
  participant Controller as UnityCliLoopServerController
  participant Executor as UnityCliLoopServerRecoveryExecutor
  participant Server as BridgeServer

  Controller->>Executor: StartRecoveryIfNeededAsync(isAfterCompile, ct)
  Executor->>Executor: skip if background process or startup protection active
  Executor->>Executor: acquire startup semaphore
  Executor->>Server: dispose stale server instance
  Executor->>Server: TryBindWithWaitAsync (retry loop, 250ms steps, 5s timeout)
  alt bind succeeds
    Server-->>Executor: server started
    Executor->>Executor: mark ready, activate startup protection
  else bind fails
    Executor->>Executor: clear session flags, throw InvalidOperationException
  end
  Executor->>Executor: release semaphore
  Executor-->>Controller: return
Loading

Possibly related PRs

  • hatayama/unity-cli-loop#1552: Both PRs extract UnityCliLoopServerControllerService recovery logic out of the controller into a dedicated recovery component.
  • hatayama/unity-cli-loop#1553: Both PRs modify the recovery flow so failure to restore a running server is surfaced, altering StartRecoveryIfNeededAsync.
  • hatayama/unity-cli-loop#1652: Directly related to the test allowlist update adding DynamicCodeMissingReturnRetryPolicy.cs.
🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed The title accurately summarizes the main refactor: extracting server recovery and bind-retry logic into a new executor.
Description check ✅ Passed The description is directly related to the refactor and aligns with the changeset details and verification steps.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
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.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch refactor/hatayama/extract-server-recovery-executor

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.

… recovery executor

UnityCliLoopServerRecoveryExecutor has exactly one construction site
(inside UnityCliLoopServerControllerService's constructor) where every
argument is already null-checked or is a compile-time-non-null lambda,
so the ArgumentNullException chain could never fire. Following the
established internal-helper convention, keep the Debug.Assert
preconditions and drop the redundant throw chain.

Also add the executor to StaticFacadeStateGuardTests' async
cancellation-token guard list, since it owns two async methods with
the standard ct parameter and was missing from the tracked path list.

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

🧹 Nitpick comments (1)
Packages/src/Editor/Infrastructure/Server/UnityCliLoopServerRecoveryExecutor.cs (1)

92-104: 🩺 Stability & Availability | 🔵 Trivial | ⚡ Quick win

Only retry expected bind failures; rethrow unexpected exceptions.

The broad catches turn non-socket bugs into generic binding_failed retries and can clear the bridge reference after a failed dispose. Keep cleanup, but only handle known bind failures; let other exceptions surface.

♻️ Proposed direction
-                    try
-                    {
-                        _getBridgeServer().Dispose();
-                        VibeLogger.LogInfo("server_disposed_before_bind", "disposed previous server instance");
-                    }
-                    catch (Exception ex)
-                    {
-                        VibeLogger.LogWarning("server_dispose_failed", ex.Message);
-                    }
-                    finally
-                    {
-                        _setBridgeServer(null);
-                    }
+                    IUnityCliLoopServerInstance previousServer = _getBridgeServer();
+                    previousServer.Dispose();
+                    _setBridgeServer(null);
+                    VibeLogger.LogInfo("server_disposed_before_bind", "disposed previous server instance");
                 catch (Exception ex)
                 {
-                    // Ensure partially created server is cleaned up on failure
                     server?.Dispose();
-                    // Unwrap SocketException details if present
-                    SocketException sockEx = ex as SocketException;
-                    if (ex is InvalidOperationException && ex.InnerException is SocketException innerSock)
+
+                    if (!TryGetSocketException(ex, out SocketException sockEx))
                     {
-                        sockEx = innerSock;
+                        throw;
                     }
 
-                    if (sockEx != null)
-                    {
-                        VibeLogger.LogWarning("binding_failed", $"target=project_ipc code={sockEx.SocketErrorCode} hresult={sockEx.HResult} native={sockEx.ErrorCode}");
-                    }
-                    else
-                    {
-                        VibeLogger.LogWarning("binding_failed", $"target=project_ipc code=Unknown hresult={ex.HResult}");
-                    }
+                    VibeLogger.LogWarning("binding_failed", $"target=project_ipc code={sockEx.SocketErrorCode} hresult={sockEx.HResult} native={sockEx.ErrorCode}");
private static bool TryGetSocketException(Exception ex, out SocketException socketException)
{
    socketException = ex as SocketException;
    if (socketException != null)
    {
        return true;
    }

    InvalidOperationException invalidOperationException = ex as InvalidOperationException;
    socketException = invalidOperationException?.InnerException as SocketException;
    return socketException != null;
}

Based on learnings, apply a Fail Fast policy: avoid broad defensive try-catch for unexpected exceptions and only catch expected domain-specific exceptions at the appropriate layer.

Also applies to: 154-166, 178-196

🤖 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/Infrastructure/Server/UnityCliLoopServerRecoveryExecutor.cs`
around lines 92 - 104, The dispose/retry handling in
UnityCliLoopServerRecoveryExecutor is too broad and is masking non-bind bugs as
recoverable failures. Update the recovery path around the bridge disposal logic
to catch only expected bind-related failures (for example via a helper like
TryGetSocketException) and rethrow any other exceptions instead of converting
them into binding_failed retries. Keep the cleanup behavior that clears the
bridge reference, but only do so in the valid failure path and avoid swallowing
unexpected exceptions in the affected recovery blocks.

Source: Learnings

🤖 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/Infrastructure/Server/UnityCliLoopServerRecoveryExecutor.cs`:
- Around line 92-104: The dispose/retry handling in
UnityCliLoopServerRecoveryExecutor is too broad and is masking non-bind bugs as
recoverable failures. Update the recovery path around the bridge disposal logic
to catch only expected bind-related failures (for example via a helper like
TryGetSocketException) and rethrow any other exceptions instead of converting
them into binding_failed retries. Keep the cleanup behavior that clears the
bridge reference, but only do so in the valid failure path and avoid swallowing
unexpected exceptions in the affected recovery blocks.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: d90782c9-c998-49cb-8cd9-1f0feaecabb1

📥 Commits

Reviewing files that changed from the base of the PR and between 67fc992 and 8ae709d.

⛔ Files ignored due to path filters (1)
  • Packages/src/Editor/Infrastructure/Server/UnityCliLoopServerRecoveryExecutor.cs.meta is excluded by none and included by none
📒 Files selected for processing (3)
  • Assets/Tests/Editor/StaticFacadeStateGuardTests.cs
  • Packages/src/Editor/Infrastructure/Server/UnityCliLoopServerController.cs
  • Packages/src/Editor/Infrastructure/Server/UnityCliLoopServerRecoveryExecutor.cs

@hatayama
hatayama merged commit 6a3ccb1 into v3-beta Jul 9, 2026
10 checks passed
@hatayama
hatayama deleted the refactor/hatayama/extract-server-recovery-executor branch July 9, 2026 05:57
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