blob: 568e0c94713164ac5b6d8e011720719b0349e9df [file] [log] [blame]
/*
* 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.configuration2.sync;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.verifyNoMoreInteractions;
import static org.mockito.Mockito.when;
import java.util.Random;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReadWriteLock;
import org.junit.jupiter.api.Test;
/**
* Test class for {@code ReadWriteSynchronizer}.
*/
public class TestReadWriteSynchronizer {
/**
* A class representing an account.
*/
private static final class Account {
/** The amount stored in this account. */
private long amount;
/**
* Changes the amount of money by the given delata.
*
* @param delta the delta
*/
public void change(final long delta) {
amount += delta;
}
/**
* Returns the amount of money stored in this account.
*
* @return the amount
*/
public long getAmount() {
return amount;
}
}
/**
* A thread which performs a number of read operations on the bank's accounts and checks whether the amount of money is
* consistent.
*/
private static final class ReaderThread extends Thread {
/** The acounts to monitor. */
private final Account[] accounts;
/** The synchronizer object. */
private final Synchronizer sync;
/** The number of read operations. */
private final int numberOfReads;
/** Stores errors detected on read operations. */
private volatile int errors;
/**
* Creates a new instance of {@code ReaderThread}.
*
* @param s the synchronizer to be used
* @param readCount the number of read operations
* @param accs the accounts to monitor
*/
public ReaderThread(final Synchronizer s, final int readCount, final Account... accs) {
accounts = accs;
sync = s;
numberOfReads = readCount;
}
/**
* Returns the number of errors occurred during read operations.
*
* @return the number of errors
*/
public int getErrors() {
return errors;
}
/**
* Performs the given number of read operations.
*/
@Override
public void run() {
for (int i = 0; i < numberOfReads; i++) {
sync.beginRead();
final long sum = sumUpAccounts(accounts);
sync.endRead();
if (sum != TOTAL_MONEY) {
errors++;
}
}
}
}
/**
* A test thread for updating account objects. This thread executes a number of transactions on two accounts. Each
* transaction determines the account containing more money. Then a random number of money is transferred from this
* account to the other one.
*/
private static final class UpdateThread extends Thread {
/** The synchronizer. */
private final Synchronizer sync;
/** Account 1. */
private final Account account1;
/** Account 2. */
private final Account account2;
/** An object for creating random numbers. */
private final Random random;
/** The number of transactions. */
private final int numberOfUpdates;
/**
* Creates a new instance of {@code UpdateThread}.
*
* @param s the synchronizer
* @param updateCount the number of updates
* @param ac1 account 1
* @param ac2 account 2
*/
public UpdateThread(final Synchronizer s, final int updateCount, final Account ac1, final Account ac2) {
sync = s;
account1 = ac1;
account2 = ac2;
numberOfUpdates = updateCount;
random = new Random();
}
/**
* Performs the given number of update transactions.
*/
@Override
public void run() {
for (int i = 0; i < numberOfUpdates; i++) {
sync.beginWrite();
final Account acSource;
Account acDest;
if (account1.getAmount() < account2.getAmount()) {
acSource = account1;
acDest = account2;
} else {
acSource = account2;
acDest = account1;
}
final long x = Math.round(random.nextDouble() * (acSource.getAmount() - 1)) + 1;
acSource.change(-x);
acDest.change(x);
sync.endWrite();
}
}
}
/** Constant for the total amount of money in the system. */
private static final long TOTAL_MONEY = 1000000L;
/**
* Helper method to calculate the sum over all accounts.
*
* @param accounts the accounts to check
* @return the sum of the money on these accounts
*/
private static long sumUpAccounts(final Account... accounts) {
long sum = 0;
for (final Account acc : accounts) {
sum += acc.getAmount();
}
return sum;
}
/**
* Tests whether a lock passed to the constructor is used.
*/
@Test
void testInitLock() {
final ReadWriteLock lock = mock(ReadWriteLock.class);
final Lock readLock = mock(Lock.class);
when(lock.readLock()).thenReturn(readLock);
final ReadWriteSynchronizer sync = new ReadWriteSynchronizer(lock);
sync.beginRead();
verify(lock).readLock();
verify(readLock).lock();
verifyNoMoreInteractions(lock, readLock);
}
/**
* Tests whether the synchronizer is reentrant. This is important for some combined operations on a configuration.
*/
@Test
void testReentrance() {
final Synchronizer sync = new ReadWriteSynchronizer();
sync.beginWrite();
sync.beginRead();
sync.beginRead();
sync.endRead();
sync.endRead();
sync.beginWrite();
sync.endWrite();
sync.endWrite();
}
/**
* Performs a test of the synchronizer based on the classic example of account objects. Money is transferred between two
* accounts. If everything goes well, the total amount of money stays constant over time.
*/
@Test
void testSynchronizerInAction() throws InterruptedException {
final int numberOfUpdates = 10000;
final int numberOfReads = numberOfUpdates / 2;
final int readThreadCount = 3;
final int updateThreadCount = 2;
final Synchronizer sync = new ReadWriteSynchronizer();
final Account account1 = new Account();
final Account account2 = new Account();
account1.change(TOTAL_MONEY / 2);
account2.change(TOTAL_MONEY / 2);
final UpdateThread[] updateThreads = new UpdateThread[updateThreadCount];
for (int i = 0; i < updateThreads.length; i++) {
updateThreads[i] = new UpdateThread(sync, numberOfUpdates, account1, account2);
updateThreads[i].start();
}
final ReaderThread[] readerThreads = new ReaderThread[readThreadCount];
for (int i = 0; i < readerThreads.length; i++) {
readerThreads[i] = new ReaderThread(sync, numberOfReads, account1, account2);
readerThreads[i].start();
}
for (final UpdateThread t : updateThreads) {
t.join();
}
for (final ReaderThread t : readerThreads) {
t.join();
assertEquals(0, t.getErrors());
}
sync.beginRead();
assertEquals(TOTAL_MONEY, sumUpAccounts(account1, account2));
sync.endRead();
}
}