Skip to content

Add testing support option to introduce latency#15937

Open
jdaugherty wants to merge 2 commits into
8.0.xfrom
feat/latency-testing
Open

Add testing support option to introduce latency#15937
jdaugherty wants to merge 2 commits into
8.0.xfrom
feat/latency-testing

Conversation

@jdaugherty

Copy link
Copy Markdown
Contributor

Description

Adds a new testing support library, grails-testing-support-latency, that injects artificial
latency 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:

  • New module grails-testing-support-latency (org.apache.grails:grails-testing-support-latency):
    • LatencyFilter — a OncePerRequestFilter that delays each matched request by a random
      amount 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 the grails.testing.latency prefix (enabled, min-delay,
      max-delay, probability, url-patterns, seed) with Spring Duration binding.
    • LatencyAutoConfiguration — conditional on grails.testing.latency.enabled=true, so the
      library is completely inert unless explicitly switched on; registers the filter at highest
      precedence so the delay covers the entire request pipeline.
  • New example app grails-test-examples/latency that enables latency for /slow/* only and
    confirms end-to-end behavior against the running application: matched requests take at least
    the configured minimum, unmatched requests do not.
  • Documentation: a new "Simulating Latency" section in the Functional Testing guide (dependency,
    activation, full property table, YAML example, CI usage pattern, production warning) and a
    "Latency Testing Support" entry in the What's New section.
  • Module registered in settings.gradle and gradle/publish-root-config.gradle; the BOM picks
    it 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 render

Contributor Checklist

Issue and Scope

  • This PR is linked to an existing issue — no issue exists yet; background is provided in
    the Description above. Happy to open a ticket for the release changelog if preferred.
  • This PR addresses the complete scope of the change: library, tests, example app, and
    documentation are all included.
  • This PR contains a single, focused change.
  • This PR targets the correct branch: it is a new feature (new module, no changes to any
    existing API) targeting 8.0.x.

Code Quality

  • I have added or updated tests: unit tests for the filter and auto-configuration, plus a
    dedicated example app with integration tests confirming runtime behavior.
  • I have verified that all existing tests pass by running ./gradlew build --rerun-tasks.
    (Targeted builds above are green; full rerun not yet executed.)
  • My code follows the project's code style guidelines (codeStyle passes on the new module).
  • This PR does not include mass reformatting, style-only changes, or refactoring.
  • Generative AI tooling was used (Claude Code with Claude Fable 5); all output was reviewed,
    and verified against the project's quality standards via the test and style checks above.

Licensing and Attribution

  • All contributed code is provided under the Apache License 2.0, and every new source file
    (Groovy, Gradle, YAML, XML, AsciiDoc) includes the Apache license header.
  • I have the necessary rights to submit this contribution.
  • Generative AI use is disclosed per the ASF generative tooling policy: this contribution was
    prepared with assistance from Claude Code (Anthropic), with human review of all changes.

Documentation

  • User-facing documentation added: "Simulating Latency" section in the Functional Testing guide.
  • What's New section updated with a "Latency Testing Support" entry.
  • No breaking changes or upgrade actions — the module is new, opt-in, and inert by default, so
    no Upgrade Notes changes are needed.
  • The PR description explains what was changed and why.

@jdaugherty

Copy link
Copy Markdown
Contributor Author

@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

codecov Bot commented Jul 8, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 93.54839% with 2 lines in your changes missing coverage. Please review.
✅ Project coverage is 49.5591%. Comparing base (23045cd) to head (75605d7).
⚠️ Report is 168 commits behind head on 8.0.x.

Files with missing lines Patch % Lines
...apache/grails/testing/latency/LatencyFilter.groovy 92.3077% 0 Missing and 2 partials ⚠️
Additional details and impacted files

Impacted file tree graph

@@                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     
Files with missing lines Coverage Δ
...ls/testing/latency/LatencyAutoConfiguration.groovy 100.0000% <100.0000%> (ø)
...apache/grails/testing/latency/LatencyFilter.groovy 92.3077% <92.3077%> (ø)

... and 60 files with indirect coverage changes

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@codeconsole codeconsole 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.

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/ping fast).
  • BOM claim is true: grails-bom/base/build.gradle loops over all subprojects that apply the publish plugin, and the new module applies org.apache.grails.buildsrc.publish, so it gets a BOM constraint automatically.
  • Conventions match: the module's build.gradle mirrors grails-testing-support-http-client (same plugin set, group = 'org.apache.grails', test-config/docs-config applies), the package org.apache.grails.testing.latency matches the sibling's org.apache.grails.testing.http.client, settings.gradle/publish-root-config.gradle entries are alphabetized, license headers are on every file, jakarta.* only, no wildcard imports, @CompileStatic on library classes and @GrailsCompileStatic on example controllers.
  • Inert-by-default claim is true: @ConditionalOnProperty(... havingValue = 'true') with no matchIfMissing, 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 the min == max short-circuit avoids a degenerate multiply.
  • Docs: property table matches the actual defaults in LatencyProperties, the dependency coordinate matches the published group, and the xref:testing/functionalTesting[...] style matches existing usage in whatsNew.adoc.

Suggestions (all minor — none blocking)

  1. LatencyFilter.doFilterInternal proceeds down the chain after an interrupt (LatencyFilter.groovy, lines 74–81). Re-asserting the interrupt flag is the right idiom, but then calling filterChain.doFilter() on an interrupted thread means the request runs anyway and typically dies somewhere confusing downstream (e.g., inside a JDBC call) during shutdown. Consider return after re-interrupting so the request is abandoned cleanly.

  2. Weak failure mode in the zero-probability test (LatencyFilterSpec.groovy, line 64). elapsedMillis < 30_000 with 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.

  3. Mild flake risk in the functional spec (LatencyFunctionalSpec.groovy, line 60): asserting /fast/ping completes 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: 3s and assert fast < 3000) would make it more robust for free.

  4. Seed reproducibility caveat worth a sentence in the javadoc/docs: the shared Random means 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: since Random implements RandomGenerator on Java 17+, nextDelayMillis() could just be random.nextLong(minDelayMillis, maxDelayMillis + 1).

  5. Thread-pool note for the docs (optional): the delay is a blocking Thread.sleep on a container thread, so a large max-delay plus 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.

@codeconsole codeconsole self-requested a review July 10, 2026 07:48

Copilot AI 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.

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-latency module with LatencyFilter, LatencyProperties, and Spring Boot auto-configuration.
  • Adds a grails-test-examples/latency example 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.

@jdaugherty

Copy link
Copy Markdown
Contributor Author

Review feedback addressed in 75605d7:

  1. Interrupt handling — the filter now re-interrupts and returns instead of continuing the chain on an interrupted thread; covered by a new unit test asserting the chain is never invoked.
  2. Zero-probability test — configured delay reduced from 30s to 5s so a failure trips quickly instead of hanging.
  3. Functional spec timing gap — example now uses min-delay: 3s / max-delay: 4s with the fast-path bound at 3s.
  4. Seed reproducibility — caveat added to the seed javadoc and the guide (shared generator means delay-to-request assignment is order-dependent under concurrency); delay generation now uses random.nextLong(min, max + 1).
  5. Thread-pool note — added next to the CI-usage paragraph in the docs.

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: :grails-testing-support-latency:build green (11 unit tests) and :grails-test-examples-latency:integrationTest green (2 tests).

@testlens-app

testlens-app Bot commented Jul 10, 2026

Copy link
Copy Markdown

✅ All tests passed ✅

🏷️ Commit: 75605d7
▶️ Tests: 44823 executed
⚪️ Checks: 61/61 completed


Learn more about TestLens at testlens.app.

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

Labels

None yet

Projects

Status: No status

Development

Successfully merging this pull request may close these issues.

3 participants