feat(testrun): route WaitForAttribute in the Test-Run sandbox (deferred #14)
Closes deferred-work register item #14 with full fidelity. The Central UI Test-Run sandbox previously threw ScriptSandboxException for Attributes.WaitAsync/WaitForAsync; now the value-equality forms route to the bound deployed instance over the same cross-site path inbound Route.To().WaitForAttribute() uses. - Commons: additive trailing RouteToWaitForAttributeRequest.RequireGoodQuality (default false; message-evolution safe) so quality-gated waits route too. - SiteRuntime: DeploymentManagerActor's routed-wait handler now threads request.RequireGoodQuality into the InstanceActor WaitForAttributeRequest (previously hard-coded to default). - CentralUI sandbox: ISandboxInstanceGateway.WaitForAttributeAsync + SandboxInstanceGateway impl (via CommunicationService.RouteToWaitForAttributeAsync); SandboxAttributeAccessor value-equality WaitAsync/WaitForAsync route through it (codec-encoded target, scope-resolved name). Predicate overloads stay unsupported (an in-process lambda can't be routed) and throw a clearer, dedicated message. - Tests: sandbox accessor routing + predicate/unbound rejection (CentralUI); site-handler RequireGoodQuality threading (SiteRuntime). Register + the historical waitfor-deferred plan doc updated. Full slnx build 0/0; CentralUI 59, SiteRuntime routed-wait 4, InboundAPI wait 32 green. Claude-Session: https://claude.ai/code/session_01MtdgwpEeCUn6cUA5f1LMPj
This commit is contained in:
@@ -1,4 +1,5 @@
|
||||
using ZB.MOM.WW.ScadaBridge.Commons.Messages.InboundApi;
|
||||
using ZB.MOM.WW.ScadaBridge.Commons.Types;
|
||||
using ZB.MOM.WW.ScadaBridge.Communication;
|
||||
|
||||
namespace ZB.MOM.WW.ScadaBridge.CentralUI.ScriptAnalysis;
|
||||
@@ -74,4 +75,22 @@ public sealed class SandboxInstanceGateway : ISandboxInstanceGateway
|
||||
$"CallScript(\"{canonicalScriptName}\") on bound instance '{_instanceUniqueName}' failed: {response.ErrorMessage}");
|
||||
return response.ReturnValue;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task<WaitResult> WaitForAttributeAsync(
|
||||
string canonicalName, string? targetValueEncoded, TimeSpan timeout, bool requireGoodQuality, CancellationToken ct)
|
||||
{
|
||||
// Reuse the inbound-API cross-site wait route (RouteToWaitForAttributeAsync) — the
|
||||
// same real event-driven waiter Route.To().WaitForAttribute() uses. ParentExecutionId
|
||||
// is null for the sandbox path (no inbound execution to link).
|
||||
var request = new RouteToWaitForAttributeRequest(
|
||||
Guid.NewGuid().ToString(), _instanceUniqueName, canonicalName,
|
||||
targetValueEncoded, timeout, DateTimeOffset.UtcNow,
|
||||
ParentExecutionId: null, RequireGoodQuality: requireGoodQuality);
|
||||
var response = await _comms.RouteToWaitForAttributeAsync(_siteId, request, _runToken);
|
||||
if (!response.Success)
|
||||
throw new ScriptSandboxException(
|
||||
$"WaitForAttribute(\"{canonicalName}\") on bound instance '{_instanceUniqueName}' failed: {response.ErrorMessage}");
|
||||
return new WaitResult(response.Matched, response.Value, response.Quality, response.TimedOut);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -114,6 +114,21 @@ public interface ISandboxInstanceGateway
|
||||
/// <returns>The script result, or null if none.</returns>
|
||||
Task<object?> CallScriptAsync(
|
||||
string canonicalScriptName, IReadOnlyDictionary<string, object?>? parameters, CancellationToken ct);
|
||||
|
||||
/// <summary>
|
||||
/// Blocks until the named attribute reaches a target value (value-equality,
|
||||
/// codec-encoded), optionally quality-gated, or the timeout elapses — routed to the
|
||||
/// bound deployed instance cross-site. Value-equality only: a script-side predicate
|
||||
/// cannot be routed, so the sandbox predicate overloads stay unsupported.
|
||||
/// </summary>
|
||||
/// <param name="canonicalName">The (already scope-resolved) canonical attribute name.</param>
|
||||
/// <param name="targetValueEncoded">The <c>AttributeValueCodec</c>-encoded target value, or null for "any change".</param>
|
||||
/// <param name="timeout">Maximum time to wait.</param>
|
||||
/// <param name="requireGoodQuality">When true, only a Good-quality value satisfies the wait.</param>
|
||||
/// <param name="ct">Cancellation token.</param>
|
||||
/// <returns>The wait outcome (matched flag, matched value, quality, timed-out flag).</returns>
|
||||
Task<WaitResult> WaitForAttributeAsync(
|
||||
string canonicalName, string? targetValueEncoded, TimeSpan timeout, bool requireGoodQuality, CancellationToken ct);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -212,6 +227,26 @@ public class SandboxInstanceContext
|
||||
"bind one in Test Run to call sibling scripts.");
|
||||
return _gateway.CallScriptAsync(scriptName, ScriptArgs.Normalize(parameters), CancellationToken.None);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Routes a value-equality attribute wait to the bound deployed instance cross-site.
|
||||
/// Needs a bound instance (the site owns the event-driven waiter); throws when unbound.
|
||||
/// </summary>
|
||||
/// <param name="canonicalName">The (already scope-resolved) canonical attribute name.</param>
|
||||
/// <param name="targetValueEncoded">The codec-encoded target value, or null for "any change".</param>
|
||||
/// <param name="timeout">Maximum time to wait.</param>
|
||||
/// <param name="requireGoodQuality">When true, only a Good-quality value satisfies the wait.</param>
|
||||
/// <returns>The wait outcome.</returns>
|
||||
public Task<WaitResult> WaitForAttribute(
|
||||
string canonicalName, string? targetValueEncoded, TimeSpan timeout, bool requireGoodQuality)
|
||||
{
|
||||
if (_gateway == null)
|
||||
throw new ScriptSandboxException(
|
||||
$"WaitForAttribute(\"{canonicalName}\") needs a deployed instance — " +
|
||||
"bind one in Test Run to exercise the event-driven attribute waiter.");
|
||||
return _gateway.WaitForAttributeAsync(
|
||||
canonicalName, targetValueEncoded, timeout, requireGoodQuality, CancellationToken.None);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -342,20 +377,23 @@ public class SandboxAttributeAccessor
|
||||
=> throw NotInSandbox(nameof(WriteBatchAndWaitAsync));
|
||||
|
||||
/// <summary>
|
||||
/// Sandbox stand-in for <c>AttributeAccessor.WaitAsync</c> (value-equality form);
|
||||
/// see <see cref="WriteBatchAndWaitAsync"/>.
|
||||
/// Value-equality attribute wait — routed to the bound deployed instance cross-site
|
||||
/// (the same real event-driven waiter a site uses). Needs a bound instance; throws
|
||||
/// <see cref="ScriptSandboxException"/> when the Test Run has no instance bound.
|
||||
/// </summary>
|
||||
/// <param name="key">The attribute key to observe.</param>
|
||||
/// <param name="targetValue">The value to wait for.</param>
|
||||
/// <param name="targetValue">The value to wait for (codec-normalized), or null for "any change".</param>
|
||||
/// <param name="timeout">The maximum time to wait.</param>
|
||||
/// <param name="requireGoodQuality">Whether the attribute must also have good quality to satisfy the wait.</param>
|
||||
/// <returns>Never returns; always throws <see cref="ScriptSandboxException"/> in the sandbox.</returns>
|
||||
public Task<bool> WaitAsync(string key, object? targetValue, TimeSpan timeout, bool requireGoodQuality = false)
|
||||
=> throw NotInSandbox(nameof(WaitAsync));
|
||||
/// <returns>True when the attribute reached the target within the timeout; false on timeout.</returns>
|
||||
public async Task<bool> WaitAsync(string key, object? targetValue, TimeSpan timeout, bool requireGoodQuality = false)
|
||||
=> (await _ctx.WaitForAttribute(
|
||||
Resolve(key), AttributeValueCodec.Encode(targetValue), timeout, requireGoodQuality)).Matched;
|
||||
|
||||
/// <summary>
|
||||
/// Sandbox stand-in for <c>AttributeAccessor.WaitAsync</c> (predicate form);
|
||||
/// see <see cref="WriteBatchAndWaitAsync"/>.
|
||||
/// Predicate-form attribute wait. Unsupported in Test Run because a script-side
|
||||
/// predicate (an in-process lambda) cannot be evaluated on the remote site — only the
|
||||
/// value-equality overloads route. Throws a clearly-labelled <see cref="ScriptSandboxException"/>.
|
||||
/// </summary>
|
||||
/// <param name="key">The attribute key to observe.</param>
|
||||
/// <param name="predicate">The predicate the attribute value must satisfy.</param>
|
||||
@@ -363,23 +401,25 @@ public class SandboxAttributeAccessor
|
||||
/// <param name="requireGoodQuality">Whether the attribute must also have good quality to satisfy the wait.</param>
|
||||
/// <returns>Never returns; always throws <see cref="ScriptSandboxException"/> in the sandbox.</returns>
|
||||
public Task<bool> WaitAsync(string key, Func<object?, bool> predicate, TimeSpan timeout, bool requireGoodQuality = false)
|
||||
=> throw NotInSandbox(nameof(WaitAsync));
|
||||
=> throw PredicateWaitNotRoutable(nameof(WaitAsync));
|
||||
|
||||
/// <summary>
|
||||
/// Sandbox stand-in for <c>AttributeAccessor.WaitForAsync</c> (value-equality form);
|
||||
/// see <see cref="WriteBatchAndWaitAsync"/>.
|
||||
/// Value-equality attribute wait returning the full <see cref="WaitResult"/> — routed
|
||||
/// to the bound deployed instance cross-site. Needs a bound instance; throws
|
||||
/// <see cref="ScriptSandboxException"/> when the Test Run has no instance bound.
|
||||
/// </summary>
|
||||
/// <param name="key">The attribute key to observe.</param>
|
||||
/// <param name="targetValue">The value to wait for.</param>
|
||||
/// <param name="targetValue">The value to wait for (codec-normalized), or null for "any change".</param>
|
||||
/// <param name="timeout">The maximum time to wait.</param>
|
||||
/// <param name="requireGoodQuality">Whether the attribute must also have good quality to satisfy the wait.</param>
|
||||
/// <returns>Never returns; always throws <see cref="ScriptSandboxException"/> in the sandbox.</returns>
|
||||
/// <returns>The wait outcome (matched flag, matched value, quality, timed-out flag).</returns>
|
||||
public Task<WaitResult> WaitForAsync(string key, object? targetValue, TimeSpan timeout, bool requireGoodQuality = false)
|
||||
=> throw NotInSandbox(nameof(WaitForAsync));
|
||||
=> _ctx.WaitForAttribute(Resolve(key), AttributeValueCodec.Encode(targetValue), timeout, requireGoodQuality);
|
||||
|
||||
/// <summary>
|
||||
/// Sandbox stand-in for <c>AttributeAccessor.WaitForAsync</c> (predicate form);
|
||||
/// see <see cref="WriteBatchAndWaitAsync"/>.
|
||||
/// Predicate-form attribute wait returning the full <see cref="WaitResult"/>.
|
||||
/// Unsupported in Test Run for the same reason as the predicate <see cref="WaitAsync(string, Func{object?, bool}, TimeSpan, bool)"/>
|
||||
/// overload — an in-process predicate cannot be routed to the remote site.
|
||||
/// </summary>
|
||||
/// <param name="key">The attribute key to observe.</param>
|
||||
/// <param name="predicate">The predicate the attribute value must satisfy.</param>
|
||||
@@ -387,12 +427,18 @@ public class SandboxAttributeAccessor
|
||||
/// <param name="requireGoodQuality">Whether the attribute must also have good quality to satisfy the wait.</param>
|
||||
/// <returns>Never returns; always throws <see cref="ScriptSandboxException"/> in the sandbox.</returns>
|
||||
public Task<WaitResult> WaitForAsync(string key, Func<object?, bool> predicate, TimeSpan timeout, bool requireGoodQuality = false)
|
||||
=> throw NotInSandbox(nameof(WaitForAsync));
|
||||
=> throw PredicateWaitNotRoutable(nameof(WaitForAsync));
|
||||
|
||||
private static ScriptSandboxException NotInSandbox(string member) =>
|
||||
new($"{member}(...) drives live device tags and the site's event-driven " +
|
||||
"attribute waiter, which aren't available in the central Test Run sandbox — " +
|
||||
"deploy to a site to exercise batch-write/wait handshakes.");
|
||||
|
||||
private static ScriptSandboxException PredicateWaitNotRoutable(string member) =>
|
||||
new($"{member}(...) with a predicate can't run in Test Run — a script-side " +
|
||||
"predicate is an in-process lambda that cannot be evaluated on the remote " +
|
||||
"site. Use the value-equality overload (which routes to the bound instance), " +
|
||||
"or deploy to a site to exercise predicate waits.");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
||||
Reference in New Issue
Block a user