Add configurable non-transparent OPC UA server redundancy

Separates ApplicationUri from namespace identity so each instance in a
redundant pair has a unique server URI while sharing the same Galaxy
namespace. Exposes RedundancySupport, ServerUriArray, and dynamic
ServiceLevel through the standard OPC UA server object. ServiceLevel
is computed from role (Primary/Secondary) and runtime health (MXAccess
and DB connectivity). Adds CLI redundancy command, second deployed
service instance, and 31 new tests including paired-server integration.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Joseph Doherty
2026-03-28 13:32:17 -04:00
parent a3c2d9b243
commit a55153d7d5
27 changed files with 1475 additions and 248 deletions

View File

@@ -25,6 +25,7 @@ namespace ZB.MOM.WW.LmxOpcUa.Host.OpcUa
private readonly AuthenticationConfiguration _authConfig;
private readonly IUserAuthenticationProvider? _authProvider;
private readonly SecurityProfileConfiguration _securityConfig;
private readonly RedundancyConfiguration _redundancyConfig;
private ApplicationInstance? _application;
private LmxOpcUaServer? _server;
@@ -43,6 +44,12 @@ namespace ZB.MOM.WW.LmxOpcUa.Host.OpcUa
/// </summary>
public bool IsRunning => _server != null;
/// <summary>
/// Updates the OPC UA ServiceLevel based on current runtime health.
/// </summary>
public void UpdateServiceLevel(bool mxAccessConnected, bool dbConnected) =>
_server?.UpdateServiceLevel(mxAccessConnected, dbConnected);
/// <summary>
/// Initializes a new host for the Galaxy-backed OPC UA server instance.
/// </summary>
@@ -54,7 +61,8 @@ namespace ZB.MOM.WW.LmxOpcUa.Host.OpcUa
HistorianDataSource? historianDataSource = null,
AuthenticationConfiguration? authConfig = null,
IUserAuthenticationProvider? authProvider = null,
SecurityProfileConfiguration? securityConfig = null)
SecurityProfileConfiguration? securityConfig = null,
RedundancyConfiguration? redundancyConfig = null)
{
_config = config;
_mxAccessClient = mxAccessClient;
@@ -63,6 +71,7 @@ namespace ZB.MOM.WW.LmxOpcUa.Host.OpcUa
_authConfig = authConfig ?? new AuthenticationConfiguration();
_authProvider = authProvider;
_securityConfig = securityConfig ?? new SecurityProfileConfiguration();
_redundancyConfig = redundancyConfig ?? new RedundancyConfiguration();
}
/// <summary>
@@ -71,6 +80,7 @@ namespace ZB.MOM.WW.LmxOpcUa.Host.OpcUa
public async Task StartAsync()
{
var namespaceUri = $"urn:{_config.GalaxyName}:LmxOpcUa";
var applicationUri = _config.ApplicationUri ?? namespaceUri;
// Resolve configured security profiles
var securityPolicies = SecurityProfileResolver.Resolve(_securityConfig.Profiles);
@@ -127,7 +137,7 @@ namespace ZB.MOM.WW.LmxOpcUa.Host.OpcUa
var appConfig = new ApplicationConfiguration
{
ApplicationName = _config.ServerName,
ApplicationUri = namespaceUri,
ApplicationUri = applicationUri,
ApplicationType = ApplicationType.Server,
ProductUri = namespaceUri,
ServerConfiguration = serverConfig,
@@ -174,11 +184,11 @@ namespace ZB.MOM.WW.LmxOpcUa.Host.OpcUa
}
_server = new LmxOpcUaServer(_config.GalaxyName, _mxAccessClient, _metrics, _historianDataSource,
_config.AlarmTrackingEnabled, _authConfig, _authProvider);
_config.AlarmTrackingEnabled, _authConfig, _authProvider, _redundancyConfig, applicationUri);
await _application.Start(_server);
Log.Information("OPC UA server started on opc.tcp://{BindAddress}:{Port}{EndpointPath} (namespace={Namespace})",
_config.BindAddress, _config.Port, _config.EndpointPath, namespaceUri);
Log.Information("OPC UA server started on opc.tcp://{BindAddress}:{Port}{EndpointPath} (applicationUri={ApplicationUri}, namespace={Namespace})",
_config.BindAddress, _config.Port, _config.EndpointPath, applicationUri, namespaceUri);
}
private void OnCertificateValidation(CertificateValidator sender, CertificateValidationEventArgs e)