Skip to content

Commit 650eb6a

Browse files
xiaoxmengfacebook-github-bot
authored andcommitted
Fix hash probe spill issues (facebookincubator#9400)
Summary: 1. Fix hash probe spill hang for right semi join types For right semi join (project and filter), it produce output from the probed rows in hash table after processing all the probe inputs. A probe input vector (set in Operator::input_) might be processed in multiple output calls until all the input rows have been processed. The probe side spill needs to spill output from the current probe input vector before clear the hash table and the current logic wait until the output is null. This is not correct for right semi joins as it always return null, and instead we shall check on if input_ has been reset. Add unit test to verify the fix plus join fuzzer run. 2. Disable hash probe spill if dynamic filters have been generated Hash probe might generate dynamic filters based on the hash join key ranges from build side and pushdown to upstream operators. If this has been triggered, then we can't spill from hash probe side as it might produce the incorrect result as detected by join fuzzer test. The fix is to disable hash probe spill if dynamic filters have been generated. Add unit test to verif the fix plus join fuzzer run. Pull Request resolved: facebookincubator#9400 Reviewed By: kagamiori, oerling Differential Revision: D55828946 Pulled By: xiaoxmeng fbshipit-source-id: ea4cd62abf0f9d169ad61cd5718bae4c44429e68
1 parent 9c83ef9 commit 650eb6a

File tree

8 files changed

+256
-51
lines changed

8 files changed

+256
-51
lines changed

velox/connectors/Connector.h

+2-2
Original file line numberDiff line numberDiff line change
@@ -355,8 +355,8 @@ class Connector {
355355
VELOX_NYI("connectorConfig is not supported yet");
356356
}
357357

358-
// Returns true if this connector would accept a filter dynamically generated
359-
// during query execution.
358+
/// Returns true if this connector would accept a filter dynamically generated
359+
/// during query execution.
360360
virtual bool canAddDynamicFilter() const {
361361
return false;
362362
}

velox/core/PlanNode.h

+1-1
Original file line numberDiff line numberDiff line change
@@ -1588,7 +1588,7 @@ class HashJoinNode : public AbstractJoinNode {
15881588
if (nullAware) {
15891589
VELOX_USER_CHECK(
15901590
isNullAwareSupported(joinType),
1591-
"Null-aware flag is supported only for semi and anti joins");
1591+
"Null-aware flag is supported only for semi project and anti joins");
15921592
VELOX_USER_CHECK_EQ(
15931593
1, leftKeys_.size(), "Null-aware joins allow only one join key");
15941594

velox/exec/Driver.cpp

+5-3
Original file line numberDiff line numberDiff line change
@@ -308,7 +308,8 @@ void Driver::pushdownFilters(int operatorIndex) {
308308
}
309309

310310
const auto& identityProjections = prevOp->identityProjections();
311-
auto inputChannel = getIdentityProjection(identityProjections, channel);
311+
const auto inputChannel =
312+
getIdentityProjection(identityProjections, channel);
312313
if (!inputChannel.has_value()) {
313314
// Filter channel is not an identity projection.
314315
VELOX_CHECK(
@@ -897,7 +898,7 @@ std::unordered_set<column_index_t> Driver::canPushdownFilters(
897898
for (auto i = 0; i < channels.size(); ++i) {
898899
auto channel = channels[i];
899900
for (auto j = filterSourceIndex - 1; j >= 0; --j) {
900-
auto prevOp = operators_[j].get();
901+
auto* prevOp = operators_[j].get();
901902

902903
if (j == 0) {
903904
// Source operator.
@@ -908,7 +909,8 @@ std::unordered_set<column_index_t> Driver::canPushdownFilters(
908909
}
909910

910911
const auto& identityProjections = prevOp->identityProjections();
911-
auto inputChannel = getIdentityProjection(identityProjections, channel);
912+
const auto inputChannel =
913+
getIdentityProjection(identityProjections, channel);
912914
if (!inputChannel.has_value()) {
913915
// Filter channel is not an identity projection.
914916
if (prevOp->canAddDynamicFilter()) {

velox/exec/HashProbe.cpp

+32-16
Original file line numberDiff line numberDiff line change
@@ -331,29 +331,30 @@ void HashProbe::asyncWaitForHashTable() {
331331
isRightSemiFilterJoin(joinType_) || isRightSemiProjectJoin(joinType_)) &&
332332
table_->hashMode() != BaseHashTable::HashMode::kHash && !isSpillInput() &&
333333
!hasMoreSpillData()) {
334-
// Find out whether there are any upstream operators that can accept
335-
// dynamic filters on all or a subset of the join keys. Create dynamic
336-
// filters to push down.
334+
// Find out whether there are any upstream operators that can accept dynamic
335+
// filters on all or a subset of the join keys. Create dynamic filters to
336+
// push down.
337337
//
338338
// NOTE: this optimization is not applied in the following cases: (1) if the
339339
// probe input is read from spilled data and there is no upstream operators
340340
// involved; (2) if there is spill data to restore, then we can't filter
341341
// probe inputs solely based on the current table's join keys.
342342
const auto& buildHashers = table_->hashers();
343-
auto channels = operatorCtx_->driverCtx()->driver->canPushdownFilters(
343+
const auto channels = operatorCtx_->driverCtx()->driver->canPushdownFilters(
344344
this, keyChannels_);
345345

346346
// Null aware Right Semi Project join needs to know whether there are any
347347
// nulls on the probe side. Hence, cannot filter these out.
348348
const auto nullAllowed = isRightSemiProjectJoin(joinType_) && nullAware_;
349349

350-
for (auto i = 0; i < keyChannels_.size(); i++) {
350+
for (auto i = 0; i < keyChannels_.size(); ++i) {
351351
if (channels.find(keyChannels_[i]) != channels.end()) {
352352
if (auto filter = buildHashers[i]->getFilter(nullAllowed)) {
353353
dynamicFilters_.emplace(keyChannels_[i], std::move(filter));
354354
}
355355
}
356356
}
357+
hasGeneratedDynamicFilters_ = !dynamicFilters_.empty();
357358
}
358359
}
359360

@@ -529,8 +530,8 @@ BlockingReason HashProbe::isBlocked(ContinueFuture* future) {
529530
}
530531

531532
void HashProbe::clearDynamicFilters() {
532-
// The join can be completely replaced with a pushed down
533-
// filter when the following conditions are met:
533+
// The join can be completely replaced with a pushed down filter when the
534+
// following conditions are met:
534535
// * hash table has a single key with unique values,
535536
// * build side has no dependent columns.
536537
if (keyChannels_.size() == 1 && !table_->hasDuplicateKeys() &&
@@ -833,7 +834,7 @@ bool HashProbe::skipProbeOnEmptyBuild() const {
833834
}
834835

835836
bool HashProbe::spillEnabled() const {
836-
return canReclaim();
837+
return canSpill() && !operatorCtx_->task()->hasMixedExecutionGroup();
837838
}
838839

839840
bool HashProbe::hasMoreSpillData() const {
@@ -1010,7 +1011,7 @@ RowVectorPtr HashProbe::getOutputInternal(bool toSpillOutput) {
10101011

10111012
numOut = evalFilter(numOut);
10121013

1013-
if (!numOut) {
1014+
if (numOut == 0) {
10141015
continue;
10151016
}
10161017

@@ -1547,7 +1548,9 @@ void HashProbe::ensureOutputFits() {
15471548
}
15481549

15491550
bool HashProbe::canReclaim() const {
1550-
return canSpill() && !operatorCtx_->task()->hasMixedExecutionGroup();
1551+
// NOTE: we can't spill from a hash probe operator if it has generated dynamic
1552+
// filters.
1553+
return spillEnabled() && !hasGeneratedDynamicFilters_;
15511554
}
15521555

15531556
void HashProbe::reclaim(
@@ -1692,13 +1695,26 @@ void HashProbe::spillOutput() {
16921695
&spillStats_);
16931696
outputSpiller->setPartitionsSpilled({0});
16941697

1695-
RowVectorPtr output;
1696-
while ((output = getOutputInternal(/*toSpillOutput=*/true))) {
1697-
// Ensure vector are lazy loaded before spilling.
1698-
for (int32_t i = 0; i < output->childrenSize(); ++i) {
1699-
output->childAt(i)->loadedVector();
1698+
RowVectorPtr output{nullptr};
1699+
for (;;) {
1700+
output = getOutputInternal(/*toSpillOutput=*/true);
1701+
if (output != nullptr) {
1702+
// Ensure vector are lazy loaded before spilling.
1703+
for (int32_t i = 0; i < output->childrenSize(); ++i) {
1704+
output->childAt(i)->loadedVector();
1705+
}
1706+
outputSpiller->spill(0, output);
1707+
continue;
17001708
}
1701-
outputSpiller->spill(0, output);
1709+
// NOTE: for right semi join types, we need to check if 'input_' has been
1710+
// cleared or not instead of checking on output. The right semi joins only
1711+
// producing the output after processing all the probe inputs.
1712+
if (input_ == nullptr) {
1713+
break;
1714+
}
1715+
VELOX_CHECK(
1716+
isRightSemiFilterJoin(joinType_) || isRightSemiProjectJoin(joinType_));
1717+
VELOX_CHECK((output == nullptr) && (input_ != nullptr));
17021718
}
17031719
VELOX_CHECK_LE(outputSpiller->spilledPartitionSet().size(), 1);
17041720

velox/exec/HashProbe.h

+9-3
Original file line numberDiff line numberDiff line change
@@ -44,9 +44,9 @@ class HashProbe : public Operator {
4444
}
4545
// NOTE: if we can't apply dynamic filtering, then we can start early to
4646
// read input even before the hash table has been built.
47-
const auto channels = operatorCtx_->driverCtx()->driver->canPushdownFilters(
48-
this, keyChannels_);
49-
return channels.empty();
47+
return operatorCtx_->driverCtx()
48+
->driver->canPushdownFilters(this, keyChannels_)
49+
.empty();
5050
}
5151

5252
void addInput(RowVectorPtr input) override;
@@ -335,6 +335,12 @@ class HashProbe : public Operator {
335335
// Channel of probe keys in 'input_'.
336336
std::vector<column_index_t> keyChannels_;
337337

338+
// True if we have generated dynamic filters from the hash build join keys.
339+
//
340+
// NOTE: 'dynamicFilters_' might have been cleared once they have been pushed
341+
// down to the upstream operators.
342+
tsan_atomic<bool> hasGeneratedDynamicFilters_{false};
343+
338344
// True if the join can become a no-op starting with the next batch of input.
339345
bool canReplaceWithDynamicFilter_{false};
340346

velox/exec/Operator.h

+13-13
Original file line numberDiff line numberDiff line change
@@ -257,24 +257,24 @@ class OperatorCtx {
257257
mutable std::unique_ptr<core::ExecCtx> execCtx_;
258258
};
259259

260-
// Query operator
260+
/// Query operator
261261
class Operator : public BaseRuntimeStatWriter {
262262
public:
263-
// Factory class for mapping a user-registered PlanNode into the corresponding
264-
// Operator.
263+
/// Factory class for mapping a user-registered PlanNode into the
264+
/// corresponding Operator.
265265
class PlanNodeTranslator {
266266
public:
267267
virtual ~PlanNodeTranslator() = default;
268268

269-
// Translates plan node to operator. Returns nullptr if the plan node cannot
270-
// be handled by this factory.
269+
/// Translates plan node to operator. Returns nullptr if the plan node
270+
/// cannot be handled by this factory.
271271
virtual std::unique_ptr<Operator>
272272
toOperator(DriverCtx* ctx, int32_t id, const core::PlanNodePtr& node) {
273273
return nullptr;
274274
}
275275

276-
// An overloaded method that should be called when the operator needs an
277-
// ExchangeClient.
276+
/// An overloaded method that should be called when the operator needs an
277+
/// ExchangeClient.
278278
virtual std::unique_ptr<Operator> toOperator(
279279
DriverCtx* ctx,
280280
int32_t id,
@@ -283,22 +283,22 @@ class Operator : public BaseRuntimeStatWriter {
283283
return nullptr;
284284
}
285285

286-
// Translates plan node to join bridge. Returns nullptr if the plan node
287-
// cannot be handled by this factory.
286+
/// Translates plan node to join bridge. Returns nullptr if the plan node
287+
/// cannot be handled by this factory.
288288
virtual std::unique_ptr<JoinBridge> toJoinBridge(
289289
const core::PlanNodePtr& /* node */) {
290290
return nullptr;
291291
}
292292

293-
// Translates plan node to operator supplier. Returns nullptr if the plan
294-
// node cannot be handled by this factory.
293+
/// Translates plan node to operator supplier. Returns nullptr if the plan
294+
/// node cannot be handled by this factory.
295295
virtual OperatorSupplier toOperatorSupplier(
296296
const core::PlanNodePtr& /* node */) {
297297
return nullptr;
298298
}
299299

300-
// Returns max driver count for the plan node. Returns std::nullopt if the
301-
// plan node cannot be handled by this factory.
300+
/// Returns max driver count for the plan node. Returns std::nullopt if the
301+
/// plan node cannot be handled by this factory.
302302
virtual std::optional<uint32_t> maxDrivers(
303303
const core::PlanNodePtr& /* node */) {
304304
return std::nullopt;

0 commit comments

Comments
 (0)