Permalink
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Showing
with
347 additions
and 0 deletions.
- +1 −0 CHANGES.md
- +151 −0 aggregator/src/test/java/org/jenkinsci/plugins/workflow/steps/WaitForConditionStepTest.java
- +160 −0 basic-steps/src/main/java/org/jenkinsci/plugins/workflow/steps/WaitForConditionStep.java
- +27 −0 ...c-steps/src/main/resources/org/jenkinsci/plugins/workflow/steps/WaitForConditionStep/config.jelly
- +7 −0 basic-steps/src/main/resources/org/jenkinsci/plugins/workflow/steps/WaitForConditionStep/help.html
- +1 −0 support/src/test/java/org/jenkinsci/plugins/workflow/test/steps/WatchYourStep.java
@@ -0,0 +1,151 @@ | ||
/* | ||
* The MIT License | ||
* | ||
* Copyright 2014 Jesse Glick. | ||
* | ||
* Permission is hereby granted, free of charge, to any person obtaining a copy | ||
* of this software and associated documentation files (the "Software"), to deal | ||
* in the Software without restriction, including without limitation the rights | ||
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell | ||
* copies of the Software, and to permit persons to whom the Software is | ||
* furnished to do so, subject to the following conditions: | ||
* | ||
* The above copyright notice and this permission notice shall be included in | ||
* all copies or substantial portions of the Software. | ||
* | ||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR | ||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, | ||
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE | ||
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER | ||
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, | ||
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN | ||
* THE SOFTWARE. | ||
*/ | ||
|
||
package org.jenkinsci.plugins.workflow.steps; | ||
|
||
import com.google.common.base.Function; | ||
import hudson.AbortException; | ||
import hudson.Util; | ||
import hudson.model.Result; | ||
import java.util.ArrayList; | ||
import java.util.List; | ||
import org.jenkinsci.plugins.workflow.SingleJobTestBase; | ||
import org.jenkinsci.plugins.workflow.cps.CpsFlowDefinition; | ||
import org.jenkinsci.plugins.workflow.job.WorkflowJob; | ||
import org.jenkinsci.plugins.workflow.test.steps.SemaphoreStep; | ||
import org.junit.Ignore; | ||
import org.junit.Test; | ||
import org.junit.runners.model.Statement; | ||
|
||
public class WaitForConditionStepTest extends SingleJobTestBase { | ||
|
||
@Test public void simple() { | ||
story.addStep(new Statement() { | ||
@Override public void evaluate() throws Throwable { | ||
p = jenkins().createProject(WorkflowJob.class, "demo"); | ||
p.setDefinition(new CpsFlowDefinition("waitUntil {semaphore 'waitSimple'}; semaphore 'waitedSimple'")); | ||
startBuilding(); | ||
SemaphoreStep.waitForStart("waitSimple/1", b); | ||
SemaphoreStep.success("waitSimple/1", false); | ||
SemaphoreStep.waitForStart("waitSimple/2", b); | ||
SemaphoreStep.success("waitSimple/2", false); | ||
SemaphoreStep.waitForStart("waitSimple/3", b); | ||
SemaphoreStep.success("waitSimple/3", true); | ||
SemaphoreStep.waitForStart("waitedSimple/1", b); | ||
SemaphoreStep.success("waitedSimple/1", null); | ||
waitForWorkflowToComplete(); | ||
assertBuildCompletedSuccessfully(); | ||
story.j.assertLogContains("Will try again after " + Util.getTimeSpanString(WaitForConditionStep.Execution.MIN_RECURRENCE_PERIOD), b); | ||
} | ||
}); | ||
} | ||
|
||
@Test public void failure() { | ||
story.addStep(new Statement() { | ||
@Override public void evaluate() throws Throwable { | ||
p = jenkins().createProject(WorkflowJob.class, "demo"); | ||
p.setDefinition(new CpsFlowDefinition("waitUntil {semaphore 'waitFailure'}")); | ||
startBuilding(); | ||
SemaphoreStep.waitForStart("waitFailure/1", b); | ||
SemaphoreStep.success("waitFailure/1", false); | ||
SemaphoreStep.waitForStart("waitFailure/2", b); | ||
String message = "broken condition"; | ||
SemaphoreStep.failure("waitFailure/2", new AbortException(message)); | ||
waitForWorkflowToComplete(); | ||
story.j.assertBuildStatus(Result.FAILURE, b); | ||
story.j.assertLogContains(message, b); | ||
} | ||
}); | ||
} | ||
|
||
@Test public void restartDuringBody() { | ||
story.addStep(new Statement() { | ||
@Override public void evaluate() throws Throwable { | ||
p = jenkins().createProject(WorkflowJob.class, "demo"); | ||
p.setDefinition(new CpsFlowDefinition("waitUntil {semaphore 'waitRestartDuringBody'}; echo 'finished waiting'")); | ||
startBuilding(); | ||
SemaphoreStep.waitForStart("waitRestartDuringBody/1", b); | ||
SemaphoreStep.success("waitRestartDuringBody/1", false); | ||
SemaphoreStep.waitForStart("waitRestartDuringBody/2", b); | ||
} | ||
}); | ||
story.addStep(new Statement() { | ||
@Override public void evaluate() throws Throwable { | ||
rebuildContext(story.j); | ||
assertThatWorkflowIsSuspended(); | ||
SemaphoreStep.success("waitRestartDuringBody/2", false); | ||
SemaphoreStep.waitForStart("waitRestartDuringBody/3", b); | ||
SemaphoreStep.success("waitRestartDuringBody/3", true); | ||
waitForWorkflowToComplete(); | ||
assertBuildCompletedSuccessfully(); | ||
story.j.assertLogContains("finished waiting", b); | ||
} | ||
}); | ||
} | ||
|
||
@Ignore("TODO JENKINS-26163 executions.isEmpty() because StepExecution.applyAll is called while body is active") | ||
@Test public void restartDuringDelay() { | ||
story.addStep(new Statement() { | ||
@SuppressWarnings("SleepWhileInLoop") | ||
@Override public void evaluate() throws Throwable { | ||
p = jenkins().createProject(WorkflowJob.class, "demo"); | ||
p.setDefinition(new CpsFlowDefinition("waitUntil {semaphore 'waitRestartDuringDelay'}; echo 'finished waiting'")); | ||
startBuilding(); | ||
SemaphoreStep.waitForStart("waitRestartDuringDelay/1", b); | ||
final List<WaitForConditionStep.Execution> executions = new ArrayList<WaitForConditionStep.Execution>(); | ||
StepExecution.applyAll(WaitForConditionStep.Execution.class, new Function<WaitForConditionStep.Execution, Void>() { | ||
@Override public Void apply(WaitForConditionStep.Execution execution) { | ||
executions.add(execution); | ||
return null; | ||
} | ||
}).get(); | ||
assertEquals(1, executions.size()); | ||
SemaphoreStep.success("waitRestartDuringDelay/1", false); | ||
SemaphoreStep.waitForStart("waitRestartDuringDelay/2", b); | ||
final long LONG_TIME = Long.MAX_VALUE / /* > RECURRENCE_PERIOD_BACKOFF */ 10; | ||
executions.get(0).recurrencePeriod = LONG_TIME; | ||
SemaphoreStep.success("waitRestartDuringDelay/2", false); | ||
while (executions.get(0).recurrencePeriod == LONG_TIME) { | ||
Thread.sleep(100); | ||
} | ||
story.j.assertLogContains("Will try again after " + Util.getTimeSpanString(LONG_TIME), b); | ||
// timer is now waiting for a long time | ||
} | ||
}); | ||
story.addStep(new Statement() { | ||
@Override public void evaluate() throws Throwable { | ||
rebuildContext(story.j); | ||
assertThatWorkflowIsSuspended(); | ||
SemaphoreStep.waitForStart("waitRestartDuringDelay/3", b); | ||
SemaphoreStep.success("waitRestartDuringDelay/3", false); | ||
SemaphoreStep.waitForStart("waitRestartDuringDelay/4", b); | ||
SemaphoreStep.success("waitRestartDuringDelay/4", true); | ||
waitForWorkflowToComplete(); | ||
assertBuildCompletedSuccessfully(); | ||
story.j.assertLogContains("finished waiting", b); | ||
} | ||
}); | ||
} | ||
|
||
} |
@@ -0,0 +1,160 @@ | ||
/* | ||
* The MIT License | ||
* | ||
* Copyright 2014 Jesse Glick. | ||
* | ||
* Permission is hereby granted, free of charge, to any person obtaining a copy | ||
* of this software and associated documentation files (the "Software"), to deal | ||
* in the Software without restriction, including without limitation the rights | ||
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell | ||
* copies of the Software, and to permit persons to whom the Software is | ||
* furnished to do so, subject to the following conditions: | ||
* | ||
* The above copyright notice and this permission notice shall be included in | ||
* all copies or substantial portions of the Software. | ||
* | ||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR | ||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, | ||
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE | ||
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER | ||
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, | ||
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN | ||
* THE SOFTWARE. | ||
*/ | ||
|
||
package org.jenkinsci.plugins.workflow.steps; | ||
|
||
import com.google.common.base.Function; | ||
import hudson.Extension; | ||
import hudson.Util; | ||
import hudson.model.TaskListener; | ||
import java.util.UUID; | ||
import java.util.concurrent.ScheduledFuture; | ||
import java.util.concurrent.TimeUnit; | ||
import javax.annotation.Nonnull; | ||
import jenkins.util.Timer; | ||
import org.kohsuke.stapler.DataBoundConstructor; | ||
|
||
public final class WaitForConditionStep extends AbstractStepImpl { | ||
|
||
@DataBoundConstructor public WaitForConditionStep() {} | ||
|
||
public static final class Execution extends AbstractStepExecutionImpl { | ||
|
||
private static final long serialVersionUID = 1; | ||
/** Unused, just to force the descriptor to request it. */ | ||
@StepContextParameter private transient TaskListener listener; | ||
private volatile BodyExecution body; | ||
private transient volatile ScheduledFuture<?> task; | ||
/** | ||
* TODO JENKINS-26148 is there no cleaner way of finding the StepExecution that created a BodyExecutionCallback? | ||
* @see #retry(String, StepContext) | ||
*/ | ||
private final String id = UUID.randomUUID().toString(); | ||
private static final float RECURRENCE_PERIOD_BACKOFF = 1.2f; | ||
static final long MIN_RECURRENCE_PERIOD = 250; // ¼s | ||
// Do we want a maximum, or can it grow to any size? | ||
long recurrencePeriod = MIN_RECURRENCE_PERIOD; | ||
|
||
@Override public boolean start() throws Exception { | ||
body = getContext().newBodyInvoker().withCallback(new Callback(id)).start(); | ||
return false; | ||
} | ||
|
||
@Override public void stop(Throwable cause) throws Exception { | ||
if (body != null) { | ||
body.cancel(cause); | ||
} | ||
if (task != null) { | ||
task.cancel(false); | ||
getContext().onFailure(cause); | ||
} | ||
} | ||
|
||
@Override public void onResume() { | ||
super.onResume(); | ||
recurrencePeriod = MIN_RECURRENCE_PERIOD; | ||
if (body == null) { | ||
// Restarted while waiting for the timer to go off. Rerun now. | ||
body = getContext().newBodyInvoker().withCallback(new Callback(id)).start(); | ||
} // otherwise we are in the middle of the body already, so let it run | ||
} | ||
|
||
private static void retry(final String id, final StepContext context) { | ||
StepExecution.applyAll(Execution.class, new Function<Execution, Void>() { | ||
@Override public Void apply(@Nonnull Execution execution) { | ||
if (execution.id.equals(id)) { | ||
execution.retry(context); | ||
} | ||
return null; | ||
} | ||
}); | ||
} | ||
|
||
private void retry(StepContext perBodyContext) { | ||
body = null; | ||
try { | ||
perBodyContext.get(TaskListener.class).getLogger().println("Will try again after " + Util.getTimeSpanString(recurrencePeriod)); | ||
} catch (Exception x) { | ||
getContext().onFailure(x); | ||
return; | ||
} | ||
task = Timer.get().schedule(new Runnable() { | ||
@Override public void run() { | ||
task = null; | ||
body = getContext().newBodyInvoker().withCallback(new Callback(id)).start(); | ||
} | ||
}, recurrencePeriod, TimeUnit.MILLISECONDS); | ||
recurrencePeriod *= RECURRENCE_PERIOD_BACKOFF; | ||
} | ||
|
||
} | ||
|
||
private static final class Callback extends BodyExecutionCallback { | ||
|
||
private static final long serialVersionUID = 1; | ||
private final String id; | ||
|
||
Callback(String id) { | ||
this.id = id; | ||
} | ||
|
||
@Override public void onSuccess(final StepContext context, Object result) { | ||
if (!(result instanceof Boolean)) { | ||
context.onFailure(new ClassCastException("body return value " + result + " is not boolean")); | ||
return; | ||
} | ||
if ((Boolean) result) { | ||
context.onSuccess(null); | ||
return; | ||
} | ||
Execution.retry(id, context); | ||
} | ||
|
||
@Override public void onFailure(StepContext context, Throwable t) { | ||
context.onFailure(t); | ||
} | ||
|
||
} | ||
|
||
@Extension public static final class DescriptorImpl extends AbstractStepDescriptorImpl { | ||
|
||
public DescriptorImpl() { | ||
super(Execution.class); | ||
} | ||
|
||
@Override public String getFunctionName() { | ||
return "waitUntil"; | ||
} | ||
|
||
@Override public String getDisplayName() { | ||
return "Wait for condition"; | ||
} | ||
|
||
@Override public boolean takesImplicitBlockArgument() { | ||
return true; | ||
} | ||
|
||
} | ||
|
||
} |
@@ -0,0 +1,27 @@ | ||
<?xml version="1.0" encoding="UTF-8"?> | ||
<!-- | ||
The MIT License | ||
Copyright 2014 Jesse Glick. | ||
Permission is hereby granted, free of charge, to any person obtaining a copy | ||
of this software and associated documentation files (the "Software"), to deal | ||
in the Software without restriction, including without limitation the rights | ||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell | ||
copies of the Software, and to permit persons to whom the Software is | ||
furnished to do so, subject to the following conditions: | ||
The above copyright notice and this permission notice shall be included in | ||
all copies or substantial portions of the Software. | ||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR | ||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, | ||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE | ||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER | ||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, | ||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN | ||
THE SOFTWARE. | ||
--> | ||
|
||
<?jelly escape-by-default='true'?> | ||
<j:jelly xmlns:j="jelly:core"/> |
@@ -0,0 +1,7 @@ | ||
<div> | ||
Runs its body repeatedly until it returns <code>true</code>. | ||
If it returns <code>false</code>, waits a while and tries again. | ||
(Subsequent failures will slow down the delay between attempts.) | ||
There is no limit to the number of retries, | ||
but if the body throws an error that is thrown up immediately. | ||
</div> |