From 1205541eb355a20afda3dd1e896fa64458b7773d Mon Sep 17 00:00:00 2001 From: Roman Ettlinger Date: Fri, 28 Nov 2025 16:38:07 +0100 Subject: [PATCH 1/7] Make EndpointIncomingRequest a readonly struct --- .../Opc.Ua.Core/Stack/Server/EndpointBase.cs | 19 ++++++++----------- Stack/Opc.Ua.Core/Stack/Server/IServerBase.cs | 6 ------ Stack/Opc.Ua.Core/Stack/Server/ServerBase.cs | 2 +- 3 files changed, 9 insertions(+), 18 deletions(-) diff --git a/Stack/Opc.Ua.Core/Stack/Server/EndpointBase.cs b/Stack/Opc.Ua.Core/Stack/Server/EndpointBase.cs index 8eb69d3ec5..4fa994e4fb 100644 --- a/Stack/Opc.Ua.Core/Stack/Server/EndpointBase.cs +++ b/Stack/Opc.Ua.Core/Stack/Server/EndpointBase.cs @@ -1074,7 +1074,7 @@ .Body is AdditionalParametersType parameters && /// /// An object that handles an incoming request for an endpoint. /// - protected class EndpointIncomingRequest : IEndpointIncomingRequest + protected readonly struct EndpointIncomingRequest : IEndpointIncomingRequest { /// /// Initialize the Object with a Request @@ -1082,18 +1082,18 @@ protected class EndpointIncomingRequest : IEndpointIncomingRequest public EndpointIncomingRequest( EndpointBase endpoint, SecureChannelContext context, - IServiceRequest request) + IServiceRequest request, + CancellationToken cancellationToken = default) { m_endpoint = endpoint; SecureChannelContext = context; Request = request; m_tcs = new TaskCompletionSource( TaskCreationOptions.RunContinuationsAsynchronously); + m_service = m_endpoint.FindService(Request.TypeId); + m_cancellationToken = cancellationToken; } - /// - public object Calldata { get; set; } - /// public SecureChannelContext SecureChannelContext { get; } @@ -1108,10 +1108,7 @@ public Task ProcessAsync(CancellationToken cancellationToken = { try { - m_cancellationToken = cancellationToken; - m_cancellationToken.Register(() => m_tcs.TrySetCanceled()); - m_service = m_endpoint.FindService(Request.TypeId); - m_endpoint.ServerForContext.ScheduleIncomingRequest(this, m_cancellationToken); + m_endpoint.ServerForContext.ScheduleIncomingRequest(this, cancellationToken); } catch (Exception e) { @@ -1185,9 +1182,9 @@ public void OperationCompleted(IServiceResponse response, ServiceResult error) } private readonly EndpointBase m_endpoint; - private CancellationToken m_cancellationToken; - private ServiceDefinition m_service; + private readonly ServiceDefinition m_service; private readonly TaskCompletionSource m_tcs; + private readonly CancellationToken m_cancellationToken; } /// diff --git a/Stack/Opc.Ua.Core/Stack/Server/IServerBase.cs b/Stack/Opc.Ua.Core/Stack/Server/IServerBase.cs index e8ff28d566..f18045f654 100644 --- a/Stack/Opc.Ua.Core/Stack/Server/IServerBase.cs +++ b/Stack/Opc.Ua.Core/Stack/Server/IServerBase.cs @@ -105,12 +105,6 @@ public interface IEndpointIncomingRequest /// The secure channel context. SecureChannelContext SecureChannelContext { get; } - /// - /// Gets or sets the call data associated with the request. - /// - /// The call data. - object Calldata { get; set; } - /// /// Used to call the default asynchronous handler. /// diff --git a/Stack/Opc.Ua.Core/Stack/Server/ServerBase.cs b/Stack/Opc.Ua.Core/Stack/Server/ServerBase.cs index ed27873a2c..483585a62b 100644 --- a/Stack/Opc.Ua.Core/Stack/Server/ServerBase.cs +++ b/Stack/Opc.Ua.Core/Stack/Server/ServerBase.cs @@ -1611,7 +1611,7 @@ protected virtual void Dispose(bool disposing) foreach (IEndpointIncomingRequest request in m_queue.ToList()) { - Utils.SilentDispose(request); + request.OperationCompleted(null, StatusCodes.BadServerHalted); } #if NETSTANDARD2_1_OR_GREATER m_queue.Clear(); From 5c39d0efdee08ba61b9f48c40f05bcaa560eae49 Mon Sep 17 00:00:00 2001 From: Roman Ettlinger Date: Fri, 28 Nov 2025 16:38:23 +0100 Subject: [PATCH 2/7] remove asyncresultbase as now broken --- Stack/Opc.Ua.Core/Opc.Ua.Core.csproj | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Stack/Opc.Ua.Core/Opc.Ua.Core.csproj b/Stack/Opc.Ua.Core/Opc.Ua.Core.csproj index f413814efa..151aafe477 100644 --- a/Stack/Opc.Ua.Core/Opc.Ua.Core.csproj +++ b/Stack/Opc.Ua.Core/Opc.Ua.Core.csproj @@ -1,7 +1,7 @@  $(DefineConstants);NET_STANDARD;NET_STANDARD_ASYNC;NET_STANDARD_OBSOLETE_SYNC;NET_STANDARD_OBSOLETE_APM - $(DefineConstants);OPCUA_INCLUDE_ASYNC; + $(DefineConstants);OPCUA_INCLUDE_ASYNC;NET_STANDARD_NO_APM $(DefineConstants);OPCUA_EXCLUDE_AccessRestrictionType ;OPCUA_EXCLUDE_ReferenceDescription; OPCUA_EXCLUDE_ReferenceDescriptionCollection; OPCUA_EXCLUDE_AttributeWriteMask; OPCUA_EXCLUDE_Argument; OPCUA_EXCLUDE_ArgumentCollection; OPCUA_EXCLUDE_IdType; OPCUA_EXCLUDE_IdTypeCollection; OPCUA_EXCLUDE_RolePermissionType; OPCUA_EXCLUDE_RolePermissionTypeCollection; OPCUA_EXCLUDE_PermissionType; OPCUA_EXCLUDE_ViewDescription; OPCUA_EXCLUDE_BrowseDescription; OPCUA_EXCLUDE_BrowseDescriptionCollection; OPCUA_EXCLUDE_StructureDefinition; OPCUA_EXCLUDE_StructureType; OPCUA_EXCLUDE_StructureField; OPCUA_EXCLUDE_StructureFieldCollection; OPCUA_EXCLUDE_InstanceNode; OPCUA_EXCLUDE_ReferenceTypeNode; OPCUA_EXCLUDE_ReferenceNode; OPCUA_EXCLUDE_ReferenceNodeCollection; OPCUA_EXCLUDE_DataTypeDefinition; OPCUA_EXCLUDE_EnumDefinition; OPCUA_EXCLUDE_EnumField; OPCUA_EXCLUDE_EnumFieldCollection; OPCUA_EXCLUDE_EnumValueType; OPCUA_EXCLUDE_EnumValueTypeCollection; OPCUA_EXCLUDE_RelativePath; OPCUA_EXCLUDE_BrowseDirection; OPCUA_EXCLUDE_RelativePathElement; OPCUA_EXCLUDE_RelativePathElementCollection; OPCUA_EXCLUDE_NodeClass; OPCUA_EXCLUDE_Node; OPCUA_EXCLUDE_ViewNode; OPCUA_EXCLUDE_ObjectNode; OPCUA_EXCLUDE_MethodNode; OPCUA_EXCLUDE_TypeNode; OPCUA_EXCLUDE_ObjectTypeNode; OPCUA_EXCLUDE_DataTypeNode; OPCUA_EXCLUDE_VariableTypeNode; OPCUA_EXCLUDE_VariableNode $(LibCoreTargetFrameworks) $(AssemblyPrefix).Core From 5f00e731bb9352b0008eede9339a56c13e623962 Mon Sep 17 00:00:00 2001 From: Roman Ettlinger Date: Fri, 28 Nov 2025 17:32:30 +0100 Subject: [PATCH 3/7] Switch to ValueTask & pool ValueTasks using ValueTaskSource --- Stack/Opc.Ua.Core/Opc.Ua.Core.csproj | 2 +- .../Opc.Ua.Core/Stack/Server/EndpointBase.cs | 51 +++++++---- Stack/Opc.Ua.Core/Stack/Server/IServerBase.cs | 2 +- .../Transport/ITransportListenerCallback.cs | 2 +- .../Types/Utils/ManualResetValueTaskSource.cs | 43 ++++++++++ Stack/Opc.Ua.Core/Types/Utils/ObjectPool.cs | 84 +++++++++++++++++++ 6 files changed, 164 insertions(+), 20 deletions(-) create mode 100644 Stack/Opc.Ua.Core/Types/Utils/ManualResetValueTaskSource.cs create mode 100644 Stack/Opc.Ua.Core/Types/Utils/ObjectPool.cs diff --git a/Stack/Opc.Ua.Core/Opc.Ua.Core.csproj b/Stack/Opc.Ua.Core/Opc.Ua.Core.csproj index 151aafe477..c90f0b6ebc 100644 --- a/Stack/Opc.Ua.Core/Opc.Ua.Core.csproj +++ b/Stack/Opc.Ua.Core/Opc.Ua.Core.csproj @@ -1,7 +1,7 @@  $(DefineConstants);NET_STANDARD;NET_STANDARD_ASYNC;NET_STANDARD_OBSOLETE_SYNC;NET_STANDARD_OBSOLETE_APM - $(DefineConstants);OPCUA_INCLUDE_ASYNC;NET_STANDARD_NO_APM + $(DefineConstants);OPCUA_INCLUDE_ASYNC $(DefineConstants);OPCUA_EXCLUDE_AccessRestrictionType ;OPCUA_EXCLUDE_ReferenceDescription; OPCUA_EXCLUDE_ReferenceDescriptionCollection; OPCUA_EXCLUDE_AttributeWriteMask; OPCUA_EXCLUDE_Argument; OPCUA_EXCLUDE_ArgumentCollection; OPCUA_EXCLUDE_IdType; OPCUA_EXCLUDE_IdTypeCollection; OPCUA_EXCLUDE_RolePermissionType; OPCUA_EXCLUDE_RolePermissionTypeCollection; OPCUA_EXCLUDE_PermissionType; OPCUA_EXCLUDE_ViewDescription; OPCUA_EXCLUDE_BrowseDescription; OPCUA_EXCLUDE_BrowseDescriptionCollection; OPCUA_EXCLUDE_StructureDefinition; OPCUA_EXCLUDE_StructureType; OPCUA_EXCLUDE_StructureField; OPCUA_EXCLUDE_StructureFieldCollection; OPCUA_EXCLUDE_InstanceNode; OPCUA_EXCLUDE_ReferenceTypeNode; OPCUA_EXCLUDE_ReferenceNode; OPCUA_EXCLUDE_ReferenceNodeCollection; OPCUA_EXCLUDE_DataTypeDefinition; OPCUA_EXCLUDE_EnumDefinition; OPCUA_EXCLUDE_EnumField; OPCUA_EXCLUDE_EnumFieldCollection; OPCUA_EXCLUDE_EnumValueType; OPCUA_EXCLUDE_EnumValueTypeCollection; OPCUA_EXCLUDE_RelativePath; OPCUA_EXCLUDE_BrowseDirection; OPCUA_EXCLUDE_RelativePathElement; OPCUA_EXCLUDE_RelativePathElementCollection; OPCUA_EXCLUDE_NodeClass; OPCUA_EXCLUDE_Node; OPCUA_EXCLUDE_ViewNode; OPCUA_EXCLUDE_ObjectNode; OPCUA_EXCLUDE_MethodNode; OPCUA_EXCLUDE_TypeNode; OPCUA_EXCLUDE_ObjectTypeNode; OPCUA_EXCLUDE_DataTypeNode; OPCUA_EXCLUDE_VariableTypeNode; OPCUA_EXCLUDE_VariableNode $(LibCoreTargetFrameworks) $(AssemblyPrefix).Core diff --git a/Stack/Opc.Ua.Core/Stack/Server/EndpointBase.cs b/Stack/Opc.Ua.Core/Stack/Server/EndpointBase.cs index 4fa994e4fb..a7dbba273d 100644 --- a/Stack/Opc.Ua.Core/Stack/Server/EndpointBase.cs +++ b/Stack/Opc.Ua.Core/Stack/Server/EndpointBase.cs @@ -11,6 +11,7 @@ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. */ using System; +using System.Collections.Concurrent; using System.Collections.Generic; using System.Diagnostics; using System.Security.Cryptography.X509Certificates; @@ -76,7 +77,7 @@ protected EndpointBase(ServerBase server) } /// - public Task ProcessRequestAsync( + public ValueTask ProcessRequestAsync( SecureChannelContext secureChannelContext, IServiceRequest request, CancellationToken cancellationToken = default) @@ -777,7 +778,7 @@ public void CallSynchronously() /// thread that calls IServerBase.ScheduleIncomingRequest(). /// This method always traps any exceptions and reports them to the client as a fault. /// - public async Task CallAsync(CancellationToken cancellationToken = default) + public async ValueTask CallAsync(CancellationToken cancellationToken = default) { await OnProcessRequestAsync(null, cancellationToken).ConfigureAwait(false); } @@ -1088,8 +1089,8 @@ public EndpointIncomingRequest( m_endpoint = endpoint; SecureChannelContext = context; Request = request; - m_tcs = new TaskCompletionSource( - TaskCreationOptions.RunContinuationsAsynchronously); + m_vts = s_vtsPool.Get(); + m_vts.Reset(); m_service = m_endpoint.FindService(Request.TypeId); m_cancellationToken = cancellationToken; } @@ -1104,7 +1105,7 @@ public EndpointIncomingRequest( /// Process an incoming request /// /// - public Task ProcessAsync(CancellationToken cancellationToken = default) + public ValueTask ProcessAsync(CancellationToken cancellationToken = default) { try { @@ -1112,14 +1113,14 @@ public Task ProcessAsync(CancellationToken cancellationToken = } catch (Exception e) { - m_tcs.TrySetResult(m_endpoint.CreateFault(Request, e)); + m_vts.SetResult(m_endpoint.CreateFault(Request, e)); } - return m_tcs.Task; + return m_vts.Task; } /// - public async Task CallAsync(CancellationToken cancellationToken = default) + public async ValueTask CallAsync(CancellationToken cancellationToken = default) { using CancellationTokenSource timeoutHintCts = (int)Request.RequestHeader.TimeoutHint > 0 ? new CancellationTokenSource((int)Request.RequestHeader.TimeoutHint) : null; @@ -1154,37 +1155,53 @@ .Body is AdditionalParametersType parameters && using (activity) { IServiceResponse response = await m_service.InvokeAsync(Request, SecureChannelContext, linkedCts.Token).ConfigureAwait(false); - m_tcs.TrySetResult(response); + m_vts.SetResult(response); } } catch (Exception e) { if (e is OperationCanceledException) { - e = new ServiceResultException(StatusCodes.BadTimeout); + if (timeoutHintCts?.IsCancellationRequested == true || + m_cancellationToken.IsCancellationRequested) + { + e = new ServiceResultException(StatusCodes.BadTimeout); + } } - - m_tcs.TrySetResult(m_endpoint.CreateFault(Request, e)); + m_vts.SetResult(m_endpoint.CreateFault(Request, e)); + } + finally + { + s_vtsPool.Return(m_vts); } } /// public void OperationCompleted(IServiceResponse response, ServiceResult error) { - if (ServiceResult.IsBad(error)) + try { - m_tcs.TrySetResult(m_endpoint.CreateFault(Request, new ServiceResultException(error))); + if (ServiceResult.IsBad(error)) + { + m_vts.SetResult(m_endpoint.CreateFault(Request, new ServiceResultException(error))); + } + else + { + m_vts.SetResult(response); + } } - else + finally { - m_tcs.TrySetResult(response); + s_vtsPool.Return(m_vts); } } private readonly EndpointBase m_endpoint; private readonly ServiceDefinition m_service; - private readonly TaskCompletionSource m_tcs; + private readonly ManualResetValueTaskSource m_vts; private readonly CancellationToken m_cancellationToken; + private static readonly ObjectPool> s_vtsPool = + new(() => new ManualResetValueTaskSource(), 100); } /// diff --git a/Stack/Opc.Ua.Core/Stack/Server/IServerBase.cs b/Stack/Opc.Ua.Core/Stack/Server/IServerBase.cs index f18045f654..71e97eeb99 100644 --- a/Stack/Opc.Ua.Core/Stack/Server/IServerBase.cs +++ b/Stack/Opc.Ua.Core/Stack/Server/IServerBase.cs @@ -113,7 +113,7 @@ public interface IEndpointIncomingRequest /// thread that calls IServerBase.ScheduleIncomingRequest(). /// This method always traps any exceptions and reports them to the client as a fault. /// - Task CallAsync(CancellationToken cancellationToken = default); + ValueTask CallAsync(CancellationToken cancellationToken = default); /// /// Used to indicate that the asynchronous operation has completed. diff --git a/Stack/Opc.Ua.Core/Stack/Transport/ITransportListenerCallback.cs b/Stack/Opc.Ua.Core/Stack/Transport/ITransportListenerCallback.cs index 8c4a7bf658..0311ee60b4 100644 --- a/Stack/Opc.Ua.Core/Stack/Transport/ITransportListenerCallback.cs +++ b/Stack/Opc.Ua.Core/Stack/Transport/ITransportListenerCallback.cs @@ -28,7 +28,7 @@ public interface ITransportListenerCallback : IAuditEventCallback /// The incoming request. /// The cancellation token. /// The response to return over the secure channel. - Task ProcessRequestAsync( + ValueTask ProcessRequestAsync( SecureChannelContext secureChannelContext, IServiceRequest request, CancellationToken cancellationToken = default); diff --git a/Stack/Opc.Ua.Core/Types/Utils/ManualResetValueTaskSource.cs b/Stack/Opc.Ua.Core/Types/Utils/ManualResetValueTaskSource.cs new file mode 100644 index 0000000000..19ef959462 --- /dev/null +++ b/Stack/Opc.Ua.Core/Types/Utils/ManualResetValueTaskSource.cs @@ -0,0 +1,43 @@ +/* Copyright (c) 1996-2022 The OPC Foundation. All rights reserved. + The source code in this file is covered under a dual-license scenario: + - RCL: for OPC Foundation Corporate Members in good-standing + - GPL V2: everybody else + RCL license terms accompanied with this source code. See http://opcfoundation.org/License/RCL/1.00/ + GNU General Public License as published by the Free Software Foundation; + version 2 of the License are accompanied with this source code. See http://opcfoundation.org/License/GPLv2 + This source code is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. +*/ + +using System; +using System.Threading; +using System.Threading.Tasks; +using System.Threading.Tasks.Sources; + +namespace Opc.Ua +{ + /// + /// A reusable value task source. + /// + /// + internal class ManualResetValueTaskSource : IValueTaskSource, IValueTaskSource + { + private ManualResetValueTaskSourceCore m_core; + + public bool RunContinuationsAsynchronously { get => m_core.RunContinuationsAsynchronously; set => m_core.RunContinuationsAsynchronously = value; } + public short Version => m_core.Version; + public void Reset() => m_core.Reset(); + public void SetResult(T result) => m_core.SetResult(result); + public void SetException(Exception error) => m_core.SetException(error); + + public T GetResult(short token) => m_core.GetResult(token); + void IValueTaskSource.GetResult(short token) => m_core.GetResult(token); + public ValueTaskSourceStatus GetStatus(short token) => m_core.GetStatus(token); + public void OnCompleted(Action continuation, object state, short token, ValueTaskSourceOnCompletedFlags flags) + => m_core.OnCompleted(continuation, state, token, flags); + + public ValueTask Task => new ValueTask(this, m_core.Version); + public ValueTask SourceTask => new ValueTask(this, m_core.Version); + } +} diff --git a/Stack/Opc.Ua.Core/Types/Utils/ObjectPool.cs b/Stack/Opc.Ua.Core/Types/Utils/ObjectPool.cs new file mode 100644 index 0000000000..43dd4a4d76 --- /dev/null +++ b/Stack/Opc.Ua.Core/Types/Utils/ObjectPool.cs @@ -0,0 +1,84 @@ +/* ======================================================================== + * Copyright (c) 2005-2024 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.Collections.Concurrent; + +namespace Opc.Ua +{ + /// + /// A simple object pool implementation. + /// + /// The type of object to pool. + internal class ObjectPool where T : class + { + private readonly ConcurrentBag m_objects; + private readonly Func m_objectGenerator; + private readonly int m_maxSize; + + /// + /// Initializes a new instance of the class. + /// + /// The function to generate new objects. + /// The maximum size of the pool. + public ObjectPool(Func objectGenerator, int maxSize) + { + m_objectGenerator = objectGenerator ?? throw new ArgumentNullException(nameof(objectGenerator)); + m_maxSize = maxSize > 0 ? maxSize : throw new ArgumentOutOfRangeException(nameof(maxSize)); + m_objects = new ConcurrentBag(); + } + + /// + /// Gets an object from the pool. + /// + /// An object from the pool or a new one if the pool is empty. + public T Get() + { + if (m_objects.TryTake(out T item)) + { + return item; + } + + return m_objectGenerator(); + } + + /// + /// Returns an object to the pool. + /// + /// The object to return. + public void Return(T item) + { + if (m_objects.Count < m_maxSize) + { + m_objects.Add(item); + } + } + } +} From 99d7b6019acd3c57232c17d8ac70567f5b20fb42 Mon Sep 17 00:00:00 2001 From: Roman Ettlinger Date: Fri, 28 Nov 2025 17:38:36 +0100 Subject: [PATCH 4/7] cleanup --- Stack/Opc.Ua.Core/Opc.Ua.Core.csproj | 2 +- .../Opc.Ua.Core/Stack/Server/EndpointBase.cs | 8 +--- Stack/Opc.Ua.Core/Types/Utils/ObjectPool.cs | 40 +++++-------------- 3 files changed, 14 insertions(+), 36 deletions(-) diff --git a/Stack/Opc.Ua.Core/Opc.Ua.Core.csproj b/Stack/Opc.Ua.Core/Opc.Ua.Core.csproj index c90f0b6ebc..f413814efa 100644 --- a/Stack/Opc.Ua.Core/Opc.Ua.Core.csproj +++ b/Stack/Opc.Ua.Core/Opc.Ua.Core.csproj @@ -1,7 +1,7 @@  $(DefineConstants);NET_STANDARD;NET_STANDARD_ASYNC;NET_STANDARD_OBSOLETE_SYNC;NET_STANDARD_OBSOLETE_APM - $(DefineConstants);OPCUA_INCLUDE_ASYNC + $(DefineConstants);OPCUA_INCLUDE_ASYNC; $(DefineConstants);OPCUA_EXCLUDE_AccessRestrictionType ;OPCUA_EXCLUDE_ReferenceDescription; OPCUA_EXCLUDE_ReferenceDescriptionCollection; OPCUA_EXCLUDE_AttributeWriteMask; OPCUA_EXCLUDE_Argument; OPCUA_EXCLUDE_ArgumentCollection; OPCUA_EXCLUDE_IdType; OPCUA_EXCLUDE_IdTypeCollection; OPCUA_EXCLUDE_RolePermissionType; OPCUA_EXCLUDE_RolePermissionTypeCollection; OPCUA_EXCLUDE_PermissionType; OPCUA_EXCLUDE_ViewDescription; OPCUA_EXCLUDE_BrowseDescription; OPCUA_EXCLUDE_BrowseDescriptionCollection; OPCUA_EXCLUDE_StructureDefinition; OPCUA_EXCLUDE_StructureType; OPCUA_EXCLUDE_StructureField; OPCUA_EXCLUDE_StructureFieldCollection; OPCUA_EXCLUDE_InstanceNode; OPCUA_EXCLUDE_ReferenceTypeNode; OPCUA_EXCLUDE_ReferenceNode; OPCUA_EXCLUDE_ReferenceNodeCollection; OPCUA_EXCLUDE_DataTypeDefinition; OPCUA_EXCLUDE_EnumDefinition; OPCUA_EXCLUDE_EnumField; OPCUA_EXCLUDE_EnumFieldCollection; OPCUA_EXCLUDE_EnumValueType; OPCUA_EXCLUDE_EnumValueTypeCollection; OPCUA_EXCLUDE_RelativePath; OPCUA_EXCLUDE_BrowseDirection; OPCUA_EXCLUDE_RelativePathElement; OPCUA_EXCLUDE_RelativePathElementCollection; OPCUA_EXCLUDE_NodeClass; OPCUA_EXCLUDE_Node; OPCUA_EXCLUDE_ViewNode; OPCUA_EXCLUDE_ObjectNode; OPCUA_EXCLUDE_MethodNode; OPCUA_EXCLUDE_TypeNode; OPCUA_EXCLUDE_ObjectTypeNode; OPCUA_EXCLUDE_DataTypeNode; OPCUA_EXCLUDE_VariableTypeNode; OPCUA_EXCLUDE_VariableNode $(LibCoreTargetFrameworks) $(AssemblyPrefix).Core diff --git a/Stack/Opc.Ua.Core/Stack/Server/EndpointBase.cs b/Stack/Opc.Ua.Core/Stack/Server/EndpointBase.cs index a7dbba273d..2a5226381d 100644 --- a/Stack/Opc.Ua.Core/Stack/Server/EndpointBase.cs +++ b/Stack/Opc.Ua.Core/Stack/Server/EndpointBase.cs @@ -1043,7 +1043,7 @@ .Body is AdditionalParametersType parameters && else { // call the service even when there is no trace information - m_response = await m_service.InvokeAsync(Request,SecureChannelContext, cancellationToken) + m_response = await m_service.InvokeAsync(Request, SecureChannelContext, cancellationToken) .ConfigureAwait(false); } } @@ -1162,11 +1162,7 @@ .Body is AdditionalParametersType parameters && { if (e is OperationCanceledException) { - if (timeoutHintCts?.IsCancellationRequested == true || - m_cancellationToken.IsCancellationRequested) - { - e = new ServiceResultException(StatusCodes.BadTimeout); - } + e = new ServiceResultException(StatusCodes.BadTimeout); } m_vts.SetResult(m_endpoint.CreateFault(Request, e)); } diff --git a/Stack/Opc.Ua.Core/Types/Utils/ObjectPool.cs b/Stack/Opc.Ua.Core/Types/Utils/ObjectPool.cs index 43dd4a4d76..70edec9004 100644 --- a/Stack/Opc.Ua.Core/Types/Utils/ObjectPool.cs +++ b/Stack/Opc.Ua.Core/Types/Utils/ObjectPool.cs @@ -1,32 +1,14 @@ -/* ======================================================================== - * Copyright (c) 2005-2024 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/ - * ======================================================================*/ +/* Copyright (c) 1996-2022 The OPC Foundation. All rights reserved. + The source code in this file is covered under a dual-license scenario: + - RCL: for OPC Foundation Corporate Members in good-standing + - GPL V2: everybody else + RCL license terms accompanied with this source code. See http://opcfoundation.org/License/RCL/1.00/ + GNU General Public License as published by the Free Software Foundation; + version 2 of the License are accompanied with this source code. See http://opcfoundation.org/License/GPLv2 + This source code is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. +*/ using System; using System.Collections.Concurrent; From d100546e6b025b7d5f3cbabbf5e7e55639feb7b3 Mon Sep 17 00:00:00 2001 From: Roman Ettlinger Date: Sun, 30 Nov 2025 18:44:40 +0100 Subject: [PATCH 5/7] Use specific ValueTaskSource with integrated pooling to ensure concurrent behaviour --- .../Opc.Ua.Core/Stack/Server/EndpointBase.cs | 28 +--- .../ManualResetValueTaskSource.cs | 52 +++++-- .../Types/Utils/{ => ValueTask}/ObjectPool.cs | 0 .../ServiceResponsePooledValueTaskSource.cs | 128 ++++++++++++++++++ 4 files changed, 176 insertions(+), 32 deletions(-) rename Stack/Opc.Ua.Core/Types/Utils/{ => ValueTask}/ManualResetValueTaskSource.cs (55%) rename Stack/Opc.Ua.Core/Types/Utils/{ => ValueTask}/ObjectPool.cs (100%) create mode 100644 Stack/Opc.Ua.Core/Types/Utils/ValueTask/ServiceResponsePooledValueTaskSource.cs diff --git a/Stack/Opc.Ua.Core/Stack/Server/EndpointBase.cs b/Stack/Opc.Ua.Core/Stack/Server/EndpointBase.cs index 2a5226381d..8fdb226c5a 100644 --- a/Stack/Opc.Ua.Core/Stack/Server/EndpointBase.cs +++ b/Stack/Opc.Ua.Core/Stack/Server/EndpointBase.cs @@ -11,9 +11,9 @@ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. */ using System; -using System.Collections.Concurrent; using System.Collections.Generic; using System.Diagnostics; +using System.Diagnostics.CodeAnalysis; using System.Security.Cryptography.X509Certificates; using System.Threading; using System.Threading.Tasks; @@ -1089,8 +1089,7 @@ public EndpointIncomingRequest( m_endpoint = endpoint; SecureChannelContext = context; Request = request; - m_vts = s_vtsPool.Get(); - m_vts.Reset(); + m_vts = ServiceResponsePooledValueTaskSource.Create(); m_service = m_endpoint.FindService(Request.TypeId); m_cancellationToken = cancellationToken; } @@ -1166,38 +1165,25 @@ .Body is AdditionalParametersType parameters && } m_vts.SetResult(m_endpoint.CreateFault(Request, e)); } - finally - { - s_vtsPool.Return(m_vts); - } } /// public void OperationCompleted(IServiceResponse response, ServiceResult error) { - try + if (ServiceResult.IsBad(error)) { - if (ServiceResult.IsBad(error)) - { - m_vts.SetResult(m_endpoint.CreateFault(Request, new ServiceResultException(error))); - } - else - { - m_vts.SetResult(response); - } + m_vts.SetResult(m_endpoint.CreateFault(Request, new ServiceResultException(error))); } - finally + else { - s_vtsPool.Return(m_vts); + m_vts.SetResult(response); } } private readonly EndpointBase m_endpoint; private readonly ServiceDefinition m_service; - private readonly ManualResetValueTaskSource m_vts; + private readonly ServiceResponsePooledValueTaskSource m_vts; private readonly CancellationToken m_cancellationToken; - private static readonly ObjectPool> s_vtsPool = - new(() => new ManualResetValueTaskSource(), 100); } /// diff --git a/Stack/Opc.Ua.Core/Types/Utils/ManualResetValueTaskSource.cs b/Stack/Opc.Ua.Core/Types/Utils/ValueTask/ManualResetValueTaskSource.cs similarity index 55% rename from Stack/Opc.Ua.Core/Types/Utils/ManualResetValueTaskSource.cs rename to Stack/Opc.Ua.Core/Types/Utils/ValueTask/ManualResetValueTaskSource.cs index 19ef959462..dc621bd2b7 100644 --- a/Stack/Opc.Ua.Core/Types/Utils/ManualResetValueTaskSource.cs +++ b/Stack/Opc.Ua.Core/Types/Utils/ValueTask/ManualResetValueTaskSource.cs @@ -11,7 +11,6 @@ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. */ using System; -using System.Threading; using System.Threading.Tasks; using System.Threading.Tasks.Sources; @@ -25,19 +24,50 @@ internal class ManualResetValueTaskSource : IValueTaskSource, IValueTaskSo { private ManualResetValueTaskSourceCore m_core; - public bool RunContinuationsAsynchronously { get => m_core.RunContinuationsAsynchronously; set => m_core.RunContinuationsAsynchronously = value; } + public bool RunContinuationsAsynchronously + { + get => m_core.RunContinuationsAsynchronously; + set => m_core.RunContinuationsAsynchronously = value; + } + public short Version => m_core.Version; - public void Reset() => m_core.Reset(); - public void SetResult(T result) => m_core.SetResult(result); - public void SetException(Exception error) => m_core.SetException(error); - public T GetResult(short token) => m_core.GetResult(token); - void IValueTaskSource.GetResult(short token) => m_core.GetResult(token); - public ValueTaskSourceStatus GetStatus(short token) => m_core.GetStatus(token); + public void Reset() + { + m_core.Reset(); + } + + public void SetResult(T result) + { + m_core.SetResult(result); + } + + public void SetException(Exception error) + { + m_core.SetException(error); + } + + public T GetResult(short token) + { + return m_core.GetResult(token); + } + + void IValueTaskSource.GetResult(short token) + { + m_core.GetResult(token); + } + + public ValueTaskSourceStatus GetStatus(short token) + { + return m_core.GetStatus(token); + } + public void OnCompleted(Action continuation, object state, short token, ValueTaskSourceOnCompletedFlags flags) - => m_core.OnCompleted(continuation, state, token, flags); + { + m_core.OnCompleted(continuation, state, token, flags); + } - public ValueTask Task => new ValueTask(this, m_core.Version); - public ValueTask SourceTask => new ValueTask(this, m_core.Version); + public ValueTask Task => new(this, m_core.Version); + public ValueTask SourceTask => new(this, m_core.Version); } } diff --git a/Stack/Opc.Ua.Core/Types/Utils/ObjectPool.cs b/Stack/Opc.Ua.Core/Types/Utils/ValueTask/ObjectPool.cs similarity index 100% rename from Stack/Opc.Ua.Core/Types/Utils/ObjectPool.cs rename to Stack/Opc.Ua.Core/Types/Utils/ValueTask/ObjectPool.cs diff --git a/Stack/Opc.Ua.Core/Types/Utils/ValueTask/ServiceResponsePooledValueTaskSource.cs b/Stack/Opc.Ua.Core/Types/Utils/ValueTask/ServiceResponsePooledValueTaskSource.cs new file mode 100644 index 0000000000..68f85b8724 --- /dev/null +++ b/Stack/Opc.Ua.Core/Types/Utils/ValueTask/ServiceResponsePooledValueTaskSource.cs @@ -0,0 +1,128 @@ +/* Copyright (c) 1996-2022 The OPC Foundation. All rights reserved. + The source code in this file is covered under a dual-license scenario: + - RCL: for OPC Foundation Corporate Members in good-standing + - GPL V2: everybody else + RCL license terms accompanied with this source code. See http://opcfoundation.org/License/RCL/1.00/ + GNU General Public License as published by the Free Software Foundation; + version 2 of the License are accompanied with this source code. See http://opcfoundation.org/License/GPLv2 + This source code is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. +*/ + +using System; +using System.Threading; +using System.Threading.Tasks; +using System.Threading.Tasks.Sources; + +namespace Opc.Ua +{ + /// + /// A pooled value task source for IServiceResponse. + /// + internal sealed class ServiceResponsePooledValueTaskSource : IValueTaskSource, IValueTaskSource + { + private static readonly ObjectPool s_pool = + new(() => new ServiceResponsePooledValueTaskSource(), 1024); + + private readonly ManualResetValueTaskSource m_source; + private int m_resultRetrieved; + + /// + /// Private constructor to enforce pooling. + /// + private ServiceResponsePooledValueTaskSource() + { + m_source = new ManualResetValueTaskSource(); + } + + /// + /// Creates or gets a pooled instance. + /// + public static ServiceResponsePooledValueTaskSource Create() + { + ServiceResponsePooledValueTaskSource source = s_pool.Get(); + source.m_resultRetrieved = 0; + return source; + } + + /// + /// Returns the object to the pool. + /// + private void ReturnToPool() + { + if (Interlocked.CompareExchange(ref m_resultRetrieved, 1, 0) == 0) + { + m_source.Reset(); + s_pool.Return(this); + } + } + + /// + /// The value task to await. + /// + public ValueTask Task => new(this, Version); + + /// + /// The value task to await. + /// + public ValueTask SourceTask => new(this, Version); + + /// + public short Version => m_source.Version; + + /// + /// Set the result of the task. + /// + public void SetResult(IServiceResponse result) + { + m_source.SetResult(result); + } + + /// + /// Set an exception for the task. + /// + public void SetException(Exception error) + { + m_source.SetException(error); + } + + /// + public IServiceResponse GetResult(short token) + { + try + { + return m_source.GetResult(token); + } + finally + { + ReturnToPool(); + } + } + + /// + void IValueTaskSource.GetResult(short token) + { + try + { + ((IValueTaskSource)m_source).GetResult(token); + } + finally + { + ReturnToPool(); + } + } + + /// + public ValueTaskSourceStatus GetStatus(short token) + { + return m_source.GetStatus(token); + } + + /// + public void OnCompleted(Action continuation, object state, short token, ValueTaskSourceOnCompletedFlags flags) + { + m_source.OnCompleted(continuation, state, token, flags); + } + } +} From 548671446e6fa116419fb6b53bb7a6c049a45f4b Mon Sep 17 00:00:00 2001 From: Roman Ettlinger Date: Tue, 2 Dec 2025 06:42:57 +0100 Subject: [PATCH 6/7] update --- .../Opc.Ua.Core/Stack/Server/EndpointBase.cs | 38 ++++++++++++++++++- 1 file changed, 36 insertions(+), 2 deletions(-) diff --git a/Stack/Opc.Ua.Core/Stack/Server/EndpointBase.cs b/Stack/Opc.Ua.Core/Stack/Server/EndpointBase.cs index 8fdb226c5a..1ca8b81dc9 100644 --- a/Stack/Opc.Ua.Core/Stack/Server/EndpointBase.cs +++ b/Stack/Opc.Ua.Core/Stack/Server/EndpointBase.cs @@ -671,7 +671,7 @@ public IServiceResponse Invoke(IServiceRequest request, SecureChannelContext sec { logger.LogWarning( "Async Service invoced sychronously. Prefer using InvokeAsync for best performance."); - return InvokeAsync(request, null).GetAwaiter().GetResult(); + return InvokeAsync(request, secureChannelContext).GetAwaiter().GetResult(); } return m_invokeService?.Invoke(request, secureChannelContext); } @@ -1075,7 +1075,7 @@ .Body is AdditionalParametersType parameters && /// /// An object that handles an incoming request for an endpoint. /// - protected readonly struct EndpointIncomingRequest : IEndpointIncomingRequest + protected readonly struct EndpointIncomingRequest : IEndpointIncomingRequest, IEquatable { /// /// Initialize the Object with a Request @@ -1180,6 +1180,40 @@ public void OperationCompleted(IServiceResponse response, ServiceResult error) } } + /// + public override bool Equals(object obj) + { + if (obj is EndpointIncomingRequest other) + { + return Request.RequestHeader.Equals(other.Request.RequestHeader); + } + return false; + } + + /// + public override int GetHashCode() + { + return Request.RequestHeader.GetHashCode(); + } + + /// + public static bool operator ==(EndpointIncomingRequest left, EndpointIncomingRequest right) + { + return left.Equals(right); + } + + /// + public static bool operator !=(EndpointIncomingRequest left, EndpointIncomingRequest right) + { + return !(left == right); + } + + /// + public bool Equals(EndpointIncomingRequest other) + { + return Request.RequestHeader.Equals(other.Request.RequestHeader); + } + private readonly EndpointBase m_endpoint; private readonly ServiceDefinition m_service; private readonly ServiceResponsePooledValueTaskSource m_vts; From 247e1c05aaef7806883701ee2b3f9d89f6aedb9f Mon Sep 17 00:00:00 2001 From: Roman Ettlinger Date: Tue, 2 Dec 2025 06:44:19 +0100 Subject: [PATCH 7/7] improve load test stability --- Tests/Opc.Ua.Client.Tests/LoadTest.cs | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/Tests/Opc.Ua.Client.Tests/LoadTest.cs b/Tests/Opc.Ua.Client.Tests/LoadTest.cs index 56eb7742fd..b0117f6af4 100644 --- a/Tests/Opc.Ua.Client.Tests/LoadTest.cs +++ b/Tests/Opc.Ua.Client.Tests/LoadTest.cs @@ -236,8 +236,9 @@ public async Task ServerLoadTestAsync() /* expected */ } - // Wait for notifications to be processed - await Task.Delay(publishingInterval * 10).ConfigureAwait(false); + // Wait for server to process last write (testDurationSeconds / writeCount -> time for a single write) + // + some publishing intervals for notifications to be processed + await Task.Delay((testDurationSeconds / (writeCount - 1)) + (publishingInterval * 10)).ConfigureAwait(false); // Verification TestContext.Out.WriteLine($"Writer task wrote {writeCount} times.");