@@ -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