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:
@@ -0,0 +1,44 @@
|
||||
using Shouldly;
|
||||
using Xunit;
|
||||
using ZB.MOM.WW.LmxOpcUa.Host.Configuration;
|
||||
|
||||
namespace ZB.MOM.WW.LmxOpcUa.Tests.Redundancy
|
||||
{
|
||||
public class RedundancyConfigurationTests
|
||||
{
|
||||
[Fact]
|
||||
public void DefaultConfig_Disabled()
|
||||
{
|
||||
var config = new RedundancyConfiguration();
|
||||
config.Enabled.ShouldBe(false);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void DefaultConfig_ModeWarm()
|
||||
{
|
||||
var config = new RedundancyConfiguration();
|
||||
config.Mode.ShouldBe("Warm");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void DefaultConfig_RolePrimary()
|
||||
{
|
||||
var config = new RedundancyConfiguration();
|
||||
config.Role.ShouldBe("Primary");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void DefaultConfig_EmptyServerUris()
|
||||
{
|
||||
var config = new RedundancyConfiguration();
|
||||
config.ServerUris.ShouldBeEmpty();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void DefaultConfig_ServiceLevelBase200()
|
||||
{
|
||||
var config = new RedundancyConfiguration();
|
||||
config.ServiceLevelBase.ShouldBe(200);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,54 @@
|
||||
using Opc.Ua;
|
||||
using Shouldly;
|
||||
using Xunit;
|
||||
using ZB.MOM.WW.LmxOpcUa.Host.OpcUa;
|
||||
|
||||
namespace ZB.MOM.WW.LmxOpcUa.Tests.Redundancy
|
||||
{
|
||||
public class RedundancyModeResolverTests
|
||||
{
|
||||
[Fact]
|
||||
public void Resolve_Disabled_ReturnsNone()
|
||||
{
|
||||
RedundancyModeResolver.Resolve("Warm", enabled: false).ShouldBe(RedundancySupport.None);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Resolve_Warm_ReturnsWarm()
|
||||
{
|
||||
RedundancyModeResolver.Resolve("Warm", enabled: true).ShouldBe(RedundancySupport.Warm);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Resolve_Hot_ReturnsHot()
|
||||
{
|
||||
RedundancyModeResolver.Resolve("Hot", enabled: true).ShouldBe(RedundancySupport.Hot);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Resolve_Unknown_FallsBackToNone()
|
||||
{
|
||||
RedundancyModeResolver.Resolve("Transparent", enabled: true).ShouldBe(RedundancySupport.None);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Resolve_CaseInsensitive()
|
||||
{
|
||||
RedundancyModeResolver.Resolve("warm", enabled: true).ShouldBe(RedundancySupport.Warm);
|
||||
RedundancyModeResolver.Resolve("WARM", enabled: true).ShouldBe(RedundancySupport.Warm);
|
||||
RedundancyModeResolver.Resolve("hot", enabled: true).ShouldBe(RedundancySupport.Hot);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Resolve_Null_FallsBackToNone()
|
||||
{
|
||||
RedundancyModeResolver.Resolve(null!, enabled: true).ShouldBe(RedundancySupport.None);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Resolve_Empty_FallsBackToNone()
|
||||
{
|
||||
RedundancyModeResolver.Resolve("", enabled: true).ShouldBe(RedundancySupport.None);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,59 @@
|
||||
using Shouldly;
|
||||
using Xunit;
|
||||
using ZB.MOM.WW.LmxOpcUa.Host.OpcUa;
|
||||
|
||||
namespace ZB.MOM.WW.LmxOpcUa.Tests.Redundancy
|
||||
{
|
||||
public class ServiceLevelCalculatorTests
|
||||
{
|
||||
private readonly ServiceLevelCalculator _calculator = new ServiceLevelCalculator();
|
||||
|
||||
[Fact]
|
||||
public void FullyHealthy_Primary_ReturnsBase()
|
||||
{
|
||||
_calculator.Calculate(200, mxAccessConnected: true, dbConnected: true).ShouldBe((byte)200);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void FullyHealthy_Secondary_ReturnsBaseMinusFifty()
|
||||
{
|
||||
_calculator.Calculate(150, mxAccessConnected: true, dbConnected: true).ShouldBe((byte)150);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void MxAccessDown_ReducesServiceLevel()
|
||||
{
|
||||
_calculator.Calculate(200, mxAccessConnected: false, dbConnected: true).ShouldBe((byte)100);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void DbDown_ReducesServiceLevel()
|
||||
{
|
||||
_calculator.Calculate(200, mxAccessConnected: true, dbConnected: false).ShouldBe((byte)150);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void BothDown_ReturnsZero()
|
||||
{
|
||||
_calculator.Calculate(200, mxAccessConnected: false, dbConnected: false).ShouldBe((byte)0);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ClampedTo255()
|
||||
{
|
||||
_calculator.Calculate(255, mxAccessConnected: true, dbConnected: true).ShouldBe((byte)255);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ClampedToZero()
|
||||
{
|
||||
_calculator.Calculate(50, mxAccessConnected: false, dbConnected: true).ShouldBe((byte)0);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ZeroBase_BothHealthy_ReturnsZero()
|
||||
{
|
||||
_calculator.Calculate(0, mxAccessConnected: true, dbConnected: true).ShouldBe((byte)0);
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user