Skip to content

Commit eaef3a9

Browse files
Validate enrich index before completing policy execution (#100106) (#100160)
This PR adds a validation step to the end of an enrich policy run to ensure the integrity of the enrich index that is about to be promoted. (cherry picked from commit 225db31) Co-authored-by: Elastic Machine <[email protected]>
1 parent 304781b commit eaef3a9

File tree

3 files changed

+152
-3
lines changed

3 files changed

+152
-3
lines changed

docs/changelog/100106.yaml

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
pr: 100106
2+
summary: Validate enrich index before completing policy execution
3+
area: Ingest Node
4+
type: bug
5+
issues: []

x-pack/plugin/enrich/src/main/java/org/elasticsearch/xpack/enrich/EnrichPolicyRunner.java

Lines changed: 63 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
import org.apache.logging.log4j.Logger;
1111
import org.apache.logging.log4j.message.ParameterizedMessage;
1212
import org.elasticsearch.ElasticsearchException;
13+
import org.elasticsearch.ResourceNotFoundException;
1314
import org.elasticsearch.action.ActionListener;
1415
import org.elasticsearch.action.ActionRequest;
1516
import org.elasticsearch.action.ActionResponse;
@@ -34,6 +35,7 @@
3435
import org.elasticsearch.client.OriginSettingClient;
3536
import org.elasticsearch.cluster.ClusterState;
3637
import org.elasticsearch.cluster.metadata.AliasMetadata;
38+
import org.elasticsearch.cluster.metadata.IndexMetadata;
3739
import org.elasticsearch.cluster.metadata.IndexNameExpressionResolver;
3840
import org.elasticsearch.cluster.metadata.MappingMetadata;
3941
import org.elasticsearch.cluster.service.ClusterService;
@@ -42,6 +44,7 @@
4244
import org.elasticsearch.common.collect.ImmutableOpenMap;
4345
import org.elasticsearch.common.settings.Settings;
4446
import org.elasticsearch.core.CheckedFunction;
47+
import org.elasticsearch.index.IndexNotFoundException;
4548
import org.elasticsearch.index.mapper.MapperService;
4649
import org.elasticsearch.index.query.QueryBuilders;
4750
import org.elasticsearch.index.reindex.BulkByScrollResponse;
@@ -569,16 +572,73 @@ private void setIndexReadOnly(final String destinationIndexName) {
569572

570573
private void waitForIndexGreen(final String destinationIndexName) {
571574
ClusterHealthRequest request = new ClusterHealthRequest(destinationIndexName).waitForGreenStatus();
572-
enrichOriginClient().admin()
573-
.cluster()
574-
.health(request, listener.delegateFailure((l, r) -> updateEnrichPolicyAlias(destinationIndexName)));
575+
enrichOriginClient().admin().cluster().health(request, listener.delegateFailure((l, r) -> {
576+
try {
577+
updateEnrichPolicyAlias(destinationIndexName);
578+
} catch (Exception e) {
579+
l.onFailure(e);
580+
}
581+
}));
582+
}
583+
584+
/**
585+
* Ensures that the index we are about to promote at the end of a policy execution exists, is intact, and has not been damaged
586+
* during the policy execution. In some cases, it is possible for the index being constructed to be deleted during the policy execution
587+
* and recreated with invalid mappings/data. We validate that the mapping exists and that it contains the expected meta fields on it to
588+
* guard against accidental removal and recreation during policy execution.
589+
*/
590+
private void validateIndexBeforePromotion(String destinationIndexName, ClusterState clusterState) {
591+
IndexMetadata destinationIndex = clusterState.metadata().index(destinationIndexName);
592+
if (destinationIndex == null) {
593+
throw new IndexNotFoundException(
594+
"was not able to promote it as part of executing enrich policy [" + policyName + "]",
595+
destinationIndexName
596+
);
597+
}
598+
MappingMetadata mapping = destinationIndex.mapping();
599+
if (mapping == null) {
600+
throw new ResourceNotFoundException(
601+
"Could not locate mapping for enrich index [{}] while completing [{}] policy run",
602+
destinationIndexName,
603+
policyName
604+
);
605+
}
606+
Map<String, Object> mappingSource = mapping.sourceAsMap();
607+
Object meta = mappingSource.get("_meta");
608+
if (meta instanceof Map<?, ?>) {
609+
Map<?, ?> metaMap = ((Map<?, ?>) meta);
610+
Object policyNameMetaField = metaMap.get(ENRICH_POLICY_NAME_FIELD_NAME);
611+
if (policyNameMetaField == null) {
612+
throw new ElasticsearchException(
613+
"Could not verify enrich index [{}] metadata before completing [{}] policy run: policy name meta field missing",
614+
destinationIndexName,
615+
policyName
616+
);
617+
} else if (policyName.equals(policyNameMetaField) == false) {
618+
throw new ElasticsearchException(
619+
"Could not verify enrich index [{}] metadata before completing [{}] policy run: policy name meta field does not "
620+
+ "match expected value of [{}], was [{}]",
621+
destinationIndexName,
622+
policyName,
623+
policyName,
624+
policyNameMetaField.toString()
625+
);
626+
}
627+
} else {
628+
throw new ElasticsearchException(
629+
"Could not verify enrich index [{}] metadata before completing [{}] policy run: mapping meta field missing",
630+
destinationIndexName,
631+
policyName
632+
);
633+
}
575634
}
576635

577636
private void updateEnrichPolicyAlias(final String destinationIndexName) {
578637
String enrichIndexBase = EnrichPolicy.getBaseName(policyName);
579638
logger.debug("Policy [{}]: Promoting new enrich index [{}] to alias [{}]", policyName, destinationIndexName, enrichIndexBase);
580639
GetAliasesRequest aliasRequest = new GetAliasesRequest(enrichIndexBase);
581640
ClusterState clusterState = clusterService.state();
641+
validateIndexBeforePromotion(destinationIndexName, clusterState);
582642
String[] concreteIndices = indexNameExpressionResolver.concreteIndexNamesWithSystemIndexAccess(clusterState, aliasRequest);
583643
ImmutableOpenMap<String, List<AliasMetadata>> aliases = clusterState.metadata().findAliases(aliasRequest, concreteIndices);
584644
IndicesAliasesRequest aliasToggleRequest = new IndicesAliasesRequest();

x-pack/plugin/enrich/src/test/java/org/elasticsearch/xpack/enrich/EnrichPolicyRunnerTests.java

Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,8 @@
1616
import org.elasticsearch.action.admin.indices.create.CreateIndexAction;
1717
import org.elasticsearch.action.admin.indices.create.CreateIndexRequest;
1818
import org.elasticsearch.action.admin.indices.create.CreateIndexResponse;
19+
import org.elasticsearch.action.admin.indices.delete.DeleteIndexAction;
20+
import org.elasticsearch.action.admin.indices.delete.DeleteIndexRequest;
1921
import org.elasticsearch.action.admin.indices.forcemerge.ForceMergeAction;
2022
import org.elasticsearch.action.admin.indices.get.GetIndexAction;
2123
import org.elasticsearch.action.admin.indices.get.GetIndexRequest;
@@ -2090,6 +2092,88 @@ protected <Request extends ActionRequest, Response extends ActionResponse> void
20902092
assertThat(exception.get().getMessage(), containsString("cancelled policy execution [test1], status ["));
20912093
}
20922094

2095+
public void testRunnerValidatesIndexIntegrity() throws Exception {
2096+
final String sourceIndex = "source-index";
2097+
IndexResponse indexRequest = client().index(
2098+
new IndexRequest().index(sourceIndex)
2099+
.id("id")
2100+
.source(
2101+
"{"
2102+
+ "\"field1\":\"value1\","
2103+
+ "\"field2\":2,"
2104+
+ "\"field3\":\"ignored\","
2105+
+ "\"field4\":\"ignored\","
2106+
+ "\"field5\":\"value5\""
2107+
+ "}",
2108+
XContentType.JSON
2109+
)
2110+
.setRefreshPolicy(WriteRequest.RefreshPolicy.IMMEDIATE)
2111+
).actionGet();
2112+
assertEquals(RestStatus.CREATED, indexRequest.status());
2113+
2114+
SearchResponse sourceSearchResponse = client().search(
2115+
new SearchRequest(sourceIndex).source(SearchSourceBuilder.searchSource().query(QueryBuilders.matchAllQuery()))
2116+
).actionGet();
2117+
assertThat(sourceSearchResponse.getHits().getTotalHits().value, equalTo(1L));
2118+
Map<String, Object> sourceDocMap = sourceSearchResponse.getHits().getAt(0).getSourceAsMap();
2119+
assertNotNull(sourceDocMap);
2120+
assertThat(sourceDocMap.get("field1"), is(equalTo("value1")));
2121+
assertThat(sourceDocMap.get("field2"), is(equalTo(2)));
2122+
assertThat(sourceDocMap.get("field3"), is(equalTo("ignored")));
2123+
assertThat(sourceDocMap.get("field4"), is(equalTo("ignored")));
2124+
assertThat(sourceDocMap.get("field5"), is(equalTo("value5")));
2125+
2126+
List<String> enrichFields = Arrays.asList("field2", "field5");
2127+
EnrichPolicy policy = new EnrichPolicy(EnrichPolicy.MATCH_TYPE, null, singletonList(sourceIndex), "field1", enrichFields);
2128+
String policyName = "test1";
2129+
2130+
final long createTime = randomNonNegativeLong();
2131+
String createdEnrichIndex = ".enrich-test1-" + createTime;
2132+
final AtomicReference<Exception> exception = new AtomicReference<>();
2133+
final CountDownLatch latch = new CountDownLatch(1);
2134+
ActionListener<ExecuteEnrichPolicyStatus> listener = createTestListener(latch, exception::set);
2135+
2136+
// Wrap the client so that when we receive the reindex action, we delete the index then resume operation. This mimics an invalid
2137+
// state for the resulting index.
2138+
Client client = new FilterClient(client()) {
2139+
@Override
2140+
protected <Request extends ActionRequest, Response extends ActionResponse> void doExecute(
2141+
ActionType<Response> action,
2142+
Request request,
2143+
ActionListener<Response> listener
2144+
) {
2145+
if (action.equals(EnrichReindexAction.INSTANCE)) {
2146+
super.doExecute(
2147+
DeleteIndexAction.INSTANCE,
2148+
new DeleteIndexRequest(createdEnrichIndex),
2149+
listener.delegateFailure((delegate, response) -> {
2150+
if (response.isAcknowledged()) {
2151+
super.doExecute(action, request, delegate);
2152+
} else {
2153+
fail("Enrich index should have been deleted but was not");
2154+
delegate.onFailure(new ElasticsearchException("Could not delete enrich index - cleaning up"));
2155+
}
2156+
})
2157+
);
2158+
} else {
2159+
super.doExecute(action, request, listener);
2160+
}
2161+
}
2162+
};
2163+
EnrichPolicyRunner enrichPolicyRunner = createPolicyRunner(client, policyName, policy, listener, createdEnrichIndex);
2164+
2165+
logger.info("Starting policy run");
2166+
enrichPolicyRunner.run();
2167+
latch.await();
2168+
Exception runnerException = exception.get();
2169+
if (runnerException == null) {
2170+
fail("Expected the runner to fail when the underlying index was deleted during policy execution!");
2171+
}
2172+
assertThat(runnerException, is(instanceOf(ElasticsearchException.class)));
2173+
assertThat(runnerException.getMessage(), containsString("Could not verify enrich index"));
2174+
assertThat(runnerException.getMessage(), containsString("mapping meta field missing"));
2175+
}
2176+
20932177
private EnrichPolicyRunner createPolicyRunner(
20942178
String policyName,
20952179
EnrichPolicy policy,

0 commit comments

Comments
 (0)