/* ========================================================================
* 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;
}
}