[API-REVIEW] RT-18702: Auto-restartable ScheduledService implementation

Daniel Zwolenski zonski at gmail.com
Tue Jan 29 15:28:05 PST 2013


Looks interesting.

Also looks quite similar to this:
http://static.springsource.org/spring/docs/3.0.x/reference/scheduling.html#scheduling-annotation-support

I've just been using this in my latest project and it is ridiculously nice
and simple. You might want to skim through to see if there's anything you
want to borrow for your API. The 'cron' pattern stuff in particular is nice
and useful.

Being a Spring junkie, I'd probably try to use the Spring thing over this
JFX version and then handle the last value side of it myself, but that's
without looking too deeply at what else your API would give me.





On Wed, Jan 30, 2013 at 10:02 AM, <richardbair at me.com> wrote:

> This has been a long time in coming! This is a followup to the original
> API design email back in 2011. I've finished the implementation & testing
> and will be pushing it shortly. I wanted to update this thread with the
> latest API (which is almost exactly as specified back in 2011) in case
> anybody would like to give a review.
>
> Randahl, you might want to look at the names particularly of the
> "backoffStrategy" and see if I got the capitalization right :-). I wasn't
> sure if it should be back off or backoff.
>
> Also, anyone with genius math skills can let me know if I implemented /
> spec'd the different backoff strategies correctly.
>
> Thanks
> Richard
>
> /**
>  * <p>The ScheduledService is a {@link Service} which will automatically
> restart
>  * itself after a successful execution, and under some conditions will
>  * restart even in case of failure. A new ScheduledService begins in
>  * the READY state, just as a normal Service. After calling
>  * <code>start</code> or <code>restart</code>, the ScheduledService will
>  * enter the SCHEDULED state for the duration specified by
> <code>delay</code>.
>  * </p>
>  *
>  * <p>Once RUNNING, the ScheduledService will execute its Task. On
> successful
>  * completion, the ScheduledService will transition to the SUCCEEDED state,
>  * and then to the READY state and back to the SCHEDULED state. The amount
>  * of time the ScheduledService will remain in this state depends on the
>  * amount of time between the last state transition to RUNNING, and the
>  * current time, and the <code>period</code>. In short, the
> <code>period</code>
>  * defines the minimum amount of time between executions. If the previous
>  * execution completed before <code>period</code> expires, then the
>  * ScheduledService will remain in the SCHEDULED state until the period
>  * expires. If on the other hand the execution took longer than the
>  * specified period, then the ScheduledService will immediately transition
>  * back to RUNNING. </p>
>  *
>  * <p>If, while RUNNING, the ScheduledService's Task throws an error or in
>  * some other way ends up transitioning to FAILED, then the
> ScheduledService
>  * will either restart or quit, depending on the values for
>  * <code>backoffStrategy</code>, <code>restartOnFailure</code>, and
>  * <code>maximumFailureCount</code>.</p>
>  *
>  * <p>If a failure occurs and <code>restartOnFailure</code> is false, then
>  * the ScheduledService will transition to FAILED and will stop. To restart
>  * a failed ScheduledService, you must call restart manually.</p>
>  *
>  * <p>If a failure occurs and <code>restartOnFailure</code> is true, then
>  * the the ScheduledService <em>may</em> restart automatically. First,
>  * the result of calling <code>backoffStrategy</code> will become the
>  * new <code>cumulativePeriod</code>. In this way, after each failure, you
> can cause
>  * the service to wait a longer and longer period of time before
> restarting.
>  * Once the task completes successfully, the cumulativePeriod is reset to
>  * the value of <code>period</code>.</p>
>  *
>  * <p>ScheduledService defines static EXPONENTIAL_BACKOFF_STRATEGY and
> LOGARITHMIC_BACKOFF_STRATEGY
>  * implementations, of which LOGARITHMIC_BACKOFF_STRATEGY is the default
> value for
>  * backoffStrategy. After <code>maximumFailureCount</code> is reached, the
>  * ScheduledService will transition to FAILED in exactly the same way as if
>  * <code>restartOnFailure</code> were false.</p>
>  *
>  * <p>If the <code>period</code> or <code>delay</code> is changed while the
>  * ScheduledService is running, the new values will be taken into account
> on the
>  * next iteration. For example, if the <code>period</code> is increased,
> then the next time the
>  * ScheduledService enters the SCHEDULED state, the new
> <code>period</code> will be used.
>  * Likewise, if the <code>delay</code> is changed, the new value will be
> honored on
>  * the next restart or reset/start.</p>
>  *
>  * The ScheduledService is typically used for use cases that involve
> polling. For
>  * example, you may want to ping a server on a regular basis to see if
> there are
>  * any updates. Such as ScheduledService might be implemented like this:
>  *
>  * <pre><code>
>  * ScheduledService&lt;Document&gt; svc = new
> ScheduledService&lt;&gt;(Duration.seconds(1)) {
>  *     protected Task&lt;Document&gt; createTask() {
>  *         return new Task&lt;Document&gt;() {
>  *             protected Document call() {
>  *                 // Connect to a Server
>  *                 // Get the XML document
>  *                 // Parse it into a document
>  *                 return document;
>  *             }
>  *         }
>  *     }
>  * }
>  * </code></pre>
>  *
>  * This example will ping the remote server every 1 second.
>  *
>  * <p>Timing for this class is not absolutely reliable. A very busy event
> thread might introduce some timing
>  * lag into the beginning of the execution of the background Task, so very
> small values for the period or
>  * delay are likely to be inaccurate. A delay or period in the hundreds of
> milliseconds or larger should be
>  * fairly reliable.</p>
>  *
>  * <p>The ScheduledService in its default configuration has a default
> <code>period</code> of 0 and a
>  * default <code>delay</code> of 0. This will cause the ScheduledService
> to execute the task immediately
>  * upon {@link #start()}, and re-executing immediately upon successful
> completion.</p>
>  *
>  * <p>For this purposes of this class, any Duration that answers true to
> {@link javafx.util.Duration#isUnknown()}
>  * will treat that duration as if it were Duration.ZERO. Likewise, any
> Duration which answers true
>  * to {@link javafx.util.Duration#isIndefinite()} will be treated as if it
> were a duration of Double.MAX_VALUE
>  * milliseconds. Any null Duration is treated as Duration.ZERO. Any custom
> implementation of an backoff strategy
>  * callback must be prepared to handle these different potential
> values.</p>
>  *
>  * <p>The ScheduledService introduces a new property called {@link
> #lastValue}. The lastValue is the value that
>  * was last successfully computed. Because a Service clears its {@code
> value} property on each run, and
>  * because the ScheduledService will reschedule a run immediately after
> completion (unless it enters the
>  * cancelled or failed states), the value property is not overly useful on
> a ScheduledService. In most cases
>  * you will want to instead use the value returned by lastValue.</p>
>  *
>  * <b>Implementer Note:</b> The {@link #ready()}, {@link #scheduled()},
> {@link #running()}, {@link #succeeded()},
>  * {@link #cancelled()}, and {@link #failed()} methods are implemented in
> this class. Subclasses which also
>  * override these methods must take care to invoke the super
> implementation.
>  *
>  * @param <V> The computed value of the ScheduledService
>  */
> public abstract class ScheduledService<V> extends Service<V> {
>     /**
>      * A Callback implementation for the <code>backoffStrategy</code>
> property which
>      * will exponentially backoff the period between re-executions in the
> case of
>      * a failure. This computation takes the original period and the
> number of
>      * consecutive failures and computes the backoff amount from that
> information.
>      *
>      * <p>If the {@code service} is null, then Duration.ZERO is returned.
> If the period is 0 then
>      * the result of this method will simply be {@code
> Math.exp(currentFailureCount)}. In all other cases,
>      * the returned value is the same as {@code period + (period *
> Math.exp(currentFailureCount))).</p>
>      */
>     public static final Callback<ScheduledService<?>, Duration>
> EXPONENTIAL_BACKOFF_STRATEGY
>             = new Callback<ScheduledService<?>, Duration>() {
>         @Override public Duration call(ScheduledService<?> service) {...};
>
>     /**
>      * A Callback implementation for the <code>backoffStrategy</code>
> property which
>      * will logarithmically backoff the period between re-executions in
> the case of
>      * a failure. This computation takes the original period and the
> number of
>      * consecutive failures and computes the backoff amount from that
> information.
>      *
>      * <p>If the {@code service} is null, then Duration.ZERO is returned.
> If the period is 0 then
>      * the result of this method will simply be {@code
> Math.log1p(currentFailureCount)}. In all other cases,
>      * the returned value is the same as {@code period + (period *
> Math.log1p(currentFailureCount))).</p>
>      */
>     public static final Callback<ScheduledService<?>, Duration>
> LOGARITHMIC_BACKOFF_STRATEGY
>             = new Callback<ScheduledService<?>, Duration>() {...};
>
>     /**
>      * A Callback implementation for the <code>backoffStrategy</code>
> property which
>      * will linearly backoff the period between re-executions in the case
> of
>      * a failure. This computation takes the original period and the
> number of
>      * consecutive failures and computes the backoff amount from that
> information.
>      *
>      * <p>If the {@code service} is null, then Duration.ZERO is returned.
> If the period is 0 then
>      * the result of this method will simply be {@code
> currentFailureCount}. In all other cases,
>      * the returned value is the same as {@code period + (period *
> currentFailureCount).</p>
>      */
>     public static final Callback<ScheduledService<?>, Duration>
> LINEAR_BACKOFF_STRATEGY
>             = new Callback<ScheduledService<?>, Duration>() {...};
>
>     /**
>      * This Timer is used to schedule the delays for each
> ScheduledService. A single timer
>      * ought to be able to easily service thousands of ScheduledService
> objects.
>      */
>     private static final Timer DELAY_TIMER = new Timer("ScheduledService
> Delay Timer", true);
>
>     /**
>      * The initial delay between when the ScheduledService is first
> started, and when it will begin
>      * operation. This is the amount of time the ScheduledService will
> remain in the SCHEDULED state,
>      * before entering the RUNNING state, following a fresh invocation of
> {@link #start()} or {@link #restart()}.
>      */
>     private ObjectProperty<Duration> delay = new
> SimpleObjectProperty<>(this, "delay", Duration.ZERO);
>     public final Duration getDelay() { return delay.get(); }
>     public final void setDelay(Duration value) { delay.set(value); }
>     public final ObjectProperty<Duration> delayProperty() { return delay; }
>
>     /**
>      * The minimum amount of time to allow between the last time the
> service was in the RUNNING state
>      * until it should run again. The actual period (also known as
> <code>cumulativePeriod</code>)
>      * will depend on this property as well as the
> <code>backoffStrategy</code> and number of failures.
>      */
>     private ObjectProperty<Duration> period = new
> SimpleObjectProperty<>(this, "period", Duration.ZERO);
>     public final Duration getPeriod() { return period.get(); }
>     public final void setPeriod(Duration value) { period.set(value); }
>     public final ObjectProperty<Duration> periodProperty() { return
> period; }
>
>     /**
>      * Computes the amount of time to add to the period on each failure.
> This cumulative amount is reset whenever
>      * the the ScheduledService is manually restarted.
>      */
>     private ObjectProperty<Callback<ScheduledService<?>,Duration>>
> backoffStrategy =
>             new SimpleObjectProperty<>(this, "backoffStrategy",
> LOGARITHMIC_BACKOFF_STRATEGY);
>     public final Callback<ScheduledService<?>,Duration>
> getBackoffStrategy() { return backoffStrategy.get(); }
>     public final void setBackoffStrategy(Callback<ScheduledService<?>,
> Duration> value) { backoffStrategy.set(value); }
>     public final ObjectProperty<Callback<ScheduledService<?>,Duration>>
> backoffStrategyProperty() { return backoffStrategy; }
>
>     /**
>      * Indicates whether the ScheduledService should automatically restart
> in the case of a failure in the Task.
>      */
>     private BooleanProperty restartOnFailure = new
> SimpleBooleanProperty(this, "restartOnFailure", true);
>     public final boolean getRestartOnFailure() { return
> restartOnFailure.get(); }
>     public final void setRestartOnFailure(boolean value) {
> restartOnFailure.set(value); }
>     public final BooleanProperty restartOnFailureProperty() { return
> restartOnFailure; }
>
>     /**
>      * The maximum number of times the ScheduledService can fail before it
> simply ends in the FAILED
>      * state. You can of course restart the ScheduledService manually,
> which will cause the current
>      * count to be reset.
>      */
>     private IntegerProperty maximumFailureCount = new
> SimpleIntegerProperty(this, "maximumFailureCount", Integer.MAX_VALUE);
>     public final int getMaximumFailureCount() { return
> maximumFailureCount.get(); }
>     public final void setMaximumFailureCount(int value) {
> maximumFailureCount.set(value); }
>     public final IntegerProperty maximumFailureCountProperty() { return
> maximumFailureCount; }
>
>     /**
>      * The current number of times the ScheduledService has failed. This
> is reset whenever the
>      * ScheduledService is manually restarted.
>      */
>     private ReadOnlyIntegerWrapper currentFailureCount = new
> ReadOnlyIntegerWrapper(this, "currentFailureCount", 0);
>     public final int getCurrentFailureCount() { return
> currentFailureCount.get(); }
>     public final ReadOnlyIntegerProperty currentFailureCountProperty() {
> return currentFailureCount.getReadOnlyProperty(); }
>
>     /**
>      * The current cumulative period in use between iterations. This will
> be the same as <code>period</code>,
>      * except after a failure, in which case the result of the
> backoffStrategy will be used as the cumulative period
>      * following each failure. This is reset whenever the ScheduledService
> is manually restarted or an iteration
>      * is successful. The cumulativePeriod is modified when the
> ScheduledService enters the scheduled state.
>      * The cumulativePeriod can be capped by setting the {@code
> maximumCumulativePeriod}.
>      */
>     private ReadOnlyObjectWrapper<Duration> cumulativePeriod = new
> ReadOnlyObjectWrapper<>(this, "cumulativePeriod", Duration.ZERO);
>     public final Duration getCumulativePeriod() { return
> cumulativePeriod.get(); }
>     public final ReadOnlyObjectProperty<Duration>
> cumulativePeriodProperty() { return cumulativePeriod.getReadOnlyProperty();
> }
>
>     /**
>      * The maximum allowed value for the cumulativePeriod. Setting this
> value will help ensure that in the case of
>      * repeated failures the back-off algorithm doesn't end up producing
> unreasonably large values for
>      * cumulative period. The cumulative period is guaranteed not to be
> any larger than this value. If the
>      * maximumCumulativePeriod is negative, then cumulativePeriod will be
> capped at 0. If maximumCumulativePeriod
>      * is NaN or null, then it will not influence the cumulativePeriod.
>      */
>     private ObjectProperty<Duration> maximumCumulativePeriod = new
> SimpleObjectProperty<>(this, "maximumCumulativePeriod",
> Duration.INDEFINITE);
>     public final Duration getMaximumCumulativePeriod() { return
> maximumCumulativePeriod.get(); }
>     public final void setMaximumCumulativePeriod(Duration value) {
> maximumCumulativePeriod.set(value); }
>     public final ObjectProperty<Duration>
> maximumCumulativePeriodProperty() { return maximumCumulativePeriod; }
>
>     /**
>      * The last successfully computed value. During each iteration, the
> "value" of the ScheduledService will be
>      * reset to null, as with any other Service. The "lastValue" however
> will be set to the most recently
>      * successfully computed value, even across iterations. It is reset
> however whenever you manually call
>      * reset or restart.
>      */
>     private ReadOnlyObjectWrapper<V> lastValue = new
> ReadOnlyObjectWrapper<>(this, "lastValue", null);
>     public final V getLastValue() { return lastValue.get(); }
>     public final ReadOnlyObjectProperty lastValueProperty() { return
> lastValue.getReadOnlyProperty(); }
>
>
> On Dec 23, 2011, at 4:10 PM, Richard Bair <richard.bair at oracle.com> wrote:
>
> > Hi,
> >
> > http://javafx-jira.kenai.com/browse/RT-18702
> >
> > While waiting for builds and such, I implemented a ScheduledService.
> Several folks on the forums had asked for something like this, so I thought
> I'd take a shot at writing a complete implementation that could make it
> into 2.1. I would end up writing all the unit tests for this, and total
> development time is estimated at no more than 4 days (so far I've got 4
> hours put into it and it isn't in too shabby of condition, so I think the
> estimate is reasonable).
> >
> > Attached to the issue is the implementation. I'd love if somebody would
> like to take a look, particularly at the API. The name comes from the
> java.util.concurrent package, which has a ScheduledExecutorService. Since
> this is performing a similar function, a similar name seemed in order. In
> addition, the "period" name comes from that package as well (since the
> names used in the animation package I think have different semantics, so
> using a different name is a good idea here, I think).
> >
> > Here it is:
> >
> > /**
> > * <p>
> > *     The ScheduledService is a service which will automatically restart
> > *     itself after a successful execution, and under some conditions will
> > *     restart even in case of failure. A new ScheduledService begins in
> > *     the READY state, just as a normal Service. After calling
> > *     <code>start</code> or <code>restart</code>, the ScheduledService
> will
> > *     enter the SCHEDULED state for the duration specified by
> <code>delay</code>.
> > * </p>
> > * <p>
> > *     Once RUNNING, the ScheduledService will execute its Task. On
> successful
> > *     completion, the ScheduledService will transition to the SUCCEEDED
> state,
> > *     and then to the READY state and back to the SCHEDULED state. The
> amount
> > *     of time the ScheduledService will remain in this state depends on
> the
> > *     amount of time between the last state transition to RUNNING, and
> the
> > *     current time, and the <code>period</code>. In short, the
> <code>period</code>
> > *     defines the minimum amount of time between executions. If the
> previous
> > *     execution completed before <code>period</code> expires, then the
> > *     ScheduledService will remain in the SCHEDULED state until the
> period
> > *     expires. If on the other hand the execution took longer than the
> > *     specified period, then the ScheduledService will immediately
> transition
> > *     back to RUNNING.
> > * </p>
> > * <p>
> > *     If, while RUNNING, the ScheduledService's Task throws an error or
> in
> > *     some other way ends up transitioning to FAILED, then the
> ScheduledService
> > *     will either restart or quit, depending on the values for
> > *     <code>computeEaseOff</code>, <code>restartOnFailure</code>, and
> > *     <code>maximumFailureCount</code>.
> > * </p>
> > * <p>
> > *     If a failure occurs and <code>restartOnFailure</code> is false,
> then
> > *     the ScheduledService will transition to FAILED and will stop. To
> restart
> > *     a failed ScheduledService, you must call restart manually.
> > * </p>
> > * <p>
> > *     If a failure occurs and <code>restartOnFailure</code> is true, then
> > *     the the ScheduledService <em>may</em> restart automatically. First,
> > *     the result of calling <code>computeEaseOff</code> will become the
> > *     new <code>cumulativePeriod</code>. In this way, after each
> failure, you can cause
> > *     the service to wait a longer and longer period of time before
> restarting.
> > *     ScheduledService defines static EXPONENTIAL_EASE_OFF and
> LOGARITHMIC_EASE_OFF
> > *     implementations, of which LOGARITHMIC_EASE_OFF is the default
> value of
> > *     computeEaseOff. After <code>maximumFailureCount</code> is reached,
> the
> > *     ScheduledService will transition to FAILED in exactly the same way
> as if
> > *     <code>restartOnFailure</code> were false.
> > * </p>
> > */
> >
> >    /**
> >     * A Callback implementation for the <code>computeEaseOff</code>
> property which
> >     * will exponentially ease off the period between re-executions in
> the case of
> >     * a failure. This computation takes the original period and the
> number of
> >     * consecutive failures and computes the ease off amount from that
> information.
> >     */
> >    public static final Callback<ScheduledService<?>, Duration>
> EXPONENTIAL_EASE_OFF....
> >
> >    /**
> >     * A Callback implementation for the <code>computeEaseOff</code>
> property which
> >     * will logarithmically ease off the period between re-executions in
> the case of
> >     * a failure. This computation takes the original period and the
> number of
> >     * consecutive failures and computes the ease off amount from that
> information.
> >     */
> >    public static final Callback<ScheduledService<?>, Duration>
> LOGARITHMIC_EASE_OFF....
> >
> >    /**
> >     * The initial delay between when the ScheduledService is first
> started, and when it will begin
> >     * operation. This is the amount of time the ScheduledService will
> remain in the SCHEDULED state,
> >     * before entering the RUNNING state.
> >     */
> >    private ObjectProperty<Duration> delay = new
> SimpleObjectProperty<Duration>(this, "delay", Duration.ZERO);
> >    ....
> >
> >    /**
> >     * The minimum amount of time to allow between the last time the
> service was in the RUNNING state
> >     * until it should run again. The actual period (also known as
> <code>cumulativePeriod</code>)
> >     * will depend on this property as well as the
> <code>computeEaseOff</code> and number of failures.
> >     */
> >    private ObjectProperty<Duration> period = new
> SimpleObjectProperty<Duration>(this, "period", Duration.ZERO);
> >    ....
> >
> >    /**
> >     * Computes the amount of time to add to the period on each failure.
> This cumulative amount is reset whenever
> >     * the the ScheduledService is manually restarted. The Callback takes
> a Duration, which is the last
> >     * <code>cumulativePeriod</code>, and returns a Duration which will
> be the new <code>cumulativePeriod</code>.
> >     */
> >    private ObjectProperty<Callback<ScheduledService<?>,Duration>>
> computeEaseOff =
> >            new
> SimpleObjectProperty<Callback<ScheduledService<?>,Duration>>(this,
> "computeEaseOff", LOGARITHMIC_EASE_OFF);
> >    ....
> >
> >    /**
> >     * Indicates whether the ScheduledService should automatically
> restart in the case of a failure.
> >     */
> >    private BooleanProperty restartOnFailure = new
> SimpleBooleanProperty(this, "restartOnFailure", false);
> >    ....
> >
> >    /**
> >     * The maximum number of times the ScheduledService can fail before
> it simply ends in the FAILED
> >     * state. You can of course restart the ScheduledService manually,
> which will cause the current
> >     * count to be reset.
> >     */
> >    private IntegerProperty maximumFailureCount = new
> SimpleIntegerProperty(this, "maximumFailureCount", Integer.MAX_VALUE);
> >    ....
> >
> >    /**
> >     * The current number of times the ScheduledService has failed. This
> is reset whenever the
> >     * ScheduledService is manually restarted.
> >     */
> >    private ReadOnlyIntegerWrapper currentFailureCount = new
> ReadOnlyIntegerWrapper(this, "currentFailureCount", 0);
> >    ....
> >
> >    /**
> >     * The current cumulative period in use between iterations. This will
> be the same as <code>period</code>,
> >     * except after a failure, in which case the
> <code>computeEaseOff</code> will compute a new period. This
> >     * is reset whenever the ScheduledService is manually restarted.
> >     */
> >    private ReadOnlyObjectWrapper<Duration> cumulativePeriod = new
> ReadOnlyObjectWrapper<Duration>(this, "cumulativePeriod", Duration.ZERO);
> >    ....
> >
> >    /**
> >     * The last successfully computed value. During each iteration, the
> "value" of the ScheduledService will be
> >     * reset to null, as with any other Service. The "lastValue" however
> will be set to the most currently
> >     * successfully computed value, even across iterations. It is reset
> however whenever you manually call
> >     * reset or restart.
> >     */
> >    private ReadOnlyObjectWrapper<V> lastValue = new
> ReadOnlyObjectWrapper<V>(this, "lastValue", null);
> >    ....
> >
> >
> > It seems pretty powerful and flexible but the API isn't too bad. I
> played with a couple configurations but this one seemed to play best with
> the existing semantics of the Service. For example, on Service the "value"
> is set to null whenever the Service is reset for another run. But this
> doesn't work as well for an auto-restarting service like ScheduledService.
> So rather than break the semantic that "value" is set to null on restart, I
> added a new variable "lastValue" which remembers the last value even across
> failures, and is only rest when the developer manually resets or restarts
> things. During an auto-restart iteration, the lastValue is not changed (it
> is only updated to the last "value" of a successful run).
> >
> > Thanks
> > Richard
>
>


More information about the openjfx-dev mailing list