using Microsoft.Extensions.Logging; using ZB.MOM.WW.OtOpcUa.Core.Authorization; namespace ZB.MOM.WW.OtOpcUa.Host.OpcUa; /// /// Runs the node manager's monitored-item revalidation pass on a low-frequency tick, so a key that /// expires by the CLOCK reaches a live subscription without waiting for a deployment (arch-review /// 03/S19). /// /// /// /// Every request-shaped operation re-checks the calling key's usability per call and so honours /// ExpiresUtc the moment it passes. A subscription is authorized once and then streams, and /// streaming is how machine consumers actually read this server — so before this the only thing /// that could stand a subscription down was a deployment. /// /// /// ⚠️ The tick must never throw. An unhandled exception on a timer callback thread takes /// the process down. Every tick is wrapped; a faulting pass is logged and the timer keeps running, /// because the next tick is the recovery. /// /// /// ⚠️ Each tick gets a fresh memo, and that is the pass's own constraint, not this class's. /// RevalidateMonitoredItems allocates its verdict dictionary per call — see the remarks on /// OtOpcUaNodeManager.OnAuthorityChanged. Nothing here may hoist it across ticks, or an /// expiry landing between two ticks would be served the earlier tick's allow. /// /// /// It adds no decision logic: the verdict comes from the authorizer's own /// , and the stand-down (queue BadUserAccessDenied, then detach) /// and its restore are the pass's, unchanged. The pass early-returns unless ACL enforcement is on, /// so this is inert on the default configuration. /// /// public sealed class AclRevalidationTimer : IDisposable { private readonly Action _revalidate; private readonly ILogger _log; private readonly ITimer? _timer; private volatile bool _disposed; /// Initializes a new instance of the class. /// The revalidation pass to run on each tick. /// /// How often to run it. Non-positive arms no timer at all — the deploy-triggered-only behaviour /// that predates 03/S19. /// /// The shared clock; its CreateTimer is what arms the tick. /// The logger. public AclRevalidationTimer(Action revalidate, TimeSpan interval, TimeProvider time, ILogger log) { ArgumentNullException.ThrowIfNull(revalidate); ArgumentNullException.ThrowIfNull(time); ArgumentNullException.ThrowIfNull(log); _revalidate = revalidate; _log = log; if (interval <= TimeSpan.Zero) { // Say so once. A disabled guard that logs nothing is indistinguishable from a guard that is // running and finding nothing, which is precisely the confusion 03/S19 is about. _log.LogInformation( "AclRevalidationTimer: disabled — {Section}:RevalidationIntervalSeconds is not positive, " + "so live monitored items are re-authorized only when a deployment lands", AclOptions.SectionName); return; } _timer = time.CreateTimer(_ => Tick(), state: null, interval, interval); _log.LogInformation( "AclRevalidationTimer: live monitored items will be re-authorized every {Interval}", interval); } /// Runs one revalidation pass, absorbing anything it throws. /// Exposed so a test can drive the callback body deterministically rather than by waiting. public void Tick() { if (_disposed) return; try { _revalidate(); } catch (Exception ex) { _log.LogError(ex, "AclRevalidationTimer: the monitored-item revalidation pass threw; " + "the timer keeps running and the next tick is the recovery"); } } /// public void Dispose() { _disposed = true; _timer?.Dispose(); } }