Skip to content

Commit

Permalink
Merge commit 'ca2ba818d4c9b17533d9b550883a1064354f6f1b' into JENKINS-…
Browse files Browse the repository at this point in the history
…44014

Conflicts:
	src/main/java/jenkins/advancedqueue/PriorityConfigurationPlaceholderTaskHelper.java
	src/main/java/jenkins/advancedqueue/sorter/AdvancedQueueSorter.java
  • Loading branch information
oleg-nenashev committed Jun 17, 2017
2 parents 35c7a22 + ca2ba81 commit a564afc
Show file tree
Hide file tree
Showing 12 changed files with 401 additions and 20 deletions.
3 changes: 2 additions & 1 deletion .gitignore
Expand Up @@ -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;
Expand All @@ -16,18 +15,21 @@ class PriorityConfigurationPlaceholderTaskHelper {

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

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

@Nonnull
PriorityConfigurationCallback getPriority(@Nonnull ExecutorStepExecution.PlaceholderTask task, @Nonnull PriorityConfigurationCallback priorityCallback) {
Queue.Task ownerTask = task.getOwnerTask();
if (ownerTask instanceof Job<?, ?>) {
Job<?, ?> job = (Job<?, ?>) ownerTask;
ItemInfo itemInfo = QueueItemCache.get().getItem(job.getName());
itemInfo.getPriority();
priorityCallback.setPrioritySelection(itemInfo.getPriority());
if (itemInfo != null) {
priorityCallback.setPrioritySelection(itemInfo.getPriority());
} else {
priorityCallback.setPrioritySelection(PrioritySorterConfiguration.get().getStrategy().getDefaultPriority());
}
} else {
if (LOGGER.isLoggable(Level.FINE)) {
LOGGER.log(Level.FINE, "Cannot determine priority of the Pipeline Placeholder Task {0}. Its owner task {1} is not a Job (type is {2}). " +
Expand All @@ -38,9 +40,9 @@ PriorityConfigurationCallback getPriority(@Nonnull ExecutorStepExecution.Placeho
return priorityCallback;
}

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

}
Expand Up @@ -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;

Expand Down Expand Up @@ -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();
Expand Down
Expand Up @@ -118,7 +118,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
*/
Expand Down Expand Up @@ -154,6 +154,7 @@ public void onLeft(@Nonnull LeftItem li) {
logCanceledItem(itemInfo);
} else {
Float weight = itemInfo.getWeight();
StartedJobItemCache.get().addItem(itemInfo, li.outcome.getPrimaryWorkUnit());
prioritySorterStrategy.onStartedItem(li, weight);
logStartedItem(itemInfo);
}
Expand Down
2 changes: 1 addition & 1 deletion src/main/java/jenkins/advancedqueue/sorter/ItemInfo.java
Expand Up @@ -153,7 +153,7 @@ public int compareTo(ItemInfo o) {
if(this.getSortableInQueueSince() == o.getSortableInQueueSince()) {
return new Integer(this.getItemId()).compareTo(o.getItemId());
}
return new Long(this.getInQueueSince()).compareTo(o.getInQueueSince());
return new Long(this.getSortableInQueueSince()).compareTo(o.getSortableInQueueSince());
}
return Float.compare(this.getWeight(), o.getWeight());
}
Expand Down
Expand Up @@ -79,8 +79,10 @@ synchronized public ItemInfo getItem(int itemId) {
* Get the ItemInfo for the last knows start of this Job Name
*
* @param jobName a name of a Job
* @return the {@link ItemInfo} for the last know start of the Job
* @return the {@link ItemInfo} for the last know start of the Job.
* Can be {@code null} if job didn't run yet
*/
@CheckForNull
synchronized public ItemInfo getItem(String jobName) {
return jobName2info.get(jobName);
}
Expand Down
155 changes: 155 additions & 0 deletions src/main/java/jenkins/advancedqueue/sorter/StartedJobItemCache.java
@@ -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;
}
}
}
}
73 changes: 73 additions & 0 deletions src/test/java/jenkins/advancedqueue/test/UpstreamTest.java
@@ -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;
}
}

0 comments on commit a564afc

Please sign in to comment.