Skip to content

Return ArrayPool buffer to pool on success only, skip return on exception#66

Merged
mycroes merged 2 commits intomainfrom
copilot/remove-try-finally-arraypool-return
Apr 24, 2026
Merged

Return ArrayPool buffer to pool on success only, skip return on exception#66
mycroes merged 2 commits intomainfrom
copilot/remove-try-finally-arraypool-return

Conversation

Copy link
Copy Markdown
Contributor

Copilot AI commented Apr 23, 2026

ArrayPool buffers rented in ReadAsync/WriteAsync were previously returned unconditionally via finally. The intent is to skip the return on exception paths (letting the GC handle it) while still returning on success.

Changes

  • Removed try/finally blocks in ReadAsync(IDataItem[], CancellationToken) and WriteAsync(IDataItem[], CancellationToken)
  • ArrayPool<ReadWriteErrorCode>.Shared.Return(results) is now placed after ThrowIfHasErrors — only reached when no exception is thrown
var results = ArrayPool<ReadWriteErrorCode>.Shared.Rent(dataItems.Length);
await ReadAsync(dataItems, results, cancellationToken).ConfigureAwait(false);

ReadWriteErrorHelpers.ThrowIfHasErrors("Read", dataItems, results.AsSpan(0, dataItems.Length));
ArrayPool<ReadWriteErrorCode>.Shared.Return(results); // only on success

@mycroes mycroes marked this pull request as ready for review April 23, 2026 21:08
Copilot AI review requested due to automatic review settings April 23, 2026 21:08
@mycroes
Copy link
Copy Markdown
Owner

mycroes commented Apr 23, 2026

@gfoidl Actually had CoPilot do this for me, so didn't need a computer to commit it 😆

Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR changes how ArrayPool<ReadWriteErrorCode> buffers are managed in S7Connection.ReadAsync(IDataItem[], CancellationToken) and WriteAsync(IDataItem[], CancellationToken), returning the rented buffer only when the operation completes without throwing.

Changes:

  • Removed try/finally blocks around pooled results buffers in ReadAsync(...) and WriteAsync(...).
  • Moved ArrayPool<ReadWriteErrorCode>.Shared.Return(results) to occur only after ReadWriteErrorHelpers.ThrowIfHasErrors(...) completes successfully.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread Sally7/S7Connection.cs
Comment on lines 229 to +233
var results = ArrayPool<ReadWriteErrorCode>.Shared.Rent(dataItems.Length);
try
{
await ReadAsync(dataItems, results, cancellationToken).ConfigureAwait(false);
await ReadAsync(dataItems, results, cancellationToken).ConfigureAwait(false);

ReadWriteErrorHelpers.ThrowIfHasErrors("Read", dataItems, results.AsSpan(0, dataItems.Length));
}
finally
{
ArrayPool<ReadWriteErrorCode>.Shared.Return(results);
}
ReadWriteErrorHelpers.ThrowIfHasErrors("Read", dataItems, results.AsSpan(0, dataItems.Length));
ArrayPool<ReadWriteErrorCode>.Shared.Return(results);
Copy link

Copilot AI Apr 23, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ArrayPool<ReadWriteErrorCode>.Shared.Rent(...) is no longer paired with a Return(...) on exception paths (including the common path where ThrowIfHasErrors throws for per-item failures). This defeats pooling under failure conditions and can cause sustained allocations/GC pressure (and potentially LOH allocations for large dataItems.Length) when a PLC is offline or returns item errors repeatedly.

Consider restoring a try/finally that always returns the buffer, or if the intent is specifically to avoid returning on transport/cancellation exceptions, then catch only the expected AggregateException from ThrowIfHasErrors (return the buffer, then rethrow) while still letting other exceptions skip return if you really need that behavior.

Copilot uses AI. Check for mistakes.
Comment thread Sally7/S7Connection.cs
Comment on lines 293 to +297
var results = ArrayPool<ReadWriteErrorCode>.Shared.Rent(dataItems.Length);
try
{
await WriteAsync(dataItems, results, cancellationToken).ConfigureAwait(false);
await WriteAsync(dataItems, results, cancellationToken).ConfigureAwait(false);

ReadWriteErrorHelpers.ThrowIfHasErrors("Write", dataItems, results.AsSpan(0, dataItems.Length));
}
finally
{
ArrayPool<ReadWriteErrorCode>.Shared.Return(results);
}
ReadWriteErrorHelpers.ThrowIfHasErrors("Write", dataItems, results.AsSpan(0, dataItems.Length));
ArrayPool<ReadWriteErrorCode>.Shared.Return(results);
Copy link

Copilot AI Apr 23, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same pooling issue as in ReadAsync: if WriteAsync(...) throws (including ThrowIfHasErrors throwing AggregateException for per-item failures), the rented ArrayPool buffer is never returned. This can significantly increase allocation rate and memory pressure in repeated error scenarios.

Recommendation: ensure the rented buffer is returned for at least the expected error case (AggregateException from ThrowIfHasErrors) and ideally via try/finally to keep pooling effective and consistent with the existing ArrayPool<byte> usage earlier in this file.

Copilot uses AI. Check for mistakes.
@mycroes mycroes merged commit 62c62b5 into main Apr 24, 2026
7 checks passed
@mycroes mycroes deleted the copilot/remove-try-finally-arraypool-return branch April 24, 2026 12:34
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants