Merge branch 'master' into fix/r3-02-stale-good-family

# Conflicts:
#	src/Server/ZB.MOM.WW.OtOpcUa.Runtime/Drivers/DriverHostActor.cs
#	tests/Server/ZB.MOM.WW.OtOpcUa.Runtime.Tests/Harness/V3ArtifactSeeder.cs
This commit is contained in:
Joseph Doherty
2026-08-20 11:03:02 -04:00
46 changed files with 5411 additions and 208 deletions
@@ -104,8 +104,6 @@ public static class OtOpcUaTelemetry
Meter.CreateCounter<long>("otopcua.redundancy.service_level_change", unit: "{change}",
description: "OPC UA Server.ServiceLevel transitions emitted by the redundancy state.");
/// <summary>Inbound operations denied by the Primary data-plane gate (archreview 03/S4) —
/// site=write|ack|alarm-emit, reason=secondary|detached|role-unknown.</summary>
/// <summary>Archreview 03/S15 — tag refs a driver left <c>BadNoCommunication</c> after it reconnected:
/// the host marked them Bad on the outage, and the driver never re-published them. Non-zero means a
/// driver that reconnects without a full re-publish (a push driver missing its initial push, or a
@@ -115,10 +113,26 @@ public static class OtOpcUaTelemetry
Meter.CreateCounter<long>("otopcua.driver.comms_latched_refs", unit: "{ref}",
description: "Refs still BadNoCommunication after their driver reconnected (driver, driver_type).");
/// <summary>Inbound operations denied by the Primary data-plane gate (archreview 03/S4) —
/// site=write|ack|alarm-emit, reason=secondary|detached|role-unknown.</summary>
public static readonly Counter<long> PrimaryGateDenied =
Meter.CreateCounter<long>("otopcua.redundancy.primary_gate_denied", unit: "{denial}",
description: "Operations denied by the Primary gate (site=write|ack|alarm-emit, reason=secondary|detached|role-unknown).");
/// <summary>
/// API-key store lookups the 03/S18 bound intervened on (outcome=timeout|cache_hit).
/// </summary>
/// <remarks>
/// One counter with an <c>outcome</c> tag rather than two: <c>timeout</c> is the alarm signal that
/// the ConfigDb is not answering the OPC UA impersonation callback, and <c>cache_hit</c> is the
/// ratio that says whether the TTL is doing anything. A missing <c>cache_hit</c> stream on a busy
/// fused central is how you notice <c>Security:ApiKeys:StoreCacheSeconds</c> was set to 0 by
/// accident. A plain read-through meters neither — the counter names interventions, not traffic.
/// </remarks>
public static readonly Counter<long> ApiKeyStoreBounded =
Meter.CreateCounter<long>("otopcua.opcua.apikey_store.bounded", unit: "{lookup}",
description: "API-key store lookups the bound intervened on (outcome=timeout|cache_hit).");
// ---------------- Convenience helpers ----------------
/// <summary>
@@ -42,4 +42,23 @@ public sealed class AclOptions
/// false, which preserves the pre-stage-5 role gates byte-for-byte.
/// </summary>
public bool Enabled { get; set; }
/// <summary>
/// How often, in seconds, live monitored items are re-authorized against the clock — in addition
/// to on every deployment (arch-review 03/S19).
/// </summary>
/// <remarks>
/// <para>
/// A subscription is authorized once and then streams, so before this a key that expired by the
/// CLOCK kept feeding a live subscription until a deployment happened to advance the generation
/// — indefinitely on a fleet that has been deployed once and left running. Every other seam
/// re-asks per request and honours <c>ExpiresUtc</c> the moment it passes.
/// </para>
/// <para>
/// <c>0</c> or negative disables the timer, leaving revalidation deploy-triggered only (the
/// pre-R3-05 behaviour). The timer is <b>inert while <see cref="Enabled"/> is false</b>: the
/// pass early-returns when ACL enforcement is off.
/// </para>
/// </remarks>
public int RevalidationIntervalSeconds { get; set; } = 60;
}
@@ -27,4 +27,29 @@ public sealed class ApiKeyOptions
/// <c>ApiKeySecret.ComputeHash</c> throws on one and this stays empty to make that fire early.
/// </remarks>
public string Pepper { get; set; } = string.Empty;
/// <summary>
/// Hard per-call deadline, in milliseconds, on the API-key store lookup that runs inline on OPC UA
/// session activation (arch-review 03/S18). Exceeding it <b>denies</b> the activation and emits a
/// distinct Warning plus <c>otopcua.opcua.apikey_store.bounded{outcome=timeout}</c>.
/// </summary>
/// <remarks>
/// <c>0</c> or negative disables the deadline, restoring the pre-R3-05 unbounded behaviour. Do not
/// do that on a node with a ConfigDb: the SDK raises impersonation synchronously under a
/// <c>SessionManager</c>-wide lock, so an unanswered lookup stalls <b>every</b> session activation
/// in the server, not just its own.
/// </remarks>
public int StoreTimeoutMs { get; set; } = 2000;
/// <summary>
/// TTL, in seconds, of the read-through cache in front of the API-key store (arch-review 03/S18).
/// Bounds how long a revocation authored in the ConfigDb can take to reach a <b>new</b> session
/// activation.
/// </summary>
/// <remarks>
/// <c>0</c> or negative disables the cache, restoring a store read per activation. The window it
/// introduces is strictly smaller than the one that already exists for <b>already-open</b> sessions
/// on every node kind, which is the next deployment — see <c>docs/security.md</c>.
/// </remarks>
public int StoreCacheSeconds { get; set; } = 5;
}