@@ -259,6 +259,84 @@ public void testCompatibilityDefaultsDoNotBypassCustomTransportOwnership() {
259259 0 , socketCloseCount .get ());
260260 }
261261
262+ @ Test (timeout = 30_000L )
263+ public void testPlainSocketShutdownAfterPeerDisconnectRetainsFd () throws Exception {
264+ Socket socket = new PlainSocket (NF , LoggerFactory .getLogger (SocketTrafficShutdownTest .class ));
265+
266+ long buffer = 0 ;
267+ int fd = -1 ;
268+ try (ServerSocket listener = new ServerSocket ()) {
269+ listener .bind (new InetSocketAddress ("127.0.0.1" , 0 ));
270+ long addrInfo = NF .getAddrInfo ("127.0.0.1" , listener .getLocalPort ());
271+ Assert .assertNotEquals (-1L , addrInfo );
272+ try {
273+ fd = NF .socketTcp (true );
274+ Assert .assertTrue ("could not allocate client socket" , fd >= 0 );
275+ Assert .assertEquals (0 , NF .connectAddrInfo (fd , addrInfo ));
276+ } finally {
277+ NF .freeAddrInfo (addrInfo );
278+ }
279+
280+ try (java .net .Socket peer = listener .accept ()) {
281+ socket .of (fd );
282+ int retainedFd = fd ;
283+ fd = -1 ;
284+ buffer = Unsafe .malloc (1 , MemoryTag .NATIVE_DEFAULT );
285+
286+ peer .close ();
287+ Assert .assertTrue ("client must observe the peer disconnect" , socket .recv (buffer , 1 ) < 0 );
288+
289+ socket .closeTraffic ();
290+ Assert .assertEquals ("traffic cancellation must retain fd ownership" , retainedFd , socket .getFd ());
291+ Assert .assertFalse ("traffic cancellation must not perform full close" , socket .isClosed ());
292+ Assert .assertTrue ("shutdown must leave the descriptor allocated" , NF .getSndBuf (retainedFd ) > 0 );
293+
294+ socket .close ();
295+ Assert .assertTrue ("full close must release the retained fd" , socket .isClosed ());
296+ Assert .assertEquals ("released descriptor must reject socket operations" , -1 , NF .getSndBuf (retainedFd ));
297+ }
298+ } finally {
299+ socket .close ();
300+ if (buffer != 0 ) {
301+ Unsafe .free (buffer , 1 , MemoryTag .NATIVE_DEFAULT );
302+ }
303+ if (fd != -1 ) {
304+ NF .close (fd );
305+ }
306+ }
307+ }
308+
309+ @ Test
310+ public void testPlainSocketShutdownFailureStillThrows () {
311+ AtomicInteger closeCount = new AtomicInteger ();
312+ NetworkFacade failingFacade = new CompatibilityNetworkFacade (closeCount ) {
313+ @ Override
314+ public int errno () {
315+ return 1234 ;
316+ }
317+
318+ @ Override
319+ public int shutdown (int fd ) {
320+ Assert .assertEquals (42 , fd );
321+ return -1 ;
322+ }
323+ };
324+ PlainSocket socket = new PlainSocket (failingFacade , LoggerFactory .getLogger (SocketTrafficShutdownTest .class ));
325+ socket .of (42 );
326+
327+ try {
328+ socket .closeTraffic ();
329+ Assert .fail ("expected genuine traffic shutdown failure" );
330+ } catch (IllegalStateException expected ) {
331+ Assert .assertEquals ("could not shut down socket traffic [fd=42, errno=1234]" , expected .getMessage ());
332+ } finally {
333+ socket .close ();
334+ }
335+
336+ Assert .assertTrue (socket .isClosed ());
337+ Assert .assertEquals ("full close must release facade ownership exactly once" , 1 , closeCount .get ());
338+ }
339+
262340 @ Test (timeout = 30_000L )
263341 public void testPlainSocketShutdownWakesMacOsKqueueAndRetainsFd () throws Exception {
264342 assertShutdownWakesMacOsKqueue (new PlainSocket (NF , LoggerFactory .getLogger (SocketTrafficShutdownTest .class )));
0 commit comments