/* * eXist Open Source Native XML Database * Copyright (C) 2003-2016 The eXist-db 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. */ package org.exist.storage; import com.evolvedbinary.j8fu.fsm.AtomicFSM; import com.evolvedbinary.j8fu.fsm.FSM; import net.jcip.annotations.GuardedBy; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; import org.exist.Database; import org.exist.EXistException; import org.exist.collections.Collection; import org.exist.collections.CollectionCache; import org.exist.collections.CollectionConfiguration; import org.exist.collections.CollectionConfigurationManager; import org.exist.collections.triggers.*; import org.exist.config.ConfigurationDocumentTrigger; import org.exist.config.Configurator; import org.exist.config.annotation.ConfigurationClass; import org.exist.config.annotation.ConfigurationFieldAsAttribute; import org.exist.debuggee.Debuggee; import org.exist.debuggee.DebuggeeFactory; import org.exist.dom.persistent.SymbolTable; import org.exist.indexing.IndexManager; import org.exist.management.AgentFactory; import org.exist.numbering.DLNFactory; import org.exist.numbering.NodeIdFactory; import org.exist.plugin.PluginsManager; import org.exist.plugin.PluginsManagerImpl; import org.exist.repo.ClasspathHelper; import org.exist.repo.ExistRepository; import org.exist.scheduler.Scheduler; import org.exist.scheduler.impl.QuartzSchedulerImpl; import org.exist.scheduler.impl.SystemTaskJobImpl; import org.exist.security.*; import org.exist.security.SecurityManager; import org.exist.security.internal.SecurityManagerImpl; import org.exist.storage.btree.DBException; import org.exist.storage.journal.JournalManager; import org.exist.storage.lock.DeadlockDetection; import org.exist.storage.lock.FileLockService; import org.exist.storage.recovery.RecoveryManager; 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.*; import org.exist.xmldb.ShutdownListener; import org.exist.xmldb.XmldbURI; import org.exist.xquery.PerformanceStats; import org.exist.xquery.XQuery; import java.io.IOException; import java.io.PrintWriter; import java.io.StringWriter; import java.lang.ref.Reference; import java.lang.reflect.Array; import java.lang.reflect.Field; import java.nio.file.FileStore; import java.nio.file.Path; import java.text.NumberFormat; import java.util.*; import java.util.Map.Entry; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentSkipListSet; import java.util.concurrent.locks.Lock; import java.util.concurrent.locks.ReentrantLock; import static com.evolvedbinary.j8fu.fsm.TransitionTable.transitionTable; /** * 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, Optional<Observer>)} * and use {@link #getInstance(String)} to retrieve an instance. * * @author Wolfgang Meier <wolfgang@exist-db.org> * @author Pierrick Brihaye <pierrick.brihaye@free.fr> * @author Adam Retter <adam@exist-db.org> */ @ConfigurationClass("pool") public class BrokerPool extends BrokerPools implements BrokerPoolConstants, Database { private final static Logger LOG = LogManager.getLogger(BrokerPool.class); private final BrokerPoolServicesManager servicesManager = new BrokerPoolServicesManager(); private StatusReporter statusReporter = null; private final XQuery xqueryService = new XQuery(); //TODO : make it non-static since every database instance may have its own policy. //TODO : make a default 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; /** * <code>true</code> if the database instance is able to perform recovery. */ private final boolean recoveryEnabled; /** * The name of the database instance */ private final String instanceName; /** * State of the BrokerPool instance */ private enum State { SHUTTING_DOWN, SHUTDOWN, INITIALIZING, INITIALIZING_SYSTEM_MODE, INITIALIZING_MULTI_USER_MODE, OPERATIONAL } private enum Event { INITIALIZE, INITIALIZE_SYSTEM_MODE, INITIALIZE_MULTI_USER_MODE, READY, START_SHUTDOWN, FINISHED_SHUTDOWN, } @SuppressWarnings("unchecked") private final FSM<State, Event> status = new AtomicFSM<>(State.SHUTDOWN, transitionTable(State.class, Event.class) .when(State.SHUTDOWN).on(Event.INITIALIZE).switchTo(State.INITIALIZING) .when(State.INITIALIZING).on(Event.INITIALIZE_SYSTEM_MODE).switchTo(State.INITIALIZING_SYSTEM_MODE) .when(State.INITIALIZING_SYSTEM_MODE).on(Event.INITIALIZE_MULTI_USER_MODE).switchTo(State.INITIALIZING_MULTI_USER_MODE) .when(State.INITIALIZING_MULTI_USER_MODE).on(Event.READY).switchTo(State.OPERATIONAL) .when(State.OPERATIONAL).on(Event.START_SHUTDOWN).switchTo(State.SHUTTING_DOWN) .when(State.SHUTTING_DOWN).on(Event.FINISHED_SHUTDOWN).switchTo(State.SHUTDOWN) .build() ); /** * The number of brokers for the database instance */ private int brokersCount = 0; /** * The minimal number of brokers for the database instance */ @ConfigurationFieldAsAttribute("min") private final int minBrokers; /** * The maximal number of brokers for the database instance */ @ConfigurationFieldAsAttribute("max") private final int maxBrokers; /** * The number of inactive brokers for the database instance */ private final Deque<DBBroker> inactiveBrokers = new ArrayDeque<>(); /** * The number of active brokers for the database instance */ private final Map<Thread, DBBroker> activeBrokers = new ConcurrentHashMap<>(); /** * Used when TRACE level logging is enabled * to provide a history of broker leases */ private final Map<String, TraceableStateChanges<TraceableBrokerLeaseChange.BrokerInfo, TraceableBrokerLeaseChange.Change>> brokerLeaseChangeTrace = LOG.isTraceEnabled() ? new HashMap<>() : null; private final Map<String, List<TraceableStateChanges<TraceableBrokerLeaseChange.BrokerInfo, TraceableBrokerLeaseChange.Change>>> brokerLeaseChangeTraceHistory = LOG.isTraceEnabled() ? new HashMap<>() : null; /** * The configuration object for the database instance */ private final Configuration conf; private final ConcurrentSkipListSet<Observer> statusObservers = new ConcurrentSkipListSet<>(); /** * <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} */ private Sync syncEvent = Sync.MINOR; private boolean checkpoint = false; /** * Indicates whether the database is operating in read-only mode */ @GuardedBy("itself") private Boolean readOnly = Boolean.FALSE; @ConfigurationFieldAsAttribute("pageSize") private final int pageSize; private FileLockService dataLock; /** * The journal manager of the database instance. */ private Optional<JournalManager> journalManager = Optional.empty(); /** * 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. */ @ConfigurationFieldAsAttribute("wait-before-shutdown") private final long maxShutdownWait; /** * The scheduler for the database instance. */ @ConfigurationFieldAsAttribute("scheduler") 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. */ @ConfigurationFieldAsAttribute("sync-period") private final long majorSyncPeriod; //the period after which a major sync should occur private long lastMajorSync = System.currentTimeMillis(); //time the last major sync occurred private final long diskSpaceMin; /** * 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 plugin manager. */ private PluginsManagerImpl pluginManager = null; /** * The global notification service used to subscribe * to document updates. */ private NotificationService notificationService = null; /** * 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. */ //TODO : rename as collectionsCache ? private CollectionCache collectionCache; /** * The pool in which the database instance's readers are stored. */ private XMLReaderPool xmlReaderPool; private final NodeIdFactory nodeFactory = new DLNFactory(); private final Lock globalXUpdateLock = new ReentrantLock(); private Subject serviceModeUser = null; private boolean inServiceMode = false; //the time that the database was started private final Calendar startupTime = Calendar.getInstance(); private final Optional<BrokerWatchdog> watchdog; private final ClassLoader classLoader; private Optional<ExistRepository> expathRepo = Optional.empty(); private StartupTriggersManager startupTriggersManager; /** * 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 * @param statusObserver Observes the status of this database instance * * @throws EXistException If the initialization fails. */ //TODO : Then write a configure(int minBrokers, int maxBrokers, Configuration conf) method BrokerPool(final String instanceName, final int minBrokers, final int maxBrokers, final Configuration conf, final Optional<Observer> statusObserver) { final NumberFormat nf = NumberFormat.getNumberInstance(); this.classLoader = Thread.currentThread().getContextClassLoader(); this.instanceName = instanceName; this.maxShutdownWait = conf.getProperty(BrokerPool.PROPERTY_SHUTDOWN_DELAY, DEFAULT_MAX_SHUTDOWN_WAIT); LOG.info("database instance '" + instanceName + "' will wait " + nf.format(this.maxShutdownWait) + " ms during shutdown"); this.recoveryEnabled = conf.getProperty(PROPERTY_RECOVERY_ENABLED, true); LOG.info("database instance '" + instanceName + "' is enabled for recovery : " + this.recoveryEnabled); this.minBrokers = conf.getProperty(PROPERTY_MIN_CONNECTIONS, minBrokers); this.maxBrokers = conf.getProperty(PROPERTY_MAX_CONNECTIONS, maxBrokers); LOG.info("database instance '" + instanceName + "' will have between " + nf.format(this.minBrokers) + " and " + nf.format(this.maxBrokers) + " brokers"); this.majorSyncPeriod = conf.getProperty(PROPERTY_SYNC_PERIOD, DEFAULT_SYNCH_PERIOD); LOG.info("database instance '" + instanceName + "' will be synchronized every " + nf.format(/*this.*/majorSyncPeriod) + " ms"); // convert from bytes to megabytes: 1024 * 1024 this.diskSpaceMin = 1024l * 1024l * conf.getProperty(BrokerPool.DISK_SPACE_MIN_PROPERTY, DEFAULT_DISK_SPACE_MIN); this.pageSize = conf.getProperty(PROPERTY_PAGE_SIZE, DEFAULT_PAGE_SIZE); //Configuration is valid, save it this.conf = conf; statusObserver.ifPresent(this.statusObservers::add); this.watchdog = Optional.ofNullable(System.getProperty(BrokerWatchdog.TRACE_BROKERS_PROPERTY_NAME)) .filter(value -> value.equals("yes")) .map(value -> new BrokerWatchdog()); } /** * Initializes the database instance. * * @throws EXistException * @throws DatabaseConfigurationException */ void initialize() throws EXistException, DatabaseConfigurationException { try { _initialize(); } catch(final Throwable e) { // remove that file lock we may have acquired in canReadDataDir synchronized(readOnly) { if (dataLock != null && !readOnly) { dataLock.release(); } } if(e instanceof EXistException) { throw (EXistException) e; } else if(e instanceof DatabaseConfigurationException) { throw (DatabaseConfigurationException) e; } else { throw new EXistException(e); } } } private void _initialize() throws EXistException, DatabaseConfigurationException { //Flag to indicate that we are initializing status.process(Event.INITIALIZE); if(LOG.isDebugEnabled()) { LOG.debug("initializing database instance '" + instanceName + "'..."); } // register core broker pool services this.scheduler = servicesManager.register(new QuartzSchedulerImpl(this)); // NOTE: this must occur after the scheduler, and before any other service which requires access to the data directory this.dataLock = servicesManager.register(new FileLockService("dbx_dir.lck", BrokerPool.PROPERTY_DATA_DIR, NativeBroker.DEFAULT_DATA_DIR)); this.securityManager = servicesManager.register(new SecurityManagerImpl(this)); this.cacheManager = servicesManager.register(new DefaultCacheManager(this)); this.xQueryPool = servicesManager.register(new XQueryPool()); this.processMonitor = servicesManager.register(new ProcessMonitor()); this.xqueryStats = servicesManager.register(new PerformanceStats(this)); final XMLReaderObjectFactory xmlReaderObjectFactory = servicesManager.register(new XMLReaderObjectFactory()); this.xmlReaderPool = servicesManager.register(new XMLReaderPool(xmlReaderObjectFactory, 5, 0)); final int bufferSize = Optional.of(conf.getInteger(PROPERTY_COLLECTION_CACHE_SIZE)) .filter(size -> size != -1) .orElse(DEFAULT_COLLECTION_BUFFER_SIZE); this.collectionCache = servicesManager.register(new CollectionCache(this, bufferSize, 0.000001)); this.collectionCacheMgr = servicesManager.register(new CollectionCacheManager(this, collectionCache)); this.notificationService = servicesManager.register(new NotificationService()); this.journalManager = recoveryEnabled ? Optional.of(new JournalManager()) : Optional.empty(); if(journalManager.isPresent()) { servicesManager.register(journalManager.get()); } final SystemTaskManager systemTaskManager = servicesManager.register(new SystemTaskManager(this)); this.transactionManager = servicesManager.register(new TransactionManager(this, journalManager, systemTaskManager)); this.symbols = servicesManager.register(new SymbolTable()); this.expathRepo = Optional.ofNullable(new ExistRepository()); if(expathRepo.isPresent()) { servicesManager.register(expathRepo.get()); } servicesManager.register(new ClasspathHelper()); this.indexManager = servicesManager.register(new IndexManager(this)); //prepare those services that require system (single-user) mode this.pluginManager = servicesManager.register(new PluginsManagerImpl()); //Get a manager to handle further collections configuration this.collectionConfigurationManager = servicesManager.register(new CollectionConfigurationManager(this)); this.startupTriggersManager = servicesManager.register(new StartupTriggersManager()); //configure the registered services try { servicesManager.configureServices(conf); } catch(final BrokerPoolServiceException e) { throw new EXistException(e); } // calculate how much memory is reserved for caches to grow final Runtime rt = Runtime.getRuntime(); final long maxMem = rt.maxMemory(); final long minFree = maxMem / 5; reservedMem = cacheManager.getTotalMem() + collectionCacheMgr.getMaxTotal() + minFree; LOG.debug("Reserved memory: " + reservedMem + "; max: " + maxMem + "; min: " + minFree); //prepare the registered services, before entering system (single-user) mode try { servicesManager.prepareServices(this); } catch(final BrokerPoolServiceException e) { throw new EXistException(e); } //setup database synchronization job if(majorSyncPeriod > 0) { final SyncTask syncTask = new SyncTask(); syncTask.configure(conf, null); scheduler.createPeriodicJob(2500, new SystemTaskJobImpl(SyncTask.getJobName(), syncTask), 2500); } try { statusReporter = new StatusReporter(SIGNAL_STARTUP); statusObservers.forEach(statusReporter::addObserver); final Thread statusThread = new Thread(statusReporter); statusThread.start(); // statusReporter may have to be terminated or the thread can/will hang. try { final boolean exportOnly = conf.getProperty(PROPERTY_EXPORT_ONLY, false); // If the initialization fails after transactionManager has been created this method better cleans up // or the FileSyncThread for the journal can/will hang. try { // Enter System Mode try(final DBBroker systemBroker = get(Optional.of(securityManager.getSystemSubject()))) { status.process(Event.INITIALIZE_SYSTEM_MODE); if(isReadOnly()) { journalManager.ifPresent(JournalManager::disableJournalling); } //Run the recovery process boolean recovered = false; if(isRecoveryEnabled()) { recovered = runRecovery(systemBroker); //TODO : extract the following from this block ? What if we are not transactional ? -pb if(!recovered) { try { if(systemBroker.getCollection(XmldbURI.ROOT_COLLECTION_URI) == null) { final Txn txn = transactionManager.beginTransaction(); try { systemBroker.getOrCreateCollection(txn, XmldbURI.ROOT_COLLECTION_URI); transactionManager.commit(txn); } catch(final IOException | TriggerException | PermissionDeniedException e) { transactionManager.abort(txn); } finally { transactionManager.close(txn); } } } catch(final PermissionDeniedException pde) { LOG.fatal(pde.getMessage(), pde); } } } /* initialise required collections if they don't exist yet */ if(!exportOnly) { try { initialiseSystemCollections(systemBroker); } catch(final PermissionDeniedException pde) { LOG.error(pde.getMessage(), pde); throw new EXistException(pde.getMessage(), pde); } } statusReporter.setStatus(SIGNAL_READINESS); try { servicesManager.startSystemServices(systemBroker); } catch(final BrokerPoolServiceException e) { throw new EXistException(e); } //If necessary, launch a task to repair the DB //TODO : merge this with the recovery process ? if(isRecoveryEnabled() && recovered) { if(!exportOnly) { reportStatus("Reindexing database files..."); try { systemBroker.repair(); } catch(final PermissionDeniedException e) { LOG.warn("Error during recovery: " + e.getMessage(), e); } } if(((Boolean) conf.getProperty(PROPERTY_RECOVERY_CHECK)).booleanValue()) { final ConsistencyCheckTask task = new ConsistencyCheckTask(); final Properties props = new Properties(); props.setProperty("backup", "no"); props.setProperty("output", "sanity"); task.configure(conf, props); task.execute(systemBroker); } } //OK : the DB is repaired; let's make a few RW operations statusReporter.setStatus(SIGNAL_WRITABLE); //initialize configurations watcher trigger if(!exportOnly) { try { initialiseTriggersForCollections(systemBroker, XmldbURI.SYSTEM_COLLECTION_URI); } catch(final PermissionDeniedException pde) { //XXX: do not catch exception! LOG.error(pde.getMessage(), pde); } } // remove temporary docs try { systemBroker.cleanUpTempResources(true); } catch(final PermissionDeniedException pde) { LOG.error(pde.getMessage(), pde); } sync(systemBroker, Sync.MAJOR); // we have completed all system mode operations // we can now prepare those services which need // system mode before entering multi-user mode try { servicesManager.startPreMultiUserSystemServices(systemBroker); } catch(final BrokerPoolServiceException e) { throw new EXistException(e); } } //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); //Create the minimal number of brokers required by the configuration for(int i = 1; i < minBrokers; i++) { createBroker(); } status.process(Event.INITIALIZE_MULTI_USER_MODE); // register some MBeans to provide access to this instance AgentFactory.getInstance().initDBInstance(this); if(LOG.isDebugEnabled()) { LOG.debug("database instance '" + instanceName + "' initialized"); } servicesManager.startMultiUserServices(this); status.process(Event.READY); statusReporter.setStatus(SIGNAL_STARTED); } catch(final Throwable t) { transactionManager.shutdown(); throw t; } } catch(final EXistException e) { throw e; } catch(final Throwable t) { throw new EXistException(t.getMessage(), t); } } finally { if(statusReporter != null) { statusReporter.terminate(); statusReporter = null; } } } /** * Initialise system collections, if it doesn't exist yet * * @param sysBroker The system broker from before the brokerpool is populated * @param sysCollectionUri XmldbURI of the collection to create * @param permissions The permissions to set on the created collection */ private void initialiseSystemCollection(final DBBroker sysBroker, final XmldbURI sysCollectionUri, final int permissions) throws EXistException, PermissionDeniedException { Collection collection = sysBroker.getCollection(sysCollectionUri); if(collection == null) { final TransactionManager transact = getTransactionManager(); try(final Txn txn = transact.beginTransaction()) { collection = sysBroker.getOrCreateCollection(txn, sysCollectionUri); if(collection == null) { throw new IOException("Could not create system collection: " + sysCollectionUri); } collection.setPermissions(permissions); sysBroker.saveCollection(txn, collection); transact.commit(txn); } catch(final Exception e) { e.printStackTrace(); final String msg = "Initialisation of system collections failed: " + e.getMessage(); LOG.error(msg, e); throw new EXistException(msg, e); } } } /** * Initialize required system collections, if they don't exist yet * * @param broker - The system broker from before the brokerpool is populated * @throws EXistException If a system collection cannot be created */ private void initialiseSystemCollections(final DBBroker broker) throws EXistException, PermissionDeniedException { //create /db/system initialiseSystemCollection(broker, XmldbURI.SYSTEM_COLLECTION_URI, Permission.DEFAULT_SYSTEM_COLLECTION_PERM); } private void initialiseTriggersForCollections(final DBBroker broker, final XmldbURI uri) throws EXistException, PermissionDeniedException { final Collection collection = broker.getCollection(uri); //initialize configurations watcher trigger if(collection != null) { final CollectionConfigurationManager manager = getConfigurationManager(); final CollectionConfiguration collConf = manager.getOrCreateCollectionConfiguration(broker, collection); final DocumentTriggerProxy triggerProxy = new DocumentTriggerProxy(ConfigurationDocumentTrigger.class); //, collection.getURI()); collConf.documentTriggers().add(triggerProxy); } } /** * Run a database recovery if required. This method is called once during * startup from {@link org.exist.storage.BrokerPool}. * * @param broker * @throws EXistException */ public boolean runRecovery(final DBBroker broker) throws EXistException { final boolean forceRestart = conf.getProperty(PROPERTY_RECOVERY_FORCE_RESTART, false); if(LOG.isDebugEnabled()) { LOG.debug("ForceRestart = " + forceRestart); } if(journalManager.isPresent()) { final RecoveryManager recovery = new RecoveryManager(broker, journalManager.get(), forceRestart); return recovery.recover(); } else { throw new IllegalStateException("Cannot run recovery without a JournalManager"); } } public long getReservedMem() { return reservedMem - cacheManager.getCurrentSize(); } public int getPageSize() { return pageSize; } /** * Returns the class loader used when this BrokerPool was configured. */ public ClassLoader getClassLoader() { return this.classLoader; } /** * Whether or not the database instance is operational, i.e. initialization * has completed * * @return <code>true</code> if the database instance is operational */ public boolean isOperational() { return status.getCurrentState() == State.OPERATIONAL; } /** * 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 * @deprecated use countActiveBrokers */ //TODO : rename as getActiveBrokers ? public int active() { return activeBrokers.size(); } /** * Returns the number of brokers currently serving requests for the database instance. * * @return The active brokers count. */ @Override public int countActiveBrokers() { return activeBrokers.size(); } public Map<Thread, DBBroker> getActiveBrokers() { return new HashMap<>(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; } public int total() { return brokersCount; } /** * 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; } public Optional<ExistRepository> getExpathRepo() { return expathRepo; } //TODO : rename as setShutdwonListener ? public void registerShutdownListener(final 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 isRecoveryEnabled() { synchronized(readOnly) { return !readOnly && recoveryEnabled; } } @Override public boolean isReadOnly() { synchronized(readOnly) { if(!readOnly) { final long freeSpace = FileUtils.measureFileStore(dataLock.getFile(), FileStore::getUsableSpace); if (freeSpace != -1 && freeSpace < diskSpaceMin) { LOG.fatal("Partition containing DATA_DIR: " + dataLock.getFile().toAbsolutePath().toString() + " is running out of disk space [" + freeSpace + "]. " + "Switching eXist-db to read only to prevent data loss!"); setReadOnly(); } } return readOnly; } } public void setReadOnly() { LOG.warn("Switching database into read-only mode!"); synchronized (readOnly) { readOnly = true; } } public boolean isInServiceMode() { return inServiceMode; } public Optional<JournalManager> getJournalManager() { return journalManager; } public TransactionManager getTransactionManager() { return transactionManager; } /** * Returns a manager for accessing the database instance's collection configuration files. * * @return The manager */ @Override public CollectionConfigurationManager getConfigurationManager() { return collectionConfigurationManager; } /** * Returns a cache in which the database instance's collections are stored. * * @return The cache */ public CollectionCache getCollectionsCache() { return collectionCache; } /** * Returns a cache in which the database instance's may store items. * * @return The cache */ @Override 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 */ @Override 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; } /** * Retuns the XQuery Service * * @return The XQuery service */ public XQuery getXQueryService() { return xqueryService; } /** * 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 final 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; } /** * Get active broker for current thread * * Note - If you call getActiveBroker() you must not call * release on both the returned active broker and the original * lease from {@link BrokerPool#getBroker()} or {@link BrokerPool#get(Optional)} * otherwise release will have been called more than get! * * @return Database broker * @throws RuntimeException NO broker available for current thread. */ public DBBroker getActiveBroker() { //throws EXistException { //synchronized(this) { //Try to get an active broker final DBBroker broker = activeBrokers.get(Thread.currentThread()); if(broker == null) { final StringBuilder sb = new StringBuilder(); sb.append("Broker was not obtained for thread '"); sb.append(Thread.currentThread()); sb.append("'."); sb.append(System.getProperty("line.separator")); for(final Entry<Thread, DBBroker> entry : activeBrokers.entrySet()) { sb.append(entry.getKey()); sb.append(" = "); sb.append(entry.getValue()); sb.append(System.getProperty("line.separator")); } LOG.debug(sb.toString()); throw new RuntimeException(sb.toString()); } return broker; //} } public DBBroker authenticate(final String username, final Object credentials) throws AuthenticationException { final Subject subject = getSecurityManager().authenticate(username, credentials); try { return get(Optional.ofNullable(subject)); } catch(final Exception e) { throw new AuthenticationException(AuthenticationException.UNNOWN_EXCEPTION, e); } } /** * Returns an active broker for the database instance. * * The current user will be inherited by this broker * * @return The broker */ public DBBroker getBroker() throws EXistException { return get(Optional.empty()); } /** * Returns an active broker for the database instance. * * @param subject Optionally a subject to set on the broker, if a user is not provided then the * current user assigned to the broker will be re-used * @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(final Optional<Subject> subject) throws EXistException { Objects.requireNonNull(subject, "Subject cannot be null, use BrokerPool#getBroker() instead"); if(!isInstanceConfigured()) { throw new EXistException("database instance '" + instanceName + "' is not available"); } //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(); broker.pushSubject(subject.orElseGet(broker::getCurrentSubject)); if(LOG.isTraceEnabled()) { if(!brokerLeaseChangeTrace.containsKey(broker.getId())) { brokerLeaseChangeTrace.put(broker.getId(), new TraceableStateChanges<>()); } brokerLeaseChangeTrace.get(broker.getId()).add(TraceableBrokerLeaseChange.get(new TraceableBrokerLeaseChange.BrokerInfo(broker.getId(), broker.getReferenceCount()))); } 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 && subject.isPresent() && !subject.equals(Optional.ofNullable(serviceModeUser))) { try { LOG.debug("Db instance is in service mode. Waiting for db to become available again ..."); wait(); } catch(final InterruptedException e) { } } synchronized(this) { //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(final InterruptedException e) { //nothing to be done! } } } broker = inactiveBrokers.pop(); //activate the broker activeBrokers.put(Thread.currentThread(), broker); if(LOG.isTraceEnabled()) { LOG.trace("+++ " + Thread.currentThread() + Stacktrace.top(Thread.currentThread().getStackTrace(), Stacktrace.DEFAULT_STACK_TOP)); } if(watchdog.isPresent()) { watchdog.get().add(broker); } broker.incReferenceCount(); broker.pushSubject(subject.orElseGet(securityManager::getGuestSubject)); if(LOG.isTraceEnabled()) { if(!brokerLeaseChangeTrace.containsKey(broker.getId())) { brokerLeaseChangeTrace.put(broker.getId(), new TraceableStateChanges<>()); } brokerLeaseChangeTrace.get(broker.getId()).add(TraceableBrokerLeaseChange.get(new TraceableBrokerLeaseChange.BrokerInfo(broker.getId(), broker.getReferenceCount()))); } //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. * * NOTE - this is intentionally package-private, it is only meant to be * called internally and from {@link DBBroker#close()} * * @param broker The broker to be released */ //TODO : rename as releaseBroker ? releaseInstance (when refactored) ? void release(final DBBroker broker) { Objects.requireNonNull(broker, "Cannot release nothing"); if(LOG.isTraceEnabled()) { if(!brokerLeaseChangeTrace.containsKey(broker.getId())) { brokerLeaseChangeTrace.put(broker.getId(), new TraceableStateChanges<>()); } brokerLeaseChangeTrace.get(broker.getId()).add(TraceableBrokerLeaseChange.release(new TraceableBrokerLeaseChange.BrokerInfo(broker.getId(), broker.getReferenceCount()))); } //first check that the broker is active ! If not, return immediately. broker.decReferenceCount(); if(broker.getReferenceCount() > 0) { broker.popSubject(); //it is still in use and thus can't be marked as inactive return; } synchronized(this) { //Broker is no more used : inactivate it for(final DBBroker inactiveBroker : inactiveBrokers) { if(broker == inactiveBroker) { LOG.error("Broker " + broker.getId() + " 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(final Entry<Thread, DBBroker> activeBroker : activeBrokers.entrySet()) { if(activeBroker.getValue() == broker) { final EXistException ex = new EXistException(); LOG.error("release() has been called from '" + Thread.currentThread() + "', but occupied at '" + activeBroker.getKey() + "'.", ex); activeBrokers.remove(activeBroker.getKey()); break; } } } else { if(LOG.isTraceEnabled()) { LOG.trace("--- " + Thread.currentThread() + Stacktrace.top(Thread.currentThread().getStackTrace(), Stacktrace.DEFAULT_STACK_TOP)); } } Subject lastUser = broker.popSubject(); //guard to ensure that the broker has popped all its subjects if(lastUser == null || broker.getCurrentSubject() != null) { LOG.warn("Broker " + broker.getId() + " was returned with extraneous Subjects, cleaning...", new IllegalStateException("DBBroker pushSubject/popSubject mismatch").fillInStackTrace()); if(LOG.isTraceEnabled()) { broker.traceSubjectChanges(); } //cleanup any remaining erroneous subjects while(broker.getCurrentSubject() != null) { lastUser = broker.popSubject(); } } inactiveBrokers.push(broker); watchdog.ifPresent(wd -> wd.remove(broker)); if(LOG.isTraceEnabled()) { if(!brokerLeaseChangeTraceHistory.containsKey(broker.getId())) { brokerLeaseChangeTraceHistory.put(broker.getId(), new ArrayList<>()); } try { brokerLeaseChangeTraceHistory.get(broker.getId()).add((TraceableStateChanges<TraceableBrokerLeaseChange.BrokerInfo, TraceableBrokerLeaseChange.Change>) brokerLeaseChangeTrace.get(broker.getId()).clone()); brokerLeaseChangeTrace.get(broker.getId()).clear(); } catch(final CloneNotSupportedException e) { LOG.error(e); } broker.clearSubjectChangesTrace(); } //If the database is now idle, do some useful stuff if(activeBrokers.size() == 0) { //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 && !lastUser.equals(serviceModeUser)) { inServiceMode = true; } } //Inform the other threads that someone is gone this.notifyAll(); } } public DBBroker enterServiceMode(final Subject 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.size() != 0) { while(!inServiceMode) { try { wait(); } catch(final InterruptedException e) { //nothing to be done } } } } inServiceMode = true; final DBBroker broker = inactiveBrokers.peek(); checkpoint = true; sync(broker, Sync.MAJOR); checkpoint = false; // Return a broker that can be used to perform system tasks return broker; } public void exitServiceMode(final Subject 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(); } } public void reportStatus(final String message) { if(statusReporter != null) { statusReporter.setStatus(message); } } 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} */ //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(final DBBroker broker, final Sync syncEvent) { broker.sync(syncEvent); //TODO : strange that it is set *after* the sunc method has been called. try { broker.pushSubject(securityManager.getSystemSubject()); if (syncEvent == Sync.MAJOR) { LOG.debug("Major sync"); try { if (!FORCE_CORRUPTION) { transactionManager.checkpoint(checkpoint); } } catch (final TransactionException e) { LOG.warn(e.getMessage(), e); } cacheManager.checkCaches(); if (pluginManager != null) { pluginManager.sync(broker); } lastMajorSync = System.currentTimeMillis(); if (LOG.isDebugEnabled()) { notificationService.debug(); } } else { cacheManager.checkDistribution(); // LOG.debug("Minor sync"); } //TODO : touch this.syncEvent and syncRequired ? } finally { broker.popSubject(); } } /** * 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 deferred * until all running threads have returned. * * @param syncEvent One of {@link org.exist.storage.sync.Sync} */ public void triggerSync(final Sync syncEvent) { //TOUNDERSTAND (pb) : synchronized, so... "schedules" or, rather, "executes" ? "schedules" (WM) final State s = status.getCurrentState(); if(s == State.SHUTDOWN || s == State.SHUTTING_DOWN) { 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. final DBBroker broker = inactiveBrokers.pop(); //Do the synchronization job sync(broker, syncEvent); inactiveBrokers.push(broker); syncRequired = false; } else { //Put the synchronization 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 deferred * until all running threads have returned. * * @param task The task */ //TOUNDERSTAND (pb) : synchronized, so... "schedules" or, rather, "executes" ? public void triggerSystemTask(final SystemTask task) { final State s = status.getCurrentState(); if(s == State.SHUTTING_DOWN) { LOG.info("Skipping SystemTask: '" + task.getName() + "' as database is shutting down..."); return; } else if(s == State.SHUTDOWN) { LOG.warn("Unable to execute SystemTask: '" + task.getName() + "' as database is shut down!"); return; } transactionManager.triggerSystemTask(task); } /** * Shuts downs the database instance */ public void shutdown() { shutdown(false); } public boolean isShuttingDown() { return status.getCurrentState() == State.SHUTTING_DOWN; } public boolean isShutDown() { return status.getCurrentState() == State.SHUTDOWN; } /** * Shuts downs the database instance * * @param killed <code>true</code> when the JVM is (cleanly) exiting */ public void shutdown(final boolean killed) { try { status.process(Event.START_SHUTDOWN); } catch(final IllegalStateException e) { // we are not operational! LOG.warn(e); return; } try { LOG.info("Database is shutting down ..."); processMonitor.stopRunningJobs(); //Shutdown the scheduler scheduler.shutdown(true); final 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) { // these may be used and set by other threads for the same or some other purpose // (unlikely). Take no chances. statusReporter = new StatusReporter(SIGNAL_SHUTDOWN); statusObservers.forEach(statusReporter::addObserver); final Thread statusThread = new Thread(statusReporter); statusThread.start(); // release transaction log to allow remaining brokers to complete // their job lock.unlock(); // DW: only in debug mode if (LOG.isDebugEnabled()) { notificationService.debug(); } //Notify all running tasks that we are shutting down //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 (isRecoveryEnabled()) { journalManager.ifPresent(jm -> jm.flush(true, true)); } final long waitStart = System.currentTimeMillis(); //Are there active brokers ? if (activeBrokers.size() > 0) { printSystemInfo(); LOG.info("Waiting " + maxShutdownWait + "ms for remaining threads to shut down..."); while (activeBrokers.size() > 0) { try { //Wait until they become inactive... this.wait(1000); } catch (final InterruptedException e) { //nothing to be done } //...or force the shutdown if (maxShutdownWait > -1 && System.currentTimeMillis() - waitStart > maxShutdownWait) { LOG.warn("Not all threads returned. Forcing shutdown ..."); break; } } } LOG.debug("Calling shutdown ..."); if (pluginManager != null) try { pluginManager.stop((DBBroker)null); } catch (final EXistException e) { LOG.warn("Error during plugin manager shutdown: " + e.getMessage(), e); } // closing down external indexes try { indexManager.shutdown(); } catch (final 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 (final 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. //TODO: always createBroker? -dmitriy { 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.pushSubject(securityManager.getSystemSubject()); broker.shutdown(); } collectionCacheMgr.deregisterCache(collectionCache); // do not write a checkpoint if some threads did not return before shutdown // there might be dirty transactions transactionManager.shutdown(); // deregister JMX MBeans AgentFactory.getInstance().closeDBInstance(this); //Clear the living instances container removeInstance(instanceName); synchronized (readOnly) { if (!readOnly) { // release the lock on the data directory dataLock.release(); } } //clearing additional resources, like ThreadLocal clearThreadLocals(); LOG.info("shutdown complete !"); if (shutdownListener != null) { shutdownListener.shutdown(instanceName, instancesCount()); } statusReporter.terminate(); statusReporter = null; } } finally { // clear instance variables, just to be sure they will be garbage collected // the test suite restarts the db a few hundred times Configurator.clear(this); transactionManager = null; collectionCache = null; collectionCacheMgr = null; xQueryPool = null; processMonitor = null; collectionConfigurationManager = null; notificationService = null; indexManager = null; xmlReaderPool = null; shutdownListener = null; securityManager = null; notificationService = null; statusObservers.clear(); } } finally { status.process(Event.FINISHED_SHUTDOWN); } } public void addStatusObserver(final Observer statusObserver) { this.statusObservers.add(statusObserver); } public boolean removeStatusObserver(final Observer statusObserver) { return this.statusObservers.remove(statusObserver); } private void clearThreadLocals() { for (final Thread thread : Thread.getAllStackTraces().keySet()){ try { cleanThreadLocalsForThread(thread); } catch (final EXistException ex) { LOG.warn("Could not clear ThreadLocals for thread: " + thread.getName()); } } } private void cleanThreadLocalsForThread(final Thread thread) throws EXistException { try { // Get a reference to the thread locals table of the current thread final Field threadLocalsField = Thread.class.getDeclaredField("threadLocals"); threadLocalsField.setAccessible(true); final Object threadLocalTable = threadLocalsField.get(thread); // Get a reference to the array holding the thread local variables inside the // ThreadLocalMap of the current thread final Class threadLocalMapClass = Class.forName("java.lang.ThreadLocal$ThreadLocalMap"); final Field tableField = threadLocalMapClass.getDeclaredField("table"); tableField.setAccessible(true); final Object table = tableField.get(threadLocalTable); // The key to the ThreadLocalMap is a WeakReference object. The referent field of this object // is a reference to the actual ThreadLocal variable final Field referentField = Reference.class.getDeclaredField("referent"); referentField.setAccessible(true); for (int i = 0; i < Array.getLength(table); i++) { // Each entry in the table array of ThreadLocalMap is an Entry object // representing the thread local reference and its value final Object entry = Array.get(table, i); if (entry != null) { // Get a reference to the thread local object and remove it from the table final ThreadLocal threadLocal = (ThreadLocal)referentField.get(entry); threadLocal.remove(); } } } catch(final Exception e) { // We will tolerate an exception here and just log it throw new EXistException(e); } } public Optional<BrokerWatchdog> getWatchdog() { return watchdog; } //TODO : move this elsewhere public void triggerCheckpoint() { if(syncRequired) { return; } synchronized(this) { syncEvent = Sync.MAJOR; 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; } public void printSystemInfo() { try(final StringWriter sout = new StringWriter(); final PrintWriter writer = new PrintWriter(sout)) { writer.println("SYSTEM INFO"); writer.format("Database instance: %s\n", getId()); writer.println("-------------------------------------------------------------------"); watchdog.ifPresent(wd -> wd.dump(writer)); DeadlockDetection.debug(writer); final String s = sout.toString(); LOG.info(s); System.err.println(s); } catch(final IOException e) { LOG.error(e); } } private static class StatusReporter extends Observable implements Runnable { private String status; private volatile boolean terminate = false; public StatusReporter(final String status) { this.status = status; } public synchronized void setStatus(final String status) { this.status = status; this.setChanged(); this.notifyObservers(status); } public synchronized void terminate() { this.terminate = true; this.notifyAll(); } public void run() { while(!terminate) { synchronized(this) { try { wait(500); } catch(final InterruptedException e) { // nothing to do } } this.setChanged(); this.notifyObservers(status); } } } @Override public Path getStoragePlace() { return (Path)conf.getProperty(BrokerPool.PROPERTY_DATA_DIR); } private final List<TriggerProxy<? extends DocumentTrigger>> documentTriggers = new ArrayList<>(); private final List<TriggerProxy<? extends CollectionTrigger>> collectionTriggers = new ArrayList<>(); @Override public List<TriggerProxy<? extends DocumentTrigger>> getDocumentTriggers() { return documentTriggers; } @Override public List<TriggerProxy<? extends CollectionTrigger>> getCollectionTriggers() { return collectionTriggers; } @Override public void registerDocumentTrigger(final Class<? extends DocumentTrigger> clazz) { documentTriggers.add(new DocumentTriggerProxy(clazz)); } @Override public void registerCollectionTrigger(final Class<? extends CollectionTrigger> clazz) { collectionTriggers.add(new CollectionTriggerProxy(clazz)); } public PluginsManager getPluginsManager() { return pluginManager; } protected MetaStorage metaStorage = null; public MetaStorage getMetaStorage() { return metaStorage; } /** * Represents a change involving {@link BrokerPool#inactiveBrokers} * or {@link BrokerPool#activeBrokers} or {@link DBBroker#referenceCount} * * Used for tracing broker leases */ private static class TraceableBrokerLeaseChange extends TraceableStateChange<TraceableBrokerLeaseChange.BrokerInfo, TraceableBrokerLeaseChange.Change> { public enum Change { GET, RELEASE } public static class BrokerInfo { final String id; final int referenceCount; public BrokerInfo(final String id, final int referenceCount) { this.id = id; this.referenceCount = referenceCount; } } private TraceableBrokerLeaseChange(final Change change, final BrokerInfo brokerInfo) { super(change, brokerInfo); } @Override public String getId() { return getState().id; } @Override public String describeState() { return Integer.toString(getState().referenceCount); } static TraceableBrokerLeaseChange get(final BrokerInfo brokerInfo) { return new TraceableBrokerLeaseChange(Change.GET, brokerInfo); } static TraceableBrokerLeaseChange release(final BrokerInfo brokerInfo) { return new TraceableBrokerLeaseChange(Change.RELEASE, brokerInfo); } } }