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"
]