/* * Copyright 2014-2015 the original author or authors. * * Licensed 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. */ package org.springframework.xd.dirt.server.admin.deployment; import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; import java.util.Collections; import java.util.List; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.context.expression.MapAccessor; import org.springframework.expression.EvaluationException; import org.springframework.expression.spel.SpelEvaluationException; import org.springframework.expression.spel.SpelMessage; import org.springframework.expression.spel.standard.SpelExpressionParser; import org.springframework.expression.spel.support.StandardEvaluationContext; import org.springframework.util.Assert; import org.springframework.util.StringUtils; import org.springframework.xd.dirt.cluster.Container; import org.springframework.xd.dirt.cluster.ContainerFilter; import org.springframework.xd.module.ModuleDeploymentProperties; import org.springframework.xd.module.ModuleDescriptor; /** * Implementation of a Container matching strategy that returns a collection of containers to deploy a * {@link ModuleDescriptor} to. This implementation examines the deployment properties for a stream to determine * the preferences for each individual module. The deployment properties can (optionally) specify two preferences: * <em>criteria</em> and <em>count</em>. * <p/> * The criteria indicates that a module should only be deployed to a container for which the criteria evaluates to * {@code true}. The criteria value should be a valid SpEL expression. If a criteria value is not provided, any * container can deploy the module. * <p/> * If a count for a module is not specified, by default one instance of that module will be deployed to one container. A * count of 0 indicates that all containers for which the criteria evaluates to {@code true} should deploy the module. * If no criteria expression is specified, all containers will deploy the module. * <p/> * In cases where all containers are not deploying a module, an attempt at container round robin distribution for module * deployments will be made (but not guaranteed). * * @author Patrick Peralta * @author Mark Fisher * @author David Turanski * @author Ilayaperumal Gopinathan */ public class ContainerMatcher { /** * Logger. */ private static final Logger logger = LoggerFactory.getLogger(ContainerMatcher.class); /** * Current index for iterating over containers. */ private int index; /** * Parser for criteria expressions. */ private final SpelExpressionParser expressionParser = new SpelExpressionParser(); /** * Evaluation context for criteria expressions. */ private final StandardEvaluationContext evaluationContext = new StandardEvaluationContext(); /** * Collection of {@link ContainerFilter}s to apply to the candidate Containers. */ private final Collection<ContainerFilter> containerFilters; /** * Creates a container matcher instance and prepares the SpEL evaluation context to support Map properties directly. */ public ContainerMatcher() { this(new ContainerFilter[0]); } /** * Creates a container matcher instance with the provided {@link ContainerFilter}s * and prepares the SpEL evaluation context to support Map properties directly. */ public ContainerMatcher(ContainerFilter... containerFilters) { this.containerFilters = (containerFilters != null) ? Collections.unmodifiableList(Arrays.asList(containerFilters)) : Collections.<ContainerFilter>emptyList(); evaluationContext.addPropertyAccessor(new MapAccessor()); } /** * Matches the provided module against one of the candidate containers. * * @param moduleDescriptor the module to match against * @param deploymentProperties deployment properties for the module; this provides * hints such as the number of containers and other * matching criteria * @param containers iterable list of containers to match against * * @return a collection of matched containers; collection is empty if no suitable containers are found */ public Collection<Container> match(ModuleDescriptor moduleDescriptor, ModuleDeploymentProperties deploymentProperties, Iterable<Container> containers) { Assert.notNull(moduleDescriptor, "'moduleDescriptor' cannot be null."); Assert.notNull(deploymentProperties, "'deploymentProperties' cannot be null."); Assert.notNull(containers, "'containers' cannot be null."); logger.debug("Matching containers for module {}", moduleDescriptor); String criteria = deploymentProperties.getCriteria(); List<Container> candidates = findAllContainersMatchingCriteria(containers, criteria); Collection<Container> filteredContainers = applyFilters(moduleDescriptor, candidates); List<Container> results = new ArrayList<Container>(filteredContainers); if (results.isEmpty() && StringUtils.hasText(criteria)) { logger.warn("No currently available containers match deployment criteria '{}' for module '{}'.", criteria, moduleDescriptor.getModuleName()); } return distributeForRequestedCount(results, deploymentProperties.getCount()); } /** * Apply all the available {@link ContainerFilter}s. * * @param moduleDescriptor the module descriptor for the module * @param candidates the collection of available containers that match the selection criteria * @return the container candidates after applying the {@link ContainerFilter}s */ private Collection<Container> applyFilters(ModuleDescriptor moduleDescriptor, Collection<Container> candidates) { for (ContainerFilter containerFilter : containerFilters) { candidates = containerFilter.filterContainers(moduleDescriptor, candidates); } return candidates; } /** * Select a subset of containers to satisfy the requested number of module instances using a round robin * distribution for successive calls. A count of 0 means all members that matched the criteria expression. count >= * candidates means each of the candidates should host a module. * * @param candidates the list of available containers that match the selection criteria * @param count the requested number of module instances to deploy * @return a subset of candidates <= count */ private Collection<Container> distributeForRequestedCount(List<Container> candidates, int count) { int candidateCount = candidates.size(); if (candidateCount == 0) { return candidates; } if (count <= 0 || count >= candidateCount) { return candidates; } else if (count == 1) { return Collections.singleton(candidates.get(getAndRotateIndex(candidateCount))); } else { // create a new list with the specific number of targeted containers; List<Container> targets = new ArrayList<Container>(); while (targets.size() < count) { targets.add(candidates.get(getAndRotateIndex(candidateCount))); } return targets; } } /** * Test all containers in the containerRepository against selection criteria * * @param containers the containers of Iterator type to match against * @param criteria an optional SpEL expression evaluated against container attribute values * * @return the list of containers matching the criteria */ private List<Container> findAllContainersMatchingCriteria(Iterable<Container> containers, String criteria) { if (StringUtils.hasText(criteria)) { logger.debug("Matching containers for criteria '{}'", criteria); } List<Container> candidates = new ArrayList<Container>(); for (Container container : containers) { logger.trace("Evaluating container {}", container); if (StringUtils.isEmpty(criteria) || isCandidate(container, criteria)) { logger.trace("\tAdded container {}", container); candidates.add(container); } } return candidates; } /** * Evaluate the criteria expression against the attributes of the provided container to see if it is a candidate for * module deployment. * * @param container the container instance whose attributes should be considered * @param criteria the criteria expression to evaluate against the container attributes * @return whether the container is a candidate */ private boolean isCandidate(Container container, String criteria) { try { return expressionParser.parseExpression(criteria).getValue(evaluationContext, container.getAttributes(), Boolean.class); } catch (SpelEvaluationException e) { if (e.getMessageCode().equals(SpelMessage.PROPERTY_OR_FIELD_NOT_READABLE)) { logger.debug("candidate does not contain an attribute referenced in the criteria {}", criteria); } return false; } catch (EvaluationException e) { logger.debug("candidate not a match due to evaluation exception", e); return false; } } /** * Rotate the cached index over the number of available containers. * * @param availableContainerCount the number of available containers * @return the current count before rotating */ private synchronized int getAndRotateIndex(int availableContainerCount) { if (availableContainerCount <= 0) { return 0; } int i = index % availableContainerCount; index = i + 1; return i; } }