Skip to content

Fix the attach race in HealthEdtDeliveryTest.aFacadeActionDeliversOnTheEdt - #5516

Merged
shai-almog merged 2 commits into
masterfrom
fix-health-edt-delivery-test-race
Aug 4, 2026
Merged

Fix the attach race in HealthEdtDeliveryTest.aFacadeActionDeliversOnTheEdt#5516
shai-almog merged 2 commits into
masterfrom
fix-health-edt-delivery-test-race

Conversation

@shai-almog

Copy link
Copy Markdown
Collaborator

The problem

HealthEdtDeliveryTest.aFacadeActionDeliversOnTheEdt fails 8/8 times when run on its own, and flakes roughly 1 in 10 when the whole class runs:

AssertionFailedError: a result must arrive on the EDT on every backend ==> expected: <true> but was: <false>
  at HealthEdtDeliveryTest.assertDeliveredOnEdt(HealthEdtDeliveryTest.java:112)
  at HealthEdtDeliveryTest.aFacadeActionDeliversOnTheEdt(HealthEdtDeliveryTest.java:316)

pr.yml runs core-unittests on every PR, so this was an intermittent red build waiting to happen.

Root cause

The product code is correctEdtResult hops to the EDT in every failing run. The test loses a race it cannot win.

Health.openHealthSettings() completes its EdtResult before it returns:

public AsyncResource<Boolean> openHealthSettings() {
    AsyncResource<Boolean> out = new EdtResult<Boolean>();
    out.complete(Boolean.FALSE);   // -> callSerially(Deliver)
    return out;
}

The test then attaches .onResult(landing) afterwards. By then the EDT has usually already run the queued Deliver, and AsyncResource.ready takes its already-settled branch (AsyncResource.java:451), invoking the listener inline on the attaching thread. The landing records isEdt() == false for a delivery that genuinely happened on the EDT.

Instrumenting the delivery proves it fires from the attach, not from the queued runnable:

at AsyncResource.ready(AsyncResource.java:470)   <-- runImmediately, already-done path
at AsyncResource.onResult(AsyncResource.java:634)
at HealthEdtDeliveryTest$1.run(...)
at com.codename1.ui.Display.invokeAndBlock(Display.java:1561)

This is the same race the class already documents on anAggregateDeliversOnTheEdt. The other six tests dodge it by holding the backend (holdRead/holdWrite/holdAggregate/holdDelete) until the listener is attached. The facade settles synchronously, so there is nothing to hold — which is why this one test was left exposed.

A second detail widens the window: JUnit runs these on main, not the EDT, so CN.invokeAndBlock takes its non-EDT branch (Display.java:1561, a plain r.run()). The real EDT spins idle alongside and grabs the queued delivery almost instantly. That is why isolation fails deterministically while a busier full-class run usually wins.

The fix

Park the EDT behind a barrier queued ahead of the delivery, and release it once the listener is attached. Serial calls run FIFO, so the ordering is guaranteed rather than hoped for.

CN.callSerially(new Runnable() {
    public void run() {
        try { attached.await(); } catch (InterruptedException ex) { Thread.currentThread().interrupt(); }
    }
});
Health.getInstance().openHealthSettings().onResult(landing);
attached.countDown();

Also in this change:

  • Corrected the assertDeliveredOnEdt docs. They stated these tests run on the EDT and that invokeAndBlock keeps the loop pumping. Neither holds, and the gap between the comment and the behaviour is what made the race invisible. Replaced with what actually happens, plus the attach-before-settle rule every test here depends on.
  • Moved the facade test's javadoc back onto the facade test. It had drifted upward and was sitting stacked above everyPublicHealthResourceDeliversOnTheEdt's own javadoc, documenting a test ~100 lines away.

No production code is touched.

Verification

Run Before After
-Dtest=HealthEdtDeliveryTest#aFacadeActionDeliversOnTheEdt 8/8 fail 0/12 fail
-Dtest=HealthEdtDeliveryTest (full class) 1/10 fail 0/30 fail

mvn verify on core-unittests is green: SpotBugs BugInstance size is 0, PMD, Checkstyle and Spotless all clean.

🤖 Generated with Claude Code

…heEdt

The test asserted a real property against an unwinnable race. `Health`'s
`openHealthSettings` completes its `EdtResult` before it returns, so by the
time the test attached `onResult` the EDT had usually already run the queued
delivery. `AsyncResource.ready` runs a listener attached after settlement
inline on the attaching thread, so the landing recorded "not the EDT" for a
delivery that had in fact happened there.

The delivery stack confirms it -- the callback fired from `ready`'s
already-done branch, not from the queued runnable.

The other six tests dodge this by holding the backend open until the listener
is attached; the facade has nothing to hold. Park the EDT instead: a blocking
runnable queued ahead of the delivery, released once the listener is on.

Also corrects the `assertDeliveredOnEdt` docs. They claim these tests run on
the EDT and that `invokeAndBlock` keeps the loop pumping. JUnit runs them on
`main`, so `invokeAndBlock` takes its non-EDT branch and runs the operation
inline while the real EDT pumps alongside -- which is exactly why the race
was wide open. Moves the facade test's javadoc back onto the facade test; it
had drifted onto `everyPublicHealthResourceDeliversOnTheEdt`.

No product change. `EdtResult` was hopping correctly in every failing run.

Before: 8/8 failures running the method alone, ~1/10 running the whole class.
After: 0/12 alone, 0/30 for the class. SpotBugs/PMD/Checkstyle/Spotless clean.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Copilot AI lite review requested due to automatic review settings August 4, 2026 03:19

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: ba2206130e

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +339 to +340
Health.getInstance().openHealthSettings().onResult(landing);
attached.countDown();

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Always release the EDT blocker

If openHealthSettings() or onResult() throws, attached.countDown() is skipped while the EDT remains blocked in await(). Instead of reporting the original failure, teardown's EDT flush or later UI tests can then hang indefinitely. Release the latch in a finally block around the facade call and listener attachment.

Useful? React with 👍 / 👎.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR fixes a deterministic/flake race in HealthEdtDeliveryTest#aFacadeActionDeliversOnTheEdt by ensuring the test attaches its result listener before the EdtResult delivery runnable is allowed to run on the EDT. It also updates nearby test documentation/comments to reflect the actual threading behavior of the JUnit harness used here.

Changes:

  • Add an EDT “barrier” (via CountDownLatch + CN.callSerially) to prevent the facade result delivery from racing ahead of listener attachment.
  • Correct and expand assertDeliveredOnEdt documentation to match how the tests actually execute (JUnit main thread + a concurrently running EDT).
  • Move the facade test JavaDoc back onto the facade test method it describes.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +330 to +340
CN.callSerially(new Runnable() {
public void run() {
try {
attached.await();
} catch (InterruptedException ex) {
Thread.currentThread().interrupt();
}
}
});
Health.getInstance().openHealthSettings().onResult(landing);
attached.countDown();
@github-actions

github-actions Bot commented Aug 4, 2026

Copy link
Copy Markdown
Contributor

✅ Continuous Quality Report

Test & Coverage

Static Analysis

  • SpotBugs [Report archive]
    • ByteCodeTranslator: 0 findings (no issues)
    • android: 0 findings (no issues)
    • codenameone-maven-plugin: 0 findings (no issues)
    • core-unittests: 0 findings (no issues)
    • ios: 0 findings (no issues)
  • PMD: 0 findings (no issues) [Report archive]
  • Checkstyle: 0 findings (no issues) [Report archive]

Generated automatically by the PR CI workflow.

Codex and Copilot both caught the same defect in the barrier: if
`openHealthSettings` or `onResult` threw, `countDown` was skipped and the EDT
stayed parked in an unbounded `await`. The suite would then hang rather than
report the throw that caused it -- a worse failure mode than the flake being
fixed.

Release the latch in a `finally`, and bound the wait at 10s as a backstop for
anything the `finally` cannot reach.

Verified with a throwaway probe replicating the barrier around a deliberate
throw: the `IllegalStateException` surfaces as itself, the blocker is
released, and a later test's serial call still runs on a live EDT -- in 0.106s,
so the `finally` did the releasing and the timeout was never reached.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Copilot AI review requested due to automatic review settings August 4, 2026 07:29
@shai-almog

Copy link
Copy Markdown
Collaborator Author

Good catch — fixed in 1653359.

You're right that the failure mode was worse than the flake being fixed: a skipped countDown() would leave the EDT parked in an unbounded await(), so the suite hangs and the throw that caused it never gets reported.

Applied both suggestions — countDown() in a finally, and the wait bounded at 10s as a backstop for anything the finally can't reach:

CN.callSerially(new Runnable() {
    public void run() {
        try {
            attached.await(10, TimeUnit.SECONDS);
        } catch (InterruptedException ex) {
            Thread.currentThread().interrupt();
        }
    }
});
try {
    Health.getInstance().openHealthSettings().onResult(landing);
} finally {
    attached.countDown();
}

Verified with a throwaway probe that replicates the barrier around a deliberate throw:

PROBE later serial call ran, isEdt=true
PROBE later test saw EDT alive=true
PROBE blocker released
[ERROR] ZzTmpHangProbeTest.throwingInsideTheBarrier... » IllegalState simulated facade failure
Tests run: 2, Failures: 0, Errors: 1 -- Time elapsed: 0.106 s

The IllegalStateException surfaces as itself, the blocker is released, and a later test's serial call still runs on a live EDT. Elapsed 0.106s, so the finally did the releasing — the 10s timeout was never reached. Probe deleted after the run.

Re-verified after the change: 0/10 failures running the method alone, 0/25 for the full class, and mvn verify still green (SpotBugs BugInstance size is 0, PMD/Checkstyle/Spotless clean).

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 1 out of 1 changed files in this pull request and generated no new comments.

@shai-almog
shai-almog merged commit d01adec into master Aug 4, 2026
13 of 14 checks passed
@shai-almog
shai-almog deleted the fix-health-edt-delivery-test-race branch August 4, 2026 08:17
shai-almog added a commit that referenced this pull request Aug 4, 2026
Master had independently fixed several of the same defects, usually better, so
those resolutions take master's side outright rather than preserving mine.

- javase-cef-ffmpeg-smoke.yml / build-android-app.sh: same two fixes, same
  reasoning. Master's ffmpeg step also runs `ffmpeg -version` to confirm the
  install rather than trusting choco's exit code, which mine did not.

- MCPLoopbackSocketTransport: master reworked the whole open/close protocol
  around an `opening` claim taken in the same critical section that checks it,
  and deliberately supports reopening one transport across stop()/start() by
  clearing `closed` when the claim is taken. My fix refused to claim while
  closed, which is directly incompatible with that -- so master's design wins
  and mine is dropped. Master's test file covers the race I was chasing more
  thoroughly than my test did (stoppingWhileTheReaderThreadIsStillOpening...,
  restartingWhileTheOldReaderIsStillOpening...), and its premise -- that a
  closed transport must refuse to reopen -- is no longer true, so my test goes
  with it.

- EdtResult: master hops the registration onto the EDT and documents why
  `except` must stay synchronous (HealthFallbackTest.errorOf and BtTestUtil
  read an error by registering a callback and depend on it). My version wrapped
  `except` too. Master's is the more careful of the two; git had auto-merged
  both into a duplicate method.

- HealthEdtDeliveryTest: master's #5516 added a test with the same name and
  intent as mine.

- BrowserComponentScreenshotTest: both tightened the blank-frame check; master's
  thresholds are stricter on both axes (a quarter of the band's pixels rather
  than bandWidth/20, and r<48 rather than r<80), so master's numbers stand with
  my note about the Linux stub kept alongside them.

Verified on the merged tree: 4754 core tests, SpotBugs clean, 26 port-status
gate tests, all four touched workflows parse, both sweep scripts parse,
copyright gate over 97 files.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants