Merge branch 'worktree-agent-acc1e4b5202e79d46' into arch-review-remediation

This commit is contained in:
Joseph Doherty
2026-08-14 21:15:31 -04:00
19 changed files with 1554 additions and 64 deletions
@@ -188,6 +188,31 @@ public class ScriptAnalysisService
private const int SandboxMaxCallSharedDepth = 16;
/// <summary>
/// Process-wide cap on concurrently executing Test Runs (arch-review WP2.4).
/// <para>
/// A sandbox run occupies a thread-pool thread for its whole duration — the
/// <c>SandboxScriptHost</c> attribute accessors are synchronous by contract and block on
/// cross-site I/O (<c>GetAwaiter().GetResult()</c>), so the thread is <em>parked</em>, not
/// merely busy, for up to the 10s sandbox timeout. Unbounded, a handful of Designers
/// each hitting Run on a slow-bound script could park enough pool threads to starve the
/// Blazor circuits sharing this process. The gate bounds that: excess runs wait for a
/// slot instead of taking a thread, and the wait is charged to the caller's own timeout.
/// </para>
/// <para>
/// Static because <c>ScriptAnalysisService</c> is registered scoped — a per-instance gate
/// would be per-request and bound nothing.
/// </para>
/// </summary>
private static readonly SemaphoreSlim SandboxRunGate =
new(SandboxMaxConcurrentRuns, SandboxMaxConcurrentRuns);
/// <summary>
/// Slot count for <see cref="SandboxRunGate"/>. Deliberately small: Test Run is an
/// authoring convenience on a node whose real job is serving circuits.
/// </summary>
private const int SandboxMaxConcurrentRuns = 4;
/// <summary>
/// Compiles and runs a script in the central process. The globals surface
/// depends on <see cref="SandboxRunRequest.Kind"/>: template and shared
@@ -437,8 +462,15 @@ public class ScriptAnalysisService
using var errorScope = captureError.BeginCapture(captured);
var stopwatch = Stopwatch.StartNew();
var gateAcquired = false;
try
{
// Bound concurrent Test Runs (see SandboxRunGate). The wait is charged to the
// caller's own linked token, so queue time counts against the sandbox timeout
// and a queued run reports Timeout rather than hanging.
await SandboxRunGate.WaitAsync(linkedCts.Token).ConfigureAwait(false);
gateAcquired = true;
// Run on a thread-pool thread with no SynchronizationContext: a
// bound script's Instance.SetAttribute / Attributes[...] block
// synchronously on cross-site I/O (the API surface is sync by
@@ -489,6 +521,10 @@ public class ScriptAnalysisService
$"{inner.GetType().Name}: {inner.Message}",
SandboxErrorKind.RuntimeError, stopwatch.ElapsedMilliseconds, null);
}
finally
{
if (gateAcquired) SandboxRunGate.Release();
}
// outScope / errorScope are disposed by their `using` declarations when the
// method returns, restoring the previous capture scope on this call-tree
// without touching process-global Console state.