Repository navigation
feat(substrait): add a hook for consuming relation inputs - #26603
alexanderbianchi wants to merge 4 commits into
Conversation
58e10b7 to
ccbcea5
Compare
geoffreyclaude
left a comment
There was a problem hiding this comment.
Looks pretty straightforward to me! As commented inline, I'd prefer it this were a bit more general (all multi-children nodes, rather than just join_rel and set_rel, if only for consistency's sake.)
| from_substrait_rel(self, rel).await | ||
| } | ||
|
|
||
| /// Consumes independent inputs of a JOIN or UNION, returning plans in input order. |
There was a problem hiding this comment.
For this to be complete, I think makes sense to extend it to other types of multi-children relations. CrossRel for instance, but probably not only.
| let left: LogicalPlanBuilder = LogicalPlanBuilder::from( | ||
| consumer.consume_rel(join.left.as_ref().unwrap()).await?, | ||
| ); | ||
| let right = LogicalPlanBuilder::from( | ||
| consumer.consume_rel(join.right.as_ref().unwrap()).await?, | ||
| ); | ||
| let (Some(left), Some(right)) = (join.left.as_deref(), join.right.as_deref()) else { | ||
| return substrait_err!("JoinRel requires left and right inputs"); | ||
| }; | ||
| let [left, right] = consumer | ||
| .consume_rel_inputs(&[left, right]) |
There was a problem hiding this comment.
It does not feel very clean to have two different APIs for consuming relations. Why not just consuming them concurrently just with futures_util::future::try_join_all?
There was a problem hiding this comment.
We have this lambda code.
consumer.push_lambda_parameters(...); // Enter the function's scope.
consumer.consume_expression(body, ...).await?;
consumer.pop_lambda_parameters(); // Leave that scope.
It keeps a stack because lambdas can be nested. The consumer carries a record of “where am I in this query?” Running two branches concurrently on one consumer can mix those records. There's a race condition here.
There was a problem hiding this comment.
🤔 How can that be, consumer is not mutable. Does that mean there's a Mutex somewhere?
There was a problem hiding this comment.
If that's the case, any chance to fix that instead so that we can consume concurrently?
There was a problem hiding this comment.
Something like this
let nested = context.with_outer_schema(outer_schema).with_lambda_parameters(parameters);
try_join_all(
inputs.iter().map(|rel| self.consume_rel(rel, context))
).awaitThat has some context where we can pass in the context explicitly removes state front the consumer but is a broader API change.
There was a problem hiding this comment.
Lambda state:
- The RwLock and parameter stack.
- Mutation through &self: acquire the write lock and push parameters.
- The sequence spanning an await: push → consume body → pop.
Correlated-subquery state:
There was a problem hiding this comment.
The reason this PR initially just added a multi input consumer was so we could override it and handle the parallelism complexity in the application code that overrides the consume.
There was a problem hiding this comment.
Then this is still a problem for the new .consume_rel_inputs() API, people implementing it can decide to do try_join_all() inside and shoot themselves in the foot producing incorrect plans because of mutation races to the lambda parameters or the correlated subqueries.
So whether we ship the .consume_rel_inputs() or do the try_join_all() ourselves in this code here seems like a matter of who is responsible for producing incorrect plans, DataFusion or its consumers, but from an API design standpoint, I don't think neither of the two are acceptable, we should not offer APIs that can silently produce incorrect plans.
There was a problem hiding this comment.
Sounds good, if we want to make the overall structure safer I think we should pursue something like #26890 which I can clean up and push for review. I can close this PR.
There was a problem hiding this comment.
That sounds more robust definitely, I'd definitely be more in favor of pursuing that
fd1057f to
ccbcea5
Compare
Problem:
Multi-input nodes need the same scheduling decisions: sequential or concurrent consumption, and how much concurrency to allow. Implementing those choices separately in each node duplicates logic.
Solution:
Route input consumption through one overridable
consume_rel_inputshook. Users can usetry_join_all, limit concurrency, or apply their own scheduling policy.Impact:
One implementation controls input scheduling across all nodes using the hook, without rewriting their conversion logic.
Parallel Consumption
We cannot just
try_join_all, because the consumer contains mutable state for CTE's and lambda functions. Blind concurrent consumption would lead to race conditions.Options
Testing
Since this PR is effectively a refactor, existing testing cover's all the relations in substrait.