LockingVisitorsTest.java

/*
 * 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
 *
 *      https://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.commons.lang3.concurrent.locks;

import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.junit.jupiter.api.Assertions.assertNotSame;
import static org.junit.jupiter.api.Assertions.assertSame;
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.junit.jupiter.api.Assertions.assertTrue;

import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReadWriteLock;
import java.util.concurrent.locks.ReentrantLock;
import java.util.concurrent.locks.ReentrantReadWriteLock;
import java.util.concurrent.locks.StampedLock;

import org.apache.commons.lang3.AbstractLangTest;
import org.apache.commons.lang3.concurrent.locks.LockingVisitors.LockVisitor;
import org.apache.commons.lang3.concurrent.locks.LockingVisitors.ReadWriteLockVisitor;
import org.apache.commons.lang3.concurrent.locks.LockingVisitors.ReentrantLockVisitor;
import org.apache.commons.lang3.concurrent.locks.LockingVisitors.StampedLockVisitor;
import org.apache.commons.lang3.function.FailableConsumer;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.ValueSource;

/**
 * Tests {@link LockingVisitors}.
 */
class LockingVisitorsTest extends AbstractLangTest {

    private static final int NUMBER_OF_THREADS = 10;

    private static boolean tryLock(final Lock lock) {
        if (!lock.tryLock()) {
            return false;
        }
        lock.unlock();
        return true;
    }

    private <T> void assertConcurrentReaders(final LockVisitor<T, ?> visitor, final ExecutorService executor) throws Exception {
        final CountDownLatch entered = new CountDownLatch(NUMBER_OF_THREADS);
        final CountDownLatch release = new CountDownLatch(1);
        final List<Future<?>> readers = new ArrayList<>();
        try {
            for (int i = 0; i < NUMBER_OF_THREADS; i++) {
                readers.add(executor.submit(() -> visitor.acceptReadLocked(values -> {
                    entered.countDown();
                    assertTrue(release.await(10, TimeUnit.SECONDS), "Readers released");
                })));
            }
            // All callbacks must enter before any callback is allowed to leave.
            assertTrue(entered.await(10, TimeUnit.SECONDS), "Read callbacks run concurrently");
        } finally {
            release.countDown();
        }
        for (final Future<?> reader : readers) {
            reader.get(10, TimeUnit.SECONDS);
        }
    }

    private <T> void invoke(final LockVisitor<T, ?> visitor, final boolean write, final boolean apply,
            final FailableConsumer<T, Exception> consumer) {
        if (apply) {
            final T result;
            if (write) {
                result = visitor.applyWriteLocked(values -> {
                    consumer.accept(values);
                    return values;
                });
            } else {
                result = visitor.applyReadLocked(values -> {
                    consumer.accept(values);
                    return values;
                });
            }
            assertSame(visitor.getObject(), result);
        } else if (write) {
            visitor.acceptWriteLocked(consumer);
        } else {
            visitor.acceptReadLocked(consumer);
        }
    }

    private <T> void runTest(final boolean write, final boolean exclusive, final LockVisitor<T, ?> visitor,
            final Lock readLock, final Lock writeLock) throws Exception {
        assertNotNull(visitor.getLock());
        assertNotNull(visitor.getObject());
        final ExecutorService executor = Executors.newFixedThreadPool(NUMBER_OF_THREADS);
        try {
            for (final boolean apply : new boolean[] { false, true }) {
                for (final boolean fail : new boolean[] { false, true }) {
                    final AtomicInteger calls = new AtomicInteger();
                    final IllegalStateException failure = new IllegalStateException("Callback failure");
                    final FailableConsumer<T, Exception> consumer = values -> {
                        assertSame(visitor.getObject(), values);
                        calls.incrementAndGet();
                        // A single other thread probes both locks, so probes cannot mask a missing visitor lock.
                        executor.submit(() -> {
                            assertEquals(!exclusive, tryLock(readLock), "Read access during the callback");
                            assertFalse(tryLock(writeLock), "Write access during the callback");
                        }).get(10, TimeUnit.SECONDS);
                        if (fail) {
                            throw failure;
                        }
                    };
                    if (fail) {
                        assertSame(failure, assertThrows(IllegalStateException.class, () -> invoke(visitor, write, apply, consumer)));
                    } else {
                        invoke(visitor, write, apply, consumer);
                    }
                    assertEquals(1, calls.get(), "Callback invoked exactly once");
                    // Probe after both successful and failing callbacks, from a thread that does not own the lock.
                    executor.submit(() -> {
                        assertTrue(tryLock(readLock), "Read lock released after the callback");
                        assertTrue(tryLock(writeLock), "Write lock released after the callback");
                    }).get(10, TimeUnit.SECONDS);
                }
            }
            if (!exclusive) {
                assertConcurrentReaders(visitor, executor);
            }
        } finally {
            executor.shutdownNow();
            assertTrue(executor.awaitTermination(10, TimeUnit.SECONDS));
        }
    }

    @ParameterizedTest
    @ValueSource(booleans = { true, false })
    void testBuilderLockVisitor(final boolean fair) throws Exception {
        final AtomicInteger obj = new AtomicInteger();
        final ReadWriteLock lock = new ReentrantReadWriteLock(fair);
        // @formatter:off
        final LockVisitor<AtomicInteger, ReadWriteLock> lockVisitor = new LockVisitor.LVBuilder()
          .setObject(obj)
          .setLock(lock)
          .setReadLockSupplier(lock::readLock)
          .setWriteLockSupplier(lock::writeLock)
          .get();
        // @formatter:on
        lockVisitor.acceptReadLocked(AtomicInteger::incrementAndGet);
        assertEquals(1, obj.get());
        lockVisitor.acceptReadLocked(AtomicInteger::incrementAndGet);
        assertEquals(2, obj.get());
        runTest(false, false, lockVisitor, lock.readLock(), lock.writeLock());
        runTest(true, true, lockVisitor, lock.readLock(), lock.writeLock());
    }

    @ParameterizedTest
    @ValueSource(booleans = { true, false })
    void testBuilderReadWriteLockVisitor(final boolean fair) throws Exception {
        final AtomicInteger obj = new AtomicInteger();
        final ReadWriteLock lock = new ReentrantReadWriteLock(fair);
        // @formatter:off
        final LockingVisitors.ReadWriteLockVisitor<AtomicInteger> lockVisitor = ReadWriteLockVisitor.<AtomicInteger>builder()
          .setObject(obj)
          .setLock(lock)
          .get();
        // @formatter:on
        lockVisitor.acceptReadLocked(AtomicInteger::incrementAndGet);
        assertEquals(1, obj.get());
        lockVisitor.acceptReadLocked(AtomicInteger::incrementAndGet);
        assertEquals(2, obj.get());
        runTest(false, false, lockVisitor, lock.readLock(), lock.writeLock());
        runTest(true, true, lockVisitor, lock.readLock(), lock.writeLock());
    }

    @ParameterizedTest
    @ValueSource(booleans = { true, false })
    void testBuilderReentrantLockVisitor(final boolean fair) throws Exception {
        final AtomicInteger obj = new AtomicInteger();
        final ReentrantLock lock = new ReentrantLock(fair);
        // @formatter:off
        final LockingVisitors.ReentrantLockVisitor<AtomicInteger> lockVisitor = ReentrantLockVisitor.<AtomicInteger>builder()
          .setObject(obj)
          .setLock(lock)
          .get();
        // @formatter:on
        lockVisitor.acceptReadLocked(AtomicInteger::incrementAndGet);
        assertEquals(1, obj.get());
        lockVisitor.acceptReadLocked(AtomicInteger::incrementAndGet);
        assertEquals(2, obj.get());
        runTest(false, true, lockVisitor, lock, lock);
        runTest(true, true, lockVisitor, lock, lock);
    }

    @ParameterizedTest
    @ValueSource(booleans = { true, false })
    void testBuilderReentrantReadWriteLockVisitor(final boolean fair) throws Exception {
        final AtomicInteger obj = new AtomicInteger();
        final ReentrantReadWriteLock lock = new ReentrantReadWriteLock(fair);
        // @formatter:off
        final LockingVisitors.ReadWriteLockVisitor<AtomicInteger> lockVisitor = ReadWriteLockVisitor.<AtomicInteger>builder()
          .setObject(obj)
          .setLock(lock)
          .get();
        // @formatter:on
        lockVisitor.acceptReadLocked(AtomicInteger::incrementAndGet);
        assertEquals(1, obj.get());
        lockVisitor.acceptReadLocked(AtomicInteger::incrementAndGet);
        assertEquals(2, obj.get());
        runTest(false, false, lockVisitor, lock.readLock(), lock.writeLock());
        runTest(true, true, lockVisitor, lock.readLock(), lock.writeLock());
    }

    @Test
    void testBuilderReentrantStampedLockVisitor() throws Exception {
        final AtomicInteger obj = new AtomicInteger();
        final StampedLock lock = new StampedLock();
        // @formatter:off
        final LockingVisitors.StampedLockVisitor<AtomicInteger> lockVisitor = StampedLockVisitor.<AtomicInteger>builder()
          .setObject(obj)
          .setLock(lock)
          .get();
        // @formatter:on
        lockVisitor.acceptReadLocked(AtomicInteger::incrementAndGet);
        assertEquals(1, obj.get());
        lockVisitor.acceptReadLocked(AtomicInteger::incrementAndGet);
        assertEquals(2, obj.get());
        runTest(false, false, lockVisitor, lock.asReadLock(), lock.asWriteLock());
        runTest(true, true, lockVisitor, lock.asReadLock(), lock.asWriteLock());
    }

    @Test
    void testCreate() {
        final AtomicInteger obj = new AtomicInteger();
        final ReadWriteLock lock = new ReentrantReadWriteLock();
        LockingVisitors.create(obj, lock).acceptReadLocked(AtomicInteger::incrementAndGet);
        LockingVisitors.create(obj, lock).acceptReadLocked(null);
        assertEquals(1, obj.get());
        LockingVisitors.create(obj, lock).acceptWriteLocked(AtomicInteger::incrementAndGet);
        LockingVisitors.create(obj, lock).acceptWriteLocked(null);
        assertEquals(2, obj.get());
    }

    @SuppressWarnings("deprecation")
    @Test
    void testDeprecatedConstructor() {
        assertNotNull(new LockingVisitors().toString());
    }

    @Test
    void testReentrantLock() throws Exception {
        final boolean[] values = new boolean[NUMBER_OF_THREADS];
        final ReentrantLockVisitor<boolean[]> visitor = LockingVisitors.reentrantLockVisitor(values);
        assertSame(values, visitor.getObject());
        runTest(false, true, visitor, visitor.getLock(), visitor.getLock());
        runTest(true, true, visitor, visitor.getLock(), visitor.getLock());
    }

    @ParameterizedTest
    @ValueSource(booleans = { true, false })
    void testReentrantLockFairness(final boolean fairness) throws Exception {
        final boolean[] values = new boolean[NUMBER_OF_THREADS];
        final ReentrantLock lock = new ReentrantLock(fairness);
        final ReentrantLockVisitor<boolean[]> visitor = LockingVisitors.create(values, lock);
        assertSame(values, visitor.getObject());
        runTest(false, true, visitor, lock, lock);
        runTest(true, true, visitor, lock, lock);
    }

    @Test
    void testReentrantReadWriteLockExclusive() throws Exception {
        final boolean[] values = new boolean[NUMBER_OF_THREADS];
        final ReadWriteLockVisitor<boolean[]> visitor = LockingVisitors.reentrantReadWriteLockVisitor(values);
        assertSame(values, visitor.getObject());
        runTest(true, true, visitor, visitor.getLock().readLock(), visitor.getLock().writeLock());
    }

    @Test
    void testReentrantReadWriteLockNotExclusive() throws Exception {
        final boolean[] values = new boolean[NUMBER_OF_THREADS];
        final ReadWriteLockVisitor<boolean[]> visitor = LockingVisitors.reentrantReadWriteLockVisitor(values);
        assertSame(values, visitor.getObject());
        runTest(false, false, visitor, visitor.getLock().readLock(), visitor.getLock().writeLock());
    }

    @Test
    void testResultValidation() {
        final Object hidden = new Object();
        final StampedLockVisitor<Object> lock = LockingVisitors.stampedLockVisitor(hidden);
        final Object o1 = lock.applyReadLocked(h -> new Object());
        assertNotNull(o1);
        assertNotSame(hidden, o1);
        final Object o2 = lock.applyWriteLocked(h -> new Object());
        assertNotNull(o2);
        assertNotSame(hidden, o2);
    }

    @Test
    void testStampedLockExclusive() throws Exception {
        final boolean[] values = new boolean[NUMBER_OF_THREADS];
        final StampedLockVisitor<boolean[]> visitor = LockingVisitors.stampedLockVisitor(values);
        assertSame(values, visitor.getObject());
        runTest(true, true, visitor, visitor.getLock().asReadLock(), visitor.getLock().asWriteLock());
    }

    @Test
    void testStampedLockNotExclusive() throws Exception {
        final boolean[] values = new boolean[NUMBER_OF_THREADS];
        final StampedLockVisitor<boolean[]> visitor = LockingVisitors.stampedLockVisitor(values);
        assertSame(values, visitor.getObject());
        runTest(false, false, visitor, visitor.getLock().asReadLock(), visitor.getLock().asWriteLock());
    }
}