/*
 * Copyright (c) 2010, 2021 Oracle and/or its affiliates. All rights reserved.
 *
 * This program and the accompanying materials are made available under the
 * terms of the Eclipse Public License v. 2.0 which is available at
 * http://www.eclipse.org/legal/epl-2.0,
 * or the Eclipse Distribution License v. 1.0 which is available at
 * http://www.eclipse.org/org/documents/edl-v10.php.
 *
 * SPDX-License-Identifier: EPL-2.0 OR BSD-3-Clause
 */

// Contributors:
//     Chris Delahunt  = 2.2 - 331921: deadlock with uow commit and query using joins
package org.eclipse.persistence.testing.tests.simultaneous;

import java.math.BigDecimal;

import org.eclipse.persistence.testing.framework.AutoVerifyTestCase;
import org.eclipse.persistence.testing.framework.TestErrorException;
import org.eclipse.persistence.testing.tests.unitofwork.ConcurrentLargeProject;
import org.eclipse.persistence.testing.tests.unitofwork.ConcurrentPerson;
import org.eclipse.persistence.testing.tests.unitofwork.ConcurrentAddress;
import org.eclipse.persistence.testing.tests.unitofwork.ConcurrentProject;
import org.eclipse.persistence.sessions.Session;
import org.eclipse.persistence.sessions.server.Server;

import org.eclipse.persistence.sessions.UnitOfWork;

/**
 * Test for Bug 262157
 * This test tests to ensure no deadlock occurs in the following scenario:
 * 1) Thread1 gets deferred lock on LargeProject
 * 2) Thread2 gets required locks on Person+Address for updates
 * 3) Thread1 attempts to get an active lock on Address, but has to wait as it is held by thread2
 * 4) Thread2 updates, commits, and during the merge tries to get a lock on LargeProject.  This causes it to transition to deferred locks.
 * 4b) thread2 attempts to release its deferred lock, causing it to waiting on threadB - a deadlock.
 *
 */
public class ConcurrentReadFetchJoinWithUOWLocksTest extends AutoVerifyTestCase {
    protected ConcurrentPerson person = null;
    protected ConcurrentLargeProject project;

    protected boolean deadlockDetected = false;

    @Override
    public void setup(){
        deadlockDetected = false;
        this.getExecutor().swapServerSession();
        UnitOfWork uow = getSession().acquireUnitOfWork();
        person = (ConcurrentPerson)uow.registerObject(new ConcurrentPerson());
        person.name = "SomeoneSpecial";

        project = (ConcurrentLargeProject)uow.registerObject(new ConcurrentLargeProject());
        project.setName("ConcurrentRFJUOWLock Project");

        ConcurrentAddress address = (ConcurrentAddress)uow.registerObject(new ConcurrentAddress());
        address.setStreet("99 Bank St");
        project.setLocation(address);

        uow.commit();
        uow.release();
    }

    @Override
    public void test(){
        Server server = this.getServerSession();
        UnitOfWork uow = server.acquireUnitOfWork();
        ConcurrentLargeProject clonedProject = (ConcurrentLargeProject)uow.registerObject(project);
        clonedProject.getLocation().setPostalCode("K1P 1A4");
        ConcurrentPerson clonedPerson = (ConcurrentPerson)uow.registerObject(person);
        clonedPerson.setHobby(clonedProject);
        uow.writeChanges();

        Thread thread1 = new Thread(new ProjectReader(server.acquireClientSession(), project.getId()));
        ConcurrentProject.RUNNING_TEST = ConcurrentProject.READ_WITH_UOW_LOCKS_TESTS;
        //start reading Project, and have the thread wait while building DTF mappings
        thread1.start();

        try {
            thread1.join(1000);//wait a token amount to be sure that thread1 isn't starved before getting deferred lock on Project.
        } catch (InterruptedException ex) {
        }

        //start uow commit, which will get locks on Person+Address, commit, then merge.
        //merge should get a deferred lock on Project.
        Thread thread2 = new Thread(new UOWCommit(uow));
        thread2.start();

        //while waiting, thread1 should wake and try to get a lock on Address.  It will deadlock if it
        //tries to get an active lock, since they will be held by the UOW which can't complete until thread1
        //releases.
        try {
            thread1.join(20000);
            if (thread1.isAlive()){
                try{
                    thread1.interrupt();
                    thread2.interrupt();
                }catch (Exception e) {
                }
                deadlockDetected = true;
            }
        } catch (InterruptedException ex) {
        }
    }

    @Override
    public void verify(){
        if (deadlockDetected){
            throw new TestErrorException("Deadlock detected in UnitOfWork when reading a joined 1-1.");
        }
    }

    @Override
    public void reset(){
        ConcurrentProject.RUNNING_TEST = ConcurrentProject.NONE;
        UnitOfWork uow = getSession().acquireUnitOfWork();

        uow.deleteObject(person);
        uow.deleteObject(project);
        uow.deleteObject(project.getLocation());

        uow.commit();
        getSession().getIdentityMapAccessor().initializeAllIdentityMaps();
        this.getExecutor().resetSession();
    }

    //threads:
    public class ProjectReader implements Runnable{
        private Session session;
        private BigDecimal idToUse;

        public ProjectReader(Session session, BigDecimal id){
            this.session = session;
            this.idToUse = id;
        }

        @Override
        public void run(){
            org.eclipse.persistence.queries.ReadAllQuery query = new org.eclipse.persistence.queries.ReadAllQuery(ConcurrentLargeProject.class);
            query.setSelectionCriteria(query.getExpressionBuilder().get("id").equal(idToUse));
            query.addJoinedAttribute("location");
            query.refreshIdentityMapResult();
            session.executeQuery(query);
        }
    }

    public class UOWCommit implements Runnable{
        private UnitOfWork uow;

        public UOWCommit(UnitOfWork uow){
            this.uow = uow;
        }

        @Override
        public void run(){
            uow.commit();
        }
    }

}
