apache / apache/grails-core

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

Open
#16,219 0 comments 0 reactions 0 assignees View on GitHub
Dominant language
Groovy
Stars
2.9k
Forks
975
Avg merge
1d 22h
Merged PRs (30d)
92

Description

`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.(AsyncPromiseSpec.groovy:)
```

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)](https://github.com/apache/grails-core/actions/runs/30488829586/job/90701275362) | conditional async uses sync mode when requested |
| 2026-07-29 21:01 | **`8.0.x`** | [Functional Tests (Java 21, indy=false)](https://github.com/apache/grails-core/actions/runs/30490747146/job/90707733744) | multi-stage process reports all stages |
| 2026-08-22 17:23 | `fix/embedded-mongo-replica-set` | [Build Grails with Groovy snapshot (shard 2)](https://github.com/apache/grails-core/actions/runs/32587420540/job/97066257811) | conditional async uses sync mode when requested |
| 2026-08-24 23:53 | `ci/…` on #16214 | [Build Grails with Groovy snapshot (shard 0)](https://github.com/apache/grails-core/actions/runs/32791117146/job/97633380610) | 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:

```groovy
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:

```groovy
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

- #16218 - the whole-repository build stops after its last task and hangs until the six hour kill. Two of the occurrences of this issue are in that same kind of build, and the logs of both carry the same `develocity.apache.org … 403: Forbidden` noise. Whether that is a common cause or a common environment is not established.
- #16217 - the scaffolding application's login form is submitted without a session cookie. No known relationship to this one.

Contributor guide

Open the contributing guide

Research direction

Start with functionaltests.async.AsyncPromiseSpec and the client timeout in gradle/functional-test-config.gradle, then inspect the failing applications and the /asyncTest/useAsyncService entry point. Capture server-side access logs or a thread dump during the functional test to determine whether requests are never served or served slowly; done means identifying the cause and preventing the intermittent timeout.

Written by the indexing model from the issue text.

Assessment

Tech stack
groovy, java
Domain
backend, testing-qa
Issue type
Bug
Difficulty
4/5
Estimated time
3-5 days
Activity status
Active
Clarity
Needs clarification
Newbie friendliness
35/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.