| /* | |
| * Copyright (c) 2018, 2023, 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 java.net Sockets | |
| * @library /test/lib | |
| * @run junit BlockingSocketOps | |
| */ | |
| /** | |
| * @test id=poller-modes | |
| * @requires (os.family == "linux") | (os.family == "mac") | |
| * @library /test/lib | |
| * @run junit/othervm -Djdk.pollerMode=1 BlockingSocketOps | |
| * @run junit/othervm -Djdk.pollerMode=2 BlockingSocketOps | |
| */ | |
| /** | |
| * @test id=no-vmcontinuations | |
| * @requires vm.continuations | |
| * @library /test/lib | |
| * @run junit/othervm -XX:+UnlockExperimentalVMOptions -XX:-VMContinuations BlockingSocketOps | |
| */ | |
| import java.io.Closeable; | |
| import java.io.IOException; | |
| import java.io.InputStream; | |
| import java.io.OutputStream; | |
| import java.net.DatagramPacket; | |
| import java.net.DatagramSocket; | |
| import java.net.InetAddress; | |
| import java.net.InetSocketAddress; | |
| import java.net.ServerSocket; | |
| import java.net.Socket; | |
| import java.net.SocketAddress; | |
| import java.net.SocketException; | |
| import java.net.SocketTimeoutException; | |
| import jdk.test.lib.thread.VThreadRunner; | |
| import org.junit.jupiter.api.Test; | |
| import static org.junit.jupiter.api.Assertions.*; | |
| class BlockingSocketOps { | |
| /** | |
| * Socket read/write, no blocking. | |
| */ | |
| @Test | |
| void testSocketReadWrite1() throws Exception { | |
| VThreadRunner.run(() -> { | |
| try (var connection = new Connection()) { | |
| Socket s1 = connection.socket1(); | |
| Socket s2 = connection.socket2(); | |
| // write should not block | |
| byte[] ba = "XXX".getBytes("UTF-8"); | |
| s1.getOutputStream().write(ba); | |
| // read should not block | |
| ba = new byte[10]; | |
| int n = s2.getInputStream().read(ba); | |
| assertTrue(n > 0); | |
| assertTrue(ba[0] == 'X'); | |
| } | |
| }); | |
| } | |
| /** | |
| * Virtual thread blocks in read. | |
| */ | |
| @Test | |
| void testSocketRead1() throws Exception { | |
| testSocketRead(0); | |
| } | |
| /** | |
| * Virtual thread blocks in timed read. | |
| */ | |
| @Test | |
| void testSocketRead2() throws Exception { | |
| testSocketRead(60_000); | |
| } | |
| void testSocketRead(int timeout) throws Exception { | |
| VThreadRunner.run(() -> { | |
| try (var connection = new Connection()) { | |
| Socket s1 = connection.socket1(); | |
| Socket s2 = connection.socket2(); | |
| // delayed write from sc1 | |
| byte[] ba1 = "XXX".getBytes("UTF-8"); | |
| runAfterParkedAsync(() -> s1.getOutputStream().write(ba1)); | |
| // read from sc2 should block | |
| if (timeout > 0) { | |
| s2.setSoTimeout(timeout); | |
| } | |
| byte[] ba2 = new byte[10]; | |
| int n = s2.getInputStream().read(ba2); | |
| assertTrue(n > 0); | |
| assertTrue(ba2[0] == 'X'); | |
| } | |
| }); | |
| } | |
| /** | |
| * Virtual thread blocks in write. | |
| */ | |
| @Test | |
| void testSocketWrite1() throws Exception { | |
| VThreadRunner.run(() -> { | |
| try (var connection = new Connection()) { | |
| Socket s1 = connection.socket1(); | |
| Socket s2 = connection.socket2(); | |
| // delayed read from s2 to EOF | |
| InputStream in = s2.getInputStream(); | |
| Thread reader = runAfterParkedAsync(() -> | |
| in.transferTo(OutputStream.nullOutputStream())); | |
| // write should block | |
| byte[] ba = new byte[100*1024]; | |
| try (OutputStream out = s1.getOutputStream()) { | |
| for (int i = 0; i < 1000; i++) { | |
| out.write(ba); | |
| } | |
| } | |
| // wait for reader to finish | |
| reader.join(); | |
| } | |
| }); | |
| } | |
| /** | |
| * Virtual thread blocks in read, peer closes connection gracefully. | |
| */ | |
| @Test | |
| void testSocketReadPeerClose1() throws Exception { | |
| VThreadRunner.run(() -> { | |
| try (var connection = new Connection()) { | |
| Socket s1 = connection.socket1(); | |
| Socket s2 = connection.socket2(); | |
| // delayed close of s2 | |
| runAfterParkedAsync(s2::close); | |
| // read from s1 should block, then read -1 | |
| int n = s1.getInputStream().read(); | |
| assertTrue(n == -1); | |
| } | |
| }); | |
| } | |
| /** | |
| * Virtual thread blocks in read, peer closes connection abruptly. | |
| */ | |
| @Test | |
| void testSocketReadPeerClose2() throws Exception { | |
| VThreadRunner.run(() -> { | |
| try (var connection = new Connection()) { | |
| Socket s1 = connection.socket1(); | |
| Socket s2 = connection.socket2(); | |
| // delayed abrupt close of s2 | |
| s2.setSoLinger(true, 0); | |
| runAfterParkedAsync(s2::close); | |
| // read from s1 should block, then throw | |
| try { | |
| int n = s1.getInputStream().read(); | |
| fail("read " + n); | |
| } catch (IOException ioe) { | |
| // expected | |
| } | |
| } | |
| }); | |
| } | |
| /** | |
| * Socket close while virtual thread blocked in read. | |
| */ | |
| @Test | |
| void testSocketReadAsyncClose1() throws Exception { | |
| testSocketReadAsyncClose(0); | |
| } | |
| /** | |
| * Socket close while virtual thread blocked in timed read. | |
| */ | |
| @Test | |
| void testSocketReadAsyncClose2() throws Exception { | |
| testSocketReadAsyncClose(0); | |
| } | |
| void testSocketReadAsyncClose(int timeout) throws Exception { | |
| VThreadRunner.run(() -> { | |
| try (var connection = new Connection()) { | |
| Socket s = connection.socket1(); | |
| // delayed close of s | |
| runAfterParkedAsync(s::close); | |
| // read from s should block, then throw | |
| if (timeout > 0) { | |
| s.setSoTimeout(timeout); | |
| } | |
| try { | |
| int n = s.getInputStream().read(); | |
| fail("read " + n); | |
| } catch (SocketException expected) { } | |
| } | |
| }); | |
| } | |
| /** | |
| * Virtual thread interrupted while blocked in Socket read. | |
| */ | |
| @Test | |
| void testSocketReadInterrupt1() throws Exception { | |
| testSocketReadInterrupt(0); | |
| } | |
| /** | |
| * Virtual thread interrupted while blocked in Socket read with timeout | |
| */ | |
| @Test | |
| void testSocketReadInterrupt2() throws Exception { | |
| testSocketReadInterrupt(60_000); | |
| } | |
| void testSocketReadInterrupt(int timeout) throws Exception { | |
| VThreadRunner.run(() -> { | |
| try (var connection = new Connection()) { | |
| Socket s = connection.socket1(); | |
| // delayed interrupt of current thread | |
| Thread thisThread = Thread.currentThread(); | |
| runAfterParkedAsync(thisThread::interrupt); | |
| // read from s should block, then throw | |
| if (timeout > 0) { | |
| s.setSoTimeout(timeout); | |
| } | |
| try { | |
| int n = s.getInputStream().read(); | |
| fail("read " + n); | |
| } catch (SocketException expected) { | |
| assertTrue(Thread.interrupted()); | |
| assertTrue(s.isClosed()); | |
| } | |
| } | |
| }); | |
| } | |
| /** | |
| * Socket close while virtual thread blocked in write. | |
| */ | |
| @Test | |
| void testSocketWriteAsyncClose() throws Exception { | |
| VThreadRunner.run(() -> { | |
| try (var connection = new Connection()) { | |
| Socket s = connection.socket1(); | |
| // delayedclose of s | |
| runAfterParkedAsync(s::close); | |
| // write to s should block, then throw | |
| try { | |
| byte[] ba = new byte[100*1024]; | |
| OutputStream out = s.getOutputStream(); | |
| for (;;) { | |
| out.write(ba); | |
| } | |
| } catch (SocketException expected) { } | |
| } | |
| }); | |
| } | |
| /** | |
| * Virtual thread interrupted while blocked in Socket write. | |
| */ | |
| @Test | |
| void testSocketWriteInterrupt() throws Exception { | |
| VThreadRunner.run(() -> { | |
| try (var connection = new Connection()) { | |
| Socket s = connection.socket1(); | |
| // delayed interrupt of current thread | |
| Thread thisThread = Thread.currentThread(); | |
| runAfterParkedAsync(thisThread::interrupt); | |
| // write to s should block, then throw | |
| try { | |
| byte[] ba = new byte[100*1024]; | |
| OutputStream out = s.getOutputStream(); | |
| for (;;) { | |
| out.write(ba); | |
| } | |
| } catch (SocketException expected) { | |
| assertTrue(Thread.interrupted()); | |
| assertTrue(s.isClosed()); | |
| } | |
| } | |
| }); | |
| } | |
| /** | |
| * Virtual thread reading urgent data when SO_OOBINLINE is enabled. | |
| */ | |
| @Test | |
| void testSocketReadUrgentData() throws Exception { | |
| VThreadRunner.run(() -> { | |
| try (var connection = new Connection()) { | |
| Socket s1 = connection.socket1(); | |
| Socket s2 = connection.socket2(); | |
| // urgent data should be received | |
| runAfterParkedAsync(() -> s2.sendUrgentData('X')); | |
| // read should block, then read the OOB byte | |
| s1.setOOBInline(true); | |
| byte[] ba = new byte[10]; | |
| int n = s1.getInputStream().read(ba); | |
| assertTrue(n == 1); | |
| assertTrue(ba[0] == 'X'); | |
| // urgent data should not be received | |
| s1.setOOBInline(false); | |
| s1.setSoTimeout(500); | |
| s2.sendUrgentData('X'); | |
| try { | |
| s1.getInputStream().read(ba); | |
| fail(); | |
| } catch (SocketTimeoutException expected) { } | |
| } | |
| }); | |
| } | |
| /** | |
| * ServerSocket accept, no blocking. | |
| */ | |
| @Test | |
| void testServerSocketAccept1() throws Exception { | |
| VThreadRunner.run(() -> { | |
| try (var listener = new ServerSocket()) { | |
| InetAddress loopback = InetAddress.getLoopbackAddress(); | |
| listener.bind(new InetSocketAddress(loopback, 0)); | |
| // establish connection | |
| var socket1 = new Socket(loopback, listener.getLocalPort()); | |
| // accept should not block | |
| var socket2 = listener.accept(); | |
| socket1.close(); | |
| socket2.close(); | |
| } | |
| }); | |
| } | |
| /** | |
| * Virtual thread blocks in accept. | |
| */ | |
| @Test | |
| void testServerSocketAccept2() throws Exception { | |
| testServerSocketAccept(0); | |
| } | |
| /** | |
| * Virtual thread blocks in timed accept. | |
| */ | |
| @Test | |
| void testServerSocketAccept3() throws Exception { | |
| testServerSocketAccept(60_000); | |
| } | |
| void testServerSocketAccept(int timeout) throws Exception { | |
| VThreadRunner.run(() -> { | |
| try (var listener = new ServerSocket()) { | |
| InetAddress loopback = InetAddress.getLoopbackAddress(); | |
| listener.bind(new InetSocketAddress(loopback, 0)); | |
| // schedule connect | |
| var socket1 = new Socket(); | |
| SocketAddress remote = listener.getLocalSocketAddress(); | |
| runAfterParkedAsync(() -> socket1.connect(remote)); | |
| // accept should block | |
| if (timeout > 0) { | |
| listener.setSoTimeout(timeout); | |
| } | |
| var socket2 = listener.accept(); | |
| socket1.close(); | |
| socket2.close(); | |
| } | |
| }); | |
| } | |
| /** | |
| * ServerSocket close while virtual thread blocked in accept. | |
| */ | |
| @Test | |
| void testServerSocketAcceptAsyncClose1() throws Exception { | |
| testServerSocketAcceptAsyncClose(0); | |
| } | |
| /** | |
| * ServerSocket close while virtual thread blocked in timed accept. | |
| */ | |
| @Test | |
| void testServerSocketAcceptAsyncClose2() throws Exception { | |
| testServerSocketAcceptAsyncClose(60_000); | |
| } | |
| void testServerSocketAcceptAsyncClose(int timeout) throws Exception { | |
| VThreadRunner.run(() -> { | |
| try (var listener = new ServerSocket()) { | |
| InetAddress loopback = InetAddress.getLoopbackAddress(); | |
| listener.bind(new InetSocketAddress(loopback, 0)); | |
| // delayed close of listener | |
| runAfterParkedAsync(listener::close); | |
| // accept should block, then throw | |
| if (timeout > 0) { | |
| listener.setSoTimeout(timeout); | |
| } | |
| try { | |
| listener.accept().close(); | |
| fail("connection accepted???"); | |
| } catch (SocketException expected) { } | |
| } | |
| }); | |
| } | |
| /** | |
| * Virtual thread interrupted while blocked in ServerSocket accept. | |
| */ | |
| @Test | |
| void testServerSocketAcceptInterrupt1() throws Exception { | |
| testServerSocketAcceptInterrupt(0); | |
| } | |
| /** | |
| * Virtual thread interrupted while blocked in ServerSocket accept with timeout. | |
| */ | |
| @Test | |
| void testServerSocketAcceptInterrupt2() throws Exception { | |
| testServerSocketAcceptInterrupt(60_000); | |
| } | |
| void testServerSocketAcceptInterrupt(int timeout) throws Exception { | |
| VThreadRunner.run(() -> { | |
| try (var listener = new ServerSocket()) { | |
| InetAddress loopback = InetAddress.getLoopbackAddress(); | |
| listener.bind(new InetSocketAddress(loopback, 0)); | |
| // delayed interrupt of current thread | |
| Thread thisThread = Thread.currentThread(); | |
| runAfterParkedAsync(thisThread::interrupt); | |
| // accept should block, then throw | |
| if (timeout > 0) { | |
| listener.setSoTimeout(timeout); | |
| } | |
| try { | |
| listener.accept().close(); | |
| fail("connection accepted???"); | |
| } catch (SocketException expected) { | |
| assertTrue(Thread.interrupted()); | |
| assertTrue(listener.isClosed()); | |
| } | |
| } | |
| }); | |
| } | |
| /** | |
| * DatagramSocket receive/send, no blocking. | |
| */ | |
| @Test | |
| void testDatagramSocketSendReceive1() throws Exception { | |
| VThreadRunner.run(() -> { | |
| try (DatagramSocket s1 = new DatagramSocket(null); | |
| DatagramSocket s2 = new DatagramSocket(null)) { | |
| InetAddress lh = InetAddress.getLoopbackAddress(); | |
| s1.bind(new InetSocketAddress(lh, 0)); | |
| s2.bind(new InetSocketAddress(lh, 0)); | |
| // send should not block | |
| byte[] bytes = "XXX".getBytes("UTF-8"); | |
| DatagramPacket p1 = new DatagramPacket(bytes, bytes.length); | |
| p1.setSocketAddress(s2.getLocalSocketAddress()); | |
| s1.send(p1); | |
| // receive should not block | |
| byte[] ba = new byte[100]; | |
| DatagramPacket p2 = new DatagramPacket(ba, ba.length); | |
| s2.receive(p2); | |
| assertEquals(s1.getLocalSocketAddress(), p2.getSocketAddress()); | |
| assertTrue(ba[0] == 'X'); | |
| } | |
| }); | |
| } | |
| /** | |
| * Virtual thread blocks in DatagramSocket receive. | |
| */ | |
| @Test | |
| void testDatagramSocketSendReceive2() throws Exception { | |
| testDatagramSocketSendReceive(0); | |
| } | |
| /** | |
| * Virtual thread blocks in DatagramSocket receive with timeout. | |
| */ | |
| @Test | |
| void testDatagramSocketSendReceive3() throws Exception { | |
| testDatagramSocketSendReceive(60_000); | |
| } | |
| private void testDatagramSocketSendReceive(int timeout) throws Exception { | |
| VThreadRunner.run(() -> { | |
| try (DatagramSocket s1 = new DatagramSocket(null); | |
| DatagramSocket s2 = new DatagramSocket(null)) { | |
| InetAddress lh = InetAddress.getLoopbackAddress(); | |
| s1.bind(new InetSocketAddress(lh, 0)); | |
| s2.bind(new InetSocketAddress(lh, 0)); | |
| // delayed send | |
| byte[] bytes = "XXX".getBytes("UTF-8"); | |
| DatagramPacket p1 = new DatagramPacket(bytes, bytes.length); | |
| p1.setSocketAddress(s2.getLocalSocketAddress()); | |
| runAfterParkedAsync(() -> s1.send(p1)); | |
| // receive should block | |
| if (timeout > 0) { | |
| s2.setSoTimeout(timeout); | |
| } | |
| byte[] ba = new byte[100]; | |
| DatagramPacket p2 = new DatagramPacket(ba, ba.length); | |
| s2.receive(p2); | |
| assertEquals(s1.getLocalSocketAddress(), p2.getSocketAddress()); | |
| assertTrue(ba[0] == 'X'); | |
| } | |
| }); | |
| } | |
| /** | |
| * Virtual thread blocks in DatagramSocket receive that times out. | |
| */ | |
| @Test | |
| void testDatagramSocketReceiveTimeout() throws Exception { | |
| VThreadRunner.run(() -> { | |
| try (DatagramSocket s = new DatagramSocket(null)) { | |
| InetAddress lh = InetAddress.getLoopbackAddress(); | |
| s.bind(new InetSocketAddress(lh, 0)); | |
| s.setSoTimeout(500); | |
| byte[] ba = new byte[100]; | |
| DatagramPacket p = new DatagramPacket(ba, ba.length); | |
| try { | |
| s.receive(p); | |
| fail(); | |
| } catch (SocketTimeoutException expected) { } | |
| } | |
| }); | |
| } | |
| /** | |
| * DatagramSocket close while virtual thread blocked in receive. | |
| */ | |
| @Test | |
| void testDatagramSocketReceiveAsyncClose1() throws Exception { | |
| testDatagramSocketReceiveAsyncClose(0); | |
| } | |
| /** | |
| * DatagramSocket close while virtual thread blocked with timeout. | |
| */ | |
| @Test | |
| void testDatagramSocketReceiveAsyncClose2() throws Exception { | |
| testDatagramSocketReceiveAsyncClose(60_000); | |
| } | |
| private void testDatagramSocketReceiveAsyncClose(int timeout) throws Exception { | |
| VThreadRunner.run(() -> { | |
| try (DatagramSocket s = new DatagramSocket(null)) { | |
| InetAddress lh = InetAddress.getLoopbackAddress(); | |
| s.bind(new InetSocketAddress(lh, 0)); | |
| // delayed close of s | |
| runAfterParkedAsync(s::close); | |
| // receive should block, then throw | |
| if (timeout > 0) { | |
| s.setSoTimeout(timeout); | |
| } | |
| try { | |
| byte[] ba = new byte[100]; | |
| DatagramPacket p = new DatagramPacket(ba, ba.length); | |
| s.receive(p); | |
| fail(); | |
| } catch (SocketException expected) { } | |
| } | |
| }); | |
| } | |
| /** | |
| * Virtual thread interrupted while blocked in DatagramSocket receive. | |
| */ | |
| @Test | |
| void testDatagramSocketReceiveInterrupt1() throws Exception { | |
| testDatagramSocketReceiveInterrupt(0); | |
| } | |
| /** | |
| * Virtual thread interrupted while blocked in DatagramSocket receive with timeout. | |
| */ | |
| @Test | |
| void testDatagramSocketReceiveInterrupt2() throws Exception { | |
| testDatagramSocketReceiveInterrupt(60_000); | |
| } | |
| private void testDatagramSocketReceiveInterrupt(int timeout) throws Exception { | |
| VThreadRunner.run(() -> { | |
| try (DatagramSocket s = new DatagramSocket(null)) { | |
| InetAddress lh = InetAddress.getLoopbackAddress(); | |
| s.bind(new InetSocketAddress(lh, 0)); | |
| // delayed interrupt of current thread | |
| Thread thisThread = Thread.currentThread(); | |
| runAfterParkedAsync(thisThread::interrupt); | |
| // receive should block, then throw | |
| if (timeout > 0) { | |
| s.setSoTimeout(timeout); | |
| } | |
| try { | |
| byte[] ba = new byte[100]; | |
| DatagramPacket p = new DatagramPacket(ba, ba.length); | |
| s.receive(p); | |
| fail(); | |
| } catch (SocketException expected) { | |
| assertTrue(Thread.interrupted()); | |
| assertTrue(s.isClosed()); | |
| } | |
| } | |
| }); | |
| } | |
| /** | |
| * Creates a loopback connection | |
| */ | |
| static class Connection implements Closeable { | |
| private final Socket s1; | |
| private final Socket s2; | |
| Connection() throws IOException { | |
| var lh = InetAddress.getLoopbackAddress(); | |
| try (var listener = new ServerSocket()) { | |
| listener.bind(new InetSocketAddress(lh, 0)); | |
| Socket s1 = new Socket(); | |
| Socket s2; | |
| try { | |
| s1.connect(listener.getLocalSocketAddress()); | |
| s2 = listener.accept(); | |
| } catch (IOException ioe) { | |
| s1.close(); | |
| throw ioe; | |
| } | |
| this.s1 = s1; | |
| this.s2 = s2; | |
| } | |
| } | |
| Socket socket1() { | |
| return s1; | |
| } | |
| Socket socket2() { | |
| return s2; | |
| } | |
| @Override | |
| public void close() throws IOException { | |
| s1.close(); | |
| s2.close(); | |
| } | |
| } | |
| @FunctionalInterface | |
| interface ThrowingRunnable { | |
| void run() throws Exception; | |
| } | |
| /** | |
| * Runs the given task asynchronously after the current virtual thread has parked. | |
| * @return the thread started to run the task | |
| */ | |
| static Thread runAfterParkedAsync(ThrowingRunnable task) { | |
| Thread target = Thread.currentThread(); | |
| if (!target.isVirtual()) | |
| throw new WrongThreadException(); | |
| return Thread.ofPlatform().daemon().start(() -> { | |
| try { | |
| Thread.State state = target.getState(); | |
| while (state != Thread.State.WAITING | |
| && state != Thread.State.TIMED_WAITING) { | |
| Thread.sleep(20); | |
| state = target.getState(); | |
| } | |
| Thread.sleep(20); // give a bit more time to release carrier | |
| task.run(); | |
| } catch (Exception e) { | |
| e.printStackTrace(); | |
| } | |
| }); | |
| } | |
| } |