Eric created CXF-9162:
-------------------------
Summary: Reusing a Client with a HttpClientHTTPConduit is not
threadsafe in regards to finalization
Key: CXF-9162
URL: https://issues.apache.org/jira/browse/CXF-9162
Project: CXF
Issue Type: Bug
Reporter: Eric
If a RestClient is created with the default HttpClientHTTPConduit it reuses the
client by default. This, however, is not threadsafe in all cases which is
especially problematic when the client is implicitly closed by the finalizer.
Consider the following example:
{code:java}
public class ResourceProblemWithCxfClientWithHttpClientHTTPConduitTest {
static final String URL = "http://localhost:8080/";
static Server server;
@BeforeAll
static void startServer() {
var serverFactoryBean = new JAXRSServerFactoryBean();
serverFactoryBean.setResourceClasses(MyServiceImpl.class);
serverFactoryBean.setAddress(URL);
server = serverFactoryBean.create();
}
@AfterAll
static void stopServer() {
server.stop();
}
@Test
void testMultipleProxyCalls() {
var clientFactoryBean = new JAXRSClientFactoryBean();
clientFactoryBean.setAddress(URL);
clientFactoryBean.setResourceClass(MyService.class);
// Create the first Client and call the RestService
var myClient1 = clientFactoryBean.create(MyService.class);
myClient1.hello();
// Create a second Client, but do not call yet
var myClient2 = clientFactoryBean.create(MyService.class);
// Register an async GC with finalizer exec and make client1 eligible
for gc
CompletableFuture.runAsync(() -> {
System.gc();
System.runFinalization();
System.gc();
System.out.println("GC'D");
});
myClient1 = null;
// Now call the second client:
// -- with Java17, the method very often just blocks forever
// -- with Java21, it might throw the following exception:
// jakarta.ws.rs.ProcessingException: java.io.IOException: IOException
invoking http://localhost:8080/hello:
// shutdownNow
myClient2.hello();
// the reason behind seems to be wether the resource-aquiciring of the
shared client collides
// with the cleanup of the first in the finalizer
}
@Path("")
public interface MyService {
@GET
@Path("/hello")
void hello();
}
public static class MyServiceImpl implements MyService {
@Override
public void hello() {
System.out.println("hello");
}
}
} {code}
When everything works fine, this code prints the following:
{noformat}
hello
GC'D
hello{noformat}
In many cases, however, it does not, because the GC collects the first client
while second tries to acquire the ressource. Here, the garbage collector is
aggressively invoked, but this has happend to us in production as well if the
GC thinks its cleanup time while the second webclient-invocation starts.
In the error case, the following can happen:
* On Java 17, the first two statements are printed, then the test hangs forever
* On Java 21+ which invokes close on the httpclient, all statments are printed
followed by an IOException:
{noformat}
17:35:13.284 [main] WARN org.apache.cxf.phase.PhaseInterceptorChain --
Interceptor for {http://rest.itest.appkit.platform.arc.finkonsens.de/}MyService
has thrown exception, unwinding now
org.apache.cxf.interceptor.Fault: Could not send Message.{noformat}
This error always happens when the gc finalizer closes the last running
httpclient while a new call tries to acquired the shared client. If you cannot
reproduce it with the test case above, then just run it in debug mode and set
the following breakpoint with a hit count of 2 and blocking only the active
thread, not all threads:
{noformat}
HttpClientHTTPConduit.RefCount.acquire{noformat}
This should block the acquire long enough so that the error will always happen.
--
This message was sent by Atlassian Jira
(v8.20.10#820010)