Skip to content

[branch-53] Cherry-pick apache/datafusion#22852#147

Closed
ajegou wants to merge 1 commit into
DataDog:branch-53from
ajegou:arnaud.jegou/cherry-pick/apache-pr-22852-20260616
Closed

[branch-53] Cherry-pick apache/datafusion#22852#147
ajegou wants to merge 1 commit into
DataDog:branch-53from
ajegou:arnaud.jegou/cherry-pick/apache-pr-22852-20260616

Conversation

@ajegou

@ajegou ajegou commented Jun 17, 2026

Copy link
Copy Markdown

cherry-picks apache#22852

fix(topk): call attempt_early_completion when filter rejects entire batch

Cherry-picked commit 6520315d41851d1fb31da0ae1b4f22e48a6b2705 from apache/datafusion main onto branch-53.

Conflict resolution: branch-53 uses true_count() (also referenced later in the function as if true_count < num_rows). Kept that variable and added the attempt_early_completion call before the short-circuit return, which is semantically equivalent to the upstream !filter.has_true() check.

…atch (apache#22852)

- Closes apache#22849
- A related cross-partition starvation case is tracked separately in
[discussion](apache#22852 (comment))
for details

`TopK::insert_batch` short-circuits when the heap's dynamic filter
rejects every row in a batch:

```rust
if !filter.has_true() {
    // nothing to filter, so no need to update
    return Ok(());
}
```

The early-exit check `attempt_early_completion(&batch)` lives later in
the same function, gated on `replacements > 0`. So a batch that the
filter rejects entirely bypasses the check.

The heap's dynamic filter is derived from the heap's worst row (via
`update_filter`). A batch whose rows all come from a strictly worse sort
prefix is exactly the batch the filter rejects entirely — i.e. the very
signal `attempt_early_completion` is designed to detect ("the next batch
is past the heap's boundary, we can stop") is what causes the function
to short-circuit *before* the check runs.

This is a feature-interaction regression between two PRs that were both
correct in isolation. The `attempt_early_completion` mechanism was added
by apache#15563 (closing apache#15529). At the time, there was no heap-derived
dynamic filter on TopK, so the only sensible call site was right after a
successful heap insertion. Two months later, apache#15770 added the
dynamic-filter pushdown for TopK sorts, introducing the
`!filter.has_true()` short-circuit. The two features address different
problems and the new short-circuit didn't connect to the existing
prefix-completion check — which is how this gap opened up.

**Consequence**: on a TopK over an input ordered on the sort prefix,
`finished = true` is never set once the heap stabilizes. Since
`finished` is the signal `SortExec` uses to stop pulling from its input
(via `Poll::Ready(None)` from the TopK stream, which cascades into
dropping the source stream), the source keeps being polled long past the
point where no further row can improve the heap. The LIMIT optimization
effectively degrades to "heap saves memory but reads everything";
sources with cancellable streams (e.g. networked sources) never receive
the cancellation signal.

Single behavioral change in `datafusion/physical-plan/src/topk/mod.rs`:
call `attempt_early_completion(&batch)` immediately before the `return
Ok(())` in the `!filter.has_true()` branch.

Why this scope, not a broader restructuring:

- The existing `attempt_early_completion` call inside `if replacements >
0` is load-bearing for a related case: a batch containing a mix of
"still valuable" rows and "past the boundary" rows. The existing
`test_try_finish_marks_finished_with_prefix` test covers this case —
Batch 2 with `a=[2,3], b=[10,20]` against a heap where `heap.max.a = 2`;
the `(2, 10)` row must be inserted before the check on the `(3, 20)`
last row triggers. Moving the call earlier would skip the insertion of
valuable rows and break that test.
- The bug is specifically that the *short-circuit* path doesn't call the
check. The fix targets exactly that path.
- A related but separate gap is not addressed here: when
`filter.has_true() == true` but `replacements == 0` (the filter accepts
some rows but `find_new_topk_items` ends up inserting none of them), the
existing call inside `if replacements > 0` is also skipped. This
requires a divergence between the heap's filter predicate and the
row-byte comparison used inside `find_new_topk_items`, which shouldn't
normally happen (the filter is derived from the heap's worst row using
the same comparator). A deterministic synthetic repro would likely
require concurrent heap updates from sibling partitions or
boundary-value edge cases (NaN/NULL semantics, type coercion). Happy to
send a follow-up if reviewers want it covered; the workload that
motivated this fix was the filter-rejection case empirically.

Yes. Added a regression test
`test_try_finish_fires_when_filter_rejects_entire_batch`. The assertion
target is `topk.finished` — the flag that signals "stop pulling from the
source" to upstream consumers (read by `TopKExec::poll_next` to emit
`Poll::Ready(None)`). Asserting that the flag transitions on the
fully-filter-rejected batch is equivalent to asserting that the
source-stopping mechanism activates.

- Builds a TopK over a `(a, b)` sort with prefix `a`, k=3.
- Inserts a batch that fills the heap with rows from `a ∈ {1, 2}`;
`update_filter` tightens the filter to `a < 2 OR (a = 2 AND b < 30)`.
- Inserts a second batch with all rows at `a = 3` — filter rejects every
row.
- Without the fix: `insert_batch` short-circuits, `topk.finished` stays
`false`. Test fails.
- With the fix: `attempt_early_completion` fires (last-row prefix `a =
3` > heap.max prefix `a = 2`), `topk.finished` becomes `true`. Test
passes.

The test also asserts the emitted top-K is unchanged from after batch 1,
confirming no candidate row was incorrectly excluded by the early bail.

All 28 existing `topk::` tests continue to pass (including
`test_try_finish_marks_finished_with_prefix`, which exercises the
mixed-prefix case).

No public API or output changes. The fix only changes when TopK marks
itself `finished = true` — specifically, it now fires
`attempt_early_completion` for batches that are entirely rejected by the
heap's dynamic filter, where previously it would silently skip the
check. Output of TopK is unchanged; only the early-exit behavior
improves.

---------

Co-authored-by: Gabriel <45515538+gabotechs@users.noreply.github.com>
(cherry picked from commit 6520315)
@datadog-prod-us1-3

datadog-prod-us1-3 Bot commented Jun 17, 2026

Copy link
Copy Markdown

Pipelines

Fix all issues with BitsAI

⚠️ Warnings

🚦 2 Pipeline jobs failed

Datafusion extended tests | Run sqllogictests with the sqlite test suite   View in Datadog   GitHub Actions

Rust | build and run with wasm-pack   View in Datadog   GitHub Actions

Useful? React with 👍 / 👎

This comment will be updated automatically if new data arrives.
🔗 Commit SHA: 5a8804c | Docs | Datadog PR Page | Give us feedback!

@ajegou

ajegou commented Jun 17, 2026

Copy link
Copy Markdown
Author

/trigger-ci

@gh-worker-devflow-routing-ef8351

Copy link
Copy Markdown

View all feedbacks in Devflow UI.

2026-06-17 13:03:31 UTC ℹ️ Start processing command /trigger-ci
If you need support, contact us on Slack #ci-infra-support!


2026-06-17 13:03:34 UTC 🚨 Devflow

cannot get branch arnaud.jegou/cherry-pick/apache-pr-22852-20260616 from github API: unexpected status code: 404 Not Found (Request ID: A2B8:25FA86:1BD5F40:66D809B:6A329B24)

Details
child workflow execution error (type: devtools.Devtools_TriggerCiFromPr, workflowID: 7ebeb5fe-bea0-4f10-a732-6a459c209130_38, runID: 019ed5ae-0303-721b-916a-f87beea94e3d, initiatedEventID: 38, startedEventID: 39): child workflow execution error (type: devtools.Devtools_TriggerCi, workflowID: 019ed5ae-0303-721b-916a-f87beea94e3d_14, runID: 019ed5ae-0853-7a79-9880-41e15f9c2b85, initiatedEventID: 14, startedEventID: 15): activity error (type: github.GithubService_GetBranch, scheduledEventID: 8, startedEventID: 9, identity: 1@devtools-worker-6f6967c4b6-sgr6v@): cannot get branch arnaud.jegou/cherry-pick/apache-pr-22852-20260616 from github API: unexpected status code: 404 Not Found (Request ID: A2B8:25FA86:1BD5F40:66D809B:6A329B24) (type: GitNotFound, retryable: false): unexpected status code: 404 Not Found

If you need support, contact us on Slack #ci-infra-support with those details!

@ajegou ajegou changed the title Cherry-pick apache/datafusion#22852 [branch-53] Cherry-pick apache/datafusion#22852 Jun 17, 2026

ajegou commented Jun 17, 2026

Copy link
Copy Markdown
Author

Retargeting to branch-54 instead.

@ajegou ajegou closed this Jun 17, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants