From eff3666194b9db1503800d4eb7b3dec01a29acbe Mon Sep 17 00:00:00 2001 From: hatayama Date: Sun, 19 Jul 2026 02:57:31 +0900 Subject: [PATCH 1/2] Suppress expected lifecycle worker errors Carry shared worker failure reasons through compilation so the launch readiness probe can fall back quietly when Unity advances its lifecycle generation. Keep genuine worker failures on the existing error reporting path. --- .../ExternalCompilerPathResolverTests.cs | 64 +++++++++++++++++++ .../SharedRoslynCompilerWorkerHostTests.cs | 34 ++++++++++ .../DynamicCompilationHealthMonitor.cs | 28 ++++++++ .../RoslynCompilerBackend.cs | 28 ++++---- .../SharedRoslynCompilerWorkerHost.cs | 27 +++++--- .../SharedRoslynCompilerWorkerHostResults.cs | 43 ++++++++++++- 6 files changed, 203 insertions(+), 21 deletions(-) diff --git a/Assets/Tests/Editor/DynamicCodeToolTests/ExternalCompilerPathResolverTests.cs b/Assets/Tests/Editor/DynamicCodeToolTests/ExternalCompilerPathResolverTests.cs index e26911308a..76477e6027 100644 --- a/Assets/Tests/Editor/DynamicCodeToolTests/ExternalCompilerPathResolverTests.cs +++ b/Assets/Tests/Editor/DynamicCodeToolTests/ExternalCompilerPathResolverTests.cs @@ -293,6 +293,70 @@ public async Task CompileAsync_WhenSharedWorkerBuildFails_ShouldFallbackToOneSho } } + /// + /// Verifies a lifecycle transition during worker startup silently falls back to one-shot Roslyn. + /// + [Test] + public async Task CompileAsync_WhenWorkerLifecycleClosesDuringStartup_ShouldFallbackWithoutErrorLogs() + { + ExternalCompilerPaths externalCompilerPaths = ExternalCompilerPathResolver.Resolve(); + Assert.That(externalCompilerPaths, Is.Not.Null, "Unity external compiler layout should be available."); + + string sourcePath = Path.Combine(_tempDirectoryPath, "LifecycleFallbackSmoke.cs"); + string dllPath = Path.Combine(_tempDirectoryPath, "LifecycleFallbackSmoke.dll"); + File.WriteAllText( + sourcePath, + "public static class LifecycleFallbackSmoke { public static int Execute() { return 11; } }"); + List references = new DynamicReferenceSetBuilderService().BuildReferenceSet( + new List(), + null, + externalCompilerPaths); + int buildCount = 0; + bool buildStarted = false; + bool buildFinished = false; + + DynamicCompilationHealthMonitor.ResetForTests(); + SharedRoslynCompilerWorkerHost.ShutdownForTests(); + LogAssert.NoUnexpectedReceived(); + + Func previousWorkerCompiler = + SharedRoslynCompilerWorkerHost.SwapWorkerAssemblyCompilerForTests( + (ExternalCompilerPaths paths, string workerSourcePath, string workerAssemblyPath, string workerCompileResponseFilePath) => + { + SharedRoslynCompilerWorkerHost.ShutdownForTests(); + return Array.Empty(); + }); + + try + { + using CancellationTokenSource compileCancellationTokenSource = new CancellationTokenSource(); + compileCancellationTokenSource.CancelAfter(FallbackCompileTimeoutMilliseconds); + DynamicCompilationBackendResult result = await RoslynCompilerBackend.CompileAsync( + sourcePath, + dllPath, + references, + externalCompilerPaths, + new RoslynCompilerOptions(Array.Empty(), false), + compileCancellationTokenSource.Token, + () => buildStarted = true, + () => buildFinished = true, + () => buildCount++); + + Assert.That(result, Is.Not.Null); + Assert.That(result.BackendKind, Is.EqualTo(DynamicCompilationBackendKind.OneShotRoslyn)); + Assert.That(File.Exists(dllPath), Is.True); + Assert.That(buildCount, Is.EqualTo(1)); + Assert.That(buildStarted, Is.True); + Assert.That(buildFinished, Is.True); + LogAssert.NoUnexpectedReceived(); + } + finally + { + SharedRoslynCompilerWorkerHost.SwapWorkerAssemblyCompilerForTests(previousWorkerCompiler); + SharedRoslynCompilerWorkerHost.ShutdownForTests(); + } + } + [Test] public void ReportInfrastructureFallback_WhenCompilerPathLayoutIsKnown_ShouldIncludeLayoutKind() { diff --git a/Assets/Tests/Editor/DynamicCodeToolTests/SharedRoslynCompilerWorkerHostTests.cs b/Assets/Tests/Editor/DynamicCodeToolTests/SharedRoslynCompilerWorkerHostTests.cs index b13609d5a5..02aff853c5 100644 --- a/Assets/Tests/Editor/DynamicCodeToolTests/SharedRoslynCompilerWorkerHostTests.cs +++ b/Assets/Tests/Editor/DynamicCodeToolTests/SharedRoslynCompilerWorkerHostTests.cs @@ -5,6 +5,7 @@ using System.Text; using System.Threading.Tasks; using NUnit.Framework; +using UnityEngine.TestTools; using UnityEditor.Compilation; using io.github.hatayama.UnityCliLoop.FirstPartyTools; @@ -17,6 +18,39 @@ namespace io.github.hatayama.UnityCliLoop.Tests.Editor.DynamicCodeToolTests [TestFixture] public class SharedRoslynCompilerWorkerHostTests { + /// + /// Verifies lifecycle closure is returned as a non-error compile outcome. + /// + [Test] + public void SharedWorkerCompileOutcome_WhenLifecycleCloses_ShouldCarryFailureReasonWithoutErrorLog() + { + DynamicCompilationHealthMonitor.ResetForTests(); + LogAssert.NoUnexpectedReceived(); + + SharedWorkerCompileOutcome outcome = SharedWorkerCompileOutcome.Failed( + SharedWorkerFailureReasons.LifecycleClosed, + new { reason = "lifecycle_generation_advanced" }); + + Assert.That(outcome.Succeeded, Is.False); + Assert.That(outcome.FailureReason, Is.EqualTo(SharedWorkerFailureReasons.LifecycleClosed)); + Assert.That(outcome.IsLifecycleClosed, Is.True); + LogAssert.NoUnexpectedReceived(); + } + + /// + /// Verifies non-lifecycle worker failures retain their failure reason for error reporting. + /// + [Test] + public void SharedWorkerCompileOutcome_WhenWorkerStartFails_ShouldCarryFailureReason() + { + SharedWorkerCompileOutcome outcome = SharedWorkerCompileOutcome.Failed( + "worker_start_failed", + new { reason = "process_start_failed" }); + + Assert.That(outcome.Succeeded, Is.False); + Assert.That(outcome.FailureReason, Is.EqualTo("worker_start_failed")); + } + [Test] public void ConfigureWorkerDotnetRuntimeEnvironment_WhenCalled_ShouldDisableMultilevelLookup() { diff --git a/Packages/src/Editor/FirstPartyTools/ExecuteDynamicCode/Compilation/DynamicCompilationHealthMonitor.cs b/Packages/src/Editor/FirstPartyTools/ExecuteDynamicCode/Compilation/DynamicCompilationHealthMonitor.cs index d9ee44db3e..1154080c94 100644 --- a/Packages/src/Editor/FirstPartyTools/ExecuteDynamicCode/Compilation/DynamicCompilationHealthMonitor.cs +++ b/Packages/src/Editor/FirstPartyTools/ExecuteDynamicCode/Compilation/DynamicCompilationHealthMonitor.cs @@ -63,6 +63,15 @@ public static void ReportSharedWorkerFailure(string reason, object context = nul "Investigate worker startup, request handling, and Unity-version-specific runtime assumptions."); } + public static void ReportSharedWorkerLifecycleClosed(object context = null) + { + LogInfoOnce( + $"shared_worker_lifecycle_closed::{SharedWorkerFailureReasons.LifecycleClosed}", + "dynamic_code_shared_worker_lifecycle_closed", + "execute-dynamic-code shared Roslyn worker closed during an expected Unity lifecycle transition", + context ?? new { reason = SharedWorkerFailureReasons.LifecycleClosed }); + } + public static void ReportOneShotCompilerStartFailure(object context) { LogErrorOnce( @@ -101,6 +110,25 @@ private static void LogErrorOnce( Debug.LogError(FormatConsoleErrorMessage(operation, message, context)); } + private static void LogInfoOnce( + string issueKey, + string operation, + string message, + object context) + { + string effectiveIssueKey = CreateEffectiveIssueKey(issueKey); + + lock (ReportedIssueLock) + { + if (!ReportedIssues.Add(effectiveIssueKey)) + { + return; + } + } + + VibeLogger.LogInfo(operation, message, context); + } + private static string CreateEffectiveIssueKey(string issueKey) { Debug.Assert(!string.IsNullOrEmpty(issueKey), "issueKey must not be empty"); diff --git a/Packages/src/Editor/FirstPartyTools/ExecuteDynamicCode/DynamicCompilation/RoslynCompilerBackend.cs b/Packages/src/Editor/FirstPartyTools/ExecuteDynamicCode/DynamicCompilation/RoslynCompilerBackend.cs index 050e331f09..56a32305bd 100644 --- a/Packages/src/Editor/FirstPartyTools/ExecuteDynamicCode/DynamicCompilation/RoslynCompilerBackend.cs +++ b/Packages/src/Editor/FirstPartyTools/ExecuteDynamicCode/DynamicCompilation/RoslynCompilerBackend.cs @@ -71,29 +71,33 @@ public static async Task CompileAsync( defineSymbols, allowUnsafeCode); - CompilerMessage[] workerMessages = await SharedRoslynCompilerWorkerHost.TryCompileAsync( + SharedWorkerCompileOutcome workerOutcome = await SharedRoslynCompilerWorkerHost.TryCompileAsync( workerRequestFilePath, externalCompilerPaths, ct, markBuildStarted, markBuildFinished, incrementBuildCount).ConfigureAwait(false); - if (workerMessages != null) + UnityEngine.Debug.Assert(workerOutcome != null, "Shared worker compile outcome must not be null"); + if (workerOutcome.Succeeded) { return new DynamicCompilationBackendResult( - workerMessages, + workerOutcome.Messages, DynamicCompilationBackendKind.SharedRoslynWorker); } - DynamicCompilationHealthMonitor.ReportSharedWorkerFallback( - "worker_unavailable", - new - { - platform = UnityEngine.Application.platform.ToString(), - dotnet_host_path = externalCompilerPaths.DotnetHostPath, - compiler_dll_path = externalCompilerPaths.CompilerDllPath, - layout_kind = externalCompilerPaths.LayoutKind.ToString() - }); + if (!workerOutcome.IsLifecycleClosed) + { + DynamicCompilationHealthMonitor.ReportSharedWorkerFallback( + "worker_unavailable", + new + { + platform = UnityEngine.Application.platform.ToString(), + dotnet_host_path = externalCompilerPaths.DotnetHostPath, + compiler_dll_path = externalCompilerPaths.CompilerDllPath, + layout_kind = externalCompilerPaths.LayoutKind.ToString() + }); + } ct.ThrowIfCancellationRequested(); OneShotCompileResult oneShotResult = await CompileWithOneShotAsync( diff --git a/Packages/src/Editor/FirstPartyTools/ExecuteDynamicCode/DynamicCompilation/SharedRoslynCompilerWorkerHost.cs b/Packages/src/Editor/FirstPartyTools/ExecuteDynamicCode/DynamicCompilation/SharedRoslynCompilerWorkerHost.cs index 757da064e1..79a9fd5823 100644 --- a/Packages/src/Editor/FirstPartyTools/ExecuteDynamicCode/DynamicCompilation/SharedRoslynCompilerWorkerHost.cs +++ b/Packages/src/Editor/FirstPartyTools/ExecuteDynamicCode/DynamicCompilation/SharedRoslynCompilerWorkerHost.cs @@ -28,7 +28,7 @@ internal static void RegisterLifecycleForEditorStartup() EditorApplication.quitting += ShutdownForQuit; } - public static Task TryCompileAsync( + public static Task TryCompileAsync( string requestFilePath, ExternalCompilerPaths externalCompilerPaths, CancellationToken ct, @@ -47,7 +47,7 @@ public static Task TryCompileAsync( ct); } - private static async Task TryCompileWithRetriesAsync( + private static async Task TryCompileWithRetriesAsync( string requestFilePath, ExternalCompilerPaths externalCompilerPaths, CancellationToken ct, @@ -71,7 +71,7 @@ private static async Task TryCompileWithRetriesAsync( if (attemptResult.Succeeded) { - return attemptResult.Messages; + return SharedWorkerCompileOutcome.SucceededWith(attemptResult.Messages); } ServiceValue.ExecuteWithStateLock(ServiceValue.ShutdownProcessLocked); @@ -81,13 +81,24 @@ private static async Task TryCompileWithRetriesAsync( continue; } - DynamicCompilationHealthMonitor.ReportSharedWorkerFailure( - attemptResult.FailureReason, - AppendAttempt(attemptResult.FailureContext, attempt)); - return null; + object failureContext = AppendAttempt(attemptResult.FailureContext, attempt); + if (attemptResult.FailureReason == SharedWorkerFailureReasons.LifecycleClosed) + { + DynamicCompilationHealthMonitor.ReportSharedWorkerLifecycleClosed(failureContext); + } + else + { + DynamicCompilationHealthMonitor.ReportSharedWorkerFailure( + attemptResult.FailureReason, + failureContext); + } + + return SharedWorkerCompileOutcome.Failed(attemptResult.FailureReason, failureContext); } - return null; + return SharedWorkerCompileOutcome.Failed( + "worker_unknown_failure", + new { reason = "retry_loop_exhausted" }); } private static async Task TryCompileOnceAsync( diff --git a/Packages/src/Editor/FirstPartyTools/ExecuteDynamicCode/DynamicCompilation/SharedRoslynCompilerWorkerHostResults.cs b/Packages/src/Editor/FirstPartyTools/ExecuteDynamicCode/DynamicCompilation/SharedRoslynCompilerWorkerHostResults.cs index e6eadfe316..a4f2eba74d 100644 --- a/Packages/src/Editor/FirstPartyTools/ExecuteDynamicCode/DynamicCompilation/SharedRoslynCompilerWorkerHostResults.cs +++ b/Packages/src/Editor/FirstPartyTools/ExecuteDynamicCode/DynamicCompilation/SharedRoslynCompilerWorkerHostResults.cs @@ -2,6 +2,47 @@ namespace io.github.hatayama.UnityCliLoop.FirstPartyTools { + internal static class SharedWorkerFailureReasons + { + internal const string LifecycleClosed = "shared_worker_lifecycle_closed"; + } + + /// + /// Carries shared worker compilation messages and the reason for a failed compilation. + /// + internal sealed class SharedWorkerCompileOutcome + { + public CompilerMessage[] Messages { get; } + + public string FailureReason { get; } + + public object FailureContext { get; } + + private SharedWorkerCompileOutcome( + CompilerMessage[] messages, + string failureReason, + object failureContext) + { + Messages = messages; + FailureReason = failureReason; + FailureContext = failureContext; + } + + public bool Succeeded => Messages != null; + + public bool IsLifecycleClosed => FailureReason == SharedWorkerFailureReasons.LifecycleClosed; + + public static SharedWorkerCompileOutcome SucceededWith(CompilerMessage[] messages) + { + return new SharedWorkerCompileOutcome(messages, null, null); + } + + public static SharedWorkerCompileOutcome Failed(string failureReason, object failureContext) + { + return new SharedWorkerCompileOutcome(null, failureReason, failureContext); + } + } + /// /// Carries the result data produced by Worker Attempt behavior. /// @@ -85,7 +126,7 @@ public static WorkerStartupResult ClosedLifecycleFailure() return new WorkerStartupResult( false, false, - "shared_worker_lifecycle_closed", + SharedWorkerFailureReasons.LifecycleClosed, new { reason = "lifecycle_generation_advanced" }); } } From c388e5e6a6ea962b53ee1c550621fec599844d1a Mon Sep 17 00:00:00 2001 From: hatayama Date: Sun, 19 Jul 2026 03:09:23 +0900 Subject: [PATCH 2/2] Refine lifecycle suppression coverage --- .../SharedRoslynCompilerWorkerHostTests.cs | 6 +----- .../Compilation/DynamicCompilationHealthMonitor.cs | 2 +- 2 files changed, 2 insertions(+), 6 deletions(-) diff --git a/Assets/Tests/Editor/DynamicCodeToolTests/SharedRoslynCompilerWorkerHostTests.cs b/Assets/Tests/Editor/DynamicCodeToolTests/SharedRoslynCompilerWorkerHostTests.cs index 02aff853c5..232dd9588e 100644 --- a/Assets/Tests/Editor/DynamicCodeToolTests/SharedRoslynCompilerWorkerHostTests.cs +++ b/Assets/Tests/Editor/DynamicCodeToolTests/SharedRoslynCompilerWorkerHostTests.cs @@ -22,11 +22,8 @@ public class SharedRoslynCompilerWorkerHostTests /// Verifies lifecycle closure is returned as a non-error compile outcome. /// [Test] - public void SharedWorkerCompileOutcome_WhenLifecycleCloses_ShouldCarryFailureReasonWithoutErrorLog() + public void SharedWorkerCompileOutcome_WhenLifecycleCloses_ShouldCarryLifecycleClosedReason() { - DynamicCompilationHealthMonitor.ResetForTests(); - LogAssert.NoUnexpectedReceived(); - SharedWorkerCompileOutcome outcome = SharedWorkerCompileOutcome.Failed( SharedWorkerFailureReasons.LifecycleClosed, new { reason = "lifecycle_generation_advanced" }); @@ -34,7 +31,6 @@ public void SharedWorkerCompileOutcome_WhenLifecycleCloses_ShouldCarryFailureRea Assert.That(outcome.Succeeded, Is.False); Assert.That(outcome.FailureReason, Is.EqualTo(SharedWorkerFailureReasons.LifecycleClosed)); Assert.That(outcome.IsLifecycleClosed, Is.True); - LogAssert.NoUnexpectedReceived(); } /// diff --git a/Packages/src/Editor/FirstPartyTools/ExecuteDynamicCode/Compilation/DynamicCompilationHealthMonitor.cs b/Packages/src/Editor/FirstPartyTools/ExecuteDynamicCode/Compilation/DynamicCompilationHealthMonitor.cs index 1154080c94..dc0d5979af 100644 --- a/Packages/src/Editor/FirstPartyTools/ExecuteDynamicCode/Compilation/DynamicCompilationHealthMonitor.cs +++ b/Packages/src/Editor/FirstPartyTools/ExecuteDynamicCode/Compilation/DynamicCompilationHealthMonitor.cs @@ -66,7 +66,7 @@ public static void ReportSharedWorkerFailure(string reason, object context = nul public static void ReportSharedWorkerLifecycleClosed(object context = null) { LogInfoOnce( - $"shared_worker_lifecycle_closed::{SharedWorkerFailureReasons.LifecycleClosed}", + SharedWorkerFailureReasons.LifecycleClosed, "dynamic_code_shared_worker_lifecycle_closed", "execute-dynamic-code shared Roslyn worker closed during an expected Unity lifecycle transition", context ?? new { reason = SharedWorkerFailureReasons.LifecycleClosed });