/* ======================================================================== * Copyright (c) 2005-2025 The OPC Foundation, Inc. All rights reserved. * * OPC Foundation MIT License 1.00 * * Permission is hereby granted, free of charge, to any person * obtaining a copy of this software and associated documentation * files (the "Software"), to deal in the Software without * restriction, including without limitation the rights to use, * copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the * Software is furnished to do so, subject to the following * conditions: * * The above copyright notice and this permission notice shall be * included in all copies or substantial portions of the Software. * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES * OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT * HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, * WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR * OTHER DEALINGS IN THE SOFTWARE. * * The complete license agreement can be found here: * http://opcfoundation.org/License/MIT/1.00/ * ======================================================================*/ using System; using System.Threading; using System.Threading.Tasks; namespace Opc.Ua.Client.AliasNames.Refresh { /// /// Refresh strategy that periodically reads the category's /// LastChange property (Part 17 ยง6.3.1) via the standard /// Read service and invalidates the resolver cache when the /// value changes (wrap-safe โ€” compares for inequality, not strict /// greater-than). /// /// /// Suitable for any server that exposes LastChange โ€” including /// servers that disable subscriptions. Pollintroduces one /// Read service round-trip per ; use /// instead when /// the server supports subscriptions and a finer push notification /// is preferable. /// public sealed class PollingAliasNameRefreshStrategy : IAliasNameRefreshStrategy { /// /// Initializes a new poller with the supplied interval. /// /// Time between successive /// LastChange reads. Must be at least 100 ms. /// Optional /// used to create the poll timer. Defaults to /// when null. public PollingAliasNameRefreshStrategy( TimeSpan pollInterval, TimeProvider? timeProvider = null) { if (pollInterval < TimeSpan.FromMilliseconds(100)) { throw new ArgumentOutOfRangeException( nameof(pollInterval), "Poll interval must be at least 100 ms."); } m_timeProvider = timeProvider ?? TimeProvider.System; PollInterval = pollInterval; } /// /// The polling interval. /// public TimeSpan PollInterval { get; } /// public ValueTask StartAsync( AliasNameClient client, Action onInvalidate, CancellationToken ct) { m_client = client ?? throw new ArgumentNullException(nameof(client)); m_onInvalidate = onInvalidate ?? throw new ArgumentNullException(nameof(onInvalidate)); m_timer = m_timeProvider.CreateTimer( Tick, state: null, dueTime: PollInterval, period: PollInterval); return default; } /// public ValueTask DisposeAsync() { ITimer? timer = m_timer; m_timer = null; timer?.Dispose(); return default; } private void Tick(object? _) { // Fire-and-forget; a server hiccup must not crash the // timer thread. _ = TickAsync(); } private async Task TickAsync() { AliasNameClient? client = m_client; Action? invalidate = m_onInvalidate; if (client == null || invalidate == null) { return; } try { uint? current = await client.ReadLastChangeAsync().ConfigureAwait(false); if (current == null) { return; } uint? last = m_lastSeen; // Wrap-safe โ€” VersionTime is uint and may wrap; any // difference (including wraparound) is a change. if (last != current) { m_lastSeen = current; invalidate(); } } catch { // Transient errors are swallowed; the next tick retries. } } private AliasNameClient? m_client; private Action? m_onInvalidate; private ITimer? m_timer; private uint? m_lastSeen; private readonly TimeProvider m_timeProvider; } }