/* * 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 2026 3A Systems, LLC. */ package org.opends.server.backends.pluggable; import static org.assertj.core.api.Assertions.assertThat; import static org.forgerock.opendj.config.ConfigurationMock.mockCfg; import static org.mockito.Mockito.atLeast; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; import static org.opends.server.util.CollectionUtils.newTreeSet; import java.util.ArrayList; import java.util.HashSet; import java.util.List; import java.util.Set; import java.util.SortedSet; import java.util.function.BooleanSupplier; import org.forgerock.opendj.config.server.ConfigException; import org.forgerock.opendj.config.server.ConfigurationAddListener; import org.forgerock.opendj.config.server.ConfigurationChangeListener; import org.forgerock.opendj.config.server.ConfigurationDeleteListener; import org.forgerock.opendj.ldap.ByteSequence; import org.forgerock.opendj.ldap.ByteString; import org.forgerock.opendj.ldap.DN; import org.forgerock.opendj.ldap.schema.AttributeType; import org.forgerock.opendj.server.config.meta.BackendIndexCfgDefn.IndexType; import org.forgerock.opendj.server.config.meta.BackendVLVIndexCfgDefn.Scope; import org.forgerock.opendj.server.config.server.BackendIndexCfg; import org.forgerock.opendj.server.config.server.BackendVLVIndexCfg; import org.forgerock.opendj.server.config.server.PDBBackendCfg; import org.forgerock.opendj.server.config.server.PluggableBackendCfg; import org.mockito.ArgumentCaptor; import org.opends.server.DirectoryServerTestCase; import org.opends.server.TestCaseUtils; import org.opends.server.backends.pdb.PDBStorage; import org.opends.server.backends.pluggable.spi.AccessMode; import org.opends.server.backends.pluggable.spi.Cursor; import org.opends.server.backends.pluggable.spi.Importer; import org.opends.server.backends.pluggable.spi.ReadOperation; 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.StorageStatus; import org.opends.server.backends.pluggable.spi.TreeName; import org.opends.server.backends.pluggable.spi.UpdateFunction; import org.opends.server.backends.pluggable.spi.WriteOperation; import org.opends.server.backends.pluggable.spi.WriteableTransaction; import org.opends.server.core.ServerContext; import org.opends.server.types.BackupConfig; import org.opends.server.types.BackupDirectory; import org.opends.server.types.DirectoryException; import org.opends.server.types.InitializationException; import org.opends.server.types.RestoreConfig; import org.testng.annotations.BeforeClass; import org.testng.annotations.Test; import com.persistit.exception.RollbackException; /** * Tests that a backend which fails to open gives back everything its opening took - see OpenDJ * issue #993. *

* {@link EntryContainer} registers itself and its two configuration managers as listeners of the * backend configuration, and every index it opens registers one of its own. Only * {@link EntryContainer#close()} takes them off again, and an entry container whose * {@link EntryContainer#open} failed is registered nowhere, so nothing will ever call it: the * listeners of a backend which is not running answer configuration changes for the life of the JVM. */ @SuppressWarnings("javadoc") @Test(groups = { "precommit", "pluggablebackend" }, sequential = true) public class FailedBackendOpenTest extends DirectoryServerTestCase { private static final String BACKEND_ID = "FailedBackendOpenTest"; private static final DN BASE_DN = DN.valueOf("dc=b993,dc=com"); /** A second base DN of the same backend, sorting after {@link #BASE_DN}. */ private static final DN SECOND_BASE_DN = DN.valueOf("dc=b993b,dc=com"); private ServerContext serverContext; private AttributeType cnType; /** The index configuration the entry container's attribute index registers with. */ private BackendIndexCfg indexCfg; /** The VLV index configuration the entry container's VLV index registers with. */ private BackendVLVIndexCfg vlvIndexCfg; @BeforeClass public void startServer() throws Exception { TestCaseUtils.startServer(); serverContext = TestCaseUtils.getServerContext(); cnType = serverContext.getSchema().getAttributeType("cn"); } /** * A VLV index whose filter does not parse fails {@code EntryContainer.open()} with a * {@link ConfigException}, after the attribute indexes ahead of it have opened and registered * their own listeners. The backend does not open, and nothing it registered may be left behind. */ @Test public void aBackendWhichFailsToOpenLeavesNothingRegistered() throws Exception { final TrackedBackend backend = new TrackedBackend(); backend.setBackendID(BACKEND_ID); final PDBBackendCfg cfg = backendCfg(newTreeSet(BASE_DN)); when(vlvIndexCfg.getFilter()).thenReturn("(&(objectClass=*)"); backend.configureBackend(cfg, serverContext); backend.storage.removeStorageFiles(); // What an index the stored configuration names, and the schema no longer supports, does. openExpectingFailure(backend, "the backend was expected not to open with a VLV index whose filter does not parse"); assertThat(stillRegisteredOn(cfg)).isEmpty(); assertThat(stillRegisteredOn(indexCfg)).isEmpty(); } /** * A VLV index registers itself as a listener of its configuration from its constructor, and only * reaches the entry container's map once it has opened. A failure in between is the one the * container has always caught, and the index it is closing is still not one it holds. */ @Test public void anIndexWhichFailsToOpenLeavesNoListenerBehind() throws Exception { final TrackedBackend backend = new TrackedBackend(); backend.setBackendID(BACKEND_ID); final PDBBackendCfg cfg = backendCfg(newTreeSet(BASE_DN)); backend.configureBackend(cfg, serverContext); backend.storage.removeStorageFiles(); backend.storage.failOpeningTree("/dc=com,dc=b993/vlv.vlv1"); // What a storage which cannot give the index its tree does. openExpectingFailure(backend, "the backend was expected not to open with a VLV index whose tree cannot be opened"); assertThat(stillRegisteredOn(cfg)).isEmpty(); assertThat(stillRegisteredOn(vlvIndexCfg)).isEmpty(); } /** * The storage a failed open opened is given back along with the listeners. * {@code BackendConfigManager} releases the backend's shared lock and never calls * {@code closeBackend()} for a backend which did not open, so a volume left open here is one no * later attempt to enable that backend can take. */ @Test public void aBackendWhichFailsToOpenGivesBackTheStorageItOpened() throws Exception { final TrackedBackend backend = new TrackedBackend(); backend.setBackendID(BACKEND_ID); final PDBBackendCfg cfg = backendCfg(newTreeSet(BASE_DN)); when(vlvIndexCfg.getFilter()).thenReturn("(&(objectClass=*)"); backend.configureBackend(cfg, serverContext); backend.storage.removeStorageFiles(); // What an index the stored configuration names, and the schema no longer supports, does. final int closesByTheFailedOpen = openExpectingFailure(backend, "the backend was expected not to open with a VLV index whose filter does not parse"); assertThat(closesByTheFailedOpen).isEqualTo(1); } /** * A root container whose storage would not open has nothing of the storage's to give back: a * {@code Storage.open()} which threw returns what it took itself, and a storage which never * opened is not one to close. What is the root container's own - the listener it registered from * its constructor - it gives back all the same. */ @Test public void aRootContainerWhichCouldNotOpenTheStorageDoesNotCloseIt() throws Exception { final TrackedBackend backend = new TrackedBackend(); backend.setBackendID(BACKEND_ID); final PDBBackendCfg cfg = backendCfg(newTreeSet(BASE_DN)); backend.configureBackend(cfg, serverContext); backend.storage.removeStorageFiles(); backend.storage.refuseToOpen(); // What a storage whose volume another process holds does. final int closesByTheFailedOpen = openExpectingFailure(backend, "the backend was expected not to open over a storage whose volume is locked"); assertThat(closesByTheFailedOpen).isEqualTo(0); assertThat(stillRegisteredOn(cfg)).isEmpty(); } /** * The give-back of a failed open walks the entry containers the attempt had registered, which * every base DN but the last one leaves behind when a later one fails: the first base DN's * container is open, registered and answering configuration changes by the time the second one * cannot open its trees. */ @Test public void aSecondBaseDNWhichFailsToOpenGivesBackTheFirst() throws Exception { final TrackedBackend backend = new TrackedBackend(); backend.setBackendID(BACKEND_ID); // A tree set, as the configuration's own is: dc=b993 sorts before dc=b993b, so it is the one // opened - and registered - first. final PDBBackendCfg cfg = backendCfg(newTreeSet(BASE_DN, SECOND_BASE_DN)); backend.configureBackend(cfg, serverContext); backend.storage.removeStorageFiles(); backend.storage.failOpeningTree("/dc=com,dc=b993b/id2entry"); // What a storage which cannot give the second container its trees does. openExpectingFailure(backend, "the backend was expected not to open with a second base DN whose trees cannot be opened"); // The road this test is about: the first container had opened before the second one failed. assertThat(backend.storage.openedTrees()).contains("/dc=com,dc=b993/id2entry"); assertThat(stillRegisteredOn(cfg)).isEmpty(); assertThat(stillRegisteredOn(indexCfg)).isEmpty(); assertThat(stillRegisteredOn(vlvIndexCfg)).isEmpty(); } /** * An entry container which opened has registered everything it ever will, and it is registered * with the root container only after its highest entry ID has been read. A failure of that read * leaves a container which nothing holds, unless it is registered before anything else can * throw. */ @Test public void anEntryContainerWhichOpenedButWasNotRegisteredIsGivenBack() throws Exception { final TrackedBackend backend = new TrackedBackend(); backend.setBackendID(BACKEND_ID); final PDBBackendCfg cfg = backendCfg(newTreeSet(BASE_DN)); backend.configureBackend(cfg, serverContext); backend.storage.removeStorageFiles(); // The read of the highest entry ID is the first cursor over id2entry once the container has // opened, which is when it registers itself: every cursor before that - the emptiness check of // EntryContainer.open() and the one each untrusted index makes as it opens - fails the open // itself, which the container catches. backend.storage.failOpeningCursor("/dc=com,dc=b993/id2entry", () -> anEntryContainerIsRegisteredOn(cfg)); // What a storage which cannot position a cursor on the last entry does. openExpectingFailure(backend, "the backend was expected not to open when the highest entry ID cannot be read"); assertThat(stillRegisteredOn(cfg)).isEmpty(); assertThat(stillRegisteredOn(indexCfg)).isEmpty(); assertThat(stillRegisteredOn(vlvIndexCfg)).isEmpty(); } /** * The positive twin of the tests above: an entry container which opened is registered, once, as * a listener of the backend configuration, and so are its two configuration managers and each * index it opened. Without it, the registrations could be dropped and every test of a failed * open would stay green. */ @Test public void anEntryContainerWhichOpenedIsRegisteredOnce() throws Exception { final TrackedBackend backend = new TrackedBackend(); backend.setBackendID(BACKEND_ID); final PDBBackendCfg cfg = backendCfg(newTreeSet(BASE_DN)); backend.configureBackend(cfg, serverContext); backend.storage.removeStorageFiles(); openExpectingSuccess(backend); final List registered; final List registeredOnIndex; final List registeredOnVLVIndex; try { registered = stillRegisteredOn(cfg); registeredOnIndex = stillRegisteredOn(indexCfg); registeredOnVLVIndex = stillRegisteredOn(vlvIndexCfg); } finally { backend.finalizeBackend(); } assertThat(registered).filteredOn(listener -> listener instanceof EntryContainer).hasSize(1); // The two configuration managers, each once as an add listener and once as a delete listener. assertThat(registered) .filteredOn(listener -> listener.getClass().getSimpleName().endsWith("IndexCfgManager")) .hasSize(4); assertThat(registeredOnIndex).hasSize(1); assertThat(registeredOnVLVIndex).hasSize(1); } /** * {@code RootContainer.open} opens and registers its entry containers inside the write a storage * may replay after a transaction conflict. The attempt which replaces a rolled back one must find * the registry as the first one found it: an entry container left registered fails it with * {@code ERR_ENTRY_CONTAINER_ALREADY_REGISTERED}, so an ordinary write-write conflict becomes a * backend which does not start, and the container it left behind keeps the listeners it opened * with. */ @Test public void aReplayedOpenLeavesOneSetOfEntryContainers() throws Exception { final TrackedBackend backend = new TrackedBackend(); backend.setBackendID(BACKEND_ID); final PDBBackendCfg cfg = backendCfg(newTreeSet(BASE_DN)); backend.configureBackend(cfg, serverContext); backend.storage.removeStorageFiles(); backend.storage.conflictAtCommit(1); openExpectingSuccess(backend); try { assertThat(backend.storage.writeAttempts()).isEqualTo(2); assertThat(backend.getRootContainer().getBaseDNs()).containsOnly(BASE_DN); } finally { backend.finalizeBackend(); } // Closing the backend takes back what the backend which is running registered, so anything // still registered here belongs to the attempt which was rolled back. assertThat(stillRegisteredOn(cfg)).isEmpty(); assertThat(stillRegisteredOn(indexCfg)).isEmpty(); assertThat(stillRegisteredOn(vlvIndexCfg)).isEmpty(); } /** * Opens a backend which is not expected to open, and gives back what the attempt left behind: * the storage a failed open may have left open, or the backend itself when it opened after all - * its base DNs stay registered with the server otherwise, and every test which follows fails in * {@code openBackend()} on them rather than on what it is about. * * @return how many times the failed open closed the storage, before this method closed it */ private static int openExpectingFailure(TrackedBackend backend, String expectation) throws Exception { try { backend.openBackend(); } catch (InitializationException expected) { final int closesByTheFailedOpen = backend.storage.closeCalls(); // A no-op once the failed open has given the storage back, and what keeps the tests which // follow runnable if it has not. backend.storage.close(); return closesByTheFailedOpen; } backend.finalizeBackend(); throw new AssertionError(expectation); } /** * Opens a backend which is expected to open. One which does not is left with its volume closed, * or every test which follows fails in {@code openBackend()} too and the one which actually * broke is lost among them. */ private static void openExpectingSuccess(TrackedBackend backend) throws Exception { try { backend.openBackend(); } catch (Exception failedToOpen) { backend.storage.close(); throw failedToOpen; } } /** Every listener added to the backend configuration and not taken off it again. */ private static List stillRegisteredOn(PluggableBackendCfg cfg) throws ConfigException { final List registered = new ArrayList<>(); final ArgumentCaptor> changeAdded = captorFor(ConfigurationChangeListener.class); verify(cfg, atLeast(0)).addPluggableChangeListener(changeAdded.capture()); registered.addAll(changeAdded.getAllValues()); final ArgumentCaptor> changeRemoved = captorFor(ConfigurationChangeListener.class); verify(cfg, atLeast(0)).removePluggableChangeListener(changeRemoved.capture()); removeEach(registered, changeRemoved.getAllValues()); final ArgumentCaptor> indexAdded = captorFor(ConfigurationAddListener.class); verify(cfg, atLeast(0)).addBackendIndexAddListener(indexAdded.capture()); registered.addAll(indexAdded.getAllValues()); final ArgumentCaptor> indexAddRemoved = captorFor(ConfigurationAddListener.class); verify(cfg, atLeast(0)).removeBackendIndexAddListener(indexAddRemoved.capture()); removeEach(registered, indexAddRemoved.getAllValues()); final ArgumentCaptor> indexDeleteAdded = captorFor(ConfigurationDeleteListener.class); verify(cfg, atLeast(0)).addBackendIndexDeleteListener(indexDeleteAdded.capture()); registered.addAll(indexDeleteAdded.getAllValues()); final ArgumentCaptor> indexDeleteRemoved = captorFor(ConfigurationDeleteListener.class); verify(cfg, atLeast(0)).removeBackendIndexDeleteListener(indexDeleteRemoved.capture()); removeEach(registered, indexDeleteRemoved.getAllValues()); final ArgumentCaptor> vlvAdded = captorFor(ConfigurationAddListener.class); verify(cfg, atLeast(0)).addBackendVLVIndexAddListener(vlvAdded.capture()); registered.addAll(vlvAdded.getAllValues()); final ArgumentCaptor> vlvAddRemoved = captorFor(ConfigurationAddListener.class); verify(cfg, atLeast(0)).removeBackendVLVIndexAddListener(vlvAddRemoved.capture()); removeEach(registered, vlvAddRemoved.getAllValues()); final ArgumentCaptor> vlvDeleteAdded = captorFor(ConfigurationDeleteListener.class); verify(cfg, atLeast(0)).addBackendVLVIndexDeleteListener(vlvDeleteAdded.capture()); registered.addAll(vlvDeleteAdded.getAllValues()); final ArgumentCaptor> vlvDeleteRemoved = captorFor(ConfigurationDeleteListener.class); verify(cfg, atLeast(0)).removeBackendVLVIndexDeleteListener(vlvDeleteRemoved.capture()); removeEach(registered, vlvDeleteRemoved.getAllValues()); return registered; } /** Every listener added to an index configuration and not taken off it again. */ private static List stillRegisteredOn(BackendIndexCfg cfg) { final List registered = new ArrayList<>(); final ArgumentCaptor> added = captorFor(ConfigurationChangeListener.class); verify(cfg, atLeast(0)).addChangeListener(added.capture()); registered.addAll(added.getAllValues()); final ArgumentCaptor> removed = captorFor(ConfigurationChangeListener.class); verify(cfg, atLeast(0)).removeChangeListener(removed.capture()); removeEach(registered, removed.getAllValues()); return registered; } /** Every listener added to a VLV index configuration and not taken off it again. */ private static List stillRegisteredOn(BackendVLVIndexCfg cfg) { final List registered = new ArrayList<>(); final ArgumentCaptor> added = captorFor(ConfigurationChangeListener.class); verify(cfg, atLeast(0)).addChangeListener(added.capture()); registered.addAll(added.getAllValues()); final ArgumentCaptor> removed = captorFor(ConfigurationChangeListener.class); verify(cfg, atLeast(0)).removeChangeListener(removed.capture()); removeEach(registered, removed.getAllValues()); return registered; } /** * Whether an entry container is registered as a listener of the backend configuration, which is * the last thing {@code EntryContainer.open()} does. */ private static boolean anEntryContainerIsRegisteredOn(PluggableBackendCfg cfg) { try { for (Object listener : stillRegisteredOn(cfg)) { if (listener instanceof EntryContainer) { return true; } } return false; } catch (ConfigException declaredButNeverThrownByAMock) { throw new AssertionError(declaredButNeverThrownByAMock); } } /** * Takes one occurrence off the registrations for every removal, rather than every occurrence * for any: a listener registered twice and taken off once is still registered. */ private static void removeEach(List registered, List removed) { for (Object listener : removed) { registered.remove(listener); } } @SuppressWarnings({ "unchecked", "rawtypes" }) private static ArgumentCaptor captorFor(Class listenerClass) { return (ArgumentCaptor) ArgumentCaptor.forClass((Class) listenerClass); } private PDBBackendCfg backendCfg(SortedSet baseDNs) throws ConfigException { final PDBBackendCfg cfg = mockCfg(PDBBackendCfg.class); when(cfg.dn()).thenReturn(DN.valueOf("ds-cfg-backend-id=" + BACKEND_ID + ",cn=Backends,cn=config")); when(cfg.getBackendId()).thenReturn(BACKEND_ID); when(cfg.getDBDirectory()).thenReturn(BACKEND_ID); when(cfg.getDBDirectoryPermissions()).thenReturn("755"); when(cfg.getDBCacheSize()).thenReturn(0L); when(cfg.getDBCachePercent()).thenReturn(20); when(cfg.getBaseDN()).thenReturn(baseDNs); when(cfg.listBackendIndexes()).thenReturn(new String[] { "cn" }); when(cfg.listBackendVLVIndexes()).thenReturn(new String[] { "vlv1" }); indexCfg = mock(BackendIndexCfg.class); when(indexCfg.getIndexType()).thenReturn(newTreeSet(IndexType.EQUALITY)); when(indexCfg.getAttribute()).thenReturn(cnType); when(indexCfg.getIndexEntryLimit()).thenReturn(4000); when(indexCfg.getSubstringLength()).thenReturn(6); when(cfg.getBackendIndex("cn")).thenReturn(indexCfg); vlvIndexCfg = mock(BackendVLVIndexCfg.class); when(vlvIndexCfg.getName()).thenReturn("vlv1"); when(vlvIndexCfg.getBaseDN()).thenReturn(baseDNs.first()); when(vlvIndexCfg.getScope()).thenReturn(Scope.WHOLE_SUBTREE); when(vlvIndexCfg.getFilter()).thenReturn("(objectClass=*)"); when(vlvIndexCfg.getSortOrder()).thenReturn("+cn"); when(cfg.getBackendVLVIndex("vlv1")).thenReturn(vlvIndexCfg); return cfg; } /** A backend whose storage the test can reach, and which counts what closes it. */ private static final class TrackedBackend extends BackendImpl { private TrackingStorage storage; @Override protected Storage configureStorage(PDBBackendCfg cfg, ServerContext serverContext) throws ConfigException { storage = new TrackingStorage(new PDBStorage(cfg, serverContext)); return storage; } } /** * Decorates a {@link Storage} so that the test can tell whether what opened it also gave it back. * {@link #close()} is answered once for each time the storage was opened, so that a test can * close what a failure left open without hiding whether it had already been closed. */ private static final class TrackingStorage implements Storage { private final Storage delegate; private boolean open; private int closeCalls; /** The full name of the tree no transaction of this storage will open, if any. */ private String failingTree; /** The full name of the tree one cursor of which no transaction of this storage will open, if any. */ private String failingCursorTree; /** Once this holds, the next cursor over {@link #failingCursorTree} is the one refused. */ private BooleanSupplier cursorRefusalDue; /** The full names of every tree a transaction of this storage opened. */ private final Set openedTrees = new HashSet<>(); /** Whether this storage refuses to open at all, as one whose volume another process holds does. */ private boolean refuseToOpen; /** How many write operations are still to be conflicted once they have run. */ private int conflictsLeft; private int writeAttempts; TrackingStorage(Storage delegate) { this.delegate = delegate; } /** How many times this storage was asked to close, whether it was open or not. */ int closeCalls() { return closeCalls; } /** Makes every transaction of this storage refuse to open the tree of the given full name. */ void failOpeningTree(String treeName) { failingTree = treeName; } /** * Makes the transactions of this storage refuse to open one cursor over the tree of the given * full name: the first one asked for once the given condition holds. */ void failOpeningCursor(String treeName, BooleanSupplier once) { failingCursorTree = treeName; cursorRefusalDue = once; } /** The full names of every tree a transaction of this storage opened. */ Set openedTrees() { return openedTrees; } /** Makes this storage refuse to open, as one whose volume another process holds does. */ void refuseToOpen() { refuseToOpen = true; } /** * Makes the next write operations conflict once they have run, as PersistIt reports a * write-write conflict at commit time. The conflict is raised from within the single write the * delegate is asked for, so the replay is the delegate's own retry loop. */ void conflictAtCommit(int conflicts) { conflictsLeft = conflicts; } /** How many times a write operation was run, the replays included. */ int writeAttempts() { return writeAttempts; } @Override public void open(AccessMode accessMode) throws Exception { if (refuseToOpen) { throw new StorageInUseException("the volume is locked by another process"); } delegate.open(accessMode); open = true; } @Override public void close() { closeCalls++; // Only what was opened is given back, so that a test can close what a failure left open // without closing the delegate twice. if (open) { open = false; delegate.close(); } } @Override public void write(final WriteOperation writeOperation) throws Exception { delegate.write(new WriteOperation() { @Override public void run(WriteableTransaction txn) throws Exception { writeAttempts++; writeOperation.run(new TrackingTransaction(txn)); if (conflictsLeft > 0) { conflictsLeft--; throw new RollbackException(); } } }); } @Override public T read(ReadOperation readOperation) throws Exception { return delegate.read(readOperation); } @Override public Importer startImport() throws ConfigException { return delegate.startImport(); } @Override public void removeStorageFiles() { delegate.removeStorageFiles(); } @Override public StorageStatus getStorageStatus() { return delegate.getStorageStatus(); } @Override public Set listTrees() { return delegate.listTrees(); } @Override public boolean supportsBackupAndRestore() { return delegate.supportsBackupAndRestore(); } @Override public void createBackup(BackupConfig backupConfig) throws DirectoryException { delegate.createBackup(backupConfig); } @Override public void removeBackup(BackupDirectory backupDirectory, String backupID) throws DirectoryException { delegate.removeBackup(backupDirectory, backupID); } @Override public void restoreBackup(RestoreConfig restoreConfig) throws DirectoryException { delegate.restoreBackup(restoreConfig); } /** * A transaction which is every bit the one it decorates, except that it records the trees it * opens and refuses what the storage was told to refuse: one named tree - what a storage which * cannot give an index the tree it asks for does - or one cursor over one. */ private final class TrackingTransaction implements WriteableTransaction { private final WriteableTransaction delegate; TrackingTransaction(WriteableTransaction delegate) { this.delegate = delegate; } @Override public void openTree(TreeName name, boolean createOnDemand) { if (name.toString().equals(failingTree)) { throw new StorageRuntimeException("cannot open " + name); } delegate.openTree(name, createOnDemand); openedTrees.add(name.toString()); } @Override public Cursor openCursor(TreeName treeName) { if (treeName.toString().equals(failingCursorTree) && cursorRefusalDue.getAsBoolean()) { failingCursorTree = null; throw new StorageRuntimeException("cannot open a cursor over " + treeName); } return delegate.openCursor(treeName); } @Override public void deleteTree(TreeName name) { delegate.deleteTree(name); } @Override public void put(TreeName treeName, ByteSequence key, ByteSequence value) { delegate.put(treeName, key, value); } @Override public boolean update(TreeName treeName, ByteSequence key, UpdateFunction f) { return delegate.update(treeName, key, f); } @Override public boolean delete(TreeName treeName, ByteSequence key) { return delegate.delete(treeName, key); } @Override public ByteString read(TreeName treeName, ByteSequence key) { return delegate.read(treeName, key); } @Override public long getRecordCount(TreeName treeName) { return delegate.getRecordCount(treeName); } @Override public boolean treeExists(TreeName treeName) { return delegate.treeExists(treeName); } } } }