params,
+ Logger log) {
+ try {
+ return tryQueue(requiredResources, queueItemId, queueItemProject, number, params, log);
+ } catch (ExecutionException ex) {
+ if (LOGGER.isLoggable(Level.WARNING)) {
+ String itemName = queueItemProject + " (id=" + queueItemId + ")";
+ LOGGER.log(
+ Level.WARNING, "Failed to queue item " + itemName, ex.getCause() != null ? ex.getCause() : ex);
+ }
+ return null;
+ }
+ }
+
+ // ---------------------------------------------------------------------------
+ /**
+ * If the lockable resource availability was evaluated before and cached to avoid frequent
+ * re-evaluations under queued pressure when there are no resources to give, we should state that
+ * a resource is again instantly available for re-evaluation when we know it was busy and right
+ * now is being freed. Note that a resource may be (both or separately) locked by a build and/or
+ * reserved by a user (or stolen from build to user) so we only un-cache it here if it becomes
+ * completely available. Called as a helper from methods that unlock/unreserve/reset (or
+ * indirectly - recycle) stuff.
+ *
+ * NOTE for people using LR or LRM methods directly to add some abilities in their pipelines
+ * that are not provided by plugin: the `cachedCandidates` is an LRM concept, so if you tell a
+ * resource (LR instance) directly to unlock/unreserve, it has no idea to clean itself from this
+ * cache, and may be considered busy in queuing for some time afterward.
+ */
+ public boolean uncacheIfFreeing(LockableResource candidate, boolean unlocking, boolean unreserving) {
+ if (candidate.isLocked() && !unlocking) return false;
+
+ // "stolen" state helps track that a resource is currently not
+ // reserved for the same entity as it was originally given to;
+ // this flag is cleared during un-reservation.
+ if ((candidate.isReserved() || candidate.isStolen()) && !unreserving) return false;
+
+ if (cachedCandidates.size() == 0) return true;
+
+ // Per https://guava.dev/releases/19.0/api/docs/com/google/common/cache/Cache.html
+ // "Modifications made to the map directly affect the cache."
+ // so it is both a way for us to iterate the cache and to edit
+ // the lists it stores per queue.
+ Map> cachedCandidatesMap = cachedCandidates.asMap();
+ for (Map.Entry> entry : cachedCandidatesMap.entrySet()) {
+ Long queueItemId = entry.getKey();
+ List candidates = entry.getValue();
+ if (candidates != null && (candidates.isEmpty() || candidates.contains(candidate))) {
+ cachedCandidates.invalidate(queueItemId);
+ }
+ }
+
+ return true;
+ }
+
+ // ---------------------------------------------------------------------------
+ /**
+ * Try to acquire the resources required by the task.
+ *
+ * @param number Number of resources to acquire. {@code 0} means all
+ * @return List of the locked resources if the task has been accepted. {@code null} if the item is
+ * still waiting for the resources
+ * @throws ExecutionException Cannot queue the resource due to the execution failure. Carries info
+ * in the cause
+ * @since 2.0
+ */
+ @CheckForNull
+ @Restricted(NoExternalUse.class)
+ public List tryQueue(
+ LockableResourcesStruct requiredResources,
+ long queueItemId,
+ String queueItemProject,
+ int number,
+ Map params,
+ Logger log)
+ throws ExecutionException {
+ List selected = new ArrayList<>();
+ synchronized (syncResources) {
+ if (!checkCurrentResourcesStatus(selected, queueItemProject, queueItemId, log)) {
+ // The project has another buildable item waiting -> bail out
+ log.log(
+ Level.FINEST,
+ "{0} has another build waiting resources." + " Waiting for it to proceed first.",
+ new Object[] {queueItemProject});
+ return null;
+ }
+
+ final SecureGroovyScript systemGroovyScript;
+ try {
+ systemGroovyScript = requiredResources.getResourceMatchScript();
+ } catch (Descriptor.FormException x) {
+ throw new ExecutionException(x);
+ }
+ boolean candidatesByScript = (systemGroovyScript != null);
+ List candidates = requiredResources.required; // default candidates
+
+ if (candidatesByScript || (requiredResources.label != null && !requiredResources.label.isEmpty())) {
+
+ candidates = cachedCandidates.getIfPresent(queueItemId);
+ if (candidates != null) {
+ candidates.retainAll(this.resources);
+ } else {
+ candidates = (systemGroovyScript == null)
+ ? getResourcesWithLabel(requiredResources.label)
+ : getResourcesMatchingScript(systemGroovyScript, params);
+ cachedCandidates.put(queueItemId, candidates);
+ }
+ }
+
+ for (LockableResource rs : candidates) {
+ if (number != 0 && (selected.size() >= number)) break;
+ if (!rs.isReserved() && !rs.isLocked() && !rs.isQueued()) selected.add(rs);
+ }
+
+ // if did not get wanted amount or did not get all
+ final int required_amount = getRequiredAmount(number, candidatesByScript, candidates);
+
+ if (selected.size() != required_amount) {
+ log.log(
+ Level.FINEST,
+ "{0} found {1} resource(s) to queue. Waiting for correct amount: {2}.",
+ new Object[] {queueItemProject, selected.size(), required_amount});
+ // just to be sure, clean up
+ for (LockableResource x : this.resources) {
+ if (x.getQueueItemProject() != null
+ && x.getQueueItemProject().equals(queueItemProject)) x.unqueue();
+ }
+ return null;
+ }
+
+ for (LockableResource rsc : selected) {
+ rsc.setQueued(queueItemId, queueItemProject);
+ }
+ }
+ return selected;
+ }
+
+ // ---------------------------------------------------------------------------
+ /**
+ * Returns the amount of resources required by the task.
+ * If the groovy script does not return any candidates, it means nothing is needed, even if a
+ * higher amount is specified. A valid use case is a Matrix job, when not all configurations need resources.
+ */
+ private static int getRequiredAmount(int number, boolean candidatesByScript, List candidates) {
+ final int required_amount;
+ if (candidatesByScript && candidates.isEmpty()) {
+ required_amount = 0;
+ } else {
+ required_amount = number == 0 ? candidates.size() : number;
+ }
+ return required_amount;
+ }
+
+ // ---------------------------------------------------------------------------
+ // Adds already selected (in previous queue round) resources to 'selected'
+ // Return false if another item queued for this project -> bail out
+ private boolean checkCurrentResourcesStatus(
+ List selected, String project, long taskId, Logger log) {
+ for (LockableResource r : this.resources) {
+ // This project might already have something in queue
+ String rProject = r.getQueueItemProject();
+ if (rProject != null && rProject.equals(project)) {
+ if (r.isQueuedByTask(taskId)) {
+ // this item has queued the resource earlier
+ selected.add(r);
+ } else {
+ // The project has another buildable item waiting -> bail out
+ log.log(
+ Level.FINEST,
+ "{0} has another build that already queued resource {1}. Continue queueing.",
+ new Object[] {project, r});
+ return false;
+ }
+ }
+ }
+ return true;
+ }
+
+ // ---------------------------------------------------------------------------
+ @Deprecated
+ public boolean lock(List resources, Run, ?> build, @Nullable StepContext context) {
+ return this.lock(resources, build);
+ }
+
+ // ---------------------------------------------------------------------------
+ @Deprecated
+ public boolean lock(
+ List resources,
+ Run, ?> build,
+ @Nullable StepContext context,
+ @Nullable String logmessage,
+ final String variable,
+ boolean inversePrecedence) {
+ return this.lock(resources, build);
+ }
+
+ // ---------------------------------------------------------------------------
+ /** Try to lock the resource and return true if locked. */
+ public boolean lock(List resourcesToLock, Run, ?> build) {
+
+ LOGGER.fine("lock it: " + resourcesToLock + " for build " + build);
+
+ if (build == null) {
+ LOGGER.warning("lock() will fails, because the build does not exits. " + resourcesToLock);
+ return false; // not locked
+ }
+
+ String cause = getCauses(resourcesToLock);
+ if (!cause.isEmpty()) {
+ LOGGER.warning("lock() for build " + build + " will fails, because " + cause);
+ return false; // not locked
+ }
+
+ for (LockableResource r : resourcesToLock) {
+ r.unqueue();
+ r.setBuild(build);
+ }
+
+ LockedResourcesBuildAction.findAndInitAction(build).addUsedResources(getResourcesNames(resourcesToLock));
+
+ save();
+
+ return true;
+ }
+
+ // ---------------------------------------------------------------------------
+ private void freeResources(List unlockResources, Run, ?> build) {
+
+ LOGGER.fine("free it: " + unlockResources);
+
+ // make sure there is a list of resource names to unlock
+ if (unlockResources == null || unlockResources.isEmpty() || build == null) {
+ return;
+ }
+
+ List toBeRemoved = new ArrayList<>();
+
+ for (LockableResource resource : unlockResources) {
+ // No more contexts, unlock resource
+
+ // the resource has been currently unlocked (like by LRM page - button unlock, or by API)
+ if (!build.equals(resource.getBuild())) continue;
+
+ resource.unqueue();
+ resource.setBuild(null);
+ uncacheIfFreeing(resource, true, false);
+
+ if (resource.isEphemeral()) {
+ LOGGER.fine("Remove ephemeral resource: " + resource);
+ toBeRemoved.add(resource);
+ }
+ }
+
+ LockedResourcesBuildAction.findAndInitAction(build).removeUsedResources(getResourcesNames(unlockResources));
+
+ // remove all ephemeral resources
+ removeResources(toBeRemoved);
+ }
+
+ public void unlockBuild(@Nullable Run, ?> build) {
+
+ if (build == null) {
+ return;
+ }
+
+ List resourcesInUse =
+ LockedResourcesBuildAction.findAndInitAction(build).getCurrentUsedResourceNames();
+
+ if (resourcesInUse.isEmpty()) {
+ return;
+ }
+ unlockNames(resourcesInUse, build);
+ }
+
+ // ---------------------------------------------------------------------------
+ public void unlockNames(@Nullable List resourceNamesToUnLock, Run, ?> build) {
+
+ // make sure there is a list of resource names to unlock
+ if (resourceNamesToUnLock == null || resourceNamesToUnLock.isEmpty()) {
+ return;
+ }
+ synchronized (syncResources) {
+ unlockResources(this.fromNames(resourceNamesToUnLock), build);
+ }
+ }
+
+ // ---------------------------------------------------------------------------
+ public void unlockResources(List resourcesToUnLock) {
+ unlockResources(resourcesToUnLock, resourcesToUnLock.get(0).getBuild());
+ }
+
+ // ---------------------------------------------------------------------------
+ public void unlockResources(List resourcesToUnLock, Run, ?> build) {
+ if (resourcesToUnLock == null || resourcesToUnLock.isEmpty()) {
+ return;
+ }
+ synchronized (syncResources) {
+ this.freeResources(resourcesToUnLock, build);
+
+ while (proceedNextContext()) {
+ // process as many contexts as possible
+ }
+
+ save();
+ }
+ }
+
+ 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) {
+ LOGGER.fine("No context is queued which can be started once these resources are free'd.");
+ return false;
+ }
+ LOGGER.finest("nextContext candidates: " + nextContext.candidates);
+ List requiredResourceForNextContext =
+ this.fromNames(nextContext.candidates, /*create un-existent resources */ true);
+ LOGGER.finest("nextContext real candidates: " + requiredResourceForNextContext);
+ // remove context from queue and process it
+
+ Run, ?> build = nextContext.getBuild();
+ if (build == null) {
+ // this shall never happen
+ // skip this context, as the build cannot be retrieved (maybe it was deleted while
+ // running?)
+ LOGGER.warning("Skip this context, as the build cannot be retrieved");
+ return true;
+ }
+ boolean locked = this.lock(requiredResourceForNextContext, build);
+ if (!locked) {
+ // defensive line, shall never happen
+ LOGGER.warning("Can not lock resources: " + requiredResourceForNextContext);
+ // to eliminate possible endless loop
+ return false;
+ }
+
+ // build env vars
+ LinkedHashMap> resourcesToLock = new LinkedHashMap<>();
+ for (LockableResource requiredResource : requiredResourceForNextContext) {
+ resourcesToLock.put(requiredResource.getName(), requiredResource.getProperties());
+ }
+
+ this.unqueueContext(nextContext.getContext());
+
+ // continue with next context
+ LOGGER.fine("Continue with next context: " + nextContext);
+ LockStepExecution.proceed(
+ resourcesToLock,
+ nextContext.getContext(),
+ nextContext.getResourceDescription(),
+ nextContext.getVariableName());
+ return true;
+ }
+
+ // ---------------------------------------------------------------------------
+ /** Returns names (IDs) of given *resources*. */
+ @Restricted(NoExternalUse.class)
+ public static List getResourcesNames(final List resources) {
+ List resourceNames = new ArrayList<>();
+ if (resources != null) {
+ for (LockableResource resource : resources) {
+ resourceNames.add(resource.getName());
+ }
+ }
+ return resourceNames;
+ }
+
+ // ---------------------------------------------------------------------------
+ /** Returns names (IDs) off all existing resources (inclusive ephemeral) */
+ @Restricted(NoExternalUse.class)
+ public List getAllResourcesNames() {
+ synchronized (syncResources) {
+ return getResourcesNames(this.resources);
+ }
+ }
+
+ // ---------------------------------------------------------------------------
+ /**
+ * Returns the next queued context with all its requirements satisfied.
+ *
+ */
+ @CheckForNull
+ private QueuedContextStruct getNextQueuedContext() {
+
+ LOGGER.fine("current queue size: " + this.queuedContexts.size());
+ LOGGER.finest("current queue: " + this.queuedContexts);
+ List orphan = new ArrayList<>();
+ QueuedContextStruct nextEntry = null;
+
+ // the first one added lock is the oldest one, and this wins
+
+ 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);
+ }
+
+ return nextEntry;
+ }
+
+ // ---------------------------------------------------------------------------
+ QueuedContextStruct getNextQueuedContextEntry(QueuedContextStruct entry) {
+ List candidates = this.getAvailableResources(entry.getResources());
+ if (candidates == null || candidates.isEmpty()) {
+ return null;
+ }
+
+ entry.candidates = getResourcesNames(candidates);
+ LOGGER.fine("take this: " + entry);
+ return entry;
+ }
+
+ // ---------------------------------------------------------------------------
+ /** Returns current queue */
+ @Restricted(NoExternalUse.class) // used by jelly
+ public List getCurrentQueuedContext() {
+ synchronized (syncResources) {
+ return Collections.unmodifiableList(this.queuedContexts);
+ }
+ }
+
+ // ---------------------------------------------------------------------------
+ /** Creates the resource if it does not exist. */
+ public boolean createResource(@CheckForNull String name) {
+ name = Util.fixEmptyAndTrim(name);
+ LockableResource resource = new LockableResource(name);
+ resource.setEphemeral(true);
+
+ return this.addResource(resource, /*doSave*/ true);
+ }
+
+ // ---------------------------------------------------------------------------
+ public boolean createResourceWithLabel(@CheckForNull String name, @CheckForNull String label) {
+ name = Util.fixEmptyAndTrim(name);
+ label = Util.fixEmptyAndTrim(label);
+ LockableResource resource = new LockableResource(name);
+ resource.setLabels(label);
+
+ return this.addResource(resource, /*doSave*/ true);
+ }
+
+ // ---------------------------------------------------------------------------
+ public boolean createResourceWithLabelAndProperties(
+ @CheckForNull String name, @CheckForNull String label, final Map properties) {
+ if (properties == null) {
+ return false;
+ }
+
+ name = Util.fixEmptyAndTrim(name);
+ label = Util.fixEmptyAndTrim(label);
+ LockableResource resource = new LockableResource(name);
+ resource.setLabels(label);
+ resource.setProperties(properties.entrySet().stream()
+ .map(e -> {
+ LockableResourceProperty p = new LockableResourceProperty();
+ p.setName(e.getKey());
+ p.setValue(e.getValue());
+ return p;
+ })
+ .collect(Collectors.toList()));
+
+ return this.addResource(resource, /*doSave*/ true);
+ }
+
+ // ---------------------------------------------------------------------------
+ @Restricted(NoExternalUse.class)
+ public boolean addResource(@Nullable final LockableResource resource) {
+ return this.addResource(resource, /*doSave*/ false);
+ }
+ // ---------------------------------------------------------------------------
+ @Restricted(NoExternalUse.class)
+ public boolean addResource(@Nullable final LockableResource resource, final boolean doSave) {
+
+ if (resource == null || resource.getName() == null || resource.getName().isEmpty()) {
+ LOGGER.warning("Internal failure: We will add wrong resource: '" + resource + "' " + getStack());
+ return false;
+ }
+ synchronized (syncResources) {
+ if (this.resourceExist(resource.getName())) {
+ LOGGER.finest("We will add existing resource: " + resource + getStack());
+ return false;
+ }
+ this.resources.add(resource);
+ LOGGER.fine("Resource added : " + resource);
+ if (doSave) {
+ this.save();
+ }
+ }
+ return true;
+ }
+
+ // ---------------------------------------------------------------------------
+ /**
+ * Reserves an available resource for the userName indefinitely (until that person, or some
+ * explicit scripted action, decides to release the resource).
+ */
+ public boolean reserve(List resources, String userName) {
+ synchronized (syncResources) {
+ for (LockableResource r : resources) {
+ if (!r.isFree()) {
+ return false;
+ }
+ }
+ for (LockableResource r : resources) {
+ r.reserve(userName);
+ }
+ save();
+ }
+ return true;
+ }
+
+ // ---------------------------------------------------------------------------
+ /**
+ * Reserves a resource that may be or not be locked by some job (or reserved by some user)
+ * already, giving it away to the userName indefinitely (until that person, or some explicit
+ * scripted action, later decides to release the resource).
+ */
+ public boolean steal(List resources, String userName) {
+ synchronized (syncResources) {
+ for (LockableResource r : resources) {
+ r.setReservedBy(userName);
+ r.setStolen();
+ }
+ unlockResources(resources);
+ Date date = new Date();
+ for (LockableResource r : resources) {
+ r.setReservedTimestamp(date);
+ }
+ save();
+ }
+ return true;
+ }
+
+ // ---------------------------------------------------------------------------
+ /**
+ * Reserves a resource that may be or not be reserved by some person already, giving it away to
+ * the userName indefinitely (until that person, or some explicit scripted action, decides to
+ * release the resource).
+ */
+ public void reassign(List resources, String userName) {
+ synchronized (syncResources) {
+ Date date = new Date();
+ for (LockableResource r : resources) {
+ if (!r.isFree()) {
+ r.unReserve();
+ }
+ r.setReservedBy(userName);
+ r.setReservedTimestamp(date);
+ }
+ save();
+ }
+ }
+
+ // ---------------------------------------------------------------------------
+ private void unreserveResources(@NonNull List resources) {
+ for (LockableResource l : resources) {
+ uncacheIfFreeing(l, false, true);
+ l.unReserve();
+ }
+ save();
+ }
+
+ // ---------------------------------------------------------------------------
+ public void unreserve(List resources) {
+ // make sure there is a list of resources to unreserve
+ if (resources == null || resources.isEmpty()) {
+ return;
+ }
+
+ synchronized (syncResources) {
+ LOGGER.fine("unreserve " + resources);
+ unreserveResources(resources);
+
+ proceedNextContext();
+
+ save();
+ }
+ }
+
+ // ---------------------------------------------------------------------------
+ @NonNull
+ @Override
+ public String getDisplayName() {
+ return Messages.LockableResourcesManager_displayName();
+ }
+
+ // ---------------------------------------------------------------------------
+ public void reset(List resources) {
+ synchronized (syncResources) {
+ for (LockableResource r : resources) {
+ uncacheIfFreeing(r, true, true);
+ r.reset();
+ }
+ save();
+ }
+ }
+
+ // ---------------------------------------------------------------------------
+ /**
+ * Make the lockable resource reusable and notify the queue(s), if any WARNING: Do not use this
+ * from inside the lock step closure which originally locked this resource, to avoid nasty
+ * surprises! Namely, this *might* let a second consumer use the resource quickly, but when the
+ * original closure ends and unlocks again that resource, a third consumer might then effectively
+ * hijack it from the second one.
+ */
+ public void recycle(List resources) {
+ synchronized (syncResources) {
+ // Not calling reset() because that also un-queues the resource
+ // and we want to proclaim it is usable (if anyone is waiting)
+ this.unlockResources(resources);
+ this.unreserve(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 (syncResources) {
+ if (newPosition < 0 || newPosition >= this.queuedContexts.size()) {
+ throw new IOException(
+ Messages.error_queuePositionOutOfRange(newPosition + 1, this.queuedContexts.size()));
+ }
+
+ 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(StaplerRequest2 req, JSONObject json) {
+ synchronized (syncResources) {
+ try (BulkChange bc = new BulkChange(this)) {
+ req.bindJSON(this, json);
+ bc.commit();
+ } catch (IOException exception) {
+ LOGGER.log(Level.WARNING, "Exception occurred while committing bulkchange operation.", exception);
+ return false;
+ }
+ }
+ return true;
+ }
+
+ // ---------------------------------------------------------------------------
+ public List getAvailableResources(final List requiredResourcesList) {
+ return this.getAvailableResources(requiredResourcesList, null, null);
+ }
+
+ // ---------------------------------------------------------------------------
+ /** Function removes all given resources */
+ public void removeResources(List toBeRemoved) {
+ synchronized (syncResources) {
+ this.resources.removeAll(toBeRemoved);
+ }
+ }
+
+ // ---------------------------------------------------------------------------
+ /**
+ * Checks if there are enough resources available to satisfy the requirements specified within
+ * requiredResources and returns the necessary available resources. If not enough resources are
+ * available, returns null.
+ */
+ public List getAvailableResources(
+ final List requiredResourcesList,
+ final @Nullable PrintStream logger,
+ final @Nullable ResourceSelectStrategy selectStrategy) {
+
+ LOGGER.finest("getAvailableResources, " + requiredResourcesList);
+ List candidates = new ArrayList<>();
+ for (LockableResourcesStruct requiredResources : requiredResourcesList) {
+ List available = new ArrayList<>();
+ // filter by labels
+ if (requiredResources.label != null && !requiredResources.label.isBlank()) {
+ // get required amount first
+ int requiredAmount = 0;
+ if (requiredResources.requiredNumber != null) {
+ try {
+ requiredAmount = Integer.parseInt(requiredResources.requiredNumber);
+ } catch (NumberFormatException ignored) {
+ }
+ }
+
+ available = this.getFreeResourcesWithLabel(
+ requiredResources.label, requiredAmount, selectStrategy, logger, candidates);
+ } else if (requiredResources.required != null) {
+ // resource by name requested
+
+ // this is a little hack. The 'requiredResources.required' is a copy, and we need to find
+ // all of them in LRM
+ // fromNames() also re-create the resource (ephemeral things)
+ available = fromNames(
+ getResourcesNames(requiredResources.required), /*create un-existent resources */ true);
+
+ if (!this.areAllAvailable(available)) {
+ available = null;
+ }
+ } else {
+ LOGGER.warning("getAvailableResources, Not implemented: " + requiredResources);
+ }
+
+ if (available == null || available.isEmpty()) {
+ LOGGER.finest("No available resources found " + requiredResourcesList);
+ return null;
+ }
+
+ final boolean isPreReserved = !Collections.disjoint(candidates, available);
+ if (isPreReserved) {
+ // FIXME I think this is failure
+ // You use filter label1 and it lock resource1 and then in extra you will lock resource1
+ // But when I allow this line, many tests will fails, and I am pretty sure it will throws
+ // exceptions on end-user pipelines
+ // So when we want to fix, it it might be braking-change
+ // Therefore keep it here as warning for now
+ printLogs("Extra filter tries to allocate pre-reserved resources.", logger, Level.WARNING);
+ available.removeAll(candidates);
+ }
+
+ candidates.addAll(available);
+ }
+
+ return candidates;
+ }
+
+ // ---------------------------------------------------------------------------
+ private boolean areAllAvailable(List resources) {
+ for (LockableResource resource : resources) {
+ if (!resource.isFree()) {
+ return false;
+ }
+ }
+ return true;
+ }
+
+ // ---------------------------------------------------------------------------
+ public static void printLogs(final String msg, final Level level, Logger L, final @Nullable PrintStream logger) {
+ L.log(level, msg);
+
+ if (logger != null) {
+ if (level == Level.WARNING || level == Level.SEVERE) logger.println(level.getLocalizedName() + ": " + msg);
+ else logger.println(msg);
+ }
+ }
+
+ // ---------------------------------------------------------------------------
+ private static void printLogs(final String msg, final @Nullable PrintStream logger, final Level level) {
+ printLogs(msg, level, LOGGER, logger);
+ }
+
+ // ---------------------------------------------------------------------------
+ @CheckForNull
+ @Restricted(NoExternalUse.class)
+ private List getFreeResourcesWithLabel(
+ @NonNull String label,
+ long amount,
+ final @Nullable ResourceSelectStrategy selectStrategy,
+ final @Nullable PrintStream logger,
+ final List alreadySelected) {
+ List found = new ArrayList<>();
+
+ List candidates = _getResourcesWithLabel(label, alreadySelected);
+ candidates.addAll(this.getResourcesWithLabel(label));
+
+ if (amount <= 0) {
+ amount = candidates.size();
+ }
+
+ if (candidates.size() < amount) {
+ printLogs(
+ "Found "
+ + candidates.size()
+ + " possible resource(s). Waiting for correct amount: "
+ + amount
+ + "."
+ + "This may remain stuck, until you create enough resources",
+ logger,
+ Level.WARNING);
+ return null; // there are not enough resources
+ }
+
+ if (selectStrategy != null && selectStrategy.equals(ResourceSelectStrategy.RANDOM)) {
+ Collections.shuffle(candidates);
+ }
+
+ for (LockableResource r : candidates) {
+ // TODO: it shall be used isFree() here, but in that case we need to change the
+ // logic in parametrized builds and that is much more effort as I want to spend here now
+ if (!r.isReserved() && !r.isLocked()) {
+ found.add(r);
+ }
+
+ if (amount > 0 && found.size() >= amount) {
+ return found;
+ }
+ }
+
+ String msg = "Found " + found.size() + " available resource(s). Waiting for correct amount: " + amount + ".";
+ if (enabledBlockedCount != 0) {
+ msg += "\nBlocking causes: " + getCauses(candidates);
+ }
+ printLogs(msg, logger, Level.FINE);
+
+ return null;
+ }
+
+ // ---------------------------------------------------------------------------
+ // for debug purpose
+ private String getCauses(List resources) {
+ StringBuilder buf = new StringBuilder();
+ int currentSize = 0;
+ for (LockableResource resource : resources) {
+ String cause = resource.getLockCauseDetail();
+ if (cause == null) continue; // means it is free, not blocked
+
+ currentSize++;
+ if (enabledBlockedCount > 0 && currentSize == enabledBlockedCount) {
+ buf.append("\n ...");
+ break;
+ }
+ buf.append("\n ").append(cause);
+
+ final String queueCause = getQueueCause(resource);
+ if (!queueCause.isEmpty()) {
+ buf.append(queueCause);
+ }
+ }
+ return buf.toString();
+ }
+
+ // ---------------------------------------------------------------------------
+ // for debug purpose
+ private String getQueueCause(final LockableResource resource) {
+ Map, Integer> usage = new HashMap<>();
+
+ for (QueuedContextStruct entry : this.queuedContexts) {
+
+ Run, ?> build = entry.getBuild();
+ if (build == null) {
+ LOGGER.warning("Why we don`t have the build? " + entry);
+ continue;
+ }
+
+ int count = 0;
+ if (usage.containsKey(build)) {
+ count = usage.get(build);
+ }
+
+ for (LockableResourcesStruct _struct : entry.getResources()) {
+ if (_struct.isResourceRequired(resource)) {
+ LOGGER.fine("found " + resource + " " + count);
+ count++;
+ break;
+ }
+ }
+
+ usage.put(build, count);
+ }
+
+ StringBuilder buf = new StringBuilder();
+ int currentSize = 0;
+ for (Map.Entry, Integer> entry : usage.entrySet()) {
+ Run, ?> build = entry.getKey();
+ int count = entry.getValue();
+
+ if (build != null && count > 0) {
+ currentSize++;
+ buf.append("\n Queued ")
+ .append(count)
+ .append(" time(s) by build ")
+ .append(build.getFullDisplayName())
+ .append(" ")
+ .append(ModelHyperlinkNote.encodeTo(build));
+
+ if (currentSize >= enabledCausesCount) {
+ buf.append("\n ...");
+ break;
+ }
+ }
+ }
+ return buf.toString();
+ }
+
+ /*
+ * 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,
+ boolean inversePrecedence,
+ int priority) {
+ synchronized (syncResources) {
+ for (QueuedContextStruct entry : this.queuedContexts) {
+ if (entry.getContext() == context) {
+ LOGGER.warning("queueContext, duplicated, " + requiredResources);
return;
+ }
+ }
+
+ int queueIndex = 0;
+ QueuedContextStruct newQueueItem =
+ new QueuedContextStruct(context, requiredResources, resourceDescription, variableName, priority);
+
+ if (!inversePrecedence || priority != 0) {
+ queueIndex = this.queuedContexts.size() - 1;
+ for (; queueIndex >= 0; queueIndex--) {
+ QueuedContextStruct entry = this.queuedContexts.get(queueIndex);
+ final int rc = entry.compare(newQueueItem);
+ if (rc > 0) {
+ continue;
+ }
+ break;
+ }
+ queueIndex++;
+ }
- try {
- getConfigFile().write(this);
- } catch (IOException e) {
- LOGGER.log(Level.WARNING, "Failed to save " + getConfigFile(),e);
+ this.queuedContexts.add(queueIndex, newQueueItem);
+ printLogs(
+ requiredResources + " added into queue at position " + queueIndex,
+ newQueueItem.getLogger(),
+ Level.FINE);
+
+ save();
+ }
+ }
+
+ // ---------------------------------------------------------------------------
+ public boolean unqueueContext(StepContext context) {
+ synchronized (syncResources) {
+ for (Iterator iter = this.queuedContexts.listIterator(); iter.hasNext(); ) {
+ if (iter.next().getContext() == context) {
+ iter.remove();
+ save();
+ return true;
}
+ }
+ }
+ return false;
+ }
+
+ // ---------------------------------------------------------------------------
+ public static LockableResourcesManager get() {
+ return (LockableResourcesManager) Jenkins.get().getDescriptorOrDie(LockableResourcesManager.class);
+ }
+
+ // ---------------------------------------------------------------------------
+ @Override
+ public void save() {
+ if (enableSave == -1) {
+ // read system property and cache it.
+ enableSave = SystemProperties.getBoolean(Constants.SYSTEM_PROPERTY_DISABLE_SAVE) ? 0 : 1;
}
- private static final Logger LOGGER = Logger.getLogger(LockableResourcesManager.class.getName());
+ if (enableSave == 0) return; // saving is disabled
+ synchronized (syncResources) {
+ if (BulkChange.contains(this)) return;
+
+ try {
+ getConfigFile().write(this);
+ } catch (IOException e) {
+ LOGGER.log(Level.WARNING, "Failed to save " + getConfigFile(), e);
+ }
+ }
+ }
+
+ // ---------------------------------------------------------------------------
+ /** For testing purpose. */
+ @Restricted(NoExternalUse.class)
+ public LockableResource getFirst() {
+ return this.getResources().get(0);
+ }
}
diff --git a/src/main/java/org/jenkins/plugins/lockableresources/RequiredResourcesProperty.java b/src/main/java/org/jenkins/plugins/lockableresources/RequiredResourcesProperty.java
index 2da486021..ce995dddd 100644
--- a/src/main/java/org/jenkins/plugins/lockableresources/RequiredResourcesProperty.java
+++ b/src/main/java/org/jenkins/plugins/lockableresources/RequiredResourcesProperty.java
@@ -8,269 +8,312 @@
* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */
package org.jenkins.plugins.lockableresources;
+import edu.umd.cs.findbugs.annotations.CheckForNull;
+import edu.umd.cs.findbugs.annotations.NonNull;
import hudson.Extension;
import hudson.Util;
import hudson.model.AbstractProject;
import hudson.model.AutoCompletionCandidates;
+import hudson.model.Descriptor;
+import hudson.model.Item;
+import hudson.model.Job;
import hudson.model.JobProperty;
import hudson.model.JobPropertyDescriptor;
-import hudson.model.Job;
import hudson.util.FormValidation;
-
import java.util.ArrayList;
import java.util.List;
-
+import jenkins.model.Jenkins;
import net.sf.json.JSONObject;
-
import org.jenkinsci.plugins.scriptsecurity.sandbox.groovy.SecureGroovyScript;
import org.jenkinsci.plugins.scriptsecurity.scripts.ApprovalContext;
+import org.kohsuke.stapler.AncestorInPath;
import org.kohsuke.stapler.DataBoundConstructor;
import org.kohsuke.stapler.QueryParameter;
-import org.kohsuke.stapler.StaplerRequest;
-
-import javax.annotation.CheckForNull;
+import org.kohsuke.stapler.StaplerRequest2;
+import org.kohsuke.stapler.interceptor.RequirePOST;
public class RequiredResourcesProperty extends JobProperty> {
- private final String resourceNames;
- private final String resourceNamesVar;
- private final String resourceNumber;
- private final String labelName;
- private final @CheckForNull SecureGroovyScript resourceMatchScript;
-
- @DataBoundConstructor
- public RequiredResourcesProperty(String resourceNames,
- String resourceNamesVar, String resourceNumber,
- String labelName, @CheckForNull SecureGroovyScript resourceMatchScript) {
- super();
-
- if (resourceNames == null || resourceNames.trim().isEmpty()) {
- this.resourceNames = null;
- } else {
- this.resourceNames = resourceNames.trim();
- }
- if (resourceNamesVar == null || resourceNamesVar.trim().isEmpty()) {
- this.resourceNamesVar = null;
- } else {
- this.resourceNamesVar = resourceNamesVar.trim();
- }
- if (resourceNumber == null || resourceNumber.trim().isEmpty()) {
- this.resourceNumber = null;
- } else {
- this.resourceNumber = resourceNumber.trim();
- }
- String labelNamePreparation = (labelName == null || labelName.trim().isEmpty()) ? null : labelName.trim();
- if (resourceMatchScript != null) {
- this.resourceMatchScript = resourceMatchScript.configuringWithKeyItem();
- this.labelName = labelNamePreparation;
- } else if (labelName != null && labelName.startsWith(LockableResource.GROOVY_LABEL_MARKER)) {
- this.resourceMatchScript = new SecureGroovyScript(labelName.substring(LockableResource.GROOVY_LABEL_MARKER.length()),
- false, null).configuring(ApprovalContext.create());
- this.labelName = null;
- } else {
- this.resourceMatchScript = null;
- this.labelName = labelNamePreparation;
- }
- }
-
- @Deprecated
- public RequiredResourcesProperty(String resourceNames,
- String resourceNamesVar, String resourceNumber,
- String labelName) {
- this(resourceNames, resourceNamesVar, resourceNumber, labelName, null);
- }
-
- private Object readResolve() {
- // SECURITY-368 migration logic
- if (resourceMatchScript == null && labelName != null && labelName.startsWith(LockableResource.GROOVY_LABEL_MARKER)) {
- return new RequiredResourcesProperty(resourceNames, resourceNamesVar, resourceNumber, null,
- new SecureGroovyScript(labelName.substring(LockableResource.GROOVY_LABEL_MARKER.length()), false, null)
- .configuring(ApprovalContext.create()));
- }
-
- return this;
- }
-
- public String[] getResources() {
- String names = Util.fixEmptyAndTrim(resourceNames);
- if (names != null)
- return names.split("\\s+");
- else
- return new String[0];
- }
-
- public String getResourceNames() {
- return resourceNames;
- }
-
- public String getResourceNamesVar() {
- return resourceNamesVar;
- }
-
- public String getResourceNumber() {
- return resourceNumber;
- }
-
- public String getLabelName() {
- return labelName;
- }
-
- /**
- * Gets a system Groovy script to be executed in order to determine if the {@link LockableResource} matches the condition.
- * @return System Groovy Script if defined
- * @since TODO
- * @see LockableResource#scriptMatches(org.jenkinsci.plugins.scriptsecurity.sandbox.groovy.SecureGroovyScript, java.util.Map)
- */
- @CheckForNull
- public SecureGroovyScript getResourceMatchScript() {
- return resourceMatchScript;
- }
-
- @Extension
- public static class DescriptorImpl extends JobPropertyDescriptor {
-
- @Override
- public String getDisplayName() {
- return "Required Lockable Resources";
- }
-
- @Override
- public boolean isApplicable(Class extends Job> jobType) {
- return AbstractProject.class.isAssignableFrom(jobType);
- }
-
- @Override
- public RequiredResourcesProperty newInstance(StaplerRequest req, JSONObject formData) throws FormException {
- if (formData.containsKey("required-lockable-resources")) {
- return (RequiredResourcesProperty) super.newInstance(req, formData.getJSONObject("required-lockable-resources"));
- }
- return null;
- }
-
- public FormValidation doCheckResourceNames(@QueryParameter String value,
- @QueryParameter String labelName,
- @QueryParameter boolean script) {
- String labelVal = Util.fixEmptyAndTrim(labelName);
- String names = Util.fixEmptyAndTrim(value);
-
- if (names == null) {
- return FormValidation.ok();
- } else if (labelVal != null || script) {
- return FormValidation.error(
- "Only label, groovy expression, or resources can be defined, not more than one.");
- } else {
- List wrongNames = new ArrayList();
- for (String name : names.split("\\s+")) {
- boolean found = false;
- for (LockableResource r : LockableResourcesManager.get()
- .getResources()) {
- if (r.getName().equals(name)) {
- found = true;
- break;
- }
- }
- if (!found)
- wrongNames.add(name);
- }
- if (wrongNames.isEmpty()) {
- return FormValidation.ok();
- } else {
- return FormValidation
- .error("The following resources do not exist: "
- + wrongNames);
- }
- }
- }
-
- public FormValidation doCheckLabelName(
- @QueryParameter String value,
- @QueryParameter String resourceNames,
- @QueryParameter boolean script) {
- String label = Util.fixEmptyAndTrim(value);
- String names = Util.fixEmptyAndTrim(resourceNames);
-
- if (label == null) {
- return FormValidation.ok();
- } else if (names != null || script) {
- return FormValidation.error(
- "Only label, groovy expression, or resources can be defined, not more than one.");
- } else {
- if (LockableResourcesManager.get().isValidLabel(label)) {
- return FormValidation.ok();
- } else {
- return FormValidation.error(
- "The label does not exist: " + label);
- }
- }
- }
-
- public FormValidation doCheckResourceNumber(@QueryParameter String value,
- @QueryParameter String resourceNames,
+ private final String resourceNames;
+ private final String resourceNamesVar;
+ private final String resourceNumber;
+ private final String labelName;
+ private final @CheckForNull SecureGroovyScript resourceMatchScript;
+
+ @DataBoundConstructor
+ public RequiredResourcesProperty(
+ String resourceNames,
+ String resourceNamesVar,
+ String resourceNumber,
+ String labelName,
+ @CheckForNull SecureGroovyScript resourceMatchScript)
+ throws Descriptor.FormException {
+ super();
+
+ if (resourceNames == null || resourceNames.trim().isEmpty()) {
+ this.resourceNames = null;
+ } else {
+ this.resourceNames = resourceNames.trim();
+ }
+ if (resourceNamesVar == null || resourceNamesVar.trim().isEmpty()) {
+ this.resourceNamesVar = null;
+ } else {
+ this.resourceNamesVar = resourceNamesVar.trim();
+ }
+ if (resourceNumber == null || resourceNumber.trim().isEmpty()) {
+ this.resourceNumber = null;
+ } else {
+ this.resourceNumber = resourceNumber.trim();
+ }
+ String labelNamePreparation = (labelName == null || labelName.trim().isEmpty()) ? null : labelName.trim();
+ if (resourceMatchScript != null) {
+ this.resourceMatchScript = resourceMatchScript.configuringWithKeyItem();
+ this.labelName = labelNamePreparation;
+ } else if (labelName != null && labelName.startsWith(LockableResource.GROOVY_LABEL_MARKER)) {
+ this.resourceMatchScript = new SecureGroovyScript(
+ labelName.substring(LockableResource.GROOVY_LABEL_MARKER.length()), false, null)
+ .configuring(ApprovalContext.create());
+ this.labelName = null;
+ } else {
+ this.resourceMatchScript = null;
+ this.labelName = labelNamePreparation;
+ }
+ }
+
+ /**
+ * @deprecated groovy script was added (since 2.0)
+ */
+ @Deprecated
+ @ExcludeFromJacocoGeneratedReport
+ public RequiredResourcesProperty(
+ String resourceNames, String resourceNamesVar, String resourceNumber, String labelName)
+ throws Descriptor.FormException {
+ this(resourceNames, resourceNamesVar, resourceNumber, labelName, null);
+ }
+
+ private Object readResolve() throws Descriptor.FormException {
+ // SECURITY-368 migration logic
+ if (resourceMatchScript == null
+ && labelName != null
+ && labelName.startsWith(LockableResource.GROOVY_LABEL_MARKER)) {
+ return new RequiredResourcesProperty(
+ resourceNames,
+ resourceNamesVar,
+ resourceNumber,
+ null,
+ new SecureGroovyScript(
+ labelName.substring(LockableResource.GROOVY_LABEL_MARKER.length()), false, null)
+ .configuring(ApprovalContext.create()));
+ }
+
+ return this;
+ }
+
+ public String[] getResources() {
+ String names = Util.fixEmptyAndTrim(resourceNames);
+ if (names != null) return names.split("\\s+");
+ else return new String[0];
+ }
+
+ public String getResourceNames() {
+ return resourceNames;
+ }
+
+ public String getResourceNamesVar() {
+ return resourceNamesVar;
+ }
+
+ public String getResourceNumber() {
+ return resourceNumber;
+ }
+
+ public String getLabelName() {
+ return labelName;
+ }
+
+ /**
+ * Gets a system Groovy script to be executed in order to determine if the {@link
+ * LockableResource} matches the condition.
+ *
+ * @return System Groovy Script if defined
+ * @since 2.0
+ * @see
+ * LockableResource#scriptMatches(org.jenkinsci.plugins.scriptsecurity.sandbox.groovy.SecureGroovyScript,
+ * java.util.Map)
+ */
+ @CheckForNull
+ public SecureGroovyScript getResourceMatchScript() {
+ return resourceMatchScript;
+ }
+
+ @Extension
+ public static class DescriptorImpl extends JobPropertyDescriptor {
+
+ @NonNull
+ @Override
+ public String getDisplayName() {
+ return Messages.RequiredResourcesProperty_displayName();
+ }
+
+ @Override
+ public boolean isApplicable(Class extends Job> jobType) {
+ return AbstractProject.class.isAssignableFrom(jobType);
+ }
+
+ @Override
+ public RequiredResourcesProperty newInstance(StaplerRequest2 req, JSONObject formData) throws FormException {
+ if (formData.containsKey("required-lockable-resources")) {
+ return (RequiredResourcesProperty)
+ super.newInstance(req, formData.getJSONObject("required-lockable-resources"));
+ }
+ return null;
+ }
+
+ @RequirePOST
+ public FormValidation doCheckResourceNames(
+ @QueryParameter String value,
@QueryParameter String labelName,
- @QueryParameter String resourceMatchScript)
- {
+ @QueryParameter boolean script,
+ @AncestorInPath Item item) {
+ // check permission, security first
+ checkPermission(item);
+
+ String labelVal = Util.fixEmptyAndTrim(labelName);
+ String names = Util.fixEmptyAndTrim(value);
+
+ if (names == null) {
+ return FormValidation.ok();
+ } else if (labelVal != null || script) {
+ return FormValidation.error(Messages.error_labelAndNameOrGroovySpecified());
+ } else {
+ List wrongNames = new ArrayList<>();
+ for (String name : names.split("\\s+")) {
+ boolean found = LockableResourcesManager.get().resourceExist(name);
+ if (!found) wrongNames.add(name);
+ }
+ if (wrongNames.isEmpty()) {
+ return FormValidation.ok();
+ } else {
+ return FormValidation.error(Messages.error_resourceDoesNotExist(wrongNames));
+ }
+ }
+ }
+
+ @RequirePOST
+ public FormValidation doCheckLabelName(
+ @QueryParameter String value,
+ @QueryParameter String resourceNames,
+ @QueryParameter boolean script,
+ @AncestorInPath Item item) {
+ // check permission, security first
+ checkPermission(item);
+
+ String label = Util.fixEmptyAndTrim(value);
+ String names = Util.fixEmptyAndTrim(resourceNames);
+
+ if (label == null) {
+ return FormValidation.ok();
+ } else if (names != null || script) {
+ return FormValidation.error(Messages.error_labelAndNameOrGroovySpecified());
+ } else {
+ if (LockableResourcesManager.get().isValidLabel(label)) {
+ return FormValidation.ok();
+ } else {
+ return FormValidation.error(Messages.error_labelDoesNotExist(label));
+ }
+ }
+ }
- String number = Util.fixEmptyAndTrim(value);
- String names = Util.fixEmptyAndTrim(resourceNames);
- String label = Util.fixEmptyAndTrim(labelName);
+ @RequirePOST
+ public FormValidation doCheckResourceNumber(
+ @QueryParameter String value,
+ @QueryParameter String resourceNames,
+ @QueryParameter String labelName,
+ @QueryParameter String resourceMatchScript,
+ @AncestorInPath Item item) {
+ // check permission, security first
+ checkPermission(item);
+
+ String number = Util.fixEmptyAndTrim(value);
+ String names = Util.fixEmptyAndTrim(resourceNames);
+ String label = Util.fixEmptyAndTrim(labelName);
String script = Util.fixEmptyAndTrim(resourceMatchScript);
- if (number == null || number.equals("") || number.trim().equals("0")) {
- return FormValidation.ok();
- }
-
- int numAsInt;
- try {
- numAsInt = Integer.parseInt(number);
- } catch(NumberFormatException e) {
- return FormValidation.error(
- "Could not parse the given value as integer.");
- }
- int numResources = 0;
- if (names != null) {
- numResources = names.split("\\s+").length;
+ if (number == null || number.isEmpty() || number.trim().equals("0")) {
+ return FormValidation.ok();
+ }
+
+ int numAsInt;
+ try {
+ numAsInt = Integer.parseInt(number);
+ } catch (NumberFormatException e) {
+ return FormValidation.error(Messages.error_couldNotParseToint());
+ }
+ int numResources = 0;
+ if (names != null) {
+ numResources = names.split("\\s+").length;
} else if (label != null || script != null) {
- numResources = Integer.MAX_VALUE;
+ numResources = Integer.MAX_VALUE;
}
- if (numResources < numAsInt) {
- return FormValidation.error(String.format(
- "Given amount %d is greater than amount of resources: %d.",
- numAsInt,
- numResources));
- }
- return FormValidation.ok();
- }
-
- public AutoCompletionCandidates doAutoCompleteLabelName(
- @QueryParameter String value) {
- AutoCompletionCandidates c = new AutoCompletionCandidates();
-
- value = Util.fixEmptyAndTrim(value);
-
- for (String l : LockableResourcesManager.get().getAllLabels())
- if (value != null && l.startsWith(value))
- c.add(l);
-
- return c;
- }
-
- public static AutoCompletionCandidates doAutoCompleteResourceNames(
- @QueryParameter String value) {
- AutoCompletionCandidates c = new AutoCompletionCandidates();
-
- value = Util.fixEmptyAndTrim(value);
-
- if (value != null) {
- for (LockableResource r : LockableResourcesManager.get()
- .getResources()) {
- if (r.getName().startsWith(value))
- c.add(r.getName());
- }
- }
-
- return c;
- }
- }
-}
+ if (numResources < numAsInt) {
+ return FormValidation.error(String.format(
+ Messages.error_givenAmountIsGreaterThatResourcesAmount(), numAsInt, numResources));
+ }
+ return FormValidation.ok();
+ }
+ @RequirePOST
+ public AutoCompletionCandidates doAutoCompleteLabelName(
+ @QueryParameter String value, @AncestorInPath Item item) {
+ // check permission, security first
+ checkPermission(item);
+
+ AutoCompletionCandidates c = new AutoCompletionCandidates();
+
+ value = Util.fixEmptyAndTrim(value);
+
+ if (value == null) {
+ return c;
+ }
+ for (String l : LockableResourcesManager.get().getAllLabels()) {
+ if (l.startsWith(value)) {
+ c.add(l);
+ }
+ }
+
+ return c;
+ }
+
+ @RequirePOST
+ public static AutoCompletionCandidates doAutoCompleteResourceNames(
+ @QueryParameter String value, @AncestorInPath Item item) {
+ // check permission, security first
+ checkPermission(item);
+
+ AutoCompletionCandidates c = new AutoCompletionCandidates();
+
+ value = Util.fixEmptyAndTrim(value);
+
+ if (value == null) {
+ return c;
+ }
+ List allNames = LockableResourcesManager.get().getAllResourcesNames();
+ for (String name : allNames) {
+ if (name.startsWith(value)) {
+ c.add(name);
+ }
+ }
+
+ return c;
+ }
+
+ private static void checkPermission(Item item) {
+ if (item != null) {
+ item.checkPermission(Item.CONFIGURE);
+ } else {
+ Jenkins.get().checkPermission(Jenkins.ADMINISTER);
+ }
+ }
+ }
+}
diff --git a/src/main/java/org/jenkins/plugins/lockableresources/ResourceSelectStrategy.java b/src/main/java/org/jenkins/plugins/lockableresources/ResourceSelectStrategy.java
new file mode 100644
index 000000000..0b1152731
--- /dev/null
+++ b/src/main/java/org/jenkins/plugins/lockableresources/ResourceSelectStrategy.java
@@ -0,0 +1,6 @@
+package org.jenkins.plugins.lockableresources;
+
+public enum ResourceSelectStrategy {
+ SEQUENTIAL,
+ RANDOM
+}
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 08770d5ae..4c9a4969b 100644
--- a/src/main/java/org/jenkins/plugins/lockableresources/actions/LockableResourcesRootAction.java
+++ b/src/main/java/org/jenkins/plugins/lockableresources/actions/LockableResourcesRootAction.java
@@ -8,163 +8,710 @@
* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */
package org.jenkins.plugins.lockableresources.actions;
+import edu.umd.cs.findbugs.annotations.CheckForNull;
+import edu.umd.cs.findbugs.annotations.NonNull;
import hudson.Extension;
+import hudson.model.Api;
+import hudson.model.Descriptor;
import hudson.model.RootAction;
-import hudson.model.User;
-import hudson.security.AccessDeniedException2;
+import hudson.model.Run;
+import hudson.security.AccessDeniedException3;
import hudson.security.Permission;
import hudson.security.PermissionGroup;
import hudson.security.PermissionScope;
-
+import jakarta.servlet.ServletException;
import java.io.IOException;
import java.util.ArrayList;
+import java.util.Collections;
+import java.util.Date;
+import java.util.LinkedHashMap;
import java.util.List;
import java.util.Set;
-
-import javax.servlet.ServletException;
-
+import java.util.logging.Logger;
import jenkins.model.Jenkins;
-
import org.jenkins.plugins.lockableresources.LockableResource;
import org.jenkins.plugins.lockableresources.LockableResourcesManager;
import org.jenkins.plugins.lockableresources.Messages;
-import org.kohsuke.stapler.StaplerRequest;
-import org.kohsuke.stapler.StaplerResponse;
+import org.jenkins.plugins.lockableresources.queue.LockableResourcesStruct;
+import org.jenkins.plugins.lockableresources.queue.QueuedContextStruct;
+import org.jenkinsci.plugins.scriptsecurity.sandbox.groovy.SecureGroovyScript;
+import org.kohsuke.accmod.Restricted;
+import org.kohsuke.accmod.restrictions.NoExternalUse;
+import org.kohsuke.stapler.StaplerRequest2;
+import org.kohsuke.stapler.StaplerResponse2;
+import org.kohsuke.stapler.export.Exported;
+import org.kohsuke.stapler.export.ExportedBean;
+import org.kohsuke.stapler.interceptor.RequirePOST;
@Extension
+@ExportedBean
public class LockableResourcesRootAction implements RootAction {
- public static final PermissionGroup PERMISSIONS_GROUP = new PermissionGroup(
- LockableResourcesManager.class, Messages._LockableResourcesRootAction_PermissionGroup());
- public static final Permission UNLOCK = new Permission(PERMISSIONS_GROUP,
- Messages.LockableResourcesRootAction_UnlockPermission(),
- Messages._LockableResourcesRootAction_UnlockPermission_Description(), Jenkins.ADMINISTER,
- PermissionScope.JENKINS);
- public static final Permission RESERVE = new Permission(PERMISSIONS_GROUP,
- Messages.LockableResourcesRootAction_ReservePermission(),
- Messages._LockableResourcesRootAction_ReservePermission_Description(), Jenkins.ADMINISTER,
- PermissionScope.JENKINS);
-
- public static final String ICON = "/plugin/lockable-resources/img/device-24x24.png";
-
- public String getIconFileName() {
- if (User.current() != null) {
- // only show if logged in
- return ICON;
- } else {
- return null;
- }
- }
-
- public String getUserName() {
- User current = User.current();
- if (current != null)
- return current.getFullName();
- else
- return null;
- }
-
- public String getDisplayName() {
- return "Lockable Resources";
- }
-
- public String getUrlName() {
- return "lockable-resources";
- }
-
- public List getResources() {
- return LockableResourcesManager.get().getResources();
- }
-
- public int getFreeResourceAmount(String label) {
- return LockableResourcesManager.get().getFreeResourceAmount(label);
- }
-
- public Set getAllLabels() {
- return LockableResourcesManager.get().getAllLabels();
- }
-
- public int getNumberOfAllLabels() {
- return LockableResourcesManager.get().getAllLabels().size();
- }
-
- public void doUnlock(StaplerRequest req, StaplerResponse rsp)
- throws IOException, ServletException {
- Jenkins.getInstance().checkPermission(UNLOCK);
-
- String name = req.getParameter("resource");
- LockableResource r = LockableResourcesManager.get().fromName(name);
- if (r == null) {
- rsp.sendError(404, "Resource not found " + name);
- return;
- }
-
- List resources = new ArrayList();
- resources.add(r);
- LockableResourcesManager.get().unlock(resources, null);
-
- rsp.forwardToPreviousPage(req);
- }
-
- public void doReserve(StaplerRequest req, StaplerResponse rsp)
- throws IOException, ServletException {
- Jenkins.getInstance().checkPermission(RESERVE);
-
- String name = req.getParameter("resource");
- LockableResource r = LockableResourcesManager.get().fromName(name);
- if (r == null) {
- rsp.sendError(404, "Resource not found " + name);
- return;
- }
-
- List resources = new ArrayList();
- resources.add(r);
- String userName = getUserName();
- if (userName != null)
- LockableResourcesManager.get().reserve(resources, userName);
-
- rsp.forwardToPreviousPage(req);
- }
-
- public void doUnreserve(StaplerRequest req, StaplerResponse rsp)
- throws IOException, ServletException {
- Jenkins.getInstance().checkPermission(RESERVE);
-
- String name = req.getParameter("resource");
- LockableResource r = LockableResourcesManager.get().fromName(name);
- if (r == null) {
- rsp.sendError(404, "Resource not found " + name);
- return;
- }
-
- String userName = getUserName();
- if ((userName == null || !userName.equals(r.getReservedBy()))
- && !Jenkins.getInstance().hasPermission(Jenkins.ADMINISTER))
- throw new AccessDeniedException2(Jenkins.getAuthentication(),
- RESERVE);
-
- List resources = new ArrayList();
- resources.add(r);
- LockableResourcesManager.get().unreserve(resources);
-
- rsp.forwardToPreviousPage(req);
- }
-
- public void doReset(StaplerRequest req, StaplerResponse rsp)
- throws IOException, ServletException {
- Jenkins.getInstance().checkPermission(UNLOCK);
-
- String name = req.getParameter("resource");
- LockableResource r = LockableResourcesManager.get().fromName(name);
- if (r == null) {
- rsp.sendError(404, "Resource not found " + name);
- return;
- }
-
- List resources = new ArrayList();
- resources.add(r);
- LockableResourcesManager.get().reset(resources);
-
- rsp.forwardToPreviousPage(req);
- }
+ private static final Logger LOGGER = Logger.getLogger(LockableResourcesRootAction.class.getName());
+
+ public static final PermissionGroup PERMISSIONS_GROUP = new PermissionGroup(
+ LockableResourcesManager.class, Messages._LockableResourcesRootAction_PermissionGroup());
+ public static final Permission UNLOCK = new Permission(
+ PERMISSIONS_GROUP,
+ "Unlock",
+ Messages._LockableResourcesRootAction_UnlockPermission_Description(),
+ Jenkins.ADMINISTER,
+ PermissionScope.JENKINS);
+ public static final Permission RESERVE = new Permission(
+ PERMISSIONS_GROUP,
+ "Reserve",
+ Messages._LockableResourcesRootAction_ReservePermission_Description(),
+ Jenkins.ADMINISTER,
+ PermissionScope.JENKINS);
+ public static final Permission STEAL = new Permission(
+ PERMISSIONS_GROUP,
+ "Steal",
+ Messages._LockableResourcesRootAction_StealPermission_Description(),
+ Jenkins.ADMINISTER,
+ PermissionScope.JENKINS);
+ public static final Permission VIEW = new Permission(
+ PERMISSIONS_GROUP,
+ "View",
+ Messages._LockableResourcesRootAction_ViewPermission_Description(),
+ Jenkins.ADMINISTER,
+ PermissionScope.JENKINS);
+ public static final Permission QUEUE = new Permission(
+ PERMISSIONS_GROUP,
+ "Queue",
+ Messages._LockableResourcesRootAction_QueueChangeOrderPermission_Description(),
+ Jenkins.ADMINISTER,
+ PermissionScope.JENKINS);
+
+ public static final String ICON = "symbol-lock-closed";
+
+ @Override
+ public String getIconFileName() {
+ return Jenkins.get().hasPermission(VIEW) ? ICON : null;
+ }
+
+ public Api getApi() {
+ return new Api(this);
+ }
+
+ @CheckForNull
+ public String getUserName() {
+ return LockableResource.getUserName();
+ }
+
+ @Override
+ public String getDisplayName() {
+ return Messages.LockableResourcesRootAction_PermissionGroup();
+ }
+
+ @Override
+ public String getUrlName() {
+ return Jenkins.get().hasPermission(VIEW) ? "lockable-resources" : "";
+ }
+
+ // ---------------------------------------------------------------------------
+ /**
+ * Get a list of resources
+ *
+ * @return All resources.
+ */
+ @Exported
+ @Restricted(NoExternalUse.class) // used by jelly
+ public List getResources() {
+ return LockableResourcesManager.get().getReadOnlyResources();
+ }
+
+ // ---------------------------------------------------------------------------
+ /**
+ * Get a list of all labels
+ *
+ * @return All possible labels.
+ */
+ @Restricted(NoExternalUse.class) // used by jelly
+ public LinkedHashMap getLabelsList() {
+ LinkedHashMap map = new LinkedHashMap<>();
+
+ for (LockableResource r : LockableResourcesManager.get().getReadOnlyResources()) {
+ if (r == null || r.getName().isEmpty()) {
+ continue; // defensive, shall never happens, but ...
+ }
+ List assignedLabels = r.getLabelsAsList();
+ if (assignedLabels.isEmpty()) {
+ continue;
+ }
+
+ for (String labelString : assignedLabels) {
+ if (labelString == null || labelString.isEmpty()) {
+ continue; // defensive, shall never happens, but ...
+ }
+ LockableResourcesLabel label = map.get(labelString);
+ if (label == null) {
+ label = new LockableResourcesLabel(labelString);
+ }
+
+ label.update(r);
+
+ map.put(labelString, label);
+ }
+ }
+
+ return map;
+ }
+
+ // ---------------------------------------------------------------------------
+ public static class LockableResourcesLabel {
+ String name;
+ int free;
+ int assigned;
+
+ // -------------------------------------------------------------------------
+ public LockableResourcesLabel(String _name) {
+ this.name = _name;
+ this.free = 0;
+ this.assigned = 0;
+ }
+
+ // -------------------------------------------------------------------------
+ public void update(LockableResource resource) {
+ this.assigned++;
+ if (resource.isFree()) free++;
+ }
+
+ // -------------------------------------------------------------------------
+ public String getName() {
+ return this.name;
+ }
+
+ // -------------------------------------------------------------------------
+ public int getFree() {
+ return this.free;
+ }
+
+ // -------------------------------------------------------------------------
+ public int getAssigned() {
+ return this.assigned;
+ }
+
+ // -------------------------------------------------------------------------
+ public int getPercentage() {
+ if (this.assigned == 0) {
+ return this.assigned;
+ }
+ return (int) ((double) this.free / (double) this.assigned * 100);
+ }
+ }
+
+ // ---------------------------------------------------------------------------
+ // used by by
+ // src\main\resources\org\jenkins\plugins\lockableresources\actions\LockableResourcesRootAction\tableResources\table.jelly
+ @Restricted(NoExternalUse.class)
+ public LockableResource getResource(final String resourceName) {
+ return LockableResourcesManager.get().fromName(resourceName);
+ }
+
+ // ---------------------------------------------------------------------------
+ /**
+ * Get amount of free resources assigned to given *labelString*
+ *
+ * @param labelString Label to search.
+ * @return Amount of free labels.
+ */
+ @Restricted(NoExternalUse.class) // used by jelly
+ @Deprecated // slow down plugin execution due concurrent modification checks
+ public int getFreeResourceAmount(final String labelString) {
+ this.informPerformanceIssue();
+ LockableResourcesLabel label = this.getLabelsList().get(labelString);
+ return (label == null) ? 0 : label.getFree();
+ }
+
+ // ---------------------------------------------------------------------------
+ /**
+ * Get percentage (0-100) usage of resources assigned to given *labelString*
+ *
+ *