fix(inbound): type SITE_UNREACHABLE on AskTimeoutException at the router await instead of message-substring sniffing (plan R2-06 T5)
This commit is contained in:
@@ -1,3 +1,4 @@
|
||||
using Akka.Actor;
|
||||
using ZB.MOM.WW.ScadaBridge.Commons.Interfaces.Services;
|
||||
using ZB.MOM.WW.ScadaBridge.Commons.Messages.InboundApi;
|
||||
using ZB.MOM.WW.ScadaBridge.Commons.Types;
|
||||
@@ -5,12 +6,16 @@ using ZB.MOM.WW.ScadaBridge.Commons.Types;
|
||||
namespace ZB.MOM.WW.ScadaBridge.InboundAPI;
|
||||
|
||||
/// <summary>
|
||||
/// C3: a routed call failed because the target site could not be reached (no contact /
|
||||
/// unreachable). Derives from <see cref="InvalidOperationException"/> so existing
|
||||
/// callers that catch the base type are unaffected, while
|
||||
/// <see cref="InboundScriptExecutor"/> catches this more specific type to surface the
|
||||
/// spec's <c>SITE_UNREACHABLE</c> error code instead of a generic <c>SCRIPT_ERROR</c>.
|
||||
/// A routing failure whose message does <em>not</em> indicate unreachability stays a
|
||||
/// C3/N3: a routed call failed because the target site never answered — the routed
|
||||
/// Ask <b>expired</b>. Unreachability is typed at the Ask boundary from
|
||||
/// <see cref="AskTimeoutException"/> (the communication layer's own signal: an
|
||||
/// unreachable/contactless site has its enveloped message dropped so the Ask times
|
||||
/// out caller-side) — it is <em>never</em> inferred from an error-message substring.
|
||||
/// Derives from <see cref="InvalidOperationException"/> so existing callers that
|
||||
/// catch the base type are unaffected, while <see cref="InboundScriptExecutor"/>
|
||||
/// catches this more specific type to surface the spec's <c>SITE_UNREACHABLE</c>
|
||||
/// error code instead of a generic <c>SCRIPT_ERROR</c>. A <c>Success=false</c>
|
||||
/// <em>response</em> means the site answered (reachable by definition) and stays a
|
||||
/// plain <see cref="InvalidOperationException"/>.
|
||||
/// </summary>
|
||||
public sealed class SiteUnreachableException : InvalidOperationException
|
||||
@@ -18,6 +23,11 @@ public sealed class SiteUnreachableException : InvalidOperationException
|
||||
/// <summary>Initializes a new <see cref="SiteUnreachableException"/> with the given message.</summary>
|
||||
/// <param name="message">The routing-level failure message that indicated the site was unreachable.</param>
|
||||
public SiteUnreachableException(string message) : base(message) { }
|
||||
|
||||
/// <summary>Initializes a new <see cref="SiteUnreachableException"/> wrapping the underlying Ask-timeout.</summary>
|
||||
/// <param name="message">The routing-level failure message.</param>
|
||||
/// <param name="innerException">The underlying <see cref="AskTimeoutException"/> from the routed Ask.</param>
|
||||
public SiteUnreachableException(string message, Exception innerException) : base(message, innerException) { }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -182,7 +192,7 @@ public class RouteTarget
|
||||
correlationId, _instanceCode, scriptName, ScriptArgs.Normalize(parameters),
|
||||
DateTimeOffset.UtcNow, _parentExecutionId);
|
||||
|
||||
var response = await _instanceRouter.RouteToCallAsync(siteId, request, token);
|
||||
var response = await RouteOrUnreachable(_instanceRouter.RouteToCallAsync(siteId, request, token));
|
||||
|
||||
if (!response.Success)
|
||||
{
|
||||
@@ -230,7 +240,7 @@ public class RouteTarget
|
||||
correlationId, _instanceCode, attributeNames.ToList(), DateTimeOffset.UtcNow,
|
||||
_parentExecutionId);
|
||||
|
||||
var response = await _instanceRouter.RouteToGetAttributesAsync(siteId, request, token);
|
||||
var response = await RouteOrUnreachable(_instanceRouter.RouteToGetAttributesAsync(siteId, request, token));
|
||||
|
||||
if (!response.Success)
|
||||
{
|
||||
@@ -294,7 +304,7 @@ public class RouteTarget
|
||||
AttributeValueCodec.Encode(targetValue), timeout, DateTimeOffset.UtcNow,
|
||||
_parentExecutionId);
|
||||
|
||||
var response = await _instanceRouter.RouteToWaitForAttributeAsync(siteId, request, token);
|
||||
var response = await RouteOrUnreachable(_instanceRouter.RouteToWaitForAttributeAsync(siteId, request, token));
|
||||
|
||||
if (!response.Success)
|
||||
{
|
||||
@@ -345,7 +355,7 @@ public class RouteTarget
|
||||
correlationId, _instanceCode, attributeValues, DateTimeOffset.UtcNow,
|
||||
_parentExecutionId);
|
||||
|
||||
var response = await _instanceRouter.RouteToSetAttributesAsync(siteId, request, token);
|
||||
var response = await RouteOrUnreachable(_instanceRouter.RouteToSetAttributesAsync(siteId, request, token));
|
||||
|
||||
if (!response.Success)
|
||||
{
|
||||
@@ -366,9 +376,9 @@ public class RouteTarget
|
||||
var siteId = await _instanceLocator.GetSiteIdForInstanceAsync(_instanceCode, cancellationToken);
|
||||
if (siteId == null)
|
||||
{
|
||||
// Route the site-resolution failure through the same classifier so a
|
||||
// message indicating the site is unreachable surfaces as SITE_UNREACHABLE;
|
||||
// a plain not-found / no-assigned-site stays a generic InvalidOperationException.
|
||||
// A null site id is a plain not-found / no-assigned-site — never
|
||||
// unreachability (nothing was routed), so it stays a generic
|
||||
// InvalidOperationException (SCRIPT_ERROR).
|
||||
throw RoutingFailure(
|
||||
$"Instance '{_instanceCode}' not found or has no assigned site");
|
||||
}
|
||||
@@ -377,19 +387,37 @@ public class RouteTarget
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// C3: classifies a routing-level failure message. When the message indicates the
|
||||
/// site could not be reached (unreachable / no contact) the failure is a
|
||||
/// <see cref="SiteUnreachableException"/> — so <see cref="InboundScriptExecutor"/>
|
||||
/// can surface the spec's <c>SITE_UNREACHABLE</c> code — otherwise a plain
|
||||
/// <see cref="InvalidOperationException"/>.
|
||||
/// N3: awaits a routed Ask and TYPES unreachability. AskTimeoutException means
|
||||
/// the site never answered (no ClusterClient contact, site down, partition) —
|
||||
/// the communication layer's own typed signal (CentralCommunicationActor drops
|
||||
/// envelopes for contactless sites so the Ask expires caller-side) — and
|
||||
/// surfaces as SiteUnreachableException so the executor maps it to the spec's
|
||||
/// SITE_UNREACHABLE code. A Success=false RESPONSE means the site answered and
|
||||
/// is therefore reachable; it flows through RoutingFailure as a plain
|
||||
/// InvalidOperationException (SCRIPT_ERROR).
|
||||
/// </summary>
|
||||
private static InvalidOperationException RoutingFailure(string message) =>
|
||||
IsUnreachable(message)
|
||||
? new SiteUnreachableException(message)
|
||||
: new InvalidOperationException(message);
|
||||
private static async Task<TResponse> RouteOrUnreachable<TResponse>(Task<TResponse> routedAsk)
|
||||
{
|
||||
try
|
||||
{
|
||||
return await routedAsk;
|
||||
}
|
||||
catch (AskTimeoutException ex)
|
||||
{
|
||||
throw new SiteUnreachableException(
|
||||
"Site did not respond to the routed request within the routing timeout — unreachable or not connected.",
|
||||
ex);
|
||||
}
|
||||
}
|
||||
|
||||
private static bool IsUnreachable(string message) =>
|
||||
message.Contains("unreachable", StringComparison.OrdinalIgnoreCase)
|
||||
|| message.Contains("no contact", StringComparison.OrdinalIgnoreCase)
|
||||
|| message.Contains("no-contact", StringComparison.OrdinalIgnoreCase);
|
||||
/// <summary>
|
||||
/// C3/N3: a routing-level failure REPORTED BY the site or the locator. The
|
||||
/// remote end answered (or the instance simply has no site), so this is never
|
||||
/// unreachability — always a plain <see cref="InvalidOperationException"/>
|
||||
/// (SCRIPT_ERROR at the endpoint). Unreachability is typed at the Ask boundary
|
||||
/// by <c>RouteOrUnreachable</c> (AskTimeoutException → SiteUnreachableException),
|
||||
/// replacing the old message-substring sniff that a harmless rewording in the
|
||||
/// communication layer could silently defeat.
|
||||
/// </summary>
|
||||
private static InvalidOperationException RoutingFailure(string message) => new(message);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user