|
| 1 | +/* |
| 2 | + * SPDX-License-Identifier: Apache-2.0 |
| 3 | + * |
| 4 | + * The OpenSearch Contributors require contributions made to |
| 5 | + * this file be licensed under the Apache-2.0 license or a |
| 6 | + * compatible open source license. |
| 7 | + */ |
| 8 | + |
| 9 | +package org.opensearch.indices.replication; |
| 10 | + |
| 11 | +import org.opensearch.action.admin.indices.stats.IndicesStatsResponse; |
| 12 | +import org.opensearch.action.admin.indices.stats.ShardStats; |
| 13 | +import org.opensearch.index.warmer.WarmerStats; |
| 14 | +import org.opensearch.test.OpenSearchIntegTestCase; |
| 15 | +import org.junit.Before; |
| 16 | + |
| 17 | +import java.util.HashMap; |
| 18 | +import java.util.Map; |
| 19 | + |
| 20 | +import static org.opensearch.test.hamcrest.OpenSearchAssertions.assertAcked; |
| 21 | +import static org.hamcrest.Matchers.greaterThan; |
| 22 | + |
| 23 | +/** |
| 24 | + * Integration tests that verify index warming (e.g., eager global ordinals loading) works correctly |
| 25 | + * on NRT replica shards during segment replication. This validates the end-to-end flow where |
| 26 | + * index warmer gets triggered when new segments arrive on replicas via segment replication. |
| 27 | + * |
| 28 | + * <p>Uses a keyword field with {@code eager_global_ordinals: true} |
| 29 | + * to exercise the warming path during segment replication.</p> |
| 30 | + */ |
| 31 | +@OpenSearchIntegTestCase.ClusterScope(scope = OpenSearchIntegTestCase.Scope.TEST, numDataNodes = 0) |
| 32 | +public class SegmentReplicationReplicaIndexWarmerIT extends SegmentReplicationBaseIT { |
| 33 | + @Before |
| 34 | + public void setup() { |
| 35 | + internalCluster().startClusterManagerOnlyNode(); |
| 36 | + } |
| 37 | + |
| 38 | + /** |
| 39 | + * Verifies that eager global ordinals are loaded on both primary and replica shards |
| 40 | + * after segment replication, by checking warmer invocation metrics from the index stats API. |
| 41 | + * |
| 42 | + * <p>This test ensures that the {@code WarmerRefreshListener} in {@code NRTReplicationEngine} |
| 43 | + * correctly invokes the {@code IndexWarmer} chain on replica shards, which in turn loads |
| 44 | + * global ordinals for the keyword field with {@code eager_global_ordinals: true}.</p> |
| 45 | + */ |
| 46 | + public void testEagerGlobalOrdinalsLoadedOnReplicaAfterSegmentReplication() throws Exception { |
| 47 | + final String primaryNode = internalCluster().startDataOnlyNode(); |
| 48 | + assertAcked( |
| 49 | + prepareCreate(INDEX_NAME).setSettings(indexSettings()).setMapping("category", "type=keyword,eager_global_ordinals=true") |
| 50 | + ); |
| 51 | + ensureYellow(INDEX_NAME); |
| 52 | + final String replicaNode = internalCluster().startDataOnlyNode(); |
| 53 | + ensureGreen(INDEX_NAME); |
| 54 | + |
| 55 | + final int docCount = randomIntBetween(10, 20); |
| 56 | + indexTestDocuments(docCount, -1); |
| 57 | + final Map<String, Long> warmerTotalBeforeRefresh = getWarmerTotalPerShardType(INDEX_NAME); |
| 58 | + refresh(INDEX_NAME); |
| 59 | + waitForSearchableDocs(docCount, primaryNode, replicaNode); |
| 60 | + |
| 61 | + // Assert warmer invoked metrics from index stats API for both primary and replica shards |
| 62 | + assertBusy(() -> { |
| 63 | + Map<String, Long> warmerTotalsAfterRefresh = getWarmerTotalPerShardType(INDEX_NAME); |
| 64 | + compareWarmerTotals(warmerTotalBeforeRefresh, warmerTotalsAfterRefresh); |
| 65 | + }); |
| 66 | + } |
| 67 | + |
| 68 | + /** |
| 69 | + * Verifies that warmer invocations continue on replica shards after a force merge on the primary, |
| 70 | + * followed by segment replication. This ensures warming is triggered on subsequent segment updates. |
| 71 | + */ |
| 72 | + public void testWarmerInvokedOnReplicaAfterForceMerge() throws Exception { |
| 73 | + final String primaryNode = internalCluster().startDataOnlyNode(); |
| 74 | + assertAcked( |
| 75 | + prepareCreate(INDEX_NAME).setSettings(indexSettings()).setMapping("category", "type=keyword,eager_global_ordinals=true") |
| 76 | + ); |
| 77 | + ensureYellow(INDEX_NAME); |
| 78 | + final String replicaNode = internalCluster().startDataOnlyNode(); |
| 79 | + ensureGreen(INDEX_NAME); |
| 80 | + |
| 81 | + // Index documents in batches to create multiple segments |
| 82 | + final int docCount = randomIntBetween(10, 20); |
| 83 | + indexTestDocuments(docCount, 3); |
| 84 | + waitForSearchableDocs(docCount, primaryNode, replicaNode); |
| 85 | + |
| 86 | + // Capture warmer stats before force merge |
| 87 | + final Map<String, Long> warmerTotalBeforeForceMerge = getWarmerTotalPerShardType(INDEX_NAME); |
| 88 | + |
| 89 | + // Force merge and wait for segment replication to complete |
| 90 | + client().admin().indices().prepareForceMerge(INDEX_NAME).setMaxNumSegments(1).setFlush(true).get(); |
| 91 | + |
| 92 | + // Verify warmer was invoked again on replica after force merge replication |
| 93 | + assertBusy(() -> { |
| 94 | + final Map<String, Long> warmerTotalAfterForceMerge = getWarmerTotalPerShardType(INDEX_NAME); |
| 95 | + compareWarmerTotals(warmerTotalBeforeForceMerge, warmerTotalAfterForceMerge, true); |
| 96 | + }); |
| 97 | + } |
| 98 | + |
| 99 | + private Map<String, Long> getWarmerTotalPerShardType(String indexName) { |
| 100 | + IndicesStatsResponse shardStatsArray = client().admin().indices().prepareStats(indexName).clear().setWarmer(true).get(); |
| 101 | + Map<String, Long> warmerTotals = new HashMap<>(); |
| 102 | + warmerTotals.put("primary", 0L); |
| 103 | + warmerTotals.put("replica", 0L); |
| 104 | + for (ShardStats shardStats : shardStatsArray.getShards()) { |
| 105 | + WarmerStats warmerStats = shardStats.getStats().getWarmer(); |
| 106 | + if (warmerStats != null) { |
| 107 | + String key = shardStats.getShardRouting().primary() ? "primary" : "replica"; |
| 108 | + warmerTotals.merge(key, warmerStats.total(), Long::sum); |
| 109 | + } |
| 110 | + } |
| 111 | + return warmerTotals; |
| 112 | + } |
| 113 | + |
| 114 | + private void indexTestDocuments(int docCount, int refreshAfterDocs) { |
| 115 | + for (int i = 0; i < docCount; i++) { |
| 116 | + client().prepareIndex(INDEX_NAME).setId(Integer.toString(i)).setSource("category", "value-" + i).get(); |
| 117 | + if (refreshAfterDocs != -1 && i % refreshAfterDocs == 0) { |
| 118 | + refresh(INDEX_NAME); |
| 119 | + } |
| 120 | + } |
| 121 | + } |
| 122 | + |
| 123 | + private void compareWarmerTotals(Map<String, Long> before, Map<String, Long> after) { |
| 124 | + compareWarmerTotals(before, after, false); |
| 125 | + } |
| 126 | + |
| 127 | + private void compareWarmerTotals(Map<String, Long> before, Map<String, Long> after, boolean onReplicaOnly) { |
| 128 | + for (String shardType : before.keySet()) { |
| 129 | + if (onReplicaOnly && shardType.equals("primary")) { |
| 130 | + continue; // Skip primary if we're only checking replica warmer totals |
| 131 | + } |
| 132 | + long beforeTotal = before.get(shardType); |
| 133 | + long afterTotal = after.get(shardType); |
| 134 | + assertThat( |
| 135 | + "Warmer total for " + shardType + " should increase after segment replication", |
| 136 | + afterTotal, |
| 137 | + greaterThan(beforeTotal) |
| 138 | + ); |
| 139 | + } |
| 140 | + } |
| 141 | +} |
0 commit comments