| // Copyright 2026 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.remote; | |
| import static com.google.common.truth.Truth.assertThat; | |
| import static org.junit.Assert.assertThrows; | |
| import com.google.devtools.build.lib.testutil.TestThread; | |
| import com.google.devtools.build.lib.testutil.TestUtils; | |
| import com.google.testing.junit.testparameterinjector.TestParameter; | |
| import com.google.testing.junit.testparameterinjector.TestParameterInjector; | |
| import java.util.Random; | |
| import java.util.concurrent.CountDownLatch; | |
| import java.util.concurrent.Executors; | |
| import java.util.concurrent.atomic.AtomicInteger; | |
| import org.junit.Test; | |
| import org.junit.runner.RunWith; | |
| /** Tests for {@link ReaderPreferringReadWriteLock}. */ | |
| @RunWith(TestParameterInjector.class) | |
| public class ReaderPreferringReadWriteLockTest { | |
| public enum Group { | |
| READ { | |
| @Override | |
| void lock(ReaderPreferringReadWriteLock lock) throws InterruptedException { | |
| lock.lockReadInterruptibly(); | |
| } | |
| @Override | |
| void unlock(ReaderPreferringReadWriteLock lock) { | |
| lock.unlockRead(); | |
| } | |
| @Override | |
| Group otherGroup() { | |
| return WRITE; | |
| } | |
| }, | |
| WRITE { | |
| @Override | |
| void lock(ReaderPreferringReadWriteLock lock) throws InterruptedException { | |
| lock.lockWriteInterruptibly(); | |
| } | |
| @Override | |
| void unlock(ReaderPreferringReadWriteLock lock) { | |
| lock.unlockWrite(); | |
| } | |
| @Override | |
| Group otherGroup() { | |
| return READ; | |
| } | |
| }; | |
| abstract void lock(ReaderPreferringReadWriteLock lock) throws InterruptedException; | |
| abstract void unlock(ReaderPreferringReadWriteLock lock); | |
| abstract Group otherGroup(); | |
| } | |
| @Test | |
| public void lockRead_multipleReaders_succeeds() throws Exception { | |
| var lock = new ReaderPreferringReadWriteLock(); | |
| for (int i = 0; i < 10; i++) { | |
| lock.lockReadInterruptibly(); | |
| } | |
| for (int i = 0; i < 10; i++) { | |
| lock.unlockRead(); | |
| } | |
| // The lock has been fully released: a writer can acquire it. | |
| lock.lockWriteInterruptibly(); | |
| lock.unlockWrite(); | |
| } | |
| @Test | |
| public void unlock_lockNotHeld_throws(@TestParameter Group group) { | |
| var lock = new ReaderPreferringReadWriteLock(); | |
| assertThrows(IllegalMonitorStateException.class, () -> group.unlock(lock)); | |
| } | |
| @Test | |
| public void unlock_lockHeldByOtherGroup_throws(@TestParameter Group group) throws Exception { | |
| var lock = new ReaderPreferringReadWriteLock(); | |
| group.lock(lock); | |
| assertThrows(IllegalMonitorStateException.class, () -> group.otherGroup().unlock(lock)); | |
| group.unlock(lock); | |
| } | |
| @Test | |
| public void lock_lockHeldByOtherGroup_blocksUntilReleased(@TestParameter Group group) | |
| throws Exception { | |
| var otherGroup = group.otherGroup(); | |
| var lock = new ReaderPreferringReadWriteLock(); | |
| group.lock(lock); | |
| var otherGroupAcquisitions = new AtomicInteger(); | |
| var otherGroupThread = | |
| new TestThread( | |
| () -> { | |
| otherGroup.lock(lock); | |
| otherGroupAcquisitions.incrementAndGet(); | |
| otherGroup.unlock(lock); | |
| }); | |
| otherGroupThread.start(); | |
| waitUntilState(otherGroupThread, Thread.State.WAITING); | |
| assertThat(otherGroupAcquisitions.get()).isEqualTo(0); | |
| group.unlock(lock); | |
| otherGroupThread.joinAndAssertState(TestUtils.WAIT_TIMEOUT_MILLISECONDS); | |
| assertThat(otherGroupAcquisitions.get()).isEqualTo(1); | |
| // group can acquire again after otherGroup has released the lock. | |
| group.lock(lock); | |
| group.unlock(lock); | |
| } | |
| @Test | |
| public void lockWrite_lockHeldByWriter_blocksUntilReleased() throws Exception { | |
| var lock = new ReaderPreferringReadWriteLock(); | |
| lock.lockWriteInterruptibly(); | |
| var otherWriterAcquisitions = new AtomicInteger(); | |
| var otherWriter = | |
| new TestThread( | |
| () -> { | |
| lock.lockWriteInterruptibly(); | |
| otherWriterAcquisitions.incrementAndGet(); | |
| lock.unlockWrite(); | |
| }); | |
| otherWriter.start(); | |
| waitUntilState(otherWriter, Thread.State.WAITING); | |
| assertThat(otherWriterAcquisitions.get()).isEqualTo(0); | |
| lock.unlockWrite(); | |
| otherWriter.joinAndAssertState(TestUtils.WAIT_TIMEOUT_MILLISECONDS); | |
| assertThat(otherWriterAcquisitions.get()).isEqualTo(1); | |
| } | |
| @Test | |
| public void lockRead_writerWaiting_isAdmitted() throws Exception { | |
| var lock = new ReaderPreferringReadWriteLock(); | |
| lock.lockReadInterruptibly(); | |
| var writerAcquisitions = new AtomicInteger(); | |
| var writer = | |
| new TestThread( | |
| () -> { | |
| lock.lockWriteInterruptibly(); | |
| writerAcquisitions.incrementAndGet(); | |
| lock.unlockWrite(); | |
| }); | |
| writer.start(); | |
| waitUntilState(writer, Thread.State.WAITING); | |
| // A new reader is admitted while the writer waits. | |
| lock.lockReadInterruptibly(); | |
| assertThat(writerAcquisitions.get()).isEqualTo(0); | |
| // The writer only acquires the lock once all readers have released it. | |
| lock.unlockRead(); | |
| assertThat(writerAcquisitions.get()).isEqualTo(0); | |
| lock.unlockRead(); | |
| writer.joinAndAssertState(TestUtils.WAIT_TIMEOUT_MILLISECONDS); | |
| assertThat(writerAcquisitions.get()).isEqualTo(1); | |
| // Both groups can acquire the lock without blocking after the writer has released it. | |
| lock.lockReadInterruptibly(); | |
| lock.unlockRead(); | |
| lock.lockWriteInterruptibly(); | |
| lock.unlockWrite(); | |
| } | |
| @Test | |
| public void unlockRead_newReaderBeforeNotification_writerIsNotStranded() throws Exception { | |
| var lock = new ReaderPreferringReadWriteLock(); | |
| lock.lockReadInterruptibly(); | |
| var writerAcquisitions = new AtomicInteger(); | |
| var writer = | |
| new TestThread( | |
| () -> { | |
| lock.lockWriteInterruptibly(); | |
| writerAcquisitions.incrementAndGet(); | |
| lock.unlockWrite(); | |
| }); | |
| writer.start(); | |
| waitUntilState(writer, Thread.State.WAITING); | |
| var releasingReader = new TestThread(lock::unlockRead); | |
| synchronized (lock) { | |
| releasingReader.start(); | |
| // Pause the last reader after it releases the lock but before it notifies the writer. | |
| waitUntilState(releasingReader, Thread.State.BLOCKED); | |
| lock.lockReadInterruptibly(); | |
| } | |
| releasingReader.joinAndAssertState(TestUtils.WAIT_TIMEOUT_MILLISECONDS); | |
| // The notified writer must record itself as waiting again while this new reader holds the lock. | |
| waitUntilState(writer, Thread.State.WAITING); | |
| assertThat(writerAcquisitions.get()).isEqualTo(0); | |
| lock.unlockRead(); | |
| writer.joinAndAssertState(TestUtils.WAIT_TIMEOUT_MILLISECONDS); | |
| assertThat(writerAcquisitions.get()).isEqualTo(1); | |
| } | |
| @Test | |
| public void lock_interruptedWhileBlocked_throwsAndKeepsLockUsable(@TestParameter Group group) | |
| throws Exception { | |
| var otherGroup = group.otherGroup(); | |
| var lock = new ReaderPreferringReadWriteLock(); | |
| group.lock(lock); | |
| var interrupted = new CountDownLatch(1); | |
| var otherGroupThread = | |
| new TestThread( | |
| () -> { | |
| try { | |
| otherGroup.lock(lock); | |
| } catch (InterruptedException e) { | |
| interrupted.countDown(); | |
| } | |
| }); | |
| otherGroupThread.start(); | |
| waitUntilState(otherGroupThread, Thread.State.WAITING); | |
| otherGroupThread.interrupt(); | |
| otherGroupThread.joinAndAssertState(TestUtils.WAIT_TIMEOUT_MILLISECONDS); | |
| assertThat(interrupted.getCount()).isEqualTo(0); | |
| // The interrupted acquisition must not have taken or corrupted the lock: group still holds it | |
| // and after releasing it, both groups can acquire it. | |
| group.unlock(lock); | |
| otherGroup.lock(lock); | |
| otherGroup.unlock(lock); | |
| group.lock(lock); | |
| group.unlock(lock); | |
| } | |
| @Test | |
| @SuppressWarnings("ThreadPriorityCheck") | |
| public void lockRead_interruptedWriter_doesNotEnterMonitor() throws Exception { | |
| var lock = new ReaderPreferringReadWriteLock(); | |
| lock.lockReadInterruptibly(); | |
| var writer = | |
| new TestThread( | |
| () -> assertThrows(InterruptedException.class, lock::lockWriteInterruptibly)); | |
| writer.start(); | |
| waitUntilState(writer, Thread.State.WAITING); | |
| writer.interrupt(); | |
| writer.joinAndAssertState(TestUtils.WAIT_TIMEOUT_MILLISECONDS); | |
| lock.unlockRead(); | |
| // Cancelling the only writer must leave subsequent readers independent of the monitor. | |
| var reader = | |
| new TestThread( | |
| () -> { | |
| for (int i = 0; i < 3; i++) { | |
| lock.lockReadInterruptibly(); | |
| lock.unlockRead(); | |
| } | |
| }); | |
| try { | |
| synchronized (lock) { | |
| reader.start(); | |
| Thread.State state; | |
| while ((state = reader.getState()) != Thread.State.TERMINATED) { | |
| assertThat(state).isNotEqualTo(Thread.State.BLOCKED); | |
| Thread.yield(); | |
| } | |
| } | |
| } finally { | |
| reader.joinAndAssertState(TestUtils.WAIT_TIMEOUT_MILLISECONDS); | |
| } | |
| } | |
| @Test | |
| public void lock_interruptedBeforeAcquisition_throwsAndKeepsLockUsable(@TestParameter Group group) | |
| throws Exception { | |
| var lock = new ReaderPreferringReadWriteLock(); | |
| try { | |
| Thread.currentThread().interrupt(); | |
| assertThrows(InterruptedException.class, () -> group.lock(lock)); | |
| } finally { | |
| // Avoid leaking the interrupt into other tests if the assertion fails. | |
| Thread.interrupted(); | |
| } | |
| // The interrupted acquisition must not have taken or corrupted the lock. | |
| group.otherGroup().lock(lock); | |
| group.otherGroup().unlock(lock); | |
| } | |
| @Test | |
| public void lockAndUnlock_concurrentThreads_readersAndWritersNeverOverlap() { | |
| var lock = new ReaderPreferringReadWriteLock(); | |
| var writers = new AtomicInteger(); | |
| var readers = new AtomicInteger(); | |
| var violations = new AtomicInteger(); | |
| try (var executor = Executors.newVirtualThreadPerTaskExecutor()) { | |
| for (int i = 0; i < 16; i++) { | |
| int seed = i; | |
| executor.execute( | |
| () -> { | |
| try { | |
| var random = new Random(seed); | |
| for (int j = 0; j < 20_000; j++) { | |
| if (random.nextInt(4) == 0) { | |
| lock.lockWriteInterruptibly(); | |
| try { | |
| // The writer is exclusive: no other writer and no reader may be active. | |
| if (writers.incrementAndGet() != 1 || readers.get() != 0) { | |
| violations.incrementAndGet(); | |
| } | |
| writers.decrementAndGet(); | |
| } finally { | |
| lock.unlockWrite(); | |
| } | |
| } else { | |
| lock.lockReadInterruptibly(); | |
| try { | |
| readers.incrementAndGet(); | |
| if (writers.get() != 0) { | |
| violations.incrementAndGet(); | |
| } | |
| readers.decrementAndGet(); | |
| } finally { | |
| lock.unlockRead(); | |
| } | |
| } | |
| } | |
| } catch (InterruptedException e) { | |
| // Interruptions are not expected. | |
| violations.incrementAndGet(); | |
| } | |
| }); | |
| } | |
| } | |
| assertThat(violations.get()).isEqualTo(0); | |
| assertThat(readers.get()).isEqualTo(0); | |
| assertThat(writers.get()).isEqualTo(0); | |
| } | |
| @SuppressWarnings("ThreadPriorityCheck") | |
| private static void waitUntilState(Thread thread, Thread.State expectedState) { | |
| Thread.State state; | |
| while ((state = thread.getState()) != expectedState) { | |
| assertThat(state).isNotEqualTo(Thread.State.TERMINATED); | |
| Thread.yield(); | |
| } | |
| } | |
| } |