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