Skip to content

Instantly share code, notes, and snippets.

@elizarov
Created November 8, 2019 08:41
Show Gist options
  • Star 3 You must be signed in to star a gist
  • Fork 1 You must be signed in to fork a gist
  • Save elizarov/c5b0fde43ca14efbb8bcab13ad43c6ca to your computer and use it in GitHub Desktop.
Save elizarov/c5b0fde43ca14efbb8bcab13ad43c6ca to your computer and use it in GitHub Desktop.
A version of withTimeoutOrNull that can be paused/resumed.
import kotlinx.coroutines.*
import kotlin.coroutines.*
/**
* Scope interface to control (pause & resume) timeout
*/
interface TimerScope : CoroutineScope {
fun pause()
fun resume()
}
/**
* A version of [withTimeoutOrNull] that supports [TimerScope] and can be paused/resumed.
*/
suspend fun <T> pauseableWithTimeoutOrNull(timeMillis: Long, block: suspend TimerScope.() -> T): T? {
// Create a timer instance in advance, so that we can check if PauseableTimeoutCancellationException coming from it
val timer = TimerScopeImpl(timeMillis)
try {
// Run the code in a separate scope that we can cancel on timeout
return coroutineScope {
timer.context = coroutineContext
timer.resume()
try {
timer.block()
} finally {
timer.pause()
}
}
} catch (e : PauseableTimeoutCancellationException) {
if (e.timer === timer) return null
throw e
}
}
// -------- private implementation details --------
private class PauseableTimeoutCancellationException(val timer: TimerScope) : CancellationException()
private class TimerScopeImpl(
private var timeout: Long
) : TimerScope {
lateinit var context: CoroutineContext
private var previousTime = 0L
private var cancellationJob: Job? = null // != null when running
override val coroutineContext: CoroutineContext
get() = context
override fun pause() {
cancellationJob?.apply { // do only when running
cancel() // cancel cancellation job
cancellationJob = null
val currentTime = System.currentTimeMillis()
timeout -= currentTime - previousTime
previousTime = currentTime
}
}
override fun resume() {
if (cancellationJob != null) return // bail out if already running
previousTime = System.currentTimeMillis()
cancellationJob = GlobalScope.launch(coroutineContext) {
delay(timeout)
// cancel top-level context on timeout
context.cancel(PauseableTimeoutCancellationException(this@TimerScopeImpl))
}
}
}
// -------- a simple demo/test --------
fun main() = runBlocking<Unit> {
pauseableWithTimeoutOrNull(1000) {
println("1. Before")
delay(500)
println("1. After 500ms")
"1. Done"
}.let(::println)
pauseableWithTimeoutOrNull(1000) {
println("2. Before")
delay(1500)
println("2. After 1500ms")
"2. Done"
}.let(::println)
pauseableWithTimeoutOrNull(1000) {
println("3. Before")
delay(500)
println("3. Pause timer after 500ms")
pause()
delay(1000)
println("3. Resume timer after 1000ms")
resume()
delay(1000)
println("3. After 1000ms more")
"3. Done"
}.let(::println)
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment