/*

   Derby - Class org.apache.derby.impl.db.SlaveDatabase

   Licensed to the Apache Software Foundation (ASF) under one or more
   contributor license agreements.  See the NOTICE file distributed with
   this work for additional information regarding copyright ownership.
   The ASF licenses this file to you 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 org.apache.derby.impl.db;

import org.apache.derby.shared.common.error.PublicAPI;
import org.apache.derby.shared.common.reference.Attribute;
import org.apache.derby.shared.common.reference.SQLState;
import org.apache.derby.shared.common.error.StandardException;
import org.apache.derby.iapi.jdbc.AuthenticationService;
import org.apache.derby.iapi.services.context.ContextManager;
import org.apache.derby.iapi.services.context.ContextService;
import org.apache.derby.iapi.services.monitor.Monitor;
import org.apache.derby.iapi.util.InterruptStatus;
import org.apache.derby.iapi.store.replication.slave.SlaveFactory;
import org.apache.derby.iapi.sql.conn.LanguageConnectionContext;

import org.apache.derby.iapi.jdbc.InternalDriver;

import java.security.PrivilegedAction;
import java.security.PrivilegedExceptionAction;
import java.security.PrivilegedActionException;
import java.security.AccessController;

import java.sql.SQLException;
import java.util.Properties;
import org.apache.derby.shared.common.reference.MessageId;
import org.apache.derby.impl.store.replication.ReplicationLogger;

/**
 * SlaveDatabase is an instance of Database, and is booted instead of
 * BasicDatabase if this database will have the replication slave
 * role. SlaveDatabase differs from BasicDatabase in the following
 * ways:
 *
 * 1: When starting a non-replicated database (i.e., BasicDatabase),
 *    only one thread is used to start all modules of the database.
 *    When booted in slave mode, the thread that boots the store
 *    module will be blocked during log recovery. To remedy this,
 *    SlaveDatabase runs the boot method of BasicDatabase in a
 *    separate thread. This ensures that the connection attempt that
 *    started slave replication mode will not hang.
 *
 * 2: While the database is in replication slave mode, the
 *    authentication services are not available because these require
 *    that the store module has been booted first. Calling
 *    getAuthenticationService when in slave mode will raise an
 *    exception.
 *
 * 3: While the database is in replication slave mode, connections are
 *    not accepted since the database cannot process transaction
 *    requests. Calling setupConnection when in slave mode will raise
 *    an exception.
 *
 * 4: If the failover command has been executed for this database, it
 *    is no longer in replication slave mode. When this has
 *    happened, SlaveDatabase works exactly as BasicDatabase.
 */

public class SlaveDatabase extends BasicDatabase {
    /** True until SlaveDatabaseBootThread has successfully booted the
     * database. Does not happen until the failover command has been
     * executed for this database */
    private volatile boolean inReplicationSlaveMode;
    private volatile boolean shutdownInitiated;

    /** True until this database has been successfully booted. Any
     * exception that occurs while inBoot is true will be handed to
     * the client thread booting this database. */
    private volatile boolean inBoot;

    /** Set by the database boot thread if it fails before slave mode
     * has been started properly (i.e., if inBoot is true). This
     * exception will then be reported to the client connection. */
    private volatile StandardException bootException;
    private String dbname; // The name of the replicated database
    private volatile SlaveFactory slaveFac;

    /////////////////////////////
    // ModuleControl interface //
    /////////////////////////////
    /**
     * Determines whether this Database implementation should be used
     * to boot the database.
     * @param startParams The properties used to decide if
     * SlaveDatabase is the correct implementation of Database for the
     * database to be booted. 
     * @return true if the database is updatable (not read-only) and
     * replication slave mode is specified in startParams
     */
    public boolean canSupport(Properties startParams) {

        boolean supported =
            Monitor.isDesiredCreateType(startParams, getEngineType());
        if (supported) {
            String repliMode =
                startParams.getProperty(SlaveFactory.REPLICATION_MODE);
            if (repliMode == null ||
                !repliMode.equals(SlaveFactory.SLAVE_MODE)) {
                supported = false;
            }
        }

        return supported;
    }

    public void boot(boolean create, Properties startParams)
        throws StandardException {

        inReplicationSlaveMode = true;
        inBoot = true;
        shutdownInitiated = false;

        dbname = startParams.getProperty(SlaveFactory.SLAVE_DB);

        // SlaveDatabaseBootThread is an internal class
        SlaveDatabaseBootThread dbBootThread =
            new SlaveDatabaseBootThread(create, startParams);
        Thread sdbThread = 
                new Thread(dbBootThread, "derby.slave.boot-" + dbname);
        sdbThread.setDaemon(true);
        sdbThread.start();

        // Check that the database was booted successfully, or throw
        // the exception that caused the boot to fail.
        verifySuccessfulBoot();

        inBoot = false;

        // This module has now been booted (hence active=true) even
        // though submodules like store and authentication may not
        // have completed their boot yet. We deal with that by raising
        // an error on attempts to use these 
        active=true;
    }

    /**
     * Called by Monitor when this module is stopped, i.e. when the
     * database is shut down. When the database is shut down using the
     * stopSlave command, the stopReplicationSlave method has already
     * been called when this method is called. In this case, the
     * replication functionality has already been stopped. If the
     * database is shutdown as part of a system shutdown, however, we
     * need to cleanup slave replication as part of database shutdown.
     */
    public void stop() {
        if (inReplicationSlaveMode && slaveFac != null) {
            try {
                slaveFac.stopSlave(true);
            } catch (StandardException ex) {
            } finally {
                slaveFac = null;
            }
        }
        super.stop();
    }
    
    /////////////////////
    // Class interface //
    /////////////////////
    public SlaveDatabase() {
    }

    ////////////////////////
    // Database interface //
    ////////////////////////
    public boolean isInSlaveMode() {
        return inReplicationSlaveMode;
    }
    
    public LanguageConnectionContext setupConnection(ContextManager cm, 
                                                     String user, 
                                                     String drdaID, 
                                                     String dbname)
        throws StandardException {

        if (inReplicationSlaveMode) {
            // do not allow connections to a database that is
            // currently in replication slave move
            throw StandardException.newException(
                        SQLState.CANNOT_CONNECT_TO_DB_IN_SLAVE_MODE, dbname);
        }
        return super.setupConnection(cm, user, drdaID, dbname);
    }

    public AuthenticationService getAuthenticationService()
        throws StandardException{
        if (inReplicationSlaveMode) {
            // Cannot get authentication service for a database that
            // is currently in replication slave move
            throw StandardException.newException(
                SQLState.CANNOT_CONNECT_TO_DB_IN_SLAVE_MODE, dbname);
        }
        return super.getAuthenticationService();
    }

    /**
     * Verify that a connection to stop the slave has been made from
     * here. If verified, the database context is given to the method
     * caller. This will ensure this database is shutdown when an
     * exception with database severity is thrown. If not verified, an
     * exception is thrown.
     * 
     * @exception StandardException Thrown if a stop slave connection
     * attempt was not made from this class
     */
    public void verifyShutdownSlave() throws StandardException {
        if (!shutdownInitiated) {
            throw StandardException.
                newException(SQLState.REPLICATION_STOPSLAVE_NOT_INITIATED);
        }
        pushDbContext(getContextService().
                      getCurrentContextManager());
    }

    /**
     * Stop replication slave mode if replication slave mode is active and 
     * the network connection with the master is down
     * 
     * @exception SQLException Thrown on error, if not in replication 
     * slave mode or if the network connection with the master is not down
     */
    public  void stopReplicationSlave() throws SQLException {

        if (shutdownInitiated) {
            // The boot thread has failed or stopReplicationSlave has
            // already been called. There is nothing more to do to
            // stop slave replication mode.
            return;
        }
        
        if (!inReplicationSlaveMode) {
            StandardException se = StandardException.
                newException(SQLState.REPLICATION_NOT_IN_SLAVE_MODE);
            throw PublicAPI.wrapStandardException(se);
        }

        // stop slave without using force, meaning that this method
        // call will fail with an exception if the network connection
        // with the master is up
        try {
            slaveFac.stopSlave(false);
        } catch (StandardException se) {
            throw PublicAPI.wrapStandardException(se);
        }

        slaveFac = null;
    }

    public void failover(String dbname) throws StandardException {
        if (inReplicationSlaveMode) {
            slaveFac.failover();
            // SlaveFactory#failover will make the
            // SlaveDatabaseBootThread complete booting of the store
            // modules, and inReplicationSlaveMode will then be set to
            // false (see SlaveDatabaseBootThread#run).
            // Wait until store is completely booted before returning from
            // this method
            while (inReplicationSlaveMode) {
                try {
                    Thread.sleep(500);
                } catch (InterruptedException ie) {
                    InterruptStatus.setInterrupted();
                }
            }
        } else {
            // If failover is performed on a master that has been a slave
            // earlier
            super.failover(dbname);
        }
    }
    
    /////////////////
    // Inner Class //
    /////////////////
    /**
     * Thread that boots the slave database. Will be blocked in
     * LogFactory.recover until database is no longer in slave
     * replication mode.
     */
    private class SlaveDatabaseBootThread implements Runnable {

        private boolean create;
        private Properties params;

        public SlaveDatabaseBootThread(boolean create, Properties startParams){
            this.create = create;
            params = startParams;
        }

        public void run() {

            // The thread needs a ContextManager since two threads
            // cannot share a context
            ContextManager bootThreadCm = null;
            try {

                bootThreadCm = getContextService().newContextManager();
                getContextService().
                    setCurrentContextManager(bootThreadCm);

                bootBasicDatabase(create, params); // will be blocked

                // if we get here, failover has been called and the
                // database can now be connected to
                inReplicationSlaveMode = false; 

                if (bootThreadCm != null) {
                    getContextService().
                        resetCurrentContextManager(bootThreadCm);
                    bootThreadCm = null;
                }
            } catch (Exception e) {
                // We get here when SlaveController#stopSlave has been called,
                // a fatal Derby exception has been thrown, or if a run-time
                // error is thrown.  Log the error unconditionally to make sure
                // it can be observed, since if this happens during or after
                // recovery on a failover, there will be no connection attempt
                // failing with the error. New connection attempts will just
                // hang...

                ReplicationLogger rl = new ReplicationLogger(dbname);
                rl.logError(MessageId.REPLICATION_FATAL_ERROR, e);
                
                if (e instanceof StandardException) {
                    handleShutdown((StandardException)e);
                }
            } 
        }
    }

    ////////////////////
    // Private Methods//
    ////////////////////

    /**
     * Verify that the slave functionality has been properly started.
     * This method will block until a successful slave startup has
     * been confirmed, or it will throw the exception that caused it
     * to fail.
     */
    private void verifySuccessfulBoot() throws StandardException {
        while (!(isSlaveFactorySet() && slaveFac.isStarted())) {
            if (bootException != null) {
                throw bootException;
            } else {
                try {
                    Thread.sleep(500);
                } catch (InterruptedException ie) {
                    InterruptStatus.setInterrupted();
                }
            }
        }

        if (bootException != null) {

            // DERBY-4186: This is a corner case. Master made us shut down
            // before the initial connect which establishes the slave has
            // finalized it setting up of the slave and returned control to the
            // application. bootException is set while we (application thread)
            // are waiting in the sleep in the loop above (by the
            // SlaveDatabaseBootThread thread in its call to handleShutdown),
            // and this was previously ignored.
            throw bootException;
        }
    }

    /** 
     * If slaveFac (the reference to the SlaveFactory) has not already
     * been set, this method will try to set it by calling
     * Monitor.findServiceModule. If slavFac was already set, the
     * method does not do anything.
     *
     * @return true if slaveFac is set after calling this method,
     * false otherwise
     */
    private boolean isSlaveFactorySet() {
        if (slaveFac != null) {
            return true;
        }

        try {
            slaveFac = (SlaveFactory)
                findServiceModule(this, SlaveFactory.MODULE);
            return true;
        } catch (StandardException se) {
            // We get a StandardException if SlaveFactory has not been 
            // booted yet. Safe to retry later.
            return false;
        }
    }

    /**
     * Used to shutdown this database. 
     *
     * If an error occurs as part of the database boot process, we
     * hand the exception that caused boot to fail to the client
     * thread. The client thread will in turn shut down this database.
     *
     * If an error occurs at a later stage than during boot, we shut
     * down the database by setting up a connection with the shutdown
     * attribute. The internal connection is required because database
     * shutdown requires EmbedConnection to do cleanup.
     *
     * @param shutdownCause the reason why the database needs to be
     * shutdown
     */
    private void handleShutdown(StandardException shutdownCause) {
        if (inBoot) {
            bootException = shutdownCause;
            return;
        } 
        try {
            shutdownInitiated = true;

            String conStr = "jdbc:derby:"+dbname+";"+
                Attribute.REPLICATION_INTERNAL_SHUTDOWN_SLAVE+
                "=true";

            InternalDriver driver = InternalDriver.activeDriver();
            if (driver != null) {
                driver.connect( conStr, (Properties) null, 0 );
            }
        } catch (Exception e) {
            // Todo: report error to derby.log if exception is not
            // SQLState.SHUTDOWN_DATABASE
        }
    }

    private void bootBasicDatabase(boolean create, Properties params)
        throws StandardException {
        // This call will be blocked while slave replication mode is
        // active
        super.boot(create, params);
    }
    
    /**
     * Privileged lookup of the ContextService. Must be private so that user code
     * can't call this entry point.
     */
    private  static  ContextService    getContextService()
    {
        return AccessController.doPrivileged
            (
             new PrivilegedAction<ContextService>()
             {
                 public ContextService run()
                 {
                     return ContextService.getFactory();
                 }
             }
             );
    }

    /**
     * Privileged startup. Must be private so that user code
     * can't call this entry point.
     */
    private  static  Object findServiceModule( final Object serviceModule, final String factoryInterface)
        throws StandardException
    {
        try {
            return AccessController.doPrivileged
                (
                 new PrivilegedExceptionAction<Object>()
                 {
                     public Object run()
                         throws StandardException
                     {
                         return Monitor.findServiceModule( serviceModule, factoryInterface );
                     }
                 }
                 );
        } catch (PrivilegedActionException pae)
        {
            throw StandardException.plainWrapException( pae );
        }
    }

}
