hive-0003

This commit is contained in:
russell@unturf.com 2026-03-29 22:19:47 -04:00
parent 7e7cd2945a
commit 0fb709cb72
11 changed files with 1163 additions and 6 deletions

View file

@ -0,0 +1,93 @@
# trino-0002: SkewedPartitionRebalancer scaledPartitions ArrayList.contains O(P²) per rebalance cycle
## Classification
- **Severity**: MEDIUM
- **CWE**: CWE-407 (Algorithmic Complexity — Inefficient Algorithmic Complexity)
- **Component**: `core/trino-main/src/main/java/io/trino/operator/output/SkewedPartitionRebalancer.java`
- **Method**: `rebalanceBasedOnTaskBucketSkewness()`
## Defect
`rebalanceBasedOnTaskBucketSkewness()` maintains `scaledPartitions` as `ArrayList<Integer>` to
track which partitions were already rebalanced in the current cycle. For each candidate partition
polled from a priority queue, it calls `scaledPartitions.contains(maxPartition)` — an O(P) linear
scan — where P is the number of scaled partitions so far.
The nested loop structure makes the total cost O(B × P²) per rebalance call, where B = number of
task buckets and P = number of partitions rebalanced this cycle. For wide tables or large fan-out
writes (P in the hundreds to thousands), this is quadratic per rebalance invocation.
`rebalance()` is called from `PartitionedOutputOperator.addInput()` whenever the output buffer is
full — i.e., continuously during a large-scale skewed write. This is a hot path in distributed
query execution.
### Defective code (lines 349, 376, 384)
```java
// line 349
List<Integer> scaledPartitions = new ArrayList<>();
while (true) {
TaskBucket maxTaskBucket = maxTaskBuckets.poll();
...
while (true) {
Integer maxPartition = maxPartitions.poll();
...
// line 376 — O(P) per iteration
if (scaledPartitions.contains(maxPartition)) {
continue;
}
...
scaledPartitions.add(maxPartition); // line 384
}
}
```
## Fix
Change `scaledPartitions` to `Set<Integer>` (use `new HashSet<>()`). The set semantics are
identical — it tracks which partitions have been scaled — but `contains()` becomes O(1).
```java
// Fix: O(1) dedup
Set<Integer> scaledPartitions = new HashSet<>();
...
if (scaledPartitions.contains(maxPartition)) { // O(1)
continue;
}
...
scaledPartitions.add(maxPartition);
```
## Complexity
| Partitions rebalanced (P) | Before (per cycle) | After (per cycle) |
|---------------------------|--------------------|--------------------|
| 100 | ~10,000 ops | ~100 ops |
| 1000 | ~1,000,000 ops | ~1,000 ops |
| 10000 | ~100,000,000 ops | ~10,000 ops |
**Speedup**: 100×10,000× for large partition rebalance cycles.
## Patch
```diff
--- a/core/trino-main/src/main/java/io/trino/operator/output/SkewedPartitionRebalancer.java
+++ b/core/trino-main/src/main/java/io/trino/operator/output/SkewedPartitionRebalancer.java
@@ -1,2 +1,3 @@
+import java.util.HashSet;
+import java.util.Set;
@GuardedBy("this")
private void rebalanceBasedOnTaskBucketSkewness(...)
{
- List<Integer> scaledPartitions = new ArrayList<>();
+ Set<Integer> scaledPartitions = new HashSet<>();
while (true) {
...
while (true) {
Integer maxPartition = maxPartitions.poll();
...
if (scaledPartitions.contains(maxPartition)) { // now O(1)
continue;
}
```

View file

@ -0,0 +1,124 @@
import java.util.*;
/**
* Unit test for trino-0002: SkewedPartitionRebalancer scaledPartitions ArrayList.contains O(P²)
*
* Simulates the defective and fixed versions of the scaledPartitions dedup check.
*/
public class TestSkewedPartitionRebalancer {
// --- Defective: ArrayList.contains per partition ---
static int simulateDefective(int numPartitions, int numTaskBuckets) {
int totalCost = 0;
List<Integer> scaledPartitions = new ArrayList<>();
for (int bucket = 0; bucket < numTaskBuckets; bucket++) {
for (int partition = 0; partition < numPartitions; partition++) {
totalCost += scaledPartitions.size(); // cost of ArrayList.contains
if (scaledPartitions.contains(partition)) {
continue;
}
scaledPartitions.add(partition);
}
}
return totalCost;
}
// --- Fixed: HashSet.contains per partition ---
static int simulateFixed(int numPartitions, int numTaskBuckets) {
int totalCost = 0;
Set<Integer> scaledPartitions = new HashSet<>();
for (int bucket = 0; bucket < numTaskBuckets; bucket++) {
for (int partition = 0; partition < numPartitions; partition++) {
totalCost += 1; // O(1) HashSet.contains
if (scaledPartitions.contains(partition)) {
continue;
}
scaledPartitions.add(partition);
}
}
return totalCost;
}
public static void main(String[] args) {
int pass = 0, fail = 0;
// Test 1: correctness - same partitions get deduplicated
{
int P = 20, B = 3;
List<Integer> seenDef = new ArrayList<>();
Set<Integer> seenFix = new HashSet<>();
for (int b = 0; b < B; b++) {
for (int p = 0; p < P; p++) {
if (!seenDef.contains(p)) seenDef.add(p);
seenFix.add(p);
}
}
boolean ok = (seenDef.size() == seenFix.size()) &&
new HashSet<>(seenDef).equals(seenFix);
System.out.printf("[%s] Correctness P=%d B=%d: defective=%d unique, fixed=%d unique%n",
ok ? "PASS" : "FAIL", P, B, seenDef.size(), seenFix.size());
if (ok) pass++; else fail++;
}
// Test 2: cost at P=100, B=5
{
int P = 100, B = 5;
int defCost = simulateDefective(P, B);
int fixCost = simulateFixed(P, B);
double ratio = (double) defCost / fixCost;
boolean ok = ratio >= 30.0; // expect significant quadratic vs linear gap
System.out.printf("[%s] Cost ratio P=%d B=%d: defective=%d, fixed=%d, ratio=%.1fx%n",
ok ? "PASS" : "FAIL", P, B, defCost, fixCost, ratio);
if (ok) pass++; else fail++;
}
// Test 3: cost at P=1000, B=10
{
int P = 1000, B = 10;
int defCost = simulateDefective(P, B);
int fixCost = simulateFixed(P, B);
double ratio = (double) defCost / fixCost;
boolean ok = ratio >= 200.0;
System.out.printf("[%s] Cost ratio P=%d B=%d: defective=%d, fixed=%d, ratio=%.1fx%n",
ok ? "PASS" : "FAIL", P, B, defCost, fixCost, ratio);
if (ok) pass++; else fail++;
}
// Test 4: empty partitions case
{
int P = 0, B = 5;
int defCost = simulateDefective(P, B);
int fixCost = simulateFixed(P, B);
boolean ok = (defCost == 0 && fixCost == 0);
System.out.printf("[%s] Empty partitions P=%d B=%d: defective=%d, fixed=%d%n",
ok ? "PASS" : "FAIL", P, B, defCost, fixCost);
if (ok) pass++; else fail++;
}
// Test 5: single bucket - verify O(P²) vs O(P) scaling
{
int[] sizes = {10, 50, 100, 200};
boolean allOk = true;
for (int P : sizes) {
int defCost = simulateDefective(P, 1);
int fixCost = simulateFixed(P, 1);
double ratio = (double) defCost / fixCost;
System.out.printf(" P=%d: defective=%d, fixed=%d, ratio=%.1fx%n", P, defCost, fixCost, ratio);
// ratio should grow roughly proportional to P
}
// Check that ratio at P=200 is >> ratio at P=10 (demonstrates quadratic growth)
double ratioSmall = (double) simulateDefective(10, 1) / simulateFixed(10, 1);
double ratioLarge = (double) simulateDefective(200, 1) / simulateFixed(200, 1);
allOk = ratioLarge > ratioSmall * 5; // quadratic: 200/10=20x more ratio
System.out.printf("[%s] Quadratic scaling check: small ratio=%.1fx, large ratio=%.1fx%n",
allOk ? "PASS" : "FAIL", ratioSmall, ratioLarge);
if (allOk) pass++; else fail++;
}
System.out.printf("%nResult: %d PASS, %d FAIL%n", pass, fail);
System.exit(fail > 0 ? 1 : 0);
}
}