Skip to content

Commit 2d6f8a0

Browse files
authored
Merge pull request jenkinsci#70 from abayer/jenkins-34433
[FIXED JENKINS-34433] Signal queued Pipeline tasks on unreserve
2 parents 9b97aea + f25d106 commit 2d6f8a0

File tree

2 files changed

+115
-2
lines changed

2 files changed

+115
-2
lines changed

src/main/java/org/jenkins/plugins/lockableresources/LockableResourcesManager.java

Lines changed: 81 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -26,11 +26,13 @@
2626
import java.util.logging.Level;
2727
import java.util.logging.Logger;
2828

29+
import hudson.model.TaskListener;
2930
import jenkins.model.GlobalConfiguration;
3031
import jenkins.model.Jenkins;
3132
import net.sf.json.JSONException;
3233
import net.sf.json.JSONObject;
3334

35+
import org.apache.commons.lang.StringUtils;
3436
import org.jenkins.plugins.lockableresources.queue.LockableResourcesStruct;
3537
import org.jenkinsci.plugins.scriptsecurity.sandbox.groovy.SecureGroovyScript;
3638
import org.jenkins.plugins.lockableresources.queue.QueuedContextStruct;
@@ -362,7 +364,7 @@ public synchronized void unlockNames(@Nullable List<String> resourceNamesToUnLoc
362364

363365
// remove context from queue and process it
364366
requiredResourceForNextContext = checkResourcesAvailability(nextContext.getResources(), null, resourceNamesToUnLock);
365-
this.queuedContexts.remove(nextContext);
367+
unqueueContext(nextContext.getContext());
366368

367369
// resourceNamesToUnlock contains the names of the previous resources.
368370
// requiredResourceForNextContext contains the resource objects which are required for the next context.
@@ -504,9 +506,86 @@ public synchronized boolean reserve(List<LockableResource> resources,
504506
return true;
505507
}
506508

509+
private void unreserveResources(@Nonnull List<LockableResource> resources) {
510+
for (LockableResource l : resources) {
511+
l.unReserve();
512+
}
513+
save();
514+
}
507515
public synchronized void unreserve(List<LockableResource> resources) {
516+
// make sure there is a list of resources to unreserve
517+
if (resources == null || (resources.size() == 0)) {
518+
return;
519+
}
520+
List<String> resourceNamesToUnreserve = new ArrayList<>();
508521
for (LockableResource r : resources) {
509-
r.unReserve();
522+
resourceNamesToUnreserve.add(r.getName());
523+
}
524+
525+
// check if there are resources which can be unlocked (and shall not be unlocked)
526+
List<LockableResource> requiredResourceForNextContext = null;
527+
QueuedContextStruct nextContext = this.getNextQueuedContext(resourceNamesToUnreserve, false);
528+
529+
// no context is queued which can be started once these resources are free'd.
530+
if (nextContext == null) {
531+
LOGGER.log(Level.FINER, "No context queued for resources " + StringUtils.join(resourceNamesToUnreserve, ", ") + " so unreserving and proceeding.");
532+
unreserveResources(resources);
533+
return;
534+
}
535+
536+
PrintStream nextContextLogger = null;
537+
try {
538+
TaskListener nextContextTaskListener = nextContext.getContext().get(TaskListener.class);
539+
if (nextContextTaskListener != null) {
540+
nextContextLogger = nextContextTaskListener.getLogger();
541+
}
542+
} catch (IOException | InterruptedException e) {
543+
LOGGER.log(Level.FINE, "Could not get logger for next context: " + e, e);
544+
}
545+
546+
// remove context from queue and process it
547+
requiredResourceForNextContext = checkResourcesAvailability(nextContext.getResources(),
548+
nextContextLogger,
549+
resourceNamesToUnreserve);
550+
this.queuedContexts.remove(nextContext);
551+
552+
// resourceNamesToUnreserve contains the names of the previous resources.
553+
// requiredResourceForNextContext contains the resource objects which are required for the next context.
554+
// It is guaranteed that there is an overlap between the two - the resources which are to be reused.
555+
boolean needToWait = false;
556+
for (LockableResource requiredResource : requiredResourceForNextContext) {
557+
if (!resourceNamesToUnreserve.contains(requiredResource.getName())) {
558+
if (requiredResource.isReserved() || requiredResource.isLocked()) {
559+
needToWait = true;
560+
break;
561+
}
562+
}
563+
}
564+
565+
if (needToWait) {
566+
unreserveResources(resources);
567+
return;
568+
} else {
569+
unreserveResources(resources);
570+
List<String> resourceNamesToLock = new ArrayList<String>();
571+
572+
// lock all (old and new resources)
573+
for (LockableResource requiredResource : requiredResourceForNextContext) {
574+
try {
575+
requiredResource.setBuild(nextContext.getContext().get(Run.class));
576+
resourceNamesToLock.add(requiredResource.getName());
577+
} catch (Exception e) {
578+
// skip this context, as the build cannot be retrieved (maybe it was deleted while running?)
579+
LOGGER.log(Level.WARNING, "Skipping queued context for lock. Can not get the Run object from the context to proceed with lock, " +
580+
"this could be a legitimate status if the build waiting for the lock was deleted or" +
581+
" hard killed. More information at Level.FINE if debug is needed.");
582+
LOGGER.log(Level.FINE, "Can not get the Run object from the context to proceed with lock", e);
583+
return;
584+
}
585+
}
586+
587+
// continue with next context
588+
LockStepExecution.proceed(resourceNamesToLock, nextContext.getContext(), nextContext.getResourceDescription(), null, false);
510589
}
511590
save();
512591
}

src/test/java/org/jenkins/plugins/lockableresources/LockStepTest.java

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@
3535

3636
import static org.junit.Assert.assertEquals;
3737
import static org.junit.Assert.assertNotNull;
38+
import static org.junit.Assert.assertTrue;
3839
import static org.junit.Assume.assumeFalse;
3940

4041
public class LockStepTest {
@@ -750,6 +751,39 @@ public void evaluate() throws Throwable {
750751
});
751752
}
752753

754+
@Issue("JENKINS-34433")
755+
@Test
756+
public void manualUnreserveUnblocksJob() throws Exception {
757+
story.addStep(new Statement() {
758+
@Override
759+
public void evaluate() throws Throwable {
760+
LockableResourcesManager.get().createResource("resource1");
761+
JenkinsRule.WebClient wc = story.j.createWebClient();
762+
763+
wc.goTo("lockable-resources/reserve?resource=resource1");
764+
LockableResource resource1 = LockableResourcesManager.get().fromName("resource1");
765+
assertNotNull(resource1);
766+
resource1.setReservedBy("someone");
767+
assertTrue(resource1.isReserved());
768+
769+
WorkflowJob p = story.j.jenkins.createProject(WorkflowJob.class, "p");
770+
p.setDefinition(new CpsFlowDefinition("retry(99) {\n" +
771+
" lock('resource1') {\n" +
772+
" semaphore('wait-inside')\n" +
773+
" }\n" +
774+
"}", true));
775+
776+
777+
WorkflowRun r = p.scheduleBuild2(0).waitForStart();
778+
story.j.waitForMessage("[resource1] is locked, waiting...", r);
779+
wc.goTo("lockable-resources/unreserve?resource=resource1");
780+
SemaphoreStep.waitForStart("wait-inside/1", r);
781+
SemaphoreStep.success("wait-inside/1", null);
782+
story.j.assertBuildStatusSuccess(story.j.waitForCompletion(r));
783+
}
784+
});
785+
}
786+
753787
private void waitAndClear(int semaphoreIndex, List<WorkflowRun> nextRuns) throws Exception {
754788
WorkflowRun toClear = nextRuns.get(0);
755789

0 commit comments

Comments
 (0)