perf(site-runtime): deserialize a deploy's flattened config once and hand the parsed object down (review 2026-10-21 01/SR-P5)
The deploy compile warm parses once off-thread; the parsed FlattenedConfiguration rides DeployCompileWarmed to the gate (new Validate(FlattenedConfiguration) overload), the redeploy buffer, CreateInstanceActor's DCL ensure and a new InstanceActor constructor overload. Stored-row paths parse once inside CreateInstanceActor; an unparseable stored row still fails init via S6.
This commit is contained in:
+98
@@ -0,0 +1,98 @@
|
||||
using System.Text.Json;
|
||||
using Akka.Actor;
|
||||
using Akka.TestKit.Xunit2;
|
||||
using Microsoft.Extensions.Logging.Abstractions;
|
||||
using ZB.MOM.WW.ScadaBridge.Commons.Messages.Deployment;
|
||||
using ZB.MOM.WW.ScadaBridge.Commons.Messages.Instance;
|
||||
using ZB.MOM.WW.ScadaBridge.Commons.Types.Enums;
|
||||
using ZB.MOM.WW.ScadaBridge.Commons.Types.Flattening;
|
||||
using ZB.MOM.WW.ScadaBridge.SiteRuntime.Actors;
|
||||
using ZB.MOM.WW.ScadaBridge.SiteRuntime.Persistence;
|
||||
using ZB.MOM.WW.ScadaBridge.SiteRuntime.Scripts;
|
||||
using ZB.MOM.WW.ScadaBridge.SiteRuntime.Tests.TestSupport;
|
||||
using ZB.MOM.WW.ScadaBridge.TestSupport;
|
||||
|
||||
namespace ZB.MOM.WW.ScadaBridge.SiteRuntime.Tests.Actors;
|
||||
|
||||
/// <summary>
|
||||
/// Review 2026-10-21 01/SR-P5: a deploy's flattened config is deserialized ONCE (in the
|
||||
/// off-thread compile warm) and the parsed object is handed to the gate, the DCL-connection
|
||||
/// ensure and the Instance Actor. These pin the two contracts that refactor must keep: a config
|
||||
/// that does not parse is still a readable gate rejection with nothing applied, and an Instance
|
||||
/// Actor built from the parsed object behaves like one built from the JSON.
|
||||
/// </summary>
|
||||
public class DeploymentManagerParseOnceTests : 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 DeploymentManagerParseOnceTests()
|
||||
{
|
||||
_localDb = TestLocalDb.CreateTemp("dm-parse-once");
|
||||
_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 CreateBootedDeploymentManager()
|
||||
{
|
||||
var health = new DeploymentManagerStartupProbe();
|
||||
var dm = ActorOf(Props.Create(() => new DeploymentManagerActor(
|
||||
_storage, _compilationService, _sharedScriptLibrary, null,
|
||||
new SiteRuntimeOptions { InstanceSweepIntervalSeconds = 0 },
|
||||
NullLogger<DeploymentManagerActor>.Instance,
|
||||
dclManager: null, healthCollector: health, serviceProvider: null,
|
||||
loggerFactory: null, configFetcher: null)));
|
||||
AwaitCondition(() => health.ReportCount > 0, TimeSpan.FromSeconds(20), TimeSpan.FromMilliseconds(25));
|
||||
return dm;
|
||||
}
|
||||
|
||||
/// <summary>Malformed JSON is rejected by the gate with the deserialization error; nothing is persisted.</summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task Deploy_MalformedConfig_IsRejectedWithTheParseError()
|
||||
{
|
||||
var dm = CreateBootedDeploymentManager();
|
||||
|
||||
dm.Tell(new DeployInstanceCommand("dep-1", "Bad1", "r1", "{not json", "tester", DateTimeOffset.UtcNow));
|
||||
var response = ExpectMsg<DeploymentStatusResponse>(TimeSpan.FromSeconds(15));
|
||||
|
||||
Assert.Equal(DeploymentStatus.Failed, response.Status);
|
||||
Assert.Contains("failed to deserialize", response.ErrorMessage);
|
||||
Assert.Null(await _storage.GetDeployedConfigAsync("Bad1"));
|
||||
}
|
||||
|
||||
/// <summary>A deploy built from the parsed object serves its configured attributes.</summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task Deploy_ParsedOncePath_CreatesAWorkingInstance()
|
||||
{
|
||||
var dm = CreateBootedDeploymentManager();
|
||||
var json = JsonSerializer.Serialize(new FlattenedConfiguration
|
||||
{
|
||||
InstanceUniqueName = "Good1",
|
||||
Attributes = [new ResolvedAttribute { CanonicalName = "A", Value = "7", DataType = "Int32" }]
|
||||
});
|
||||
|
||||
dm.Tell(new DeployInstanceCommand("dep-2", "Good1", "r1", json, "tester", DateTimeOffset.UtcNow));
|
||||
Assert.Equal(DeploymentStatus.Success, ExpectMsg<DeploymentStatusResponse>(TimeSpan.FromSeconds(15)).Status);
|
||||
|
||||
var instance = await Sys.ActorSelection(dm.Path / "Good1").ResolveOne(TimeSpan.FromSeconds(5));
|
||||
instance.Tell(new GetAttributeRequest("g", "Good1", "A", DateTimeOffset.UtcNow));
|
||||
Assert.Equal("7", ExpectMsg<GetAttributeResponse>().Value?.ToString());
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user