FED-47: Finish porting compute_best_plan_from_closed_branches
#277
Conversation
- returning an Iterator, rather than a slice
- So, I could use the existing `merge` method.
QueryPlanningTraversal::compute_best_plan_from_closed_branches
QueryPlanningTraversal::compute_best_plan_from_closed_branches
compute_best_plan_from_closed_branches
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I am going to "approve" so you're not stuck without a review due to timezones. But it would be good to change the GenerateContext
trait into methods directly on the QueryPlanningTraversal
impl. It'll simplify some of the definitions and the usage in generate_all_plans_and_find_best
.
impl std::fmt::Display for SimultaneousPaths { | ||
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { | ||
self.fmt_indented(&mut IndentedFormatter::new(f)) | ||
} | ||
} | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'd just have the fmt_indented straight up in here, I don't think it needs an extra Impl block.
I also feel a bit strange about matching on sliced elements. A nicer pattern would be to do a .split_first()
.
impl std::fmt::Display for SimultaneousPaths { | |
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { | |
self.fmt_indented(&mut IndentedFormatter::new(f)) | |
} | |
} | |
impl std::fmt::Display for SimultaneousPaths { | |
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { | |
let mut state = IndentedFormatter::new(f); | |
if let Some((first, elements)) = self.0.split_first() { | |
state.write_fmt(format_args!("{{ {first} }}"))?; | |
state.write("{")?; | |
write_indented_lines(&mut state, elements, |f, elem| f.write(elem))?; | |
state.write("}") | |
} else { | |
state.write("<no path>") | |
} | |
} | |
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Although fmt_indented
is not used elsewhere in federation-next, the original JS has other places using it. So, I wanted to keep this method available for later.
As to matching as_slice()
is that the formatting is different if the self.0
has none, one element or more than one. So, I think matching on empty, one item and multiple items cases is easier to understand.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I agree with Iryna that we should just move that logic to the display trait as even if fmt_indented
was used elsewhere in JS codebase -> if display is just referencing the fmt_indented
in all other places anyway we could now just rely on the display to do the same thing.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It's an anti-pattern in Rust to have formatting done in a non-Display or non-Debug implementations. The Rust implementation should be using those whenever a formatted version of a struct is needed, and not a one-off method. It's an expectation of the ecosystem.
impl SimultaneousPaths { | ||
pub(crate) fn fmt_indented(&self, f: &mut IndentedFormatter) -> std::fmt::Result { | ||
match self.0.as_slice() { | ||
[] => f.write("<no path>"), | ||
|
||
[first] => f.write_fmt(format_args!("{{ {first} }}")), | ||
|
||
_ => { | ||
f.write("{")?; | ||
write_indented_lines(f, &self.0, |f, elem| f.write(elem))?; | ||
f.write("}") | ||
} | ||
} | ||
} | ||
} | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'd just have this as part of Display impl below (see the following suggestion)
impl SimultaneousPaths { | |
pub(crate) fn fmt_indented(&self, f: &mut IndentedFormatter) -> std::fmt::Result { | |
match self.0.as_slice() { | |
[] => f.write("<no path>"), | |
[first] => f.write_fmt(format_args!("{{ {first} }}")), | |
_ => { | |
f.write("{")?; | |
write_indented_lines(f, &self.0, |f, elem| f.write(elem))?; | |
f.write("}") | |
} | |
} | |
} | |
} |
src/query_plan/display.rs
Outdated
impl<'a, 'b> State<'a, 'b> { | ||
pub(crate) fn new(output: &'a mut fmt::Formatter<'b>) -> State<'a, 'b> { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is more for a hygiene follow up in the future. It seems like we now how specific display impls in query_plan
and query_graph
modules. I like that query_plan
groups it all together in display.rs
, query_graph
doesn't have that. I think we can have this State
live outside of the two modules (straight up in src
?) and have display.rs
be implemented in both.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I agree. This is struct is generic and reusable across.
src/query_plan/generate.rs
Outdated
// as arguments. However, more than one capture the QueryPlanningTraversal object and | ||
// one of them mutably captures it, which is not allowed in Rust. Therefore, we use a trait | ||
// that implements all three methods. | ||
pub trait GenerateContext<Plan, Element> { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Maybe something like PlanContext
? GenerateContext
seems a bit vague here.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I agree. I think it should've been PlanGeneratorContext
.
src/query_plan/generate.rs
Outdated
/// with both the cost of that plan and the best cost we have generated thus far | ||
/// (if that's not the first plan generated). | ||
/// This mostly exists to allow some debugging. | ||
fn on_plan(&mut self, plan: &Plan, cost: QueryPlanCost, prev_cost: Option<QueryPlanCost>); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
on_plan
is vague as well, anything can happen in this method kind of thing!🤯 How about context
or cost_context
or get_context
?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
While add
/cost
has specific functions, on_plan
is just an event callback and has no expected behaviors. I'll change it to use immutable &self
and rename it as on_plan_generated
, which would be a bit clearer.
I also decided to rename add
to add_to_plan
and cost
to compute_plan_cost
for clarity.
src/query_plan/generate.rs
Outdated
// as arguments. However, more than one capture the QueryPlanningTraversal object and | ||
// one of them mutably captures it, which is not allowed in Rust. Therefore, we use a trait | ||
// that implements all three methods. | ||
pub trait GenerateContext<Plan, Element> { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I am also wondering if this needs to be a trait, or can just be a struct, or can these methods live right in the QueryGraphTraversal
impl?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
generate_all_plans_and_find_best
was already written generically (using closures) and tested without QueryGraphTraversal
impl. So, I couldn't use QueryGraphTraversal
directly in it, which is a bit odd (since it is the only real ussage) but not a bad thing nontheless. Instead, I made it a trait, as an alternative to closure.
fn add( | ||
&mut self, | ||
(plan_graph, plan_tree): &(FetchDependencyGraph, Arc<OpPathTree>), | ||
tree: Arc<OpPathTree>, | ||
) -> (FetchDependencyGraph, Arc<OpPathTree>) { | ||
let mut updated_graph = plan_graph.clone(); | ||
let result = self | ||
.outer_self | ||
.updated_dependency_graph(&mut updated_graph, &tree); | ||
if result.is_ok() { | ||
let updated_tree = plan_tree.merge(&tree); | ||
(updated_graph, updated_tree) | ||
} else { | ||
// Failed to update. Return the original plan. | ||
(updated_graph, plan_tree.clone()) | ||
} | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Having read through this impl of the trait's .add
, I really don't think it needs to be a trait. This add can just live as a function on this struct instead.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I agree that QueryPlanningTraversal can implement those functions directly. Thought I'd have to keep the trait, I added impl trait for QueryPlanningTraversal
.
(plan_graph, plan_tree): &(FetchDependencyGraph, Arc<OpPathTree>), | ||
tree: Arc<OpPathTree>, | ||
) -> (FetchDependencyGraph, Arc<OpPathTree>) { | ||
let mut updated_graph = plan_graph.clone(); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don't know if it's going to be possible, but perhaps we can see if we can avoid this clone 🤔
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Sachin said cloning is a part of the original algorithm. I don't think we can avoid it as it currently stands.
At least, Rust clone()
implementation is much cleaner than the JS counterpart.
@lrlna Thanks for the review. I think I addressed most of your comments. But, I kept |
…lographql/federation-next#277) - refactor: moved `query_plan::display::State` to `crate::indented_display` module since it's quite reusable across the board.
Mostly finished, but still in progress. I wanted to get some feedback in a few areas.This PR is now ready for review.
OpPathTree::merge(&self, ...) -> Self
I neededOpPathTree::merge(&self, ...) -> Self
, butmerge
name is taken.We already have
PathTree::merge(self: &Arc<Self>, ...) -> Arc<Self>
.Probably, we can refactor the common part out. (@SimonSapin What do you think? Would there be a better way?)
But, how would you like to name the new method? I tentatively chose
merge_raw
.Update: I decided to use the existing
PathTree::merge
method and it worked.impl Clone for FetchDependencyGraph
I added a simple cloning, but it's probably wrong, since the JS version looks a lot more complicated.Update: Sachin confirmed the derived Clone implementation would suffice.
Debug logging
JS code hasdebug.log(...)
lines. How do we want to port those?Update: I left the logging code, but commented it out. We can decide what to do after rover integration.