java-topology/defects/solr/patch/solr-001-cluster-status-live-nodes-list-contains.md

2.5 KiB
Raw Blame History

UNDF: UNDF-2026-000000533

solr-001: ClusterStatus O(n²) liveNodes.contains in crossCheckReplicaStateWithLiveNodes

Classification

  • CWE: CWE-407 (Inefficient Algorithmic Complexity)
  • Severity: HIGH
  • Path: Admin API handler — called on every CLUSTERSTATUS request, which monitoring tools typically poll every few seconds

Location

solr/core/src/java/org/apache/solr/handler/admin/ClusterStatus.java:303

Defect

protected void crossCheckReplicaStateWithLiveNodes(
        List<String> liveNodes, Map<String, Object> collectionProps) {
    var shards = (Map<String, Object>) collectionProps.get("shards");
    for (Object nextShard : shards.values()) {                      // O(shards)
        var replicas = (Map<String, Object>) shardMap.get("replicas");
        for (Object nextReplica : replicas.values()) {              // O(replicas/shard)
            // ...
            String node_name = (String) replicaMap.get(ZkStateReader.NODE_NAME_PROP);
            if (!liveNodes.contains(node_name)) {                   // O(liveNodes)
                replicaMap.put(ZkStateReader.STATE_PROP, Replica.State.DOWN.toString());
            }
        }
    }
}

liveNodes is fetched via zkStateReader.getZkClient().getChildren(ZkStateReader.LIVE_NODES_ZKNODE, null) which returns a plain List<String>. The double-nested shard/replica loop calls liveNodes.contains() for every replica.

For a cluster with:

  • N live nodes
  • S shards
  • R replicas/shard

Total comparisons per CLUSTERSTATUS call: O(N × S × R)

A production Solr cluster with 100 nodes, 500 shards, 3 replicas/shard = 150,000 string comparisons per call. With monitoring polling at 5s intervals: 1.8 million string comparisons per minute, completely wasted.

Fix

Convert liveNodes to a HashSet<String> before the nested loops:

Set<String> liveNodeSet = new HashSet<>(liveNodes);   // O(N) once
for (Object nextShard : shards.values()) {
    for (Object nextReplica : replicas.values()) {
        if (!liveNodeSet.contains(node_name)) {        // O(1)
            ...
        }
    }
}

The caller at line 129 already has liveNodes as a List; the fix can be applied inside crossCheckReplicaStateWithLiveNodes without changing any callers.

Complexity

Metric Before After
contains() O(N) O(1)
Full cross-check O(N×S×R) O(N + S×R)
At N=100, S=500, R=3 150,000 comparisons ~1,600
Speedup 1× ~94×

Affected Versions

Present in all Solr versions with the ClusterStatus handler (Solr 5+).