bug: serialize send to allow for concurrent session use #132

Merged
nemith merged 1 commit from brb/push-ywyqwrvmxrpp into main 2026-01-08 16:59:59 +00:00
nemith commented 2026-01-07 23:59:04 +00:00 (Migrated from github.com)

While doing some performance testing it was clear that I ditched the previous serialization for writes. The old v0.0.2 method was just to put all sends behind a mutex. This is a bit more advanced using an outbound queue and writing each message as they come in.

While doing some performance testing it was clear that I ditched the previous serialization for writes. The old v0.0.2 method was just to put all sends behind a mutex. This is a bit more advanced using an outbound queue and writing each message as they come in.
copilot-pull-request-reviewer[bot] (Migrated from github.com) reviewed 2026-01-08 00:03:51 +00:00
copilot-pull-request-reviewer[bot] (Migrated from github.com) left a comment

Pull request overview

This PR introduces serialized message sending to enable concurrent session use. Previously, the code allowed concurrent writes which could cause issues with the transport layer that only supports one active writer at a time. The new implementation uses an outbound message queue processed by a dedicated goroutine to serialize all writes.

Key changes:

  • Added sendLoop() goroutine and outQ channel to serialize all outbound messages
  • Renamed pendingReq to pendingResp for better semantic clarity
  • Refactored Do() method to use queueSend() instead of writing directly to transport

Reviewed changes

Copilot reviewed 2 out of 2 changed files in this pull request and generated 4 comments.

File Description
session.go Implements outbound message queue with sendLoop(), queueSend(), and sendMsg() functions; adds outQ channel to Session struct; refactors Do() to use queue-based sending; starts sendLoop goroutine after handshake
session_test.go Updates test to use renamed pendingResp type instead of pendingReq

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

## Pull request overview This PR introduces serialized message sending to enable concurrent session use. Previously, the code allowed concurrent writes which could cause issues with the transport layer that only supports one active writer at a time. The new implementation uses an outbound message queue processed by a dedicated goroutine to serialize all writes. **Key changes:** - Added `sendLoop()` goroutine and `outQ` channel to serialize all outbound messages - Renamed `pendingReq` to `pendingResp` for better semantic clarity - Refactored `Do()` method to use `queueSend()` instead of writing directly to transport ### Reviewed changes Copilot reviewed 2 out of 2 changed files in this pull request and generated 4 comments. | File | Description | | ---- | ----------- | | session.go | Implements outbound message queue with `sendLoop()`, `queueSend()`, and `sendMsg()` functions; adds `outQ` channel to Session struct; refactors Do() to use queue-based sending; starts sendLoop goroutine after handshake | | session_test.go | Updates test to use renamed `pendingResp` type instead of `pendingReq` | --- 💡 <a href="/nemith/netconf/new/main/.github/instructions?filename=*.instructions.md" class="Link--inTextBlock" target="_blank" rel="noopener noreferrer">Add Copilot custom instructions</a> for smarter, more guided reviews. <a href="https://docs.github.com/en/copilot/customizing-copilot/adding-repository-custom-instructions-for-github-copilot" class="Link--inTextBlock" target="_blank" rel="noopener noreferrer">Learn how to get started</a>.
copilot-pull-request-reviewer[bot] (Migrated from github.com) commented 2026-01-08 00:03:50 +00:00

The outQ channel is created as an unbuffered channel. This means that queueSend will block at line 352 until sendLoop is ready to receive from it. If sendLoop is slow or blocked, this could cause all callers of queueSend (including Do) to block. Consider using a buffered channel to allow some queueing without blocking the sender, or document this blocking behavior clearly.

The outQ channel is created as an unbuffered channel. This means that queueSend will block at line 352 until sendLoop is ready to receive from it. If sendLoop is slow or blocked, this could cause all callers of queueSend (including Do) to block. Consider using a buffered channel to allow some queueing without blocking the sender, or document this blocking behavior clearly.
@ -338,1 +433,4 @@
if !errors.Is(err, io.EOF) {
s.logger.Error("failed to receive message", "error", err)
}
}
copilot-pull-request-reviewer[bot] (Migrated from github.com) commented 2026-01-08 00:03:51 +00:00

If the outQ channel is closed (which happens during session Close at line 663), sending to it at line 352 will panic. This can occur if Do is called concurrently with Close. The function should check if the session is closing or use a recover mechanism, or the Close method should prevent new messages from being queued before closing the channel.

If the outQ channel is closed (which happens during session Close at line 663), sending to it at line 352 will panic. This can occur if Do is called concurrently with Close. The function should check if the session is closing or use a recover mechanism, or the Close method should prevent new messages from being queued before closing the channel.
copilot-pull-request-reviewer[bot] (Migrated from github.com) commented 2026-01-08 00:03:50 +00:00

The new concurrent send functionality introduced by sendLoop and queueSend lacks adequate test coverage. Specifically, there are no tests that: 1) verify that calling Do concurrently with Close doesn't cause panics or deadlocks, 2) ensure that sendLoop properly drains the queue before exiting, or 3) validate that the session handles the case where messages are queued while the session is closing. Consider adding tests for these edge cases to prevent regressions.

The new concurrent send functionality introduced by sendLoop and queueSend lacks adequate test coverage. Specifically, there are no tests that: 1) verify that calling Do concurrently with Close doesn't cause panics or deadlocks, 2) ensure that sendLoop properly drains the queue before exiting, or 3) validate that the session handles the case where messages are queued while the session is closing. Consider adding tests for these edge cases to prevent regressions.
copilot-pull-request-reviewer[bot] (Migrated from github.com) commented 2026-01-08 00:03:51 +00:00

There's a critical race condition in the Close method. After closing the outQ channel at line 663, any concurrent goroutine that attempts to call Do (which calls queueSend) will panic when trying to send on the closed channel at line 352 in queueSend. Additionally, there's no synchronization to wait for sendLoop to finish processing queued messages before closing the transport at line 666. This could result in sendLoop attempting to write to a closed transport. Consider: 1) checking the closing flag before attempting to queue messages, 2) using a sync.WaitGroup to wait for sendLoop to exit after closing outQ, and 3) ensuring proper error handling for operations attempted after Close is called.


There's a critical race condition in the Close method. After closing the outQ channel at line 663, any concurrent goroutine that attempts to call Do (which calls queueSend) will panic when trying to send on the closed channel at line 352 in queueSend. Additionally, there's no synchronization to wait for sendLoop to finish processing queued messages before closing the transport at line 666. This could result in sendLoop attempting to write to a closed transport. Consider: 1) checking the closing flag before attempting to queue messages, 2) using a sync.WaitGroup to wait for sendLoop to exit after closing outQ, and 3) ensuring proper error handling for operations attempted after Close is called. ```suggestion ```
nemith (Migrated from github.com) reviewed 2026-01-08 00:09:12 +00:00
nemith (Migrated from github.com) commented 2026-01-08 00:09:12 +00:00

it blocks on receive. This is fine.

it blocks on receive. This is fine.
copilot-pull-request-reviewer[bot] (Migrated from github.com) reviewed 2026-01-08 16:37:57 +00:00
copilot-pull-request-reviewer[bot] (Migrated from github.com) left a comment

Pull request overview

Copilot reviewed 4 out of 4 changed files in this pull request and generated 4 comments.


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

## Pull request overview Copilot reviewed 4 out of 4 changed files in this pull request and generated 4 comments. --- 💡 <a href="/nemith/netconf/new/main/.github/instructions?filename=*.instructions.md" class="Link--inTextBlock" target="_blank" rel="noopener noreferrer">Add Copilot custom instructions</a> for smarter, more guided reviews. <a href="https://docs.github.com/en/copilot/customizing-copilot/adding-repository-custom-instructions-for-github-copilot" class="Link--inTextBlock" target="_blank" rel="noopener noreferrer">Learn how to get started</a>.
@ -586,1 +681,3 @@
}
type closeSession struct {
XMLName xml.Name `xml:"close-session"`
}
copilot-pull-request-reviewer[bot] (Migrated from github.com) commented 2026-01-08 16:37:56 +00:00

The Close method is not protected against being called multiple times. If Close is called twice, the second call will panic when trying to close the already-closed channels at lines 687 and 690. Consider using sync.Once or checking if the channels are already closed before attempting to close them again.

The Close method is not protected against being called multiple times. If Close is called twice, the second call will panic when trying to close the already-closed channels at lines 687 and 690. Consider using sync.Once or checking if the channels are already closed before attempting to close them again.
copilot-pull-request-reviewer[bot] (Migrated from github.com) commented 2026-01-08 16:37:56 +00:00

The Transport struct documentation should be updated to reflect the new restriction that only one active reader and one active writer are allowed at a time. This is a significant behavioral constraint that callers need to be aware of.

The Transport struct documentation should be updated to reflect the new restriction that only one active reader and one active writer are allowed at a time. This is a significant behavioral constraint that callers need to be aware of.
@ -67,0 +79,4 @@
if t.activeWriter {
return nil, transport.ErrStreamBusy
}
copilot-pull-request-reviewer[bot] (Migrated from github.com) commented 2026-01-08 16:37:57 +00:00

The error message io.ErrClosedPipe is misleading in this context. The pipe is not closed; rather, there is already an active reader. Consider using a more descriptive custom error such as errors.New("reader already active") to make debugging easier for users of this test utility.

The error message io.ErrClosedPipe is misleading in this context. The pipe is not closed; rather, there is already an active reader. Consider using a more descriptive custom error such as errors.New("reader already active") to make debugging easier for users of this test utility.
copilot-pull-request-reviewer[bot] (Migrated from github.com) commented 2026-01-08 16:37:57 +00:00

The error message io.ErrClosedPipe is misleading in this context. The pipe is not closed; rather, there is already an active writer. Consider using a more descriptive custom error such as errors.New("writer already active") to make debugging easier for users of this test utility.

The error message io.ErrClosedPipe is misleading in this context. The pipe is not closed; rather, there is already an active writer. Consider using a more descriptive custom error such as errors.New("writer already active") to make debugging easier for users of this test utility.
copilot-pull-request-reviewer[bot] (Migrated from github.com) reviewed 2026-01-08 16:56:25 +00:00
copilot-pull-request-reviewer[bot] (Migrated from github.com) left a comment

Pull request overview

Copilot reviewed 4 out of 4 changed files in this pull request and generated no new comments.


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

## Pull request overview Copilot reviewed 4 out of 4 changed files in this pull request and generated no new comments. --- 💡 <a href="/nemith/netconf/new/main/.github/instructions?filename=*.instructions.md" class="Link--inTextBlock" target="_blank" rel="noopener noreferrer">Add Copilot custom instructions</a> for smarter, more guided reviews. <a href="https://docs.github.com/en/copilot/customizing-copilot/adding-repository-custom-instructions-for-github-copilot" class="Link--inTextBlock" target="_blank" rel="noopener noreferrer">Learn how to get started</a>.
Sign in to join this conversation.
No description provided.