Siyao Meng created HDDS-17279:
---------------------------------

             Summary: Adding or removing an SCM on a running datanode can make 
report producers throw, losing reports and stopping command status reports 
until restart
                 Key: HDDS-17279
                 URL: https://issues.apache.org/jira/browse/HDDS-17279
             Project: Apache Ozone
          Issue Type: Bug
            Reporter: Siyao Meng
         Attachments: 
MC-7-synchronize-scm-endpoint-add-remove-with-report-fan-out.patch, 
TestBugMC7AddEndpointRace.java

h3. Mechanism
{{StateContext}} keeps one queue per SCM endpoint for incremental reports, 
container actions and pipeline actions, plus a plain {{HashSet}} of endpoints. 
The producers ({{addIncrementalReport}}, {{addContainerAction}}, 
{{addContainerActionIfAbsent}}, {{addPipelineActionIfAbsent}}) iterate that set 
and look up each endpoint's queue. {{StateContext.addEndpoint}} adds the 
endpoint to the set before it creates the queues, and neither {{addEndpoint}} 
nor {{removeEndpoint}} takes the locks the producers hold. A producer running 
at the same time therefore gets {{null}} for the new (or just removed) endpoint 
and throws a {{NullPointerException}}, or throws 
{{ConcurrentModificationException}} from the set iterator. The endpoints after 
it in the iteration miss that report or action. The queue size getters read by 
{{DatanodeQueueMetrics}} iterate the same maps without the locks, so a metrics 
read can also throw {{ConcurrentModificationException}}.

{{ReportPublisher.publishReport}} catches only {{IOException}}, and the 
publishers run with {{scheduleAtFixedRate}}. When the exception is thrown 
inside {{CommandStatusReportPublisher}}, the scheduled task ends and is never 
rescheduled. The command statuses it had just removed from {{cmdStatusMap}} 
reach no SCM, and no command status report is sent to any SCM until the 
datanode restarts. {{refreshFullReport}} iterates the plain {{HashMap}} 
{{isFullReportReadyToBeSent}} that {{addEndpoint}} inserts into, so the full 
report publishers can end the same way (argued from the code, not run).

h3. Trigger
An Ozone administrator adds or removes an SCM with {{ozone admin reconfig}} of 
{{ozone.scm.nodes.<service>}} on a running datanode 
({{HddsDatanodeService.reconfigScmNodes}}, added by HDDS-13890), while another 
thread is fanning out a report or action. Producers include container state 
changes (ICRs), pipeline reports, container and pipeline actions, and the 
periodic command status report (default every 60s). The window is the few 
microseconds {{addEndpoint}} or {{removeEndpoint}} take, so a single 
reconfiguration hits it rarely.

h3. Impact
* The existing SCMs miss the report or action that hit the window (for example 
an ICR).
* If the command status report publisher hits it, command status reports stop 
for the life of the process. They are how SCM learns that block deletion 
commands completed, so SCM gets no more block deletion acknowledgements from 
this datanode until it restarts, and completed command statuses accumulate in 
{{cmdStatusMap}} (argued from the code, not run).
* If a full report publisher hits it, SCM gets no more refreshed node reports 
(capacity and usage) or periodic full container reports, depending on the 
publisher, from this datanode until it restarts (argued from the code, not run).
* A throw inside container request handling (the close container action that 
{{HddsDispatcher}} adds for a write to a full or unhealthy container, or the 
ICR sent when a write creates a container) fails that write and marks the Ratis 
container state machine unhealthy, which closes the pipeline on this datanode. 
The client retries on another pipeline (argued from the code, not run).
* No crash of the datanode process and no data loss.

h3. Reproduction
Deterministic, on unmodified source: [^TestBugMC7AddEndpointRace.java] 
({{hadoop-hdds/container-service}}). It uses the public {{StateContext}} and 
{{CommandStatusReportPublisher}} APIs as production calls them. The window is 
held open by adding an endpoint whose {{hashCode}} parks the reconfiguration 
thread on the first map update after the endpoint entered the set. The report 
interval is lowered to 100 ms. Both tests assert the correct behaviour and fail 
on the unmodified code: {{addIncrementalReport}} throws 
{{NullPointerException}}, and the scheduled {{CommandStatusReportPublisher}} 
task ends after its run. With the patch both pass.

h3. Patch
[^MC-7-synchronize-scm-endpoint-add-remove-with-report-fan-out.patch], against 
b10a7d427bdf83807a7e48a1be507d3b02d5a503. It also applies to master at 
e0de543cbe9.

Each producer now iterates the values of the per endpoint map it writes to, 
under the lock that already guards that map, instead of iterating the separate 
endpoint set. {{addEndpoint}} and {{removeEndpoint}} update those maps under 
the same locks, and {{isFullReportReadyToBeSent}} becomes a 
{{ConcurrentHashMap}}. The queue size getters used by {{DatanodeQueueMetrics}} 
now read those maps under the same locks too. No new lock and no change in 
behaviour when the SCM set does not change. {{DatanodeQueueMetrics}} has the 
same pattern on its own maps (iterated in {{getMetrics}}, written by its 
{{addEndpoint}} and {{removeEndpoint}}) and is not covered by this patch.

Covered by {{testEndpointChangeDuringReportFanOut}} in the existing 
{{TestStateContext}}: the test thread fans out reports and actions and reads 
the queue sizes for 10000 rounds while another thread keeps adding and removing 
an endpoint, then checks that the existing endpoint got every report and 
action. Without the change it failed in 4 of 4 runs with a 
{{ConcurrentModificationException}} (a {{NullPointerException}} is also 
possible); with only the getters left unlocked it failed in 3 of 3 runs in a 
getter; with the patch it passes in about 3s. {{TestStateContext}}, 
{{TestHddsDispatcher}}, {{TestHeartbeatEndpointTask}}, {{TestReportPublisher}}, 
{{TestReportManager}}, {{TestRunningDatanodeState}} and the mini cluster test 
{{TestDatanodeSCMNodesReconfiguration}} pass, checkstyle is clean.

Found by TLA+ model checking and code review of the datanode registration and 
heartbeat paths under HDDS-15926, on commit 
b10a7d427bdf83807a7e48a1be507d3b02d5a503. Checked against HDDS issues and 
apache/ozone pull requests for duplicates before filing. Related but not 
duplicates: HDDS-13890 (added the runtime SCM add and remove) and HDDS-15369 
(datanode shutdown on {{ozone.scm.nodes}} reconfiguration). 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