/*
* 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.jackrabbit.core.state;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
import junit.framework.TestCase;
import org.apache.jackrabbit.core.id.ItemId;
import org.apache.jackrabbit.core.id.NodeId;
import org.apache.jackrabbit.core.id.NodeReferencesId;
import org.apache.jackrabbit.core.state.ISMLocking.ReadLock;
import org.apache.jackrabbit.core.state.ISMLocking.WriteLock;
import org.apache.jackrabbit.spi.commons.name.NameConstants;
import org.apache.jackrabbit.uuid.UUID;
/**
* <code>AbstractISMLockingTest</code> contains test cases for the ISMLocking requirements.
*/
public abstract class AbstractISMLockingTest extends TestCase {
/**
* The {@link ISMLocking} instance under test.
*/
protected ISMLocking locking;
/**
* Test node state.
*/
protected NodeState state;
/**
* Node references instance targeting {@link #state}.
*/
protected NodeReferences refs;
/**
* List of change logs, each with a different modification for {@link #state}.
*/
protected List logs;
protected void setUp() throws Exception {
super.setUp();
locking = createISMLocking();
NodeId id = new NodeId(UUID.randomUUID());
state = new NodeState(id, NameConstants.NT_BASE, null, ItemState.STATUS_EXISTING, true);
refs = new NodeReferences(new NodeReferencesId(state.getNodeId()));
logs = new ArrayList();
ChangeLog log = new ChangeLog();
log.added(state);
logs.add(log);
log = new ChangeLog();
log.deleted(state);
logs.add(log);
log = new ChangeLog();
log.modified(state);
logs.add(log);
log = new ChangeLog();
log.modified(refs);
logs.add(log);
}
/**
* @return an {@link ISMLocking} instance which is exercised by the tests
*/
public abstract ISMLocking createISMLocking();
/**
* Checks the following requirement: <p/> <i>While a read lock is held for a given item with id
* <code>I</code> an implementation must ensure that no write lock is issued for a change log that
* contains a reference to an item with id <code>I</code>. </i>
*
* @throws InterruptedException on interruption; this will err the test
*/
public void testReadBlocksWrite() throws InterruptedException {
ReadLock rLock = locking.acquireReadLock(state.getId());
for (Iterator it = logs.iterator(); it.hasNext();) {
ChangeLog changeLog = (ChangeLog) it.next();
verifyBlocked(startWriterThread(locking, changeLog));
}
rLock.release();
}
/**
* Checks the following requirement: <p/> <i>While a write lock is held for a given change log
* <code>C</code> an implementation must ensure that no read lock is issued for an item that is contained
* in <code>C</code>, unless the current thread is the owner of the write lock!</i> <p/> The "unless"
* clause is tested by {@link #testWriteBlocksRead_notIfSameThread()} test.
*
* @throws InterruptedException on interruption; this will err the test
*/
public void testWriteBlocksRead() throws InterruptedException {
for (Iterator it = logs.iterator(); it.hasNext();) {
ChangeLog changeLog = (ChangeLog) it.next();
WriteLock wLock = locking.acquireWriteLock(changeLog);
verifyBlocked(startReaderThread(locking, state.getId()));
wLock.release();
}
}
public void testWriteBlocksRead_notIfSameThread() throws InterruptedException {
for (Iterator it = logs.iterator(); it.hasNext();) {
final ChangeLog changeLog = (ChangeLog) it.next();
Thread t = new Thread(new Runnable() {
public void run() {
try {
WriteLock wLock = locking.acquireWriteLock(changeLog);
locking.acquireReadLock(state.getId()).release();
wLock.release();
} catch (InterruptedException e) {
}
}
});
t.start();
verifyNotBlocked(t);
}
}
/**
* Checks the following requirement: <p/> While a write lock is held for a given change log <code>C</code>
* an implementation must ensure that no write lock is issued for a change log <code>C'</code> that intersects
* with <code>C</code>. That is both change logs contain a reference to the same item. Please note that an
* implementation is free to block requests entirely for additional write lock while a write lock is
* active. It is not a requirement to support concurrent write locks.
*
* @throws InterruptedException on interruption; this will err the test
*/
public void testIntersectingWrites() throws InterruptedException {
ChangeLog cl = new ChangeLog();
cl.added(state);
WriteLock wLock = locking.acquireWriteLock(cl);
for (Iterator it = logs.iterator(); it.hasNext();) {
ChangeLog changeLog = (ChangeLog) it.next();
verifyBlocked(startWriterThread(locking, changeLog));
}
wLock.release();
}
/**
* Checks if a downgraded write lock allows other threads to read again.
*
* @throws InterruptedException on interruption; this will err the test
*/
public void testDowngrade() throws InterruptedException {
for (Iterator it = logs.iterator(); it.hasNext();) {
ChangeLog changeLog = (ChangeLog) it.next();
WriteLock wLock = locking.acquireWriteLock(changeLog);
verifyBlocked(startReaderThread(locking, state.getId()));
ReadLock rLock = wLock.downgrade();
verifyNotBlocked(startReaderThread(locking, state.getId()));
rLock.release();
}
}
// ------------------------------< utilities >-------------------------------
/**
* Creates and starts a thread that acquires and releases the write lock of the given
* <code>ISMLocking</code> for the given changelog. The thread's interrupted status is set if it was
* interrupted during the acquire-release sequence and could therefore not finish it.
*
* @param lock the <code>ISMLocking</code> to use
* @param changeLog the <code>ChangeLog</code> to use
* @return a thread that has been started
*/
protected final Thread startWriterThread(final ISMLocking lock, final ChangeLog changeLog) {
Thread t = new Thread(new Runnable() {
public void run() {
try {
lock.acquireWriteLock(changeLog).release();
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}
});
t.start();
return t;
}
/**
* Creates and starts an thread that acquires and releases the read lock of the given
* <code>ISMLocking</code> for the given id. The thread's interrupted status is set if it was interrupted
* during the acquire-release sequence and could therefore not finish it.
*
* @param lock the <code>ISMLocking</code> to use
* @param id the id to use
* @return a thread that has been started
*/
protected final Thread startReaderThread(final ISMLocking lock, final ItemId id) {
Thread t = new Thread(new Runnable() {
public void run() {
try {
lock.acquireReadLock(id).release();
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}
});
t.start();
return t;
}
/**
* Verifies that the given thread is blocked. Then it interrupts the thread and waits a certain amount of
* time for it to complete. (If it doesn't complete within that time then the test that calls this method
* fails).
*
* @param other a started thread
* @throws InterruptedException on interruption
*/
protected final void verifyBlocked(final Thread other) throws InterruptedException {
Thread.sleep(100);
assertTrue(other.isAlive());
other.interrupt();
other.join(100);
assertFalse(other.isAlive());
}
/**
* Verifies that the given thread is not blocked and runs to completion within a certain amount of time
* and without interruption. (If it doesn't complete within that time without interruption then the test
* that calls this method fails).
*
* @param other a started thread
* @throws InterruptedException on interruption
*/
protected final void verifyNotBlocked(final Thread other) throws InterruptedException {
other.join(1000);
assertFalse(other.isInterrupted());
assertFalse(other.isAlive());
}
}