Siyao Meng created HDDS-17548:
---------------------------------

             Summary: OM client turns follower read off for the rest of its 
life, or keeps retrying a not ready leader, after an OM rejects a LOCAL_LEASE 
read
                 Key: HDDS-17548
                 URL: https://issues.apache.org/jira/browse/HDDS-17548
             Project: Apache Ozone
          Issue Type: Bug
            Reporter: Siyao Meng
         Attachments: 
MC-1-MC-4-local-lease-rejection-keeps-follower-read.patch, 
TestBugMC1LocalLeaseRejectionDisablesFollowerRead.java, 
TestBugMC4LocalLeaseNotReadyLeaderRetry.java

h3. Mechanism
{{OzoneManagerProtocolServerSideTranslatorPB.submitReadRequestToOmLocalLease}} 
rejects a LOCAL_LEASE read with {{createLeaderErrorException}} when local lease 
is disabled on the OM or the OM fails {{allowFollowerReadLocalLease}} (last 
leader RPC older than the lease time, applied index too far behind the leader's 
commit index, or no known leader with a finite log limit). On a follower that 
is {{OMNotLeaderException}}, on a leader that is not ready yet it is 
{{OMLeaderNotReadyException}}. Both client providers, 
{{HadoopRpcOMFollowerReadFailoverProxyProvider}} and 
{{GrpcOMFollowerReadFailoverProxyProvider}}, treat these answers as routing 
errors:
* {{OMNotLeaderException}} from the follower read node calls 
{{disableFollowerRead}}. The flag is shared by the whole client and only the 
constructor sets it. It is meant for a cluster whose followers do not serve 
follower reads at all (for example a Ratis read option other than LINEARIZABLE, 
where a LINEARIZABLE_ALLOW_FOLLOWER read gets the same exception). Afterwards 
{{shouldUseFollowerRead}} is false for every request, and reads without a hint 
get the leader consistency ({{ozone.client.leader.read.default.consistency}}, 
DEFAULT) instead of the follower consistency.
* {{OMLeaderNotReadyException}} is rethrown to the retry policy without moving 
the follower read node. The policy retries the OM it believes is the leader 
({{setNextOmProxy(getCurrentProxyOMNodeId())}}), and 
{{selectFollowerReadNodeId}} skips that OM for a LOCAL_LEASE read, so it picks 
the same not ready OM again. The rethrow assumes that the not ready OM is the 
believed leader, which is not the case after an election the client has not 
seen yet.

h3. Trigger
Both need a LOCAL_LEASE read: 
{{ozone.client.follower.read.default.consistency=LOCAL_LEASE}}, a per request 
hint through {{ClientProtocol.setThreadLocalReadConsistency}}, or the S3 header 
{{x-ozone-read-consistency: follower-stale}} (with an optional 
{{x-ozone-local-lease-log-limit}}). The S3 gateway honours the header only when 
security is off; with security on it sends every S3 request as 
LINEARIZABLE_LEADER_ONLY. Both providers are always installed, and a read with 
an explicit LOCAL_LEASE hint goes to a follower even with 
{{ozone.client.follower.read.enabled=false}} (the default). The consistency 
change in the first impact below needs 
{{ozone.client.follower.read.enabled=true}} with LINEARIZABLE_ALLOW_FOLLOWER as 
the follower default; with follower read off on the client, unhinted reads 
already go to the leader as DEFAULT, and a rejected hinted read only stops 
later hinted reads from using followers.
* Follower read turned off: one LOCAL_LEASE read reaches a follower that fails 
the lease check. At default settings that happens during an election, after a 
gap of more than 5 s in leader RPCs, or more than 10000 entries behind the 
leader. With a small per request log limit (0 from the S3 header) a follower 
that is one entry behind under write load is enough. An OM with 
{{ozone.om.follower.read.local.lease.enabled=false}} (the default) rejects 
every LOCAL_LEASE read this way.
* Retries pinned to a not ready leader: a new leader is elected before the 
client learns about it (it learns from the {{OMNotLeaderException}} redirect of 
a request sent to the old leader). The new leader rejects LOCAL_LEASE reads 
while it is not ready and its apply backlog is larger than the log limit (10000 
by default, or the per request limit). The old leader, now a follower, and the 
third OM pass the same check and could serve.

h3. Impact
* Follower read stays off for the rest of the client's life. The S3 gateway 
uses one {{OzoneClient}} for all requests, so a single request can turn it off 
for every user of that gateway until it restarts. All reads then go to the 
leader. Reads without a hint that the client would send as 
LINEARIZABLE_ALLOW_FOLLOWER (the default follower consistency) are sent as 
DEFAULT, which the leader serves locally after checking its own Raft role, 
without a ReadIndex round. That is what a client without follower read gets, 
but it is weaker than the configured linearizable read: a leader that has lost 
leadership without noticing yet can return stale data (argued from the code, 
not shown). For a client whose default is LOCAL_LEASE the reads get stronger, 
and the effect is only the loss of follower read. On a cluster with local lease 
disabled, LOCAL_LEASE reads also switch off the LINEARIZABLE_ALLOW_FOLLOWER 
follower reads the cluster does serve.
* The LOCAL_LEASE read is retried only on the not ready OM, with the growing 
wait of the retry policy, until that OM is ready or 
{{ozone.client.failover.max.attempts}} is used up, while the other OMs could 
serve it at once. At default settings (500 attempts, 2 s base wait) this is 
mostly a stall that can last past the not ready window; a failed read needs a 
smaller budget or a long window. Another request on the same client that 
corrects the believed leader ends it. Since HDDS-17448 the gRPC client (the S3 
gateway path) does the same as the Hadoop RPC client; before it, the gRPC 
client fell back to the leader here.

h3. Reproduction
[^TestBugMC1LocalLeaseRejectionDisablesFollowerRead.java] runs in 
{{hadoop-ozone/integration-test}} on the shared three OM cluster of 
{{OzoneManagerHAFollowerReadTests}} (Ratis LINEARIZABLE read, local lease 
enabled), with the Hadoop RPC and the gRPC transport, through the client API 
plus the existing test hooks that pin the follower read node 
({{changeInitialProxyForTest}}, {{changeLeaderProxyForTest}}, 
{{changeFollowerReadInitialProxy}}). One LOCAL_LEASE read with a per request 
lease time of 0 ms is rejected by the follower (its 
{{NumFollowerReadLocalLeaseFailTime}} metric goes up) and served by the leader. 
Afterwards 5 reads without a hint, pointed at the same follower, did 0 
linearizable reads on the follower and 0 on the leader with both transports. 
With the patch the follower served all 5 as linearizable reads.

[^TestBugMC4LocalLeaseNotReadyLeaderRetry.java] runs in 
{{hadoop-ozone/common}}. The Hadoop RPC retry proxy and the gRPC transport run 
unmodified against three simulated OMs that answer like 
{{submitReadRequestToOmLocalLease}} does for their role: a write teaches the 
client that om1 is the leader, then om2 becomes the leader and stays not ready. 
With a budget of 6 failovers the LOCAL_LEASE read went to om2 on every attempt 
(7 with Hadoop RPC, 6 with gRPC) and failed, and om1 and om3 got no request. 
With the patch it is served at once. The pinned retries were not produced on a 
real cluster: the existing test hooks cannot hold a newly elected OM not ready 
with a backlog above the limit.

h3. Patch
[^MC-1-MC-4-local-lease-rejection-keeps-follower-read.patch], against 
efe0c7eba1527530d1001f6517b733576fdc6d61, which is also current master.

In both providers, an {{OMNotLeaderException}} or {{OMLeaderNotReadyException}} 
answer to a LOCAL_LEASE read now moves the follower read node to the next OM 
and sends this read to the leader, as the {{OMNotLeaderException}} branch 
already did, but without {{disableFollowerRead}}. Other reads keep the current 
handling: a cluster that does not serve LINEARIZABLE_ALLOW_FOLLOWER still turns 
follower read off 
({{TestOzoneManagerHAWithAllRunning.testOMFollowerReadWithClusterDisabled}}), 
and a not ready leader is still retried after a wait (HDDS-17448). There is no 
server, protocol or configuration change. A separate server change makes OMs 
reject more LOCAL_LEASE reads; this client fix should land first or together 
with it. One behaviour changes for a setup without the bug: a LOCAL_LEASE 
client against OMs with local lease disabled, or against OMs older than 
follower read support (2.1.x and older answer every read on a follower with 
{{OMNotLeaderException}}, which matters when clients are upgraded first), now 
sends each LOCAL_LEASE read to one follower and then to the leader (one extra 
RPC per read), where before the first rejection sent all later reads to the 
leader. That cost is acceptable because the rejection is a cheap role and 
configuration check on the OM, is logged at DEBUG only, and only hits clients 
that asked for LOCAL_LEASE, while the old behaviour silently changed the read 
consistency of every later read. Trying the remaining followers before the 
leader, as for {{ReadIndexException}}, would cost up to one RPC per OM per read 
in that setup, with a warning per failed follower on the Hadoop RPC path, so 
the patch goes to the leader right away.

Covered by {{testLocalLeaseRejectionKeepsFollowerRead}} and 
{{testLocalLeaseReadOnNotReadyLeaderUnknownToClient}} in 
{{TestHadoopRpcOMFollowerReadFailoverProxyProvider}} (through the retry proxy) 
and {{testLocalLeaseRejectionFallsBackToLeader}} (explicit hint) and 
{{testLocalLeaseRejectionWithLocalLeaseDefault}} (no hint, client default 
LOCAL_LEASE) for both exceptions in 
{{TestGrpcOMFollowerReadFailoverProxyProvider}}. Without the change they fail 
with "expected: <true> but was: <false>" for the follower read flag and with 
the {{OMLeaderNotReadyException}} thrown to the caller. With it the client side 
follower read and failover suites in {{hadoop-ozone/common}} pass (98 tests), 
as do {{TestOzoneManagerHAFollowerReadWithAllRunning}}, 
{{TestOzoneManagerHAFollowerReadWithStoppedNodes}}, 
{{TestOzoneShellHAWithFollowerRead}} and 
{{testOMFollowerReadWithClusterDisabled}} (26 tests), and checkstyle is clean.

Found by TLA+ model checking and code review of the OM follower read and read 
consistency routing paths under HDDS-15926, on commit 
efe0c7eba1527530d1001f6517b733576fdc6d61. Checked against HDDS issues and 
apache/ozone pull requests for duplicates before filing. Related but not 
duplicates: HDDS-16435 (a superseded leader serving leader local reads, which 
is the consistency the first symptom falls back to), HDDS-17448 (resolved, 
retry of a not ready leader that is the believed leader) and HDDS-14509 
(resolved, added the client read consistency levels). The attached patch is a 
proposal for review. Generated with Specula (Claude Opus 5.5).



--
This message was sent by Atlassian Jira
(v10.3.26#10030026)

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

Reply via email to