Skip to content
Open
Original file line number Diff line number Diff line change
Expand Up @@ -106,7 +106,57 @@ public class TestSuiteBootstrap implements LauncherSessionListener {
private static final Integer ELASTIC_BATCH_SIZE = 10;
private static final IndexMappingLanguage ELASTIC_SEARCH_INDEX_MAPPING_LANGUAGE =
IndexMappingLanguage.EN;
private static final String ELASTIC_SEARCH_CLUSTER_ALIAS = "openmetadata";

/**
* Pattern allowed for {@code -DclusterAlias} overrides — must be a valid OpenSearch /
* Elasticsearch index name prefix (lowercase alphanumeric, underscore, or hyphen; must start
* with a letter or digit; max 63 chars).
*
* <p>Declared <em>before</em> {@link #ELASTIC_SEARCH_CLUSTER_ALIAS} on purpose: static fields
* initialize in declaration order, and {@link #resolveClusterAlias()} reads this pattern. If
* this declaration moved below, override validation would NPE on the only path that uses it.
*/
private static final java.util.regex.Pattern CLUSTER_ALIAS_PATTERN =
java.util.regex.Pattern.compile("[a-z0-9][a-z0-9_\\-]{0,62}");

/**
* Cluster alias used as the prefix for all search indices in this test session.
*
* <p>The OpenSearch / Elasticsearch testcontainer is shared across the entire JUnit launcher
* session (single static container, see {@link #SEARCH_CONTAINER}). When tests run in parallel
* (the {@code parallel-tests} profile sets {@code junit.jupiter.execution.parallel.enabled=true}
* and {@code reuseForks=true} keeps everything in one JVM), every test reads and writes against
* the same set of indices. {@link org.openmetadata.it.util.TestNamespace} only isolates entity
* FQNs in the database — it does not isolate documents in the search index.
*
* <p>To prevent cross-test pollution between concurrent CI runs that share the cluster, the alias
* is randomized per session by default so each session writes to its own {@code <alias>_*}
* indices. Set {@code -DclusterAlias=openmetadata} (or any fixed value matching {@link
* #CLUSTER_ALIAS_PATTERN}) to pin the alias for reproducible debugging.
*/
private static final String ELASTIC_SEARCH_CLUSTER_ALIAS = resolveClusterAlias();

Comment on lines +137 to +138
private static String resolveClusterAlias() {
String override = System.getProperty("clusterAlias");
if (override == null || override.isBlank()) {
return "omtest_" + java.util.UUID.randomUUID().toString().replace("-", "").substring(0, 8);
}
String normalized = override.trim().toLowerCase(java.util.Locale.ROOT);
if (!CLUSTER_ALIAS_PATTERN.matcher(normalized).matches()) {
throw new IllegalArgumentException(
"Invalid -DclusterAlias='"
+ override
+ "'. Must match "
+ CLUSTER_ALIAS_PATTERN.pattern()
+ " (lowercase alphanumeric, underscore, or hyphen; must start with a letter or"
+ " digit; max 63 chars) so it forms a valid OpenSearch/Elasticsearch index prefix.");
}
return normalized;
}
Comment on lines +139 to +155

public static String getClusterAlias() {
return ELASTIC_SEARCH_CLUSTER_ALIAS;
}
Comment on lines +110 to +159

// Default images (can be overridden by system properties)
private static final String DEFAULT_POSTGRES_IMAGE = "postgres:15";
Expand Down Expand Up @@ -164,6 +214,7 @@ public void launcherSessionOpened(LauncherSession session) {
LOG.info("=== TestSuiteBootstrap: Starting test infrastructure ===");
LOG.info("Database type: {}", databaseType);
LOG.info("Search type: {}", searchType);
LOG.info("Search cluster alias: {}", ELASTIC_SEARCH_CLUSTER_ALIAS);
LOG.info("RDF enabled: {}", rdfEnabled);
Comment thread
mohityadav766 marked this conversation as resolved.
LOG.info("Cache provider: {}", cacheProvider);
boolean k8sEnabled = isK8sTestsRequested();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -427,12 +427,16 @@ private HttpResponse<String> exportGlossaryRaw(
default -> TURTLE_CONTENT_TYPE;
};

// RDF/XML serialization in Jena is significantly slower than Turtle/N-Triples/JSON-LD
// (O(N^2)-ish QName resolution) and contends with Quartz/WorkflowEventConsumer daemon
// threads that @Isolated does not stop. Observed ~69s server time in CI for what is
// typically a sub-second call; 180s gives headroom without masking real hangs.
HttpRequest request =
HttpRequest.newBuilder()
.uri(URI.create(url))
.header("Authorization", "Bearer " + token)
.header("Accept", acceptHeader)
.timeout(Duration.ofSeconds(60))
.timeout(Duration.ofSeconds(180))
.GET()
.build();
Comment on lines +430 to 441

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1085,40 +1085,43 @@ void patch_addDeleteReviewers(TestNamespace ns) {

@Test
void patch_addDeleteReferences(TestNamespace ns) {
OpenMetadataClient client = SdkClients.adminClient();
Glossary glossary = getOrCreateGlossary(ns);

// Create term without references
CreateGlossaryTerm request =
new CreateGlossaryTerm()
.withName(ns.prefix("term_references"))
.withGlossary(glossary.getFullyQualifiedName())
.withDescription("Term for reference patch test");
GlossaryTerm term = createEntity(request);
String termId = term.getId().toString();

// Add reference
// Refresh local state before each patch so the JSON diff does not accidentally include an
// entityStatus transition driven by the async GlossaryTermApprovalWorkflow (which can move
// the server-side status to IN_REVIEW between calls and would otherwise trip the reviewer
// check in EntityRepository.checkUpdatedByReviewer).
org.openmetadata.schema.api.data.TermReference ref1 =
new org.openmetadata.schema.api.data.TermReference()
.withName("reference1")
.withEndpoint(java.net.URI.create("http://reference1.example.com"));
term = getEntity(termId);
term.setReferences(List.of(ref1));
GlossaryTerm updated = patchEntity(term.getId().toString(), term);
GlossaryTerm updated = patchEntity(termId, term);
assertNotNull(updated.getReferences());
assertEquals(1, updated.getReferences().size());

// Add another reference
org.openmetadata.schema.api.data.TermReference ref2 =
new org.openmetadata.schema.api.data.TermReference()
.withName("reference2")
.withEndpoint(java.net.URI.create("http://reference2.example.com"));
updated = getEntity(termId);
updated.setReferences(List.of(ref1, ref2));
GlossaryTerm updated2 = patchEntity(updated.getId().toString(), updated);
GlossaryTerm updated2 = patchEntity(termId, updated);
assertNotNull(updated2.getReferences());
assertEquals(2, updated2.getReferences().size());

// Remove a reference
updated2 = getEntity(termId);
updated2.setReferences(List.of(ref2));
GlossaryTerm updated3 = patchEntity(updated2.getId().toString(), updated2);
GlossaryTerm updated3 = patchEntity(termId, updated2);
assertNotNull(updated3.getReferences());
assertEquals(1, updated3.getReferences().size());
}
Expand Down Expand Up @@ -2265,7 +2268,6 @@ void test_glossaryTermVersionIncrement(TestNamespace ns) {

@Test
void test_glossaryTermReviewersMultipleUpdates(TestNamespace ns) {
OpenMetadataClient client = SdkClients.adminClient();
Glossary glossary = getOrCreateGlossary(ns);

CreateGlossaryTerm request =
Expand All @@ -2274,20 +2276,28 @@ void test_glossaryTermReviewersMultipleUpdates(TestNamespace ns) {
.withGlossary(glossary.getFullyQualifiedName())
.withDescription("Term for multiple reviewer updates");
GlossaryTerm term = createEntity(request);
String termId = term.getId().toString();

// Refresh local state before each patch. The async GlossaryTermApprovalWorkflow promotes
// entityStatus DRAFT -> IN_REVIEW once reviewers exist; sending a stale local copy causes
// the JSON diff to include an entityStatus transition that trips
// EntityRepository.checkUpdatedByReviewer (admin is not in the reviewer list).
term = getEntity(termId);
term.setReviewers(List.of(testUser1().getEntityReference()));
GlossaryTerm updated1 = patchEntity(term.getId().toString(), term);
GlossaryTerm updated1 = patchEntity(termId, term);
assertNotNull(updated1.getReviewers());
assertEquals(1, updated1.getReviewers().size());

updated1 = getEntity(termId);
updated1.setReviewers(
List.of(testUser1().getEntityReference(), testUser2().getEntityReference()));
GlossaryTerm updated2 = patchEntity(updated1.getId().toString(), updated1);
GlossaryTerm updated2 = patchEntity(termId, updated1);
assertNotNull(updated2.getReviewers());
assertTrue(updated2.getReviewers().size() >= 2);

updated2 = getEntity(termId);
updated2.setReviewers(List.of(testUser2().getEntityReference()));
GlossaryTerm updated3 = patchEntity(updated2.getId().toString(), updated2);
GlossaryTerm updated3 = patchEntity(termId, updated2);
assertNotNull(updated3.getReviewers());
assertEquals(1, updated3.getReviewers().size());
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@
public class IndexTemplateIT {

private static final ObjectMapper MAPPER = new ObjectMapper();
private static final String CLUSTER_ALIAS = "openmetadata";
private static final String CLUSTER_ALIAS = TestSuiteBootstrap.getClusterAlias();

@Test
void testIndexTemplatesExist(TestNamespace ns) throws Exception {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@
@TestMethodOrder(OrderAnnotation.class)
public class OrphanedIndexCleanerScopedCleanupIT {

private static final String CLUSTER_ALIAS = "openmetadata";
private static final String CLUSTER_ALIAS = TestSuiteBootstrap.getClusterAlias();
private static final String FOREIGN_PREFIX = "foreigntenant_it_orphans";
private static final String OUR_PREFIX = CLUSTER_ALIAS + "_it_orphans";

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -65,8 +65,10 @@ public class SearchIndexFieldLimitIT {
private static final ObjectMapper OBJECT_MAPPER = new ObjectMapper();
private static final String TABLE_TYPE_NAME = "table";
private static final int NUM_CUSTOM_PROPERTIES = 50;
// Index name with cluster alias prefix (from TestSuiteBootstrap.ELASTIC_SEARCH_CLUSTER_ALIAS)
private static final String TABLE_INDEX = "openmetadata_table_search_index";
// Index name uses the cluster alias resolved by TestSuiteBootstrap (randomized per session by
// default; pin with -DclusterAlias=... for reproducible debugging).
private static final String TABLE_INDEX =
org.openmetadata.it.bootstrap.TestSuiteBootstrap.getClusterAlias() + "_table_search_index";

private static Type STRING_TYPE;
private static Type INTEGER_TYPE;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4586,11 +4586,13 @@ protected Table getEntityIncludeDeleted(String id) {
// ===================================================================

/**
* Get the full Elasticsearch index name with cluster alias prefix.
* In test environment, cluster alias is "openmetadata" so table index is "openmetadata_table_search_index"
* Get the full Elasticsearch index name with cluster alias prefix. The alias is randomized per
* JUnit session by default in {@link org.openmetadata.it.bootstrap.TestSuiteBootstrap}; it can
* be pinned via {@code -DclusterAlias=...} for reproducible debugging.
*/
private String getTableSearchIndexName() {
return "openmetadata_table_search_index";
return org.openmetadata.it.bootstrap.TestSuiteBootstrap.getClusterAlias()
+ "_table_search_index";
Comment on lines +4594 to +4595
Comment on lines +4594 to +4595
}

// ===================================================================
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1139,7 +1139,8 @@ private void putPipelineStatus(
}

private String getTestSuiteSearchIndexName() {
return "openmetadata_test_suite_search_index";
return org.openmetadata.it.bootstrap.TestSuiteBootstrap.getClusterAlias()
+ "_test_suite_search_index";
Comment on lines +1142 to +1143
Comment on lines +1142 to +1143
}

private void refreshTestSuiteSearchIndex(Rest5Client searchClient) throws Exception {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2173,45 +2173,49 @@ void testUserContextCachePerformance(TestNamespace ns) throws HttpResponseExcept

SubjectCache.invalidateAll();

// Warm up JVM (exclude from measurements)
for (int i = 0; i < 3; i++) {
// Warm up JVM (exclude from measurements). More iterations than before so JIT has had a
// chance to compile the SubjectContext.getSubjectContext path before we measure anything.
for (int i = 0; i < 20; i++) {
SubjectContext.getSubjectContext(userName);
}
SubjectCache.invalidateAll();

// Test 1: Cache Miss (First call - should be slower)
long cacheMissStartTime = System.nanoTime();
SubjectContext context1 = SubjectContext.getSubjectContext(userName);
double cacheMissTime = (System.nanoTime() - cacheMissStartTime) / 1_000_000.0;
assertNotNull(context1);
assertEquals(userName, context1.user().getName());
// Test 1: Cache Miss (multiple samples — invalidate before each)
// A single nanoTime sample at sub-millisecond scale is dominated by GC, JIT, and OS
// scheduling jitter, which produced flaky -100%+ "improvement" failures. Take the median
// across N runs to suppress that noise.
int sampleCount = 7;
List<Double> cacheMissTimes = new ArrayList<>(sampleCount);
for (int i = 0; i < sampleCount; i++) {
SubjectCache.invalidateAll();
long start = System.nanoTime();
SubjectContext miss = SubjectContext.getSubjectContext(userName);
Comment on lines 2174 to +2191
cacheMissTimes.add((System.nanoTime() - start) / 1_000_000.0);
assertNotNull(miss);
assertEquals(userName, miss.user().getName());
}

// Test 2: Cache Hit (Multiple subsequent calls - should be much faster)
// Test 2: Cache Hit (many samples, no invalidate)
List<Double> cacheHitTimes = new ArrayList<>();
for (int i = 0; i < 10; i++) {
long cacheHitStartTime = System.nanoTime();
SubjectContext context = SubjectContext.getSubjectContext(userName);
double cacheHitTime = (System.nanoTime() - cacheHitStartTime) / 1_000_000.0;

cacheHitTimes.add(cacheHitTime);
assertNotNull(context);
assertEquals(userName, context.user().getName());
for (int i = 0; i < 50; i++) {
long start = System.nanoTime();
SubjectContext hit = SubjectContext.getSubjectContext(userName);
cacheHitTimes.add((System.nanoTime() - start) / 1_000_000.0);
assertNotNull(hit);
assertEquals(userName, hit.user().getName());
}

// Calculate cache hit performance statistics
double avgCacheHitTime =
cacheHitTimes.stream().mapToDouble(Double::doubleValue).average().orElse(0.0);

// Performance assertions
double performanceImprovement =
cacheMissTime > 0 ? ((cacheMissTime - avgCacheHitTime) / cacheMissTime) * 100 : 0.0;
double medianMiss = median(cacheMissTimes);
double medianHit = median(cacheHitTimes);
double avgCacheHitTime = cacheHitTimes.stream().mapToDouble(Double::doubleValue).average().orElse(0.0);

// Assert significant performance improvement
// Sanity: cache hit median should be at least as fast as cache miss median. We don't assert
// a percentage improvement — at sub-millisecond scale it's not statistically meaningful and
// produces flaky failures. The absolute regression bound below catches real regressions.
assertTrue(
performanceImprovement > 30.0,
medianHit <= medianMiss,
String.format(
"Expected >30%% improvement, got %.1f%% (%.3fms -> %.3fms)",
performanceImprovement, cacheMissTime, avgCacheHitTime));
"Cache hit should not be slower than miss at the median (miss=%.3fms hit=%.3fms)",
medianMiss, medianHit));
Comment on lines 2215 to +2219
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Edge Case: Median cache-hit <= cache-miss assertion can still flake

At sub-millisecond scale, medianHit <= medianMiss (strict ≤) can still flake: a warm JIT + hot CPU cache means a cache-miss sample that stays resident can complete as fast as a hit, or GC/scheduling noise can make a single median crossover. Consider adding a small tolerance (e.g., medianHit <= medianMiss + 0.5) or dropping the ordering assertion entirely and relying solely on the absolute < 200 ms bound, which is the meaningful regression gate.

Suggested fix:

assertTrue(
    medianHit <= medianMiss + 0.5,
    String.format(
        "Cache hit should not be materially slower than miss (miss=%.3fms hit=%.3fms)",
        medianMiss, medianHit));

Was this helpful? React with 👍 / 👎 | Reply gitar fix to apply this suggestion

assertTrue(
avgCacheHitTime < 200,
String.format("Cache hits should be <200ms, got %.3fms", avgCacheHitTime));
Expand Down Expand Up @@ -2286,6 +2290,15 @@ void testUserContextCachePerformance(TestNamespace ns) throws HttpResponseExcept
deleteEntity(testUser.getId().toString());
}

private static double median(List<Double> values) {
List<Double> sorted = values.stream().sorted().toList();
int n = sorted.size();
if (n == 0) return 0.0;
return n % 2 == 1
? sorted.get(n / 2)
: (sorted.get(n / 2 - 1) + sorted.get(n / 2)) / 2.0;
}

// ===================================================================
// VERSION HISTORY SUPPORT
// ===================================================================
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -294,6 +294,17 @@
}
}
},
"displayName": {
"type": "text",
"analyzer": "om_analyzer",
"fields": {
"keyword": {
"type": "keyword",
"normalizer": "lowercase_normalizer",
"ignore_above": 256
}
}
},
Comment on lines +297 to +307
"fullyQualifiedName": {
"type": "text"
},
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -183,6 +183,17 @@
}
}
},
"displayName": {
"type": "text",
"analyzer": "om_analyzer",
"fields": {
"keyword": {
"type": "keyword",
"normalizer": "lowercase_normalizer",
"ignore_above": 256
}
}
},
"fullyQualifiedName": {
"type": "text"
},
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -307,6 +307,17 @@
}
}
},
"displayName": {
"type": "text",
"analyzer": "om_analyzer",
"fields": {
"keyword": {
"type": "keyword",
"normalizer": "lowercase_normalizer",
"ignore_above": 256
}
}
},
"fullyQualifiedName": {
"type": "text"
},
Expand Down
Loading
Loading