Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
57 changes: 55 additions & 2 deletions internal/rows/arrowbased/arrowIPCStreamIterator.go
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,12 @@ type arrowIPCStreamIterator struct {
resultPageIterator rowscanner.ResultPageIterator
isFinished bool
arrowSchemaBytes []byte
// err holds a page-fetch error detected while advancing the result page
// iterator. It must be surfaced through Next() rather than being masked as
// a normal end-of-results (io.EOF). See issue #507. Once reported (or on
// Close) the iterator is finished, so HasNext never re-captures it from the
// result page iterator.
err error
}

var _ rows.ArrowIPCStreamIterator = (*arrowIPCStreamIterator)(nil)
Expand All @@ -40,6 +46,13 @@ func (ri *arrowIPCStreamIterator) Next() (io.Reader, error) {
return nil, io.EOF
}

// If advancing the result page failed, surface that error rather than
// masking a failed page fetch as a normal end-of-results. See #507. The
// error is reported once and then the iterator is finished.
if err := ri.takePendingErr(); err != nil {
return nil, err
}

if ri.ipcStreamIterator != nil && ri.ipcStreamIterator.HasNext() {
return ri.ipcStreamIterator.Next()
}
Expand All @@ -57,8 +70,27 @@ func (ri *arrowIPCStreamIterator) Next() (io.Reader, error) {
return nil, io.EOF
}

// takePendingErr returns the pending page-fetch error, if any, and marks it as
// reported so it is surfaced exactly once; the iterator is then finished.
// Mirrors arrowRecordIterator.takePendingErr.
func (ri *arrowIPCStreamIterator) takePendingErr() error {
if ri.err == nil {
return nil
}
err := ri.err
ri.err = nil
ri.Close()
return err
}

// HasNext returns true if there are more streams available
func (ri *arrowIPCStreamIterator) HasNext() bool {
// A pending page-fetch error still needs to be reported via Next(), so the
// iterator is not considered exhausted until that error is consumed.
if ri.err != nil {
return true
}

if ri.isFinished {
return false
}
Expand All @@ -67,15 +99,27 @@ func (ri *arrowIPCStreamIterator) HasNext() bool {
return true
}

if ri.resultPageIterator == nil || !ri.resultPageIterator.HasNext() {
if ri.resultPageIterator == nil {
return false
}

if !ri.resultPageIterator.HasNext() {
// Distinguish a failed page fetch from a fully consumed result set.
if err := ri.resultPageIterator.Err(); err != nil {
ri.err = err
return true
}
return false
}

return true
}

// Close releases resources
// Close releases resources. It also discards any pending, not-yet-reported
// page-fetch error so a closed iterator reports HasNext()==false and
// Next()==io.EOF.
func (ri *arrowIPCStreamIterator) Close() {
ri.err = nil
if ri.ipcStreamIterator != nil {
ri.ipcStreamIterator.Close()
ri.ipcStreamIterator = nil
Expand All @@ -101,7 +145,16 @@ func (ri *arrowIPCStreamIterator) fetchNextData() error {
}

if ri.resultPageIterator == nil || !ri.resultPageIterator.HasNext() {
// Normally a page-fetch error is captured by HasNext() and surfaced by
// Next() via takePendingErr() before we get here. Check it anyway so a
// failed fetch can never be masked as io.EOF (#507) if call ordering
// changes.
ri.isFinished = true
if ri.resultPageIterator != nil {
if err := ri.resultPageIterator.Err(); err != nil {
return err
}
}
return io.EOF
}

Expand Down
81 changes: 71 additions & 10 deletions internal/rows/arrowbased/arrowRecordIterator.go
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,12 @@ type arrowRecordIterator struct {
isFinished bool
arrowSchemaBytes []byte
arrowSchema *arrow.Schema
// err holds a page-fetch error detected while advancing the result page
// iterator. It must be surfaced through Next() rather than being masked as
// a normal end-of-results (io.EOF). See issue #507. Once captured the
// iterator is finished, so checkFinished never re-captures it from the
// result page iterator.
err error
}

var _ rows.ArrowBatchIterator = (*arrowRecordIterator)(nil)
Expand All @@ -49,6 +55,13 @@ func (ri *arrowRecordIterator) Next() (arrow.Record, error) {
return nil, io.EOF
}

// If advancing the result page failed, surface that error rather than
// masking a failed page fetch as a normal end-of-results. See #507. The
// error is reported once and then the iterator is finished.
if err := ri.takePendingErr(); err != nil {
return nil, err
}

// make sure we have the current batch
err := ri.getCurrentBatch()
if err != nil {
Expand All @@ -63,14 +76,38 @@ func (ri *arrowRecordIterator) Next() (arrow.Record, error) {
return r, err
}

// takePendingErr returns the pending page-fetch error, if any, and marks it as
// reported so it is surfaced exactly once; the iterator is then finished.
func (ri *arrowRecordIterator) takePendingErr() error {
if ri.err == nil {
return nil
}
err := ri.err
ri.err = nil
ri.Close()
return err
}

// Indicate whether there are any more records available
func (ri *arrowRecordIterator) HasNext() bool {
ri.checkFinished()
return !ri.isFinished
// A pending page-fetch error still needs to be reported via Next(), so the
Comment thread
peco-review-bot[bot] marked this conversation as resolved.
// iterator is not considered exhausted until that error is consumed.
return !ri.isFinished || ri.err != nil
Comment thread
peco-review-bot[bot] marked this conversation as resolved.
}

// Free any resources associated with this iterator
// Free any resources associated with this iterator. An explicit Close also
// discards any pending, not-yet-reported page-fetch error so a closed iterator
// reports HasNext()==false and Next()==io.EOF.
func (ri *arrowRecordIterator) Close() {
ri.err = nil
ri.release()
}

// release frees the iterator's resources and marks it finished without
// touching any pending page-fetch error, so that error can still be surfaced
// via Next() when the end of the result set is reached internally.
func (ri *arrowRecordIterator) release() {
if !ri.isFinished {
ri.isFinished = true
if ri.currentBatch != nil {
Expand All @@ -88,14 +125,33 @@ func (ri *arrowRecordIterator) Close() {
}

func (ri *arrowRecordIterator) checkFinished() {
finished := ri.isFinished ||
((ri.currentBatch == nil || !ri.currentBatch.HasNext()) &&
(ri.batchIterator == nil || !ri.batchIterator.HasNext()) &&
(ri.resultPageIterator == nil || !ri.resultPageIterator.HasNext()))

if finished {
// Reached end of result set so Close
ri.Close()
// Once finished (including after an explicit Close()), never touch the
// result page iterator again: its HasNext() would issue a FetchResults
// round-trip against an operation that may already be closed.
if ri.isFinished {
return
}

noLocalData := (ri.currentBatch == nil || !ri.currentBatch.HasNext()) &&
Comment thread
peco-review-bot[bot] marked this conversation as resolved.
(ri.batchIterator == nil || !ri.batchIterator.HasNext())

// resultPageIterator.HasNext() issues a real FetchResults round-trip, so it
// is evaluated lazily — only once all local data is drained — to avoid
// eagerly prefetching the next page while the current one is still in use.
noMoreData := noLocalData &&
(ri.resultPageIterator == nil || !ri.resultPageIterator.HasNext())

// If the result page iterator reported no more pages because a page fetch
// failed (rather than because the result set was fully consumed), hold on
// to that error so Next() can surface it instead of io.EOF. See #507.
if noMoreData && ri.resultPageIterator != nil && ri.err == nil {
ri.err = ri.resultPageIterator.Err()
}

if noMoreData {
// Reached end of result set so release resources, keeping any
// pending page-fetch error for Next() to report.
ri.release()
}
}

Expand Down Expand Up @@ -185,6 +241,11 @@ func (ri *arrowRecordIterator) Schema() (*arrow.Schema, error) {
// Try to get schema bytes if not already available
if ri.arrowSchemaBytes == nil {
if ri.HasNext() {
// A pending page-fetch error is reported here (once) rather than
// via getCurrentBatch, so a later Next() does not repeat it.
if err := ri.takePendingErr(); err != nil {
return nil, err
}
if err := ri.getCurrentBatch(); err != nil {
return nil, err
}
Expand Down
Loading
Loading