From add86d3c7c047215f886c51e2b29a4c7f1e86c0c Mon Sep 17 00:00:00 2001
From: Valery Kharseko <vharseko@3a-systems.ru>
Date: Thu, 03 Sep 2026 08:21:14 +0000
Subject: [PATCH] [#877] Bound a statement of the JDBC backend by the class of the work it belongs to (#882)

---
 opendj-server-legacy/src/main/java/org/opends/server/backends/pluggable/ShardedCounter.java |   55 +++++++++++++++++++++++++++++++++++++++++++++++++------
 1 files changed, 49 insertions(+), 6 deletions(-)

diff --git a/opendj-server-legacy/src/main/java/org/opends/server/backends/pluggable/ShardedCounter.java b/opendj-server-legacy/src/main/java/org/opends/server/backends/pluggable/ShardedCounter.java
index efde5df..9e5769d 100644
--- a/opendj-server-legacy/src/main/java/org/opends/server/backends/pluggable/ShardedCounter.java
+++ b/opendj-server-legacy/src/main/java/org/opends/server/backends/pluggable/ShardedCounter.java
@@ -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;
 
@@ -72,16 +73,30 @@
     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)
@@ -107,8 +122,35 @@
 
   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())
       {
@@ -121,7 +163,8 @@
   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())

--
Gitblit v1.10.0