Skip to content

Commit ba501e6

Browse files
authored
fix race condition in PipesClient (#2849)
1 parent acab9b7 commit ba501e6

1 file changed

Lines changed: 50 additions & 18 deletions

File tree

  • tika-pipes/tika-pipes-core/src/main/java/org/apache/tika/pipes/core

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

Lines changed: 50 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -76,7 +76,11 @@ public class PipesClient implements Closeable {
7676
private final boolean ownsServerManager;
7777
private final int pipesClientId;
7878

79-
private ConnectionTuple connectionTuple;
79+
// volatile + connectionLock: closeConnection() can be called concurrently by the
80+
// in-flight parse thread (timeout/crash paths in waitForServer) and the thread
81+
// calling close(). The lock lets one thread atomically claim and null the tuple.
82+
private final Object connectionLock = new Object();
83+
private volatile ConnectionTuple connectionTuple;
8084
private int filesProcessed = 0;
8185

8286
/**
@@ -118,16 +122,20 @@ public int getFilesProcessed() {
118122
}
119123

120124
private boolean ping() {
121-
if (connectionTuple == null) {
125+
// Snapshot the volatile once: a concurrent closeConnection() can null the
126+
// field at any point, but the local reference stays valid (close() unblocks
127+
// us by closing the socket, surfacing as IOException below - not an NPE).
128+
ConnectionTuple tuple = connectionTuple;
129+
if (tuple == null) {
122130
return false;
123131
}
124132
// Check if server process is still running
125133
if (!serverManager.isRunning()) {
126134
return false;
127135
}
128136
try {
129-
PipesMessage.ping().write(connectionTuple.output);
130-
PipesMessage response = PipesMessage.read(connectionTuple.input);
137+
PipesMessage.ping().write(tuple.output);
138+
PipesMessage response = PipesMessage.read(tuple.input);
131139
if (response.type() == PipesMessageType.PING) {
132140
return true;
133141
}
@@ -158,20 +166,27 @@ public int getPipesClientId() {
158166
* Server lifecycle is managed by PipesParser.
159167
*/
160168
private void closeConnection() throws InterruptedException {
161-
if (connectionTuple == null) {
169+
// Atomically claim the tuple and null the field so concurrent callers
170+
// (parse thread vs. close() thread) don't deref a field another thread
171+
// has already nulled. Whoever loses the race sees a null tuple and bails.
172+
ConnectionTuple tuple;
173+
synchronized (connectionLock) {
174+
tuple = connectionTuple;
175+
connectionTuple = null;
176+
}
177+
if (tuple == null) {
162178
return;
163179
}
164180
LOG.debug("pipesClientId={}: closing connection", pipesClientId);
165181
try {
166-
PipesMessage.shutDown().write(connectionTuple.output);
182+
PipesMessage.shutDown().write(tuple.output);
167183
} catch (IOException e) {
168184
// swallow
169185
}
170186
List<IOException> exceptions = new ArrayList<>();
171-
tryToClose(connectionTuple.input, exceptions);
172-
tryToClose(connectionTuple.output, exceptions);
173-
tryToClose(connectionTuple.socket, exceptions);
174-
connectionTuple = null;
187+
tryToClose(tuple.input, exceptions);
188+
tryToClose(tuple.output, exceptions);
189+
tryToClose(tuple.socket, exceptions);
175190
}
176191

177192
private void tryToClose(Closeable closeable, List<IOException> exceptions) {
@@ -292,20 +307,33 @@ private void reconnect() throws InterruptedException, IOException, TimeoutExcept
292307
// Connect to server
293308
Socket socket = serverManager.connect((int) pipesConfig.getSocketTimeoutMs());
294309

295-
connectionTuple = new ConnectionTuple(socket,
296-
new DataInputStream(new BufferedInputStream(socket.getInputStream())),
297-
new DataOutputStream(new BufferedOutputStream(socket.getOutputStream())));
310+
synchronized (connectionLock) {
311+
connectionTuple = new ConnectionTuple(socket,
312+
new DataInputStream(new BufferedInputStream(socket.getInputStream())),
313+
new DataOutputStream(new BufferedOutputStream(socket.getOutputStream())));
314+
}
298315

299316
waitForStartup();
300317
}
301318

302319
private void writeTask(FetchEmitTuple t) throws IOException {
320+
ConnectionTuple tuple = connectionTuple;
321+
if (tuple == null) {
322+
throw new IOException("connection closed");
323+
}
303324
LOG.debug("pipesClientId={}: sending NEW_REQUEST for id={}", pipesClientId, t.getId());
304325
byte[] bytes = JsonPipesIpc.toBytes(t);
305-
PipesMessage.newRequest(bytes).write(connectionTuple.output);
326+
PipesMessage.newRequest(bytes).write(tuple.output);
306327
}
307328

308329
private PipesResult waitForServer(FetchEmitTuple t, IntermediateResult intermediateResult) throws InterruptedException {
330+
// Snapshot the volatile once; a concurrent close() may null the field, but the
331+
// local stays valid and its blocking read unblocks via socket close (IOException).
332+
ConnectionTuple tuple = connectionTuple;
333+
if (tuple == null) {
334+
return buildFatalResult(t.getId(), t.getEmitKey(), UNSPECIFIED_CRASH,
335+
intermediateResult.get());
336+
}
309337
TimeoutLimits limits = TimeoutLimits.get(t.getParseContext());
310338
long progressTimeoutMillis = limits.getProgressTimeoutMillis();
311339
long totalTaskTimeoutMillis = limits.getTotalTaskTimeoutMillis();
@@ -337,12 +365,12 @@ private PipesResult waitForServer(FetchEmitTuple t, IntermediateResult intermedi
337365
intermediateResult.get());
338366
}
339367
try {
340-
PipesMessage msg = PipesMessage.read(connectionTuple.input);
368+
PipesMessage msg = PipesMessage.read(tuple.input);
341369
LOG.trace("clientId={}: received message type={} id={}", pipesClientId, msg.type(), t.getId());
342370

343371
// Send ACK only for messages that require it
344372
if (msg.type().requiresAck()) {
345-
PipesMessage.ack().write(connectionTuple.output);
373+
PipesMessage.ack().write(tuple.output);
346374
}
347375

348376
switch (msg.type()) {
@@ -429,12 +457,16 @@ private PipesResult buildFatalResult(String id, EmitKey emitKey, PipesResult.RES
429457
}
430458

431459
private void waitForStartup() throws IOException {
432-
PipesMessage msg = PipesMessage.read(connectionTuple.input);
460+
ConnectionTuple tuple = connectionTuple;
461+
if (tuple == null) {
462+
throw new IOException("connection closed");
463+
}
464+
PipesMessage msg = PipesMessage.read(tuple.input);
433465
if (msg.type() == PipesMessageType.READY) {
434466
LOG.info("clientId={}: server successfully started", pipesClientId);
435467
} else if (msg.type() == PipesMessageType.STARTUP_FAILED) {
436468
// Send ACK for startup failure
437-
PipesMessage.ack().write(connectionTuple.output);
469+
PipesMessage.ack().write(tuple.output);
438470
String errorMsg = new String(msg.payload(), StandardCharsets.UTF_8);
439471
LOG.error("clientId={}: Server failed to start: {}", pipesClientId, errorMsg);
440472
throw new ServerInitializationException(errorMsg);

0 commit comments

Comments
 (0)