feat(mesh-phase4): driver-only node must be FetchAndCache (validator)
Claude-Session: https://claude.ai/code/session_01GASWkNEi68FSCtvr6rLoEW
This commit is contained in:
@@ -1,3 +1,4 @@
|
|||||||
|
using Microsoft.Extensions.Configuration;
|
||||||
using Microsoft.Extensions.Options;
|
using Microsoft.Extensions.Options;
|
||||||
|
|
||||||
namespace ZB.MOM.WW.OtOpcUa.Cluster;
|
namespace ZB.MOM.WW.OtOpcUa.Cluster;
|
||||||
@@ -14,6 +15,20 @@ namespace ZB.MOM.WW.OtOpcUa.Cluster;
|
|||||||
/// </remarks>
|
/// </remarks>
|
||||||
public sealed class ConfigSourceOptionsValidator : IValidateOptions<ConfigSourceOptions>
|
public sealed class ConfigSourceOptionsValidator : IValidateOptions<ConfigSourceOptions>
|
||||||
{
|
{
|
||||||
|
private readonly IConfiguration _configuration;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// DI-constructed by <c>AddValidatedOptions</c> (a plain <c>AddSingleton</c>), so a
|
||||||
|
/// constructor dependency is safe here. <see cref="IConfiguration"/> — not
|
||||||
|
/// <c>IClusterRoleInfo</c> — is the source of this node's roles: <c>IClusterRoleInfo</c>'s
|
||||||
|
/// implementation needs the <c>ActorSystem</c>, which does not exist yet at
|
||||||
|
/// <c>ValidateOnStart</c> time.
|
||||||
|
/// </summary>
|
||||||
|
public ConfigSourceOptionsValidator(IConfiguration configuration)
|
||||||
|
{
|
||||||
|
_configuration = configuration;
|
||||||
|
}
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public ValidateOptionsResult Validate(string? name, ConfigSourceOptions options)
|
public ValidateOptionsResult Validate(string? name, ConfigSourceOptions options)
|
||||||
{
|
{
|
||||||
@@ -76,6 +91,24 @@ public sealed class ConfigSourceOptionsValidator : IValidateOptions<ConfigSource
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Per-cluster mesh Phase 4: a driver-only node has no central ConfigDb connection to fall back
|
||||||
|
// on, so Direct is only ever valid on a fused admin+driver node (admin holds the SQL
|
||||||
|
// connection). Roles come from Cluster:Roles directly, not IClusterRoleInfo — see the ctor
|
||||||
|
// remark.
|
||||||
|
var roles = _configuration.GetSection("Cluster:Roles").Get<string[]>() ?? Array.Empty<string>();
|
||||||
|
var isDriver = Array.IndexOf(roles, "driver") >= 0;
|
||||||
|
var isAdmin = Array.IndexOf(roles, "admin") >= 0;
|
||||||
|
|
||||||
|
if (isDriver && !isAdmin && isDirect)
|
||||||
|
{
|
||||||
|
errors.Add(
|
||||||
|
"Cluster:Roles is a driver-only node (has 'driver', not 'admin') but "
|
||||||
|
+ $"{ConfigSourceOptions.SectionName}:{nameof(ConfigSourceOptions.Mode)} is 'Direct'. A "
|
||||||
|
+ "driver-only node has no central ConfigDb to read from — it must use 'FetchAndCache'. "
|
||||||
|
+ $"Set {ConfigSourceOptions.SectionName}:{nameof(ConfigSourceOptions.Mode)}="
|
||||||
|
+ $"{ConfigSourceOptions.ModeFetchAndCache} (see docs/Configuration.md → ConfigSource).");
|
||||||
|
}
|
||||||
|
|
||||||
return errors.Count == 0
|
return errors.Count == 0
|
||||||
? ValidateOptionsResult.Success
|
? ValidateOptionsResult.Success
|
||||||
: ValidateOptionsResult.Fail(string.Join(" ", errors));
|
: ValidateOptionsResult.Fail(string.Join(" ", errors));
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
using Microsoft.Extensions.Configuration;
|
||||||
using Microsoft.Extensions.Options;
|
using Microsoft.Extensions.Options;
|
||||||
using Shouldly;
|
using Shouldly;
|
||||||
using Xunit;
|
using Xunit;
|
||||||
@@ -11,8 +12,18 @@ namespace ZB.MOM.WW.OtOpcUa.Cluster.Tests;
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public class ConfigSourceOptionsValidatorTests
|
public class ConfigSourceOptionsValidatorTests
|
||||||
{
|
{
|
||||||
private static ValidateOptionsResult Validate(ConfigSourceOptions o) =>
|
private static ValidateOptionsResult Validate(ConfigSourceOptions o, params string[] roles)
|
||||||
new ConfigSourceOptionsValidator().Validate(ConfigSourceOptions.SectionName, o);
|
{
|
||||||
|
var pairs = new Dictionary<string, string?>();
|
||||||
|
for (var i = 0; i < roles.Length; i++)
|
||||||
|
{
|
||||||
|
pairs[$"Cluster:Roles:{i}"] = roles[i];
|
||||||
|
}
|
||||||
|
|
||||||
|
var configuration = new ConfigurationBuilder().AddInMemoryCollection(pairs).Build();
|
||||||
|
|
||||||
|
return new ConfigSourceOptionsValidator(configuration).Validate(ConfigSourceOptions.SectionName, o);
|
||||||
|
}
|
||||||
|
|
||||||
private static ConfigSourceOptions ValidFetch() => new()
|
private static ConfigSourceOptions ValidFetch() => new()
|
||||||
{
|
{
|
||||||
@@ -117,4 +128,41 @@ public class ConfigSourceOptionsValidatorTests
|
|||||||
FetchTimeoutSeconds = 0,
|
FetchTimeoutSeconds = 0,
|
||||||
}).Succeeded.ShouldBeTrue();
|
}).Succeeded.ShouldBeTrue();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void Driver_only_node_on_Direct_fails()
|
||||||
|
{
|
||||||
|
// A driver-only node has no central ConfigDb connection (Phase 4) — Direct silently produces a
|
||||||
|
// node that can never read its configuration.
|
||||||
|
var result = Validate(
|
||||||
|
new ConfigSourceOptions { Mode = ConfigSourceOptions.ModeDirect },
|
||||||
|
"driver");
|
||||||
|
|
||||||
|
result.Failed.ShouldBeTrue();
|
||||||
|
result.FailureMessage.ShouldContain(ConfigSourceOptions.ModeFetchAndCache);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void Fused_admin_and_driver_node_on_Direct_is_valid()
|
||||||
|
{
|
||||||
|
// A fused node holds the ConfigDb connection via its admin role, so Direct is still legitimate.
|
||||||
|
Validate(
|
||||||
|
new ConfigSourceOptions { Mode = ConfigSourceOptions.ModeDirect },
|
||||||
|
"admin", "driver").Succeeded.ShouldBeTrue();
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void Driver_only_node_on_FetchAndCache_is_valid()
|
||||||
|
{
|
||||||
|
Validate(ValidFetch(), "driver").Succeeded.ShouldBeTrue();
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void Admin_only_node_on_Direct_is_valid()
|
||||||
|
{
|
||||||
|
// An admin-only node has no driver role at all, so the driver-only rule does not apply to it.
|
||||||
|
Validate(
|
||||||
|
new ConfigSourceOptions { Mode = ConfigSourceOptions.ModeDirect },
|
||||||
|
"admin").Succeeded.ShouldBeTrue();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user