feat(siteruntime): event-driven Attributes.WaitAsync attribute-change helper
Adds InstanceActor one-shot waiter registry (fast-path + change-match + scheduled timeout self-eviction), threads per-script timeout token through ScriptRuntimeContext, and exposes Attributes.WaitAsync(value|predicate, timeout). Replaces handshake busy-poll. Implements spec docs/plans/2026-06-17-waitfor-attribute-change-helper-spec.md §3-§5; §6 routed variant + WaitForAsync + quality-only mode deferred.
This commit is contained in:
@@ -46,6 +46,16 @@ public class ScriptRuntimeContext
|
||||
private readonly ILogger _logger;
|
||||
private readonly string _instanceName;
|
||||
|
||||
/// <summary>
|
||||
/// WaitForAttribute (spec §4.3): the per-script execution-timeout token from
|
||||
/// the owning <c>ScriptExecutionActor</c>/<c>AlarmExecutionActor</c>
|
||||
/// (<c>cts.Token</c>). Bounds the <c>Attributes.WaitAsync</c> Ask so a script
|
||||
/// that hits its own <c>ExecutionTimeoutSeconds</c> abandons the wait. Defaults
|
||||
/// to <see cref="CancellationToken.None"/> for contexts that do not thread one
|
||||
/// (legacy callers / tests / the alarm path when it has no CTS).
|
||||
/// </summary>
|
||||
private readonly CancellationToken _scriptTimeoutToken;
|
||||
|
||||
/// <summary>
|
||||
/// WP-13: External system client for ExternalSystem.Call/CachedCall.
|
||||
/// </summary>
|
||||
@@ -194,6 +204,13 @@ public class ScriptRuntimeContext
|
||||
/// <c>ILogger.LogError</c> + throw. When null the existing behaviour is
|
||||
/// unchanged; all existing callers and tests remain source-compatible.
|
||||
/// </param>
|
||||
/// <param name="scriptTimeoutToken">
|
||||
/// WaitForAttribute (spec §4.3): the per-script execution-timeout token
|
||||
/// (<c>cts.Token</c> on the owning execution actor) used to bound
|
||||
/// <c>Attributes.WaitAsync</c>. Defaults to
|
||||
/// <see cref="CancellationToken.None"/> for callers / tests that do not
|
||||
/// thread one — those waits are bounded only by their own timeout.
|
||||
/// </param>
|
||||
public ScriptRuntimeContext(
|
||||
IActorRef instanceActor,
|
||||
IActorRef self,
|
||||
@@ -215,7 +232,8 @@ public class ScriptRuntimeContext
|
||||
Guid? executionId = null,
|
||||
Guid? parentExecutionId = null,
|
||||
string? sourceNode = null,
|
||||
ISiteEventLogger? siteEventLogger = null)
|
||||
ISiteEventLogger? siteEventLogger = null,
|
||||
CancellationToken scriptTimeoutToken = default)
|
||||
{
|
||||
_instanceActor = instanceActor;
|
||||
_self = self;
|
||||
@@ -245,6 +263,9 @@ public class ScriptRuntimeContext
|
||||
_parentExecutionId = parentExecutionId;
|
||||
// M2.12 (#25): optional — null when not wired (tests / AlarmExecutionActor).
|
||||
_siteEventLogger = siteEventLogger;
|
||||
// WaitForAttribute (spec §4.3): default(CancellationToken) == None when
|
||||
// not threaded in — the WaitAsync Ask is then bounded only by its own timeout.
|
||||
_scriptTimeoutToken = scriptTimeoutToken;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -297,7 +318,11 @@ public class ScriptRuntimeContext
|
||||
// …parented to THIS run's execution id (the spawner).
|
||||
parentExecutionId: _executionId,
|
||||
sourceNode: _sourceNode,
|
||||
siteEventLogger: _siteEventLogger);
|
||||
siteEventLogger: _siteEventLogger,
|
||||
// WaitForAttribute (spec §4.3): an inline shared-script call shares the
|
||||
// parent run's execution-timeout token so a WaitAsync inside the shared
|
||||
// script is bounded by the SAME script deadline.
|
||||
scriptTimeoutToken: _scriptTimeoutToken);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -360,6 +385,42 @@ public class ScriptRuntimeContext
|
||||
return response.Value;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// WaitForAttribute (spec §3-§5): waits event-driven for an attribute to reach
|
||||
/// a value (encoded-equality), satisfy a site-local predicate, or change at all,
|
||||
/// bounded by <paramref name="timeout"/>. Returns <c>true</c> if matched within
|
||||
/// the timeout, <c>false</c> on timeout — NEVER throws on timeout. The backing
|
||||
/// <c>Attributes.WaitAsync</c> for the accessor.
|
||||
///
|
||||
/// <para>
|
||||
/// The Ask is bounded by the script's own execution-timeout token (§4.3): a
|
||||
/// script that hits its <c>ExecutionTimeoutSeconds</c> abandons the wait. The
|
||||
/// Ask timeout is the wait timeout plus a small <see cref="_askTimeout"/> slack
|
||||
/// so the InstanceActor's own scheduled timeout reply is the authoritative path
|
||||
/// for the false/timed-out outcome, not the Ask deadline.
|
||||
/// </para>
|
||||
/// </summary>
|
||||
/// <param name="name">The scope-resolved attribute name to wait on.</param>
|
||||
/// <param name="targetValueEncoded">
|
||||
/// The codec-encoded target value; null (with null <paramref name="predicate"/>)
|
||||
/// means "any change".
|
||||
/// </param>
|
||||
/// <param name="predicate">Site-local predicate; null when the encoded target is used.</param>
|
||||
/// <param name="timeout">How long to wait before returning false.</param>
|
||||
/// <returns><c>true</c> on match within the timeout; <c>false</c> on timeout.</returns>
|
||||
public async Task<bool> WaitAttribute(
|
||||
string name, string? targetValueEncoded, Func<object?, bool>? predicate, TimeSpan timeout)
|
||||
{
|
||||
var cid = Guid.NewGuid().ToString();
|
||||
var req = new WaitForAttributeRequest(
|
||||
cid, _instanceName, name, targetValueEncoded, predicate, timeout, DateTimeOffset.UtcNow);
|
||||
|
||||
var resp = await _instanceActor.Ask<WaitForAttributeResponse>(
|
||||
req, timeout + _askTimeout, _scriptTimeoutToken);
|
||||
|
||||
return resp.Matched;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Sets an attribute value. For data-connected attributes the Instance Actor
|
||||
/// forwards the write to the DCL, which writes the physical device; the
|
||||
|
||||
Reference in New Issue
Block a user