using System.Text.Json;
using Akka.Actor;
using Microsoft.EntityFrameworkCore;
using Shouldly;
using Xunit;
using ZB.MOM.WW.OtOpcUa.Commons.Interfaces;
using ZB.MOM.WW.OtOpcUa.Commons.Messages.Deploy;
using ZB.MOM.WW.OtOpcUa.Commons.Messages.Fleet;
using ZB.MOM.WW.OtOpcUa.Commons.Types;
using ZB.MOM.WW.OtOpcUa.Configuration;
using ZB.MOM.WW.OtOpcUa.Configuration.Entities;
using ZB.MOM.WW.OtOpcUa.Configuration.Enums;
using ZB.MOM.WW.OtOpcUa.Runtime.Drivers;
using ZB.MOM.WW.OtOpcUa.Runtime.Tests.Harness;
namespace ZB.MOM.WW.OtOpcUa.Runtime.Tests.Drivers;
///
/// Perf roadmap Task 26 — the Direct-mode artifact read is I/O (an EF read of a potentially
/// multi-MB gzip blob out of central SQL) and must not run on the actor thread.
///
///
///
/// The FetchAndCache path has always piped its artifact fetch back to Self for
/// exactly this reason. Direct read the blob INLINE inside the receive, so every value
/// batch, diagnostics request and driver command queued behind central SQL for the whole read —
/// on a WAN link or a loaded server, for as long as that read takes.
///
///
/// The load-bearing assertion is therefore that the host answers an unrelated message
/// while the read is still in flight, not that the ACK eventually arrives: a blocking read still
/// acks, just late and behind everything else.
///
///
public sealed class DriverHostActorDirectApplyOffMailboxTests : RuntimeActorTestBase
{
private static readonly NodeId TestNode = NodeId.Parse("driver-test");
private static readonly RevisionHash RevA = RevisionHash.Parse(new string('a', 64));
[Fact]
public void A_slow_direct_artifact_read_does_not_block_the_host_mailbox()
{
var inner = NewInMemoryDbFactory();
var db = new GatedConfigDbFactory(inner);
var deploymentId = SeedDeployment(inner, RevA);
var coordinator = CreateTestProbe();
var actor = Sys.ActorOf(DriverHostActor.Props(
db, TestNode, coordinator.Ref,
localRoles: new HashSet { "driver" }));
try
{
// Barrier: an answered GetDiagnostics proves PreStart (and its bootstrap ConfigDb read) is
// done, so the allowance below counts only the apply's own contexts.
actor.Tell(new GetDiagnostics(CorrelationId.NewId()), coordinator.Ref);
coordinator.ExpectMsg(TimeSpan.FromSeconds(5));
// Let ONE more context through — the apply's Applying-row upsert, which is a deliberate
// on-thread write (bounded, one row) — and block the next, which is the artifact read.
// Both misconfigurations of this number fail LOUDLY rather than passing vacuously: too
// small blocks the upsert and the assertion below times out even on a fixed build; too
// large never engages the gate and the presence assertion on Blocked fails.
db.CloseAfter(allowThrough: 1);
actor.Tell(new DispatchDeployment(deploymentId, RevA, CorrelationId.NewId()));
// The read was entered and has NOT completed — the gate is still closed.
AwaitAssert(() => db.Blocked.ShouldBe(1), duration: PresenceBudget);
db.Unblocked.ShouldBe(0);
// THE assertion: with the artifact read still in flight the host is still processing its
// mailbox. Read inline on the actor thread, this times out.
actor.Tell(new GetDiagnostics(CorrelationId.NewId()), coordinator.Ref);
coordinator.ExpectMsg(TimeSpan.FromSeconds(3));
// Releasing the read completes the apply exactly as before — same Applied ACK, same revision.
db.Open();
coordinator.ExpectMsg(TimeSpan.FromSeconds(10)).Outcome.ShouldBe(ApplyAckOutcome.Applied);
actor.Tell(new GetDiagnostics(CorrelationId.NewId()), coordinator.Ref);
coordinator.ExpectMsg(TimeSpan.FromSeconds(5)).CurrentRevision.ShouldBe(RevA);
}
finally
{
// Never leave the gate closed: a parked read would hang ActorSystem shutdown.
db.Open();
}
}
/// Seeds a sealed, parseable deployment and returns its id.
private static DeploymentId SeedDeployment(IDbContextFactory db, RevisionHash rev)
{
var artifact = JsonSerializer.SerializeToUtf8Bytes(new { DriverInstances = Array.Empty