/* The contents of this file are subject to the license and copyright terms
* detailed in the license directory at the root of the source tree (also
* available online at http://fedora-commons.org/license/).
*/
package fedora.server.journal.readerwriter.multifile;
import java.io.File;
import java.io.FileWriter;
import java.io.IOException;
import java.io.InputStream;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import junit.framework.TestCase;
import fedora.common.Constants;
import fedora.server.Context;
import fedora.server.errors.ServerException;
import fedora.server.journal.JournalConstants;
import fedora.server.journal.JournalConsumer;
import fedora.server.journal.MockJournalRecoveryLog;
import fedora.server.journal.MockServerForJournalTesting;
import fedora.server.journal.ServerInterface;
import fedora.server.management.MockManagementDelegate;
public class TestLockingFollowingJournalReader
extends TestCase
implements Constants, JournalConstants, MultiFileJournalConstants {
private static final int WAIT_INTERVAL = 5;
private static final String JOURNAL_FILENAME_PREFIX = "unit";
private static final String DUMMY_HASH_VALUE = "Dummy Hash";
private File journalDirectory;
private File archiveDirectory;
private File lockRequestFile;
private File lockAcceptedFile;
private Map<String, String> parameters;
private ServerInterface server;
private final String role = "DumbGrunt";
private MyMockManagementDelegate delegate;
private int initialNumberOfThreads;
public TestLockingFollowingJournalReader(String name) {
super(name);
}
@Override
protected void setUp() throws Exception {
super.setUp();
journalDirectory = createTempDirectory("fedoraTestingJournalFiles");
archiveDirectory = createTempDirectory("fedoraTestingArchiveFiles");
lockRequestFile =
new File(journalDirectory.getPath() + File.separator
+ "lockRequested");
lockRequestFile.delete();
lockAcceptedFile =
new File(journalDirectory.getPath() + File.separator
+ "lockAccepted");
lockAcceptedFile.delete();
delegate = new MyMockManagementDelegate();
server = new MockServerForJournalTesting(delegate, DUMMY_HASH_VALUE);
parameters = new HashMap<String, String>();
parameters.put(PARAMETER_JOURNAL_RECOVERY_LOG_CLASSNAME,
MockJournalRecoveryLog.class.getName());
parameters.put(PARAMETER_JOURNAL_READER_CLASSNAME,
"fedora.server.journal.readerwriter.multifile."
+ "LockingFollowingJournalReader");
parameters.put(PARAMETER_JOURNAL_DIRECTORY, journalDirectory.getPath());
parameters.put(PARAMETER_ARCHIVE_DIRECTORY, archiveDirectory.getPath());
parameters.put(PARAMETER_FOLLOW_POLLING_INTERVAL, "1");
parameters.put(PARAMETER_JOURNAL_FILENAME_PREFIX,
JOURNAL_FILENAME_PREFIX);
parameters.put(PARAMETER_LOCK_REQUESTED_FILENAME, lockRequestFile
.getPath());
parameters.put(PARAMETER_LOCK_ACCEPTED_FILENAME, lockAcceptedFile
.getPath());
initialNumberOfThreads = getNumberOfCurrentThreads();
}
/**
* Create 3 files and watch it process all of them
*/
public void testSimpleNoLocking() {
try {
// create 3 files, each with an ingest
createJournalFileFromString(getSimpleIngestString());
createJournalFileFromString(getSimpleIngestString());
createJournalFileFromString(getSimpleIngestString());
// create the JournalConsumer and run it.
JournalConsumer consumer =
new JournalConsumer(parameters, role, server);
startConsumerThread(consumer);
waitWhileThreadRuns(WAIT_INTERVAL);
consumer.shutdown();
assertEquals("Expected to see 3 ingests", 3, delegate
.getCallCount());
assertEquals("Journal files not all gone",
0,
howManyFilesInDirectory(journalDirectory));
assertEquals("Wrong number of archive files",
3,
howManyFilesInDirectory(archiveDirectory));
} catch (Throwable e) {
processException(e);
}
}
/**
* A lock request created before startup will prevent processing. When the
* request is removed, processing will occur.
*/
public void disabledtestLockBeforeStartingAndResume() {
try {
// create 3 files, each with an ingest, and create a lock request.
createJournalFileFromString(getSimpleIngestString());
createJournalFileFromString(getSimpleIngestString());
createJournalFileFromString(getSimpleIngestString());
createLockRequest();
// create the JournalConsumer and run it.
JournalConsumer consumer =
new JournalConsumer(parameters, role, server);
startConsumerThread(consumer);
// we should see the lock accepted and no processing going on.
waitForLockAccepted();
waitWhileThreadRuns(WAIT_INTERVAL);
assertEquals("Journal files should not be processed", 0, delegate
.getCallCount());
assertEquals("Journal files should not be processed",
3,
howManyFilesInDirectory(journalDirectory));
assertEquals("Journal files should not be processed",
0,
howManyFilesInDirectory(archiveDirectory));
int lockMessageIndex = assertLockMessageInLog();
// remove the request. We should see the lock released and
// processing should run to completion.
removeLockRequest();
waitForLockReleased();
waitWhileThreadRuns(WAIT_INTERVAL);
consumer.shutdown();
assertEquals("Expected to see 3 ingests", 3, delegate
.getCallCount());
assertEquals("Journal files not all gone",
0,
howManyFilesInDirectory(journalDirectory));
assertEquals("Wrong number of archive files",
3,
howManyFilesInDirectory(archiveDirectory));
assertUnlockMessageInLog(lockMessageIndex);
} catch (Throwable e) {
processException(e);
}
}
/**
* A lock request created while a file is in progress, which should prevent
* further processing until it is removed.
*/
public void testLockWhileProcessingAndResume() {
try {
// create 3 files, each with an ingest
createJournalFileFromString(getSimpleIngestString());
createJournalFileFromString(getSimpleIngestString());
createJournalFileFromString(getSimpleIngestString());
// a lock request will be created while the second file is being
// processed.
delegate.setIngestOperation(new LockAfterSecondIngest());
// create the JournalConsumer and run it.
JournalConsumer consumer =
new JournalConsumer(parameters, role, server);
startConsumerThread(consumer);
// we should see the lock accepted and processing stop after the
// second file.
waitForLockAccepted();
waitWhileThreadRuns(WAIT_INTERVAL);
assertEquals("We should stop after the second ingest", 2, delegate
.getCallCount());
assertEquals("One Journal file should not be processed",
1,
howManyFilesInDirectory(journalDirectory));
assertEquals("Only two Journal files should be processed",
2,
howManyFilesInDirectory(archiveDirectory));
int lockMessageIndex = assertLockMessageInLog();
// remove the request. We should see the lock released and
// processing should run to completion.
removeLockRequest();
waitForLockReleased();
waitWhileThreadRuns(WAIT_INTERVAL);
consumer.shutdown();
assertEquals("Expected to see 3 ingests", 3, delegate
.getCallCount());
assertEquals("Journal files not all gone",
0,
howManyFilesInDirectory(journalDirectory));
assertEquals("Wrong number of archive files",
3,
howManyFilesInDirectory(archiveDirectory));
assertUnlockMessageInLog(lockMessageIndex);
} catch (Throwable e) {
processException(e);
}
}
/**
* A lock request created while the system if polling, which should prevent
* further processing until it is removed. Create 1 files and watch it
* process all of them. Create a lock and wait for the ack. Create a 2nd
* file, and it will not be processed. Remove the lock; ack is removed and
* last file is processed.
*/
public void disabledtestLockWhilePollingAndResume() {
try {
// create 1 file, with an ingest
createJournalFileFromString(getSimpleIngestString());
// create the JournalConsumer and run it.
JournalConsumer consumer =
new JournalConsumer(parameters, role, server);
startConsumerThread(consumer);
// the file should be processed and we being polling.
waitWhileThreadRuns(WAIT_INTERVAL);
assertEquals("The first file should have been processed.",
1,
delegate.getCallCount());
assertEquals("The first file should have been processed.",
0,
howManyFilesInDirectory(journalDirectory));
assertEquals("The first file should have been processed.",
1,
howManyFilesInDirectory(archiveDirectory));
// create a lock request and wait for the acceptance.
createLockRequest();
waitForLockAccepted();
// create another Journal file, but it won't be processed.
createJournalFileFromString(getSimpleIngestString());
waitWhileThreadRuns(WAIT_INTERVAL);
assertEquals("The second file should not have been processed.",
1,
delegate.getCallCount());
assertEquals("The second file should not have been processed.",
1,
howManyFilesInDirectory(journalDirectory));
assertEquals("The second file should not have been processed.",
1,
howManyFilesInDirectory(archiveDirectory));
int lockMessageIndex = assertLockMessageInLog();
// remove the lock and the file is processed.
removeLockRequest();
waitForLockReleased();
waitWhileThreadRuns(WAIT_INTERVAL);
consumer.shutdown();
assertEquals("Expected to see 2 ingests", 2, delegate
.getCallCount());
assertEquals("Journal files not all gone",
0,
howManyFilesInDirectory(journalDirectory));
assertEquals("Wrong number of archive files",
2,
howManyFilesInDirectory(archiveDirectory));
assertUnlockMessageInLog(lockMessageIndex);
} catch (Throwable e) {
processException(e);
}
}
private void createLockRequest() throws IOException {
lockRequestFile.createNewFile();
}
private void removeLockRequest() {
lockRequestFile.delete();
}
private int howManyFilesInDirectory(File directory) {
return MultiFileJournalHelper
.getSortedArrayOfJournalFiles(directory,
JOURNAL_FILENAME_PREFIX).length;
}
/**
* Wait until the JournalConsumerThread stops, or until the time limit
* expires, whichever comes first.
*/
private void waitWhileThreadRuns(int maxSecondsToWait) {
for (int i = 0; i < maxSecondsToWait; i++) {
if (getNumberOfCurrentThreads() == initialNumberOfThreads) {
return;
}
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
/**
* Wait until the lock is accepted, or until the time runs out. If the
* latter, complain.
*/
private void waitForLockAccepted() {
int maxWait = 3;
for (int i = 0; i < maxWait; i++) {
if (lockAcceptedFile.exists()) {
return;
}
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
fail("Lock was not accepted after " + maxWait + " seconds.");
}
/**
* Wait until the lock is released, or until the time runs out. If the
* latter, complain.
*/
private void waitForLockReleased() {
int maxWait = 3;
for (int i = 0; i < maxWait; i++) {
if (!lockAcceptedFile.exists()) {
return;
}
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
fail("Lock was not released after " + maxWait + " seconds.");
}
/**
* Set the ManagementDelegate into the JournalConsumer, which will create
* the JournalConsumerThread.
*/
private void startConsumerThread(JournalConsumer consumer) {
consumer.setManagementDelegate(delegate);
}
private int getNumberOfCurrentThreads() {
int i =
Thread.currentThread().getThreadGroup()
.enumerate(new Thread[500]);
return i;
}
private void createJournalFileFromString(String text) throws IOException {
File journal =
File.createTempFile(JOURNAL_FILENAME_PREFIX,
null,
journalDirectory);
journal.deleteOnExit();
FileWriter writer = new FileWriter(journal);
writer.write(text);
writer.close();
}
/**
* Confirm that the last message in the log is a lock message, and return
* its position in the log.
*/
private int assertLockMessageInLog() {
List<String> messages = MockJournalRecoveryLog.getMessages();
int lastMessageIndex = messages.size() - 1;
String lastMessage = messages.get(lastMessageIndex);
assertStringStartsWith(lastMessage, "Lock request detected:");
return lastMessageIndex;
}
/**
* Confirm that the log message following the lock message is in fact an
* unlock message.
*/
private void assertUnlockMessageInLog(int lockMessageIndex) {
List<String> messages = MockJournalRecoveryLog.getMessages();
int unlockMessageIndex = lockMessageIndex + 1;
assertTrue(messages.size() > unlockMessageIndex);
String unlockMessage = messages.get(unlockMessageIndex);
assertStringStartsWith(unlockMessage, "Lock request removed");
}
private void assertStringStartsWith(String string, String prefix) {
if (!string.startsWith(prefix)) {
fail("String does not start as expected: string='" + string
+ "', prefix='" + prefix + "'");
}
}
private void processException(Throwable e) {
if (e instanceof ServerException) {
System.err.println("ServerException: code='"
+ ((ServerException) e).getCode() + "', class='"
+ e.getClass().getName() + "'");
StackTraceElement[] traces = e.getStackTrace();
for (StackTraceElement element : traces) {
System.err.println(element);
}
Throwable cause = e.getCause();
if (cause != null) {
cause.printStackTrace();
}
fail("Threw a ServerException");
} else {
e.printStackTrace();
fail("Threw an exception");
}
}
private File createTempDirectory(String name) {
File directory = new File(System.getProperty("java.io.tmpdir"), name);
directory.mkdir();
cleanOutDirectory(directory);
directory.deleteOnExit();
return directory;
}
private void cleanOutDirectory(File directory) {
File[] files = directory.listFiles();
for (File element : files) {
element.delete();
}
}
private String getSimpleIngestString() {
return "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n"
+ "<FedoraJournal repositoryHash=\""
+ DUMMY_HASH_VALUE
+ "\" timestamp=\"2006-08-11T11:14:43.011-0400\">\n"
+ " <JournalEntry method=\"ingest\" timestamp=\"2006-08-11T11:14:42.690-0400\" clientIpAddress=\"128.84.103.30\" loginId=\"fedoraAdmin\">\n"
+ " <context>\n"
+ " <password>junk</password>\n"
+ " <noOp>false</noOp>\n"
+ " <now>2006-08-11T11:14:42.690-0400</now>\n"
+ " <multimap name=\"environment\">\n"
+ " <multimapkey name=\"urn:fedora:names:fedora:2.1:environment:httpRequest:authType\">\n"
+ " <multimapvalue>BASIC</multimapvalue>\n"
+ " </multimapkey>\n"
+ " </multimap>\n"
+ " <multimap name=\"subject\"></multimap>\n"
+ " <multimap name=\"action\"> </multimap>\n"
+ " <multimap name=\"resource\"></multimap>\n"
+ " <multimap name=\"recovery\"></multimap>\n"
+ " </context>\n"
+ " <argument name=\"serialization\" type=\"stream\">PD94</argument>\n"
+ " <argument name=\"message\" type=\"string\">Minimal Ingest sample</argument>\n"
+ " <argument name=\"format\" type=\"string\">"
+ FOXML1_1.uri
+ "</argument>\n"
+ " <argument name=\"encoding\" type=\"string\">UTF-8</argument>\n"
+ " <argument name=\"newPid\" type=\"boolean\">true</argument>\n"
+ " </JournalEntry>\n" + "</FedoraJournal>\n";
}
/**
* Set one of these as the ingest object on the ManagementDelegate. When the
* second ingest operation begins, a Lock Request will be created.
*/
private final class LockAfterSecondIngest
implements Runnable {
public void run() {
if (delegate.getCallCount() == 2) {
try {
createLockRequest();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
/**
* This sub-class of {@link MockManagementDelegate} allows us to insert a
* {@link Runnable} that will be executed in the middle of an ingest call.
*
* @author Firstname Lastname
*/
private static class MyMockManagementDelegate
extends MockManagementDelegate {
private Runnable ingestOperation;
public void setIngestOperation(Runnable ingestOperation) {
this.ingestOperation = ingestOperation;
}
@Override
public String ingest(Context context,
InputStream serialization,
String logMessage,
String format,
String encoding,
boolean newPid) throws ServerException {
String result =
super.ingest(context,
serialization,
logMessage,
format,
encoding,
newPid);
if (ingestOperation != null) {
ingestOperation.run();
}
return result;
}
}
}