From 9bb675f89dd1eeec423ca96cb3f96d29f5de477c Mon Sep 17 00:00:00 2001 From: Jaikiran Pai Date: Thu, 27 Jun 2024 04:38:32 +0000 Subject: [PATCH] 8334719: (se) Deferred close of SelectableChannel may result in a Selector doing the final close before concurrent I/O on channel has completed Co-authored-by: Alan Bateman Reviewed-by: alanb, dfuchs --- .../sun/nio/ch/DatagramChannelImpl.java | 5 + .../sun/nio/ch/ServerSocketChannelImpl.java | 3 + .../classes/sun/nio/ch/SocketChannelImpl.java | 5 + .../classes/sun/nio/ch/SinkChannelImpl.java | 3 + .../classes/sun/nio/ch/SourceChannelImpl.java | 3 + .../DeferredClose/DeferredCloseTest.java | 484 ++++++++++++++++++ .../java.base/java/net/InetSocketAddress.java | 202 ++++++++ 7 files changed, 705 insertions(+) create mode 100644 test/jdk/java/nio/channels/Selector/DeferredClose/DeferredCloseTest.java create mode 100644 test/jdk/java/nio/channels/Selector/DeferredClose/java.base/java/net/InetSocketAddress.java diff --git a/src/java.base/share/classes/sun/nio/ch/DatagramChannelImpl.java b/src/java.base/share/classes/sun/nio/ch/DatagramChannelImpl.java index 31823cfbb87..5c06ac7d9be 100644 --- a/src/java.base/share/classes/sun/nio/ch/DatagramChannelImpl.java +++ b/src/java.base/share/classes/sun/nio/ch/DatagramChannelImpl.java @@ -1945,6 +1945,11 @@ class DatagramChannelImpl @Override public void kill() { + // wait for any read/write operations to complete before trying to close + readLock.lock(); + readLock.unlock(); + writeLock.lock(); + writeLock.unlock(); synchronized (stateLock) { if (state == ST_CLOSING) { tryFinishClose(); diff --git a/src/java.base/share/classes/sun/nio/ch/ServerSocketChannelImpl.java b/src/java.base/share/classes/sun/nio/ch/ServerSocketChannelImpl.java index c6ddad2a05c..0bd72bacf0b 100644 --- a/src/java.base/share/classes/sun/nio/ch/ServerSocketChannelImpl.java +++ b/src/java.base/share/classes/sun/nio/ch/ServerSocketChannelImpl.java @@ -654,6 +654,9 @@ class ServerSocketChannelImpl @Override public void kill() { + // wait for any accept operation to complete before trying to close + acceptLock.lock(); + acceptLock.unlock(); synchronized (stateLock) { if (state == ST_CLOSING) { tryFinishClose(); diff --git a/src/java.base/share/classes/sun/nio/ch/SocketChannelImpl.java b/src/java.base/share/classes/sun/nio/ch/SocketChannelImpl.java index f23a0aa0152..ebbf55acd97 100644 --- a/src/java.base/share/classes/sun/nio/ch/SocketChannelImpl.java +++ b/src/java.base/share/classes/sun/nio/ch/SocketChannelImpl.java @@ -1216,6 +1216,11 @@ class SocketChannelImpl @Override public void kill() { + // wait for any read/write operations to complete before trying to close + readLock.lock(); + readLock.unlock(); + writeLock.lock(); + writeLock.unlock(); synchronized (stateLock) { if (state == ST_CLOSING) { tryFinishClose(); diff --git a/src/java.base/unix/classes/sun/nio/ch/SinkChannelImpl.java b/src/java.base/unix/classes/sun/nio/ch/SinkChannelImpl.java index 491e0104765..bc15d8d156b 100644 --- a/src/java.base/unix/classes/sun/nio/ch/SinkChannelImpl.java +++ b/src/java.base/unix/classes/sun/nio/ch/SinkChannelImpl.java @@ -201,6 +201,9 @@ class SinkChannelImpl @Override public void kill() { + // wait for any write operation to complete before trying to close + writeLock.lock(); + writeLock.unlock(); synchronized (stateLock) { if (state == ST_CLOSING) { tryFinishClose(); diff --git a/src/java.base/unix/classes/sun/nio/ch/SourceChannelImpl.java b/src/java.base/unix/classes/sun/nio/ch/SourceChannelImpl.java index 102041dd294..2990d35b516 100644 --- a/src/java.base/unix/classes/sun/nio/ch/SourceChannelImpl.java +++ b/src/java.base/unix/classes/sun/nio/ch/SourceChannelImpl.java @@ -200,6 +200,9 @@ class SourceChannelImpl } @Override public void kill() { + // wait for any read operation to complete before trying to close + readLock.lock(); + readLock.unlock(); synchronized (stateLock) { assert !isOpen(); if (state == ST_CLOSING) { diff --git a/test/jdk/java/nio/channels/Selector/DeferredClose/DeferredCloseTest.java b/test/jdk/java/nio/channels/Selector/DeferredClose/DeferredCloseTest.java new file mode 100644 index 00000000000..42dd7d42f56 --- /dev/null +++ b/test/jdk/java/nio/channels/Selector/DeferredClose/DeferredCloseTest.java @@ -0,0 +1,484 @@ +/* + * Copyright (c) 2024, 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. + */ + +import java.io.IOException; +import java.io.UncheckedIOException; +import java.net.InetAddress; +import java.net.InetSocketAddress; +import java.net.SocketAddress; +import java.nio.ByteBuffer; +import java.nio.channels.ClosedChannelException; +import java.nio.channels.ClosedSelectorException; +import java.nio.channels.DatagramChannel; +import java.nio.channels.Pipe; +import java.nio.channels.SelectableChannel; +import java.nio.channels.SelectionKey; +import java.nio.channels.Selector; +import java.nio.channels.ServerSocketChannel; +import java.nio.channels.SocketChannel; +import java.time.Instant; +import java.util.Objects; +import java.util.concurrent.Callable; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.Future; +import java.util.function.Function; +import java.util.stream.Stream; + +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; + +/* + * @test + * @bug 8334719 + * @summary verifies that if a registered channel has in-progress operations, then + * the Selector during its deferred close implementation won't prematurely release + * the channel's resources + * + * @comment we use a patched java.net.InetSocketAddress to allow the test to intentionally + * craft some delays at specific locations in the implementation of InetSocketAddress + * to trigger race conditions + * @compile/module=java.base java/net/InetSocketAddress.java + * @run junit/othervm DeferredCloseTest + */ +public class DeferredCloseTest { + + private static final int NUM_ITERATIONS = 10; + private static final InetSocketAddress BIND_ADDR = new InetSocketAddress( + InetAddress.getLoopbackAddress(), 0); + + @BeforeAll + public static void beforeAll() throws Exception { + // configure our patched java.net.InetSocketAddress implementation + // to introduce delay in certain methods which get invoked + // internally from the DC.send() implementation + InetSocketAddress.enableDelay(); + } + + @AfterAll + public static void afterAll() throws Exception { + // delays in patched InetSocketAddress are no longer needed + InetSocketAddress.disableDelay(); + } + + private static Stream dcOperations() { + return Stream.of( + Arguments.of( + // repeatedly do DC.send() till there's a ClosedChannelException + "DC.send()", + null, + (Function) (dc) -> { + ByteBuffer bb = ByteBuffer.allocate(100); + try { + // We send to ourselves. Target, content and + // receipt of the Datagram isn't of importance + // in this test. + SocketAddress target = dc.getLocalAddress(); + System.out.println("DC: " + dc + " sending to " + target); + while (true) { + bb.clear(); + dc.send(bb, target); + } + } catch (ClosedChannelException _) { + } catch (Exception e) { + throw new RuntimeException(e); + } + return null; + } + ), + Arguments.of( + // repeatedly do DC.receive() till there's a ClosedChannelException + "DC.receive()", + (Function) (dc) -> { + try { + SocketAddress target = dc.getLocalAddress(); + ByteBuffer sendBB = ByteBuffer.allocate(100); + // first send() a few datagrams so that subsequent + // receive() does receive them and thus triggers + // the potential race with the deferred close + for (int i = 0; i < 5; i++) { + sendBB.clear(); + dc.send(sendBB, target); + } + } catch (Exception e) { + throw new RuntimeException(e); + } + return null; + }, + (Function) (dc) -> { + try { + ByteBuffer rcvBB = ByteBuffer.allocate(10); + while (true) { + rcvBB.clear(); + dc.receive(rcvBB); + } + } catch (ClosedChannelException _) { + } catch (Exception e) { + throw new RuntimeException(e); + } + return null; + } + ) + ); + } + + /** + * Runs the test for DatagramChannel. + * + * @see #runTest(ExecutorService, SelectionKey, Callable, CountDownLatch) + */ + @ParameterizedTest + @MethodSource("dcOperations") + public void testDatagramChannel(String opName, Function preOp, + Function dcOperation) + throws Exception { + try (ExecutorService executor = Executors.newFixedThreadPool(2)) { + for (int i = 1; i <= NUM_ITERATIONS; i++) { + System.out.format("%s DatagramChannel - %d of %d ...%n", + Instant.now(), i, NUM_ITERATIONS); + try (Selector sel = Selector.open(); + DatagramChannel dc = DatagramChannel.open()) { + // create a non-blocking bound DatagramChannel + dc.bind(BIND_ADDR); + dc.configureBlocking(false); + // register the DatagramChannel with a selector + // (doesn't matter the interestOps) + SelectionKey key = dc.register(sel, SelectionKey.OP_READ); + if (preOp != null) { + preOp.apply(dc); + } + CountDownLatch opStartLatch = new CountDownLatch(1); + runTest(executor, key, () -> { + // notify that we will now start operation on the DC + opStartLatch.countDown(); + return dcOperation.apply(dc); + }, opStartLatch); + } + } + } + } + + + private static Stream scOperations() { + return Stream.of( + Arguments.of( + // repeatedly do SC.write() till there's a ClosedChannelException + "SC.write()", (Function) (sc) -> { + ByteBuffer bb = ByteBuffer.allocate(100); + try { + System.out.println("SC: " + sc + " writing"); + while (true) { + bb.clear(); + sc.write(bb); + } + } catch (ClosedChannelException _) { + } catch (IOException ioe) { + throw new UncheckedIOException(ioe); + } + return null; + } + ), + Arguments.of( + // repeatedly do SC.read() till there's a ClosedChannelException + "SC.read()", (Function) (sc) -> { + ByteBuffer bb = ByteBuffer.allocate(100); + try { + System.out.println("SC: " + sc + " reading"); + while (true) { + bb.clear(); + sc.read(bb); + } + } catch (ClosedChannelException _) { + } catch (IOException ioe) { + throw new UncheckedIOException(ioe); + } + return null; + } + ) + ); + } + + /** + * Runs the test for SocketChannel + * + * @see #runTest(ExecutorService, SelectionKey, Callable, CountDownLatch) + */ + @ParameterizedTest + @MethodSource("scOperations") + public void testSocketChannel(String opName, Function scOperation) + throws Exception { + try (ExecutorService executor = Executors.newFixedThreadPool(3)) { + for (int i = 1; i <= NUM_ITERATIONS; i++) { + System.out.format("%s SocketChannel - %d of %d ...%n", + Instant.now(), i, NUM_ITERATIONS); + try (Selector sel = Selector.open(); + SocketChannel sc = SocketChannel.open()) { + // create and bind a SocketChannel + sc.bind(BIND_ADDR); + // stay in blocking mode till the SocketChannel is connected + sc.configureBlocking(true); + Future acceptedChannel; + SocketChannel conn; + // create a remote server and connect to it + try (ServerSocketChannel server = ServerSocketChannel.open()) { + server.bind(BIND_ADDR); + SocketAddress remoteAddr = server.getLocalAddress(); + acceptedChannel = executor.submit(new ConnAcceptor(server)); + System.out.println("connecting to " + remoteAddr); + sc.connect(remoteAddr); + conn = acceptedChannel.get(); + } + try (conn) { + // switch to non-blocking + sc.configureBlocking(false); + System.out.println("switched to non-blocking: " + sc); + // register the SocketChannel with a selector + // (doesn't matter the interestOps) + SelectionKey key = sc.register(sel, SelectionKey.OP_READ); + CountDownLatch opStartLatch = new CountDownLatch(1); + runTest(executor, key, () -> { + // notify that we will now start operation on the SC + opStartLatch.countDown(); + return scOperation.apply(sc); + }, opStartLatch); + } + } + } + } + } + + /** + * Runs the test for ServerSocketChannel + * + * @see #runTest(ExecutorService, SelectionKey, Callable, CountDownLatch) + */ + @Test + public void testServerSocketChannel() throws Exception { + try (ExecutorService executor = Executors.newFixedThreadPool(2)) { + for (int i = 1; i <= NUM_ITERATIONS; i++) { + System.out.format("%s ServerSocketChannel - %d of %d ...%n", + Instant.now(), i, NUM_ITERATIONS); + try (Selector sel = Selector.open(); + ServerSocketChannel ssc = ServerSocketChannel.open()) { + // create and bind a ServerSocketChannel + ssc.bind(BIND_ADDR); + ssc.configureBlocking(false); + // register the ServerSocketChannel with a selector + SelectionKey key = ssc.register(sel, SelectionKey.OP_ACCEPT); + CountDownLatch opStartLatch = new CountDownLatch(1); + runTest(executor, key, () -> { + // notify that we will now start accept()ing + opStartLatch.countDown(); + // repeatedly do SSC.accept() till there's a ClosedChannelException + try { + while (true) { + ssc.accept(); + } + } catch (ClosedChannelException _) { + } + return null; + }, opStartLatch); + } + } + } + } + + /** + * Runs the test for SinkChannel + * + * @see #runTest(ExecutorService, SelectionKey, Callable, CountDownLatch) + */ + @Test + public void testSinkChannel() throws Exception { + try (ExecutorService executor = Executors.newFixedThreadPool(2)) { + for (int i = 1; i <= NUM_ITERATIONS; i++) { + System.out.format("%s SinkChannel - %d of %d ...%n", + Instant.now(), i, NUM_ITERATIONS); + Pipe pipe = Pipe.open(); + try (Selector sel = Selector.open(); + Pipe.SinkChannel sink = pipe.sink()) { + sink.configureBlocking(false); + SelectionKey key = sink.register(sel, SelectionKey.OP_WRITE); + CountDownLatch opStartLatch = new CountDownLatch(1); + runTest(executor, key, () -> { + // notify that we will now start write()ing + opStartLatch.countDown(); + // repeatedly do SC.write() till there's a ClosedChannelException + ByteBuffer bb = ByteBuffer.allocate(100); + try { + while (true) { + bb.clear(); + sink.write(bb); + } + } catch (ClosedChannelException _) { + } + return null; + }, opStartLatch); + } + } + } + } + + /** + * Runs the test for SourceChannel + * + * @see #runTest(ExecutorService, SelectionKey, Callable, CountDownLatch) + */ + @Test + public void testSourceChannel() throws Exception { + try (ExecutorService executor = Executors.newFixedThreadPool(2)) { + for (int i = 1; i <= NUM_ITERATIONS; i++) { + System.out.format("%s SourceChannel - %d of %d ...%n", + Instant.now(), i, NUM_ITERATIONS); + Pipe pipe = Pipe.open(); + try (Selector sel = Selector.open(); + Pipe.SourceChannel source = pipe.source()) { + source.configureBlocking(false); + SelectionKey key = source.register(sel, SelectionKey.OP_READ); + CountDownLatch opStartLatch = new CountDownLatch(1); + runTest(executor, key, () -> { + // notify that we will now start read()ing + opStartLatch.countDown(); + // repeatedly do SC.read() till there's a ClosedChannelException + ByteBuffer bb = ByteBuffer.allocate(100); + try { + while (true) { + bb.clear(); + source.read(bb); + } + } catch (ClosedChannelException _) { + } + return null; + }, opStartLatch); + } + } + } + } + + /** + * SelectableChannel implementations internally have a deferred close implementation. When a + * channel is registered with a Selector and close() is invoked on the channel from a certain + * thread, then the implementation of close() defers the actual close if the channel has + * in-progress operations (for example, read/write/send/receive and such) in some other thread. + * A subsequent operation through the Selector (like Selector.select()) then completes the + * deferred close (waiting for any in-progress operations to complete). This test method + * verifies that the deferred close implementation doesn't prematurely close and release + * the resources used by the channel, while there are in-progress operations. + *

+ * Launches 2 threads, T1 and T2. When T1 and T2 are in progress, this method closes the + * channel that is registered with the Selector. + * T1 is running the channelOperation (which keeps running operations on the channel). + * T2 is running a task which keeps invoking Selector.select(), until the channel is closed. + * When T2 notices that the channel is closed, it cancels the selectionKey and then + * invokes one last Selector.select() operation to finish the deferred close of the channel. + */ + private static void runTest(ExecutorService executor, SelectionKey selectionKey, + Callable channelOperation, CountDownLatch chanOpStartLatch) + throws Exception { + + SelectableChannel channel = selectionKey.channel(); + assertFalse(channel.isBlocking(), "channel isn't non-blocking: " + channel); + selectionKey.selector().selectNow(); + // run the channel operations + Future channelOpResult = executor.submit(channelOperation); + CountDownLatch selectorTaskStartLatch = new CountDownLatch(1); + // run the Selector.select() task + Future selectorTaskResult = executor.submit( + new SelectorTask(selectionKey, selectorTaskStartLatch)); + // await for the channel operation task and the selector task to start + chanOpStartLatch.await(); + selectorTaskStartLatch.await(); + // close the channel while it's still registered with the Selector, + // so that the close is deferred by the channel implementations. + System.out.println("closing channel: " + channel); + assertTrue(channel.isOpen(), "channel already closed: " + channel); + assertTrue(channel.isRegistered(), "channel isn't registered: " + channel); + channel.close(); + // wait for the operation on the channel and the selector task to complete + channelOpResult.get(); + selectorTaskResult.get(); + } + + /* + * Keeps invoking Selector.select() until the channel is closed, after which + * it cancels the SelectionKey and does one last Selector.select() to finish + * the deferred close. + */ + private static final class SelectorTask implements Callable { + private final SelectionKey selectionKey; + private final CountDownLatch startedLatch; + + private SelectorTask(SelectionKey selectionKey, CountDownLatch startedLatch) { + this.selectionKey = Objects.requireNonNull(selectionKey); + this.startedLatch = startedLatch; + } + + @Override + public Void call() throws Exception { + try { + Selector selector = selectionKey.selector(); + SelectableChannel channel = selectionKey.channel(); + // notify that the task has started + startedLatch.countDown(); + while (true) { + selector.select(10); + if (!channel.isOpen()) { + // the channel is (defer) closed, cancel the registration and then + // issue a select() so that the Selector finishes the deferred + // close of the channel. + System.out.println("channel: " + channel + " isn't open," + + " now cancelling key: " + selectionKey); + selectionKey.cancel(); + System.out.println("initiating select after key cancelled: " + selectionKey); + selector.select(5); + break; + } + } + } catch (ClosedSelectorException _) { + } + return null; + } + } + + private static final class ConnAcceptor implements Callable { + private final ServerSocketChannel serverSocketChannel; + + private ConnAcceptor(ServerSocketChannel serverSocketChannel) { + this.serverSocketChannel = serverSocketChannel; + } + + @Override + public SocketChannel call() throws Exception { + SocketChannel accepted = serverSocketChannel.accept(); + System.out.println("Accepted connection: " + accepted); + return accepted; + } + } +} diff --git a/test/jdk/java/nio/channels/Selector/DeferredClose/java.base/java/net/InetSocketAddress.java b/test/jdk/java/nio/channels/Selector/DeferredClose/java.base/java/net/InetSocketAddress.java new file mode 100644 index 00000000000..5994e9268a1 --- /dev/null +++ b/test/jdk/java/nio/channels/Selector/DeferredClose/java.base/java/net/InetSocketAddress.java @@ -0,0 +1,202 @@ +/* + * Copyright (c) 2024, 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. + */ +package java.net; + + +import java.util.Locale; + +// Patched implementation only meant to be used in certain tests +public class InetSocketAddress extends SocketAddress { + + @java.io.Serial + private static final long serialVersionUID = 5076001401234631237L; + + private static boolean enableDelay; + + static { + System.out.println("patched InetSocketAddress class in use"); + } + + private final String hostname; + private final InetAddress addr; + private final int port; + + public InetSocketAddress(int port) { + this(InetAddress.anyLocalAddress(), port); + } + + public InetSocketAddress(InetAddress addr, int port) { + this(null, + addr == null ? InetAddress.anyLocalAddress() : addr, + checkPort(port)); + } + + public InetSocketAddress(String hostname, int port) { + checkHost(hostname); + InetAddress addr = null; + String host = null; + try { + addr = InetAddress.getByName(hostname); + } catch (UnknownHostException e) { + host = hostname; + } + this.hostname = host; + this.addr = addr; + this.port = checkPort(port); + } + + public static InetSocketAddress createUnresolved(String host, int port) { + return new InetSocketAddress(checkHost(host), null, checkPort(port)); + } + + public static void enableDelay() { + enableDelay = true; + } + + public static void disableDelay() { + enableDelay = false; + } + + private InetSocketAddress(String hostname, InetAddress addr, int port) { + this.hostname = hostname; + this.addr = addr; + this.port = port; + if (enableDelay) { + doDelay(); + } + } + + /** + * Gets the port number. + * + * @return the port number. + */ + public final int getPort() { + if (enableDelay) { + doDelay(); + } + return this.port; + } + + /** + * Gets the {@code InetAddress}. + * + * @return the InetAddress or {@code null} if it is unresolved. + */ + public final InetAddress getAddress() { + return this.addr; + } + + public final String getHostName() { + if (hostname != null) { + return hostname; + } + if (addr != null) { + return addr.getHostName(); + } + return null; + } + + public final String getHostString() { + if (hostname != null) { + return hostname; + } + if (addr != null) { + if (addr.holder().getHostName() != null) { + return addr.holder().getHostName(); + } else { + return addr.getHostAddress(); + } + } + return null; + } + + public final boolean isUnresolved() { + return addr == null; + } + + @Override + public String toString() { + String formatted; + if (isUnresolved()) { + formatted = hostname + "/"; + } else { + formatted = addr.toString(); + if (addr instanceof Inet6Address) { + int i = formatted.lastIndexOf("/"); + formatted = formatted.substring(0, i + 1) + + "[" + formatted.substring(i + 1) + "]"; + } + } + return formatted + ":" + port; + } + + @Override + public final boolean equals(Object other) { + if (!(other instanceof InetSocketAddress that)) { + return false; + } + boolean sameIP; + if (addr != null) { + sameIP = addr.equals(that.addr); + } else if (hostname != null) { + sameIP = (that.addr == null) && + hostname.equalsIgnoreCase(that.hostname); + } else { + sameIP = (that.addr == null) && (that.hostname == null); + } + return sameIP && (port == that.port); + } + + @Override + public final int hashCode() { + if (addr != null) { + return addr.hashCode() + port; + } + if (hostname != null) { + return hostname.toLowerCase(Locale.ROOT).hashCode() + port; + } + return port; + } + + private static int checkPort(int port) { + if (port < 0 || port > 0xFFFF) + throw new IllegalArgumentException("port out of range:" + port); + return port; + } + + private static String checkHost(String hostname) { + if (hostname == null) + throw new IllegalArgumentException("hostname can't be null"); + return hostname; + } + + private static void doDelay() { + System.out.println("intentional delay injected in InetSocketAddress"); + try { + Thread.sleep(10); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + } + } +}