Skip to content

Added delay capability to retry step #153

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 12 commits into
base: master
Choose a base branch
from
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 @@ -24,38 +24,57 @@

package org.jenkinsci.plugins.workflow.steps;

import hudson.Extension;
import hudson.model.TaskListener;
import java.io.Serializable;
import java.util.Collection;
import java.util.Collections;
import java.util.Set;

import org.jenkinsci.plugins.workflow.support.steps.retry.RetryDelay;
import org.kohsuke.stapler.DataBoundConstructor;
import org.kohsuke.stapler.DataBoundSetter;

import hudson.Extension;
import hudson.model.Descriptor;
import hudson.model.TaskListener;

/**
* 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 RetryDelay delay = null;

public int left;

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

public int getCount() {
return count;
}

@DataBoundSetter public void setDelay(RetryDelay delay) {
this.delay = delay;
}

public RetryDelay getDelay() {
return delay;
}

@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 @@ -81,6 +100,10 @@ public Set<? extends Class<?>> getRequiredContext() {
return Collections.singleton(TaskListener.class);
}

public Collection<? extends Descriptor<?>> getApplicableDescriptors() {
return RetryDelay.RetryDelayDescriptor.all();
}
}

private static final long serialVersionUID = 1L;
}
Original file line number Diff line number Diff line change
@@ -1,53 +1,155 @@
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.model.Run;
import hudson.Util;
import hudson.model.TaskListener;
import jenkins.model.CauseOfInterruption;

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 {
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.getDelay().computeRetryDelay();
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 will be calculated ms " +
"during the next run." ;
}
}

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 @@ -61,8 +163,15 @@ 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());
Expand All @@ -72,8 +181,12 @@ public void onFailure(StepContext context, Throwable t) {
} else {
Functions.printStackTrace(t, l.error("Execution failed"));
}
l.getLogger().println("Retrying");
context.newBodyInvoker().withCallback(this).start();
if(step != null && step.getDelay() == null) {
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
@@ -0,0 +1,94 @@
package org.jenkinsci.plugins.workflow.support.steps.retry;

import java.io.Serializable;

import javax.annotation.Nonnull;

import org.jenkinsci.Symbol;
import org.kohsuke.stapler.DataBoundConstructor;

import hudson.Extension;

/**
* {@link ExponentialDelay} allows the delay to exponentially grow
* larger for issues that cannot be immediately fixed. The delay
* starts out at the min and then increases 2^x * 1 {@link java.util.concurrent.TimeUnit}
* each round after until it reaches the max, then the max for the
* remaining rounds.
*/
@Extension
public class ExponentialDelay extends RetryDelay implements Serializable {

private static final int base = 2;

private final long min;
private final long max;
private final int multiplier;
private int lastMultiplier = 0;

public ExponentialDelay() {
super();
this.multiplier = 0;
this.max = 0;
this.min = 0;
}

@DataBoundConstructor
public ExponentialDelay(int multiplier, long min, long max) {
this.multiplier = multiplier;
this.max = max;
this.min = min;
}

public int getMultiplier() {
return multiplier;
}

public long getMin() {
return min;
}

public long getMax() {
return max;
}

@Override
public long computeRetryDelay() {
if(lastMultiplier > 0) {
lastMultiplier += 1;
} else {
lastMultiplier = multiplier;
}
long delay = powerN(base, lastMultiplier) + min;

// Check to see if greater than max
if(delay > max) {
delay = max;
}
return unit.toMillis(delay);
}

protected static long powerN(long number, int power){
long res = 1;
long sq = number;
while(power > 0){
if(power % 2 == 1){
res *= sq;
}
sq = sq * sq;
power /= 2;
}
return res;
}

@Extension @Symbol("exponential")
public static class DescriptorImpl extends RetryDelayDescriptor {
@Override
@Nonnull
public String getDisplayName() {
return "Exponential";
}
private static final long serialVersionUID = 1L;
}
private static final long serialVersionUID = 1L;
}
Loading