/* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF 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. */ package org.apache.brooklyn.entity.group.zoneaware; import java.util.Collection; import java.util.Collections; import java.util.Comparator; import java.util.List; import java.util.Map; import org.apache.brooklyn.api.entity.Entity; import org.apache.brooklyn.api.location.Location; import org.apache.brooklyn.core.entity.trait.Startable; import org.apache.brooklyn.entity.group.DynamicCluster.NodePlacementStrategy; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import com.google.common.base.Predicates; import com.google.common.collect.ImmutableList; import com.google.common.collect.Iterables; import com.google.common.collect.Lists; import com.google.common.collect.Maps; import com.google.common.collect.Multimap; /** * Default node placement strategy: attempts to keep the number of nodes balanced across the available locations. */ public class BalancingNodePlacementStrategy implements NodePlacementStrategy { private static final Logger LOG = LoggerFactory.getLogger(BalancingNodePlacementStrategy.class); @Override public List<Location> locationsForAdditions(Multimap<Location, Entity> currentMembers, Collection<? extends Location> locs, int numToAdd) { if (locs.isEmpty() && numToAdd > 0) { throw new IllegalArgumentException("No locations supplied, when requesting locations for "+numToAdd+" nodes"); } List<Location> result = Lists.newArrayList(); Map<Location, Integer> locSizes = toMutableLocationSizes(currentMembers, locs); for (int i = 0; i < numToAdd; i++) { // TODO Inefficient to loop this many times! But not called with big numbers. Location leastPopulatedLoc = null; int leastPopulatedLocSize = 0; for (Location loc : locs) { int locSize = locSizes.get(loc); if (leastPopulatedLoc == null || locSize < leastPopulatedLocSize) { leastPopulatedLoc = loc; leastPopulatedLocSize = locSize; } } assert leastPopulatedLoc != null : "leastPopulatedLoc=null; locs="+locs+"; currentMembers="+currentMembers; result.add(leastPopulatedLoc); locSizes.put(leastPopulatedLoc, locSizes.get(leastPopulatedLoc)+1); } return result; } @Override public List<Entity> entitiesToRemove(Multimap<Location, Entity> currentMembers, int numToRemove) { if (currentMembers.isEmpty()) { throw new IllegalArgumentException("No members supplied, when requesting removal of "+numToRemove+" nodes"); } if (currentMembers.size() < numToRemove) { LOG.warn("Request to remove "+numToRemove+" when only "+currentMembers.size()+" members (continuing): "+currentMembers); numToRemove = currentMembers.size(); } Map<Location, Integer> numToRemovePerLoc = Maps.newLinkedHashMap(); Map<Location, Integer> locSizes = toMutableLocationSizes(currentMembers, ImmutableList.<Location>of()); for (int i = 0; i < numToRemove; i++) { // TODO Inefficient to loop this many times! But not called with big numbers. Location mostPopulatedLoc = null; int mostPopulatedLocSize = 0; for (Location loc : locSizes.keySet()) { int locSize = locSizes.get(loc); if (locSize > 0 && (mostPopulatedLoc == null || locSize > mostPopulatedLocSize)) { mostPopulatedLoc = loc; mostPopulatedLocSize = locSize; } } assert mostPopulatedLoc != null : "leastPopulatedLoc=null; currentMembers="+currentMembers; numToRemovePerLoc.put(mostPopulatedLoc, ((numToRemovePerLoc.get(mostPopulatedLoc) == null) ? 0 : numToRemovePerLoc.get(mostPopulatedLoc))+ 1); locSizes.put(mostPopulatedLoc, locSizes.get(mostPopulatedLoc)-1); } List<Entity> result = Lists.newArrayList(); for (Map.Entry<Location, Integer> entry : numToRemovePerLoc.entrySet()) { result.addAll(pickNewest(currentMembers.get(entry.getKey()), entry.getValue())); } return result; } protected Map<Location,Integer> toMutableLocationSizes(Multimap<Location, Entity> currentMembers, Iterable<? extends Location> otherLocs) { Map<Location,Integer> result = Maps.newLinkedHashMap(); for (Location key : currentMembers.keySet()) { result.put(key, currentMembers.get(key).size()); } for (Location otherLoc : otherLocs) { if (!result.containsKey(otherLoc)) { result.put(otherLoc, 0); } } return result; } protected Collection<Entity> pickNewest(Collection<Entity> contenders, Integer numToPick) { // choose newest entity that is stoppable; sort so newest is first List<Entity> stoppables = Lists.newLinkedList(Iterables.filter(contenders, Predicates.instanceOf(Startable.class))); Collections.sort(stoppables, new Comparator<Entity>() { @Override public int compare(Entity a, Entity b) { return (int) (b.getCreationTime() - a.getCreationTime()); } }); return stoppables.subList(0, Math.min(numToPick, stoppables.size())); } }