Siyao Meng created HDDS-16889:
---------------------------------
Summary: Admin pipeline activate reopens a CLOSED pipeline, and
deactivate moves a CLOSED or ALLOCATED pipeline to DORMANT
Key: HDDS-16889
URL: https://issues.apache.org/jira/browse/HDDS-16889
Project: Apache Ozone
Issue Type: Bug
Reporter: Siyao Meng
Attachments: MC-4-reject-activate-deactivate-of-closed-pipeline.patch,
TestBugMC4ActivateClosedPipeline.java
h3. Mechanism
{{PipelineManagerImpl.activatePipeline}} and
{{PipelineManagerImpl.deactivatePipeline}}, called by
{{SCMClientProtocolServer}} for {{ozone admin pipeline activate|deactivate}}
after the admin check, take the pipeline manager write lock and call
{{PipelineStateManager.updatePipelineState}} without looking at the current
state. {{PipelineStateMap.updatePipelineState}} has no transition check either,
so any state can become OPEN or DORMANT, and a move to OPEN puts the pipeline
back into the list used for container allocation.
Everywhere else CLOSED is terminal: {{openPipeline}} refuses a CLOSED pipeline
("Closed pipeline can not be opened"), the scrubber only deletes a CLOSED
pipeline, and the Javadoc of the two methods says "Activates a dormant
pipeline" and "Deactivates an active pipeline". By the time a pipeline is
CLOSED its containers have been finalized, and the way back to a writable
pipeline is a new one from the background pipeline creator.
h3. Trigger
# A Ratis THREE or EC pipeline is OPEN and has a container.
# An admin deactivates it (for example while debugging it).
# SCM closes the pipeline on its own: a datanode of it goes stale and
{{StaleNodeHandler}} closes its pipelines (other SCM side closes are
{{DeadNodeHandler}}, an admin {{ozone admin pipeline close}} and the scrubber's
unregistered node rule). The container goes to CLOSING and the pipeline to
CLOSED. It stays listed until the scrubber deletes it after
{{ozone.scm.pipeline.destroy.timeout}} (default 300s).
# Within that window the admin runs {{ozone admin pipeline activate}} to undo
step 2. The CLOSED pipeline becomes OPEN.
Variants: {{deactivate}} on a CLOSED pipeline makes it DORMANT; {{deactivate}}
on an ALLOCATED pipeline makes it DORMANT, after which neither the datanode
report ({{openPipeline}} only opens ALLOCATED pipelines) nor the ALLOCATED
timeout of the scrubber acts on it.
h3. Impact
* A reopened CLOSED pipeline is offered to
{{ContainerManagerImpl.allocateContainer}} again, which creates a new OPEN
container on it (run). The scrubber skips it because it is no longer CLOSED, so
{{removePipeline}} never sends the {{ClosePipelineCommand}} to its datanodes
(run, past the destroy timeout).
* A CLOSED pipeline made DORMANT is skipped by the scrubber and
{{deletePipeline}} refuses it with "is not yet closed" (run).
* How long this lasts depends on who closed the pipeline. When the pipeline was
closed because of a datanode close action, the datanodes resend that action
with every heartbeat until the pipeline is removed from them (HDDS-9735), so
SCM closes it again within one heartbeat interval and the container allocated
in between goes to CLOSING. It stays OPEN, or DORMANT, only when SCM closed the
pipeline itself (stale or dead node, admin close, scrubber), for example an EC
pipeline closed by {{StaleNodeHandler}}; then it lasts until an admin closes
it. These points are argued from code; the reproduction does not model
datanodes.
* Only an admin can trigger it, and no data loss was shown.
h3. Reproduction
PASS, deterministic, unmodified source.
[^TestBugMC4ActivateClosedPipeline.java] ({{hadoop-hdds/server-scm}}) uses a
real {{PipelineManagerImpl}} and {{ContainerManagerImpl}} on RocksDB with a
mock node manager, the single node HA stub and a mock clock, and makes the
calls SCM makes for a stale node close, the admin commands, container
allocation and the scrubber. The three tests show a CLOSED pipeline reopened
and given a new OPEN container and still OPEN after the destroy timeout scrub,
a CLOSED pipeline made DORMANT that the scrubber and {{deletePipeline}} do not
remove, and an ALLOCATED pipeline left DORMANT after a datanode report and the
ALLOCATED timeout scrub. A passing test means the defect is present.
h3. Patch
[^MC-4-reject-activate-deactivate-of-closed-pipeline.patch], against
b0aa6475b7856d2c7f467dd3db12b9a1dd116f63. It also applies to master at
6818707698c.
{{activatePipeline}} throws {{InvalidPipelineStateException}} for a CLOSED
pipeline, like {{openPipeline}}. Activating an ALLOCATED pipeline stays
allowed, because it is used to force open pipelines (the freon
{{SCMThroughputBenchmark}} and {{TestContainerCommandsEC}}).
{{SCMThroughputBenchmark}} activates every listed pipeline, so a CLOSED
pipeline in that list now makes it fail; that is unlikely because its fake
datanodes never report the pipelines, which therefore stay ALLOCATED (not run).
{{deactivatePipeline}} accepts only OPEN (DORMANT stays a no op) and throws for
ALLOCATED and CLOSED. The ALLOCATED rejection is separate from the CLOSED one
and can be dropped for the narrowest patch. The check runs on the leader under
the same write lock as the state update, before the update is submitted, so the
replicated apply path, the Ratis log and the RPC messages are unchanged.
Operators now get an error for these transitions instead of a silent state
change. An unknown pipeline ID now fails with {{PipelineNotFoundException}}, as
for {{pipeline close}}, instead of succeeding silently.
{{TestContainerCommandsEC#testCreateRecoveryContainerAfterDNRestart}} activated
its EC pipeline again after a datanode restart, when {{StaleNodeHandler}} had
already closed it. The write chunk that follows goes straight to the datanode,
which does not check the SCM pipeline state, so the patch drops that activate
call.
Covered by {{testActivateAndDeactivateRejectClosedPipeline}} and
{{testDeactivateRejectsAllocatedPipeline}} (which also asserts the unknown ID
case) in the existing {{TestPipelineManagerImpl}};
{{testDeactivatePipelineShouldFailOnFollower}} now opens its pipeline first so
that it still tests the follower path. Without the change both new tests fail
because nothing is thrown. With it all test classes of the SCM pipeline package
and {{TestSCMClientProtocolServer}} (269 tests) and the whole
{{TestContainerCommandsEC}} class (54 tests) pass, and checkstyle reports no
violations. With the patch the three reproduction tests fail at the admin call
with {{InvalidPipelineStateException}}.
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. 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]