From 72b7051ff1198d876067a85ff8d3259832c1a039 Mon Sep 17 00:00:00 2001 From: sonika-shah <58761340+sonika-shah@users.noreply.github.com> Date: Tue, 13 Oct 2026 08:41:16 +0530 Subject: [PATCH 1/2] Fixes #34954: make the glossary hierarchy search IT independent of other tests The hierarchy search IT waited for "search(word) total == 30" over the whole shared index. Glossary search matches fuzzily and on name n-grams, so other tests' terms counted too and it failed in the parallel lane with 33. Its page and child-count checks had the same exposure, and it never cleaned up its terms. - No wait or global count: entity create indexes synchronously with a refresh, so the terms are searchable when create returns. - Every search is post-filtered to the test's own glossary. - The search word uses letters absent from all generated names, so parents can never match it or take a page slot from a match. - The glossary is hard-deleted recursively in finally. - Adds the same check for matches nested four levels deep. --- .../it/tests/GlossaryTermResourceIT.java | 125 +++++++++++------- 1 file changed, 78 insertions(+), 47 deletions(-) diff --git a/openmetadata-integration-tests/src/test/java/org/openmetadata/it/tests/GlossaryTermResourceIT.java b/openmetadata-integration-tests/src/test/java/org/openmetadata/it/tests/GlossaryTermResourceIT.java index 7b988b33605f..d8ecd970c957 100644 --- a/openmetadata-integration-tests/src/test/java/org/openmetadata/it/tests/GlossaryTermResourceIT.java +++ b/openmetadata-integration-tests/src/test/java/org/openmetadata/it/tests/GlossaryTermResourceIT.java @@ -3403,76 +3403,107 @@ void test_glossaryTermSearchIndexUpdatedWhenGlossaryReviewerChanges(TestNamespac @Test void test_hierarchySearchKeepsParentOfEveryMatchOnThePage(TestNamespace ns) throws Exception { + assertHierarchySearchKeepsFullPath(ns, "hier_page", 1); + } + + @Test + void test_hierarchySearchKeepsFullPathOfDeeplyNestedMatches(TestNamespace ns) throws Exception { + assertHierarchySearchKeepsFullPath(ns, "hier_deep", 4); + } + + // More matches than the page holds, nested under parents that don't match the search: every + // match on the page must still come back under its full path. Creates index synchronously with a + // refresh, so no wait is needed; the post_filter keeps other tests' terms out of the result. + private void assertHierarchySearchKeepsFullPath(TestNamespace ns, String prefix, int depth) + throws Exception { OpenMetadataClient client = SdkClients.adminClient(); - ObjectMapper mapper = new ObjectMapper(); - String word = "pagecut" + UUID.randomUUID().toString().replace("-", "").substring(0, 10); + // Letters that never occur in the generated names (hex + "hier_page/deep_g/l/m"), so no n-gram + // of the word can fuzzily match a parent's name and parents never compete with the matches + String word = randomWordFrom("jknoqstuvwxyz", 16); int matchCount = 30; int pageSize = 25; - Glossary glossary = client .glossaries() .create( new CreateGlossary() - .withName(ns.prefix("hier_page_glossary")) - .withDescription("Glossary for hierarchy search paging")); - GlossaryTerm parent = + .withName(ns.shortPrefix(prefix + "_g")) + .withDescription("Glossary for hierarchy search")); + try { + List path = new ArrayList<>(); + for (int level = 0; level < depth; level++) { + path.add( + createEntity( + new CreateGlossaryTerm() + .withName(ns.shortPrefix(prefix + "_l" + level)) + .withGlossary(glossary.getFullyQualifiedName()) + .withParent(level == 0 ? null : path.get(level - 1).getFullyQualifiedName()) + .withDescription("Parent that does not match the search word"))); + } + String deepestParent = path.get(depth - 1).getFullyQualifiedName(); + for (int i = 0; i < matchCount; i++) { createEntity( new CreateGlossaryTerm() - .withName(ns.prefix("hier_page_parent")) + .withName(ns.shortPrefix(prefix + "_m" + i)) .withGlossary(glossary.getFullyQualifiedName()) - .withDescription("Parent that does not match the search word")); - for (int i = 0; i < matchCount; i++) { - createEntity( - new CreateGlossaryTerm() - .withName(ns.prefix("hier_page_child_" + i)) - .withGlossary(glossary.getFullyQualifiedName()) - .withParent(parent.getFullyQualifiedName()) - .withDescription("Child matching " + word)); + .withParent(deepestParent) + .withDescription("Child matching " + word)); + } + + String ownGlossary = + "{\"query\":{\"term\":{\"glossary.name.keyword\":\"" + + glossary.getName().toLowerCase() + + "\"}}}"; + JsonNode node = + findByFqn( + searchGlossaryTerms(client, word, ownGlossary, pageSize, true), + glossary.getFullyQualifiedName()); + assertNotNull(node, "Glossary of the matches should be returned"); + for (GlossaryTerm parent : path) { + node = findByFqn(node.path("children"), parent.getFullyQualifiedName()); + assertNotNull(node, "Parent " + parent.getName() + " should be returned"); + } + assertEquals( + pageSize, + node.path("children").size(), + "Every match on the page should sit under its full path"); + } finally { + client + .glossaries() + .delete( + glossary.getId().toString(), + java.util.Map.of("hardDelete", "true", "recursive", "true")); } + } - Awaitility.await("all matching terms should be indexed") - .atMost(java.time.Duration.ofSeconds(60)) - .pollInterval(java.time.Duration.ofSeconds(1)) - .ignoreExceptions() - .untilAsserted( - () -> { - String response = - client - .search() - .query(word) - .index("glossary_term_search_index") - .size(matchCount) - .execute(); - assertEquals( - matchCount, - mapper.readTree(response).path("hits").path("total").path("value").asInt()); - }); + private static String randomWordFrom(String alphabet, int length) { + java.util.concurrent.ThreadLocalRandom random = + java.util.concurrent.ThreadLocalRandom.current(); + StringBuilder word = new StringBuilder(length); + for (int i = 0; i < length; i++) { + word.append(alphabet.charAt(random.nextInt(alphabet.length()))); + } + return word.toString(); + } - // More matches than the page holds push the non-matching parent out of the ranked page + private static JsonNode searchGlossaryTerms( + OpenMetadataClient client, String query, String postFilter, int size, boolean hierarchy) + throws Exception { RequestOptions options = RequestOptions.builder() - .queryParam("q", word) + .queryParam("q", query) .queryParam("index", "glossary_term_search_index") .queryParam("from", "0") - .queryParam("size", String.valueOf(pageSize)) + .queryParam("size", String.valueOf(size)) .queryParam("deleted", "false") - .queryParam("getHierarchy", "true") + .queryParam("post_filter", postFilter) + .queryParam("getHierarchy", String.valueOf(hierarchy)) .build(); - JsonNode glossaries = - mapper.readTree( + return new ObjectMapper() + .readTree( client .getHttpClient() .executeForString(HttpMethod.GET, "/v1/search/query", null, options)); - - JsonNode glossaryNode = findByFqn(glossaries, glossary.getFullyQualifiedName()); - assertNotNull(glossaryNode, "Glossary of the matches should be returned"); - JsonNode parentNode = findByFqn(glossaryNode.path("children"), parent.getFullyQualifiedName()); - assertNotNull(parentNode, "Parent of the matches should be returned even though it ranks last"); - assertEquals( - pageSize, - parentNode.path("children").size(), - "Every match on the page should sit under its parent"); } private static JsonNode findByFqn(JsonNode nodes, String fqn) { From 9bcc2ce23d2826e74248d0209f8b81bb5c3eab6e Mon Sep 17 00:00:00 2001 From: sonika-shah <58761340+sonika-shah@users.noreply.github.com> Date: Tue, 13 Oct 2026 09:00:11 +0530 Subject: [PATCH 2/2] Hold the search-settings read lock and make cleanup best-effort in hierarchy ITs The hierarchy search ITs depend on the shared glossary-term search settings, so take SEARCH_SETTINGS in READ mode: they never overlap a test that rewrites the settings under the READ_WRITE lock, and still run alongside other readers. A failed cleanup delete no longer fails a test whose assertions passed; the glossary is namespace-scoped. --- .../it/tests/GlossaryTermResourceIT.java | 20 ++++++++++++++----- 1 file changed, 15 insertions(+), 5 deletions(-) diff --git a/openmetadata-integration-tests/src/test/java/org/openmetadata/it/tests/GlossaryTermResourceIT.java b/openmetadata-integration-tests/src/test/java/org/openmetadata/it/tests/GlossaryTermResourceIT.java index d8ecd970c957..a135727b3f48 100644 --- a/openmetadata-integration-tests/src/test/java/org/openmetadata/it/tests/GlossaryTermResourceIT.java +++ b/openmetadata-integration-tests/src/test/java/org/openmetadata/it/tests/GlossaryTermResourceIT.java @@ -22,11 +22,14 @@ import org.junit.jupiter.api.Test; import org.junit.jupiter.api.parallel.Execution; import org.junit.jupiter.api.parallel.ExecutionMode; +import org.junit.jupiter.api.parallel.ResourceAccessMode; +import org.junit.jupiter.api.parallel.ResourceLock; import org.openmetadata.it.auth.JwtAuthProvider; import org.openmetadata.it.factories.DatabaseSchemaTestFactory; import org.openmetadata.it.factories.DatabaseServiceTestFactory; import org.openmetadata.it.factories.GlossaryTestFactory; import org.openmetadata.it.util.SdkClients; +import org.openmetadata.it.util.SharedResourceLocks; import org.openmetadata.it.util.TestNamespace; import org.openmetadata.schema.api.AddGlossaryToAssetsRequest; import org.openmetadata.schema.api.data.CreateGlossary; @@ -3402,11 +3405,13 @@ void test_glossaryTermSearchIndexUpdatedWhenGlossaryReviewerChanges(TestNamespac } @Test + @ResourceLock(value = SharedResourceLocks.SEARCH_SETTINGS, mode = ResourceAccessMode.READ) void test_hierarchySearchKeepsParentOfEveryMatchOnThePage(TestNamespace ns) throws Exception { assertHierarchySearchKeepsFullPath(ns, "hier_page", 1); } @Test + @ResourceLock(value = SharedResourceLocks.SEARCH_SETTINGS, mode = ResourceAccessMode.READ) void test_hierarchySearchKeepsFullPathOfDeeplyNestedMatches(TestNamespace ns) throws Exception { assertHierarchySearchKeepsFullPath(ns, "hier_deep", 4); } @@ -3468,11 +3473,16 @@ private void assertHierarchySearchKeepsFullPath(TestNamespace ns, String prefix, node.path("children").size(), "Every match on the page should sit under its full path"); } finally { - client - .glossaries() - .delete( - glossary.getId().toString(), - java.util.Map.of("hardDelete", "true", "recursive", "true")); + // Best-effort: a cleanup hiccup must not fail a test whose assertions passed + try { + client + .glossaries() + .delete( + glossary.getId().toString(), + java.util.Map.of("hardDelete", "true", "recursive", "true")); + } catch (Exception ignored) { + // the namespace-scoped glossary cannot affect other tests' assertions + } } }