From 774272578900c27258f497fd01da347de49e430f Mon Sep 17 00:00:00 2001
From: Valery Kharseko <vharseko@3a-systems.ru>
Date: Tue, 22 Sep 2026 09:09:49 +0000
Subject: [PATCH] [#930] Build the names a JDBC backend gives its own trees once, from the id it was created with (#1007)

---
 opendj-server-legacy/src/main/java/org/opends/server/backends/jdbc/JDBCStorage.java |  120 ++++++++++++++++++++++++++++++++++++++++++++++-------------
 1 files changed, 93 insertions(+), 27 deletions(-)

diff --git a/opendj-server-legacy/src/main/java/org/opends/server/backends/jdbc/JDBCStorage.java b/opendj-server-legacy/src/main/java/org/opends/server/backends/jdbc/JDBCStorage.java
index fad5fa3..caacc46 100644
--- a/opendj-server-legacy/src/main/java/org/opends/server/backends/jdbc/JDBCStorage.java
+++ b/opendj-server-legacy/src/main/java/org/opends/server/backends/jdbc/JDBCStorage.java
@@ -1205,11 +1205,13 @@
 	 * The catalog is per backend and named after the backend id alone: a process that has opened
 	 * nothing can still find its table, and backends sharing one database URL - which nothing
 	 * forbids (#873) - never name each other's trees. The id goes in escaped, for the reason {@link
-	 * #escapedBackendId} states: a name that does not survive being read back is a table of this
-	 * backend that its own clear cannot recognize.
+	 * OwnNames#escapedBackendId} states: a name that does not survive being read back is a table of
+	 * this backend that its own clear cannot recognize.
+	 * <p>
+	 * Built once and remembered; see {@link OwnNames}.
 	 */
 	TreeName getCatalogTree() {
-		return new TreeName(CATALOG_BASE_DN, escapedBackendId());
+		return ownNames().catalogTree;
 	}
 
 	/**
@@ -3287,6 +3289,8 @@
 				leftovers.unreadable.addAll(standing);
 				return leftovers;
 			}
+			// normalized once for the whole scan and not per table: see isOwnTree()
+			final Set<String> ownBaseDNs=ownBaseDNs();
 			for (final String tableName : standing) {
 				final TreeName stamp;
 				try {
@@ -3306,7 +3310,7 @@
 				}
 				if (stamp==null) {
 					leftovers.unattributed.add(tableName);
-				} else if (isOwnTree(stamp)) {
+				} else if (isOwnTree(stamp, ownBaseDNs)) {
 					leftovers.ours.add(tableName+" ("+stamp+")");
 				}
 			}
@@ -3347,23 +3351,75 @@
 	 * here for the reason {@link #SHARED_COMPRESSED_SCHEMA_TREES} is: the prefix is built by a private
 	 * method of {@code PersistentCompressedSchema}, escapes and all. A table stamped with one of these
 	 * carries this backend's id in plain text, so a clear that finds one standing can say whose it is.
+	 * <p>
+	 * Built once and remembered; see {@link OwnNames}.
 	 */
-	private String ownCompressedSchemaBaseDN() {
-		return SHARED_COMPRESSED_SCHEMA_BASE_DN+"_"+escapedBackendId();
+	String ownCompressedSchemaBaseDN() {
+		return ownNames().compressedSchemaBaseDN;
 	}
 
 	/**
-	 * The backend id as one component of a tree name. A tree name is {@code /<base DN>/<id>} and is
-	 * read back by splitting on its slashes ({@code TreeName.valueOf}), so an id carrying one of them
-	 * would name a tree that parses into another tree than it was built from - and a table is stamped
-	 * with that name (#866), so a clear reading the stamp of a table of this backend's own would then
-	 * fail to recognize it and pass it over in silence. The escape is the one {@code
-	 * PersistentCompressedSchema} spells its own prefix with, percent first so that the escape of the
-	 * slash cannot be produced twice, and it leaves an id of the ordinary shape exactly as it is -
-	 * which is what keeps the table names of an installation unchanged.
+	 * The names this backend gives the trees that are its own rather than a base DN's: the escaped
+	 * id of {@link #escapedBackendId}, the catalog tree of {@link #getCatalogTree} and the base DN of
+	 * {@link #ownCompressedSchemaBaseDN}. Held in one object so that the three can never come from
+	 * two different ids.
+	 * <p>
+	 * Read from the configuration once rather than per call, for the reason {@link
+	 * #poolConnectionString} is read once: {@code applyConfigurationChange()} replaces {@code config}
+	 * whole, while the tables of this storage - the catalog table among them - stand under the id
+	 * they were created with, and a name read again from a configuration that has moved would leave
+	 * this storage naming a catalog nothing has ever written and its own tables attributed to
+	 * nobody. The configuration framework holds backend-id read-only - "The backend ID may not be
+	 * altered after the backend is created in the server" - so no change made through it renames a
+	 * live backend, which is what makes reading the id once correct in the first place.
+	 * <p>
+	 * That it also takes the escape and the allocation off every enrolment, off every clear and off
+	 * every table of a clear's leftover scan (#930) is the smaller half of it: every one of those
+	 * call sites is already paying a round trip to the database.
 	 */
-	private String escapedBackendId() {
-		return config.getBackendId().replace("%", "%25").replace("/", "%2F");
+	private static final class OwnNames {
+		/**
+		 * The backend id as one component of a tree name. A tree name is {@code /<base DN>/<id>} and is
+		 * read back by splitting on its slashes ({@code TreeName.valueOf}), so an id carrying one of
+		 * them would name a tree that parses into another tree than it was built from - and a table is
+		 * stamped with that name (#866), so a clear reading the stamp of a table of this backend's own
+		 * would then fail to recognize it and pass it over in silence. The escape is the one {@code
+		 * PersistentCompressedSchema} spells its own prefix with, percent first so that the escape of
+		 * the slash cannot be produced twice, and it leaves an id of the ordinary shape exactly as it
+		 * is - which is what keeps the table names of an installation unchanged.
+		 */
+		final String escapedBackendId;
+		final TreeName catalogTree;
+		final String compressedSchemaBaseDN;
+
+		OwnNames(String backendId) {
+			escapedBackendId=backendId.replace("%", "%25").replace("/", "%2F");
+			catalogTree=new TreeName(CATALOG_BASE_DN, escapedBackendId);
+			compressedSchemaBaseDN=SHARED_COMPRESSED_SCHEMA_BASE_DN+"_"+escapedBackendId;
+		}
+	}
+
+	private volatile OwnNames ownNames;
+
+	/**
+	 * The names above, built at the first call that needs one, and not in the constructor: a storage
+	 * is constructed by callers that go on to name no tree of it at all - the bounds of a statement
+	 * are asked of one whose configuration carries no backend id whatsoever in the tests of {@code
+	 * JDBCStatementBoundTestCase} - and a construction reading the id would fail there, where today
+	 * nothing reads it.
+	 * <p>
+	 * Two callers arriving at once may each build one, and the names of both are the same names.
+	 * Every field of {@link OwnNames} is final, so a caller reading the reference reads the names
+	 * whole and not half-built.
+	 */
+	private OwnNames ownNames() {
+		final OwnNames built=ownNames;
+		if (built!=null) {
+			return built;
+		}
+		final OwnNames names=new OwnNames(config.getBackendId());
+		ownNames=names;
+		return names;
 	}
 
 	/**
@@ -3374,23 +3430,33 @@
 	 * backend id (#873) and so belongs to this backend as plainly as any tree of a base DN it serves -
 	 * where the legacy pair, named from a literal, belongs to no backend in particular and is reported
 	 * by nobody.
+	 * <p>
+	 * The base DNs are handed in rather than read here: they are the same for every table of one
+	 * scan, and normalizing a DN builds its string from every RDN of it ({@code
+	 * DN.toNormalizedUrlSafeString} memoizes nothing), which is a cost the caller pays once instead
+	 * of once per table.
 	 */
-	private boolean isOwnTree(TreeName treeName) {
-		if (getCatalogTree().equals(treeName) || ownCompressedSchemaBaseDN().equals(treeName.getBaseDN())) {
-			return true;
-		}
+	private boolean isOwnTree(TreeName treeName, Set<String> ownBaseDNs) {
+		return getCatalogTree().equals(treeName)
+			|| ownCompressedSchemaBaseDN().equals(treeName.getBaseDN())
+			|| ownBaseDNs.contains(treeName.getBaseDN());
+	}
+
+	/**
+	 * The base DNs this backend serves, in the form the trees of an entry container are named after:
+	 * every one of them is named from the normalized base DN, which is what {@code EntryContainer}
+	 * builds its tree names from.
+	 */
+	private Set<String> ownBaseDNs() {
 		final SortedSet<DN> baseDNs=config.getBaseDN();
 		if (baseDNs==null) {
-			return false;
+			return Collections.emptySet();
 		}
+		final Set<String> normalized=new HashSet<>();
 		for (final DN baseDN : baseDNs) {
-			// every tree of an entry container is named after the normalized form of its base DN,
-			// which is what EntryContainer builds its tree names from
-			if (treeName.getBaseDN().equals(baseDN.toNormalizedUrlSafeString())) {
-				return true;
-			}
+			normalized.add(baseDN.toNormalizedUrlSafeString());
 		}
-		return false;
+		return normalized;
 	}
 
 	/**

--
Gitblit v1.10.0