/* * eXist Open Source Native XML Database * Copyright (C) 2009 The eXist Project * http://exist-db.org * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public License * as published by the Free Software Foundation; either version 2 * of the License, or (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. * * $Id$ */ package org.exist.storage; import java.io.File; import java.io.IOException; import java.text.NumberFormat; import java.util.Calendar; import java.util.HashMap; import java.util.Iterator; import java.util.Map; import java.util.Observable; import java.util.Observer; import java.util.Properties; import java.util.Stack; import java.util.TreeMap; import java.util.Vector; import org.apache.log4j.Logger; import org.exist.EXistException; import org.exist.collections.Collection; import org.exist.collections.CollectionCache; import org.exist.collections.CollectionConfigurationManager; import org.exist.debuggee.Debuggee; import org.exist.debuggee.DebuggeeFactory; import org.exist.dom.SymbolTable; import org.exist.indexing.IndexManager; import org.exist.management.AgentFactory; import org.exist.numbering.DLNFactory; import org.exist.numbering.NodeIdFactory; import org.exist.scheduler.Scheduler; import org.exist.scheduler.SystemTaskJob; import org.exist.security.PermissionDeniedException; import org.exist.security.SecurityManager; import org.exist.security.User; import org.exist.storage.btree.DBException; import org.exist.storage.lock.FileLock; import org.exist.storage.lock.Lock; import org.exist.storage.lock.ReentrantReadWriteLock; import org.exist.storage.sync.Sync; import org.exist.storage.sync.SyncTask; import org.exist.storage.txn.TransactionException; import org.exist.storage.txn.TransactionManager; import org.exist.storage.txn.Txn; import org.exist.util.Configuration; import org.exist.util.DatabaseConfigurationException; import org.exist.util.ReadOnlyException; import org.exist.util.XMLReaderObjectFactory; import org.exist.util.XMLReaderPool; import org.exist.xmldb.ShutdownListener; import org.exist.xmldb.XmldbURI; import org.exist.xquery.PerformanceStats; /** * This class controls all available instances of the database. * Use it to configure, start and stop database instances. * You may have multiple instances defined, each using its own configuration. * To define multiple instances, pass an identification string to {@link #configure(String, int, int, Configuration)} * and use {@link #getInstance(String)} to retrieve an instance. * *@author Wolfgang Meier <wolfgang@exist-db.org> *@author Pierrick Brihaye <pierrick.brihaye@free.fr> */ //TODO : in the future, separate the design between the Map of DBInstances and their non static implementation public class BrokerPool extends Observable { private final static Logger LOG = Logger.getLogger(BrokerPool.class); private final static TreeMap instances = new TreeMap(); public final static String SIGNAL_STARTUP = "startup"; public final static String SIGNAL_SHUTDOWN = "shutdown"; /** * The name of a default database instance for those who are too lazy to provide parameters ;-). */ public final static String DEFAULT_INSTANCE_NAME = "exist"; public static final String CONFIGURATION_CONNECTION_ELEMENT_NAME = "db-connection"; public static final String CONFIGURATION_POOL_ELEMENT_NAME = "pool"; public static final String CONFIGURATION_SECURITY_ELEMENT_NAME = "security"; public static final String CONFIGURATION_RECOVERY_ELEMENT_NAME = "recovery"; public static final String DATA_DIR_ATTRIBUTE = "files"; //TODO : move elsewhere ? public final static String RECOVERY_ENABLED_ATTRIBUTE = "enabled"; public final static String RECOVERY_POST_RECOVERY_CHECK = "consistency-check"; //TODO : move elsewhere ? public final static String COLLECTION_CACHE_SIZE_ATTRIBUTE = "collectionCacheSize"; public final static String MIN_CONNECTIONS_ATTRIBUTE = "min"; public final static String MAX_CONNECTIONS_ATTRIBUTE = "max"; public final static String SYNC_PERIOD_ATTRIBUTE = "sync-period"; public final static String SHUTDOWN_DELAY_ATTRIBUTE = "wait-before-shutdown"; public final static String NODES_BUFFER_ATTRIBUTE = "nodesBuffer"; //Various configuration property keys (set by the configuration manager) public static final String PROPERTY_DATA_DIR = "db-connection.data-dir"; public final static String PROPERTY_MIN_CONNECTIONS = "db-connection.pool.min"; public final static String PROPERTY_MAX_CONNECTIONS = "db-connection.pool.max"; public final static String PROPERTY_SYNC_PERIOD = "db-connection.pool.sync-period"; public final static String PROPERTY_SHUTDOWN_DELAY = "wait-before-shutdown"; //TODO : move elsewhere ? public final static String PROPERTY_COLLECTION_CACHE_SIZE = "db-connection.collection-cache-size"; //TODO : move elsewhere ? Get fully qualified class name ? public final static String DEFAULT_SECURITY_CLASS = "org.exist.security.XMLSecurityManager"; public final static String PROPERTY_SECURITY_CLASS = "db-connection.security.class"; public final static String PROPERTY_RECOVERY_ENABLED = "db-connection.recovery.enabled"; public final static String PROPERTY_RECOVERY_CHECK = "db-connection.recovery.consistency-check"; public final static String PROPERTY_SYSTEM_TASK_CONFIG = "db-connection.system-task-config"; public static final String PROPERTY_NODES_BUFFER = "db-connection.nodes-buffer"; public static final String PROPERTY_EXPORT_ONLY = "db-connection.emergency"; public static final String DOC_ID_MODE_ATTRIBUTE = "doc-ids"; public static final String DOC_ID_MODE_PROPERTY = "db-connection.doc-ids.mode"; //TODO : inline the class ? or... make it configurable ? // WM: inline. I don't think users need to be able to overwrite this. // They can register their own shutdown hooks any time. private final static Thread shutdownHook = new Thread() { /** * Make sure that all instances are cleanly shut down. */ public void run() { LOG.info("Executing shutdown thread"); BrokerPool.stopAll(true); } }; //TODO : make this defaut value configurable ? useless if we have a registerShutdownHook(Thread aThread) method (null = deregister) private static boolean registerShutdownHook = true; private static Observer statusObserver = null; /** * Whether of not the JVM should run the shutdown thread. * @param register <code>true</code> if the JVM should run the thread */ //TODO : rename as activateShutdownHook ? or registerShutdownHook(Thread aThread) // WM: it is probably not necessary to allow users to register their own hook. This method // is only used once, by class org.exist.JettyStart, which registers its own hook. public final static void setRegisterShutdownHook(boolean register) { /* * TODO : call Runtime.getRuntime().removeShutdownHook or Runtime.getRuntime().registerShutdownHook * depending of the value of register * Since Java doesn't provide a convenient way to know if a shutdown hook has been registrered, * we may have to catch IllegalArgumentException */ //TODO : check that the JVM is not shutting down registerShutdownHook = register; } //TODO : make it non-static since every database instance may have its own policy. //TODO : make a defaut value that could be overwritten by the configuration // WM: this is only used by junit tests to test the recovery process. /** * For testing only: triggers a database corruption by disabling the page caches. The effect is * similar to a sudden power loss or the jvm being killed. The flag is used by some * junit tests to test the recovery process. */ public static boolean FORCE_CORRUPTION = false; /** * Creates and configures a default database instance and adds it to the pool. * Call this before calling {link #getInstance()}. * If a default database instance already exists, the new configuration is ignored. * @param minBrokers The minimum number of concurrent brokers for handling requests on the database instance. * @param maxBrokers The maximum number of concurrent brokers for handling requests on the database instance. * @param config The configuration object for the database instance * @throws EXistException *@exception EXistException If the initialization fails. */ //TODO : in the future, we should implement a Configurable interface public final static void configure(int minBrokers, int maxBrokers, Configuration config) throws EXistException, DatabaseConfigurationException { configure(DEFAULT_INSTANCE_NAME, minBrokers, maxBrokers, config); } /** * Creates and configures a database instance and adds it to the pool. * Call this before calling {link #getInstance()}. * If a database instance with the same name already exists, the new configuration is ignored. * @param instanceName A <strong>unique</strong> name for the database instance. * It is possible to have more than one database instance (with different configurations for example). * @param minBrokers The minimum number of concurrent brokers for handling requests on the database instance. * @param maxBrokers The maximum number of concurrent brokers for handling requests on the database instance. * @param config The configuration object for the database instance * @throws EXistException If the initialization fails. */ //TODO : in the future, we should implement a Configurable interface public final static void configure( String instanceName, int minBrokers, int maxBrokers, Configuration config) throws EXistException, DatabaseConfigurationException { //Check if there is a database instance in the pool with the same id BrokerPool instance = (BrokerPool) instances.get(instanceName); if (instance == null) { LOG.debug("configuring database instance '" + instanceName + "'..."); try { //Create the instance instance = new BrokerPool(instanceName, minBrokers, maxBrokers, config); //Add it to the pool instances.put(instanceName, instance); //We now have at least an instance... if(instances.size() == 1) { //... so a ShutdownHook may be interesting if(registerShutdownHook) { try { //... currently an eXist-specific one. TODO : make it configurable ? Runtime.getRuntime().addShutdownHook(shutdownHook); LOG.debug("shutdown hook registered"); } catch(IllegalArgumentException e) { LOG.warn("shutdown hook already registered"); } } } } catch (Throwable ex){ // Catch all possible issues and report. LOG.error("Unable to initialialize database instance '" + instanceName + "': "+ex.getMessage(), ex); } //TODO : throw an exception here rather than silently ignore an *explicit* parameter ? // WM: maybe throw an exception. Users can check if a db is already configured. } else LOG.warn("database instance '" + instanceName + "' is already configured"); } /** Returns whether or not the default database instance is configured. * @return <code>true</code> if it is configured */ //TODO : in the future, we should implement a Configurable interface public final static boolean isConfigured() { return isConfigured(DEFAULT_INSTANCE_NAME); } /** Returns whether or not a database instance is configured. * @param id The name of the database instance * @return <code>true</code> if it is configured */ //TODO : in the future, we should implement a Configurable interface public final static boolean isConfigured(String id) { //Check if there is a database instance in the pool with the same id BrokerPool instance = (BrokerPool) instances.get(id); //No : it *can't* be configured if (instance == null) return false; //Yes : it *may* be configured return instance.isInstanceConfigured(); } /**Returns a broker pool for the default database instance. * @return The broker pool * @throws EXistException If the database instance is not available (not created, stopped or not configured) */ public final static BrokerPool getInstance() throws EXistException { return getInstance(DEFAULT_INSTANCE_NAME); } /**Returns a broker pool for a database instance. * @param instanceName The name of the database instance * @return The broker pool * @throws EXistException If the instance is not available (not created, stopped or not configured) */ public final static BrokerPool getInstance(String instanceName) throws EXistException { //Check if there is a database instance in the pool with the same id BrokerPool instance = (BrokerPool) instances.get(instanceName); if (instance != null) //TODO : call isConfigured(id) and throw an EXistException if relevant ? return instance; else throw new EXistException("database instance '" + instanceName + "' is not available"); } /** Returns an iterator over the database instances. * @return The iterator */ public final static Iterator getInstances() { return instances.values().iterator(); } /** Stops the default database instance. After calling this method, it is * no longer configured. * @throws EXistException If the default database instance is not available (not created, stopped or not configured) */ public final static void stop() throws EXistException { stop(DEFAULT_INSTANCE_NAME); } /** Stops the given database instance. After calling this method, it is * no longer configured. * @param id The name of the database instance * @throws EXistException If the database instance is not available (not created, stopped or not configured) */ public final static void stop(String id) throws EXistException { BrokerPool instance = (BrokerPool) instances.get(id); if (instance == null) throw new EXistException("database instance '" + id + "' is not available"); instance.shutdown(); } /** Stops all the database instances. After calling this method, the database instances are * no longer configured. * @param killed <code>true</code> when invoked by an exiting JVM */ public final static void stopAll(boolean killed) { //Create a temporary vector Vector tmpInstances = new Vector(); for (Iterator i = instances.values().iterator(); i.hasNext();) { //and feed it with the living database instances tmpInstances.add(i.next()); } BrokerPool instance; //Iterate over the living database instances for (Iterator i = tmpInstances.iterator(); i.hasNext();) { instance = (BrokerPool) i.next(); if (instance.conf != null) //Shut them down instance.shutdown(killed); } //Clear the living instances container : they are all sentenced to death... instances.clear(); } public static void registerStatusObserver(Observer observer) { statusObserver = observer; LOG.debug("registering observer: " + observer.getClass().getName()); } /* END OF STATIC IMPLEMENTATION */ /** * Default values */ //TODO : make them static when we have 2 classes private final int DEFAULT_MIN_BROKERS = 1; private final int DEFAULT_MAX_BROKERS = 15; public final long DEFAULT_SYNCH_PERIOD = 120000; public final long DEFAULT_MAX_SHUTDOWN_WAIT = 45000; //TODO : move this default setting to org.exist.collections.CollectionCache ? public final int DEFAULT_COLLECTION_BUFFER_SIZE = 512; public static final String PROPERTY_PAGE_SIZE = "db-connection.page-size"; public static final int DEFAULT_PAGE_SIZE = 4096; /** * <code>true</code> if the database instance is able to handle transactions. */ private boolean transactionsEnabled; /** * The name of the database instance */ private String instanceName; private final static int OPERATING = 0; private final static int INITIALIZING = 1; private final static int SHUTDOWN = 2; private int status = OPERATING; /** * The number of brokers for the database instance */ private int brokersCount = 0; /** * The minimal number of brokers for the database instance */ private int minBrokers; /** * The maximal number of brokers for the database instance */ private int maxBrokers; /** * The number of inactive brokers for the database instance */ private Stack<DBBroker> inactiveBrokers = new Stack(); /** * The number of active brokers for the database instance */ private Map<Thread, DBBroker> activeBrokers = new HashMap<Thread, DBBroker>(); /** * The configuration object for the database instance */ protected Configuration conf = null; /** * <code>true</code> if a cache synchronization event is scheduled */ //TODO : rename as syncScheduled ? //TODO : alternatively, delete this member and create a Sync.NOSYNC event private boolean syncRequired = false; /** * The kind of scheduled cache synchronization event. * One of {@link org.exist.storage.sync.Sync#MAJOR_SYNC} or {@link org.exist.storage.sync.Sync#MINOR_SYNC} */ private int syncEvent = 0; private boolean checkpoint = false; /** * <code>true</code> if the database instance is running in read-only mode. */ //TODO : this should be computed by the DBrokers depending of their configuration/capabilities //TODO : for now, this member is used for recovery management private boolean isReadOnly; private int pageSize; private FileLock dataLock; /** * The transaction manager of the database instance. */ private TransactionManager transactionManager = null; /** * Delay (in ms) for running jobs to return when the database instance shuts down. */ private long maxShutdownWait; /** * The scheduler for the database instance. */ private Scheduler scheduler; /** * Manages pluggable index structures. */ private IndexManager indexManager; /** * Global symbol table used to encode element and attribute qnames. */ private SymbolTable symbols; /** * Cache synchronization on the database instance. */ private long majorSyncPeriod = DEFAULT_SYNCH_PERIOD; //the period after which a major sync should occur private long lastMajorSync = System.currentTimeMillis(); //time the last major sync occurred /** * The listener that is notified when the database instance shuts down. */ private ShutdownListener shutdownListener = null; /** * The security manager of the database instance. */ private SecurityManager securityManager = null; /** * The global notification service used to subscribe * to document updates. */ private NotificationService notificationService = null; private long nextSystemStatus = System.currentTimeMillis(); /** * The cache in which the database instance may store items. */ private DefaultCacheManager cacheManager; private CollectionCacheManager collectionCacheMgr; private long reservedMem; /** * The pool in which the database instance's <strong>compiled</strong> XQueries are stored. */ private XQueryPool xQueryPool; /** * The monitor in which the database instance's strong>running</strong> XQueries are managed. */ private ProcessMonitor processMonitor; /** * Global performance stats to gather function execution statistics * from all queries running on this database instance. */ private PerformanceStats xqueryStats; /** * The global manager for accessing collection configuration files from the database instance. */ private CollectionConfigurationManager collectionConfigurationManager = null; /** * The cache in which the database instance's collections are stored. */ protected CollectionCache collectionCache; /** * The pool in which the database instance's readers are stored. */ protected XMLReaderPool xmlReaderPool; private NodeIdFactory nodeFactory = new DLNFactory(); //TODO : is another value possible ? If no, make it static // WM: no, we need one lock per database instance. Otherwise we would lock another database. private Lock globalXUpdateLock = new ReentrantReadWriteLock("xupdate"); private User serviceModeUser = null; private boolean inServiceMode = false; //the time that the database was started private final Calendar startupTime = Calendar.getInstance(); /** Creates and configures the database instance. * @param instanceName A name for the database instance. * @param minBrokers The minimum number of concurrent brokers for handling requests on the database instance. * @param maxBrokers The maximum number of concurrent brokers for handling requests on the database instance. * @param conf The configuration object for the database instance * @throws EXistException If the initialization fails. */ //TODO : Then write a configure(int minBrokers, int maxBrokers, Configuration conf) method private BrokerPool(String instanceName, int minBrokers, int maxBrokers, Configuration conf) throws EXistException, DatabaseConfigurationException { Integer anInteger; Long aLong; Boolean aBoolean; NumberFormat nf = NumberFormat.getNumberInstance(); if (statusObserver != null) addObserver(statusObserver); //TODO : ensure that the instance name is unique ? //WM: needs to be done in the configure method. this.instanceName = instanceName; //TODO : find a nice way to (re)set the default values //TODO : create static final members for configuration keys this.minBrokers = DEFAULT_MIN_BROKERS; this.maxBrokers = DEFAULT_MAX_BROKERS; this.maxShutdownWait = DEFAULT_MAX_SHUTDOWN_WAIT; //TODO : read from configuration this.transactionsEnabled = true; this.minBrokers = minBrokers; this.maxBrokers = maxBrokers; /* * strange enough, the settings provided by the constructor may be overriden * by the ones *explicitely* provided by the constructor * TODO : consider a private constructor BrokerPool(String instanceName) then configure(int minBrokers, int maxBrokers, Configuration config) */ anInteger = (Integer) conf.getProperty(PROPERTY_MIN_CONNECTIONS); if (anInteger != null) this.minBrokers = anInteger.intValue(); anInteger = (Integer) conf.getProperty(PROPERTY_MAX_CONNECTIONS); if (anInteger != null) this.maxBrokers = anInteger.intValue(); //TODO : sanity check : minBrokers shall be lesser than or equal to maxBrokers //TODO : sanity check : minBrokers shall be positive LOG.info("database instance '" + instanceName + "' will have between " + nf.format(this.minBrokers) + " and " + nf.format(this.maxBrokers) + " brokers"); //TODO : use the periodicity of a SystemTask (see below) aLong = (Long) conf.getProperty(PROPERTY_SYNC_PERIOD); if (aLong != null) /*this.*/majorSyncPeriod = aLong.longValue(); //TODO : sanity check : the synch period should be reasonable LOG.info("database instance '" + instanceName + "' will be synchronized every " + nf.format(/*this.*/majorSyncPeriod) + " ms"); aLong = (Long) conf.getProperty(BrokerPool.PROPERTY_SHUTDOWN_DELAY); if (aLong != null) { this.maxShutdownWait = aLong.longValue(); } //TODO : sanity check : the shutdown period should be reasonable LOG.info("database instance '" + instanceName + "' will wait " + nf.format(this.maxShutdownWait) + " ms during shutdown"); aBoolean = (Boolean) conf.getProperty(PROPERTY_RECOVERY_ENABLED); if (aBoolean != null) { this.transactionsEnabled = aBoolean.booleanValue(); } LOG.info("database instance '" + instanceName + "' is enabled for transactions : " + this.transactionsEnabled); pageSize = conf.getInteger(PROPERTY_PAGE_SIZE); if (pageSize < 0) pageSize = DEFAULT_PAGE_SIZE; /* TODO: start -adam- remove OLD SystemTask initialization */ //How ugly : needs refactoring... /* Configuration.SystemTaskConfig systemTasksConfigs[] = (Configuration.SystemTaskConfig[]) conf.getProperty(BrokerPool.PROPERTY_SYSTEM_TASK_CONFIG); if (systemTasksConfigs != null) { for (int i = 0; i < systemTasksConfigs.length; i++) { try { Class clazz = Class.forName(systemTasksConfigs[i].getClassName()); SystemTask task = (SystemTask) clazz.newInstance(); if (!(task instanceof SystemTask)) //TODO : shall we ignore the exception ? throw new EXistException("'" + task.getClass().getName() + "' is not an instance of org.exist.storage.SystemTask"); task.configure(conf, systemTasksConfigs[i].getProperties()); systemTasks.add(task); //TODO : remove when SystemTask has a getPeriodicity() method systemTasksPeriods.add(systemTasksConfigs[i]); LOG.info("added system task instance '" + task.getClass().getName() + "' to be executed every " + nf.format(systemTasksConfigs[i].getPeriod()) + " ms"); } catch (ClassNotFoundException e) { //TODO : shall we ignore the exception ? throw new EXistException("system task class '" + systemTasksConfigs[i].getClassName() + "' not found"); } catch (InstantiationException e) { //TODO : shall we ignore the exception ? throw new EXistException("system task '" + systemTasksConfigs[i].getClassName() + "' can not be instantiated"); } catch (IllegalAccessException e) { //TODO : shall we ignore the exception ? throw new EXistException("system task '" + systemTasksConfigs[i].getClassName() + "' can not be accessed"); } } //TODO : why not add a default Sync task here if there is no instanceof Sync in systemTasks ? } */ /* TODO: end -adam- remove OLD SystemTask initialization */ //TODO : move this to initialize ? (cant as we need it for FileLockHeartBeat) scheduler = new Scheduler(this, conf); //TODO : since we need one :-( (see above) this.isReadOnly = !canReadDataDir(conf); LOG.debug("isReadOnly: " + isReadOnly); //Configuration is valid, save it this.conf = conf; //TODO : in the future, we should implement an Initializable interface initialize(); //TODO : move this to initialize ? //setup database synchronization job if (majorSyncPeriod > 0) { //TODO : why not automatically register Sync in system tasks ? // scheduler.createPeriodicJob(2500, new Sync(), 2500); SyncTask syncTask = new SyncTask(); scheduler.createPeriodicJob(2500, new SystemTaskJob(SyncTask.getJobName(), syncTask), 2500); } } //TODO : create a canReadJournalDir() method in the *relevant* class. The two directories may be different. protected boolean canReadDataDir(Configuration conf) throws EXistException { String dataDir = (String) conf.getProperty(PROPERTY_DATA_DIR); if (dataDir == null) dataDir = "data"; //TODO : DEFAULT_DATA_DIR File dir = new File(dataDir); if (!dir.exists()) { try { //TODO : shall we force the creation ? use a parameter to decide ? LOG.info("Data directory '" + dir.getAbsolutePath() + "' does not exist. Creating one ..."); dir.mkdirs(); } catch (SecurityException e) { LOG.info("Cannot create data directory '" + dir.getAbsolutePath() + "'. Switching to read-only mode."); return false; } } //Save it for further use. //TODO : "data-dir" has sense for *native* brokers conf.setProperty(PROPERTY_DATA_DIR, dataDir); if (!dir.canWrite()) { LOG.info("Cannot write to data directory: " + dir.getAbsolutePath() + ". Switching to read-only mode."); return false; } // try to acquire lock on the data dir dataLock = new FileLock(this, dir, "dbx_dir.lck"); try { boolean locked = dataLock.tryLock(); if (!locked) { throw new EXistException("The database directory seems to be locked by another " + "database instance. Found a valid lock file: " + dataLock.getFile()); } } catch (ReadOnlyException e) { LOG.info(e.getMessage() + ". Switching to read-only mode!!!"); return false; } return true; } protected SecurityManager newSecurityManager() { try { Class smClass = (Class)conf.getProperty(PROPERTY_SECURITY_CLASS); return (SecurityManager)smClass.newInstance(); } catch (Throwable ex) { LOG.warn("Exception while instantiating security manager class.", ex); } return null; } /**Initializes the database instance. * @throws EXistException */ protected void initialize() throws EXistException, DatabaseConfigurationException { if (LOG.isDebugEnabled()) LOG.debug("initializing database instance '" + instanceName + "'..."); //Flag to indicate that we are initializing status = INITIALIZING; signalSystemStatus(SIGNAL_STARTUP); boolean exportOnly = (Boolean) conf.getProperty(PROPERTY_EXPORT_ONLY, false); //REFACTOR : construct then configure cacheManager = new DefaultCacheManager(this); //REFACTOR : construct then configure xQueryPool = new XQueryPool(conf); //REFACTOR : construct then... configure processMonitor = new ProcessMonitor(); xqueryStats = new PerformanceStats(this); //REFACTOR : construct then... configure xmlReaderPool = new XMLReaderPool(conf, new XMLReaderObjectFactory(this), 5, 0); //REFACTOR : construct then... configure int bufferSize = conf.getInteger(PROPERTY_COLLECTION_CACHE_SIZE); if(bufferSize == -1) bufferSize = DEFAULT_COLLECTION_BUFFER_SIZE; collectionCache = new CollectionCache(this, bufferSize, 0.0001); collectionCacheMgr = new CollectionCacheManager(this, collectionCache); // compute how much memory should be reserved for caches to grow Runtime rt = Runtime.getRuntime(); long maxMem = rt.maxMemory(); long minFree = maxMem / 5; reservedMem = cacheManager.getTotalMem() + collectionCacheMgr.getMaxTotal() + minFree; LOG.debug("Reserved memory: " + reservedMem + "; max: " + maxMem + "; min: " + minFree); notificationService = new NotificationService(); //REFACTOR : construct then... configure //TODO : journal directory *may* be different from BrokerPool.PROPERTY_DATA_DIR transactionManager = new TransactionManager(this, new File((String) conf.getProperty(BrokerPool.PROPERTY_DATA_DIR)), isTransactional()); try { transactionManager.initialize(); } catch (ReadOnlyException e) { LOG.warn(e.getMessage() + ". Switching to read-only mode!!!"); isReadOnly = true; } symbols = new SymbolTable(this, conf); if (!symbols.getFile().canWrite()) { LOG.warn("Cannot write to " + symbols.getFile().getName() + ". Switching to read-only mode."); isReadOnly = true; } indexManager = new IndexManager(this, conf); //TODO : replace the following code by get()/release() statements ? // WM: I would rather tend to keep this broker reserved as a system broker. // create a first broker to initialize the security manager createBroker(); //TODO : this broker is *not* marked as active and *might* be reused by another process ! Is it intended ? // at this stage, the database is still single-threaded, so reusing the broker later is not a problem. DBBroker broker = inactiveBrokers.peek(); if (isReadOnly()) { transactionManager.setEnabled(false); } //Run the recovery process //TODO : assume boolean recovered = false; if (isTransactional()) { recovered = transactionManager.runRecovery(broker); //TODO : extract the following from this block ? What if we ware not transactional ? -pb if (!recovered) { if (broker.getCollection(XmldbURI.ROOT_COLLECTION_URI) == null) { Txn txn = transactionManager.beginTransaction(); try { //TODO : use a root collection final member broker.getOrCreateCollection(txn, XmldbURI.ROOT_COLLECTION_URI); transactionManager.commit(txn); } catch (IOException e) { transactionManager.abort(txn); } catch (PermissionDeniedException e) { transactionManager.abort(txn); } } } } /* initialise required collections if they dont exist yet */ if (!exportOnly) initialiseSystemCollections(broker); //TODO : from there, rethink the sequence of calls. // WM: attention: a small change in the sequence of calls can break // either normal startup or recovery. //create the security manager //TODO : why only the first broker has a security manager ? Global or attached to each broker ? // WM: there's only one security manager per BrokerPool, but it needs a DBBroker instance to read // the system collection. SecurityManager localSecurityManager = newSecurityManager(); securityManager = null; localSecurityManager.attach(this, broker); securityManager = localSecurityManager; status = OPERATING; //have to do this after initializing = false // so that the policies collection is saved if(securityManager.isXACMLEnabled()) securityManager.getPDP().initializePolicyCollection(); //Get a manager to handle further collectios configuration try { collectionConfigurationManager = new CollectionConfigurationManager(broker); } catch (Exception e) { LOG.error("Found an error while initializing database: " + e.getMessage(), e); } //If necessary, launch a task to repair the DB //TODO : merge this with the recovery process ? if (recovered) { try { broker.setUser(SecurityManager.SYSTEM_USER); broker.repair(); } catch (PermissionDeniedException e) { LOG.warn("Error during recovery: " + e.getMessage(), e); } if (((Boolean)conf.getProperty(PROPERTY_RECOVERY_CHECK)).booleanValue()) { ConsistencyCheckTask task = new ConsistencyCheckTask(); Properties props = new Properties(); props.setProperty("backup", "no"); props.setProperty("output", "sanity"); task.configure(conf, props); task.execute(broker); } } //OK : the DB is repaired; let's make a few RW operations // remove temporary docs broker.cleanUpTempResources(true); sync(broker, Sync.MAJOR_SYNC); //Create a default configuration file for the root collection //TODO : why can't we call this from within CollectionConfigurationManager ? //TODO : understand why we get a test suite failure //collectionConfigurationManager.checkRootCollectionConfigCollection(broker); //collectionConfigurationManager.checkRootCollectionConfig(broker); /* TODO: start adam */ //Schedule the system tasks /*for (int i = 0; i < systemTasks.size(); i++) { //TODO : remove first argument when SystemTask has a getPeriodicity() method initSystemTask((SingleInstanceConfiguration.SystemTaskConfig) systemTasksPeriods.get(i), (SystemTask)systemTasks.get(i)); } systemTasksPeriods = null;*/ /* TODO: end adam */ //Create the minimal number of brokers required by the configuration for (int i = 1; i < minBrokers; i++) createBroker(); // register some MBeans to provide access to this instance AgentFactory.getInstance().initDBInstance(this); if (LOG.isDebugEnabled()) LOG.debug("database instance '" + instanceName + "' initialized"); //setup any configured jobs //scheduler.setupConfiguredJobs(); //execute any startup jobs //scheduler.executeStartupJobs(); scheduler.run(); } //TODO : remove the period argument when SystemTask has a getPeriodicity() method //TODO : make it protected ? /*private void initSystemTask(SingleInstanceConfiguration.SystemTaskConfig config, SystemTask task) throws EXistException { try { if (config.getCronExpr() == null) { LOG.debug("Scheduling system maintenance task " + task.getClass().getName() + " every " + config.getPeriod() + " ms"); scheduler.createPeriodicJob(config.getPeriod(), new SystemTaskJob(task), config.getPeriod()); } else { LOG.debug("Scheduling system maintenance task " + task.getClass().getName() + " with cron expression: " + config.getCronExpr()); scheduler.createCronJob(config.getCronExpr(), new SystemTaskJob(task)); } } catch (Exception e) { LOG.warn(e.getMessage(), e); throw new EXistException("Failed to initialize system maintenance task: " + e.getMessage()); } }*/ /** * Initialise system collections, if it doesnt exist yet * * @param sysBroker The system broker from before the brokerpool is populated * @param sysCollectionUri XmldbURI of the collection to create * @param permissions The persmissions to set on the created collection */ private void initialiseSystemCollection(DBBroker sysBroker, XmldbURI sysCollectionUri, int permissions) throws EXistException { //create /db/system Collection sysCollection = sysBroker.getCollection(sysCollectionUri); if (sysCollection == null) { TransactionManager transact = getTransactionManager(); Txn txn = transact.beginTransaction(); try { sysCollection = sysBroker.getOrCreateCollection(txn, sysCollectionUri); if (sysCollection == null) throw new IOException("Could not create system collection: " + sysCollectionUri); sysCollection.setPermissions(permissions); sysBroker.saveCollection(txn, sysCollection); transact.commit(txn); } catch (Exception e) { transact.abort(txn); e.printStackTrace(); String msg = "Initialisation of system collections failed: " + e.getMessage(); LOG.error(msg, e); throw new EXistException(msg, e); } } } /** * Initialise required system collections, if they dont exist yet * * @param sysBroker - The system broker from before the brokerpool is populated * * @throws EXistException If a system collection cannot be created */ private void initialiseSystemCollections(DBBroker sysBroker) throws EXistException { //create /db/system initialiseSystemCollection(sysBroker, XmldbURI.SYSTEM_COLLECTION_URI, 0770); } public long getReservedMem() { return reservedMem - cacheManager.getSizeInBytes(); } public int getPageSize() { return pageSize; } public void signalSystemStatus(String signal) { if (System.currentTimeMillis() > nextSystemStatus) { setChanged(); notifyObservers(signal); nextSystemStatus = System.currentTimeMillis() + 10000; } } /** * Whether or not the database instance is being initialized. * * @return <code>true</code> is the database instance is being initialized */ // TODO : let's be positive and rename it as isInitialized ? public boolean isInitializing() { return status == INITIALIZING; } /** Returns the database instance's name. * @return The id */ //TODO : rename getInstanceName public String getId() { return instanceName; } /** * Returns the number of brokers currently serving requests for the database instance. * *@return The brokers count */ //TODO : rename as getActiveBrokers ? public int active() { return activeBrokers.size(); } public Map<Thread, DBBroker> getActiveBrokers() { return new HashMap<Thread, DBBroker>(activeBrokers); } /** * Returns the number of inactive brokers for the database instance. *@return The brokers count */ //TODO : rename as getInactiveBrokers ? public int available() { return inactiveBrokers.size(); } //TODO : getMin() method ? /** * Returns the maximal number of brokers for the database instance. * *@return The brokers count */ //TODO : rename as getMaxBrokers ? public int getMax() { return maxBrokers; } /** * Returns whether the database instance has been configured. * *@return <code>true</code> if the datbase instance is configured */ public final boolean isInstanceConfigured() { return conf != null; } /** * Returns the configuration object for the database instance. *@return The configuration */ public Configuration getConfiguration() { return conf; } //TODO : rename as setShutdwonListener ? public void registerShutdownListener(ShutdownListener listener) { //TODO : check that we are not shutting down shutdownListener = listener; } public NodeIdFactory getNodeFactory() { return nodeFactory; } /** * Returns the database instance's security manager * *@return The security manager */ public SecurityManager getSecurityManager() { return securityManager; } /** Returns the Scheduler * @return The scheduler */ public Scheduler getScheduler() { return scheduler; } public SymbolTable getSymbols() { return symbols; } public NotificationService getNotificationService() { return notificationService; } /** * Returns whether transactions can be handled by the database instance. * * @return <code>true</code> if transactions can be handled */ public boolean isTransactional() { //TODO : confusion between dataDir and a so-called "journalDir" ! return !isReadOnly && transactionsEnabled; } public boolean isReadOnly() { return isReadOnly; } public void setReadOnly() { isReadOnly = true; } public boolean isInServiceMode() { return inServiceMode; } public TransactionManager getTransactionManager() { return this.transactionManager; } /** * Returns a manager for accessing the database instance's collection configuration files. * @return The manager */ public CollectionConfigurationManager getConfigurationManager() { return collectionConfigurationManager; } /** * Returns a cache in which the database instance's collections are stored. * * @return The cache */ //TODO : rename as getCollectionCache ? public CollectionCache getCollectionsCache() { return collectionCache; } /** * Returns a cache in which the database instance's may store items. * * @return The cache */ public DefaultCacheManager getCacheManager() { return cacheManager; } public CollectionCacheManager getCollectionCacheMgr() { return collectionCacheMgr; } /** * Returns the index manager which handles all additional indexes not * being part of the database core. * * @return The IndexManager */ public IndexManager getIndexManager() { return indexManager; } /** * Returns a pool in which the database instance's <strong>compiled</strong> XQueries are stored. * * @return The pool */ public XQueryPool getXQueryPool() { return xQueryPool; } /** * Returns a monitor in which the database instance's <strong>running</strong> XQueries are managed. * * @return The monitor */ public ProcessMonitor getProcessMonitor() { return processMonitor; } /** * Returns the global profiler used to gather execution statistics * from all XQueries running on this db instance. * * @return the profiler */ public PerformanceStats getPerformanceStats() { return xqueryStats; } /** * Returns a pool in which the database instance's readers are stored. * * @return The pool */ public XMLReaderPool getParserPool() { return xmlReaderPool; } /** * Returns the global update lock for the database instance. * This lock is used by XUpdate operations to avoid that * concurrent XUpdate requests modify the database until all * document locks have been correctly set. * * @return The global lock */ //TODO : rename as getUpdateLock ? public Lock getGlobalUpdateLock() { return globalXUpdateLock; } /** Creates an inactive broker for the database instance. * @return The broker * @throws EXistException */ protected DBBroker createBroker() throws EXistException { //TODO : in the future, don't pass the whole configuration, just the part relevant to brokers DBBroker broker = BrokerFactory.getInstance(this, this.getConfiguration()); inactiveBrokers.push(broker); brokersCount++; broker.setId(broker.getClass().getName() + '_' + instanceName + "_" + brokersCount); LOG.debug( "created broker '" + broker.getId() + " for database instance '" + instanceName + "'"); return broker; } /** Returns an active broker for the database instance. * @return The broker * @throws EXistException If the instance is not available (stopped or not configured) */ //TODO : rename as getBroker ? getInstance (when refactored) ? public DBBroker get(User user) throws EXistException { if (!isInstanceConfigured()) { throw new EXistException("database instance '" + instanceName + "' is not available"); } synchronized(this) { //Try to get an active broker DBBroker broker = activeBrokers.get(Thread.currentThread()); //Use it... //TOUNDERSTAND (pb) : why not pop a broker from the inactive ones rather than maintaining reference counters ? // WM: a thread may call this more than once in the sequence of operations, i.e. calls to get/release can // be nested. Returning a new broker every time would lead to a deadlock condition if two threads have // to wait for a broker to become available. We thus use reference counts and return // the same broker instance for each thread. if(broker != null) { //increase its number of uses broker.incReferenceCount(); if (user != null) broker.setUser(user); return broker; //TODO : share the code with what is below (including notifyAll) ? // WM: notifyAll is not necessary if we don't have to wait for a broker. } //No active broker : get one ASAP while (serviceModeUser != null && user != null && !user.equals(serviceModeUser)) { try { LOG.debug("Db instance is in service mode. Waiting for db to become available again ..."); wait(); } catch (InterruptedException e) { } } //Are there any available brokers ? if (inactiveBrokers.isEmpty()) { //There are no available brokers. If allowed... if (brokersCount < maxBrokers) //... create one createBroker(); else //... or wait until there is one available while (inactiveBrokers.isEmpty()) { LOG.debug("waiting for a broker to become available"); try { this.wait(); } catch (InterruptedException e) { } } } broker = inactiveBrokers.pop(); //activate the broker activeBrokers.put(Thread.currentThread(), broker); broker.incReferenceCount(); if (user != null) broker.setUser(user); //Inform the other threads that we have a new-comer // TODO: do they really need to be informed here??????? this.notifyAll(); return broker; } } /** * Releases a broker for the database instance. If it is no more used, make if invactive. * If there are pending system maintenance tasks, * the method will block until these tasks have finished. * *@param broker The broker to be released */ //TODO : rename as releaseBroker ? releaseInstance (when refactored) ? public void release(DBBroker broker) { //TODO : Is this test accurate ? // might be null as release() is often called within a finally block if (broker == null) return; synchronized (this) { //TOUNDERSTAND (pb) : why maintain reference counters rather than pushing the brokers to the stack ? //TODO : first check that the broker is active ! If not, return immediately. broker.decReferenceCount(); if(broker.getReferenceCount() > 0) { //it is still in use and thus can't be marked as inactive return; } //Broker is no more used : inactivate it for (DBBroker inactiveBroker : inactiveBrokers) { if (broker == inactiveBroker) { LOG.error("Broker is already in the inactive list!!!"); return; } } if (activeBrokers.remove(Thread.currentThread())==null) { LOG.error("release() has been called from the wrong thread for broker "+broker.getId()); // Cleanup the state of activeBrokers for (Thread t : activeBrokers.keySet()) { if (activeBrokers.get(t)==broker) { activeBrokers.remove(t); break; } } } inactiveBrokers.push(broker); //If the database is now idle, do some useful stuff if(activeBrokers.isEmpty()) { //TODO : use a "clean" dedicated method (we have some below) ? if (syncRequired) { //Note that the broker is not yet really inactive ;-) sync(broker, syncEvent); this.syncRequired = false; this.checkpoint = false; } if (serviceModeUser != null && !broker.getUser().equals(serviceModeUser)) { inServiceMode = true; } } //Inform the other threads that someone is gone this.notifyAll(); } } public DBBroker enterServiceMode(User user) throws PermissionDeniedException { if (!user.hasDbaRole()) throw new PermissionDeniedException("Only users of group dba can switch the db to service mode"); serviceModeUser = user; synchronized (this) { if (!activeBrokers.isEmpty()) { while(!inServiceMode) { try { wait(); } catch (InterruptedException e) { } } } } inServiceMode = true; DBBroker broker = inactiveBrokers.peek(); checkpoint = true; sync(broker, Sync.MAJOR_SYNC); checkpoint = false; // Return a broker that can be used to perform system tasks return broker; } public void exitServiceMode(User user) throws PermissionDeniedException { if (!user.equals(serviceModeUser)) throw new PermissionDeniedException("The db has been locked by a different user"); serviceModeUser = null; inServiceMode = false; synchronized (this) { this.notifyAll(); } } /** * Reloads the security manager of the database instance. This method is * called for example when the <code>users.xml</code> file has been changed. * * @param broker A broker responsible for executing the job * * TOUNDERSTAND (pb) : why do we need a broker here ? Why not get and * release one when we're done? * WM: this is called from the Collection.store() methods to signal * that /db/system/users.xml has changed. * A broker is already available in these methods, so we use it here. */ public void reloadSecurityManager(DBBroker broker) { securityManager = newSecurityManager(); securityManager.attach(this, broker); LOG.debug("Security manager reloaded"); } public long getMajorSyncPeriod() { return majorSyncPeriod; } public long getLastMajorSync() { return lastMajorSync; } /** * Executes a waiting cache synchronization for the database instance. * @param broker A broker responsible for executing the job * @param syncEvent One of {@link org.exist.storage.sync.Sync#MINOR_SYNC} or {@link org.exist.storage.sync.Sync#MINOR_SYNC} */ //TODO : rename as runSync ? executeSync ? //TOUNDERSTAND (pb) : *not* synchronized, so... "executes" or, rather, "schedules" ? "executes" (WM) //TOUNDERSTAND (pb) : why do we need a broker here ? Why not get and release one when we're done ? // WM: the method will always be under control of the BrokerPool. It is guaranteed that no // other brokers are active when it is called. That's why we don't need to synchronize here. //TODO : make it protected ? public void sync(DBBroker broker, int syncEvent) { broker.sync(syncEvent); User user = broker.getUser(); //TODO : strange that it is set *after* the sunc method has been called. broker.setUser(SecurityManager.SYSTEM_USER); if (status != SHUTDOWN) broker.cleanUpTempResources(); if (syncEvent == Sync.MAJOR_SYNC) { try { if (!FORCE_CORRUPTION) transactionManager.checkpoint(checkpoint); } catch (TransactionException e) { LOG.warn(e.getMessage(), e); } cacheManager.checkCaches(); lastMajorSync = System.currentTimeMillis(); if (LOG.isDebugEnabled()) notificationService.debug(); } else { cacheManager.checkDistribution(); } //TODO : touch this.syncEvent and syncRequired ? //After setting the SYSTEM_USER above we must change back to the DEFAULT User to prevent a security problem //broker.setUser(User.DEFAULT); broker.setUser(user); } /** * Schedules a cache synchronization for the database instance. If the database instance is idle, * the cache synchronization will be run immediately. Otherwise, the task will be deffered * until all running threads have returned. * @param syncEvent One of {@link org.exist.storage.sync.Sync#MINOR_SYNC} or * {@link org.exist.storage.sync.Sync#MINOR_SYNC} */ public void triggerSync(int syncEvent) { //TOUNDERSTAND (pb) : synchronized, so... "schedules" or, rather, "executes" ? "schedules" (WM) if (status == SHUTDOWN) return; LOG.debug("Triggering sync: " + syncEvent); synchronized (this) { //Are there available brokers ? // TOUNDERSTAND (pb) : the trigger is ignored ! // WM: yes, it seems wrong!! // if(inactiveBrokers.size() == 0) // return; //TODO : switch on syncEvent and throw an exception if it is inaccurate ? //Is the database instance idle ? if (inactiveBrokers.size() == brokersCount) { //Borrow a broker //TODO : this broker is *not* marked as active and may be reused by another process ! // No other brokers are running at this time, so there's no risk. //TODO : use get() then release the broker ? // No, might lead to a deadlock. DBBroker broker = inactiveBrokers.pop(); //Do the synchonization job sync(broker, syncEvent); inactiveBrokers.push(broker); syncRequired = false; } else { //Put the synchonization job into the queue //TODO : check that we don't replace high priority Sync.MAJOR_SYNC by a lesser priority sync ! this.syncEvent = syncEvent; syncRequired = true; } } } /** * Schedules a system maintenance task for the database instance. If the database is idle, * the task will be run immediately. Otherwise, the task will be deffered * until all running threads have returned. * @param task The task */ //TOUNDERSTAND (pb) : synchronized, so... "schedules" or, rather, "executes" ? public void triggerSystemTask(SystemTask task) { transactionManager.triggerSystemTask(task); } /** * Shuts downs the database instance */ public void shutdown() { shutdown(false); } public boolean isShuttingDown() { return(status == SHUTDOWN); } /** * Shuts downs the database instance * @param killed <code>true</code> when the JVM is (cleanly) exiting */ public void shutdown(boolean killed) { if (status == SHUTDOWN) // we are already shut down return; LOG.info("Database is shutting down ..."); status = SHUTDOWN; processMonitor.stopRunningJobs(); java.util.concurrent.locks.Lock lock = transactionManager.getLock(); try { // wait for currently running system tasks before we shutdown // they will have a lock on the transactionManager lock.lock(); synchronized (this) { // release transaction log to allow remaining brokers to complete // their job lock.unlock(); notificationService.debug(); //Notify all running tasks that we are shutting down //Shutdown the scheduler scheduler.shutdown(false); //asynchronous while(!scheduler.isShutdown()) //wait for shutdown { try { wait(250); } catch(InterruptedException e) {} signalSystemStatus(SIGNAL_SHUTDOWN); } //Notify all running XQueries that we are shutting down processMonitor.killAll(500); //TODO : close other objects using varying methods ? set them to null ? //cacheManager.something(); //xQueryPool.something(); //collectionConfigurationManager.something(); //collectionCache.something(); //xmlReaderPool.close(); if (isTransactional()) transactionManager.getJournal().flushToLog(true, true); boolean hangingThreads = false; long waitStart = System.currentTimeMillis(); //Are there active brokers ? if (!activeBrokers.isEmpty()) { LOG.info("Waiting " + maxShutdownWait + "ms for remaining threads to shut down..."); while (!activeBrokers.isEmpty()) { try { //Wait until they become inactive... this.wait(1000); } catch (InterruptedException e) { } signalSystemStatus(SIGNAL_SHUTDOWN); //...or force the shutdown if(maxShutdownWait > -1 && System.currentTimeMillis() - waitStart > maxShutdownWait) { LOG.warn("Not all threads returned. Forcing shutdown ..."); hangingThreads = true; break; } } } LOG.debug("Calling shutdown ..."); // closing down external indexes try { indexManager.shutdown(); } catch (DBException e) { LOG.warn("Error during index shutdown: " + e.getMessage(), e); } //TODO : replace the following code by get()/release() statements ? // WM: deadlock risk if not all brokers returned properly. DBBroker broker = null; if (inactiveBrokers.isEmpty()) try { broker = createBroker(); } catch (EXistException e) { LOG.warn("could not create instance for shutdown. Giving up."); } else //TODO : this broker is *not* marked as active and may be reused by another process ! //TODO : use get() then release the broker ? // WM: deadlock risk if not all brokers returned properly. broker = inactiveBrokers.peek(); //TOUNDERSTAND (pb) : shutdown() is called on only *one* broker ? // WM: yes, the database files are shared, so only one broker is needed to close them for all if (broker != null) { broker.setUser(SecurityManager.SYSTEM_USER); broker.shutdown(); } collectionCacheMgr.deregisterCache(collectionCache); signalSystemStatus(SIGNAL_SHUTDOWN); if (hangingThreads) // do not write a checkpoint if some threads did not return before shutdown // there might be dirty transactions transactionManager.shutdown(false); else transactionManager.shutdown(true); // deregister JMX MBeans AgentFactory.getInstance().closeDBInstance(this); //Invalidate the configuration conf = null; //Clear the living instances container instances.remove(instanceName); if (!isReadOnly) // release the lock on the data directory dataLock.release(); LOG.info("shutdown complete !"); //Last instance closes the house... //TOUNDERSTAND (pb) : !killed or, rather, killed ? // TODO: WM: check usage of killed! if(instances.size() == 0 && !killed) { LOG.debug("removing shutdown hook"); Runtime.getRuntime().removeShutdownHook(shutdownHook); } if (shutdownListener != null) shutdownListener.shutdown(instanceName, instances.size()); } } finally { // clear instance variables, just to be sure they will be garbage collected // the test suite restarts the db a few hundred times transactionManager = null; collectionCache = null; collectionCacheMgr = null; xQueryPool = null; processMonitor = null; collectionConfigurationManager = null; notificationService = null; indexManager = null; scheduler = null; xmlReaderPool = null; shutdownListener = null; securityManager = null; notificationService = null; } } //TODO : move this elsewhere public void triggerCheckpoint() { if (syncRequired) return; synchronized (this) { syncEvent = Sync.MAJOR_SYNC; syncRequired = true; checkpoint = true; } } private Debuggee debuggee = null; public Debuggee getDebuggee() { synchronized (this) { if (debuggee == null) debuggee = DebuggeeFactory.getInstance(); } return debuggee; } public Calendar getStartupTime() { return startupTime; } }