100 lines
4.4 KiB
C#
100 lines
4.4 KiB
C#
using Microsoft.Extensions.Logging;
|
|
using ZB.MOM.WW.OtOpcUa.Core.Authorization;
|
|
|
|
namespace ZB.MOM.WW.OtOpcUa.Host.OpcUa;
|
|
|
|
/// <summary>
|
|
/// 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).
|
|
/// </summary>
|
|
/// <remarks>
|
|
/// <para>
|
|
/// Every request-shaped operation re-checks the calling key's usability per call and so honours
|
|
/// <c>ExpiresUtc</c> 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.
|
|
/// </para>
|
|
/// <para>
|
|
/// ⚠️ <b>The tick must never throw.</b> 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.
|
|
/// </para>
|
|
/// <para>
|
|
/// ⚠️ <b>Each tick gets a fresh memo, and that is the pass's own constraint, not this class's.</b>
|
|
/// <c>RevalidateMonitoredItems</c> allocates its verdict dictionary per call — see the remarks on
|
|
/// <c>OtOpcUaNodeManager.OnAuthorityChanged</c>. Nothing here may hoist it across ticks, or an
|
|
/// expiry landing between two ticks would be served the earlier tick's allow.
|
|
/// </para>
|
|
/// <para>
|
|
/// It adds no decision logic: the verdict comes from the authorizer's own
|
|
/// <see cref="TimeProvider"/>, and the stand-down (queue <c>BadUserAccessDenied</c>, 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.
|
|
/// </para>
|
|
/// </remarks>
|
|
public sealed class AclRevalidationTimer : IDisposable
|
|
{
|
|
private readonly Action _revalidate;
|
|
private readonly ILogger _log;
|
|
private readonly ITimer? _timer;
|
|
private volatile bool _disposed;
|
|
|
|
/// <summary>Initializes a new instance of the <see cref="AclRevalidationTimer"/> class.</summary>
|
|
/// <param name="revalidate">The revalidation pass to run on each tick.</param>
|
|
/// <param name="interval">
|
|
/// How often to run it. Non-positive arms no timer at all — the deploy-triggered-only behaviour
|
|
/// that predates 03/S19.
|
|
/// </param>
|
|
/// <param name="time">The shared clock; its <c>CreateTimer</c> is what arms the tick.</param>
|
|
/// <param name="log">The logger.</param>
|
|
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);
|
|
}
|
|
|
|
/// <summary>Runs one revalidation pass, absorbing anything it throws.</summary>
|
|
/// <remarks>Exposed so a test can drive the callback body deterministically rather than by waiting.</remarks>
|
|
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");
|
|
}
|
|
}
|
|
|
|
/// <inheritdoc />
|
|
public void Dispose()
|
|
{
|
|
_disposed = true;
|
|
_timer?.Dispose();
|
|
}
|
|
}
|