/*
 * 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.jena.reasoner.rulesys.test;

import java.lang.management.ManagementFactory;
import java.lang.management.ThreadInfo;
import java.lang.management.ThreadMXBean;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

import org.apache.jena.atlas.lib.Creator;
import org.apache.jena.ontology.OntClass;
import org.apache.jena.ontology.OntModel;
import org.apache.jena.ontology.OntModelSpec;
import org.apache.jena.ontology.OntResource;
import org.apache.jena.rdf.model.ModelFactory;
import org.apache.jena.rdf.model.StmtIterator;
import org.apache.jena.shared.JenaException;
import org.apache.jena.shared.Lock;
import org.apache.jena.util.PrintUtil;
import org.apache.jena.util.iterator.ExtendedIterator;
import org.junit.Assert;
import junit.framework.TestCase;
import junit.framework.TestSuite;

/**
 * Test for deadlock and concurrency problems in rule engines.
 *
 * <p>Test inspired by suggestions from Timm Linder</p>
 */
public class ConcurrencyTest  extends TestCase {

    // For routine jena tests we do minimal exercise here, otherwise too slow
    // If problems crop up then switch to full tests
    final static boolean FULL_TEST = false;

    // Number of class instances to create in the model under test
    final static int MODEL_SIZE = FULL_TEST ? 100 : 10;

    // Number of threads to create in the tests
    final static int NUM_THREADS = FULL_TEST ? 50 : 20;

    // Length of time to run the threads under test, in ms
    final static int TEST_LENGTH = FULL_TEST ? 3000 : 20;

    // Number of times to run the test cycle
    final static int NUM_RUNS = FULL_TEST ? 30 : 3;

    /**
     * Boilerplate for junit
     */
    public ConcurrencyTest( String name ) {
        super( name );
    }

    /**
     * Boilerplate for junit.
     * This is its own test suite
     */
    public static TestSuite suite() {
        return new TestSuite( ConcurrencyTest.class );
    }

    @SuppressWarnings("removal")
    private void runConcurrencyTest(Creator<OntModel> modelCreator, String runId) throws InterruptedException  {
        try {
            for(int i = 0; i < NUM_RUNS; ++i) {
                doTestConcurrency(modelCreator.create());
            }
        } catch (JenaException e ) {
            assertTrue(e.getMessage(), false);
        }
    }

    @SuppressWarnings("removal")
    private void doTestConcurrency(final OntModel model) throws InterruptedException {
        // initialize the model
        final String NS = PrintUtil.egNS;

        model.enterCriticalSection(Lock.WRITE);
        final OntClass Top = model.createClass(NS + "Top");
        for (int i = 0; i < MODEL_SIZE; i++) {
            OntClass C = model.createClass(NS + "C" + i);
            Top.addSubClass(C);
            model.createIndividual(NS + "i" + i, C);
        }
        model.leaveCriticalSection();

        class QueryExecutingRunnable implements Runnable {
            @Override
            @SuppressWarnings("unchecked")
            public void run() {
                // Keep this thread running until the specified duration has expired
                long runStartedAt = System.currentTimeMillis();
                while(System.currentTimeMillis() - runStartedAt < TEST_LENGTH)
                {
                    Thread.yield();

                    model.enterCriticalSection(Lock.READ);
                    try {
                        // Iterate over all statements
                        StmtIterator it = model.listStatements();

//                        // Debug
//                        List<Statement> s = it.toList();
//                        it = new StmtIteratorImpl(s.iterator());

                        while(it.hasNext()) it.nextStatement();
                        it.close();

                        // Check number of instances of Top class
                        int count = 0;
                        ExtendedIterator<OntResource> ei = (ExtendedIterator<OntResource>) Top.listInstances();
                        while (ei.hasNext()) {
                            ei.next();
                            count++;
                        }
                        ei.close();
                        if (MODEL_SIZE != count) {
                            if (FULL_TEST) System.err.println("Failure - found " + count + " instance, expected " + MODEL_SIZE);
                            throw new JenaException("Failure - found " + count + " instance, expected " + MODEL_SIZE);
                        }
                    }
                    finally {   model.leaveCriticalSection(); }
                }
            }
        }

        // Start the threads
        ExecutorService executorService = Executors.newFixedThreadPool(NUM_THREADS);
        for(int i = 0; i < NUM_THREADS; ++i) {
            executorService.submit(new QueryExecutingRunnable());
        }

        // Wait for threads to finish
        executorService.shutdown(); // this will *not* terminate any threads currently running
        Thread.sleep(TEST_LENGTH + 50);

        // Possibly in deadlock, wait a little longer to be sure
        for(int i = 0; i < 50 && !executorService.isTerminated(); i++) {
            Thread.sleep(20);
        }

        if(!executorService.isTerminated()) {
            /* uncomment this block to perform deadlock checking, only on java 1.6 */
            // Check for deadlock
            ThreadMXBean tmx = ManagementFactory.getThreadMXBean();
            long[] ids = tmx.findDeadlockedThreads();
            if (ids != null) {
                ThreadInfo[] infos = tmx.getThreadInfo(ids, true, true);

                System.err.println("*** Deadlocked threads");
                for (ThreadInfo ti : infos) {
                    System.err.println("Thread \"" + ti.getThreadName() + "\" id=" + ti.getThreadId() + " "
                            + ti.getThreadState().toString());
                    System.err.println("Lock name: " + ti.getLockName() + " owned by \""
                            + ti.getLockOwnerName() + "\" id=" + ti.getLockOwnerId());
                    System.err.println("\nStack trace:");
                    for(StackTraceElement st : ti.getStackTrace())
                        System.err.println("   " + st.getClassName() + "." + st.getMethodName()
                                + " (" + st.getFileName() + ":" + st.getLineNumber() + ")" );
                    System.err.println();
                }
            }
            Assert.assertTrue("Deadlock detected!", false);
            /* end deadlock block */
            assertTrue("Failed to terminate execution", false);
        }
    }

    @SuppressWarnings("removal")
    public void testWithOWLMemMicroRuleInfModel() throws InterruptedException {
        runConcurrencyTest( ()->ModelFactory.createOntologyModel(OntModelSpec.OWL_MEM_MICRO_RULE_INF),
                            "OWL_MEM_MICRO_RULE_INF");
    }
}
