All Downloads are FREE. Search and download functionalities are using the official Maven repository.

com.oracle.coherence.common.internal.net.socketbus.AbstractSocketBus Maven / Gradle / Ivy

/*
 * Copyright (c) 2000, 2022, Oracle and/or its affiliates.
 *
 * Licensed under the Universal Permissive License v 1.0 as shown at
 * https://oss.oracle.com/licenses/upl.
 */
package com.oracle.coherence.common.internal.net.socketbus;

import com.oracle.coherence.common.base.Blocking;
import com.oracle.coherence.common.base.Collector;
import com.oracle.coherence.common.base.Continuation;
import com.oracle.coherence.common.base.Disposable;
import com.oracle.coherence.common.collections.Arrays;
import com.oracle.coherence.common.internal.continuations.AbstractContinuationFrame;
import com.oracle.coherence.common.internal.continuations.Continuations;
import com.oracle.coherence.common.internal.continuations.WrapperContinuation;
import com.oracle.coherence.common.net.SafeSelectionHandler;
import com.oracle.coherence.common.net.SelectionService;
import com.oracle.coherence.common.net.Sockets;
import com.oracle.coherence.common.net.exabus.Bus;
import com.oracle.coherence.common.net.exabus.EndPoint;
import com.oracle.coherence.common.net.exabus.Event;
import com.oracle.coherence.common.net.exabus.util.SimpleEvent;
import com.oracle.coherence.common.net.exabus.util.UrlEndPoint;

import java.io.IOException;

import java.net.*;

import java.nio.ByteBuffer;
import java.nio.channels.ClosedChannelException;
import java.nio.channels.GatheringByteChannel;
import java.nio.channels.ScatteringByteChannel;
import java.nio.channels.SelectableChannel;
import java.nio.channels.ServerSocketChannel;
import java.nio.channels.SocketChannel;

import java.util.*;

import com.oracle.coherence.common.util.SafeClock;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;
import java.util.concurrent.ThreadLocalRandom;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.atomic.AtomicLong;
import java.util.concurrent.locks.ReadWriteLock;
import java.util.concurrent.locks.ReentrantReadWriteLock;
import java.util.concurrent.locks.Lock;
import java.util.logging.Level;
import java.util.logging.LogRecord;
import java.util.logging.Logger;

import javax.net.ssl.SSLException;

import java.util.zip.CRC32;


/**
 * AbstractSocketBus provides a common base class for Socket based Bus
 * implementations.
 * 

* This abstract implementation handles the underlying connection management. * * @author mf 2010.11.2 */ public abstract class AbstractSocketBus implements Bus { // ----- constructors --------------------------------------------------- /** * Create an AbstractSocketBus around a ServerSocketChannel * * @param driver the SocketDriver used to produce the bus sockets * @param pointLocal the local EndPoint * * @throws IOException if an I/O error occurs */ public AbstractSocketBus(SocketBusDriver driver, UrlEndPoint pointLocal) throws IOException { f_driver = driver; String sProtocol = getProtocolName(); if (!sProtocol.equals(pointLocal.getProtocol())) { throw new IllegalArgumentException("unsupported protocol: " + pointLocal.getProtocol()); } ServerSocketChannel chan = driver.getDependencies().getSocketProvider() .openServerSocketChannel(); Sockets.configureBlocking(chan, false); configureSocket(chan.socket()); chan.socket().bind(pointLocal.getAddress()); f_nDropRatio = driver.getDependencies().getDropRatio(); f_nCorruptionRatio = driver.getDependencies().getCorruptionRatio(); f_fCrc = driver.getDependencies().isCrcEnabled(); f_channelServer = chan; // as the supplied endpoint may have been wildcard and/or ephemeral, // re-resolve based on what we actually bound to m_pointLocal = driver.resolveBindPoint(pointLocal, chan.socket()); } // ----- Bus interface -------------------------------------------------- /** * {@inheritDoc} */ public EndPoint getLocalEndPoint() { return m_pointLocal; } /** * {@inheritDoc} */ public void open() { Lock lock = f_lockState.writeLock(); lock.lock(); boolean fInterrupted = isInterrupted(); try { verifyState(BusState.INITIAL); m_nState = BusState.OPEN; onOpen(); try { ServerSocketChannel channel = f_channelServer; getSelectionService().register(f_channelServer, new AcceptHandler(channel)); } catch (IOException e) { // We can't just close the bus, there is no concept of an unsolicited close, we are still bound to // the port, just leave it be. getLogger().log(makeExceptionRecord(Level.SEVERE, e, "{0} ServerSocket failure; no new connection will be accepted", getLocalEndPoint())); } } finally { lock.unlock(); if (fInterrupted) { Thread.currentThread().interrupt(); } } } /** * {@inheritDoc} */ public void close() { Lock lock = f_lockState.writeLock(); lock.lock(); boolean fInterrupted = isInterrupted(); try { if (m_nState.ordinal() >= BusState.CLOSING.ordinal()) { return; // close is idempotent } verifyState(BusState.OPEN); // stop allowing new connections m_nState = BusState.CLOSING; Map mapConn = f_mapConnections; // identify how many things we have to "close" before we can put out the // CLOSE event final AtomicInteger cConnections = new AtomicInteger(mapConn.size() + 1); // close the server socket getSelectionService().invoke(f_channelServer,new Runnable() { public void run() { try { f_channelServer.close(); } catch (IOException e) {} if (cConnections.decrementAndGet() == 0) { onClose(); } } }, /*cMillisDelay*/ 0); // release all open connections for (Connection conn : mapConn.values()) { synchronized (conn) { conn.scheduleShutdown(null, /*fRelease*/ true, new Continuation() { @Override public void proceed(Void v) { if (cConnections.decrementAndGet() == 0) { onClose(); } } }); } } } catch (IOException e) { throw new IllegalStateException(e); } finally { lock.unlock(); if (fInterrupted) { Thread.currentThread().interrupt(); } } } /** * {@inheritDoc} */ public void connect(EndPoint peer) { Lock lock = f_lockState.readLock(); lock.lock(); boolean fInterrupted = isInterrupted(); try { verifyState(BusState.OPEN); if (getLocalEndPoint().equals(peer)) { // Support for self-connect if desired needs to be added in // derived classes, there is no reason to push those bits through // the network stack throw new IllegalArgumentException("SocketBus does not support connections to self"); } Connection conn = makeConnection(verifyEndPoint(peer)); synchronized (conn) { if (f_mapConnections.putIfAbsent(peer, conn) == null) { conn.connect(); } else { // there is already an existing Connection for this peer // and it has not reached the RELEASE state in a visible way // so we can't replace it. This is not an error, it is the // equivalent, of a double connect, i.e. a no-op conn.dispose(); } } } finally { lock.unlock(); if (fInterrupted) { Thread.currentThread().interrupt(); } } } /** * {@inheritDoc} */ public void disconnect(EndPoint peer) { Connection conn = ensureConnection(peer); synchronized (conn) { conn.ensureValid().scheduleDisconnect(null); } } /** * {@inheritDoc} */ public void release(EndPoint peer) { Connection conn = ensureConnection(peer); synchronized (conn) { conn.ensureValid().scheduleShutdown(null, /*fRelease*/ true, /*continuation*/ null); } } @Override public String toString(EndPoint peer) { try { return ensureConnection(peer).toString(); } catch (Throwable e) { return "unknown peer " + peer; } } /** * {@inheritDoc} */ public void flush() { BusState nState = m_nState; if (nState != BusState.OPEN && nState != BusState.CLOSING) { throw new IllegalStateException("invalid bus state: " + nState); } boolean fInterrupted = isInterrupted(); try { // flush the connections in random order, this helps prevent one heavily used connection // from starving others if it happens to exist earlier in the unordered set. Connection[] aConn = f_setFlush.toArray(EMPTY_CONNECTION_ARRAY); Arrays.shuffle(aConn); for (Connection conn : aConn) { conn.optimisticFlush(); // it is up to the connection to remove itself from the flush set } } finally { if (fInterrupted) { Thread.currentThread().interrupt(); } } } /** * {@inheritDoc} */ public void setEventCollector(Collector collector) { Lock lock = f_lockState.readLock(); lock.lock(); try { verifyState(BusState.INITIAL); m_collectorEvent = collector; } finally { lock.unlock(); } } /** * {@inheritDoc} */ public Collector getEventCollector() { return m_collectorEvent; } // ----- AbstractSocketBus interface ------------------------------------ /** * Schedule a task for future execution. * * @param proc the task to run * @param cMillis the delay before execution, or 0 for immediate * * @throws IllegalStateException if the Bus is not open */ protected void scheduleTask(final Runnable proc, long cMillis) { scheduleTask(f_channelServer, proc, cMillis); } /** * Schedule a task for future execution. Unlike scheduleTask, this variant will run the task even * if the bus has been closed * * @param proc the task to run * @param cMillis the delay before execution, or 0 for immediate * * @throws IllegalStateException if the Bus is not open */ protected void scheduleUnsafeTask(final Runnable proc, long cMillis) { scheduleUnsafeTask(f_channelServer, proc, cMillis); } /** * Schedule a task for future execution. * * @param chan the channel that the task is associated with * @param proc the task to run * @param cMillis the delay before execution, or 0 for immediate * * @throws IllegalStateException if the Bus is not open */ protected void scheduleTask(final SelectableChannel chan, final Runnable proc, long cMillis) { BusState nState = m_nState; if (nState != BusState.OPEN && nState != BusState.CLOSING) { throw new IllegalStateException("invalid bus state: " + nState); } scheduleUnsafeTask(chan, new Runnable() { @Override public void run() { if (m_nState != BusState.CLOSED) { proc.run(); } } }, cMillis); } /** * Schedule a task for future execution. Unlike scheduleTask, this variant will run the task even * if the bus has been closed * * @param chan the channel that the task is associated with * @param proc the task to run * @param cMillis the delay before execution, or 0 for immediate * * @throws IllegalStateException if the Bus is not open */ protected void scheduleUnsafeTask(final SelectableChannel chan, final Runnable proc, long cMillis) { try { getSocketDriver().getDependencies().getSelectionService().invoke(chan, proc, cMillis); } catch (IOException e) { throw new IllegalStateException(e); } } /** * Aggressively tear stop the bus instance. *

* The intent of this method is not to properly shutdown (close) the bus, but stop a bus which * has entered an "unrecoverable" state. Specifically we are trying to protect our peers not us. */ protected void halt() { try { f_channelServer.close(); } catch (Exception e) {} for (Connection conn : getConnections()) { try { conn.m_channel.close(); } catch (Exception e) {} } } /** * Determine if a connection drop should be simulated * * @return true if the connection has been dropped */ private boolean checkDrop(SocketChannel channel) { if (f_nDropRatio != 0 && ThreadLocalRandom.current().nextInt(Math.abs(f_nDropRatio)) == 0) { closeChannel(channel); return true; } return false; } /** * Determine if a data corruption should be simulated, if so force it. */ private void checkForceCorruption(ByteBuffer buffer, int cb) { Random random = ThreadLocalRandom.current(); if (f_nCorruptionRatio != 0 && random.nextInt(Math.abs(f_nCorruptionRatio)) == 0) { int nCorrupt = buffer.position() - (random.nextInt(cb) + 1); buffer.put(nCorrupt, (byte) random.nextInt()); } } /** * Return the first buffer that has bytes remaing, or null if none match. * * @return the first buffer that has bytes remaing, or null if none match. */ private ByteBuffer getFirstAvailableForCorruption(ByteBuffer[] aBuffer, int of) { int c = aBuffer.length; for (; of < c && !aBuffer[of].hasRemaining(); ++of) {} return of < c ? aBuffer[of] : null; } /** * For debugging purposes only. * * Close the underlying TCP socket associated with the specified peer. * This allows for testing of connection reestablishment. * * @param sPeerName the peer to act on, or null for all * @param nClose the type of close to perform, see CLOSE_SOCKET* */ public void sever(String sPeerName, int nClose) { Connection[] aConn; if (sPeerName == null) { aConn = f_mapConnections.values().stream().toArray(Connection[]::new); } else { aConn = new Connection[] {ensureConnection(f_driver.getDepot().resolveEndPoint(sPeerName))}; } for (Connection conn : aConn) try { switch (nClose) { case SOCKET_SHUTDOWN_INPUT: conn.m_channel.shutdownInput(); break; case SOCKET_SHUTDOWN_OUTPUT: conn.m_channel.shutdownOutput(); break; case SOCKET_SHUTDOWN_INPUT_OUTPUT: conn.m_channel.shutdownInput(); conn.m_channel.shutdownOutput(); break; case SOCKET_DROP_OUTPUT: conn.m_fDropOutput = true; // Note, to safely set this back to false we'd need to ensure we could do it in such a // way that we close the current channel, then set to false, then migrate, otherwise // some random bytes could make it on the wire break; case CLOSE_SOCKET: default: conn.m_channel.close(); break; } } catch (IOException e) {} } public static final int CLOSE_SOCKET = 0; public static final int SOCKET_SHUTDOWN_INPUT = 1; public static final int SOCKET_SHUTDOWN_OUTPUT = 2; public static final int SOCKET_SHUTDOWN_INPUT_OUTPUT = 3; public static final int SOCKET_DROP_OUTPUT = 4; /** * Close the specified channel * * @param chan the channel to close */ protected static void closeChannel(SelectableChannel chan) { try { chan.close(); } catch (IOException e) {} } /** * Called once a bus has been opened. * * AbstractSocketBus.onOpen will emit the OPEN event and must be called. */ protected void onOpen() { final long cMillisHeartbeat = f_driver.getDependencies().getHeartbeatMillis(); if (cMillisHeartbeat > 0) { scheduleTask(new Runnable() { @Override public void run() { // reschedule self for next periodic heartbeat scheduleTask(this, cMillisHeartbeat); getConnections().forEach(Connection::heartbeat); } }, cMillisHeartbeat); } final long cMillisAckTimeout = f_driver.getDependencies().getAckTimeoutMillis(); final long cMillisAckFatalTimeout = f_driver.getDependencies().getAckFatalTimeoutMillis(); if (cMillisAckTimeout > 0 || cMillisAckFatalTimeout > 0) { long cIntervalMillis = Math.max(100, (cMillisAckTimeout == 0 ? cMillisAckFatalTimeout : cMillisAckFatalTimeout == 0 ? cMillisAckTimeout : Math.min(cMillisAckTimeout, cMillisAckFatalTimeout)) / 20); scheduleTask(new Runnable() { @Override public void run() { // reschedule self for next periodic health check scheduleTask(this, cIntervalMillis); long ldtNow = SafeClock.INSTANCE.getSafeTimeMillis(); getRegisteredConnections().forEach(conn -> conn.checkHealth(ldtNow)); } }, cIntervalMillis); } EndPoint epThis = getLocalEndPoint(); getLogger().log(makeRecord(Level.FINER, "{0} opened using {1}", epThis, f_channelServer.socket())); emitEvent(new SimpleEvent(Event.Type.OPEN, epThis)); } /** * Called as part of the closing the bus. * * AbstractSocketBus.onClose will emit the CLOSE event and must be called */ protected void onClose() { EndPoint epThis = getLocalEndPoint(); getLogger().log(makeRecord(Level.FINER, "{0} closed using {1}", epThis, f_channelServer.socket())); m_nState = BusState.CLOSED; emitEvent(new SimpleEvent(Event.Type.CLOSE, epThis)); } /** * Return a collection containing all the currently registered connections. * * @return the current connections */ protected Collection getRegisteredConnections() { return f_mapConnections.values(); } protected String getDescription() { return getDescription(true); } /** * Returns a description of this SocketBus. * * @param fVerbose if true, generate a description with connections details * * @return a String description of this SocketBus. */ protected String getDescription(boolean fVerbose) { StringBuilder sb = new StringBuilder() .append(getLocalEndPoint()) .append(", state=").append(m_nState); ConcurrentMap mapCon = f_mapConnections; int cConnections = 0; int cActive = 0; for (Connection conn : mapCon.values()) { ++cConnections; if (conn.m_state == ConnectionState.ACTIVE) { ++cActive; } } sb.append(", connections "); if (fVerbose) { int cCon = mapCon.size(); sb.append("["); for (Connection conn : mapCon.values()) { sb.append("\n\t").append(conn); } sb.append("]\n"); } sb.append("active=").append(cActive).append('/').append(cConnections); return sb.toString(); } /** * Returns a string representation of this SocketBus. If called in * verbose mode, include connections details. * * @param fVerbose if true then print connections details * * @return a String representation of this SocketBus */ public String toString(boolean fVerbose) { return new StringBuilder() .append(getClass().getSimpleName()).append('(') .append(getDescription(fVerbose)) .append(')').toString(); } // ----- Object interface ----------------------------------------------- /** * {@inheritDoc} */ @Override public String toString() { return toString(true); } // ----- AbstractSocketBus helpers -------------------------------------- /** * Return true if the calling thread is currently interrupted, and clear that state. * * @return true if the calling thread is currently interrupted. */ protected boolean isInterrupted() { return Blocking.interrupted(); } /** * Return the bus's logger. * * @return the bus's logger */ protected Logger getLogger() { return getSocketDriver().getDependencies().getLogger(); } /** * Construct a log record. * * @param level the log level * @param sMsg the message * @param oaParams the parameters * * @return the record */ protected LogRecord makeRecord(Level level, String sMsg, Object ... oaParams) { LogRecord rec = new LogRecord(level, sMsg); rec.setParameters(oaParams); return rec; } /** * Construct a log record with an exception. * * @param level the log level * @param t the exception * @param sMsg the message * @param oaParams the parameters * * @return the record */ protected LogRecord makeExceptionRecord(Level level, Throwable t, String sMsg, Object ... oaParams) { LogRecord rec = makeRecord(level, sMsg, oaParams); rec.setThrown(t); return rec; } /** * Add a Connection to the set of connections awaiting a flush. * * @param conn the connection to add */ protected void addFlushable(Connection conn) { f_setFlush.add(conn); } /** * Remove a Connection from the flushable set. * * @param conn the connection to remove */ protected void removeFlushable(Connection conn) { f_setFlush.remove(conn); } /** * Return true iff the specified connection is currently in the flushable set. * * @param conn the connection * * @return true iff the connection is in the flushable set */ protected boolean isFlushable(Connection conn) { return f_setFlush.contains(conn); } /** * Emit the specified Event to the Event Collector. * * @param event the event to emit */ protected void emitEvent(Event event) { Collector coll = m_collectorEvent; if (coll != null) { try { coll.add(event); coll.flush(); } catch (Throwable t) { // TODO: disconnect?, log? } } } /** * Add the specified Event to the Event Collector. *

* This method does not flush the collector. * * @param event the event to add */ protected void addEvent(Event event) { Collector coll = m_collectorEvent; if (coll != null) { try { coll.add(event); } catch (Throwable t) { // TODO: disconnect?, log? } } } /** * Flush the event collector. */ protected void flushEvents() { Collector coll = m_collectorEvent; if (coll != null) { try { coll.flush(); } catch (Throwable t) { // TODO: disconnect?, log? } } } /** * Return the currently managed connections. * * @return the currently managed connections */ protected Collection getConnections() { return f_mapConnections.values(); } /** * Configure the specified Socket * * @param socket the socket to configure * * @throws IOException on an I/O error */ protected void configureSocket(Socket socket) throws IOException { Sockets.configure(socket, getSocketDriver().getDependencies().getSocketOptions()); } /** * Configure the specified ServerSocket * * @param socket the ServerSocket to configure * * @throws IOException on an I/O error */ protected void configureSocket(ServerSocket socket) throws IOException { Sockets.configure(socket, getSocketDriver().getDependencies().getSocketOptions()); } /** * Verify that the bus is in a given state. * * @param nState the requite state * * @throws IllegalStateException if the bus is not in the specified state */ protected void verifyState(BusState nState) { BusState nStateCurr = m_nState; if (nStateCurr != nState) { throw new IllegalStateException("invalid bus state: required " + nState + ", actual " + nStateCurr); } } /** * Verify that the supplied EndPoint is a usable EndPoint * * @param peer the EndPoint to verify * * @return the UrlEndPoint */ protected UrlEndPoint verifyEndPoint(EndPoint peer) { if (f_driver.isSupported(peer) && ((UrlEndPoint) peer).getProtocol().equals(getProtocolName())) { return (UrlEndPoint) peer; } throw new IllegalArgumentException("unsupported EndPoint " + peer); } /** * Return the SelectionService for this Bus. * * @return the SelectionService for this Bus. */ protected SelectionService getSelectionService() { return getSocketDriver().getDependencies().getSelectionService(); } /** * Return the SocketDriver for this bus. * * @return the SocketDriver */ protected SocketBusDriver getSocketDriver() { return f_driver; } /** * Return the bus connection for the specified EndPoint. * * @param peer the EndPoint to ensure * * @return the Connection * * @throws IllegalStateException if the bus is not open * @throws IllegalArgumentException if the connection does not exist */ protected Connection ensureConnection(EndPoint peer) { Connection conn = f_mapConnections.get(peer); if (conn == null) { verifyState(BusState.OPEN); throw new IllegalArgumentException("unknown peer " + peer); } return conn; } /** * Return the protocol identifier. * * @return the protocol identifier */ protected int getProtocolIdentifier() { return getClass().getName().hashCode() ^ getSocketDriver().getClass().getName().hashCode(); } /** * Return the protocol name. * * @return the protocol name */ protected String getProtocolName() { return getClass().getSimpleName(); } /** * Return minimum protocol version understood by this implementation. * * @return the minimum protocol version */ protected short getMinimumProtocolVersion() { return 0; } /** * Return maximum protocol version understood by this implementation. * * @return the maximum protocol version */ protected short getMaximumProtocolVersion() { // version 1 adds reconnect support // version 2 adds identity // version 3 adds ability to request a remote heap dump on sync // version 4 sends local and remote ID for CONNECT_MIGRATE, and pads an ID spot for CONNECT_NEW but always sends 0 // version 5 change the message size to long, add checksum for message body and message header return 5; } // ----- ConnectionState ------------------------------------------------ /** * ConnectionState represents the state of the underlying Connection. */ protected enum ConnectionState { /** * CONNECT event emitted but socket has yet to connect (may not even exist) */ OPEN, /** * Indicates a usable connection, we can exchange messages. */ ACTIVE, /** * DISCONNECT event emitted, no more exchanges allowed, SelectionService * is no longer managing the channel. */ DEFUNCT, /** * RELEASE event emitted, all done. */ FINAL } // ----- HandshakePhase -------------------------------------------------- /** * HandshakePhase represents the state of the handshake protocol. */ protected enum HandshakePhase { /** * The negotiation phase of the handshake identifies the basic * protocol, ensuring that before reading any further into the stream * that the other end speaks the same language, i.e. we've connected * to another SocketBus. *

* The format is: *

    *
  • 4B (int) protocol identifier
  • *
  • 2B (short) min version
  • *
  • 2B (short) max version
  • *
  • 2B (short) name char length
  • *
  • ?B (char[]) name
  • *
*/ NEGOTIATE, /** * The introduction phase waits for the canonical name of the peer. * The length of which was obtained in the identification phase. */ INTRODUCE, /** * The accept phase involves each peer sending a single (otherwise * useless) byte to accept the connection. Until this byte has been * received the connection has not been accepted, and additional data * should not be sent. */ ACCEPT, /** * The abandon phase is entered only if the bus decides to not * pursue a connection with the peer. Once all IO required for this * phase is completed the channel will be closed. * * This can happen because it already is the the process of opening * the same, and it is of higher priority. Rather then rejecting the * connection by closing it, it simply waits for the peer, to also * realize the collision, and to accept the higher priority EndPoint's * connection. At this point it will close its connection, allowing the * higher priority peer to do the same. If we were to actively reject * the connection, the peer could see this before realizing the collision * and emit a DISCONNECT event which is not what we want. * * This can also happen as a result of a peer trying to reconnect a * connection which is unknown to this bus. */ ABANDON } // ----- Connection ----------------------------------------------------- /** * Factory pattern method for instantiating Connection objects. * * @param peer the peer * * @return the Connection */ protected abstract Connection makeConnection(UrlEndPoint peer); /** * Connection contains the state associated with the connection to each * connected peer. */ protected abstract class Connection implements SelectionService.Handler, GatheringByteChannel, ScatteringByteChannel, Disposable { // ----- constructors ------------------------------------------- /** * Construct a Connection for the specified EndPoint. * * @param peer the remote end of the connection */ public Connection(UrlEndPoint peer) { this.f_peer = peer; if (f_fCrc) { f_crcRx = new CRC32(); f_crcTx = new CRC32(); getLogger().log(makeRecord(Level.FINER, "Packet corruption detection enabled for connection {0} to {1}", getLocalEndPoint(), peer)); } } // ----- Connection interface ----------------------------------- /** * Open the connection. * * @throws IOException if an IO error occurs */ protected void open() throws IOException { if (m_state != null) { throw new IllegalStateException("state = " + m_state); } m_state = ConnectionState.OPEN; EndPoint peer = f_peer; getLogger().log(makeRecord(Level.FINER, "{0} opening connection with {1} using {2}", getLocalEndPoint(), peer, m_channel.socket())); emitEvent(new SimpleEvent(Event.Type.CONNECT, peer)); } /** * Return if the connection is valid * * @return true iff the connection is valid */ protected boolean isValid() { ConnectionState state = m_state; if (state == null) { // we can get here for LightMessageBus if there is a concurrent connect and the remote side // wins; since LWMB doesn't synchronized in send we have to double check before declaring // a failure before open; simply making m_state volatile would not be sufficient because when // the connection comes from the remote side it is inserted into the map before it is opened // but is inserted while under sync. It still must be volatile though for the double-check // to be safe synchronized (this) { state = m_state; if (state == null) { // ok, this really is an invalid connection return false; } // else; now we have a stable view of the state } } return state != ConnectionState.FINAL; } /** * Ensure that the connection is usable. * * @return this */ public Connection ensureValid() { if (isValid()) { return this; } throw new IllegalArgumentException("connection to " + f_peer + " is not open, in state " + m_state); } /** * Perform the actual connect to the peer * * @throws IOException on an I/O error */ private void connect() { ConnectionState state = m_state; if (state != null && state.ordinal() >= ConnectionState.DEFUNCT.ordinal()) { throw new IllegalStateException("state = " + m_state); } else if (m_channel != null && m_channel.isOpen()) { // reconnect of open channel throw new IllegalStateException(); } SocketChannel channel; try { m_channel = channel = getSocketDriver().getDependencies() .getSocketProvider().openSocketChannel(); } catch (IOException e) { throw new RuntimeException(e); } // configure and connect try { Sockets.configureBlocking(channel, false); configureSocket(channel.socket()); channel.connect(f_peer.getAddress()); } catch (IOException e) { onException(e); } try { // emit connect event; allowing operations to be scheduled against the endpoint if (state == null) { open(); } // else this is a re-connect and open has already been issued // register initial I/O handler SelectionService.Handler handler = new HandshakeHandler(channel, this); getSelectionService().register(channel, handler); m_handler = handler; } catch (IOException e) { scheduleDisconnect(e); } } /** * Schedule a disconnect. * * @param eReason the reason for the disconnect */ public void scheduleDisconnect(Throwable eReason) { scheduleShutdown(eReason, false, /*continuation*/ null); } /** * Register a Runnable with the SelectionService * to perform the disconnect logic and optionally release it. It schedules * the disconnect to happen on the SelectionService thread where the * connection is registered. * * @param eReason the reason for the disconnect or null * @param fRelease true iff the connection should also be released * @param continuation continuation once the operation has completed */ public void scheduleShutdown(final Throwable eReason, final boolean fRelease, Continuation continuation) { invoke(new AbstractContinuationFrame(continuation) { @Override public Void call() { boolean fEmitDisconnect = false; HandshakeHandler handlerNext; synchronized (Connection.this) { handlerNext = m_next; switch (m_state) { case OPEN: case ACTIVE: fEmitDisconnect = true; // emit below; outside of sync block m_state = ConnectionState.DEFUNCT; //fall through case DEFUNCT: if (fRelease) { m_next = null; m_state = ConnectionState.FINAL; } break; case FINAL: // this can occur if there were pending // jobs when we processed the release // job return null; default: throw new IllegalStateException("state = " + m_state); } } final Continuation continuation = new WrapperContinuation(getContinuation()) { @Override public void proceed(Void v) { flushEvents(); super.proceed(v); } }; if (fRelease) { if (fEmitDisconnect) { final HandshakeHandler handlerRelease = handlerNext; doDisconnect(fEmitDisconnect, eReason, new Continuation() { @Override public void proceed(Void v) { doRelease(handlerRelease, continuation); } }); } else { doRelease(handlerNext, continuation); } } else { doDisconnect(fEmitDisconnect, eReason, continuation); } return continueAsync(); } }); } /** * Emit any and all receipts */ protected abstract void drainReceipts(); /** * Perform disconnect processing. *

* This may be called multiple times. * * @param fFirst true iff this is the first disconnect * @param eReason the reason (if any) for the disconnect * @param continuation the continuation to run after the disconnect has been performed */ protected void doDisconnect(boolean fFirst, Throwable eReason, Continuation continuation) { try { if (fFirst) { onDisconnected(eReason); } try { m_channel.close(); // may already be closed } catch (IOException e) {} // in case of multiple disconnects force drain of receipts on each pass // while not required this helps from leaving receipts queue'd until // release drainReceipts(); } finally { Continuations.proceed(continuation, null); } } /** * Perform release processing. * * @param handlerNext the handler for the next connection * @param continuation the continuation to run after the release has been performed */ protected void doRelease(HandshakeHandler handlerNext, Continuation continuation) { try { EndPoint peer = getPeer(); onReleased(); // emit queue'd receipts drainReceipts(); // we take out a write lock to handle the cases where a new connection is concurrently // made for the one we are actively releasing Lock lockWrite = AbstractSocketBus.this.f_lockState.writeLock(); lockWrite.lock(); try { if (handlerNext != null && !handlerNext.getChannel().socket().isInputShutdown() && AbstractSocketBus.this.m_nState == BusState.OPEN) { // replace with pending connection synchronized (handlerNext.m_connection) { // In order to ensure that the connection is visible once we emit the // CONNECT event, the connection must be in the connection map before // emitting the even. Of course until the CONNECT event is emitted the // connection isn't usable. To make these two operations atomic we must // sync on the new connection, put it in the map, and then emit the event. // We also don't want to emit the RELEASE event for the old connection // while it is still in the map, because once RELEASEd new connects are // allowable. The net result is that we must do a replace, RELEASE, // open all while sync'd on the new connection AbstractSocketBus.this.f_mapConnections.replace(peer, handlerNext.m_connection); getLogger().log(makeRecord(Level.FINER, "{0} releasing connection with {1}", getLocalEndPoint(), peer)); addEvent(new SimpleEvent(Event.Type.RELEASE, peer)); try { handlerNext.m_connection.open(); // emits CONNECT event getSelectionService().register(handlerNext.getChannel(), handlerNext); handlerNext.m_connection.m_handler = handlerNext; } catch (IOException e) { handlerNext.m_connection.scheduleDisconnect(e); } } handlerNext = null; } else { if (handlerNext != null) { handlerNext.close(null); } AbstractSocketBus.this.f_mapConnections.remove(peer); getLogger().log(makeRecord(Level.FINER, "{0} releasing connection with {1}", getLocalEndPoint(), peer)); addEvent(new SimpleEvent(Event.Type.RELEASE, peer)); } } finally { lockWrite.unlock(); } if (handlerNext != null) { // we didn't proceed with the pending connection try { handlerNext.getChannel().close(); } catch (Exception e) {} } } finally { Continuations.proceed(continuation, null); } } /** * Called as a connection is being disconnected. * * @param eReason the cause of the disconnect */ public void onDisconnected(Throwable eReason) { getLogger().log(makeExceptionRecord( eReason instanceof SSLException ? Level.WARNING : Level.FINER, eReason, "{0} disconnected connection with {1}", getLocalEndPoint(), this)); addEvent(new SimpleEvent(Event.Type.DISCONNECT, getPeer(), eReason)); } /** * Called as a connection is being released. */ public void onReleased() { removeFlushable(this); dispose(); } /** * Called as part of migrating a connection. * * The caller must hold synchronization on the Connection while calling this method and * this method must be run on the SS thread associated with the connection. */ public void onMigration() { ++m_cMigrations; } @Override public void dispose() { } /** * Flush the connection. */ protected abstract void flush(); /** * Issue a heartbeat if necessary * * @return true if a heartbeat was issued, false if it was determined one wasn't needed */ protected abstract boolean heartbeat(); /** * Check the connection for any ack timeouts. * * @param ldtNow the current safe time */ protected void checkHealth(long ldtNow) { } /** * Perform an optimistic flush, i.e. flush only if the connection is not already being flushed. * * Caller's need not be synchronized on the connection when calling this method, though the method * will synchronize if it determines that it will flush. */ public void optimisticFlush() { AtomicBoolean lockFlush = f_lockFlush; if (lockFlush.compareAndSet(false, true)) { synchronized (this) // wait for any concurrent send to complete { try { ensureValid().flush(); } catch (IllegalArgumentException e) { // connection may have been released } finally { // we must unlock while still sync'd to ensure that no thread can add to the send queue // while we hold the flush lock lockFlush.set(false); } } } // else; another thread is actively flushing this connection. Because both conn.send and conn.flush // are performed while sync'd on the conn we know the other thread's flush will include all data on // the connection and thus this thread can skip over that connection. The intent of this optimization // is to avoid blocking on flush when there will be nothing left to flush anyway. This is especially // important as SocketBus scalability comes primarily from having many connections, with the idea // that threads are less likely to contend on any given connection. Flushing however involves all // used threads and would become a high contention point, thus we need to avoid needlessly blocking // here. } /** * Return true if some thread is actively waiting to flush this connection. * * @return true if some thread is actively waiting to flush this connection */ public final boolean isFlushInProgress() { return f_lockFlush.get(); } /** * Force the SelectionService to process this channel. * * @throws IOException if an I/O error occurs * * @return true iff the wakeup was scheduled * * @throws IOException if the connection has been closed */ protected boolean wakeup() throws IOException { //COH-19338: m_state is null for new connection before open is called if (m_state == null) { return false; } switch (m_state) { case OPEN: return false; case ACTIVE: synchronized (Connection.this) { getSelectionService().register(m_channel, m_handler); } return true; case DEFUNCT: case FINAL: default: throw new ClosedChannelException(); } } /** * Return the send buffer size for the underlying socket. * * @return the send buffer size, or -1 if not connected * * @throws SocketException if an I/O error occurs */ protected int getSendBufferSize() throws SocketException { SocketChannel chan = m_channel; if (chan == null) { return -1; } return chan.socket().getSendBufferSize(); } /** * Return the packet size for this connection. * * @return the packet size for this connection, or -1 if not connected */ protected int getPacketSize() { int cbPacket = m_cbPacket; if (cbPacket <= 0) { SocketChannel chan = m_channel; if (chan != null) { Socket socket = chan.socket(); if (socket.isBound()) { m_cbPacket = cbPacket = Sockets.getMTU(socket); } } } return cbPacket; } /** * Return the receive buffer size for the underlying socket. * * @return the receive buffer size, or -1 if not connected * * @throws SocketException if an I/O error occurs */ protected int getReceiveBufferSize() throws SocketException { SocketChannel chan = m_channel; if (chan == null) { return -1; } return chan.socket().getReceiveBufferSize(); } /** * Return the peer associated with this connection. * * @return the peer */ public EndPoint getPeer() { return f_peer; } /** * Return the protocol version for this connection. * * @return the protocol version or -1 if not net negotiated */ public int getProtocolVersion() { return m_nProtocol; } /** * Schedule an invocation against this channel on the SelectionService. * * @param runnable the runnable to invoke */ protected synchronized void invoke(Runnable runnable) { Queue queueDeferred = m_queueDeferred; if (queueDeferred == null) { try { getSelectionService().invoke(m_channel, runnable, /*cMillisDelay*/ 0); } catch (IOException e) { throw new RuntimeException(e); } } else { queueDeferred.add(runnable); } } // ----- Channel interface ------------------------------------------ /** * Register a new SelectionService.Handler for this connection. * * @param handler the handler or null to unregister * * @throws IOException if an IO error occurs */ public void registerHandler(SelectionService.Handler handler) throws IOException { getSelectionService().register(m_channel, handler); } /** * {@inheritDoc} */ public void close() { scheduleDisconnect(null); } /** * Return true iff the connection has not been released. * * @return true iff the connection has not been released */ public boolean isOpen() { return m_state.ordinal() < ConnectionState.FINAL.ordinal(); } /** * {@inheritDoc} */ public long write(ByteBuffer[] srcs, int offset, int length) throws IOException { switch (m_state) { case OPEN: return 0; // not ready yet case ACTIVE: { SocketChannel chan = m_channel; long cbWrite = 0; long cb; long cbPre = 0; for (int i = offset, e = offset + length; i < e; ++i) { cbPre += srcs[i].remaining(); } if (m_fDropOutput) // for testing purposes only { for (int i = offset, e = offset + length; i < e; ++i) { srcs[i].position(srcs[i].limit()); } m_cbWrite += cbPre; return cbPre; } try { int i = offset; int c = length; do { cbWrite += cb = chan.write(srcs, i, c); // According the the JRockit team the underlying // OS will generally only support a maximum number // of gather buffers (they said 16). So it is // possible that an incomplete write was not do to // lack of buffer space but do to this OS issue. // so if we wrote something, and have lots of buffers // advance through the written ones and try again while (cb != 0 && c > 0 && !srcs[i].hasRemaining()) { ++i; --c; } } while (cb != 0 && c > 0); } catch (IOException ex) { // out of paranoia (chan.write doc isn't specific) test to see if any buffer positions were // updated, we need cbWrite to be correct long cbPost = 0; for (int i = offset, e = offset + length; i < e; ++i) { cbPost += srcs[i].remaining(); } cbWrite = cbPre - cbPost; if (cbWrite == 0) { throw ex; } // apparently we managed to do some writes before the socket was closed. We either need to // rewind the buffers we wrote or pretend that we didn't see that the socket was closed. The // latter is far easier so we take that approach. A subsequent write will of course fail without // sending anything and then we'll surface the exception } m_cbWrite += cbWrite; return cbWrite; } default: throw new ClosedChannelException(); } } /** * {@inheritDoc} */ public long write(ByteBuffer[] srcs) throws IOException { switch (m_state) { case OPEN: return 0; // not ready yet case ACTIVE: return write(srcs, 0, srcs.length); default: throw new ClosedChannelException(); } } /** * {@inheritDoc} */ public int write(ByteBuffer src) throws IOException { switch (m_state) { case OPEN: return 0; // not ready yet case ACTIVE: int cb = m_channel.write(src); m_cbWrite += cb; return cb; default: throw new ClosedChannelException(); } } /** * {@inheritDoc} */ public long read(ByteBuffer[] dsts, int offset, int length) throws IOException { switch (m_state) { case OPEN: return 0; // not ready yet case ACTIVE: // TODO: consider insulating against scatter/gather issue // see write above ByteBuffer bufFirstCorrupt = null; int cbFirst = 0; if (f_nCorruptionRatio != 0) { bufFirstCorrupt = getFirstAvailableForCorruption(dsts, offset); cbFirst = bufFirstCorrupt.remaining(); } long cb = m_channel.read(dsts, offset, length); if (cb >= 0) { if (f_nCorruptionRatio != 0) { checkForceCorruption(bufFirstCorrupt, (int) Math.min(cb, cbFirst)); } if (f_nDropRatio == 0 || !checkDrop(m_channel)) { m_cbRead += cb; return cb; } } // else; fall through default: return -1; } } /** * {@inheritDoc} */ public long read(ByteBuffer[] dsts) throws IOException { return read(dsts, 0, dsts.length); } /** * {@inheritDoc} */ public int read(ByteBuffer dst) throws IOException { switch (m_state) { case OPEN: return 0; // not ready yet case ACTIVE: int cb = m_channel.read(dst); if (cb >= 0) { if (f_fCrc && f_nCorruptionRatio != 0) { checkForceCorruption(dst, cb); } if (f_nDropRatio == 0 || !checkDrop(m_channel)) { m_cbRead += cb; return cb; } } // else; fall through default: return -1; } } /** * Migrate this bus connection to a new socket connection. * * @param eReason optional exception describing why current connection needs to be replaced */ public synchronized void migrate(Throwable eReason) { SocketBusDriver.Dependencies depsDriver = f_driver.getDependencies(); int cSocketReconnectLimit = depsDriver.getSocketReconnectLimit(); // COH-24389 - a reconnect limit of < 0 indicates that migrations are disabled if (getProtocolVersion() == 0 || cSocketReconnectLimit < 0) { scheduleDisconnect(eReason); } else // protocol not yet negotiated or >= 1, in either case we can attempt a migration { if (eReason instanceof ConnectException && ++m_cReconnectAttempts > cSocketReconnectLimit) { // we've exhausted our reconnect attempts, disconnect scheduleDisconnect(eReason); return; } // delay reconnect on initial connect and to also help avoid an endless conflict // if both sides were to keep trying to simultaneously reconnect and in doing so invalidate the other // side's reconnect attempt. long cMillisDelay = eReason instanceof ConnectException || getLocalEndPoint().getCanonicalName().compareTo(getPeer().getCanonicalName()) > 0 ? depsDriver.getSocketReconnectDelayMillis() : 0; SocketChannel chan = m_channel; String sChan = chan.toString(); // to preserve port info for subsequent logging closeChannel(chan); scheduleUnsafeTask(chan, new Runnable() { @Override public void run() { synchronized (Connection.this) { if (m_state.ordinal() < ConnectionState.DEFUNCT.ordinal() && chan == m_channel) { getLogger().log(makeExceptionRecord(Level.FINER, eReason, "{0} migrating connection with {1} off of {2} on {3}", getLocalEndPoint(), getPeer(), sChan, Connection.this)); m_eMigrationCause = eReason; onMigration(); // we're sync'd on the connection so nothing new can be scheduled try { getSelectionService().register(chan, null); m_handler = null; } catch (IOException e) {} // replaces m_channel, so any subsequent exceptions on the old channel won't make it into this if block // schedule task to ensure register and connect are processed sequentially scheduleUnsafeTask(chan, ()-> connect(), cMillisDelay); } } } }, cMillisDelay); } } /** * Called when the channel has been selected. *

* If this method throws an exception it will be handled by {@link * #onException} * * @param nOps the selected ops * * @return the new interest set * * @throws IOException on an I/O error */ protected abstract int onReadySafe(int nOps) throws IOException; /** * Called in the event that {@link #onReadySafe} resulted in an * exception. *

* The default implementation simply disconnects the connection. * * @param t the exception * * @return the new interest set, the default implementation returns 0 */ protected int onException(Throwable t) { ConnectionState state = m_state; if (t instanceof IOException && !(t instanceof SSLException) && // don't migrate if the failure was due to security (state == null || state.ordinal() < ConnectionState.DEFUNCT.ordinal())) { migrate(t); } else { scheduleDisconnect(t); } return 0; } // ----- Handler interface -------------------------------------- /** * {@inheritDoc} */ public final int onReady(int nOps) { try { return onReadySafe(nOps); } catch (Throwable t) { return onException(t); } } /** * Set the protocol version. * * @param nProt the version to set */ protected void setProtocolVersion(int nProt) { int nProtocol = m_nProtocol; if (nProtocol == -1 || nProtocol == nProt) { m_nProtocol = nProt; } else { throw new IllegalStateException(); } } // ----- Object interface --------------------------------------- /** * {@inheritDoc} */ public String toString() { SocketChannel channel = m_channel; Socket socket = channel == null ? null : channel.socket(); int cMigrations = m_cMigrations; return "peer=" + getPeer() + ", state=" + m_state + ", socket=" + socket + (cMigrations == 0 ? "" : ", migrations=" + cMigrations) + ", bytes(in=" + m_cbRead + ", out=" + m_cbWrite + ")" + ", flushlock " + f_lockFlush.get(); } @Override public int hashCode() { return f_peer.hashCode(); // only because profiler says Object.hashCode is expensive here } // ----- data members ------------------------------------------- /** * The peer's EndPoint. */ private final UrlEndPoint f_peer; /** * Atomic indicating if the connection is being flushed. */ private final AtomicBoolean f_lockFlush = new AtomicBoolean(); /** * The connect state of this connection. * * volatile for LightBus usage see isValid */ protected volatile ConnectionState m_state; /** * The identity of this side of the logical connection. The identity is used during reconnects * to ensure we aren't connecting to a different connection which is simply reusing the ip:port * * This is a non-zero value, which is unique across usages of this ip:port. Note it is not unique * otherwise, and multiple peer connections may share the same identity. */ protected final long f_lIdentity = f_atomicIdGenerator.incrementAndGet(); /** * Our peer's identity. Note this must not be changed once set to a non-zero value. */ protected long m_lIdentityPeer; /** * The channel connecting this bus to the peer. */ private SocketChannel m_channel; /** * For testing purposes only, force the connection to drop all output traffic. */ private boolean m_fDropOutput; /** * The packet size for this connection */ private int m_cbPacket = -1; /** * A handshaking connection waiting for this connection to be released */ private HandshakeHandler m_next; /** * If non-null then any invocations must inserted into the queue rather then * scheduled with the SelectionService */ private Queue m_queueDeferred; /** * The total number of bytes read from the socket. */ protected long m_cbRead; /** * The total number of bytes written to the socket. */ protected long m_cbWrite; /** * The negotiated protocol version, or -1 if not yet known. */ protected int m_nProtocol = -1; /** * The number of connection migrations that have occured. */ protected int m_cMigrations; /** * The cause of the last initiated migration. */ protected Throwable m_eMigrationCause; /** * The number of sequential reconnect attempts which have been made on this connection. */ protected int m_cReconnectAttempts; /** * CRC32 for read thread. */ protected CRC32 f_crcRx; /** * CRC32 for write threads. */ protected CRC32 f_crcTx; /** * Current HandShakeHandler for this connection. */ protected volatile SelectionService.Handler m_handler; } // ---- HandshakeHandler ------------------------------------------------ /** * HandshakeHandler handles the initial transmissions on a SocketChannel * as two buses handshake. */ protected class HandshakeHandler extends SafeSelectionHandler { /** * Construct a HandshakeHandler for the give SocketChannel. * * @param channel the socket channel * @param connection the optional connection */ public HandshakeHandler(SocketChannel channel, Connection connection) { super(channel); this.m_connection = connection; int cbNegotiate = 4 + // (int) protocol id 2 + // (short) min ver 2 + // (short) max ver 2; // (short) name char length m_headerIn = ByteBuffer.allocate(cbNegotiate); ByteBuffer headerOut = m_headerOut = ByteBuffer.allocate(cbNegotiate); headerOut.putInt(getProtocolIdentifier()) .putShort(getMinimumProtocolVersion()) .putShort(getMaximumProtocolVersion()) .putShort((short) getLocalEndPoint().getCanonicalName().length()); headerOut.flip(); } /** * {@inheritDoc} */ public int onReadySafe(int nOps) throws IOException { SocketChannel channel = getChannel(); Connection connection = m_connection; ByteBuffer headerOut = m_headerOut; ByteBuffer headerIn = m_headerIn; HandshakePhase phase = m_phase; int nInterest = 0; if (channel.isConnectionPending()) { if (!channel.finishConnect()) { getLogger().log(makeRecord(Level.FINEST, "{0} finishConnect pending for {1} on {2}", AbstractSocketBus.this.getLocalEndPoint(), connection.getPeer(), channel.socket())); return OP_CONNECT; } connection.m_cReconnectAttempts = 0; getLogger().log(makeRecord(Level.FINEST, "{0} socket connected for {1} on {2}", AbstractSocketBus.this.getLocalEndPoint(), connection.getPeer(), channel.socket())); } if (f_nDropRatio > 0 && checkDrop(channel)) { throw new IOException("test drop; " + phase); } channel.write(headerOut); if ((nOps & OP_READ) != 0 && channel.read(headerIn) < 0) { throw new IOException("InputShutdown during handshake " + phase + " in " + headerIn + " out " + headerOut); } if (headerIn.hasRemaining()) { nInterest = OP_READ; } if (headerOut.hasRemaining()) { nInterest |= OP_WRITE; } if (nInterest == 0) { // end of read & write means we're ready for the next phase getLogger().log(makeRecord(Level.FINEST, "{0} processing {1} handshake for {2} on {3}", AbstractSocketBus.this.getLocalEndPoint(), phase, connection == null ? null : connection.getPeer(), channel.socket())); switch (phase) { case NEGOTIATE: nInterest = onNegotiate(); break; case INTRODUCE: nInterest = onIntroduce(); break; case ACCEPT: nInterest = onAccept(); break; case ABANDON: default: nInterest = onAbandon(); break; } if (m_phase != phase) { getLogger().log(makeRecord(Level.FINEST, "{0} waiting for {1} handshake for {2} on {3} with interest {4}, {5}B to read, {6}B to write", AbstractSocketBus.this.getLocalEndPoint(), m_phase, m_connection == null ? null : m_connection.getPeer(), getChannel().socket(), nInterest, m_headerIn.remaining(), m_headerOut.remaining())); } } return nInterest; } /** * {@inheritDoc} */ public int onException(Throwable eReason) { Connection connection = m_connection; if (connection == null) { close(eReason); } else { ConnectionState state = connection.m_state; if (connection.m_channel == getChannel() && state != null && state.ordinal() < ConnectionState.DEFUNCT.ordinal()) { connection.onException(eReason); } } return 0; } /** * Process the peer's protocol identification. * * @return the new interest set */ public int onNegotiate() { Connection connection = m_connection; ByteBuffer headerIn = m_headerIn; headerIn.flip(); // verify protocol int nId = headerIn.getInt(); int nIdReq = getProtocolIdentifier(); if (nId != nIdReq) { if (nId >>> 8 == (nIdReq & 0x00FFFFFF)) { // Note: very rarely (about one in a million attempts) // on OS X 10.6 we'll hit a OS X bug which results in the // first byte off the connection being missing. This has // been verified in small provably correct tests as well. nId = nIdReq; // reset header to parse the remainder headerIn.position(3); } else // unknown protocol { getLogger().log(makeRecord(Level.WARNING, "{0} rejecting connection from {1}" + " using incompatible protocol id {2}, required {3}", getLocalEndPoint(), getChannel().socket().getInetAddress(), nId, nIdReq)); close(new IOException("incompatible protocol")); return 0; } } // verify version short nMin = headerIn.getShort(); short nMax = headerIn.getShort(); if (nMin > getMaximumProtocolVersion() || nMax < getMinimumProtocolVersion()) { // no overlapping version getLogger().log(makeRecord(Level.WARNING, "{0} rejecting connection from {1}" + " using unsupported protocol {2} version " + "({3} ... {4}), supported ({5} ... {6})", getLocalEndPoint(), getChannel().socket().getInetAddress(), nId, nMin, nMax, getMinimumProtocolVersion(), getMaximumProtocolVersion())); close(new IOException("protocol version mismatch")); return 0; } // we support overlappying versions; select the highest shared version int nProt = m_nProtocol = Math.min(getMaximumProtocolVersion(), nMax); getLogger().log(makeRecord(Level.FINEST, "{0} handshaking with {1}" + " using protocol {2} version {3}", getLocalEndPoint(), getChannel().socket().getInetAddress(), nId, nProt)); m_phase = HandshakePhase.INTRODUCE; // prepare outbound introduction String sName = getLocalEndPoint().getCanonicalName(); boolean fSendConnect = nProt > 0 && connection != null; // connect type only sent starting with v1 boolean fSendIdentity = nProt > 1 && fSendConnect; // id only sent starting with v2 ByteBuffer bufOut = m_headerOut = ByteBuffer.allocate(sName.length() * 2 + (fSendConnect ? 1 : 0) + (fSendIdentity ? nProt > 3 ? 16 : 8 : 0)); for (int i = 0, c = sName.length(); i < c; ++i) { bufOut.putChar(sName.charAt(i)); } if (fSendConnect) { // note: accepting side doesn't have sufficient information to send the connect type until // it has received the initiators full introduction. byte nConnect = connection.m_state == ConnectionState.OPEN && connection.m_lIdentityPeer == 0 ? CONNECT_NEW : CONNECT_MIGRATE; bufOut.put(nConnect); if (fSendIdentity) { if (nProt > 3) { bufOut.putLong(connection.f_lIdentity) .putLong(connection.m_lIdentityPeer); } else { bufOut.putLong(nConnect == CONNECT_NEW ? connection.f_lIdentity : connection.m_lIdentityPeer); } } } bufOut.flip(); // prepare for inbound introduction m_headerIn = ByteBuffer.allocate(headerIn.getShort() * 2 + (nProt > 0 ? 1 : 0) + // see connect type note above (nProt > 3 ? 16 : nProt > 1 ? 8 : 0)); return OP_READ | OP_WRITE; } /** * Evaluate the introduction. * * @return the new interest set * * @throws IOException on an I/O error */ public int onIntroduce() throws IOException { // We've verified that we can communicate, but we now need to // ensure that we don't allow multiple concurrent connections // between two peers at once. Now that we have the protocol // header we know we've ensured that both sides will know who // is at the other end of the socket. Each side will ensure that // they aren't already opening up another socket in the reverse // direction, and that they don't already have an existing socket. // In the case that both sides simultaneous open sockets to one // another the peer with the lower canonical name will win. So here // we will do one of the following: // a. Identify that we've initiated this connection and move to // the accept phase. // b. Identify that we did not initiate this connection, but that // we have no outgoing connection to the same peer, and move // to the accept phase // c. Identify that we did not initiate this connection, and that // we do have an outgoing connection to a lower peer. We will // close our existing socket, and replace it with this one, on // the same Connection object. // d. Identify that we did not initiate this connection, and that // we do have an outgoing connection to the same peer. In this // case we will "abandon" this connection. It is important // that we don't close the connection until we know that peer // has performed option "c" from above. // e. Identify that we have an active connection to this peer. // While this may seem like an illegal state it can happen for // one legitimate reasons. The existing connection may // have been disconnected on the remote end, and we just haven't // received the "close" packet yet, and the new "open" packet // which isn't required to appear in order wrt the close arrived // first. // TODO: now that we have migration support we don't technically // need to worry about seniority during simultaneous connect, we // can let any new connection simply close any existing connection // this will simply trigger a migration. Note it is possible that // two peers could do this indefinitely, i.e. closing each others // connections, we may need to add in some migration backoff ByteBuffer headerIn = m_headerIn; headerIn.flip(); int nProt = m_nProtocol; int cbName = headerIn.limit() - ((nProt > 0 ? 1 : 0) + // connect type (nProt > 3 ? 16 : nProt > 1 ? 8 : 0)); // ID char[] achName = new char[cbName / 2]; for (int i = 0, c = achName.length; i < c; ++i) { achName[i] = headerIn.getChar(); } final UrlEndPoint peer = f_driver.resolveSocketEndPoint(new String(achName)); int nInterest = OP_READ | OP_WRITE; Connection connection = m_connection; boolean fInbound = connection == null; byte nConnect = nProt > 0 ? headerIn.get() : CONNECT_NEW; long lIdThatRx; long lIdThisRx; if (nProt > 3) { lIdThatRx = headerIn.getLong(); lIdThisRx = headerIn.getLong(); } else if (nProt > 1) { switch (nConnect) { case CONNECT_NEW: lIdThatRx = headerIn.getLong(); lIdThisRx = 0; break; case CONNECT_MIGRATE: lIdThisRx = headerIn.getLong(); lIdThatRx = 0; break; default: throw new IOException("protocol error"); } } else { lIdThatRx = lIdThisRx = 0; } if (fInbound) { // in-bound connection // check if we already have an existing connection for this // peer, this is unlikely, so we will attempt to register // our new connection at the same time final Connection connNew; switch (nConnect) { case CONNECT_NEW: { Connection connOld = AbstractSocketBus.this.f_mapConnections.get(peer); if (connOld == null || lIdThatRx == 0 || connOld.m_lIdentityPeer != lIdThatRx || connOld.m_state.ordinal() >= ConnectionState.DEFUNCT.ordinal()) { connNew = makeConnection(peer); connNew.m_nProtocol = nProt; connNew.m_channel = getChannel(); connNew.m_lIdentityPeer = lIdThatRx; break; } // else; we lost the connection when we were half connected, i.e. we had learned our peer's ID, // but it had not learned ours. Since it didn't know ours it could only do a CONNECT_NEW, but // since we'd managed to learn its ID, we do have sufficient info to migrate. nConnect = CONNECT_MIGRATE; lIdThisRx = connOld.f_lIdentity; // fall through } case CONNECT_MIGRATE: { // as part of migrating a connection we must ensure that we're migrating to the // same logical connection. This is accomplished by only allowing migration if // the connection IDs (ours and our peers) remain the same. We'll each validate // each other's IDs. Note, it is ok to have only one of the two sets match so long // as the other set doesn't match because of an unknown (0). We handle them potentially // not knowing our ID above. Connection connOld = AbstractSocketBus.this.f_mapConnections.get(peer); if (connOld != null && (connOld.f_lIdentity == lIdThisRx || nProt == 1) && (lIdThatRx == 0 || connOld.m_lIdentityPeer == 0 || connOld.m_lIdentityPeer == lIdThatRx) && // peer id may still be unknown, and we learn it now connOld.m_state.ordinal() < ConnectionState.DEFUNCT.ordinal()) { SocketChannel chanOld = connOld.m_channel; SocketChannel chanNew = getChannel(); scheduleUnsafeTask(chanOld, new Runnable() { @Override public void run() { synchronized (connOld) { if (connOld.m_channel == chanOld && connOld.m_state.ordinal() <= ConnectionState.ACTIVE.ordinal()) { if (connOld.m_state == ConnectionState.ACTIVE) { // to get this far we know that the TCP connection went down without the user choosing // for it to, and without the other end dying, thus we should log a higher level log // message, there is something wrong with the environment, though we are auto-correcting // it. getLogger().log( makeExceptionRecord(Level.WARNING, connOld.m_eMigrationCause, "{0} accepting connection migration with {1}, replacing {2} with {3}: {4}", getLocalEndPoint(), peer, connOld.m_channel, getChannel(), connOld)); connOld.m_eMigrationCause = null; } // else; during connection establishment we may have simply had a connect timeout, this is not // worth logging a warning over // move onto the accept phase m_phase = HandshakePhase.ACCEPT; m_headerIn.clear().limit(1); m_headerOut.clear(); m_headerOut.put(CONNECT_MIGRATE); connOld.m_nProtocol = nProt; // may not have been learned previously if (nProt > 3) { // it's possible that we hadn't yet learned our peer's ID, though it // had clearly learned ours if (connOld.m_lIdentityPeer == 0) { connOld.m_lIdentityPeer = lIdThatRx; } // else; we've already asserted that the ids are equal above m_headerOut.putLong(connOld.f_lIdentity); m_headerOut.putLong(connOld.m_lIdentityPeer); } else if (nProt > 1) { m_headerOut.putLong(connOld.m_lIdentityPeer); } m_headerOut.put((byte) 0).flip(); // accept indicator m_connection = connOld; closeChannel(chanOld); connOld.onMigration(); connOld.m_channel = chanNew; // re-register this channel on the original handler to resume processing try { getSelectionService().register(getChannel(), HandshakeHandler.this); connOld.m_handler = HandshakeHandler.this; } catch (IOException e) { closeChannel(chanNew); } } else { closeChannel(chanNew); } } } }, /*cMillis*/ 0); return 0; } // we can't reconnect what we do not have (or to what we've disconnected from); abandon the connection getLogger().log( makeRecord(Level.FINE, "{0} rejecting connection migration from {1} on {2}, no existing connection {3}/{4}", getLocalEndPoint(), peer, getChannel().socket().getLocalSocketAddress(), lIdThisRx, (connOld == null ? 0 : connOld.f_lIdentity))); m_headerIn.clear(); // some read space > accept size m_phase = HandshakePhase.ABANDON; ByteBuffer bufOut = m_headerOut = ByteBuffer.allocate(1 + (nProt > 3 ? 16 : nProt > 1 ? 8 : 0)); bufOut.put(CONNECT_NEW); // indicate that we don't have awareness of this connection // we don't have a connection, and thus don't have an identity; just write zeros if (nProt > 3) { bufOut.putLong(0).putLong(0); } else if (nProt > 1) { bufOut.putLong(0); } bufOut.flip(); return OP_READ | OP_WRITE; } default: throw new IOException("protocol error"); } try { synchronized (connNew) { final Connection connOld; Lock lock = AbstractSocketBus.this.f_lockState.readLock(); lock.lock(); try { BusState nStateCurr = AbstractSocketBus.this.m_nState; if (nStateCurr != BusState.OPEN) { // Bus is closed. Close the handler and return getLogger().log( makeRecord(Level.FINE, "{0} rejecting connection from {1}, bus is closing", getLocalEndPoint(), getChannel().socket().getInetAddress())); close(null); return 0; } connOld = AbstractSocketBus.this.f_mapConnections.putIfAbsent(peer, connNew); if (connOld == null) { // common case, simply open the new connection m_connection = connection = connNew; connection.open(); } } finally { lock.unlock(); // only need for putIfAbsent, and connection.open() } if (connOld != null) { synchronized (connOld) { switch (connOld.m_state) { case OPEN: { // We have an out-bound pending connection which // has yet to finish handshaking; apparently our // peer has the same. We need to choose one, and // ensure that the peer makes the same choice. // lesser of the two acceptor endpoints wins final EndPoint self = getLocalEndPoint(); if (self.getCanonicalName() .compareTo(peer.getCanonicalName()) < 0) { // this bus wins; don't accept the // peer's connection, our initiated // connection will eventually get // accepted by them, and cause them // to close this losing channel. getLogger().log(makeRecord(Level.FINER, "{0} wins simultaneous connect with {1}, abandoning {2}", self, peer, getChannel().socket())); m_phase = HandshakePhase.ABANDON; m_headerIn.clear().limit(2); // some read space > accept size m_headerOut.clear().flip(); // no accept byte return OP_READ; } else { // this bus looses; we will continue to // use connOld, but with the peer's // initiated channel getLogger().log(makeRecord(Level.FINER, "{0} loses simultaneous connect with {1}, closing {2}", self, peer, connOld.m_channel.socket())); // couple this new channel, and // HandshakeHandler with the old Connection // This requires executing all Runnables on the // SelectionSvc thread associated with the old channel // before the new channel is linked with the // old Connection. (for maintaining execution order). // Register a Runnable on the old channel. // Execution of this Runnable ensures that there is no // pending Runnables on the old SS thread for this connection. getLogger().log(makeRecord(Level.FINER, "{0} deferring handshake attempt with {1} on {2}", self, peer, getChannel().socket())); final SocketChannel channelNew = getChannel(); final SelectionService.Handler handlerNew = this; connOld.invoke( new Runnable() { @Override public void run() { boolean fContinue; synchronized (connOld) { try { connOld.m_channel.close(); } catch (IOException ioe) { } if (connOld.m_state.ordinal() < ConnectionState.DEFUNCT.ordinal()) { connOld.m_channel = channelNew; connOld.m_lIdentityPeer = lIdThatRx; fContinue = true; } else { // application released the connection during a concurrent // connect, just drop the new channel fContinue = false; try { channelNew.close(); } catch (IOException e) { } } // re-open invocation Queue queueDeferred = connOld.m_queueDeferred; connOld.m_queueDeferred = null; // reschedule any deferred invocations for (Runnable runnable : queueDeferred) { connOld.invoke(runnable); } } if (fContinue) { getLogger().log(makeRecord(Level.FINER, "{0} continue handshake attempt with {1} on {2}", self, peer, getChannel().socket())); try { // Enable read/write interest for the new channel getSelectionService().register(channelNew, handlerNew); connOld.m_handler = handlerNew; } catch (IOException ioe) { onException(ioe); } } } } ); m_connection = connection = connOld; // disable read/write interest for the new channel. It // will be re-enabled when all the pending Runnables on the old // channel has been executed. nInterest = 0; // defer new Runnables until we switch channels if (connOld.m_queueDeferred == null) { connOld.m_queueDeferred = new LinkedList(); } // else; deferral is already in progress (not sure this can even happen) } } break; case ACTIVE: // We can end up in this situation if the local // disconnect hasn't yet happened. // Fall though and enqueue the new connection // so that it can/ be processed when the old // connection eventually goes away. // fall through case DEFUNCT: // our connection is DISCONNECTED but // the app has yet to release it. // record this into connOld, closing any // prior one if (connOld.m_next == null) { getLogger().log(makeRecord(Level.FINE, "{0} deferring reconnect attempt from {1} on {2}, pending release", getLocalEndPoint(), peer, getChannel().socket())); } else { getLogger().log(makeRecord(Level.FINE, "{0} replacing deferred reconnect attempt from {1} on {2}, pending release", getLocalEndPoint(), peer, getChannel().socket())); connOld.m_next.close(null); } m_connection = connection = connNew; connOld.m_next = this; nInterest = 0; // deffer accept until release break; case FINAL: // to see this the old connection has or // is in the process of being unregistered // try again, i.e. just pop out and let the // next selection operation call back in // for another try // NOTE: headers have been left unchanged return OP_WRITE; default: throw new IllegalStateException("state = " + connOld.m_state); } } } } } finally { if (m_connection != connNew) { connNew.dispose(); } } m_headerOut.clear(); if (nProt > 0) { // Note, that if we're here nConnect == CONNECT_NEW, as inbound migrations are completely handled above. // the acceptor doesn't have sufficient information to send the connect type until it // has received the initator's introduction, so we send it at the start of the accept // phase. Note that on the wire this doesn't look asymetrical since the two phases are // back to back, i.e. the initiator is still in its introduction phase awaiting this byte. m_headerOut.put(nConnect); // if we got this far we're just echoing back the same connect type as the other side if (nProt > 1) { m_headerOut.putLong(connection.f_lIdentity); if (nProt > 3) { m_headerOut.putLong(connection.m_lIdentityPeer); } } } } else // outbound connection { if (!connection.getPeer().equals(peer)) { // out-bound connection, but the peer replied with // a different name then we used. While this is ok from an // inet perspective, it is not ok from a bus perspective since // in order to disallow multiple connections between peers // each peer can only be known via one name. getLogger().log(makeRecord(Level.FINER, "{0} Out-bound connection to" + " {1}, found {2}, single connection pair cannot be ensured", getLocalEndPoint(), connection.getPeer(), peer)); // Should this be threaded as an error? // the reason we don't treat it as an error is that the other side could be listening // on the wildcard address, and we're connecting to it via a specific IP. So here // we choose to be practical and allow the possibility of multiple connections between // peers rather then to not allow connections at all. // TODO improve the protocol so that can be both practical and accurate /* close(new IOException("peer mismatch, expected " + connection.getPeer() + " found " + peer)); return 0;*/ } switch (nConnect) // from peer's perspective { case CONNECT_NEW: long lIdPeerCurr = connection.m_lIdentityPeer; if (lIdPeerCurr == 0) { // common case; initial connection, we learn the peer's identity connection.m_lIdentityPeer = lIdThatRx; } else { // since we'd previously known our peer's id we must have initiated a migration // and the peer responded with CONNECT_NEW indicating that they didn't have a // matching connection. lId should be 0 if its not that is some form of protocol error connection.scheduleDisconnect(new IOException( "connection migration rejected by peer; no existing connection")); return OP_READ; } // else; we're migrating to the same peer break; case CONNECT_MIGRATE: // the accepting peer indicated it is a migration, apparently we must have also sent a migration // or the peer had more info then us (prior half connect) and had a connection with our connection // ID, thus allowing a migration if (nProt > 1) { if (connection.f_lIdentity != lIdThisRx) { // but we've been recycled, i.e. not the the same logical connection connection.scheduleDisconnect(new IOException( "connection migration failed; mismatch on local identity " + connection.f_lIdentity + "/" + lIdThisRx)); return OP_READ; } else if (connection.m_lIdentityPeer == 0) { // this can only happen because of a prior half connect, where we hadn't learned our peer's // connection ID, but it had learned ours. Our peer then decided that this could be // a migration, and thus we learn their ID now connection.m_lIdentityPeer = lIdThatRx; // fall through } else if (nProt > 3 && connection.m_lIdentityPeer != lIdThatRx) { // should not be possible; this is basically an assertion connection.scheduleDisconnect(new IOException( "connection migration failed (protocol error); mismatch on remote identity " + connection.m_lIdentityPeer + "/" + lIdThatRx)); return OP_READ; } // else; identity match, accept the migration } getLogger().log( makeExceptionRecord(Level.WARNING, connection.m_eMigrationCause, "{0} accepted connection migration with {1} on {2}: {3}", getLocalEndPoint(), peer, getChannel(), connection)); connection.m_eMigrationCause = null; break; } m_headerOut.clear(); } connection.setProtocolVersion(nProt); // move onto the accept phase m_phase = HandshakePhase.ACCEPT; m_headerIn.clear().limit(1); m_headerOut.put((byte) 0) // accept byte .flip(); return nInterest; } /** * Evaluate the "accept" byte. * * @return the new interest set * * @throws IOException on an I/O error */ public int onAccept() throws IOException { // getting here means that we've received the accept byte // the value is actually meaningless, but getting the byte is // as it will only be sent if the other side has accepted our // connection, otherwise they would just close the socket Connection connection = m_connection; synchronized (connection) { switch (connection.m_state) { case OPEN: connection.m_state = ConnectionState.ACTIVE; // fall through case ACTIVE: // because of migration // switch out the handlers getSelectionService().register(getChannel(), connection); connection.m_handler = connection; break; default: throw new IllegalStateException("state = " + connection.m_state); } } return 0; } /** * Handle extra data supplied to an abandoned connection. * * @return the new interest set. */ public int onAbandon() { // to get here we've read more then just the peer's accept byte // this means that it has started to use the connection, which // should not be possible, actively reject the connection close(new IOException("protocol error")); return OP_READ; } /** * Close the HandshakeHandler's channel. * * @param eReason the reason for the disconnect, or null */ public void close(Throwable eReason) { // we are closing a channel which never reached the point where // we could exchange data, this channel may or may not be // associated with a local Connection. SocketChannel channel = getChannel(); Connection connection = m_connection; HandshakePhase phase = m_phase; if (connection == null) { if (phase != HandshakePhase.ABANDON) { getLogger().log(makeExceptionRecord( eReason instanceof SSLException ? Level.WARNING : Level.FINEST, eReason, "{0} close due to exception during handshake phase {1} on {2}", getLocalEndPoint(), phase, channel.socket())); } } else { connection.scheduleDisconnect(eReason); } closeChannel(channel); } // ----- data members ------------------------------------------- /** * Non-null once we can associate the handshake with a local * Connection. *

* For out-bound connections this happens immediately, while for * in-bound connections the data isn't available until we've finished * the introduction phase. * */ protected Connection m_connection; /** * The handshake state. */ protected HandshakePhase m_phase = HandshakePhase.NEGOTIATE; /** * The out-bound protocol header. *

* The buffer will be resized as needed for each phase. */ protected ByteBuffer m_headerOut; /** * The in-bound protocol header. *

* Initially only large enough to complete the identification phase, * it will be resized for the introduction phase once we know that * we can communicate with the peer. */ protected ByteBuffer m_headerIn; /** * The negotiated protocol version. */ protected int m_nProtocol; } // ---- AcceptHandler --------------------------------------------------- /** * AcceptHandler accepts new client connections. */ protected class AcceptHandler extends SafeSelectionHandler { /** * Construct an AcceptHandler for the bus. * * @param channel the ServerSocketChannel */ protected AcceptHandler(ServerSocketChannel channel) { super(channel); } /** * {@inheritDoc} */ public int onReadySafe(int nOps) { SocketChannel chan = null; try { chan = getChannel().accept(); if (chan != null) { getLogger().log(makeRecord(Level.FINEST, "{0} starting phase NEGOTIATE on {1}", getLocalEndPoint(), chan.socket())); Sockets.configureBlocking(chan, false); configureSocket(chan.socket()); getSelectionService().register(chan, new HandshakeHandler(chan, null)); } } catch (IOException e) { if (chan == null) { // error in accept throw new RuntimeException(e); } else // error with new channel; just close it { try { chan.close(); } catch (IOException e1) {} } } return OP_ACCEPT; } /** * {@inheritDoc} */ public int onException(Throwable t) { // in the event of an unexpected exception such as OOME, we // likely don't want to close our server socket ServerSocketChannel channel = getChannel(); if (channel.isOpen()) { getLogger().log(makeExceptionRecord(Level.INFO, t, "{0} unexpected exception during Bus accept, ignoring", getLocalEndPoint())); return OP_ACCEPT; } else // not open { synchronized (AbstractSocketBus.this) { if (m_nState == BusState.OPEN) { // our ServerSocket was unexpectedly closed; can this even happen? getLogger().log(makeExceptionRecord(Level.SEVERE, t, "{0} ServerSocket failure; no new connection will be accepted", getLocalEndPoint())); } return 0; } } } } /** * BusState represents the various states a Bus may be in. */ protected enum BusState { /** * State indicate that the bus has yet to be opened. */ INITIAL, /** * State indicate that the bus has been opened. */ OPEN, /** * State indicate that the bus is closing. */ CLOSING, /** * State indicate that the bus has been closed. */ CLOSED } // ----- data members --------------------------------------------------- /** * The SocketDriver which produced this bus. */ protected final SocketBusDriver f_driver; /** * For the purpose of testing failed connections. */ protected final int f_nDropRatio; /** * For the purpose of testing corrupted data stream. */ protected final int f_nCorruptionRatio; /** * True if CRC validation is enabled for this bus. */ protected final boolean f_fCrc; /** * The ServerSocketChannel on which this bus accepts new connections. */ private final ServerSocketChannel f_channelServer; /** * The state of the bus. *

* Changes to the state must be done which holding the write lock on * f_lockState. */ private volatile BusState m_nState = BusState.INITIAL; /** * Lock protecting changes to the bus state. *

* The write lock must be held when changing the bus state. The read lock * must be held when adding or releasing connections. Otherwise locking is * done on a per-connection basis, by synchronizing on the connection. */ private final ReadWriteLock f_lockState = new ReentrantReadWriteLock(); /** * The local EndPoint for the bus. */ protected UrlEndPoint m_pointLocal; /** * The registered event collector. */ private Collector m_collectorEvent; /** * Map of current connections. *

* Changes to this map must be made while holding the read lock on * f_lockState. */ private final ConcurrentMap f_mapConnections = new ConcurrentHashMap<>(); /** * Set of connections to flush. */ private final Set f_setFlush = Collections.newSetFromMap(new ConcurrentHashMap<>()); /** * Empty connection array. */ private static final Connection[] EMPTY_CONNECTION_ARRAY = new Connection[0]; /** * The connection ID generator. */ protected static final AtomicLong f_atomicIdGenerator = new AtomicLong(SafeClock.INSTANCE.getSafeTimeMillis()); /** * used to indicate that a new connection is desired. */ private static final byte CONNECT_NEW = 0; /** * Used to indicate that the peer wishes to migrate a connection to a new channel */ private static final byte CONNECT_MIGRATE = 1; }





© 2015 - 2024 Weber Informatics LLC | Privacy Policy