feat(core): BufferExec — generic flow-control op with a memory-pool Dam mode#2095
Merged
Merged
Conversation
Contributor
Author
|
@phillipleblanc FYI |
avantgardnerio
added a commit
to avantgardnerio/arrow-ballista
that referenced
this pull request
Jul 20, 2026
…n shrink Two changes from phillipleblanc's review on apache#2095: 1. Explicit `ExecutionPlan` methods for the algebraic behaviour of a passthrough operator (`required_input_distribution`, `required_input_ordering`, `maintains_input_order`, `benefits_from_input_partitioning`, `partition_statistics`, `cardinality_effect`). Two actively override defaults — `maintains_input_order` (true, not false) and `cardinality_effect` (Equal, not Unknown). Punts on the pushdown/state/rewrite hooks. 2. Drain now holds the `MemoryReservation` inside a `stream::unfold` state and shrinks it by each batch's fill-time size as the batch is yielded, instead of dropping the reservation at end of fill. Pool accounting stays in sync with what's physically buffered right up until the last batch leaves. New test `dam_reservation_shrinks_incrementally_during_drain` pins the invariant — asserts `pool.reserved()` progression across the drain and lands at 0 on stream end. Regressing to the old "drop at end of fill" would fail on the first assertion. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
phillipleblanc
approved these changes
Jul 20, 2026
phillipleblanc
left a comment
Contributor
There was a problem hiding this comment.
This makes sense to me, thanks @avantgardnerio.
Member
|
This one needs a rebase @avantgardnerio |
…ool Dam mode � Conflicts: � ballista/core/proto/ballista.proto � ballista/core/src/serde/generated/ballista.rs � ballista/core/src/serde/mod.rs
avantgardnerio
force-pushed
the
brent/buffer-exec
branch
from
July 21, 2026 13:06
7450973 to
afe6da1
Compare
Contributor
Author
|
@andygrove rebase complete, if you have a moment for review 🙇 |
Member
|
Thanks @avantgardnerio and @phillipleblanc |
avantgardnerio
added a commit
to avantgardnerio/arrow-ballista
that referenced
this pull request
Jul 21, 2026
…Exec Preemptive polish before opening the PR for review — matches the pattern of reviewer asks Phillip made on apache#2094 and apache#2095: - Implement all 6 algebraic ExecutionPlan methods explicitly (required_input_ distribution, required_input_ordering, maintains_input_order, benefits_from_ input_partitioning, partition_statistics, cardinality_effect), each with a docstring justifying the value. maintains_input_order=false and partition_statistics=Unknown are the two that materially differ from the merged passthrough ops (RuntimeStatsExec, BufferExec). - Reject nullable routing expressions in try_new. The sibling RuntimeStatsExec already enforces this at plan-assembly time, but defense-in-depth catches the no-stats-pair path where discovery would silently fall back to single-bucket. TODO marker points at the KLL swap that lifts the restriction. - Add try_new_rejects_nullable_routing_key covering the new invariant. Includes two authorial touch-ups picked up along the way: a tone softening on the preserves_distribution docstring and a TODO on split_batch_by_range's row-by-row loop pointing at a vectorized Arrow rewrite. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Adds
BufferExec: a passthrough physical operator that buffers upstream batches with a policy-configurable release trigger. Currently supports one mode:BufferMode::Dam— buffer batches while aMemoryReservationgrows against DataFusion'sMemoryPool. Break the dam whenMemoryPool::try_growrefuses further allocation (any pressure — this operator, or contention from elsewhere in the query). After the dam breaks, buffered batches drain in order and the remainder of the input passes through unchanged. No user-tunable byte threshold; the pool decides pressure, mirroring the mechanismSortExecuses to trigger spill.Growth path is documented on the mode enum:
PassThrough,BufferAll,SpillOnPressure,BufferAndSpill,StageBoundary. OnlyDamis implemented today; the other names are fixed so future variants slot in without renaming callers. Ultimately this operator should land upstream in DataFusion so every blocking op (SortExec,HashAggregateExec, joins, ...) can share the same materialise/spill primitive instead of each rolling its own.Why this exists — pairing with
RuntimeStatsExec(#2094)Sketch-based routing has a chicken-and-egg problem: a range-repartition operator wants to pick cut points from an upstream
RuntimeStatsExecT-Digest, but a streaming pipeline pulls one batch through the tap and hands it straight to the router — the router runs with a one-batch sample.BufferMode::Daminserted betweenRuntimeStatsExecand the sketch-consuming router closes the gap without a plan-time size guess:MemoryPool::try_growrefuses — the pool decides "enough sample".The pool — not a magic byte count in code — decides when the sample is large enough. Same mechanism
SortExecuses to decide spill timing; no new tuning knob.Scope of this PR
Just the operator, its proto/serde plumbing, and tests. Nothing inserts
BufferExecinto a plan yet — no rewrite rule, no scheduler hook. It lands alongside the parallel-window detection rule and the range-repartition operators (OrderedRangeRepartitionExec/UnorderedRangeRepartitionExec) that will consume the sketch.Included:
BufferExecNode { BufferMode mode }onBallistaPhysicalPlanNode.oneof(tag 6) andenum BufferMode { BUFFER_MODE_DAM = 0; }.BallistaPhysicalExtensionCodec. Unknown mode discriminants surface as a decode error rather than a panic.Tests (4)
dam_drains_full_buffer_when_pool_has_headroom— input fits within the pool, everything buffers and drains at end-of-stream.dam_breaks_on_pool_pressure_and_passes_remainder— pool tight enough thattry_growrefuses partway through the stream; buffered rows plus the remainder all pass through, none lost or duplicated.dam_propagates_oom_when_first_batch_wont_fit— pool so tight even the first batch's reservation fails; operator surfaces OOM (mirrorsSortExecbehavior with nothing already buffered).test_buffer_exec_dam_roundtrip— plan-node serde roundtrip through the codec.All three streaming tests use
GreedyMemoryPoolwith a per-test byte cap so the dam-break branch fires without hardcoded thresholds in the operator.Test plan
cargo test -p ballista-core(121 lib + 2 doc tests pass; 4 new).cargo check --workspace --all-targets --lockedclean (no lock churn — no new external deps).cargo clippy --all-targetsclean.cargo fmt --all.Relation to #2094
Independent PR. If #2094 (RuntimeStatsExec) lands first, this rebases to tag 7 for
BufferExecNode; if this lands first, RuntimeStatsExec rebases the same way. No wire compatibility concerns since neither has shipped.🤖 Generated with Claude Code