Siyao Meng created HDDS-17228:
---------------------------------
Summary: 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
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]