Make WarmupTest.readOrderIsSequential deterministic#259
Conversation
maximthomas
left a comment
There was a problem hiding this comment.
Findings (verified against the source in this checkout)
-
Comment misstates the helper.
PersistitUnitTestCase.disableBackgroundCleanup()
(PersistitUnitTestCase.java:238) only setsCleanupManager.setPollInterval(-1)
andJournalManager.setWritePagePruningEnabled(false). TheBufferPool.PageWriter
(BufferPool.java:294,1351, started atPersistit.java:687) and the
CheckpointManagerkeep running. The comment blames "CLEANUP_MANAGER /
page-writer / checkpoint activity" and claims disabling it makes the drain
deterministic; two of the three named actors are untouched. -
"Drain can be left incomplete" contradicts
copyBack().JournalManager.copyBack()
(1657–1669) blocks until the copier clears_copyFast, which happens at
JournalManager.java:2205only when_copyList.isEmpty(). Nothing is silently
abandoned. Re-journaling that matters must happen aftercopyBackPages()returns. -
The likely real source of journal-resident pages is
close(), untouched here.
Persistit.close()callsrecordBufferPoolInventory()first (Persistit.java:1632);
BufferPool.recordBufferInventory()(1448) issues ~bufferCountstore()calls into
the_buffer_inventory_tree, and the final_checkpointManager.close(flush)
(Persistit.java:1654) writes those dirty pages to the journal with no
subsequentcopyBack().VolumeStorageV2.readPage()consults
readPageFromJournal()first (VolumeStorageV2.java:450), so those pages never
touch the injected volume channel. -
Unfalsified. PR body states the flake does not reproduce locally and the fix rests
on "mechanism analysis" — which is the weak link per 1–3. One green CI run proves
nothing about an intermittent, platform-specific failure. -
assertOrderedis a no-op.TrackingFileChannel.assertOrdered(164–174) declares
final long previous = -1and never updates it in the loop, soforward=true
asserts onlyposition > -1. The test namedreadOrderIsSequentialnever checks
ordering; the sole failing assertion isgetReadPositionList().size() > 0. -
Restarted instance keeps background cleanup on.
disableBackgroundCleanup()binds
_persistitat call time; the test then replaces it withnew Persistit().
TrackingFileChannel._readPositionsis an unsynchronizedArrayListmutated in
read(), so a concurrent background volume read is a data race on the asserted list. -
Minor: stale assertion message ("...from journal file" — it is the volume channel).
-
Minor: 14 lines of comment for 1 line of code, encoding CI trivia that will rot.
Sibling call sites (Bug927701Test,IntegrityCheckTest,MVCCPruneBufferTest)
use the helper with no commentary.
Recommended follow-ups (only if the user asks me to implement)
- Fix
TrackingFileChannel.assertOrderedto actually trackprevious. - Correct the assertion message to say "volume file".
- Move/duplicate
disableBackgroundCleanup()to after_persistit.initialize(). - Replace the narrative comment with 2–3 lines that claim only what the helper does,
linking #254. - Add an explicit precondition assertion before restart (e.g. on
JournalManager.getPageMapSize(),JournalManager.java:357) so a failure reports
"journal not drained" instead of the opaquesize() > 0.
…xes) Rework the previous stabilization, which was ineffective and rested on a wrong hypothesis (see review on OpenIdentityPlatform#259): - disableBackgroundCleanup() bound the first Persistit instance, which the test then replaces with new Persistit(); the second instance -- the one that runs preload and the injected channel -- kept background cleanup on. - The real fragility is the read source: a recorded page may be served from the volume file or the journal (VolumeStorageV2.readPage() consults readPageFromJournal() first), non-deterministically across a restart, so counting reads on a TrackingFileChannel injected into the volume channel can legitimately see zero. TrackingFileChannel.assertOrdered was also a no-op (its `previous` cursor never advanced) and its read list was mutated without synchronization by background reads. Assert on the buffer pool's miss counter instead: getMissCounter() increments whenever get() loads a page from disk regardless of source, so it is the source-agnostic signal that preload actually read the recorded pages back. This drops the fragile channel injection and the dead order check.
…xes) Rework the previous stabilization, which was ineffective and rested on a wrong hypothesis (see review on OpenIdentityPlatform#259): - disableBackgroundCleanup() bound the first Persistit instance, which the test then replaces with new Persistit(); the second instance -- the one that runs preload and the injected channel -- kept background cleanup on. - The real fragility is the read source: a recorded page may be served from the volume file or the journal (VolumeStorageV2.readPage() consults readPageFromJournal() first), non-deterministically across a restart, so counting reads on a TrackingFileChannel injected into the volume channel can legitimately see zero. TrackingFileChannel.assertOrdered was also a no-op (its `previous` cursor never advanced) and its read list was mutated without synchronization by background reads. Assert on the buffer pool's miss counter instead: getMissCounter() increments whenever get() loads a page from disk regardless of source, so it is the source-agnostic signal that preload actually read the recorded pages back. This drops the fragile channel injection and the dead order check.
…xes) Rework the previous stabilization, which was ineffective and rested on a wrong hypothesis (see review on OpenIdentityPlatform#259): - disableBackgroundCleanup() bound the first Persistit instance, which the test then replaces with new Persistit(); the second instance -- the one that runs preload and the injected channel -- kept background cleanup on. - The real fragility is the read source: a recorded page may be served from the volume file or the journal (VolumeStorageV2.readPage() consults readPageFromJournal() first), non-deterministically across a restart, so counting reads on a TrackingFileChannel injected into the volume channel can legitimately see zero. TrackingFileChannel.assertOrdered was also a no-op (its `previous` cursor never advanced) and its read list was mutated without synchronization by background reads. Assert on the buffer pool's miss counter instead: getMissCounter() increments whenever get() loads a page from disk regardless of source, so it is the source-agnostic signal that preload actually read the recorded pages back. This drops the fragile channel injection and the dead order check.
…xes) Rework the previous stabilization, which was ineffective and rested on a wrong hypothesis (see review on OpenIdentityPlatform#259): - disableBackgroundCleanup() bound the first Persistit instance, which the test then replaces with new Persistit(); the second instance -- the one that runs preload and the injected channel -- kept background cleanup on. - The real fragility is the read source: a recorded page may be served from the volume file or the journal (VolumeStorageV2.readPage() consults readPageFromJournal() first), non-deterministically across a restart, so counting reads on a TrackingFileChannel injected into the volume channel can legitimately see zero. TrackingFileChannel.assertOrdered was also a no-op (its `previous` cursor never advanced) and its read list was mutated without synchronization by background reads. Assert on the buffer pool's miss counter instead: getMissCounter() increments whenever get() loads a page from disk regardless of source, so it is the source-agnostic signal that preload actually read the recorded pages back. This drops the fragile channel injection and the dead order check.
…xes) Rework the previous stabilization, which was ineffective and rested on a wrong hypothesis (see review on OpenIdentityPlatform#259): - disableBackgroundCleanup() bound the first Persistit instance, which the test then replaces with new Persistit(); the second instance -- the one that runs preload and the injected channel -- kept background cleanup on. - The real fragility is the read source: a recorded page may be served from the volume file or the journal (VolumeStorageV2.readPage() consults readPageFromJournal() first), non-deterministically across a restart, so counting reads on a TrackingFileChannel injected into the volume channel can legitimately see zero. TrackingFileChannel.assertOrdered was also a no-op (its `previous` cursor never advanced) and its read list was mutated without synchronization by background reads. Assert on the buffer pool's miss counter instead: getMissCounter() increments whenever get() loads a page from disk regardless of source, so it is the source-agnostic signal that preload actually read the recorded pages back. This drops the fragile channel injection and the dead order check.
|
Thanks for the detailed review — you're right, the previous commit was ineffective. Reworked in 0bf3d04:
On point 4: I still can't reproduce the flake locally, but the new assertion no longer depends on the non-deterministic read source, rather than resting on the earlier (wrong) hypothesis. Kept the comment short (point 8). |
maximthomas
left a comment
There was a problem hiding this comment.
Thanks for the rework — dropping disableBackgroundCleanup() and the channel injection is right,
and it resolves points 5–7. But the replacement assertion can't fail, so I'm still requesting changes.
1. getMissCounter() > missesBefore is vacuous
preloadBufferInventory() (BufferPool.java:1503) opens the _buffer_inventory_ tree via
getBufferInventoryExchange() (1579) and walks it with exchange.previous() / next() (1516, 1536)
before it loads any recorded page. Those page loads go through get(..., wantRead=true) and hit
bumpMissCounter() (866). Pools are keyed by buffer size alone (Persistit.java:1499) and the test
config has one 16 KiB volume (PersistitUnitTestCase.java:70), so the inventory tree, the data volume
and the sampled pool all share one _missCounter.
Reading the inventory tree alone satisfies the assertion. If volumeForHandle() returned null for
every entry (1546, continue) or every get() threw and was swallowed (1567,
catch (PersistitException e) { // ignore it }), preload would load zero pages and the test would
still pass — exactly the regressions this test exists to catch.
Also, disableBackgroundCleanup() was removed rather than moved after initialize(), so the restarted
instance's CleanupManager / CheckpointManager can bump that same AtomicLong (161) independently.
2. Read-order coverage drops to zero
PageNode.READ_COMPARATOR exists (BufferPool.java:1544) so warmup issues reads in ascending page
order, and this test is the only caller of preloadBufferInventory() outside Persistit.java:707.
My point 5 was to fix assertOrdered, not delete its call site. The test name now describes
something it no longer checks.
3. TrackingFileChannel is orphaned, not removed
The class stays in the tree with zero callers and the broken previous cursor intact. Either fix it
and keep using it, or delete the file.
On the root cause
Close, but inverted. Given the journal page map, the source serving a given page is deterministic —
what varies is which pages get recorded. recordBufferInventory() (1448) interleaves
exchange.to(index).store() (1478) with its scan of _buffers[index], so with 20 buffers the
inventory tree's own pages evict data pages mid-scan and later indices record inventory pages. Those
are dirtied by the inventory write, flushed to the journal by the final _checkpointManager.close(flush)
with no subsequent copyBack(), and served from the journal on restart. When the interleaving catches
all 20 entries, the volume channel sees zero reads.
Suggested direction
- Fix
TrackingFileChannel.assertOrderedto advanceprevious— vacuously true on an empty list, so it
can never be the flaky assertion — and keep the injection. - Get determinism from setup instead of a weaker assertion: after the first
close(), reopen with
bufferinventorydisabled, callcopyBackPages(), close again, then restart and preload. - Assert
JournalManager.getPageMapSize() == 0(JournalManager.java:357) before preload, so a failure
reports "journal not drained" rather than an opaque zero. - If you keep the miss-counter approach instead: have
preloadBufferInventory()returncount, assert on
that, movedisableBackgroundCleanup()afterinitialize(), and rename the test.
|
Thanks — you were right again: the miss-counter assertion was vacuous for exactly the reason you gave (preload's walk of the shared
Verification: instrumented run shows preload issuing 18 reads through the injected channel at strictly ascending positions (32768 … 1425408), and the fixed (The branch also carries the |
The test records the buffer inventory at shutdown, restarts with a TrackingFileChannel injected into the volume channel, preloads, and asserts the channel saw the reads. Intermittently it saw zero reads: recordBufferInventory() interleaves its own _buffer_inventory_ tree stores with the pool scan, so some recorded pages are the inventory tree's own pages; the closing checkpoint flushes those to the journal with no subsequent copyBack(), and on restart VolumeStorageV2.readPage() serves them from the journal first, bypassing the injected volume channel. Make the setup deterministic: after the first close, reopen with buffer inventory recording disabled, run copyBackPages() to drain the journal into the volume, and close again, so every recorded page lives in the volume file. Preload then reads the recorded pages through the injected channel in strictly ascending page order. Also fix TrackingFileChannel.assertOrdered, which never advanced `previous` and so only checked `position > -1`; it now verifies read order.
f7323b5 to
590f2ef
Compare
Problem
WarmupTest.readOrderIsSequentialis flaky: it restarts Persistit, injects aTrackingFileChannelinto the volume channel, callspreloadBufferInventory(), and intermittently sees zero reads on that channel:Root cause
recordBufferInventory()at shutdown interleaves its own_buffer_inventory_tree stores with its scan of the 20-buffer pool, so some recorded pages are the inventory tree's own pages. The closing checkpoint flushes those to the journal with no subsequentcopyBack(). On restart,VolumeStorageV2.readPage()consultsreadPageFromJournal()first and returns without touching the volume channel, so journal-resident recorded pages bypass the injected channel entirely. When the interleaving catches enough entries, the channel records zero reads.Two earlier attempts here were ineffective and are superseded by the head commit:
disableBackgroundCleanup()bound the pre-restart instance (the test replaces it), and the background-race hypothesis was wrong;pool.getMissCounter()was vacuous — preload's walk of the shared inventory tree alone bumps the pool's single miss counter, so the test passed even if zero recorded pages loaded.Fix
Make the channel-based check deterministic through setup (per review):
close(), reopen with buffer inventory recording disabled, runcopyBackPages()to drain the journal into the volume, and close again — so every recorded page (including the inventory tree's own pages) lives in the volume file.TrackingFileChannel, preload, and assert the channel saw a non-empty, strictly ascending sequence of reads.TrackingFileChannel.assertOrdered: it never advanced itspreviouscursor, so it only checkedposition > -1; it now actually verifies read order (restoring the coverage the test is named for).Note: a strict
getPageMapSize() == 0precondition before preload turned out unachievable — the finalclose()always leaves one checkpoint page (not a data page) in the journal — so the assertions are the channel read count plus read order.Also includes the
Bug1017957Teststabilization commit (cherry-picked from #257) so this branch's CI is not blocked by that unrelated flake.Verification
32768 … 1425408);assertOrdered(true, true)verifies them with the fixed cursor.readOrderIsSequential: 10/10 consecutive green runs; fullWarmupTest: 2/2.