Skip to content

JIT: always throw scoped async exceptions via new async category#23367

Open
babsingh wants to merge 1 commit intoeclipse-openj9:masterfrom
babsingh:main7_baseline
Open

JIT: always throw scoped async exceptions via new async category#23367
babsingh wants to merge 1 commit intoeclipse-openj9:masterfrom
babsingh:main7_baseline

Conversation

@babsingh
Copy link
Contributor

Scoped async exceptions were previously tagged under
J9_CHECK_ASYNC_THROW_EXCEPTION. Under that category, the JIT paths
do not always guarantee exception delivery.

This is incorrect for scoped exceptions, which must be thrown
immediately when observed.

Introduce a new async action category,
J9_CHECK_ASYNC_SCOPED_EXCEPTION, and update the JIT async check and
dispatch paths to treat it as a must-throw condition from compiled
code. This ensures scoped exceptions are raised immediately from the
JIT side rather than being merged with the general async throw logic.

Changes include:

  • Define new J9_CHECK_ASYNC_SCOPED_EXCEPTION async flag
  • Classify scoped exceptions under the new category instead of
    J9_CHECK_ASYNC_THROW_EXCEPTION
  • Update JIT async check handling to always trigger a throw for the
    scoped exception category
  • Adjust helper / dispatch logic accordingly

This preserves existing behavior for normal async exceptions while
tightening semantics for scoped exceptions.

Related: #22934

Scoped async exceptions were previously tagged under
J9_CHECK_ASYNC_THROW_EXCEPTION. Under that category, the JIT paths
do not always guarantee exception delivery.

This is incorrect for scoped exceptions, which must be thrown
immediately when observed.

Introduce a new async action category,
J9_CHECK_ASYNC_SCOPED_EXCEPTION, and update the JIT async check and
dispatch paths to treat it as a must-throw condition from compiled
code. This ensures scoped exceptions are raised immediately from the
JIT side rather than being merged with the general async throw logic.

Changes include:
- Define new J9_CHECK_ASYNC_SCOPED_EXCEPTION async flag
- Classify scoped exceptions under the new category instead of
  J9_CHECK_ASYNC_THROW_EXCEPTION
- Update JIT async check handling to always trigger a throw for the
  scoped exception category
- Adjust helper / dispatch logic accordingly

This preserves existing behavior for normal async exceptions while
tightening semantics for scoped exceptions.

Related: eclipse-openj9#22934

Signed-off-by: Babneet Singh <sbabneet@ca.ibm.com>
@babsingh babsingh marked this pull request as ready for review February 13, 2026 22:19
@babsingh babsingh requested a review from gacholio February 13, 2026 22:20
babsingh added a commit to babsingh/aqa-tests that referenced this pull request Feb 13, 2026
It is fixed by the below PRs.

Extension Repo PRs (Test fix):
- JDK-next: ibmruntimes/openj9-openjdk-jdk#1174
- JDK26: ibmruntimes/openj9-openjdk-jdk26#22

OpenJ9 PR (VM/JIT fix): eclipse-openj9/openj9#23367

Related: eclipse-openj9/openj9#22934

Signed-off-by: Babneet Singh <sbabneet@ca.ibm.com>
babsingh added a commit to babsingh/aqa-tests that referenced this pull request Feb 13, 2026
TestSharedCloseJvmti.java is fixed by the below PRs.

Extension Repo PRs (Test fix):
- JDK-next: ibmruntimes/openj9-openjdk-jdk#1174
- JDK26: ibmruntimes/openj9-openjdk-jdk26#22

OpenJ9 PR (VM/JIT fix): eclipse-openj9/openj9#23367

Related: eclipse-openj9/openj9#22934

Signed-off-by: Babneet Singh <sbabneet@ca.ibm.com>
@gacholio
Copy link
Contributor

I have not looked at this in detail, but the JIT does not support throwing exceptions from the async handler called by compiled code. Adding new categories of exceptions does not fix this. JIT changes would be required to support this (in which case, we should probably just allow it in general).

@babsingh
Copy link
Contributor Author

JIT was repeatedly re-entering hasMemoryScope, which appears to lead to an infinite recursion/loop in this scenario. With this PR applied, the scoped exception is thrown consistently in my testing.

@gacholio Could you share what JIT-side changes would be required to properly support this case?

Looping in @hzongaro as well for JIT perspective.

@gacholio
Copy link
Contributor

I believe the restriction is due to control flow complexity - if every async can potentially throw, the JIT needs to do more bookkeeping on all the paths to the async, potentially resulting in excessive register spills.

I think it would be a simple enough experiment to implement this and see what the costs are.

@babsingh
Copy link
Contributor Author

babsingh commented Feb 18, 2026

if every async can potentially throw, the JIT needs to do more bookkeeping on all the paths to the async ...

@hzongaro Thoughts on implementing the above?

@gacholio Just to confirm, once the JIT guarantees that async exceptions are thrown immediately, the synchronization below can be removed, correct? There shouldn’t be any other gaps where async exception processing could be delayed, right?

#if JAVA_SPEC_VERSION >= 22
/* There are gaps where async exceptions are not processed in time
* (e.g. JIT compiled code in a loop). Wait until J9VMThread->scopedError
* (async exception) is transferred to J9VMThread->currentException. The
* wait prevents a MemorySession to be closed until no more operations are
* being performed on it.
*/
omrthread_monitor_enter(vm->closeScopeMutex);
while (0 != vm->closeScopeNotifyCount) {
omrthread_monitor_wait(vm->closeScopeMutex);
}
omrthread_monitor_exit(vm->closeScopeMutex);
#endif /* JAVA_SPEC_VERSION >= 22 */

@gacholio
Copy link
Contributor

gacholio commented Feb 19, 2026

I don't think so at first glance - is there a guarantee that the scope exception will not be caught inside code that's sill inside the scope?

I'm not familiar enough with this feature - the code above appears to either be assuming there's only ever one close going on, or that we wait until all closes are complete before allowing a single close to complete, which seems incorrect. It would be simple to construct a case where close never completes.

@babsingh
Copy link
Contributor Author

  1. Yes, there is no guarantee that the scoped exception cannot be caught by Java code. The safety property is that a thread must not continue to perform scoped accesses on a closed session. Catching the exception does not make it "safe" to keep using the session; the next access must still fail after close.
  2. Per MemorySessionImpl, there will only be one invocation of closeScope0. This is serialized at the Java level: SharedSession.justClose. Agreed that using closeScopeNotifyCount (global counter) will have side-effects when closeScope0 is invoked for different instances of MemorySessionImpl. The global counter should be converted into a VM-injected field inside MemorySessionImpl (local).
  3. Even with the global counter all closes should still complete. The only hang scenarios where close never completes: if the JIT doesn't process the async exception or the Java user indefinitely blocks on a lock preventing the JVM to process the async exception.

@gacholio
Copy link
Contributor

Java user indefinitely blocks on a lock

This seems an obvious hole in the specification - can we interrupt the thread and throw from the point where the interrupt is detected? This doesn't seem like an OpenJ9-specific problem - what does the RI do?

@babsingh
Copy link
Contributor Author

can we interrupt the thread and throw from the point where the interrupt is detected?

if my understanding is correct, they have handshake mechanisms where threads do extra JVM internal work (e.g. inspections) at safepoints via adding things on top of the stack similar to their virtual thread handling. we swap the stack, but they have the ability to build on top of the stack. this allows them to throw the scoped exception even when the thread is indefinitely waiting on a lock.

this doesn't seem like an OpenJ9-specific problem - what does the RI do?

the public API for close doesn't promise non-blocking behavior or progress guarantees. it only promises temporal safety (no use-after-close). so, it is left to the JVM's internal implementation on how it should be handled.

for the interrupt solution, not sure if it will be possible to distinguish between an actual interrupt that should yield InterruptedException vs. the scoped exception. also, forcing a thread out of CountDownLatch.await() without countDown() changes Java-level semantics; would this be acceptable by a Java user?

for java 26, will there be much value in supporting such an extreme corner case where the Java user itself is responsible for the deadlock? can this be a backlog task that can be supported in the future?

@gacholio
Copy link
Contributor

As long as the java code has invalidated the scope (i.e. no one new can enter it), then there's no reason close couldn't be completely asynchronous. This would require maintaining a list of closing scopes with an entry count so when the last thread finally throws the exception, we can perform the final cleanup.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants