| /* | |
| * Copyright (c) 2018, 2025, Oracle and/or its affiliates. All rights reserved. | |
| * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. | |
| * | |
| * This code is free software; you can redistribute it and/or modify it | |
| * under the terms of the GNU General Public License version 2 only, as | |
| * published by the Free Software Foundation. | |
| * | |
| * This code is distributed in the hope that it will be useful, but WITHOUT | |
| * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or | |
| * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License | |
| * version 2 for more details (a copy is included in the LICENSE file that | |
| * accompanied this code). | |
| * | |
| * You should have received a copy of the GNU General Public License version | |
| * 2 along with this work; if not, write to the Free Software Foundation, | |
| * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. | |
| * | |
| * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA | |
| * or visit www.oracle.com if you need additional information or have any | |
| * questions. | |
| */ | |
| /* | |
| * @test id=default | |
| * @bug 8284161 | |
| * @summary Test virtual threads doing blocking I/O on NIO channels | |
| * @library /test/lib | |
| * @run junit BlockingChannelOps | |
| */ | |
| /* | |
| * @test id=poller-modes | |
| * @requires (os.family == "linux") | (os.family == "mac") | |
| * @library /test/lib | |
| * @run junit/othervm -Djdk.pollerMode=1 BlockingChannelOps | |
| * @run junit/othervm -Djdk.pollerMode=2 BlockingChannelOps | |
| */ | |
| /* | |
| * @test id=no-vmcontinuations | |
| * @requires vm.continuations | |
| * @library /test/lib | |
| * @run junit/othervm -XX:+UnlockExperimentalVMOptions -XX:-VMContinuations BlockingChannelOps | |
| */ | |
| import java.io.Closeable; | |
| import java.io.IOException; | |
| import java.net.DatagramPacket; | |
| import java.net.InetAddress; | |
| import java.net.InetSocketAddress; | |
| import java.net.Socket; | |
| import java.net.SocketAddress; | |
| import java.net.SocketException; | |
| import java.nio.ByteBuffer; | |
| import java.nio.channels.AsynchronousCloseException; | |
| import java.nio.channels.ClosedByInterruptException; | |
| import java.nio.channels.ClosedChannelException; | |
| import java.nio.channels.DatagramChannel; | |
| import java.nio.channels.Pipe; | |
| import java.nio.channels.ReadableByteChannel; | |
| import java.nio.channels.ServerSocketChannel; | |
| import java.nio.channels.SocketChannel; | |
| import java.nio.channels.WritableByteChannel; | |
| import java.util.concurrent.locks.LockSupport; | |
| import jdk.test.lib.thread.VThreadRunner; | |
| import org.junit.jupiter.api.Test; | |
| import static org.junit.jupiter.api.Assertions.*; | |
| class BlockingChannelOps { | |
| /** | |
| * SocketChannel read/write, no blocking. | |
| */ | |
| @Test | |
| void testSocketChannelReadWrite1() throws Exception { | |
| VThreadRunner.run(() -> { | |
| try (var connection = new Connection()) { | |
| SocketChannel sc1 = connection.channel1(); | |
| SocketChannel sc2 = connection.channel2(); | |
| // write to sc1 | |
| ByteBuffer bb = ByteBuffer.wrap("XXX".getBytes("UTF-8")); | |
| int n = sc1.write(bb); | |
| assertTrue(n > 0); | |
| // read from sc2 should not block | |
| bb = ByteBuffer.allocate(10); | |
| n = sc2.read(bb); | |
| assertTrue(n > 0); | |
| assertTrue(bb.get(0) == 'X'); | |
| } | |
| }); | |
| } | |
| /** | |
| * Virtual thread blocks in SocketChannel read. | |
| */ | |
| @Test | |
| void testSocketChannelRead() throws Exception { | |
| VThreadRunner.run(() -> { | |
| try (var connection = new Connection()) { | |
| SocketChannel sc1 = connection.channel1(); | |
| SocketChannel sc2 = connection.channel2(); | |
| // write to sc1 when current thread blocks in sc2.read | |
| ByteBuffer bb1 = ByteBuffer.wrap("XXX".getBytes("UTF-8")); | |
| runAfterParkedAsync(() -> sc1.write(bb1)); | |
| // read from sc2 should block | |
| ByteBuffer bb2 = ByteBuffer.allocate(10); | |
| int n = sc2.read(bb2); | |
| assertTrue(n > 0); | |
| assertTrue(bb2.get(0) == 'X'); | |
| } | |
| }); | |
| } | |
| /** | |
| * Virtual thread blocks in SocketChannel write. | |
| */ | |
| @Test | |
| void testSocketChannelWrite() throws Exception { | |
| VThreadRunner.run(() -> { | |
| try (var connection = new Connection()) { | |
| SocketChannel sc1 = connection.channel1(); | |
| SocketChannel sc2 = connection.channel2(); | |
| // read from sc2 to EOF when current thread blocks in sc1.write | |
| Thread reader = runAfterParkedAsync(() -> readToEOF(sc2)); | |
| // write to sc1 should block | |
| ByteBuffer bb = ByteBuffer.allocate(100*1024); | |
| for (int i=0; i<1000; i++) { | |
| int n = sc1.write(bb); | |
| assertTrue(n > 0); | |
| bb.clear(); | |
| } | |
| sc1.close(); | |
| // wait for reader to finish | |
| reader.join(); | |
| } | |
| }); | |
| } | |
| /** | |
| * SocketChannel close while virtual thread blocked in read. | |
| */ | |
| @Test | |
| void testSocketChannelReadAsyncClose() throws Exception { | |
| VThreadRunner.run(() -> { | |
| try (var connection = new Connection()) { | |
| SocketChannel sc = connection.channel1(); | |
| runAfterParkedAsync(sc::close); | |
| try { | |
| int n = sc.read(ByteBuffer.allocate(100)); | |
| fail("read returned " + n); | |
| } catch (AsynchronousCloseException expected) { } | |
| } | |
| }); | |
| } | |
| /** | |
| * SocketChannel shutdownInput while virtual thread blocked in read. | |
| */ | |
| @Test | |
| void testSocketChannelReadAsyncShutdownInput() throws Exception { | |
| VThreadRunner.run(() -> { | |
| try (var connection = new Connection()) { | |
| SocketChannel sc = connection.channel1(); | |
| runAfterParkedAsync(sc::shutdownInput); | |
| int n = sc.read(ByteBuffer.allocate(100)); | |
| assertEquals(-1, n); | |
| assertTrue(sc.isOpen()); | |
| } | |
| }); | |
| } | |
| /** | |
| * Virtual thread interrupted while blocked in SocketChannel read. | |
| */ | |
| @Test | |
| void testSocketChannelReadInterrupt() throws Exception { | |
| VThreadRunner.run(() -> { | |
| try (var connection = new Connection()) { | |
| SocketChannel sc = connection.channel1(); | |
| // interrupt current thread when it blocks in read | |
| Thread thisThread = Thread.currentThread(); | |
| runAfterParkedAsync(thisThread::interrupt); | |
| try { | |
| int n = sc.read(ByteBuffer.allocate(100)); | |
| fail("read returned " + n); | |
| } catch (ClosedByInterruptException expected) { | |
| assertTrue(Thread.interrupted()); | |
| } | |
| } | |
| }); | |
| } | |
| /** | |
| * SocketChannel close while virtual thread blocked in write. | |
| */ | |
| @Test | |
| void testSocketChannelWriteAsyncClose() throws Exception { | |
| VThreadRunner.run(() -> { | |
| boolean done = false; | |
| while (!done) { | |
| try (var connection = new Connection()) { | |
| SocketChannel sc = connection.channel1(); | |
| // close sc when current thread blocks in write | |
| runAfterParkedAsync(sc::close, true); | |
| // write until channel is closed | |
| try { | |
| ByteBuffer bb = ByteBuffer.allocate(100*1024); | |
| for (;;) { | |
| int n = sc.write(bb); | |
| assertTrue(n > 0); | |
| bb.clear(); | |
| } | |
| } catch (AsynchronousCloseException expected) { | |
| // closed when blocked in write | |
| done = true; | |
| } catch (ClosedChannelException e) { | |
| // closed but not blocked in write, need to retry test | |
| System.err.format("%s, need to retry!%n", e); | |
| } | |
| } | |
| } | |
| }); | |
| } | |
| /** | |
| * SocketChannel shutdownOutput while virtual thread blocked in write. | |
| */ | |
| @Test | |
| void testSocketChannelWriteAsyncShutdownOutput() throws Exception { | |
| VThreadRunner.run(() -> { | |
| try (var connection = new Connection()) { | |
| SocketChannel sc = connection.channel1(); | |
| // shutdown output when current thread blocks in write | |
| runAfterParkedAsync(sc::shutdownOutput); | |
| try { | |
| ByteBuffer bb = ByteBuffer.allocate(100*1024); | |
| for (;;) { | |
| int n = sc.write(bb); | |
| assertTrue(n > 0); | |
| bb.clear(); | |
| } | |
| } catch (ClosedChannelException e) { | |
| // expected | |
| } | |
| assertTrue(sc.isOpen()); | |
| } | |
| }); | |
| } | |
| /** | |
| * Virtual thread interrupted while blocked in SocketChannel write. | |
| */ | |
| @Test | |
| void testSocketChannelWriteInterrupt() throws Exception { | |
| VThreadRunner.run(() -> { | |
| boolean done = false; | |
| while (!done) { | |
| try (var connection = new Connection()) { | |
| SocketChannel sc = connection.channel1(); | |
| // interrupt current thread when it blocks in write | |
| Thread thisThread = Thread.currentThread(); | |
| runAfterParkedAsync(thisThread::interrupt, true); | |
| // write until channel is closed | |
| try { | |
| ByteBuffer bb = ByteBuffer.allocate(100*1024); | |
| for (;;) { | |
| int n = sc.write(bb); | |
| assertTrue(n > 0); | |
| bb.clear(); | |
| } | |
| } catch (ClosedByInterruptException e) { | |
| // closed when blocked in write | |
| assertTrue(Thread.interrupted()); | |
| done = true; | |
| } catch (ClosedChannelException e) { | |
| // closed but not blocked in write, need to retry test | |
| System.err.format("%s, need to retry!%n", e); | |
| } | |
| } | |
| } | |
| }); | |
| } | |
| /** | |
| * Virtual thread blocks in SocketChannel adaptor read. | |
| */ | |
| @Test | |
| void testSocketAdaptorRead1() throws Exception { | |
| testSocketAdaptorRead(0); | |
| } | |
| /** | |
| * Virtual thread blocks in SocketChannel adaptor read with timeout. | |
| */ | |
| @Test | |
| void testSocketAdaptorRead2() throws Exception { | |
| testSocketAdaptorRead(60_000); | |
| } | |
| private void testSocketAdaptorRead(int timeout) throws Exception { | |
| VThreadRunner.run(() -> { | |
| try (var connection = new Connection()) { | |
| SocketChannel sc1 = connection.channel1(); | |
| SocketChannel sc2 = connection.channel2(); | |
| // write to sc1 when currnet thread blocks reading from sc2 | |
| ByteBuffer bb = ByteBuffer.wrap("XXX".getBytes("UTF-8")); | |
| runAfterParkedAsync(() -> sc1.write(bb)); | |
| // read from sc2 should block | |
| byte[] array = new byte[100]; | |
| if (timeout > 0) | |
| sc2.socket().setSoTimeout(timeout); | |
| int n = sc2.socket().getInputStream().read(array); | |
| assertTrue(n > 0); | |
| assertTrue(array[0] == 'X'); | |
| } | |
| }); | |
| } | |
| /** | |
| * ServerSocketChannel accept, no blocking. | |
| */ | |
| @Test | |
| void testServerSocketChannelAccept1() throws Exception { | |
| VThreadRunner.run(() -> { | |
| try (var ssc = ServerSocketChannel.open()) { | |
| ssc.bind(new InetSocketAddress(InetAddress.getLoopbackAddress(), 0)); | |
| var sc1 = SocketChannel.open(ssc.getLocalAddress()); | |
| // accept should not block | |
| var sc2 = ssc.accept(); | |
| sc1.close(); | |
| sc2.close(); | |
| } | |
| }); | |
| } | |
| /** | |
| * Virtual thread blocks in ServerSocketChannel accept. | |
| */ | |
| @Test | |
| void testServerSocketChannelAccept2() throws Exception { | |
| VThreadRunner.run(() -> { | |
| try (var ssc = ServerSocketChannel.open()) { | |
| ssc.bind(new InetSocketAddress(InetAddress.getLoopbackAddress(), 0)); | |
| var sc1 = SocketChannel.open(); | |
| // connect when current thread when it blocks in accept | |
| runAfterParkedAsync(() -> sc1.connect(ssc.getLocalAddress())); | |
| // accept should block | |
| var sc2 = ssc.accept(); | |
| sc1.close(); | |
| sc2.close(); | |
| } | |
| }); | |
| } | |
| /** | |
| * SeverSocketChannel close while virtual thread blocked in accept. | |
| */ | |
| @Test | |
| void testServerSocketChannelAcceptAsyncClose() throws Exception { | |
| VThreadRunner.run(() -> { | |
| try (var ssc = ServerSocketChannel.open()) { | |
| InetAddress lh = InetAddress.getLoopbackAddress(); | |
| ssc.bind(new InetSocketAddress(lh, 0)); | |
| runAfterParkedAsync(ssc::close); | |
| try { | |
| SocketChannel sc = ssc.accept(); | |
| sc.close(); | |
| fail("connection accepted???"); | |
| } catch (AsynchronousCloseException expected) { } | |
| } | |
| }); | |
| } | |
| /** | |
| * Virtual thread interrupted while blocked in ServerSocketChannel accept. | |
| */ | |
| @Test | |
| void testServerSocketChannelAcceptInterrupt() throws Exception { | |
| VThreadRunner.run(() -> { | |
| try (var ssc = ServerSocketChannel.open()) { | |
| InetAddress lh = InetAddress.getLoopbackAddress(); | |
| ssc.bind(new InetSocketAddress(lh, 0)); | |
| // interrupt current thread when it blocks in accept | |
| Thread thisThread = Thread.currentThread(); | |
| runAfterParkedAsync(thisThread::interrupt); | |
| try { | |
| SocketChannel sc = ssc.accept(); | |
| sc.close(); | |
| fail("connection accepted???"); | |
| } catch (ClosedByInterruptException expected) { | |
| assertTrue(Thread.interrupted()); | |
| } | |
| } | |
| }); | |
| } | |
| /** | |
| * Virtual thread blocks in ServerSocketChannel adaptor accept. | |
| */ | |
| @Test | |
| void testSocketChannelAdaptorAccept1() throws Exception { | |
| testSocketChannelAdaptorAccept(0); | |
| } | |
| /** | |
| * Virtual thread blocks in ServerSocketChannel adaptor accept with timeout. | |
| */ | |
| @Test | |
| void testSocketChannelAdaptorAccept2() throws Exception { | |
| testSocketChannelAdaptorAccept(60_000); | |
| } | |
| private void testSocketChannelAdaptorAccept(int timeout) throws Exception { | |
| VThreadRunner.run(() -> { | |
| try (var ssc = ServerSocketChannel.open()) { | |
| ssc.bind(new InetSocketAddress(InetAddress.getLoopbackAddress(), 0)); | |
| var sc = SocketChannel.open(); | |
| // interrupt current thread when it blocks in accept | |
| runAfterParkedAsync(() -> sc.connect(ssc.getLocalAddress())); | |
| // accept should block | |
| if (timeout > 0) | |
| ssc.socket().setSoTimeout(timeout); | |
| Socket s = ssc.socket().accept(); | |
| sc.close(); | |
| s.close(); | |
| } | |
| }); | |
| } | |
| /** | |
| * DatagramChannel receive/send, no blocking. | |
| */ | |
| @Test | |
| void testDatagramChannelSendReceive1() throws Exception { | |
| VThreadRunner.run(() -> { | |
| try (DatagramChannel dc1 = DatagramChannel.open(); | |
| DatagramChannel dc2 = DatagramChannel.open()) { | |
| InetAddress lh = InetAddress.getLoopbackAddress(); | |
| dc2.bind(new InetSocketAddress(lh, 0)); | |
| // send should not block | |
| ByteBuffer bb = ByteBuffer.wrap("XXX".getBytes("UTF-8")); | |
| int n = dc1.send(bb, dc2.getLocalAddress()); | |
| assertTrue(n > 0); | |
| // receive should not block | |
| bb = ByteBuffer.allocate(10); | |
| dc2.receive(bb); | |
| assertTrue(bb.get(0) == 'X'); | |
| } | |
| }); | |
| } | |
| /** | |
| * Virtual thread blocks in DatagramChannel receive. | |
| */ | |
| @Test | |
| void testDatagramChannelSendReceive2() throws Exception { | |
| VThreadRunner.run(() -> { | |
| try (DatagramChannel dc1 = DatagramChannel.open(); | |
| DatagramChannel dc2 = DatagramChannel.open()) { | |
| InetAddress lh = InetAddress.getLoopbackAddress(); | |
| dc2.bind(new InetSocketAddress(lh, 0)); | |
| // send from dc1 when current thread blocked in dc2.receive | |
| ByteBuffer bb1 = ByteBuffer.wrap("XXX".getBytes("UTF-8")); | |
| runAfterParkedAsync(() -> dc1.send(bb1, dc2.getLocalAddress())); | |
| // read from dc2 should block | |
| ByteBuffer bb2 = ByteBuffer.allocate(10); | |
| dc2.receive(bb2); | |
| assertTrue(bb2.get(0) == 'X'); | |
| } | |
| }); | |
| } | |
| /** | |
| * DatagramChannel close while virtual thread blocked in receive. | |
| */ | |
| @Test | |
| void testDatagramChannelReceiveAsyncClose() throws Exception { | |
| VThreadRunner.run(() -> { | |
| try (DatagramChannel dc = DatagramChannel.open()) { | |
| InetAddress lh = InetAddress.getLoopbackAddress(); | |
| dc.bind(new InetSocketAddress(lh, 0)); | |
| runAfterParkedAsync(dc::close); | |
| try { | |
| dc.receive(ByteBuffer.allocate(100)); | |
| fail("receive returned"); | |
| } catch (AsynchronousCloseException expected) { } | |
| } | |
| }); | |
| } | |
| /** | |
| * Virtual thread interrupted while blocked in DatagramChannel receive. | |
| */ | |
| @Test | |
| void testDatagramChannelReceiveInterrupt() throws Exception { | |
| VThreadRunner.run(() -> { | |
| try (DatagramChannel dc = DatagramChannel.open()) { | |
| InetAddress lh = InetAddress.getLoopbackAddress(); | |
| dc.bind(new InetSocketAddress(lh, 0)); | |
| // interrupt current thread when it blocks in receive | |
| Thread thisThread = Thread.currentThread(); | |
| runAfterParkedAsync(thisThread::interrupt); | |
| try { | |
| dc.receive(ByteBuffer.allocate(100)); | |
| fail("receive returned"); | |
| } catch (ClosedByInterruptException expected) { | |
| assertTrue(Thread.interrupted()); | |
| } | |
| } | |
| }); | |
| } | |
| /** | |
| * Virtual thread blocks in DatagramSocket adaptor receive. | |
| */ | |
| @Test | |
| void testDatagramSocketAdaptorReceive1() throws Exception { | |
| testDatagramSocketAdaptorReceive(0); | |
| } | |
| /** | |
| * Virtual thread blocks in DatagramSocket adaptor receive with timeout. | |
| */ | |
| @Test | |
| void testDatagramSocketAdaptorReceive2() throws Exception { | |
| testDatagramSocketAdaptorReceive(60_000); | |
| } | |
| private void testDatagramSocketAdaptorReceive(int timeout) throws Exception { | |
| VThreadRunner.run(() -> { | |
| try (DatagramChannel dc1 = DatagramChannel.open(); | |
| DatagramChannel dc2 = DatagramChannel.open()) { | |
| InetAddress lh = InetAddress.getLoopbackAddress(); | |
| dc2.bind(new InetSocketAddress(lh, 0)); | |
| // send from dc1 when current thread blocks in dc2 receive | |
| ByteBuffer bb = ByteBuffer.wrap("XXX".getBytes("UTF-8")); | |
| runAfterParkedAsync(() -> dc1.send(bb, dc2.getLocalAddress())); | |
| // receive should block | |
| byte[] array = new byte[100]; | |
| DatagramPacket p = new DatagramPacket(array, 0, array.length); | |
| if (timeout > 0) | |
| dc2.socket().setSoTimeout(timeout); | |
| dc2.socket().receive(p); | |
| assertTrue(p.getLength() == 3 && array[0] == 'X'); | |
| } | |
| }); | |
| } | |
| /** | |
| * DatagramChannel close while virtual thread blocked in adaptor receive. | |
| */ | |
| @Test | |
| void testDatagramSocketAdaptorReceiveAsyncClose1() throws Exception { | |
| testDatagramSocketAdaptorReceiveAsyncClose(0); | |
| } | |
| /** | |
| * DatagramChannel close while virtual thread blocked in adaptor receive | |
| * with timeout. | |
| */ | |
| @Test | |
| void testDatagramSocketAdaptorReceiveAsyncClose2() throws Exception { | |
| testDatagramSocketAdaptorReceiveAsyncClose(60_1000); | |
| } | |
| private void testDatagramSocketAdaptorReceiveAsyncClose(int timeout) throws Exception { | |
| VThreadRunner.run(() -> { | |
| try (DatagramChannel dc = DatagramChannel.open()) { | |
| InetAddress lh = InetAddress.getLoopbackAddress(); | |
| dc.bind(new InetSocketAddress(lh, 0)); | |
| byte[] array = new byte[100]; | |
| DatagramPacket p = new DatagramPacket(array, 0, array.length); | |
| if (timeout > 0) | |
| dc.socket().setSoTimeout(timeout); | |
| // close channel/socket when current thread blocks in receive | |
| runAfterParkedAsync(dc::close); | |
| assertThrows(SocketException.class, () -> dc.socket().receive(p)); | |
| } | |
| }); | |
| } | |
| /** | |
| * Virtual thread interrupted while blocked in DatagramSocket adaptor receive. | |
| */ | |
| @Test | |
| void testDatagramSocketAdaptorReceiveInterrupt1() throws Exception { | |
| testDatagramSocketAdaptorReceiveInterrupt(0); | |
| } | |
| /** | |
| * Virtual thread interrupted while blocked in DatagramSocket adaptor receive | |
| * with timeout. | |
| */ | |
| @Test | |
| void testDatagramSocketAdaptorReceiveInterrupt2() throws Exception { | |
| testDatagramSocketAdaptorReceiveInterrupt(60_1000); | |
| } | |
| private void testDatagramSocketAdaptorReceiveInterrupt(int timeout) throws Exception { | |
| VThreadRunner.run(() -> { | |
| try (DatagramChannel dc = DatagramChannel.open()) { | |
| InetAddress lh = InetAddress.getLoopbackAddress(); | |
| dc.bind(new InetSocketAddress(lh, 0)); | |
| byte[] array = new byte[100]; | |
| DatagramPacket p = new DatagramPacket(array, 0, array.length); | |
| if (timeout > 0) | |
| dc.socket().setSoTimeout(timeout); | |
| // interrupt current thread when it blocks in receive | |
| Thread thisThread = Thread.currentThread(); | |
| runAfterParkedAsync(thisThread::interrupt); | |
| try { | |
| dc.socket().receive(p); | |
| fail(); | |
| } catch (ClosedByInterruptException expected) { | |
| assertTrue(Thread.interrupted()); | |
| } | |
| } | |
| }); | |
| } | |
| /** | |
| * Pipe read/write, no blocking. | |
| */ | |
| @Test | |
| void testPipeReadWrite1() throws Exception { | |
| VThreadRunner.run(() -> { | |
| Pipe p = Pipe.open(); | |
| try (Pipe.SinkChannel sink = p.sink(); | |
| Pipe.SourceChannel source = p.source()) { | |
| // write should not block | |
| ByteBuffer bb = ByteBuffer.wrap("XXX".getBytes("UTF-8")); | |
| int n = sink.write(bb); | |
| assertTrue(n > 0); | |
| // read should not block | |
| bb = ByteBuffer.allocate(10); | |
| n = source.read(bb); | |
| assertTrue(n > 0); | |
| assertTrue(bb.get(0) == 'X'); | |
| } | |
| }); | |
| } | |
| /** | |
| * Virtual thread blocks in Pipe.SourceChannel read. | |
| */ | |
| @Test | |
| void testPipeReadWrite2() throws Exception { | |
| VThreadRunner.run(() -> { | |
| Pipe p = Pipe.open(); | |
| try (Pipe.SinkChannel sink = p.sink(); | |
| Pipe.SourceChannel source = p.source()) { | |
| // write from sink when current thread blocks reading from source | |
| ByteBuffer bb1 = ByteBuffer.wrap("XXX".getBytes("UTF-8")); | |
| runAfterParkedAsync(() -> sink.write(bb1)); | |
| // read should block | |
| ByteBuffer bb2 = ByteBuffer.allocate(10); | |
| int n = source.read(bb2); | |
| assertTrue(n > 0); | |
| assertTrue(bb2.get(0) == 'X'); | |
| } | |
| }); | |
| } | |
| /** | |
| * Virtual thread blocks in Pipe.SinkChannel write. | |
| */ | |
| @Test | |
| void testPipeReadWrite3() throws Exception { | |
| VThreadRunner.run(() -> { | |
| Pipe p = Pipe.open(); | |
| try (Pipe.SinkChannel sink = p.sink(); | |
| Pipe.SourceChannel source = p.source()) { | |
| // read from source to EOF when current thread blocking in write | |
| Thread reader = runAfterParkedAsync(() -> readToEOF(source)); | |
| // write to sink should block | |
| ByteBuffer bb = ByteBuffer.allocate(100*1024); | |
| for (int i=0; i<1000; i++) { | |
| int n = sink.write(bb); | |
| assertTrue(n > 0); | |
| bb.clear(); | |
| } | |
| sink.close(); | |
| // wait for reader to finish | |
| reader.join(); | |
| } | |
| }); | |
| } | |
| /** | |
| * Pipe.SourceChannel close while virtual thread blocked in read. | |
| */ | |
| @Test | |
| void testPipeReadAsyncClose() throws Exception { | |
| VThreadRunner.run(() -> { | |
| Pipe p = Pipe.open(); | |
| try (Pipe.SinkChannel sink = p.sink(); | |
| Pipe.SourceChannel source = p.source()) { | |
| runAfterParkedAsync(source::close); | |
| try { | |
| int n = source.read(ByteBuffer.allocate(100)); | |
| fail("read returned " + n); | |
| } catch (AsynchronousCloseException expected) { } | |
| } | |
| }); | |
| } | |
| /** | |
| * Virtual thread interrupted while blocked in Pipe.SourceChannel read. | |
| */ | |
| @Test | |
| void testPipeReadInterrupt() throws Exception { | |
| VThreadRunner.run(() -> { | |
| Pipe p = Pipe.open(); | |
| try (Pipe.SinkChannel sink = p.sink(); | |
| Pipe.SourceChannel source = p.source()) { | |
| // interrupt current thread when it blocks reading from source | |
| Thread thisThread = Thread.currentThread(); | |
| runAfterParkedAsync(thisThread::interrupt); | |
| try { | |
| int n = source.read(ByteBuffer.allocate(100)); | |
| fail("read returned " + n); | |
| } catch (ClosedByInterruptException expected) { | |
| assertTrue(Thread.interrupted()); | |
| } | |
| } | |
| }); | |
| } | |
| /** | |
| * Pipe.SinkChannel close while virtual thread blocked in write. | |
| */ | |
| @Test | |
| void testPipeWriteAsyncClose() throws Exception { | |
| VThreadRunner.run(() -> { | |
| boolean done = false; | |
| while (!done) { | |
| Pipe p = Pipe.open(); | |
| try (Pipe.SinkChannel sink = p.sink(); | |
| Pipe.SourceChannel source = p.source()) { | |
| // close sink when current thread blocks in write | |
| runAfterParkedAsync(sink::close, true); | |
| // write until channel is closed | |
| try { | |
| ByteBuffer bb = ByteBuffer.allocate(100*1024); | |
| for (;;) { | |
| int n = sink.write(bb); | |
| assertTrue(n > 0); | |
| bb.clear(); | |
| } | |
| } catch (AsynchronousCloseException e) { | |
| // closed when blocked in write | |
| done = true; | |
| } catch (ClosedChannelException e) { | |
| // closed but not blocked in write, need to retry test | |
| System.err.format("%s, need to retry!%n", e); | |
| } | |
| } | |
| } | |
| }); | |
| } | |
| /** | |
| * Virtual thread interrupted while blocked in Pipe.SinkChannel write. | |
| */ | |
| @Test | |
| void testPipeWriteInterrupt() throws Exception { | |
| VThreadRunner.run(() -> { | |
| boolean done = false; | |
| while (!done) { | |
| Pipe p = Pipe.open(); | |
| try (Pipe.SinkChannel sink = p.sink(); | |
| Pipe.SourceChannel source = p.source()) { | |
| // interrupt current thread when it blocks in write | |
| Thread thisThread = Thread.currentThread(); | |
| runAfterParkedAsync(thisThread::interrupt, true); | |
| // write until channel is closed | |
| try { | |
| ByteBuffer bb = ByteBuffer.allocate(100*1024); | |
| for (;;) { | |
| int n = sink.write(bb); | |
| assertTrue(n > 0); | |
| bb.clear(); | |
| } | |
| } catch (ClosedByInterruptException expected) { | |
| // closed when blocked in write | |
| assertTrue(Thread.interrupted()); | |
| done = true; | |
| } catch (ClosedChannelException e) { | |
| // closed but not blocked in write, need to retry test | |
| System.err.format("%s, need to retry!%n", e); | |
| } | |
| } | |
| } | |
| }); | |
| } | |
| /** | |
| * Creates a loopback connection | |
| */ | |
| static class Connection implements Closeable { | |
| private final SocketChannel sc1; | |
| private final SocketChannel sc2; | |
| Connection() throws IOException { | |
| var lh = InetAddress.getLoopbackAddress(); | |
| try (var listener = ServerSocketChannel.open()) { | |
| listener.bind(new InetSocketAddress(lh, 0)); | |
| SocketChannel sc1 = SocketChannel.open(); | |
| SocketChannel sc2 = null; | |
| try { | |
| sc1.socket().connect(listener.getLocalAddress()); | |
| sc2 = listener.accept(); | |
| } catch (IOException ioe) { | |
| sc1.close(); | |
| throw ioe; | |
| } | |
| this.sc1 = sc1; | |
| this.sc2 = sc2; | |
| } | |
| } | |
| SocketChannel channel1() { | |
| return sc1; | |
| } | |
| SocketChannel channel2() { | |
| return sc2; | |
| } | |
| @Override | |
| public void close() throws IOException { | |
| sc1.close(); | |
| sc2.close(); | |
| } | |
| } | |
| /** | |
| * Read from a channel until all bytes have been read or an I/O error occurs. | |
| */ | |
| static void readToEOF(ReadableByteChannel rbc) throws IOException { | |
| ByteBuffer bb = ByteBuffer.allocate(16*1024); | |
| int n; | |
| while ((n = rbc.read(bb)) > 0) { | |
| bb.clear(); | |
| } | |
| } | |
| @FunctionalInterface | |
| interface ThrowingRunnable { | |
| void run() throws Exception; | |
| } | |
| /** | |
| * Runs the given task asynchronously after the current virtual thread parks. | |
| * @param writing if the thread will block in write | |
| * @return the thread started to run the task | |
| */ | |
| private static Thread runAfterParkedAsync(ThrowingRunnable task, boolean writing) { | |
| Thread target = Thread.currentThread(); | |
| if (!target.isVirtual()) | |
| throw new WrongThreadException(); | |
| return Thread.ofPlatform().daemon().start(() -> { | |
| try { | |
| // wait for target thread to park | |
| while (!isWaiting(target)) { | |
| Thread.sleep(20); | |
| } | |
| // if the target thread is parked in write then we nudge it a few times | |
| // to avoid wakeup with some bytes written | |
| if (writing) { | |
| for (int i = 0; i < 3; i++) { | |
| LockSupport.unpark(target); | |
| while (!isWaiting(target)) { | |
| Thread.sleep(20); | |
| } | |
| } | |
| } | |
| task.run(); | |
| } catch (Exception e) { | |
| e.printStackTrace(); | |
| } | |
| }); | |
| } | |
| private static Thread runAfterParkedAsync(ThrowingRunnable task) { | |
| return runAfterParkedAsync(task, false); | |
| } | |
| /** | |
| * Return true if the given Thread is parked. | |
| */ | |
| private static boolean isWaiting(Thread target) { | |
| Thread.State state = target.getState(); | |
| assertNotEquals(Thread.State.TERMINATED, state); | |
| return (state == Thread.State.WAITING || state == Thread.State.TIMED_WAITING); | |
| } | |
| } |