package com.fxexperience.concurrent; import javafx.animation.PauseTransition; import javafx.beans.property.*; import javafx.concurrent.Service; import javafx.concurrent.Task; import javafx.event.ActionEvent; import javafx.event.EventHandler; import javafx.util.Callback; import javafx.util.Duration; /** *

* 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 * start or restart, the ScheduledService will * enter the SCHEDULED state for the duration specified by delay. *

*

* 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 period. In short, the period * defines the minimum amount of time between executions. If the previous * execution completed before period 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. *

*

* 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 * computeEaseOff, restartOnFailure, and * maximumFailureCount. *

*

* If a failure occurs and restartOnFailure is false, then * the ScheduledService will transition to FAILED and will stop. To restart * a failed ScheduledService, you must call restart manually. *

*

* If a failure occurs and restartOnFailure is true, then * the the ScheduledService may restart automatically. First, * the result of calling computeEaseOff will become the * new cumulativePeriod. 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 maximumFailureCount is reached, the * ScheduledService will transition to FAILED in exactly the same way as if * restartOnFailure were false. *

*/ public abstract class ScheduledService extends Service { /** * A Callback implementation for the computeEaseOff 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, Duration> EXPONENTIAL_EASE_OFF = new Callback, Duration>() { @Override public Duration call(ScheduledService service) { double period = (service == null || service.getPeriod() == null) ? 0 : service.getPeriod().toMillis(); double x = (service == null) ? 0 : service.getCurrentFailureCount(); return Duration.millis(period + (period * Math.exp(x))); } }; /** * A Callback implementation for the computeEaseOff 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, Duration> LOGARITHMIC_EASE_OFF = new Callback, Duration>() { @Override public Duration call(ScheduledService service) { double period = (service == null || service.getPeriod() == null) ? 0 : service.getPeriod().toMillis(); double x = (service == null) ? 0 : service.getCurrentFailureCount(); return Duration.millis(period + (period * Math.log1p(x))); } }; /** * 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 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 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 cumulativePeriod) * will depend on this property as well as the computeEaseOff and number of failures. */ private ObjectProperty 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 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. The Callback takes a Duration, which is the last * cumulativePeriod, and returns a Duration which will be the new cumulativePeriod. */ private ObjectProperty,Duration>> computeEaseOff = new SimpleObjectProperty,Duration>>(this, "computeEaseOff", LOGARITHMIC_EASE_OFF); public final Callback,Duration> getComputeEaseOff() { return computeEaseOff.get(); } public final void setComputeEaseOff(Callback,Duration> value) { computeEaseOff.set(value); } public final ObjectProperty,Duration>> computeEaseOffProperty() { return computeEaseOff; } /** * Indicates whether the ScheduledService should automatically restart in the case of a failure. */ private BooleanProperty restartOnFailure = new SimpleBooleanProperty(this, "restartOnFailure", false); 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 period, * except after a failure, in which case the easeOffDuration will be added to the period * for each failure when. This is reset whenever the ScheduledService is manually restarted. */ private ReadOnlyObjectWrapper cumulativePeriod = new ReadOnlyObjectWrapper(this, "cumulativePeriod", Duration.ZERO); public final Duration getCumulativePeriod() { return cumulativePeriod.get(); } public final ReadOnlyObjectProperty cumulativePeriodProperty() { return cumulativePeriod.getReadOnlyProperty(); } /** * 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 lastValue = new ReadOnlyObjectWrapper(this, "lastValue", null); public final V getLastValue() { return lastValue.get(); } public final ReadOnlyObjectProperty lastValueProperty() { return lastValue.getReadOnlyProperty(); } /** * The timestamp of the last time the thing was run */ private long lastRunTime = 0L; private boolean freshStart = true; // private boolean performIteration = false; private PauseTransition pauseTransition = null; @Override protected void succeeded() { super.succeeded(); lastValue.set(getValue()); // Reset the cumulative time Duration d = getPeriod(); setCumulativePeriod(d == null ? Duration.ZERO : d); // Call the super implementation of reset, which will not cause us // to think this is a new fresh start. ScheduledService.this.superReset(); // Fire it up! ScheduledService.this.start(); } @Override protected void cancelled() { super.cancelled(); // Stop the pauseTransition if it exists if (pauseTransition != null) { pauseTransition.stop(); pauseTransition = null; } } @Override protected void failed() { super.failed(); // Stop the pauseTransition if it exists if (pauseTransition != null) { pauseTransition.stop(); pauseTransition = null; } // Restart as necessary setCurrentFailureCount(getCurrentFailureCount() + 1); if (getMaximumFailureCount() > getCurrentFailureCount()) { // We've not yet maxed out the number of failures we can // encounter, so we're going to iterate Callback,Duration> func = getComputeEaseOff(); if (func != null) { Duration d = func.call(this); setCumulativePeriod(d == null ? Duration.ZERO : d); } ScheduledService.this.superReset(); // Fire it up! ScheduledService.this.start(); } else { // We've maxed out, so do nothing and things will just stop. } } private void superReset() { super.reset(); } @Override public void reset() { super.reset(); Duration p = getPeriod(); setCumulativePeriod(p == null ? Duration.ZERO : p); lastValue.set(null); currentFailureCount.set(0); lastRunTime = 0L; freshStart = true; } @Override protected void executeTask(final Task task) { // TODO checkThread() if (freshStart) { // The pauseTransition should have concluded and been made null by this point. // If not, then somehow we were paused waiting for another iteration and // somebody caused the system to run again. However resetting things should // have cleared the transition. assert pauseTransition == null; // Pause for the "delay" amount of time then execute final Duration d = getDelay(); if (d == null || d.toMillis() == 0) { // If the delay is zero or null, then just start immediately executeTask(task); } else { pauseTransition = new PauseTransition(d == null ? Duration.ZERO : d); pauseTransition.setOnFinished(new EventHandler() { @Override public void handle(ActionEvent actionEvent) { executeTaskNow(task); pauseTransition = null; } }); pauseTransition.play(); } } else { // We are executing as a result of an iteration, not a fresh start. // If the runPeriod (time between the last run and now) exceeds the cumulativePeriod, then // we need to execute immediately. Otherwise, we will pause until the cumulativePeriod has // been reached, and then run. double cumulative = getCumulativePeriod().toMillis(); // Can never be null. double runPeriod = System.currentTimeMillis() - lastRunTime; if (runPeriod < cumulative) { // Pause and then execute assert pauseTransition == null; pauseTransition = new PauseTransition(Duration.millis(cumulative - runPeriod)); pauseTransition.setOnFinished(new EventHandler() { @Override public void handle(ActionEvent actionEvent) { executeTaskNow(task); pauseTransition = null; } }); pauseTransition.play(); } else { // Execute immediately executeTaskNow(task); } } } private void executeTaskNow(Task task) { lastRunTime = System.currentTimeMillis(); freshStart = false; super.executeTask(task); } void setCumulativePeriod(Duration value) { if (value == null) throw new NullPointerException("cumulative period cannot be null"); cumulativePeriod.set(value); } void setCurrentFailureCount(int value) { currentFailureCount.set(value); } }