/* ========================================================================
* 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/
* ======================================================================*/
#if NET8_0_OR_GREATER
#nullable enable
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using BitFaster.Caching;
using BitFaster.Caching.Lfu;
using BitFaster.Caching.Lru;
namespace Opc.Ua.Client
{
///
/// This node cache is a real cache and consists of three LRU caches.
/// These caches contain nodes and references (Read and browse).
/// Cache entries expire after not being accessed for a certain time.
/// The cache is thread-safe and can be accessed by multiple readers.
/// It is also limited to a capacity at which point least recently used
/// entries will be evicted.
///
public sealed class LruNodeCache : ILruNodeCache
{
///
/// Create cache
///
public LruNodeCache(
INodeCacheContext context,
ITelemetryContext telemetry,
TimeSpan? cacheExpiry = null,
int capacity = 4096,
bool withMetrics = false)
{
m_context = context;
cacheExpiry ??= TimeSpan.FromMinutes(5);
BitFaster.Caching.Lru.Builder.AtomicAsyncConcurrentLruBuilder nodesBuilder =
new ConcurrentLruBuilder()
.WithAtomicGetOrAdd()
.AsAsyncCache()
.WithCapacity(capacity)
.WithKeyComparer(Comparers.Instance)
.WithExpireAfterAccess(cacheExpiry.Value);
BitFaster.Caching.Lru.Builder.AtomicAsyncConcurrentLruBuilder<
NodeId,
List
> refsBuilder = new ConcurrentLruBuilder>()
.WithAtomicGetOrAdd()
.AsAsyncCache()
.WithCapacity(capacity)
.WithKeyComparer(Comparers.Instance)
.WithExpireAfterAccess(cacheExpiry.Value);
BitFaster.Caching.Lru.Builder.AtomicAsyncConcurrentLruBuilder valuesBuilder =
new ConcurrentLruBuilder()
.WithAtomicGetOrAdd()
.AsAsyncCache()
.WithCapacity(capacity)
.WithKeyComparer(Comparers.Instance)
.WithExpireAfterAccess(cacheExpiry.Value);
if (withMetrics)
{
nodesBuilder = nodesBuilder.WithMetrics();
valuesBuilder = valuesBuilder.WithMetrics();
refsBuilder = refsBuilder.WithMetrics();
}
m_nodes = nodesBuilder.Build();
m_values = valuesBuilder.Build();
m_refs = refsBuilder.Build();
}
///
/// Node metrics
///
public ICacheMetrics? NodesMetrics => m_nodes.Metrics.Value;
///
/// Value metrics
///
public ICacheMetrics? ValuesMetrics => m_values.Metrics.Value;
///
/// References metrics
///
public ICacheMetrics? ReferencesMetrics => m_refs.Metrics.Value;
///
public NamespaceTable NamespaceUris => m_context.NamespaceUris;
///
public ValueTask GetNodeAsync(NodeId nodeId, CancellationToken ct)
{
return m_nodes.TryGet(nodeId, out INode? node)
? ValueTask.FromResult(node)
: FindAsyncCore(nodeId, ct);
ValueTask FindAsyncCore(NodeId nodeId, CancellationToken ct)
{
return m_nodes.GetOrAddAsync(
nodeId,
async (nodeId, context) => await context.ctx.FetchNodeAsync(
null,
nodeId,
NodeClass.Unspecified,
ct: context.ct)
.ConfigureAwait(false),
(ctx: m_context, ct));
}
}
///
public ValueTask> GetNodesAsync(
IReadOnlyList nodeIds,
CancellationToken ct)
{
int count = nodeIds.Count;
var result = new List(nodeIds.Count);
if (count != 0)
{
var notFound = new List();
foreach (NodeId nodeId in nodeIds)
{
if (m_nodes.TryGet(nodeId, out INode? node))
{
result.Add(node);
continue;
}
notFound.Add(nodeId);
result.Add(null);
}
if (notFound.Count != 0)
{
return FetchRemainingAsync(notFound, result, ct);
}
}
Debug.Assert(!result.Any(r => r == null)); // None now should be null
return ValueTask.FromResult>(result!);
}
///
public ValueTask GetValueAsync(NodeId nodeId, CancellationToken ct)
{
return m_values.TryGet(nodeId, out DataValue? dataValue)
? ValueTask.FromResult(dataValue)
: FindAsyncCore(nodeId, ct);
ValueTask FindAsyncCore(NodeId nodeId, CancellationToken ct)
{
return m_values.GetOrAddAsync(
nodeId,
async (nodeId, context) =>
await context.ctx.FetchValueAsync(null, nodeId, context.ct)
.ConfigureAwait(false),
(ctx: m_context, ct));
}
}
///
public ValueTask> GetValuesAsync(
IReadOnlyList nodeIds,
CancellationToken ct)
{
int count = nodeIds.Count;
var result = new List(nodeIds.Count);
if (count != 0)
{
var notFound = new List();
foreach (NodeId nodeId in nodeIds)
{
if (m_values.TryGet(nodeId, out DataValue? dataValue))
{
result.Add(dataValue);
continue;
}
notFound.Add(nodeId);
result.Add(null);
}
if (notFound.Count != 0)
{
return FetchRemainingAsync(notFound, result, ct);
}
}
Debug.Assert(!result.Any(r => r == null)); // None now should be null
return ValueTask.FromResult>(result!);
}
///
public ValueTask> GetReferencesAsync(
NodeId nodeId,
NodeId referenceTypeId,
bool isInverse,
bool includeSubtypes,
CancellationToken ct)
{
return
(!includeSubtypes || IsTypeHierarchyLoaded([referenceTypeId])) &&
m_refs.TryGet(nodeId, out List? references)
? GetNodesAsync(
FilterNodes(references, isInverse, referenceTypeId, includeSubtypes),
ct)
: FindReferencesAsyncCore(nodeId, referenceTypeId, isInverse, includeSubtypes, ct);
async ValueTask> FindReferencesAsyncCore(
NodeId nodeId,
NodeId referenceTypeId,
bool isInverse,
bool includeSubtypes,
CancellationToken ct)
{
if (includeSubtypes)
{
await LoadTypeHierarchyAsync([referenceTypeId], ct).ConfigureAwait(false);
}
List references = await GetOrAddReferencesAsync(nodeId, ct)
.ConfigureAwait(false);
return await GetNodesAsync(
FilterNodes(references, isInverse, referenceTypeId, includeSubtypes),
ct)
.ConfigureAwait(false);
}
List FilterNodes(
IEnumerable references,
bool isInverse,
NodeId refTypeId,
bool includeSubtypes)
{
return
[
.. references
.Where(r =>
r.IsForward == !isInverse &&
(
r.ReferenceTypeId == refTypeId ||
(includeSubtypes && IsTypeOf(r.ReferenceTypeId, refTypeId))))
.Select(r => ToNodeId(r.NodeId))
.Where(n => !NodeId.IsNull(n))
];
}
}
///
public ValueTask> GetReferencesAsync(
IReadOnlyList nodeIds,
IReadOnlyList referenceTypeIds,
bool isInverse,
bool includeSubtypes,
CancellationToken ct)
{
var targetIds = new List();
var notFound = new List();
if (includeSubtypes && !IsTypeHierarchyLoaded(referenceTypeIds))
{
return FindReferencesAsyncCore(
notFound,
referenceTypeIds,
isInverse,
includeSubtypes,
targetIds,
ct);
}
foreach (NodeId nodeId in nodeIds)
{
if (NodeId.IsNull(nodeId))
{
continue;
}
if (m_refs.TryGet(nodeId, out List? references))
{
targetIds.AddRange(
FilterNodes(references, isInverse, referenceTypeIds, includeSubtypes));
}
else
{
notFound.Add(nodeId);
}
}
return notFound.Count != 0
? FindReferencesAsyncCore(
notFound,
referenceTypeIds,
isInverse,
includeSubtypes,
targetIds,
ct)
: GetNodesAsync(targetIds, ct);
async ValueTask> FindReferencesAsyncCore(
IReadOnlyList nodeIds,
IReadOnlyList referenceTypeIds,
bool isInverse,
bool includeSubtypes,
List targetIds,
CancellationToken ct)
{
if (includeSubtypes)
{
await LoadTypeHierarchyAsync(referenceTypeIds, ct).ConfigureAwait(false);
}
foreach (NodeId nodeId in nodeIds)
{
List references = await GetOrAddReferencesAsync(
nodeId,
ct)
.ConfigureAwait(false);
targetIds.AddRange(
FilterNodes(references, isInverse, referenceTypeIds, includeSubtypes));
}
return await GetNodesAsync(targetIds, ct).ConfigureAwait(false);
}
List FilterNodes(
IEnumerable references,
bool isInverse,
IReadOnlyList referenceTypeIds,
bool includeSubtypes)
{
return
[
.. references
.Where(r =>
r.IsForward == !isInverse &&
referenceTypeIds.Any(refTypeId =>
r.ReferenceTypeId == refTypeId ||
(includeSubtypes && IsTypeOf(r.ReferenceTypeId, refTypeId))))
.Select(r => ToNodeId(r.NodeId))
.Where(n => !NodeId.IsNull(n))
];
}
}
///
public async ValueTask LoadTypeHierarchyAsync(
IReadOnlyList typeIds,
CancellationToken ct)
{
IReadOnlyList nodes = await GetReferencesAsync(
typeIds,
[ReferenceTypeIds.HasSubtype],
false,
false,
ct)
.ConfigureAwait(false);
if (nodes.Count > 0)
{
if (nodes is not List subTypes)
{
subTypes = [.. nodes];
}
await LoadTypeHierarchyAsync(subTypes.ConvertAll(n => ToNodeId(n.NodeId)), ct)
.ConfigureAwait(false);
}
}
///
public bool IsTypeOf(NodeId subTypeId, NodeId superTypeId)
{
if (subTypeId == superTypeId)
{
return true;
}
if (!m_refs.TryGet(subTypeId, out List? references))
{
// block - we can throw here but user should load
references = GetOrAddReferencesAsync(subTypeId, default).AsTask().GetAwaiter()
.GetResult();
}
subTypeId = GetSuperTypeFromReferences(references);
return !NodeId.IsNull(subTypeId) && IsTypeOf(subTypeId, superTypeId);
}
///
public ValueTask GetSuperTypeAsync(NodeId typeId, CancellationToken ct)
{
return m_refs.TryGet(typeId, out List? references)
? ValueTask.FromResult(GetSuperTypeFromReferences(references))
: FindSuperTypeAsyncCore(typeId, ct);
async ValueTask FindSuperTypeAsyncCore(NodeId typeId, CancellationToken ct)
{
List references = await GetOrAddReferencesAsync(typeId, ct)
.ConfigureAwait(false);
return GetSuperTypeFromReferences(references);
}
}
///
public async ValueTask GetBuiltInTypeAsync(
NodeId datatypeId,
CancellationToken ct)
{
NodeId typeId = datatypeId;
while (!NodeId.IsNull(typeId))
{
if (typeId.NamespaceIndex == 0 && typeId.IdType == IdType.Numeric)
{
var id = (BuiltInType)(int)(uint)typeId.Identifier;
if (id is > BuiltInType.Null and <= BuiltInType.Enumeration and not BuiltInType.DiagnosticInfo)
{
return id;
}
}
typeId = await GetSuperTypeAsync(typeId, ct).ConfigureAwait(false);
}
return BuiltInType.Null;
}
///
public async ValueTask GetNodeWithBrowsePathAsync(
NodeId nodeId,
QualifiedNameCollection browsePath,
CancellationToken ct)
{
INode? found = null;
foreach (QualifiedName? browseName in browsePath)
{
found = null;
while (true)
{
if (NodeId.IsNull(nodeId))
{
// Nothing can be found since there is no
return null;
}
//
// Get all hierarchical references of the node and
// match browse name
//
IReadOnlyList references = await GetReferencesAsync(
nodeId,
ReferenceTypeIds.HierarchicalReferences,
false,
true,
ct)
.ConfigureAwait(false);
foreach (INode target in references)
{
if (target.BrowseName == browseName)
{
nodeId = ToNodeId(target.NodeId);
if (!NodeId.IsNull(nodeId))
{
found = target;
}
break;
}
}
if (found != null)
{
break;
}
// Try find name in super type
nodeId = await GetSuperTypeAsync(nodeId, ct).ConfigureAwait(false);
}
nodeId = ToNodeId(found.NodeId);
}
return found;
}
///
public void Clear()
{
m_nodes.Clear();
m_values.Clear();
m_refs.Clear();
}
///
/// Get or add references to cache
///
private ValueTask> GetOrAddReferencesAsync(
NodeId nodeId,
CancellationToken ct)
{
Debug.Assert(!NodeId.IsNull(nodeId));
return m_refs.GetOrAddAsync(
nodeId,
async (nodeId, context) =>
{
ReferenceDescriptionCollection references =
await context.ctx.FetchReferencesAsync(null, nodeId, context.ct)
.ConfigureAwait(false) ??
[];
foreach (ReferenceDescription? reference in references)
{
// transform absolute identifiers.
if (reference.NodeId?.IsAbsolute == true)
{
reference.NodeId = ExpandedNodeId.ToNodeId(
reference.NodeId,
context.ctx.NamespaceUris);
}
}
return references;
},
(ctx: m_context, ct));
}
///
/// Fetch remaining nodes not yet in the result list
///
private async ValueTask> FetchRemainingAsync(
List remainingIds,
List result,
CancellationToken ct)
{
Debug.Assert(result.Count(r => r == null) == remainingIds.Count);
// fetch nodes and references from server.
var localIds = new NodeIdCollection(remainingIds);
(IReadOnlyList? nodes, IReadOnlyList? readErrors) =
await m_context.FetchNodesAsync(null, localIds, ct: ct)
.ConfigureAwait(false);
Debug.Assert(nodes.Count == localIds.Count);
Debug.Assert(readErrors.Count == localIds.Count);
int resultMissingIndex = 0;
for (int index = 0; index < localIds.Count; index++)
{
if (!ServiceResult.IsBad(readErrors[index]))
{
m_nodes.AddOrUpdate(remainingIds[index], nodes[index]);
}
while (result[resultMissingIndex] != null)
{
resultMissingIndex++;
Debug.Assert(resultMissingIndex < result.Count);
}
result[resultMissingIndex] = nodes[index];
}
Debug.Assert(!result.Any(r => r == null)); // None now should be null
return result!;
}
///
/// Fetch remaining nodes not yet in the result list
///
private async ValueTask> FetchRemainingAsync(
List remainingIds,
List result,
CancellationToken ct)
{
Debug.Assert(result.Count(r => r == null) == remainingIds.Count);
// fetch nodes and references from server.
(IReadOnlyList? values, IReadOnlyList? readErrors) =
await m_context.FetchValuesAsync(null, remainingIds, ct: ct)
.ConfigureAwait(false);
Debug.Assert(values.Count == remainingIds.Count);
Debug.Assert(readErrors.Count == remainingIds.Count);
int resultMissingIndex = 0;
for (int index = 0; index < remainingIds.Count; index++)
{
if (ServiceResult.IsBad(readErrors[index]))
{
values[index].StatusCode = readErrors[index].StatusCode;
}
if (StatusCode.IsGood(values[index].StatusCode))
{
// Add to cache
m_values.AddOrUpdate(remainingIds[index], values[index]);
}
while (result[resultMissingIndex] != null)
{
resultMissingIndex++;
Debug.Assert(resultMissingIndex < result.Count);
}
result[resultMissingIndex] = values[index];
}
Debug.Assert(!result.Any(r => r == null)); // None now should be null
return result!;
}
///
/// Check whether type hierarchy is loaded
///
private bool IsTypeHierarchyLoaded(IEnumerable typeIds)
{
var types = new Queue(typeIds.Where(nodeId => !NodeId.IsNull(nodeId)));
while (types.TryDequeue(out NodeId? typeId))
{
if (!m_refs.TryGet(typeId, out List? references))
{
return false;
}
foreach (ReferenceDescription reference in references)
{
if (reference.ReferenceTypeId == ReferenceTypeIds.HasSubtype &&
reference.IsForward &&
!reference.NodeId.IsAbsolute)
{
types.Enqueue(ToNodeId(reference.NodeId));
}
}
}
return true;
}
///
/// Get supertype from references
///
private NodeId GetSuperTypeFromReferences(List references)
{
return references
.Where(r => !r.IsForward && r.ReferenceTypeId == ReferenceTypeIds.HasSubtype)
.Select(r => ExpandedNodeId.ToNodeId(r.NodeId, NamespaceUris))
.DefaultIfEmpty(NodeId.Null)
.First();
}
///
/// Convert to node id from expanded node id if the expanded node id
/// is not absolute. Otherwise return a null node id
///
private NodeId ToNodeId(ExpandedNodeId expandedNodeId)
{
return expandedNodeId.IsAbsolute
? NodeId.Null
: ExpandedNodeId.ToNodeId(expandedNodeId, NamespaceUris);
}
///
/// Node id comparer
///
internal class Comparers : IEqualityComparer, IEqualityComparer
{
///
/// Get singleton comparer
///
public static Comparers Instance { get; } = new Comparers();
///
public bool Equals(ExpandedNodeId? x, ExpandedNodeId? y)
{
return ReferenceEquals(x, y) || x == y;
}
///
public int GetHashCode(ExpandedNodeId obj)
{
return obj.GetHashCode();
}
///
public bool Equals(NodeId? x, NodeId? y)
{
return ReferenceEquals(x, y) || x == y;
}
///
public int GetHashCode(NodeId obj)
{
return (obj?.GetHashCode()) ?? 0;
}
}
private readonly IAsyncCache m_nodes;
private readonly IAsyncCache> m_refs;
private readonly IAsyncCache m_values;
private readonly INodeCacheContext m_context;
}
}
#endif