/** * 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.apache.aurora.scheduler.metadata; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.Objects; import java.util.Set; import java.util.stream.Collectors; import java.util.stream.StreamSupport; import javax.inject.Inject; import com.google.common.annotations.VisibleForTesting; import com.google.common.base.Functions; import com.google.common.base.Ticker; import com.google.common.cache.CacheBuilder; import com.google.common.cache.CacheLoader; import com.google.common.cache.LoadingCache; import com.google.common.collect.ImmutableSet; import com.google.common.collect.Iterables; import com.google.common.eventbus.Subscribe; import org.apache.aurora.GuavaUtils; import org.apache.aurora.common.quantity.Amount; import org.apache.aurora.common.quantity.Time; import org.apache.aurora.gen.ScheduleStatus; import org.apache.aurora.scheduler.base.TaskGroupKey; import org.apache.aurora.scheduler.base.Tasks; import org.apache.aurora.scheduler.events.PubsubEvent.EventSubscriber; import org.apache.aurora.scheduler.events.PubsubEvent.TaskStateChange; import org.apache.aurora.scheduler.events.PubsubEvent.TasksDeleted; import org.apache.aurora.scheduler.events.PubsubEvent.Vetoed; import org.apache.aurora.scheduler.filter.SchedulingFilter.Veto; import org.apache.aurora.scheduler.scheduling.TaskGroup; /** * Tracks vetoes against scheduling decisions and maintains the closest fit among all the vetoes * for a task. */ public class NearestFit implements EventSubscriber { @VisibleForTesting static final Amount<Long, Time> EXPIRATION = Amount.of(10L, Time.MINUTES); @VisibleForTesting static final ImmutableSet<Veto> NO_VETO = ImmutableSet.of(); private final LoadingCache<TaskGroupKey, Fit> fitByGroupKey; public NearestFit(Ticker ticker) { fitByGroupKey = CacheBuilder.newBuilder() .expireAfterWrite(EXPIRATION.getValue(), EXPIRATION.getUnit().getTimeUnit()) .ticker(ticker) .build(new CacheLoader<TaskGroupKey, Fit>() { @Override public Fit load(TaskGroupKey groupKey) { return new Fit(); } }); } @Inject NearestFit() { this(Ticker.systemTicker()); } /** * Gets the vetoes that represent the nearest fit for the given task. * * @param groupKey The task group key to look up. * @return The nearest fit vetoes for the given task. This will return an empty set if * no vetoes have been recorded for the task. */ public synchronized ImmutableSet<Veto> getNearestFit(TaskGroupKey groupKey) { Fit fit = fitByGroupKey.getIfPresent(groupKey); return (fit == null) ? NO_VETO : fit.vetoes; } /** * Records a task deletion event. * * @param deletedEvent Task deleted event. */ @Subscribe public synchronized void remove(TasksDeleted deletedEvent) { fitByGroupKey.invalidateAll(Iterables.transform(deletedEvent.getTasks(), Functions.compose( TaskGroupKey::from, Tasks::getConfig))); } /** * Records a task state change event. * This will ignore any events where the previous state is not {@link ScheduleStatus#PENDING}. * * @param event Task state change. */ @Subscribe public synchronized void stateChanged(TaskStateChange event) { if (event.isTransition() && event.getOldState().get() == ScheduleStatus.PENDING) { fitByGroupKey.invalidate(TaskGroupKey.from(event.getTask().getAssignedTask().getTask())); } } /** * Records a task veto event. * * @param vetoEvent Veto event. */ @Subscribe public synchronized void vetoed(Vetoed vetoEvent) { Objects.requireNonNull(vetoEvent); fitByGroupKey.getUnchecked(vetoEvent.getGroupKey()).maybeUpdate(vetoEvent.getVetoes()); } /** * Determine the pending reason, for each of the given tasks in taskGroups. * * @param taskGroups Group of pending tasks. * @return A map with key=TaskGroupKey and value=List of reasons. */ public synchronized Map<TaskGroupKey, List<String>> getPendingReasons( Iterable<TaskGroup> taskGroups) { return StreamSupport.stream(taskGroups.spliterator(), false).map(t -> { List<String> reasons = getNearestFit(t.getKey()).stream() .map(Veto::getReason).collect(Collectors.toList()); return new HashMap.SimpleEntry<>(t.getKey(), reasons); }).collect(GuavaUtils.toImmutableMap(Map.Entry::getKey, Map.Entry::getValue)); } private static class Fit { private ImmutableSet<Veto> vetoes; private static int score(Iterable<Veto> vetoes) { int total = 0; for (Veto veto : vetoes) { total += veto.getScore(); } return total; } private void update(Iterable<Veto> newVetoes) { vetoes = ImmutableSet.copyOf(newVetoes); } /** * Updates the nearest fit if the provided vetoes represents a closer fit than the current * best fit. * <p> * Vetoes with a lower aggregate score are considered a better fit regardless of the total veto * count. See {@link Veto} for more details on scoring differences. * @param newVetoes The vetoes for the scheduling assignment with {@code newHost}. */ void maybeUpdate(Set<Veto> newVetoes) { if (vetoes == null) { update(newVetoes); return; } if (score(newVetoes) < score(vetoes)) { update(newVetoes); } } } }