From 655132fb059cfa70fb338d54f1bb0fb48198fb0f Mon Sep 17 00:00:00 2001
From: Valery Kharseko <vharseko@3a-systems.ru>
Date: Wed, 02 Sep 2026 06:44:10 +0000
Subject: [PATCH] [#873] Give each backend its own compressed schema trees (#881)

---
 opendj-server-legacy/src/main/java/org/opends/server/backends/cassandra/CASStorage.java |   60 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
 1 files changed, 60 insertions(+), 0 deletions(-)

diff --git a/opendj-server-legacy/src/main/java/org/opends/server/backends/cassandra/CASStorage.java b/opendj-server-legacy/src/main/java/org/opends/server/backends/cassandra/CASStorage.java
index e30c748..920b866 100644
--- a/opendj-server-legacy/src/main/java/org/opends/server/backends/cassandra/CASStorage.java
+++ b/opendj-server-legacy/src/main/java/org/opends/server/backends/cassandra/CASStorage.java
@@ -27,6 +27,7 @@
 import java.util.NoSuchElementException;
 import java.util.Objects;
 import java.util.Set;
+import java.util.regex.Pattern;
 
 import org.forgerock.i18n.LocalizableMessage;
 import org.forgerock.i18n.slf4j.LocalizedLogger;
@@ -63,6 +64,7 @@
 import com.datastax.oss.driver.api.core.cql.ResultSet;
 import com.datastax.oss.driver.api.core.cql.Row;
 import com.datastax.oss.driver.api.core.cql.Statement;
+import com.datastax.oss.driver.api.core.servererrors.InvalidQueryException;
 
 import com.github.benmanes.caffeine.cache.Caffeine;
 import com.github.benmanes.caffeine.cache.LoadingCache;
@@ -204,6 +206,13 @@
 			System.setProperty("datastax-java-driver.profiles."+profile+".basic.request.timeout", "30 seconds");
 		}
 	}
+	// The wordings a server uses for a table or a keyspace that is not there. Matched rather than
+	// the exception type, which covers the whole INVALID protocol code: see namesAnAbsentTable().
+	// "Undefined column name ..." deliberately does not match - that table exists.
+	static final Pattern ABSENT_TABLE=Pattern.compile(
+		"unconfigured (table|columnfamily)|(table|keyspace)[^,]* does not exist|undefined (table|keyspace)",
+		Pattern.CASE_INSENSITIVE);
+
 	private final class TransactionImpl implements ReadableTransaction,WriteableTransaction {
 		
 		final AccessMode accessMode;
@@ -248,6 +257,57 @@
 		}
 
 		@Override
+		public boolean treeExists(TreeName treeName) {
+			// Every tree of this backend is a partition of the one table named after the backend id,
+			// so a tree has no existence apart from its rows: "exists" here means "holds at least one
+			// record". A LIMIT 1 lookup rather than getRecordCount() to avoid the full partition scan
+			// a count would run.
+			try {
+				return execute(
+					prepared.get("SELECT key FROM "+getTableName()+" WHERE baseDN=:baseDN and indexId=:indexId LIMIT 1").bind()
+						.setString("baseDN", treeName.getBaseDN()).setString("indexId", treeName.getIndexId())
+				).one()!=null;
+			}catch (InvalidQueryException e) {
+				// The backend's own table has not been created yet - a read-only open of a backend
+				// that was never written, where openTree() creates nothing - so none of its trees
+				// can exist either. The driver reports it from prepare() as much as from execute().
+				//
+				// Narrowly, twice over, because calling a populated tree absent would have the
+				// compressed schema restart its token allocation from zero and overwrite the
+				// definitions the entries already written were encoded with (#873):
+				//
+				// - InvalidQueryException carries the whole INVALID protocol code, so the rejection
+				//   has to name an absent table or keyspace. An undefined column, say, means the
+				//   table is there with a shape this backend did not write, and answering "absent"
+				//   for it would be that same corruption;
+				// - and only a read-only open may answer it at all. A writeable open has just run
+				//   CREATE TABLE IF NOT EXISTS through openTree(), so a rejection there is a fault
+				//   and must fail the open, as it did before this method existed and loadTrees()
+				//   opened its cursor unconditionally. This is also where a table a coordinator has
+				//   not caught up with lands - what a rolling upgrade produces, since schema
+				//   agreement is never reached in a mixed-version cluster - and it fails loudly
+				//   rather than being taken for a table that was never created.
+				if (accessMode.isWriteable() || !namesAnAbsentTable(e)) {
+					throw e;
+				}
+				return false;
+			}
+		}
+
+		/**
+		 * Whether a rejected query says that the table or the keyspace is not there, as opposed to
+		 * anything else the INVALID protocol code covers. The driver offers nothing finer than the
+		 * server's own message - the code is one value for the whole bucket - so the wordings of
+		 * the server are matched, across the versions that changed them ("unconfigured
+		 * columnfamily" became "unconfigured table", and a keyspace is reported as not existing).
+		 * A wording that is not among them is not read as an absent table: it fails the open, which
+		 * is the side to err on.
+		 */
+		private boolean namesAnAbsentTable(InvalidQueryException e) {
+			return e.getMessage()!=null && ABSENT_TABLE.matcher(e.getMessage()).find();
+		}
+
+		@Override
 		public void deleteTree(TreeName treeName) {
 			checkReadOnly();
 			openTree(treeName,true);

--
Gitblit v1.10.0