From ce5cfcb1bd0246b0ba0be1450c1d1e1b1e8dfaf1 Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Thu, 21 May 2026 10:00:14 +0000
Subject: [PATCH 1/5] Implement ValueTaskSource-based channel operation waits
Agent-Logs-Url: https://github.com/OPCFoundation/UA-.NETStandard/sessions/24828edb-ccf0-41cb-b93d-218f7111aff6
Co-authored-by: romanett <7413710+romanett@users.noreply.github.com>
---
.../Stack/Tcp/ChannelAsyncOperation.cs | 173 +++++++++++++-----
.../Stack/Tcp/UaSCBinaryClientChannel.cs | 8 +-
.../Transport/ChannelAsyncOperationTests.cs | 103 +++++++++++
3 files changed, 236 insertions(+), 48 deletions(-)
create mode 100644 Tests/Opc.Ua.Core.Tests/Stack/Transport/ChannelAsyncOperationTests.cs
diff --git a/Stack/Opc.Ua.Core/Stack/Tcp/ChannelAsyncOperation.cs b/Stack/Opc.Ua.Core/Stack/Tcp/ChannelAsyncOperation.cs
index feba487b4d..89ecc474db 100644
--- a/Stack/Opc.Ua.Core/Stack/Tcp/ChannelAsyncOperation.cs
+++ b/Stack/Opc.Ua.Core/Stack/Tcp/ChannelAsyncOperation.cs
@@ -31,6 +31,7 @@
using System.Collections.Generic;
using System.Threading;
using System.Threading.Tasks;
+using System.Threading.Tasks.Sources;
using Microsoft.Extensions.Logging;
namespace Opc.Ua.Bindings
@@ -39,7 +40,7 @@ namespace Opc.Ua.Bindings
/// Stores the results of an asynchronous operation.
///
///
- public class ChannelAsyncOperation : IAsyncResult, IDisposable
+ public class ChannelAsyncOperation : IAsyncResult, IDisposable, IValueTaskSource, IValueTaskSource
{
///
/// Initializes the object with a callback
@@ -51,10 +52,14 @@ public ChannelAsyncOperation(int timeout, AsyncCallback? callback, object? async
m_synchronous = false;
m_completed = false;
m_logger = logger;
+ m_asyncWaitSource.RunContinuationsAsynchronously = true;
if (timeout is > 0 and not int.MaxValue)
{
- m_timer = new Timer(new TimerCallback(OnTimeout), null, timeout, Timeout.Infinite);
+ m_timeoutCancellationTokenSource = new CancellationTokenSource(timeout);
+ m_timeoutCancellationRegistration = m_timeoutCancellationTokenSource.Token.Register(
+ static state => ((ChannelAsyncOperation)state!).OnTimeout(),
+ this);
}
}
@@ -76,8 +81,9 @@ protected virtual void Dispose(bool disposing)
{
lock (m_lock)
{
- m_timer?.Dispose();
- m_timer = null;
+ m_timeoutCancellationRegistration.Dispose();
+ m_timeoutCancellationTokenSource?.Dispose();
+ m_timeoutCancellationTokenSource = null;
if (m_event != null)
{
@@ -86,13 +92,10 @@ protected virtual void Dispose(bool disposing)
m_event = null;
}
- if (m_tcs != null)
+ if (m_asyncWaitPending)
{
- if (!m_tcs.Task.IsCompleted)
- {
- m_tcs.TrySetCanceled();
- }
- m_tcs = null;
+ m_asyncWaitSource.SetException(new TaskCanceledException());
+ m_asyncWaitPending = false;
}
}
}
@@ -223,13 +226,25 @@ public T End(int timeout, bool throwOnError = true)
/// The awaitable response returned from the server.
///
///
- public async Task EndAsync(
+ public Task EndAsync(
+ int timeout,
+ bool throwOnError = true,
+ CancellationToken ct = default)
+ {
+ return EndValueTaskAsync(timeout, throwOnError, ct).AsTask();
+ }
+
+ ///
+ /// The low-allocation awaitable response returned from the server.
+ ///
+ internal async ValueTask EndValueTaskAsync(
int timeout,
bool throwOnError = true,
CancellationToken ct = default)
{
// check if the request has already completed.
bool mustWait = false;
+ ValueTask waitTask = default;
lock (m_lock)
{
@@ -237,8 +252,8 @@ public async Task EndAsync(
if (mustWait)
{
- m_tcs = new TaskCompletionSource(
- TaskCreationOptions.RunContinuationsAsynchronously);
+ m_asyncWaitPending = true;
+ waitTask = new ValueTask(this, m_asyncWaitSource.Version);
}
}
@@ -248,19 +263,13 @@ public async Task EndAsync(
bool badRequestInterrupted = false;
try
{
- Task awaitableTask = m_tcs!.Task;
- if (timeout != int.MaxValue)
+ if (timeout != int.MaxValue || ct != default)
{
- awaitableTask = m_tcs.Task
- .WaitAsync(TimeSpan.FromMilliseconds(timeout), ct);
+ _ = await WaitAsync(waitTask, timeout, ct).ConfigureAwait(false);
}
- else if (ct != default)
+ else
{
- awaitableTask = m_tcs.Task.WaitAsync(ct);
- }
- if (!await awaitableTask.ConfigureAwait(false))
- {
- badRequestInterrupted = true;
+ _ = await waitTask.ConfigureAwait(false);
}
}
catch (TimeoutException)
@@ -271,12 +280,9 @@ public async Task EndAsync(
{
badRequestInterrupted = true;
}
- finally
+ catch (OperationCanceledException)
{
- lock (m_lock)
- {
- m_tcs = null;
- }
+ badRequestInterrupted = true;
}
if (badRequestInterrupted && throwOnError)
@@ -368,17 +374,6 @@ public bool IsCompleted
}
}
- ///
- /// Called when the operation times out.
- ///
- private void OnTimeout(object? state)
- {
- if (m_timer != null)
- {
- InternalComplete(false, new ServiceResult(StatusCodes.BadRequestTimeout));
- }
- }
-
///
/// Called when an asynchronous operation completes.
///
@@ -403,12 +398,17 @@ protected virtual bool InternalComplete(bool doNotBlock, object? result)
m_completed = true;
- m_timer?.Dispose();
- m_timer = null;
+ m_timeoutCancellationRegistration.Dispose();
+ m_timeoutCancellationTokenSource?.Dispose();
+ m_timeoutCancellationTokenSource = null;
m_event?.Set();
- m_tcs?.TrySetResult(true);
+ if (m_asyncWaitPending)
+ {
+ m_asyncWaitSource.SetResult(true);
+ m_asyncWaitPending = false;
+ }
}
AsyncCallback? callback = m_callback;
@@ -436,17 +436,102 @@ protected virtual bool InternalComplete(bool doNotBlock, object? result)
return true;
}
+ ValueTaskSourceStatus IValueTaskSource.GetStatus(short token)
+ {
+ return m_asyncWaitSource.GetStatus(token);
+ }
+
+ ValueTaskSourceStatus IValueTaskSource.GetStatus(short token)
+ {
+ return m_asyncWaitSource.GetStatus(token);
+ }
+
+ bool IValueTaskSource.GetResult(short token)
+ {
+ return m_asyncWaitSource.GetResult(token);
+ }
+
+ void IValueTaskSource.GetResult(short token)
+ {
+ ((IValueTaskSource)m_asyncWaitSource).GetResult(token);
+ }
+
+ void IValueTaskSource.OnCompleted(
+ Action