Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -26,36 +26,72 @@

import hudson.Extension;
import hudson.model.TaskListener;
import hudson.util.ListBoxModel;

import java.io.Serializable;
import java.util.Collections;
import java.util.Set;
import java.util.concurrent.TimeUnit;

import org.kohsuke.stapler.DataBoundConstructor;
import org.kohsuke.stapler.DataBoundSetter;

/**
* Executes the body up to N times.
*
* @author Kohsuke Kawaguchi
*/
public class RetryStep extends Step {
public class RetryStep extends Step implements Serializable {

private final int count;
private int timeDelay;
private TimeUnit unit = TimeUnit.SECONDS;
private boolean useTimeDelay = false;

public int left;

@DataBoundConstructor
public RetryStep(int count) {
this.count = count;
this.left = count;
}

public int getCount() {
return count;
}

@DataBoundSetter public void setUseTimeDelay(boolean useTimeDelay) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this needed? shouldn't a non null / non default time delay mean this is enabled?

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It does seem to be a bit extra here

this.useTimeDelay = useTimeDelay;
}

public boolean isUseTimeDelay() {
return useTimeDelay;
}

@DataBoundSetter public void setTimeDelay(int timeDelay) {
this.timeDelay = timeDelay;
}

public int getTimeDelay() {
return timeDelay;
}

@DataBoundSetter public void setUnit(TimeUnit unit) {
this.unit = unit;
}

public TimeUnit getUnit() {
return unit;
}

@Override
public DescriptorImpl getDescriptor() {
return (DescriptorImpl)super.getDescriptor();
}

@Override
public StepExecution start(StepContext context) throws Exception {
return new RetryStepExecution(count, context);
return new RetryStepExecution(this, context);
}

@Extension
Expand All @@ -76,11 +112,20 @@ public String getDisplayName() {
return "Retry the body up to N times";
}

public ListBoxModel doFillUnitItems() {
ListBoxModel r = new ListBoxModel();
for (TimeUnit unit : TimeUnit.values()) {
r.add(unit.name());
}
return r;
}

@Override
public Set<? extends Class<?>> getRequiredContext() {
return Collections.singleton(TaskListener.class);
}

}

private static final long serialVersionUID = 1L;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

normally constants are at the top of a class in java

}
Original file line number Diff line number Diff line change
@@ -1,51 +1,153 @@
package org.jenkinsci.plugins.workflow.steps;

import com.google.common.base.Function;
import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;
import hudson.AbortException;
import hudson.Functions;
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;

/**
* @author Kohsuke Kawaguchi
*/
public class RetryStepExecution extends AbstractStepExecutionImpl {

@SuppressFBWarnings(value="SE_TRANSIENT_FIELD_NOT_RESTORED", justification="Only used when starting.")

@SuppressFBWarnings(value = "SE_TRANSIENT_FIELD_NOT_RESTORED", justification = "Only used when starting.")
private transient final RetryStep step;
@SuppressFBWarnings(value = "SE_TRANSIENT_FIELD_NOT_RESTORED", justification = "Only used when starting.")
private transient final int count;
private transient volatile ScheduledFuture<?> task;

/** Used to track whether this is timing out on inactivity without needing to reference {@link #step}. */
private boolean executing = false;
/** Token for {@link #executing} callbacks. */
private final String id = UUID.randomUUID().toString();


@Deprecated
RetryStepExecution(int count, StepContext context) {
super(context);
this.count = count;
this.count =count;
this.step = null;
}

RetryStepExecution(@Nonnull RetryStep step, StepContext context) {
super(context);
this.step = step;
this.count = step.getCount();
}

@Override
public boolean start() throws Exception {
@Override public boolean start() throws Exception {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@Override would normally go above a method, and it already was above there before you changed it so I would suggest you move it back

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@Override would normally go above a method, and it already was above there before you changed it so I would suggest you move it back

StepContext context = getContext();
context.newBodyInvoker()
.withCallback(new Callback(count))
.start();
if(step == null) {
context.newBodyInvoker()
.withCallback(new Callback(count))
.start();
} else {
executing = true;
context.newBodyInvoker()
.withCallback(new Callback(id,step))
.start();
}
return false; // execution is asynchronous
}

@Override public void stop(Throwable cause) throws Exception {
if (task != null) {
task.cancel(false);
}
super.stop(cause);
}

@Override public void onResume() {
if (!executing && step != null) {
// Restarted while waiting for the timer to go off. Rerun now.
getContext().newBodyInvoker().withCallback(new Callback(id, step)).start();
executing = true;
} // 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(RetryStepExecution.class, new Function<RetryStepExecution, Void>() {
@Override public Void apply(@Nonnull RetryStepExecution execution) {
if (execution.id.equals(id)) {
execution.retry(context);
}
return null;
}
});
}

private void retry(StepContext perBodyContext) {
executing = false;
getContext().saveState();

try {
TaskListener l = getContext().get(TaskListener.class);
if(step.left>0) {
long delay = step.getUnit().toMillis(step.getTimeDelay());
l.getLogger().println(
"Will try again after " +
Util.getTimeSpanString(delay));
task = Timer.get().schedule(new Runnable() {
@Override public void run() {
task = null;
try {
l.getLogger().println("Retrying");
} catch (Exception x) {
getContext().onFailure(x);
return;
}
getContext().newBodyInvoker().withCallback(new Callback(id,step)).start();
executing = true;
}
}, delay, TimeUnit.MILLISECONDS);
}
} catch (Throwable p) {
getContext().onFailure(p);
}
}

@Override public void onResume() {}
@Override public String getStatus() {
if (executing) {
return "running body";
} else if (task == null) {
return "no body, no task, not sure what happened";
} else if (task.isDone()) {
return "scheduled task is done, but no body";
} else if (task.isCancelled()) {
return "scheduled task was cancelled";
} else {
return "waiting to rerun; next recurrence period: " +
step.getUnit().toMillis(step.getTimeDelay()) + "ms";
}
}

private static class Callback extends BodyExecutionCallback {

private final RetryStep step;
private int left;
private final String id;

@Deprecated
Callback(int count) {
left = count;
this.step = null;
this.id = "-1";
}

/* Could be added, but seems unnecessary, given the message already printed in onFailure:
@Override public void onStart(StepContext context) {
try {
context.get(TaskListener.class).getLogger().println(left + " tries left");
} catch (Exception x) {
context.onFailure(x);
}
Callback(String id, RetryStep step) {
this.id = id;
this.step = step;
left = step.getCount();
}
*/

@Override
public void onSuccess(StepContext context, Object result) {
Expand All @@ -59,16 +161,27 @@ public void onFailure(StepContext context, Throwable t) {
context.onFailure(t);
return;
}
left--;
if (left>0) {
int remaining = 0;
if(step != null) {
step.left--;
remaining = step.left;
} else {
left--;
remaining = left;
}
if (remaining>0) {
TaskListener l = context.get(TaskListener.class);
if (t instanceof AbortException) {
l.error(t.getMessage());
} else {
Functions.printStackTrace(t, l.error("Execution failed"));
}
l.getLogger().println("Retrying");
context.newBodyInvoker().withCallback(this).start();
if(step != null && !step.isUseTimeDelay()) {
l.getLogger().println("Retrying");
context.newBodyInvoker().withCallback(this).start();
} else {
RetryStepExecution.retry(id, context);
}
} else {
// No need to print anything in this case, since it will be thrown up anyway.
context.onFailure(t);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,4 +28,13 @@ THE SOFTWARE.
<f:entry field="count" title="${%Retry Count}">
<f:number clazz="positive-number"/>
</f:entry>

<f:optionalBlock field="useTimeDelay" inline="true" title="${%Use a time delay between retries.}">
<f:entry field="timeDelay" title="${%Sleep between retry}">
<f:number clazz="positive-number"/>
</f:entry>
<f:entry field="unit" title="${%Unit}">
<f:select/>
</f:entry>
</f:optionalBlock>
</j:jelly>
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
<div>
The number of times to retry if an exception happens dury its body of execution.
</div>
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
<div>
The time delay that will be used in between retries. The default unit is 'SECONDS'.
</div>
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
<div>
The unit of time to be applied to the time delay.
</div>
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
<div>
Use a time delay in between retries.
</div>
Loading