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:
@@ -39,5 +39,10 @@ namespace ZB.MOM.WW.LmxOpcUa.Host.Configuration
|
||||
/// Gets or sets the transport security settings that control which OPC UA security profiles are exposed.
|
||||
/// </summary>
|
||||
public SecurityProfileConfiguration Security { get; set; } = new SecurityProfileConfiguration();
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the redundancy settings that control how this server participates in a redundant pair.
|
||||
/// </summary>
|
||||
public RedundancyConfiguration Redundancy { get; set; } = new RedundancyConfiguration();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -104,6 +104,47 @@ namespace ZB.MOM.WW.LmxOpcUa.Host.Configuration
|
||||
Log.Warning("Only the 'None' security profile is configured — transport security is disabled");
|
||||
}
|
||||
|
||||
// Redundancy
|
||||
if (config.OpcUa.ApplicationUri != null)
|
||||
Log.Information("OpcUa.ApplicationUri={ApplicationUri}", config.OpcUa.ApplicationUri);
|
||||
|
||||
Log.Information("Redundancy.Enabled={Enabled}, Mode={Mode}, Role={Role}, ServiceLevelBase={ServiceLevelBase}",
|
||||
config.Redundancy.Enabled, config.Redundancy.Mode, config.Redundancy.Role, config.Redundancy.ServiceLevelBase);
|
||||
|
||||
if (config.Redundancy.ServerUris.Count > 0)
|
||||
Log.Information("Redundancy.ServerUris=[{ServerUris}]", string.Join(", ", config.Redundancy.ServerUris));
|
||||
|
||||
if (config.Redundancy.Enabled)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(config.OpcUa.ApplicationUri))
|
||||
{
|
||||
Log.Error("OpcUa.ApplicationUri must be set when redundancy is enabled — each instance needs a unique identity");
|
||||
valid = false;
|
||||
}
|
||||
|
||||
if (config.Redundancy.ServerUris.Count < 2)
|
||||
{
|
||||
Log.Warning("Redundancy.ServerUris contains fewer than 2 entries — a redundant set typically has at least 2 servers");
|
||||
}
|
||||
|
||||
if (config.OpcUa.ApplicationUri != null && !config.Redundancy.ServerUris.Contains(config.OpcUa.ApplicationUri))
|
||||
{
|
||||
Log.Warning("Local OpcUa.ApplicationUri '{ApplicationUri}' is not listed in Redundancy.ServerUris", config.OpcUa.ApplicationUri);
|
||||
}
|
||||
|
||||
var mode = RedundancyModeResolver.Resolve(config.Redundancy.Mode, true);
|
||||
if (mode == Opc.Ua.RedundancySupport.None)
|
||||
{
|
||||
Log.Warning("Redundancy is enabled but Mode '{Mode}' is not recognized — will fall back to None", config.Redundancy.Mode);
|
||||
}
|
||||
}
|
||||
|
||||
if (config.Redundancy.ServiceLevelBase < 1 || config.Redundancy.ServiceLevelBase > 255)
|
||||
{
|
||||
Log.Error("Redundancy.ServiceLevelBase must be between 1 and 255");
|
||||
valid = false;
|
||||
}
|
||||
|
||||
Log.Information("=== Configuration {Status} ===", valid ? "Valid" : "INVALID");
|
||||
return valid;
|
||||
}
|
||||
|
||||
@@ -31,6 +31,13 @@ namespace ZB.MOM.WW.LmxOpcUa.Host.Configuration
|
||||
/// </summary>
|
||||
public string GalaxyName { get; set; } = "ZB";
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the explicit application URI for this server instance.
|
||||
/// When <see langword="null"/>, defaults to <c>urn:{GalaxyName}:LmxOpcUa</c>.
|
||||
/// Must be set to a unique value per instance when redundancy is enabled.
|
||||
/// </summary>
|
||||
public string? ApplicationUri { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the maximum number of simultaneous OPC UA sessions accepted by the host.
|
||||
/// </summary>
|
||||
|
||||
@@ -0,0 +1,41 @@
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace ZB.MOM.WW.LmxOpcUa.Host.Configuration
|
||||
{
|
||||
/// <summary>
|
||||
/// Non-transparent redundancy settings that control how the server advertises itself
|
||||
/// within a redundant pair and computes its dynamic ServiceLevel.
|
||||
/// </summary>
|
||||
public class RedundancyConfiguration
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets or sets whether redundancy is enabled. When <see langword="false"/> (default),
|
||||
/// the server reports <c>RedundancySupport.None</c> and <c>ServiceLevel = 255</c>.
|
||||
/// </summary>
|
||||
public bool Enabled { get; set; } = false;
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the redundancy mode. Valid values: <c>Warm</c>, <c>Hot</c>.
|
||||
/// </summary>
|
||||
public string Mode { get; set; } = "Warm";
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the role of this instance. Valid values: <c>Primary</c>, <c>Secondary</c>.
|
||||
/// The primary advertises a higher ServiceLevel than the secondary when both are healthy.
|
||||
/// </summary>
|
||||
public string Role { get; set; } = "Primary";
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the ApplicationUri values for all servers in the redundant set.
|
||||
/// Must include this instance's own <c>OpcUa.ApplicationUri</c>.
|
||||
/// </summary>
|
||||
public List<string> ServerUris { get; set; } = new List<string>();
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the base ServiceLevel when the server is fully healthy.
|
||||
/// The secondary automatically receives <c>ServiceLevelBase - 50</c>.
|
||||
/// Valid range: 1-255.
|
||||
/// </summary>
|
||||
public int ServiceLevelBase { get; set; } = 200;
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
|
||||
41
src/ZB.MOM.WW.LmxOpcUa.Host/OpcUa/RedundancyModeResolver.cs
Normal file
41
src/ZB.MOM.WW.LmxOpcUa.Host/OpcUa/RedundancyModeResolver.cs
Normal file
@@ -0,0 +1,41 @@
|
||||
using System;
|
||||
using Opc.Ua;
|
||||
using Serilog;
|
||||
|
||||
namespace ZB.MOM.WW.LmxOpcUa.Host.OpcUa
|
||||
{
|
||||
/// <summary>
|
||||
/// Maps a configured redundancy mode string to the OPC UA <see cref="RedundancySupport"/> enum.
|
||||
/// </summary>
|
||||
public static class RedundancyModeResolver
|
||||
{
|
||||
private static readonly ILogger Log = Serilog.Log.ForContext(typeof(RedundancyModeResolver));
|
||||
|
||||
/// <summary>
|
||||
/// Resolves the configured mode string to a <see cref="RedundancySupport"/> value.
|
||||
/// Returns <see cref="RedundancySupport.None"/> when redundancy is disabled or the mode is unrecognized.
|
||||
/// </summary>
|
||||
/// <param name="mode">The mode string from configuration (e.g., "Warm", "Hot").</param>
|
||||
/// <param name="enabled">Whether redundancy is enabled.</param>
|
||||
/// <returns>The resolved redundancy support mode.</returns>
|
||||
public static RedundancySupport Resolve(string mode, bool enabled)
|
||||
{
|
||||
if (!enabled)
|
||||
return RedundancySupport.None;
|
||||
|
||||
var resolved = (mode ?? "").Trim().ToLowerInvariant() switch
|
||||
{
|
||||
"warm" => RedundancySupport.Warm,
|
||||
"hot" => RedundancySupport.Hot,
|
||||
_ => RedundancySupport.None
|
||||
};
|
||||
|
||||
if (resolved == RedundancySupport.None)
|
||||
{
|
||||
Log.Warning("Unknown redundancy mode '{Mode}' — falling back to None. Supported modes: Warm, Hot", mode);
|
||||
}
|
||||
|
||||
return resolved;
|
||||
}
|
||||
}
|
||||
}
|
||||
33
src/ZB.MOM.WW.LmxOpcUa.Host/OpcUa/ServiceLevelCalculator.cs
Normal file
33
src/ZB.MOM.WW.LmxOpcUa.Host/OpcUa/ServiceLevelCalculator.cs
Normal file
@@ -0,0 +1,33 @@
|
||||
using System;
|
||||
|
||||
namespace ZB.MOM.WW.LmxOpcUa.Host.OpcUa
|
||||
{
|
||||
/// <summary>
|
||||
/// Computes the OPC UA ServiceLevel byte from a baseline and runtime health inputs.
|
||||
/// </summary>
|
||||
public sealed class ServiceLevelCalculator
|
||||
{
|
||||
/// <summary>
|
||||
/// Calculates the current ServiceLevel from a role-adjusted baseline and health state.
|
||||
/// </summary>
|
||||
/// <param name="baseLevel">The role-adjusted baseline (e.g., 200 for primary, 150 for secondary).</param>
|
||||
/// <param name="mxAccessConnected">Whether the MXAccess runtime connection is healthy.</param>
|
||||
/// <param name="dbConnected">Whether the Galaxy repository database is reachable.</param>
|
||||
/// <returns>A ServiceLevel byte between 0 and 255.</returns>
|
||||
public byte Calculate(int baseLevel, bool mxAccessConnected, bool dbConnected)
|
||||
{
|
||||
if (!mxAccessConnected && !dbConnected)
|
||||
return 0;
|
||||
|
||||
int level = baseLevel;
|
||||
|
||||
if (!mxAccessConnected)
|
||||
level -= 100;
|
||||
|
||||
if (!dbConnected)
|
||||
level -= 50;
|
||||
|
||||
return (byte)Math.Max(0, Math.Min(level, 255));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -59,6 +59,7 @@ namespace ZB.MOM.WW.LmxOpcUa.Host
|
||||
// Clear the default Profiles list before binding so JSON values replace rather than append
|
||||
_config.Security.Profiles.Clear();
|
||||
configuration.GetSection("Security").Bind(_config.Security);
|
||||
configuration.GetSection("Redundancy").Bind(_config.Redundancy);
|
||||
|
||||
_mxProxy = new MxProxyAdapter();
|
||||
_galaxyRepository = new GalaxyRepositoryService(_config.GalaxyRepository);
|
||||
@@ -164,7 +165,7 @@ namespace ZB.MOM.WW.LmxOpcUa.Host
|
||||
? new Domain.ConfigUserAuthenticationProvider(_config.Authentication.Users)
|
||||
: (Domain.IUserAuthenticationProvider?)null;
|
||||
_serverHost = new OpcUaServerHost(_config.OpcUa, effectiveMxClient, _metrics, historianDataSource,
|
||||
_config.Authentication, authProvider, _config.Security);
|
||||
_config.Authentication, authProvider, _config.Security, _config.Redundancy);
|
||||
|
||||
// Step 9-10: Query hierarchy, start server, build address space
|
||||
DateTime? initialDeployTime = null;
|
||||
@@ -222,6 +223,12 @@ namespace ZB.MOM.WW.LmxOpcUa.Host
|
||||
_statusWebServer.Start();
|
||||
}
|
||||
|
||||
// Wire ServiceLevel updates from MXAccess health changes
|
||||
if (_config.Redundancy.Enabled)
|
||||
{
|
||||
effectiveMxClient.ConnectionStateChanged += OnMxAccessStateChangedForServiceLevel;
|
||||
}
|
||||
|
||||
// Step 14
|
||||
Log.Information("LmxOpcUa service started successfully");
|
||||
}
|
||||
@@ -294,6 +301,14 @@ namespace ZB.MOM.WW.LmxOpcUa.Host
|
||||
}
|
||||
}
|
||||
|
||||
private void OnMxAccessStateChangedForServiceLevel(object? sender, Domain.ConnectionStateChangedEventArgs e)
|
||||
{
|
||||
var mxConnected = e.CurrentState == Domain.ConnectionState.Connected;
|
||||
var dbConnected = _galaxyStats?.DbConnected ?? false;
|
||||
_serverHost?.UpdateServiceLevel(mxConnected, dbConnected);
|
||||
Log.Debug("ServiceLevel updated: MxAccess={MxState}, DB={DbState}", e.CurrentState, dbConnected);
|
||||
}
|
||||
|
||||
private static void OnUnhandledException(object sender, UnhandledExceptionEventArgs e)
|
||||
{
|
||||
Log.Fatal(e.ExceptionObject as Exception, "Unhandled exception (IsTerminating={IsTerminating})", e.IsTerminating);
|
||||
|
||||
@@ -122,6 +122,28 @@ namespace ZB.MOM.WW.LmxOpcUa.Host
|
||||
return this;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Sets the redundancy configuration for the test host.
|
||||
/// </summary>
|
||||
/// <param name="redundancy">The redundancy configuration to inject.</param>
|
||||
/// <returns>The current builder so additional overrides can be chained.</returns>
|
||||
public OpcUaServiceBuilder WithRedundancy(RedundancyConfiguration redundancy)
|
||||
{
|
||||
_config.Redundancy = redundancy;
|
||||
return this;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Sets the application URI for the test host, distinct from the namespace URI.
|
||||
/// </summary>
|
||||
/// <param name="applicationUri">The unique application URI for this server instance.</param>
|
||||
/// <returns>The current builder so additional overrides can be chained.</returns>
|
||||
public OpcUaServiceBuilder WithApplicationUri(string applicationUri)
|
||||
{
|
||||
_config.OpcUa.ApplicationUri = applicationUri;
|
||||
return this;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Sets the security profile configuration for the test host.
|
||||
/// </summary>
|
||||
|
||||
@@ -7,7 +7,8 @@
|
||||
"GalaxyName": "ZB",
|
||||
"MaxSessions": 100,
|
||||
"SessionTimeoutMinutes": 30,
|
||||
"AlarmTrackingEnabled": false
|
||||
"AlarmTrackingEnabled": false,
|
||||
"ApplicationUri": null
|
||||
},
|
||||
"MxAccess": {
|
||||
"ClientName": "LmxOpcUa",
|
||||
@@ -45,6 +46,13 @@
|
||||
"PkiRootPath": null,
|
||||
"CertificateSubject": null
|
||||
},
|
||||
"Redundancy": {
|
||||
"Enabled": false,
|
||||
"Mode": "Warm",
|
||||
"Role": "Primary",
|
||||
"ServerUris": [],
|
||||
"ServiceLevelBase": 200
|
||||
},
|
||||
"Historian": {
|
||||
"Enabled": false,
|
||||
"ConnectionString": "Server=localhost;Database=Runtime;Integrated Security=true;",
|
||||
|
||||
Reference in New Issue
Block a user