/* * The contents of this file are subject to the terms of the Common Development and * Distribution License (the License). You may not use this file except in compliance with the * License. * * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the * specific language governing permission and limitations under the License. * * When distributing Covered Software, include this CDDL Header Notice in each file and include * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL * Header, with the fields enclosed by brackets [] replaced by your own identifying * information: "Portions Copyright [year] [name of copyright owner]". * * Copyright 2007-2010 Sun Microsystems, Inc. * Portions Copyright 2013-2016 ForgeRock AS. * Portions Copyright 2025-2026 3A Systems, LLC */ package org.opends.server.backends.pluggable; import static org.forgerock.util.Reject.*; import static org.forgerock.util.Utils.closeSilently; import static org.opends.messages.BackendMessages.*; import static org.opends.server.util.ServerConstants.*; import static org.opends.server.util.StaticUtils.*; import java.io.IOException; import java.util.ArrayList; import java.util.Collections; import java.util.HashSet; import java.util.List; import java.util.Set; import java.util.SortedSet; import java.util.concurrent.ExecutionException; import org.forgerock.i18n.LocalizableException; import org.forgerock.i18n.LocalizableMessage; import org.forgerock.i18n.slf4j.LocalizedLogger; import org.forgerock.opendj.config.server.ConfigChangeResult; import org.forgerock.opendj.config.server.ConfigException; import org.forgerock.opendj.config.server.ConfigurationChangeListener; import org.forgerock.opendj.ldap.ConditionResult; import org.forgerock.opendj.ldap.DN; import org.forgerock.opendj.ldap.ResultCode; import org.forgerock.opendj.ldap.schema.AttributeType; import org.forgerock.opendj.server.config.server.PluggableBackendCfg; import org.forgerock.util.Reject; import org.opends.server.api.LocalBackend; import org.opends.server.api.MonitorProvider; import org.opends.server.backends.RebuildConfig; import org.opends.server.backends.VerifyConfig; import org.opends.server.backends.pluggable.spi.AccessMode; import org.opends.server.backends.pluggable.spi.Storage; import org.opends.server.backends.pluggable.spi.StorageInUseException; import org.opends.server.backends.pluggable.spi.StorageRuntimeException; import org.opends.server.backends.pluggable.spi.TreeName; import org.opends.server.backends.pluggable.spi.WriteOperation; import org.opends.server.backends.pluggable.spi.WriteableTransaction; import org.opends.server.core.AddOperation; import org.opends.server.core.BackendConfigManager; import org.opends.server.core.DeleteOperation; import org.opends.server.core.DirectoryServer; import org.opends.server.core.ModifyDNOperation; import org.opends.server.core.ModifyOperation; import org.opends.server.core.SearchOperation; import org.opends.server.core.ServerContext; import org.opends.server.types.BackupConfig; import org.opends.server.types.BackupDirectory; import org.opends.server.types.CanceledOperationException; import org.opends.server.types.DirectoryException; import org.opends.server.types.Entry; import org.opends.server.types.IndexType; import org.opends.server.types.InitializationException; import org.opends.server.types.LDIFExportConfig; import org.opends.server.types.LDIFImportConfig; import org.opends.server.types.LDIFImportResult; import org.opends.server.types.Operation; import org.opends.server.types.RestoreConfig; import org.opends.server.util.CollectionUtils; import org.opends.server.util.LDIFException; import org.opends.server.util.RuntimeInformation; /** * This is an implementation of a Directory Server Backend which stores entries locally * in a pluggable storage. * * @param * the type of the BackendCfg for the current backend */ public abstract class BackendImpl extends LocalBackend implements ConfigurationChangeListener { private static final LocalizedLogger logger = LocalizedLogger.getLoggerForThisClass(); /** The configuration of this backend. */ private PluggableBackendCfg cfg; /** The root container to use for this backend. */ private RootContainer rootContainer; /** * A count of the total operation threads currently in the backend. Bumped * twice per operation by all worker threads, so it uses a striped counter * to avoid contending on a single cache line; it is only read when waiting * for the backend to become quiescent, which is why it is not a LongAdder — * see {@link StripedCounter}. */ private final StripedCounter threadTotalCount = new StripedCounter(); /** The base DNs defined for this backend instance. */ private Set baseDNs; private MonitorProvider rootContainerMonitor; /** The underlying storage engine. */ private Storage storage; /** The controls supported by this backend. */ private static final Set supportedControls = CollectionUtils.newHashSet( OID_SUBTREE_DELETE_CONTROL, OID_PAGED_RESULTS_CONTROL, OID_MANAGE_DSAIT_CONTROL, OID_SERVER_SIDE_SORT_REQUEST_CONTROL, OID_VLV_REQUEST_CONTROL); private ServerContext serverContext; /** * Begin a Backend API method that accesses the {@link EntryContainer} for entryDN * and returns it. * @param operation requesting the storage * @param entryDN the target DN for the operation * @return EntryContainer where entryDN resides */ private EntryContainer accessBegin(Operation operation, DN entryDN) throws DirectoryException { return accessBegin(operation, entryDN, ResultCode.UNDEFINED); } /** * Begin a Backend API method that accesses the {@link EntryContainer} for entryDN * and returns it. * @param operation requesting the storage * @param entryDN the target DN for the operation * @param noEntryContainerResultCode the result code to report when this backend holds no * entry container for entryDN * @return EntryContainer where entryDN resides */ private EntryContainer accessBegin(Operation operation, DN entryDN, ResultCode noEntryContainerResultCode) throws DirectoryException { checkRootContainerInitialized(); rootContainer.checkForEnoughResources(operation); EntryContainer ec = rootContainer.getEntryContainer(entryDN); if (ec == null) { throw new DirectoryException( noEntryContainerResultCode, ERR_BACKEND_ENTRY_DOESNT_EXIST.get(entryDN, getBackendID())); } threadTotalCount.increment(); return ec; } /** End a Backend API method that accesses the EntryContainer. */ private void accessEnd() { threadTotalCount.decrement(); } /** * Wait until there are no more threads accessing the storage. It is assumed * that new threads have been prevented from entering the storage at the time * this method is called. */ private void waitUntilQuiescent() { while (threadTotalCount.sum() > 0) { // Still have threads accessing the storage so sleep a little try { Thread.sleep(500); } catch (InterruptedException e) { logger.traceException(e); } } } @Override public void configureBackend(C cfg, ServerContext serverContext) throws ConfigException { Reject.ifNull(cfg, "cfg must not be null"); this.cfg = cfg; this.serverContext = serverContext; baseDNs = new HashSet<>(cfg.getBaseDN()); storage = new TracedStorage(configureStorage(cfg, serverContext), cfg.getBackendId()); } @Override public void openBackend() throws ConfigException, InitializationException { if (mustOpenRootContainer()) { rootContainer = newRootContainer(AccessMode.READ_WRITE); } // Preload the tree cache. rootContainer.preload(cfg.getPreloadTimeLimit()); try { // Log an informational message about the number of entries. logger.info(NOTE_BACKEND_STARTED, cfg.getBackendId(), getEntryCount()); } catch (StorageRuntimeException e) { LocalizableMessage message = WARN_GET_ENTRY_COUNT_FAILED.get(e.getMessage()); throw new InitializationException(message, e); } for (DN dn : cfg.getBaseDN()) { try { serverContext.getBackendConfigManager().registerBaseDN(dn, this, false); } catch (Exception e) { throw new InitializationException(ERR_BACKEND_CANNOT_REGISTER_BASEDN.get(dn, e), e); } } // Register a monitor provider for the environment. rootContainerMonitor = rootContainer.getMonitorProvider(); DirectoryServer.registerMonitorProvider(rootContainerMonitor); // Register this backend as a change listener. cfg.addPluggableChangeListener(this); } @Override public void closeBackend() { cfg.removePluggableChangeListener(this); // Deregister our base DNs. for (DN dn : rootContainer.getBaseDNs()) { try { serverContext.getBackendConfigManager().deregisterBaseDN(dn); } catch (Exception e) { logger.traceException(e); } } DirectoryServer.deregisterMonitorProvider(rootContainerMonitor); // We presume the server will prevent more operations coming into this // backend, but there may be existing operations already in the // backend. We need to wait for them to finish. waitUntilQuiescent(); // Close RootContainer and Storage. try { rootContainer.close(); rootContainer = null; } catch (StorageRuntimeException e) { logger.traceException(e); logger.error(ERR_DATABASE_EXCEPTION, e.getMessage()); } // Make sure the thread counts are zero for next initialization. threadTotalCount.reset(); // Log an informational message. logger.info(NOTE_BACKEND_OFFLINE, cfg.getBackendId()); } @Override public boolean isIndexed(AttributeType attributeType, IndexType indexType) { try { EntryContainer ec = rootContainer.getEntryContainer(baseDNs.iterator().next()); AttributeIndex ai = ec.getAttributeIndex(attributeType); return ai != null ? ai.isIndexed(indexType) : false; } catch (Exception e) { logger.traceException(e); return false; } } @Override public boolean supports(BackendOperation backendOperation) { switch (backendOperation) { case BACKUP: case RESTORE: // Responsibility of the underlying storage. return storage.supportsBackupAndRestore(); default: // INDEXING, LDIF_EXPORT, LDIF_IMPORT // Responsibility of this pluggable backend. return true; } } @Override public Set getSupportedFeatures() { return Collections.emptySet(); } @Override public Set getSupportedControls() { return supportedControls; } @Override public Set getBaseDNs() { return baseDNs; } @Override public long getEntryCount() { if (rootContainer != null) { try { return rootContainer.getEntryCount(); } catch (Exception e) { logger.traceException(e); } } return -1; } @Override public ConditionResult hasSubordinates(DN entryDN) throws DirectoryException { EntryContainer container; try { container = accessBegin(null, entryDN); } catch (DirectoryException de) { if (de.getResultCode() == ResultCode.UNDEFINED) { return ConditionResult.UNDEFINED; } throw de; } container.beginSharedAccess(); try { return ConditionResult.valueOf(container.hasSubordinates(entryDN)); } catch (StorageRuntimeException e) { throw createDirectoryException(e); } finally { container.endSharedAccess(); accessEnd(); } } @Override public long getNumberOfEntriesInBaseDN(DN baseDN) throws DirectoryException { checkNotNull(baseDN, "baseDN must not be null"); final EntryContainer ec = accessBegin(null, baseDN); ec.beginSharedAccess(); try { return ec.getNumberOfEntriesInBaseDN(); } catch (Exception e) { throw new DirectoryException( serverContext.getCoreConfigManager().getServerErrorResultCode(), LocalizableMessage.raw(e.getMessage()), e); } finally { ec.endSharedAccess(); accessEnd(); } } @Override public long getNumberOfChildren(DN parentDN) throws DirectoryException { checkNotNull(parentDN, "parentDN must not be null"); EntryContainer ec; /* * Only place where we need special handling. Should return -1 instead of an * error if the EntryContainer is null... */ try { ec = accessBegin(null, parentDN); } catch (DirectoryException de) { if (de.getResultCode() == ResultCode.UNDEFINED) { return -1; } throw de; } ec.beginSharedAccess(); try { return ec.getNumberOfChildren(parentDN); } catch (StorageRuntimeException e) { throw createDirectoryException(e); } finally { ec.endSharedAccess(); accessEnd(); } } @Override public boolean entryExists(final DN entryDN) throws DirectoryException { EntryContainer ec = accessBegin(null, entryDN); ec.beginSharedAccess(); try { return ec.entryExists(entryDN); } catch (StorageRuntimeException e) { throw createDirectoryException(e); } finally { ec.endSharedAccess(); accessEnd(); } } @Override public Entry getEntry(DN entryDN) throws DirectoryException { EntryContainer ec = accessBegin(null, entryDN); ec.beginSharedAccess(); try { return ec.getEntry(entryDN); } catch (StorageRuntimeException e) { throw createDirectoryException(e); } finally { ec.endSharedAccess(); accessEnd(); } } @Override public void addEntry(Entry entry, AddOperation addOperation) throws DirectoryException, CanceledOperationException { EntryContainer ec = accessBegin(addOperation, entry.getName()); ec.beginSharedAccess(); try { ec.addEntry(entry, addOperation); } catch (StorageRuntimeException e) { throw createDirectoryException(e); } finally { ec.endSharedAccess(); accessEnd(); } } @Override public void deleteEntry(DN entryDN, DeleteOperation deleteOperation) throws DirectoryException, CanceledOperationException { EntryContainer ec = accessBegin(deleteOperation, entryDN); ec.beginSharedAccess(); try { ec.deleteEntry(entryDN, deleteOperation); } catch (StorageRuntimeException e) { throw createDirectoryException(e); } finally { ec.endSharedAccess(); accessEnd(); } } @Override public void replaceEntry(Entry oldEntry, Entry newEntry, ModifyOperation modifyOperation) throws DirectoryException, CanceledOperationException { EntryContainer ec = accessBegin(modifyOperation, newEntry.getName()); ec.beginSharedAccess(); try { ec.replaceEntry(oldEntry, newEntry, modifyOperation); } catch (StorageRuntimeException e) { throw createDirectoryException(e); } finally { ec.endSharedAccess(); accessEnd(); } } @Override public void renameEntry(DN currentDN, Entry entry, ModifyDNOperation modifyDNOperation) throws DirectoryException, CanceledOperationException { EntryContainer currentContainer = accessBegin(modifyDNOperation, currentDN); EntryContainer container = rootContainer.getEntryContainer(entry.getName()); if (currentContainer != container) { accessEnd(); // FIXME: No reason why we cannot implement a move between containers // since the containers share the same "container" throw new DirectoryException(ResultCode.UNWILLING_TO_PERFORM, WARN_FUNCTION_NOT_SUPPORTED.get()); } currentContainer.beginSharedAccess(); try { currentContainer.renameEntry(currentDN, entry, modifyDNOperation); } catch (StorageRuntimeException e) { throw createDirectoryException(e); } finally { currentContainer.endSharedAccess(); accessEnd(); } } @Override public void search(SearchOperation searchOperation) throws DirectoryException, CanceledOperationException { // a base DN held by no entry container of this backend does not exist as far as a client // is concerned: report it as such instead of the UNDEFINED result code used internally. EntryContainer ec = accessBegin(searchOperation, searchOperation.getBaseDN(), ResultCode.NO_SUCH_OBJECT); ec.beginSharedAccess(); try { ec.search(searchOperation); } catch (StorageRuntimeException e) { throw createDirectoryException(e); } finally { ec.endSharedAccess(); accessEnd(); } } private void checkRootContainerInitialized() throws DirectoryException { if (rootContainer == null) { LocalizableMessage msg = ERR_ROOT_CONTAINER_NOT_INITIALIZED.get(getBackendID()); throw new DirectoryException(serverContext.getCoreConfigManager().getServerErrorResultCode(), msg); } } @Override public void exportLDIF(LDIFExportConfig exportConfig) throws DirectoryException { // If the backend already has the root container open, we must use the same // underlying root container boolean openRootContainer = mustOpenRootContainer(); try { if (openRootContainer) { rootContainer = getReadOnlyRootContainer(); } ExportJob exportJob = new ExportJob(exportConfig); exportJob.exportLDIF(rootContainer); } catch (IOException ioe) { throw new DirectoryException(serverContext.getCoreConfigManager() .getServerErrorResultCode(), ERR_EXPORT_IO_ERROR.get(ioe.getMessage()), ioe); } catch (StorageRuntimeException de) { throw createDirectoryException(de); } catch (ConfigException | InitializationException | LDIFException e) { throw new DirectoryException( serverContext.getCoreConfigManager().getServerErrorResultCode(), e.getMessageObject(), e); } finally { closeTemporaryRootContainer(openRootContainer); } } private boolean mustOpenRootContainer() { return rootContainer == null; } @Override public LDIFImportResult importLDIF(LDIFImportConfig importConfig, ServerContext serverContext) throws DirectoryException { RuntimeInformation.logInfo(); // If the rootContainer is open, the backend is initialized by something else. // We can't do import while the backend is online. if (rootContainer != null) { throw new DirectoryException( serverContext.getCoreConfigManager().getServerErrorResultCode(), ERR_IMPORT_BACKEND_ONLINE.get()); } try { try { if (importConfig.clearBackend()) { // clear all files before opening the root container storage.removeStorageFiles(); } } catch (Exception e) { throw new DirectoryException( serverContext.getCoreConfigManager().getServerErrorResultCode(), ERR_REMOVE_FAIL.get(e.getMessage()), e); } rootContainer = newRootContainer(AccessMode.READ_WRITE); rootContainer.getStorage().close(); return getImportStrategy(rootContainer).importLDIF(importConfig); } catch (Exception e) { throw createDirectoryException(e); } finally { try { if (rootContainer != null) { long startTime = System.currentTimeMillis(); rootContainer.close(); long finishTime = System.currentTimeMillis(); long closeTime = (finishTime - startTime) / 1000; logger.info(NOTE_IMPORT_LDIF_ROOTCONTAINER_CLOSE, closeTime); rootContainer = null; } logger.info(NOTE_IMPORT_CLOSING_DATABASE); } catch (StorageRuntimeException de) { logger.traceException(de); } } } private ImportStrategy getImportStrategy(final RootContainer rootContainer) { return new OnDiskMergeImporter.StrategyImpl(serverContext, rootContainer, cfg); } @Override public long verifyBackend(VerifyConfig verifyConfig) throws InitializationException, ConfigException, DirectoryException { // If the backend already has the root container open, we must use the same // underlying root container final boolean openRootContainer = mustOpenRootContainer(); try { if (openRootContainer) { rootContainer = getReadOnlyRootContainer(); } return new VerifyJob(rootContainer, verifyConfig).verifyBackend(); } catch (StorageRuntimeException e) { throw createDirectoryException(e); } finally { closeTemporaryRootContainer(openRootContainer); } } /** * If a root container was opened in the calling method method as read only, * close it to leave the backend in the same state. */ private void closeTemporaryRootContainer(boolean openRootContainer) { if (openRootContainer && rootContainer != null) { try { rootContainer.close(); rootContainer = null; } catch (StorageRuntimeException e) { logger.traceException(e); } } } @Override public void rebuildBackend(RebuildConfig rebuildConfig, ServerContext serverContext) throws InitializationException, ConfigException, DirectoryException { // If the backend already has the root container open, we must use the same // underlying root container boolean openRootContainer = mustOpenRootContainer(); /* * If the rootContainer is open, the backend is initialized by something else. * We can't do any rebuild of system indexes while others are using this backend. */ if (!openRootContainer && rebuildConfig.includesSystemIndex()) { throw new DirectoryException( serverContext.getCoreConfigManager().getServerErrorResultCode(), ERR_REBUILD_BACKEND_ONLINE.get()); } try { if (openRootContainer) { rootContainer = newRootContainer(AccessMode.READ_WRITE); } getImportStrategy(rootContainer).rebuildIndex(rebuildConfig); } catch (InitializationException | ConfigException e) { throw e; } catch (Exception e) { throw createDirectoryException(e); } finally { closeTemporaryRootContainer(openRootContainer); } } @Override public void createBackup(BackupConfig backupConfig) throws DirectoryException { storage.createBackup(backupConfig); } @Override public void removeBackup(BackupDirectory backupDirectory, String backupID) throws DirectoryException { storage.removeBackup(backupDirectory, backupID); } @Override public void restoreBackup(RestoreConfig restoreConfig) throws DirectoryException { storage.restoreBackup(restoreConfig); } /** * Creates the storage engine which will be used by this pluggable backend. Implementations should * create and configure a new storage engine but not open it. * * @param cfg * the configuration object * @param serverContext * this Directory Server intsance's server context * @return The storage engine to be used by this pluggable backend. * @throws ConfigException * If there is an error in the configuration. */ protected abstract Storage configureStorage(C cfg, ServerContext serverContext) throws ConfigException; @Override public boolean isConfigurationAcceptable(C config, List unacceptableReasons, ServerContext serverContext) { return isConfigurationChangeAcceptable(config, unacceptableReasons); } @Override public boolean isConfigurationChangeAcceptable(PluggableBackendCfg cfg, List unacceptableReasons) { return true; } /** * {@inheritDoc} *

* {@link Storage#write(WriteOperation)} replays its operation after a transaction conflict, so * the operation below is confined to work a rollback undoes: the trees are deleted and opened * there, while the registries, which no rollback reaches, are updated once the write has * committed. Getting this the wrong way round leaves the change half applied, and its replay * reports the missing half rather than the conflict that caused it. *

* What makes the operation replayable is that the base DNs to remove and to add are worked out * once, ahead of the write, so that no attempt can see different work to do than the attempt it * is replacing. */ @Override public ConfigChangeResult applyConfigurationChange(final PluggableBackendCfg newCfg) { final ConfigChangeResult ccr = new ConfigChangeResult(); // Read once: importLDIF, rebuildBackend, exportLDIF and verifyBackend all assign this field // and null it out again, and this method now goes on using it past the commit. final RootContainer rc = rootContainer; if (rc == null) { return ccr; } final SortedSet newBaseDNs = newCfg.getBaseDN(); // Ask the root container what this backend holds rather than the configuration it was last // given: a base DN which an earlier, failed change left behind is work to do, and a // configuration which was never applied is not. RootContainer.getBaseDNs() is a live view of // the registered containers, so take a copy of it before anything registers one. final Set currentBaseDNs = new HashSet<>(rc.getBaseDNs()); final List deleted = new ArrayList<>(); for (DN baseDN : currentBaseDNs) { if (!newBaseDNs.contains(baseDN)) { final EntryContainer ec = rc.getEntryContainer(baseDN); // Answered exactly, never with an ancestor's container: getEntryContainer walks up the DN // until it finds one, which is how an entry is routed to the base DN above it, and one // backend holds hierarchically related base DNs whenever a registry which refused one left // its container behind. Deleting the trees an ancestor answered with is deleting the trees // of a base DN this backend is still serving. if (ec == null || !baseDN.equals(ec.getBaseDN())) { // Unregistered since the copy above was taken, which is what closing the root container // leaves behind: importLDIF, rebuildBackend and exportLDIF all do that, as does a backend // being disabled. There is nothing to delete and nothing to say about the rest of the // change, so none of it is attempted - and a result is returned rather than an exception, // which is what the administration framework is owed whatever happens. ccr.setResultCode(serverContext.getCoreConfigManager().getServerErrorResultCode()); ccr.addMessage(ERR_BACKEND_BASEDN_NO_LONGER_HELD.get(getBackendID(), baseDN)); return ccr; } deleted.add(ec); } } final List added = new ArrayList<>(); for (DN baseDN : newBaseDNs) { if (!currentBaseDNs.contains(baseDN)) { added.add(baseDN); } } if (deleted.isEmpty() && added.isEmpty()) { // The common case - index-entry-limit, db-cache-percent, preload-time-limit and the rest, // which the entry containers apply through their own listeners. There is no storage work to // do, so no transaction is opened to commit nothing. baseDNs = new HashSet<>(newBaseDNs); cfg = newCfg; return ccr; } // Opened by the write operation, registered only once it has committed. final List created = new ArrayList<>(); try { try { changeBaseDNTrees(rc, deleted, added, created); } catch (Exception e) { logger.traceException(e); ccr.setResultCode(serverContext.getCoreConfigManager().getServerErrorResultCode()); // The failure alone never says which base DNs the change was about, so name them. // // Only persistit and je roll the whole write back, leaving nothing at all applied and // neither registry to touch. The jdbc backend does not, on any of its engines: its // commitStatement() issues the statement and commits it, and commitsBeforeDdl() decides // only which side of the statement the attempt stops being replayable on, never whether a // completed "create table" or "drop table" survives the rollback of the write around it. // Neither does cassandra, which has no transaction to roll back. // giveUpBaseDNsWhoseTreesAreGone below reads what is actually left rather than trusting // either answer. ccr.addMessage(ERR_BACKEND_CANNOT_CHANGE_BASEDNS.get( getBackendID(), baseDNsOf(deleted), added, stackTraceToSingleLineString(e))); // Read before the entry containers are closed: what a container holds is what says which // trees belong to it. giveUpBaseDNsWhoseTreesAreGone(rc, deleted, ccr); closeSilently(created); return ccr; } // The change is durable from here on, so every base DN is seen through even if one fails. for (EntryContainer ec : deleted) { deregisterDeletedBaseDN(rc, ec, ccr); } registerNewBaseDNs(rc, created, ccr); // Put the new configuration in place. cfg = newCfg; } finally { // What the root container ended up holding, not what was asked for: a base DN whose // registration failed is not one this backend serves, and getBaseDNs() is what the monitors, // isIndexed() and closeBackend() are answered from. Taken on the way out of every path which // reached the write, the failed ones included, so that the two never disagree. The change // which had no storage work to do sets it from the new configuration above; the one which // found a base DN this backend no longer holds leaves it alone, since the root container it // would be read from is being closed underneath it. baseDNs = new HashSet<>(rc.getBaseDNs()); } return ccr; } /** * Deletes the trees of the base DNs being removed and opens the ones being added, as the single * write operation a storage engine may replay. *

* The trees of a removed base DN are deleted while it is still registered, so its entry container * is held exclusively for as long as the write runs, retries included, as * {@link RootContainer#close()}, EntryContainer's index delete listener and AttributeIndex all do. * That keeps out the operations which arrive during that window; an operation which had taken hold * of the container before the lock still ends up in a closed one once it is released, as it did * before this ordering. *

* The locks are given up with the write and are never held into the registry work which follows it. * {@link BackendConfigManager} guards its registry with a single lock which the server already * takes in the opposite order - {@code shutdownLocalBackends}, a backend being disabled and * {@code applyConfigurationDelete} all hold it while finalizing a backend, which closes its root * container and locks every entry container in turn. Holding the container lock into * {@code deregisterBaseDN} would deadlock a base DN change against a shutdown, with no timeout on * either side. */ private void changeBaseDNTrees(final RootContainer rc, final List deleted, final List added, final List created) throws Exception { for (EntryContainer ec : deleted) { // Taken outside the try, because EntryContainer.lock() has no throwing path - the write side // of a ReentrantReadWriteLock, then a drain which swallows the interrupt - so every one of // them is held by the time it is entered, and unlocking what was never locked cannot happen. ec.lock(); } try { rc.getStorage().write(new WriteOperation() { @Override public void run(WriteableTransaction txn) throws Exception { // Give up what a previous, rolled back attempt had opened: its trees are gone, and its // entry containers still hold the configuration listeners they registered. closeSilently(created); created.clear(); // Opening the added base DNs comes first, so that the failure this operation is most // likely to meet is met while everything is still there to roll back to. Once a tree // has been deleted, a storage engine which does not undo that has nothing to give // back. for (DN baseDN : added) { created.add(rc.openEntryContainer(baseDN, txn, AccessMode.READ_WRITE)); } for (EntryContainer ec : deleted) { ec.delete(txn); } } }); } finally { for (EntryContainer ec : deleted) { ec.unlock(); } } } /** * Gives up the base DNs whose trees the failed write took with it. There is anything to give up * only on an engine which does not roll the write back whole: mysql and oracle commit their DDL of * their own accord, cassandra has no transaction at all, and the jdbc backend commits after each * statement on every engine it supports. A base DN kept registered without its trees answers every * operation with a storage error, where its removal was meant to leave a plain "no such entry"; * one whose trees the rollback put back is left exactly as it was. *

* The trees the same write created for a base DN which is not being added after all are left where * they are, and this is the only place which could have taken them back. The configuration naming * that base DN was stored before this listener was called - {@code * ConfigurationHandler.replaceEntry} writes the entry, and only then notifies its change listeners * - and the failure does not take it back, so {@link RootContainer#open} opens that base DN again * from it the next time this backend is opened, adopting the trees which survived and creating the * ones which did not. Deleting them here would take away the trees of a base DN the stored * configuration still asks this backend to serve, and would reach only the base DNs whose opening * succeeded anyway: one which failed while being opened never became an entry container, and * nothing but its own trees names them. */ private void giveUpBaseDNsWhoseTreesAreGone(RootContainer rc, List deleted, ConfigChangeResult ccr) { if (deleted.isEmpty()) { return; } final Set storedTrees; try { storedTrees = rc.getStorage().listTrees(); } catch (Exception e) { // Nothing can be said about what survived, so nothing is given up on the strength of it - and // that is the case where the failure itself says least about what this backend is left // serving, so it is said outright rather than left to a bare admin action. logger.traceException(e); ccr.setAdminActionRequired(true); ccr.addMessage(ERR_BACKEND_CANNOT_LIST_TREES_AFTER_BASEDN_CHANGE.get( getBackendID(), stackTraceToSingleLineString(e))); return; } for (EntryContainer ec : deleted) { if (!storedTrees.containsAll(treeNamesOf(ec))) { ccr.setAdminActionRequired(true); deregisterDeletedBaseDN(rc, ec, ccr); } } } private static Set treeNamesOf(EntryContainer ec) { final Set names = new HashSet<>(); for (Tree tree : ec.listTrees()) { names.add(tree.getName()); } return names; } private void deregisterDeletedBaseDN(RootContainer rc, EntryContainer ec, ConfigChangeResult ccr) { final DN baseDN = ec.getBaseDN(); final BackendConfigManager backendConfigManager = serverContext.getBackendConfigManager(); try { backendConfigManager.deregisterBaseDN(baseDN); } catch (Exception e) { logger.traceException(e); if (backendConfigManager.getLocalBackendWithBaseDN(baseDN) == this) { // deregisterBaseDN puts its new registry in place only once it has succeeded, so this base // DN is still routed here. Leave the entry container registered: closeBackend() reclaims a // base DN through rootContainer.getBaseDNs(), and one taken out of there would stay claimed // by a backend which no longer holds it until the server is restarted. That is the opposite // of what deregisterBaseDNsWhoseTreesAreGone does, and for the opposite reason: there the // registry has already stopped routing to the base DN, so keeping the container only leaves // a storage error where a "no such entry" was meant to be, while here the registry is still // routing to it and dropping the container is what would leave that error behind. ccr.setResultCode(serverContext.getCoreConfigManager().getServerErrorResultCode()); ccr.setAdminActionRequired(true); ccr.addMessage(ERR_BACKEND_CANNOT_DEREGISTER_BASEDN.get(baseDN, stackTraceToSingleLineString(e))); return; } // It is not registered here, which is what an earlier change whose registerBaseDN failed // leaves behind. Nothing routes to it, so there is nothing to hold on to. } rc.unregisterEntryContainer(baseDN); closeSilently(ec); } private void registerNewBaseDNs(RootContainer rc, List created, ConfigChangeResult ccr) { for (EntryContainer ec : created) { final DN baseDN = ec.getBaseDN(); boolean registered = false; try { rc.registerEntryContainer(baseDN, ec); registered = true; serverContext.getBackendConfigManager().registerBaseDN(baseDN, this, false); } catch (Exception e) { logger.traceException(e); ccr.setResultCode(serverContext.getCoreConfigManager().getServerErrorResultCode()); ccr.setAdminActionRequired(true); ccr.addMessage(ERR_BACKEND_CANNOT_REGISTER_BASEDN.get(baseDN, stackTraceToSingleLineString(e))); if (!registered) { // Nothing else can reclaim it: closeBackend() and RootContainer.close() both work from // the registered containers, and this one keeps the configuration listeners its // constructor registered for as long as it is alive. closeSilently(ec); } } } } private static List baseDNsOf(List entryContainers) { final List baseDNs = new ArrayList<>(entryContainers.size()); for (EntryContainer ec : entryContainers) { baseDNs.add(ec.getBaseDN()); } return baseDNs; } /** * Returns a handle to the root container currently used by this backend. * The rootContainer could be NULL if the backend is not initialized. * * @return The RootContainer object currently used by this backend. */ public final RootContainer getRootContainer() { return rootContainer; } /** * Returns a new read-only handle to the root container for this backend. * The caller is responsible for closing the root container after use. * * @return The read-only RootContainer object for this backend. * * @throws ConfigException If an unrecoverable problem arises during * initialization. * @throws InitializationException If a problem occurs during initialization * that is not related to the server * configuration. */ RootContainer getReadOnlyRootContainer() throws ConfigException, InitializationException { return newRootContainer(AccessMode.READ_ONLY); } /** * Creates a customized DirectoryException from the StorageRuntimeException * thrown by the backend. * * @param e * The StorageRuntimeException to be converted. * @return DirectoryException created from exception. */ private DirectoryException createDirectoryException(Throwable e) { if (e instanceof DirectoryException) { return (DirectoryException) e; } if (e instanceof ExecutionException) { return createDirectoryException(e.getCause()); } if (e instanceof LocalizableException) { return new DirectoryException(serverContext .getCoreConfigManager().getServerErrorResultCode(), ((LocalizableException) e).getMessageObject()); } return new DirectoryException(serverContext .getCoreConfigManager().getServerErrorResultCode(), LocalizableMessage.raw(e.getMessage()==null?e.toString():e.getMessage()), e); } private RootContainer newRootContainer(AccessMode accessMode) throws ConfigException, InitializationException { // Open the storage try { final RootContainer rc = new RootContainer(getBackendID(), serverContext, storage, cfg); rc.open(accessMode); return rc; } catch (StorageInUseException e) { throw new InitializationException(ERR_VERIFY_BACKEND_ONLINE.get(), e); } catch (StorageRuntimeException e) { throw new InitializationException(ERR_OPEN_ENV_FAIL.get(e.getMessage()), e); } } }