├── Jenkinsfile ├── .gitignore ├── src └── main │ ├── resources │ ├── index.jelly │ └── cz │ │ └── muni │ │ └── fi │ │ └── xkozubi1 │ │ └── ShutdownQueueConfiguration │ │ ├── help-timeOpenQueueMillis.html │ │ ├── help-sorterOn.html │ │ ├── help-permeability.html │ │ ├── help-periodRunnable.html │ │ ├── config.jelly │ │ └── help-strategyType.html │ └── java │ └── cz │ └── muni │ └── fi │ └── xkozubi1 │ ├── ShutdownQueueQueueListener.java │ ├── DefaultSorter.java │ ├── EstimatedDurationComparator.java │ ├── ShutdownTask.java │ ├── ShutdownQueueSorter.java │ ├── Utils.java │ ├── ShutdownQueueComputerListener.java │ ├── ShutdownQueueConfiguration.java │ └── HandleQuietingDown.java ├── README.md ├── release.properties ├── LICENSE ├── .github └── workflows │ └── cd.yml ├── pom.xml └── pom.xml.releaseBackup /Jenkinsfile: -------------------------------------------------------------------------------- 1 | buildPlugin() 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .idea/ 2 | target/ 3 | work 4 | *.iml 5 | -------------------------------------------------------------------------------- /src/main/resources/index.jelly: -------------------------------------------------------------------------------- 1 | 2 |
3 | This plugin enables short jobs to overtake frozen queue during the shutdown time of long-running tasks. 4 |
5 | -------------------------------------------------------------------------------- /src/main/resources/cz/muni/fi/xkozubi1/ShutdownQueueConfiguration/help-timeOpenQueueMillis.html: -------------------------------------------------------------------------------- 1 |
2 | Time for which Jenkins quieting down is interrupted. During this time, buildable items have a 3 | chance to be executed. Time is in milliseconds. 4 |
-------------------------------------------------------------------------------- /src/main/resources/cz/muni/fi/xkozubi1/ShutdownQueueConfiguration/help-sorterOn.html: -------------------------------------------------------------------------------- 1 |
2 | If checked, buildable items are sorted by an estimated duration in ascending order. 3 | If plugin is turned off, items are sorted by Jenkins default sorter. 4 |
5 | -------------------------------------------------------------------------------- /src/main/resources/cz/muni/fi/xkozubi1/ShutdownQueueConfiguration/help-permeability.html: -------------------------------------------------------------------------------- 1 |
2 | If set to X, jobs with an estimated duration * X <= longest remaining executor time are allowed 3 | to overrun frozen queue while shutdown. Allowed values are from interval <0;1>. 4 |
-------------------------------------------------------------------------------- /src/main/resources/cz/muni/fi/xkozubi1/ShutdownQueueConfiguration/help-periodRunnable.html: -------------------------------------------------------------------------------- 1 |
2 | Every given X seconds following conditions are checked:

3 | if Jenkins is in shutdown mode
4 | if the plugin is on
5 | if the buildable queue is not empty

6 | If these conditions are satisfied, appropriate logic is being done. 7 |

-------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # shutdown-queue 2 | 3 | This plugin ensures that short jobs can overtake frozen queue during the shutdown time of long-running tasks. 4 | 5 | In shutdown mode, no new jobs are scheduled. But if there is last 24h taking job, and queue is full of short jobs, it is wasted time to not run them. 6 | This plugin, allows to do exactly this. Set acceptance treshold, set strategy, and no longer waste HW cycles in long shutdown mode. 7 | -------------------------------------------------------------------------------- /src/main/java/cz/muni/fi/xkozubi1/ShutdownQueueQueueListener.java: -------------------------------------------------------------------------------- 1 | package cz.muni.fi.xkozubi1; 2 | 3 | import hudson.model.Queue; 4 | import hudson.model.queue.QueueListener; 5 | 6 | 7 | public class ShutdownQueueQueueListener extends QueueListener { 8 | @Override 9 | public void onEnterWaiting(Queue.WaitingItem wi) { 10 | if (Utils.isCanAddToQueue()) { 11 | super.onEnterWaiting(wi); 12 | } 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /src/main/java/cz/muni/fi/xkozubi1/DefaultSorter.java: -------------------------------------------------------------------------------- 1 | package cz.muni.fi.xkozubi1; 2 | 3 | import hudson.model.Queue; 4 | import hudson.model.queue.AbstractQueueSorterImpl; 5 | 6 | import java.io.Serializable; 7 | import java.util.List; 8 | 9 | 10 | /** 11 | * Imitates Jenkins default sorter. Sorts items based on their getQueueSince(). 12 | */ 13 | public class DefaultSorter extends AbstractQueueSorterImpl implements Serializable { 14 | 15 | @Override 16 | public void sortBuildableItems(List list) { 17 | list.sort(this); 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/main/java/cz/muni/fi/xkozubi1/EstimatedDurationComparator.java: -------------------------------------------------------------------------------- 1 | package cz.muni.fi.xkozubi1; 2 | 3 | import hudson.model.Queue; 4 | 5 | import java.io.Serializable; 6 | import java.util.Comparator; 7 | 8 | 9 | /** 10 | * Compares Queue.BuildableItems by getEstimatedDuration(). 11 | * Used with ShutdownQueueSorter. 12 | * @author Dominik Kozubik 13 | */ 14 | public class EstimatedDurationComparator implements Comparator, Serializable { 15 | 16 | @Override 17 | public int compare(Queue.BuildableItem buildableItem0, Queue.BuildableItem buildableItem1) { 18 | 19 | if (buildableItem0.task.getEstimatedDuration() < buildableItem1.task.getEstimatedDuration()) { 20 | return -1; 21 | } 22 | 23 | if (buildableItem0.task.getEstimatedDuration() > buildableItem1.task.getEstimatedDuration()) { 24 | return 1; 25 | } 26 | 27 | return 0; 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/main/resources/cz/muni/fi/xkozubi1/ShutdownQueueConfiguration/config.jelly: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | -------------------------------------------------------------------------------- /release.properties: -------------------------------------------------------------------------------- 1 | #release configuration 2 | #Sat Dec 25 11:48:22 CET 2021 3 | projectVersionPolicyId=default 4 | remoteTagging=true 5 | scm.commentPrefix=[maven-release-plugin] 6 | project.scm.io.jenkins.plugins\:shutdown-queue.connection=scm\:git\:git\://github.com/jenkinsci/${project.artifactId}-plugin.git 7 | project.scm.io.jenkins.plugins\:shutdown-queue.developerConnection=scm\:git\:git@github.com\:jenkinsci/${project.artifactId}-plugin.git 8 | project.scm.io.jenkins.plugins\:shutdown-queue.tag=HEAD 9 | completedPhase=end-release 10 | scm.url=scm\:git\:git@github.com\:jenkinsci/shutdown-queue-plugin.git 11 | exec.additionalArguments= 12 | scm.tagNameFormat=@{project.artifactId}-@{project.version} 13 | pushChanges=true 14 | scm.tag=shutdown-queue-1.2 15 | project.rel.io.jenkins.plugins\:shutdown-queue=1.2 16 | exec.snapshotReleasePluginAllowed=false 17 | project.dev.io.jenkins.plugins\:shutdown-queue=1.3-SNAPSHOT 18 | preparationGoals=clean install 19 | project.scm.io.jenkins.plugins\:shutdown-queue.url=https\://github.com/jenkinsci/${project.artifactId}-plugin 20 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 dkozubik 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /src/main/resources/cz/muni/fi/xkozubi1/ShutdownQueueConfiguration/help-strategyType.html: -------------------------------------------------------------------------------- 1 |
2 | Note:
3 | "buildable overflowing": Shutdown mode is canceled for "Open queue time". During this time, buildable items can 4 | left the buildable queue in order to be executed. After "Open queue time", shutdown mode is turned back on. 5 | 6 |

7 | 8 |

9 | Copying strategy
10 | Creates a copy of the buildable queue and chooses items that satisfy conditions to overtake the frozen queue 11 | (whitelist). If there are any, the original buildable queue is cleared. After "buildable overflowing", items 12 | except whitelist are put back into the queue. 13 |

14 | 15 |

16 | Remove longer strategy
17 | Simply removes tasks from the queue whose estimated duration does not satisfy condition before 18 | "buildable overflowing". 19 |

20 | 21 |

22 | Sort and remove longer strategy
23 | Sorts the queue by an estimated duration and a rest is the same as Remove longer strategy. 24 | Due to sorting at the beginning, more tasks might be executed in the same amount of time. 25 |

26 |
27 | -------------------------------------------------------------------------------- /src/main/java/cz/muni/fi/xkozubi1/ShutdownTask.java: -------------------------------------------------------------------------------- 1 | package cz.muni.fi.xkozubi1; 2 | 3 | import hudson.model.Computer; 4 | import jenkins.model.Jenkins; 5 | 6 | import java.util.logging.Logger; 7 | 8 | 9 | /** 10 | * Plugin runnable. 11 | * If the plugin is on, Jenkins is going to shut down, and the buildable queue is not empty, calls a method 12 | * handleLogic() from HandleQuietingDown which takes care of the rest of the plugin's logic. 13 | * 14 | * @author Dominik Kozubik 15 | */ 16 | public class ShutdownTask implements Runnable { 17 | private HandleQuietingDown handleQuietingDown; 18 | private static Logger logger = Logger.getLogger(ShutdownTask.class.getName()); 19 | 20 | public ShutdownTask(Computer computer) { 21 | handleQuietingDown = new HandleQuietingDown(computer); 22 | } 23 | 24 | @Override 25 | public void run() { 26 | if (ShutdownQueueConfiguration.getInstance().getPluginOn() && 27 | Jenkins.get().isQuietingDown() && 28 | Jenkins.get().getQueue().getBuildableItems().size() > 0) { 29 | try { 30 | handleQuietingDown.handleLogic(); 31 | } catch (InterruptedException e) { 32 | logger.warning("Interrupted exception occurred while doing shutdown-queue plugin logic."); 33 | } 34 | } 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /src/main/java/cz/muni/fi/xkozubi1/ShutdownQueueSorter.java: -------------------------------------------------------------------------------- 1 | package cz.muni.fi.xkozubi1; 2 | 3 | import edu.umd.cs.findbugs.annotations.SuppressFBWarnings; 4 | import hudson.model.Queue; 5 | import hudson.model.queue.QueueSorter; 6 | import jenkins.model.Jenkins; 7 | 8 | import java.util.Collections; 9 | import java.util.List; 10 | 11 | 12 | /** 13 | * Plugin's sorter. It is used if sorterOn or strategy type "Sort and remove longer" is on. 14 | * Also preserves default Jenkins sorter. 15 | */ 16 | public class ShutdownQueueSorter extends QueueSorter { 17 | 18 | private final QueueSorter originalQueueSorter; 19 | private final EstimatedDurationComparator comparator; 20 | 21 | public ShutdownQueueSorter(QueueSorter sorter) { 22 | originalQueueSorter = sorter; 23 | comparator = new EstimatedDurationComparator(); 24 | } 25 | 26 | @Override 27 | public void sortBuildableItems(List buildableItems) { 28 | if (originalQueueSorter != null) { 29 | originalQueueSorter.sortBuildableItems(buildableItems); 30 | } 31 | 32 | Collections.sort(buildableItems, comparator); 33 | } 34 | 35 | @SuppressFBWarnings(value = {"NP_NULL_ON_SOME_PATH_FROM_RETURN_VALUE"}, justification = "filled async, but may be correct") 36 | void reset() { 37 | sortBuildableItems(Jenkins.getInstanceOrNull().getQueue().getBuildableItems()); 38 | Queue.getInstance().maintain(); 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /.github/workflows/cd.yml: -------------------------------------------------------------------------------- 1 | # Note: additional setup is required, see https://www.jenkins.io/redirect/continuous-delivery-of-plugins 2 | 3 | name: cd 4 | on: 5 | workflow_dispatch: 6 | check_run: 7 | types: 8 | - completed 9 | 10 | jobs: 11 | validate: 12 | runs-on: ubuntu-latest 13 | outputs: 14 | should_release: ${{ steps.verify-ci-status.outputs.result == 'success' && steps.interesting-categories.outputs.interesting == 'true' }} 15 | steps: 16 | - name: Verify CI status 17 | uses: jenkins-infra/verify-ci-status-action@v1.2.0 18 | id: verify-ci-status 19 | with: 20 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 21 | output_result: true 22 | 23 | - name: Release Drafter 24 | uses: release-drafter/release-drafter@v5 25 | if: steps.verify-ci-status.outputs.result == 'success' 26 | with: 27 | name: next 28 | tag: next 29 | version: next 30 | env: 31 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 32 | 33 | - name: Check interesting categories 34 | uses: jenkins-infra/interesting-category-action@v1.0.0 35 | id: interesting-categories 36 | if: steps.verify-ci-status.outputs.result == 'success' 37 | with: 38 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 39 | 40 | release: 41 | runs-on: ubuntu-latest 42 | needs: [validate] 43 | if: needs.validate.outputs.should_release == 'true' 44 | steps: 45 | - name: Check out 46 | uses: actions/checkout@v2.3.4 47 | with: 48 | fetch-depth: 0 49 | - name: Set up JDK 8 50 | uses: actions/setup-java@v3.0.0 51 | with: 52 | distribution: 'temurin' 53 | java-version: 8 54 | - name: Release 55 | uses: jenkins-infra/jenkins-maven-cd-action@v1.2.0 56 | with: 57 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 58 | MAVEN_USERNAME: ${{ secrets.MAVEN_USERNAME }} 59 | MAVEN_TOKEN: ${{ secrets.MAVEN_TOKEN }} 60 | -------------------------------------------------------------------------------- /src/main/java/cz/muni/fi/xkozubi1/Utils.java: -------------------------------------------------------------------------------- 1 | package cz.muni.fi.xkozubi1; 2 | 3 | import edu.umd.cs.findbugs.annotations.SuppressFBWarnings; 4 | import hudson.model.queue.QueueSorter; 5 | import jenkins.model.Jenkins; 6 | 7 | 8 | /** 9 | * @author Dominik Kozubik 10 | */ 11 | public class Utils { 12 | 13 | 14 | private static boolean canAddToQueue = true; 15 | 16 | public static void setCanAddToQueue(boolean canAddToQueue) { 17 | Utils.canAddToQueue = canAddToQueue; 18 | } 19 | 20 | public static boolean isCanAddToQueue() { 21 | return canAddToQueue; 22 | } 23 | 24 | /** 25 | * Based on input condition, sets QueueSorter either to ShutdownQueueSorter or the original sorter. 26 | * @param isSorterOn boolean value from the settings option "Sorter on" 27 | */ 28 | @SuppressFBWarnings(value = {"NP_NULL_ON_SOME_PATH_FROM_RETURN_VALUE"}, justification = "filled async, but may be correct") 29 | public static void handleSorterOn(boolean isSorterOn) { 30 | if (isSorterOn) { 31 | QueueSorter originalSorter = Jenkins.getInstanceOrNull().getQueue().getSorter(); 32 | if (originalSorter == null) { 33 | originalSorter = new DefaultSorter(); 34 | } 35 | Jenkins.getInstanceOrNull().getQueue().setSorter(new ShutdownQueueSorter(originalSorter)); 36 | 37 | } else { 38 | doReset(); 39 | } 40 | } 41 | 42 | /** 43 | * Sets QueueSorter to DefaultSorter if it was ShutdownQueueSorter before. 44 | */ 45 | @SuppressFBWarnings(value = {"NP_NULL_ON_SOME_PATH_FROM_RETURN_VALUE"}, justification = "filled async, but may be correct") 46 | public static void doReset() { 47 | QueueSorter sorter = Jenkins.getInstanceOrNull().getQueue().getSorter(); 48 | if (sorter instanceof ShutdownQueueSorter) { 49 | Jenkins.getInstanceOrNull().getQueue().setSorter(new DefaultSorter()); 50 | ((ShutdownQueueSorter) sorter).reset(); 51 | } 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /src/main/java/cz/muni/fi/xkozubi1/ShutdownQueueComputerListener.java: -------------------------------------------------------------------------------- 1 | package cz.muni.fi.xkozubi1; 2 | 3 | import edu.umd.cs.findbugs.annotations.SuppressFBWarnings; 4 | import hudson.Extension; 5 | import hudson.model.Computer; 6 | import hudson.model.TaskListener; 7 | import hudson.slaves.ComputerListener; 8 | 9 | import java.util.concurrent.Executors; 10 | import java.util.concurrent.ScheduledExecutorService; 11 | import java.util.concurrent.ScheduledFuture; 12 | import java.util.concurrent.TimeUnit; 13 | import java.util.logging.Logger; 14 | 15 | 16 | /** 17 | * Plugin initiation 18 | * @author Dominik Kozubik 19 | */ 20 | @Extension 21 | public class ShutdownQueueComputerListener extends ComputerListener { 22 | private static Logger logger = Logger.getLogger(ShutdownQueueConfiguration.class.getName()); 23 | private static ScheduledExecutorService executorService; 24 | private static ShutdownTask shutdownTask; 25 | private static ScheduledFuture futureTask; 26 | 27 | @Override 28 | public void onOnline(Computer c, TaskListener listener) { 29 | onTemporarilyOnline(c); 30 | } 31 | 32 | @Override 33 | @SuppressFBWarnings(value = {"ST_WRITE_TO_STATIC_FROM_INSTANCE_METHOD"}, justification = "intentional global state keepers") 34 | public void onTemporarilyOnline(Computer computer) { 35 | executorService = Executors.newSingleThreadScheduledExecutor(); 36 | shutdownTask = new ShutdownTask(computer); 37 | changeScheduleInterval(ShutdownQueueConfiguration.getInstance().getPeriodRunnable()); 38 | 39 | logger.info("Shutdown-queue plugin thread has started."); 40 | } 41 | 42 | /** 43 | * Changes delay value of a periodic callable shutdownTask. 44 | * @param time seconds 45 | */ 46 | static synchronized void changeScheduleInterval(long time) 47 | { 48 | if(time > 0) 49 | { 50 | if (futureTask != null) 51 | { 52 | futureTask.cancel(false); 53 | } 54 | 55 | futureTask = executorService.scheduleAtFixedRate(shutdownTask, 1, time, TimeUnit.SECONDS); 56 | } 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4.0.0 4 | 5 | org.jenkins-ci.plugins 6 | plugin 7 | 4.12 8 | 9 | 10 | io.jenkins.plugins 11 | shutdown-queue 12 | 1.4-SNAPSHOT 13 | hpi 14 | 15 | 2.235.1 16 | 8 17 | true 18 | 19 | 20 | Shutdown Queue 21 | Enables short jobs to overtake frozen queue during the shutdown time of long-running tasks. 22 | 23 | 24 | MIT License 25 | https://opensource.org/licenses/MIT 26 | 27 | 28 | 29 | 30 | 31 | dkozubik 32 | Dominik Kozubik 33 | xkozubi1@fi.muni.cz 34 | 35 | 36 | 37 | 38 | https://github.com/jenkinsci/${project.artifactId}-plugin 39 | 40 | scm:git:git://github.com/jenkinsci/${project.artifactId}-plugin.git 41 | scm:git:git@github.com:jenkinsci/${project.artifactId}-plugin.git 42 | https://github.com/jenkinsci/${project.artifactId}-plugin 43 | HEAD 44 | 45 | 46 | 47 | 48 | repo.jenkins-ci.org 49 | https://repo.jenkins-ci.org/public/ 50 | 51 | 52 | 53 | 54 | repo.jenkins-ci.org 55 | https://repo.jenkins-ci.org/public/ 56 | 57 | 58 | 59 | 60 | -------------------------------------------------------------------------------- /pom.xml.releaseBackup: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4.0.0 4 | 5 | org.jenkins-ci.plugins 6 | plugin 7 | 4.12 8 | 9 | 10 | io.jenkins.plugins 11 | shutdown-queue 12 | 1.2-SNAPSHOT 13 | hpi 14 | 15 | 2.235.1 16 | 8 17 | true 18 | 19 | 20 | Shutdown Queue 21 | Enables short jobs to overtake frozen queue during the shutdown time of long-running tasks. 22 | 23 | 24 | MIT License 25 | https://opensource.org/licenses/MIT 26 | 27 | 28 | 29 | 30 | 31 | dkozubik 32 | Dominik Kozubik 33 | xkozubi1@fi.muni.cz 34 | 35 | 36 | 37 | 38 | https://github.com/jenkinsci/${project.artifactId}-plugin 39 | 40 | scm:git:git://github.com/jenkinsci/${project.artifactId}-plugin.git 41 | scm:git:git@github.com:jenkinsci/${project.artifactId}-plugin.git 42 | https://github.com/jenkinsci/${project.artifactId}-plugin 43 | HEAD 44 | 45 | 46 | 47 | 48 | repo.jenkins-ci.org 49 | https://repo.jenkins-ci.org/public/ 50 | 51 | 52 | 53 | 54 | repo.jenkins-ci.org 55 | https://repo.jenkins-ci.org/public/ 56 | 57 | 58 | 59 | 60 | -------------------------------------------------------------------------------- /src/main/java/cz/muni/fi/xkozubi1/ShutdownQueueConfiguration.java: -------------------------------------------------------------------------------- 1 | package cz.muni.fi.xkozubi1; 2 | 3 | import hudson.Extension; 4 | import hudson.util.FormValidation; 5 | import hudson.util.ListBoxModel; 6 | import jenkins.model.GlobalConfiguration; 7 | import net.sf.json.JSONObject; 8 | import org.kohsuke.stapler.QueryParameter; 9 | import org.kohsuke.stapler.StaplerRequest; 10 | 11 | import java.util.logging.Logger; 12 | 13 | 14 | /** 15 | * Settings for the plugin. Can be found in Jenkins global settings. 16 | * @author Dominik Kozubik 17 | */ 18 | @Extension 19 | public class ShutdownQueueConfiguration extends GlobalConfiguration { 20 | private static Logger logger = Logger.getLogger(ShutdownQueueConfiguration.class.getName()); 21 | 22 | private boolean pluginOn; 23 | private boolean sorterOn; 24 | private String strategyOption; 25 | private double permeability; 26 | private long periodRunnable; 27 | private long timeOpenQueueMillis; 28 | 29 | public ShutdownQueueConfiguration() { 30 | load(); 31 | setDefaultValues(); 32 | } 33 | 34 | public boolean getPluginOn() { 35 | return pluginOn; 36 | } 37 | 38 | public String getStrategyOption() { 39 | return strategyOption; 40 | } 41 | 42 | public long getPeriodRunnable() { 43 | return periodRunnable; 44 | } 45 | 46 | public double getPermeability() { 47 | return permeability; 48 | } 49 | 50 | public long getTimeOpenQueueMillis() { 51 | return timeOpenQueueMillis; 52 | } 53 | 54 | @Override 55 | public boolean configure(StaplerRequest staplerRequest, JSONObject json) throws FormException { 56 | pluginOn = json.optBoolean("pluginOn", true); 57 | sorterOn = json.optBoolean("sorterOn", false); 58 | strategyOption = json.optString("strategyType", "copying"); 59 | periodRunnable = json.optLong("periodRunnable", 10); 60 | permeability = json.optDouble("permeability", 0.7); 61 | timeOpenQueueMillis = json.optLong("timeOpenQueueMillis", 500); 62 | 63 | logger.info("\nShutdown-queue plugin CONFIGURATION\n" + 64 | "\nplugin: " + pluginOn + 65 | "\nsorter: " + sorterOn + 66 | "\nstrategy: " + strategyOption + 67 | "\nperiod: " + periodRunnable + 68 | "\npermeability: " + permeability + 69 | "\ntimeOpenQueueMillis " + timeOpenQueueMillis 70 | ); 71 | 72 | if (!pluginOn) { 73 | Utils.doReset(); 74 | } 75 | 76 | Utils.handleSorterOn(sorterOn); 77 | save(); 78 | ShutdownQueueComputerListener.changeScheduleInterval(periodRunnable); 79 | 80 | return super.configure(staplerRequest, json); 81 | } 82 | 83 | public FormValidation doCheckPermeability(@QueryParameter String value) { 84 | try { 85 | double valueD = Double.parseDouble(value); 86 | if (valueD < 0 || valueD > 1) { 87 | return FormValidation.error("Please, enter a number in the interval <0;1>."); 88 | } 89 | return FormValidation.ok(); 90 | 91 | } catch (NumberFormatException e) { 92 | return FormValidation.error("Please, enter an integer."); 93 | } 94 | } 95 | 96 | public FormValidation doCheckPeriodRunnable(@QueryParameter String value) { 97 | try { 98 | long valueI = Long.parseLong(value); 99 | if (valueI < 0) { 100 | return FormValidation.error("Please, enter a positive integer."); 101 | } 102 | return FormValidation.ok(); 103 | 104 | } catch (NumberFormatException e) { 105 | return FormValidation.error("Please, enter an integer."); 106 | } 107 | } 108 | 109 | public ListBoxModel doFillStrategyTypeItems() { 110 | ListBoxModel items = new ListBoxModel(); 111 | 112 | items.add("Copying", "copying"); 113 | items.add("Remove longer", "removeLonger"); 114 | items.add("Sort and remove longer", "sortRemoveLonger"); 115 | 116 | return items; 117 | } 118 | 119 | private void setDefaultValues() 120 | { 121 | pluginOn = true; 122 | sorterOn = false; 123 | strategyOption = "copying"; 124 | permeability = 0.6; 125 | periodRunnable = 10; 126 | timeOpenQueueMillis = 500; 127 | } 128 | 129 | public static ShutdownQueueConfiguration getInstance() { 130 | return GlobalConfiguration.all().get(ShutdownQueueConfiguration.class); 131 | } 132 | } 133 | -------------------------------------------------------------------------------- /src/main/java/cz/muni/fi/xkozubi1/HandleQuietingDown.java: -------------------------------------------------------------------------------- 1 | package cz.muni.fi.xkozubi1; 2 | 3 | import edu.umd.cs.findbugs.annotations.SuppressFBWarnings; 4 | import hudson.model.Computer; 5 | import hudson.model.Queue; 6 | import jenkins.model.Jenkins; 7 | 8 | import java.util.Collection; 9 | import java.util.List; 10 | import java.util.OptionalLong; 11 | import java.util.logging.Logger; 12 | import java.util.stream.Collectors; 13 | 14 | 15 | /** 16 | * Based on a strategy type, appropriate logic is performed. 17 | * @author Dominik Kozubik 18 | */ 19 | public class HandleQuietingDown { 20 | private final Computer computer; 21 | private final Jenkins jenkinsInstance; 22 | private static Logger logger = Logger.getLogger(HandleQuietingDown.class.getName()); 23 | 24 | HandleQuietingDown(Computer computer) { 25 | this.computer = computer; 26 | this.jenkinsInstance = Jenkins.getInstanceOrNull(); 27 | } 28 | 29 | /** 30 | * The main function, calls other methods based on obtained strategy type if there is any idle executor. 31 | * @throws InterruptedException in case the method is interrupted 32 | */ 33 | public void handleLogic() throws InterruptedException { 34 | long idleExecutorsCount = getIdleExecutorsCount(); 35 | if (idleExecutorsCount == 0) { 36 | logger.warning("No idle executor is available"); 37 | return; 38 | } 39 | 40 | long longestRemainingTime = getLongestExecutorRemainingTime(); 41 | if (longestRemainingTime <= -1L) { // check whether all executors are idle, i.e. longest task finished 42 | logger.info("The longest task has finished."); 43 | return; 44 | } 45 | 46 | double ratio = ShutdownQueueConfiguration.getInstance().getPermeability(); 47 | String strategyOption = ShutdownQueueConfiguration.getInstance().getStrategyOption(); 48 | 49 | if (strategyOption.equals("copying")) { 50 | logger.info("Performing strategy."); 51 | strategyCopying(idleExecutorsCount, longestRemainingTime, ratio); 52 | } 53 | else if (strategyOption.equals("removeLonger")) { 54 | logger.info("Performing strategy"); 55 | strategyRemoveLonger(longestRemainingTime, ratio); 56 | } 57 | else if (strategyOption.equals("sortRemoveLonger")) { 58 | logger.info("Performing strategy"); 59 | 60 | Utils.handleSorterOn(true); 61 | jenkinsInstance.getQueue().getSorter().sortBuildableItems(jenkinsInstance.getQueue().getBuildableItems()); 62 | 63 | // first it sorts buildables and then does the same as remove longer strategy 64 | strategyRemoveLonger(longestRemainingTime, ratio); 65 | Utils.doReset(); 66 | } 67 | } 68 | 69 | /** 70 | * Performs Copying strategy. 71 | * @param idleExecutorsCount the number of idle executors 72 | * @param longestExecutorTime executor's longest estimated duration 73 | * @param ratio permeability value from the settings 74 | * @throws InterruptedException 75 | */ 76 | private void strategyCopying(long idleExecutorsCount, long longestExecutorTime, double ratio) throws InterruptedException { 77 | List whiteListIDs = getWhiteListIDs(longestExecutorTime, ratio, idleExecutorsCount); 78 | 79 | if (whiteListIDs.size() == 0) { 80 | logger.info("No tasks satisfy condition."); 81 | return; 82 | } 83 | 84 | Queue.BuildableItem[] buildablesCopy = getBuildablesCopy(); 85 | logBuildablesCopy(buildablesCopy); 86 | 87 | logger.info("Executor longest remaining time: " + longestExecutorTime + 88 | "\nIdle executors count: " + idleExecutorsCount + 89 | "\nwhiteListIDs count: " + whiteListIDs.size()); 90 | 91 | cancelTasksButWhitelist(whiteListIDs); 92 | cancelAndDoQuietDown(ShutdownQueueConfiguration.getInstance().getTimeOpenQueueMillis()); 93 | putTasksBackToQueueBut(buildablesCopy, whiteListIDs); 94 | } 95 | 96 | /** 97 | * This method performs "remove longer strategy" 98 | * @param longestExecutorTime executor's longest estimated duration 99 | * @param ratio permeability value from the settings 100 | * @throws InterruptedException 101 | */ 102 | private void strategyRemoveLonger(long longestExecutorTime, double ratio) throws InterruptedException { 103 | if (longestExecutorTime != 0) { 104 | cancelTasksLongerThan(longestExecutorTime, ratio); 105 | } 106 | 107 | cancelAndDoQuietDown(ShutdownQueueConfiguration.getInstance().getTimeOpenQueueMillis()); 108 | } 109 | 110 | /** 111 | * @return the longest estimated remaining time of all executors 112 | */ 113 | private long getLongestExecutorRemainingTime() { 114 | OptionalLong maxRemainingTime = computer.getExecutors() 115 | .stream() 116 | .mapToLong(e -> e.getEstimatedRemainingTimeMillis()) 117 | .max(); 118 | 119 | return maxRemainingTime.isPresent() ? maxRemainingTime.getAsLong() : -1L; 120 | } 121 | 122 | /** 123 | * @return the number of idle executors 124 | */ 125 | private long getIdleExecutorsCount() { 126 | return computer.getExecutors() 127 | .stream() 128 | .filter(e -> e.isIdle()) 129 | .count(); 130 | } 131 | 132 | /** 133 | * Cancels tasks from buildable queue which estimated duration * ratio is longer than longestExecutorTime 134 | * @param longestExecutorTime executor's longest estimated duration 135 | * @param ratio "permeability" value from the settings 136 | */ 137 | private void cancelTasksLongerThan(long longestExecutorTime, double ratio) { 138 | Queue queue = Jenkins.get().getQueue(); 139 | 140 | queue.getBuildableItems() 141 | .stream() 142 | .filter(b -> b.task.getEstimatedDuration() * ratio >= longestExecutorTime) 143 | .forEach(buildableItem -> { 144 | logCancelTaskInfo(buildableItem); 145 | queue.cancel(buildableItem); 146 | }); 147 | } 148 | 149 | /** 150 | * Cancels tasks from the buildable queue which are not in the whitelist 151 | * @param whitelistIDs list of BuildableItems' IDs which are allowed to stay in the BuildableQueue 152 | */ 153 | private void cancelTasksButWhitelist(List whitelistIDs) { 154 | Queue queue = Jenkins.get().getQueue(); 155 | 156 | queue.getBuildableItems() 157 | .stream() 158 | .filter(b -> !whitelistIDs.contains(b.getId())) 159 | .forEach(b -> queue.cancel(b)); 160 | } 161 | 162 | /** 163 | * @param longestExecutorTime executor's longest estimated duration 164 | * @param ratio permeability value from the settings 165 | * @param idleExecutorsCount the number of idle executors 166 | * @returns list of BuildableItems' IDs which satisfy conditions. Length of the list is less than or equal to the 167 | * number of idle executors. 168 | */ 169 | private List getWhiteListIDs(long longestExecutorTime, double ratio, long idleExecutorsCount) { 170 | return Jenkins.get().getQueue().getBuildableItems() 171 | .stream() 172 | .filter(b -> b.task.getEstimatedDuration() != -1L 173 | && b.task.getEstimatedDuration() * ratio < longestExecutorTime) 174 | .map(b -> b.getId()) 175 | .limit(idleExecutorsCount) 176 | .collect(Collectors.toList()); 177 | } 178 | 179 | /** 180 | * Logs info about an item to be cancelled 181 | * @param item Queue.BuildableItem 182 | */ 183 | private void logCancelTaskInfo(Queue.BuildableItem item) { 184 | logger.info("Canceling task " + item.task.getName() + " with an estimated duration " 185 | + item.task.getEstimatedDuration()); 186 | } 187 | 188 | /** 189 | * @return array copy of BuildableItems 190 | */ 191 | private Queue.BuildableItem[] getBuildablesCopy() { 192 | Collection buildables = Jenkins.get().getQueue().getBuildableItems(); 193 | return buildables.toArray(new Queue.BuildableItem[buildables.size()]); 194 | } 195 | 196 | /** 197 | * logs info about items 198 | * @param items array of Queue.BuildableItem 199 | */ 200 | private void logBuildablesCopy(Queue.BuildableItem[] items) { 201 | logger.info("BUILDABLE ITEMS COPY\n"); 202 | for (Queue.BuildableItem item : items) { 203 | logger.info("Task name: " + item.task.getName() + " ID: " + item.getId()); 204 | } 205 | 206 | //could use Arrays.stream()... 207 | } 208 | 209 | /** 210 | * Cancels quieting down, waits for timeMillis and then starts quieting down again 211 | * @param timeMillis "Open queue time" from the settings 212 | * @throws InterruptedException 213 | */ 214 | @SuppressFBWarnings(value = {"ST_WRITE_TO_STATIC_FROM_INSTANCE_METHOD"}, justification = "intentional global state keepers") 215 | private void cancelAndDoQuietDown(long timeMillis) throws InterruptedException { 216 | if (jenkinsInstance.getQueue().getBuildableItems().size() > 0) 217 | { 218 | Utils.setCanAddToQueue(false); 219 | logger.warning("Canceling shutdown for " + timeMillis + " milliseconds."); 220 | jenkinsInstance.doCancelQuietDown(); 221 | Thread.sleep(timeMillis); 222 | logger.warning("Start shutdown."); 223 | jenkinsInstance.doQuietDown(); 224 | } 225 | 226 | Utils.setCanAddToQueue(false); 227 | } 228 | 229 | /** 230 | * schedules BuildableItems' tasks which are not in whiteListIDs and were cancelled before 231 | * @param buildablesCopy array copy of BuildableItems 232 | * @param whiteListIDs list of BuildableItems' IDs which are allowed to stay in the BuildableQueue 233 | */ 234 | private void putTasksBackToQueueBut(Queue.BuildableItem[] buildablesCopy, List whiteListIDs) { 235 | for (Queue.BuildableItem item : buildablesCopy) { 236 | if (!whiteListIDs.contains(item.getId())) { 237 | logger.info("Adding " + item.task.getName() + " back to the queue."); 238 | jenkinsInstance.getQueue().schedule(item.task, 0); 239 | } 240 | } 241 | } 242 | } 243 | --------------------------------------------------------------------------------