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();
}
}