Conversation
Codecov Report✅ All modified and coverable lines are covered by tests. 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:
|
bcd74b0 to
6583f28
Compare
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
6583f28 to
9658da8
Compare
papegaaij
left a comment
There was a problem hiding this comment.
dontKeepPendingPagesInMapIfSessionExpires can now hang indefinitely; see the inline comment.
| asyncPageStore.destroy(); | ||
|
|
||
| semaphore.release(); |
There was a problem hiding this comment.
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:
destroy()interrupts the thread, and the delegate'scatch (InterruptedException e) {}swallows the interrupt, clearing the flag.- The
while (!Thread.interrupted())loop inPageAddingRunnabletherefore carries on and returns toqueue.poll(...), so the thread never exits. destroy()callsjoin()without a timeout, so it waits forever andsemaphore.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:
| 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.
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