Siyao Meng created HDDS-17330:
---------------------------------

             Summary: ReplicationSupervisorMetrics can throw 
ConcurrentModificationException when the first task of a new type is added 
during a metrics snapshot
                 Key: HDDS-17330
                 URL: https://issues.apache.org/jira/browse/HDDS-17330
             Project: Apache Ozone
          Issue Type: Bug
            Reporter: Siyao Meng
         Attachments: CR-2-concurrent-replication-supervisor-metrics-map.patch, 
TestBugCR2MetricsMapFirstTaskOfType.java

h3. Mechanism
{{ReplicationSupervisor}} keeps the metric names of the task types it has seen 
in {{METRICS_MAP}}, a static plain {{HashMap}}. 
{{ReplicationSupervisor.initCounters}} puts a new entry when the first task of 
a type is added, under {{synchronized (this)}}. 
{{ReplicationSupervisor.getMetricsMap}} returns an unmodifiable view of the 
same map, and {{ReplicationSupervisorMetrics.getMetrics}} iterates it with 
{{forEach}} without any lock. A {{put}} during that loop makes 
{{HashMap.forEach}} throw {{ConcurrentModificationException}} after the loop, 
so the gauges added after it ({{numInflight*}}) are missing from the record.

h3. Trigger
# A datanode has already run a task of one type (container replication, EC 
reconstruction or container reconciliation), so the map is not empty.
# The first task of another type is added (for example the first reconcile 
command) while a metrics snapshot is running: the sampling timer when a sink 
such as the Prometheus endpoint is registered, a JMX read, or 
{{publishMetricsNow}}.

There are three task types, so this can happen at most twice per datanode 
process, and only if the {{put}} falls inside the few microseconds the snapshot 
spends in the loop. No fault is needed, but it is very unlikely on a real 
cluster.

h3. Impact
Minor. Hadoop's {{MetricsSourceAdapter.getMetrics}} catches the exception and 
logs {{Error getting metrics from source ReplicationSupervisorMetrics}} with 
the stack trace at ERROR. The partial record is still published: the per type 
gauges are in it, the {{numInflight*}} gauges are missing for that one sample. 
The next snapshot is complete. No task, counter or replication is affected. 
(The adapter behaviour is from reading hadoop-common 3.4.3; my reproduction 
calls {{getMetrics}} directly.) In {{MiniOzoneCluster}}, where several 
datanodes share the static map under different locks, the first two inserts can 
also race and lose one entry, so that type's gauges are not published in that 
JVM; this is argued from the code, not run.

h3. Reproduction
PASS on unmodified source, deterministic. 
[^TestBugCR2MetricsMapFirstTaskOfType.java] ({{hadoop-hdds/container-service}}) 
uses only real task types: it adds a {{ReplicationTask}}, then calls 
{{ReplicationSupervisorMetrics.getMetrics}} with a mock record builder that 
adds the first {{ReconcileContainerTask}} while the replication gauges are 
being added, that is inside the {{forEach}}. {{getMetrics}} throws 
{{ConcurrentModificationException}} and no {{numInflight*}} gauge is published. 
The overlap is forced on the snapshot thread instead of a command handler 
thread; the map sees the same {{put}} inside the same loop. The test needs its 
own JVM because the map is static. A passing test means the defect is present. 
A concurrent run with real timing was not attempted, since there are at most 
two windows per process.

h3. Patch
[^CR-2-concurrent-replication-supervisor-metrics-map.patch], against 
07512078fe639f387acd52104491d5a7f3edb84c. It also applies to master at 
2851f5a3057.

{{METRICS_MAP}} becomes a {{ConcurrentHashMap}}. Its {{forEach}} is weakly 
consistent and never throws, and concurrent inserts from several supervisors 
are safe. The map stays static and nothing else changes, so the published 
metric names are the same as before. No consumer depends on gauge order.

Covered by {{testMetricsSnapshotWhileFirstTaskOfNewTypeIsAdded}} in the 
existing {{TestReplicationSupervisor}}, which adds a task of a new type from 
inside a snapshot and checks that the snapshot completes with its 
{{numInflight}} gauge and that the next one publishes the new type. Without the 
change it fails with {{ConcurrentModificationException}} at 
{{ReplicationSupervisorMetrics.getMetrics}}. With it 
{{TestReplicationSupervisor}} and the replicate, reconstruct and reconcile 
command handler tests (62 tests) pass and checkstyle is clean. With the patch 
the reproduction fails, because the snapshot no longer throws.

Found by code review of the datanode ReplicationSupervisor thread pool resize 
and metrics paths, as part of the TLA+ verification effort under HDDS-15926, on 
commit 07512078fe639f387acd52104491d5a7f3edb84c. Checked against HDDS issues 
and apache/ozone pull requests for duplicates before filing (HDDS-6314 fixed 
the same pattern in {{SCMContainerMetrics}}). The attached patch is a proposal 
for review. Generated with Specula (Claude Opus 5.5).



--
This message was sent by Atlassian Jira
(v10.3.26#10030026)

---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]

Reply via email to