Siyao Meng created HDDS-17579:
---------------------------------

             Summary: New OM leader reports ready while previous term entries 
are still unapplied, so default reads miss acknowledged writes
                 Key: HDDS-17579
                 URL: https://issues.apache.org/jira/browse/HDDS-17579
             Project: Apache Ozone
          Issue Type: Bug
            Reporter: Siyao Meng
         Attachments: 
MC-2-new-om-leader-ready-only-after-previous-term-applied.patch, 
TestBugMC2NewLeaderStaleRead.java

h3. Mechanism
{{OzoneManagerRatisServer.getLeaderStatus}} returns {{LEADER_AND_READY}} as 
soon as Ratis reports the division as leader ready. Ratis (3.3.1) sets that 
when its state machine updater reaches the leader's startup configuration entry 
({{LeaderStateImpl.checkReady}}), and the updater counts an entry as applied 
once {{applyTransaction}} has returned its future. 
{{OzoneManagerStateMachine.applyTransaction}} only queues the entry on the 
single OM apply executor ({{CompletableFuture.supplyAsync}}), and 
{{notifyLeaderReady}} does not wait for that queue. So a new leader can be 
ready while entries from the previous term, including writes already 
acknowledged to their clients, are still queued and not in the OM table cache 
or DB.

On {{LEADER_AND_READY}} the default read path 
({{OzoneManagerProtocolServerSideTranslatorPB.submitReadRequestToOmDefault}}) 
and the leader local branches of the other read paths answer from local state. 
Background services that check {{OzoneManager.isLeaderReady}} start in the same 
window (HDDS-16795 is one consequence).

h3. Trigger
Leadership moves to an OM whose apply executor has a backlog, or an OM restarts 
with committed entries that were not flushed before it stopped, and a read 
arrives before the backlog is applied.

# Leader change (run): in a 3 OM ring, a follower's apply executor is held up 
(in the test by holding a bucket lock on that follower). A key create in that 
bucket, then a create of {{key1}} in another bucket, are acknowledged by the 
leader and replicated. Leadership is transferred to the lagging follower. Ratis 
logs "is ready since appliedIndex == startIndex", {{getLeaderStatus}} returns 
{{LEADER_AND_READY}}, and a client {{getKey(key1)}} returns {{KEY_NOT_FOUND}}, 
{{listKeys}} returns nothing. After the queue drains the key is there.
# Single OM restart (run during the analysis, not part of the attached test): 
an OM stopped after acknowledging writes but before the double buffer flushed 
them replays them in a new term after restart, and gives the same result. The 
unflushed stop was simulated by pausing the double buffer before the stop; the 
replay backlog was natural in one run (1500 key commits, Ratis ready at index 
4507 while OM had applied up to 1953) and came from a held bucket lock in the 
other.

The window lasts until the executor has caught up, at most 
{{ozone.om.unflushed.transaction.max.count}} (default 10000) entries behind. In 
the leader change case, a follower that keeps up has no backlog and is not 
affected.

h3. Impact
* A client reading through the default path (and the leader local branches of 
the follower read paths) gets {{KEY_NOT_FOUND}}, an empty listing or other 
stale metadata for objects whose write was already acknowledged. The answer is 
final for the client; nothing retries or corrects it.
* Services gated by {{isLeaderReady}} (key and directory deletion, open key and 
multipart cleanup, lifecycle, Ranger sync, upgrade finalization) can run 
against state that is missing committed entries.
* Linearizable reads served by the new leader through Ratis are affected the 
same way. Ratis 3.3.1 ({{StateMachineUpdater.applyLog}}) passes a configuration 
entry's index to the ReadIndex wait as applied right away, while the futures of 
earlier OM entries can still be pending, and with no new writes the read index 
is the startup index. On unmodified source with 
{{ozone.om.ratis.server.read.option=LINEARIZABLE}}, 
{{ozone.om.allow.leader.skip.linearizable.read=false}} and the leader lease 
off, reads of {{key1}} with {{LINEARIZABLE_LEADER_ONLY}} and with 
{{LINEARIZABLE_ALLOW_FOLLOWER}}, both served by the new leader through Ratis, 
returned {{KEY_NOT_FOUND}} (run during the review of this report).

h3. Reproduction
PASS on unmodified source. [^TestBugMC2NewLeaderStaleRead.java] 
({{hadoop-ozone/integration-test}}) runs a 3 OM mini cluster, holds the bucket 
lock of {{bucket2}} on a follower, writes a key in {{bucket2}} and {{key1}} in 
{{bucket1}} through the client, transfers leadership to that follower with the 
Ratis admin API and reads {{key1}} through the client. It asserts 
{{LEADER_AND_READY}}, {{KEY_NOT_FOUND}} and an empty listing, then that 
{{key1}} is readable once the lock is released. A passing test means the defect 
is present. The held lock stands in for a slow apply; the restart variant was 
shown earlier with a natural replay backlog.

h3. Patch
[^MC-2-new-om-leader-ready-only-after-previous-term-applied.patch], against 
424482eb9cfc4b835e1bf73bcdaa9e7d70df59f0. It also applies to master at 
fef9cde752a.

{{getLeaderStatus}} now also requires that the term of the OM state machine's 
last applied index equals the current term. OM advances that index after the 
double buffer has flushed an entry, and moves it onto a following Ratis 
configuration or metadata entry once everything before it is flushed. Every 
term starts with the leader's startup configuration entry, so the index is in 
the current term exactly when everything committed in earlier terms is applied 
and flushed. A leader with no backlog is not held back: the index moves onto 
the startup entry right after Ratis applies it. With a backlog the leader 
reports {{LEADER_AND_NOT_READY}} and clients retry. Writes are rejected during 
the backlog too, so clients retry after 
{{ozone.client.wait.between.retries.millis}} instead of queueing behind it. 
That also keeps {{createClientRequest}} and {{preExecute}} from running against 
the same stale local state, where a write could fail with {{BUCKET_NOT_FOUND}} 
for a bucket created just before the leader change, or be built for the wrong 
bucket layout (argued from code, not run). No new state or config.

On the new leader this closes the no hint read path, {{DEFAULT}} and 
{{LINEARIZABLE_LEADER_ONLY}}, which now reject with 
{{OMLeaderNotReadyException}} until the backlog is applied (with the patch, a 
{{LINEARIZABLE_LEADER_ONLY}} read kept retrying while the backlog was held, run 
during the review). It does not close {{LINEARIZABLE_ALLOW_FOLLOWER}} landing 
on a not ready leader, which still goes to the Ratis ReadIndex wait described 
above (argued from code), {{LOCAL_LEASE}} reads on a not ready leader, covered 
by HDDS-17550, or linearizable reads on a follower, whose own ReadIndex wait 
passes configuration entries in the same way (argued from code). Rejecting 
{{LEADER_AND_NOT_READY}} in the {{LINEARIZABLE_ALLOW_FOLLOWER}} branch, as the 
{{LINEARIZABLE_LEADER_ONLY}} branch already does, is a possible follow up.

Related: this is not a duplicate of HDDS-16795, which reports double purges and 
quota drift in {{KeyDeletingService}} with the same root cause. 
{{KeyDeletingService}} and {{DirectoryDeletingService}} check {{isLeaderReady}} 
before both the active object store and the snapshot branches, so this patch 
also closes the window of HDDS-16795 for runs that start after the leader 
change or restart, including the snapshot path, and the separate check proposed 
there is not needed if this change lands. HDDS-16794 (a run already in progress 
across the change) is not covered. RATIS-2313 asked Ratis to wait for pending 
state machine futures before marking the leader ready and was resolved Not A 
Bug: a state machine that needs this should wait itself, so the check belongs 
in Ozone. HDDS-13357 records the same assumption for 
{{resetInFlightSnapshotCount}}. Before Ozone 1.4.0, Ratis 2.x readiness 
required the state machine's own applied index ({{LeaderStateImpl.isReady}}); 
the window opened with the move to Ratis 3 (HDDS-9192), and this patch restores 
that meaning in OM. HDDS-10546 made OM move that index onto the following 
configuration entries once earlier entries are flushed, which this check relies 
on.

Covered by {{testNewLeaderNotReadyUntilPreviousTermApplied}} in the existing 
{{TestOzoneManagerHAWithStoppedNodes}}: the same leader change with a held 
lock, asserting {{LEADER_AND_NOT_READY}} while Ratis already reports the 
division ready, and that the key is readable once the backlog drains. Without 
the change to {{OzoneManagerRatisServer}} it fails with "expected: 
LEADER_AND_NOT_READY but was: LEADER_AND_READY". With it, 
{{TestOzoneManagerHAWithStoppedNodes}} and {{TestOzoneManagerHAWithAllRunning}} 
(44 tests), the OM state machine, Ratis server, double buffer, background 
service and key manager unit suites (566 tests), 
{{TestTransferLeadershipShell}}, {{TestAddRemoveOzoneManager}}, 
{{TestOzoneManagerHAFollowerReadWithAllRunning}}, {{TestOzoneManagerRestart}} 
and {{TestOMUpgradeFinalization}} (34 tests) pass, checkstyle is clean. With 
the patch the reproduction fails at its {{LEADER_AND_READY}} assertion, because 
the new leader reports {{LEADER_AND_NOT_READY}} until the backlog is applied. 
The single OM restart variant was not rerun with the patch.

Found by TLA+ model checking and code review of the OM and SCM Ratis state 
machine integration (leader readiness, snapshot install and log failure 
handling) under HDDS-15926, on commit 424482eb9cfc4b835e1bf73bcdaa9e7d70df59f0. 
Checked against HDDS issues and apache/ozone pull requests for duplicates 
before filing. 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