package hudson.plugins.throttleconcurrents; import hudson.Extension; import hudson.matrix.MatrixConfiguration; import hudson.model.AbstractDescribableImpl; import hudson.model.Descriptor; import hudson.model.Item; import hudson.model.ItemGroup; import hudson.model.Job; import hudson.model.JobProperty; import hudson.model.JobPropertyDescriptor; import hudson.model.Queue; import hudson.model.Run; import hudson.model.TaskListener; import hudson.util.FormValidation; import hudson.util.ListBoxModel; import hudson.Util; import hudson.matrix.MatrixProject; import hudson.matrix.MatrixRun; import java.io.IOException; import java.util.Arrays; import java.util.ArrayList; import java.util.Collection; import java.util.Collections; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.TreeMap; import java.util.WeakHashMap; import java.util.concurrent.CopyOnWriteArrayList; import java.util.logging.Level; import java.util.logging.Logger; import javax.annotation.CheckForNull; import javax.annotation.Nonnull; import jenkins.model.Jenkins; import net.sf.json.JSONObject; import org.apache.commons.lang.ArrayUtils; import org.apache.commons.lang.StringUtils; import org.jenkinsci.plugins.workflow.flow.FlowExecution; import org.jenkinsci.plugins.workflow.flow.FlowExecutionOwner; import org.jenkinsci.plugins.workflow.graph.FlowNode; import org.kohsuke.stapler.DataBoundConstructor; import org.kohsuke.stapler.QueryParameter; import org.kohsuke.stapler.StaplerRequest; public class ThrottleJobProperty extends JobProperty<Job<?,?>> { // Replaced by categories, to support, well, multiple categories per job (starting from 1.3) @Deprecated transient String category; private Integer maxConcurrentPerNode; private Integer maxConcurrentTotal; private List<String> categories; private boolean throttleEnabled; private String throttleOption; private boolean limitOneJobWithMatchingParams; private transient boolean throttleConfiguration; private @CheckForNull ThrottleMatrixProjectOptions matrixOptions; private String paramsToUseForLimit; private transient List<String> paramsToCompare; /** * Store a config version so we're able to migrate config on various * functionality upgrades. */ private Long configVersion; @DataBoundConstructor public ThrottleJobProperty(Integer maxConcurrentPerNode, Integer maxConcurrentTotal, List<String> categories, boolean throttleEnabled, String throttleOption, boolean limitOneJobWithMatchingParams, String paramsToUseForLimit, @CheckForNull ThrottleMatrixProjectOptions matrixOptions ) { this.maxConcurrentPerNode = maxConcurrentPerNode; this.maxConcurrentTotal = maxConcurrentTotal; this.categories = categories == null ? new CopyOnWriteArrayList<String>() : new CopyOnWriteArrayList<String>(categories); this.throttleEnabled = throttleEnabled; this.throttleOption = throttleOption; this.limitOneJobWithMatchingParams = limitOneJobWithMatchingParams; this.matrixOptions = matrixOptions; this.paramsToUseForLimit = paramsToUseForLimit; if ((this.paramsToUseForLimit != null)) { if ((this.paramsToUseForLimit.length() > 0)) { this.paramsToCompare = Arrays.asList(ArrayUtils.nullToEmpty(StringUtils.split(this.paramsToUseForLimit))); } else { this.paramsToCompare = new ArrayList<String>(); } } else { this.paramsToCompare = new ArrayList<String>(); } } /** * Migrates deprecated/obsolete data. * * @return Migrated version of the config */ public Object readResolve() { if (configVersion == null) { configVersion = 0L; } if (categories == null) { categories = new CopyOnWriteArrayList<String>(); } if (category != null) { categories.add(category); category = null; } if (configVersion < 1 && throttleOption == null) { if (categories.isEmpty()) { throttleOption = "project"; } else { throttleOption = "category"; maxConcurrentPerNode = 0; maxConcurrentTotal = 0; } } configVersion = 1L; // Handle the throttleConfiguration in custom builds (not released) if (throttleConfiguration && matrixOptions == null) { matrixOptions = new ThrottleMatrixProjectOptions(false, true); } return this; } @Override protected void setOwner(Job<?,?> owner) { super.setOwner(owner); if (throttleEnabled && categories != null) { DescriptorImpl descriptor = (DescriptorImpl) getDescriptor(); synchronized (descriptor.propertiesByCategoryLock) { for (String c : categories) { Map<ThrottleJobProperty,Void> properties = descriptor.propertiesByCategory.get(c); if (properties == null) { properties = new WeakHashMap<ThrottleJobProperty,Void>(); descriptor.propertiesByCategory.put(c, properties); } properties.put(this, null); } } } } public boolean getThrottleEnabled() { return throttleEnabled; } public boolean isLimitOneJobWithMatchingParams() { return limitOneJobWithMatchingParams; } public String getThrottleOption() { return throttleOption; } public List<String> getCategories() { return categories; } public Integer getMaxConcurrentPerNode() { if (maxConcurrentPerNode == null) maxConcurrentPerNode = 0; return maxConcurrentPerNode; } public Integer getMaxConcurrentTotal() { if (maxConcurrentTotal == null) maxConcurrentTotal = 0; return maxConcurrentTotal; } public String getParamsToUseForLimit() { return paramsToUseForLimit; } @CheckForNull public ThrottleMatrixProjectOptions getMatrixOptions() { return matrixOptions; } /** * Check if the build throttles {@link MatrixProject}s. * @return {@code true} if {@link MatrixProject}s should be throttled * @since 1.8.3 */ public boolean isThrottleMatrixBuilds() { return matrixOptions != null ? matrixOptions.isThrottleMatrixBuilds() : ThrottleMatrixProjectOptions.DEFAULT.isThrottleMatrixBuilds(); } /** * Check if the build throttles {@link MatrixConfiguration}s. * @return {@code true} if {@link MatrixRun}s should be throttled * @since 1.8.3 */ public boolean isThrottleMatrixConfigurations() { return matrixOptions != null ? matrixOptions.isThrottleMatrixConfigurations() : ThrottleMatrixProjectOptions.DEFAULT.isThrottleMatrixConfigurations(); } public List<String> getParamsToCompare() { if (paramsToCompare == null) { if ((paramsToUseForLimit != null)) { if ((paramsToUseForLimit.length() > 0)) { paramsToCompare = Arrays.asList(paramsToUseForLimit.split(",")); } else { paramsToCompare = new ArrayList<String>(); } } else { paramsToCompare = new ArrayList<String>(); } } return paramsToCompare; } /** * Get the list of categories for a given run ID (from {@link Run#getExternalizableId()}) and flow node ID (from * {@link FlowNode#getId()}, if that run/flow node combination is recorded for one or more categories. * * @param runId The run ID * @param flowNodeId The flow node ID * @return A list of category names. May be empty. */ @Nonnull static List<String> getCategoriesForRunAndFlowNode(@Nonnull String runId, @Nonnull String flowNodeId) { List<String> categories = new ArrayList<>(); final DescriptorImpl descriptor = fetchDescriptor(); for (ThrottleCategory cat : descriptor.getCategories()) { Map<String,List<String>> runs = descriptor.getThrottledPipelinesForCategory(cat.getCategoryName()); if (!runs.isEmpty() && runs.containsKey(runId) && runs.get(runId).contains(flowNodeId)) { categories.add(cat.getCategoryName()); } } return categories; } /** * Get all {@link Queue.Task}s with {@link ThrottleJobProperty}s attached to them. * * @param category a non-null string, the category name. * @return A list of {@link Queue.Task}s with {@link ThrottleJobProperty} attached. */ static List<Queue.Task> getCategoryTasks(@Nonnull String category) { assert !StringUtils.isEmpty(category); List<Queue.Task> categoryTasks = new ArrayList<Queue.Task>(); Collection<ThrottleJobProperty> properties; DescriptorImpl descriptor = fetchDescriptor(); synchronized (descriptor.propertiesByCategoryLock) { Map<ThrottleJobProperty, Void> _properties = descriptor.propertiesByCategory.get(category); properties = _properties != null ? new ArrayList<ThrottleJobProperty>(_properties.keySet()) : Collections.<ThrottleJobProperty>emptySet(); } for (ThrottleJobProperty t : properties) { if (t.getThrottleEnabled()) { if (t.getCategories() != null && t.getCategories().contains(category)) { Job<?, ?> p = t.owner; if (/*is a task*/ p instanceof Queue.Task && /* not deleted */getItem(p.getParent(), p.getName()) == p && /* has not since been reconfigured */ p.getProperty(ThrottleJobProperty.class) == t) { categoryTasks.add((Queue.Task) p); if (p instanceof MatrixProject && t.isThrottleMatrixConfigurations()) { for (MatrixConfiguration mc : ((MatrixProject) p).getActiveConfigurations()) { categoryTasks.add(mc); } } } } } } return categoryTasks; } /** * Gets a map of IDs for {@link Run}s to a list of {@link FlowNode}s currently running for a given category. Removes any * no longer valid run/flow node combinations from the internal tracking for that category, due to the run not being * found, the run not being a {@link FlowExecutionOwner.Executable}, the run no longer building, etc * * @param category The category name to look for. * @return a map of IDs for {@link Run}s to lists of {@link FlowNode}s for this category, if any. May be empty. */ @Nonnull static Map<String,List<FlowNode>> getThrottledPipelineRunsForCategory(@Nonnull String category) { Map<String,List<FlowNode>> throttledPipelines = new TreeMap<>(); final DescriptorImpl descriptor = fetchDescriptor(); for (Map.Entry<String,List<String>> currentPipeline : descriptor.getThrottledPipelinesForCategory(category).entrySet()) { Run<?, ?> flowNodeRun = Run.fromExternalizableId(currentPipeline.getKey()); List<FlowNode> flowNodes = new ArrayList<>(); if (flowNodeRun == null || !(flowNodeRun instanceof FlowExecutionOwner.Executable) || !flowNodeRun.isBuilding()) { descriptor.removeAllFromPipelineRunForCategory(currentPipeline.getKey(), category, null); } else { FlowExecutionOwner executionOwner = ((FlowExecutionOwner.Executable) flowNodeRun).asFlowExecutionOwner(); if (executionOwner != null) { FlowExecution execution = executionOwner.getOrNull(); if (execution == null) { descriptor.removeAllFromPipelineRunForCategory(currentPipeline.getKey(), category, null); } else { for (String flowNodeId : currentPipeline.getValue()) { try { FlowNode node = execution.getNode(flowNodeId); if (node != null) { flowNodes.add(node); } else { descriptor.removeThrottledPipelineForCategory(currentPipeline.getKey(), flowNodeId, category, null); } } catch (IOException e) { // do nothing } } } } } if (!flowNodes.isEmpty()) { throttledPipelines.put(currentPipeline.getKey(), flowNodes); } } return throttledPipelines; } private static Item getItem(ItemGroup group, String name) { if (group instanceof Jenkins) { return ((Jenkins) group).getItemMap().get(name); } else { return group.getItem(name); } } public static DescriptorImpl fetchDescriptor() { return Jenkins.getActiveInstance().getDescriptorByType(DescriptorImpl.class); } @Extension public static final class DescriptorImpl extends JobPropertyDescriptor { private static final Logger LOGGER = Logger.getLogger(DescriptorImpl.class.getName()); private List<ThrottleCategory> categories; private Map<String,Map<String,List<String>>> throttledPipelinesByCategory; /** Map from category names, to properties including that category. */ private transient Map<String,Map<ThrottleJobProperty,Void>> propertiesByCategory = new HashMap<String,Map<ThrottleJobProperty,Void>>(); /** A sync object for {@link #propertiesByCategory} */ private final transient Object propertiesByCategoryLock = new Object(); public DescriptorImpl() { super(ThrottleJobProperty.class); synchronized(propertiesByCategoryLock) { load(); // Explictly handle the persisted data from the version 1.8.1 if (propertiesByCategory == null) { propertiesByCategory = new HashMap<String,Map<ThrottleJobProperty,Void>>(); } if (!propertiesByCategory.isEmpty()) { propertiesByCategory.clear(); save(); // Save the configuration to remove obsolete data } } } @Override public String getDisplayName() { return "Throttle Concurrent Builds"; } @Override @SuppressWarnings("rawtypes") public boolean isApplicable(Class<? extends Job> jobType) { return Job.class.isAssignableFrom(jobType) && Queue.Task.class.isAssignableFrom(jobType); } public boolean isMatrixProject(Job job) { return job instanceof MatrixProject; } @Override public boolean configure(StaplerRequest req, JSONObject formData) throws FormException { req.bindJSON(this, formData); save(); return true; } public FormValidation doCheckCategoryName(@QueryParameter String value) { if (Util.fixEmptyAndTrim(value) == null) { return FormValidation.error("Empty category names are not allowed."); } else { return FormValidation.ok(); } } public FormValidation doCheckMaxConcurrentPerNode(@QueryParameter String value) { return checkNullOrInt(value); } private FormValidation checkNullOrInt(String value) { // Allow nulls - we'll just translate those to 0s. if (Util.fixEmptyAndTrim(value) != null) { return FormValidation.validateNonNegativeInteger(value); } else { return FormValidation.ok(); } } public FormValidation doCheckMaxConcurrentTotal(@QueryParameter String value) { return checkNullOrInt(value); } public ThrottleCategory getCategoryByName(String categoryName) { ThrottleCategory category = null; for (ThrottleCategory tc : categories) { if (tc.getCategoryName().equals(categoryName)) { category = tc; } } return category; } public void setCategories(List<ThrottleCategory> categories) { this.categories = new CopyOnWriteArrayList<ThrottleCategory>(categories); } public List<ThrottleCategory> getCategories() { if (categories == null) { categories = new CopyOnWriteArrayList<ThrottleCategory>(); } return categories; } public ListBoxModel doFillCategoryItems() { ListBoxModel m = new ListBoxModel(); m.add("(none)", ""); for (ThrottleCategory tc : getCategories()) { m.add(tc.getCategoryName()); } return m; } @Override public void load() { super.load(); initThrottledPipelines(); LOGGER.log(Level.FINE, "load: {0}", throttledPipelinesByCategory); } private synchronized void initThrottledPipelines() { if (throttledPipelinesByCategory == null) { throttledPipelinesByCategory = new TreeMap<>(); } } @Override public void save() { super.save(); LOGGER.log(Level.FINE, "save: {0}", throttledPipelinesByCategory); } @Nonnull public synchronized Map<String,List<String>> getThrottledPipelinesForCategory(@Nonnull String category) { return internalGetThrottledPipelinesForCategory(category); } @Nonnull private Map<String,List<String>> internalGetThrottledPipelinesForCategory(@Nonnull String category) { if (getCategoryByName(category) != null) { if (throttledPipelinesByCategory.containsKey(category)) { return throttledPipelinesByCategory.get(category); } } return new TreeMap<>(); } public synchronized void addThrottledPipelineForCategory(@Nonnull String runId, @Nonnull String flowNodeId, @Nonnull String category, TaskListener listener) { if (getCategoryByName(category) == null) { if (listener != null) { listener.getLogger().println(Messages.ThrottleJobProperty_DescriptorImpl_NoSuchCategory(category)); } } else { Map<String,List<String>> currentPipelines = internalGetThrottledPipelinesForCategory(category); List<String> flowNodes = currentPipelines.get(runId); if (flowNodes == null) { flowNodes = new ArrayList<>(); } flowNodes.add(flowNodeId); currentPipelines.put(runId, flowNodes); throttledPipelinesByCategory.put(category, currentPipelines); } } public synchronized void removeThrottledPipelineForCategory(@Nonnull String runId, @Nonnull String flowNodeId, @Nonnull String category, TaskListener listener) { if (getCategoryByName(category) == null) { if (listener != null) { listener.getLogger().println(Messages.ThrottleJobProperty_DescriptorImpl_NoSuchCategory(category)); } } else { Map<String,List<String>> currentPipelines = internalGetThrottledPipelinesForCategory(category); if (!currentPipelines.isEmpty()) { List<String> flowNodes = currentPipelines.get(runId); if (flowNodes != null && flowNodes.contains(flowNodeId)) { flowNodes.remove(flowNodeId); } if (flowNodes != null && !flowNodes.isEmpty()) { currentPipelines.put(runId, flowNodes); } else { currentPipelines.remove(runId); } } if (currentPipelines.isEmpty()) { throttledPipelinesByCategory.remove(category); } else { throttledPipelinesByCategory.put(category, currentPipelines); } } } public synchronized void removeAllFromPipelineRunForCategory(@Nonnull String runId, @Nonnull String category, TaskListener listener) { if (getCategoryByName(category) == null) { if (listener != null) { listener.getLogger().println(Messages.ThrottleJobProperty_DescriptorImpl_NoSuchCategory(category)); } } else { Map<String,List<String>> currentPipelines = internalGetThrottledPipelinesForCategory(category); if (!currentPipelines.isEmpty()) { if (currentPipelines.containsKey(runId)) { currentPipelines.remove(runId); } } if (currentPipelines.isEmpty()) { throttledPipelinesByCategory.remove(category); } else { throttledPipelinesByCategory.put(category, currentPipelines); } } } } public static final class ThrottleCategory extends AbstractDescribableImpl<ThrottleCategory> { private Integer maxConcurrentPerNode; private Integer maxConcurrentTotal; private String categoryName; private List<NodeLabeledPair> nodeLabeledPairs; @DataBoundConstructor public ThrottleCategory(String categoryName, Integer maxConcurrentPerNode, Integer maxConcurrentTotal, List<NodeLabeledPair> nodeLabeledPairs) { this.maxConcurrentPerNode = maxConcurrentPerNode; this.maxConcurrentTotal = maxConcurrentTotal; this.categoryName = categoryName; this.nodeLabeledPairs = nodeLabeledPairs == null ? new ArrayList<NodeLabeledPair>() : nodeLabeledPairs; } public Integer getMaxConcurrentPerNode() { if (maxConcurrentPerNode == null) maxConcurrentPerNode = 0; return maxConcurrentPerNode; } public Integer getMaxConcurrentTotal() { if (maxConcurrentTotal == null) maxConcurrentTotal = 0; return maxConcurrentTotal; } public String getCategoryName() { return categoryName; } public List<NodeLabeledPair> getNodeLabeledPairs() { if (nodeLabeledPairs == null) nodeLabeledPairs = new ArrayList<NodeLabeledPair>(); return nodeLabeledPairs; } @Extension public static class DescriptorImpl extends Descriptor<ThrottleCategory> { @Override public String getDisplayName() { return ""; } } } /** * @author marco.miller@ericsson.com */ public static final class NodeLabeledPair extends AbstractDescribableImpl<NodeLabeledPair> { private String throttledNodeLabel; private Integer maxConcurrentPerNodeLabeled; @DataBoundConstructor public NodeLabeledPair(String throttledNodeLabel, Integer maxConcurrentPerNodeLabeled) { this.throttledNodeLabel = throttledNodeLabel == null ? "" : throttledNodeLabel; this.maxConcurrentPerNodeLabeled = maxConcurrentPerNodeLabeled; } public String getThrottledNodeLabel() { if(throttledNodeLabel == null) { throttledNodeLabel = ""; } return throttledNodeLabel; } public Integer getMaxConcurrentPerNodeLabeled() { if(maxConcurrentPerNodeLabeled == null) { maxConcurrentPerNodeLabeled = 0; } return maxConcurrentPerNodeLabeled; } @Extension public static class DescriptorImpl extends Descriptor<NodeLabeledPair> { @Override public String getDisplayName() { return ""; } } } }