| New file |
| | |
| | | /* |
| | | * The contents of this file are subject to the terms of the Common Development and |
| | | * Distribution License (the License). You may not use this file except in compliance with the |
| | | * License. |
| | | * |
| | | * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the |
| | | * specific language governing permission and limitations under the License. |
| | | * |
| | | * When distributing Covered Software, include this CDDL Header Notice in each file and include |
| | | * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL |
| | | * Header, with the fields enclosed by brackets [] replaced by your own identifying |
| | | * information: "Portions copyright [year] [name of copyright owner]". |
| | | * |
| | | * Copyright 2026 3A Systems, LLC. |
| | | */ |
| | | package org.opends.server.backends.pluggable; |
| | | |
| | | import static org.assertj.core.api.Assertions.assertThat; |
| | | import static org.forgerock.opendj.config.ConfigurationMock.mockCfg; |
| | | import static org.mockito.Mockito.any; |
| | | import static org.mockito.Mockito.atLeast; |
| | | import static org.mockito.Mockito.mock; |
| | | import static org.mockito.Mockito.times; |
| | | 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 org.forgerock.opendj.config.server.ConfigException; |
| | | import org.forgerock.opendj.config.server.ConfigurationChangeListener; |
| | | 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.server.BackendIndexCfg; |
| | | 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.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.RestoreConfig; |
| | | import org.testng.annotations.AfterMethod; |
| | | import org.testng.annotations.BeforeClass; |
| | | import org.testng.annotations.Test; |
| | | |
| | | import com.persistit.exception.RollbackException; |
| | | |
| | | /** |
| | | * Tests that {@link RootContainer#open(AccessMode)} survives a replay of its {@link WriteOperation}. |
| | | * {@link Storage#write(WriteOperation)} may replay the operation after a transaction conflict, and |
| | | * {@code RootContainer.open} opens and registers the entry container of every base DN inside a |
| | | * single one of them, so every side effect that write performs must either be transactional or be |
| | | * idempotent - see OpenDJ issue #896. |
| | | * <p> |
| | | * The conflict is raised from inside the operation as the {@link RollbackException} PersistIt itself |
| | | * raises, so that the replay is driven by {@code PDBStorage.write}'s own retry loop rather than by a |
| | | * second call to it, as {@link ReplayedConfigChangeTest} does for the sibling path. |
| | | */ |
| | | @SuppressWarnings("javadoc") |
| | | @Test(groups = { "precommit", "pluggablebackend" }, sequential = true) |
| | | public class ReplayedOpenTest extends DirectoryServerTestCase |
| | | { |
| | | private static final String BACKEND_ID = "ReplayedOpenTest"; |
| | | /** Opened and registered first, since the base DNs are opened in the order of the sorted set. */ |
| | | private static final DN FIRST = DN.valueOf("dc=b896a,dc=com"); |
| | | /** Opened while the first one is already registered, which is where a conflict reaches the bug. */ |
| | | private static final DN SECOND = DN.valueOf("dc=b896b,dc=com"); |
| | | |
| | | private ServerContext serverContext; |
| | | private AttributeType cnType; |
| | | /** The configuration of the cn index, which the attribute index of every base DN registers with. */ |
| | | private BackendIndexCfg indexCfg; |
| | | |
| | | @BeforeClass |
| | | public void startServer() throws Exception |
| | | { |
| | | TestCaseUtils.startServer(); |
| | | serverContext = TestCaseUtils.getServerContext(); |
| | | cnType = serverContext.getSchema().getAttributeType("cn"); |
| | | } |
| | | |
| | | /** |
| | | * These tests open a backend which is designed to fail, and a failing one can leave a base DN |
| | | * behind in the server wide registry, where it would outlive the test and break the next one. |
| | | */ |
| | | @AfterMethod |
| | | public void deregisterLeftoverBaseDNs() |
| | | { |
| | | for (DN baseDN : new DN[] { FIRST, SECOND }) |
| | | { |
| | | try |
| | | { |
| | | serverContext.getBackendConfigManager().deregisterBaseDN(baseDN); |
| | | } |
| | | catch (Exception alreadyGone) |
| | | { |
| | | // Which is what the test should have left behind. |
| | | } |
| | | } |
| | | } |
| | | |
| | | /** |
| | | * The case the report is written from: the conflict is raised while the second base DN is being |
| | | * opened, so the replay meets the first one already registered by the attempt it replaces. |
| | | */ |
| | | @Test |
| | | public void openIsReplayableWhenTheTransactionConflictsWhileTheSecondBaseDNIsOpened() throws Exception |
| | | { |
| | | final ReplayingBackend backend = newBackend(newTreeSet(FIRST, SECOND)); |
| | | boolean opened = false; |
| | | try |
| | | { |
| | | backend.storage.conflictAtTreesOf(SECOND, 1); |
| | | backend.openBackend(); |
| | | opened = true; |
| | | |
| | | assertThat(backend.storage.attempts()).isEqualTo(2); |
| | | final RootContainer rootContainer = backend.getRootContainer(); |
| | | assertThat(rootContainer.getBaseDNs()).containsOnly(FIRST, SECOND); |
| | | // Each base DN is answered with a container of its own, rather than with the one an ancestor |
| | | // registered, and the trees of both are there to be read. |
| | | for (DN baseDN : new DN[] { FIRST, SECOND }) |
| | | { |
| | | final EntryContainer ec = rootContainer.getEntryContainer(baseDN); |
| | | assertThat((Object) ec.getBaseDN()).isEqualTo(baseDN); |
| | | assertThat(rootContainer.getStorage().listTrees()).containsAll(treesOf(ec)); |
| | | } |
| | | // What the attempt which was replaced opened is off the backend configuration, and the live |
| | | // pair is on it: SECOND of that attempt was given up by its own close(), raised from |
| | | // EntryContainer.open's own catch, and FIRST by the replay, which begins by unregistering and |
| | | // closing what the attempt it replaces had registered. Counting the removals would say neither |
| | | // which containers were given back nor that the adds balance: one of the two takes off a |
| | | // listener SECOND had never added, the conflict being raised at its id2entry, before |
| | | // EntryContainer.open registers anything. |
| | | assertThat(entryContainersRegisteredOn(backend.configuredWith)) |
| | | .containsOnly(rootContainer.getEntryContainer(FIRST), rootContainer.getEntryContainer(SECOND)); |
| | | // The same balance one level down: a container given back without the listener each of its |
| | | // indexes registered would satisfy the assertion above. |
| | | assertThat(indexesRegisteredOn(indexCfg)) |
| | | .containsOnly(rootContainer.getEntryContainer(FIRST).getAttributeIndex(cnType), |
| | | rootContainer.getEntryContainer(SECOND).getAttributeIndex(cnType)); |
| | | } |
| | | finally |
| | | { |
| | | close(backend, opened); |
| | | } |
| | | } |
| | | |
| | | /** |
| | | * A conflict raised once the operation has run to completion, before the commit, replays an |
| | | * operation which ran to completion, so every base DN of the backend is registered by the |
| | | * attempt the replay replaces. One base DN would be enough to reach that, and |
| | | * {@code FailedBackendOpenTest.aReplayedOpenLeavesOneSetOfEntryContainers} drives the same |
| | | * conflict with one; two are used here so that the case differs from the one above only in where |
| | | * the conflict was raised, and so that the give-back has more than one container to walk. |
| | | */ |
| | | @Test |
| | | public void openIsReplayableWhenTheTransactionConflictsAtCommitTime() throws Exception |
| | | { |
| | | final ReplayingBackend backend = newBackend(newTreeSet(FIRST, SECOND)); |
| | | boolean opened = false; |
| | | try |
| | | { |
| | | backend.storage.conflictAtCommit(1); |
| | | backend.openBackend(); |
| | | opened = true; |
| | | |
| | | assertThat(backend.storage.attempts()).isEqualTo(2); |
| | | final RootContainer rootContainer = backend.getRootContainer(); |
| | | assertThat(rootContainer.getBaseDNs()).containsOnly(FIRST, SECOND); |
| | | // The two entry containers the rolled back attempt opened registered five configuration |
| | | // listeners each, and one more per index, which only their close() takes back, so the replay |
| | | // has to give both up before it opens another pair: what is left of the two attempts is the |
| | | // live pair alone. |
| | | assertThat(entryContainersRegisteredOn(backend.configuredWith)) |
| | | .containsOnly(rootContainer.getEntryContainer(FIRST), rootContainer.getEntryContainer(SECOND)); |
| | | assertThat(indexesRegisteredOn(indexCfg)) |
| | | .containsOnly(rootContainer.getEntryContainer(FIRST).getAttributeIndex(cnType), |
| | | rootContainer.getEntryContainer(SECOND).getAttributeIndex(cnType)); |
| | | } |
| | | finally |
| | | { |
| | | close(backend, opened); |
| | | } |
| | | } |
| | | |
| | | /** |
| | | * A failure the storage engine does not replay ends the open, and {@code RootContainer}'s own |
| | | * give-back is the only one there is: {@code BackendImpl.newRootContainer} drops the root |
| | | * container without closing it, so what the attempt had opened by then - both entry containers, |
| | | * the root container's own listener and the storage - is handed back by |
| | | * {@code RootContainer.giveUpAfterFailedOpen} or not at all. |
| | | */ |
| | | @Test |
| | | public void anOpenWhichIsNotReplayedGivesUpTheEntryContainersItOpened() throws Exception |
| | | { |
| | | final ReplayingBackend backend = newBackend(newTreeSet(FIRST, SECOND)); |
| | | try |
| | | { |
| | | backend.storage.failWithoutReplay(); |
| | | |
| | | try |
| | | { |
| | | backend.openBackend(); |
| | | throw new AssertionError("the open was expected to fail"); |
| | | } |
| | | catch (Exception expected) |
| | | { |
| | | // The failure itself is the caller's business; what it left behind is this test's. |
| | | } |
| | | |
| | | // The failure is the one the engines do not replay, so the open ends at the first attempt: |
| | | // a replay would open a second pair of containers over the first. |
| | | assertThat(backend.storage.attempts()).isEqualTo(1); |
| | | // The two entry containers the attempt opened, and the root container's own listener, which |
| | | // goes with them since #993: nothing closes a root container which did not open. |
| | | verify(backend.configuredWith, times(3)).removePluggableChangeListener(any()); |
| | | } |
| | | finally |
| | | { |
| | | close(backend, false); |
| | | } |
| | | } |
| | | |
| | | private static Set<TreeName> treesOf(EntryContainer ec) |
| | | { |
| | | final Set<TreeName> names = new HashSet<>(); |
| | | for (Tree tree : ec.listTrees()) |
| | | { |
| | | names.add(tree.getName()); |
| | | } |
| | | return names; |
| | | } |
| | | |
| | | /** |
| | | * Closes a backend which opened; for one which did not, closes its storage a second time, in case |
| | | * the give-back of the failed open did not reach it - a no-op under {@code PDBStorage.close}'s |
| | | * {@code db != null} guard when it did, and a volume left open fails every following test with a |
| | | * {@code StorageInUseException} rather than with what actually broke. |
| | | */ |
| | | private static void close(ReplayingBackend backend, boolean opened) |
| | | { |
| | | if (opened) |
| | | { |
| | | backend.finalizeBackend(); |
| | | } |
| | | else |
| | | { |
| | | backend.storage.close(); |
| | | } |
| | | } |
| | | |
| | | /** |
| | | * The entry containers still registered as listeners of the backend configuration, by identity. |
| | | * <p> |
| | | * A replayed open registers a container per base DN twice and gives the first set back, so what |
| | | * the case is about is which containers are left rather than how many calls were made: a removal |
| | | * says nothing on its own - an entry container whose open failed before it had registered |
| | | * anything is taken off all the same, by the {@code close()} of {@code EntryContainer.open}'s own |
| | | * catch. |
| | | */ |
| | | private static List<EntryContainer> entryContainersRegisteredOn(PluggableBackendCfg cfg) |
| | | { |
| | | final ArgumentCaptor<ConfigurationChangeListener<PluggableBackendCfg>> added = |
| | | captorFor(ConfigurationChangeListener.class); |
| | | verify(cfg, atLeast(0)).addPluggableChangeListener(added.capture()); |
| | | final ArgumentCaptor<ConfigurationChangeListener<PluggableBackendCfg>> removed = |
| | | captorFor(ConfigurationChangeListener.class); |
| | | verify(cfg, atLeast(0)).removePluggableChangeListener(removed.capture()); |
| | | return balanceOf(EntryContainer.class, added.getAllValues(), removed.getAllValues()); |
| | | } |
| | | |
| | | /** The attribute indexes still registered as listeners of an index configuration, by identity. */ |
| | | private static List<AttributeIndex> indexesRegisteredOn(BackendIndexCfg cfg) |
| | | { |
| | | final ArgumentCaptor<ConfigurationChangeListener<BackendIndexCfg>> added = |
| | | captorFor(ConfigurationChangeListener.class); |
| | | verify(cfg, atLeast(0)).addChangeListener(added.capture()); |
| | | final ArgumentCaptor<ConfigurationChangeListener<BackendIndexCfg>> removed = |
| | | captorFor(ConfigurationChangeListener.class); |
| | | verify(cfg, atLeast(0)).removeChangeListener(removed.capture()); |
| | | return balanceOf(AttributeIndex.class, added.getAllValues(), removed.getAllValues()); |
| | | } |
| | | |
| | | /** |
| | | * What was added to a configuration and not taken off it again, of one kind of listener: the root |
| | | * container and the two configuration managers of every entry container listen to the backend |
| | | * configuration as well, and neither is what these cases are about. Identity, since none of these |
| | | * classes overrides {@code equals()}. |
| | | */ |
| | | private static <T> List<T> balanceOf(Class<T> kind, List<?> added, List<?> removed) |
| | | { |
| | | final List<T> registered = new ArrayList<>(); |
| | | for (Object listener : added) |
| | | { |
| | | if (kind.isInstance(listener)) |
| | | { |
| | | registered.add(kind.cast(listener)); |
| | | } |
| | | } |
| | | for (Object listener : removed) |
| | | { |
| | | if (kind.isInstance(listener)) |
| | | { |
| | | registered.remove(kind.cast(listener)); |
| | | } |
| | | } |
| | | return registered; |
| | | } |
| | | |
| | | @SuppressWarnings({ "unchecked", "rawtypes" }) |
| | | private static <T> ArgumentCaptor<T> captorFor(Class<?> listenerClass) |
| | | { |
| | | return (ArgumentCaptor<T>) ArgumentCaptor.forClass((Class) listenerClass); |
| | | } |
| | | |
| | | private ReplayingBackend newBackend(SortedSet<DN> baseDNs) throws Exception |
| | | { |
| | | final ReplayingBackend backend = new ReplayingBackend(); |
| | | backend.setBackendID(BACKEND_ID); |
| | | backend.configuredWith = backendCfg(baseDNs); |
| | | backend.configureBackend(backend.configuredWith, serverContext); |
| | | // Start from a pristine on-disk state so that a previous run cannot mask the defect. |
| | | backend.storage.removeStorageFiles(); |
| | | return backend; |
| | | } |
| | | |
| | | 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[0]); |
| | | |
| | | 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); |
| | | return cfg; |
| | | } |
| | | |
| | | /** A backend whose storage makes the next write operation conflict, and so be replayed. */ |
| | | private static final class ReplayingBackend extends BackendImpl<PDBBackendCfg> |
| | | { |
| | | private ReplayingStorage storage; |
| | | /** The configuration the entry containers register their listeners with. */ |
| | | private PDBBackendCfg configuredWith; |
| | | |
| | | @Override |
| | | protected Storage configureStorage(PDBBackendCfg cfg, ServerContext serverContext) throws ConfigException |
| | | { |
| | | storage = new ReplayingStorage(new PDBStorage(cfg, serverContext)); |
| | | return storage; |
| | | } |
| | | } |
| | | |
| | | /** A failure which no storage engine replays, unlike {@link RollbackException}. */ |
| | | private static final class UnreplayableFailure extends Exception |
| | | { |
| | | private static final long serialVersionUID = 1L; |
| | | } |
| | | |
| | | /** |
| | | * Decorates a {@link Storage} so that the next {@link Storage#write(WriteOperation)} conflicts a |
| | | * given number of times before it is let through. The conflict is raised from within the single |
| | | * {@code write} the delegate is asked for, so the delegate's own retry loop performs the replay. |
| | | */ |
| | | private static final class ReplayingStorage implements Storage |
| | | { |
| | | /** Where the conflict is raised, which decides how much of the operation has run. */ |
| | | private enum ConflictPoint |
| | | { |
| | | /** As the first tree of a given base DN is opened, with the base DNs before it registered. */ |
| | | TREES_OF_BASE_DN, |
| | | /** Once the operation has run to completion, before the commit. */ |
| | | COMMIT, |
| | | /** Once the operation has run to completion, as a failure which is not replayed at all. */ |
| | | NO_REPLAY |
| | | } |
| | | |
| | | private final Storage delegate; |
| | | private ConflictPoint conflictPoint; |
| | | private String conflictingPrefix; |
| | | private int conflictsLeft; |
| | | private int attempts; |
| | | |
| | | ReplayingStorage(Storage delegate) |
| | | { |
| | | this.delegate = delegate; |
| | | } |
| | | |
| | | void conflictAtTreesOf(DN baseDN, int conflicts) |
| | | { |
| | | conflictingPrefix = baseDN.toNormalizedUrlSafeString(); |
| | | arm(ConflictPoint.TREES_OF_BASE_DN, conflicts); |
| | | } |
| | | |
| | | void conflictAtCommit(int conflicts) |
| | | { |
| | | arm(ConflictPoint.COMMIT, conflicts); |
| | | } |
| | | |
| | | void failWithoutReplay() |
| | | { |
| | | arm(ConflictPoint.NO_REPLAY, 1); |
| | | } |
| | | |
| | | private void arm(ConflictPoint where, int conflicts) |
| | | { |
| | | conflictPoint = where; |
| | | conflictsLeft = conflicts; |
| | | attempts = 0; |
| | | } |
| | | |
| | | /** How many times the armed operation was run, the first attempt included. */ |
| | | int attempts() |
| | | { |
| | | return attempts; |
| | | } |
| | | |
| | | @Override |
| | | public void write(final WriteOperation writeOperation) throws Exception |
| | | { |
| | | final ConflictPoint armed = conflictPoint; |
| | | if (armed == null) |
| | | { |
| | | delegate.write(writeOperation); |
| | | return; |
| | | } |
| | | conflictPoint = null; |
| | | // A single call, so that the replay is the delegate's own and keeps whatever the delegate |
| | | // holds for the duration of a write, rather than starting afresh as a second call would. |
| | | delegate.write(new WriteOperation() |
| | | { |
| | | @Override |
| | | public void run(WriteableTransaction txn) throws Exception |
| | | { |
| | | attempts++; |
| | | if (conflictsLeft-- <= 0) |
| | | { |
| | | writeOperation.run(txn); |
| | | return; |
| | | } |
| | | if (armed == ConflictPoint.TREES_OF_BASE_DN) |
| | | { |
| | | writeOperation.run(new ConflictingAtTreesOf(txn, conflictingPrefix)); |
| | | return; |
| | | } |
| | | writeOperation.run(txn); |
| | | if (armed == ConflictPoint.NO_REPLAY) |
| | | { |
| | | throw new UnreplayableFailure(); |
| | | } |
| | | throw new RollbackException(); |
| | | } |
| | | }); |
| | | } |
| | | |
| | | @Override |
| | | public Importer startImport() throws ConfigException |
| | | { |
| | | return delegate.startImport(); |
| | | } |
| | | |
| | | @Override |
| | | public void open(AccessMode accessMode) throws Exception |
| | | { |
| | | delegate.open(accessMode); |
| | | } |
| | | |
| | | @Override |
| | | public <T> T read(ReadOperation<T> 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<TreeName> listTrees() |
| | | { |
| | | return delegate.listTrees(); |
| | | } |
| | | |
| | | @Override |
| | | public void close() |
| | | { |
| | | delegate.close(); |
| | | } |
| | | } |
| | | |
| | | /** |
| | | * A transaction which conflicts as the first tree of one base DN is opened, and delegates |
| | | * everything the operation did before that. This is where an entry container which is being opened |
| | | * meets a write-write conflict: PDBStorage wraps the {@link RollbackException} PersistIt raises at |
| | | * the store into a {@link StorageRuntimeException}, which is what {@code EntryContainer.open}'s own |
| | | * catch unwinds on, so the base DNs opened before this one have been registered and the one being |
| | | * opened has not. |
| | | */ |
| | | private static final class ConflictingAtTreesOf implements WriteableTransaction |
| | | { |
| | | private final WriteableTransaction delegate; |
| | | private final String conflictingPrefix; |
| | | |
| | | ConflictingAtTreesOf(WriteableTransaction delegate, String conflictingPrefix) |
| | | { |
| | | this.delegate = delegate; |
| | | this.conflictingPrefix = conflictingPrefix; |
| | | } |
| | | |
| | | @Override |
| | | public void openTree(TreeName name, boolean createOnDemand) |
| | | { |
| | | if (conflictingPrefix.equals(name.getBaseDN())) |
| | | { |
| | | // What PDBStorage delivers: the engine's RollbackException wrapped, which |
| | | // EntryContainer.open's catch(StorageRuntimeException) unwinds on. |
| | | throw new StorageRuntimeException(new RollbackException()); |
| | | } |
| | | delegate.openTree(name, createOnDemand); |
| | | } |
| | | |
| | | @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 Cursor<ByteString, ByteString> openCursor(TreeName treeName) |
| | | { |
| | | return delegate.openCursor(treeName); |
| | | } |
| | | |
| | | @Override |
| | | public long getRecordCount(TreeName treeName) |
| | | { |
| | | return delegate.getRecordCount(treeName); |
| | | } |
| | | |
| | | @Override |
| | | public boolean treeExists(TreeName treeName) |
| | | { |
| | | return delegate.treeExists(treeName); |
| | | } |
| | | } |
| | | } |