changeset: 229:9ee4de8ee2ce tag: qtip tag: scheduled tag: tip user: rbair date: Mon Jan 09 15:09:23 2012 -0800 summary: [mq]: scheduled diff -r 160b675b10e3 -r 9ee4de8ee2ce javafx-concurrent/src/javafx/concurrent/ObservableListTask.java --- a/javafx-concurrent/src/javafx/concurrent/ObservableListTask.java Mon Jan 09 14:21:59 2012 -0800 +++ b/javafx-concurrent/src/javafx/concurrent/ObservableListTask.java Mon Jan 09 15:09:23 2012 -0800 @@ -1,5 +1,5 @@ /* - * Copyright (c) 2011, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2012, Oracle and/or its affiliates. All rights reserved. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * * This code is free software; you can redistribute it and/or modify it diff -r 160b675b10e3 -r 9ee4de8ee2ce javafx-concurrent/src/javafx/concurrent/ScheduledService.java --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/javafx-concurrent/src/javafx/concurrent/ScheduledService.java Mon Jan 09 15:09:23 2012 -0800 @@ -0,0 +1,319 @@ +/* + * Copyright (c) 2012, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ + +package javafx.concurrent; + +import javafx.animation.PauseTransition; +import javafx.beans.property.*; +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) { + 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); + } +} \ No newline at end of file diff -r 160b675b10e3 -r 9ee4de8ee2ce javafx-concurrent/src/javafx/concurrent/TaskBase.java --- a/javafx-concurrent/src/javafx/concurrent/TaskBase.java Mon Jan 09 14:21:59 2012 -0800 +++ b/javafx-concurrent/src/javafx/concurrent/TaskBase.java Mon Jan 09 15:09:23 2012 -0800 @@ -1,5 +1,5 @@ /* - * Copyright (c) 2011, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2012, Oracle and/or its affiliates. All rights reserved. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * * This code is free software; you can redistribute it and/or modify it