Files
heritrix3/commons/src/main/java/org/archive/bdb/BdbModule.java
T
gojomo 9fd648f3ef HER-1467 Allow periodic checkpointing and checkpoint-copy-logs to be set/unset during crawl
* CheckpointService.java
    reorganize so that checkpoint timedTask is reset whenever interval is reset on a running crawl
* CheckpointSuccessEvent.java, CrawlJob.java
    report successes as new kind of ApplicationEvent; propagagte to job log
* BdbModule.java
    remove obsolete unconsulted checkpointCopyLogs setting
2010-04-23 00:15:44 +00:00

664 lines
22 KiB
Java

/*
* This file is part of the Heritrix web crawler (crawler.archive.org).
*
* Licensed to the Internet Archive (IA) by one or more individual
* contributors.
*
* The IA 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.archive.bdb;
import java.io.Closeable;
import java.io.File;
import java.io.FileFilter;
import java.io.IOException;
import java.io.ObjectOutputStream;
import java.io.Serializable;
import java.math.BigInteger;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.TimeUnit;
import java.util.logging.Level;
import java.util.logging.Logger;
import org.apache.commons.io.FileUtils;
import org.apache.commons.io.filefilter.FileFilterUtils;
import org.apache.commons.io.filefilter.IOFileFilter;
import org.archive.checkpointing.Checkpoint;
import org.archive.checkpointing.Checkpointable;
import org.archive.spring.ConfigPath;
import org.archive.util.CachedBdbMap;
import org.archive.util.ObjectIdentityBdbCache;
import org.archive.util.ObjectIdentityCache;
import org.archive.util.bdbje.EnhancedEnvironment;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.Lifecycle;
import com.sleepycat.bind.EntryBinding;
import com.sleepycat.bind.serial.SerialBinding;
import com.sleepycat.bind.serial.StoredClassCatalog;
import com.sleepycat.bind.tuple.TupleBinding;
import com.sleepycat.je.CheckpointConfig;
import com.sleepycat.je.Database;
import com.sleepycat.je.DatabaseConfig;
import com.sleepycat.je.DatabaseException;
import com.sleepycat.je.DatabaseNotFoundException;
import com.sleepycat.je.EnvironmentConfig;
import com.sleepycat.je.util.DbBackup;
/**
* Utility module for managing a shared BerkeleyDB-JE environment
*
* @contributor pjack
* @contributor gojomo
*/
public class BdbModule implements Lifecycle, Checkpointable, Closeable {
private static final long serialVersionUID = 1L;
final private static Logger LOGGER =
Logger.getLogger(BdbModule.class.getName());
private static class DatabasePlusConfig implements Serializable {
private static final long serialVersionUID = 1L;
public transient Database database;
public String name;
public BdbConfig config;
}
/**
* Configuration object for databases. Needed because
* {@link DatabaseConfig} is not serializable. Also it prevents invalid
* configurations. (All databases opened through this module must be
* deferred-write, because otherwise they can't sync(), and you can't
* run a checkpoint without doing sync() first.)
*
* @author pjack
*
*/
public static class BdbConfig implements Serializable {
private static final long serialVersionUID = 1L;
boolean allowCreate;
boolean sortedDuplicates;
boolean transactional;
boolean deferredWrite = true;
public BdbConfig() {
}
public boolean isAllowCreate() {
return allowCreate;
}
public void setAllowCreate(boolean allowCreate) {
this.allowCreate = allowCreate;
}
public boolean getSortedDuplicates() {
return sortedDuplicates;
}
public void setSortedDuplicates(boolean sortedDuplicates) {
this.sortedDuplicates = sortedDuplicates;
}
public DatabaseConfig toDatabaseConfig() {
DatabaseConfig result = new DatabaseConfig();
result.setDeferredWrite(deferredWrite);
result.setTransactional(transactional);
result.setAllowCreate(allowCreate);
result.setSortedDuplicates(sortedDuplicates);
return result;
}
public boolean isTransactional() {
return transactional;
}
public void setTransactional(boolean transactional) {
this.transactional = transactional;
}
public void setDeferredWrite(boolean b) {
this.deferredWrite = true;
}
}
protected ConfigPath dir = new ConfigPath("bdbmodule subdirectory","state");
public ConfigPath getDir() {
return dir;
}
public void setDir(ConfigPath dir) {
this.dir = dir;
}
int cachePercent = -1;
public int getCachePercent() {
return cachePercent;
}
public void setCachePercent(int cachePercent) {
this.cachePercent = cachePercent;
}
boolean useSharedCache = true;
public boolean getUseSharedCache() {
return useSharedCache;
}
public void setUseSharedCache(boolean useSharedCache) {
this.useSharedCache = useSharedCache;
}
/**
* Expected number of concurrent threads; used to tune nLockTables
* according to JE FAQ
* http://www.oracle.com/technology/products/berkeley-db/faq/je_faq.html#33
*/
int expectedConcurrency = 25;
public int getExpectedConcurrency() {
return expectedConcurrency;
}
public void setExpectedConcurrency(int expectedConcurrency) {
this.expectedConcurrency = expectedConcurrency;
}
private transient EnhancedEnvironment bdbEnvironment;
private transient StoredClassCatalog classCatalog;
private Map<String,ObjectIdentityCache> oiCaches =
new ConcurrentHashMap<String,ObjectIdentityCache>();
private Map<String,DatabasePlusConfig> databases =
new ConcurrentHashMap<String,DatabasePlusConfig>();
private transient Thread shutdownHook;
public BdbModule() {
}
public void start() {
if (isRunning()) {
return;
}
try {
boolean isRecovery = false;
if(recoveryCheckpoint!=null) {
isRecovery = true;
doRecover();
}
setup(getDir().getFile(), !isRecovery);
} catch (DatabaseException e) {
throw new IllegalStateException(e);
} catch (IOException e) {
throw new IllegalStateException(e);
}
shutdownHook = new BdbShutdownHook(this);
Runtime.getRuntime().addShutdownHook(shutdownHook);
}
public boolean isRunning() {
return shutdownHook!=null;
}
public void stop() {
if (!isRunning()) {
return;
}
close();
}
protected void setup(File f, boolean create)
throws DatabaseException {
EnvironmentConfig config = new EnvironmentConfig();
config.setAllowCreate(create);
config.setLockTimeout(75, TimeUnit.MINUTES); // set to max
if(getCachePercent()>0) {
config.setCachePercent(getCachePercent());
}
config.setSharedCache(getUseSharedCache());
// we take the advice literally from...
// http://www.oracle.com/technology/products/berkeley-db/faq/je_faq.html#33
long nLockTables = getExpectedConcurrency()-1;
while(!BigInteger.valueOf(nLockTables).isProbablePrime(Integer.MAX_VALUE)) {
nLockTables--;
}
config.setConfigParam("je.lock.nLockTables", Long.toString(nLockTables));
// triple this value to 6K because stats show many faults
config.setConfigParam("je.log.faultReadSize", "6144");
// to support checkpoints, prevent BDB's cleaner from deleting log files
config.setConfigParam("je.cleaner.expunge", "false");
f.mkdirs();
this.bdbEnvironment = new EnhancedEnvironment(f, config);
this.classCatalog = this.bdbEnvironment.getClassCatalog();
}
public void closeDatabase(Database db) {
try {
closeDatabase(db.getDatabaseName());
} catch (DatabaseException e) {
LOGGER.log(Level.SEVERE, "Error getting db name", e);
}
}
public void closeDatabase(String name) {
DatabasePlusConfig dpc = databases.remove(name);
if (dpc == null) {
throw new IllegalStateException("No such database: " + name);
}
Database db = dpc.database;
try {
db.sync();
db.close();
} catch (DatabaseException e) {
LOGGER.log(Level.WARNING, "Error closing db " + name, e);
}
}
public void disposeDatabase(String name) {
DatabasePlusConfig dpc = databases.remove(name);
if (dpc == null) {
throw new IllegalStateException("No such database: " + name);
}
Database db = dpc.database;
try {
db.close();
bdbEnvironment.removeDatabase(null, name);
} catch (DatabaseException e) {
LOGGER.log(Level.WARNING, "Error closing db " + name, e);
}
}
/**
* Open a Database inside this BdbModule's environment, and
* remember it for automatic close-at-module-stop.
*
* @param name
* @param config
* @param usePriorData
* @return
* @throws DatabaseException
*/
public Database openDatabase(String name, BdbConfig config, boolean usePriorData)
throws DatabaseException {
if(!isRunning()) {
// proper initialization hasn't occurred
throw new IllegalStateException(
"BdbModule not started; as a Lifecycle bean it must not be an inner bean.");
}
if (databases.containsKey(name)) {
DatabasePlusConfig dpc = databases.get(name);
if(dpc.config == config) {
// object-identical configs: OK to share DB
return dpc.database;
}
// unshared config object: might be name collision; error
throw new IllegalStateException("Database already exists: " +name);
}
DatabasePlusConfig dpc = new DatabasePlusConfig();
if (!usePriorData) {
try {
bdbEnvironment.truncateDatabase(null, name, false);
} catch (DatabaseNotFoundException e) {
// Ignored
}
}
dpc.database = bdbEnvironment.openDatabase(null, name, config.toDatabaseConfig());
dpc.name = name;
dpc.config = config;
databases.put(name, dpc);
return dpc.database;
}
public StoredClassCatalog getClassCatalog() {
return classCatalog;
}
public <K extends Serializable> StoredQueue<K> getStoredQueue(String dbname, Class<K> clazz, boolean b) {
try {
Database queueDb;
queueDb = openDatabase(dbname,
StoredQueue.databaseConfig(), false);
return new StoredQueue<K>(queueDb, clazz, getClassCatalog());
} catch (DatabaseException e) {
throw new RuntimeException(e);
}
}
/**
* Get a CachedBdbMap, backed by a BDB Database of the given name,
* with the given key and value class types. If 'recycle' is true,
* reuse values already in the database; otherwise start with an
* empty map.
*
* @param <K>
* @param <V>
* @param dbName
* @param recycle
* @param key
* @param value
* @return
* @throws DatabaseException
* @deprecated use ObjectIdentityBdbCache instead
*/
public <K,V> CachedBdbMap<K,V> getCBMMap(String dbName, boolean recycle,
Class<? super K> key, Class<? super V> value)
throws DatabaseException {
if (!recycle) {
try {
bdbEnvironment.truncateDatabase(null, dbName, false);
} catch (DatabaseNotFoundException e) {
// ignored
}
}
CachedBdbMap<K, V> r = new CachedBdbMap<K,V>(dbName);
r.initialize(bdbEnvironment, key, value, classCatalog);
oiCaches.put(dbName, r);
return r;
}
/**
* Get an ObjectIdentityBdbCache, backed by a BDB Database of the
* given name, with the given value class type. If 'recycle' is true,
* reuse values already in the database; otherwise start with an
* empty cache.
*
* @param <V>
* @param dbName
* @param recycle
* @param valueClass
* @return
* @throws DatabaseException
*/
public <V> ObjectIdentityBdbCache<V> getOIBCCache(String dbName, boolean recycle,
Class<? super V> valueClass)
throws DatabaseException {
if (!recycle) {
try {
bdbEnvironment.truncateDatabase(null, dbName, false);
} catch (DatabaseNotFoundException e) {
// ignored
}
}
ObjectIdentityBdbCache<V> oic = new ObjectIdentityBdbCache<V>();
oic.initialize(bdbEnvironment, dbName, valueClass, classCatalog);
oiCaches.put(dbName, oic);
return oic;
}
/** controls which alternate ObjectIdentityCache implementation to use */
private static boolean USE_OIBC = true;
/**
* Get an ObjectIdentityCache, backed by a BDB Database of the given
* name, with objects of the given valueClass type. If 'recycle' is
* true, reuse values already in the database; otherwise start with
* an empty cache.
*
* @param <V>
* @param dbName
* @param recycle
* @param valueClass
* @return
* @throws DatabaseException
*/
public <V> ObjectIdentityCache<String, V> getObjectCache(String dbName, boolean recycle,
Class<? super V> valueClass)
throws DatabaseException {
@SuppressWarnings("unchecked")
ObjectIdentityCache<String,V> oic = oiCaches.get(dbName);
if(oic!=null) {
return oic;
}
if(USE_OIBC) {
oic = getOIBCCache(dbName, recycle, valueClass);
} else {
oic = getCBMMap(dbName, recycle, String.class, valueClass);
}
return oic;
}
private void writeObject(ObjectOutputStream out) throws IOException {
out.defaultWriteObject();
}
public void startCheckpoint(Checkpoint checkpointInProgress) {}
@SuppressWarnings("unchecked")
public void doCheckpoint(Checkpoint checkpointInProgress) throws IOException {
// First sync objectCaches
for (ObjectIdentityCache oic : oiCaches.values()) {
oic.sync();
}
try {
// sync all databases
for (DatabasePlusConfig dbc: databases.values()) {
dbc.database.sync();
}
// Do a force checkpoint. Thats what a sync does (i.e. doSync).
CheckpointConfig chkptConfig = new CheckpointConfig();
chkptConfig.setForce(true);
// Mark Hayes of sleepycat says:
// "The default for this property is false, which gives the current
// behavior (allow deltas). If this property is true, deltas are
// prohibited -- full versions of internal nodes are always logged
// during the checkpoint. When a full version of an internal node
// is logged during a checkpoint, recovery does not need to process
// it at all. It is only fetched if needed by the application,
// during normal DB operations after recovery. When a delta of an
// internal node is logged during a checkpoint, recovery must
// process it by fetching the full version of the node from earlier
// in the log, and then applying the delta to it. This can be
// pretty slow, since it is potentially a large amount of
// random I/O."
// chkptConfig.setMinimizeRecoveryTime(true);
bdbEnvironment.checkpoint(chkptConfig);
LOGGER.fine("Finished bdb checkpoint.");
DbBackup dbBackup = new DbBackup(bdbEnvironment);
try {
dbBackup.startBackup();
File envCpDir = new File(dir.getFile(),checkpointInProgress.getName());
envCpDir.mkdirs();
File logfilesList = new File(envCpDir,"jdbfiles.manifest");
FileUtils.writeLines(logfilesList,
Arrays.asList(dbBackup.getLogFilesInBackupSet()));
LOGGER.fine("Finished processing bdb log files.");
} finally {
dbBackup.endBackup();
}
} catch (DatabaseException e) {
throw new IOException(e);
}
}
@SuppressWarnings("unchecked")
protected void doRecover() throws IOException {
File logfilesList = new File(
new File(dir.getFile(),recoveryCheckpoint.getName()),
"jdbfiles.manifest");
Set<String> retainLogfiles = new HashSet<String>(FileUtils.readLines(logfilesList));
IOFileFilter filter = FileFilterUtils.orFileFilter(
FileFilterUtils.suffixFileFilter(".jdb"),
FileFilterUtils.suffixFileFilter(".del"));
filter = FileFilterUtils.makeFileOnly(filter);
for(File f : dir.getFile().listFiles((FileFilter)filter)) {
if(retainLogfiles.contains(f.getName())) {
retainLogfiles.remove(f.getName());
continue;
}
String undelName = f.getName().replace(".del", ".jdb");
if(retainLogfiles.contains(undelName)) {
if(!f.renameTo(new File(f.getParentFile(),undelName))) {
throw new IOException("Unable to rename " + f + " to " +
undelName);
}
retainLogfiles.remove(f.getName());
}
// not needed; move aside
org.archive.util.FileUtils.moveAsideIfExists(f);
// TODO: log/warn of ruined later checkpoints?
}
if(retainLogfiles.size()>0) {
// some needed files weren't present
LOGGER.severe("needed log files missing: "+retainLogfiles);
}
}
public void finishCheckpoint(Checkpoint checkpointInProgress) {}
Checkpoint recoveryCheckpoint;
@Autowired(required=false)
public void setRecoveryCheckpoint(Checkpoint checkpoint) {
this.recoveryCheckpoint = checkpoint;
}
public void close() {
close2();
if(shutdownHook!=null) {
Runtime.getRuntime().removeShutdownHook(shutdownHook);
shutdownHook = null;
}
}
@SuppressWarnings("unchecked")
void close2() {
if (classCatalog == null) {
return;
}
for(ObjectIdentityCache cache : oiCaches.values()) {
try {
cache.close();
} catch (Exception e) {
LOGGER.log(Level.SEVERE, "Error closing oiCache " + cache, e);
}
}
List<String> dbNames = new ArrayList<String>(databases.keySet());
for (String dbName: dbNames) try {
closeDatabase(dbName);
} catch (Exception e) {
LOGGER.log(Level.SEVERE, "Error closing db " + dbName, e);
}
try {
this.bdbEnvironment.sync();
this.bdbEnvironment.close();
} catch (Exception e) {
LOGGER.log(Level.SEVERE, "Error closing environment.", e);
}
}
public Database getDatabase(String name) {
DatabasePlusConfig dpc = databases.get(name);
if (dpc == null) {
return null;
}
return dpc.database;
}
private static class BdbShutdownHook extends Thread {
final private BdbModule bdb;
public BdbShutdownHook(BdbModule bdb) {
this.bdb = bdb;
}
public void run() {
this.bdb.close2();
}
}
/** uniqueness serial number for temp map databases */
long sn = 0;
/**
* Creates a database-backed TempStoredSortedMap for transient
* reporting requirements. Calling the returned map's destroy()
* method when done discards the associated Database.
*
* @param <K>
* @param <V>
* @param dbName Database name to use; if null a name will be synthesized
* @param keyClass Class of keys; should be a Java primitive type
* @param valueClass Class of values; may be any serializable type
* @param allowDuplicates whether duplicate keys allowed
* @return
*/
public <K,V> DisposableStoredSortedMap<K, V> getStoredMap(String dbName, Class<K> keyClass, Class<V> valueClass, boolean allowDuplicates, boolean usePriorData) {
BdbConfig config = new BdbConfig();
config.setSortedDuplicates(allowDuplicates);
config.setAllowCreate(!usePriorData);
Database mapDb;
if(dbName==null) {
dbName = "tempMap-"+System.identityHashCode(this)+"-"+sn;
sn++;
}
final String openName = dbName;
try {
mapDb = openDatabase(openName,config,usePriorData);
} catch (DatabaseException e) {
throw new RuntimeException(e);
}
EntryBinding<V> valueBinding = TupleBinding.getPrimitiveBinding(valueClass);
if(valueBinding == null) {
valueBinding = new SerialBinding<V>(classCatalog, valueClass);
}
DisposableStoredSortedMap<K,V> storedMap = new DisposableStoredSortedMap<K, V>(
mapDb,
TupleBinding.getPrimitiveBinding(keyClass),
valueBinding,
true) {
@Override
public void dispose() {
BdbModule.this.disposeDatabase(openName);
}
;
};
return storedMap;
}
}