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);
