/******************************************************************************* * * Copyright (c) 2010, InfraDNA, Inc. * * 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: * * * * *******************************************************************************/ package hudson.model.queue; import com.google.common.collect.ImmutableList; import com.google.common.collect.Iterables; import hudson.matrix.MatrixConfiguration; import hudson.model.AbstractProject; import hudson.model.Computer; import hudson.model.Executor; import hudson.model.Hudson; import hudson.model.ItemGroup; import hudson.model.JobProperty; import hudson.model.Label; import hudson.model.LoadBalancer; import hudson.model.Node; import hudson.model.Queue.BuildableItem; import hudson.model.Queue.Executable; import hudson.model.Queue.JobOffer; import hudson.model.Queue.Task; import hudson.model.labels.LabelAssignmentAction; import java.io.IOException; import static java.lang.Math.*; import java.util.AbstractList; import java.util.ArrayList; import java.util.Collection; import java.util.HashMap; import java.util.LinkedHashMap; import java.util.List; import java.util.Map; import java.util.Map.Entry; import org.eclipse.hudson.security.team.Team; import org.eclipse.hudson.security.team.TeamManager; /** * Defines a mapping problem for answering "where do we execute this task?" * * <p> The heart of the placement problem is a mapping problem. We are given a * {@link Task}, (which in the general case consists of a set of * {@link SubTask}s), and we are also given a number of idle {@link Executor}s, * and our goal is to find a mapping from the former to the latter, which * determines where each {@link SubTask} gets executed. * * <p> This mapping is done under two constraints: * * <ul> <li> "Same node" constraint. Some of the subtasks need to be co-located * on the same node. See {@link SubTask#getSameNodeConstraint()} <li> Label * constraint. {@link SubTask}s can specify that it can be only run on nodes * that has the label. </ul> * * <p> We first fold the former constraint into the problem definition. That is, * we now consider a set of {@link SubTask}s that need to be co-located as a * single {@link WorkChunk}. Similarly, we consider a set of all * {@link Executor}s from the same node as {@link ExecutorChunk}. Now, the * problem becomes the weighted matching problem from {@link WorkChunk} to * {@link ExecutorChunk}. * * <p> An instance of {@link MappingWorksheet} captures a problem definition, * plus which {@link ExecutorChunk} and {@link WorkChunk} are compatible. The * purpose of this class (and {@link ExecutorChunk} and {@link WorkChunk}) are * to expose a lot of convenience methods to assist various algorithms that * produce the solution of this mapping problem, which is represented as * {@link Mapping}. * * @see LoadBalancer#map(Task, MappingWorksheet) * @author Kohsuke Kawaguchi */ public class MappingWorksheet { //TODO: review and check whether we can do it private public final List<ExecutorChunk> executors; public final List<WorkChunk> works; /** * {@link BuildableItem} for which we are trying to figure out the execution * plan. Never null. */ public final BuildableItem item; public List<ExecutorChunk> getExecutors() { return executors; } public List<WorkChunk> getWorks() { return works; } public BuildableItem getItem() { return item; } private static class ReadOnlyList<E> extends AbstractList<E> { protected final List<E> base; ReadOnlyList(List<E> base) { this.base = base; } public E get(int index) { return base.get(index); } public int size() { return base.size(); } } public final class ExecutorChunk extends ReadOnlyList<ExecutorSlot> { //TODO: review and check whether we can do it private public final int index; public final Computer computer; public final Node node; private ExecutorChunk(List<ExecutorSlot> base, int index) { super(base); this.index = index; assert !base.isEmpty(); computer = base.get(0).getExecutor().getOwner(); node = computer.getNode(); } public int getIndex() { return index; } public Computer getComputer() { return computer; } public Node getNode() { return node; } /** * Is this executor chunk and the given work chunk compatible? Can the * latter be run on the former? */ public boolean canAccept(WorkChunk c) { return this.size() >= c.size() && (c.assignedLabel == null || c.assignedLabel.contains(node)); } /** * Node name. */ public String getName() { return node.getNodeName(); } /** * Number of executors in this chunk. Alias for size but more readable. */ public int capacity() { return size(); } private void execute(WorkChunk wc, WorkUnitContext wuc) { assert capacity() >= wc.size(); int e = 0; for (SubTask s : wc) { while (!get(e).isAvailable()) { e++; } get(e++).set(wuc.createWorkUnit(s)); } } } public class WorkChunk extends ReadOnlyList<SubTask> { public final int index; // the main should be always at position 0 // /** // * This chunk includes {@linkplain WorkUnit#isMainWork() the main work unit}. // */ // public final boolean isMain; /** * If this task needs to be run on a node with a particular label, * return that {@link Label}. Otherwise null, indicating it can run on * anywhere. */ public final Label assignedLabel; /** * If the previous execution of this task run on a certain node and this * task prefers to run on the same node, return that. Otherwise null. */ public final ExecutorChunk lastBuiltOn; private WorkChunk(List<SubTask> base, int index) { super(base); assert !base.isEmpty(); this.index = index; this.assignedLabel = getAssignedLabel(base.get(0)); Node lbo = base.get(0).getLastBuiltOn(); for (ExecutorChunk ec : executors) { if (ec.node == lbo) { lastBuiltOn = ec; return; } } lastBuiltOn = null; } private Label getAssignedLabel(SubTask task) { for (LabelAssignmentAction laa : item.getActions(LabelAssignmentAction.class)) { Label l = laa.getAssignedLabel(task); if (l!=null) return l; } return task.getAssignedLabel(); } public List<ExecutorChunk> applicableExecutorChunks() { List<ExecutorChunk> r = new ArrayList<ExecutorChunk>(executors.size()); for (ExecutorChunk e : executors) { if (e.canAccept(this)) { r.add(e); } } return r; } } /** * Represents the solution to the mapping problem. It's a mapping from every * {@link WorkChunk} to {@link ExecutorChunk} that satisfies the * constraints. */ public final class Mapping { // for each WorkChunk, identify ExecutorChunk where it is assigned to. private final ExecutorChunk[] mapping = new ExecutorChunk[works.size()]; /** * {@link ExecutorChunk} assigned to the n-th work chunk. */ public ExecutorChunk assigned(int n) { return mapping[n]; } /** * n-th {@link WorkChunk}. */ public WorkChunk get(int n) { return works.get(n); } /** * Update the mapping to execute n-th {@link WorkChunk} on the specified * {@link ExecutorChunk}. */ public ExecutorChunk assign(int index, ExecutorChunk element) { ExecutorChunk o = mapping[index]; mapping[index] = element; return o; } /** * Number of {@link WorkUnit}s that require assignments. */ public int size() { return mapping.length; } /** * Returns the assignment as a map. */ public Map<WorkChunk, ExecutorChunk> toMap() { Map<WorkChunk, ExecutorChunk> r = new HashMap<WorkChunk, ExecutorChunk>(); for (int i = 0; i < size(); i++) { r.put(get(i), assigned(i)); } return r; } /** * Checks if the assignments made thus far are valid an within the * constraints. */ public boolean isPartiallyValid() { int[] used = new int[executors.size()]; for (int i = 0; i < mapping.length; i++) { ExecutorChunk ec = mapping[i]; if (ec == null) { continue; } if (!ec.canAccept(works(i))) { return false; // invalid assignment } if ((used[ec.index] += works(i).size()) > ec.capacity()) { return false; } } return true; } /** * Makes sure that all the assignments are made and it is within the * constraints. */ public boolean isCompletelyValid() { for (ExecutorChunk ec : mapping) { if (ec == null) { return false; // unassigned } } return isPartiallyValid(); } /** * Executes this mapping by handing over {@link Executable}s to * {@link JobOffer} as defined by the mapping. */ public void execute(WorkUnitContext wuc) { if (!isCompletelyValid()) { throw new IllegalStateException(); } for (int i = 0; i < size(); i++) { assigned(i).execute(get(i), wuc); } } } public MappingWorksheet(BuildableItem item, List<? extends ExecutorSlot> offers) { this(item, offers, LoadPredictor.all()); } public MappingWorksheet(BuildableItem item, List<? extends ExecutorSlot> offers, Collection<? extends LoadPredictor> loadPredictors) { this.item = item; // group executors by their computers Map<Computer, List<ExecutorSlot>> j = new HashMap<Computer, List<ExecutorSlot>>(); for (ExecutorSlot o : offers) { Computer c = o.getExecutor().getOwner(); boolean canBuildInNode = true; TeamManager teamManager = Hudson.getInstance().getTeamManager(); if (teamManager.isTeamManagementEnabled()) { String name = c.getName(); if (c instanceof Hudson.MasterComputer) { name = "Master"; } String jobName = item.task.getName(); //All jobs must be part of a team, if not this task item may be a sub-job. //Try to find the parent job that belongs to the team Team jobTeam = teamManager.findJobOwnerTeam(jobName); if (jobTeam == null) { AbstractProject jobTask = null; if (item.task instanceof AbstractProject) { jobTask = (AbstractProject) item.task; } if (jobTask != null) { do { // Check if the parent of this sub-job is a team job if (jobTask.getParent() instanceof AbstractProject) { jobTask = (AbstractProject) jobTask.getParent(); jobName = jobTask.getName(); jobTeam = teamManager.findJobOwnerTeam(jobName); } else if (jobTask.getParent() instanceof ItemGroup) { ItemGroup itemGroup = (ItemGroup) jobTask.getParent(); // This item group may be added as Job property to parent if (itemGroup instanceof JobProperty) { JobProperty property = (JobProperty) itemGroup; if (property.getOwner() instanceof AbstractProject) { jobTask = (AbstractProject) property.getOwner(); jobName = jobTask.getName(); jobTeam = teamManager.findJobOwnerTeam(jobName); } } } else { break; } } while (jobTeam == null); } } // If job team is still null do a last ditch test if (jobTeam == null) { if (item.task instanceof MatrixConfiguration) { MatrixConfiguration matrixConfiguration = (MatrixConfiguration) item.task; jobName = matrixConfiguration.getParent().getName(); } } canBuildInNode = teamManager.canNodeExecuteJob(name, jobName); } List<ExecutorSlot> l = j.get(c); if (l == null) { j.put(c, l = new ArrayList<ExecutorSlot>()); } if (canBuildInNode) { l.add(o); } } {// take load prediction into account and reduce the available executor pool size accordingly long duration = item.task.getEstimatedDuration(); if (duration > 0) { long now = System.currentTimeMillis(); for (Entry<Computer, List<ExecutorSlot>> e : j.entrySet()) { final List<ExecutorSlot> list = e.getValue(); final int max = e.getKey().countExecutors(); // build up the prediction model. cut the chase if we hit the max. Timeline timeline = new Timeline(); int peak = 0; OUTER: for (LoadPredictor lp : loadPredictors) { for (FutureLoad fl : Iterables.limit(lp.predict(this, e.getKey(), now, now + duration), 100)) { peak = max(peak, timeline.insert(fl.startTime, fl.startTime + fl.duration, fl.numExecutors)); if (peak >= max) { break OUTER; } } } int minIdle = max - peak; // minimum number of idle nodes during this time period if (minIdle < list.size()) { e.setValue(list.subList(0, minIdle)); } } } } // build into the final shape List<ExecutorChunk> executors = new ArrayList<ExecutorChunk>(); for (List<ExecutorSlot> group : j.values()) { if (group.isEmpty()) { continue; // evict empty group } ExecutorChunk ec = new ExecutorChunk(group, executors.size()); if (ec.node == null) { continue; // evict out of sync node } executors.add(ec); } this.executors = ImmutableList.copyOf(executors); // group execution units into chunks. use of LinkedHashMap ensures that the main work comes at the top Map<Object, List<SubTask>> m = new LinkedHashMap<Object, List<SubTask>>(); for (SubTask meu : Tasks.getSubTasksOf(item.task)) { Object c = Tasks.getSameNodeConstraintOf(meu); if (c == null) { c = new Object(); } List<SubTask> l = m.get(c); if (l == null) { m.put(c, l = new ArrayList<SubTask>()); } l.add(meu); } // build into the final shape List<WorkChunk> works = new ArrayList<WorkChunk>(); for (List<SubTask> group : m.values()) { works.add(new WorkChunk(group, works.size())); } this.works = ImmutableList.copyOf(works); } public WorkChunk works(int index) { return works.get(index); } public ExecutorChunk executors(int index) { return executors.get(index); } public static abstract class ExecutorSlot { public abstract Executor getExecutor(); public abstract boolean isAvailable(); protected abstract void set(WorkUnit p) throws UnsupportedOperationException; } }