Skip to content

Commit bac6bde

Browse files
qwwdfsadelizarov
authored andcommitted
Provide BlockingChecker mechanism which checks current context
Fixes #227
1 parent fb4a332 commit bac6bde

File tree

13 files changed

+135
-33
lines changed

13 files changed

+135
-33
lines changed

binary-compatibility-validator/reference-public-api/kotlinx-coroutines-android.txt

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,3 +21,8 @@ public final class kotlinx/coroutines/experimental/android/HandlerContextKt {
2121
public static final fun getUI ()Lkotlinx/coroutines/experimental/android/HandlerContext;
2222
}
2323

24+
public final class kotlinx/coroutines/experimental/android/MainLooperChecker : kotlinx/coroutines/experimental/BlockingChecker {
25+
public fun <init> ()V
26+
public fun runBlockingAllowed ()Z
27+
}
28+

binary-compatibility-validator/reference-public-api/kotlinx-coroutines-core.txt

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,10 @@ public final class kotlinx/coroutines/experimental/AwaitKt {
2222
public static final fun joinAll ([Lkotlinx/coroutines/experimental/Job;Lkotlin/coroutines/experimental/Continuation;)Ljava/lang/Object;
2323
}
2424

25+
public abstract interface class kotlinx/coroutines/experimental/BlockingChecker {
26+
public abstract fun runBlockingAllowed ()Z
27+
}
28+
2529
public final class kotlinx/coroutines/experimental/BuildersKt {
2630
public static final synthetic fun launch (Lkotlin/coroutines/experimental/CoroutineContext;Lkotlinx/coroutines/experimental/CoroutineStart;Lkotlin/jvm/functions/Function2;)Lkotlinx/coroutines/experimental/Job;
2731
public static final fun launch (Lkotlin/coroutines/experimental/CoroutineContext;Lkotlinx/coroutines/experimental/CoroutineStart;Lkotlinx/coroutines/experimental/Job;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function2;)Lkotlinx/coroutines/experimental/Job;

binary-compatibility-validator/reference-public-api/kotlinx-coroutines-javafx.txt

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,8 @@
1+
public final class kotlinx/coroutines/experimental/javafx/ApplicationThreadChecker : kotlinx/coroutines/experimental/BlockingChecker {
2+
public fun <init> ()V
3+
public fun runBlockingAllowed ()Z
4+
}
5+
16
public final class kotlinx/coroutines/experimental/javafx/JavaFx : kotlinx/coroutines/experimental/CoroutineDispatcher, kotlinx/coroutines/experimental/Delay {
27
public static final field INSTANCE Lkotlinx/coroutines/experimental/javafx/JavaFx;
38
public final fun awaitPulse (Lkotlin/coroutines/experimental/Continuation;)Ljava/lang/Object;

binary-compatibility-validator/reference-public-api/kotlinx-coroutines-swing.txt

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,8 @@
1+
public final class kotlinx/coroutines/experimental/swing/EventDispatchThreadChecker : kotlinx/coroutines/experimental/BlockingChecker {
2+
public fun <init> ()V
3+
public fun runBlockingAllowed ()Z
4+
}
5+
16
public final class kotlinx/coroutines/experimental/swing/Swing : kotlinx/coroutines/experimental/CoroutineDispatcher, kotlinx/coroutines/experimental/Delay {
27
public static final field INSTANCE Lkotlinx/coroutines/experimental/swing/Swing;
38
public fun delay (JLjava/util/concurrent/TimeUnit;Lkotlin/coroutines/experimental/Continuation;)Ljava/lang/Object;

core/kotlinx-coroutines-core/src/Builders.kt

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77

88
package kotlinx.coroutines.experimental
99

10+
import java.util.*
1011
import java.util.concurrent.locks.*
1112
import kotlin.coroutines.experimental.*
1213

@@ -29,12 +30,19 @@ import kotlin.coroutines.experimental.*
2930
*
3031
* See [newCoroutineContext] for a description of debugging facilities that are available for newly created coroutine.
3132
*
33+
* @throws IllegalStateException if blocking is not allowed in current thread.
34+
* Blocking is checked by [BlockingChecker] registered in [ServiceLoader]
3235
* @param context context of the coroutine. The default value is an implementation of [EventLoop].
3336
* @param block the coroutine code.
3437
*/
3538
@Throws(InterruptedException::class)
3639
public fun <T> runBlocking(context: CoroutineContext = EmptyCoroutineContext, block: suspend CoroutineScope.() -> T): T {
3740
val currentThread = Thread.currentThread()
41+
42+
if (blockingChecker?.runBlockingAllowed() == false) {
43+
throw IllegalStateException("runBlocking is forbidden in $currentThread")
44+
}
45+
3846
val contextInterceptor = context[ContinuationInterceptor]
3947
val privateEventLoop = contextInterceptor == null // create private event loop if no dispatcher is specified
4048
val eventLoop = if (privateEventLoop) BlockingEventLoop(currentThread) else contextInterceptor as? EventLoop
@@ -46,6 +54,36 @@ public fun <T> runBlocking(context: CoroutineContext = EmptyCoroutineContext, bl
4654
return coroutine.joinBlocking()
4755
}
4856

57+
/**
58+
* Element of classpath which determines whether invoking [runBlocking] in current thread is allowed.
59+
* [runBlocking] discovers all checkers via [ServiceLoader]
60+
*
61+
* Common example is checking whether we're not in UI thread:
62+
* ```
63+
* class UiFilter : BlockingFilter {
64+
* fun runBlockingAllowed(): Boolean = !UiFramework.isInUiThread()
65+
* }
66+
* ```
67+
*/
68+
interface BlockingChecker {
69+
70+
/**
71+
* @return whether [runBlocking] calls are allowed in current thread
72+
*/
73+
fun runBlockingAllowed(): Boolean
74+
}
75+
76+
// Nullable to enable DCE when no filters are present in classpath
77+
private val blockingChecker: BlockingChecker? = loadBlockingCheckers()
78+
79+
private fun loadBlockingCheckers(): BlockingChecker? {
80+
val filters = ServiceLoader.load(BlockingChecker::class.java).toList().toTypedArray()
81+
if (filters.isEmpty()) return null
82+
return object : BlockingChecker {
83+
override fun runBlockingAllowed(): Boolean = filters.all { it.runBlockingAllowed() }
84+
}
85+
}
86+
4987
private class BlockingCoroutine<T>(
5088
parentContext: CoroutineContext,
5189
private val blockedThread: Thread,
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
kotlinx.coroutines.experimental.android.MainLooperChecker

ui/kotlinx-coroutines-android/src/HandlerContext.kt

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -4,12 +4,11 @@
44

55
package kotlinx.coroutines.experimental.android
66

7-
import android.os.Handler
8-
import android.os.Looper
9-
import android.view.Choreographer
7+
import android.os.*
8+
import android.view.*
109
import kotlinx.coroutines.experimental.*
11-
import java.util.concurrent.TimeUnit
12-
import kotlin.coroutines.experimental.CoroutineContext
10+
import java.util.concurrent.*
11+
import kotlin.coroutines.experimental.*
1312

1413
/**
1514
* Dispatches execution onto Android main UI thread and provides native [delay][Delay.delay] support.
@@ -89,3 +88,7 @@ public class HandlerContext(
8988
override fun equals(other: Any?): Boolean = other is HandlerContext && other.handler === handler
9089
override fun hashCode(): Int = System.identityHashCode(handler)
9190
}
91+
92+
class MainLooperChecker : BlockingChecker {
93+
override fun runBlockingAllowed(): Boolean = Looper.myLooper() != Looper.getMainLooper()
94+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
kotlinx.coroutines.experimental.javafx.ApplicationThreadChecker

ui/kotlinx-coroutines-javafx/src/JavaFx.kt

Lines changed: 13 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -4,19 +4,15 @@
44

55
package kotlinx.coroutines.experimental.javafx
66

7-
import com.sun.javafx.application.PlatformImpl
8-
import javafx.animation.AnimationTimer
9-
import javafx.animation.KeyFrame
10-
import javafx.animation.Timeline
11-
import javafx.application.Platform
12-
import javafx.event.ActionEvent
13-
import javafx.event.EventHandler
14-
import javafx.util.Duration
7+
import com.sun.javafx.application.*
8+
import javafx.animation.*
9+
import javafx.application.*
10+
import javafx.event.*
11+
import javafx.util.*
1512
import kotlinx.coroutines.experimental.*
1613
import kotlinx.coroutines.experimental.javafx.JavaFx.delay
17-
import java.util.concurrent.CopyOnWriteArrayList
18-
import java.util.concurrent.TimeUnit
19-
import kotlin.coroutines.experimental.CoroutineContext
14+
import java.util.concurrent.*
15+
import kotlin.coroutines.experimental.*
2016

2117
/**
2218
* Dispatches execution onto JavaFx application thread and provides native [delay] support.
@@ -43,14 +39,14 @@ object JavaFx : CoroutineDispatcher(), Delay {
4339
}
4440

4541
override fun scheduleResumeAfterDelay(time: Long, unit: TimeUnit, continuation: CancellableContinuation<Unit>) {
46-
val timeline = schedule(time, unit, EventHandler<ActionEvent> {
42+
val timeline = schedule(time, unit, EventHandler {
4743
with(continuation) { resumeUndispatched(Unit) }
4844
})
4945
continuation.invokeOnCancellation { timeline.stop() }
5046
}
5147

5248
override fun invokeOnTimeout(time: Long, unit: TimeUnit, block: Runnable): DisposableHandle {
53-
val timeline = schedule(time, unit, EventHandler<ActionEvent> {
49+
val timeline = schedule(time, unit, EventHandler {
5450
block.run()
5551
})
5652
return object : DisposableHandle {
@@ -81,6 +77,10 @@ object JavaFx : CoroutineDispatcher(), Delay {
8177
override fun toString() = "JavaFx"
8278
}
8379

80+
class ApplicationThreadChecker : BlockingChecker {
81+
override fun runBlockingAllowed(): Boolean = !Platform.isFxApplicationThread()
82+
}
83+
8484
internal fun initPlatform() {
8585
PlatformImpl.startup {}
8686
}

ui/kotlinx-coroutines-javafx/test/JavaFxTest.kt

Lines changed: 26 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,10 +4,11 @@
44

55
package kotlinx.coroutines.experimental.javafx
66

7-
import javafx.application.Platform
7+
import javafx.application.*
88
import kotlinx.coroutines.experimental.*
9-
import org.junit.Before
9+
import org.junit.*
1010
import org.junit.Test
11+
import kotlin.test.*
1112

1213
class JavaFxTest : TestBase() {
1314
@Before
@@ -37,4 +38,26 @@ class JavaFxTest : TestBase() {
3738
finish(4)
3839
}
3940
}
40-
}
41+
42+
@Test
43+
fun testRunBlockingForbidden() {
44+
try {
45+
initPlatform()
46+
} catch (e: UnsupportedOperationException) {
47+
println("Skipping JavaFxTest in headless environment")
48+
return // ignore test in headless environments
49+
}
50+
51+
runBlocking(JavaFx) {
52+
expect(1)
53+
try {
54+
runBlocking(JavaFx) {
55+
expectUnreached()
56+
}
57+
} catch (e: Exception) {
58+
assertTrue(e.message!!.contains("runBlocking"))
59+
finish(2)
60+
}
61+
}
62+
}
63+
}

0 commit comments

Comments
 (0)