Skip to content

Fix missing search aliases after reindex by deleting the concrete index atomically within the alias swap - #28667

Merged
pmbrull merged 4 commits into
mainfrom
fix-openmetadata-all-alias
Jun 3, 2026
Merged

Fix missing search aliases after reindex by deleting the concrete index atomically within the alias swap#28667
pmbrull merged 4 commits into
mainfrom
fix-openmetadata-all-alias

Conversation

@mohityadav766

@mohityadav766 mohityadav766 commented Jun 3, 2026

Copy link
Copy Markdown
Member

Describe your changes:

Fixes the recurring "Failed to find index openmetadata_*_search_index" error caused by canonical search aliases going missing or pointing at nothing after a "Recreate Indexes" reindex.

Symptom

Users see, typically after the daily reindex:

Failed to find index openmetadata_table_search_index, openmetadata_topic_search_index,
openmetadata_dashboard_search_index, ...

The Elasticsearch/OpenSearch cluster is healthy and the data is intact — a *_search_index_rebuild_<ts> index exists with all documents, but the canonical alias (*_search_index) is attached to nothing. It is intermittent: re-running "Recreate Indexes" sometimes fixes it, and the only consistent manual workaround is to repoint the alias at the live _rebuild_* index by hand.

Root cause

The zero-downtime recreate flow builds *_rebuild_<ts> and then swaps the canonical alias onto it. On a fresh install (and after any prior orphaning), the canonical name table_search_index exists as a concrete index, not an alias — and OS/ES forbid an alias sharing a name with an existing index. So the handler did two separate cluster operations:

searchClient.deleteIndexWithBackoff(canonicalIndex);   // 1) delete the concrete index
...
searchClient.swapAliases(oldIndices, stagedIndex, aliases); // 2) add the alias (separate request)

If step 2 fails or is interrupted between the two — e.g. the index delete hasn't fully propagated and ES/OS still rejects the alias-add with "an index exists with the same name as the alias" — the concrete index is gone, the alias is not attached, and the canonical name resolves to nothing → orphan. Because it's a propagation race it's intermittent, and createMissingIndexes() (run on every server boot) recreates the canonical as a concrete index again, re-arming the same window each cycle.

Two secondary issues made this worse / harder to reason about:

  1. If the alias set ever resolved empty, the swap was skipped but the old index was still deleted, orphaning the alias.
  2. finalizeReindex read the alias set off the live cluster (getAliases(activeIndexName)), which is non-deterministic (propagates stray aliases), adds a round-trip, and diverged from the distributed promotion path that already derived aliases from indexMapping.json.

Changes

  1. Atomic concrete-index removal during the swap. Added an overload swapAliases(oldIndices, newIndex, aliases, indicesToRemove) that emits a remove_index action (ES with mustExist(false)) in the same updateAliases request as the alias add. The concrete canonical index is now deleted and the alias attached in one atomic operation — so a failure is a no-op (the concrete index and its live aliases survive and the reindex simply retries) instead of an orphan. The old 3-arg signature is kept as a default delegating with an empty removal set. DefaultRecreateHandler no longer calls deleteIndexWithBackoff(canonicalIndex) before the swap; it uses resolveCanonicalRemoval(...) to decide what to hand to the atomic swap.

  2. Never delete the old index when no aliases resolve. finalizeReindex / promoteEntityIndex now abort (abortPromotionWithoutAliases) and record a promotion failure if the alias set comes up empty — the old serving index is left intact for a retry instead of being deleted into a void.

  3. Aliases come solely from indexMapping.json. Removed the getAliases(activeIndexName) cluster read. Both the recreate and finalize paths now derive the set via the single getAliasesFromMapping helper ({ parent aliases, short alias, raw index name }), matching what promoteEntityIndex already did. The set is deterministic and the two promotion paths are unified.

Type of change:

  • Bug fix

High-level design:

  • IndexManagementClient: new atomic swapAliases(..., indicesToRemove); old 3-arg is now a default delegating with Set.of().
  • ElasticSearchIndexManager / OpenSearchIndexManager: build a single UpdateAliasesRequest containing remove (alias from old indices) + remove_index (concrete) + add (alias to staged) actions.
  • ElasticSearchClient / OpenSearchClient: delegate the 4-arg overload to their managers.
  • DefaultRecreateHandler:
    • resolveCanonicalRemoval(...) — returns the concrete canonical index (if any) to remove atomically and prunes the alias name from the delete set.
    • abortPromotionWithoutAliases(...) — guards the empty-alias case without deleting the old index.
    • recreateIndexFromMapping / finalizeReindex derive aliases via getAliasesFromMapping; no cluster read.

Test plan:

Added/updated unit tests in DefaultRecreateHandlerTest (all 33 pass):

  • testFinalizeReindexRemovesConcreteCanonicalAtomically — the concrete canonical is removed inside the atomic swap, never via a separate deleteIndexWithBackoff.
  • testFinalizeReindexFailedSwapDoesNotOrphanConcreteCanonical — on a failed atomic swap the concrete index and its aliases survive (no orphan). This is the regression guard for the reported bug.
  • testPromoteEntityIndexDoesNotOrphanAliasWhenMappingHasNoAliases / testFinalizeReindexDoesNotOrphanAliasWhenNoAliasesResolved — empty alias set never deletes the old serving index.
  • testRecreateIndexFromMappingUsesAliasTargetAsActiveIndex — asserts verify(never()).getAliases(...); aliases are derived from the mapping only.

The orphan was reproduced deterministically against the pre-fix code (concrete canonical + forced swap failure → concrete deleted, alias resolves to nothing) and confirmed gone after the fix.

UI changes:

Not applicable.

Checklist:

  • I have read the CONTRIBUTING document.
  • I have commented on my code, particularly in hard-to-understand areas.
  • I have added tests (unit) and listed them above.
  • I have added a test that covers the exact scenario we are fixing.

@mohityadav766 mohityadav766 self-assigned this Jun 3, 2026
Copilot AI review requested due to automatic review settings June 3, 2026 11:42
@github-actions github-actions Bot added backend safe to test Add this label to run secure Github workflows on PRs labels Jun 3, 2026

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR hardens search index promotion during (re)indexing to handle the “fresh install” shape where the canonical name (e.g., *_search_index) is still a concrete index (and therefore cannot simultaneously be an alias). It introduces an atomic alias swap that can also remove the conflicting concrete index in the same request, preventing the canonical name (including all) from becoming orphaned.

Changes:

  • Extend swapAliases to optionally delete concrete indices atomically within the same aliases update request (ES/OS).
  • Make promotion/finalization attach aliases derived only from indexMapping.json (deterministic), and abort safely when no aliases resolve (orphan guard).
  • Add/adjust unit tests to cover first-install and orphan-prevention scenarios.

Reviewed changes

Copilot reviewed 7 out of 7 changed files in this pull request and generated no comments.

Show a summary per file
File Description
openmetadata-service/src/main/java/org/openmetadata/service/search/DefaultRecreateHandler.java Adds orphan-guard + canonical concrete removal resolution; uses deterministic aliases from mapping and performs atomic swap with optional remove_index.
openmetadata-service/src/main/java/org/openmetadata/service/search/IndexManagementClient.java Updates swapAliases API to accept indicesToRemove and provides a backward-compatible default overload.
openmetadata-service/src/main/java/org/openmetadata/service/search/elasticsearch/ElasticSearchClient.java Wires through the new swapAliases(..., indicesToRemove) signature.
openmetadata-service/src/main/java/org/openmetadata/service/search/elasticsearch/ElasticSearchIndexManager.java Implements atomic alias swap that can also remove_index (with mustExist(false)) in ES.
openmetadata-service/src/main/java/org/openmetadata/service/search/opensearch/OpenSearchClient.java Wires through the new swapAliases(..., indicesToRemove) signature.
openmetadata-service/src/main/java/org/openmetadata/service/search/opensearch/OpenSearchIndexManager.java Implements atomic alias swap that can also remove_index in OpenSearch.
openmetadata-service/src/test/java/org/openmetadata/service/search/DefaultRecreateHandlerTest.java Updates existing expectations and adds coverage for first-install concrete canonical removal + orphan-guard behavior.

@mohityadav766 mohityadav766 changed the title Fix openmetadata all alias is missing on fresh installation Fix missing search aliases after reindex by deleting the concrete index atomically within the alias swap Jun 3, 2026
pmbrull
pmbrull previously approved these changes Jun 3, 2026
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@github-actions

github-actions Bot commented Jun 3, 2026

Copy link
Copy Markdown
Contributor

🟡 Playwright Results — all passed (13 flaky)

✅ 4260 passed · ❌ 0 failed · 🟡 13 flaky · ⏭️ 88 skipped

Shard Passed Failed Flaky Skipped
✅ Shard 1 299 0 0 4
🟡 Shard 2 800 0 3 9
🟡 Shard 3 802 0 2 8
🟡 Shard 4 852 0 2 12
🟡 Shard 5 719 0 1 47
🟡 Shard 6 788 0 5 8
🟡 13 flaky test(s) (passed on retry)
  • Features/DataQuality/DataQuality.spec.ts › TestCase filters (shard 2, 1 retry)
  • Features/DataQuality/TestCaseImportExportE2eFlow.spec.ts › Admin: Complete export-import-validate flow (shard 2, 1 retry)
  • Features/DataQuality/TestCaseResultPermissions.spec.ts › User with only VIEW cannot PATCH results (shard 2, 1 retry)
  • Features/RTL.spec.ts › Verify Following widget functionality (shard 3, 2 retries)
  • Flow/ExploreAggregationCountsMatching.spec.ts › should verify left panel counts and tab search results for normal search (shard 3, 1 retry)
  • Pages/CustomProperties.spec.ts › Enum (shard 4, 1 retry)
  • Pages/CustomProperties.spec.ts › String (shard 4, 1 retry)
  • Pages/ExplorePageRightPanel_KnowledgeCenter.spec.ts › Should remove user owner for knowledgeCenter (shard 5, 1 retry)
  • Pages/Glossary.spec.ts › Change glossary term hierarchy using menu options across glossary (shard 6, 1 retry)
  • Pages/Glossary.spec.ts › Column dropdown drag-and-drop functionality for Glossary Terms table (shard 6, 1 retry)
  • Pages/Lineage/LineageFilters.spec.ts › Verify lineage schema filter selection (shard 6, 1 retry)
  • Pages/ODCSImportExport.spec.ts › Multi-object ODCS contract - object selector shows all schema objects (shard 6, 1 retry)
  • Pages/ServiceEntity.spec.ts › Inactive Announcement create & delete (shard 6, 1 retry)

📦 Download artifacts

How to debug locally
# Download playwright-test-results-<shard> artifact and unzip
npx playwright show-trace path/to/trace.zip    # view trace

Copilot AI review requested due to automatic review settings June 3, 2026 15:08

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 7 out of 7 changed files in this pull request and generated no new comments.

@pmbrull
pmbrull merged commit 70cad10 into main Jun 3, 2026
45 of 48 checks passed
@pmbrull
pmbrull deleted the fix-openmetadata-all-alias branch June 3, 2026 15:22
@gitar-bot

gitar-bot Bot commented Jun 3, 2026

Copy link
Copy Markdown
Code Review ✅ Approved 1 resolved / 1 findings

Implements atomic index deletion and alias swapping during reindexing to prevent orphan states, while unifying alias derivation via index mappings. The OpenSearch removeIndex operation now correctly handles existence checks, resolving all identified missing alias issues.

✅ 1 resolved
Bug: OpenSearch removeIndex omits mustExist(false) unlike ES path

📄 openmetadata-service/src/main/java/org/openmetadata/service/search/opensearch/OpenSearchIndexManager.java:442-447 📄 openmetadata-service/src/main/java/org/openmetadata/service/search/elasticsearch/ElasticSearchIndexManager.java:359-367
In the new atomic swapAliases, the ElasticSearch implementation builds the remove_index action with .mustExist(false) (ElasticSearchIndexManager.java ~line 366), but the OpenSearch implementation omits it (removeIndexBuilder.index(indexToRemove) at OpenSearchIndexManager.java:446). For a remove_index alias action the must_exist flag controls whether the whole atomic updateAliases request fails when the named index is absent. concreteToRemove is populated only after searchClient.indexExists(canonicalIndex) returns true, but there is a TOCTOU window (another node/process in the distributed reindex path could delete or swap the canonical concrete index between the check and the swap). On ES this is tolerated; on OpenSearch the request would fail and swapAliases returns false, aborting promotion and forcing an operator retry even though the desired end-state (concrete index gone, alias on staged) is what we want. This defeats the resilience the PR is explicitly adding and makes the two backends behave differently. Add .mustExist(false) to the OpenSearch remove_index builder to match the ES path and the documented intent.

Options

Display: compact → Showing less information.

Comment with these commands to change:

Compact
gitar display:verbose         

Was this helpful? React with 👍 / 👎 | Gitar

@github-actions

github-actions Bot commented Jun 3, 2026

Copy link
Copy Markdown
Contributor

Failed to cherry-pick changes to the 1.13 branch.
Please cherry-pick the changes manually.
You can find more details here.

@sonarqubecloud

sonarqubecloud Bot commented Jun 3, 2026

Copy link
Copy Markdown

mohityadav766 added a commit that referenced this pull request Jun 3, 2026
…ex atomically within the alias swap (#28667)

(cherry picked from commit 70cad10)
Shreyansh100704 pushed a commit that referenced this pull request Jun 4, 2026
… succeeds (#28700)

PR #28667 added an atomic alias swap that folds the canonical concrete-index
delete into the _aliases request via a remove_index action. The action was built
as removeIndex(index).mustExist(false). OpenSearch's _aliases parser does not
accept must_exist on remove_index and rejects the whole request with
"[remove_index] unknown field [must_exist]" -> "[aliases] failed to parse field
[actions]" (HTTP 400), so the alias add in the same body never applies.
Elasticsearch tolerates the field, which is why it passed review.

On a fresh install every canonical *_search_index is a concrete index, so the
remove_index action fires for all entities and every swap fails -> no canonical
aliases are attached -> the canonical name resolves to nothing. This surfaced as
the AI Platform CAIP integration test failing with "table_search_index missing
embedding field".

Drop must_exist from the remove_index action in both OpenSearchIndexManager and
ElasticSearchIndexManager. It is unnecessary: resolveCanonicalRemoval only
forwards indices already confirmed to exist via indexExists(). Both engine paths
are now identical.

Add AliasSwapConcreteRemovalIT, which exercises the first-install shape (concrete
canonical in indicesToRemove) against the live cluster: it fails before this fix
on OpenSearch and passes after. The existing DefaultRecreateHandlerTest mocks
swapAliases, so it never exercised the request body where this bug lives.

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
mohityadav766 added a commit that referenced this pull request Jun 4, 2026
… succeeds (#28700)

PR #28667 added an atomic alias swap that folds the canonical concrete-index
delete into the _aliases request via a remove_index action. The action was built
as removeIndex(index).mustExist(false). OpenSearch's _aliases parser does not
accept must_exist on remove_index and rejects the whole request with
"[remove_index] unknown field [must_exist]" -> "[aliases] failed to parse field
[actions]" (HTTP 400), so the alias add in the same body never applies.
Elasticsearch tolerates the field, which is why it passed review.

On a fresh install every canonical *_search_index is a concrete index, so the
remove_index action fires for all entities and every swap fails -> no canonical
aliases are attached -> the canonical name resolves to nothing. This surfaced as
the AI Platform CAIP integration test failing with "table_search_index missing
embedding field".

Drop must_exist from the remove_index action in both OpenSearchIndexManager and
ElasticSearchIndexManager. It is unnecessary: resolveCanonicalRemoval only
forwards indices already confirmed to exist via indexExists(). Both engine paths
are now identical.

Add AliasSwapConcreteRemovalIT, which exercises the first-install shape (concrete
canonical in indicesToRemove) against the live cluster: it fails before this fix
on OpenSearch and passes after. The existing DefaultRecreateHandlerTest mocks
swapAliases, so it never exercised the request body where this bug lives.

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
(cherry picked from commit d800ba4)
mohityadav766 added a commit that referenced this pull request Jun 9, 2026
…ex atomically within the alias swap (#28667)

Cherry-picked from main (squash commit 70cad10) onto 1.12.11.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
mohityadav766 added a commit that referenced this pull request Jul 3, 2026
… succeeds (#28700) (#29723)

PR #28667 added an atomic alias swap that folds the canonical concrete-index
delete into the _aliases request via a remove_index action. The action was built
as removeIndex(index).mustExist(false). OpenSearch's _aliases parser does not
accept must_exist on remove_index and rejects the whole request with
"[remove_index] unknown field [must_exist]" -> "[aliases] failed to parse field
[actions]" (HTTP 400), so the alias add in the same body never applies.
Elasticsearch tolerates the field, which is why it passed review.

On a fresh install every canonical *_search_index is a concrete index, so the
remove_index action fires for all entities and every swap fails -> no canonical
aliases are attached -> the canonical name resolves to nothing. This surfaced as
the AI Platform CAIP integration test failing with "table_search_index missing
embedding field".

Drop must_exist from the remove_index action in both OpenSearchIndexManager and
ElasticSearchIndexManager. It is unnecessary: resolveCanonicalRemoval only
forwards indices already confirmed to exist via indexExists(). Both engine paths
are now identical.

Add AliasSwapConcreteRemovalIT, which exercises the first-install shape (concrete
canonical in indicesToRemove) against the live cluster: it fails before this fix
on OpenSearch and passes after. The existing DefaultRecreateHandlerTest mocks
swapAliases, so it never exercised the request body where this bug lives.


(cherry picked from commit d800ba4)

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

backend safe to test Add this label to run secure Github workflows on PRs To release Will cherry-pick this PR into the release branch

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants