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

acosentino pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/camel.git


The following commit(s) were added to refs/heads/main by this push:
     new 3ba2a9aa2e63 CAMEL-23124 - camel-docling: fix CLI mode ProcessBuilder 
deadlock with concurrent stream reading (#21694)
3ba2a9aa2e63 is described below

commit 3ba2a9aa2e63960027930d0d41688ebd9fafdbe4
Author: Andrea Cosentino <[email protected]>
AuthorDate: Tue Mar 3 12:03:56 2026 +0100

    CAMEL-23124 - camel-docling: fix CLI mode ProcessBuilder deadlock with 
concurrent stream reading (#21694)
    
    Stdout and stderr were read sequentially — first all of stdout, then
    stderr. If the process wrote enough to stderr to fill the OS pipe
    buffer (~64KB), it would block writing to stderr while Java blocked
    reading stdout, causing a deadlock.
    
    Read stderr in a separate daemon thread so both streams drain
    concurrently. The stderr thread is joined with a bounded timeout
    after the process completes.
    
    Signed-off-by: Andrea Cosentino <[email protected]>
---
 .../camel/component/docling/DoclingProducer.java   |  25 +++--
 .../component/docling/DoclingCliDeadlockTest.java  | 101 +++++++++++++++++++++
 2 files changed, 120 insertions(+), 6 deletions(-)

diff --git 
a/components/camel-ai/camel-docling/src/main/java/org/apache/camel/component/docling/DoclingProducer.java
 
b/components/camel-ai/camel-docling/src/main/java/org/apache/camel/component/docling/DoclingProducer.java
index df0f851be49d..17bfc05e851b 100644
--- 
a/components/camel-ai/camel-docling/src/main/java/org/apache/camel/component/docling/DoclingProducer.java
+++ 
b/components/camel-ai/camel-docling/src/main/java/org/apache/camel/component/docling/DoclingProducer.java
@@ -1660,22 +1660,35 @@ public class DoclingProducer extends DefaultProducer {
             StringBuilder output = new StringBuilder();
             StringBuilder error = new StringBuilder();
 
-            try (BufferedReader outputReader = new BufferedReader(new 
InputStreamReader(process.getInputStream()));
-                 BufferedReader errorReader = new BufferedReader(new 
InputStreamReader(process.getErrorStream()))) {
+            // Read stdout and stderr concurrently to prevent deadlock.
+            // If stderr fills the OS pipe buffer (~64KB) while we're blocked
+            // reading stdout, the process would hang waiting to write to 
stderr.
+            Thread stderrReader = new Thread(() -> {
+                try (BufferedReader errorReader = new BufferedReader(new 
InputStreamReader(process.getErrorStream()))) {
+                    String line;
+                    while ((line = errorReader.readLine()) != null) {
+                        error.append(line).append("\n");
+                    }
+                } catch (IOException e) {
+                    LOG.debug("Error reading stderr: {}", e.getMessage());
+                }
+            }, "docling-stderr-reader");
+            stderrReader.setDaemon(true);
+            stderrReader.start();
 
+            try (BufferedReader outputReader = new BufferedReader(new 
InputStreamReader(process.getInputStream()))) {
                 String line;
                 while ((line = outputReader.readLine()) != null) {
                     LOG.debug("Docling output: {}", line);
                     output.append(line).append("\n");
                 }
-
-                while ((line = errorReader.readLine()) != null) {
-                    error.append(line).append("\n");
-                }
             }
 
             boolean finished = 
process.waitFor(configuration.getProcessTimeout(), TimeUnit.MILLISECONDS);
 
+            // Wait for stderr reader to finish (with a bounded wait to avoid 
hanging)
+            stderrReader.join(5000);
+
             if (!finished) {
                 process.destroyForcibly();
                 throw new RuntimeException(
diff --git 
a/components/camel-ai/camel-docling/src/test/java/org/apache/camel/component/docling/DoclingCliDeadlockTest.java
 
b/components/camel-ai/camel-docling/src/test/java/org/apache/camel/component/docling/DoclingCliDeadlockTest.java
new file mode 100644
index 000000000000..dffa700eec48
--- /dev/null
+++ 
b/components/camel-ai/camel-docling/src/test/java/org/apache/camel/component/docling/DoclingCliDeadlockTest.java
@@ -0,0 +1,101 @@
+/*
+ * 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.camel.component.docling;
+
+import java.io.BufferedReader;
+import java.io.IOException;
+import java.io.InputStreamReader;
+import java.util.List;
+import java.util.concurrent.TimeUnit;
+
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.condition.DisabledOnOs;
+import org.junit.jupiter.api.condition.OS;
+
+import static org.junit.jupiter.api.Assertions.*;
+
+/**
+ * Demonstrates the ProcessBuilder deadlock pattern and verifies the fix.
+ *
+ * <p>
+ * Before the fix, stdout and stderr were read sequentially in {@code 
executeDoclingCommand()}. If the child process
+ * wrote more than ~64KB to stderr while the Java side was blocked reading 
stdout, both processes would deadlock: the
+ * child blocked writing to the full stderr pipe, and Java blocked reading 
from stdout (waiting for EOF that never comes
+ * because the child is stuck).
+ *
+ * <p>
+ * The fix reads stderr in a separate daemon thread, allowing both streams to 
drain concurrently.
+ */
+@DisabledOnOs(OS.WINDOWS)
+class DoclingCliDeadlockTest {
+
+    /**
+     * Verifies that reading stdout and stderr concurrently (the fixed 
approach) completes even when the process writes
+     * large amounts to both streams.
+     */
+    @Test
+    void concurrentStreamReadingDoesNotDeadlock() throws Exception {
+        // Spawn a process that writes >64KB to both stdout and stderr 
simultaneously.
+        // With sequential reading, this would deadlock. With concurrent 
reading, it completes.
+        ProcessBuilder pb = new ProcessBuilder(
+                List.of(
+                        "bash", "-c",
+                        // Write 100KB to stderr and 100KB to stdout
+                        "for i in $(seq 1 2000); do echo \"stderr line $i: 
padding to fill buffer\" >&2; "
+                                      + "echo \"stdout line $i: padding to 
fill buffer\"; done"));
+
+        Process process = pb.start();
+
+        StringBuilder stdout = new StringBuilder();
+        StringBuilder stderr = new StringBuilder();
+
+        // Read stderr in a separate thread (the fix)
+        Thread stderrReader = new Thread(() -> {
+            try (BufferedReader reader = new BufferedReader(new 
InputStreamReader(process.getErrorStream()))) {
+                String line;
+                while ((line = reader.readLine()) != null) {
+                    stderr.append(line).append("\n");
+                }
+            } catch (IOException e) {
+                // ignore
+            }
+        }, "test-stderr-reader");
+        stderrReader.setDaemon(true);
+        stderrReader.start();
+
+        // Read stdout in the main thread
+        try (BufferedReader reader = new BufferedReader(new 
InputStreamReader(process.getInputStream()))) {
+            String line;
+            while ((line = reader.readLine()) != null) {
+                stdout.append(line).append("\n");
+            }
+        }
+
+        // Should complete within a reasonable time — no deadlock
+        boolean finished = process.waitFor(30, TimeUnit.SECONDS);
+        stderrReader.join(5000);
+
+        assertTrue(finished, "Process should complete without deadlock");
+        assertEquals(0, process.exitValue());
+
+        // Verify both streams were fully read
+        assertTrue(stdout.length() > 64 * 1024,
+                "Should have read >64KB from stdout but got " + 
stdout.length());
+        assertTrue(stderr.length() > 64 * 1024,
+                "Should have read >64KB from stderr but got " + 
stderr.length());
+    }
+}

Reply via email to