Skip to content
Permalink
Browse files

[FIXED JENKINS-25570] Added waitUntil step.

Originally-Committed-As: 4de61d25f9c32d631baad8f366bff5fecc90d137
  • Loading branch information
jglick committed Dec 18, 2014
1 parent 663fa30 commit 4658d61fa0effb8f2fb04ae487b2915dccae8683
@@ -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>

0 comments on commit 4658d61

Please sign in to comment.
You can’t perform that action at this time.