test: cover startResponseReader serialization against concurrent buffer reads - #357
David Levy (dlevy-msft-sql) wants to merge 7 commits into
Conversation
Codecov Report✅ All modified and coverable lines are covered by tests. Additional details and impacted files@@ Coverage Diff @@
## main #357 +/- ##
===========================================
+ Coverage 77.91% 97.18% +19.26%
===========================================
Files 35 93 +58
Lines 7222 74687 +67465
===========================================
+ Hits 5627 72582 +66955
- Misses 1332 2068 +736
+ Partials 263 37 -226
Flags with carried forward coverage won't be shown. Click here to find out more. 🚀 New features to boost your workflow:
|
b8420f2 to
e1b94e3
Compare
There was a problem hiding this comment.
Pull request overview
This PR addresses a go test -race data race in the TDS response-reading path by ensuring only one processSingleResponse goroutine can read from a session’s shared tdsBuffer at a time (fixes #171).
Changes:
- Add
readDone chan struct{}totdsSessionto track completion of the active response-reader goroutine. - Introduce
(*tdsSession).startResponseReader(...)to wait for the previous reader to exit before starting the next one, and use it from bothstartReading()and the cancellation retry path innextToken(). - Add a unit test intended to validate the new serialization behavior.
Reviewed changes
Copilot reviewed 3 out of 3 changed files in this pull request and generated 1 comment.
| File | Description |
|---|---|
tds.go |
Adds tdsSession.readDone used as a completion gate for response readers. |
token.go |
Centralizes spawning of processSingleResponse via startResponseReader, serializing readers to prevent concurrent buffer reads. |
token_test.go |
Adds a test for response-reader serialization behavior. |
1f77eed to
3fdaa3f
Compare
4c2a4b8 to
3f9668c
Compare
There was a problem hiding this comment.
🟢 Approval recommended
The change is test-only, compiles cleanly in-context, and the new test’s synchronization strategy is consistent with the intended serialization guarantee.
Review details
- Files reviewed: 1/1 changed files
- Comments generated: 0 new
- Review effort level: Lite
3f9668c to
45ca4ce
Compare
There was a problem hiding this comment.
🟢 Approval recommended
The diff is test-only, matches the PR description, and the added test appears safe and self-contained without weakening existing coverage.
Review details
- Files reviewed: 1/1 changed files
- Comments generated: 0 new
- Review effort level: Lite
45ca4ce to
45c4b9b
Compare
45c4b9b to
e2f0042
Compare
There was a problem hiding this comment.
🔵 Needs a closer look
The test does not directly prove that the second transport read waits, leaving concurrent buffer access insufficiently guarded.
Review details
Suppressed comments (1)
token_test.go:527
- This assertion only observes when the second
startResponseReadercall returns; it never observes when itsprocessSingleResponsegoroutine entersRead. An implementation that launches the second reader before waiting on the previousreadDone, but delays returning until after that wait, would still pass this test while allowing concurrenttdsBufferreads. Use the existingreadEnteredsignal (the first signal has already been consumed) to fail if a second transport read starts beforecloseUnblock().
go func() {
close(goroutineStarted)
sess.startResponseReader(context.Background(), ch2, outputs{})
secondStarted.Store(1)
- Files reviewed: 1/1 changed files
- Comments generated: 0 new
- Review effort level: Lite
Use a blockingTransport that holds the first reader goroutine in-flight, then assert that the second startResponseReader call blocks until the first completes. This catches the actual race condition instead of testing the already-finished path.
Replace time.Sleep with readEntered channel for deterministic assertion that the first reader is blocked. Fix comment to accurately describe EOF error path (not panic recovery).
…Serializes Addresses reviewer feedback: the assertion that secondStarted is still 0 could be a false positive if the goroutine wasn't scheduled yet. Now we wait for a goroutine-started channel plus a 100ms grace period before checking.
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: aba0bc6c-33a0-46e6-a22b-970b03b73f2c
ab18768 to
dd73ddf
Compare
Summary
Adds the regression test for
startResponseReaderserialization. The production fix landed separately in #359, so this PR is test-only.Background
startReading()spawns aprocessSingleResponsegoroutine that reads fromsess.buf. If the caller does not fully drain the rows before executing the next statement, a second goroutine reads the same buffer concurrently. Both writetdsBuffer.rposandtdsBuffer.rsizewithout synchronisation, whichgo test -racereports as a data race.The fix is the
readDone chan struct{}field ontdsSession: eachprocessSingleResponsegoroutine closes its channel on exit, andstartResponseReaderwaits on the previous one before spawning a new goroutine. That shipped in65e137f(#359, "make readCancelConfirmation respect context cancellation") and is already onmain— it is the only commit onmainthat touchesreadDone. This PR adds the coverage that was missing.What the test does
TestStartResponseReaderSerializesholds the first reader insideReadusing a blocking transport, starts a secondstartResponseReaderin a goroutine, and asserts it has not returned. It then unblocks the first reader and asserts the second proceeds.Verification
Mutation-tested rather than assumed. With the wait disabled in
token.go:the test fails as intended:
Restoring the wait returns it to green over
-count=5.On the
time.Sleep(100 * time.Millisecond): it guards a negative assertion — proving the second call has not returned — which cannot be made fully deterministic without a hook into the blocked receive. I measured how much of it is load-bearing. With the wait disabled and the sleep cut to1 * time.Microsecond, the regression is still caught. The 100ms is headroom, not a threshold the result depends on.