package org.zstack.core.cascade; import org.springframework.beans.factory.annotation.Autowired; import org.zstack.core.componentloader.PluginRegistry; import org.zstack.core.workflow.*; import org.zstack.header.Component; import org.zstack.header.core.Completion; import org.zstack.header.core.workflow.*; import org.zstack.header.errorcode.ErrorCode; import org.zstack.header.exception.CloudRuntimeException; import org.zstack.utils.Bucket; import org.zstack.utils.DebugUtils; import org.zstack.utils.Utils; import org.zstack.utils.logging.CLogger; import java.util.*; import java.util.concurrent.Callable; /** */ public class CascadeFacadeImpl implements CascadeFacade, Component { private static final CLogger logger = Utils.getLogger(CascadeFacadeImpl.class); private class Node implements Comparable<Node> { private CascadeExtensionPoint extension; private TreeSet<Node> edges; private String name; @Override public int compareTo(Node n) { return this.getName().compareTo(n.getName()); } public CascadeExtensionPoint getExtension() { return extension; } public void setExtension(CascadeExtensionPoint extension) { this.extension = extension; } public TreeSet<Node> getEdges() { return edges; } public void setEdges(TreeSet<Node> edges) { this.edges = edges; } public String getName() { return name; } public void setName(String name) { this.name = name; } } private static class TreeNode implements Comparable<TreeNode> { private Node node; private TreeSet<TreeNode> leafs; @Override public int compareTo(TreeNode t) { return this.node.getName().compareTo(t.node.getName()); } } @Autowired private PluginRegistry pluginRgty; private Map<String, Node> nodes = new HashMap<>(); private Map<String, TreeNode> cascadeTree = new HashMap<>(); private void doSyncCascade(TreeNode treeNode, boolean init, CascadeAction action) throws CascadeException { CascadeAction currentAction; Node node = treeNode.node; if (!init) { currentAction = node.getExtension().createActionForChildResource(action); } else { currentAction = action; } if (currentAction != null) { for (TreeNode tn : treeNode.leafs) { doSyncCascade(tn, false, currentAction); } } logger.debug(String.format("[Sync cascade (%s)]: %s --> %s", action.getActionCode(), action.getParentIssuer(), node.getName())); node.getExtension().syncCascade(action); } @Override public void syncCascade(String actionCode, String issuer, Object context) throws CascadeException { CascadeAction action = new CascadeAction().setRootIssuer(issuer).setRootIssuerContext(context) .setParentIssuer(issuer).setParentIssuerContext(context).setActionCode(actionCode); syncCascade(action); } @Override public void syncCascade(CascadeAction action) throws CascadeException { assert action.getRootIssuer() != null; assert action.getParentIssuer() != null; assert action.getActionCode() != null; TreeNode root = cascadeTree.get(action.getRootIssuer()); doSyncCascade(root, true, action); } private void checkForNullElement(Node node, CascadeAction currentAction) { Object parentIssuerContext = currentAction.getParentIssuerContext(); if (parentIssuerContext != null && parentIssuerContext instanceof List) { List lst = (List) parentIssuerContext; for (Object obj : lst) { if (obj == null) { throw new CloudRuntimeException( String.format("CascadeExtensionPoint[%s] returns parent content that is a List but containing NULL element", node.getExtension().getClass().getName())); } } } Object rootIssuerContext = currentAction.getRootIssuerContext(); if (rootIssuerContext != null && rootIssuerContext instanceof List) { List lst = (List) rootIssuerContext; for (Object obj : lst) { if (obj == null) { throw new CloudRuntimeException( String.format("CascadeExtensionPoint[%s] returns root content that is a List but containing NULL element", node.getExtension().getClass().getName())); } } } } private void collectPathsForAsyncCascade(TreeNode treeNode, boolean init, boolean fullTraverse, CascadeAction action, List<Bucket> result) { CascadeAction currentAction; Node node = treeNode.node; if (!init) { currentAction = node.getExtension().createActionForChildResource(action); } else { currentAction = action; } if (fullTraverse) { if (currentAction == null) { currentAction = new CascadeAction(); currentAction.setActionCode(action.getActionCode()); currentAction.setRootIssuer(action.getRootIssuer()); currentAction.setRootIssuerContext(action.getRootIssuerContext()); currentAction.setParentIssuer(node.getName()); currentAction.setParentIssuerContext(null); } for (TreeNode tn : treeNode.leafs) { collectPathsForAsyncCascade(tn, false, true, currentAction, result); } } else { if (currentAction != null) { checkForNullElement(node, currentAction); for (TreeNode tn : treeNode.leafs) { collectPathsForAsyncCascade(tn, false, false, currentAction, result); } } } result.add(Bucket.newBucket(node, action)); } @Override public void asyncCascade(String actionCode, String issuer, Object context, Completion completion) { CascadeAction action = new CascadeAction(). setRootIssuer(issuer). setRootIssuerContext(context). setParentIssuer(issuer). setParentIssuerContext(context). setActionCode(actionCode); asyncCascade(action, completion); } @Override public void asyncCascadeFull(String actionCode, String issuer, Object context, Completion completion) { CascadeAction action = new CascadeAction(). setRootIssuer(issuer). setRootIssuerContext(context). setParentIssuer(issuer). setParentIssuerContext(context). setActionCode(actionCode). setFullTraverse(true); asyncCascade(action, completion); } @Override public void asyncCascade(CascadeAction action, final Completion completion) { assert action.getRootIssuer() != null; assert action.getParentIssuer() != null; assert action.getActionCode() != null; TreeNode root = cascadeTree.get(action.getRootIssuer()); DebugUtils.Assert(root != null, String.format("found no CascadeExtension for %s", action.getRootIssuer())); List<Bucket> paths = new ArrayList<>(); collectPathsForAsyncCascade(root, true, action.isFullTraverse(), action, paths); FlowChain chain = FlowChainBuilder.newSimpleFlowChain(); for (Bucket path : paths) { final Node node = path.get(0); final CascadeAction caction = path.get(1); chain.then(new NoRollbackFlow() { @Override public void run(final FlowTrigger trigger, Map data) { logger.debug(String.format("[Async cascade (%s)]: %s --> %s", caction.getActionCode(), caction.getParentIssuer(), node.getName())); node.getExtension().asyncCascade(caction, new Completion(trigger) { @Override public void success() { trigger.next(); } @Override public void fail(ErrorCode errorCode) { trigger.fail(errorCode); } }); } }); } chain.done(new FlowDoneHandler(completion) { @Override public void handle(Map data) { completion.success(); } }).error(new FlowErrorHandler(completion) { @Override public void handle(ErrorCode errCode, Map data) { completion.fail(errCode); } }).setName(String.format("Cascade: %s", action.getActionCode())).start(); } @Override public void syncCascadeNoException(String actionCode, String issuer, Object context) { try { syncCascade(actionCode, issuer, context); } catch (CascadeException e) { logger.warn(e.getMessage(), e); } } @Override public void syncCascadeNoException(CascadeAction action) { try { syncCascade(action); } catch (CascadeException e) { logger.warn(e.getMessage(), e); } } private void traverse(Node node, List<Node> resolved, List<List<Node>> paths) { if (resolved.contains(node)) { List<Node> path = new ArrayList<>(); path.addAll(resolved); paths.add(path); return; } resolved.add(node); if (!node.getEdges().isEmpty()) { for (Node e : node.getEdges()) { traverse(e, resolved, paths); } } else { List<Node> path = new ArrayList<>(); path.addAll(resolved); paths.add(path); } resolved.remove(node); } private TreeNode createTraversingTree(String issuer) { Node node = nodes.get(issuer); List<List<Node>> paths = new ArrayList<>(); if (node.getEdges().isEmpty()) { List<Node> resolved = new ArrayList<>(); resolved.add(node); traverse(node, resolved, paths); } else { for (Node n : node.getEdges()) { List<Node> resolved = new ArrayList<>(); resolved.add(node); traverse(n, resolved, paths); } } List<List<String>> ret = new ArrayList<>(); for (List<Node> path : paths) { List<String> spath = new ArrayList<>(); for (Node n : path) { spath.add(n.getName()); } ret.add(spath); } logger.debug(String.format("Cascade operation[%s]'s traversing branches (branches will be merged as a tree to reduce duplicate paths):", issuer)); for (List<String> lst : ret) { logger.debug(lst.toString()); } return makeTree(paths); } private TreeNode makeTree(List<List<Node>> paths) { int maxLevel = 0; for (List<Node> path : paths) { if (path.size() > maxLevel) { maxLevel = path.size(); } } Map<String, TreeNode> root = new HashMap<>(); Map<String, TreeNode> prev = root; Map<String, TreeNode> curr = prev; for (int i = 0; i < maxLevel; i++) { for (List<Node> path : paths) { if (i >= path.size()) { continue; } Node n = path.get(i); TreeNode tn = curr.get(n.getName()); if (tn == null) { tn = new TreeNode(); tn.node = n; tn.leafs = new TreeSet<>(); curr.put(n.getName(), tn); } int j = i - 1; if (j < 0 || j >= path.size()) { continue; } Node parent = path.get(j); TreeNode pn = prev.get(parent.getName()); pn.leafs.add(tn); } prev = curr; curr = new HashMap<>(); } assert root.size() == 1; return root.values().iterator().next(); } private void populateCascadeNodes(Map<String, CascadeExtensionPoint> exts) { // resolve tree for (CascadeExtensionPoint ext : exts.values()) { Node n = nodes.get(ext.getCascadeResourceName()); if (n == null) { n = new Node(); n.setName(ext.getCascadeResourceName()); n.setExtension(ext); n.setEdges(new TreeSet<>()); nodes.put(n.getName(), n); } for (String parent : ext.getEdgeNames()) { Node p = nodes.get(parent); if (p == null) { p = new Node(); p.setName(parent); p.setEdges(new TreeSet<>()); CascadeExtensionPoint dext = exts.get(parent); if (dext == null) { throw new CloudRuntimeException( String.format("cannot find parent CascadeExtensionPoint[%s] for CascadeExtensionPoint[name: %s, class: %s]", parent, ext.getCascadeResourceName(), ext.getClass().getName())); } p.setExtension(dext); nodes.put(parent, p); } p.getEdges().add(n); } } for (Node n : nodes.values()) { if (n.getExtension().getEdgeNames().contains(n.getExtension().getCascadeResourceName())) { throw new CloudRuntimeException( String.format("CascadeExtensionPoint[%s] is self referenced, a CascadeExtensionPoint cannot put itself as parent", n.getExtension().getClass().getName())); } } } private class CascadeWrapper implements CascadeExtensionPoint { CascadeExtensionPoint origin; List<CascadeExtensionPoint> addons = new ArrayList<>(); public CascadeWrapper(CascadeExtensionPoint origin) { this.origin = origin; } private CascadeExtensionPoint findExtensionPointByParent(String parent) { if (origin.getCascadeResourceName().equals(parent) || origin.getEdgeNames().contains(parent)) { return origin; } else { Optional<CascadeExtensionPoint> o = addons.stream().filter(c->c.getEdgeNames().contains(parent)).findFirst(); DebugUtils.Assert(o.isPresent(), String.format("cannot find any cascade extension point has the parent[%s] for the resource[%s]", parent, origin.getCascadeResourceName())); return o.get(); } } @Override public void syncCascade(CascadeAction action) throws CascadeException { findExtensionPointByParent(action.getParentIssuer()).syncCascade(action); } @Override public void asyncCascade(CascadeAction action, Completion completion) { findExtensionPointByParent(action.getParentIssuer()).asyncCascade(action, completion); } @Override public List<String> getEdgeNames() { return addons.isEmpty() ? origin.getEdgeNames() : new Callable<List<String>>() { @Override public List<String> call() { List<String> es = new ArrayList<>(); es.addAll(origin.getEdgeNames()); for (CascadeExtensionPoint a : addons) { es.addAll(a.getEdgeNames()); } return es; } }.call(); } @Override public String getCascadeResourceName() { return origin.getCascadeResourceName(); } @Override public CascadeAction createActionForChildResource(CascadeAction action) { return findExtensionPointByParent(action.getParentIssuer()).createActionForChildResource(action); } } private void populateNodes() { Map<String, CascadeExtensionPoint> exts = new HashMap<>(); for (CascadeExtensionPoint extp : pluginRgty.getExtensionList(CascadeExtensionPoint.class)) { CascadeExtensionPoint oext = exts.get(extp.getCascadeResourceName()); if (oext != null) { throw new CloudRuntimeException(String.format("duplicate CascadeExtensionPoint[%s, %s] for type[%s]", extp.getClass().getName(), oext.getClass().getName(), extp.getCascadeResourceName())); } exts.put(extp.getCascadeResourceName(), new CascadeWrapper(extp)); } for (CascadeExtensionPoint e : exts.values()) { CascadeWrapper w = (CascadeWrapper) e; for (CascadeAddOnExtensionPoint a : pluginRgty.getExtensionList(CascadeAddOnExtensionPoint.class)) { CascadeExtensionPoint c = a.cascadeAddOn(e.getCascadeResourceName()); if (c != null) { if (!c.getCascadeResourceName().equals(w.getCascadeResourceName())) { throw new CloudRuntimeException(String.format("the resourceName[%s] of the addon returned" + " by %s is not %s. The plugin cannot populate an cascade addon with different" + " resource name", c.getCascadeResourceName(), a.getClass(), w.getCascadeResourceName())); } w.addons.add(c); } } } populateCascadeNodes(exts); } private void populateTree() { for (Node n : nodes.values()) { TreeNode tn = createTraversingTree(n.getName()); cascadeTree.put(n.getName(), tn); } } @Override public boolean start() { populateNodes(); populateTree(); return true; } @Override public boolean stop() { return true; } }