Skip to content
Draft
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 @@ -5,6 +5,8 @@ import org.gradle.api.Task
import org.gradle.api.provider.Provider
import org.gradle.kotlin.dsl.extra

const val CI_SHARD_PROJECT_PATHS = "ciShardProjectPaths"

/**
* Determines if the current project is in the selected slot.
*
Expand Down
13 changes: 11 additions & 2 deletions buildSrc/src/main/kotlin/dd-trace-java.ci-jobs.gradle.kts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import datadog.gradle.plugin.ci.CI_SHARD_PROJECT_PATHS
import datadog.gradle.plugin.ci.isInSelectedSlot
import org.gradle.api.tasks.testing.Test
import java.io.File
Expand All @@ -17,8 +18,16 @@ if (project != rootProject) {
allprojects {
// Enable tests only on the selected slot (if -Pslot=n/t is provided)
tasks.withType<Test>().configureEach {
onlyIf("Project is in selected slot") {
project.isInSelectedSlot.get()
onlyIf("Project is in selected slot") { testTask ->
val rootExtraProperties = rootProject.extensions.extraProperties
val shardProjectPaths = if (rootExtraProperties.has(CI_SHARD_PROJECT_PATHS)) {
rootExtraProperties.get(CI_SHARD_PROJECT_PATHS) as? Map<*, *>
} else {
null
}
val shardProjectPath = shardProjectPaths?.get(testTask.path)?.toString()
val shardProject = shardProjectPath?.let(rootProject::project) ?: testTask.project
shardProject.isInSelectedSlot.get()
}
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@
"Source: Datadog CI Visibility, sum(@duration) over test modules grouped by",
"Gradle project, JVM 11, job test_inst, 7-day window ending 2026-10-11,",
"divided by the 491 observed pipelines.",
"JDBC virtual-project weights use coverage-complete test results from",
"pipeline 144158285 on 2026-10-12.",
"Projects absent from this map fall back to the default weight.",
"See docs/ci-test-shard-weights.md to regenerate."
],
Expand All @@ -14,44 +16,58 @@
"jvm": "11",
"job": "test_inst",
"pipelines_observed": 491,
"projects_measured": 36
"projects_measured": 35,
"projects_derived_from_measured_suites": 14
},
"weights": {
":dd-java-agent:instrumentation:jdbc": 1116.7,
":dd-java-agent:instrumentation:lettuce:lettuce-5.0": 978.1,
":dd-java-agent:instrumentation:akka:akka-http:akka-http-10.0": 612.3,
":dd-java-agent:instrumentation:spring:spring-webflux:spring-webflux-5.0": 509.4,
":dd-java-agent:instrumentation:kafka:kafka-clients-0.11": 379.1,
":dd-java-agent:instrumentation:jdbc:jdbc-remote-old-h2-test": 321.831,
":dd-java-agent:instrumentation:jdbc:jdbc-remote-dbm-forked-test": 310.736,
":dd-java-agent:instrumentation:jdbc:jdbc-remote-instrumentation-forked-test": 279.971,
":dd-java-agent:instrumentation:akka:akka-http:akka-http-10.6": 258.0,
":dd-java-agent:instrumentation:jdbc:jdbc-remote-old-postgres-test": 241.128,
":dd-java-agent:instrumentation:jdbc:jdbc-remote-dbm-prepared-forked-test": 228.78,
":dd-java-agent:instrumentation:aws-java:aws-java-sdk-2.2": 220.7,
":dd-java-agent:instrumentation:mule-4.5": 217.1,
":dd-java-agent:instrumentation:grpc-1.5": 196.7,
":dd-java-agent:instrumentation:junit:junit-5:junit-5.3": 195.4,
":dd-java-agent:instrumentation:liberty:liberty-20.0": 187.3,
":dd-java-agent:instrumentation:datastax-cassandra:datastax-cassandra-3.8": 175.4,
":dd-java-agent:instrumentation:jdbc:jdbc-old-postgres-test": 173.218,
":dd-java-agent:instrumentation:maven:maven-3.2.1": 172.2,
":dd-java-agent:instrumentation:mongo:mongo-driver:mongo-driver-4.0": 171.3,
":dd-java-agent:instrumentation:jdbc:jdbc-old-h2-test": 167.347,
":dd-java-agent:instrumentation:couchbase:couchbase-3.2": 165.8,
":dd-java-agent:instrumentation:rabbitmq-amqp-2.7": 159.7,
":dd-java-agent:instrumentation:aws-java:aws-java-sqs-1.0": 152.1,
":dd-java-agent:instrumentation:couchbase:couchbase-3.1": 151.2,
":dd-java-agent:instrumentation:aws-java:aws-java-sqs-2.0": 148.7,
":dd-java-agent:instrumentation:vertx:vertx-web:vertx-web-4.0": 140.9,
":dd-java-agent:instrumentation:jdbc:jdbc-test": 139.615,
":dd-java-agent:instrumentation:spark:spark_2.12": 113.9,
":dd-java-agent:instrumentation:aws-java:aws-java-sdk-1.11": 104.0,
":dd-java-agent:instrumentation:hibernate:hibernate-core-3.3": 93.6,
":dd-java-agent:instrumentation:jersey:jersey-2.0": 84.8,
":dd-java-agent:instrumentation:couchbase:couchbase-2.0": 82.5,
":dd-java-agent:instrumentation:jdbc:jdbc-remote-test": 69.627,
":dd-java-agent:instrumentation:jdbc:jdbc-instrumentation-forked-tests": 68.703,
":dd-java-agent:instrumentation:akka:akka-actor-2.5": 66.8,
":dd-java-agent:instrumentation:jetty:jetty-server:jetty-server-9.4.21": 64.6,
":dd-java-agent:instrumentation:lettuce:lettuce-4.0": 47.3,
":dd-java-agent:instrumentation:jedis:jedis-3.0": 42.0,
":dd-java-agent:instrumentation:jdbc:jdbc-decorator-forked-tests": 35.57,
":dd-java-agent:instrumentation:mongo:mongo-driver:mongo-driver-3:mongo-driver-3.1": 34.7,
":dd-java-agent:instrumentation:elasticsearch:elasticsearch-rest:elasticsearch-rest-7.0": 31.7,
":dd-java-agent:instrumentation:couchbase:couchbase-2.6": 25.6,
":dd-java-agent:instrumentation:jdbc:jdbc-dbm-forked-tests": 25.599,
":dd-java-agent:instrumentation:jdbc:jdbc-oracle-forked-tests": 25.523,
":dd-java-agent:instrumentation:synapse-3.0": 24.5,
":dd-java-agent:instrumentation:spring:spring-webmvc:spring-webmvc-5.3": 21.9,
":dd-java-agent:instrumentation:spring:spring-boot-1.3": 16.6,
":dd-java-agent:instrumentation:vertx:vertx-web:vertx-web-3.9": 12.6
":dd-java-agent:instrumentation:vertx:vertx-web:vertx-web-3.9": 12.6,
":dd-java-agent:instrumentation:jdbc:jdbc-sql-server-forked-tests": 11.867
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -109,10 +109,9 @@ class TestShardBalancerTest {

@Test
fun `bundled weights file is parsed`() {
assertThat(WEIGHTS).hasSize(36)
assertThat(WEIGHTS).hasSize(49)
assertThat(WEIGHTS.values).allSatisfy { weight -> assertThat(weight).isPositive() }
// docs/ci-test-shard-weights.md names the two heaviest projects
assertThat(heaviestProjects(2)).containsExactly(JDBC, LETTUCE_5_0)
assertThat(heaviestProjects(2)).containsExactly(LETTUCE_5_0, AKKA_HTTP_10_0)
}

@Test
Expand All @@ -139,8 +138,8 @@ class TestShardBalancerTest {

companion object {
private const val INSTRUMENTATION_PREFIX = ":dd-java-agent:instrumentation:"
private const val JDBC = ":dd-java-agent:instrumentation:jdbc"
private const val LETTUCE_5_0 = ":dd-java-agent:instrumentation:lettuce:lettuce-5.0"
private const val AKKA_HTTP_10_0 = ":dd-java-agent:instrumentation:akka:akka-http:akka-http-10.0"

private val WEIGHTS = loadDefaultWeights()

Expand Down
21 changes: 20 additions & 1 deletion dd-java-agent/instrumentation/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -83,7 +83,26 @@ TaskProvider<JavaExec> registerIndexTask(String indexTaskName, String indexer, S
}

instrumentationNaming {
exclusions = ["org-json-20230227"] // org-json does not use semver
// CI-only virtual projects delegate to the JDBC module's existing test tasks.
exclusions = [
// org-json does not use semver
"org-json-20230227",
"jdbc-test",
"jdbc-remote-test",
"jdbc-old-h2-test",
"jdbc-remote-old-h2-test",
"jdbc-old-postgres-test",
"jdbc-remote-old-postgres-test",
"jdbc-dbm-forked-tests",
"jdbc-decorator-forked-tests",
"jdbc-instrumentation-forked-tests",
"jdbc-oracle-forked-tests",
"jdbc-sql-server-forked-tests",
"jdbc-remote-dbm-forked-test",
"jdbc-remote-dbm-prepared-forked-test",
"jdbc-remote-instrumentation-forked-test",
"jdbc-other-forked-tests",
]
suffixes = ["-common", "-stubs", "-iast"]
}

Expand Down
131 changes: 130 additions & 1 deletion dd-java-agent/instrumentation/jdbc/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,90 @@ tasks.named("latestDepJava11Test", Test) {
}
}

def ciForkedTestGroups = [
jdbcDbmForkedTest: [
includes: ['**/DBM*ForkedTest*'],
],
jdbcDecoratorForkedTest: [
includes: ['**/JDBCDecorator*ForkedTest*'],
],
jdbcInstrumentationForkedTest: [
includes: ['**/DriverInstrumentation*ForkedTest*', '**/JDBCInstrumentation*ForkedTest*',],
],
jdbcOracleForkedTest: [
includes: ['**/Oracle*ForkedTest*'],
],
jdbcSqlServerForkedTest: [
includes: ['**/SQLServer*ForkedTest*'],
],
jdbcRemoteDbmForkedTest: [
includes: ['**/RemoteDBMTraceInjectedForkedTest.class'],
usesContainers: true,
],
jdbcRemoteDbmPreparedForkedTest: [
includes: ['**/RemoteDBMTraceInjectedForkedTestTracePreparedStatements.class'],
usesContainers: true,
],
jdbcRemoteInstrumentationForkedTest: [
includes: ['**/RemoteJDBCInstrumentationV1ForkedTest.class'],
usesContainers: true,
],
// Keep an empty catch-all so newly added forked tests remain covered.
jdbcOtherForkedTest: [
includes: ['**/*ForkedTest*'],
excludes: [
'**/DBM*ForkedTest*',
'**/DriverInstrumentation*ForkedTest*',
'**/JDBCDecorator*ForkedTest*',
'**/JDBCInstrumentation*ForkedTest*',
'**/Oracle*ForkedTest*',
'**/RemoteDBMTraceInjectedForkedTest.class',
'**/RemoteDBMTraceInjectedForkedTestTracePreparedStatements.class',
'**/RemoteJDBCInstrumentationV1ForkedTest.class',
'**/SQLServer*ForkedTest*',
],
usesContainers: true,
allowNoTests: true,
],
]

ciForkedTestGroups.each { taskName, groupConfig ->
tasks.register(taskName, Test) {
group = LifecycleBasePlugin.VERIFICATION_GROUP
useJUnitPlatform()
testClassesDirs = sourceSets.test.output.classesDirs
classpath = sourceSets.test.runtimeClasspath
setIncludes(groupConfig.includes)
setExcludes(groupConfig.excludes ?: [])
if (groupConfig.allowNoTests) {
failOnNoDiscoveredTests = false
}
}
}

// The generated forkedTest task is replaced by the disjoint tasks above. This keeps local check and
// allTests coverage unchanged while allowing CI to schedule each group independently.
tasks.named("forkedTest", Test) {
enabled = false
}

def remoteTestPattern = '**/RemoteJDBCInstrumentationV0Test*'
def ciRemoteTestGroups = [
jdbcRemoteTest: [sourceSet: sourceSets.test, originalTask: tasks.named('test')],
jdbcRemoteOldH2Test: [sourceSet: sourceSets.oldH2Test, originalTask: tasks.named('oldH2Test')],
jdbcRemoteOldPostgresTest: [sourceSet: sourceSets.oldPostgresTest, originalTask: tasks.named('oldPostgresTest')],
]

ciRemoteTestGroups.each { taskName, groupConfig ->
tasks.register(taskName, Test) {
group = LifecycleBasePlugin.VERIFICATION_GROUP
useJUnitPlatform()
testClassesDirs = groupConfig.sourceSet.output.classesDirs
classpath = groupConfig.sourceSet.runtimeClasspath
include remoteTestPattern
}
}

tasks.withType(GroovyCompile).configureEach {
configureCompiler(
it,
Expand All @@ -105,8 +189,53 @@ tasks.withType(GroovyCompile).configureEach {
}

tasks.withType(Test).configureEach {
usesService(testcontainersLimit)
if (!ciForkedTestGroups.containsKey(name) || ciForkedTestGroups[name].usesContainers) {
usesService(testcontainersLimit)
}
}

// In sharded CI, the virtual child projects below own the default JDBC suites. Outside sharded CI,
// allTests retains the convention plugin's normal dependencies and remains the single local entry point.
if (rootProject.providers.gradleProperty("slot").isPresent()) {
tasks.named("allTests") {
setDependsOn([])
}
ciRemoteTestGroups.each { taskName, groupConfig ->
groupConfig.originalTask.configure {
exclude remoteTestPattern
}
}
}

def ciTestShards = [
'jdbc-test': tasks.named('test'),
'jdbc-remote-test': tasks.named('jdbcRemoteTest'),
'jdbc-old-h2-test': tasks.named('oldH2Test'),
'jdbc-remote-old-h2-test': tasks.named('jdbcRemoteOldH2Test'),
'jdbc-old-postgres-test': tasks.named('oldPostgresTest'),
'jdbc-remote-old-postgres-test': tasks.named('jdbcRemoteOldPostgresTest'),
'jdbc-dbm-forked-tests': tasks.named('jdbcDbmForkedTest'),
'jdbc-decorator-forked-tests': tasks.named('jdbcDecoratorForkedTest'),
'jdbc-instrumentation-forked-tests': tasks.named('jdbcInstrumentationForkedTest'),
'jdbc-oracle-forked-tests': tasks.named('jdbcOracleForkedTest'),
'jdbc-sql-server-forked-tests': tasks.named('jdbcSqlServerForkedTest'),
'jdbc-remote-dbm-forked-test': tasks.named('jdbcRemoteDbmForkedTest'),
'jdbc-remote-dbm-prepared-forked-test': tasks.named('jdbcRemoteDbmPreparedForkedTest'),
'jdbc-remote-instrumentation-forked-test': tasks.named('jdbcRemoteInstrumentationForkedTest'),
'jdbc-other-forked-tests': tasks.named('jdbcOtherForkedTest'),
]

def ciShardProjectPaths = [:]
ciTestShards.each { childName, testTask ->
def childPath = ":dd-java-agent:instrumentation:jdbc:$childName"
ciShardProjectPaths[testTask.get().path] = childPath
project(childPath) {
tasks.register("allTests") {
dependsOn(testTask)
}
}
}
rootProject.extensions.extraProperties.set('ciShardProjectPaths', ciShardProjectPaths)

tasks.named("latestDepJava11Test", Test) {
testJvmConstraints {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
// CI shard project configured by the parent JDBC build.
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
// CI-only virtual project; its allTests task is registered by the parent JDBC project.
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
// CI-only virtual project; its allTests task is registered by the parent JDBC project.
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
// CI shard project configured by the parent JDBC build.
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
// CI shard project configured by the parent JDBC build.
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
// CI shard project configured by the parent JDBC build.
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
// CI-only virtual project; its allTests task is registered by the parent JDBC project.
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
// CI-only virtual project; its allTests task is registered by the parent JDBC project.
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
// CI-only virtual project; its allTests task is registered by the parent JDBC project.
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
// CI-only virtual project; its allTests task is registered by the parent JDBC project.
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
// CI shard project configured by the parent JDBC build.
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
// CI shard project configured by the parent JDBC build.
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
// CI shard project configured by the parent JDBC build.
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
// CI shard project configured by the parent JDBC build.
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
// CI shard project configured by the parent JDBC build.
37 changes: 28 additions & 9 deletions docs/ci-test-shard-weights.md
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,8 @@ Weights come from Datadog CI Visibility test events at the **module** level, ove
4. Divide each project's total by the pipeline count, and convert from nanoseconds to seconds. The result is a mean
per pipeline, not a median.

For example, `jdbc` summed to 548,323.5 seconds over 491 pipelines, giving a weight of `1116.7`.
For example, `jdbc` summed to 548,323.5 seconds over 491 pipelines, giving a weight of `1116.7`
before its test suites were split into independent CI shard projects.

Measure every project listed in `settings.gradle.kts` one by one (e.g. `@test.module:lettuce-5.0_*`) instead of
reading a single grouped aggregation or guessing family prefixes:
Expand All @@ -59,29 +60,47 @@ date in `_comment`, update the expected entry count in `TestShardBalancerTest`,
heaviest projects land in different shards, and that the modeled instrumentation shards stay balanced, with a
max/min load ratio of at most 1.25. That ratio is computed from the weights, not measured, see below.

## JDBC suite shards

The JDBC instrumentation keeps one Gradle source module, but its `test_inst` work is exposed through 15 virtual
child projects so the balancer can assign its existing suites to different shards. The child projects delegate to
test tasks in the parent project; they do not copy tests or change dependencies. Local `allTests` and `check` still
run every suite from the JDBC project as before.

Pipeline `144158285` provided a coverage-complete measurement of the refined split on JVM 11: 46 suites and 1,814
passing tests. It showed that the remote database suites dominate several tasks. The three regular version tasks
therefore expose `RemoteJDBCInstrumentationV0Test` separately from their remaining suites, and the three remote
forked classes each have their own virtual project. The remaining forked tasks form exhaustive, disjoint groups
across all 22 concrete forked classes; an unweighted catch-all keeps newly added forked classes covered.

The largest measured JDBC unit after this split is the old H2 remote suite at 321.831 s, down 71.18% from the
original 1116.7-second JDBC project. The next largest units are the remote DBM fork at 310.736 s and the remote
instrumentation fork at 279.971 s. The checked-in weights come directly from that pipeline's suite durations.

## Known limitation: the heaviest project sets a floor

A project's tests always run in a single shard, and tests run in parallel within a shard. The balancer only adds up
seconds, so it cannot see that a shard dominated by one large project finishes later than a shard with the same total
spread over many small projects. In the first pipeline using this balancer, shards 1/8 and 4/8 ran almost the same
total test time (2312 s and 2292 s), yet took 39 and 24 minutes: shard 1/8 held `jdbc`.

The two heaviest projects per pipeline are currently:
Before the JDBC suite split, the two heaviest projects per pipeline were:

- `:dd-java-agent:instrumentation:jdbc`: 1116.7 s, about 18.6 minutes;
- `:dd-java-agent:instrumentation:lettuce:lettuce-5.0`: 978.1 s, about 16.3 minutes.

The slowest shard can therefore never be faster than the heaviest single project, plus job setup. A project's
wall-clock span is also longer than its summed test time: in that first pipeline, the `jdbc` tests (20.5 minutes of
summed test time) spanned 27.6 minutes, starting about 10 minutes into the job, so its shard took 39 minutes. Under
the previous hash-based assignment, the `jdbc` shard was already the slowest one.
After the split, JDBC no longer sets that floor; `lettuce-5.0` is the heaviest remaining project. The slowest shard
can never be faster than the heaviest single project, plus job setup. A project's wall-clock span is also longer
than its summed test time: in that first pipeline, the `jdbc` tests (20.5 minutes of summed test time) spanned 27.6
minutes, starting about 10 minutes into the job, so its shard took 39 minutes. Under the previous hash-based
assignment, the `jdbc` shard was already the slowest one.

The modeled max/min ratio is optimistic whenever one project approaches the target shard time, so treat the 1.25
bound as a check of the model rather than a guarantee for wall-clock time.

The two heaviest projects must not share a shard, otherwise their floors stack. Capping weights would not help:
under-weighting `jdbc` makes the model look balanced while letting more work pile onto the `jdbc` shard. Lowering the
floor needs finer-grained sharding than whole projects.
The two heaviest projects must not share a shard, otherwise their floors stack. Capping weights only makes the model
look balanced while letting more work pile onto the affected shard. Lowering the floor requires splitting the work
into independently scheduled units, as the JDBC suite shards do above.

## When to refresh

Expand Down
Loading
Loading