/** * Licensed to the Apache Software Foundation (ASF) under one or more * contributor license agreements. See the NOTICE file distributed with * this work for additional information regarding copyright ownership. * The ASF licenses this file to You under the Apache License, Version 2.0 * (the "License"); you may not use this file except in compliance with * the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package org.apache.openejb.server.discovery; import org.apache.openejb.monitoring.Event; import org.apache.openejb.monitoring.Managed; import org.apache.openejb.server.ServerRuntimeException; import org.apache.openejb.util.DaemonThreadFactory; import org.apache.openejb.util.Duration; import org.apache.openejb.util.Join; import org.apache.openejb.util.LogCategory; import org.apache.openejb.util.Logger; import java.io.ByteArrayOutputStream; import java.io.EOFException; import java.io.IOException; import java.net.InetAddress; import java.net.InetSocketAddress; import java.net.ServerSocket; import java.net.Socket; import java.net.URI; import java.nio.ByteBuffer; import java.nio.channels.CancelledKeyException; import java.nio.channels.ClosedChannelException; import java.nio.channels.SelectionKey; import java.nio.channels.Selector; import java.nio.channels.ServerSocketChannel; import java.nio.channels.SocketChannel; import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; import java.util.Comparator; import java.util.HashMap; import java.util.HashSet; import java.util.Iterator; import java.util.LinkedHashSet; import java.util.LinkedList; import java.util.List; import java.util.Map; import java.util.Random; import java.util.Set; import java.util.concurrent.Callable; import java.util.concurrent.ExecutionException; import java.util.concurrent.Executor; import java.util.concurrent.Executors; import java.util.concurrent.FutureTask; import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeoutException; import java.util.concurrent.atomic.AtomicBoolean; import java.util.concurrent.locks.Condition; import java.util.concurrent.locks.Lock; import java.util.concurrent.locks.ReentrantLock; /** * @version $Rev$ $Date$ */ @Managed public class MultipointServer { private static final Logger log = Logger.getInstance(LogCategory.OPENEJB_SERVER.createChild("discovery").createChild("multipoint"), MultipointServer.class); private static final URI END_LIST = URI.create("end:list"); private final int port; private final URI me; private final Set<URI> roots = new LinkedHashSet<URI>(); private final Event runs = new Event(); private final Event heartbeats = new Event(); private final Event reconnects = new Event(); private final Event sessionsCreated = new Event(); /** * Only used for toString to make debugging easier */ private final String name; private final Tracker tracker; private final LinkedList<Host> connect = new LinkedList<Host>(); private final Map<URI, Session> connections = new HashMap<URI, Session>(); private long joined = 0; private final long reconnectDelay; private final ServerSocketChannel serverChannel; private final Selector selector; private final Lock lock = new ReentrantLock(); private final Condition started = lock.newCondition(); private final Condition stopped = lock.newCondition(); public MultipointServer(final int port, final Tracker tracker) throws IOException { this("localhost", "localhost", port, tracker, randomColor(), true, new HashSet<URI>(0), new Duration(30, TimeUnit.SECONDS)); } public MultipointServer(final String bindHost, String broadcastHost, final int port, final Tracker tracker, final String name, final boolean debug, final Set<URI> roots, Duration reconnectDelay) throws IOException { if (tracker == null) throw new NullPointerException("tracker cannot be null"); if (bindHost == null) throw new NullPointerException("host cannot be null"); if (broadcastHost == null) broadcastHost = bindHost; if (reconnectDelay == null) reconnectDelay = new Duration(30, TimeUnit.SECONDS); this.tracker = tracker; this.name = name; if (roots != null) { for (final URI uri : roots) { this.roots.add(normalize(uri)); } } this.reconnectDelay = reconnectDelay.getTime(TimeUnit.NANOSECONDS); final String format = String.format("MultipointServer(bindHost=%s, discoveryHost=%s, port=%s, name=%s, debug=%s, roots=%s, reconnectDelay='%s')", bindHost, broadcastHost, port, name, debug, this.roots.size(), reconnectDelay.toString()); log.debug(format); selector = Selector.open(); final InetSocketAddress address = new InetSocketAddress(bindHost, port); serverChannel = ServerSocketChannel.open(); serverChannel.configureBlocking(false); final ServerSocket serverSocket = serverChannel.socket(); serverSocket.bind(address); this.port = serverSocket.getLocalPort(); me = createURI(broadcastHost, this.port); serverChannel.register(selector, SelectionKey.OP_ACCEPT); println("Broadcasting"); } private URI normalize(final URI uri) { return createURI(uri.getHost(), uri.getPort()); } private URI createURI(final String host, final int port) { return URI.create("conn://" + host.toLowerCase() + ":" + port); } public URI getMe() { return me; } public Set<URI> getRoots() { return roots; } public Event getRuns() { return runs; } public Event getHeartbeats() { return heartbeats; } public Event getReconnects() { return reconnects; } public Event getSessionsCreated() { return sessionsCreated; } public String getName() { return name; } public long getJoined() { return joined; } public List<URI> getSessions() { return new ArrayList<URI>(connections.keySet()); } public List<URI> getConnectionsQueued() { synchronized (connect) { final ArrayList<URI> uris = new ArrayList<URI>(connect.size()); for (final Host host : connect) { uris.add(host.getUri()); } return uris; } } public long getReconnectDelay() { return reconnectDelay; } public int getPort() { return port; } /** * Attempt to connect back to the network if * - We aren't already connected * - We aren't already attempting to connect * - It has been a while since we last tried (reconnectDelay) */ private void rejoin() { if (roots.size() <= 0) return; if (System.nanoTime() - joined <= reconnectDelay) return; for (final URI uri : roots) { final Host host = new Host(uri); synchronized (connect) { if (!connections.containsKey(uri) && !connect.contains(host)) { log.info("Reconnect{uri=" + uri + "}"); connect.addLast(host); host.resolveDns(); this.joined = System.nanoTime(); } } } } public MultipointServer start() { if (running.compareAndSet(false, true)) { final String multipointServer = Join.join(".", "MultipointServer", name, port); log.info("MultipointServer Starting : Thread '" + multipointServer + "'"); final Thread thread = new Thread(new Runnable() { @Override public void run() { signal(started); try { _run(); } finally { signal(stopped); } } }); thread.setName(multipointServer); thread.start(); await(started, 10, TimeUnit.SECONDS); } return this; } private void signal(final Condition condition) { lock.lock(); try { condition.signal(); } finally { lock.unlock(); } } private void await(final Condition condition, final long time, final TimeUnit unit) { lock.lock(); try { condition.await(time, unit); } catch (InterruptedException e) { Thread.interrupted(); } finally { lock.unlock(); } } public void stop() { running.set(false); try { serverChannel.close(); } catch (IOException e) { throw new CloseException(e); } finally { await(stopped, 10, TimeUnit.SECONDS); } } public static class CloseException extends RuntimeException { public CloseException(final Throwable cause) { super(cause); } } public class Session { private static final int EOF = 3; private final SocketChannel channel; private final ByteBuffer read = ByteBuffer.allocate(1024); private final SelectionKey key; private final List<URI> listed = new ArrayList<URI>(); private final long created = System.currentTimeMillis(); private ByteBuffer write; @Managed private State state = State.OPEN; private URI uri; public boolean hangup; private final boolean client; public Session(final SocketChannel channel, final InetSocketAddress address, final URI uri) throws ClosedChannelException { this.channel = channel; this.client = uri != null; this.uri = uri != null ? uri : createURI(address.getHostName(), address.getPort()); this.key = channel.register(selector, 0, this); sessionsCreated.record(); log.info("Constructing " + this); } public Session ops(final int ops) { key.interestOps(ops); return this; } public long getCreated() { return created; } public void state(final int ops, final State state) { // trace("transition "+state +" "+ops); if (this.state != state) { if (log.isDebugEnabled()) { log.debug(message(state.name())); } } this.state = state; if (ops > 0) key.interestOps(ops); } public void setURI(final URI uri) { this.uri = uri; } private void trace(final String str) { // println(message(str)); if (log.isDebugEnabled()) { log.debug(message(str)); // new Exception().fillInStackTrace().printStackTrace(); } } private void info(final String str) { // println(message(str)); if (log.isInfoEnabled()) { log.info(message(str)); } } private String message(final String str) { final StringBuilder sb = new StringBuilder(); sb.append(name); sb.append(":"); sb.append(port); sb.append(" "); if (key.isValid()) { if ((key.interestOps() & SelectionKey.OP_READ) == SelectionKey.OP_READ) sb.append("<"); if ((key.interestOps() & SelectionKey.OP_WRITE) == SelectionKey.OP_WRITE) sb.append(">"); if ((key.interestOps() == 0)) sb.append("-"); } else { sb.append(":"); } sb.append(" "); sb.append(uri.getPort()); sb.append(" "); sb.append(this.state); sb.append(" "); sb.append(str); return sb.toString(); } public void write(final URI uri) throws IOException { write(Arrays.asList(uri)); } public void write(final Collection<?> uris) throws IOException { final ByteArrayOutputStream baos = new ByteArrayOutputStream(); for (final Object uri : uris) { final String s = uri.toString(); final byte[] b = s.getBytes("UTF-8"); baos.write(b); baos.write(EOF); } this.write = ByteBuffer.wrap(baos.toByteArray()); } public boolean drain() throws IOException { this.channel.write(write); return write.remaining() == 0; } public String read() throws IOException { if (channel.read(read) == -1) throw new EOFException(); final byte[] buf = read.array(); final int end = endOfText(buf, 0, read.position()); if (end < 0) return null; // Copy the string without the terminator char final String text = new String(buf, 0, end, "UTF-8"); final int newPos = read.position() - end; System.arraycopy(buf, end + 1, buf, 0, newPos - 1); read.position(newPos - 1); return text; } private int endOfText(final byte[] data, final int offset, final int pos) { for (int i = offset; i < pos; i++) { if (data[i] == EOF) return i; } return -1; } @Override public String toString() { return "Session{" + "uri=" + uri + ", created=" + created + ", state=" + state + ", owner=" + port + ", s=" + (client ? channel.socket().getPort() : channel.socket().getLocalPort()) + ", c=" + (!client ? channel.socket().getPort() : channel.socket().getLocalPort()) + ", " + (client ? "client" : "server") + '}'; } private long last = 0; public void tick() throws IOException { if (state != State.HEARTBEAT) return; final long now = System.currentTimeMillis(); final long delay = now - last; if (delay >= tracker.getHeartRate()) { last = now; heartbeat(); } } private void heartbeat() throws IOException { heartbeats.record(); final Set<String> strings = tracker.getRegisteredServices(); // for (String string : strings) { // trace(string); // } write(strings); state(SelectionKey.OP_READ | SelectionKey.OP_WRITE, State.HEARTBEAT); } } private static enum State { OPEN, GREETING, LISTING, HEARTBEAT, CLOSED } private final AtomicBoolean running = new AtomicBoolean(); private void _run() { // The selectorTimeout ensures that even when there are no IO events, // this loop will "wake up" and execute at least as frequently as the // expected heartrate. // // We initiate WRITE events (the heartbeats we send) in this loop, so that // detail is critical. long selectorTimeout = tracker.getHeartRate(); // For reliability purposes we will actually adjust the selectorTimeout // on each iteration of the loop, shrinking it down just a little to a // account for the execution time of the loop itself. int failed = 0; while (running.get()) { runs.record(); final long start = System.nanoTime(); try { selector.select(selectorTimeout); failed = 0; } catch (IOException ex) { if (failed++ > 100) { log.fatal("Too many Multipoint Failures. Terminating service.", ex); return; } log.error("Multipoint Failure.", ex); try { Thread.sleep(100); } catch (InterruptedException e) { Thread.interrupted(); } } final Set keys = selector.selectedKeys(); final Iterator iterator = keys.iterator(); while (iterator.hasNext()) { final SelectionKey key = (SelectionKey) iterator.next(); iterator.remove(); try { if (key.isAcceptable()) doAccept(key); if (key.isConnectable()) doConnect(key); if (key.isReadable()) doRead(key); if (key.isWritable()) doWrite(key); } catch (CancelledKeyException | ClosedChannelException ex) { synchronized (connect) { final Session session = (Session) key.attachment(); if (session.state != State.CLOSED) { close(key); } } } catch (IOException ex) { final Session session = (Session) key.attachment(); session.trace(ex.getClass().getSimpleName() + ": " + ex.getMessage()); close(key); } } // This loop can generate WRITE keys (the heartbeats we send) for (final SelectionKey key : selector.keys()) { final Session session = (Session) key.attachment(); try { if (session != null && session.state == State.HEARTBEAT) session.tick(); } catch (IOException ex) { close(key); } } // Here is where we actually will expire missing services tracker.checkServices(); // Fill 'connections' list if we are fully disconnected rejoin(); // Connect to anyone in the 'connections' list initiateConnections(); // Adjust selector timeout so we execute in even increments // This keeps the heartbeat and rejoin regular selectorTimeout = adjustedSelectorTimeout(start); } log.info("MultipointServer has terminated."); } private long adjustedSelectorTimeout(final long start) { final long end = System.nanoTime(); final long elapsed = TimeUnit.NANOSECONDS.toMillis(end - start); final long heartRate = tracker.getHeartRate(); return Math.max(1, heartRate - elapsed); } private void initiateConnections() { synchronized (connect) { final LinkedList<Host> unresolved = new LinkedList<Host>(); while (connect.size() > 0) { final Host host = connect.removeFirst(); log.debug("Initiate(uri=" + host.getUri() + ")"); if (connections.containsKey(host.getUri())) continue; if (!host.isDone()) { unresolved.add(host); log.debug("Unresolved(uri=" + host.getUri() + ")"); continue; } final InetSocketAddress address; try { address = host.getSocketAddress(); } catch (ExecutionException e) { final Throwable t = (e.getCause() != null) ? e.getCause() : e; final String message = String.format("Failed Connect{uri=%s} %s{message=\"%s\"}", host.getUri(), t.getClass().getSimpleName(), t.getMessage()); log.warning(message); continue; } catch (TimeoutException e) { unresolved.add(host); log.debug("Unresolved(uri=" + host.getUri() + ")"); continue; } try { final URI uri = host.getUri(); println("open " + uri); // Create a non-blocking NIO channel final SocketChannel socketChannel = SocketChannel.open(); socketChannel.configureBlocking(false); socketChannel.connect(address); final Session session = new Session(socketChannel, address, uri); session.ops(SelectionKey.OP_CONNECT); session.trace("client"); connections.put(session.uri, session); // seen - needs to get maintained as "connected" // TODO remove from seen } catch (IOException e) { throw new ServerRuntimeException(e); } } connect.addAll(unresolved); } } private void doWrite(final SelectionKey key) throws IOException { final Session session = (Session) key.attachment(); switch (session.state) { case GREETING: { // write // Only CLIENTs write a GREETING message // As we are a client, the first thing we do // is READ the server's LIST if (session.drain()) { session.state(SelectionKey.OP_READ, State.LISTING); } } break; case LISTING: { // write if (session.drain()) { if (session.client) { // CLIENTs list last, so at this point we've read // the server's list and have written ours // session.trace("DONE WRITING"); session.state(SelectionKey.OP_READ, State.HEARTBEAT); } else { // SERVERs always write their list first, so at this // point we switch to LIST READ mode session.state(SelectionKey.OP_READ, State.LISTING); } } } break; case HEARTBEAT: { // write if (session.drain()) { session.last = System.currentTimeMillis(); // session.trace("send"); session.state(SelectionKey.OP_READ, State.HEARTBEAT); } } break; } } private void doRead(final SelectionKey key) throws IOException { final Session session = (Session) key.attachment(); switch (session.state) { case GREETING: { // read // This state is only reachable as a SERVER // The client connected and said hello by sending // its URI to let us know who they are // Once this is read, the client will expect us // to send our full list of URIs followed by the // "end" address. // So we switch to WRITE LISTING and they switch // to READ LISTING // Then we will switch to READ LISTING and they // will switch to WRITE LISTING final String message = session.read(); if (message == null) break; // need to read more session.setURI(URI.create(message)); connected(session); session.trace("welcome"); final ArrayList<URI> list = connections(); // When they read themselves on the list // they'll know it's time to list their URIs list.remove(me); // yank list.add(END_LIST); // add to the end session.write(list); session.state(SelectionKey.OP_WRITE, State.LISTING); session.trace("STARTING"); } break; case LISTING: { // read String message; while ((message = session.read()) != null) { // session.trace(message); final URI uri = URI.create(message); if (END_LIST.equals(uri)) { if (session.client) { final ArrayList<URI> list = connections(); for (final URI reported : session.listed) { list.remove(reported); } // When they read us on the list // they'll know it's time to switch to heartbeat list.remove(session.uri); list.add(END_LIST); session.write(list); session.state(SelectionKey.OP_WRITE, State.LISTING); } else { // We are a SERVER in this relationship, so we will have already // listed our known peers by this point. From here we switch to // heartbeating // heartbeat time if (session.hangup) { session.state(0, State.CLOSED); session.trace("hangup"); hangup(key); } else { session.trace("DONE READING"); session.state(SelectionKey.OP_READ, State.HEARTBEAT); } } break; } else { session.listed.add(uri); try { connect(uri); } catch (Exception e) { println("connect failed " + uri + " - " + e.getMessage()); e.printStackTrace(); } } } } break; case HEARTBEAT: { // read String message; while ((message = session.read()) != null) { // session.trace(message); tracker.processData(message); } } break; } } private void doConnect(final SelectionKey key) throws IOException { // we are a client final Session session = (Session) key.attachment(); session.channel.finishConnect(); session.trace("connected"); // when you are a client, first say high to everyone // before accepting data // once a server reads our address, it will send it's // full list of known addresses, followed by the "end" // address to signal that it is done. // we will then send our full list of known addresses, // followed by the "end" address to signal we are done. // Afterward the server will only pulls its heartbeat // separately, we will initiate connections to everyone // in the list who we have not yet seen. // WRITE our GREETING session.write(me); session.state(SelectionKey.OP_WRITE, State.GREETING); } private void doAccept(final SelectionKey key) throws IOException { // we are a server // when you are a server, we must first listen for the // address of the client before sending data. // once they send us their address, we will send our // full list of known addresses, followed by the "end" // address to signal that we are done. // Afterward we will only pulls our heartbeat final ServerSocketChannel server = (ServerSocketChannel) key.channel(); final SocketChannel client = server.accept(); final InetSocketAddress address = (InetSocketAddress) client.socket().getRemoteSocketAddress(); client.configureBlocking(false); final Session session = new Session(client, address, null); session.trace("accept"); session.state(SelectionKey.OP_READ, State.GREETING); } private ArrayList<URI> connections() { synchronized (connect) { final ArrayList<URI> list = new ArrayList<URI>(connections.keySet()); for (final Host host : connect) { list.add(host.getUri()); } return list; } } private void close(final SelectionKey key) { final Session session = (Session) key.attachment(); if (session.hangup) { // This was a duplicate connection and was closed // do not remove this URI from the 'connections' // map as this particular session is not in that // map -- only the good session that will not be // closed is in there. log.info("Hungup " + session); session.trace("hungup"); } else { log.info("Closed " + session); session.trace("closed"); synchronized (connect) { connections.remove(session.uri); } } session.state(0, State.CLOSED); hangup(key); } private void hangup(final SelectionKey key) { key.cancel(); try { key.channel().close(); } catch (IOException cex) { //Ignore } } public void connect(final MultipointServer s) throws Exception { connect(s.port); } public void connect(final int port) throws Exception { connect(URI.create("conn://localhost:" + port)); } public void connect(URI uri) { uri = normalize(uri); if (me.equals(uri)) return; final Host host = new Host(uri); synchronized (connect) { if (!connections.containsKey(uri) && !connect.contains(host)) { log.info("Queuing{uri=" + uri + "}"); connect.addLast(host); host.resolveDns(); } } } private void connected(Session session) { synchronized (connect) { Session duplicate = connections.get(session.uri); // Session duplicate = null; if (duplicate != null) { session.trace("duplicate"); // At this point we know we have two sockets open // to the client, this can happen in two different ways // // 1. one created by them and one created by us. // 2. two created by them and none created by us. // // For case #1, we will both have detected this situation // and know it needs fixing. Only one of us can hangup // // For case #2, the client was likely disconnected and // is calling back. final Session[] sessions = {session, duplicate}; if (!sessions[0].client && !sessions[1].client) { // Case 1 -- Client is calling back Arrays.sort(sessions, new Comparator<Session>() { @Override public int compare(final Session a, final Session b) { return (int) (b.created - a.created); } }); } else { // Case 2 -- We called each other at the same time Arrays.sort(sessions, new Comparator<Session>() { // Goal: Keep the connection with the lowest port number /// // Low vs high is not very significant. The critical // part is that they both choose the same connection. // // Port numbers are seen on both sides. There are two // ports (one client and one server) for each connection. // // Both sides will agree to kill the connection with the // lowest server port. If those are the same, then both // sides will agree to kill the connection with the lowest // client port. If those are the same, we still close a // connection and hope for the best. If both connections // are killed we will try again next time another node // lists the server and we notice we are not connected. // @Override public int compare(final Session a, final Session b) { final int serverRank = server(a) - server(b); if (serverRank != 0) return serverRank; return client(a) - client(b); } private int server(final Session a) { final Socket socket = a.channel.socket(); return a.client ? socket.getPort() : socket.getLocalPort(); } private int client(final Session a) { final Socket socket = a.channel.socket(); return !a.client ? socket.getPort() : socket.getLocalPort(); } }); } session = sessions[0]; duplicate = sessions[1]; session.trace(session + "@" + session.hashCode() + " KEEP"); duplicate.trace(duplicate + "@" + duplicate.hashCode() + " KILL"); duplicate.hangup = true; } if (session.state == State.GREETING) { session.info(session + "@" + session.hashCode() + " DISCOVERED"); } connections.put(session.uri, session); } } private void println(final String s) { // if (debug && s.matches(".*(Listening|DONE|KEEP|KILL)")) { // System.out.format("%1$tH:%1$tM:%1$tS.%1$tL - %2$s\n", System.currentTimeMillis(), s); // } } @Override public String toString() { return "MultipointServer{" + "name='" + name + '\'' + ", me=" + me + '}'; } public static String randomColor() { final String[] colors = { "almond", "amber", "amethyst", "apple", "apricot", "aqua", "aquamarine", "ash", "azure", "banana", "beige", "black", "blue", "brick", "bronze", "brown", "burgundy", "carrot", "charcoal", "cherry", "chestnut", "chocolate", "chrome", "cinnamon", "citrine", "cobalt", "copper", "coral", "cornflower", "cotton", "cream", "crimson", "cyan", "ebony", "emerald", "forest", "fuchsia", "ginger", "gold", "goldenrod", "gray", "green", "grey", "indigo", "ivory", "jade", "jasmine", "khaki", "lava", "lavender", "lemon", "lilac", "lime", "macaroni", "magenta", "magnolia", "mahogany", "malachite", "mango", "maroon", "mauve", "mint", "moonstone", "navy", "ocean", "olive", "onyx", "orange", "orchid", "papaya", "peach", "pear", "pearl", "periwinkle", "pine", "pink", "pistachio", "platinum", "plum", "prune", "pumpkin", "purple", "quartz", "raspberry", "red", "rose", "rosewood", "ruby", "salmon", "sapphire", "scarlet", "sienna", "silver", "slate", "strawberry", "tan", "tangerine", "taupe", "teal", "titanium", "topaz", "turquoise", "umber", "vanilla", "violet", "watermelon", "white", "yellow" }; final Random random = new Random(); long l = random.nextLong(); if (l < 0) l *= -1; final long index = l % colors.length; return colors[(int) index]; } private final Executor dnsResolutionQueue = Executors.newFixedThreadPool(2, new DaemonThreadFactory("multipoint-server-")); private class Host { private final URI uri; private final FutureTask<InetAddress> address; private Host(final URI uri) { this.uri = uri; this.address = new FutureTask<InetAddress>(new Callable<InetAddress>() { @Override public InetAddress call() throws Exception { return InetAddress.getByName(Host.this.uri.getHost()); } }); } public void resolveDns() { dnsResolutionQueue.execute(address); } public boolean isDone() { return address.isDone(); } public InetSocketAddress getSocketAddress() throws ExecutionException, TimeoutException { try { final InetAddress inetAddress = address.get(0, TimeUnit.NANOSECONDS); return new InetSocketAddress(inetAddress, uri.getPort()); } catch (InterruptedException e) { Thread.interrupted(); throw new TimeoutException(); } } public URI getUri() { return uri; } @Override public boolean equals(final Object o) { if (this == o) return true; if (o == null || getClass() != o.getClass()) return false; final Host host = (Host) o; return uri.equals(host.uri); } @Override public int hashCode() { return uri.hashCode(); } } }