Add testing support option to introduce latency#15937
Conversation
|
@jamesfredley this gives us a way to reproduce the test run failures locally. The idea is we can add these to our test apps to introduce latency, then have AI iterate to fix the flakiness so this isn't a problem going forward. |
Codecov Report❌ Patch coverage is
Additional details and impacted files@@ Coverage Diff @@
## 8.0.x #15937 +/- ##
==================================================
+ Coverage 49.3287% 49.5591% +0.2304%
- Complexity 16768 16948 +180
==================================================
Files 1985 2001 +16
Lines 93327 93785 +458
Branches 16336 16427 +91
==================================================
+ Hits 46037 46479 +442
+ Misses 40165 40140 -25
- Partials 7125 7166 +41
🚀 New features to boost your workflow:
|
codeconsole
left a comment
There was a problem hiding this comment.
Approve with minor suggestions. The PR is well-scoped, follows the repo's conventions closely, and every claim in the description checked out when I verified it locally.
What I verified locally (JDK 21, base 8.0.x)
./gradlew :grails-testing-support-latency:build— green, all 10 unit tests pass (7 filter + 3 auto-config), including the style checks../gradlew :grails-test-examples-latency:integrationTest— green, both end-to-end tests pass (delayed/slow/ping≥ 1500ms, undelayed/fast/pingfast).- BOM claim is true:
grails-bom/base/build.gradleloops over all subprojects that apply the publish plugin, and the new module appliesorg.apache.grails.buildsrc.publish, so it gets a BOM constraint automatically. - Conventions match: the module's
build.gradlemirrorsgrails-testing-support-http-client(same plugin set,group = 'org.apache.grails', test-config/docs-config applies), the packageorg.apache.grails.testing.latencymatches the sibling'sorg.apache.grails.testing.http.client,settings.gradle/publish-root-config.gradleentries are alphabetized, license headers are on every file,jakarta.*only, no wildcard imports,@CompileStaticon library classes and@GrailsCompileStaticon example controllers. - Inert-by-default claim is true:
@ConditionalOnProperty(... havingValue = 'true')with nomatchIfMissing, covered by the "does nothing when disabled or not configured" test. - Delay math is correct:
min + (long)(nextDouble() * (max - min + 1))yields an inclusive[min, max]range, and themin == maxshort-circuit avoids a degenerate multiply. - Docs: property table matches the actual defaults in
LatencyProperties, the dependency coordinate matches the published group, and thexref:testing/functionalTesting[...]style matches existing usage inwhatsNew.adoc.
Suggestions (all minor — none blocking)
-
LatencyFilter.doFilterInternalproceeds down the chain after an interrupt (LatencyFilter.groovy, lines 74–81). Re-asserting the interrupt flag is the right idiom, but then callingfilterChain.doFilter()on an interrupted thread means the request runs anyway and typically dies somewhere confusing downstream (e.g., inside a JDBC call) during shutdown. Considerreturnafter re-interrupting so the request is abandoned cleanly. -
Weak failure mode in the zero-probability test (
LatencyFilterSpec.groovy, line 64).elapsedMillis < 30_000with a 30s configured delay means the failure mode is a 30-second hang before the assertion trips. A smaller configured delay with a tighter bound (e.g., 5s delay, assert < 5s) keeps the intent and fails faster. -
Mild flake risk in the functional spec (
LatencyFunctionalSpec.groovy, line 60): asserting/fast/pingcompletes in under 1500ms could flake on a very slow CI box — ironic given the module's purpose. Risk is low (the slow test runs first and warms up the stack), but widening the gap (e.g.,min-delay: 3sand assert fast < 3000) would make it more robust for free. -
Seed reproducibility caveat worth a sentence in the javadoc/docs: the shared
Randommeans that with concurrent requests, which request gets which delay is order-dependent — a fixed seed only gives a reproducible sequence when requests are serialized. Related micro-nit: sinceRandomimplementsRandomGeneratoron Java 17+,nextDelayMillis()could just berandom.nextLong(minDelayMillis, maxDelayMillis + 1). -
Thread-pool note for the docs (optional): the delay is a blocking
Thread.sleepon a container thread, so a largemax-delayplus highly parallel functional tests can exhaust the servlet thread pool and distort results. One sentence next to the CI-usage paragraph would preempt confusion.
Design assessment
A separate opt-in module is the right call here rather than folding this into grails-testing-support-core — it matches the existing fine-grained pattern (-dbcleanup-h2, -dbcleanup-postgresql, -http-client) and keeps a deliberately dangerous-in-production filter off the classpath unless explicitly added. Registering at Ordered.HIGHEST_PRECEDENCE so the delay covers the whole pipeline (including security filters) is the correct simulation of a slow network. Validation failing fast at context startup, with tests for each invalid config, is exactly what you want from a test aid.
The one unchecked checklist item (full ./gradlew build --rerun-tasks) seems fine to leave to CI since the change adds a new module and touches no existing code paths.
There was a problem hiding this comment.
Pull request overview
Adds an opt-in “latency injection” testing-support module to help make functional-test timing bugs reproducible by randomly delaying matched HTTP requests in the application under test, along with an example app and documentation updates.
Changes:
- Introduces new
grails-testing-support-latencymodule withLatencyFilter,LatencyProperties, and Spring Boot auto-configuration. - Adds a
grails-test-examples/latencyexample app + integration tests demonstrating URL-pattern-scoped latency behavior. - Updates functional testing guide and “What’s New”, and wires the new projects into the build/publishing configuration.
Reviewed changes
Copilot reviewed 19 out of 19 changed files in this pull request and generated 4 comments.
Show a summary per file
| File | Description |
|---|---|
| settings.gradle | Registers the new testing-support module and the new example app as included builds/projects. |
| gradle/publish-root-config.gradle | Adds the new testing-support module to the published projects list. |
| grails-testing-support-latency/build.gradle | Defines the new module’s build, dependencies, and publishing configuration. |
| grails-testing-support-latency/src/main/groovy/org/apache/grails/testing/latency/LatencyFilter.groovy | Implements a OncePerRequestFilter that injects randomized per-request latency with validation and optional seed. |
| grails-testing-support-latency/src/main/groovy/org/apache/grails/testing/latency/LatencyProperties.groovy | Adds @ConfigurationProperties binding for grails.testing.latency.* (durations, probability, URL patterns, seed). |
| grails-testing-support-latency/src/main/groovy/org/apache/grails/testing/latency/LatencyAutoConfiguration.groovy | Spring Boot auto-configuration that conditionally registers the latency filter at highest precedence. |
| grails-testing-support-latency/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports | Exposes the module’s auto-configuration to Spring Boot via imports. |
| grails-testing-support-latency/src/test/groovy/org/apache/grails/testing/latency/LatencyFilterSpec.groovy | Unit tests for delay bounds, probability passthrough, seeded randomness, and config validation. |
| grails-testing-support-latency/src/test/groovy/org/apache/grails/testing/latency/LatencyAutoConfigurationSpec.groovy | Verifies conditional registration/binding behavior via WebApplicationContextRunner. |
| grails-test-examples/latency/build.gradle | Declares the example app and wires in the latency module for integration-test runtime. |
| grails-test-examples/latency/grails-app/init/latencyapp/Application.groovy | Example app bootstrap. |
| grails-test-examples/latency/grails-app/controllers/latencyapp/UrlMappings.groovy | Default URL mapping to route /slow/* and /fast/* endpoints. |
| grails-test-examples/latency/grails-app/controllers/latencyapp/SlowController.groovy | Endpoint expected to be delayed by configured URL patterns. |
| grails-test-examples/latency/grails-app/controllers/latencyapp/FastController.groovy | Endpoint expected to remain undelayed (outside URL patterns). |
| grails-test-examples/latency/grails-app/conf/application.yml | Enables latency for test env with /slow/* patterns and configured delay range. |
| grails-test-examples/latency/grails-app/conf/logback.xml | Example app logging configuration for test runs. |
| grails-test-examples/latency/src/integration-test/groovy/latencyapp/LatencyFunctionalSpec.groovy | End-to-end verification that matched requests are delayed and unmatched are not. |
| grails-doc/src/en/guide/testing/functionalTesting.adoc | Documents how to enable/configure latency injection in functional/integration testing. |
| grails-doc/src/en/guide/introduction/whatsNew.adoc | Adds a “Latency Testing Support” entry to highlight the new module. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
…, deflake timing assertions, doc caveats
|
Review feedback addressed in 75605d7:
Also from the Copilot comments: elapsed-time assertions compare nanoseconds instead of truncated millis, and probability validation now rejects NaN/Infinity (with new test rows). Verified locally: |
✅ All tests passed ✅🏷️ Commit: 75605d7 Learn more about TestLens at testlens.app. |
Description
Adds a new testing support library,
grails-testing-support-latency, that injects artificiallatency into a running Grails application so that requests randomly take longer.
Why: Functional tests are prone to timing bugs — assertions that race a click-triggered
navigation, missing waits, response-ordering assumptions. These pass reliably on fast local
machines and fail intermittently on slower CI infrastructure (see the recent
User1FunctionalSpec.'edit report 20'flake in the spring-security ACL functional test app,which only reproduced when the server response was slow). Slowing responses down widens the
window in which these bugs occur, turning rare flakes into deterministic failures that can be
found and fixed locally.
What:
grails-testing-support-latency(org.apache.grails:grails-testing-support-latency):LatencyFilter— aOncePerRequestFilterthat delays each matched request by a randomamount between a configurable minimum and maximum, for a configurable fraction of requests,
with an optional fixed seed for reproducible delay sequences. Invalid configuration
(negative delays,
max-delay<min-delay, probability outside 0..1) fails fast at startup.LatencyProperties— bound from thegrails.testing.latencyprefix (enabled,min-delay,max-delay,probability,url-patterns,seed) with SpringDurationbinding.LatencyAutoConfiguration— conditional ongrails.testing.latency.enabled=true, so thelibrary is completely inert unless explicitly switched on; registers the filter at highest
precedence so the delay covers the entire request pipeline.
grails-test-examples/latencythat enables latency for/slow/*only andconfirms end-to-end behavior against the running application: matched requests take at least
the configured minimum, unmatched requests do not.
activation, full property table, YAML example, CI usage pattern, production warning) and a
"Latency Testing Support" entry in the What's New section.
settings.gradleandgradle/publish-root-config.gradle; the BOM picksit up automatically via the subprojects constraint loop.
Verification performed:
./gradlew :grails-testing-support-latency:build— 10 unit tests passing (delay bounds,zero-probability passthrough, configuration validation, conditional auto-configuration)
./gradlew :grails-test-examples-latency:integrationTest— 2 end-to-end tests passing./gradlew :grails-testing-support-latency:codeStyle— clean./gradlew :grails-doc:publishGuide -x aggregateGroovydoc— docs build clean, new sections renderContributor Checklist
Issue and Scope
the Description above. Happy to open a ticket for the release changelog if preferred.
documentation are all included.
existing API) targeting
8.0.x.Code Quality
dedicated example app with integration tests confirming runtime behavior.
./gradlew build --rerun-tasks.(Targeted builds above are green; full rerun not yet executed.)
codeStylepasses on the new module).and verified against the project's quality standards via the test and style checks above.
Licensing and Attribution
(Groovy, Gradle, YAML, XML, AsciiDoc) includes the Apache license header.
prepared with assistance from Claude Code (Anthropic), with human review of all changes.
Documentation
no Upgrade Notes changes are needed.