From 237b9ca457c032885ed98e29620f0ed8c0e55ca0 Mon Sep 17 00:00:00 2001 From: Si-So <72555364+Si-So@users.noreply.github.com> Date: Tue, 19 Jul 2022 17:39:31 +0200 Subject: [PATCH] [JENKINS-68780] Blocked upstream projects in the queue block downstream projects (#6675) --- .../java/hudson/model/AbstractProject.java | 34 +++++- .../src/test/java/hudson/model/QueueTest.java | 105 ++++++++++++++++++ .../block-downstream-building.html | 3 + .../block-downstream-building_de.html | 8 ++ .../block-upstream-building.html | 3 + .../block-upstream-building_de.html | 3 + 6 files changed, 150 insertions(+), 6 deletions(-) create mode 100644 war/src/main/webapp/help/project-config/block-downstream-building_de.html diff --git a/core/src/main/java/hudson/model/AbstractProject.java b/core/src/main/java/hudson/model/AbstractProject.java index 8b46d0cd0a7e..ad7f2f9f7830 100644 --- a/core/src/main/java/hudson/model/AbstractProject.java +++ b/core/src/main/java/hudson/model/AbstractProject.java @@ -1100,16 +1100,27 @@ public CauseOfBlockage getCauseOfBlockage() { /** * Returns the project if any of the downstream project is either - * building, waiting, pending or buildable. + * building, or queued and not blocked by an upstream/downstream project build. *

* This means eventually there will be an automatic triggering of * the given project (provided that all builds went smoothly.) */ public AbstractProject getBuildingDownstream() { - Set unblockedTasks = Jenkins.get().getQueue().getUnblockedTasks(); + // Unblocked downstream tasks must block this project. + Set tasks = Jenkins.get().getQueue().getUnblockedTasks(); + // Blocked downstream tasks must block this project. + // Projects blocked by upstream or downstream builds + // are ignored to break deadlocks. + for (Queue.Item item : Jenkins.get().getQueue().getBlockedItems()) { + if (item.getCauseOfBlockage() instanceof AbstractProject.BecauseOfUpstreamBuildInProgress || + item.getCauseOfBlockage() instanceof AbstractProject.BecauseOfDownstreamBuildInProgress) { + continue; + } + tasks.add(item.task); + } for (AbstractProject tup : getTransitiveDownstreamProjects()) { - if (tup != this && (tup.isBuilding() || unblockedTasks.contains(tup))) + if (tup != this && (tup.isBuilding() || tasks.contains(tup))) return tup; } return null; @@ -1117,16 +1128,27 @@ public AbstractProject getBuildingDownstream() { /** * Returns the project if any of the upstream project is either - * building or is in the queue. + * building, or queued and not blocked by an upstream/downstream project build. *

* This means eventually there will be an automatic triggering of * the given project (provided that all builds went smoothly.) */ public AbstractProject getBuildingUpstream() { - Set unblockedTasks = Jenkins.get().getQueue().getUnblockedTasks(); + // Unblocked upstream tasks must block this project. + Set tasks = Jenkins.get().getQueue().getUnblockedTasks(); + // Blocked upstream tasks must block this project. + // Projects blocked by upstream or downstream builds + // are ignored to break deadlocks. + for (Queue.Item item : Jenkins.get().getQueue().getBlockedItems()) { + if (item.getCauseOfBlockage() instanceof AbstractProject.BecauseOfUpstreamBuildInProgress || + item.getCauseOfBlockage() instanceof AbstractProject.BecauseOfDownstreamBuildInProgress) { + continue; + } + tasks.add(item.task); + } for (AbstractProject tup : getTransitiveUpstreamProjects()) { - if (tup != this && (tup.isBuilding() || unblockedTasks.contains(tup))) + if (tup != this && (tup.isBuilding() || tasks.contains(tup))) return tup; } return null; diff --git a/test/src/test/java/hudson/model/QueueTest.java b/test/src/test/java/hudson/model/QueueTest.java index 85ba24bbc4df..e38f9d92dcc2 100644 --- a/test/src/test/java/hudson/model/QueueTest.java +++ b/test/src/test/java/hudson/model/QueueTest.java @@ -568,6 +568,111 @@ public void upstreamDownstreamCycle() throws Exception { assertThat("The cycle should have been defanged and chain3 executed", queue.getItem(chain3), nullValue()); } + + @TestExtension({"upstreamProjectsInQueueBlock", "downstreamProjectsInQueueBlock"}) + public static class BlockingQueueTaskDispatcher extends QueueTaskDispatcher { + + public static final String NAME_OF_BLOCKED_PROJECT = "blocked project"; + + @Override + public CauseOfBlockage canRun(hudson.model.Queue.Item item) { + if (item.task.getOwnerTask().getDisplayName().equals(NAME_OF_BLOCKED_PROJECT)) { + return new CauseOfBlockage() { + + @Override + public String getShortDescription() { + return NAME_OF_BLOCKED_PROJECT + " is permanently blocked."; + } + + }; + } + return super.canRun(item); + } + + } + + private void waitUntilWaitingListIsEmpty(Queue q) throws InterruptedException { + boolean waitingItemsPresent = true; + while (waitingItemsPresent) { + waitingItemsPresent = false; + for (Queue.Item i : q.getItems()) { + if (i instanceof WaitingItem) { + waitingItemsPresent = true; + break; + } + } + Thread.sleep(1000); + } + } + + @Issue("JENKINS-68780") + @Test + public void upstreamProjectsInQueueBlock() throws Exception { + + FreeStyleProject a = r.createFreeStyleProject(BlockingQueueTaskDispatcher.NAME_OF_BLOCKED_PROJECT); + FreeStyleProject b = r.createFreeStyleProject(); + a.getPublishersList().add(new BuildTrigger(b.getName(), true)); + b.setBlockBuildWhenUpstreamBuilding(true); + + r.jenkins.rebuildDependencyGraph(); + + a.scheduleBuild(0, new UserIdCause()); + + Queue q = r.jenkins.getQueue(); + + waitUntilWaitingListIsEmpty(q); + + b.scheduleBuild(0, new UserIdCause()); + + waitUntilWaitingListIsEmpty(q); + + // This call is necessary because the queue blocks projects + // at first only temporarily. By calling the maintain method + // all temporarily blocked projects either become buildable or + // become permanently blocked + q.scheduleMaintenance().get(); + + assertEquals("Queue should contain two blocked items but didn't.", 2, q.getBlockedItems().size()); + + //Ensure orderly shutdown + q.clear(); + r.waitUntilNoActivity(); + } + + @Issue("JENKINS-68780") + @Test + public void downstreamProjectsInQueueBlock() throws Exception { + + FreeStyleProject a = r.createFreeStyleProject(); + FreeStyleProject b = r.createFreeStyleProject(BlockingQueueTaskDispatcher.NAME_OF_BLOCKED_PROJECT); + a.getPublishersList().add(new BuildTrigger(b.getName(), true)); + a.setBlockBuildWhenDownstreamBuilding(true); + + r.jenkins.rebuildDependencyGraph(); + + b.scheduleBuild(0, new UserIdCause()); + + Queue q = r.jenkins.getQueue(); + + waitUntilWaitingListIsEmpty(q); + + a.scheduleBuild(0, new UserIdCause()); + + waitUntilWaitingListIsEmpty(q); + + // This call is necessary because the queue blocks projects + // at first only temporarily. By calling the maintain method + // all temporarily blocked projects either become buildable or + // become permanently blocked + q.scheduleMaintenance().get(); + + assertEquals("Queue should contain two blocked items but didn't.", 2, q.getBlockedItems().size()); + + //Ensure orderly shutdown + q.clear(); + r.waitUntilNoActivity(); + } + public static class TestFlyweightTask extends TestTask implements Queue.FlyweightTask { Executor exec; private final Label assignedLabel; diff --git a/war/src/main/webapp/help/project-config/block-downstream-building.html b/war/src/main/webapp/help/project-config/block-downstream-building.html index 441d6c51b20b..9b1cfdae21bf 100644 --- a/war/src/main/webapp/help/project-config/block-downstream-building.html +++ b/war/src/main/webapp/help/project-config/block-downstream-building.html @@ -1,3 +1,6 @@

When this option is checked, Jenkins will prevent the project from building when a child of this project is in the queue, or building. The children include the direct as well as the transitive children.
+
+ However, if projects are circular dependent on each other, Jenkins will break the cycle by starting with the oldest requested build from the cycle. +
\ No newline at end of file diff --git a/war/src/main/webapp/help/project-config/block-downstream-building_de.html b/war/src/main/webapp/help/project-config/block-downstream-building_de.html new file mode 100644 index 000000000000..0df022fb3280 --- /dev/null +++ b/war/src/main/webapp/help/project-config/block-downstream-building_de.html @@ -0,0 +1,8 @@ +
+ Wenn angewählt, verhindert das Bauen des Projekts, solange sich ein nachgelagertes Projekt in + der Build-Warteschlange befindet oder gerade gebaut wird. Dabei werden sowohl + direkte als auch transitive Abhängigkeiten berücksichtigt. +
+
+ Jedoch werden Verklemmung von zirkulär abhängigen Projekten aufgelöst, indem der älteste angeforderte Build begonnen wird. +
\ No newline at end of file diff --git a/war/src/main/webapp/help/project-config/block-upstream-building.html b/war/src/main/webapp/help/project-config/block-upstream-building.html index cee2b6248169..2e39c7410209 100644 --- a/war/src/main/webapp/help/project-config/block-upstream-building.html +++ b/war/src/main/webapp/help/project-config/block-upstream-building.html @@ -1,3 +1,6 @@
When this option is checked, Jenkins will prevent the project from building when a dependency of this project is in the queue, or building. The dependencies include the direct as well as the transitive dependencies.
+
+ However, if projects are circular dependent on each other, Jenkins will break the cycle by starting with the oldest requested build from the cycle. +
diff --git a/war/src/main/webapp/help/project-config/block-upstream-building_de.html b/war/src/main/webapp/help/project-config/block-upstream-building_de.html index 72ca4bae984b..25fead38c291 100644 --- a/war/src/main/webapp/help/project-config/block-upstream-building_de.html +++ b/war/src/main/webapp/help/project-config/block-upstream-building_de.html @@ -3,3 +3,6 @@ der Build-Warteschlange befindet oder gerade gebaut wird. Dabei werden sowohl direkte als auch transitive Abhängigkeiten berücksichtigt. +
+ Jedoch werden Verklemmung von zirkulär abhängigen Projekten aufgelöst, indem der älteste angeforderte Build begonnen wird. +
\ No newline at end of file