@@ -170,10 +170,7 @@ public void testCloseDoesNotFreePathScratchWhenWorkerStillAlive() throws Excepti
170170 Assert .assertTrue ("precondition: path scratch should be allocated" ,
171171 readPathScratchImpl (manager ) != 0L );
172172
173- // Exercise the same branch as a timed-out join without making
174- // the test sleep for 5 seconds: join() returns while the worker
175- // is still alive. close() must leave worker-owned native memory
176- // alone so the worker can resume safely.
173+ manager .setWorkerJoinTimeoutMillis (50L );
177174 Thread .currentThread ().interrupt ();
178175 manager .close ();
179176 Assert .assertTrue ("close should preserve interrupted status" ,
@@ -185,6 +182,7 @@ public void testCloseDoesNotFreePathScratchWhenWorkerStillAlive() throws Excepti
185182 readPathScratchImpl (manager ) != 0L );
186183
187184 releaseWorker .countDown ();
185+ manager .setWorkerJoinTimeoutMillis (TimeUnit .SECONDS .toMillis (60 ));
188186 manager .close ();
189187 managerClosed = true ;
190188 Assert .assertNull ("successful close should clear workerThread" ,
@@ -206,6 +204,85 @@ public void testCloseDoesNotFreePathScratchWhenWorkerStillAlive() throws Excepti
206204 });
207205 }
208206
207+ @ Test (timeout = 15_000L )
208+ public void testInterruptedCallerDoesNotAbandonReapableWorker () throws Exception {
209+ TestUtils .assertMemoryLeak (() -> {
210+ long segSize = MmapSegment .HEADER_SIZE + (MmapSegment .FRAME_HEADER_SIZE + 32 );
211+ String slot = tmpDir + "/interrupt-slot" ;
212+ Assert .assertEquals (0 , Files .mkdir (slot , Files .DIR_MODE_DEFAULT ));
213+ MmapSegment initial = MmapSegment .create (slot + "/sf-initial.sfa" , 0L , segSize );
214+ SegmentRing ring = new SegmentRing (initial , segSize );
215+ SegmentManager manager = new SegmentManager (segSize , TimeUnit .SECONDS .toNanos (60 ));
216+ CountDownLatch workerBlocked = new CountDownLatch (1 );
217+ CountDownLatch releaseWorker = new CountDownLatch (1 );
218+ CountDownLatch closeReturned = new CountDownLatch (1 );
219+ AtomicBoolean fired = new AtomicBoolean ();
220+ AtomicBoolean interruptPreserved = new AtomicBoolean ();
221+ AtomicReference <Throwable > hookErr = new AtomicReference <>();
222+ AtomicReference <Throwable > closeErr = new AtomicReference <>();
223+ boolean managerClosed = false ;
224+ try {
225+ manager .register (ring , slot );
226+ manager .setBeforeInstallSyncHook (() -> {
227+ if (!fired .compareAndSet (false , true )) return ;
228+ workerBlocked .countDown ();
229+ try {
230+ if (!releaseWorker .await (10 , TimeUnit .SECONDS )) {
231+ hookErr .compareAndSet (null ,
232+ new AssertionError ("timed out waiting for test to release worker" ));
233+ }
234+ } catch (Throwable t ) {
235+ hookErr .compareAndSet (null , t );
236+ }
237+ });
238+ manager .start ();
239+ Assert .assertTrue ("worker did not reach install hook" ,
240+ workerBlocked .await (5 , TimeUnit .SECONDS ));
241+
242+ Thread closer = new Thread (() -> {
243+ Thread .currentThread ().interrupt ();
244+ try {
245+ manager .close ();
246+ } catch (Throwable t ) {
247+ closeErr .compareAndSet (null , t );
248+ } finally {
249+ interruptPreserved .set (Thread .currentThread ().isInterrupted ());
250+ closeReturned .countDown ();
251+ }
252+ }, "interrupted-closer" );
253+ closer .start ();
254+
255+ Assert .assertFalse ("interrupted close() abandoned a live worker instead of waiting" ,
256+ closeReturned .await (300 , TimeUnit .MILLISECONDS ));
257+
258+ releaseWorker .countDown ();
259+ Assert .assertTrue ("close() never returned after the worker was released" ,
260+ closeReturned .await (10 , TimeUnit .SECONDS ));
261+ closer .join (TimeUnit .SECONDS .toMillis (5 ));
262+ managerClosed = readWorkerThread (manager ) == null ;
263+
264+ if (closeErr .get () != null ) {
265+ throw new AssertionError ("close() threw" , closeErr .get ());
266+ }
267+ Assert .assertNull ("close() must reap the worker despite the pending interrupt" ,
268+ readWorkerThread (manager ));
269+ Assert .assertTrue ("close() must restore the caller's interrupt status" ,
270+ interruptPreserved .get ());
271+ if (hookErr .get () != null ) {
272+ throw new AssertionError ("install hook failed" , hookErr .get ());
273+ }
274+ } finally {
275+ manager .setBeforeInstallSyncHook (null );
276+ releaseWorker .countDown ();
277+ if (!managerClosed ) {
278+ Thread .interrupted ();
279+ manager .close ();
280+ }
281+ ring .close ();
282+ }
283+ });
284+ }
285+
209286 private static void cleanupRecursively (String dir ) {
210287 if (!Files .exists (dir )) return ;
211288 long find = Files .findFirst (dir );
0 commit comments