Client channel: decode a single-chunk response without a chunk collection

A final chunk was saved into a new BufferCollection and taken back out
even when it was the whole response. When no chunks of the message are
saved, the partial message is open and the chunk is within the limits,
the response is now decoded from the chunk directly and its buffer
returned afterwards; anything else takes the saved-chunks path as
before.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Roman Ettlinger
2026-10-19 14:32:58 +02:00
co-authored by Claude Opus 5.5
parent 2135f7d7f9
commit 70655ffa07
2 changed files with 40 additions and 5 deletions
@@ -820,6 +820,32 @@ namespace Opc.Ua.Bindings
return TakeSavedChunks(); return TakeSavedChunks();
} }
/// <summary>
/// Whether a final chunk is a whole message by itself, which most are: no
/// chunks of the message are saved, the partial message is not closed and
/// the chunk is within the message limits. The caller then processes the
/// chunk directly instead of passing it to <see cref="GetSavedChunks"/>,
/// and keeps owning its buffer.
/// </summary>
/// <param name="chunk">The body of the final chunk.</param>
/// <param name="isServerContext">Whether the chunk is part of a request.</param>
protected bool IsSingleChunkMessage(ArraySegment<byte> chunk, bool isServerContext)
{
if (chunk.Array == null)
{
return false;
}
lock (m_partialMessageLock)
{
if (m_partialMessageClosed || m_partialMessageChunks != null)
{
return false;
}
}
// a chunk over the limits takes the path that reports them.
return !MessageLimitsExceeded(isServerContext, chunk.Count, 1);
}
/// <summary> /// <summary>
/// Detaches the chunks saved so far without offering another one. Used /// Detaches the chunks saved so far without offering another one. Used
/// where the final chunk has already been saved and only the collection /// where the final chunk has already been saved and only the collection
@@ -2033,6 +2033,7 @@ namespace Opc.Ua.Bindings
} }
BufferCollection? chunksToProcess = null; BufferCollection? chunksToProcess = null;
ArraySegment<byte> singleChunk = default;
try try
{ {
// check for an abort. // check for an abort.
@@ -2062,11 +2063,18 @@ namespace Opc.Ua.Bindings
return true; return true;
} }
// get the chunks to process. // get response: a response in one chunk, which most are, is decoded
chunksToProcess = GetSavedChunks(requestId, messageBody, false, gateHeld: false); // from that chunk without a collection of the chunks.
if (IsSingleChunkMessage(messageBody, isServerContext: false))
// get response. {
operation.MessageBody = ParseResponse(chunksToProcess); singleChunk = messageBody;
operation.MessageBody = DecodeMessage<IServiceResponse>(singleChunk, Quotas.MessageContext);
}
else
{
chunksToProcess = GetSavedChunks(requestId, messageBody, false, gateHeld: false);
operation.MessageBody = ParseResponse(chunksToProcess);
}
if (operation.MessageBody == null) if (operation.MessageBody == null)
{ {
@@ -2095,6 +2103,7 @@ namespace Opc.Ua.Bindings
finally finally
{ {
chunksToProcess?.Release(BufferManager, "ProcessResponseMessage"); chunksToProcess?.Release(BufferManager, "ProcessResponseMessage");
ReturnBuffer(singleChunk, "ProcessResponseMessage");
} }
} }