blob: 2948e70d90c718ab06d7ba636183a64cc0ec9b6f [file] [log] [blame]
/*
* Copyright (C) 2022 The Android Open Source Project
*
* 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 com.android.adblib.utils
import kotlinx.coroutines.CancellationException
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.CoroutineStart
import kotlinx.coroutines.InternalCoroutinesApi
import kotlinx.coroutines.Job
import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.cancel
import kotlinx.coroutines.currentCoroutineContext
import kotlinx.coroutines.job
import kotlinx.coroutines.launch
import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.withLock
import kotlinx.coroutines.withContext
import kotlin.coroutines.CoroutineContext
import kotlin.coroutines.EmptyCoroutineContext
/**
* Same as [launch], except cancellation from the child coroutine [block] is propagated to the
* parent coroutine (scope).
*
* The behavior is the same as [launch] wrt the following aspects:
* * The parent coroutine won't complete until the child coroutine [block] completes.
* * The parent coroutine fails with an exception if the child coroutine [block] throws
* an exception.
*/
inline fun CoroutineScope.launchCancellable(
context: CoroutineContext = EmptyCoroutineContext,
start: CoroutineStart = CoroutineStart.DEFAULT,
crossinline block: suspend CoroutineScope.() -> Unit
): Job {
return launch(context, start) {
try {
block()
} catch (e: CancellationException) {
// Note: this is a no-op is the parent scope is already cancelled
this@launchCancellable.cancel(e)
throw e
}
}
}
/**
* Creates a child [CoroutineScope] of this scope.
*
* @param isSupervisor whether to use a regular [Job] or a [SupervisorJob]
* @param context [CoroutineContext] to apply in addition to the parent scope [CoroutineContext]
*/
fun CoroutineScope.createChildScope(
isSupervisor: Boolean = false,
context: CoroutineContext = EmptyCoroutineContext
): CoroutineScope {
val newJob = if (isSupervisor) {
SupervisorJob(this.coroutineContext.job)
} else {
Job(this.coroutineContext.job)
}
return CoroutineScope(this.coroutineContext + newJob + context)
}
/**
* Runs [block] as a regular `suspend` function, except that it gets cancelled when [otherScope]
* is cancelled.
*/
suspend inline fun <R> runAlongOtherScope(
otherScope: CoroutineScope,
crossinline block: suspend () -> R
): R {
// Attach a completion handler that cancels this coroutine when "otherScope" is cancelled
// The completion handler is removed as soon as the execution of `block` ends, so that
// we don't cancel the caller at some point later in the execution path.
val currentJob = currentCoroutineContext().job
// Note: As of April 2024, the `invokeOnCompletion` overload that take `onCancelling` parameter
// is marked as internal API. The other overload is fully public and supported, but uses the
// `onCancelling = false` semantics, which delays the completion handler invocation, which
// may lead to deadlocks if using a "single thread" Dispatcher, i.e. `block` may not be
// cancelled right when `outerScope` is cancelled, leading to `block` still executing and
// using a Dispatcher slot.
@OptIn(InternalCoroutinesApi::class)
val handler = otherScope.coroutineContext.job.invokeOnCompletion(onCancelling = true) { throwable ->
when (throwable) {
is CancellationException -> {
currentJob.cancel(throwable)
}
null -> {
/* Nothing to do */
}
else -> {
currentJob.cancel(CancellationException(throwable.message, throwable))
}
}
}
return try {
block()
} finally {
handler.dispose()
}
}
/**
* Re-entrant version of [Mutex.lock]
*
* See [Phantom of the Coroutine](https://elizarov.medium.com/phantom-of-the-coroutine-afc63b03a131)
* See [Reentrant lock #1686](https://github.com/Kotlin/kotlinx.coroutines/issues/1686#issuecomment-777357672)
* See [ReentrantMutex implementation for Kotlin Coroutines](https://gist.github.com/elizarov/9a48b9709ffd508909d34fab6786acfe)
*/
suspend fun <T> Mutex.withReentrantLock(block: suspend () -> T): T {
val key = ReentrantMutexContextKey(this)
// call block directly when this mutex is already locked in the context
if (currentCoroutineContext()[key] != null) return block()
// otherwise add it to the context and lock the mutex
return withContext(ReentrantMutexContextElement(key)) {
withLock(null) { block() }
}
}
private class ReentrantMutexContextElement(
override val key: ReentrantMutexContextKey
) : CoroutineContext.Element
private data class ReentrantMutexContextKey(
val mutex: Mutex
) : CoroutineContext.Key<ReentrantMutexContextElement>