| // Copyright 2015 The Bazel Authors. All rights reserved. | |
| // | |
| // Licensed 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 com.google.devtools.build.lib.concurrent; | |
| import static com.google.common.truth.Truth.assertThat; | |
| import static com.google.common.truth.Truth.assertWithMessage; | |
| import static java.util.concurrent.TimeUnit.SECONDS; | |
| import static org.junit.Assert.assertThrows; | |
| import com.google.common.base.Preconditions; | |
| import com.google.common.util.concurrent.MoreExecutors; | |
| import com.google.devtools.build.lib.concurrent.KeyedLocker.AutoUnlocker; | |
| import com.google.devtools.build.lib.concurrent.KeyedLocker.AutoUnlocker.IllegalUnlockException; | |
| import com.google.devtools.build.lib.testutil.TestUtils; | |
| import java.util.HashSet; | |
| import java.util.Set; | |
| import java.util.concurrent.CountDownLatch; | |
| import java.util.concurrent.ExecutorService; | |
| import java.util.concurrent.Executors; | |
| import java.util.concurrent.atomic.AtomicBoolean; | |
| import java.util.concurrent.atomic.AtomicInteger; | |
| import java.util.concurrent.atomic.AtomicReference; | |
| import java.util.function.Supplier; | |
| import org.junit.After; | |
| import org.junit.Before; | |
| import org.junit.Test; | |
| import org.junit.runner.RunWith; | |
| import org.junit.runners.JUnit4; | |
| /** Tests for {@link StripedKeyedLocker}. */ | |
| @RunWith(JUnit4.class) | |
| public final class StripedKeyedLockerTest { | |
| private static final int NUM_EXECUTOR_THREADS = 1000; | |
| private KeyedLocker<String> locker; | |
| private ExecutorService executorService; | |
| private final AtomicReference<Throwable> throwableFromRunnable = new AtomicReference<>(); | |
| private KeyedLocker<String> makeFreshLocker() { | |
| return new StripedKeyedLocker<>(17); | |
| } | |
| @Before | |
| public final void setUp() { | |
| locker = makeFreshLocker(); | |
| executorService = Executors.newFixedThreadPool(NUM_EXECUTOR_THREADS); | |
| } | |
| @After | |
| public final void shutdownExecutor() { | |
| locker = null; | |
| MoreExecutors.shutdownAndAwaitTermination( | |
| executorService, TestUtils.WAIT_TIMEOUT_SECONDS, SECONDS); | |
| if (throwableFromRunnable.get() != null) { | |
| throw new RuntimeException("Uncaught from thread", throwableFromRunnable.get()); | |
| } | |
| } | |
| private Supplier<AutoUnlocker> makeLockInvoker(String key) { | |
| return () -> locker.writeLock(key); | |
| } | |
| private Supplier<AutoUnlocker> makeLockFn1() { | |
| return makeLockInvoker("1"); | |
| } | |
| private Supplier<AutoUnlocker> makeLockFn2() { | |
| return makeLockInvoker("2"); | |
| } | |
| @SuppressWarnings("ReturnValueIgnored") | |
| @Test | |
| public void simpleSingleThreaded_noUnlocks() { | |
| Supplier<AutoUnlocker> lockFn1 = makeLockFn1(); | |
| Supplier<AutoUnlocker> lockFn2 = makeLockFn2(); | |
| lockFn1.get(); | |
| lockFn2.get(); | |
| lockFn1.get(); | |
| lockFn2.get(); | |
| } | |
| @Test | |
| public void simpleSingleThreaded_withUnlocks() { | |
| Supplier<AutoUnlocker> lockFn1 = makeLockFn1(); | |
| Supplier<AutoUnlocker> lockFn2 = makeLockFn2(); | |
| try (AutoUnlocker unlockerCat1 = lockFn1.get(); | |
| AutoUnlocker unlockerDog1 = lockFn2.get(); | |
| AutoUnlocker unlockerCat2 = lockFn1.get(); | |
| AutoUnlocker unlockerDog2 = lockFn2.get()) { | |
| // Do nothing. | |
| } | |
| } | |
| @Test | |
| public void doubleUnlockOnSameAutoUnlockerNotAllowed() { | |
| AutoUnlocker unlocker = makeLockFn1().get(); | |
| unlocker.close(); | |
| assertThrows(IllegalUnlockException.class, unlocker::close); | |
| } | |
| @Test | |
| public void unlockOnDifferentAutoUnlockersAllowed() { | |
| Supplier<AutoUnlocker> lockFn = makeLockFn1(); | |
| AutoUnlocker unlocker1 = lockFn.get(); | |
| AutoUnlocker unlocker2 = lockFn.get(); | |
| unlocker1.close(); | |
| unlocker2.close(); | |
| } | |
| @Test | |
| public void threadLocksMultipleTimesBeforeUnlocking() { | |
| Supplier<AutoUnlocker> lockFn = makeLockFn1(); | |
| AtomicReference<Long> currentThreadIdRef = new AtomicReference<>(-1L); | |
| AtomicInteger count = new AtomicInteger(0); | |
| Runnable runnable = | |
| () -> { | |
| Long currentThreadId = Thread.currentThread().getId(); | |
| try (AutoUnlocker unlocker1 = lockFn.get()) { | |
| currentThreadIdRef.set(currentThreadId); | |
| try (AutoUnlocker unlocker2 = lockFn.get()) { | |
| assertThat(currentThreadIdRef.get()).isEqualTo(currentThreadId); | |
| try (AutoUnlocker unlocker3 = lockFn.get()) { | |
| assertThat(currentThreadIdRef.get()).isEqualTo(currentThreadId); | |
| try (AutoUnlocker unlocker4 = lockFn.get()) { | |
| assertThat(currentThreadIdRef.get()).isEqualTo(currentThreadId); | |
| try (AutoUnlocker unlocker5 = lockFn.get()) { | |
| assertThat(currentThreadIdRef.get()).isEqualTo(currentThreadId); | |
| count.incrementAndGet(); | |
| } | |
| } | |
| } | |
| } | |
| } | |
| }; | |
| for (int i = 0; i < NUM_EXECUTOR_THREADS; i++) { | |
| executorService.execute(wrap(runnable)); | |
| } | |
| assertThatExecutorShutsDown(); | |
| assertThat(count.get()).isEqualTo(NUM_EXECUTOR_THREADS); | |
| } | |
| @Test | |
| public void unlockOnOtherThreadNotAllowed() { | |
| AtomicReference<AutoUnlocker> unlockerRef = new AtomicReference<>(); | |
| CountDownLatch unlockerRefSetLatch = new CountDownLatch(1); | |
| AtomicBoolean runnable2Executed = new AtomicBoolean(false); | |
| Runnable runnable1 = | |
| () -> { | |
| unlockerRef.set(makeLockFn1().get()); | |
| unlockerRefSetLatch.countDown(); | |
| }; | |
| Runnable runnable2 = | |
| () -> { | |
| try { | |
| unlockerRefSetLatch.await(TestUtils.WAIT_TIMEOUT_SECONDS, SECONDS); | |
| } catch (InterruptedException e) { | |
| throw new IllegalStateException(e); | |
| } | |
| assertThrows( | |
| IllegalMonitorStateException.class, | |
| () -> Preconditions.checkNotNull(unlockerRef.get()).close()); | |
| runnable2Executed.set(true); | |
| }; | |
| executorService.execute(wrap(runnable1)); | |
| executorService.execute(wrap(runnable2)); | |
| assertThatExecutorShutsDown(); | |
| assertThat(runnable2Executed.get()).isTrue(); | |
| } | |
| private void runRefCountingSanity(Supplier<AutoUnlocker> lockFn) { | |
| Set<AutoUnlocker> unlockers = new HashSet<>(); | |
| for (int i = 0; i < 1000; i++) { | |
| try (AutoUnlocker unlocker = lockFn.get()) { | |
| assertThat(unlockers.add(unlocker)).isTrue(); | |
| } | |
| } | |
| } | |
| @Test | |
| public void refCountingSanity() { | |
| runRefCountingSanity(makeLockFn1()); | |
| } | |
| @Test | |
| public void simpleMultiThreaded_mutualExclusion() { | |
| CountDownLatch runnableLatch = new CountDownLatch(NUM_EXECUTOR_THREADS); | |
| AtomicInteger mutexCounter = new AtomicInteger(0); | |
| AtomicInteger runnableCounter = new AtomicInteger(0); | |
| Runnable runnable = | |
| () -> { | |
| runnableLatch.countDown(); | |
| try { | |
| // Wait until all the Runnables are ready to try to acquire the lock all at once. | |
| runnableLatch.await(TestUtils.WAIT_TIMEOUT_SECONDS, SECONDS); | |
| } catch (InterruptedException e) { | |
| throw new IllegalStateException(e); | |
| } | |
| try (AutoUnlocker unlocker = makeLockFn1().get()) { | |
| runnableCounter.incrementAndGet(); | |
| assertThat(mutexCounter.incrementAndGet()).isEqualTo(1); | |
| assertThat(mutexCounter.decrementAndGet()).isEqualTo(0); | |
| } | |
| }; | |
| for (int i = 0; i < NUM_EXECUTOR_THREADS; i++) { | |
| executorService.execute(wrap(runnable)); | |
| } | |
| assertThatExecutorShutsDown(); | |
| assertThat(runnableCounter.get()).isEqualTo(NUM_EXECUTOR_THREADS); | |
| } | |
| private Runnable wrap(Runnable runnable) { | |
| return () -> { | |
| try { | |
| runnable.run(); | |
| } catch (Throwable e) { | |
| throwableFromRunnable.compareAndSet(null, e); | |
| } | |
| }; | |
| } | |
| private void assertThatExecutorShutsDown() { | |
| assertWithMessage("Shouldn't have been interrupted") | |
| .that(ExecutorUtil.interruptibleShutdown(executorService)) | |
| .isFalse(); | |
| } | |
| } |