server/src/jetbrains/buildServer/serverSide/priority/BuildQueuePriorityOrdering.java (244 lines of code) (raw):

package jetbrains.buildServer.serverSide.priority; import java.util.*; import java.util.stream.Collectors; import jetbrains.buildServer.serverSide.*; import org.apache.log4j.Logger; import org.jetbrains.annotations.NotNull; /** * BuildQueue ordering strategy based on build type priorities * @author dmitry.neverov */ public final class BuildQueuePriorityOrdering implements BuildQueueOrderingStrategy { private static final long DEFAULT_DURATION = 10 * 60;//10 minutes private final Logger myLogger = Logger.getLogger(BuildQueuePriorityOrdering.class.getName()); //Next 3 maps use SQueuedBuild.getItemId() as keys, because SQueuedBuild doesn't implement equals and hashCode: private final Map<String, Double> myItemWeights = new HashMap<String, Double>(); private final Map<String, Integer> myMovedItemsPriorities = new HashMap<String, Integer>(); private final Map<String, Integer> myPrioritiesOnTheInsertMoment = new HashMap<String, Integer>(); private List<SQueuedBuild> myLastResult = new ArrayList<SQueuedBuild>(); private final double myPriorityCoefficient; private final double myWaitCoefficient; private final PriorityClassManager myPriorityClassManager; private final BuildQueue myBuildQueue; public BuildQueuePriorityOrdering(@NotNull final BuildQueue queue, @NotNull final PriorityClassManager priorityClassManager) { myBuildQueue = queue; myPriorityClassManager = priorityClassManager; myPriorityCoefficient = parseDouble(TeamCityProperties.getProperty("teamcity.buildqueue.priorityWeight", "1.0")); myWaitCoefficient = parseDouble(TeamCityProperties.getProperty("teamcity.buildqueue.waitWeight", "1.0")); } @NotNull public synchronized List<SQueuedBuild> addBuilds(@NotNull final List<SQueuedBuild> itemsToAdd, @NotNull final List<SQueuedBuild> currentQueueItems) { if (!TeamCityProperties.getBooleanOrTrue("teamcity.buildQueue.priorityOrdering.enabled")) return Collections.emptyList(); try { clearDataOfRemovedItems(currentQueueItems); ensureHaveDataOnCurrentItems(currentQueueItems); updateWeights(currentQueueItems); addNewItems(itemsToAdd, currentQueueItems); myLastResult = new ArrayList<SQueuedBuild>(currentQueueItems); return currentQueueItems; } catch (Throwable t) { myLogger.error("Error while compute new queue order", t); return Collections.emptyList(); } } @Override public synchronized void restoreQueue(@NotNull final List<SQueuedBuild> queuedBuilds) { try { myItemWeights.clear(); myMovedItemsPriorities.clear(); myPrioritiesOnTheInsertMoment.clear(); myLastResult.clear(); final List<SQueuedBuild> result = new ArrayList<>(); for (SQueuedBuild item: queuedBuilds) { int buildTypePriority = getCurrentBuildTypePriority(item); double weight = myPriorityCoefficient * buildTypePriority; int position = getNewItemPosition(weight, result); result.add(position, item); myItemWeights.put(item.getItemId(), weight); myPrioritiesOnTheInsertMoment.put(item.getItemId(), buildTypePriority); } myLastResult = new ArrayList<SQueuedBuild>(result); } catch (Throwable t) { myLogger.error("Error while compute new queue order", t); } } private void addNewItems(@NotNull final List<SQueuedBuild> itemsToAdd, @NotNull final List<SQueuedBuild> currentQueueItems) { Set<String> buildIds = getIds(currentQueueItems); for (SQueuedBuild item: itemsToAdd) { if (buildIds.contains(item.getItemId())) { myLogger.info("The current queue items alredy contain the build " + item + ", don't add it to the priority order"); continue; } int buildTypePriority = getCurrentBuildTypePriority(item); double weight = myPriorityCoefficient * buildTypePriority; int position = getNewItemPosition(weight, currentQueueItems); currentQueueItems.add(position, item); myItemWeights.put(item.getItemId(), weight); myPrioritiesOnTheInsertMoment.put(item.getItemId(), buildTypePriority); logItemAdded(currentQueueItems, item, position, weight); } } private void logItemAdded(final List<SQueuedBuild> items, final SQueuedBuild item, final int position, final double weight) { final int defaultPosition = items.size() - 1; //default position is in the end of the queue, minus 1 because item already added if (myLogger.isDebugEnabled()) { myLogger.debug("Current item priorities: " + myItemWeights + ", new item " + item + " with weight " + weight + " inserted at position " + position); } else if (myLogger.isInfoEnabled()) { if (position != defaultPosition) { SQueuedBuild previousItem = null; if (position > 0) { previousItem = items.get(position - 1); } SQueuedBuild nextItem = items.get(position + 1); if (previousItem != null) { Double previousItemWeight = getItemWeight(previousItem.getItemId()); Double nextItemWeight = getItemWeight(nextItem.getItemId()); myLogger.info(String.format(Locale.ENGLISH, "New item %s with weight %.2f inserted at position %d instead of %d, between items %s (weight %.2f) and %s (weight %.2f)", item.toString(), weight, position, defaultPosition, previousItem, previousItemWeight, nextItem, nextItemWeight)); } else { Double nextItemWeight = getItemWeight(nextItem.getItemId()); myLogger.info(String.format(Locale.ENGLISH, "New item %s with weight %.2f inserted at position %d instead of %d, before item %s (weight %.2f)", item.toString(), weight, position, defaultPosition, nextItem, nextItemWeight)); } } else { myLogger.info(String.format(Locale.ENGLISH, "New item %s with weight %.2f inserted at the default position %d in the end of the queue", item.toString(), weight, position)); } } } /** * Get new item position according to it's weight and weights of other items * @param newItemWeight weight of new item * @param currentQueueItems current state of the queue * @return position there new item should be inserted */ private int getNewItemPosition(double newItemWeight, List<SQueuedBuild> currentQueueItems) { //move up until first item with higher or equal priority for (int i = currentQueueItems.size() - 1; i >= 0; i--) { if (newItemWeight <= getItemWeight(currentQueueItems.get(i).getItemId())) { return i + 1; } } return 0; } /** * Delete data about queued builds removed from queue. * When this method returns myItemWeights, myMovedItemsPriorities, myLastResult and myPrioritiesOnTheInsertMoment * contain only data for currentQueueItems. * @param currentQueueItems */ private void clearDataOfRemovedItems(@NotNull List<SQueuedBuild> currentQueueItems) { Set<String> currentItemIds = getIds(currentQueueItems); myItemWeights.keySet().retainAll(currentItemIds); myMovedItemsPriorities.keySet().retainAll(currentItemIds); myPrioritiesOnTheInsertMoment.keySet().retainAll(currentItemIds); List<SQueuedBuild> newResult = new ArrayList<>(); for (SQueuedBuild qb: myLastResult) { if (currentItemIds.contains(qb.getItemId())) { newResult.add(qb); } } myLastResult = newResult; } @NotNull private Set<String> getIds(@NotNull List<SQueuedBuild> currentQueueItems) { return currentQueueItems.stream().map(i -> i.getItemId()).collect(Collectors.toSet()); } //Should be called after clearDataOfRemovedItems() private void ensureHaveDataOnCurrentItems(List<SQueuedBuild> items) { for (SQueuedBuild item : items) { String itemId = item.getItemId(); Integer priority = myPrioritiesOnTheInsertMoment.get(itemId); if (priority == null) { priority = getCurrentBuildTypePriority(item); myLogger.warn("Cannot find priority of the item " + item + ", use default = " + priority); myPrioritiesOnTheInsertMoment.put(itemId, priority); } Double weight = myItemWeights.get(itemId); if (weight == null) { weight = myPriorityCoefficient * priority; myLogger.warn("Cannot find weight of the item " + item + ", use default = " + weight); myItemWeights.put(itemId, weight); } } } /** * Recalculate queued builds weights according to movements in the build queue and theirs wait times. * @param currentQueueItems current state of build queue */ private void updateWeights(List<SQueuedBuild> currentQueueItems) { updateMovedItemsPriorities(currentQueueItems); Date now = new Date(); for (Map.Entry<String, Double> entry: myItemWeights.entrySet()) { String itemId = entry.getKey(); SQueuedBuild queuedBuild = myBuildQueue.findQueued(itemId); if (queuedBuild != null) { entry.setValue(getItemWeightAtTheMoment(queuedBuild, now)); } else { throw new IllegalStateException(String.format("Cannot find queued build with itemId=%s", itemId)); } } } /** * Recalculate priorities of moved queued builds. Each moved queued build get priority of * queued build which place it holds in new order. * @param newQueueOrder new order of build queue */ private void updateMovedItemsPriorities(List<SQueuedBuild> newQueueOrder) { if (myLastResult.size() > newQueueOrder.size()) myLogger.warn("Wrong queued builds, last result: " + myLastResult + ", new order: " + newQueueOrder); if (!myLastResult.isEmpty()) { for (int i = 0; i < myLastResult.size(); i++) { SQueuedBuild lastResultItem = myLastResult.get(i); SQueuedBuild newOrderItem = newQueueOrder.get(i); if (!lastResultItem.getItemId().equals(newOrderItem.getItemId())) { myMovedItemsPriorities.put(newOrderItem.getItemId(), getBuildTypePriorityOnTheInsertMoment(lastResultItem)); } } } } /** * Get weight for queued item at the moment * @param item queued item * @param moment moment in time * @return weight for item at the moment */ private double getItemWeightAtTheMoment(SQueuedBuild item, Date moment) { double durationMillis = getDurationSeconds(item) * 1000.0; long waitMillis = moment.getTime() - item.getWhenQueued().getTime(); double waitPart = myWaitCoefficient * waitMillis / durationMillis; double configPart = myPriorityCoefficient * getEffectiveBuildTypePriority(item); if (Double.isNaN(waitPart)) { return configPart; } else { return waitPart + configPart; } } /** * Get queued item estimate duration in seconds * @param item queue item * @return queued item estimate duration or DEFAULT_DURATION if estimate duration could not be calculated */ private long getDurationSeconds(SQueuedBuild item) { BuildEstimates estimates = item.getBuildEstimates(); if (estimates == null) return DEFAULT_DURATION; TimeInterval timeInterval = estimates.getTimeInterval(); if (timeInterval == null) return DEFAULT_DURATION; Long duration = timeInterval.getDurationSeconds(); if (duration == null) return DEFAULT_DURATION; return duration == 0 ? 1 : duration; } /** * Get priority of queued build * @param item item of interest * @return priority of item's build type or if item was moved it's recomputed priority */ private int getEffectiveBuildTypePriority(SQueuedBuild item) { Integer movedItemPriority = myMovedItemsPriorities.get(item.getItemId()); if (movedItemPriority != null) { return movedItemPriority; } else { return getBuildTypePriorityOnTheInsertMoment(item); } } /** * Get item build type priority on the moment when it was added to the queue * @param item queued item * @return priority of item build type on the moment when it was added to the queue */ private int getBuildTypePriorityOnTheInsertMoment(SQueuedBuild item) { Integer priorityOnTheMomentOfInsert = myPrioritiesOnTheInsertMoment.get(item.getItemId()); if (priorityOnTheMomentOfInsert != null) { return priorityOnTheMomentOfInsert; } else { myLogger.error("Item " + item.toString() + " was added, but it's build type priority on the insert moment is lost"); return 0; } } private int getCurrentBuildTypePriority(SQueuedBuild item) { if (item.isPersonal()) { return myPriorityClassManager.getPersonalPriorityClass().getPriority(); } else { try { return myPriorityClassManager.getBuildTypePriorityClass(item.getBuildType()).getPriority(); } catch (BuildTypeNotFoundException e) { return 0; } } } /** * For tests only * @return current build queue items priorities */ Map<String, Double> getCurrentPriorities() { return myItemWeights; } public String toString() { return String.format("BuildQueuePriorityOrdering state: myItemWeights=%s, myMovedItemsPriorities=%s, myPrioritiesOnTheInsertMoment=%s, myLastResult=%s.", myItemWeights, myMovedItemsPriorities, myPrioritiesOnTheInsertMoment, myLastResult); } private double parseDouble(String priorityCoefficientString) { try { return Double.parseDouble(priorityCoefficientString); } catch (NumberFormatException e) { return 1.0; } } private Double getItemWeight(String itemId) { Double weight = myItemWeights.get(itemId); if (weight != null) { return weight; } else { myLogger.error("Item itemId=" + itemId + " was added, but it's weight is lost"); return 0.0; } } }