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

net.openhft.chronicle.map.TcpReplicator Maven / Gradle / Ivy

There is a newer version: 3.27ea0
Show newest version
/*
 *     Copyright (C) 2015  higherfrequencytrading.com
 *
 *     This program is free software: you can redistribute it and/or modify
 *     it under the terms of the GNU Lesser General Public License as published by
 *     the Free Software Foundation, either version 3 of the License.
 *
 *     This program is distributed in the hope that it will be useful,
 *     but WITHOUT ANY WARRANTY; without even the implied warranty of
 *     MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 *     GNU Lesser General Public License for more details.
 *
 *     You should have received a copy of the GNU Lesser General Public License
 *     along with this program.  If not, see .
 */

package net.openhft.chronicle.map;

import net.openhft.chronicle.hash.replication.ConnectionListener;
import net.openhft.chronicle.hash.replication.RemoteNodeValidator;
import net.openhft.chronicle.hash.replication.TcpTransportAndNetworkConfig;
import net.openhft.chronicle.hash.replication.ThrottlingConfig;
import net.openhft.chronicle.hash.serialization.BytesReader;
import net.openhft.lang.collection.ATSDirectBitSet;
import net.openhft.lang.collection.DirectBitSet;
import net.openhft.lang.io.AbstractBytes;
import net.openhft.lang.io.ByteBufferBytes;
import net.openhft.lang.io.Bytes;
import net.openhft.lang.io.DirectStore;
import net.openhft.lang.threadlocal.ThreadLocalCopies;
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 static java.nio.channels.SelectionKey.*;
import static java.util.concurrent.TimeUnit.MILLISECONDS;
import static net.openhft.chronicle.map.AbstractChannelReplicator.SIZE_OF_SIZE;
import static net.openhft.chronicle.map.AbstractChannelReplicator.SIZE_OF_TRANSACTION_ID;
import static net.openhft.chronicle.map.BuildVersion.version;
import static net.openhft.chronicle.map.StatelessChronicleMap.EventId.HEARTBEAT;
import static net.openhft.lang.MemoryUnit.*;

interface Work {

    /**
     * @param in the buffer that we will fill up
     * @return true when all the work is complete
     */
    boolean doWork(@NotNull Bytes in);
}

/**
 * Used with a {@link net.openhft.chronicle.map.ReplicatedChronicleMap} to send data between the
 * maps using a socket connection {@link net.openhft.chronicle.map.TcpReplicator}
 *
 * @author Rob Austin.
 */
final class TcpReplicator extends AbstractChannelReplicator implements Closeable {

    public static final long TIMESTAMP_FACTOR = 10000;
    public static final long SPIN_LOOP_TIME_IN_NONOSECONDS = TimeUnit.MICROSECONDS.toNanos(500);
    private static final int STATELESS_CLIENT = -127;
    private static final byte NOT_SET = (byte) HEARTBEAT.ordinal();
    private static final Logger LOG = LoggerFactory.getLogger(TcpReplicator.class.getName());
    private static final int BUFFER_SIZE = 0x100000; // 1MB
    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 ConnectionListener connectionListener;
    @NotNull
    private final Replica replica;
    private final byte localIdentifier;
    @NotNull
    private final Replica.EntryExternalizable externalizable;
    @NotNull
    private final TcpTransportAndNetworkConfig replicationConfig;
    private final
    @Nullable
    RemoteNodeValidator remoteNodeValidator;
    private final String name;
    StatelessClientParameters statelessClientParameters;
    private long largestEntrySoFar = 128;
    private long selectorTimeout;

    /**
     * @throws IOException on an io error.
     */
    public TcpReplicator(@NotNull final Replica replica,
                         @NotNull final Replica.EntryExternalizable externalizable,
                         @NotNull final TcpTransportAndNetworkConfig replicationConfig,
                         @Nullable final RemoteNodeValidator remoteNodeValidator,
                         @Nullable final StatelessClientParameters statelessClientParameters,
                         @Nullable final ConnectionListener connectionListener) throws IOException {

        super("TcpSocketReplicator-" + replica.identifier(), replicationConfig.throttlingConfig());

        this.statelessClientParameters = statelessClientParameters;

        final ThrottlingConfig throttlingConfig = replicationConfig.throttlingConfig();
        long throttleBucketInterval = throttlingConfig.bucketInterval(MILLISECONDS);

        heartBeatIntervalMillis = replicationConfig.heartBeatInterval(MILLISECONDS);

        selectorTimeout = Math.min(heartBeatIntervalMillis / 4, throttleBucketInterval);

        this.replica = replica;
        this.localIdentifier = replica.identifier();

        this.externalizable = externalizable;
        this.replicationConfig = replicationConfig;
        this.connectionListener = connectionListener;
        this.remoteNodeValidator = remoteNodeValidator;
        this.name = replicationConfig.name();
        start();
    }

    @Override
    void processEvent() {
        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 = select();

                // 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 (useJavaNIOSelectionKeys) {
                    // use the standard java nio selector

                    if (nSelectedKeys == 0)
                        continue;    // go back and check pendingRegistrations

                    final Set selectionKeys = selector.selectedKeys();
                    for (final SelectionKey key : selectionKeys) {
                        processKey(approxTime, key);
                    }
                    selectionKeys.clear();
                } else {
                    // use the netty like selector

                    final SelectionKey[] keys = selectedKeys.flip();

                    try {
                        for (int i = 0; i < keys.length && keys[i] != null; i++) {
                            final SelectionKey key = keys[i];

                            try {
                                processKey(approxTime, key);
                            } catch (BufferUnderflowException e) {
                                if (!isClosed)
                                    LOG.error("", e);
                            }
                        }
                    } finally {
                        for (int i = 0; i < keys.length && keys[i] != null; i++) {
                            keys[i] = null;
                        }
                    }
                }
            }
        } catch (CancelledKeyException | ConnectException | ClosedChannelException |
                ClosedSelectorException e) {
            if (LOG.isDebugEnabled())
                LOG.debug("", e);
        } catch (Exception e) {
            LOG.error("", e);
        } catch (Throwable e) {
            LOG.error("", e);
            throw e;
        } finally {

            if (LOG.isDebugEnabled())
                LOG.debug("closing name=" + name);
            if (!isClosed) {
                closeResources();
            }
        }
    }

    private void processKey(long approxTime, @NotNull SelectionKey key) {
        try {

            if (!key.isValid())
                return;

            if (key.isAcceptable()) {
                if (LOG.isDebugEnabled())
                    LOG.debug("onAccept - " + name);
                onAccept(key);
            }

            if (key.isConnectable()) {
                if (LOG.isDebugEnabled())
                    LOG.debug("onConnect - " + name);
                onConnect(key);
            }

            if (key.isReadable()) {
                if (LOG.isDebugEnabled())
                    LOG.debug("onRead - " + name);
                onRead(key, approxTime);
            }
            if (key.isWritable()) {
                if (LOG.isDebugEnabled())
                    LOG.debug("onWrite - " + name);
                onWrite(key, approxTime);
            }
        } catch (InterruptedException e) {
            quietClose(key, e);
        } catch (BufferUnderflowException | IOException |
                ClosedSelectorException | CancelledKeyException e) {
            if (!isClosed)
                quietClose(key, e);
        } catch (Exception e) {
            LOG.info("", e);
            if (!isClosed)
                closeEarlyAndQuietly(key.channel());
        }
    }

    /**
     * spin loops 100000 times first before calling the selector with timeout
     *
     * @return The number of keys, possibly zero, whose ready-operation sets were updated
     * @throws IOException
     */
    private int select() throws IOException {

        long start = System.nanoTime();

        while (System.nanoTime() < start + SPIN_LOOP_TIME_IN_NONOSECONDS) {
            final int keys = selector.selectNow();
            if (keys != 0)
                return keys;
        }

        return selector.select(selectorTimeout);
    }

    /**
     * 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 == null)
                    continue;

                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(@NotNull 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) {

        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);

        // todo OZAN :
        //  null check (Attached)key.attachment();
        //      connectionListener.onDiconnect( ((SocketChannel)key.channel()).socket().getInetAddress(),         ((Attached)key.attachment()).remoteIdentifier);


        closeEarlyAndQuietly(key.channel());
    }

    /**
     * called when the selector receives a OP_CONNECT message
     */
    private void onConnect(@NotNull final SelectionKey key)
            throws IOException {

        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.

            attached.connector.connectLater();

            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();
        attached.entryReader = new TcpSocketChannelEntryReader();
        attached.entryWriter = new TcpSocketChannelEntryWriter();

        attached.entryWriter.identifierToBuffer(localIdentifier);
        attached.isServer = true;

        channel.register(selector, OP_READ, attached);

        throttle(channel);
    }

    /**
     * check that the version number is valid text,
     *
     * @param versionNumber the version number to check
     * @return true, if the version number looks correct
     */
    boolean isValidVersionNumber(String versionNumber) {

        if (versionNumber.length()<=2)
            return false;

        for (char c : versionNumber.toCharArray()) {

            if (c >= '0' && c <= '9')
                continue;

            if (c == '.' || c == '-' || c == '_')
                continue;

            if (c >= 'A' && c <= 'Z')
                continue;

            if (c >= 'a' && c <= 'z')
                continue;

            return false;

        }

        return true;
    }

    private void checkVersions(final Attached attached) {

        final String localVersion = BuildVersion.version();
        final String remoteVersion = attached.serverVersion;


        if (!remoteVersion.equals(localVersion)) {
            byte remoteIdentifier = attached.remoteIdentifier;
            LOG.warn("DIFFERENT CHRONICLE-MAP VERSIONS : " +
                            "local-map=" + localVersion +
                            ", remote-map-id-" + remoteIdentifier + "=" +
                            remoteVersion +

                            ", The Remote Chronicle Map with " +
                            "identifier=" + remoteIdentifier +
                            " and this Chronicle Map are on different " +
                            "versions, we suggest that you use the same version."

            );
        }
    }

    /**
     * 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,
                               @NotNull SocketChannel socketChannel)
            throws IOException {
        final Attached attached = (Attached) key.attachment();
        final TcpSocketChannelEntryWriter writer = attached.entryWriter;
        final TcpSocketChannelEntryReader reader = attached.entryReader;

        socketChannel.register(selector, OP_READ | OP_WRITE, attached);

        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;


            // todo OZAN : add call to onConnectedListener.
            //connectionListener. onConnected(channel.socket().getInetAddress() ,  attached
            //         .remoteIdentifier, attached.isServer );

            // 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 SocketAddress remoteAddress = socketChannel.getRemoteAddress();

            if ((remoteNodeValidator != null &&
                    !remoteNodeValidator.validate(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);
            if (LOG.isDebugEnabled())
                LOG.debug("handshaking for localIdentifier=" + localIdentifier + "," +
                        "remoteIdentifier=" + remoteIdentifier);

            attached.remoteModificationIterator =
                    replica.acquireModificationIterator(remoteIdentifier);

            attached.remoteModificationIterator.setModificationNotifier(attached);

            long timeStampOfLastMessage = replica.lastModificationTime(remoteIdentifier);
            writer.writeRemoteBootstrapTimestamp(timeStampOfLastMessage);

            writer.writeServerVersion();

            // 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.serverVersion == null) {
            try {
                attached.serverVersion = reader.readRemoteServerVersion();
            } catch (IllegalStateException e1) {
                socketChannel.close();
                return;
            }

            if (attached.serverVersion == null)
                return;

            if (!isValidVersionNumber(attached.serverVersion)) {
                LOG.warn("Closing the remote connection : Please check that you don't have a third " +
                        "party system incorrectly " +
                        "connecting to ChronicleMap, remoteAddress=" + socketChannel.getRemoteAddress() +
                        ", so closing the remote connection as Chronicle can not make sense of the " +
                        "remote version number received from the external connection, " +
                        "version=" + attached.serverVersion + ", " +
                        "Chronicle is expecting the version number to only contain " +
                        "'.','-', ,A-Z,a-z,0-9");
                socketChannel.close();
                return;
            }

            checkVersions(attached);
        }

        if (!attached.hasRemoteHeartbeatInterval) {
            final long value = reader.readRemoteHeartbeatIntervalFromBuffer();

            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.remoteModificationIterator.dirtyEntries(attached.remoteBootstrapTimestamp);
            reader.entriesFromBuffer(attached, key);
            attached.handShakingComplete = true;
        }
    }

    /**
     * called when the selector receives a OP_WRITE message
     */
    private void onWrite(@NotNull final SelectionKey key,
                         final long approxTime) throws IOException, InterruptedException {
        final SocketChannel socketChannel = (SocketChannel) key.channel();
        final Attached attached = (Attached) key.attachment();
        if (attached == null) {
            LOG.info("Closing connection " + socketChannel + ", nothing attached");
            socketChannel.close();
            return;
        }

        TcpSocketChannelEntryWriter entryWriter = attached.entryWriter;

        if (entryWriter == null) throw
                new NullPointerException("No entryWriter");

        if (entryWriter.isWorkIncomplete()) {
            final boolean completed = entryWriter.doWork();

            if (completed)
                entryWriter.workCompleted();

        } else if (attached.remoteModificationIterator != null)
            entryWriter.entriesToBuffer(attached.remoteModificationIterator);

        try {
            final int len = entryWriter.writeBufferToSocket(socketChannel, approxTime);

            if (len == -1)
                socketChannel.close();

            if (len > 0)
                contemplateThrottleWrites(len);

            if (!entryWriter.hasBytesToWrite()
                    && !entryWriter.isWorkIncomplete()
                    && !hasNext(attached)
                    && attached.isHandShakingComplete()) {
                // 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
                // The OP_WRITE  will get added back in as soon as we have data to write

                if (LOG.isDebugEnabled())
                    LOG.debug("Disabling OP_WRITE to remoteIdentifier=" +
                            attached.remoteIdentifier +
                            ", localIdentifier=" + localIdentifier);
                key.interestOps(key.interestOps() & ~OP_WRITE);
            }
        } catch (IOException e) {
            quietClose(key, e);
            if (!attached.isServer)
                attached.connector.connectLater();
            throw e;
        }
    }

    private boolean hasNext(Attached attached) {
        return attached.remoteModificationIterator != null
                && attached.remoteModificationIterator.hasNext();
    }

    /**
     * called when the selector receives a OP_READ message
     */

    private void onRead(@NotNull final SelectionKey key,
                        final long approxTime) throws IOException {

        final SocketChannel socketChannel = (SocketChannel) key.channel();
        final Attached attached = (Attached) key.attachment();

        if (attached == null) {
            LOG.info("Closing connection " + socketChannel + ", nothing attached");
            socketChannel.close();
            return;
        }

        try {

            int len = attached.entryReader.readSocketToBuffer(socketChannel);
            if (len == -1) {
                socketChannel.register(selector, 0);
                if (replicationConfig.autoReconnectedUponDroppedConnection()) {
                    AbstractConnector connector = attached.connector;
                    if (connector != null)
                        connector.connectLater();
                } else
                    socketChannel.close();
                return;
            }

            if (len == 0)
                return;

            if (attached.entryWriter.isWorkIncomplete())
                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, key);
        } else {
            doHandShaking(key, socketChannel);
        }
    }

    @Nullable
    private ServerSocketChannel openServerSocketChannel() throws IOException {
        ServerSocketChannel result = null;

        try {
            result = ServerSocketChannel.open();
        } finally {
            if (result != null)
                closeables.add(result);
        }
        return result;
    }

    static class StatelessClientParameters {
        VanillaChronicleMap map;
        SerializationBuilder keySerializationBuilder;
        SerializationBuilder valueSerializationBuilder;

        public StatelessClientParameters(VanillaChronicleMap map,
                                         SerializationBuilder keySerializationBuilder,
                                         SerializationBuilder valueSerializationBuilder) {
            this.map = map;
            this.keySerializationBuilder = keySerializationBuilder;
            this.valueSerializationBuilder = valueSerializationBuilder;
        }
    }

    /**
     * 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 {

        @NotNull
        private final DirectBitSet changeOfOpWriteRequired;
        @NotNull
        private final SelectionKey[] selectionKeys;
        private final int op;

        KeyInterestUpdater(int op, @NotNull final SelectionKey[] selectionKeys) {
            this.op = op;
            this.selectionKeys = selectionKeys;
            long bitSetSize = LONGS.align(BYTES.alignAndConvert(selectionKeys.length, BITS), BYTES);
            changeOfOpWriteRequired = new ATSDirectBitSet(DirectStore.allocate(bitSetSize).bytes());
        }

        public void applyUpdates() {
            for (long i = changeOfOpWriteRequired.clearNextSetBit(0L); i >= 0;
                 i = changeOfOpWriteRequired.clearNextSetBit(i + 1)) {
                final SelectionKey key = selectionKeys[((int) 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.setIfClear(keyIndex);
        }
    }

    private class ServerConnector extends AbstractConnector {

        @NotNull
        private final Details details;

        private ServerConnector(@NotNull Details details) {
            super("TCP-ServerConnector-" + localIdentifier);
            this.details = details;
        }

        @NotNull
        @Override
        public String toString() {
            return "ServerConnector{" +
                    "" + details +
                    '}';
        }

        @Nullable
        SelectableChannel doConnect() throws
                IOException, InterruptedException {

            final ServerSocketChannel serverChannel = openServerSocketChannel();

            if (serverChannel == null)
                return null;

            serverChannel.socket().setReceiveBufferSize(BUFFER_SIZE);
            serverChannel.configureBlocking(false);
            serverChannel.register(TcpReplicator.this.selector, 0);
            ServerSocket serverSocket = null;

            try {
                serverSocket = serverChannel.socket();
            } finally {
                if (serverSocket != null)
                    closeables.add(serverSocket);
            }

            if (serverSocket == null)
                return null;

            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.debug("", e);
                    }
                }
            });

            selector.wakeup();

            return serverChannel;
        }
    }

    private class ClientConnector extends AbstractConnector {

        @NotNull
        private final Details details;

        private ClientConnector(@NotNull Details details) {
            super("TCP-ClientConnector-" + details.localIdentifier());
            this.details = details;
        }

        @NotNull
        @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.connect(details.address());

                // 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);
                    }
                    this.connectLater();
                }
            }
        }
    }

    /**
     * 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;

        @Nullable
        public Replica.ModificationIterator remoteModificationIterator;
        // used for connect later
        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;
        public boolean handShakingComplete;
        public String serverVersion;
        public long remoteHeartbeatInterval = heartBeatIntervalMillis;

        boolean isHandShakingComplete() {
            return handShakingComplete;
        }

        /**
         * 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.
     */
    class TcpSocketChannelEntryWriter {


        @NotNull
        private final EntryCallback entryCallback;
        // if uncompletedWork is set ( not null ) , this must be completed before any further work
        // is  carried out
        @Nullable
        public Work uncompletedWork;
        StatelessServerConnector statelessServer;
        private long lastSentTime;

        private TcpSocketChannelEntryWriter() {
            entryCallback = new EntryCallback(externalizable, replicationConfig.tcpBufferSize());

            if (statelessClientParameters != null)
                statelessServer = new StatelessServerConnector(
                        statelessClientParameters.map, entryCallback,
                        replicationConfig.tcpBufferSize(),
                        statelessClientParameters.keySerializationBuilder,
                        statelessClientParameters.valueSerializationBuilder);
        }

        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);
        }

        void ensureBufferSize(long size) {
            if (in().remaining() < size) {
                size += entryCallback.in().position();
                if (size > Integer.MAX_VALUE)
                    throw new UnsupportedOperationException();
                entryCallback.resizeBuffer((int) size);
            }
        }

        void resizeToMessage(@NotNull IllegalStateException e) {

            String message = e.getMessage();
            if (message.startsWith("java.io.IOException: Not enough available space for writing ")) {
                String substring = message.substring("java.io.IOException: Not enough available space for writing ".length(), message.length());
                int i = substring.indexOf(' ');
                if (i != -1) {
                    int size = Integer.parseInt(substring.substring(0, i));

                    long requiresExtra = size - in().remaining();
                    ensureBufferSize((int) (in().capacity() + requiresExtra));
                } else
                    throw e;
            } else
                throw e;
        }

        Bytes in() {
            return entryCallback.in();
        }

        private ByteBuffer out() {
            return entryCallback.out();
        }

        /**
         * 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);
        }

        void writeServerVersion() {
            in().write(String.format("%1$" + 64 + "s", version()).toCharArray());
        }

        /**
         * 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
         *
         */
        void entriesToBuffer(@NotNull final Replica.ModificationIterator modificationIterator) throws InterruptedException {

            int entriesWritten = 0;
            try {
                for (; ; entriesWritten++) {

                    long start = in().position();

                    boolean success = modificationIterator.nextEntry(entryCallback, 0);

                    // if not success this is most likely due to the next entry not fitting into
                    // the buffer and the buffer can not be re-sized above Integer.max_value, in
                    // this case success will return false, so we return so that we can send to
                    // the socket what we have.
                    if (!success)
                        return;

                    long entrySize = in().position() - start;

                    if (entrySize > largestEntrySoFar)
                        largestEntrySoFar = entrySize;

                    // we've filled up the buffer lets give another channel a chance to send
                    // some data
                    if (in().remaining() <= largestEntrySoFar || in().position() >
                            replicationConfig.tcpBufferSize())
                        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
                }
            } finally {
                if (LOG.isDebugEnabled())
                    LOG.debug("Entries written: {}", entriesWritten);
            }
        }

        /**
         * 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 {

            final Bytes in = in();
            final ByteBuffer out = out();

            if (in.position() == 0)
                return 0;

            // if we still have some unwritten writer from last time
            lastSentTime = approxTime;
            assert in.position() <= Integer.MAX_VALUE;
            int size = (int) in.position();

            out.limit(size);

            final int len = socketChannel.write(out);

            if (LOG.isDebugEnabled())
                LOG.debug("bytes-written=" + len);

            if (len == size) {
                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);
        }


        public boolean doWork() {
            return uncompletedWork != null && uncompletedWork.doWork(in());
        }

        public boolean hasBytesToWrite() {
            return in().position() > 0;
        }
    }

    /**
     * Reads map entries from a socket, this could be a client or server socket
     */
    class TcpSocketChannelEntryReader {
        public static final int HEADROOM = 1024;
        public static final int SIZE_OF_BOOTSTRAP_TIMESTAMP = 8;
        public long lastHeartBeatReceived = System.currentTimeMillis();
        ByteBuffer in;
        ByteBufferBytes out;
        private long sizeInBytes;
        private byte state;

        private TcpSocketChannelEntryReader() {
            in = ByteBuffer.allocateDirect(replicationConfig.tcpBufferSize());
            out = new ByteBufferBytes(in.slice());
            out.limit(0);
            in.clear();
        }

        void resizeBuffer(long size) {
            assert size < Integer.MAX_VALUE;

            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());

            // TODO why copy byte by byte?!
            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);
        }

        /**
         * 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
         * @param key
         * @throws InterruptedException
         */
        void entriesFromBuffer(@NotNull Attached attached, @NotNull SelectionKey key) {
            int entriesRead = 0;
            try {
                for (; ; entriesRead++) {
                    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();

                        assert sizeInBytes >= 0;

                        // 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 + SIZE_OF_BOOTSTRAP_TIMESTAMP;
                        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;
                    assert nextEntryPos > 0;
                    final long limit = out.limit();
                    out.limit(nextEntryPos);

                    boolean isStatelessClient = (state != 1);

                    if (isStatelessClient) {

                        final StatelessServerConnector statelessServerConnector = attached
                                .entryWriter.statelessServer;

                        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, attached.entryReader.out);

                            // turn the OP_WRITE on
                            key.interestOps(key.interestOps() | OP_WRITE);

                            // 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) {
                                try {  // we will complete what we can for now
                                    boolean isComplete = futureWork.doWork(attached.entryWriter
                                            .in());
                                    if (!isComplete)
                                        attached.entryWriter.uncompletedWork = futureWork;
                                } catch (Exception e) {
                                    LOG.error("", e);
                                }
                            }
                        }
                    } else {
                        externalizable.readExternalEntry(copies, segmentState, out);

                    }

                    out.limit(limit);

                    // skip onto the next entry
                    out.position(nextEntryPos);

                    state = NOT_SET;
                    sizeInBytes = 0;
                }
            } finally {
                if (LOG.isDebugEnabled())
                    LOG.debug("Entries read: {}", entriesRead);
            }
        }

        /**
         * 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() > largestEntrySoFar)
                return;

            in.limit(in.position());
            assert out.position() < Integer.MAX_VALUE;
            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 the timestamp or -1 if unsuccessful
         */
        String readRemoteServerVersion() {
            if (out.remaining() >= 64) {
                char[] chars = new char[64];
                out.readFully(chars, 0, chars.length);
                return new String(chars).trim();
            } else
                return null;
        }


        public long readRemoteHeartbeatIntervalFromBuffer() {
            return (out.remaining() >= 8) ? out.readLong() : Long.MIN_VALUE;
        }
    }
}

/**
 * @author Rob Austin.
 */
class StatelessServerConnector {

    public static final StatelessChronicleMap.EventId[] VALUES
            = StatelessChronicleMap.EventId.values();
    public static final int SIZE_OF_IS_EXCEPTION = 1;
    public static final int HEADER_SIZE = SIZE_OF_SIZE + SIZE_OF_IS_EXCEPTION +
            SIZE_OF_TRANSACTION_ID;
    private static final Logger LOG = LoggerFactory.getLogger(StatelessServerConnector.class
            .getName());
    @NotNull
    private final ReaderWithSize keyReaderWithSize;

    @NotNull
    private final WriterWithSize keyWriterWithSize;

    @NotNull
    private final ReaderWithSize valueReaderWithSize;

    @NotNull
    private final WriterWithSize valueWriterWithSize;

    @NotNull
    private final VanillaChronicleMap map;
    private final SerializationBuilder keySerializationBuilder;
    private final SerializationBuilder valueSerializationBuilder;
    private final int tcpBufferSize;


    StatelessServerConnector(
            @NotNull VanillaChronicleMap map,
            @NotNull final BufferResizer bufferResizer, int tcpBufferSize,
            final SerializationBuilder keySerializationBuilder,
            final SerializationBuilder valueSerializationBuilder) {
        this.tcpBufferSize = tcpBufferSize;

        this.keySerializationBuilder = keySerializationBuilder;
        this.valueSerializationBuilder = valueSerializationBuilder;
        keyReaderWithSize = new ReaderWithSize<>(keySerializationBuilder);
        keyWriterWithSize = new WriterWithSize<>(keySerializationBuilder, bufferResizer);
        valueReaderWithSize = new ReaderWithSize<>(valueSerializationBuilder);
        valueWriterWithSize = new WriterWithSize<>(valueSerializationBuilder, bufferResizer);
        this.map = map;
    }

    @Nullable
    Work processStatelessEvent(final byte eventId,
                               @NotNull final TcpReplicator.TcpSocketChannelEntryWriter writer,
                               @NotNull final ByteBufferBytes reader) {
        final StatelessChronicleMap.EventId event = VALUES[eventId];

        long transactionId = reader.readLong();

        // the time stamp and the transaction are usually the same, or out by the shift
        long timestamp = transactionId / TcpReplicator.TIMESTAMP_FACTOR;
        byte identifier = reader.readByte();
        int headerSize = reader.readInt();
        reader.skip(headerSize);


        // these methods don't return a result to the client or don't return a result to the
        // client immediately
        switch (event) {
            case KEY_SET:
                return keySet(reader, writer, transactionId);

            case VALUES:
                return values(reader, writer, transactionId);

            case ENTRY_SET:
                return entrySet(reader, writer, transactionId);

            case PUT_WITHOUT_ACC:
                return put(reader, timestamp, identifier);

            case PUT_ALL_WITHOUT_ACC:
                return putAll(reader, timestamp, identifier);

            case REMOVE_WITHOUT_ACC:
                return remove(reader, timestamp, identifier);
        }

        final long sizeLocation = reflectTransactionId(writer.in(), transactionId);


        // these methods return a result

        switch (event) {
            case LONG_SIZE:
                return longSize(writer, sizeLocation);

            case IS_EMPTY:
                return isEmpty(writer, sizeLocation);

            case CONTAINS_KEY:
                return containsKey(reader, writer, sizeLocation);

            case CONTAINS_VALUE:
                return containsValue(reader, writer, sizeLocation);

            case GET:
                return get(reader, writer, sizeLocation, timestamp);

            case PUT:
                return put(reader, writer, sizeLocation, timestamp, identifier);

            case REMOVE:
                return remove(reader, writer, sizeLocation, timestamp, identifier);

            case CLEAR:
                return clear(writer, sizeLocation, timestamp, identifier);

            case REPLACE:
                return replace(reader, writer, sizeLocation, timestamp, identifier);

            case REPLACE_WITH_OLD_AND_NEW_VALUE:
                return replaceWithOldAndNew(reader, writer,
                        sizeLocation, timestamp, identifier);

            case PUT_IF_ABSENT:
                return putIfAbsent(reader, writer, sizeLocation, timestamp, identifier);

            case REMOVE_WITH_VALUE:
                return removeWithValue(reader, writer, sizeLocation, timestamp, identifier);

            case TO_STRING:
                return toString(writer, sizeLocation);

            case APPLICATION_VERSION:
                return applicationVersion(writer, sizeLocation);

            case PERSISTED_DATA_VERSION:
                return persistedDataVersion(writer, sizeLocation);

            case PUT_ALL:
                return putAll(reader, writer, sizeLocation, timestamp, identifier);

            case HASH_CODE:
                return hashCode(writer, sizeLocation);

            case MAP_FOR_KEY:
                return mapForKey(reader, writer, sizeLocation);

            case PUT_MAPPED:
                return putMapped(reader, writer, sizeLocation);

            case KEY_BUILDER:
                return writeBuilder(writer, sizeLocation, keySerializationBuilder);

            case VALUE_BUILDER:
                return writeBuilder(writer, sizeLocation, valueSerializationBuilder);

            default:
                throw new IllegalStateException("unsupported event=" + event);
        }
    }

    private void writeObject(TcpReplicator.TcpSocketChannelEntryWriter writer, Object o) {
        for (; ; ) {
            long position = writer.in().position();

            try {
                writer.in().writeObject(o);
                return;
            } catch (IllegalStateException e) {
                if (e.getMessage().contains("Not enough available space")) {
                    writer.resizeToMessage(e);
                    writer.in().position(position);
                } else
                    throw e;
            }
        }
    }

    private Work writeBuilder(TcpReplicator.TcpSocketChannelEntryWriter writer, long sizeLocation, SerializationBuilder builder) {

        try {
            writeObject(writer, builder);
        } catch (Exception e) {
            LOG.info("", e);

            return sendException(writer, sizeLocation, e);
        }

        writeSizeAndFlags(sizeLocation, false, writer.in());
        return null;
    }


    @Nullable
    public Work mapForKey(@NotNull ByteBufferBytes reader, @NotNull TcpReplicator.TcpSocketChannelEntryWriter writer,
                          long sizeLocation) {
        final K key = keyReaderWithSize.read(reader, null, null);
        final Function function = (Function) reader.readObject();
        try {
            Object result = map.getMapped(key, function);
            writeObject(writer, result);
        } catch (Throwable e) {
            LOG.info("", e);
            return sendException(writer, sizeLocation, e);
        }

        writeSizeAndFlags(sizeLocation, false, writer.in());
        return null;
    }

    @Nullable
    public Work putMapped(@NotNull ByteBufferBytes reader, @NotNull TcpReplicator.TcpSocketChannelEntryWriter writer,
                          long sizeLocation) {
        final K key = keyReaderWithSize.read(reader, null, null);
        final UnaryOperator unaryOperator = (UnaryOperator) reader.readObject();
        try {
            Object result = map.putMapped(key, unaryOperator);
            writeObject(writer, result);
        } catch (Throwable e) {
            LOG.info("", e);
            return sendException(writer, sizeLocation, e);
        }

        writeSizeAndFlags(sizeLocation, false, writer.in());
        return null;
    }

    @Nullable
    private Work removeWithValue(Bytes reader, @NotNull TcpReplicator.TcpSocketChannelEntryWriter writer, final long sizeLocation,
                                 long timestamp, byte id) {
        try {
            writer.in().writeBoolean(map.removeBytesEntry(reader));
        } catch (Throwable e) {
            return sendException(writer, sizeLocation, e);
        }
        writeSizeAndFlags(sizeLocation, false, writer.in());
        return null;
    }

    @Nullable
    private Work replaceWithOldAndNew(Bytes reader, @NotNull TcpReplicator.TcpSocketChannelEntryWriter writer, final long
            sizeLocation, long timestamp, byte id) {
        try {
            map.replaceWithOldAndNew(reader, writer.in());
        } catch (Throwable e) {
            return sendException(writer, sizeLocation, e);
        }
        writeSizeAndFlags(sizeLocation, false, writer.in());
        return null;
    }

    @Nullable
    private Work longSize(@NotNull TcpReplicator.TcpSocketChannelEntryWriter writer, final long sizeLocation) {
        try {
            writer.in().writeLong(map.longSize());
        } catch (Throwable e) {
            return sendException(writer, sizeLocation, e);
        }
        writeSizeAndFlags(sizeLocation, false, writer.in());
        return null;
    }

    @Nullable
    private Work hashCode(@NotNull TcpReplicator.TcpSocketChannelEntryWriter writer, final long sizeLocation) {
        try {
            writer.in().writeInt(map.hashCode());
        } catch (Throwable e) {
            return sendException(writer, sizeLocation, e);
        }

        writeSizeAndFlags(sizeLocation, false, writer.in());
        return null;
    }

    @Nullable
    private Work toString(@NotNull TcpReplicator.TcpSocketChannelEntryWriter writer, final long sizeLocation) {
        final String str;

        final long remaining = writer.in().remaining();
        try {
            str = map.toString();
        } catch (Throwable 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)) + "...";

        writeObject(writer, result);
        writeSizeAndFlags(sizeLocation, false, writer.in());
        return null;
    }


    @Nullable
    private Work applicationVersion(@NotNull TcpReplicator.TcpSocketChannelEntryWriter writer, final long sizeLocation) {


        final long remaining = writer.in().remaining();
        try {
            String result = map.applicationVersion();
            writeObject(writer, result);
        } catch (Throwable e) {
            return sendException(writer, sizeLocation, e);
        }

        assert remaining > 4;

        writeSizeAndFlags(sizeLocation, false, writer.in());
        return null;
    }


    @Nullable
    private Work persistedDataVersion(@NotNull TcpReplicator.TcpSocketChannelEntryWriter writer, final long sizeLocation) {


        final long remaining = writer.in().remaining();
        try {
            String result = map.persistedDataVersion();
            writeObject(writer, result);
        } catch (Throwable e) {
            return sendException(writer, sizeLocation, e);
        }

        assert remaining > 4;

        writeSizeAndFlags(sizeLocation, false, writer.in());
        return null;
    }

    @SuppressWarnings("SameReturnValue")
    @Nullable
    private Work sendException(@NotNull TcpReplicator.TcpSocketChannelEntryWriter writer,
                               long sizeLocation, @NotNull Throwable e) {
        // move the position to ignore any bytes written so far
        writer.in().position(sizeLocation + HEADER_SIZE);

        writeException(writer, e);

        writeSizeAndFlags(sizeLocation , true, writer.in());
        return null;
    }

    @Nullable
    private Work isEmpty(@NotNull TcpReplicator.TcpSocketChannelEntryWriter writer,
                         final long sizeLocation) {
        try {
            writer.in().writeBoolean(map.isEmpty());
        } catch (Throwable e) {
            return sendException(writer, sizeLocation, e);
        }

        writeSizeAndFlags(sizeLocation, false, writer.in());
        return null;
    }

    @Nullable
    private Work containsKey(Bytes reader, @NotNull TcpReplicator.TcpSocketChannelEntryWriter writer, final long sizeLocation) {
        try {
            writer.in().writeBoolean(map.containsBytesKey(reader));
        } catch (Throwable e) {
            return sendException(writer, sizeLocation, e);
        }
        writeSizeAndFlags(sizeLocation, false, writer.in());
        return null;
    }

    @Nullable
    private Work containsValue(Bytes reader, @NotNull TcpReplicator.TcpSocketChannelEntryWriter writer, final long sizeLocation) {
        // todo optimize -- eliminate
        final V v = valueReaderWithSize.read(reader, null, null);

        try {
            writer.in().writeBoolean(map.containsValue(v));
        } catch (Throwable e) {
            return sendException(writer, sizeLocation, e);
        }
        writeSizeAndFlags(sizeLocation, false, writer.in());
        return null;
    }

    @Nullable
    private Work get(Bytes reader,
                     TcpReplicator.TcpSocketChannelEntryWriter writer,
                     final long sizeLocation, long transactionId) {
        try {
            map.getBytes(reader, writer);
        } catch (Throwable e) {
            return sendException(writer, sizeLocation, e);
        }
        writeSizeAndFlags(sizeLocation, false, writer.in());
        return null;
    }

    @SuppressWarnings("SameReturnValue")
    @Nullable
    private Work put(Bytes reader, long timestamp, byte id) {
        map.put(reader);
        return null;
    }

    @Nullable
    private Work put(Bytes reader, TcpReplicator.TcpSocketChannelEntryWriter writer,
                     final long sizeLocation, long timestamp, byte id) {
        try {
            map.put(reader, writer);
        } catch (Throwable e) {
            return sendException(writer, sizeLocation, e);
        }
        writeSizeAndFlags(sizeLocation, false, writer.in());
        return null;
    }

    @SuppressWarnings("SameReturnValue")
    @Nullable
    private Work remove(Bytes reader, long timestamp, byte id) {
        map.removeBytesKeyWithoutOutput(reader);
        return null;
    }

    @Nullable
    private Work remove(Bytes reader, TcpReplicator.TcpSocketChannelEntryWriter writer,
                        final long sizeLocation, long timestamp, byte id) {
        try {
            map.removeBytesKeyOutputPrevValue(reader, writer);
        } catch (Throwable e) {
            return sendException(writer, sizeLocation, e);
        }
        writeSizeAndFlags(sizeLocation, false, writer.in());
        return null;
    }

    @Nullable
    private Work putAll(@NotNull Bytes reader,
                        @NotNull TcpReplicator.TcpSocketChannelEntryWriter writer,
                        final long sizeLocation,
                        long timestamp, byte id) {
        try {
            map.putAll(reader);
        } catch (Throwable e) {
            return sendException(writer, sizeLocation, e);
        }
        writeSizeAndFlags(sizeLocation, false, writer.in());
        return null;
    }

    @SuppressWarnings("SameReturnValue")
    @Nullable
    private Work putAll(@NotNull Bytes reader, long timestamp, byte id) {
        map.putAll(reader);
        return null;
    }

    @Nullable
    private Work clear(@NotNull TcpReplicator.TcpSocketChannelEntryWriter writer, final long sizeLocation, long timestamp, byte id) {
        try {
            map.clear();
        } catch (Throwable e) {
            return sendException(writer, sizeLocation, e);
        }

        writeSizeAndFlags(sizeLocation, false, writer.in());
        return null;
    }

    @Nullable
    private Work values(@NotNull Bytes reader, @NotNull TcpReplicator.TcpSocketChannelEntryWriter writer, final long transactionId) {

        Collection values;

        try {
            values = map.values();
        } catch (Throwable e) {
            return sendException(reader, writer, e);
        }

        final Iterator iterator = values.iterator();

        // this allows us to write more data than the buffer will allow
        return new Work() {
            @Override
            public boolean doWork(@NotNull Bytes out) {
                final long sizeLocation = header(out, transactionId);

                ThreadLocalCopies copies = valueWriterWithSize.getCopies(null);
                Object valueWriter = valueWriterWithSize.writerForLoop(copies);

                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 (out.position() > tcpBufferSize) {
                        writeHeader(out, sizeLocation, count, true);
                        return false;
                    }

                    count++;

                    valueWriterWithSize.writeInLoop(out, iterator.next(), valueWriter, copies);
                }

                writeHeader(out, sizeLocation, count, false);
                return true;
            }
        };
    }

    @Nullable
    private Work keySet(@NotNull Bytes reader, @NotNull final TcpReplicator.TcpSocketChannelEntryWriter writer,
                        final long transactionId) {

        Set ks;

        try {
            ks = map.keySet();
        } catch (Throwable e) {
            return sendException(reader, writer, e);
        }

        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 Bytes out) {
                final long sizeLocation = header(out, transactionId);

                ThreadLocalCopies copies = keyWriterWithSize.getCopies(null);
                Object keyWriter = keyWriterWithSize.writerForLoop(copies);

                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 (out.position() > tcpBufferSize) {
                        writeHeader(out, sizeLocation, count, true);
                        return false;
                    }

                    count++;
                    K key = iterator.next();
                    keyWriterWithSize.writeInLoop(out, key, keyWriter, copies);
                }

                writeHeader(out, sizeLocation, count, false);
                return true;
            }
        };
    }

    @Nullable
    private Work entrySet(@NotNull final Bytes reader,
                          @NotNull TcpReplicator.TcpSocketChannelEntryWriter writer,
                          final long transactionId) {

        final Set> entries;

        try {
            entries = map.entrySet();
        } catch (Throwable e) {
            return sendException(reader, writer, 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(@NotNull Bytes out) {
                if (out.position() > tcpBufferSize)
                    return false;

                final long sizeLocation = header(out, transactionId);

                ThreadLocalCopies copies = keyWriterWithSize.getCopies(null);
                Object keyWriter = keyWriterWithSize.writerForLoop(copies);
                copies = valueWriterWithSize.getCopies(copies);
                Object valueWriter = valueWriterWithSize.writerForLoop(copies);

                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 (out.position() > tcpBufferSize) {
                        writeHeader(out, sizeLocation, count, true);
                        return false;
                    }

                    count++;
                    final Map.Entry next = iterator.next();
                    keyWriterWithSize.writeInLoop(out, next.getKey(), keyWriter, copies);
                    valueWriterWithSize.writeInLoop(out, next.getValue(), valueWriter, copies);
                }

                writeHeader(out, sizeLocation, count, false);
                return true;
            }
        };
    }

    @Nullable
    private Work putIfAbsent(Bytes reader, TcpReplicator.TcpSocketChannelEntryWriter writer,
                             final long sizeLocation, long timestamp, byte id) {
        try {
            map.putIfAbsent(reader, writer);
        } catch (Throwable e) {
            return sendException(writer, sizeLocation, e);
        }
        writeSizeAndFlags(sizeLocation, false, writer.in());
        return null;
    }

    @Nullable
    private Work replace(Bytes reader, TcpReplicator.TcpSocketChannelEntryWriter writer,
                         final long sizeLocation, long timestamp, byte id) {
        try {
            map.replaceKV(reader, writer);
        } catch (Throwable e) {
            return sendException(writer, sizeLocation, e);
        }
        writeSizeAndFlags(sizeLocation, false, writer.in());
        return null;
    }

    private long reflectTransactionId(@NotNull Bytes writer, final long transactionId) {
        final long sizeLocation = writer.position();
        writer.skip(SIZE_OF_SIZE);
        assert transactionId != 0;
        writer.writeLong(transactionId);
        writer.writeBoolean(false); // isException
        return sizeLocation;
    }

    private void writeSizeAndFlags(long locationOfSize, boolean isException, @NotNull Bytes out) {
        final long size = out.position() - locationOfSize;

        out.writeInt(locationOfSize, (int) size); // size

        // write isException
        out.writeBoolean(locationOfSize + SIZE_OF_SIZE + SIZE_OF_TRANSACTION_ID, isException);

        long pos = out.position();
        long limit = out.limit();

        //   System.out.println("Sending with size=" + size);

        if (LOG.isDebugEnabled()) {
            out.position(locationOfSize);
            out.limit(pos);
            LOG.info("Sending to the stateless client, bytes=" + AbstractBytes.toHex(out) + "," +
                    "len=" + out.remaining());
            out.limit(limit);
            out.position(pos);
        }


    }

    private void writeException(@NotNull TcpReplicator.TcpSocketChannelEntryWriter out, Throwable e) {
        writeObject(out, e);
    }

    @NotNull
    private Map readEntries(@NotNull Bytes reader) {
        final long numberOfEntries = reader.readStopBit();
        final Map result = new HashMap();

        ThreadLocalCopies copies = keyReaderWithSize.getCopies(null);
        BytesReader keyReader = keyReaderWithSize.readerForLoop(copies);
        copies = valueReaderWithSize.getCopies(copies);
        BytesReader valueReader = valueReaderWithSize.readerForLoop(copies);

        for (long i = 0; i < numberOfEntries; i++) {
            K key = keyReaderWithSize.readInLoop(reader, keyReader);
            V value = valueReaderWithSize.readInLoop(reader, valueReader);
            result.put(key, value);
        }
        return result;
    }

    @Nullable
    private Work sendException(@NotNull Bytes reader, @NotNull TcpReplicator.TcpSocketChannelEntryWriter writer, @NotNull Throwable e) {
        final long sizeLocation = reflectTransactionId(writer.in(), reader.readLong());
        return sendException(writer, sizeLocation, e);
    }

    private long header(@NotNull Bytes writer, final long transactionId) {
        final long sizeLocation = writer.position();

        writer.skip(SIZE_OF_SIZE);
        writer.writeLong(transactionId);

        // exception
        writer.skip(1);

        //  hasAnotherChunk
        writer.skip(1);

        // count
        writer.skip(4);
        return sizeLocation;
    }

    private void writeHeader(@NotNull 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);

        //transaction id;
        writer.skip(8);

        // is exception
        writer.writeBoolean(false);

        writer.writeBoolean(hasAnotherChunk);

        // count
        writer.writeInt(count);
        writer.position(end);
    }

}





© 2015 - 2025 Weber Informatics LLC | Privacy Policy