/*-
* See the file LICENSE for redistribution information.
*
* Copyright (c) 2002-2005
*      Sleepycat Software.  All rights reserved.
*
* $Id: RecoveryCheckpointTest.java,v 1.29 2004/12/22 14:11:46 linda Exp $
*/

package com.sleepycat.je.recovery;

import java.util.Hashtable;

import com.sleepycat.je.Cursor;
import com.sleepycat.je.Database;
import com.sleepycat.je.DatabaseConfig;
import com.sleepycat.je.DatabaseEntry;
import com.sleepycat.je.DatabaseException;
import com.sleepycat.je.DbInternal;
import com.sleepycat.je.Environment;
import com.sleepycat.je.EnvironmentConfig;
import com.sleepycat.je.EnvironmentStats;
import com.sleepycat.je.LockMode;
import com.sleepycat.je.OperationStatus;
import com.sleepycat.je.Transaction;
import com.sleepycat.je.config.EnvironmentParams;
import com.sleepycat.je.dbi.DatabaseId;
import com.sleepycat.je.dbi.DatabaseImpl;
import com.sleepycat.je.dbi.DbTree;
import com.sleepycat.je.dbi.EnvironmentImpl;
import com.sleepycat.je.junit.JUnitThread;
import com.sleepycat.je.util.TestUtils;

public class RecoveryCheckpointTest extends RecoveryTestBase {

    volatile int sequence = 0;

    public void setExtraProperties()
	throws DatabaseException {

        /*
         * Make sure that the environments in this unit test always run with
         * checkpointing off, so we can call it explcitly.
         */
        envConfig.setConfigParam
            (EnvironmentParams.ENV_RUN_CHECKPOINTER.getName(), "false");
    }

    /**
     * Run checkpoints on empty dbs.
     */
    public void testEmptyCheckpoint()
        throws Throwable {

        createEnvAndDbs(1 << 20, true, NUM_DBS);

        try {

            /*
             * Run checkpoint on empty environment. Should be the second one
             * run, the first was run by recovery when the environment was
             * opened.
             */
            env.checkpoint(forceConfig);
            EnvironmentStats stats = env.getStats(TestUtils.FAST_STATS);
            assertEquals(2, stats.getNCheckpoints());
            assertEquals(2, stats.getLastCheckpointId());

            /* Shutdown, recover. */
            Hashtable expectedData = new Hashtable(); // expected values
            closeEnv();  
            recoverAndVerify(expectedData, NUM_DBS); // 3 checkpoints

            /* Another checkpoint. */
            EnvironmentConfig envConfig = new EnvironmentConfig();
            envConfig.setTransactional(true);
            env = new Environment(envHome, envConfig);
            env.checkpoint(forceConfig);
            stats = env.getStats(TestUtils.FAST_STATS);
            assertEquals(2, stats.getNCheckpoints());
            assertEquals(7, stats.getLastCheckpointId());

            /* Shutdown, recover. */
            env.close();
            recoverAndVerify(expectedData, NUM_DBS);
        } catch (Throwable t) {
            t.printStackTrace();
            throw t;
        }
    }

    /**
     * Test checkpoints that end up using BINDeltas -- the recovery must work.
     */
    public void testBinDelta()
	throws Throwable {

	createEnvAndDbs(1 << 20, true, NUM_DBS);

        try {
            int numRecs = 28;
            Hashtable expectedData = new Hashtable();
            Transaction txn = env.beginTransaction(null, null);
            insertData(txn, 0, numRecs, expectedData, 1, true, NUM_DBS);
            env.checkpoint(forceConfig);
            insertData(txn, numRecs+1, numRecs+2, expectedData,
		       1, true, NUM_DBS);
            txn.commit();
            
            /* 
	     * This next checkpoint will end up using a BINDelta to log the *
	     * last inserted record. It will have practically nothing but * the
	     * root in the checkpoint.
             */
            env.checkpoint(forceConfig);

            /* Shutdown, recover from a checkpoint that uses BINDeltas. */
            closeEnv();
            recoverAndVerify(expectedData, NUM_DBS);
        } catch (Throwable t) {
            t.printStackTrace();
            throw t;
        }
    }

    /** 
     * Test the rollback of transactions that are active during a checkpoint.
     */
    public void testActiveWhileCheckpointing() 
        throws Throwable {

	createEnvAndDbs(1 << 20, true, NUM_DBS);

        try {
            int numRecs = 1;
            Hashtable expectedData = new Hashtable();
            Transaction txn = env.beginTransaction(null, null);
            insertData(txn, 0, numRecs, expectedData, 1, false, NUM_DBS);

            /* Now run a checkpoint while this operation hasn't finished. */
            env.checkpoint(forceConfig);
            txn.abort();
            
            /* Shutdown, recover. */
            closeEnv();
            recoverAndVerify(expectedData, NUM_DBS);
        } catch (Throwable t) {
            t.printStackTrace();
            throw t;
        }
    }

    public void testSR11293()
	throws Throwable {

	createEnv(1 << 20, false);

	Transaction dbTxn = env.beginTransaction(null, null);
	EnvironmentImpl envImpl = DbInternal.envGetEnvironmentImpl(env);
	final DbTree dbTree = envImpl.getDbMapTree();

        DatabaseConfig dbConfig = new DatabaseConfig();
        dbConfig.setTransactional(true);
        dbConfig.setAllowCreate(true);
        dbConfig.setSortedDuplicates(true);
	final Database db = env.openDatabase(dbTxn, "foo", dbConfig);
	dbTxn.commit();
	final Transaction txn = env.beginTransaction(null, null);
	sequence = 0;

	/**
	 * The sequence between the two tester threads is:
	 *
	 * tester2: write 1/1 into the database.  This causes the initial tree
	 * to be created (IN/BIN/LN).  Flush that out to the disk with a full
	 * checkpoint.  Signal tester1 and wait.
	 *
	 * tester1: Lock the MapLN for "foo" db.  Signal tester2 and wait.
	 *
	 * tester2: Add 2/2 to the tree which causes the BIN to be dirtied.
	 * Signal tester1 to continue, perform a full checkpoint which will
	 * causes the root IN to be dirtied and flushed.  DbTree.modifyDbRoot
	 * will block on the MapLN lock held by tester1.
	 *
	 * tester1: while tester2 is blocking on the MapLN lock, this thread is
	 * sleeping.  When it wakes up, it releases the MapLN lock by aborting
	 * the transaction.
	 *
	 * tester2: modifyDbRoot finally acquires the write lock on foo-db's
	 * MapLN write lock, performs the update to the DbTree and returns from
	 * the sync().
	 */
	JUnitThread tester1 =
	    new JUnitThread("testSR11293DbTreeLocker") {
		    public void testBody() {
			try {
			    /* Wait for tester2. */
			    while (sequence < 1) {
				Thread.yield();
			    }

			    /* Lock the MapLN for the database. */
			    DatabaseId fooId =
				DbInternal.dbGetDatabaseImpl(db).getId();
			    DatabaseImpl fooDb = dbTree.getDb(fooId, 500000L);
			    assert fooDb != null;

			    sequence++;

			    /* Wait for tester2. */
			    while (sequence < 3) {
				Thread.yield();
			    }

			    try {
				Thread.sleep(3000);
			    } catch (Exception E) {
			    }

			    try {
				txn.abort();
				db.close();
				env.close();
			    } catch (DatabaseException DBE) {
				DBE.printStackTrace();
				fail("unexpected exception: " + DBE);
			    }
			} catch (DatabaseException DBE) {
			    DBE.printStackTrace();
			    fail("caught DatabaseException " + DBE);
			}
		    }
		};

	JUnitThread tester2 =
	    new JUnitThread("testSR11293DbWriter") {
		    public void testBody() {
			try {
			    DatabaseEntry key =
				new DatabaseEntry(new byte[] { 1 });
			    DatabaseEntry data =
				new DatabaseEntry(new byte[] { 1 });
			    assertEquals(OperationStatus.SUCCESS,
					 db.put(null, key, data));
			    env.sync();

			    sequence++;
			    while (sequence < 2) {
				Thread.yield();
			    }

			    key.setData(new byte[] { 2 });
			    data.setData(new byte[] { 2 });
			    assertEquals(OperationStatus.SUCCESS,
					 db.put(null, key, data));
			    sequence++;
			    env.sync();
			} catch (DatabaseException DBE) {
			    DBE.printStackTrace();
			    fail("unexpected exception: " + DBE);
			}
		    }
		};

	tester1.start();
	tester2.start();
	tester1.finishTest();
	tester2.finishTest();

        EnvironmentConfig recoveryConfig = new EnvironmentConfig();

        recoveryConfig.setConfigParam
	    (EnvironmentParams.ENV_RUN_CHECKPOINTER.getName(), "false");
	recoveryConfig.setConfigParam
	    (EnvironmentParams.ENV_RUN_CLEANER.getName(), "false");
	recoveryConfig.setConfigParam
	    (EnvironmentParams.ENV_RUN_EVICTOR.getName(), "false");

        env = new Environment(envHome, recoveryConfig);
	dbConfig.setAllowCreate(false);
	dbConfig.setTransactional(false);
	Database db2 = env.openDatabase(null, "foo", dbConfig);
	Cursor c = db2.openCursor(null, null);
	DatabaseEntry key = new DatabaseEntry();
	DatabaseEntry data = new DatabaseEntry();
	assertEquals(OperationStatus.SUCCESS,
		     c.getNext(key, data, LockMode.DEFAULT));
	assertEquals((key.getData())[0], 1);
	assertEquals((data.getData())[0], 1);

	assertEquals(OperationStatus.SUCCESS,
		     c.getNext(key, data, LockMode.DEFAULT));
	assertEquals((key.getData())[0], 2);
	assertEquals((data.getData())[0], 2);
	assertEquals(OperationStatus.NOTFOUND,
		     c.getNext(key, data, LockMode.DEFAULT));

	c.close();
	db2.close();
	env.close();
    }
}
