docs(siteruntime): mark WaitAsync deferred items implemented (§3/§4.2/§6) + fast-path throwing-predicate test
This commit is contained in:
+72
@@ -565,6 +565,78 @@ public class InstanceActorWaitForAttributeTests : TestKit, IDisposable
|
||||
ExpectNoMsg(TimeSpan.FromMilliseconds(500));
|
||||
}
|
||||
|
||||
// ── 8b. CRITICAL 2 (fast-path): throwing predicate on already-held value ──
|
||||
|
||||
/// <summary>
|
||||
/// CRITICAL 2 regression (fast-path analogue of
|
||||
/// <see cref="WaitForAttribute_ThrowingPredicate_IsIsolated_SiblingStillMatches"/>):
|
||||
/// a predicate that THROWS is registered against an attribute that ALREADY holds a
|
||||
/// value, so the fast-path <c>test(current)</c> runs and throws. The actor must
|
||||
/// (a) reply a non-matched <c>WaitForAttributeResponse</c> with a non-null
|
||||
/// <c>ErrorMessage</c> (predicate-threw), (b) stay alive/responsive (it answers a
|
||||
/// subsequent <c>GetAttributeRequest</c>), and (c) NOT register the waiter — there
|
||||
/// is no later/second reply even after a value change on that attribute (the
|
||||
/// fast-path guard returns WITHOUT scheduling a timeout or storing the waiter).
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void WaitForAttribute_ThrowingPredicate_FastPath_RepliesError_NoRegistration_ActorStaysAlive()
|
||||
{
|
||||
const string tag = "ns=3;s=State";
|
||||
var config = new FlattenedConfiguration
|
||||
{
|
||||
InstanceUniqueName = "Pump1",
|
||||
Attributes =
|
||||
[
|
||||
new ResolvedAttribute
|
||||
{
|
||||
// Present from construction so the fast-path TryGetValue HITS and
|
||||
// the predicate runs on the current value (and throws).
|
||||
CanonicalName = "State", Value = "init", DataType = "String",
|
||||
DataSourceReference = tag, BoundDataConnectionName = "PLC"
|
||||
}
|
||||
]
|
||||
};
|
||||
|
||||
var dcl = CreateTestProbe();
|
||||
var actor = ActorOf(Props.Create(() => new InstanceActor(
|
||||
"Pump1",
|
||||
JsonSerializer.Serialize(config),
|
||||
_storage,
|
||||
_compilationService,
|
||||
_sharedScriptLibrary,
|
||||
null,
|
||||
_options,
|
||||
NullLogger<InstanceActor>.Instance,
|
||||
dcl.Ref)));
|
||||
|
||||
dcl.ExpectMsg<SubscribeTagsRequest>(TimeSpan.FromSeconds(5));
|
||||
|
||||
// Predicate THROWS unconditionally — the current value "init" is already
|
||||
// present, so the fast-path test(current) executes it and throws.
|
||||
Func<object?, bool> boom = _ => throw new InvalidOperationException("kaboom");
|
||||
actor.Tell(new WaitForAttributeRequest(
|
||||
"wfa-fp-throw", "Pump1", "State",
|
||||
null, boom, TimeSpan.FromSeconds(30), DateTimeOffset.UtcNow));
|
||||
|
||||
// (a) Non-matched error reply (predicate-threw), guarded on the fast-path.
|
||||
var response = ExpectMsg<WaitForAttributeResponse>(TimeSpan.FromSeconds(5));
|
||||
Assert.Equal("wfa-fp-throw", response.CorrelationId);
|
||||
Assert.False(response.Matched);
|
||||
Assert.False(response.TimedOut);
|
||||
Assert.NotNull(response.ErrorMessage);
|
||||
Assert.Contains("Wait predicate threw", response.ErrorMessage);
|
||||
|
||||
// (b) The actor stayed alive and responsive: a follow-up request resolves.
|
||||
actor.Tell(new GetAttributeRequest("get-after-fp", "Pump1", "State", DateTimeOffset.UtcNow));
|
||||
var get = ExpectMsg<GetAttributeResponse>(TimeSpan.FromSeconds(5));
|
||||
Assert.Equal("init", get.Value);
|
||||
|
||||
// (c) The waiter was NOT registered (no timeout scheduled): driving a value
|
||||
// change on "State" produces NO further WaitForAttributeResponse.
|
||||
actor.Tell(new TagValueUpdate("PLC", tag, "ready", QualityCode.Good, DateTimeOffset.UtcNow));
|
||||
ExpectNoMsg(TimeSpan.FromMilliseconds(500));
|
||||
}
|
||||
|
||||
// ── 9. Quality-gated ("Good"-only) matching (spec §4.2) ──────────────────
|
||||
|
||||
/// <summary>
|
||||
|
||||
Reference in New Issue
Block a user