refactor: rename ScadaLink → ZB.MOM.WW.ScadaBridge (code + projects + namespaces)
Solution + 23 src projects + 26 test projects renamed; folders, csproj, namespaces, and ScadaLinkDbContext/ScadaBridgeDbContext class updated. ActorSystem "scadalink" → "scadabridge", Akka seed-node URLs migrated. SQL roles/logins, LDAP domains, CLI command name, and CLI config dir (~/.scadalink → ~/.scadabridge) also renamed. Build green; 5 Host.Tests fail awaiting SQL login rename in next commit. Pre-existing StaleTagMonitor timing flakes unchanged. Rename script committed at tools/rename-to-scadabridge.sh.
This commit is contained in:
@@ -0,0 +1,267 @@
|
||||
using ZB.MOM.WW.ScadaBridge.Commons.Interfaces.Services;
|
||||
using ZB.MOM.WW.ScadaBridge.Commons.Messages.InboundApi;
|
||||
using ZB.MOM.WW.ScadaBridge.Commons.Types;
|
||||
|
||||
namespace ZB.MOM.WW.ScadaBridge.InboundAPI;
|
||||
|
||||
/// <summary>
|
||||
/// WP-4: Route.To() helper for cross-site calls from inbound API scripts.
|
||||
/// 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 IInstanceRouter _instanceRouter;
|
||||
private readonly CancellationToken _deadlineToken;
|
||||
private readonly Guid? _parentExecutionId;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new <see cref="RouteHelper"/> with no deadline token and no parent execution id.
|
||||
/// </summary>
|
||||
/// <param name="instanceLocator">Service to resolve the site id for a given instance code.</param>
|
||||
/// <param name="instanceRouter">Service to route cross-site calls to the resolved site.</param>
|
||||
public RouteHelper(
|
||||
IInstanceLocator instanceLocator,
|
||||
IInstanceRouter instanceRouter)
|
||||
: this(instanceLocator, instanceRouter, CancellationToken.None, parentExecutionId: null)
|
||||
{
|
||||
}
|
||||
|
||||
private RouteHelper(
|
||||
IInstanceLocator instanceLocator,
|
||||
IInstanceRouter instanceRouter,
|
||||
CancellationToken deadlineToken,
|
||||
Guid? parentExecutionId)
|
||||
{
|
||||
_instanceLocator = instanceLocator;
|
||||
_instanceRouter = instanceRouter;
|
||||
_deadlineToken = deadlineToken;
|
||||
_parentExecutionId = parentExecutionId;
|
||||
}
|
||||
|
||||
/// <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>
|
||||
/// <param name="deadlineToken">The executing method's timeout cancellation token to inherit for routed calls.</param>
|
||||
public RouteHelper WithDeadline(CancellationToken deadlineToken) =>
|
||||
new(_instanceLocator, _instanceRouter, deadlineToken, _parentExecutionId);
|
||||
|
||||
/// <summary>
|
||||
/// Audit Log #23 (ParentExecutionId): returns a <see cref="RouteHelper"/> whose
|
||||
/// routed <see cref="RouteTarget.Call"/> requests carry
|
||||
/// <paramref name="parentExecutionId"/> as <see cref="RouteToCallRequest.ParentExecutionId"/>.
|
||||
/// For an inbound API request this is the inbound request's own per-request
|
||||
/// execution id, so the routed site script records the inbound request as its
|
||||
/// parent. <see cref="InboundScriptExecutor"/> calls this when it builds the
|
||||
/// script context.
|
||||
/// </summary>
|
||||
/// <param name="parentExecutionId">The inbound request's execution id to stamp as the spawning parent on routed calls, or null for non-routed runs.</param>
|
||||
public RouteHelper WithParentExecutionId(Guid? parentExecutionId) =>
|
||||
new(_instanceLocator, _instanceRouter, _deadlineToken, parentExecutionId);
|
||||
|
||||
/// <summary>
|
||||
/// Creates a route target for the specified instance.
|
||||
/// </summary>
|
||||
/// <param name="instanceCode">The unique code of the instance to route calls to.</param>
|
||||
public RouteTarget To(string instanceCode)
|
||||
{
|
||||
return new RouteTarget(
|
||||
instanceCode, _instanceLocator, _instanceRouter, _deadlineToken, _parentExecutionId);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// WP-4: Represents a route target (an instance) for cross-site calls.
|
||||
/// </summary>
|
||||
public class RouteTarget
|
||||
{
|
||||
private readonly string _instanceCode;
|
||||
private readonly IInstanceLocator _instanceLocator;
|
||||
private readonly IInstanceRouter _instanceRouter;
|
||||
private readonly CancellationToken _deadlineToken;
|
||||
private readonly Guid? _parentExecutionId;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new <see cref="RouteTarget"/> for the specified instance.
|
||||
/// </summary>
|
||||
/// <param name="instanceCode">The unique code of the target instance.</param>
|
||||
/// <param name="instanceLocator">Service to resolve the site id for the instance.</param>
|
||||
/// <param name="instanceRouter">Service to route cross-site calls.</param>
|
||||
/// <param name="deadlineToken">Cancellation token representing the method-level deadline.</param>
|
||||
/// <param name="parentExecutionId">Optional parent execution id for audit correlation on routed calls.</param>
|
||||
internal RouteTarget(
|
||||
string instanceCode,
|
||||
IInstanceLocator instanceLocator,
|
||||
IInstanceRouter instanceRouter,
|
||||
CancellationToken deadlineToken,
|
||||
Guid? parentExecutionId)
|
||||
{
|
||||
_instanceCode = instanceCode;
|
||||
_instanceLocator = instanceLocator;
|
||||
_instanceRouter = instanceRouter;
|
||||
_deadlineToken = deadlineToken;
|
||||
_parentExecutionId = parentExecutionId;
|
||||
}
|
||||
|
||||
/// <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>
|
||||
/// <param name="scriptName">Name of the script to call on the remote instance.</param>
|
||||
/// <param name="parameters">Optional parameters passed to the script; may be a dictionary or anonymous object.</param>
|
||||
/// <param name="cancellationToken">Optional cancellation token; defaults to the method deadline when not supplied.</param>
|
||||
public async Task<object?> Call(
|
||||
string scriptName,
|
||||
object? parameters = null,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
var token = Effective(cancellationToken);
|
||||
var siteId = await ResolveSiteAsync(token);
|
||||
var correlationId = Guid.NewGuid().ToString();
|
||||
|
||||
// Audit Log #23 (ParentExecutionId): stamp the spawning execution's id
|
||||
// (the inbound API request's ExecutionId) so the routed site script
|
||||
// records this call's parent. CorrelationId above is a separate concern
|
||||
// — the per-operation lifecycle id, freshly minted per routed call.
|
||||
var request = new RouteToCallRequest(
|
||||
correlationId, _instanceCode, scriptName, ScriptArgs.Normalize(parameters),
|
||||
DateTimeOffset.UtcNow, _parentExecutionId);
|
||||
|
||||
var response = await _instanceRouter.RouteToCallAsync(siteId, request, token);
|
||||
|
||||
if (!response.Success)
|
||||
{
|
||||
throw new InvalidOperationException(
|
||||
response.ErrorMessage ?? "Remote script call failed");
|
||||
}
|
||||
|
||||
return response.ReturnValue;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets a single attribute value from the remote instance.
|
||||
/// </summary>
|
||||
/// <param name="attributeName">Name of the attribute to read.</param>
|
||||
/// <param name="cancellationToken">Optional cancellation token; defaults to the method deadline.</param>
|
||||
public async Task<object?> GetAttribute(
|
||||
string attributeName,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
var result = await GetAttributes(new[] { attributeName }, cancellationToken);
|
||||
return result.TryGetValue(attributeName, out var value) ? value : null;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets multiple attribute values from the remote instance (batch read).
|
||||
/// </summary>
|
||||
/// <param name="attributeNames">Names of the attributes to read.</param>
|
||||
/// <param name="cancellationToken">Optional cancellation token; defaults to the method deadline.</param>
|
||||
public async Task<IReadOnlyDictionary<string, object?>> GetAttributes(
|
||||
IEnumerable<string> attributeNames,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
var token = Effective(cancellationToken);
|
||||
var siteId = await ResolveSiteAsync(token);
|
||||
var correlationId = Guid.NewGuid().ToString();
|
||||
|
||||
// Audit Log #23 (ParentExecutionId): mirrors the Call path — stamp the
|
||||
// spawning inbound request's ExecutionId so future site-side audit
|
||||
// emission for routed reads can record this read's parent. Symmetric
|
||||
// with RouteToCallRequest so script authors get the same correlation
|
||||
// across Call / GetAttributes / SetAttributes.
|
||||
var request = new RouteToGetAttributesRequest(
|
||||
correlationId, _instanceCode, attributeNames.ToList(), DateTimeOffset.UtcNow,
|
||||
_parentExecutionId);
|
||||
|
||||
var response = await _instanceRouter.RouteToGetAttributesAsync(siteId, request, token);
|
||||
|
||||
if (!response.Success)
|
||||
{
|
||||
throw new InvalidOperationException(
|
||||
response.ErrorMessage ?? "Remote attribute read failed");
|
||||
}
|
||||
|
||||
return response.Values;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Sets a single attribute value on the remote instance.
|
||||
/// </summary>
|
||||
/// <param name="attributeName">Name of the attribute to write.</param>
|
||||
/// <param name="value">Value to set on the attribute.</param>
|
||||
/// <param name="cancellationToken">Optional cancellation token; defaults to the method deadline.</param>
|
||||
public async Task SetAttribute(
|
||||
string attributeName,
|
||||
string value,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
await SetAttributes(
|
||||
new Dictionary<string, string> { { attributeName, value } },
|
||||
cancellationToken);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Sets multiple attribute values on the remote instance (batch write).
|
||||
/// </summary>
|
||||
/// <param name="attributeValues">Map of attribute names to values to write.</param>
|
||||
/// <param name="cancellationToken">Optional cancellation token; defaults to the method deadline.</param>
|
||||
public async Task SetAttributes(
|
||||
IReadOnlyDictionary<string, string> attributeValues,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
var token = Effective(cancellationToken);
|
||||
var siteId = await ResolveSiteAsync(token);
|
||||
var correlationId = Guid.NewGuid().ToString();
|
||||
|
||||
// Audit Log #23 (ParentExecutionId): mirrors the Call path — stamp the
|
||||
// spawning inbound request's ExecutionId so future site-side audit
|
||||
// emission for routed writes can record this write's parent. Symmetric
|
||||
// with RouteToCallRequest so script authors get the same correlation
|
||||
// across Call / GetAttributes / SetAttributes.
|
||||
var request = new RouteToSetAttributesRequest(
|
||||
correlationId, _instanceCode, attributeValues, DateTimeOffset.UtcNow,
|
||||
_parentExecutionId);
|
||||
|
||||
var response = await _instanceRouter.RouteToSetAttributesAsync(siteId, request, token);
|
||||
|
||||
if (!response.Success)
|
||||
{
|
||||
throw new InvalidOperationException(
|
||||
response.ErrorMessage ?? "Remote attribute write failed");
|
||||
}
|
||||
}
|
||||
|
||||
/// <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);
|
||||
if (siteId == null)
|
||||
{
|
||||
throw new InvalidOperationException(
|
||||
$"Instance '{_instanceCode}' not found or has no assigned site");
|
||||
}
|
||||
|
||||
return siteId;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user