This is an automated email from the ASF dual-hosted git repository.

jdaugherty pushed a commit to branch feat/latency-testing
in repository https://gitbox.apache.org/repos/asf/grails-core.git


The following commit(s) were added to refs/heads/feat/latency-testing by this 
push:
     new 75605d7db1 feedback: abandon request on interrupt, reject non-finite 
probability, deflake timing assertions, doc caveats
75605d7db1 is described below

commit 75605d7db197ef626c9fc8195ae8015e943f56e6
Author: James Daugherty <[email protected]>
AuthorDate: Fri Jul 10 08:40:49 2026 -0400

    feedback: abandon request on interrupt, reject non-finite probability, 
deflake timing assertions, doc caveats
---
 .../src/en/guide/testing/functionalTesting.adoc    |  9 ++++++-
 .../latency/grails-app/conf/application.yml        |  4 +--
 .../groovy/latencyapp/LatencyFunctionalSpec.groovy | 12 ++++-----
 .../grails/testing/latency/LatencyFilter.groovy    |  7 +++--
 .../testing/latency/LatencyProperties.groovy       |  4 +++
 .../testing/latency/LatencyFilterSpec.groovy       | 31 +++++++++++++++++-----
 6 files changed, 50 insertions(+), 17 deletions(-)

diff --git a/grails-doc/src/en/guide/testing/functionalTesting.adoc 
b/grails-doc/src/en/guide/testing/functionalTesting.adoc
index e33611915a..47a236fa55 100644
--- a/grails-doc/src/en/guide/testing/functionalTesting.adoc
+++ b/grails-doc/src/en/guide/testing/functionalTesting.adoc
@@ -134,6 +134,10 @@ url-patterns,`/*`,Servlet url patterns the filter applies 
to
 seed,none,Optional fixed random seed for reproducible delay sequences
 |===
 
+NOTE: The random generator is shared by all requests, so a fixed `seed` 
produces a reproducible
+_sequence_ of delays, but with concurrent requests which request receives 
which delay depends on
+arrival order. Delays are only fully reproducible per request when requests 
are serialized.
+
 Durations without a unit suffix are interpreted as milliseconds; suffixes such 
as `500ms`, `2s` are
 also supported. For example, to delay half of all requests by between a 
quarter and two seconds:
 
@@ -151,6 +155,9 @@ grails:
 A useful pattern is a dedicated CI job (or an occasional local run) that 
executes the functional
 test suite with latency enabled and a generous `max-delay`. Tests that only 
pass on a fast server —
 for example a Geb assertion that reads the page before a click-triggered 
navigation completes —
-will fail deterministically under latency instead of flaking rarely in CI.
+will fail deterministically under latency instead of flaking rarely in CI. 
Keep in mind that each
+delay blocks a container thread for its duration, so a large `max-delay` 
combined with highly
+parallel tests can exhaust the servlet thread pool and distort results — size 
delays and test
+parallelism accordingly.
 
 WARNING: This module is a testing aid. Never enable it in production 
configuration.
diff --git a/grails-test-examples/latency/grails-app/conf/application.yml 
b/grails-test-examples/latency/grails-app/conf/application.yml
index 30b2dfed4a..efe766350c 100644
--- a/grails-test-examples/latency/grails-app/conf/application.yml
+++ b/grails-test-examples/latency/grails-app/conf/application.yml
@@ -41,7 +41,7 @@ environments:
             testing:
                 latency:
                     enabled: true
-                    min-delay: 1500ms
-                    max-delay: 3s
+                    min-delay: 3s
+                    max-delay: 4s
                     url-patterns:
                       - '/slow/*'
diff --git 
a/grails-test-examples/latency/src/integration-test/groovy/latencyapp/LatencyFunctionalSpec.groovy
 
b/grails-test-examples/latency/src/integration-test/groovy/latencyapp/LatencyFunctionalSpec.groovy
index 57e57aee26..c3878e1994 100644
--- 
a/grails-test-examples/latency/src/integration-test/groovy/latencyapp/LatencyFunctionalSpec.groovy
+++ 
b/grails-test-examples/latency/src/integration-test/groovy/latencyapp/LatencyFunctionalSpec.groovy
@@ -28,35 +28,35 @@ import 
org.apache.grails.testing.http.client.HttpClientSupport
 /**
  * Confirms the behavior of the {@code grails-testing-support-latency} module 
against a running
  * application. The test environment enables latency for {@code /slow/*} with 
a minimum delay of
- * 1500ms (see {@code application.yml}), so matched requests must take at 
least that long while
+ * 3 seconds (see {@code application.yml}), so matched requests must take at 
least that long while
  * unmatched requests stay well under it.
  */
 @Integration
 class LatencyFunctionalSpec extends Specification implements HttpClientSupport 
{
 
-    private static final long MIN_DELAY_MILLIS = 1500
+    private static final long MIN_DELAY_NANOS = Duration.ofSeconds(3).toNanos()
 
     void 'requests matching the latency url patterns are delayed by at least 
the configured minimum'() {
         when:
         long start = System.nanoTime()
         def response = http('/slow/ping')
-        long elapsedMillis = Duration.ofNanos(System.nanoTime() - 
start).toMillis()
+        long elapsedNanos = System.nanoTime() - start
 
         then:
         response.assertStatus(200)
         response.assertContains('pong')
-        elapsedMillis >= MIN_DELAY_MILLIS
+        elapsedNanos >= MIN_DELAY_NANOS
     }
 
     void 'requests outside the latency url patterns are not delayed'() {
         when:
         long start = System.nanoTime()
         def response = http('/fast/ping')
-        long elapsedMillis = Duration.ofNanos(System.nanoTime() - 
start).toMillis()
+        long elapsedNanos = System.nanoTime() - start
 
         then:
         response.assertStatus(200)
         response.assertContains('pong')
-        elapsedMillis < MIN_DELAY_MILLIS
+        elapsedNanos < MIN_DELAY_NANOS
     }
 }
diff --git 
a/grails-testing-support-latency/src/main/groovy/org/apache/grails/testing/latency/LatencyFilter.groovy
 
b/grails-testing-support-latency/src/main/groovy/org/apache/grails/testing/latency/LatencyFilter.groovy
index 25a062cd6e..44bec1aafd 100644
--- 
a/grails-testing-support-latency/src/main/groovy/org/apache/grails/testing/latency/LatencyFilter.groovy
+++ 
b/grails-testing-support-latency/src/main/groovy/org/apache/grails/testing/latency/LatencyFilter.groovy
@@ -59,7 +59,7 @@ class LatencyFilter extends OncePerRequestFilter {
             throw new 
IllegalArgumentException("grails.testing.latency.max-delay 
(${properties.maxDelay}) " +
                     "must not be less than min-delay (${properties.minDelay})")
         }
-        if (probability < 0.0d || probability > 1.0d) {
+        if (!Double.isFinite(probability) || probability < 0.0d || probability 
> 1.0d) {
             throw new 
IllegalArgumentException("grails.testing.latency.probability must be between 
0.0 and 1.0, was $probability")
         }
         random = properties.seed == null ? new Random() : new 
Random(properties.seed)
@@ -75,7 +75,10 @@ class LatencyFilter extends OncePerRequestFilter {
                 Thread.sleep(delayMillis)
             }
             catch (InterruptedException ignored) {
+                // The container is shutting down the worker thread; abandon 
the request
+                // instead of running it on an interrupted thread.
                 Thread.currentThread().interrupt()
+                return
             }
         }
         filterChain.doFilter(request, response)
@@ -88,6 +91,6 @@ class LatencyFilter extends OncePerRequestFilter {
         if (minDelayMillis == maxDelayMillis) {
             return minDelayMillis
         }
-        minDelayMillis + (long) (random.nextDouble() * (maxDelayMillis - 
minDelayMillis + 1))
+        random.nextLong(minDelayMillis, maxDelayMillis + 1)
     }
 }
diff --git 
a/grails-testing-support-latency/src/main/groovy/org/apache/grails/testing/latency/LatencyProperties.groovy
 
b/grails-testing-support-latency/src/main/groovy/org/apache/grails/testing/latency/LatencyProperties.groovy
index 89fea292fd..adf053b469 100644
--- 
a/grails-testing-support-latency/src/main/groovy/org/apache/grails/testing/latency/LatencyProperties.groovy
+++ 
b/grails-testing-support-latency/src/main/groovy/org/apache/grails/testing/latency/LatencyProperties.groovy
@@ -75,6 +75,10 @@ class LatencyProperties {
     /**
      * Optional fixed seed for the random number generator, for reproducible 
delay sequences.
      * When unset, delays vary from run to run.
+     * <p>
+     * The generator is shared by all requests, so with concurrent requests 
which request
+     * receives which delay is order-dependent; a fixed seed only yields a 
fully reproducible
+     * assignment of delays to requests when requests are serialized.
      */
     Long seed
 }
diff --git 
a/grails-testing-support-latency/src/test/groovy/org/apache/grails/testing/latency/LatencyFilterSpec.groovy
 
b/grails-testing-support-latency/src/test/groovy/org/apache/grails/testing/latency/LatencyFilterSpec.groovy
index a6660780f2..0b39936228 100644
--- 
a/grails-testing-support-latency/src/test/groovy/org/apache/grails/testing/latency/LatencyFilterSpec.groovy
+++ 
b/grails-testing-support-latency/src/test/groovy/org/apache/grails/testing/latency/LatencyFilterSpec.groovy
@@ -39,18 +39,18 @@ class LatencyFilterSpec extends Specification {
         when:
         long start = System.nanoTime()
         filter.doFilter(new MockHttpServletRequest('GET', '/report/show'), new 
MockHttpServletResponse(), chain)
-        long elapsedMillis = Duration.ofNanos(System.nanoTime() - 
start).toMillis()
+        long elapsedNanos = System.nanoTime() - start
 
         then:
-        elapsedMillis >= 100
+        elapsedNanos >= Duration.ofMillis(100).toNanos()
         chain.request != null
     }
 
     void 'does not delay when the probability is zero'() {
         given:
         def filter = new LatencyFilter(new LatencyProperties(
-                minDelay: Duration.ofSeconds(30),
-                maxDelay: Duration.ofSeconds(30),
+                minDelay: Duration.ofSeconds(5),
+                maxDelay: Duration.ofSeconds(5),
                 probability: 0.0d
         ))
         def chain = new MockFilterChain()
@@ -58,13 +58,30 @@ class LatencyFilterSpec extends Specification {
         when:
         long start = System.nanoTime()
         filter.doFilter(new MockHttpServletRequest('GET', '/'), new 
MockHttpServletResponse(), chain)
-        long elapsedMillis = Duration.ofNanos(System.nanoTime() - 
start).toMillis()
+        long elapsedNanos = System.nanoTime() - start
 
         then: 'the request completes long before the configured delay'
-        elapsedMillis < 30_000
+        elapsedNanos < Duration.ofSeconds(5).toNanos()
         chain.request != null
     }
 
+    void 'abandons the request when interrupted while delaying'() {
+        given:
+        def filter = new LatencyFilter(new LatencyProperties(
+                minDelay: Duration.ofSeconds(5),
+                maxDelay: Duration.ofSeconds(5)
+        ))
+        def chain = new MockFilterChain()
+
+        when: 'the thread is interrupted so the delay is cut short'
+        Thread.currentThread().interrupt()
+        filter.doFilter(new MockHttpServletRequest('GET', '/'), new 
MockHttpServletResponse(), chain)
+
+        then: 'the interrupt flag is re-asserted and the chain is never 
invoked'
+        Thread.interrupted()
+        chain.request == null
+    }
+
     void 'random delays never undercut the configured minimum'() {
         given:
         def filter = new LatencyFilter(new LatencyProperties(
@@ -90,6 +107,8 @@ class LatencyFilterSpec extends Specification {
         'negative min-delay'         | new LatencyProperties(minDelay: 
Duration.ofMillis(-1))
         'probability above 1.0'      | new LatencyProperties(probability: 1.5d)
         'negative probability'       | new LatencyProperties(probability: 
-0.5d)
+        'NaN probability'            | new LatencyProperties(probability: 
Double.NaN)
+        'infinite probability'       | new LatencyProperties(probability: 
Double.POSITIVE_INFINITY)
     }
 
     private static List<Long> timings(LatencyFilter filter, int requests) {

Reply via email to