-
Notifications
You must be signed in to change notification settings - Fork 1.9k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Fix 'kotlinx.coroutines.debug' & 'kotlinx.coroutines.debug.internal' split package problem #4247
Conversation
f4d6051
to
66b26d5
Compare
I've looked through https://grep.app/search?q=kotlinx.coroutines.debug.AgentPremain, searching for places where people maybe rely on the current fully qualified name programmatically, and this is the only thing I've found: https://github.com/JetBrains/intellij-platform-gradle-plugin/blob/b6916aa28808b31d5cda83da5da4e381595bd0b6/src/main/kotlin/org/jetbrains/intellij/platform/gradle/tasks/InitializeIntelliJPlatformPluginTask.kt#L148 . This was added in the commit JetBrains/intellij-platform-gradle-plugin@a759cec . Looks like this change could break coroutine debugging for IDEA plugin authors? Could you investigate? |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The idea behind the changes is clear and seems sound. The only questions I have are for comments, the build scripts (but I'm still reading those), and the possibility of this causing breakage (which may be fine if we communicate this properly).
* | ||
* Usage Note: Fleet (Reflection): FleetDebugProbes | ||
* Usage Note: Android (Hard Coded, ignored for Leak Detection) | ||
* Usage Note: IntelliJ (Suppress KotlinInternalInJava): CoroutineDumpState |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
What is this comment trying to convey?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I tried to add small notes on internal APIs which are forcefully used by clients (by reflection or ignoring internal visibility). Ss those clients are 'not to be broken', I think its worth marking the code somehow as "internal, but breached'
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ok, makes sense. Though a comment would not stop us: who reads them? Better to mark this as @PublishedApi
so that this appears in the binary compatibility validator dump. Also, it would help a lot if the notes were reworded, like "This object must not be renamed or moved, because this would break binary compatibility for the following clients that forcefully access it:", and then the list. Even with zero context, this gives a clear and actionable impression.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I used
* The entity (despite being internal) has usages in the following products
* - Fleet (Reflection): FleetDebugProbes
* - Android (Hard Coded, ignored for Leak Detection)
* - IntelliJ (Suppress KotlinInternalInJava): CoroutineDumpState
*/
@PublishedApi
I hope this is clear with this rewording?
* Using: | ||
* | ||
* jvmArgs("--add-exports=kotlinx.coroutines.debug/kotlinx.coroutines.repackaged.net.bytebuddy=com.sun.jna") | ||
* jvmArgs("--add-exports=kotlinx.coroutines.debug/kotlinx.coroutines.repackaged.net.bytebuddy.agent=com.sun.jna") |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
What does this mean?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
You see that this test is Ignored: I could not get JPMS and Dynamic Debug Probes to be happy with each other. I suspect that the shading of byte-buddy is a problem here.
This feature was not working previously, but I thought it's nice to keep the test and comment the
- conditions under which this was tested
- the issue we were facing
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Let me double-check my understanding: do you mean that, if these JVM runtime arguments are added, the test will pass, but without them, it won't? Why not introduce a separate source set where these arguments are passed, so that we know this workaround works reliably?
Also, there could be a separate test that shows that there still is this problem; something like assertFailsWith<IllegalStateException> { test body }
. With the additional comment like "we want this test to fail", we'll be able to immediately learn if something changes and this usage becomes possible without workarounds.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is not correct, I updated the comment.
It would be potentially nice to support Dynamic Attach with JPMS.
However, it seems like its blocked by the shaded byte-buddy.
The closest I could get it to working was adding this experiment, adding the jvmargs, but then still observing the issue. I would check in this test so that it is logged and can be easily picked up again, however I also totally accept if we would rather remove the test altogether
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The comment that you added is now gone.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
(Your comments didn't display before, so I didn't see them)
The closest I could get it to working was adding this experiment, adding the jvmargs, but then still observing the issue. I would check in this test so that it is logged and can be easily picked up again, however I also totally accept if we would rather remove the test altogether
Alright, thanks! This should be reflected in the in-code comment in any case. I saw that you added a more elaborate comment already, but this was in the file that's now deleted.
Removing the test is also fine by me, as it doesn't check anything and also can become outdated, leading to confusion down the line.
Alternatively, we can reformulate the test a bit: run it with the specified JVM arguments, assert that it fails, and drop a comment: "It would be nice for this not to fail; with this test, we check that it still fails. If the test stops passing, it means we succeeded and can remove the assertFailsWith
and make it a proper test". Up to you.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
As far as I am aware, Titouan Bion is already working on finding ways of making this test pass!
* | ||
* Caused by: java.lang.IllegalStateException: The Byte Buddy agent is not loaded or this method is not called via the system class loader | ||
* at kotlinx.coroutines.debug/kotlinx.coroutines.repackaged.net.bytebuddy.agent.Installer.getInstrumentation(Installer.java:61) | ||
* ... 54 more |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
And this?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
See comment above: This is the reason why the case is not working yet
I am aware of this usage and I think notifying the team ahead is fine, or a PR can be created |
I agreed with @hsz that I will track the kotlinx coroutines release: |
* | ||
* Usage Note: Fleet (Reflection): FleetDebugProbes | ||
* Usage Note: Android (Hard Coded, ignored for Leak Detection) | ||
* Usage Note: IntelliJ (Suppress KotlinInternalInJava): CoroutineDumpState |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ok, makes sense. Though a comment would not stop us: who reads them? Better to mark this as @PublishedApi
so that this appears in the binary compatibility validator dump. Also, it would help a lot if the notes were reworded, like "This object must not be renamed or moved, because this would break binary compatibility for the following clients that forcefully access it:", and then the list. Even with zero context, this gives a clear and actionable impression.
* | ||
* Usage Note: Fleet (Reflection): FleetDebugProbes | ||
* Usage Note: Android (Hard Coded, ignored for Leak Detection) | ||
* Usage Note: IntelliJ (Suppress KotlinInternalInJava): CoroutineDumpState | ||
*/ | ||
internal object AgentInstallationType { | ||
internal var isInstalledStatically = false |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
No sense to mark this as internal
, as we want this in the BCV dump.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Making it public however would invite more issues?
I think @PublishedApi + comment is better, some usages can be migrated (Fleet and IntelliJ)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
isInstalledStatically
is used from the listed products, rights? Then it, too, should be in the API dump, so not internal
.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Got it! Also checked it in ✅
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
(Your comments didn't display before, so I didn't see them)
Making it public however would invite more issues?
I think @PublishedApi + comment is better, some usages can be migrated (Fleet and IntelliJ)
@PublishedApi
internal object A {
val x: Int = 3
}
would make both A
and A.x
inaccessible to those not abusing reflection/error suppressions, but makes x
available to anyone who already accessed A
. Both A
and A.x
are included in the API dump.
@PublishedApi internal val x
has the same effect, it's just needlessly verbose.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@PublishedApi
internal object AgentInstallationType {
var isInstalledStatically = false
}
Results in
public final class kotlinx.coroutines.debug.internal.AgentInstallationType {
public static final kotlinx.coroutines.debug.internal.AgentInstallationType INSTANCE;
public final boolean isInstalledStatically();
public final void setInstalledStatically(boolean);
static {};
}
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
However the original code
@PublishedApi
internal object AgentInstallationType {
internal var isInstalledStatically = false
}
Results in
public final class kotlinx.coroutines.debug.internal.AgentInstallationType {
public static final kotlinx.coroutines.debug.internal.AgentInstallationType INSTANCE;
public final boolean isInstalledStatically$kotlinx_coroutines_core();
public final void setInstalledStatically$kotlinx_coroutines_core(boolean);
static {};
}
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Therefore, making it public will break current reflection users
Code example from Fleet
override fun setInstalledStatically(installed: Boolean) {
c.getDeclaredMethod("setInstalledStatically\$kotlinx_coroutines_core", Boolean::class.java).invoke(instance, installed)
}
override fun isInstalledStatically(): Boolean {
return c.getDeclaredMethod("isInstalledStatically\$kotlinx_coroutines_core").invoke(instance) as Boolean
}
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Interestingly even
@PublishedApi
internal object AgentInstallationType {
@PublishedApi
internal var isInstalledStatically = false
}
would break the binary signature
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Oh, ok, you are right. Looks like @PublishedApi internal
is the way to go.
* Using: | ||
* | ||
* jvmArgs("--add-exports=kotlinx.coroutines.debug/kotlinx.coroutines.repackaged.net.bytebuddy=com.sun.jna") | ||
* jvmArgs("--add-exports=kotlinx.coroutines.debug/kotlinx.coroutines.repackaged.net.bytebuddy.agent=com.sun.jna") |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Let me double-check my understanding: do you mean that, if these JVM runtime arguments are added, the test will pass, but without them, it won't? Why not introduce a separate source set where these arguments are passed, so that we know this workaround works reliably?
Also, there could be a separate test that shows that there still is this problem; something like assertFailsWith<IllegalStateException> { test body }
. With the additional comment like "we want this test to fail", we'll be able to immediately learn if something changes and this usage becomes possible without workarounds.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Please also add descriptions of the new tests to integration-testing/README.md
Done ✅ |
* | ||
* Usage Note: Fleet (Reflection): FleetDebugProbes | ||
* Usage Note: Android (Hard Coded, ignored for Leak Detection) | ||
* Usage Note: IntelliJ (Suppress KotlinInternalInJava): CoroutineDumpState | ||
*/ | ||
internal object AgentInstallationType { | ||
internal var isInstalledStatically = false |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
isInstalledStatically
is used from the listed products, rights? Then it, too, should be in the API dump, so not internal
.
} | ||
} | ||
|
||
@Test() |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@Test() | |
@Test |
} | ||
} | ||
|
||
@Test() |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@Test() | |
@Test |
@@ -0,0 +1,54 @@ | |||
@file:OptIn(ExperimentalCoroutinesApi::class) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It looks like the whole integration-testing/src/debugDynamicAgentJpmsTest
directory is not even used at all and is just a copy of integration-testing/jpmsTest/src
?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes, this is an artifact, thank you for finding it! Fixed ✅
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Please use the develop
branch as the target and rebase your changes on top of it.
* | ||
* Usage Note: Fleet (Reflection): FleetDebugProbes | ||
* Usage Note: Android (Hard Coded, ignored for Leak Detection) | ||
* Usage Note: IntelliJ (Suppress KotlinInternalInJava): CoroutineDumpState | ||
*/ | ||
internal object AgentInstallationType { | ||
internal var isInstalledStatically = false |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Oh, ok, you are right. Looks like @PublishedApi internal
is the way to go.
- `kotlinx.coroutines.debug.internal` owned by coroutines-core - `kotlinx.coroutines.debug` owned by coroutines-debug (Which also reflects the nature of the debug module, which exposes _some_ public API from the .debug.internal package) This requires moving: - AgentPremain -> .debug.internal - ByteBuddyDynamicAttach -> .debug - NoOpProbes.kt -> .debug
As those dependencies are not *required* to be available at runtime - bytebuddy: Is shadowed and packaged into the -debug.jar - junit: Is optional
…API stable ... as it has known usages in products which we do not want to break
39e7e79
to
faa26b8
Compare
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thank you! This issue was really annoying, it's great that you fixed it!
https://youtrack.jetbrains.com/issue/IDEA-359879/coroutines-debugging-Investigate-split-package-problem
#4124