fix(siteruntime): normalize routed script return value for cross-process transport
A routed inbound-API call (Route.To(inst).Call(script)) runs the script on
the Site and returns its value to Central inside RouteToCallResponse, which
crosses the Central<->Site PROCESS boundary. A script's natural
'return new { ... }' is a compiler-generated anonymous type that Akka's
cross-process serializer cannot reconstruct on the receiving node, so the
reply was silently dropped and the caller's Route.To().Call() Ask timed out
at 30s with 'Script execution timed out' -- even though the script completed
and all device writes committed.
DeploymentManagerActor.RouteInboundApiCall now projects the routed return
value to a plain CLR graph (Dictionary/List/string/long/double/bool/null)
via a JSON round-trip before placing it in RouteToCallResponse. The graph
round-trips the wire and re-serializes to the same JSON shape the inbound
API expects for the HTTP body / ReturnDefinition validation.
Diagnosed live: IpsenMESMoveIn writes committed + site_events showed the
IpsenMoveIn script completed in ~0.6s, yet the inbound POST returned 500 at
30s; Central's Akka serializer logged 'Writing value of type
<>f__AnonymousType0`1 as Json' at the timeout moment.
379/379 SiteRuntime tests green.
This commit is contained in:
@@ -954,7 +954,18 @@ public class DeploymentManagerActor : ReceiveActor, IWithTimers
|
||||
{
|
||||
var result = t.Result;
|
||||
return new RouteToCallResponse(
|
||||
request.CorrelationId, result.Success, result.ReturnValue,
|
||||
request.CorrelationId, result.Success,
|
||||
// The routed script's return value crosses the Central↔Site
|
||||
// PROCESS boundary inside this response. A script's natural
|
||||
// `return new { ... }` is an anonymous type, which Akka's
|
||||
// cross-process serializer cannot round-trip — the reply is
|
||||
// silently dropped and the caller's Route.To().Call() Ask
|
||||
// times out even though the script succeeded. Project the
|
||||
// value to a plain CLR graph (Dictionary/List/primitive) that
|
||||
// round-trips the wire and reproduces the same JSON shape on
|
||||
// the inbound side (which JsonSerializer-serializes it for the
|
||||
// HTTP body / ReturnDefinition validation).
|
||||
NormalizeRoutedReturnValue(result.ReturnValue),
|
||||
result.ErrorMessage, DateTimeOffset.UtcNow);
|
||||
}
|
||||
return new RouteToCallResponse(
|
||||
@@ -972,6 +983,59 @@ public class DeploymentManagerActor : ReceiveActor, IWithTimers
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Projects a routed script's return value to a cross-process-serializable plain
|
||||
/// CLR graph (<see cref="Dictionary{TKey,TValue}"/> / <see cref="List{T}"/> /
|
||||
/// string / long / double / bool / null) via a JSON round-trip. A script's natural
|
||||
/// <c>return new { ... }</c> is a compiler-generated anonymous type that Akka's
|
||||
/// cross-process serializer cannot reconstruct on the receiving node, so the
|
||||
/// <see cref="RouteToCallResponse"/> reply is silently dropped and the caller's
|
||||
/// Ask times out. The projected graph round-trips the wire and re-serializes to the
|
||||
/// same JSON shape the inbound API expects. Returns <paramref name="value"/>
|
||||
/// unchanged on a (non-expected) serialization failure so a quirky value still has a
|
||||
/// chance to deliver rather than being forced to null.
|
||||
/// </summary>
|
||||
private object? NormalizeRoutedReturnValue(object? value)
|
||||
{
|
||||
if (value is null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
var json = System.Text.Json.JsonSerializer.Serialize(value);
|
||||
using var doc = System.Text.Json.JsonDocument.Parse(json);
|
||||
return FromJsonElement(doc.RootElement);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogWarning(ex,
|
||||
"Failed to normalize routed script return value of type {Type} for transport; sending as-is",
|
||||
value.GetType().Name);
|
||||
return value;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Converts a <see cref="System.Text.Json.JsonElement"/> to a plain CLR value
|
||||
/// (string / long / double / bool / null, or nested Dictionary / List) — never a
|
||||
/// <see cref="System.Text.Json.JsonElement"/> (which is itself not cross-process
|
||||
/// serializable). Companion to <see cref="NormalizeRoutedReturnValue"/>.
|
||||
/// </summary>
|
||||
private static object? FromJsonElement(System.Text.Json.JsonElement e) => e.ValueKind switch
|
||||
{
|
||||
System.Text.Json.JsonValueKind.Object =>
|
||||
e.EnumerateObject().ToDictionary(p => p.Name, p => FromJsonElement(p.Value)),
|
||||
System.Text.Json.JsonValueKind.Array =>
|
||||
e.EnumerateArray().Select(FromJsonElement).ToList(),
|
||||
System.Text.Json.JsonValueKind.String => e.GetString(),
|
||||
System.Text.Json.JsonValueKind.Number => e.TryGetInt64(out var l) ? l : e.GetDouble(),
|
||||
System.Text.Json.JsonValueKind.True => true,
|
||||
System.Text.Json.JsonValueKind.False => false,
|
||||
_ => null,
|
||||
};
|
||||
|
||||
/// <summary>
|
||||
/// Reads attribute values from a deployed instance for a Route.To().GetAttribute(s)
|
||||
/// call (or a central Test Run bound to the instance). Asks the Instance Actor
|
||||
|
||||
Reference in New Issue
Block a user