Siyao Meng created HDDS-16745:
---------------------------------

             Summary: An OM client call that exits before issuing an RPC leaves 
Client.callId set and fails every later call on that thread
                 Key: HDDS-16745
                 URL: https://issues.apache.org/jira/browse/HDDS-16745
             Project: Apache Ozone
          Issue Type: Bug
            Reporter: Siyao Meng
         Attachments: 
CR-4-clear-unconsumed-call-id-and-recreate-refreshed-proxy.patch

h3. Mechanism
{{RetryInvocationHandler.Call#invokeMethod}} 
({{RetryInvocationHandler.java:155-160}}) stores the call ID in the 
{{Client.callId}} ThreadLocal before every attempt, with no {{finally}}. The 
only thing that clears it is the {{Client.Call}} constructor 
({{Client.java:244}}), and the next attempt asserts it is clear with 
{{Preconditions.checkState(callId.get() == null)}} ({{Client.java:94}}). An 
invocation that returns or throws without constructing a {{Client.Call}}, that 
is without issuing an RPC, leaves the call ID behind.

{{FollowerReadInvocationHandler#invoke}} has such an exit. 
{{OMProxyInfo#refreshAddressIfChanged}} sets the cached proxy to null by design 
({{OMProxyInfo.java:219}}) so the next dial uses the new address, and the retry 
policy calls it between two attempts of one request 
({{OMFailoverProxyProviderBase.java:266}}, {{:536-551}}). The leader path 
recreates the proxy through {{createOMProxyIfNeeded}}, but the follower loop 
uses {{getCurrentProxy()}}, which returns the existing {{OMProxyInfo}} as is. 
{{method.invoke(current.getProxy(), args)}} at 
{{HadoopRpcOMFollowerReadFailoverProxyProvider.java:267}} then throws 
{{NullPointerException}}, which is not an {{InvocationTargetException}} and 
escapes the handler.

{{TestHadoopRpcOMFollowerReadFailoverProxyProvider.java:445-447}} already works 
around the first half, disabling the call ID with the comment that it "is set 
but not unset properly".

h3. Trigger
A client with {{ozone.client.follower.read.enabled}} and 
{{ozone.client.failover.resolve-needed}} set, both of which default to false, 
issues a read after the OM's IP address has changed, the case the second option 
exists for. The first attempt fails with a connection error, the retry policy 
re-resolves the address, and the next attempt dereferences the null proxy. One 
thread, no concurrency, no node failure beyond the address change.

h3. Impact
The request that triggered the refresh fails instead of reaching the new 
address, so the refresh does not help follower reads.

Worse, the calling thread is left unusable for OM RPC: every later call on it, 
including one from an unrelated client instance to a healthy OM, fails with 
{{IllegalStateException}} at {{Client.setCallIdAndRetryCount}}. Nothing clears 
the ThreadLocal, so a pooled thread such as an S3 Gateway handler stays broken 
until it is discarded.

h3. Reproduction
PASS. A real Hadoop RPC server stands in for the OM; the only injected state is 
a stale cached address through {{OMProxyInfo#setCachedAddressForTest}}. After 
the failed read, a second independent client on the same thread fails at 
{{Client.java:94}}. Artifacts under {{confirmation/CR-4/}} and {{repro/}}.

The original candidate named three more exits from {{invoke}}. Those were 
examined and found unreachable from production callers, so this issue covers 
only the follower loop.

h3. Suggested fix
Make the call ID self cleaning: clear it in a {{finally}} in 
{{RetryInvocationHandler.Call#invokeMethod}}, so that no present or future exit 
can leave it behind. Separately, have the follower loop recreate the proxy a 
refresh dropped, as the leader path already does.

h3. Patch
[^CR-4-clear-unconsumed-call-id-and-recreate-refreshed-proxy.patch], against 
20e6a1c0f6039248373efd3eb1d7f2afcf7f1535.

{{Client}} gains {{clearCallId()}} and 
{{RetryInvocationHandler.Call#invokeMethod}} calls it in a {{finally}}. 
{{SET_CALL_ID_FOR_TEST}} existed only for the test workaround above, so both 
are removed. {{HadoopRpcOMFollowerReadFailoverProxyProvider#getCurrentProxy}} 
now goes through {{createOMProxyIfNeeded}}.

The existing {{TestHadoopRpcOMFollowerReadFailoverProxyProvider}} covers the 
first half once its workaround is gone: 17 of its tests fail with 
{{IllegalStateException}} without the {{finally}}. The new 
{{testReadAfterDnsRefreshOfCurrentProxy}} in the same suite covers the second 
half and fails with {{NullPointerException}} without the proxy change. 
{{TestRetryProxy}} and the other OM failover proxy provider suites pass. 
Checkstyle clean.

Found by TLA+ model checking and code review of the OM client retry cache and 
at most once mutation paths under HDDS-15926, on commit 
20e6a1c0f6039248373efd3eb1d7f2afcf7f1535. Checked against open HDDS issues for 
duplicates before filing, in addition to the repository's merged git history. 
Generated with Specula (Claude Opus 5).



--
This message was sent by Atlassian Jira
(v8.20.10#820010)

---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]

Reply via email to