Skip to content

Commit

Permalink
BlockHound integration (Kotlin#1873)
Browse files Browse the repository at this point in the history
* Integration with BlockHound
* Improve build configuration of integration tests
* publication-validator is renamed to integration-testing;
* Add an integration test for coroutine debugger java agent
* Use JNA-based attach mechanism for dynamic attach

Fixes Kotlin#1821 
Fixes Kotlin#1060

Co-authored-by: Vsevolod Tolstopyatov <qwwdfsad@gmail.com>
Co-authored-by: Sergei Egorov <bsideup@gmail.com>
  • Loading branch information
3 people authored and recheej committed Dec 28, 2020
1 parent 87261a3 commit e127d69
Show file tree
Hide file tree
Showing 21 changed files with 280 additions and 73 deletions.
4 changes: 2 additions & 2 deletions build.gradle
Expand Up @@ -8,8 +8,8 @@ apply from: rootProject.file("gradle/experimental.gradle")
def rootModule = "kotlinx.coroutines"
def coreModule = "kotlinx-coroutines-core"
// Not applicable for Kotlin plugin
def sourceless = ['kotlinx.coroutines', 'site', 'kotlinx-coroutines-bom', 'publication-validator']
def internal = ['kotlinx.coroutines', 'site', 'benchmarks', 'js-stub', 'stdlib-stubs', 'publication-validator']
def sourceless = ['kotlinx.coroutines', 'site', 'kotlinx-coroutines-bom', 'integration-testing']
def internal = ['kotlinx.coroutines', 'site', 'benchmarks', 'js-stub', 'stdlib-stubs', 'integration-testing']
// Not published
def unpublished = internal + ['example-frontend-js', 'android-unit-tests']

Expand Down
4 changes: 3 additions & 1 deletion gradle.properties
Expand Up @@ -14,13 +14,15 @@ knit_version=0.1.3
html_version=0.6.8
lincheck_version=2.5.3
dokka_version=0.9.16-rdev-2-mpp-hacks
byte_buddy_version=1.9.3
byte_buddy_version=1.10.9
reactor_vesion=3.2.5.RELEASE
reactive_streams_version=1.0.2
rxjava2_version=2.2.8
javafx_version=11.0.2
javafx_plugin_version=0.0.8
binary_compatibility_validator_version=0.2.2
blockhound_version=1.0.2.RELEASE
jna_version=5.5.0

# Android versions
android_version=4.1.1.4
Expand Down
14 changes: 14 additions & 0 deletions integration-testing/README.md
@@ -0,0 +1,14 @@
# Integration tests

This is a supplementary subproject of kotlinx.coroutines that provides
integration tests.

The tests are the following:
* `NpmPublicationValidator` tests that version of NPM artifact is correct and that it has neither source nor package dependencies on atomicfu
In order for the test to work, one needs to run gradle with `-PdryRun=true`.
`-PdryRun` affects `npmPublish` so that it only provides a packed publication
and does not in fact attempt to send the build for publication.
* `MavenPublicationValidator` depends on the published artifacts and tests artifacts binary content and absence of atomicfu in the classpath
* `DebugAgentTest` checks that the coroutine debugger can be run as a Java agent.

All the available tests can be run with `integration-testing:test`.
80 changes: 80 additions & 0 deletions integration-testing/build.gradle
@@ -0,0 +1,80 @@
/*
* Copyright 2016-2020 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license.
*/

apply from: rootProject.file("gradle/compile-jvm.gradle")

repositories {
mavenLocal()
mavenCentral()
}

sourceSets {
npmTest {
kotlin
compileClasspath += sourceSets.test.runtimeClasspath
runtimeClasspath += sourceSets.test.runtimeClasspath
}
mavenTest {
kotlin
compileClasspath += sourceSets.test.runtimeClasspath
runtimeClasspath += sourceSets.test.runtimeClasspath
}
debugAgentTest {
kotlin
compileClasspath += sourceSets.test.runtimeClasspath
runtimeClasspath += sourceSets.test.runtimeClasspath
}
}

task npmTest(type: Test) {
def sourceSet = sourceSets.npmTest
environment "projectRoot", project.rootDir
environment "deployVersion", version
def dryRunNpm = project.properties['dryRun']
def doRun = dryRunNpm == "true" // so that we don't accidentally publish anything, especially before the test
onlyIf { doRun }
if (doRun) { // `onlyIf` only affects execution of the task, not the dependency subtree
dependsOn(project(':').getTasksByName("publishNpm", true))
}
testClassesDirs = sourceSet.output.classesDirs
classpath = sourceSet.runtimeClasspath
}

task mavenTest(type: Test) {
def sourceSet = sourceSets.mavenTest
dependsOn(project(':').getTasksByName("publishToMavenLocal", true))
dependsOn.remove(project(':').getTasksByName("dokka", true))
testClassesDirs = sourceSet.output.classesDirs
classpath = sourceSet.runtimeClasspath
// we can't depend on the subprojects because we need to test the classfiles that are published in the end.
// also, we can't put this in the `dependencies` block because the resolution would happen before publication.
classpath += project.configurations.detachedConfiguration(
project.dependencies.create("org.jetbrains.kotlinx:kotlinx-coroutines-core:$version"),
project.dependencies.create("org.jetbrains.kotlinx:kotlinx-coroutines-android:$version"))
}

task debugAgentTest(type: Test) {
def sourceSet = sourceSets.debugAgentTest
dependsOn(project(':kotlinx-coroutines-debug').shadowJar)
jvmArgs ('-javaagent:' + project(':kotlinx-coroutines-debug').shadowJar.outputs.files.getFiles()[0])
testClassesDirs = sourceSet.output.classesDirs
classpath = sourceSet.runtimeClasspath
}

dependencies {
testCompile "org.jetbrains.kotlin:kotlin-stdlib-jdk8"
testCompile 'junit:junit:4.12'
npmTestCompile 'org.apache.commons:commons-compress:1.18'
npmTestCompile 'com.google.code.gson:gson:2.8.5'
debugAgentTestCompile project(':kotlinx-coroutines-core')
debugAgentTestCompile project(':kotlinx-coroutines-debug')
}

compileTestKotlin {
kotlinOptions.jvmTarget = "1.8"
}

test {
dependsOn([npmTest, mavenTest, debugAgentTest])
}
20 changes: 20 additions & 0 deletions integration-testing/src/debugAgentTest/kotlin/DebugAgentTest.kt
@@ -0,0 +1,20 @@
/*
* Copyright 2016-2020 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license.
*/
import org.junit.*
import kotlinx.coroutines.*
import kotlinx.coroutines.debug.*
import java.io.*

class DebugAgentTest {

@Test
fun agentDumpsCoroutines() = runBlocking {
val baos = ByteArrayOutputStream()
DebugProbes.dumpCoroutines(PrintStream(baos))
// if the agent works, then dumps should contain something,
// at least the fact that this test is running.
Assert.assertTrue(baos.toString().contains("agentDumpsCoroutines"))
}

}
Expand Up @@ -6,7 +6,6 @@ package kotlinx.coroutines.validator

import org.junit.*
import org.junit.Assert.assertTrue
import java.io.*
import java.util.jar.*

class MavenPublicationValidator {
Expand Down
17 changes: 17 additions & 0 deletions kotlinx-coroutines-core/jvm/src/scheduling/CoroutineScheduler.kt
Expand Up @@ -951,3 +951,20 @@ internal class CoroutineScheduler(
TERMINATED
}
}

/**
* Checks if the thread is part of a thread pool that supports coroutines.
* This function is needed for integration with BlockHound.
*/
@Suppress("UNUSED")
@JvmName("isSchedulerWorker")
internal fun isSchedulerWorker(thread: Thread) = thread is CoroutineScheduler.Worker

/**
* Checks if the thread is running a CPU-bound task.
* This function is needed for integration with BlockHound.
*/
@Suppress("UNUSED")
@JvmName("mayNotBlock")
internal fun mayNotBlock(thread: Thread) = thread is CoroutineScheduler.Worker &&
thread.state == CoroutineScheduler.WorkerState.CPU_ACQUIRED
5 changes: 5 additions & 0 deletions kotlinx-coroutines-debug/README.md
Expand Up @@ -13,6 +13,11 @@ suspension stacktraces.
Additionally, it is possible to process the list of such coroutines via [DebugProbes.dumpCoroutinesInfo] or dump isolated parts
of coroutines hierarchy referenced by a [Job] or [CoroutineScope] instances using [DebugProbes.printJob] and [DebugProbes.printScope] respectively.

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

### Using in your project

Add `kotlinx-coroutines-debug` to your project test dependencies:
Expand Down
5 changes: 5 additions & 0 deletions kotlinx-coroutines-debug/api/kotlinx-coroutines-debug.api
Expand Up @@ -8,6 +8,11 @@ public final class kotlinx/coroutines/debug/CoroutineInfo {
public fun toString ()Ljava/lang/String;
}

public final class kotlinx/coroutines/debug/CoroutinesBlockHoundIntegration : reactor/blockhound/integration/BlockHoundIntegration {
public fun <init> ()V
public fun applyTo (Lreactor/blockhound/BlockHound$Builder;)V
}

public final class kotlinx/coroutines/debug/DebugProbes {
public static final field INSTANCE Lkotlinx/coroutines/debug/DebugProbes;
public final fun dumpCoroutines (Ljava/io/PrintStream;)V
Expand Down
8 changes: 6 additions & 2 deletions kotlinx-coroutines-debug/build.gradle
Expand Up @@ -14,14 +14,18 @@ configurations {
* but in that case it changes dependency type to "runtime" and resolves it
* (so it cannot be further modified). Otherwise, shadow just ignores all dependencies.
*/
shadow.extendsFrom(compile) // shadow - resulting configuration with shaded jar file
shadow.extendsFrom(api) // shadow - resulting configuration with shaded jar file
configureKotlinJvmPlatform(shadow)
}

dependencies {
compileOnly "junit:junit:$junit_version"
shadowDeps "net.bytebuddy:byte-buddy:$byte_buddy_version"
shadowDeps "net.bytebuddy:byte-buddy-agent:$byte_buddy_version"
compileOnly "io.projectreactor.tools:blockhound:$blockhound_version"
testCompile "io.projectreactor.tools:blockhound:$blockhound_version"
api "net.java.dev.jna:jna:$jna_version"
api "net.java.dev.jna:jna-platform:$jna_version"
}

jar {
Expand All @@ -35,5 +39,5 @@ shadowJar {
classifier null
// Shadow only byte buddy, do not package kotlin stdlib
configurations = [project.configurations.shadowDeps]
relocate 'net.bytebuddy', 'kotlinx.coroutines.repackaged.net.bytebuddy'
relocate('net.bytebuddy', 'kotlinx.coroutines.repackaged.net.bytebuddy')
}
@@ -0,0 +1 @@
kotlinx.coroutines.debug.CoroutinesBlockHoundIntegration
16 changes: 16 additions & 0 deletions kotlinx-coroutines-debug/src/CoroutinesBlockHoundIntegration.kt
@@ -0,0 +1,16 @@
@file:Suppress("INVISIBLE_REFERENCE", "INVISIBLE_MEMBER")
package kotlinx.coroutines.debug

import reactor.blockhound.BlockHound
import kotlinx.coroutines.scheduling.*
import reactor.blockhound.integration.*

@Suppress("UNUSED")
public class CoroutinesBlockHoundIntegration: BlockHoundIntegration {

override fun applyTo(builder: BlockHound.Builder) {
builder.addDynamicThreadPredicate { isSchedulerWorker(it) }
builder.nonBlockingThreadPredicate { p -> p.or { mayNotBlock(it) } }
}

}
2 changes: 1 addition & 1 deletion kotlinx-coroutines-debug/src/internal/DebugProbesImpl.kt
Expand Up @@ -57,7 +57,7 @@ internal object DebugProbesImpl {
public fun install(): Unit = coroutineStateLock.write {
if (++installations > 1) return

ByteBuddyAgent.install()
ByteBuddyAgent.install(ByteBuddyAgent.AttachmentProvider.ForEmulatedAttachment.INSTANCE)
val cl = Class.forName("kotlin.coroutines.jvm.internal.DebugProbesKt")
val cl2 = Class.forName("kotlinx.coroutines.debug.DebugProbesKt")

Expand Down
73 changes: 73 additions & 0 deletions kotlinx-coroutines-debug/test/BlockHoundTest.kt
@@ -0,0 +1,73 @@
package kotlinx.coroutines.debug
import kotlinx.coroutines.*
import org.junit.*
import reactor.blockhound.*

class BlockHoundTest : TestBase() {

@Before
fun init() {
BlockHound.install()
}

@Test(expected = BlockingOperationError::class)
fun shouldDetectBlockingInDefault() = runTest {
withContext(Dispatchers.Default) {
Thread.sleep(1)
}
}

@Test
fun shouldNotDetectBlockingInIO() = runTest {
withContext(Dispatchers.IO) {
Thread.sleep(1)
}
}

@Test
fun shouldNotDetectNonblocking() = runTest {
withContext(Dispatchers.Default) {
val a = 1
val b = 2
assert(a + b == 3)
}
}

@Test
fun testReusingThreads() = runTest {
val n = 100
repeat(n) {
async(Dispatchers.IO) {
Thread.sleep(1)
}
}
repeat(n) {
async(Dispatchers.Default) {
}
}
repeat(n) {
async(Dispatchers.IO) {
Thread.sleep(1)
}
}
}

@Test(expected = BlockingOperationError::class)
fun testReusingThreadsFailure() = runTest {
val n = 100
repeat(n) {
async(Dispatchers.IO) {
Thread.sleep(1)
}
}
async(Dispatchers.Default) {
Thread.sleep(1)
}
repeat(n) {
async(Dispatchers.IO) {
Thread.sleep(1)
}
}
}

}
13 changes: 8 additions & 5 deletions kotlinx-coroutines-debug/test/CoroutinesDumpTest.kt
Expand Up @@ -39,7 +39,7 @@ class CoroutinesDumpTest : DebugTestBase() {

@Test
fun testRunningCoroutine() = runBlocking {
val deferred = async(Dispatchers.Default) {
val deferred = async(Dispatchers.IO) {
activeMethod(shouldSuspend = false)
assertTrue(true)
}
Expand Down Expand Up @@ -70,7 +70,7 @@ class CoroutinesDumpTest : DebugTestBase() {

@Test
fun testRunningCoroutineWithSuspensionPoint() = runBlocking {
val deferred = async(Dispatchers.Default) {
val deferred = async(Dispatchers.IO) {
activeMethod(shouldSuspend = true)
yield() // tail-call
}
Expand Down Expand Up @@ -100,7 +100,7 @@ class CoroutinesDumpTest : DebugTestBase() {

@Test
fun testCreationStackTrace() = runBlocking {
val deferred = async(Dispatchers.Default) {
val deferred = async(Dispatchers.IO) {
activeMethod(shouldSuspend = true)
}

Expand Down Expand Up @@ -129,7 +129,7 @@ class CoroutinesDumpTest : DebugTestBase() {

@Test
fun testFinishedCoroutineRemoved() = runBlocking {
val deferred = async(Dispatchers.Default) {
val deferred = async(Dispatchers.IO) {
activeMethod(shouldSuspend = true)
}

Expand All @@ -149,7 +149,10 @@ class CoroutinesDumpTest : DebugTestBase() {
if (shouldSuspend) yield()
notifyCoroutineStarted()
while (coroutineContext[Job]!!.isActive) {
runCatching { Thread.sleep(60_000) }
try {
Thread.sleep(60_000)
} catch (_ : InterruptedException) {
}
}
}

Expand Down
Expand Up @@ -133,7 +133,7 @@ class RunningThreadStackMergeTest : DebugTestBase() {
}

private fun CoroutineScope.launchEscapingCoroutineWithoutContext() {
launch(Dispatchers.Default) {
launch(Dispatchers.IO) {
suspendingFunctionWithoutContext()
assertTrue(true)
}
Expand Down

0 comments on commit e127d69

Please sign in to comment.