fix(inbound-api): resolve InboundAPI-014..017 — return-value validation, reflection-gateway hardening, deadline-bound routed calls, RouteHelper test coverage
This commit is contained in:
@@ -1,34 +1,58 @@
|
||||
using ScadaLink.Commons.Interfaces.Services;
|
||||
using ScadaLink.Commons.Messages.InboundApi;
|
||||
using ScadaLink.Commons.Types;
|
||||
using ScadaLink.Communication;
|
||||
|
||||
namespace ScadaLink.InboundAPI;
|
||||
|
||||
/// <summary>
|
||||
/// WP-4: Route.To() helper for cross-site calls from inbound API scripts.
|
||||
/// Resolves instance to site, routes via CommunicationService, blocks until response or timeout.
|
||||
/// Site unreachable returns error (no store-and-forward).
|
||||
/// Resolves instance to site, routes via <see cref="IInstanceRouter"/>, blocks until
|
||||
/// response or timeout. Site unreachable returns error (no store-and-forward).
|
||||
///
|
||||
/// InboundAPI-016: the helper carries the executing method's <see cref="CancellationToken"/>
|
||||
/// (the method-level timeout). Routed calls inherit that deadline by default, so a
|
||||
/// natural script — <c>Route.To("inst").Call("doWork", p)</c> — is timeout-bounded
|
||||
/// without the script having to thread a token explicitly.
|
||||
/// </summary>
|
||||
public class RouteHelper
|
||||
{
|
||||
private readonly IInstanceLocator _instanceLocator;
|
||||
private readonly CommunicationService _communicationService;
|
||||
private readonly IInstanceRouter _instanceRouter;
|
||||
private readonly CancellationToken _deadlineToken;
|
||||
|
||||
public RouteHelper(
|
||||
IInstanceLocator instanceLocator,
|
||||
CommunicationService communicationService)
|
||||
IInstanceRouter instanceRouter)
|
||||
: this(instanceLocator, instanceRouter, CancellationToken.None)
|
||||
{
|
||||
}
|
||||
|
||||
private RouteHelper(
|
||||
IInstanceLocator instanceLocator,
|
||||
IInstanceRouter instanceRouter,
|
||||
CancellationToken deadlineToken)
|
||||
{
|
||||
_instanceLocator = instanceLocator;
|
||||
_communicationService = communicationService;
|
||||
_instanceRouter = instanceRouter;
|
||||
_deadlineToken = deadlineToken;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// InboundAPI-016: returns a <see cref="RouteHelper"/> whose routed calls inherit
|
||||
/// <paramref name="deadlineToken"/> (the executing method's timeout) by default.
|
||||
/// <see cref="InboundScriptExecutor"/> calls this when it builds the script
|
||||
/// context so the method timeout actually covers routed calls, as the design doc
|
||||
/// requires.
|
||||
/// </summary>
|
||||
public RouteHelper WithDeadline(CancellationToken deadlineToken) =>
|
||||
new(_instanceLocator, _instanceRouter, deadlineToken);
|
||||
|
||||
/// <summary>
|
||||
/// Creates a route target for the specified instance.
|
||||
/// </summary>
|
||||
public RouteTarget To(string instanceCode)
|
||||
{
|
||||
return new RouteTarget(instanceCode, _instanceLocator, _communicationService);
|
||||
return new RouteTarget(instanceCode, _instanceLocator, _instanceRouter, _deadlineToken);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -39,36 +63,43 @@ public class RouteTarget
|
||||
{
|
||||
private readonly string _instanceCode;
|
||||
private readonly IInstanceLocator _instanceLocator;
|
||||
private readonly CommunicationService _communicationService;
|
||||
private readonly IInstanceRouter _instanceRouter;
|
||||
private readonly CancellationToken _deadlineToken;
|
||||
|
||||
internal RouteTarget(
|
||||
string instanceCode,
|
||||
IInstanceLocator instanceLocator,
|
||||
CommunicationService communicationService)
|
||||
IInstanceRouter instanceRouter,
|
||||
CancellationToken deadlineToken)
|
||||
{
|
||||
_instanceCode = instanceCode;
|
||||
_instanceLocator = instanceLocator;
|
||||
_communicationService = communicationService;
|
||||
_instanceRouter = instanceRouter;
|
||||
_deadlineToken = deadlineToken;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Calls a script on the remote instance. Synchronous from API caller's
|
||||
/// perspective. <paramref name="parameters"/> may be a dictionary or an
|
||||
/// anonymous object (<c>new { name = "Bob" }</c>) — see <see cref="ScriptArgs"/>.
|
||||
///
|
||||
/// InboundAPI-016: when <paramref name="cancellationToken"/> is not supplied the
|
||||
/// routed call inherits the executing method's timeout, so the call is bounded by
|
||||
/// the method-level deadline with no token argument.
|
||||
/// </summary>
|
||||
public async Task<object?> Call(
|
||||
string scriptName,
|
||||
object? parameters = null,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
var siteId = await ResolveSiteAsync(cancellationToken);
|
||||
var token = Effective(cancellationToken);
|
||||
var siteId = await ResolveSiteAsync(token);
|
||||
var correlationId = Guid.NewGuid().ToString();
|
||||
|
||||
var request = new RouteToCallRequest(
|
||||
correlationId, _instanceCode, scriptName, ScriptArgs.Normalize(parameters), DateTimeOffset.UtcNow);
|
||||
|
||||
var response = await _communicationService.RouteToCallAsync(
|
||||
siteId, request, cancellationToken);
|
||||
var response = await _instanceRouter.RouteToCallAsync(siteId, request, token);
|
||||
|
||||
if (!response.Success)
|
||||
{
|
||||
@@ -97,14 +128,14 @@ public class RouteTarget
|
||||
IEnumerable<string> attributeNames,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
var siteId = await ResolveSiteAsync(cancellationToken);
|
||||
var token = Effective(cancellationToken);
|
||||
var siteId = await ResolveSiteAsync(token);
|
||||
var correlationId = Guid.NewGuid().ToString();
|
||||
|
||||
var request = new RouteToGetAttributesRequest(
|
||||
correlationId, _instanceCode, attributeNames.ToList(), DateTimeOffset.UtcNow);
|
||||
|
||||
var response = await _communicationService.RouteToGetAttributesAsync(
|
||||
siteId, request, cancellationToken);
|
||||
var response = await _instanceRouter.RouteToGetAttributesAsync(siteId, request, token);
|
||||
|
||||
if (!response.Success)
|
||||
{
|
||||
@@ -135,14 +166,14 @@ public class RouteTarget
|
||||
IReadOnlyDictionary<string, string> attributeValues,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
var siteId = await ResolveSiteAsync(cancellationToken);
|
||||
var token = Effective(cancellationToken);
|
||||
var siteId = await ResolveSiteAsync(token);
|
||||
var correlationId = Guid.NewGuid().ToString();
|
||||
|
||||
var request = new RouteToSetAttributesRequest(
|
||||
correlationId, _instanceCode, attributeValues, DateTimeOffset.UtcNow);
|
||||
|
||||
var response = await _communicationService.RouteToSetAttributesAsync(
|
||||
siteId, request, cancellationToken);
|
||||
var response = await _instanceRouter.RouteToSetAttributesAsync(siteId, request, token);
|
||||
|
||||
if (!response.Success)
|
||||
{
|
||||
@@ -151,6 +182,13 @@ public class RouteTarget
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// InboundAPI-016: a routed call with no explicit token inherits the executing
|
||||
/// method's deadline. An explicitly supplied token (for a tighter bound) wins.
|
||||
/// </summary>
|
||||
private CancellationToken Effective(CancellationToken explicitToken) =>
|
||||
explicitToken.CanBeCanceled ? explicitToken : _deadlineToken;
|
||||
|
||||
private async Task<string> ResolveSiteAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
var siteId = await _instanceLocator.GetSiteIdForInstanceAsync(_instanceCode, cancellationToken);
|
||||
|
||||
Reference in New Issue
Block a user