perf(adminui): pooled DbContext factory; role-grants reads per-call, not per-circuit
This commit is contained in:
@@ -139,7 +139,10 @@ internal static class UnsTreeTestDb
|
||||
|
||||
/// <summary>
|
||||
/// Minimal <see cref="IDbContextFactory{TContext}"/> that hands back contexts sharing a
|
||||
/// single InMemory database name — the test-side stand-in for the runtime pooled factory.
|
||||
/// single InMemory database name — the test-side stand-in for the runtime factory, which IS
|
||||
/// pooled (<c>AddPooledDbContextFactory</c> in <c>ServiceCollectionExtensions.AddOtOpcUaConfigDb</c>).
|
||||
/// This stand-in builds a fresh context per call rather than renting one: pooling is a
|
||||
/// construction-cost optimisation, and nothing under test can observe the difference.
|
||||
/// </summary>
|
||||
private sealed class NamedFactory(string name) : IDbContextFactory<OtOpcUaConfigDbContext>
|
||||
{
|
||||
|
||||
+46
-14
@@ -25,20 +25,21 @@ public sealed class DependencyMuxActorTests : RuntimeActorTestBase
|
||||
mux.Tell(new DriverInstanceActor.AttributeValuePublished("driver-1", "tag-3", 30, OpcUaStatus.Good, DateTime.UtcNow));
|
||||
mux.Tell(new DriverInstanceActor.AttributeValuePublished("driver-1", "tag-2", 20, OpcUaStatus.Good, DateTime.UtcNow));
|
||||
|
||||
// The mux emits ONLY batches — the per-value comms-loss ingress fans out as a single-item batch.
|
||||
// subA hears tag-1 + tag-2.
|
||||
var aMsgs = new[]
|
||||
{
|
||||
subA.ExpectMsg<VirtualTagActor.DependencyValueChanged>(),
|
||||
subA.ExpectMsg<VirtualTagActor.DependencyValueChanged>(),
|
||||
}.OrderBy(m => m.TagId).ToList();
|
||||
subA.ExpectMsg<VirtualTagActor.DependencyValueBatch>(),
|
||||
subA.ExpectMsg<VirtualTagActor.DependencyValueBatch>(),
|
||||
}.Select(b => b.Items.ShouldHaveSingleItem()).OrderBy(m => m.TagId).ToList();
|
||||
aMsgs.Select(m => m.TagId).ShouldBe(new[] { "tag-1", "tag-2" });
|
||||
|
||||
// subB hears tag-3 + tag-2.
|
||||
var bMsgs = new[]
|
||||
{
|
||||
subB.ExpectMsg<VirtualTagActor.DependencyValueChanged>(),
|
||||
subB.ExpectMsg<VirtualTagActor.DependencyValueChanged>(),
|
||||
}.OrderBy(m => m.TagId).ToList();
|
||||
subB.ExpectMsg<VirtualTagActor.DependencyValueBatch>(),
|
||||
subB.ExpectMsg<VirtualTagActor.DependencyValueBatch>(),
|
||||
}.Select(b => b.Items.ShouldHaveSingleItem()).OrderBy(m => m.TagId).ToList();
|
||||
bMsgs.Select(m => m.TagId).ShouldBe(new[] { "tag-2", "tag-3" });
|
||||
|
||||
subA.ExpectNoMsg(TimeSpan.FromMilliseconds(100));
|
||||
@@ -67,7 +68,7 @@ public sealed class DependencyMuxActorTests : RuntimeActorTestBase
|
||||
|
||||
mux.Tell(new DependencyMuxActor.RegisterInterest(new[] { "tag-1" }, sub.Ref));
|
||||
mux.Tell(new DriverInstanceActor.AttributeValuePublished("driver-1", "tag-1", 10, OpcUaStatus.Good, DateTime.UtcNow));
|
||||
sub.ExpectMsg<VirtualTagActor.DependencyValueChanged>();
|
||||
sub.ExpectMsg<VirtualTagActor.DependencyValueBatch>();
|
||||
|
||||
mux.Tell(new DependencyMuxActor.UnregisterInterest(sub.Ref));
|
||||
mux.Tell(new DriverInstanceActor.AttributeValuePublished("driver-1", "tag-1", 20, OpcUaStatus.Good, DateTime.UtcNow));
|
||||
@@ -88,7 +89,7 @@ public sealed class DependencyMuxActorTests : RuntimeActorTestBase
|
||||
sub.ExpectNoMsg(TimeSpan.FromMilliseconds(200));
|
||||
|
||||
mux.Tell(new DriverInstanceActor.AttributeValuePublished("driver-1", "tag-2", 20, OpcUaStatus.Good, DateTime.UtcNow));
|
||||
sub.ExpectMsg<VirtualTagActor.DependencyValueChanged>().TagId.ShouldBe("tag-2");
|
||||
sub.ExpectMsg<VirtualTagActor.DependencyValueBatch>().Items.ShouldHaveSingleItem().TagId.ShouldBe("tag-2");
|
||||
}
|
||||
|
||||
/// <summary>Verifies that a virtual tag actor registers dependencies with the mux during PreStart and that evaluation fires end-to-end.</summary>
|
||||
@@ -148,12 +149,13 @@ public sealed class DependencyMuxActorTests : RuntimeActorTestBase
|
||||
new DriverInstanceActor.AttributeValueItem("ref-1", 42, OpcUaStatus.Good, DateTime.UtcNow),
|
||||
}));
|
||||
|
||||
subscriber.ExpectMsg<VirtualTagActor.DependencyValueChanged>().TagId.ShouldBe("ref-1");
|
||||
subscriber.ExpectMsg<VirtualTagActor.DependencyValueBatch>()
|
||||
.Items.ShouldHaveSingleItem().TagId.ShouldBe("ref-1");
|
||||
}
|
||||
|
||||
/// <summary>A batched driver burst (the primary ingress from DriverHostActor since the value-pipeline
|
||||
/// batching work) is filtered per item: a subscriber hears exactly one DependencyValueChanged for the
|
||||
/// ref it registered, carrying that item's own value/timestamp/quality, and nothing for the rest of the
|
||||
/// batching work) is filtered per item: a subscriber hears one batch carrying exactly the item for the
|
||||
/// ref it registered, with that item's own value/timestamp/quality, and nothing for the rest of the
|
||||
/// burst.</summary>
|
||||
[Fact]
|
||||
public void AttributeValueBatchPublished_is_filtered_per_item_to_interested_subscribers()
|
||||
@@ -170,14 +172,43 @@ public sealed class DependencyMuxActorTests : RuntimeActorTestBase
|
||||
new DriverInstanceActor.AttributeValueItem("tag-x", 10, OpcUaStatus.BadNoCommunication, tsX),
|
||||
}));
|
||||
|
||||
var dep = sub.ExpectMsg<VirtualTagActor.DependencyValueChanged>(PresenceBudget);
|
||||
// Nothing for tag-y — no subscriber registered for it, so the batch carries tag-x alone.
|
||||
var dep = sub.ExpectMsg<VirtualTagActor.DependencyValueBatch>(PresenceBudget).Items.ShouldHaveSingleItem();
|
||||
dep.TagId.ShouldBe("tag-x");
|
||||
dep.Value.ShouldBe(10);
|
||||
dep.TimestampUtc.ShouldBe(tsX);
|
||||
// #519: the item's own status sub-code rides through, not a Good/Bad bucket.
|
||||
dep.StatusCode.ShouldBe(OpcUaStatus.BadNoCommunication);
|
||||
|
||||
// Nothing for tag-y — no subscriber registered for it.
|
||||
sub.ExpectNoMsg(TimeSpan.FromMilliseconds(200));
|
||||
}
|
||||
|
||||
/// <summary>One driver burst costs each subscriber exactly ONE mailbox message, not one per matched
|
||||
/// item: the mux accumulates each subscriber's matched slice and Tells it as a single
|
||||
/// <c>DependencyValueBatch</c>. The unmatched item never reaches the subscriber, and no second message
|
||||
/// follows the batch.</summary>
|
||||
[Fact]
|
||||
public void A_burst_reaches_each_subscriber_as_one_batch_message()
|
||||
{
|
||||
var mux = Sys.ActorOf(DependencyMuxActor.Props());
|
||||
var sub = CreateTestProbe();
|
||||
mux.Tell(new DependencyMuxActor.RegisterInterest(new[] { "a", "b" }, sub.Ref));
|
||||
|
||||
var tsA = new DateTime(2026, 8, 15, 10, 0, 0, DateTimeKind.Utc);
|
||||
var tsB = new DateTime(2026, 8, 15, 10, 0, 1, DateTimeKind.Utc);
|
||||
mux.Tell(new DriverInstanceActor.AttributeValueBatchPublished("driver-1", new[]
|
||||
{
|
||||
new DriverInstanceActor.AttributeValueItem("a", 1, OpcUaStatus.Good, tsA),
|
||||
new DriverInstanceActor.AttributeValueItem("b", 2, OpcUaStatus.Good, tsB),
|
||||
new DriverInstanceActor.AttributeValueItem("c", 3, OpcUaStatus.Good, tsB),
|
||||
}));
|
||||
|
||||
var batch = sub.ExpectMsg<VirtualTagActor.DependencyValueBatch>(PresenceBudget);
|
||||
batch.Items.Select(i => i.TagId).ShouldBe(new[] { "a", "b" });
|
||||
batch.Items.Select(i => i.Value).ShouldBe(new object?[] { 1, 2 });
|
||||
batch.Items.Select(i => i.TimestampUtc).ShouldBe(new[] { tsA, tsB });
|
||||
|
||||
// The unmatched "c" produced nothing, and the two matched items did NOT arrive as two messages.
|
||||
sub.ExpectNoMsg(TimeSpan.FromMilliseconds(200));
|
||||
}
|
||||
|
||||
@@ -193,7 +224,8 @@ public sealed class DependencyMuxActorTests : RuntimeActorTestBase
|
||||
mux.Tell(new DriverInstanceActor.AttributeValuePublished(
|
||||
"driver-1", "tag-1", 10, OpcUaStatus.Bad, DateTime.UtcNow));
|
||||
|
||||
sub.ExpectMsg<VirtualTagActor.DependencyValueChanged>().StatusCode.ShouldBe(OpcUaStatus.Bad);
|
||||
sub.ExpectMsg<VirtualTagActor.DependencyValueBatch>()
|
||||
.Items.ShouldHaveSingleItem().StatusCode.ShouldBe(OpcUaStatus.Bad);
|
||||
}
|
||||
|
||||
private sealed class EchoSumEvaluator : ZB.MOM.WW.OtOpcUa.Commons.Engines.IVirtualTagEvaluator
|
||||
|
||||
@@ -205,6 +205,77 @@ public sealed class VirtualTagActorTests : RuntimeActorTestBase
|
||||
parent.ExpectMsg<VirtualTagActor.EvaluationResult>().StatusCode.ShouldBe(OpcUaStatus.BadInternalError);
|
||||
}
|
||||
|
||||
/// <summary>A batch carrying K changed inputs evaluates the script ONCE, with every input already
|
||||
/// applied — not once per input. The emitted result is computed from all three values (7 = 1+2+4), so
|
||||
/// the collapse cannot be an evaluation that simply missed the later items.</summary>
|
||||
[Fact]
|
||||
public void A_batch_touching_K_inputs_evaluates_once()
|
||||
{
|
||||
var parent = CreateTestProbe();
|
||||
var evaluator = new CountingSumEvaluator();
|
||||
var actor = parent.ChildActorOf(VirtualTagActor.Props("vt-1", "a+b+c", evaluator: evaluator));
|
||||
|
||||
var ts = new DateTime(2026, 8, 15, 10, 0, 0, DateTimeKind.Utc);
|
||||
actor.Tell(new VirtualTagActor.DependencyValueBatch(new[]
|
||||
{
|
||||
new VirtualTagActor.DependencyValueChanged("a", 1, ts.AddSeconds(-2)),
|
||||
new VirtualTagActor.DependencyValueChanged("b", 2, ts.AddSeconds(-1)),
|
||||
new VirtualTagActor.DependencyValueChanged("c", 4, ts),
|
||||
}));
|
||||
|
||||
var result = parent.ExpectMsg<VirtualTagActor.EvaluationResult>();
|
||||
result.Value.ShouldBe(7);
|
||||
// The single evaluation is stamped with the newest item in the burst.
|
||||
result.TimestampUtc.ShouldBe(ts);
|
||||
|
||||
parent.ExpectNoMsg(TimeSpan.FromMilliseconds(200));
|
||||
evaluator.Calls.ShouldBe(1);
|
||||
}
|
||||
|
||||
/// <summary>Falsification control for <see cref="A_batch_touching_K_inputs_evaluates_once"/>: the SAME
|
||||
/// three values delivered as three separate <c>DependencyValueChanged</c> messages evaluate three times
|
||||
/// and emit three results (1 → 3 → 7, all distinct so the value dedup cannot collapse them). Without
|
||||
/// this, a batch test that counted 1 would also pass against a build where dedup — not the batch
|
||||
/// apply-then-evaluate split — did the collapsing.</summary>
|
||||
[Fact]
|
||||
public void The_same_values_sent_singly_evaluate_once_per_message()
|
||||
{
|
||||
var parent = CreateTestProbe();
|
||||
var evaluator = new CountingSumEvaluator();
|
||||
var actor = parent.ChildActorOf(VirtualTagActor.Props("vt-1", "a+b+c", evaluator: evaluator));
|
||||
|
||||
var ts = new DateTime(2026, 8, 15, 10, 0, 0, DateTimeKind.Utc);
|
||||
actor.Tell(new VirtualTagActor.DependencyValueChanged("a", 1, ts.AddSeconds(-2)));
|
||||
actor.Tell(new VirtualTagActor.DependencyValueChanged("b", 2, ts.AddSeconds(-1)));
|
||||
actor.Tell(new VirtualTagActor.DependencyValueChanged("c", 4, ts));
|
||||
|
||||
parent.ExpectMsg<VirtualTagActor.EvaluationResult>().Value.ShouldBe(1);
|
||||
parent.ExpectMsg<VirtualTagActor.EvaluationResult>().Value.ShouldBe(3);
|
||||
parent.ExpectMsg<VirtualTagActor.EvaluationResult>().Value.ShouldBe(7);
|
||||
|
||||
evaluator.Calls.ShouldBe(3);
|
||||
}
|
||||
|
||||
/// <summary>Test evaluator that sums integer dependencies and counts how many times it was called.</summary>
|
||||
private sealed class CountingSumEvaluator : IVirtualTagEvaluator
|
||||
{
|
||||
private int _calls;
|
||||
|
||||
/// <summary>Gets the number of <see cref="Evaluate"/> calls observed so far.</summary>
|
||||
public int Calls => Volatile.Read(ref _calls);
|
||||
|
||||
/// <summary>Counts the call and evaluates the expression by summing integer dependencies.</summary>
|
||||
/// <param name="id">The tag identifier.</param>
|
||||
/// <param name="expr">The expression string.</param>
|
||||
/// <param name="deps">The dependency values.</param>
|
||||
/// <returns>The sum of integer values in the dependencies.</returns>
|
||||
public VirtualTagEvalResult Evaluate(string id, string expr, IReadOnlyDictionary<string, object?> deps)
|
||||
{
|
||||
Interlocked.Increment(ref _calls);
|
||||
return VirtualTagEvalResult.Ok(deps.Values.OfType<int>().Sum());
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Test evaluator returning a fixed sequence of results, one per Evaluate call; the last
|
||||
/// result repeats once the queue is drained.</summary>
|
||||
private sealed class QueuedEvaluator : IVirtualTagEvaluator
|
||||
|
||||
Reference in New Issue
Block a user