/*
* Licensed to Crate under one or more contributor license agreements.
* See the NOTICE file distributed with this work for additional
* information regarding copyright ownership. Crate licenses this file
* to you under the Apache License, Version 2.0 (the "License"); you may
* not use this file except in compliance with the License. You may
* obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
* implied. See the License for the specific language governing
* permissions and limitations under the License.
*
* However, if you have executed another commercial license agreement
* with Crate these terms will supersede the license and you may use the
* software solely pursuant to the terms of the relevant commercial
* agreement.
*/
package io.crate.executor.transport;
import io.crate.operation.NodeOperation;
import io.crate.operation.NodeOperationTree;
import io.crate.planner.Merge;
import io.crate.planner.MultiPhasePlan;
import io.crate.planner.Plan;
import io.crate.planner.PlanVisitor;
import io.crate.planner.distribution.DistributionType;
import io.crate.planner.distribution.UpstreamPhase;
import io.crate.planner.node.ExecutionPhase;
import io.crate.planner.node.ExecutionPhases;
import io.crate.planner.node.dql.Collect;
import io.crate.planner.node.dql.CountPlan;
import io.crate.planner.node.dql.QueryThenFetch;
import io.crate.planner.node.dql.join.NestedLoop;
import javax.annotation.Nullable;
import java.util.*;
/**
* Class used to generate a NodeOperationTree
* <p>
* <p>
* E.g. a plan like NL:
*
* <pre>
* NL
* 1 NLPhase
* 2 MergePhase
* / \
* / \
* QAF QAF
* 3 CollectPhase 5 CollectPhase
* 4 MergePhase 6 MergePhase
* </pre>
*
* Will have a data flow like this:
*
* <pre>
* 3 -- 4
* -- 1 -- 2
* 5 -- 6
* </pre>
* The NodeOperation tree will have 5 NodeOperations (3-4, 4-1, 5-6, 6-1, 1-2)
* And leaf will be 2 (the Phase which will provide the final result)
* <p>
* <p>
* Implementation detail:
* <p>
* <p>
* The phases are added in the following order
* <p>
* 2 - 1 [new branch 0] 4 - 3
* [new branch 1] 5 - 6
* <p>
* every time addPhase is called a NodeOperation is added
* that connects the previous phase (if there is one) to the current phase
*/
public final class NodeOperationTreeGenerator extends PlanVisitor<NodeOperationTreeGenerator.NodeOperationTreeContext, Void> {
private final static NodeOperationTreeGenerator INSTANCE = new NodeOperationTreeGenerator();
private NodeOperationTreeGenerator() {
}
private static class Branch {
private final Deque<ExecutionPhase> phases = new ArrayDeque<>();
private final byte inputId;
Branch(byte inputId) {
this.inputId = inputId;
}
}
static class NodeOperationTreeContext {
private final String localNodeId;
private final List<NodeOperation> nodeOperations = new ArrayList<>();
private final Deque<Branch> branches = new ArrayDeque<>();
private final Branch root;
private Branch currentBranch;
NodeOperationTreeContext(String localNodeId) {
this.localNodeId = localNodeId;
root = new Branch((byte) 0);
currentBranch = root;
}
/**
* adds a Phase to the "NodeOperation execution tree"
* should be called in the reverse order of how data flows.
* <p>
* E.g. in a plan where data flows from CollectPhase to MergePhase
* it should be called first for MergePhase and then for CollectPhase
*/
void addPhase(@Nullable ExecutionPhase executionPhase) {
addPhase(executionPhase, nodeOperations, true);
}
void addContextPhase(@Nullable ExecutionPhase executionPhase) {
addPhase(executionPhase, nodeOperations, false);
}
private void addPhase(@Nullable ExecutionPhase executionPhase,
List<NodeOperation> nodeOperations,
boolean setDownstreamNodes) {
if (executionPhase == null) {
return;
}
if (branches.size() == 0 && currentBranch.phases.isEmpty()) {
currentBranch.phases.add(executionPhase);
return;
}
byte inputId;
ExecutionPhase previousPhase;
if (currentBranch.phases.isEmpty()) {
previousPhase = branches.peekLast().phases.getLast();
inputId = currentBranch.inputId;
} else {
previousPhase = currentBranch.phases.getLast();
// same branch, so use the default input id
inputId = 0;
}
if (setDownstreamNodes) {
assert saneConfiguration(executionPhase, previousPhase.nodeIds()) : String.format(Locale.ENGLISH,
"NodeOperation with %s and %s as downstreams cannot work",
ExecutionPhases.debugPrint(executionPhase), previousPhase.nodeIds());
nodeOperations.add(NodeOperation.withDownstream(executionPhase, previousPhase, inputId, localNodeId));
} else {
nodeOperations.add(NodeOperation.withoutDownstream(executionPhase));
}
currentBranch.phases.add(executionPhase);
}
private boolean saneConfiguration(ExecutionPhase executionPhase, Collection<String> downstreamNodes) {
if (executionPhase instanceof UpstreamPhase &&
((UpstreamPhase) executionPhase).distributionInfo().distributionType() ==
DistributionType.SAME_NODE) {
return downstreamNodes.isEmpty() || downstreamNodes.equals(executionPhase.nodeIds());
}
return true;
}
void branch(byte inputId) {
branches.add(currentBranch);
currentBranch = new Branch(inputId);
}
void leaveBranch() {
currentBranch = branches.pollLast();
}
Collection<NodeOperation> nodeOperations() {
return nodeOperations;
}
}
public static NodeOperationTree fromPlan(Plan plan, String localNodeId) {
NodeOperationTreeContext nodeOperationTreeContext = new NodeOperationTreeContext(localNodeId);
INSTANCE.process(plan, nodeOperationTreeContext);
return new NodeOperationTree(nodeOperationTreeContext.nodeOperations(),
nodeOperationTreeContext.root.phases.getFirst());
}
@Override
public Void visitCountPlan(CountPlan plan, NodeOperationTreeContext context) {
context.addPhase(plan.mergePhase());
context.addPhase(plan.countPhase());
return null;
}
@Override
public Void visitCollect(Collect plan, NodeOperationTreeContext context) {
context.addPhase(plan.collectPhase());
return null;
}
@Override
public Void visitMerge(Merge merge, NodeOperationTreeContext context) {
context.addPhase(merge.mergePhase());
process(merge.subPlan(), context);
return null;
}
public Void visitQueryThenFetch(QueryThenFetch node, NodeOperationTreeContext context) {
process(node.subPlan(), context);
context.addContextPhase(node.fetchPhase());
return null;
}
@Override
public Void visitMultiPhasePlan(MultiPhasePlan multiPhasePlan, NodeOperationTreeContext context) {
// MultiPhasePlan's should be executed by the MultiPhaseExecutor, but it doesn't remove
// them from the tree in order to avoid re-creating plans with the MultiPhasePlan removed,
// so here it's fine to just skip over the multiPhasePlan because it has already been executed
process(multiPhasePlan.rootPlan(), context);
return null;
}
@Override
public Void visitNestedLoop(NestedLoop plan, NodeOperationTreeContext context) {
context.addPhase(plan.nestedLoopPhase());
context.branch((byte) 0);
process(plan.left(), context);
context.leaveBranch();
context.branch((byte) 1);
process(plan.right(), context);
context.leaveBranch();
return null;
}
@Override
protected Void visitPlan(Plan plan, NodeOperationTreeContext context) {
throw new UnsupportedOperationException(String.format(Locale.ENGLISH, "Can't create NodeOperationTree from plan %s", plan));
}
}