Skip to content

[#357] Wait for performed cleanup actions, not an empty queue, in persistit tests - #360

Merged
vharseko merged 1 commit into
OpenIdentityPlatform:masterfrom
vharseko:persistit/fix-cleanup-wait-357
Oct 18, 2026
Merged

vharseko merged 1 commit into
OpenIdentityPlatform:masterfrom
vharseko:persistit/fix-cleanup-wait-357

Conversation

@vharseko

Copy link
Copy Markdown
Member

Fixes #357

Cause

CleanupManager.poll() drains the queue into a work list with drainTo(), then performs the actions and increments the performed count after each one. While the work list is being performed, getEnqueuedCount() is already 0. IntegrityCheckTest.waitForCleanupManager() waited only for an empty queue, so it could return before the four CleanupIndexHole actions ran, and testIndexFixHoles saw a performed count of 0 (expected:<0> but was:<4>, with the arguments swapped).

Change

  • PersistitUnitTestCase.waitForCleanupManager() waits until the performed and error counts add up to the accepted count, kicking the manager between checks. It fails after 60 seconds with the counts and the manager's last exception. JournalManagerTest and Bug1056489Test already wait on these counters.
  • IntegrityCheckTest.testIndexFixHoles and testPrune use it instead of their private queue-size wait, and the testIndexFixHoles assertion has its arguments in order.
  • TreeLifetimeTest.testCleanupManagerShouldNotInstantiateTrees uses it as well. Its queue-size wait could end before the action ran, and then the test passed without checking anything.

TransactionAbandonedTest.readAndWrite, which the issue also lists, is not changed. It disables background cleanup and runs cm.runTask() on its own thread, so its loop ends only after the test thread has performed the actions. Its real problem is that it never writes in the abandoned transaction: #358.

Verification

  • With a temporary 2-second sleep in poll() between drainTo() and performing the actions, the unchanged testIndexFixHoles fails with the CI message IntegrityCheckTest.testIndexFixHoles:242 expected:<0> but was:<4>. With this change, testIndexFixHoles, testPrune and testCleanupManagerShouldNotInstantiateTrees pass, and the last one now takes 2.4 seconds because it waits for the action.
  • The full persistit/core suite passes on JDK 11: 600 tests, 0 failures, 5 skipped. After review fixes to the helper (nanoTime deadline, last exception in the message, Javadoc), IntegrityCheckTest, TreeLifetimeTest, TransactionAbandonedTest and CleanupManagerTest pass again.

Related: #359 (CleanupManager.offer() counts an action as accepted after poll() can already perform it).

… empty queue, in persistit tests

CleanupManager.poll() drains the queue into a work list before it performs
the actions and counts each one only after performing it. A wait on
getEnqueuedCount() can therefore end while the drained actions are still
pending, and IntegrityCheckTest.testIndexFixHoles then saw a performed
count of 0 (expected:<0> but was:<4>, the arguments being swapped).

Add PersistitUnitTestCase.waitForCleanupManager(), which waits until the
performed and error counts add up to the accepted count, and use it in
IntegrityCheckTest and TreeLifetimeTest. Put the arguments of the
testIndexFixHoles assertion in order.

TransactionAbandonedTest is left as it is: it disables background cleanup
and runs the actions on its own thread, so its wait has no such window.

Fixes OpenIdentityPlatform#357

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

praise: The wait now counts what poll() does after an action runs, not the queue that poll() empties first.

  • PersistitUnitTestCase.waitForCleanupManager exits once getPerformedCount() + getErrorCount() reaches getAcceptedCount(), and CleanupManager.poll() advances _performed/_errors only after performAction.
  • The 60-second failure message carries all three counters and getLastException(), so a timeout explains itself.
  • IntegrityCheckTest.testIndexFixHoles now passes holeCount to assertEquals as the expected value.

@vharseko
vharseko merged commit 42b0593 into OpenIdentityPlatform:master Oct 18, 2026
14 checks passed
@vharseko
vharseko deleted the persistit/fix-cleanup-wait-357 branch October 18, 2026 08:04
vharseko added a commit that referenced this pull request Oct 19, 2026
…tent (#368)

Fixes #359
Fixes #367

## Problem
- **#359**: `CleanupManager.offer()` put the action into the queue first
and counted it as accepted afterwards. `poll()` drains the queue without
the monitor that `offer()` holds, so in between it could perform the
action and count it as performed. For that moment `getPerformedCount() +
getErrorCount()` could exceed `getAcceptedCount()`, or equal it while an
accepted action was still pending. The counter-based waits in the tests
rely on that not happening.
- **#367**: two paths took accepted actions out of the queue without
counting them, so the performed and error counts never caught up with
the accepted count again:
- `poll()` caught only `PersistitException` around each action. A
`RuntimeException` left `poll()` (`IOTaskRunnable.run()` only logs it),
was neither counted as an error nor posted to the `AlertMonitor`, and
the rest of the drained work list (up to 2,000 actions) was lost.
  - `clear()` discarded the queued actions without counting them.
- Found in the review: logging an `AlertMonitor` event replaced a
`RuntimeException` among the event's arguments with its formatted stack
trace. `PersistitLogMessage.Enabled.logMessage()` formatted the
arguments in place, and `Event` passes its own argument array, so after
the first log `Event.getFirstArg()` and `getArgs()` returned a `String`
instead of the exception.

## Fix
- **`offer()`**: checks the `remainingCapacity()` of the queue, counts
the action as accepted and only then adds it. Only `offer()` adds to the
queue, and it holds the monitor while it does, so the checked capacity
is still there when the action is added. Counting first and taking the
count back on refusal would make the accepted count briefly too high and
let it go down; with the capacity check both the accepted and the
refused count stay exact and only ever grow.
- **`poll()`**: a `RuntimeException` from an action is handled like a
`PersistitException`: the action counts as an error, the alert is
posted, and the rest of the work list is performed. An `Error` still
ends the loop and the `CLEANUP_MANAGER` thread, as before; the comment
says so.
- **`clear()`**: drains the queue into a list and counts the discarded
actions; `CleanupManagerMXBean` reports them as the new
`getDiscardedCount()`. `drainTo()` hands each action either to `clear()`
or to a concurrent `poll()`, never to both.
- **`PersistitLogMessage.Enabled.logMessage()`**: formats a copy of the
arguments, so the event keeps the exception.
- New `CLEANUP_OFFER_A`/`B`/`C` sequencer locations and
`CLEANUP_OFFER_SCHEDULE`; `offer()` passes `CLEANUP_OFFER_A` between
counting the action and adding it to the queue.

## Tests
- `testActionAcceptedBeforePerformed`: parks the test's `offer()` at
`CLEANUP_OFFER_A` (a `Condition`, set before the schedule is enabled,
limits the location to the test's thread) and checks that the action
already counts as accepted and is not in the queue yet. A test timeout
turns a missed `CLEANUP_OFFER_A` into a failure instead of a hung build.
- `testRuntimeExceptionCountedAsError`: an action failing with a
`RuntimeException` counts as an error, the action after it is performed,
`getLastException()` returns the exception, and the `AlertMonitor` holds
one cleanup event carrying it.
- `testClearCountsDiscardedActions`: after a `poll()` and a `clear()`,
the accepted count equals the performed, error and discarded counts.
- `testOverflow`: checks the exact accepted and refused counts instead
of `>= 0`.
- These four tests use a `CleanupManager` that is never started: no
`CLEANUP_MANAGER` thread polls it and no Persistit thread offers to it,
so only the test changes its counters.
- `AlertMonitorTest.testLoggingKeepsEventArguments`: a logged event
still holds its `RuntimeException`, and the log message still contains
the stack trace.
- `CheckpointCloseRaceTest.copyBackPagesConcurrentWithClose` sets its
`Condition` before it enables the schedule as well.
- Mutants killed:
- accepted counted after the action is added (the #359 order),
`add(action); sequence(CLEANUP_OFFER_A); _accepted.incrementAndGet();`:
`testActionAcceptedBeforePerformed`, `expected:<1> but was:<0>`;
- the hook after both statements, in either order:
`testActionAcceptedBeforePerformed`, `expected:<0> but was:<1>`;
- `offer()` never reaching `CLEANUP_OFFER_A` (refusing every action,
timeout cut to 5 s for the run): `testActionAcceptedBeforePerformed`
fails with `TestTimedOutException` and the rest of the class runs;
- accepted counted before the capacity check: `testOverflow`,
`expected:<100000> but was:<200000>`;
- only `PersistitException` caught: `testRuntimeExceptionCountedAsError`
fails with the `IllegalStateException` out of `poll()`;
- the error counted but not posted:
`testRuntimeExceptionCountedAsError`, `the RuntimeException should be
posted to the AlertMonitor`;
- the old `clear()`: `testClearCountsDiscardedActions`, `expected:<7>
but was:<0>`;
- arguments formatted in place: `testLoggingKeepsEventArguments` and
`testRuntimeExceptionCountedAsError`, `expected same`.
- Full `persistit/core` suite on JDK 27 at fb95353: 604 tests, 0
failures, 0 errors, 5 skipped.

## Note
`waitForCleanupManager()` from #360 waits until the performed and error
counts add up to the accepted count. No test calls `clear()` before such
a wait, so it needs no change for the new discarded count.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

concurrency Race conditions, locking and thread-safety java Pull requests that update java code tests Test code changes

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Flaky test: IntegrityCheckTest.testIndexFixHoles checks the performed count before the cleanup actions run

2 participants