Skip to content

Commit

Permalink
[JENKINS-35112] Re-implement based on JENKINS-35160 impl in core
Browse files Browse the repository at this point in the history
  • Loading branch information
stephenc committed Mar 10, 2017
1 parent d43aa4d commit 7d0c781
Show file tree
Hide file tree
Showing 4 changed files with 352 additions and 60 deletions.
174 changes: 141 additions & 33 deletions src/main/java/com/cloudbees/hudson/plugins/folder/AbstractFolder.java
Expand Up @@ -38,24 +38,11 @@
import hudson.XmlFile;
import hudson.init.InitMilestone;
import hudson.init.Initializer;
import hudson.model.AbstractItem;
import hudson.model.Action;
import hudson.model.AllView;
import hudson.model.Descriptor;
import hudson.model.HealthReport;
import hudson.model.Item;
import hudson.model.ItemGroup;
import hudson.model.ItemGroupMixIn;
import hudson.model.Items;
import hudson.model.Job;
import hudson.model.ModifiableViewGroup;
import hudson.model.Run;
import hudson.model.TaskListener;
import hudson.model.TopLevelItem;
import hudson.model.View;
import hudson.model.ViewGroupMixIn;
import hudson.model.*;
import hudson.model.listeners.ItemListener;
import hudson.model.listeners.RunListener;
import hudson.model.queue.Tasks;
import hudson.model.queue.WorkUnit;
import hudson.search.CollectionSearchIndex;
import hudson.search.SearchIndexBuilder;
import hudson.search.SearchItem;
Expand All @@ -82,6 +69,8 @@
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Random;
Expand Down Expand Up @@ -120,6 +109,7 @@
import org.kohsuke.stapler.interceptor.RequirePOST;

import static hudson.Util.fixEmpty;
import static hudson.model.queue.Executables.getParentOf;

/**
* A general-purpose {@link ItemGroup}.
Expand Down Expand Up @@ -1199,28 +1189,146 @@ public void onDeleted(I item) throws IOException {
public void delete() throws IOException, InterruptedException {
// Some parts copied from AbstractItem.
checkPermission(DELETE);
// delete individual items first
// (disregard whether they would be deletable in isolation)
// JENKINS-34939: do not hold the monitor on this folder while deleting them
// (thus we cannot do this inside performDelete)
SecurityContext orig = ACL.impersonate(ACL.SYSTEM);
// TODO remove once baseline core has JENKINS-35160
boolean responsibleForAbortingBuilds = !ItemDeletion.contains(this);
boolean ownsRegistration = ItemDeletion.register(this);
if (!ownsRegistration && ItemDeletion.isRegistered(this)) {
// we are not the owning thread and somebody else is concurrently deleting this exact item
throw new Failure(Messages.AbstractFolder_BeingDeleted(getPronoun()));
}
try {
for (Item i : new ArrayList<Item>(items.values())) {
try {
i.delete();
} catch (AbortException e) {
throw (AbortException) new AbortException(
"Failed to delete " + i.getFullDisplayName() + " : " + e.getMessage()).initCause(e);
} catch (IOException e) {
throw new IOException("Failed to delete " + i.getFullDisplayName(), e);
// if a build is in progress. Cancel it.
if (responsibleForAbortingBuilds || ownsRegistration) {
Queue queue = Queue.getInstance();
if (this instanceof Queue.Task) {
// clear any items in the queue so they do not get picked up
queue.cancel((Queue.Task) this);
}
// now cancel any child items - this happens after ItemDeletion registration, so we can use a snapshot
for (Queue.Item i : queue.getItems()) {
Item item = ItemDeletion.getItemOf(i.task);
while (item != null) {
if (item == this) {
queue.cancel(i);
break;
}
if (item.getParent() instanceof Item) {
item = (Item) item.getParent();
} else {
break;
}
}
}
// interrupt any builds in progress (and this should be a recursive test so that folders do not pay
// the 15 second delay for every child item). This happens after queue cancellation, so will be
// a complete set of builds in flight
Map<Executor, Queue.Executable> buildsInProgress = new LinkedHashMap<>();
for (Computer c : Jenkins.getActiveInstance().getComputers()) {
for (Executor e : c.getExecutors()) {
WorkUnit workUnit = e.getCurrentWorkUnit();
if (workUnit != null) {
Item item = ItemDeletion.getItemOf(getParentOf(workUnit.getExecutable()));
if (item != null) {
while (item != null) {
if (item == this) {
buildsInProgress.put(e, e.getCurrentExecutable());
e.interrupt(Result.ABORTED);
break;
}
if (item.getParent() instanceof Item) {
item = (Item) item.getParent();
} else {
break;
}
}
}
}
}
for (Executor e : c.getOneOffExecutors()) {
WorkUnit workUnit = e.getCurrentWorkUnit();
if (workUnit != null) {
Item item = ItemDeletion.getItemOf(getParentOf(workUnit.getExecutable()));
if (item != null) {
while (item != null) {
if (item == this) {
buildsInProgress.put(e, e.getCurrentExecutable());
e.interrupt(Result.ABORTED);
break;
}
if (item.getParent() instanceof Item) {
item = (Item) item.getParent();
} else {
break;
}
}
}
}
}
}
if (!buildsInProgress.isEmpty()) {
// give them 15 seconds or so to respond to the interrupt
long expiration = System.nanoTime() + TimeUnit.SECONDS.toNanos(15);
// comparison with executor.getCurrentExecutable() == computation currently should always be true
// as we no longer recycle Executors, but safer to future-proof in case we ever revisit recycling
while (!buildsInProgress.isEmpty() && expiration - System.nanoTime() > 0L) {
// we know that ItemDeletion will prevent any new builds in the queue
// ItemDeletion happens-before Queue.cancel so we know that the Queue will stay clear
// Queue.cancel happens-before collecting the buildsInProgress list
// thus buildsInProgress contains the complete set we need to interrupt and wait for
for (Iterator<Map.Entry<Executor, Queue.Executable>> iterator =
buildsInProgress.entrySet().iterator();
iterator.hasNext(); ) {
Map.Entry<Executor, Queue.Executable> entry = iterator.next();
// comparison with executor.getCurrentExecutable() == executable currently should always be
// true as we no longer recycle Executors, but safer to future-proof in case we ever
// revisit recycling.
if (!entry.getKey().isAlive()
|| entry.getValue() != entry.getKey().getCurrentExecutable()) {
iterator.remove();
}
// I don't know why, but we have to keep interrupting
entry.getKey().interrupt(Result.ABORTED);
}
Thread.sleep(50L);
}
if (!buildsInProgress.isEmpty()) {
throw new Failure(Messages.AbstractFolder_FailureToStopBuilds(
buildsInProgress.size(), getFullDisplayName()
));
}
}
}
// END remove once baseline core has JENKINS-35160

// delete individual items first
// (disregard whether they would be deletable in isolation)
// JENKINS-34939: do not hold the monitor on this folder while deleting them
// (thus we cannot do this inside performDelete)
SecurityContext orig = ACL.impersonate(ACL.SYSTEM);
try {
for (Item i : new ArrayList<Item>(items.values())) {
try {
i.delete();
} catch (AbortException e) {
throw (AbortException) new AbortException(
"Failed to delete " + i.getFullDisplayName() + " : " + e.getMessage()).initCause(e);
} catch (IOException e) {
throw new IOException("Failed to delete " + i.getFullDisplayName(), e);
}
}
} finally {
SecurityContextHolder.setContext(orig);
}
synchronized (this) {
performDelete();
}
// TODO remove once baseline core has JENKINS-35160
} finally {
SecurityContextHolder.setContext(orig);
}
synchronized (this) {
performDelete();
if (ownsRegistration) {
ItemDeletion.deregister(this);
}
}
// END remove once baseline core has JENKINS-35160
getParent().onDeleted(AbstractFolder.this);
Jenkins.getActiveInstance().rebuildDependencyGraphAsync();
}
Expand Down

0 comments on commit 7d0c781

Please sign in to comment.