Repository navigation
ArrowBatchIterator turns a failed FetchResults into io.EOF (#507) - #509
peco-engineer-bot[bot] wants to merge 9 commits into
Conversation
Signed-off-by: peco-engineer-bot[bot] <3815206+peco-engineer-bot[bot]@users.noreply.github.com>
There was a problem hiding this comment.
Verdict: 1 Medium
Looks mostly good — the error-propagation fix is correct (Err() vs io.EOF, the errReported guard, and surfacing via Next()), but one medium concern: splitting the short-circuited checkFinished() chain into separate statements makes resultPageIterator.HasNext() run unconditionally, turning lazy next-page fetching into eager prefetching on every Next()/HasNext() call.
Addresses: - #4341629617 at internal/rows/arrowbased/arrowRecordIterator.go:115 Signed-off-by: peco-engineer-bot[bot] <peco-engineer-bot[bot]@users.noreply.github.com>
There was a problem hiding this comment.
Verdict: 1 Medium
Looks good — the core fix is correct and behavior-preserving for the database/sql path (which calls ResultPageIterator.Next() directly, not HasNext()), and Err() correctly filters io.EOF. One medium concern: the parallel arrowIPCStreamIterator shares the identical fetch-error-as-EOF defect and was left unfixed despite the new interface method being available to fix it.
Other findings
- 🟡 Medium — The sibling
arrowIPCStreamIteratorhas the identical error-swallowing bug that this PR fixes inarrowRecordIterator, and it is left unfixed even thoughErr()was added to the sharedResultPageIteratorinterface for exactly this purpose.
When an inter-page FetchResults fails, resultPageIterator.HasNext() stores the error and returns false. Here HasNext() (line 70) then returns false, and Next() (line 39: if !ri.HasNext() { return nil, io.EOF }) converts that failure into io.EOF. A for it.HasNext() { it.Next() } caller over GetArrowIPCStreams() therefore exits cleanly and never observes the fetch error — the same #507 symptom, on the exported IPC-stream path.
Consider wiring the new resultPageIterator.Err() into arrowIPCStreamIterator.HasNext()/Next() (mirroring the ri.err handling added to arrowRecordIterator), or scoping it out explicitly with a note/test if it is intentionally deferred.
There was a problem hiding this comment.
Verdict: 1 Medium · 1 Low
The fix looks right for arrowRecordIterator. I found 1 medium concern: the sibling arrowIPCStreamIterator still turns a failed inter-page fetch into io.EOF. I also found 1 low gap in what the new test checks. Nit: the t.Logf debug line in the new test can be removed or replaced with assertions.
Addresses: - #4355373515 at internal/rows/rowscanner/resultPageIterator.go:29 - #4355373521 at internal/rows/arrowbased/arrowRecordIterator_test.go:236 Signed-off-by: peco-engineer-bot[bot] <peco-engineer-bot[bot]@users.noreply.github.com>
There was a problem hiding this comment.
Verdict: 1 High · 2 Nit
Needs changes. One high-severity regression: arrowRecordIterator.checkFinished no longer stops early once the iterator is finished, so calling HasNext() after Close() can send a FetchResults request to an already-closed operation and report a spurious pending error. The description lists 3 changed files, but the diff also changes arrowIPCStreamIterator.go and category_test.go.
Other findings
- ⚪ Nit —
TestArrowIPCStreamIteratorSurfacesPageErroris weaker than its sibling record-iterator test. It only checksstreams > 0. It does not check that every stream from direct results and from the first fetched page arrives before the error, and it does not assert the FetchResults call count (noassert.Equal(t, 2, calls)). So a regression that drops the first fetched page, or that re-fetches after the error, would still pass. Please mirror theexpectedRows/callsassertions fromTestArrowRecordIteratorSwallowsPageError. - ⚪ Nit — Keeping the fetch error (instead of replacing it with
io.EOF) also changes thedatabase/sqlpath.rows.fetchResultPagecallsResultPageIterator.Next()(internal/rows/rows.go:570), so a repeatedrows.Next()after a failed page fetch now returns the fetch error again instead ofio.EOF. That is arguably more correct, but it is a behavior change outside the arrow-iterator scope described in the PR. It deserves a test inrows_test.go(or at least a mention in the description) so the new contract is intentional and pinned.
Addresses: - #4355423077 at internal/rows/arrowbased/arrowRecordIterator.go:112 Signed-off-by: peco-engineer-bot[bot] <peco-engineer-bot[bot]@users.noreply.github.com>
There was a problem hiding this comment.
Verdict: 1 Low · 1 Nit
The fix looks correct: page-fetch errors are now stored and returned once from Next() instead of being reported as io.EOF, and adding Err() to the interface is safe because resultPageIterator is the only implementation. Two notes: the PR description lists 3 changed files, but the diff also changes arrowIPCStreamIterator.go and category_test.go. And on the database/sql path, resultPageIterator.Next() now keeps returning the stored error after a failure instead of switching to io.EOF; that is intended, but no rows test covers it.
Other findings
- ⚪ Nit — This new
Err()branch infetchNextDatacan't really be reached on the normal path.Next()always callsHasNext()first, and whenresultPageIterator.HasNext()is false,HasNext()already stores the error inri.err, whichNext()then returns beforefetchNextDataruns. The only other way here is aftererrReported, and by thenisFinishedis already true, so the earlyreturn io.EOFat the top fires first. Either delete the branch or add a comment saying it's defensive, so a later reader doesn't think it's the main way errors get reported.
Addresses: - #4355457211 at internal/rows/arrowbased/arrowRecordIterator_test.go:335 Signed-off-by: peco-engineer-bot[bot] <peco-engineer-bot[bot]@users.noreply.github.com>
There was a problem hiding this comment.
Verdict: 1 Low
The fix looks correct: a failed inter-page FetchResults now reaches the caller through Next() on both the ArrowBatch and IPC-stream iterators, and resultPageIterator.HasNext() no longer replaces the stored error with io.EOF. There is one low-severity edge case in Schema(), where the same error can be reported twice. Nit: the PR description's file list leaves out arrowIPCStreamIterator.go and category_test.go, even though the diff changes both.
Addresses: - #4355495859 at internal/rows/arrowbased/arrowRecordIterator.go:88 Signed-off-by: peco-engineer-bot[bot] <peco-engineer-bot[bot]@users.noreply.github.com>
There was a problem hiding this comment.
Verdict: 1 Low
Looks good, with one low-severity concern. The fix correctly keeps the fetch error stored in resultPageIterator and reports it once through both the Arrow record iterator and the IPC stream iterator, and the tests cover the main paths well. Nits: the PR description lists 3 changed files, but the diff also changes arrowIPCStreamIterator.go and category_test.go. errReported in arrowRecordIterator is now redundant because of the new isFinished early return in checkFinished. The name TestArrowRecordIteratorSwallowsPageError describes the bug rather than the behaviour being tested.
Addresses: - #4355534513 at internal/rows/arrowbased/arrowRecordIterator.go:99 Signed-off-by: peco-engineer-bot[bot] <peco-engineer-bot[bot]@users.noreply.github.com>
There was a problem hiding this comment.
Verdict: 1 Low
The fix looks correct, with one low-severity cleanup finding. I traced both arrow iterators and resultPageIterator. A failed inter-page FetchResults is now reported once through Next(), Close() drops any error that hasn't been reported yet, and the lazy page fetch in checkFinished still only runs once local data is used up. Nit: the PR description lists 3 changed files, but the diff also changes arrowIPCStreamIterator.go and category_test.go. The test name TestArrowRecordIteratorSwallowsPageError describes the bug, not the behavior the test checks; something like ...SurfacesPageError would be clearer.
Addresses: - #4355577382 at internal/rows/arrowbased/arrowIPCStreamIterator.go:145 Signed-off-by: peco-engineer-bot[bot] <peco-engineer-bot[bot]@users.noreply.github.com>
There was a problem hiding this comment.
Verdict: 2 Nit
Looks good, with no correctness concerns found. The new Err() accessor and keeping HasNext() true until the error is read let both Arrow iterators (record and IPC stream) return a failed inter-page FetchResults from Next() exactly once instead of io.EOF. The tests cover surfacing the error, reporting it only once, Close discarding a pending error, and no fetch after an early close. The database/sql rows.Next path still works because it already treats only non-EOF errors as failures.
Other findings
- ⚪ Nit — In
arrowRecordIterator.checkFinished,finished := ri.isFinished || noMoreDatacan never take theri.isFinishedbranch, because the function already returned early whenri.isFinishedis true. It could just beif noMoreData { ... }. - ⚪ Nit — The PR description says three files changed, but the diff also changes
arrowIPCStreamIterator.go(the same fix applied to the IPC stream iterator) androwscanner/category_test.go. Updating the description would help reviewers.
Summary
Automated fix for #507 — ArrowBatchIterator turns a failed FetchResults into io.EOF.
Added an Err() accessor to the ResultPageIterator interface (and stopped HasNext from masking a stored fetch error with io.EOF) so arrowRecordIterator can distinguish a failed page fetch from a finished result set; the arrow iterator now keeps HasNext()==true while a page-fetch error is pending and returns that error from Next() instead of io.EOF (mirrors the database/sql fix #255). Offline-only per the carve-out: this is a client-side error-propagation contract (analogue of #254/#255), anchored in the issue's stated expectation, and not observable end-to-end since it requires forcing a mid-stream FetchResults failure against the live server.
Root cause & plan
Root cause: arrowRecordIterator swallows a failed inter-page FetchResults as io.EOF. When FetchResults fails between pages, resultPageIterator.HasNext() stores the error (rpf.err), closes, sets isFinished, and returns false. arrowRecordIterator.checkFinished() ANDs !resultPageIterator.HasNext() into its
finisheddecision with NO error check, so it treats the failure as end-of-results and calls Close(). arrowRecordIterator.HasNext() then returns false, so afor it.HasNext()caller loop exits without ever calling Next() and never sees the error (lastErr stays nil); the failed page looks like a complete result set. Compounding it, a later resultPageIterator.HasNext() call overwrites the stored error with io.EOF viaif rpf.isFinished && rpf.nextResultPage == nil { rpf.err = io.EOF }. This is the Arrow-batch analogue of #254; the database/sql path was fixed in #255 by having rows.Next return the fetch error, but the arrow path decides termination via HasNext() and collapses error and done into one isFinished flag.Files:
internal/rows/rowscanner/resultPageIterator.go,internal/rows/arrowbased/arrowRecordIterator.go,internal/rows/arrowbased/arrowRecordIterator_test.goPlanned coverage:
for it.HasNext() { rec, err := it.Next(); if err != nil { lastErr = err; break } ... }. Assert lastErr ErrorContains 'connection reset'. Today the loop ends with lastErr == nil (bug); after the fix Next() returns the page-fetch error instead of io.EOF. (ArrowBatchIterator (arrowRecordIterator) turns a failed inter-page FetchResults into io.EOF instead of surfacing the fetch error)Files changed
internal/rows/arrowbased/arrowRecordIterator_test.gointernal/rows/rowscanner/resultPageIterator.gointernal/rows/arrowbased/arrowRecordIterator.goTest plan
TestArrowRecordIteratorSwallowsPageError— fails (red) against the original code, passes (green) after the fix🤖 Generated by engineer-bot (bug-fix flow) — review before merge.