Skip to content

Commit dbeef92

Browse files
fix(index): clean up indicies DB rows after physical delete — Fixes #35306
ContentletIndexAPIImpl.delete() physically removed the index from the search provider but never deleted the corresponding row from the indicies table, leaving stale DB entries that could cause startup failures or silent inconsistencies on the next boot. After a successful physical delete, versionedIndicesAPI.removeByIndexName() is now called to clean up both the bare and os:: forms of the row. The cleanup is phase-safe: - Physical delete: indexAPI (IndexAPIImpl router) fans out to ES, OS, or both depending on the active migration phase. - DB cleanup: versionedIndicesAPI.removeByIndexName() deletes by index_name and handles both legacy (index_version IS NULL) and versioned (os:: tagged) rows in a single call. The deprecated legacyIndiciesAPI is not involved. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
1 parent fc12f6b commit dbeef92

6 files changed

Lines changed: 77 additions & 2 deletions

File tree

dotCMS/src/main/java/com/dotcms/content/elasticsearch/business/ContentletIndexAPIImpl.java

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1285,8 +1285,17 @@ private void logSwitchover(final IndiciesInfo oldInfo, final String luckyServer)
12851285

12861286
}
12871287

1288-
public boolean delete(String indexName) {
1289-
return indexAPI.delete(indexName);
1288+
public boolean delete(final String indexName) {
1289+
final boolean deleted = indexAPI.delete(indexName);
1290+
if (deleted) {
1291+
try {
1292+
versionedIndicesAPI.removeByIndexName(indexName);
1293+
} catch (DotDataException e) {
1294+
Logger.error(this, "Index [" + indexName + "] was physically deleted but the " +
1295+
"indices DB row could not be removed: " + e.getMessage(), e);
1296+
}
1297+
}
1298+
return deleted;
12901299
}
12911300

12921301
public boolean optimize(List<String> indexNames) {

dotCMS/src/main/java/com/dotcms/content/index/IndicesFactory.java

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -127,4 +127,14 @@ public interface IndicesFactory {
127127
void insertIndexIfPresent(String indexName, String indexType,
128128
String version) throws DotDataException;
129129

130+
/**
131+
* Removes the {@code indicies} table row for the given index name.
132+
* Handles both plain and {@code os::}-prefixed forms of the name so that
133+
* callers do not need to know how the name is stored in the database.
134+
*
135+
* @param indexName the physical index name (with or without vendor tag)
136+
* @throws DotDataException if the name is blank or a SQL error occurs
137+
*/
138+
void removeByIndexName(String indexName) throws DotDataException;
139+
130140
}

dotCMS/src/main/java/com/dotcms/content/index/IndicesFactoryImpl.java

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,8 @@ public class IndicesFactoryImpl implements IndicesFactory {
4343
"SELECT COUNT(*) as count FROM indicies WHERE index_version = ? AND index_version IS NOT NULL";
4444
private static final String COUNT_INDICES_BY_VERSION_SQL =
4545
"SELECT COUNT(*) as count FROM indicies WHERE index_version = ? AND index_version IS NOT NULL";
46+
private static final String DELETE_INDEX_BY_NAME_SQL =
47+
"DELETE FROM indicies WHERE index_name = ? OR index_name = ?";
4648

4749
@Override
4850
public Optional<VersionedIndices> loadIndices(String version) throws DotDataException {
@@ -360,6 +362,24 @@ public void insertIndexIfPresent(String indexName, String indexType, String vers
360362
}
361363
}
362364

365+
@Override
366+
public void removeByIndexName(final String indexName) throws DotDataException {
367+
if (!UtilMethods.isSet(indexName)) {
368+
throw new DotDataException("Index name cannot be null or empty");
369+
}
370+
// Always work from the bare name so we can reliably compute both DB forms
371+
final String bareName = IndexTag.strip(indexName);
372+
final String taggedName = IndexTag.OS.tag(bareName);
373+
try {
374+
final DotConnect dotConnect = new DotConnect();
375+
final int deleted = dotConnect.executeUpdate(DELETE_INDEX_BY_NAME_SQL, bareName, taggedName);
376+
Logger.info(this, "Removed " + deleted + " row(s) from indicies for index: " + bareName);
377+
} catch (Exception e) {
378+
Logger.error(this, "Failed to remove indicies row for index: " + bareName, e);
379+
throw new DotDataException("Failed to remove indicies row for index: " + bareName, e);
380+
}
381+
}
382+
363383
/**
364384
* Builds a VersionedIndicesInfo from legacy non-versioned database results.
365385
* Expects results to have columns: index_name, index_type (no index_version)

dotCMS/src/main/java/com/dotcms/content/index/VersionedIndicesAPI.java

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -104,6 +104,16 @@ public interface VersionedIndicesAPI {
104104
*/
105105
Optional<VersionedIndices> loadDefaultVersionedIndices() throws DotDataException;
106106

107+
/**
108+
* Removes the {@code indicies} table row for the given index name.
109+
* Handles both plain and {@code os::}-prefixed forms of the name so that
110+
* callers do not need to know how the name is stored in the database.
111+
*
112+
* @param indexName the physical index name (with or without vendor tag)
113+
* @throws DotDataException if the name is blank or a SQL error occurs
114+
*/
115+
void removeByIndexName(String indexName) throws DotDataException;
116+
107117
/**
108118
* Clears all cached indices data.
109119
* This should be called when indices are modified outside of this API

dotCMS/src/main/java/com/dotcms/content/index/VersionedIndicesAPIImpl.java

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -226,6 +226,23 @@ public Optional<VersionedIndices> loadDefaultVersionedIndices() throws DotDataEx
226226
return loadIndices(VersionedIndices.OPENSEARCH_3X);
227227
}
228228

229+
/**
230+
* {@inheritDoc}
231+
*/
232+
@WrapInTransaction
233+
@Override
234+
public void removeByIndexName(final String indexName) throws DotDataException {
235+
Logger.debug(this, "Removing indices row for index: " + indexName);
236+
indicesFactory.removeByIndexName(indexName);
237+
// Flush all index-related caches so no stale names survive the deletion:
238+
// 1. VersionedIndicesCache — our own versioned-index cache
239+
cache.clearCache();
240+
// 2. IndiciesCache — legacy (ES, non-versioned) index cache used by IndiciesFactory
241+
CacheLocator.getIndiciesCache().clearCache();
242+
// 3. ESQueryCache — cached search queries that may reference the deleted index name
243+
CacheLocator.getESQueryCache().clearCache();
244+
}
245+
229246
/**
230247
* {@inheritDoc}
231248
*/

dotcms-integration/src/test/java/com/dotcms/content/elasticsearch/business/ContentletIndexAPIImplTest.java

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -374,6 +374,15 @@ public void createContentIndexAndDelete() throws Exception {
374374

375375
//Verify we just added two more indices
376376
assertEquals(oldIndices, newIndices);
377+
378+
// Verify the indicies table has no orphan rows for the deleted indices
379+
final DotConnect dc = new DotConnect();
380+
dc.setSQL("SELECT COUNT(*) AS cnt FROM indicies WHERE index_name = ? OR index_name = ?");
381+
dc.addParam(workingIndex);
382+
dc.addParam(liveIndex);
383+
final List<Map<String, Object>> rows = dc.loadResults();
384+
assertEquals("indicies table must have no orphan rows after delete",
385+
0L, Long.parseLong(rows.get(0).get("cnt").toString()));
377386
}
378387

379388
/**

0 commit comments

Comments
 (0)