Skip to content
Permalink
Browse files

Merge pull request #38 from rschuetz/feature/JENKINS-42584-Upstream-j…

…ob-priority-ignored

[JENKINS-42584] upstream job priority ignored
  • Loading branch information
oleg-nenashev committed Jun 17, 2017
2 parents 20af6e4 + df09df3 commit ca2ba818d4c9b17533d9b550883a1064354f6f1b
@@ -4,4 +4,5 @@ work
*.iml
.settings
.project
.classpath
.classpath
/bin/
@@ -1,6 +1,5 @@
package jenkins.advancedqueue;


import hudson.Plugin;
import hudson.model.Job;
import hudson.model.Queue;
@@ -11,21 +10,29 @@

class PriorityConfigurationPlaceholderTaskHelper {

boolean isPlaceholderTask(Queue.Task task) {
return isPlaceholderTaskUsed() && task instanceof ExecutorStepExecution.PlaceholderTask;
}

PriorityConfigurationCallback getPriority(ExecutorStepExecution.PlaceholderTask task, PriorityConfigurationCallback priorityCallback) {
Job<?, ?> job = (Job<?, ?>) task.getOwnerTask();
ItemInfo itemInfo = QueueItemCache.get().getItem(job.getName());
itemInfo.getPriority();
priorityCallback.setPrioritySelection(itemInfo.getPriority());
return priorityCallback;
}

static boolean isPlaceholderTaskUsed() {
Plugin plugin = Jenkins.getInstance().getPlugin("workflow-durable-task-step");
return plugin != null && plugin.getWrapper().isActive();
}
boolean isPlaceholderTask(Queue.Task task) {
return isPlaceholderTaskUsed() && task instanceof ExecutorStepExecution.PlaceholderTask;
}

PriorityConfigurationCallback getPriority(ExecutorStepExecution.PlaceholderTask task,
PriorityConfigurationCallback priorityCallback) {
Job<?, ?> job = (Job<?, ?>) task.getOwnerTask();
ItemInfo itemInfo = QueueItemCache.get().getItem(job.getName());

// Can be null if job didn't run yet

if (itemInfo != null) {
priorityCallback.setPrioritySelection(itemInfo.getPriority());
} else {
priorityCallback.setPrioritySelection(PrioritySorterConfiguration.get().getStrategy().getDefaultPriority());
}

return priorityCallback;
}

static boolean isPlaceholderTaskUsed() {
Plugin plugin = Jenkins.getInstance().getPlugin("workflow-durable-task-step");
return plugin != null && plugin.getWrapper().isActive();
}

}
@@ -33,6 +33,7 @@
import jenkins.advancedqueue.PrioritySorterConfiguration;
import jenkins.advancedqueue.sorter.ItemInfo;
import jenkins.advancedqueue.sorter.QueueItemCache;
import jenkins.advancedqueue.sorter.StartedJobItemCache;

import org.kohsuke.stapler.DataBoundConstructor;

@@ -64,8 +65,10 @@ private UpstreamCause getUpstreamCause(Queue.Item item) {
}

public int getPriority(Queue.Item item) {
int upstreamBuildId = getUpstreamCause(item).getUpstreamBuild();
ItemInfo upstreamItem = QueueItemCache.get().getItem(upstreamBuildId);
UpstreamCause upstreamCause = getUpstreamCause(item);
String upstreamProject = upstreamCause.getUpstreamProject();
int upstreamBuildId = upstreamCause.getUpstreamBuild();
ItemInfo upstreamItem = StartedJobItemCache.get().getStartedItem(upstreamProject, upstreamBuildId);
// Upstream Item being null should be very very rare
if (upstreamItem != null) {
return upstreamItem.getPriority();
@@ -117,7 +117,7 @@ public int compare(BuildableItem o1, BuildableItem o2) {
* Returned the calculated, cached, weight or calculates the weight if missing. Should only be
* called when the value should already be there, if the item is new {@link #onNewItem(Item)} is
* the method to call.
*
*
* @param item the item to get the weight for
* @return the calculated weight
*/
@@ -147,6 +147,7 @@ public void onLeft(LeftItem li) {
prioritySorterStrategy.onCanceledItem(li);
logCanceledItem(itemInfo);
} else {
StartedJobItemCache.get().addItem(itemInfo, li.outcome.getPrimaryWorkUnit());
prioritySorterStrategy.onStartedItem(li, weight);
logStartedItem(itemInfo);
}
@@ -0,0 +1,155 @@
/*
* The MIT License
*
* Copyright (c) 2017, Ronny Schuetz
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*/
package jenkins.advancedqueue.sorter;

import java.util.Iterator;
import java.util.LinkedList;
import java.util.concurrent.TimeUnit;

import javax.annotation.CheckForNull;

import com.google.common.base.Objects;
import com.google.common.cache.Cache;
import com.google.common.cache.CacheBuilder;

import hudson.model.Run;
import hudson.model.Queue.Executable;
import hudson.model.queue.WorkUnit;

/**
* Keeps track of the Queue.Items seen by the Sorter, but removed from the queue
* to become jobs, for UpstreamCauseStrategy.
*
* @author Ronny Schuetz
* @since 3.6.0
*/
public class StartedJobItemCache {

private static final int RETENTION_COUNT = 10000;
private static final int RETENTION_TIME_HOURS = 12;

private static StartedJobItemCache startedJobItemCache = null;

static {
startedJobItemCache = new StartedJobItemCache();
}

public static StartedJobItemCache get() {
return startedJobItemCache;
}

private static class PendingItem {
final long startTime;
final ItemInfo itemInfo;
final WorkUnit workUnit;

public PendingItem(final ItemInfo itemInfo, final WorkUnit workUnit) {
this.startTime = System.currentTimeMillis();
this.itemInfo = itemInfo;
this.workUnit = workUnit;
}
}

private static class StartedItem {
final String projectName;
final int buildNumber;

public StartedItem(final String projectName, final int buildNumber) {
this.projectName = projectName;
this.buildNumber = buildNumber;
}

@Override
public int hashCode() {
return Objects.hashCode(projectName, buildNumber);
}

@Override
public boolean equals(final Object obj) {
if (obj == null)
return false;
if (getClass() != obj.getClass())
return false;
final StartedItem other = (StartedItem) obj;
return Objects.equal(this.projectName, other.projectName) && this.buildNumber == other.buildNumber;
}
}

private final LinkedList<PendingItem> pendingItems = new LinkedList<PendingItem>();

private final Cache<StartedItem, ItemInfo> startedItems = CacheBuilder.newBuilder()
.expireAfterWrite(RETENTION_TIME_HOURS, TimeUnit.HOURS).maximumSize(RETENTION_COUNT).build();

private StartedJobItemCache() {
}

/**
* Gets the Item for a started job, already removed from the queue
*
* @param projectName
* the project name
* @param buildNumber
* the build number
* @return the {@link ItemInfo} for the provided id or <code>null</code> if
* projectName/buildNumber combination is unknown
*/
public synchronized @CheckForNull ItemInfo getStartedItem(final String projectName, final int buildNumber) {
maintainCache();
return startedItems.getIfPresent(new StartedItem(projectName, buildNumber));
}

public synchronized void addItem(final ItemInfo itemInfo, final WorkUnit primaryWorkUnit) {
pendingItems.addLast(new PendingItem(itemInfo, primaryWorkUnit));
maintainCache();
}

private void maintainCache() {
// Collect job information from pending items to drop WorkUnit reference

for (final Iterator<PendingItem> it = pendingItems.iterator(); it.hasNext();) {
final PendingItem pi = it.next();
final Executable e = pi.workUnit.getExecutable();

if (e instanceof Run) {
startedItems.put(new StartedItem(pi.itemInfo.getJobName(), ((Run<?, ?>) e).getNumber()), pi.itemInfo);
it.remove();
}
}

// Cleanup pendingItems

if (pendingItems.size() > RETENTION_COUNT) {
pendingItems.subList(0, pendingItems.size() - RETENTION_COUNT).clear();
}

for (final Iterator<PendingItem> it = pendingItems.iterator(); it.hasNext();) {
final PendingItem pi = it.next();
if (pi.startTime < System.currentTimeMillis() - RETENTION_TIME_HOURS * 60 * 60 * 1000) {
it.remove();
} else {
break;
}
}
}
}
@@ -0,0 +1,73 @@
package jenkins.advancedqueue.test;

import java.lang.reflect.Constructor;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;

import org.junit.Rule;
import org.junit.Test;
import org.jvnet.hudson.test.JenkinsRule;
import org.jvnet.hudson.test.recipes.LocalData;

import hudson.model.Cause;
import hudson.model.Cause.UpstreamCause;
import hudson.model.Cause.UserIdCause;
import jenkins.advancedqueue.testutil.ExpectedItem;
import jenkins.advancedqueue.testutil.JobHelper;
import jenkins.advancedqueue.testutil.TestRunListener;

public class UpstreamTest {
@Rule
public JenkinsRule j = new JenkinsRule();

private final JobHelper jobHelper = new JobHelper(j);

@Test
@LocalData
public void testOrphanDownstreamJob() throws Exception {
// Job 0 should run with default priority, as upstream build is unknown
TestRunListener.init(new ExpectedItem("Job 0", 5));
jobHelper.scheduleProjects(createUpstreamCause("Job X", 987)).go();
j.waitUntilNoActivity();

TestRunListener.assertStartedItems();
}

@Test
@LocalData
public void testUserJobAndAssociatedDownstreamJob() throws Exception {
// Upstream job should run with high priority (user triggered)
TestRunListener.init(new ExpectedItem("Upstream", 1));
jobHelper.scheduleProject("Upstream", new UserIdCause()).go();
j.waitUntilNoActivity();

// Downstream job 1 should run with priority of upstream job build 1
TestRunListener.init(new ExpectedItem("Downstream1", 1));
jobHelper.scheduleProject("Downstream1", createUpstreamCause("Upstream", 1)).go();
j.waitUntilNoActivity();

// Downstream job 2 should run with priority of upstream job build 2 (not present, i.e. default priority
// should be used)
TestRunListener.init(new ExpectedItem("Downstream2", 5));
jobHelper.scheduleProject("Downstream2", createUpstreamCause("Upstream", 2)).go();
j.waitUntilNoActivity();

TestRunListener.assertStartedItems();
}

private UpstreamCause createUpstreamCause(final String upstreamProject, final int upstreamBuild) throws Exception {
final Class<?> clazz = UpstreamCause.class;
final Constructor<?>[] constructors = clazz.getDeclaredConstructors();

for (final Constructor<?> cons : constructors) {
if (Arrays.equals(cons.getParameterTypes(),
new Class<?>[] { String.class, int.class, String.class, List.class })) {
cons.setAccessible(true);
return (UpstreamCause) cons.newInstance(upstreamProject, upstreamBuild, "url",
Collections.<Cause>emptyList());
}
}
return null;
}
}
@@ -21,7 +21,7 @@
import org.jvnet.hudson.test.JenkinsRule;

public class JobHelper {

private final static Logger LOGGER = Logger.getLogger(JobHelper.class.getName());

public JenkinsRule j;
@@ -47,6 +47,12 @@ public boolean perform(AbstractBuild<?, ?> build, Launcher launcher, BuildListen
}
}

public FreeStyleProject createProject(String name) throws Exception {
FreeStyleProject project = j.createFreeStyleProject(name);
project.getBuildersList().add(new TestBuilder(100));
return project;
}

public List<FreeStyleProject> createProjects(int numberOfProjects) throws Exception {
List<FreeStyleProject> projects = new ArrayList<FreeStyleProject>(numberOfProjects);
for (int i = 0; i < numberOfProjects; i++) {
@@ -69,7 +75,7 @@ public boolean perform(AbstractBuild<?, ?> build, Launcher launcher, BuildListen
}
return projects;
}

public JobHelper scheduleMatrixProjects(Cause... causes) throws Exception {
List<MatrixProject> projects = createMatrixProjects(causes.length);
// Scheduling executors is zero
@@ -80,6 +86,14 @@ public JobHelper scheduleMatrixProjects(Cause... causes) throws Exception {
return this;
}

public JobHelper scheduleProject(String name, Cause cause) throws Exception {
FreeStyleProject project = createProject(name);
// Scheduling executors is zero
project.scheduleBuild(0, cause);
Thread.sleep(100);
return this;
}

public JobHelper scheduleProjects(Cause... causes) throws Exception {
List<FreeStyleProject> projects = createProjects(causes.length);
// Scheduling executors is zero
@@ -94,7 +108,7 @@ public void go() throws Exception {
// Set the executors to one and restart
Jenkins.getInstance().setNumExecutors(1);
// TODO: is there any other way to make the 1 take effect than a reload?
Jenkins.getInstance().reload();
Jenkins.getInstance().reload();
}

}

0 comments on commit ca2ba81

Please sign in to comment.
You can’t perform that action at this time.