Siyao Meng created HDDS-17016:
---------------------------------

             Summary: Datanode with more failed volumes than tolerated keeps 
serving after restart, then stops on a later volume check that finds nothing 
wrong
                 Key: HDDS-17016
                 URL: https://issues.apache.org/jira/browse/HDDS-17016
             Project: Apache Ozone
          Issue Type: Bug
            Reporter: Siyao Meng
         Attachments: 
MC-12-enforce-volume-failure-tolerance-on-every-check.patch, 
TestBugMC12ToleranceAtStartup.java

h3. Mechanism
{{MutableVolumeSet.hasEnoughVolumes}} is only called from 
{{handleVolumeFailures}}, and {{checkAllVolumes}} calls that only when the 
check itself finds a new failure. Volumes that are failed directly through 
{{failVolume}} are never compared with 
{{hdds.datanode.failed.data.volumes.tolerated}} (or its metadata and DB 
variants) at that point. At startup these are volumes rejected by 
{{initializeVolumeSet}} (inconsistent or unloadable root), by 
{{VersionEndpointTask.checkVolumeSet}} and by {{ContainerReader}}; at runtime 
{{ScmHAFinalizeUpgradeActionDatanode}} does the same. The startup 
{{checkAllVolumes}} in {{OzoneContainer.start}} then finds no new failure, so 
the datanode starts and serves although it is over the limit.

The callback of {{checkVolumeAsync}} calls {{handleVolumeFailures}} even when 
the check found nothing. The next on demand check of any healthy volume 
(started by an I/O error on a container) therefore finds the set over the limit 
and calls {{handleFatalVolumeFailures}}, stopping the datanode at an arbitrary 
later time.

This dates from the introduction of the tolerance: the startup exception that 
HDDS-9022 replaced with {{handleFatalVolumeFailures}} was also only thrown from 
{{handleVolumeFailures}}.

h3. Trigger
A finite tolerance and a restart with more unusable volumes than tolerated, for 
example volumes whose {{VERSION}} file is missing or corrupt. With the default 
{{-1}} only one corner case is affected: {{ContainerReader}} at startup or 
{{ScmHAFinalizeUpgradeActionDatanode}} at runtime fails every data volume 
directly. The datanode then keeps running with no data volume, because no 
healthy volume is left to start an on demand check.

h3. Impact
* The datanode serves with more failed volumes than the operator allowed, until 
any later failure handling runs.
* It is then stopped by a check that found nothing wrong, for example the on 
demand check of a healthy volume.
* Shown at unit level with a real volume set and checker and a mocked 
{{DatanodeStateMachine}}; not exercised on a datanode registered with SCM.

h3. Reproduction
PASS on unmodified source. [^TestBugMC12ToleranceAtStartup.java] 
({{hadoop-hdds/container-service}}, target path and Maven command in the class 
comment) sets the data volume tolerance to 1, formats three volumes through 
{{StorageVolumeUtil.checkVolume}}, removes the {{VERSION}} file of two, 
rebuilds the volume set with a real {{StorageVolumeChecker}} and runs 
{{checkAllVolumes()}} as {{OzoneContainer.start}} does. Output: {{healthy=1 
failed=2 tolerated=1}}, and the startup check makes no fatal call. 
{{StorageVolumeUtil.onFailure}} on the healthy volume then logs "Not enough 
volumes ... MaxVolumeFailuresTolerated: 1, ActiveVolumes: 1, FailedVolumes: 2" 
and calls {{handleFatalVolumeFailures}} while the checked volume is still 
healthy. A passing test means the defect is present.

h3. Patch
[^MC-12-enforce-volume-failure-tolerance-on-every-check.patch], against 
cce1ca28944886218132f01c0e271c1fd0700078. It also applies to master at 
2536fe9cbc3.

{{MutableVolumeSet.checkAllVolumes}} now also calls 
{{handleFatalVolumeFailures}} when the check found no new failure but 
{{hasEnoughVolumes()}} is false. {{OzoneContainer.start}} runs it on the data, 
metadata and DB volume sets after all startup paths that fail volumes directly, 
so an over limit datanode stops at startup, and the periodic check covers 
direct failures at runtime. Stopping at startup is the intended behaviour: 
HDDS-5219 threw "Don't have enough good volumes on startup", and 
{{TestDatanodeHddsVolumeFailureToleration.testDNCorrectlyHandlesVolumeFailureOnStartup}}
 expects a restart with two bad volumes and tolerance 1 to exit. Both only work 
when the startup check itself finds the failures; the patch applies the same 
rule to volumes that failed before that check. Calling {{handleVolumeFailures}} 
unconditionally was not chosen because it would also run the failed volume 
listener on every periodic check.

Covered by {{testVolumesFailedOnLoadCountTowardsTolerance}} in the existing 
{{TestVolumeSetDiskChecks}}: three volumes, two of them failed by 
{{initializeVolumeSet}}, a checker that reports no failure. With tolerance 1 it 
expects one fatal call and without the change fails with "Wanted but not 
invoked: datanodeStateMachine.handleFatalVolumeFailures()"; with tolerance 2 
and with the default {{-1}} it expects none and passes either way. With the 
change the volume, container, reader, upgrade, handler and state machine suites 
(309 tests) and {{TestDatanodeHddsVolumeFailureToleration}} pass, and 
checkstyle is clean. With the patch the reproduction fails at its check that 
startup made no fatal call.

*Upgrade note.* A datanode with a finite 
{{hdds.datanode.failed.data.volumes.tolerated}} (or the metadata or DB variant) 
that has more unusable volumes than tolerated now exits at startup with 
"DatanodeStateMachine Shutdown due to too many bad volumes", instead of serving 
until a later volume check stops it. Such a datanode will not come back after 
the upgrade restart, so a rolling upgrade stalls at it. Before upgrading, 
compare {{NumFailedVolumes}} of {{VolumeHealthMetrics-DATA_VOLUME}} (and 
{{VolumeHealthMetrics-META_VOLUME}}, {{VolumeHealthMetrics-DB_VOLUME}}) with 
the tolerance on each such datanode, then repair or remove the failed 
directories or raise the tolerance. With the default {{-1}} a datanode now also 
exits when every data volume has been failed directly, instead of running with 
none.

Found by TLA+ model checking and code review of the datanode volume and 
per-disk RocksDB lifecycle paths under HDDS-15926, on commit 
cce1ca28944886218132f01c0e271c1fd0700078. Checked against HDDS issues and 
apache/ozone pull requests for duplicates before filing. Related but not 
duplicates: HDDS-5219 (introduced the tolerance), HDDS-9022 (replaced the 
startup exception with the fatal handler), HDDS-14871 (volume check latch 
timeout) and HDDS-11770 (proposed default 0, abandoned). 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