/*
* 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