java-topology/defects/vertx/patch/vertx-0001-ha-manager-nodes-list.md

77 lines
2.7 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# UNDF: UNDF-2026-000000570
# vertx-0001 — HAManager nodeLeft() nodes List.contains O(C²) in cluster failover scan
**Classification:** CWE-407 Algorithmic Complexity — Inefficient Membership Test
**Severity:** MEDIUM
**Component:** vert.x core — `io.vertx.core.impl.HAManager`
**File:** `vertx-core/src/main/java/io/vertx/core/impl/HAManager.java`
**Line:** 307317
## Description
When a cluster node leaves, `HAManager.nodeLeft()` scans the entire `clusterMap` to find any
in-progress failovers that were themselves abandoned. For each entry in `clusterMap` it calls
`nodes.contains(entry.getKey())` where `nodes` is a `List<String>` returned by
`clusterManager.getNodes()`. The `List.contains()` is an O(N) linear scan, making the entire
loop O(C × N) = O(N²) in cluster size.
## Defective code
```java
// HAManager.java:307-317
List<String> nodes = clusterManager.getNodes();
for (Map.Entry<String, String> entry: clusterMap.entrySet()) {
if (!leftNodeID.equals(entry.getKey()) && !nodes.contains(entry.getKey())) {
JsonObject haInfo = new JsonObject(entry.getValue());
checkFailover(entry.getKey(), haInfo);
}
}
```
`nodes` is declared as `List<String> getNodes()` in `ClusteredNode.java:47`. The `clusterMap` has
one entry per cluster node. Both collections grow linearly with cluster size, so the inner
`nodes.contains()` is called C times, each taking O(N) time → O(N²) total.
## Fix
Convert `nodes` to a `HashSet<String>` before the loop:
```java
List<String> nodesList = clusterManager.getNodes();
Set<String> nodesSet = new HashSet<>(nodesList); // O(N) one-time build
for (Map.Entry<String, String> entry: clusterMap.entrySet()) {
if (!leftNodeID.equals(entry.getKey()) && !nodesSet.contains(entry.getKey())) {
JsonObject haInfo = new JsonObject(entry.getValue());
checkFailover(entry.getKey(), haInfo);
}
}
```
The same fix applies to the second call site at line 319:
```java
// before:
if (clusterManager.getNodes().contains(nodeID) && ...
// after: use the already-constructed nodesSet
if (nodesSet.contains(nodeID) && ...
```
## Complexity
| | Before | After |
|---|---|---|
| `nodes.contains()` per call | O(N) | O(1) |
| Full `nodeLeft()` scan | O(N²) | O(N) |
| HashSet construction | — | O(N) one-time |
## Speedup estimate
At N=500 cluster nodes: ~500× operation-count reduction in the membership scan.
Typically triggered once per node departure event, so the absolute time is small in small
clusters. In large clusters (1001000 nodes, common in cloud deployments) and high churn rates
this becomes a significant stall on the event-loop thread.
## Affected versions
All versions through current HEAD (2026-03-28).