Remove the hash-partitioning shuffle writer - #2106
Conversation
Guard try_new() to reject Some(Partitioning::Hash(..)) and delete the now-unreachable Hash match arm from execute_shuffle_write, along with WriteTracker and the repart_time metric it used. Hash-repartition stages are handled by SortShuffleWriterExec instead; ShuffleWriterExec now only ever writes the single-file (None) passthrough path. Update the module's existing tests that exercised the Hash arm to exercise the surviving None path instead (test, test_partitioned folded into test, display_renders_child_operator_metrics, test_hash_repart_write_failure_propagates renamed to test_create_dir_failure_propagates), and add try_new_rejects_hash_partitioning to cover the new guard.
|
should we merge this after 54.1 has been released (if we assume releasing current main) as it will bring breaking change ? |
|
@phillipleblanc could you help review? |
I'm planning on cherry picking commits to the 54 release branch |
avantgardnerio
left a comment
There was a problem hiding this comment.
I think this conflicts with the direction I was heading. I'm "requesting changes" so I have a chance to look it over even if someone else approves. I'll respond with real feedback shortly.
|
I see this removes |
avantgardnerio
left a comment
There was a problem hiding this comment.
Changing to "comment" so I don't hold up the works, now that I know my critical concern was a non-issue.
Trying to retract my change request.
|
I'm considering case B here, now that we have multi-partition-per-task, there's some instances where it could be better to hash parition: But what I was working towards anyway was removing that hashing from Which I think this PR still allows. |
If there are valid cases for keeping hash-based partition where there are tiny number of output partitions, we can keep it in. I am targeting use cases that could have very high number of output partitions (hundreds/thousands). |
avantgardnerio
left a comment
There was a problem hiding this comment.
I was also looking to remove ShuffleWriter(Hash), but was looking at this from a slightly different perspective: I'd love to remove all shuffling from writers and only have ShuffleWriter(None) (I'm not sure if this is feasible or not yet for SortShuffleWriter.
The end goal for me is something like:
ArbitraryPartitioner -> ShuffleWriter(Passthrough)
Which allows for the RangeRepartitioning I was about to put up a PR for. I think this PR preserves that ability, but I'd like to explicitly call it out, and even go so far as to say standard hash partitioning is better for some cases where partitions_per_task > 1 (see comments for an example).
It's possible that we'll hit some friction down the road by conflating partitioning with writing, but we can deal with that when we get there (shortly). On the whole I think this PR is a step in the right direction, by deleting the Hash branch.
|
To put this more simply:
|
phillipleblanc
left a comment
There was a problem hiding this comment.
Looks good to me - we should probably put the api change label on this PR since its a breaking change.
|
Thanks for the reviews/feedback. I will aim to address feedback tomorrow. |
`ShuffleWriterExec` no longer repartitions, so its `Option<Partitioning>` was dead in every real path: the planner passes `None` at all call sites, the AQE adapter passes `None` or `Hash` (which routes to `SortShuffleWriterExec`), and proto decode can only produce `None` or `Hash`. Rather than reject `Some(Partitioning::Hash(..))` at runtime, remove the parameter and field entirely so the writer is unambiguously the passthrough writer. - `ShuffleWriterExec::try_new` drops its `shuffle_output_partitioning` argument; the struct drops the field and derives its properties from the child plan. - The `ShuffleWriter` trait impl returns `None` unconditionally. - Proto decode errors on a `ShuffleWriterExecNode` that still carries an `output_partitioning`; encode always writes `None`. - `create_shuffle_writer_with_config` errors on a non-hash `Some(..)` instead of silently building a writer that could not be encoded. - `ShuffleWriterVariant::Hash` is renamed to `Passthrough`, which is what the variant has actually meant since the hash write path was removed. Plan display keeps `partitioning: None` so TPC-H plan-stability golden files are unchanged.
|
@avantgardnerio @phillipleblanc I addressed feedback. PTAL when you can. |
I stand by my previous ✔️ . Thanks for doing this! It's funny we were both working on the same thing at the same time. |
* fix(core): report the real partitioning from ShuffleWriterExec `ShuffleWriterExec`'s `DisplayAs` wrote the string literal "partitioning: None". It used to print a value, until #2106 removed the `shuffle_output_partitioning` field along with the hash-partitioning writer; the token was then frozen as a literal so the TPC-H plan-stability goldens would not churn in the same diff. The result carried no information and was kept because it carried none: a constant cannot drift, so it cannot break a golden. It was also misleading, since the `None` refers to the absence of a repartitioning scheme, not to the writer's output partitioning. This writer preserves its input's, and `try_new` already sets its `PlanProperties` accordingly. Report `self.properties().output_partitioning()`, matching how `SortShuffleWriterExec` renders its own. Stage plans now show either the inherited hash partitioning or the collapsed single partition, both verifiable against the child plan directly below. Goldens regenerated with BALLISTA_GENERATE_GOLDEN=1. * test: update plan expectations for the real ShuffleWriterExec partitioning The display change left three sets of expectations still asserting the old "partitioning: None" literal: - `context_checks.rs` EXPLAIN / EXPLAIN ANALYZE tables, which now show the hash partitioning the final stage actually carries - eight `assert_plan!` inline snapshots in the scheduler (planner and AQE tests), regenerated with `cargo insta` - stale `/* Expected result: */` comment blocks in `planner.rs` and the sample stage plan in `dev/bin/showplan.sh` CI only reported the client failures because `cargo test` stops at the first failing test binary, so the scheduler snapshots never ran.

Which issue does this PR close?
N/A
Rationale for this change
Ballista had two shuffle writer implementations selected by a session config
(
ballista.shuffle.sort_based.enabled, defaulttrue):SortShuffleWriterExec) — writes one consolidated data file plus anindex file per input partition (
2 x Nfiles), coalesces small batches, and boundsmemory via spilling.
ShuffleWriterExec's hash path) — keeps one open ArrowStreamWriterper output partition and writes one file per
(input_partition, output_partition)pair (
N x Mfiles). This per-output-partition state does not scale to high outputpartition counts.
Sort-based has been the default and supersedes the hash path for the case that matters
(hash-repartition stages). Keeping the non-scaling hash writer and a config toggle to
select it added complexity and a foot-gun without a real use case. This PR removes it.
What changes are included in this PR?
Partitioning::Hash) now always useSortShuffleWriterExec; theballista.shuffle.sort_based.enabledgate is gone.create_shuffle_writer_with_configerrors on any otherSome(..)partitioninginstead of silently building a writer that could not be encoded.
ShuffleWriterExec's hash write path (the per-output-partitionWriteTrackerarray +BatchPartitioner) and the unusedrepart_timemetric.ShuffleWriterExecis retained for single-partition (None) output — the finaloutput stage,
CoalescePartitionsExec,SortPreservingMergeExec, and broadcast-buildstages. Since it no longer repartitions at all, its
Option<Partitioning>is goneoutright:
try_newdrops the argument, the struct drops the field and derives itsproperties from the child plan, and the
ShuffleWritertrait impl returnsNoneunconditionally.
ShuffleWriterVariant::Hashis renamed toPassthroughto match.(
is_sort_shuffle_output), which was already the authoritative signal; the redundantconfig-derived boolean and the
ballista_sort_shuffle_enabledtrait method are removed.ballista.shuffle.sort_based.enabledkey, itsConfigEntry,and its accessor. Sort-shuffle tuning keys (
batch_size,memory_limit_per_task_bytes, writer channel capacity) are unchanged.shuffle_benchis now sort-only (dropped--writer hash).tests; adapted writer/reader unit tests that constructed hash-partitioned writers to the
single-partition path.
sort_based.enabledconfig rows.TPC-H plan-stability golden files are unchanged: they already show
SortShuffleWriterExecfor hash-repartition stages andShuffleWriterExec: partitioning: Nonefor terminal stages, so query plan shape does not move under the defaultconfiguration.
Are there any user-facing changes?
Yes — breaking. The
ballista.shuffle.sort_based.enabledconfig key and the publicSessionConfigExt::ballista_sort_shuffle_enabledtrait method are removed, and thehash-based shuffle writer is no longer available.
BallistaConfigdoes not reject unknownkeys, so a config that still sets the removed key will be accepted and silently ignored
(sort-based shuffle is used unconditionally) rather than erroring.
Two further public API breaks for downstream projects that build plans directly:
ShuffleWriterExec::try_newloses its trailingshuffle_output_partitioningargument,and
ShuffleWriterVariant::Hashis renamed toShuffleWriterVariant::Passthrough.The
api changelabel is applied.Note on wire compatibility: scheduler and executor deploy together and serialized plans
are ephemeral, but for completeness — a legacy physical plan carrying a hash
output_partitioninginside aShuffleWriterExecNodenow fails to decode, sincehash-repartition stages are always emitted as
SortShuffleWriterExec.