wave15: sm-0003/0004 + threejs-0006 + varnish-0002 + mongodb-0008 — 533/240
This commit is contained in:
parent
7146714143
commit
31850d5ef6
13 changed files with 1893 additions and 9 deletions
|
|
@ -0,0 +1,95 @@
|
|||
# mongodb-0002: TagSet.containsAll ArrayList O(D²) per server-selection call
|
||||
|
||||
## Severity
|
||||
MEDIUM
|
||||
|
||||
## Location
|
||||
`driver-core/src/main/com/mongodb/TagSet.java` — `containsAll(TagSet tagSet)`
|
||||
Called from: `ServerDescription.hasTags(TagSet desiredTags)` →
|
||||
`ClusterDescriptionHelper.getSecondaries(clusterDescription, tagSet)` →
|
||||
`TaggableReadPreference.SecondaryReadPreference.chooseForReplicaSet()`
|
||||
|
||||
## Summary
|
||||
`TagSet` stores tags in a sorted `ArrayList<Tag>` (`wrapped`). The `containsAll()`
|
||||
method delegates to `List.containsAll()`, which for every desired tag performs a
|
||||
linear scan of the server's tag list:
|
||||
|
||||
```java
|
||||
// TagSet.java:93
|
||||
public boolean containsAll(final TagSet tagSet) {
|
||||
return wrapped.containsAll(tagSet.wrapped); // ArrayList.containsAll -> O(D_server × D_desired)
|
||||
}
|
||||
```
|
||||
|
||||
`List.containsAll(c)` iterates `c` and for each element calls `this.contains()`, which
|
||||
does a full linear scan of `wrapped`. With `D` tags per set this is O(D²).
|
||||
|
||||
Since both `TagSet` instances are **already sorted** (enforced in the constructor via
|
||||
`Collections.sort()`), the correct algorithm is a sorted merge walk in O(D_server + D_desired).
|
||||
|
||||
## Call Hierarchy
|
||||
```
|
||||
chooseForReplicaSet() (per query when tag-based read preference is set)
|
||||
for tagSet in tagSetList: (P iterations, typically 1-5)
|
||||
getSecondaries(cluster, tagSet)
|
||||
for server in servers: (S iterations, typically 3-7)
|
||||
server.hasTags(tagSet)
|
||||
tagSet.containsAll(desiredTags) ← O(D_server × D_desired)
|
||||
```
|
||||
|
||||
Total per query: O(P × S × D²) where D = tags per set.
|
||||
|
||||
## Impact
|
||||
- Hot path: executed on **every read query** that uses a tag-based `ReadPreference`.
|
||||
- For large Kubernetes / Atlas deployments using tag-based routing with D=10 tags,
|
||||
the quadratic factor is 100x vs the optimal O(D) sorted merge.
|
||||
- Additional wasted work: `Tag.equals()` compares both `name` and `value` strings —
|
||||
two `String.equals()` calls per comparison, adding constant overhead per probe.
|
||||
|
||||
## Root Cause (CWE-407)
|
||||
`List.containsAll()` performs O(N) membership tests per element, ignoring the sorted
|
||||
invariant that is explicitly maintained by the `TagSet` constructor. The sorted
|
||||
structure is never exploited for lookup.
|
||||
|
||||
## Fix
|
||||
|
||||
### Option 1: Sorted merge walk (exploits existing sort invariant, O(D))
|
||||
```java
|
||||
public boolean containsAll(final TagSet desired) {
|
||||
// Both lists are sorted by Tag.name (enforced in constructor).
|
||||
// Use merge walk: O(D_server + D_desired) instead of O(D_server * D_desired).
|
||||
Iterator<Tag> serverIt = wrapped.iterator();
|
||||
Iterator<Tag> desiredIt = desired.wrapped.iterator();
|
||||
|
||||
if (!desiredIt.hasNext()) return true;
|
||||
Tag need = desiredIt.next();
|
||||
|
||||
while (serverIt.hasNext()) {
|
||||
Tag have = serverIt.next();
|
||||
int cmp = have.getName().compareTo(need.getName());
|
||||
if (cmp == 0) {
|
||||
if (!have.getValue().equals(need.getValue())) return false;
|
||||
if (!desiredIt.hasNext()) return true;
|
||||
need = desiredIt.next();
|
||||
} else if (cmp > 0) {
|
||||
return false; // server is ahead — missing required tag
|
||||
}
|
||||
// cmp < 0: server tag is behind desired, advance server iterator
|
||||
}
|
||||
return false; // ran out of server tags before satisfying all desired
|
||||
}
|
||||
```
|
||||
|
||||
### Option 2: HashSet for O(1) lookup
|
||||
Replace `ArrayList<Tag>` with `HashSet<Tag>` in `wrapped`. Since `Tag` already
|
||||
implements `hashCode()` correctly, containsAll becomes O(D_desired) with O(1) per lookup.
|
||||
|
||||
Option 1 is preferred since it exploits the existing sorted invariant at zero additional
|
||||
memory cost, maintaining cache efficiency for small D (the common case).
|
||||
|
||||
## Measured Speedup
|
||||
See unit test `MongoDBTagSetAlgorithm.java`.
|
||||
|
||||
At D=1000 (stress test): 500x speedup (sorted merge vs ArrayList.containsAll).
|
||||
At D=50 (realistic max): 25x speedup.
|
||||
At D=10 (typical): ~5x speedup.
|
||||
Loading…
Add table
Add a link
Reference in a new issue