-
Notifications
You must be signed in to change notification settings - Fork 29k
[SPARK-28346][SQL] clone the query plan between analyzer, optimizer and planner #25111
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Conversation
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 fragile to use member variable to keep stats, as they will be lost after copy.
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 fragile to use member variable to keep stats, as they will be lost after copy.
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.
The clone defined in TreeNode doesn't work for case object.
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.
The mapChildren in TreeNode will change the map type. (from CaseInsensitiveMap to a normal map)
|
Test build #107513 has finished for PR 25111 at commit
|
|
retest this please |
|
Test build #107523 has finished for PR 25111 at commit
|
|
Test build #107534 has finished for PR 25111 at commit
|
|
Test build #107573 has finished for PR 25111 at commit
|
|
Test build #107587 has finished for PR 25111 at commit
|
| sparkSession.sessionState.analyzer.executeAndCheck(logical, tracker) | ||
| } | ||
|
|
||
| lazy val withCachedData: LogicalPlan = { |
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 not necessary, but should we clone logical too before sending to analyzer?
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.
yea I think we should
| @volatile var statsOfPlanToCache: Statistics = null | ||
| def getStatsOfPlanToCache(): Statistics = { | ||
| getTagValue(STATS_OF_PLAN_TO_CACHE_TAG).get | ||
| } |
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.
Hmm, statsOfPlanToCache has volatile semantics. But making it as a TreeNodeTag, seems we don't preserve that?
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.
good point. tree node tag should be thread safe as well.
|
Test build #107657 has finished for PR 25111 at commit
|
|
Test build #107686 has finished for PR 25111 at commit
|
|
Test build #107753 has finished for PR 25111 at commit
|
|
The code looks good, but one thing we should be aware of: |
|
@maryannxue AFAIK we don't rely on plan instance equality in Spark. AQE is the only one I'm aware of that needs to check plan instance equality. |
|
Thank you, @cloud-fan! LGTM. |
|
|
||
| lazy val optimizedPlan: LogicalPlan = tracker.measurePhase(QueryPlanningTracker.OPTIMIZATION) { | ||
| sparkSession.sessionState.optimizer.executeAndTrack(withCachedData, tracker) | ||
| sparkSession.sessionState.optimizer.executeAndTrack(withCachedData.clone(), tracker) |
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.
Since now query plan is mutable, I think it's better to limit the life cycle of a query plan instance. We can clone the query plan between analyzer, optimizer and planner, so that the life cycle is limited in one stage.
If we decide to clone the plan after each stage, will any test fail if we do not clone it?
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.
test added
|
Test build #107914 has finished for PR 25111 at commit
|
| assert(error.getMessage.contains("error")) | ||
| } | ||
|
|
||
| test("analyzed plan should not change after it's generated") { |
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.
The tests still can pass without calling clone() in QueryExecution
| spark.experimental.extraStrategies = Nil | ||
| } | ||
|
|
||
| test("SPARK-28346: clone the query plan between analyzer, optimizer and planner") { |
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 test fails in the latest master branch.
|
Test build #108044 has finished for PR 25111 at commit
|
|
LGTM Thanks! Merged to master |
### What changes were proposed in this pull request? Since JIRA SPARK-28346,PR [25111](#25111), QueryExecution will copy all node stage-by-stage. This make all node instance twice almost. So we should make all class fields lazy to avoid create more unexpected object. ### Why are the changes needed? Avoid create more unexpected object. ### Does this PR introduce any user-facing change? No. ### How was this patch tested? Exists UT. Closes #26565 from ulysses-you/make-val-lazy. Authored-by: ulysses <youxiduo@weidian.com> Signed-off-by: Dongjoon Hyun <dhyun@apple.com>
…nd planner query plan was designed to be immutable, but sometimes we do allow it to carry mutable states, because of the complexity of the SQL system. One example is `TreeNodeTag`. It's a state of `TreeNode` and can be carried over during copy and transform. The adaptive execution framework relies on it to link the logical and physical plans. This leads to a problem: when we get `QueryExecution#analyzed`, the plan can be changed unexpectedly because it's mutable. I hit a real issue in apache#25107 : I use `TreeNodeTag` to carry dataset id in logical plans. However, the analyzed plan ends up with many duplicated dataset id tags in different nodes. It turns out that, the optimizer transforms the logical plan and add the tag to more nodes. For example, the logical plan is `SubqueryAlias(Filter(...))`, and I expect only the `SubqueryAlais` has the dataset id tag. However, the optimizer removes `SubqueryAlias` and carries over the dataset id tag to `Filter`. When I go back to the analyzed plan, both `SubqueryAlias` and `Filter` has the dataset id tag, which breaks my assumption. Since now query plan is mutable, I think it's better to limit the life cycle of a query plan instance. We can clone the query plan between analyzer, optimizer and planner, so that the life cycle is limited in one stage. new test Closes apache#25111 from cloud-fan/clone. Authored-by: Wenchen Fan <wenchen@databricks.com> Signed-off-by: gatorsmile <gatorsmile@gmail.com>
|
@cloud-fan I'm seeing some memory issues because of all these clone calls. I have a big query tree, maybe of ~20 height, so all the clone calls are recursive and keep everything in the stack alive: https://gist.github.com/MasterDDT/af98ad20ab0ed301476b9e8c58d8f5bb. 4g driver memory isnt enough on Spark 3.3, but I can run exact same workload on Spark 2.4 without any problems. In my forked code, could I disable all the clone calls if AQE is off? Will that cause any correctness issues? |
|
To @MasterDDT , I'd like to recommend to file an official JIRA issue. Otherwise, it's difficult to get any further discussion or help because this is too old thread. |
What changes were proposed in this pull request?
query plan was designed to be immutable, but sometimes we do allow it to carry mutable states, because of the complexity of the SQL system. One example is
TreeNodeTag. It's a state ofTreeNodeand can be carried over during copy and transform. The adaptive execution framework relies on it to link the logical and physical plans.This leads to a problem: when we get
QueryExecution#analyzed, the plan can be changed unexpectedly because it's mutable. I hit a real issue in #25107 : I useTreeNodeTagto carry dataset id in logical plans. However, the analyzed plan ends up with many duplicated dataset id tags in different nodes. It turns out that, the optimizer transforms the logical plan and add the tag to more nodes.For example, the logical plan is
SubqueryAlias(Filter(...)), and I expect only theSubqueryAlaishas the dataset id tag. However, the optimizer removesSubqueryAliasand carries over the dataset id tag toFilter. When I go back to the analyzed plan, bothSubqueryAliasandFilterhas the dataset id tag, which breaks my assumption.Since now query plan is mutable, I think it's better to limit the life cycle of a query plan instance. We can clone the query plan between analyzer, optimizer and planner, so that the life cycle is limited in one stage.
How was this patch tested?
new test