Skip to content

Flaky: AsyncPromiseSpec times out waiting for a response the endpoint cannot have taken that long to produce #16219

Description

@codeconsole

functionaltests.async.AsyncPromiseSpec fails intermittently, and not always on the same feature, when the HTTP client gives up waiting for a response:

java.net.http.HttpTimeoutException: request timed out
    at java.net.http/jdk.internal.net.http.HttpClientImpl.send(HttpClientImpl.java:953)
    at org.apache.grails.testing.http.client.HttpClientSupport$Trait$Helper.send(HttpClientSupport.groovy:1133)
    at org.apache.grails.testing.http.client.HttpClientSupport$Trait$Helper.http(HttpClientSupport.groovy:203)
    at functionaltests.async.AsyncPromiseSpec.<feature>(AsyncPromiseSpec.groovy:<line>)

The client timeout is 120 seconds (grails.http.client.timeout, set in gradle/functional-test-config.gradle).

Occurrences

Date (UTC) Branch Job Feature that timed out
2026-07-29 20:32 feat/8.0.x-legacy-command-compat Hibernate7 Functional Tests (Java 21, indy=true) conditional async uses sync mode when requested
2026-07-29 21:01 8.0.x Functional Tests (Java 21, indy=false) multi-stage process reports all stages
2026-08-22 17:23 fix/embedded-mongo-replica-set Build Grails with Groovy snapshot (shard 2) conditional async uses sync mode when requested
2026-08-24 23:53 ci/… on #16214 Build Grails with Groovy snapshot (shard 0) async service processes string input

Found by scanning the failed job logs of every CI and CI - Groovy Joint Validation Build run between 2026-07-20 and 2026-08-24 - 308 logs, none unavailable - for that spec together with that exception. These four are all of them in that period.

Three different features, two different applications (grails-test-examples-app1 and grails-test-examples-hibernate7-app1), both indy settings, and one of them on 8.0.x itself. Everything else in the same specification passes in the same run: on 2026-08-24, 695 tests completed, 1 failed.

Why a timeout here is odd

The 2026-08-24 failure was on /asyncTest/useAsyncService, and that action cannot take 120 seconds:

def useAsyncService() {
    def input = params.input ?: 'hello'
    def future = asyncProcessingService.processAsync(input)
    def result = future.get(5, TimeUnit.SECONDS)     // bounded
    render([input: input, result: result] as JSON)
}

The worst it can do is throw after five seconds and return a 500. A client that waits 120 seconds and receives nothing suggests the request was never served, rather than that this action was slow.

Possibly relevant, not established

The feature declared immediately before it in the same controller deliberately abandons a thread on every data-driven case:

def workThread = Thread.start {
    sleep(delay)
    completed.set(true)
}
workThread.join(timeout)          // 10 ms, while the work sleeps 100 ms
if (!completed.get()) {
    workThread.interrupt()
    message = "Task exceeded timeout of ${timeout} ms"
}

A raw thread per request, interrupted mid-sleep, is the kind of thing that leaves debris under load - but nothing here shows that it is what starves the later request, and the 2026-07-29 failures were on other features.

What has been ruled out

The promise decorator change in #16192 (AsyncWebRequestPromiseDecoratorLookupStrategy, which stops decorating a request the container will not start a new asynchronous cycle on). Two of the four occurrences are from 2026-07-29, three and a half weeks before that change was written, and one of those is on 8.0.x.

Reproducing

Not reproduced locally. :grails-test-examples-app1:integrationTest --tests "*AsyncPromiseSpec*" was run four times in a row - 84 tests, no failures - and the hibernate7 application's copy three times before that.

Suggested next step

The failure says only that the client gave up. What would settle it is the server's side of the same moment: whether the request arrived at all. An access log on these applications during functional tests, or a thread dump taken when a request exceeds the client timeout, would distinguish "never served" from "served slowly" - which is the fork this depends on.

Other CI flakes tracked separately

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    Status
    No status

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions