Skip to content

Commit 30fda50

Browse files
committed
[VL] Fix ValueStream empty-batch and finished-split handling
ValueStreamDataSource::next() had three defects: 1. Unbounded recursion: each exhausted iterator, null batch, or fully-filtered batch retried by calling next() recursively, so a long run of empty batches (e.g. a dynamic filter eliminating every row of consecutive batches) could grow the native stack without bound. The retry logic is now an iterative loop. 2. Finished-split signaling: when no iterators remain, return an explicit engaged null RowVectorPtr so TableScan reads the result as "current split finished" instead of relying on the implicit conversion of nullptr into the std::optional return type. 3. Null/zero-row batch normalization: RowVectorStream::next() converted every batch unconditionally, so a null batch from the upstream iterator would crash the conversion (a latent path given the iterator's peek-and-cache contract), and zero-row batches cost an extra scan round-trip apiece. Null and zero-row batches are now normalized to nullptr and skipped by the data source loop.
1 parent c2ff19b commit 30fda50

2 files changed

Lines changed: 100 additions & 36 deletions

File tree

cpp/velox/operators/plannodes/RowVectorStream.cc

Lines changed: 43 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -99,9 +99,14 @@ std::shared_ptr<ColumnarBatch> RowVectorStream::nextInternal() {
9999

100100
facebook::velox::RowVectorPtr RowVectorStream::next() {
101101
auto cb = nextInternal();
102+
if (cb == nullptr || cb->numRows() == 0) {
103+
return nullptr;
104+
}
102105
const std::shared_ptr<VeloxColumnarBatch>& vb = VeloxColumnarBatch::from(pool_, cb);
103106
auto vp = vb->getRowVector();
104-
VELOX_DCHECK(vp != nullptr);
107+
if (vp == nullptr || vp->size() == 0) {
108+
return nullptr;
109+
}
105110
return std::make_shared<facebook::velox::RowVector>(
106111
vp->pool(), outputType_, facebook::velox::BufferPtr(0), vp->size(), vp->children());
107112
}
@@ -136,47 +141,49 @@ void ValueStreamDataSource::addSplit(std::shared_ptr<facebook::velox::connector:
136141
std::optional<facebook::velox::RowVectorPtr> ValueStreamDataSource::next(
137142
uint64_t size,
138143
facebook::velox::ContinueFuture& future) {
139-
// Try to get current iterator if we don't have one
140-
while (!currentIterator_) {
141-
if (pendingIterators_.empty()) {
142-
// No more iterators to process
143-
return nullptr;
144-
}
145-
146-
// Get next RowVectorStream from queue
147-
currentIterator_ = pendingIterators_.front();
148-
pendingIterators_.erase(pendingIterators_.begin());
149-
}
144+
for (;;) {
145+
// Try to get current iterator if we don't have one.
146+
while (!currentIterator_) {
147+
if (pendingIterators_.empty()) {
148+
// No more iterators to process. Return an engaged null RowVectorPtr to
149+
// tell TableScan the current split is finished, not connector-blocked.
150+
return facebook::velox::RowVectorPtr(nullptr);
151+
}
150152

151-
// Check if current stream has more data
152-
if (!currentIterator_->hasNext()) {
153-
// Current stream exhausted, try next one
154-
currentIterator_ = nullptr;
155-
return next(size, future); // Recursively try next stream
156-
}
153+
// Get next RowVectorStream from queue.
154+
currentIterator_ = pendingIterators_.front();
155+
pendingIterators_.erase(pendingIterators_.begin());
156+
}
157157

158-
// Get next batch from current stream (RowVectorStream handles conversion)
159-
auto rowVector = currentIterator_->next();
158+
// Check if current stream has more data.
159+
if (!currentIterator_->hasNext()) {
160+
currentIterator_ = nullptr;
161+
continue;
162+
}
160163

161-
if (!rowVector) {
162-
currentIterator_ = nullptr;
163-
return next(size, future); // Recursively try next stream
164-
}
164+
// Get next batch from current stream. Null or zero-row batches are skipped
165+
// in-loop: a long run of empty batches must not grow the native stack, so
166+
// no recursion here.
167+
auto rowVector = currentIterator_->next();
168+
if (!rowVector) {
169+
continue;
170+
}
165171

166-
// Update metrics
167-
completedRows_ += rowVector->size();
168-
completedBytes_ += rowVector->estimateFlatSize();
172+
// Update metrics.
173+
completedRows_ += rowVector->size();
174+
completedBytes_ += rowVector->estimateFlatSize();
169175

170-
// Apply dynamic filters if any have been pushed down.
171-
if (!dynamicFilters_.empty()) {
172-
rowVector = applyDynamicFilters(rowVector);
173-
if (!rowVector) {
174-
// All rows filtered out, try next batch.
175-
return next(size, future);
176+
// Apply dynamic filters if any have been pushed down.
177+
if (!dynamicFilters_.empty()) {
178+
rowVector = applyDynamicFilters(rowVector);
179+
if (!rowVector) {
180+
// All rows filtered out, try next batch.
181+
continue;
182+
}
176183
}
177-
}
178184

179-
return rowVector;
185+
return rowVector;
186+
}
180187
}
181188

182189
facebook::velox::RowVectorPtr ValueStreamDataSource::applyDynamicFilters(const facebook::velox::RowVectorPtr& input) {
@@ -278,4 +285,4 @@ void ValueStreamDataSource::applyFilterOnColumn(
278285
rows.updateBounds();
279286
}
280287

281-
} // namespace gluten
288+
} // namespace gluten

cpp/velox/tests/ValueStreamDynamicFilterTest.cc

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,10 @@
1818
#include <gtest/gtest.h>
1919

2020
#include "memory/VeloxColumnarBatch.h"
21+
#include "operators/functions/RegistrationAllFunctions.h"
2122
#include "operators/plannodes/RowVectorStream.h"
23+
#include "velox/exec/tests/utils/PlanBuilder.h"
24+
#include "velox/parse/TypeResolver.h"
2225
#include "velox/type/Filter.h"
2326
#include "velox/vector/DecodedVector.h"
2427
#include "velox/vector/FlatVector.h"
@@ -51,6 +54,8 @@ class ValueStreamDynamicFilterTest : public ::testing::Test, public VectorTestBa
5154
protected:
5255
static void SetUpTestCase() {
5356
memory::MemoryManager::testingSetInstance(memory::MemoryManager::Options{});
57+
gluten::registerAllFunctions();
58+
parse::registerTypeResolver();
5459
}
5560

5661
void SetUp() override {
@@ -116,6 +121,58 @@ TEST_F(ValueStreamDynamicFilterTest, noFilterPassesAllRows) {
116121
ASSERT_EQ(ids, (std::vector<int64_t>{10, 20, 30}));
117122
}
118123

124+
TEST_F(ValueStreamDynamicFilterTest, skipsEmptyBatchesWithinSplit) {
125+
auto empty = makeRowVector({"id"}, {makeFlatVector<int64_t>(std::vector<int64_t>{})});
126+
auto batch = makeRowVector({"id"}, {makeFlatVector<int64_t>({40, 50})});
127+
auto outputType = asRowType(batch->type());
128+
auto scanNode = makeTableScanNode("vs-empty", outputType);
129+
130+
auto queryCtx = core::QueryCtx::create();
131+
auto task =
132+
Task::create("test-empty-batches", core::PlanFragment{scanNode}, 0, queryCtx, Task::ExecutionMode::kSerial);
133+
134+
task->addSplit(scanNode->id(), Split{makeSplit({empty, batch, empty})});
135+
task->noMoreSplits(scanNode->id());
136+
137+
auto ids = readAllInt64(task.get());
138+
ASSERT_EQ(ids, (std::vector<int64_t>{40, 50}));
139+
}
140+
141+
TEST_F(ValueStreamDynamicFilterTest, emptySplitProducesNoRows) {
142+
auto outputType = ROW({"id"}, {BIGINT()});
143+
auto scanNode = makeTableScanNode("vs-empty-split", outputType);
144+
145+
auto queryCtx = core::QueryCtx::create();
146+
auto task = Task::create("test-empty-split", core::PlanFragment{scanNode}, 0, queryCtx, Task::ExecutionMode::kSerial);
147+
148+
task->addSplit(scanNode->id(), Split{makeSplit({})});
149+
task->noMoreSplits(scanNode->id());
150+
151+
auto ids = readAllInt64(task.get());
152+
ASSERT_TRUE(ids.empty());
153+
}
154+
155+
TEST_F(ValueStreamDynamicFilterTest, filterProjectOverValueStream) {
156+
auto batch = makeRowVector({"id"}, {makeFlatVector<int64_t>({1, 2, 3})});
157+
auto outputType = asRowType(batch->type());
158+
auto scanNode = makeTableScanNode("vs-filter-project", outputType);
159+
auto planNodeIdGenerator = std::make_shared<core::PlanNodeIdGenerator>();
160+
auto planNode = exec::test::PlanBuilder(scanNode, planNodeIdGenerator, pool_.get())
161+
.filter("greaterthan(id, 1)")
162+
.project({"add(id, 1)"})
163+
.planNode();
164+
165+
auto queryCtx = core::QueryCtx::create();
166+
auto task =
167+
Task::create("test-filter-project", core::PlanFragment{planNode}, 0, queryCtx, Task::ExecutionMode::kSerial);
168+
169+
task->addSplit(scanNode->id(), Split{makeSplit({batch})});
170+
task->noMoreSplits(scanNode->id());
171+
172+
auto ids = readAllInt64(task.get());
173+
ASSERT_EQ(ids, (std::vector<int64_t>{3, 4}));
174+
}
175+
119176
// Test that filtering works when filter is injected after first batch.
120177
TEST_F(ValueStreamDynamicFilterTest, filterBigintRange) {
121178
auto batch1 = makeRowVector({"id"}, {makeFlatVector<int64_t>({1, 2, 3, 4, 5})});

0 commit comments

Comments
 (0)