Skip to content

Commit 7f25b6d

Browse files
chore(ci): update gradle config, test infrastructure, and testng suit… (#15207)
1 parent 6f81e72 commit 7f25b6d

28 files changed

+579
-81
lines changed

gradle.properties

Lines changed: 17 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,9 +5,23 @@ org.gradle.caching=true
55
# Cycle daemons after 30m
66
org.gradle.daemon.idletimeout=1800000
77

8-
# Increase gradle JVM memory to 5GB to allow tests to run locally
9-
org.gradle.jvmargs=-Xmx5120m -XX:MaxMetaspaceSize=512m
10-
org.gradle.workers.max=4
8+
# Gradle JVM memory configuration
9+
# Default: 2GB heap (optimized for 8GB systems)
10+
# Override options:
11+
# 1. Memory Profile (recommended): Set GRADLE_MEMORY_PROFILE env var or -Dgradle.memory.profile
12+
# - "low": 2GB heap, 384MB metaspace (8GB systems)
13+
# - "default": 5GB heap, 512MB metaspace (16GB+ systems)
14+
# - "high": 8GB heap, 768MB metaspace (32GB+ systems)
15+
# 2. Direct override for org.gradle.jvmargs (requires stopping daemon first):
16+
# ./gradlew --stop
17+
# ./gradlew -Dorg.gradle.jvmargs="-Xmx5g -XX:MaxMetaspaceSize=512m" build
18+
# 3. Via GRADLE_OPTS environment variable:
19+
# export GRADLE_OPTS="-Xmx5g -XX:MaxMetaspaceSize=512m"
20+
# 4. Via ~/.gradle/gradle.properties (user-specific, takes precedence)
21+
# Note: org.gradle.jvmargs affects the Gradle daemon JVM, not test processes
22+
# Test process memory is controlled by build.gradle based on memory profile
23+
org.gradle.jvmargs=-Xmx2g -XX:MaxMetaspaceSize=384m
24+
org.gradle.workers.max=2
1125

1226
# Increase retries to 5 (from default of 3) and increase interval from 125ms to 1s.
1327
# Based on this thread https://github.com/gradle/gradle/issues/4629, it's unclear

metadata-io/build.gradle

Lines changed: 164 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,98 @@ plugins {
77

88
apply from: '../gradle/coverage/java-coverage.gradle'
99

10+
// Memory profile configuration - supports environment variable GRADLE_MEMORY_PROFILE or system property
11+
// Values: "low" (8GB systems), "default" (16GB+ systems), "high" (32GB+ systems)
12+
// Auto-detects system memory if not explicitly set
13+
def detectMemoryProfile() {
14+
// Check if explicitly set via environment variable or system property
15+
def explicitProfile = System.getenv("GRADLE_MEMORY_PROFILE") ?: System.getProperty("gradle.memory.profile")
16+
if (explicitProfile) {
17+
return explicitProfile
18+
}
19+
20+
// Auto-detect based on actual system memory (total RAM) using OperatingSystemMXBean
21+
try {
22+
def osBean = java.lang.management.ManagementFactory.getOperatingSystemMXBean()
23+
def sunOsBean = Class.forName("com.sun.management.OperatingSystemMXBean")
24+
if (!sunOsBean.isInstance(osBean)) {
25+
throw new Exception("OperatingSystemMXBean does not support getTotalPhysicalMemorySize()")
26+
}
27+
28+
def method = sunOsBean.getMethod("getTotalPhysicalMemorySize")
29+
def totalMemoryBytes = method.invoke(osBean) as Long
30+
def totalMemoryGB = totalMemoryBytes / (1024.0 * 1024.0 * 1024.0)
31+
32+
def detectedProfile
33+
if (totalMemoryGB < 12) {
34+
detectedProfile = "low" // < 12GB: use low profile (8GB systems)
35+
} else if (totalMemoryGB < 24) {
36+
detectedProfile = "default" // 12-24GB: use default profile (16GB+ systems)
37+
} else {
38+
detectedProfile = "high" // >= 24GB: use high profile (32GB+ systems)
39+
}
40+
41+
println "Auto-detected memory profile: ${detectedProfile} (System RAM: ${String.format("%.1f", totalMemoryGB)}GB)"
42+
return detectedProfile
43+
} catch (Exception e) {
44+
// Fallback to default if detection fails
45+
println "Warning: Could not detect system memory (${e.message}), using 'default' profile"
46+
return "default"
47+
}
48+
}
49+
50+
def memoryProfile = detectMemoryProfile()
51+
52+
// Define memory configurations per profile
53+
// since it's read before build.gradle runs
54+
def memoryConfigs = [
55+
"low": [
56+
testHeap: "1g",
57+
testMinHeap: "256m",
58+
testMetaspace: "384m",
59+
// Docker container memory settings (for ES/OS/Cassandra/Neo4j) - applied via system properties
60+
elasticsearchHeap: "256m",
61+
opensearchHeap: "512m",
62+
cassandraHeap: "128m",
63+
neo4jHeap: "256m"
64+
],
65+
"default": [
66+
testHeap: "2g",
67+
testMinHeap: "512m",
68+
testMetaspace: "512m",
69+
elasticsearchHeap: "512m",
70+
opensearchHeap: "1024m",
71+
cassandraHeap: "128m",
72+
neo4jHeap: "512m"
73+
],
74+
"high": [
75+
testHeap: "3g",
76+
testMinHeap: "1g",
77+
testMetaspace: "768m",
78+
elasticsearchHeap: "768m",
79+
opensearchHeap: "1536m",
80+
cassandraHeap: "256m",
81+
neo4jHeap: "768m"
82+
]
83+
]
84+
85+
def memoryConfig = memoryConfigs[memoryProfile] ?: memoryConfigs["low"]
86+
println "Using memory profile: ${memoryProfile}"
87+
println " Test processes: ${memoryConfig.testHeap} heap (min ${memoryConfig.testMinHeap}), ${memoryConfig.testMetaspace} metaspace"
88+
println " Docker containers:"
89+
println " - Elasticsearch: ${memoryConfig.elasticsearchHeap}"
90+
println " - OpenSearch: ${memoryConfig.opensearchHeap}"
91+
println " - Cassandra: ${memoryConfig.cassandraHeap}"
92+
println " - Neo4j: ${memoryConfig.neo4jHeap}"
93+
println " Note: Gradle daemon memory is set in gradle.properties (see file for details)"
94+
println " Override: Set GRADLE_MEMORY_PROFILE environment variable or -Dgradle.memory.profile system property"
95+
96+
// Set system properties for container memory configuration (containers read these at runtime)
97+
System.setProperty("testcontainers.elasticsearch.heap", memoryConfig.elasticsearchHeap)
98+
System.setProperty("testcontainers.opensearch.heap", memoryConfig.opensearchHeap)
99+
System.setProperty("testcontainers.cassandra.heap", memoryConfig.cassandraHeap)
100+
System.setProperty("testcontainers.neo4j.heap", memoryConfig.neo4jHeap)
101+
10102
configurations {
11103
enhance
12104
}
@@ -151,12 +243,84 @@ test {
151243
environment 'STRICT_URN_VALIDATION_ENABLED', 'true'
152244
}
153245

246+
task testEs7CassandraNeo4j(type: Test) {
247+
doFirst {
248+
maxParallelForks = 1
249+
}
250+
useTestNG() {
251+
suites 'src/test/resources/testng-es7-cassandra-neo4j.xml'
252+
}
253+
testLogging {
254+
events "failed", "skipped", "passed"
255+
exceptionFormat "full"
256+
showStandardStreams = true
257+
}
258+
259+
environment 'STRICT_URN_VALIDATION_ENABLED', 'true'
260+
261+
group = 'verification'
262+
description = 'Runs only the testng-es7-cassandra-neo4j.xml test suite'
263+
}
264+
265+
task testOpenSearch(type: Test) {
266+
doFirst {
267+
maxParallelForks = 1
268+
}
269+
useTestNG() {
270+
suites 'src/test/resources/testng-opensearch.xml'
271+
}
272+
testLogging {
273+
events "failed", "skipped", "passed"
274+
exceptionFormat "full"
275+
showStandardStreams = true
276+
}
277+
278+
environment 'STRICT_URN_VALIDATION_ENABLED', 'true'
279+
280+
group = 'verification'
281+
description = 'Runs only the testng-opensearch.xml test suite'
282+
}
283+
284+
task testOther(type: Test) {
285+
doFirst {
286+
maxParallelForks = 1
287+
}
288+
useTestNG() {
289+
suites 'src/test/resources/testng-other.xml'
290+
}
291+
testLogging {
292+
events "failed", "skipped", "passed", "started"
293+
exceptionFormat "full"
294+
showStandardStreams = true
295+
}
296+
297+
environment 'STRICT_URN_VALIDATION_ENABLED', 'true'
298+
299+
group = 'verification'
300+
description = 'Runs only other test suite to debug the failing tests'
301+
}
302+
154303
ebean {
155304
debugLevel = 1 // 0 - 9
156305
}
157306

158307
tasks.withType(Test) {
159308
enableAssertions = false
309+
// Set heap size for test JVM processes based on memory profile
310+
// This is separate from gradle.properties which only sets memory for the Gradle daemon
311+
maxHeapSize = memoryConfig.testHeap
312+
minHeapSize = memoryConfig.testMinHeap
313+
314+
// Add JVM args for memory efficiency and to prevent OOM kills
315+
// -XX:MaxMetaspaceSize: Limit metaspace to prevent unbounded growth
316+
// -XX:+UseG1GC: Use G1 garbage collector for better memory efficiency
317+
// -XX:MaxGCPauseMillis: Target pause time for GC (higher for low-memory systems)
318+
def gcPauseMillis = memoryProfile == "low" ? "1000" : "500"
319+
jvmArgs = [
320+
"-XX:MaxMetaspaceSize=${memoryConfig.testMetaspace}",
321+
"-XX:+UseG1GC",
322+
"-XX:MaxGCPauseMillis=${gcPauseMillis}"
323+
]
160324
}
161325

162326
clean {

metadata-io/src/test/java/com/linkedin/metadata/CassandraTestUtils.java

Lines changed: 28 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
import java.util.Map;
1818
import java.util.stream.Collectors;
1919
import javax.annotation.Nonnull;
20+
import javax.annotation.Nullable;
2021
import javax.net.ssl.SSLContext;
2122
import lombok.extern.slf4j.Slf4j;
2223
import org.testcontainers.cassandra.CassandraContainer;
@@ -33,9 +34,18 @@ private CassandraTestUtils() {}
3334
public static CassandraContainer setupContainer() {
3435
final DockerImageName imageName = DockerImageName.parse(IMAGE_NAME);
3536

37+
// Read heap size from system property, default to 128m for backwards compatibility
38+
// System property is set by build.gradle based on GRADLE_MEMORY_PROFILE
39+
String cassandraHeap = System.getProperty("testcontainers.cassandra.heap", "128m");
3640
CassandraContainer container =
3741
new CassandraContainer(imageName)
38-
.withEnv("JVM_OPTS", "-Xms128M -Xmx128M -Djava.net.preferIPv4Stack=true")
42+
.withEnv(
43+
"JVM_OPTS",
44+
"-Xms"
45+
+ cassandraHeap
46+
+ " -Xmx"
47+
+ cassandraHeap
48+
+ " -Djava.net.preferIPv4Stack=true")
3949
.withStartupTimeout(Duration.ofMinutes(2))
4050
.withReuse(true);
4151

@@ -185,4 +195,21 @@ public static void purgeData(CassandraContainer container) {
185195
assertEquals(rs.size(), 0);
186196
}
187197
}
198+
199+
/**
200+
* Close a CqlSession to prevent Netty thread leaks (e.g., "s15-admin-0" threads). Each CqlSession
201+
* creates Netty event loop threads that must be closed.
202+
*
203+
* @param session The CqlSession instance to close, or null if none
204+
*/
205+
public static void closeSession(@Nullable CqlSession session) {
206+
if (session != null && !session.isClosed()) {
207+
try {
208+
session.close();
209+
} catch (Exception e) {
210+
// Log but don't fail - this is called from test cleanup
211+
System.err.println("Failed to close CqlSession: " + e.getMessage());
212+
}
213+
}
214+
}
188215
}

metadata-io/src/test/java/com/linkedin/metadata/aspect/utils/DefaultAspectsUtilTest.java

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,16 +23,19 @@
2323
import java.util.List;
2424
import java.util.stream.Collectors;
2525
import org.testng.Assert;
26+
import org.testng.annotations.AfterMethod;
2627
import org.testng.annotations.Test;
2728

2829
public class DefaultAspectsUtilTest {
2930

31+
private Database server;
32+
3033
public DefaultAspectsUtilTest() {}
3134

3235
@Test
3336
public void testAdditionalChanges() {
3437
OperationContext opContext = TestOperationContexts.systemContextNoSearchAuthorization();
35-
Database server = EbeanTestUtils.createTestServer(DefaultAspectsUtilTest.class.getSimpleName());
38+
server = EbeanTestUtils.createTestServer(DefaultAspectsUtilTest.class.getSimpleName());
3639
EbeanAspectDao aspectDao = new EbeanAspectDao(server, EbeanConfiguration.testDefault, null);
3740
aspectDao.setConnectionValidated(true);
3841
EventProducer mockProducer = mock(EventProducer.class);
@@ -75,4 +78,9 @@ public void testAdditionalChanges() {
7578
.collect(Collectors.toList()),
7679
List.of(ChangeType.CREATE, ChangeType.CREATE, ChangeType.CREATE, ChangeType.CREATE));
7780
}
81+
82+
@AfterMethod
83+
public void cleanup() {
84+
EbeanTestUtils.shutdownDatabase(server);
85+
}
7886
}

metadata-io/src/test/java/com/linkedin/metadata/elasticsearch/update/ESBulkProcessorTest.java

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@
2727
import org.opensearch.index.reindex.UpdateByQueryRequest;
2828
import org.opensearch.script.Script;
2929
import org.opensearch.script.ScriptType;
30+
import org.testng.annotations.AfterMethod;
3031
import org.testng.annotations.BeforeMethod;
3132
import org.testng.annotations.Test;
3233

@@ -42,8 +43,27 @@ public class ESBulkProcessorTest {
4243
private String TEST_TASK_STRING = "nodeid123:1234";
4344

4445
@BeforeMethod
45-
public void setup() {
46+
public void setup() throws IOException {
4647
mocks = MockitoAnnotations.openMocks(this);
48+
// Stub generateBulkProcessor and generateAsyncBulkProcessor to do nothing
49+
// This prevents any real BulkProcessor instances from being created
50+
doNothing()
51+
.when(mockSearchClient)
52+
.generateBulkProcessor(any(), any(), anyInt(), anyLong(), anyLong(), anyInt(), anyInt());
53+
doNothing()
54+
.when(mockSearchClient)
55+
.generateAsyncBulkProcessor(
56+
any(), any(), anyInt(), anyLong(), anyLong(), anyInt(), anyInt());
57+
// Ensure closeBulkProcessor can be called without issues
58+
doNothing().when(mockSearchClient).closeBulkProcessor();
59+
doNothing().when(mockSearchClient).flushBulkProcessor();
60+
}
61+
62+
@AfterMethod
63+
public void tearDown() throws Exception {
64+
if (mocks != null) {
65+
mocks.close();
66+
}
4767
}
4868

4969
@Test

metadata-io/src/test/java/com/linkedin/metadata/entity/EbeanAspectMigrationsDaoTest.java

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -21,17 +21,19 @@
2121
import java.util.Set;
2222
import java.util.stream.Collectors;
2323
import java.util.stream.Stream;
24+
import org.testng.annotations.AfterMethod;
2425
import org.testng.annotations.BeforeMethod;
2526
import org.testng.annotations.Test;
2627

2728
public class EbeanAspectMigrationsDaoTest extends AspectMigrationsDaoTest<EbeanAspectDao> {
2829

30+
private Database server;
31+
2932
public EbeanAspectMigrationsDaoTest() {}
3033

3134
@BeforeMethod
3235
public void setupTest() {
33-
Database server =
34-
EbeanTestUtils.createTestServer(EbeanAspectMigrationsDaoTest.class.getSimpleName());
36+
server = EbeanTestUtils.createTestServer(EbeanAspectMigrationsDaoTest.class.getSimpleName());
3537
_mockProducer = mock(EventProducer.class);
3638
EbeanAspectDao dao = new EbeanAspectDao(server, EbeanConfiguration.testDefault, null);
3739
dao.setConnectionValidated(true);
@@ -67,4 +69,11 @@ public void testStreamAspects() throws AssertionError {
6769
assertTrue(urnsFetched.contains(urn));
6870
}
6971
}
72+
73+
@AfterMethod
74+
public void cleanup() {
75+
// Shutdown Database instance to prevent thread pool and connection leaks
76+
// This includes the "gma.heartBeat" thread and connection pools
77+
EbeanTestUtils.shutdownDatabase(server);
78+
}
7079
}

metadata-io/src/test/java/com/linkedin/metadata/entity/EbeanEntityServiceOptimizationTest.java

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@
2828
import java.util.List;
2929
import java.util.Map;
3030
import java.util.stream.Collectors;
31+
import org.testng.annotations.AfterMethod;
3132
import org.testng.annotations.BeforeMethod;
3233
import org.testng.annotations.Test;
3334

@@ -61,11 +62,13 @@ public class EbeanEntityServiceOptimizationTest {
6162
TestOperationContexts.systemContextNoSearchAuthorization();
6263

6364
private EntityServiceImpl entityService;
65+
private Database server;
6466

6567
@BeforeMethod
6668
public void setupTest() {
67-
Database server =
69+
server =
6870
EbeanTestUtils.createTestServer(EbeanEntityServiceOptimizationTest.class.getSimpleName());
71+
6972
AspectDao aspectDao = new EbeanAspectDao(server, EbeanConfiguration.testDefault, null);
7073
PreProcessHooks preProcessHooks = new PreProcessHooks();
7174
preProcessHooks.setUiEnabled(true);
@@ -316,4 +319,11 @@ private static String formatUnknownStatements(List<String> statements) {
316319
}
317320
return builder.toString();
318321
}
322+
323+
@AfterMethod
324+
public void cleanup() {
325+
// Shutdown Database instance to prevent thread pool and connection leaks
326+
// This includes the "gma.heartBeat" thread and connection pools
327+
EbeanTestUtils.shutdownDatabase(server);
328+
}
319329
}

0 commit comments

Comments
 (0)