Skip to content

Split Akka HTTP and Spring WebFlux tests across CI shards - #12848

Draft
cgcote wants to merge 2 commits into
codex/apmci-51from
codex/akka-webflux-shards
Draft

cgcote wants to merge 2 commits into
codex/apmci-51from
codex/akka-webflux-shards

Conversation

@cgcote

@cgcote cgcote commented Oct 13, 2026 •

Copy link
Copy Markdown
Contributor

Summary

  • split Akka HTTP 10.0 into five virtual CI projects using its existing version, forked, IAST, and Lagom tasks
  • split the Spring WebFlux 5.0 base task into four balanced two-class groups, retain a catch-all for new classes, and group its forked and IAST tasks
  • replace the two parent weights with measurements from CI pipeline 144209984 and rebalance the eight instrumentation shards

Stack

CI Visibility measurements

Pipeline 144209984 completed successfully and provided coverage-complete weights:

  • Akka HTTP: 40 passing suites; 1,361 passing and 683 skipped test executions
  • largest Akka unit: 166.919 s, down 72.7% from the 612.3 s parent weight
  • Spring WebFlux: 12 passing suites; 229 passing and 130 skipped test executions
  • largest WebFlux unit: 122.910 s, down 75.9% from the 509.4 s parent weight
  • slowest JVM 11 instrumentation shard: 26m29s, down from 34m43s (23.7%)
  • aggregate JVM 11 shard compute: 10,871 s, down from 13,519 s (19.6%)
  • modeled instrumentation shard max/min ratio after tuning: 1.0170
  • all five Akka units land on separate shards; the five active WebFlux units span five shards

The full pipeline took 62m03s because the unrelated linux-java-spring-petclinic-startup-parallel job took 46m21s, compared with 13m50s in the parent run. The instrumentation shards finished much earlier.

Validation

  • ./gradlew spotlessApply --no-daemon
  • ./gradlew help -Pslot=1/8 --no-daemon
  • no test suites were run locally

@cgcote cgcote added type: feature Enhancements and improvements tag: no release notes Changes to exclude from release notes inst: spring Spring instrumentation inst: akka Akka instrumentation tag: ai generated Largely based on code generated by an AI or LLM labels Oct 13, 2026
@datadog-datadog-us1-prod

This comment has been minimized.

@dd-octo-sts

dd-octo-sts Bot commented Oct 13, 2026 •

Copy link
Copy Markdown
Contributor

🟢 Java Benchmark SLOs — All performance SLOs passed

Suite Status
Startup 🟢 pass

SLO thresholds are defined here based on automatically generated metrics. A warning is raised when results are within 5% of the threshold.

PR vs. master results
Scenario Candidate master Δ (95% CI of mean)
startup:insecure-bank:iast:Agent 13.87 s 13.98 s [-1.5%; -0.0%] (maybe better)
startup:insecure-bank:tracing:Agent 12.86 s 12.97 s [-1.6%; -0.1%] (maybe better)
startup:petclinic:appsec:Agent 17.11 s 17.08 s [-0.7%; +1.0%] (no difference)
startup:petclinic:iast:Agent 16.95 s 16.31 s [-0.4%; +8.2%] (no difference)
startup:petclinic:profiling:Agent 16.73 s 16.80 s [-1.6%; +0.8%] (no difference)
startup:petclinic:sca:Agent 17.01 s 16.96 s [-0.6%; +1.1%] (no difference)
startup:petclinic:tracing:Agent 16.12 s 15.68 s [-1.5%; +7.1%] (no difference)

Commit: 6bf07f78 · CI Pipeline · Benchmarking Platform UI


Load and DaCapo benchmarks can be triggered manually in the GitLab pipeline. Results will appear in the Benchmarking Platform UI after completion.

@bric3 bric3 left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

I find this exploration interesting, but as is this implementation can't go in, it goes against some Gradle best practices. Moreover I'm concerned about the maintainability of non-standard things ; this doesn't mean it can't be done properly, but we need to work toward a design that aligns properly with best-practices.

There's another PR that explored the using S3 as build cache which showed "spectacular" improvements, with less changes. See

project(childPath) {
tasks.register("allTests") {
dependsOn(testTasks)
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

question: Should instrumentationCheck support running tests when -PskipTests is absent?

check_inst intentionally passes -PskipTests; test_inst uses instrumentationTest, which selects the children's allTests. I compared all eight JVM 11 test jobs in parent pipeline 144164571 and PR pipeline 144239586: both recorded 29,385 tests, including 22,747 passed and 6,638 skipped. The 40 Akka HTTP and 12 WebFlux suite report inventories also matched. This observation does not establish lost coverage in the actual pipeline.

For a direct instrumentationCheck -Pslot=5/8 invocation without -PskipTests, the WebFlux parent check is selected, but its main tests are assigned to other slots. Those slots never select the tests through this aggregate because the children only register allTests. If this entry point should retain its test-running behaviour, child check aliases would restore selection. Otherwise this is a non-blocking question about the aggregate's contract.

The relevant root selection is here:

excludePrefixes: List<String>,
forceCoverage: Boolean
) {
val coverage = forceCoverage || rootProject.providers.gradleProperty("checkCoverage").isPresent
tasks.register(rootTaskName) {
subprojects.forEach { subproject ->
if (
subproject.isInSelectedSlot.get() &&
includePrefixes.any { subproject.path.startsWith(it) } &&
!excludePrefixes.any { subproject.path.startsWith(it) }
) {
val testTask = subproject.tasks.findByName(subProjTaskName)
var isAffected = true
if (testTask != null) {
val useGitChanges = rootProject.extra.get("useGitChanges") as Boolean
if (useGitChanges) {
@Suppress("UNCHECKED_CAST")
val affectedProjects = rootProject.extra.get("affectedProjects") as Map<Project, Set<String>>

Comment on lines +189 to +192
if (rootProject.providers.gradleProperty("slot").isPresent()) {
tasks.named("allTests") {
setDependsOn([])
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

issue: This kinda break the model of declaring aggregating tasks.

I.e. clearing these dependencies replaces automatic suite discovery with a fixed list. A newly added suite can therefore run locally but be omitted from sharded CI until this list is updated. I understand this happens only when there's a slot property but this looks like it's changing the task contract under the hood.

The WebFlux split has the same issue.

ciAkkaTestShards.each { childName, testTasks ->
def childPath = ":dd-java-agent:instrumentation:akka:akka-http:akka-http-10.0:$childName"
testTasks.each { testTask ->
ciShardProjectPaths[testTask.get().path] = childPath

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

issue: This reealizes a test task early breaking the Gradle's task configuration best practices.

This loop runs during every build configuration, including unrelated invocations like help, the call to .get() eagerly configures each test task just to read its path. If that's the only thing needed then TaskProvider.name is available without realisation. The same change applies to other modified project in this PR stack.

Suggested change
ciShardProjectPaths[testTask.get().path] = childPath
ciShardProjectPaths[project.path + ':' + testTask.name] = childPath

'akka-http-version-102-tests': [tasks.named('version102Scala213Test')],
]

def ciShardProjectPaths = rootProject.extensions.extraProperties.get('ciShardProjectPaths') as Map

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

issue: While Gradle support storing objects there, using extra properties is most likely the wrong approach, which is presently the case.

Possibly I would rather go toward a build service.

In this case there is two concrete problems

  • This is shared a mutable state whose initialization depends on another project's configuration.
  • The map is created only by the JDBC instrumentation project from I could see, while others like this one read it directly. This creates a hidden ordering assumption. And I believe this could break in various ways. E.g. configuration caching.
  • Also, generally speaking, modifying another project (from a project) is an anti-pattern.

Gradle's convention-plugin guidance.

testClassesDirs = sourceSets.test.output.classesDirs
classpath = sourceSets.test.runtimeClasspath
setIncludes(includes)
onlyIf("CI sharding is enabled") { rootProject.providers.gradleProperty("slot").isPresent() }

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

suggestion: Could we capture the sharding flag locally during task configuration and read that value or provider in onlyIf?

This predicate reads rootProject through the build script at execution time. A small Gradle 9.8 fixture fails to store the configuration cache with that pattern; a locally captured provider stores and reuses it. The new root-map predicate in the CI plugin also reads project objects at execution time.

This extends an existing configuration-cache limitation rather than introducing a new failure in the current CI pipeline, but it would be useful to keep the new wiring compatible as that work progresses.

Gradle's execution-time project-access guidance.

Comment on lines +210 to +212
tasks.register("allTests") {
dependsOn(testTasks)
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

suggestion (non-blocking): Could we give the new child aggregates a verification group and a description?

The 32 child allTests tasks across the stack have neither. Adding this metadata in the shared registration code would make their purpose visible in Gradle task listings.

Suggested change
tasks.register("allTests") {
dependsOn(testTasks)
}
tasks.register("allTests") {
group = "verification"
description = "Runs the tests assigned to this CI shard."
dependsOn(testTasks)
}

Gradle's task metadata guidance.

@@ -0,0 +1 @@
// CI-only virtual project; its allTests task is registered by the parent project.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

suggestion (non-blocking): I'm really not keep on having virtual projects, but if we go this way, could we use build.gradle.kts for the new child build files if these scheduling projects remain?

Also for all the other 32 comment-only build files, so there is no Groovy logic to migrate in them. This follows Gradle's recommendation for new build scripts; the existing module scripts can remain as they are.

Gradle's Kotlin DSL guidance.

@bric3
bric3 added this pull request to stack #12912 October 16, 2026 14:15

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

inst: akka Akka instrumentation inst: spring Spring instrumentation tag: ai generated Largely based on code generated by an AI or LLM tag: no release notes Changes to exclude from release notes type: feature Enhancements and improvements

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants