docs(xmldoc): fill missing XML docs + strip tracking-ID comments across src
v2-ci / build (push) Failing after 41s
v2-ci / unit-tests (tests/Core/ZB.MOM.WW.OtOpcUa.Cluster.Tests) (push) Has been skipped
v2-ci / unit-tests (tests/Server/ZB.MOM.WW.OtOpcUa.ControlPlane.Tests) (push) Has been skipped
v2-ci / unit-tests (tests/Server/ZB.MOM.WW.OtOpcUa.OpcUaServer.Tests) (push) Has been skipped
v2-ci / unit-tests (tests/Server/ZB.MOM.WW.OtOpcUa.Runtime.Tests) (push) Has been skipped
v2-ci / unit-tests (tests/Server/ZB.MOM.WW.OtOpcUa.Security.Tests) (push) Has been skipped
v2-ci / integration (tests/Server/ZB.MOM.WW.OtOpcUa.Host.IntegrationTests) (push) Has been skipped
v2-ci / integration (tests/Server/ZB.MOM.WW.OtOpcUa.OpcUaServer.IntegrationTests) (push) Has been skipped
v2-ci / build (push) Failing after 41s
v2-ci / unit-tests (tests/Core/ZB.MOM.WW.OtOpcUa.Cluster.Tests) (push) Has been skipped
v2-ci / unit-tests (tests/Server/ZB.MOM.WW.OtOpcUa.ControlPlane.Tests) (push) Has been skipped
v2-ci / unit-tests (tests/Server/ZB.MOM.WW.OtOpcUa.OpcUaServer.Tests) (push) Has been skipped
v2-ci / unit-tests (tests/Server/ZB.MOM.WW.OtOpcUa.Runtime.Tests) (push) Has been skipped
v2-ci / unit-tests (tests/Server/ZB.MOM.WW.OtOpcUa.Security.Tests) (push) Has been skipped
v2-ci / integration (tests/Server/ZB.MOM.WW.OtOpcUa.Host.IntegrationTests) (push) Has been skipped
v2-ci / integration (tests/Server/ZB.MOM.WW.OtOpcUa.OpcUaServer.IntegrationTests) (push) Has been skipped
Adds <summary>/<param>/<returns>/<inheritdoc> where missing and removes project bookkeeping IDs (task/tracking refs) from shipped code comments, so the docs read cleanly and CommentChecker is quiet except for known false positives (PLC/protocol terms, event/IEqualityComparer inheritdoc). Doc/comment-only; no logic changed; solution builds clean.
This commit is contained in:
@@ -20,7 +20,7 @@ public sealed class ClusterNode
|
||||
|
||||
/// <summary>
|
||||
/// OPC UA <c>ApplicationUri</c> — MUST be unique per node per OPC UA spec. Clients pin trust here.
|
||||
/// Fleet-wide unique index enforces no two nodes share a value (decision #86).
|
||||
/// Fleet-wide unique index enforces no two nodes share a value.
|
||||
/// Stored explicitly, NOT derived from <see cref="Host"/> at runtime — silent rewrite on
|
||||
/// hostname change would break all client trust.
|
||||
/// </summary>
|
||||
@@ -31,7 +31,7 @@ public sealed class ClusterNode
|
||||
|
||||
/// <summary>
|
||||
/// Per-node override JSON keyed by DriverInstanceId, merged onto cluster-level DriverConfig
|
||||
/// at apply time. Minimal by intent (decision #81). Nullable when no overrides exist.
|
||||
/// at apply time. Minimal by intent. Nullable when no overrides exist.
|
||||
/// </summary>
|
||||
public string? DriverConfigOverridesJson { get; set; }
|
||||
|
||||
|
||||
@@ -4,7 +4,7 @@ namespace ZB.MOM.WW.OtOpcUa.Configuration.Entities;
|
||||
|
||||
/// <summary>
|
||||
/// Authenticates a <see cref="ClusterNode"/> to the central config DB.
|
||||
/// Per decision #83 — credentials bind to NodeId, not ClusterId.
|
||||
/// Credentials bind to NodeId, not ClusterId.
|
||||
/// </summary>
|
||||
public sealed class ClusterNodeCredential
|
||||
{
|
||||
|
||||
@@ -11,8 +11,8 @@ namespace ZB.MOM.WW.OtOpcUa.Configuration.Entities;
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// <para>
|
||||
/// Closes the data-layer piece of LMX follow-up #7 (per-AppEngine Admin dashboard
|
||||
/// drill-down). The publisher hosted service on the Server side subscribes to every
|
||||
/// Supports the per-AppEngine Admin dashboard drill-down. The publisher hosted
|
||||
/// service on the Server side subscribes to every
|
||||
/// registered driver's <c>OnHostStatusChanged</c> and upserts rows on transitions +
|
||||
/// periodic liveness heartbeats. <see cref="LastSeenUtc"/> advances on every
|
||||
/// heartbeat so the Admin UI can flag stale rows from a crashed Server.
|
||||
|
||||
@@ -14,7 +14,7 @@ public sealed class DriverInstance
|
||||
|
||||
/// <summary>
|
||||
/// Logical FK to <see cref="Namespace.NamespaceId"/>. Same-cluster binding enforced by
|
||||
/// <c>sp_ValidateDraft</c> per decision #122: Namespace.ClusterId must equal DriverInstance.ClusterId.
|
||||
/// <c>sp_ValidateDraft</c>: Namespace.ClusterId must equal DriverInstance.ClusterId.
|
||||
/// </summary>
|
||||
public required string NamespaceId { get; set; }
|
||||
|
||||
@@ -27,12 +27,12 @@ public sealed class DriverInstance
|
||||
/// <summary>Gets or sets a value indicating whether this driver instance is enabled.</summary>
|
||||
public bool Enabled { get; set; } = true;
|
||||
|
||||
/// <summary>Schemaless per-driver-type JSON config. Validated against registered JSON schema at draft-publish time (decision #91).</summary>
|
||||
/// <summary>Schemaless per-driver-type JSON config. Validated against registered JSON schema at draft-publish time.</summary>
|
||||
public required string DriverConfig { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Optional per-instance overrides for the Phase 6.1 shared Polly resilience pipeline.
|
||||
/// Null = use the driver's tier defaults (decision #143). When populated, expected shape:
|
||||
/// Null = use the driver's tier defaults. When populated, expected shape:
|
||||
/// <code>
|
||||
/// {
|
||||
/// "bulkheadMaxConcurrent": 16,
|
||||
|
||||
@@ -2,8 +2,6 @@ namespace ZB.MOM.WW.OtOpcUa.Configuration.Entities;
|
||||
|
||||
/// <summary>
|
||||
/// UNS level-5 entity. Only for drivers in Equipment-kind namespaces.
|
||||
/// Per decisions #109 (first-class), #116 (5-identifier model), #125 (system-generated EquipmentId),
|
||||
/// #138–139 (OPC 40010 Identification fields as first-class columns).
|
||||
/// </summary>
|
||||
public sealed class Equipment
|
||||
{
|
||||
@@ -12,7 +10,7 @@ public sealed class Equipment
|
||||
|
||||
/// <summary>
|
||||
/// System-generated stable internal logical ID. Format: <c>'EQ-' + first 12 hex chars of EquipmentUuid</c>.
|
||||
/// NEVER operator-supplied, NEVER in CSV imports, NEVER editable in Admin UI (decision #125).
|
||||
/// NEVER operator-supplied, NEVER in CSV imports, NEVER editable in Admin UI.
|
||||
/// </summary>
|
||||
public required string EquipmentId { get; set; }
|
||||
|
||||
@@ -34,7 +32,7 @@ public sealed class Equipment
|
||||
/// <summary>UNS level 5 segment, matches <c>^[a-z0-9-]{1,32}$</c>.</summary>
|
||||
public required string Name { get; set; }
|
||||
|
||||
// Operator-facing / external-system identifiers (decision #116)
|
||||
// Operator-facing / external-system identifiers
|
||||
|
||||
/// <summary>Operator colloquial id (e.g. "machine_001"). Unique within cluster. Required.</summary>
|
||||
public required string MachineCode { get; set; }
|
||||
@@ -45,7 +43,7 @@ public sealed class Equipment
|
||||
/// <summary>SAP PM equipment id. Unique fleet-wide via <see cref="ExternalIdReservation"/>.</summary>
|
||||
public string? SAPID { get; set; }
|
||||
|
||||
// OPC UA Companion Spec OPC 40010 Machinery Identification fields (decision #139).
|
||||
// OPC UA Companion Spec OPC 40010 Machinery Identification fields.
|
||||
// All nullable so equipment can be added before identity is fully captured.
|
||||
/// <summary>Gets or sets the manufacturer name for this equipment.</summary>
|
||||
public string? Manufacturer { get; set; }
|
||||
@@ -66,7 +64,7 @@ public sealed class Equipment
|
||||
/// <summary>Gets or sets the device manual URI for this equipment.</summary>
|
||||
public string? DeviceManualUri { get; set; }
|
||||
|
||||
/// <summary>Nullable hook for future schemas-repo template ID (decision #112).</summary>
|
||||
/// <summary>Nullable hook for future schemas-repo template ID.</summary>
|
||||
public string? EquipmentClassRef { get; set; }
|
||||
|
||||
/// <summary>Gets or sets whether this equipment is enabled.</summary>
|
||||
|
||||
@@ -46,8 +46,8 @@ public sealed class EquipmentImportBatch
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// One staged row under an <see cref="EquipmentImportBatch"/>. Mirrors the decision #117
|
||||
/// + decision #139 columns from the CSV importer's output + an
|
||||
/// One staged row under an <see cref="EquipmentImportBatch"/>. Mirrors the required
|
||||
/// + optional columns from the CSV importer's output + an
|
||||
/// <see cref="IsAccepted"/> flag + a <see cref="RejectReason"/> string the preview modal
|
||||
/// renders.
|
||||
/// </summary>
|
||||
@@ -68,7 +68,6 @@ public sealed class EquipmentImportRow
|
||||
/// <summary>Gets or sets the reason this row was rejected, if applicable.</summary>
|
||||
public string? RejectReason { get; set; }
|
||||
|
||||
// Required (decision #117)
|
||||
/// <summary>Gets or sets the Z tag identifier.</summary>
|
||||
public required string ZTag { get; set; }
|
||||
|
||||
@@ -93,7 +92,6 @@ public sealed class EquipmentImportRow
|
||||
/// <summary>Gets or sets the UNS line name.</summary>
|
||||
public required string UnsLineName { get; set; }
|
||||
|
||||
// Optional (decision #139 — OPC 40010 Identification)
|
||||
/// <summary>Gets or sets the manufacturer name.</summary>
|
||||
public string? Manufacturer { get; set; }
|
||||
|
||||
|
||||
@@ -3,7 +3,7 @@ using ZB.MOM.WW.OtOpcUa.Configuration.Enums;
|
||||
namespace ZB.MOM.WW.OtOpcUa.Configuration.Entities;
|
||||
|
||||
/// <summary>
|
||||
/// Fleet-wide rollback-safe reservation of ZTag and SAPID. Per decision #124 — NOT generation-versioned.
|
||||
/// Fleet-wide rollback-safe reservation of ZTag and SAPID. NOT generation-versioned.
|
||||
/// Exists outside generation flow specifically because old generations and disabled equipment can
|
||||
/// still hold the same external IDs; per-generation uniqueness indexes fail under rollback/re-enable.
|
||||
/// </summary>
|
||||
|
||||
@@ -8,7 +8,7 @@ namespace ZB.MOM.WW.OtOpcUa.Configuration.Entities;
|
||||
/// applies fleet-wide.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// <para>Per <c>docs/v2/plan.md</c> decisions #105 and #150 — this entity is <b>control-plane
|
||||
/// <para>This entity is <b>control-plane
|
||||
/// only</b>. The OPC UA data-path evaluator does not read these rows; it reads
|
||||
/// <see cref="NodeAcl"/> joined directly against the session's resolved LDAP group
|
||||
/// memberships. Collapsing the two would let a user inherit tag permissions via an
|
||||
|
||||
@@ -3,7 +3,7 @@ using ZB.MOM.WW.OtOpcUa.Configuration.Enums;
|
||||
namespace ZB.MOM.WW.OtOpcUa.Configuration.Entities;
|
||||
|
||||
/// <summary>
|
||||
/// OPC UA namespace served by a cluster. Generation-versioned per decision #123 —
|
||||
/// OPC UA namespace served by a cluster. Generation-versioned —
|
||||
/// namespaces are content (affect what consumers see at the endpoint), not topology.
|
||||
/// </summary>
|
||||
public sealed class Namespace
|
||||
|
||||
@@ -4,7 +4,7 @@ namespace ZB.MOM.WW.OtOpcUa.Configuration.Entities;
|
||||
|
||||
/// <summary>
|
||||
/// One ACL grant: an LDAP group gets a set of <see cref="NodePermissions"/> at a specific scope.
|
||||
/// Generation-versioned per decision #130. See <c>acl-design.md</c> for evaluation algorithm.
|
||||
/// Generation-versioned per decision. See <c>acl-design.md</c> for evaluation algorithm.
|
||||
/// </summary>
|
||||
public sealed class NodeAcl
|
||||
{
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
namespace ZB.MOM.WW.OtOpcUa.Configuration.Entities;
|
||||
|
||||
/// <summary>
|
||||
/// Per Phase 7 plan decision #8 — user-authored C# script source, referenced by
|
||||
/// User-authored C# script source, referenced by
|
||||
/// <see cref="VirtualTag"/> and <see cref="ScriptedAlarm"/>. One row per script,
|
||||
/// per generation. <c>SourceHash</c> is the compile-cache key.
|
||||
/// </summary>
|
||||
|
||||
@@ -1,15 +1,15 @@
|
||||
namespace ZB.MOM.WW.OtOpcUa.Configuration.Entities;
|
||||
|
||||
/// <summary>
|
||||
/// Per Phase 7 plan decisions #5, #13, #15 — a scripted OPC UA Part 9 alarm whose
|
||||
/// A scripted OPC UA Part 9 alarm whose
|
||||
/// condition is the predicate <see cref="Script"/> referenced by
|
||||
/// <see cref="PredicateScriptId"/>. Materialized by <c>Core.ScriptedAlarms</c> as a
|
||||
/// concrete <c>AlarmConditionType</c> subtype per <see cref="AlarmType"/>.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// <para>
|
||||
/// Message tokens (<c>{TagPath}</c>) resolved at emission time per plan decision #13.
|
||||
/// <see cref="HistorizeToAveva"/> (plan decision #15) gates whether transitions
|
||||
/// Message tokens (<c>{TagPath}</c>) resolved at emission time.
|
||||
/// <see cref="HistorizeToAveva"/> gates whether transitions
|
||||
/// route through the Core.AlarmHistorian SQLite queue + Galaxy.Host to the Aveva
|
||||
/// Historian alarm schema.
|
||||
/// </para>
|
||||
@@ -41,7 +41,7 @@ public sealed class ScriptedAlarm
|
||||
public required string PredicateScriptId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Plan decision #15 — when true, transitions route through the SQLite store-and-forward
|
||||
/// When true, transitions route through the SQLite store-and-forward
|
||||
/// queue to the Aveva Historian. Defaults on for scripted alarms because they are the
|
||||
/// primary motivation for the historian sink; operator can disable per alarm.
|
||||
/// </summary>
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
namespace ZB.MOM.WW.OtOpcUa.Configuration.Entities;
|
||||
|
||||
/// <summary>
|
||||
/// Per Phase 7 plan decision #14 — persistent runtime state for each scripted alarm.
|
||||
/// Survives process restart so operators don't re-ack and ack history survives for
|
||||
/// Persistent runtime state for each scripted alarm. Survives process restart so
|
||||
/// operators don't re-ack and ack history survives for
|
||||
/// GxP / 21 CFR Part 11 compliance. Keyed on <c>ScriptedAlarmId</c> logically (not
|
||||
/// per-generation) because ack state follows the alarm's stable identity across
|
||||
/// generations — a Modified alarm keeps its ack history.
|
||||
@@ -24,7 +24,7 @@ public sealed class ScriptedAlarmState
|
||||
/// <summary>Logical FK — matches <see cref="ScriptedAlarm.ScriptedAlarmId"/>. One row per alarm identity.</summary>
|
||||
public required string ScriptedAlarmId { get; set; }
|
||||
|
||||
/// <summary>Enabled/Disabled. Persists across restart per plan decision #14.</summary>
|
||||
/// <summary>Enabled/Disabled. Persists across restart.</summary>
|
||||
public required string EnabledState { get; set; } = "Enabled";
|
||||
|
||||
/// <summary>Unacknowledged / Acknowledged.</summary>
|
||||
|
||||
@@ -14,7 +14,7 @@ public sealed class ServerCluster
|
||||
/// <summary>Gets or sets the display name for the server cluster.</summary>
|
||||
public required string Name { get; set; }
|
||||
|
||||
/// <summary>UNS level 1. Canonical org value: "zb" per decision #140.</summary>
|
||||
/// <summary>UNS level 1. Canonical org value: "zb".</summary>
|
||||
public required string Enterprise { get; set; }
|
||||
|
||||
/// <summary>UNS level 2, e.g. "warsaw-west".</summary>
|
||||
|
||||
@@ -51,7 +51,7 @@ public sealed class Tag
|
||||
/// </summary>
|
||||
public required TagAccessLevel AccessLevel { get; set; }
|
||||
|
||||
/// <summary>Per decisions #44–45 — opt-in for write retry eligibility.</summary>
|
||||
/// <summary>Opt-in for write retry eligibility.</summary>
|
||||
public bool WriteIdempotent { get; set; }
|
||||
|
||||
/// <summary>
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
namespace ZB.MOM.WW.OtOpcUa.Configuration.Entities;
|
||||
|
||||
/// <summary>UNS level-3 segment. Generation-versioned per decision #115.</summary>
|
||||
/// <summary>UNS level-3 segment. Generation-versioned.</summary>
|
||||
public sealed class UnsArea
|
||||
{
|
||||
/// <summary>Gets or sets the unique row identifier.</summary>
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
namespace ZB.MOM.WW.OtOpcUa.Configuration.Entities;
|
||||
|
||||
/// <summary>UNS level-4 segment. Generation-versioned per decision #115.</summary>
|
||||
/// <summary>UNS level-4 segment. Generation-versioned.</summary>
|
||||
public sealed class UnsLine
|
||||
{
|
||||
/// <summary>Gets or sets the unique row identifier for this UNS line.</summary>
|
||||
|
||||
@@ -1,20 +1,19 @@
|
||||
namespace ZB.MOM.WW.OtOpcUa.Configuration.Entities;
|
||||
|
||||
/// <summary>
|
||||
/// Per Phase 7 plan decision #2 — a virtual (calculated) tag that lives in the
|
||||
/// Equipment tree alongside driver tags. Value is produced by the
|
||||
/// <see cref="Script"/> referenced by <see cref="ScriptId"/>.
|
||||
/// A virtual (calculated) tag that lives in the Equipment tree alongside driver tags.
|
||||
/// Value is produced by the <see cref="Script"/> referenced by <see cref="ScriptId"/>.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// <para>
|
||||
/// <see cref="EquipmentId"/> is mandatory — virtual tags are always scoped to an
|
||||
/// Equipment node per plan decision #2 (unified Equipment tree, not a separate
|
||||
/// Equipment node (unified Equipment tree, not a separate
|
||||
/// /Virtual namespace). <see cref="DataType"/> matches the shape used by
|
||||
/// <c>Tag.DataType</c>.
|
||||
/// </para>
|
||||
/// <para>
|
||||
/// <see cref="ChangeTriggered"/> and <see cref="TimerIntervalMs"/> together realize
|
||||
/// plan decision #3 (change + timer). At least one must produce evaluations; the
|
||||
/// change + timer evaluation. At least one must produce evaluations; the
|
||||
/// Core.VirtualTags engine rejects an all-disabled tag at load time.
|
||||
/// </para>
|
||||
/// </remarks>
|
||||
|
||||
@@ -8,14 +8,14 @@ namespace ZB.MOM.WW.OtOpcUa.Configuration.Enums;
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// <para>
|
||||
/// Per <c>docs/v2/plan.md</c> decision #150 the two concerns share zero runtime code path:
|
||||
/// Per <c>docs/v2/plan.md</c> the two concerns share zero runtime code path:
|
||||
/// the control plane (Admin UI) consumes <see cref="Entities.LdapGroupRoleMapping"/>; the
|
||||
/// data plane consumes <see cref="Entities.NodeAcl"/> rows directly. Having them in one
|
||||
/// table would collapse the distinction + let a user inherit tag permissions via their
|
||||
/// admin-role claim path.
|
||||
/// </para>
|
||||
/// <para>
|
||||
/// Task 1.7 standardized the member names on the canonical control-plane role vocabulary
|
||||
/// The member names were standardized on the canonical control-plane role vocabulary
|
||||
/// (<c>ZB.MOM.WW.Auth</c> <c>CanonicalRole</c>): <c>ConfigViewer → Viewer</c>,
|
||||
/// <c>ConfigEditor → Designer</c>, <c>FleetAdmin → Administrator</c>. The appsettings-only
|
||||
/// <c>DriverOperator</c> string role likewise became <c>Operator</c>. These members persist
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
namespace ZB.MOM.WW.OtOpcUa.Configuration.Enums;
|
||||
|
||||
/// <summary>Credential kind for <see cref="Entities.ClusterNodeCredential"/>. Per decision #83.</summary>
|
||||
/// <summary>Credential kind for <see cref="Entities.ClusterNodeCredential"/>.</summary>
|
||||
public enum CredentialKind
|
||||
{
|
||||
SqlLogin,
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
namespace ZB.MOM.WW.OtOpcUa.Configuration.Enums;
|
||||
|
||||
/// <summary>OPC UA namespace kind per decision #107. One of each kind per cluster per generation.</summary>
|
||||
/// <summary>OPC UA namespace kind. One of each kind per cluster per generation.</summary>
|
||||
public enum NamespaceKind
|
||||
{
|
||||
/// <summary>
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
namespace ZB.MOM.WW.OtOpcUa.Configuration.Enums;
|
||||
|
||||
/// <summary>External-ID reservation kind. Per decision #124.</summary>
|
||||
/// <summary>External-ID reservation kind.</summary>
|
||||
public enum ReservationKind
|
||||
{
|
||||
ZTag,
|
||||
|
||||
@@ -3,7 +3,7 @@ using LiteDB;
|
||||
namespace ZB.MOM.WW.OtOpcUa.Configuration.LocalCache;
|
||||
|
||||
/// <summary>
|
||||
/// Generation-sealed LiteDB cache per <c>docs/v2/plan.md</c> decision #148 and Phase 6.1
|
||||
/// Generation-sealed LiteDB cache per <c>docs/v2/plan.md</c> and Phase 6.1
|
||||
/// Stream D.1. Each published generation writes one <b>read-only</b> LiteDB file under
|
||||
/// <c><cache-root>/<clusterId>/<generationId>.db</c>. A per-cluster
|
||||
/// <c>CURRENT</c> text file holds the currently-active generation id; it is updated
|
||||
@@ -32,7 +32,7 @@ public sealed class GenerationSealedCache
|
||||
// BsonMapper.Global is a process-wide singleton whose lazy per-type member registration is
|
||||
// not thread-safe across concurrently-constructed LiteDatabase instances; a seal racing a
|
||||
// read (or this cache racing LiteDbConfigCache) corrupts the global mapper, surfacing as
|
||||
// "Member … not found on BsonMapper" or a bogus duplicate-_id insert — Configuration-012.
|
||||
// "Member … not found on BsonMapper" or a bogus duplicate-_id insert.
|
||||
private static BsonMapper BuildMapper()
|
||||
{
|
||||
var mapper = new BsonMapper();
|
||||
|
||||
@@ -2,7 +2,7 @@ namespace ZB.MOM.WW.OtOpcUa.Configuration.LocalCache;
|
||||
|
||||
/// <summary>
|
||||
/// Per-node local cache of the most-recently-applied generation(s). Used to bootstrap the
|
||||
/// address space when the central DB is unreachable (decision #79 — degraded-but-running).
|
||||
/// address space when the central DB is unreachable (degraded-but-running).
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// <para><b>Concurrency contract:</b> implementations must serialize writes — specifically,
|
||||
@@ -21,10 +21,12 @@ public interface ILocalConfigCache
|
||||
/// <summary>Stores a generation snapshot in the local cache.</summary>
|
||||
/// <param name="snapshot">The generation snapshot to store.</param>
|
||||
/// <param name="ct">The cancellation token.</param>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
Task PutAsync(GenerationSnapshot snapshot, CancellationToken ct = default);
|
||||
/// <summary>Removes old generations, keeping only the most recent N.</summary>
|
||||
/// <param name="clusterId">The cluster identifier.</param>
|
||||
/// <param name="keepLatest">The number of latest generations to keep.</param>
|
||||
/// <param name="ct">The cancellation token.</param>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
Task PruneOldGenerationsAsync(string clusterId, int keepLatest = 10, CancellationToken ct = default);
|
||||
}
|
||||
|
||||
@@ -6,7 +6,7 @@ namespace ZB.MOM.WW.OtOpcUa.Configuration.LocalCache;
|
||||
/// LiteDB-backed <see cref="ILocalConfigCache"/>. One file per node (default
|
||||
/// <c>config_cache.db</c>), one collection per snapshot. Corruption surfaces as
|
||||
/// <see cref="LocalConfigCacheCorruptException"/> on construction or read — callers should
|
||||
/// delete and re-fetch from the central DB (decision #80).
|
||||
/// delete and re-fetch from the central DB.
|
||||
/// </summary>
|
||||
public sealed class LiteDbConfigCache : ILocalConfigCache, IDisposable
|
||||
{
|
||||
@@ -17,7 +17,7 @@ public sealed class LiteDbConfigCache : ILocalConfigCache, IDisposable
|
||||
// instances. When several caches (this one + GenerationSealedCache) initialise in parallel
|
||||
// the global mapper races, surfacing as "Member ClusterId not found on BsonMapper" or a
|
||||
// bogus "duplicate key _id = 0" (the int auto-id mapping was lost so Insert writes a literal
|
||||
// 0 twice) — Configuration-012. Give each database a private, pre-registered mapper so member
|
||||
// 0 twice). Give each database a private, pre-registered mapper so member
|
||||
// resolution happens once, single-threaded, at construction and never touches the global.
|
||||
private static BsonMapper BuildMapper()
|
||||
{
|
||||
@@ -30,7 +30,7 @@ public sealed class LiteDbConfigCache : ILocalConfigCache, IDisposable
|
||||
private readonly ILiteCollection<GenerationSnapshot> _col;
|
||||
// PutAsync is a find-then-insert/update; without serialization, two concurrent puts for the
|
||||
// same (ClusterId, GenerationId) can both observe `existing is null` and both Insert,
|
||||
// producing duplicate rows (Configuration-005). Serialize writes through this semaphore so
|
||||
// producing duplicate rows. Serialize writes through this semaphore so
|
||||
// the read-modify-write block is atomic for a given instance. LiteDB itself only locks the
|
||||
// page-level write, not the find-then-insert window.
|
||||
private readonly SemaphoreSlim _writeGate = new(initialCount: 1, maxCount: 1);
|
||||
@@ -60,9 +60,7 @@ public sealed class LiteDbConfigCache : ILocalConfigCache, IDisposable
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Gets the most recent snapshot for the specified cluster.</summary>
|
||||
/// <param name="clusterId">The cluster ID.</param>
|
||||
/// <param name="ct">Cancellation token.</param>
|
||||
/// <inheritdoc />
|
||||
public Task<GenerationSnapshot?> GetMostRecentAsync(string clusterId, CancellationToken ct = default)
|
||||
{
|
||||
ct.ThrowIfCancellationRequested();
|
||||
@@ -73,15 +71,13 @@ public sealed class LiteDbConfigCache : ILocalConfigCache, IDisposable
|
||||
return Task.FromResult<GenerationSnapshot?>(snapshot);
|
||||
}
|
||||
|
||||
/// <summary>Stores a snapshot in the cache.</summary>
|
||||
/// <param name="snapshot">The snapshot to store.</param>
|
||||
/// <param name="ct">Cancellation token.</param>
|
||||
/// <inheritdoc />
|
||||
public async Task PutAsync(GenerationSnapshot snapshot, CancellationToken ct = default)
|
||||
{
|
||||
ct.ThrowIfCancellationRequested();
|
||||
// Serialize the find-then-insert/update so concurrent callers do not observe a stale
|
||||
// `existing is null` and both Insert (Configuration-005). LiteDB's per-call lock is
|
||||
// not enough — the read and the write are independent calls.
|
||||
// `existing is null` and both Insert. LiteDB's per-call lock is not enough — the
|
||||
// read and the write are independent calls.
|
||||
await _writeGate.WaitAsync(ct).ConfigureAwait(false);
|
||||
try
|
||||
{
|
||||
@@ -104,10 +100,7 @@ public sealed class LiteDbConfigCache : ILocalConfigCache, IDisposable
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Removes old generation snapshots, keeping only the latest ones.</summary>
|
||||
/// <param name="clusterId">The cluster ID.</param>
|
||||
/// <param name="keepLatest">Number of latest generations to keep.</param>
|
||||
/// <param name="ct">Cancellation token.</param>
|
||||
/// <inheritdoc />
|
||||
public Task PruneOldGenerationsAsync(string clusterId, int keepLatest = 10, CancellationToken ct = default)
|
||||
{
|
||||
ct.ThrowIfCancellationRequested();
|
||||
|
||||
@@ -69,7 +69,7 @@ public sealed class ResilientConfigReader
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Configuration-010: redact connection-string fragments (Password, User Id, Pwd, etc.)
|
||||
/// Redacts connection-string fragments (Password, User Id, Pwd, etc.)
|
||||
/// that a caller's exception message could carry. Conservative regex pass — anything
|
||||
/// matching <c>Key=Value</c> with a known credential key gets its value replaced.
|
||||
/// </summary>
|
||||
@@ -120,7 +120,7 @@ public sealed class ResilientConfigReader
|
||||
// that case, not propagate. Only rethrow if the caller actually requested cancellation.
|
||||
catch (Exception ex) when (ex is not OperationCanceledException || !cancellationToken.IsCancellationRequested)
|
||||
{
|
||||
// Configuration-010: do NOT pass the raw exception object — it carries the stack
|
||||
// Do NOT pass the raw exception object — it carries the stack
|
||||
// and inner-exception chain, and SqlException/wrapping delegates can surface
|
||||
// connection-string fragments (Password=…, User Id=…) embedded in messages.
|
||||
// Log only the exception type and a scrubbed message so secrets stay out of logs.
|
||||
|
||||
@@ -72,8 +72,6 @@ public sealed class OtOpcUaConfigDbContext(DbContextOptions<OtOpcUaConfigDbConte
|
||||
/// <summary>Gets the DbSet of data protection keys.</summary>
|
||||
public DbSet<DataProtectionKey> DataProtectionKeys => Set<DataProtectionKey>();
|
||||
|
||||
/// <summary>Configures the entity model when the context is first created.</summary>
|
||||
/// <param name="modelBuilder">The model builder used to configure the context.</param>
|
||||
/// <inheritdoc />
|
||||
protected override void OnModelCreating(ModelBuilder modelBuilder)
|
||||
{
|
||||
@@ -151,13 +149,12 @@ public sealed class OtOpcUaConfigDbContext(DbContextOptions<OtOpcUaConfigDbConte
|
||||
.HasForeignKey(x => x.ClusterId)
|
||||
.OnDelete(DeleteBehavior.Restrict);
|
||||
|
||||
// Fleet-wide unique per decision #86
|
||||
// Fleet-wide unique.
|
||||
e.HasIndex(x => x.ApplicationUri).IsUnique().HasDatabaseName("UX_ClusterNode_ApplicationUri");
|
||||
e.HasIndex(x => x.ClusterId).HasDatabaseName("IX_ClusterNode_ClusterId");
|
||||
// v2: the "one Primary per cluster" filtered unique index (and the RedundancyRole
|
||||
// column it filtered on) are gone. Akka cluster leader-of-driver-role is the
|
||||
// authoritative primary signal (see RedundancyStateActor + ServiceLevelCalculator,
|
||||
// Task 35).
|
||||
// authoritative primary signal (see RedundancyStateActor + ServiceLevelCalculator).
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -6,7 +6,7 @@ namespace ZB.MOM.WW.OtOpcUa.Configuration.Services;
|
||||
/// <summary>
|
||||
/// CRUD surface for <see cref="LdapGroupRoleMapping"/> — the control-plane mapping from
|
||||
/// LDAP groups to Admin UI roles. Consumed only by Admin UI code paths; the OPC UA
|
||||
/// data-path evaluator MUST NOT depend on this interface (see decision #150 and the
|
||||
/// data-path evaluator MUST NOT depend on this interface (see the
|
||||
/// Phase 6.2 compliance check on control/data-plane separation).
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
@@ -28,11 +28,13 @@ public interface ILdapGroupRoleMappingService
|
||||
/// </remarks>
|
||||
/// <param name="ldapGroups">The LDAP groups to search for.</param>
|
||||
/// <param name="cancellationToken">The cancellation token.</param>
|
||||
/// <returns>The mappings whose LDAP group matches one of <paramref name="ldapGroups"/>.</returns>
|
||||
Task<IReadOnlyList<LdapGroupRoleMapping>> GetByGroupsAsync(
|
||||
IEnumerable<string> ldapGroups, CancellationToken cancellationToken);
|
||||
|
||||
/// <summary>Enumerate every mapping; Admin UI listing only.</summary>
|
||||
/// <param name="cancellationToken">The cancellation token.</param>
|
||||
/// <returns>Every LDAP group role mapping.</returns>
|
||||
Task<IReadOnlyList<LdapGroupRoleMapping>> ListAllAsync(CancellationToken cancellationToken);
|
||||
|
||||
/// <summary>Create a new grant.</summary>
|
||||
@@ -43,11 +45,13 @@ public interface ILdapGroupRoleMappingService
|
||||
/// </exception>
|
||||
/// <param name="row">The LDAP group role mapping to create.</param>
|
||||
/// <param name="cancellationToken">The cancellation token.</param>
|
||||
/// <returns>The created mapping row.</returns>
|
||||
Task<LdapGroupRoleMapping> CreateAsync(LdapGroupRoleMapping row, CancellationToken cancellationToken);
|
||||
|
||||
/// <summary>Delete a mapping by its surrogate key.</summary>
|
||||
/// <param name="id">The unique identifier of the mapping to delete.</param>
|
||||
/// <param name="cancellationToken">The cancellation token.</param>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
Task DeleteAsync(Guid id, CancellationToken cancellationToken);
|
||||
}
|
||||
|
||||
|
||||
@@ -10,10 +10,7 @@ namespace ZB.MOM.WW.OtOpcUa.Configuration.Services;
|
||||
/// </summary>
|
||||
public sealed class LdapGroupRoleMappingService(OtOpcUaConfigDbContext db) : ILdapGroupRoleMappingService
|
||||
{
|
||||
/// <summary>Gets LDAP group role mappings for the specified groups.</summary>
|
||||
/// <param name="ldapGroups">The LDAP group names to query.</param>
|
||||
/// <param name="cancellationToken">The cancellation token.</param>
|
||||
/// <returns>The matching role mappings.</returns>
|
||||
/// <inheritdoc />
|
||||
public async Task<IReadOnlyList<LdapGroupRoleMapping>> GetByGroupsAsync(
|
||||
IEnumerable<string> ldapGroups, CancellationToken cancellationToken)
|
||||
{
|
||||
@@ -28,9 +25,7 @@ public sealed class LdapGroupRoleMappingService(OtOpcUaConfigDbContext db) : ILd
|
||||
.ConfigureAwait(false);
|
||||
}
|
||||
|
||||
/// <summary>Lists all LDAP group role mappings.</summary>
|
||||
/// <param name="cancellationToken">The cancellation token.</param>
|
||||
/// <returns>All role mappings ordered by group and cluster ID.</returns>
|
||||
/// <inheritdoc />
|
||||
public async Task<IReadOnlyList<LdapGroupRoleMapping>> ListAllAsync(CancellationToken cancellationToken)
|
||||
=> await db.LdapGroupRoleMappings
|
||||
.AsNoTracking()
|
||||
@@ -39,10 +34,7 @@ public sealed class LdapGroupRoleMappingService(OtOpcUaConfigDbContext db) : ILd
|
||||
.ToListAsync(cancellationToken)
|
||||
.ConfigureAwait(false);
|
||||
|
||||
/// <summary>Creates a new LDAP group role mapping.</summary>
|
||||
/// <param name="row">The mapping to create.</param>
|
||||
/// <param name="cancellationToken">The cancellation token.</param>
|
||||
/// <returns>The created mapping with generated ID and timestamp.</returns>
|
||||
/// <inheritdoc />
|
||||
public async Task<LdapGroupRoleMapping> CreateAsync(LdapGroupRoleMapping row, CancellationToken cancellationToken)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(row);
|
||||
@@ -56,10 +48,7 @@ public sealed class LdapGroupRoleMappingService(OtOpcUaConfigDbContext db) : ILd
|
||||
return row;
|
||||
}
|
||||
|
||||
/// <summary>Deletes an LDAP group role mapping.</summary>
|
||||
/// <param name="id">The mapping identifier.</param>
|
||||
/// <param name="cancellationToken">The cancellation token.</param>
|
||||
/// <returns>A task that completes when the deletion is done.</returns>
|
||||
/// <inheritdoc />
|
||||
public async Task DeleteAsync(Guid id, CancellationToken cancellationToken)
|
||||
{
|
||||
var existing = await db.LdapGroupRoleMappings.FindAsync([id], cancellationToken).ConfigureAwait(false);
|
||||
|
||||
@@ -5,7 +5,7 @@ using ZB.MOM.WW.OtOpcUa.Configuration.Enums;
|
||||
namespace ZB.MOM.WW.OtOpcUa.Configuration.Validation;
|
||||
|
||||
/// <summary>
|
||||
/// Managed-code pre-publish validator per decision #91. Complements the structural checks in
|
||||
/// Managed-code pre-publish validator. Complements the structural checks in
|
||||
/// <c>sp_ValidateDraft</c> — this layer owns schema validation for JSON columns, UNS segment
|
||||
/// regex, EquipmentId derivation, cross-cluster checks, and anything else that's uncomfortable
|
||||
/// to express in T-SQL. Returns every failing rule in one pass (decision: surface all errors,
|
||||
@@ -21,6 +21,7 @@ public static class DraftValidator
|
||||
/// Validates a draft snapshot and returns all validation errors found in a single pass.
|
||||
/// </summary>
|
||||
/// <param name="draft">The draft snapshot to validate.</param>
|
||||
/// <returns>Every validation error found; empty when the draft is valid.</returns>
|
||||
public static IReadOnlyList<ValidationError> Validate(DraftSnapshot draft)
|
||||
{
|
||||
var errors = new List<ValidationError>();
|
||||
@@ -204,8 +205,9 @@ public static class DraftValidator
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Decision #125: EquipmentId = 'EQ-' + lowercase first 12 hex chars of the UUID.</summary>
|
||||
/// <summary>EquipmentId = 'EQ-' + lowercase first 12 hex chars of the UUID.</summary>
|
||||
/// <param name="uuid">The equipment UUID to derive the ID from.</param>
|
||||
/// <returns>The derived <c>EQ-</c>-prefixed EquipmentId.</returns>
|
||||
public static string DeriveEquipmentId(Guid uuid) =>
|
||||
"EQ-" + uuid.ToString("N")[..12].ToLowerInvariant();
|
||||
|
||||
@@ -222,7 +224,7 @@ public static class DraftValidator
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Phase 6.3 Stream A.2 + task #148 part 2 — managed pre-publish guard for cluster
|
||||
/// Managed pre-publish guard for cluster
|
||||
/// topology vs. <see cref="ServerCluster.RedundancyMode"/>. The SQL
|
||||
/// <c>CK_ServerCluster_RedundancyMode_NodeCount</c> CHECK already enforces the
|
||||
/// (NodeCount, RedundancyMode) pair on the row itself, but it cannot see the
|
||||
@@ -240,6 +242,8 @@ public static class DraftValidator
|
||||
/// </remarks>
|
||||
/// <param name="cluster">The server cluster to validate.</param>
|
||||
/// <param name="clusterNodes">The cluster nodes to validate against the cluster configuration.</param>
|
||||
/// <returns>Every failing topology rule found; empty when the cluster's declared and enabled-node
|
||||
/// topology is consistent with its <see cref="ServerCluster.RedundancyMode"/>.</returns>
|
||||
public static IReadOnlyList<ValidationError> ValidateClusterTopology(
|
||||
ServerCluster cluster,
|
||||
IReadOnlyList<ClusterNode> clusterNodes)
|
||||
@@ -273,7 +277,7 @@ public static class DraftValidator
|
||||
cluster.ClusterId));
|
||||
|
||||
// v2: the v1 "exactly one Primary per cluster" invariant is gone. RedundancyRole was
|
||||
// dropped in Task 14d; in v2 the Akka cluster's role-leader-of-"driver" elects the
|
||||
// dropped; in v2 the Akka cluster's role-leader-of-"driver" elects the
|
||||
// primary at runtime, so there is no static configuration to validate here.
|
||||
|
||||
return errors;
|
||||
|
||||
Reference in New Issue
Block a user