andygrove opened a new issue, #23642:
URL: https://github.com/apache/datafusion/issues/23642

   ### Describe the bug
   
   `datafusion-proto` encodes `EmptyExec` as its schema only, so the partition 
count set by `EmptyExec::with_partitions(n)` is silently lost across a 
physical-plan round-trip. A plan that reported `n` partitions before 
serialization reports `1` after.
   
   `EmptyExecNode` has a single field:
   
   ```protobuf
   message EmptyExecNode {
     datafusion_common.Schema schema = 1;
   }
   ```
   
   and the decoder rebuilds the node with the default partition count:
   
   
https://github.com/apache/datafusion/blob/main/datafusion/proto/src/physical_plan/mod.rs
 — `try_into_physical_plan` for the `Empty` arm does 
`Ok(Arc::new(EmptyExec::new(schema)))`, dropping `partitions`, while the 
encoder writes only `schema`.
   
   This is a silent data-loss bug rather than an error: the decoded plan is 
well-formed but describes a *different* plan than the one encoded. It bites any 
distributed setup that plans on one process and executes on another. In Apache 
DataFusion Ballista, an optimizer rule collapses provably-empty sub-plans into 
an `EmptyExec` that inherits the partition count of the node it replaces; the 
scheduler sizes the stage's task count from that count, ships the plan to an 
executor, and every task above partition 0 then fails with:
   
   ```
   Internal("Assertion failed: partition < self.partitions: EmptyExec invalid 
partition 1 (expected less than 1)")
   ```
   
   `PlaceholderRowExec` has the same shape (`with_partitions` + schema-only 
proto) and looks like it has the same defect.
   
   ### To Reproduce
   
   ```rust
   use datafusion::arrow::datatypes::{DataType, Field, Schema};
   use datafusion::physical_plan::empty::EmptyExec;
   use datafusion::physical_plan::{ExecutionPlan, ExecutionPlanProperties};
   use datafusion::prelude::SessionContext;
   use datafusion_proto::physical_plan::{AsExecutionPlan, 
DefaultPhysicalExtensionCodec};
   use datafusion_proto::protobuf::PhysicalPlanNode;
   use std::sync::Arc;
   
   let schema = Arc::new(Schema::new(vec![Field::new("a", DataType::Int32, 
false)]));
   let plan: Arc<dyn ExecutionPlan> = 
Arc::new(EmptyExec::new(schema).with_partitions(4));
   assert_eq!(plan.output_partitioning().partition_count(), 4);
   
   let codec = DefaultPhysicalExtensionCodec {};
   let proto = PhysicalPlanNode::try_from_physical_plan(plan, &codec).unwrap();
   let ctx = SessionContext::new().task_ctx();
   let decoded = proto.try_into_physical_plan(&ctx, &codec).unwrap();
   
   // fails: left: 1, right: 4
   assert_eq!(decoded.output_partitioning().partition_count(), 4);
   ```
   
   ### Expected behavior
   
   The decoded `EmptyExec` reports the same partition count as the encoded one 
— the assertion above passes.
   
   ### Additional context
   
   Reproduced on 54.0.0; the same code is present on `main`.
   
   Fixing this means adding a `partitions` field to `EmptyExecNode` (and 
`PlaceholderRowExecNode`) and calling `.with_partitions(...)` on decode. A 
`uint32 partitions` field defaulting to `0` on absent can be mapped to `1` for 
backward compatibility with already-encoded plans.
   
   This overlaps #23501 (migrating `EmptyExec` / `PlaceholderRowExec` to the 
`try_to_proto` / `try_from_proto` pattern under #23494), which currently 
specifies "schema only" and "keep the wire format byte-for-byte identical" — 
that migration would preserve this bug, so it may be worth folding the field 
addition into that work.
   


-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: [email protected]

For queries about this service, please contact Infrastructure at:
[email protected]


---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]

Reply via email to