Skip to content

Commit 376e5d7

Browse files
authored
TIKA-4817: reclaim a shared-mode worker abandoned by a client disconnect (#3015)
1 parent cf9c2c8 commit 376e5d7

2 files changed

Lines changed: 74 additions & 21 deletions

File tree

tika-pipes/tika-pipes-core/src/main/java/org/apache/tika/pipes/core/PerClientServerManager.java

Lines changed: 44 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -85,12 +85,15 @@ private static int forkHeapPercentage(int numClients) {
8585
private final Path tikaConfigPath;
8686
private final int clientId;
8787

88-
private Process process;
89-
private ServerSocket serverSocket;
90-
private Path tmpDir;
91-
private int port = -1;
88+
private volatile Process process;
89+
private volatile ServerSocket serverSocket;
90+
private volatile Path tmpDir;
91+
private volatile int port = -1;
9292
private long filesProcessed = 0;
93-
private boolean pendingRestart = false;
93+
private volatile boolean pendingRestart = false;
94+
// Set once by shutdown()/close(); guards a request thread from starting a fresh
95+
// process after the manager has been torn down (which would leak the child).
96+
private volatile boolean closed = false;
9497

9598
public PerClientServerManager(PipesConfig pipesConfig, Path tikaConfigPath, int clientId) {
9699
this.pipesConfig = pipesConfig;
@@ -227,7 +230,10 @@ public int handleCrashAndGetExitCode() {
227230
}
228231

229232
@Override
230-
public void ensureRunning() throws IOException, InterruptedException, TimeoutException, ServerInitializationException {
233+
public synchronized void ensureRunning() throws IOException, InterruptedException, TimeoutException, ServerInitializationException {
234+
if (closed) {
235+
throw new IllegalStateException("PerClientServerManager is closed");
236+
}
231237
// Check if server is running AND not marked for restart
232238
if (isRunning() && !pendingRestart) {
233239
return;
@@ -237,25 +243,32 @@ public void ensureRunning() throws IOException, InterruptedException, TimeoutExc
237243

238244
@Override
239245
public Socket connect(int socketTimeoutMillis) throws IOException, ServerInitializationException {
240-
if (serverSocket == null) {
246+
// Capture the socket up front: shutdown() may null the field concurrently, but this
247+
// request keeps using (and detects the close on) the instance it started with.
248+
ServerSocket ss = serverSocket;
249+
if (ss == null) {
241250
throw new IllegalStateException("Server not started. Call ensureRunning() first.");
242251
}
243252

244253
// Accept incoming connection from the server process
245-
serverSocket.setSoTimeout(1000); // 1 second timeout for each poll
254+
ss.setSoTimeout(1000); // 1 second timeout for each poll
246255
long startTime = System.currentTimeMillis();
247256

248257
while (true) {
249258
try {
250-
Socket socket = serverSocket.accept();
259+
Socket socket = ss.accept();
251260
socket.setSoTimeout(socketTimeoutMillis);
252261
socket.setTcpNoDelay(true);
253262
LOG.debug("clientId={}: accepted connection from server", clientId);
254263
return socket;
255264
} catch (SocketTimeoutException e) {
256-
// Check if the process died before connecting
257-
if (!process.isAlive()) {
258-
int exitValue = process.exitValue();
265+
// Check if the process died (or the manager was shut down) before connecting.
266+
Process p = process;
267+
if (p == null) {
268+
throw new IOException("Server manager was shut down while connecting");
269+
}
270+
if (!p.isAlive()) {
271+
int exitValue = p.exitValue();
259272
LOG.error("clientId={}: Process exited with code {} before connecting to socket",
260273
clientId, exitValue);
261274
ServerProcessIO.surfaceCrashDiagnostics(LOG, "clientId=" + clientId, tmpDir);
@@ -281,10 +294,14 @@ public Socket connect(int socketTimeoutMillis) throws IOException, ServerInitial
281294
}
282295
}
283296

284-
private void startServer() throws IOException, InterruptedException, TimeoutException, ServerInitializationException {
285-
// Clean up any previous server
297+
private synchronized void startServer() throws IOException, InterruptedException, TimeoutException, ServerInitializationException {
298+
if (closed) {
299+
throw new IllegalStateException("PerClientServerManager is closed");
300+
}
301+
// Clean up any previous server (restart) -- teardown, not shutdown, so we do not
302+
// mark the manager closed.
286303
if (process != null || serverSocket != null || tmpDir != null) {
287-
shutdown();
304+
teardown();
288305
}
289306

290307
// Create new server socket to get a free port
@@ -348,8 +365,18 @@ private void startServer() throws IOException, InterruptedException, TimeoutExce
348365
}
349366

350367
@Override
351-
public void shutdown() throws InterruptedException {
352-
LOG.debug("clientId={}: shutting down server", clientId);
368+
public synchronized void shutdown() throws InterruptedException {
369+
closed = true;
370+
teardown();
371+
}
372+
373+
/**
374+
* Tears down the current server process, socket, and temp dir without marking the manager
375+
* closed -- shared by the final {@link #shutdown()} and by {@link #startServer()} on restart.
376+
* Callers hold the monitor.
377+
*/
378+
private void teardown() throws InterruptedException {
379+
LOG.debug("clientId={}: tearing down server", clientId);
353380

354381
if (serverSocket != null) {
355382
try {

tika-pipes/tika-pipes-core/src/main/java/org/apache/tika/pipes/core/server/ConnectionHandler.java

Lines changed: 30 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -250,13 +250,27 @@ private void loopUntilDone(FetchEmitTuple fetchEmitTuple, ParseContext mergedCon
250250
long totalTaskTimeoutMillis = limits.getTotalTaskTimeoutMillis();
251251
long heartbeatCounter = 1;
252252
boolean wroteIntermediateResult = false;
253+
// If the client disconnects mid-parse we stop writing to the dead socket but keep
254+
// polling and enforcing the timeouts below, so an abandoned worker that will not stop
255+
// still trips checkTotalTimeout/checkProgressTimeout -> System.exit -> the shared JVM
256+
// recycles it. Otherwise (per-JVM shared mode has no per-request process to kill) a
257+
// runaway parse would spin forever with its heap pinned.
258+
boolean clientGone = false;
253259

254260
while (running) {
255261
// Check for intermediate result
256262
if (!wroteIntermediateResult) {
257263
Metadata intermediate = intermediateResult.poll(100, TimeUnit.MILLISECONDS);
258264
if (intermediate != null) {
259-
protocolIO.writeIntermediate(intermediate);
265+
if (!clientGone) {
266+
try {
267+
protocolIO.writeIntermediate(intermediate);
268+
} catch (IOException e) {
269+
clientGone = true;
270+
LOG.debug("handlerId={}: client gone (writing intermediate); keeping the "
271+
+ "worker under its timeout so a runaway parse is reclaimed", handlerId);
272+
}
273+
}
260274
countDownLatch.countDown();
261275
wroteIntermediateResult = true;
262276
}
@@ -285,15 +299,27 @@ private void loopUntilDone(FetchEmitTuple fetchEmitTuple, ParseContext mergedCon
285299
}
286300
LOG.debug("handlerId={}: finished task id={} status={}", handlerId,
287301
fetchEmitTuple.getId(), pipesResult.status());
288-
protocolIO.writeFinished(pipesResult);
302+
if (!clientGone) {
303+
try {
304+
protocolIO.writeFinished(pipesResult);
305+
} catch (IOException e) {
306+
LOG.debug("handlerId={}: client gone before final result could be sent", handlerId);
307+
}
308+
}
289309
return;
290310
}
291311

292312
// Send fire-and-forget heartbeat
293313
long elapsed = (System.nanoTime() - startNanos) / 1_000_000L;
294-
if (elapsed > heartbeatCounter * heartbeatIntervalMillis) {
314+
if (!clientGone && elapsed > heartbeatCounter * heartbeatIntervalMillis) {
295315
LOG.trace("handlerId={}: still processing, counter={}", handlerId, heartbeatCounter);
296-
PipesMessage.working().write(output);
316+
try {
317+
PipesMessage.working().write(output);
318+
} catch (IOException e) {
319+
clientGone = true;
320+
LOG.debug("handlerId={}: client gone (heartbeat); keeping the worker under its "
321+
+ "timeout so a runaway parse is reclaimed", handlerId);
322+
}
297323
heartbeatCounter++;
298324
}
299325

0 commit comments

Comments
 (0)