| New file |
| | |
| | | /* |
| | | * 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. |
| | | * <p> |
| | | * {@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<Object> registered; |
| | | final List<Object> registeredOnIndex; |
| | | final List<Object> 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<Object> stillRegisteredOn(PluggableBackendCfg cfg) throws ConfigException |
| | | { |
| | | final List<Object> registered = new ArrayList<>(); |
| | | |
| | | final ArgumentCaptor<ConfigurationChangeListener<PluggableBackendCfg>> changeAdded = |
| | | captorFor(ConfigurationChangeListener.class); |
| | | verify(cfg, atLeast(0)).addPluggableChangeListener(changeAdded.capture()); |
| | | registered.addAll(changeAdded.getAllValues()); |
| | | final ArgumentCaptor<ConfigurationChangeListener<PluggableBackendCfg>> changeRemoved = |
| | | captorFor(ConfigurationChangeListener.class); |
| | | verify(cfg, atLeast(0)).removePluggableChangeListener(changeRemoved.capture()); |
| | | removeEach(registered, changeRemoved.getAllValues()); |
| | | |
| | | final ArgumentCaptor<ConfigurationAddListener<BackendIndexCfg>> indexAdded = |
| | | captorFor(ConfigurationAddListener.class); |
| | | verify(cfg, atLeast(0)).addBackendIndexAddListener(indexAdded.capture()); |
| | | registered.addAll(indexAdded.getAllValues()); |
| | | final ArgumentCaptor<ConfigurationAddListener<BackendIndexCfg>> indexAddRemoved = |
| | | captorFor(ConfigurationAddListener.class); |
| | | verify(cfg, atLeast(0)).removeBackendIndexAddListener(indexAddRemoved.capture()); |
| | | removeEach(registered, indexAddRemoved.getAllValues()); |
| | | |
| | | final ArgumentCaptor<ConfigurationDeleteListener<BackendIndexCfg>> indexDeleteAdded = |
| | | captorFor(ConfigurationDeleteListener.class); |
| | | verify(cfg, atLeast(0)).addBackendIndexDeleteListener(indexDeleteAdded.capture()); |
| | | registered.addAll(indexDeleteAdded.getAllValues()); |
| | | final ArgumentCaptor<ConfigurationDeleteListener<BackendIndexCfg>> indexDeleteRemoved = |
| | | captorFor(ConfigurationDeleteListener.class); |
| | | verify(cfg, atLeast(0)).removeBackendIndexDeleteListener(indexDeleteRemoved.capture()); |
| | | removeEach(registered, indexDeleteRemoved.getAllValues()); |
| | | |
| | | final ArgumentCaptor<ConfigurationAddListener<BackendVLVIndexCfg>> vlvAdded = |
| | | captorFor(ConfigurationAddListener.class); |
| | | verify(cfg, atLeast(0)).addBackendVLVIndexAddListener(vlvAdded.capture()); |
| | | registered.addAll(vlvAdded.getAllValues()); |
| | | final ArgumentCaptor<ConfigurationAddListener<BackendVLVIndexCfg>> vlvAddRemoved = |
| | | captorFor(ConfigurationAddListener.class); |
| | | verify(cfg, atLeast(0)).removeBackendVLVIndexAddListener(vlvAddRemoved.capture()); |
| | | removeEach(registered, vlvAddRemoved.getAllValues()); |
| | | |
| | | final ArgumentCaptor<ConfigurationDeleteListener<BackendVLVIndexCfg>> vlvDeleteAdded = |
| | | captorFor(ConfigurationDeleteListener.class); |
| | | verify(cfg, atLeast(0)).addBackendVLVIndexDeleteListener(vlvDeleteAdded.capture()); |
| | | registered.addAll(vlvDeleteAdded.getAllValues()); |
| | | final ArgumentCaptor<ConfigurationDeleteListener<BackendVLVIndexCfg>> 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<Object> stillRegisteredOn(BackendIndexCfg cfg) |
| | | { |
| | | final List<Object> registered = new ArrayList<>(); |
| | | final ArgumentCaptor<ConfigurationChangeListener<BackendIndexCfg>> added = |
| | | captorFor(ConfigurationChangeListener.class); |
| | | verify(cfg, atLeast(0)).addChangeListener(added.capture()); |
| | | registered.addAll(added.getAllValues()); |
| | | final ArgumentCaptor<ConfigurationChangeListener<BackendIndexCfg>> 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<Object> stillRegisteredOn(BackendVLVIndexCfg cfg) |
| | | { |
| | | final List<Object> registered = new ArrayList<>(); |
| | | final ArgumentCaptor<ConfigurationChangeListener<BackendVLVIndexCfg>> added = |
| | | captorFor(ConfigurationChangeListener.class); |
| | | verify(cfg, atLeast(0)).addChangeListener(added.capture()); |
| | | registered.addAll(added.getAllValues()); |
| | | final ArgumentCaptor<ConfigurationChangeListener<BackendVLVIndexCfg>> 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<Object> registered, List<?> removed) |
| | | { |
| | | for (Object listener : removed) |
| | | { |
| | | registered.remove(listener); |
| | | } |
| | | } |
| | | |
| | | @SuppressWarnings({ "unchecked", "rawtypes" }) |
| | | private static <T> ArgumentCaptor<T> captorFor(Class<?> listenerClass) |
| | | { |
| | | return (ArgumentCaptor<T>) ArgumentCaptor.forClass((Class) listenerClass); |
| | | } |
| | | |
| | | private PDBBackendCfg backendCfg(SortedSet<DN> 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<PDBBackendCfg> |
| | | { |
| | | 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<String> 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<String> 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> T read(ReadOperation<T> 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<TreeName> 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<ByteString, ByteString> 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); |
| | | } |
| | | } |
| | | } |
| | | } |