73e74e270d
StartDeployment read the whole config DB twice with no transaction — DraftSnapshotFactory for the validator, then ConfigComposer for the artifact, plus clusters/nodes and Script.SourceCode again in between. An AdminUI save landing between the two shipped an artifact the DraftValidator never saw. - DraftSnapshot carries ServerClusters, ClusterNodes and ApiKeys; DraftSnapshotFactory reads every table inside one SNAPSHOT transaction on a relational provider (no-op on InMemory or under an ambient transaction). - ConfigComposer.Compose(DraftSnapshot) is pure over the snapshot (ordinal-sorted keys); SnapshotAndFlattenAsync(db) delegates to it, byte-identical. - AdminOperationsActor validates topology, estimates compile cost and composes the artifact from the one snapshot it holds. - Migration EnableSnapshotIsolation: ALTER DATABASE CURRENT SET ALLOW_SNAPSHOT_ISOLATION ON (suppressTransaction; no model change). Tests: the SQL Server snapshot read ignores a row committed mid-read (positive control: a later read sees it); an edit injected between the Equipment and Tags reads is absent from the shipped artifact and rejected by the next deploy; Compose(snapshot) == SnapshotAndFlattenAsync(db). Refs #733 Claude-Session: https://claude.ai/code/session_01Ub9hGmYLQvn9XETzw7CMsT
92 lines
5.2 KiB
C#
92 lines
5.2 KiB
C#
using ZB.MOM.WW.OtOpcUa.Configuration.Entities;
|
|
|
|
namespace ZB.MOM.WW.OtOpcUa.Configuration.Validation;
|
|
|
|
/// <summary>
|
|
/// Inputs for draft validation. Contains the draft's rows plus the minimum prior-generation
|
|
/// rows needed for cross-generation invariants (EquipmentUuid stability, UnsArea identity).
|
|
/// </summary>
|
|
public sealed class DraftSnapshot
|
|
{
|
|
/// <summary>Gets the draft generation identifier.</summary>
|
|
public required long GenerationId { get; init; }
|
|
/// <summary>Gets the cluster identifier.</summary>
|
|
public required string ClusterId { get; init; }
|
|
|
|
/// <summary>
|
|
/// Cluster's Enterprise segment (UNS level 1). When set, <see cref="DraftValidator"/> uses
|
|
/// the actual length for path-length checks instead of a conservative 32-char upper bound.
|
|
/// </summary>
|
|
public string? Enterprise { get; init; }
|
|
|
|
/// <summary>
|
|
/// Cluster's Site segment (UNS level 2). When set, <see cref="DraftValidator"/> uses the
|
|
/// actual length for path-length checks instead of a conservative 32-char upper bound.
|
|
/// </summary>
|
|
public string? Site { get; init; }
|
|
|
|
// v3: the Namespace entity is retired (the two OPC UA namespaces are implicit). The Raw tree
|
|
// (RawFolders → DriverInstances → Devices → TagGroups → Tags) + the UNS projection
|
|
// (UnsTagReferences) feed the v3 validator rules (raw-name charset, historized-tagname length, UNS
|
|
// effective-leaf uniqueness).
|
|
/// <summary>Gets the list of Raw-tree folders (drive the raw-name charset rule + RawPath computation).</summary>
|
|
public IReadOnlyList<RawFolder> RawFolders { get; init; } = [];
|
|
/// <summary>Gets the list of driver instances.</summary>
|
|
public IReadOnlyList<DriverInstance> DriverInstances { get; init; } = [];
|
|
/// <summary>Gets the list of devices.</summary>
|
|
public IReadOnlyList<Device> Devices { get; init; } = [];
|
|
/// <summary>Gets the list of tag-groups (drive the raw-name charset rule + RawPath computation).</summary>
|
|
public IReadOnlyList<TagGroup> TagGroups { get; init; } = [];
|
|
/// <summary>Gets the list of UNS areas.</summary>
|
|
public IReadOnlyList<UnsArea> UnsAreas { get; init; } = [];
|
|
/// <summary>Gets the list of UNS lines.</summary>
|
|
public IReadOnlyList<UnsLine> UnsLines { get; init; } = [];
|
|
/// <summary>Gets the list of equipment.</summary>
|
|
public IReadOnlyList<Equipment> Equipment { get; init; } = [];
|
|
/// <summary>Gets the list of raw tags.</summary>
|
|
public IReadOnlyList<Tag> Tags { get; init; } = [];
|
|
|
|
/// <summary>Gets the UNS tag references (raw tag → equipment projection). Drives the UNS effective-leaf
|
|
/// uniqueness rule (effective name = DisplayNameOverride else the backing raw tag's Name).</summary>
|
|
public IReadOnlyList<UnsTagReference> UnsTagReferences { get; init; } = [];
|
|
|
|
/// <summary>Equipment-bound VirtualTags (script-derived signals). Part of the UNS effective-leaf
|
|
/// uniqueness set within an equipment (references + VirtualTags + ScriptedAlarms).</summary>
|
|
public IReadOnlyList<VirtualTag> VirtualTags { get; init; } = [];
|
|
/// <summary>Equipment-bound scripted alarms. Part of the UNS effective-leaf uniqueness set.</summary>
|
|
public IReadOnlyList<ScriptedAlarm> ScriptedAlarms { get; init; } = [];
|
|
|
|
/// <summary>User-authored scripts (shared by VirtualTags, ScriptedAlarms, and Calculation tags). Drives
|
|
/// the Calculation gates: <c>scriptId</c> existence + the calc→calc dependency-cycle check.</summary>
|
|
public IReadOnlyList<Script> Scripts { get; init; } = [];
|
|
/// <summary>Gets the list of poll groups.</summary>
|
|
public IReadOnlyList<PollGroup> PollGroups { get; init; } = [];
|
|
|
|
/// <summary>ACL grants. Drive the scope-resolution rule: a scope that names nothing deployed is
|
|
/// silently dropped when the permission trie is built, so the grant has to be caught here instead.</summary>
|
|
public IReadOnlyList<NodeAcl> NodeAcls { get; init; } = [];
|
|
|
|
/// <summary>
|
|
/// Cluster rows. Not read by the row-level rules; carried so the deploy path validates the cluster
|
|
/// topology (<see cref="DraftValidator.ValidateClusterTopology"/>) and composes the artifact from
|
|
/// the SAME read as every other table — a second read between validate and compose is exactly how
|
|
/// an edit ships unvalidated.
|
|
/// </summary>
|
|
public IReadOnlyList<ServerCluster> ServerClusters { get; init; } = [];
|
|
|
|
/// <summary>Cluster-node rows; see <see cref="ServerClusters"/> for why they ride the snapshot.</summary>
|
|
public IReadOnlyList<ClusterNode> ClusterNodes { get; init; } = [];
|
|
|
|
/// <summary>
|
|
/// Data-plane API keys (peppered HMAC only, never a secret). Not validated; carried so the artifact
|
|
/// composer ships the keys a driver-only node authenticates against from the one consistent read.
|
|
/// </summary>
|
|
public IReadOnlyList<ApiKey> ApiKeys { get; init; } = [];
|
|
|
|
/// <summary>Prior Equipment rows (any generation, same cluster) for stability checks.</summary>
|
|
public IReadOnlyList<Equipment> PriorEquipment { get; init; } = [];
|
|
|
|
/// <summary>Active reservations (<c>ReleasedAt IS NULL</c>) for pre-flight.</summary>
|
|
public IReadOnlyList<ExternalIdReservation> ActiveReservations { get; init; } = [];
|
|
}
|