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

tballison pushed a commit to branch TIKA-4809-stage-3
in repository https://gitbox.apache.org/repos/asf/tika.git

commit 17ed803174cf7f4d5a3b502f20831410026ab9c3
Author: tallison <[email protected]>
AuthorDate: Sun Aug 9 10:53:07 2026 -0400

    TIKA-4809: Make per-process timeout defaults reachable
---
 .../ROOT/pages/advanced/setting-limits.adoc        | 19 ++++++++------
 docs/modules/ROOT/pages/pipes/timeouts.adoc        |  4 ++-
 .../java/org/apache/tika/config/TimeoutLimits.java | 29 ++++++++++++++++------
 .../org/apache/tika/parser/vlm/VLMOCRConfig.java   |  2 +-
 .../org/apache/tika/config/TimeoutLimitsTest.java  | 11 +++++---
 5 files changed, 45 insertions(+), 20 deletions(-)

diff --git a/docs/modules/ROOT/pages/advanced/setting-limits.adoc 
b/docs/modules/ROOT/pages/advanced/setting-limits.adoc
index 3c0dbbc7cc..4ca3c3d36c 100644
--- a/docs/modules/ROOT/pages/advanced/setting-limits.adoc
+++ b/docs/modules/ROOT/pages/advanced/setting-limits.adoc
@@ -259,7 +259,7 @@ since the parser last reported progress.
 |Maximum wall-clock time in milliseconds for the entire parse task.
 
 |`progressTimeoutMillis`
-|60000 (1 minute)
+|120000 (2 minutes)
 |Maximum time in milliseconds since the parser last reported progress. Catches
 infinite loops and hung processes.
 |===
@@ -282,12 +282,17 @@ can extend well past `progressTimeoutMillis`, because 
each finished page resets
 the timer — but a *single* call that runs longer than `progressTimeoutMillis`
 is still cut short.
 
-That interaction matters at the defaults: `TesseractOCRConfig.timeoutSeconds`
-is 120 seconds, while `progressTimeoutMillis` is 60 seconds. A single
-tesseract call taking between 60 and 120 seconds is stopped by the progress
-timeout before its own per-process timeout applies. If you OCR large or
-high-resolution pages, raise `progressTimeoutMillis` to at least the
-per-process OCR timeout.
+Because of this, `progressTimeoutMillis` also caps how long any single external
+process may run. Parsers that spawn processes size their own timeout via
+`TimeoutLimits.getProcessTimeoutMillis(context, ...)`, which never allows a
+value beyond `progressTimeoutMillis`, so the process is stopped just before the
+progress watchdog would fire.
+
+The shipped defaults are aligned: `progressTimeoutMillis` is 120 seconds and
+the bundled process-spawning parsers (OCR, strings, inference) each default to
+a 120-second per-process timeout, so those defaults are reachable. **If you
+raise a per-process timeout above 120 seconds, raise `progressTimeoutMillis`
+to match** — raising the parser's own timeout alone has no effect.
 
 For most documents — anything without one of the parsers above in the chain —
 the effective ceiling is `progressTimeoutMillis`, not `totalTaskTimeoutMillis`.
diff --git a/docs/modules/ROOT/pages/pipes/timeouts.adoc 
b/docs/modules/ROOT/pages/pipes/timeouts.adoc
index 1cdddd7746..ddbaee8dee 100644
--- a/docs/modules/ROOT/pages/pipes/timeouts.adoc
+++ b/docs/modules/ROOT/pages/pipes/timeouts.adoc
@@ -23,7 +23,9 @@ Tika Pipes uses a two-tier timeout system to handle both 
long-running tasks and
 
 * **`progressTimeoutMillis`** -- Maximum time between progress updates.
   If no progress is reported within this interval, the task is considered 
stalled and killed.
-  Default: `60000` (1 minute).
+  Default: `120000` (2 minutes).
+  This also caps how long any single external process (OCR, `ExternalParser`, 
VLM)
+  may run, since those parsers report progress only once a process completes.
 
 * **`totalTaskTimeoutMillis`** -- Maximum wall-clock time for an entire task.
   Even if the parser is making progress, the task is killed after this time.
diff --git a/tika-core/src/main/java/org/apache/tika/config/TimeoutLimits.java 
b/tika-core/src/main/java/org/apache/tika/config/TimeoutLimits.java
index 34fca0a13c..36e880c7ba 100644
--- a/tika-core/src/main/java/org/apache/tika/config/TimeoutLimits.java
+++ b/tika-core/src/main/java/org/apache/tika/config/TimeoutLimits.java
@@ -28,7 +28,7 @@ import org.apache.tika.parser.ParseContext;
  *   <li>{@code totalTaskTimeoutMillis} — bounds entire task wall-clock time
  *       (default: 3,600,000 ms = 1 hour)</li>
  *   <li>{@code progressTimeoutMillis} — bounds time since the last progress 
update;
- *       catches infinite loops and hung processes (default: 60,000 ms = 1 
minute)</li>
+ *       catches infinite loops and hung processes (default: 120,000 ms = 2 
minutes)</li>
  * </ul>
  * <p>
  * Parsers that never call {@link TikaProgressTracker#update()} effectively get
@@ -56,7 +56,14 @@ public class TimeoutLimits implements Serializable {
     private static final long serialVersionUID = 2L;
 
     public static final long DEFAULT_TOTAL_TASK_TIMEOUT_MILLIS = 3_600_000L;
-    public static final long DEFAULT_PROGRESS_TIMEOUT_MILLIS = 60_000L;
+
+    /**
+     * Also caps how long a single external process may run (see
+     * {@link #getProcessTimeoutMillis(ParseContext, long)}), so this must not 
be
+     * shorter than the per-process timeouts the bundled process-spawning 
parsers
+     * default to, or those defaults become unreachable.
+     */
+    public static final long DEFAULT_PROGRESS_TIMEOUT_MILLIS = 120_000L;
 
     private long totalTaskTimeoutMillis = DEFAULT_TOTAL_TASK_TIMEOUT_MILLIS;
     private long progressTimeoutMillis = DEFAULT_PROGRESS_TIMEOUT_MILLIS;
@@ -133,13 +140,19 @@ public class TimeoutLimits implements Serializable {
     /**
      * Returns the per-process timeout to use for external process execution.
      * <p>
-     * This checks for {@link TimeoutLimits} in the ParseContext and returns
-     * {@code max(0, progressTimeoutMillis - 100)} to give the monitoring loop
-     * a small window to detect the timeout before the process itself times 
out.
-     * Falls back to {@code defaultMs} if no TimeoutLimits is found.
+     * External processes must not outlive the progress watchdog: a parser only
+     * reports progress once its process has finished, so a process allowed to 
run
+     * past {@code progressTimeoutMillis} would be killed as a hang. This caps 
the
+     * caller's timeout at {@code progressTimeoutMillis - 100}, leaving the 
monitoring
+     * loop a small window to observe the process exit first.
+     * <p>
+     * The cap is a ceiling, not a replacement: a caller asking for less than 
the cap
+     * keeps its own shorter value. Falls back to {@code defaultMs} when no
+     * TimeoutLimits is in the context.
      *
      * @param context   the ParseContext (may be null)
-     * @param defaultMs default timeout if no TimeoutLimits in context
+     * @param defaultMs the caller's configured timeout; also used if no 
TimeoutLimits
+     *                  is in the context
      * @return timeout in milliseconds for external process execution
      */
     public static long getProcessTimeoutMillis(ParseContext context, long 
defaultMs) {
@@ -150,7 +163,7 @@ public class TimeoutLimits implements Serializable {
         if (limits == null) {
             return defaultMs;
         }
-        return Math.max(0, limits.progressTimeoutMillis - 100);
+        return Math.max(0, Math.min(defaultMs, limits.progressTimeoutMillis - 
100));
     }
 
     @Override
diff --git 
a/tika-parsers/tika-parsers-ml/tika-vlm/src/main/java/org/apache/tika/parser/vlm/VLMOCRConfig.java
 
b/tika-parsers/tika-parsers-ml/tika-vlm/src/main/java/org/apache/tika/parser/vlm/VLMOCRConfig.java
index 671851596d..0f683dec99 100644
--- 
a/tika-parsers/tika-parsers-ml/tika-vlm/src/main/java/org/apache/tika/parser/vlm/VLMOCRConfig.java
+++ 
b/tika-parsers/tika-parsers-ml/tika-vlm/src/main/java/org/apache/tika/parser/vlm/VLMOCRConfig.java
@@ -65,7 +65,7 @@ public class VLMOCRConfig implements Serializable {
      * HTTP timeout in seconds for the chat completions request.
      * VLM inference can be slow; default is generous.
      */
-    private int timeoutSeconds = 300;
+    private int timeoutSeconds = 120;
 
     /** Optional API key for authenticated endpoints. Empty means no auth. */
     private String apiKey = "";
diff --git 
a/tika-serialization/src/test/java/org/apache/tika/config/TimeoutLimitsTest.java
 
b/tika-serialization/src/test/java/org/apache/tika/config/TimeoutLimitsTest.java
index d82f040fad..5d05fd7eca 100644
--- 
a/tika-serialization/src/test/java/org/apache/tika/config/TimeoutLimitsTest.java
+++ 
b/tika-serialization/src/test/java/org/apache/tika/config/TimeoutLimitsTest.java
@@ -55,7 +55,7 @@ public class TimeoutLimitsTest extends TikaTest {
     public void testDefaults() {
         TimeoutLimits limits = new TimeoutLimits();
         assertEquals(TimeoutLimits.DEFAULT_PROGRESS_TIMEOUT_MILLIS, 
limits.getProgressTimeoutMillis());
-        assertEquals(60000, limits.getProgressTimeoutMillis());
+        assertEquals(120000, limits.getProgressTimeoutMillis());
         assertEquals(TimeoutLimits.DEFAULT_TOTAL_TASK_TIMEOUT_MILLIS, 
limits.getTotalTaskTimeoutMillis());
         assertEquals(3600000, limits.getTotalTaskTimeoutMillis());
     }
@@ -91,10 +91,15 @@ public class TimeoutLimitsTest extends TikaTest {
         ParseContext context = new ParseContext();
         assertEquals(5000, TimeoutLimits.getProcessTimeoutMillis(context, 
5000));
 
-        // Test with context that has TimeoutLimits
+        // progressTimeoutMillis is a ceiling, not a replacement: a caller 
asking for
+        // less than the cap keeps its own shorter value
         TimeoutLimits limits = new TimeoutLimits(3600000, 60000);
         context.set(TimeoutLimits.class, limits);
-        assertEquals(59900, TimeoutLimits.getProcessTimeoutMillis(context, 
5000));
+        assertEquals(5000, TimeoutLimits.getProcessTimeoutMillis(context, 
5000));
+
+        // a caller asking for more than the cap is capped just under it, so 
the
+        // process exits before the progress watchdog fires
+        assertEquals(59900, TimeoutLimits.getProcessTimeoutMillis(context, 
300000));
 
         // Test with very small progress timeout
         TimeoutLimits smallLimits = new TimeoutLimits(3600000, 50);

Reply via email to