-
Notifications
You must be signed in to change notification settings - Fork 2.7k
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
Managed executor thread has the AppClassLoader on HttpClient dependent task #41311
Comments
Setting |
Thanks for reporting. I believe this is not a Quarkus issue |
Hi @geoand. Could you expand on that? |
We don't do anything specific for either the HTTP client or respond specifically to container limits. I would suggest trying to reproduce something similar (essentially the executor not being taken into account) with plain Java |
I may be wrong, but the executor seems OK to me. From this code: @PostConstruct
void createHttpClient() {
this.httpClient = HttpClient.newBuilder()
.executor(managedExecutor)
.version(HttpClient.Version.HTTP_2)
.build();
managedExecutor.supplyAsync(() -> "").thenApplyAsync(response -> {
Log.infof("PostConstruct thread=%s", Thread.currentThread().hashCode());
Log.infof("PostConstruct classLoaderAfter=%s", Thread.currentThread().getContextClassLoader());
return Thread.currentThread().getContextClassLoader().getClass().getName();
}, managedExecutor);
}
@GET
@Produces({MediaType.TEXT_PLAIN})
public CompletableFuture<String> test() {
URI uri = URI.create("http://www.columbia.edu/~fdc/sample.html");
HttpRequest request = HttpRequest.newBuilder().GET().uri(uri).build();
Log.infof("classLoaderBefore=%s", Thread.currentThread().getContextClassLoader());
return httpClient.sendAsync(request, HttpResponse.BodyHandlers.ofString()).thenApplyAsync(response -> {
Log.infof("thread=%s", Thread.currentThread().hashCode());
Log.infof("classLoaderAfter=%s", Thread.currentThread().getContextClassLoader());
return Thread.currentThread().getContextClassLoader().getClass().getName();
}, managedExecutor);
} I get this log:
The thread appears to be from the managed executor. The problem seems related to the classloader not being set/propagated on thread context. Correct? |
This does seem like a real bug to me. Our thread pools should always be propagating the correct class loader, so it's odd that it's being erased somehow. |
cc @manovotn for the ManagedExecutor part |
Looking at this part:
The Now, the above is what I think happens, I need to get a closer look this afternoon to verify that claim. |
Thanks @manovotn. I set the |
Yes, I noticed that. I am looking deeper into it just now.
This looks like only FTR, this section of docs describes what contexts are (conditionally) present in Quarkus. The one handling TCCL should be the |
Ok, so normally SR CP has a dedicated module that performs this (here) but Quarkus doesn't seem to use this at all, so I assume our thread pool does this elsewhere automagically. Anyhow, I did try adding the SR module manually and even with that in place the same issue appears - propagation actually happens but at some point the TCCL underneath changes and the propagation carries that information forward. I cannot put my finger on the exact spot which causes the issue but it seems to be originating inside I won't pretend I understand why is this happening and especially why it isn't happening when you don't set the CPU limits so ideas are welcome 🤔 |
I don't know about how any of the higher-level frameworks do it, but at the lowest level the thread pool ( |
I see. The biggest mystery here is that the reproducer actually works if you increase the |
Poking around in the JDK, it looks like it only uses the delegate executor sometimes. Sometimes you're running from the selector manager thread which doesn't necessarily do anything with TCCL and is controlled completely within the HttpClient implementation. I guess the problem likely originates here. Maybe having more threads available increases the likelihood of the source "infecting" task running on a safe thread pool thread instead of the selector thread? It doesn't look like there's any way to access that thread, in any event, other than through a subscription to a socket flow event (like read- or write-ready events). |
Ok that seems to confirm my earlier findings, thanks for looking into it as well!
I think there is no guarantee that the executor you pass to the HttpClient will "just execute the task" on it - it has no notion of possible context propagation we are trying to do and hence can affect the result in certain ways, TCCL included 🤷 |
In addition to the findings of Matej and David, another reason I still believe this is that we've never encountered such an issue with any Quarkus managed component |
A workaround could be to wrap the injected ManagedExecutor with your own executor that simply initializes the TCCL before submitting the task. |
By "selector thread" you mean the thread that is awoken when the socket receives data? Any pointer to the source code? However it seems deterministic. I've never encountered the issue with
Something like this? private static Executor createContextualExecutor(Executor innerExecutor) {
final ClassLoader classLoaderToPropagate = Thread.currentThread().getContextClassLoader();
return command -> {
Thread currentThread = Thread.currentThread();
ClassLoader old = currentThread.getContextClassLoader();
currentThread.setContextClassLoader(classLoaderToPropagate);
try {
innerExecutor.execute(command);
} finally {
currentThread.setContextClassLoader(old);
}
};
} Thanks! |
This is code directly in the JDK's
Yes, the reproducer works well in that regard (thanks for taking the time to make it!).
Yes, that should work |
Closing this issue as I don't think we can do more here. |
Describe the bug
I'm passing the
ManagedExecutor
to the Java HttpClient in order for it to run its tasks. However, when I set a "small" CPU limit on the docker container (viadocker run
or k8s CPU limit) in dependent tasks the contextual classloader is the JVMjdk.internal.loader.ClassLoaders$AppClassLoader
. As a consequence, I haveClassNotFoundException
s.Expected behavior
The classloader on threads running dependent tasks should be the
RunnerClassLoader
from Quarkus.Actual behavior
The classloader on threads running dependent tasks is the JVM
jdk.internal.loader.ClassLoaders$AppClassLoader
.How to Reproduce?
Clone https://github.com/antoniomacri/quarkus-reproducer-http-client-classloader.git
Then run
./build-image.sh && ./run-image.sh
On another shell run:
The expected output should be:
but instead it is:
If I set
--cpus=2.1
or greater, the classloader is correct.Output of
uname -a
orver
No response
Output of
java -version
No response
Quarkus version or git rev
3.11.2
Build tool (ie. output of
mvnw --version
orgradlew --version
)No response
Additional information
Running MacOS on M2 Pro with 12 cores
The text was updated successfully, but these errors were encountered: