fix(management): FlowDebugRunner.RunScriptAsync — always write the root row after a routed call; timeout + unserializable-return tests
Claude-Session: https://claude.ai/code/session_01NcMG6xXdsJbmD2wYAgkK29
This commit is contained in:
@@ -158,8 +158,7 @@ public sealed class FlowDebugRunner
|
||||
}
|
||||
catch (OperationCanceledException) when (!ct.IsCancellationRequested)
|
||||
{
|
||||
result = new InboundScriptResult(false, null,
|
||||
$"Flow Debugger run timed out after {timeout.TotalSeconds}s + {RunGrace.TotalSeconds}s grace", TimeoutCode);
|
||||
result = new InboundScriptResult(false, null, TimeoutMessage(timeout), TimeoutCode);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -316,13 +315,31 @@ public sealed class FlowDebugRunner
|
||||
catch (OperationCanceledException) when (!ct.IsCancellationRequested)
|
||||
{
|
||||
timedOut = true;
|
||||
response = new RouteToCallResponse(request.CorrelationId, false, null,
|
||||
$"Flow Debugger run timed out after {timeout.TotalSeconds}s + {RunGrace.TotalSeconds}s grace", DateTimeOffset.UtcNow);
|
||||
response = new RouteToCallResponse(
|
||||
request.CorrelationId, false, null, TimeoutMessage(timeout), DateTimeOffset.UtcNow);
|
||||
}
|
||||
}
|
||||
|
||||
// Everything from here to WriteCentralAsync runs AFTER the site already executed the
|
||||
// script — a real side effect. Nothing in this stretch may throw, or the root row is never
|
||||
// written and the operator sees ManagementActor.MapFault's generic "internal error" for a
|
||||
// call that actually succeeded. So the trace drain skips a faulting event and the return
|
||||
// value falls back to a placeholder, the same way InboundScriptExecutor turns a script's
|
||||
// own faults into a captured error rather than an escaping exception.
|
||||
foreach (var e in response.TraceEvents ?? [])
|
||||
collector.Add(e);
|
||||
{
|
||||
try
|
||||
{
|
||||
collector.Add(e);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogWarning(ex,
|
||||
"Flow Debugger dropped an inline trace event from the run of {Script} on {Instance}",
|
||||
script.CanonicalName, instance.UniqueName);
|
||||
}
|
||||
}
|
||||
|
||||
var events = collector.Complete();
|
||||
|
||||
var rootRow = ScadaBridgeAuditEventFactory.Create(
|
||||
@@ -338,7 +355,7 @@ public sealed class FlowDebugRunner
|
||||
durationMs: (int)Math.Min(sw.ElapsedMilliseconds, int.MaxValue),
|
||||
errorMessage: response.ErrorMessage,
|
||||
requestSummary: cmd.Parameters is { } p ? p.GetRawText() : null,
|
||||
responseSummary: response.ReturnValue is null ? null : JsonSerializer.Serialize(response.ReturnValue, ScriptTraceExtra.JsonOptions),
|
||||
responseSummary: SerializeReturnValue(response.ReturnValue),
|
||||
extra: new ScriptTraceExtra { Source = SourceTag, ChildExecutionId = response.ChildExecutionId }.ToJson());
|
||||
await WriteCentralAsync(sp, rootRow, ct).ConfigureAwait(false);
|
||||
|
||||
@@ -477,6 +494,40 @@ public sealed class FlowDebugRunner
|
||||
return new FlowDebugRunResult(rootId, true, null, null, events, false, startedAt, DateTimeOffset.UtcNow);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The timed-out message shared by both Live entry points, so the two paths never drift.
|
||||
/// </summary>
|
||||
private static string TimeoutMessage(TimeSpan timeout) =>
|
||||
$"Flow Debugger run timed out after {timeout.TotalSeconds}s + {RunGrace.TotalSeconds}s grace";
|
||||
|
||||
/// <summary>
|
||||
/// Serializes a routed call's return value for the root row's <c>ResponseSummary</c>, NEVER
|
||||
/// throwing: <see cref="ScriptTraceExtra.JsonOptions"/> rejects named floating-point literals,
|
||||
/// so a script returning <c>double.NaN</c> / an infinity — or any other value the serializer
|
||||
/// cannot represent — would otherwise take down the audit write of a call that already ran.
|
||||
/// The placeholder keeps the row (and the operator's Live result) intact.
|
||||
///
|
||||
/// <para>
|
||||
/// The catch is deliberately broad (as <see cref="WriteCentralAsync"/>'s already is): the value
|
||||
/// crossing this seam came from a user-authored script, and the serializer's failure mode varies
|
||||
/// by value — <c>ArgumentException</c> for a non-finite number, <see cref="JsonException"/> for a
|
||||
/// cycle, <see cref="NotSupportedException"/> for an unmappable type. A best-effort audit summary
|
||||
/// must not be the thing that fails a call the site already ran.
|
||||
/// </para>
|
||||
/// </summary>
|
||||
private static string? SerializeReturnValue(object? returnValue)
|
||||
{
|
||||
if (returnValue is null) return null;
|
||||
try
|
||||
{
|
||||
return JsonSerializer.Serialize(returnValue, ScriptTraceExtra.JsonOptions);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
return $"<unserializable return value: {ex.GetType().Name}>";
|
||||
}
|
||||
}
|
||||
|
||||
private static void EnsureActiveNode(IServiceProvider sp)
|
||||
{
|
||||
var gate = sp.GetService<IActiveNodeGate>();
|
||||
|
||||
Reference in New Issue
Block a user