Skip to content
This repository has been archived by the owner on May 3, 2024. It is now read-only.

FED-47: Finish porting compute_best_plan_from_closed_branches #277

Merged
merged 9 commits into from Apr 30, 2024

Conversation

duckki
Copy link
Contributor

@duckki duckki commented Apr 26, 2024

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 needed OpPathTree::merge(&self, ...) -> Self, but merge 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 has debug.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.

@duckki duckki changed the title [Graph Traversal] Finish porting QueryPlanningTraversal::compute_best_plan_from_closed_branches FED-47: Finish porting QueryPlanningTraversal::compute_best_plan_from_closed_branches Apr 27, 2024
@duckki duckki changed the title FED-47: Finish porting QueryPlanningTraversal::compute_best_plan_from_closed_branches FED-47: Finish porting compute_best_plan_from_closed_branches Apr 27, 2024
@duckki duckki marked this pull request as ready for review April 27, 2024 00:16
Copy link
Member

@lrlna lrlna left a 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.

Comment on lines +458 to +463
impl std::fmt::Display for SimultaneousPaths {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
self.fmt_indented(&mut IndentedFormatter::new(f))
}
}

Copy link
Member

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().

Suggested change
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>")
}
}
}

Copy link
Contributor Author

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.

Copy link
Member

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.

Copy link
Member

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.

Comment on lines +434 to +449
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("}")
}
}
}
}

Copy link
Member

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)

Suggested change
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("}")
}
}
}
}

Comment on lines 10 to 11
impl<'a, 'b> State<'a, 'b> {
pub(crate) fn new(output: &'a mut fmt::Formatter<'b>) -> State<'a, 'b> {
Copy link
Member

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.

Copy link
Contributor Author

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.

// 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> {
Copy link
Member

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.

Copy link
Contributor Author

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.

/// 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>);
Copy link
Member

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?

Copy link
Contributor Author

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.

// 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> {
Copy link
Member

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?

Copy link
Contributor Author

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.

Comment on lines 557 to 573
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())
}
}
Copy link
Member

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.

Copy link
Contributor Author

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();
Copy link
Member

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 🤔

Copy link
Contributor Author

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.

src/query_plan/generate.rs Show resolved Hide resolved
@duckki
Copy link
Contributor Author

duckki commented Apr 30, 2024

@lrlna Thanks for the review. I think I addressed most of your comments. But, I kept fmt_indented and the trait for generate_all_plans_and_find_best argument. I hope that's ok with you. I'll go ahead and merge.

@duckki duckki merged commit 7858182 into main Apr 30, 2024
7 checks passed
@duckki duckki deleted the duckki/FED-47 branch April 30, 2024 20:29
SimonSapin pushed a commit to apollographql/router that referenced this pull request May 3, 2024
…lographql/federation-next#277)

- refactor: moved `query_plan::display::State` to `crate::indented_display` module since it's quite reusable across the board.
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

5 participants