/*
 * Copyright 2009 Udai Gupta, Ralf Joachim
 *
 * 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 org.castor.cpa.test.test06;

import java.sql.Connection;
import java.sql.Statement;
import java.util.Enumeration;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.castor.cpa.test.framework.CPATestCase;
import org.castor.cpa.test.framework.xml.types.DatabaseEngineType;
import org.exolab.castor.jdo.Database;
import org.exolab.castor.jdo.JDOManager;
import org.exolab.castor.jdo.OQLQuery;

/**
 * Concurrent stress test. Stress tests on data objects modification by multiple
 * thread and detect if by any race condition occurs.
 */
public final class TestRaceCondition extends CPATestCase {
    /** The <a href="http://jakarta.apache.org/commons/logging/">Jakarta Commons
     *  Logging</a> instance used for all logging. */
    private static final Log LOG = LogFactory.getLog(TestRaceCondition.class);

    private static final int NUM_OF_RACING_THREADS = 8;
    private static final int NUM_OF_VALUE_PAIRS = 4;
    private static final int NUM_OF_TRIALS = 5;

    private static final String DBNAME = "test06";
    private static final String MAPPING = "/org/castor/cpa/test/test06/mapping.xml";
    
    private JDOManager _jdo;

    private Database _masterDB;

    private Database[] _dbForRace;

    private Thread _thread;

    private static String _className;

    private static Class<?> _classType;

    private static boolean _leak;

    public TestRaceCondition(final String name) {
        super(name);
    }

    // Test are only included/excluded for engines that have been tested with this test suite.

    public boolean include(final DatabaseEngineType engine) {
        return (engine == DatabaseEngineType.HSQL)
            || (engine == DatabaseEngineType.MYSQL)
            || (engine == DatabaseEngineType.ORACLE)
            || (engine == DatabaseEngineType.POSTGRESQL);
    }

    // Test fails on DERBY even if isolation levels and locking are supported
    public boolean exclude(final DatabaseEngineType engine) {
        return (engine == DatabaseEngineType.DERBY);
    }

    public void setUp() throws Exception {
        _jdo = getJDOManager(DBNAME, MAPPING);

        _masterDB = _jdo.getDatabase();
        
        _dbForRace = new Database[NUM_OF_RACING_THREADS];
        for (int i = 0; i < NUM_OF_RACING_THREADS; i++) {
            _dbForRace[i] = _jdo.getDatabase();
        }
    }

    public void testRaceCondition() throws Exception {
        _thread = Thread.currentThread();

        for (int i = 0; i < 4; i++) {
            runOnce(i);
        }
    }

    public void tearDown() throws Exception {
        if (_masterDB.isActive()) {
            _masterDB.rollback();
        }
        _masterDB.close();

        for (int i = 0; i < NUM_OF_RACING_THREADS; i++) {
            if (_dbForRace[i].isActive()) {
                _dbForRace[i].rollback();
            }
            _dbForRace[i].close();
        }
    }

    public void runOnce(final int cachetype) throws Exception {
        // clear the table
        Connection conn = _jdo.getConnectionFactory().createConnection();
        Statement stmt = conn.createStatement();
        int del = stmt.executeUpdate("DELETE FROM test06_race");
        stmt.close();
        conn.close();
        LOG.debug("row deleted in table core_race: " + del);

        // create pairs of number
        _masterDB.begin();

        Race[] jdos = new Race[NUM_OF_VALUE_PAIRS];
        RaceSync[] controls = new RaceSync[NUM_OF_VALUE_PAIRS];
        switch (cachetype) {
        case 0:
            _classType = RaceCount.class;
            _className = _classType.getName();
            for (int i = 0; i < jdos.length; i++) {
                jdos[i] = new RaceCount();
                jdos[i].setId(i);
                _masterDB.create(jdos[i]);

                controls[i] = new RaceSync();
            }
            break;
        case 1:
            _classType = RaceTime.class;
            _className = _classType.getName();
            for (int i = 0; i < jdos.length; i++) {
                jdos[i] = new RaceTime();
                jdos[i].setId(i);
                _masterDB.create(jdos[i]);

                controls[i] = new RaceSync();
            }
            break;
        case 2:
            _classType = RaceNone.class;
            _className = _classType.getName();
            for (int i = 0; i < jdos.length; i++) {
                jdos[i] = new RaceNone();
                jdos[i].setId(i);
                _masterDB.create(jdos[i]);

                controls[i] = new RaceSync();
            }
            break;
        case 3:
            _classType = RaceUnlimited.class;
            _className = _classType.getName();
            for (int i = 0; i < jdos.length; i++) {
                jdos[i] = new RaceUnlimited();
                jdos[i].setId(i);
                _masterDB.create(jdos[i]);

                controls[i] = new RaceSync();
            }
            break;
        default:
            LOG.error("Unknown cache type");
        }

        _masterDB.commit();

        // create threads, make a race so each thread
        // keeping increment to the pairs of number.
        RaceThread[] ts = new RaceThread[NUM_OF_RACING_THREADS];
        for (int i = 0; i < ts.length; i++) {
            ts[i] = new RaceThread(this, _dbForRace[i], controls, NUM_OF_TRIALS);
            ts[i].start();
        }

        // wait till everybody done
        boolean isAllDone = false;
        int num;
        while (!isAllDone) {
            Thread.sleep(200);
            num = 0;
            for (int i = 0; i < ts.length; i++) {
                if (ts[i].isDone()) {
                    num++;
                }
            }
            if (num == ts.length) {
                isAllDone = true;
            }
        }

        // see if their sum agree
        _masterDB.begin();

        num = 0;
        for (int i = 0; i < jdos.length; i++) {
            OQLQuery oql = _masterDB.getOQLQuery("SELECT object FROM " + _className
                    + " object WHERE id = $1");
            oql.bind(i);
            Enumeration<?> enumeration = oql.execute();
            if (enumeration.hasMoreElements()) {
                Race tr = (Race) enumeration.nextElement();
                if (tr.getValue1() == controls[i].getValue1()) {
                    num++;
                }
                LOG.debug("Number Pair " + i + " -- JDO: " + tr.getValue1() + " control: "
                        + controls[i].getValue1() + " total trials: " + NUM_OF_TRIALS
                        * NUM_OF_RACING_THREADS);
                LOG.debug("Test passes if Number Pair equals control.");
                LOG.debug("Total trails often not equals control. "
                        + "The reason for this is, that if two thread are "
                        + "interested on the same object only one will "
                        + "get the lock and the other will abort.");
            }
        }

        _masterDB.commit();

        for (int i = 0; i < ts.length; i++) {
            if (ts[i].getException() != null) {
                LOG.error("Fatal exception thrown: ", ts[i].getException());
                fail("Fatal exception thrown");
            }
        }

        // report result
        assertTrue("Value in database does not match the expected value", num == jdos.length);
        assertTrue("Leak detected", !_leak);
    }

    public Thread getMain() {
        return _thread;
    }

    public static String getClassName() {
        return _className;
    }

    public static void setClassName(final String className) {
        _className = className;
    }

    public static Class<?> getClassType() {
        return _classType;
    }

    public static void setClassType(final Class<?> classType) {
        _classType = classType;
    }

    public static boolean isLeak() {
        return _leak;
    }

    public static void setLeak(final boolean leak) {
        _leak = leak;
    }
}
