fix(runtime): review findings — recursion-safe run cap, atomic detach counter, summary edge cases, per-row event-log fallback

This commit is contained in:
Joseph Doherty
2026-08-14 23:42:29 -04:00
parent b1de9dfdd4
commit 950c54c5fc
10 changed files with 626 additions and 42 deletions
@@ -526,6 +526,18 @@ public class ScriptActor : ReceiveActor, IWithTimers
/// but the script body runs on the bounded set of dedicated threads, so blocking script
/// I/O is contained there and cannot starve the shared .NET thread pool. No per-run child
/// actor is created.</para>
///
/// <para>The cap governs NEW work only (<paramref name="callDepth"/> 0 — trigger-driven
/// runs and depth-0 Ask calls). A nested <c>CallScript</c> is exempt: the calling run is
/// itself still counted in <see cref="_runsInFlight"/> while it awaits its callee (the
/// slot is released only by <see cref="ScriptExecutionCompleted"/>, sent after the body
/// returns), and a script calling ITSELF routes back to this same actor — so counting the
/// nested launch against the cap would refuse legitimate self-recursion at depth
/// <see cref="SiteRuntimeOptions.MaxConcurrentRunsPerScript"/> with a misleading "shed",
/// making <see cref="SiteRuntimeOptions.MaxScriptCallDepth"/> — the limit that actually
/// owns this path, enforced in <c>ScriptRuntimeContext.CallScript</c> — unreachable.
/// Nested depth is bounded by MaxScriptCallDepth instead, which is what the cap would
/// otherwise be doing badly.</para>
/// </summary>
private void SpawnExecution(
IReadOnlyDictionary<string, object?>? parameters,
@@ -534,7 +546,7 @@ public class ScriptActor : ReceiveActor, IWithTimers
string correlationId,
Guid? parentExecutionId = null)
{
if (_runsInFlight >= _options.MaxConcurrentRunsPerScript)
if (callDepth == 0 && _runsInFlight >= _options.MaxConcurrentRunsPerScript)
{
ShedRun(replyTo, correlationId);
return;
@@ -604,8 +616,10 @@ public class ScriptActor : ReceiveActor, IWithTimers
/// <see cref="SiteRuntimeOptions.MaxConcurrentRunsPerScript"/> runs are already in flight.
/// The four already queued/running are kept — they are closest to their own deadlines and
/// already charged against them — so nothing is ever reordered. A trigger-driven run is
/// simply not launched; an Ask-based <c>CallScript</c> gets an explicit error so a nested
/// call or inbound-API route fails fast rather than hanging to its Ask timeout.
/// simply not launched; a depth-0 Ask (an inbound-API route, or a <c>CallScript</c> from
/// an unrelated script's run) gets an explicit error so the caller fails fast rather than
/// hanging to its Ask timeout. Nested (<c>callDepth &gt; 0</c>) launches never reach here —
/// see <see cref="SpawnExecution"/>.
/// </summary>
private void ShedRun(IActorRef replyTo, string correlationId)
{