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

Siyao Meng updated HDDS-17228:
------------------------------
    Attachment: TestBugCR13VersionFileLostOnFailedWrite.java

> A failed rewrite of a VERSION file publishes the empty temporary file, so 
> SCM, OM or a datanode cannot start again
> ------------------------------------------------------------------------------------------------------------------
>
>                 Key: HDDS-17228
>                 URL: https://issues.apache.org/jira/browse/HDDS-17228
>             Project: Apache Ozone
>          Issue Type: Bug
>            Reporter: Siyao Meng
>            Priority: Major
>         Attachments: CR-13-keep-version-file-on-failed-write.patch, 
> TestBugCR13VersionFileLostOnFailedWrite.java
>
>
> h3. Mechanism
> {{IOUtils.writePropertiesToFile}} opens an {{AtomicFileOutputStream}} as a 
> try with resources resource and calls {{Properties.store}} inside the block. 
> {{Properties.store}} flushes its buffered writer before it returns, so a 
> write that fails (full disk, quota, failing device) throws from inside the 
> block with part of the content, or none of it, already in the temporary file. 
> The implicit {{close()}} then runs anyway, and 
> {{AtomicFileOutputStream.close()}} moves the temporary file over the target. 
> The previous file is replaced by an empty or truncated one, which is the 
> opposite of what the atomic write is there for. 
> {{AtomicFileOutputStream.abort()}}, which deletes the temporary file instead, 
> is never called.
> This is the helper that writes every {{VERSION}} file. 
> {{Storage.persistCurrentState}} and {{Storage.forceInitialize}} call it 
> through {{StorageInfo.writeTo}} on an existing file, so each of these 
> rewrites can destroy it: the datanode layout {{VERSION}} on every datanode 
> start ({{OzoneContainer.initializeContainerServices}}, also 
> {{DatanodeVersionManager}}), OM and Recon certificate renewal 
> ({{OzoneManager.saveNewCertId}}, {{ReconServer.saveNewCertId}}), SCM root CA 
> rotation ({{RootCARotationHandlerImpl}}), SCM and OM layout finalization 
> ({{ScmVersionManager}}, {{OMVersionManager}}), and the SCM conversions that 
> call {{forceInitialize}} on a cluster that already holds data 
> ({{StorageContainerManager.initializeSecurityIfNeeded}} when a non secure 
> cluster is made secure, {{initializeRatis}} when Ratis is enabled). Datanode 
> volume {{VERSION}} files are only created, never rewritten, so they are not 
> affected.
> h3. Trigger
> # A service rewrites its {{VERSION}} file. The most frequent case is routine: 
> every datanode start rewrites the datanode layout {{VERSION}} with the 
> content it has just read, from {{OzoneContainer.start}} on the first SCM 
> handshake. Certificate renewal (OM, Recon), root CA rotation and layout 
> finalization do the same for their own file.
> # The write of that small file fails. A full or quota limited metadata 
> volume, or a device error, is enough.
> # The helper commits the temporary file anyway, so {{VERSION}} is now empty 
> or holds a fragment.
> No client request and no admin command is involved; both the write and the 
> failure are internal.
> h3. Impact
> * The service cannot read its own {{VERSION}} file after this. {{new 
> StorageInfo(type, versionFile)}} throws {{NullPointerException}} from 
> {{NodeType.valueOf(null)}}, so SCM, OM, Recon or the datanode layout storage 
> fails to start until an operator rebuilds the file by hand (run, see 
> Reproduction). A datanode whose metadata volume is full or failing therefore 
> destroys its layout {{VERSION}} on a restart and then cannot start at all, 
> because {{HddsDatanodeService.start}} constructs {{DatanodeStorage}} over the 
> same file.
> * On the certificate paths the service also takes itself down right away: 
> {{OzoneManager.saveNewCertId}} calls {{shutDown}}, 
> {{ReconServer.saveNewCertId}} calls {{terminateRecon}} and 
> {{RootCARotationHandlerImpl}} calls {{scm.shutDown}} when the write throws. 
> The node therefore stops and then cannot come back up, with no warning that 
> the file it was protecting is already gone (code reading).
> * The helper has been in this form since HDDS-12456, so 2.0.0, 2.1.x and 
> 2.2.x are affected. Before that change {{StorageInfo.writeTo}} wrote in 
> place, where a failure before the first byte left the old content intact.
> * No user data is lost; the damage is to service metadata, and the clusterID 
> and certificate serial id in a lost {{VERSION}} file have to be reconstructed 
> by the operator.
> h3. Reproduction
> PASS on an unfixed checkout, deterministic, no mocking framework. 
> [^TestBugCR13VersionFileLostOnFailedWrite.java] ({{hadoop-hdds/framework}}) 
> creates a minimal {{Storage}} for an SCM style {{VERSION}} file, writes it 
> (175 bytes), then rewrites it with a {{Properties}} whose {{store}} fails the 
> way a full disk does. The {{VERSION}} file is 8 bytes (or 0 bytes in the 
> second test) afterwards, no temporary file is left, and constructing the 
> {{Storage}} again throws {{NullPointerException}}, which is the start 
> failure. A passing test means the defect is present. The failure is injected 
> in the test; a real out of space condition was not exercised.
> h3. Patch
> [^CR-13-keep-version-file-on-failed-write.patch], against 
> b0aa6475b7856d2c7f467dd3db12b9a1dd116f63. It also applies to master at 
> 20d95ed37fe.
> The helper now keeps the {{AtomicFileOutputStream}} out of the resource list, 
> writes, and closes it only on success, and calls {{abort()}} on any failure, 
> so the temporary file is deleted and the target keeps its previous content. 
> The success path writes the same bytes, and {{abort()}} swallows I/O errors 
> while deleting the temporary file, so the caller still sees the original 
> failure. Regression tests: 
> {{TestIOUtils#failedPropertiesWriteKeepsPreviousFile}} and 
> {{TestIOUtils#uncheckedWriteFailureKeepsPreviousFile}}, both failing without 
> the change. The tests of the affected callers ({{TestDatanodeVersionFile}}, 
> {{TestHddsVolume}}, {{TestDatanodeVersionManager}}, 
> {{TestScmVersionManager}}, {{TestEndPoint}}, {{TestOMStorage}}, 
> {{TestOMVersionManager}}) and checkstyle pass. One behaviour note for review: 
> after the change a failed certificate id persist leaves the previous serial 
> id in {{VERSION}}, so the service starts again with the previous certificate 
> instead of not starting at all.
> HDDS-17227 fixes the same pattern in {{YamlUtils.dump}}, the helper that 
> writes the YAML files; the two changes touch different files and can go in in 
> either order.
> Found by code review of the SCM datanode decommission and maintenance admin 
> paths, as part of the TLA+ verification effort 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).



--
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