From 0ee6ec043217553a227927ca511bd5db5830fba7 Mon Sep 17 00:00:00 2001
From: Valery Kharseko <vharseko@3a-systems.ru>
Date: Mon, 21 Sep 2026 09:32:11 +0000
Subject: [PATCH] [#990] Drop what a previous index left behind instead of adopting it when an index is added (#998)
---
opendj-server-legacy/src/main/java/org/opends/server/backends/pluggable/EntryContainer.java | 75 +++++++++++++++++++++++++++++++++++++
1 files changed, 75 insertions(+), 0 deletions(-)
diff --git a/opendj-server-legacy/src/main/java/org/opends/server/backends/pluggable/EntryContainer.java b/opendj-server-legacy/src/main/java/org/opends/server/backends/pluggable/EntryContainer.java
index 70bf822..4c6d71b 100644
--- a/opendj-server-legacy/src/main/java/org/opends/server/backends/pluggable/EntryContainer.java
+++ b/opendj-server-legacy/src/main/java/org/opends/server/backends/pluggable/EntryContainer.java
@@ -181,6 +181,12 @@
@Override
public boolean isConfigurationAddAcceptable(final BackendIndexCfg cfg, List<LocalizableMessage> unacceptableReasons)
{
+ final LocalizableMessage alreadyIndexed = alreadyIndexedBy(cfg);
+ if (alreadyIndexed != null)
+ {
+ unacceptableReasons.add(alreadyIndexed);
+ return false;
+ }
try
{
newAttributeIndex(cfg, null);
@@ -193,14 +199,56 @@
}
}
+ /**
+ * Why an index for the attribute of this configuration must not be added, or null if it may be.
+ * <p>
+ * The map is keyed by the attribute type, which every one of the attribute's names and its OID resolve to,
+ * while the configuration entry is named by whichever of them was typed. An index declared under another of
+ * them - commonName or 2.5.4.3 for cn - names the very trees the live index serves, and the add would drop
+ * them as left behind by an index which is gone.
+ */
+ private LocalizableMessage alreadyIndexedBy(final BackendIndexCfg cfg)
+ {
+ final AttributeIndex existing = attrIndexMap.get(cfg.getAttribute());
+ if (existing == null)
+ {
+ return null;
+ }
+ return ERR_CONFIG_INDEX_ATTRIBUTE_ALREADY_INDEXED.get(cfg.getAttribute().getNameOrOID(), getBaseDN(),
+ existing.getConfiguration().dn());
+ }
+
@Override
public ConfigChangeResult applyConfigurationAdd(final BackendIndexCfg cfg)
{
final ConfigChangeResult ccr = new ConfigChangeResult();
+ // Refused by isConfigurationAddAcceptable() before the configuration is written; asked again here since what
+ // follows drops the trees of the index it would have found.
+ final LocalizableMessage alreadyIndexed = alreadyIndexedBy(cfg);
+ if (alreadyIndexed != null)
+ {
+ ccr.setResultCode(ResultCode.UNWILLING_TO_PERFORM);
+ ccr.addMessage(alreadyIndexed);
+ return ccr;
+ }
+ // Dropped in a write of its own, committed before the write which opens the index: the two must not
+ // share a transaction, see AttributeIndex.dropLeftovers(). discarded is filled by that write and reported
+ // from a finally below: every attempt fills it afresh, so a replayed attempt repeats nothing, and the
+ // report is not skipped when the write which opens the index throws after it, nor when the drop write
+ // fails at its own commit - for the reason AttributeIndex.dropLeftovers() gives.
+ final AtomicBoolean discarded = new AtomicBoolean();
try
{
final CryptoSuite cryptoSuite = newCryptoSuite(cfg.isConfidentialityEnabled());
final AttributeIndex index = newAttributeIndex(cfg, cryptoSuite);
+ storage.write(new WriteOperation()
+ {
+ @Override
+ public void run(WriteableTransaction txn) throws Exception
+ {
+ discarded.set(index.dropLeftovers(txn));
+ }
+ });
final AtomicBoolean trusted = new AtomicBoolean();
storage.write(new WriteOperation()
{
@@ -229,6 +277,13 @@
ccr.setResultCode(DirectoryServer.getCoreConfigManager().getServerErrorResultCode());
ccr.addMessage(LocalizableMessage.raw(e.getLocalizedMessage()));
}
+ finally
+ {
+ if (discarded.get())
+ {
+ AttributeIndex.reportDiscardedLeftovers(ccr, cfg.getAttribute().getNameOrOID(), getBaseDN());
+ }
+ }
return ccr;
}
@@ -306,8 +361,21 @@
public ConfigChangeResult applyConfigurationAdd(final BackendVLVIndexCfg cfg)
{
final ConfigChangeResult ccr = new ConfigChangeResult();
+ // Dropped in a write of its own, committed before the write which builds and opens the index, for the
+ // reason given in the index add listener above. discarded is filled by that write and reported from a
+ // finally below, on the terms given there: not repeated by a replayed attempt, not skipped when the write
+ // which builds and opens the index throws after it, nor when the drop write fails at its own commit.
+ final AtomicBoolean discarded = new AtomicBoolean();
try
{
+ storage.write(new WriteOperation()
+ {
+ @Override
+ public void run(WriteableTransaction txn) throws Exception
+ {
+ discarded.set(VLVIndex.dropLeftovers(txn, EntryContainer.this, state, cfg.getName()));
+ }
+ });
final AtomicReference<VLVIndex> built = new AtomicReference<>();
final AtomicBoolean trusted = new AtomicBoolean();
storage.write(new WriteOperation()
@@ -343,6 +411,13 @@
ccr.setResultCode(DirectoryServer.getCoreConfigManager().getServerErrorResultCode());
ccr.addMessage(LocalizableMessage.raw(StaticUtils.stackTraceToSingleLineString(e)));
}
+ finally
+ {
+ if (discarded.get())
+ {
+ AttributeIndex.reportDiscardedLeftovers(ccr, cfg.getName(), getBaseDN());
+ }
+ }
return ccr;
}
--
Gitblit v1.10.0