712c5117ee
Prerequisite for OtOpcUa Warm redundancy (lmxopcua#717): a Warm standby keeps a
healthy session but serves BadOutOfService at a standby ServiceLevel (90/50, boot 1),
so a connection that lands there sat on Bad quality forever.
- OPC UA connections with a backup poll Server_ServiceLevel (i=2267) on the connected
endpoint (IServiceLevelReadable, implemented by OpcUaDataConnection; never raises
Disconnected). A level < 200 sustained for the degraded window (default 10 s), or
the first sample after any (re)connect, triggers a probe of the other endpoint
(short-lived session: connect, read, dispose). Switch only when it reports a
Healthy (>= 200) higher level, and not within the minimum dwell (default 60 s) of
the previous ServiceLevel switch. Both degraded, other unreachable or level
unknown: stay put. 250/240 (today's OtOpcUa) never starts a streak.
- The switch reuses the failover path: SwitchToOtherEndpoint() now shared by all
three triggers, then BecomeReconnecting(ReconnectCause.ServiceLevelFailover) -
zero delay, no unstable accounting, ReSubscribeAll on the new endpoint.
- Config: OpcUaEndpointConfig.ServiceLevelFailover {enabled, degradedWindowSeconds,
minDwellSeconds}, read from the PRIMARY endpoint's JSON only; null = defaults
(enabled, 10, 60). Inside the encrypted protocol JSON, so no schema change or
migration. Central UI: fields in the Backup card next to Failover Retry Count.
Global: ScadaBridge:DataConnection:ServiceLevelPollInterval (2 s) and
ServiceLevelProbeTimeout (15 s).
- Tests: actor tests per trigger/non-trigger with positive controls, an unsolicited
promotion RefreshStart/RefreshEnd test at client and actor level, adapter tests,
and a real in-process OPC UA server pair end to end. All 15 mutants killed.
- Docs: Component-DataConnectionLayer, components/DataConnectionLayer, Component-CLI.
Claude-Session: https://claude.ai/code/session_01GT67ACmpMR7b9LGRPLijMi
75 lines
4.5 KiB
C#
75 lines
4.5 KiB
C#
using ZB.MOM.WW.Configuration;
|
|
|
|
namespace ZB.MOM.WW.ScadaBridge.DataConnectionLayer;
|
|
|
|
/// <summary>
|
|
/// Validates <see cref="DataConnectionOptions"/> at startup (ValidateOnStart) so a
|
|
/// malformed "ScadaBridge:DataConnection" appsettings section fails fast at boot with a
|
|
/// key-naming message instead of surfacing later at runtime. The intervals drive
|
|
/// the reconnect/tag-resolution timers and the seed-read retry loop; a
|
|
/// zero/negative duration (or a non-positive <c>SeedReadMaxAttempts</c>) would
|
|
/// otherwise produce an opaque failure far from the offending config key.
|
|
/// </summary>
|
|
public sealed class DataConnectionOptionsValidator : OptionsValidatorBase<DataConnectionOptions>
|
|
{
|
|
/// <inheritdoc />
|
|
protected override void Validate(ValidationBuilder builder, DataConnectionOptions options)
|
|
{
|
|
builder.RequireThat(options.ReconnectInterval > TimeSpan.Zero,
|
|
$"ScadaBridge:DataConnection:ReconnectInterval must be a positive duration (was {options.ReconnectInterval}).");
|
|
|
|
builder.RequireThat(options.TagResolutionRetryInterval > TimeSpan.Zero,
|
|
$"ScadaBridge:DataConnection:TagResolutionRetryInterval must be a positive duration (was {options.TagResolutionRetryInterval}).");
|
|
|
|
builder.RequireThat(options.WriteTimeout > TimeSpan.Zero,
|
|
$"ScadaBridge:DataConnection:WriteTimeout must be a positive duration (was {options.WriteTimeout}).");
|
|
|
|
builder.RequireThat(options.SeedReadTimeout > TimeSpan.Zero,
|
|
$"ScadaBridge:DataConnection:SeedReadTimeout must be a positive duration (was {options.SeedReadTimeout}).");
|
|
|
|
builder.RequireThat(options.StableConnectionThreshold > TimeSpan.Zero,
|
|
$"ScadaBridge:DataConnection:StableConnectionThreshold must be a positive duration (was {options.StableConnectionThreshold}).");
|
|
|
|
builder.RequireThat(options.SeedReadRetryDelay > TimeSpan.Zero,
|
|
$"ScadaBridge:DataConnection:SeedReadRetryDelay must be a positive duration (was {options.SeedReadRetryDelay}).");
|
|
|
|
builder.RequireThat(options.SeedReadMaxAttempts > 0,
|
|
$"ScadaBridge:DataConnection:SeedReadMaxAttempts must be positive (was {options.SeedReadMaxAttempts}).");
|
|
|
|
builder.RequireThat(options.TagResolutionRetryMaxInterval >= options.TagResolutionRetryInterval,
|
|
"ScadaBridge:DataConnection:TagResolutionRetryMaxInterval must be at least " +
|
|
$"TagResolutionRetryInterval (was {options.TagResolutionRetryMaxInterval} vs {options.TagResolutionRetryInterval}).");
|
|
|
|
builder.RequireThat(options.SubscribeBatchSize > 0,
|
|
$"ScadaBridge:DataConnection:SubscribeBatchSize must be positive (was {options.SubscribeBatchSize}).");
|
|
|
|
builder.RequireThat(options.SubscribeBatchDelay >= TimeSpan.Zero,
|
|
$"ScadaBridge:DataConnection:SubscribeBatchDelay must not be negative (was {options.SubscribeBatchDelay}).");
|
|
|
|
builder.RequireThat(options.SeedReadBatchSize > 0,
|
|
$"ScadaBridge:DataConnection:SeedReadBatchSize must be positive (was {options.SeedReadBatchSize}).");
|
|
|
|
builder.RequireThat(options.SeedReadMaxParallelism > 0,
|
|
$"ScadaBridge:DataConnection:SeedReadMaxParallelism must be positive (was {options.SeedReadMaxParallelism}).");
|
|
|
|
builder.RequireThat(options.SeedOverallTimeout > TimeSpan.Zero,
|
|
$"ScadaBridge:DataConnection:SeedOverallTimeout must be a positive duration (was {options.SeedOverallTimeout}).");
|
|
|
|
builder.RequireThat(options.QualityFlushInterval > TimeSpan.Zero,
|
|
$"ScadaBridge:DataConnection:QualityFlushInterval must be a positive duration (was {options.QualityFlushInterval}).");
|
|
|
|
builder.RequireThat(options.MxSupervisoryAdviseParallelism > 0,
|
|
$"ScadaBridge:DataConnection:MxSupervisoryAdviseParallelism must be positive (was {options.MxSupervisoryAdviseParallelism}).");
|
|
|
|
builder.RequireThat(options.MxAlarmStreamReopenDebounce >= TimeSpan.Zero
|
|
&& options.MxAlarmStreamReopenDebounce <= TimeSpan.FromSeconds(5),
|
|
$"ScadaBridge:DataConnection:MxAlarmStreamReopenDebounce must be between 0 and 5 seconds (was {options.MxAlarmStreamReopenDebounce}).");
|
|
|
|
builder.RequireThat(options.ServiceLevelPollInterval > TimeSpan.Zero,
|
|
$"ScadaBridge:DataConnection:ServiceLevelPollInterval must be a positive duration (was {options.ServiceLevelPollInterval}).");
|
|
|
|
builder.RequireThat(options.ServiceLevelProbeTimeout > TimeSpan.Zero,
|
|
$"ScadaBridge:DataConnection:ServiceLevelProbeTimeout must be a positive duration (was {options.ServiceLevelProbeTimeout}).");
|
|
}
|
|
}
|