From 28be4cc6fa1609e214249f8d3865111b7ae62c0b Mon Sep 17 00:00:00 2001 From: Martin Pokorny <89339813+mPokornyETM@users.noreply.github.com> Date: Tue, 19 Mar 2024 22:28:01 +0100 Subject: [PATCH] Lockable priority strategy (#632) --- README.md | 35 ++ pom.xml | 2 +- .../BackwardCompatibility.java | 4 +- .../plugins/lockableresources/LockStep.java | 16 +- .../lockableresources/LockStepExecution.java | 23 +- .../lockableresources/LockStepResource.java | 26 +- .../LockableResourcesManager.java | 200 ++++++---- .../RequiredResourcesProperty.java | 4 +- .../actions/LockableResourcesRootAction.java | 83 +++- .../queue/QueuedContextStruct.java | 44 +- .../lockableresources/LockStep/config.jelly | 5 +- .../LockStep/config.properties | 3 +- .../LockStep/config_cs.properties | 2 +- .../LockStep/config_de.properties | 2 +- .../LockStep/config_fr.properties | 2 +- .../LockStep/config_sk.properties | 2 +- .../LockStep/help-priority.html | 14 + .../lockableresources/Messages.properties | 9 +- .../LockableResourcesRootAction/index.jelly | 2 +- .../tableQueue/table.jelly | 35 +- .../tableQueue/table.properties | 19 +- src/main/webapp/css/style.css | 4 + src/main/webapp/js/lockable-resources.js | 143 +++++-- .../InteroperabilityTest.java | 4 +- .../LockStepHardKillTest.java | 10 +- .../lockableresources/LockStepTest.java | 376 +++++++++++++++++- .../LockStepWithRestartTest.java | 18 +- .../lockableresources/PressureTest.java | 4 +- .../LockableResourcesRootActionTest.java | 58 +++ 29 files changed, 942 insertions(+), 207 deletions(-) create mode 100644 src/main/resources/org/jenkins/plugins/lockableresources/LockStep/help-priority.html diff --git a/README.md b/README.md index 2a1c5cdda..02bdc8926 100644 --- a/README.md +++ b/README.md @@ -112,6 +112,41 @@ lock(resource: 'staging-server', inversePrecedence: true) { } ``` +> It is not allowed to mixed **inversePrecedence** and **priority**. + +start time | job | resource | inversePrecedence +------ |--- |--- |--- +00:01 | j1 | resource1 | false +00:02 | j2 | resource1 | false +00:03 | j3 | resource1 | true +00:04 | j4 | resource1 | false +00:05 | j5 | resource1 | true +00:06 | j6 | resource1 | false + +Resulting lock order: j1 -> j5 -> j3 -> j2 -> j4 -> j6 + +#### lock (queue) priority + +```groovy +lock(resource: 'staging-server', priority: 10) { + node { + servers.deploy 'staging' + } + input message: "Does ${jettyUrl}staging/ look good?" +} +``` + + start time | job | resource | priority + ------ |--- |--- |--- + 00:01 | j1 | resource1 | 0 + 00:02 | j2 | resource1 | + 00:03 | j3 | resource1 | -1 + 00:04 | j4 | resource1 | 10 + 00:05 | j5 | resource1 | -2 + 00:06 | j6 | resource1 | 100 + + Resulting lock order: j1 -> j6 -> j4 -> j2 -> j3 -> j5 + #### Resolve a variable configured with the resource name and properties ```groovy diff --git a/pom.xml b/pom.xml index 66c9d689d..9740d3a69 100644 --- a/pom.xml +++ b/pom.xml @@ -59,7 +59,7 @@ 999999-SNAPSHOT - 2.387.3 + 2.440.1 jenkinsci/${project.artifactId}-plugin Max Low diff --git a/src/main/java/org/jenkins/plugins/lockableresources/BackwardCompatibility.java b/src/main/java/org/jenkins/plugins/lockableresources/BackwardCompatibility.java index 4bb9cded1..d5bdd48e8 100644 --- a/src/main/java/org/jenkins/plugins/lockableresources/BackwardCompatibility.java +++ b/src/main/java/org/jenkins/plugins/lockableresources/BackwardCompatibility.java @@ -52,7 +52,9 @@ public static void compatibilityMigration() { queuedContext, Collections.singletonList(resourceHolder), resource.getName(), - null); + null, + false, + 0); } queuedContexts.clear(); } diff --git a/src/main/java/org/jenkins/plugins/lockableresources/LockStep.java b/src/main/java/org/jenkins/plugins/lockableresources/LockStep.java index 4742958e0..4eba9f493 100644 --- a/src/main/java/org/jenkins/plugins/lockableresources/LockStep.java +++ b/src/main/java/org/jenkins/plugins/lockableresources/LockStep.java @@ -61,6 +61,9 @@ public class LockStep extends Step implements Serializable { @SuppressFBWarnings(value = "PA_PUBLIC_PRIMITIVE_ATTRIBUTE", justification = "Preserve API compatibility.") public List extra = null; + @SuppressFBWarnings(value = "PA_PUBLIC_PRIMITIVE_ATTRIBUTE", justification = "Preserve API compatibility.") + public int priority = 0; + // it should be LockStep() - without params. But keeping this for backward compatibility // so `lock('resource1')` still works and `lock(label: 'label1', quantity: 3)` works too (resource // is not required) @@ -105,6 +108,11 @@ public void setQuantity(int quantity) { this.quantity = quantity; } + @DataBoundSetter + public void setPriority(int priority) { + this.priority = priority; + } + @DataBoundSetter public void setExtra(@CheckForNull List extra) { this.extra = extra; @@ -184,7 +192,11 @@ public String toString() { .map(res -> "{" + res.toString() + "}") .collect(Collectors.joining(",")); } else if (resource != null || label != null) { - return LockStepResource.toString(resource, label, quantity); + String ret = LockStepResource.toString(resource, label, quantity); + if (this.priority != 0) { + ret += ", Priority: " + this.priority; + } + return ret; } else { return "nothing"; } @@ -193,7 +205,7 @@ public String toString() { // ------------------------------------------------------------------------- /** Label and resource are mutual exclusive. */ public void validate() { - LockStepResource.validate(resource, label, resourceSelectStrategy, extra); + LockStepResource.validate(resource, label, resourceSelectStrategy, extra, priority, inversePrecedence); } // ------------------------------------------------------------------------- diff --git a/src/main/java/org/jenkins/plugins/lockableresources/LockStepExecution.java b/src/main/java/org/jenkins/plugins/lockableresources/LockStepExecution.java index 8add5ba9e..e43f3c060 100644 --- a/src/main/java/org/jenkins/plugins/lockableresources/LockStepExecution.java +++ b/src/main/java/org/jenkins/plugins/lockableresources/LockStepExecution.java @@ -98,7 +98,7 @@ public boolean start() throws Exception { resourceNames.put(resource.getName(), resource.getProperties()); } } - LockStepExecution.proceed(resourceNames, getContext(), step.toString(), step.variable, step.inversePrecedence); + LockStepExecution.proceed(resourceNames, getContext(), step.toString(), step.variable); return false; } @@ -120,7 +120,13 @@ private void onLockFailed(PrintStream logger, List reso LockableResourcesManager.printLogs( "[" + step + "] is not free, waiting for execution ...", Level.FINE, LOGGER, logger); LockableResourcesManager lrm = LockableResourcesManager.get(); - lrm.queueContext(getContext(), resourceHolderList, step.toString(), step.variable); + lrm.queueContext( + getContext(), + resourceHolderList, + step.toString(), + step.variable, + step.inversePrecedence, + step.priority); } } @@ -144,8 +150,7 @@ public static void proceed( final LinkedHashMap> lockedResources, StepContext context, String resourceDescription, - final String variable, - boolean inversePrecedence) { + final String variable) { Run build; FlowNode node = null; PrintStream logger = null; @@ -165,8 +170,7 @@ public static void proceed( LockedResourcesBuildAction.updateAction(build, new ArrayList<>(lockedResources.keySet())); PauseAction.endCurrentPause(node); BodyInvoker bodyInvoker = context.newBodyInvoker() - .withCallback(new Callback( - new ArrayList<>(lockedResources.keySet()), resourceDescription, inversePrecedence)); + .withCallback(new Callback(new ArrayList<>(lockedResources.keySet()), resourceDescription)); if (variable != null && !variable.isEmpty()) { // set the variable for the duration of the block bodyInvoker.withContext( @@ -210,18 +214,15 @@ private static final class Callback extends BodyExecutionCallback.TailCall { private static final long serialVersionUID = -2024890670461847666L; private final List resourceNames; private final String resourceDescription; - private final boolean inversePrecedence; - Callback(List resourceNames, String resourceDescription, boolean inversePrecedence) { + Callback(List resourceNames, String resourceDescription) { this.resourceNames = resourceNames; this.resourceDescription = resourceDescription; - this.inversePrecedence = inversePrecedence; } @Override protected void finished(StepContext context) throws Exception { - LockableResourcesManager.get() - .unlockNames(this.resourceNames, context.get(Run.class), this.inversePrecedence); + LockableResourcesManager.get().unlockNames(this.resourceNames, context.get(Run.class)); LockableResourcesManager.printLogs( "Lock released on resource [" + resourceDescription + "]", Level.FINE, diff --git a/src/main/java/org/jenkins/plugins/lockableresources/LockStepResource.java b/src/main/java/org/jenkins/plugins/lockableresources/LockStepResource.java index c78373052..59f623ae4 100644 --- a/src/main/java/org/jenkins/plugins/lockableresources/LockStepResource.java +++ b/src/main/java/org/jenkins/plugins/lockableresources/LockStepResource.java @@ -75,7 +75,7 @@ public static String toString(String resource, String label, int quantity) { } // make sure there is an actual resource specified if (resource != null) { - return resource; + return "Resource: " + resource; } return "[no resource/label specified - probably a bug]"; } @@ -83,14 +83,19 @@ public static String toString(String resource, String label, int quantity) { // ------------------------------------------------------------------------- /** Label and resource are mutual exclusive. */ public void validate() { - validate(resource, label, null, false); + validate(resource, label, null, false, 0, false); } // ------------------------------------------------------------------------- /** Validate input parameters*/ public static void validate( - String resource, String label, String resourceSelectStrategy, List extra) { - validate(resource, label, resourceSelectStrategy, extra != null); + String resource, + String label, + String resourceSelectStrategy, + List extra, + int priority, + boolean inversePrecedence) { + validate(resource, label, resourceSelectStrategy, extra != null, priority, inversePrecedence); if (extra != null) { for (LockStepResource e : extra) { e.validate(); @@ -103,10 +108,21 @@ public static void validate( * Label and resource are mutual exclusive. The label, if provided, must be configured (at least * one resource must have this label). */ - public static void validate(String resource, String label, String resourceSelectStrategy, boolean hasExtra) { + public static void validate( + String resource, + String label, + String resourceSelectStrategy, + boolean hasExtra, + int priority, + boolean inversePrecedence) { if (!hasExtra && label == null && resource == null) { throw new IllegalArgumentException(Messages.error_labelOrNameMustBeSpecified()); } + + if (priority != 0 && inversePrecedence) { + throw new IllegalArgumentException(Messages.error_inversePrecedenceAndPriorityAreSet()); + } + if (label != null && !label.isEmpty() && resource != null && !resource.isEmpty()) { throw new IllegalArgumentException(Messages.error_labelAndNameSpecified()); } diff --git a/src/main/java/org/jenkins/plugins/lockableresources/LockableResourcesManager.java b/src/main/java/org/jenkins/plugins/lockableresources/LockableResourcesManager.java index c6ee39979..0d3025eb7 100644 --- a/src/main/java/org/jenkins/plugins/lockableresources/LockableResourcesManager.java +++ b/src/main/java/org/jenkins/plugins/lockableresources/LockableResourcesManager.java @@ -67,8 +67,6 @@ public class LockableResourcesManager extends GlobalConfiguration { * (freestyle builds) regular Jenkins queue is used. */ private List queuedContexts = new ArrayList<>(); - // remember last processed queue index - private transient int lastCheckedQueueIndex = -1; // cache to enable / disable saving lockable-resources state private int enableSave = -1; @@ -339,7 +337,10 @@ public LockableResource fromName(@CheckForNull String resourceName) { // --------------------------------------------------------------------------- @Restricted(NoExternalUse.class) - public List fromNames(final List names) { + public List fromNames(@Nullable final List names) { + if (names == null) { + return null; + } return fromNames(names, false); } @@ -616,7 +617,15 @@ public boolean lock(List resources, Run build) { // --------------------------------------------------------------------------- private void freeResources(List unlockResources, @Nullable Run build) { + LOGGER.fine("free it: " + unlockResources); + + // make sure there is a list of resource names to unlock + if (unlockResources == null || unlockResources.isEmpty()) { + return; + } + + List toBeRemoved = new ArrayList<>(); for (LockableResource resource : unlockResources) { // No more contexts, unlock resource if (build != null && build != resource.getBuild()) { @@ -627,28 +636,40 @@ private void freeResources(List unlockResources, @Nullable Run uncacheIfFreeing(resource, true, false); if (resource.isEphemeral()) { - LOGGER.fine("Remove ephemeral resource: " + resource); - this.resources.remove(resource); + LOGGER.info("Remove ephemeral resource: " + resource); + toBeRemoved.add(resource); } } + // remove all ephemeral resources + removeResources(toBeRemoved); } // --------------------------------------------------------------------------- + @Deprecated + @ExcludeFromJacocoGeneratedReport public void unlock(List resourcesToUnLock, @Nullable Run build) { - unlock(resourcesToUnLock, build, false); + List resourceNamesToUnLock = LockableResourcesManager.getResourcesNames(resourcesToUnLock); + this.unlockNames(resourceNamesToUnLock, build); } // --------------------------------------------------------------------------- + @Deprecated + @ExcludeFromJacocoGeneratedReport public void unlock( @Nullable List resourcesToUnLock, @Nullable Run build, boolean inversePrecedence) { - List resourceNamesToUnLock = LockableResourcesManager.getResourcesNames(resourcesToUnLock); - this.unlockNames(resourceNamesToUnLock, build, inversePrecedence); + unlock(resourcesToUnLock, build); } - // --------------------------------------------------------------------------- - @SuppressFBWarnings(value = "REC_CATCH_EXCEPTION", justification = "not sure which exceptions might be catch.") + @Deprecated + @ExcludeFromJacocoGeneratedReport public void unlockNames( @Nullable List resourceNamesToUnLock, @Nullable Run build, boolean inversePrecedence) { + this.unlockNames(resourceNamesToUnLock, build); + } + // --------------------------------------------------------------------------- + @SuppressFBWarnings(value = "REC_CATCH_EXCEPTION", justification = "not sure which exceptions might be catch.") + public void unlockNames(@Nullable List resourceNamesToUnLock, @Nullable Run build) { + // make sure there is a list of resource names to unlock if (resourceNamesToUnLock == null || resourceNamesToUnLock.isEmpty()) { return; @@ -657,31 +678,16 @@ public void unlockNames( synchronized (this.syncResources) { this.freeResources(this.fromNames(resourceNamesToUnLock), build); - // process as many contexts as possible - this.lastCheckedQueueIndex = -1; - while (resourceNamesToUnLock.size() > 0 && proceedNextContext(inversePrecedence)) { - for (String resourceName : resourceNamesToUnLock) { - LockableResource r = fromName(resourceName); - if (r == null) { - // probably it was ephemeral resource and does not exists now - // therefore we need to check the whole queue later - break; - } - if (!r.isFree()) { - // i sno more free, that means, we does not need to check it in the queue now - resourceNamesToUnLock.remove(resourceName); - break; - } - } + while (proceedNextContext()) { + // process as many contexts as possible } save(); } } - private boolean proceedNextContext(boolean inversePrecedence) { - LOGGER.finest("inversePrecedence: " + inversePrecedence); - QueuedContextStruct nextContext = this.getNextQueuedContext(inversePrecedence); + private boolean proceedNextContext() { + QueuedContextStruct nextContext = this.getNextQueuedContext(); LOGGER.finest("nextContext: " + nextContext); // no context is queued which can be started once these resources are free'd. if (nextContext == null) { @@ -723,8 +729,7 @@ private boolean proceedNextContext(boolean inversePrecedence) { resourcesToLock, nextContext.getContext(), nextContext.getResourceDescription(), - nextContext.getVariableName(), - inversePrecedence); + nextContext.getVariableName()); return true; } @@ -754,65 +759,34 @@ public List getAllResourcesNames() { /** * Returns the next queued context with all its requirements satisfied. * - * @param resourceNamesToUnLock resource names locked at the moment but available if required (as - * they are going to be unlocked soon) - * @param resourceNamesToUnReserve resource names reserved at the moment but available if required - * (as they are going to be un-reserved soon) - * @param inversePrecedence false pick up context as they are in the queue or true to take the - * most recent one (satisfying requirements) - * @return the context or null */ @CheckForNull - private QueuedContextStruct getNextQueuedContext(boolean inversePrecedence) { + private QueuedContextStruct getNextQueuedContext() { LOGGER.fine("current queue size: " + this.queuedContexts.size()); LOGGER.finest("current queue: " + this.queuedContexts); List orphan = new ArrayList<>(); QueuedContextStruct nextEntry = null; - if (inversePrecedence) { - // the last one added lock ist the newest one, and this wins - if (this.lastCheckedQueueIndex == -1) { - this.lastCheckedQueueIndex = this.queuedContexts.size() - 1; - } else this.lastCheckedQueueIndex++; - for (; this.lastCheckedQueueIndex >= 0 && nextEntry == null; this.lastCheckedQueueIndex--) { - QueuedContextStruct entry = this.queuedContexts.get(this.lastCheckedQueueIndex); - // check queue list first - if (!entry.isValid()) { - orphan.add(entry); - continue; - } - LOGGER.finest("inversePrecedence - index: " + this.lastCheckedQueueIndex + " " + entry); - nextEntry = getNextQueuedContextEntry(entry); - } - } else { - // the first one added lock is the oldest one, and this wins - if (this.lastCheckedQueueIndex == -1) { - this.lastCheckedQueueIndex = 0; - } else this.lastCheckedQueueIndex--; - for (; - this.lastCheckedQueueIndex < this.queuedContexts.size() && nextEntry == null; - this.lastCheckedQueueIndex++) { - QueuedContextStruct entry = this.queuedContexts.get(this.lastCheckedQueueIndex); - // check queue list first - if (!entry.isValid()) { - orphan.add(entry); - continue; - } - LOGGER.finest("oldest win - index: " + this.lastCheckedQueueIndex + " " + entry); + // the first one added lock is the oldest one, and this wins - nextEntry = getNextQueuedContextEntry(entry); + for (int idx = 0; idx < this.queuedContexts.size() && nextEntry == null; idx++) { + QueuedContextStruct entry = this.queuedContexts.get(idx); + // check queue list first + if (!entry.isValid()) { + LOGGER.fine("well be removed: " + idx + " " + entry); + orphan.add(entry); + continue; } + LOGGER.finest("oldest win - index: " + idx + " " + entry); + + nextEntry = getNextQueuedContextEntry(entry); } if (!orphan.isEmpty()) { this.queuedContexts.removeAll(orphan); - this.lastCheckedQueueIndex = -1; } - if (nextEntry == null) { - this.lastCheckedQueueIndex = -1; - } return nextEntry; } @@ -989,7 +963,7 @@ public void unreserve(List resources) { LOGGER.fine("unreserve " + resources); unreserveResources(resources); - proceedNextContext(false /*inversePrecedence*/); + proceedNextContext(); save(); } @@ -1030,6 +1004,35 @@ public void recycle(List resources) { } } + // --------------------------------------------------------------------------- + /** Change the order (position) of the given item in the queue*/ + @Restricted(NoExternalUse.class) // used by jelly + public void changeQueueOrder(final String queueId, final int newPosition) throws IOException { + synchronized (this.syncResources) { + if (newPosition < 0 || newPosition >= this.queuedContexts.size()) { + throw new IOException( + Messages.error_queuePositionOutOfRange(newPosition + 1, this.queuedContexts.size())); + } + + QueuedContextStruct queueItem = null; + int oldIndex = -1; + for (int i = 0; i < this.queuedContexts.size(); i++) { + QueuedContextStruct entry = this.queuedContexts.get(i); + if (entry.getId().equals(queueId)) { + oldIndex = i; + break; + } + } + + if (oldIndex < 0) { + // no more exists !? + throw new IOException(Messages.error_queueDoesNotExist(queueId)); + } + + Collections.swap(this.queuedContexts, oldIndex, newPosition); + } + } + // --------------------------------------------------------------------------- @Override public boolean configure(StaplerRequest req, JSONObject json) { @@ -1067,6 +1070,19 @@ public List getAvailableResources(final List getAvailableResources(final QueuedContextStruct entry) { + return this.getAvailableResources(entry.getResources(), entry.getLogger(), null); + } + + // --------------------------------------------------------------------------- + /** Function removes all given resources */ + public void removeResources(List toBeRemoved) { + synchronized (this.syncResources) { + this.resources.removeAll(toBeRemoved); + } + } + // --------------------------------------------------------------------------- /** * Checks if there are enough resources available to satisfy the requirements specified within @@ -1297,11 +1313,14 @@ private String getQueueCause(final LockableResource resource) { * Adds the given context and the required resources to the queue if * this context is not yet queued. */ + @Restricted(NoExternalUse.class) public void queueContext( StepContext context, List requiredResources, String resourceDescription, - String variableName) { + String variableName, + boolean inversePrecedence, + int priority) { synchronized (this.syncResources) { for (QueuedContextStruct entry : this.queuedContexts) { if (entry.getContext() == context) { @@ -1310,8 +1329,33 @@ public void queueContext( } } - this.queuedContexts.add( - new QueuedContextStruct(context, requiredResources, resourceDescription, variableName)); + int queueIndex = 0; + QueuedContextStruct newQueueItem = + new QueuedContextStruct(context, requiredResources, resourceDescription, variableName, priority); + + if (inversePrecedence && priority == 0) { + queueIndex = 0; + } else { + queueIndex = this.queuedContexts.size() - 1; + // LOGGER.info("newQueueItem " + newQueueItem.toString()); + for (; queueIndex >= 0; queueIndex--) { + QueuedContextStruct entry = this.queuedContexts.get(queueIndex); + final int rc = entry.compare(newQueueItem); + // LOGGER.info("compare " + rc + " " + entry.toString()); + if (rc > 0) { + continue; + } + break; + } + queueIndex++; + } + + this.queuedContexts.add(queueIndex, newQueueItem); + printLogs( + requiredResources + " added into queue at position " + queueIndex, + newQueueItem.getLogger(), + Level.INFO); + save(); } } diff --git a/src/main/java/org/jenkins/plugins/lockableresources/RequiredResourcesProperty.java b/src/main/java/org/jenkins/plugins/lockableresources/RequiredResourcesProperty.java index 91b8ab97a..f058f6260 100644 --- a/src/main/java/org/jenkins/plugins/lockableresources/RequiredResourcesProperty.java +++ b/src/main/java/org/jenkins/plugins/lockableresources/RequiredResourcesProperty.java @@ -254,8 +254,8 @@ public FormValidation doCheckResourceNumber( } if (numResources < numAsInt) { - return FormValidation.error( - String.format(Messages.error_givenAmountIsGreaterThatResurcesAmount(), numAsInt, numResources)); + return FormValidation.error(String.format( + Messages.error_givenAmountIsGreaterThatResourcesAmount(), numAsInt, numResources)); } return FormValidation.ok(); } diff --git a/src/main/java/org/jenkins/plugins/lockableresources/actions/LockableResourcesRootAction.java b/src/main/java/org/jenkins/plugins/lockableresources/actions/LockableResourcesRootAction.java index 3b056aa98..45ffd3137 100644 --- a/src/main/java/org/jenkins/plugins/lockableresources/actions/LockableResourcesRootAction.java +++ b/src/main/java/org/jenkins/plugins/lockableresources/actions/LockableResourcesRootAction.java @@ -74,6 +74,12 @@ public class LockableResourcesRootAction implements RootAction { Messages._LockableResourcesRootAction_ViewPermission_Description(), Jenkins.ADMINISTER, PermissionScope.JENKINS); + public static final Permission QUEUE = new Permission( + PERMISSIONS_GROUP, + Messages.LockableResourcesRootAction_QueueChangeOrderPermission(), + Messages._LockableResourcesRootAction_QueueChangeOrderPermission_Description(), + Jenkins.ADMINISTER, + PermissionScope.JENKINS); public static final String ICON = "symbol-lock-closed"; @@ -298,7 +304,7 @@ public Queue getQueue() { for (QueuedContextStruct context : currentQueueContext) { for (LockableResourcesStruct resourceStruct : context.getResources()) { - queue.add(resourceStruct, context.getBuild()); + queue.add(resourceStruct, context); } } @@ -319,8 +325,8 @@ public Queue() { // ------------------------------------------------------------------------- @Restricted(NoExternalUse.class) // used by jelly - public void add(final LockableResourcesStruct resourceStruct, final Run build) { - QueueStruct queueStruct = new QueueStruct(resourceStruct, build); + public void add(final LockableResourcesStruct resourceStruct, final QueuedContextStruct context) { + QueueStruct queueStruct = new QueueStruct(resourceStruct, context); queue.add(queueStruct); if (resourceStruct.queuedAt == 0) { // Older versions of this plugin might miss this information. @@ -352,17 +358,23 @@ public static class QueueStruct { String groovyScript; String requiredNumber; long queuedAt = 0; + int priority = 0; + String id = null; Run build; - public QueueStruct(final LockableResourcesStruct resourceStruct, final Run build) { + public QueueStruct(final LockableResourcesStruct resourceStruct, final QueuedContextStruct context) { this.requiredResources = resourceStruct.required; this.requiredLabel = resourceStruct.label; this.requiredNumber = resourceStruct.requiredNumber; this.queuedAt = resourceStruct.queuedAt; - this.build = build; + this.build = context.getBuild(); + this.priority = context.getPriority(); + this.id = context.getId(); final SecureGroovyScript systemGroovyScript = resourceStruct.getResourceMatchScript(); - if (systemGroovyScript != null) this.groovyScript = systemGroovyScript.getScript(); + if (systemGroovyScript != null) { + this.groovyScript = systemGroovyScript.getScript(); + } } // ----------------------------------------------------------------------- @@ -385,7 +397,7 @@ public String getRequiredLabel() { @NonNull @Restricted(NoExternalUse.class) // used by jelly public String getRequiredNumber() { - return this.requiredNumber == null ? "N/A" : this.requiredNumber; + return this.requiredNumber == null ? "0" : this.requiredNumber; } // ----------------------------------------------------------------------- @@ -424,6 +436,32 @@ public Date getQueuedTimestamp() { return new Date(this.queuedAt); } + // ----------------------------------------------------------------------- + /** Returns queue priority. */ + @Restricted(NoExternalUse.class) // used by jelly + public int getPriority() { + if (this.id == null) { + // defensive + // in case of jenkins update from older version and you have some queue + // might happens, that there are no priority set + return 0; + } + return this.priority; + } + + // ----------------------------------------------------------------------- + /** Returns queue ID. */ + @Restricted(NoExternalUse.class) + public String getId() { + if (this.id == null) { + // defensive + // in case of jenkins update from older version and you have some queue + // might happens, that there are no priority set + return "NN"; + } + return this.id; + } + @Restricted(NoExternalUse.class) // used by jelly public boolean resourcesMatch() { return (requiredResources != null && requiredResources.size() > 0); @@ -624,6 +662,37 @@ public void doSaveNote(final StaplerRequest req, final StaplerResponse rsp) thro } } + // --------------------------------------------------------------------------- + /** Change queue order (item position) */ + @Restricted(NoExternalUse.class) // used by jelly + @RequirePOST + public void doChangeQueueOrder(final StaplerRequest req, final StaplerResponse rsp) + throws IOException, ServletException { + Jenkins.get().checkPermission(QUEUE); + + final String queueId = req.getParameter("id"); + final String newIndexStr = req.getParameter("index"); + + LOGGER.fine("doChangeQueueOrder, id: " + queueId + " newIndexStr: " + newIndexStr); + + final int newIndex; + try { + newIndex = Integer.parseInt(newIndexStr); + } catch (NumberFormatException e) { + rsp.sendError(423, Messages.error_isNotANumber(newIndexStr)); + return; + } + + try { + LockableResourcesManager.get().changeQueueOrder(queueId, newIndex - 1); + } catch (IOException e) { + rsp.sendError(423, e.toString().replace("java.io.IOException: ", "")); + return; + } + + rsp.forwardToPreviousPage(req); + } + // --------------------------------------------------------------------------- private List getResourcesFromRequest(final StaplerRequest req, final StaplerResponse rsp) throws IOException, ServletException { diff --git a/src/main/java/org/jenkins/plugins/lockableresources/queue/QueuedContextStruct.java b/src/main/java/org/jenkins/plugins/lockableresources/queue/QueuedContextStruct.java index 685e0b8d6..a131de21c 100644 --- a/src/main/java/org/jenkins/plugins/lockableresources/queue/QueuedContextStruct.java +++ b/src/main/java/org/jenkins/plugins/lockableresources/queue/QueuedContextStruct.java @@ -14,8 +14,8 @@ import java.io.IOException; import java.io.PrintStream; import java.io.Serializable; -import java.util.Date; import java.util.List; +import java.util.UUID; import java.util.logging.Level; import java.util.logging.Logger; import org.jenkinsci.plugins.workflow.steps.StepContext; @@ -49,13 +49,15 @@ public class QueuedContextStruct implements Serializable { */ private String variableName; - private long queuedAt = 0; + private int priority = 0; // cached candidates public transient List candidates = null; private static final Logger LOGGER = Logger.getLogger(QueuedContextStruct.class.getName()); + private String id = null; + /* * Constructor for the QueuedContextStruct class. */ @@ -64,12 +66,34 @@ public QueuedContextStruct( StepContext context, List lockableResourcesStruct, String resourceDescription, - String variableName) { + String variableName, + int priority) { this.context = context; this.lockableResourcesStruct = lockableResourcesStruct; this.resourceDescription = resourceDescription; this.variableName = variableName; - this.queuedAt = new Date().getTime(); + this.priority = priority; + this.id = UUID.randomUUID().toString(); + } + + @Restricted(NoExternalUse.class) + public int compare(QueuedContextStruct other) { + if (this.priority > other.getPriority()) return -1; + else if (this.priority == other.getPriority()) return 0; + else return 1; + } + + @Restricted(NoExternalUse.class) + public int getPriority() { + return this.priority; + } + + @Restricted(NoExternalUse.class) + public String getId() { + if (this.id == null) { + this.id = UUID.randomUUID().toString(); + } + return this.id; } /* @@ -134,20 +158,16 @@ public String getVariableName() { return this.variableName; } - /** Get time-ticks, when the item has been added into queue */ - @Restricted(NoExternalUse.class) - public long getAddTime() { - return queuedAt; - } - @Restricted(NoExternalUse.class) public String toString() { return "build: " + this.getBuild() + " resources: " + this.getResourceDescription() - + " added at: " - + this.getAddTime(); + + " priority: " + + this.priority + + " id: " + + this.getId(); } @Restricted(NoExternalUse.class) diff --git a/src/main/resources/org/jenkins/plugins/lockableresources/LockStep/config.jelly b/src/main/resources/org/jenkins/plugins/lockableresources/LockStep/config.jelly index c481710b9..b39d2bfcc 100644 --- a/src/main/resources/org/jenkins/plugins/lockableresources/LockStep/config.jelly +++ b/src/main/resources/org/jenkins/plugins/lockableresources/LockStep/config.jelly @@ -17,11 +17,14 @@ - + + + + diff --git a/src/main/resources/org/jenkins/plugins/lockableresources/LockStep/config.properties b/src/main/resources/org/jenkins/plugins/lockableresources/LockStep/config.properties index 05ec09e76..3a11c8217 100644 --- a/src/main/resources/org/jenkins/plugins/lockableresources/LockStep/config.properties +++ b/src/main/resources/org/jenkins/plugins/lockableresources/LockStep/config.properties @@ -25,7 +25,8 @@ entry.label.title=Label entry.quantity.title=Quantity entry.variable.title=Result variable entry.inversePrecedence.checkbox.title=Inverse precedence -entry.inversePrecedence.skipIfLocked.title=Skip queue +entry.skipIfLocked.title=Skip if locked +entry.priority.title=Queue priority entry.resourceSelectStrategy.title=Strategy for resource selection entry.extra.title=Extra resources entry.extra.add=Add Resource diff --git a/src/main/resources/org/jenkins/plugins/lockableresources/LockStep/config_cs.properties b/src/main/resources/org/jenkins/plugins/lockableresources/LockStep/config_cs.properties index f8e1171ba..e8c783f38 100644 --- a/src/main/resources/org/jenkins/plugins/lockableresources/LockStep/config_cs.properties +++ b/src/main/resources/org/jenkins/plugins/lockableresources/LockStep/config_cs.properties @@ -25,7 +25,7 @@ entry.label.title=Popisek entry.quantity.title=Mno\u017estv\u00ed entry.variable.title=Prom\u011bnn\u00e1 v\u00fdsledk\u016f entry.inversePrecedence.checkbox.title=Obr\u00e1cen\u00e9 po\u0159ad\u00ed -entry.inversePrecedence.skipIfLocked.title=P\u0159esko\u010dit frontu +entry.skipIfLocked.title=P\u0159esko\u010dit frontu entry.resourceSelectStrategy.title=Strategie v\u00fdb\u011bru zdroj\u016f entry.extra.title=Dodate\u010dn\u00e9 zdroje entry.extra.add=P\u0159idat zdroj diff --git a/src/main/resources/org/jenkins/plugins/lockableresources/LockStep/config_de.properties b/src/main/resources/org/jenkins/plugins/lockableresources/LockStep/config_de.properties index 6bf4800d1..6294b40c8 100644 --- a/src/main/resources/org/jenkins/plugins/lockableresources/LockStep/config_de.properties +++ b/src/main/resources/org/jenkins/plugins/lockableresources/LockStep/config_de.properties @@ -25,7 +25,7 @@ entry.label.title=Label entry.quantity.title=Anzahl entry.variable.title=Ergebnisvariable entry.inversePrecedence.checkbox.title=Umgekehrter Vorrang -entry.inversePrecedence.skipIfLocked.title=Warteschlange \u00fcberspringen +entry.skipIfLocked.title=Warteschlange \u00fcberspringen entry.resourceSelectStrategy.title=Strategie f\u00fcr Ressourcenauswahl entry.extra.title=Zus\u00e4tzliche Ressourcen entry.extra.add=Ressource hinzuf\u00fcgen diff --git a/src/main/resources/org/jenkins/plugins/lockableresources/LockStep/config_fr.properties b/src/main/resources/org/jenkins/plugins/lockableresources/LockStep/config_fr.properties index 11f985a2e..2268459e1 100644 --- a/src/main/resources/org/jenkins/plugins/lockableresources/LockStep/config_fr.properties +++ b/src/main/resources/org/jenkins/plugins/lockableresources/LockStep/config_fr.properties @@ -25,6 +25,6 @@ entry.label.title=Libell\u00e9 entry.quantity.title=Quantit\u00e9 entry.variable.title=Variable r\u00e9sultat entry.inversePrecedence.checkbox.title=Priorit\u00e9 invers\u00e9e -entry.inversePrecedence.skipIfLocked.title=Sauter la file d'attente +entry.skipIfLocked.title=Sauter la file d'attente entry.extra.title=Ressources suppl\u00e9mentaires entry.extra.add=Ajouter une ressource \ No newline at end of file diff --git a/src/main/resources/org/jenkins/plugins/lockableresources/LockStep/config_sk.properties b/src/main/resources/org/jenkins/plugins/lockableresources/LockStep/config_sk.properties index 3af226660..3ba850a01 100644 --- a/src/main/resources/org/jenkins/plugins/lockableresources/LockStep/config_sk.properties +++ b/src/main/resources/org/jenkins/plugins/lockableresources/LockStep/config_sk.properties @@ -25,7 +25,7 @@ entry.label.title=\u0160t\u00edtok entry.quantity.title=Po\u010det entry.variable.title=Pramenn\u00e1 s v\u00fdsledkami entry.inversePrecedence.checkbox.title=Opa\u010dn\u00e9 poradie -entry.inversePrecedence.skipIfLocked.title=Predbehn\u00fa\u0165 rad +entry.skipIfLocked.title=Predbehn\u00fa\u0165 rad entry.resourceSelectStrategy.title=Strat\u00e9gia v\u00fdberu zdrojov entry.extra.title=Dodato\u010dn\u00e9 zdroje entry.extra.add=Prida\u0165 zdroj diff --git a/src/main/resources/org/jenkins/plugins/lockableresources/LockStep/help-priority.html b/src/main/resources/org/jenkins/plugins/lockableresources/LockStep/help-priority.html new file mode 100644 index 000000000..9289be778 --- /dev/null +++ b/src/main/resources/org/jenkins/plugins/lockableresources/LockStep/help-priority.html @@ -0,0 +1,14 @@ +
+

+ The priority of the lock, +

+

+ which takes an integer number that defines the order in which concurrent jobs waiting for the same resource are served. + The job with the highest number would get the resource first. If the priority is equal, the current precedence (first comes first) would be applied. +

+

+ See also + examples + . +

+
diff --git a/src/main/resources/org/jenkins/plugins/lockableresources/Messages.properties b/src/main/resources/org/jenkins/plugins/lockableresources/Messages.properties index 7c85b5e1a..195b34781 100644 --- a/src/main/resources/org/jenkins/plugins/lockableresources/Messages.properties +++ b/src/main/resources/org/jenkins/plugins/lockableresources/Messages.properties @@ -20,6 +20,9 @@ LockableResourcesRootAction.StealPermission.Description=This permission grants t LockableResourcesRootAction.ViewPermission=View LockableResourcesRootAction.ViewPermission.Description=This permission grants the ability to view \ lockable resources. +LockableResourcesRootAction.QueueChangeOrderPermission=Queue +LockableResourcesRootAction.QueueChangeOrderPermission.Description=This permission grants the ability to \ + manually manipulate the lockable resources queue.. LockedResourcesBuildAction.displayName=Used lockable resources # Java errors error.labelDoesNotExist=The resource label does not exist: {0}. @@ -28,9 +31,13 @@ error.labelOrNameMustBeSpecified=Either resource label or resource name must be error.labelAndNameSpecified=Resource label and resource name cannot be specified simultaneously. error.labelAndNameOrGroovySpecified=Only resource label, groovy expression, or resource names can be defined, not more than one. error.couldNotParseToint=Could not parse the given value as integer. -error.givenAmountIsGreaterThatResurcesAmount=Given amount %d is greater than amount of resources: %d. +error.givenAmountIsGreaterThatResourcesAmount=Given amount %d is greater than amount of resources: %d. error.resourceAlreadyLocked=Resource {0} already reserved or locked! error.invalidResourceSelectionStrategy=The strategy "{0}" is not supported. Valid options are {1}. +error.isNotANumber=The queue position must be a number. Given: {0} +error.queuePositionOutOfRange=The queue position {0} is out of range (1 - {1})! +error.queueDoesNotExist=The queue {0} does not (anymore) exist. +error.inversePrecedenceAndPriorityAreSet=The "inverse precedence" option is not compatible with "queue priority" option! # display-names LockStep.displayName=Lock shared resource LockStepResource.displayName=Resource diff --git a/src/main/resources/org/jenkins/plugins/lockableresources/actions/LockableResourcesRootAction/index.jelly b/src/main/resources/org/jenkins/plugins/lockableresources/actions/LockableResourcesRootAction/index.jelly index 206c7ecd9..a279eb09f 100644 --- a/src/main/resources/org/jenkins/plugins/lockableresources/actions/LockableResourcesRootAction/index.jelly +++ b/src/main/resources/org/jenkins/plugins/lockableresources/actions/LockableResourcesRootAction/index.jelly @@ -51,7 +51,7 @@
-
+
diff --git a/src/main/resources/org/jenkins/plugins/lockableresources/actions/LockableResourcesRootAction/tableQueue/table.jelly b/src/main/resources/org/jenkins/plugins/lockableresources/actions/LockableResourcesRootAction/tableQueue/table.jelly index aac07c82b..4f6231c00 100644 --- a/src/main/resources/org/jenkins/plugins/lockableresources/actions/LockableResourcesRootAction/tableQueue/table.jelly +++ b/src/main/resources/org/jenkins/plugins/lockableresources/actions/LockableResourcesRootAction/tableQueue/table.jelly @@ -23,14 +23,25 @@ THE SOFTWARE. --> + +