-
Notifications
You must be signed in to change notification settings - Fork 127
Added delay capability to retry #97
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
base: master
Are you sure you want to change the base?
Changes from all commits
02cc317
b4c0a76
721d8aa
960d430
50d2ec9
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -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) { | ||
| 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 | ||
|
|
@@ -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; | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 { | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
|
||
| 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) { | ||
|
|
@@ -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); | ||
|
|
||
| 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> |
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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