Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Removing AbstractFolder.delete override #356

Merged
merged 6 commits into from
Jan 18, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@
<changelist>999999-SNAPSHOT</changelist>
<gitHubRepo>jenkinsci/${project.artifactId}-plugin</gitHubRepo>
<!-- remember to change the io.jenkins.tools.bom artifact when changing this -->
<jenkins.version>2.387.3</jenkins.version>
<jenkins.version>2.433</jenkins.version>
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not yet in an LTS version of course. I have no strong opinion as to whether we should wait for that or not.

<no-test-jar>false</no-test-jar>
<hpi.compatibleSinceVersion>5.2</hpi.compatibleSinceVersion>
</properties>
Expand Down Expand Up @@ -57,8 +57,8 @@
<dependencies>
<dependency>
<groupId>io.jenkins.tools.bom</groupId>
<artifactId>bom-2.387.x</artifactId>
<version>2543.vfb_1a_5fb_9496d</version>
<artifactId>bom-2.426.x</artifactId>
<version>2555.v3190a_8a_c60c6</version>
<scope>import</scope>
<type>pom</type>
</dependency>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,6 @@
import com.cloudbees.hudson.plugins.folder.views.AbstractFolderViewHolder;
import com.cloudbees.hudson.plugins.folder.views.DefaultFolderViewHolder;
import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;
import hudson.AbortException;
import hudson.BulkChange;
import hudson.Extension;
import hudson.Util;
Expand All @@ -43,9 +42,7 @@
import hudson.model.AbstractItem;
import hudson.model.Action;
import hudson.model.AllView;
import hudson.model.Computer;
import hudson.model.Descriptor;
import hudson.model.Executor;
import hudson.model.Failure;
import hudson.model.HealthReport;
import hudson.model.Item;
Expand All @@ -55,22 +52,16 @@
import hudson.model.Job;
import hudson.model.ModifiableViewGroup;
import hudson.model.Queue;
import hudson.model.Result;
import hudson.model.Run;
import hudson.model.TaskListener;
import hudson.model.TopLevelItem;
import hudson.model.View;
import hudson.model.ViewGroupMixIn;
import hudson.model.listeners.ItemListener;
import hudson.model.listeners.RunListener;
import static hudson.model.queue.Executables.getParentOf;
import hudson.model.queue.Tasks;
import hudson.model.queue.WorkUnit;
import hudson.search.CollectionSearchIndex;
import hudson.search.SearchIndexBuilder;
import hudson.search.SearchItem;
import hudson.security.ACL;
import hudson.security.ACLContext;
import hudson.util.AlternativeUiTextProvider;
import hudson.util.CopyOnWriteMap;
import hudson.util.DescribableList;
Expand All @@ -89,8 +80,6 @@
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 All @@ -110,7 +99,6 @@
import jenkins.model.ModelObjectWithChildren;
import jenkins.model.ProjectNamingStrategy;
import jenkins.model.TransientActionFactory;
import jenkins.model.queue.ItemDeletion;
import net.sf.json.JSONObject;
import org.acegisecurity.AccessDeniedException;
import org.apache.commons.io.FileUtils;
Expand Down Expand Up @@ -1087,137 +1075,6 @@ public void onDeleted(I item) throws IOException {
save();
}

/**
* {@inheritDoc}
*/
@Override
public void delete() throws IOException, InterruptedException {
// Some parts copied from AbstractItem.
checkPermission(DELETE);
// TODO JENKINS-35160 core API is not reusable: we need part of AbstractItem.delete but with the deletion of children interjected (and not inside a monitor)
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 {
// 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 = Tasks.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.get().getComputers()) {
for (Executor e : c.getAllExecutors()) {
WorkUnit workUnit = e.getCurrentWorkUnit();
if (workUnit != null) {
Queue.Executable executable = workUnit.getExecutable();
if (executable != null) {
Item item = Tasks.getItemOf(getParentOf(executable));
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 of attempted reuse from 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)
try (ACLContext oldContext = ACL.as(ACL.SYSTEM)) {
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);
}
}
}
synchronized (this) {
performDelete();
}
// TODO start of attempted reuse of JENKINS-35160
} finally {
if (ownsRegistration) {
ItemDeletion.deregister(this);
}
}
// END of attempted reuse of JENKINS-35160
getParent().onDeleted(AbstractFolder.this);
Jenkins.get().rebuildDependencyGraphAsync();
}

/**
* Is this folder disabled. A disabled folder should have all child items disabled.
*
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,3 @@
AbstractFolder.BeingDeleted={0} is currently being deleted
AbstractFolder.FailureToStopBuilds=Failed to interrupt and stop {0,choice,1#{0,number,integer} build|1<{0,number,\
integer} builds} of {1}
Folder.DisplayName=Folder
Folder.DefaultPronoun=Item
Folder.Description=Creates a container that stores nested items in it. Useful for grouping things together. Unlike \
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,3 @@
AbstractFolder.BeingDeleted={0} wird gerade gelöscht
AbstractFolder.FailureToStopBuilds=Fehler beim Unterbrechen und Stoppen von {0,choice,1#{0,number,integer} build|1<{0,number,\
integer} builds} von {1}
Folder.DisplayName=Ordner
Folder.DefaultPronoun=Element
Folder.Description=Erstellt einen Container, in dem verschachtelte Elemente gespeichert werden.\
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,3 @@
AbstractFolder.BeingDeleted={0} est en cours de suppression
AbstractFolder.FailureToStopBuilds=Impossible de suspendre et de stopper {0,choice,1#{0,number,integer} construction|1<{0,number,\
integer} constructions} de {1}
Folder.DisplayName=Dossier
Folder.DefaultPronoun=\u00c9l\u00e9ment
Folder.Description=Cr\u00e9e un conteneur qui stocke des objets imbriqu\u00e9s. Utile pour grouper ensemble des \u00e9l\u00e9ments. \
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,6 @@
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
# THE SOFTWARE.

AbstractFolder.BeingDeleted={0} \u51C6\u5907\u5220\u9664
Folder.DisplayName=\u6587\u4EF6\u5939
Folder.Description=\u521B\u5EFA\u4E00\u4E2A\u53EF\u4EE5\u5D4C\u5957\u5B58\u50A8\u7684\u5BB9\u5668\u3002\u5229\u7528\u5B83\u53EF\u4EE5\u8FDB\u884C\u5206\u7EC4\u3002 \
\u89C6\u56FE\u4EC5\u4EC5\u662F\u4E00\u4E2A\u8FC7\u6EE4\u5668\uFF0C\u800C\u6587\u4EF6\u5939\u5219\u662F\u4E00\u4E2A\u72EC\u7ACB\u7684\u547D\u540D\u7A7A\u95F4\uFF0C \
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -39,8 +39,13 @@
import hudson.model.Item;
import hudson.model.Job;
import hudson.model.ListView;
import hudson.model.ParametersDefinitionProperty;
import hudson.model.Queue;
import hudson.model.Result;
import hudson.model.StringParameterDefinition;
import hudson.model.User;
import hudson.model.listeners.ItemListener;
import hudson.model.queue.QueueTaskFuture;
import hudson.search.SearchItem;
import hudson.security.ACL;
import hudson.security.ACLContext;
Expand Down Expand Up @@ -81,8 +86,10 @@
import static org.junit.Assert.assertTrue;
import static org.junit.Assert.fail;

import org.junit.ClassRule;
import org.junit.Rule;
import org.junit.Test;
import org.jvnet.hudson.test.BuildWatcher;
import org.jvnet.hudson.test.Issue;
import org.jvnet.hudson.test.JenkinsRule;
import org.jvnet.hudson.test.MockAuthorizationStrategy;
Expand All @@ -93,6 +100,7 @@
public class FolderTest {

@Rule public JenkinsRule r = new JenkinsRule();
@ClassRule public static BuildWatcher bw = new BuildWatcher();

/**
* Tests rename operation.
Expand Down Expand Up @@ -225,6 +233,27 @@
}
}

@Issue("JENKINS-35160")
@Test
public void interruptOnDelete() throws Exception {
// adapted from JobTest in core
r.jenkins.setNumExecutors(2);
Queue.getInstance().maintain();
Folder d = r.createProject(Folder.class, "d");
FreeStyleProject p = d.createProject(FreeStyleProject.class, "p");
p.addProperty(new ParametersDefinitionProperty(new StringParameterDefinition("dummy", "0")));
p.setConcurrentBuild(true);
p.getBuildersList().add(new SleepBuilder(Long.MAX_VALUE));
FreeStyleBuild build1 = p.scheduleBuild2(0).getStartCondition().get();
FreeStyleBuild build2 = p.scheduleBuild2(0).getStartCondition().get();
QueueTaskFuture<FreeStyleBuild> build3 = p.scheduleBuild2(0);
Thread.sleep(1000); // TODO Queue.cancel(Item) can return false immediately after scheduling

Check warning on line 250 in src/test/java/com/cloudbees/hudson/plugins/folder/FolderTest.java

View check run for this annotation

ci.jenkins.io / Open Tasks Scanner

TODO

NORMAL: Queue.cancel(Item) can return false immediately after scheduling
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

d.delete();
r.assertBuildStatus(Result.ABORTED, r.waitForCompletion(build1));
r.assertBuildStatus(Result.ABORTED, r.waitForCompletion(build2));
assertTrue(build3.isCancelled());
Comment on lines +252 to +254
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actually this passes even with src/main/ from this PR and pom.xml from master, i.e. using the original inherited AbstractItem.delete implementation which already cancelled builds and queue items for folder children. It would not call FreeStyleProject.delete prior to Folder.delete but there was not much practical impact (perhaps only missing calls to ItemListener.fireOnDeleted) since all of $JENKINS_HOME/jobs/d/ was deleted.

}

/**
* This is more of a test of the core, but make sure the triggers resolve between ourselves.
*/
Expand Down