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