@@ -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 {
0 commit comments