docs(siteruntime): mark WaitAsync deferred items implemented (§3/§4.2/§6) + fast-path throwing-predicate test
This commit is contained in:
@@ -82,9 +82,13 @@ Task<bool> Attributes.WaitAsync(string name, Func<object?, bool> predicate, Time
|
|||||||
|
|
||||||
// Optional richer overload that also returns the matched value + quality.
|
// Optional richer overload that also returns the matched value + quality.
|
||||||
Task<WaitResult> Attributes.WaitForAsync(string name, object? targetValue, TimeSpan timeout);
|
Task<WaitResult> Attributes.WaitForAsync(string name, object? targetValue, TimeSpan timeout);
|
||||||
// record WaitResult(bool Matched, object? Value, string Quality, bool TimedOut);
|
// record WaitResult(bool Matched, object? Value, string? Quality, bool TimedOut);
|
||||||
```
|
```
|
||||||
|
|
||||||
|
> **Status:** IMPLEMENTED. `Attributes.WaitForAsync(...)` returns a `WaitResult`
|
||||||
|
> (`readonly record struct WaitResult(bool Matched, object? Value, string? Quality, bool TimedOut)`
|
||||||
|
> in Commons), populated on match (Value + Quality) and `Matched:false, TimedOut:true` on timeout.
|
||||||
|
|
||||||
Return **bool** (not throw) for the common case — the handshake wants matched/timed-out, not an
|
Return **bool** (not throw) for the common case — the handshake wants matched/timed-out, not an
|
||||||
exception. The value-equality overload is the one the handshake needs and is the one that can also
|
exception. The value-equality overload is the one the handshake needs and is the one that can also
|
||||||
be exposed on the inbound/routed side (§6), because a value serializes and a delegate does not.
|
be exposed on the inbound/routed side (§6), because a value serializes and a delegate does not.
|
||||||
@@ -159,6 +163,10 @@ public record WaitForAttributeTimeout(string CorrelationId);
|
|||||||
- Optional: a `quality == "Good"`-only mode (parameter on the request) if a handshake must ignore
|
- Optional: a `quality == "Good"`-only mode (parameter on the request) if a handshake must ignore
|
||||||
Bad-quality transients.
|
Bad-quality transients.
|
||||||
|
|
||||||
|
> **Status:** IMPLEMENTED as an opt-in `requireGoodQuality` parameter on `WaitAsync`/`WaitForAsync`
|
||||||
|
> (additive trailing `RequireGoodQuality` field on `WaitForAttributeRequest`, gated at both the
|
||||||
|
> fast-path and resolve-loop match sites). Default `false` = quality-agnostic (matches on value only).
|
||||||
|
|
||||||
### 4.3 `ScriptRuntimeContext` (`src/…/SiteRuntime/Scripts/ScriptRuntimeContext.cs`)
|
### 4.3 `ScriptRuntimeContext` (`src/…/SiteRuntime/Scripts/ScriptRuntimeContext.cs`)
|
||||||
- **Thread the script timeout token in.** Add a `CancellationToken scriptTimeoutToken` constructor
|
- **Thread the script timeout token in.** Add a `CancellationToken scriptTimeoutToken` constructor
|
||||||
parameter (today only `_askTimeout` is available to helpers; the per-script `cts.Token` is **not**
|
parameter (today only `_askTimeout` is available to helpers; the per-script `cts.Token` is **not**
|
||||||
@@ -228,6 +236,11 @@ so the routed form takes the encoded target value (the predicate overload stays
|
|||||||
optional: the receiver handshake runs **inside** the template script (site-local), so §3–§5 alone
|
optional: the receiver handshake runs **inside** the template script (site-local), so §3–§5 alone
|
||||||
fully cover the DELMIA/MES use case.
|
fully cover the DELMIA/MES use case.
|
||||||
|
|
||||||
|
> **Status:** IMPLEMENTED. `Route.To(code).WaitForAttribute(name, targetValue, timeout)` is wired
|
||||||
|
> end-to-end (`RouteToWaitForAttributeRequest/Response` → `IInstanceRouter` → `CommunicationService`
|
||||||
|
> → `SiteCommunicationActor` → `DeploymentManagerActor` → `InstanceActor`), value-equality only
|
||||||
|
> across the wire. NOT wired into the CentralUI Test-Run sandbox — that remains a follow-up.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 7. Acceptance criteria
|
## 7. Acceptance criteria
|
||||||
|
|||||||
@@ -189,6 +189,7 @@ Inbound API scripts **cannot** call shared scripts directly — shared scripts a
|
|||||||
- `Route.To("instanceUniqueCode").GetAttributes("attr1", "attr2", ...)` — Read multiple attribute values in a **single call**, returned as a dictionary of name-value pairs.
|
- `Route.To("instanceUniqueCode").GetAttributes("attr1", "attr2", ...)` — Read multiple attribute values in a **single call**, returned as a dictionary of name-value pairs.
|
||||||
- `Route.To("instanceUniqueCode").SetAttribute("attributeName", value)` — Write a single attribute value on a specific instance at any site.
|
- `Route.To("instanceUniqueCode").SetAttribute("attributeName", value)` — Write a single attribute value on a specific instance at any site.
|
||||||
- `Route.To("instanceUniqueCode").SetAttributes(dictionary)` — Write multiple attribute values in a **single call**, accepting a dictionary of name-value pairs.
|
- `Route.To("instanceUniqueCode").SetAttributes(dictionary)` — Write multiple attribute values in a **single call**, accepting a dictionary of name-value pairs.
|
||||||
|
- `Route.To("instanceUniqueCode").WaitForAttribute("attributeName", targetValue, timeout)` — Wait, event-driven, until an attribute on a specific instance at any site reaches `targetValue` (value-equality only across the wire), bounded by `timeout`. Returns `true` if matched within the timeout, `false` if it timed out. The cluster call is bounded by the wait timeout rather than the generic integration timeout.
|
||||||
|
|
||||||
#### Input/Output
|
#### Input/Output
|
||||||
- **Input parameters** are available as defined in the method definition.
|
- **Input parameters** are available as defined in the method definition.
|
||||||
|
|||||||
+72
@@ -565,6 +565,78 @@ public class InstanceActorWaitForAttributeTests : TestKit, IDisposable
|
|||||||
ExpectNoMsg(TimeSpan.FromMilliseconds(500));
|
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) ──────────────────
|
// ── 9. Quality-gated ("Good"-only) matching (spec §4.2) ──────────────────
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
|||||||
Reference in New Issue
Block a user