/*
* 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.mock;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
import static org.opends.messages.BackendMessages.ERR_CONFIG_BACKEND_DATA_CHANGE_FAILED;
import static org.opends.messages.BackendMessages.ERR_CONFIG_INDEX_CHANGE_FAILED;
import static org.opends.messages.BackendMessages.ERR_CONFIG_INDEX_DELETE_FAILED;
import static org.opends.messages.BackendMessages.ERR_CONFIG_VLV_INDEX_DELETE_FAILED;
import static org.opends.server.util.CollectionUtils.newTreeSet;
import java.util.HashSet;
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.config.server.ConfigurationDeleteListener;
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.PDBBackendCfg;
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.AttributeIndex.MatchingRuleIndex;
import org.opends.server.backends.pluggable.spi.AccessMode;
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.types.BackupConfig;
import org.opends.server.types.BackupDirectory;
import org.opends.server.types.DirectoryException;
import org.opends.server.types.RestoreConfig;
import org.testng.annotations.AfterMethod;
import org.testng.annotations.BeforeClass;
import org.testng.annotations.Test;
/**
* Tests what a pluggable backend tells the operator when a {@link Storage#write(WriteOperation)}
* gives up on a configuration change. A bounded storage stops replaying a conflicting transaction
* and reports the failure, while the configuration entry the change came from has already been
* persisted; the in-memory state the four change paths keep around that write is then no longer
* what the storage holds. Every one of them has to say so - see OpenDJ issue #962.
*
* The bound is spent here by a storage decorator which runs the operation and then fails it, which
* is what the last attempt of a bounded retry loop does: the transaction is rolled back and the
* caller is handed the failure.
*/
@SuppressWarnings("javadoc")
@Test(groups = { "precommit", "pluggablebackend" }, sequential = true)
public class ConfigChangeGivesUpTest extends DirectoryServerTestCase
{
private static final String BACKEND_ID = "ConfigChangeGivesUpTest";
private static final DN BASE_DN = DN.valueOf("dc=b962,dc=com");
private static final String VLV_INDEX_NAME = "b962vlv";
private ServerContext serverContext;
private AttributeType cnType;
@BeforeClass
public void startServer() throws Exception
{
TestCaseUtils.startServer();
serverContext = TestCaseUtils.getServerContext();
cnType = serverContext.getSchema().getAttributeType("cn");
}
/**
* 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 trees of an index whose deletion the storage gave up on are left behind while the
* configuration entry naming them is gone, so the change has to ask for an administrative action
* and name the index, rather than hand back a bare stack trace.
*/
@Test
public void anIndexDeletionWhichGivesUpAsksForAnAdministrativeAction() throws Exception
{
final GivingUpBackend backend = openBackend();
try
{
final EntryContainer ec = backend.getRootContainer().getEntryContainer(BASE_DN);
final Set indexTrees = treesOf(ec.getAttributeIndex(cnType));
backend.storage.giveUpOnNextWrite();
final ConfigChangeResult ccr = indexDeleteListener(backend).applyConfigurationDelete(backend.cnIndexCfg);
assertThat(ccr.getResultCode()).isNotEqualTo(ResultCode.SUCCESS);
assertThat(ccr.adminActionRequired()).isTrue();
assertThat(ordinalsOf(ccr)).contains(ERR_CONFIG_INDEX_DELETE_FAILED.ordinal());
assertThat(ccr.getMessages().toString()).contains("cn").contains(BASE_DN.toString());
assertThat(backend.getRootContainer().getStorage().listTrees())
.as("the trees the failed deletion left behind").containsAll(indexTrees);
}
finally
{
backend.finalizeBackend();
}
}
/**
* The same for a VLV index, whose data is held by two trees: the index itself and the counter
* which goes with it.
*/
@Test
public void aVlvIndexDeletionWhichGivesUpAsksForAnAdministrativeAction() throws Exception
{
final GivingUpBackend backend = openBackend();
try
{
final EntryContainer ec = backend.getRootContainer().getEntryContainer(BASE_DN);
final TreeName vlvTree = new TreeName(ec.getTreePrefix(), "vlv." + VLV_INDEX_NAME);
final TreeName vlvCounterTree = new TreeName(ec.getTreePrefix(), "counter.vlv." + VLV_INDEX_NAME);
backend.storage.giveUpOnNextWrite();
final ConfigChangeResult ccr = vlvIndexDeleteListener(backend).applyConfigurationDelete(backend.vlvIndexCfg);
assertThat(ccr.getResultCode()).isNotEqualTo(ResultCode.SUCCESS);
assertThat(ccr.adminActionRequired()).isTrue();
assertThat(ordinalsOf(ccr)).contains(ERR_CONFIG_VLV_INDEX_DELETE_FAILED.ordinal());
assertThat(ccr.getMessages().toString()).contains(VLV_INDEX_NAME).contains(BASE_DN.toString());
assertThat(backend.getRootContainer().getStorage().listTrees())
.as("the trees the failed deletion left behind").contains(vlvTree, vlvCounterTree);
}
finally
{
backend.finalizeBackend();
}
}
/**
* An index change is applied by three writes, and the second one deletes the trees of the
* indexes the new configuration no longer asks for. When it gives up, those trees are still
* there, so the index has to go on naming them: an index taken out of the map while its trees
* survive is an index nothing maintains and nothing deletes.
*/
@Test
public void anIndexChangeWhichGivesUpDeletingGoesOnNamingWhatItCouldNotDelete() throws Exception
{
final GivingUpBackend backend = openBackend();
try
{
final EntryContainer ec = backend.getRootContainer().getEntryContainer(BASE_DN);
final AttributeIndex index = ec.getAttributeIndex(cnType);
final Set indexIdsBefore = new HashSet<>(index.getNameToIndexes().keySet());
final Set treesBefore = treesOf(index);
// Dropping the substring index is what gives the second write something to delete.
backend.storage.giveUpOnWrite(2);
final ConfigChangeResult ccr =
index.applyConfigurationChange(indexCfg(newTreeSet(IndexType.EQUALITY), 4000));
assertThat(ccr.getResultCode()).isNotEqualTo(ResultCode.SUCCESS);
assertThat(ccr.adminActionRequired()).isTrue();
assertThat(ordinalsOf(ccr)).contains(ERR_CONFIG_INDEX_CHANGE_FAILED.ordinal());
assertThat(ccr.getMessages().toString()).contains("cn").contains(BASE_DN.toString());
assertThat(index.getNameToIndexes().keySet())
.as("the indexes whose trees the failed deletion left behind").isEqualTo(indexIdsBefore);
assertThat(backend.getRootContainer().getStorage().listTrees()).containsAll(treesBefore);
}
finally
{
backend.finalizeBackend();
}
}
/**
* The third write is what untrusts the indexes which stay when the new configuration raises
* their entry limit, and the limit itself is applied to them only once it has committed. When it
* gives up, the configuration must not be published either, or the index claims settings which
* were never applied to it. What it declares is read here through the index types it names, the
* only reader of that field this test can reach.
*/
@Test
public void anIndexChangeWhichGivesUpUpdatingDoesNotPublishWhatItCouldNotApply() throws Exception
{
final GivingUpBackend backend = openBackend();
try
{
final EntryContainer ec = backend.getRootContainer().getEntryContainer(BASE_DN);
final AttributeIndex index = ec.getAttributeIndex(cnType);
final Set indexIdsBefore = new HashSet<>(index.getNameToIndexes().keySet());
// A presence index is added and the entry limit of the ones which stay is raised, so that
// all three writes have work to do and the third is the one which gives up: a lowered limit
// untrusts nothing and opens no third write.
backend.storage.giveUpOnWrite(3);
final ConfigChangeResult ccr = index.applyConfigurationChange(
indexCfg(newTreeSet(IndexType.EQUALITY, IndexType.SUBSTRING, IndexType.PRESENCE), 5000));
assertThat(ccr.getResultCode()).isNotEqualTo(ResultCode.SUCCESS);
assertThat(ccr.adminActionRequired()).isTrue();
assertThat(ordinalsOf(ccr)).contains(ERR_CONFIG_INDEX_CHANGE_FAILED.ordinal());
assertThat(index.getNameToIndexes().keySet())
.as("the index the writes which did commit opened")
.containsAll(indexIdsBefore).hasSize(indexIdsBefore.size() + 1);
assertThat(index.isIndexed(IndexType.PRESENCE))
.as("a configuration the write which gave up never applied").isFalse();
}
finally
{
backend.finalizeBackend();
}
}
/**
* The compression, encoding and encryption settings of a backend are applied to the entries and
* to every index of that backend, and nothing else can apply half of them. A change which cannot
* be applied has to ask for an administrative action and leave what it did not apply alone.
*/
@Test
public void aBackendDataChangeWhichCannotBeAppliedAsksForAnAdministrativeAction() throws Exception
{
final GivingUpBackend backend = openBackend();
try
{
final EntryContainer ec = backend.getRootContainer().getEntryContainer(BASE_DN);
final PDBBackendCfg newCfg = backendCfg(backend.cnIndexCfg, backend.vlvIndexCfg);
when(newCfg.isConfidentialityEnabled()).thenReturn(true);
when(newCfg.isEntriesCompressed()).thenThrow(new IllegalStateException("no settings to be had"));
final ConfigChangeResult ccr = ec.applyConfigurationChange(newCfg);
assertThat(ccr.getResultCode()).isNotEqualTo(ResultCode.SUCCESS);
assertThat(ccr.adminActionRequired()).isTrue();
assertThat(ordinalsOf(ccr)).contains(ERR_CONFIG_BACKEND_DATA_CHANGE_FAILED.ordinal());
assertThat(ccr.getMessages().toString()).contains(BASE_DN.toString());
assertThat(ec.isConfidentialityEnabled())
.as("a configuration none of which was applied").isFalse();
}
finally
{
backend.finalizeBackend();
}
}
/**
* Applying those settings writes nothing: it hands the entries and the indexes new parameters to
* encode with from now on, and neither of those is a record in a tree. A transaction opened for
* it is one a storage can give up on, and giving up half way is what leaves the entries and the
* indexes encoded under settings which no longer agree.
*/
@Test
public void aBackendDataChangeOpensNoTransaction() throws Exception
{
final GivingUpBackend backend = openBackend();
try
{
final EntryContainer ec = backend.getRootContainer().getEntryContainer(BASE_DN);
final PDBBackendCfg newCfg = backendCfg(backend.cnIndexCfg, backend.vlvIndexCfg);
when(newCfg.isEntriesCompressed()).thenReturn(true);
final int writesBefore = backend.storage.writes();
final ConfigChangeResult ccr = ec.applyConfigurationChange(newCfg);
assertThat(ccr.getResultCode()).isEqualTo(ResultCode.SUCCESS);
assertThat(ccr.getMessages()).isEmpty();
assertThat(ccr.adminActionRequired()).isFalse();
assertThat(backend.storage.writes())
.as("a transaction opened for work none of which is transactional").isEqualTo(writesBefore);
}
finally
{
backend.finalizeBackend();
}
}
/** A deletion which does delete the trees has nothing to tell the operator. */
@Test
public void anIndexDeletionWhichSucceedsAsksForNothing() throws Exception
{
final GivingUpBackend backend = openBackend();
try
{
final EntryContainer ec = backend.getRootContainer().getEntryContainer(BASE_DN);
final Set indexTrees = treesOf(ec.getAttributeIndex(cnType));
final ConfigChangeResult ccr = indexDeleteListener(backend).applyConfigurationDelete(backend.cnIndexCfg);
assertThat(ccr.getResultCode()).isEqualTo(ResultCode.SUCCESS);
assertThat(ccr.getMessages()).isEmpty();
assertThat(ccr.adminActionRequired()).isFalse();
assertThat(backend.getRootContainer().getStorage().listTrees())
.doesNotContain(indexTrees.toArray(new TreeName[0]));
}
finally
{
backend.finalizeBackend();
}
}
/** A VLV deletion which does delete the trees has nothing to tell the operator either. */
@Test
public void aVlvIndexDeletionWhichSucceedsAsksForNothing() throws Exception
{
final GivingUpBackend backend = openBackend();
try
{
final EntryContainer ec = backend.getRootContainer().getEntryContainer(BASE_DN);
final TreeName vlvTree = new TreeName(ec.getTreePrefix(), "vlv." + VLV_INDEX_NAME);
final TreeName vlvCounterTree = new TreeName(ec.getTreePrefix(), "counter.vlv." + VLV_INDEX_NAME);
final ConfigChangeResult ccr = vlvIndexDeleteListener(backend).applyConfigurationDelete(backend.vlvIndexCfg);
assertThat(ccr.getResultCode()).isEqualTo(ResultCode.SUCCESS);
assertThat(ccr.getMessages()).isEmpty();
assertThat(ccr.adminActionRequired()).isFalse();
assertThat(backend.getRootContainer().getStorage().listTrees())
.as("the trees the deletion removed").doesNotContain(vlvTree, vlvCounterTree);
}
finally
{
backend.finalizeBackend();
}
}
/**
* A change which does apply publishes every part of it, each after the write which applied that
* part: the map of indexes after the deletion, the indexing options with it, and the
* configuration after the update. The administrative action this one asks for is the other kind -
* an index which has just been added has yet to be rebuilt - and not a divergence.
*/
@Test
public void anIndexChangeWhichSucceedsPublishesEveryPartOfIt() throws Exception
{
final GivingUpBackend backend = openBackend();
try
{
final EntryContainer ec = backend.getRootContainer().getEntryContainer(BASE_DN);
final AttributeIndex index = ec.getAttributeIndex(cnType);
final Set indexIdsBefore = new HashSet<>(index.getNameToIndexes().keySet());
final ConfigChangeResult ccr = index.applyConfigurationChange(
indexCfg(newTreeSet(IndexType.EQUALITY, IndexType.PRESENCE), 4000, 3));
assertThat(ccr.getResultCode()).isEqualTo(ResultCode.SUCCESS);
assertThat(ordinalsOf(ccr)).doesNotContain(ERR_CONFIG_INDEX_CHANGE_FAILED.ordinal());
assertThat(index.isIndexed(IndexType.PRESENCE))
.as("an index type this change declares").isTrue();
assertThat(index.isIndexed(IndexType.SUBSTRING))
.as("an index type this change no longer declares").isFalse();
assertThat(index.getIndexingOptions().substringKeySize())
.as("the indexing options this change declares").isEqualTo(3);
assertThat(index.getNameToIndexes().keySet())
.as("the indexes this change opened, and none of the ones it deleted")
.isNotEqualTo(indexIdsBefore);
assertThat(backend.getRootContainer().getStorage().listTrees())
.as("every tree the published map names").containsAll(treesOf(index));
}
finally
{
backend.finalizeBackend();
}
}
/**
* Pins what {@code ERR_CONFIG_INDEX_DELETE_FAILED} tells the operator: an index created again for
* the same attribute adopts the trees a failed deletion left behind, and is trusted over their
* stale content without a word about rebuilding it. This is not the behaviour being asked for
* here - it is the behaviour that message describes (#990). When it is fixed, this test fails and
* the message has to be rewritten, rather than quietly becoming untrue. Its VLV counterpart
* says the same of {@code VLVIndex.afterOpen}, which nothing here holds.
*/
@Test
public void anIndexCreatedAgainAdoptsTheTreesAFailedDeletionLeftBehind() throws Exception
{
final GivingUpBackend backend = openBackend();
try
{
// An index of an empty backend is trusted whatever its trees hold, so this needs an entry.
backend.addEntry(
TestCaseUtils.makeEntry("dn: " + BASE_DN, "objectClass: top", "objectClass: domain", "dc: b962"),
mock(AddOperation.class));
final EntryContainer ec = backend.getRootContainer().getEntryContainer(BASE_DN);
final Set indexTrees = treesOf(ec.getAttributeIndex(cnType));
backend.storage.giveUpOnNextWrite();
indexDeleteListener(backend).applyConfigurationDelete(backend.cnIndexCfg);
final ConfigChangeResult ccr = indexAddListener(backend).applyConfigurationAdd(backend.cnIndexCfg);
assertThat(ccr.getResultCode()).isEqualTo(ResultCode.SUCCESS);
assertThat(treesOf(ec.getAttributeIndex(cnType)))
.as("the trees the index created again holds").isEqualTo(indexTrees);
assertThat(ec.getAttributeIndex(cnType).isTrusted())
.as("an index trusted over the content of the trees it adopted").isTrue();
assertThat(ccr.getMessages())
.as("nothing tells the operator this index has to be rebuilt").isEmpty();
}
finally
{
backend.finalizeBackend();
}
}
/** The messages a change result carries, by identity rather than by their formatted text. */
private static Set ordinalsOf(ConfigChangeResult ccr)
{
final Set ordinals = new HashSet<>();
for (LocalizableMessage message : ccr.getMessages())
{
ordinals.add(message.ordinal());
}
return ordinals;
}
private static Set treesOf(AttributeIndex index)
{
final Set names = new HashSet<>();
for (MatchingRuleIndex matchingRuleIndex : index.getNameToIndexes().values())
{
names.add(matchingRuleIndex.getName());
}
return names;
}
@SuppressWarnings({ "unchecked", "rawtypes" })
private static ConfigurationDeleteListener indexDeleteListener(GivingUpBackend backend)
throws ConfigException
{
final ArgumentCaptor captor =
ArgumentCaptor.forClass(ConfigurationDeleteListener.class);
verify(backend.configuredWith).addBackendIndexDeleteListener(captor.capture());
return captor.getValue();
}
@SuppressWarnings({ "unchecked", "rawtypes" })
private static ConfigurationDeleteListener vlvIndexDeleteListener(GivingUpBackend backend)
throws ConfigException
{
final ArgumentCaptor captor =
ArgumentCaptor.forClass(ConfigurationDeleteListener.class);
verify(backend.configuredWith).addBackendVLVIndexDeleteListener(captor.capture());
return captor.getValue();
}
@SuppressWarnings({ "unchecked", "rawtypes" })
private static ConfigurationAddListener indexAddListener(GivingUpBackend backend)
throws ConfigException
{
final ArgumentCaptor captor = ArgumentCaptor.forClass(ConfigurationAddListener.class);
verify(backend.configuredWith).addBackendIndexAddListener(captor.capture());
return captor.getValue();
}
private GivingUpBackend openBackend() throws Exception
{
final GivingUpBackend backend = new GivingUpBackend();
backend.setBackendID(BACKEND_ID);
backend.cnIndexCfg = indexCfg(newTreeSet(IndexType.EQUALITY, IndexType.SUBSTRING), 4000);
backend.vlvIndexCfg = vlvIndexCfg();
backend.configuredWith = backendCfg(backend.cnIndexCfg, backend.vlvIndexCfg);
backend.configureBackend(backend.configuredWith, serverContext);
// 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.
try
{
if (backend.getRootContainer() != null)
{
backend.finalizeBackend();
}
else
{
backend.storage.close();
}
}
catch (Exception cleanupFailure)
{
e.addSuppressed(cleanupFailure);
}
throw e;
}
return backend;
}
private PDBBackendCfg backendCfg(BackendIndexCfg cnIndexCfg, BackendVLVIndexCfg vlvIndexCfg) 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(newTreeSet(BASE_DN));
when(cfg.listBackendIndexes()).thenReturn(new String[] { "cn" });
when(cfg.getBackendIndex("cn")).thenReturn(cnIndexCfg);
when(cfg.listBackendVLVIndexes()).thenReturn(new String[] { VLV_INDEX_NAME });
when(cfg.getBackendVLVIndex(VLV_INDEX_NAME)).thenReturn(vlvIndexCfg);
return cfg;
}
private BackendIndexCfg indexCfg(SortedSet indexTypes, int indexEntryLimit)
{
return indexCfg(indexTypes, indexEntryLimit, 6);
}
private BackendIndexCfg indexCfg(SortedSet indexTypes, int indexEntryLimit, int substringLength)
{
final BackendIndexCfg cfg = mock(BackendIndexCfg.class);
when(cfg.getIndexType()).thenReturn(indexTypes);
when(cfg.getAttribute()).thenReturn(cnType);
when(cfg.getIndexEntryLimit()).thenReturn(indexEntryLimit);
when(cfg.getSubstringLength()).thenReturn(substringLength);
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 whose storage gives up on a write instead of replaying it until it succeeds. */
private static final class GivingUpBackend extends BackendImpl
{
private GivingUpStorage storage;
/** The configuration the entry container registers its listeners with. */
private PDBBackendCfg configuredWith;
private BackendIndexCfg cnIndexCfg;
private BackendVLVIndexCfg vlvIndexCfg;
@Override
protected Storage configureStorage(PDBBackendCfg cfg, ServerContext serverContext) throws ConfigException
{
storage = new GivingUpStorage(new PDBStorage(cfg, serverContext));
return storage;
}
}
/** What a storage which has spent its replay bound hands its caller. */
private static final class StorageGaveUp extends Exception
{
private static final long serialVersionUID = 1L;
}
/**
* Decorates a {@link Storage} so that a chosen write gives up rather than succeeding. The
* operation is run inside the transaction the delegate opens and the failure is raised from
* within it, so that the transaction is rolled back and the caller is handed the failure - which
* is what the last attempt of a bounded retry loop leaves behind.
*/
private static final class GivingUpStorage implements Storage
{
private final Storage delegate;
/**
* Counted over every write this storage is asked for, by whoever asks: the backend it belongs
* to is private to one test at a time and this module runs its tests one at a time, so a write
* armed here is the write the test is about. A backend which wrote on a thread of its own would
* break that, and would have to arm the operation rather than the count.
*/
private int writes;
/** Which write, counted over the life of this storage, gives up; zero when none does. */
private int givesUpAt;
GivingUpStorage(Storage delegate)
{
this.delegate = delegate;
}
void giveUpOnNextWrite()
{
giveUpOnWrite(1);
}
/** Gives up on the {@code nth} write asked for from now on, the next one being the first. */
void giveUpOnWrite(int nth)
{
givesUpAt = writes + nth;
}
/** How many write operations this storage was asked for. */
int writes()
{
return writes;
}
@Override
public void write(final WriteOperation writeOperation) throws Exception
{
writes++;
if (writes != givesUpAt)
{
delegate.write(writeOperation);
return;
}
givesUpAt = 0;
delegate.write(new WriteOperation()
{
@Override
public void run(WriteableTransaction txn) throws Exception
{
writeOperation.run(txn);
throw new StorageGaveUp();
}
});
}
@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();
}
}
}