blob: c522380f09432c9a67ede67ef52460f9c033e3a5 [file] [log] [blame]
/*
* Copyright (C) 2014 Square, 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 okio
import java.io.IOException
import java.io.InterruptedIOException
import java.util.concurrent.TimeUnit
actual open class Timeout {
/**
* True if `deadlineNanoTime` is defined. There is no equivalent to null or 0 for
* [System.nanoTime].
*/
private var hasDeadline = false
private var deadlineNanoTime = 0L
private var timeoutNanos = 0L
/**
* Wait at most `timeout` time before aborting an operation. Using a per-operation timeout means
* that as long as forward progress is being made, no sequence of operations will fail.
*
* If `timeout == 0`, operations will run indefinitely. (Operating system timeouts may still
* apply.)
*/
open fun timeout(timeout: Long, unit: TimeUnit): Timeout {
require(timeout >= 0) { "timeout < 0: $timeout" }
timeoutNanos = unit.toNanos(timeout)
return this
}
/** Returns the timeout in nanoseconds, or `0` for no timeout. */
open fun timeoutNanos(): Long = timeoutNanos
/** Returns true if a deadline is enabled. */
open fun hasDeadline(): Boolean = hasDeadline
/**
* Returns the [nano time][System.nanoTime] when the deadline will be reached.
*
* @throws IllegalStateException if no deadline is set.
*/
open fun deadlineNanoTime(): Long {
check(hasDeadline) { "No deadline" }
return deadlineNanoTime
}
/**
* Sets the [nano time][System.nanoTime] when the deadline will be reached. All operations must
* complete before this time. Use a deadline to set a maximum bound on the time spent on a
* sequence of operations.
*/
open fun deadlineNanoTime(deadlineNanoTime: Long): Timeout {
this.hasDeadline = true
this.deadlineNanoTime = deadlineNanoTime
return this
}
/** Set a deadline of now plus `duration` time. */
fun deadline(duration: Long, unit: TimeUnit): Timeout {
require(duration > 0) { "duration <= 0: $duration" }
return deadlineNanoTime(System.nanoTime() + unit.toNanos(duration))
}
/** Clears the timeout. Operating system timeouts may still apply. */
open fun clearTimeout(): Timeout {
timeoutNanos = 0
return this
}
/** Clears the deadline. */
open fun clearDeadline(): Timeout {
hasDeadline = false
return this
}
/**
* Throws an [InterruptedIOException] if the deadline has been reached or if the current thread
* has been interrupted. This method doesn't detect timeouts; that should be implemented to
* asynchronously abort an in-progress operation.
*/
@Throws(IOException::class)
open fun throwIfReached() {
if (Thread.currentThread().isInterrupted) {
// If the current thread has been interrupted.
throw InterruptedIOException("interrupted")
}
if (hasDeadline && deadlineNanoTime - System.nanoTime() <= 0) {
throw InterruptedIOException("deadline reached")
}
}
/**
* Waits on `monitor` until it is notified. Throws [InterruptedIOException] if either the thread
* is interrupted or if this timeout elapses before `monitor` is notified. The caller must be
* synchronized on `monitor`.
*
* Here's a sample class that uses `waitUntilNotified()` to await a specific state. Note that the
* call is made within a loop to avoid unnecessary waiting and to mitigate spurious notifications.
* ```
* class Dice {
* Random random = new Random();
* int latestTotal;
*
* public synchronized void roll() {
* latestTotal = 2 + random.nextInt(6) + random.nextInt(6);
* System.out.println("Rolled " + latestTotal);
* notifyAll();
* }
*
* public void rollAtFixedRate(int period, TimeUnit timeUnit) {
* Executors.newScheduledThreadPool(0).scheduleAtFixedRate(new Runnable() {
* public void run() {
* roll();
* }
* }, 0, period, timeUnit);
* }
*
* public synchronized void awaitTotal(Timeout timeout, int total)
* throws InterruptedIOException {
* while (latestTotal != total) {
* timeout.waitUntilNotified(this);
* }
* }
* }
* ```
*/
@Throws(InterruptedIOException::class)
fun waitUntilNotified(monitor: Any) {
try {
val hasDeadline = hasDeadline()
val timeoutNanos = timeoutNanos()
if (!hasDeadline && timeoutNanos == 0L) {
(monitor as Object).wait() // There is no timeout: wait forever.
return
}
// Compute how long we'll wait.
val start = System.nanoTime()
val waitNanos = if (hasDeadline && timeoutNanos != 0L) {
val deadlineNanos = deadlineNanoTime() - start
minOf(timeoutNanos, deadlineNanos)
} else if (hasDeadline) {
deadlineNanoTime() - start
} else {
timeoutNanos
}
// Attempt to wait that long. This will break out early if the monitor is notified.
var elapsedNanos = 0L
if (waitNanos > 0L) {
val waitMillis = waitNanos / 1000000L
(monitor as Object).wait(waitMillis, (waitNanos - waitMillis * 1000000L).toInt())
elapsedNanos = System.nanoTime() - start
}
// Throw if the timeout elapsed before the monitor was notified.
if (elapsedNanos >= waitNanos) {
throw InterruptedIOException("timeout")
}
} catch (e: InterruptedException) {
Thread.currentThread().interrupt() // Retain interrupted status.
throw InterruptedIOException("interrupted")
}
}
/**
* Applies the minimum intersection between this timeout and `other`, run `block`, then finally
* rollback this timeout's values.
*/
inline fun intersectWith(other: Timeout, block: () -> Unit) {
val originalTimeout = this.timeoutNanos()
this.timeout(minTimeout(other.timeoutNanos(), this.timeoutNanos()), TimeUnit.NANOSECONDS)
if (this.hasDeadline()) {
val originalDeadline = this.deadlineNanoTime()
if (other.hasDeadline()) {
this.deadlineNanoTime(Math.min(this.deadlineNanoTime(), other.deadlineNanoTime()))
}
try {
block()
} finally {
this.timeout(originalTimeout, TimeUnit.NANOSECONDS)
if (other.hasDeadline()) {
this.deadlineNanoTime(originalDeadline)
}
}
} else {
if (other.hasDeadline()) {
this.deadlineNanoTime(other.deadlineNanoTime())
}
try {
block()
} finally {
this.timeout(originalTimeout, TimeUnit.NANOSECONDS)
if (other.hasDeadline()) {
this.clearDeadline()
}
}
}
}
actual companion object {
@JvmField actual val NONE: Timeout = object : Timeout() {
override fun timeout(timeout: Long, unit: TimeUnit): Timeout = this
override fun deadlineNanoTime(deadlineNanoTime: Long): Timeout = this
override fun throwIfReached() {}
}
fun minTimeout(aNanos: Long, bNanos: Long) = when {
aNanos == 0L -> bNanos
bNanos == 0L -> aNanos
aNanos < bNanos -> aNanos
else -> bNanos
}
}
}