SOLR-9355: decide update retries from the cause chain, not one fixed position in it - #4678
Open
serhiy-bzhezytskyy wants to merge 1 commit into
Open
Conversation
…position in it checkRetry asked whether a failure was retriable in two different ways depending on which exception happened to be outermost. If it was a SolrServerException it unwrapped to the root cause; otherwise it tested only the top-level type. The async client reports a connection failure as an ExecutionException wrapping the socket cause, so it took the second branch, the leaf test saw ExecutionException, and the update was not retried. The replica then goes into recovery on exactly the transient glitch this issue describes. Both StdNode and ForwardNode now scan the cause chain rather than checking either end of it, because neither end is reliably the right place to look. The async client wraps the failure from above, and Jetty's ClientConnector wraps the underlying failure in a SocketException of its own, so the retriable frame can sit above the root cause as well as below the top. The scan is bounded. A cause chain can be made cyclic, which SolrException's own getRootCause carries a TODO about, so an unbounded walk could spin; real chains are a few frames deep and the cap only guards the pathological case. The retriable set is unchanged: SocketException or SocketTimeoutException for StdNode, ConnectException for ForwardNode. Only where we look changes. The retry bound is untouched and still lives in Req.shouldRetry, which requires retries < maxRetries and excludes delete-by-query. Tests were written before the fix; two of them fail on unmodified main. A further test records a limit rather than hiding it: ClosedChannelException is what the JDK transport reports for a dropped update connection and is retriable nowhere in Solr today, which is a separate question from this one.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Contributes to SOLR-9355. See my comment there for how this relates to the three questions in that issue's description.
Split out of #4638 at markrmiller's suggestion — his note there was that the expectation of a
SolrServerExceptionis fragile, and that unrolling the exception chain to get the root cause is the better shape.The problem
SolrCmdDistributor.StdNode.checkRetryasks whether a failure is retriable in two different ways, depending on which exception happens to be outermost:isRetriableExceptionis a leaf test onSocketException/SocketTimeoutException. The async client reports a connection failure as a rawExecutionExceptionwrapping the socket cause, so it takes the second branch, the leaf test seesExecutionException, and the update is not retried — the replica goes into recovery on a transient glitch.ForwardNode.checkRetryhas the same asymmetry, narrowed toConnectException.The change
Both now scan the cause chain instead of checking one fixed position in it.
Why scan rather than call
getRootCause— the retriable frame is not always at either end. Jetty'sClientConnectorwraps an underlying failure in aSocketExceptionof its own, so going straight to the root cause walks past it; and the async client wraps from above, so the top-level type misses it too.Why bounded — a cause chain can be made cyclic, which
SolrException.getRootCausecarries a// TODO: This doesn't handle cause loopsabout. Real chains are a few frames deep; the cap only guards the pathological case.What does not change
SocketException/SocketTimeoutExceptionforStdNode,ConnectExceptionforForwardNodeReq.shouldRetry, requiringretries < maxRetriesand excluding delete-by-queryTests
CheckRetryUnrollTest, 10 tests, written before the fix. Two fail on unmodifiedmain— the two shapes where a retriable cause is wrapped in something that is not aSolrServerException— and pass after.The rest are guards rather than new assertions: the two shapes that already worked, the
MAX_CAUSE_DEPTHbound against a cyclic chain, theretry == falsegate, and three negative controls confirming nothing new became retriable.One of them records a limit rather than hiding it:
testClosedChannelExceptionIsStillNotRetriableEitherWay.ClosedChannelExceptionis what the JDK transport reports as the root cause of a dropped update connection, and it is retriable nowhere in Solr today. That is a separate question — see the flag on SOLR-9355 — and this PR deliberately does not widen anything.Coverage gap worth naming
ForwardNode.hasConnectExceptionInChainhas no unit test here.ForwardNodeneeds a liveZkStateReaderto construct, so rather than mock it I noted the limitation in the test class javadoc. It is exercised bySolrCmdDistributorTest(which passes, includingtestForwardNodeWontRetrySocketError), but that is inherited coverage, not coverage added for the new branch. I can add a heavier test if you'd rather have it explicit.