/*
|
* 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);
|
}
|
}
|
}
|
}
|