Siyao Meng created HDDS-16895:
---------------------------------

             Summary: SCM can leave safe mode below the container threshold 
when a container report is processed during the container rule refresh
                 Key: HDDS-16895
                 URL: https://issues.apache.org/jira/browse/HDDS-16895
             Project: Apache Ozone
          Issue Type: Bug
            Reporter: Siyao Meng
         Attachments: MC-1-count-only-containers-removed-by-deleted-scan.patch, 
TestBugMC1RefreshSubtractsReportedContainers.java

h3. Mechanism
{{AbstractContainerSafeModeRule.reinitializeRule}} removes DELETED containers 
from the rule's container map and then lowers {{totalContainers}} by the 
difference between the map size before and after the scan. The refresh holds 
the rule's lock, but report processing does not: {{SafeModeExitRule.onMessage}} 
calls {{process()}} between two {{validate()}} calls, and 
{{handleReportedContainer}} removes each reported container from the same map 
and increments {{containersWithMinReplicas}}. Every container that a report 
removes while the refresh is between its two size reads is therefore counted as 
reported and also subtracted from the total. The ratio can exceed 1.0, the rule 
validates, and once the other rules pass SCM leaves safe mode. The exit is not 
reversed.

{{ECContainerSafeModeRule.handleReportedContainer}} has a second, smaller gap 
of the same kind: it increments the count without checking that its own 
{{remove()}} removed the container, so an EC container removed by the refresh 
between {{containsKey}} and {{remove}} is still counted (code reading, not run).

h3. Trigger
# SCM starts in safe mode. The {{safemode-refresh-thread}} calls 
{{refresh(false)}} on every rule every 
{{hdds.scm.safemode.rule.refresh.interval}} (default 5s) while the rule is not 
met.
# A refresh starts while a datanode's registration container report is being 
processed.

No fault and no operator action is needed. {{refresh()}} and {{validate()}} are 
synchronized on the rule, and {{onMessage}} calls {{validate()}} before 
{{process()}}, so a report that arrives during a refresh waits for it. Only the 
one report already inside {{process()}} when the refresh starts can overlap, 
and the chance of a hit per registration report is roughly that report's 
processing time divided by the refresh interval. The window itself is the 
DELETED lookup (a copy of the DELETED containers under the container state read 
lock) plus the removal loop: about 50ms per 100,000 DELETED records in a unit 
timing with a mocked {{ContainerManager}}. It decides how much of the report in 
progress fits inside the window.

h3. Impact
* SCM leaves safe mode with fewer containers reported than 
{{hdds.scm.safemode.threshold.pct}} (default 0.99). Each refresh can subtract 
at most the containers that the one report in progress when it starts removes 
during the refresh, because the next report waits for the rule lock in 
{{validate()}}. Successive refreshes add up. At the default 5s interval a hit 
is rare, and the expected shortfall is estimated at a few percent of the 
containers below the threshold (estimated from the code, not measured). A 
single hit on a large early report in a small cluster can cost more, up to that 
report's share of the containers. The other rules, mainly the 90% one replica 
pipeline rule, still need most datanodes registered before SCM can exit.
* On exit, block allocation and {{allocateContainer}} stop returning 
{{SAFE_MODE_EXCEPTION}}, and {{getContainerWithPipeline}} stops refusing open 
containers without enough replicas. ReplicationManager, block deletion and the 
pipeline scrubber start after {{hdds.scm.wait.time.after.safemode.exit}} 
(default 5m). The background pipeline creator already runs in safe mode once 
the pre check passes ({{hdds.scm.safemode.pipeline.creation}}, default true), 
so the exit does not change it. Containers whose replicas have still not been 
reported by then are handled as missing or under replicated (argued from the 
code, not run). No data loss is expected.
* The subtraction was added by HDDS-15238 and is in 2.2.0 and 2.2.1. There the 
refresh runs from {{SCMStateMachine.applyTransaction}} while in safe mode 
instead of from the periodic thread, so the same race should be reachable 
(argued, not run on 2.2.x).

h3. Reproduction
[^TestBugMC1RefreshSubtractsReportedContainers.java] 
({{hadoop-hdds/server-scm}}) uses the real {{SCMSafeModeManager}}, its refresh 
thread (interval lowered to 100ms) and the EventQueue handler of 
{{RatisContainerSafeModeRule}}, with a mocked {{ContainerManager}} holding 3 
closed containers. One datanode reports 2 of them. Two latches only order the 
threads: the handler waits just before {{process()}} until the refresh is 
inside the DELETED lookup, and the refresh waits there until the report is 
processed. On the unmodified commit it fails every time with {{SCM left safe 
mode with 2 of 3 containers reported}}, and the log shows {{Refreshed RATIS 
Containers threshold count to 1.}} followed by {{SCM exiting safe mode.}} With 
the patch it passes.

Without any ordering, a stress run (20,000 closed containers, 200,000 DELETED 
containers in the mock, one report of 19,700 containers, i.e. 98.5%, fired at a 
random offset to a 100ms refresh) left safe mode in 6 of 20 trials on the 
unmodified commit and in 0 of 20 with the patch. In the failing trials the 
total fell to as little as 5,132 of 20,000. That size comes from the stress 
settings: a 100ms interval and a DELETED lookup of about 100ms (200,000 
records) against one report of 19,700 containers, so a large part of that 
single report fell inside refresh windows. It is not a typical shortfall. The 
stress harness is not attached. Not run at the default 5s interval or on a real 
cluster.

h3. Patch
[^MC-1-count-only-containers-removed-by-deleted-scan.patch], against 
b0aa6475b7856d2c7f467dd3db12b9a1dd116f63. It also applies to master at 
6818707698c.

{{reinitializeRule}} now subtracts only the containers that its own 
{{containers.remove()}} removed (non null return) instead of the change in map 
size. Each container is then removed exactly once, either by a report (counted, 
kept in the total) or by the DELETED scan (subtracted, not counted). 
{{ECContainerSafeModeRule.handleReportedContainer}} increments only when its 
{{remove()}} returned a value, as the Ratis rule already does. Report 
processing stays outside the rule lock. Making {{process()}} synchronized would 
also close the race, but it would hold the lock for a whole registration report 
and block {{validate()}}, the safe mode status call and the refresh meanwhile.

Covered by {{testRefreshKeepsContainersReportedDuringRefresh}} in the existing 
{{AbstractContainerSafeModeRuleTest}}, which runs for the Ratis and the EC 
rule. A report is processed inside the mocked DELETED lookup, and the test 
expects only the DELETED container to leave the total. Without the change it 
fails for both rules ({{expected: <2> but was: <1>}}). With it, the 
{{org.apache.hadoop.hdds.scm.safemode}} unit tests (76) and 
{{TestSCMSafeModeWithPipelineRules}} on a mini cluster (2) pass, and checkstyle 
is clean.

Found by TLA+ model checking and code review of the SCM safe mode rules 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]

Reply via email to