hive-0003
This commit is contained in:
parent
7e7cd2945a
commit
0fb709cb72
11 changed files with 1163 additions and 6 deletions
|
|
@ -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;
|
||||
}
|
||||
```
|
||||
124
defects/trino/unit/TestSkewedPartitionRebalancer.java
Normal file
124
defects/trino/unit/TestSkewedPartitionRebalancer.java
Normal 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);
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue