Skip to content

Commit ff2d687

Browse files
authored
TIKA-4839 - simplify signature (#3081)
1 parent ccec84e commit ff2d687

10 files changed

Lines changed: 172 additions & 234 deletions

File tree

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

Lines changed: 32 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -179,6 +179,7 @@ private static long totalMemorySize() {
179179
private volatile Path tmpDir;
180180
private volatile int port = -1;
181181
private long filesProcessed = 0;
182+
private volatile long generation;
182183
private volatile boolean pendingRestart = false;
183184
private final RestartCounter restarts = new RestartCounter();
184185
// Set once by shutdown()/close(); guards a request thread from starting a fresh
@@ -291,18 +292,31 @@ public boolean needsRestart() {
291292
return pendingRestart;
292293
}
293294

295+
/**
296+
* One client owns one manager here, so {@code generation} carries no information a sibling
297+
* could invalidate and is accepted only to satisfy the single {@link ServerManager} spelling.
298+
* Shared mode is where staleness is real.
299+
*/
294300
@Override
295-
public void markServerForRestart() {
296-
markServerForRestart(RestartReason.CRASH);
301+
public void markServerForRestart(RestartReason reason, long ignoredGeneration) {
302+
LOG.info("clientId={}: marking server for restart ({})", clientId, reason);
303+
markForRestart(reason);
297304
}
298305

306+
/** Counts forks so {@code PipesParser.getGeneration()} is meaningful in per-client mode too. */
299307
@Override
300-
public void markServerForRestart(RestartReason reason) {
301-
LOG.info("clientId={}: marking server for restart ({})", clientId, reason);
302-
markForRestart(reason);
308+
public long getGeneration() {
309+
return generation;
303310
}
304311

305-
private void markForRestart(RestartReason reason) {
312+
/**
313+
* Takes the same monitor as {@link #ensureRunning()}, which consumes the mark: recording the
314+
* reason and raising the flag must not straddle a restart, or a reason lands against a
315+
* restart that has already been counted. Correctness here previously rested on an
316+
* undocumented one-client-per-manager invariant; shared mode, where siblings are the norm,
317+
* already locked for this and TIKA-4844 is what a stale mark costs.
318+
*/
319+
private synchronized void markForRestart(RestartReason reason) {
306320
restarts.mark(reason);
307321
pendingRestart = true;
308322
}
@@ -319,7 +333,7 @@ public void connectionAbandoned() {
319333
}
320334

321335
@Override
322-
public int handleCrashAndGetExitCode() {
336+
public int handleCrashAndGetExitCode(long generation) {
323337
// Not marked: RestartCounter attributes by exit code; the caller refines OOM/TIMEOUT.
324338
pendingRestart = true;
325339
if (process != null) {
@@ -475,6 +489,7 @@ private synchronized void startServer() throws IOException, InterruptedException
475489

476490
try {
477491
process = pb.start();
492+
generation++;
478493
} catch (Exception e) {
479494
deleteDir(tmpDir);
480495
tmpDir = null;
@@ -531,11 +546,17 @@ private void teardown() throws InterruptedException {
531546
private void destroyProcess() throws InterruptedException {
532547
if (process != null) {
533548
process.destroyForcibly();
534-
process.waitFor(WAIT_ON_DESTROY_MS, TimeUnit.MILLISECONDS);
535-
if (process.isAlive()) {
536-
LOG.error("clientId={}: process still alive after {}ms", clientId, WAIT_ON_DESTROY_MS);
549+
try {
550+
process.waitFor(WAIT_ON_DESTROY_MS, TimeUnit.MILLISECONDS);
551+
if (process.isAlive()) {
552+
LOG.error("clientId={}: process still alive after {}ms", clientId, WAIT_ON_DESTROY_MS);
553+
}
554+
} finally {
555+
// An interrupt here must not leave the field pointing at a SIGKILLed process:
556+
// ensureRunning would then see process == previous and skip counting the restart,
557+
// startServer() would try to reap it again, and tmpDir would never be deleted.
558+
process = null;
537559
}
538-
process = null;
539560
}
540561
}
541562

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

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -235,6 +235,16 @@ public PipesResult process(FetchEmitTuple t) throws IOException, InterruptedExce
235235
closeConnection();
236236
return buildFatalResult(t.getId(), t.getEmitKey(), PipesResult.RESULT_STATUS.FAILED_TO_INITIALIZE,
237237
intermediateResult.get());
238+
} catch (IllegalStateException e) {
239+
// Typically the manager was closed underneath us: a request thread racing PipesParser.close()
240+
// or AsyncProcessor.close(), which interrupts workers without awaiting them. Nothing
241+
// to restart and nothing to recover -- but report it rather than letting an unchecked
242+
// exception escape PipesParser.parse() to a caller that cannot act on it.
243+
LOG.warn("clientId={}: server manager rejected initialization of {}", pipesClientId,
244+
t.getId(), e);
245+
closeConnection();
246+
return buildFatalResult(t.getId(), t.getEmitKey(), PipesResult.RESULT_STATUS.FAILED_TO_INITIALIZE,
247+
intermediateResult.get(), e.getMessage());
238248
}
239249

240250
try {

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

Lines changed: 14 additions & 50 deletions
Original file line numberDiff line numberDiff line change
@@ -95,61 +95,28 @@ public interface ServerManager extends Closeable {
9595
*/
9696
java.nio.file.Path getTempDirectory();
9797

98-
/**
99-
* Marks the server for restart due to a fatal error (OOM, timeout, etc.).
100-
* <p>
101-
* This is called by clients when they receive a fatal error status from the server.
102-
* It signals that the server process is stopping, even if {@link #isRunning()}
103-
* might still return true briefly. The next call to {@link #ensureRunning()} will
104-
* wait for the process to fully exit and then restart.
105-
* <p>
106-
* The reason form below defaults to this one, so this must NOT default to the reason form:
107-
* an implementation overriding neither would recurse until the stack blew. Concrete managers
108-
* in tika-pipes override both, so callers of either spelling reach a real implementation.
109-
*/
110-
default void markServerForRestart() {
111-
// Default no-op: preserves implementations written before RestartReason existed.
112-
}
113-
114-
/** As {@link #markServerForRestart()}, attributing the restart to {@code reason}. Override this one. */
115-
default void markServerForRestart(RestartReason reason) {
116-
markServerForRestart();
117-
}
118-
11998
/**
12099
* The generation of the currently running process: a counter incremented every time this
121100
* manager forks a replacement. A client captures it when it connects and hands it back with
122101
* every report, so a report about a process that has already been replaced can be recognised
123102
* and dropped rather than being applied to its healthy successor.
124103
*/
125-
default long getGeneration() {
126-
return 0;
127-
}
128-
129-
/**
130-
* As {@link #markServerForRestart(RestartReason)}, but only if {@code generation} is still
131-
* current. Reports about a superseded process are dropped.
132-
*/
133-
default void markServerForRestart(RestartReason reason, long generation) {
134-
markServerForRestart(reason);
135-
}
104+
long getGeneration();
136105

137106
/**
138-
* The reasonless spelling of the above, kept for callers that cannot attribute the failure.
139-
* Routed through the reason form rather than the bare no-arg default: that default exists
140-
* only to keep pre-RestartReason implementations working, and delegating here would leave
141-
* this silently inert for any implementation that overrides only the reason form.
142-
*/
143-
default void markServerForRestart(long generation) {
144-
markServerForRestart(RestartReason.CRASH, generation);
145-
}
146-
147-
/**
148-
* As {@link #handleCrashAndGetExitCode()}, but only if {@code generation} is still current.
107+
* Marks the server for restart due to a fatal error, attributed to {@code reason}, but only
108+
* if {@code generation} is still current -- reports about a superseded process are dropped.
109+
* <p>
110+
* Called by a client that received a fatal status: the process is stopping even if
111+
* {@link #isRunning()} still says otherwise, and the next {@link #ensureRunning()} waits for
112+
* it to exit and restarts it.
113+
* <p>
114+
* Deliberately the only spelling, and deliberately abstract. Earlier revisions offered a
115+
* no-arg and a reasonless form defaulting to one another; an implementation that overrode
116+
* only one left the others silently inert, which is how a worker known to be poisoned kept
117+
* being handed documents.
149118
*/
150-
default int handleCrashAndGetExitCode(long generation) {
151-
return handleCrashAndGetExitCode();
152-
}
119+
void markServerForRestart(RestartReason reason, long generation);
153120

154121
/** Restarts performed so far for {@code reason}; monotonic, never reset. */
155122
default long getRestartCount(RestartReason reason) {
@@ -205,9 +172,6 @@ default boolean needsRestart() {
205172
*
206173
* @return the exit code if available, or -1 if the process is still running or unavailable
207174
*/
208-
default int handleCrashAndGetExitCode() {
209-
markServerForRestart(RestartReason.CRASH);
210-
return -1;
211-
}
175+
int handleCrashAndGetExitCode(long generation);
212176

213177
}

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

Lines changed: 0 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -169,19 +169,6 @@ public void ensureRunning() throws IOException, InterruptedException, TimeoutExc
169169
* Called by a client that received OOM or TIMEOUT: the process is exiting even if
170170
* isRunning() still says otherwise; the next ensureRunning() restarts it.
171171
*/
172-
@Override
173-
public void markServerForRestart() {
174-
markServerForRestart(RestartReason.CRASH);
175-
}
176-
177-
@Override
178-
public void markServerForRestart(RestartReason reason) {
179-
synchronized (lock) {
180-
LOG.debug("Server marked for restart ({}) - will restart on next ensureRunning()", reason);
181-
markForRestart(reason);
182-
}
183-
}
184-
185172
@Override
186173
public void markServerForRestart(RestartReason reason, long generation) {
187174
synchronized (lock) {
@@ -228,15 +215,6 @@ public long getRestartCount(RestartReason reason) {
228215
}
229216

230217
/** Another client may already have attributed this crash (OOM/TIMEOUT); don't overwrite it. */
231-
@Override
232-
public int handleCrashAndGetExitCode() {
233-
synchronized (lock) {
234-
restarts.markIfUnmarked(RestartReason.CRASH);
235-
pendingRestart = true;
236-
}
237-
return -1;
238-
}
239-
240218
@Override
241219
public int handleCrashAndGetExitCode(long generation) {
242220
synchronized (lock) {
Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
/*
2+
* Licensed to the Apache Software Foundation (ASF) under one or more
3+
* contributor license agreements. See the NOTICE file distributed with
4+
* this work for additional information regarding copyright ownership.
5+
* The ASF licenses this file to You under the Apache License, Version 2.0
6+
* (the "License"); you may not use this file except in compliance with
7+
* the License. You may obtain a copy of the License at
8+
*
9+
* http://www.apache.org/licenses/LICENSE-2.0
10+
*
11+
* Unless required by applicable law or agreed to in writing, software
12+
* distributed under the License is distributed on an "AS IS" BASIS,
13+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14+
* See the License for the specific language governing permissions and
15+
* limitations under the License.
16+
*/
17+
package org.apache.tika.pipes.core;
18+
19+
import static org.junit.jupiter.api.Assertions.assertEquals;
20+
import static org.junit.jupiter.api.Assertions.assertFalse;
21+
import static org.junit.jupiter.api.Assertions.assertNull;
22+
import static org.junit.jupiter.api.Assertions.assertTrue;
23+
24+
import java.net.ServerSocket;
25+
26+
import org.junit.jupiter.api.Test;
27+
import org.junit.jupiter.api.Timeout;
28+
29+
import org.apache.tika.metadata.Metadata;
30+
import org.apache.tika.parser.ParseContext;
31+
import org.apache.tika.pipes.api.FetchEmitTuple;
32+
import org.apache.tika.pipes.api.PipesResult;
33+
import org.apache.tika.pipes.api.emitter.EmitKey;
34+
import org.apache.tika.pipes.api.fetcher.FetchKey;
35+
36+
public class PipesClientClosedManagerTest {
37+
38+
/**
39+
* A request that reaches initialization after its manager was closed (a parse racing
40+
* PipesParser.close()/AsyncProcessor.close()) must come back as FAILED_TO_INITIALIZE
41+
* rather than escaping as an unchecked IllegalStateException -- and must not mark a
42+
* worker for restart, since there is nothing left to restart.
43+
*/
44+
@Test
45+
@Timeout(30)
46+
public void closedManagerDuringInitReturnsFailedToInitialize() throws Exception {
47+
try (ServerSocket serverSocket = new ServerSocket(0)) {
48+
SentinelServerManager manager = new SentinelServerManager(serverSocket.getLocalPort());
49+
manager.closed = true;
50+
try (PipesClient client = new PipesClient(new PipesConfig(), manager)) {
51+
PipesResult result = client.process(new FetchEmitTuple("closed-manager-test",
52+
new FetchKey("fetcher", "key"), new EmitKey(), new Metadata(),
53+
new ParseContext(), FetchEmitTuple.ON_PARSE_EXCEPTION.SKIP));
54+
55+
assertEquals(PipesResult.RESULT_STATUS.FAILED_TO_INITIALIZE, result.status(),
56+
"got: " + result.status() + " / " + result.message());
57+
assertTrue(result.message().contains("closed"),
58+
"message should carry the manager's reason, got: " + result.message());
59+
assertNull(manager.marked, "nothing to restart on a closed manager");
60+
assertFalse(manager.abandoned, "no connection was established to abandon");
61+
}
62+
}
63+
}
64+
}

tika-pipes/tika-pipes-core/src/test/java/org/apache/tika/pipes/core/PipesClientInterruptTest.java

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
*/
1717
package org.apache.tika.pipes.core;
1818

19+
import static org.junit.jupiter.api.Assertions.assertNull;
1920
import static org.junit.jupiter.api.Assertions.assertTrue;
2021

2122
import java.io.DataInputStream;
@@ -96,6 +97,11 @@ public void interruptClosesTheConnection() throws Exception {
9697
assertTrue(manager.abandoned,
9798
"the manager was not told; a per-client worker never dials back, so the "
9899
+ "next connect() would wait out the accept timeout for nothing");
100+
// Recycling on an abandoned connection travels connectionAbandoned(), which the
101+
// real managers attribute to CONNECTION_ABANDONED. Marking here too would double
102+
// count the restart and overwrite that reason with a less specific one.
103+
assertNull(manager.marked,
104+
"an interrupt must recycle via connectionAbandoned(), not by marking");
99105
}
100106
}
101107
}

tika-pipes/tika-pipes-core/src/test/java/org/apache/tika/pipes/core/PipesClientPayloadLimitTest.java

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818

1919
import static org.junit.jupiter.api.Assertions.assertEquals;
2020
import static org.junit.jupiter.api.Assertions.assertFalse;
21+
import static org.junit.jupiter.api.Assertions.assertNull;
2122
import static org.junit.jupiter.api.Assertions.assertTrue;
2223

2324
import java.io.DataInputStream;
@@ -72,6 +73,9 @@ public void oversizedRequestFailsFastWithoutSending() throws Exception {
7273
assertTrue(result.message().contains("maxIpcPayloadBytes"),
7374
"message should name the limit, got: " + result.message());
7475
assertFalse(manager.abandoned, "nothing was sent; no reason to abandon");
76+
assertNull(manager.marked,
77+
"the request was refused before anything was written; the worker is "
78+
+ "healthy and must not be recycled");
7579
assertFalse(connectionClosed.await(300, TimeUnit.MILLISECONDS),
7680
"nothing was sent; the connection must stay usable");
7781
}

tika-pipes/tika-pipes-core/src/test/java/org/apache/tika/pipes/core/SentinelServerManager.java

Lines changed: 22 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,9 @@
2626
final class SentinelServerManager implements ServerManager {
2727
private final int port;
2828
volatile boolean abandoned;
29+
volatile RestartReason marked;
30+
/** When set, {@link #ensureRunning()} throws like a real manager that has been closed. */
31+
volatile boolean closed;
2932

3033
SentinelServerManager(int port) {
3134
this.port = port;
@@ -43,6 +46,9 @@ public int getPort() {
4346

4447
@Override
4548
public void ensureRunning() {
49+
if (closed) {
50+
throw new IllegalStateException("sentinel server manager is closed");
51+
}
4652
// the scripted server is already listening
4753
}
4854

@@ -68,8 +74,23 @@ public Path getTempDirectory() {
6874
return null;
6975
}
7076

77+
@Override
78+
public long getGeneration() {
79+
return 0;
80+
}
81+
82+
@Override
83+
public void markServerForRestart(RestartReason reason, long generation) {
84+
marked = reason;
85+
}
86+
87+
@Override
88+
public int handleCrashAndGetExitCode(long generation) {
89+
return -1;
90+
}
91+
7192
@Override
7293
public void close() {
73-
// nothing to close
94+
closed = true;
7495
}
7596
}

0 commit comments

Comments
 (0)