Skip to content

Make the asynchronous page store context test observe its own assertions - #1617

Open
reiern70 wants to merge 1 commit into
masterfrom
fix-async-pagestore-test
Open

reiern70 wants to merge 1 commit into
masterfrom
fix-async-pagestore-test

Conversation

@reiern70

Copy link
Copy Markdown
Contributor

storeAsynchronousContextClosed checks what an IPageStore may do with the IPageContext it is handed once the add has moved to the page saving thread. It queued the add, then called destroy() on the delegate store rather than on the AsynchronousPageStore, so nothing ever interrupted or joined that thread. The test read its failure reference and returned while the saving thread had usually not started, and passed by never looking.

That left two problems. Assertion failures raised on the saving thread are Errors, so PageAddingRunnable's catch of Exception does not hold them; they escaped onto a daemon thread that outlived the test, writing into a store the test had already destroyed. Reported at whatever point the runner noticed, this is the intermittent failure seen in CI. And because the assertions were never reached, a wrong expectation went unnoticed: the test required getSessionAttribute("key2", () -> null) to throw asynchronously.

It does not, and should not. PendingAdd#getSessionAttribute throws only where a value would be changed - a missing entry whose supplier yields a value. A read of a missing key with a null default changes nothing and returns null, which is what the getSessionData block in the same test already expects. The attribute expectations now mirror it: a cached key reads back, a missing key reads null, and only an attempted set throws. No production behaviour changes.

The test now counts down a latch in a finally around the asynchronous body and awaits it, so an add that never happens fails the test instead of passing it. The body records any Throwable for the test thread to rethrow, which lets the sentinel "set a marker exception, then swallow the expected one" idiom give way to assertThrows. destroy() is called on the AsynchronousPageStore, interrupting and joining the saving thread, and still reaches the delegate through DelegatingPageStore.

GitHub issue #1616: #1616

@reiern70
reiern70 requested a review from papegaaij September 23, 2026 18:46
@codecov-commenter

codecov-commenter commented Sep 23, 2026 •

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 61.89%. Comparing base (6550afd) to head (9658da8).

Additional details and impacted files
@@            Coverage Diff            @@
##             master    #1617   +/-   ##
=========================================
  Coverage     61.89%   61.89%           
- Complexity    11242    11244    +2     
=========================================
  Files          1247     1247           
  Lines         48367    48367           
  Branches       6788     6788           
=========================================
+ Hits          29935    29938    +3     
  Misses        15725    15725           
+ Partials       2707     2704    -3     
🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@reiern70
reiern70 force-pushed the fix-async-pagestore-test branch from bcd74b0 to 6583f28 Compare September 23, 2026 19:45
storeAsynchronousContextClosed checks what an IPageStore may do with the
IPageContext it is handed once the add has moved to the page saving thread.
It queued the add, then called destroy() on the delegate store rather than on
the AsynchronousPageStore, so nothing ever interrupted or joined that thread.
The test read its failure reference and returned while the saving thread had
usually not started, and passed by never looking.

That left two problems. Assertion failures raised on the saving thread are
Errors, so PageAddingRunnable's catch of Exception does not hold them; they
escaped onto a daemon thread that outlived the test, writing into a store the
test had already destroyed. Reported at whatever point the runner noticed,
this is the intermittent failure seen in CI. And because the assertions were
never reached, a wrong expectation went unnoticed: the test required
getSessionAttribute("key2", () -> null) to throw asynchronously.

It does not, and should not. PendingAdd#getSessionAttribute throws only where
a value would be changed - a missing entry whose supplier yields a value. A
read of a missing key with a null default changes nothing and returns null,
which is what the getSessionData block in the same test already expects. The
attribute expectations now mirror it: a cached key reads back, a missing key
reads null, and only an attempted set throws. No production behaviour changes.

The test now counts down a latch in a finally around the asynchronous body and
awaits it, so an add that never happens fails the test instead of passing it.
The body records any Throwable for the test thread to rethrow, which lets the
sentinel "set a marker exception, then swallow the expected one" idiom give way
to assertThrows. destroy() is called on the AsynchronousPageStore, interrupting
and joining the saving thread, and still reaches the delegate through
DelegatingPageStore.

runTest, which drives the other four tests in the class, had both faults too.
It discarded the result of its latch await, so a run where the pages were never
added passed regardless, and it destroyed the delegate rather than the
AsynchronousPageStore, leaving a saving thread behind for each of those tests.
It now asserts the await and destroys the facade.

The three remaining tests wrap their store in an AsynchronousPageStore and then
destroyed the delegate, so each left a saving thread running for the rest of the
suite. They destroy the facade as well.

GitHub issue #1616: #1616
@reiern70
reiern70 force-pushed the fix-async-pagestore-test branch from 6583f28 to 9658da8 Compare September 23, 2026 21:09

@papegaaij papegaaij 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.

dontKeepPendingPagesInMapIfSessionExpires can now hang indefinitely; see the inline comment.

Comment on lines +276 to 278
asyncPageStore.destroy();

semaphore.release();

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.

Destroying the AsynchronousPageStore before semaphore.release() can make this test hang forever. It is a race, which is why CI passed; I reproduced the hang locally, with main stuck in Thread.join at AsynchronousPageStore.destroy().

When the page saving thread picks up the first add before removeAllPages runs, it ends up blocked in the delegate's semaphore.acquire(). From there:

  1. destroy() interrupts the thread, and the delegate's catch (InterruptedException e) {} swallows the interrupt, clearing the flag.
  2. The while (!Thread.interrupted()) loop in PageAddingRunnable therefore carries on and returns to queue.poll(...), so the thread never exits.
  3. destroy() calls join() without a timeout, so it waits forever and semaphore.release() is never reached.

When the saving thread has not yet taken the first add, removeAllPages clears both, the interrupt lands in poll(), and the thread exits cleanly. That is the path the CI runs happened to take. The class is tagged SLOW, so -Pfast skips it locally.

Releasing the semaphore first lets the blocked add complete, after which the interrupt reaches the thread in poll() and destroy() returns:

Suggested change
asyncPageStore.destroy();
semaphore.release();
semaphore.release();
asyncPageStore.destroy();

Restoring the interrupt in the delegate's catch (Thread.currentThread().interrupt();) would also fix it, and makes the test robust against the same ordering mistake later.

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.

3 participants