[#952] Keep a failed state write from killing the checkpointer and hanging the shutdown - #977
Conversation
|
Ordinals moved: Six open branches had each read 310 as the first ordinal free in master and taken it - #935, #945, Nothing catches this on the way in. The additions land in different parts of the file, so git merges The open PRs which add to the file now hold 310-325 with nothing claimed twice:
No Java moved with it: the generated constant is the key name without its ordinal, so the rename is |
c1cdeb3 to
7d4f983
Compare
… checkpointer and hanging the shutdown
…kpointer thread flushThread is a final field assigned unconditionally by the only constructor, so the guard in shutdown() never fired. It did make the method inconsistent with itself once the wait for the thread became a join() on the same field outside the guard, which is what CodeQL reports as a possible null dereference.
…nothing else claims
7d4f983 to
2830d3b
Compare
|
Rebased onto master ( Only
The commits are the same three, re-parented: 3141ae7, 6dcd250, 2830d3b. Code scanning reports the The description is updated to match. |
Fixes #952.
The hang
ServerStateFlush.run()guarded only against interruption, so any unchecked exception from a state write ended the thread withdonestillfalse-PersistentServerState.save()runs a modify, andModifyOperationBasis.run()lets everything but aCanceledOperationExceptionout.shutdown()waited on that flag with no bound, and it is called fromMultimasterReplication.finalizeSynchronizationProvider(), that is fromDirectoryServer.shutDown()itself: the shutdown never went on tofinalizePlugins(),shutdownLocalBackends()or the release ofserver.lock, so the backends were never closed and the JVM stayed up.ServerShutdownMonitordoes not help - it excludes the thread which initiated the shutdown, which is exactly the one waiting. The domain also stopped checkpointing for the rest of the life of the server, with nothing but the uncaught-exception alert to say so.The exception does not have to be exotic. A pluggable backend in an error state only produces a result code, because
BackendImplconvertsStorageRuntimeExceptioninto aDirectoryException- but an exception which is not aStorageRuntimeExceptiongoes straight through.CASStorage.execute()callssession.execute()directly, so anAllNodesFailedExceptionor aDriverTimeoutExceptionon a Cassandra-backed domain lands in the checkpointer.This is independent of #948, which has landed since and changes
save()itself: the exception still comes out ofsave()after it.The change
state.save()calls of the checkpointer go throughsaveState(), which catchesRuntimeException, logs it, and keeps the loop going. The state is left marked as unsaved, so the next checkpoint writes it again.shutdown()takes that monitor to wake the checkpointer up, so a write which does not come back used to block the shutdown before it ever reached the wait it does for the thread.doneis gone, and with it the hand-rolled join it stood for.shutdown()now waits withflushThread.join()and logs when the thread is still alive afterwards.join()covers a thread killed by anError- which nocatchin the loop can be expected to hold - a thread stuck in a write, and a thread which was never started, which is what the initialdone = truestood for. The 30 s it waits is the budgetServerShutdownMonitorgives a thread before it starts interrupting them.if (flushThread != null)around the wake-up goes with it. The field isfinaland the one constructor of the class assigns it unconditionally, so the guard could never fire - and it was the only reason thejoin()below it looked like the dereference of a nullable field, which is what the CodeQL alert on this PR reported.Rebased on master
Twice since it was opened. Neither rebase moved anything of the change itself: the diff of the branch against master is the same line for line before and after each of them, apart from context.
First onto f3076a0, over #948 and #908. Two files conflicted:
LDAPReplicationDomain.java- [#916] Keep an update that lands during a ServerState save out of the saved flag #948 put the final save ofrun()under a!disabled && !importInProgress()guard, where this branch replaces that same call withsaveState(). Both are kept: the guard and the comment which explains it stay, and the call inside goes throughsaveState(). An exception from that last write is exactly the one which used to endrun()withdonestillfalse, so it is the one which least deserves to be let out.replication.properties- [#908] Wait for the changes being applied before a domain going down saves its ServerState #945 landed 319-320 right where this branch adds its own. Merged by keeping both sides, without renumbering anything.Then onto 13d57e0, over #935, #959, #972, #973, #975 and #976. Only
replication.propertiesconflicted, and only because #972 put its 326-327 at the end of the file, where this branch's two messages sit. Merged by keeping both sides again: master's 326-327 stay where they are and this branch's 323-324 follow them.LDAPReplicationDomain.javamerged on its own - #972 madeeclDomainvolatile a few lines above thedoneflag this branch removes, and #976 reworded a trace message far from the checkpointer.Testing
ServerStateFlushTestdrives a domain over a memory backend whose write ofds-sync-statethrows, dies of anError, or blocks on a latch. Every test was watched failing against master first:checkpointerKeepsCheckpointingAfterAStateWriteThatThrowsmsgID=140, throughLDAPReplicationDomain:566andModifyOperationBasis:337) andds-sync-stateis never written againshutdownCompletesWhenEveryStateWriteThrowsshutdown()shutdownCompletesWhenTheCheckpointerDiedOfAnErrorshutdown()shutdownCompletesWhileAStateWriteIsStuckshutdown()PersistentServerStateTest,ReplicationServerLoadBalancingTestandStateMachineTestwere run alongside it and pass.Re-run after each rebase:
ServerStateFlushTestis green on the branch rebased onto f3076a0 and again on the one rebased onto 13d57e0 (2830d3b),Tests run: 4, Failures: 0, Errors: 0, Skipped: 0both times.The other direction was watched again on master at f3076a0, the commit this branch sat on after the first rebase, and it
does not even get past the first test.
checkpointerKeepsCheckpointingAfterAStateWriteThatThrowsfailsthe same way it did before - the checkpointer terminates abnormally (
msgID=140, now throughLDAPReplicationDomain:620andModifyOperationBasis:337) andds-sync-stateis never written again -and then the tear-down of that very test never returns. The surefire thread dump taken 40 minutes in has
mainparked inLDAPReplicationDomain.shutdown()on theThread.sleep(50)of thewhile (!done)loop at
LDAPReplicationDomain:2495, underMultimasterReplication.deleteDomain(), with thecheckpointer long dead and
donefalse for good. The run was still there eleven hours later and had tobe killed - which is the hang of this issue, with a test suite in the place of a server shutdown.
Left alone on purpose, and worth an issue of its own: when the checkpointer is interrupted while no shutdown has been initiated, the
catchputs the interrupt flag back and the nextwait(1000)throws at once - a hot loop which never writes the state. Nothing in the tree interrupts that thread today.Ordinals
ERR_CHECKPOINTING_STATE_FAILED_323andERR_STATE_CHECKPOINTER_NOT_STOPPED_324, moved off 310 and311 in 2830d3b. Six open branches had each read 310 as the first ordinal free in master and taken
it, and git merges those additions without reporting a conflict - they land in different places in
the file - so the duplicate would only have surfaced afterwards, as two unrelated messages sharing
one support ID. The generator does not check either: it keys on name and ordinal together, so both
compile.
#935, #945 and #959 have landed since, so 310-314 and 319-320 are in master now, and #972 took
326-327 on top of them. 323-324 are still claimed by no other open PR which adds to
replication.properties.