| | |
| | | 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 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; |
| | |
| | | return true; |
| | | } |
| | | |
| | | /** |
| | | * {@inheritDoc} |
| | | * <p> |
| | | * {@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. |
| | | * <p> |
| | | * 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(); |
| | | try |
| | | // 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) |
| | | { |
| | | if(rootContainer != null) |
| | | return ccr; |
| | | } |
| | | |
| | | final SortedSet<DN> 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<DN> currentBaseDNs = new HashSet<>(rc.getBaseDNs()); |
| | | final List<EntryContainer> deleted = new ArrayList<>(); |
| | | for (DN baseDN : currentBaseDNs) |
| | | { |
| | | if (!newBaseDNs.contains(baseDN)) |
| | | { |
| | | rootContainer.getStorage().write(new WriteOperation() |
| | | 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())) |
| | | { |
| | | @Override |
| | | public void run(WriteableTransaction txn) throws Exception |
| | | { |
| | | SortedSet<DN> newBaseDNs = newCfg.getBaseDN(); |
| | | |
| | | // Check for changes to the base DNs. |
| | | removeDeletedBaseDNs(newBaseDNs, txn); |
| | | if (!createNewBaseDNs(newBaseDNs, ccr, txn)) |
| | | { |
| | | return; |
| | | } |
| | | |
| | | baseDNs = new HashSet<>(newBaseDNs); |
| | | |
| | | // Put the new configuration in place. |
| | | cfg = newCfg; |
| | | } |
| | | }); |
| | | // 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); |
| | | } |
| | | } |
| | | catch (Exception e) |
| | | final List<DN> added = new ArrayList<>(); |
| | | for (DN baseDN : newBaseDNs) |
| | | { |
| | | ccr.setResultCode(serverContext.getCoreConfigManager().getServerErrorResultCode()); |
| | | ccr.addMessage(LocalizableMessage.raw(stackTraceToSingleLineString(e))); |
| | | 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<EntryContainer> 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; |
| | | } |
| | | |
| | | private void removeDeletedBaseDNs(SortedSet<DN> newBaseDNs, WriteableTransaction txn) throws DirectoryException |
| | | /** |
| | | * 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. |
| | | * <p> |
| | | * 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. |
| | | * <p> |
| | | * 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<EntryContainer> deleted, |
| | | final List<DN> added, final List<EntryContainer> created) throws Exception |
| | | { |
| | | for (DN baseDN : cfg.getBaseDN()) |
| | | for (EntryContainer ec : deleted) |
| | | { |
| | | if (!newBaseDNs.contains(baseDN)) |
| | | // 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() |
| | | { |
| | | // The base DN was deleted. |
| | | serverContext.getBackendConfigManager().deregisterBaseDN(baseDN); |
| | | EntryContainer ec = rootContainer.unregisterEntryContainer(baseDN); |
| | | ec.close(); |
| | | ec.delete(txn); |
| | | @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(); |
| | | } |
| | | } |
| | | } |
| | | |
| | | private boolean createNewBaseDNs(Set<DN> newBaseDNs, ConfigChangeResult ccr, WriteableTransaction txn) |
| | | /** |
| | | * 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. |
| | | * <p> |
| | | * 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<EntryContainer> deleted, ConfigChangeResult ccr) |
| | | { |
| | | for (DN baseDN : newBaseDNs) |
| | | if (deleted.isEmpty()) |
| | | { |
| | | if (!rootContainer.getBaseDNs().contains(baseDN)) |
| | | return; |
| | | } |
| | | final Set<TreeName> 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))) |
| | | { |
| | | try |
| | | { |
| | | // The base DN was added. |
| | | EntryContainer ec = rootContainer.openEntryContainer(baseDN, txn, AccessMode.READ_WRITE); |
| | | rootContainer.registerEntryContainer(baseDN, ec); |
| | | serverContext.getBackendConfigManager().registerBaseDN(baseDN, this, false); |
| | | } |
| | | catch (Exception e) |
| | | { |
| | | logger.traceException(e); |
| | | ccr.setAdminActionRequired(true); |
| | | deregisterDeletedBaseDN(rc, ec, ccr); |
| | | } |
| | | } |
| | | } |
| | | |
| | | ccr.setResultCode(serverContext.getCoreConfigManager().getServerErrorResultCode()); |
| | | ccr.addMessage(ERR_BACKEND_CANNOT_REGISTER_BASEDN.get(baseDN, e)); |
| | | return false; |
| | | private static Set<TreeName> treeNamesOf(EntryContainer ec) |
| | | { |
| | | final Set<TreeName> 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<EntryContainer> 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); |
| | | } |
| | | } |
| | | } |
| | | return true; |
| | | } |
| | | |
| | | private static List<DN> baseDNsOf(List<EntryContainer> entryContainers) |
| | | { |
| | | final List<DN> baseDNs = new ArrayList<>(entryContainers.size()); |
| | | for (EntryContainer ec : entryContainers) |
| | | { |
| | | baseDNs.add(ec.getBaseDN()); |
| | | } |
| | | return baseDNs; |
| | | } |
| | | |
| | | /** |