Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
import java.util.ArrayList;
import java.util.List;
import java.util.Locale;
import java.util.UUID;
import java.util.concurrent.TimeUnit;
import org.awaitility.Awaitility;
import org.junit.jupiter.api.Test;
Expand Down Expand Up @@ -2124,4 +2125,28 @@ void testValidQueryFilterStillApplies() throws Exception {

assertEquals(200, response.statusCode(), response.body());
}

@ParameterizedTest
@ValueSource(
strings = {
"/v1/search/query?q=*&index=nope_search_index",
"/v1/search/query?q=*&index=table_search_index,nope_search_index",
"/v1/search/get/nope_search_index/doc/00000000-0000-0000-0000-000000000000"
})
void testUnknownIndexIsRejectedAsBadRequest(String path) throws Exception {
HttpResponse<String> response = httpGetSearch(path);

assertEquals(400, response.statusCode(), path + " returned " + response.body());
assertTrue(
response.body().contains("Unknown search index [nope_search_index]"),
path + " returned " + response.body());
}

@Test
void testMissingDocumentInAKnownIndexIsNotFound() throws Exception {
HttpResponse<String> response =
httpGetSearch("/v1/search/get/table_search_index/doc/" + UUID.randomUUID());

assertEquals(404, response.statusCode(), response.body());
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@
import jakarta.ws.rs.core.Response;
import java.util.List;
import org.openmetadata.sdk.exception.SearchException;
import org.openmetadata.sdk.exception.SearchIndexNotFoundException;
import org.openmetadata.service.Entity;

/**
* Translates a search engine's own HTTP failure into the status OpenMetadata reports to its caller.
Expand All @@ -14,13 +16,40 @@
*
* <p>Only {@code 400} is re-mapped. Everything else, including the {@code 429} the engine returns
* when a circuit breaker trips, stays a 500: those are genuinely not the caller's doing.
*
* <p>A missing index ({@code 404}) depends on whose name it is. A name OpenMetadata does not manage
* is the caller's mistake and is a 400. A managed index that is missing from the cluster, for
* example after a failed reindex, is an outage and stays a 500. Issue #31878.
*/
public final class SearchEngineErrors {

private static final int BAD_REQUEST = 400;

private SearchEngineErrors() {}

/** Classifies a missing index using the names the running server manages. */
public static SearchIndexNotFoundException indexNotFound(String resolvedIndex) {
SearchRepository searchRepository = Entity.getSearchRepository();
List<String> unknownNames =
searchRepository == null ? List.of() : searchRepository.unknownIndexNames(resolvedIndex);
return indexNotFound(resolvedIndex, unknownNames);
}

/**
* @param resolvedIndex the index expression sent to the engine
* @param unknownNames the names in it that OpenMetadata does not manage, as the caller typed them
*/
public static SearchIndexNotFoundException indexNotFound(
String resolvedIndex, List<String> unknownNames) {
if (unknownNames.isEmpty()) {
return new SearchIndexNotFoundException(
String.format("Failed to find index %s", resolvedIndex));
}
return SearchIndexNotFoundException.withStatus(
Response.Status.BAD_REQUEST,
String.format("Unknown search index [%s]", String.join(", ", unknownNames)));
}

/**
* @param upstreamStatus HTTP status the search engine returned
* @param message the engine's own message
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package org.openmetadata.service.search;

import static org.openmetadata.common.utils.CommonUtil.listOrEmpty;
import static org.openmetadata.common.utils.CommonUtil.nullOrEmpty;
import static org.openmetadata.search.IndexMapping.INDEX_NAME_SEPARATOR;
import static org.openmetadata.service.apps.bundles.insights.search.DataInsightsSearchInterface.getStringWithClusterAlias;
Expand All @@ -20,6 +21,7 @@
import java.util.Arrays;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
Expand Down Expand Up @@ -140,6 +142,66 @@ private static String resolveSingleAliasToken(
return clusterPrefix == null ? token : clusterPrefix + token;
}

/**
* Every name, without cluster prefix, that resolves to an index or alias OpenMetadata manages:
* entity types, canonical index names and their aliases.
*/
public static Set<String> knownIndexNames(Map<String, IndexMapping> entityIndexMap) {
if (entityIndexMap == null) {
return Set.of();
}
Set<String> names = new HashSet<>(entityIndexMap.keySet());
entityIndexMap.values().forEach(mapping -> addMappingNames(names, mapping));
names.remove(null);
return Set.copyOf(names);
}

private static void addMappingNames(Set<String> names, IndexMapping mapping) {
names.add(mapping.getIndexName());
names.add(mapping.getAlias());
names.addAll(listOrEmpty(mapping.getParentAliases()));
names.addAll(listOrEmpty(mapping.getChildAliases()));
names.addAll(listOrEmpty(mapping.getDataInsightAliases()));
}

/**
* Names in an index expression, already resolved by {@link #getIndexOrAliasName}, that
* OpenMetadata does not manage, as the caller typed them. Wildcards, exclusions and Data Insights
* names never count as unknown. An empty result means every name is managed, so a missing index
* is a server fault rather than the caller's mistake.
*/
public static List<String> unknownIndexNames(
String resolvedIndex, Set<String> knownIndexNames, String clusterAlias) {
if (nullOrEmpty(resolvedIndex) || nullOrEmpty(knownIndexNames)) {
return List.of();
}
return Arrays.stream(resolvedIndex.split(","))
.map(String::trim)
.filter(token -> isPlainIndexName(token, clusterAlias))
.map(token -> withoutClusterPrefix(token, clusterAlias))
.filter(name -> !knownIndexNames.contains(name))
.distinct()
.toList();
}

private static boolean isPlainIndexName(String token, String clusterAlias) {
return !token.isEmpty()
&& !token.contains("*")
&& !token.startsWith("-")
&& !token.startsWith(DI_SEARCH_INDEX_PREFIX)
&& !token.startsWith(getStringWithClusterAlias(clusterAlias, DI_SEARCH_INDEX_PREFIX));
}

private static String withoutClusterPrefix(String token, String clusterAlias) {
if (nullOrEmpty(clusterAlias)) {
return token;
}
String prefix = clusterAlias + INDEX_NAME_SEPARATOR;
return token.startsWith(prefix) && token.length() > prefix.length()
? token.substring(prefix.length())
: token;
}

/**
* Resolves comma-separated entity names, canonical indexes, and {@code all}/{@code dataAsset}
* aliases to sorted, distinct registered entity types. Unknown or empty targets have no members.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -418,6 +418,7 @@ public static void deferOrRunSearchWrite(

@Getter private Map<String, IndexMapping> entityIndexMap;
private Map<String, IndexMapping> aliasIndexMap;
private Set<String> knownIndexNames = Set.of();

/**
* Staged index names being populated by an in-flight reindex, keyed by the canonical index name
Expand Down Expand Up @@ -548,6 +549,7 @@ private void loadIndexMappings() {
IndexMappingLoader mappingLoader = IndexMappingLoader.getInstance();
entityIndexMap = mappingLoader.getIndexMapping();
aliasIndexMap = buildAliasIndexMap(entityIndexMap);
knownIndexNames = SearchIndexUtils.knownIndexNames(entityIndexMap);
}

private static Map<String, IndexMapping> buildAliasIndexMap(
Expand Down Expand Up @@ -1005,6 +1007,11 @@ public String getIndexOrAliasName(String name) {
return SearchIndexUtils.getIndexOrAliasName(name, entityIndexMap, aliasIndexMap, clusterAlias);
}

/** @see SearchIndexUtils#unknownIndexNames(String, Set, String) */
public List<String> unknownIndexNames(String resolvedIndex) {
return SearchIndexUtils.unknownIndexNames(resolvedIndex, knownIndexNames, clusterAlias);
}

/** @see SearchIndexUtils#getEntityTypesForIndex(String, Map, Map, String) */
public List<String> getEntityTypesForIndex(String index) {
return SearchIndexUtils.getEntityTypesForIndex(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -69,11 +69,11 @@
import org.openmetadata.schema.type.entityRelationship.NodeInformation;
import org.openmetadata.schema.utils.JsonUtils;
import org.openmetadata.sdk.exception.SearchException;
import org.openmetadata.sdk.exception.SearchIndexNotFoundException;
import org.openmetadata.search.IndexMapping;
import org.openmetadata.service.Entity;
import org.openmetadata.service.search.EntityManagementClient;
import org.openmetadata.service.search.SearchClient;
import org.openmetadata.service.search.SearchEngineErrors;
import org.openmetadata.service.search.SearchIndexRetryQueue;
import org.openmetadata.service.search.SearchIndexUtils;
import org.openmetadata.service.search.SearchRetryUtil;
Expand Down Expand Up @@ -597,12 +597,9 @@ public Response getDocByID(String indexName, String entityId) throws IOException
return getResponse(Response.Status.NOT_FOUND, "Document not found.");
}

String index = Entity.getSearchRepository().getIndexOrAliasName(indexName);
try {
GetResponse<Map> response =
client.get(
g ->
g.index(Entity.getSearchRepository().getIndexOrAliasName(indexName)).id(entityId),
Map.class);
GetResponse<Map> response = client.get(g -> g.index(index).id(entityId), Map.class);

// This path runs no query and has no SubjectContext, so it cannot tell whose restricted
// memory a document is: a non-org-wide memory reads as not found rather than leaking.
Expand All @@ -613,8 +610,7 @@ public Response getDocByID(String indexName, String entityId) throws IOException
}
} catch (ElasticsearchException e) {
if (e.status() == 404) {
throw new SearchIndexNotFoundException(
String.format("Failed to find doc with id %s", entityId));
throw SearchEngineErrors.indexNotFound(index);
} else {
throw new SearchException(String.format("Search failed due to %s", e.getMessage()));
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -68,7 +68,6 @@
import org.openmetadata.schema.type.Include;
import org.openmetadata.schema.utils.JsonUtils;
import org.openmetadata.sdk.exception.SearchException;
import org.openmetadata.sdk.exception.SearchIndexNotFoundException;
import org.openmetadata.service.Entity;
import org.openmetadata.service.jdbi3.ListFilter;
import org.openmetadata.service.jdbi3.TableRepository;
Expand Down Expand Up @@ -369,7 +368,7 @@ private SearchResultListMapper doListWithOffset(
return new SearchResultListMapper(results, totalHits);
} catch (ElasticsearchException e) {
if (e.status() == 404) {
throw new SearchIndexNotFoundException(String.format("Failed to find index %s", index));
throw SearchEngineErrors.indexNotFound(index);
} else {
throw buildSearchException(e);
}
Expand Down Expand Up @@ -682,7 +681,7 @@ public SearchResultListMapper listWithDeepPagination(
results, totalHits, lastHitSortValues, lastDocumentsInBatch);
} catch (ElasticsearchException e) {
if (e.status() == 404) {
throw new SearchIndexNotFoundException(String.format("Failed to find index %s", index));
throw SearchEngineErrors.indexNotFound(index);
} else {
throw buildSearchException(e);
}
Expand Down Expand Up @@ -1146,8 +1145,7 @@ public Response doSearch(

} catch (ElasticsearchException e) {
if (e.status() == 404) {
throw new SearchIndexNotFoundException(
String.format("Failed to find index %s", request.getIndex()));
throw SearchEngineErrors.indexNotFound(request.getIndex());
} else {
throw buildSearchException(e);
}
Expand Down Expand Up @@ -1334,8 +1332,7 @@ public SearchResultListMapper searchForExport(
return new SearchResultListMapper(results, totalHits, lastHitSortValues);
} catch (ElasticsearchException e) {
if (e.status() == 404) {
throw new SearchIndexNotFoundException(
String.format("Failed to find index %s", request.getIndex()));
throw SearchEngineErrors.indexNotFound(request.getIndex());
} else {
throw buildSearchException(e);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -46,11 +46,11 @@
import org.openmetadata.schema.type.entityRelationship.NodeInformation;
import org.openmetadata.schema.utils.JsonUtils;
import org.openmetadata.sdk.exception.SearchException;
import org.openmetadata.sdk.exception.SearchIndexNotFoundException;
import org.openmetadata.search.IndexMapping;
import org.openmetadata.service.Entity;
import org.openmetadata.service.search.EntityManagementClient;
import org.openmetadata.service.search.SearchClient;
import org.openmetadata.service.search.SearchEngineErrors;
import org.openmetadata.service.search.SearchIndexRetryQueue;
import org.openmetadata.service.search.SearchIndexUtils;
import org.openmetadata.service.search.SearchRetryUtil;
Expand Down Expand Up @@ -644,12 +644,9 @@ public Response getDocByID(String indexName, String entityId) throws IOException
return getResponse(Response.Status.NOT_FOUND, "Document not found.");
}

String index = Entity.getSearchRepository().getIndexOrAliasName(indexName);
try {
GetResponse<Map> response =
client.get(
g ->
g.index(Entity.getSearchRepository().getIndexOrAliasName(indexName)).id(entityId),
Map.class);
GetResponse<Map> response = client.get(g -> g.index(index).id(entityId), Map.class);

// This path runs no query and has no SubjectContext, so it cannot tell whose restricted
// memory a document is: a non-org-wide memory reads as not found rather than leaking.
Expand All @@ -660,8 +657,7 @@ public Response getDocByID(String indexName, String entityId) throws IOException
}
} catch (OpenSearchException e) {
if (e.status() == 404) {
throw new SearchIndexNotFoundException(
String.format("Failed to find doc with id %s", entityId));
throw SearchEngineErrors.indexNotFound(index);
} else {
throw new SearchException(String.format("Search failed due to %s", e.getMessage()));
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,6 @@
import org.openmetadata.schema.type.Include;
import org.openmetadata.schema.utils.JsonUtils;
import org.openmetadata.sdk.exception.SearchException;
import org.openmetadata.sdk.exception.SearchIndexNotFoundException;
import org.openmetadata.service.Entity;
import org.openmetadata.service.config.CacheConfiguration;
import org.openmetadata.service.jdbi3.ListFilter;
Expand Down Expand Up @@ -354,7 +353,7 @@ private SearchResultListMapper doListWithOffset(
return new SearchResultListMapper(results, totalHits);
} catch (OpenSearchException e) {
if (e.status() == 404) {
throw new SearchIndexNotFoundException(String.format("Failed to find index %s", index));
throw SearchEngineErrors.indexNotFound(index);
} else {
throw buildSearchException(e);
}
Expand Down Expand Up @@ -555,7 +554,7 @@ public SearchResultListMapper listWithDeepPagination(
results, totalHits, lastHitSortValues, lastDocumentsInBatch);
} catch (OpenSearchException e) {
if (e.status() == 404) {
throw new SearchIndexNotFoundException(String.format("Failed to find index %s", index));
throw SearchEngineErrors.indexNotFound(index);
} else {
throw buildSearchException(e);
}
Expand Down Expand Up @@ -1216,8 +1215,7 @@ public Response doSearch(

} catch (OpenSearchException e) {
if (e.status() == 404) {
throw new SearchIndexNotFoundException(
String.format("Failed to find index %s", request.getIndex()));
throw SearchEngineErrors.indexNotFound(request.getIndex());
} else {
throw buildSearchException(e);
}
Expand Down Expand Up @@ -1404,8 +1402,7 @@ public SearchResultListMapper searchForExport(
return new SearchResultListMapper(results, totalHits, lastHitSortValues);
} catch (OpenSearchException e) {
if (e.status() == 404) {
throw new SearchIndexNotFoundException(
String.format("Failed to find index %s", request.getIndex()));
throw SearchEngineErrors.indexNotFound(request.getIndex());
} else {
throw buildSearchException(e);
}
Expand Down
Loading
Loading