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

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


The following commit(s) were added to refs/heads/main by this push:
     new b0aba9b2c5 TIKA-4983: tika-server starts its forks at startup so a bad 
fork conf… (#3371)
b0aba9b2c5 is described below

commit b0aba9b2c5a886d71fd0996d19308974fdff12dc
Author: Tim Allison <[email protected]>
AuthorDate: Mon Oct 19 21:12:27 2026 -0400

    TIKA-4983: tika-server starts its forks at startup so a bad fork conf… 
(#3371)
    
    * TIKA-4983: tika-server starts its forks at startup so a bad fork config 
fails the server
    
    Generated-by: Claude Opus 5.5
---
 CHANGES.txt                                        |  4 +++
 docs/modules/ROOT/pages/pipes/cpu-sizing.adoc      |  3 +-
 .../ROOT/pages/using-tika/server/index.adoc        |  4 +++
 .../apache/tika/server/core/TikaServerProcess.java |  7 ++++
 .../server/core/TikaServerIntegrationTest.java     | 10 ++++++
 .../core/TikaServerMetricsIntegrationTest.java     | 41 +++++++++++++++-------
 ...tarts.json => tika-config-server-bad-fork.json} |  7 ++--
 ...s.json => tika-config-server-metrics-idle.json} |  1 -
 ...n => tika-config-server-metrics-max-files.json} |  1 -
 9 files changed, 57 insertions(+), 21 deletions(-)

diff --git a/CHANGES.txt b/CHANGES.txt
index fc0e7f6a6c..2402b3fb0f 100644
--- a/CHANGES.txt
+++ b/CHANGES.txt
@@ -58,6 +58,10 @@ Release 4.2.0 - unreleased
      
    * Per-request limits may tighten the server's but no longer loosen them 
(TIKA-4978).
      
+   * tika-server starts its forks before opening its port, so a fork that 
can't start
+     (bad forkedJvmArgs or javaPath, a missing plugin) fails startup instead 
of the first
+     request (TIKA-4983).
+     
    * Image checks (byte-size, min-image-size, blank-image-check; new 
tika-image-processing
      module) make it less likely that blank or tiny images reach text 
recognizers. VLM
      output gets tk:ocr-warnings (truncated, repetition, empty); VLM parsers 
take
diff --git a/docs/modules/ROOT/pages/pipes/cpu-sizing.adoc 
b/docs/modules/ROOT/pages/pipes/cpu-sizing.adoc
index 8e2208eac7..26a0201b9f 100644
--- a/docs/modules/ROOT/pages/pipes/cpu-sizing.adoc
+++ b/docs/modules/ROOT/pages/pipes/cpu-sizing.adoc
@@ -94,7 +94,8 @@ starts hurting throughput.
 `numClients` is a *ceiling* on concurrent forks, not a resident count:
 
 * **Lazy start.** No fork is started at construction; each starts on the
-  first request that needs it.
+  first request that needs it. `tika-server` calls `start()` at startup so a
+  bad fork config fails the server; idle shutdown then trims the pool.
 * **Warm reuse (LIFO).** The client pool hands out the most-recently-used
   client first. Sequential or lightly-concurrent traffic stays on the same
   warm fork (or forks) instead of round-robining every fork awake; a cold
diff --git a/docs/modules/ROOT/pages/using-tika/server/index.adoc 
b/docs/modules/ROOT/pages/using-tika/server/index.adoc
index fd5c2169d5..cd073a78c0 100644
--- a/docs/modules/ROOT/pages/using-tika/server/index.adoc
+++ b/docs/modules/ROOT/pages/using-tika/server/index.adoc
@@ -43,6 +43,10 @@ xref:pipes/cpu-sizing.adoc[Forked-JVM CPU and Heap Sizing] 
before deploying; the
 example configs is not safe boilerplate.
 ====
 
+The server starts every fork before it opens its port, so a fork that can't 
start (bad
+`forkedJvmArgs` or `javaPath`, a missing plugin) fails startup instead of the 
first request.
+Forks still exit after `socketTimeoutMillis` idle and restart on the next 
request that needs one.
+
 == Backpressure: the server can tell you it is busy
 
 A saturated `tika-server` says so. When every fork in a pool is in use and 
none frees up within
diff --git 
a/tika-server/tika-server-core/src/main/java/org/apache/tika/server/core/TikaServerProcess.java
 
b/tika-server/tika-server-core/src/main/java/org/apache/tika/server/core/TikaServerProcess.java
index be314cd92c..c8713ca74f 100644
--- 
a/tika-server/tika-server-core/src/main/java/org/apache/tika/server/core/TikaServerProcess.java
+++ 
b/tika-server/tika-server-core/src/main/java/org/apache/tika/server/core/TikaServerProcess.java
@@ -714,6 +714,13 @@ public class TikaServerProcess {
 
         // Create PipesParser
         PipesParser pipesParser = PipesParser.load(tikaJsonConfig, 
pipesConfig);
+        // a fork that can't start fails the server here, not its first request
+        try {
+            pipesParser.start();
+        } catch (Exception e) {
+            pipesParser.close();
+            throw e;
+        }
 
         // Create and return the helper
         PipesParsingHelper helper = new PipesParsingHelper(pipesParser, 
pipesConfig,
diff --git 
a/tika-server/tika-server-core/src/test/java/org/apache/tika/server/core/TikaServerIntegrationTest.java
 
b/tika-server/tika-server-core/src/test/java/org/apache/tika/server/core/TikaServerIntegrationTest.java
index 755caf89a3..e25b284cfd 100644
--- 
a/tika-server/tika-server-core/src/test/java/org/apache/tika/server/core/TikaServerIntegrationTest.java
+++ 
b/tika-server/tika-server-core/src/test/java/org/apache/tika/server/core/TikaServerIntegrationTest.java
@@ -18,6 +18,8 @@ package org.apache.tika.server.core;
 
 import static java.nio.charset.StandardCharsets.UTF_8;
 import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNotEquals;
+import static org.junit.jupiter.api.Assertions.assertTrue;
 import static org.junit.jupiter.api.Assertions.fail;
 
 import java.io.IOException;
@@ -29,6 +31,7 @@ import java.nio.file.Path;
 import java.nio.file.Paths;
 import java.security.GeneralSecurityException;
 import java.util.List;
+import java.util.concurrent.TimeUnit;
 
 import jakarta.ws.rs.core.Response;
 import org.apache.commons.io.IOUtils;
@@ -97,6 +100,13 @@ public class TikaServerIntegrationTest extends 
IntegrationTestBase {
 
     }
 
+    @Test
+    public void testForkThatCannotStartFailsStartup() throws Exception {
+        startProcess(new String[]{"-config", 
getConfig("tika-config-server-bad-fork.json")});
+        assertTrue(process.waitFor(MAX_WAIT_MS, TimeUnit.MILLISECONDS), 
"server should exit");
+        assertNotEquals(0, process.exitValue());
+    }
+
     @Test
     public void test1WayTLS() throws Exception {
         startProcess(new String[]{"-config", 
ProcessUtils.escapeCommandLine(TIKA_TLS_ONE_WAY_CONFIG
diff --git 
a/tika-server/tika-server-core/src/test/java/org/apache/tika/server/core/TikaServerMetricsIntegrationTest.java
 
b/tika-server/tika-server-core/src/test/java/org/apache/tika/server/core/TikaServerMetricsIntegrationTest.java
index 9a8be43b2e..9f451234fd 100644
--- 
a/tika-server/tika-server-core/src/test/java/org/apache/tika/server/core/TikaServerMetricsIntegrationTest.java
+++ 
b/tika-server/tika-server-core/src/test/java/org/apache/tika/server/core/TikaServerMetricsIntegrationTest.java
@@ -108,19 +108,32 @@ public class TikaServerMetricsIntegrationTest extends 
IntegrationTestBase {
                 
.put(ClassLoader.getSystemResourceAsStream(TEST_HELLO_WORLD)).getStatus());
     }
 
-    /** Routine restarts (max files, idle exit 24) must not be counted as 
crashes. */
+    /** A routine max-files recycle is counted as such, not as a crash. */
     @Test
     @Timeout(240)
-    public void testRoutineRestartReasons() throws Exception {
-        startProcess(new String[]{"-config", 
getConfig("tika-config-server-metrics-restarts.json"),
+    public void testMaxFilesRestartReason() throws Exception {
+        startProcess(new String[]{"-config", 
getConfig("tika-config-server-metrics-max-files.json"),
                 "--metricsPort", String.valueOf(metricsPort)});
         awaitServerStartup();
+        // max 2 files per fork: the third request restarts it
         for (int i = 0; i < 3; i++) {
             assertEquals(200, rmeta(TEST_HELLO_WORLD).getStatus());
         }
-        // Past the idle socket timeout the fork exits 24 and is restarted by 
the next request.
-        String body = awaitSample("tika_pipes_worker_restarts_total", 
"pool=\"sync\",reason=\"idle\"", 1.0);
+        String body = get(metricsEndPoint + MetricsServer.PATH).body();
         assertSample(body, "tika_pipes_worker_restarts_total", 
"pool=\"sync\",reason=\"max_files\"", 1.0);
+        assertSample(body, "tika_pipes_worker_restarts_total", 
"pool=\"sync\",reason=\"idle\"", 0.0);
+        assertSample(body, "tika_pipes_worker_restarts_total", 
"pool=\"sync\",reason=\"crash\"", 0.0);
+    }
+
+    /** A fork that exits 24 after sitting idle is counted as idle, not as a 
crash. */
+    @Test
+    @Timeout(240)
+    public void testIdleRestartReason() throws Exception {
+        startProcess(new String[]{"-config", 
getConfig("tika-config-server-metrics-idle.json"),
+                "--metricsPort", String.valueOf(metricsPort)});
+        awaitServerStartup();
+        // a floor: how many idle exits precede the first poll depends on how 
fast the server started
+        String body = awaitIdleRestart();
         assertSample(body, "tika_pipes_worker_restarts_total", 
"pool=\"sync\",reason=\"crash\"", 0.0);
     }
 
@@ -139,21 +152,23 @@ public class TikaServerMetricsIntegrationTest extends 
IntegrationTestBase {
         assertSample(body, "tika_pipes_worker_restarts_total", 
"pool=\"sync\",reason=\"crash\"", 0.0);
     }
 
-    /** Polls (parse + scrape) until the sample reaches {@code expected}; 
returns the last body. */
-    private String awaitSample(String name, String labels, double expected) 
throws Exception {
-        long deadline = System.currentTimeMillis() + 30_000;
+    /**
+     * Idles past the config's 2s socketTimeoutMillis, then parses so the 
exited fork is restarted
+     * and counted; repeats until an idle restart shows up. Returns the last 
scrape.
+     */
+    private String awaitIdleRestart() throws Exception {
+        long deadline = System.currentTimeMillis() + 60_000;
         String body;
         do {
-            // Each parse resets the fork's idle clock, so sleep past the 
config's socketTimeoutMillis.
-            Thread.sleep(3000);
+            // well past the timeout, so the fork has finished exiting before 
the next parse
+            Thread.sleep(5000);
             assertEquals(200, rmeta(TEST_HELLO_WORLD).getStatus());
             body = get(metricsEndPoint + MetricsServer.PATH).body();
-            if (sample(body, name, labels) == expected) {
+            if (sample(body, "tika_pipes_worker_restarts_total", 
"pool=\"sync\",reason=\"idle\"") >= 1.0) {
                 return body;
             }
         } while (System.currentTimeMillis() < deadline);
-        throw new AssertionError("timed out waiting for " + name + "{" + 
labels + "}=" + expected
-                + " in:\n" + body);
+        throw new AssertionError("no idle restart counted in:\n" + body);
     }
 
     private Response rmeta(String resource) {
diff --git 
a/tika-server/tika-server-core/src/test/resources/configs/tika-config-server-metrics-restarts.json
 
b/tika-server/tika-server-core/src/test/resources/configs/tika-config-server-bad-fork.json
similarity index 54%
copy from 
tika-server/tika-server-core/src/test/resources/configs/tika-config-server-metrics-restarts.json
copy to 
tika-server/tika-server-core/src/test/resources/configs/tika-config-server-bad-fork.json
index 665245531c..c9fc178bb5 100644
--- 
a/tika-server/tika-server-core/src/test/resources/configs/tika-config-server-metrics-restarts.json
+++ 
b/tika-server/tika-server-core/src/test/resources/configs/tika-config-server-bad-fork.json
@@ -1,17 +1,14 @@
 {
   "server": {
     "port": 9999,
-    "allowPipes": true,
     "endpoints": [
       "rmeta"
     ]
   },
   "pipes": {
-    "numClients": 1,
-    "maxFilesProcessedPerProcess": 2,
-    "socketTimeoutMillis": 2000,
+    "numClients": 2,
     "forkedJvmArgs": [
-      "-Xmx256m"
+      "-XX:+NoSuchTikaFlag"
     ]
   },
   "plugin-roots": "target/plugins"
diff --git 
a/tika-server/tika-server-core/src/test/resources/configs/tika-config-server-metrics-restarts.json
 
b/tika-server/tika-server-core/src/test/resources/configs/tika-config-server-metrics-idle.json
similarity index 87%
copy from 
tika-server/tika-server-core/src/test/resources/configs/tika-config-server-metrics-restarts.json
copy to 
tika-server/tika-server-core/src/test/resources/configs/tika-config-server-metrics-idle.json
index 665245531c..f48752d0c8 100644
--- 
a/tika-server/tika-server-core/src/test/resources/configs/tika-config-server-metrics-restarts.json
+++ 
b/tika-server/tika-server-core/src/test/resources/configs/tika-config-server-metrics-idle.json
@@ -8,7 +8,6 @@
   },
   "pipes": {
     "numClients": 1,
-    "maxFilesProcessedPerProcess": 2,
     "socketTimeoutMillis": 2000,
     "forkedJvmArgs": [
       "-Xmx256m"
diff --git 
a/tika-server/tika-server-core/src/test/resources/configs/tika-config-server-metrics-restarts.json
 
b/tika-server/tika-server-core/src/test/resources/configs/tika-config-server-metrics-max-files.json
similarity index 88%
rename from 
tika-server/tika-server-core/src/test/resources/configs/tika-config-server-metrics-restarts.json
rename to 
tika-server/tika-server-core/src/test/resources/configs/tika-config-server-metrics-max-files.json
index 665245531c..1dddc2e159 100644
--- 
a/tika-server/tika-server-core/src/test/resources/configs/tika-config-server-metrics-restarts.json
+++ 
b/tika-server/tika-server-core/src/test/resources/configs/tika-config-server-metrics-max-files.json
@@ -9,7 +9,6 @@
   "pipes": {
     "numClients": 1,
     "maxFilesProcessedPerProcess": 2,
-    "socketTimeoutMillis": 2000,
     "forkedJvmArgs": [
       "-Xmx256m"
     ]

Reply via email to