Skip to content

Improve indexing - #26154

Merged
mohityadav766 merged 53 commits into
mainfrom
improve-indexing
Mar 3, 2026
Merged

mohityadav766 merged 53 commits into
mainfrom
improve-indexing

Conversation

@mohityadav766

@mohityadav766 mohityadav766 commented Feb 27, 2026 •

Copy link
Copy Markdown
Member

Describe your changes:

Fixes

I worked on ... because ...

Type of change:

  • Bug fix
  • Improvement
  • New feature
  • Breaking change (fix or feature that would cause existing functionality to not work as expected)
  • Documentation

Checklist:

  • I have read the CONTRIBUTING document.
  • My PR title is Fixes <issue-number>: <short explanation>
  • I have commented on my code, particularly in hard-to-understand areas.
  • For JSON Schema changes: I updated the migration scripts or explained why it is not needed.

Summary by Gitar

  • Resilience & Fault Tolerance:

    • Sliding-window circuit breaker prevents cascading failures when search backend experiences issues
    • Always-promote strategy ensures partially-indexed data is served rather than leaving index stale
    • Adaptive exponential backoff replaces fixed-delay retry loops
  • Performance Optimization:

    • Entity priority tiers ensure correct reindexing order (services → identity → assets → time-series)
    • Time-series filtering via timeSeriesMaxDays and timeSeriesEntityDays limits historical data reindexing
    • Batch size estimator auto-tunes document batch sizes based on entity type (large/small/medium)
  • Observability & Metrics:

    • ReindexingMetrics with Micrometer integration tracks job lifecycle, bulk requests, and circuit breaker state
    • DiagnosticsResource endpoint provides single-call performance snapshot (JVM, thread pool, DB connections, request latencies)
    • Extensible listener pattern for progress reporting
  • Distributed Execution:

    • Multi-server reindexing with fault tolerance and partition-based work distribution
    • Coordinate, plan, and monitor distributed jobs across OpenMetadata cluster
  • Load Testing Infrastructure:

    • perf-test.sh generates 50K–5M test entities with time-series and lineage data
    • DIAGNOSTICS.md and USAGE.md guide bottleneck analysis during high-load scenarios

This will update automatically on new commits.

…the existing reindexing atomic counters to Prometheus so operators can alert on failures, latency spikes, and backpressure without relying solely on database-flushed stats.

  - Add ReindexingMetrics singleton (initialize/getInstance pattern matching
    CacheMetrics) with job lifecycle counters, stage success/failed/warnings
    counters, bulk request timers with SLA buckets, payload size distribution,
    backpressure and promotion counters, and active/pending gauges
  - Register in MicrometerBundle after StreamableLogsMetrics
  - Instrument ReindexingOrchestrator.run() with job started/completed/failed/stopped
  - Bridge StageStatsTracker.flush() deltas to Prometheus per stage and entity type
  - Add bulk request latency timer and payload size recording in OpenSearchBulkSink
  - Record backpressure events in SearchIndexExecutor.handleBackpressure()
  - Record promotion success/failure in DefaultRecreateHandler
  - Add ReindexingMetricsTest with 24 tests covering all metric types
@github-actions

Copy link
Copy Markdown
Contributor

TypeScript types have been updated based on the JSON schema changes in the PR

if (state.compareAndSet(State.HALF_OPEN, State.OPEN)) {
openedAt = now;
LOG.warn("Circuit breaker transitioning HALF_OPEN → OPEN (probe failed)");
recordTransition("closed_to_open");

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Bug: Circuit breaker records wrong transition metric on probe failure

In BulkCircuitBreaker.recordFailure(), when the circuit breaker transitions from HALF_OPEN → OPEN (probe request failed), the metric recorded via recordTransition("closed_to_open") at line 81 is incorrect. It should be "half_open_to_open".

The log message on line 80 correctly describes the transition as "HALF_OPEN → OPEN (probe failed)", but the metric tag passed to ReindexingMetrics.recordCircuitBreakerTrip() says "closed_to_open", which:

  1. Inflates the closed_to_open transition count with events that are actually half_open_to_open
  2. Makes it impossible to distinguish between initial circuit opens and probe failures in monitoring dashboards
  3. Hides the probe failure rate, which is critical for diagnosing search cluster recovery issues

Suggested fix:

if (state.compareAndSet(State.HALF_OPEN, State.OPEN)) {
  openedAt = now;
  LOG.warn("Circuit breaker transitioning HALF_OPEN → OPEN (probe failed)");
  recordTransition("half_open_to_open");
}

Was this helpful? React with 👍 / 👎

harshach
harshach previously approved these changes Mar 3, 2026
@gitar-bot

gitar-bot Bot commented Mar 3, 2026 •

Copy link
Copy Markdown
🔍 CI failure analysis for 6c3becd: playwright-ci-postgresql (5, 6) on commit 6c3becd failed with flaky frontend tests - browser crashes, element visibility/content assertion failures. Same pre-existing test infrastructure pattern. All failures remain unrelated to PR's search indexing refactoring.

Issue 1: playwright-ci-postgresql (5, 6) - Flaky Frontend Tests (Latest Occurrence)

The playwright-ci-postgresql (5, 6) job (65530432145) failed on commit 6c3becd with test failures after 589 passed.

Root Cause

Errors:

  • page.waitForResponse: Target page, context or browser has been closed - Browser crashes
  • expect(locator).toContainText(expected) failed - Element text assertion failures
  • expect(locator).not.toBeVisible() failed - Element visibility assertion failures

Affected tests:

  • Pages-EntityDataConsumer - Tier - Add, Update and Remove
  • Pages-EntityDataConsumer - Description - Remove for child entities
  • Pages-EntityDataConsumer - SearchIndex - Remove for child entities

Details

This failure is unrelated to the PR changes. The PR modifies Java backend search indexing architecture but does not modify frontend test files or UI components.

The failures are characteristic of pre-existing flaky frontend tests:

  1. Browser crashes: "Target page, context or browser has been closed" errors
  2. Timing issues: Element content/visibility assertions failing intermittently
  3. Infrastructure instability: Test environment issues

Issue 2: Integration Tests - Vector Embedding Bugs (12+ Occurrences)

Vector embedding failures continue on every commit. Pre-existing systemic issue unrelated to PR.


Issues 3-5: Other Unrelated Failures

  1. maven-sonarcloud-ci - 44-53 test failures (fluctuating, from main merges)
  2. Integration Tests - Docker Network Failure (transient infrastructure)
  3. maven-collate-ci - Persistent Timeout (external repository)

Summary: All failures are unrelated to the PR's search indexing architecture refactoring.

Code Review ⚠️ Changes requested 13 resolved / 16 findings

Three previously identified important bugs remain unresolved: IndexingPipeline.initializeStats still reads totals from uninitialized stats (always 0), SingleServerIndexingStrategy still doesn't close the SearchIndexExecutor (despite it now implementing AutoCloseable), and DefaultRecreateHandler still deletes the canonical index before the alias swap.

⚠️ Bug: IndexingPipeline.initializeStats always sets entity totals to 0

📄 openmetadata-service/src/main/java/org/openmetadata/service/apps/bundles/searchIndex/IndexingPipeline.java:374 📄 openmetadata-service/src/main/java/org/openmetadata/service/apps/bundles/searchIndex/IndexingPipeline.java:400

initializeStats() (line 365) calls getTotalEntityRecords(entityType) for each entity type to determine how many records to index. But getTotalEntityRecords() (line 400) looks up the count from stats.get(), which hasn't been set yet — stats.set() is called at line 89 with the return value of initializeStats(), so during initialization stats.get() is null and the method returns 0 for every entity type.

This means all entity totalRecords are set to 0, and subsequently in readAllEntities() at line 165, getTotalEntityRecords(entityType) returns 0, causing entityReader.readEntity() to skip reading (it has a totalRecords <= 0 guard at line 103 of EntityReader). The pipeline would silently process zero entities.

The class is currently unreferenced (no usages found in the codebase), so this won't break anything today, but it will immediately fail when integrated.

The fix is to query the actual entity count from the database/search repository rather than self-referencing the stats object during initialization.

Suggested fix
  private Stats initializeStats(Set<String> entities) {
    Stats s = new Stats();
    s.setEntityStats(new org.openmetadata.schema.system.EntityStats());
    s.setJobStats(new StepStats());
    s.setReaderStats(new StepStats());
    s.setSinkStats(new StepStats());

    int total = 0;
    for (String entityType : entities) {
      int entityTotal = searchRepository.getEntityCount(entityType);
      total += entityTotal;
      StepStats es = new StepStats();
      es.setTotalRecords(entityTotal);
      es.setSuccessRecords(0);
      es.setFailedRecords(0);
      s.getEntityStats().getAdditionalProperties().put(entityType, es);
    }
⚠️ Bug: SearchIndexExecutor never closed: sink leak + missing finalize on failure

📄 openmetadata-service/src/main/java/org/openmetadata/service/apps/bundles/searchIndex/SearchIndexExecutor.java:244 📄 openmetadata-service/src/main/java/org/openmetadata/service/apps/bundles/searchIndex/SearchIndexExecutor.java:1924 📄 openmetadata-service/src/main/java/org/openmetadata/service/apps/bundles/searchIndex/SingleServerIndexingStrategy.java:23

When executeSingleServer() throws (e.g., during reIndexFromStartToEnd), the catch in execute() returns a FAILED result but never calls closeSinkIfNeeded() or finalizeReindex(). The close() method at line 1924 has the correct cleanup logic (closing the sink, flushing stats, and finalizing reindex), but neither SingleServerIndexingStrategy nor ReindexingOrchestrator ever invokes it.

This causes two problems on failure paths:

  1. Resource leak: The BulkSink (ElasticSearch/OpenSearch client connections, internal thread pools, semaphores) is never closed. These resources accumulate across failed reindex attempts.

  2. Always-promote strategy not applied: The cleanup() → finalizeReindex() chain that implements the "always-promote" strategy (promoting partial data from staged indices) is skipped. This directly contradicts the PR's stated design goal — partial data in staged indices is abandoned rather than promoted, and staged indices become orphaned.

The fix should ensure close() is called in a finally block or use try-with-resources on the executor.

Suggested fix
  // In SingleServerIndexingStrategy.execute():
  @Override
  public ExecutionResult execute(ReindexingConfiguration config, ReindexingJobContext context) {
    try {
      return executor.execute(config, context);
    } finally {
      executor.close();
    }
  }
⚠️ Edge Case: Index deletion before alias swap can leave entity unsearchable

📄 openmetadata-service/src/main/java/org/openmetadata/service/search/DefaultRecreateHandler.java:125 📄 openmetadata-service/src/main/java/org/openmetadata/service/search/DefaultRecreateHandler.java:279

Both finalizeReindex() (line 125-131) and promoteEntityIndex() (line 279-285) delete the canonical index before performing the atomic alias swap. If deleteIndexWithBackoff succeeds but the subsequent swapAliases call fails (e.g., due to a network issue or cluster timeout), the canonical index is already gone and the aliases aren't pointing to the new staged index.

The code at lines 136-141 / 290-298 returns early on swap failure without recovery, leaving the entity type completely unsearchable until the next successful reindex.

This edge case is partly inherent to the ES/OS limitation (can't have an index and alias with the same name), but can be mitigated:

  1. Before deleting the canonical index, verify the staged index is healthy
  2. Consider creating a temporary alias before deletion as a safety net
  3. At minimum, attempt to re-create the canonical index pointing to staged data if swap fails

In normal post-first-reindex operation this path shouldn't trigger (canonical name becomes an alias), but during initial setup or after manual index manipulation, it represents a data-loss risk.

✅ 13 resolved
✅ Bug: Poison pill offer() can silently fail, hanging consumers

📄 openmetadata-service/src/main/java/org/openmetadata/service/apps/bundles/searchIndex/IndexingPipeline.java:270 📄 openmetadata-service/src/main/java/org/openmetadata/service/apps/bundles/searchIndex/SearchIndexExecutor.java:777
In IndexingPipeline.signalConsumersToStop() (line 272), taskQueue.offer() is used to send poison pills to consumer threads. Since the queue is a bounded LinkedBlockingQueue, offer() returns false without blocking when the queue is full. If the queue is full at this point (e.g., because consumers are still processing a burst of tasks), poison pills are silently dropped, and the corresponding consumer threads will never receive the shutdown signal.

This causes the consumerLatch.await() at line 142 to block indefinitely, effectively deadlocking the pipeline. The consumer threads keep polling the queue, find no more tasks (producers are done), and loop forever on the 200ms poll timeout — they'll never see the poison pill that was dropped.

The same issue exists in SearchIndexExecutor.signalConsumersToStop() (line 780), though it at least logs the failure. Both should use put() (blocking) instead of offer(), or drain enough items first to guarantee space.

✅ Bug: jobExecutor thread pool not bounded by MAX_TOTAL_THREADS

📄 openmetadata-service/src/main/java/org/openmetadata/service/apps/bundles/searchIndex/SearchIndexExecutor.java:395 📄 openmetadata-service/src/main/java/org/openmetadata/service/apps/bundles/searchIndex/SearchIndexExecutor.java:418 📄 openmetadata-service/src/main/java/org/openmetadata/service/apps/bundles/searchIndex/IndexingPipeline.java:119
adjustThreadsForLimit() includes entityCount in the total when detecting the threshold breach, but only reduces numProducers and numConsumers — the entityCount itself is passed through unmodified to initializeQueueAndExecutors(), which creates jobExecutor with that full count.

For a full "all" reindex with 30+ entity types, the jobExecutor alone uses 30+ threads, plus reduced producer/consumer counts, easily exceeding MAX_TOTAL_THREADS = 50. The same unbounded pattern exists in IndexingPipeline.runPipeline() (line 119) where entities.size() is used directly without any cap.

Impact: On systems with many entity types, the total thread count can significantly exceed the intended limit, causing resource contention and potentially exhausting OS thread limits under concurrent reindexing jobs.

✅ Bug: Phaser parties leak in EntityReader if submitReaders throws

📄 openmetadata-service/src/main/java/org/openmetadata/service/apps/bundles/searchIndex/EntityReader.java:107 📄 openmetadata-service/src/main/java/org/openmetadata/service/apps/bundles/searchIndex/IndexingPipeline.java:198
In EntityReader.readEntity(), phaser.bulkRegister(numReaders) is called at line 109 before submitReaders(). If submitReaders() throws an exception (e.g., boundaryFinder.findBoundaries() fails on a DB call, or readerFactory.get() throws), the registered phaser parties are never deregistered.

The caller in IndexingPipeline.readAllEntities() (line 189-192) catches the exception and deregisters only the one party for the outer job task. The numReaders parties registered by bulkRegister become orphaned.

This causes producerPhaser.awaitAdvanceInterruptibly() at line 204 to loop indefinitely (with 1-second timeouts) until either stopped is set or the thread is interrupted, effectively hanging the pipeline on any transient error during reader setup.

Fix: Wrap the post-bulkRegister code in a try-catch that deregisters all registered parties on failure:

✅ Bug: Monitor task exception silently hangs distributed job forever

📄 openmetadata-service/src/main/java/org/openmetadata/service/apps/bundles/searchIndex/DistributedIndexingStrategy.java:229
In monitorDistributedJob(), the monitoring Runnable is submitted via scheduleAtFixedRate(). Per the ScheduledExecutorService contract, if any execution of the task throws an uncaught exception, all subsequent executions are silently suppressed.

The monitoring task calls distributedExecutor.getJobWithFreshStats() (line 238), which queries the database and can throw RuntimeException (e.g., database connectivity issues, SQL errors). It also calls updateStatsFromDistributedJob() (line 254), which internally queries searchIndexServerStatsDAO().getAggregatedStats() (line 280) — another database operation that can throw.

If either of these throws, the scheduled task is permanently cancelled. Since completionLatch.countDown() is never called, completionLatch.await() on line 260 blocks the thread indefinitely. The stop() method sets stopped = true, but the monitor task is no longer running to check this flag, so the job hangs forever with no way to recover except killing the JVM.

Impact: Any transient database error during distributed job monitoring permanently hangs the reindexing thread, requiring a server restart to recover.

✅ Bug: Semaphore leak when circuit breaker prevents retry

📄 openmetadata-service/src/main/java/org/openmetadata/service/apps/bundles/searchIndex/ElasticSearchBulkSink.java:738 📄 openmetadata-service/src/main/java/org/openmetadata/service/apps/bundles/searchIndex/ElasticSearchBulkSink.java:756
In ElasticSearchBulkSink.CustomBulkProcessor.executeBulkWithRetry(), there is a resource leak path when the circuit breaker prevents a retry. The finally block (line 738-745) decides whether to release the semaphore and decrement activeBulkRequests based on shouldRetry(attemptNumber, error). However, handleBulkFailure() has an additional condition: circuitBreaker.getState() != OPEN.

When shouldRetry() returns true (retryable error, within max attempts) but the circuit breaker is OPEN, handleBulkFailure does NOT schedule a retry (falls to the else branch at line 770). But the finally block still thinks a retry is happening and skips releasing the semaphore/decrementing activeBulkRequests.

This means:

  • The concurrentRequestSemaphore permit is permanently lost
  • activeBulkRequests is permanently incremented
  • Over time, the semaphore is exhausted, blocking flushInternal() at line 671 forever
  • flushAndWait() / awaitClose() will also hang since activeBulkRequests never reaches 0

This can cause the entire indexing pipeline to deadlock under circuit breaker conditions — exactly when resilience is most needed.

The same pattern likely exists in OpenSearchBulkSink.

...and 8 more resolved from earlier reviews

🤖 Prompt for agents
Code Review: Three previously identified important bugs remain unresolved: IndexingPipeline.initializeStats still reads totals from uninitialized stats (always 0), SingleServerIndexingStrategy still doesn't close the SearchIndexExecutor (despite it now implementing AutoCloseable), and DefaultRecreateHandler still deletes the canonical index before the alias swap.

1. ⚠️ Bug: IndexingPipeline.initializeStats always sets entity totals to 0
   Files: openmetadata-service/src/main/java/org/openmetadata/service/apps/bundles/searchIndex/IndexingPipeline.java:374, openmetadata-service/src/main/java/org/openmetadata/service/apps/bundles/searchIndex/IndexingPipeline.java:400

   `initializeStats()` (line 365) calls `getTotalEntityRecords(entityType)` for each entity type to determine how many records to index. But `getTotalEntityRecords()` (line 400) looks up the count from `stats.get()`, which hasn't been set yet — `stats.set()` is called at line 89 with the return value of `initializeStats()`, so during initialization `stats.get()` is null and the method returns 0 for every entity type.
   
   This means all entity `totalRecords` are set to 0, and subsequently in `readAllEntities()` at line 165, `getTotalEntityRecords(entityType)` returns 0, causing `entityReader.readEntity()` to skip reading (it has a `totalRecords <= 0` guard at line 103 of EntityReader). The pipeline would silently process zero entities.
   
   The class is currently unreferenced (no usages found in the codebase), so this won't break anything today, but it will immediately fail when integrated.
   
   The fix is to query the actual entity count from the database/search repository rather than self-referencing the stats object during initialization.

   Suggested fix:
   private Stats initializeStats(Set<String> entities) {
     Stats s = new Stats();
     s.setEntityStats(new org.openmetadata.schema.system.EntityStats());
     s.setJobStats(new StepStats());
     s.setReaderStats(new StepStats());
     s.setSinkStats(new StepStats());
   
     int total = 0;
     for (String entityType : entities) {
       int entityTotal = searchRepository.getEntityCount(entityType);
       total += entityTotal;
       StepStats es = new StepStats();
       es.setTotalRecords(entityTotal);
       es.setSuccessRecords(0);
       es.setFailedRecords(0);
       s.getEntityStats().getAdditionalProperties().put(entityType, es);
     }

2. ⚠️ Bug: SearchIndexExecutor never closed: sink leak + missing finalize on failure
   Files: openmetadata-service/src/main/java/org/openmetadata/service/apps/bundles/searchIndex/SearchIndexExecutor.java:244, openmetadata-service/src/main/java/org/openmetadata/service/apps/bundles/searchIndex/SearchIndexExecutor.java:1924, openmetadata-service/src/main/java/org/openmetadata/service/apps/bundles/searchIndex/SingleServerIndexingStrategy.java:23

   When `executeSingleServer()` throws (e.g., during `reIndexFromStartToEnd`), the catch in `execute()` returns a FAILED result but never calls `closeSinkIfNeeded()` or `finalizeReindex()`. The `close()` method at line 1924 has the correct cleanup logic (closing the sink, flushing stats, and finalizing reindex), but neither `SingleServerIndexingStrategy` nor `ReindexingOrchestrator` ever invokes it.
   
   This causes two problems on failure paths:
   
   1. **Resource leak**: The `BulkSink` (ElasticSearch/OpenSearch client connections, internal thread pools, semaphores) is never closed. These resources accumulate across failed reindex attempts.
   
   2. **Always-promote strategy not applied**: The `cleanup()` → `finalizeReindex()` chain that implements the "always-promote" strategy (promoting partial data from staged indices) is skipped. This directly contradicts the PR's stated design goal — partial data in staged indices is abandoned rather than promoted, and staged indices become orphaned.
   
   The fix should ensure `close()` is called in a `finally` block or use try-with-resources on the executor.

   Suggested fix:
   // In SingleServerIndexingStrategy.execute():
   @Override
   public ExecutionResult execute(ReindexingConfiguration config, ReindexingJobContext context) {
     try {
       return executor.execute(config, context);
     } finally {
       executor.close();
     }
   }

3. ⚠️ Edge Case: Index deletion before alias swap can leave entity unsearchable
   Files: openmetadata-service/src/main/java/org/openmetadata/service/search/DefaultRecreateHandler.java:125, openmetadata-service/src/main/java/org/openmetadata/service/search/DefaultRecreateHandler.java:279

   Both `finalizeReindex()` (line 125-131) and `promoteEntityIndex()` (line 279-285) delete the canonical index *before* performing the atomic alias swap. If `deleteIndexWithBackoff` succeeds but the subsequent `swapAliases` call fails (e.g., due to a network issue or cluster timeout), the canonical index is already gone and the aliases aren't pointing to the new staged index.
   
   The code at lines 136-141 / 290-298 returns early on swap failure without recovery, leaving the entity type completely unsearchable until the next successful reindex.
   
   This edge case is partly inherent to the ES/OS limitation (can't have an index and alias with the same name), but can be mitigated:
   1. Before deleting the canonical index, verify the staged index is healthy
   2. Consider creating a temporary alias before deletion as a safety net
   3. At minimum, attempt to re-create the canonical index pointing to staged data if swap fails
   
   In normal post-first-reindex operation this path shouldn't trigger (canonical name becomes an alias), but during initial setup or after manual index manipulation, it represents a data-loss risk.

Tip

Comment Gitar fix CI or enable auto-apply: gitar auto-apply:on

Options

Auto-apply is off → Gitar will not commit updates to this branch.
Display: compact → Showing less information.

Comment with these commands to change:

Auto-apply Compact
gitar auto-apply:on         
gitar display:verbose         

Was this helpful? React with 👍 / 👎 | Gitar

}

@Override
public ExecutionResult execute(ReindexingConfiguration config, ReindexingJobContext context) {

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Bug: SearchIndexExecutor never closed: sink leak + missing finalize on failure

When executeSingleServer() throws (e.g., during reIndexFromStartToEnd), the catch in execute() returns a FAILED result but never calls closeSinkIfNeeded() or finalizeReindex(). The close() method at line 1924 has the correct cleanup logic (closing the sink, flushing stats, and finalizing reindex), but neither SingleServerIndexingStrategy nor ReindexingOrchestrator ever invokes it.

This causes two problems on failure paths:

  1. Resource leak: The BulkSink (ElasticSearch/OpenSearch client connections, internal thread pools, semaphores) is never closed. These resources accumulate across failed reindex attempts.

  2. Always-promote strategy not applied: The cleanup() → finalizeReindex() chain that implements the "always-promote" strategy (promoting partial data from staged indices) is skipped. This directly contradicts the PR's stated design goal — partial data in staged indices is abandoned rather than promoted, and staged indices become orphaned.

The fix should ensure close() is called in a finally block or use try-with-resources on the executor.

Suggested fix:

// In SingleServerIndexingStrategy.execute():
@Override
public ExecutionResult execute(ReindexingConfiguration config, ReindexingJobContext context) {
  try {
    return executor.execute(config, context);
  } finally {
    executor.close();
  }
}

Was this helpful? React with 👍 / 👎 | Reply gitar fix to apply this suggestion

oldIndicesToDelete,
stagedIndex);

if (oldIndicesToDelete.contains(canonicalIndex)) {

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Edge Case: Index deletion before alias swap can leave entity unsearchable

Both finalizeReindex() (line 125-131) and promoteEntityIndex() (line 279-285) delete the canonical index before performing the atomic alias swap. If deleteIndexWithBackoff succeeds but the subsequent swapAliases call fails (e.g., due to a network issue or cluster timeout), the canonical index is already gone and the aliases aren't pointing to the new staged index.

The code at lines 136-141 / 290-298 returns early on swap failure without recovery, leaving the entity type completely unsearchable until the next successful reindex.

This edge case is partly inherent to the ES/OS limitation (can't have an index and alias with the same name), but can be mitigated:

  1. Before deleting the canonical index, verify the staged index is healthy
  2. Consider creating a temporary alias before deletion as a safety net
  3. At minimum, attempt to re-create the canonical index pointing to staged data if swap fails

In normal post-first-reindex operation this path shouldn't trigger (canonical name becomes an alias), but during initial setup or after manual index manipulation, it represents a data-loss risk.

Was this helpful? React with 👍 / 👎 | Reply gitar fix to apply this suggestion

@sonarqubecloud

sonarqubecloud Bot commented Mar 3, 2026

Copy link
Copy Markdown

@sonarqubecloud

sonarqubecloud Bot commented Mar 3, 2026

Copy link
Copy Markdown

This branch had an error being deployed

1 failed deployment
test — 6c3becd7 Deployed Mar 3, 2026 by mohityadav766 via playwright-ci-postgresql (4, 6) #35738
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.

[Search Indexing] Comprehensive indexing resilience, performance, and observability overhaul (#26154)

3 participants