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) {