Skip to content
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

Allow polyglot embedders to control throwDeniedThreadAccess #8266

Open
wants to merge 9 commits into
base: master
Choose a base branch
from

Conversation

JaroslavTulach
Copy link
Contributor

@JaroslavTulach JaroslavTulach commented Jan 29, 2024

Let's fix #8033 by controlling what happens when throwDeniedThreadAccess is called.

The simplest way to address #8033 is to replace the current action in throwDeniedThreadAccess - e.g. throw PolyglotEngineException.illegalState with a callback to embedder to decide what to do - don't throw, but retry. However calling foreign code at the moment of throwDeniedThreadAccess is slightly dangerous as internal lock on PolyglotContextImpl is being held. As such, let's start with the first step 82b14c3 - let's make sure the exception is always thrown without holding internal lock(s). To do so, I am introducing an internal checked exception and propagating it up to the point where the lock isn't held. Only then it is converted to original PolyglotEngineException.illegalState exception and thrown. The fact that the exception is checked, instructs the javac to check all paths that may yield the PolyglotThreadAccessException and help me to never forget to handle the conversion. The unusual LOOP: for (;;) {... continue LOOP.... break LOOP} style is used as a preparation for the next step...

The next step then defines Builder.onDeniedThreadAccess(Consumer<RuntimeException> handler) which allows the embedders to decide whether to throw the exception or do something more intrinsic.

The third step is to use the API. Great, it seems to work as planned.

@oracle-contributor-agreement oracle-contributor-agreement bot added the OCA Verified All contributors have signed the Oracle Contributor Agreement. label Jan 29, 2024
@JaroslavTulach
Copy link
Contributor Author

JaroslavTulach commented Jan 30, 2024

With onDeniedThreadAccess as proposed by this PR the embedders have a way to act like an Ethernet on collision and successfully orchestrate two threads accessing the same context to run into completion.

More info on Ethernet collision detection and behavior - most important "Calculate and wait the random backoff".

The good thing from the Truffle API perspective is:

  • the ugly Ethernet like behavior isn't part of Truffle code base
  • people can use different style - try to access GIL instead of random waiting - etc.
  • the Truffle API doesn't prescribe the correct style, just opens the door to coordinate the access somehow
  • all that is possible by this simple onDeniedThreadAccess callback

The onDeniedThreadAccess callback is an API seam that allows accepting something that would otherwise be unacceptable. All the ugliness remains in the hands of embedders...

@chumer
Copy link
Member

chumer commented Jan 30, 2024

I like the idea in principle.

What I like about this approach:

  • Does not need any checks on the fast-path.
  • Flexibility to implement your own strategy in case of multi-threaded access without allowing actual multi-threaded access.
  • Careful considerations of current locking strategy.

What I don't like about this approach:

  • It makes already quite complex control flow even more complex. I think some of the changes could be improved for better readability.
  • It only works if you exclusively use single-threaded languages. Note that we force transition to multi-threaded as soon as language is initialized that depends on it to avoid late deoptimization. We would need to make sure we would need to change:
     boolean transitionToMultiThreading = isSingleThreaded() && hasActiveOtherThread(true, false);

to something like this (just a sketch)

     boolean multiThreadedAccess = (isSingleThreaded() || multiThreadedCallback) &&  hasActiveOtherThread(true, false);
  • Following up the previous problem I think we should change the API to be a callback whenever a multi-threaded access is performed. (e.g. Builder#onMultiThreadedAccess)
  • We lock ourselves in with the current approach of doing slow-path and fast-path thread transitions. If we ever want to have fast transitions (enter/leave) from multiple threads this will be harder to do as we need to keep supporting the callback.
  • Further investigation and testing needed for toggling behavior. What if multiple threads try to enter/leave concurrently. Can it happen that no thread ever wins?
  • Its a complex API to use correctly. Lots of traps there. So we need extensive documentation of dos and don'ts. Ideally also examples on how to use it.
  • We also need to extend the TruffleContext API in a complete implementation.

@JaroslavTulach
Copy link
Contributor Author

JaroslavTulach commented Feb 5, 2024

I like the idea in principle.

OK, I'll write some unit tests and then we can continue the review. I am not sure I fully understand all the comments, for example:

only works if you exclusively use single-threaded languages

Sure, I only care about Graal.js - all the other languages we are interested in are mutli threading ready. All I need is to make sure when there is a collision, it doesn't end up that fatal as right now and there is a way to recover. I don't get the comment about multi-threaded languages. We can sort it out when I prepare some unit tests for review. But I'd prefer to keep my PR as minimal as possible - solving complete Truffle corner cases is job for a professional, not for an outsider like me.

quite complex control flow even more complex

Do you mean those for (;;) { ... continue ... } loops? They look more scary in the diff than in reality. I just had to shift the indentation and that makes the change look bigger than it is.

I'll find some time this week and add some test cases to this PR.

@JaroslavTulach
Copy link
Contributor Author

The first test added in addb748 verifies that all the ways that originally yielded illegalState exception can reach the onDeniedThreadAccess callback which can then throw some other exception.

The other test (added in f91faeb) verifies that the onDeniedThreadAccess callback can request a retry and then the threaded access succeeds.

@chumer, what additional tests you want me to provide?

@JaroslavTulach
Copy link
Contributor Author

Hello @chumer, is there anything else I shall do to get this API change in?

Copy link
Member

@chumer chumer left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A great step forward. Some of the things I previously commented on were unfortunately not adressed. Added a few more comments.

@@ -1150,7 +1150,14 @@ public Thread createThread(Object polyglotLanguageContext, Runnable runnable, Ob
threadContext = innerContext.getContext(threadContext.language);
}
PolyglotThread newThread = new PolyglotThread(threadContext, runnable, group, stackSize, beforeEnter, afterLeave);
threadContext.context.checkMultiThreadedAccess(newThread);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can't you move the loop into checkMultiThreadedAccess?

fail();
} catch (PolyglotException e) {
assertTrue(e.getMessage().contains("SecurityException"));
assertTrue(e.isInternalError());
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Internal errors must never be reachable. Whatever exception is thrown by onDeniedThreadAccess must be a host exception.

}

@Test
public void testNoThreadAllowedRetry() {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would like to see an integration test that uses it in a way that involves multiple threads.
Something that really models how you want to use it.

How about something that would deadlock if executed within the lock?

synchronized (this) {
PolyglotThreadInfo threadInfo = getCurrentThreadInfo();
PolyglotThreadAccessException threadAccessException = null;
LOOP: for (;;) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

minor: We are not actually using upper-case loop labels. We typically use lower-cased ones. Let's be consistent.

* yielded by default. By installing this {@code handler} one can control what shall happen.
* Either to throw the provided exception or resolve the multithreaded situation and return
* to retry the thread access again.
*
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This could use usage examples. For example customization of the error message and or using a locking strategy?

* @param handler callback that either throws the provided {@code RuntimeException} or
* returns to signal request for retry
* @return this builder
* @since 23.2
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Target version is 24.1

* @return this builder
* @since 23.2
*/
public Builder onDeniedThreadAccess(Consumer<RuntimeException> handler) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

API changes like this need an entry in the changelog.

*/
package com.oracle.truffle.polyglot;

final class PolyglotThreadAccessException extends Exception {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This exception does not need a stack trace as it is supposed to be caught always.

@@ -1180,6 +1182,23 @@ public Builder allowCreateThread(boolean enabled) {
return this;
}

/**
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The corresponding TruffleContext API is missing.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
OCA Verified All contributors have signed the Oracle Contributor Agreement.
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Let languages control multi threaded access ... is not allowed for language(s) js
2 participants