questdb / questdb/java-questdb-client

Flaky on JDK 11: BackgroundDrainerMidDrainCapabilityGapTest.testDeliveringBetweenTwoGapWindowsGrantsAFreshSettleBudget

Open
#100 0 comments 0 reactions 0 assignees View on GitHub

Nobody has claimed this yet.

bug QWP
Dominant language
Java
Stars
10
Forks
6
Avg merge
6h 40m
Merged PRs (30d)
2

Description

BackgroundDrainerMidDrainCapabilityGapTest#testDeliveringBetweenTwoGapWindowsGrantsAFreshSettleBudget fails intermittently on JDK 11, on main as well as on feature branches. Found while reviewing #99; it is not caused by that PR.

Symptom
java.lang.AssertionError: delivering between the windows ends the episode, so neither window
reaches the threshold and the slot must still drain [attempts=20] expected:<SUCCESS> but was:<FAILED>
    at ...BackgroundDrainerMidDrainCapabilityGapTest.lambda$testDeliveringBetweenTwoGapWindowsGrantsAFreshSettleBudget$0(BackgroundDrainerMidDrainCapabilityGapTest.java:240)
    at ...TestUtils.assertMemoryLeak(TestUtils.java:135)
    at ...BackgroundDrainerMidDrainCapabilityGapTest.testDeliveringBetweenTwoGapWindowsGrantsAFreshSettleBudget(BackgroundDrainerMidDrainCapabilityGapTest.java:201)

The drainer ends the episode with DrainOutcome.FAILED and quarantines the slot, where the test expects SUCCESS. factory.attempts() is 20 — the exact end of the second scripted gap window.

Reproduction
JAVA_HOME=~/.sdkman/candidates/java/11.0.26-amzn \
  mvn -pl core test -Dtest='BackgroundDrainerMidDrainCapabilityGapTest'

Re-run several times — it does not fail every run.

Evidence that it is flaky and not revision-specific

JDK 11.0.26-amzn (Corretto), Linux x86-64, repeated runs of the same test:

revision failures / runs
981bdb0 (main) 4 / 7
065c7be (PR #99 branch) 3 / 7

Both revisions both pass and fail it. It initially looked like a regression on the PR branch (branch failed, main passed), but the outcome also inverts depending on how the test is invoked: running the single method in isolation vs. running the whole class flipped which revision failed, in both directions, across repeats. That rules out a code difference between the two revisions as the cause.

Not observed on JDK 26: a full mvn -pl core test there passed (3456 tests, 0 failures) — though that is a single observation, not a claim that the JDK matters.

What I ruled out
  • Machine load. Reproduced on an idle machine (load ~1.6), not only under parallel builds.
  • The 60 s wall-clock settle budget. RECONNECT_MAX_DURATION_MILLIS = 60_000 in the test, backoff 1–4 ms, and the whole run finishes in ~10 s, so capabilityGapElapsedNanos >= reconnectBudgetNanos (BackgroundDrainer.java:618) is not the terminal that fires.
  • The settle-budget accumulators failing to reset as a group. capabilityGapAttempts, capabilityGapElapsedNanos and lastCapabilityGapNanos are reset together at all three sites (BackgroundDrainer.java:525-527, 563-565, 683-685), and a captured log of a passing run shows the episode restarting correctly — the second gap window begins again at attempt 1:
21:11:51.088 ... durable-ack capability gap mid-drain (...), re-entering settle budget
21:11:51.089 ... attempt 1: durable-ack unavailable, retrying after backoff
   ... attempts 2..8 ...
21:11:56.044 ... durable-ack capability gap mid-drain (...), re-entering settle budget   <- ~5 s delivering gap
21:11:56.054 ... attempt 1: durable-ack unavailable, retrying after backoff
What I did not determine

Root cause. Two candidates remain, and I could not separate them:

  1. Test-harness race. The scripted delivering session (connection 2 acks frames 0 and 1, then drops — drops.put(2, 1L)) sometimes loses the race and never advances the watermark, so no reset happens, the two 9-sweep windows accumulate to 18 ≥ DEFAULT_MAX_DURABLE_ACK_MISMATCH_ATTEMPTS (16), and FAILED is the correct outcome for what actually happened on the wire.
  2. A genuine intermittent failure of the episode reset in BackgroundDrainer.

The test drives a real TestWebSocketServer over a real loopback socket, so the delivering session's ack is genuinely timing-dependent.

The decisive diagnostic is the give-up line from a failing run, which reports which terminal fired and the elapsed time:

drainer giving up on slot {} after {} durable-ack-mismatch attempts ({}ms): {}

(BackgroundDrainer.java:620). If it reports ~18 attempts, candidate 1 is the explanation and the fix belongs in the test harness (make the delivering session's ack deterministic before the connection drops). I only managed to capture that log on passing runs.

Impact

Low for CI — build-jdk8 is the only job that runs tests, so this never reaches a pipeline today. It costs developer time on local JDK 11+ runs, and it is masking whichever of the two candidates above is true.

🤖 Generated with Claude Code

https://claude.ai/code/session_013ynWBNvVkrQpTxt1d429ED

Contributor guide

Open the contributing guide

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

Research direction

Reproduce BackgroundDrainerMidDrainCapabilityGapTest#testDeliveringBetweenTwoGapWindowsGrantsAFreshSettleBudget with the provided Maven command on JDK 11. Inspect BackgroundDrainer.java around lines 525-527, 563-565, 618, and 683-685, then capture the failing give-up line to distinguish a TestWebSocketServer harness race from an episode-reset failure. Done means the test no longer flakes and the correct reset or deterministic acknowledgement behavior is covered.

Written by the indexing model from the issue text.

Assessment

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

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.