[ 
https://issues.apache.org/jira/browse/HDDS-17268?page=com.atlassian.jira.plugin.system.issuetabpanels:all-tabpanel
 ]

Siyao Meng updated HDDS-17268:
------------------------------
    Attachment: TestBugMC4StaleLeaderCommandBeforeTermInit.java

> Datanode ignores the heartbeat SCM term before term init and updates it 
> without the lock, so a stale SCM leader's command can run
> ---------------------------------------------------------------------------------------------------------------------------------
>
>                 Key: HDDS-17268
>                 URL: https://issues.apache.org/jira/browse/HDDS-17268
>             Project: Apache Ozone
>          Issue Type: Bug
>    Affects Versions: 1.4.0, 2.0.0, 1.4.1, 2.1.0, 2.2.0, 2.1.1, 2.1.2, 2.2.1
>            Reporter: Siyao Meng
>            Priority: Major
>         Attachments: 
> MC-4-use-heartbeat-term-before-term-init-and-lock-term-update.patch, 
> TestBugMC4StaleLeaderCommandBeforeTermInit.java, 
> TestBugMC5HeartbeatTermRegression.java
>
>
> h3. Mechanism
> The datanode fences commands of a stale SCM leader with 
> {{StateContext.termOfLeaderSCM}} (HDDS-3988): {{getNextCommand}} drops a 
> command whose term is lower than the latest term seen. HDDS-7621 added the 
> term to heartbeat responses so that the datanode also follows a new leader 
> that sends no commands. Two defects in 
> {{StateContext.updateTermOfLeaderSCM(long)}}, the method that applies the 
> heartbeat term, weaken that fence.
> * *Heartbeat term ignored before init.* The method returns without doing 
> anything while {{termOfLeaderSCM}} is empty, which it is after every datanode 
> start. {{initTermOfLeaderSCM}} later sets the term from the queued commands 
> only. If the first commands come from the old leader, the datanode settles on 
> the old term and runs them, although it had already received the new term in 
> heartbeat responses. The javadoc of {{initTermOfLeaderSCM}} says the datanode 
> must "detect latest leader SCM before handling any SCMCommand, so that it 
> won't be disturbed by stale leader SCM".
> * *Unlocked check then set.* {{HeartbeatEndpointTask.processResponse}} calls 
> the method without {{StateContext.lock}}, while {{addCommand}} and 
> {{getNextCommand}} call it under the lock. Each SCM endpoint has its own 
> heartbeat thread. Two responses with terms 2 and 3 processed at the same time 
> can leave the term at 2 after 3 was applied, and {{getNextCommand}} then 
> returns a queued term 2 command instead of dropping it.
> An old leader does deliver commands of its term. It stamps the term when it 
> creates a command and can create them until Ratis notifies it that it lost 
> leadership. {{CommandQueue}} is not cleared on step down, and 
> {{SCMNodeManager.processHeartbeat}} hands queued commands out on followers 
> too, without a term in the response.
> h3. Trigger
> Heartbeat term ignored before init: a datanode starts (or restarts) while SCM 
> leadership has moved from SCM A (term 1) to SCM B (term 2), and A still has 
> commands of term 1 queued for that datanode, or A has not yet stepped down. B 
> answers the datanode's heartbeats with term 2 but has no command for it yet. 
> A's command is then the first in the datanode's queue.
> Unlocked update: two SCMs answer the same datanode as leader at the same time 
> (an old leader at term 2 that has not stepped down yet, the new leader at 
> term 3), the two responses are processed concurrently and the term 2 write 
> lands between the read and the write of the term 3 update.
> h3. Impact
> * A command decided by the old leader runs on the datanode after the new 
> leader took over, for example a force delete of a container replica. In the 
> reproduction a force delete of term 1 deleted the replica after the datanode 
> had received five heartbeat responses with term 2. The new leader did not 
> decide that delete and does not know about it: when an SCM becomes leader, 
> {{ReplicationManager.notifyStatusChanged}} clears the pending replication and 
> delete ops, with the comment "They are no longer needed as the DN will 
> discard any commands when the term changes". Its own over replication 
> handling can therefore delete another replica of the same container, which is 
> the double delete of HDDS-16449, there described as masked by this datanode 
> term gate. Shown for one replica, multiple replicas and the resulting 
> redundancy were not exercised. Re-replication restores redundancy afterwards.
> * The damage is bounded in time. ReplicationManager commands carry a datanode 
> deadline ({{hdds.scm.replication.event.timeout}} 12m minus 
> {{hdds.scm.replication.event.timeout.datanode.offset}} 6m) after which 
> {{DeleteContainerCommandHandler}} and {{ReplicationSupervisor}} drop them, so 
> a stale ReplicationManager delete runs only within about 6 minutes of its 
> creation, and the new leader's ReplicationManager waits 
> {{hdds.scm.wait.time.after.safemode.exit}} (5m) after it becomes ready before 
> it acts. The reproduction sends a command without a deadline.
> * With the race, the term goes back from 3 to 2 until the next heartbeat of 
> the new leader (default interval 30s), and term 2 commands queued in that 
> time are dispatched. The race needs two SCMs answering as leader at once, so 
> it is rare.
> * A datanode that already has its term is not affected by the first defect; 
> it drops the same command ("Detect and drop a SCMCommand ... from stale 
> leader SCM").
> h3. Reproduction
> Both attached classes pass on unmodified source when the bug is present 
> (target path and Maven command in each class comment). Not run with real SCMs 
> or a real SCM failover.
> * [^TestBugMC4StaleLeaderCommandBeforeTermInit.java] 
> ({{hadoop-hdds/container-service}}): a real {{DatanodeStateMachine}} daemon 
> over Hadoop RPC against three {{ScmTestMock}} SCMs that put their term into 
> the heartbeat response only while they believe they lead, like 
> {{SCMDatanodeProtocolServer.sendHeartbeat}}. Two cases: the old leader still 
> believes it leads, or it is a follower that still holds the command. In both, 
> the term 1 force delete removes the container (about 15s).
> * [^TestBugMC5HeartbeatTermRegression.java] 
> ({{hadoop-hdds/container-service}}): two real {{HeartbeatEndpointTask}}s on 
> their own threads against mocked SCM proxies (term 2 with a close container 
> command, term 3 without). A Mockito spy on the {{StateContext}} holds the 
> term 2 write between its check and its write until the term 3 update has 
> finished, which makes the race deterministic. The term ends at 2 and 
> {{getNextCommand}} returns the term 2 command.
> h3. Patch
> [^MC-4-use-heartbeat-term-before-term-init-and-lock-term-update.patch], 
> against b10a7d427bdf83807a7e48a1be507d3b02d5a503. It also applies to master 
> at e0de543cbe9.
> While the term is not initialized, {{updateTermOfLeaderSCM(long)}} records 
> the highest term it is given, and {{initTermOfLeaderSCM}} uses the larger of 
> that and the highest queued command term. The method takes 
> {{StateContext.lock}}, the lock its other two callers already hold, which 
> also keeps recording and init atomic. {{termOfLeaderSCM}} becomes volatile 
> for the command handlers that read it from their own threads. Nothing changes 
> once the term is set, and init still happens at the same moment.
> This narrows the window but does not close it: if the datanode initializes 
> its term before it has processed a heartbeat response of the new leader (init 
> only needs a majority of SCMs in HEARTBEAT state), a command of the old term 
> is still accepted. Waiting for a leader heartbeat term before init would 
> close it, but would stop command processing when no SCM sends a term (an SCM 
> older than HDDS-7621, or no leader), so it is left out. Clearing 
> {{CommandQueue}} when an SCM steps down would remove the follower route on 
> the SCM side.
> Covered by {{dropsCommandOfStaleLeaderIfNewerTermWasSeenBeforeInit}}, 
> {{initsTermFromCommandIfOlderTermWasSeenBeforeInit}} (negative case) and 
> {{concurrentTermUpdatesKeepHighestTerm}} in the existing 
> {{TestStateContext}}. Without the change the first fails with the term 1 
> command returned and the third with term 2 instead of 3. With it 
> {{TestStateContext}}, {{TestHeartbeatEndpointTask}}, 
> {{TestHeartbeatEndpointTaskDnsRefresh}}, 
> {{TestDeleteContainerCommandHandler}}, {{TestReplicationSupervisor}}, 
> {{TestRunningDatanodeState}} and {{TestCloseContainerCommandHandler}} pass 
> (113 tests), checkstyle is clean, and both attached reproductions fail 
> because the stale command is dropped.
> Found by TLA+ model checking and code review of the datanode registration and 
> heartbeat paths under HDDS-15926, on commit 
> b10a7d427bdf83807a7e48a1be507d3b02d5a503. Checked against HDDS issues and 
> apache/ozone pull requests for duplicates before filing. Related but not 
> duplicates: HDDS-3988 (introduced the term fencing), HDDS-7621 (added the 
> heartbeat term and the early return), HDDS-5330 (stale commands in sub queues 
> after the term is set), HDDS-16449 (redundant delete after an SCM leadership 
> transfer, masked by the datanode term gate) and HDDS-16804 
> ({{ReplicationSupervisor}} loses a command of the new leader behind an equal 
> task of the old leader). The attached patch is a proposal for review. 
> Generated with Specula (Claude Opus 5.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