perf(site-runtime): batch Route.To().GetAttributes/SetAttributes into one Instance Actor message (review 2026-10-21 01/SR-P4)
One Ask per attribute became one site-local InstanceGet/SetAttributesRequest answered in a single pass, keeping the per-attribute GetAttributeResponse / SetStaticAttributeResponse outcomes. The single-write path is refactored to a Task<SetStaticAttributeResponse> outcome shared by both paths; static writes and rejections still reply synchronously.
This commit is contained in:
@@ -3108,8 +3108,9 @@ public class DeploymentManagerActor : ReceiveActor, IWithTimers
|
||||
|
||||
/// <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
|
||||
/// per attribute and combines the results.
|
||||
/// call (or a central Test Run bound to the instance). One batched Ask to the Instance
|
||||
/// Actor answers every attribute in a single pass (review 2026-10-21 01/SR-P4) — this used
|
||||
/// to be one Ask (temp actor + mailbox round trip + 30 s timer) per attribute.
|
||||
/// </summary>
|
||||
private void RouteInboundApiGetAttributes(RouteToGetAttributesRequest request)
|
||||
{
|
||||
@@ -3124,17 +3125,14 @@ public class DeploymentManagerActor : ReceiveActor, IWithTimers
|
||||
|
||||
var sender = Sender;
|
||||
var names = request.AttributeNames;
|
||||
var asks = names
|
||||
.Select(name => instanceActor.Ask<GetAttributeResponse>(
|
||||
new GetAttributeRequest(
|
||||
request.CorrelationId, request.InstanceUniqueName, name, DateTimeOffset.UtcNow),
|
||||
TimeSpan.FromSeconds(30)))
|
||||
.ToArray();
|
||||
|
||||
Task.WhenAll(asks).ContinueWith(t =>
|
||||
instanceActor.Ask<InstanceGetAttributesResponse>(
|
||||
new InstanceGetAttributesRequest(request.CorrelationId, request.InstanceUniqueName, names),
|
||||
TimeSpan.FromSeconds(30))
|
||||
.ContinueWith(t =>
|
||||
{
|
||||
if (t.IsCompletedSuccessfully)
|
||||
{
|
||||
var results = t.Result.Results;
|
||||
var values = new Dictionary<string, object?>();
|
||||
for (var i = 0; i < names.Count; i++)
|
||||
// Each attribute value crosses the Central↔Site PROCESS boundary inside
|
||||
@@ -3146,8 +3144,8 @@ public class DeploymentManagerActor : ReceiveActor, IWithTimers
|
||||
// through the same normalizer so the wire carries a plain CLR graph
|
||||
// (Dictionary/List/primitive). Keys are preserved; scalars/strings/null pass
|
||||
// through unchanged.
|
||||
values[names[i]] = t.Result[i].Found
|
||||
? NormalizeRoutedReturnValue(t.Result[i].Value)
|
||||
values[names[i]] = results[i].Found
|
||||
? NormalizeRoutedReturnValue(results[i].Value)
|
||||
: null;
|
||||
return new RouteToGetAttributesResponse(
|
||||
request.CorrelationId, values, true, null, DateTimeOffset.UtcNow);
|
||||
@@ -3214,11 +3212,11 @@ public class DeploymentManagerActor : ReceiveActor, IWithTimers
|
||||
|
||||
/// <summary>
|
||||
/// Writes attribute values on a deployed instance for a Route.To().SetAttribute(s)
|
||||
/// call (or a central Test Run bound to the instance). Each write is Ask'd to the
|
||||
/// Instance Actor, which routes data-sourced attributes through the DCL and static
|
||||
/// attributes to a persisted override. The response reflects the real per-attribute
|
||||
/// outcome (a non-existent attribute or a failed device write reports failure),
|
||||
/// rather than an unconditional optimistic ack.
|
||||
/// call (or a central Test Run bound to the instance). The writes go to the Instance Actor
|
||||
/// in ONE batched Ask (review 2026-10-21 01/SR-P4), which routes data-sourced attributes
|
||||
/// through the DCL and static attributes to a persisted override. The response reflects the
|
||||
/// real per-attribute outcome (a non-existent attribute or a failed device write reports
|
||||
/// failure), rather than an unconditional optimistic ack.
|
||||
/// </summary>
|
||||
private void RouteInboundApiSetAttributes(RouteToSetAttributesRequest request)
|
||||
{
|
||||
@@ -3233,9 +3231,8 @@ public class DeploymentManagerActor : ReceiveActor, IWithTimers
|
||||
|
||||
var sender = Sender;
|
||||
var correlationId = request.CorrelationId;
|
||||
var asks = request.AttributeValues
|
||||
.Select(kvp => instanceActor.Ask<SetStaticAttributeResponse>(
|
||||
new SetStaticAttributeCommand(
|
||||
var writes = request.AttributeValues
|
||||
.Select(kvp => new SetStaticAttributeCommand(
|
||||
correlationId, request.InstanceUniqueName, kvp.Key, kvp.Value, DateTimeOffset.UtcNow,
|
||||
// (ParentExecutionId tag-cascade): the routed
|
||||
// write's originating execution is the inbound API request
|
||||
@@ -3245,11 +3242,16 @@ public class DeploymentManagerActor : ReceiveActor, IWithTimers
|
||||
// script run under the inbound execution.
|
||||
SourceExecutionId: request.ParentExecutionId,
|
||||
// Flow Debugger (design §5.3): a traced routed write arms the echo stamp.
|
||||
Trace: request.Trace),
|
||||
TimeSpan.FromSeconds(30)))
|
||||
.ToArray();
|
||||
Trace: request.Trace))
|
||||
.ToList();
|
||||
|
||||
Task.WhenAll(asks).ContinueWith(t =>
|
||||
// Each data-sourced write inside the batch is bounded by its own 30 s DCL Ask, and the
|
||||
// writes run concurrently; the slack keeps a device timeout surfacing as that attribute's
|
||||
// own failure rather than as a whole-batch Ask timeout.
|
||||
instanceActor.Ask<InstanceSetAttributesResponse>(
|
||||
new InstanceSetAttributesRequest(correlationId, writes),
|
||||
TimeSpan.FromSeconds(35))
|
||||
.ContinueWith(t =>
|
||||
{
|
||||
if (!t.IsCompletedSuccessfully)
|
||||
return new RouteToSetAttributesResponse(
|
||||
@@ -3257,7 +3259,7 @@ public class DeploymentManagerActor : ReceiveActor, IWithTimers
|
||||
t.Exception?.GetBaseException().Message ?? "Attribute write timed out",
|
||||
DateTimeOffset.UtcNow);
|
||||
|
||||
var failures = t.Result
|
||||
var failures = t.Result.Results
|
||||
.Where(r => !r.Success)
|
||||
.Select(r => $"{r.AttributeName}: {r.ErrorMessage}")
|
||||
.ToArray();
|
||||
|
||||
Reference in New Issue
Block a user