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:
+131
@@ -0,0 +1,131 @@
|
||||
using System.Text.Json;
|
||||
using Akka.Actor;
|
||||
using Akka.TestKit.Xunit2;
|
||||
using Microsoft.Extensions.Logging.Abstractions;
|
||||
using ZB.MOM.WW.ScadaBridge.Commons.Messages.DataConnection;
|
||||
using ZB.MOM.WW.ScadaBridge.Commons.Messages.Instance;
|
||||
using ZB.MOM.WW.ScadaBridge.Commons.Types.Flattening;
|
||||
using ZB.MOM.WW.ScadaBridge.SiteRuntime.Actors;
|
||||
using ZB.MOM.WW.ScadaBridge.SiteRuntime.Messages;
|
||||
using ZB.MOM.WW.ScadaBridge.SiteRuntime.Persistence;
|
||||
using ZB.MOM.WW.ScadaBridge.SiteRuntime.Scripts;
|
||||
using ZB.MOM.WW.ScadaBridge.TestSupport;
|
||||
|
||||
namespace ZB.MOM.WW.ScadaBridge.SiteRuntime.Tests.Actors;
|
||||
|
||||
/// <summary>
|
||||
/// Review 2026-10-21 01/SR-P4: <c>Route.To().GetAttributes()/SetAttributes()</c> used one Ask
|
||||
/// per attribute. The Instance Actor now answers a whole batch in one message while keeping the
|
||||
/// per-attribute outcome a single read/write would report.
|
||||
/// </summary>
|
||||
public class InstanceActorAttributeBatchTests : TestKit, IDisposable
|
||||
{
|
||||
private readonly SiteStorageService _storage;
|
||||
private readonly ScriptCompilationService _compilationService;
|
||||
private readonly SharedScriptLibrary _sharedScriptLibrary;
|
||||
private readonly TestLocalDb _localDb;
|
||||
|
||||
/// <summary>Creates a temporary LocalDb-backed site store per test.</summary>
|
||||
public InstanceActorAttributeBatchTests()
|
||||
{
|
||||
_localDb = TestLocalDb.CreateTemp("instance-attr-batch");
|
||||
_storage = new SiteStorageService(_localDb.Db, NullLogger<SiteStorageService>.Instance);
|
||||
_storage.InitializeAsync().GetAwaiter().GetResult();
|
||||
_compilationService = new ScriptCompilationService(NullLogger<ScriptCompilationService>.Instance);
|
||||
_sharedScriptLibrary = new SharedScriptLibrary(
|
||||
_compilationService, NullLogger<SharedScriptLibrary>.Instance);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
void IDisposable.Dispose()
|
||||
{
|
||||
Shutdown();
|
||||
var path = _localDb.Path;
|
||||
_localDb.Dispose();
|
||||
TestLocalDb.DeleteFiles(path);
|
||||
}
|
||||
|
||||
private IActorRef StartInstance(IActorRef? dcl = null)
|
||||
{
|
||||
var config = new FlattenedConfiguration
|
||||
{
|
||||
InstanceUniqueName = "Batch1",
|
||||
Attributes =
|
||||
[
|
||||
new ResolvedAttribute { CanonicalName = "Mode", Value = "Manual", DataType = "String" },
|
||||
new ResolvedAttribute { CanonicalName = "Count", Value = "3", DataType = "Int32" },
|
||||
new ResolvedAttribute
|
||||
{
|
||||
CanonicalName = "Speed", Value = "0", DataType = "Int32",
|
||||
DataSourceReference = "ns=2;s=Speed", BoundDataConnectionName = "conn-1"
|
||||
}
|
||||
]
|
||||
};
|
||||
var json = JsonSerializer.Serialize(config);
|
||||
return ActorOf(Props.Create(() => new InstanceActor(
|
||||
"Batch1", json, _storage, _compilationService, _sharedScriptLibrary, null,
|
||||
new SiteRuntimeOptions(), NullLogger<InstanceActor>.Instance, dcl)));
|
||||
}
|
||||
|
||||
private static SetStaticAttributeCommand Write(string attribute, string value) =>
|
||||
new("corr", "Batch1", attribute, value, DateTimeOffset.UtcNow);
|
||||
|
||||
/// <summary>One reply, index-aligned, with found/quality per attribute.</summary>
|
||||
[Fact]
|
||||
public void GetAttributes_AnswersEveryNameInOneReply()
|
||||
{
|
||||
var instance = StartInstance();
|
||||
|
||||
instance.Tell(new InstanceGetAttributesRequest("corr", "Batch1", ["Count", "Missing", "Mode", "Speed"]));
|
||||
var reply = ExpectMsg<InstanceGetAttributesResponse>(TimeSpan.FromSeconds(10));
|
||||
|
||||
Assert.Equal(["Count", "Missing", "Mode", "Speed"], reply.Results.Select(r => r.AttributeName));
|
||||
Assert.Equal("3", reply.Results[0].Value?.ToString());
|
||||
Assert.False(reply.Results[1].Found);
|
||||
Assert.Equal("Manual", reply.Results[2].Value?.ToString());
|
||||
Assert.Equal("Uncertain", reply.Results[3].Quality);
|
||||
ExpectNoMsg(TimeSpan.FromMilliseconds(200));
|
||||
}
|
||||
|
||||
/// <summary>Static writes apply; an unknown attribute fails on its own row only.</summary>
|
||||
[Fact]
|
||||
public void SetAttributes_StaticAndUnknown_ReportsPerAttributeOutcome()
|
||||
{
|
||||
var instance = StartInstance();
|
||||
|
||||
instance.Tell(new InstanceSetAttributesRequest("corr", [Write("Mode", "Auto"), Write("Nope", "1")]));
|
||||
var reply = ExpectMsg<InstanceSetAttributesResponse>(TimeSpan.FromSeconds(10));
|
||||
|
||||
Assert.True(reply.Results[0].Success);
|
||||
Assert.False(reply.Results[1].Success);
|
||||
Assert.Contains("Unknown attribute 'Nope'", reply.Results[1].ErrorMessage);
|
||||
|
||||
instance.Tell(new GetAttributeRequest("g", "Batch1", "Mode", DateTimeOffset.UtcNow));
|
||||
Assert.Equal("Auto", ExpectMsg<GetAttributeResponse>().Value?.ToString());
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// A data-sourced write inside the batch goes to the DCL; the batch reply waits for the
|
||||
/// device answer and carries its failure on that attribute's row.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void SetAttributes_DataSourcedWrite_WaitsForTheDclAndCarriesItsOutcome()
|
||||
{
|
||||
var dcl = CreateTestProbe();
|
||||
var instance = StartInstance(dcl.Ref);
|
||||
dcl.ExpectMsg<SubscribeTagsRequest>(TimeSpan.FromSeconds(10));
|
||||
|
||||
instance.Tell(new InstanceSetAttributesRequest("corr", [Write("Count", "9"), Write("Speed", "42")]));
|
||||
|
||||
var write = dcl.ExpectMsg<WriteTagRequest>(TimeSpan.FromSeconds(10));
|
||||
Assert.Equal("ns=2;s=Speed", write.TagPath);
|
||||
ExpectNoMsg(TimeSpan.FromMilliseconds(300)); // the reply waits for the device
|
||||
dcl.Reply(new WriteTagResponse(write.CorrelationId, false, "device rejected", DateTimeOffset.UtcNow));
|
||||
|
||||
var reply = ExpectMsg<InstanceSetAttributesResponse>(TimeSpan.FromSeconds(10));
|
||||
Assert.True(reply.Results[0].Success);
|
||||
Assert.False(reply.Results[1].Success);
|
||||
Assert.Equal("Speed", reply.Results[1].AttributeName);
|
||||
Assert.Equal("device rejected", reply.Results[1].ErrorMessage);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user