/*
* 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.any;
import static org.mockito.Mockito.atLeastOnce;
import static org.mockito.Mockito.eq;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
import static org.opends.messages.BackendMessages.ERR_CONFIG_INDEX_ATTRIBUTE_ALREADY_INDEXED;
import static org.opends.messages.BackendMessages.NOTE_INDEX_ADD_REQUIRES_REBUILD;
import static org.opends.messages.BackendMessages.WARN_INDEX_ADD_DISCARDED_LEFTOVER_TREES;
import static org.opends.server.backends.pluggable.State.IndexFlag.TRUSTED;
import static org.opends.server.backends.pluggable.SuffixContainer.STATE_INDEX_NAME;
import static org.opends.server.util.CollectionUtils.newTreeSet;
import java.util.ArrayList;
import java.util.Collection;
import java.util.EnumSet;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.SortedSet;
import org.forgerock.i18n.LocalizableMessage;
import org.forgerock.opendj.config.server.ConfigChangeResult;
import org.forgerock.opendj.config.server.ConfigException;
import org.forgerock.opendj.config.server.ConfigurationAddListener;
import org.forgerock.opendj.ldap.ByteString;
import org.forgerock.opendj.ldap.DN;
import org.forgerock.opendj.ldap.ResultCode;
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.ErrorLogPublisherCfg;
import org.forgerock.opendj.server.config.server.JEBackendCfg;
import org.forgerock.opendj.server.config.server.PDBBackendCfg;
import org.forgerock.opendj.server.config.server.PluggableBackendCfg;
import org.mockito.ArgumentCaptor;
import org.opends.messages.Severity;
import org.opends.server.DirectoryServerTestCase;
import org.opends.server.TestCaseUtils;
import org.opends.server.backends.jeb.JEStorage;
import org.opends.server.backends.pdb.PDBStorage;
import org.opends.server.backends.pluggable.AttributeIndex.MatchingRuleIndex;
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.StorageStatus;
import org.opends.server.backends.pluggable.spi.TreeName;
import org.opends.server.backends.pluggable.spi.WriteOperation;
import org.opends.server.backends.pluggable.spi.WriteableTransaction;
import org.opends.server.core.AddOperation;
import org.opends.server.core.ServerContext;
import org.opends.server.loggers.ErrorLogPublisher;
import org.opends.server.loggers.ErrorLogger;
import org.opends.server.types.BackupConfig;
import org.opends.server.types.BackupDirectory;
import org.opends.server.types.DirectoryException;
import org.opends.server.types.Entry;
import org.opends.server.types.RestoreConfig;
import org.testng.annotations.AfterMethod;
import org.testng.annotations.BeforeClass;
import org.testng.annotations.DataProvider;
import org.testng.annotations.Test;
/**
* Tests what a pluggable backend does with the trees of an index which is declared again after the
* configuration naming them is gone. The name of an index tree is a pure function of the base DN,
* the attribute and the index id, and opening an index creates its trees only if they are not
* there, so an index added for an attribute a previous index of the same name served reopens
* exactly the trees that one left behind - with their content and with the TRUSTED flag their
* {@code state} records still carry. Neither belongs to the index being added: what those trees
* hold is whatever the backend was told before the configuration stopped naming them, and every
* entry written in between is missing from it - see OpenDJ issue #990.
*
* The trees are left behind here by declaring them, filling them and then reopening the backend
* from a configuration which no longer names them, which is what an index deleted while the
* backend is disabled - or through an offline {@code dsconfig} - leaves behind: the entry
* container registers the listener which would have deleted the trees only while it is open
* ({@code EntryContainer}, its constructor and {@code close()}). A deletion the storage gives up on
* and a stop between the deletion of the trees and the commit of the configuration change leave the
* same state; #962 covers the first of those.
*
* Every case runs over PDB and over JE. The two lock differently: JE write-locks the name of a
* deleted tree until the transaction commits, and opening a tree of that name from the same
* transaction - which JE does under a transaction of its own - waits for it forever, so a drop and
* an open sharing one write never return there. The time limit is what makes that a failure rather
* than a hang; under Maven {@code TestListener} replaces it with {@code org.opends.test.timeout}.
*/
@SuppressWarnings("javadoc")
@Test(groups = { "precommit", "pluggablebackend" }, sequential = true)
public class IndexAddedOverLeftoverTreesTest extends DirectoryServerTestCase
{
private static final String BACKEND_ID = "IndexAddedOverLeftoverTreesTest";
private static final DN BASE_DN = DN.valueOf("dc=b990,dc=com");
private static final String VLV_INDEX_NAME = "b990vlv";
private static final long HANG_TIMEOUT_MS = 60_000;
/** The storages the cases run over, each in a directory of its own. */
enum StorageKind
{
PDB
{
@Override
PluggableBackendCfg newBackendCfg(String dbDirectory)
{
final PDBBackendCfg cfg = mockCfg(PDBBackendCfg.class);
when(cfg.getDBDirectory()).thenReturn(dbDirectory);
when(cfg.getDBDirectoryPermissions()).thenReturn("755");
when(cfg.getDBCacheSize()).thenReturn(0L);
when(cfg.getDBCachePercent()).thenReturn(20);
return cfg;
}
@Override
Storage newStorage(PluggableBackendCfg cfg, ServerContext serverContext) throws ConfigException
{
return new PDBStorage((PDBBackendCfg) cfg, serverContext);
}
},
JE
{
@Override
PluggableBackendCfg newBackendCfg(String dbDirectory)
{
final JEBackendCfg cfg = mockCfg(JEBackendCfg.class);
when(cfg.getDBDirectory()).thenReturn(dbDirectory);
when(cfg.getDBDirectoryPermissions()).thenReturn("755");
when(cfg.getDBCacheSize()).thenReturn(0L);
when(cfg.getDBCachePercent()).thenReturn(20);
when(cfg.getDBNumCleanerThreads()).thenReturn(2);
when(cfg.getDBNumLockTables()).thenReturn(63);
return cfg;
}
@Override
Storage newStorage(PluggableBackendCfg cfg, ServerContext serverContext) throws ConfigException
{
return new JEStorage((JEBackendCfg) cfg, serverContext);
}
};
/** A configuration of this storage, with its files under the given directory. */
abstract PluggableBackendCfg newBackendCfg(String dbDirectory);
abstract Storage newStorage(PluggableBackendCfg cfg, ServerContext serverContext) throws ConfigException;
}
private ServerContext serverContext;
private AttributeType cnType;
@BeforeClass
public void startServer() throws Exception
{
TestCaseUtils.startServer();
serverContext = TestCaseUtils.getServerContext();
cnType = serverContext.getSchema().getAttributeType("cn");
}
@DataProvider(name = "storages")
public Object[][] storages()
{
return new Object[][] { { StorageKind.PDB }, { StorageKind.JE } };
}
/**
* A test which fails before it finalizes its backend leaves the base DN behind in the server wide
* registry, where it would outlive the test and break the next one to use it.
*/
@AfterMethod
public void deregisterLeftoverBaseDN()
{
try
{
serverContext.getBackendConfigManager().deregisterBaseDN(BASE_DN);
}
catch (Exception alreadyGone)
{
// Which is what a test that finalized its backend leaves behind.
}
}
/**
* The index the add opens is not the index whose trees are still there: it has indexed none of
* the entries, so it must be untrusted and the operator must be told to rebuild it, exactly as
* for any other index added to a backend which already holds entries - and told, in the change
* result and in the error log, that content was discarded to get there.
*/
@Test(dataProvider = "storages", timeOut = HANG_TIMEOUT_MS)
public void anIndexAddedOverTheTreesLeftBehindIsNotTrusted(StorageKind kind) throws Exception
{
final LeftoverBackend backend = leaveTreesBehind(kind);
final ErrorLogPublisher errorLog = registerErrorLogCapture();
try
{
final EntryContainer ec = backend.getRootContainer().getEntryContainer(BASE_DN);
assertThat(backend.getRootContainer().getStorage().listTrees())
.as("the trees no configuration names any more").containsAll(backend.leftoverIndexTrees);
final ConfigChangeResult ccr = indexAddListener(backend).applyConfigurationAdd(backend.cnIndexCfg);
assertThat(ccr.getResultCode()).isEqualTo(ResultCode.SUCCESS);
assertThat(ec.getAttributeIndex(cnType).isTrusted())
.as("an index trusted over the content of trees it did not fill").isFalse();
assertThat(ccr.adminActionRequired()).isTrue();
assertThat(ordinalsOf(ccr.getMessages())).contains(NOTE_INDEX_ADD_REQUIRES_REBUILD.ordinal(),
WARN_INDEX_ADD_DISCARDED_LEFTOVER_TREES.ordinal());
assertThat(renderedOf(ccr.getMessages())).as("the index and the base DN the report names")
.contains(WARN_INDEX_ADD_DISCARDED_LEFTOVER_TREES.get("cn", BASE_DN).toString());
assertThat(warningOrdinalsLoggedTo(errorLog))
.as("the discard reported in the error log, not only in the change result")
.contains(WARN_INDEX_ADD_DISCARDED_LEFTOVER_TREES.ordinal());
}
finally
{
ErrorLogger.getInstance().removeLogPublisher(errorLog);
backend.finalizeBackend();
}
}
/**
* The flag alone does not make the answers right again: {@code DefaultIndex.get} hands back what
* a key holds whenever it holds anything, whatever the flag says, and only a key with nothing
* behind it answers "undefined" and sends the search to the entries. So the content of the
* adopted trees has to go, or every key they hold goes on answering with the entries of another
* index and misses everything written since.
*/
@Test(dataProvider = "storages", timeOut = HANG_TIMEOUT_MS)
public void anIndexAddedOverTheTreesLeftBehindDoesNotAnswerWithTheirContent(StorageKind kind) throws Exception
{
final LeftoverBackend backend = leaveTreesBehind(kind);
try
{
final EntryContainer ec = backend.getRootContainer().getEntryContainer(BASE_DN);
final Storage storage = backend.getRootContainer().getStorage();
final Map keysHeld = keysHeldBy(storage, backend.leftoverIndexTrees);
assertThat(keysHeld).as("the keys the trees left behind hold").isNotEmpty();
final ConfigChangeResult ccr = indexAddListener(backend).applyConfigurationAdd(backend.cnIndexCfg);
assertThat(ordinalsOf(ccr.getMessages())).contains(WARN_INDEX_ADD_DISCARDED_LEFTOVER_TREES.ordinal());
final AttributeIndex index = ec.getAttributeIndex(cnType);
for (final MatchingRuleIndex opened : index.getNameToIndexes().values())
{
final ByteString keyHeld = keysHeld.get(opened.getName());
if (keyHeld != null)
{
assertThat(answers(storage, opened, keyHeld))
.as("a key of " + opened.getName() + " answered out of the tree left behind").isFalse();
}
}
}
finally
{
backend.finalizeBackend();
}
}
/**
* The trees can be gone while their {@code state} records are not: on JDBC the drop of a table is
* DDL which commits of its own accord, while the {@code state.deleteRecord} of the same
* {@code closeAndDelete} belongs to the transaction, so a rollback after the drop restores the
* records over tables which are already gone. The index added next creates empty trees and reads
* TRUSTED out of those records - an empty index which every search believes. Nothing is
* discarded on this road, so nothing says it was: the record goes, and the rebuild is asked for.
*/
@Test(dataProvider = "storages", timeOut = HANG_TIMEOUT_MS)
public void anIndexAddedOverAStateRecordWhoseTreesAreGoneIsNotTrusted(StorageKind kind) throws Exception
{
final LeftoverBackend backend = leaveTreesBehind(kind);
try
{
final EntryContainer ec = backend.getRootContainer().getEntryContainer(BASE_DN);
final Storage storage = backend.getRootContainer().getStorage();
storage.write(new WriteOperation()
{
@Override
public void run(WriteableTransaction txn) throws Exception
{
for (final TreeName leftover : backend.leftoverIndexTrees)
{
txn.deleteTree(leftover);
}
}
});
for (final TreeName leftover : backend.leftoverIndexTrees)
{
assertThat(persistedFlags(backend, leftover))
.as("the state record left over the tree which is gone").contains(TRUSTED);
}
final ConfigChangeResult ccr = indexAddListener(backend).applyConfigurationAdd(backend.cnIndexCfg);
assertThat(ccr.getResultCode()).isEqualTo(ResultCode.SUCCESS);
assertThat(ec.getAttributeIndex(cnType).isTrusted())
.as("an empty index trusted over a state record which outlived its trees").isFalse();
assertThat(ccr.adminActionRequired()).isTrue();
assertThat(ordinalsOf(ccr.getMessages())).contains(NOTE_INDEX_ADD_REQUIRES_REBUILD.ordinal());
assertThat(ordinalsOf(ccr.getMessages())).as("content reported as discarded where no tree was")
.doesNotContain(WARN_INDEX_ADD_DISCARDED_LEFTOVER_TREES.ordinal());
}
finally
{
backend.finalizeBackend();
}
}
/**
* {@code dropLeftovers} folds each id's result with {@code dropped |= ...} rather than the last one
* assigned; the two only disagree when the ids themselves do, which every other case avoids by giving
* all of them a leftover or none. Deleting one of the two trees {@code leaveTreesBehind} left - the
* cn index has EQUALITY and SUBSTRING - ahead of the add leaves the other one real, so the fold and
* the assignment can differ on whether the change discarded anything.
*/
@Test(dataProvider = "storages", timeOut = HANG_TIMEOUT_MS)
public void anIndexAddedOverOnlyOneOfTwoLeftoverTreesIsReportedDiscarded(StorageKind kind) throws Exception
{
final LeftoverBackend backend = leaveTreesBehind(kind);
try
{
final Storage storage = backend.getRootContainer().getStorage();
final TreeName oneOfTwo = backend.leftoverIndexTrees.iterator().next();
storage.write(new WriteOperation()
{
@Override
public void run(WriteableTransaction txn) throws Exception
{
txn.deleteTree(oneOfTwo);
}
});
final ConfigChangeResult ccr = indexAddListener(backend).applyConfigurationAdd(backend.cnIndexCfg);
assertThat(ccr.getResultCode()).isEqualTo(ResultCode.SUCCESS);
assertThat(ordinalsOf(ccr.getMessages()))
.as("the other of the two ids still had a real tree to discard")
.contains(WARN_INDEX_ADD_DISCARDED_LEFTOVER_TREES.ordinal());
}
finally
{
backend.finalizeBackend();
}
}
/**
* A VLV index adopts what it left behind in the same way, in its tree and in its counter. Both are
* dropped, and so is the {@code state} record the next open of the backend would read the flag
* out of.
*/
@Test(dataProvider = "storages", timeOut = HANG_TIMEOUT_MS)
public void aVlvIndexAddedOverTheTreesLeftBehindIsNotTrusted(StorageKind kind) throws Exception
{
final LeftoverBackend backend = leaveTreesBehind(kind);
final ErrorLogPublisher errorLog = registerErrorLogCapture();
try
{
final EntryContainer ec = backend.getRootContainer().getEntryContainer(BASE_DN);
final Storage storage = backend.getRootContainer().getStorage();
assertThat(storage.listTrees())
.as("the VLV trees no configuration names any more")
.contains(backend.leftoverVlvTree, backend.leftoverVlvCounterTree);
assertThat(holdsAnything(storage, backend.leftoverVlvTree)).as("the VLV tree left behind").isTrue();
assertThat(holdsAnything(storage, backend.leftoverVlvCounterTree)).as("the VLV counter left behind").isTrue();
assertThat(persistedFlags(backend, backend.leftoverVlvTree)).contains(TRUSTED);
final ConfigChangeResult ccr = vlvIndexAddListener(backend).applyConfigurationAdd(backend.vlvIndexCfg);
assertThat(ccr.getResultCode()).isEqualTo(ResultCode.SUCCESS);
assertThat(ec.getVLVIndex(VLV_INDEX_NAME).isTrusted())
.as("a VLV index trusted over the content of trees it did not fill").isFalse();
assertThat(ccr.adminActionRequired()).isTrue();
assertThat(ordinalsOf(ccr.getMessages())).contains(NOTE_INDEX_ADD_REQUIRES_REBUILD.ordinal(),
WARN_INDEX_ADD_DISCARDED_LEFTOVER_TREES.ordinal());
assertThat(renderedOf(ccr.getMessages())).as("the index and the base DN the report names")
.contains(WARN_INDEX_ADD_DISCARDED_LEFTOVER_TREES.get(VLV_INDEX_NAME, BASE_DN).toString());
assertThat(warningOrdinalsLoggedTo(errorLog))
.as("the discard reported in the error log, not only in the change result")
.contains(WARN_INDEX_ADD_DISCARDED_LEFTOVER_TREES.ordinal());
assertThat(holdsAnything(storage, backend.leftoverVlvTree))
.as("the VLV tree left behind still holds its content").isFalse();
assertThat(holdsAnything(storage, backend.leftoverVlvCounterTree))
.as("the VLV counter left behind still holds its count").isFalse();
assertThat(persistedFlags(backend, backend.leftoverVlvTree))
.as("the TRUSTED record outlived the add").doesNotContain(TRUSTED);
}
finally
{
ErrorLogger.getInstance().removeLogPublisher(errorLog);
backend.finalizeBackend();
}
}
/**
* The state record can outlive the VLV tree and its counter the same way it outlives an
* attribute index's tree
* ({@link #anIndexAddedOverAStateRecordWhoseTreesAreGoneIsNotTrusted(StorageKind)}): deleting
* them by hand exercises the two {@code treeExists} guards in
* {@code VLVIndex.dropLeftovers} on their false arm, which the fixture of the case above never
* reaches. On JE, {@code deleteTree} of a tree which is not there is silent, so a guard removed
* or inverted would say content was discarded where none was.
*/
@Test(dataProvider = "storages", timeOut = HANG_TIMEOUT_MS)
public void aVlvIndexAddedOverAStateRecordWhoseTreesAreGoneIsNotTrusted(StorageKind kind) throws Exception
{
final LeftoverBackend backend = leaveTreesBehind(kind);
try
{
final EntryContainer ec = backend.getRootContainer().getEntryContainer(BASE_DN);
final Storage storage = backend.getRootContainer().getStorage();
storage.write(new WriteOperation()
{
@Override
public void run(WriteableTransaction txn) throws Exception
{
txn.deleteTree(backend.leftoverVlvTree);
txn.deleteTree(backend.leftoverVlvCounterTree);
}
});
assertThat(persistedFlags(backend, backend.leftoverVlvTree))
.as("the state record left over the VLV tree which is gone").contains(TRUSTED);
final ConfigChangeResult ccr = vlvIndexAddListener(backend).applyConfigurationAdd(backend.vlvIndexCfg);
assertThat(ccr.getResultCode()).isEqualTo(ResultCode.SUCCESS);
assertThat(ec.getVLVIndex(VLV_INDEX_NAME).isTrusted())
.as("an empty VLV index trusted over a state record which outlived its trees").isFalse();
assertThat(ccr.adminActionRequired()).isTrue();
assertThat(ordinalsOf(ccr.getMessages())).contains(NOTE_INDEX_ADD_REQUIRES_REBUILD.ordinal());
assertThat(ordinalsOf(ccr.getMessages())).as("content reported as discarded where no tree was")
.doesNotContain(WARN_INDEX_ADD_DISCARDED_LEFTOVER_TREES.ordinal());
}
finally
{
backend.finalizeBackend();
}
}
/**
* An index type declared again for an attribute which is still indexed goes through
* {@code AttributeIndex.applyConfigurationChange} rather than through the add listener, and opens
* its tree the same way. Only the tree of the id declared again goes: the tree of the id the index
* was serving all along keeps what it holds.
*/
@Test(dataProvider = "storages", timeOut = HANG_TIMEOUT_MS)
public void anIndexTypeAddedOverTheTreeLeftBehindIsNotTrusted(StorageKind kind) throws Exception
{
final LeftoverBackend backend = leaveTreesBehind(kind, indexCfg(newTreeSet(IndexType.EQUALITY)));
try
{
final EntryContainer ec = backend.getRootContainer().getEntryContainer(BASE_DN);
final Storage storage = backend.getRootContainer().getStorage();
final AttributeIndex index = ec.getAttributeIndex(cnType);
assertThat(index.isIndexed(IndexType.SUBSTRING))
.as("the index type this configuration no longer names").isFalse();
final Map keysHeld = keysHeldBy(storage, backend.leftoverIndexTrees);
// The index ids are the matching rules' - the one the index serves now is the equality one.
final Map servedBefore = new HashMap<>(index.getNameToIndexes());
assertThat(servedBefore).hasSize(1);
final MatchingRuleIndex equality = servedBefore.values().iterator().next();
final ByteString equalityKey = keysHeld.get(equality.getName());
assertThat(equalityKey).as("a key the live equality tree holds").isNotNull();
final ConfigChangeResult ccr =
index.applyConfigurationChange(indexCfg(newTreeSet(IndexType.EQUALITY, IndexType.SUBSTRING)));
assertThat(ccr.getResultCode()).isEqualTo(ResultCode.SUCCESS);
assertThat(index.isIndexed(IndexType.SUBSTRING)).isTrue();
assertThat(index.isTrusted())
.as("an index type trusted over the content of the tree it did not fill").isFalse();
assertThat(ccr.adminActionRequired()).isTrue();
assertThat(ordinalsOf(ccr.getMessages())).contains(NOTE_INDEX_ADD_REQUIRES_REBUILD.ordinal(),
WARN_INDEX_ADD_DISCARDED_LEFTOVER_TREES.ordinal());
final Map added = new HashMap<>(index.getNameToIndexes());
added.keySet().removeAll(servedBefore.keySet());
assertThat(added).hasSize(1);
final MatchingRuleIndex substring = added.values().iterator().next();
// Named by its tree on this road, where the add listener names the index by its attribute.
assertThat(renderedOf(ccr.getMessages())).as("the tree and the base DN the report names")
.contains(WARN_INDEX_ADD_DISCARDED_LEFTOVER_TREES.get(substring.getName(), BASE_DN).toString());
assertThat(keysHeld).as("the substring tree left behind held a key").containsKey(substring.getName());
assertThat(answers(storage, substring, keysHeld.get(substring.getName())))
.as("a substring key answered out of the tree left behind").isFalse();
assertThat(answers(storage, equality, equalityKey))
.as("the live equality tree went with the leftover").isTrue();
}
finally
{
backend.finalizeBackend();
}
}
/**
* What the drop write found is reported from a {@code finally}, so a throw from the write which
* opens the index - after the drop has committed and the content is gone for good - does not
* swallow it: the change fails, and the change result and the error log still say what was
* discarded. Neither storage engine can be made to throw on its own, so the failure is injected
* through {@link RefusingStorage}: the drop write goes through, the open write is refused before
* it runs.
*/
@Test(dataProvider = "storages", timeOut = HANG_TIMEOUT_MS)
public void aDiscardIsStillReportedWhenTheWriteWhichOpensTheIndexFails(StorageKind kind) throws Exception
{
final LeftoverBackend backend = leaveTreesBehind(kind);
final ErrorLogPublisher errorLog = registerErrorLogCapture();
try
{
final Storage storage = backend.getRootContainer().getStorage();
final int writesBefore = backend.storage.writes();
backend.storage.refuseWrite(2);
final ConfigChangeResult ccr = indexAddListener(backend).applyConfigurationAdd(backend.cnIndexCfg);
assertThat(backend.storage.writes()).as("the refused write was the second, and the last one made")
.isEqualTo(writesBefore + 2);
assertThat(ccr.getResultCode()).isNotEqualTo(ResultCode.SUCCESS);
assertThat(ordinalsOf(ccr.getMessages())).as("the discard the failed change swallowed")
.contains(WARN_INDEX_ADD_DISCARDED_LEFTOVER_TREES.ordinal());
assertThat(warningOrdinalsLoggedTo(errorLog)).contains(WARN_INDEX_ADD_DISCARDED_LEFTOVER_TREES.ordinal());
for (final TreeName leftover : backend.leftoverIndexTrees)
{
assertThat(storage. read(txn -> txn.treeExists(leftover)))
.as("the tree the report says was discarded: " + leftover).isFalse();
}
}
finally
{
ErrorLogger.getInstance().removeLogPublisher(errorLog);
backend.finalizeBackend();
}
}
/** The VLV road reports from a {@code finally} the same way. */
@Test(dataProvider = "storages", timeOut = HANG_TIMEOUT_MS)
public void aVlvDiscardIsStillReportedWhenTheWriteWhichBuildsTheIndexFails(StorageKind kind) throws Exception
{
final LeftoverBackend backend = leaveTreesBehind(kind);
final ErrorLogPublisher errorLog = registerErrorLogCapture();
try
{
final Storage storage = backend.getRootContainer().getStorage();
final int writesBefore = backend.storage.writes();
backend.storage.refuseWrite(2);
final ConfigChangeResult ccr = vlvIndexAddListener(backend).applyConfigurationAdd(backend.vlvIndexCfg);
assertThat(backend.storage.writes()).as("the refused write was the second, and the last one made")
.isEqualTo(writesBefore + 2);
assertThat(ccr.getResultCode()).isNotEqualTo(ResultCode.SUCCESS);
assertThat(ordinalsOf(ccr.getMessages())).as("the discard the failed change swallowed")
.contains(WARN_INDEX_ADD_DISCARDED_LEFTOVER_TREES.ordinal());
assertThat(warningOrdinalsLoggedTo(errorLog)).contains(WARN_INDEX_ADD_DISCARDED_LEFTOVER_TREES.ordinal());
assertThat(storage. read(txn -> txn.treeExists(backend.leftoverVlvTree)))
.as("the VLV tree the report says was discarded").isFalse();
assertThat(storage. read(txn -> txn.treeExists(backend.leftoverVlvCounterTree)))
.as("the VLV counter the report says was discarded").isFalse();
}
finally
{
ErrorLogger.getInstance().removeLogPublisher(errorLog);
backend.finalizeBackend();
}
}
/**
* And so does {@code AttributeIndex.applyConfigurationChange}, whose drop write is the first of
* the writes a change which adds an index type makes, and whose open write is the second.
*/
@Test(dataProvider = "storages", timeOut = HANG_TIMEOUT_MS)
public void anIndexTypeDiscardIsStillReportedWhenTheWriteWhichOpensItFails(StorageKind kind) throws Exception
{
final LeftoverBackend backend = leaveTreesBehind(kind, indexCfg(newTreeSet(IndexType.EQUALITY)));
final ErrorLogPublisher errorLog = registerErrorLogCapture();
try
{
final EntryContainer ec = backend.getRootContainer().getEntryContainer(BASE_DN);
final Storage storage = backend.getRootContainer().getStorage();
final AttributeIndex index = ec.getAttributeIndex(cnType);
final Map keysHeld = keysHeldBy(storage, backend.leftoverIndexTrees);
final MatchingRuleIndex equality = index.getNameToIndexes().values().iterator().next();
final Set substringTrees = new HashSet<>(backend.leftoverIndexTrees);
substringTrees.remove(equality.getName());
assertThat(substringTrees).hasSize(1);
final int writesBefore = backend.storage.writes();
backend.storage.refuseWrite(2);
final ConfigChangeResult ccr =
index.applyConfigurationChange(indexCfg(newTreeSet(IndexType.EQUALITY, IndexType.SUBSTRING)));
assertThat(backend.storage.writes()).as("the refused write was the second, and the last one made")
.isEqualTo(writesBefore + 2);
assertThat(ccr.getResultCode()).isNotEqualTo(ResultCode.SUCCESS);
assertThat(ordinalsOf(ccr.getMessages())).as("the discard the failed change swallowed")
.contains(WARN_INDEX_ADD_DISCARDED_LEFTOVER_TREES.ordinal());
assertThat(warningOrdinalsLoggedTo(errorLog)).contains(WARN_INDEX_ADD_DISCARDED_LEFTOVER_TREES.ordinal());
final TreeName substringTree = substringTrees.iterator().next();
assertThat(storage. read(txn -> txn.treeExists(substringTree)))
.as("the tree the report says was discarded").isFalse();
assertThat(index.isIndexed(IndexType.SUBSTRING)).as("an index type the failed change declared").isFalse();
assertThat(answers(storage, equality, keysHeld.get(equality.getName())))
.as("the live equality tree went with the leftover").isTrue();
}
finally
{
ErrorLogger.getInstance().removeLogPublisher(errorLog);
backend.finalizeBackend();
}
}
/**
* The drop write can fail at its own commit, once it has run and answered - a disk which is full
* when the engine writes its log. JE and PDB roll the drop back with it: the trees are still
* there, and so is the TRUSTED record the next open of the backend reads. The report stands all
* the same, and says more than happened. It is kept that way on purpose: the configuration entry
* is already written when the listener runs, so what comes next is not another add but that
* open, which adopts the trees - and the rebuild the report asks for is what puts it right. On
* JDBC the same failure comes after a DROP which committed on its own, and the report is the only
* trace of it; a report made to wait for the commit would be silent there, in the one case the
* drop exists for. Pinned so that moving it behind the commit is red here rather than silent
* there.
*/
@Test(dataProvider = "storages", timeOut = HANG_TIMEOUT_MS)
public void aDiscardIsStillReportedWhenTheDropWriteFailsAtItsCommit(StorageKind kind) throws Exception
{
final LeftoverBackend backend = leaveTreesBehind(kind);
try
{
final Storage storage = backend.getRootContainer().getStorage();
final int writesBefore = backend.storage.writes();
backend.storage.failWriteAfterItRan(1);
final ConfigChangeResult ccr = indexAddListener(backend).applyConfigurationAdd(backend.cnIndexCfg);
assertThat(backend.storage.writes()).as("the failed write was the first, and the last one made")
.isEqualTo(writesBefore + 1);
assertThat(ccr.getResultCode()).isNotEqualTo(ResultCode.SUCCESS);
assertThat(ordinalsOf(ccr.getMessages())).as("the report of a drop the engine rolled back")
.contains(WARN_INDEX_ADD_DISCARDED_LEFTOVER_TREES.ordinal());
for (final TreeName leftover : backend.leftoverIndexTrees)
{
assertThat(holdsAnything(storage, leftover)).as("the drop of " + leftover + " rolled back with the write")
.isTrue();
assertThat(persistedFlags(backend, leftover)).as("the record of " + leftover).contains(TRUSTED);
}
}
finally
{
backend.finalizeBackend();
}
}
/**
* The VLV road keeps its report when the drop write fails at its commit, on the terms the case
* above gives: the tree and its counter are back, and so is the TRUSTED record.
*/
@Test(dataProvider = "storages", timeOut = HANG_TIMEOUT_MS)
public void aVlvDiscardIsStillReportedWhenTheDropWriteFailsAtItsCommit(StorageKind kind) throws Exception
{
final LeftoverBackend backend = leaveTreesBehind(kind);
try
{
final Storage storage = backend.getRootContainer().getStorage();
final int writesBefore = backend.storage.writes();
backend.storage.failWriteAfterItRan(1);
final ConfigChangeResult ccr = vlvIndexAddListener(backend).applyConfigurationAdd(backend.vlvIndexCfg);
assertThat(backend.storage.writes()).as("the failed write was the first, and the last one made")
.isEqualTo(writesBefore + 1);
assertThat(ccr.getResultCode()).isNotEqualTo(ResultCode.SUCCESS);
assertThat(ordinalsOf(ccr.getMessages())).as("the report of a drop the engine rolled back")
.contains(WARN_INDEX_ADD_DISCARDED_LEFTOVER_TREES.ordinal());
assertThat(holdsAnything(storage, backend.leftoverVlvTree)).as("the VLV tree rolled back with the write")
.isTrue();
assertThat(holdsAnything(storage, backend.leftoverVlvCounterTree))
.as("the VLV counter rolled back with the write").isTrue();
assertThat(persistedFlags(backend, backend.leftoverVlvTree)).as("the record of the VLV tree").contains(TRUSTED);
}
finally
{
backend.finalizeBackend();
}
}
/**
* And so does {@code AttributeIndex.applyConfigurationChange}: the tree of the index type
* declared again is back with its TRUSTED record, the change has not been applied, and the
* report stands.
*/
@Test(dataProvider = "storages", timeOut = HANG_TIMEOUT_MS)
public void anIndexTypeDiscardIsStillReportedWhenTheDropWriteFailsAtItsCommit(StorageKind kind) throws Exception
{
final LeftoverBackend backend = leaveTreesBehind(kind, indexCfg(newTreeSet(IndexType.EQUALITY)));
try
{
final EntryContainer ec = backend.getRootContainer().getEntryContainer(BASE_DN);
final Storage storage = backend.getRootContainer().getStorage();
final AttributeIndex index = ec.getAttributeIndex(cnType);
final Map keysHeld = keysHeldBy(storage, backend.leftoverIndexTrees);
final MatchingRuleIndex equality = index.getNameToIndexes().values().iterator().next();
final Set substringTrees = new HashSet<>(backend.leftoverIndexTrees);
substringTrees.remove(equality.getName());
assertThat(substringTrees).hasSize(1);
final TreeName substringTree = substringTrees.iterator().next();
final int writesBefore = backend.storage.writes();
backend.storage.failWriteAfterItRan(1);
final ConfigChangeResult ccr =
index.applyConfigurationChange(indexCfg(newTreeSet(IndexType.EQUALITY, IndexType.SUBSTRING)));
assertThat(backend.storage.writes()).as("the failed write was the first, and the last one made")
.isEqualTo(writesBefore + 1);
assertThat(ccr.getResultCode()).isNotEqualTo(ResultCode.SUCCESS);
assertThat(ordinalsOf(ccr.getMessages())).as("the report of a drop the engine rolled back")
.contains(WARN_INDEX_ADD_DISCARDED_LEFTOVER_TREES.ordinal());
assertThat(holdsAnything(storage, substringTree)).as("the substring tree rolled back with the write").isTrue();
assertThat(persistedFlags(backend, substringTree)).as("the record of the substring tree").contains(TRUSTED);
assertThat(index.isIndexed(IndexType.SUBSTRING)).as("an index type the failed change declared").isFalse();
assertThat(answers(storage, equality, keysHeld.get(equality.getName())))
.as("the live equality tree went with the leftover").isTrue();
}
finally
{
backend.finalizeBackend();
}
}
/**
* An index added to a backend which holds no entry has nothing to index and nothing to adopt, so
* it stays trusted and asks for nothing. Pins the upgrade {@code DefaultIndex.afterOpen} makes.
*/
@Test(dataProvider = "storages", timeOut = HANG_TIMEOUT_MS)
public void anIndexAddedToAnEmptyBackendIsTrustedAndAsksForNothing(StorageKind kind) throws Exception
{
final LeftoverBackend backend = openBackend(kind, true, null, null);
try
{
final ConfigChangeResult ccr = indexAddListener(backend).applyConfigurationAdd(backend.cnIndexCfg);
assertThat(ccr.getResultCode()).isEqualTo(ResultCode.SUCCESS);
assertThat(backend.getRootContainer().getEntryContainer(BASE_DN).getAttributeIndex(cnType).isTrusted())
.as("an index of a backend with nothing to index").isTrue();
assertThat(ccr.adminActionRequired()).isFalse();
assertThat(ccr.getMessages()).isEmpty();
}
finally
{
backend.finalizeBackend();
}
}
/**
* An index added to a backend which holds entries, with nothing left behind for it to adopt, is
* untrusted over trees it created empty and asks to be rebuilt. Pins what the fix has to leave
* alone: this is the outcome an index added over leftover trees has to reach as well, and nothing
* was discarded to reach it here.
*/
@Test(dataProvider = "storages", timeOut = HANG_TIMEOUT_MS)
public void anIndexAddedToANonEmptyBackendAsksForARebuild(StorageKind kind) throws Exception
{
final LeftoverBackend backend = openBackend(kind, true, null, null);
try
{
addEntry(backend, "dn: " + BASE_DN, "objectClass: top", "objectClass: domain", "dc: b990");
final ConfigChangeResult ccr = indexAddListener(backend).applyConfigurationAdd(backend.cnIndexCfg);
assertThat(ccr.getResultCode()).isEqualTo(ResultCode.SUCCESS);
assertThat(backend.getRootContainer().getEntryContainer(BASE_DN).getAttributeIndex(cnType).isTrusted())
.as("an index which has indexed none of the entries").isFalse();
assertThat(ccr.adminActionRequired()).isTrue();
assertThat(ordinalsOf(ccr.getMessages())).contains(NOTE_INDEX_ADD_REQUIRES_REBUILD.ordinal());
assertThat(ordinalsOf(ccr.getMessages())).as("content reported as discarded where nothing was left behind")
.doesNotContain(WARN_INDEX_ADD_DISCARDED_LEFTOVER_TREES.ordinal());
}
finally
{
backend.finalizeBackend();
}
}
/**
* The entry container keys its indexes by the attribute type, which every name of the attribute
* and its OID resolve to, while a configuration entry is named by whichever of them was typed. An
* index declared for cn as commonName or as 2.5.4.3 is therefore a second index for an attribute
* which is already indexed, naming the very trees the live index serves - and those must not go
* the way of trees left behind. The add is refused instead, before the configuration is written
* and again where it would have dropped them.
*/
@Test(dataProvider = "storages", timeOut = HANG_TIMEOUT_MS)
public void aSecondIndexForAnAttributeAlreadyIndexedIsRefused(StorageKind kind) throws Exception
{
final LeftoverBackend backend =
openBackend(kind, true, indexCfg(newTreeSet(IndexType.EQUALITY, IndexType.SUBSTRING)), null);
try
{
addEntry(backend, "dn: " + BASE_DN, "objectClass: top", "objectClass: domain", "dc: b990");
addEntry(backend, "dn: cn=live," + BASE_DN, "objectClass: top", "objectClass: organizationalRole", "cn: live");
final EntryContainer ec = backend.getRootContainer().getEntryContainer(BASE_DN);
final Storage storage = backend.getRootContainer().getStorage();
final AttributeIndex live = ec.getAttributeIndex(cnType);
assertThat(live.isTrusted()).isTrue();
final Map keysHeld = keysHeldBy(storage, treesOf(live));
assertThat(keysHeld).as("the keys the live index holds").isNotEmpty();
// What was typed names the configuration entry; the attribute type it resolves to is the one already indexed.
final DN typedAs = DN.valueOf("ds-cfg-attribute=commonName,cn=Index," + backend.configuredWith.dn());
final BackendIndexCfg declaredAgain = indexCfg(newTreeSet(IndexType.EQUALITY));
when(declaredAgain.dn()).thenReturn(typedAs);
final ConfigurationAddListener listener = indexAddListener(backend);
// An index for an attribute which is not indexed yet is still accepted - pins the refusing arm above
// against a mutant which refuses every add.
final BackendIndexCfg fresh = indexCfg(newTreeSet(IndexType.EQUALITY));
when(fresh.getAttribute()).thenReturn(serverContext.getSchema().getAttributeType("sn"));
final List reasons = new ArrayList<>();
assertThat(listener.isConfigurationAddAcceptable(fresh, reasons)).isTrue();
assertThat(reasons).isEmpty();
assertThat(listener.isConfigurationAddAcceptable(declaredAgain, reasons)).isFalse();
assertThat(ordinalsOf(reasons)).contains(ERR_CONFIG_INDEX_ATTRIBUTE_ALREADY_INDEXED.ordinal());
final ConfigChangeResult ccr = listener.applyConfigurationAdd(declaredAgain);
assertThat(ccr.getResultCode()).isNotEqualTo(ResultCode.SUCCESS);
assertThat(ordinalsOf(ccr.getMessages())).contains(ERR_CONFIG_INDEX_ATTRIBUTE_ALREADY_INDEXED.ordinal());
assertThat(ec.getAttributeIndex(cnType)).as("the live index was replaced").isSameAs(live);
assertThat(live.isTrusted()).isTrue();
for (final MatchingRuleIndex opened : live.getNameToIndexes().values())
{
assertThat(answers(storage, opened, keysHeld.get(opened.getName())))
.as("a key of " + opened.getName() + " the live index held before").isTrue();
}
}
finally
{
backend.finalizeBackend();
}
}
private LeftoverBackend leaveTreesBehind(StorageKind kind) throws Exception
{
return leaveTreesBehind(kind, null);
}
/**
* Opens a backend whose configuration names a cn index and a VLV index, fills them with an entry,
* and reopens it from a configuration which names {@code reopenedWith} instead - null for a
* configuration which names no index at all. The trees of everything it no longer names are left
* behind, and the entry added afterwards is in none of them.
*/
private LeftoverBackend leaveTreesBehind(StorageKind kind, BackendIndexCfg reopenedWith) throws Exception
{
final LeftoverBackend indexed =
openBackend(kind, true, indexCfg(newTreeSet(IndexType.EQUALITY, IndexType.SUBSTRING)), vlvIndexCfg());
final Set indexTrees;
final TreeName vlvTree;
final TreeName vlvCounterTree;
try
{
addEntry(indexed, "dn: " + BASE_DN, "objectClass: top", "objectClass: domain", "dc: b990");
addEntry(indexed, "dn: cn=stale," + BASE_DN, "objectClass: top", "objectClass: organizationalRole",
"cn: stale");
final EntryContainer ec = indexed.getRootContainer().getEntryContainer(BASE_DN);
indexTrees = treesOf(ec.getAttributeIndex(cnType));
vlvTree = new TreeName(ec.getTreePrefix(), "vlv." + VLV_INDEX_NAME);
vlvCounterTree = new TreeName(ec.getTreePrefix(), "counter.vlv." + VLV_INDEX_NAME);
}
finally
{
indexed.finalizeBackend();
}
final LeftoverBackend reopened = openBackend(kind, false, reopenedWith, null);
try
{
addEntry(reopened, "dn: cn=fresh," + BASE_DN, "objectClass: top", "objectClass: organizationalRole",
"cn: fresh");
}
catch (Exception e)
{
reopened.finalizeBackend();
throw e;
}
reopened.leftoverIndexTrees = indexTrees;
reopened.leftoverVlvTree = vlvTree;
reopened.leftoverVlvCounterTree = vlvCounterTree;
return reopened;
}
/** The first key each of the given trees holds, for the trees which hold any. */
private static Map keysHeldBy(Storage storage, Set trees) throws Exception
{
final Map keys = new HashMap<>();
for (final TreeName tree : trees)
{
final ByteString key = storage.read(txn -> {
try (Cursor cursor = txn.openCursor(tree))
{
return cursor.next() ? cursor.getKey() : null;
}
});
if (key != null)
{
keys.put(tree, key);
}
}
return keys;
}
private static boolean holdsAnything(Storage storage, TreeName tree) throws Exception
{
return storage.read(txn -> {
try (Cursor cursor = txn.openCursor(tree))
{
return cursor.next();
}
});
}
/**
* Whether the index answers the key with a non-empty entry ID set, as a search reading it would get one.
* {@code DefaultIndex.get} answers {@code newDefinedSet()} for a missing key while the index is trusted, so
* {@code isDefined()} alone would pin the trusted flag rather than the content a leftover tree still holds.
*/
private static boolean answers(Storage storage, MatchingRuleIndex index, ByteString key) throws Exception
{
return storage.read(txn -> {
final EntryIDSet ids = index.get(txn, key);
return ids.isDefined() && ids.size() > 0;
});
}
/** Reads back the flags an index tree carries, as they are stored. */
private EnumSet persistedFlags(LeftoverBackend backend, TreeName index) throws Exception
{
final EntryContainer ec = backend.getRootContainer().getEntryContainer(BASE_DN);
final State state = new State(new TreeName(ec.getTreePrefix(), STATE_INDEX_NAME));
return backend.getRootContainer().getStorage().read(txn -> state.getIndexFlags(txn, index));
}
/** The messages a change carries, as the operator reads them: identity and arguments both. */
private static List renderedOf(Collection messages)
{
final List rendered = new ArrayList<>();
for (final LocalizableMessage message : messages)
{
rendered.add(message.toString());
}
return rendered;
}
/** The messages a change carries, by identity rather than by their formatted text. */
private static Set ordinalsOf(Collection messages)
{
final Set ordinals = new HashSet<>();
for (final LocalizableMessage message : messages)
{
ordinals.add(message.ordinal());
}
return ordinals;
}
/**
* Registers a mock {@code ErrorLogPublisher}, enabled for every category and severity, so that
* {@link #warningOrdinalsLoggedTo} can read back what {@code AttributeIndex.reportDiscardedLeftovers}
* put in the error log - the half of it the change result alone does not exercise. The caller removes
* it once done, from a {@code finally}: it would otherwise go on capturing messages other tests log.
*/
@SuppressWarnings("unchecked")
private static ErrorLogPublisher registerErrorLogCapture()
{
final ErrorLogPublisher errorLog = mock(ErrorLogPublisher.class);
when(errorLog.isEnabledFor(any(), any())).thenReturn(true);
ErrorLogger.getInstance().addLogPublisher(errorLog);
return errorLog;
}
/** The ordinals of the messages logged at {@link Severity#WARNING} through the given capture. */
private static Set warningOrdinalsLoggedTo(ErrorLogPublisher errorLog)
{
final ArgumentCaptor logged = ArgumentCaptor.forClass(LocalizableMessage.class);
verify(errorLog, atLeastOnce()).log(any(), eq(Severity.WARNING), logged.capture(), any());
return ordinalsOf(logged.getAllValues());
}
private static Set treesOf(AttributeIndex index)
{
final Set names = new HashSet<>();
for (final MatchingRuleIndex matchingRuleIndex : index.getNameToIndexes().values())
{
names.add(matchingRuleIndex.getName());
}
return names;
}
private static void addEntry(LeftoverBackend backend, String... ldifLines) throws Exception
{
final Entry entry = TestCaseUtils.makeEntry(ldifLines);
backend.addEntry(entry, mock(AddOperation.class));
}
@SuppressWarnings({ "unchecked", "rawtypes" })
private static ConfigurationAddListener indexAddListener(LeftoverBackend backend)
throws ConfigException
{
final ArgumentCaptor captor = ArgumentCaptor.forClass(ConfigurationAddListener.class);
verify(backend.configuredWith).addBackendIndexAddListener(captor.capture());
return captor.getValue();
}
@SuppressWarnings({ "unchecked", "rawtypes" })
private static ConfigurationAddListener vlvIndexAddListener(LeftoverBackend backend)
throws ConfigException
{
final ArgumentCaptor captor = ArgumentCaptor.forClass(ConfigurationAddListener.class);
verify(backend.configuredWith).addBackendVLVIndexAddListener(captor.capture());
return captor.getValue();
}
/**
* Opens a backend whose configuration names the given index and VLV index - null for one it does
* not name, whose trees are then left to whatever is already in the storage.
*/
private LeftoverBackend openBackend(StorageKind kind, boolean pristine, BackendIndexCfg cnIndexCfg,
BackendVLVIndexCfg vlvIndexCfg) throws Exception
{
final LeftoverBackend backend = new LeftoverBackend(kind);
backend.setBackendID(BACKEND_ID);
backend.cnIndexCfg = cnIndexCfg != null ? cnIndexCfg
: indexCfg(newTreeSet(IndexType.EQUALITY, IndexType.SUBSTRING));
backend.vlvIndexCfg = vlvIndexCfg != null ? vlvIndexCfg : vlvIndexCfg();
backend.configuredWith = backendCfg(kind, cnIndexCfg, vlvIndexCfg);
backend.configureBackend(backend.configuredWith, serverContext);
if (pristine)
{
// Start from a pristine on-disk state, so that a previous run cannot mask what this one leaves.
backend.storage.removeStorageFiles();
}
try
{
backend.openBackend();
}
catch (Exception e)
{
// openBackend() opens the root container before it registers the base DNs and the monitor, so a failure in
// any of those leaves the volume open and every following test failing here too. finalizeBackend() is no
// use for it: closeBackend() deregisters the monitor before it closes the root container, and NPEs on the
// monitor which was never registered.
try
{
final RootContainer root = backend.getRootContainer();
if (root != null)
{
root.close();
}
else
{
backend.storage.close();
}
}
catch (Exception cleanupFailure)
{
e.addSuppressed(cleanupFailure);
}
throw e;
}
return backend;
}
private PluggableBackendCfg backendCfg(StorageKind kind, BackendIndexCfg cnIndexCfg, BackendVLVIndexCfg vlvIndexCfg)
throws ConfigException
{
final PluggableBackendCfg cfg = kind.newBackendCfg(BACKEND_ID + "." + kind);
when(cfg.dn()).thenReturn(DN.valueOf("ds-cfg-backend-id=" + BACKEND_ID + ",cn=Backends,cn=config"));
when(cfg.getBackendId()).thenReturn(BACKEND_ID);
when(cfg.getBaseDN()).thenReturn(newTreeSet(BASE_DN));
when(cfg.listBackendIndexes()).thenReturn(cnIndexCfg != null ? new String[] { "cn" } : new String[0]);
when(cfg.getBackendIndex("cn")).thenReturn(cnIndexCfg);
when(cfg.listBackendVLVIndexes())
.thenReturn(vlvIndexCfg != null ? new String[] { VLV_INDEX_NAME } : new String[0]);
when(cfg.getBackendVLVIndex(VLV_INDEX_NAME)).thenReturn(vlvIndexCfg);
return cfg;
}
private BackendIndexCfg indexCfg(SortedSet indexTypes)
{
final BackendIndexCfg cfg = mock(BackendIndexCfg.class);
when(cfg.dn()).thenReturn(DN.valueOf("ds-cfg-attribute=cn,cn=Index,ds-cfg-backend-id=" + BACKEND_ID
+ ",cn=Backends,cn=config"));
when(cfg.getIndexType()).thenReturn(indexTypes);
when(cfg.getAttribute()).thenReturn(cnType);
when(cfg.getIndexEntryLimit()).thenReturn(4000);
when(cfg.getSubstringLength()).thenReturn(6);
return cfg;
}
private BackendVLVIndexCfg vlvIndexCfg()
{
final BackendVLVIndexCfg cfg = mock(BackendVLVIndexCfg.class);
when(cfg.getName()).thenReturn(VLV_INDEX_NAME);
when(cfg.getBaseDN()).thenReturn(BASE_DN);
when(cfg.getScope()).thenReturn(Scope.WHOLE_SUBTREE);
when(cfg.getFilter()).thenReturn("(objectClass=*)");
when(cfg.getSortOrder()).thenReturn("+cn");
return cfg;
}
/** A backend over the storage of the given kind, which keeps hold of the configuration its entry container registered with. */
private static final class LeftoverBackend extends BackendImpl
{
private final StorageKind kind;
private RefusingStorage storage;
/** The configuration the entry container registers its listeners with. */
private PluggableBackendCfg configuredWith;
private BackendIndexCfg cnIndexCfg;
private BackendVLVIndexCfg vlvIndexCfg;
/** The trees of the indexes an earlier configuration named, which nothing names now. */
private Set leftoverIndexTrees;
private TreeName leftoverVlvTree;
private TreeName leftoverVlvCounterTree;
private LeftoverBackend(StorageKind kind)
{
this.kind = kind;
}
@Override
protected Storage configureStorage(PluggableBackendCfg cfg, ServerContext serverContext) throws ConfigException
{
storage = new RefusingStorage(kind.newStorage(cfg, serverContext));
return storage;
}
}
/** A failure no storage engine replays, unlike a conflict. */
private static final class RefusedWrite extends Exception
{
private static final long serialVersionUID = 1L;
RefusedWrite()
{
super("write refused by the test");
}
}
/**
* Delegates to the storage of the engine, and refuses the one write a case arms: neither engine
* can be made to fail a write on its own, and both are final. The refusal comes at one of two
* points - before the operation runs, which is what a write the engine cannot begin looks like
* to the caller, or once the operation has run and before it is committed, which is what a
* failure at commit looks like: the engine rolls the operation back, and whatever the operation
* noted on the way stands. Unarmed, it is the engine's storage.
*/
private static final class RefusingStorage implements Storage
{
private final Storage delegate;
/** Which write, counted over the life of this storage, is armed; zero for none. */
private int armedWrite;
private boolean afterItRan;
private int writes;
RefusingStorage(Storage delegate)
{
this.delegate = delegate;
}
/** Refuses the {@code nth} write asked for from now on, the next one being the first, before it runs. */
void refuseWrite(int nth)
{
armedWrite = writes + nth;
afterItRan = false;
}
/** Fails the {@code nth} write asked for from now on once its operation has run, before it is committed. */
void failWriteAfterItRan(int nth)
{
armedWrite = writes + nth;
afterItRan = true;
}
/** How many write operations this storage was asked for, refused or not. */
int writes()
{
return writes;
}
@Override
public void write(final WriteOperation operation) throws Exception
{
writes++;
if (writes != armedWrite)
{
delegate.write(operation);
return;
}
armedWrite = 0;
if (!afterItRan)
{
throw new RefusedWrite();
}
delegate.write(new WriteOperation()
{
@Override
public void run(WriteableTransaction txn) throws Exception
{
operation.run(txn);
throw new RefusedWrite();
}
});
}
@Override
public Importer startImport() throws ConfigException
{
return delegate.startImport();
}
@Override
public void open(AccessMode accessMode) throws Exception
{
delegate.open(accessMode);
}
@Override
public T read(ReadOperation readOperation) throws Exception
{
return delegate.read(readOperation);
}
@Override
public void removeStorageFiles()
{
delegate.removeStorageFiles();
}
@Override
public StorageStatus getStorageStatus()
{
return delegate.getStorageStatus();
}
@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);
}
@Override
public Set listTrees()
{
return delegate.listTrees();
}
@Override
public void close()
{
delegate.close();
}
}
}