Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
Expand Up @@ -1118,7 +1118,8 @@ public Void run(ReadableTransaction txn) throws Exception
long undefined = 0;
long count = 0;
BackendTreeKeyValue keyDecoder = new BackendTreeKeyValue(index);
try (Cursor<ByteString, EntryIDSet> cursor = index.openCursor(txn))
// dbtest walks the index whole, on the command line of an operator: bulk work either way
try (Cursor<ByteString, EntryIDSet> cursor = index.openBulkCursor(txn))
{
while (cursor.next())
{
Expand Down Expand Up @@ -1286,7 +1287,8 @@ public TreeStats run(ReadableTransaction txn) throws Exception
long count = 0;
long totalKeySize = 0;
long totalDataSize = 0;
try (final Cursor<ByteString, ByteString> cursor = txn.openCursor(target.getTreeName()))
// dbtest walks the tree whole, on the command line of an operator: bulk work either way
try (final Cursor<ByteString, ByteString> cursor = txn.openBulkCursor(target.getTreeName()))
{
ByteString key;
ByteString maxKey = null;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
*
* Copyright 2006-2010 Sun Microsystems, Inc.
* Portions Copyright 2012-2016 ForgeRock AS.
* Portions Copyright 2026 3A Systems, LLC.
*/
package org.opends.server.backends.pluggable;

Expand Down Expand Up @@ -131,7 +132,19 @@ public String valueToString(ByteString value)
public final Cursor<ByteString, EntryIDSet> openCursor(ReadableTransaction txn)
{
checkNotNull(txn, "txn must not be null");
return CursorTransformer.transformValues(txn.openCursor(getName()),
return decoding(txn.openCursor(getName()));
}

@Override
public final Cursor<ByteString, EntryIDSet> openBulkCursor(ReadableTransaction txn)
{
checkNotNull(txn, "txn must not be null");
return decoding(txn.openBulkCursor(getName()));
}

private Cursor<ByteString, EntryIDSet> decoding(Cursor<ByteString, ByteString> cursor)
{
return CursorTransformer.transformValues(cursor,
new ValueTransformer<ByteString, ByteString, EntryIDSet, NeverThrowsException>()
{
@Override
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2380,7 +2380,29 @@ public Long run(ReadableTransaction txn) throws Exception

long getNumberOfEntriesInBaseDN0(ReadableTransaction txn)
{
return id2childrenCount.getTotalCount(txn);
return getNumberOfEntriesInBaseDN0(txn, false);
}

/**
* The same count, told which kind of work it is part of: {@code verify-index} reads it before
* walking the whole backend, with nobody waiting on the walk or on the count that sizes it, while
* {@code cn=monitor} and the searches of {@code GroupManager} and {@code SubentryManager} read it
* for a client.
* <p>
* The read behind {@code NOTE_BACKEND_STARTED} is deliberately left with the client callers,
* though nobody waits on it either: it goes through {@code BackendImpl.getEntryCount()}, which is
* the same method those three call, and what a bound costs it there is a log line reporting -1
* entries - that method answers -1 for any failure rather than failing the open.
*
* @param txn storage transaction
* @param partOfAWholeTreeWalk whether this read belongs to a walk of a whole tree rather than to
* a client operation
* @return The number of entries stored in this entry container including the baseDN.
* @see ReadableTransaction#openBulkCursor(TreeName)
*/
long getNumberOfEntriesInBaseDN0(ReadableTransaction txn, boolean partOfAWholeTreeWalk)
{
return id2childrenCount.getTotalCount(txn, partOfAWholeTreeWalk);
}

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
*
* Copyright 2006-2008 Sun Microsystems, Inc.
* Portions Copyright 2012-2016 ForgeRock AS.
* Portions Copyright 2026 3A Systems, LLC.
*/
package org.opends.server.backends.pluggable;

Expand Down Expand Up @@ -168,11 +169,17 @@ public Void run(ReadableTransaction txn) throws Exception
* @throws LDIFException If an error occurs while trying to determine
* whether to write an entry.
*/
private void exportContainer(ReadableTransaction txn, EntryContainer entryContainer)
// Visible to the test that pins the class of the cursor opened here: a walk of the whole of
// id2entry with nobody waiting on it, which a storage engine that bounds a statement must not
// bound as it bounds an operation (#877).
void exportContainer(ReadableTransaction txn, EntryContainer entryContainer)
throws StorageRuntimeException, IOException, LDIFException
{
ID2Entry id2entry = entryContainer.getID2Entry();
try (final Cursor<ByteString, ByteString> cursor = txn.openCursor(id2entry.getName()))
// The whole of id2entry with nobody waiting on the walk: an export-ldif, or the generation ID
// a replicated domain computes for itself the first time it starts (LDAPReplicationDomain
// .computeGenerationId), which is why this must not be bounded as the work of an operation.
try (final Cursor<ByteString, ByteString> cursor = txn.openBulkCursor(id2entry.getName()))
{
while (cursor.next())
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -57,9 +57,16 @@ public EntryID apply(ByteString value) throws NeverThrowsException
this.counter = new ShardedCounter(name);
}

SequentialCursor<EntryID, Void> openCursor(ReadableTransaction txn)
/**
* Walks the children counts whole, which {@code verify-index} does and no client operation does:
* there is no overload of this method that would take the bound of an operation by accident.
* Reading the count of a single entry is another matter - see {@link #getCount}.
*
* @see ReadableTransaction#openBulkCursor(TreeName)
*/
SequentialCursor<EntryID, Void> openBulkCursor(ReadableTransaction txn)
{
return transformKeysAndValues(counter.openCursor(txn),
return transformKeysAndValues(counter.openBulkCursor(txn),
TO_ENTRY_ID, CursorTransformer.<ByteString, Void> keepValuesUnchanged());
}

Expand Down Expand Up @@ -141,7 +148,23 @@ public ByteString generateKey(String data)
*/
long getCount(ReadableTransaction txn, EntryID entryID)
{
return counter.getCount(txn, toKey(entryID));
return getCount(txn, entryID, false);
}

/**
* Get the number of children for the given entry, as part of a walk of a whole tree rather than
* of a client operation. {@code verify-index} reads one of these per DN while it walks dn2id
* whole, and no client is waiting on any of them.
*
* @param txn storage transaction
* @param entryID The entryID identifying to the counter
* @param partOfAWholeTreeWalk whether this read belongs to a walk of a whole tree
* @return Value of the counter. 0 if no counter is associated yet.
* @see ReadableTransaction#openBulkCursor(TreeName)
*/
long getCount(ReadableTransaction txn, EntryID entryID, boolean partOfAWholeTreeWalk)
{
return counter.getCount(txn, toKey(entryID), partOfAWholeTreeWalk);
}

/**
Expand All @@ -151,7 +174,26 @@ long getCount(ReadableTransaction txn, EntryID entryID)
*/
long getTotalCount(ReadableTransaction txn)
{
return getCount(txn, TOTAL_COUNT_ENTRY_ID);
return getTotalCount(txn, false);
}

/**
* The same total, told which kind of work it is part of. It is a read of this tree like any
* other - a cursor positioned on one key, which on a storage engine that walks a table rather
* than an index is a scan of it - so what it may take follows who is waiting on it:
* {@code verify-index} reads it once to size the progress report of a walk of the whole backend,
* with nobody waiting, while {@code cn=monitor} and the searches of {@code GroupManager} and
* {@code SubentryManager} read the same total for a client.
*
* @param txn storage transaction
* @param partOfAWholeTreeWalk whether this read belongs to a walk of a whole tree rather than to
* a client operation
* @return Sum of all the counter contained in this tree
* @see ReadableTransaction#openBulkCursor(TreeName)
*/
long getTotalCount(ReadableTransaction txn, boolean partOfAWholeTreeWalk)
{
return getCount(txn, TOTAL_COUNT_ENTRY_ID, partOfAWholeTreeWalk);
}

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -382,7 +382,11 @@ void afterOpen(WriteableTransaction txn, boolean createOnDemand) throws StorageR
{
// Make sure the tree is there and readable, even if the storage is READ_ONLY.
// Would be nice if there were a better way...
try (final Cursor<ByteString, ByteString> cursor = txn.openCursor(getName()))
// Bulk: the first batch of a cursor carries no seek predicate, so this is a walk of the whole
// tree as far as the storage is concerned, and it runs on every open of the backend. A bound
// meant for an entry read would keep a large backend from opening at all on an engine where
// such a batch is not a step along an index.
try (final Cursor<ByteString, ByteString> cursor = txn.openBulkCursor(getName()))
{
cursor.next();
}
Expand Down Expand Up @@ -498,6 +502,12 @@ Cursor<EntryID, Entry> openCursor(ReadableTransaction txn)

/**
* Check that a record entry exists in the entry tree.
* <p>
* Bulk, like the walk it belongs to: {@code VerifyJob.iterateID2ChildrenCount()} is its one
* caller and asks this once per record of the children count tree, inside a cursor over the
* whole of it. Read as a client operation those would put the bound of an entry read over a
* job nobody is waiting on, once per record - the same hazard the walk around them was given
* {@link ReadableTransaction#openBulkCursor(TreeName)} for (#877).
*
* @param txn a non null transaction
* @param entryID The entry ID which forms the key.
Expand All @@ -508,7 +518,7 @@ public boolean containsEntryID(ReadableTransaction txn, EntryID entryID)
{
checkNotNull(txn, "txn must not be null");
checkNotNull(entryID, "entryID must not be null");
try(final Cursor<ByteString, ByteString> cursor = txn.openCursor(getName())) {
try(final Cursor<ByteString, ByteString> cursor = txn.openBulkCursor(getName())) {
return cursor.positionToKey(entryID.toByteString());
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
*
* Copyright 2006-2010 Sun Microsystems, Inc.
* Portions Copyright 2012-2016 ForgeRock AS.
* Portions Copyright 2026 3A Systems, LLC.
*/
package org.opends.server.backends.pluggable;

Expand All @@ -37,6 +38,23 @@ interface Index extends Tree

Cursor<ByteString, EntryIDSet> openCursor(ReadableTransaction txn);

/**
* Opens a cursor over the whole index for a task no client operation is waiting on, such as
* {@code verify-index} or {@code dbtest}.
* <p>
* Abstract rather than a {@code default} answering as {@link #openCursor(ReadableTransaction)}
* does, which is the compatibility the SPI needs for engines outside this repository: this
* interface is package-private with one implementor, and a second one inheriting that default
* would silently walk a whole index under the bound of a client operation. A compile error is
* the better answer here.
*
* @param txn
* the transaction to read the index with
* @return a cursor over every key of this index
* @see ReadableTransaction#openBulkCursor(org.opends.server.backends.pluggable.spi.TreeName)
*/
Cursor<ByteString, EntryIDSet> openBulkCursor(ReadableTransaction txn);

boolean setIndexEntryLimit(int indexEntryLimit);

boolean setConfidential(boolean indexConfidential);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -349,7 +349,7 @@ private long copyMissingRecords(WriteableTransaction txn, TreeName from, TreeNam
return 0;
}
long copied = 0;
try (Cursor<ByteString, ByteString> cursor = txn.openCursor(from))
try (Cursor<ByteString, ByteString> cursor = txn.openBulkCursor(from))
{
while (cursor.next())
{
Expand All @@ -375,9 +375,10 @@ private void loadTrees(ReadableTransaction txn, TreeName ocTree, TreeName adTree
// Cursor through the object class database and load the object class set
// definitions. At the same time, figure out the highest token value and
// initialize the object class counter to one greater than that.
// Both trees are read whole while the backend opens, with no client operation waiting on it.
if (txn.treeExists(ocTree))
{
try (Cursor<ByteString, ByteString> ocCursor = txn.openCursor(ocTree))
try (Cursor<ByteString, ByteString> ocCursor = txn.openBulkCursor(ocTree))
{
while (ocCursor.next())
{
Expand All @@ -403,7 +404,7 @@ private void loadTrees(ReadableTransaction txn, TreeName ocTree, TreeName adTree
// Cursor through the attribute description database and load the attribute set definitions.
if (txn.treeExists(adTree))
{
try (Cursor<ByteString, ByteString> adCursor = txn.openCursor(adTree))
try (Cursor<ByteString, ByteString> adCursor = txn.openBulkCursor(adTree))
{
while (adCursor.next())
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
* information: "Portions Copyright [year] [name of copyright owner]".
*
* Copyright 2015 ForgeRock AS.
* Portions Copyright 2026 3A Systems, LLC.
*/
package org.opends.server.backends.pluggable;

Expand Down Expand Up @@ -72,16 +73,30 @@ public ByteString apply(ByteString shardedKey)
super(name);
}

SequentialCursor<ByteString, Void> openCursor(ReadableTransaction txn)
/**
* Walks this counter whole, which {@code verify-index} does and no client operation does: there
* is no overload of this method that would take the bound of an operation by accident. Reading a
* single counter is another matter - {@link #getCount} opens a cursor of its own, and one of a
* client operation unless the caller says otherwise.
*
* @see ReadableTransaction#openBulkCursor(TreeName)
*/
SequentialCursor<ByteString, Void> openBulkCursor(ReadableTransaction txn)
{
return uniqueKeys(txn.openBulkCursor(getName()));
}

private SequentialCursor<ByteString, Void> uniqueKeys(Cursor<ByteString, ByteString> cursor)
{
return new UniqueKeysCursor<>(transformKeysAndValues(
txn.openCursor(getName()), TO_KEY,
cursor, TO_KEY,
CursorTransformer.<ByteString, ByteString, Void> constant(null)));
}

private Cursor<ByteString, Long> openCursor0(ReadableTransaction txn)
private Cursor<ByteString, Long> openCursor0(ReadableTransaction txn, boolean partOfAWholeTreeWalk)
{
return transformKeysAndValues(txn.openCursor(getName()), TO_KEY, TO_LONG);
return transformKeysAndValues(
partOfAWholeTreeWalk ? txn.openBulkCursor(getName()) : txn.openCursor(getName()), TO_KEY, TO_LONG);
}

void addCount(final WriteableTransaction txn, ByteSequence key, final long delta)
Expand All @@ -106,9 +121,36 @@ void importPut(Importer importer, ByteSequence key, long delta)
}

long getCount(final ReadableTransaction txn, ByteSequence key)
{
return getCount(txn, key, false);
}

/**
* The same read, told which kind of work it is part of. A client operation reads a counter of its
* own and takes the bound of one - {@code numSubordinates} of a search
* ({@code EntryContainer.getNumberOfChildren}), the entry count of a VLV index a search is paging
* through ({@code VLVIndex.getEntryCount}) - while {@code verify-index} reads one per DN of the
* tree it is walking, with nobody waiting on it: bounding those as client operations is what #877
* exists to stop, and on the JDBC backend it aborted a verify of a backend large enough.
* <p>
* The third caller is {@code ID2ChildrenCount.getTotalCount}, which is read both ways and is told
* which it is by its own caller: a verify sizes its progress report with it, {@code cn=monitor}
* and the searches of {@code GroupManager} and {@code SubentryManager} read it for a client. A
* delete and a modify DN reach neither form - they go through {@link #removeCount}, which is a
* client operation by construction.
*
* @param txn storage transaction
* @param key the counter to read
* @param partOfAWholeTreeWalk whether this read belongs to a walk of a whole tree rather than to
* a client operation
* @return Value of the counter. 0 if no counter is associated yet.
* @see ReadableTransaction#openBulkCursor(TreeName)
*/
long getCount(final ReadableTransaction txn, ByteSequence key, boolean partOfAWholeTreeWalk)
{
long counterValue = 0;
try (final SequentialCursor<ByteString, Long> cursor = new ShardCursor(openCursor0(txn), key))
try (final SequentialCursor<ByteString, Long> cursor =
new ShardCursor(openCursor0(txn, partOfAWholeTreeWalk), key))
{
while (cursor.next())
{
Expand All @@ -121,7 +163,8 @@ long getCount(final ReadableTransaction txn, ByteSequence key)
long removeCount(final WriteableTransaction txn, ByteSequence key)
{
long counterValue = 0;
try (final SequentialCursor<ByteString, Long> cursor = new ShardCursor(openCursor0(txn), key))
// a removal is always a client operation: an entry is being deleted or moved
try (final SequentialCursor<ByteString, Long> cursor = new ShardCursor(openCursor0(txn, false), key))
{
// Iterate over and remove all the thread local shards
while (cursor.next())
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -305,6 +305,15 @@ public Cursor<ByteString, ByteString> openCursor(final TreeName name)
return new TracedCursor(cursor);
}

@Override
public Cursor<ByteString, ByteString> openBulkCursor(final TreeName name)
{
traceEnter("openBulkCursor", "name", name);
final Cursor<ByteString, ByteString> cursor = txn.openBulkCursor(name);
traceLeave("openBulkCursor", "name", name);
return new TracedCursor(cursor);
}

@Override
public ByteString read(final TreeName name, final ByteSequence key)
{
Expand Down Expand Up @@ -392,6 +401,15 @@ public Cursor<ByteString, ByteString> openCursor(final TreeName name)
return new TracedCursor(cursor);
}

@Override
public Cursor<ByteString, ByteString> openBulkCursor(final TreeName name)
{
traceEnter("openBulkCursor", "name", name);
final Cursor<ByteString, ByteString> cursor = txn.openBulkCursor(name);
traceLeave("openBulkCursor", "name", name);
return new TracedCursor(cursor);
}

@Override
public void openTree(final TreeName name, boolean createOnDemand)
{
Expand Down
Loading
Loading