Skip to content

Commit ed7c668

Browse files
BlockHound integration (#1821)
* BlockHound integration for Dispatchers.Default and Dispatchers.IO * Use JNA-based self-attach mechanism in order to workaround a problem when tools.jar is loader by multiple classloaders * publication-validator is renamed to integration-testing; * Each test is now in a separate source set, which allows for more flexibility in their configuration; for example, failing to set `dryRun=true` doesn't prevent tests other than NPM to run, and it is possible to run the tests (and their dependencies) separately. * Add an integration test for coroutine debugger java agent Fixes #1060 Fixes #1031 Co-authored-by: Vsevolod Tolstopyatov <[email protected]>
1 parent e0a6533 commit ed7c668

21 files changed

+276
-65
lines changed

build.gradle

+2-2
Original file line numberDiff line numberDiff line change
@@ -8,8 +8,8 @@ apply from: rootProject.file("gradle/experimental.gradle")
88
def rootModule = "kotlinx.coroutines"
99
def coreModule = "kotlinx-coroutines-core"
1010
// Not applicable for Kotlin plugin
11-
def sourceless = ['kotlinx.coroutines', 'site', 'kotlinx-coroutines-bom', 'publication-validator']
12-
def internal = ['kotlinx.coroutines', 'site', 'benchmarks', 'js-stub', 'stdlib-stubs', 'publication-validator']
11+
def sourceless = ['kotlinx.coroutines', 'site', 'kotlinx-coroutines-bom', 'integration-testing']
12+
def internal = ['kotlinx.coroutines', 'site', 'benchmarks', 'js-stub', 'stdlib-stubs', 'integration-testing']
1313
// Not published
1414
def unpublished = internal + ['example-frontend-js', 'android-unit-tests']
1515

gradle.properties

+3-1
Original file line numberDiff line numberDiff line change
@@ -14,13 +14,15 @@ knit_version=0.1.3
1414
html_version=0.6.8
1515
lincheck_version=2.5.3
1616
dokka_version=0.9.16-rdev-2-mpp-hacks
17-
byte_buddy_version=1.9.3
17+
byte_buddy_version=1.10.7
1818
reactor_vesion=3.2.5.RELEASE
1919
reactive_streams_version=1.0.2
2020
rxjava2_version=2.2.8
2121
javafx_version=11.0.2
2222
javafx_plugin_version=0.0.8
2323
binary_compatibility_validator_version=0.2.2
24+
blockhound_version=1.0.2.RELEASE
25+
jna_version=5.5.0
2426

2527
# Android versions
2628
android_version=4.1.1.4

integration-testing/README.md

+14
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
# Integration tests
2+
3+
This is a supplementary subproject of kotlinx.coroutines that provides
4+
integration tests.
5+
6+
The tests are the following:
7+
* `NpmPublicationValidator` tests that version of NPM artifact is correct and that it has neither source nor package dependencies on atomicfu
8+
In order for the test to work, one needs to run gradle with `-PdryRun=true`.
9+
`-PdryRun` affects `npmPublish` so that it only provides a packed publication
10+
and does not in fact attempt to send the build for publication.
11+
* `MavenPublicationValidator` depends on the published artifacts and tests artifacts binary content and absence of atomicfu in the classpath
12+
* `DebugAgentTest` checks that the coroutine debugger can be run as a Java agent.
13+
14+
All the available tests can be run with `integration-testing:test`.

integration-testing/build.gradle

+77
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
/*
2+
* Copyright 2016-2020 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license.
3+
*/
4+
5+
apply from: rootProject.file("gradle/compile-jvm.gradle")
6+
7+
repositories {
8+
mavenLocal()
9+
mavenCentral()
10+
}
11+
12+
sourceSets {
13+
npmTest {
14+
kotlin
15+
compileClasspath += sourceSets.test.runtimeClasspath
16+
runtimeClasspath += sourceSets.test.runtimeClasspath
17+
}
18+
mavenTest {
19+
kotlin
20+
compileClasspath += sourceSets.test.runtimeClasspath
21+
runtimeClasspath += sourceSets.test.runtimeClasspath
22+
}
23+
debugAgentTest {
24+
kotlin
25+
compileClasspath += sourceSets.test.runtimeClasspath
26+
runtimeClasspath += sourceSets.test.runtimeClasspath
27+
}
28+
}
29+
30+
task npmTest(type: Test) {
31+
def sourceSet = sourceSets.npmTest
32+
environment "projectRoot", project.rootDir
33+
environment "deployVersion", version
34+
def dryRunNpm = project.properties['dryRun']
35+
def doRun = dryRunNpm == "true" // so that we don't accidentally publish anything, especially before the test
36+
onlyIf { doRun }
37+
if (doRun) { // `onlyIf` only affects execution of the task, not the dependency subtree
38+
dependsOn(project(':').getTasksByName("publishNpm", true))
39+
}
40+
testClassesDirs = sourceSet.output.classesDirs
41+
classpath = sourceSet.runtimeClasspath
42+
}
43+
44+
task mavenTest(type: Test) {
45+
def sourceSet = sourceSets.mavenTest
46+
dependsOn(project(':').getTasksByName("publishToMavenLocal", true))
47+
dependsOn.remove(project(':').getTasksByName("dokka", true))
48+
testClassesDirs = sourceSet.output.classesDirs
49+
classpath = sourceSet.runtimeClasspath
50+
}
51+
52+
task debugAgentTest(type: Test) {
53+
def sourceSet = sourceSets.debugAgentTest
54+
dependsOn(project(':kotlinx-coroutines-debug').shadowJar)
55+
jvmArgs ('-javaagent:' + project(':kotlinx-coroutines-debug').shadowJar.outputs.files.getFiles()[0])
56+
testClassesDirs = sourceSet.output.classesDirs
57+
classpath = sourceSet.runtimeClasspath
58+
}
59+
60+
dependencies {
61+
testCompile "org.jetbrains.kotlin:kotlin-stdlib-jdk8"
62+
testCompile 'junit:junit:4.12'
63+
npmTestCompile 'org.apache.commons:commons-compress:1.18'
64+
npmTestCompile 'com.google.code.gson:gson:2.8.5'
65+
mavenTestRuntimeOnly project(':kotlinx-coroutines-core')
66+
mavenTestRuntimeOnly project(':kotlinx-coroutines-android')
67+
debugAgentTestCompile project(':kotlinx-coroutines-core')
68+
debugAgentTestCompile project(':kotlinx-coroutines-debug')
69+
}
70+
71+
compileTestKotlin {
72+
kotlinOptions.jvmTarget = "1.8"
73+
}
74+
75+
test {
76+
dependsOn([npmTest, mavenTest, debugAgentTest])
77+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
/*
2+
* Copyright 2016-2020 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license.
3+
*/
4+
import org.junit.*
5+
import kotlinx.coroutines.*
6+
import kotlinx.coroutines.debug.*
7+
import java.io.*
8+
9+
class DebugAgentTest {
10+
11+
@Test
12+
fun agentDumpsCoroutines() = runBlocking {
13+
val baos = ByteArrayOutputStream()
14+
DebugProbes.dumpCoroutines(PrintStream(baos))
15+
// if the agent works, then dumps should contain something,
16+
// at least the fact that this test is running.
17+
Assert.assertTrue(baos.toString().contains("agentDumpsCoroutines"))
18+
}
19+
20+
}
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,6 @@ package kotlinx.coroutines.validator
66

77
import org.junit.*
88
import org.junit.Assert.assertTrue
9-
import java.io.*
109
import java.util.jar.*
1110

1211
class MavenPublicationValidator {

kotlinx-coroutines-core/jvm/src/scheduling/CoroutineScheduler.kt

+17
Original file line numberDiff line numberDiff line change
@@ -951,3 +951,20 @@ internal class CoroutineScheduler(
951951
TERMINATED
952952
}
953953
}
954+
955+
/**
956+
* Checks if the thread is part of a thread pool that supports coroutines.
957+
* This function is needed for integration with BlockHound.
958+
*/
959+
@Suppress("UNUSED")
960+
@JvmName("isSchedulerWorker")
961+
internal fun isSchedulerWorker(thread: Thread) = thread is CoroutineScheduler.Worker
962+
963+
/**
964+
* Checks if the thread is running a CPU-bound task.
965+
* This function is needed for integration with BlockHound.
966+
*/
967+
@Suppress("UNUSED")
968+
@JvmName("mayNotBlock")
969+
internal fun mayNotBlock(thread: Thread) = thread is CoroutineScheduler.Worker &&
970+
thread.state == CoroutineScheduler.WorkerState.CPU_ACQUIRED

kotlinx-coroutines-debug/README.md

+5
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,11 @@ suspension stacktraces.
1313
Additionally, it is possible to process the list of such coroutines via [DebugProbes.dumpCoroutinesInfo] or dump isolated parts
1414
of coroutines hierarchy referenced by a [Job] or [CoroutineScope] instances using [DebugProbes.printJob] and [DebugProbes.printScope] respectively.
1515

16+
This module also provides an automatic [BlockHound](https://github.com/reactor/BlockHound) integration
17+
that detects when a blocking operation was called in a coroutine context that prohibits it. In order to use it,
18+
please follow the BlockHound [quick start guide](
19+
https://github.com/reactor/BlockHound/blob/1.0.2.RELEASE/docs/quick_start.md).
20+
1621
### Using in your project
1722

1823
Add `kotlinx-coroutines-debug` to your project test dependencies:

kotlinx-coroutines-debug/api/kotlinx-coroutines-debug.api

+5
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,11 @@ public final class kotlinx/coroutines/debug/CoroutineInfo {
88
public fun toString ()Ljava/lang/String;
99
}
1010

11+
public final class kotlinx/coroutines/debug/CoroutinesBlockHoundIntegration : reactor/blockhound/integration/BlockHoundIntegration {
12+
public fun <init> ()V
13+
public fun applyTo (Lreactor/blockhound/BlockHound$Builder;)V
14+
}
15+
1116
public final class kotlinx/coroutines/debug/DebugProbes {
1217
public static final field INSTANCE Lkotlinx/coroutines/debug/DebugProbes;
1318
public final fun dumpCoroutines (Ljava/io/PrintStream;)V

kotlinx-coroutines-debug/build.gradle

+5-1
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,10 @@ dependencies {
2222
compileOnly "junit:junit:$junit_version"
2323
shadowDeps "net.bytebuddy:byte-buddy:$byte_buddy_version"
2424
shadowDeps "net.bytebuddy:byte-buddy-agent:$byte_buddy_version"
25+
compileOnly "io.projectreactor.tools:blockhound:$blockhound_version"
26+
testCompile "io.projectreactor.tools:blockhound:$blockhound_version"
27+
runtime "net.java.dev.jna:jna:$jna_version"
28+
runtime "net.java.dev.jna:jna-platform:$jna_version"
2529
}
2630

2731
jar {
@@ -35,5 +39,5 @@ shadowJar {
3539
classifier null
3640
// Shadow only byte buddy, do not package kotlin stdlib
3741
configurations = [project.configurations.shadowDeps]
38-
relocate 'net.bytebuddy', 'kotlinx.coroutines.repackaged.net.bytebuddy'
42+
relocate('net.bytebuddy', 'kotlinx.coroutines.repackaged.net.bytebuddy')
3943
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
kotlinx.coroutines.debug.CoroutinesBlockHoundIntegration
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
@file:Suppress("INVISIBLE_REFERENCE", "INVISIBLE_MEMBER")
2+
package kotlinx.coroutines.debug
3+
4+
import reactor.blockhound.BlockHound
5+
import kotlinx.coroutines.scheduling.*
6+
import reactor.blockhound.integration.*
7+
8+
@Suppress("UNUSED")
9+
public class CoroutinesBlockHoundIntegration: BlockHoundIntegration {
10+
11+
override fun applyTo(builder: BlockHound.Builder) {
12+
builder.addDynamicThreadPredicate { isSchedulerWorker(it) }
13+
builder.nonBlockingThreadPredicate { p -> p.or { mayNotBlock(it) } }
14+
}
15+
16+
}

kotlinx-coroutines-debug/src/internal/DebugProbesImpl.kt

+1-1
Original file line numberDiff line numberDiff line change
@@ -57,7 +57,7 @@ internal object DebugProbesImpl {
5757
public fun install(): Unit = coroutineStateLock.write {
5858
if (++installations > 1) return
5959

60-
ByteBuddyAgent.install()
60+
ByteBuddyAgent.install(ByteBuddyAgent.AttachmentProvider.ForEmulatedAttachment.INSTANCE)
6161
val cl = Class.forName("kotlin.coroutines.jvm.internal.DebugProbesKt")
6262
val cl2 = Class.forName("kotlinx.coroutines.debug.DebugProbesKt")
6363

Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
package kotlinx.coroutines.debug
2+
import kotlinx.coroutines.*
3+
import org.junit.*
4+
import reactor.blockhound.*
5+
6+
class BlockHoundTest : TestBase() {
7+
8+
@Before
9+
fun init() {
10+
BlockHound.install()
11+
}
12+
13+
@Test(expected = BlockingOperationError::class)
14+
fun shouldDetectBlockingInDefault() = runTest {
15+
withContext(Dispatchers.Default) {
16+
Thread.sleep(1)
17+
}
18+
}
19+
20+
@Test
21+
fun shouldNotDetectBlockingInIO() = runTest {
22+
withContext(Dispatchers.IO) {
23+
Thread.sleep(1)
24+
}
25+
}
26+
27+
@Test
28+
fun shouldNotDetectNonblocking() = runTest {
29+
withContext(Dispatchers.Default) {
30+
val a = 1
31+
val b = 2
32+
assert(a + b == 3)
33+
}
34+
}
35+
36+
@Test
37+
fun testReusingThreads() = runTest {
38+
val n = 100
39+
repeat(n) {
40+
async(Dispatchers.IO) {
41+
Thread.sleep(1)
42+
}
43+
}
44+
repeat(n) {
45+
async(Dispatchers.Default) {
46+
}
47+
}
48+
repeat(n) {
49+
async(Dispatchers.IO) {
50+
Thread.sleep(1)
51+
}
52+
}
53+
}
54+
55+
@Test(expected = BlockingOperationError::class)
56+
fun testReusingThreadsFailure() = runTest {
57+
val n = 100
58+
repeat(n) {
59+
async(Dispatchers.IO) {
60+
Thread.sleep(1)
61+
}
62+
}
63+
async(Dispatchers.Default) {
64+
Thread.sleep(1)
65+
}
66+
repeat(n) {
67+
async(Dispatchers.IO) {
68+
Thread.sleep(1)
69+
}
70+
}
71+
}
72+
73+
}

kotlinx-coroutines-debug/test/CoroutinesDumpTest.kt

+8-5
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,7 @@ class CoroutinesDumpTest : DebugTestBase() {
3939

4040
@Test
4141
fun testRunningCoroutine() = runBlocking {
42-
val deferred = async(Dispatchers.Default) {
42+
val deferred = async(Dispatchers.IO) {
4343
activeMethod(shouldSuspend = false)
4444
assertTrue(true)
4545
}
@@ -70,7 +70,7 @@ class CoroutinesDumpTest : DebugTestBase() {
7070

7171
@Test
7272
fun testRunningCoroutineWithSuspensionPoint() = runBlocking {
73-
val deferred = async(Dispatchers.Default) {
73+
val deferred = async(Dispatchers.IO) {
7474
activeMethod(shouldSuspend = true)
7575
yield() // tail-call
7676
}
@@ -100,7 +100,7 @@ class CoroutinesDumpTest : DebugTestBase() {
100100

101101
@Test
102102
fun testCreationStackTrace() = runBlocking {
103-
val deferred = async(Dispatchers.Default) {
103+
val deferred = async(Dispatchers.IO) {
104104
activeMethod(shouldSuspend = true)
105105
}
106106

@@ -129,7 +129,7 @@ class CoroutinesDumpTest : DebugTestBase() {
129129

130130
@Test
131131
fun testFinishedCoroutineRemoved() = runBlocking {
132-
val deferred = async(Dispatchers.Default) {
132+
val deferred = async(Dispatchers.IO) {
133133
activeMethod(shouldSuspend = true)
134134
}
135135

@@ -149,7 +149,10 @@ class CoroutinesDumpTest : DebugTestBase() {
149149
if (shouldSuspend) yield()
150150
notifyCoroutineStarted()
151151
while (coroutineContext[Job]!!.isActive) {
152-
runCatching { Thread.sleep(60_000) }
152+
try {
153+
Thread.sleep(60_000)
154+
} catch (_ : InterruptedException) {
155+
}
153156
}
154157
}
155158

kotlinx-coroutines-debug/test/RunningThreadStackMergeTest.kt

+1-1
Original file line numberDiff line numberDiff line change
@@ -133,7 +133,7 @@ class RunningThreadStackMergeTest : DebugTestBase() {
133133
}
134134

135135
private fun CoroutineScope.launchEscapingCoroutineWithoutContext() {
136-
launch(Dispatchers.Default) {
136+
launch(Dispatchers.IO) {
137137
suspendingFunctionWithoutContext()
138138
assertTrue(true)
139139
}

0 commit comments

Comments
 (0)