/*
 * TestPessimisticLocking.java
 *
 * Created on October 13, 2006, 3:17 PM
 *
 * To change this template, choose Tools | Template Manager
 * and open the template in the editor.
 */

/*
 * 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.openjpa.persistence.kernel;

import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.locks.ReentrantLock;

import org.apache.openjpa.jdbc.conf.JDBCConfiguration;
import org.apache.openjpa.persistence.OpenJPAEntityManager;
import org.apache.openjpa.persistence.OpenJPAEntityManagerFactory;
import org.apache.openjpa.persistence.jdbc.FetchMode;
import org.apache.openjpa.persistence.jdbc.JDBCFetchPlan;
import org.apache.openjpa.persistence.kernel.common.apps.RuntimeTest1;
import org.apache.openjpa.persistence.kernel.common.apps.RuntimeTest2;
import org.apache.openjpa.persistence.kernel.common.apps.RuntimeTest3;

public class TestPessimisticLocking extends BaseKernelTest {

    private Object _id = null;
    private int _bugCount = 0;
    private OpenJPAEntityManagerFactory _factory = null;

    public TestPessimisticLocking(String name) {
        super(name);
    }

    /**
     * Creates a new instance of TestPessimisticLocking
     */
    public TestPessimisticLocking() {
    }

    protected boolean skipTest() {
        if (getConfiguration() instanceof JDBCConfiguration) {
            JDBCConfiguration conf = (JDBCConfiguration) getConfiguration();
            return !conf.getDBDictionaryInstance().supportsSelectForUpdate;
        }
        return false;
    }

    /**
     * Use a locking persistence manager with subclass fetch mode to set to
     * "none" to avoid subclass joins.
     */
    protected OpenJPAEntityManager getLockingPM() {
        OpenJPAEntityManager pm = _factory.createEntityManager();
        ((JDBCFetchPlan) pm.getFetchPlan())
            .setSubclassFetchMode(FetchMode.NONE);
        return pm;
    }

    @Override
    public void setUp() throws Exception {
        super.setUp(RuntimeTest1.class, RuntimeTest2.class, RuntimeTest3.class);

        Map propsMap = new HashMap();
        propsMap.put("openjpa.LockManager", "pessimistic");
        _factory = getEmf(propsMap);

        OpenJPAEntityManager pm = getLockingPM();
        startTx(pm);

        RuntimeTest1 a = new RuntimeTest1("name", 0);
        pm.persist(a);
        _id = pm.getObjectId(a);

        endTx(pm);
        endEm(pm);
    }

    @Override
    public void tearDown() throws Exception {
        try {
            if (_factory != null) {
                _factory.close();
                _factory = null;
            }
        } catch (Exception e) {
        }
        super.tearDown();
    }

    /**
     * Test that pessimistic locking is working in the data store.
     */
    public void testPessimisticLocking() throws Throwable {
        pessimisticLockingTest(false);
    }

    /**
     * Test that the test case itself is working be using a ReentrantLock. This
     * test will validate that the test case itself is working correctly, not
     * that the datastore's pessimistic locking is working.
     */
    public void testPessimisticLockingInternal() throws Throwable {
        pessimisticLockingTest(true);
    }

    /**
     * Test that pessimistic locking is working by attempting to update the same
     * object in the data store.
     *
     * @param useReentrantLock true if we want to synchronize on a lock instead
     * of relying on the data store (used for validating the test case).
     */
    public void pessimisticLockingTest(boolean useReentrantLock)
        throws Throwable {
        if (skipTest()) {
            return;
        }

        long timeout = System.currentTimeMillis() + (60 * 5 * 1000);

        ReentrantLock lock = null;
        if (useReentrantLock)
            lock = new ReentrantLock();

        TestThread t1 = new TestThread(lock);
        TestThread t2 = new TestThread(lock);
        t1.start();
        t2.start();

        getLog().trace("started thread");

        // wait for threads to die or timeout
        while ((t1.isAlive() || t2.isAlive())
            && System.currentTimeMillis() < timeout) {
            Thread.sleep(1000);
            getLog().trace(
                "thread waiting for completion ("
                    + (timeout - System.currentTimeMillis())
                    + " ms left)");
        }

        getLog().trace("checking if thread is alive");
        System.out.flush();

        if (t1.isAlive() || t2.isAlive()) {
            getLog().trace("thread is still alive");
            System.out.flush();

            // do out best to clean them up
            try {
                t1.interrupt();
            }
            catch (Exception e) {
            }
            try {
                t2.interrupt();
            }
            catch (Exception e) {
            }

            throw new Exception("Thread did not complete after timeout ("
                + timeout + "): possible deadlock");
        }

        getLog().trace("checking exception for t1");
        if (t1.exception != null)
            throw t1.exception;

        getLog().trace("checking exception for t2");
        if (t2.exception != null)
            throw t2.exception;

        getLog().trace("verifying pessimistic locking worked...");
        OpenJPAEntityManager pm = getLockingPM();
        RuntimeTest1 a = pm.find(RuntimeTest1.class, _id);
        assertEquals(20 - _bugCount, a.getIntField1());
        getLog().trace("closing pm");
        endEm(pm);
        getLog().trace("done");
    }

    /**
     * Update thread that tries to increment an int field.
     *
     * @author <a href="mailto:marc@solarmetric.com">Marc Prud'hommeaux</a>
     */
    private class TestThread extends Thread {

        private OpenJPAEntityManager _pm = getLockingPM();

        public Exception exception = null;

        private final ReentrantLock _lock;

        /**
         * Constructor
         *
         * @param lock the ReentrantLock we should use, or null to rely on
         * pessimistic locking in the data store.
         */
        public TestThread(ReentrantLock lock) {
            this._lock = lock;
        }

        @Override
        public synchronized void run() {
            getLog().trace(
                Thread.currentThread().getName()
                    + ": starting update thread");
            try {
                for (int i = 0; i < 10; i++) {
                    if (_lock != null)
                        _lock.lock();

                    try {
                        _pm.setOptimistic(false);
                        startTx(_pm);
                        RuntimeTest1 a = (RuntimeTest1) _pm.find(
                            RuntimeTest1.class, _id);
                        getLog().trace(
                            Thread.currentThread().getName()
                                + ": obtained and locked: " + a);
                        Thread.yield();
                        super.wait(50);
                        getLog().trace(
                            Thread.currentThread().getName()
                                + ": updating age from "
                                + a.getIntField1());
                        a.setIntField1(a.getIntField1() + 1);
                        getLog().trace(
                            Thread.currentThread().getName()
                                + ": committed update");
                        try {
                            _pm.flush();
                            endTx(_pm);
                        }
                        catch (Exception ex) {
                            throw new org.apache.openjpa.util.UserException(
                                "Optimistic lock probably failed after "
                                    + i + " iterations ("
                                    + Thread.currentThread().getName()
                                    + ")", ex);
                        }
                        Thread.yield();
                    }
                    finally {
                        if (_lock != null)
                            _lock.unlock();
                    }
                }
            }
            catch (Exception e) {
                exception = e;
            }
        }
    }

}
