mirror of
				https://github.com/open-metadata/OpenMetadata.git
				synced 2025-10-31 02:29:03 +00:00 
			
		
		
		
	Improvements: SearchReindexApp, recreate indexes should swap immediately after indexing is done (#23749)
* Improvements: SearchReindexApp, recreate indexes should swap immediately after indexing is done * Fix search index with alias configured
This commit is contained in:
		
							parent
							
								
									9ba65ac0d2
								
							
						
					
					
						commit
						c96e39fb5f
					
				| @ -374,6 +374,21 @@ public class SearchIndexApp extends AbstractNativeApplication { | |||||||
|     return Optional.empty(); |     return Optional.empty(); | ||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
|  |   private void finalizeEntityIndex(String entityType, boolean success) { | ||||||
|  |     if (recreateIndexHandler == null | ||||||
|  |         || recreateContext == null | ||||||
|  |         || !Boolean.TRUE.equals(jobData.getRecreateIndex())) { | ||||||
|  |       return; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     try { | ||||||
|  |       recreateIndexHandler.finalizeEntityReindex(recreateContext, entityType, success); | ||||||
|  |       LOG.info("Finalized index for entity '{}' with success={}", entityType, success); | ||||||
|  |     } catch (Exception ex) { | ||||||
|  |       LOG.error("Failed to finalize index for entity '{}'", entityType, ex); | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|   private void finalizeRecreateIndexes(boolean success) { |   private void finalizeRecreateIndexes(boolean success) { | ||||||
|     if (recreateIndexHandler == null || recreateContext == null) { |     if (recreateIndexHandler == null || recreateContext == null) { | ||||||
|       return; |       return; | ||||||
| @ -878,27 +893,52 @@ public class SearchIndexApp extends AbstractNativeApplication { | |||||||
|       int loadPerThread = calculateNumberOfThreads(totalEntityRecords); |       int loadPerThread = calculateNumberOfThreads(totalEntityRecords); | ||||||
| 
 | 
 | ||||||
|       if (totalEntityRecords > 0) { |       if (totalEntityRecords > 0) { | ||||||
|         submitBatchTasks(entityType, loadPerThread, producerLatch); |         // Create entity-specific latch to wait for all batches of this entity | ||||||
|  |         CountDownLatch entityLatch = new CountDownLatch(loadPerThread); | ||||||
|  |         submitBatchTasks(entityType, loadPerThread, producerLatch, entityLatch); | ||||||
|  | 
 | ||||||
|  |         // Wait for all batches of this entity to complete | ||||||
|  |         try { | ||||||
|  |           entityLatch.await(); | ||||||
|  |         } catch (InterruptedException e) { | ||||||
|  |           Thread.currentThread().interrupt(); | ||||||
|  |           LOG.warn("Interrupted while waiting for entity '{}' batches to complete", entityType); | ||||||
|  |           finalizeEntityIndex(entityType, false); | ||||||
|  |           return; | ||||||
|  |         } | ||||||
|       } |       } | ||||||
| 
 | 
 | ||||||
|       if (jobLogger != null) { |       if (jobLogger != null) { | ||||||
|         jobLogger.markEntityCompleted(entityType); |         jobLogger.markEntityCompleted(entityType); | ||||||
|       } |       } | ||||||
|  | 
 | ||||||
|  |       // Finalize index for this entity immediately after all its batches complete | ||||||
|  |       finalizeEntityIndex(entityType, true); | ||||||
|     } catch (Exception e) { |     } catch (Exception e) { | ||||||
|       LOG.error("Error processing entity type {}", entityType, e); |       LOG.error("Error processing entity type {}", entityType, e); | ||||||
|  |       // Cleanup staged index on failure | ||||||
|  |       finalizeEntityIndex(entityType, false); | ||||||
|     } |     } | ||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
|   private void submitBatchTasks( |   private void submitBatchTasks( | ||||||
|       String entityType, int loadPerThread, CountDownLatch producerLatch) { |       String entityType, | ||||||
|  |       int loadPerThread, | ||||||
|  |       CountDownLatch producerLatch, | ||||||
|  |       CountDownLatch entityLatch) { | ||||||
|     for (int i = 0; i < loadPerThread; i++) { |     for (int i = 0; i < loadPerThread; i++) { | ||||||
|       LOG.debug("Submitting virtual thread producer task for batch {}/{}", i + 1, loadPerThread); |       LOG.debug("Submitting virtual thread producer task for batch {}/{}", i + 1, loadPerThread); | ||||||
|       int currentOffset = i * batchSize.get(); |       int currentOffset = i * batchSize.get(); | ||||||
|       producerExecutor.submit(() -> processBatch(entityType, currentOffset, producerLatch)); |       producerExecutor.submit( | ||||||
|  |           () -> processBatch(entityType, currentOffset, producerLatch, entityLatch)); | ||||||
|     } |     } | ||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
|   private void processBatch(String entityType, int currentOffset, CountDownLatch producerLatch) { |   private void processBatch( | ||||||
|  |       String entityType, | ||||||
|  |       int currentOffset, | ||||||
|  |       CountDownLatch producerLatch, | ||||||
|  |       CountDownLatch entityLatch) { | ||||||
|     try { |     try { | ||||||
|       if (shouldSkipBatch()) { |       if (shouldSkipBatch()) { | ||||||
|         return; |         return; | ||||||
| @ -917,6 +957,9 @@ public class SearchIndexApp extends AbstractNativeApplication { | |||||||
|     } finally { |     } finally { | ||||||
|       LOG.debug("Virtual thread completed batch, remaining: {}", producerLatch.getCount() - 1); |       LOG.debug("Virtual thread completed batch, remaining: {}", producerLatch.getCount() - 1); | ||||||
|       producerLatch.countDown(); |       producerLatch.countDown(); | ||||||
|  |       if (entityLatch != null) { | ||||||
|  |         entityLatch.countDown(); | ||||||
|  |       } | ||||||
|     } |     } | ||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
|  | |||||||
| @ -87,6 +87,41 @@ public class DefaultRecreateHandler implements RecreateIndexHandler { | |||||||
|     return context; |     return context; | ||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
|  |   @Override | ||||||
|  |   public void finalizeEntityReindex(ReindexContext context, String entityType, boolean success) { | ||||||
|  |     if (context == null || entityType == null) { | ||||||
|  |       return; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     // Check if already finalized | ||||||
|  |     if (context.isFinalized(entityType)) { | ||||||
|  |       LOG.debug("Entity '{}' already finalized, skipping", entityType); | ||||||
|  |       return; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     String canonicalIndex = context.getCanonicalIndex(entityType).orElse(null); | ||||||
|  |     String activeIndex = context.getOriginalIndex(entityType).orElse(null); | ||||||
|  |     String stagedIndex = context.getStagedIndex(entityType).orElse(null); | ||||||
|  | 
 | ||||||
|  |     if (canonicalIndex == null || stagedIndex == null) { | ||||||
|  |       LOG.debug("Skipping finalization for entity '{}' - missing index information", entityType); | ||||||
|  |       return; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     SearchRepository searchRepository = Entity.getSearchRepository(); | ||||||
|  |     SearchClient<?> searchClient = searchRepository.getSearchClient(); | ||||||
|  | 
 | ||||||
|  |     if (success) { | ||||||
|  |       promoteIndexForEntity( | ||||||
|  |           context, entityType, canonicalIndex, activeIndex, stagedIndex, searchClient); | ||||||
|  |     } else { | ||||||
|  |       cleanupStagedIndexForEntity(entityType, stagedIndex, searchClient); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     // Mark as finalized | ||||||
|  |     context.markFinalized(entityType); | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|   @Override |   @Override | ||||||
|   public void finalizeReindex(ReindexContext context, boolean success) { |   public void finalizeReindex(ReindexContext context, boolean success) { | ||||||
|     if (context == null || context.isEmpty()) { |     if (context == null || context.isEmpty()) { | ||||||
| @ -97,6 +132,13 @@ public class DefaultRecreateHandler implements RecreateIndexHandler { | |||||||
|     SearchClient<?> searchClient = searchRepository.getSearchClient(); |     SearchClient<?> searchClient = searchRepository.getSearchClient(); | ||||||
| 
 | 
 | ||||||
|     for (String entityType : context.getEntities()) { |     for (String entityType : context.getEntities()) { | ||||||
|  |       // Skip if already finalized per-entity | ||||||
|  |       if (context.isFinalized(entityType)) { | ||||||
|  |         LOG.debug( | ||||||
|  |             "Entity '{}' already finalized per-entity, skipping in batch finalization", entityType); | ||||||
|  |         continue; | ||||||
|  |       } | ||||||
|  | 
 | ||||||
|       String canonicalIndex = context.getCanonicalIndex(entityType).orElse(null); |       String canonicalIndex = context.getCanonicalIndex(entityType).orElse(null); | ||||||
|       String activeIndex = context.getOriginalIndex(entityType).orElse(null); |       String activeIndex = context.getOriginalIndex(entityType).orElse(null); | ||||||
|       String stagedIndex = context.getStagedIndex(entityType).orElse(null); |       String stagedIndex = context.getStagedIndex(entityType).orElse(null); | ||||||
| @ -106,11 +148,35 @@ public class DefaultRecreateHandler implements RecreateIndexHandler { | |||||||
|       } |       } | ||||||
| 
 | 
 | ||||||
|       if (success) { |       if (success) { | ||||||
|  |         promoteIndexForEntity( | ||||||
|  |             context, entityType, canonicalIndex, activeIndex, stagedIndex, searchClient); | ||||||
|  |       } else { | ||||||
|  |         cleanupStagedIndexForEntity(entityType, stagedIndex, searchClient); | ||||||
|  |       } | ||||||
|  | 
 | ||||||
|  |       // Mark as finalized | ||||||
|  |       context.markFinalized(entityType); | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   private void promoteIndexForEntity( | ||||||
|  |       ReindexContext context, | ||||||
|  |       String entityType, | ||||||
|  |       String canonicalIndex, | ||||||
|  |       String activeIndex, | ||||||
|  |       String stagedIndex, | ||||||
|  |       SearchClient<?> searchClient) { | ||||||
|     try { |     try { | ||||||
|       Set<String> aliasesToAttach = new HashSet<>(); |       Set<String> aliasesToAttach = new HashSet<>(); | ||||||
|       aliasesToAttach.addAll(context.getExistingAliases(entityType)); |       aliasesToAttach.addAll(context.getExistingAliases(entityType)); | ||||||
|       context.getCanonicalAlias(entityType).ifPresent(aliasesToAttach::add); |       context.getCanonicalAlias(entityType).ifPresent(aliasesToAttach::add); | ||||||
|  | 
 | ||||||
|  |       // Add canonical index name as an alias so queries using the full index name still work | ||||||
|  |       // But only if no index exists with that name | ||||||
|  |       if (!searchClient.indexExists(canonicalIndex)) { | ||||||
|         aliasesToAttach.add(canonicalIndex); |         aliasesToAttach.add(canonicalIndex); | ||||||
|  |       } | ||||||
|  | 
 | ||||||
|       List<String> parentAliases = context.getParentAliases(entityType); |       List<String> parentAliases = context.getParentAliases(entityType); | ||||||
|       if (parentAliases != null) { |       if (parentAliases != null) { | ||||||
|         parentAliases.stream() |         parentAliases.stream() | ||||||
| @ -169,7 +235,10 @@ public class DefaultRecreateHandler implements RecreateIndexHandler { | |||||||
|       LOG.error( |       LOG.error( | ||||||
|           "Failed to promote staged index '{}' for entity '{}'.", stagedIndex, entityType, ex); |           "Failed to promote staged index '{}' for entity '{}'.", stagedIndex, entityType, ex); | ||||||
|     } |     } | ||||||
|       } else { |   } | ||||||
|  | 
 | ||||||
|  |   private void cleanupStagedIndexForEntity( | ||||||
|  |       String entityType, String stagedIndex, SearchClient<?> searchClient) { | ||||||
|     try { |     try { | ||||||
|       if (searchClient.indexExists(stagedIndex)) { |       if (searchClient.indexExists(stagedIndex)) { | ||||||
|         searchClient.deleteIndex(stagedIndex); |         searchClient.deleteIndex(stagedIndex); | ||||||
| @ -186,8 +255,6 @@ public class DefaultRecreateHandler implements RecreateIndexHandler { | |||||||
|           ex); |           ex); | ||||||
|     } |     } | ||||||
|   } |   } | ||||||
|     } |  | ||||||
|   } |  | ||||||
| 
 | 
 | ||||||
|   private String buildStagedIndexName(String originalIndexName) { |   private String buildStagedIndexName(String originalIndexName) { | ||||||
|     return String.format("%s_rebuild_%d", originalIndexName, System.currentTimeMillis()); |     return String.format("%s_rebuild_%d", originalIndexName, System.currentTimeMillis()); | ||||||
|  | |||||||
| @ -17,6 +17,16 @@ public interface RecreateIndexHandler { | |||||||
| 
 | 
 | ||||||
|   default void finalizeReindex(ReindexContext context, boolean success) {} |   default void finalizeReindex(ReindexContext context, boolean success) {} | ||||||
| 
 | 
 | ||||||
|  |   /** | ||||||
|  |    * Finalize reindex for a specific entity. This allows per-entity index swapping | ||||||
|  |    * to reduce memory footprint during reindexing. | ||||||
|  |    * | ||||||
|  |    * @param context The reindex context containing staged index information | ||||||
|  |    * @param entityType The entity type to finalize | ||||||
|  |    * @param success Whether the reindexing was successful for this entity | ||||||
|  |    */ | ||||||
|  |   default void finalizeEntityReindex(ReindexContext context, String entityType, boolean success) {} | ||||||
|  | 
 | ||||||
|   class ReindexContext { |   class ReindexContext { | ||||||
|     private final Map<String, String> canonicalIndexByEntity = new HashMap<>(); |     private final Map<String, String> canonicalIndexByEntity = new HashMap<>(); | ||||||
|     private final Map<String, String> originalIndexByEntity = new HashMap<>(); |     private final Map<String, String> originalIndexByEntity = new HashMap<>(); | ||||||
| @ -24,6 +34,7 @@ public interface RecreateIndexHandler { | |||||||
|     private final Map<String, Set<String>> existingAliasesByEntity = new HashMap<>(); |     private final Map<String, Set<String>> existingAliasesByEntity = new HashMap<>(); | ||||||
|     private final Map<String, String> canonicalAliasByEntity = new HashMap<>(); |     private final Map<String, String> canonicalAliasByEntity = new HashMap<>(); | ||||||
|     private final Map<String, List<String>> parentAliasesByEntity = new HashMap<>(); |     private final Map<String, List<String>> parentAliasesByEntity = new HashMap<>(); | ||||||
|  |     private final Set<String> finalizedEntities = new HashSet<>(); | ||||||
| 
 | 
 | ||||||
|     public void add( |     public void add( | ||||||
|         String entity, |         String entity, | ||||||
| @ -42,6 +53,14 @@ public interface RecreateIndexHandler { | |||||||
|       parentAliasesByEntity.put(entity, parentAliases != null ? parentAliases : List.of()); |       parentAliasesByEntity.put(entity, parentAliases != null ? parentAliases : List.of()); | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|  |     public synchronized void markFinalized(String entity) { | ||||||
|  |       finalizedEntities.add(entity); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     public synchronized boolean isFinalized(String entity) { | ||||||
|  |       return finalizedEntities.contains(entity); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|     public Optional<String> getCanonicalIndex(String entity) { |     public Optional<String> getCanonicalIndex(String entity) { | ||||||
|       return Optional.ofNullable(canonicalIndexByEntity.get(entity)); |       return Optional.ofNullable(canonicalIndexByEntity.get(entity)); | ||||||
|     } |     } | ||||||
|  | |||||||
| @ -2,6 +2,7 @@ package org.openmetadata.service.apps.bundles.searchIndex; | |||||||
| 
 | 
 | ||||||
| import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; | import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; | ||||||
| import static org.junit.jupiter.api.Assertions.assertEquals; | import static org.junit.jupiter.api.Assertions.assertEquals; | ||||||
|  | import static org.junit.jupiter.api.Assertions.assertFalse; | ||||||
| import static org.junit.jupiter.api.Assertions.assertInstanceOf; | import static org.junit.jupiter.api.Assertions.assertInstanceOf; | ||||||
| import static org.junit.jupiter.api.Assertions.assertNotNull; | import static org.junit.jupiter.api.Assertions.assertNotNull; | ||||||
| import static org.junit.jupiter.api.Assertions.assertTrue; | import static org.junit.jupiter.api.Assertions.assertTrue; | ||||||
| @ -322,6 +323,7 @@ class SearchIndexAppTest extends OpenMetadataApplicationTest { | |||||||
|             .withName("SearchIndexingApplication") |             .withName("SearchIndexingApplication") | ||||||
|             .withAppConfiguration(JsonUtils.convertValue(testJobData, Object.class)); |             .withAppConfiguration(JsonUtils.convertValue(testJobData, Object.class)); | ||||||
|     searchIndexApp.init(testApp); |     searchIndexApp.init(testApp); | ||||||
|  |     searchIndexApp.getJobData().setStatus(EventPublisherJob.Status.RUNNING); | ||||||
|     injectMockSink(); |     injectMockSink(); | ||||||
| 
 | 
 | ||||||
|     List<EntityError> entityErrors = |     List<EntityError> entityErrors = | ||||||
| @ -352,11 +354,7 @@ class SearchIndexAppTest extends OpenMetadataApplicationTest { | |||||||
|     List<EntityInterface> entities = List.of(mockEntity, mockEntity); |     List<EntityInterface> entities = List.of(mockEntity, mockEntity); | ||||||
|     ResultList<EntityInterface> resultList = new ResultList<>(entities, null, null, 2); |     ResultList<EntityInterface> resultList = new ResultList<>(entities, null, null, 2); | ||||||
| 
 | 
 | ||||||
|     Map<String, Object> contextData = new HashMap<>(); |     lenient().doThrow(searchIndexException).when(mockSink).write(eq(entities), any(Map.class)); | ||||||
|     contextData.put("entityType", "table"); |  | ||||||
|     contextData.put("recreateIndex", false); |  | ||||||
| 
 |  | ||||||
|     lenient().doThrow(searchIndexException).when(mockSink).write(eq(entities), eq(contextData)); |  | ||||||
| 
 | 
 | ||||||
|     SearchIndexApp.IndexingTask<EntityInterface> task = |     SearchIndexApp.IndexingTask<EntityInterface> task = | ||||||
|         new SearchIndexApp.IndexingTask<>("table", resultList, 0); |         new SearchIndexApp.IndexingTask<>("table", resultList, 0); | ||||||
| @ -1044,6 +1042,379 @@ class SearchIndexAppTest extends OpenMetadataApplicationTest { | |||||||
|     assertEquals(1000000L, jobData.getPayLoadSize()); |     assertEquals(1000000L, jobData.getPayLoadSize()); | ||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
|  |   @Test | ||||||
|  |   void testPerEntityIndexFinalization() { | ||||||
|  |     SearchRepository searchRepo = Entity.getSearchRepository(); | ||||||
|  |     SearchClient<?> searchClient = searchRepo.getSearchClient(); | ||||||
|  |     String clusterAlias = searchRepo.getClusterAlias(); | ||||||
|  | 
 | ||||||
|  |     // Create test indexes | ||||||
|  |     String stagedIndex = "test_table_search_index_rebuild_" + System.currentTimeMillis(); | ||||||
|  |     String oldIndex = "test_table_search_index_old_" + System.currentTimeMillis(); | ||||||
|  |     String canonicalIndex = "test_table_search_index"; | ||||||
|  | 
 | ||||||
|  |     try { | ||||||
|  |       // Setup: Create the old index with aliases | ||||||
|  |       searchClient.createIndex(oldIndex, "{}"); | ||||||
|  |       searchClient.addAliases(oldIndex, Set.of("test_table", canonicalIndex, "test_all")); | ||||||
|  | 
 | ||||||
|  |       // Create the staged index | ||||||
|  |       searchClient.createIndex(stagedIndex, "{}"); | ||||||
|  | 
 | ||||||
|  |       RecreateIndexHandler.ReindexContext context = new RecreateIndexHandler.ReindexContext(); | ||||||
|  |       context.add( | ||||||
|  |           "test_table", | ||||||
|  |           canonicalIndex, | ||||||
|  |           oldIndex, | ||||||
|  |           stagedIndex, | ||||||
|  |           Set.of("test_table", canonicalIndex, "test_all"), | ||||||
|  |           "test_table", | ||||||
|  |           List.of("test_all")); | ||||||
|  | 
 | ||||||
|  |       DefaultRecreateHandler handler = new DefaultRecreateHandler(); | ||||||
|  | 
 | ||||||
|  |       // Finalize just the table entity | ||||||
|  |       handler.finalizeEntityReindex(context, "test_table", true); | ||||||
|  | 
 | ||||||
|  |       // Verify the staged index was promoted | ||||||
|  |       assertFalse(searchClient.indexExists(oldIndex), "Old index should be deleted"); | ||||||
|  |       assertTrue(searchClient.indexExists(stagedIndex), "Staged index should exist"); | ||||||
|  | 
 | ||||||
|  |       Set<String> stagedAliases = searchClient.getAliases(stagedIndex); | ||||||
|  |       assertTrue(stagedAliases.contains("test_table")); | ||||||
|  |       assertTrue(stagedAliases.contains(canonicalIndex)); | ||||||
|  |       assertTrue(stagedAliases.contains("test_all")); | ||||||
|  | 
 | ||||||
|  |       // Verify the entity is marked as finalized | ||||||
|  |       assertTrue(context.isFinalized("test_table")); | ||||||
|  |     } finally { | ||||||
|  |       // Cleanup | ||||||
|  |       if (searchClient.indexExists(stagedIndex)) { | ||||||
|  |         searchClient.deleteIndex(stagedIndex); | ||||||
|  |       } | ||||||
|  |       if (searchClient.indexExists(oldIndex)) { | ||||||
|  |         searchClient.deleteIndex(oldIndex); | ||||||
|  |       } | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   @Test | ||||||
|  |   void testFinalizedEntitiesNotReprocessed() { | ||||||
|  |     SearchRepository searchRepo = Entity.getSearchRepository(); | ||||||
|  |     SearchClient<?> searchClient = searchRepo.getSearchClient(); | ||||||
|  | 
 | ||||||
|  |     long timestamp = System.currentTimeMillis(); | ||||||
|  |     String tableStaged = "test_table_staged_" + timestamp; | ||||||
|  |     String tableOld = "test_table_old_" + timestamp; | ||||||
|  |     String userStaged = "test_user_staged_" + timestamp; | ||||||
|  |     String userOld = "test_user_old_" + timestamp; | ||||||
|  | 
 | ||||||
|  |     try { | ||||||
|  |       // Create test indexes | ||||||
|  |       searchClient.createIndex(tableOld, "{}"); | ||||||
|  |       searchClient.addAliases(tableOld, Set.of("test_table_alias")); | ||||||
|  |       searchClient.createIndex(tableStaged, "{}"); | ||||||
|  | 
 | ||||||
|  |       searchClient.createIndex(userOld, "{}"); | ||||||
|  |       searchClient.addAliases(userOld, Set.of("test_user_alias")); | ||||||
|  |       searchClient.createIndex(userStaged, "{}"); | ||||||
|  | 
 | ||||||
|  |       RecreateIndexHandler.ReindexContext context = new RecreateIndexHandler.ReindexContext(); | ||||||
|  |       context.add( | ||||||
|  |           "test_table", | ||||||
|  |           "test_table_index", | ||||||
|  |           tableOld, | ||||||
|  |           tableStaged, | ||||||
|  |           Set.of("test_table_alias"), | ||||||
|  |           "test_table", | ||||||
|  |           List.of()); | ||||||
|  |       context.add( | ||||||
|  |           "test_user", | ||||||
|  |           "test_user_index", | ||||||
|  |           userOld, | ||||||
|  |           userStaged, | ||||||
|  |           Set.of("test_user_alias"), | ||||||
|  |           "test_user", | ||||||
|  |           List.of()); | ||||||
|  | 
 | ||||||
|  |       DefaultRecreateHandler handler = new DefaultRecreateHandler(); | ||||||
|  | 
 | ||||||
|  |       // Finalize table entity first | ||||||
|  |       handler.finalizeEntityReindex(context, "test_table", true); | ||||||
|  |       assertTrue(context.isFinalized("test_table")); | ||||||
|  |       assertFalse(searchClient.indexExists(tableOld), "Table old index should be deleted"); | ||||||
|  | 
 | ||||||
|  |       // Now call batch finalization (should skip table, only process user) | ||||||
|  |       handler.finalizeReindex(context, true); | ||||||
|  | 
 | ||||||
|  |       // Both should be finalized | ||||||
|  |       assertTrue(context.isFinalized("test_table")); | ||||||
|  |       assertTrue(context.isFinalized("test_user")); | ||||||
|  | 
 | ||||||
|  |       // User should be processed | ||||||
|  |       assertFalse(searchClient.indexExists(userOld), "User old index should be deleted"); | ||||||
|  |       assertTrue(searchClient.indexExists(userStaged), "User staged index should exist"); | ||||||
|  |     } finally { | ||||||
|  |       // Cleanup | ||||||
|  |       for (String index : List.of(tableStaged, tableOld, userStaged, userOld)) { | ||||||
|  |         if (searchClient.indexExists(index)) { | ||||||
|  |           searchClient.deleteIndex(index); | ||||||
|  |         } | ||||||
|  |       } | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   @Test | ||||||
|  |   void testEntityFinalizationOnSuccess() { | ||||||
|  |     SearchRepository searchRepo = Entity.getSearchRepository(); | ||||||
|  |     SearchClient<?> searchClient = searchRepo.getSearchClient(); | ||||||
|  | 
 | ||||||
|  |     long timestamp = System.currentTimeMillis(); | ||||||
|  |     String stagedIndex = "test_dashboard_staged_" + timestamp; | ||||||
|  |     String oldIndex = "test_dashboard_old_" + timestamp; | ||||||
|  | 
 | ||||||
|  |     try { | ||||||
|  |       // Create indexes | ||||||
|  |       searchClient.createIndex(oldIndex, "{}"); | ||||||
|  |       searchClient.addAliases(oldIndex, Set.of("test_dashboard", "test_all")); | ||||||
|  |       searchClient.createIndex(stagedIndex, "{}"); | ||||||
|  | 
 | ||||||
|  |       RecreateIndexHandler.ReindexContext context = new RecreateIndexHandler.ReindexContext(); | ||||||
|  |       context.add( | ||||||
|  |           "test_dashboard", | ||||||
|  |           "test_dashboard_index", | ||||||
|  |           oldIndex, | ||||||
|  |           stagedIndex, | ||||||
|  |           Set.of("test_dashboard", "test_all"), | ||||||
|  |           "test_dashboard", | ||||||
|  |           List.of("test_all")); | ||||||
|  | 
 | ||||||
|  |       DefaultRecreateHandler handler = new DefaultRecreateHandler(); | ||||||
|  |       handler.finalizeEntityReindex(context, "test_dashboard", true); | ||||||
|  | 
 | ||||||
|  |       // On success, old index should be deleted and aliases moved | ||||||
|  |       assertFalse(searchClient.indexExists(oldIndex), "Old index should be deleted"); | ||||||
|  |       assertTrue(searchClient.indexExists(stagedIndex), "Staged index should exist"); | ||||||
|  | 
 | ||||||
|  |       Set<String> aliases = searchClient.getAliases(stagedIndex); | ||||||
|  |       assertTrue(aliases.contains("test_dashboard")); | ||||||
|  |       assertTrue(aliases.contains("test_all")); | ||||||
|  |       assertTrue(context.isFinalized("test_dashboard")); | ||||||
|  |     } finally { | ||||||
|  |       // Cleanup | ||||||
|  |       for (String index : List.of(stagedIndex, oldIndex)) { | ||||||
|  |         if (searchClient.indexExists(index)) { | ||||||
|  |           searchClient.deleteIndex(index); | ||||||
|  |         } | ||||||
|  |       } | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   @Test | ||||||
|  |   void testEntityFinalizationOnFailure() { | ||||||
|  |     SearchRepository searchRepo = Entity.getSearchRepository(); | ||||||
|  |     SearchClient<?> searchClient = searchRepo.getSearchClient(); | ||||||
|  | 
 | ||||||
|  |     long timestamp = System.currentTimeMillis(); | ||||||
|  |     String stagedIndex = "test_pipeline_staged_" + timestamp; | ||||||
|  |     String oldIndex = "test_pipeline_old_" + timestamp; | ||||||
|  | 
 | ||||||
|  |     try { | ||||||
|  |       // Create indexes | ||||||
|  |       searchClient.createIndex(oldIndex, "{}"); | ||||||
|  |       searchClient.addAliases(oldIndex, Set.of("test_pipeline", "test_all")); | ||||||
|  |       searchClient.createIndex(stagedIndex, "{}"); | ||||||
|  | 
 | ||||||
|  |       RecreateIndexHandler.ReindexContext context = new RecreateIndexHandler.ReindexContext(); | ||||||
|  |       context.add( | ||||||
|  |           "test_pipeline", | ||||||
|  |           "test_pipeline_index", | ||||||
|  |           oldIndex, | ||||||
|  |           stagedIndex, | ||||||
|  |           Set.of("test_pipeline", "test_all"), | ||||||
|  |           "test_pipeline", | ||||||
|  |           List.of("test_all")); | ||||||
|  | 
 | ||||||
|  |       DefaultRecreateHandler handler = new DefaultRecreateHandler(); | ||||||
|  |       handler.finalizeEntityReindex(context, "test_pipeline", false); | ||||||
|  | 
 | ||||||
|  |       // On failure, staged index should be deleted, old index should remain | ||||||
|  |       assertFalse(searchClient.indexExists(stagedIndex), "Staged index should be deleted"); | ||||||
|  |       assertTrue(searchClient.indexExists(oldIndex), "Old index should remain"); | ||||||
|  | 
 | ||||||
|  |       Set<String> aliases = searchClient.getAliases(oldIndex); | ||||||
|  |       assertTrue(aliases.contains("test_pipeline")); | ||||||
|  |       assertTrue(aliases.contains("test_all")); | ||||||
|  |       assertTrue(context.isFinalized("test_pipeline")); | ||||||
|  |     } finally { | ||||||
|  |       // Cleanup | ||||||
|  |       for (String index : List.of(stagedIndex, oldIndex)) { | ||||||
|  |         if (searchClient.indexExists(index)) { | ||||||
|  |           searchClient.deleteIndex(index); | ||||||
|  |         } | ||||||
|  |       } | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   @Test | ||||||
|  |   void testPerEntityFinalizationWithClusterAlias() { | ||||||
|  |     SearchRepository searchRepo = Entity.getSearchRepository(); | ||||||
|  |     SearchClient<?> searchClient = searchRepo.getSearchClient(); | ||||||
|  |     String clusterAlias = searchRepo.getClusterAlias(); | ||||||
|  | 
 | ||||||
|  |     long timestamp = System.currentTimeMillis(); | ||||||
|  |     String canonicalIndexName = | ||||||
|  |         clusterAlias.isEmpty() | ||||||
|  |             ? "test_cluster_table_index" | ||||||
|  |             : clusterAlias + "_test_cluster_table_index"; | ||||||
|  |     String oldIndex = canonicalIndexName + "_old_" + timestamp; | ||||||
|  |     String stagedIndex = canonicalIndexName + "_rebuild_" + timestamp; | ||||||
|  | 
 | ||||||
|  |     try { | ||||||
|  |       // Create old index with canonical name as alias | ||||||
|  |       searchClient.createIndex(oldIndex, "{}"); | ||||||
|  |       if (!clusterAlias.isEmpty()) { | ||||||
|  |         // Add canonical index name as alias (simulating existing setup) | ||||||
|  |         searchClient.addAliases(oldIndex, Set.of(canonicalIndexName, "test_cluster_alias")); | ||||||
|  |       } else { | ||||||
|  |         searchClient.addAliases(oldIndex, Set.of("test_cluster_alias")); | ||||||
|  |       } | ||||||
|  | 
 | ||||||
|  |       // Create staged index | ||||||
|  |       searchClient.createIndex(stagedIndex, "{}"); | ||||||
|  | 
 | ||||||
|  |       RecreateIndexHandler.ReindexContext context = new RecreateIndexHandler.ReindexContext(); | ||||||
|  |       Set<String> existingAliases = | ||||||
|  |           clusterAlias.isEmpty() | ||||||
|  |               ? Set.of("test_cluster_alias") | ||||||
|  |               : Set.of(canonicalIndexName, "test_cluster_alias"); | ||||||
|  | 
 | ||||||
|  |       context.add( | ||||||
|  |           "test_cluster_table", | ||||||
|  |           canonicalIndexName, | ||||||
|  |           oldIndex, | ||||||
|  |           stagedIndex, | ||||||
|  |           existingAliases, | ||||||
|  |           "test_cluster_table", | ||||||
|  |           List.of()); | ||||||
|  | 
 | ||||||
|  |       DefaultRecreateHandler handler = new DefaultRecreateHandler(); | ||||||
|  | 
 | ||||||
|  |       // Finalize the entity | ||||||
|  |       handler.finalizeEntityReindex(context, "test_cluster_table", true); | ||||||
|  | 
 | ||||||
|  |       // Verify old index is deleted | ||||||
|  |       assertFalse(searchClient.indexExists(oldIndex), "Old index should be deleted"); | ||||||
|  |       assertTrue(searchClient.indexExists(stagedIndex), "Staged index should exist"); | ||||||
|  | 
 | ||||||
|  |       // Verify aliases are attached to staged index | ||||||
|  |       Set<String> stagedAliases = searchClient.getAliases(stagedIndex); | ||||||
|  |       assertTrue(stagedAliases.contains("test_cluster_alias"), "Should have test_cluster_alias"); | ||||||
|  | 
 | ||||||
|  |       // Verify canonical index name works as alias (if cluster alias is configured) | ||||||
|  |       if (!clusterAlias.isEmpty()) { | ||||||
|  |         assertTrue( | ||||||
|  |             stagedAliases.contains(canonicalIndexName), | ||||||
|  |             "Should have canonical index name as alias: " + canonicalIndexName); | ||||||
|  |       } | ||||||
|  | 
 | ||||||
|  |       assertTrue(context.isFinalized("test_cluster_table")); | ||||||
|  |     } finally { | ||||||
|  |       // Cleanup | ||||||
|  |       for (String index : List.of(oldIndex, stagedIndex)) { | ||||||
|  |         if (searchClient.indexExists(index)) { | ||||||
|  |           searchClient.deleteIndex(index); | ||||||
|  |         } | ||||||
|  |       } | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   @Test | ||||||
|  |   void testMultipleEntitiesWithPerEntityFinalization() { | ||||||
|  |     SearchRepository searchRepo = Entity.getSearchRepository(); | ||||||
|  |     SearchClient<?> searchClient = searchRepo.getSearchClient(); | ||||||
|  | 
 | ||||||
|  |     long timestamp = System.currentTimeMillis(); | ||||||
|  |     String tableStaged = "test_multi_table_staged_" + timestamp; | ||||||
|  |     String tableOld = "test_multi_table_old_" + timestamp; | ||||||
|  |     String userStaged = "test_multi_user_staged_" + timestamp; | ||||||
|  |     String userOld = "test_multi_user_old_" + timestamp; | ||||||
|  |     String dashStaged = "test_multi_dash_staged_" + timestamp; | ||||||
|  |     String dashOld = "test_multi_dash_old_" + timestamp; | ||||||
|  | 
 | ||||||
|  |     try { | ||||||
|  |       // Setup three entities | ||||||
|  |       searchClient.createIndex(tableOld, "{}"); | ||||||
|  |       searchClient.addAliases(tableOld, Set.of("test_table", "test_all")); | ||||||
|  |       searchClient.createIndex(tableStaged, "{}"); | ||||||
|  | 
 | ||||||
|  |       searchClient.createIndex(userOld, "{}"); | ||||||
|  |       searchClient.addAliases(userOld, Set.of("test_user", "test_all")); | ||||||
|  |       searchClient.createIndex(userStaged, "{}"); | ||||||
|  | 
 | ||||||
|  |       searchClient.createIndex(dashOld, "{}"); | ||||||
|  |       searchClient.addAliases(dashOld, Set.of("test_dashboard", "test_all")); | ||||||
|  |       searchClient.createIndex(dashStaged, "{}"); | ||||||
|  | 
 | ||||||
|  |       RecreateIndexHandler.ReindexContext context = new RecreateIndexHandler.ReindexContext(); | ||||||
|  |       context.add( | ||||||
|  |           "test_table", | ||||||
|  |           "test_table_index", | ||||||
|  |           tableOld, | ||||||
|  |           tableStaged, | ||||||
|  |           Set.of("test_table", "test_all"), | ||||||
|  |           "test_table", | ||||||
|  |           List.of("test_all")); | ||||||
|  |       context.add( | ||||||
|  |           "test_user", | ||||||
|  |           "test_user_index", | ||||||
|  |           userOld, | ||||||
|  |           userStaged, | ||||||
|  |           Set.of("test_user", "test_all"), | ||||||
|  |           "test_user", | ||||||
|  |           List.of("test_all")); | ||||||
|  |       context.add( | ||||||
|  |           "test_dashboard", | ||||||
|  |           "test_dashboard_index", | ||||||
|  |           dashOld, | ||||||
|  |           dashStaged, | ||||||
|  |           Set.of("test_dashboard", "test_all"), | ||||||
|  |           "test_dashboard", | ||||||
|  |           List.of("test_all")); | ||||||
|  | 
 | ||||||
|  |       DefaultRecreateHandler handler = new DefaultRecreateHandler(); | ||||||
|  | 
 | ||||||
|  |       // Simulate processing entities one by one | ||||||
|  |       handler.finalizeEntityReindex(context, "test_table", true); | ||||||
|  |       assertFalse(searchClient.indexExists(tableOld), "Table old index should be deleted"); | ||||||
|  |       assertTrue(context.isFinalized("test_table")); | ||||||
|  | 
 | ||||||
|  |       handler.finalizeEntityReindex(context, "test_user", true); | ||||||
|  |       assertFalse(searchClient.indexExists(userOld), "User old index should be deleted"); | ||||||
|  |       assertTrue(context.isFinalized("test_user")); | ||||||
|  | 
 | ||||||
|  |       // Dashboard fails | ||||||
|  |       handler.finalizeEntityReindex(context, "test_dashboard", false); | ||||||
|  |       assertFalse( | ||||||
|  |           searchClient.indexExists(dashStaged), "Dashboard staged should be deleted on failure"); | ||||||
|  |       assertTrue(searchClient.indexExists(dashOld), "Dashboard old should remain on failure"); | ||||||
|  |       assertTrue(context.isFinalized("test_dashboard")); | ||||||
|  | 
 | ||||||
|  |       // Verify final state - successful entities have staged indexes | ||||||
|  |       assertTrue(searchClient.indexExists(tableStaged)); | ||||||
|  |       assertTrue(searchClient.indexExists(userStaged)); | ||||||
|  |       assertFalse(searchClient.indexExists(dashStaged)); | ||||||
|  |     } finally { | ||||||
|  |       // Cleanup | ||||||
|  |       for (String index : | ||||||
|  |           List.of(tableStaged, tableOld, userStaged, userOld, dashStaged, dashOld)) { | ||||||
|  |         if (searchClient.indexExists(index)) { | ||||||
|  |           searchClient.deleteIndex(index); | ||||||
|  |         } | ||||||
|  |       } | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|   private static class AliasState { |   private static class AliasState { | ||||||
|     final Map<String, Set<String>> indexAliases = new HashMap<>(); |     final Map<String, Set<String>> indexAliases = new HashMap<>(); | ||||||
|     final Set<String> deletedIndices = new HashSet<>(); |     final Set<String> deletedIndices = new HashSet<>(); | ||||||
|  | |||||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user
	 Sriharsha Chintalapani
						Sriharsha Chintalapani