/******************************************************************************* * Copyright (c) 2012-2017 Codenvy, S.A. * All rights reserved. This program and the accompanying materials * are made available under the terms of the Eclipse Public License v1.0 * which accompanies this distribution, and is available at * http://www.eclipse.org/legal/epl-v10.html * * Contributors: * Codenvy, S.A. - initial API and implementation *******************************************************************************/ package org.eclipse.che.api.environment.server; import com.google.common.base.Joiner; import com.google.common.collect.Sets; import org.eclipse.che.api.environment.server.model.CheServiceImpl; import org.eclipse.che.api.environment.server.model.CheServicesEnvironmentImpl; import java.util.HashMap; import java.util.Iterator; import java.util.List; import java.util.Map; import java.util.Optional; import java.util.Set; import java.util.stream.Collectors; import static java.lang.String.format; /** * Finds order of Che services to start that respects dependencies between services. * * @author Alexander Garagatyi * @author Alexander Andrienko */ public class DefaultServicesStartStrategy { /** * Resolves order of start for machines in an environment. * * @throws IllegalArgumentException * if order of machines can not be calculated */ public List<String> order(CheServicesEnvironmentImpl composeEnvironment) throws IllegalArgumentException { Map<String, Integer> weights = weightMachines(composeEnvironment.getServices()); return sortByWeight(weights); } /** * Returns mapping of names of machines to its weights in dependency graph. * * @throws IllegalArgumentException * if weights of machines can not be calculated */ private Map<String, Integer> weightMachines(Map<String, CheServiceImpl> services) throws IllegalArgumentException { HashMap<String, Integer> weights = new HashMap<>(); // create machines dependency graph Map<String, Set<String>> dependencies = new HashMap<>(services.size()); for (Map.Entry<String, CheServiceImpl> serviceEntry : services.entrySet()) { CheServiceImpl service = serviceEntry.getValue(); Set<String> machineDependencies = Sets.newHashSetWithExpectedSize(service.getDependsOn().size() + service.getLinks().size() + service.getVolumesFrom().size()); for (String dependsOn : service.getDependsOn()) { checkDependency(dependsOn, serviceEntry.getKey(), services, "A machine can not depend on itself"); machineDependencies.add(dependsOn); } // links also counts as dependencies for (String link : service.getLinks()) { String dependency = getServiceFromLink(link); checkDependency(dependency, serviceEntry.getKey(), services, "A machine can not link to itself"); machineDependencies.add(dependency); } // volumesFrom also counts as dependencies for (String volumesFrom : service.getVolumesFrom()) { String dependency = getServiceFromVolumesFrom(volumesFrom); checkDependency(dependency, serviceEntry.getKey(), services, "A machine can not contain 'volumes_from' to itself"); machineDependencies.add(dependency); } dependencies.put(serviceEntry.getKey(), machineDependencies); } // Find weight of each machine in graph. // Weight of machine is calculated as sum of all weights of machines it depends on. // Nodes with no dependencies gets weight 0 while (!dependencies.isEmpty()) { int previousSize = dependencies.size(); for (Iterator<Map.Entry<String, Set<String>>> it = dependencies.entrySet().iterator(); it.hasNext();) { // process not yet processed machines only Map.Entry<String, Set<String>> serviceEntry = it.next(); String service = serviceEntry.getKey(); Set<String> serviceDependencies = serviceEntry.getValue(); if (serviceDependencies.isEmpty()) { // no links - smallest weight 0 weights.put(service, 0); it.remove(); } else { // machine has dependencies - check if it has not weighted dependencies if (weights.keySet().containsAll(serviceDependencies)) { // all connections are weighted - lets evaluate current machine Optional<String> maxWeight = serviceDependencies.stream() .max((o1, o2) -> weights.get(o1).compareTo(weights.get(o2))); // optional can't be empty because size of the list is checked above //noinspection OptionalGetWithoutIsPresent weights.put(service, weights.get(maxWeight.get()) + 1); it.remove(); } } } if (dependencies.size() == previousSize) { throw new IllegalArgumentException("Launch order of machines '" + Joiner.on(", ").join(dependencies.keySet()) + "' can't be evaluated. Circular dependency."); } } return weights; } /** * Parses link content into depends_on field representation - removes column and further chars */ private String getServiceFromLink(String link) throws IllegalArgumentException { String service = link; if (link != null) { String[] split = service.split(":"); if (split.length > 2) { throw new IllegalArgumentException(format("Service link '%s' is invalid", link)); } service = split[0]; } return service; } /** * Parses volumesFrom content into depends_on field representation - removes column and further chars */ private String getServiceFromVolumesFrom(String volumesFrom) throws IllegalArgumentException { String service = volumesFrom; if (volumesFrom != null) { String[] split = service.split(":"); if (split.length > 2) { throw new IllegalArgumentException(format("Service volumes_from '%s' is invalid", volumesFrom)); } service = split[0]; } return service; } private List<String> sortByWeight(Map<String, Integer> weights) { return weights.entrySet() .stream() .sorted((o1, o2) -> o1.getValue().compareTo(o2.getValue())) .map(Map.Entry::getKey) .collect(Collectors.toList()); } private void checkDependency(String dependency, String serviceName, Map<String, CheServiceImpl> services, String errorMessage) { if (serviceName.equals(dependency)) { throw new IllegalArgumentException(errorMessage + ": " + serviceName); } if (!services.containsKey(dependency)) { throw new IllegalArgumentException( format("Dependency '%s' in machine '%s' points to unknown machine.", dependency, serviceName)); } } }