Problem Statement
Two bugs in ReindexThread.java that together caused push-published content to remain invisible on Production for ~2 days (observed on 26.07.06-3 Evergreen in a dotCMS Cloud Production environment).
Since PR #33885, push-published content is indexed asynchronously via the reindex queue on the receiver. Both bugs prevent that queue from draining reliably.
Bug 1 — Shutdown hot-loop
During JVM shutdown, ShutdownCoordinator.isRequestDraining() becomes true before stopThread() sets state to STOPPED. The inner loop in runReindexLoop() detects shutdown and breaks — but this only exits the inner loop and returns control to the outer loop in ReindexThreadRunnable:
// Outer loop — ReindexThreadRunnable (line 155)
while (state.get() != ThreadState.STOPPED) {
try {
runReindexLoop(); // breaks due to shutdown, returns here
} catch (Exception e) { ... }
// state is still RUNNING → outer loop calls runReindexLoop() again → immediate log + break → repeat
}
Observed impact: ~1.2 million "Shutdown detected, stopping reindex operations" log lines in ~2 minutes — putting severe pressure on I/O and contributing to JVM instability/crash.
Bug 2 — Dead runnable not detected in unpauseImpl()
unpauseImpl() assumes that if state == PAUSED, the ReindexThreadRunnable is alive in its sleep() loop:
if (state == ThreadState.PAUSED) {
Logger.info(ReindexThread.class, "--- Unpausing reindex thread ");
cache.get().remove(REINDEX_THREAD_PAUSED);
getInstance().state(ThreadState.RUNNING); // flips flag — but nobody is listening
}
After a crash, the runnable may exit (uncaught Throwable, executor shutdown) while leaving state at PAUSED. When content is push-published, unpauseImpl() logs "Unpausing reindex thread", sets state to RUNNING — but there is no live thread to act on it. The queue never drains. No error is logged.
The STOPPED branch correctly re-submits the runnable, but it is never reached.
Steps to Reproduce
Bug 1:
- Start dotCMS
- Trigger a JVM shutdown while
ReindexThread is in RUNNING state
- Observe millions of "Shutdown detected, stopping reindex operations" log lines before the process exits
The hot-loop only runs during the window between ShutdownCoordinator.isRequestDraining() becoming true and stopThread() being called. In production this window was long due to a JVM stall. To reproduce locally, use one of these approaches:
-
Debugger breakpoint (no code change): Set a breakpoint on the first line of stopThread() (line 299 of ReindexThread.java). Trigger a server shutdown — the breakpoint pauses stopThread() while ReindexThread hot-loops on its own thread. Watch the log accumulate lines rapidly, then resume to complete shutdown.
-
Temporary sleep in ReindexThreadShutdownTask: Add a Thread.sleep(5000) before the stopThread() call to create a 5-second window. Start dotCMS, wait for ReindexThread to reach RUNNING state, shut down, and observe the dense burst of log lines.
-
Unit test: Mock ShutdownCoordinator.isRequestDraining() to return true, start ReindexThreadRunnable in a thread without calling stopThread(), let it run for ~200 ms, then assert "Shutdown detected" was logged more than once — proving the hot-loop. Use mockStatic if isRequestDraining() is a static method.
Bug 2:
- Start dotCMS —
ReindexThread starts, queue is empty, thread enters PAUSED state
- Pod/JVM becomes unstable (thread stall, executor shutdown) — runnable exits but
state remains PAUSED
- Push-publish content from a remote environment
- Observe: "Unpausing reindex thread" is logged (transaction committed), state is set to
RUNNING
- Observe: content does NOT appear on the target site — queue is never drained
- Manual reindex makes content visible immediately (bypasses the queue)
Acceptance Criteria
dotCMS Version
26.07.06-3 Evergreen. Likely affects all Evergreen builds containing PR #33885 (async receiver reindex).
Severity
High - Major functionality broken
Links
Problem Statement
Two bugs in
ReindexThread.javathat together caused push-published content to remain invisible on Production for ~2 days (observed on 26.07.06-3 Evergreen in a dotCMS Cloud Production environment).Since PR #33885, push-published content is indexed asynchronously via the reindex queue on the receiver. Both bugs prevent that queue from draining reliably.
Bug 1 — Shutdown hot-loop
During JVM shutdown,
ShutdownCoordinator.isRequestDraining()becomestruebeforestopThread()sets state toSTOPPED. The inner loop inrunReindexLoop()detects shutdown andbreaks — but this only exits the inner loop and returns control to the outer loop inReindexThreadRunnable:Observed impact: ~1.2 million "Shutdown detected, stopping reindex operations" log lines in ~2 minutes — putting severe pressure on I/O and contributing to JVM instability/crash.
Bug 2 — Dead runnable not detected in
unpauseImpl()unpauseImpl()assumes that ifstate == PAUSED, theReindexThreadRunnableis alive in itssleep()loop:After a crash, the runnable may exit (uncaught
Throwable, executor shutdown) while leavingstateatPAUSED. When content is push-published,unpauseImpl()logs "Unpausing reindex thread", sets state toRUNNING— but there is no live thread to act on it. The queue never drains. No error is logged.The
STOPPEDbranch correctly re-submits the runnable, but it is never reached.Steps to Reproduce
Bug 1:
ReindexThreadis inRUNNINGstateThe hot-loop only runs during the window between
ShutdownCoordinator.isRequestDraining()becomingtrueandstopThread()being called. In production this window was long due to a JVM stall. To reproduce locally, use one of these approaches:Debugger breakpoint (no code change): Set a breakpoint on the first line of
stopThread()(line 299 ofReindexThread.java). Trigger a server shutdown — the breakpoint pausesstopThread()whileReindexThreadhot-loops on its own thread. Watch the log accumulate lines rapidly, then resume to complete shutdown.Temporary sleep in
ReindexThreadShutdownTask: Add aThread.sleep(5000)before thestopThread()call to create a 5-second window. Start dotCMS, wait forReindexThreadto reachRUNNINGstate, shut down, and observe the dense burst of log lines.Unit test: Mock
ShutdownCoordinator.isRequestDraining()to returntrue, startReindexThreadRunnablein a thread without callingstopThread(), let it run for ~200 ms, then assert "Shutdown detected" was logged more than once — proving the hot-loop. UsemockStaticifisRequestDraining()is a static method.Bug 2:
ReindexThreadstarts, queue is empty, thread entersPAUSEDstatestateremainsPAUSEDRUNNINGAcceptance Criteria
ShutdownCoordinator.isRequestDraining()returnstrueinsiderunReindexLoop(), state is set toSTOPPEDbefore breaking, so the outer loop inReindexThreadRunnableexits without re-enteringrunReindexLoop()ReindexThreadRunnablesets a liveness flag (AtomicBoolean) totrueon start and clears it in afinallyblock on exit, including when an uncaughtThrowablecauses the runnable to exit unexpectedlyunpauseImpl()is called andstate == PAUSEDbut the liveness flag isfalse(runnable has exited), the method re-submits theReindexThreadRunnablerather than only flipping state toRUNNINGReindexThreadin a dead-but-PAUSEDstate, push-published content becomes visible on Production without requiring a manual reindexcatch (Exception e)inReindexThreadRunnableis widened tocatch (Throwable e)so JVM-level errors (e.g.,OutOfMemoryError) do not bypass thefinallyliveness-cleardotCMS Version
26.07.06-3 Evergreen. Likely affects all Evergreen builds containing PR #33885 (async receiver reindex).
Severity
High - Major functionality broken
Links