/* * Copyright 2014 Higher Frequency Trading * * http://www.higherfrequencytrading.com * * Licensed 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 net.openhft.chronicle.map; import net.openhft.lang.io.ByteBufferBytes; import net.openhft.lang.io.Bytes; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import java.io.Closeable; import java.io.IOException; import java.net.*; import java.nio.BufferUnderflowException; import java.nio.ByteBuffer; import java.nio.ByteOrder; import java.nio.channels.*; import java.util.*; import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicBoolean; import static java.nio.channels.SelectionKey.*; import static net.openhft.chronicle.map.StatelessChronicleMap.EventId.HEARTBEAT; /** * Used with a {@see net.openhft.map.ReplicatedSharedHashMap} to send data between the maps using a * socket connection {@see net.openhft.map.OutSocketReplicator} * * @author Rob Austin. */ class TcpReplicator extends AbstractChannelReplicator implements Closeable { private static final Logger LOG = LoggerFactory.getLogger(TcpReplicator.class.getName()); private static final int BUFFER_SIZE = 0x100000; // 1MB public static final int STATELESS_CLIENT = -127; public static final byte NOT_SET = (byte) HEARTBEAT.ordinal(); private final SelectionKey[] selectionKeysStore = new SelectionKey[Byte.MAX_VALUE + 1]; // used to instruct the selector thread to set OP_WRITE on a key correlated by the bit index in the // bitset private final KeyInterestUpdater opWriteUpdater = new KeyInterestUpdater(OP_WRITE, selectionKeysStore); private final BitSet activeKeys = new BitSet(selectionKeysStore.length); private final long heartBeatIntervalMillis; private final Replica replica; private final byte localIdentifier; private final int maxEntrySizeBytes; private final Replica.EntryExternalizable externalizable; private final TcpReplicationConfig replicationConfig; private final StatelessServerConnector statelessServerConnector; private long selectorTimeout; /** * @param maxEntrySizeBytes used to check that the last entry will fit into the buffer, * it can not be smaller than the size of and entry, if it is * set smaller the buffer will over flow, it can be larger then * the entry, but setting it too large reduces the workable * space in the buffer. * @param statelessServerConnector set to NULL if not required * @throws IOException */ TcpReplicator(@NotNull final Replica replica, @NotNull final Replica.EntryExternalizable externalizable, @NotNull final TcpReplicationConfig replicationConfig, final int maxEntrySizeBytes, @Nullable StatelessServerConnector statelessServerConnector) throws IOException { super("TcpSocketReplicator-" + replica.identifier(), replicationConfig.throttlingConfig(), maxEntrySizeBytes); this.statelessServerConnector = statelessServerConnector; final ThrottlingConfig throttlingConfig = replicationConfig.throttlingConfig(); long throttleBucketInterval = TimeUnit.MILLISECONDS.convert(throttlingConfig .bucketInterval(), throttlingConfig.bucketIntervalUnit()); heartBeatIntervalMillis = TimeUnit.MILLISECONDS.convert(replicationConfig.heartBeatInterval(), replicationConfig.heartBeatIntervalUnit()); selectorTimeout = Math.min(heartBeatIntervalMillis, throttleBucketInterval); this.replica = replica; this.localIdentifier = replica.identifier(); this.maxEntrySizeBytes = maxEntrySizeBytes; this.externalizable = externalizable; this.replicationConfig = replicationConfig; start(); } @Override void process() throws IOException { try { final InetSocketAddress serverInetSocketAddress = new InetSocketAddress(replicationConfig .serverPort()); final Details serverDetails = new Details(serverInetSocketAddress, localIdentifier); new ServerConnector(serverDetails).connect(); for (InetSocketAddress client : replicationConfig.endpoints()) { final Details clientDetails = new Details(client, localIdentifier); new ClientConnector(clientDetails).connect(); } while (selector.isOpen()) { registerPendingRegistrations(); final int nSelectedKeys = selector.select(selectorTimeout); // its less resource intensive to set this less frequently and use an approximation final long approxTime = System.currentTimeMillis(); checkThrottleInterval(); // check that we have sent and received heartbeats heartBeatMonitor(approxTime); // set the OP_WRITE when data is ready to send opWriteUpdater.applyUpdates(); if (nSelectedKeys == 0) continue; // go back and check pendingRegistrations final Set selectionKeys = selector.selectedKeys(); for (final SelectionKey key : selectionKeys) { try { if (!key.isValid()) continue; if (key.isAcceptable()) onAccept(key); if (key.isConnectable()) onConnect(key); if (key.isReadable()) onRead(key, approxTime); if (key.isWritable()) onWrite(key, approxTime); } catch (CancelledKeyException e) { if (!isClosed) quietClose(key, e); } catch (ClosedSelectorException e) { if (!isClosed) quietClose(key, e); } catch (IOException e) { if (!isClosed) quietClose(key, e); } catch (InterruptedException e) { if (!isClosed) quietClose(key, e); } catch (Exception e) { LOG.info("", e); if (!isClosed) closeEarlyAndQuietly(key.channel()); } } selectionKeys.clear(); } } catch (CancelledKeyException e) { if (LOG.isDebugEnabled()) LOG.debug("", e); } catch (ClosedSelectorException e) { if (LOG.isDebugEnabled()) LOG.debug("", e); } catch (ClosedChannelException e) { if (LOG.isDebugEnabled()) LOG.debug("", e); } catch (ConnectException e) { if (LOG.isDebugEnabled()) LOG.debug("", e); } catch (Exception e) { LOG.error("", e); } finally { if (!isClosed) { close(); } } } /** * checks that we receive heartbeats and send out heart beats. * * @param approxTime the approximate time in milliseconds */ void heartBeatMonitor(long approxTime) { for (int i = activeKeys.nextSetBit(0); i >= 0; i = activeKeys.nextSetBit(i + 1)) { try { final SelectionKey key = selectionKeysStore[i]; if (!key.isValid() || !key.channel().isOpen()) { activeKeys.clear(i); continue; } final Attached attachment = (Attached) key.attachment(); if (!attachment.hasRemoteHeartbeatInterval) continue; try { sendHeartbeatIfRequired(approxTime, key); } catch (Exception e) { if (LOG.isDebugEnabled()) LOG.debug("", e); } try { heartbeatCheckHasReceived(key, approxTime); } catch (Exception e) { if (LOG.isDebugEnabled()) LOG.debug("", e); } } catch (Exception e) { if (LOG.isDebugEnabled()) LOG.debug("", e); } } } /** * check to see if its time to send a heartbeat, and send one if required * * @param approxTime the current time ( approximately ) * @param key nio selection key */ private void sendHeartbeatIfRequired(final long approxTime, @NotNull final SelectionKey key) { final Attached attachment = (Attached) key.attachment(); if (attachment.isHandShakingComplete() && attachment.entryWriter.lastSentTime + heartBeatIntervalMillis < approxTime) { attachment.entryWriter.lastSentTime = approxTime; attachment.entryWriter.writeHeartbeatToBuffer(); enableOpWrite(key); if (LOG.isDebugEnabled()) LOG.debug("sending heartbeat"); } } private void enableOpWrite(SelectionKey key) { int ops = key.interestOps(); if ((ops & (OP_CONNECT | OP_ACCEPT)) == 0) key.interestOps(ops | OP_WRITE); } /** * check to see if we have lost connection with the remote node and if we have attempts a * reconnect. * * @param key the key relating to the heartbeat that we are checking * @param approxTimeOutTime the approximate time in milliseconds * @throws ConnectException */ private void heartbeatCheckHasReceived(@NotNull final SelectionKey key, final long approxTimeOutTime) throws ConnectException { final Attached attached = (Attached) key.attachment(); // we wont attempt to reconnect the server socket if (attached.isServer || !attached.isHandShakingComplete()) return; final SocketChannel channel = (SocketChannel) key.channel(); if (approxTimeOutTime > attached.entryReader.lastHeartBeatReceived + attached.remoteHeartbeatInterval) { if (LOG.isDebugEnabled()) LOG.debug("lost connection, attempting to reconnect. " + "missed heartbeat from identifier=" + attached.remoteIdentifier); activeKeys.clear(attached.remoteIdentifier); closeables.closeQuietly(channel.socket()); // when node discovery is used ( by nodes broadcasting out their host:port over UDP ), // when new or restarted nodes are started up. they attempt to find the nodes // on the grid by listening to the host and ports of the other nodes, so these nodes will establish the connection when they come back up, // hence under these circumstances, polling a dropped node to attempt to reconnect is no-longer // required as the remote node will establish the connection its self on startup. if (replicationConfig.autoReconnectedUponDroppedConnection()) attached.connector.connectLater(); } } /** * closes and only logs the exception at debug * * @param key the SelectionKey * @param e the Exception that caused the issue */ private void quietClose(@NotNull final SelectionKey key, @NotNull final Exception e) { if (LOG.isDebugEnabled()) LOG.debug("", e); closeEarlyAndQuietly(key.channel()); } /** * called when the selector receives a OP_CONNECT message */ private void onConnect(@NotNull final SelectionKey key) throws IOException, InterruptedException { SocketChannel channel = null; try { channel = (SocketChannel) key.channel(); } finally { closeables.add(channel); } final Attached attached = (Attached) key.attachment(); try { if (!channel.finishConnect()) { return; } } catch (SocketException e) { quietClose(key, e); // when node discovery is used ( by nodes broadcasting out their host:port over UDP ), // when new or restarted nodes are started up. they attempt to find the nodes // on the grid by listening to the host and ports of the other nodes, // so these nodes will establish the connection when they come back up, // hence under these circumstances, polling a dropped node to attempt to reconnect is no-longer // required as the remote node will establish the connection its self on startup. if (replicationConfig.autoReconnectedUponDroppedConnection()) attached.connector.connect(); throw e; } attached.connector.setSuccessfullyConnected(); if (LOG.isDebugEnabled()) LOG.debug("successfully connected to {}, local-id={}", channel.socket().getInetAddress(), localIdentifier); channel.configureBlocking(false); channel.socket().setTcpNoDelay(true); channel.socket().setSoTimeout(0); channel.socket().setSoLinger(false, 0); attached.entryReader = new TcpSocketChannelEntryReader(); attached.entryWriter = new TcpSocketChannelEntryWriter(); key.interestOps(OP_WRITE | OP_READ); throttle(channel); // register it with the selector and store the ModificationIterator for this key attached.entryWriter.identifierToBuffer(localIdentifier); } /** * called when the selector receives a OP_ACCEPT message */ private void onAccept(@NotNull final SelectionKey key) throws IOException { ServerSocketChannel server = null; try { server = (ServerSocketChannel) key.channel(); } finally { if (server != null) closeables.add(server); } SocketChannel channel = null; try { channel = server.accept(); } finally { if (channel != null) closeables.add(channel); } channel.configureBlocking(false); channel.socket().setReuseAddress(true); channel.socket().setTcpNoDelay(true); channel.socket().setSoTimeout(0); channel.socket().setSoLinger(false, 0); final Attached attached = new Attached(); channel.register(selector, OP_WRITE | OP_READ, attached); throttle(channel); attached.entryReader = new TcpSocketChannelEntryReader(); attached.entryWriter = new TcpSocketChannelEntryWriter(); attached.isServer = true; attached.entryWriter.identifierToBuffer(localIdentifier); } /** * this can be called when a new CHM is added to a cluster, we have to rebootstrap so will clear * all the old bootstrap information * * @param key the nio SelectionKey */ private void clearHandshaking(SelectionKey key) { final Attached attached = (Attached) key.attachment(); activeKeys.clear(attached.remoteIdentifier); selectionKeysStore[attached.remoteIdentifier] = null; attached.clearHandShaking(); } /** * used to exchange identifiers and timestamps and heartbeat intervals between the server and * client * * @param key the SelectionKey relating to the this cha * @param socketChannel * @throws java.io.IOException * @throws InterruptedException */ private void doHandShaking(@NotNull final SelectionKey key, SocketChannel socketChannel) throws IOException, InterruptedException { final Attached attached = (Attached) key.attachment(); final TcpSocketChannelEntryWriter writer = attached.entryWriter; final TcpSocketChannelEntryReader reader = attached.entryReader; if (attached.remoteIdentifier == Byte.MIN_VALUE) { final byte remoteIdentifier = reader.identifierFromBuffer(); if (remoteIdentifier == STATELESS_CLIENT) { attached.handShakingComplete = true; attached.hasRemoteHeartbeatInterval = false; return; } if (remoteIdentifier == Byte.MIN_VALUE) return; attached.remoteIdentifier = remoteIdentifier; // we use the as iterating the activeKeys via the bitset wont create and Objects // but if we use the selector.keys() this will. selectionKeysStore[remoteIdentifier] = key; activeKeys.set(remoteIdentifier); if (LOG.isDebugEnabled()) { LOG.debug("server-connection id={}, remoteIdentifier={}", localIdentifier, remoteIdentifier); } // this can occur sometimes when if 2 or more remote node attempt to use node discovery at the // same time final IdentifierListener identifierListener = replicationConfig.identifierListener; final SocketAddress remoteAddress = socketChannel.getRemoteAddress(); if ((identifierListener != null && !identifierListener.isIdentifierUnique(remoteIdentifier, remoteAddress)) || remoteIdentifier == localIdentifier) // throwing this exception will cause us to disconnect, both the client and server // will be able to detect the the remote and local identifiers are the same, // as the identifier is send early on in the hand shaking via the connect(() and accept() // methods throw new IllegalStateException("dropping connection, " + "as the remote-identifier is already being used, identifier=" + remoteIdentifier); attached.remoteModificationIterator = replica.acquireModificationIterator(remoteIdentifier, attached); writer.writeRemoteBootstrapTimestamp(replica.lastModificationTime(remoteIdentifier)); // tell the remote node, what are heartbeat interval is writer.writeRemoteHeartbeatInterval(heartBeatIntervalMillis); } if (attached.remoteBootstrapTimestamp == Long.MIN_VALUE) { attached.remoteBootstrapTimestamp = reader.remoteBootstrapTimestamp(); if (attached.remoteBootstrapTimestamp == Long.MIN_VALUE) return; } if (!attached.hasRemoteHeartbeatInterval) { long value = reader.remoteHeartbeatIntervalFromBuffer(); if (value == Long.MIN_VALUE) return; if (value < 0) { LOG.error("value=" + value); } // we add a 10% safety margin to the timeout time due to latency fluctuations on the network, // in other words we wont consider a connection to have // timed out, unless the heartbeat interval has exceeded 25% of the expected time. attached.remoteHeartbeatInterval = (long) (value * 1.25); // we have to make our selector poll interval at least as short as the minimum selector timeout selectorTimeout = Math.min(selectorTimeout, value); if (selectorTimeout < 0) LOG.info(""); attached.hasRemoteHeartbeatInterval = true; // now we're finished we can get on with reading the entries attached.handShakingComplete = true; attached.remoteModificationIterator.dirtyEntries(attached.remoteBootstrapTimestamp); reader.entriesFromBuffer(attached); } } /** * called when the selector receives a OP_WRITE message */ private void onWrite(@NotNull final SelectionKey key, final long approxTime) throws InterruptedException, IOException { final SocketChannel socketChannel = (SocketChannel) key.channel(); final Attached attached = (Attached) key.attachment(); if (attached.entryWriter.isWorkIncomplete()) { boolean completed = attached.entryWriter.doWork(); if (completed) attached.entryWriter.workCompleted(); // attached.hasRemoteHeartbeatInterval = false; } else if (attached.remoteModificationIterator != null) attached.entryWriter.entriesToBuffer(attached.remoteModificationIterator, key); try { int bytesJustWritten = attached.entryWriter.writeBufferToSocket(socketChannel, approxTime); contemplateThrottleWrites(bytesJustWritten); } catch (IOException e) { quietClose(key, e); if (!attached.isServer) attached.connector.connectLater(); throw e; } } /** * called when the selector receives a OP_READ message */ private void onRead(final SelectionKey key, final long approxTime) throws IOException, InterruptedException { final SocketChannel socketChannel = (SocketChannel) key.channel(); final Attached attached = (Attached) key.attachment(); if (attached.entryWriter.isWorkIncomplete()) return; try { if (attached.entryReader.readSocketToBuffer(socketChannel) <= 0) return; } catch (IOException e) { if (!attached.isServer) attached.connector.connectLater(); throw e; } if (LOG.isDebugEnabled()) LOG.debug("heartbeat or data received."); attached.entryReader.lastHeartBeatReceived = approxTime; if (attached.isHandShakingComplete()) { attached.entryReader.entriesFromBuffer(attached); } else { doHandShaking(key, socketChannel); } } /** * sets interestOps to "selector keys",The change to interestOps much be on the same thread as * the selector. This class, allows via {@link AbstractChannelReplicator * .KeyInterestUpdater#set(int)} to holds a pending change in interestOps ( via a bitset ), * this change is processed later on the same thread as the selector */ private static class KeyInterestUpdater { private final AtomicBoolean wasChanged = new AtomicBoolean(); private final BitSet changeOfOpWriteRequired; private final SelectionKey[] selectionKeys; private final int op; KeyInterestUpdater(int op, final SelectionKey[] selectionKeys) { this.op = op; this.selectionKeys = selectionKeys; changeOfOpWriteRequired = new BitSet(selectionKeys.length); } public void applyUpdates() { if (wasChanged.getAndSet(false)) { for (int i = changeOfOpWriteRequired.nextSetBit(0); i >= 0; i = changeOfOpWriteRequired.nextSetBit(i + 1)) { changeOfOpWriteRequired.clear(i); final SelectionKey key = selectionKeys[i]; try { key.interestOps(key.interestOps() | op); } catch (Exception e) { LOG.debug("", e); } } } } /** * @param keyIndex the index of the key that has changed, the list of keys is provided by * the constructor {@link KeyInterestUpdater(int, SelectionKey[])} */ public void set(int keyIndex) { changeOfOpWriteRequired.set(keyIndex); wasChanged.lazySet(true); } } private class ServerConnector extends AbstractConnector { private final Details details; private ServerConnector(@NotNull Details details) { super("TCP-ServerConnector-" + localIdentifier); this.details = details; } @Override public String toString() { return "ServerConnector{" + "" + details + '}'; } SelectableChannel doConnect() throws IOException, InterruptedException { final ServerSocketChannel serverChannel = openServerSocketChannel(); serverChannel.socket().setReceiveBufferSize(BUFFER_SIZE); serverChannel.configureBlocking(false); ServerSocket serverSocket = null; try { serverSocket = serverChannel.socket(); } finally { if (serverSocket != null) closeables.add(serverSocket); } serverSocket.setReuseAddress(true); serverSocket.bind(details.address()); // these can be run on this thread addPendingRegistration(new Runnable() { @Override public void run() { final Attached attached = new Attached(); attached.connector = ServerConnector.this; try { serverChannel.register(TcpReplicator.this.selector, OP_ACCEPT, attached); } catch (ClosedChannelException e) { LOG.error("", e); } } }); selector.wakeup(); return serverChannel; } } private ServerSocketChannel openServerSocketChannel() throws IOException { ServerSocketChannel result = null; try { result = ServerSocketChannel.open(); } finally { if (result != null) closeables.add(result); } return result; } private class ClientConnector extends AbstractConnector { private final Details details; private ClientConnector(@NotNull Details details) { super("TCP-ClientConnector-" + details.localIdentifier()); this.details = details; } @Override public String toString() { return "ClientConnector{" + details + '}'; } /** * blocks until connected */ @Override SelectableChannel doConnect() throws IOException, InterruptedException { boolean success = false; final SocketChannel socketChannel = openSocketChannel(TcpReplicator.this.closeables); try { socketChannel.configureBlocking(false); socketChannel.socket().setReuseAddress(true); socketChannel.socket().setSoLinger(false, 0); socketChannel.socket().setSoTimeout(0); socketChannel.socket().setTcpNoDelay(true); try { socketChannel.connect(details.address()); } catch (UnresolvedAddressException e) { this.connectLater(); } // Under experiment, the concoction was found to be more successful if we // paused before registering the OP_CONNECT Thread.sleep(10); // the registration has be be run on the same thread as the selector addPendingRegistration(new Runnable() { @Override public void run() { final Attached attached = new Attached(); attached.connector = ClientConnector.this; try { socketChannel.register(selector, OP_CONNECT, attached); } catch (ClosedChannelException e) { if (socketChannel.isOpen()) LOG.error("", e); } } }); selector.wakeup(); success = true; return socketChannel; } finally { if (!success) { try { try { socketChannel.socket().close(); } catch (Exception e) { LOG.error("", e); } socketChannel.close(); } catch (IOException e) { LOG.error("", e); } } } } } /** * Attached to the NIO selection key via methods such as {@link SelectionKey#attach(Object)} */ class Attached implements Replica.ModificationNotifier { public TcpSocketChannelEntryReader entryReader; public TcpSocketChannelEntryWriter entryWriter; public Replica.ModificationIterator remoteModificationIterator; public AbstractConnector connector; public long remoteBootstrapTimestamp = Long.MIN_VALUE; public byte remoteIdentifier = Byte.MIN_VALUE; public boolean hasRemoteHeartbeatInterval; // true if its socket is a ServerSocket public boolean isServer; // the frequency the remote node will send a heartbeat public long remoteHeartbeatInterval = heartBeatIntervalMillis; public boolean handShakingComplete; boolean isHandShakingComplete() { return handShakingComplete; } void clearHandShaking() { handShakingComplete = false; remoteIdentifier = Byte.MIN_VALUE; remoteBootstrapTimestamp = Long.MIN_VALUE; remoteHeartbeatInterval = heartBeatIntervalMillis; hasRemoteHeartbeatInterval = false; remoteModificationIterator = null; } /** * called whenever there is a change to the modification iterator */ @Override public void onChange() { if (remoteIdentifier != Byte.MIN_VALUE) TcpReplicator.this.opWriteUpdater.set(remoteIdentifier); } } /** * @author Rob Austin. */ private class TcpSocketChannelEntryWriter { private final ByteBuffer out; final ByteBufferBytes in; private final EntryCallback entryCallback; private long lastSentTime; // if uncompletedWork is set ( not null ) , this must be completed before any further work // is carried out public Work uncompletedWork; private TcpSocketChannelEntryWriter() { out = ByteBuffer.allocateDirect(replicationConfig.packetSize() + maxEntrySizeBytes); in = new ByteBufferBytes(out); entryCallback = new EntryCallback(externalizable, in); } public boolean isWorkIncomplete() { return uncompletedWork != null; } public void workCompleted() { uncompletedWork = null; } /** * writes the timestamp into the buffer * * @param localIdentifier the current nodes identifier */ void identifierToBuffer(final byte localIdentifier) { in.writeByte(localIdentifier); } /** * sends the identity and timestamp of this node to a remote node * * @param timeStampOfLastMessage the last timestamp we received a message from that node */ void writeRemoteBootstrapTimestamp(final long timeStampOfLastMessage) { in.writeLong(timeStampOfLastMessage); } /** * writes all the entries that have changed, to the buffer which will later be written to * TCP/IP * * @param modificationIterator a record of which entries have modification * @param selectionKey */ void entriesToBuffer(@NotNull final Replica.ModificationIterator modificationIterator, @NotNull final SelectionKey selectionKey) throws InterruptedException, IOException { final SocketChannel socketChannel = (SocketChannel) selectionKey.channel(); final Attached attached = (Attached) selectionKey.attachment(); // this can occur when new map are added to a channel final boolean handShakingComplete = attached.isHandShakingComplete(); for (; ; ) { final boolean wasDataRead = modificationIterator.nextEntry(entryCallback, 0); if (!wasDataRead) { // if we have no more data to write to the socket then we will // un-register OP_WRITE on the selector, until more data becomes available if (in.position() == 0 && handShakingComplete) disableWrite(socketChannel, attached); return; } // we've filled up the buffer lets give another channel a chance to send some data if (in.remaining() <= maxEntrySizeBytes) return; // if we have space in the buffer to write more data and we just wrote data into the // buffer then let try and write some more } } /** * writes the contents of the buffer to the socket * * @param socketChannel the socket to publish the buffer to * @param approxTime an approximation of the current time in millis * @throws IOException */ private int writeBufferToSocket(@NotNull final SocketChannel socketChannel, final long approxTime) throws IOException { if (in.position() == 0) return 0; // if we still have some unwritten writer from last time lastSentTime = approxTime; out.limit((int) in.position()); final int len = socketChannel.write(out); if (LOG.isDebugEnabled()) LOG.debug("bytes-written=" + len); if (out.remaining() == 0) { out.clear(); in.clear(); } else { out.compact(); in.position(out.position()); in.limit(in.capacity()); out.clear(); } return len; } /** * used to send an single zero byte if we have not send any data for up to the * localHeartbeatInterval */ private void writeHeartbeatToBuffer() { // denotes the state - 0 for a heartbeat in.writeByte(HEARTBEAT.ordinal()); // denotes the size in bytes in.writeInt(0); } private void writeRemoteHeartbeatInterval(long localHeartbeatInterval) { in.writeLong(localHeartbeatInterval); } /** * removes back in the OP_WRITE from the selector, otherwise it'll spin loop. The OP_WRITE * will get added back in as soon as we have data to write * * @param socketChannel the socketChannel we wish to stop writing to * @param attached data associated with the socketChannels key */ public synchronized void disableWrite(@NotNull final SocketChannel socketChannel, @NotNull final Attached attached) { try { SelectionKey key = socketChannel.keyFor(selector); if (key != null) { if (attached.isHandShakingComplete() && selector.isOpen()) { if (LOG.isDebugEnabled()) LOG.debug("Disabling OP_WRITE to remoteIdentifier=" + attached.remoteIdentifier + ", localIdentifier=" + localIdentifier); key.interestOps(key.interestOps() & ~OP_WRITE); } } } catch (Exception e) { LOG.error("", e); } } public boolean doWork() { return uncompletedWork.doWork(in); } } /** * Reads map entries from a socket, this could be a client or server socket */ class TcpSocketChannelEntryReader { public static final int HEADROOM = 1024; ByteBuffer in; ByteBufferBytes out; public long lastHeartBeatReceived = System.currentTimeMillis(); private long sizeInBytes; private byte state; private TcpSocketChannelEntryReader() { in = ByteBuffer.allocateDirect(replicationConfig.packetSize() + maxEntrySizeBytes); out = new ByteBufferBytes(in.slice()); out.limit(0); in.clear(); } void resizeBuffer(long size) { assert size < Integer.MAX_VALUE; LOG.info("in before position=" + in.position() + ", " + "limit=" + in.limit() + ",capacity=" + in.capacity()); LOG.info("out before position=" + out.position() + ", " + "limit=" + out.limit() + ",capacity=" + out.capacity()); if (size < in.capacity()) throw new IllegalStateException("it not possible to resize the buffer smaller"); final ByteBuffer buffer = ByteBuffer.allocateDirect((int) size).order(ByteOrder.nativeOrder()); final int inPosition = in.position(); long outPosition = out.position(); long outLimit = out.limit(); out = new ByteBufferBytes(buffer.slice()); in.position(0); for (int i = 0; i < inPosition; i++) { buffer.put(in.get()); } in = buffer; in.limit(in.capacity()); in.position(inPosition); out.limit(outLimit); out.position(outPosition); LOG.info("after in position=" + in.position() + ", " + "limit=" + in.limit() + ",capacity=" + in.capacity()); LOG.info("after out position=" + out.position() + ", " + "limit=" + out.limit() + ",capacity=" + out.capacity()); } /** * reads from the socket and writes them to the buffer * * @param socketChannel the socketChannel to read from * @return the number of bytes read * @throws IOException */ private int readSocketToBuffer(@NotNull final SocketChannel socketChannel) throws IOException { compactBuffer(); final int len = socketChannel.read(in); out.limit(in.position()); return len; } /** * reads entries from the buffer till empty * * @param attached * @throws InterruptedException */ private void entriesFromBuffer(Attached attached) throws InterruptedException, IOException { for (; ; ) { out.limit(in.position()); // its set to MIN_VALUE when it should be read again if (state == NOT_SET) { if (out.remaining() < SIZE_OF_SIZE + 1) { return; } // state is used for both heartbeat and stateless state = out.readByte(); sizeInBytes = out.readInt(); // if the buffer is too small to read this payload we will have to grow the // size of the buffer long requiredSize = sizeInBytes + SIZE_OF_SIZE + 1; if (out.capacity() < requiredSize) { attached.entryReader.resizeBuffer(requiredSize + HEADROOM); } // this is the : // -- heartbeat if its 0 // -- stateful update if its 1 // -- the id of the stateful event if (state == NOT_SET) continue; } if (out.remaining() < sizeInBytes) { return; } final long nextEntryPos = out.position() + sizeInBytes; final long limit = out.limit(); out.limit(nextEntryPos); boolean isStateless = (state != 1); if (isStateless) { if (statelessServerConnector == null) { LOG.error("", new IllegalArgumentException("received an event " + "from a stateless map, stateless maps are not " + "currently supported when using Chronicle Channels")); } else { final Work futureWork = statelessServerConnector.processStatelessEvent(state, attached.entryWriter.in, attached.entryReader.out); // in some cases it may not be possible to send out all the data before we // fill out the write buffer, so this data will be send when the buffer // is no longer full, and as such is treated as future work if (futureWork != null) { // we will complete what we can now boolean isComplete = futureWork.doWork(attached.entryWriter.in); if (!isComplete) attached.entryWriter.uncompletedWork = futureWork; } } } else externalizable.readExternalEntry(out); out.limit(limit); // skip onto the next entry out.position(nextEntryPos); state = NOT_SET; sizeInBytes = 0; } } /** * compacts the buffer and updates the {@code in} and {@code out} accordingly */ private void compactBuffer() { // the maxEntrySizeBytes used here may not be the maximum size of the entry in its serialized form // however, its only use as an indication that the buffer is becoming full and should be compacted // the buffer can be compacted at any time if (in.position() == 0 || in.remaining() > maxEntrySizeBytes) return; in.limit(in.position()); in.position((int) out.position()); in.compact(); out.position(0); } /** * @return the identifier or -1 if unsuccessful */ byte identifierFromBuffer() { return (out.remaining() >= 1) ? out.readByte() : Byte.MIN_VALUE; } /** * @return the timestamp or -1 if unsuccessful */ long remoteBootstrapTimestamp() { if (out.remaining() >= 8) return out.readLong(); else return Long.MIN_VALUE; // return (out.remaining() >= 8) ? out.readLong() : Long.MIN_VALUE; } public long remoteHeartbeatIntervalFromBuffer() { return (out.remaining() >= 8) ? out.readLong() : Long.MIN_VALUE; } } } interface Work { /** * @param in the buffer that we will fill up * @return true when all the work is complete */ boolean doWork(@NotNull Bytes in); } /** * @author Rob Austin. */ class StatelessServerConnector { private static final Logger LOG = LoggerFactory.getLogger(StatelessServerConnector.class .getName()); ; private final KeyValueSerializer keyValueSerializer; private final ChronicleMap map; private double maxEntrySizeBytes; StatelessServerConnector(@NotNull KeyValueSerializer keyValueSerializer, @NotNull ChronicleMap map, int maxEntrySizeBytes) { this.keyValueSerializer = keyValueSerializer; this.map = map; this.maxEntrySizeBytes = maxEntrySizeBytes; } Work processStatelessEvent(final byte eventId, @NotNull final Bytes writer, @NotNull final ByteBufferBytes reader) { /* LOG.info("processStatelessEvent - writer position=" + reader.position() + ", " + "limit=" + reader.limit() + ",capacity=" + reader.capacity()); LOG.info("StatelessServerConnector- reader position=" + writer.position() + ", " + "limit=" + writer.limit() + ",capacity=" + writer.capacity());*/ final StatelessChronicleMap.EventId event = StatelessChronicleMap.EventId.values()[eventId]; switch (event) { case LONG_SIZE: return longSize(reader, writer); case IS_EMPTY: return isEmpty(reader, writer); case CONTAINS_KEY: return containsKey(reader, writer); case CONTAINS_VALUE: return containsValue(reader, writer); case GET: return get(reader, writer); case PUT: return put(reader, writer); case REMOVE: return remove(reader, writer); case CLEAR: return clear(reader, writer); case KEY_SET: return keySet(reader, writer); case VALUES: return values(reader, writer); case ENTRY_SET: return entrySet(reader, writer); case REPLACE: return replace(reader, writer); case REPLACE_WITH_OLD_AND_NEW_VALUE: return replaceWithOldAndNew(reader, writer); case PUT_IF_ABSENT: return putIfAbsent(reader, writer); case REMOVE_WITH_VALUE: return removeWithValue(reader, writer); case SIZE: return size(reader, writer); case TO_STRING: return toString(reader, writer); case PUT_ALL: return putAll(reader, writer); case HASH_CODE: return hashCode(reader, writer); case EQUALS: return equals(reader, writer); default: throw new IllegalStateException("unsupported event=" + event); } } private Work removeWithValue(Bytes reader, Bytes writer) { final K key = readKey(reader); final V readValue = readValue(reader); final long sizeLocation = reflectTransactionId(reader, writer); final boolean result; try { result = map.remove(key, readValue); } catch (RuntimeException e) { return sendException(writer, sizeLocation, e); } writer.writeBoolean(result); return null; } private Work replaceWithOldAndNew(Bytes reader, Bytes writer) { final K key = readKey(reader); final V oldValue = readValue(reader); final V newValue = readValue(reader); boolean replaced; long sizeLocation = reflectTransactionId(reader, writer); try { replaced = map.replace(key, oldValue, newValue); } catch (RuntimeException e) { return sendException(writer, sizeLocation, e); } writer.writeBoolean(replaced); writeSizeAndFlags(sizeLocation, false, writer); return null; } private Work longSize(Bytes reader, Bytes writer) { final long sizeLocation = reflectTransactionId(reader, writer); try { writer.writeLong(map.longSize()); } catch (RuntimeException e) { return sendException(writer, sizeLocation, e); } writeSizeAndFlags(sizeLocation, false, writer); return null; } private Work size(Bytes reader, Bytes writer) { final long sizeLocation = reflectTransactionId(reader, writer); try { writer.writeInt(map.size()); } catch (RuntimeException e) { return sendException(writer, sizeLocation, e); } writeSizeAndFlags(sizeLocation, false, writer); return null; } private Work hashCode(ByteBufferBytes reader, Bytes writer) { final long sizeLocation = reflectTransactionId(reader, writer); try { writer.writeInt(map.hashCode()); } catch (RuntimeException e) { return sendException(writer, sizeLocation, e); } writeSizeAndFlags(sizeLocation, false, writer); return null; } private Work toString(Bytes reader, Bytes writer) { final long sizeLocation = reflectTransactionId(reader, writer); final String str; final long remaining = writer.remaining(); try { str = map.toString(); } catch (RuntimeException e) { return sendException(writer, sizeLocation, e); } assert remaining > 4; // this stops the toString overflowing the buffer final String result = (str.length() < remaining) ? str : str.substring(0, (int) (remaining - 4)) + "..."; writer.writeObject(result); writeSizeAndFlags(sizeLocation, false, writer); return null; } private Work sendException(Bytes writer, long sizeLocation, RuntimeException e) { writeException(e, writer); writeSizeAndFlags(sizeLocation, true, writer); return null; } private Work isEmpty(Bytes reader, Bytes writer) { final long sizeLocation = reflectTransactionId(reader, writer); try { writer.writeBoolean(map.isEmpty()); } catch (RuntimeException e) { return sendException(writer, sizeLocation, e); } writeSizeAndFlags(sizeLocation, false, writer); return null; } private Work containsKey(Bytes reader, Bytes writer) { final K k = readKey(reader); final long sizeLocation = reflectTransactionId(reader, writer); try { writer.writeBoolean(map.containsKey(k)); } catch (RuntimeException e) { return sendException(writer, sizeLocation, e); } writeSizeAndFlags(sizeLocation, false, writer); return null; } private Work containsValue(Bytes reader, Bytes writer) { final V v = readValue(reader); final long sizeLocation = reflectTransactionId(reader, writer); try { writer.writeBoolean(map.containsValue(v)); } catch (RuntimeException e) { return sendException(writer, sizeLocation, e); } writeSizeAndFlags(sizeLocation, false, writer); return null; } private Work get(Bytes reader, Bytes writer) { final K k = readKey(reader); final long sizeLocation = reflectTransactionId(reader, writer); try { writeValue(map.get(k), writer); } catch (RuntimeException e) { return sendException(writer, sizeLocation, e); } writeSizeAndFlags(sizeLocation, false, writer); return null; } private Work put(Bytes reader, Bytes writer) { final K k = readKey(reader); final V v = readValue(reader); long sizeLocation = reflectTransactionId(reader, writer); try { writeValue(map.put(k, v), writer); } catch (RuntimeException e) { return sendException(writer, sizeLocation, e); } writeSizeAndFlags(sizeLocation, false, writer); return null; } private Work remove(Bytes reader, Bytes writer) { final K key = readKey(reader); final long sizeLocation = reflectTransactionId(reader, writer); try { writeValue(map.remove(key), writer); } catch (RuntimeException e) { return sendException(writer, sizeLocation, e); } writeSizeAndFlags(sizeLocation, false, writer); return null; } private Work putAll(Bytes reader, Bytes writer) { final Map m = readEntries(reader); final long sizeLocation = reflectTransactionId(reader, writer); try { map.putAll(m); } catch (RuntimeException e) { return sendException(writer, sizeLocation, e); } writeSizeAndFlags(sizeLocation, false, writer); return null; } private Work equals(Bytes reader, Bytes writer) { final Map m = readEntries(reader); final long sizeLocation = reflectTransactionId(reader, writer); boolean result; try { result = map.equals(m); } catch (RuntimeException e) { return sendException(writer, sizeLocation, e); } writer.writeBoolean(result); writeSizeAndFlags(sizeLocation, false, writer); return null; } private Work clear(Bytes reader, Bytes writer) { final long sizeLocation = reflectTransactionId(reader, writer); try { map.clear(); } catch (RuntimeException e) { return sendException(writer, sizeLocation, e); } writeSizeAndFlags(sizeLocation, false, writer); return null; } private Work keySet(Bytes reader, final Bytes writer) { final long sizeLocation = reflectTransactionId(reader, writer); final Set ks; try { ks = map.keySet(); } catch (RuntimeException e) { return sendException(writer, sizeLocation, e); } writer.writeStopBit(ks.size()); final Iterator iterator = ks.iterator(); // this allows us to write more data than the buffer will allow return new Work() { @Override public boolean doWork(@NotNull final Bytes writer) { while (iterator.hasNext()) { // we've filled up the buffer, so lets give another channel a chance to send // some data, we don't know the max key size, we will use the entrySize instead if (writer.remaining() <= maxEntrySizeBytes) return false; writeKey(iterator.next(), writer); } writeSizeAndFlags(sizeLocation, false, writer); return true; } }; } private Work values(Bytes reader, Bytes writer) { final long sizeLocation = reflectTransactionId(reader, writer); final Collection values; try { values = map.values(); } catch (RuntimeException e) { return sendException(writer, sizeLocation, e); } writer.writeStopBit(values.size()); final Iterator iterator = values.iterator(); // this allows us to write more data than the buffer will allow return new Work() { @Override public boolean doWork(Bytes writer) { while (iterator.hasNext()) { // we've filled up the buffer, so lets give another channel a chance to send // some data, we don't know the max key size, we will use the entrySize instead if (writer.remaining() <= maxEntrySizeBytes) return false; writeValue(iterator.next(), writer); } writeSizeAndFlags(sizeLocation, false, writer); return true; } }; } private Work entrySet(Bytes reader, Bytes writer) { final long transactionId = reader.readLong(); final Set> entries; try { entries = map.entrySet(); } catch (RuntimeException e) { final long sizeLocation = reflectTransactionId(reader, writer); return sendException(writer, sizeLocation, e); } final Iterator> iterator = entries.iterator(); // this allows us to write more data than the buffer will allow return new Work() { @Override public boolean doWork(Bytes writer) { final long sizeLocation = writer.position(); writer.skip(AbstractChannelReplicator.SIZE_OF_SIZE + 1); // SIZE_OF_SIZE + is // exception writer.writeLong(transactionId); // hasAnotherChunk writer.skip(1); // count writer.skip(2); int count = 0; while (iterator.hasNext()) { // we've filled up the buffer, so lets give another channel a chance to send // some data, we don't know the max key size, we will use the entrySize instead if (writer.remaining() <= maxEntrySizeBytes) { writeHeader(writer, sizeLocation, count, true); return false; } count++; final Map.Entry next = iterator.next(); writeKey(next.getKey(), writer); writeValue(next.getValue(), writer); } writeHeader(writer, sizeLocation, count, false); return true; } private void writeHeader(Bytes writer, long sizeLocation, int count, final boolean hasAnotherChunk) { final long end = writer.position(); final int size = (int) (end - sizeLocation); writer.position(sizeLocation); // size in bytes writer.writeInt(size); // is exception writer.writeBoolean(false); //transaction id; writer.skip(8); writer.writeBoolean(hasAnotherChunk); // count writer.writeUnsignedShort(count); writer.position(end); } }; } private void writeMoreData(long location, boolean hasMoreData, Bytes out) { out.writeBoolean(location, hasMoreData); } private void entryCount(long location, int count, Bytes out) { } private Work putIfAbsent(Bytes reader, Bytes writer) { final K key = readKey(reader); final V v = readValue(reader); final long sizeLocation = reflectTransactionId(reader, writer); final V value; try { value = map.putIfAbsent(key, v); } catch (RuntimeException e) { return sendException(writer, sizeLocation, e); } writeValue(value, writer); writeSizeAndFlags(sizeLocation, false, writer); return null; } private Work replace(Bytes reader, Bytes writer) { final K k = readKey(reader); final V v = readValue(reader); final V replaced; long sizeLocation = reflectTransactionId(reader, writer); try { replaced = map.replace(k, v); } catch (RuntimeException e) { return sendException(writer, sizeLocation, e); } writeValue(replaced, writer); writeSizeAndFlags(sizeLocation, false, writer); return null; } /** * write the keysize and the key to the the {@code target} buffer * * @param key the key of the map */ private void writeKey(K key, Bytes writer) { keyValueSerializer.writeKey(key, writer); } private long reflectTransactionId(Bytes reader, Bytes writer) { final long sizeLocation = writer.position(); writer.skip(AbstractChannelReplicator.SIZE_OF_SIZE + 1); // isException + SIZE_OF_SIZE final long transactionId = reader.readLong(); writer.writeLong(transactionId); return sizeLocation; } private void writeValue(V value, final Bytes writer) { keyValueSerializer.writeValue(value, writer); } private K readKey(Bytes reader) { return keyValueSerializer.readKey(reader); } private V readValue(Bytes reader) { return keyValueSerializer.readValue(reader); } private void writeSizeAndFlags(long locationOfSize, boolean isException, Bytes out) { final long size = out.position() - locationOfSize; out.writeInt(0L, (int) size); out.writeBoolean(AbstractChannelReplicator.SIZE_OF_SIZE, isException); } private void writeException(RuntimeException e, Bytes out) { out.writeObject(e); } private Map readEntries(Bytes reader) { final long numberOfEntries = reader.readStopBit(); final HashMap result = new HashMap(); for (long i = 0; i < numberOfEntries; i++) { result.put(readKey(reader), readValue(reader)); } return result; } } class KeyValueSerializer { final Serializer valueSerializer; final Serializer keySerializer; KeyValueSerializer(SerializationBuilder keySerializer, SerializationBuilder valueSerializer) { this.keySerializer = new Serializer(keySerializer); this.valueSerializer = new Serializer(valueSerializer); } V readValue(Bytes reader) { if (nullCheck(reader)) return null; return valueSerializer.readMarshallable(reader); } private boolean nullCheck(Bytes in) { try { return in.readBoolean(); } catch (BufferUnderflowException e) { e.printStackTrace(); throw e; } } K readKey(Bytes reader) { if (nullCheck(reader)) return null; return keySerializer.readMarshallable(reader); } /** * write the keysize and the key to the the {@code target} buffer * * @param key the key of the map */ void writeKey(K key, Bytes writer) { writer.writeBoolean(key == null); if (key != null) keySerializer.writeMarshallable(key, writer); } void writeValue(V value, Bytes writer) { assert writer.limit() == writer.capacity(); writer.writeBoolean(value == null); assert writer.limit() == writer.capacity(); if (value != null) { assert writer.limit() == writer.capacity(); valueSerializer.writeMarshallable(value, writer); } } }