Hiram R. Chirino 2009-07-31 20:10:05 +00:00
parent 1ec71bdff1
commit fbc5eb5eb0
7 changed files with 319 additions and 43 deletions

View File

@ -63,14 +63,7 @@ import org.apache.kahadb.journal.Location;
import org.apache.kahadb.page.Page; import org.apache.kahadb.page.Page;
import org.apache.kahadb.page.PageFile; import org.apache.kahadb.page.PageFile;
import org.apache.kahadb.page.Transaction; import org.apache.kahadb.page.Transaction;
import org.apache.kahadb.util.ByteSequence; import org.apache.kahadb.util.*;
import org.apache.kahadb.util.DataByteArrayInputStream;
import org.apache.kahadb.util.DataByteArrayOutputStream;
import org.apache.kahadb.util.LockFile;
import org.apache.kahadb.util.LongMarshaller;
import org.apache.kahadb.util.Marshaller;
import org.apache.kahadb.util.StringMarshaller;
import org.apache.kahadb.util.VariableMarshaller;
public class MessageDatabase { public class MessageDatabase {
@ -155,6 +148,8 @@ public class MessageDatabase {
protected AtomicBoolean started = new AtomicBoolean(); protected AtomicBoolean started = new AtomicBoolean();
protected AtomicBoolean opened = new AtomicBoolean(); protected AtomicBoolean opened = new AtomicBoolean();
private LockFile lockFile; private LockFile lockFile;
private boolean ignoreMissingJournalfiles = false;
private int indexCacheSize = 100;
public MessageDatabase() { public MessageDatabase() {
} }
@ -218,24 +213,6 @@ public class MessageDatabase {
* @throws IOException * @throws IOException
*/ */
public void open() throws IOException { public void open() throws IOException {
File lockFileName = new File(directory, "lock");
lockFile = new LockFile(lockFileName, true);
if (failIfDatabaseIsLocked) {
lockFile.lock();
} else {
while (true) {
try {
lockFile.lock();
break;
} catch (IOException e) {
LOG.info("Database "+lockFileName+" is locked... waiting " + (DATABASE_LOCKED_WAIT_DELAY / 1000) + " seconds for the database to be unlocked. Reason: " + e);
try {
Thread.sleep(DATABASE_LOCKED_WAIT_DELAY);
} catch (InterruptedException e1) {
}
}
}
}
if( opened.compareAndSet(false, true) ) { if( opened.compareAndSet(false, true) ) {
getJournal().start(); getJournal().start();
@ -272,23 +249,44 @@ public class MessageDatabase {
} }
} }
private void lock() throws IOException {
if( lockFile == null ) {
File lockFileName = new File(directory, "lock");
lockFile = new LockFile(lockFileName, true);
if (failIfDatabaseIsLocked) {
lockFile.lock();
} else {
while (true) {
try {
lockFile.lock();
break;
} catch (IOException e) {
LOG.info("Database "+lockFileName+" is locked... waiting " + (DATABASE_LOCKED_WAIT_DELAY / 1000) + " seconds for the database to be unlocked. Reason: " + e);
try {
Thread.sleep(DATABASE_LOCKED_WAIT_DELAY);
} catch (InterruptedException e1) {
}
}
}
}
}
}
public void load() throws IOException { public void load() throws IOException {
synchronized (indexMutex) { synchronized (indexMutex) {
lock();
if (deleteAllMessages) {
getJournal().start();
getJournal().delete();
getJournal().close();
journal = null;
getPageFile().delete();
LOG.info("Persistence store purged.");
deleteAllMessages = false;
}
open(); open();
if (deleteAllMessages) {
journal.delete();
pageFile.unload();
pageFile.delete();
metadata = new Metadata();
LOG.info("Persistence store purged.");
deleteAllMessages = false;
loadPageFile();
}
store(new KahaTraceCommand().setMessage("LOADED " + new Date())); store(new KahaTraceCommand().setMessage("LOADED " + new Date()));
} }
@ -348,7 +346,6 @@ public class MessageDatabase {
* *
* @throws IOException * @throws IOException
* @throws IOException * @throws IOException
* @throws InvalidLocationException
* @throws IllegalStateException * @throws IllegalStateException
*/ */
private void recover() throws IllegalStateException, IOException { private void recover() throws IllegalStateException, IOException {
@ -406,6 +403,75 @@ public class MessageDatabase {
// TODO: do we need to modify the ack positions for the pub sub case? // TODO: do we need to modify the ack positions for the pub sub case?
} }
} }
// Lets be extra paranoid here and verify that all the datafiles being referenced
// by the indexes still exists.
final SequenceSet ss = new SequenceSet();
for (StoredDestination sd : storedDestinations.values()) {
// Use a visitor to cut down the number of pages that we load
sd.locationIndex.visit(tx, new BTreeVisitor<Location, Long>() {
int last=-1;
public boolean isInterestedInKeysBetween(Location first, Location second) {
if( first==null ) {
return !ss.contains(0, second.getDataFileId());
} else if( second==null ) {
return true;
} else {
return !ss.contains(first.getDataFileId(), second.getDataFileId());
}
}
public void visit(List<Location> keys, List<Long> values) {
for (Location l : keys) {
int fileId = l.getDataFileId();
if( last != fileId ) {
ss.add(fileId);
last = fileId;
}
}
}
});
}
HashSet<Integer> missingJournalFiles = new HashSet<Integer>();
while( !ss.isEmpty() ) {
missingJournalFiles.add( (int)ss.removeFirst() );
}
missingJournalFiles.removeAll( journal.getFileMap().keySet() );
if( !missingJournalFiles.isEmpty() ) {
if( ignoreMissingJournalfiles ) {
for (StoredDestination sd : storedDestinations.values()) {
final ArrayList<Long> matches = new ArrayList<Long>();
for (Integer missing : missingJournalFiles) {
sd.locationIndex.visit(tx, new BTreeVisitor.BetweenVisitor<Location, Long>(new Location(missing,0), new Location(missing+1,0)) {
@Override
protected void matched(Location key, Long value) {
matches.add(value);
}
});
}
for (Long sequenceId : matches) {
MessageKeys keys = sd.orderIndex.remove(tx, sequenceId);
sd.locationIndex.remove(tx, keys.location);
sd.messageIdIndex.remove(tx, keys.messageId);
undoCounter++;
// TODO: do we need to modify the ack positions for the pub sub case?
}
}
} else {
throw new IOException("Detected missing journal files: "+missingJournalFiles);
}
}
long end = System.currentTimeMillis(); long end = System.currentTimeMillis();
if( undoCounter > 0 ) { if( undoCounter > 0 ) {
// The rolledback operations are basically in flight journal writes. To avoid getting these the end user // The rolledback operations are basically in flight journal writes. To avoid getting these the end user
@ -1263,6 +1329,7 @@ public class MessageDatabase {
PageFile index = new PageFile(directory, "db"); PageFile index = new PageFile(directory, "db");
index.setEnableWriteThread(isEnableIndexWriteAsync()); index.setEnableWriteThread(isEnableIndexWriteAsync());
index.setWriteBatchSize(getIndexWriteBatchSize()); index.setWriteBatchSize(getIndexWriteBatchSize());
index.setPageCacheSize(indexCacheSize);
return index; return index;
} }
@ -1358,4 +1425,20 @@ public class MessageDatabase {
public void setFailIfDatabaseIsLocked(boolean failIfDatabaseIsLocked) { public void setFailIfDatabaseIsLocked(boolean failIfDatabaseIsLocked) {
this.failIfDatabaseIsLocked = failIfDatabaseIsLocked; this.failIfDatabaseIsLocked = failIfDatabaseIsLocked;
} }
public boolean isIgnoreMissingJournalfiles() {
return ignoreMissingJournalfiles;
}
public void setIgnoreMissingJournalfiles(boolean ignoreMissingJournalfiles) {
this.ignoreMissingJournalfiles = ignoreMissingJournalfiles;
}
public int getIndexCacheSize() {
return indexCacheSize;
}
public void setIndexCacheSize(int indexCacheSize) {
this.indexCacheSize = indexCacheSize;
}
} }

View File

@ -2883,6 +2883,8 @@ false so that messages actually reside long term in the JDBC database.
<xs:attribute name='journalMaxFileLength' type='xs:long'/> <xs:attribute name='journalMaxFileLength' type='xs:long'/>
<xs:attribute name='enableIndexWriteAsync' type='xs:boolean'/> <xs:attribute name='enableIndexWriteAsync' type='xs:boolean'/>
<xs:attribute name='enableJournalDiskSyncs' type='xs:boolean'/> <xs:attribute name='enableJournalDiskSyncs' type='xs:boolean'/>
<xs:attribute name='ignoreMissingJournalfiles' type='xs:boolean'/>
<xs:attribute name='indexCacheSize' type='xs:integer'/>
<xs:attribute name='size' type='xs:string'/> <xs:attribute name='size' type='xs:string'/>
<xs:attribute name='usageManager' type='xs:string'/> <xs:attribute name='usageManager' type='xs:string'/>
<xs:attribute name='id' type='xs:ID'/> <xs:attribute name='id' type='xs:ID'/>

View File

@ -976,6 +976,8 @@ false so that messages actually reside long term in the JDBC database.</td></tr>
<tr><td>indexWriteBatchSize</td><td>xs:integer</td><td></td></tr> <tr><td>indexWriteBatchSize</td><td>xs:integer</td><td></td></tr>
<tr><td>enableIndexWriteAsync</td><td>xs:boolean</td><td></td></tr> <tr><td>enableIndexWriteAsync</td><td>xs:boolean</td><td></td></tr>
<tr><td>enableJournalDiskSyncs</td><td>xs:boolean</td><td></td></tr> <tr><td>enableJournalDiskSyncs</td><td>xs:boolean</td><td></td></tr>
<tr><td>ignoreMissingJournalfiles</td><td>xs:boolean</td><td></td></tr>
<tr><td>indexCacheSize</td><td>xs:integer</td><td></td></tr>
</table> </table>
<table> <table>
<tr><th>Element</th><th>Type</th><th>Description</th> <tr><th>Element</th><th>Type</th><th>Description</th>

View File

@ -1247,6 +1247,8 @@ h4. Properties
| indexWriteBatchSize | _int_ | {html}{html} | | indexWriteBatchSize | _int_ | {html}{html} |
| enableIndexWriteAsync | _boolean_ | {html}{html} | | enableIndexWriteAsync | _boolean_ | {html}{html} |
| enableJournalDiskSyncs | _boolean_ | {html}{html} | | enableJournalDiskSyncs | _boolean_ | {html}{html} |
| ignoreMissingJournalfiles | _boolean_ | {html}{html} |
| indexCacheSize | _int_ | {html}{html} |
| size | _java.util.concurrent.atomic.AtomicLong_ | {html}{html} | | size | _java.util.concurrent.atomic.AtomicLong_ | {html}{html} |
| usageManager | _[org.apache.activemq.usage.SystemUsage|#org.apache.activemq.usage.SystemUsage-types]_ | {html}{html} | | usageManager | _[org.apache.activemq.usage.SystemUsage|#org.apache.activemq.usage.SystemUsage-types]_ | {html}{html} |

View File

@ -0,0 +1,147 @@
/**
* 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.activemq.store.kahadb;
import junit.framework.TestCase;
import org.apache.activemq.broker.BrokerService;
import org.apache.activemq.ActiveMQConnectionFactory;
import org.apache.activemq.command.ActiveMQQueue;
import javax.jms.*;
import java.io.File;
import java.io.IOException;
/**
* @author chirino
*/
public class KahaDBTest extends TestCase {
protected BrokerService createBroker(KahaDBStore kaha) throws Exception {
BrokerService broker = new BrokerService();
broker.setUseJmx(false);
broker.setPersistenceAdapter(kaha);
broker.start();
return broker;
}
private KahaDBStore createStore(boolean delete) throws IOException {
KahaDBStore kaha = new KahaDBStore();
kaha.setDirectory(new File("target/activemq-data/kahadb"));
if( delete ) {
kaha.deleteAllMessages();
}
return kaha;
}
public void testIgnoreMissingJournalfilesOptionSetFalse() throws Exception {
KahaDBStore kaha = createStore(true);
kaha.setJournalMaxFileLength(1024*100);
assertFalse(kaha.isIgnoreMissingJournalfiles());
BrokerService broker = createBroker(kaha);
sendMessages(1000);
broker.stop();
// Delete some journal files..
assertExistsAndDelete(new File(kaha.getDirectory(), "db-4.log"));
assertExistsAndDelete(new File(kaha.getDirectory(), "db-8.log"));
kaha = createStore(false);
kaha.setJournalMaxFileLength(1024*100);
assertFalse(kaha.isIgnoreMissingJournalfiles());
try {
broker = createBroker(kaha);
fail("expected IOException");
} catch (IOException e) {
assertTrue( e.getMessage().startsWith("Detected missing journal files") );
}
}
public void testIgnoreMissingJournalfilesOptionSetTrue() throws Exception {
KahaDBStore kaha = createStore(true);
kaha.setJournalMaxFileLength(1024*100);
assertFalse(kaha.isIgnoreMissingJournalfiles());
BrokerService broker = createBroker(kaha);
sendMessages(1000);
broker.stop();
// Delete some journal files..
assertExistsAndDelete(new File(kaha.getDirectory(), "db-4.log"));
assertExistsAndDelete(new File(kaha.getDirectory(), "db-8.log"));
kaha = createStore(false);
kaha.setIgnoreMissingJournalfiles(true);
kaha.setJournalMaxFileLength(1024*100);
broker = createBroker(kaha);
// We know we won't get all the messages but we should get most of them.
int count = receiveMessages();
assertTrue( count > 800 );
assertTrue( count < 1000 );
broker.stop();
}
private void assertExistsAndDelete(File file) {
assertTrue(file.exists());
file.delete();
assertFalse(file.exists());
}
private void sendMessages(int count) throws JMSException {
ActiveMQConnectionFactory cf = new ActiveMQConnectionFactory("vm://localhost");
Connection connection = cf.createConnection();
try {
Session session = connection.createSession(false, Session.AUTO_ACKNOWLEDGE);
MessageProducer producer = session.createProducer(new ActiveMQQueue("TEST"));
for (int i = 0; i < count; i++) {
producer.send(session.createTextMessage(createContent(i)));
}
} finally {
connection.close();
}
}
private int receiveMessages() throws JMSException {
int rc=0;
ActiveMQConnectionFactory cf = new ActiveMQConnectionFactory("vm://localhost");
Connection connection = cf.createConnection();
try {
connection.start();
Session session = connection.createSession(false, Session.AUTO_ACKNOWLEDGE);
MessageConsumer messageConsumer = session.createConsumer(new ActiveMQQueue("TEST"));
while ( messageConsumer.receive(1000) !=null ) {
rc++;
}
return rc;
} finally {
connection.close();
}
}
private String createContent(int i) {
StringBuilder sb = new StringBuilder(i+":");
while( sb.length() < 1024 ) {
sb.append("*");
}
return sb.toString();
}
}

View File

@ -67,6 +67,32 @@ public interface BTreeVisitor<Key,Value> {
abstract protected void matched(Key key, Value value); abstract protected void matched(Key key, Value value);
} }
abstract class BetweenVisitor<Key extends Comparable<Key>, Value> implements BTreeVisitor<Key, Value>{
private final Key first;
private final Key last;
public BetweenVisitor(Key first, Key last) {
this.first = first;
this.last = last;
}
public boolean isInterestedInKeysBetween(Key first, Key second) {
return (second==null || second.compareTo(this.first)>=0)
&& (first==null || first.compareTo(last)<0);
}
public void visit(List<Key> keys, List<Value> values) {
for( int i=0; i < keys.size(); i++) {
Key key = keys.get(i);
if( key.compareTo(first)>=0 && key.compareTo(last)<0 ) {
matched(key, values.get(i));
}
}
}
abstract protected void matched(Key key, Value value);
}
abstract class GTEVisitor<Key extends Comparable<Key>, Value> implements BTreeVisitor<Key, Value>{ abstract class GTEVisitor<Key extends Comparable<Key>, Value> implements BTreeVisitor<Key, Value>{
final private Key value; final private Key value;

View File

@ -255,4 +255,18 @@ public class SequenceSet extends LinkedNodeList<Sequence> {
return rc; return rc;
} }
public boolean contains(int first, int last) {
if (isEmpty()) {
return false;
}
Sequence sequence = getHead();
while (sequence != null) {
if (sequence.first <= first ) {
return last <= sequence.last ;
}
sequence = sequence.getNext();
}
return false;
}
} }