/**
* Copyright 2010 Google Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
*/
package org.waveprotocol.wave.util.concurrent;
import com.google.common.annotations.VisibleForTesting;
import static com.google.common.base.Preconditions.checkState;
import org.joda.time.Duration;
import static java.util.concurrent.TimeUnit.NANOSECONDS;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.ScheduledFuture;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.ScheduledThreadPoolExecutor;
import java.util.concurrent.Executors;
/**
* Class for creating and managing timers that are cancellable, and when
* cancelled, will not reference the Runnable that is the callback for the timer.
* This is required in some circumstances because even if you cancel the
* timer, the scheduler keeps it around until the original time expires,
* possibly locking stuff in memory.
*
* Thread safe.
*
* @author amcrae@google.com (Andrew McRae)
*/
public class CancellableTimer {
/**
* Interface used for holding reference to cancellable task.
*/
public interface CancellableTask {
/**
* Cancel this task, if it hasn't already started.
* There is no guarantee that the task isn't already in progress,
* in which it will run to completion.
*/
void cancel();
}
/**
* Class used for timeout callback. Allows the handle to be cleared
* so that when or if the timeout is cancelled, the callback does not
* reference any of the resources via the runnable.
* Upon a cancel request, the scheduled task is cancelled, and the
* reference to the runnable is nulled so that no reference is kept
* to the runnable (and all the associated objects). In essence
* this class acts as a "cut-out" between the scheduled task and the callback.
* It is done this way instead of relying on Future.cancel()
* because Future.cancel() will not actually remove the Future from the queue
* to be executed, just mark it as cancelled - which means that Future is
* still referencing all the resources associated with the Runnable for an
* indeterminate length of time.
*/
private static class CancellableTaskImpl implements Runnable, CancellableTask {
private volatile Runnable runnable;
private volatile ScheduledFuture<?> future;
private CancellableTaskImpl(Runnable callback) {
this.runnable = callback;
}
@Override
public void run() {
// Make a copy in case it gets cancelled under us.
Runnable copy = runnable;
if (copy != null) {
// Make sure there are no longer any references to runnable.
runnable = null;
copy.run();
}
}
@Override
public void cancel() {
// Remove the runnable so that this task does not any hanging
// reference to the runnable (and any resources associated with the runnable).
runnable = null;
// Cancel the scheduled task. If it has already run, this will be a no-op.
if (future != null) {
future.cancel(false);
}
}
/**
* Register the future of the task.
*/
private void setFuture(ScheduledFuture<?> taskFuture) {
future = taskFuture;
}
}
private final ScheduledExecutorService scheduler;
private final Stopwatch stopwatch;
/**
* Constructor for testing.
*
* @param scheduler Executor service to use.
* @param stopwatch The stopwatch to use for timing.
*/
@VisibleForTesting
CancellableTimer(ScheduledExecutorService scheduler, Stopwatch stopwatch) {
this.scheduler = scheduler;
this.stopwatch = stopwatch;
stopwatch.start();
}
/**
* Constructor with custom scheduler.
*
* @param scheduler Executor service to use.
*/
public CancellableTimer(ScheduledExecutorService scheduler) {
this(scheduler, new Stopwatch());
}
/**
* Default constructor. Uses a single threaded executor, so that each of the
* tasks are executed in sequence, and never in parallel.
*/
public CancellableTimer() {
// TODO(arb): STPE eats exceptions. open source the SafeExecutor class from the waveserver code.
this(new ScheduledThreadPoolExecutor(1, Executors.defaultThreadFactory()), new Stopwatch());
}
/**
* Factory to create a new timeout task using a Duration.
*
* @param runnable Callback to invoke when timeout expires.
* @param duration Duration until timeout runnable is called.
*/
public CancellableTask newTask(Runnable runnable, Duration duration) {
return newTask(runnable, duration.getMillis(), TimeUnit.MILLISECONDS);
}
/**
* Factory for using timeunits.
*
* @param runnable Callback to invoke when timeout expires.
* @param time Timeout value.
* @param units TimeUnits of value
*/
public CancellableTask newTask(Runnable runnable, long time, TimeUnit units) {
CancellableTaskImpl task = new CancellableTaskImpl(runnable);
task.setFuture(scheduler.schedule(task, time, units));
return task;
}
/**
* @return the current elapsed duration.
*/
public Duration elapsedDuration() {
return stopwatch.elapsedDuration();
}
private static class Stopwatch {
private Ticker ticker;
private boolean isRunning;
private long startTick;
private long elapsedNanos;
/**
* A time source; returns a time value representing the number of nanoseconds elapsed since some
* fixed but arbitrary point in time.
*/
public interface Ticker {
/**
* Returns the number of nanoseconds elapsed since this ticker's fixed point of reference.
*/
long read();
}
/**
* The ticker that returns {@link System#nanoTime()}.
*/
public static final Ticker JAVA_TICKER = new Ticker() {
public long read() {
return System.nanoTime();
}
};
/**
* Creates (but does not start) a new stopwatch using {@link System#nanoTime} as its time
* source.
*/
public Stopwatch() {
this.ticker = JAVA_TICKER;
}
/**
* Starts the stopwatch.
*
* @throws IllegalStateException if the stopwatch is already running.
*/
public Stopwatch start() {
checkState(!isRunning);
isRunning = true;
startTick = ticker.read();
return this;
}
/**
* Stops the stopwatch. Future reads will return the fixed duration that had elapsed up to this
* point.
*
* @throws IllegalStateException if the stopwatch is already stopped.
*/
public Stopwatch stop() {
long tick = ticker.read();
checkState(isRunning);
isRunning = false;
elapsedNanos += tick - startTick;
return this;
}
private long elapsedNanos() {
return isRunning ? ticker.read() - startTick + elapsedNanos : elapsedNanos;
}
/**
* Returns the current elapsed time shown on this stopwatch, expressed in the desired time unit,
* with any fraction rounded down.
*
* <p>Note that the overhead of measurement can be more than a microsecond, so it is generally
* not useful to specify {@link TimeUnit#NANOSECONDS} precision here.
*/
public long elapsedTime(TimeUnit desiredUnit) {
return desiredUnit.convert(elapsedNanos(), NANOSECONDS);
}
/**
* Returns the current elapsed time value shown on this stopwatch, rounded down to the nearest
* millisecond, expressed as a Joda-Time {@code Duration}.
*/
public Duration elapsedDuration() {
return new Duration(elapsedTime(TimeUnit.MILLISECONDS));
}
}
}