/*-
 *
 *  This file is part of Oracle NoSQL Database
 *  Copyright (C) 2011, 2014 Oracle and/or its affiliates.  All rights reserved.
 *
 *  Oracle NoSQL Database is free software: you can redistribute it and/or
 *  modify it under the terms of the GNU Affero General Public License
 *  as published by the Free Software Foundation, version 3.
 *
 *  Oracle NoSQL Database 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
 *  Affero General Public License for more details.
 *
 *  You should have received a copy of the GNU Affero General Public
 *  License in the LICENSE file along with Oracle NoSQL Database.  If not,
 *  see <http://www.gnu.org/licenses/>.
 *
 *  An active Oracle commercial licensing agreement for this product
 *  supercedes this license.
 *
 *  For more information please contact:
 *
 *  Vice President Legal, Development
 *  Oracle America, Inc.
 *  5OP-10
 *  500 Oracle Parkway
 *  Redwood Shores, CA 94065
 *
 *  or
 *
 *  berkeleydb-info_us@oracle.com
 *
 *  [This line intentionally left blank.]
 *  [This line intentionally left blank.]
 *  [This line intentionally left blank.]
 *  [This line intentionally left blank.]
 *  [This line intentionally left blank.]
 *  [This line intentionally left blank.]
 *  EOF
 *
 */

package oracle.kv.impl.rep;

import java.util.logging.Logger;

import oracle.kv.FaultException;
import oracle.kv.impl.fault.ClientAccessException;
import oracle.kv.impl.fault.ProcessExitCode;
import oracle.kv.impl.fault.ProcessFaultHandler;
import oracle.kv.impl.fault.RNUnavailableException;
import oracle.kv.impl.fault.WrappedClientException;
import oracle.kv.impl.test.TestStatus;
import oracle.kv.impl.util.ConfigurableService.ServiceStatus;
import oracle.kv.impl.util.server.LoggerUtils;

import com.sleepycat.je.EnvironmentFailureException;

/**
 * The fault handler for the RepNodeService
 */
public class RepNodeServiceFaultHandler extends ProcessFaultHandler {

    private final RepNodeService repNodeService;

    /** The fault that results in the process shutting down. */
    private Throwable shutdownFault;

    /**
     * Creates a fault handler for the RepNodeService
     *
     * @param repNodeService the service whose faults are being handled
     * @param logger the logger associated with the service
     * @param defaultExitCode the default exit code to use on a process exit
     */
    public RepNodeServiceFaultHandler(RepNodeService repNodeService,
                                      Logger logger,
                                      ProcessExitCode defaultExitCode) {
        super(logger, defaultExitCode);
        this.repNodeService = repNodeService;
    }

    /**
     * Initiates an async shutdown in a separate thread. Note that there is no
     * way to really guarantee, that the process does not exit before the call
     * completes. From a practical standpoint this is a very unlikely event.
     */
    @Override
    protected void queueShutdownInternal(Throwable fault,
                                         ProcessExitCode exitCode) {
        assert (fault != null);

        shutdownFault = fault;
        if (TestStatus.isActive()) {
            new AsyncTestShutdownThread().start();
        } else {
            new AsyncShutdownThread(exitCode).start();
        }
    }

    /**
     * Wrap it inside a FaultException, if it isn't one already.
     * <p>
     * FaultExceptions in the service originate from "forwarding" operations
     * and are just passed through.
     */
    @Override
    protected RuntimeException
        getThrowException(RuntimeException requestException) {

        if ((requestException instanceof RNUnavailableException) ||
            (requestException instanceof WrappedClientException) ||
            (requestException instanceof FaultException)) {
            return requestException;
        }

        if (requestException instanceof ClientAccessException) {
            /*
             * This is a security exception generated by the client.
             * Unwrap it so that the client sees it in its orginal form.
             */
            return ((ClientAccessException) requestException).getCause();
        }

        if (requestException instanceof EnvironmentFailureException) {

            /*
             * Map it to a RNUnavailableException so that the request can be
             * retried at a different node, while this node is (typically)
             * restarted by the SN to clear out the state
             */
            return new RNUnavailableException(requestException.getMessage());
        }

        return new FaultException(requestException, true /* isRemote */);
    }

    /**
     * Treat a <code>FaultException</code> like an operation failure, that is,
     * don't exit the process.
     */
    @Override
    public ProcessExitCode getExitCode(RuntimeException requestException,
                                       ProcessExitCode exitCode) {

        if ((requestException instanceof RNUnavailableException) ||
            (requestException instanceof WrappedClientException) ||
            (requestException instanceof FaultException)) {
            return null;
        }

        return exitCode ;
    }

    /**
     * Returns the fault that resulted in the shutdown.
     */
    public Throwable getShutdownFault() {
        return shutdownFault;
    }

    /**
     * Thread used for shutdown in a test environment. The run method here
     * ensures that repNodeService.stop is invoked, so that subsequent unit
     * tests can start with a clean state.
     */
    private class AsyncTestShutdownThread extends Thread {

        AsyncTestShutdownThread() {
            super();
            assert repNodeService.getUsingThreads();
            /* Don't hold up the exit. */
            setDaemon(true);
        }

        @Override
        public void run() {

            /*
             * Test environment. Make it a clean stop, so that the
             * next unit test can start with a clean state.
             */
            try {

                /*
                 * Make it a force stop, placing it in the terminal STOPPED
                 * state.
                 */
                repNodeService.stop(true);
            } catch (Exception e) {

                /*
                 * Best faith effort, we are on our way to the exits, Ignore
                 * final exceptions. Don't log, the logger may be in an unknown
                 * state.
                 */
                System.err.println("Exception during exit: " + e);
            }
        }
    }

    /**
     * Thread for shutdown in a real RN environment. This thread is also used
     * in KVLite where the registry, RN and admin may all be configured to run
     * in the same process. In such a configuration, the KVLite process will
     * itself exit if the RN fault handling requires that the RN exit.
     */
    private class AsyncShutdownThread extends Thread {
        final ProcessExitCode exitCode;

        AsyncShutdownThread(ProcessExitCode exitCode) {
            super();
            this.exitCode = exitCode;
            /* Don't hold up the exit. */
            setDaemon(true);
        }

        @Override
        public void run() {
            Thread.yield();
            try {
                /*
                 * The exception should already have been logged, but print it
                 * here to stderr as a last resource in case the logging system
                 * has itself failed, say because we have run out of disk
                 * space.
                 */
                System.err.println("Process exiting due to fault");
                if (shutdownFault != null) {
                    shutdownFault.printStackTrace(System.err);
                }

                /*
                 * The service is going to exit and services will be stopped
                 * perhaps abruptly as a result.
                 */
                final ServiceStatus status =
                    ProcessExitCode.NO_RESTART.getValue() ==
                    exitCode.getValue() ?
                        ServiceStatus.ERROR_NO_RESTART :
                            ServiceStatus.ERROR_RESTARTING;
                repNodeService.getStatusTracker().update(status);

                /*
                 * Flush logs and stderr.
                 */
                LoggerUtils.closeAllHandlers();
                System.err.flush();

            } finally {
                /* Exit for sure even if there were exceptions */
                System.exit(exitCode.getValue());
            }
        }
    }
}
