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

Siyao Meng updated HDDS-16890:
------------------------------
    Attachment: MC-2-close-containers-allocated-during-pipeline-close.patch

> SCM closePipeline leaves a container allocated during the close OPEN on the 
> CLOSED pipeline until the pipeline is deleted
> -------------------------------------------------------------------------------------------------------------------------
>
>                 Key: HDDS-16890
>                 URL: https://issues.apache.org/jira/browse/HDDS-16890
>             Project: Apache Ozone
>          Issue Type: Bug
>            Reporter: Siyao Meng
>            Priority: Minor
>         Attachments: 
> MC-2-close-containers-allocated-during-pipeline-close.patch, 
> TestBugMC2ClosePipelineOrphanContainer.java
>
>
> h3. Mechanism
> {{PipelineManagerImpl.closePipeline}} first calls 
> {{closeContainersForPipeline}}, which reads the pipeline's containers and 
> FINALIZEs each OPEN one (FINALIZE moves it to CLOSING and removes it from the 
> pipeline, then a CLOSE_CONTAINER event is fired). Only after that does it 
> take the pipeline manager write lock and move the pipeline to CLOSED. No lock 
> is held between the read and the CLOSED state. Allocation holds only the 
> pipeline manager read lock ({{WritableRatisContainerProvider.getContainer}}, 
> used by {{BlockManagerImpl.allocateBlock}}, and 
> {{ContainerManagerImpl.allocateContainer}}, used by the admin 
> {{allocateContainer}} RPC), and 
> {{PipelineStateMap.addContainerToOpenPipeline}} rejects only CLOSED 
> pipelines. A container allocated in that window is therefore added to the 
> pipeline and never FINALIZEd. It stays OPEN on the CLOSED pipeline, or on a 
> deleted one when the caller deletes right after closing ({{DeadNodeHandler}}, 
> {{closeStalePipelines}}, the datastream port close).
> HDDS-5973 ordered the close as containers first, then pipeline, so that SCM 
> never has an OPEN container on a closed pipeline; its review states the goal 
> as "containers are in closing before pipeline remove/close log in ratis". 
> Concurrent allocation was not discussed there. HDDS-5655 added the read lock, 
> which keeps allocation away from the CLOSED transition itself but not from 
> the window before it.
> h3. Trigger
> Any pipeline close (pipeline action from a datanode, stale or dead node, 
> decommission or maintenance start, admin close, scrubber, full EC pipeline) 
> while an allocation that creates a new container runs on the same pipeline. 
> For Ratis this is ordinary block allocation. A Ratis pipeline in steady state 
> is at its open container limit, and each FINALIZE in the first pass removes a 
> container from the pipeline, so a block allocation that picks this still OPEN 
> pipeline during the close finds it below the limit and creates a new 
> container ({{ContainerManagerImpl.getMatchingContainer}}). The close itself 
> makes allocation create the late container. The window lasts as long as the 
> FINALIZE calls of the first pass. For EC, block allocation adds a container 
> only to a pipeline it has just created and not yet opened 
> ({{WritableECContainerProvider}}). That pipeline is already registered with 
> the node manager, so a stale node, dead node or decommission handler that 
> closes it in that moment leaves the container OPEN on it, and the following 
> {{openPipeline}} fails with "Closed pipeline can not be opened". That is 
> rare; otherwise only the admin {{allocateContainer}} RPC hits the EC route.
> h3. Impact
> * The late container gets no FINALIZE and no close command. Allocation offers 
> only OPEN pipelines, so no later block goes into it.
> * {{OpenContainerHandler}} in {{ReplicationManager}} closes an OPEN container 
> when {{ReplicationManager.hasHealthyPipeline}} is false or a replica is 
> reported in another state. {{hasHealthyPipeline}} only checks that the 
> pipeline exists, so a late container whose replicas are OPEN (or that has 
> none yet) is closed only after the scrubber deletes the CLOSED pipeline. With 
> the default values ({{ozone.scm.pipeline.destroy.timeout}} 66s, 
> {{ozone.scm.pipeline.scrub.interval}} 5m, 
> {{hdds.scm.replication.thread.interval}} 300s) that is up to about 11 minutes 
> after the close. For callers that delete right away it is the next 
> ReplicationManager run. If ReplicationManager or the scrubber is stopped, the 
> container stays OPEN.
> * When the close command finally arrives the Ratis group is gone, so the 
> datanode quasi closes a Ratis container ({{CloseContainerCommandHandler}}, no 
> write channel and no force flag) instead of closing it through Ratis, and 
> ReplicationManager has to resolve the quasi closed replicas later.
> * What the writer sees: {{BlockManagerImpl.newBlock}} reads the pipeline 
> again after the container was chosen, so {{allocateBlock}} can return the 
> CLOSED pipeline with a block in a container SCM still has as OPEN. After a 
> close without delete the Ratis group stays up until the scrubber deletes the 
> pipeline, so writes of that block into the OPEN container go to a live group 
> and succeed. After a close with delete the group is removed, the write fails 
> and the client excludes the pipeline and asks for a new block, as it does for 
> the other containers of that pipeline. No path that acknowledges a write on a 
> removed group was found.
> The ReplicationManager, scrubber, datanode and client steps above are argued 
> from code. The reproduction stops after {{closePipeline}} returns.
> h3. Reproduction
> [^TestBugMC2ClosePipelineOrphanContainer.java] ({{hadoop-hdds/server-scm}}, 
> target path and Maven command in the class comment) runs a real 
> {{PipelineManagerImpl}} and {{ContainerManagerImpl}} on RocksDB with 
> {{MockNodeManager}} and two real threads. A latch in the container manager 
> that the closer uses holds {{closePipeline}} just before it FINALIZEs the 
> last container it read, which only fixes the timing. The test thread then 
> allocates through {{WritableRatisContainerProvider.getContainer}} (Ratis) or 
> {{ContainerManager.allocateContainer}} (EC 3+2, as the admin RPC does). 
> Output on unmodified source, the same for both routes:
> {noformat}
> close race: closePipeline read [#1], pipeline state while it runs = OPEN
> close race: after closePipeline: #1 (read by closePipeline) state = CLOSING
> close race: allocation during close handed out #2 on Pipeline-...
> close race: after closePipeline: pipeline state = CLOSED, containers still in 
> pipeline = [#2], #2 state = OPEN
> close race: OPEN pipelines offered to new allocations = []
> {noformat}
> No CLOSE_CONTAINER event was fired for #2. A passing test means the defect is 
> present.
> h3. Suggested fix
> The patch removes the cause. {{ReplicationManager.hasHealthyPipeline}} could 
> in addition treat a CLOSED pipeline (not a DORMANT one, whose containers are 
> meant to stay OPEN) as unhealthy, so that any OPEN container left on a CLOSED 
> pipeline is closed on the next ReplicationManager run while its Ratis group 
> still exists. With the patch the only remaining way I found to get there is 
> an SCM crash or leader change between the CLOSED transition and the second 
> container pass (argued), so this is not part of the patch.
> h3. Patch
> [^MC-2-close-containers-allocated-during-pipeline-close.patch], against 
> b0aa6475b7856d2c7f467dd3db12b9a1dd116f63. It also applies to master at 
> 6818707698c.
> {{closePipeline}} calls {{closeContainersForPipeline}} a second time after 
> the pipeline is CLOSED. FINALIZE removes a container from its pipeline, so 
> the second call only finds containers added while the first one ran, and 
> normally reads an empty set. {{addContainerToOpenPipeline}} checks for CLOSED 
> under the same state manager lock as the CLOSED transition, so once the 
> pipeline is CLOSED no container can be added and the second call finds every 
> late one. The allocation path is unchanged and takes no new lock, and the 
> HDDS-5973 order is kept for every container that existed when the close 
> started. Holding the write lock from the read to the CLOSED transition would 
> also work but blocks all allocation while the FINALIZE calls go through 
> Ratis; moving the pipeline to a new non OPEN state before the read would 
> change the pipeline state machine. The second call relies on CLOSED being 
> final. At this commit {{activatePipeline}} can move a CLOSED pipeline back to 
> OPEN, which is reported and fixed separately in HDDS-16889.
> Covered by {{testClosePipelineClosesContainerAllocatedDuringClose}} in the 
> existing {{TestPipelineManagerImpl}}, which allocates a container from inside 
> the first FINALIZE and checks that both containers are CLOSING, none is left 
> in the pipeline and each got exactly one CLOSE_CONTAINER event. Without the 
> change it fails with "expected: CLOSING but was: OPEN". With it the SCM 
> pipeline, block, container manager, close container, dead node and client 
> protocol suites pass (230 tests), as do {{TestPipelineClose}} on a mini 
> cluster (5 tests) and the Recon pipeline manager tests (5 tests), and 
> checkstyle is clean. With the patch both tests of the reproduction fail, 
> because the late container is CLOSING.
> Found by TLA+ model checking and code review of the SCM pipeline lifecycle 
> paths under HDDS-15926, on commit b0aa6475b7856d2c7f467dd3db12b9a1dd116f63. 
> Checked against HDDS issues and apache/ozone pull requests for duplicates 
> before filing. Related but not duplicates: HDDS-5973 (resolved, close order 
> for SCM restarts), HDDS-5655 (resolved, read lock around allocation), 
> HDDS-7829 (resolved, rejects adding a container to a CLOSED pipeline), 
> HDDS-10231 (resolved, ReplicationManager closes OPEN containers whose 
> pipeline is missing) and HDDS-11521 (Not A Problem, client wait on pipeline 
> creation). 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