Siyao Meng created HDDS-16893:
---------------------------------

             Summary: SCM can grant a datanode's container slot again after the 
ICR of a new container, until the datanode's next node report
                 Key: HDDS-16893
                 URL: https://issues.apache.org/jira/browse/HDDS-16893
             Project: Apache Ozone
          Issue Type: Bug
            Reporter: Siyao Meng
         Attachments: MC-1-send-node-report-with-new-container-icr.patch, 
TestBugMC1IcrBeforeNodeReport.java

h3. Mechanism
SCM counts free container slots on a datanode from two inputs 
({{PendingContainerTracker}}, {{TwoWindowBucket.checkSpaceAndAdd}}): the usable 
space in the last storage report it processed, and the containers it has 
allocated on that datanode but not yet seen reported. It removes the pending 
entry of a new container when it processes the container's ICR 
({{IncrementalContainerReportHandler.processICR}}) or first sees it in a full 
container report ({{ContainerReportHandler}}).

The datanode commits the container's space on its volume when it creates the 
container and sends the ICR at once, in an immediately triggered heartbeat. 
That heartbeat carries no fresh node report. The node report is rebuilt only by 
{{NodeReportPublisher}} every {{hdds.node.report.interval}} (60s by default) 
and stored with {{StateContext.refreshFullReport}}. Until the next one, SCM 
counts the new container neither as pending nor in the reported committed 
space, so it can grant the same slot again.

The question was raised in review of HDDS-14921 (apache/ozone#10000: "Please 
check if node report is also send in ICR") and not followed up.

h3. Trigger
Default configuration, no faults. A datanode creates a container on the first 
write after SCM allocated it, so the pending entry normally lasts only 
milliseconds to seconds. For up to one node report interval afterwards, every 
allocation on that datanode is checked against space that does not include the 
new container. This happens for every new container. It becomes visible when 
the datanode is close enough to full that the extra grants exceed its remaining 
slots.

h3. Impact
* SCM allocates containers on datanodes that have no room for them, the over 
allocation the tracker was added to prevent (HDDS-14921, HDDS-15024).
* The datanode then refuses to create such a container ({{DISK_OUT_OF_SPACE}} 
from the volume choice in {{KeyValueContainer.create}}).
* On a Ratis pipeline the create runs while the first chunk is written 
({{ContainerStateMachine.writeStateMachineData}}, then the implicit create in 
{{HddsDispatcher.dispatchRequest}}). {{DISK_OUT_OF_SPACE}} is not one of the 
results {{ContainerStateMachine.handleCommandResult}} tolerates, so the state 
machine is marked unhealthy, the Ratis log failure reaches 
{{XceiverServerRatis.handleNodeLogFailure}} and the pipeline is closed. All its 
open containers close, and in flight writes of other clients on that pipeline 
fail and are retried. For EC the write that triggers the create fails and the 
client retries elsewhere. Read from code, not run end to end.
* Since HDDS-14925, the datanode accepts creates down to a lower hard limit 
than the spare it reports to SCM. With default settings the gap is 0 on volumes 
up to 1TB, 5GB (one default container) at 1.25TB and 80GB on 16TB, so on large 
volumes a few extra grants still succeed inside that band.

h3. Reproduction
[^TestBugMC1IcrBeforeNodeReport.java] ({{hadoop-hdds/server-scm}}) uses real 
SCM components ({{SCMNodeManager}}, {{PipelineManagerImpl}}, 
{{ContainerManagerImpl.allocateContainer}}, {{SCMDatanodeHeartbeatDispatcher}}, 
{{NodeReportHandler}}, {{IncrementalContainerReportHandler}}). Three datanodes 
each have one real {{HddsVolume}} sized for one container slot, with their 
storage reports built by {{StorageLocationReport}}. It runs deterministically 
on unmodified source, with the default node report interval and a 1GB container 
size to keep the volumes small.
* {{icrWithoutNodeReportGrantsSlotTwice}} plays the current datanode: create, 
then a heartbeat with only the ICR. A second allocation while the first is 
pending is refused. After the ICR it succeeds on the same full pipeline, and 
the datanode's volume choice for that container throws 
{{DiskOutOfSpaceException}}. The test asserts the correct behaviour and fails: 
{{[second container granted on datanodes with room for one] expected: null but 
was: ContainerInfo\{id=#3, state=OPEN, ...\}}}.
* {{icrWithNodeReportRefusesSecondSlot}} sends the node report in the same 
heartbeat as the ICR and passes: SCM refuses the second allocation.

h3. Patch
[^MC-1-send-node-report-with-new-container-icr.patch], against 
b0aa6475b7856d2c7f467dd3db12b9a1dd116f63. It also applies to master at 
6818707698c.

When the datanode's ICR sender ({{OzoneContainer}}) sends the ICR of an OPEN 
replica, which only happens after a container create, it first refreshes the 
node report with {{getNodeReport()}}. The heartbeat that carries the ICR then 
normally also carries a node report that includes the committed space, so SCM 
sees that space no later than, or shortly after, it releases the pending entry 
on the ICR. SCM handles {{NODE_REPORT}} and ICRs on different executors, so 
when the node report is processed first the container is briefly counted twice, 
which only delays allocations. SCM code is unchanged.

Cost: on the first write of each new container, every replica builds a node 
report, which walks the datanode's whole container set 
({{ContainerSet.getOpenContainerCountsByVolume}}) and takes the volume set read 
lock ({{MutableVolumeSet.getStorageReport}}), inside the ICR sender's 
{{synchronized (containerSet)}}. That is linear in the number of containers on 
the datanode, once per create. The extra report goes in the heartbeat the ICR 
already triggers, so there is no extra RPC; SCM and Recon process one more node 
report per create.

Remaining windows: the heartbeat thread reads the full reports and the 
incremental reports separately 
({{StateContext.getAllAvailableReportsUpToLimit}}), so the ICR can occasionally 
go out one heartbeat before the node report; a periodic {{NodeReportPublisher}} 
run that built its report before the create can overwrite the fresh one; SCM 
may process the ICR before the node report of the same heartbeat; and a 
heartbeat that fails before SCM processes it is resent with the ICR but without 
the node report. The same gap for replication imports 
({{SCMNodeManager.opCompleted}} on the import ICR) is not addressed here.

An SCM side alternative would keep the pending entry after the ICR until SCM 
has processed a node report from a later heartbeat than the one that carried 
the ICR. That is nearly sound and would also cover older datanodes, replication 
imports and the resend after a failed heartbeat. It needs more per datanode 
state, the heartbeat order carried to the separate report executors, and it 
counts each new container twice for up to one node report interval. The 
datanode change is the smaller first step; the two do not exclude each other.

Covered by {{testNewContainerReportSentWithNodeReport}} in the existing 
{{TestOzoneContainer}}. It checks that the node report reaches {{StateContext}} 
before the ICR and contains the container's committed bytes, and that later 
ICRs of the container do not refresh it. Without the change it fails for all 8 
schema and layout cases. With it, the test and the container-service suites 
around the report path ({{TestHddsDispatcher}}, {{TestStateContext}}, 
{{TestReportPublisher}}, {{TestHeartbeatEndpointTask}}, 
{{TestKeyValueHandler}}, {{TestKeyValueHandlerWithUnhealthyContainer}}, 
{{TestContainerPersistence}}, {{TestCloseContainerCommandHandler}}, 
{{TestDeleteContainerCommandHandler}}) pass. Checkstyle is clean. Two tests 
that bind the default port 9886 could not run on the test machine, with or 
without the change.

Found by TLA+ model checking and code review of the SCM pending container 
allocation tracking 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