-
Notifications
You must be signed in to change notification settings - Fork 3.9k
core: ResettableTimer. #1994
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
Closed
Closed
core: ResettableTimer. #1994
Changes from all commits
Commits
Show all changes
8 commits
Select commit
Hold shift + click to select a range
10d46af
core: ResettableTimer.
zhangkun83 f0aba93
Do not hold the lock when calling timerExpired().
zhangkun83 83d2042
Address threading concerns.
zhangkun83 bcb8c84
Fix a bug that the timer becomes unusable after stop().
zhangkun83 14e5c73
Add shutdown().
zhangkun83 300a1dd
Fix a bug in stop().
zhangkun83 cc2814b
Fix isCancelled().
zhangkun83 6014e6f
Add start().
zhangkun83 File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
295 changes: 295 additions & 0 deletions
295
core/src/main/java/io/grpc/internal/ResettableTimer.java
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,295 @@ | ||
/* | ||
* Copyright 2016, Google Inc. All rights reserved. | ||
* | ||
* Redistribution and use in source and binary forms, with or without | ||
* modification, are permitted provided that the following conditions are | ||
* met: | ||
* | ||
* * Redistributions of source code must retain the above copyright | ||
* notice, this list of conditions and the following disclaimer. | ||
* * Redistributions in binary form must reproduce the above | ||
* copyright notice, this list of conditions and the following disclaimer | ||
* in the documentation and/or other materials provided with the | ||
* distribution. | ||
* | ||
* * Neither the name of Google Inc. nor the names of its | ||
* contributors may be used to endorse or promote products derived from | ||
* this software without specific prior written permission. | ||
* | ||
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS | ||
* "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT | ||
* LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR | ||
* A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT | ||
* OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, | ||
* SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT | ||
* LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, | ||
* DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY | ||
* THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT | ||
* (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE | ||
* OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. | ||
*/ | ||
|
||
package io.grpc.internal; | ||
|
||
import static com.google.common.base.Preconditions.checkNotNull; | ||
import static com.google.common.base.Preconditions.checkState; | ||
|
||
import com.google.common.base.Stopwatch; | ||
|
||
import java.util.concurrent.ScheduledExecutorService; | ||
import java.util.concurrent.ScheduledFuture; | ||
import java.util.concurrent.TimeUnit; | ||
import javax.annotation.Nullable; | ||
import javax.annotation.concurrent.GuardedBy; | ||
import javax.annotation.concurrent.ThreadSafe; | ||
|
||
/** | ||
* A timer that is optimized for being reset frequently. | ||
* | ||
* <p>When a scheduled timer is cancelled or reset, it doesn't cancel the task from the scheduled | ||
* executor. Instead, when the task is run, it checks the current state and may schedule a new task | ||
* for the new expiration time. | ||
* | ||
* <h3>Threading considerations</h3> | ||
* | ||
* <p>The callback method {@link #timerExpired} is not called under any lock, which makes it | ||
* possible for {@link #stop} or {@link #resetAndStart} to proceed in the middle of {@link | ||
* #timerExpired}. In some cases, you may want to use {@link TimerState} to decide whether the run | ||
* should proceed. | ||
* | ||
* <p>For example, suppose we want to bookkeep the idleness of a system. If {@code onActive()} has | ||
* not been called for TIMEOUT, the system goes to idle state. Here is a <strong>seemingly</strong> | ||
* correct implementation: | ||
* | ||
* <p><pre> | ||
* boolean idle; | ||
* | ||
* ResettableTimer idleTimer = new ResettableTimer(TIMEOUT, ...) { | ||
* void timerExpired(TimerState state) { | ||
* synchronized (mylock) { | ||
* idle = true; | ||
* } | ||
* } | ||
* }; | ||
* | ||
* void onActive() { | ||
* synchronized (mylock) { | ||
* idle = false; | ||
* idleTimer.resetAndStart(); | ||
* } | ||
* } | ||
* </pre> | ||
* | ||
* <p>The pathological scenario is: | ||
* <ol> | ||
* <li>{@code timerExpired()} starts, but hasn't entered the synchronized block.</li> | ||
* <li>{@code onActive()} enters and exits synchronized block. Since {@code timerExpired} has | ||
* already started, it won't be stopped.</li> | ||
* <li>{@code timerExpired()} enters synchronized block, and set idle to true.</li> | ||
* </ol> | ||
* | ||
* <p>The end result is, the system is now in idle state despite that {@code onActive} has just been | ||
* called. | ||
* | ||
* <p>Following is the correct implementation. {@code resetAndStart()} will make {@code | ||
* state.isCancelled()} return {@code false} in the forementioned scenario, which stops {@code idle} | ||
* from being unexpectedlly set. | ||
* | ||
* <p><pre> | ||
* boolean idle; | ||
* | ||
* ResettableTimer idleTimer = new ResettableTimer(TIMEOUT, ...) { | ||
* void timerExpired(TimerState state) { | ||
* synchronized (mylock) { | ||
* if (state.isCancelled()) { | ||
* return; | ||
* } | ||
* idle = true; | ||
* } | ||
* } | ||
* }; | ||
* | ||
* void onActive() { | ||
* synchronized (mylock) { | ||
* idle = false; | ||
* idleTimer.resetAndStart(); // makes state.isCancelled() true if timerExpired() has started | ||
* } | ||
* } | ||
* </pre> | ||
*/ | ||
@ThreadSafe | ||
abstract class ResettableTimer { | ||
private final long timeoutNanos; | ||
private final ScheduledExecutorService executor; | ||
private final Stopwatch stopwatch; | ||
private final Object lock = new Object(); | ||
|
||
private class Task implements Runnable { | ||
final TimerState state = new TimerState(); | ||
ScheduledFuture<?> handle; | ||
|
||
@Override | ||
public void run() { | ||
synchronized (lock) { | ||
if (!stopwatch.isRunning()) { | ||
// stop() has been called | ||
currentTask = null; | ||
return; | ||
} | ||
long leftNanos = timeoutNanos - stopwatch.elapsed(TimeUnit.NANOSECONDS); | ||
if (leftNanos > 0) { | ||
currentTask = null; | ||
scheduleTask(leftNanos); | ||
return; | ||
} | ||
callbackRunning = true; | ||
} | ||
try { | ||
// We explicitly don't run the callback under the lock, so that the callback can have its | ||
// own synchronization and have the freedom to do something outside of any lock. | ||
timerExpired(state); | ||
} finally { | ||
synchronized (lock) { | ||
callbackRunning = false; | ||
currentTask = null; | ||
if (schedulePending) { | ||
schedulePending = false; | ||
scheduleTask(timeoutNanos); | ||
} | ||
} | ||
} | ||
} | ||
|
||
@Override | ||
public String toString() { | ||
return ResettableTimer.this.toString(); | ||
} | ||
} | ||
|
||
@GuardedBy("lock") | ||
@Nullable | ||
private Task currentTask; | ||
|
||
@GuardedBy("lock") | ||
private boolean callbackRunning; | ||
|
||
@GuardedBy("lock") | ||
private boolean schedulePending; | ||
|
||
@GuardedBy("lock") | ||
private boolean shutdown; | ||
|
||
protected ResettableTimer(long timeout, TimeUnit unit, ScheduledExecutorService executor, | ||
Stopwatch stopwatch) { | ||
this.timeoutNanos = unit.toNanos(timeout); | ||
this.executor = checkNotNull(executor); | ||
this.stopwatch = checkNotNull(stopwatch); | ||
} | ||
|
||
/** | ||
* Handler to run when timer expired. | ||
* | ||
* <p>Timer won't restart automatically. | ||
* | ||
* @param state gives the handler the chance to check whether the timer is cancelled in the middle | ||
* of the run | ||
|
||
*/ | ||
abstract void timerExpired(TimerState state); | ||
|
||
/** | ||
* Reset the timer and start it. {@link #timerExpired} will be run after timeout from now unless | ||
* {@link #resetAndStart} or {@link #stop} is called before that. | ||
*/ | ||
final void resetAndStart() { | ||
synchronized (lock) { | ||
checkState(!shutdown, "already shutdown"); | ||
stopwatch.reset().start(); | ||
if (currentTask == null) { | ||
scheduleTask(timeoutNanos); | ||
} else { | ||
if (callbackRunning) { | ||
currentTask.state.cancelled = true; | ||
// currentTask has not been cleared yet, will let Task.run() schedule the timer after it's | ||
// done. | ||
schedulePending = true; | ||
} | ||
} | ||
} | ||
} | ||
|
||
/** | ||
* Start the timer, if it has not started yet. | ||
* | ||
* @return {@code false} if the timer has already started | ||
*/ | ||
final boolean start() { | ||
synchronized (lock) { | ||
checkState(!shutdown, "already shutdown"); | ||
if (currentTask == null) { | ||
resetAndStart(); | ||
return true; | ||
} else { | ||
return false; | ||
} | ||
} | ||
} | ||
|
||
/** | ||
* Stop the timer. | ||
*/ | ||
final void stop() { | ||
synchronized (lock) { | ||
if (currentTask != null) { | ||
if (stopwatch.isRunning()) { | ||
stopwatch.stop(); | ||
} | ||
if (callbackRunning) { | ||
currentTask.state.cancelled = true; | ||
} | ||
} | ||
} | ||
} | ||
|
||
/** | ||
* Shutdown this timer permanently. {@link #resetAndStart} won't be allowed after this. | ||
*/ | ||
final void shutdown() { | ||
synchronized (lock) { | ||
if (shutdown) { | ||
return; | ||
} | ||
shutdown = true; | ||
if (currentTask != null) { | ||
currentTask.handle.cancel(false); | ||
stop(); | ||
} | ||
} | ||
} | ||
|
||
@GuardedBy("lock") | ||
private void scheduleTask(long nanos) { | ||
checkState(currentTask == null, "task already scheduled or running"); | ||
currentTask = new Task(); | ||
currentTask.handle = executor.schedule( | ||
new LogExceptionRunnable(currentTask), nanos, TimeUnit.NANOSECONDS); | ||
} | ||
|
||
/** | ||
* Holds the most up-to-date states of the timer. | ||
*/ | ||
final class TimerState { | ||
@GuardedBy("lock") | ||
private boolean cancelled; | ||
|
||
/** | ||
* Returns {@code true} if the current run is cancelled (either by {@link #stop} or {@link | ||
* #resetAndStart}). | ||
*/ | ||
boolean isCancelled() { | ||
synchronized (lock) { | ||
return cancelled; | ||
} | ||
} | ||
} | ||
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
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.
Why not just make this a method on
ResettableTimer
?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.
Oh. I guess I see, since it is reused. I'll look deeper.