This is an automated email from the ASF dual-hosted git repository.

ffang pushed a commit to branch 4.1.x-fixes
in repository https://gitbox.apache.org/repos/asf/cxf.git


The following commit(s) were added to refs/heads/4.1.x-fixes by this push:
     new 5c4ddf429cc [CXF-9234]Concurrent DynamicClientFactory.createClient for 
the same W… (#3350)
5c4ddf429cc is described below

commit 5c4ddf429cc6c625161c2444cb16d59b181a4de6
Author: Freeman(Yue) Fang <[email protected]>
AuthorDate: Tue Aug 4 21:05:28 2026 -0400

    [CXF-9234]Concurrent DynamicClientFactory.createClient for the same W… 
(#3350)
    
    * [CXF-9234]Concurrent DynamicClientFactory.createClient for the same WSDL 
url corrupts the cached schema DOM and spins forever at 100% CPU
    
    * [CXF-9234] Make DynamicClientFactory.cloneNode private
    
    Prevents subclasses from overriding it and silently dropping the
    synchronized(document) guard added for the concurrent createClient()
    fix; no external callers exist, so narrowing visibility is safe.
    
    (cherry picked from commit 457dfbefb2ed5268d9e6ccf3cd6bb9b09c6fe153)
---
 .../cxf/endpoint/dynamic/DynamicClientFactory.java |  12 ++-
 .../jaxws/DynamicClientConcurrencyTest.java        | 120 +++++++++++++++++++++
 2 files changed, 130 insertions(+), 2 deletions(-)

diff --git 
a/rt/frontend/simple/src/main/java/org/apache/cxf/endpoint/dynamic/DynamicClientFactory.java
 
b/rt/frontend/simple/src/main/java/org/apache/cxf/endpoint/dynamic/DynamicClientFactory.java
index c19b3f116f9..c342bd436c9 100644
--- 
a/rt/frontend/simple/src/main/java/org/apache/cxf/endpoint/dynamic/DynamicClientFactory.java
+++ 
b/rt/frontend/simple/src/main/java/org/apache/cxf/endpoint/dynamic/DynamicClientFactory.java
@@ -972,14 +972,22 @@ public class DynamicClientFactory {
         return addedToNotDone ? null : element;
     }
 
-    public Node cloneNode(Document document, Node node, boolean deep) throws 
DOMException {
+    private Node cloneNode(Document document, Node node, boolean deep) throws 
DOMException {
         if (document == null || node == null) {
             return null;
         }
         int type = node.getNodeType();
 
         if (node.getOwnerDocument() == document) {
-            return node.cloneNode(deep);
+            // The schema/WSDL Document backing this node may be cached and 
shared across
+            // concurrent createClient() calls (see WSDLManagerImpl). The 
native cloneNode(true)
+            // walk triggers UserDataHandler callbacks (e.g. 
StaxUtils$LocationUserDataHandler)
+            // that mutate the source document's internal userData table, 
which is not
+            // thread-safe (WeakHashMap in Xerces-J, HashMap in the JDK DOM 
impl). Concurrent
+            // clones of the same shared document must therefore be serialized 
(CXF-9234).
+            synchronized (document) {
+                return node.cloneNode(deep);
+            }
         }
         Node clone;
         switch (type) {
diff --git 
a/systests/jaxws/src/test/java/org/apache/cxf/systest/jaxws/DynamicClientConcurrencyTest.java
 
b/systests/jaxws/src/test/java/org/apache/cxf/systest/jaxws/DynamicClientConcurrencyTest.java
new file mode 100644
index 00000000000..52c192ff29f
--- /dev/null
+++ 
b/systests/jaxws/src/test/java/org/apache/cxf/systest/jaxws/DynamicClientConcurrencyTest.java
@@ -0,0 +1,120 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.apache.cxf.systest.jaxws;
+
+import java.net.URL;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.concurrent.Callable;
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
+import java.util.concurrent.Future;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.atomic.AtomicInteger;
+
+import org.apache.cxf.Bus;
+import org.apache.cxf.bus.spring.SpringBusFactory;
+import org.apache.cxf.endpoint.Client;
+import org.apache.cxf.jaxws.endpoint.dynamic.JaxWsDynamicClientFactory;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotNull;
+
+/**
+ * Regression test for CXF-9234.
+ *
+ * Concurrent DynamicClientFactory.createClient() calls for the same WSDL 
share a single
+ * cached schema DOM (WSDLManagerImpl#schemaCacheMap). 
DynamicClientFactory#removeImportElement
+ * deep-clones that shared DOM via the native Node.cloneNode(true), which fires
+ * StaxUtils$LocationUserDataHandler callbacks that mutate the source 
document's internal
+ * userData table. That table (WeakHashMap in Xerces-J, HashMap in the JDK DOM 
impl) is not
+ * thread-safe, so concurrent clones used to corrupt it and spin forever. This 
test drives many
+ * threads through createClient() for the same, already-cached WSDL/schema at 
once.
+ */
+public class DynamicClientConcurrencyTest {
+
+    private static final int THREAD_COUNT = 16;
+    private static final int ITERATIONS_PER_THREAD = 8;
+
+    private Bus bus;
+
+    @Before
+    public void setUp() {
+        bus = new SpringBusFactory().createBus();
+    }
+
+    @After
+    public void tearDown() {
+        bus.shutdown(true);
+    }
+
+    @Test(timeout = 90000)
+    public void testConcurrentCreateClientDoesNotHang() throws Exception {
+        final URL wsdlUrl = 
getClass().getClassLoader().getResource("wsdl_systest_jaxws/cxf8979.wsdl");
+        assertNotNull("test wsdl not found on classpath", wsdlUrl);
+
+        // Warm the WSDLManager cache once, single-threaded, so every worker 
below races on
+        // the exact same cached Definition/SchemaInfo/DOM instance.
+        
JaxWsDynamicClientFactory.newInstance(bus).createClient(wsdlUrl).destroy();
+
+        ExecutorService executor = Executors.newFixedThreadPool(THREAD_COUNT, 
r -> {
+            Thread t = new Thread(r);
+            t.setDaemon(true);
+            return t;
+        });
+        final CountDownLatch startLatch = new CountDownLatch(1);
+        final AtomicInteger failures = new AtomicInteger();
+        List<Callable<Void>> tasks = new ArrayList<>();
+        for (int i = 0; i < THREAD_COUNT; i++) {
+            tasks.add(() -> {
+                // separate factory instance per thread, but same shared 
Bus/WSDLManager cache
+                JaxWsDynamicClientFactory dcf = 
JaxWsDynamicClientFactory.newInstance(bus);
+                startLatch.await();
+                for (int j = 0; j < ITERATIONS_PER_THREAD; j++) {
+                    try {
+                        Client client = dcf.createClient(wsdlUrl);
+                        client.destroy();
+                    } catch (Exception e) {
+                        failures.incrementAndGet();
+                    }
+                }
+                return null;
+            });
+        }
+
+        List<Future<Void>> futures = new ArrayList<>();
+        for (Callable<Void> task : tasks) {
+            futures.add(executor.submit(task));
+        }
+        startLatch.countDown();
+
+        for (Future<Void> future : futures) {
+            future.get(60, TimeUnit.SECONDS);
+        }
+        executor.shutdownNow();
+
+        assertEquals("no concurrent createClient() invocation should have 
failed", 0, failures.get());
+    }
+}

Reply via email to