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

net.openhft.chronicle.map.AbstractChannelReplicator 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.ThrottlingConfig;
import net.openhft.lang.io.ByteBufferBytes;
import net.openhft.lang.io.Bytes;
import net.openhft.lang.thread.NamedThreadFactory;
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.lang.reflect.Field;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.ByteOrder;
import java.nio.channels.*;
import java.security.AccessController;
import java.security.PrivilegedAction;
import java.util.Queue;
import java.util.Set;
import java.util.concurrent.*;

import static java.lang.Math.round;
import static java.nio.channels.SelectionKey.OP_WRITE;
import static java.util.concurrent.TimeUnit.DAYS;
import static java.util.concurrent.TimeUnit.MILLISECONDS;

/**
 * @author Rob Austin.
 */
abstract class AbstractChannelReplicator implements Closeable {

    public static final int BITS_IN_A_BYTE = 8;

    public static final int SIZE_OF_SIZE = 4;
    public static final int SIZE_OF_TRANSACTION_ID = 8;

    private static final Logger LOG = LoggerFactory.getLogger(AbstractChannelReplicator.class);

    // currently this is not safe to use as it wont work with the stateless client
    static boolean useJavaNIOSelectionKeys = Boolean.valueOf(System.getProperty("useJavaNIOSelectionKeys"));

    final SelectedSelectionKeySet selectedKeys = new SelectedSelectionKeySet();

    final CloseablesManager closeables = new CloseablesManager();
    final Selector selector;
    private final ExecutorService executorService;
    private final Queue pendingRegistrations = new ConcurrentLinkedQueue();
    @Nullable
    private final Throttler throttler;
    volatile boolean isClosed = false;
    ThreadLocalCopies copies;
    VanillaChronicleMap.SegmentState segmentState;
    private volatile Thread lastThread;
    private final ExecutorService connectExecutorService = Executors.newCachedThreadPool(
            new NamedThreadFactory("TCP-connect", true) {
                @Override
                public Thread newThread(@net.openhft.lang.model.constraints.NotNull Runnable r) {
                    return lastThread = super.newThread(r);
                }
            });
    private Throwable startedHere;
    private Future future;

    AbstractChannelReplicator(String name, ThrottlingConfig throttlingConfig) throws IOException {
        executorService = Executors.newSingleThreadExecutor(
                new NamedThreadFactory(name, true) {
                    @Override
                    public Thread newThread(@net.openhft.lang.model.constraints.NotNull Runnable r) {
                        return lastThread = super.newThread(r);
                    }
                });

        selector = openSelector(closeables);
        throttler = throttlingConfig.throttling(DAYS) > 0 ?
                new Throttler(selector,
                        throttlingConfig.bucketInterval(MILLISECONDS),
                        throttlingConfig.throttling(DAYS)) : null;

        startedHere = new Throwable("Started here");
    }

    static SocketChannel openSocketChannel(final CloseablesManager closeables) throws IOException {
        SocketChannel result = null;

        try {
            result = SocketChannel.open();
            result.socket().setTcpNoDelay(true);
        } finally {
            if (result != null)
                try {
                    closeables.add(result);
                } catch (IllegalStateException e) {
                    // already closed
                }
        }
        return result;
    }

    static ClassLoader getSystemClassLoader() {
        if (System.getSecurityManager() == null) {
            return ClassLoader.getSystemClassLoader();
        } else {
            return AccessController.doPrivileged(new PrivilegedAction() {
                @Override
                public ClassLoader run() {
                    return ClassLoader.getSystemClassLoader();
                }
            });
        }
    }

    Selector openSelector(final CloseablesManager closeables) throws IOException {

        Selector result = Selector.open();
        closeables.add(result);

        if (!useJavaNIOSelectionKeys) {
            closeables.add(new Closeable() {
                               @Override
                               public void close() throws IOException {
                                   {
                                       SelectionKey[] keys = selectedKeys.flip();
                                       for (int i = 0; i < keys.length && keys[i] != null; i++) {
                                           keys[i] = null;
                                       }
                                   }
                                   {
                                       SelectionKey[] keys = selectedKeys.flip();
                                       for (int i = 0; i < keys.length && keys[i] != null; i++) {
                                           keys[i] = null;
                                       }
                                   }
                               }
                           }
            );
            return openSelector(result, selectedKeys);
        }

        return result;
    }

    /**
     * this is similar to the code in Netty
     *
     * @param selector
     * @return
     */
    private Selector openSelector(@NotNull final Selector selector,
                                  @NotNull final SelectedSelectionKeySet selectedKeySet) {
        try {

            Class selectorImplClass =
                    Class.forName("sun.nio.ch.SelectorImpl", false, getSystemClassLoader());

            // Ensure the current selector implementation is what we can instrument.
            if (!selectorImplClass.isAssignableFrom(selector.getClass())) {
                return selector;
            }

            Field selectedKeysField = selectorImplClass.getDeclaredField("selectedKeys");
            Field publicSelectedKeysField = selectorImplClass.getDeclaredField("publicSelectedKeys");

            selectedKeysField.setAccessible(true);
            publicSelectedKeysField.setAccessible(true);

            selectedKeysField.set(selector, selectedKeySet);
            publicSelectedKeysField.set(selector, selectedKeySet);

            //   logger.trace("Instrumented an optimized java.util.Set into: {}", selector);
        } catch (Exception e) {
            LOG.error("", e);
            //   logger.trace("Failed to instrument an optimized java.util.Set into: {}", selector, t);
        }

        return selector;
    }

    void addPendingRegistration(Runnable registration) {
        pendingRegistrations.add(registration);
    }

    void registerPendingRegistrations() {
        for (Runnable runnable = pendingRegistrations.poll(); runnable != null;
             runnable = pendingRegistrations.poll()) {
            try {
                runnable.run();
            } catch (Exception e) {
                LOG.info("", e);
            }
        }
    }

    abstract void processEvent();

    final void start() {
        future = executorService.submit(
                new Runnable() {
                    @Override
                    public void run() {
                        try {
                            copies = VanillaChronicleMap.SegmentState.getCopies(null);
                            segmentState = VanillaChronicleMap.SegmentState.get(copies);
                            processEvent();
                        } catch (Exception e) {
                            LOG.error("", e);
                        }
                    }
                }
        );
    }

    @Override
    public void close() {

        if (Thread.interrupted())
            LOG.warn("Already interrupted");
        long start = System.currentTimeMillis();
        isClosed = true;
        connectExecutorService.shutdown();
        try {
            if (!connectExecutorService.awaitTermination(5, TimeUnit.MILLISECONDS)) {
                connectExecutorService.shutdownNow();
                connectExecutorService.awaitTermination(10, TimeUnit.SECONDS);
            }
        } catch (InterruptedException e) {
            LOG.error("", e);
        }

        try {
            closeResources();
            if (!executorService.awaitTermination(5, TimeUnit.MILLISECONDS)) {
                executorService.shutdownNow();
                executorService.awaitTermination(10, TimeUnit.SECONDS);
            }
        } catch (InterruptedException e) {
            LOG.error("", e);
        }


        try {
            if (future != null)
                future.cancel(true);

            // we HAVE to be sure we have terminated before calling close() on the ReplicatedChronicleMap
            // if this thread is still running and you close() the ReplicatedChronicleMap without
            // fulling terminating this, you can cause the ReplicatedChronicleMap to attempt to
            // process data on a map that has been closed, which would result in a core dump.

            Thread thread = lastThread;
            if (thread != null && Thread.currentThread() != lastThread)
                for (int i = 0; i < 10; i++) {
                    if (thread.isAlive()) {
                        thread.join(1000);
                        dumpThreadStackTrace(start);
                    }
                }
        } catch (InterruptedException e) {
            dumpThreadStackTrace(start);
            LOG.error("", e);
            LOG.error("", startedHere);
        }
    }

    public void closeResources() {
        isClosed = true;
        executorService.shutdown();

        closeables.closeQuietly();
        if (segmentState != null)
            segmentState.close();
    }

    private void dumpThreadStackTrace(long start) {
        if (lastThread != null && lastThread.isAlive()) {
            StringBuilder sb = new StringBuilder();
            sb.append("Replicator thread still running after ");
            sb.append((System.currentTimeMillis() - start) / 100 / 10.0);
            sb.append(" secs ");
            sb.append(lastThread);
            sb.append(" isAlive= ");
            sb.append(lastThread.isAlive());
            for (StackTraceElement ste : lastThread.getStackTrace()) {
                sb.append("\n\t").append(ste);
            }
            LOG.warn(sb.toString());
        }
    }

    void closeEarlyAndQuietly(SelectableChannel channel) {
        if (throttler != null)
            throttler.remove(channel);
        closeables.closeQuietly(channel);
    }

    void checkThrottleInterval() {
        if (throttler != null)
            throttler.checkThrottleInterval();
    }

    void contemplateThrottleWrites(int bytesJustWritten) {
        if (throttler != null)
            throttler.contemplateThrottleWrites(bytesJustWritten);
    }

    void throttle(SelectableChannel channel) {
        if (throttler != null)
            throttler.add(channel);
    }

    /**
     * throttles 'writes' to ensure the network is not swamped, this is achieved by periodically
     * de-registering the write selector during periods of high volume.
     */
    static class Throttler {

        private final Selector selector;
        private final Set channels = new CopyOnWriteArraySet();
        private final long throttleInterval;
        private final long maxBytesInInterval;

        private long lastTime = System.currentTimeMillis();
        private long bytesWritten;

        Throttler(@NotNull Selector selector,
                  long throttleIntervalInMillis,
                  long bitsPerDay) {
            this.selector = selector;
            this.throttleInterval = throttleIntervalInMillis;
            double bytesPerMs = ((double) bitsPerDay) / DAYS.toMillis(1) / BITS_IN_A_BYTE;
            this.maxBytesInInterval = round(bytesPerMs * throttleInterval);
        }

        public void add(SelectableChannel selectableChannel) {
            channels.add(selectableChannel);
        }

        public void remove(SelectableChannel socketChannel) {
            channels.remove(socketChannel);
        }

        /**
         * re register the 'write' on the selector if the throttleInterval has passed
         *
         * @throws java.nio.channels.ClosedChannelException
         */
        public void checkThrottleInterval() {
            final long time = System.currentTimeMillis();

            if (lastTime + throttleInterval >= time)
                return;

            lastTime = time;
            bytesWritten = 0;

            if (LOG.isDebugEnabled())
                LOG.debug("Restoring OP_WRITE on all channels");

            for (SelectableChannel selectableChannel : channels) {
                final SelectionKey selectionKey = selectableChannel.keyFor(selector);
                if (selectionKey != null)
                    selectionKey.interestOps(selectionKey.interestOps() | OP_WRITE);
            }
        }

        /**
         * checks the number of bytes written in this interval, if this number of bytes exceeds a
         * threshold, the selected will de-register the socket that is being written to, until the
         * interval is finished.
         *
         * @throws ClosedChannelException
         */
        public void contemplateThrottleWrites(int bytesJustWritten) {
            bytesWritten += bytesJustWritten;
            if (bytesWritten > maxBytesInInterval) {
                for (SelectableChannel channel : channels) {
                    final SelectionKey selectionKey = channel.keyFor(selector);
                    if (selectionKey != null) {
                        selectionKey.interestOps(selectionKey.interestOps() & ~OP_WRITE);
                    }
                    if (LOG.isDebugEnabled())
                        LOG.debug("Throttling UDP writes");
                }
            }
        }
    }

    /**
     * details about the socket connection
     */
    static class Details {

        private final InetSocketAddress address;
        private final byte localIdentifier;

        Details(@NotNull final InetSocketAddress address, final byte localIdentifier) {
            this.address = address;
            this.localIdentifier = localIdentifier;
        }

        public InetSocketAddress address() {
            return address;
        }

        public byte localIdentifier() {
            return localIdentifier;
        }

        @Override
        public String toString() {
            return "Details{" +
                    "address=" + address +
                    ", localIdentifier=" + localIdentifier +
                    '}';
        }
    }

    static class EntryCallback extends Replica.EntryCallback implements BufferResizer {

        private final Replica.EntryExternalizable externalizable;

        @NotNull
        private ByteBufferBytes in;

        @NotNull
        private ByteBuffer out;

        EntryCallback(@NotNull final Replica.EntryExternalizable externalizable,
                      final int tcpBufferSize) {
            this.externalizable = externalizable;
            out = ByteBuffer.allocateDirect(tcpBufferSize);
            in = new ByteBufferBytes(out);
        }

        @NotNull
        public ByteBufferBytes in() {
            return in;
        }

        @NotNull
        public ByteBuffer out() {
            return out;
        }


        public Bytes resizeBuffer(int size) {

            if (LOG.isDebugEnabled())
                LOG.debug("resizing buffer to size=" + size);

            if (size < out.capacity())
                throw new IllegalStateException("it not possible to resize the buffer smaller");

            assert size < Integer.MAX_VALUE;

            final ByteBuffer result = ByteBuffer.allocate(size).order(ByteOrder.nativeOrder());
            final long bytesPosition = in.position();

            in = new ByteBufferBytes(result);

            out.position(0);
            out.limit((int) bytesPosition);
            in.write(out);
            out = result;

            assert out.capacity() == in.capacity();
            assert out.capacity() == size;
            assert out.capacity() == in.capacity();
            assert in.limit() == in.capacity();
            return in;
        }

        public boolean shouldBeIgnored(final Bytes entry, final int chronicleId) {
            return !externalizable.identifierCheck(entry, chronicleId);
        }


        @Override
        public boolean onEntry(@NotNull final Bytes entry,
                               final int chronicleId,
                               final long bootstrapTime) {

            long startOfEntry = entry.position();
            long pos0 = in.position();
            long start = 0;
            try {
                // used to denote that this is not a stateless map event
                in.writeByte(StatelessChronicleMap.EventId.STATEFUL_UPDATE.ordinal());

                long sizeLocation = in.position();

                // this is where we will store the size of the entry
                in.skip(SIZE_OF_SIZE);

                start = in.position();

                externalizable.writeExternalEntry(entry, in, chronicleId,bootstrapTime);

                if (in.position() == start) {
                    in.position(pos0);
                    return false;
                }

                // write the length of the entry, just before the start, so when we read it back
                // we read the length of the entry first and hence know how many preceding writer to read
                final long bytesWritten = (int) (in.position() - start);

                if (bytesWritten > Integer.MAX_VALUE)
                    throw new IllegalStateException("entry too large, " +
                            "entries are limited to a size of " + Integer.MAX_VALUE);

                if (LOG.isDebugEnabled())
                    LOG.debug("sending entry of entrySize=" + (int) bytesWritten);

                in.writeInt(sizeLocation, (int) bytesWritten);

            } catch (IllegalArgumentException e) {

                // reset the entries position
                entry.position(startOfEntry);

                // reset the in-buffers position
                in.position(pos0);
                long remaining = in.remaining();
                int entrySize = externalizable.sizeOfEntry(entry, chronicleId);


                if (entrySize > remaining) {

                    long newSize = start + entrySize;

                    // This can occur when we pack a number of entries into the buffer and the
                    // last entry is very large.
                    if (newSize > Integer.MAX_VALUE)
                        return false;

                    resizeBuffer((int) newSize);

                    in.position(pos0);
                    entry.position(startOfEntry);

                    return onEntry(entry, chronicleId, bootstrapTime);
                } else
                    throw e;

            }
            return true;
        }
    }

    abstract class AbstractConnector {

        private final String name;
        private int connectionAttempts = 0;
        private volatile SelectableChannel socketChannel;

        public AbstractConnector(String name) {
            this.name = name;
        }

        abstract SelectableChannel doConnect() throws IOException, InterruptedException;

        /**
         * connects or reconnects, but first waits a period of time proportional to the {@code
         * connectionAttempts}
         */
        public final void connectLater() {
            if (socketChannel != null) {
                closeables.closeQuietly(socketChannel);
                socketChannel = null;
            }

            final long reconnectionInterval = connectionAttempts * 100;
            if (connectionAttempts < 5)
                connectionAttempts++;
            doConnect(reconnectionInterval);
        }

        /**
         * connects or reconnects immediately
         */
        public void connect() {
            doConnect(0);
        }

        /**
         * @param reconnectionInterval the period to wait before connecting
         */
        private void doConnect(final long reconnectionInterval) {
            try {
                connectExecutorService.submit(new Runnable() {
                    public void run() {
                        SelectableChannel socketChannel = null;
                        try {
                            if (reconnectionInterval > 0)
                                Thread.sleep(reconnectionInterval);

                            socketChannel = doConnect();

                            try {
                                closeables.add(socketChannel);
                            } catch (IllegalStateException e) {
                                // close could have be called from another thread, while we were in Thread.sleep()
                                // which would cause a IllegalStateException
                                closeQuietly(socketChannel);
                                return;
                            }

                            AbstractConnector.this.socketChannel = socketChannel;
                        } catch (Exception e) {
                            closeQuietly(socketChannel);
                            LOG.debug("", e);
                        }
                    }
                });
            } catch (Exception e) {
                if (!isClosed)
                    LOG.error("", e);
            }
        }

        private void closeQuietly(SelectableChannel socketChannel) {
            if (socketChannel == null)
                return;
            try {
                socketChannel.close();
            } catch (Exception e1) {
                // do nothing
            }
        }


        public void setSuccessfullyConnected() {
            connectionAttempts = 0;
        }

    }

}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy