/*
 * 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.distributed;

import java.util.HashSet;
import java.util.Iterator;
import java.util.Set;
import java.util.concurrent.Callable;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicReference;
import java.util.concurrent.locks.Lock;
import javax.cache.Cache;
import org.apache.ignite.Ignite;
import org.apache.ignite.IgniteCache;
import org.apache.ignite.cache.CacheMode;
import org.apache.ignite.cache.affinity.Affinity;
import org.apache.ignite.configuration.CacheConfiguration;
import org.apache.ignite.configuration.IgniteConfiguration;
import org.apache.ignite.internal.IgniteInternalFuture;
import org.apache.ignite.spi.discovery.tcp.TcpDiscoverySpi;
import org.apache.ignite.spi.discovery.tcp.ipfinder.TcpDiscoveryIpFinder;
import org.apache.ignite.spi.discovery.tcp.ipfinder.vm.TcpDiscoveryVmIpFinder;
import org.apache.ignite.testframework.GridTestThread;
import org.apache.ignite.testframework.GridTestUtils;
import org.apache.ignite.testframework.junits.common.GridCommonAbstractTest;
import org.jetbrains.annotations.Nullable;

import static org.apache.ignite.cache.CacheAtomicityMode.TRANSACTIONAL;
import static org.apache.ignite.cache.CacheRebalanceMode.SYNC;
import static org.apache.ignite.cache.CacheWriteSynchronizationMode.FULL_ASYNC;

/**
 * Test cases for multi-threaded tests.
 */
@SuppressWarnings({"FieldCanBeLocal"})
public abstract class GridCacheLockAbstractTest extends GridCommonAbstractTest {
    /** Grid1. */
    private static Ignite ignite1;

    /** Grid2. */
    private static Ignite ignite2;

    /** (for convenience). */
    private static IgniteCache<Integer, String> cache1;

    /** (for convenience). */
    private static IgniteCache<Integer, String> cache2;

    /** Ip-finder. */
    private static TcpDiscoveryIpFinder ipFinder = new TcpDiscoveryVmIpFinder(true);

    /**
     *
     */
    protected GridCacheLockAbstractTest() {
        super(false /*start grid. */);
    }

    /** {@inheritDoc} */
    @Override protected IgniteConfiguration getConfiguration(String igniteInstanceName) throws Exception {
        IgniteConfiguration cfg = super.getConfiguration(igniteInstanceName);

        TcpDiscoverySpi disco = new TcpDiscoverySpi();

        disco.setIpFinder(ipFinder);

        cfg.setDiscoverySpi(disco);

        cfg.setCacheConfiguration(cacheConfiguration());

        return cfg;
    }

    /**
     * @return Cache configuration.
     */
    protected CacheConfiguration cacheConfiguration() {
        CacheConfiguration cacheCfg = defaultCacheConfiguration();

        cacheCfg.setCacheMode(cacheMode());
        cacheCfg.setWriteSynchronizationMode(FULL_ASYNC);
        cacheCfg.setRebalanceMode(SYNC);
        cacheCfg.setAtomicityMode(TRANSACTIONAL);

        return cacheCfg;
    }

    /**
     * @return Cache mode.
     */
    protected abstract CacheMode cacheMode();

    /** {@inheritDoc} */
    @Override protected void beforeTestsStarted() throws Exception {
        ignite1 = startGrid(1);
        ignite2 = startGrid(2);

        cache1 = ignite1.cache(DEFAULT_CACHE_NAME);
        cache2 = ignite2.cache(DEFAULT_CACHE_NAME);
    }

    /** {@inheritDoc} */
    @Override protected void afterTestsStopped() throws Exception {
        stopAllGrids();
    }

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

        info("Executing afterTest() callback...");

        info("Before 1st removeAll().");

        cache1.clear();
        cache1.removeAll();

        // Fix for tests where mapping was removed at primary node
        // but was not removed at others.
        // removeAll() removes mapping only when it presents at a primary node.
        // To remove all mappings used force remove by key.
        if (cache1.size() > 0) {
            Iterator<Cache.Entry<Integer, String>> it = cache1.iterator();
            while (it.hasNext())
                cache1.remove(it.next().getKey());
        }

        info("Before 2nd removeAll().");

        cache2.clear();
        cache2.removeAll();

        // Fix for tests where mapping was removed at primary node
        // but was not removed at others.
        // removeAll() removes mapping only when it presents at a primary node.
        // To remove all mappings used force remove by key.
        if (cache2.size() > 0) {
            Iterator<Cache.Entry<Integer, String>> it = cache2.iterator();
            while (it.hasNext())
                cache2.remove(it.next().getKey());
        }

        assert cache1.size() == 0 : "Cache is not empty: " + cache1;
        assert cache2.size() == 0 : "Cache is not empty: " + cache2;
    }

    /**
     * @return Partitioned flag.
     */
    protected boolean isPartitioned() {
        return false;
    }

    /**
     * @param k Key to check.
     * @param idx Grid index.
     * @return {@code True} if locked.
     */
    private boolean locked(Integer k, int idx) {
        if (isPartitioned())
            return near(idx).isLockedNearOnly(k);

        return jcache(idx).isLocalLocked(k, false);
    }

    /**
     * @param keys Keys to check.
     * @param idx Grid index.
     * @return {@code True} if locked.
     */
    private boolean locked(Iterable<Integer> keys, int idx) {
        if (isPartitioned())
            return near(idx).isAllLockedNearOnly(keys);

        for (Integer key : keys) {
            if (!jcache(idx).isLocalLocked(key, true))
                return false;
        }

        return true;
    }

    /**
     * @throws Exception If test failed.
     */
    @SuppressWarnings({"TooBroadScope"})
    public void testLockSingleThread() throws Exception {
        int k = 1;
        String v = String.valueOf(k);

        info("Before lock for key: " + k);

        Lock lock = cache1.lock(k);

        lock.lock();

        info("After lock for key: " + k);

        try {
            assert cache1.isLocalLocked(k, false);
            assert cache1.isLocalLocked(k, true);

            // Put to cache.
            cache1.put(k, v);

            info("Put " + k + '=' + k + " key pair into cache.");
        }
        finally {
            lock.unlock();

            info("Unlocked key: " + k);
        }

        assert !locked(k, 1);
        assert !cache1.isLocalLocked(k, true);
    }

    /**
     * @throws Exception If test failed.
     */
    @SuppressWarnings({"TooBroadScope"})
    public void testLock() throws Exception {
        final int kv = 1;

        final CountDownLatch l1 = new CountDownLatch(1);
        final CountDownLatch l2 = new CountDownLatch(1);

        final Lock lock = cache1.lock(kv);

        GridTestThread t1 = new GridTestThread(new Callable<Object>() {
            @Nullable @Override public Object call() throws Exception {
                info("Before lock for key: " + kv);

                lock.lock();

                info("After lock for key: " + kv);

                try {
                    assert cache1.isLocalLocked(kv, false);
                    assert cache1.isLocalLocked(kv, true);

                    l1.countDown();

                    info("Let thread2 proceed.");

                    cache1.put(kv, Integer.toString(kv));

                    info("Put " + kv + '=' + Integer.toString(kv) + " key pair into cache.");
                }
                finally {
                    Thread.sleep(1000);

                    lock.unlock();

                    info("Unlocked key in thread 1: " + kv);
                }

                l2.await();

                assert !cache1.isLocalLocked(kv, true);
                assert !locked(kv, 1);

                return null;
            }
        });

        GridTestThread t2 = new GridTestThread(new Callable<Object>() {
            @Nullable @Override public Object call() throws Exception {
                info("Waiting for latch1...");

                l1.await();

                lock.lock();

                try {
                    String v = cache2.get(kv);

                    assert v != null : "Value is null for key: " + kv;

                    assertEquals(Integer.toString(kv), v);
                }
                finally {
                    lock.unlock();

                    info("Unlocked key in thread 2: " + kv);
                }

                assert !locked(kv, 2);
                assert !cache2.isLocalLocked(kv, true);

                Thread.sleep(1000);

                l2.countDown();

                return null;
            }
        });

        t1.start();
        t2.start();

        t1.join();
        t2.join();

        t1.checkError();
        t2.checkError();
    }

    /**
     * @throws Exception If test failed.
     */
    public void testLockAndPut() throws Exception {
        final CountDownLatch l1 = new CountDownLatch(1);
        final CountDownLatch l2 = new CountDownLatch(1);

        GridTestThread t1 = new GridTestThread(new Callable<Object>() {
            @Nullable @Override public Object call() throws Exception {
                Lock lock = cache1.lock(1);

                lock.lock();

                info("Locked cache key: 1");

                try {
                    assert cache1.isLocalLocked(1, false);
                    assert cache1.isLocalLocked(1, true);

                    info("Verified that cache key is locked: 1");

                    cache1.put(1, "1");

                    info("Put key value pair into cache: 1='1'");

                    l1.countDown();

                    info("Released latch1");

                    // Hold lock for a bit.
                    Thread.sleep(50);

                    info("Woke up from sleep.");
                }
                finally {
                    lock.unlock();

                    info("Unlocked cache key: 1");
                }

                l2.await();

                assert !locked(1, 1);
                assert !cache1.isLocalLocked(1, true);

                return null;
            }
        });

        GridTestThread t2 = new GridTestThread(new Callable<Object>() {
            @Nullable @Override public Object call() throws Exception {
                info("Beginning to await on latch 1");

                l1.await();

                info("Finished awaiting on latch 1");

                assertEquals("1", cache1.get(1));

                info("Retrieved value from cache for key: 1");

                cache1.put(1, "2");

                info("Put key-value pair into cache: 1='2'");

                assertEquals("2", cache1.getAndRemove(1));

                l2.countDown();

                info("Removed key from cache: 1");

                return null;
            }
        });

        t1.start();
        t2.start();

        t1.join();
        t2.join();

        t1.checkError();
        t2.checkError();
    }

    /**
     * @throws Exception If test failed.
     */
    @SuppressWarnings({"TooBroadScope"})
    public void testLockTimeoutTwoThreads() throws Exception {
        int keyCnt = 1;

        final Set<Integer> keys = new HashSet<>();

        for (int i = 1; i <= keyCnt; i++)
            keys.add(i);

        final CountDownLatch l1 = new CountDownLatch(1);
        final CountDownLatch l2 = new CountDownLatch(1);

        IgniteInternalFuture<?> fut1 = GridTestUtils.runMultiThreadedAsync(new Callable<Object>() {
                @Nullable @Override public Object call() throws Exception {
                    info("Before lock for keys.");

                    Lock lock = cache1.lockAll(keys);

                    lock.lock();

                    info("After lock for keys.");

                    try {
                        for (Integer key : keys) {
                            assert cache1.isLocalLocked(key, false);
                            assert cache1.isLocalLocked(key, true);
                        }

                        l1.countDown();

                        info("Let thread2 proceed.");

                        for (int i : keys) {
                            info("Before put key: " + i);

                            cache1.put(i, Integer.toString(i));

                            if (i % 50 == 0)
                                info("Stored key pairs in cache: " + i);
                        }
                    }
                    finally {
                        l2.await();

                        info("Before unlock keys in thread 1: " + keys);

                        lock.unlock();

                        info("Unlocked entry for keys.");
                    }

                    assert !locked(keys, 1);

                    return null;
                }
            }, 1, "TEST-THREAD-1");

        IgniteInternalFuture<?> fut2 = GridTestUtils.runMultiThreadedAsync(new Callable<Object>() {
                @Nullable @Override public Object call() throws Exception {
                    info("Waiting for latch1...");

                    try {
                        l1.await();

                        // This call should not acquire the lock since
                        // other thread is holding it.
                        assert !cache1.lockAll(keys).tryLock();

                        info("Before unlock keys in thread 2: " + keys);

                        GridTestUtils.assertThrows(null, new Callable<Object>() {
                            @Override public Object call() throws Exception {
                                cache1.lockAll(keys).unlock();
                                return null;
                            }
                        }, IllegalStateException.class, null);

                        // The keys should still be locked.
                        for (Integer key : keys)
                            assert cache1.isLocalLocked(key, false);
                    }
                    finally {
                        l2.countDown();
                    }

                    return null;
                }
            }, 1, "TEST-THREAD-2");

        fut1.get();
        fut2.get();
    }

    /**
     * @throws Throwable If failed.
     */
    public void testLockReentrancy() throws Throwable {
        Affinity<Integer> aff = ignite1.affinity(DEFAULT_CACHE_NAME);

        for (int i = 10; i < 100; i++) {
            log.info("Test lock [key=" + i +
                ", primary=" + aff.isPrimary(ignite1.cluster().localNode(), i) +
                ", backup=" + aff.isBackup(ignite1.cluster().localNode(), i) + ']');

            final int i0 = i;

            final Lock lock = cache1.lock(i);

            lock.lockInterruptibly();

            try {
                final AtomicReference<Throwable> err = new AtomicReference<>();

                Thread t =  new Thread(new Runnable() {
                    @Override public void run() {
                        try {
                            assert !lock.tryLock();
                            assert !lock.tryLock(100, TimeUnit.MILLISECONDS);

                            assert !cache1.lock(i0).tryLock();
                            assert !cache1.lock(i0).tryLock(100, TimeUnit.MILLISECONDS);
                        }
                        catch (Throwable e) {
                            err.set(e);
                        }
                    }
                });

                t.start();
                t.join();

                if (err.get() != null)
                    throw err.get();

                lock.lock();
                lock.unlock();

                t =  new Thread(new Runnable() {
                    @Override public void run() {
                        try {
                            assert !lock.tryLock();
                            assert !lock.tryLock(100, TimeUnit.MILLISECONDS);

                            assert !cache1.lock(i0).tryLock();
                            assert !cache1.lock(i0).tryLock(100, TimeUnit.MILLISECONDS);
                        }
                        catch (Throwable e) {
                            err.set(e);
                        }
                    }
                });

                t.start();
                t.join();

                if (err.get() != null)
                    throw err.get();
            }
            finally {
                lock.unlock();
            }
        }
    }
}
