dorkbox.network.connection.Shutdownable.java Source code

Java tutorial

Introduction

Here is the source code for dorkbox.network.connection.Shutdownable.java

Source

/*
 * Copyright 2019 dorkbox, llc.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package dorkbox.network.connection;

import static dorkbox.network.pipeline.ConnectionType.EPOLL;
import static dorkbox.network.pipeline.ConnectionType.KQUEUE;
import static dorkbox.network.pipeline.ConnectionType.NIO;
import static dorkbox.network.pipeline.ConnectionType.OIO;

import java.security.AccessControlException;
import java.util.ArrayList;
import java.util.LinkedList;
import java.util.List;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicBoolean;

import org.slf4j.Logger;

import dorkbox.network.NativeLibrary;
import dorkbox.network.pipeline.ConnectionType;
import dorkbox.util.NamedThreadFactory;
import dorkbox.util.OS;
import dorkbox.util.Property;
import io.netty.channel.Channel;
import io.netty.channel.ChannelFuture;
import io.netty.channel.DefaultEventLoopGroup;
import io.netty.channel.EventLoopGroup;
import io.netty.channel.epoll.EpollEventLoopGroup;
import io.netty.channel.kqueue.KQueueEventLoopGroup;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.oio.OioEventLoopGroup;
import io.netty.util.concurrent.EventExecutor;
import io.netty.util.concurrent.Future;
import io.netty.util.internal.PlatformDependent;

/**
 * This is the highest level endpoint, for lifecycle support/management.
 */
public class Shutdownable {
    static {
        //noinspection Duplicates
        try {
            // doesn't work when running from inside eclipse.
            // Needed for NIO selectors on Android 2.2, and to force IPv4.
            System.setProperty("java.net.preferIPv4Stack", Boolean.TRUE.toString());
            System.setProperty("java.net.preferIPv6Addresses", Boolean.FALSE.toString());

            // java6 has stack overflow problems when loading certain classes in it's classloader. The result is a StackOverflow when
            // loading them normally. This calls AND FIXES this issue.
            if (OS.javaVersion == 6) {
                if (PlatformDependent.hasUnsafe()) {
                    //noinspection ResultOfMethodCallIgnored
                    PlatformDependent.newFixedMpscQueue(8);
                }
            }
        } catch (AccessControlException ignored) {
        }
    }

    protected static final String shutdownHookName = "::SHUTDOWN_HOOK::";
    protected static final String stopTreadName = "::STOP_THREAD::";

    public static final String THREADGROUP_NAME = "(Netty)";

    /**
     * The HIGH and LOW watermark points for connections
     */
    @Property
    public static final int WRITE_BUFF_HIGH = 32 * 1024;
    @Property
    public static final int WRITE_BUFF_LOW = 8 * 1024;

    /**
     * The amount of time in milli-seconds to wait for this endpoint to close all {@link Channel}s and shutdown gracefully.
     */
    @Property
    public static long maxShutdownWaitTimeInMilliSeconds = 2000L; // in milliseconds

    /**
     * Checks to see if we are running in the netty thread. This is (usually) to prevent potential deadlocks in code that CANNOT be run from
     * inside a netty worker.
     */
    public static boolean isNettyThread() {
        return Thread.currentThread().getThreadGroup().getName().contains(THREADGROUP_NAME);
    }

    /**
     * Runs a runnable inside a NEW thread that is NOT in the same thread group as Netty
     */
    public static void runNewThread(final String threadName, final Runnable runnable) {
        Thread thread = new Thread(Thread.currentThread().getThreadGroup().getParent(), runnable);
        thread.setDaemon(true);
        thread.setName(threadName);
        thread.start();
    }

    protected final org.slf4j.Logger logger;

    protected final ThreadGroup threadGroup;

    protected final Class<? extends Shutdownable> type;

    protected final Object shutdownInProgress = new Object();
    private volatile boolean isShutdown = false;

    // the eventLoop groups are used to track and manage the event loops for startup/shutdown
    private final List<EventLoopGroup> eventLoopGroups = new ArrayList<EventLoopGroup>(8);
    private final List<ChannelFuture> shutdownChannelList = new ArrayList<ChannelFuture>();

    // make sure that the endpoint is closed on JVM shutdown (if it's still open at that point in time)
    private Thread shutdownHook;

    private final CountDownLatch blockUntilDone = new CountDownLatch(1);

    private AtomicBoolean stopCalled = new AtomicBoolean(false);

    public Shutdownable(final Class<? extends Shutdownable> type) {
        this.type = type;

        // setup the thread group to easily ID what the following threads belong to (and their spawned threads...)
        SecurityManager s = System.getSecurityManager();
        threadGroup = new ThreadGroup(s != null ? s.getThreadGroup() : Thread.currentThread().getThreadGroup(),
                type.getSimpleName() + " " + THREADGROUP_NAME);
        threadGroup.setDaemon(true);

        logger = org.slf4j.LoggerFactory.getLogger(type.getSimpleName());

        shutdownHook = new Thread() {
            @Override
            public void run() {
                if (Shutdownable.this.shouldShutdownHookRun()) {
                    Shutdownable.this.stop();
                }
            }
        };
        shutdownHook.setName(shutdownHookName);
        try {
            Runtime.getRuntime().addShutdownHook(shutdownHook);
        } catch (Throwable ignored) {
            // if we are in the middle of shutdown, we cannot do this.
        }
    }

    /**
     * Add a channel future to be tracked and managed for shutdown.
     */
    protected final void manageForShutdown(ChannelFuture future) {
        synchronized (shutdownChannelList) {
            shutdownChannelList.add(future);
        }
    }

    /**
     * Add an eventloop group to be tracked & managed for shutdown
     */
    protected final void manageForShutdown(EventLoopGroup loopGroup) {
        synchronized (eventLoopGroups) {
            eventLoopGroups.add(loopGroup);
        }
    }

    /**
     * Remove an eventloop group to be tracked & managed for shutdown
     */
    protected final void removeFromShutdown(EventLoopGroup loopGroup) {
        synchronized (eventLoopGroups) {
            eventLoopGroups.remove(loopGroup);
        }
    }

    // server only does this on stop. Client does this on closeConnections
    void shutdownAllChannels() {
        synchronized (shutdownChannelList) {
            // now we stop all of our channels. For the server, this will close the server manager for UDP sessions
            for (ChannelFuture f : shutdownChannelList) {
                Channel channel = f.channel();
                if (channel.isOpen()) {
                    // from the git example on how to shutdown a channel
                    channel.close().syncUninterruptibly();
                    Thread.yield();
                }
            }

            // we have to clear the shutdown list. (
            shutdownChannelList.clear();
        }
    }

    // shutdown all event loops associated
    void shutdownEventLoops() {
        // we want to WAIT until after the event executors have completed shutting down.
        List<Future<?>> shutdownThreadList = new LinkedList<Future<?>>();

        List<EventLoopGroup> loopGroups;
        synchronized (eventLoopGroups) {
            loopGroups = new ArrayList<EventLoopGroup>(eventLoopGroups.size());
            loopGroups.addAll(eventLoopGroups);
        }

        for (EventLoopGroup loopGroup : loopGroups) {
            Future<?> future = loopGroup.shutdownGracefully(maxShutdownWaitTimeInMilliSeconds / 10,
                    maxShutdownWaitTimeInMilliSeconds, TimeUnit.MILLISECONDS);
            shutdownThreadList.add(future);
            Thread.yield();
        }

        // now wait for them to finish!
        // It can take a few seconds to shut down the executor. This will affect unit testing, where connections are quickly created/stopped
        for (Future<?> f : shutdownThreadList) {
            try {
                f.await(maxShutdownWaitTimeInMilliSeconds);
            } catch (InterruptedException ignored) {
            }
            Thread.yield();
        }
    }

    protected final String stopWithErrorMessage(Logger logger, String errorMessage, Throwable throwable) {
        if (logger.isDebugEnabled() && throwable != null) {
            // extra info if debug is enabled
            logger.error(errorMessage, throwable.getCause());
        } else {
            logger.error(errorMessage);
        }

        stop();
        return errorMessage;
    }

    /**
     * Starts the shutdown process during JVM shutdown, if necessary.
     * </p>
     * By default, we always can shutdown via the JVM shutdown hook.
     */
    protected boolean shouldShutdownHookRun() {
        return true;
    }

    /**
     * Creates a new event loop based on the OS type and specified configuration
     *
     * @param threadCount number of threads for the event loop
     *
     * @return a new event loop group based on the specified parameters
     */
    protected EventLoopGroup newEventLoop(final int threadCount, final String threadName) {
        if (OS.isAndroid()) {
            // android ONLY supports OIO
            return newEventLoop(OIO, threadCount, threadName);
        } else if (OS.isLinux() && NativeLibrary.isAvailable()) {
            // epoll network stack is MUCH faster (but only on linux)
            return newEventLoop(EPOLL, threadCount, threadName);
        } else if (OS.isMacOsX() && NativeLibrary.isAvailable()) {
            // KQueue network stack is MUCH faster (but only on macosx)
            return newEventLoop(KQUEUE, threadCount, threadName);
        } else {
            return newEventLoop(NIO, threadCount, threadName);
        }
    }

    /**
     * Creates a new event loop based on the specified configuration
     *
     * @param connectionType LOCAL, NIO, EPOLL, etc...
     * @param threadCount number of threads for the event loop
     *
     * @return a new event loop group based on the specified parameters
     */
    protected EventLoopGroup newEventLoop(final ConnectionType connectionType, final int threadCount,
            final String threadName) {
        NamedThreadFactory threadFactory = new NamedThreadFactory(threadName, threadGroup);

        EventLoopGroup group;

        switch (connectionType) {
        case LOCAL:
            group = new DefaultEventLoopGroup(threadCount, threadFactory);
            break;
        case OIO:
            group = new OioEventLoopGroup(threadCount, threadFactory);
            break;
        case NIO:
            group = new NioEventLoopGroup(threadCount, threadFactory);
            break;
        case EPOLL:
            group = new EpollEventLoopGroup(threadCount, threadFactory);
            break;
        case KQUEUE:
            group = new KQueueEventLoopGroup(threadCount, threadFactory);
            break;

        default:
            group = new DefaultEventLoopGroup(threadCount, threadFactory);
            break;
        }

        manageForShutdown(group);
        return group;
    }

    /**
     * Check to see if the current thread is running from it's OWN thread, or from Netty... This is used to prevent deadlocks.
     *
     * @return true if the specified thread is as Netty thread, false if it's own thread.
     */
    protected boolean isInEventLoop(Thread thread) {
        for (EventLoopGroup loopGroup : eventLoopGroups) {
            for (EventExecutor next : loopGroup) {
                if (next.inEventLoop(thread)) {
                    return true;
                }
            }
        }

        return false;
    }

    /**
     * Safely closes all associated resources/threads/connections.
     * <p/>
     * If we want to WAIT for this endpoint to shutdown, we must explicitly call waitForShutdown()
     * <p/>
     * Override stopExtraActions() if you want to provide extra behavior while stopping the endpoint
     */
    public final void stop() {
        // only permit us to "stop" once!
        if (!stopCalled.compareAndSet(false, true)) {
            return;
        }

        // check to make sure we are in our OWN thread, otherwise, this thread will never exit -- because it will wait indefinitely
        // for itself to finish (since it blocks itself).
        // This occurs when calling stop from within a listener callback.
        Thread currentThread = Thread.currentThread();
        String threadName = currentThread.getName();
        boolean isShutdownThread = !threadName.equals(shutdownHookName) && !threadName.equals(stopTreadName);

        // used to check the event groups to see if we are running from one of them. NOW we force to
        // ALWAYS shutdown inside a NEW thread
        if (!isShutdownThread || !isInEventLoop(currentThread)) {
            stopInThread();
        } else {
            Thread thread = new Thread(new Runnable() {
                @Override
                public void run() {
                    Shutdownable.this.stopInThread();
                }
            });
            thread.setDaemon(false);
            thread.setName(stopTreadName);
            thread.start();
        }
    }

    /**
     * Extra EXTERNAL actions to perform when stopping this endpoint.
     */
    protected void stopExtraActions() {
    }

    /**
     * Actions that happen by the endpoint before the channels are shutdown
     */
    protected void shutdownChannelsPre() {
    }

    /**
     * Actions that happen by the endpoint before any extra actions are run.
     */
    protected void stopExtraActionsInternal() {
    }

    // This actually does the "stopping", since there is some logic to making sure we don't deadlock, this is important
    private void stopInThread() {
        // make sure we are not trying to stop during a startup procedure.
        // This will wait until we have finished starting up/shutting down.
        synchronized (shutdownInProgress) {
            shutdownChannelsPre();
            shutdownAllChannels();

            shutdownEventLoops();

            logger.info("Stopping endpoint.");

            // there is no need to call "stop" again if we close the connection.
            // however, if this is called WHILE from the shutdown hook, blammo! problems!

            // Also, you can call client/server.stop from another thread, which is run when the JVM is shutting down
            // (as there is nothing left to do), and also have problems.
            if (!Thread.currentThread().getName().equals(shutdownHookName)) {
                try {
                    Runtime.getRuntime().removeShutdownHook(shutdownHook);
                } catch (Exception e) {
                    // ignore
                }
            }

            stopExtraActionsInternal();

            // when the eventloop closes, the associated selectors are ALSO closed!
            stopExtraActions();

            isShutdown = true;
        }

        // tell the blocked "bind" method that it may continue (and exit)
        blockUntilDone.countDown();
    }

    /**
     * Blocks the current thread until the endpoint has been stopped. If the endpoint is already stopped, this do nothing.
     */
    public final void waitForShutdown() {
        // we now BLOCK until the stop method is called.
        try {
            blockUntilDone.await();
        } catch (InterruptedException e) {
            logger.error("Thread interrupted while waiting for stop!");
        }
    }

    /**
     * @return true if we have already shutdown, false otherwise
     */
    public final boolean isShutdown() {
        synchronized (shutdownInProgress) {
            return isShutdown;
        }
    }

    @Override
    public String toString() {
        return "EndPoint [" + getName() + "]";
    }

    public String getName() {
        return type.getSimpleName();
    }
}