/*
 * 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.ignite.internal.processors.cache;

import java.util.concurrent.Callable;
import java.util.concurrent.CyclicBarrier;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.atomic.AtomicReference;
import org.apache.ignite.Ignite;
import org.apache.ignite.IgniteCache;
import org.apache.ignite.IgniteCheckedException;
import org.apache.ignite.cache.CacheAtomicityMode;
import org.apache.ignite.configuration.CacheConfiguration;
import org.apache.ignite.configuration.TransactionConfiguration;
import org.apache.ignite.internal.IgniteEx;
import org.apache.ignite.internal.IgniteInternalFuture;
import org.apache.ignite.internal.IgniteInterruptedCheckedException;
import org.apache.ignite.internal.TransactionsMXBeanImpl;
import org.apache.ignite.internal.processors.cache.distributed.dht.preloader.GridDhtPartitionsExchangeFuture;
import org.apache.ignite.internal.util.lang.GridAbsPredicate;
import org.apache.ignite.internal.util.typedef.G;
import org.apache.ignite.internal.util.typedef.internal.U;
import org.apache.ignite.mxbean.TransactionsMXBean;
import org.apache.ignite.testframework.GridTestUtils;
import org.apache.ignite.testframework.junits.common.GridCommonAbstractTest;
import org.apache.ignite.transactions.Transaction;
import org.apache.ignite.transactions.TransactionRollbackException;
import org.apache.ignite.transactions.TransactionTimeoutException;
import org.junit.Test;

import static org.apache.ignite.internal.util.typedef.X.hasCause;
import static org.apache.ignite.transactions.TransactionConcurrency.PESSIMISTIC;
import static org.apache.ignite.transactions.TransactionIsolation.REPEATABLE_READ;

/**
 *
 */
public class SetTxTimeoutOnPartitionMapExchangeTest extends GridCommonAbstractTest {
    /** Wait condition timeout. */
    private static final long WAIT_CONDITION_TIMEOUT = 10_000L;

    /** {@inheritDoc} */
    @Override protected void afterTest() throws Exception {
        super.afterTest();

        stopAllGrids();
    }

    /**
     *
     */
    @Test
    public void testDefaultTxTimeoutOnPartitionMapExchange() throws Exception {
        IgniteEx ig1 = startGrid(1);
        IgniteEx ig2 = startGrid(2);

        TransactionConfiguration txCfg1 = ig1.configuration().getTransactionConfiguration();
        TransactionConfiguration txCfg2 = ig2.configuration().getTransactionConfiguration();

        final long expDfltTimeout = TransactionConfiguration.TX_TIMEOUT_ON_PARTITION_MAP_EXCHANGE;

        assertEquals(expDfltTimeout, txCfg1.getTxTimeoutOnPartitionMapExchange());
        assertEquals(expDfltTimeout, txCfg2.getTxTimeoutOnPartitionMapExchange());
    }

    /**
     *
     */
    @Test
    public void testJmxSetTxTimeoutOnPartitionMapExchange() throws Exception {
        startGrid(1);
        startGrid(2);

        TransactionsMXBean mxBean1 = txMXBean(1);
        TransactionsMXBean mxBean2 = txMXBean(2);

        final long expTimeout1 = 20_000L;
        final long expTimeout2 = 30_000L;

        mxBean1.setTxTimeoutOnPartitionMapExchange(expTimeout1);
        assertTxTimeoutOnPartitionMapExchange(expTimeout1);
        assertEquals(expTimeout1, mxBean1.getTxTimeoutOnPartitionMapExchange());

        mxBean2.setTxTimeoutOnPartitionMapExchange(expTimeout2);
        assertTxTimeoutOnPartitionMapExchange(expTimeout2);
        assertEquals(expTimeout2, mxBean2.getTxTimeoutOnPartitionMapExchange());
    }

    /**
     *
     */
    @Test
    public void testClusterSetTxTimeoutOnPartitionMapExchange() throws Exception {
        Ignite ig1 = startGrid(1);
        Ignite ig2 = startGrid(2);

        final long expTimeout1 = 20_000L;
        final long expTimeout2 = 30_000L;

        ig1.cluster().setTxTimeoutOnPartitionMapExchange(expTimeout1);
        assertTxTimeoutOnPartitionMapExchange(expTimeout1);

        ig2.cluster().setTxTimeoutOnPartitionMapExchange(expTimeout2);
        assertTxTimeoutOnPartitionMapExchange(expTimeout2);
    }

    /**
     * Tests applying new txTimeoutOnPartitionMapExchange while an exchange future runs.
     *
     * @throws Exception If fails.
     */
    @Test
    public void testSetTxTimeoutDuringPartitionMapExchange() throws Exception {
        IgniteEx ig = (IgniteEx) startGrids(2);

        checkSetTxTimeoutDuringPartitionMapExchange(ig);
    }

    /**
     * Tests applying new txTimeoutOnPartitionMapExchange while an exchange future runs on client node.
     *
     * @throws Exception If fails.
     */
    @Test
    public void testSetTxTimeoutOnClientDuringPartitionMapExchange() throws Exception {
        IgniteEx ig = startGrids(2);
        IgniteEx client = startClientGrid(getConfiguration("client"));

        checkSetTxTimeoutDuringPartitionMapExchange(client);
    }

    /**
     * @param ig Ignite instance where deadlock tx will start.
     * @throws Exception If fails.
     */
    private void checkSetTxTimeoutDuringPartitionMapExchange(IgniteEx ig) throws Exception {
        final long longTimeout = 600_000L;
        final long shortTimeout = 5_000L;

        TransactionsMXBean mxBean = txMXBean(0);

        // Case 1: set very long txTimeoutOnPME, transaction should be rolled back.
        mxBean.setTxTimeoutOnPartitionMapExchange(longTimeout);
        assertTxTimeoutOnPartitionMapExchange(longTimeout);

        AtomicReference<Exception> txEx = new AtomicReference<>();

        IgniteInternalFuture<Long> fut = startDeadlock(ig, txEx, 0);

        startGridAsync(2);

        waitForExchangeStarted(ig);

        mxBean.setTxTimeoutOnPartitionMapExchange(shortTimeout);

        awaitPartitionMapExchange();

        fut.get();

        assertTrue("Transaction should be rolled back", hasCause(txEx.get(), TransactionRollbackException.class));

        // Case 2: txTimeoutOnPME will be set to 0 after starting of PME, transaction should be cancelled on timeout.
        mxBean.setTxTimeoutOnPartitionMapExchange(longTimeout);
        assertTxTimeoutOnPartitionMapExchange(longTimeout);

        fut = startDeadlock(ig, txEx, 10000L);

        startGridAsync(3);

        waitForExchangeStarted(ig);

        mxBean.setTxTimeoutOnPartitionMapExchange(0);

        fut.get();

        assertTrue("Transaction should be canceled on timeout", hasCause(txEx.get(), TransactionTimeoutException.class));
    }

    /**
     * Start test deadlock
     *
     * @param ig Ig.
     * @param txEx Atomic reference to transaction exception.
     * @param timeout Transaction timeout.
     */
    private IgniteInternalFuture<Long> startDeadlock(Ignite ig, AtomicReference<Exception> txEx, long timeout)
        throws InterruptedException {
        // Without the awaiting txs can map on different topology versions causing unexpected behavior.
        awaitPartitionMapExchange();

        IgniteCache<Object, Object> cache = ig.getOrCreateCache(new CacheConfiguration<>(DEFAULT_CACHE_NAME)
                .setAtomicityMode(CacheAtomicityMode.TRANSACTIONAL));

        AtomicInteger thCnt = new AtomicInteger();

        CyclicBarrier barrier = new CyclicBarrier(2);

        return GridTestUtils.runMultiThreadedAsync(new Callable<Void>() {
            @Override public Void call() {
                int thNum = thCnt.incrementAndGet();

                try (Transaction tx = ig.transactions().txStart(PESSIMISTIC, REPEATABLE_READ, timeout, 0)) {
                    cache.put(thNum, 1);

                    barrier.await();

                    cache.put(thNum % 2 + 1, 1);

                    tx.commit();
                }
                catch (Exception e) {
                    txEx.set(e);
                }

                return null;
            }
        }, 2, "tx-thread");
    }

    /**
     * Starts grid asynchronously and returns just before grid starting.
     * Avoids blocking on PME.
     *
     * @param idx Test grid index.
     * @throws Exception If fails.
     */
    private void startGridAsync(int idx) throws Exception {
        GridTestUtils.runAsync(new Runnable() {
            @Override public void run() {
                try {
                    startGrid(idx);
                }
                catch (Exception ignore) {
                    // no-op.
                }
            }
        });
    }

    /**
     * Waits for srarting PME on grid.
     *
     * @param ig Ignite grid.
     * @throws IgniteCheckedException If fails.
     */
    private void waitForExchangeStarted(IgniteEx ig) throws IgniteCheckedException {
        GridTestUtils.waitForCondition(new GridAbsPredicate() {
            @Override public boolean apply() {
                for (GridDhtPartitionsExchangeFuture fut: ig.context().cache().context().exchange().exchangeFutures()) {
                    if (!fut.isDone())
                        return true;
                }

                return false;
            }
        }, WAIT_CONDITION_TIMEOUT);

        // Additional waiting to ensure that code really start waiting for partition release.
        U.sleep(5_000L);
    }

    /** */
    private TransactionsMXBean txMXBean(int igniteInt) throws Exception {
        return getMxBean(getTestIgniteInstanceName(igniteInt), "Transactions",
            TransactionsMXBeanImpl.class, TransactionsMXBean.class);
    }

    /**
     * Checking the transaction timeout on all grids.
     *
     * @param expTimeout Expected timeout.
     * @throws IgniteInterruptedCheckedException If failed.
     */
    private void assertTxTimeoutOnPartitionMapExchange(final long expTimeout)
        throws IgniteInterruptedCheckedException {

        assertTrue(GridTestUtils.waitForCondition(new GridAbsPredicate() {
            @Override public boolean apply() {
                for (Ignite ignite : G.allGrids()) {
                    long actualTimeout = ignite.configuration()
                        .getTransactionConfiguration().getTxTimeoutOnPartitionMapExchange();

                    if (actualTimeout != expTimeout) {
                        log.warning(String.format(
                            "Wrong transaction timeout on partition map exchange [grid=%s, timeout=%d, expected=%d]",
                            ignite.name(), actualTimeout, expTimeout));

                        return false;
                    }
                }

                return true;

            }
        }, WAIT_CONDITION_TIMEOUT));
    }
}
