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:
@@ -1,3 +1,4 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Text;
|
||||
using Opc.Ua;
|
||||
@@ -11,7 +12,8 @@ using ZB.MOM.WW.LmxOpcUa.Host.Metrics;
|
||||
namespace ZB.MOM.WW.LmxOpcUa.Host.OpcUa
|
||||
{
|
||||
/// <summary>
|
||||
/// Custom OPC UA server that creates the LmxNodeManager and handles user authentication. (OPC-001, OPC-012)
|
||||
/// Custom OPC UA server that creates the LmxNodeManager, handles user authentication,
|
||||
/// and exposes redundancy state through the standard server object. (OPC-001, OPC-012)
|
||||
/// </summary>
|
||||
public class LmxOpcUaServer : StandardServer
|
||||
{
|
||||
@@ -24,6 +26,9 @@ namespace ZB.MOM.WW.LmxOpcUa.Host.OpcUa
|
||||
private readonly bool _alarmTrackingEnabled;
|
||||
private readonly AuthenticationConfiguration _authConfig;
|
||||
private readonly IUserAuthenticationProvider? _authProvider;
|
||||
private readonly RedundancyConfiguration _redundancyConfig;
|
||||
private readonly string? _applicationUri;
|
||||
private readonly ServiceLevelCalculator _serviceLevelCalculator = new ServiceLevelCalculator();
|
||||
private LmxNodeManager? _nodeManager;
|
||||
|
||||
/// <summary>
|
||||
@@ -45,7 +50,8 @@ namespace ZB.MOM.WW.LmxOpcUa.Host.OpcUa
|
||||
|
||||
public LmxOpcUaServer(string galaxyName, IMxAccessClient mxAccessClient, PerformanceMetrics metrics,
|
||||
HistorianDataSource? historianDataSource = null, bool alarmTrackingEnabled = false,
|
||||
AuthenticationConfiguration? authConfig = null, IUserAuthenticationProvider? authProvider = null)
|
||||
AuthenticationConfiguration? authConfig = null, IUserAuthenticationProvider? authProvider = null,
|
||||
RedundancyConfiguration? redundancyConfig = null, string? applicationUri = null)
|
||||
{
|
||||
_galaxyName = galaxyName;
|
||||
_mxAccessClient = mxAccessClient;
|
||||
@@ -54,6 +60,8 @@ namespace ZB.MOM.WW.LmxOpcUa.Host.OpcUa
|
||||
_alarmTrackingEnabled = alarmTrackingEnabled;
|
||||
_authConfig = authConfig ?? new AuthenticationConfiguration();
|
||||
_authProvider = authProvider;
|
||||
_redundancyConfig = redundancyConfig ?? new RedundancyConfiguration();
|
||||
_applicationUri = applicationUri;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
@@ -72,6 +80,104 @@ namespace ZB.MOM.WW.LmxOpcUa.Host.OpcUa
|
||||
{
|
||||
base.OnServerStarted(server);
|
||||
server.SessionManager.ImpersonateUser += OnImpersonateUser;
|
||||
|
||||
ConfigureRedundancy(server);
|
||||
}
|
||||
|
||||
private void ConfigureRedundancy(IServerInternal server)
|
||||
{
|
||||
var mode = RedundancyModeResolver.Resolve(_redundancyConfig.Mode, _redundancyConfig.Enabled);
|
||||
|
||||
try
|
||||
{
|
||||
// Set RedundancySupport via the diagnostics node manager
|
||||
var redundancySupportNodeId = VariableIds.Server_ServerRedundancy_RedundancySupport;
|
||||
var redundancySupportNode = server.DiagnosticsNodeManager?.FindPredefinedNode(
|
||||
redundancySupportNodeId, typeof(BaseVariableState)) as BaseVariableState;
|
||||
|
||||
if (redundancySupportNode != null)
|
||||
{
|
||||
redundancySupportNode.Value = (int)mode;
|
||||
redundancySupportNode.ClearChangeMasks(server.DefaultSystemContext, false);
|
||||
Log.Information("Set RedundancySupport to {Mode}", mode);
|
||||
}
|
||||
|
||||
// Set ServerUriArray for non-transparent redundancy
|
||||
if (_redundancyConfig.Enabled && _redundancyConfig.ServerUris.Count > 0)
|
||||
{
|
||||
var serverUriArrayNodeId = VariableIds.Server_ServerRedundancy_ServerUriArray;
|
||||
var serverUriArrayNode = server.DiagnosticsNodeManager?.FindPredefinedNode(
|
||||
serverUriArrayNodeId, typeof(BaseVariableState)) as BaseVariableState;
|
||||
|
||||
if (serverUriArrayNode != null)
|
||||
{
|
||||
serverUriArrayNode.Value = _redundancyConfig.ServerUris.ToArray();
|
||||
serverUriArrayNode.ClearChangeMasks(server.DefaultSystemContext, false);
|
||||
Log.Information("Set ServerUriArray to [{Uris}]", string.Join(", ", _redundancyConfig.ServerUris));
|
||||
}
|
||||
else
|
||||
{
|
||||
Log.Warning("ServerUriArray node not found in address space — SDK may not expose it for RedundancySupport.None base type");
|
||||
}
|
||||
}
|
||||
|
||||
// Set initial ServiceLevel
|
||||
var initialLevel = CalculateCurrentServiceLevel(true, true);
|
||||
SetServiceLevelValue(server, initialLevel);
|
||||
Log.Information("Initial ServiceLevel set to {ServiceLevel}", initialLevel);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Log.Warning(ex, "Failed to configure redundancy nodes — redundancy state may not be visible to clients");
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Updates the server's ServiceLevel based on current runtime health.
|
||||
/// Called by the service layer when MXAccess or DB health changes.
|
||||
/// </summary>
|
||||
/// <param name="mxAccessConnected">Whether the MXAccess connection is healthy.</param>
|
||||
/// <param name="dbConnected">Whether the Galaxy repository database is reachable.</param>
|
||||
public void UpdateServiceLevel(bool mxAccessConnected, bool dbConnected)
|
||||
{
|
||||
var level = CalculateCurrentServiceLevel(mxAccessConnected, dbConnected);
|
||||
try
|
||||
{
|
||||
if (ServerInternal != null)
|
||||
{
|
||||
SetServiceLevelValue(ServerInternal, level);
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Log.Debug(ex, "Failed to update ServiceLevel node");
|
||||
}
|
||||
}
|
||||
|
||||
private byte CalculateCurrentServiceLevel(bool mxAccessConnected, bool dbConnected)
|
||||
{
|
||||
if (!_redundancyConfig.Enabled)
|
||||
return 255; // SDK default when redundancy is not configured
|
||||
|
||||
var isPrimary = string.Equals(_redundancyConfig.Role, "Primary", StringComparison.OrdinalIgnoreCase);
|
||||
var baseLevel = isPrimary
|
||||
? _redundancyConfig.ServiceLevelBase
|
||||
: Math.Max(0, _redundancyConfig.ServiceLevelBase - 50);
|
||||
|
||||
return _serviceLevelCalculator.Calculate(baseLevel, mxAccessConnected, dbConnected);
|
||||
}
|
||||
|
||||
private static void SetServiceLevelValue(IServerInternal server, byte level)
|
||||
{
|
||||
var serviceLevelNodeId = VariableIds.Server_ServiceLevel;
|
||||
var serviceLevelNode = server.DiagnosticsNodeManager?.FindPredefinedNode(
|
||||
serviceLevelNodeId, typeof(BaseVariableState)) as BaseVariableState;
|
||||
|
||||
if (serviceLevelNode != null)
|
||||
{
|
||||
serviceLevelNode.Value = level;
|
||||
serviceLevelNode.ClearChangeMasks(server.DefaultSystemContext, false);
|
||||
}
|
||||
}
|
||||
|
||||
private void OnImpersonateUser(Session session, ImpersonateEventArgs args)
|
||||
|
||||
Reference in New Issue
Block a user